feat: 支持模版创建流水线

pull/453/head
xiaojunnuo 2025-06-23 18:20:49 +08:00
parent 8bb1ed3e95
commit 2559f0e822
9 changed files with 258 additions and 110 deletions

View File

@ -32,6 +32,16 @@ export const certdResources = [
isMenu: false,
},
},
{
title: "执行历史记录",
name: "PipelineHistory",
path: "/certd/history",
component: "/certd/history/index.vue",
meta: {
icon: "ion:timer-outline",
keepAlive: true,
},
},
{
title: "流水线模版",
name: "PipelineTemplate",
@ -39,6 +49,7 @@ export const certdResources = [
component: "/certd/pipeline/template/index.vue",
meta: {
isMenu: true,
icon: "ion:duplicate-outline",
},
},
{
@ -50,16 +61,6 @@ export const certdResources = [
isMenu: false,
},
},
{
title: "执行历史记录",
name: "PipelineHistory",
path: "/certd/history",
component: "/certd/history/index.vue",
meta: {
icon: "ion:timer-outline",
keepAlive: true,
},
},
{
title: "证书仓库",
name: "CertStore",

View File

@ -12,6 +12,16 @@ export async function GetList(query: any) {
});
}
export async function GetSimpleByIds(ids: any) {
return await request({
url: apiPrefix + "/getSimpleByIds",
method: "post",
data: {
ids,
},
});
}
export async function AddObj(obj: any) {
return await request({
url: apiPrefix + "/add",
@ -100,7 +110,6 @@ export async function BatchUpdateNotificaiton(pipelineIds: number[], notificatio
});
}
export async function BatchDelete(pipelineIds: number[]): Promise<void> {
return await request({
url: apiPrefix + "/batchDelete",
@ -116,8 +125,6 @@ export async function BatchRerun(pipelineIds: number[]): Promise<void> {
});
}
export async function GetFiles(pipelineId: number) {
return await request({
url: historyApiPrefix + "/files",

View File

@ -17,7 +17,6 @@ import { useCertViewer } from "/@/views/certd/pipeline/use";
export default function ({ crudExpose, context: { groupDictRef, selectedRowKeys } }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const router = useRouter();
const { t } = useI18n();
const lastResRef = ref();
const { openAddCertdPipelineDialog } = useCertPipelineCreator();
@ -481,6 +480,7 @@ export default function ({ crudExpose, context: { groupDictRef, selectedRowKeys
{ value: "cert", label: "证书申请" },
{ value: "cert_upload", label: "证书上传" },
{ value: "custom", label: "自定义" },
{ value: "template", label: "模版" },
],
}),
form: {

View File

@ -1,10 +1,9 @@
// @ts-ignore
import { useI18n } from "vue-i18n";
import { AddReq, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, EditReq, useFormWrapper, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
import { AddReq, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, useFormWrapper, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
import { templateApi } from "./api";
import { useRouter } from "vue-router";
import { useModal } from "/@/use/use-modal";
import createCrudOptionsPipeline from "../crud";
import * as pipelineApi from "../api";
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const api = templateApi;
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
@ -66,6 +65,10 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
rowHandle: {
// width: 100,
fixed: "right",
buttons: {
edit: { show: false },
copy: { show: false },
},
},
columns: {
id: {
@ -94,17 +97,50 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
column: {
width: 200,
sorter: true,
cellRender({ row, value }) {
return <router-link to={{ path: "/certd/pipeline/template/edit", query: { templateId: row.id } }}>{value}</router-link>;
},
},
},
pipelineId: {
title: "流水线ID",
type: "text",
search: {
show: true,
type: "table-select",
search: { show: true },
dict: dict({
value: "id",
label: "title",
//重要根据value懒加载数据
getNodesByValues: async (values: any[]) => {
return await pipelineApi.GetSimpleByIds(values);
},
}),
editForm: {
show: false,
},
column: {
width: 200,
sorter: true,
form: {
component: {
valuesFormat: {
labelFormatter: (item: any) => {
return `${item.id}.${item.title}`;
},
},
select: {
placeholder: "点击选择",
},
showSelect: false,
createCrudOptions: createCrudOptionsPipeline,
crudOptionsOverride: {
actionbar: {
show: false,
},
toolbar: {
show: false,
},
tabs: {
name: "type",
},
},
},
},
},
},

View File

@ -1,49 +1,75 @@
<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>
<a-collapse>
<a-collapse-panel v-for="step of steps" 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%]">
<div class="flex flex-between" :title="input.define.helper">
<div class="flex flex-1 overflow-hidden mr-5">
<span style="min-width: 140px" class="bas">
<a-tag color="green">{{ input.define.title }}</a-tag>
</span>
<span :title="input.value" class="ellipsis flex-1 text-nowrap">= {{ input.value }}</span>
</div>
<fs-button v-if="!templateProps.input?.[key]" size="small" type="primary" icon="ion:add" title="添加为模版变量" @click="addToProps(step.id, key, input)"></fs-button>
<fs-button v-else size="small" danger icon="ion:close" title="删除模版变量" @click="removeToProps(step.id, key)" />
</div>
</div>
</div>
</a-collapse-panel>
</a-collapse>
<fs-page>
<template #header>
<div class="title flex flex-1">
<fs-button class="back" icon="ion:chevron-back-outline" @click="goBack"></fs-button>
<text-editable v-if="detail?.template" v-model="detail.template.title" class="ml-10" :hover-show="false"></text-editable>
</div>
<div class="template-props w-50%">
<div class="block-title">模版变量</div>
<div class="p-10">
<fs-form v-bind="templateFormOptions"></fs-form>
<div class="more flex items-center flex-1 justify-end">
<loading-button type="primary" @click="doSave"></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>
<a-collapse 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%]">
<div class="flex flex-between" :title="input.define.helper">
<div class="flex flex-1 overflow-hidden mr-5">
<span style="min-width: 140px" class="bas">
<a-tag color="green">{{ input.define.title }}</a-tag>
</span>
<span :title="input.value" class="ellipsis flex-1 text-nowrap">= {{ input.value }}</span>
</div>
<fs-button v-if="!templateProps.input[stepId + '.' + key]" size="small" type="primary" icon="ion:add" title="添加为模版变量" @click="addToProps(step.id, key)"></fs-button>
<fs-button v-else size="small" danger icon="ion:close" title="删除模版变量" @click="removeToProps(step.id, key)" />
</div>
</div>
</div>
</a-collapse-panel>
</a-collapse>
</div>
<div class="template-props w-50%">
<div class="block-title">
模版变量
<div class="helper">根据模版创建流水线时只需要输入以下这些字段其他字段将使用左侧的值</div>
</div>
<div class="p-10">
<!-- <fs-form v-bind="templateFormOptions"></fs-form>-->
<template-form :input="templateProps.input" :pipeline="detail?.pipeline"></template-form>
</div>
</div>
</div>
</div>
</div>
</fs-page>
</template>
<script setup lang="ts">
import { computed, onMounted, ref, Ref } from "vue";
import { useRoute } from "vue-router";
import { computed, nextTick, onMounted, ref, Ref } from "vue";
import { useRoute, useRouter } from "vue-router";
import { templateApi } from "./api";
import { eachSteps } from "../utils";
import { usePluginStore } from "/@/store/plugin";
import { useStepHelper } from "./utils";
import TemplateForm from "./form.vue";
const route = useRoute();
const templateId = route.query.templateId as string;
const router = useRouter();
function goBack() {
router.back();
}
type TemplateDetail = {
template: any;
pipeline: any;
@ -55,81 +81,50 @@ const detail: Ref<TemplateDetail> = ref();
async function getTemplateDetail() {
const res = await templateApi.GetDetail(parseInt(templateId));
detail.value = res;
templateProps.value = JSON.parse(res.template.content ?? "{}");
templateProps.value = JSON.parse(res.template.content ?? "{input:{}}");
}
const pluginStore = usePluginStore();
const activeKey = ref([]);
onMounted(async () => {
await pluginStore.init();
await getTemplateDetail();
nextTick(() => {
const keys = Object.keys(steps.value);
if (keys.length > 0) {
activeKey.value = [keys[0]];
}
});
});
const { getStepsMap } = useStepHelper(pluginStore);
const steps = computed(() => {
if (!detail.value) {
return [];
return {};
}
const list: any[] = [];
eachSteps(detail.value.pipeline, (step: any) => {
const plugin = pluginStore.getPluginDefineSync(step.type);
if (!plugin) {
return;
}
const inputs: any = {};
for (const key in plugin.input) {
const input: any = plugin.input[key];
if (input.template === false || input.component?.name === "output-selector") {
continue;
}
inputs[key] = {
value: step.input[key],
define: plugin.input[key],
};
}
list.push({
id: step.id,
title: step.title,
type: step.type,
input: inputs,
});
});
return list;
return getStepsMap(detail.value.pipeline);
});
const templateFormOptions = computed(() => {
const columns: any = {};
for (const key in templateProps.value.input) {
const input = templateProps.value.input[key];
columns[key] = {
title: input.define.title,
type: "text",
value: input.value,
...input.define,
};
}
return {
columns,
labelCol: {
style: {
width: "120px",
},
},
};
});
function addToProps(stepId: string, key: any, input: { value: any; define: any }) {
function addToProps(stepId: string, key: any) {
if (!templateProps.value.input) {
templateProps.value.input = {};
}
inputKey = stepId + "." + key;
templateProps.value.input[inputKey] = input;
const inputKey = stepId + "." + key;
templateProps.value.input[inputKey] = true;
}
function removeToProps(stepId: string, key: any) {
inputKey = stepId + "." + key;
const inputKey = stepId + "." + key;
delete templateProps.value.input[inputKey];
}
async function doSave() {
await templateApi.UpdateObj({
id: detail.value.template.id,
title: detail.value.template.title,
content: JSON.stringify(templateProps.value),
});
}
</script>

View File

@ -0,0 +1,64 @@
<template>
<a-form ref="templateFormRef" class="template-form" :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>
</a-form>
</template>
<script setup lang="ts">
import { get, set } from "lodash-es";
import { computed, reactive, ref, defineProps } from "vue";
import { useStepHelper } from "./utils";
import { usePluginStore } from "/@/store/plugin";
defineOptions({
name: "TemplateForm",
});
const props = defineProps<{
input: any;
pipeline: any;
}>();
const pluginStore = usePluginStore();
const { getStepsMap } = useStepHelper(pluginStore);
const steps = computed(() => {
if (!props.pipeline) {
return {};
}
return getStepsMap(props.pipeline);
});
const labelCol = ref({ span: 6 });
const wrapperCol = ref({ span: 16 });
const templateForm: any = reactive({});
const templateFormColumns = computed(() => {
const formColumns: any = {};
const inputs = props.input || {};
for (const inputKey in inputs) {
const [stepId, key] = inputKey.split(".");
const step = steps.value[stepId];
if (!step) {
continue;
}
formColumns[inputKey] = {
...step.input[key].define,
name: [stepId, key],
};
}
return formColumns;
});
function getScopeFunc(inputKey: string) {
const [stepId, key] = inputKey.split(".");
return () => {
return {
form: templateForm[stepId],
};
};
}
defineExpose({
getForm() {
return templateForm;
},
});
</script>

View File

@ -0,0 +1,38 @@
import { eachSteps } from "/@/views/certd/pipeline/utils";
export function useStepHelper(pluginStore: any) {
function getStepsMap(pipeline: any) {
const stepMap: any = {};
eachSteps(pipeline, (step: any) => {
const plugin = pluginStore.getPluginDefineSync(step.type);
if (!plugin) {
return;
}
const inputs: any = {};
for (const key in plugin.input) {
const input: any = plugin.input[key];
if (input.template === false || input.component?.name === "output-selector") {
continue;
}
inputs[key] = {
value: step.input[key],
define: {
...plugin.input[key],
name: [step.id, key],
},
};
}
stepMap[step.id] = {
id: step.id,
title: step.title,
type: step.type,
input: inputs,
};
});
return stepMap;
}
return {
getStepsMap,
};
}

View File

@ -53,6 +53,13 @@ export class PipelineController extends CrudController<PipelineService> {
return this.ok(pageRet);
}
@Post('/getSimpleByIds', { summary: Constants.per.authOnly })
async getSimpleById(@Body(ALL) body) {
const ret = await this.getService().getSimplePipelines(body.ids,this.getUserId() );
return this.ok(ret);
}
@Post('/add', { summary: Constants.per.authOnly })
async add(@Body(ALL) bean: PipelineEntity) {
bean.userId = this.getUserId();

View File

@ -29,7 +29,7 @@ export class PipelineEntity {
@Column({ comment: '启用/禁用', nullable: true, default: false })
disabled: boolean;
// cert_apply: 证书申请cert_upload: 证书上传; backup: 备份; custom:自定义;
// cert_apply: 证书申请cert_upload: 证书上传; backup: 备份; custom:自定义; template: 模板
@Column({ comment: '类型', nullable: true, default: 'cert' })
type: string;