perf: 流水线增加上传证书快捷方式

pull/361/head
xiaojunnuo 2025-03-21 01:02:57 +08:00
parent 8b0daf7200
commit 425bba67c5
15 changed files with 342 additions and 54 deletions

View File

@ -55,6 +55,14 @@ export type PluginDefine = Registrable & {
[key: string]: any; [key: string]: any;
}; };
shortcut?: {
[key: string]: {
title: string;
icon: string;
action: string;
form: any;
};
};
needPlus?: boolean; needPlus?: boolean;
}; };

View File

@ -18,6 +18,47 @@ export type { CertInfo };
runStrategy: RunStrategy.AlwaysRun, runStrategy: RunStrategy.AlwaysRun,
}, },
}, },
shortcut: {
certUpdate: {
title: "上传证书",
icon: "ph:upload",
action: "onCertUpdate",
form: {
columns: {
crt: {
title: "证书",
type: "textarea",
form: {
component: {
name: "pem-input",
vModel: "modelValue",
textarea: {
row: 4,
},
},
rules: [{ required: true, message: "此项必填" }],
col: { span: 24 },
},
},
key: {
title: "私钥",
type: "textarea",
form: {
component: {
name: "pem-input",
vModel: "modelValue",
textarea: {
row: 4,
},
},
rules: [{ required: true, message: "此项必填" }],
col: { span: 24 },
},
},
},
},
},
},
}) })
export class CertApplyUploadPlugin extends CertApplyBaseConvertPlugin { export class CertApplyUploadPlugin extends CertApplyBaseConvertPlugin {
@TaskInput({ @TaskInput({
@ -95,6 +136,8 @@ export class CertApplyUploadPlugin extends CertApplyBaseConvertPlugin {
await this.output(certReader, true); await this.output(certReader, true);
return; return;
} }
async onCertUpdate(data: any) {}
} }
new CertApplyUploadPlugin(); new CertApplyUploadPlugin();

View File

@ -61,7 +61,17 @@ export class AsyncSsh2Client {
this.conn = conn; this.conn = conn;
resolve(this.conn); resolve(this.conn);
}) })
.connect(this.connConf); .connect({
...this.connConf,
algorithms: {
kex: [
"ecdh-sha2-nistp256",
"diffie-hellman-group1-sha1",
"diffie-hellman-group14-sha1", // 示例:添加服务器支持的旧算法
"diffie-hellman-group-exchange-sha256",
],
},
});
} catch (e) { } catch (e) {
reject(e); reject(e);
} }

View File

@ -11,11 +11,13 @@ import LoadingButton from "./loading-button.vue";
import IconSelect from "./icon-select.vue"; import IconSelect from "./icon-select.vue";
import ExpiresTimeText from "./expires-time-text.vue"; import ExpiresTimeText from "./expires-time-text.vue";
import FileInput from "./file-input.vue"; import FileInput from "./file-input.vue";
import PemInput from "./pem-input.vue";
export default { export default {
install(app: any) { install(app: any) {
app.component("PiContainer", PiContainer); app.component("PiContainer", PiContainer);
app.component("TextEditable", TextEditable); app.component("TextEditable", TextEditable);
app.component("FileInput", FileInput); app.component("FileInput", FileInput);
app.component("PemInput", PemInput);
app.component("CronLight", CronLight); app.component("CronLight", CronLight);
app.component("CronEditor", CronEditor); app.component("CronEditor", CronEditor);

View File

@ -0,0 +1,59 @@
<template>
<div class="pem-selector">
<file-input v-bind="fileInput" class="mb-5" type="primary" text="选择文件" @change="onChange" />
<a-textarea v-bind="textarea" v-model:value="textRef"></a-textarea>
</div>
</template>
<script setup lang="ts">
import { notification } from "ant-design-vue";
import { ref, watch, defineEmits } from "vue";
const props = defineProps<{
modelValue?: string;
textarea?: any;
fileInput?: any;
}>();
const emit = defineEmits(["update:modelValue"]);
const textRef = ref();
function emitValue(value: string) {
emit("update:modelValue", value);
}
function onChange(e: any) {
const file = e.target.files[0];
const size = file.size;
if (size > 100 * 1024) {
notification.error({
message: "文件超过100k请选择正确的证书文件",
});
return;
}
const fileReader = new FileReader();
fileReader.onload = function (e: any) {
const value = e.target.result;
emitValue(value);
};
fileReader.readAsText(file); //
}
watch(
() => props.modelValue,
value => {
textRef.value = value;
},
{
immediate: true,
}
);
</script>
<style lang="less">
.pem-selector {
display: flex;
flex-direction: column;
gap: 5px;
}
</style>

View File

@ -23,5 +23,5 @@ export default {
app.component("RemoteSelect", RemoteSelect); app.component("RemoteSelect", RemoteSelect);
app.component("CertDomainsGetter", CertDomainsGetter); app.component("CertDomainsGetter", CertDomainsGetter);
app.component("InputPassword", InputPassword); app.component("InputPassword", InputPassword);
} },
}; };

View File

@ -24,9 +24,9 @@ export async function doRequest(req: RequestHandleReq, opts: any = {}) {
typeName, typeName,
action, action,
data, data,
input input,
}, },
...opts ...opts,
}); });
return res; return res;
} }

