style: card style

This commit is contained in:
jialin
2026-04-22 13:16:48 +08:00
committed by jialin
parent daa3d74500
commit c92f56ec8f
19 changed files with 532 additions and 222 deletions
+1 -1
View File
@@ -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);
`;
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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 {
@@ -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<DateType>('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);
};
+59 -48
View File
@@ -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<UsageFilterItem, 'label' | 'deleted'>;
type GroupOptionType = GroupOption<UsageFilterItem>;
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: (
<ModelsTable
key="models"
models={filters.models || []}
dateRange={dateRange}
scope={scope}
/>
)
},
{
key: 'users',
label: 'Users',
forceRender: true,
children: (
<UsersTable
key="users"
users={filters.users || []}
dateRange={dateRange}
scope={scope}
/>
)
},
{
key: 'api_keys',
label: 'API Keys',
forceRender: true,
children: (
<ApiKeysTable
key="api_keys"
apiKeys={filters.api_keys || []}
dateRange={dateRange}
scope={scope}
/>
)
}
].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: (
<ModelsTable
key="models"
models={models}
dateRange={dateRange}
scope={scope}
refreshKey={refreshKey}
/>
)
},
{
key: 'users',
label: 'Users',
forceRender: true,
children: (
<UsersTable
key="users"
users={users}
dateRange={dateRange}
scope={scope}
refreshKey={refreshKey}
/>
)
},
{
key: 'api_keys',
label: 'API Keys',
forceRender: true,
children: (
<ApiKeysTable
key="api_keys"
apiKeys={apiKeys}
dateRange={dateRange}
scope={scope}
refreshKey={refreshKey}
/>
)
}
].filter((item) => {
if (item.key === 'users') {
return scope === 'all';
}
return true;
});
}, [apiKeys, dateRange, models, refreshKey, scope, users]);
return (
<div style={{ marginTop: 16 }}>
+14 -61
View File
@@ -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<DailyUsageProps> = (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<DailyUsageProps> = (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<DailyUsageProps> = (props) => {
}));
}, [timeSeriesData, metric]);
const handleOnGroupByChange = (value: string) => {
onGroupByChange(value || null);
};
return (
<div>
<CardWrapper style={{ width: '100%', marginTop: 20 }}>
@@ -154,16 +107,18 @@ const DailyUsage: React.FC<DailyUsageProps> = (props) => {
value={metric}
popupMatchSelectWidth={false}
onChange={onMetricChange}
style={{ width: 180 }}
style={{ width: 'max-content' }}
/>
<BaseSelect
allowClear
variant="borderless"
prefix={<ControlLabel>Group by</ControlLabel>}
options={groupByOptions}
value={groupBy}
popupMatchSelectWidth={false}
onChange={onGroupByChange}
onChange={handleOnGroupByChange}
style={{ width: 'max-content' }}
/>
</div>
<Segmented
@@ -173,8 +128,6 @@ const DailyUsage: React.FC<DailyUsageProps> = (props) => {
onChange={onGranularityChange}
></Segmented>
</ControlsWrapper>
{/* <SimpleCard dataList={summaryCards} height={80} /> */}
<MixLineBar
chartData={chartData}
seriesData={[]}
+199 -31
View File
@@ -1,36 +1,88 @@
import AutoTooltip from '@/components/auto-tooltip';
import ModalFooter from '@/components/modal-footer';
import ScrollerModal from '@/components/scroller-modal';
import { exportJsonToExcel } from '@/utils/excel-reader';
import { useIntl } from '@umijs/max';
import { Table, TableColumnType } from 'antd';
import React, { useEffect } from 'react';
import { useUsageFilters } from '../hooks/use-usage-filters';
import useQueryBreakdownList from '../services/use-query-breakdown-list';
import getBreakdownRowKey from '../utils/get-breakdown-row-key';
import FilterBar from './filter-bar';
type DateType = 'date' | 'week' | 'month' | 'quarter' | 'year';
type ValueType = string | number | null;
const ExportData: React.FC<{
open: boolean;
onCancel: () => 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<any>[] = [
{
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 (
<AutoTooltip ghost>
<span className="text-primary">{text}</span>
</AutoTooltip>
);
}
},
{
title: intl.formatMessage({ id: 'dashboard.usage.export.user' }),
dataIndex: 'user_name',
title: 'Cluster',
dataIndex: ['model', 'identity', 'value', 'cluster_name'],
render: (text: string) => {
return <AutoTooltip ghost>{text}</AutoTooltip>;
}
},
{
title: intl.formatMessage({ id: 'dashboard.usage.export.model' }),
dataIndex: 'model_id',
dataIndex: ['model', 'identity', 'value', 'model_name'],
render: (text: string, record: any) => {
return (
<AutoTooltip ghost>
{record?.model?.identity?.value?.provider_name
? `${record?.model?.identity?.value?.provider_name}/${text}`
: text}
</AutoTooltip>
);
}
},
{
title: intl.formatMessage({ id: 'dashboard.usage.export.user' }),
dataIndex: ['user', 'label'],
render: (text: string) => {
return <AutoTooltip ghost>{text}</AutoTooltip>;
}
},
{
title: 'API Key',
dataIndex: ['api_key', 'label'],
render: (text: string, record: any) => {
return <AutoTooltip ghost>{text}</AutoTooltip>;
}
},
{
title: 'API Key',
dataIndex: 'api_key',
render: (text: string, record: any) => {
return <AutoTooltip ghost>{text}</AutoTooltip>;
}
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 (
<ScrollerModal
title={intl.formatMessage({ id: 'dashboard.usage.export' })}
@@ -105,17 +258,32 @@ const ExportData: React.FC<{
></ModalFooter>
}
>
<FilterBar {...filterBar} pageType="modal"></FilterBar>
<FilterBar
{...filterBar}
pageType="modal"
handlePickerChange={handlePickerChange}
></FilterBar>
<Table
columns={exportTableColumns}
tableLayout={'auto'}
style={{ width: '100%', marginTop: '16px', minHeight: 300 }}
dataSource={timeSeriesData.series || []}
loading={loading}
rowKey="id"
dataSource={dataSource.dataList || []}
rowKey={getBreakdownRowKey}
loading={{
spinning: loading,
size: 'middle'
}}
virtual
scroll={{ y: 400 }}
pagination={false}
pagination={{
size: 'small',
pageSize: pageParams.perPage,
current: pageParams.page,
total: dataSource.total || 0,
onChange: handlePageChange,
hideOnSinglePage: pageParams.perPage === 100,
showSizeChanger: true
}}
></Table>
</ScrollerModal>
);
+41 -16
View File
@@ -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<UsageFilterItem>[];
userOptions: OptionType[];
apiKeyOptions: GroupOption<UsageFilterItem>[];
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<FilterBarProps> = (props) => {
modelOptions,
userOptions,
apiKeyOptions,
handlePickerChange,
onDateChange,
onModelsChange,
onUsersChange,
@@ -107,12 +113,22 @@ const FilterBar: React.FC<FilterBarProps> = (props) => {
const exportMenuItems: MenuProps['items'] = [
{
key: 'chart',
label: 'Export Chart Data',
label: (
<span className="flex-center gap-8">
<IconFont type="icon-chart-01" style={{ fontSize: 14 }} />
<span>Export Chart Data</span>
</span>
),
onClick: onExportChart
},
{
key: 'table',
label: 'Export Table Data',
label: (
<span className="flex-center gap-8">
<IconFont type="icon-table" style={{ fontSize: 14 }} />
<span>Export Table Data</span>
</span>
),
onClick: onExportTable
}
];
@@ -187,6 +203,11 @@ const FilterBar: React.FC<FilterBarProps> = (props) => {
);
};
const onPickerChange = (picker: DateType) => {
handleOnPickerChange(picker);
handlePickerChange(picker);
};
const renderFooter = () => {
return (
<Segmented
@@ -208,19 +229,19 @@ const FilterBar: React.FC<FilterBarProps> = (props) => {
flex: 1
}
}}
onChange={handleOnPickerChange}
onChange={onPickerChange}
options={[
{
label: 'Day',
value: 'date'
},
{
label: 'Week',
value: 'week'
},
{
label: 'Month',
value: 'month'
},
{
label: 'Week',
value: 'week'
}
]}
></Segmented>
@@ -232,11 +253,18 @@ const FilterBar: React.FC<FilterBarProps> = (props) => {
return (
<span className="flex-center gap-8">
<span>{data.label}</span>
{data.isCurrent && <span className="text-tertiary"> [Current]</span>}
{data.isCurrent && (
<span className="text-tertiary"> [Current Account]</span>
)}
</span>
);
};
// only for dates greater than the current day should be disabled.
const disabledDate = (current: dayjs.Dayjs) => {
return current && current > dayjs().endOf('day');
};
return (
<div className={FilterBarCss.wrapper}>
<div className={FilterBarCss.filters}>
@@ -248,12 +276,11 @@ const FilterBar: React.FC<FilterBarProps> = (props) => {
]}
format={'YYYY-MM-DD'}
picker={picker}
disabledDate={disabledRangeDaysDate}
disabledDate={disabledDate}
presets={rangePresets}
allowClear={false}
style={{ width: 240 }}
onChange={onDateChange}
renderExtraFooter={renderFooter}
/>
<div
style={{
@@ -285,9 +312,7 @@ const FilterBar: React.FC<FilterBarProps> = (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<FilterBarProps> = (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<FilterBarProps> = (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<FilterBarProps> = (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 }
}}
+6 -5
View File
@@ -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<T, TChild>(
});
}
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());
@@ -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) => (
<AutoTooltip ghost style={{ maxWidth: 400 }}>
+3 -3
View File
@@ -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) => (
+66 -7
View File
@@ -16,6 +16,7 @@ const DefaultDateConfig = {
type UserOptionType = UsageFilterItem & {
value: string;
};
type ValueType = string | number | null;
type FilterOptionType = Omit<UsageFilterItem, 'label' | 'deleted'>;
type GroupOptionType = GroupOption<UsageFilterItem>;
@@ -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<ValueType[][]>(initialActiveModels);
const [activeApiKeys, setActiveApiKeys] =
useState<ValueType[][]>(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,
+2 -2
View File
@@ -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) => (
+37 -8
View File
@@ -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 (
<div>
<FilterBar
{...filterBar}
handleSearch={handleSearch}
handlePickerChange={handlePickerChange}
onExportTable={exportTable}
onExportChart={handleExportChart}
/>
@@ -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 = () => {
/>
<BreakdownTabs
filters={filters}
dateRange={{
start_date: commonFilters.start_date,
end_date: commonFilters.end_date
}}
dateRange={breakdownDateRange}
scope={commonFilters.scope}
refreshKey={breakdownRefreshKey}
></BreakdownTabs>
<ExportData
metaData={metaData}
granularity={chartFilters.granularity}
initialScope={commonFilters.scope}
initialCommonFilters={commonFilters}
open={openExportModal}
handlePickerChange={handlePickerChange}
onCancel={() => setOpenExportModal(false)}
initialState={{
activeModels: filterBar.activeModels,
activeApiKeys: filterBar.activeApiKeys
}}
></ExportData>
</div>
);
+16 -5
View File
@@ -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<{
<Table
columns={columns}
dataSource={dataSource.dataList}
rowKey={getBreakdownRowKey}
loading={{
spinning: loading,
size: 'middle'
}}
sortDirections={TABLE_SORT_DIRECTIONS}
showSorterTooltip={false}
rowKey="id"
onChange={handleTableChange}
pagination={{
size: 'middle',
+20 -9
View File
@@ -9,12 +9,14 @@ import { useEffect, useState } from 'react';
import { FilterOptionType } from '../config/types';
import useUsersColumns from '../hooks/use-models-columns';
import useQueryBreakdownList from '../services/use-query-breakdown-list';
import getBreakdownRowKey from '../utils/get-breakdown-row-key';
const Models: React.FC<{
models: FilterOptionType[];
dateRange: { start_date: string; end_date: string };
scope: string;
}> = ({ 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={<IconFont type="icon-key" />}
image={<IconFont type="icon-resources" />}
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' })}
></NoResult>
);
};
@@ -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<{
<Table
columns={columns}
dataSource={dataSource.dataList}
rowKey={getBreakdownRowKey}
loading={{
spinning: loading,
size: 'middle'
}}
sortDirections={TABLE_SORT_DIRECTIONS}
showSorterTooltip={false}
rowKey="id"
onChange={handleTableChange}
pagination={{
size: 'middle',
+20 -9
View File
@@ -9,12 +9,14 @@ import { useEffect, useState } from 'react';
import { FilterOptionType } from '../config/types';
import useUsersColumns from '../hooks/use-users-columns';
import useQueryBreakdownList from '../services/use-query-breakdown-list';
import getBreakdownRowKey from '../utils/get-breakdown-row-key';
const Users: React.FC<{
users: FilterOptionType[];
dateRange: { start_date: string; end_date: string };
scope: string;
}> = ({ 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={<IconFont type="icon-key" />}
image={<IconFont type="icon-users" />}
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' })}
></NoResult>
);
};
@@ -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<{
<Table
columns={columns}
dataSource={dataSource.dataList}
rowKey={getBreakdownRowKey}
loading={{
spinning: loading,
size: 'middle'
}}
sortDirections={TABLE_SORT_DIRECTIONS}
showSorterTooltip={false}
rowKey="id"
onChange={handleTableChange}
pagination={{
size: 'middle',
@@ -0,0 +1,35 @@
import { BreakdownItem } from '../config/types';
export const getBreakdownRowKey = (record: BreakdownItem): string => {
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;