fix: usage ux
This commit is contained in:
@@ -68,12 +68,14 @@ export const SimpleCardItem: React.FC<{
|
|||||||
<div className={cx({ bordered: bordered }, styles.wrapper)} style={style}>
|
<div className={cx({ bordered: bordered }, styles.wrapper)} style={style}>
|
||||||
<div className="title">{title}</div>
|
<div className="title">{title}</div>
|
||||||
<div className="content">
|
<div className="content">
|
||||||
<span
|
{iconType && (
|
||||||
className={cx([iconType], 'icon')}
|
<span
|
||||||
style={{
|
className={cx([iconType], 'icon')}
|
||||||
backgroundColor: color || 'transparent'
|
style={{
|
||||||
}}
|
backgroundColor: color || 'transparent'
|
||||||
></span>
|
}}
|
||||||
|
></span>
|
||||||
|
)}
|
||||||
<span>{content}</span>
|
<span>{content}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -89,8 +91,12 @@ export const SimpleCard: React.FC<{
|
|||||||
}[];
|
}[];
|
||||||
height?: string | number;
|
height?: string | number;
|
||||||
bordered?: boolean;
|
bordered?: boolean;
|
||||||
|
styles?: {
|
||||||
|
wrapper?: React.CSSProperties;
|
||||||
|
item?: React.CSSProperties;
|
||||||
|
};
|
||||||
}> = (props) => {
|
}> = (props) => {
|
||||||
const { dataList, bordered } = props;
|
const { dataList, bordered, styles } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SimpleCardItemWrapper style={{ height: props.height || '100%' }}>
|
<SimpleCardItemWrapper style={{ height: props.height || '100%' }}>
|
||||||
@@ -102,6 +108,9 @@ export const SimpleCard: React.FC<{
|
|||||||
bordered={bordered}
|
bordered={bordered}
|
||||||
color={item.color}
|
color={item.color}
|
||||||
iconType={item.iconType}
|
iconType={item.iconType}
|
||||||
|
style={{
|
||||||
|
...styles?.item
|
||||||
|
}}
|
||||||
></SimpleCardItem>
|
></SimpleCardItem>
|
||||||
))}
|
))}
|
||||||
</SimpleCardItemWrapper>
|
</SimpleCardItemWrapper>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
import { DatePickerProps } from 'antd';
|
import { DatePickerProps } from 'antd';
|
||||||
import dayjs, { type Dayjs } from 'dayjs';
|
import dayjs, { type Dayjs } from 'dayjs';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
interface RangePickerPreset {
|
interface RangePickerPreset {
|
||||||
range: number;
|
range: number;
|
||||||
@@ -17,10 +18,17 @@ export default function useRangePickerPreset(options?: RangePickerPreset): {
|
|||||||
label: React.ReactNode;
|
label: React.ReactNode;
|
||||||
value: [Dayjs, Dayjs] | (() => [Dayjs, Dayjs]);
|
value: [Dayjs, Dayjs] | (() => [Dayjs, Dayjs]);
|
||||||
}[];
|
}[];
|
||||||
|
handleOnPickerChange: (
|
||||||
|
value: 'date' | 'week' | 'month' | 'quarter' | 'year'
|
||||||
|
) => void;
|
||||||
|
picker: 'date' | 'week' | 'month' | 'quarter' | 'year';
|
||||||
range: number;
|
range: number;
|
||||||
} {
|
} {
|
||||||
const { range = 60, disabledDate, presetRanges } = options || {};
|
const { range = 60, disabledDate, presetRanges } = options || {};
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
const [picker, setPicker] = useState<
|
||||||
|
'date' | 'week' | 'month' | 'quarter' | 'year'
|
||||||
|
>('month');
|
||||||
|
|
||||||
const getYearMonth = (date: Dayjs) => date.year() * 12 + date.month();
|
const getYearMonth = (date: Dayjs) => date.year() * 12 + date.month();
|
||||||
|
|
||||||
@@ -55,6 +63,12 @@ export default function useRangePickerPreset(options?: RangePickerPreset): {
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleOnPickerChange = (
|
||||||
|
value: 'date' | 'week' | 'month' | 'quarter' | 'year'
|
||||||
|
) => {
|
||||||
|
setPicker(value);
|
||||||
|
};
|
||||||
|
|
||||||
const rangePresets: {
|
const rangePresets: {
|
||||||
label: React.ReactNode;
|
label: React.ReactNode;
|
||||||
value: [Dayjs, Dayjs] | (() => [Dayjs, Dayjs]);
|
value: [Dayjs, Dayjs] | (() => [Dayjs, Dayjs]);
|
||||||
@@ -79,7 +93,9 @@ export default function useRangePickerPreset(options?: RangePickerPreset): {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
disabledRangeDaysDate,
|
disabledRangeDaysDate,
|
||||||
|
handleOnPickerChange,
|
||||||
rangePresets,
|
rangePresets,
|
||||||
|
picker,
|
||||||
range
|
range
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import CardWrapper from '@/components/card-wrapper';
|
import CardWrapper from '@/components/card-wrapper';
|
||||||
import { SimpleCard } from '@/components/card-wrapper/simple-card';
|
|
||||||
import MixLineBar from '@/components/echarts/mix-line-bar';
|
import MixLineBar from '@/components/echarts/mix-line-bar';
|
||||||
import BaseSelect from '@/components/seal-form/base/select';
|
import BaseSelect from '@/components/seal-form/base/select';
|
||||||
import { baseColorMap } from '@/pages/dashboard/config';
|
import { baseColorMap } from '@/pages/dashboard/config';
|
||||||
import { formatLargeNumber } from '@/utils';
|
import { formatLargeNumber } from '@/utils';
|
||||||
import { Divider, Segmented } from 'antd';
|
import { Segmented } from 'antd';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
@@ -151,7 +150,7 @@ const DailyUsage: React.FC<DailyUsageProps> = (props) => {
|
|||||||
<ControlsWrapper>
|
<ControlsWrapper>
|
||||||
<div className="group">
|
<div className="group">
|
||||||
<BaseSelect
|
<BaseSelect
|
||||||
variant="outlined"
|
variant="borderless"
|
||||||
prefix={<ControlLabel>Metric</ControlLabel>}
|
prefix={<ControlLabel>Metric</ControlLabel>}
|
||||||
options={metricOptions}
|
options={metricOptions}
|
||||||
value={metric}
|
value={metric}
|
||||||
@@ -160,7 +159,7 @@ const DailyUsage: React.FC<DailyUsageProps> = (props) => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<BaseSelect
|
<BaseSelect
|
||||||
variant="outlined"
|
variant="borderless"
|
||||||
prefix={<ControlLabel>Group by</ControlLabel>}
|
prefix={<ControlLabel>Group by</ControlLabel>}
|
||||||
options={groupByOptions}
|
options={groupByOptions}
|
||||||
value={groupBy}
|
value={groupBy}
|
||||||
@@ -176,9 +175,9 @@ const DailyUsage: React.FC<DailyUsageProps> = (props) => {
|
|||||||
onChange={onGranularityChange}
|
onChange={onGranularityChange}
|
||||||
></Segmented>
|
></Segmented>
|
||||||
</ControlsWrapper>
|
</ControlsWrapper>
|
||||||
<Divider style={{ margin: 0 }} />
|
{/* <Divider style={{ margin: 0 }} /> */}
|
||||||
|
|
||||||
<SimpleCard dataList={summaryCards} height={80} />
|
{/* <SimpleCard dataList={summaryCards} height={80} /> */}
|
||||||
<MixLineBar
|
<MixLineBar
|
||||||
chartData={chartData}
|
chartData={chartData}
|
||||||
seriesData={[]}
|
seriesData={[]}
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
import AutoTooltip from '@/components/auto-tooltip';
|
||||||
|
import ModalFooter from '@/components/modal-footer';
|
||||||
|
import ScrollerModal from '@/components/scroller-modal';
|
||||||
|
import { useIntl } from '@umijs/max';
|
||||||
|
import { Table, TableColumnType } from 'antd';
|
||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { useUsageFilters } from '../hooks/use-usage-filters';
|
||||||
|
import FilterBar from './filter-bar';
|
||||||
|
|
||||||
|
const ExportData: React.FC<{
|
||||||
|
open: boolean;
|
||||||
|
onCancel: () => 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 <AutoTooltip ghost>{text}</AutoTooltip>;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: intl.formatMessage({ id: 'dashboard.usage.export.model' }),
|
||||||
|
dataIndex: 'model_id',
|
||||||
|
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>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
filterBar.onExportChart();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOnCancel = () => {
|
||||||
|
onCancel?.();
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open) {
|
||||||
|
fetchData(commonFilters, {});
|
||||||
|
} else {
|
||||||
|
}
|
||||||
|
}, [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={1200}
|
||||||
|
style={{
|
||||||
|
top: '10%'
|
||||||
|
}}
|
||||||
|
footer={
|
||||||
|
<ModalFooter
|
||||||
|
onOk={handleSubmit}
|
||||||
|
onCancel={handleOnCancel}
|
||||||
|
okText={intl.formatMessage({ id: 'common.button.export' })}
|
||||||
|
></ModalFooter>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FilterBar {...filterBar} pageType="modal"></FilterBar>
|
||||||
|
<Table
|
||||||
|
columns={exportTableColumns}
|
||||||
|
tableLayout={'auto'}
|
||||||
|
style={{ width: '100%', marginTop: '16px', minHeight: 300 }}
|
||||||
|
dataSource={timeSeriesData.series || []}
|
||||||
|
loading={loading}
|
||||||
|
rowKey="id"
|
||||||
|
virtual
|
||||||
|
scroll={{ y: 400 }}
|
||||||
|
pagination={false}
|
||||||
|
></Table>
|
||||||
|
</ScrollerModal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ExportData;
|
||||||
@@ -6,7 +6,7 @@ import ProviderLogo from '@/pages/maas-provider/components/provider-logo';
|
|||||||
import { useModel } from '@@/plugin-model';
|
import { useModel } from '@@/plugin-model';
|
||||||
import { DownloadOutlined, SyncOutlined } from '@ant-design/icons';
|
import { DownloadOutlined, SyncOutlined } from '@ant-design/icons';
|
||||||
import { useIntl } from '@umijs/max';
|
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 dayjs from 'dayjs';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { GroupOption } from '../config';
|
import { GroupOption } from '../config';
|
||||||
@@ -22,6 +22,7 @@ type OptionType = UsageFilterItem & {
|
|||||||
value: string;
|
value: string;
|
||||||
};
|
};
|
||||||
interface FilterBarProps {
|
interface FilterBarProps {
|
||||||
|
pageType?: 'page' | 'modal';
|
||||||
scope: string;
|
scope: string;
|
||||||
startDate: string;
|
startDate: string;
|
||||||
endDate: string;
|
endDate: string;
|
||||||
@@ -38,22 +39,20 @@ interface FilterBarProps {
|
|||||||
onApiKeysChange: (value: string[]) => void;
|
onApiKeysChange: (value: string[]) => void;
|
||||||
onExport?: () => void;
|
onExport?: () => void;
|
||||||
handleSearch?: () => void;
|
handleSearch?: () => void;
|
||||||
onExportChart: () => void;
|
onExportChart?: () => void;
|
||||||
onExportTable: () => void;
|
onExportTable?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FilterBar: React.FC<FilterBarProps> = (props) => {
|
const FilterBar: React.FC<FilterBarProps> = (props) => {
|
||||||
const {
|
const {
|
||||||
scope,
|
pageType = 'page',
|
||||||
startDate,
|
startDate,
|
||||||
endDate,
|
endDate,
|
||||||
selectedModels,
|
|
||||||
selectedUsers,
|
selectedUsers,
|
||||||
selectedApiKeys,
|
selectedApiKeys,
|
||||||
modelOptions,
|
modelOptions,
|
||||||
userOptions,
|
userOptions,
|
||||||
apiKeyOptions,
|
apiKeyOptions,
|
||||||
onScopeChange,
|
|
||||||
onDateChange,
|
onDateChange,
|
||||||
onModelsChange,
|
onModelsChange,
|
||||||
onUsersChange,
|
onUsersChange,
|
||||||
@@ -63,41 +62,40 @@ const FilterBar: React.FC<FilterBarProps> = (props) => {
|
|||||||
handleSearch
|
handleSearch
|
||||||
} = props;
|
} = props;
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const { disabledRangeDaysDate, rangePresets } = useRangePickerPreset({
|
const { disabledRangeDaysDate, rangePresets, picker, handleOnPickerChange } =
|
||||||
range: DefaultDateConfig.maxRange,
|
useRangePickerPreset({
|
||||||
disabledDate: true,
|
range: DefaultDateConfig.maxRange,
|
||||||
presetRanges: [
|
disabledDate: true,
|
||||||
{
|
presetRanges: [
|
||||||
label: intl.formatMessage({
|
{
|
||||||
id: 'dashboard.usage.datePicker.last7days'
|
label: intl.formatMessage({
|
||||||
}),
|
id: 'dashboard.usage.datePicker.last7days'
|
||||||
value: [dayjs().add(-6, 'd'), dayjs()]
|
}),
|
||||||
},
|
value: [dayjs().add(-6, 'd'), dayjs()]
|
||||||
{
|
},
|
||||||
label: intl.formatMessage({
|
{
|
||||||
id: 'dashboard.usage.datePicker.last30days'
|
label: intl.formatMessage({
|
||||||
}),
|
id: 'dashboard.usage.datePicker.last30days'
|
||||||
value: [dayjs().add(-29, 'd'), dayjs()]
|
}),
|
||||||
},
|
value: [dayjs().add(-29, 'd'), dayjs()]
|
||||||
{
|
},
|
||||||
label: intl.formatMessage({
|
{
|
||||||
id: 'dashboard.usage.datePicker.last60days'
|
label: intl.formatMessage({
|
||||||
}),
|
id: 'dashboard.usage.datePicker.last60days'
|
||||||
value: [dayjs().add(-59, 'd'), dayjs()]
|
}),
|
||||||
},
|
value: [dayjs().add(-59, 'd'), dayjs()]
|
||||||
{
|
},
|
||||||
label: intl.formatMessage({
|
{
|
||||||
id: 'dashboard.usage.datePicker.last90days'
|
label: intl.formatMessage({
|
||||||
}),
|
id: 'dashboard.usage.datePicker.last90days'
|
||||||
value: [dayjs().add(-89, 'd'), dayjs()]
|
}),
|
||||||
}
|
value: [dayjs().add(-89, 'd'), dayjs()]
|
||||||
]
|
}
|
||||||
});
|
]
|
||||||
|
});
|
||||||
const [activeModels, setActiveModels] = React.useState<valueType[][]>([]);
|
const [activeModels, setActiveModels] = React.useState<valueType[][]>([]);
|
||||||
const [activeApiKeys, setActiveApiKeys] = React.useState<valueType[][]>([]);
|
const [activeApiKeys, setActiveApiKeys] = React.useState<valueType[][]>([]);
|
||||||
const [activeUserApiKeys, setActiveUserApiKeys] = React.useState<valueType[]>(
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
const initialInfo = useModel('@@initialState');
|
const initialInfo = useModel('@@initialState');
|
||||||
const { initialState } = initialInfo || {};
|
const { initialState } = initialInfo || {};
|
||||||
|
|
||||||
@@ -184,6 +182,46 @@ const FilterBar: React.FC<FilterBarProps> = (props) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const renderFooter = () => {
|
||||||
|
return (
|
||||||
|
<Segmented
|
||||||
|
size="middle"
|
||||||
|
shape="round"
|
||||||
|
value={picker}
|
||||||
|
styles={{
|
||||||
|
root: {
|
||||||
|
width: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
marginBlock: 8
|
||||||
|
},
|
||||||
|
item: {
|
||||||
|
flex: 1
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
flex: 1
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onChange={handleOnPickerChange}
|
||||||
|
options={[
|
||||||
|
{
|
||||||
|
label: 'Day',
|
||||||
|
value: 'date'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Week',
|
||||||
|
value: 'week'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Month',
|
||||||
|
value: 'month'
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
></Segmented>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={FilterBarCss.wrapper}>
|
<div className={FilterBarCss.wrapper}>
|
||||||
<div className={FilterBarCss.filters}>
|
<div className={FilterBarCss.filters}>
|
||||||
@@ -193,12 +231,14 @@ const FilterBar: React.FC<FilterBarProps> = (props) => {
|
|||||||
dayjs().add(-DefaultDateConfig.defaultRange, 'd'),
|
dayjs().add(-DefaultDateConfig.defaultRange, 'd'),
|
||||||
dayjs()
|
dayjs()
|
||||||
]}
|
]}
|
||||||
|
picker={picker}
|
||||||
disabledDate={disabledRangeDaysDate}
|
disabledDate={disabledRangeDaysDate}
|
||||||
presets={rangePresets}
|
presets={rangePresets}
|
||||||
allowClear={false}
|
allowClear={false}
|
||||||
style={{ width: 240 }}
|
style={{ width: 240 }}
|
||||||
value={[dayjs(startDate), dayjs(endDate)]}
|
value={[dayjs(startDate), dayjs(endDate)]}
|
||||||
onChange={onDateChange}
|
onChange={onDateChange}
|
||||||
|
renderExtraFooter={renderFooter}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -318,9 +358,11 @@ const FilterBar: React.FC<FilterBarProps> = (props) => {
|
|||||||
icon={<SyncOutlined></SyncOutlined>}
|
icon={<SyncOutlined></SyncOutlined>}
|
||||||
></Button>
|
></Button>
|
||||||
</div>
|
</div>
|
||||||
<Dropdown menu={{ items: exportMenuItems }}>
|
{pageType === 'page' && (
|
||||||
<Button icon={<DownloadOutlined />} />
|
<Dropdown menu={{ items: exportMenuItems }}>
|
||||||
</Dropdown>
|
<Button icon={<DownloadOutlined />} />
|
||||||
|
</Dropdown>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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<UsageFilterItem, 'label' | 'deleted'>;
|
||||||
|
type GroupOptionType = GroupOption<UsageFilterItem>;
|
||||||
|
|
||||||
|
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
|
||||||
|
};
|
||||||
|
};
|
||||||
+80
-181
@@ -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 { useModel } from '@@/plugin-model';
|
||||||
import dayjs from 'dayjs';
|
|
||||||
import React, { useEffect, useMemo, useState } from 'react';
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
import BreakdownTabs from './components/breakdown-tabs';
|
import BreakdownTabs from './components/breakdown-tabs';
|
||||||
import DailyUsage from './components/daily-usage';
|
import DailyUsage from './components/daily-usage';
|
||||||
|
import ExportData from './components/export-data';
|
||||||
import FilterBar from './components/filter-bar';
|
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 useExportTable from './hooks/use-export-table';
|
||||||
|
import { useUsageFilters } from './hooks/use-usage-filters';
|
||||||
import useQueryUsageMetaData from './services/use-query-meta-data';
|
import useQueryUsageMetaData from './services/use-query-meta-data';
|
||||||
import useQueryTimeSeriesData from './services/use-query-timeseries-data';
|
|
||||||
|
|
||||||
const DefaultDateConfig = {
|
|
||||||
defaultRange: 29
|
|
||||||
};
|
|
||||||
|
|
||||||
type FilterOptionType = Omit<UsageFilterItem, 'label' | 'deleted'>;
|
|
||||||
type GroupOptionType = GroupOption<UsageFilterItem>;
|
|
||||||
|
|
||||||
const Usage: React.FC = () => {
|
const Usage: React.FC = () => {
|
||||||
const initialInfo = useModel('@@initialState');
|
const initialInfo = useModel('@@initialState');
|
||||||
const { initialState } = initialInfo || {};
|
const { initialState } = initialInfo || {};
|
||||||
const { exportTable } = useExportTable();
|
const { exportTable } = useExportTable();
|
||||||
|
const [openExportModal, setOpenExportModal] = useState(false);
|
||||||
|
|
||||||
const summaryColumns = [
|
const summaryColumns = [
|
||||||
{
|
{
|
||||||
@@ -64,195 +54,98 @@ const Usage: React.FC = () => {
|
|||||||
group_by: 'model',
|
group_by: 'model',
|
||||||
granularity: 'day'
|
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 } =
|
const { detailData: metaData, fetchData: fetchMetaData } =
|
||||||
useQueryUsageMetaData();
|
useQueryUsageMetaData();
|
||||||
const { detailData: timeSeriesData, fetchData: fetchTimeSeriesData } =
|
|
||||||
useQueryTimeSeriesData();
|
|
||||||
|
|
||||||
const modelOptions = metaData?.models || [];
|
const { filters, commonFilters, fetchData, timeSeriesData, filterBar } =
|
||||||
const userOptions = metaData?.users || [];
|
useUsageFilters({
|
||||||
const apiKeyOptions = metaData?.api_keys || [];
|
initialScope: initialState?.currentUser?.is_admin ? 'all' : 'self',
|
||||||
|
metaData,
|
||||||
const buildFilters = (selected: {
|
chartFilters,
|
||||||
models: string[];
|
summaryColumns
|
||||||
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)
|
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchMetaData();
|
fetchMetaData();
|
||||||
fetchData(commonFilters, chartFilters);
|
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) => {
|
const handleChartFilterChange = (type: string, value: string) => {
|
||||||
setChartFilters((prev) => ({ ...prev, [type]: value }));
|
setChartFilters((prev) => ({ ...prev, [type]: value }));
|
||||||
fetchData(commonFilters, { ...chartFilters, [type]: value });
|
fetchData(commonFilters, { ...chartFilters, [type]: value });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSearch = () => {
|
const summaryCards = useMemo(() => {
|
||||||
fetchData(commonFilters, chartFilters);
|
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 handleExportChart = () => {
|
||||||
|
setOpenExportModal(true);
|
||||||
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<string, any>
|
|
||||||
),
|
|
||||||
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<string, any>
|
|
||||||
),
|
|
||||||
formatMap: {}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<FilterBar
|
<FilterBar
|
||||||
scope={commonFilters.scope}
|
{...filterBar}
|
||||||
startDate={commonFilters.start_date}
|
|
||||||
endDate={commonFilters.end_date}
|
|
||||||
selectedModels={commonFilters.models || []}
|
|
||||||
selectedUsers={commonFilters.users || []}
|
|
||||||
selectedApiKeys={commonFilters.api_keys || []}
|
|
||||||
modelOptions={metaData?.models || []}
|
|
||||||
userOptions={metaData?.users || []}
|
|
||||||
apiKeyOptions={metaData?.api_keys || []}
|
|
||||||
handleSearch={handleSearch}
|
|
||||||
onScopeChange={handleScopeChange}
|
|
||||||
onDateChange={handleDateChange}
|
|
||||||
onModelsChange={(value) => handleFilterChange('models', value)}
|
|
||||||
onUsersChange={(value) => handleFilterChange('users', value)}
|
|
||||||
onApiKeysChange={(value) => handleFilterChange('api_keys', value)}
|
|
||||||
onExportChart={handleOnExportChart}
|
|
||||||
onExportTable={exportTable}
|
onExportTable={exportTable}
|
||||||
|
onExportChart={handleExportChart}
|
||||||
/>
|
/>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
marginBlock: 24
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SimpleCard
|
||||||
|
dataList={summaryCards}
|
||||||
|
height={80}
|
||||||
|
styles={{
|
||||||
|
item: {
|
||||||
|
borderBottom: '1px solid var(--ant-color-split)',
|
||||||
|
backgroundColor: 'var(--ant-color-fill-quaternary)',
|
||||||
|
borderRadius: '6px 6px 0 0'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<DailyUsage
|
<DailyUsage
|
||||||
timeSeriesData={timeSeriesData}
|
timeSeriesData={timeSeriesData}
|
||||||
metric={chartFilters.metric}
|
metric={chartFilters.metric}
|
||||||
@@ -272,6 +165,12 @@ const Usage: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
scope={commonFilters.scope}
|
scope={commonFilters.scope}
|
||||||
></BreakdownTabs>
|
></BreakdownTabs>
|
||||||
|
<ExportData
|
||||||
|
metaData={metaData}
|
||||||
|
initialScope={commonFilters.scope}
|
||||||
|
open={openExportModal}
|
||||||
|
onCancel={() => setOpenExportModal(false)}
|
||||||
|
></ExportData>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user