diff --git a/src/components/echarts/config.ts b/src/components/echarts/config.ts index 9f8d7477..de0c7742 100644 --- a/src/components/echarts/config.ts +++ b/src/components/echarts/config.ts @@ -41,24 +41,35 @@ export default function useChartConfig() { borderColor: 'transparent', formatter(params: any, callback?: (val: any) => any) { let result = `${params[0].axisValue}`; + let visibleItemCount = 0; params.forEach((item: any) => { let value = isFunction(callback) ? callback?.(item.data.value) - : item.data.value; + : item.data?.value; + if (value === null || value === undefined) { + return; + } + + visibleItemCount += 1; const borderRadius = item.seriesType === 'bar' ? '2px' : '8px'; result += ` - - ${item.seriesName}: + + ${item.seriesName}: ${value} `; }); - return `
${result}
`; + const wrapperClassName = + visibleItemCount >= 12 + ? 'tooltip-wrapper tooltip-grid' + : 'tooltip-wrapper'; + + return `
${result}
`; } }; diff --git a/src/global.less b/src/global.less index 6eede573..903a1062 100644 --- a/src/global.less +++ b/src/global.less @@ -744,7 +744,7 @@ body { font-size: 11px; background-color: transparent; min-width: 100px; - max-width: 360px; + max-width: 500px; &.scatter { display: grid; @@ -752,6 +752,16 @@ body { gap: 8px; } + &.tooltip-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 4px 12px; + + .tooltip-x-name { + grid-column: 1 / -1; + } + } + .tooltip-x-name { font-size: var(--font-size-small); color: var(--ant-color-text-tertiary); @@ -766,12 +776,34 @@ body { justify-content: space-between; align-items: center; + .tooltip-item-name { + display: inline-flex; + align-items: center; + min-width: 0; + flex: 1; + overflow: hidden; + } + + .tooltip-item-dot { + display: inline-block; + width: 8px; + height: 8px; + margin-right: 5px; + flex-shrink: 0; + } + .tooltip-item-title { margin-right: 2px; + display: inline-block; + max-width: 200px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } .tooltip-value { margin-left: 10px; + flex-shrink: 0; color: var(--ant-color-text); text-overflow: ellipsis; overflow: hidden; diff --git a/src/locales/en-US/usage.ts b/src/locales/en-US/usage.ts index 607b4b3d..0b2a0814 100644 --- a/src/locales/en-US/usage.ts +++ b/src/locales/en-US/usage.ts @@ -27,5 +27,6 @@ export default { 'usage.export.table': 'Export Table Data', 'usage.user.currentAccount': 'Current Account', 'usage.table.cluster': 'Cluster', - 'usage.table.provider': 'Provider' + 'usage.table.provider': 'Provider', + 'usage.table.deleted': 'Deleted' }; diff --git a/src/locales/ja-JP/usage.ts b/src/locales/ja-JP/usage.ts index 607b4b3d..0b2a0814 100644 --- a/src/locales/ja-JP/usage.ts +++ b/src/locales/ja-JP/usage.ts @@ -27,5 +27,6 @@ export default { 'usage.export.table': 'Export Table Data', 'usage.user.currentAccount': 'Current Account', 'usage.table.cluster': 'Cluster', - 'usage.table.provider': 'Provider' + 'usage.table.provider': 'Provider', + 'usage.table.deleted': 'Deleted' }; diff --git a/src/locales/ru-RU/usage.ts b/src/locales/ru-RU/usage.ts index 607b4b3d..0b2a0814 100644 --- a/src/locales/ru-RU/usage.ts +++ b/src/locales/ru-RU/usage.ts @@ -27,5 +27,6 @@ export default { 'usage.export.table': 'Export Table Data', 'usage.user.currentAccount': 'Current Account', 'usage.table.cluster': 'Cluster', - 'usage.table.provider': 'Provider' + 'usage.table.provider': 'Provider', + 'usage.table.deleted': 'Deleted' }; diff --git a/src/locales/tr-TR/usage.ts b/src/locales/tr-TR/usage.ts index 607b4b3d..0b2a0814 100644 --- a/src/locales/tr-TR/usage.ts +++ b/src/locales/tr-TR/usage.ts @@ -27,5 +27,6 @@ export default { 'usage.export.table': 'Export Table Data', 'usage.user.currentAccount': 'Current Account', 'usage.table.cluster': 'Cluster', - 'usage.table.provider': 'Provider' + 'usage.table.provider': 'Provider', + 'usage.table.deleted': 'Deleted' }; diff --git a/src/locales/zh-CN/usage.ts b/src/locales/zh-CN/usage.ts index 3d48e851..9a8b02c4 100644 --- a/src/locales/zh-CN/usage.ts +++ b/src/locales/zh-CN/usage.ts @@ -27,5 +27,6 @@ export default { 'usage.export.table': '导出表格数据', 'usage.user.currentAccount': '当前账户', 'usage.table.cluster': '集群', - 'usage.table.provider': '提供商' + 'usage.table.provider': '提供商', + 'usage.table.deleted': '已删除' }; diff --git a/src/pages/dashboard/components/usage-inner/request-token-inner.tsx b/src/pages/dashboard/components/usage-inner/request-token-inner.tsx index 5a32e430..3b01e140 100644 --- a/src/pages/dashboard/components/usage-inner/request-token-inner.tsx +++ b/src/pages/dashboard/components/usage-inner/request-token-inner.tsx @@ -97,6 +97,8 @@ const RequestTokenInner: React.FC = (props) => { })); }, [requestData, tokenData]); + console.log('xAxisData', xAxisData, tokenData); + return ( diff --git a/src/pages/usage/components/daily-usage.tsx b/src/pages/usage/components/daily-usage.tsx index a941d4c6..c85f674a 100644 --- a/src/pages/usage/components/daily-usage.tsx +++ b/src/pages/usage/components/daily-usage.tsx @@ -28,6 +28,20 @@ const ControlLabel = styled.span` margin-right: 8px; `; +function getColorForIndex(index: number) { + const hue = (index * 137.5) % 360; + return `oklch(70% 0.12 ${hue})`; +} + +const colors = [ + baseColorMap.base, + baseColorMap.baseR3, + baseColorMap.baseL1, + baseColorMap.baseR1, + baseColorMap.baseR2, + baseColorMap.baseL2 +]; + interface DailyUsageProps { timeSeriesData: TimeSeriesData | null; metric: string; @@ -60,22 +74,33 @@ const DailyUsage: React.FC = (props) => { } const series = timeSeriesData.series; - const xAxis = series[0]?.timeline?.map((item) => item.date) || []; + const dateSet = new Set(); - const colors = [ - baseColorMap.base, - baseColorMap.baseR3, - baseColorMap.baseL1, - baseColorMap.baseR1, - baseColorMap.baseR2, - baseColorMap.baseL2 - ]; + for (const item of series) { + for (const point of item.timeline) { + dateSet.add(point.date); + } + } - const chartSeries = series.map((item, index) => ({ - name: item.label, - data: item.timeline.map((t) => ({ time: t.date, value: t.value })), - color: colors[index % colors.length] - })); + // sort dates in ascending order + const xAxis = Array.from(dateSet).sort( + (a, b) => dayjs(a).valueOf() - dayjs(b).valueOf() + ); + + const chartSeries = series.map((item, index) => { + const timelineMap = new Map( + item.timeline.map((point) => [ + point.date, + { time: point.date, value: point.value } + ]) + ); + + return { + name: item.label, + data: xAxis.map((date) => timelineMap.get(date) ?? null), + color: colors[index % colors.length] + }; + }); // API requests use line chart, tokens use bar chart return { diff --git a/src/pages/usage/components/export-data.tsx b/src/pages/usage/components/export-data.tsx index 6b65235a..b3ca250e 100644 --- a/src/pages/usage/components/export-data.tsx +++ b/src/pages/usage/components/export-data.tsx @@ -84,7 +84,8 @@ const ExportData: React.FC<{ }) => { fetchExportData({ ...pageParams, - granularity: nextChartFilters.granularity, + granularity: 'day', + sort_by: '-date', group_by: ['date', 'user', 'model', 'api_key'], filters: nextFilters, scope: initialScope, @@ -221,9 +222,10 @@ const ExportData: React.FC<{ ...pageParams, page, perPage: pageSize, - granularity, + granularity: 'day', group_by: ['date', 'user', 'model', 'api_key'], filters, + sort_by: '-date', scope: initialScope, start_date: commonFilters.start_date, end_date: commonFilters.end_date @@ -239,9 +241,10 @@ const ExportData: React.FC<{ setPageParams(INITIAL_PAGE_PARAMS); fetchExportData({ ...INITIAL_PAGE_PARAMS, - granularity, + granularity: 'day', group_by: ['date', 'user', 'model', 'api_key'], filters, + sort_by: '-date', scope: initialScope, start_date: commonFilters.start_date, end_date: commonFilters.end_date @@ -263,7 +266,7 @@ const ExportData: React.FC<{ closable: false }} keyboard={false} - width={1200} + width={1280} style={{ top: '10%' }} diff --git a/src/pages/usage/components/filter-bar.tsx b/src/pages/usage/components/filter-bar.tsx index 4cf162ed..82aecf20 100644 --- a/src/pages/usage/components/filter-bar.tsx +++ b/src/pages/usage/components/filter-bar.tsx @@ -7,7 +7,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, Segmented } from 'antd'; +import { Button, DatePicker, Dropdown, MenuProps } from 'antd'; import dayjs from 'dayjs'; import React from 'react'; import { GroupOption } from '../config'; @@ -120,8 +120,6 @@ const FilterBar: React.FC = (props) => { } ] }); - // const [activeModels, setActiveModels] = React.useState([]); - // const [activeApiKeys, setActiveApiKeys] = React.useState([]); const initialInfo = useModel('@@initialState'); const { initialState } = initialInfo || {}; @@ -149,8 +147,15 @@ const FilterBar: React.FC = (props) => { } ]; + const renderTag = (tag: string) => { + return ( + + [{tag}] + + ); + }; + const handleOnModelsChange = (value: valueType[][], selectedOptions: any) => { - // setActiveModels(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 @@ -165,7 +170,6 @@ const FilterBar: React.FC = (props) => { value: valueType[][], selectedOptions: any ) => { - // setActiveApiKeys(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 @@ -194,10 +198,14 @@ const FilterBar: React.FC = (props) => { const optionRender = (option: any) => { const { data } = option; - console.log('optionRender', option); - if (!data.isParent) { - return {data.label}; + return ( + + {data.label} + {data.deleted && + renderTag(intl.formatMessage({ id: 'usage.table.deleted' }))} + + ); } if (data.type === 'deployments') { @@ -221,6 +229,36 @@ const FilterBar: React.FC = (props) => { ); }; + const apiKeyOptionRender = (option: any) => { + const { data } = option; + if (!data.isParent) { + return ( + + {data.label} + {data.deleted && + renderTag(intl.formatMessage({ id: 'usage.table.deleted' }))} + + ); + } + + return ( + + {data.label} + + ); + }; + + const singleOptionRender = (option: any) => { + const { data } = option; + return ( + + {data.label} + {data.deleted && + renderTag(intl.formatMessage({ id: 'usage.table.deleted' }))} + + ); + }; + const onPickerChange = (picker: DateType) => { handleOnPickerChange(picker); handlePickerChange(picker); @@ -231,57 +269,15 @@ const FilterBar: React.FC = (props) => { ? normalizeRangeValue(startDate, endDate, picker) : [dayjs().add(-DefaultDateConfig.defaultRange, 'd'), dayjs()]; - const renderFooter = () => { - return ( - - ); - }; - const userOptionRender = (option: any) => { const { data } = option; return ( - - {data.label} - {data.isCurrent && ( - - {' '} - [{intl.formatMessage({ id: 'usage.user.currentAccount' })}] - - )} + + {data.label} + {data.isCurrent && + renderTag(intl.formatMessage({ id: 'usage.user.currentAccount' }))} + {data.deleted && + renderTag(intl.formatMessage({ id: 'usage.table.deleted' }))} ); }; @@ -394,6 +390,7 @@ const FilterBar: React.FC = (props) => { options={apiKeyOptions} showCheckedStrategy="SHOW_CHILD" value={activeApiKeys} + optionNode={apiKeyOptionRender} onChange={handleOnApiKeysChange} getPopupContainer={(triggerNode) => triggerNode.parentNode} > @@ -412,6 +409,7 @@ const FilterBar: React.FC = (props) => { wrapper: { flex: 1, maxWidth: 240, minWidth: 100 } }} value={selectedApiKeys} + optionRender={singleOptionRender} onChange={onApiKeysChange} /> )} diff --git a/src/pages/usage/config/types.ts b/src/pages/usage/config/types.ts index 5bbae53b..cfdab6a3 100644 --- a/src/pages/usage/config/types.ts +++ b/src/pages/usage/config/types.ts @@ -45,7 +45,7 @@ export interface TimeSeriesData { series: TimeSeriesItem[]; } -export type BreakdownItem = UsageFilterItem & { +export type BreakdownItem = { cluster_name: string; model_name: string; user_name: string; @@ -60,6 +60,9 @@ export type BreakdownItem = UsageFilterItem & { last_active: string; provider_type: string; provider_name: string; + user: UsageFilterItem; + model: UsageFilterItem; + api_key: UsageFilterItem; }; export interface UsageMeta { diff --git a/src/pages/usage/hooks/use-apikeys-columns.tsx b/src/pages/usage/hooks/use-apikeys-columns.tsx index 92fc58a0..be004243 100644 --- a/src/pages/usage/hooks/use-apikeys-columns.tsx +++ b/src/pages/usage/hooks/use-apikeys-columns.tsx @@ -1,6 +1,7 @@ // columns.ts import AutoTooltip from '@/components/auto-tooltip'; import { useIntl } from '@umijs/max'; +import { Tag } from 'antd'; import { useMemo } from 'react'; import { BreakdownItem as ListItem } from '../config/types'; @@ -19,9 +20,23 @@ const useModelsColumns = (): Array<{ key: 'api_key_name', render: (text: string, record: ListItem) => ( - + {text}}> {text} + {record.api_key?.deleted && ( + + {intl.formatMessage({ id: 'usage.table.deleted' })} + + )} ) }, diff --git a/src/pages/usage/hooks/use-models-columns.tsx b/src/pages/usage/hooks/use-models-columns.tsx index bda9849d..107f9a04 100644 --- a/src/pages/usage/hooks/use-models-columns.tsx +++ b/src/pages/usage/hooks/use-models-columns.tsx @@ -2,6 +2,7 @@ import AutoTooltip from '@/components/auto-tooltip'; import ProviderLogo from '@/pages/maas-provider/components/provider-logo'; import { useIntl } from '@umijs/max'; +import { Tag } from 'antd'; import { useMemo } from 'react'; import { BreakdownItem as ListItem } from '../config/types'; @@ -24,9 +25,27 @@ const useModelsColumns = (): Array<{ key: 'model_name', render: (text: string, record: ListItem) => ( - + {text}} + > {text} + {record.model?.deleted && ( + + {intl.formatMessage({ id: 'usage.table.deleted' })} + + )} ) }, @@ -40,15 +59,14 @@ const useModelsColumns = (): Array<{ }, { title: intl.formatMessage({ id: 'usage.table.provider' }), - dataIndex: 'provider_name', - key: 'provider_name', + dataIndex: ['model', 'identity', 'value', 'provider_type'], + key: 'provider_type', render: (text: string, record: ListItem) => ( - + {text || - (!record.provider_type && - intl.formatMessage({ id: 'menu.models.deployment' }))} + (!text && intl.formatMessage({ id: 'menu.models.deployment' }))} ) diff --git a/src/pages/usage/hooks/use-users-columns.tsx b/src/pages/usage/hooks/use-users-columns.tsx index 3790ffee..620d963d 100644 --- a/src/pages/usage/hooks/use-users-columns.tsx +++ b/src/pages/usage/hooks/use-users-columns.tsx @@ -1,6 +1,7 @@ // columns.ts import AutoTooltip from '@/components/auto-tooltip'; import { useIntl } from '@umijs/max'; +import { Tag } from 'antd'; import { useMemo } from 'react'; import { BreakdownItem as ListItem } from '../config/types'; @@ -23,9 +24,27 @@ const useModelsColumns = (): Array<{ key: 'user_name', render: (text: string, record: ListItem) => ( - + {text}} + > {text} + {record.user?.deleted && ( + + {intl.formatMessage({ id: 'usage.table.deleted' })} + + )} ) }, diff --git a/src/pages/usage/services/use-query-meta-data.ts b/src/pages/usage/services/use-query-meta-data.ts index 33be9293..aded6eb0 100644 --- a/src/pages/usage/services/use-query-meta-data.ts +++ b/src/pages/usage/services/use-query-meta-data.ts @@ -32,8 +32,8 @@ export default function useQueryUsageMetaData() { if (!currentUserId) return users; const sortedUsers = [...users].sort((a, b) => { - if (a.identity.current.user_id === currentUserId) return -1; - if (b.identity.current.user_id === currentUserId) return 1; + if (a.identity.current?.user_id === currentUserId) return -1; + if (b.identity.current?.user_id === currentUserId) return 1; return 0; }); @@ -58,7 +58,7 @@ export default function useQueryUsageMetaData() { sortedUsers.map((item) => ({ value: item.label, isCurrent: - item.identity.current.user_id === initialState?.currentUser?.id, + item.identity.current?.user_id === initialState?.currentUser?.id, ...item })) || [], api_keys: groupToOptions(res?.filters?.api_keys || [], { diff --git a/src/pages/usage/tables/apikeys-table.tsx b/src/pages/usage/tables/apikeys-table.tsx index 9e322e4e..f55eb26b 100644 --- a/src/pages/usage/tables/apikeys-table.tsx +++ b/src/pages/usage/tables/apikeys-table.tsx @@ -30,7 +30,7 @@ const APIKeys: React.FC<{ }>({ page: 1, perPage: 10, - sort_by: '' + sort_by: '-total_tokens' }); const pendingPageResetRef = useRef(false); @@ -113,7 +113,7 @@ const APIKeys: React.FC<{ getBreakdownRowKey(record, 'api_keys')} loading={{ spinning: loading, size: 'middle' diff --git a/src/pages/usage/tables/models-table.tsx b/src/pages/usage/tables/models-table.tsx index 70ab0d6c..37aa14cb 100644 --- a/src/pages/usage/tables/models-table.tsx +++ b/src/pages/usage/tables/models-table.tsx @@ -30,7 +30,7 @@ const Models: React.FC<{ }>({ page: 1, perPage: 10, - sort_by: '' + sort_by: '-total_tokens' }); const pendingPageResetRef = useRef(false); @@ -115,7 +115,7 @@ const Models: React.FC<{
getBreakdownRowKey(record, 'models')} loading={{ spinning: loading, size: 'middle' diff --git a/src/pages/usage/tables/users-table.tsx b/src/pages/usage/tables/users-table.tsx index 16ca6267..dde66e43 100644 --- a/src/pages/usage/tables/users-table.tsx +++ b/src/pages/usage/tables/users-table.tsx @@ -30,7 +30,7 @@ const Users: React.FC<{ }>({ page: 1, perPage: 10, - sort_by: '' + sort_by: '-total_tokens' }); const pendingPageResetRef = useRef(false); @@ -116,7 +116,7 @@ const Users: React.FC<{
getBreakdownRowKey(record, 'users')} loading={{ spinning: loading, size: 'middle' diff --git a/src/pages/usage/utils/get-breakdown-row-key.ts b/src/pages/usage/utils/get-breakdown-row-key.ts index 60467851..20939dd6 100644 --- a/src/pages/usage/utils/get-breakdown-row-key.ts +++ b/src/pages/usage/utils/get-breakdown-row-key.ts @@ -1,35 +1,22 @@ import { BreakdownItem } from '../config/types'; -export const getBreakdownRowKey = (record: BreakdownItem): string => { - const identity = record?.identity; - const current = identity?.current; - const value = identity?.value; +export const getBreakdownRowKey = ( + record: BreakdownItem, + type: string +): string => { + if (type === 'models') { + return `${record.model.label}`; + } - 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('__'); + if (type === 'users') { + return `${record.user.label}`; + } - return key || JSON.stringify(record); + if (type === 'api_keys') { + return `${record.api_key.label}`; + } + + return record.last_active; }; export default getBreakdownRowKey;