perf: 通知选择器优化

pull/265/head
xiaojunnuo 2024-12-02 14:06:55 +08:00
parent 68a503796c
commit 2c0cbdd29e
11 changed files with 274 additions and 150 deletions

View File

@ -98,10 +98,22 @@ export function createAxiosService({ logger }: { logger: Logger }) {
config.timeout = 15000; config.timeout = 15000;
} }
let agents = defaultAgents; let agents = defaultAgents;
if (config.skipSslVerify) { if (config.skipSslVerify || config.httpProxy) {
logger.info('跳过SSL验证'); let rejectUnauthorized = true;
agents = createAgent({ rejectUnauthorized: false } as any); if (config.skipSslVerify) {
logger.info('跳过SSL验证');
rejectUnauthorized = false;
}
const proxy: any = {};
if (config.httpProxy) {
logger.info('使用自定义http代理:', config.httpProxy);
proxy.httpProxy = config.httpProxy;
proxy.httpsProxy = config.httpProxy;
}
agents = createAgent({ rejectUnauthorized, ...proxy } as any);
} }
delete config.skipSslVerify; delete config.skipSslVerify;
config.httpsAgent = agents.httpsAgent; config.httpsAgent = agents.httpsAgent;
config.httpAgent = agents.httpAgent; config.httpAgent = agents.httpAgent;
@ -200,6 +212,7 @@ export type HttpRequestConfig<D = any> = {
skipCheckRes?: boolean; skipCheckRes?: boolean;
logParams?: boolean; logParams?: boolean;
logRes?: boolean; logRes?: boolean;
httpProxy?: string;
} & AxiosRequestConfig<D>; } & AxiosRequestConfig<D>;
export type HttpClient = { export type HttpClient = {
request<D = any, R = any>(config: HttpRequestConfig<D>): Promise<HttpClientResponse<R>>; request<D = any, R = any>(config: HttpRequestConfig<D>): Promise<HttpClientResponse<R>>;

View File

@ -388,6 +388,7 @@ export class Executor {
if (!notification.when.includes(when)) { if (!notification.when.includes(when)) {
continue; continue;
} }
if (notification.type === "email") { if (notification.type === "email") {
try { try {
await this.options.emailService?.send({ await this.options.emailService?.send({
@ -401,7 +402,16 @@ export class Executor {
} else { } else {
try { try {
//构建notification插件发送通知 //构建notification插件发送通知
const notifyConfig = await this.options.notificationService.getById(notification.notificationId); let notifyConfig: any;
if (notification.notificationId === 0) {
notifyConfig = await this.options.notificationService.getDefault();
} else {
notifyConfig = await this.options.notificationService.getById(notification.notificationId);
}
if (notifyConfig == null) {
throw new Error(`通知配置<id:${notification.notificationId}>不存在`);
}
const notificationPlugin = notificationRegistry.get(notifyConfig.type); const notificationPlugin = notificationRegistry.get(notifyConfig.type);
const notificationCls: any = notificationPlugin.target; const notificationCls: any = notificationPlugin.target;
const notificationSender = new notificationCls(); const notificationSender = new notificationCls();

View File

@ -77,6 +77,9 @@ h1, h2, h3, h4, h5, h6 {
.flex-1 { .flex-1 {
flex: 1; flex: 1;
} }
.flex-0 {
flex: 0;
}
.flex-col { .flex-col {
display: flex; display: flex;

View File

@ -43,6 +43,13 @@ export function createApi() {
}); });
}, },
async GetOptions(id: number) {
return await request({
url: apiPrefix + "/options",
method: "post"
});
},
async SetDefault(id: number) { async SetDefault(id: number) {
return await request({ return await request({
url: apiPrefix + "/setDefault", url: apiPrefix + "/setDefault",
@ -66,6 +73,13 @@ export function createApi() {
}); });
}, },
async GetDefineTypes() {
return await request({
url: apiPrefix + "/getTypeDict",
method: "post"
});
},
async GetProviderDefine(type: string) { async GetProviderDefine(type: string) {
return await request({ return await request({
url: apiPrefix + "/define", url: apiPrefix + "/define",

View File

@ -1,12 +1,9 @@
// @ts-ignore
import { useI18n } from "vue-i18n";
import { ref } from "vue"; import { ref } from "vue";
import { getCommonColumnDefine } from "./common"; import { getCommonColumnDefine } from "./common";
import { AddReq, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud"; import { AddReq, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, EditReq, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
import { createApi } from "/@/views/certd/notification/api";
const api = createApi();
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet { export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const { t } = useI18n();
const api = context.api;
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => { const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
return await api.GetList(query); return await api.GetList(query);
}; };

View File

@ -1,162 +1,156 @@
<template> <template>
<div class="notification-selector"> <div class="notification-selector">
<span v-if="target?.name" class="mr-5 cd-flex-inline"> <div class="flex-o w-100">
<a-tag class="mr-5" color="green">{{ target.name }}</a-tag> <fs-dict-select
<fs-icon class="cd-icon-button" icon="ion:close-circle-outline" @click="clear"></fs-icon> class="flex-1"
</span> :value="modelValue"
<span v-else class="mlr-5 text-gray">{{ placeholder }}</span> :dict="optionsDictRef"
<a-button class="ml-5" :disabled="disabled" :size="size" @click="chooseForm.open"></a-button> :disabled="disabled"
<a-form-item-rest v-if="chooseForm.show"> :render-label="renderLabel"
<a-modal v-model:open="chooseForm.show" title="选择通知渠道" width="905px" @ok="chooseForm.ok"> :slots="selectSlots"
<div style="height: 400px; position: relative"> :allow-clear="true"
<cert-notification-modal v-model="selectedId"></cert-notification-modal> @update:value="onChange"
</div> />
</a-modal> <fs-table-select
</a-form-item-rest> ref="tableSelectRef"
class="flex-0"
:model-value="modelValue"
:dict="optionsDictRef"
:create-crud-options="createCrudOptions"
:crud-options-override="{
search: { show: false },
table: {
scroll: {
x: 540
}
}
}"
:show-current="false"
:show-select="false"
:dialog="{ width: 960 }"
:destroy-on-close="false"
@update:model-value="onChange"
>
<template #default="scope">
<fs-button class="ml-5" :disabled="disabled" :size="size" type="primary" icon="ant-design:edit-outlined" @click="scope.open"></fs-button>
</template>
</fs-table-select>
</div>
</div> </div>
</template> </template>
<script> <script lang="tsx" setup>
import { defineComponent, reactive, ref, watch, inject } from "vue"; import { inject, ref, Ref, watch } from "vue";
import CertNotificationModal from "./modal/index.vue";
import { createApi } from "../api"; import { createApi } from "../api";
import { message } from "ant-design-vue"; import { message } from "ant-design-vue";
import { dict } from "@fast-crud/fast-crud";
import createCrudOptions from "../crud";
export default defineComponent({ defineOptions({
name: "NotificationSelector", name: "NotificationSelector"
components: { CertNotificationModal }, });
props: {
modelValue: {
type: [Number, String],
default: null
},
type: {
type: String,
default: ""
},
placeholder: {
type: String,
default: "请选择"
},
size: {
type: String,
default: "middle"
},
disabled: {
type: Boolean,
default: false
},
useDefault: {
type: Boolean,
default: false
}
},
emits: ["update:modelValue", "selectedChange", "change"],
setup(props, ctx) {
const api = createApi();
const target = ref({}); const props = defineProps<{
const selectedId = ref(); modelValue?: number | string;
async function refreshTarget(value) { type?: string;
selectedId.value = value; placeholder?: string;
if (value > 0) { size?: string;
target.value = await api.GetSimpleInfo(value); disabled?: boolean;
} }>();
}
async function loadDefault() { const onChange = async (value: number) => {
const defId = await api.GetDefaultId(); await emitValue(value);
if (defId) { };
await emitValue(defId);
}
}
loadDefault(); const emit = defineEmits(["update:modelValue", "selectedChange", "change"]);
function clear() { const api = createApi();
if (props.disabled) {
return;
}
emitValue(null);
}
async function emitValue(value) { // const types = ref({});
if (pipeline?.value && target?.value && pipeline.value.userId !== target.value.userId) { // async function loadNotificationTypes() {
message.error("对不起,您不能修改他人流水线的通知"); // const types = await api.GetDefineTypes();
return; // const map: any = {};
} // for (const item of types) {
if (value == null) { // map[item.type] = item;
selectedId.value = ""; // }
target.value = null; // types.value = map;
} else { // }
selectedId.value = value; // loadNotificationTypes();
await refreshTarget(selectedId.value); const tableSelectRef = ref();
} const optionsDictRef = dict({
ctx.emit("change", selectedId.value); url: "/pi/notification/options",
ctx.emit("update:modelValue", selectedId.value); value: "id",
ctx.emit("selectedChange", target.value); label: "name",
} onReady: ({ dict }) => {
const data = [
watch(
() => {
return props.modelValue;
},
async (value) => {
selectedId.value = null;
target.value = null;
if (value == null) {
return;
}
await refreshTarget(value);
},
{ {
immediate: true id: 0,
} name: "使用默认通知",
); icon: "ion:notifications"
const providerDefine = ref({});
async function refreshProviderDefine(type) {
providerDefine.value = await api.GetProviderDefine(type);
}
// watch(
// () => {
// return props.type;
// },
// async (value) => {
// await refreshProviderDefine(value);
// },
// {
// immediate: true
// }
// );
//pipeline
const pipeline = inject("pipeline", null);
const chooseForm = reactive({
show: false,
open() {
chooseForm.show = true;
}, },
ok: () => { ...dict.data
console.log("choose ok:", selectedId.value); ];
emitValue(selectedId.value); dict.setData(data);
chooseForm.show = false;
}
});
return {
clear,
target,
selectedId,
providerDefine,
chooseForm
};
} }
}); });
const renderLabel = (option: any) => {
return <span>{option.name}</span>;
};
async function openTableSelectDialog(e: any) {
e.preventDefault();
await tableSelectRef.value.open();
await tableSelectRef.value.crudExpose.openAdd({});
}
const selectSlots = ref({
dropdownRender({ menuNode }: any) {
const res = [];
res.push(menuNode);
res.push(<a-divider style="margin: 4px 0" />);
res.push(<a-space style="padding: 4px 8px" />);
res.push(<fs-button class="w-100" type="text" icon="plus-outlined" text="新建通知渠道" onClick={openTableSelectDialog}></fs-button>);
return res;
}
});
const target: Ref<any> = ref({});
function clear() {
if (props.disabled) {
return;
}
emitValue(null);
}
async function emitValue(value: any) {
const target = optionsDictRef.dataMap[value];
if (value !== 0 && pipeline?.value && target && pipeline.value.userId !== target.userId) {
message.error("对不起,您不能修改他人流水线的通知");
return;
}
emit("change", value);
emit("update:modelValue", value);
emit("selectedChange", target);
}
watch(
() => {
return props.modelValue;
},
async (value) => {
await optionsDictRef.loadDict();
target.value = optionsDictRef.dataMap[value];
},
{
immediate: true
}
);
//pipeline
const pipeline = inject("pipeline", null);
</script> </script>
<style lang="less"> <style lang="less">
.notification-selector { .notification-selector {
width: 100%;
} }
</style> </style>

View File

@ -107,6 +107,7 @@ export default function (certPluginGroup: PluginGroup, formWrapperRef: any): Cre
title: "失败通知", title: "失败通知",
type: "text", type: "text",
form: { form: {
value: 0,
component: { component: {
name: NotificationSelector, name: NotificationSelector,
vModel: "modelValue", vModel: "modelValue",

View File

@ -140,4 +140,17 @@ export class NotificationController extends CrudController<NotificationService>
const res = await this.service.setDefault(id, this.getUserId()); const res = await this.service.setDefault(id, this.getUserId());
return this.ok(res); return this.ok(res);
} }
@Post('/options', { summary: Constants.per.authOnly })
async options() {
const res = await this.service.list({
query: {
userId: this.getUserId(),
},
});
for (const item of res) {
delete item.setting;
}
return this.ok(res);
}
} }

View File

@ -30,6 +30,27 @@ export class DiscordNotification extends BaseNotification {
}) })
mentionedList!: string[]; mentionedList!: string[];
@NotificationInput({
title: '代理',
component: {
placeholder: 'http://xxxxx:xx',
},
helper: '使用https_proxy',
required: false,
})
httpsProxy = '';
@NotificationInput({
title: '忽略证书校验',
value: false,
component: {
name: 'a-switch',
vModel: 'checked',
},
required: false,
})
skipSslVerify: boolean;
async send(body: NotificationBody) { async send(body: NotificationBody) {
if (!this.webhook) { if (!this.webhook) {
throw new Error('Webhook URL 不能为空'); throw new Error('Webhook URL 不能为空');
@ -49,6 +70,8 @@ export class DiscordNotification extends BaseNotification {
url: this.webhook, url: this.webhook,
method: 'POST', method: 'POST',
data: json, data: json,
httpProxy: this.httpsProxy,
skipSslVerify: this.skipSslVerify,
}); });
} }
} }

View File

@ -17,6 +17,27 @@ export class SlackNotification extends BaseNotification {
}) })
webhook = ''; webhook = '';
@NotificationInput({
title: '代理',
component: {
placeholder: 'http://xxxxx:xx',
},
helper: '使用https_proxy',
required: false,
})
httpsProxy = '';
@NotificationInput({
title: '忽略证书校验',
value: false,
component: {
name: 'a-switch',
vModel: 'checked',
},
required: false,
})
skipSslVerify: boolean;
async send(body: NotificationBody) { async send(body: NotificationBody) {
if (!this.webhook) { if (!this.webhook) {
throw new Error('token不能为空'); throw new Error('token不能为空');
@ -28,6 +49,8 @@ export class SlackNotification extends BaseNotification {
data: { data: {
text: `${body.title}\n${body.content}\n[查看详情](${body.url})`, text: `${body.title}\n${body.content}\n[查看详情](${body.url})`,
}, },
httpProxy: this.httpsProxy,
skipSslVerify: this.skipSslVerify,
}); });
} }
} }

View File

@ -7,6 +7,16 @@ import { BaseNotification, IsNotification, NotificationBody, NotificationInput }
needPlus: true, needPlus: true,
}) })
export class TelegramNotification extends BaseNotification { export class TelegramNotification extends BaseNotification {
@NotificationInput({
title: 'URL',
value: 'https://api.telegram.org',
component: {
placeholder: 'https://api.telegram.org',
},
required: true,
})
endpoint = 'https://api.telegram.org';
@NotificationInput({ @NotificationInput({
title: 'Bot Token', title: 'Bot Token',
component: { component: {
@ -27,6 +37,27 @@ export class TelegramNotification extends BaseNotification {
}) })
chatId = ''; chatId = '';
@NotificationInput({
title: '代理',
component: {
placeholder: 'http://xxxxx:xx',
},
helper: '使用https_proxy',
required: false,
})
httpsProxy = '';
@NotificationInput({
title: '忽略证书校验',
value: false,
component: {
name: 'a-switch',
vModel: 'checked',
},
required: false,
})
skipSslVerify: boolean;
async send(body: NotificationBody) { async send(body: NotificationBody) {
if (!this.botToken || !this.chatId) { if (!this.botToken || !this.chatId) {
throw new Error('Bot Token 和聊天ID不能为空'); throw new Error('Bot Token 和聊天ID不能为空');
@ -47,6 +78,8 @@ export class TelegramNotification extends BaseNotification {
text: messageContent, text: messageContent,
parse_mode: 'MarkdownV2', // 或使用 'HTML' 取决于需要的格式 parse_mode: 'MarkdownV2', // 或使用 'HTML' 取决于需要的格式
}, },
httpProxy: this.httpsProxy,
skipSslVerify: this.skipSslVerify,
}); });
} }
} }