diff --git a/src/components/card-wrapper/index.tsx b/src/components/card-wrapper/index.tsx index 0b5c1bea..95a0e7ed 100644 --- a/src/components/card-wrapper/index.tsx +++ b/src/components/card-wrapper/index.tsx @@ -4,7 +4,7 @@ const Wrapper = styled.div` border-radius: var(--border-radius-lg); background-color: var(--ant-color-bg-container); box-shadow: none; - padding: 10px 16px; + padding: 8px 16px; border: 1px solid var(--ant-color-border); `; diff --git a/src/components/card-wrapper/simple-card.tsx b/src/components/card-wrapper/simple-card.tsx index fbf4f5e4..d0d4214b 100644 --- a/src/components/card-wrapper/simple-card.tsx +++ b/src/components/card-wrapper/simple-card.tsx @@ -87,7 +87,7 @@ export const SimpleCard: React.FC<{ label: string; value: React.ReactNode; color: string; - iconType: string; + iconType?: string; }[]; height?: string | number; bordered?: boolean; diff --git a/src/components/icon-font/index.tsx b/src/components/icon-font/index.tsx index a1d3f372..08df0a0b 100644 --- a/src/components/icon-font/index.tsx +++ b/src/components/icon-font/index.tsx @@ -2,7 +2,7 @@ import { createFromIconfontCN } from '@ant-design/icons'; // import './iconfont/iconfont.js'; const IconFont = createFromIconfontCN({ - scriptUrl: '//at.alicdn.com/t/c/font_4613488_htxf2x5ewb.js' + scriptUrl: '//at.alicdn.com/t/c/font_4613488_mk9rqojjoqk.js' }); export default IconFont; diff --git a/src/global.less b/src/global.less index d4cbb08f..6eede573 100644 --- a/src/global.less +++ b/src/global.less @@ -169,7 +169,7 @@ body { } } - .ant-table-container .ant-table-content table { + .ant-table-container table { // border-spacing: 0 20px; .ant-table-thead th.ant-table-column-sort { diff --git a/src/pages/dashboard/hooks/use-rangepicker-preset.ts b/src/pages/dashboard/hooks/use-rangepicker-preset.ts index 47097c88..b763775d 100644 --- a/src/pages/dashboard/hooks/use-rangepicker-preset.ts +++ b/src/pages/dashboard/hooks/use-rangepicker-preset.ts @@ -12,6 +12,8 @@ interface RangePickerPreset { disabledDate?: boolean; } +type DateType = 'date' | 'week' | 'month' | 'quarter' | 'year'; + export default function useRangePickerPreset(options?: RangePickerPreset): { disabledRangeDaysDate: DatePickerProps['disabledDate']; rangePresets: { @@ -21,19 +23,15 @@ export default function useRangePickerPreset(options?: RangePickerPreset): { normalizeRangeValue: ( startDate: string, endDate: string, - picker: 'date' | 'week' | 'month' | 'quarter' | 'year' + picker: DateType ) => [Dayjs, Dayjs]; - handleOnPickerChange: ( - value: 'date' | 'week' | 'month' | 'quarter' | 'year' - ) => void; - picker: 'date' | 'week' | 'month' | 'quarter' | 'year'; + handleOnPickerChange: (value: DateType) => void; + picker: DateType; range: number; } { const { range = 60, disabledDate, presetRanges } = options || {}; const intl = useIntl(); - const [picker, setPicker] = useState< - 'date' | 'week' | 'month' | 'quarter' | 'year' - >('date'); + const [picker, setPicker] = useState('date'); const getYearMonth = (date: Dayjs) => date.year() * 12 + date.month(); @@ -132,9 +130,7 @@ export default function useRangePickerPreset(options?: RangePickerPreset): { return false; }; - const handleOnPickerChange = ( - value: 'date' | 'week' | 'month' | 'quarter' | 'year' - ) => { + const handleOnPickerChange = (value: DateType) => { setPicker(value); }; diff --git a/src/pages/usage/components/breakdown-tabs.tsx b/src/pages/usage/components/breakdown-tabs.tsx index 60e1d6ae..cafc831e 100644 --- a/src/pages/usage/components/breakdown-tabs.tsx +++ b/src/pages/usage/components/breakdown-tabs.tsx @@ -1,5 +1,5 @@ import { Tabs } from 'antd'; -import React from 'react'; +import React, { useMemo } from 'react'; import { GroupOption } from '../config'; import { UsageFilterItem } from '../config/types'; import ApiKeysTable from '../tables/apikeys-table'; @@ -8,6 +8,7 @@ import UsersTable from '../tables/users-table'; type FilterOptionType = Omit; type GroupOptionType = GroupOption; +const EMPTY_FILTERS: FilterOptionType[] = []; const BreakdownTabs: React.FC<{ dateRange: { @@ -15,58 +16,68 @@ const BreakdownTabs: React.FC<{ end_date: string; }; scope: string; + refreshKey?: number; filters: { models?: FilterOptionType[]; users?: FilterOptionType[]; api_keys?: FilterOptionType[]; }; -}> = ({ filters, dateRange, scope }) => { - const items = [ - { - key: 'models', - label: 'Models', - forceRender: true, - children: ( - - ) - }, - { - key: 'users', - label: 'Users', - forceRender: true, - children: ( - - ) - }, - { - key: 'api_keys', - label: 'API Keys', - forceRender: true, - children: ( - - ) - } - ].filter((item) => { - if (item.key === 'users') { - return scope === 'all'; - } - return true; - }); +}> = ({ filters, dateRange, scope, refreshKey = 0 }) => { + const models = filters.models || EMPTY_FILTERS; + const users = filters.users || EMPTY_FILTERS; + const apiKeys = filters.api_keys || EMPTY_FILTERS; + + const items = useMemo(() => { + return [ + { + key: 'models', + label: 'Models', + forceRender: true, + children: ( + + ) + }, + { + key: 'users', + label: 'Users', + forceRender: true, + children: ( + + ) + }, + { + key: 'api_keys', + label: 'API Keys', + forceRender: true, + children: ( + + ) + } + ].filter((item) => { + if (item.key === 'users') { + return scope === 'all'; + } + return true; + }); + }, [apiKeys, dateRange, models, refreshKey, scope, users]); return (
diff --git a/src/pages/usage/components/daily-usage.tsx b/src/pages/usage/components/daily-usage.tsx index 89cdd4ee..92eda4ee 100644 --- a/src/pages/usage/components/daily-usage.tsx +++ b/src/pages/usage/components/daily-usage.tsx @@ -2,7 +2,6 @@ import CardWrapper from '@/components/card-wrapper'; import MixLineBar from '@/components/echarts/mix-line-bar'; import BaseSelect from '@/components/seal-form/base/select'; import { baseColorMap } from '@/pages/dashboard/config'; -import { formatLargeNumber } from '@/utils'; import { Segmented } from 'antd'; import dayjs from 'dayjs'; import React, { useMemo } from 'react'; @@ -31,10 +30,10 @@ const ControlLabel = styled.span` interface DailyUsageProps { timeSeriesData: TimeSeriesData | null; metric: string; - groupBy: string; + groupBy: string | null; granularity: string; onMetricChange: (value: string) => void; - onGroupByChange: (value: string) => void; + onGroupByChange: (value: string | null) => void; onGranularityChange: (value: string) => void; } @@ -53,49 +52,6 @@ const DailyUsage: React.FC = (props) => { onGranularityChange } = props; - const summary = timeSeriesData?.summary || { - input_tokens: 0, - output_tokens: 0, - total_tokens: 0, - api_requests: 0, - models_called: 0 - }; - - const summaryCards = useMemo(() => { - return [ - { - label: formatLargeNumber(summary.input_tokens) as string, - value: 'Input tokens', - color: baseColorMap.baseR3, - iconType: 'roundRect' - }, - { - label: formatLargeNumber(summary.output_tokens) as string, - value: 'Output tokens', - color: baseColorMap.base, - iconType: 'roundRect' - }, - { - label: formatLargeNumber(summary.total_tokens) as string, - value: 'Total tokens', - color: baseColorMap.baseL1, - iconType: 'roundRect' - }, - { - label: formatLargeNumber(summary.api_requests) as string, - value: 'API requests', - color: baseColorMap.baseR1, - iconType: 'circle' - }, - { - label: summary.models_called.toString(), - value: 'Models used', - color: baseColorMap.baseR2, - iconType: 'roundRect' - } - ]; - }, [summary]); - const { chartData, xAxisData } = useMemo(() => { if (!timeSeriesData?.series || timeSeriesData.series.length === 0) { return { chartData: { line: [], bar: [] }, xAxisData: [] }; @@ -120,17 +76,10 @@ const DailyUsage: React.FC = (props) => { })); // API requests use line chart, tokens use bar chart - if (metric === 'api_requests') { - return { - chartData: { line: chartSeries, bar: [] }, - xAxisData: xAxis - }; - } else { - return { - chartData: { line: [], bar: chartSeries }, - xAxisData: xAxis - }; - } + return { + chartData: { line: [], bar: chartSeries }, + xAxisData: xAxis + }; }, [timeSeriesData, metric]); const legendData = useMemo(() => { @@ -142,6 +91,10 @@ const DailyUsage: React.FC = (props) => { })); }, [timeSeriesData, metric]); + const handleOnGroupByChange = (value: string) => { + onGroupByChange(value || null); + }; + return (
@@ -154,16 +107,18 @@ const DailyUsage: React.FC = (props) => { value={metric} popupMatchSelectWidth={false} onChange={onMetricChange} - style={{ width: 180 }} + style={{ width: 'max-content' }} /> Group by} options={groupByOptions} value={groupBy} popupMatchSelectWidth={false} - onChange={onGroupByChange} + onChange={handleOnGroupByChange} + style={{ width: 'max-content' }} />
= (props) => { onChange={onGranularityChange} > - - {/* */} void; initialScope: string; metaData: any; + granularity: string; + initialState: { + activeModels: ValueType[][]; + activeApiKeys: ValueType[][]; + }; + + handlePickerChange: (picker: DateType) => void; }> = (props) => { - const { open, onCancel, initialScope, metaData } = props || {}; + const { + open, + onCancel, + initialScope, + metaData, + granularity, + handlePickerChange, + initialState + } = props || {}; const intl = useIntl(); - const { - filters, - commonFilters, - fetchData, - loading, - timeSeriesData, - filterBar - } = useUsageFilters({ - initialScope: initialScope, - metaData, - chartFilters: {}, - summaryColumns: {} + const [pageParams, setPageParams] = React.useState<{ + page: number; + perPage: number; + }>({ + page: 1, + perPage: 100 }); - const exportTableColumns: TableColumnType[] = [ + const { + fetchData: fetchExportData, + loading, + dataSource, + cancelRequest + } = useQueryBreakdownList({ + key: 'exportTableData' + }); + + const { filters, commonFilters, filterBar } = useUsageFilters({ + initialScope: initialScope, + metaData, + chartFilters: { + metric: 'total_tokens', + group_by: null, + granularity: props.granularity || 'day' + }, + initialState: initialState, + summaryColumns: [], + autoFetchOnFilterChange: true, + onFetchData: ({ + chartFilters: nextChartFilters, + filters: nextFilters, + commonFilters: nextCommonFilters + }) => { + fetchExportData({ + ...pageParams, + granularity: nextChartFilters.granularity, + group_by: ['date', 'user', 'model', 'api_key'], + filters: nextFilters, + scope: initialScope, + start_date: nextCommonFilters.start_date, + end_date: nextCommonFilters.end_date + }); + } + }); + + const exportTableColumns: TableColumnType[] = [ { title: intl.formatMessage({ id: 'resources.table.index' }), width: 80, @@ -40,34 +92,124 @@ const ExportData: React.FC<{ }, { title: intl.formatMessage({ id: 'dashboard.usage.export.date' }), - dataIndex: 'date' + dataIndex: ['date', 'label'], + render: (text: string) => { + return ( + + {text} + + ); + } }, { - title: intl.formatMessage({ id: 'dashboard.usage.export.user' }), - dataIndex: 'user_name', + title: 'Cluster', + dataIndex: ['model', 'identity', 'value', 'cluster_name'], render: (text: string) => { return {text}; } }, { title: intl.formatMessage({ id: 'dashboard.usage.export.model' }), - dataIndex: 'model_id', + dataIndex: ['model', 'identity', 'value', 'model_name'], + render: (text: string, record: any) => { + return ( + + {record?.model?.identity?.value?.provider_name + ? `${record?.model?.identity?.value?.provider_name}/${text}` + : text} + + ); + } + }, + { + title: intl.formatMessage({ id: 'dashboard.usage.export.user' }), + dataIndex: ['user', 'label'], + render: (text: string) => { + return {text}; + } + }, + { + title: 'API Key', + dataIndex: ['api_key', 'label'], render: (text: string, record: any) => { return {text}; } }, - { - title: 'API Key', - dataIndex: 'api_key', - render: (text: string, record: any) => { - return {text}; - } + title: 'Input Tokens', + dataIndex: 'input_tokens' + }, + { + title: 'Output Tokens', + dataIndex: 'output_tokens' + }, + { + title: 'Total Tokens', + dataIndex: 'total_tokens' + }, + { + title: 'API Requests', + dataIndex: 'api_requests' } ]; const handleSubmit = () => { - filterBar.onExportChart(); + exportJsonToExcel({ + fileName: `usage_export_${commonFilters.start_date}_${commonFilters.end_date}.xlsx`, + sheets: [ + { + jsonData: (dataSource.dataList || []).map((item: any) => ({ + date: item?.date?.label, + user: item?.user?.label, + cluster: item?.model?.identity?.value?.cluster_name, + model: item?.model?.identity?.value?.model_name, + api_key: item?.api_key?.label, + input_tokens: item?.input_tokens, + output_tokens: item?.output_tokens, + total_tokens: item?.total_tokens, + api_requests: item?.api_requests + })), + sheetName: 'usage', + fields: [ + 'date', + 'user', + 'model', + 'api_key', + 'input_tokens', + 'output_tokens', + 'total_tokens', + 'api_requests' + ], + fieldLabels: { + date: intl.formatMessage({ id: 'dashboard.usage.export.date' }), + user: intl.formatMessage({ id: 'dashboard.usage.export.user' }), + cluster: 'Cluster', + model: intl.formatMessage({ id: 'dashboard.usage.export.model' }), + api_key: 'API Key', + input_tokens: 'Input Tokens', + output_tokens: 'Output Tokens', + total_tokens: 'Total Tokens', + api_requests: 'API Requests' + }, + formatMap: {} + } + ] + }); + }; + + const handlePageChange = (page: number, pageSize: number) => { + setPageParams({ page, perPage: pageSize }); + fetchExportData({ + ...pageParams, + page, + perPage: pageSize, + granularity, + group_by: ['date', 'user', 'model', 'api_key'], + filters, + scope: initialScope, + start_date: commonFilters.start_date, + end_date: commonFilters.end_date + }); }; const handleOnCancel = () => { @@ -76,11 +218,22 @@ const ExportData: React.FC<{ useEffect(() => { if (open) { - fetchData(commonFilters, {}); + fetchExportData({ + ...pageParams, + granularity, + group_by: ['date', 'user', 'model', 'api_key'], + filters, + scope: initialScope, + start_date: commonFilters.start_date, + end_date: commonFilters.end_date + }); } else { + cancelRequest(); } }, [open]); + console.log('export dataSource', filterBar); + return ( } > - +
); diff --git a/src/pages/usage/components/filter-bar.tsx b/src/pages/usage/components/filter-bar.tsx index c6f849db..20a2d7d5 100644 --- a/src/pages/usage/components/filter-bar.tsx +++ b/src/pages/usage/components/filter-bar.tsx @@ -1,4 +1,5 @@ import AutoTooltip from '@/components/auto-tooltip'; +import IconFont from '@/components/icon-font'; 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'; @@ -21,6 +22,9 @@ const DefaultDateConfig = { type OptionType = UsageFilterItem & { value: string; }; + +type DateType = 'date' | 'week' | 'month' | 'quarter' | 'year'; + interface FilterBarProps { pageType?: 'page' | 'modal'; scope: string; @@ -32,6 +36,7 @@ interface FilterBarProps { modelOptions: GroupOption[]; userOptions: OptionType[]; apiKeyOptions: GroupOption[]; + handlePickerChange: (picker: DateType) => void; onScopeChange: (value: string) => void; onDateChange: (dates: any, dateStrings: [string, string]) => void; onModelsChange: (value: string[]) => void; @@ -53,6 +58,7 @@ const FilterBar: React.FC = (props) => { modelOptions, userOptions, apiKeyOptions, + handlePickerChange, onDateChange, onModelsChange, onUsersChange, @@ -107,12 +113,22 @@ const FilterBar: React.FC = (props) => { const exportMenuItems: MenuProps['items'] = [ { key: 'chart', - label: 'Export Chart Data', + label: ( + + + Export Chart Data + + ), onClick: onExportChart }, { key: 'table', - label: 'Export Table Data', + label: ( + + + Export Table Data + + ), onClick: onExportTable } ]; @@ -187,6 +203,11 @@ const FilterBar: React.FC = (props) => { ); }; + const onPickerChange = (picker: DateType) => { + handleOnPickerChange(picker); + handlePickerChange(picker); + }; + const renderFooter = () => { return ( = (props) => { flex: 1 } }} - onChange={handleOnPickerChange} + onChange={onPickerChange} options={[ { label: 'Day', value: 'date' }, - { - label: 'Week', - value: 'week' - }, { label: 'Month', value: 'month' + }, + { + label: 'Week', + value: 'week' } ]} > @@ -232,11 +253,18 @@ const FilterBar: React.FC = (props) => { return ( {data.label} - {data.isCurrent && [Current]} + {data.isCurrent && ( + [Current Account] + )} ); }; + // only for dates greater than the current day should be disabled. + const disabledDate = (current: dayjs.Dayjs) => { + return current && current > dayjs().endOf('day'); + }; + return (
@@ -248,12 +276,11 @@ const FilterBar: React.FC = (props) => { ]} format={'YYYY-MM-DD'} picker={picker} - disabledDate={disabledRangeDaysDate} + disabledDate={disabledDate} presets={rangePresets} allowClear={false} style={{ width: 240 }} onChange={onDateChange} - renderExtraFooter={renderFooter} />
= (props) => { }} maxTagCount={1} size="small" - placeholder={intl.formatMessage({ - id: 'dashboard.usage.selectmodel' - })} + placeholder={'Filter by model'} options={modelOptions} showCheckedStrategy="SHOW_CHILD" displayRender={displayRender} @@ -304,7 +329,7 @@ const FilterBar: React.FC = (props) => { showSearch mode="multiple" options={userOptions} - placeholder="User" + placeholder="Filter by user" styles={{ wrapper: { flex: 1, maxWidth: 240, minWidth: 150 } }} @@ -342,7 +367,7 @@ const FilterBar: React.FC = (props) => { }} maxTagCount={1} size="small" - placeholder="API Key" + placeholder="Filter by API key" options={apiKeyOptions} showCheckedStrategy="SHOW_CHILD" value={activeApiKeys} @@ -359,7 +384,7 @@ const FilterBar: React.FC = (props) => { mode="multiple" options={apiKeyOptions?.[0]?.children || []} maxTagCount={0} - placeholder="API Key" + placeholder="Filter by API key" styles={{ wrapper: { flex: 1, maxWidth: 240, minWidth: 100 } }} diff --git a/src/pages/usage/config/index.ts b/src/pages/usage/config/index.ts index b8eef55b..b11b30de 100644 --- a/src/pages/usage/config/index.ts +++ b/src/pages/usage/config/index.ts @@ -1,10 +1,10 @@ import { UsageFilterItem } from './types'; export const groupByOptions = [ - { - value: '', - label: 'None' - }, + // { + // value: null, + // label: 'None' + // }, { value: 'model', label: 'Model' @@ -163,7 +163,8 @@ export function groupToOptions( }); } - groupedMap.get(groupKey)!.children.push(options.getChild(item)); + const child = options.getChild(item) as TChild & { value: string }; + groupedMap.get(groupKey)!.children.push(child); }); return Array.from(groupedMap.values()); diff --git a/src/pages/usage/hooks/use-apikeys-columns.tsx b/src/pages/usage/hooks/use-apikeys-columns.tsx index f270d7c8..fa796f40 100644 --- a/src/pages/usage/hooks/use-apikeys-columns.tsx +++ b/src/pages/usage/hooks/use-apikeys-columns.tsx @@ -7,7 +7,7 @@ import { BreakdownItem as ListItem } from '../config/types'; const useModelsColumns = (): Array<{ title: string; - dataIndex: string; + dataIndex: string | string[]; key: string; }> => { const intl = useIntl(); @@ -16,7 +16,7 @@ const useModelsColumns = (): Array<{ return [ { title: intl.formatMessage({ id: 'common.table.name' }), - dataIndex: 'api_key_name', + dataIndex: ['api_key', 'identity', 'value', 'api_key_name'], key: 'api_key_name', sorter: tableSorter(1), render: (text: string, record: ListItem) => ( @@ -29,7 +29,7 @@ const useModelsColumns = (): Array<{ }, { title: 'User', - dataIndex: 'user_name', + dataIndex: ['api_key', 'identity', 'value', 'user_name'], key: 'user_name', render: (text: string, record: ListItem) => ( diff --git a/src/pages/usage/hooks/use-models-columns.tsx b/src/pages/usage/hooks/use-models-columns.tsx index 72719557..1586dfa2 100644 --- a/src/pages/usage/hooks/use-models-columns.tsx +++ b/src/pages/usage/hooks/use-models-columns.tsx @@ -12,7 +12,7 @@ interface ColumnsHookProps { const useModelsColumns = (): Array<{ title: string; - dataIndex: string; + dataIndex: string | string[]; key: string; }> => { const intl = useIntl(); @@ -21,7 +21,7 @@ const useModelsColumns = (): Array<{ return [ { title: intl.formatMessage({ id: 'common.table.name' }), - dataIndex: 'model_name', + dataIndex: ['model', 'identity', 'value', 'model_name'], key: 'model_name', sorter: tableSorter(1), render: (text: string, record: ListItem) => ( @@ -34,7 +34,7 @@ const useModelsColumns = (): Array<{ }, { title: 'Cluster', - dataIndex: 'cluster_name', + dataIndex: ['model', 'identity', 'value', 'cluster_name'], key: 'cluster_name', sorter: tableSorter(2), render: (text: string, record: ListItem) => ( diff --git a/src/pages/usage/hooks/use-usage-filters.ts b/src/pages/usage/hooks/use-usage-filters.ts index 33eb200f..51014685 100644 --- a/src/pages/usage/hooks/use-usage-filters.ts +++ b/src/pages/usage/hooks/use-usage-filters.ts @@ -16,6 +16,7 @@ const DefaultDateConfig = { type UserOptionType = UsageFilterItem & { value: string; }; +type ValueType = string | number | null; type FilterOptionType = Omit; type GroupOptionType = GroupOption; @@ -29,21 +30,49 @@ interface UseUsageFiltersParams { }; chartFilters: { metric: string; - group_by: string; + group_by: string | null; granularity: string; }; + initialState?: { + activeModels?: ValueType[][]; + activeApiKeys?: ValueType[][]; + }; summaryColumns: { title: string; dataIndex: string; key: string; }[]; + autoFetchOnFilterChange?: boolean; + onFetchData?: (params: { + chartFilters: UseUsageFiltersParams['chartFilters']; + filters: { + models?: FilterOptionType[]; + users?: FilterOptionType[]; + api_keys?: FilterOptionType[]; + }; + commonFilters: { + scope: string; + models: string[]; + users: string[]; + api_keys: string[]; + start_date: string; + end_date: string; + }; + }) => void; } export const useUsageFilters = ({ initialScope, metaData, chartFilters, - summaryColumns + summaryColumns, + initialState: { + activeModels: initialActiveModels = [], + activeApiKeys: initialActiveApiKeys = [] + } = {}, + + autoFetchOnFilterChange = true, + onFetchData }: UseUsageFiltersParams) => { const { detailData: timeSeriesData, @@ -64,6 +93,10 @@ export const useUsageFilters = ({ const modelOptions = metaData?.models || []; const userOptions = metaData?.users || []; const apiKeyOptions = metaData?.api_keys || []; + const [activeModels, setActiveModels] = + useState(initialActiveModels); + const [activeApiKeys, setActiveApiKeys] = + useState(initialActiveApiKeys); const buildFilters = (selected: typeof commonFilters) => { const filters: { @@ -114,18 +147,31 @@ export const useUsageFilters = ({ currentSelectedFilters = commonFilters, currentChartFilters = chartFilters ) => { + const nextFilters = buildFilters(currentSelectedFilters); + + if (onFetchData) { + onFetchData({ + chartFilters: currentChartFilters, + filters: nextFilters, + commonFilters: currentSelectedFilters + }); + return; + } + fetchTimeSeriesData({ ...currentChartFilters, start_date: currentSelectedFilters.start_date, end_date: currentSelectedFilters.end_date, - filters: buildFilters(currentSelectedFilters) + filters: nextFilters }); }; const handleScopeChange = (value: string) => { const next = { ...commonFilters, scope: value }; setCommonFilters(next); - fetchData(next, chartFilters); + if (autoFetchOnFilterChange) { + fetchData(next, chartFilters); + } }; const handleDateChange = (_: any, dateStrings: [string, string]) => { @@ -133,16 +179,27 @@ export const useUsageFilters = ({ const [start_date, end_date] = dateStrings; const next = { ...commonFilters, start_date, end_date }; setCommonFilters(next); - fetchData(next, chartFilters); + if (autoFetchOnFilterChange) { + fetchData(next, chartFilters); + } }; const handleFilterChange = ( type: 'models' | 'users' | 'api_keys', value: string[] ) => { - const next = { ...commonFilters, [type]: 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; + }); + + const next = { ...commonFilters, [type]: selectedValues }; setCommonFilters(next); - fetchData(next, chartFilters); + if (autoFetchOnFilterChange) { + fetchData(next, chartFilters); + } }; const handleSearch = () => { @@ -188,6 +245,8 @@ export const useUsageFilters = ({ modelOptions, userOptions, apiKeyOptions, + activeApiKeys, + activeModels, handleSearch, onScopeChange: handleScopeChange, onDateChange: handleDateChange, diff --git a/src/pages/usage/hooks/use-users-columns.tsx b/src/pages/usage/hooks/use-users-columns.tsx index 993b53ae..704a2b8e 100644 --- a/src/pages/usage/hooks/use-users-columns.tsx +++ b/src/pages/usage/hooks/use-users-columns.tsx @@ -11,7 +11,7 @@ interface ColumnsHookProps { const useModelsColumns = (): Array<{ title: string; - dataIndex: string; + dataIndex: string | string[]; key: string; }> => { const intl = useIntl(); @@ -20,7 +20,7 @@ const useModelsColumns = (): Array<{ return [ { title: intl.formatMessage({ id: 'common.table.name' }), - dataIndex: 'user_name', + dataIndex: ['user', 'identity', 'value', 'user_name'], key: 'user_name', sorter: tableSorter(1), render: (text: string, record: ListItem) => ( diff --git a/src/pages/usage/index.tsx b/src/pages/usage/index.tsx index 0c65a9ac..889dadcf 100644 --- a/src/pages/usage/index.tsx +++ b/src/pages/usage/index.tsx @@ -11,11 +11,14 @@ import useExportTable from './hooks/use-export-table'; import { useUsageFilters } from './hooks/use-usage-filters'; import useQueryUsageMetaData from './services/use-query-meta-data'; +type DateType = 'date' | 'week' | 'month' | 'quarter' | 'year'; + const Usage: React.FC = () => { const initialInfo = useModel('@@initialState'); const { initialState } = initialInfo || {}; const { exportTable } = useExportTable(); const [openExportModal, setOpenExportModal] = useState(false); + const [breakdownRefreshKey, setBreakdownRefreshKey] = useState(0); const summaryColumns = [ { @@ -47,11 +50,11 @@ const Usage: React.FC = () => { const [chartFilters, setChartFilters] = useState<{ metric: string; - group_by: string; + group_by: string | null; granularity: string; }>({ metric: 'total_tokens', - group_by: 'model', + group_by: null, granularity: 'day' }); @@ -118,14 +121,36 @@ const Usage: React.FC = () => { ]; }, [timeSeriesData.summary]); + const breakdownDateRange = useMemo( + () => ({ + start_date: commonFilters.start_date, + end_date: commonFilters.end_date + }), + [commonFilters.end_date, commonFilters.start_date] + ); + + const handlePickerChange = (picker: DateType) => { + setChartFilters((prev) => ({ + ...prev, + granularity: picker === 'date' ? 'day' : picker + })); + }; + const handleExportChart = () => { setOpenExportModal(true); }; + const handleSearch = () => { + filterBar.handleSearch(); + setBreakdownRefreshKey((prev) => prev + 1); + }; + return (
@@ -139,9 +164,8 @@ const Usage: React.FC = () => { height={80} styles={{ item: { - borderBottom: '1px solid var(--ant-color-split)', backgroundColor: 'var(--ant-color-fill-quaternary)', - borderRadius: '6px 6px 0 0' + borderRadius: '6px' } }} /> @@ -159,17 +183,22 @@ const Usage: React.FC = () => { /> setOpenExportModal(false)} + initialState={{ + activeModels: filterBar.activeModels, + activeApiKeys: filterBar.activeApiKeys + }} >
); diff --git a/src/pages/usage/tables/apikeys-table.tsx b/src/pages/usage/tables/apikeys-table.tsx index 1b30e8c7..94144ed2 100644 --- a/src/pages/usage/tables/apikeys-table.tsx +++ b/src/pages/usage/tables/apikeys-table.tsx @@ -9,12 +9,14 @@ import { useEffect, useState } from 'react'; import { FilterOptionType } from '../config/types'; import useAPIKeys from '../hooks/use-apikeys-columns'; import useQueryBreakdownList from '../services/use-query-breakdown-list'; +import getBreakdownRowKey from '../utils/get-breakdown-row-key'; const APIKeys: React.FC<{ apiKeys: FilterOptionType[]; dateRange: { start_date: string; end_date: string }; scope: string; -}> = ({ apiKeys, dateRange, scope }) => { + refreshKey?: number; +}> = ({ apiKeys, dateRange, scope, refreshKey = 0 }) => { const intl = useIntl(); const { loading, dataSource, fetchData } = useQueryBreakdownList({ @@ -57,14 +59,23 @@ const APIKeys: React.FC<{ useEffect(() => { fetchData({ ...queryParams, - group_by: 'api_key', + group_by: ['api_key'], filters: { - api_keys: apiKeys || [] + api_keys: apiKeys }, scope: scope, ...dateRange }); - }, [dateRange, apiKeys, queryParams, scope]); + }, [ + apiKeys, + dateRange.end_date, + dateRange.start_date, + queryParams.page, + queryParams.perPage, + queryParams.sort_by, + refreshKey, + scope + ]); return ( <> @@ -73,13 +84,13 @@ const APIKeys: React.FC<{ = ({ models, dateRange, scope }) => { + refreshKey?: number; +}> = ({ models, dateRange, scope, refreshKey = 0 }) => { const intl = useIntl(); const { loading, dataSource, fetchData } = useQueryBreakdownList({ @@ -43,13 +45,13 @@ const Models: React.FC<{ loading={loading} loadend={dataSource.loadend} dataSource={dataSource.dataList} - image={} + image={} filters={_.omit(queryParams, ['sort_by'])} noFoundText={intl.formatMessage({ - id: 'noresult.keys.nofound' + id: 'noresult.mymodels.nofound' })} - title={intl.formatMessage({ id: 'noresult.keys.title' })} - subTitle={intl.formatMessage({ id: 'noresult.keys.subTitle' })} + title={intl.formatMessage({ id: 'noresult.deployments.title' })} + subTitle={intl.formatMessage({ id: 'noresult.deployments.subTitle' })} > ); }; @@ -57,14 +59,23 @@ const Models: React.FC<{ useEffect(() => { fetchData({ ...queryParams, - group_by: 'model', + group_by: ['model'], filters: { - models: models || [] + models }, scope: scope, ...dateRange }); - }, [dateRange, models, queryParams, scope]); + }, [ + dateRange.end_date, + dateRange.start_date, + models, + queryParams.page, + queryParams.perPage, + queryParams.sort_by, + refreshKey, + scope + ]); return ( <> @@ -73,13 +84,13 @@ const Models: React.FC<{
= ({ users, dateRange, scope }) => { + refreshKey?: number; +}> = ({ users, dateRange, scope, refreshKey = 0 }) => { const intl = useIntl(); const { loading, dataSource, fetchData } = useQueryBreakdownList({ @@ -43,13 +45,13 @@ const Users: React.FC<{ loading={loading} loadend={dataSource.loadend} dataSource={dataSource.dataList} - image={} + image={} filters={_.omit(queryParams, ['sort_by'])} noFoundText={intl.formatMessage({ - id: 'noresult.keys.nofound' + id: 'noresult.users.nofound' })} - title={intl.formatMessage({ id: 'noresult.keys.title' })} - subTitle={intl.formatMessage({ id: 'noresult.keys.subTitle' })} + title={intl.formatMessage({ id: 'noresult.users.title' })} + subTitle={intl.formatMessage({ id: 'noresult.users.subTitle' })} > ); }; @@ -58,15 +60,24 @@ const Users: React.FC<{ if (scope === 'all') { fetchData({ ...queryParams, - group_by: 'user', + group_by: ['user'], filters: { - users: users || [] + users }, scope: scope, ...dateRange }); } - }, [dateRange, users, queryParams, scope]); + }, [ + dateRange.end_date, + dateRange.start_date, + queryParams.page, + queryParams.perPage, + queryParams.sort_by, + refreshKey, + scope, + users + ]); return ( <> @@ -75,13 +86,13 @@ const Users: React.FC<{
{ + const identity = record?.identity; + const current = identity?.current; + const value = identity?.value; + + const key = [ + current?.model_id, + current?.user_id, + current?.api_key_id, + value?.model_name, + value?.user_name, + value?.api_key_name, + value?.cluster_name, + value?.provider_name, + value?.provider_type, + record?.label, + record?.cluster_name, + record?.model_name, + record?.user_name, + record?.api_key_name, + record?.last_active, + record?.input_tokens, + record?.output_tokens, + record?.total_tokens, + record?.api_requests + ] + .filter((item) => item !== null && item !== undefined && item !== '') + .join('__'); + + return key || JSON.stringify(record); +}; + +export default getBreakdownRowKey;