feat(component): 添加组件环境配置功能

master
钟良源 2 months ago
parent 7aa5efb9ba
commit 43324037c4

@ -36,4 +36,14 @@ export function getInstanceLog(params) {
// 组件实例列表
export function getInstanceList(params, identifier) {
return axios.get(`${urlPrefix}/componentInstance/list/${identifier}`, { params });
}
// 获取组件环境配置
export function getComponentEnvConfig(instanceId) {
return axios.get(`${urlPrefix}/componentInstance/config/${instanceId}`);
}
// 保存组件环境配置
export function saveComponentEnvConfig(instanceId, params) {
return axios.post(`${urlPrefix}/componentInstance/config/${instanceId}`, params);
}

@ -0,0 +1,250 @@
import React, { useState, useEffect } from 'react';
import { Modal, Button, Input, Message, Tooltip } from '@arco-design/web-react';
import { IconQuestionCircle, IconCopy, IconDelete, IconPlus } from '@arco-design/web-react/icon';
import styles from './style/envConfigModal.module.less';
import { getComponentEnvConfig, saveComponentEnvConfig } from '@/api/componentInstance';
interface EnvConfigModalProps {
language: string;
visible: boolean;
instanceId: string;
onCancel: () => void;
onSuccess?: () => void;
}
interface ConfigItem {
name: string;
key: string;
value: string;
desc: string;
unmodifiable?: boolean;
isCustom?: boolean; // 标记是否为用户自定义添加的
}
const EnvConfigModal: React.FC<EnvConfigModalProps> = ({
language,
visible,
instanceId,
onCancel,
onSuccess
}) => {
const [loading, setLoading] = useState(false);
const [saveLoading, setSaveLoading] = useState(false);
const [configData, setConfigData] = useState<any>(null);
const [configList, setConfigList] = useState<ConfigItem[]>([]);
useEffect(() => {
if (visible && instanceId) {
fetchEnvConfig();
}
}, [visible, instanceId]);
const fetchEnvConfig = async () => {
try {
setLoading(true);
const res: any = await getComponentEnvConfig(instanceId);
if (res.code === 200) {
setConfigData(res.data);
setConfigList(res.data.config || []);
}
else {
Message.error(res.msg || '获取环境配置失败');
}
} catch (error) {
console.error('获取环境配置失败:', error);
Message.error('获取环境配置失败');
} finally {
setLoading(false);
}
};
const handleConfigChange = (index: number, field: keyof ConfigItem, value: string) => {
const newConfigList = [...configList];
newConfigList[index][field] = value;
setConfigList(newConfigList);
};
// 添加新配置行
const handleAddConfig = () => {
const newConfig: ConfigItem = {
name: '',
key: '',
value: '',
desc: '',
isCustom: true
};
setConfigList([...configList, newConfig]);
};
// 删除配置行
const handleDeleteConfig = (index: number) => {
const newConfigList = configList.filter((_, i) => i !== index);
setConfigList(newConfigList);
};
const handleCopyCommand = () => {
if (configData?.javaCommand) {
navigator.clipboard.writeText(configData.javaCommand);
Message.success('复制成功');
}
};
const handleSave = async () => {
try {
setSaveLoading(true);
const params = {
...configData,
config: configList
};
const res: any = await saveComponentEnvConfig(instanceId, params);
if (res.code === 200) {
Message.success('保存成功');
onSuccess?.();
onCancel();
}
else {
Message.error(res.msg || '保存失败');
}
} catch (error) {
console.error('保存失败:', error);
Message.error('保存失败');
} finally {
setSaveLoading(false);
}
};
return (
<Modal
title="环境配置"
visible={visible}
onCancel={onCancel}
style={{ width: 1000 }}
footer={
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 12 }}>
<Button onClick={onCancel}></Button>
<Button type="primary" onClick={handleSave} loading={saveLoading}>
</Button>
</div>
}
>
<div className={styles['env-config-modal']}>
{loading ? (
<div className={styles['loading-state']}>
<div className={styles['loading-spinner']}></div>
<p>...</p>
</div>
) : (
<>
{/* 命令展示 */}
{configData?.javaCommand && (
<div className={styles['command-section']}>
<div className={styles['command-header']}>
<Button
type="text"
size="mini"
icon={<IconCopy />}
onClick={handleCopyCommand}
>
</Button>
</div>
<div className={styles['command-content']}>
{language.includes('Python') ? configData.pythonCommand : configData.javaCommand}
</div>
<div className={styles['command-hint']}>
&#34;&#34;
</div>
</div>
)}
{/* 配置表格 */}
<div className={styles['config-table']}>
<div className={styles['table-header']}>
<div className={styles['col-index']}>#</div>
<div className={styles['col-name']}>
<span className={styles['required']}>*</span>
</div>
<div className={styles['col-key']}>
<span className={styles['required']}>*</span>
key
</div>
<div className={styles['col-value']}></div>
<div className={styles['col-desc']}></div>
<div className={styles['col-action']}></div>
</div>
<div className={styles['table-body']}>
{configList.map((item, index) => (
<div key={index} className={styles['table-row']}>
<div className={styles['col-index']}>{index + 1}</div>
<div className={styles['col-name']}>
<Input
value={item.name}
onChange={(value) => handleConfigChange(index, 'name', value)}
placeholder="请输入启动名称"
size="small"
/>
</div>
<div className={styles['col-key']}>
<Input
value={item.key}
onChange={(value) => handleConfigChange(index, 'key', value)}
placeholder="请输入参数名"
size="small"
/>
</div>
<div className={styles['col-value']}>
<Input
value={item.value}
onChange={(value) => handleConfigChange(index, 'value', value)}
placeholder="请输入配置值"
size="small"
/>
</div>
<div className={styles['col-desc']}>
<Input
value={item.desc}
onChange={(value) => handleConfigChange(index, 'desc', value)}
placeholder="请输入描述说明"
size="small"
/>
</div>
<div className={styles['col-action']}>
{item.isCustom && (
<Button
type="text"
status="danger"
icon={<IconDelete />}
onClick={() => handleDeleteConfig(index)}
size="small"
>
</Button>
)}
</div>
</div>
))}
</div>
{/* 添加新配置按钮 */}
<div className={styles['add-config-btn']}>
<Button
type="dashed"
icon={<IconPlus />}
onClick={handleAddConfig}
long
>
</Button>
</div>
</div>
</>
)}
</div>
</Modal>
);
};
export default EnvConfigModal;

