feat(webui): user and group search filter on usage and charts views

Add a "Filter users or groups" search input to UsageView, mirroring
OverviewView's model search (same Input component, icon, and
clear-button styling). Case-insensitive, trimmed substring match:

- A user matches when its own name contains the query.
- A group matches when its own name contains the query OR it has at
  least one matching member.

Groups accordion shows only matching groups, sorted through the
existing Groups toolbar unchanged. A group that matches only via a
member auto-expands (using the same tested override semantics as
Overview's provider search); a group matched by its own name is not
force-expanded, since the match is already visible on its collapsed
trigger row. The nested members table inside an expanded group shows
every member when the group matched by name, and only the matching
members when it matched via a member.

The flat Users table filters to matching users, plus every member of
a name-matched group. The Total card and ChartsView's chart rendering
are untouched by the filter.

ChartsView's scope picker (Select) gets the identical search input,
filtering the user/group option list it offers. "Total" always stays
in the list, and the currently selected scope is always kept in the
list even if a new query would otherwise hide it, so an active chart
is never torn down or re-labeled by narrowing the picker.

Generalize lib/provider-expand.ts into lib/search-expand.ts: the same
two-set (manuallyExpanded/manuallyCollapsed) auto-expand/override state
machine now drives both Overview's providers and Usage's groups, keyed
on generic string ids instead of provider names specifically.
OverviewView.vue is updated to the renamed
computeExpandedItems/toggleItemExpand exports; its own behavior is
unchanged. The moved spec (search-expand.spec.ts) keeps all 9 original
cases plus 2 new ones pinning the same semantics against non-provider
(group-shaped) ids.

Add lib/usage-search.ts with the shared user/group matching helpers
(userMatches, groupNameMatches, membersOfGroup,
matchingMembersOfGroup, groupMatches) so UsageView and ChartsView
share one matching implementation, with its own vitest spec.

UsageTable.vue gains an optional emptyMessage prop (threaded to
DataTable) so UsageView can show a "no users match ..."/"no groups
match ..." message under an active filter, matching Overview's own
alias-search empty-state pattern.

No backend/Go logic changes; filtering is client-side over the
already-polled GET /admin/api/usage response.
This commit is contained in:
2026-08-21 12:16:57 +01:00
parent dded3a56fe
commit d6b9879813
11 changed files with 534 additions and 149 deletions
+3
View File
@@ -19,6 +19,8 @@ const props = defineProps<{
entries: AdminUsageEntryView[]
secondaryColumnLabel: string
secondaryValue: (entry: AdminUsageEntryView) => string
/** Shown in the empty-state row when entries is empty — e.g. a filtered-search "no X match ..." message. Falls through to DataTable's own 'none' default when omitted. */
emptyMessage?: string
}>()
const columns = computed(() => usageColumns(props.idLabel, props.secondaryColumnLabel, props.secondaryValue))
@@ -28,6 +30,7 @@ const columns = computed(() => usageColumns(props.idLabel, props.secondaryColumn
<DataTable
:columns="columns"
:data="entries"
:empty-message="emptyMessage"
:row-class="(entry) => (entry.storeDown ? 'bg-destructive/10' : undefined)"
/>
</template>