chore: add resource worker
This commit is contained in:
@@ -27,6 +27,7 @@
|
||||
"dayjs": "^1.11.11",
|
||||
"echarts": "^5.5.1",
|
||||
"has-ansi": "^5.0.1",
|
||||
"highlight.js": "^11.10.0",
|
||||
"jotai": "^2.8.4",
|
||||
"localforage": "^1.10.0",
|
||||
"lodash": "^4.17.21",
|
||||
|
||||
Generated
+36
-4
@@ -53,6 +53,9 @@ dependencies:
|
||||
has-ansi:
|
||||
specifier: ^5.0.1
|
||||
version: 5.0.1
|
||||
highlight.js:
|
||||
specifier: ^11.10.0
|
||||
version: 11.10.0
|
||||
jotai:
|
||||
specifier: ^2.8.4
|
||||
version: 2.8.4(@types/react@18.3.1)(react@18.2.0)
|
||||
@@ -4438,7 +4441,7 @@ packages:
|
||||
dependencies:
|
||||
'@babel/core': 7.24.5
|
||||
postcss: 7.0.39
|
||||
postcss-syntax: 0.36.2(postcss@8.4.38)
|
||||
postcss-syntax: 0.36.2(postcss@7.0.39)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: false
|
||||
@@ -4479,7 +4482,7 @@ packages:
|
||||
postcss-syntax: '>=0.36.2'
|
||||
dependencies:
|
||||
postcss: 7.0.39
|
||||
postcss-syntax: 0.36.2(postcss@8.4.38)
|
||||
postcss-syntax: 0.36.2(postcss@7.0.39)
|
||||
remark: 13.0.0
|
||||
unist-util-find-all-after: 3.0.2
|
||||
transitivePeerDependencies:
|
||||
@@ -10450,6 +10453,11 @@ packages:
|
||||
hasBin: true
|
||||
dev: false
|
||||
|
||||
/highlight.js@11.10.0:
|
||||
resolution: {integrity: sha512-SYVnVFswQER+zu1laSya563s+F8VDGt7o35d4utbamowvUNLLMovFqwCLSocpZTz3MgaSRA1IbqRWZv97dtErQ==, tarball: https://registry.npmjs.org/highlight.js/-/highlight.js-11.10.0.tgz}
|
||||
engines: {node: '>=12.0.0'}
|
||||
dev: false
|
||||
|
||||
/history@4.10.1:
|
||||
resolution: {integrity: sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==, tarball: https://registry.npmjs.org/history/-/history-4.10.1.tgz}
|
||||
dependencies:
|
||||
@@ -13093,7 +13101,7 @@ packages:
|
||||
dependencies:
|
||||
htmlparser2: 3.10.1
|
||||
postcss: 7.0.39
|
||||
postcss-syntax: 0.36.2(postcss@8.4.38)
|
||||
postcss-syntax: 0.36.2(postcss@7.0.39)
|
||||
dev: false
|
||||
|
||||
/postcss-image-set-function@4.0.7(postcss@8.4.38):
|
||||
@@ -13567,6 +13575,30 @@ packages:
|
||||
lodash: 4.17.21
|
||||
postcss: 8.4.38
|
||||
|
||||
/postcss-syntax@0.36.2(postcss@7.0.39):
|
||||
resolution: {integrity: sha512-nBRg/i7E3SOHWxF3PpF5WnJM/jQ1YpY9000OaVXlAQj6Zp/kIqJxEDWIZ67tAd7NLuk7zqN4yqe9nc0oNAOs1w==}
|
||||
peerDependencies:
|
||||
postcss: '>=5.0.0'
|
||||
postcss-html: '*'
|
||||
postcss-jsx: '*'
|
||||
postcss-less: '*'
|
||||
postcss-markdown: '*'
|
||||
postcss-scss: '*'
|
||||
peerDependenciesMeta:
|
||||
postcss-html:
|
||||
optional: true
|
||||
postcss-jsx:
|
||||
optional: true
|
||||
postcss-less:
|
||||
optional: true
|
||||
postcss-markdown:
|
||||
optional: true
|
||||
postcss-scss:
|
||||
optional: true
|
||||
dependencies:
|
||||
postcss: 7.0.39
|
||||
dev: false
|
||||
|
||||
/postcss-syntax@0.36.2(postcss@8.4.38):
|
||||
resolution: {integrity: sha512-nBRg/i7E3SOHWxF3PpF5WnJM/jQ1YpY9000OaVXlAQj6Zp/kIqJxEDWIZ67tAd7NLuk7zqN4yqe9nc0oNAOs1w==}
|
||||
peerDependencies:
|
||||
@@ -16724,7 +16756,7 @@ packages:
|
||||
postcss-sass: 0.4.4
|
||||
postcss-scss: 2.1.1
|
||||
postcss-selector-parser: 6.0.16
|
||||
postcss-syntax: 0.36.2(postcss@8.4.38)
|
||||
postcss-syntax: 0.36.2(postcss@7.0.39)
|
||||
postcss-value-parser: 4.2.0
|
||||
resolve-from: 5.0.0
|
||||
slash: 3.0.0
|
||||
|
||||
@@ -56,6 +56,7 @@ const CopyButton: React.FC<CopyButtonProps> = ({
|
||||
|
||||
return (
|
||||
<Button
|
||||
className="copy-button"
|
||||
ref={buttonRef}
|
||||
type={type}
|
||||
shape={shape}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import hljs from 'highlight.js';
|
||||
import 'highlight.js/styles/atom-one-dark.css';
|
||||
import CopyButton from '../copy-button';
|
||||
import { escapeHtml } from './utils';
|
||||
|
||||
interface CodeViewerProps {
|
||||
code: string;
|
||||
lang: string;
|
||||
autodetect?: boolean;
|
||||
ignoreIllegals?: boolean;
|
||||
copyable?: boolean;
|
||||
}
|
||||
const CodeViewer: React.FC<CodeViewerProps> = (props) => {
|
||||
const {
|
||||
code,
|
||||
lang,
|
||||
autodetect = true,
|
||||
ignoreIllegals = true,
|
||||
copyable = true
|
||||
} = props || {};
|
||||
|
||||
const renderCode = () => {
|
||||
const autodetectLang = autodetect && !lang;
|
||||
const cannotDetectLanguage = !autodetectLang && !hljs.getLanguage(lang);
|
||||
let className = '';
|
||||
|
||||
if (!cannotDetectLanguage) {
|
||||
className = `hljs ${lang}`;
|
||||
}
|
||||
|
||||
// No idea what language to use, return raw code
|
||||
if (cannotDetectLanguage) {
|
||||
console.warn(`The language "${lang}" you specified could not be found.`);
|
||||
return {
|
||||
value: escapeHtml(code),
|
||||
className: className
|
||||
};
|
||||
}
|
||||
|
||||
if (autodetectLang) {
|
||||
const result = hljs.highlightAuto(code);
|
||||
return {
|
||||
value: result.value,
|
||||
className: className
|
||||
};
|
||||
}
|
||||
const result = hljs.highlight(code, {
|
||||
language: lang,
|
||||
ignoreIllegals: ignoreIllegals
|
||||
});
|
||||
return {
|
||||
value: result.value,
|
||||
className: className
|
||||
};
|
||||
};
|
||||
|
||||
const highlightedCode = renderCode();
|
||||
|
||||
return (
|
||||
<pre className="code-pre">
|
||||
<code
|
||||
className={highlightedCode.className}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: highlightedCode.value
|
||||
}}
|
||||
></code>
|
||||
<CopyButton
|
||||
text={highlightedCode.value}
|
||||
size="small"
|
||||
style={{ color: '#abb2bf' }}
|
||||
></CopyButton>
|
||||
</pre>
|
||||
);
|
||||
};
|
||||
|
||||
export default CodeViewer;
|
||||
@@ -0,0 +1,17 @@
|
||||
import CodeViewer from './code-viewer';
|
||||
import './style.less';
|
||||
|
||||
const HighlightCode: React.FC<{
|
||||
code: string;
|
||||
lang?: string;
|
||||
}> = (props) => {
|
||||
const { code, lang = 'bash' } = props;
|
||||
|
||||
return (
|
||||
<div className="high-light-wrapper">
|
||||
<CodeViewer lang={lang} code={code} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default HighlightCode;
|
||||
@@ -0,0 +1,22 @@
|
||||
.high-light-wrapper {
|
||||
text-align: left;
|
||||
|
||||
.hljs {
|
||||
font-weight: var(--font-weight-normal);
|
||||
padding-inline: 0;
|
||||
padding-block: 1.2em;
|
||||
}
|
||||
|
||||
.code-pre {
|
||||
padding-inline: 12px 32px;
|
||||
position: relative;
|
||||
background-color: #282c34;
|
||||
border-radius: var(--border-radius-mini);
|
||||
|
||||
.copy-button {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
right: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export function escapeHtml(value: string): string {
|
||||
return value
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
@@ -114,6 +114,7 @@ html {
|
||||
--ant-modal-header-margin-bottom: 20px;
|
||||
--ant-modal-footer-margin-top: 30px;
|
||||
--ant-modal-content-padding: 20px 24px 24px;
|
||||
--ant-modal-title-color: var(--ant-color-text);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ export function useUpdateChunkedList(options: {
|
||||
clearTimeout(timerRef.current);
|
||||
timerRef.current = setTimeout(() => {
|
||||
options.setDataList?.([...cacheDataListRef.current]);
|
||||
}, 100);
|
||||
}, 80);
|
||||
};
|
||||
const updateChunkedList = (
|
||||
data: ChunkedCollection,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export default {
|
||||
'resources.title': 'Resources',
|
||||
'resources.nodes': 'Nodes',
|
||||
'resources.button.create': 'Add Worker',
|
||||
'resources.table.hostname': 'Hostname',
|
||||
'resources.table.ip': 'IP',
|
||||
'resources.table.cpu': 'CPU',
|
||||
@@ -18,5 +19,9 @@ export default {
|
||||
'resources.table.total': 'Total',
|
||||
'resources.table.used': 'Used',
|
||||
'resources.table.wokers': 'workers',
|
||||
'resources.table.unified': 'Unified Memory'
|
||||
'resources.table.unified': 'Unified Memory',
|
||||
'resources.worker.add.step1': 'Get Token',
|
||||
'resources.worker.add.step2': 'Register Worker',
|
||||
'resources.worker.add.step3':
|
||||
'Refresh worker list, you can see the newly added worker'
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export default {
|
||||
'resources.title': '资源',
|
||||
'resources.button.create': '添加 Worker',
|
||||
'resources.nodes': '节点',
|
||||
'resources.table.hostname': '主机名',
|
||||
'resources.table.ip': 'IP',
|
||||
@@ -18,5 +19,8 @@ export default {
|
||||
'resources.table.total': '总量',
|
||||
'resources.table.used': '已用',
|
||||
'resources.table.wokers': 'workers',
|
||||
'resources.table.unified': '统一内存'
|
||||
'resources.table.unified': '统一内存',
|
||||
'resources.worker.add.step1': '获取 Token',
|
||||
'resources.worker.add.step2': '注册 Worker',
|
||||
'resources.worker.add.step3': '刷新 worker 列表,可以看到新添加的 worker'
|
||||
};
|
||||
|
||||
@@ -194,12 +194,20 @@ const LoginForm = () => {
|
||||
label={intl.formatMessage({ id: 'common.form.password' })}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="autoLogin" valuePropName="checked">
|
||||
<Checkbox style={{ marginLeft: 10 }}>
|
||||
{intl.formatMessage({ id: 'common.login.rember' })}
|
||||
<Form.Item noStyle name="autoLogin" valuePropName="checked">
|
||||
<Checkbox style={{ marginLeft: 5, marginBottom: 12 }}>
|
||||
<span style={{ color: 'var(--ant-color-text-secondary)' }}>
|
||||
{' '}
|
||||
{intl.formatMessage({ id: 'common.login.rember' })}
|
||||
</span>
|
||||
</Checkbox>
|
||||
</Form.Item>
|
||||
<Button htmlType="submit" type="primary" block>
|
||||
<Button
|
||||
htmlType="submit"
|
||||
type="primary"
|
||||
block
|
||||
style={{ height: '54px', fontSize: '16px' }}
|
||||
>
|
||||
{intl.formatMessage({ id: 'common.button.login' })}
|
||||
</Button>
|
||||
</Form>
|
||||
|
||||
@@ -65,26 +65,6 @@ const PasswordForm: React.FC = () => {
|
||||
{intl.formatMessage({ id: 'users.password.modify.description' })}
|
||||
</span>
|
||||
</h2>
|
||||
{/*
|
||||
<Form.Item
|
||||
name="current_password"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: intl.formatMessage(
|
||||
{ id: 'common.form.rule.input' },
|
||||
{
|
||||
name: intl.formatMessage({ id: 'users.form.currentpassword' })
|
||||
}
|
||||
)
|
||||
}
|
||||
]}
|
||||
>
|
||||
<SealInput.Password
|
||||
prefix={<LockOutlined />}
|
||||
label={intl.formatMessage({ id: 'users.form.currentpassword' })}
|
||||
/>
|
||||
</Form.Item> */}
|
||||
<Form.Item
|
||||
name="new_password"
|
||||
rules={[
|
||||
@@ -136,7 +116,7 @@ const PasswordForm: React.FC = () => {
|
||||
htmlType="submit"
|
||||
type="primary"
|
||||
block
|
||||
style={{ marginTop: 20 }}
|
||||
style={{ height: '54px', fontSize: '16px', marginTop: 10 }}
|
||||
>
|
||||
{intl.formatMessage({ id: 'common.button.submit' })}
|
||||
</Button>
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import HighlightCode from '@/components/highlight-code';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Modal } from 'antd';
|
||||
import React from 'react';
|
||||
import { addWorkerGuide } from '../config';
|
||||
|
||||
type ViewModalProps = {
|
||||
open: boolean;
|
||||
onCancel: () => void;
|
||||
};
|
||||
|
||||
const AddWorker: React.FC<ViewModalProps> = (props) => {
|
||||
const { open, onCancel } = props || {};
|
||||
const intl = useIntl();
|
||||
|
||||
const origin = window.location.origin;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={intl.formatMessage({ id: 'resources.button.create' })}
|
||||
open={open}
|
||||
centered={true}
|
||||
onCancel={onCancel}
|
||||
destroyOnClose={true}
|
||||
closeIcon={true}
|
||||
maskClosable={false}
|
||||
keyboard={false}
|
||||
width={600}
|
||||
footer={null}
|
||||
>
|
||||
<div>
|
||||
<h3>1. {intl.formatMessage({ id: 'resources.worker.add.step1' })}</h3>
|
||||
<h4>Linux Or MacOS </h4>
|
||||
<HighlightCode code={addWorkerGuide.mac.getToken}></HighlightCode>
|
||||
<h4>Windows </h4>
|
||||
<HighlightCode code={addWorkerGuide.win.getToken}></HighlightCode>
|
||||
<h3>2. {intl.formatMessage({ id: 'resources.worker.add.step2' })}</h3>
|
||||
<h4>Linux Or MacOS </h4>
|
||||
<HighlightCode
|
||||
code={addWorkerGuide.mac.registerWorker(origin)}
|
||||
></HighlightCode>
|
||||
<h4>Windows </h4>
|
||||
<HighlightCode
|
||||
code={addWorkerGuide.win.registerWorker(origin)}
|
||||
></HighlightCode>
|
||||
<h3>3. {intl.formatMessage({ id: 'resources.worker.add.step3' })}</h3>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(AddWorker);
|
||||
@@ -8,6 +8,7 @@ import { convertFileSize, handleBatchRequest } from '@/utils';
|
||||
import {
|
||||
DeleteOutlined,
|
||||
InfoCircleOutlined,
|
||||
PlusOutlined,
|
||||
SyncOutlined
|
||||
} from '@ant-design/icons';
|
||||
import { useIntl } from '@umijs/max';
|
||||
@@ -17,6 +18,7 @@ import { memo, useEffect, useRef, useState } from 'react';
|
||||
import { deleteWorker, queryWorkersList } from '../apis';
|
||||
import { WorkerStatusMapValue, status } from '../config';
|
||||
import { Filesystem, GPUDeviceItem, ListItem } from '../config/types';
|
||||
import AddWorker from './add-worker';
|
||||
const { Column } = Table;
|
||||
|
||||
const Resources: React.FC = () => {
|
||||
@@ -30,6 +32,7 @@ const Resources: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
const [total, setTotal] = useState(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [dataSource, setDataSource] = useState<ListItem[]>([]);
|
||||
const [queryParams, setQueryParams] = useState({
|
||||
page: 1,
|
||||
@@ -94,6 +97,10 @@ const Resources: React.FC = () => {
|
||||
return mountRoot ? formateUtilazation(mountRoot.used, mountRoot.total) : 0;
|
||||
};
|
||||
|
||||
const handleAddWorker = () => {
|
||||
setOpen(true);
|
||||
};
|
||||
|
||||
const handleDelete = (row: ListItem) => {
|
||||
modalRef.current.show({
|
||||
content: 'worker',
|
||||
@@ -166,14 +173,23 @@ const Resources: React.FC = () => {
|
||||
</Space>
|
||||
}
|
||||
right={
|
||||
<Button
|
||||
icon={<DeleteOutlined />}
|
||||
danger
|
||||
onClick={handleDeleteBatch}
|
||||
disabled={!rowSelection.selectedRowKeys.length}
|
||||
>
|
||||
{intl.formatMessage({ id: 'common.button.delete' })}
|
||||
</Button>
|
||||
<Space size={20}>
|
||||
<Button
|
||||
icon={<PlusOutlined></PlusOutlined>}
|
||||
type="primary"
|
||||
onClick={handleAddWorker}
|
||||
>
|
||||
{intl.formatMessage({ id: 'resources.button.create' })}
|
||||
</Button>
|
||||
<Button
|
||||
icon={<DeleteOutlined />}
|
||||
danger
|
||||
onClick={handleDeleteBatch}
|
||||
disabled={!rowSelection.selectedRowKeys.length}
|
||||
>
|
||||
{intl.formatMessage({ id: 'common.button.delete' })}
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
></PageTools>
|
||||
<Table
|
||||
@@ -371,6 +387,7 @@ const Resources: React.FC = () => {
|
||||
/>
|
||||
</Table>
|
||||
<DeleteModal ref={modalRef}></DeleteModal>
|
||||
<AddWorker open={open} onCancel={() => setOpen(false)}></AddWorker>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -14,3 +14,19 @@ export const status: any = {
|
||||
[WorkerStatusMap.ready]: StatusMaps.success,
|
||||
[WorkerStatusMap.not_ready]: StatusMaps.error
|
||||
};
|
||||
|
||||
export const addWorkerGuide = {
|
||||
mac: {
|
||||
getToken: 'cat /var/lib/gpustack/token',
|
||||
registerWorker(server: string) {
|
||||
return `curl -sfL https://get.gpustack.ai | sh -s - --server-url ${server} --token mytoken`;
|
||||
}
|
||||
},
|
||||
win: {
|
||||
getToken:
|
||||
'Get-Content -Path (Join-Path -Path $env:APPDATA -ChildPath "gpustack\\token") -Raw',
|
||||
registerWorker(server: string) {
|
||||
return `Invoke-Expression "& { $((Invoke-WebRequest -Uri "https://get.gpustack.ai" -UseBasicParsing).Content) } -ServerURL ${server} -Token mytoken"`;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user