diff --git a/src/components/icon-font/index.tsx b/src/components/icon-font/index.tsx index 61a42720..c3d89719 100644 --- a/src/components/icon-font/index.tsx +++ b/src/components/icon-font/index.tsx @@ -2,7 +2,7 @@ import { createFromIconfontCN } from '@ant-design/icons'; // import './iconfont/iconfont.js'; const IconFont = createFromIconfontCN({ - scriptUrl: '//at.alicdn.com/t/c/font_4613488_qgb5xlexehp.js' + scriptUrl: '//at.alicdn.com/t/c/font_4613488_pouhrqmodr9.js' }); export default IconFont; diff --git a/src/pages/cluster-management/clusters.tsx b/src/pages/cluster-management/clusters.tsx index 7a384cb5..1a5057a9 100644 --- a/src/pages/cluster-management/clusters.tsx +++ b/src/pages/cluster-management/clusters.tsx @@ -2,6 +2,7 @@ import AutoTooltip from '@/components/auto-tooltip'; import DeleteModal from '@/components/delete-modal'; import DropDownActions from '@/components/drop-down-actions'; import DropdownButtons from '@/components/drop-down-buttons'; +import IconFont from '@/components/icon-font'; import PageTools from '@/components/page-tools'; import { PageAction } from '@/config'; import type { PageActionType } from '@/config/types'; @@ -13,7 +14,6 @@ import { EditOutlined, KubernetesOutlined, ProfileOutlined, - SnippetsOutlined, SyncOutlined } from '@ant-design/icons'; import { PageContainer } from '@ant-design/pro-components'; @@ -36,6 +36,7 @@ import { updateCredential } from './apis'; import AddCluster from './components/add-cluster'; +import AddPool from './components/add-pool'; import { ClusterDataList } from './config'; import { ClusterFormData as FormData, @@ -49,14 +50,21 @@ const addActions = [ locale: false, value: 'custom', key: 'custom', - icon: + icon: }, { label: 'Kubernetes', locale: false, value: 'kubernetes', key: 'kubernetes', - icon: + icon: + }, + { + label: 'Digital Ocean', + locale: false, + value: 'digitalocean', + key: 'digitalocean', + icon: } ]; @@ -105,11 +113,18 @@ const ActionList = [ locale: false, icon: }, + { key: 'terminal', label: 'common.button.detail', icon: }, + { + key: 'addPool', + label: 'Add Node Pool', + locale: false, + icon: + }, { key: 'delete', props: { @@ -143,23 +158,50 @@ const Credentials: React.FC = () => { const intl = useIntl(); const [open, setOpen] = useState(false); const [openAddModal, setOpenAddModal] = useState(false); - const [clusterType, setClusterType] = useState('custom'); + const [provider, setProvider] = useState('custom'); const [action, setAction] = useState(PageAction.CREATE); const [title, setTitle] = useState(''); + const [addPoolStatus, setAddPoolStatus] = useState<{ + open: boolean; + action: PageActionType; + title: string; + provider: string; + }>({ + open: false, + action: PageAction.CREATE, + title: '', + provider: 'digitalocean' + }); const [currentData, setCurrentData] = useState( undefined ); - const handleAddUser = (value: string) => { + const setActions = (row: ListItem) => { + if (row.provider !== 'custom') { + return ActionList.filter((item) => item.key !== 'add'); + } + return ActionList; + }; + + const handleAddCluster = (value: string) => { setOpenAddModal(true); setAction(PageAction.CREATE); - setClusterType(value); - setTitle('Add Cluster'); + setProvider(value); + const label = addActions.find((item) => item.value === value)?.label; + setTitle(`Add ${label} Cluster`); + }; + + const handleAddPool = (value: string) => { + setAddPoolStatus({ + open: true, + action: PageAction.CREATE, + title: `Add Node Pool`, + provider: value + }); }; const handleClickDropdown = (item: any) => { - console.log('handleClickDropdown', item); - handleAddUser(item.key); + handleAddCluster(item.key); }; const handleModalOk = async (data: FormData) => { @@ -194,7 +236,7 @@ const Credentials: React.FC = () => { setCurrentData(row); setOpenAddModal(true); setAction(PageAction.EDIT); - setTitle('Edit Cluster'); + setTitle(`Edit ${row.name} Cluster`); }; const handleSelect = (val: any, row: ListItem) => { @@ -205,6 +247,8 @@ const Credentials: React.FC = () => { } else if (val === 'add') { setOpen(true); setCurrentData(row); + } else if (val === 'addPool') { + handleAddPool(row.provider); } }; @@ -260,7 +304,7 @@ const Credentials: React.FC = () => { items: addActions, onClick: handleClickDropdown }} - trigger={['hover']} + trigger={['click']} placement="bottomRight" > + + } open={open} onClose={onCancel} destroyOnClose={true} @@ -53,7 +67,7 @@ const AddCluster: React.FC = ({ >
- name="name" + name="display_name" rules={[ { required: true, @@ -71,104 +85,15 @@ const AddCluster: React.FC = ({ required > - {clusterType === 'kubernetes' && ( - <> - {' '} - - name="provider" - rules={[ - { - required: true, - message: intl.formatMessage( - { id: 'common.form.rule.input' }, - { - name: 'Provider' - } - ) - } - ]} - > - ({ - label: item, - value: item - }))} - > - - - name="region" - rules={[ - { - required: true, - message: intl.formatMessage( - { id: 'common.form.rule.input' }, - { - name: 'Region' - } - ) - } - ]} - > - ({ - label: item, - value: item - }))} - > - - - )} - - name="gpuPlan" - rules={[ - { - required: true, - message: intl.formatMessage( - { id: 'common.form.rule.input' }, - { - name: 'GPU Plan' - } - ) - } - ]} - > - ({ - label: item, - value: item - }))} - > - - - name="workers" - rules={[ - { - required: true, - message: intl.formatMessage( - { id: 'common.form.rule.input' }, - { - name: 'Workers' - } - ) - } - ]} - > - - - + {provider === 'digitalocean' && } + {provider === 'kubernetes' && } name="description" rules={[{ required: false }]}> - + ); }; diff --git a/src/pages/cluster-management/components/add-credential.tsx b/src/pages/cluster-management/components/add-credential.tsx index 0aed23ff..f5beae09 100644 --- a/src/pages/cluster-management/components/add-credential.tsx +++ b/src/pages/cluster-management/components/add-credential.tsx @@ -1,7 +1,6 @@ import ModalFooter from '@/components/modal-footer'; import ScrollerModal from '@/components/scroller-modal'; import SealInput from '@/components/seal-form/seal-input'; -import SealSelect from '@/components/seal-form/seal-select'; import { PageAction, PasswordReg } from '@/config'; import { PageActionType } from '@/config/types'; import { useIntl } from '@umijs/max'; @@ -16,6 +15,7 @@ type AddModalProps = { onOk: (values: FormData) => void; data?: ListItem; onCancel: () => void; + provider: string; // 'kubernetes' | 'digitalocean'; }; const AddModal: React.FC = ({ title, @@ -23,6 +23,7 @@ const AddModal: React.FC = ({ open, onOk, data, + provider, onCancel }) => { const [form] = Form.useForm(); @@ -69,7 +70,7 @@ const AddModal: React.FC = ({ required > - + {/* name="provider" rules={[ { @@ -86,42 +87,55 @@ const AddModal: React.FC = ({ ({ + options={['Digital Ocean', 'Kubernetes', 'Custom'].map((item) => ({ label: item, value: item }))} > - - - name="access_key" - rules={[ - { - required: action === PageAction.CREATE, - pattern: PasswordReg, - message: intl.formatMessage({ id: 'users.form.rule.password' }) - } - ]} - > - - - - name="secret_key" - rules={[ - { - required: action === PageAction.CREATE, - pattern: PasswordReg, - message: intl.formatMessage({ id: 'users.form.rule.password' }) - } - ]} - > - - + */} + {provider === 'digital_ocean' && ( + <> + + name="access_key" + rules={[ + { + required: action === PageAction.CREATE, + pattern: PasswordReg, + message: intl.formatMessage({ + id: 'users.form.rule.password' + }) + } + ]} + > + + + + name="secret_key" + rules={[ + { + required: action === PageAction.CREATE, + pattern: PasswordReg, + message: intl.formatMessage({ + id: 'users.form.rule.password' + }) + } + ]} + > + + + + )} + {provider === 'kubernetes' && ( + name="kubeconfig" rules={[{ required: false }]}> + + + )} name="description" rules={[{ required: false }]}> void; + data?: ListItem; + onCancel: () => void; +}; +const AddCluster: React.FC = ({ + title, + action, + open, + provider, + onOk, + data, + onCancel +}) => { + const [form] = Form.useForm(); + const intl = useIntl(); + + const handleSumit = () => { + form.submit(); + }; + + const handleCancel = () => { + form.resetFields(); + onCancel(); + }; + + return ( + + {title} + + + } + open={open} + onClose={onCancel} + destroyOnClose={true} + closeIcon={false} + maskClosable={false} + keyboard={false} + width={600} + styles={{}} + footer={ + + } + > +
+ + name="instance_type" + rules={[ + { + required: true, + message: intl.formatMessage( + { id: 'common.form.rule.input' }, + { + name: intl.formatMessage({ id: 'common.table.name' }) + } + ) + } + ]} + > + + + + name="replicas" + rules={[ + { + required: true, + message: intl.formatMessage( + { id: 'common.form.rule.input' }, + { + name: 'Replicas' + } + ) + } + ]} + > + + + + name="batch_size" + rules={[ + { + required: true, + message: intl.formatMessage( + { id: 'common.form.rule.input' }, + { + name: 'Batch Size' + } + ) + } + ]} + > + + + + 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(); + } + }) + ]} + > + + + + name="cloud_options" + 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: 'cloud_options' + } + ) + ); + } + } + return Promise.resolve(); + } + }) + ]} + > + + + +
+ ); +}; + +export default AddCluster; diff --git a/src/pages/cluster-management/components/cloud-provider-form.tsx b/src/pages/cluster-management/components/cloud-provider-form.tsx new file mode 100644 index 00000000..c7ae90d2 --- /dev/null +++ b/src/pages/cluster-management/components/cloud-provider-form.tsx @@ -0,0 +1,90 @@ +import SealSelect from '@/components/seal-form/seal-select'; +import { useIntl } from '@umijs/max'; +import { Form } from 'antd'; +import React from 'react'; +import { ClusterFormData as FormData } from '../config/types'; + +interface CloudProviderProps { + provider: string; // 'kubernetes' | 'digitalocean'; +} + +const CloudProvider: React.FC = () => { + const intl = useIntl(); + return ( + <> + + name="credential_id" + rules={[ + { + required: true, + message: intl.formatMessage( + { id: 'common.form.rule.input' }, + { + name: 'credential' + } + ) + } + ]} + > + ({ + label: item, + value: item + }) + )} + > + + + name="region" + rules={[ + { + required: true, + message: intl.formatMessage( + { id: 'common.form.rule.input' }, + { + name: 'Region' + } + ) + } + ]} + > + ({ + label: item, + value: item + }))} + > + + + name="zone" + rules={[ + { + required: true, + message: intl.formatMessage( + { id: 'common.form.rule.input' }, + { + name: 'Zone' + } + ) + } + ]} + > + ({ + label: item, + value: item + }))} + > + + + ); +}; + +export default CloudProvider; diff --git a/src/pages/cluster-management/components/k8s-provider-form.tsx b/src/pages/cluster-management/components/k8s-provider-form.tsx new file mode 100644 index 00000000..2e56de42 --- /dev/null +++ b/src/pages/cluster-management/components/k8s-provider-form.tsx @@ -0,0 +1,44 @@ +import SealSelect from '@/components/seal-form/seal-select'; +import { useIntl } from '@umijs/max'; +import { Form } from 'antd'; +import React from 'react'; +import { ClusterFormData as FormData } from '../config/types'; + +interface CloudProviderProps { + provider: string; // 'kubernetes' | 'digitalocean'; +} + +const CloudProvider: React.FC = () => { + const intl = useIntl(); + return ( + <> + + name="credential_id" + rules={[ + { + required: true, + message: intl.formatMessage( + { id: 'common.form.rule.input' }, + { + name: 'credential' + } + ) + } + ]} + > + ({ + label: item, + value: item + }) + )} + > + + + ); +}; + +export default CloudProvider; diff --git a/src/pages/cluster-management/config/index.ts b/src/pages/cluster-management/config/index.ts index e6f704dc..dc85ec45 100644 --- a/src/pages/cluster-management/config/index.ts +++ b/src/pages/cluster-management/config/index.ts @@ -1,8 +1,17 @@ export const ClusterDataList = [ + { + id: 3, + name: 'Custom-cluster', + provider: 'custom', + clusterType: 'Custom', + workers: 4, + gpus: 8, + deployments: 3 + }, { id: 1, - name: 'custom-cluster', - provider: 'Custom', + name: 'kubernetes-cluster', + provider: 'kubernetes', clusterType: 'Kubernetes', workers: 2, gpus: 4, @@ -11,19 +20,9 @@ export const ClusterDataList = [ { id: 2, name: 'Digital-Ocean-cluster', - provider: 'Digital Ocean', - clusterType: 'Custom', + provider: 'digitalocean', workers: 3, gpus: 6, deployments: 2 - }, - { - id: 3, - name: 'AutoDL-cluster', - provider: 'AutoDL', - clusterType: 'Custom', - workers: 4, - gpus: 8, - deployments: 3 } ]; diff --git a/src/pages/cluster-management/config/types.ts b/src/pages/cluster-management/config/types.ts index 707a2ab7..18dadabd 100644 --- a/src/pages/cluster-management/config/types.ts +++ b/src/pages/cluster-management/config/types.ts @@ -29,12 +29,29 @@ export interface ClusterListItem { } export interface ClusterFormData { - name: string; + display_name: string; + description: string; provider: string; - credential: string; + credential_id: number; + zone: string; region: string; - workers: number; - gpuPlan: string; - description?: string; - id?: number; +} + +export interface NodePoolListItem { + id: number; + instance_type: string; + replicas: number; + batch_size: number; + labels: Record; + cloud_options: Record; + created_at: string; + updated_at: string; +} + +export interface NodePoolFormData { + instance_type: string; + replicas: number; + batch_size: number; + labels: Record; + cloud_options: Record; } diff --git a/src/pages/cluster-management/credentials.tsx b/src/pages/cluster-management/credentials.tsx index 85f30599..d4301bec 100644 --- a/src/pages/cluster-management/credentials.tsx +++ b/src/pages/cluster-management/credentials.tsx @@ -2,16 +2,16 @@ import AutoTooltip from '@/components/auto-tooltip'; import DeleteModal from '@/components/delete-modal'; import DropDownActions from '@/components/drop-down-actions'; import DropdownButtons from '@/components/drop-down-buttons'; +import IconFont from '@/components/icon-font'; import PageTools from '@/components/page-tools'; import { PageAction } from '@/config'; import type { PageActionType } from '@/config/types'; import useTableFetch from '@/hooks/use-table-fetch'; import { - CloudOutlined, DeleteOutlined, DownOutlined, EditOutlined, - KeyOutlined, + KubernetesOutlined, SyncOutlined } from '@ant-design/icons'; import { PageContainer } from '@ant-design/pro-components'; @@ -55,18 +55,18 @@ const ActionList = [ const addActions = [ { - label: 'Cloud Credential', + label: 'Digital Ocean', locale: false, - value: 'cloud_credential', - key: 'cloud_credential', - icon: + value: 'digital_ocean', + key: 'digital_ocean', + icon: }, { - label: 'SSH Key', + label: 'Kubernetes', locale: false, - value: 'ssh_key', - key: 'ssh_key', - icon: + value: 'kubernetes', + key: 'kubernetes', + icon: } ]; @@ -91,18 +91,32 @@ const Credentials: React.FC = () => { }); const intl = useIntl(); - const [openAddModal, setOpenAddModal] = useState(false); - - const [action, setAction] = useState(PageAction.CREATE); - const [title, setTitle] = useState(''); + const [openModalStatus, setOpenModalStatus] = useState<{ + provider: string; + open: boolean; + action: PageActionType; + title: string; + }>({ + provider: '', + open: false, + action: PageAction.CREATE, + title: '' + }); const [currentData, setCurrentData] = useState( undefined ); - const handleAddUser = () => { - setOpenAddModal(true); - setAction(PageAction.CREATE); - setTitle('Add Credential'); + const handleAddCredential = (value: string) => { + const title = + value === 'digital_ocean' + ? 'Add Digital Ocean Credential' + : 'Add Kubernetes Credential'; + setOpenModalStatus({ + provider: value, + open: true, + action: PageAction.CREATE, + title: title + }); }; const handleModalOk = async (data: FormData) => { @@ -110,7 +124,7 @@ const Credentials: React.FC = () => { ...data }; try { - if (action === PageAction.EDIT) { + if (openModalStatus.action === PageAction.EDIT) { await updateCredential({ data: { ...params, @@ -121,23 +135,26 @@ const Credentials: React.FC = () => { await createCredential({ data: params }); } fetchData(); - setOpenAddModal(false); + setOpenModalStatus({ ...openModalStatus, open: false }); message.success(intl.formatMessage({ id: 'common.message.success' })); } catch (error) { - setOpenAddModal(false); + setOpenModalStatus({ ...openModalStatus, open: false }); } }; const handleModalCancel = () => { console.log('handleModalCancel'); - setOpenAddModal(false); + setOpenModalStatus({ ...openModalStatus, open: false }); }; const handleEditUser = (row: ListItem) => { setCurrentData(row); - setOpenAddModal(true); - setAction(PageAction.EDIT); - setTitle('Edit Credential'); + setOpenModalStatus({ + provider: row.provider, + open: true, + action: PageAction.EDIT, + title: `Edit ${row.provider} Credential` + }); }; const handleSelect = (val: any, row: ListItem) => { @@ -161,7 +178,8 @@ const Credentials: React.FC = () => { }; const handleClickDropdown = (e: any) => { - handleAddUser(); + const value = e.key; + handleAddCredential(value); }; return ( @@ -330,9 +348,10 @@ const Credentials: React.FC = () => { { ); }} /> + { + return ( + + digital-ocean-cluster + + ); + }} + />