feat: dashboard

This commit is contained in:
jialin
2024-05-22 09:29:30 +08:00
parent a416bc7eab
commit 46a9e6a931
21 changed files with 1522 additions and 53 deletions
@@ -0,0 +1,42 @@
import { Pie } from '@ant-design/plots';
import numeral from 'numeral';
const TooltipContent = (props: any) => {
return (
<div>
<div>{props.x}</div>
<div>{props.y}</div>
</div>
);
};
const GPUUtilization: React.FC = () => {
const data = [
{ label: 'Idle GPUs', value: 20 },
{ label: 'Allocated GPUs', value: 120 }
];
return (
<Pie
height={340}
radius={0.9}
angleField="value"
colorField="label"
data={data as any}
legend={{
color: {
position: 'bottom',
layout: {
justifyContent: 'center'
}
}
}}
label={{
position: 'outside',
text: (item: { label: number; value: number }) => {
return `${item.label}: ${numeral(item.value).format('0,0')}`;
}
}}
/>
);
};
export default GPUUtilization;
@@ -0,0 +1,191 @@
import PageTools from '@/components/page-tools';
import { Column } from '@ant-design/plots';
import { DatePicker, Select, Space } from 'antd';
import { useState } from 'react';
const { RangePicker } = DatePicker;
const ModelBills: React.FC = () => {
const [selectName, setSelectName] = useState('model1');
const handleNameChange = (value: string) => {
setSelectName(value);
};
const models = [
{
value: 'model1',
label: 'model1'
},
{
value: 'model2',
label: 'model2'
},
{
value: 'model3',
label: 'model3'
}
];
const config = {
data: [
{
time: '2021-01',
name: 'model1',
value: 3.9
},
{
time: '2021-01',
name: 'model2',
value: 4.3
},
{
time: '2021-01',
name: 'model3',
value: 3.8
},
{
time: '2021-02',
name: 'model1',
value: 4.1
},
{
time: '2021-02',
name: 'model2',
value: 5.7
},
{
time: '2021-02',
name: 'model3',
value: 3.9
},
{
time: '2021-03',
name: 'model1',
value: 5.6
},
{
time: '2021-03',
name: 'model2',
value: 7.3
},
{
time: '2021-03',
name: 'model3',
value: 4.9
},
{
time: '2021-04',
name: 'model1',
value: 7.2
},
{
time: '2021-04',
name: 'model2',
value: 9.6
},
{
time: '2021-04',
name: 'model3',
value: 6.3
},
{
time: '2021-05',
name: 'model1',
value: 9.3
},
{
time: '2021-05',
name: 'model2',
value: 11.9
},
{
time: '2021-05',
name: 'model3',
value: 8.6
},
{
time: '2021-06',
name: 'model1',
value: 10.3
},
{
time: '2021-06',
name: 'model2',
value: 13.9
},
{
time: '2021-06',
name: 'model3',
value: 9.6
}
],
xField: 'time',
yField: 'value',
colorField: 'name',
group: true,
legend: {
color: {
position: 'top',
layout: {
justifyContent: 'center'
}
}
},
axis: {
x: {
xAxis: true
}
},
split: {
type: 'line',
line: {
color: 'red',
style: {
color: 'red',
lineDash: [4, 5]
}
}
},
style: {
radiusTopLeft: 10,
radiusTopRight: 10,
width: 25
}
};
const handleSelectDate = (value: any) => {
console.log(value);
};
return (
<>
<PageTools
marginBottom={10}
marginTop={0}
left={<span>Bills by model</span>}
right={
<Space>
<Select
size="middle"
options={models}
value={selectName}
style={{ width: 300, height: 34 }}
onChange={handleNameChange}
></Select>
<RangePicker
showTime={false}
format="YYYY-MM-DD"
onChange={(value, dateString) => {
console.log('Selected Time: ', value);
console.log('Formatted Selected Time: ', dateString);
}}
onOk={handleSelectDate}
/>
</Space>
}
></PageTools>
<Column {...config} />
</>
);
};
export default ModelBills;
@@ -0,0 +1,268 @@
import PageTools from '@/components/page-tools';
import StatusTag from '@/components/status-tag';
import useTableRowSelection from '@/hooks/use-table-row-selection';
import { NodeItem } from '@/pages/nodes/config/types';
import { Progress, Select, Space, Table } from 'antd';
import _ from 'lodash';
import { memo, useMemo, useState } from 'react';
const { Column } = Table;
const models = [
{ value: 'llama3:latest', label: 'llama3:latest' },
{ value: 'wangfuyun/AnimateLCM', label: 'wangfuyun/AnimateLCM' },
{ value: 'Revanthraja/Text_to_Vision', label: 'Revanthraja/Text_to_Vision' }
];
const dataSource: NodeItem[] = [
{
id: 1,
name: '192.168.1.2',
address: '192.168.1.2',
hostname: 'node-1',
labels: {},
resources: {
capacity: {
cpu: 4,
gpu: 2,
memory: '64 GiB',
gram: '24 Gib'
},
allocable: {
cpu: 2.5,
gpu: 1.6,
memory: '64',
gram: '24 Gib'
}
},
state: 'ALIVE'
},
{
id: 1,
name: '192.168.1.2',
address: '192.168.1.5',
hostname: 'node-2',
labels: {},
resources: {
capacity: {
cpu: 4,
gpu: 2,
memory: '64 GiB',
gram: '24 Gib'
},
allocable: {
cpu: 2,
gpu: 1.5,
memory: '32 GiB',
gram: '12 Gib'
}
},
state: 'ALIVE'
},
{
id: 3,
name: '192.168.1.3',
address: '192.168.1.3',
hostname: 'node-3',
labels: {},
resources: {
capacity: {
cpu: 4,
gpu: 2,
memory: '64 GiB',
gram: '24 Gib'
},
allocable: {
cpu: 1,
gpu: 0.5,
memory: '28 GiB',
gram: '6 Gib'
}
},
state: 'ALIVE'
}
];
const Models: React.FC = () => {
const rowSelection = useTableRowSelection();
const [selectName, setSelectName] = useState<string>('llama3:latest');
const [loading, setLoading] = useState(false);
const [queryParams, setQueryParams] = useState({
current: 1,
pageSize: 10,
name: ''
});
const handleShowSizeChange = (current: number, size: number) => {
console.log(current, size);
};
const handlePageChange = (page: number, pageSize: number | undefined) => {
console.log(page, pageSize);
};
const handleTableChange = (pagination: any, filters: any, sorter: any) => {
// console.log('handleTableChange=======', pagination, filters, sorter);
};
const fetchData = async () => {
console.log('fetchData');
};
const handleSearch = (e: any) => {
fetchData();
};
const handleNameChange = (value: string) => {
setSelectName(value);
setQueryParams({
...queryParams,
name: value
});
};
const RenderProgress = memo(
(props: { record: NodeItem; dataIndex: string }) => {
const { record, dataIndex } = props;
const value1 = useMemo(() => {
let value = _.get(record, ['resources', 'allocable', dataIndex]);
if (['gram', 'memory'].includes(dataIndex)) {
value = _.toNumber(value.replace(/GiB|Gib/, ''));
}
return value;
}, [record, dataIndex]);
const value2 = useMemo(() => {
let value = _.get(record, ['resources', 'capacity', dataIndex]);
if (['gram', 'memory'].includes(dataIndex)) {
value = _.toNumber(value.replace(/GiB|Gib/, ''));
}
return value;
}, [record, dataIndex]);
if (!value1 || !value2) {
return <Progress percent={0} strokeColor="var(--ant-color-primary)" />;
}
const percent = _.round(value1 / value2, 2) * 100;
const strokeColor = useMemo(() => {
if (percent <= 50) {
return 'var(--ant-color-primary)';
}
if (percent <= 80) {
return 'var(--ant-color-warning)';
}
return 'var(--ant-color-error)';
}, [percent]);
return (
<Progress
steps={5}
format={() => {
return (
<span style={{ color: 'var(--ant-color-text)' }}>{percent}%</span>
);
}}
percent={percent}
strokeColor={strokeColor}
/>
);
}
);
return (
<>
<div>
<PageTools
marginBottom={10}
marginTop={0}
left={<span>Nodes by model</span>}
right={
<Space>
<Select
size="middle"
options={models}
value={selectName}
style={{ width: 300, height: 34 }}
onChange={handleNameChange}
></Select>
</Space>
}
></PageTools>
<Table
dataSource={dataSource}
loading={loading}
onChange={handleTableChange}
pagination={false}
>
<Column title="Host Name" dataIndex="hostname" key="hostname" />
<Column
title="State"
dataIndex="state"
key="state"
render={(text, record) => {
return (
<StatusTag
statusValue={{
status: 'success',
text: 'ALIVE'
}}
></StatusTag>
);
}}
/>
<Column title="IP / PID" dataIndex="address" key="address" />
<Column
title="CPU"
dataIndex="CPU"
key="CPU"
render={(text, record: NodeItem) => {
return (
<RenderProgress
record={record}
dataIndex="cpu"
></RenderProgress>
);
}}
/>
<Column
title="Memory"
dataIndex="memory"
key="Memory"
render={(text, record: NodeItem) => {
return (
<RenderProgress
record={record}
dataIndex="memory"
></RenderProgress>
);
}}
/>
<Column
title="GPU"
dataIndex="GPU"
key="GPU"
render={(text, record: NodeItem) => {
return (
<RenderProgress
record={record}
dataIndex="gpu"
></RenderProgress>
);
}}
/>
<Column
title="GRAM"
dataIndex="GRAM"
key="GRAM"
render={(text, record: NodeItem) => {
return (
<RenderProgress
record={record}
dataIndex="gram"
></RenderProgress>
);
}}
/>
</Table>
</div>
</>
);
};
export default Models;
@@ -0,0 +1,20 @@
:local(.card-body) {
:global(.ant-card-body) {
height: 100px;
display: flex;
justify-content: space-around;
}
}
: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,41 @@
import { Card, Col, Row } from 'antd';
import { overviewConfigs } from '../config';
import styles from './over-view.less';
const CardItem: React.FC<{ label: string; value: number; bgColor: string }> = ({
label,
value,
bgColor
}) => {
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 = (props) => {
const { data = {} } = props;
return (
<Row gutter={[20, 20]} className={styles.row}>
{overviewConfigs.map((config, index) => (
<Col span={5}>
<CardItem
key={config.key}
label={config.label}
value={data[config.key] || 0}
bgColor={config.backgroundColor}
/>
</Col>
))}
</Row>
);
};
export default Overview;
@@ -0,0 +1,93 @@
import PageTools from '@/components/page-tools';
import { Line } from '@ant-design/plots';
import { DatePicker } from 'antd';
import _ from 'lodash';
const UtilizationOvertime: React.FC = () => {
const timeList = [
'01:00:00',
'02:00:00',
'03:00:00',
'04:00:00',
'05:00:00',
'06:00:00',
'07:00:00',
'08:00:00',
'09:00:00',
'10:00:00',
'11:00:00',
'12:00:00',
'13:00:00',
'14:00:00',
'15:00:00'
];
const typeList = ['GPU', 'CPU', 'Memory'];
const generateData = () => {
const data = [];
for (let i = 0; i < timeList.length; i++) {
for (let j = 0; j < typeList.length; j++) {
data.push({
time: timeList[i],
type: typeList[j],
value: _.round(Math.random(), 3)
});
}
}
return data;
};
const data = generateData();
const handleSelectDate = (date: string) => {
console.log('dateString============', date);
};
const config = {
xField: 'time',
yField: 'value',
color: ['red', 'blue', 'green'],
colorField: 'type',
slider: false,
axis: {
x: {
textStyle: {
autoRoate: true
}
}
},
xAxis: {
autoRotate: true
},
yAxis: {
nice: true,
label: {
autoRotate: true
}
},
point: {
shapeField: 'circle',
sizeField: 2
},
legend: {
color: {
layout: { justifyContent: 'center' }
}
},
label: {
autoRotate: true
}
};
return (
<>
<PageTools
marginBottom={10}
marginTop={0}
left={<span>Utilization Over Time</span>}
right={
<DatePicker onChange={handleSelectDate} style={{ width: 300 }} />
}
/>
<Line height={400} data={data} {...config} />
</>
);
};
export default UtilizationOvertime;
+32
View File
@@ -0,0 +1,32 @@
export const overviewConfigs = [
{
key: 'models',
label: 'Models',
backgroundColor:
'linear-gradient(180deg, rgba(0,188,203,.2) 0%, rgba(40,207,181,.2) 100%)'
},
{
key: 'nodes',
label: 'Nodes',
backgroundColor:
'linear-gradient(180deg, rgba(0,188,203,.2) 0%, rgba(40,207,181,.2) 100%)'
},
{
key: 'gpus',
label: 'Total GPUs',
backgroundColor:
'linear-gradient(180deg, rgba(0,188,203,.2) 0%, rgba(40,207,181,.2) 100%)'
},
{
key: 'idleGpus',
label: 'Idle GPUs',
backgroundColor:
'linear-gradient(180deg, rgba(0,188,203,.2) 0%, rgba(40,207,181,.2) 100%)'
},
{
key: 'allocatedGpus',
label: 'Allocated GPUs',
backgroundColor:
'linear-gradient(180deg, rgba(0,188,203,.2) 0%, rgba(40,207,181,.2) 100%)'
}
];
+52 -12
View File
@@ -1,19 +1,59 @@
import DividerLine from '@/components/divider-line';
import PageTools from '@/components/page-tools';
import { PageContainer } from '@ant-design/pro-components';
import { Access, useAccess } from '@umijs/max';
import { Button } from 'antd';
import { Col, Row } from 'antd';
import GPUUtilization from './components/gpu-utilization';
import ModelBills from './components/model-bills';
import ModelNodes from './components/model-nodes';
import Overview from './components/over-view';
import UtilizationOvertime from './components/utilization-overtime';
const Dashboard: React.FC = () => {
const access = useAccess();
return (
<PageContainer
ghost
>
<Access accessible={access.canSeeAdmin}>
<Button type="primary"> Admin </Button>
</Access>
</PageContainer>
<>
<PageContainer ghost title={false}>
<Overview></Overview>
</PageContainer>
<DividerLine></DividerLine>
<PageContainer ghost title={false}>
<Row gutter={20} style={{ marginTop: '0px' }}>
<Col span={17}>
<ModelNodes></ModelNodes>
</Col>
<Col span={7}>
<PageTools
marginBottom={10}
marginTop={0}
left={
<div
style={{
height: '34px',
display: 'flex',
alignItems: 'center'
}}
>
GPU Utilization
</div>
}
></PageTools>
<GPUUtilization></GPUUtilization>
</Col>
</Row>
</PageContainer>
<DividerLine></DividerLine>
<PageContainer ghost title={false}>
<div style={{ marginTop: '0px' }}>
<ModelBills></ModelBills>
</div>
</PageContainer>
<DividerLine></DividerLine>
<PageContainer ghost title={false}>
<div style={{ marginTop: '0px' }}>
<UtilizationOvertime></UtilizationOvertime>
</div>
</PageContainer>
</>
);
};
export default Dashboard;
export default Dashboard;