perf: 模版导入流水线

pull/453/head
xiaojunnuo 2025-06-26 18:43:16 +08:00
parent 529482a83e
commit dcc8c56969
13 changed files with 462 additions and 66 deletions

View File

@ -8,7 +8,7 @@ export const EVENT_CERT_APPLY_SUCCESS = "CertApply.success";
export abstract class CertApplyBaseConvertPlugin extends AbstractTaskPlugin {
@TaskInput({
title: "域名",
title: "证书域名",
component: {
name: "a-select",
vModel: "value",

View File

@ -19,7 +19,9 @@ defineOptions({
});
const getScope: any = inject("get:scope");
const getPluginType: any = inject("get:plugin:type");
const getPluginType: any = inject("get:plugin:type", () => {
return "access";
});
const formItemContext = Form.useInjectFormItemContext();
const props = defineProps<{} & ComponentPropsType>();

View File

@ -27,7 +27,9 @@ const emit = defineEmits<{
}>();
const getScope: any = inject("get:scope");
const getPluginType: any = inject("get:plugin:type");
const getPluginType: any = inject("get:plugin:type", () => {
return "plugin";
});
const attrs = useAttrs();

View File

@ -69,9 +69,15 @@ const emit = defineEmits<{
const attrs = useAttrs();
const getCurrentPluginDefine: any = inject("getCurrentPluginDefine");
const getScope: any = inject("get:scope");
const getPluginType: any = inject("get:plugin:type");
const getCurrentPluginDefine: any = inject("getCurrentPluginDefine", () => {
return {};
});
const getScope: any = inject("get:scope", () => {
return {};
});
const getPluginType: any = inject("get:plugin:type", () => {
return "plugin";
});
const searchKeyRef = ref("");
const optionsRef = ref([]);
@ -96,7 +102,7 @@ const getOptions = async () => {
}
const pluginType = getPluginType();
const { form } = getScope();
const input = pluginType === "plugin" ? form.input : form;
const input = (pluginType === "plugin" ? form?.input : form) || {};
for (let key in define.input) {
const inWatches = props.watches.includes(key);
@ -186,7 +192,7 @@ watch(
() => {
const pluginType = getPluginType();
const { form, key } = getScope();
const input = pluginType === "plugin" ? form.input : form;
const input = (pluginType === "plugin" ? form?.input : form) || {};
const watches = {};
for (const key of props.watches) {
watches[key] = input[key];
@ -198,10 +204,11 @@ watch(
},
async (value, oldValue) => {
const { form } = value;
const oldForm = oldValue.form;
const oldForm: any = oldValue?.form;
let changed = oldForm == null || optionsRef.value.length == 0;
for (const key of props.watches) {
if (form[key] != oldForm[key]) {
//@ts-ignore
if (oldForm && form[key] != oldForm[key]) {
changed = true;
break;
}

View File

@ -27,7 +27,9 @@ const attrs = useAttrs();
const otpCodeRef = ref("");
const getScope: any = inject("get:scope");
const getPluginType: any = inject("get:plugin:type");
const getPluginType: any = inject("get:plugin:type", () => {
return "access";
});
async function loginWithOTPCode(otpCode: string) {
const { form } = getScope();

View File

@ -61,6 +61,15 @@ export const certdResources = [
isMenu: false,
},
},
{
title: "流水线模版批量创建",
name: "PipelineTemplateImport",
path: "/certd/pipeline/template/import",
component: "/certd/pipeline/template/import/index.vue",
meta: {
isMenu: false,
},
},
{
title: "证书仓库",
name: "CertStore",

View File

@ -4,6 +4,7 @@ import { useRouter } from "vue-router";
import { useModal } from "/@/use/use-modal";
import createCrudOptionsPipeline from "../crud";
import * as pipelineApi from "../api";
import { useTemplate } from "/@/views/certd/pipeline/template/use";
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const api = templateApi;
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
@ -29,6 +30,9 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
const router = useRouter();
const model = useModal();
const { openCreateFromTemplateDialog } = useTemplate();
return {
crudOptions: {
request: {
@ -73,6 +77,27 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
buttons: {
edit: { show: false },
copy: { show: false },
use: {
text: null,
title: "使用此模版创建流水线",
icon: "ion:duplicate-outline",
click({ row }) {
openCreateFromTemplateDialog({
templateId: row.id,
onCreated: ({ id }) => {
router.push({ path: "/certd/pipeline/detail", query: { id, editMode: true } });
},
});
},
},
import: {
text: null,
title: "批量导入创建",
icon: "ion:duplicate",
click({ row }) {
router.push({ path: "/certd/pipeline/template/import", query: { templateId: row.id } });
},
},
},
},
columns: {

View File

@ -0,0 +1,80 @@
import { CreateCrudOptionsProps, CreateCrudOptionsRet, importTable } from "@fast-crud/fast-crud";
import { Modal, notification } from "ant-design-vue";
export default function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOptionsRet {
return {
crudOptions: {
mode: {
name: "local",
isMergeWhenUpdate: true,
isAppendWhenAdd: true,
},
//启用addRow按钮
actionbar: {
buttons: {
//禁用弹框添加
add: { show: false },
//启用添加行
addRow: { show: true },
//导入按钮
import: {
show: false,
text: "批量导入",
type: "primary",
click() {
const modal = Modal.info({
title: "批量导入",
okText: "关闭",
content() {
async function onChange(e: any) {
const file = e.target.files[0];
await importTable(crudExpose, { file, append: true });
modal.destroy();
notification.success({
message: "导入成功",
});
}
return (
<div>
<p>
1<a href={"template-import.csv"}></a>
</p>
<p>
2<span></span>
</p>
<p>
<span>3</span>
<input type={"file"} onInput={onChange}></input>
</p>
</div>
);
},
});
},
},
},
},
table: {
remove: {
//删除数据后不请求后台
refreshTable: false,
},
editable: {
enabled: true,
mode: "row",
activeTrigger: false,
},
},
search: {
show: false,
},
toolbar: {
show: false,
},
pagination: {
show: false,
},
columns: {},
},
};
}

View File

@ -0,0 +1,30 @@
import { useColumns } from "@fast-crud/fast-crud";
import { createExtraColumns } from "/@/views/certd/pipeline/template/use";
import TemplateImportTable from "/@/views/certd/pipeline/template/import/table.vue";
import { Ref } from "vue";
export function createFormOptions(detail: Ref): any {
const { buildFormOptions } = useColumns();
const crudOptions = {
columns: {
...createExtraColumns(),
templateProps: {
title: "流水线导入",
type: "text",
form: {
order: 1,
component: {
name: TemplateImportTable,
detail: detail,
},
col: {
span: 24,
},
},
},
},
};
return buildFormOptions(crudOptions);
}

View File

@ -0,0 +1,111 @@
<template>
<fs-page class="page-template-import">
<template #header>
<div class="title flex flex-1 items-center">
<fs-button class="back" icon="ion:chevron-back-outline" @click="goBack"></fs-button>
<div class="ml-10">从模版{{ detail?.template?.title }}批量创建流水线</div>
</div>
</template>
<fs-form v-if="importFromOptions" ref="formRef" class="mt-10" v-bind="importFromOptions"> </fs-form>
<div class="p-10">
<a-button class="ml-20" type="primary" @click="doImport"> </a-button>
</div>
</fs-page>
</template>
<script setup lang="tsx">
import { onMounted, ref, Ref } from "vue";
import { useRoute, useRouter } from "vue-router";
import { templateApi } from "../api";
import { createFormOptions } from "/@/views/certd/pipeline/template/import/form";
import { cloneDeep } from "lodash-es";
import { fillPipelineByDefaultForm } from "/@/views/certd/pipeline/certd-form/use";
import { eachSteps } from "/@/views/certd/pipeline/utils";
const route = useRoute();
const templateId = route.query.templateId as string;
const router = useRouter();
function goBack() {
router.back();
}
type TemplateDetail = {
template: any;
pipeline: any;
};
const detail: Ref<TemplateDetail> = ref();
async function getTemplateDetail() {
if (!templateId) {
return;
}
detail.value = await templateApi.GetDetail(parseInt(templateId));
}
const importFromOptions = ref();
onMounted(async () => {
await getTemplateDetail();
importFromOptions.value = createFormOptions(detail);
});
const formRef = ref();
async function doImport() {
await formRef.value.validate();
const form = formRef.value.getFormData();
const importTableRef = formRef.value.getComponentRef("templateProps");
const templateList = importTableRef.value.getData();
for (let i = 0; i < templateList.length; i++) {
const tempInputs = templateList[i];
const title = tempInputs.title;
delete tempInputs.title;
let newPipeline = cloneDeep(detail.value.pipeline);
newPipeline = fillPipelineByDefaultForm(newPipeline, form);
//
const steps: any = {};
eachSteps(newPipeline, (step: any) => {
steps[step.id] = step;
});
for (const inputKey in tempInputs) {
const [stepId, key] = inputKey.split(".");
const step = steps[stepId];
if (step) {
step.input[key] = tempInputs[inputKey];
}
}
newPipeline.title = title;
const groupId = form.groupId;
await templateApi.CreatePipelineByTemplate({
title,
content: JSON.stringify(newPipeline),
keepHistoryCount: 30,
groupId,
templateId: parseInt(templateId),
pipeline: {
title: form.title,
templateProps: templateList,
},
});
}
}
</script>
<style lang="less">
.page-template-import {
.ant-table-container {
.ant-select {
width: 100%;
}
}
}
</style>

View File

@ -0,0 +1,115 @@
<template>
<fs-crud ref="crudRef" class="template-import-table" v-bind="crudBinding">
<template #actionbar-right>
<div class="helper ml-10">1. 点击添加按钮添加一行记录 2.输入流水线参数 3. 点击右边确认创建批量创建流水线</div>
</template>
</fs-crud>
</template>
<script setup lang="tsx">
import { computed, onMounted, ref, Ref, nextTick, watch } from "vue";
import { useRoute, useRouter } from "vue-router";
import { templateApi } from "../api";
import { usePluginStore } from "/@/store/plugin";
import { useStepHelper } from "../utils";
import { useFs } from "@fast-crud/fast-crud";
import createCrudOptions from "./crud";
import { Form } from "ant-design-vue";
defineOptions({
name: "TemplateImportTable",
});
const formItemContext = Form.useInjectFormItemContext();
type TemplateDetail = {
template: any;
pipeline: any;
};
const templateProps: Ref = ref({
input: {},
});
const props = defineProps<{
detail: TemplateDetail;
}>();
const pluginStore = usePluginStore();
const { getStepsMap } = useStepHelper(pluginStore);
function buildColumns(steps: any) {
const columns: any = {
title: {
title: "流水线标题",
type: "text",
form: {
component: {
placeholder: "请输入流水线标题",
},
rules: [{ required: true, message: "请输入流水线标题" }],
},
},
};
for (const inputKey in templateProps.value.input) {
const [stepId, key] = inputKey.split(".");
const item = steps[stepId].input[key];
columns[inputKey] = {
title: item.define.title,
type: "text",
form: {
...item.define,
},
column: {},
};
}
return {
table: {
slots: {
headerCell({ column }: any) {
const col = columns[column.key];
if (col && col?.form?.helper) {
return (
<span class={"flex "}>
{col.title}
<a-tooltip title={col.form.helper}>
<fs-icon class={"ml-5"} icon={"ion:alert-circle-outline"}></fs-icon>
</a-tooltip>
</span>
);
}
},
},
},
columns,
};
}
//
const { crudBinding, crudRef, crudExpose, appendCrudOptions } = useFs({ createCrudOptions, context: {} });
onMounted(async () => {
await pluginStore.init();
await nextTick();
const steps = getStepsMap(props.detail.pipeline);
templateProps.value = JSON.parse(props.detail.template?.content ?? "{input:{}}");
appendCrudOptions({ ...buildColumns(steps) });
crudBinding.value.data = [];
await crudExpose.editable.enable({ mode: "row" });
});
defineExpose({
getData() {
return crudBinding.value.data;
},
});
</script>
<style lang="less">
.template-import-table {
.ant-table-container {
.ant-select {
width: 100%;
}
}
}
</style>

View File

@ -8,10 +8,69 @@ import { ref } from "vue";
import { fillPipelineByDefaultForm } from "/@/views/certd/pipeline/certd-form/use";
import { cloneDeep } from "lodash-es";
export function createExtraColumns() {
const groupDictRef = dict({
url: "/pi/pipeline/group/all",
value: "id",
label: "name",
});
const randomHour = Math.floor(Math.random() * 6);
const randomMin = Math.floor(Math.random() * 60);
return {
triggerCron: {
title: "定时触发",
type: "text",
form: {
value: `0 ${randomMin} ${randomHour} * * *`,
component: {
name: "cron-editor",
vModel: "modelValue",
placeholder: "0 0 4 * * *",
},
col: {
span: 24,
},
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: 999,
},
},
};
}
export function useTemplate() {
const { openCrudFormDialog } = useFormWrapper();
async function openCreateFromTemplateDialog(req: { templateId?: number }) {
async function openCreateFromTemplateDialog(req: { templateId?: number; onCreated?: (ctx: any) => void }) {
//检查是否流水线数量超出限制
await checkPipelineLimit();
const detail = await templateApi.GetDetail(req.templateId);
@ -24,12 +83,6 @@ export function useTemplate() {
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) {
@ -38,8 +91,6 @@ export function useTemplate() {
return wrapperRef.value.getFormData();
}
const randomHour = Math.floor(Math.random() * 6);
const randomMin = Math.floor(Math.random() * 60);
const templateFormRef = ref();
async function doSubmit(opts: { form: any }) {
@ -67,13 +118,16 @@ export function useTemplate() {
const title = form.title;
newPipeline.title = title;
const groupId = form.groupId;
await templateApi.CreatePipelineByTemplate({
const { id } = await templateApi.CreatePipelineByTemplate({
title,
content: JSON.stringify(newPipeline),
keepHistoryCount: 30,
groupId,
templateId: detail.template.id,
});
if (req.onCreated) {
req.onCreated({ id });
}
}
const crudOptions = {
@ -104,50 +158,7 @@ export function useTemplate() {
rules: [{ required: true, message: "请输入流水线标题" }],
},
},
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: 99,
},
},
...createExtraColumns(),
},
};

View File

@ -48,10 +48,10 @@ export class TemplateService extends BaseService<TemplateEntity> {
newPipeline.title = template.title + "模版流水线"
newPipeline.templateId = template.id
newPipeline.isTemplate = true
newPipeline.userId = template.userId
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)
@ -121,6 +121,8 @@ export class TemplateService extends BaseService<TemplateEntity> {
}
await this.pipelineService.save(newPipeline)
return newPipeline
}
}