fix: models, apikeys Cascader
This commit is contained in:
@@ -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: {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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)' }}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -12,3 +12,10 @@
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.optionsWrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user