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[] = [ { 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 ( {text} ); } }, { title: intl.formatMessage({ id: 'usage.filter.group.model' }), dataIndex: ['route', 'label'], render: (text: string, record: any) => { return ( {withDeletedMark( text, record?.route?.deleted, deletedWord, record?.route?.identity?.current?.route_id )} ); } }, { title: intl.formatMessage({ id: 'dashboard.usage.export.user' }), dataIndex: ['user', 'label'], render: (text: string, record: any) => { return ( {withDeletedMark( text, record?.user?.deleted, deletedWord, record?.user?.identity?.current?.user_id )} ); } }, { title: intl.formatMessage({ id: 'usage.filter.group.apikey' }), dataIndex: ['api_key', 'label'], render: (text: string, record: any) => { return ( {withDeletedMark( text, record?.api_key?.deleted, deletedWord, record?.api_key?.identity?.current?.api_key_id )} ); } }, { title: ( {intl.formatMessage({ id: 'usage.filter.inputTokens' })} ), width: 130, dataIndex: 'input_tokens', render: (text: string, record: any) => { return {text}; } }, { title: ( {intl.formatMessage({ id: 'usage.table.inputTokensCached' })} ), width: 150, dataIndex: 'input_cached_tokens', render: (text: string, record: any) => { return {text}; } }, { title: ( {intl.formatMessage({ id: 'usage.filter.outputTokens' })} ), dataIndex: 'output_tokens', render: (text: string, record: any) => { return {text}; } }, { title: ( {intl.formatMessage({ id: 'usage.filter.totalTokens' })} ), dataIndex: 'total_tokens', render: (text: string, record: any) => { return {text}; } }, { title: ( {intl.formatMessage({ id: 'usage.filter.apiRequests' })} ), dataIndex: 'api_requests', render: (text: string, record: any) => { return {text}; } } ]; // 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 ( } >
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 }} >
); }; export default ExportData;