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;
};
shortcut?: {
[key: string]: {
title: string;
icon: string;
action: string;
form: any;
};
};
needPlus?: boolean;
};

View File

@ -18,6 +18,47 @@ export type { CertInfo };
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 {
@TaskInput({
@ -95,6 +136,8 @@ export class CertApplyUploadPlugin extends CertApplyBaseConvertPlugin {
await this.output(certReader, true);
return;
}
async onCertUpdate(data: any) {}
}
new CertApplyUploadPlugin();

View File

@ -61,7 +61,17 @@ export class AsyncSsh2Client {
this.conn = 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) {
reject(e);
}

View File

@ -11,11 +11,13 @@ import LoadingButton from "./loading-button.vue";
import IconSelect from "./icon-select.vue";
import ExpiresTimeText from "./expires-time-text.vue";
import FileInput from "./file-input.vue";
import PemInput from "./pem-input.vue";
export default {
install(app: any) {
app.component("PiContainer", PiContainer);
app.component("TextEditable", TextEditable);
app.component("FileInput", FileInput);
app.component("PemInput", PemInput);
app.component("CronLight", CronLight);
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("CertDomainsGetter", CertDomainsGetter);
app.component("InputPassword", InputPassword);
}
},
};

View File

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

View File

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

View File

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

View File

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

View File

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