diff --git a/src/components/card-wrapper/simple-card.tsx b/src/components/card-wrapper/simple-card.tsx index 624644fd..fbf4f5e4 100644 --- a/src/components/card-wrapper/simple-card.tsx +++ b/src/components/card-wrapper/simple-card.tsx @@ -68,12 +68,14 @@ export const SimpleCardItem: React.FC<{
{title}
- + {iconType && ( + + )} {content}
@@ -89,8 +91,12 @@ export const SimpleCard: React.FC<{ }[]; height?: string | number; bordered?: boolean; + styles?: { + wrapper?: React.CSSProperties; + item?: React.CSSProperties; + }; }> = (props) => { - const { dataList, bordered } = props; + const { dataList, bordered, styles } = props; return ( @@ -102,6 +108,9 @@ export const SimpleCard: React.FC<{ bordered={bordered} color={item.color} iconType={item.iconType} + style={{ + ...styles?.item + }} > ))} diff --git a/src/pages/dashboard/hooks/use-rangepicker-preset.ts b/src/pages/dashboard/hooks/use-rangepicker-preset.ts index d2308c3a..024756b9 100644 --- a/src/pages/dashboard/hooks/use-rangepicker-preset.ts +++ b/src/pages/dashboard/hooks/use-rangepicker-preset.ts @@ -1,6 +1,7 @@ import { useIntl } from '@umijs/max'; import { DatePickerProps } from 'antd'; import dayjs, { type Dayjs } from 'dayjs'; +import { useState } from 'react'; interface RangePickerPreset { range: number; @@ -17,10 +18,17 @@ export default function useRangePickerPreset(options?: RangePickerPreset): { label: React.ReactNode; value: [Dayjs, Dayjs] | (() => [Dayjs, Dayjs]); }[]; + handleOnPickerChange: ( + value: 'date' | 'week' | 'month' | 'quarter' | 'year' + ) => void; + picker: 'date' | 'week' | 'month' | 'quarter' | 'year'; range: number; } { const { range = 60, disabledDate, presetRanges } = options || {}; const intl = useIntl(); + const [picker, setPicker] = useState< + 'date' | 'week' | 'month' | 'quarter' | 'year' + >('month'); const getYearMonth = (date: Dayjs) => date.year() * 12 + date.month(); @@ -55,6 +63,12 @@ export default function useRangePickerPreset(options?: RangePickerPreset): { return false; }; + const handleOnPickerChange = ( + value: 'date' | 'week' | 'month' | 'quarter' | 'year' + ) => { + setPicker(value); + }; + const rangePresets: { label: React.ReactNode; value: [Dayjs, Dayjs] | (() => [Dayjs, Dayjs]); @@ -79,7 +93,9 @@ export default function useRangePickerPreset(options?: RangePickerPreset): { return { disabledRangeDaysDate, + handleOnPickerChange, rangePresets, + picker, range }; } diff --git a/src/pages/usage/components/daily-usage.tsx b/src/pages/usage/components/daily-usage.tsx index de1d8c20..c8627578 100644 --- a/src/pages/usage/components/daily-usage.tsx +++ b/src/pages/usage/components/daily-usage.tsx @@ -1,10 +1,9 @@ import CardWrapper from '@/components/card-wrapper'; -import { SimpleCard } from '@/components/card-wrapper/simple-card'; 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 { Divider, Segmented } from 'antd'; +import { Segmented } from 'antd'; import dayjs from 'dayjs'; import React, { useMemo } from 'react'; import styled from 'styled-components'; @@ -151,7 +150,7 @@ const DailyUsage: React.FC = (props) => {
Metric} options={metricOptions} value={metric} @@ -160,7 +159,7 @@ const DailyUsage: React.FC = (props) => { /> Group by} options={groupByOptions} value={groupBy} @@ -176,9 +175,9 @@ const DailyUsage: React.FC = (props) => { onChange={onGranularityChange} > - + {/* */} - + {/* */} void; + initialScope: string; + metaData: any; +}> = (props) => { + const { open, onCancel, initialScope, metaData } = props || {}; + const intl = useIntl(); + + const { + filters, + commonFilters, + fetchData, + loading, + timeSeriesData, + filterBar + } = useUsageFilters({ + initialScope: initialScope, + metaData, + chartFilters: {}, + summaryColumns: {} + }); + + 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' + }, + { + title: intl.formatMessage({ id: 'dashboard.usage.export.user' }), + dataIndex: 'user_name', + render: (text: string) => { + return {text}; + } + }, + { + title: intl.formatMessage({ id: 'dashboard.usage.export.model' }), + dataIndex: 'model_id', + render: (text: string, record: any) => { + return {text}; + } + }, + + { + title: 'API Key', + dataIndex: 'api_key', + render: (text: string, record: any) => { + return {text}; + } + } + ]; + + const handleSubmit = () => { + filterBar.onExportChart(); + }; + + const handleOnCancel = () => { + onCancel?.(); + }; + + useEffect(() => { + if (open) { + fetchData(commonFilters, {}); + } else { + } + }, [open]); + + return ( + + } + > + +
+
+ ); +}; + +export default ExportData; diff --git a/src/pages/usage/components/filter-bar.tsx b/src/pages/usage/components/filter-bar.tsx index bcac5793..7ba3eab4 100644 --- a/src/pages/usage/components/filter-bar.tsx +++ b/src/pages/usage/components/filter-bar.tsx @@ -6,7 +6,7 @@ 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 { Button, DatePicker, Dropdown, MenuProps, Segmented } from 'antd'; import dayjs from 'dayjs'; import React from 'react'; import { GroupOption } from '../config'; @@ -22,6 +22,7 @@ type OptionType = UsageFilterItem & { value: string; }; interface FilterBarProps { + pageType?: 'page' | 'modal'; scope: string; startDate: string; endDate: string; @@ -38,22 +39,20 @@ interface FilterBarProps { onApiKeysChange: (value: string[]) => void; onExport?: () => void; handleSearch?: () => void; - onExportChart: () => void; - onExportTable: () => void; + onExportChart?: () => void; + onExportTable?: () => void; } const FilterBar: React.FC = (props) => { const { - scope, + pageType = 'page', startDate, endDate, - selectedModels, selectedUsers, selectedApiKeys, modelOptions, userOptions, apiKeyOptions, - onScopeChange, onDateChange, onModelsChange, onUsersChange, @@ -63,41 +62,40 @@ const FilterBar: React.FC = (props) => { handleSearch } = props; const intl = useIntl(); - const { disabledRangeDaysDate, rangePresets } = useRangePickerPreset({ - range: DefaultDateConfig.maxRange, - disabledDate: true, - presetRanges: [ - { - label: intl.formatMessage({ - id: 'dashboard.usage.datePicker.last7days' - }), - value: [dayjs().add(-6, 'd'), dayjs()] - }, - { - label: intl.formatMessage({ - id: 'dashboard.usage.datePicker.last30days' - }), - value: [dayjs().add(-29, 'd'), dayjs()] - }, - { - label: intl.formatMessage({ - id: 'dashboard.usage.datePicker.last60days' - }), - value: [dayjs().add(-59, 'd'), dayjs()] - }, - { - label: intl.formatMessage({ - id: 'dashboard.usage.datePicker.last90days' - }), - value: [dayjs().add(-89, 'd'), dayjs()] - } - ] - }); + const { disabledRangeDaysDate, rangePresets, picker, handleOnPickerChange } = + useRangePickerPreset({ + range: DefaultDateConfig.maxRange, + disabledDate: true, + presetRanges: [ + { + label: intl.formatMessage({ + id: 'dashboard.usage.datePicker.last7days' + }), + value: [dayjs().add(-6, 'd'), dayjs()] + }, + { + label: intl.formatMessage({ + id: 'dashboard.usage.datePicker.last30days' + }), + value: [dayjs().add(-29, 'd'), dayjs()] + }, + { + label: intl.formatMessage({ + id: 'dashboard.usage.datePicker.last60days' + }), + value: [dayjs().add(-59, 'd'), dayjs()] + }, + { + label: intl.formatMessage({ + id: 'dashboard.usage.datePicker.last90days' + }), + value: [dayjs().add(-89, 'd'), dayjs()] + } + ] + }); const [activeModels, setActiveModels] = React.useState([]); const [activeApiKeys, setActiveApiKeys] = React.useState([]); - const [activeUserApiKeys, setActiveUserApiKeys] = React.useState( - [] - ); + const initialInfo = useModel('@@initialState'); const { initialState } = initialInfo || {}; @@ -184,6 +182,46 @@ const FilterBar: React.FC = (props) => { ); }; + const renderFooter = () => { + return ( + + ); + }; + return (
@@ -193,12 +231,14 @@ const FilterBar: React.FC = (props) => { dayjs().add(-DefaultDateConfig.defaultRange, 'd'), dayjs() ]} + picker={picker} disabledDate={disabledRangeDaysDate} presets={rangePresets} allowClear={false} style={{ width: 240 }} value={[dayjs(startDate), dayjs(endDate)]} onChange={onDateChange} + renderExtraFooter={renderFooter} />
= (props) => { icon={} >
- -
); }; diff --git a/src/pages/usage/hooks/use-usage-filters.ts b/src/pages/usage/hooks/use-usage-filters.ts new file mode 100644 index 00000000..4dee8f69 --- /dev/null +++ b/src/pages/usage/hooks/use-usage-filters.ts @@ -0,0 +1,207 @@ +import { exportJsonToExcel } from '@/utils/excel-reader'; +import dayjs from 'dayjs'; +import { useMemo, useState } from 'react'; +import { + generateColumns, + GroupOption, + transformTimelineToTable +} from '../config'; +import { UsageFilterItem } from '../config/types'; +import useQueryTimeSeriesData from '../services/use-query-timeseries-data'; + +const DefaultDateConfig = { + defaultRange: 29 +}; + +type UserOptionType = UsageFilterItem & { + value: string; +}; + +type FilterOptionType = Omit; +type GroupOptionType = GroupOption; + +interface UseUsageFiltersParams { + initialScope: string; + metaData: { + models?: GroupOptionType[]; + users?: UserOptionType[]; + api_keys?: GroupOptionType[]; + }; + chartFilters: { + metric: string; + group_by: string; + granularity: string; + }; + summaryColumns: { + title: string; + dataIndex: string; + key: string; + }[]; +} + +export const useUsageFilters = ({ + initialScope, + metaData, + chartFilters, + summaryColumns +}: UseUsageFiltersParams) => { + const { + detailData: timeSeriesData, + loading, + fetchData: fetchTimeSeriesData + } = useQueryTimeSeriesData(); + const [commonFilters, setCommonFilters] = useState({ + scope: initialScope, + models: [] as string[], + users: [] as string[], + api_keys: [] as string[], + start_date: dayjs() + .subtract(DefaultDateConfig.defaultRange, 'days') + .format('YYYY-MM-DD'), + end_date: dayjs().format('YYYY-MM-DD') + }); + + const modelOptions = metaData?.models || []; + const userOptions = metaData?.users || []; + const apiKeyOptions = metaData?.api_keys || []; + + const buildFilters = (selected: typeof commonFilters) => { + const filters: { + models?: FilterOptionType[]; + users?: FilterOptionType[]; + api_keys?: FilterOptionType[]; + } = {}; + + if (selected.models.length > 0) { + const modelsSet = new Set(selected.models); + filters.models = modelOptions + .flatMap((group) => + group.children.filter((item) => modelsSet.has(item.value)) + ) + .map((item) => ({ + identity: item.identity + })); + } + + if (selected.users.length > 0) { + filters.users = userOptions + .filter((item) => selected.users.includes(item.value || '')) + .map((item) => ({ + identity: item.identity + })); + } + + if (selected.api_keys.length > 0) { + const apiKeysSet = new Set(selected.api_keys); + filters.api_keys = apiKeyOptions + .flatMap((group) => + group.children.filter((item) => apiKeysSet.has(item.value)) + ) + .map((item) => ({ + identity: item.identity + })); + } + + return filters; + }; + + const filters = useMemo( + () => buildFilters(commonFilters), + [commonFilters, modelOptions, userOptions, apiKeyOptions] + ); + + const fetchData = ( + currentSelectedFilters = commonFilters, + currentChartFilters = chartFilters + ) => { + fetchTimeSeriesData({ + ...currentChartFilters, + start_date: currentSelectedFilters.start_date, + end_date: currentSelectedFilters.end_date, + filters: buildFilters(currentSelectedFilters) + }); + }; + + const handleScopeChange = (value: string) => { + const next = { ...commonFilters, scope: value }; + setCommonFilters(next); + fetchData(next, chartFilters); + }; + + const handleDateChange = (_: any, dateStrings: [string, string]) => { + const [start_date, end_date] = dateStrings; + const next = { ...commonFilters, start_date, end_date }; + setCommonFilters(next); + fetchData(next, chartFilters); + }; + + const handleFilterChange = ( + type: 'models' | 'users' | 'api_keys', + value: string[] + ) => { + const next = { ...commonFilters, [type]: value }; + setCommonFilters(next); + fetchData(next, chartFilters); + }; + + const handleSearch = () => { + fetchData(commonFilters, chartFilters); + }; + + const handleOnExportChart = () => { + const columns = generateColumns(timeSeriesData?.series || []); + const tableData = transformTimelineToTable(timeSeriesData?.series || []); + + exportJsonToExcel({ + fileName: `${chartFilters.metric}_${chartFilters.group_by}_${chartFilters.granularity}_${commonFilters.start_date}_${commonFilters.end_date}.xlsx`, + sheets: [ + { + jsonData: tableData, + sheetName: chartFilters.metric, + fields: columns.map((col) => col.dataIndex).filter(Boolean), + fieldLabels: Object.fromEntries( + columns.map((col) => [col.dataIndex, col.title]) + ), + formatMap: {} + }, + { + jsonData: [timeSeriesData?.summary || {}], + sheetName: 'summary', + fields: summaryColumns.map((col) => col.dataIndex), + fieldLabels: Object.fromEntries( + summaryColumns.map((col) => [col.dataIndex, col.title]) + ), + formatMap: {} + } + ] + }); + }; + + const filterBar = { + scope: commonFilters.scope, + startDate: commonFilters.start_date, + endDate: commonFilters.end_date, + selectedModels: commonFilters.models, + selectedUsers: commonFilters.users, + selectedApiKeys: commonFilters.api_keys, + modelOptions, + userOptions, + apiKeyOptions, + handleSearch, + onScopeChange: handleScopeChange, + onDateChange: handleDateChange, + onModelsChange: (value: string[]) => handleFilterChange('models', value), + onUsersChange: (value: string[]) => handleFilterChange('users', value), + onApiKeysChange: (value: string[]) => handleFilterChange('api_keys', value), + onExportChart: handleOnExportChart + }; + + return { + filters, + loading, + commonFilters, + timeSeriesData, + fetchData, + filterBar + }; +}; diff --git a/src/pages/usage/index.tsx b/src/pages/usage/index.tsx index c930bbb2..0c65a9ac 100644 --- a/src/pages/usage/index.tsx +++ b/src/pages/usage/index.tsx @@ -1,31 +1,21 @@ -import { exportJsonToExcel } from '@/utils/excel-reader'; +import { SimpleCard } from '@/components/card-wrapper/simple-card'; +import { baseColorMap } from '@/pages/dashboard/config'; +import { formatLargeNumber } from '@/utils'; import { useModel } from '@@/plugin-model'; -import dayjs from 'dayjs'; import React, { useEffect, useMemo, useState } from 'react'; import BreakdownTabs from './components/breakdown-tabs'; import DailyUsage from './components/daily-usage'; +import ExportData from './components/export-data'; import FilterBar from './components/filter-bar'; -import { - generateColumns, - GroupOption, - transformTimelineToTable -} from './config'; -import { UsageFilterItem } from './config/types'; import useExportTable from './hooks/use-export-table'; +import { useUsageFilters } from './hooks/use-usage-filters'; import useQueryUsageMetaData from './services/use-query-meta-data'; -import useQueryTimeSeriesData from './services/use-query-timeseries-data'; - -const DefaultDateConfig = { - defaultRange: 29 -}; - -type FilterOptionType = Omit; -type GroupOptionType = GroupOption; const Usage: React.FC = () => { const initialInfo = useModel('@@initialState'); const { initialState } = initialInfo || {}; const { exportTable } = useExportTable(); + const [openExportModal, setOpenExportModal] = useState(false); const summaryColumns = [ { @@ -64,195 +54,98 @@ const Usage: React.FC = () => { group_by: 'model', granularity: 'day' }); - const [commonFilters, setCommonFilters] = useState<{ - models: string[]; - users: string[]; - api_keys: string[]; - start_date: string; - end_date: string; - scope: string; - }>({ - scope: initialState?.currentUser?.is_admin ? 'all' : 'self', - models: [], - users: [], - api_keys: [], - start_date: dayjs() - .subtract(DefaultDateConfig.defaultRange, 'days') - .format('YYYY-MM-DD'), - end_date: dayjs().format('YYYY-MM-DD') - }); const { detailData: metaData, fetchData: fetchMetaData } = useQueryUsageMetaData(); - const { detailData: timeSeriesData, fetchData: fetchTimeSeriesData } = - useQueryTimeSeriesData(); - const modelOptions = metaData?.models || []; - const userOptions = metaData?.users || []; - const apiKeyOptions = metaData?.api_keys || []; - - const buildFilters = (selected: { - models: string[]; - users: string[]; - api_keys: string[]; - }) => { - const filters: { - models?: FilterOptionType[]; - users?: FilterOptionType[]; - api_keys?: FilterOptionType[]; - } = {}; - - if (selected.models?.length > 0) { - const modelsSet = new Set(selected.models); - // filter the options - filters.models = modelOptions - .flatMap((group) => - group.children.filter((item) => modelsSet.has(item.value)) - ) - .map((item) => ({ - identity: item.identity - })); - } - - if (selected.users?.length > 0) { - filters.users = userOptions - .filter((item) => selected.users?.includes(item.value || '')) - .map((item) => ({ - identity: item.identity - })); - } - - if (selected.api_keys?.length > 0) { - const apiKeysSet = new Set(selected.api_keys); - filters.api_keys = apiKeyOptions - .flatMap((group) => - group.children.filter((item) => apiKeysSet.has(item.value)) - ) - .map((item) => ({ - identity: item.identity - })); - } - - return filters; - }; - - const filters = useMemo( - () => buildFilters(commonFilters), - [commonFilters, modelOptions, userOptions, apiKeyOptions] - ); - - const fetchData = ( - currentSelectedFilters = commonFilters, - currentChartFilters = chartFilters - ) => { - fetchTimeSeriesData({ - ...currentChartFilters, - start_date: currentSelectedFilters.start_date, - end_date: currentSelectedFilters.end_date, - filters: buildFilters(currentSelectedFilters) + const { filters, commonFilters, fetchData, timeSeriesData, filterBar } = + useUsageFilters({ + initialScope: initialState?.currentUser?.is_admin ? 'all' : 'self', + metaData, + chartFilters, + summaryColumns }); - }; useEffect(() => { fetchMetaData(); fetchData(commonFilters, chartFilters); }, []); - const handleScopeChange = (value: string) => { - setCommonFilters((prev) => ({ ...prev, scope: value })); - fetchData( - { - ...commonFilters, - scope: value - }, - chartFilters - ); - }; - - const handleDateChange = (_dates: any, dateStrings: [string, string]) => { - const [start_date, end_date] = dateStrings; - setCommonFilters((prev) => ({ ...prev, start_date, end_date })); - fetchData({ ...commonFilters, start_date, end_date }, chartFilters); - }; - - const handleFilterChange = (type: string, value: string[]) => { - console.log('handleFilterChange:', type, value); - setCommonFilters((prev) => ({ ...prev, [type]: value })); - fetchData({ ...commonFilters, [type]: value }, chartFilters); - }; - const handleChartFilterChange = (type: string, value: string) => { setChartFilters((prev) => ({ ...prev, [type]: value })); fetchData(commonFilters, { ...chartFilters, [type]: value }); }; - const handleSearch = () => { - fetchData(commonFilters, chartFilters); - }; + const summaryCards = useMemo(() => { + const summary = timeSeriesData?.summary || { + input_tokens: 0, + output_tokens: 0, + total_tokens: 0, + api_requests: 0, + models_called: 0 + }; + 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' + } + ]; + }, [timeSeriesData.summary]); - console.log('timeSeriesData:', timeSeriesData); - - const handleOnExportChart = () => { - const columns = generateColumns(timeSeriesData.series); - const tableData = transformTimelineToTable(timeSeriesData.series); - exportJsonToExcel({ - fileName: `${chartFilters.metric}_${chartFilters.group_by}_${chartFilters.granularity}_${commonFilters.start_date}_${commonFilters.end_date}.xlsx`, - sheets: [ - { - jsonData: tableData || [], - sheetName: `${chartFilters.metric}`, - fields: columns - .map((col) => col.dataIndex) - .filter(Boolean) as string[], - fieldLabels: columns.reduce( - (map, col) => { - map[col.dataIndex] = col.title; - return map; - }, - {} as Record - ), - formatMap: {} - }, - { - jsonData: [timeSeriesData.summary || {}], - sheetName: `summary`, - fields: summaryColumns - .map((col) => col.dataIndex) - .filter(Boolean) as string[], - fieldLabels: summaryColumns.reduce( - (map, col) => { - map[col.dataIndex] = col.title; - return map; - }, - {} as Record - ), - formatMap: {} - } - ] - }); + const handleExportChart = () => { + setOpenExportModal(true); }; return (
handleFilterChange('models', value)} - onUsersChange={(value) => handleFilterChange('users', value)} - onApiKeysChange={(value) => handleFilterChange('api_keys', value)} - onExportChart={handleOnExportChart} + {...filterBar} onExportTable={exportTable} + onExportChart={handleExportChart} /> +
+ +
{ }} scope={commonFilters.scope} > + setOpenExportModal(false)} + >
); };