| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | import React, { useState, useEffect, useRef } from 'react'; |
| | import { |
| | Modal, |
| | Form, |
| | Input, |
| | InputNumber, |
| | Typography, |
| | Card, |
| | Space, |
| | Divider, |
| | Button, |
| | Banner, |
| | Tag, |
| | Collapse, |
| | TextArea, |
| | Switch, |
| | } from '@douyinfe/semi-ui'; |
| | import { |
| | FaCog, |
| | FaDocker, |
| | FaKey, |
| | FaTerminal, |
| | FaNetworkWired, |
| | FaExclamationTriangle, |
| | FaPlus, |
| | FaMinus |
| | } from 'react-icons/fa'; |
| | import { API, showError, showSuccess } from '../../../../helpers'; |
| |
|
| | const { Text, Title } = Typography; |
| |
|
| | const UpdateConfigModal = ({ |
| | visible, |
| | onCancel, |
| | deployment, |
| | onSuccess, |
| | t |
| | }) => { |
| | const formRef = useRef(null); |
| | const [loading, setLoading] = useState(false); |
| | const [envVars, setEnvVars] = useState([]); |
| | const [secretEnvVars, setSecretEnvVars] = useState([]); |
| |
|
| | |
| | useEffect(() => { |
| | if (visible && deployment) { |
| | |
| | const initialValues = { |
| | image_url: deployment.container_config?.image_url || '', |
| | traffic_port: deployment.container_config?.traffic_port || null, |
| | entrypoint: deployment.container_config?.entrypoint?.join(' ') || '', |
| | registry_username: '', |
| | registry_secret: '', |
| | command: '', |
| | }; |
| | |
| | if (formRef.current) { |
| | formRef.current.setValues(initialValues); |
| | } |
| | |
| | |
| | const envVarsList = deployment.container_config?.env_variables |
| | ? Object.entries(deployment.container_config.env_variables).map(([key, value]) => ({ |
| | key, value: String(value) |
| | })) |
| | : []; |
| | |
| | setEnvVars(envVarsList); |
| | setSecretEnvVars([]); |
| | } |
| | }, [visible, deployment]); |
| |
|
| | const handleUpdate = async () => { |
| | try { |
| | const formValues = formRef.current ? await formRef.current.validate() : {}; |
| | setLoading(true); |
| |
|
| | |
| | const payload = {}; |
| | |
| | if (formValues.image_url) payload.image_url = formValues.image_url; |
| | if (formValues.traffic_port) payload.traffic_port = formValues.traffic_port; |
| | if (formValues.registry_username) payload.registry_username = formValues.registry_username; |
| | if (formValues.registry_secret) payload.registry_secret = formValues.registry_secret; |
| | if (formValues.command) payload.command = formValues.command; |
| | |
| | |
| | if (formValues.entrypoint) { |
| | payload.entrypoint = formValues.entrypoint.split(' ').filter(cmd => cmd.trim()); |
| | } |
| | |
| | |
| | if (envVars.length > 0) { |
| | payload.env_variables = envVars.reduce((acc, env) => { |
| | if (env.key && env.value !== undefined) { |
| | acc[env.key] = env.value; |
| | } |
| | return acc; |
| | }, {}); |
| | } |
| | |
| | |
| | if (secretEnvVars.length > 0) { |
| | payload.secret_env_variables = secretEnvVars.reduce((acc, env) => { |
| | if (env.key && env.value !== undefined) { |
| | acc[env.key] = env.value; |
| | } |
| | return acc; |
| | }, {}); |
| | } |
| |
|
| | const response = await API.put(`/api/deployments/${deployment.id}`, payload); |
| |
|
| | if (response.data.success) { |
| | showSuccess(t('容器配置更新成功')); |
| | onSuccess?.(response.data.data); |
| | handleCancel(); |
| | } |
| | } catch (error) { |
| | showError(t('更新配置失败') + ': ' + (error.response?.data?.message || error.message)); |
| | } finally { |
| | setLoading(false); |
| | } |
| | }; |
| |
|
| | const handleCancel = () => { |
| | if (formRef.current) { |
| | formRef.current.reset(); |
| | } |
| | setEnvVars([]); |
| | setSecretEnvVars([]); |
| | onCancel(); |
| | }; |
| |
|
| | const addEnvVar = () => { |
| | setEnvVars([...envVars, { key: '', value: '' }]); |
| | }; |
| |
|
| | const removeEnvVar = (index) => { |
| | const newEnvVars = envVars.filter((_, i) => i !== index); |
| | setEnvVars(newEnvVars); |
| | }; |
| |
|
| | const updateEnvVar = (index, field, value) => { |
| | const newEnvVars = [...envVars]; |
| | newEnvVars[index][field] = value; |
| | setEnvVars(newEnvVars); |
| | }; |
| |
|
| | const addSecretEnvVar = () => { |
| | setSecretEnvVars([...secretEnvVars, { key: '', value: '' }]); |
| | }; |
| |
|
| | const removeSecretEnvVar = (index) => { |
| | const newSecretEnvVars = secretEnvVars.filter((_, i) => i !== index); |
| | setSecretEnvVars(newSecretEnvVars); |
| | }; |
| |
|
| | const updateSecretEnvVar = (index, field, value) => { |
| | const newSecretEnvVars = [...secretEnvVars]; |
| | newSecretEnvVars[index][field] = value; |
| | setSecretEnvVars(newSecretEnvVars); |
| | }; |
| |
|
| | return ( |
| | <Modal |
| | title={ |
| | <div className="flex items-center gap-2"> |
| | <FaCog className="text-blue-500" /> |
| | <span>{t('更新容器配置')}</span> |
| | </div> |
| | } |
| | visible={visible} |
| | onCancel={handleCancel} |
| | onOk={handleUpdate} |
| | okText={t('更新配置')} |
| | cancelText={t('取消')} |
| | confirmLoading={loading} |
| | width={700} |
| | className="update-config-modal" |
| | > |
| | <div className="space-y-4 max-h-[600px] overflow-y-auto"> |
| | {/* Container Info */} |
| | <Card className="border-0 bg-gray-50"> |
| | <div className="flex items-center justify-between"> |
| | <div> |
| | <Text strong className="text-base"> |
| | {deployment?.container_name} |
| | </Text> |
| | <div className="mt-1"> |
| | <Text type="secondary" size="small"> |
| | ID: {deployment?.id} |
| | </Text> |
| | </div> |
| | </div> |
| | <Tag color="blue">{deployment?.status}</Tag> |
| | </div> |
| | </Card> |
| | |
| | {/* Warning Banner */} |
| | <Banner |
| | type="warning" |
| | icon={<FaExclamationTriangle />} |
| | title={t('重要提醒')} |
| | description={ |
| | <div className="space-y-2"> |
| | <p>{t('更新容器配置可能会导致容器重启,请确保在合适的时间进行此操作。')}</p> |
| | <p>{t('某些配置更改可能需要几分钟才能生效。')}</p> |
| | </div> |
| | } |
| | /> |
| | |
| | <Form |
| | getFormApi={(api) => (formRef.current = api)} |
| | layout="vertical" |
| | > |
| | <Collapse defaultActiveKey={['docker']}> |
| | {/* Docker Configuration */} |
| | <Collapse.Panel |
| | header={ |
| | <div className="flex items-center gap-2"> |
| | <FaDocker className="text-blue-600" /> |
| | <span>{t('Docker 配置')}</span> |
| | </div> |
| | } |
| | itemKey="docker" |
| | > |
| | <div className="space-y-4"> |
| | <Form.Input |
| | field="image_url" |
| | label={t('镜像地址')} |
| | placeholder={t('例如: nginx:latest')} |
| | rules={[ |
| | { |
| | type: 'string', |
| | message: t('请输入有效的镜像地址') |
| | } |
| | ]} |
| | /> |
| | |
| | <Form.Input |
| | field="registry_username" |
| | label={t('镜像仓库用户名')} |
| | placeholder={t('如果镜像为私有,请填写用户名')} |
| | /> |
| | |
| | <Form.Input |
| | field="registry_secret" |
| | label={t('镜像仓库密码')} |
| | mode="password" |
| | placeholder={t('如果镜像为私有,请填写密码或Token')} |
| | /> |
| | </div> |
| | </Collapse.Panel> |
| | |
| | {/* Network Configuration */} |
| | <Collapse.Panel |
| | header={ |
| | <div className="flex items-center gap-2"> |
| | <FaNetworkWired className="text-green-600" /> |
| | <span>{t('网络配置')}</span> |
| | </div> |
| | } |
| | itemKey="network" |
| | > |
| | <Form.InputNumber |
| | field="traffic_port" |
| | label={t('流量端口')} |
| | placeholder={t('容器对外暴露的端口')} |
| | min={1} |
| | max={65535} |
| | style={{ width: '100%' }} |
| | rules={[ |
| | { |
| | type: 'number', |
| | min: 1, |
| | max: 65535, |
| | message: t('端口号必须在1-65535之间') |
| | } |
| | ]} |
| | /> |
| | </Collapse.Panel> |
| | |
| | {/* Startup Configuration */} |
| | <Collapse.Panel |
| | header={ |
| | <div className="flex items-center gap-2"> |
| | <FaTerminal className="text-purple-600" /> |
| | <span>{t('启动配置')}</span> |
| | </div> |
| | } |
| | itemKey="startup" |
| | > |
| | <div className="space-y-4"> |
| | <Form.Input |
| | field="entrypoint" |
| | label={t('启动命令 (Entrypoint)')} |
| | placeholder={t('例如: /bin/bash -c "python app.py"')} |
| | helpText={t('多个命令用空格分隔')} |
| | /> |
| | |
| | <Form.Input |
| | field="command" |
| | label={t('运行命令 (Command)')} |
| | placeholder={t('容器启动后执行的命令')} |
| | /> |
| | </div> |
| | </Collapse.Panel> |
| | |
| | {/* Environment Variables */} |
| | <Collapse.Panel |
| | header={ |
| | <div className="flex items-center gap-2"> |
| | <FaKey className="text-orange-600" /> |
| | <span>{t('环境变量')}</span> |
| | <Tag size="small">{envVars.length}</Tag> |
| | </div> |
| | } |
| | itemKey="env" |
| | > |
| | <div className="space-y-4"> |
| | {/* Regular Environment Variables */} |
| | <div> |
| | <div className="flex items-center justify-between mb-3"> |
| | <Text strong>{t('普通环境变量')}</Text> |
| | <Button |
| | size="small" |
| | icon={<FaPlus />} |
| | onClick={addEnvVar} |
| | theme="borderless" |
| | type="primary" |
| | > |
| | {t('添加')} |
| | </Button> |
| | </div> |
| | |
| | {envVars.map((envVar, index) => ( |
| | <div key={index} className="flex items-end gap-2 mb-2"> |
| | <Input |
| | placeholder={t('变量名')} |
| | value={envVar.key} |
| | onChange={(value) => updateEnvVar(index, 'key', value)} |
| | style={{ flex: 1 }} |
| | /> |
| | <Text>=</Text> |
| | <Input |
| | placeholder={t('变量值')} |
| | value={envVar.value} |
| | onChange={(value) => updateEnvVar(index, 'value', value)} |
| | style={{ flex: 2 }} |
| | /> |
| | <Button |
| | size="small" |
| | icon={<FaMinus />} |
| | onClick={() => removeEnvVar(index)} |
| | theme="borderless" |
| | type="danger" |
| | /> |
| | </div> |
| | ))} |
| | |
| | {envVars.length === 0 && ( |
| | <div className="text-center text-gray-500 py-4 border-2 border-dashed border-gray-300 rounded-lg"> |
| | <Text type="secondary">{t('暂无环境变量')}</Text> |
| | </div> |
| | )} |
| | </div> |
| | |
| | <Divider /> |
| | |
| | {/* Secret Environment Variables */} |
| | <div> |
| | <div className="flex items-center justify-between mb-3"> |
| | <div className="flex items-center gap-2"> |
| | <Text strong>{t('机密环境变量')}</Text> |
| | <Tag size="small" type="danger"> |
| | {t('加密存储')} |
| | </Tag> |
| | </div> |
| | <Button |
| | size="small" |
| | icon={<FaPlus />} |
| | onClick={addSecretEnvVar} |
| | theme="borderless" |
| | type="danger" |
| | > |
| | {t('添加')} |
| | </Button> |
| | </div> |
| | |
| | {secretEnvVars.map((envVar, index) => ( |
| | <div key={index} className="flex items-end gap-2 mb-2"> |
| | <Input |
| | placeholder={t('变量名')} |
| | value={envVar.key} |
| | onChange={(value) => updateSecretEnvVar(index, 'key', value)} |
| | style={{ flex: 1 }} |
| | /> |
| | <Text>=</Text> |
| | <Input |
| | mode="password" |
| | placeholder={t('变量值')} |
| | value={envVar.value} |
| | onChange={(value) => updateSecretEnvVar(index, 'value', value)} |
| | style={{ flex: 2 }} |
| | /> |
| | <Button |
| | size="small" |
| | icon={<FaMinus />} |
| | onClick={() => removeSecretEnvVar(index)} |
| | theme="borderless" |
| | type="danger" |
| | /> |
| | </div> |
| | ))} |
| | |
| | {secretEnvVars.length === 0 && ( |
| | <div className="text-center text-gray-500 py-4 border-2 border-dashed border-red-200 rounded-lg bg-red-50"> |
| | <Text type="secondary">{t('暂无机密环境变量')}</Text> |
| | </div> |
| | )} |
| | |
| | <Banner |
| | type="info" |
| | title={t('机密环境变量说明')} |
| | description={t('机密环境变量将被加密存储,适用于存储密码、API密钥等敏感信息。')} |
| | size="small" |
| | /> |
| | </div> |
| | </div> |
| | </Collapse.Panel> |
| | </Collapse> |
| | </Form> |
| | |
| | {/* Final Warning */} |
| | <div className="bg-yellow-50 border border-yellow-200 rounded-lg p-3"> |
| | <div className="flex items-start gap-2"> |
| | <FaExclamationTriangle className="text-yellow-600 mt-0.5" /> |
| | <div> |
| | <Text strong className="text-yellow-800"> |
| | {t('配置更新确认')} |
| | </Text> |
| | <div className="mt-1"> |
| | <Text size="small" className="text-yellow-700"> |
| | {t('更新配置后,容器可能需要重启以应用新的设置。请确保您了解这些更改的影响。')} |
| | </Text> |
| | </div> |
| | </div> |
| | </div> |
| | </div> |
| | </div> |
| | </Modal> |
| | ); |
| | }; |
| |
|
| | export default UpdateConfigModal; |