View File

@ -1,17 +1,6 @@
// @ts-ignore // @ts-ignore
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { import { AddReq, compute, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, useFormWrapper, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
AddReq,
compute,
CreateCrudOptionsProps,
CreateCrudOptionsRet,
DelReq,
dict,
EditReq,
useFormWrapper,
UserPageQuery,
UserPageRes
} from "@fast-crud/fast-crud";
import { certInfoApi } from "./api"; import { certInfoApi } from "./api";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
@ -166,7 +155,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
show: true, show: true,
}, },
rowHandle: { rowHandle: {
width: 140, width: 100,
fixed: "right", fixed: "right",
buttons: { buttons: {
view: { show: false }, view: { show: false },
@ -195,9 +184,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
}, },
remove: { remove: {
order: 10, order: 10,
show: compute(({ row }) => { show: false,
return row.fromType === "upload";
}),
}, },
}, },
}, },

View File

@ -113,7 +113,6 @@ export function useCertd(certdFormRef: any) {
content: JSON.stringify(pipeline), content: JSON.stringify(pipeline),
keepHistoryCount: 30, keepHistoryCount: 30,
type: "cert", type: "cert",
from: "custom",
}); });
message.success("创建成功,请添加证书部署任务"); message.success("创建成功,请添加证书部署任务");
router.push({ path: "/certd/pipeline/detail", query: { id, editMode: "true" } }); router.push({ path: "/certd/pipeline/detail", query: { id, editMode: "true" } });

View File

