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
+8 -26
View File
@@ -4,14 +4,14 @@ export default [
key: 'dashboard',
layout: false,
icon: 'home',
redirect: '/dashboard',
redirect: '/dashboard'
},
{
name: 'Dashboard',
path: '/dashboard',
key: 'dashboard',
icon: 'home',
component: './dashboard',
component: './dashboard'
},
{
name: 'Playground',
@@ -19,52 +19,34 @@ export default [
path: '/playground',
key: 'playground',
icon: 'Comment',
component: './playground',
component: './playground'
},
{
name: 'Models',
path: '/models',
key: 'models',
icon: 'Block',
component: './models',
component: './models'
},
{
name: 'Nodes',
path: '/nodes',
key: 'nodes',
icon: 'CloudServer',
component: './nodes',
component: './nodes'
},
{
name: 'Users',
path: '/users',
key: 'users',
icon: 'Team',
component: './users',
component: './users'
},
// {
// name: '首页',
// path: '/home',
// key: 'homes',
// component: './Home',
// },
// {
// name: '权限演示',
// path: '/access',
// key: 'access',
// component: './Access',
// },
// {
// name: ' CRUD 示例',
// path: '/table',
// key: 'table',
// component: './Table',
// },
{
name: '404',
path: '*',
key: '404',
layout: false,
component: './404',
},
component: './404'
}
];
+3 -1
View File
@@ -12,13 +12,15 @@
},
"dependencies": {
"@ant-design/icons": "^5.3.7",
"@ant-design/plots": "^2.2.2",
"@ant-design/pro-components": "^2.7.1",
"@types/lodash": "^4.17.4",
"@umijs/max": "^4.2.1",
"antd": "^5.17.0",
"antd-style": "^3.6.2",
"classnames": "^2.5.1",
"lodash": "^4.17.21"
"lodash": "^4.17.21",
"numeral": "^2.0.6"
},
"devDependencies": {
"@types/react": "^18.3.1",
+24
View File
@@ -0,0 +1,24 @@
import { IApi } from '@umijs/max';
export default (api: IApi) => {
api.modifyHTML(($) => {
return $;
});
api.onStart(() => {
console.log('pllugins=========start');
});
// api.modifyConfig((memo: any) => {
// // some beautiful code
// console.log('pllugins=========memo', memo);
// return memo;
// });
// api.addLayouts(() => {
// return [
// {
// id: 'layout',
// file: require.resolve('./src/global-layouts/index.tsx')
// }
// ];
// });
};
+687 -1
View File
File diff suppressed because it is too large Load Diff
+1 -2
View File
@@ -1,11 +1,10 @@
export default (initialState: API.UserInfo) => {
console.log('initialState==========', initialState)
// 在这里按照初始化数据定义项目中的权限,统一管理
// 参考文档 https://umijs.org/docs/max/access
const canSeeAdmin = !!(
initialState && initialState.name !== 'dontHaveAccess'
);
return {
canSeeAdmin,
canSeeAdmin
};
};
+20
View File
@@ -0,0 +1,20 @@
.divider-line {
height: 8px;
// border-radius: 4px;
width: 100%;
// background-color: var(--color-fill-1);
z-index: 100;
margin: 0;
position: relative;
&::after {
content: '';
position: absolute;
top: 0;
left: -9px;
bottom: 0;
right: 0;
height: 100%;
background: var(--color-fill-1);
// border-radius: 4px;
}
}
+6
View File
@@ -0,0 +1,6 @@
import styles from './index.less';
const DividerLine: React.FC = () => {
return <div className={styles['divider-line']}></div>;
};
export default DividerLine;
+10
View File
@@ -3,4 +3,14 @@
justify-content: space-between;
align-items: center;
margin-top: 70px;
.left {
display: flex;
align-items: center;
font-size: 14px;
}
.right {
display: flex;
align-items: center;
font-size: 14px;
}
}
+5 -9
View File
@@ -1,5 +1,5 @@
import { useMemo } from 'react';
import styles from './index.less';
import './index.less';
type PageToolsProps = {
left?: React.ReactNode;
@@ -9,21 +9,17 @@ type PageToolsProps = {
};
const PageTools: React.FC<PageToolsProps> = (props) => {
const { left, right, marginBottom, marginTop } = props;
const { left, right, marginBottom = 0, marginTop = 70 } = props;
const newStyle: Record<string, string> = useMemo(() => {
const style: Record<string, string> = {};
if (marginBottom) {
style.marginBottom = `${marginBottom}px`;
}
if (marginTop) {
style.marginTop = `${marginTop}px`;
}
style.marginBottom = `${marginBottom}px`;
style.marginTop = `${marginTop}px`;
return style;
}, [marginBottom, marginTop]);
return (
<div className={styles['page-tools']} style={newStyle}>
<div className="page-tools" style={newStyle}>
<div className="left">{left}</div>
<div className="right">{right}</div>
</div>
+7 -1
View File
@@ -207,8 +207,14 @@ body {
.ant-pro-layout {
.ant-pro-sider {
.ant-menu {
.ant-menu-item:not(.ant-menu-item-selected) {
color: var(--ant-color-text);
}
.ant-menu-item:hover {
color: inherit;
color: var(--ant-color-text);
}
.ant-menu-item.ant-menu-item-selected:hover {
color: var(--ant-color-primary);
}
}
}
+2 -1
View File
@@ -91,6 +91,7 @@ export default (props: any) => {
};
const formatMessage = undefined;
const runtimeConfig = pluginManager.applyPlugins({
key: 'layout',
type: 'modify',
@@ -101,7 +102,7 @@ export default (props: any) => {
}
});
console.log(
'clientRoute===========',
'clientRoute==========2=',
clientRoutes,
runtimeConfig,
initialInfo
+2
View File
@@ -71,6 +71,8 @@ export function getRightRenderContent(opts: {
]
};
// antd@5 和 4.24 之后推荐使用 menu,性能更好
console.log('version+++++++++=', opts.runtimeConfig, version);
let dropdownProps;
if (version.startsWith('5.') || version.startsWith('4.24.')) {
dropdownProps = { menu: langMenu };
@@ -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;
+8
View File
@@ -10,6 +10,7 @@ import {
WechatWorkOutlined
} from '@ant-design/icons';
import { PageContainer } from '@ant-design/pro-components';
import { useNavigate } from '@umijs/max';
import {
Button,
Input,
@@ -43,6 +44,7 @@ const dataSource = [
];
const Models: React.FC = () => {
const navigate = useNavigate();
const rowSelection = useTableRowSelection();
const { sortOrder, setSortOrder } = useTableSort({
defaultSortOrder: 'descend'
@@ -117,6 +119,11 @@ const Models: React.FC = () => {
}
});
};
const handleOpenPlayGround = (row: any) => {
console.log('handleOpenPlayGround', row);
navigate('/playground');
};
return (
<>
<PageContainer
@@ -214,6 +221,7 @@ const Models: React.FC = () => {
<Button
size="small"
type="primary"
onClick={() => handleOpenPlayGround(record)}
icon={<WechatWorkOutlined />}
></Button>
</Tooltip>