feat: k8s volume mounts
This commit is contained in:
@@ -134,5 +134,29 @@ export default {
|
||||
'clusters.addworker.theadNotes-02':
|
||||
'T-Head PPU uses the Container Device Interface (CDI) for device injection and requires the <span class="bold-text">/var/run/cdi</span> directory to be available for CDI generation.',
|
||||
'clusters.addworker.nvidiaNotes':
|
||||
'The built-in inference backends in GPUStack v2.1 require <span class="bold-text">CUDA 12.6+</span>. Please ensure your NVIDIA driver version is <span class="bold-text">560</span> or newer.'
|
||||
'The built-in inference backends in GPUStack v2.1 require <span class="bold-text">CUDA 12.6+</span>. Please ensure your NVIDIA driver version is <span class="bold-text">560</span> or newer.',
|
||||
'clusters.volume.title': 'Volume Mounts',
|
||||
'clusters.volume.name': 'Volume Name',
|
||||
'clusters.volume.mountPath': 'Container Path',
|
||||
'clusters.volume.mountPath.format': 'Path must start with /',
|
||||
'clusters.volume.readOnly': 'Read Only',
|
||||
'clusters.volume.sourceType': 'Source Type',
|
||||
'clusters.volume.sourceType.hostPath': 'Host Path',
|
||||
'clusters.volume.sourceType.pvc': 'Persistent Volume Claim (PVC)',
|
||||
'clusters.volume.sourceType.configMap': 'ConfigMap',
|
||||
'clusters.volume.hostPath.path': 'Host Path',
|
||||
'clusters.volume.hostPath.type': 'Path Type',
|
||||
'clusters.volume.hostPath.type.directory': 'Directory',
|
||||
'clusters.volume.hostPath.type.directoryOrCreate':
|
||||
'Directory (create if not exists)',
|
||||
'clusters.volume.hostPath.type.file': 'File',
|
||||
'clusters.volume.hostPath.type.fileOrCreate': 'File (create if not exists)',
|
||||
'clusters.volume.hostPath.type.socket': 'Socket',
|
||||
'clusters.volume.hostPath.type.charDevice': 'Character Device',
|
||||
'clusters.volume.hostPath.type.blockDevice': 'Block Device',
|
||||
'clusters.volume.pvc.claimName': 'PVC Name',
|
||||
'clusters.volume.pvc.readOnly': 'Read Only',
|
||||
'clusters.volume.configMap.name': 'ConfigMap Name',
|
||||
'clusters.volume.configMap.optional': 'Optional',
|
||||
'clusters.volume.add': 'Add Volume Mount'
|
||||
};
|
||||
|
||||
@@ -134,7 +134,31 @@ export default {
|
||||
'clusters.addworker.theadNotes-02':
|
||||
'T-Head PPU uses the Container Device Interface (CDI) for device injection and requires the <span class="bold-text">/var/run/cdi</span> directory to be available for CDI generation.',
|
||||
'clusters.addworker.nvidiaNotes':
|
||||
'The built-in inference backends in GPUStack v2.1 require <span class="bold-text">CUDA 12.6+</span>. Please ensure your NVIDIA driver version is <span class="bold-text">560</span> or newer.'
|
||||
'The built-in inference backends in GPUStack v2.1 require <span class="bold-text">CUDA 12.6+</span>. Please ensure your NVIDIA driver version is <span class="bold-text">560</span> or newer.',
|
||||
'clusters.volume.title': 'Volume Mounts',
|
||||
'clusters.volume.name': 'Volume Name',
|
||||
'clusters.volume.mountPath': 'Container Path',
|
||||
'clusters.volume.mountPath.format': 'Path must start with /',
|
||||
'clusters.volume.readOnly': 'Read Only',
|
||||
'clusters.volume.sourceType': 'Source Type',
|
||||
'clusters.volume.sourceType.hostPath': 'Host Path',
|
||||
'clusters.volume.sourceType.pvc': 'Persistent Volume Claim (PVC)',
|
||||
'clusters.volume.sourceType.configMap': 'ConfigMap',
|
||||
'clusters.volume.hostPath.path': 'Host Path',
|
||||
'clusters.volume.hostPath.type': 'Path Type',
|
||||
'clusters.volume.hostPath.type.directory': 'Directory',
|
||||
'clusters.volume.hostPath.type.directoryOrCreate':
|
||||
'Directory (create if not exists)',
|
||||
'clusters.volume.hostPath.type.file': 'File',
|
||||
'clusters.volume.hostPath.type.fileOrCreate': 'File (create if not exists)',
|
||||
'clusters.volume.hostPath.type.socket': 'Socket',
|
||||
'clusters.volume.hostPath.type.charDevice': 'Character Device',
|
||||
'clusters.volume.hostPath.type.blockDevice': 'Block Device',
|
||||
'clusters.volume.pvc.claimName': 'PVC Name',
|
||||
'clusters.volume.pvc.readOnly': 'Read Only',
|
||||
'clusters.volume.configMap.name': 'ConfigMap Name',
|
||||
'clusters.volume.configMap.optional': 'Optional',
|
||||
'clusters.volume.add': 'Add Volume Mount'
|
||||
};
|
||||
|
||||
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
||||
|
||||
@@ -135,7 +135,31 @@ export default {
|
||||
'clusters.addworker.theadNotes-02':
|
||||
'T-Head PPU uses the Container Device Interface (CDI) for device injection and requires the <span class="bold-text">/var/run/cdi</span> directory to be available for CDI generation.',
|
||||
'clusters.addworker.nvidiaNotes':
|
||||
'The built-in inference backends in GPUStack v2.1 require <span class="bold-text">CUDA 12.6+</span>. Please ensure your NVIDIA driver version is <span class="bold-text">560</span> or newer.'
|
||||
'The built-in inference backends in GPUStack v2.1 require <span class="bold-text">CUDA 12.6+</span>. Please ensure your NVIDIA driver version is <span class="bold-text">560</span> or newer.',
|
||||
'clusters.volume.title': 'Volume Mounts',
|
||||
'clusters.volume.name': 'Volume Name',
|
||||
'clusters.volume.mountPath': 'Container Path',
|
||||
'clusters.volume.mountPath.format': 'Path must start with /',
|
||||
'clusters.volume.readOnly': 'Read Only',
|
||||
'clusters.volume.sourceType': 'Source Type',
|
||||
'clusters.volume.sourceType.hostPath': 'Host Path',
|
||||
'clusters.volume.sourceType.pvc': 'Persistent Volume Claim (PVC)',
|
||||
'clusters.volume.sourceType.configMap': 'ConfigMap',
|
||||
'clusters.volume.hostPath.path': 'Host Path',
|
||||
'clusters.volume.hostPath.type': 'Path Type',
|
||||
'clusters.volume.hostPath.type.directory': 'Directory',
|
||||
'clusters.volume.hostPath.type.directoryOrCreate':
|
||||
'Directory (create if not exists)',
|
||||
'clusters.volume.hostPath.type.file': 'File',
|
||||
'clusters.volume.hostPath.type.fileOrCreate': 'File (create if not exists)',
|
||||
'clusters.volume.hostPath.type.socket': 'Socket',
|
||||
'clusters.volume.hostPath.type.charDevice': 'Character Device',
|
||||
'clusters.volume.hostPath.type.blockDevice': 'Block Device',
|
||||
'clusters.volume.pvc.claimName': 'PVC Name',
|
||||
'clusters.volume.pvc.readOnly': 'Read Only',
|
||||
'clusters.volume.configMap.name': 'ConfigMap Name',
|
||||
'clusters.volume.configMap.optional': 'Optional',
|
||||
'clusters.volume.add': 'Add Volume Mount'
|
||||
};
|
||||
|
||||
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
||||
|
||||
@@ -31,7 +31,7 @@ export default {
|
||||
'clusters.create.steps.selectProvider': 'Sağlayıcı Seç',
|
||||
'clusters.create.configBasic': 'Temel Yapılandırma',
|
||||
'clusters.create.execCommand': 'Komutu Çalıştır',
|
||||
'clusters.create.supportedGpu': 'Desteklenen GPU\'lar',
|
||||
'clusters.create.supportedGpu': "Desteklenen GPU'lar",
|
||||
'clusters.create.skipfornow': 'Şimdilik Atla',
|
||||
'clusters.create.noImages': 'Kullanılabilir imaj yok',
|
||||
'clusters.create.noInstanceTypes': 'Kullanılabilir örnek türü yok',
|
||||
@@ -70,16 +70,16 @@ export default {
|
||||
'clusters.addworker.checkEnv': 'Ortamı Kontrol Et',
|
||||
'clusters.addworker.specifyArgs': 'Argümanları Belirle',
|
||||
'clusters.addworker.runCommand': 'Komutu Çalıştır',
|
||||
'clusters.addworker.specifyWorkerIP': 'İşçi Düğüm IP\'si',
|
||||
'clusters.addworker.detectWorkerIP': 'İşçi Düğüm IP\'sini Otomatik Algıla',
|
||||
'clusters.addworker.specifyWorkerIP': "İşçi Düğüm IP'si",
|
||||
'clusters.addworker.detectWorkerIP': "İşçi Düğüm IP'sini Otomatik Algıla",
|
||||
'clusters.addworker.specifyWorkerAddress': 'İşçi Düğüm Harici Adresi',
|
||||
'clusters.addworker.detectWorkerAddress': 'İşçi Düğüm Harici Adresi',
|
||||
'clusters.addworker.detectWorkerAddress.tips':
|
||||
'Belirtilmezse İşçi Düğüm IP\'si varsayılır.',
|
||||
"Belirtilmezse İşçi Düğüm IP'si varsayılır.",
|
||||
'clusters.addworker.externalIP.tips':
|
||||
'VPC veya özel ağda çalıştırılıyorsa, lütfen GPUStack Sunucusuna erişilebilir İşçi Düğüm harici adresini belirtin.',
|
||||
'clusters.addworker.enterWorkerIP': 'İşçi düğüm IP\'sini girin',
|
||||
'clusters.addworker.enterWorkerIP.error': 'Lütfen işçi düğüm IP\'sini girin.',
|
||||
'clusters.addworker.enterWorkerIP': "İşçi düğüm IP'sini girin",
|
||||
'clusters.addworker.enterWorkerIP.error': "Lütfen işçi düğüm IP'sini girin.",
|
||||
'clusters.addworker.enterWorkerAddress': 'İşçi düğüm harici adresini girin',
|
||||
'clusters.addworker.enterWorkerAddress.error':
|
||||
'Lütfen işçi düğüm harici adresini girin.',
|
||||
@@ -89,7 +89,7 @@ export default {
|
||||
'Model önbellek dizinini özelleştirmek istiryorsanız, bağlamak için yolu belirtebilirsiniz.',
|
||||
'clusters.addworker.configSummary': 'Yapılandırma Özeti',
|
||||
'clusters.addworker.gpuVendor': 'GPU Üreticisi',
|
||||
'clusters.addworker.workerIP': 'İşçi Düğüm IP\'si',
|
||||
'clusters.addworker.workerIP': "İşçi Düğüm IP'si",
|
||||
'clusters.addworker.workerExternalIP': 'İşçi Düğüm Harici Adresi',
|
||||
'clusters.addworker.notSpecified': 'Belirtilmedi',
|
||||
'clusters.addworker.autoDetect': 'Otomatik',
|
||||
@@ -105,7 +105,7 @@ export default {
|
||||
'{count} yeni işçi düğüm kümeye eklendi.',
|
||||
'clusters.addworker.message.success_multiple':
|
||||
'{count} yeni işçi düğüm kümeye eklendi.',
|
||||
'clusters.create.serverUrl': 'GPUStack Sunucu URL\'si',
|
||||
'clusters.create.serverUrl': "GPUStack Sunucu URL'si",
|
||||
'clusters.create.workerConfig': 'İşçi Düğüm Yapılandırması',
|
||||
'clusters.addworker.containerName': 'İşçi Düğüm Konteyner Adı',
|
||||
'clusters.addworker.containerName.tips':
|
||||
@@ -116,7 +116,7 @@ export default {
|
||||
'clusters.table.ip.internal': 'Dahili',
|
||||
'clusters.table.ip.external': 'Harici',
|
||||
'clusters.form.serverUrl.tips':
|
||||
'İşçi düğüm GPUStack Sunucusuna doğrudan erişemiyorsa, harici olarak erişilebilir bir GPUStack hizmet URL\'si belirtin.',
|
||||
"İşçi düğüm GPUStack Sunucusuna doğrudan erişemiyorsa, harici olarak erişilebilir bir GPUStack hizmet URL'si belirtin.",
|
||||
'clusters.form.setDefault': 'Varsayılan Olarak Ayarla',
|
||||
'clusters.form.setDefault.tips': 'Dağıtım için varsayılan.',
|
||||
'clusters.addworker.noClusters': 'Kullanılabilir Docker kümesi bulunamadı',
|
||||
@@ -126,7 +126,8 @@ export default {
|
||||
'clusters.create.dockerTips1': 'Sonraki adım, bu kümeye işçi düğüm ekleyin.',
|
||||
'clusters.create.dockerTips2':
|
||||
'Bu adımı atlayabilir ve daha sonra küme listesinden ekleyebilirsiniz.',
|
||||
'clusters.create.k8sTips1': 'Sonraki adım, mevcut Kubernetes kümesini kaydedin.',
|
||||
'clusters.create.k8sTips1':
|
||||
'Sonraki adım, mevcut Kubernetes kümesini kaydedin.',
|
||||
'clusters.create.k8sTips2':
|
||||
'Bu adımı atlayabilir ve daha sonra küme listesinden kaydedebilirsiniz.',
|
||||
'clusters.addworker.theadNotes':
|
||||
@@ -134,5 +135,29 @@ export default {
|
||||
'clusters.addworker.theadNotes-02':
|
||||
'T-Head PPU, cihaz enjeksiyonu için Container Device Interface (CDI) kullanır ve CDI oluşturma için <span class="bold-text">/var/run/cdi</span> dizininin kullanılabilir olmasını gerektirir.',
|
||||
'clusters.addworker.nvidiaNotes':
|
||||
'GPUStack v2.1\'deki yerleşik çıkarım altyapıları <span class="bold-text">CUDA 12.6+</span> gerektirir. Lütfen NVIDIA sürücü sürümünüzün <span class="bold-text">560</span> veya daha yeni olduğundan emin olun.'
|
||||
'GPUStack v2.1\'deki yerleşik çıkarım altyapıları <span class="bold-text">CUDA 12.6+</span> gerektirir. Lütfen NVIDIA sürücü sürümünüzün <span class="bold-text">560</span> veya daha yeni olduğundan emin olun.',
|
||||
'clusters.volume.title': 'Volume Mounts',
|
||||
'clusters.volume.name': 'Volume Name',
|
||||
'clusters.volume.mountPath': 'Container Path',
|
||||
'clusters.volume.mountPath.format': 'Path must start with /',
|
||||
'clusters.volume.readOnly': 'Read Only',
|
||||
'clusters.volume.sourceType': 'Source Type',
|
||||
'clusters.volume.sourceType.hostPath': 'Host Path',
|
||||
'clusters.volume.sourceType.pvc': 'Persistent Volume Claim (PVC)',
|
||||
'clusters.volume.sourceType.configMap': 'ConfigMap',
|
||||
'clusters.volume.hostPath.path': 'Host Path',
|
||||
'clusters.volume.hostPath.type': 'Path Type',
|
||||
'clusters.volume.hostPath.type.directory': 'Directory',
|
||||
'clusters.volume.hostPath.type.directoryOrCreate':
|
||||
'Directory (create if not exists)',
|
||||
'clusters.volume.hostPath.type.file': 'File',
|
||||
'clusters.volume.hostPath.type.fileOrCreate': 'File (create if not exists)',
|
||||
'clusters.volume.hostPath.type.socket': 'Socket',
|
||||
'clusters.volume.hostPath.type.charDevice': 'Character Device',
|
||||
'clusters.volume.hostPath.type.blockDevice': 'Block Device',
|
||||
'clusters.volume.pvc.claimName': 'PVC Name',
|
||||
'clusters.volume.pvc.readOnly': 'Read Only',
|
||||
'clusters.volume.configMap.name': 'ConfigMap Name',
|
||||
'clusters.volume.configMap.optional': 'Optional',
|
||||
'clusters.volume.add': 'Add Volume Mount'
|
||||
};
|
||||
|
||||
@@ -128,5 +128,28 @@ export default {
|
||||
'clusters.addworker.theadNotes-02':
|
||||
'平头哥(T-Head)PPU 使用容器设备接口(CDI)进行设备注入,因此需要确保 <span class="bold-text">/var/run/cdi</span> 目录可用以生成 CDI。',
|
||||
'clusters.addworker.nvidiaNotes':
|
||||
'GPUStack v2.1 内置推理后端依赖 <span class="bold-text">CUDA 12.6</span> 及以上版本,请确保 NVIDIA 驱动版本为 <span class="bold-text">560</span> 或以上。'
|
||||
'GPUStack v2.1 内置推理后端依赖 <span class="bold-text">CUDA 12.6</span> 及以上版本,请确保 NVIDIA 驱动版本为 <span class="bold-text">560</span> 或以上。',
|
||||
'clusters.volume.title': '卷挂载',
|
||||
'clusters.volume.name': '卷名称',
|
||||
'clusters.volume.mountPath': '容器内路径',
|
||||
'clusters.volume.mountPath.format': '路径必须以 / 开头',
|
||||
'clusters.volume.readOnly': '只读',
|
||||
'clusters.volume.sourceType': '存储来源',
|
||||
'clusters.volume.sourceType.hostPath': '主机路径',
|
||||
'clusters.volume.sourceType.pvc': '存储卷声明(PVC)',
|
||||
'clusters.volume.sourceType.configMap': '配置映射(ConfigMap)',
|
||||
'clusters.volume.hostPath.path': '宿主机路径',
|
||||
'clusters.volume.hostPath.type': '路径类型',
|
||||
'clusters.volume.hostPath.type.directory': '目录',
|
||||
'clusters.volume.hostPath.type.directoryOrCreate': '目录(不存在则创建)',
|
||||
'clusters.volume.hostPath.type.file': '文件',
|
||||
'clusters.volume.hostPath.type.fileOrCreate': '文件(不存在则创建)',
|
||||
'clusters.volume.hostPath.type.socket': '套接字',
|
||||
'clusters.volume.hostPath.type.charDevice': '字符设备',
|
||||
'clusters.volume.hostPath.type.blockDevice': '块设备',
|
||||
'clusters.volume.pvc.claimName': 'PVC 名称',
|
||||
'clusters.volume.pvc.readOnly': '只读',
|
||||
'clusters.volume.configMap.name': '配置名称',
|
||||
'clusters.volume.configMap.optional': '可选',
|
||||
'clusters.volume.add': '添加卷挂载'
|
||||
};
|
||||
|
||||
@@ -259,7 +259,7 @@ const VersionsForm: React.FC<AddModalProps> = ({
|
||||
<div
|
||||
key={name}
|
||||
style={{
|
||||
borderRadius: 'var(--ant-border-radius)',
|
||||
borderRadius: 'var(--ant-border-radius-lg)',
|
||||
border: '1px solid var(--ant-color-split)'
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
} from '../config/types';
|
||||
import AdvanceConfig from '../step-forms/advance-config';
|
||||
import CloudProvider from './cloud-provider-form';
|
||||
import K8SVolumeMount from './k8s-volume-mount';
|
||||
|
||||
type AddModalProps = {
|
||||
action: PageActionType;
|
||||
@@ -60,7 +61,17 @@ const ClusterForm: React.FC<AddModalProps> = forwardRef(
|
||||
|
||||
useEffect(() => {
|
||||
if (currentData) {
|
||||
form.setFieldsValue(currentData);
|
||||
const volumeMounts = currentData?.k8s_volume_mounts || [];
|
||||
const realVolumeList = (volumeMounts || []).map(
|
||||
(item: any, index: number) => ({
|
||||
...item,
|
||||
sourceType: Object.keys(item.volumeSource || {})[0] || 'hostPath'
|
||||
})
|
||||
);
|
||||
form.setFieldsValue({
|
||||
...currentData,
|
||||
k8s_volume_mounts: realVolumeList
|
||||
});
|
||||
}
|
||||
}, [currentData]);
|
||||
|
||||
@@ -151,6 +162,7 @@ const ClusterForm: React.FC<AddModalProps> = forwardRef(
|
||||
credentialList={credentialList}
|
||||
></CloudProvider>
|
||||
)}
|
||||
|
||||
<Form.Item<FormData>
|
||||
name="description"
|
||||
rules={[{ required: false }]}
|
||||
@@ -161,6 +173,12 @@ const ClusterForm: React.FC<AddModalProps> = forwardRef(
|
||||
label={intl.formatMessage({ id: 'common.table.description' })}
|
||||
></SealTextArea>
|
||||
</Form.Item>
|
||||
{provider === ProviderValueMap.Kubernetes && (
|
||||
<K8SVolumeMount
|
||||
action={action}
|
||||
currentData={currentData}
|
||||
></K8SVolumeMount>
|
||||
)}
|
||||
<CollapsePanel
|
||||
accordion={false}
|
||||
activeKey={activeKey}
|
||||
|
||||
@@ -0,0 +1,432 @@
|
||||
import CollapsibleContainer from '@/components/collapse-container';
|
||||
import SealCheckbox from '@/components/seal-form/seal-checkbox';
|
||||
import SealInput from '@/components/seal-form/seal-input';
|
||||
import SealSelect from '@/components/seal-form/seal-select';
|
||||
import SealSwitch from '@/components/seal-form/seal-switch';
|
||||
import { PageAction } from '@/config';
|
||||
import { PageActionType } from '@/config/types';
|
||||
import useAppUtils from '@/hooks/use-app-utils';
|
||||
import { MinusOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Button, Flex, Form } from 'antd';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { hostTypeOptions, sourceTypeOptions } from '../config';
|
||||
|
||||
const Label = styled.span`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
color: var(--ant-color-text-secondary);
|
||||
`;
|
||||
|
||||
const Title = styled.div`
|
||||
position: sticky;
|
||||
top: -16px;
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background-color: var(--ant-color-bg-container);
|
||||
font-weight: 600;
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
`;
|
||||
|
||||
const VolumeMountsForm: React.FC<{ action: PageActionType }> = ({ action }) => {
|
||||
const form = Form.useFormInstance();
|
||||
const intl = useIntl();
|
||||
const { getRuleMessage } = useAppUtils();
|
||||
|
||||
const [collapseKey, setCollapseKey] = useState<Set<number | string>>(
|
||||
new Set([0])
|
||||
);
|
||||
|
||||
const volumeList = [
|
||||
{
|
||||
name: 'volume-1',
|
||||
mountPath: '',
|
||||
readOnly: false,
|
||||
sourceType: 'hostPath',
|
||||
volumeSource: {
|
||||
hostPath: {
|
||||
path: '',
|
||||
type: 'DirectoryOrCreate'
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
if (action === PageAction.CREATE) {
|
||||
form.setFieldValue('k8s_volume_mounts', volumeList);
|
||||
}
|
||||
}, [action]);
|
||||
|
||||
const onToggle = (open: boolean, key: number) => {
|
||||
setCollapseKey(open ? new Set([key]) : new Set());
|
||||
};
|
||||
|
||||
const handleAdd = async () => {
|
||||
try {
|
||||
await form.validateFields(['k8s_volume_mounts'], {
|
||||
recursive: true
|
||||
});
|
||||
|
||||
const list = form.getFieldValue('k8s_volume_mounts') || [];
|
||||
|
||||
form.setFieldValue('k8s_volume_mounts', [
|
||||
...list,
|
||||
{
|
||||
name: `volume-${list.length + 1}`,
|
||||
mountPath: '',
|
||||
readOnly: false,
|
||||
sourceType: 'hostPath',
|
||||
volumeSource: {
|
||||
hostPath: {
|
||||
path: '',
|
||||
type: 'DirectoryOrCreate'
|
||||
}
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
setTimeout(() => {
|
||||
setCollapseKey(new Set([list.length]));
|
||||
}, 100);
|
||||
} catch (e: any) {
|
||||
const errorIndex = e?.errorFields?.[0]?.name?.[1];
|
||||
if (typeof errorIndex === 'number') {
|
||||
setCollapseKey(new Set([errorIndex]));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleSourceChange = (value: string, index: number) => {
|
||||
const list = form.getFieldValue('k8s_volume_mounts') || [];
|
||||
|
||||
const updated = list.map((item: any, i: number) => {
|
||||
if (i !== index) return item;
|
||||
|
||||
let volumeSource: any = {};
|
||||
|
||||
if (value === 'hostPath') {
|
||||
volumeSource = {
|
||||
hostPath: { path: '', type: 'DirectoryOrCreate' }
|
||||
};
|
||||
} else if (value === 'pvc') {
|
||||
volumeSource = {
|
||||
persistentVolumeClaim: { claimName: '', readOnly: false }
|
||||
};
|
||||
} else if (value === 'configMap') {
|
||||
volumeSource = {
|
||||
configMap: { name: '', optional: false }
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...item,
|
||||
sourceType: value,
|
||||
volumeSource
|
||||
};
|
||||
});
|
||||
|
||||
form.setFieldValue('k8s_volume_mounts', updated);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Title>
|
||||
<div className="flex-center gap-8">
|
||||
<span>{intl.formatMessage({ id: 'clusters.volume.title' })}</span>
|
||||
<Button type="link" onClick={handleAdd}>
|
||||
<PlusOutlined /> {intl.formatMessage({ id: 'clusters.volume.add' })}
|
||||
</Button>
|
||||
</div>
|
||||
</Title>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '16px',
|
||||
marginBottom: '24px'
|
||||
}}
|
||||
>
|
||||
<Form.List name="k8s_volume_mounts">
|
||||
{(fields, { remove }) => {
|
||||
const list = form.getFieldValue('k8s_volume_mounts') || [];
|
||||
|
||||
return fields.map(({ name }) => {
|
||||
const item = list[name] || {};
|
||||
|
||||
return (
|
||||
<div
|
||||
key={name}
|
||||
style={{
|
||||
border: '1px solid var(--ant-color-split)',
|
||||
borderRadius: 'var(--ant-border-radius-lg)'
|
||||
}}
|
||||
>
|
||||
<CollapsibleContainer
|
||||
collapsible={true}
|
||||
showExpandIcon={true}
|
||||
open={collapseKey.has(name)}
|
||||
onToggle={(open) => onToggle(open, name)}
|
||||
styles={{
|
||||
body: collapseKey.has(name) ? { padding: 16 } : {},
|
||||
content: { paddingTop: 0 },
|
||||
header: {
|
||||
backgroundColor: 'unset'
|
||||
}
|
||||
}}
|
||||
title={
|
||||
<Label>
|
||||
<span>
|
||||
{intl.formatMessage({ id: 'clusters.volume.name' })}:
|
||||
</span>
|
||||
<span>{list[name]?.name}</span>
|
||||
</Label>
|
||||
}
|
||||
right={
|
||||
<Button
|
||||
size="small"
|
||||
shape="circle"
|
||||
onClick={() => remove(name)}
|
||||
>
|
||||
<MinusOutlined />
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Form.Item
|
||||
name={[name, 'name']}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: getRuleMessage(
|
||||
'input',
|
||||
'clusters.volume.name'
|
||||
)
|
||||
}
|
||||
]}
|
||||
>
|
||||
<SealInput.Input
|
||||
label={intl.formatMessage({
|
||||
id: 'clusters.volume.name'
|
||||
})}
|
||||
required
|
||||
></SealInput.Input>
|
||||
</Form.Item>
|
||||
<Flex style={{ gap: 16, width: '100%' }}>
|
||||
{/* Mount Path */}
|
||||
<div style={{ flex: 1 }}>
|
||||
<Form.Item
|
||||
name={[name, 'mountPath']}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: getRuleMessage(
|
||||
'input',
|
||||
'clusters.volume.mountPath'
|
||||
)
|
||||
}
|
||||
]}
|
||||
>
|
||||
<SealInput.Input
|
||||
required
|
||||
placeholder={intl.formatMessage({
|
||||
id: 'clusters.volume.mountPath.format'
|
||||
})}
|
||||
label={intl.formatMessage({
|
||||
id: 'clusters.volume.mountPath'
|
||||
})}
|
||||
></SealInput.Input>
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
{/* ReadOnly */}
|
||||
<Form.Item
|
||||
style={{ width: 240 }}
|
||||
name={[name, 'readOnly']}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<SealSwitch
|
||||
label={intl.formatMessage({
|
||||
id: 'clusters.volume.readOnly'
|
||||
})}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Flex>
|
||||
|
||||
{/* Source Type */}
|
||||
<Form.Item
|
||||
name={[name, 'sourceType']}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: getRuleMessage(
|
||||
'select',
|
||||
'clusters.volume.sourceType'
|
||||
)
|
||||
}
|
||||
]}
|
||||
>
|
||||
<SealSelect
|
||||
required
|
||||
label={intl.formatMessage({
|
||||
id: 'clusters.volume.sourceType'
|
||||
})}
|
||||
onChange={(value) => handleSourceChange(value, name)}
|
||||
options={sourceTypeOptions}
|
||||
value={item.sourceType}
|
||||
></SealSelect>
|
||||
</Form.Item>
|
||||
|
||||
{/* Dynamic Source Form */}
|
||||
{item.sourceType === 'hostPath' && (
|
||||
<Flex style={{ gap: 16 }}>
|
||||
<div style={{ flex: 1 }}>
|
||||
<Form.Item
|
||||
name={[name, 'volumeSource', 'hostPath', 'path']}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: getRuleMessage(
|
||||
'input',
|
||||
'clusters.volume.hostPath.path'
|
||||
)
|
||||
}
|
||||
]}
|
||||
>
|
||||
<SealInput.Input
|
||||
required
|
||||
placeholder={intl.formatMessage({
|
||||
id: 'clusters.volume.mountPath.format'
|
||||
})}
|
||||
label={intl.formatMessage({
|
||||
id: 'clusters.volume.sourceType.hostPath'
|
||||
})}
|
||||
></SealInput.Input>
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
<Form.Item
|
||||
style={{ width: 240 }}
|
||||
name={[name, 'volumeSource', 'hostPath', 'type']}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: getRuleMessage(
|
||||
'select',
|
||||
'clusters.volume.hostPath.type'
|
||||
)
|
||||
}
|
||||
]}
|
||||
>
|
||||
<SealSelect
|
||||
required
|
||||
options={hostTypeOptions}
|
||||
label={intl.formatMessage({
|
||||
id: 'clusters.volume.hostPath.type'
|
||||
})}
|
||||
></SealSelect>
|
||||
</Form.Item>
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
{item.sourceType === 'pvc' && (
|
||||
<Flex style={{ gap: 16 }}>
|
||||
<div style={{ flex: 1 }}>
|
||||
<Form.Item
|
||||
name={[
|
||||
name,
|
||||
'volumeSource',
|
||||
'persistentVolumeClaim',
|
||||
'claimName'
|
||||
]}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: getRuleMessage(
|
||||
'input',
|
||||
'clusters.volume.pvc.claimName'
|
||||
)
|
||||
}
|
||||
]}
|
||||
>
|
||||
<SealInput.Input
|
||||
label={intl.formatMessage({
|
||||
id: 'clusters.volume.pvc.claimName'
|
||||
})}
|
||||
required
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
<Form.Item
|
||||
style={{ width: 240 }}
|
||||
name={[
|
||||
name,
|
||||
'volumeSource',
|
||||
'persistentVolumeClaim',
|
||||
'readOnly'
|
||||
]}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<SealSwitch
|
||||
label={intl.formatMessage({
|
||||
id: 'clusters.volume.pvc.readOnly'
|
||||
})}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
{item.sourceType === 'configMap' && (
|
||||
<Flex style={{ gap: 16 }}>
|
||||
<div style={{ flex: 1 }}>
|
||||
<Form.Item
|
||||
name={[name, 'volumeSource', 'configMap', 'name']}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: getRuleMessage(
|
||||
'input',
|
||||
'clusters.volume.configMap.name'
|
||||
)
|
||||
}
|
||||
]}
|
||||
>
|
||||
<SealInput.Input
|
||||
label={intl.formatMessage({
|
||||
id: 'clusters.volume.configMap.name'
|
||||
})}
|
||||
required
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
<Form.Item
|
||||
style={{ width: 240 }}
|
||||
name={[name, 'volumeSource', 'configMap', 'optional']}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<SealCheckbox
|
||||
label={intl.formatMessage({
|
||||
id: 'clusters.volume.configMap.optional'
|
||||
})}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Flex>
|
||||
)}
|
||||
</CollapsibleContainer>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
}}
|
||||
</Form.List>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default VolumeMountsForm;
|
||||
@@ -97,3 +97,55 @@ export const CloudOptionItems = [
|
||||
key: 'volumes'
|
||||
}
|
||||
];
|
||||
|
||||
export const hostTypeOptions = [
|
||||
{
|
||||
label: 'clusters.volume.hostPath.type.directory',
|
||||
locale: true,
|
||||
value: 'Directory'
|
||||
},
|
||||
{
|
||||
label: 'clusters.volume.hostPath.type.directoryOrCreate',
|
||||
locale: true,
|
||||
value: 'DirectoryOrCreate'
|
||||
},
|
||||
{
|
||||
label: 'clusters.volume.hostPath.type.file',
|
||||
locale: true,
|
||||
value: 'File'
|
||||
},
|
||||
{
|
||||
label: 'clusters.volume.hostPath.type.fileOrCreate',
|
||||
locale: true,
|
||||
value: 'FileOrCreate'
|
||||
},
|
||||
{
|
||||
label: 'clusters.volume.hostPath.type.socket',
|
||||
locale: true,
|
||||
value: 'Socket'
|
||||
},
|
||||
{
|
||||
label: 'clusters.volume.hostPath.type.charDevice',
|
||||
locale: true,
|
||||
value: 'CharDevice'
|
||||
},
|
||||
{
|
||||
label: 'clusters.volume.hostPath.type.blockDevice',
|
||||
locale: true,
|
||||
value: 'BlockDevice'
|
||||
}
|
||||
];
|
||||
|
||||
export const sourceTypeOptions = [
|
||||
{
|
||||
label: 'clusters.volume.sourceType.hostPath',
|
||||
locale: true,
|
||||
value: 'hostPath'
|
||||
},
|
||||
{ label: 'clusters.volume.sourceType.pvc', locale: true, value: 'pvc' },
|
||||
{
|
||||
label: 'clusters.volume.sourceType.configMap',
|
||||
locale: true,
|
||||
value: 'configMap'
|
||||
}
|
||||
];
|
||||
|
||||
@@ -48,6 +48,25 @@ export interface NodePoolListItem extends NodePoolFormData {
|
||||
updated_at: string;
|
||||
cluster_id: number;
|
||||
}
|
||||
export interface VolumeMount {
|
||||
name: string;
|
||||
mountPath: string;
|
||||
readOnly: boolean;
|
||||
volumeSource: {
|
||||
hostPath: {
|
||||
path: string;
|
||||
type: string;
|
||||
};
|
||||
persistentVolumeClaim: {
|
||||
claimName: string;
|
||||
readOnly: boolean;
|
||||
};
|
||||
configMap: {
|
||||
name: string;
|
||||
optional: boolean;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface ClusterListItem {
|
||||
name: string;
|
||||
@@ -68,6 +87,7 @@ export interface ClusterListItem {
|
||||
state: ClusterStatusType;
|
||||
state_message: string;
|
||||
worker_pools: NodePoolListItem[];
|
||||
k8s_volume_mounts?: VolumeMount[];
|
||||
}
|
||||
|
||||
export interface ClusterFormData {
|
||||
@@ -81,6 +101,7 @@ export interface ClusterFormData {
|
||||
server_url?: string;
|
||||
worker_config?: Record<string, any>;
|
||||
worker_pools?: NodePoolFormData[];
|
||||
k8s_volume_mounts?: VolumeMount[];
|
||||
}
|
||||
|
||||
export interface SystemConfig {
|
||||
|
||||
@@ -1,20 +1,8 @@
|
||||
import { PageActionType } from '@/config/types';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { forwardRef, useImperativeHandle, useRef } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import ClusterForm from '../components/cluster-form';
|
||||
import { ProviderType } from '../config';
|
||||
|
||||
const Title = styled.span`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
.text {
|
||||
font-size: 20px;
|
||||
}
|
||||
`;
|
||||
interface BasicFormProps {
|
||||
provider: ProviderType;
|
||||
action: PageActionType;
|
||||
@@ -23,7 +11,6 @@ interface BasicFormProps {
|
||||
}
|
||||
|
||||
const BasicForm = forwardRef((props: BasicFormProps, ref) => {
|
||||
const intl = useIntl();
|
||||
const { provider, credentialList, action, currentData } = props;
|
||||
const formRef = useRef<any>(null);
|
||||
|
||||
@@ -38,25 +25,14 @@ const BasicForm = forwardRef((props: BasicFormProps, ref) => {
|
||||
}));
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* <PageTools
|
||||
marginBottom={16}
|
||||
left={
|
||||
<Title>
|
||||
{intl.formatMessage({ id: 'clusters.create.configBasic' })}
|
||||
</Title>
|
||||
}
|
||||
marginTop={0}
|
||||
></PageTools> */}
|
||||
<ClusterForm
|
||||
provider={provider}
|
||||
action={action}
|
||||
ref={formRef}
|
||||
credentialList={credentialList}
|
||||
onFinish={handleOnFinish}
|
||||
currentData={currentData}
|
||||
></ClusterForm>
|
||||
</div>
|
||||
<ClusterForm
|
||||
provider={provider}
|
||||
action={action}
|
||||
ref={formRef}
|
||||
credentialList={credentialList}
|
||||
onFinish={handleOnFinish}
|
||||
currentData={currentData}
|
||||
></ClusterForm>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user