@ -406,6 +406,7 @@ export default function ({ crudExpose, context: { certdFormRef, groupDictRef, se
}, },
column: { column: {
sorter: true, sorter: true,
width: 150,
align: "center", align: "center",
}, },
}, },
@ -481,6 +482,34 @@ export default function ({ crudExpose, context: { certdFormRef, groupDictRef, se
component: { component: {
color: "auto", color: "auto",
}, },
sorter: true,
},
},
type: {
title: "类型",
type: "dict-select",
search: {
show: true,
},
dict: dict({
data: [
{ value: "cert", label: "证书申请" },
{ value: "cert_upload", label: "证书上传" },
{ value: "custom", label: "自定义" },
],
}),
form: {
show: false,
value: "custom",
},
column: {
sorter: true,
width: 90,
align: "center",
show: true,
component: {
color: "auto",
},
}, },
}, },
order: { order: {
@ -505,6 +534,7 @@ export default function ({ crudExpose, context: { certdFormRef, groupDictRef, se
column: { column: {
width: 130, width: 130,
show: false, show: false,
sorter: true,
}, },
}, },
createTime: { createTime: {
@ -528,6 +558,7 @@ export default function ({ crudExpose, context: { certdFormRef, groupDictRef, se
column: { column: {
width: 125, width: 125,
show: false, show: false,
sorter: true,
}, },
}, },
}, },

View File

@ -0,0 +1,88 @@
<template>
<a-tooltip :title="title">
<div class="task-shortcut" :title="title" @click="openDialog">
<fs-icon :icon="icon" v-bind="attrs"></fs-icon>
</div>
</a-tooltip>
</template>
<script setup lang="ts">
import { doRequest } from "/@/components/plugins/lib";
import { ref, useAttrs } from "vue";
import { useFormWrapper } from "@fast-crud/fast-crud";
defineOptions({
name: "TaskShortcut",
});
const { openCrudFormDialog } = useFormWrapper();
const props = defineProps<{
icon: string;
title: string;
action: string;
form: any;
input: any;
pluginName: string;
}>();
const attrs = useAttrs();
const loading = ref(false);
async function openDialog() {
function createCrudOptions() {
return {
crudOptions: {
columns: props.form.columns,
form: {
wrapper: {
title: props.title,
saveRemind: false,
},
async doSubmit({ form }: any) {
return await doPluginFormSubmit(form);
},
},
},
};
}
const { crudOptions } = createCrudOptions();
await openCrudFormDialog({ crudOptions });
}
const doPluginFormSubmit = async (formData: any) => {
if (loading.value) {
return;
}
loading.value = true;
try {
const res = await doRequest({
type: "plugin",
typeName: props.pluginName,
action: props.action,
input: props.input,
data: formData,
});
return res;
} finally {
loading.value = false;
}
};
</script>
<style lang="less">
.task-shortcut {
width: 25px;
height: 25px;
border: 1px solid #e3e3e3;
border-radius: 0 0 6px 6px;
background: #fff;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
border-top: 0;
&:hover {
background: #fff;
border-color: #38a0fb;
}
}
</style>

View File

@ -0,0 +1,50 @@
<template>
<div class="task-shortcuts">
<TaskShortcut v-for="(item, index) of shortcuts" :key="index" v-bind="item" />
</div>
</template>
<script setup lang="ts">
import { ref, watch } from "vue";
import * as pluginApi from "/@/views/certd/pipeline/api.plugin";
import TaskShortcut from "./task-shortcut.vue";
defineOptions({
name: "TaskShortcuts",
});
const props = defineProps<{
task: any;
}>();
watch(
() => props.task,
value => {
init();
},
{ immediate: true }
);
const shortcuts = ref([]);
async function init() {
const steps = props.task?.steps || [];
if (steps.length === 0) {
return;
}
const list = [];
for (const step of steps) {
const stepType = step.type;
const pluginDefine = await pluginApi.GetPluginDefine(stepType);
if (pluginDefine.shortcut) {
for (const key in pluginDefine.shortcut) {
const shortcut = pluginDefine.shortcut[key];
list.push({
...shortcut,
pluginName: stepType,
input: step.input,
});
}
}
}
shortcuts.value = list;
}
</script>

View File

@ -79,7 +79,7 @@
class="task-container" class="task-container"
:class="{ :class="{
'first-task': taskIndex === 0, 'first-task': taskIndex === 0,
'validate-error': hasValidateError(task.id) 'validate-error': hasValidateError(task.id),
}" }"
> >
<div class="line line-left"> <div class="line line-left">
@ -114,6 +114,9 @@
<div v-plus class="icon-box task-move-handle action drag"> <div v-plus class="icon-box task-move-handle action drag">
<fs-icon v-if="editMode" title="拖动排序" icon="ion:move-outline"></fs-icon> <fs-icon v-if="editMode" title="拖动排序" icon="ion:move-outline"></fs-icon>
</div> </div>
<div class="shortcut">
<TaskShortcuts :task="task" />
</div>
</div> </div>
</div> </div>
</template> </template>
@ -269,29 +272,30 @@ import PiHistoryTimelineItem from "/@/views/certd/pipeline/pipeline/component/hi
import { FsIcon } from "@fast-crud/fast-crud"; import { FsIcon } from "@fast-crud/fast-crud";
import { useSettingStore } from "/@/store/modules/settings"; import { useSettingStore } from "/@/store/modules/settings";
import { useUserStore } from "/@/store/modules/user"; import { useUserStore } from "/@/store/modules/user";
import TaskShortcuts from "./component/shortcut/task-shortcuts.vue";
export default defineComponent({ export default defineComponent({
name: "PipelineEdit", name: "PipelineEdit",
// eslint-disable-next-line vue/no-unused-components // eslint-disable-next-line vue/no-unused-components
components: { FsIcon, PiHistoryTimelineItem, PiTaskForm, PiTriggerForm, PiTaskView, PiStatusShow, PiNotificationForm, VDraggable }, components: { FsIcon, PiHistoryTimelineItem, PiTaskForm, PiTriggerForm, PiTaskView, PiStatusShow, PiNotificationForm, VDraggable, TaskShortcuts },
props: { props: {
pipelineId: { pipelineId: {
type: [Number, String], type: [Number, String],
default: 0 default: 0,
}, },
historyId: { historyId: {
type: [Number, String], type: [Number, String],
default: 0 default: 0,
}, },
editMode: { editMode: {
type: Boolean, type: Boolean,
default: false default: false,
}, },
options: { options: {
type: Object as PropType<PipelineOptions>, type: Object as PropType<PipelineOptions>,
default() { default() {
return {}; return {};
} },
} },
}, },
emits: ["update:modelValue", "update:editMode"], emits: ["update:modelValue", "update:editMode"],
setup(props, ctx) { setup(props, ctx) {
@ -341,8 +345,9 @@ export default defineComponent({
histories.value = historyList; histories.value = historyList;
if (historyList.length > 0) { if (historyList.length > 0) {
//@ts-ignore
if (props.historyId > 0) { if (props.historyId > 0) {
const found = historyList.find((item) => { const found = historyList.find(item => {
//==int //==int
return item.id == props.historyId; return item.id == props.historyId;
}); });
@ -383,7 +388,7 @@ export default defineComponent({
() => { () => {
return props.editMode; return props.editMode;
}, },
(editMode) => { editMode => {
if (editMode) { if (editMode) {
changeCurrentHistory(); changeCurrentHistory();
} else if (histories.value.length > 0) { } else if (histories.value.length > 0) {
@ -407,7 +412,7 @@ export default defineComponent({
await loadHistoryList(true); await loadHistoryList(true);
}, },
{ {
immediate: true immediate: true,
} }
); );
@ -438,7 +443,7 @@ export default defineComponent({
}; };
return { return {
taskViewOpen, taskViewOpen,
taskViewRef taskViewRef,
}; };
} }
@ -503,7 +508,7 @@ export default defineComponent({
id: nanoid(), id: nanoid(),
title: "新阶段", title: "新阶段",
tasks: [], tasks: [],
status: null status: null,
}; };
//stage: any, stageIndex: number, onSuccess //stage: any, stageIndex: number, onSuccess
useTaskRet.taskAdd(stage, stageIndex, () => { useTaskRet.taskAdd(stage, stageIndex, () => {
@ -519,7 +524,7 @@ export default defineComponent({
} }
return { return {
stageAdd, stageAdd,
isLastStage isLastStage,
}; };
} }
@ -551,7 +556,7 @@ export default defineComponent({
return { return {
triggerAdd, triggerAdd,
triggerEdit, triggerEdit,
triggerFormRef triggerFormRef,
}; };
} }
@ -586,7 +591,7 @@ export default defineComponent({
return { return {
notificationAdd, notificationAdd,
notificationEdit, notificationEdit,
notificationFormRef notificationFormRef,
}; };
} }
@ -604,7 +609,7 @@ export default defineComponent({
}, },
onCancel() { onCancel() {
resolve(false); resolve(false);
} },
}); });
}); });
if (!res) { if (!res) {
@ -628,7 +633,7 @@ export default defineComponent({
watchNewHistoryList(); watchNewHistoryList();
await props.options.doTrigger({ pipelineId: pipeline.value.id, stepId: stepId }); await props.options.doTrigger({ pipelineId: pipeline.value.id, stepId: stepId });
notification.success({ message: "管道已经开始运行" }); notification.success({ message: "管道已经开始运行" });
} },
}); });
}; };
@ -684,10 +689,10 @@ export default defineComponent({
hasError = true; hasError = true;
const message = `任务${step.title}的前置输出步骤${paramName}不存在,请重新修改此任务`; const message = `任务${step.title}的前置输出步骤${paramName}不存在,请重新修改此任务`;
addValidateError(task.id, { addValidateError(task.id, {
message message,
}); });
addValidateError(step.id, { addValidateError(step.id, {
message message,
}); });
errorMessage += message + ""; errorMessage += message + "";
} }
@ -744,7 +749,7 @@ export default defineComponent({
edit, edit,
cancel, cancel,
saveLoading, saveLoading,
hasValidateError hasValidateError,
}; };
} }
@ -767,7 +772,7 @@ export default defineComponent({
historyView, historyView,
historyCancel, historyCancel,
logsCollapse, logsCollapse,
toggleLogsCollapse toggleLogsCollapse,
}; };
} }
@ -829,9 +834,9 @@ export default defineComponent({
...useActions(), ...useActions(),
...useHistory(), ...useHistory(),
...useNotification(), ...useNotification(),
...useScroll() ...useScroll(),
}; };
} },
}); });
</script> </script>
<style lang="less"> <style lang="less">
@ -1034,8 +1039,8 @@ export default defineComponent({
.action { .action {
position: absolute; position: absolute;
right: 60px; right: 10px;
top: 18px; top: 17px;
//font-size: 18px; //font-size: 18px;
cursor: pointer; cursor: pointer;
z-index: 10; z-index: 10;
@ -1043,10 +1048,10 @@ export default defineComponent({
color: #1890ff; color: #1890ff;
} }
&.copy { &.copy {
right: 80px; right: 30px;
} }
&.drag { &.drag {
right: 60px; right: 10px;
cursor: move; cursor: move;
} }
} }
@ -1054,6 +1059,13 @@ export default defineComponent({
.ant-btn { .ant-btn {
width: 200px; width: 200px;
} }
position: relative;
.shortcut {
position: absolute;
bottom: -15px;
left: 20px;
}
} }
} }
} }

View File

@ -112,12 +112,12 @@ export class CertUploadService extends BaseService<CertInfoEntity> {
tasks: [ tasks: [
{ {
id: nanoid(10), id: nanoid(10),
title: "上传证书解析任务", title: "上传证书解析转换",
runnableType: "task", runnableType: "task",
steps: [ steps: [
{ {
id: nanoid(10), id: nanoid(10),
title: "上传证书解析", title: "上传证书解析转换",
runnableType: "step", runnableType: "step",
input: { input: {
certInfoId: newCertInfo.id, certInfoId: newCertInfo.id,
@ -140,8 +140,7 @@ export class CertUploadService extends BaseService<CertInfoEntity> {
const newPipeline = await tx.getRepository(PipelineEntity).save({ const newPipeline = await tx.getRepository(PipelineEntity).save({
userId, userId,
title: pipelineTitle, title: pipelineTitle,
type:"cert", type:"cert_upload",
from:"cert_upload",
content: JSON.stringify(pipeline), content: JSON.stringify(pipeline),
keepHistory:20, keepHistory:20,
}) })

View File

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