chore: create cluster ux
This commit is contained in:
@@ -54,16 +54,6 @@ export default [
|
||||
defaultIcon: 'icon-reranker',
|
||||
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',
|
||||
title: 'Speech',
|
||||
|
||||
@@ -324,3 +324,7 @@ textarea:hover {
|
||||
.ant-result-extra {
|
||||
color: var(--ant-color-text-secondary);
|
||||
}
|
||||
|
||||
.text-success {
|
||||
color: var(--ant-color-success);
|
||||
}
|
||||
|
||||
@@ -24,6 +24,8 @@ export default function useChartConfig() {
|
||||
splitLineColor: token.colorBorder,
|
||||
tickLineColor: token.colorSplit,
|
||||
axislabelColor: token.colorTextTertiary,
|
||||
colorSecondary: token.colorTextSecondary,
|
||||
colorTertiary: token.colorTextTertiary,
|
||||
gaugeBgColor: token.colorFillSecondary,
|
||||
gaugeSplitLineColor: isDarkTheme
|
||||
? 'rgba(255,255,255,.3)'
|
||||
@@ -172,8 +174,8 @@ export default function useChartConfig() {
|
||||
}
|
||||
},
|
||||
axisTick: {
|
||||
distance: -11,
|
||||
length: 6,
|
||||
distance: -10,
|
||||
length: 5,
|
||||
splitNumber: 5,
|
||||
lineStyle: {
|
||||
width: 1.5,
|
||||
@@ -206,13 +208,13 @@ export default function useChartConfig() {
|
||||
rich: {
|
||||
value: {
|
||||
fontSize: 16,
|
||||
fontWeight: '500',
|
||||
fontWeight: '600',
|
||||
color: chartColorMap.titleColor
|
||||
},
|
||||
unit: {
|
||||
fontSize: 14,
|
||||
color: chartColorMap.titleColor,
|
||||
fontWeight: '500',
|
||||
fontWeight: '600',
|
||||
padding: [0, 0, 0, 2]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Chart from '@/components/echarts/chart';
|
||||
import useChartConfig from '@/components/echarts/config';
|
||||
import EmptyData from '@/components/empty-data';
|
||||
import React, { memo } from 'react';
|
||||
import React from 'react';
|
||||
import { ChartProps } from './types';
|
||||
|
||||
const strokeColorFunc = (percent: number) => {
|
||||
@@ -34,10 +34,19 @@ const GaugeChart: React.FC<Omit<ChartProps, 'seriesData' | 'xAxisData'>> = (
|
||||
...gaugeItemConfig,
|
||||
...gaugeConfig
|
||||
};
|
||||
|
||||
combineGaugeConfig.detail.rich.value.color = colorValue;
|
||||
combineGaugeConfig.detail.rich.unit.color = colorValue;
|
||||
|
||||
return {
|
||||
title: {
|
||||
...titleConfig,
|
||||
text: title,
|
||||
textStyle: {
|
||||
fontSize: 12,
|
||||
color: chartColorMap.colorSecondary,
|
||||
fontWeight: 400
|
||||
},
|
||||
top: '0',
|
||||
left: 'center'
|
||||
},
|
||||
@@ -58,7 +67,7 @@ const GaugeChart: React.FC<Omit<ChartProps, 'seriesData' | 'xAxisData'>> = (
|
||||
color: 'transparent'
|
||||
},
|
||||
detail: {
|
||||
...gaugeItemConfig.detail,
|
||||
...combineGaugeConfig.detail,
|
||||
formatter: labelFormatter || gaugeItemConfig.detail.formatter
|
||||
},
|
||||
data: [{ value }]
|
||||
@@ -78,4 +87,4 @@ const GaugeChart: React.FC<Omit<ChartProps, 'seriesData' | 'xAxisData'>> = (
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(GaugeChart);
|
||||
export default GaugeChart;
|
||||
|
||||
@@ -1,10 +1,20 @@
|
||||
import OverlayScroller from '@/components/overlay-scroller';
|
||||
import useBodyScroll from '@/hooks/use-body-scroll';
|
||||
import useOverlayScroller from '@/hooks/use-overlay-scroller';
|
||||
import { Modal, type ModalProps } from 'antd';
|
||||
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 scroller = React.useRef<any>(null);
|
||||
const { saveScrollHeight, restoreScrollHeight } = useBodyScroll();
|
||||
const { initialize, destroyInstance } = useOverlayScroller();
|
||||
|
||||
React.useEffect(() => {
|
||||
if (props.open) {
|
||||
@@ -14,6 +24,24 @@ const ScrollerModal = (props: ModalProps & { maxContentHeight?: number }) => {
|
||||
}
|
||||
}, [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 (
|
||||
<Modal
|
||||
{...props}
|
||||
@@ -34,12 +62,16 @@ const ScrollerModal = (props: ModalProps & { maxContentHeight?: number }) => {
|
||||
}
|
||||
}}
|
||||
>
|
||||
<OverlayScroller
|
||||
style={{ paddingInline: 24, paddingBlockEnd: props.footer ? 0 : 32 }}
|
||||
maxHeight={props.maxContentHeight || 500}
|
||||
<Wrapper
|
||||
ref={scroller}
|
||||
data-overlayscrollbars-initialize
|
||||
className="overlay-scroller-wrapper"
|
||||
$maxHeight={props.maxContentHeight || 500}
|
||||
hidden={false}
|
||||
style={{ paddingInline: 24, paddingBlockEnd: props.footer ? 0 : 24 }}
|
||||
>
|
||||
{props.children}
|
||||
</OverlayScroller>
|
||||
</Wrapper>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,18 @@
|
||||
import useUserSettings from '@/hooks/use-user-settings';
|
||||
import { Tag, TagProps } from 'antd';
|
||||
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 }> = ({
|
||||
opacity,
|
||||
@@ -11,7 +23,7 @@ const ThemeTag: React.FC<TagProps & { opacity?: number }> = ({
|
||||
const { userSettings } = useUserSettings();
|
||||
const { isDarkTheme } = userSettings;
|
||||
return (
|
||||
<Tag
|
||||
<TagWrapper
|
||||
{...restProps}
|
||||
style={{
|
||||
...style,
|
||||
@@ -19,7 +31,7 @@ const ThemeTag: React.FC<TagProps & { opacity?: number }> = ({
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Tag>
|
||||
</TagWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -8,13 +8,14 @@ interface CardProps {
|
||||
children?: React.ReactNode;
|
||||
clickable?: boolean;
|
||||
ghost?: boolean;
|
||||
header?: React.ReactNode;
|
||||
footer?: React.ReactNode;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
const CardWrapper = styled.div`
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
padding: 16px 20px;
|
||||
flex-direction: column;
|
||||
justify-content: 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 {
|
||||
className,
|
||||
@@ -46,6 +53,8 @@ const Card: React.FC<CardProps> = (props) => {
|
||||
children,
|
||||
clickable = true,
|
||||
ghost = false,
|
||||
header,
|
||||
footer,
|
||||
onClick
|
||||
} = props;
|
||||
|
||||
@@ -58,7 +67,9 @@ const Card: React.FC<CardProps> = (props) => {
|
||||
style={{ height: height || '180px' }}
|
||||
onClick={onClick}
|
||||
>
|
||||
{children}
|
||||
{header}
|
||||
<CardContent>{children}</CardContent>
|
||||
{footer}
|
||||
</CardWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -55,5 +55,9 @@ export default function useBodyScroll() {
|
||||
};
|
||||
}, []);
|
||||
|
||||
return { saveScrollHeight, restoreScrollHeight };
|
||||
return {
|
||||
saveScrollHeight,
|
||||
restoreScrollHeight,
|
||||
bodyScroller: bodyScroller.current
|
||||
};
|
||||
}
|
||||
|
||||
@@ -199,6 +199,12 @@ export default function useOverlayScroller(data?: {
|
||||
[initialize, instance]
|
||||
);
|
||||
|
||||
const destroyInstance = () => {
|
||||
instanceRef.current?.destroy?.();
|
||||
removeWheelEvent();
|
||||
instanceRef.current = null;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
instanceRef.current?.destroy?.();
|
||||
@@ -212,6 +218,7 @@ export default function useOverlayScroller(data?: {
|
||||
scrollEventElement: scrollEventElement.current,
|
||||
initialized: initialized.current,
|
||||
generateInstance,
|
||||
destroyInstance: destroyInstance,
|
||||
updateScrollerPosition: throttledUpdateScrollerPosition,
|
||||
updateScrollerPositionToTop: updateScrollerPositionToTop
|
||||
};
|
||||
|
||||
@@ -44,8 +44,7 @@ export default {
|
||||
'resources.worker.add.step1':
|
||||
'Get Token <span class="note-text">(Run on the server)</span>',
|
||||
'resources.worker.add.step2': 'Register Worker',
|
||||
'resources.worker.add.step2.tips':
|
||||
'(Run on the worker to be added, <span class="bold-text">token</span> is the value obtained in the first step.)',
|
||||
'resources.worker.add.step2.tips': '(Run on the worker to be added.)',
|
||||
'resources.worker.add.step3':
|
||||
'After success, refresh the workers list to view the new worker.',
|
||||
'resources.worker.container.supported': 'Do not support macOS or Windows.',
|
||||
|
||||
@@ -44,8 +44,7 @@ export default {
|
||||
'resources.worker.add.step1':
|
||||
'トークンを取得 <span class="note-text">(サーバーで実行)</span>',
|
||||
'resources.worker.add.step2': 'ワーカーを登録',
|
||||
'resources.worker.add.step2.tips':
|
||||
'(追加するワーカーで実行し、<span class="bold-text">トークン</span> は最初のステップで取得した値です。)',
|
||||
'resources.worker.add.step2.tips': '(追加するワーカーで実行し。)',
|
||||
'resources.worker.add.step3':
|
||||
'成功後、ワーカーリストを更新して新しいワーカーを確認してください。',
|
||||
'resources.worker.container.supported':
|
||||
|
||||
@@ -44,8 +44,7 @@ export default {
|
||||
'resources.worker.add.step1':
|
||||
'Получить токен <span class="note-text">(Запустить на сервере)</span>',
|
||||
'resources.worker.add.step2': 'Зарегистрировать воркер',
|
||||
'resources.worker.add.step2.tips':
|
||||
'(Запустить на добавляемом воркере, <span class="bold-text">token</span> — это значение, полученное на первом шаге.)', // Translated
|
||||
'resources.worker.add.step2.tips': '(Запустить на добавляемом воркере.)', // Translated
|
||||
'resources.worker.add.step3':
|
||||
'После успешной регистрации обновите список воркеров.',
|
||||
'resources.worker.container.supported': 'Только для Linux.',
|
||||
|
||||
@@ -44,8 +44,7 @@ export default {
|
||||
'resources.worker.add.step1':
|
||||
'获取 Token<span class="note-text">(在 Server 上运行)</span>',
|
||||
'resources.worker.add.step2': '注册 Worker',
|
||||
'resources.worker.add.step2.tips':
|
||||
'(在需要添加的 Worker 上运行,<span class="bold-text">token</span> 为第一步获取到的值。)',
|
||||
'resources.worker.add.step2.tips': '(在需要添加的 Worker 上运行。)',
|
||||
'resources.worker.add.step3': '成功后,刷新 Worker 列表即可看到新的 Worker',
|
||||
'resources.worker.container.supported': '不支持 macOS 和 Windows',
|
||||
'resources.worker.current.version': '当前版本为 {version}',
|
||||
|
||||
@@ -25,7 +25,6 @@ import {
|
||||
updateCredential
|
||||
} from './apis';
|
||||
import AddCluster from './components/add-cluster';
|
||||
import AddPool from './components/add-pool';
|
||||
import ClusterItem from './components/cluster-item';
|
||||
import { ClusterDataList } from './config';
|
||||
import {
|
||||
@@ -146,7 +145,10 @@ const Credentials: React.FC = () => {
|
||||
});
|
||||
|
||||
const intl = useIntl();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [openAddWorker, setOpenAddWorker] = useState({
|
||||
open: false,
|
||||
token: ''
|
||||
});
|
||||
const [openAddModal, setOpenAddModal] = useState(false);
|
||||
const [provider, setProvider] = useState<string>('custom');
|
||||
const [action, setAction] = useState<PageActionType>(PageAction.CREATE);
|
||||
@@ -222,7 +224,7 @@ const Credentials: React.FC = () => {
|
||||
setOpenAddModal(false);
|
||||
};
|
||||
|
||||
const handleEditUser = (row: ListItem) => {
|
||||
const handleEditCluster = (row: ListItem) => {
|
||||
setCurrentData(row);
|
||||
setOpenAddModal(true);
|
||||
setAction(PageAction.EDIT);
|
||||
@@ -231,11 +233,14 @@ const Credentials: React.FC = () => {
|
||||
|
||||
const handleSelect = (val: any, row: ListItem) => {
|
||||
if (val === 'edit') {
|
||||
handleEditUser(row);
|
||||
handleEditCluster(row);
|
||||
} else if (val === 'delete') {
|
||||
handleDelete({ ...row, name: row.name });
|
||||
} else if (val === 'add_worker') {
|
||||
setOpen(true);
|
||||
setOpenAddWorker({
|
||||
open: true,
|
||||
token: '${token}'
|
||||
});
|
||||
setCurrentData(row);
|
||||
} else if (val === 'addPool') {
|
||||
handleAddPool(row.provider);
|
||||
@@ -308,29 +313,11 @@ const Credentials: React.FC = () => {
|
||||
onCancel={handleModalCancel}
|
||||
onOk={handleModalOk}
|
||||
></AddCluster>
|
||||
<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: PageAction.CREATE,
|
||||
title: '',
|
||||
provider: 'digitalocean'
|
||||
});
|
||||
}}
|
||||
></AddPool>
|
||||
<AddWorker open={open} onCancel={() => setOpen(false)}></AddWorker>
|
||||
<AddWorker
|
||||
open={openAddWorker.open}
|
||||
onCancel={() => setOpenAddWorker({ open: false, token: '' })}
|
||||
token={openAddWorker.token}
|
||||
></AddWorker>
|
||||
<DeleteModal ref={modalRef}></DeleteModal>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -2,15 +2,18 @@ import ModalFooter from '@/components/modal-footer';
|
||||
import ScrollerModal from '@/components/scroller-modal/index';
|
||||
import SealInput from '@/components/seal-form/seal-input';
|
||||
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 { Button, Form } from 'antd';
|
||||
import React from 'react';
|
||||
import { Form } from 'antd';
|
||||
import React, { useMemo } from 'react';
|
||||
import { ProviderValueMap } from '../config';
|
||||
import {
|
||||
ClusterFormData as FormData,
|
||||
ClusterListItem as ListItem
|
||||
} from '../config/types';
|
||||
import CloudProvider from './cloud-provider-form';
|
||||
import RegisterClusterInner from './resiter-cluster-inner';
|
||||
|
||||
type AddModalProps = {
|
||||
title: string;
|
||||
@@ -18,7 +21,6 @@ type AddModalProps = {
|
||||
open: boolean;
|
||||
provider: string; // 'kubernetes' | 'custom' | 'digitalocean';
|
||||
onOk: (values: FormData) => void;
|
||||
data?: ListItem;
|
||||
onCancel: () => void;
|
||||
};
|
||||
const AddCluster: React.FC<AddModalProps> = ({
|
||||
@@ -27,11 +29,14 @@ const AddCluster: React.FC<AddModalProps> = ({
|
||||
open,
|
||||
provider,
|
||||
onOk,
|
||||
data,
|
||||
onCancel
|
||||
}) => {
|
||||
const [form] = Form.useForm();
|
||||
const intl = useIntl();
|
||||
const [submissionStatus, setSubmissionStatus] = React.useState<{
|
||||
success: boolean;
|
||||
data: ListItem;
|
||||
}>({ success: true, data: {} as ListItem });
|
||||
|
||||
const handleSubmit = () => {
|
||||
form.submit();
|
||||
@@ -42,56 +47,98 @@ const AddCluster: React.FC<AddModalProps> = ({
|
||||
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 (
|
||||
<ScrollerModal
|
||||
title={
|
||||
<div className="flex-between flex-center">
|
||||
<span>{title}</span>
|
||||
<Button type="text" size="small" onClick={handleCancel}>
|
||||
<CloseOutlined></CloseOutlined>
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
title={modalTitle}
|
||||
open={open}
|
||||
onClose={onCancel}
|
||||
onCancel={handleCancel}
|
||||
destroyOnClose={true}
|
||||
closeIcon={false}
|
||||
closeIcon={true}
|
||||
maskClosable={false}
|
||||
keyboard={false}
|
||||
width={600}
|
||||
footer={
|
||||
<ModalFooter onOk={handleSubmit} onCancel={onCancel}></ModalFooter>
|
||||
}
|
||||
width={680}
|
||||
footer={renderFooter()}
|
||||
>
|
||||
<Form form={form} onFinish={onOk} preserve={false}>
|
||||
<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>
|
||||
)}
|
||||
<Form.Item<FormData> name="description" rules={[{ required: false }]}>
|
||||
<SealInput.TextArea
|
||||
label={intl.formatMessage({ id: 'common.table.description' })}
|
||||
></SealInput.TextArea>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
{submissionStatus.success ? (
|
||||
renderAddWorkerContent()
|
||||
) : (
|
||||
<Form form={form} onFinish={onOk} preserve={false}>
|
||||
<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>
|
||||
)}
|
||||
<Form.Item<FormData> name="description" rules={[{ required: false }]}>
|
||||
<SealInput.TextArea
|
||||
label={intl.formatMessage({ id: 'common.table.description' })}
|
||||
></SealInput.TextArea>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
)}
|
||||
</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 IconFont from '@/components/icon-font';
|
||||
import StatusTag from '@/components/status-tag';
|
||||
import ThemeTag from '@/components/tags-wrapper/theme-tag';
|
||||
import Card from '@/components/templates/card';
|
||||
import { PageAction } from '@/config';
|
||||
import {
|
||||
DeleteOutlined,
|
||||
EditOutlined,
|
||||
KubernetesOutlined
|
||||
} 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 styled from 'styled-components';
|
||||
import {
|
||||
@@ -18,65 +20,21 @@ import {
|
||||
ProviderValueMap
|
||||
} from '../config';
|
||||
import { ClusterListItem as ListItem, NodePoolListItem } from '../config/types';
|
||||
import AddPool from './add-pool';
|
||||
import RegisterCluster from './register-cluster';
|
||||
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 = [
|
||||
{
|
||||
key: 'edit',
|
||||
label: 'common.button.edit',
|
||||
icon: <EditOutlined />
|
||||
},
|
||||
{
|
||||
key: 'view',
|
||||
label: 'common.button.view',
|
||||
icon: <EditOutlined />
|
||||
},
|
||||
{
|
||||
key: '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`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -116,7 +128,7 @@ const Inner = styled.div`
|
||||
cursor: default;
|
||||
|
||||
.title {
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
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 {
|
||||
data: ListItem;
|
||||
onSelect?: (key: string, row: ListItem) => void;
|
||||
@@ -165,14 +209,51 @@ const gaugeConfig = {
|
||||
};
|
||||
|
||||
const CardItem: React.FC<CardProps> = (props) => {
|
||||
const chartHeight = 160;
|
||||
const { data, onSelect } = props;
|
||||
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) => {
|
||||
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);
|
||||
};
|
||||
|
||||
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(() => {
|
||||
return actionItems.filter((item) => {
|
||||
@@ -184,14 +265,82 @@ const CardItem: React.FC<CardProps> = (props) => {
|
||||
}, [data.provider]);
|
||||
|
||||
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>
|
||||
<div className="title">
|
||||
<span className="flex-center gap-8">
|
||||
<span className="text">{data.name}</span>
|
||||
<Tag style={{ borderRadius: 4 }}>
|
||||
{ProviderLabelMap[data.provider]}
|
||||
</Tag>
|
||||
<ThemeTag>{ProviderLabelMap[data.provider]}</ThemeTag>
|
||||
<StatusTag
|
||||
statusValue={{
|
||||
status: ClusterStatus[data.status],
|
||||
@@ -210,7 +359,7 @@ const CardItem: React.FC<CardProps> = (props) => {
|
||||
<CardBox>
|
||||
<CardWrapper bordered={false}>
|
||||
<div className="label">Workers</div>
|
||||
<div className="value">1</div>
|
||||
<div className="value">1/1</div>
|
||||
</CardWrapper>
|
||||
<CardWrapper bordered={false}>
|
||||
<div className="label">GPUs</div>
|
||||
@@ -221,64 +370,40 @@ const CardItem: React.FC<CardProps> = (props) => {
|
||||
<div className="value">2</div>
|
||||
</CardWrapper>
|
||||
</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>
|
||||
{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>
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,44 +1,21 @@
|
||||
import ModalFooter from '@/components/modal-footer';
|
||||
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 {
|
||||
ClusterFormData as FormData,
|
||||
ClusterListItem as ListItem
|
||||
} from '../config/types';
|
||||
import CloudProvider from './cloud-provider-form';
|
||||
import K8SProvider from './k8s-provider-form';
|
||||
import { ClusterListItem as ListItem } from '../config/types';
|
||||
import RegisterClusterInner from './resiter-cluster-inner';
|
||||
|
||||
type AddModalProps = {
|
||||
title: string;
|
||||
action: PageActionType;
|
||||
open: boolean;
|
||||
provider: string; // 'kubernetes' | 'custom' | 'digitalocean';
|
||||
onOk: (values: FormData) => void;
|
||||
data?: ListItem;
|
||||
data: ListItem;
|
||||
onCancel: () => void;
|
||||
};
|
||||
const AddCluster: React.FC<AddModalProps> = ({
|
||||
title,
|
||||
action,
|
||||
open,
|
||||
provider,
|
||||
onOk,
|
||||
data,
|
||||
onCancel
|
||||
}) => {
|
||||
const [form] = Form.useForm();
|
||||
const intl = useIntl();
|
||||
|
||||
const handleSubmit = () => {
|
||||
form.submit();
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
form.resetFields();
|
||||
onCancel();
|
||||
};
|
||||
|
||||
@@ -52,42 +29,9 @@ const AddCluster: React.FC<AddModalProps> = ({
|
||||
maskClosable={false}
|
||||
keyboard={false}
|
||||
width={600}
|
||||
footer={
|
||||
<ModalFooter onOk={handleSubmit} onCancel={onCancel}></ModalFooter>
|
||||
}
|
||||
footer={false}
|
||||
>
|
||||
<Form form={form} onFinish={onOk} preserve={false}>
|
||||
<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>
|
||||
<RegisterClusterInner data={data} />
|
||||
</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 = [
|
||||
{
|
||||
title: 'Type',
|
||||
dataIndex: 'type',
|
||||
key: 'type'
|
||||
title: 'Instance Type',
|
||||
dataIndex: 'instance_type',
|
||||
key: 'instance_type'
|
||||
},
|
||||
{
|
||||
title: 'Replicas',
|
||||
@@ -126,7 +126,7 @@ const WorkerPools: React.FC<WorkerPoolsProps> = ({
|
||||
const mockData = Array.from({ length: 3 }, (_, index) => ({
|
||||
id: index + 1,
|
||||
key: index,
|
||||
type: `Type ${index + 1}`,
|
||||
instance_type: `Type ${index + 1}`,
|
||||
replicas: Math.floor(Math.random() * 10) + 1,
|
||||
batchSize: Math.floor(Math.random() * 100) + 1,
|
||||
gpu: `NVIDIA 4090`,
|
||||
|
||||
@@ -59,3 +59,12 @@ export const ProviderLabelMap = {
|
||||
[ProviderValueMap.DigitalOcean]: 'Digital Ocean',
|
||||
[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}'`;
|
||||
};
|
||||
|
||||
@@ -20,8 +20,8 @@ const renderCardItem = (data: {
|
||||
className={styles['card-body']}
|
||||
>
|
||||
<div className={styles.content}>
|
||||
<div className="label font-500">{label}</div>
|
||||
<div className="value font-500">{value}</div>
|
||||
<div className="label text-secondary">{label}</div>
|
||||
<div className="value font-600 font-size-16">{value}</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -12,7 +12,7 @@ const GGUFResult: React.FC = () => {
|
||||
/>
|
||||
}
|
||||
title={false}
|
||||
subTitle="GGUF model is not supported yet."
|
||||
subTitle="GGUF model is not supported."
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,64 +1,17 @@
|
||||
import ScrollerModal from '@/components/scroller-modal';
|
||||
import {
|
||||
AppleOutlined,
|
||||
LinuxOutlined,
|
||||
WindowsOutlined
|
||||
} from '@ant-design/icons';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { TabsProps } from 'antd';
|
||||
import React from 'react';
|
||||
import MacOS from './add-worker-macos';
|
||||
import ContainerInstall from './container-install';
|
||||
|
||||
type ViewModalProps = {
|
||||
open: boolean;
|
||||
token: string;
|
||||
onCancel: () => void;
|
||||
};
|
||||
|
||||
const AddWorker: React.FC<ViewModalProps> = (props) => {
|
||||
const { open, onCancel } = props || {};
|
||||
const { open, onCancel, token = '${token}' } = props || {};
|
||||
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 (
|
||||
<ScrollerModal
|
||||
|
||||
@@ -42,7 +42,7 @@ const AddWorker: React.FC<ViewModalProps> = (props) => {
|
||||
return commandCode?.registerWorker({
|
||||
server: origin,
|
||||
tag: tag,
|
||||
token: '${token}',
|
||||
token: props.token || '${token}',
|
||||
workerip: '${workerip}'
|
||||
});
|
||||
}, [versionInfo, activeKey, props.token, npuKey]);
|
||||
@@ -53,38 +53,8 @@ const AddWorker: React.FC<ViewModalProps> = (props) => {
|
||||
|
||||
return (
|
||||
<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">
|
||||
2. {intl.formatMessage({ id: 'resources.worker.add.step2' })}{' '}
|
||||
1. {intl.formatMessage({ id: 'resources.worker.add.step2' })}{' '}
|
||||
<span
|
||||
className="font-size-12"
|
||||
style={{ color: 'var(--ant-color-text-tertiary)' }}
|
||||
@@ -146,7 +116,7 @@ const AddWorker: React.FC<ViewModalProps> = (props) => {
|
||||
></HighlightCode>
|
||||
)}
|
||||
<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>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -22,14 +22,14 @@ export const addWorkerGuide: Record<string, any> = {
|
||||
mac: {
|
||||
getToken: 'cat /var/lib/gpustack/token',
|
||||
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: {
|
||||
getToken:
|
||||
'Get-Content -Path (Join-Path -Path $env:APPDATA -ChildPath "gpustack\\token") -Raw',
|
||||
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: {
|
||||
@@ -41,11 +41,14 @@ export const addWorkerGuide: Record<string, any> = {
|
||||
token: string;
|
||||
workerip: string;
|
||||
}) {
|
||||
return `docker run -d \\
|
||||
--net=host \\
|
||||
-v /var/lib/gpustack:/var/lib/gpustack \\
|
||||
--privileged gpustack/gpustack:xxxx \\
|
||||
--registration ${params.token}`;
|
||||
return `docker run -d --name gpustack \\
|
||||
--restart=unless-stopped \\
|
||||
--gpus all \\
|
||||
--network=host \\
|
||||
--ipc=host \\
|
||||
-v gpustack-data:/var/lib/gpustack \\
|
||||
gpustack/gpustack:${params.tag} \\
|
||||
--server-url ${params.server} --registration ${params.token} --worker-ip ${params.workerip}`;
|
||||
}
|
||||
},
|
||||
npu: {
|
||||
@@ -73,7 +76,7 @@ export const addWorkerGuide: Record<string, any> = {
|
||||
--ipc=host \\
|
||||
-v gpustack-data:/var/lib/gpustack \\
|
||||
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: {
|
||||
@@ -101,7 +104,7 @@ export const addWorkerGuide: Record<string, any> = {
|
||||
--ipc=host \\
|
||||
-v gpustack-data:/var/lib/gpustack \\
|
||||
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: {
|
||||
@@ -119,7 +122,7 @@ export const addWorkerGuide: Record<string, any> = {
|
||||
--ipc=host \\
|
||||
-v gpustack-data:/var/lib/gpustack \\
|
||||
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: {
|
||||
@@ -136,7 +139,7 @@ export const addWorkerGuide: Record<string, any> = {
|
||||
--network=host \\
|
||||
-v gpustack-data:/var/lib/gpustack \\
|
||||
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: {
|
||||
@@ -157,7 +160,7 @@ export const addWorkerGuide: Record<string, any> = {
|
||||
--security-opt seccomp=unconfined \\
|
||||
-v gpustack-data:/var/lib/gpustack \\
|
||||
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: {
|
||||
@@ -180,7 +183,7 @@ export const addWorkerGuide: Record<string, any> = {
|
||||
--security-opt seccomp=unconfined \\
|
||||
-v gpustack-data:/var/lib/gpustack \\
|
||||
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: {
|
||||
@@ -201,7 +204,7 @@ export const addWorkerGuide: Record<string, any> = {
|
||||
--ipc=host \\
|
||||
-v gpustack-data:/var/lib/gpustack \\
|
||||
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: {
|
||||
@@ -216,8 +219,8 @@ export const containerInstallOptions = [
|
||||
{ label: 'Ascend CANN', value: 'npu' },
|
||||
{ label: 'Hygon DTK', value: 'dcu' },
|
||||
{ label: 'Moore Threads MUSA', value: 'musa' },
|
||||
{ label: 'Iluvatar Corex', value: 'corex' },
|
||||
{ label: 'CPU', value: 'cpu' }
|
||||
{ label: 'Iluvatar Corex', value: 'corex' }
|
||||
// { label: 'CPU', value: 'cpu' }
|
||||
];
|
||||
|
||||
export const ModelfileStateMap = {
|
||||
|
||||
Reference in New Issue
Block a user