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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user