parent
0a47c4c067
commit
1d92571d09
@ -0,0 +1,20 @@
|
||||
:local(.status-content-wrapper) {
|
||||
// padding-top: 24px;
|
||||
&:hover {
|
||||
:global(.copy-button-wrapper) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
:global {
|
||||
.copy-button-wrapper {
|
||||
display: none;
|
||||
background-color: #383838;
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
right: 0;
|
||||
top: 0;
|
||||
border-radius: 0 8px 0 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
export default {
|
||||
'usage.title': 'Usage',
|
||||
'usage.filter.user': 'Filter by User',
|
||||
'usage.filter.model': 'Filter by Model'
|
||||
};
|
||||
@ -0,0 +1,5 @@
|
||||
export default {
|
||||
'usage.title': '使用量',
|
||||
'usage.filter.user': '按用户查询',
|
||||
'usage.filter.model': '按模型查询'
|
||||
};
|
||||
@ -0,0 +1,34 @@
|
||||
import { CloseCircleOutlined } from '@ant-design/icons';
|
||||
import { Space } from 'antd';
|
||||
import _ from 'lodash';
|
||||
import React from 'react';
|
||||
import '../style/thumb-img.less';
|
||||
|
||||
const ThumbImg: React.FC<{
|
||||
dataList: any[];
|
||||
onDelete: (uid: number) => void;
|
||||
}> = ({ dataList, onDelete }) => {
|
||||
const handleOnDelete = (uid: number) => {
|
||||
onDelete(uid);
|
||||
};
|
||||
|
||||
return (
|
||||
<Space wrap size={10} className="thumb-list-wrap">
|
||||
{_.map(dataList, (item: any) => {
|
||||
return (
|
||||
<span key={item.uid} className="thumb-img">
|
||||
<span
|
||||
style={{ backgroundImage: `url(${item.dataUrl})` }}
|
||||
className="img"
|
||||
></span>
|
||||
<span className="del" onClick={() => handleOnDelete(item.uid)}>
|
||||
<CloseCircleOutlined />
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</Space>
|
||||
);
|
||||
};
|
||||
|
||||
export default ThumbImg;
|
||||
@ -0,0 +1,42 @@
|
||||
.thumb-img {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
|
||||
.img {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
border-radius: var(--border-radius-base);
|
||||
cursor: pointer;
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
border: 1px solid var(--ant-color-border);
|
||||
}
|
||||
|
||||
.del {
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
right: -2px;
|
||||
font-size: var(--font-size-middle);
|
||||
cursor: pointer;
|
||||
background-color: var(--color-white-1);
|
||||
display: none;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.del {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.thumb-list-wrap {
|
||||
padding: 10px;
|
||||
// background-color: var(--color-fill-1);
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
import { request } from '@umijs/max';
|
||||
|
||||
export const DASHBOARD_API = '/dashboard';
|
||||
|
||||
export async function queryDashboardData() {
|
||||
return request(DASHBOARD_API);
|
||||
}
|
||||
@ -0,0 +1,136 @@
|
||||
import PageTools from '@/components/page-tools';
|
||||
import { convertFileSize } from '@/utils';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Col, Row, Table } from 'antd';
|
||||
import { useContext } from 'react';
|
||||
import { DashboardContext } from '../config/dashboard-context';
|
||||
|
||||
const projectColumns = [
|
||||
{
|
||||
title: 'Name',
|
||||
dataIndex: 'name',
|
||||
key: 'name'
|
||||
},
|
||||
{
|
||||
title: 'Token Quota',
|
||||
dataIndex: 'quota',
|
||||
key: 'quota',
|
||||
render: (text: any, record: any) => <span>{record.quota}k</span>
|
||||
},
|
||||
{
|
||||
title: 'Token Utilization',
|
||||
dataIndex: 'utilization',
|
||||
key: 'utilization',
|
||||
render: (text: any, record: any) => <span>{record.utilization}%</span>
|
||||
},
|
||||
{
|
||||
title: 'Members',
|
||||
dataIndex: 'members',
|
||||
key: 'members'
|
||||
}
|
||||
];
|
||||
|
||||
const projectData = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'copilot-dev',
|
||||
quota: 100,
|
||||
utilization: 50,
|
||||
members: 4
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'rag-wiki',
|
||||
quota: 200,
|
||||
utilization: 70,
|
||||
members: 3
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'smart-auto-agent',
|
||||
quota: 100,
|
||||
utilization: 20,
|
||||
members: 5
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'office-auto-docs',
|
||||
quota: 100,
|
||||
utilization: 25,
|
||||
members: 1
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: 'smart-customer-service',
|
||||
quota: 100,
|
||||
utilization: 46,
|
||||
members: 2
|
||||
}
|
||||
];
|
||||
const ActiveTable = () => {
|
||||
const intl = useIntl();
|
||||
const data = useContext(DashboardContext).active_models || [];
|
||||
const modelColumns = [
|
||||
{
|
||||
title: intl.formatMessage({ id: 'common.table.name' }),
|
||||
dataIndex: 'name',
|
||||
key: 'name'
|
||||
},
|
||||
// {
|
||||
// title: intl.formatMessage({ id: 'dashboard.gpuutilization' }),
|
||||
// dataIndex: 'gpu_utilization',
|
||||
// key: 'gpu_utilization',
|
||||
// render: (text: any, record: any) => (
|
||||
// <ProgressBar percent={_.round(text, 0)}></ProgressBar>
|
||||
// )
|
||||
// },
|
||||
{
|
||||
title: intl.formatMessage({ id: 'dashboard.allocatevram' }),
|
||||
dataIndex: 'resource_claim.memory',
|
||||
key: 'gpu_memory',
|
||||
render: (text: any, record: any) => {
|
||||
return (
|
||||
<span>
|
||||
{convertFileSize(record.resource_claim?.gpu_memory || 0)} /{' '}
|
||||
{convertFileSize(record.resource_claim?.memory || 0)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
title: intl.formatMessage({ id: 'models.form.replicas' }),
|
||||
dataIndex: 'instance_count',
|
||||
key: 'instance_count'
|
||||
},
|
||||
{
|
||||
title: intl.formatMessage({ id: 'dashboard.tokens' }),
|
||||
dataIndex: 'token_count',
|
||||
key: 'token_count'
|
||||
}
|
||||
];
|
||||
return (
|
||||
<Row gutter={[20, 0]}>
|
||||
<Col xs={24} sm={24} md={24} lg={24} xl={24}>
|
||||
<PageTools
|
||||
style={{ margin: '26px 0px' }}
|
||||
left={
|
||||
<span style={{ padding: '9px 0' }}>
|
||||
{intl.formatMessage({ id: 'dashboard.activeModels' })}
|
||||
</span>
|
||||
}
|
||||
right={false}
|
||||
/>
|
||||
<div>
|
||||
<Table
|
||||
columns={modelColumns}
|
||||
dataSource={data}
|
||||
pagination={false}
|
||||
rowKey="id"
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
|
||||
export default ActiveTable;
|
||||
@ -0,0 +1,63 @@
|
||||
import PageTools from '@/components/page-tools';
|
||||
import { SyncOutlined } from '@ant-design/icons';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Button, Input, Select, Space } from 'antd';
|
||||
import { memo, useCallback, useEffect, useState } from 'react';
|
||||
import { queryDashboardData } from '../apis';
|
||||
import DashboardContext from '../config/dashboard-context';
|
||||
import { DashboardProps } from '../config/types';
|
||||
import ActiveTable from './active-table';
|
||||
import Overview from './over-view';
|
||||
import Usage from './usage';
|
||||
|
||||
const Page: React.FC<{ setLoading: (loading: boolean) => void }> = ({
|
||||
setLoading
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const [data, setData] = useState<DashboardProps>({} as DashboardProps);
|
||||
|
||||
const getDashboardData = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const res = await queryDashboardData();
|
||||
setData(res);
|
||||
setLoading(false);
|
||||
} catch (error) {
|
||||
setLoading(false);
|
||||
setData({} as DashboardProps);
|
||||
}
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
getDashboardData();
|
||||
}, []);
|
||||
return (
|
||||
<DashboardContext.Provider value={{ ...data, fetchData: getDashboardData }}>
|
||||
<Overview></Overview>
|
||||
{/* <SystemLoad></SystemLoad> */}
|
||||
<PageTools
|
||||
left={
|
||||
<Space>
|
||||
<Input
|
||||
placeholder={intl.formatMessage({ id: 'usage.filter.user' })}
|
||||
style={{ width: 200 }}
|
||||
allowClear
|
||||
></Input>
|
||||
<Select
|
||||
style={{ width: 300 }}
|
||||
placeholder={intl.formatMessage({ id: 'usage.filter.model' })}
|
||||
></Select>
|
||||
<Button
|
||||
type="text"
|
||||
style={{ color: 'var(--ant-color-text-tertiary)' }}
|
||||
icon={<SyncOutlined></SyncOutlined>}
|
||||
></Button>
|
||||
</Space>
|
||||
}
|
||||
></PageTools>
|
||||
<Usage></Usage>
|
||||
<ActiveTable></ActiveTable>
|
||||
</DashboardContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(Page);
|
||||
@ -0,0 +1,26 @@
|
||||
:local(.card-body) {
|
||||
box-shadow: none !important;
|
||||
|
||||
:global(.ant-card-body) {
|
||||
height: 110px;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
border-radius: var(--ant-border-radius-lg);
|
||||
border: 1px solid var(--color-border-1);
|
||||
}
|
||||
}
|
||||
|
||||
:local(.row) {
|
||||
:global(.ant-col-5) {
|
||||
flex: 0 0 20%;
|
||||
max-width: 20%;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
}
|
||||
@ -0,0 +1,77 @@
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Card, Col, Row, Space } from 'antd';
|
||||
import _ from 'lodash';
|
||||
import React, { useContext } from 'react';
|
||||
import { overviewConfigs } from '../config';
|
||||
import { DashboardContext } from '../config/dashboard-context';
|
||||
import '../styles/index.less';
|
||||
import styles from './over-view.less';
|
||||
|
||||
const renderCardItem = (data: {
|
||||
label: string;
|
||||
value: React.ReactNode;
|
||||
bgColor: string;
|
||||
}) => {
|
||||
const { label, value, bgColor } = data;
|
||||
return (
|
||||
<Card
|
||||
bordered={false}
|
||||
style={{ background: bgColor }}
|
||||
className={styles['card-body']}
|
||||
>
|
||||
<div className={styles.content}>
|
||||
<div className="label">{label}</div>
|
||||
<div className="value">{value}</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
const Overview: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
const data = useContext(DashboardContext).resource_counts || {};
|
||||
console.log('overview===');
|
||||
const renderValue = (
|
||||
value:
|
||||
| number
|
||||
| {
|
||||
healthy: number;
|
||||
warning: number;
|
||||
error: number;
|
||||
}
|
||||
) => {
|
||||
if (typeof value === 'number') {
|
||||
return value;
|
||||
}
|
||||
return (
|
||||
<Space className="value-box" size={20}>
|
||||
<span className={'value-healthy'}>{value.healthy}</span>
|
||||
<span className={'value-warning'}>{value.warning}</span>
|
||||
<span className={'value-error'}>{value.error}</span>
|
||||
</Space>
|
||||
);
|
||||
};
|
||||
return (
|
||||
<div>
|
||||
<Row gutter={[24, 20]} className={styles.row}>
|
||||
{overviewConfigs.map((config, index) => (
|
||||
<Col
|
||||
xs={{ flex: '100%' }}
|
||||
sm={{ flex: '50%' }}
|
||||
md={{ flex: '50%' }}
|
||||
lg={{ flex: '25%' }}
|
||||
xl={{ flex: '25%' }}
|
||||
key={config.key}
|
||||
>
|
||||
{renderCardItem({
|
||||
label: intl.formatMessage({ id: config.label }),
|
||||
value: renderValue(_.get(data, config.key, 0)),
|
||||
bgColor: config.backgroundColor
|
||||
})}
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Overview;
|
||||
@ -0,0 +1,168 @@
|
||||
import LineChart from '@/components/echarts/line-chart';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import dayjs from 'dayjs';
|
||||
import _ from 'lodash';
|
||||
import { memo, useContext, useMemo } from 'react';
|
||||
import { DashboardContext } from '../config/dashboard-context';
|
||||
|
||||
const chartColorMap = {
|
||||
tickLineColor: 'rgba(217,217,217,0.5)',
|
||||
axislabelColor: 'rgba(0, 0, 0, 0.4)'
|
||||
};
|
||||
|
||||
const TypeKeyMap = {
|
||||
cpu: {
|
||||
label: 'CPU',
|
||||
type: 'CPU',
|
||||
intl: false,
|
||||
color: 'rgba(250, 173, 20,.8)'
|
||||
},
|
||||
memory: {
|
||||
label: 'dashboard.memory',
|
||||
type: 'Memory',
|
||||
intl: true,
|
||||
color: 'rgba(114, 46, 209,.8)'
|
||||
},
|
||||
gpu: {
|
||||
label: 'GPU',
|
||||
type: 'GPU',
|
||||
intl: false,
|
||||
color: 'rgba(84, 204, 152,.8)'
|
||||
},
|
||||
gpu_memory: {
|
||||
label: 'dashboard.vram',
|
||||
type: 'VRAM',
|
||||
intl: true,
|
||||
color: 'rgba(255, 107, 179, 80%)'
|
||||
}
|
||||
};
|
||||
|
||||
const option = {
|
||||
title: {
|
||||
text: ''
|
||||
},
|
||||
legend: {
|
||||
itemWidth: 8,
|
||||
itemHeight: 8,
|
||||
data: []
|
||||
},
|
||||
grid: {
|
||||
left: 0,
|
||||
right: 20,
|
||||
bottom: 20,
|
||||
containLabel: true
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
formatter(params: any) {
|
||||
let result = `<span class="tooltip-x-name">${params[0].axisValue}</span>`;
|
||||
params.forEach((item: any) => {
|
||||
result += `<span class="tooltip-item">
|
||||
<span class="tooltip-item-name">
|
||||
<span style="display:inline-block;margin-right:5px;border-radius:8px;width:8px;height:8px;background-color:${item.color};"></span>
|
||||
<span class="tooltip-title">${item.seriesName}</span>:
|
||||
</span>
|
||||
<span class="tooltip-value">${item.data.value}</span>
|
||||
</span>`;
|
||||
});
|
||||
return `<div class="tooltip-wrapper">${result}</div>`;
|
||||
}
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: true,
|
||||
axisTick: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: chartColorMap.tickLineColor
|
||||
}
|
||||
},
|
||||
axisLabel: {
|
||||
color: chartColorMap.axislabelColor,
|
||||
// fontFamily: 'unset',
|
||||
fontSize: 12
|
||||
},
|
||||
axisLine: {
|
||||
show: false
|
||||
},
|
||||
data: []
|
||||
},
|
||||
yAxis: {
|
||||
max: 100,
|
||||
min: 0,
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
type: 'dashed'
|
||||
}
|
||||
},
|
||||
axisLabel: {
|
||||
color: chartColorMap.axislabelColor,
|
||||
// fontFamily: 'unset',
|
||||
fontSize: 12
|
||||
},
|
||||
axisTick: {
|
||||
show: false
|
||||
},
|
||||
type: 'value'
|
||||
},
|
||||
series: []
|
||||
};
|
||||
|
||||
const UtilizationOvertime: React.FC = () => {
|
||||
console.log('systemload=====================');
|
||||
const intl = useIntl();
|
||||
const data = useContext(DashboardContext)?.system_load?.history || {};
|
||||
|
||||
const typeList = ['gpu', 'cpu', 'memory', 'gpu_memory'];
|
||||
|
||||
const tooltipValueFormatter = (value: any) => {
|
||||
return !value ? value : `${value}%`;
|
||||
};
|
||||
const generateData = useMemo(() => {
|
||||
const legendData: string[] = [];
|
||||
const xAxisData: string[] = [];
|
||||
let seriesData: { value: number; time: string; type: string }[] = [];
|
||||
seriesData = _.map(typeList, (item: string) => {
|
||||
const itemConfig = _.get(TypeKeyMap, item, {});
|
||||
const name = itemConfig.intl
|
||||
? intl.formatMessage({ id: itemConfig.label })
|
||||
: itemConfig.label;
|
||||
legendData.push(name);
|
||||
const itemDataList = _.get(data, item, []);
|
||||
return {
|
||||
name: name,
|
||||
color: itemConfig.color,
|
||||
data: _.map(itemDataList, (item: any) => {
|
||||
xAxisData.push(dayjs(item.timestamp * 1000).format('HH:mm:ss'));
|
||||
return {
|
||||
time: dayjs(item.timestamp * 1000).format('HH:mm:ss'),
|
||||
value: _.round(_.get(item, 'value', 0), 1)
|
||||
};
|
||||
})
|
||||
};
|
||||
});
|
||||
return {
|
||||
seriesData,
|
||||
legendData,
|
||||
xAxisData: _.uniq(xAxisData)
|
||||
};
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<LineChart
|
||||
height={390}
|
||||
seriesData={generateData.seriesData}
|
||||
legendData={generateData.legendData}
|
||||
xAxisData={generateData.xAxisData}
|
||||
tooltipValueFormatter={tooltipValueFormatter}
|
||||
smooth={true}
|
||||
width="100%"
|
||||
yAxisName="(%)"
|
||||
></LineChart>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(UtilizationOvertime);
|
||||
@ -0,0 +1,119 @@
|
||||
import CardWrapper from '@/components/card-wrapper';
|
||||
import GaugeChart from '@/components/echarts/gauge';
|
||||
import PageTools from '@/components/page-tools';
|
||||
import breakpoints from '@/config/breakpoints';
|
||||
import useWindowResize from '@/hooks/use-window-resize';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Col, Row } from 'antd';
|
||||
import dayjs from 'dayjs';
|
||||
import _ from 'lodash';
|
||||
import { useContext, useEffect, useState } from 'react';
|
||||
import { DashboardContext } from '../config/dashboard-context';
|
||||
import ResourceUtilization from './resource-utilization';
|
||||
|
||||
const strokeColorFunc = (percent: number) => {
|
||||
if (percent <= 50) {
|
||||
return 'rgb(84, 204, 152, 80%)';
|
||||
}
|
||||
if (percent <= 80) {
|
||||
return 'rgba(250, 173, 20, 80%)';
|
||||
}
|
||||
return ' rgba(255, 77, 79, 80%)';
|
||||
};
|
||||
|
||||
const SystemLoad = () => {
|
||||
const intl = useIntl();
|
||||
const data = useContext(DashboardContext)?.system_load?.current || {};
|
||||
const { size } = useWindowResize();
|
||||
const [paddingRight, setPaddingRight] = useState<string>('20px');
|
||||
const [smallChartHeight, setSmallChartHeight] = useState<number>(190);
|
||||
const [largeChartHeight, setLargeChartHeight] = useState<number>(400);
|
||||
const thresholds = [0.5, 0.7, 1];
|
||||
const height = 400;
|
||||
const currentDate = dayjs().format('YYYY-MM-DD');
|
||||
|
||||
const handleSelectDate = (date: any) => {};
|
||||
|
||||
useEffect(() => {
|
||||
if (size.width < breakpoints.xl) {
|
||||
setPaddingRight('0');
|
||||
} else {
|
||||
setPaddingRight('20px');
|
||||
}
|
||||
}, [size.width]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="system-load">
|
||||
<PageTools
|
||||
style={{ margin: '26px 0px' }}
|
||||
left={
|
||||
<span>{intl.formatMessage({ id: 'dashboard.systemload' })}</span>
|
||||
}
|
||||
/>
|
||||
<Row style={{ width: '100%' }} gutter={[0, 20]}>
|
||||
<Col
|
||||
xs={24}
|
||||
sm={24}
|
||||
md={24}
|
||||
lg={24}
|
||||
xl={16}
|
||||
style={{ paddingRight: paddingRight }}
|
||||
>
|
||||
<CardWrapper style={{ height: height, width: '100%' }}>
|
||||
<ResourceUtilization />
|
||||
</CardWrapper>
|
||||
</Col>
|
||||
<Col xs={24} sm={24} md={24} lg={24} xl={8}>
|
||||
<CardWrapper style={{ height: largeChartHeight, width: '100%' }}>
|
||||
<Row style={{ height: largeChartHeight, width: '100%' }}>
|
||||
<Col span={12} style={{ height: smallChartHeight }}>
|
||||
<GaugeChart
|
||||
height={smallChartHeight}
|
||||
value={_.round(data.gpu || 0, 1)}
|
||||
color={strokeColorFunc(data.gpu)}
|
||||
title={intl.formatMessage({
|
||||
id: 'dashboard.gpuutilization'
|
||||
})}
|
||||
></GaugeChart>
|
||||
</Col>
|
||||
<Col span={12} style={{ height: smallChartHeight }}>
|
||||
<GaugeChart
|
||||
title={intl.formatMessage({
|
||||
id: 'dashboard.vramutilization'
|
||||
})}
|
||||
height={smallChartHeight}
|
||||
color={strokeColorFunc(data.gpu_memory)}
|
||||
value={_.round(data.gpu_memory || 0, 1)}
|
||||
></GaugeChart>
|
||||
</Col>
|
||||
<Col span={12} style={{ height: smallChartHeight }}>
|
||||
<GaugeChart
|
||||
title={intl.formatMessage({
|
||||
id: 'dashboard.cpuutilization'
|
||||
})}
|
||||
height={smallChartHeight}
|
||||
color={strokeColorFunc(data.cpu)}
|
||||
value={_.round(data.cpu || 0, 1)}
|
||||
></GaugeChart>
|
||||
</Col>
|
||||
<Col span={12} style={{ height: smallChartHeight }}>
|
||||
<GaugeChart
|
||||
title={intl.formatMessage({
|
||||
id: 'dashboard.memoryutilization'
|
||||
})}
|
||||
height={smallChartHeight}
|
||||
color={strokeColorFunc(data.memory)}
|
||||
value={_.round(data.memory || 0, 1)}
|
||||
></GaugeChart>
|
||||
</Col>
|
||||
</Row>
|
||||
</CardWrapper>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SystemLoad;
|
||||
@ -0,0 +1,179 @@
|
||||
import PageTools from '@/components/page-tools';
|
||||
|
||||
import { useIntl } from '@umijs/max';
|
||||
import dayjs from 'dayjs';
|
||||
import _ from 'lodash';
|
||||
import { memo, useCallback, useContext } from 'react';
|
||||
import { DashboardContext } from '../../config/dashboard-context';
|
||||
import RequestTokenInner from './request-token-inner';
|
||||
|
||||
const baseColorMap = {
|
||||
baseL2: 'rgba(13,171,219,0.8)',
|
||||
baseL1: 'rgba(0,34,255,0.8)',
|
||||
base: 'rgba(0,85,255,0.8)',
|
||||
baseR1: 'rgba(0,255,233,0.8)',
|
||||
baseR2: 'rgba(48,0,255,0.8)',
|
||||
baseR3: 'rgba(85,167,255,0.8)'
|
||||
};
|
||||
|
||||
const getCurrentMonthDays = () => {
|
||||
const now = dayjs();
|
||||
const daysInMonth = now.daysInMonth();
|
||||
const year = dayjs().year();
|
||||
const month = dayjs().month() + 1;
|
||||
|
||||
const dates = [];
|
||||
for (let day = 1; day <= daysInMonth; day++) {
|
||||
dates.push(dayjs(`${year}-${month}-${day}`).format('YYYY-MM-DD'));
|
||||
}
|
||||
return dates;
|
||||
};
|
||||
|
||||
const UsageInner: React.FC<{ paddingRight: string }> = ({ paddingRight }) => {
|
||||
const intl = useIntl();
|
||||
const currentDate = dayjs();
|
||||
const currentMonthDays = getCurrentMonthDays();
|
||||
let requestData: {
|
||||
name: string;
|
||||
color: string;
|
||||
areaStyle: any;
|
||||
data: { time: string; value: number }[];
|
||||
}[] = [];
|
||||
let tokenData: { time: string; value: number }[] = [];
|
||||
let userData: { name: string; value: number }[] = [];
|
||||
const xAxisData: string[] = currentMonthDays;
|
||||
let topUserList: string[] = [];
|
||||
|
||||
const { model_usage } = useContext(DashboardContext);
|
||||
const data = model_usage || {};
|
||||
|
||||
const handleSelectDate = (date: any) => {
|
||||
// fetchData?.();
|
||||
};
|
||||
|
||||
const generateData = useCallback(() => {
|
||||
const requestList: {
|
||||
name: string;
|
||||
color: string;
|
||||
areaStyle: any;
|
||||
data: { time: string; value: number }[];
|
||||
} = {
|
||||
name: 'API requests',
|
||||
areaStyle: {},
|
||||
color: baseColorMap.base,
|
||||
data: []
|
||||
};
|
||||
|
||||
const completionData: any = {
|
||||
name: 'Completion tokens',
|
||||
color: baseColorMap.base,
|
||||
data: []
|
||||
};
|
||||
const prompData: any = {
|
||||
name: 'Prompt tokens',
|
||||
color: baseColorMap.baseR3,
|
||||
data: []
|
||||
};
|
||||
|
||||
const topUserPrompt: any = {
|
||||
name: 'Prompt tokens',
|
||||
color: baseColorMap.baseR3,
|
||||
data: []
|
||||
};
|
||||
const topUserCompletion: any = {
|
||||
name: 'Completion tokens',
|
||||
color: baseColorMap.base,
|
||||
data: []
|
||||
};
|
||||
|
||||
_.each(xAxisData, (date: string) => {
|
||||
// tokens data
|
||||
const item = _.find(data.completion_token_history, (item: any) => {
|
||||
return dayjs(item.timestamp * 1000).format('YYYY-MM-DD') === date;
|
||||
});
|
||||
if (!item) {
|
||||
completionData.data.push({
|
||||
titme: date,
|
||||
value: 0
|
||||
});
|
||||
} else {
|
||||
completionData.data.push({
|
||||
value: item.value,
|
||||
time: dayjs(item.timestamp * 1000).format('YYYY-MM-DD')
|
||||
});
|
||||
}
|
||||
|
||||
const promptItem = _.find(data.prompt_token_history, (item: any) => {
|
||||
return dayjs(item.timestamp * 1000).format('YYYY-MM-DD') === date;
|
||||
});
|
||||
if (!promptItem) {
|
||||
prompData.data.push({
|
||||
value: 0,
|
||||
time: date
|
||||
});
|
||||
} else {
|
||||
prompData.data.push({
|
||||
value: promptItem.value,
|
||||
time: dayjs(promptItem.timestamp * 1000).format('YYYY-MM-DD')
|
||||
});
|
||||
}
|
||||
|
||||
// ============== api request data =================
|
||||
const requestItem = _.find(data.api_request_history, (item: any) => {
|
||||
return dayjs(item.timestamp * 1000).format('YYYY-MM-DD') === date;
|
||||
});
|
||||
|
||||
if (!requestItem) {
|
||||
requestList.data.push({
|
||||
time: date,
|
||||
value: 0
|
||||
});
|
||||
} else {
|
||||
requestList.data.push({
|
||||
time: dayjs(requestItem.timestamp * 1000).format('YYYY-MM-DD'),
|
||||
value: requestItem.value
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// ========== top users ============
|
||||
if (!data.top_users?.length) {
|
||||
userData = [];
|
||||
topUserList = [];
|
||||
} else {
|
||||
const users: string[] = [];
|
||||
_.each(data.top_users, (item: any) => {
|
||||
users.push(item.username);
|
||||
topUserPrompt.data.push({
|
||||
name: item.username,
|
||||
value: item.prompt_token_count
|
||||
});
|
||||
topUserCompletion.data.push({
|
||||
name: item.username,
|
||||
value: item.completion_token_count
|
||||
});
|
||||
});
|
||||
topUserList = _.uniq(users);
|
||||
userData = [topUserCompletion, topUserPrompt];
|
||||
}
|
||||
|
||||
requestData = [requestList];
|
||||
tokenData = [completionData, prompData];
|
||||
}, [data, xAxisData]);
|
||||
|
||||
generateData();
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageTools />
|
||||
<RequestTokenInner
|
||||
requestData={requestData}
|
||||
xAxisData={xAxisData}
|
||||
tokenData={tokenData}
|
||||
paddingRight={paddingRight}
|
||||
></RequestTokenInner>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(UsageInner);
|
||||
@ -0,0 +1,63 @@
|
||||
import CardWrapper from '@/components/card-wrapper';
|
||||
import BarChart from '@/components/echarts/bar-chart';
|
||||
import LineChart from '@/components/echarts/line-chart';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Col, Row } from 'antd';
|
||||
import dayjs from 'dayjs';
|
||||
import React from 'react';
|
||||
|
||||
interface RequestTokenInnerProps {
|
||||
requestData: {
|
||||
name: string;
|
||||
color: string;
|
||||
areaStyle: any;
|
||||
data: { time: string; value: number }[];
|
||||
}[];
|
||||
tokenData: { time: string; value: number }[];
|
||||
xAxisData: string[];
|
||||
}
|
||||
const RequestTokenInner: React.FC<
|
||||
RequestTokenInnerProps & { paddingRight: string }
|
||||
> = (props) => {
|
||||
console.log('request token inner=====================');
|
||||
const { requestData, tokenData, xAxisData, paddingRight } = props;
|
||||
const intl = useIntl();
|
||||
const labelFormatter = (v: any) => {
|
||||
return dayjs(v).format('MM-DD');
|
||||
};
|
||||
return (
|
||||
<Row style={{ width: '100%' }} gutter={[0, 20]}>
|
||||
<Col
|
||||
xs={24}
|
||||
sm={24}
|
||||
md={24}
|
||||
lg={24}
|
||||
xl={12}
|
||||
style={{ paddingRight: paddingRight }}
|
||||
>
|
||||
<CardWrapper style={{ width: '100%' }}>
|
||||
<LineChart
|
||||
title={intl.formatMessage({ id: 'dashboard.apirequest' })}
|
||||
seriesData={requestData}
|
||||
xAxisData={xAxisData}
|
||||
height={360}
|
||||
labelFormatter={labelFormatter}
|
||||
></LineChart>
|
||||
</CardWrapper>
|
||||
</Col>
|
||||
<Col xs={24} sm={24} md={24} lg={24} xl={12}>
|
||||
<CardWrapper style={{ width: '100%' }}>
|
||||
<BarChart
|
||||
title={intl.formatMessage({ id: 'dashboard.tokens' })}
|
||||
seriesData={tokenData}
|
||||
xAxisData={xAxisData}
|
||||
height={360}
|
||||
labelFormatter={labelFormatter}
|
||||
></BarChart>
|
||||
</CardWrapper>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(RequestTokenInner);
|
||||
@ -0,0 +1,27 @@
|
||||
import CardWrapper from '@/components/card-wrapper';
|
||||
import HBar from '@/components/echarts/h-bar';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import React from 'react';
|
||||
|
||||
interface TopUserProps {
|
||||
userData: { name: string; value: number }[];
|
||||
topUserList: string[];
|
||||
}
|
||||
const TopUser: React.FC<TopUserProps> = (props) => {
|
||||
console.log('TopUser=====================');
|
||||
const { userData, topUserList } = props;
|
||||
const intl = useIntl();
|
||||
|
||||
return (
|
||||
<CardWrapper>
|
||||
<HBar
|
||||
title={intl.formatMessage({ id: 'dashboard.topusers' })}
|
||||
seriesData={userData}
|
||||
xAxisData={topUserList}
|
||||
height={360}
|
||||
></HBar>
|
||||
</CardWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(TopUser);
|
||||
@ -0,0 +1,23 @@
|
||||
import breakpoints from '@/config/breakpoints';
|
||||
import useWindowResize from '@/hooks/use-window-resize';
|
||||
import { useEffect, useState } from 'react';
|
||||
import UserInner from './usage-inner';
|
||||
|
||||
const Usage = () => {
|
||||
const {
|
||||
size: { width }
|
||||
} = useWindowResize();
|
||||
const [paddingRight, setPaddingRight] = useState<string>('20px');
|
||||
|
||||
useEffect(() => {
|
||||
if (width < breakpoints.xl) {
|
||||
setPaddingRight('0');
|
||||
} else {
|
||||
setPaddingRight('20px');
|
||||
}
|
||||
}, [width]);
|
||||
|
||||
return <UserInner paddingRight={paddingRight}></UserInner>;
|
||||
};
|
||||
|
||||
export default Usage;
|
||||
@ -0,0 +1,8 @@
|
||||
import { createContext } from 'react';
|
||||
import { DashboardProps } from './types';
|
||||
|
||||
export const DashboardContext = createContext<
|
||||
DashboardProps & { fetchData: () => Promise<void> }
|
||||
>({} as DashboardProps & { fetchData: () => Promise<void> });
|
||||
|
||||
export default DashboardContext;
|
||||
@ -0,0 +1,23 @@
|
||||
export const overviewConfigs = [
|
||||
{
|
||||
key: 'worker_count',
|
||||
label: 'dashboard.workers',
|
||||
backgroundColor: 'var(--color-white-1)'
|
||||
},
|
||||
{
|
||||
key: 'gpu_count',
|
||||
label: 'dashboard.totalgpus',
|
||||
backgroundColor: 'var(--color-white-1)'
|
||||
},
|
||||
|
||||
{
|
||||
key: 'model_count',
|
||||
label: 'dashboard.models',
|
||||
backgroundColor: 'var(--color-white-1)'
|
||||
},
|
||||
{
|
||||
key: 'model_instance_count',
|
||||
label: 'models.form.replicas',
|
||||
backgroundColor: 'var(--color-white-1)'
|
||||
}
|
||||
];
|
||||
@ -0,0 +1,46 @@
|
||||
export interface DashboardProps {
|
||||
resource_counts: {
|
||||
worker_count: number;
|
||||
gpu_count: number;
|
||||
model_count: number;
|
||||
model_instance_count: number;
|
||||
};
|
||||
system_load: {
|
||||
current: {
|
||||
cpu: number;
|
||||
memory: number;
|
||||
gpu: number;
|
||||
gpu_memory: number;
|
||||
};
|
||||
history: {
|
||||
cpu: {
|
||||
timestamp: number;
|
||||
value: number;
|
||||
}[];
|
||||
memory: {
|
||||
timestamp: number;
|
||||
value: number;
|
||||
}[];
|
||||
gpu: {
|
||||
timestamp: number;
|
||||
value: number;
|
||||
}[];
|
||||
gpu_memory: {
|
||||
timestamp: number;
|
||||
value: number;
|
||||
}[];
|
||||
};
|
||||
};
|
||||
model_usage: {
|
||||
api_request_history: any[];
|
||||
completion_token_history: any[];
|
||||
prompt_token_history: any[];
|
||||
top_users: {
|
||||
user_id: number;
|
||||
username: string;
|
||||
prompt_token_count: number;
|
||||
completion_token_count: number;
|
||||
}[];
|
||||
};
|
||||
active_models: any[];
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
import { PageContainer } from '@ant-design/pro-components';
|
||||
import { Spin } from 'antd';
|
||||
import { memo, useState } from 'react';
|
||||
import DashboardInner from './components/dahboard-inner';
|
||||
|
||||
const Dashboard: React.FC = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageContainer ghost extra={[]}>
|
||||
<Spin spinning={loading}>
|
||||
<DashboardInner setLoading={setLoading} />
|
||||
</Spin>
|
||||
</PageContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(Dashboard);
|
||||
@ -0,0 +1,17 @@
|
||||
.value-box {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
|
||||
.value-healthy {
|
||||
color: var(--ant-green-6);
|
||||
}
|
||||
|
||||
.value-warning {
|
||||
color: var(--ant-gold-6);
|
||||
}
|
||||
|
||||
.value-error {
|
||||
color: var(--ant-red-6);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in new issue