feat: cloud options
This commit is contained in:
@@ -251,14 +251,14 @@ const Credentials: React.FC = () => {
|
||||
data: formdata,
|
||||
clusterId: addPoolStatus.clusterId
|
||||
});
|
||||
setAddPoolStatus({
|
||||
...addPoolStatus,
|
||||
open: false
|
||||
});
|
||||
message.success(intl.formatMessage({ id: 'common.message.success' }));
|
||||
} catch (error) {
|
||||
// error
|
||||
}
|
||||
setAddPoolStatus({
|
||||
...addPoolStatus,
|
||||
open: false
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,19 +1,12 @@
|
||||
import LabelSelector from '@/components/label-selector';
|
||||
import ModalFooter from '@/components/modal-footer';
|
||||
import ScrollerModal from '@/components/scroller-modal';
|
||||
import SealInputNumber from '@/components/seal-form/input-number';
|
||||
import SealInput from '@/components/seal-form/seal-input';
|
||||
import { PageAction } from '@/config';
|
||||
import { PageActionType } from '@/config/types';
|
||||
import useAppUtils from '@/hooks/use-app-utils';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Form } from 'antd';
|
||||
import _ from 'lodash';
|
||||
import React, { useEffect } from 'react';
|
||||
import React, { useRef } from 'react';
|
||||
import {
|
||||
NodePoolFormData as FormData,
|
||||
NodePoolListItem as ListItem
|
||||
} from '../config/types';
|
||||
import PoolForm from './pool-form';
|
||||
|
||||
type AddModalProps = {
|
||||
title: string;
|
||||
@@ -24,7 +17,7 @@ type AddModalProps = {
|
||||
onOk: (values: FormData) => void;
|
||||
onCancel: () => void;
|
||||
};
|
||||
const AddCluster: React.FC<AddModalProps> = ({
|
||||
const AddPool: React.FC<AddModalProps> = ({
|
||||
title,
|
||||
action,
|
||||
open,
|
||||
@@ -33,45 +26,21 @@ const AddCluster: React.FC<AddModalProps> = ({
|
||||
currentData,
|
||||
onCancel
|
||||
}) => {
|
||||
const [form] = Form.useForm();
|
||||
const intl = useIntl();
|
||||
const { getRuleMessage } = useAppUtils();
|
||||
const formRef = useRef<any>(null);
|
||||
|
||||
const handleSumit = () => {
|
||||
form.submit();
|
||||
const handleSubmit = () => {
|
||||
formRef.current?.submit?.();
|
||||
};
|
||||
|
||||
const handleOnOk = async (data: FormData) => {
|
||||
const { volumes, ...rest } = data;
|
||||
|
||||
await onOk({
|
||||
...rest,
|
||||
cloud_options: !_.isEmpty(volumes)
|
||||
? {
|
||||
volumes: [
|
||||
{
|
||||
...volumes
|
||||
}
|
||||
]
|
||||
}
|
||||
: {}
|
||||
});
|
||||
const handleOnFinish = async (data: FormData) => {
|
||||
onOk(data);
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
form.resetFields();
|
||||
formRef.current?.reset?.();
|
||||
onCancel();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (currentData) {
|
||||
form.setFieldsValue({
|
||||
...currentData,
|
||||
volumes: currentData.cloud_options?.volumes?.[0] || {}
|
||||
});
|
||||
}
|
||||
}, [currentData]);
|
||||
|
||||
return (
|
||||
<ScrollerModal
|
||||
title={title}
|
||||
@@ -83,144 +52,18 @@ const AddCluster: React.FC<AddModalProps> = ({
|
||||
keyboard={false}
|
||||
width={600}
|
||||
footer={
|
||||
<ModalFooter onOk={handleSumit} onCancel={onCancel}></ModalFooter>
|
||||
<ModalFooter onOk={handleSubmit} onCancel={onCancel}></ModalFooter>
|
||||
}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
onFinish={handleOnOk}
|
||||
preserve={false}
|
||||
initialValues={{
|
||||
replicas: 1,
|
||||
batch_size: 1
|
||||
}}
|
||||
>
|
||||
<Form.Item<FormData>
|
||||
name="instance_type"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: getRuleMessage(
|
||||
'input',
|
||||
'clusters.workerpool.instanceType'
|
||||
)
|
||||
}
|
||||
]}
|
||||
>
|
||||
<SealInput.Input
|
||||
label={intl.formatMessage({
|
||||
id: 'clusters.workerpool.instanceType'
|
||||
})}
|
||||
required
|
||||
></SealInput.Input>
|
||||
</Form.Item>
|
||||
<Form.Item<FormData>
|
||||
name="replicas"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: getRuleMessage('input', 'clusters.workerpool.replicas')
|
||||
}
|
||||
]}
|
||||
>
|
||||
<SealInputNumber
|
||||
label={intl.formatMessage({ id: 'clusters.workerpool.replicas' })}
|
||||
required
|
||||
></SealInputNumber>
|
||||
</Form.Item>
|
||||
<Form.Item<FormData>
|
||||
name="batch_size"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: getRuleMessage('input', 'clusters.workerpool.batchSize')
|
||||
}
|
||||
]}
|
||||
>
|
||||
<SealInputNumber
|
||||
label={intl.formatMessage({ id: 'clusters.workerpool.batchSize' })}
|
||||
required
|
||||
></SealInputNumber>
|
||||
</Form.Item>
|
||||
<Form.Item<FormData>
|
||||
name="os_image"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: getRuleMessage('input', 'clusters.workerpool.osImage')
|
||||
}
|
||||
]}
|
||||
>
|
||||
<SealInput.Input
|
||||
disabled={action === PageAction.EDIT}
|
||||
label={intl.formatMessage({ id: 'clusters.workerpool.osImage' })}
|
||||
required
|
||||
></SealInput.Input>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item<FormData>
|
||||
name="labels"
|
||||
rules={[
|
||||
({ getFieldValue }) => ({
|
||||
validator(rule, value) {
|
||||
if (_.keys(value).length > 0) {
|
||||
if (_.some(_.keys(value), (k: string) => !value[k])) {
|
||||
return Promise.reject(
|
||||
intl.formatMessage(
|
||||
{
|
||||
id: 'common.validate.value'
|
||||
},
|
||||
{
|
||||
name: 'labels'
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
})
|
||||
]}
|
||||
>
|
||||
<LabelSelector
|
||||
label={intl.formatMessage({ id: 'resources.table.labels' })}
|
||||
labels={form.getFieldValue('labels') || {}}
|
||||
btnText={intl.formatMessage({ id: 'common.button.addLabel' })}
|
||||
></LabelSelector>
|
||||
</Form.Item>
|
||||
<Form.Item<FormData>
|
||||
name="volumes"
|
||||
rules={[
|
||||
({ getFieldValue }) => ({
|
||||
validator(rule, value) {
|
||||
if (_.keys(value).length > 0) {
|
||||
if (_.some(_.keys(value), (k: string) => !value[k])) {
|
||||
return Promise.reject(
|
||||
intl.formatMessage(
|
||||
{
|
||||
id: 'common.validate.value'
|
||||
},
|
||||
{
|
||||
name: 'Volumes'
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
})
|
||||
]}
|
||||
>
|
||||
<LabelSelector
|
||||
label="Volumes"
|
||||
labels={form.getFieldValue('volumes') || {}}
|
||||
btnText="Add Option"
|
||||
></LabelSelector>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
<PoolForm
|
||||
ref={formRef}
|
||||
action={action}
|
||||
provider={provider}
|
||||
currentData={currentData}
|
||||
onFinish={handleOnFinish}
|
||||
></PoolForm>
|
||||
</ScrollerModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddCluster;
|
||||
export default AddPool;
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
import DropDownActions from '@/components/drop-down-actions';
|
||||
import ListMap from '@/components/dynamic-form/components/list-map';
|
||||
import { FieldSchema } from '@/components/dynamic-form/config/types';
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import { useMemoizedFn } from 'ahooks';
|
||||
import { Button, Form } from 'antd';
|
||||
import _ from 'lodash';
|
||||
import React, { forwardRef, useImperativeHandle, useMemo } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { CloudOptionItems } from '../config';
|
||||
import { fieldConfig } from '../config/cloud-options-config';
|
||||
|
||||
const Title = styled.div`
|
||||
display: flex;
|
||||
height: 40px;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
// background-color: var(--ant-color-fill-secondary);
|
||||
border-radius: var(--ant-border-radius);
|
||||
margin-bottom: 22px;
|
||||
font-weight: 600;
|
||||
`;
|
||||
|
||||
const ButtonWrapper = styled.span`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
font-weight: 400;
|
||||
cursor: pointer;
|
||||
gap: 8px;
|
||||
&:hover {
|
||||
color: var(--ant-color-text-secondary);
|
||||
}
|
||||
`;
|
||||
|
||||
const CloudOptions: React.FC<{
|
||||
ref?: any;
|
||||
}> = forwardRef((props, ref) => {
|
||||
// form instance
|
||||
const form = Form.useFormInstance();
|
||||
const [selectedOptions, setSelectedOptions] = React.useState<Set<string>>(
|
||||
new Set()
|
||||
);
|
||||
const [fieldList, setFieldList] = React.useState<FieldSchema[]>([]);
|
||||
|
||||
const items = useMemo(() => {
|
||||
return CloudOptionItems.map((item) => ({
|
||||
...item,
|
||||
disabled: selectedOptions.has(item.key)
|
||||
}));
|
||||
}, [selectedOptions]);
|
||||
|
||||
const handleAddOption = useMemoizedFn((item: { key: string }) => {
|
||||
const field = fieldConfig[item.key];
|
||||
setFieldList((prev) => [...prev, { ...field, name: item.key }]);
|
||||
setSelectedOptions((prev) => new Set(prev).add(item.key));
|
||||
});
|
||||
|
||||
const menu = useMemo(() => {
|
||||
return {
|
||||
items: items,
|
||||
onClick: handleAddOption
|
||||
};
|
||||
}, [items, handleAddOption]);
|
||||
|
||||
const handleOnChange = (name: string, value: any) => {
|
||||
form.setFieldValue(['cloud_options', name], value);
|
||||
if (_.isEmpty(value)) {
|
||||
setFieldList((prev) => prev.filter((field) => field.name !== name));
|
||||
setSelectedOptions((prev) => {
|
||||
const newSelected = new Set(prev);
|
||||
newSelected.delete(name);
|
||||
return newSelected;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// init field list by form data
|
||||
const initFieldList = () => {
|
||||
const cloudOptions = form.getFieldValue('cloud_options');
|
||||
if (cloudOptions) {
|
||||
const fields = Object.entries(cloudOptions)
|
||||
.filter(([, value]) => {
|
||||
return !_.isEmpty(value);
|
||||
})
|
||||
.map(([key, value]) => {
|
||||
const field = fieldConfig[key];
|
||||
return { ...field, name: key };
|
||||
});
|
||||
setFieldList(fields);
|
||||
setSelectedOptions(new Set(Object.keys(cloudOptions)));
|
||||
}
|
||||
};
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
initFieldList
|
||||
}));
|
||||
|
||||
return (
|
||||
<>
|
||||
<Title>
|
||||
<DropDownActions menu={menu}>
|
||||
<Button variant="filled" color="default">
|
||||
<PlusOutlined />
|
||||
<span>Add Cloud Options</span>
|
||||
</Button>
|
||||
</DropDownActions>
|
||||
</Title>
|
||||
{fieldList.length > 0 &&
|
||||
fieldList.map((field) => (
|
||||
<Form.Item
|
||||
key={field.name}
|
||||
name={['cloud_options', field.name as string]}
|
||||
>
|
||||
<ListMap
|
||||
btnText={'Add Item'}
|
||||
label={field.title}
|
||||
dataList={form.getFieldValue(['cloud_options', field.name]) || []}
|
||||
properties={field.properties || {}}
|
||||
onChange={(value) => handleOnChange(field.name, value)}
|
||||
/>
|
||||
</Form.Item>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export default CloudOptions;
|
||||
@@ -0,0 +1,167 @@
|
||||
import LabelSelector from '@/components/label-selector';
|
||||
import SealInputNumber from '@/components/seal-form/input-number';
|
||||
import SealInput from '@/components/seal-form/seal-input';
|
||||
import { PageAction } from '@/config';
|
||||
import { PageActionType } from '@/config/types';
|
||||
import useAppUtils from '@/hooks/use-app-utils';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Form } from 'antd';
|
||||
import _ from 'lodash';
|
||||
import React, {
|
||||
forwardRef,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useRef
|
||||
} from 'react';
|
||||
import {
|
||||
NodePoolFormData as FormData,
|
||||
NodePoolListItem as ListItem
|
||||
} from '../config/types';
|
||||
import CloudOptions from './cloud-options';
|
||||
|
||||
type AddModalProps = {
|
||||
ref: any;
|
||||
action: PageActionType;
|
||||
provider: string; // 'kubernetes' | 'custom' | 'digitalocean';
|
||||
currentData?: ListItem | null;
|
||||
onFinish: (values: FormData) => void;
|
||||
};
|
||||
const PoolForm: React.FC<AddModalProps> = forwardRef(
|
||||
({ action, onFinish, currentData }, ref) => {
|
||||
const cloudOptionsRef = useRef<any>(null);
|
||||
const [form] = Form.useForm();
|
||||
const intl = useIntl();
|
||||
const { getRuleMessage } = useAppUtils();
|
||||
|
||||
useEffect(() => {
|
||||
if (currentData) {
|
||||
form.setFieldsValue({
|
||||
...currentData,
|
||||
volumes: currentData.cloud_options?.volumes?.[0] || {}
|
||||
});
|
||||
cloudOptionsRef.current?.initFieldList();
|
||||
}
|
||||
}, [currentData]);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
reset: () => {
|
||||
form.resetFields();
|
||||
},
|
||||
submit: () => {
|
||||
form.submit();
|
||||
},
|
||||
validateFields: async () => {
|
||||
return await form.validateFields();
|
||||
}
|
||||
}));
|
||||
|
||||
return (
|
||||
<Form
|
||||
form={form}
|
||||
onFinish={onFinish}
|
||||
preserve={false}
|
||||
initialValues={{
|
||||
replicas: 1,
|
||||
batch_size: 1
|
||||
}}
|
||||
>
|
||||
<Form.Item<FormData>
|
||||
name="instance_type"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: getRuleMessage(
|
||||
'input',
|
||||
'clusters.workerpool.instanceType'
|
||||
)
|
||||
}
|
||||
]}
|
||||
>
|
||||
<SealInput.Input
|
||||
label={intl.formatMessage({
|
||||
id: 'clusters.workerpool.instanceType'
|
||||
})}
|
||||
required
|
||||
></SealInput.Input>
|
||||
</Form.Item>
|
||||
<Form.Item<FormData>
|
||||
name="replicas"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: getRuleMessage('input', 'clusters.workerpool.replicas')
|
||||
}
|
||||
]}
|
||||
>
|
||||
<SealInputNumber
|
||||
label={intl.formatMessage({ id: 'clusters.workerpool.replicas' })}
|
||||
required
|
||||
></SealInputNumber>
|
||||
</Form.Item>
|
||||
<Form.Item<FormData>
|
||||
name="batch_size"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: getRuleMessage('input', 'clusters.workerpool.batchSize')
|
||||
}
|
||||
]}
|
||||
>
|
||||
<SealInputNumber
|
||||
label={intl.formatMessage({ id: 'clusters.workerpool.batchSize' })}
|
||||
required
|
||||
></SealInputNumber>
|
||||
</Form.Item>
|
||||
<Form.Item<FormData>
|
||||
name="os_image"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: getRuleMessage('input', 'clusters.workerpool.osImage')
|
||||
}
|
||||
]}
|
||||
>
|
||||
<SealInput.Input
|
||||
disabled={action === PageAction.EDIT}
|
||||
label={intl.formatMessage({ id: 'clusters.workerpool.osImage' })}
|
||||
required
|
||||
></SealInput.Input>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item<FormData>
|
||||
name="labels"
|
||||
rules={[
|
||||
({ getFieldValue }) => ({
|
||||
validator(rule, value) {
|
||||
if (_.keys(value).length > 0) {
|
||||
if (_.some(_.keys(value), (k: string) => !value[k])) {
|
||||
return Promise.reject(
|
||||
intl.formatMessage(
|
||||
{
|
||||
id: 'common.validate.value'
|
||||
},
|
||||
{
|
||||
name: 'labels'
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
})
|
||||
]}
|
||||
>
|
||||
<LabelSelector
|
||||
label={intl.formatMessage({ id: 'resources.table.labels' })}
|
||||
labels={form.getFieldValue('labels') || {}}
|
||||
btnText={intl.formatMessage({ id: 'common.button.addLabel' })}
|
||||
></LabelSelector>
|
||||
</Form.Item>
|
||||
<CloudOptions ref={cloudOptionsRef}></CloudOptions>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export default PoolForm;
|
||||
@@ -116,15 +116,15 @@ const WorkerPools = () => {
|
||||
id: addPoolStatus.currentData!.id
|
||||
});
|
||||
}
|
||||
setAddPoolStatus({
|
||||
...addPoolStatus,
|
||||
open: false
|
||||
});
|
||||
message.success(intl.formatMessage({ id: 'common.message.success' }));
|
||||
handleSearch();
|
||||
} catch (error) {
|
||||
// error
|
||||
}
|
||||
setAddPoolStatus({
|
||||
...addPoolStatus,
|
||||
open: false
|
||||
});
|
||||
};
|
||||
|
||||
const columns = usePoolsColumns(sortOrder, onSelect);
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { FieldSchema } from '@/components/dynamic-form/config/types';
|
||||
|
||||
export const fields = {
|
||||
volumes: {
|
||||
type: 'array',
|
||||
minItems: 1,
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string' },
|
||||
size_gb: { type: 'number', unit: 'GB' },
|
||||
format: { type: 'string' }
|
||||
},
|
||||
required: ['name', 'size_gb', 'format']
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const fieldConfig: Record<string, FieldSchema> = {
|
||||
volumes: {
|
||||
type: 'array',
|
||||
title: 'Volumes',
|
||||
name: 'volumes',
|
||||
properties: {
|
||||
name: {
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
title: 'Name',
|
||||
widget: 'Input'
|
||||
},
|
||||
size_gb: {
|
||||
name: 'size_gb',
|
||||
type: 'number',
|
||||
title: 'Size (GB)',
|
||||
widget: 'InputNumber',
|
||||
min: 0,
|
||||
style: { width: 120 }
|
||||
},
|
||||
format: {
|
||||
name: 'format',
|
||||
type: 'string',
|
||||
title: 'Format',
|
||||
widget: 'Select',
|
||||
enum: ['ext4', 'xfs', 'btrfs'],
|
||||
style: { width: 150 }
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -155,3 +155,10 @@ export const regionList: {
|
||||
{ label: 'Frankfurt', datacenter: 'Datacenter 1', value: 'fra1', icon: '🇩🇪' },
|
||||
{ label: 'Sydney', datacenter: 'Datacenter 1', value: 'syd1', icon: '🇦🇺' }
|
||||
];
|
||||
|
||||
export const CloudOptionItems = [
|
||||
{
|
||||
label: 'Volumes',
|
||||
key: 'volumes'
|
||||
}
|
||||
];
|
||||
|
||||
@@ -347,7 +347,7 @@ const AdvanceConfig: React.FC<AdvanceConfigProps> = (props) => {
|
||||
})
|
||||
: ''
|
||||
}
|
||||
btnText="common.button.addParams"
|
||||
btnText={intl.formatMessage({ id: 'common.button.addParams' })}
|
||||
label={intl.formatMessage({
|
||||
id: 'models.form.backend_parameters'
|
||||
})}
|
||||
@@ -395,7 +395,7 @@ const AdvanceConfig: React.FC<AdvanceConfigProps> = (props) => {
|
||||
id: 'models.form.env'
|
||||
})}
|
||||
labels={EnviromentVars}
|
||||
btnText="common.button.vars"
|
||||
btnText={intl.formatMessage({ id: 'common.button.vars' })}
|
||||
onBlur={handleEnvSelectorOnBlur}
|
||||
onDelete={handleDeleteEnvSelector}
|
||||
onChange={handleEnviromentVarsChange}
|
||||
|
||||
@@ -1,14 +1,29 @@
|
||||
import { downloadFile } from '@/utils/download-stream';
|
||||
import { request } from '@umijs/max';
|
||||
import { message } from 'antd';
|
||||
import { GPUDeviceItem, ListItem, ModelFile } from '../config/types';
|
||||
|
||||
export const WORKERS_API = '/workers';
|
||||
export const GPU_DEVICES_API = '/gpu-devices';
|
||||
export const MODEL_FILES_API = '/model-files';
|
||||
|
||||
export async function downloadWorkerPrivateKey(id: string | number) {
|
||||
return request(`${WORKERS_API}/${id}/privatekey`, {
|
||||
method: 'GET'
|
||||
});
|
||||
// download stream data and save as a csv file
|
||||
export async function downloadWorkerPrivateKey({
|
||||
id,
|
||||
name
|
||||
}: {
|
||||
id: string | number;
|
||||
name?: string;
|
||||
}) {
|
||||
try {
|
||||
const res = await fetch(`/v1${WORKERS_API}/${id}/privatekey`);
|
||||
if (res.ok) {
|
||||
const blob = await res.blob();
|
||||
downloadFile(blob, `${name}-privatekey.csv`);
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('Download failed');
|
||||
}
|
||||
}
|
||||
|
||||
export async function queryWorkersList<T extends Record<string, any>>(
|
||||
|
||||
@@ -102,7 +102,7 @@ const UpdateLabels: React.FC<ViewModalProps> = (props) => {
|
||||
id: 'resources.table.labels'
|
||||
})}
|
||||
labels={labels}
|
||||
btnText="common.button.addLabel"
|
||||
btnText={intl.formatMessage({ id: 'common.button.addLabel' })}
|
||||
onChange={handleLabelsChange}
|
||||
></LabelSelector>
|
||||
</Form.Item>
|
||||
|
||||
@@ -146,7 +146,10 @@ const Workers: React.FC = () => {
|
||||
handleViewDetail(record);
|
||||
}
|
||||
if (val === 'download_ssh_key') {
|
||||
downloadWorkerPrivateKey(record.id);
|
||||
downloadWorkerPrivateKey({
|
||||
id: record.id,
|
||||
name: record.name
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ export const status: any = {
|
||||
[WorkerStatusMap.not_ready]: StatusMaps.error,
|
||||
[WorkerStatusMap.unreachable]: StatusMaps.error,
|
||||
[WorkerStatusMap.provisioning]: StatusMaps.transitioning,
|
||||
[WorkerStatusMap.deleting]: StatusMaps.warning,
|
||||
[WorkerStatusMap.deleting]: StatusMaps.transitioning,
|
||||
[WorkerStatusMap.error]: StatusMaps.error
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user