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;
}
let agents = defaultAgents;
if (config.skipSslVerify) {
logger.info('跳过SSL验证');
agents = createAgent({ rejectUnauthorized: false } as any);
if (config.skipSslVerify || config.httpProxy) {
let rejectUnauthorized = true;
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;
config.httpsAgent = agents.httpsAgent;
config.httpAgent = agents.httpAgent;
@ -200,6 +212,7 @@ export type HttpRequestConfig<D = any> = {
skipCheckRes?: boolean;
logParams?: boolean;
logRes?: boolean;
httpProxy?: string;
} & AxiosRequestConfig<D>;
export type HttpClient = {
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)) {
continue;
}
if (notification.type === "email") {
try {
await this.options.emailService?.send({
@ -401,7 +402,16 @@ export class Executor {
} else {
try {
//构建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 notificationCls: any = notificationPlugin.target;
const notificationSender = new notificationCls();

View File

@ -77,6 +77,9 @@ h1, h2, h3, h4, h5, h6 {
.flex-1 {
flex: 1;
}
.flex-0 {
flex: 0;
}
.flex-col {
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) {
return await request({
url: apiPrefix + "/setDefault",
@ -66,6 +73,13 @@ export function createApi() {
});
},
async GetDefineTypes() {
return await request({
url: apiPrefix + "/getTypeDict",
method: "post"
});
},
async GetProviderDefine(type: string) {
return await request({
url: apiPrefix + "/define",

View File

@ -1,12 +1,9 @@
// @ts-ignore
import { useI18n } from "vue-i18n";
import { ref } from "vue";
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 {
const { t } = useI18n();
const api = context.api;
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
return await api.GetList(query);
};

View File

@ -1,162 +1,156 @@
<template>
<div class="notification-selector">
<span v-if="target?.name" class="mr-5 cd-flex-inline">
<a-tag class="mr-5" color="green">{{ target.name }}</a-tag>
<fs-icon class="cd-icon-button" icon="ion:close-circle-outline" @click="clear"></fs-icon>
</span>
<span v-else class="mlr-5 text-gray">{{ placeholder }}</span>
<a-button class="ml-5" :disabled="disabled" :size="size" @click="chooseForm.open"></a-button>
<a-form-item-rest v-if="chooseForm.show">
<a-modal v-model:open="chooseForm.show" title="选择通知渠道" width="905px" @ok="chooseForm.ok">
<div style="height: 400px; position: relative">
<cert-notification-modal v-model="selectedId"></cert-notification-modal>
</div>
</a-modal>
</a-form-item-rest>
<div class="flex-o w-100">
<fs-dict-select
class="flex-1"
:value="modelValue"
:dict="optionsDictRef"
:disabled="disabled"
:render-label="renderLabel"
:slots="selectSlots"
:allow-clear="true"
@update:value="onChange"
/>
<fs-table-select
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>
</template>
<script>
import { defineComponent, reactive, ref, watch, inject } from "vue";
import CertNotificationModal from "./modal/index.vue";
<script lang="tsx" setup>
import { inject, ref, Ref, watch } from "vue";
import { createApi } from "../api";
import { message } from "ant-design-vue";
import { dict } from "@fast-crud/fast-crud";
import createCrudOptions from "../crud";
export default defineComponent({
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();
defineOptions({
name: "NotificationSelector"
});
const target = ref({});
const selectedId = ref();
async function refreshTarget(value) {
selectedId.value = value;
if (value > 0) {
target.value = await api.GetSimpleInfo(value);
}
}
const props = defineProps<{
modelValue?: number | string;
type?: string;
placeholder?: string;
size?: string;
disabled?: boolean;
}>();
async function loadDefault() {
const defId = await api.GetDefaultId();
if (defId) {
await emitValue(defId);
}
}
const onChange = async (value: number) => {
await emitValue(value);
};
loadDefault();
const emit = defineEmits(["update:modelValue", "selectedChange", "change"]);
function clear() {
if (props.disabled) {
return;
}
emitValue(null);
}
const api = createApi();
async function emitValue(value) {
if (pipeline?.value && target?.value && pipeline.value.userId !== target.value.userId) {
message.error("对不起,您不能修改他人流水线的通知");
return;
}
if (value == null) {
selectedId.value = "";
target.value = null;
} else {
selectedId.value = value;
await refreshTarget(selectedId.value);
}
ctx.emit("change", selectedId.value);
ctx.emit("update:modelValue", selectedId.value);
ctx.emit("selectedChange", target.value);
}
watch(
() => {
return props.modelValue;
},
async (value) => {
selectedId.value = null;
target.value = null;
if (value == null) {
return;
}
await refreshTarget(value);
},
// const types = ref({});
// async function loadNotificationTypes() {
// const types = await api.GetDefineTypes();
// const map: any = {};
// for (const item of types) {
// map[item.type] = item;
// }
// types.value = map;
// }
// loadNotificationTypes();
const tableSelectRef = ref();
const optionsDictRef = dict({
url: "/pi/notification/options",
value: "id",
label: "name",
onReady: ({ dict }) => {
const data = [
{
immediate: true
}
);
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;
id: 0,
name: "使用默认通知",
icon: "ion:notifications"
},
ok: () => {
console.log("choose ok:", selectedId.value);
emitValue(selectedId.value);
chooseForm.show = false;
}
});
return {
clear,
target,
selectedId,
providerDefine,
chooseForm
};
...dict.data
];
dict.setData(data);
}
});
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>
<style lang="less">
.notification-selector {
width: 100%;
}
</style>

View File

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

View File

@ -140,4 +140,17 @@ export class NotificationController extends CrudController<NotificationService>
const res = await this.service.setDefault(id, this.getUserId());
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[];
@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) {
if (!this.webhook) {
throw new Error('Webhook URL 不能为空');
@ -49,6 +70,8 @@ export class DiscordNotification extends BaseNotification {
url: this.webhook,
method: 'POST',
data: json,
httpProxy: this.httpsProxy,
skipSslVerify: this.skipSslVerify,
});
}
}

View File

@ -17,6 +17,27 @@ export class SlackNotification extends BaseNotification {
})
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) {
if (!this.webhook) {
throw new Error('token不能为空');
@ -28,6 +49,8 @@ export class SlackNotification extends BaseNotification {
data: {
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,
})
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({
title: 'Bot Token',
component: {
@ -27,6 +37,27 @@ export class TelegramNotification extends BaseNotification {
})
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) {
if (!this.botToken || !this.chatId) {
throw new Error('Bot Token 和聊天ID不能为空');
@ -47,6 +78,8 @@ export class TelegramNotification extends BaseNotification {
text: messageContent,
parse_mode: 'MarkdownV2', // 或使用 'HTML' 取决于需要的格式
},
httpProxy: this.httpsProxy,
skipSslVerify: this.skipSslVerify,
});
}
}