feat: k8s volume mounts
This commit is contained in:
@@ -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