feat: 实现环境变量预设功能 & 移除稀疏检出
## 后端改动 - 添加 Project.envPresets 字段(JSON 格式) - 移除 Deployment.env 字段,统一使用 envVars - 更新部署 DTO,支持 envVars (Record<string, string>) - pipeline-runner 支持解析并注入 envVars 到环境 - 移除稀疏检出模板和相关环境变量 - 优化代码格式(Biome lint & format) ## 前端改动 - 新增 EnvPresetsEditor 组件(支持单选/多选/输入框类型) - 项目创建/编辑界面集成环境预设编辑器 - 部署界面基于预设动态生成环境变量表单 - 移除稀疏检出表单项 - 项目详情页添加环境变量预设配置 tab - 优化部署界面布局(基本参数 & 环境变量分区) ## 文档 - 添加完整文档目录结构(docs/) - 创建设计文档 design-0005(部署流程重构) - 添加 API 文档、架构设计文档等 ## 数据库 - 执行 prisma db push 同步 schema 变更
This commit is contained in:
@@ -1,23 +1,24 @@
|
||||
import {
|
||||
Button,
|
||||
Form,
|
||||
Input,
|
||||
Message,
|
||||
Modal,
|
||||
Select,
|
||||
} from '@arco-design/web-react';
|
||||
import { formatDateTime } from '../../../../utils/time';
|
||||
import { IconDelete, IconPlus } from '@arco-design/web-react/icon';
|
||||
import { Form, Input, Message, Modal, Select } from '@arco-design/web-react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import type { Branch, Commit, Pipeline } from '../../types';
|
||||
import { formatDateTime } from '../../../../utils/time';
|
||||
import type { Branch, Commit, Pipeline, Project } from '../../types';
|
||||
import { detailService } from '../service';
|
||||
|
||||
interface EnvPreset {
|
||||
key: string;
|
||||
label: string;
|
||||
type: 'select' | 'multiselect' | 'input';
|
||||
required?: boolean;
|
||||
options?: Array<{ label: string; value: string }>;
|
||||
}
|
||||
|
||||
interface DeployModalProps {
|
||||
visible: boolean;
|
||||
onCancel: () => void;
|
||||
onOk: () => void;
|
||||
pipelines: Pipeline[];
|
||||
projectId: number;
|
||||
project?: Project | null;
|
||||
}
|
||||
|
||||
function DeployModal({
|
||||
@@ -26,12 +27,29 @@ function DeployModal({
|
||||
onOk,
|
||||
pipelines,
|
||||
projectId,
|
||||
project,
|
||||
}: DeployModalProps) {
|
||||
const [form] = Form.useForm();
|
||||
const [branches, setBranches] = useState<Branch[]>([]);
|
||||
const [commits, setCommits] = useState<Commit[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [branchLoading, setBranchLoading] = useState(false);
|
||||
const [envPresets, setEnvPresets] = useState<EnvPreset[]>([]);
|
||||
|
||||
// 解析项目环境预设
|
||||
useEffect(() => {
|
||||
if (project?.envPresets) {
|
||||
try {
|
||||
const presets = JSON.parse(project.envPresets);
|
||||
setEnvPresets(presets);
|
||||
} catch (error) {
|
||||
console.error('解析环境预设失败:', error);
|
||||
setEnvPresets([]);
|
||||
}
|
||||
} else {
|
||||
setEnvPresets([]);
|
||||
}
|
||||
}, [project]);
|
||||
|
||||
const fetchCommits = useCallback(
|
||||
async (branch: string) => {
|
||||
@@ -91,16 +109,27 @@ function DeployModal({
|
||||
try {
|
||||
const values = await form.validate();
|
||||
const selectedCommit = commits.find((c) => c.sha === values.commitHash);
|
||||
const selectedPipeline = pipelines.find((p) => p.id === values.pipelineId);
|
||||
const selectedPipeline = pipelines.find(
|
||||
(p) => p.id === values.pipelineId,
|
||||
);
|
||||
|
||||
if (!selectedCommit || !selectedPipeline) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 格式化环境变量
|
||||
const env = values.envVars
|
||||
?.map((item: { key: string; value: string }) => `${item.key}=${item.value}`)
|
||||
.join('\n');
|
||||
// 收集所有环境变量(从预设项中提取)
|
||||
const envVars: Record<string, string> = {};
|
||||
for (const preset of envPresets) {
|
||||
const value = values[preset.key];
|
||||
if (value !== undefined && value !== null) {
|
||||
// 对于 multiselect,将数组转为逗号分隔的字符串
|
||||
if (preset.type === 'multiselect' && Array.isArray(value)) {
|
||||
envVars[preset.key] = value.join(',');
|
||||
} else {
|
||||
envVars[preset.key] = String(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await detailService.createDeployment({
|
||||
projectId,
|
||||
@@ -108,8 +137,7 @@ function DeployModal({
|
||||
branch: values.branch,
|
||||
commitHash: selectedCommit.sha,
|
||||
commitMessage: selectedCommit.commit.message,
|
||||
env: env,
|
||||
sparseCheckoutPaths: values.sparseCheckoutPaths,
|
||||
envVars, // 提交所有环境变量
|
||||
});
|
||||
|
||||
Message.success('部署任务已创建');
|
||||
@@ -128,126 +156,162 @@ function DeployModal({
|
||||
onCancel={onCancel}
|
||||
autoFocus={false}
|
||||
focusLock={true}
|
||||
style={{ width: 650 }}
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item
|
||||
label="选择流水线"
|
||||
field="pipelineId"
|
||||
rules={[{ required: true, message: '请选择流水线' }]}
|
||||
>
|
||||
<Select placeholder="请选择流水线">
|
||||
{pipelines.map((pipeline) => (
|
||||
<Select.Option key={pipeline.id} value={pipeline.id}>
|
||||
{pipeline.name}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
{/* 基本参数 */}
|
||||
<div className="mb-4 pb-4 border-b border-gray-200">
|
||||
<div className="text-sm font-semibold text-gray-700 mb-3">
|
||||
基本参数
|
||||
</div>
|
||||
|
||||
<Form.Item
|
||||
label="选择分支"
|
||||
field="branch"
|
||||
rules={[{ required: true, message: '请选择分支' }]}
|
||||
>
|
||||
<Select
|
||||
placeholder="请选择分支"
|
||||
loading={branchLoading}
|
||||
onChange={handleBranchChange}
|
||||
<Form.Item
|
||||
label="选择流水线"
|
||||
field="pipelineId"
|
||||
rules={[{ required: true, message: '请选择流水线' }]}
|
||||
>
|
||||
{branches.map((branch) => (
|
||||
<Select.Option key={branch.name} value={branch.name}>
|
||||
{branch.name}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="选择提交"
|
||||
field="commitHash"
|
||||
rules={[{ required: true, message: '请选择提交记录' }]}
|
||||
>
|
||||
<Select
|
||||
placeholder="请选择提交记录"
|
||||
loading={loading}
|
||||
renderFormat={(option) => {
|
||||
const commit = commits.find((c) => c.sha === option?.value);
|
||||
return commit ? commit.sha.substring(0, 7) : '';
|
||||
}}
|
||||
>
|
||||
{commits.map((commit) => (
|
||||
<Select.Option key={commit.sha} value={commit.sha}>
|
||||
<div className="flex flex-col py-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-mono font-medium">
|
||||
{commit.sha.substring(0, 7)}
|
||||
</span>
|
||||
<span className="text-gray-500 text-xs">
|
||||
{formatDateTime(commit.commit.author.date)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-gray-600 text-sm truncate">
|
||||
{commit.commit.message}
|
||||
</div>
|
||||
<div className="text-gray-400 text-xs">
|
||||
{commit.commit.author.name}
|
||||
</div>
|
||||
</div>
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="稀疏检出路径(用于monorepo项目,每行一个路径)"
|
||||
field="sparseCheckoutPaths"
|
||||
tooltip="在monorepo项目中,指定需要检出的目录路径,每行一个路径。留空则检出整个仓库。"
|
||||
>
|
||||
<Input.TextArea
|
||||
placeholder={`例如:\n/packages/frontend\n/packages/backend`}
|
||||
autoSize={{ minRows: 2, maxRows: 6 }}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<div className="mb-2 font-medium text-gray-700">环境变量</div>
|
||||
<Form.List field="envVars">
|
||||
{(fields, { add, remove }) => (
|
||||
<div>
|
||||
{fields.map((item, index) => (
|
||||
<div key={item.key} className="flex items-center gap-2 mb-2">
|
||||
<Form.Item
|
||||
field={`${item.field}.key`}
|
||||
noStyle
|
||||
rules={[{ required: true, message: '请输入变量名' }]}
|
||||
>
|
||||
<Input placeholder="变量名" />
|
||||
</Form.Item>
|
||||
<span className="text-gray-400">=</span>
|
||||
<Form.Item
|
||||
field={`${item.field}.value`}
|
||||
noStyle
|
||||
rules={[{ required: true, message: '请输入变量值' }]}
|
||||
>
|
||||
<Input placeholder="变量值" />
|
||||
</Form.Item>
|
||||
<Button
|
||||
icon={<IconDelete />}
|
||||
status="danger"
|
||||
onClick={() => remove(index)}
|
||||
/>
|
||||
</div>
|
||||
<Select placeholder="请选择流水线">
|
||||
{pipelines.map((pipeline) => (
|
||||
<Select.Option key={pipeline.id} value={pipeline.id}>
|
||||
{pipeline.name}
|
||||
</Select.Option>
|
||||
))}
|
||||
<Button
|
||||
type="dashed"
|
||||
long
|
||||
onClick={() => add()}
|
||||
icon={<IconPlus />}
|
||||
>
|
||||
添加环境变量
|
||||
</Button>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="选择分支"
|
||||
field="branch"
|
||||
rules={[{ required: true, message: '请选择分支' }]}
|
||||
>
|
||||
<Select
|
||||
placeholder="请选择分支"
|
||||
loading={branchLoading}
|
||||
onChange={handleBranchChange}
|
||||
>
|
||||
{branches.map((branch) => (
|
||||
<Select.Option key={branch.name} value={branch.name}>
|
||||
{branch.name}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="选择提交"
|
||||
field="commitHash"
|
||||
rules={[{ required: true, message: '请选择提交记录' }]}
|
||||
>
|
||||
<Select
|
||||
placeholder="请选择提交记录"
|
||||
loading={loading}
|
||||
renderFormat={(option) => {
|
||||
const commit = commits.find((c) => c.sha === option?.value);
|
||||
return commit ? commit.sha.substring(0, 7) : '';
|
||||
}}
|
||||
>
|
||||
{commits.map((commit) => (
|
||||
<Select.Option key={commit.sha} value={commit.sha}>
|
||||
<div className="flex flex-col py-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-mono font-medium">
|
||||
{commit.sha.substring(0, 7)}
|
||||
</span>
|
||||
<span className="text-gray-500 text-xs">
|
||||
{formatDateTime(commit.commit.author.date)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-gray-600 text-sm truncate">
|
||||
{commit.commit.message}
|
||||
</div>
|
||||
<div className="text-gray-400 text-xs">
|
||||
{commit.commit.author.name}
|
||||
</div>
|
||||
</div>
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
{/* 环境变量预设 */}
|
||||
{envPresets.length > 0 && (
|
||||
<div>
|
||||
<div className="text-sm font-semibold text-gray-700 mb-3">
|
||||
环境变量
|
||||
</div>
|
||||
)}
|
||||
</Form.List>
|
||||
{envPresets.map((preset) => {
|
||||
if (preset.type === 'select' && preset.options) {
|
||||
return (
|
||||
<Form.Item
|
||||
key={preset.key}
|
||||
label={preset.label}
|
||||
field={preset.key}
|
||||
rules={
|
||||
preset.required
|
||||
? [{ required: true, message: `请选择${preset.label}` }]
|
||||
: []
|
||||
}
|
||||
>
|
||||
<Select placeholder={`请选择${preset.label}`}>
|
||||
{preset.options.map((option) => (
|
||||
<Select.Option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
|
||||
if (preset.type === 'multiselect' && preset.options) {
|
||||
return (
|
||||
<Form.Item
|
||||
key={preset.key}
|
||||
label={preset.label}
|
||||
field={preset.key}
|
||||
rules={
|
||||
preset.required
|
||||
? [{ required: true, message: `请选择${preset.label}` }]
|
||||
: []
|
||||
}
|
||||
>
|
||||
<Select
|
||||
mode="multiple"
|
||||
placeholder={`请选择${preset.label}`}
|
||||
allowClear
|
||||
>
|
||||
{preset.options.map((option) => (
|
||||
<Select.Option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
|
||||
if (preset.type === 'input') {
|
||||
return (
|
||||
<Form.Item
|
||||
key={preset.key}
|
||||
label={preset.label}
|
||||
field={preset.key}
|
||||
rules={
|
||||
preset.required
|
||||
? [{ required: true, message: `请输入${preset.label}` }]
|
||||
: []
|
||||
}
|
||||
>
|
||||
<Input placeholder={`请输入${preset.label}`} />
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
import { Button, Checkbox, Input, Select, Space } from '@arco-design/web-react';
|
||||
import { IconDelete, IconPlus } from '@arco-design/web-react/icon';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export interface EnvPreset {
|
||||
key: string;
|
||||
label: string;
|
||||
type: 'select' | 'multiselect' | 'input';
|
||||
required?: boolean; // 是否必填
|
||||
options?: Array<{ label: string; value: string }>;
|
||||
}
|
||||
|
||||
interface EnvPresetsEditorProps {
|
||||
value?: EnvPreset[];
|
||||
onChange?: (value: EnvPreset[]) => void;
|
||||
}
|
||||
|
||||
function EnvPresetsEditor({ value = [], onChange }: EnvPresetsEditorProps) {
|
||||
const [presets, setPresets] = useState<EnvPreset[]>(value);
|
||||
|
||||
// 当外部 value 变化时同步到内部状态
|
||||
useEffect(() => {
|
||||
setPresets(value);
|
||||
}, [value]);
|
||||
|
||||
const handleAddPreset = () => {
|
||||
const newPreset: EnvPreset = {
|
||||
key: '',
|
||||
label: '',
|
||||
type: 'select',
|
||||
options: [{ label: '', value: '' }],
|
||||
};
|
||||
const newPresets = [...presets, newPreset];
|
||||
setPresets(newPresets);
|
||||
onChange?.(newPresets);
|
||||
};
|
||||
|
||||
const handleRemovePreset = (index: number) => {
|
||||
const newPresets = presets.filter((_, i) => i !== index);
|
||||
setPresets(newPresets);
|
||||
onChange?.(newPresets);
|
||||
};
|
||||
|
||||
const handlePresetChange = (
|
||||
index: number,
|
||||
field: keyof EnvPreset,
|
||||
val: string | boolean | EnvPreset['type'] | EnvPreset['options'],
|
||||
) => {
|
||||
const newPresets = [...presets];
|
||||
newPresets[index] = { ...newPresets[index], [field]: val };
|
||||
setPresets(newPresets);
|
||||
onChange?.(newPresets);
|
||||
};
|
||||
|
||||
const handleAddOption = (presetIndex: number) => {
|
||||
const newPresets = [...presets];
|
||||
if (!newPresets[presetIndex].options) {
|
||||
newPresets[presetIndex].options = [];
|
||||
}
|
||||
newPresets[presetIndex].options?.push({ label: '', value: '' });
|
||||
setPresets(newPresets);
|
||||
onChange?.(newPresets);
|
||||
};
|
||||
|
||||
const handleRemoveOption = (presetIndex: number, optionIndex: number) => {
|
||||
const newPresets = [...presets];
|
||||
newPresets[presetIndex].options = newPresets[presetIndex].options?.filter(
|
||||
(_, i) => i !== optionIndex,
|
||||
);
|
||||
setPresets(newPresets);
|
||||
onChange?.(newPresets);
|
||||
};
|
||||
|
||||
const handleOptionChange = (
|
||||
presetIndex: number,
|
||||
optionIndex: number,
|
||||
field: 'label' | 'value',
|
||||
val: string,
|
||||
) => {
|
||||
const newPresets = [...presets];
|
||||
if (newPresets[presetIndex].options) {
|
||||
newPresets[presetIndex].options![optionIndex][field] = val;
|
||||
setPresets(newPresets);
|
||||
onChange?.(newPresets);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{presets.map((preset, presetIndex) => (
|
||||
<div
|
||||
key={`preset-${preset.key || presetIndex}`}
|
||||
className="border border-gray-200 rounded p-4"
|
||||
>
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="font-medium text-gray-700">
|
||||
预设项 #{presetIndex + 1}
|
||||
</div>
|
||||
<Button
|
||||
size="small"
|
||||
status="danger"
|
||||
icon={<IconDelete />}
|
||||
onClick={() => handleRemovePreset(presetIndex)}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<Input
|
||||
placeholder="变量名 (key)"
|
||||
value={preset.key}
|
||||
onChange={(val) => handlePresetChange(presetIndex, 'key', val)}
|
||||
/>
|
||||
<Input
|
||||
placeholder="显示名称 (label)"
|
||||
value={preset.label}
|
||||
onChange={(val) =>
|
||||
handlePresetChange(presetIndex, 'label', val)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<Select
|
||||
placeholder="选择类型"
|
||||
value={preset.type}
|
||||
onChange={(val) => handlePresetChange(presetIndex, 'type', val)}
|
||||
>
|
||||
<Select.Option value="select">单选</Select.Option>
|
||||
<Select.Option value="multiselect">多选</Select.Option>
|
||||
<Select.Option value="input">输入框</Select.Option>
|
||||
</Select>
|
||||
|
||||
<div className="flex items-center">
|
||||
<Checkbox
|
||||
checked={preset.required || false}
|
||||
onChange={(checked) =>
|
||||
handlePresetChange(presetIndex, 'required', checked)
|
||||
}
|
||||
>
|
||||
必填项
|
||||
</Checkbox>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{(preset.type === 'select' || preset.type === 'multiselect') && (
|
||||
<div className="mt-2">
|
||||
<div className="text-sm text-gray-600 mb-2">选项:</div>
|
||||
{preset.options?.map((option, optionIndex) => (
|
||||
<div
|
||||
key={`option-${option.value || optionIndex}`}
|
||||
className="flex items-center gap-2 mb-2"
|
||||
>
|
||||
<Input
|
||||
size="small"
|
||||
placeholder="显示文本"
|
||||
value={option.label}
|
||||
onChange={(val) =>
|
||||
handleOptionChange(
|
||||
presetIndex,
|
||||
optionIndex,
|
||||
'label',
|
||||
val,
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
size="small"
|
||||
placeholder="值"
|
||||
value={option.value}
|
||||
onChange={(val) =>
|
||||
handleOptionChange(
|
||||
presetIndex,
|
||||
optionIndex,
|
||||
'value',
|
||||
val,
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
size="small"
|
||||
status="danger"
|
||||
icon={<IconDelete />}
|
||||
onClick={() =>
|
||||
handleRemoveOption(presetIndex, optionIndex)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
<Button
|
||||
size="small"
|
||||
type="dashed"
|
||||
long
|
||||
icon={<IconPlus />}
|
||||
onClick={() => handleAddOption(presetIndex)}
|
||||
>
|
||||
添加选项
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</Space>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<Button type="dashed" long icon={<IconPlus />} onClick={handleAddPreset}>
|
||||
添加环境预设
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default EnvPresetsEditor;
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
} from '@arco-design/web-react';
|
||||
import {
|
||||
IconCode,
|
||||
IconCommand,
|
||||
IconCopy,
|
||||
IconDelete,
|
||||
IconEdit,
|
||||
@@ -49,9 +50,12 @@ import { useEffect, useState } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router';
|
||||
import { useAsyncEffect } from '../../../hooks/useAsyncEffect';
|
||||
import { formatDateTime } from '../../../utils/time';
|
||||
import type { Deployment, Pipeline, Project, Step, WorkspaceDirStatus, WorkspaceStatus } from '../types';
|
||||
import type { Deployment, Pipeline, Project, Step } from '../types';
|
||||
import DeployModal from './components/DeployModal';
|
||||
import DeployRecordItem from './components/DeployRecordItem';
|
||||
import EnvPresetsEditor, {
|
||||
type EnvPreset,
|
||||
} from './components/EnvPresetsEditor';
|
||||
import PipelineStepItem from './components/PipelineStepItem';
|
||||
import { detailService } from './service';
|
||||
|
||||
@@ -84,7 +88,8 @@ function ProjectDetailPage() {
|
||||
null,
|
||||
);
|
||||
const [pipelineModalVisible, setPipelineModalVisible] = useState(false);
|
||||
const [editingPipeline, setEditingPipeline] = useState<PipelineWithEnabled | null>(null);
|
||||
const [editingPipeline, setEditingPipeline] =
|
||||
useState<PipelineWithEnabled | null>(null);
|
||||
const [form] = Form.useForm();
|
||||
const [pipelineForm] = Form.useForm();
|
||||
const [deployRecords, setDeployRecords] = useState<Deployment[]>([]);
|
||||
@@ -92,12 +97,18 @@ function ProjectDetailPage() {
|
||||
|
||||
// 流水线模板相关状态
|
||||
const [isCreatingFromTemplate, setIsCreatingFromTemplate] = useState(false);
|
||||
const [selectedTemplateId, setSelectedTemplateId] = useState<number | null>(null);
|
||||
const [templates, setTemplates] = useState<Array<{id: number, name: string, description: string}>>([]);
|
||||
const [selectedTemplateId, setSelectedTemplateId] = useState<number | null>(
|
||||
null,
|
||||
);
|
||||
const [templates, setTemplates] = useState<
|
||||
Array<{ id: number; name: string; description: string }>
|
||||
>([]);
|
||||
|
||||
// 项目设置相关状态
|
||||
const [projectEditModalVisible, setProjectEditModalVisible] = useState(false);
|
||||
const [isEditingProject, setIsEditingProject] = useState(false);
|
||||
const [projectForm] = Form.useForm();
|
||||
const [envPresets, setEnvPresets] = useState<EnvPreset[]>([]);
|
||||
const [envPresetsLoading, setEnvPresetsLoading] = useState(false);
|
||||
|
||||
const { id } = useParams();
|
||||
|
||||
@@ -172,8 +183,14 @@ function ProjectDetailPage() {
|
||||
setDeployRecords(records);
|
||||
|
||||
// 如果当前选中的记录正在运行,则更新选中记录
|
||||
const selectedRecord = records.find((r: Deployment) => r.id === selectedRecordId);
|
||||
if (selectedRecord && (selectedRecord.status === 'running' || selectedRecord.status === 'pending')) {
|
||||
const selectedRecord = records.find(
|
||||
(r: Deployment) => r.id === selectedRecordId,
|
||||
);
|
||||
if (
|
||||
selectedRecord &&
|
||||
(selectedRecord.status === 'running' ||
|
||||
selectedRecord.status === 'pending')
|
||||
) {
|
||||
// 保持当前选中状态,但更新数据
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -354,14 +371,15 @@ function ProjectDetailPage() {
|
||||
selectedTemplateId,
|
||||
Number(id),
|
||||
values.name,
|
||||
values.description || ''
|
||||
values.description || '',
|
||||
);
|
||||
|
||||
// 更新本地状态 - 需要转换步骤数据结构
|
||||
const transformedSteps = newPipeline.steps?.map(step => ({
|
||||
...step,
|
||||
enabled: step.valid === 1
|
||||
})) || [];
|
||||
const transformedSteps =
|
||||
newPipeline.steps?.map((step) => ({
|
||||
...step,
|
||||
enabled: step.valid === 1,
|
||||
})) || [];
|
||||
|
||||
const pipelineWithDefaults = {
|
||||
...newPipeline,
|
||||
@@ -592,6 +610,21 @@ function ProjectDetailPage() {
|
||||
}
|
||||
};
|
||||
|
||||
// 解析环境变量预设
|
||||
useEffect(() => {
|
||||
if (detail?.envPresets) {
|
||||
try {
|
||||
const presets = JSON.parse(detail.envPresets);
|
||||
setEnvPresets(presets);
|
||||
} catch (error) {
|
||||
console.error('解析环境变量预设失败:', error);
|
||||
setEnvPresets([]);
|
||||
}
|
||||
} else {
|
||||
setEnvPresets([]);
|
||||
}
|
||||
}, [detail]);
|
||||
|
||||
// 项目设置相关函数
|
||||
const handleEditProject = () => {
|
||||
if (detail) {
|
||||
@@ -600,16 +633,21 @@ function ProjectDetailPage() {
|
||||
description: detail.description,
|
||||
repository: detail.repository,
|
||||
});
|
||||
setProjectEditModalVisible(true);
|
||||
setIsEditingProject(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleProjectEditSuccess = async () => {
|
||||
const handleCancelEditProject = () => {
|
||||
setIsEditingProject(false);
|
||||
projectForm.resetFields();
|
||||
};
|
||||
|
||||
const handleSaveProject = async () => {
|
||||
try {
|
||||
const values = await projectForm.validate();
|
||||
await detailService.updateProject(Number(id), values);
|
||||
Message.success('项目更新成功');
|
||||
setProjectEditModalVisible(false);
|
||||
setIsEditingProject(false);
|
||||
|
||||
// 刷新项目详情
|
||||
if (id) {
|
||||
@@ -622,6 +660,27 @@ function ProjectDetailPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveEnvPresets = async () => {
|
||||
try {
|
||||
setEnvPresetsLoading(true);
|
||||
await detailService.updateProject(Number(id), {
|
||||
envPresets: JSON.stringify(envPresets),
|
||||
});
|
||||
Message.success('环境变量预设保存成功');
|
||||
|
||||
// 刷新项目详情
|
||||
if (id) {
|
||||
const projectDetail = await detailService.getProjectDetail(Number(id));
|
||||
setDetail(projectDetail);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('保存环境变量预设失败:', error);
|
||||
Message.error('保存环境变量预设失败');
|
||||
} finally {
|
||||
setEnvPresetsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteProject = () => {
|
||||
Modal.confirm({
|
||||
title: '删除项目',
|
||||
@@ -671,7 +730,7 @@ function ProjectDetailPage() {
|
||||
);
|
||||
|
||||
// 获取选中的流水线
|
||||
const selectedPipeline = pipelines.find(
|
||||
const _selectedPipeline = pipelines.find(
|
||||
(pipeline) => pipeline.id === selectedPipelineId,
|
||||
);
|
||||
|
||||
@@ -681,11 +740,13 @@ function ProjectDetailPage() {
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`;
|
||||
return `${(bytes / k ** i).toFixed(2)} ${sizes[i]}`;
|
||||
};
|
||||
|
||||
// 获取工作目录状态标签
|
||||
const getWorkspaceStatusTag = (status: string): { text: string; color: string } => {
|
||||
const getWorkspaceStatusTag = (
|
||||
status: string,
|
||||
): { text: string; color: string } => {
|
||||
const statusMap: Record<string, { text: string; color: string }> = {
|
||||
not_created: { text: '未创建', color: 'gray' },
|
||||
empty: { text: '空目录', color: 'orange' },
|
||||
@@ -703,7 +764,15 @@ function ProjectDetailPage() {
|
||||
const statusInfo = getWorkspaceStatusTag(workspaceStatus.status as string);
|
||||
|
||||
return (
|
||||
<Card className="mb-6" title={<Space><IconFolder />工作目录状态</Space>}>
|
||||
<Card
|
||||
className="mb-6"
|
||||
title={
|
||||
<Space>
|
||||
<IconFolder />
|
||||
工作目录状态
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Descriptions
|
||||
column={2}
|
||||
data={[
|
||||
@@ -717,7 +786,9 @@ function ProjectDetailPage() {
|
||||
},
|
||||
{
|
||||
label: '目录大小',
|
||||
value: workspaceStatus.size ? formatSize(workspaceStatus.size) : '-',
|
||||
value: workspaceStatus.size
|
||||
? formatSize(workspaceStatus.size)
|
||||
: '-',
|
||||
},
|
||||
{
|
||||
label: '当前分支',
|
||||
@@ -727,16 +798,24 @@ function ProjectDetailPage() {
|
||||
label: '最后提交',
|
||||
value: workspaceStatus.gitInfo?.lastCommit ? (
|
||||
<Space direction="vertical" size="mini">
|
||||
<Typography.Text code>{workspaceStatus.gitInfo.lastCommit}</Typography.Text>
|
||||
<Typography.Text type="secondary">{workspaceStatus.gitInfo.lastCommitMessage}</Typography.Text>
|
||||
<Typography.Text code>
|
||||
{workspaceStatus.gitInfo.lastCommit}
|
||||
</Typography.Text>
|
||||
<Typography.Text type="secondary">
|
||||
{workspaceStatus.gitInfo.lastCommitMessage}
|
||||
</Typography.Text>
|
||||
</Space>
|
||||
) : '-',
|
||||
) : (
|
||||
'-'
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
{workspaceStatus.error && (
|
||||
<div className="mt-4 p-3 bg-red-50 border border-red-200 rounded">
|
||||
<Typography.Text type="danger">{workspaceStatus.error}</Typography.Text>
|
||||
<Typography.Text type="danger">
|
||||
{workspaceStatus.error}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
@@ -763,7 +842,15 @@ function ProjectDetailPage() {
|
||||
size="large"
|
||||
className="h-full flex flex-col [&>.arco-tabs-content]:flex-1 [&>.arco-tabs-content]:overflow-hidden [&>.arco-tabs-content_.arco-tabs-content-inner]:h-full [&>.arco-tabs-pane]:h-full"
|
||||
>
|
||||
<Tabs.TabPane title={<Space><IconHistory />部署记录</Space>} key="deployRecords">
|
||||
<Tabs.TabPane
|
||||
title={
|
||||
<Space>
|
||||
<IconHistory />
|
||||
部署记录
|
||||
</Space>
|
||||
}
|
||||
key="deployRecords"
|
||||
>
|
||||
<div className="grid grid-cols-5 gap-6 h-full">
|
||||
{/* 左侧部署记录列表 */}
|
||||
<div className="col-span-2 space-y-4 h-full flex flex-col">
|
||||
@@ -813,7 +900,9 @@ function ProjectDetailPage() {
|
||||
type="primary"
|
||||
icon={<IconRefresh />}
|
||||
size="small"
|
||||
onClick={() => handleRetryDeployment(selectedRecord.id)}
|
||||
onClick={() =>
|
||||
handleRetryDeployment(selectedRecord.id)
|
||||
}
|
||||
>
|
||||
重新执行
|
||||
</Button>
|
||||
@@ -838,7 +927,15 @@ function ProjectDetailPage() {
|
||||
</div>
|
||||
</div>
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane title={<Space><IconCode />流水线</Space>} key="pipeline">
|
||||
<Tabs.TabPane
|
||||
title={
|
||||
<Space>
|
||||
<IconCode />
|
||||
流水线
|
||||
</Space>
|
||||
}
|
||||
key="pipeline"
|
||||
>
|
||||
<div className="grid grid-cols-5 gap-6 h-full">
|
||||
{/* 左侧流水线列表 */}
|
||||
<div className="col-span-2 space-y-4">
|
||||
@@ -951,9 +1048,7 @@ function ProjectDetailPage() {
|
||||
{pipeline.description}
|
||||
</Typography.Text>
|
||||
<div className="flex items-center justify-between text-xs text-gray-500">
|
||||
<span>
|
||||
{pipeline.steps?.length || 0} 个步骤
|
||||
</span>
|
||||
<span>{pipeline.steps?.length || 0} 个步骤</span>
|
||||
<span>{formatDateTime(pipeline.updatedAt)}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1005,7 +1100,11 @@ function ProjectDetailPage() {
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<SortableContext
|
||||
items={selectedPipeline.steps?.map(step => step.id) || []}
|
||||
items={
|
||||
selectedPipeline.steps?.map(
|
||||
(step) => step.id,
|
||||
) || []
|
||||
}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
<div className="space-y-3 max-h-[calc(100vh-300px)] overflow-y-auto">
|
||||
@@ -1046,48 +1145,140 @@ function ProjectDetailPage() {
|
||||
</Tabs.TabPane>
|
||||
|
||||
{/* 项目设置标签页 */}
|
||||
<Tabs.TabPane key="settings" title={<Space><IconSettings />项目设置</Space>}>
|
||||
<Tabs.TabPane
|
||||
key="settings"
|
||||
title={
|
||||
<Space>
|
||||
<IconSettings />
|
||||
项目设置
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<div className="p-6">
|
||||
<Card title="项目信息" className="mb-4">
|
||||
<Descriptions
|
||||
column={1}
|
||||
data={[
|
||||
{
|
||||
label: '项目名称',
|
||||
value: detail?.name,
|
||||
},
|
||||
{
|
||||
label: '项目描述',
|
||||
value: detail?.description || '-',
|
||||
},
|
||||
{
|
||||
label: 'Git 仓库',
|
||||
value: detail?.repository,
|
||||
},
|
||||
{
|
||||
label: '工作目录',
|
||||
value: detail?.projectDir || '-',
|
||||
},
|
||||
{
|
||||
label: '创建时间',
|
||||
value: formatDateTime(detail?.createdAt),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<div className="mt-4 flex gap-2">
|
||||
<Button type="primary" onClick={handleEditProject}>
|
||||
编辑项目
|
||||
</Button>
|
||||
<Button status="danger" onClick={handleDeleteProject}>
|
||||
删除项目
|
||||
</Button>
|
||||
</div>
|
||||
{!isEditingProject ? (
|
||||
<>
|
||||
<Descriptions
|
||||
column={1}
|
||||
data={[
|
||||
{
|
||||
label: '项目名称',
|
||||
value: detail?.name,
|
||||
},
|
||||
{
|
||||
label: '项目描述',
|
||||
value: detail?.description || '-',
|
||||
},
|
||||
{
|
||||
label: 'Git 仓库',
|
||||
value: detail?.repository,
|
||||
},
|
||||
{
|
||||
label: '工作目录',
|
||||
value: detail?.projectDir || '-',
|
||||
},
|
||||
{
|
||||
label: '创建时间',
|
||||
value: formatDateTime(detail?.createdAt),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<div className="mt-4 flex gap-2">
|
||||
<Button type="primary" onClick={handleEditProject}>
|
||||
编辑项目
|
||||
</Button>
|
||||
<Button status="danger" onClick={handleDeleteProject}>
|
||||
删除项目
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Form form={projectForm} layout="vertical">
|
||||
<Form.Item
|
||||
field="name"
|
||||
label="项目名称"
|
||||
rules={[
|
||||
{ required: true, message: '请输入项目名称' },
|
||||
{ minLength: 2, message: '项目名称至少2个字符' },
|
||||
]}
|
||||
>
|
||||
<Input placeholder="例如:我的应用" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
field="description"
|
||||
label="项目描述"
|
||||
rules={[
|
||||
{ maxLength: 200, message: '描述不能超过200个字符' },
|
||||
]}
|
||||
>
|
||||
<Input.TextArea
|
||||
placeholder="请输入项目描述"
|
||||
rows={3}
|
||||
maxLength={200}
|
||||
showWordLimit
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
field="repository"
|
||||
label="Git 仓库地址"
|
||||
rules={[{ required: true, message: '请输入仓库地址' }]}
|
||||
>
|
||||
<Input placeholder="例如:https://github.com/user/repo.git" />
|
||||
</Form.Item>
|
||||
<div className="text-sm text-gray-500 mb-4">
|
||||
<strong>工作目录:</strong> {detail?.projectDir || '-'}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500 mb-4">
|
||||
<strong>创建时间:</strong>{' '}
|
||||
{formatDateTime(detail?.createdAt)}
|
||||
</div>
|
||||
</Form>
|
||||
<div className="mt-4 flex gap-2">
|
||||
<Button type="primary" onClick={handleSaveProject}>
|
||||
保存
|
||||
</Button>
|
||||
<Button onClick={handleCancelEditProject}>取消</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* 工作目录状态 */}
|
||||
{renderWorkspaceStatus()}
|
||||
</div>
|
||||
</Tabs.TabPane>
|
||||
|
||||
{/* 环境变量预设标签页 */}
|
||||
<Tabs.TabPane
|
||||
key="envPresets"
|
||||
title={
|
||||
<Space>
|
||||
<IconCommand />
|
||||
环境变量
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<div className="p-6">
|
||||
<Card
|
||||
title="环境变量预设"
|
||||
extra={
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={handleSaveEnvPresets}
|
||||
loading={envPresetsLoading}
|
||||
>
|
||||
保存预设
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<div className="text-sm text-gray-600 mb-4">
|
||||
配置项目的环境变量预设,在部署时可以选择这些预设值。支持单选、多选和输入框类型。
|
||||
</div>
|
||||
<EnvPresetsEditor value={envPresets} onChange={setEnvPresets} />
|
||||
</Card>
|
||||
</div>
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
@@ -1139,7 +1330,9 @@ function ProjectDetailPage() {
|
||||
<Select.Option key={template.id} value={template.id}>
|
||||
<div>
|
||||
<div>{template.name}</div>
|
||||
<div className="text-xs text-gray-500">{template.description}</div>
|
||||
<div className="text-xs text-gray-500">
|
||||
{template.description}
|
||||
</div>
|
||||
</div>
|
||||
</Select.Option>
|
||||
))}
|
||||
@@ -1155,10 +1348,7 @@ function ProjectDetailPage() {
|
||||
>
|
||||
<Input placeholder="例如:前端部署流水线、Docker部署流水线..." />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
field="description"
|
||||
label="流水线描述"
|
||||
>
|
||||
<Form.Item field="description" label="流水线描述">
|
||||
<Input.TextArea
|
||||
placeholder="描述这个流水线的用途和特点..."
|
||||
rows={3}
|
||||
@@ -1176,10 +1366,7 @@ function ProjectDetailPage() {
|
||||
>
|
||||
<Input placeholder="例如:前端部署流水线、Docker部署流水线..." />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
field="description"
|
||||
label="流水线描述"
|
||||
>
|
||||
<Form.Item field="description" label="流水线描述">
|
||||
<Input.TextArea
|
||||
placeholder="描述这个流水线的用途和特点..."
|
||||
rows={3}
|
||||
@@ -1228,47 +1415,6 @@ function ProjectDetailPage() {
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
{/* 编辑项目模态框 */}
|
||||
<Modal
|
||||
title="编辑项目"
|
||||
visible={projectEditModalVisible}
|
||||
onOk={handleProjectEditSuccess}
|
||||
onCancel={() => setProjectEditModalVisible(false)}
|
||||
style={{ width: 500 }}
|
||||
>
|
||||
<Form form={projectForm} layout="vertical">
|
||||
<Form.Item
|
||||
field="name"
|
||||
label="项目名称"
|
||||
rules={[
|
||||
{ required: true, message: '请输入项目名称' },
|
||||
{ minLength: 2, message: '项目名称至少2个字符' },
|
||||
]}
|
||||
>
|
||||
<Input placeholder="例如:我的应用" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
field="description"
|
||||
label="项目描述"
|
||||
rules={[{ maxLength: 200, message: '描述不能超过200个字符' }]}
|
||||
>
|
||||
<Input.TextArea
|
||||
placeholder="请输入项目描述"
|
||||
rows={3}
|
||||
maxLength={200}
|
||||
showWordLimit
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
field="repository"
|
||||
label="Git 仓库地址"
|
||||
rules={[{ required: true, message: '请输入仓库地址' }]}
|
||||
>
|
||||
<Input placeholder="例如:https://github.com/user/repo.git" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<DeployModal
|
||||
visible={deployModalVisible}
|
||||
onCancel={() => setDeployModalVisible(false)}
|
||||
@@ -1286,6 +1432,7 @@ function ProjectDetailPage() {
|
||||
}}
|
||||
pipelines={pipelines}
|
||||
projectId={Number(id)}
|
||||
project={detail}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
import { type APIResponse, net } from '@shared';
|
||||
import type { Branch, Commit, Deployment, Pipeline, Project, Step, CreateDeploymentRequest } from '../types';
|
||||
import type {
|
||||
Branch,
|
||||
Commit,
|
||||
CreateDeploymentRequest,
|
||||
Deployment,
|
||||
Pipeline,
|
||||
Project,
|
||||
Step,
|
||||
} from '../types';
|
||||
|
||||
class DetailService {
|
||||
async getProject(id: string) {
|
||||
@@ -19,7 +27,9 @@ class DetailService {
|
||||
|
||||
// 获取可用的流水线模板
|
||||
async getPipelineTemplates() {
|
||||
const { data } = await net.request<APIResponse<{id: number, name: string, description: string}[]>>({
|
||||
const { data } = await net.request<
|
||||
APIResponse<{ id: number; name: string; description: string }[]>
|
||||
>({
|
||||
url: '/api/pipelines/templates',
|
||||
});
|
||||
return data;
|
||||
@@ -59,7 +69,7 @@ class DetailService {
|
||||
templateId: number,
|
||||
projectId: number,
|
||||
name: string,
|
||||
description?: string
|
||||
description?: string,
|
||||
) {
|
||||
const { data } = await net.request<APIResponse<Pipeline>>({
|
||||
url: '/api/pipelines/from-template',
|
||||
@@ -68,7 +78,7 @@ class DetailService {
|
||||
templateId,
|
||||
projectId,
|
||||
name,
|
||||
description
|
||||
description,
|
||||
},
|
||||
});
|
||||
return data;
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
import { Button, Form, Input, Message, Modal } from '@arco-design/web-react';
|
||||
import {
|
||||
Button,
|
||||
Collapse,
|
||||
Form,
|
||||
Input,
|
||||
Message,
|
||||
Modal,
|
||||
} from '@arco-design/web-react';
|
||||
import { useState } from 'react';
|
||||
import { projectService } from '../service';
|
||||
import EnvPresetsEditor from '../../detail/components/EnvPresetsEditor';
|
||||
import type { Project } from '../../types';
|
||||
import { projectService } from '../service';
|
||||
|
||||
interface CreateProjectModalProps {
|
||||
visible: boolean;
|
||||
@@ -22,7 +30,15 @@ function CreateProjectModal({
|
||||
const values = await form.validate();
|
||||
setLoading(true);
|
||||
|
||||
const newProject = await projectService.create(values);
|
||||
// 序列化环境预设
|
||||
const submitData = {
|
||||
...values,
|
||||
envPresets: values.envPresets
|
||||
? JSON.stringify(values.envPresets)
|
||||
: undefined,
|
||||
};
|
||||
|
||||
const newProject = await projectService.create(submitData);
|
||||
|
||||
Message.success('项目创建成功');
|
||||
onSuccess(newProject);
|
||||
@@ -114,7 +130,9 @@ function CreateProjectModal({
|
||||
if (value.includes('..') || value.includes('~')) {
|
||||
return cb('不能包含路径遍历字符(.. 或 ~)');
|
||||
}
|
||||
if (/[<>:"|?*\x00-\x1f]/.test(value)) {
|
||||
// 检查非法字符(控制字符 0x00-0x1F)
|
||||
// biome-ignore lint/suspicious/noControlCharactersInRegex: 需要检测路径中的控制字符
|
||||
if (/[<>:"|?*\u0000-\u001f]/.test(value)) {
|
||||
return cb('路径包含非法字符');
|
||||
}
|
||||
cb();
|
||||
@@ -124,6 +142,14 @@ function CreateProjectModal({
|
||||
>
|
||||
<Input placeholder="请输入绝对路径,如: /data/projects/my-app" />
|
||||
</Form.Item>
|
||||
|
||||
<Collapse defaultActiveKey={[]} style={{ marginTop: 16 }}>
|
||||
<Collapse.Item header="环境变量预设配置(可选)" name="envPresets">
|
||||
<Form.Item field="envPresets" noStyle>
|
||||
<EnvPresetsEditor />
|
||||
</Form.Item>
|
||||
</Collapse.Item>
|
||||
</Collapse>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,17 @@
|
||||
import { Button, Form, Input, Message, Modal } from '@arco-design/web-react';
|
||||
import {
|
||||
Button,
|
||||
Collapse,
|
||||
Form,
|
||||
Input,
|
||||
Message,
|
||||
Modal,
|
||||
} from '@arco-design/web-react';
|
||||
import React, { useState } from 'react';
|
||||
import EnvPresetsEditor, {
|
||||
type EnvPreset,
|
||||
} from '../../detail/components/EnvPresetsEditor';
|
||||
import type { Project } from '../../types';
|
||||
import { projectService } from '../service';
|
||||
import type { Project } from '../types';
|
||||
|
||||
interface EditProjectModalProps {
|
||||
visible: boolean;
|
||||
@@ -22,10 +32,20 @@ function EditProjectModal({
|
||||
// 当项目信息变化时,更新表单数据
|
||||
React.useEffect(() => {
|
||||
if (project && visible) {
|
||||
let envPresets: EnvPreset[] = [];
|
||||
try {
|
||||
if (project.envPresets) {
|
||||
envPresets = JSON.parse(project.envPresets);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('解析环境预设失败:', error);
|
||||
}
|
||||
|
||||
form.setFieldsValue({
|
||||
name: project.name,
|
||||
description: project.description,
|
||||
repository: project.repository,
|
||||
envPresets,
|
||||
});
|
||||
}
|
||||
}, [project, visible, form]);
|
||||
@@ -37,7 +57,18 @@ function EditProjectModal({
|
||||
|
||||
if (!project) return;
|
||||
|
||||
const updatedProject = await projectService.update(project.id, values);
|
||||
// 序列化环境预设
|
||||
const submitData = {
|
||||
...values,
|
||||
envPresets: values.envPresets
|
||||
? JSON.stringify(values.envPresets)
|
||||
: undefined,
|
||||
};
|
||||
|
||||
const updatedProject = await projectService.update(
|
||||
project.id,
|
||||
submitData,
|
||||
);
|
||||
|
||||
Message.success('项目更新成功');
|
||||
onSuccess(updatedProject);
|
||||
@@ -111,6 +142,14 @@ function EditProjectModal({
|
||||
>
|
||||
<Input placeholder="请输入仓库地址,如: https://github.com/user/repo" />
|
||||
</Form.Item>
|
||||
|
||||
<Collapse defaultActiveKey={[]} style={{ marginTop: 16 }}>
|
||||
<Collapse.Item header="环境变量预设配置" name="envPresets">
|
||||
<Form.Item field="envPresets" noStyle>
|
||||
<EnvPresetsEditor />
|
||||
</Form.Item>
|
||||
</Collapse.Item>
|
||||
</Collapse>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
Card,
|
||||
Space,
|
||||
Tag,
|
||||
Tooltip,
|
||||
Typography,
|
||||
} from '@arco-design/web-react';
|
||||
import {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Button, Grid, Message, Typography } from '@arco-design/web-react';
|
||||
import { Button, Grid, Typography } from '@arco-design/web-react';
|
||||
import { IconPlus } from '@arco-design/web-react/icon';
|
||||
import { useState } from 'react';
|
||||
import { useAsyncEffect } from '../../../hooks/useAsyncEffect';
|
||||
|
||||
@@ -36,6 +36,7 @@ export interface Project {
|
||||
description: string;
|
||||
repository: string;
|
||||
projectDir: string; // 项目工作目录路径(必填)
|
||||
envPresets?: string; // 环境预设配置(JSON格式)
|
||||
valid: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
@@ -77,12 +78,11 @@ export interface Pipeline {
|
||||
export interface Deployment {
|
||||
id: number;
|
||||
branch: string;
|
||||
env?: string;
|
||||
envVars?: string; // JSON 字符串
|
||||
status: string;
|
||||
commitHash?: string;
|
||||
commitMessage?: string;
|
||||
buildLog?: string;
|
||||
sparseCheckoutPaths?: string; // 稀疏检出路径,用于monorepo项目
|
||||
startedAt: string;
|
||||
finishedAt?: string;
|
||||
valid: number;
|
||||
@@ -127,6 +127,5 @@ export interface CreateDeploymentRequest {
|
||||
branch: string;
|
||||
commitHash: string;
|
||||
commitMessage: string;
|
||||
env?: string;
|
||||
sparseCheckoutPaths?: string; // 稀疏检出路径,用于monorepo项目
|
||||
envVars?: Record<string, string>; // 环境变量 key-value 对象
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user