413 lines
11 KiB
TypeScript
413 lines
11 KiB
TypeScript
import { AutoTooltip, ModalFooter, ScrollerModal } from '@gpustack/core-ui';
|
|
import { exportJsonToExcel } from '@gpustack/core-ui/excel';
|
|
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 { withDeletedMark } from '../utils/deleted-label';
|
|
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 INITIAL_PAGE_PARAMS = {
|
|
page: 1,
|
|
perPage: 100
|
|
};
|
|
|
|
const ExportData: React.FC<{
|
|
open: boolean;
|
|
onCancel: () => void;
|
|
initialScope: string;
|
|
metaData: any;
|
|
granularity: string;
|
|
initialState: {
|
|
activeRoutes: string[];
|
|
activeApiKeys: ValueType[][];
|
|
users: string[];
|
|
start_date: string;
|
|
end_date: string;
|
|
};
|
|
commonFilters: {
|
|
scope: string;
|
|
start_date: string;
|
|
end_date: string;
|
|
routes: string[];
|
|
users: string[];
|
|
api_keys: string[];
|
|
};
|
|
|
|
handlePickerChange: (picker: DateType) => void;
|
|
}> = (props) => {
|
|
const {
|
|
open,
|
|
onCancel,
|
|
initialScope,
|
|
metaData,
|
|
granularity,
|
|
handlePickerChange,
|
|
initialState
|
|
} = props || {};
|
|
const intl = useIntl();
|
|
const deletedWord = intl.formatMessage({ id: 'usage.table.deleted' });
|
|
|
|
// Members are forced to self scope, where the backend forbids grouping by
|
|
// user (privacy) — including it 403s the export request. Drop the user
|
|
// dimension (and its column) when we can't group by it.
|
|
const canGroupByUser = initialScope !== 'self';
|
|
const exportGroupBy = canGroupByUser
|
|
? ['date', 'user', 'route', 'api_key']
|
|
: ['date', 'route', 'api_key'];
|
|
|
|
const [pageParams, setPageParams] = React.useState<{
|
|
page: number;
|
|
perPage: number;
|
|
}>(INITIAL_PAGE_PARAMS);
|
|
|
|
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: 'day',
|
|
sort_by: '-date',
|
|
group_by: exportGroupBy,
|
|
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,
|
|
render(text: any, row: any, index: number) {
|
|
return index + 1;
|
|
}
|
|
},
|
|
{
|
|
title: intl.formatMessage({ id: 'dashboard.usage.export.date' }),
|
|
dataIndex: ['date', 'label'],
|
|
render: (text: string) => {
|
|
return (
|
|
<AutoTooltip ghost>
|
|
<span className="text-primary">{text}</span>
|
|
</AutoTooltip>
|
|
);
|
|
}
|
|
},
|
|
{
|
|
title: intl.formatMessage({ id: 'usage.filter.group.model' }),
|
|
dataIndex: ['route', 'label'],
|
|
render: (text: string, record: any) => {
|
|
return (
|
|
<AutoTooltip ghost>
|
|
{withDeletedMark(
|
|
text,
|
|
record?.route?.deleted,
|
|
deletedWord,
|
|
record?.route?.identity?.current?.route_id
|
|
)}
|
|
</AutoTooltip>
|
|
);
|
|
}
|
|
},
|
|
{
|
|
title: intl.formatMessage({ id: 'dashboard.usage.export.user' }),
|
|
dataIndex: ['user', 'label'],
|
|
render: (text: string, record: any) => {
|
|
return (
|
|
<AutoTooltip ghost>
|
|
{withDeletedMark(
|
|
text,
|
|
record?.user?.deleted,
|
|
deletedWord,
|
|
record?.user?.identity?.current?.user_id
|
|
)}
|
|
</AutoTooltip>
|
|
);
|
|
}
|
|
},
|
|
{
|
|
title: intl.formatMessage({ id: 'usage.filter.group.apikey' }),
|
|
dataIndex: ['api_key', 'label'],
|
|
render: (text: string, record: any) => {
|
|
return (
|
|
<AutoTooltip ghost>
|
|
{withDeletedMark(
|
|
text,
|
|
record?.api_key?.deleted,
|
|
deletedWord,
|
|
record?.api_key?.identity?.current?.api_key_id
|
|
)}
|
|
</AutoTooltip>
|
|
);
|
|
}
|
|
},
|
|
{
|
|
title: (
|
|
<AutoTooltip ghost>
|
|
{intl.formatMessage({ id: 'usage.filter.inputTokens' })}
|
|
</AutoTooltip>
|
|
),
|
|
width: 130,
|
|
dataIndex: 'input_tokens',
|
|
render: (text: string, record: any) => {
|
|
return <AutoTooltip ghost>{text}</AutoTooltip>;
|
|
}
|
|
},
|
|
{
|
|
title: (
|
|
<AutoTooltip ghost>
|
|
{intl.formatMessage({ id: 'usage.table.inputTokensCached' })}
|
|
</AutoTooltip>
|
|
),
|
|
width: 150,
|
|
dataIndex: 'input_cached_tokens',
|
|
render: (text: string, record: any) => {
|
|
return <AutoTooltip ghost>{text}</AutoTooltip>;
|
|
}
|
|
},
|
|
{
|
|
title: (
|
|
<AutoTooltip ghost>
|
|
{intl.formatMessage({ id: 'usage.filter.outputTokens' })}
|
|
</AutoTooltip>
|
|
),
|
|
dataIndex: 'output_tokens',
|
|
render: (text: string, record: any) => {
|
|
return <AutoTooltip ghost>{text}</AutoTooltip>;
|
|
}
|
|
},
|
|
{
|
|
title: (
|
|
<AutoTooltip ghost>
|
|
{intl.formatMessage({ id: 'usage.filter.totalTokens' })}
|
|
</AutoTooltip>
|
|
),
|
|
dataIndex: 'total_tokens',
|
|
render: (text: string, record: any) => {
|
|
return <AutoTooltip ghost>{text}</AutoTooltip>;
|
|
}
|
|
},
|
|
{
|
|
title: (
|
|
<AutoTooltip ghost>
|
|
{intl.formatMessage({ id: 'usage.filter.apiRequests' })}
|
|
</AutoTooltip>
|
|
),
|
|
dataIndex: 'api_requests',
|
|
render: (text: string, record: any) => {
|
|
return <AutoTooltip ghost>{text}</AutoTooltip>;
|
|
}
|
|
}
|
|
];
|
|
|
|
// Hide the User column when we can't group by user (self scope) — it'd be
|
|
// empty otherwise.
|
|
const visibleColumns = canGroupByUser
|
|
? exportTableColumns
|
|
: exportTableColumns.filter(
|
|
(c) => !(Array.isArray(c.dataIndex) && c.dataIndex[0] === 'user')
|
|
);
|
|
|
|
const handleSubmit = () => {
|
|
exportJsonToExcel({
|
|
fileName: `usage_export_${commonFilters.start_date}_${commonFilters.end_date}.xlsx`,
|
|
sheets: [
|
|
{
|
|
jsonData: (dataSource.dataList || []).map((item: any) => ({
|
|
date: item?.date?.label,
|
|
user: withDeletedMark(
|
|
item?.user?.label ?? '',
|
|
item?.user?.deleted,
|
|
deletedWord,
|
|
item?.user?.identity?.current?.user_id
|
|
),
|
|
route: withDeletedMark(
|
|
item?.route?.label ?? '',
|
|
item?.route?.deleted,
|
|
deletedWord,
|
|
item?.route?.identity?.current?.route_id
|
|
),
|
|
api_key: withDeletedMark(
|
|
item?.api_key?.label ?? '',
|
|
item?.api_key?.deleted,
|
|
deletedWord,
|
|
item?.api_key?.identity?.current?.api_key_id
|
|
),
|
|
input_tokens: item?.input_tokens,
|
|
input_cached_tokens: item?.input_cached_tokens,
|
|
output_tokens: item?.output_tokens,
|
|
total_tokens: item?.total_tokens,
|
|
api_requests: item?.api_requests
|
|
})),
|
|
sheetName: 'usage',
|
|
fields: [
|
|
'date',
|
|
...(canGroupByUser ? ['user'] : []),
|
|
'route',
|
|
'api_key',
|
|
'input_tokens',
|
|
'input_cached_tokens',
|
|
'output_tokens',
|
|
'total_tokens',
|
|
'api_requests'
|
|
],
|
|
fieldLabels: {
|
|
date: intl.formatMessage({ id: 'dashboard.usage.export.date' }),
|
|
user: intl.formatMessage({ id: 'dashboard.usage.export.user' }),
|
|
route: intl.formatMessage({ id: 'usage.filter.group.model' }),
|
|
api_key: intl.formatMessage({ id: 'usage.filter.group.apikey' }),
|
|
input_tokens: intl.formatMessage({
|
|
id: 'usage.filter.inputTokens'
|
|
}),
|
|
input_cached_tokens: intl.formatMessage({
|
|
id: 'usage.table.inputTokensCached'
|
|
}),
|
|
output_tokens: intl.formatMessage({
|
|
id: 'usage.filter.outputTokens'
|
|
}),
|
|
total_tokens: intl.formatMessage({
|
|
id: 'usage.filter.totalTokens'
|
|
}),
|
|
api_requests: intl.formatMessage({ id: 'usage.filter.apiRequests' })
|
|
},
|
|
formatMap: {}
|
|
}
|
|
]
|
|
});
|
|
};
|
|
|
|
const handlePageChange = (page: number, pageSize: number) => {
|
|
setPageParams({ page, perPage: pageSize });
|
|
fetchExportData({
|
|
...pageParams,
|
|
page,
|
|
perPage: pageSize,
|
|
granularity: 'day',
|
|
group_by: exportGroupBy,
|
|
filters,
|
|
sort_by: '-date',
|
|
scope: initialScope,
|
|
start_date: commonFilters.start_date,
|
|
end_date: commonFilters.end_date
|
|
});
|
|
};
|
|
|
|
const handleOnCancel = () => {
|
|
onCancel?.();
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (open) {
|
|
setPageParams(INITIAL_PAGE_PARAMS);
|
|
fetchExportData({
|
|
...INITIAL_PAGE_PARAMS,
|
|
granularity: 'day',
|
|
group_by: exportGroupBy,
|
|
filters,
|
|
sort_by: '-date',
|
|
scope: initialScope,
|
|
start_date: commonFilters.start_date,
|
|
end_date: commonFilters.end_date
|
|
});
|
|
} else {
|
|
cancelRequest();
|
|
}
|
|
}, [open]);
|
|
|
|
return (
|
|
<ScrollerModal
|
|
title={intl.formatMessage({ id: 'dashboard.usage.export' })}
|
|
open={open}
|
|
centered={false}
|
|
onCancel={handleOnCancel}
|
|
destroyOnHidden={true}
|
|
closeIcon={true}
|
|
mask={{
|
|
closable: false
|
|
}}
|
|
keyboard={false}
|
|
width={1280}
|
|
style={{
|
|
top: '10%'
|
|
}}
|
|
footer={
|
|
<ModalFooter
|
|
onOk={handleSubmit}
|
|
onCancel={handleOnCancel}
|
|
okText={intl.formatMessage({ id: 'common.button.export' })}
|
|
></ModalFooter>
|
|
}
|
|
>
|
|
<div
|
|
style={{
|
|
position: 'sticky',
|
|
top: 0,
|
|
zIndex: 1,
|
|
backgroundColor: 'var(--ant-color-bg-elevated)',
|
|
paddingBottom: 8
|
|
}}
|
|
>
|
|
<FilterBar
|
|
{...filterBar}
|
|
pageType="modal"
|
|
handlePickerChange={handlePickerChange}
|
|
></FilterBar>
|
|
</div>
|
|
<Table
|
|
columns={visibleColumns}
|
|
className={'scroll-table'}
|
|
tableLayout={'auto'}
|
|
style={{ width: '100%', marginTop: '16px', minHeight: 400 }}
|
|
dataSource={dataSource.dataList || []}
|
|
rowKey={(record) => getBreakdownRowKey(record, 'export')}
|
|
loading={{
|
|
spinning: loading,
|
|
size: 'middle'
|
|
}}
|
|
virtual
|
|
scroll={{ y: 400 }}
|
|
pagination={{
|
|
size: 'small',
|
|
pageSize: pageParams.perPage,
|
|
current: pageParams.page,
|
|
total: dataSource.total || 0,
|
|
onChange: handlePageChange,
|
|
hideOnSinglePage: pageParams.perPage === 100,
|
|
showSizeChanger: true
|
|
}}
|
|
></Table>
|
|
</ScrollerModal>
|
|
);
|
|
};
|
|
|
|
export default ExportData;
|