fix: models, apikeys Cascader

This commit is contained in:
jialin
2026-04-22 13:16:48 +08:00
committed by jialin
parent 3a58bd1eef
commit 084822814d
8 changed files with 295 additions and 61 deletions
@@ -1,11 +1,13 @@
import { Tabs } from 'antd';
import React from 'react';
import { GroupOption } from '../config';
import { UsageFilterItem } from '../config/types';
import ApiKeysTable from '../tables/apikeys-table';
import ModelsTable from '../tables/models-table';
import UsersTable from '../tables/users-table';
type FilterOptionType = Omit<UsageFilterItem, 'label' | 'deleted'>;
type GroupOptionType = GroupOption<UsageFilterItem>;
const BreakdownTabs: React.FC<{
dateRange: {
+1 -1
View File
@@ -183,7 +183,7 @@ const DailyUsage: React.FC<DailyUsageProps> = (props) => {
chartData={chartData}
seriesData={[]}
xAxisData={xAxisData}
height={360}
height={280}
smooth={false}
legendData={legendData}
labelFormatter={labelFormatter}
+183 -40
View File
@@ -1,14 +1,19 @@
import AutoTooltip from '@/components/auto-tooltip';
import SealCascader from '@/components/seal-form/seal-cascader';
import SimpleSelect from '@/components/seal-form/simple-select';
import useRangePickerPreset from '@/pages/dashboard/hooks/use-rangepicker-preset';
import ProviderLogo from '@/pages/maas-provider/components/provider-logo';
import { useModel } from '@@/plugin-model';
import { DownloadOutlined, SyncOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max';
import { Button, DatePicker, Dropdown, MenuProps } from 'antd';
import dayjs from 'dayjs';
import React from 'react';
import { GroupOption } from '../config';
import { UsageFilterItem } from '../config/types';
import FilterBarCss from '../styles/filter-bar.less';
type valueType = string | number | null;
const DefaultDateConfig = {
maxRange: 90,
defaultRange: 29
@@ -23,9 +28,9 @@ interface FilterBarProps {
selectedModels: string[];
selectedUsers: string[];
selectedApiKeys: string[];
modelOptions: OptionType[];
modelOptions: GroupOption<UsageFilterItem>[];
userOptions: OptionType[];
apiKeyOptions: OptionType[];
apiKeyOptions: GroupOption<UsageFilterItem>[];
onScopeChange: (value: string) => void;
onDateChange: (dates: any, dateStrings: [string, string]) => void;
onModelsChange: (value: string[]) => void;
@@ -88,6 +93,11 @@ const FilterBar: React.FC<FilterBarProps> = (props) => {
}
]
});
const [activeModels, setActiveModels] = React.useState<valueType[][]>([]);
const [activeApiKeys, setActiveApiKeys] = React.useState<valueType[][]>([]);
const [activeUserApiKeys, setActiveUserApiKeys] = React.useState<valueType[]>(
[]
);
const initialInfo = useModel('@@initialState');
const { initialState } = initialInfo || {};
@@ -104,17 +114,79 @@ const FilterBar: React.FC<FilterBarProps> = (props) => {
}
];
const handleOnModelsChange = (value: valueType[][], selectedOptions: any) => {
setActiveModels(value);
const selectedValues: string[] = value.map((item) => {
if (Array.isArray(item)) {
return item[item.length - 1] as string; // Get the last value in the array
}
return item as string;
});
onModelsChange(selectedValues);
};
const handleOnApiKeysChange = (
value: valueType[][],
selectedOptions: any
) => {
setActiveApiKeys(value);
const selectedValues: string[] = value.map((item) => {
if (Array.isArray(item)) {
return item[item.length - 1] as string; // Get the last value in the array
}
return item as string;
});
onApiKeysChange(selectedValues);
};
const displayRender = (labels: any[], option: any) => {
return (
<AutoTooltip
ghost
maxWidth={150}
title={
<span>
{labels[0]} / {labels[1]}
</span>
}
>
{labels[0]} / {labels[1]}
</AutoTooltip>
);
};
const optionRender = (option: any) => {
const { data } = option;
console.log('optionRender', option);
if (!data.isParent) {
return <AutoTooltip ghost>{data.label}</AutoTooltip>;
}
if (data.type === 'deployments') {
return (
<span className={FilterBarCss.optionsWrapper}>
<ProviderLogo provider={data.type as string} />
<AutoTooltip ghost>
{intl.formatMessage({ id: 'menu.models.deployment' })}
</AutoTooltip>
</span>
);
}
return (
<span className={FilterBarCss.optionsWrapper}>
<ProviderLogo provider={data.type as string} />
<AutoTooltip ghost>
<span>{data.label}</span>
</AutoTooltip>
</span>
);
};
return (
<div className={FilterBarCss.wrapper}>
<div className={FilterBarCss.filters}>
{/* {initialState?.currentUser?.is_admin && (
<BaseSelect
options={scopeOptions}
value={scope}
onChange={onScopeChange}
style={{ width: 150 }}
/>
)} */}
<DatePicker.RangePicker
maxDate={dayjs()}
defaultValue={[
@@ -128,46 +200,117 @@ const FilterBar: React.FC<FilterBarProps> = (props) => {
value={[dayjs(startDate), dayjs(endDate)]}
onChange={onDateChange}
/>
<SimpleSelect
allowClear
showSearch
mode="multiple"
options={modelOptions}
maxTagCount={0}
placeholder="Model"
styles={{
wrapper: { flex: 1, maxWidth: 300, minWidth: 150 }
<div
style={{
maxWidth: 400,
flex: 1,
minWidth: 200
}}
value={selectedModels}
onChange={onModelsChange}
/>
>
<SealCascader
showSearch
multiple={true}
classNames={{
popup: {
root: 'cascader-popup-wrapper gpu-selector'
}
}}
styles={{
root: {
width: '100%'
},
popup: {
list: {
flex: 1
},
listItem: {
padding: '5px 10px'
}
}
}}
maxTagCount={1}
size="small"
placeholder={intl.formatMessage({
id: 'dashboard.usage.selectmodel'
})}
options={modelOptions}
showCheckedStrategy="SHOW_CHILD"
displayRender={displayRender}
optionNode={optionRender}
value={activeModels}
onChange={handleOnModelsChange}
getPopupContainer={(triggerNode) => triggerNode.parentNode}
></SealCascader>
</div>
{initialState?.currentUser?.is_admin && (
<>
<SimpleSelect
allowClear
showSearch
mode="multiple"
options={userOptions}
placeholder="User"
styles={{
wrapper: { flex: 1, maxWidth: 240, minWidth: 150 }
}}
value={selectedUsers}
onChange={onUsersChange}
/>
<div
style={{
maxWidth: 400,
flex: 1,
minWidth: 200
}}
>
<SealCascader
showSearch
multiple={true}
classNames={{
popup: {
root: 'cascader-popup-wrapper gpu-selector'
}
}}
styles={{
root: {
width: '100%'
},
popup: {
list: {
flex: 1
},
listItem: {
padding: '5px 10px'
}
}
}}
maxTagCount={1}
size="small"
placeholder="API Key"
options={apiKeyOptions}
showCheckedStrategy="SHOW_CHILD"
value={activeApiKeys}
onChange={handleOnApiKeysChange}
getPopupContainer={(triggerNode) => triggerNode.parentNode}
></SealCascader>
</div>
</>
)}
{!initialState?.currentUser?.is_admin && (
<SimpleSelect
allowClear
showSearch
mode="multiple"
options={userOptions}
placeholder="User"
options={apiKeyOptions?.[0]?.children || []}
maxTagCount={0}
placeholder="API Key"
styles={{
wrapper: { flex: 1, maxWidth: 240, minWidth: 150 }
wrapper: { flex: 1, maxWidth: 240, minWidth: 100 }
}}
value={selectedUsers}
onChange={onUsersChange}
value={selectedApiKeys}
onChange={onApiKeysChange}
/>
)}
<SimpleSelect
allowClear
showSearch
mode="multiple"
options={apiKeyOptions}
maxTagCount={0}
placeholder="API Key"
styles={{
wrapper: { flex: 1, maxWidth: 240, minWidth: 100 }
}}
value={selectedApiKeys}
onChange={onApiKeysChange}
/>
<Button
type="text"
style={{ color: 'var(--ant-color-text-tertiary)' }}
+62
View File
@@ -1,3 +1,5 @@
import { UsageFilterItem } from './types';
export const groupByOptions = [
{
value: '',
@@ -106,3 +108,63 @@ export const generateColumns = (data: SourceItem[]) => {
}))
];
};
interface DataItem {
identity: {
value: {
provider_name: string | null;
provider_type: string | null;
model_name: string;
};
};
label: string;
}
interface GroupedOption {
value: string;
label: string;
providerType: string;
children: {
label: string;
value: string;
identity: UsageFilterItem['identity'];
}[];
}
export interface GroupOption<TChild> {
value: string | number | null;
label: string;
type: string;
isParent: boolean;
children: (TChild & { value: string })[];
}
export function groupToOptions<T, TChild>(
data: T[],
options: {
getGroupKey: (item: T) => string;
getGroupType: (item: T) => string;
getChild: (item: T) => TChild;
}
): GroupOption<TChild>[] {
const groupedMap = new Map<string, GroupOption<TChild>>();
data.forEach((item) => {
const groupKey = options.getGroupKey(item);
const groupType = options.getGroupType(item);
if (!groupedMap.has(groupKey)) {
groupedMap.set(groupKey, {
value: groupKey,
label: groupKey,
type: groupType,
isParent: true,
children: []
});
}
groupedMap.get(groupKey)!.children.push(options.getChild(item));
});
return Array.from(groupedMap.values());
}
+2
View File
@@ -7,6 +7,8 @@ export interface UsageFilterItem {
api_key_name: string | null;
access_key: string | null;
api_key_is_custom: boolean | null;
provider_type: string | null;
provider_name: string | null;
};
current: {
model_id: string | null;
+15 -8
View File
@@ -1,12 +1,15 @@
import { exportJsonToExcel } from '@/utils/excel-reader';
import { useModel } from '@@/plugin-model';
import dayjs from 'dayjs';
import _ from 'lodash';
import React, { useEffect, useMemo, useState } from 'react';
import BreakdownTabs from './components/breakdown-tabs';
import DailyUsage from './components/daily-usage';
import FilterBar from './components/filter-bar';
import { generateColumns, transformTimelineToTable } from './config';
import {
generateColumns,
GroupOption,
transformTimelineToTable
} from './config';
import { UsageFilterItem } from './config/types';
import useExportTable from './hooks/use-export-table';
import useQueryUsageMetaData from './services/use-query-meta-data';
@@ -17,6 +20,7 @@ const DefaultDateConfig = {
};
type FilterOptionType = Omit<UsageFilterItem, 'label' | 'deleted'>;
type GroupOptionType = GroupOption<UsageFilterItem>;
const Usage: React.FC = () => {
const initialInfo = useModel('@@initialState');
@@ -99,9 +103,12 @@ const Usage: React.FC = () => {
} = {};
if (selected.models?.length > 0) {
const modelsSet = new Set(selected.models);
// filter the options
filters.models = modelOptions
.filter((item) => selected.models?.includes(item.value || ''))
.flatMap((group) =>
group.children.filter((item) => modelsSet.has(item.value))
)
.map((item) => ({
identity: item.identity
}));
@@ -116,8 +123,11 @@ const Usage: React.FC = () => {
}
if (selected.api_keys?.length > 0) {
const apiKeysSet = new Set(selected.api_keys);
filters.api_keys = apiKeyOptions
.filter((item) => selected.api_keys?.includes(item.value || ''))
.flatMap((group) =>
group.children.filter((item) => apiKeysSet.has(item.value))
)
.map((item) => ({
identity: item.identity
}));
@@ -135,10 +145,6 @@ const Usage: React.FC = () => {
currentSelectedFilters = commonFilters,
currentChartFilters = chartFilters
) => {
const filteredChartFilters = _.pickBy(
currentChartFilters,
(value: string) => value
);
fetchTimeSeriesData({
...currentChartFilters,
start_date: currentSelectedFilters.start_date,
@@ -170,6 +176,7 @@ const Usage: React.FC = () => {
};
const handleFilterChange = (type: string, value: string[]) => {
console.log('handleFilterChange:', type, value);
setCommonFilters((prev) => ({ ...prev, [type]: value }));
fetchData({ ...commonFilters, [type]: value }, chartFilters);
};
+23 -12
View File
@@ -1,9 +1,10 @@
import { useQueryData } from '@/hooks/use-query-data-list';
import { useState } from 'react';
import { queryUsageMetaData } from '../apis';
import { GroupOption, groupToOptions } from '../config';
import { UsageFilterItem, UsageMeta } from '../config/types';
type OptionType = UsageFilterItem & {
type UserOptionType = UsageFilterItem & {
value: string;
};
@@ -15,9 +16,9 @@ export default function useQueryUsageMetaData() {
});
const [result, setResult] = useState<{
models: OptionType[];
users: OptionType[];
api_keys: OptionType[];
models: GroupOption<UsageFilterItem>[];
users: UserOptionType[];
api_keys: GroupOption<UsageFilterItem>[];
}>({
models: [],
users: [],
@@ -28,21 +29,31 @@ export default function useQueryUsageMetaData() {
const res = await fetchData({});
const data = {
models:
res?.filters?.models?.map((item) => ({
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,
...item
})) || [],
label: item.identity.value.model_name || ''
})
}),
users:
res?.filters?.users?.map((item) => ({
value: item.label,
...item
})) || [],
api_keys:
res?.filters?.api_keys?.map((item) => ({
api_keys: groupToOptions(res?.filters?.api_keys || [], {
getGroupKey: (item) => item.identity.value.user_name || 'unknown_user',
getGroupType: (item) =>
item.identity.value.api_key_is_custom ? 'custom' : 'default',
getChild: (item) => ({
...item,
value: item.label,
...item
})) || []
label: item.identity.value.api_key_name || ''
})
})
};
setResult(data);
};
+7
View File
@@ -12,3 +12,10 @@
flex: 1;
align-items: center;
}
.optionsWrapper {
display: flex;
align-items: center;
gap: 8px;
padding-right: 4px;
}