mirror of https://github.com/certd/certd
feat: 支持模版创建流水线
parent
8bb1ed3e95
commit
2559f0e822
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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,
|
||||
};
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
Loading…
Reference in New Issue