chore: create cluster ux

This commit is contained in:
jialin
2025-09-16 11:26:17 +08:00
parent 55b0a20a4a
commit 62dbd18ef8
26 changed files with 690 additions and 392 deletions
-10
View File
@@ -54,16 +54,6 @@ export default [
defaultIcon: 'icon-reranker', defaultIcon: 'icon-reranker',
component: './playground/rerank' component: './playground/rerank'
}, },
// {
// name: 'text2images',
// title: 'Text2Images',
// path: keepAliveRoutes.text2images,
// key: 'text2images',
// icon: 'icon-image1',
// selectedIcon: 'icon-image-filled',
// defaultIcon: 'icon-image1',
// component: './playground/images'
// },
{ {
name: 'speech', name: 'speech',
title: 'Speech', title: 'Speech',
+4
View File
@@ -324,3 +324,7 @@ textarea:hover {
.ant-result-extra { .ant-result-extra {
color: var(--ant-color-text-secondary); color: var(--ant-color-text-secondary);
} }
.text-success {
color: var(--ant-color-success);
}
+6 -4
View File
@@ -24,6 +24,8 @@ export default function useChartConfig() {
splitLineColor: token.colorBorder, splitLineColor: token.colorBorder,
tickLineColor: token.colorSplit, tickLineColor: token.colorSplit,
axislabelColor: token.colorTextTertiary, axislabelColor: token.colorTextTertiary,
colorSecondary: token.colorTextSecondary,
colorTertiary: token.colorTextTertiary,
gaugeBgColor: token.colorFillSecondary, gaugeBgColor: token.colorFillSecondary,
gaugeSplitLineColor: isDarkTheme gaugeSplitLineColor: isDarkTheme
? 'rgba(255,255,255,.3)' ? 'rgba(255,255,255,.3)'
@@ -172,8 +174,8 @@ export default function useChartConfig() {
} }
}, },
axisTick: { axisTick: {
distance: -11, distance: -10,
length: 6, length: 5,
splitNumber: 5, splitNumber: 5,
lineStyle: { lineStyle: {
width: 1.5, width: 1.5,
@@ -206,13 +208,13 @@ export default function useChartConfig() {
rich: { rich: {
value: { value: {
fontSize: 16, fontSize: 16,
fontWeight: '500', fontWeight: '600',
color: chartColorMap.titleColor color: chartColorMap.titleColor
}, },
unit: { unit: {
fontSize: 14, fontSize: 14,
color: chartColorMap.titleColor, color: chartColorMap.titleColor,
fontWeight: '500', fontWeight: '600',
padding: [0, 0, 0, 2] padding: [0, 0, 0, 2]
} }
} }
+12 -3
View File
@@ -1,7 +1,7 @@
import Chart from '@/components/echarts/chart'; import Chart from '@/components/echarts/chart';
import useChartConfig from '@/components/echarts/config'; import useChartConfig from '@/components/echarts/config';
import EmptyData from '@/components/empty-data'; import EmptyData from '@/components/empty-data';
import React, { memo } from 'react'; import React from 'react';
import { ChartProps } from './types'; import { ChartProps } from './types';
const strokeColorFunc = (percent: number) => { const strokeColorFunc = (percent: number) => {
@@ -34,10 +34,19 @@ const GaugeChart: React.FC<Omit<ChartProps, 'seriesData' | 'xAxisData'>> = (
...gaugeItemConfig, ...gaugeItemConfig,
...gaugeConfig ...gaugeConfig
}; };
combineGaugeConfig.detail.rich.value.color = colorValue;
combineGaugeConfig.detail.rich.unit.color = colorValue;
return { return {
title: { title: {
...titleConfig, ...titleConfig,
text: title, text: title,
textStyle: {
fontSize: 12,
color: chartColorMap.colorSecondary,
fontWeight: 400
},
top: '0', top: '0',
left: 'center' left: 'center'
}, },
@@ -58,7 +67,7 @@ const GaugeChart: React.FC<Omit<ChartProps, 'seriesData' | 'xAxisData'>> = (
color: 'transparent' color: 'transparent'
}, },
detail: { detail: {
...gaugeItemConfig.detail, ...combineGaugeConfig.detail,
formatter: labelFormatter || gaugeItemConfig.detail.formatter formatter: labelFormatter || gaugeItemConfig.detail.formatter
}, },
data: [{ value }] data: [{ value }]
@@ -78,4 +87,4 @@ const GaugeChart: React.FC<Omit<ChartProps, 'seriesData' | 'xAxisData'>> = (
); );
}; };
export default memo(GaugeChart); export default GaugeChart;
+37 -5
View File
@@ -1,10 +1,20 @@
import OverlayScroller from '@/components/overlay-scroller';
import useBodyScroll from '@/hooks/use-body-scroll'; import useBodyScroll from '@/hooks/use-body-scroll';
import useOverlayScroller from '@/hooks/use-overlay-scroller';
import { Modal, type ModalProps } from 'antd'; import { Modal, type ModalProps } from 'antd';
import React from 'react'; import React from 'react';
import styled from 'styled-components';
const Wrapper = styled.div<{ $maxHeight?: number }>`
max-height: ${({ $maxHeight }) =>
typeof $maxHeight === 'number' ? `${$maxHeight}px` : $maxHeight};
overflow-y: auto;
width: 100%;
`;
const ScrollerModal = (props: ModalProps & { maxContentHeight?: number }) => { const ScrollerModal = (props: ModalProps & { maxContentHeight?: number }) => {
const scroller = React.useRef<any>(null);
const { saveScrollHeight, restoreScrollHeight } = useBodyScroll(); const { saveScrollHeight, restoreScrollHeight } = useBodyScroll();
const { initialize, destroyInstance } = useOverlayScroller();
React.useEffect(() => { React.useEffect(() => {
if (props.open) { if (props.open) {
@@ -14,6 +24,24 @@ const ScrollerModal = (props: ModalProps & { maxContentHeight?: number }) => {
} }
}, [props.open]); }, [props.open]);
// init scroller, delay to ensure modal is fully open
React.useEffect(() => {
let timeout = null;
if (props.open) {
timeout = setTimeout(() => {
if (scroller.current) {
initialize(scroller.current);
}
}, 100);
}
return () => {
if (timeout) {
clearTimeout(timeout);
}
destroyInstance();
};
}, [props.open, initialize]);
return ( return (
<Modal <Modal
{...props} {...props}
@@ -34,12 +62,16 @@ const ScrollerModal = (props: ModalProps & { maxContentHeight?: number }) => {
} }
}} }}
> >
<OverlayScroller <Wrapper
style={{ paddingInline: 24, paddingBlockEnd: props.footer ? 0 : 32 }} ref={scroller}
maxHeight={props.maxContentHeight || 500} data-overlayscrollbars-initialize
className="overlay-scroller-wrapper"
$maxHeight={props.maxContentHeight || 500}
hidden={false}
style={{ paddingInline: 24, paddingBlockEnd: props.footer ? 0 : 24 }}
> >
{props.children} {props.children}
</OverlayScroller> </Wrapper>
</Modal> </Modal>
); );
}; };
+14 -2
View File
@@ -1,6 +1,18 @@
import useUserSettings from '@/hooks/use-user-settings'; import useUserSettings from '@/hooks/use-user-settings';
import { Tag, TagProps } from 'antd'; import { Tag, TagProps } from 'antd';
import React from 'react'; import React from 'react';
import styled from 'styled-components';
const TagWrapper = styled(Tag)`
display: flex;
align-items: center;
justify-content: center;
padding: 2px 6px;
border-radius: 4px;
font-size: 12px;
height: 22px;
opacity: 0.7;
`;
const ThemeTag: React.FC<TagProps & { opacity?: number }> = ({ const ThemeTag: React.FC<TagProps & { opacity?: number }> = ({
opacity, opacity,
@@ -11,7 +23,7 @@ const ThemeTag: React.FC<TagProps & { opacity?: number }> = ({
const { userSettings } = useUserSettings(); const { userSettings } = useUserSettings();
const { isDarkTheme } = userSettings; const { isDarkTheme } = userSettings;
return ( return (
<Tag <TagWrapper
{...restProps} {...restProps}
style={{ style={{
...style, ...style,
@@ -19,7 +31,7 @@ const ThemeTag: React.FC<TagProps & { opacity?: number }> = ({
}} }}
> >
{children} {children}
</Tag> </TagWrapper>
); );
}; };
+13 -2
View File
@@ -8,13 +8,14 @@ interface CardProps {
children?: React.ReactNode; children?: React.ReactNode;
clickable?: boolean; clickable?: boolean;
ghost?: boolean; ghost?: boolean;
header?: React.ReactNode;
footer?: React.ReactNode;
onClick?: () => void; onClick?: () => void;
} }
const CardWrapper = styled.div` const CardWrapper = styled.div`
overflow: hidden; overflow: hidden;
display: flex; display: flex;
padding: 16px 20px;
flex-direction: column; flex-direction: column;
justify-content: flex-start; justify-content: flex-start;
align-items: flex-start; align-items: flex-start;
@@ -39,6 +40,12 @@ const CardWrapper = styled.div`
} }
`; `;
const CardContent = styled.div`
padding: 16px 20px;
width: 100%;
flex: 1;
`;
const Card: React.FC<CardProps> = (props) => { const Card: React.FC<CardProps> = (props) => {
const { const {
className, className,
@@ -46,6 +53,8 @@ const Card: React.FC<CardProps> = (props) => {
children, children,
clickable = true, clickable = true,
ghost = false, ghost = false,
header,
footer,
onClick onClick
} = props; } = props;
@@ -58,7 +67,9 @@ const Card: React.FC<CardProps> = (props) => {
style={{ height: height || '180px' }} style={{ height: height || '180px' }}
onClick={onClick} onClick={onClick}
> >
{children} {header}
<CardContent>{children}</CardContent>
{footer}
</CardWrapper> </CardWrapper>
); );
}; };
+5 -1
View File
@@ -55,5 +55,9 @@ export default function useBodyScroll() {
}; };
}, []); }, []);
return { saveScrollHeight, restoreScrollHeight }; return {
saveScrollHeight,
restoreScrollHeight,
bodyScroller: bodyScroller.current
};
} }
+7
View File
@@ -199,6 +199,12 @@ export default function useOverlayScroller(data?: {
[initialize, instance] [initialize, instance]
); );
const destroyInstance = () => {
instanceRef.current?.destroy?.();
removeWheelEvent();
instanceRef.current = null;
};
useEffect(() => { useEffect(() => {
return () => { return () => {
instanceRef.current?.destroy?.(); instanceRef.current?.destroy?.();
@@ -212,6 +218,7 @@ export default function useOverlayScroller(data?: {
scrollEventElement: scrollEventElement.current, scrollEventElement: scrollEventElement.current,
initialized: initialized.current, initialized: initialized.current,
generateInstance, generateInstance,
destroyInstance: destroyInstance,
updateScrollerPosition: throttledUpdateScrollerPosition, updateScrollerPosition: throttledUpdateScrollerPosition,
updateScrollerPositionToTop: updateScrollerPositionToTop updateScrollerPositionToTop: updateScrollerPositionToTop
}; };
+1 -2
View File
@@ -44,8 +44,7 @@ export default {
'resources.worker.add.step1': 'resources.worker.add.step1':
'Get Token <span class="note-text">(Run on the server)</span>', 'Get Token <span class="note-text">(Run on the server)</span>',
'resources.worker.add.step2': 'Register Worker', 'resources.worker.add.step2': 'Register Worker',
'resources.worker.add.step2.tips': 'resources.worker.add.step2.tips': '(Run on the worker to be added.)',
'(Run on the worker to be added, <span class="bold-text">token</span> is the value obtained in the first step.)',
'resources.worker.add.step3': 'resources.worker.add.step3':
'After success, refresh the workers list to view the new worker.', 'After success, refresh the workers list to view the new worker.',
'resources.worker.container.supported': 'Do not support macOS or Windows.', 'resources.worker.container.supported': 'Do not support macOS or Windows.',
+1 -2
View File
@@ -44,8 +44,7 @@ export default {
'resources.worker.add.step1': 'resources.worker.add.step1':
'トークンを取得 <span class="note-text">(サーバーで実行)</span>', 'トークンを取得 <span class="note-text">(サーバーで実行)</span>',
'resources.worker.add.step2': 'ワーカーを登録', 'resources.worker.add.step2': 'ワーカーを登録',
'resources.worker.add.step2.tips': 'resources.worker.add.step2.tips': '(追加するワーカーで実行し。)',
'(追加するワーカーで実行し、<span class="bold-text">トークン</span> は最初のステップで取得した値です。)',
'resources.worker.add.step3': 'resources.worker.add.step3':
'成功後、ワーカーリストを更新して新しいワーカーを確認してください。', '成功後、ワーカーリストを更新して新しいワーカーを確認してください。',
'resources.worker.container.supported': 'resources.worker.container.supported':
+1 -2
View File
@@ -44,8 +44,7 @@ export default {
'resources.worker.add.step1': 'resources.worker.add.step1':
'Получить токен <span class="note-text">(Запустить на сервере)</span>', 'Получить токен <span class="note-text">(Запустить на сервере)</span>',
'resources.worker.add.step2': 'Зарегистрировать воркер', 'resources.worker.add.step2': 'Зарегистрировать воркер',
'resources.worker.add.step2.tips': 'resources.worker.add.step2.tips': '(Запустить на добавляемом воркере.)', // Translated
'(Запустить на добавляемом воркере, <span class="bold-text">token</span> — это значение, полученное на первом шаге.)', // Translated
'resources.worker.add.step3': 'resources.worker.add.step3':
'После успешной регистрации обновите список воркеров.', 'После успешной регистрации обновите список воркеров.',
'resources.worker.container.supported': 'Только для Linux.', 'resources.worker.container.supported': 'Только для Linux.',
+1 -2
View File
@@ -44,8 +44,7 @@ export default {
'resources.worker.add.step1': 'resources.worker.add.step1':
'获取 Token<span class="note-text">(在 Server 上运行)</span>', '获取 Token<span class="note-text">(在 Server 上运行)</span>',
'resources.worker.add.step2': '注册 Worker', 'resources.worker.add.step2': '注册 Worker',
'resources.worker.add.step2.tips': 'resources.worker.add.step2.tips': '(在需要添加的 Worker 上运行。)',
'(在需要添加的 Worker 上运行,<span class="bold-text">token</span> 为第一步获取到的值。)',
'resources.worker.add.step3': '成功后,刷新 Worker 列表即可看到新的 Worker', 'resources.worker.add.step3': '成功后,刷新 Worker 列表即可看到新的 Worker',
'resources.worker.container.supported': '不支持 macOS 和 Windows', 'resources.worker.container.supported': '不支持 macOS 和 Windows',
'resources.worker.current.version': '当前版本为 {version}', 'resources.worker.current.version': '当前版本为 {version}',
+15 -28
View File
@@ -25,7 +25,6 @@ import {
updateCredential updateCredential
} from './apis'; } from './apis';
import AddCluster from './components/add-cluster'; import AddCluster from './components/add-cluster';
import AddPool from './components/add-pool';
import ClusterItem from './components/cluster-item'; import ClusterItem from './components/cluster-item';
import { ClusterDataList } from './config'; import { ClusterDataList } from './config';
import { import {
@@ -146,7 +145,10 @@ const Credentials: React.FC = () => {
}); });
const intl = useIntl(); const intl = useIntl();
const [open, setOpen] = useState(false); const [openAddWorker, setOpenAddWorker] = useState({
open: false,
token: ''
});
const [openAddModal, setOpenAddModal] = useState(false); const [openAddModal, setOpenAddModal] = useState(false);
const [provider, setProvider] = useState<string>('custom'); const [provider, setProvider] = useState<string>('custom');
const [action, setAction] = useState<PageActionType>(PageAction.CREATE); const [action, setAction] = useState<PageActionType>(PageAction.CREATE);
@@ -222,7 +224,7 @@ const Credentials: React.FC = () => {
setOpenAddModal(false); setOpenAddModal(false);
}; };
const handleEditUser = (row: ListItem) => { const handleEditCluster = (row: ListItem) => {
setCurrentData(row); setCurrentData(row);
setOpenAddModal(true); setOpenAddModal(true);
setAction(PageAction.EDIT); setAction(PageAction.EDIT);
@@ -231,11 +233,14 @@ const Credentials: React.FC = () => {
const handleSelect = (val: any, row: ListItem) => { const handleSelect = (val: any, row: ListItem) => {
if (val === 'edit') { if (val === 'edit') {
handleEditUser(row); handleEditCluster(row);
} else if (val === 'delete') { } else if (val === 'delete') {
handleDelete({ ...row, name: row.name }); handleDelete({ ...row, name: row.name });
} else if (val === 'add_worker') { } else if (val === 'add_worker') {
setOpen(true); setOpenAddWorker({
open: true,
token: '${token}'
});
setCurrentData(row); setCurrentData(row);
} else if (val === 'addPool') { } else if (val === 'addPool') {
handleAddPool(row.provider); handleAddPool(row.provider);
@@ -308,29 +313,11 @@ const Credentials: React.FC = () => {
onCancel={handleModalCancel} onCancel={handleModalCancel}
onOk={handleModalOk} onOk={handleModalOk}
></AddCluster> ></AddCluster>
<AddPool <AddWorker
provider={addPoolStatus.provider} open={openAddWorker.open}
open={addPoolStatus.open} onCancel={() => setOpenAddWorker({ open: false, token: '' })}
action={addPoolStatus.action} token={openAddWorker.token}
title={addPoolStatus.title} ></AddWorker>
onCancel={() => {
setAddPoolStatus({
open: false,
action: PageAction.CREATE,
title: '',
provider: 'digitalocean'
});
}}
onOk={() => {
setAddPoolStatus({
open: false,
action: PageAction.CREATE,
title: '',
provider: 'digitalocean'
});
}}
></AddPool>
<AddWorker open={open} onCancel={() => setOpen(false)}></AddWorker>
<DeleteModal ref={modalRef}></DeleteModal> <DeleteModal ref={modalRef}></DeleteModal>
</> </>
); );
@@ -2,15 +2,18 @@ import ModalFooter from '@/components/modal-footer';
import ScrollerModal from '@/components/scroller-modal/index'; import ScrollerModal from '@/components/scroller-modal/index';
import SealInput from '@/components/seal-form/seal-input'; import SealInput from '@/components/seal-form/seal-input';
import { PageActionType } from '@/config/types'; import { PageActionType } from '@/config/types';
import { CloseOutlined } from '@ant-design/icons'; import ContainerInstall from '@/pages/resources/components/container-install';
import { CheckCircleFilled } from '@ant-design/icons';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Button, Form } from 'antd'; import { Form } from 'antd';
import React from 'react'; import React, { useMemo } from 'react';
import { ProviderValueMap } from '../config';
import { import {
ClusterFormData as FormData, ClusterFormData as FormData,
ClusterListItem as ListItem ClusterListItem as ListItem
} from '../config/types'; } from '../config/types';
import CloudProvider from './cloud-provider-form'; import CloudProvider from './cloud-provider-form';
import RegisterClusterInner from './resiter-cluster-inner';
type AddModalProps = { type AddModalProps = {
title: string; title: string;
@@ -18,7 +21,6 @@ type AddModalProps = {
open: boolean; open: boolean;
provider: string; // 'kubernetes' | 'custom' | 'digitalocean'; provider: string; // 'kubernetes' | 'custom' | 'digitalocean';
onOk: (values: FormData) => void; onOk: (values: FormData) => void;
data?: ListItem;
onCancel: () => void; onCancel: () => void;
}; };
const AddCluster: React.FC<AddModalProps> = ({ const AddCluster: React.FC<AddModalProps> = ({
@@ -27,11 +29,14 @@ const AddCluster: React.FC<AddModalProps> = ({
open, open,
provider, provider,
onOk, onOk,
data,
onCancel onCancel
}) => { }) => {
const [form] = Form.useForm(); const [form] = Form.useForm();
const intl = useIntl(); const intl = useIntl();
const [submissionStatus, setSubmissionStatus] = React.useState<{
success: boolean;
data: ListItem;
}>({ success: true, data: {} as ListItem });
const handleSubmit = () => { const handleSubmit = () => {
form.submit(); form.submit();
@@ -42,56 +47,98 @@ const AddCluster: React.FC<AddModalProps> = ({
onCancel(); onCancel();
}; };
const renderAddWorkerContent = () => {
if (provider === ProviderValueMap.Custom) {
return <ContainerInstall token="${token}" />;
}
if (provider === ProviderValueMap.Kubernetes) {
return (
<RegisterClusterInner
data={submissionStatus.data}
></RegisterClusterInner>
);
}
return null;
};
const modalTitle = useMemo(() => {
if (submissionStatus.success) {
return (
<div className="flex-center">
<CheckCircleFilled
className="text-success font-size-20"
style={{ marginRight: '8px' }}
/>
<span>Cluster added! Now you can register a worker.</span>
</div>
);
}
return title;
}, [submissionStatus.success, title]);
const renderFooter = () => {
if (submissionStatus.success) {
return (
<ModalFooter
onOk={onCancel}
onCancel={onCancel}
showCancelBtn={false}
okText="Skip for Now"
okBtnProps={{
style: {
width: 'auto'
}
}}
></ModalFooter>
);
}
return <ModalFooter onOk={handleSubmit} onCancel={onCancel}></ModalFooter>;
};
return ( return (
<ScrollerModal <ScrollerModal
title={ title={modalTitle}
<div className="flex-between flex-center">
<span>{title}</span>
<Button type="text" size="small" onClick={handleCancel}>
<CloseOutlined></CloseOutlined>
</Button>
</div>
}
open={open} open={open}
onClose={onCancel} onCancel={handleCancel}
destroyOnClose={true} destroyOnClose={true}
closeIcon={false} closeIcon={true}
maskClosable={false} maskClosable={false}
keyboard={false} keyboard={false}
width={600} width={680}
footer={ footer={renderFooter()}
<ModalFooter onOk={handleSubmit} onCancel={onCancel}></ModalFooter>
}
> >
<Form form={form} onFinish={onOk} preserve={false}> {submissionStatus.success ? (
<Form.Item<FormData> renderAddWorkerContent()
name="display_name" ) : (
rules={[ <Form form={form} onFinish={onOk} preserve={false}>
{ <Form.Item<FormData>
required: true, name="display_name"
message: intl.formatMessage( rules={[
{ id: 'common.form.rule.input' }, {
{ required: true,
name: intl.formatMessage({ id: 'common.table.name' }) message: intl.formatMessage(
} { id: 'common.form.rule.input' },
) {
} name: intl.formatMessage({ id: 'common.table.name' })
]} }
> )
<SealInput.Input }
label={intl.formatMessage({ id: 'common.table.name' })} ]}
required >
></SealInput.Input> <SealInput.Input
</Form.Item> label={intl.formatMessage({ id: 'common.table.name' })}
{provider === 'digitalocean' && ( required
<CloudProvider provider={provider}></CloudProvider> ></SealInput.Input>
)} </Form.Item>
<Form.Item<FormData> name="description" rules={[{ required: false }]}> {provider === 'digitalocean' && (
<SealInput.TextArea <CloudProvider provider={provider}></CloudProvider>
label={intl.formatMessage({ id: 'common.table.description' })} )}
></SealInput.TextArea> <Form.Item<FormData> name="description" rules={[{ required: false }]}>
</Form.Item> <SealInput.TextArea
</Form> label={intl.formatMessage({ id: 'common.table.description' })}
></SealInput.TextArea>
</Form.Item>
</Form>
)}
</ScrollerModal> </ScrollerModal>
); );
}; };
@@ -0,0 +1,140 @@
import GaugeChart from '@/components/echarts/gauge';
import { PageAction } from '@/config';
import { Col, Row } from 'antd';
import React from 'react';
import styled from 'styled-components';
import { ProviderValueMap } from '../config';
import { ClusterListItem, NodePoolListItem } from '../config/types';
import AddPool from './add-pool';
import WorkerPools from './worker-pools';
const SubTitle = styled.div`
font-size: var(--font-size-middle);
font-weight: 500;
color: var(--ant-color-text);
margin-block: 24px 16px;
`;
interface ClusterDetailProps {
provider: string;
data: ClusterListItem;
show: boolean;
}
const gaugeConfig = {
radius: '100%',
progress: {
show: true,
roundCap: false,
width: 8
},
axisLine: {
roundCap: false,
lineStyle: {
width: 8,
color: [
[0.5, 'rgba(84, 204, 152, 80%)'],
[0.8, 'rgba(250, 173, 20, 80%)'],
[1, 'rgba(255, 77, 79, 80%)']
]
}
}
};
const ClusterDetail: React.FC<ClusterDetailProps> = ({ data, show }) => {
const chartHeight = 160;
const [addPoolStatus, setAddPoolStatus] = React.useState({
open: false,
action: PageAction.CREATE,
title: '',
provider: 'digitalocean'
});
// pool action handler
const handleOnAction = (action: string, record: NodePoolListItem) => {
if (action === 'edit') {
setAddPoolStatus({
open: true,
action: PageAction.CREATE,
title: 'Edit Worker Pool',
provider: data.provider
});
}
};
return (
<div>
<div className="chart-wrapper">
<Row gutter={16} style={{ width: '100%' }}>
<Col span={6}>
<GaugeChart
title="GPU Utilization"
value={85}
height={chartHeight}
gaugeConfig={gaugeConfig}
/>
</Col>
<Col span={6}>
<GaugeChart
title="CPU Utilization"
value={50}
height={chartHeight}
gaugeConfig={gaugeConfig}
/>
</Col>
<Col span={6}>
<GaugeChart
title="RAM Utilization"
value={70}
height={chartHeight}
gaugeConfig={gaugeConfig}
/>
</Col>
<Col span={6}>
<GaugeChart
title="VRAM Utilization"
value={60}
height={chartHeight}
gaugeConfig={gaugeConfig}
/>
</Col>
</Row>
</div>
{data.provider === ProviderValueMap.DigitalOcean && (
<>
<SubTitle>Worker Pools</SubTitle>
<WorkerPools
provider={data.provider}
workerPools={data.worker_pools}
height={show ? 'auto' : 0}
onAction={handleOnAction}
/>
</>
)}
<AddPool
provider={addPoolStatus.provider}
open={addPoolStatus.open}
action={addPoolStatus.action}
title={addPoolStatus.title}
onCancel={() => {
setAddPoolStatus({
open: false,
action: PageAction.CREATE,
title: '',
provider: 'digitalocean'
});
}}
onOk={() => {
setAddPoolStatus({
open: false,
action: addPoolStatus.action,
title: '',
provider: 'digitalocean'
});
}}
></AddPool>
</div>
);
};
export default ClusterDetail;
@@ -2,13 +2,15 @@ import DropdownButtons from '@/components/drop-down-buttons';
import GaugeChart from '@/components/echarts/gauge'; import GaugeChart from '@/components/echarts/gauge';
import IconFont from '@/components/icon-font'; import IconFont from '@/components/icon-font';
import StatusTag from '@/components/status-tag'; import StatusTag from '@/components/status-tag';
import ThemeTag from '@/components/tags-wrapper/theme-tag';
import Card from '@/components/templates/card'; import Card from '@/components/templates/card';
import { PageAction } from '@/config';
import { import {
DeleteOutlined, DeleteOutlined,
EditOutlined, EditOutlined,
KubernetesOutlined KubernetesOutlined
} from '@ant-design/icons'; } from '@ant-design/icons';
import { Card as ACard, Button, Col, Row, Tag } from 'antd'; import { Card as ACard, Col, Collapse, Row } from 'antd';
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import { import {
@@ -18,65 +20,21 @@ import {
ProviderValueMap ProviderValueMap
} from '../config'; } from '../config';
import { ClusterListItem as ListItem, NodePoolListItem } from '../config/types'; import { ClusterListItem as ListItem, NodePoolListItem } from '../config/types';
import AddPool from './add-pool';
import RegisterCluster from './register-cluster';
import WorkerPools from './worker-pools'; import WorkerPools from './worker-pools';
const CollapseTitle = styled.div`
display: flex;
align-items: center;
gap: 8px;
font-size: var(--font-size-middle);
font-weight: 500;
color: var(--ant-color-text);
height: 32px;
cursor: pointer;
`;
const Content = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
.chart-wrapper {
flex: 1;
}
`;
const CardWrapper = styled(ACard)`
text-align: center;
box-shadow: none;
flex: 1;
.ant-card {
box-shadow: none;
}
.ant-card-body {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
gap: 8px;
}
.label {
font-weight: 500;
font-size: var(--font-size-middle);
}
.value {
font-weight: 500;
color: var(--ant-color-text);
}
`;
const CardBox = styled.div`
display: flex;
gap: 16px;
flex: 0.5;
`;
const actionItems = [ const actionItems = [
{ {
key: 'edit', key: 'edit',
label: 'common.button.edit', label: 'common.button.edit',
icon: <EditOutlined /> icon: <EditOutlined />
}, },
{
key: 'view',
label: 'common.button.view',
icon: <EditOutlined />
},
{ {
key: 'add_worker', key: 'add_worker',
label: 'Add Worker', label: 'Add Worker',
@@ -108,6 +66,60 @@ const actionItems = [
} }
]; ];
const CollapseTitle = styled.div`
display: flex;
align-items: center;
gap: 8px;
font-size: var(--font-size-middle);
font-weight: 500;
color: var(--ant-color-text);
height: 32px;
cursor: pointer;
`;
const Content = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
.chart-wrapper {
flex: 1;
}
`;
const CardWrapper = styled(ACard)`
text-align: center;
box-shadow: none !important;
flex: 1;
.ant-card {
box-shadow: none;
}
.ant-card-body {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
padding: 6px 10px;
}
.label {
font-weight: 400;
margin-bottom: 12px;
font-size: var(--font-size-middle);
color: var(--ant-color-text-secondary);
}
.value {
font-weight: 600;
color: var(--ant-color-text);
font-size: var(--font-size-large);
}
`;
const CardBox = styled.div`
display: flex;
gap: 16px;
flex: 1;
`;
const Inner = styled.div` const Inner = styled.div`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -116,7 +128,7 @@ const Inner = styled.div`
cursor: default; cursor: default;
.title { .title {
margin-bottom: 16px; margin-bottom: 12px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
@@ -139,6 +151,38 @@ const Inner = styled.div`
} }
`; `;
const CollapseWrapper = styled(Collapse)`
width: 100%;
border: none;
border-radius: 0;
border-top: 1px solid var(--ant-color-split);
background-color: var(--ant-color-fill-quaternary);
.ant-collapse-content {
border-top: 1px solid var(--ant-color-split);
}
.ant-collapse-header {
font-size: var(--font-size-middle);
padding-block: 10px !important;
.ant-collapse-expand-icon {
margin-left: 0 !important;
}
}
table .ant-table-thead tr {
background-color: transparent !important;
th {
border-bottom: 1px solid var(--ant-color-split) !important;
font-weight: 500 !important;
}
}
`;
const SubTitle = styled.div`
font-size: var(--font-size-middle);
font-weight: 500;
color: var(--ant-color-text);
margin-block: 24px 16px;
`;
interface CardProps { interface CardProps {
data: ListItem; data: ListItem;
onSelect?: (key: string, row: ListItem) => void; onSelect?: (key: string, row: ListItem) => void;
@@ -165,14 +209,51 @@ const gaugeConfig = {
}; };
const CardItem: React.FC<CardProps> = (props) => { const CardItem: React.FC<CardProps> = (props) => {
const chartHeight = 160;
const { data, onSelect } = props; const { data, onSelect } = props;
const [show, setShow] = React.useState(false); const [show, setShow] = React.useState(false);
const [addPoolStatus, setAddPoolStatus] = React.useState({
open: false,
action: PageAction.CREATE,
title: '',
provider: 'digitalocean'
});
const [registerClusterStatus, setRegisterClusterStatus] = React.useState({
open: false
});
// cluster action handler
const handleOnSelect = (key: string) => { const handleOnSelect = (key: string) => {
if (key === 'addPool') {
setAddPoolStatus({
open: true,
action: PageAction.CREATE,
title: 'Add Worker Pool',
provider: data.provider
});
return;
}
if (key === 'register_cluster') {
setRegisterClusterStatus({
open: true
});
return;
}
onSelect?.(key, data); onSelect?.(key, data);
}; };
const handleOnAction = (action: string, record: NodePoolListItem) => {}; // pool action handler
const handleOnAction = (action: string, record: NodePoolListItem) => {
if (action === 'edit') {
setAddPoolStatus({
open: true,
action: PageAction.CREATE,
title: 'Edit Worker Pool',
provider: data.provider
});
}
};
const actions = useMemo(() => { const actions = useMemo(() => {
return actionItems.filter((item) => { return actionItems.filter((item) => {
@@ -184,14 +265,82 @@ const CardItem: React.FC<CardProps> = (props) => {
}, [data.provider]); }, [data.provider]);
return ( return (
<Card height={'auto'} clickable={false} ghost> <Card
height={'auto'}
clickable={false}
ghost
footer={
<CollapseWrapper
onChange={() => setShow(!show)}
expandIconPosition="end"
expandIcon={({ isActive }) => (
<IconFont type="icon-down" rotate={isActive ? 0 : -90} />
)}
items={[
{
key: '1',
label: 'More Information',
children: (
<>
<div className="chart-wrapper">
<Row gutter={16} style={{ width: '100%' }}>
<Col span={6}>
<GaugeChart
title="GPU Utilization"
value={85}
height={chartHeight}
gaugeConfig={gaugeConfig}
/>
</Col>
<Col span={6}>
<GaugeChart
title="CPU Utilization"
value={50}
height={chartHeight}
gaugeConfig={gaugeConfig}
/>
</Col>
<Col span={6}>
<GaugeChart
title="RAM Utilization"
value={70}
height={chartHeight}
gaugeConfig={gaugeConfig}
/>
</Col>
<Col span={6}>
<GaugeChart
title="VRAM Utilization"
value={60}
height={chartHeight}
gaugeConfig={gaugeConfig}
/>
</Col>
</Row>
</div>
{data.provider === ProviderValueMap.DigitalOcean && (
<>
<SubTitle>Worker Pools</SubTitle>
<WorkerPools
provider={data.provider}
workerPools={data.worker_pools}
height={show ? 'auto' : 0}
onAction={handleOnAction}
/>
</>
)}
</>
)
}
]}
></CollapseWrapper>
}
>
<Inner> <Inner>
<div className="title"> <div className="title">
<span className="flex-center gap-8"> <span className="flex-center gap-8">
<span className="text">{data.name}</span> <span className="text">{data.name}</span>
<Tag style={{ borderRadius: 4 }}> <ThemeTag>{ProviderLabelMap[data.provider]}</ThemeTag>
{ProviderLabelMap[data.provider]}
</Tag>
<StatusTag <StatusTag
statusValue={{ statusValue={{
status: ClusterStatus[data.status], status: ClusterStatus[data.status],
@@ -210,7 +359,7 @@ const CardItem: React.FC<CardProps> = (props) => {
<CardBox> <CardBox>
<CardWrapper bordered={false}> <CardWrapper bordered={false}>
<div className="label">Workers</div> <div className="label">Workers</div>
<div className="value">1</div> <div className="value">1/1</div>
</CardWrapper> </CardWrapper>
<CardWrapper bordered={false}> <CardWrapper bordered={false}>
<div className="label">GPUs</div> <div className="label">GPUs</div>
@@ -221,64 +370,40 @@ const CardItem: React.FC<CardProps> = (props) => {
<div className="value">2</div> <div className="value">2</div>
</CardWrapper> </CardWrapper>
</CardBox> </CardBox>
<div className="chart-wrapper">
<Row gutter={16} style={{ width: '100%' }}>
<Col span={6}>
<GaugeChart
title="GPU Utilization"
value={85}
height={160}
gaugeConfig={gaugeConfig}
/>
</Col>
<Col span={6}>
<GaugeChart
title="CPU Utilization"
value={50}
height={160}
gaugeConfig={gaugeConfig}
/>
</Col>
<Col span={6}>
<GaugeChart
title="RAM Utilization"
value={70}
height={160}
gaugeConfig={gaugeConfig}
/>
</Col>
<Col span={6}>
<GaugeChart
title="VRAM Utilization"
value={60}
height={160}
gaugeConfig={gaugeConfig}
/>
</Col>
</Row>
</div>
</Content> </Content>
{data.provider === ProviderValueMap.DigitalOcean && (
<>
<CollapseTitle>
<Button
type="text"
size="small"
icon={<IconFont type="icon-down" rotate={show ? 0 : -90} />}
onClick={() => setShow(!show)}
>
Worker Pools
</Button>
</CollapseTitle>
<WorkerPools
provider={data.provider}
workerPools={data.worker_pools}
height={show ? 'auto' : 0}
onAction={handleOnAction}
/>
</>
)}
</Inner> </Inner>
<AddPool
provider={addPoolStatus.provider}
open={addPoolStatus.open}
action={addPoolStatus.action}
title={addPoolStatus.title}
onCancel={() => {
setAddPoolStatus({
open: false,
action: PageAction.CREATE,
title: '',
provider: 'digitalocean'
});
}}
onOk={() => {
setAddPoolStatus({
open: false,
action: addPoolStatus.action,
title: '',
provider: 'digitalocean'
});
}}
></AddPool>
<RegisterCluster
title="Register Cluster"
open={registerClusterStatus.open}
data={data}
onCancel={() => {
setRegisterClusterStatus({
open: false
});
}}
></RegisterCluster>
</Card> </Card>
); );
}; };
@@ -1,44 +1,21 @@
import ModalFooter from '@/components/modal-footer';
import ScrollerModal from '@/components/scroller-modal/index'; import ScrollerModal from '@/components/scroller-modal/index';
import SealInput from '@/components/seal-form/seal-input';
import { PageActionType } from '@/config/types';
import { useIntl } from '@umijs/max';
import { Form } from 'antd';
import React from 'react'; import React from 'react';
import { import { ClusterListItem as ListItem } from '../config/types';
ClusterFormData as FormData, import RegisterClusterInner from './resiter-cluster-inner';
ClusterListItem as ListItem
} from '../config/types';
import CloudProvider from './cloud-provider-form';
import K8SProvider from './k8s-provider-form';
type AddModalProps = { type AddModalProps = {
title: string; title: string;
action: PageActionType;
open: boolean; open: boolean;
provider: string; // 'kubernetes' | 'custom' | 'digitalocean'; data: ListItem;
onOk: (values: FormData) => void;
data?: ListItem;
onCancel: () => void; onCancel: () => void;
}; };
const AddCluster: React.FC<AddModalProps> = ({ const AddCluster: React.FC<AddModalProps> = ({
title, title,
action,
open, open,
provider,
onOk,
data, data,
onCancel onCancel
}) => { }) => {
const [form] = Form.useForm();
const intl = useIntl();
const handleSubmit = () => {
form.submit();
};
const handleCancel = () => { const handleCancel = () => {
form.resetFields();
onCancel(); onCancel();
}; };
@@ -52,42 +29,9 @@ const AddCluster: React.FC<AddModalProps> = ({
maskClosable={false} maskClosable={false}
keyboard={false} keyboard={false}
width={600} width={600}
footer={ footer={false}
<ModalFooter onOk={handleSubmit} onCancel={onCancel}></ModalFooter>
}
> >
<Form form={form} onFinish={onOk} preserve={false}> <RegisterClusterInner data={data} />
<Form.Item<FormData>
name="display_name"
rules={[
{
required: true,
message: intl.formatMessage(
{ id: 'common.form.rule.input' },
{
name: intl.formatMessage({ id: 'common.table.name' })
}
)
}
]}
>
<SealInput.Input
label={intl.formatMessage({ id: 'common.table.name' })}
required
></SealInput.Input>
</Form.Item>
{provider === 'digitalocean' && (
<CloudProvider provider={provider}></CloudProvider>
)}
{provider === 'kubernetes' && (
<K8SProvider provider={provider}></K8SProvider>
)}
<Form.Item<FormData> name="description" rules={[{ required: false }]}>
<SealInput.TextArea
label={intl.formatMessage({ id: 'common.table.description' })}
></SealInput.TextArea>
</Form.Item>
</Form>
</ScrollerModal> </ScrollerModal>
); );
}; };
@@ -0,0 +1,53 @@
import HighlightCode from '@/components/highlight-code';
import React, { useEffect } from 'react';
import { queryClusterToken } from '../apis';
import { generateRegisterCommand } from '../config';
import { ClusterListItem as ListItem } from '../config/types';
type AddModalProps = {
data: ListItem;
};
const AddCluster: React.FC<AddModalProps> = ({ data }) => {
const [code, setCode] = React.useState<string>('');
const getToken = async () => {
const res = await queryClusterToken(data?.id);
return res.data?.token || '';
};
const getCode = async () => {
try {
const token = await getToken();
const command = generateRegisterCommand({
server: window.location.origin,
clusterId: data?.id || 0,
registrationToken: token
});
setCode(command);
} catch (error) {
setCode(
generateRegisterCommand({
server: window.location.origin,
clusterId: data?.id || 0,
registrationToken: '{token}'
})
);
}
};
useEffect(() => {
getCode();
}, []);
return (
<div>
<HighlightCode
theme="dark"
code={code.replace(/\\/g, '')}
copyValue={code}
lang="bash"
></HighlightCode>
</div>
);
};
export default AddCluster;
@@ -71,9 +71,9 @@ const WorkerPools: React.FC<WorkerPoolsProps> = ({
const columns = [ const columns = [
{ {
title: 'Type', title: 'Instance Type',
dataIndex: 'type', dataIndex: 'instance_type',
key: 'type' key: 'instance_type'
}, },
{ {
title: 'Replicas', title: 'Replicas',
@@ -126,7 +126,7 @@ const WorkerPools: React.FC<WorkerPoolsProps> = ({
const mockData = Array.from({ length: 3 }, (_, index) => ({ const mockData = Array.from({ length: 3 }, (_, index) => ({
id: index + 1, id: index + 1,
key: index, key: index,
type: `Type ${index + 1}`, instance_type: `Type ${index + 1}`,
replicas: Math.floor(Math.random() * 10) + 1, replicas: Math.floor(Math.random() * 10) + 1,
batchSize: Math.floor(Math.random() * 100) + 1, batchSize: Math.floor(Math.random() * 100) + 1,
gpu: `NVIDIA 4090`, gpu: `NVIDIA 4090`,
@@ -59,3 +59,12 @@ export const ProviderLabelMap = {
[ProviderValueMap.DigitalOcean]: 'Digital Ocean', [ProviderValueMap.DigitalOcean]: 'Digital Ocean',
[ProviderValueMap.Custom]: 'Custom' [ProviderValueMap.Custom]: 'Custom'
}; };
export const generateRegisterCommand = (params: {
server: string;
clusterId: number;
registrationToken: string;
}) => {
return `curl -k -L '${params.server}/v2/clusters/${params.clusterId}/manifests' \\
--header 'Authorization: Bearer ${params.registrationToken}'`;
};
+2 -2
View File
@@ -20,8 +20,8 @@ const renderCardItem = (data: {
className={styles['card-body']} className={styles['card-body']}
> >
<div className={styles.content}> <div className={styles.content}>
<div className="label font-500">{label}</div> <div className="label text-secondary">{label}</div>
<div className="value font-500">{value}</div> <div className="value font-600 font-size-16">{value}</div>
</div> </div>
</Card> </Card>
); );
@@ -12,7 +12,7 @@ const GGUFResult: React.FC = () => {
/> />
} }
title={false} title={false}
subTitle="GGUF model is not supported yet." subTitle="GGUF model is not supported."
/> />
); );
}; };
+2 -49
View File
@@ -1,64 +1,17 @@
import ScrollerModal from '@/components/scroller-modal'; import ScrollerModal from '@/components/scroller-modal';
import {
AppleOutlined,
LinuxOutlined,
WindowsOutlined
} from '@ant-design/icons';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { TabsProps } from 'antd';
import React from 'react'; import React from 'react';
import MacOS from './add-worker-macos';
import ContainerInstall from './container-install'; import ContainerInstall from './container-install';
type ViewModalProps = { type ViewModalProps = {
open: boolean; open: boolean;
token: string;
onCancel: () => void; onCancel: () => void;
}; };
const AddWorker: React.FC<ViewModalProps> = (props) => { const AddWorker: React.FC<ViewModalProps> = (props) => {
const { open, onCancel } = props || {}; const { open, onCancel, token = '${token}' } = props || {};
const intl = useIntl(); const intl = useIntl();
const [token, setToken] = React.useState('');
const [activeKey, setActiveKey] = React.useState('container');
const items: TabsProps['items'] = [
{
key: 'container',
label: 'Linux',
icon: <LinuxOutlined />,
children: <ContainerInstall token={token} />
},
{
key: 'macos',
label: 'macOS',
icon: <AppleOutlined />,
children: (
<MacOS
token={token}
platform={{
os: 'macOS',
downloadurl: 'https://gpustack.ai/',
supportVersions: 'resource.register.maos.support'
}}
/>
)
},
{
key: 'windows',
label: 'Windows',
icon: <WindowsOutlined />,
children: (
<MacOS
token={token}
platform={{
os: 'Windows',
downloadurl: 'https://gpustack.ai/',
supportVersions: 'resource.register.windows.support'
}}
/>
)
}
];
return ( return (
<ScrollerModal <ScrollerModal
@@ -42,7 +42,7 @@ const AddWorker: React.FC<ViewModalProps> = (props) => {
return commandCode?.registerWorker({ return commandCode?.registerWorker({
server: origin, server: origin,
tag: tag, tag: tag,
token: '${token}', token: props.token || '${token}',
workerip: '${workerip}' workerip: '${workerip}'
}); });
}, [versionInfo, activeKey, props.token, npuKey]); }, [versionInfo, activeKey, props.token, npuKey]);
@@ -53,38 +53,8 @@ const AddWorker: React.FC<ViewModalProps> = (props) => {
return ( return (
<div className="container-install"> <div className="container-install">
<ul className="notes">
<li>
{intl.formatMessage(
{ id: 'resources.worker.current.version' },
{ version: versionInfo.version }
)}
</li>
<li>
<span
dangerouslySetInnerHTML={{
__html: intl.formatMessage({
id: 'resources.worker.driver.install'
})
}}
></span>
</li>
</ul>
<h3 className="font-size-14 font-600">
1.{' '}
<span
dangerouslySetInnerHTML={{
__html: intl.formatMessage({ id: 'resources.worker.add.step1' })
}}
></span>
</h3>
<HighlightCode
code={addWorkerGuide.container.getToken}
theme="dark"
lang="bash"
></HighlightCode>
<h3 className="m-t-10 font-size-14 font-600"> <h3 className="m-t-10 font-size-14 font-600">
2. {intl.formatMessage({ id: 'resources.worker.add.step2' })}{' '} 1. {intl.formatMessage({ id: 'resources.worker.add.step2' })}{' '}
<span <span
className="font-size-12" className="font-size-12"
style={{ color: 'var(--ant-color-text-tertiary)' }} style={{ color: 'var(--ant-color-text-tertiary)' }}
@@ -146,7 +116,7 @@ const AddWorker: React.FC<ViewModalProps> = (props) => {
></HighlightCode> ></HighlightCode>
)} )}
<h3 className="m-b-0 m-t-10 font-size-14 font-600"> <h3 className="m-b-0 m-t-10 font-size-14 font-600">
3. {intl.formatMessage({ id: 'resources.worker.add.step3' })} 2. {intl.formatMessage({ id: 'resources.worker.add.step3' })}
</h3> </h3>
</div> </div>
); );
+19 -16
View File
@@ -22,14 +22,14 @@ export const addWorkerGuide: Record<string, any> = {
mac: { mac: {
getToken: 'cat /var/lib/gpustack/token', getToken: 'cat /var/lib/gpustack/token',
registerWorker(params: { server: string; token: string }) { registerWorker(params: { server: string; token: string }) {
return `curl -sfL https://get.gpustack.ai | sh -s - --server-url ${params.server} --token ${params.token}`; return `curl -sfL https://get.gpustack.ai | sh -s - --server-url ${params.server} --registration ${params.token}`;
} }
}, },
win: { win: {
getToken: getToken:
'Get-Content -Path (Join-Path -Path $env:APPDATA -ChildPath "gpustack\\token") -Raw', 'Get-Content -Path (Join-Path -Path $env:APPDATA -ChildPath "gpustack\\token") -Raw',
registerWorker(params: { server: string; token: string }) { registerWorker(params: { server: string; token: string }) {
return `Invoke-Expression "& { $((Invoke-WebRequest -Uri 'https://get.gpustack.ai' -UseBasicParsing).Content) } --server-url '${params.server}' --token '${params.token}'"`; return `Invoke-Expression "& { $((Invoke-WebRequest -Uri 'https://get.gpustack.ai' -UseBasicParsing).Content) } --server-url '${params.server}' --registration '${params.token}'"`;
} }
}, },
cuda: { cuda: {
@@ -41,11 +41,14 @@ export const addWorkerGuide: Record<string, any> = {
token: string; token: string;
workerip: string; workerip: string;
}) { }) {
return `docker run -d \\ return `docker run -d --name gpustack \\
--net=host \\ --restart=unless-stopped \\
-v /var/lib/gpustack:/var/lib/gpustack \\ --gpus all \\
--privileged gpustack/gpustack:xxxx \\ --network=host \\
--registration ${params.token}`; --ipc=host \\
-v gpustack-data:/var/lib/gpustack \\
gpustack/gpustack:${params.tag} \\
--server-url ${params.server} --registration ${params.token} --worker-ip ${params.workerip}`;
} }
}, },
npu: { npu: {
@@ -73,7 +76,7 @@ export const addWorkerGuide: Record<string, any> = {
--ipc=host \\ --ipc=host \\
-v gpustack-data:/var/lib/gpustack \\ -v gpustack-data:/var/lib/gpustack \\
gpustack/gpustack:${params.tag} \\ gpustack/gpustack:${params.tag} \\
--server-url ${params.server} --token ${params.token} --worker-ip ${params.workerip}`; --server-url ${params.server} --registration ${params.token} --worker-ip ${params.workerip}`;
} }
}, },
npu310p: { npu310p: {
@@ -101,7 +104,7 @@ export const addWorkerGuide: Record<string, any> = {
--ipc=host \\ --ipc=host \\
-v gpustack-data:/var/lib/gpustack \\ -v gpustack-data:/var/lib/gpustack \\
gpustack/gpustack:${params.tag}-310p \\ gpustack/gpustack:${params.tag}-310p \\
--server-url ${params.server} --token ${params.token} --worker-ip ${params.workerip}`; --server-url ${params.server} --registration ${params.token} --worker-ip ${params.workerip}`;
} }
}, },
musa: { musa: {
@@ -119,7 +122,7 @@ export const addWorkerGuide: Record<string, any> = {
--ipc=host \\ --ipc=host \\
-v gpustack-data:/var/lib/gpustack \\ -v gpustack-data:/var/lib/gpustack \\
gpustack/gpustack:${params.tag} \\ gpustack/gpustack:${params.tag} \\
--server-url ${params.server} --token ${params.token} --worker-ip ${params.workerip}`; --server-url ${params.server} --registration ${params.token} --worker-ip ${params.workerip}`;
} }
}, },
cpu: { cpu: {
@@ -136,7 +139,7 @@ export const addWorkerGuide: Record<string, any> = {
--network=host \\ --network=host \\
-v gpustack-data:/var/lib/gpustack \\ -v gpustack-data:/var/lib/gpustack \\
gpustack/gpustack:${params.tag} \\ gpustack/gpustack:${params.tag} \\
--server-url ${params.server} --token ${params.token} --worker-ip ${params.workerip}`; --server-url ${params.server} --registration ${params.token} --worker-ip ${params.workerip}`;
} }
}, },
rocm: { rocm: {
@@ -157,7 +160,7 @@ export const addWorkerGuide: Record<string, any> = {
--security-opt seccomp=unconfined \\ --security-opt seccomp=unconfined \\
-v gpustack-data:/var/lib/gpustack \\ -v gpustack-data:/var/lib/gpustack \\
gpustack/gpustack:${params.tag} \\ gpustack/gpustack:${params.tag} \\
--server-url ${params.server} --token ${params.token} --worker-ip ${params.workerip}`; --server-url ${params.server} --registration ${params.token} --worker-ip ${params.workerip}`;
} }
}, },
dcu: { dcu: {
@@ -180,7 +183,7 @@ export const addWorkerGuide: Record<string, any> = {
--security-opt seccomp=unconfined \\ --security-opt seccomp=unconfined \\
-v gpustack-data:/var/lib/gpustack \\ -v gpustack-data:/var/lib/gpustack \\
gpustack/gpustack:${params.tag} \\ gpustack/gpustack:${params.tag} \\
--server-url ${params.server} --token ${params.token} --worker-ip ${params.workerip}`; --server-url ${params.server} --registration ${params.token} --worker-ip ${params.workerip}`;
} }
}, },
corex: { corex: {
@@ -201,7 +204,7 @@ export const addWorkerGuide: Record<string, any> = {
--ipc=host \\ --ipc=host \\
-v gpustack-data:/var/lib/gpustack \\ -v gpustack-data:/var/lib/gpustack \\
gpustack/gpustack:${params.tag} \\ gpustack/gpustack:${params.tag} \\
--server-url ${params.server} --token ${params.token} --worker-ip ${params.workerip}`; --server-url ${params.server} --registration ${params.token} --worker-ip ${params.workerip}`;
} }
}, },
container: { container: {
@@ -216,8 +219,8 @@ export const containerInstallOptions = [
{ label: 'Ascend CANN', value: 'npu' }, { label: 'Ascend CANN', value: 'npu' },
{ label: 'Hygon DTK', value: 'dcu' }, { label: 'Hygon DTK', value: 'dcu' },
{ label: 'Moore Threads MUSA', value: 'musa' }, { label: 'Moore Threads MUSA', value: 'musa' },
{ label: 'Iluvatar Corex', value: 'corex' }, { label: 'Iluvatar Corex', value: 'corex' }
{ label: 'CPU', value: 'cpu' } // { label: 'CPU', value: 'cpu' }
]; ];
export const ModelfileStateMap = { export const ModelfileStateMap = {