feat: nodes list

main
jialin 2 years ago
parent 313da7a9d2
commit 96e83878d1

@ -13,10 +13,12 @@
"dependencies": {
"@ant-design/icons": "^5.3.7",
"@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"
"classnames": "^2.5.1",
"lodash": "^4.17.21"
},
"devDependencies": {
"@types/react": "^18.3.1",

@ -11,6 +11,9 @@ dependencies:
'@ant-design/pro-components':
specifier: ^2.7.1
version: 2.7.1(antd@5.17.0)(rc-field-form@1.44.0)(react-dom@18.3.1)(react@18.3.1)
'@types/lodash':
specifier: ^4.17.4
version: 4.17.4
'@umijs/max':
specifier: ^4.2.1
version: 4.2.1(@babel/core@7.24.5)(@types/react-dom@18.3.0)(@types/react@18.3.1)(dva@2.5.0-beta.2)(prettier@3.2.5)(rc-field-form@1.44.0)(react-dom@18.3.1)(react@18.3.1)(typescript@5.4.5)(webpack@5.91.0)
@ -23,6 +26,9 @@ dependencies:
classnames:
specifier: ^2.5.1
version: 2.5.1
lodash:
specifier: ^4.17.21
version: 4.17.21
devDependencies:
'@types/react':
@ -2611,6 +2617,10 @@ packages:
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
dev: false
/@types/lodash@4.17.4:
resolution: {integrity: sha512-wYCP26ZLxaT3R39kiN2+HcJ4kTd3U1waI/cY7ivWYqFP6pW3ZNpvi6Wd6PHZx7T/t8z0vlkXMg3QYLa7DZ/IJQ==}
dev: false
/@types/minimist@1.2.5:
resolution: {integrity: sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==}
dev: false

@ -5,7 +5,7 @@
// 全局初始化数据配置,用于 Layout 用户信息和权限初始化
// 更多信息见文档https://umijs.org/docs/api/runtime-config#getinitialstate
export async function getInitialState(): Promise<{ name: string }> {
return { name: 'admin' };
return { name: 'admin', logout: true };
}
// export const layout: RunTimeLayoutConfig = () => {

@ -0,0 +1,9 @@
.status-tag {
display: flex;
justify-content: center;
align-items: center;
padding: 2px 10px;
border-radius: 20px;
width: fit-content;
font-size: var(--font-size-base);
}

@ -0,0 +1,44 @@
import { StatusColorMap } from '@/config';
import { StatusType } from '@/config/types';
import { useEffect, useState } from 'react';
import styles from './index.less';
export const StatusMaps = {
running: 'blue',
error: 'red',
warning: 'orange',
success: 'success'
};
type StatusTagProps = {
statusValue: {
status: StatusType;
text: string;
};
};
const StatusTag: React.FC<StatusTagProps> = ({ statusValue }) => {
const { text, status } = statusValue;
const [statusColor, setStatusColor] = useState<{ text: string; bg: string }>({
text: '',
bg: ''
});
useEffect(() => {
setStatusColor(StatusColorMap[status]);
}, [status]);
return (
<span
className={styles['status-tag']}
style={{
color: statusColor.text,
border: `1px solid ${statusColor.text}`
}}
>
{text}
</span>
);
};
export default StatusTag;

@ -1,7 +1,31 @@
import { PageActionType } from './types';
import { PageActionType, StatusType } from './types';
export const PageAction: Record<string, PageActionType> = {
CREATE: 'create',
UPDATE: 'update',
VIEW: 'view'
};
export const StatusColorMap: Record<StatusType, { text: string; bg: string }> =
{
error: {
text: `var(--ant-red-6)`,
bg: `var(--ant-red-1)`
},
warning: {
text: `var(--ant-orange-6)`,
bg: `var(--ant-orange-1)`
},
transitioning: {
text: `var(--ant-blue-6)`,
bg: `var(--ant-blue-1)`
},
success: {
text: `var(--ant-color-primary)`,
bg: `var(--ant-green-1)`
},
inactive: {
text: `var(--ant-color-border)`,
bg: `var(--ant-color-fill)`
}
};

@ -7,3 +7,10 @@ export interface DropDownItem {
}
export type PageActionType = 'create' | 'update' | 'view';
export type StatusType =
| 'error'
| 'warning'
| 'transitioning'
| 'success'
| 'inactive';

@ -9,7 +9,7 @@ html {
--color-white-1: #fff;
--font-weight-normal: 500;
--font-weight-bold: 700;
--color-text-1: #1d2129;
--color-text-1: var(--ant-color-text);
--color-bg-light-1: #f0fff6;
--font-size-base: 12px;
--ant-color-fill-secondary: rgba(0, 0, 0, 0.06);

@ -12,19 +12,19 @@ import {
useAppData,
useLocation,
useNavigate,
type IRoute,
type IRoute
} from '@umijs/max';
import { useMemo } from 'react';
import Exception from './Exception';
import './Layout.css';
import Logo from './Logo';
import { getRightRenderContent } from './rightRender';
import { renderMenuIcon,patchRoutes } from './runtime';
import { patchRoutes } from './runtime';
// 过滤出需要显示的路由, 这里的filterFn 指 不希望显示的层级
const filterRoutes = (
routes: IRoute[],
filterFn: (route: IRoute) => boolean,
filterFn: (route: IRoute) => boolean
) => {
if (routes.length === 0) {
return [];
@ -78,34 +78,33 @@ export default (props: any) => {
const navigate = useNavigate();
const { clientRoutes, pluginManager } = useAppData();
console.log('pluginManager===========', pluginManager);
const initialInfo = (useModel && useModel('@@initialState')) || {
initialState: undefined,
loading: false,
setInitialState: null,
setInitialState: null
};
const { initialState, loading, setInitialState } = initialInfo;
const userConfig = {
title: '',
layout: 'mix',
layout: 'mix'
};
const formatMessage = undefined;
const runtimeConfig = pluginManager.applyPlugins({
key: 'layout',
type: 'modify',
logout: true,
initialValue: {
...initialInfo,
notFound: <div>not found</div>
},
}
});
console.log(
'clientRoute===========',
clientRoutes,
runtimeConfig,
initialInfo,
initialInfo
);
// 现在的 layout 及 wrapper 实现是通过父路由的形式实现的, 会导致路由数据多了冗余层级, proLayout 消费时, 无法正确展示菜单, 这里对冗余数据进行过滤操作
@ -116,18 +115,17 @@ export default (props: any) => {
(!!route.isLayout && route.id !== '@@/global-layout') ||
!!route.isWrapper
);
},
}
);
console.log('clientRoutes===========', clientRoutes, newRoutes)
console.log('clientRoutes===========', clientRoutes, newRoutes);
const [route] = useAccessMarkedRoutes(mapRoutes(newRoutes));
patchRoutes({ routes: route?.children || [] });
const matchedRoute = useMemo(
() => matchRoutes(route?.children || [], location.pathname)?.pop?.()?.route,
[location.pathname],
[location.pathname]
);
console.log('route===========', route)
console.log('route===========', route);
return (
<ProLayout
route={route}
@ -147,7 +145,7 @@ console.log('route===========', route)
menu={{ locale: userConfig.locale }}
logo={Logo}
menuItemRender={(menuItemProps, defaultDom) => {
console.log('meurender=========',{ defaultDom})
console.log('meurender=========', { defaultDom });
if (menuItemProps.isUrl || menuItemProps.children) {
return defaultDom;
}
@ -158,15 +156,11 @@ console.log('route===========', route)
to={menuItemProps.path.replace('/*', '')}
target={menuItemProps.target}
>
{defaultDom}
{defaultDom}
</Link>
);
}
return (
<>
{defaultDom}
</>
);
return <>{defaultDom}</>;
}}
itemRender={(route, _, routes) => {
const { breadcrumbName, title, path } = route;
@ -190,7 +184,7 @@ console.log('route===========', route)
runtimeConfig,
loading,
initialState,
setInitialState,
setInitialState
});
if (runtimeConfig.rightContentRender) {
return runtimeConfig.rightContentRender(layoutProps, dom, {
@ -199,7 +193,7 @@ console.log('route===========', route)
runtimeConfig,
loading,
initialState,
setInitialState,
setInitialState
});
}
return dom;

