fix(webui): shared SearchInput/useSearchQuery, correct expand rationale

Address review of the user/group search filter (feat/user-search):

- Correct the "group name match does not auto-expand" rationale in
  UsageView.vue and lib/search-expand.ts. The prior comment claimed
  Overview has no provider-name search — false: OverviewView's
  providerMatches checks each model's full ROUTABLE id
  (`${provider}/${model}`, lib/format.ts's routableModelId), so a
  provider-name query does match and auto-expand there. The actual
  reason Usage withholds auto-expand from a name-only match: a group's
  name is already legible on its collapsed trigger row, so expanding
  every name match too would dump full member rosters for no reason;
  a member-only match has no such shortcut and still needs the reveal.
  Behavior is unchanged — only the (now accurate) justification.

- Extract components/SearchInput.vue and composables/useSearchQuery.ts.
  OverviewView's model search, UsageView's user/group search, and
  ChartsView's scope-picker search were three ~22-line copies of the
  same icon+Input+clear-button markup, plus three copies of the
  query/normalized/hasQuery/clear quartet. All three views now consume
  the shared component/composable. SearchInput's clear button is a real
  shadcn Button (variant="ghost", size="icon-xs" — 24x24px) instead of a
  hand-rolled <button>, fixing a WCAG 2.2 SC 2.5.8 hit-target/
  focus-visible gap once instead of three times.

- ChartsView's search input: w-48 (clipped the 22-char placeholder) to
  w-56, matching the sibling SelectTrigger's width.

- UsageView.vue's visibleMembers now calls the exported, tested
  matchingMembersOfGroup helper instead of re-implementing the same
  filter inline with userMatches.

- Reworded the "shared with ChartsView.vue" claims on membersOfGroup
  (usage-search.ts) and in UsageView.vue's visibleMembers doc comment:
  ChartsView never calls membersOfGroup directly, only indirectly via
  groupMatches -> matchingMembersOfGroup.

- UsageView's groups/users empty-state messages now follow Overview's
  aliasEmptyMessage three-way pattern ("none configured" vs "no
  groups/users match ..." vs "none"), distinguishing nothing-configured
  from nothing-matches instead of collapsing both to the same text.

Gate unchanged: npm run build + npm test + vue-tsc, make admin-ui
(separate regen commit), go test ./... -count=1, gofmt/vet, make
yaegi-check.
This commit is contained in:
2026-08-21 12:17:04 +01:00
parent 46537fa16a
commit 24809ed25a
7 changed files with 173 additions and 132 deletions
+6 -4
View File
@@ -34,10 +34,12 @@ export interface ExpandState {
* manuallyExpanded plus every id in matchingIds that is not in
* manuallyCollapsed (auto-expand every match, unless the user explicitly
* closed that one). The caller decides what "matching" means for its own
* view — OverviewView.vue passes every provider with a matching model;
* UsageView.vue passes every group that matches ONLY via a member, not via
* its own name (see that view's own doc comment for why a name match
* alone does not force-expand there).
* view, and this module has no opinion on it either way — OverviewView.vue
* passes every provider that itself matched at all (every match there
* implies at least one matching model, so every match auto-expands);
* UsageView.vue instead passes only the subset of its own matches that
* matched via a member rather than via the group's own name (see that
* view's own doc comment for why it draws that narrower distinction).
*/
export function computeExpandedItems(state: ExpandState, hasQuery: boolean, matchingIds: string[]): Set<string> {
if (!hasQuery) return new Set(state.manuallyExpanded)
+6 -3
View File
@@ -28,9 +28,12 @@ export function groupNameMatches(group: Pick<AdminUsageEntryView, 'id'>, query:
* `groupName === group.id` — a user's row already carries its own group
* name (admin.go's adminUsageEntryView.GroupName, set in buildAdminUsage),
* so this is a plain client-side join against the already-polled
* GET /admin/api/usage response, no extra fetch. Shared so UsageView.vue
* and ChartsView.vue never re-derive this join differently (it replaces
* UsageView.vue's own former local membersOf helper).
* GET /admin/api/usage response, no extra fetch. UsageView.vue calls this
* directly (replacing its own former local membersOf helper);
* matchingMembersOfGroup below builds on it too, which is how
* ChartsView.vue's groupMatches call ends up depending on this same join
* — indirectly, through matchingMembersOfGroup — without calling
* membersOfGroup itself.
*/
export function membersOfGroup(group: Pick<AdminUsageEntryView, 'id'>, users: AdminUsageEntryView[]): AdminUsageEntryView[] {
return users.filter((u) => u.groupName === group.id)