From 4f4b527014fea5682e9fa82691cbc0acf21ad9a5 Mon Sep 17 00:00:00 2001 From: jialin Date: Thu, 9 Jul 2026 20:00:40 +0800 Subject: [PATCH] fix(usage): unique filter option values so same-named entries don't co-select --- src/pages/usage/config/index.ts | 8 +++-- src/pages/usage/config/types.ts | 4 --- .../usage/services/use-query-meta-data.ts | 34 +++++++++---------- 3 files changed, 21 insertions(+), 25 deletions(-) diff --git a/src/pages/usage/config/index.ts b/src/pages/usage/config/index.ts index c5571bac..88ac463e 100644 --- a/src/pages/usage/config/index.ts +++ b/src/pages/usage/config/index.ts @@ -137,12 +137,14 @@ export function groupToOptions( options: { getGroupKey: (item: T) => string; getGroupType: (item: T) => string; - getChild: (item: T) => TChild; + // ``index`` is a stable per-row token so ``getChild`` can mint a unique + // option value for id-less (deleted) entries that share a display name. + getChild: (item: T, index: number) => TChild; } ): GroupOption[] { const groupedMap = new Map>(); - data.forEach((item) => { + data.forEach((item, index) => { const groupKey = options.getGroupKey(item); const groupType = options.getGroupType(item); @@ -156,7 +158,7 @@ export function groupToOptions( }); } - const child = options.getChild(item) as TChild & { value: string }; + const child = options.getChild(item, index) as TChild & { value: string }; groupedMap.get(groupKey)!.children.push(child); }); diff --git a/src/pages/usage/config/types.ts b/src/pages/usage/config/types.ts index 75a2c01e..0c6a83ed 100644 --- a/src/pages/usage/config/types.ts +++ b/src/pages/usage/config/types.ts @@ -2,7 +2,6 @@ export interface UsageFilterItem { identity: { value: { cluster_name: string; - model_name: string | null; user_name: string; api_key_name: string | null; access_key: string | null; @@ -12,7 +11,6 @@ export interface UsageFilterItem { route_name: string | null; }; current: { - model_id: string | null; user_id: number | null; api_key_id: string | null; route_id: number | null; @@ -49,7 +47,6 @@ export interface TimeSeriesData { export type BreakdownItem = { cluster_name: string; - model_name: string; user_name: string; api_key_name: string; input_tokens: number; @@ -88,7 +85,6 @@ export interface UsageBreakdownResponse { export interface UsageMeta { filters: { - models: UsageFilterItem[]; users: UsageFilterItem[]; api_keys: UsageFilterItem[]; routes: UsageFilterItem[]; diff --git a/src/pages/usage/services/use-query-meta-data.ts b/src/pages/usage/services/use-query-meta-data.ts index c42c229c..ebe3612a 100644 --- a/src/pages/usage/services/use-query-meta-data.ts +++ b/src/pages/usage/services/use-query-meta-data.ts @@ -13,6 +13,16 @@ type RouteOptionType = UsageFilterItem & { value: string; }; +// The multi-select keys options by ``value``; two entries sharing a display +// name (e.g. an active model and a deleted one with the same name) must get +// DIFFERENT values or selecting one selects the other. Prefer the real id; +// id-less (deleted) entries fall back to a per-row token so they stay +// independently selectable. +const optionValue = ( + id: string | number | null | undefined, + index: number +): string => (id != null ? `id:${id}` : `row:${index}`); + export default function useQueryUsageMetaData() { const { detailData, loading, cancelRequest, fetchData } = useQueryData({ @@ -21,12 +31,10 @@ export default function useQueryUsageMetaData() { }); const { initialState } = useModel('@@initialState'); const [result, setResult] = useState<{ - models: GroupOption[]; users: UserOptionType[]; api_keys: GroupOption[]; routes: RouteOptionType[]; }>({ - models: [], users: [], api_keys: [], routes: [] @@ -50,19 +58,9 @@ export default function useQueryUsageMetaData() { const res = await fetchData({}); const sortedUsers = sortUsers(res?.filters?.users || []); const data = { - models: groupToOptions(res?.filters?.models || [], { - getGroupKey: (item) => item.identity.value.provider_name || 'gpustack', - getGroupType: (item) => - item.identity.value.provider_type || 'deployments', - getChild: (item) => ({ - ...item, - value: item.label, - label: item.identity.value.model_name || '' - }) - }), users: - sortedUsers.map((item) => ({ - value: item.label, + sortedUsers.map((item, index) => ({ + value: optionValue(item.identity.current?.user_id, index), isCurrent: item.identity.current?.user_id === initialState?.currentUser?.id, ...item @@ -71,16 +69,16 @@ export default function useQueryUsageMetaData() { getGroupKey: (item) => item.identity.value.user_name || 'unknown_user', getGroupType: (item) => item.identity.value.api_key_is_custom ? 'custom' : 'default', - getChild: (item) => ({ + getChild: (item, index) => ({ ...item, - value: item.label, + value: optionValue(item.identity.current?.api_key_id, index), label: item.identity.value.api_key_name || '' }) }), routes: - (res?.filters?.routes || []).map((item) => ({ + (res?.filters?.routes || []).map((item, index) => ({ ...item, - value: item.label + value: optionValue(item.identity.current?.route_id, index) })) || [] }; setResult(data);