diff --git a/internal/control/home.go b/internal/control/home.go index d95e7b0b..74acec3a 100644 --- a/internal/control/home.go +++ b/internal/control/home.go @@ -3,6 +3,7 @@ package control import ( "context" "fmt" + "sort" "strings" "time" @@ -30,6 +31,7 @@ type HomeAccessKey struct { Name string `json:"name"` MaskedKey string `json:"masked_key"` Protocols []protocol.Protocol `json:"protocols"` + Models []string `json:"models"` } type HomeBase struct { @@ -179,7 +181,7 @@ func (s *Service) readHomeBase( return HomeBase{}, app_errors.ErrUnauthorized } } - accessKeys, err := mapHomeAccessKeys(accessKeyRows) + accessKeys, err := mapHomeAccessKeys(accessKeyRows, snapshot) if err != nil { return HomeBase{}, err } @@ -338,6 +340,14 @@ func countScopedHomeModels( allowedGroups map[uint]struct{}, accessKey state.AccessKeyView, ) int64 { + return int64(len(scopedHomeModelNames(snapshot, allowedGroups, accessKey))) +} + +func scopedHomeModelNames( + snapshot *state.ConfigSnapshot, + allowedGroups map[uint]struct{}, + accessKey state.AccessKeyView, +) []string { names := make(map[string]struct{}) for groupID := range allowedGroups { group, exists := snapshot.Groups[groupID] @@ -357,7 +367,12 @@ func countScopedHomeModels( names[name] = struct{}{} } } - return int64(len(names)) + result := make([]string, 0, len(names)) + for name := range names { + result = append(result, name) + } + sort.Strings(result) + return result } func homeExternalModelName(model state.ModelConfig) string { @@ -486,7 +501,10 @@ func countAvailableHomeCredentialsInGroups( return available, nil } -func mapHomeAccessKeys(rows []homeAccessKeyRow) ([]HomeAccessKey, error) { +func mapHomeAccessKeys( + rows []homeAccessKeyRow, + snapshot *state.ConfigSnapshot, +) ([]HomeAccessKey, error) { result := make([]HomeAccessKey, 0, len(rows)) for _, row := range rows { if uint64(row.ID) > uint64(maxSafeInteger) { @@ -517,10 +535,24 @@ func mapHomeAccessKeys(rows []homeAccessKeyRow) ([]HomeAccessKey, error) { } else { protocols = append([]protocol.Protocol(nil), protocols...) } + accessKey, exists := snapshot.AccessKeysByID[row.ID] + if !exists || accessKey.Status != state.AccessKeyStatusActive { + return nil, fmt.Errorf( + "map home access key %d: runtime configuration mismatch: %w", + row.ID, + app_errors.ErrInternalServer, + ) + } + models := scopedHomeModelNames( + snapshot, + accessibleHomeGroups(snapshot, accessKey), + accessKey, + ) result = append(result, HomeAccessKey{ ID: row.ID, Name: row.Name, MaskedKey: maskedAccessKey(row.KeyPrefix, row.KeySuffix), Protocols: protocols, + Models: models, }) } return result, nil diff --git a/internal/control/home_test.go b/internal/control/home_test.go index 3cc3b414..ef53fcf5 100644 --- a/internal/control/home_test.go +++ b/internal/control/home_test.go @@ -161,11 +161,17 @@ func TestReadHomeBaseUsesPersistedAndRuntimeSnapshots(t *testing.T) { protocol.OpenAICompletions, protocol.Gemini, }, + Models: []string{ + "client-primary", "client-secondary", "client-third", "ignored-empty", + }, }, { ID: 9, Name: "all protocols", MaskedKey: "sk-gl-****c0de", Protocols: protocol.DataPlaneProtocols(), + Models: []string{ + "client-primary", "client-secondary", "client-third", "ignored-empty", + }, }, }, } @@ -236,6 +242,7 @@ func TestReadAccessKeyHomeBaseScopesInventoryToRoutableModels(t *testing.T) { } if result.Inventory != (HomeInventory{GroupCount: 1, ModelCount: 1}) || len(result.AccessKeys) != 1 || result.AccessKeys[0].ID != created.ID || + !reflect.DeepEqual(result.AccessKeys[0].Models, []string{"client-allowed"}) || result.CurrentAccessKey == nil || result.CurrentAccessKey.ID != created.ID { t.Fatalf("ReadAccessKeyHomeBase() = %#v", result) } diff --git a/web/package.json b/web/package.json index 36ffd13a..0c98877f 100644 --- a/web/package.json +++ b/web/package.json @@ -17,6 +17,7 @@ "type-check": "vue-tsc --noEmit -p tsconfig.app.json && tsc --noEmit -p tsconfig.node.json" }, "dependencies": { + "@internationalized/date": "3.12.3", "@lucide/vue": "1.39.0", "@tanstack/vue-query": "5.102.8", "reka-ui": "2.10.4", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index 60969640..d9dfe93c 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -14,6 +14,9 @@ importers: .: dependencies: + '@internationalized/date': + specifier: 3.12.3 + version: 3.12.3 '@lucide/vue': specifier: 1.39.0 version: 1.39.0(vue@3.5.42(typescript@6.0.3)) diff --git a/web/src/app/resources/home.ts b/web/src/app/resources/home.ts index de08b747..ad2ca25f 100644 --- a/web/src/app/resources/home.ts +++ b/web/src/app/resources/home.ts @@ -45,6 +45,7 @@ export interface HomeBaseDto { name: string masked_key: string protocols: AccessProtocol[] + models: string[] }> current_access_key: AccessKeyCollectionItemDto | null } @@ -145,7 +146,7 @@ const inventoryFields = [ 'available_credential_count', 'model_count', ] as const -const accessKeyFields = ['id', 'name', 'masked_key', 'protocols'] as const +const accessKeyFields = ['id', 'name', 'masked_key', 'protocols', 'models'] as const const statisticsFields = [ 'range', 'granularity', @@ -236,12 +237,15 @@ function projectHomeAccessKey(value: unknown): HomeBaseDto['access_keys'][number const protocols = projectArray(record.protocols, (protocol) => projectEnum(protocol, knownAccessProtocols), ) + const models = projectArray(record.models, projectNonBlankTrimmedString) if (new Set(protocols).size !== protocols.length) invalidResponse() + if (new Set(models).size !== models.length) invalidResponse() return { id: projectSafeInteger(record.id, { minimum: 1 }), name: projectNonBlankTrimmedString(record.name), masked_key: maskedKey, protocols, + models, } } diff --git a/web/src/components/ui/AppCombobox.vue b/web/src/components/ui/AppCombobox.vue new file mode 100644 index 00000000..f1c046fc --- /dev/null +++ b/web/src/components/ui/AppCombobox.vue @@ -0,0 +1,307 @@ + + + + + diff --git a/web/src/components/ui/AppDateTimeRangePicker.vue b/web/src/components/ui/AppDateTimeRangePicker.vue index 0a027b13..60648a96 100644 --- a/web/src/components/ui/AppDateTimeRangePicker.vue +++ b/web/src/components/ui/AppDateTimeRangePicker.vue @@ -1,7 +1,25 @@