feat(component-env): 添加用户容器状态管理与控制功能

master
钟良源 2 months ago
parent c2d1876a5e
commit de2d0d7852

@ -0,0 +1,19 @@
import axios from 'axios';
// 公共路径
const urlPrefix = '/api/v1/bpms-workbench';
// 容器注册/容器配置
export function containerRegister(params) {
return axios.post(`${urlPrefix}/userContainer/register`, params);
}
// 根据组件实例ID启动容器
export function startContainer(deployEnvId) {
return axios.get(`${urlPrefix}/userContainer/${deployEnvId}/start`);
}
// 根据组件实例ID停止容器
export function stopContainer(deployEnvId) {
return axios.get(`${urlPrefix}/userContainer/${deployEnvId}/stop`);
}

@ -59,7 +59,6 @@ const ListNode: React.FC<ListNodeProps> = ({ componentData }) => {
// 获取实例列表
const fetchInstanceList = async () => {
console.log("componentData:",componentData);
if (!componentData?.identifier) return;
setLoading(true);

@ -0,0 +1,374 @@
import React, { useEffect, useState } from 'react' ;
import { Modal, Form, Select, Grid, Slider, Switch, Input, Message } from '@arco-design/web-react';
import EditableTable from '@/components/EditableTable';
import { createInstance, localStart } from '@/api/componentInstance';
import { getHostList } from '@/api/componentDeployEnv';
const FormItem = Form.Item;
const Option = Select.Option;
const runTypes = [
{
label: '本地运行',
value: 'local'
},
{
label: '线上运行',
value: 'online'
}
];
const portColumns = [
{
title: '主机端口',
dataIndex: 'hostPort',
editable: true
},
{
title: '容器端口',
dataIndex: 'containerPort',
editable: true
},
{
title: '备注',
dataIndex: 'remake',
editable: true
}
];
const directoryColumns = [
{
title: '主机路径',
dataIndex: 'hostPath',
editable: true
},
{
title: '容器路径',
dataIndex: 'containerPath',
editable: true
},
{
title: '备注',
dataIndex: 'remake',
editable: true
}
];
const deviceColumns = [
{
title: '主机路径',
dataIndex: 'hostPath',
editable: true
},
{
title: '容器路径',
dataIndex: 'containerPath',
editable: true
},
{
title: '权限',
dataIndex: 'permissions',
editable: true
},
{
title: '备注',
dataIndex: 'remake',
editable: true
}
];
const ContainerModal = ({ addItem, visible, envType, setVisible, onSuccess }) => {
const [form] = Form.useForm();
const [currentRunType, setCurrentRunType] = useState('local');
const [envTypeOptions, setEnvTypeOptions] = useState([]);
const [portMappingData, setPortMappingData] = useState([]); // 端口映射数据
const [directoryMountData, setDirectoryMountData] = useState([]); // 目录挂载数据
const [deviceMountData, setDeviceMountData] = useState([]); // 设备挂载数据
const [loading, setLoading] = useState(false);
const [hostOptions, setHostOptions] = useState([]); // 主机
const getHostOptions = async () => {
const res: any = await getHostList({
type: 'docker-env',
componentBaseId: addItem.componentBaseId
});
if (res.code === 200) setHostOptions(res.data);
};
// 处理取消
const handleCancel = () => {
form.resetFields();
setCurrentRunType('local');
setPortMappingData([]);
setDirectoryMountData([]);
setDeviceMountData([]);
setVisible(false);
};
// 处理确认
const handleOk = async () => {
try {
setLoading(true);
const values = await form.validate();
// 组装提交数据
const submitData = {
...values,
portMapping: portMappingData, // 端口映射
directoryMount: directoryMountData, // 目录挂载
deviceMount: deviceMountData, // 设备挂载
componentBaseId: addItem?.componentBaseId
};
console.log('提交数据:', submitData);
// TODO: 调用接口提交数据
// const res = await createContainerConfig(submitData);
// if (res.code === 200) {
// Message.success('容器配置成功');
// handleCancel();
// onSuccess?.();
// } else {
// Message.error('容器配置失败: ' + res.message);
// }
} catch (error) {
console.error('表单校验失败:', error);
Message.error('请检查表单必填项');
} finally {
setLoading(false);
}
};
useEffect(() => {
if (envType.length > 0) setEnvTypeOptions(envType);
}, [envType]);
useEffect(() => {
if (visible) {
getHostOptions();
}
else {
// 关闭时重置表单和数据
form.resetFields();
setPortMappingData([]);
setDirectoryMountData([]);
setDeviceMountData([]);
}
}, [visible]);
return (
<Modal
title="容器配置"
visible={visible}
onOk={handleOk}
onCancel={handleCancel}
confirmLoading={loading}
autoFocus={false}
focusLock={true}
style={{ width: '80%' }}
>
<Form form={form} autoComplete="off" labelCol={{ flex: 'none' }}>
<>
<Grid.Row gutter={8}>
<Grid.Col span={12}>
<FormItem
label="部署环境:"
field="tags"
rules={[{ required: true, message: '请选择部署环境' }]}
>
<Select
placeholder="请选择部署环境"
style={{ width: '90%' }}
>
{envTypeOptions.map((option, index) => (
<Option key={option.id} value={option.dictKey}>
{option.classifyName}
</Option>
))}
</Select>
</FormItem>
</Grid.Col>
<Grid.Col span={12}>
<FormItem
label="主机:"
field="type"
rules={[{ required: true, message: '请选择主机' }]}
>
<Select
placeholder="请选择主机"
style={{ width: '90%' }}
>
{hostOptions.map((option, index) => (
<Option key={option.ip} value={option.id}>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span>{option.name}</span>
<span style={{ color: '#999', fontSize: 12 }}>{option.ip}</span>
</div>
</Option>
))}
</Select>
</FormItem>
</Grid.Col>
</Grid.Row>
<Grid.Row gutter={8}>
<Grid.Col span={12}>
<FormItem
label="CPU核数"
field="cpuCores"
rules={[{ required: true, message: '请选择CPU核数' }]}
>
<Slider
defaultValue={1}
min={1}
max={4}
step={1}
showTicks={true}
marks={{
1: '1核',
2: '',
3: '',
4: '4核'
}}
style={{
width: '90%',
whiteSpace: 'nowrap'
}}
/>
</FormItem>
</Grid.Col>
<Grid.Col span={12}>
<FormItem
label="是否使用GPU"
field="useGpu"
rules={[{ required: true, message: '请选择是否使用GPU' }]}
>
<Switch checkedText="ON" uncheckedText="OFF" />
</FormItem>
</Grid.Col>
</Grid.Row>
<FormItem
label="最大内存:"
field="maxMemory"
rules={[{ required: true, message: '请设置最大内存' }]}
>
<Slider
defaultValue={2048}
min={64}
max={4069}
step={64}
marks={{
64: '64MB',
1024: '1GB',
2048: '2GB',
4069: '4GB'
}}
showInput={{
hideControl: false,
suffix: 'MB',
style: {
width: 100
}
}}
style={{ width: '100%' }}
/>
</FormItem>
<Grid.Row gutter={8}>
<Grid.Col span={8}>
<FormItem
label="网络模式:"
field="networkMode"
rules={[{ required: true, message: '请选择网络模式' }]}
>
<Select
placeholder="请选择网络模式"
style={{ width: '90%' }}
>
{runTypes.map((option, index) => (
<Option key={option.value} value={option.value}>
{option.label}
</Option>
))}
</Select>
</FormItem>
</Grid.Col>
<Grid.Col span={8}>
<FormItem
label="网卡:"
field="networkCard"
rules={[{ required: true, message: '请选择网卡' }]}
>
<Select
placeholder="请选择网卡"
style={{ width: '90%' }}
>
{runTypes.map((option, index) => (
<Option key={option.value} value={option.value}>
{option.label}
</Option>
))}
</Select>
</FormItem>
</Grid.Col>
<Grid.Col span={8}>
<FormItem
label="IP"
field="ip"
rules={[{ required: true, message: '请输入IP' }]}
>
<Input style={{ width: '90%' }} allowClear placeholder="请输入IP" />
</FormItem>
</Grid.Col>
</Grid.Row>
<FormItem label="端口映射:">
<EditableTable
columns={portColumns}
data={portMappingData}
onDataChange={(newData) => setPortMappingData(newData)}
showAddButton={true}
addButtonText="添加行"
showDeleteButton={true}
deleteButtonText="删除"
tableProps={{
pagination: { pageSize: 5 },
scroll: { y: 400 }
}}
/>
</FormItem>
<FormItem label="目录挂载:">
<EditableTable
columns={directoryColumns}
data={directoryMountData}
onDataChange={(newData) => setDirectoryMountData(newData)}
showAddButton={true}
addButtonText="添加行"
showDeleteButton={true}
deleteButtonText="删除"
tableProps={{
pagination: { pageSize: 5 },
scroll: { y: 400 }
}}
/>
</FormItem>
<FormItem label="设备挂载:">
<EditableTable
columns={deviceColumns}
data={deviceMountData}
onDataChange={(newData) => setDeviceMountData(newData)}
showAddButton={true}
addButtonText="添加行"
showDeleteButton={true}
deleteButtonText="删除"
tableProps={{
pagination: { pageSize: 5 },
scroll: { y: 400 }
}}
/>
</FormItem>
</>
</Form>
</Modal>
);
};
export default ContainerModal;

@ -10,12 +10,15 @@ import {
TableColumnProps,
Message,
Modal,
Notification
Notification,
Tag
} from '@arco-design/web-react';
import { getComponentClassify } from '@/api/componentClassify';
import { IconSearch } from '@arco-design/web-react/icon';
import ContainerModal from './containerModal';
import AddModal from './addModal';
import { getEnvConfigList, deleteEnvConfig, testEnv } from '@/api/componentDeployEnv';
import { startContainer, stopContainer } from '@/api/userContainer';
const Option = Select.Option;
@ -23,11 +26,13 @@ const ComponentEnv = () => {
const [envType, setEnvType] = useState([]); // 环境类型
const [architectureType, setArchitectureType] = useState(['x86_64', 'aarch64']); // 结构类型
const [visible, setVisible] = useState(false);
const [visible1, setVisible1] = useState(false);
const [data, setData] = useState([]);
const [editingRecord, setEditingRecord] = useState(null);
const [selectedEnvType, setSelectedEnvType] = useState(null); // 选中的环境类型
const [selectedArch, setSelectedArch] = useState(null); // 选中的架构类型
const [searchText, setSearchText] = useState(''); // 搜索文本
const [addItem, setAddItem] = useState(null); // 点击环境配置按钮时记录当前信息
const columns: TableColumnProps[] = [
{
@ -58,12 +63,49 @@ const ComponentEnv = () => {
title: '实例数量',
dataIndex: 'instanceCount'
},
{
title: '用户容器状态',
dataIndex: 'isEnable',
render: (_, record: any) => (
<div>
{record.isEnable === 'NOT_EXIST' && (
<Tag color="gray">
</Tag>
)}
{record.isEnable === 'RUNNING' && (
<Tag color="green">
</Tag>
)}
{record.isEnable === 'STOPPED' && (
<Tag color="red">
</Tag>
)}
</div>
)
},
{
title: '操作',
width: 230,
align: 'center',
render: (_, record: any) => (
<Space>
{record.isEnable === 'NOT_EXIST' && (
<Button type="text" onClick={(record) => {
setAddItem(record);
setVisible1(true);
}}></Button>
)}
{record.isEnable === 'RUNNING' && (
<Button type="text" status="danger" onClick={() => {
handleStop(record.id);
}}></Button>
)}
{record.isEnable === 'STOPPED' && (
<Button type="text" onClick={() => handleStart(record.id)}></Button>
)}
<Button type="text" onClick={() => handleTestEnv(record)}></Button>
<Button type="text" onClick={() => handleConfigEnv(record)}></Button>
<Button type="text" status="danger" onClick={() => handleDeleteEnv(record)}></Button>
@ -72,6 +114,42 @@ const ComponentEnv = () => {
}
];
const handleStop = async (id) => {
const loadingMessage = Message.loading('正在停止容器,请稍候...');
try {
const res: any = await stopContainer(id);
loadingMessage();
if (res.code === 200) {
Message.success('停止成功');
getEnvList();
}
else {
Message.error('停止失败: ' + res.message);
}
} catch (error) {
loadingMessage();
Message.error('停止失败: ' + error.message);
}
};
const handleStart = async (id) => {
const loadingMessage = Message.loading('正在启动容器,请稍候...');
try {
const res: any = await startContainer(id);
loadingMessage();
if (res.code === 200) {
Message.success('启动成功');
getEnvList();
}
else {
Message.error('启动失败: ' + res.message);
}
} catch (error) {
loadingMessage();
Message.error('启动失败: ' + error.message);
}
};
// 环境测试处理函数
const handleTestEnv = async (record: any) => {
Message.info(`正在测试环境 ${record.name}...`);
@ -166,96 +244,107 @@ const ComponentEnv = () => {
}, []);
return (
<div className={styles['component-env']}>
<div className={styles['component-env-header']}>
<div className={styles['component-env-header-left']}>
<Space size={20}>
<div className={styles['handle-row-item']}>
<span></span>
<Select
placeholder="选择环境类型"
style={{ width: 154 }}
allowClear
value={selectedEnvType}
onChange={(value) => {
setSelectedEnvType(value);
getEnvList(value, 'type');
}}
>
{envType.map((option, index) => (
<Option key={option.id} value={option.id}>
{option.classifyName}
</Option>
))}
</Select>
</div>
<div className={styles['handle-row-item']}>
<span></span>
<Select
placeholder="选择架构类型"
style={{ width: 154 }}
allowClear
value={selectedArch}
onChange={(value) => {
setSelectedArch(value);
getEnvList(value, 'arch');
}}
>
{architectureType.map((option, index) => (
<Option key={option} value={option}>
{option}
</Option>
))}
</Select>
</div>
</Space>
</div>
<>
<div className={styles['component-env']}>
<div className={styles['component-env-header']}>
<div className={styles['component-env-header-left']}>
<Space size={20}>
<div className={styles['handle-row-item']}>
<span></span>
<Select
placeholder="选择环境类型"
style={{ width: 154 }}
allowClear
value={selectedEnvType}
onChange={(value) => {
setSelectedEnvType(value);
getEnvList(value, 'type');
}}
>
{envType.map((option, index) => (
<Option key={option.id} value={option.id}>
{option.classifyName}
</Option>
))}
</Select>
</div>
<div className={styles['handle-row-item']}>
<span></span>
<Select
placeholder="选择架构类型"
style={{ width: 154 }}
allowClear
value={selectedArch}
onChange={(value) => {
setSelectedArch(value);
getEnvList(value, 'arch');
}}
>
{architectureType.map((option, index) => (
<Option key={option} value={option}>
{option}
</Option>
))}
</Select>
</div>
</Space>
</div>
<div className={styles['component-env-header-right']}>
<Space split={<Divider type="vertical" />}>
<div>
<Input
prefix={<IconSearch />}
placeholder={'搜索'}
style={{ width: 236, marginRight: 5 }}
value={searchText}
onChange={(value) => setSearchText(value)}
/>
<div className={styles['component-env-header-right']}>
<Space split={<Divider type="vertical" />}>
<div>
<Input
prefix={<IconSearch />}
placeholder={'搜索'}
style={{ width: 236, marginRight: 5 }}
value={searchText}
onChange={(value) => setSearchText(value)}
/>
<Button
type="primary"
style={{ borderRadius: 4 }}
onClick={() => getEnvList(searchText, 'name')}
>
</Button>
</div>
<Button
type="primary"
style={{ borderRadius: 4 }}
onClick={() => getEnvList(searchText, 'name')}
onClick={() => {
setEditingRecord(null);
setVisible(true);
}}
>
+
</Button>
</div>
<Button
type="primary"
style={{ borderRadius: 4 }}
onClick={() => {
setEditingRecord(null);
setVisible(true);
}}
>
+
</Button>
</Space>
</Space>
</div>
</div>
<Table columns={columns} data={data} />
<AddModal
visible={visible}
envType={envType}
setVisible={setVisible}
onOk={(value) => {
setEditingRecord(value);
getEnvList(); // 新增完成后刷新列表
}}
record={editingRecord}
/>
</div>
<Table columns={columns} data={data} />
<AddModal
visible={visible}
<ContainerModal
addItem={addItem}
envType={envType}
setVisible={setVisible}
onOk={(value) => {
setEditingRecord(value);
getEnvList(); // 新增完成后刷新列表
}}
record={editingRecord}
visible={visible1}
setVisible={setVisible1}
onSuccess={getEnvList}
/>
</div>
</>
);
};

Loading…
Cancel
Save