chore: 模版创建流水线

pull/453/head
xiaojunnuo 2025-06-25 14:41:27 +08:00
parent 821c6d807d
commit 9296ba7492
15 changed files with 290 additions and 23 deletions

View File

@ -84,6 +84,7 @@ export abstract class BaseService<T> {
...where,
});
await this.modifyAfter(idArr);
return ids
}
resolveIdArr(ids: string | any[]) {

View File

@ -1,6 +1,6 @@
<script lang="ts" setup>
defineOptions({
name: "LayoutFooter"
name: "LayoutFooter",
});
</script>

View File

@ -37,6 +37,11 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
editRequest,
delRequest,
},
addForm: {
onSuccess: ({ res }) => {
router.push({ path: "/certd/pipeline/template/edit", query: { templateId: res.id, editMode: "true" } });
},
},
form: {
labelCol: {
//固定label宽度
@ -95,7 +100,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
show: true,
},
column: {
width: 200,
width: 400,
sorter: true,
cellRender({ row, value }) {
return <router-link to={{ path: "/certd/pipeline/template/edit", query: { templateId: row.id } }}>{value}</router-link>;
@ -117,8 +122,12 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
editForm: {
show: false,
},
form: {
column: {
show: false,
},
form: {
show: true,
helper: "复制该流水线配置作为模版来源",
component: {
valuesFormat: {
labelFormatter: (item: any) => {

View File

@ -7,18 +7,33 @@
</div>
<div class="more flex items-center flex-1 justify-end">
<loading-button type="primary" @click="doSave"></loading-button>
<loading-button type="primary" @click="doSave"></loading-button>
<loading-button class="ml-10" type="primary" @click="useTemplateCreate">使</loading-button>
<loading-button class="ml-10" type="primary" danger @click="doDelete"></loading-button>
</div>
</template>
<div class="page-template-edit">
<div class="base"></div>
<div class="props flex p-10">
<div class="task-list w-50%">
<div class="block-title">
原始任务参数
<div class="helper">点击加号将字段作为模版变量</div>
<div class="block-title flex flex-between">
<div>
模版流水线参数
<div class="helper">点击加号将字段作为模版变量</div>
</div>
<div class="more">
<router-link
v-if="detail?.template?.pipelineId > 0"
:to="{
path: '/certd/pipeline/detail',
query: { id: detail?.template?.pipelineId, editMode: true },
}"
>
修改模版流水线
</router-link>
</div>
</div>
<a-collapse v-model:active-key="activeKey">
<a-collapse v-if="detail?.template?.pipelineId > 0" v-model:active-key="activeKey">
<a-collapse-panel v-for="(step, stepId) in steps" :key="stepId" class="step-item" :header="step.title">
<div class="step-inputs flex flex-wrap">
<div v-for="(input, key) of step.input" :key="key" class="hover:bg-gray-100 p-5 w-full xl:w-[50%]">
@ -36,6 +51,17 @@
</div>
</a-collapse-panel>
</a-collapse>
<div v-else-if="detail?.template?.pipelineId === 0">
<div class="p-20 flex flex-col flex-center text-sm">
<div class="mb-10">还未绑定模版流水线</div>
<div>
<a-button type="primary" @click="bindPipelineByCreate">线</a-button>
<a-button type="primary" @click="bindPipelineByCopy">线</a-button>
</div>
</div>
</div>
</div>
<div class="template-props w-50%">
@ -60,7 +86,9 @@ import { templateApi } from "./api";
import { usePluginStore } from "/@/store/plugin";
import { useStepHelper } from "./utils";
import TemplateForm from "./form.vue";
import { Modal, notification } from "ant-design-vue";
import { useTabbarStore } from "/@/vben/stores";
import { useTemplate } from "./use";
const route = useRoute();
const templateId = route.query.templateId as string;
@ -79,6 +107,9 @@ const templateProps: Ref = ref({
});
const detail: Ref<TemplateDetail> = ref();
async function getTemplateDetail() {
if (!templateId) {
return;
}
const res = await templateApi.GetDetail(parseInt(templateId));
detail.value = res;
templateProps.value = JSON.parse(res.template.content ?? "{input:{}}");
@ -100,7 +131,7 @@ onMounted(async () => {
const { getStepsMap } = useStepHelper(pluginStore);
const steps = computed(() => {
if (!detail.value) {
if (!detail.value || !detail.value.pipeline) {
return {};
}
@ -126,5 +157,36 @@ async function doSave() {
title: detail.value.template.title,
content: JSON.stringify(templateProps.value),
});
notification.success({
message: "保存成功",
});
}
const tabbar = useTabbarStore();
async function doDelete() {
Modal.confirm({
title: "确定删除模版?",
content: "删除后,该模版流水线将不能再使用",
onOk() {
templateApi.DelObj(detail.value.template.id);
notification.success({
message: "删除成功",
});
tabbar.closeTab({ fullPath: route.fullPath } as any, router);
},
});
}
async function bindPipelineByCreate() {
//
// openAddCertdPipelineDialog({ templateId: detail.value.template.id });
}
async function bindPipelineByCopy() {}
const { openCreateFromTemplateDialog } = useTemplate();
async function useTemplateCreate() {
openCreateFromTemplateDialog({ templateId: detail.value.template.id });
}
</script>

View File

@ -1,5 +1,5 @@
<template>
<a-form ref="templateFormRef" class="template-form" :model="templateForm" :label-col="labelCol" :wrapper-col="wrapperCol">
<a-form ref="templateFormRef" class="template-form w-full" :model="templateForm" :label-col="labelCol" :wrapper-col="wrapperCol">
<template v-for="(item, key) in templateFormColumns" :key="key">
<fs-form-item v-if="item.show !== false" :model-value="get(templateForm, key)" :item="item" :get-context-fn="getScopeFunc(key)" @update:model-value="set(templateForm, key, $event)" />
</template>
@ -28,7 +28,12 @@ const steps = computed(() => {
return getStepsMap(props.pipeline);
});
const labelCol = ref({ span: 6 });
const labelCol = ref({
span: null,
style: {
width: "145px",
},
});
const wrapperCol = ref({ span: 16 });
const templateForm: any = reactive({});
const templateFormColumns = computed(() => {

View File

@ -0,0 +1,112 @@
import { dict, useFormWrapper } from "@fast-crud/fast-crud";
import { checkPipelineLimit } from "/@/views/certd/pipeline/utils";
import { templateApi } from "/@/views/certd/pipeline/template/api";
import TemplateForm from "./form.vue";
import NotificationSelector from "/@/views/certd/notification/notification-selector/index.vue";
import GroupSelector from "/@/views/certd/pipeline/group/group-selector.vue";
import { ref } from "vue";
export function useTemplate() {
const { openCrudFormDialog } = useFormWrapper();
async function openCreateFromTemplateDialog(req: { templateId?: number }) {
//检查是否流水线数量超出限制
await checkPipelineLimit();
const detail = await templateApi.GetDetail(req.templateId);
if (!detail) {
throw new Error("模板不存在");
}
if (!detail.template?.pipelineId) {
throw new Error("还未绑定模版流水线");
}
const templateProps = JSON.parse(detail.template.content || "{}");
const pipeline = detail.pipeline;
const groupDictRef = dict({
url: "/pi/pipeline/group/all",
value: "id",
label: "name",
});
const wrapperRef = ref();
function getFormData() {
if (!wrapperRef.value) {
return null;
}
return wrapperRef.value.getFormData();
}
const randomHour = Math.floor(Math.random() * 6);
const randomMin = Math.floor(Math.random() * 60);
const crudOptions = {
form: {
wrapper: {
title: `从模版<${detail.template.title}>创建流水线`,
width: 1100,
slots: {
"form-body-top": () => {
return (
<div class={"w-full flex"}>
<TemplateForm input={templateProps.input} pipeline={pipeline} />
</div>
);
},
},
},
},
columns: {
triggerCron: {
title: "定时触发",
type: "text",
form: {
value: `0 ${randomMin} ${randomHour} * * *`,
component: {
name: "cron-editor",
vModel: "modelValue",
placeholder: "0 0 4 * * *",
},
helper: "点击上面的按钮,选择每天几点定时执行。\n建议设置为每天触发一次证书未到期之前任务会跳过不会重复执行",
order: 100,
},
},
notification: {
title: "失败通知",
type: "text",
form: {
value: 0,
component: {
name: NotificationSelector,
vModel: "modelValue",
on: {
selectedChange(opts: any) {
opts.form.notificationTarget = opts.$event;
},
},
},
order: 101,
helper: "任务执行失败实时提醒",
},
},
groupId: {
title: "流水线分组",
type: "dict-select",
dict: groupDictRef,
form: {
component: {
name: GroupSelector,
vModel: "modelValue",
},
order: 9999,
},
},
},
};
const wrapper = await openCrudFormDialog({ crudOptions });
wrapperRef.value = wrapper;
}
return {
openCreateFromTemplateDialog,
};
}

View File

@ -13,3 +13,6 @@ CREATE TABLE "pi_template"
CREATE INDEX "index_template_user_id" ON "pi_template" ("user_id");
CREATE INDEX "index_template_pipeline_id" ON "pi_template" ("pipeline_id");
ALTER TABLE pi_pipeline ADD COLUMN "template_id" integer DEFAULT 0;
CREATE INDEX "index_pipeline_template_id" ON "pi_pipeline" ("template_id");

View File

@ -59,8 +59,8 @@ export class TemplateController extends CrudController<TemplateService> {
@Post('/delete', { summary: Constants.per.authOnly })
async delete(@Query('id') id: number) {
await this.service.checkUserId(id, this.getUserId());
return super.delete(id);
await this.service.batchDelete([id], this.getUserId());
return this.ok({});
}
@Post('/batchDelete', { summary: Constants.per.authOnly })

View File

@ -52,6 +52,7 @@ export class CnameProviderService extends BaseService<CnameProviderEntity> {
}
}
//@ts-ignore
async delete(ids: any) {
if (!ids) {
return;

View File

@ -37,6 +37,9 @@ export class PipelineEntity {
@Column({ comment: '来源', nullable: true, default: '' })
from: string;
@Column({ name:"template_id", comment: '是否模版', nullable: true, default: '' })
templateId: number;
@Column({
name: 'last_history_time',
comment: '最后一次执行时间',

View File

@ -46,6 +46,7 @@ import {UserSuiteEntity, UserSuiteService} from "@certd/commercial-core";
import {CertInfoService} from "../../monitor/service/cert-info-service.js";
import {TaskServiceBuilder} from "./task-service-getter.js";
import {nanoid} from "nanoid";
import {set} from "lodash-es";
const runningTasks: Map<string | number, Executor> = new Map();
@ -117,6 +118,8 @@ export class PipelineService extends BaseService<PipelineEntity> {
}
async page(pageReq: PageReq<PipelineEntity>) {
//模版流水线不要被查询出来
set(pageReq,"query.templateId",0)
const result = await super.page(pageReq);
await this.fillLastVars(result.records);
@ -281,6 +284,7 @@ export class PipelineService extends BaseService<PipelineEntity> {
},
where: {
disabled: false,
templateId: 0,
},
});
const ids = idEntityList.map(item => {
@ -385,7 +389,8 @@ export class PipelineService extends BaseService<PipelineEntity> {
}
}
async delete(id: any) {
//@ts-ignore
async delete(id:any) {
await this.clearTriggers(id);
//TODO 删除storage
// const storage = new DbStorage(pipeline.userId, this.storageService);

View File

@ -1,12 +1,15 @@
import {Inject, Provide, Scope, ScopeEnum} from '@midwayjs/core';
import {BaseService, SysSettingsService} from '@certd/lib-server';
import {InjectEntityModel} from '@midwayjs/typeorm';
import {Repository} from 'typeorm';
import { TemplateEntity } from '../entity/template.js';
import { PipelineService } from './pipeline-service.js';
import {In, Repository} from 'typeorm';
import {TemplateEntity} from '../entity/template.js';
import {PipelineService} from './pipeline-service.js';
import {cloneDeep} from "lodash-es";
import {PipelineEntity} from "../entity/pipeline.js";
import {Pipeline} from "@certd/pipeline";
@Provide()
@Scope(ScopeEnum.Request, { allowDowngrade: true })
@Scope(ScopeEnum.Request, {allowDowngrade: true})
export class TemplateService extends BaseService<TemplateEntity> {
@InjectEntityModel(TemplateEntity)
repository: Repository<TemplateEntity>;
@ -22,6 +25,47 @@ export class TemplateService extends BaseService<TemplateEntity> {
return this.repository;
}
async add(param: any) {
const pipelineId = param.pipelineId;
delete param.pipelineId;
const pipelineEntity = await this.pipelineService.info(pipelineId);
if (!pipelineEntity) {
throw new Error('pipeline not found');
}
if (pipelineEntity.userId !== param.userId) {
throw new Error('permission denied');
}
let template = null
await this.transaction(async (tx: any) => {
template = await tx.getRepository(TemplateEntity).save(param);
let newPipeline = cloneDeep(pipelineEntity)
//创建pipeline模版
newPipeline.id = undefined;
newPipeline.title = template.title+"模版流水线"
newPipeline.templateId = template.id
const pipelineJson:Pipeline = JSON.parse(newPipeline.content)
delete pipelineJson.triggers
pipelineJson.id = template.id
pipelineJson.userId = template.userId
pipelineJson.title = newPipeline.title
newPipeline.content = JSON.stringify(pipelineJson)
newPipeline = await tx.getRepository(PipelineEntity).save(newPipeline)
const update :any= {}
update.id = template.id
update.pipelineId = newPipeline.id
await tx.getRepository(TemplateEntity).save(update)
})
return template
}
async detail(id: number, userId: number) {
const info = await this.info(id)
if (!info) {
@ -30,12 +74,31 @@ export class TemplateService extends BaseService<TemplateEntity> {
if (info.userId !== userId) {
throw new Error('无权限');
}
const pipeline = await this.pipelineService.info(info.pipelineId);
let pipeline = null
if (info.pipelineId) {
const pipelineEntity = await this.pipelineService.info(info.pipelineId);
pipeline = JSON.parse(pipelineEntity.content)
}
return {
template:info,
pipeline: JSON.parse(pipeline.content),
template: info,
pipeline,
}
}
async batchDelete(ids: number[], userId: number) {
const where :any= {
id: In(ids),
}
if (userId > 0) {
where.userId = userId
}
const list = await this.getRepository().find({where })
ids = list.map(item => item.id)
const pipelineIds = list.map(item => item.pipelineId)
await this.delete(ids);
await this.pipelineService.batchDelete(pipelineIds,userId)
}
}

View File

@ -129,6 +129,7 @@ export class RoleService extends BaseService<RoleEntity> {
return permissionSet;
}
//@ts-ignore
async delete(id: any) {
const idArr = this.resolveIdArr(id);
//@ts-ignore

View File

@ -253,6 +253,7 @@ export class UserService extends BaseService<UserEntity> {
await this.update(param);
}
//@ts-ignore
async delete(ids: any) {
if (typeof ids === 'string') {
ids = ids.split(',');

View File

@ -7,6 +7,7 @@ import {
createCertDomainGetterInputDefine,
createRemoteSelectInputDefine
} from "@certd/plugin-lib";
import {PageReq} from "@certd/lib-server";
@IsTaskPlugin({
name: 'AliyunDeployCertToWaf',
@ -168,7 +169,7 @@ export class AliyunDeployCertToWaf extends AbstractTaskPlugin {
}
}
async onGetCnameList(data: any) {
async onGetCnameList(data: PageReq) {
if (!this.accessId) {
throw new Error('请选择Access授权');
}