@ -24,6 +24,7 @@ import {
import { runStatusConstant, runStatusDic, runTypeConstant, runTypeDic } from '@/const/isdp/componentDeploy';
import dayjs from 'dayjs';
import EditInstanceModal from './editInstanceModal';
import EnvConfigModal from './envConfigModal';
const { RangePicker } = DatePicker;
const { TextArea } = Input;
@ -52,8 +53,13 @@ const ListNode: React.FC<ListNodeProps> = ({ componentData }) => {
const [editModalVisible, setEditModalVisible] = useState(false);
const [editingInstance, setEditingInstance] = useState<any>(null);
// 环境配置 Modal 相关状态
const [envModalVisible, setEnvModalVisible] = useState(false);
const [envInstanceId, setEnvInstanceId] = useState<string>('');
// 获取实例列表
const fetchInstanceList = async () => {
console.log("componentData:",componentData);
if (!componentData?.identifier) return;
setLoading(true);
@ -276,6 +282,23 @@ const ListNode: React.FC<ListNodeProps> = ({ componentData }) => {
setEditingInstance(null);
};
// 打开环境配置 Modal
const handleOpenEnvConfig = (record: any) => {
setEnvInstanceId(record.id);
setEnvModalVisible(true);
};
// 关闭环境配置 Modal
const handleEnvConfigCancel = () => {
setEnvModalVisible(false);
setEnvInstanceId('');
};
// 环境配置保存成功回调
const handleEnvConfigSuccess = () => {
fetchInstanceList(); // 刷新列表
};
const columns: TableColumnProps[] = [
{
title: '#',
@ -340,7 +363,9 @@ const ListNode: React.FC<ListNodeProps> = ({ componentData }) => {
</Button>
<Button type="text" onClick={() => handleOpenLog(record)}></Button>
<Button type="text"></Button>
{!isRunning && (
<Button type="text" onClick={() => handleOpenEnvConfig(record)}></Button>
)}
<Button type="text"
onClick={() => handleOpenEdit(record)}
icon={<img
@ -462,6 +487,15 @@ const ListNode: React.FC<ListNodeProps> = ({ componentData }) => {
onCancel={handleEditCancel}
onOk={handleEditOk}
/>
{/* 环境配置 Modal */}
<EnvConfigModal
language={componentData.codeLanguage}
visible={envModalVisible}
instanceId={envInstanceId}
onCancel={handleEnvConfigCancel}
onSuccess={handleEnvConfigSuccess}
/>
</>
);
};

@ -0,0 +1,174 @@
.env-config-modal {
.loading-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
text-align: center;
.loading-spinner {
width: 40px;
height: 40px;
border: 4px solid #f0f2f5;
border-top-color: #165dff;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 16px;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
p {
font-size: 14px;
color: #4e5969;
}
}
.command-section {
margin-bottom: 24px;
padding: 16px;
background: #f7f8fa;
border-radius: 4px;
.command-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
span {
font-size: 14px;
font-weight: 600;
color: #1d2129;
}
}
.command-content {
padding: 12px;
background: #fff;
border: 1px solid #e5e6eb;
border-radius: 4px;
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 12px;
color: #4e5969;
line-height: 1.6;
word-break: break-all;
white-space: pre-wrap;
max-height: 150px;
overflow-y: auto;
}
.command-hint {
margin-top: 8px;
font-size: 12px;
color: #86909c;
}
}
.config-table {
.table-header {
display: flex;
align-items: center;
padding: 12px 8px;
background: #f7f8fa;
border-radius: 4px 4px 0 0;
border: 1px solid #e5e6eb;
font-size: 14px;
font-weight: 600;
color: #1d2129;
> div {
padding: 0 8px;
}
.required {
color: #f53f3f;
margin-right: 4px;
}
}
.table-body {
border: 1px solid #e5e6eb;
border-top: none;
.table-row {
display: flex;
align-items: center;
padding: 12px 8px;
border-bottom: 1px solid #e5e6eb;
transition: background 0.2s;
&:last-child {
border-bottom: none;
}
&:hover {
background: #f7f8fa;
}
> div {
padding: 0 8px;
}
.required {
color: #f53f3f;
margin-right: 4px;
}
.desc-text {
display: flex;
align-items: center;
font-size: 13px;
color: #4e5969;
cursor: help;
}
}
}
.col-index {
width: 50px;
flex-shrink: 0;
text-align: center;
}
.col-name {
width: 120px;
flex-shrink: 0;
}
.col-key {
width: 180px;
flex-shrink: 0;
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 12px;
}
.col-value {
flex: 1;
min-width: 200px;
}
.col-desc {
width: 150px;
flex-shrink: 0;
font-size: 12px;
color: #86909c;
}
.col-action {
width: 80px;
flex-shrink: 0;
text-align: center;
}
}
.add-config-btn {
margin-top: 12px;
padding: 0 8px;
}
}

@ -11,7 +11,7 @@ import {
exportTestCases,
importTestCases
} from '@/api/componentTestCase';
import TestCaseModal from './TestCaseModal';
import TestCaseModal from './testCaseModal';
import useWebSocket from '@/hooks/useWebSocket';
const CollapseItem = Collapse.Item;

Loading…
Cancel
Save