@ -1,6 +1,7 @@
// @ts-nocheck
// This file is generated by Umi automatically
// DO NOT CHANGE IT MANUALLY!
import avatarImg from '@/assets/images/avatar.png';
import { LogoutOutlined } from '@ant-design/icons';
import { Avatar, Dropdown, Menu, Spin, version } from 'antd';
@ -14,7 +15,7 @@ export function getRightRenderContent(opts: {
return opts.runtimeConfig.rightRender(
opts.initialState,
opts.setInitialState,
opts.runtimeConfig,
opts.runtimeConfig
);
}
@ -32,10 +33,7 @@ export function getRightRenderContent(opts: {
<Avatar
size="small"
className="umi-plugin-layout-avatar"
src={
opts.initialState?.avatar ||
'https://gw.alipayobjects.com/zos/antfincdn/XAosXuNZyF/BiazfanxmamNRoxxVxka.png'
}
src={opts.initialState?.avatar || avatarImg}
alt="avatar"
/>
) : null}
@ -68,9 +66,9 @@ export function getRightRenderContent(opts: {
),
onClick: () => {
opts?.runtimeConfig?.logout?.(opts.initialState);
},
},
],
}
}
]
};
// antd@5 和 4.24 之后推荐使用 menu性能更好
let dropdownProps;
@ -86,7 +84,7 @@ export function getRightRenderContent(opts: {
</Menu.Item>
))}
</Menu>
),
)
};
} else {
// 需要 antd 4.20.0 以上版本

@ -0,0 +1,19 @@
export interface Capacity {
cpu: number;
gpu: number;
memory: string;
gram: string;
}
export interface NodeItem {
id: number;
name: string;
address: string;
hostname: string;
labels: object;
resources: {
capacity: Capacity;
allocable: Capacity;
};
state: string;
}

@ -1,16 +1,277 @@
import PageTools from '@/components/page-tools';
import StatusTag from '@/components/status-tag';
import useTableRowSelection from '@/hooks/use-table-row-selection';
import useTableSort from '@/hooks/use-table-sort';
import { SyncOutlined } from '@ant-design/icons';
import { PageContainer } from '@ant-design/pro-components';
import { Button, Input, Progress, Space, Table } from 'antd';
import _ from 'lodash';
import { memo, useMemo, useState } from 'react';
import { NodeItem } from './config/types';
const { Column } = Table;
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'
}
];
const Models: React.FC = () => {
const rowSelection = useTableRowSelection();
const { sortOrder, setSortOrder } = useTableSort({
defaultSortOrder: 'descend'
});
const [total, setTotal] = useState(100);
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) => {
setSortOrder(sorter.order);
};
const fetchData = async () => {
console.log('fetchData');
};
const handleSearch = (e: any) => {
fetchData();
};
const handleNameChange = (e: any) => {
setQueryParams({
...queryParams,
name: e.target.value
});
};
const handleClickMenu = (e: any) => {
console.log('click', e);
};
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}
/>
);
}
);
const Dashboard: React.FC = () => {
return (
<PageContainer
ghost
header={{
title: 'Dashboard',
}}
>
<div>Nodes</div>
</PageContainer>
<>
<PageContainer
ghost
header={{
title: 'Nodes'
}}
extra={[]}
>
<PageTools
marginBottom={22}
left={
<Space>
<Input
placeholder="按名称查询"
style={{ width: 300 }}
onChange={handleNameChange}
></Input>
<Button
type="text"
style={{ color: 'var(--ant-color-primary)' }}
onClick={handleSearch}
icon={<SyncOutlined></SyncOutlined>}
></Button>
</Space>
}
></PageTools>
<Table
dataSource={dataSource}
rowSelection={rowSelection}
loading={loading}
onChange={handleTableChange}
pagination={{
showSizeChanger: true,
pageSize: 10,
current: 2,
total: total,
hideOnSinglePage: true,
onShowSizeChange: handleShowSizeChange,
onChange: handlePageChange
}}
>
<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>
);
}}
/>
<Column
title="Operation"
key="operation"
render={(text, record) => {
return (
<Space>
<Button size="middle">Logs</Button>
</Space>
);
}}
/>
</Table>
</PageContainer>
</>
);
};
export default Dashboard;
export default Models;

1
typings.d.ts vendored

@ -14,5 +14,6 @@ declare module 'omit.js';
declare module 'numeral';
declare module 'mockjs';
declare module 'react-fittext';
declare module 'lodash';
declare const REACT_APP_ENV: 'test' | 'dev' | 'pre' | false;

Loading…
Cancel
Save