| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| 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('镜像配置')}</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; |
|
|