fix(usage): unique filter option values so same-named entries don't co-select

This commit is contained in:
jialin
2026-07-09 20:36:52 +08:00
committed by jialin
parent 0d5812ba88
commit 4f4b527014
3 changed files with 21 additions and 25 deletions
+5 -3
View File
@@ -137,12 +137,14 @@ export function groupToOptions<T, TChild>(
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<TChild>[] {
const groupedMap = new Map<string, GroupOption<TChild>>();
data.forEach((item) => {
data.forEach((item, index) => {
const groupKey = options.getGroupKey(item);
const groupType = options.getGroupType(item);
@@ -156,7 +158,7 @@ export function groupToOptions<T, TChild>(
});
}
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);
});
-4
View File
@@ -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[];
+16 -18
View File
@@ -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<UsageMeta>({
@@ -21,12 +31,10 @@ export default function useQueryUsageMetaData() {
});
const { initialState } = useModel('@@initialState');
const [result, setResult] = useState<{
models: GroupOption<UsageFilterItem>[];
users: UserOptionType[];
api_keys: GroupOption<UsageFilterItem>[];
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);