feat: usage export data

main
jialin 8 months ago
parent 471e1ae96f
commit a161fa953e

@ -9,6 +9,7 @@ const SimpleCardItemWrapper = styled.div`
width: 100%;
height: 100%;
gap: 10px;
margin-bottom: 24px;
`;
const useStyles = createStyles(({ css, token }) => ({
@ -25,6 +26,9 @@ const useStyles = createStyles(({ css, token }) => ({
gap: ${token.padding}px;
.title {
font-size: ${token.fontSize}px;
font-weight: var(--font-weight-medium);
}
.content {
font-weight: var(--font-weight-500);
}
`
@ -48,11 +52,12 @@ export const SimpleCardItem: React.FC<{
export const SimpleCard: React.FC<{
dataList: { label: string; value: React.ReactNode }[];
height?: string | number;
}> = (props) => {
const { dataList } = props;
return (
<SimpleCardItemWrapper>
<SimpleCardItemWrapper style={{ height: props.height || '100%' }}>
{dataList.map((item, index) => (
<SimpleCardItem
key={index}

@ -48,11 +48,65 @@ const Chart: React.FC<{
);
useEffect(() => {
const handleOnFinished = () => {
if (!chart.current) return;
const currentChart = chart.current;
const optionsYAxis = currentChart.getOption()?.yAxis;
if (
!optionsYAxis ||
!Array.isArray(optionsYAxis) ||
optionsYAxis.length < 2
)
return;
// @ts-ignore
const model = currentChart.getModel();
const yAxisModels = [
model.getComponent('yAxis', 0),
model.getComponent('yAxis', 1)
];
if (!yAxisModels[0] || !yAxisModels[1]) return;
const axes = yAxisModels.map((m) => m.axis);
const intervals = axes.map((axis) => axis.scale.getInterval());
const ticksList = axes.map((axis) => axis.scale.getTicks());
const counts = ticksList.map((t) => t.length);
if (counts[0] === counts[1]) return;
const unifiedCount = Math.max(counts[0], counts[1]);
const newMax0 = intervals[0] * (unifiedCount - 1);
const newMax1 = intervals[1] * (unifiedCount - 1);
const yAxis: any[] = [{}, {}];
if (counts[0] < unifiedCount) {
yAxis[0].max = newMax0;
}
if (counts[1] < unifiedCount) {
yAxis[1].max = newMax1;
}
setTimeout(() => {
currentChart.setOption({
yAxis: yAxis
});
}, 0);
};
if (container.current) {
init();
chart.current?.on('finished', handleOnFinished);
}
return () => {
chart.current?.dispose();
chart.current?.off('finished', handleOnFinished);
};
}, [init]);

@ -1,24 +1,9 @@
import useUserSettings from '@/hooks/use-user-settings';
import { formatLargeNumber } from '@/utils';
import { theme } from 'antd';
import { isFunction } from 'lodash';
import { useMemo } from 'react';
const formatLargeNumber = (value: number) => {
if (typeof value !== 'number' || isNaN(value)) {
return value;
}
if (value >= 1e9) {
return (value / 1e9).toFixed(1).replace(/\.0$/, '') + 'B';
} else if (value >= 1e6) {
return (value / 1e6).toFixed(1).replace(/\.0$/, '') + 'M';
} else if (value >= 1e3) {
return (value / 1e3).toFixed(1).replace(/\.0$/, '') + 'K';
} else {
return value;
}
};
export const grid = {
left: 0,
right: 20,

@ -62,12 +62,7 @@ const MixLineBarChart: React.FC<
yAxis,
legend: {
...legend,
data: legendData.map((item: any) => {
return {
name: item,
icon: 'circle'
};
})
data: legendData
},
series: []
@ -112,16 +107,10 @@ const MixLineBarChart: React.FC<
},
yAxis: [
{
...options.yAxis,
min: 0,
splitNumber: 7
...options.yAxis
},
{
...options.yAxis,
min: 0,
max: 70,
splitNumber: 7,
nameTextStyle: {
fontSize: 12,
align: 'right'

@ -1,8 +1,9 @@
import type { LegendComponentOption } from 'echarts/components';
export interface ChartProps {
seriesData: any[];
showEmpty?: boolean;
xAxisData: string[];
legendData?: string[];
legendData?: LegendComponentOption['data'];
labelFormatter?: (val?: any) => string;
tooltipValueFormatter?: (val: any) => string;
height: string | number;

@ -5,3 +5,7 @@ export const DASHBOARD_API = '/dashboard';
export async function queryDashboardData() {
return request(DASHBOARD_API);
}
export async function queryDashboardUsageData() {
return request(`${DASHBOARD_API}/usage`);
}

@ -0,0 +1,96 @@
import ModalFooter from '@/components/modal-footer';
import ScrollerModal from '@/components/scroller-modal';
import useTableFetch from '@/hooks/use-table-fetch';
import { useIntl } from '@umijs/max';
import { DatePicker, Select, Space, Table } from 'antd';
import React from 'react';
import { queryDashboardUsageData } from '../../apis';
import { exportTableColumns } from '../../config';
const ExportData: React.FC<{
open: boolean;
onCancel: () => void;
}> = (props) => {
const {
dataSource,
rowSelection,
queryParams,
modalRef,
handleDelete,
handleDeleteBatch,
fetchData,
handlePageChange,
handleTableChange,
handleSearch,
handleNameChange
} = useTableFetch({
fetchAPI: queryDashboardUsageData
});
const { open, onCancel } = props || {};
const intl = useIntl();
const handleSubmit = () => {};
return (
<ScrollerModal
title={'Export Data'}
open={open}
centered={false}
onCancel={onCancel}
destroyOnClose={true}
closeIcon={true}
maskClosable={false}
keyboard={false}
width={800}
styles={{
content: {
padding: '0px'
},
header: {
padding: 'var(--ant-modal-content-padding)',
paddingBottom: '0'
},
body: {
padding: '0 var(--ant-modal-content-padding)'
},
footer: {
padding: '0 var(--ant-modal-content-padding)'
}
}}
footer={
<ModalFooter
onOk={handleSubmit}
onCancel={onCancel}
okText={intl.formatMessage({ id: 'common.button.export' })}
></ModalFooter>
}
>
<Space>
<DatePicker.RangePicker></DatePicker.RangePicker>
<Select placeholder="Select user"></Select>
<Select placeholder="Select model"></Select>
</Space>
<Table
columns={exportTableColumns}
tableLayout={dataSource.loadend ? 'auto' : 'fixed'}
style={{ width: '100%', marginTop: '16px' }}
dataSource={dataSource.dataList}
loading={dataSource.loading}
rowKey="id"
onChange={handleTableChange}
pagination={{
showSizeChanger: true,
pageSize: queryParams.perPage,
current: queryParams.page,
total: dataSource.total,
hideOnSinglePage: queryParams.perPage === 10,
onChange: handlePageChange
}}
>
{' '}
</Table>
</ScrollerModal>
);
};
export default ExportData;

@ -1,21 +1,45 @@
import PageTools from '@/components/page-tools';
import { ExportOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max';
import { Col, Row } from 'antd';
import { FC, memo, useContext } from 'react';
import { Button, Col, DatePicker, Row, Select } from 'antd';
import { FC, memo, useContext, useState } from 'react';
import styled from 'styled-components';
import { DashboardContext } from '../../config/dashboard-context';
import ExportData from './export-data';
import RequestTokenInner from './request-token-inner';
import TopUser from './top-user';
import useUsageData from './use-usage-data';
const FilterWrapper = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
.selection {
display: flex;
align-items: center;
gap: 8px;
}
`;
const UsageInner: FC<{ paddingRight: string }> = ({ paddingRight }) => {
const intl = useIntl();
const { model_usage } = useContext(DashboardContext);
const [open, setOpen] = useState(false);
const { requestTokenData, topUserData } = useUsageData(model_usage || {});
const handleOnCancel = () => {
setOpen(false);
};
const handleExport = () => {
setOpen(true);
};
return (
<>
<div>
<PageTools
style={{ margin: '26px 0px' }}
left={
@ -33,6 +57,20 @@ const UsageInner: FC<{ paddingRight: string }> = ({ paddingRight }) => {
xl={16}
style={{ paddingRight: paddingRight }}
>
<FilterWrapper>
<div className="selection">
<DatePicker.RangePicker></DatePicker.RangePicker>
<Select placeholder="Select user"></Select>
<Select placeholder="Select model"></Select>
</div>
<Button
type="text"
icon={<ExportOutlined />}
onClick={handleExport}
>
{intl.formatMessage({ id: 'common.button.export' })}
</Button>
</FilterWrapper>
<RequestTokenInner
requestData={requestTokenData.requestData}
xAxisData={requestTokenData.xAxisData}
@ -46,7 +84,8 @@ const UsageInner: FC<{ paddingRight: string }> = ({ paddingRight }) => {
></TopUser>
</Col>
</Row>
</>
<ExportData open={open} onCancel={handleOnCancel}></ExportData>
</div>
);
};

@ -1,8 +1,7 @@
import CardWrapper from '@/components/card-wrapper';
import BarChart from '@/components/echarts/bar-chart';
import LineChart from '@/components/echarts/line-chart';
import { SimpleCard } from '@/components/card-wrapper/simple-card';
import MixLineBar from '@/components/echarts/mix-line-bar';
import { useIntl } from '@umijs/max';
import { Col, Row } from 'antd';
import dayjs from 'dayjs';
import React from 'react';
@ -29,13 +28,19 @@ const dataList = [
{ label: '120K', value: 'API Requests' }
];
const legendData = [
{ name: 'Completion tokens', icon: 'roundRect' },
{ name: 'Prompt tokens', icon: 'roundRect' },
{ name: 'API requests', icon: 'circle' }
];
const RequestTokenInner: React.FC<RequestTokenInnerProps> = (props) => {
const { requestData, tokenData, xAxisData } = props;
const intl = useIntl();
return (
<CardWrapper style={{ width: '100%' }}>
<Row style={{ width: '100%' }}>
{/* <Row style={{ width: '100%' }}>
<Col span={12}>
<LineChart
title={intl.formatMessage({ id: 'dashboard.apirequest' })}
@ -54,7 +59,20 @@ const RequestTokenInner: React.FC<RequestTokenInnerProps> = (props) => {
labelFormatter={labelFormatter}
></BarChart>
</Col>
</Row>
</Row> */}
<SimpleCard dataList={dataList} height={80}></SimpleCard>
<MixLineBar
chartData={{
line: requestData,
bar: tokenData
}}
seriesData={[]}
xAxisData={xAxisData}
height={360}
smooth={true}
legendData={legendData}
labelFormatter={labelFormatter}
></MixLineBar>
</CardWrapper>
);
};

@ -17,7 +17,7 @@ const TopUser: React.FC<TopUserProps> = (props) => {
title={intl.formatMessage({ id: 'dashboard.topusers' })}
seriesData={userData}
xAxisData={topUserList}
height={360}
height={520}
></HBar>
</CardWrapper>
);

@ -92,8 +92,10 @@ export default function useUseageData(data: any) {
data: { time: string; value: number }[];
} = {
name: 'API requests',
areaStyle: {},
color: baseColorMap.base,
areaStyle: {
color: 'rgba(13,171,219,0.15)'
},
color: baseColorMap.baseR1,
data: generateData(dateRange, generateValueMap(apiRequestHistory))
};

@ -1,3 +1,6 @@
import { formatLargeNumber } from '@/utils';
import dayjs from 'dayjs';
export const overviewConfigs = [
{
key: 'worker_count',
@ -21,3 +24,49 @@ export const overviewConfigs = [
backgroundColor: 'var(--color-white-1)'
}
];
export const exportTableColumns = [
{
title: 'Date',
dataIndex: 'date',
render: (text: string) => {
return dayjs(text).format('YYYY-MM-DD');
}
},
{
title: 'User',
dataIndex: 'user',
render: (text: string) => {
return text || 'admin';
}
},
{
title: 'Model',
dataIndex: 'model',
render: (text: string) => {
return text || 'All Models';
}
},
{
title: 'Completion Tokens',
dataIndex: 'completion_tokens',
render: (text: number) => {
return formatLargeNumber(text) || 0;
}
},
{
title: 'Prompt Tokens',
dataIndex: 'prompt_tokens',
render: (text: number) => {
return formatLargeNumber(text) || 0;
}
},
{
title: 'API Requests',
dataIndex: 'request_count',
render: (text: number) => {
return formatLargeNumber(text) || 0;
}
}
];

@ -87,6 +87,22 @@ export const formatNumber = (num: number) => {
}
};
export const formatLargeNumber = (value: number) => {
if (typeof value !== 'number' || isNaN(value)) {
return value;
}
if (value >= 1e9) {
return (value / 1e9).toFixed(1).replace(/\.0$/, '') + 'B';
} else if (value >= 1e6) {
return (value / 1e6).toFixed(1).replace(/\.0$/, '') + 'M';
} else if (value >= 1e3) {
return (value / 1e3).toFixed(1).replace(/\.0$/, '') + 'K';
} else {
return value;
}
};
export function loadLanguageConfig(language: string) {
// @ts-ignore
const requireContext = require.context(`./${language}`, false, /\.ts$/);

Loading…
Cancel
Save