feat: Introduce DTOs for API validation and new deployment features, including a Git controller and UI components.
This commit is contained in:
7
apps/server/controllers/auth/dto.ts
Normal file
7
apps/server/controllers/auth/dto.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
export const loginSchema = z.object({
|
||||||
|
code: z.string().min(1, { message: 'Code不能为空' }),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type LoginInput = z.infer<typeof loginSchema>;
|
||||||
@@ -3,6 +3,7 @@ import { Controller, Get, Post } from '../../decorators/route.ts';
|
|||||||
import { prisma } from '../../libs/prisma.ts';
|
import { prisma } from '../../libs/prisma.ts';
|
||||||
import { log } from '../../libs/logger.ts';
|
import { log } from '../../libs/logger.ts';
|
||||||
import { gitea } from '../../libs/gitea.ts';
|
import { gitea } from '../../libs/gitea.ts';
|
||||||
|
import { loginSchema } from './dto.ts';
|
||||||
|
|
||||||
@Controller('/auth')
|
@Controller('/auth')
|
||||||
export class AuthController {
|
export class AuthController {
|
||||||
@@ -20,7 +21,7 @@ export class AuthController {
|
|||||||
if (ctx.session.user) {
|
if (ctx.session.user) {
|
||||||
return ctx.session.user;
|
return ctx.session.user;
|
||||||
}
|
}
|
||||||
const { code } = ctx.request.body as LoginRequestBody;
|
const { code } = loginSchema.parse(ctx.request.body);
|
||||||
const { access_token, refresh_token, expires_in } =
|
const { access_token, refresh_token, expires_in } =
|
||||||
await gitea.getToken(code);
|
await gitea.getToken(code);
|
||||||
const giteaAuth = {
|
const giteaAuth = {
|
||||||
@@ -81,7 +82,3 @@ export class AuthController {
|
|||||||
return ctx.session?.user;
|
return ctx.session?.user;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LoginRequestBody {
|
|
||||||
code: string;
|
|
||||||
}
|
|
||||||
|
|||||||
19
apps/server/controllers/deployment/dto.ts
Normal file
19
apps/server/controllers/deployment/dto.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
export const listDeploymentsQuerySchema = z.object({
|
||||||
|
page: z.coerce.number().int().min(1).optional().default(1),
|
||||||
|
pageSize: z.coerce.number().int().min(1).max(100).optional().default(10),
|
||||||
|
projectId: z.coerce.number().int().positive().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const createDeploymentSchema = z.object({
|
||||||
|
projectId: z.number().int().positive({ message: '项目ID必须是正整数' }),
|
||||||
|
pipelineId: z.number().int().positive({ message: '流水线ID必须是正整数' }),
|
||||||
|
branch: z.string().min(1, { message: '分支不能为空' }),
|
||||||
|
commitHash: z.string().min(1, { message: '提交哈希不能为空' }),
|
||||||
|
commitMessage: z.string().min(1, { message: '提交信息不能为空' }),
|
||||||
|
env: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ListDeploymentsQuery = z.infer<typeof listDeploymentsQuerySchema>;
|
||||||
|
export type CreateDeploymentInput = z.infer<typeof createDeploymentSchema>;
|
||||||
@@ -1,45 +1,61 @@
|
|||||||
import { Controller, Get, Post } from '../../decorators/route.ts';
|
import { Controller, Get, Post } from '../../decorators/route.ts';
|
||||||
import type { Prisma } from '../../generated/prisma/index.js';
|
import type { Prisma } from '../../generated/client.ts';
|
||||||
import { prisma } from '../../libs/prisma.ts';
|
import { prisma } from '../../libs/prisma.ts';
|
||||||
import type { Context } from 'koa';
|
import type { Context } from 'koa';
|
||||||
|
import { listDeploymentsQuerySchema, createDeploymentSchema } from './dto.ts';
|
||||||
|
|
||||||
@Controller('/deployments')
|
@Controller('/deployments')
|
||||||
export class DeploymentController {
|
export class DeploymentController {
|
||||||
@Get('')
|
@Get('')
|
||||||
async list(ctx: Context) {
|
async list(ctx: Context) {
|
||||||
const { page = 1, pageSize = 10 } = ctx.query;
|
const { page, pageSize, projectId } = listDeploymentsQuerySchema.parse(ctx.query);
|
||||||
|
const where: Prisma.DeploymentWhereInput = {
|
||||||
|
valid: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (projectId) {
|
||||||
|
where.projectId = projectId;
|
||||||
|
}
|
||||||
|
|
||||||
const result = await prisma.deployment.findMany({
|
const result = await prisma.deployment.findMany({
|
||||||
where: {
|
where,
|
||||||
valid: 1,
|
take: pageSize,
|
||||||
},
|
skip: (page - 1) * pageSize,
|
||||||
take: Number(pageSize),
|
|
||||||
skip: (Number(page) - 1) * Number(pageSize),
|
|
||||||
orderBy: {
|
orderBy: {
|
||||||
createdAt: 'desc',
|
createdAt: 'desc',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const total = await prisma.deployment.count();
|
const total = await prisma.deployment.count({ where });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data: result,
|
data: result,
|
||||||
page: Number(page),
|
page,
|
||||||
pageSize: Number(pageSize),
|
pageSize,
|
||||||
total: total,
|
total,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('')
|
@Post('')
|
||||||
async create(ctx: Context) {
|
async create(ctx: Context) {
|
||||||
const body = ctx.request.body as Prisma.DeploymentCreateInput;
|
const body = createDeploymentSchema.parse(ctx.request.body);
|
||||||
|
|
||||||
prisma.deployment.create({
|
const result = await prisma.deployment.create({
|
||||||
data: {
|
data: {
|
||||||
branch: body.branch,
|
branch: body.branch,
|
||||||
commitHash: body.commitHash,
|
commitHash: body.commitHash,
|
||||||
commitMessage: body.commitMessage,
|
commitMessage: body.commitMessage,
|
||||||
|
status: 'pending',
|
||||||
|
Project: {
|
||||||
|
connect: { id: body.projectId },
|
||||||
|
},
|
||||||
|
pipelineId: body.pipelineId,
|
||||||
|
env: body.env || 'dev',
|
||||||
|
buildLog: '',
|
||||||
|
createdBy: 'system', // TODO: get from user
|
||||||
|
updatedBy: 'system',
|
||||||
valid: 1,
|
valid: 1,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
13
apps/server/controllers/git/dto.ts
Normal file
13
apps/server/controllers/git/dto.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
export const getCommitsQuerySchema = z.object({
|
||||||
|
projectId: z.coerce.number().int().positive({ message: 'Project ID is required' }),
|
||||||
|
branch: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const getBranchesQuerySchema = z.object({
|
||||||
|
projectId: z.coerce.number().int().positive({ message: 'Project ID is required' }),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type GetCommitsQuery = z.infer<typeof getCommitsQuerySchema>;
|
||||||
|
export type GetBranchesQuery = z.infer<typeof getBranchesQuerySchema>;
|
||||||
113
apps/server/controllers/git/index.ts
Normal file
113
apps/server/controllers/git/index.ts
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import type { Context } from 'koa';
|
||||||
|
import { Controller, Get } from '../../decorators/route.ts';
|
||||||
|
import { prisma } from '../../libs/prisma.ts';
|
||||||
|
import { gitea } from '../../libs/gitea.ts';
|
||||||
|
import { BusinessError } from '../../middlewares/exception.ts';
|
||||||
|
import { getCommitsQuerySchema, getBranchesQuerySchema } from './dto.ts';
|
||||||
|
|
||||||
|
@Controller('/git')
|
||||||
|
export class GitController {
|
||||||
|
@Get('/commits')
|
||||||
|
async getCommits(ctx: Context) {
|
||||||
|
const { projectId, branch } = getCommitsQuerySchema.parse(ctx.query);
|
||||||
|
|
||||||
|
const project = await prisma.project.findFirst({
|
||||||
|
where: {
|
||||||
|
id: projectId,
|
||||||
|
valid: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!project) {
|
||||||
|
throw new BusinessError('Project not found', 1002, 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse repository URL to get owner and repo
|
||||||
|
// Supports:
|
||||||
|
// https://gitea.com/owner/repo.git
|
||||||
|
// http://gitea.com/owner/repo
|
||||||
|
const { owner, repo } = this.parseRepoUrl(project.repository);
|
||||||
|
|
||||||
|
// Get access token from session
|
||||||
|
const accessToken = ctx.session?.gitea?.access_token;
|
||||||
|
console.log('Access token present:', !!accessToken);
|
||||||
|
|
||||||
|
if (!accessToken) {
|
||||||
|
throw new BusinessError('Gitea access token not found. Please login again.', 1004, 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const commits = await gitea.getCommits(owner, repo, accessToken, branch);
|
||||||
|
return commits;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch commits:', error);
|
||||||
|
throw new BusinessError('Failed to fetch commits from Gitea', 1005, 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('/branches')
|
||||||
|
async getBranches(ctx: Context) {
|
||||||
|
const { projectId } = getBranchesQuerySchema.parse(ctx.query);
|
||||||
|
|
||||||
|
const project = await prisma.project.findFirst({
|
||||||
|
where: {
|
||||||
|
id: projectId,
|
||||||
|
valid: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!project) {
|
||||||
|
throw new BusinessError('Project not found', 1002, 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { owner, repo } = this.parseRepoUrl(project.repository);
|
||||||
|
|
||||||
|
const accessToken = ctx.session?.gitea?.access_token;
|
||||||
|
|
||||||
|
if (!accessToken) {
|
||||||
|
throw new BusinessError('Gitea access token not found. Please login again.', 1004, 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const branches = await gitea.getBranches(owner, repo, accessToken);
|
||||||
|
return branches;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch branches:', error);
|
||||||
|
throw new BusinessError('Failed to fetch branches from Gitea', 1006, 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseRepoUrl(url: string) {
|
||||||
|
let cleanUrl = url.trim();
|
||||||
|
if (cleanUrl.endsWith('/')) {
|
||||||
|
cleanUrl = cleanUrl.slice(0, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle SCP-like syntax: git@host:owner/repo.git
|
||||||
|
if (!cleanUrl.includes('://') && cleanUrl.includes(':')) {
|
||||||
|
const scpMatch = cleanUrl.match(/:([^\/]+)\/([^\/]+?)(\.git)?$/);
|
||||||
|
if (scpMatch) {
|
||||||
|
return { owner: scpMatch[1], repo: scpMatch[2] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle HTTP/HTTPS/SSH URLs
|
||||||
|
try {
|
||||||
|
const urlObj = new URL(cleanUrl);
|
||||||
|
const parts = urlObj.pathname.split('/').filter(Boolean);
|
||||||
|
if (parts.length >= 2) {
|
||||||
|
const repo = parts.pop()!.replace(/\.git$/, '');
|
||||||
|
const owner = parts.pop()!;
|
||||||
|
return { owner, repo };
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Fallback to simple regex
|
||||||
|
const match = cleanUrl.match(/([^\/]+)\/([^\/]+?)(\.git)?$/);
|
||||||
|
if (match) {
|
||||||
|
return { owner: match[1], repo: match[2] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new BusinessError('Invalid repository URL format', 1003, 400);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,3 +5,4 @@ export { AuthController } from './auth/index.ts';
|
|||||||
export { DeploymentController } from './deployment/index.ts';
|
export { DeploymentController } from './deployment/index.ts';
|
||||||
export { PipelineController } from './pipeline/index.ts';
|
export { PipelineController } from './pipeline/index.ts';
|
||||||
export { StepController } from './step/index.ts'
|
export { StepController } from './step/index.ts'
|
||||||
|
export { GitController } from './git/index.ts';
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
updatePipelineSchema,
|
updatePipelineSchema,
|
||||||
pipelineIdSchema,
|
pipelineIdSchema,
|
||||||
listPipelinesQuerySchema,
|
listPipelinesQuerySchema,
|
||||||
} from './schema.ts';
|
} from './dto.ts';
|
||||||
|
|
||||||
@Controller('/pipelines')
|
@Controller('/pipelines')
|
||||||
export class PipelineController {
|
export class PipelineController {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
updateProjectSchema,
|
updateProjectSchema,
|
||||||
listProjectQuerySchema,
|
listProjectQuerySchema,
|
||||||
projectIdSchema,
|
projectIdSchema,
|
||||||
} from './schema.ts';
|
} from './dto.ts';
|
||||||
|
|
||||||
@Controller('/projects')
|
@Controller('/projects')
|
||||||
export class ProjectController {
|
export class ProjectController {
|
||||||
|
|||||||
103
apps/server/controllers/step/dto.ts
Normal file
103
apps/server/controllers/step/dto.ts
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
// 定义验证架构
|
||||||
|
export const createStepSchema = z.object({
|
||||||
|
name: z
|
||||||
|
.string({
|
||||||
|
message: '步骤名称必须是字符串',
|
||||||
|
})
|
||||||
|
.min(1, { message: '步骤名称不能为空' })
|
||||||
|
.max(100, { message: '步骤名称不能超过100个字符' }),
|
||||||
|
|
||||||
|
description: z
|
||||||
|
.string({
|
||||||
|
message: '步骤描述必须是字符串',
|
||||||
|
})
|
||||||
|
.max(500, { message: '步骤描述不能超过500个字符' })
|
||||||
|
.optional(),
|
||||||
|
|
||||||
|
order: z
|
||||||
|
.number({
|
||||||
|
message: '步骤顺序必须是数字',
|
||||||
|
})
|
||||||
|
.int()
|
||||||
|
.min(0, { message: '步骤顺序必须是非负整数' }),
|
||||||
|
|
||||||
|
script: z
|
||||||
|
.string({
|
||||||
|
message: '脚本命令必须是字符串',
|
||||||
|
})
|
||||||
|
.min(1, { message: '脚本命令不能为空' }),
|
||||||
|
|
||||||
|
pipelineId: z
|
||||||
|
.number({
|
||||||
|
message: '流水线ID必须是数字',
|
||||||
|
})
|
||||||
|
.int()
|
||||||
|
.positive({ message: '流水线ID必须是正整数' }),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const updateStepSchema = z.object({
|
||||||
|
name: z
|
||||||
|
.string({
|
||||||
|
message: '步骤名称必须是字符串',
|
||||||
|
})
|
||||||
|
.min(1, { message: '步骤名称不能为空' })
|
||||||
|
.max(100, { message: '步骤名称不能超过100个字符' })
|
||||||
|
.optional(),
|
||||||
|
|
||||||
|
description: z
|
||||||
|
.string({
|
||||||
|
message: '步骤描述必须是字符串',
|
||||||
|
})
|
||||||
|
.max(500, { message: '步骤描述不能超过500个字符' })
|
||||||
|
.optional(),
|
||||||
|
|
||||||
|
order: z
|
||||||
|
.number({
|
||||||
|
message: '步骤顺序必须是数字',
|
||||||
|
})
|
||||||
|
.int()
|
||||||
|
.min(0, { message: '步骤顺序必须是非负整数' })
|
||||||
|
.optional(),
|
||||||
|
|
||||||
|
script: z
|
||||||
|
.string({
|
||||||
|
message: '脚本命令必须是字符串',
|
||||||
|
})
|
||||||
|
.min(1, { message: '脚本命令不能为空' })
|
||||||
|
.optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const stepIdSchema = z.object({
|
||||||
|
id: z.coerce.number().int().positive({ message: '步骤 ID 必须是正整数' }),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const listStepsQuerySchema = z
|
||||||
|
.object({
|
||||||
|
pipelineId: z.coerce
|
||||||
|
.number()
|
||||||
|
.int()
|
||||||
|
.positive({ message: '流水线ID必须是正整数' })
|
||||||
|
.optional(),
|
||||||
|
page: z.coerce
|
||||||
|
.number()
|
||||||
|
.int()
|
||||||
|
.min(1, { message: '页码必须大于0' })
|
||||||
|
.optional()
|
||||||
|
.default(1),
|
||||||
|
limit: z.coerce
|
||||||
|
.number()
|
||||||
|
.int()
|
||||||
|
.min(1, { message: '每页数量必须大于0' })
|
||||||
|
.max(100, { message: '每页数量不能超过100' })
|
||||||
|
.optional()
|
||||||
|
.default(10),
|
||||||
|
})
|
||||||
|
.optional();
|
||||||
|
|
||||||
|
// TypeScript 类型
|
||||||
|
export type CreateStepInput = z.infer<typeof createStepSchema>;
|
||||||
|
export type UpdateStepInput = z.infer<typeof updateStepSchema>;
|
||||||
|
export type StepIdParams = z.infer<typeof stepIdSchema>;
|
||||||
|
export type ListStepsQuery = z.infer<typeof listStepsQuerySchema>;
|
||||||
@@ -3,109 +3,12 @@ import { prisma } from '../../libs/prisma.ts';
|
|||||||
import { log } from '../../libs/logger.ts';
|
import { log } from '../../libs/logger.ts';
|
||||||
import { BusinessError } from '../../middlewares/exception.ts';
|
import { BusinessError } from '../../middlewares/exception.ts';
|
||||||
import { Controller, Get, Post, Put, Delete } from '../../decorators/route.ts';
|
import { Controller, Get, Post, Put, Delete } from '../../decorators/route.ts';
|
||||||
import { z } from 'zod';
|
import {
|
||||||
|
createStepSchema,
|
||||||
// 定义验证架构
|
updateStepSchema,
|
||||||
const createStepSchema = z.object({
|
stepIdSchema,
|
||||||
name: z
|
listStepsQuerySchema,
|
||||||
.string({
|
} from './dto.ts';
|
||||||
message: '步骤名称必须是字符串',
|
|
||||||
})
|
|
||||||
.min(1, { message: '步骤名称不能为空' })
|
|
||||||
.max(100, { message: '步骤名称不能超过100个字符' }),
|
|
||||||
|
|
||||||
description: z
|
|
||||||
.string({
|
|
||||||
message: '步骤描述必须是字符串',
|
|
||||||
})
|
|
||||||
.max(500, { message: '步骤描述不能超过500个字符' })
|
|
||||||
.optional(),
|
|
||||||
|
|
||||||
order: z
|
|
||||||
.number({
|
|
||||||
message: '步骤顺序必须是数字',
|
|
||||||
})
|
|
||||||
.int()
|
|
||||||
.min(0, { message: '步骤顺序必须是非负整数' }),
|
|
||||||
|
|
||||||
script: z
|
|
||||||
.string({
|
|
||||||
message: '脚本命令必须是字符串',
|
|
||||||
})
|
|
||||||
.min(1, { message: '脚本命令不能为空' }),
|
|
||||||
|
|
||||||
pipelineId: z
|
|
||||||
.number({
|
|
||||||
message: '流水线ID必须是数字',
|
|
||||||
})
|
|
||||||
.int()
|
|
||||||
.positive({ message: '流水线ID必须是正整数' }),
|
|
||||||
});
|
|
||||||
|
|
||||||
const updateStepSchema = z.object({
|
|
||||||
name: z
|
|
||||||
.string({
|
|
||||||
message: '步骤名称必须是字符串',
|
|
||||||
})
|
|
||||||
.min(1, { message: '步骤名称不能为空' })
|
|
||||||
.max(100, { message: '步骤名称不能超过100个字符' })
|
|
||||||
.optional(),
|
|
||||||
|
|
||||||
description: z
|
|
||||||
.string({
|
|
||||||
message: '步骤描述必须是字符串',
|
|
||||||
})
|
|
||||||
.max(500, { message: '步骤描述不能超过500个字符' })
|
|
||||||
.optional(),
|
|
||||||
|
|
||||||
order: z
|
|
||||||
.number({
|
|
||||||
message: '步骤顺序必须是数字',
|
|
||||||
})
|
|
||||||
.int()
|
|
||||||
.min(0, { message: '步骤顺序必须是非负整数' })
|
|
||||||
.optional(),
|
|
||||||
|
|
||||||
script: z
|
|
||||||
.string({
|
|
||||||
message: '脚本命令必须是字符串',
|
|
||||||
})
|
|
||||||
.min(1, { message: '脚本命令不能为空' })
|
|
||||||
.optional(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const stepIdSchema = z.object({
|
|
||||||
id: z.coerce.number().int().positive({ message: '步骤 ID 必须是正整数' }),
|
|
||||||
});
|
|
||||||
|
|
||||||
const listStepsQuerySchema = z
|
|
||||||
.object({
|
|
||||||
pipelineId: z.coerce
|
|
||||||
.number()
|
|
||||||
.int()
|
|
||||||
.positive({ message: '流水线ID必须是正整数' })
|
|
||||||
.optional(),
|
|
||||||
page: z.coerce
|
|
||||||
.number()
|
|
||||||
.int()
|
|
||||||
.min(1, { message: '页码必须大于0' })
|
|
||||||
.optional()
|
|
||||||
.default(1),
|
|
||||||
limit: z.coerce
|
|
||||||
.number()
|
|
||||||
.int()
|
|
||||||
.min(1, { message: '每页数量必须大于0' })
|
|
||||||
.max(100, { message: '每页数量不能超过100' })
|
|
||||||
.optional()
|
|
||||||
.default(10),
|
|
||||||
})
|
|
||||||
.optional();
|
|
||||||
|
|
||||||
// TypeScript 类型
|
|
||||||
type CreateStepInput = z.infer<typeof createStepSchema>;
|
|
||||||
type UpdateStepInput = z.infer<typeof updateStepSchema>;
|
|
||||||
type StepIdParams = z.infer<typeof stepIdSchema>;
|
|
||||||
type ListStepsQuery = z.infer<typeof listStepsQuerySchema>;
|
|
||||||
|
|
||||||
@Controller('/steps')
|
@Controller('/steps')
|
||||||
export class StepController {
|
export class StepController {
|
||||||
@@ -185,7 +88,6 @@ export class StepController {
|
|||||||
const step = await prisma.step.create({
|
const step = await prisma.step.create({
|
||||||
data: {
|
data: {
|
||||||
name: validatedData.name,
|
name: validatedData.name,
|
||||||
description: validatedData.description || '',
|
|
||||||
order: validatedData.order,
|
order: validatedData.order,
|
||||||
script: validatedData.script,
|
script: validatedData.script,
|
||||||
pipelineId: validatedData.pipelineId,
|
pipelineId: validatedData.pipelineId,
|
||||||
|
|||||||
26
apps/server/controllers/user/dto.ts
Normal file
26
apps/server/controllers/user/dto.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
export const userIdSchema = z.object({
|
||||||
|
id: z.coerce.number().int().positive({ message: '用户ID必须是正整数' }),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const createUserSchema = z.object({
|
||||||
|
name: z.string().min(1, { message: '用户名不能为空' }),
|
||||||
|
email: z.string().email({ message: '邮箱格式不正确' }),
|
||||||
|
status: z.enum(['active', 'inactive']).optional().default('active'),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const updateUserSchema = z.object({
|
||||||
|
name: z.string().min(1).optional(),
|
||||||
|
email: z.string().email().optional(),
|
||||||
|
status: z.enum(['active', 'inactive']).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const searchUserQuerySchema = z.object({
|
||||||
|
keyword: z.string().optional(),
|
||||||
|
status: z.enum(['active', 'inactive']).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type CreateUserInput = z.infer<typeof createUserSchema>;
|
||||||
|
export type UpdateUserInput = z.infer<typeof updateUserSchema>;
|
||||||
|
export type SearchUserQuery = z.infer<typeof searchUserQuerySchema>;
|
||||||
@@ -1,6 +1,12 @@
|
|||||||
import type { Context } from 'koa';
|
import type { Context } from 'koa';
|
||||||
import { Controller, Get, Post, Put, Delete } from '../../decorators/route.ts';
|
import { Controller, Get, Post, Put, Delete } from '../../decorators/route.ts';
|
||||||
import { BusinessError } from '../../middlewares/exception.ts';
|
import { BusinessError } from '../../middlewares/exception.ts';
|
||||||
|
import {
|
||||||
|
userIdSchema,
|
||||||
|
createUserSchema,
|
||||||
|
updateUserSchema,
|
||||||
|
searchUserQuerySchema,
|
||||||
|
} from './dto.ts';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 用户控制器
|
* 用户控制器
|
||||||
@@ -22,18 +28,18 @@ export class UserController {
|
|||||||
|
|
||||||
@Get('/detail/:id')
|
@Get('/detail/:id')
|
||||||
async detail(ctx: Context) {
|
async detail(ctx: Context) {
|
||||||
const { id } = ctx.params;
|
const { id } = userIdSchema.parse(ctx.params);
|
||||||
|
|
||||||
// 模拟根据ID查找用户
|
// 模拟根据ID查找用户
|
||||||
const user = {
|
const user = {
|
||||||
id: Number(id),
|
id,
|
||||||
name: 'User ' + id,
|
name: 'User ' + id,
|
||||||
email: `user${id}@example.com`,
|
email: `user${id}@example.com`,
|
||||||
status: 'active',
|
status: 'active',
|
||||||
createdAt: new Date().toISOString()
|
createdAt: new Date().toISOString()
|
||||||
};
|
};
|
||||||
|
|
||||||
if (Number(id) > 100) {
|
if (id > 100) {
|
||||||
throw new BusinessError('用户不存在', 2001, 404);
|
throw new BusinessError('用户不存在', 2001, 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,14 +48,14 @@ export class UserController {
|
|||||||
|
|
||||||
@Post('')
|
@Post('')
|
||||||
async create(ctx: Context) {
|
async create(ctx: Context) {
|
||||||
const body = (ctx.request as any).body;
|
const body = createUserSchema.parse(ctx.request.body);
|
||||||
|
|
||||||
// 模拟创建用户
|
// 模拟创建用户
|
||||||
const newUser = {
|
const newUser = {
|
||||||
id: Date.now(),
|
id: Date.now(),
|
||||||
...body,
|
...body,
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
status: 'active'
|
status: body.status
|
||||||
};
|
};
|
||||||
|
|
||||||
return newUser;
|
return newUser;
|
||||||
@@ -57,12 +63,12 @@ export class UserController {
|
|||||||
|
|
||||||
@Put('/:id')
|
@Put('/:id')
|
||||||
async update(ctx: Context) {
|
async update(ctx: Context) {
|
||||||
const { id } = ctx.params;
|
const { id } = userIdSchema.parse(ctx.params);
|
||||||
const body = (ctx.request as any).body;
|
const body = updateUserSchema.parse(ctx.request.body);
|
||||||
|
|
||||||
// 模拟更新用户
|
// 模拟更新用户
|
||||||
const updatedUser = {
|
const updatedUser = {
|
||||||
id: Number(id),
|
id,
|
||||||
...body,
|
...body,
|
||||||
updatedAt: new Date().toISOString()
|
updatedAt: new Date().toISOString()
|
||||||
};
|
};
|
||||||
@@ -72,9 +78,9 @@ export class UserController {
|
|||||||
|
|
||||||
@Delete('/:id')
|
@Delete('/:id')
|
||||||
async delete(ctx: Context) {
|
async delete(ctx: Context) {
|
||||||
const { id } = ctx.params;
|
const { id } = userIdSchema.parse(ctx.params);
|
||||||
|
|
||||||
if (Number(id) === 1) {
|
if (id === 1) {
|
||||||
throw new BusinessError('管理员账户不能删除', 2002, 403);
|
throw new BusinessError('管理员账户不能删除', 2002, 403);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,7 +94,7 @@ export class UserController {
|
|||||||
|
|
||||||
@Get('/search')
|
@Get('/search')
|
||||||
async search(ctx: Context) {
|
async search(ctx: Context) {
|
||||||
const { keyword, status } = ctx.query;
|
const { keyword, status } = searchUserQuerySchema.parse(ctx.query);
|
||||||
|
|
||||||
// 模拟搜索逻辑
|
// 模拟搜索逻辑
|
||||||
let results = [
|
let results = [
|
||||||
@@ -98,8 +104,8 @@ export class UserController {
|
|||||||
|
|
||||||
if (keyword) {
|
if (keyword) {
|
||||||
results = results.filter(user =>
|
results = results.filter(user =>
|
||||||
user.name.toLowerCase().includes(String(keyword).toLowerCase()) ||
|
user.name.toLowerCase().includes(keyword.toLowerCase()) ||
|
||||||
user.email.toLowerCase().includes(String(keyword).toLowerCase())
|
user.email.toLowerCase().includes(keyword.toLowerCase())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -80,6 +80,54 @@ class Gitea {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取仓库分支列表
|
||||||
|
* @param owner 仓库拥有者
|
||||||
|
* @param repo 仓库名称
|
||||||
|
* @param accessToken 访问令牌
|
||||||
|
*/
|
||||||
|
async getBranches(owner: string, repo: string, accessToken: string) {
|
||||||
|
const response = await fetch(
|
||||||
|
`${this.config.giteaUrl}/api/v1/repos/${owner}/${repo}/branches`,
|
||||||
|
{
|
||||||
|
method: 'GET',
|
||||||
|
headers: this.getHeaders(accessToken),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Fetch failed: ${response.status}`);
|
||||||
|
}
|
||||||
|
const result = await response.json();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取仓库提交记录
|
||||||
|
* @param owner 仓库拥有者
|
||||||
|
* @param repo 仓库名称
|
||||||
|
* @param accessToken 访问令牌
|
||||||
|
* @param sha 分支名称或提交SHA
|
||||||
|
*/
|
||||||
|
async getCommits(owner: string, repo: string, accessToken: string, sha?: string) {
|
||||||
|
const url = new URL(`${this.config.giteaUrl}/api/v1/repos/${owner}/${repo}/commits`);
|
||||||
|
if (sha) {
|
||||||
|
url.searchParams.append('sha', sha);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
url.toString(),
|
||||||
|
{
|
||||||
|
method: 'GET',
|
||||||
|
headers: this.getHeaders(accessToken),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Fetch failed: ${response.status}`);
|
||||||
|
}
|
||||||
|
const result = await response.json();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
private getHeaders(accessToken?: string) {
|
private getHeaders(accessToken?: string) {
|
||||||
const headers: Record<string, string> = {
|
const headers: Record<string, string> = {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ import {
|
|||||||
AuthController,
|
AuthController,
|
||||||
DeploymentController,
|
DeploymentController,
|
||||||
PipelineController,
|
PipelineController,
|
||||||
StepController
|
StepController,
|
||||||
|
GitController
|
||||||
} from '../controllers/index.ts';
|
} from '../controllers/index.ts';
|
||||||
import { log } from '../libs/logger.ts';
|
import { log } from '../libs/logger.ts';
|
||||||
|
|
||||||
@@ -43,7 +44,8 @@ export class Router implements Middleware {
|
|||||||
AuthController,
|
AuthController,
|
||||||
DeploymentController,
|
DeploymentController,
|
||||||
PipelineController,
|
PipelineController,
|
||||||
StepController
|
StepController,
|
||||||
|
GitController
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// 输出注册的路由信息
|
// 输出注册的路由信息
|
||||||
|
|||||||
Binary file not shown.
243
apps/web/src/pages/project/detail/components/DeployModal.tsx
Normal file
243
apps/web/src/pages/project/detail/components/DeployModal.tsx
Normal file
@@ -0,0 +1,243 @@
|
|||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Form,
|
||||||
|
Input,
|
||||||
|
Message,
|
||||||
|
Modal,
|
||||||
|
Select,
|
||||||
|
} from '@arco-design/web-react';
|
||||||
|
import { IconDelete, IconPlus } from '@arco-design/web-react/icon';
|
||||||
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
import type { Branch, Commit, Pipeline } from '../../types';
|
||||||
|
import { detailService } from '../service';
|
||||||
|
|
||||||
|
interface DeployModalProps {
|
||||||
|
visible: boolean;
|
||||||
|
onCancel: () => void;
|
||||||
|
onOk: () => void;
|
||||||
|
pipelines: Pipeline[];
|
||||||
|
projectId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DeployModal({
|
||||||
|
visible,
|
||||||
|
onCancel,
|
||||||
|
onOk,
|
||||||
|
pipelines,
|
||||||
|
projectId,
|
||||||
|
}: 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 fetchCommits = useCallback(
|
||||||
|
async (branch: string) => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const data = await detailService.getCommits(projectId, branch);
|
||||||
|
setCommits(data);
|
||||||
|
if (data.length > 0) {
|
||||||
|
form.setFieldValue('commitHash', data[0].sha);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取提交记录失败:', error);
|
||||||
|
Message.error('获取提交记录失败');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[projectId, form],
|
||||||
|
);
|
||||||
|
|
||||||
|
const fetchBranches = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
setBranchLoading(true);
|
||||||
|
const data = await detailService.getBranches(projectId);
|
||||||
|
setBranches(data);
|
||||||
|
// 默认选中 master 或 main
|
||||||
|
const defaultBranch = data.find(
|
||||||
|
(b) => b.name === 'master' || b.name === 'main',
|
||||||
|
);
|
||||||
|
if (defaultBranch) {
|
||||||
|
form.setFieldValue('branch', defaultBranch.name);
|
||||||
|
fetchCommits(defaultBranch.name);
|
||||||
|
} else if (data.length > 0) {
|
||||||
|
form.setFieldValue('branch', data[0].name);
|
||||||
|
fetchCommits(data[0].name);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取分支列表失败:', error);
|
||||||
|
Message.error('获取分支列表失败');
|
||||||
|
} finally {
|
||||||
|
setBranchLoading(false);
|
||||||
|
}
|
||||||
|
}, [projectId, form, fetchCommits]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (visible && projectId) {
|
||||||
|
fetchBranches();
|
||||||
|
}
|
||||||
|
}, [visible, projectId, fetchBranches]);
|
||||||
|
|
||||||
|
const handleBranchChange = (value: string) => {
|
||||||
|
fetchCommits(value);
|
||||||
|
form.setFieldValue('commitHash', undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
try {
|
||||||
|
const values = await form.validate();
|
||||||
|
const selectedCommit = commits.find((c) => c.sha === values.commitHash);
|
||||||
|
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');
|
||||||
|
|
||||||
|
await detailService.createDeployment({
|
||||||
|
projectId,
|
||||||
|
pipelineId: values.pipelineId,
|
||||||
|
branch: values.branch,
|
||||||
|
commitHash: selectedCommit.sha,
|
||||||
|
commitMessage: selectedCommit.commit.message,
|
||||||
|
env: env,
|
||||||
|
});
|
||||||
|
|
||||||
|
Message.success('部署任务已创建');
|
||||||
|
onOk();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('创建部署失败:', error);
|
||||||
|
Message.error('创建部署失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title="开始部署"
|
||||||
|
visible={visible}
|
||||||
|
onOk={handleSubmit}
|
||||||
|
onCancel={onCancel}
|
||||||
|
autoFocus={false}
|
||||||
|
focusLock={true}
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<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">
|
||||||
|
{new Date(commit.commit.author.date).toLocaleString()}
|
||||||
|
</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 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>
|
||||||
|
))}
|
||||||
|
<Button
|
||||||
|
type="dashed"
|
||||||
|
long
|
||||||
|
onClick={() => add()}
|
||||||
|
icon={<IconPlus />}
|
||||||
|
>
|
||||||
|
添加环境变量
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Form.List>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DeployModal;
|
||||||
@@ -1,17 +1,8 @@
|
|||||||
import { List, Space, Tag } from '@arco-design/web-react';
|
import { List, Space, Tag } from '@arco-design/web-react';
|
||||||
|
import type { Deployment } from '../../types';
|
||||||
// 部署记录类型定义
|
|
||||||
interface DeployRecord {
|
|
||||||
id: number;
|
|
||||||
branch: string;
|
|
||||||
env: string;
|
|
||||||
commit: string;
|
|
||||||
status: 'success' | 'running' | 'failed' | 'pending';
|
|
||||||
createdAt: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DeployRecordItemProps {
|
interface DeployRecordItemProps {
|
||||||
item: DeployRecord;
|
item: Deployment;
|
||||||
isSelected: boolean;
|
isSelected: boolean;
|
||||||
onSelect: (id: number) => void;
|
onSelect: (id: number) => void;
|
||||||
}
|
}
|
||||||
@@ -22,11 +13,8 @@ function DeployRecordItem({
|
|||||||
onSelect,
|
onSelect,
|
||||||
}: DeployRecordItemProps) {
|
}: DeployRecordItemProps) {
|
||||||
// 状态标签渲染函数
|
// 状态标签渲染函数
|
||||||
const getStatusTag = (status: DeployRecord['status']) => {
|
const getStatusTag = (status: Deployment['status']) => {
|
||||||
const statusMap: Record<
|
const statusMap: Record<string, { color: string; text: string }> = {
|
||||||
DeployRecord['status'],
|
|
||||||
{ color: string; text: string }
|
|
||||||
> = {
|
|
||||||
success: { color: 'green', text: '成功' },
|
success: { color: 'green', text: '成功' },
|
||||||
running: { color: 'blue', text: '运行中' },
|
running: { color: 'blue', text: '运行中' },
|
||||||
failed: { color: 'red', text: '失败' },
|
failed: { color: 'red', text: '失败' },
|
||||||
@@ -67,7 +55,7 @@ function DeployRecordItem({
|
|||||||
#{item.id}
|
#{item.id}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-gray-600 text-sm font-mono bg-gray-100 px-2 py-1 rounded">
|
<span className="text-gray-600 text-sm font-mono bg-gray-100 px-2 py-1 rounded">
|
||||||
{item.commit}
|
{item.commitHash?.substring(0, 7)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -79,7 +67,7 @@ function DeployRecordItem({
|
|||||||
<span className="font-medium text-gray-700">{item.branch}</span>
|
<span className="font-medium text-gray-700">{item.branch}</span>
|
||||||
</span>
|
</span>
|
||||||
<span className="text-sm text-gray-500">
|
<span className="text-sm text-gray-500">
|
||||||
环境: {getEnvTag(item.env)}
|
环境: {getEnvTag(item.env || 'unknown')}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-sm text-gray-500">
|
<span className="text-sm text-gray-500">
|
||||||
状态: {getStatusTag(item.status)}
|
状态: {getStatusTag(item.status)}
|
||||||
|
|||||||
@@ -6,29 +6,18 @@ import {
|
|||||||
} from '@arco-design/web-react/icon';
|
} from '@arco-design/web-react/icon';
|
||||||
import { useSortable } from '@dnd-kit/sortable';
|
import { useSortable } from '@dnd-kit/sortable';
|
||||||
import { CSS } from '@dnd-kit/utilities';
|
import { CSS } from '@dnd-kit/utilities';
|
||||||
|
import type { Step } from '../../types';
|
||||||
|
|
||||||
// 流水线步骤类型定义(更新为与后端一致)
|
interface StepWithEnabled extends Step {
|
||||||
interface PipelineStep {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
description?: string;
|
|
||||||
order: number;
|
|
||||||
script: string; // 执行的脚本命令
|
|
||||||
valid: number;
|
|
||||||
createdAt: string;
|
|
||||||
updatedAt: string;
|
|
||||||
createdBy: string;
|
|
||||||
updatedBy: string;
|
|
||||||
pipelineId: number;
|
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PipelineStepItemProps {
|
interface PipelineStepItemProps {
|
||||||
step: PipelineStep;
|
step: StepWithEnabled;
|
||||||
index: number;
|
index: number;
|
||||||
pipelineId: number;
|
pipelineId: number;
|
||||||
onToggle: (pipelineId: number, stepId: number, enabled: boolean) => void;
|
onToggle: (pipelineId: number, stepId: number, enabled: boolean) => void;
|
||||||
onEdit: (pipelineId: number, step: PipelineStep) => void;
|
onEdit: (pipelineId: number, step: StepWithEnabled) => void;
|
||||||
onDelete: (pipelineId: number, stepId: number) => void;
|
onDelete: (pipelineId: number, stepId: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,52 +40,23 @@ import {
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useParams } from 'react-router';
|
import { useParams } from 'react-router';
|
||||||
import { useAsyncEffect } from '../../../hooks/useAsyncEffect';
|
import { useAsyncEffect } from '../../../hooks/useAsyncEffect';
|
||||||
import type { Project } from '../types';
|
import type { Deployment, Pipeline, Project, Step } from '../types';
|
||||||
|
import DeployModal from './components/DeployModal';
|
||||||
import DeployRecordItem from './components/DeployRecordItem';
|
import DeployRecordItem from './components/DeployRecordItem';
|
||||||
import PipelineStepItem from './components/PipelineStepItem';
|
import PipelineStepItem from './components/PipelineStepItem';
|
||||||
import { detailService } from './service';
|
import { detailService } from './service';
|
||||||
|
|
||||||
// 部署记录类型定义
|
interface StepWithEnabled extends Step {
|
||||||
interface DeployRecord {
|
|
||||||
id: number;
|
|
||||||
branch: string;
|
|
||||||
env: string;
|
|
||||||
commit: string;
|
|
||||||
status: 'success' | 'running' | 'failed' | 'pending';
|
|
||||||
createdAt: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 流水线步骤类型定义(更新为与后端一致)
|
|
||||||
interface PipelineStep {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
description?: string;
|
|
||||||
order: number;
|
|
||||||
script: string; // 执行的脚本命令
|
|
||||||
valid: number;
|
|
||||||
createdAt: string;
|
|
||||||
updatedAt: string;
|
|
||||||
createdBy: string;
|
|
||||||
updatedBy: string;
|
|
||||||
pipelineId: number;
|
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 流水线类型定义
|
interface PipelineWithEnabled extends Pipeline {
|
||||||
interface Pipeline {
|
steps?: StepWithEnabled[];
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
valid: number;
|
|
||||||
createdAt: string;
|
|
||||||
updatedAt: string;
|
|
||||||
createdBy: string;
|
|
||||||
updatedBy: string;
|
|
||||||
projectId?: number;
|
|
||||||
steps?: PipelineStep[];
|
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function ProjectDetailPage() {
|
function ProjectDetailPage() {
|
||||||
const [detail, setDetail] = useState<Project | null>();
|
const [detail, setDetail] = useState<Project | null>();
|
||||||
|
|
||||||
@@ -97,44 +68,19 @@ function ProjectDetailPage() {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
const [selectedRecordId, setSelectedRecordId] = useState<number>(1);
|
const [selectedRecordId, setSelectedRecordId] = useState<number>(1);
|
||||||
const [pipelines, setPipelines] = useState<Pipeline[]>([]);
|
const [pipelines, setPipelines] = useState<PipelineWithEnabled[]>([]);
|
||||||
const [editModalVisible, setEditModalVisible] = useState(false);
|
const [editModalVisible, setEditModalVisible] = useState(false);
|
||||||
const [selectedPipelineId, setSelectedPipelineId] = useState<number>(0);
|
const [selectedPipelineId, setSelectedPipelineId] = useState<number>(0);
|
||||||
const [editingStep, setEditingStep] = useState<PipelineStep | null>(null);
|
const [editingStep, setEditingStep] = useState<StepWithEnabled | null>(null);
|
||||||
const [editingPipelineId, setEditingPipelineId] = useState<number | null>(
|
const [editingPipelineId, setEditingPipelineId] = useState<number | null>(
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
const [pipelineModalVisible, setPipelineModalVisible] = useState(false);
|
const [pipelineModalVisible, setPipelineModalVisible] = useState(false);
|
||||||
const [editingPipeline, setEditingPipeline] = useState<Pipeline | null>(null);
|
const [editingPipeline, setEditingPipeline] = useState<PipelineWithEnabled | null>(null);
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const [pipelineForm] = Form.useForm();
|
const [pipelineForm] = Form.useForm();
|
||||||
const [deployRecords, _setDeployRecords] = useState<DeployRecord[]>([
|
const [deployRecords, setDeployRecords] = useState<Deployment[]>([]);
|
||||||
{
|
const [deployModalVisible, setDeployModalVisible] = useState(false);
|
||||||
id: 1,
|
|
||||||
branch: 'main',
|
|
||||||
env: 'development',
|
|
||||||
commit: '1d1224ae1',
|
|
||||||
status: 'success',
|
|
||||||
createdAt: '2024-09-07 14:30:25',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
branch: 'develop',
|
|
||||||
env: 'staging',
|
|
||||||
commit: '2f4b5c8e9',
|
|
||||||
status: 'running',
|
|
||||||
createdAt: '2024-09-07 13:45:12',
|
|
||||||
},
|
|
||||||
// 移除了 ID 为 3 的部署记录,避免可能的冲突
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
branch: 'main',
|
|
||||||
env: 'production',
|
|
||||||
commit: '4e8b6a5c3',
|
|
||||||
status: 'success',
|
|
||||||
createdAt: '2024-09-07 10:15:30',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
useAsyncEffect(async () => {
|
useAsyncEffect(async () => {
|
||||||
@@ -164,52 +110,33 @@ function ProjectDetailPage() {
|
|||||||
console.error('获取流水线数据失败:', error);
|
console.error('获取流水线数据失败:', error);
|
||||||
Message.error('获取流水线数据失败');
|
Message.error('获取流水线数据失败');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取部署记录
|
||||||
|
try {
|
||||||
|
const records = await detailService.getDeployments(Number(id));
|
||||||
|
setDeployRecords(records);
|
||||||
|
if (records.length > 0) {
|
||||||
|
setSelectedRecordId(records[0].id);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取部署记录失败:', error);
|
||||||
|
Message.error('获取部署记录失败');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 获取模拟的构建日志
|
// 获取构建日志
|
||||||
const getBuildLogs = (recordId: number): string[] => {
|
const getBuildLogs = (recordId: number): string[] => {
|
||||||
const logs: Record<number, string[]> = {
|
const record = deployRecords.find((r) => r.id === recordId);
|
||||||
1: [
|
if (!record || !record.buildLog) {
|
||||||
'[2024-09-07 14:30:25] 开始构建...',
|
return ['暂无日志记录'];
|
||||||
'[2024-09-07 14:30:26] 拉取代码: git clone https://github.com/user/repo.git',
|
}
|
||||||
'[2024-09-07 14:30:28] 切换分支: git checkout main',
|
return record.buildLog.split('\n');
|
||||||
'[2024-09-07 14:30:29] 安装依赖: npm install',
|
};
|
||||||
'[2024-09-07 14:31:15] 运行测试: npm test',
|
|
||||||
'[2024-09-07 14:31:30] ✅ 所有测试通过',
|
// 触发部署
|
||||||
'[2024-09-07 14:31:31] 构建项目: npm run build',
|
const handleDeploy = () => {
|
||||||
'[2024-09-07 14:32:10] 构建镜像: docker build -t app:latest .',
|
setDeployModalVisible(true);
|
||||||
'[2024-09-07 14:33:25] 推送镜像: docker push registry.com/app:latest',
|
|
||||||
'[2024-09-07 14:34:10] 部署到开发环境...',
|
|
||||||
'[2024-09-07 14:34:45] ✅ 部署成功',
|
|
||||||
],
|
|
||||||
2: [
|
|
||||||
'[2024-09-07 13:45:12] 开始构建...',
|
|
||||||
'[2024-09-07 13:45:13] 拉取代码: git clone https://github.com/user/repo.git',
|
|
||||||
'[2024-09-07 13:45:15] 切换分支: git checkout develop',
|
|
||||||
'[2024-09-07 13:45:16] 安装依赖: npm install',
|
|
||||||
'[2024-09-07 13:46:02] 运行测试: npm test',
|
|
||||||
'[2024-09-07 13:46:18] ✅ 所有测试通过',
|
|
||||||
'[2024-09-07 13:46:19] 构建项目: npm run build',
|
|
||||||
'[2024-09-07 13:47:05] 构建镜像: docker build -t app:develop .',
|
|
||||||
'[2024-09-07 13:48:20] 🔄 正在推送镜像...',
|
|
||||||
],
|
|
||||||
// 移除了 ID 为 3 的模拟数据,避免可能的冲突
|
|
||||||
4: [
|
|
||||||
'[2024-09-07 10:15:30] 开始构建...',
|
|
||||||
'[2024-09-07 10:15:31] 拉取代码: git clone https://github.com/user/repo.git',
|
|
||||||
'[2024-09-07 10:15:33] 切换分支: git checkout main',
|
|
||||||
'[2024-09-07 10:15:34] 安装依赖: npm install',
|
|
||||||
'[2024-09-07 10:16:20] 运行测试: npm test',
|
|
||||||
'[2024-09-07 10:16:35] ✅ 所有测试通过',
|
|
||||||
'[2024-09-07 10:16:36] 构建项目: npm run build',
|
|
||||||
'[2024-09-07 10:17:22] 构建镜像: docker build -t app:v1.0.0 .',
|
|
||||||
'[2024-09-07 10:18:45] 推送镜像: docker push registry.com/app:v1.0.0',
|
|
||||||
'[2024-09-07 10:19:30] 部署到生产环境...',
|
|
||||||
'[2024-09-07 10:20:15] ✅ 部署成功',
|
|
||||||
],
|
|
||||||
};
|
|
||||||
return logs[recordId] || ['暂无日志记录'];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 添加新流水线
|
// 添加新流水线
|
||||||
@@ -220,7 +147,7 @@ function ProjectDetailPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 编辑流水线
|
// 编辑流水线
|
||||||
const handleEditPipeline = (pipeline: Pipeline) => {
|
const handleEditPipeline = (pipeline: PipelineWithEnabled) => {
|
||||||
setEditingPipeline(pipeline);
|
setEditingPipeline(pipeline);
|
||||||
pipelineForm.setFieldsValue({
|
pipelineForm.setFieldsValue({
|
||||||
name: pipeline.name,
|
name: pipeline.name,
|
||||||
@@ -263,7 +190,7 @@ function ProjectDetailPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 复制流水线
|
// 复制流水线
|
||||||
const handleCopyPipeline = async (pipeline: Pipeline) => {
|
const handleCopyPipeline = async (pipeline: PipelineWithEnabled) => {
|
||||||
Modal.confirm({
|
Modal.confirm({
|
||||||
title: '确认复制',
|
title: '确认复制',
|
||||||
content: '确定要复制这个流水线吗?',
|
content: '确定要复制这个流水线吗?',
|
||||||
@@ -404,7 +331,7 @@ function ProjectDetailPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 编辑步骤
|
// 编辑步骤
|
||||||
const handleEditStep = (pipelineId: number, step: PipelineStep) => {
|
const handleEditStep = (pipelineId: number, step: StepWithEnabled) => {
|
||||||
setEditingStep(step);
|
setEditingStep(step);
|
||||||
setEditingPipelineId(pipelineId);
|
setEditingPipelineId(pipelineId);
|
||||||
form.setFieldsValue({
|
form.setFieldsValue({
|
||||||
@@ -573,11 +500,8 @@ function ProjectDetailPage() {
|
|||||||
const buildLogs = getBuildLogs(selectedRecordId);
|
const buildLogs = getBuildLogs(selectedRecordId);
|
||||||
|
|
||||||
// 简单的状态标签渲染函数(仅用于构建日志区域)
|
// 简单的状态标签渲染函数(仅用于构建日志区域)
|
||||||
const renderStatusTag = (status: DeployRecord['status']) => {
|
const renderStatusTag = (status: Deployment['status']) => {
|
||||||
const statusMap: Record<
|
const statusMap: Record<string, { color: string; text: string }> = {
|
||||||
DeployRecord['status'],
|
|
||||||
{ color: string; text: string }
|
|
||||||
> = {
|
|
||||||
success: { color: 'green', text: '成功' },
|
success: { color: 'green', text: '成功' },
|
||||||
running: { color: 'blue', text: '运行中' },
|
running: { color: 'blue', text: '运行中' },
|
||||||
failed: { color: 'red', text: '失败' },
|
failed: { color: 'red', text: '失败' },
|
||||||
@@ -588,7 +512,7 @@ function ProjectDetailPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 渲染部署记录项
|
// 渲染部署记录项
|
||||||
const renderDeployRecordItem = (item: DeployRecord, _index: number) => {
|
const renderDeployRecordItem = (item: Deployment, _index: number) => {
|
||||||
const isSelected = item.id === selectedRecordId;
|
const isSelected = item.id === selectedRecordId;
|
||||||
return (
|
return (
|
||||||
<DeployRecordItem
|
<DeployRecordItem
|
||||||
@@ -609,17 +533,21 @@ function ProjectDetailPage() {
|
|||||||
</Typography.Title>
|
</Typography.Title>
|
||||||
<Typography.Text type="secondary">自动化地部署项目</Typography.Text>
|
<Typography.Text type="secondary">自动化地部署项目</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
<Button type="primary" icon={<IconPlayArrow />}>
|
<Button type="primary" icon={<IconPlayArrow />} onClick={handleDeploy}>
|
||||||
部署
|
部署
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-white p-6 rounded-lg shadow-md flex-1">
|
<div className="bg-white p-6 rounded-lg shadow-md flex-1 flex flex-col overflow-hidden">
|
||||||
<Tabs type="line" size="large">
|
<Tabs
|
||||||
|
type="line"
|
||||||
|
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-content_.arco-tabs-pane]:h-full"
|
||||||
|
>
|
||||||
<Tabs.TabPane title="部署记录" key="deployRecords">
|
<Tabs.TabPane title="部署记录" key="deployRecords">
|
||||||
<div className="grid grid-cols-5 gap-6 h-full">
|
<div className="grid grid-cols-5 gap-6 h-full">
|
||||||
{/* 左侧部署记录列表 */}
|
{/* 左侧部署记录列表 */}
|
||||||
<div className="col-span-2 space-y-4">
|
<div className="col-span-2 space-y-4 h-full flex flex-col">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between shrink-0">
|
||||||
<Typography.Text type="secondary">
|
<Typography.Text type="secondary">
|
||||||
共 {deployRecords.length} 条部署记录
|
共 {deployRecords.length} 条部署记录
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
@@ -627,7 +555,7 @@ function ProjectDetailPage() {
|
|||||||
刷新
|
刷新
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-full overflow-y-auto">
|
<div className="flex-1 overflow-y-auto min-h-0">
|
||||||
{deployRecords.length > 0 ? (
|
{deployRecords.length > 0 ? (
|
||||||
<List
|
<List
|
||||||
className="bg-white rounded-lg border"
|
className="bg-white rounded-lg border"
|
||||||
@@ -644,8 +572,8 @@ function ProjectDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 右侧构建日志 */}
|
{/* 右侧构建日志 */}
|
||||||
<div className="col-span-3 bg-white rounded-lg border h-full overflow-hidden">
|
<div className="col-span-3 bg-white rounded-lg border h-full overflow-hidden flex flex-col">
|
||||||
<div className="p-4 border-b bg-gray-50">
|
<div className="p-4 border-b bg-gray-50 shrink-0">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<Typography.Title heading={5} className="!m-0">
|
<Typography.Title heading={5} className="!m-0">
|
||||||
@@ -665,8 +593,8 @@ function ProjectDetailPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-4 h-full overflow-y-auto">
|
<div className="p-4 flex-1 overflow-hidden flex flex-col">
|
||||||
<div className="bg-gray-900 text-green-400 p-4 rounded font-mono text-sm h-full overflow-y-auto">
|
<div className="bg-gray-900 text-green-400 p-4 rounded font-mono text-sm flex-1 overflow-y-auto">
|
||||||
{buildLogs.map((log: string, index: number) => (
|
{buildLogs.map((log: string, index: number) => (
|
||||||
<div
|
<div
|
||||||
key={`${selectedRecordId}-${log.slice(0, 30)}-${index}`}
|
key={`${selectedRecordId}-${log.slice(0, 30)}-${index}`}
|
||||||
@@ -706,7 +634,7 @@ function ProjectDetailPage() {
|
|||||||
key={pipeline.id}
|
key={pipeline.id}
|
||||||
className={`cursor-pointer transition-all duration-200 ${
|
className={`cursor-pointer transition-all duration-200 ${
|
||||||
isSelected
|
isSelected
|
||||||
? 'bg-blue-50 border-l-4 border-blue-500 border-blue-300'
|
? 'bg-blue-50 border-l-4 border-blue-500'
|
||||||
: 'hover:bg-gray-50 border-gray-200'
|
: 'hover:bg-gray-50 border-gray-200'
|
||||||
}`}
|
}`}
|
||||||
onClick={() => setSelectedPipelineId(pipeline.id)}
|
onClick={() => setSelectedPipelineId(pipeline.id)}
|
||||||
@@ -821,6 +749,7 @@ function ProjectDetailPage() {
|
|||||||
const selectedPipeline = pipelines.find(
|
const selectedPipeline = pipelines.find(
|
||||||
(p) => p.id === selectedPipelineId,
|
(p) => p.id === selectedPipelineId,
|
||||||
);
|
);
|
||||||
|
if (!selectedPipeline) return null;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="p-4 border-b bg-gray-50">
|
<div className="p-4 border-b bg-gray-50">
|
||||||
@@ -966,6 +895,25 @@ function ProjectDetailPage() {
|
|||||||
</Tabs.TabPane>
|
</Tabs.TabPane>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<DeployModal
|
||||||
|
visible={deployModalVisible}
|
||||||
|
onCancel={() => setDeployModalVisible(false)}
|
||||||
|
onOk={() => {
|
||||||
|
setDeployModalVisible(false);
|
||||||
|
// 刷新部署记录
|
||||||
|
if (id) {
|
||||||
|
detailService.getDeployments(Number(id)).then((records) => {
|
||||||
|
setDeployRecords(records);
|
||||||
|
if (records.length > 0) {
|
||||||
|
setSelectedRecordId(records[0].id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
pipelines={pipelines}
|
||||||
|
projectId={Number(id)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { type APIResponse, net } from '@shared';
|
import { type APIResponse, net } from '@shared';
|
||||||
import type { Pipeline, Project, Step } from '../types';
|
import type { Branch, Commit, Deployment, Pipeline, Project, Step } from '../types';
|
||||||
|
|
||||||
class DetailService {
|
class DetailService {
|
||||||
async getProject(id: string) {
|
async getProject(id: string) {
|
||||||
@@ -17,6 +17,14 @@ class DetailService {
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取项目的部署记录
|
||||||
|
async getDeployments(projectId: number) {
|
||||||
|
const { data } = await net.request<any>({
|
||||||
|
url: `/api/deployments?projectId=${projectId}`,
|
||||||
|
});
|
||||||
|
return data.data;
|
||||||
|
}
|
||||||
|
|
||||||
// 创建流水线
|
// 创建流水线
|
||||||
async createPipeline(
|
async createPipeline(
|
||||||
pipeline: Omit<
|
pipeline: Omit<
|
||||||
@@ -120,6 +128,39 @@ class DetailService {
|
|||||||
});
|
});
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取项目的提交记录
|
||||||
|
async getCommits(projectId: number, branch?: string) {
|
||||||
|
const { data } = await net.request<APIResponse<Commit[]>>({
|
||||||
|
url: `/api/git/commits?projectId=${projectId}${branch ? `&branch=${branch}` : ''}`,
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取项目的分支列表
|
||||||
|
async getBranches(projectId: number) {
|
||||||
|
const { data } = await net.request<APIResponse<Branch[]>>({
|
||||||
|
url: `/api/git/branches?projectId=${projectId}`,
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建部署
|
||||||
|
async createDeployment(deployment: {
|
||||||
|
projectId: number;
|
||||||
|
pipelineId: number;
|
||||||
|
branch: string;
|
||||||
|
commitHash: string;
|
||||||
|
commitMessage: string;
|
||||||
|
env?: string;
|
||||||
|
}) {
|
||||||
|
const { data } = await net.request<APIResponse<Deployment>>({
|
||||||
|
url: '/api/deployments',
|
||||||
|
method: 'POST',
|
||||||
|
data: deployment,
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const detailService = new DetailService();
|
export const detailService = new DetailService();
|
||||||
|
|||||||
@@ -45,3 +45,48 @@ export interface Pipeline {
|
|||||||
projectId?: number;
|
projectId?: number;
|
||||||
steps?: Step[];
|
steps?: Step[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Deployment {
|
||||||
|
id: number;
|
||||||
|
branch: string;
|
||||||
|
env?: string;
|
||||||
|
status: string;
|
||||||
|
commitHash?: string;
|
||||||
|
commitMessage?: string;
|
||||||
|
buildLog?: string;
|
||||||
|
startedAt: string;
|
||||||
|
finishedAt?: string;
|
||||||
|
valid: number;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
createdBy: string;
|
||||||
|
updatedBy: string;
|
||||||
|
projectId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Commit {
|
||||||
|
sha: string;
|
||||||
|
commit: {
|
||||||
|
message: string;
|
||||||
|
author: {
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
date: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
html_url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Branch {
|
||||||
|
name: string;
|
||||||
|
commit: {
|
||||||
|
id: string;
|
||||||
|
message: string;
|
||||||
|
url: string;
|
||||||
|
author: {
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
date: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user