fix: cluster value setting, volumes setting
This commit is contained in:
@@ -5,6 +5,7 @@ import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { ProviderType, ProviderValueMap } from '../config';
|
||||
import AddWorkerCommand from './add-worker-command';
|
||||
import CheckEnvCommand from './check-env-command';
|
||||
import RegisterClusterInner from './register-cluster-inner';
|
||||
import SupportedGPUs from './support-gpus';
|
||||
|
||||
@@ -62,8 +63,8 @@ const AddWorkerStep: React.FC<AddModalProps> = ({
|
||||
registrationInfo
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const [currentSelection, setCurrentSelection] =
|
||||
React.useState<string>('cuda');
|
||||
const [currentGPU, setCurrentGPU] = React.useState<string>('cuda');
|
||||
React.useState<string>('cuda');
|
||||
const [workerCommand, setWorkerCommand] = React.useState<Record<string, any>>(
|
||||
{
|
||||
label: 'NVIDIA',
|
||||
@@ -73,7 +74,7 @@ const AddWorkerStep: React.FC<AddModalProps> = ({
|
||||
|
||||
const handleSelectProvider = (value: string, item: any) => {
|
||||
if (provider !== ProviderValueMap.Docker) return;
|
||||
setCurrentSelection(value);
|
||||
setCurrentGPU(value);
|
||||
setWorkerCommand(item);
|
||||
};
|
||||
|
||||
@@ -84,31 +85,35 @@ const AddWorkerStep: React.FC<AddModalProps> = ({
|
||||
</Title>
|
||||
<SupportedGPUs
|
||||
onSelect={handleSelectProvider}
|
||||
current={provider === ProviderValueMap.Docker ? currentSelection : ''}
|
||||
clickable={provider === ProviderValueMap.Docker}
|
||||
current={currentGPU}
|
||||
clickable={true}
|
||||
/>
|
||||
{provider === ProviderValueMap.Docker && (
|
||||
<Content>
|
||||
<Line></Line>
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
icon={<BulbOutlined />}
|
||||
message={
|
||||
<span
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: intl.formatMessage(
|
||||
{ id: 'clusters.create.addworker.tips' },
|
||||
{ label: workerCommand.label, link: workerCommand.link }
|
||||
)
|
||||
}}
|
||||
></span>
|
||||
}
|
||||
></Alert>
|
||||
</Content>
|
||||
)}
|
||||
|
||||
<Content>
|
||||
<Line></Line>
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
icon={<BulbOutlined />}
|
||||
message={
|
||||
<span
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: intl.formatMessage(
|
||||
{ id: 'clusters.create.addworker.tips' },
|
||||
{ label: workerCommand.label, link: workerCommand.link }
|
||||
)
|
||||
}}
|
||||
></span>
|
||||
}
|
||||
></Alert>
|
||||
</Content>
|
||||
|
||||
<div className="command-info">
|
||||
{intl.formatMessage({ id: 'clusters.create.addCommand.tips' })}
|
||||
1. {intl.formatMessage({ id: 'cluster.create.checkEnv.tips' })}
|
||||
</div>
|
||||
<CheckEnvCommand provider={provider} currentGPU={currentGPU} />
|
||||
<div className="command-info">
|
||||
2. {intl.formatMessage({ id: 'clusters.create.addCommand.tips' })}
|
||||
</div>
|
||||
{provider === ProviderValueMap.Kubernetes ? (
|
||||
<RegisterClusterInner registrationInfo={registrationInfo} />
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import HighlightCode from '@/components/highlight-code';
|
||||
import { addWorkerGuide } from '@/pages/resources/config';
|
||||
import React from 'react';
|
||||
import { ProviderType } from '../config';
|
||||
|
||||
type ViewModalProps = {
|
||||
provider: ProviderType;
|
||||
currentGPU: string;
|
||||
};
|
||||
|
||||
const AddWorkerCommand: React.FC<ViewModalProps> = ({
|
||||
provider = '',
|
||||
currentGPU
|
||||
}) => {
|
||||
const code = React.useMemo(() => {
|
||||
const command = addWorkerGuide['cuda'];
|
||||
return command.checkEnvCommand[provider || ''];
|
||||
}, [provider]);
|
||||
|
||||
return (
|
||||
<HighlightCode
|
||||
theme="dark"
|
||||
code={code}
|
||||
copyValue={code}
|
||||
lang="bash"
|
||||
></HighlightCode>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddWorkerCommand;
|
||||
@@ -0,0 +1,390 @@
|
||||
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 { Card as ACard, Col, Collapse, Row } from 'antd';
|
||||
import React, { useMemo } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { queryClusterToken } from '../apis';
|
||||
import {
|
||||
ClusterStatus,
|
||||
ClusterStatusLabelMap,
|
||||
ProviderLabelMap,
|
||||
ProviderValueMap,
|
||||
clusterActionList
|
||||
} 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 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;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
cursor: default;
|
||||
|
||||
.title {
|
||||
margin-bottom: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-bottom: 8px;
|
||||
|
||||
.text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: var(--font-size-middle);
|
||||
font-weight: 500;
|
||||
color: var(--ant-color-text);
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
flex: 1;
|
||||
}
|
||||
`;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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 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: ProviderValueMap.DigitalOcean
|
||||
});
|
||||
const [registerClusterStatus, setRegisterClusterStatus] = React.useState<{
|
||||
open: boolean;
|
||||
registrationInfo: {
|
||||
token: string;
|
||||
image: string;
|
||||
server_url: string;
|
||||
cluster_id: number;
|
||||
};
|
||||
}>({
|
||||
open: false,
|
||||
registrationInfo: {
|
||||
token: '',
|
||||
image: '',
|
||||
server_url: '',
|
||||
cluster_id: 0
|
||||
}
|
||||
});
|
||||
|
||||
const handleRegisterCluster = async () => {
|
||||
try {
|
||||
const info = await queryClusterToken({ id: data.id });
|
||||
setRegisterClusterStatus({
|
||||
open: true,
|
||||
registrationInfo: {
|
||||
...info,
|
||||
cluster_id: data.id
|
||||
}
|
||||
});
|
||||
} catch (error) {}
|
||||
};
|
||||
|
||||
// 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') {
|
||||
handleRegisterCluster();
|
||||
return;
|
||||
}
|
||||
onSelect?.(key, data);
|
||||
};
|
||||
|
||||
// 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 clusterActionList.filter((item) => {
|
||||
if (item.provider) {
|
||||
return item.provider === data.provider;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}, [data.provider]);
|
||||
|
||||
return (
|
||||
<Card
|
||||
height={'auto'}
|
||||
clickable={false}
|
||||
ghost
|
||||
footerHolder={
|
||||
<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>
|
||||
<ThemeTag color="purple">
|
||||
{ProviderLabelMap[data.provider]}
|
||||
</ThemeTag>
|
||||
<StatusTag
|
||||
statusValue={{
|
||||
status: ClusterStatus[data.state],
|
||||
text: ClusterStatusLabelMap[data.state]
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
<span>
|
||||
<DropdownButtons
|
||||
items={actions}
|
||||
onSelect={handleOnSelect}
|
||||
></DropdownButtons>
|
||||
</span>
|
||||
</div>
|
||||
<Content>
|
||||
<CardBox>
|
||||
<CardWrapper bordered={false}>
|
||||
<div className="label">Workers</div>
|
||||
<div className="value">
|
||||
{data.ready_workers} / {data.workers}
|
||||
</div>
|
||||
</CardWrapper>
|
||||
<CardWrapper bordered={false}>
|
||||
<div className="label">GPUs</div>
|
||||
<div className="value">{data.gpus}</div>
|
||||
</CardWrapper>
|
||||
<CardWrapper bordered={false}>
|
||||
<div className="label">Deployments</div>
|
||||
<div className="value">{data.models}</div>
|
||||
</CardWrapper>
|
||||
</CardBox>
|
||||
</Content>
|
||||
</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}
|
||||
registrationInfo={registerClusterStatus.registrationInfo}
|
||||
onCancel={() => {
|
||||
setRegisterClusterStatus({
|
||||
open: false,
|
||||
registrationInfo: {
|
||||
token: '',
|
||||
image: '',
|
||||
server_url: '',
|
||||
cluster_id: 0
|
||||
}
|
||||
});
|
||||
}}
|
||||
></RegisterCluster>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default CardItem;
|
||||
@@ -205,8 +205,6 @@ const PoolForm: React.FC<AddModalProps> = forwardRef((props, ref) => {
|
||||
value: string | number;
|
||||
}) => {
|
||||
if (action === PageAction.EDIT) {
|
||||
console.log('imageLabelRender========::', action, currentData, data);
|
||||
// return currentData?.image_name || currentData?.os_image;
|
||||
const vendor = _.split(currentData?.image_name || '', ' ')[0];
|
||||
const iconType = _.get(vendorIconMap, vendor.toLowerCase());
|
||||
return (
|
||||
@@ -217,7 +215,6 @@ const PoolForm: React.FC<AddModalProps> = forwardRef((props, ref) => {
|
||||
);
|
||||
}
|
||||
const selectImage = osImageList.find((item) => item.value === data.value);
|
||||
console.log('imageLabelRender========::', selectImage);
|
||||
if (selectImage) {
|
||||
return (
|
||||
<RenderLabel label={data.label} vendor={selectImage.vendor || ''} />
|
||||
@@ -272,14 +269,6 @@ const PoolForm: React.FC<AddModalProps> = forwardRef((props, ref) => {
|
||||
);
|
||||
};
|
||||
|
||||
const filterImageOption = (inputValue: string, option: any) => {
|
||||
return (
|
||||
option.label.toLowerCase().includes(inputValue.toLowerCase()) ||
|
||||
option.value.toLowerCase().includes(inputValue.toLowerCase()) ||
|
||||
option.description.toLowerCase().includes(inputValue.toLowerCase())
|
||||
);
|
||||
};
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
resetFields: () => {
|
||||
form.resetFields();
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import ListMap from '@/components/dynamic-form/components/list-map';
|
||||
import { statusType } from '@/components/dynamic-form/config/types';
|
||||
import useValidateFields from '@/components/dynamic-form/hooks/use-validate-fields';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Form } from 'antd';
|
||||
import React, { forwardRef } from 'react';
|
||||
import React, { forwardRef, useState } from 'react';
|
||||
import { fieldConfig } from '../config/cloud-options-config';
|
||||
|
||||
const volumeOptions = fieldConfig.volumes;
|
||||
@@ -14,21 +16,56 @@ const CloudOptions: React.FC<{
|
||||
const intl = useIntl();
|
||||
const form = Form.useFormInstance();
|
||||
const volumes = Form.useWatch(['cloud_options', volumeOptions.name], form);
|
||||
const [validateStatusList, setValidateStatusList] = useState<
|
||||
{ [key: string]: statusType }[]
|
||||
>([]);
|
||||
|
||||
const handleOnChange = (name: string, value: any) => {
|
||||
console.log('handleOnChange========', name, value);
|
||||
form.setFieldValue(['cloud_options', name], value);
|
||||
const updateValidateStatusList = (list: { [key: string]: statusType }[]) => {
|
||||
setValidateStatusList(list);
|
||||
};
|
||||
|
||||
const { listMapValidator, toggleValidation } = useValidateFields({
|
||||
requiredFields: volumeOptions.required,
|
||||
setValidateStatusList: updateValidateStatusList
|
||||
});
|
||||
|
||||
const handleOnChange = (value: any) => {
|
||||
toggleValidation(true);
|
||||
form.setFieldValue(['cloud_options', volumeOptions.name], value);
|
||||
};
|
||||
|
||||
const handleOnAdd = (data: any[]) => {
|
||||
toggleValidation(false);
|
||||
form.setFieldValue(['cloud_options', volumeOptions.name], data);
|
||||
toggleValidation(true);
|
||||
};
|
||||
|
||||
const handleOnDelete = (deletedItem: any, data: any[]) => {
|
||||
toggleValidation(false);
|
||||
form.setFieldValue(['cloud_options', volumeOptions.name], data);
|
||||
toggleValidation(true);
|
||||
};
|
||||
console.log('disabled=====', disabled);
|
||||
return (
|
||||
<Form.Item name={['cloud_options', volumeOptions.name as string]}>
|
||||
<Form.Item
|
||||
name={['cloud_options', volumeOptions.name as string]}
|
||||
rules={[
|
||||
{
|
||||
validator: listMapValidator
|
||||
}
|
||||
]}
|
||||
>
|
||||
<ListMap
|
||||
validateStatusList={validateStatusList}
|
||||
btnText={intl.formatMessage({ id: 'clusters.workerpool.volumes.add' })}
|
||||
label={intl.formatMessage({ id: 'clusters.workerpool.volumes' })}
|
||||
dataList={volumes || []}
|
||||
properties={volumeOptions.properties || {}}
|
||||
requiredFields={volumeOptions.required || []}
|
||||
disabled={disabled}
|
||||
onChange={(value) => handleOnChange(volumeOptions.name, value)}
|
||||
onAdd={handleOnAdd}
|
||||
onDelete={handleOnDelete}
|
||||
onChange={handleOnChange}
|
||||
/>
|
||||
</Form.Item>
|
||||
);
|
||||
|
||||
@@ -21,6 +21,7 @@ export const fieldConfig: Record<string, FieldSchema> = {
|
||||
type: 'array',
|
||||
title: 'Volumes',
|
||||
name: 'volumes',
|
||||
required: ['name', 'size_gb', 'format'],
|
||||
properties: {
|
||||
name: {
|
||||
name: 'name',
|
||||
|
||||
Reference in New Issue
Block a user