Browse Source

Merge pull request #1181 from colinin/implement-webhook-management

Implement webhook management
pull/1211/head
yx lin 11 months ago
committed by GitHub
parent
commit
530b1fa30e
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      apps/vben5/apps/app-antd/package.json
  2. 7
      apps/vben5/apps/app-antd/src/locales/langs/en-US/abp.json
  3. 7
      apps/vben5/apps/app-antd/src/locales/langs/zh-CN/abp.json
  4. 46
      apps/vben5/apps/app-antd/src/router/routes/modules/abp.ts
  5. 15
      apps/vben5/apps/app-antd/src/views/webhooks/definitions/index.vue
  6. 15
      apps/vben5/apps/app-antd/src/views/webhooks/groups/index.vue
  7. 15
      apps/vben5/apps/app-antd/src/views/webhooks/send-attempts/index.vue
  8. 15
      apps/vben5/apps/app-antd/src/views/webhooks/subscriptions/index.vue
  9. 44
      apps/vben5/packages/@abp/request/src/constants/httpStatus.ts
  10. 1
      apps/vben5/packages/@abp/request/src/constants/index.ts
  11. 1
      apps/vben5/packages/@abp/request/src/hooks/index.ts
  12. 76
      apps/vben5/packages/@abp/request/src/hooks/useHttpStatusCodeMap.ts
  13. 1
      apps/vben5/packages/@abp/request/src/index.ts
  14. 46
      apps/vben5/packages/@abp/webhooks/package.json
  15. 4
      apps/vben5/packages/@abp/webhooks/src/api/index.ts
  16. 93
      apps/vben5/packages/@abp/webhooks/src/api/useSendAttemptsApi.ts
  17. 123
      apps/vben5/packages/@abp/webhooks/src/api/useSubscriptionsApi.ts
  18. 91
      apps/vben5/packages/@abp/webhooks/src/api/useWebhookDefinitionsApi.ts
  19. 100
      apps/vben5/packages/@abp/webhooks/src/api/useWebhookGroupDefinitionsApi.ts
  20. 150
      apps/vben5/packages/@abp/webhooks/src/components/definitions/groups/WebhookGroupDefinitionModal.vue
  21. 258
      apps/vben5/packages/@abp/webhooks/src/components/definitions/groups/WebhookGroupDefinitionTable.vue
  22. 357
      apps/vben5/packages/@abp/webhooks/src/components/definitions/webhooks/WebhookDefinitionModal.vue
  23. 342
      apps/vben5/packages/@abp/webhooks/src/components/definitions/webhooks/WebhookDefinitionTable.vue
  24. 4
      apps/vben5/packages/@abp/webhooks/src/components/index.ts
  25. 239
      apps/vben5/packages/@abp/webhooks/src/components/send-attempts/WebhookSendAttemptDrawer.vue
  26. 397
      apps/vben5/packages/@abp/webhooks/src/components/send-attempts/WebhookSendAttemptTable.vue
  27. 309
      apps/vben5/packages/@abp/webhooks/src/components/subscriptions/WebhookSubscriptionModal.vue
  28. 317
      apps/vben5/packages/@abp/webhooks/src/components/subscriptions/WebhookSubscriptionTable.vue
  29. 41
      apps/vben5/packages/@abp/webhooks/src/constants/permissions.ts
  30. 3
      apps/vben5/packages/@abp/webhooks/src/index.ts
  31. 40
      apps/vben5/packages/@abp/webhooks/src/types/definitions.ts
  32. 31
      apps/vben5/packages/@abp/webhooks/src/types/groups.ts
  33. 4
      apps/vben5/packages/@abp/webhooks/src/types/index.ts
  34. 51
      apps/vben5/packages/@abp/webhooks/src/types/sendAttempts.ts
  35. 73
      apps/vben5/packages/@abp/webhooks/src/types/subscriptions.ts
  36. 6
      apps/vben5/packages/@abp/webhooks/tsconfig.json

1
apps/vben5/apps/app-antd/package.json

@ -43,6 +43,7 @@
"@abp/settings": "workspace:*",
"@abp/tasks": "workspace:*",
"@abp/ui": "workspace:*",
"@abp/webhooks": "workspace:*",
"@vben/access": "workspace:*",
"@vben/common-ui": "workspace:*",
"@vben/constants": "workspace:*",

7
apps/vben5/apps/app-antd/src/locales/langs/en-US/abp.json

@ -113,5 +113,12 @@
"jobInfo": {
"title": "Job Manage"
}
},
"webhooks": {
"title": "Webhooks",
"groups": "Groups",
"definitions": "Definitions",
"subscriptions": "Subscriptions",
"sendAttempts": "Send Attempts"
}
}

7
apps/vben5/apps/app-antd/src/locales/langs/zh-CN/abp.json

@ -113,5 +113,12 @@
"jobInfo": {
"title": "作业管理"
}
},
"webhooks": {
"title": "Webhook管理",
"groups": "Webhook分组",
"definitions": "Webhook定义",
"subscriptions": "管理订阅",
"sendAttempts": "发送记录"
}
}

46
apps/vben5/apps/app-antd/src/router/routes/modules/abp.ts

@ -450,6 +450,52 @@ const routes: RouteRecordRaw[] = [
},
],
},
{
meta: {
title: $t('abp.webhooks.title'),
icon: 'material-symbols:webhook',
},
name: 'WebhooksManagement',
path: '/webhooks',
children: [
{
meta: {
title: $t('abp.webhooks.groups'),
icon: 'lucide:group',
},
name: 'WebhookGroupDefinitions',
path: '/webhooks/groups',
component: () => import('#/views/webhooks/groups/index.vue'),
},
{
meta: {
title: $t('abp.webhooks.definitions'),
icon: 'material-symbols:webhook',
},
name: 'WebhookDefinitions',
path: '/webhooks/definitions',
component: () => import('#/views/webhooks/definitions/index.vue'),
},
{
meta: {
title: $t('abp.webhooks.subscriptions'),
icon: 'material-symbols:subscriptions',
},
name: 'WebhookSubscriptions',
path: '/webhooks/subscriptions',
component: () => import('#/views/webhooks/subscriptions/index.vue'),
},
{
meta: {
title: $t('abp.webhooks.sendAttempts'),
icon: 'material-symbols:history',
},
name: 'WebhookSendAttempts',
path: '/webhooks/send-attempts',
component: () => import('#/views/webhooks/send-attempts/index.vue'),
},
],
},
{
name: 'AbpDemo',
path: '/abp/demos',

15
apps/vben5/apps/app-antd/src/views/webhooks/definitions/index.vue

@ -0,0 +1,15 @@
<script lang="ts" setup>
import { Page } from '@vben/common-ui';
import { WebhookDefinitionTable } from '@abp/webhooks';
defineOptions({
name: 'WebhookDefinitions',
});
</script>
<template>
<Page>
<WebhookDefinitionTable />
</Page>
</template>

15
apps/vben5/apps/app-antd/src/views/webhooks/groups/index.vue

@ -0,0 +1,15 @@
<script lang="ts" setup>
import { Page } from '@vben/common-ui';
import { WebhookGroupDefinitionTable } from '@abp/webhooks';
defineOptions({
name: 'WebhookGroupDefinitions',
});
</script>
<template>
<Page>
<WebhookGroupDefinitionTable />
</Page>
</template>

15
apps/vben5/apps/app-antd/src/views/webhooks/send-attempts/index.vue

@ -0,0 +1,15 @@
<script lang="ts" setup>
import { Page } from '@vben/common-ui';
import { WebhookSendAttemptTable } from '@abp/webhooks';
defineOptions({
name: 'WebhookSendAttempts',
});
</script>
<template>
<Page>
<WebhookSendAttemptTable />
</Page>
</template>

15
apps/vben5/apps/app-antd/src/views/webhooks/subscriptions/index.vue

@ -0,0 +1,15 @@
<script lang="ts" setup>
import { Page } from '@vben/common-ui';
import { WebhookSubscriptionTable } from '@abp/webhooks';
defineOptions({
name: 'WebhookSubscriptions',
});
</script>
<template>
<Page>
<WebhookSubscriptionTable />
</Page>
</template>

44
apps/vben5/packages/@abp/request/src/constants/httpStatus.ts

@ -0,0 +1,44 @@
export enum HttpStatusCode {
Accepted = 202,
Ambiguous = 300,
BadGateway = 502,
BadRequest = 400,
Conflict = 409,
Continue = 100,
Created = 201,
ExpectationFailed = 417,
Forbidden = 403,
GatewayTimeout = 504,
Gone = 410,
HttpVersionNotSupported = 505,
InternalServerError = 500,
LengthRequired = 411,
MethodNotAllowed = 405,
Moved = 301,
NoContent = 204,
NonAuthoritativeInformation = 203,
NotAcceptable = 406,
NotFound = 404,
NotImplemented = 501,
NotModified = 304,
OK = 200,
PartialContent = 206,
PaymentRequired = 402,
PreconditionFailed = 412,
ProxyAuthenticationRequired = 407,
Redirect = 302,
RedirectKeepVerb = 307,
RedirectMethod = 303,
RequestedRangeNotSatisfiable = 416,
RequestEntityTooLarge = 413,
RequestTimeout = 408,
RequestUriTooLong = 414,
ResetContent = 205,
ServiceUnavailable = 503,
SwitchingProtocols = 101,
Unauthorized = 401,
UnsupportedMediaType = 415,
Unused = 306,
UpgradeRequired = 426,
UseProxy = 305,
}

1
apps/vben5/packages/@abp/request/src/constants/index.ts

@ -0,0 +1 @@
export * from './httpStatus';

1
apps/vben5/packages/@abp/request/src/hooks/index.ts

@ -1,3 +1,4 @@
export * from './useErrorFormat';
export * from './useHttpStatusCodeMap';
export * from './useRequest';
export * from './useWrapperResult';

76
apps/vben5/packages/@abp/request/src/hooks/useHttpStatusCodeMap.ts

@ -0,0 +1,76 @@
import { HttpStatusCode } from '../constants/httpStatus';
export function useHttpStatusCodeMap() {
const httpStatusCodeMap: { [key: number]: string } = {
[HttpStatusCode.Accepted]: '202 - Accepted',
[HttpStatusCode.Ambiguous]: '300 - Ambiguous/Multiple Choices',
[HttpStatusCode.BadGateway]: '502 - Bad Gateway',
[HttpStatusCode.BadRequest]: '400 - Bad Request',
[HttpStatusCode.Conflict]: '409 - Conflict',
[HttpStatusCode.Continue]: '100 - Continue',
[HttpStatusCode.Created]: '201 - Created',
[HttpStatusCode.ExpectationFailed]: '417 - Expectation Failed',
[HttpStatusCode.Forbidden]: '403 - Forbidden',
[HttpStatusCode.GatewayTimeout]: '504 - Gateway Timeout',
[HttpStatusCode.Gone]: '410 - Gone',
[HttpStatusCode.HttpVersionNotSupported]:
'505 - Http Version Not Supported',
[HttpStatusCode.InternalServerError]: '500 - Internal Server Error',
[HttpStatusCode.LengthRequired]: '411 - Length Required',
[HttpStatusCode.MethodNotAllowed]: '405 - Method Not Allowed',
[HttpStatusCode.Moved]: '301 - Moved/Moved Permanently',
[HttpStatusCode.NoContent]: '204 - No Content',
[HttpStatusCode.NonAuthoritativeInformation]:
'203 - Non Authoritative Information',
[HttpStatusCode.NotAcceptable]: '406 - Not Acceptable',
[HttpStatusCode.NotFound]: '404 - Not Found',
[HttpStatusCode.NotImplemented]: '501 - Not Implemented',
[HttpStatusCode.NotModified]: '304 - Not Modified',
[HttpStatusCode.OK]: '200 - OK',
[HttpStatusCode.PartialContent]: '206 - Partial Content',
[HttpStatusCode.PaymentRequired]: '402 - Payment Required',
[HttpStatusCode.PreconditionFailed]: '412 - Precondition Failed',
[HttpStatusCode.ProxyAuthenticationRequired]:
'407 - Proxy Authentication Required',
[HttpStatusCode.Redirect]: '302 - Found/Redirect',
[HttpStatusCode.RedirectKeepVerb]:
'307 - Redirect Keep Verb/Temporary Redirect',
[HttpStatusCode.RedirectMethod]: '303 - Redirect Method/See Other',
[HttpStatusCode.RequestedRangeNotSatisfiable]:
'416 - Requested Range Not Satisfiable',
[HttpStatusCode.RequestEntityTooLarge]: '413 - Request Entity Too Large',
[HttpStatusCode.RequestTimeout]: '408 - Request Timeout',
[HttpStatusCode.RequestUriTooLong]: '414 - Request Uri Too Long',
[HttpStatusCode.ResetContent]: '205 - Reset Content',
[HttpStatusCode.ServiceUnavailable]: '503 - Service Unavailable',
[HttpStatusCode.SwitchingProtocols]: '101 - Switching Protocols',
[HttpStatusCode.Unauthorized]: '401 - Unauthorized',
[HttpStatusCode.UnsupportedMediaType]: '415 - Unsupported Media Type',
[HttpStatusCode.Unused]: '306 - Unused',
[HttpStatusCode.UpgradeRequired]: '426 - Upgrade Required',
[HttpStatusCode.UseProxy]: '305 - Use Proxy',
};
function getHttpStatusColor(statusCode: HttpStatusCode) {
if (statusCode < 200) {
return 'default';
}
if (statusCode >= 200 && statusCode < 300) {
return 'success';
}
if (statusCode >= 300 && statusCode < 400) {
return 'processing';
}
if (statusCode >= 400 && statusCode < 500) {
return 'warning';
}
if (statusCode >= 500) {
return 'error';
}
}
return {
getHttpStatusColor,
httpStatusCodeMap,
};
}

1
apps/vben5/packages/@abp/request/src/index.ts

@ -6,6 +6,7 @@ import type { RequestClientOptions } from '@vben/request';
import { useAppConfig } from '@vben/hooks';
import { RequestClient } from '@vben/request';
export * from './constants';
export * from './hooks';
export * from './types';

46
apps/vben5/packages/@abp/webhooks/package.json

@ -0,0 +1,46 @@
{
"name": "@abp/webhooks",
"version": "9.0.4",
"homepage": "https://github.com/colinin/abp-next-admin",
"bugs": "https://github.com/colinin/abp-next-admin/issues",
"repository": {
"type": "git",
"url": "git+https://github.com/colinin/abp-next-admin.git",
"directory": "packages/@abp/webhooks"
},
"license": "MIT",
"type": "module",
"sideEffects": [
"**/*.css"
],
"exports": {
".": {
"types": "./src/index.ts",
"default": "./src/index.ts"
}
},
"dependencies": {
"@abp/components": "workspace:*",
"@abp/core": "workspace:*",
"@abp/features": "workspace:*",
"@abp/request": "workspace:*",
"@abp/saas": "workspace:*",
"@abp/ui": "workspace:*",
"@ant-design/icons-vue": "catalog:",
"@vben/access": "workspace:*",
"@vben/common-ui": "workspace:*",
"@vben/hooks": "workspace:*",
"@vben/icons": "workspace:*",
"@vben/layouts": "workspace:*",
"@vben/locales": "workspace:*",
"ant-design-vue": "catalog:",
"lodash.clonedeep": "catalog:",
"lodash.debounce": "catalog:",
"vue": "catalog:*",
"vxe-table": "catalog:"
},
"devDependencies": {
"@types/lodash.clonedeep": "catalog:",
"@types/lodash.debounce": "catalog:"
}
}

4
apps/vben5/packages/@abp/webhooks/src/api/index.ts

@ -0,0 +1,4 @@
export * from './useSendAttemptsApi';
export * from './useSubscriptionsApi';
export * from './useWebhookDefinitionsApi';
export * from './useWebhookGroupDefinitionsApi';

93
apps/vben5/packages/@abp/webhooks/src/api/useSendAttemptsApi.ts

@ -0,0 +1,93 @@
import type { PagedResultDto } from '@abp/core';
import type {
WebhookSendRecordDeleteManyInput,
WebhookSendRecordDto,
WebhookSendRecordGetListInput,
WebhookSendRecordResendManyInput,
} from '../types/sendAttempts';
import { useRequest } from '@abp/request';
export function useSendAttemptsApi() {
const { cancel, request } = useRequest();
/**
*
* @param id Id
* @returns Dto
*/
function getApi(id: string): Promise<WebhookSendRecordDto> {
return request<WebhookSendRecordDto>(`/api/webhooks/send-attempts/${id}`, {
method: 'GET',
});
}
/**
*
* @param id Id
*/
function deleteApi(id: string): Promise<void> {
return request(`/api/webhooks/send-attempts/${id}`, {
method: 'DELETE',
});
}
/**
*
* @param input
*/
function bulkDeleteApi(
input: WebhookSendRecordDeleteManyInput,
): Promise<void> {
return request(`/api/webhooks/send-attempts/delete-many`, {
data: input,
method: 'DELETE',
});
}
/**
*
* @param input
* @returns Dto分页列表
*/
function getPagedListApi(
input: WebhookSendRecordGetListInput,
): Promise<PagedResultDto<WebhookSendRecordDto>> {
return request<PagedResultDto<WebhookSendRecordDto>>(
`/api/webhooks/send-attempts`,
{
method: 'GET',
params: input,
},
);
}
/**
*
* @param id Id
*/
function reSendApi(id: string): Promise<void> {
return request(`/api/webhooks/send-attempts/${id}/resend`, {
method: 'POST',
});
}
/**
*
* @param input
*/
function bulkReSendApi(
input: WebhookSendRecordResendManyInput,
): Promise<void> {
return request(`/api/webhooks/send-attempts/resend-many`, {
data: input,
method: 'POST',
});
}
return {
bulkDeleteApi,
bulkReSendApi,
cancel,
deleteApi,
getApi,
getPagedListApi,
reSendApi,
};
}

123
apps/vben5/packages/@abp/webhooks/src/api/useSubscriptionsApi.ts

@ -0,0 +1,123 @@
import type { ListResultDto, PagedResultDto } from '@abp/core';
import type {
WebhookAvailableGroupDto,
WebhookSubscriptionCreateDto,
WebhookSubscriptionDeleteManyInput,
WebhookSubscriptionDto,
WebhookSubscriptionGetListInput,
WebhookSubscriptionUpdateDto,
} from '../types/subscriptions';
import { useRequest } from '@abp/request';
export function useSubscriptionsApi() {
const { cancel, request } = useRequest();
/**
*
* @param input
* @returns Dto
*/
function createApi(
input: WebhookSubscriptionCreateDto,
): Promise<WebhookSubscriptionDto> {
return request<WebhookSubscriptionDto>(`/api/webhooks/subscriptions`, {
data: input,
method: 'POST',
});
}
/**
*
* @param id Id
*/
function deleteApi(id: string): Promise<void> {
return request(`/api/webhooks/subscriptions/${id}`, {
method: 'DELETE',
});
}
/**
*
* @param input
*/
function bulkDeleteApi(
input: WebhookSubscriptionDeleteManyInput,
): Promise<void> {
return request(`/api/webhooks/subscriptions/delete-many`, {
data: input,
method: 'DELETE',
});
}
/**
* Webhook分组列表
* @returns Webhook分组列表
*/
function getAllAvailableWebhooksApi(): Promise<
ListResultDto<WebhookAvailableGroupDto>
> {
return request<ListResultDto<WebhookAvailableGroupDto>>(
`/api/webhooks/subscriptions/availables`,
{
method: 'GET',
},
);
}
/**
*
* @param id Id
* @returns Dto
*/
function getApi(id: string): Promise<WebhookSubscriptionDto> {
return request<WebhookSubscriptionDto>(
`/api/webhooks/subscriptions/${id}`,
{
method: 'GET',
},
);
}
/**
*
* @param input
* @returns Dto列表
*/
function getPagedListApi(
input: WebhookSubscriptionGetListInput,
): Promise<PagedResultDto<WebhookSubscriptionDto>> {
return request<PagedResultDto<WebhookSubscriptionDto>>(
`/api/webhooks/subscriptions`,
{
method: 'GET',
params: input,
},
);
}
/**
*
* @param id Id
* @param input
* @returns Dto
*/
function updateApi(
id: string,
input: WebhookSubscriptionUpdateDto,
): Promise<WebhookSubscriptionDto> {
return request<WebhookSubscriptionDto>(
`/api/webhooks/subscriptions/${id}`,
{
data: input,
method: 'PUT',
},
);
}
return {
bulkDeleteApi,
cancel,
createApi,
deleteApi,
getAllAvailableWebhooksApi,
getApi,
getPagedListApi,
updateApi,
};
}

91
apps/vben5/packages/@abp/webhooks/src/api/useWebhookDefinitionsApi.ts

@ -0,0 +1,91 @@
import type { ListResultDto } from '@abp/core';
import type {
WebhookDefinitionCreateDto,
WebhookDefinitionDto,
WebhookDefinitionGetListInput,
WebhookDefinitionUpdateDto,
} from '../types/definitions';
import { useRequest } from '@abp/request';
export function useWebhookDefinitionsApi() {
const { cancel, request } = useRequest();
/**
* Webhook定义
* @param name Webhook名称
*/
function deleteApi(name: string): Promise<void> {
return request(`/api/webhooks/definitions/${name}`, {
method: 'DELETE',
});
}
/**
* Webhook定义
* @param name Webhook名称
* @returns Webhook定义数据传输对象
*/
function getApi(name: string): Promise<WebhookDefinitionDto> {
return request<WebhookDefinitionDto>(`/api/webhooks/definitions/${name}`, {
method: 'GET',
});
}
/**
* Webhook定义列表
* @param input Webhook过滤条件
* @returns Webhook定义数据传输对象列表
*/
function getListApi(
input?: WebhookDefinitionGetListInput,
): Promise<ListResultDto<WebhookDefinitionDto>> {
return request<ListResultDto<WebhookDefinitionDto>>(
`/api/webhooks/definitions`,
{
method: 'GET',
params: input,
},
);
}
/**
* Webhook定义
* @param input Webhook定义参数
* @returns Webhook定义数据传输对象
*/
function createApi(
input: WebhookDefinitionCreateDto,
): Promise<WebhookDefinitionDto> {
return request<WebhookDefinitionDto>('/api/webhooks/definitions', {
data: input,
method: 'POST',
});
}
/**
* Webhook定义
* @param name Webhook名称
* @param input Webhook定义参数
* @returns Webhook定义数据传输对象
*/
function updateApi(
name: string,
input: WebhookDefinitionUpdateDto,
): Promise<WebhookDefinitionDto> {
return request<WebhookDefinitionDto>(`/api/webhooks/definitions/${name}`, {
data: input,
method: 'PUT',
});
}
return {
cancel,
createApi,
deleteApi,
getApi,
getListApi,
updateApi,
};
}

100
apps/vben5/packages/@abp/webhooks/src/api/useWebhookGroupDefinitionsApi.ts

@ -0,0 +1,100 @@
import type { ListResultDto } from '@abp/core';
import type {
WebhookGroupDefinitionCreateDto,
WebhookGroupDefinitionDto,
WebhookGroupDefinitionGetListInput,
WebhookGroupDefinitionUpdateDto,
} from '../types/groups';
import { useRequest } from '@abp/request';
export function useWebhookGroupDefinitionsApi() {
const { cancel, request } = useRequest();
/**
* Webhook分组定义
* @param name Webhook分组名称
*/
function deleteApi(name: string): Promise<void> {
return request(`/api/webhooks/definitions/groups/${name}`, {
method: 'DELETE',
});
}
/**
* Webhook分组定义
* @param name Webhook分组名称
* @returns Webhook分组定义数据传输对象
*/
function getApi(name: string): Promise<WebhookGroupDefinitionDto> {
return request<WebhookGroupDefinitionDto>(
`/api/webhooks/definitions/groups/${name}`,
{
method: 'GET',
},
);
}
/**
* Webhook分组定义列表
* @param input Webhook分组过滤条件
* @returns Webhook分组定义数据传输对象列表
*/
function getListApi(
input?: WebhookGroupDefinitionGetListInput,
): Promise<ListResultDto<WebhookGroupDefinitionDto>> {
return request<ListResultDto<WebhookGroupDefinitionDto>>(
`/api/webhooks/definitions/groups`,
{
method: 'GET',
params: input,
},
);
}
/**
* Webhook分组定义
* @param input Webhook分组定义参数
* @returns Webhook分组定义数据传输对象
*/
function createApi(
input: WebhookGroupDefinitionCreateDto,
): Promise<WebhookGroupDefinitionDto> {
return request<WebhookGroupDefinitionDto>(
'/api/webhooks/definitions/groups',
{
data: input,
method: 'POST',
},
);
}
/**
* Webhook分组定义
* @param name Webhook分组名称
* @param input Webhook分组定义参数
* @returns Webhook分组定义数据传输对象
*/
function updateApi(
name: string,
input: WebhookGroupDefinitionUpdateDto,
): Promise<WebhookGroupDefinitionDto> {
return request<WebhookGroupDefinitionDto>(
`/api/webhooks/definitions/groups/${name}`,
{
data: input,
method: 'PUT',
},
);
}
return {
cancel,
createApi,
deleteApi,
getApi,
getListApi,
updateApi,
};
}

150
apps/vben5/packages/@abp/webhooks/src/components/definitions/groups/WebhookGroupDefinitionModal.vue

@ -0,0 +1,150 @@
<script setup lang="ts">
import type { PropertyInfo } from '@abp/ui';
import type { FormInstance } from 'ant-design-vue';
import type { WebhookGroupDefinitionDto } from '../../../types/groups';
import { defineEmits, defineOptions, ref, toValue, useTemplateRef } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { $t } from '@vben/locales';
import { LocalizableInput, PropertyTable } from '@abp/ui';
import { Form, Input, message, Tabs } from 'ant-design-vue';
import { useWebhookGroupDefinitionsApi } from '../../../api/useWebhookGroupDefinitionsApi';
defineOptions({
name: 'WebhookGroupDefinitionModal',
});
const emits = defineEmits<{
(event: 'change', data: WebhookGroupDefinitionDto): void;
}>();
const FormItem = Form.Item;
const TabPane = Tabs.TabPane;
type TabKeys = 'basic' | 'props';
const defaultModel = {} as WebhookGroupDefinitionDto;
const isEditModel = ref(false);
const activeTab = ref<TabKeys>('basic');
const form = useTemplateRef<FormInstance>('form');
const formModel = ref<WebhookGroupDefinitionDto>({ ...defaultModel });
const { cancel, createApi, getApi, updateApi } =
useWebhookGroupDefinitionsApi();
const [Modal, modalApi] = useVbenModal({
class: 'w-1/2',
draggable: true,
fullscreenButton: false,
onCancel() {
modalApi.close();
},
onClosed() {
cancel('WebhookGroupDefinitionModal has closed!');
},
onConfirm: async () => {
await form.value?.validate();
const api = isEditModel.value
? updateApi(formModel.value.name, toValue(formModel))
: createApi(toValue(formModel));
modalApi.setState({ submitting: true });
api
.then((res) => {
message.success($t('AbpUi.SavedSuccessfully'));
emits('change', res);
modalApi.close();
})
.finally(() => {
modalApi.setState({ submitting: false });
});
},
onOpenChange: async (isOpen: boolean) => {
if (isOpen) {
isEditModel.value = false;
activeTab.value = 'basic';
formModel.value = { ...defaultModel };
modalApi.setState({
showConfirmButton: true,
title: $t('WebhooksManagement.GroupDefinitions:AddNew'),
});
try {
modalApi.setState({ loading: true });
const { name } = modalApi.getData<WebhookGroupDefinitionDto>();
name && (await onGet(name));
} finally {
modalApi.setState({ loading: false });
}
}
},
title: $t('WebhooksManagement.GroupDefinitions:AddNew'),
});
async function onGet(name: string) {
isEditModel.value = true;
const dto = await getApi(name);
formModel.value = dto;
modalApi.setState({
showConfirmButton: !dto.isStatic,
title: `${$t('WebhooksManagement.GroupDefinitions')} - ${dto.name}`,
});
}
function onPropChange(prop: PropertyInfo) {
formModel.value.extraProperties ??= {};
formModel.value.extraProperties[prop.key] = prop.value;
}
function onPropDelete(prop: PropertyInfo) {
formModel.value.extraProperties ??= {};
delete formModel.value.extraProperties[prop.key];
}
</script>
<template>
<Modal>
<Form
ref="form"
:label-col="{ span: 6 }"
:model="formModel"
:wrapper-col="{ span: 18 }"
>
<Tabs v-model:active-key="activeTab">
<!-- 基本信息 -->
<TabPane key="basic" :tab="$t('WebhooksManagement.BasicInfo')">
<FormItem
:label="$t('WebhooksManagement.DisplayName:Name')"
name="name"
required
>
<Input
v-model:value="formModel.name"
:disabled="formModel.isStatic || isEditModel"
autocomplete="off"
/>
</FormItem>
<FormItem
:label="$t('WebhooksManagement.DisplayName:DisplayName')"
name="displayName"
required
>
<LocalizableInput
v-model:value="formModel.displayName"
:disabled="formModel.isStatic"
/>
</FormItem>
</TabPane>
<!-- 属性 -->
<TabPane key="props" :tab="$t('WebhooksManagement.Properties')">
<PropertyTable
:data="formModel.extraProperties"
:disabled="formModel.isStatic"
@change="onPropChange"
@delete="onPropDelete"
/>
</TabPane>
</Tabs>
</Form>
</Modal>
</template>
<style scoped></style>

258
apps/vben5/packages/@abp/webhooks/src/components/definitions/groups/WebhookGroupDefinitionTable.vue

@ -0,0 +1,258 @@
<script setup lang="ts">
import type { VxeGridListeners, VxeGridProps } from '@abp/ui';
import type { MenuInfo } from 'ant-design-vue/es/menu/src/interface';
import type { VbenFormProps } from '@vben/common-ui';
import type { WebhookGroupDefinitionDto } from '../../../types/groups';
import { defineAsyncComponent, h, onMounted, reactive, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { createIconifyIcon } from '@vben/icons';
import { $t } from '@vben/locales';
import { useLocalization, useLocalizationSerializer } from '@abp/core';
import { useVbenVxeGrid } from '@abp/ui';
import {
DeleteOutlined,
EditOutlined,
EllipsisOutlined,
PlusOutlined,
} from '@ant-design/icons-vue';
import { Button, Dropdown, Menu, message, Modal } from 'ant-design-vue';
import { useWebhookGroupDefinitionsApi } from '../../../api/useWebhookGroupDefinitionsApi';
import { GroupDefinitionsPermissions } from '../../../constants/permissions';
defineOptions({
name: 'WebhookGroupDefinitionTable',
});
const MenuItem = Menu.Item;
const WebhookIcon = createIconifyIcon('material-symbols:webhook');
const permissionGroups = ref<WebhookGroupDefinitionDto[]>([]);
const pageState = reactive({
current: 1,
size: 10,
total: 0,
});
const { Lr } = useLocalization();
const { deserialize } = useLocalizationSerializer();
const { deleteApi, getListApi } = useWebhookGroupDefinitionsApi();
const formOptions: VbenFormProps = {
collapsed: false,
handleReset: onReset,
async handleSubmit(params) {
pageState.current = 1;
await onGet(params);
},
schema: [
{
component: 'Input',
fieldName: 'filter',
formItemClass: 'col-span-2 items-baseline',
label: $t('AbpUi.Search'),
},
],
showCollapseButton: true,
submitOnEnter: true,
};
const gridOptions: VxeGridProps<WebhookGroupDefinitionDto> = {
columns: [
{
align: 'left',
field: 'name',
minWidth: 150,
title: $t('WebhooksManagement.DisplayName:Name'),
},
{
align: 'left',
field: 'displayName',
minWidth: 150,
title: $t('WebhooksManagement.DisplayName:DisplayName'),
},
{
field: 'action',
fixed: 'right',
slots: { default: 'action' },
title: $t('AbpUi.Actions'),
width: 220,
},
],
exportConfig: {},
keepSource: true,
toolbarConfig: {
custom: true,
export: true,
refresh: false,
zoom: true,
},
};
const gridEvents: VxeGridListeners<WebhookGroupDefinitionDto> = {
pageChange(params) {
pageState.current = params.currentPage;
pageState.size = params.pageSize;
onPageChange();
},
};
const [WebhookGroupDefinitionModal, groupModalApi] = useVbenModal({
connectedComponent: defineAsyncComponent(
() => import('./WebhookGroupDefinitionModal.vue'),
),
});
const [WebhookDefinitionModal, defineModalApi] = useVbenModal({
connectedComponent: defineAsyncComponent(
() => import('../webhooks/WebhookDefinitionModal.vue'),
),
});
const [Grid, gridApi] = useVbenVxeGrid({
formOptions,
gridEvents,
gridOptions,
});
async function onGet(input?: Record<string, string>) {
try {
gridApi.setLoading(true);
const { items } = await getListApi(input);
pageState.total = items.length;
permissionGroups.value = items.map((item) => {
const localizableString = deserialize(item.displayName);
return {
...item,
displayName: Lr(localizableString.resourceName, localizableString.name),
};
});
onPageChange();
} finally {
gridApi.setLoading(false);
}
}
async function onReset() {
await gridApi.formApi.resetForm();
const input = await gridApi.formApi.getValues();
await onGet(input);
}
function onPageChange() {
const items = permissionGroups.value.slice(
(pageState.current - 1) * pageState.size,
pageState.current * pageState.size,
);
gridApi.setGridOptions({
data: items,
pagerConfig: {
currentPage: pageState.current,
pageSize: pageState.size,
total: pageState.total,
},
});
}
function onCreate() {
groupModalApi.setData({});
groupModalApi.open();
}
function onUpdate(row: WebhookGroupDefinitionDto) {
groupModalApi.setData(row);
groupModalApi.open();
}
function onMenuClick(row: WebhookGroupDefinitionDto, info: MenuInfo) {
switch (info.key) {
case 'webhooks': {
defineModalApi.setData({
groupName: row.name,
});
defineModalApi.open();
break;
}
}
}
function onDelete(row: WebhookGroupDefinitionDto) {
Modal.confirm({
centered: true,
content: `${$t('AbpUi.ItemWillBeDeletedMessageWithFormat', [row.name])}`,
onOk: async () => {
await deleteApi(row.name);
message.success($t('AbpUi.DeletedSuccessfully'));
onGet();
},
title: $t('AbpUi.AreYouSure'),
});
}
onMounted(onGet);
</script>
<template>
<Grid :table-title="$t('WebhooksManagement.GroupDefinitions')">
<template #toolbar-tools>
<Button
:icon="h(PlusOutlined)"
type="primary"
v-access:code="[GroupDefinitionsPermissions.Create]"
@click="onCreate"
>
{{ $t('WebhooksManagement.GroupDefinitions:AddNew') }}
</Button>
</template>
<template #action="{ row }">
<div class="flex flex-row">
<div :class="`${row.isStatic ? 'w-full' : 'basis-1/3'}`">
<Button
:icon="h(EditOutlined)"
block
type="link"
v-access:code="[GroupDefinitionsPermissions.Update]"
@click="onUpdate(row)"
>
{{ $t('AbpUi.Edit') }}
</Button>
</div>
<template v-if="!row.isStatic">
<div class="basis-1/3">
<Button
:icon="h(DeleteOutlined)"
block
danger
type="link"
v-access:code="[GroupDefinitionsPermissions.Delete]"
@click="onDelete(row)"
>
{{ $t('AbpUi.Delete') }}
</Button>
</div>
<div class="basis-1/3">
<Dropdown>
<template #overlay>
<Menu @click="(info) => onMenuClick(row, info)">
<MenuItem key="webhooks" :icon="h(WebhookIcon)">
{{ $t('WebhooksManagement.Webhooks:AddNew') }}
</MenuItem>
</Menu>
</template>
<Button :icon="h(EllipsisOutlined)" type="link" />
</Dropdown>
</div>
</template>
</div>
</template>
</Grid>
<WebhookGroupDefinitionModal @change="() => onGet()" />
<WebhookDefinitionModal />
</template>
<style scoped></style>

357
apps/vben5/packages/@abp/webhooks/src/components/definitions/webhooks/WebhookDefinitionModal.vue

@ -0,0 +1,357 @@
<script setup lang="ts">
import type {
FeatureDefinitionDto,
FeatureGroupDefinitionDto,
} from '@abp/features';
import type { PropertyInfo } from '@abp/ui';
import type { FormInstance } from 'ant-design-vue';
import type { WebhookDefinitionDto } from '../../../types/definitions';
import type { WebhookGroupDefinitionDto } from '../../../types/groups';
import {
computed,
defineEmits,
defineOptions,
ref,
toValue,
useTemplateRef,
} from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { $t } from '@vben/locales';
import {
isNullOrWhiteSpace,
listToTree,
useAuthorization,
useLocalization,
useLocalizationSerializer,
} from '@abp/core';
import {
useFeatureDefinitionsApi,
useFeatureGroupDefinitionsApi,
} from '@abp/features';
import { LocalizableInput, PropertyTable, valueTypeSerializer } from '@abp/ui';
import {
Checkbox,
Form,
Input,
message,
Select,
Tabs,
TreeSelect,
} from 'ant-design-vue';
import { useWebhookGroupDefinitionsApi } from '../../../api';
import { useWebhookDefinitionsApi } from '../../../api/useWebhookDefinitionsApi';
defineOptions({
name: 'WebhookDefinitionModal',
});
const emits = defineEmits<{
(event: 'change', data: WebhookDefinitionDto): void;
}>();
const FormItem = Form.Item;
const TabPane = Tabs.TabPane;
interface FeatureTreeData {
checkable?: boolean;
children: FeatureTreeData[];
displayName: string;
groupName: string;
name: string;
}
type TabKeys = 'basic' | 'props';
const defaultModel: WebhookDefinitionDto = {
displayName: '',
extraProperties: {},
groupName: '',
isEnabled: true,
isStatic: false,
name: '',
requiredFeatures: [],
};
const isEditModel = ref(false);
const activeTab = ref<TabKeys>('basic');
const form = useTemplateRef<FormInstance>('form');
const formModel = ref<WebhookDefinitionDto>({ ...defaultModel });
const webhookGroups = ref<WebhookGroupDefinitionDto[]>([]);
const features = ref<FeatureDefinitionDto[]>([]);
const featureGroups = ref<FeatureGroupDefinitionDto[]>([]);
const { isGranted } = useAuthorization();
const { cancel, createApi, getApi, updateApi } = useWebhookDefinitionsApi();
const { getListApi: getGroupDefinitionsApi } = useWebhookGroupDefinitionsApi();
const { getListApi: getFeaturesApi } = useFeatureDefinitionsApi();
const { getListApi: getFeatureGroupsApi } = useFeatureGroupDefinitionsApi();
const { Lr } = useLocalization();
const { deserialize } = useLocalizationSerializer();
const getFeatureOptions = computed(() => {
const featureOptions: FeatureTreeData[] = [];
featureGroups.value.forEach((group) => {
featureOptions.push({
checkable: false,
displayName: group.displayName,
groupName: group.name,
name: group.name,
children: listToTree(
features.value.filter((x) => x.groupName === group.name),
{
id: 'name',
pid: 'parentName',
},
),
});
});
return featureOptions;
});
const getRequiredFeatures = computed(() => {
const featureNames = formModel.value.requiredFeatures ?? [];
const requiredFeatures = features.value
.filter((feature) => featureNames.includes(feature.name))
.map((feature) => {
return {
label: feature.displayName,
value: feature.name,
};
});
return requiredFeatures;
});
const [Modal, modalApi] = useVbenModal({
class: 'w-1/2',
draggable: true,
fullscreenButton: false,
onCancel() {
modalApi.close();
},
onClosed() {
cancel('WebhookDefinitionModal has closed!');
},
onConfirm: async () => {
await form.value?.validate();
const input = toValue(formModel);
const api = isEditModel.value
? updateApi(formModel.value.name, input)
: createApi(input);
try {
modalApi.setState({ submitting: true });
const res = await api;
emits('change', res);
message.success($t('AbpUi.SavedSuccessfully'));
modalApi.close();
} finally {
modalApi.setState({ submitting: false });
}
},
onOpenChange: async (isOpen: boolean) => {
if (isOpen) {
isEditModel.value = false;
activeTab.value = 'basic';
formModel.value = { ...defaultModel };
modalApi.setState({
loading: true,
showConfirmButton: true,
title: $t('WebhooksManagement.Webhooks:AddNew'),
});
try {
const { groupName, name } = modalApi.getData<WebhookDefinitionDto>();
await onInit(groupName);
name && (await onGet(name));
} finally {
modalApi.setState({ loading: false });
}
}
},
title: $t('WebhooksManagement.Webhooks:AddNew'),
});
async function onGet(name: string) {
isEditModel.value = true;
const dto = await getApi(name);
formModel.value = dto;
modalApi.setState({
showConfirmButton: !dto.isStatic,
title: `${$t('WebhooksManagement.WebhookDefinitions')} - ${dto.name}`,
});
}
async function onInit(groupName?: string) {
const [webhookGroupRes, featureGroupItems, featureItems] = await Promise.all([
getGroupDefinitionsApi({ filter: groupName }),
onInitFeatureGroups(),
onInitFeatures(),
]);
featureGroups.value = featureGroupItems;
features.value = featureItems;
webhookGroups.value = webhookGroupRes.items.map((group) => {
const displayName = deserialize(group.displayName);
return {
...group,
displayName: Lr(displayName.resourceName, displayName.name),
};
});
if (webhookGroupRes.items.length === 1) {
formModel.value.groupName = webhookGroupRes.items[0]!.name;
}
}
async function onInitFeatures() {
if (!isGranted(['FeatureManagement.Definitions'])) {
return [];
}
const { items } = await getFeaturesApi();
return items
.filter((item) => {
if (!isNullOrWhiteSpace(item.valueType)) {
const valueType = valueTypeSerializer.deserialize(item.valueType);
if (valueType.validator.name !== 'BOOLEAN') {
return false;
}
}
return true;
})
.map((item) => {
const displayName = deserialize(item.displayName);
const feature = {
...item,
displayName: Lr(displayName.resourceName, displayName.name),
};
return feature;
});
}
async function onInitFeatureGroups() {
if (!isGranted(['FeatureManagement.GroupDefinitions'])) {
return [];
}
const { items } = await getFeatureGroupsApi();
return items.map((item) => {
const displayName = deserialize(item.displayName);
return {
...item,
displayName: Lr(displayName.resourceName, displayName.name),
};
});
}
function onFeaturesChange(value: any[]) {
formModel.value.requiredFeatures = value.map((item) => item.value);
}
function onPropChange(prop: PropertyInfo) {
formModel.value.extraProperties ??= {};
formModel.value.extraProperties[prop.key] = prop.value;
}
function onPropDelete(prop: PropertyInfo) {
formModel.value.extraProperties ??= {};
delete formModel.value.extraProperties[prop.key];
}
</script>
<template>
<Modal>
<Form
ref="form"
:label-col="{ span: 6 }"
:model="formModel"
:wrapper-col="{ span: 18 }"
>
<Tabs v-model:active-key="activeTab">
<!-- 基本信息 -->
<TabPane key="basic" :tab="$t('WebhooksManagement.BasicInfo')">
<FormItem
name="isEnabled"
:label="$t('WebhooksManagement.DisplayName:IsEnabled')"
>
<Checkbox
:disabled="formModel.isStatic"
v-model:checked="formModel.isEnabled"
>
{{ $t('WebhooksManagement.DisplayName:IsEnabled') }}
</Checkbox>
</FormItem>
<FormItem
name="groupName"
:label="$t('WebhooksManagement.DisplayName:GroupName')"
required
>
<Select
:disabled="formModel.isStatic"
:allow-clear="true"
v-model:value="formModel.groupName"
:options="webhookGroups"
:field-names="{ label: 'displayName', value: 'name' }"
/>
</FormItem>
<FormItem
:label="$t('WebhooksManagement.DisplayName:Name')"
name="name"
required
>
<Input
v-model:value="formModel.name"
:disabled="formModel.isStatic"
autocomplete="off"
/>
</FormItem>
<FormItem
:label="$t('WebhooksManagement.DisplayName:DisplayName')"
name="displayName"
required
>
<LocalizableInput
v-model:value="formModel.displayName"
:disabled="formModel.isStatic"
/>
</FormItem>
<FormItem
name="description"
:label="$t('WebhooksManagement.DisplayName:Description')"
>
<LocalizableInput
:disabled="formModel.isStatic"
:allow-clear="true"
v-model:value="formModel.description"
/>
</FormItem>
<FormItem
v-if="
isGranted(
[
'FeatureManagement.GroupDefinitions',
'FeatureManagement.Definitions',
],
true,
)
"
name="requiredFeatures"
:label="$t('WebhooksManagement.DisplayName:RequiredFeatures')"
>
<TreeSelect
:tree-data="getFeatureOptions"
:disabled="formModel.isStatic"
allow-clear
tree-checkable
tree-check-strictly
:field-names="{ label: 'displayName', value: 'name' }"
:value="getRequiredFeatures"
@change="onFeaturesChange"
/>
</FormItem>
</TabPane>
<!-- 属性 -->
<TabPane key="props" :tab="$t('WebhooksManagement.Properties')">
<PropertyTable
:data="formModel.extraProperties"
:disabled="formModel.isStatic"
@change="onPropChange"
@delete="onPropDelete"
/>
</TabPane>
</Tabs>
</Form>
</Modal>
</template>
<style scoped></style>

342
apps/vben5/packages/@abp/webhooks/src/components/definitions/webhooks/WebhookDefinitionTable.vue

@ -0,0 +1,342 @@
<script setup lang="ts">
import type { VxeGridListeners, VxeGridProps } from '@abp/ui';
import type { VbenFormProps } from '@vben/common-ui';
import type { WebhookDefinitionDto } from '../../../types/definitions';
import type { WebhookGroupDefinitionDto } from '../../../types/groups';
import { defineAsyncComponent, h, onMounted, reactive, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { $t } from '@vben/locales';
import {
listToTree,
useLocalization,
useLocalizationSerializer,
} from '@abp/core';
import { useVbenVxeGrid } from '@abp/ui';
import {
CheckOutlined,
CloseOutlined,
DeleteOutlined,
EditOutlined,
PlusOutlined,
} from '@ant-design/icons-vue';
import { Button, message, Modal, Tag } from 'ant-design-vue';
import { VxeGrid } from 'vxe-table';
import { useWebhookDefinitionsApi } from '../../../api/useWebhookDefinitionsApi';
import { useWebhookGroupDefinitionsApi } from '../../../api/useWebhookGroupDefinitionsApi';
import { WebhookDefinitionsPermissions } from '../../../constants/permissions';
defineOptions({
name: 'WebhookDefinitionTable',
});
interface DefinitionItem {
children: DefinitionItem[];
description?: string;
displayName: string;
groupName: string;
isEnabled: boolean;
isStatic: boolean;
name: string;
requiredFeatures?: string[];
}
interface DefinitionGroup {
displayName: string;
items: DefinitionItem[];
name: string;
}
const { Lr } = useLocalization();
const { deserialize } = useLocalizationSerializer();
const { getListApi: getGroupsApi } = useWebhookGroupDefinitionsApi();
const { deleteApi, getListApi: getDefinitionsApi } = useWebhookDefinitionsApi();
const definitionGroups = ref<DefinitionGroup[]>([]);
const pageState = reactive({
current: 1,
size: 10,
total: 0,
});
const formOptions: VbenFormProps = {
//
collapsed: false,
handleReset: onReset,
async handleSubmit(params) {
pageState.current = 1;
await onGet(params);
},
schema: [
{
component: 'Input',
fieldName: 'filter',
formItemClass: 'col-span-2 items-baseline',
label: $t('AbpUi.Search'),
},
],
//
showCollapseButton: true,
//
submitOnEnter: true,
};
const gridOptions: VxeGridProps<WebhookGroupDefinitionDto> = {
columns: [
{
align: 'center',
type: 'seq',
width: 50,
},
{
align: 'left',
field: 'group',
slots: { content: 'group' },
type: 'expand',
width: 50,
},
{
align: 'left',
field: 'name',
minWidth: 150,
title: $t('WebhooksManagement.DisplayName:Name'),
},
{
align: 'left',
field: 'displayName',
minWidth: 150,
title: $t('WebhooksManagement.DisplayName:DisplayName'),
},
],
expandConfig: {
padding: true,
trigger: 'row',
},
exportConfig: {},
keepSource: true,
toolbarConfig: {
custom: true,
export: true,
refresh: false,
zoom: true,
},
};
const subGridColumns: VxeGridProps<WebhookDefinitionDto>['columns'] = [
{
align: 'center',
type: 'seq',
width: 50,
},
{
align: 'left',
field: 'isEnabled',
minWidth: 120,
slots: { default: 'isEnabled' },
title: $t('WebhooksManagement.DisplayName:IsEnabled'),
},
{
align: 'left',
field: 'name',
minWidth: 150,
title: $t('WebhooksManagement.DisplayName:Name'),
treeNode: true,
},
{
align: 'left',
field: 'displayName',
minWidth: 120,
title: $t('WebhooksManagement.DisplayName:DisplayName'),
},
{
align: 'left',
field: 'description',
minWidth: 120,
title: $t('WebhooksManagement.DisplayName:Description'),
},
{
align: 'left',
field: 'requiredFeatures',
minWidth: 150,
slots: { default: 'requiredFeatures' },
title: $t('WebhooksManagement.DisplayName:RequiredFeatures'),
},
{
field: 'action',
fixed: 'right',
slots: { default: 'action' },
title: $t('AbpUi.Actions'),
width: 220,
},
];
const gridEvents: VxeGridListeners<WebhookGroupDefinitionDto> = {
pageChange(params) {
pageState.current = params.currentPage;
pageState.size = params.pageSize;
onPageChange();
},
};
const [GroupGrid, gridApi] = useVbenVxeGrid({
formOptions,
gridEvents,
gridOptions,
});
const [WebhookDefinitionModal, groupModalApi] = useVbenModal({
connectedComponent: defineAsyncComponent(
() => import('./WebhookDefinitionModal.vue'),
),
});
async function onGet(input?: Record<string, string>) {
try {
gridApi.setLoading(true);
const groupRes = await getGroupsApi(input);
const definitionRes = await getDefinitionsApi(input);
pageState.total = groupRes.items.length;
definitionGroups.value = groupRes.items.map((group) => {
const localizableGroup = deserialize(group.displayName);
const definitions = definitionRes.items
.filter((definition) => definition.groupName === group.name)
.map((definition) => {
const displayName = deserialize(definition.displayName);
const description = deserialize(definition.displayName);
return {
...definition,
description: Lr(description.resourceName, description.name),
displayName: Lr(displayName.resourceName, displayName.name),
};
});
return {
...group,
displayName: Lr(localizableGroup.resourceName, localizableGroup.name),
items: listToTree(definitions, {
id: 'name',
pid: 'parentName',
}),
};
});
onPageChange();
} finally {
gridApi.setLoading(false);
}
}
async function onReset() {
await gridApi.formApi.resetForm();
const input = await gridApi.formApi.getValues();
await onGet(input);
}
function onPageChange() {
const items = definitionGroups.value.slice(
(pageState.current - 1) * pageState.size,
pageState.current * pageState.size,
);
gridApi.setGridOptions({
data: items,
pagerConfig: {
currentPage: pageState.current,
pageSize: pageState.size,
total: pageState.total,
},
});
}
function onCreate() {
groupModalApi.setData({});
groupModalApi.open();
}
function onUpdate(row: WebhookDefinitionDto) {
groupModalApi.setData(row);
groupModalApi.open();
}
function onDelete(row: WebhookDefinitionDto) {
Modal.confirm({
centered: true,
content: `${$t('AbpUi.ItemWillBeDeletedMessageWithFormat', [row.name])}`,
onOk: async () => {
await deleteApi(row.name);
message.success($t('AbpUi.DeletedSuccessfully'));
onGet();
},
title: $t('AbpUi.AreYouSure'),
});
}
onMounted(onGet);
</script>
<template>
<GroupGrid :table-title="$t('WebhooksManagement.WebhookDefinitions')">
<template #toolbar-tools>
<Button
:icon="h(PlusOutlined)"
type="primary"
v-access:code="[WebhookDefinitionsPermissions.Create]"
@click="onCreate"
>
{{ $t('WebhooksManagement.Webhooks:AddNew') }}
</Button>
</template>
<template #group="{ row: group }">
<VxeGrid
:columns="subGridColumns"
:data="group.items"
:tree-config="{
trigger: 'row',
rowField: 'name',
childrenField: 'children',
}"
>
<template #isEnabled="{ row }">
<div class="flex flex-row justify-center">
<CheckOutlined v-if="row.isEnabled" class="text-green-500" />
<CloseOutlined v-else class="text-red-500" />
</div>
</template>
<template #requiredFeatures="{ row }">
<div class="flex flex-row justify-center gap-1">
<template v-for="feature in row.requiredFeatures" :key="feature">
<Tag color="blue">{{ feature }}</Tag>
</template>
</div>
</template>
<template #action="{ row: definition }">
<div class="flex flex-row">
<Button
:icon="h(EditOutlined)"
block
type="link"
v-access:code="[WebhookDefinitionsPermissions.Update]"
@click="onUpdate(definition)"
>
{{ $t('AbpUi.Edit') }}
</Button>
<Button
v-if="!definition.isStatic"
:icon="h(DeleteOutlined)"
block
danger
type="link"
v-access:code="[WebhookDefinitionsPermissions.Delete]"
@click="onDelete(definition)"
>
{{ $t('AbpUi.Delete') }}
</Button>
</div>
</template>
</VxeGrid>
</template>
</GroupGrid>
<WebhookDefinitionModal @change="() => onGet()" />
</template>
<style scoped></style>

4
apps/vben5/packages/@abp/webhooks/src/components/index.ts

@ -0,0 +1,4 @@
export { default as WebhookGroupDefinitionTable } from './definitions/groups/WebhookGroupDefinitionTable.vue';
export { default as WebhookDefinitionTable } from './definitions/webhooks/WebhookDefinitionTable.vue';
export { default as WebhookSendAttemptTable } from './send-attempts/WebhookSendAttemptTable.vue';
export { default as WebhookSubscriptionTable } from './subscriptions/WebhookSubscriptionTable.vue';

239
apps/vben5/packages/@abp/webhooks/src/components/send-attempts/WebhookSendAttemptDrawer.vue

@ -0,0 +1,239 @@
<script setup lang="ts">
import type { TenantDto } from '@abp/saas';
import type { WebhookSendRecordDto, WebhookSubscriptionDto } from '../../types';
import { ref } from 'vue';
import { useAccess } from '@vben/access';
import { useVbenDrawer } from '@vben/common-ui';
import { CodeEditor } from '@abp/components/codeeditor';
import { Tinymce } from '@abp/components/tinymce';
import { formatToDateTime, isNullOrWhiteSpace } from '@abp/core';
import { useHttpStatusCodeMap } from '@abp/request';
import { useTenantsApi } from '@abp/saas';
import {
Checkbox,
DatePicker,
Form,
Input,
InputPassword,
Tabs,
Tag,
Textarea,
} from 'ant-design-vue';
import { useSendAttemptsApi, useSubscriptionsApi } from '../../api';
import { WebhookSubscriptionPermissions } from '../../constants/permissions';
const FormItem = Form.Item;
const TabPane = Tabs.TabPane;
type TabKey = 'basic' | 'event' | 'subscriber';
const activeTabKey = ref<TabKey>('basic');
const formModel = ref<WebhookSendRecordDto>();
const webhookSubscription = ref<WebhookSubscriptionDto>();
const webhookTenant = ref<TenantDto>();
const { hasAccessByCodes } = useAccess();
const { getApi } = useSendAttemptsApi();
const { getApi: getTenantApi } = useTenantsApi();
const { getApi: getSubscriptionApi } = useSubscriptionsApi();
const { getHttpStatusColor, httpStatusCodeMap } = useHttpStatusCodeMap();
const [Drawer, drawerApi] = useVbenDrawer({
class: 'w-1/2',
async onOpenChange(isOpen) {
formModel.value = undefined;
webhookTenant.value = undefined;
webhookSubscription.value = undefined;
if (isOpen) {
await onInit();
}
},
});
async function onInit() {
const dto = drawerApi.getData<WebhookSendRecordDto>();
if (isNullOrWhiteSpace(dto.id)) {
return;
}
const [sendRecordDto, subscriptionDto, tenantDto] = await Promise.all([
getApi(dto.id),
onInitSubscription(dto.webhookSubscriptionId),
onInitTenant(dto.tenantId),
]);
formModel.value = sendRecordDto;
webhookSubscription.value = subscriptionDto;
webhookTenant.value = tenantDto;
}
async function onInitSubscription(subscriptionId: string) {
if (!hasAccessByCodes([WebhookSubscriptionPermissions.Default])) {
return undefined;
}
return await getSubscriptionApi(subscriptionId);
}
async function onInitTenant(tenantId?: string) {
// TODO: saas?
if (isNullOrWhiteSpace(tenantId) || !hasAccessByCodes(['AbpSaas.Tenants'])) {
return undefined;
}
return await getTenantApi(tenantId);
}
</script>
<template>
<Drawer :title="$t('WebhooksManagement.SendAttempts')">
<Form :model="formModel" layout="vertical">
<Tabs v-if="formModel" v-model:active-key="activeTabKey">
<TabPane key="basic" :tab="$t('WebhooksManagement.BasicInfo')">
<FormItem
v-if="webhookTenant"
name="tenantId"
:label="$t('WebhooksManagement.DisplayName:TenantId')"
>
<Input :value="webhookTenant.name" disabled />
</FormItem>
<FormItem name="sendExactSameData">
<Checkbox :checked="formModel.sendExactSameData" disabled>
{{ $t('WebhooksManagement.DisplayName:SendExactSameData') }}
</Checkbox>
</FormItem>
<FormItem
name="creationTime"
:label="$t('WebhooksManagement.DisplayName:CreationTime')"
>
<DatePicker
class="w-full"
value-format="YYYY-MM-DD HH:mm:ss"
:value="formModel.creationTime"
disabled
show-time
/>
</FormItem>
<FormItem
name="requestHeaders"
:label="$t('WebhooksManagement.DisplayName:RequestHeaders')"
>
<CodeEditor :value="formModel.requestHeaders" readonly />
</FormItem>
<FormItem
v-if="formModel.responseStatusCode"
name="responseStatusCode"
:label="$t('WebhooksManagement.DisplayName:ResponseStatusCode')"
>
<Tag :color="getHttpStatusColor(formModel.responseStatusCode)">
{{ httpStatusCodeMap[formModel.responseStatusCode] }}
</Tag>
</FormItem>
<FormItem
name="responseHeaders"
:label="$t('WebhooksManagement.DisplayName:ResponseHeaders')"
>
<CodeEditor :value="formModel.responseHeaders" readonly />
</FormItem>
<FormItem
name="response"
:label="$t('WebhooksManagement.DisplayName:Response')"
>
<Tinymce
:value="formModel.response"
:toolbar="[]"
:plugins="[]"
readonly
/>
</FormItem>
</TabPane>
<TabPane key="event" :tab="$t('WebhooksManagement.WebhookEvent')">
<FormItem
name="webhookEventId"
:label="$t('WebhooksManagement.DisplayName:WebhookEventId')"
>
<Input :value="formModel.webhookEventId" disabled />
</FormItem>
<FormItem
:name="['webhookEvent', 'webhookName']"
:label="$t('WebhooksManagement.DisplayName:WebhookName')"
>
<Input :value="formModel.webhookEvent.webhookName" disabled />
</FormItem>
<FormItem
:name="['webhookEvent', 'creationTime']"
:label="$t('WebhooksManagement.DisplayName:CreationTime')"
>
<DatePicker
class="w-full"
value-format="YYYY-MM-DD HH:mm:ss"
:value="formModel.webhookEvent.creationTime"
disabled
show-time
/>
</FormItem>
<FormItem
:name="['webhookEvent', 'data']"
:label="$t('WebhooksManagement.DisplayName:Data')"
>
<CodeEditor :value="formModel.webhookEvent.data" readonly />
</FormItem>
</TabPane>
<TabPane
v-if="webhookSubscription"
key="subscriber"
:tab="$t('WebhooksManagement.Subscriptions')"
>
<FormItem>
<Checkbox :checked="webhookSubscription.isActive" disabled>
{{ $t('WebhooksManagement.DisplayName:IsActive') }}
</Checkbox>
</FormItem>
<FormItem
:label="$t('WebhooksManagement.DisplayName:WebhookSubscriptionId')"
>
<Input :value="webhookSubscription.id" disabled />
</FormItem>
<FormItem :label="$t('WebhooksManagement.DisplayName:WebhookUri')">
<Input :value="webhookSubscription.webhookUri" disabled />
</FormItem>
<FormItem :label="$t('WebhooksManagement.DisplayName:Description')">
<Textarea
:auto-size="{ minRows: 3 }"
:value="webhookSubscription.description"
disabled
/>
</FormItem>
<FormItem :label="$t('WebhooksManagement.DisplayName:Secret')">
<InputPassword :value="webhookSubscription.secret" disabled />
</FormItem>
<FormItem :label="$t('WebhooksManagement.DisplayName:CreationTime')">
<DatePicker
class="w-full"
value-format="YYYY-MM-DD HH:mm:ss"
:value="formatToDateTime(webhookSubscription.creationTime)"
disabled
show-time
/>
</FormItem>
<FormItem :label="$t('WebhooksManagement.DisplayName:Webhooks')">
<template
v-for="webhook in webhookSubscription.webhooks"
:key="webhook"
>
<Tag color="blue">
{{ webhook }}
</Tag>
</template>
</FormItem>
<FormItem :label="$t('WebhooksManagement.DisplayName:Headers')">
<CodeEditor :value="webhookSubscription.headers" readonly />
</FormItem>
</TabPane>
</Tabs>
</Form>
</Drawer>
</template>
<style scoped></style>

397
apps/vben5/packages/@abp/webhooks/src/components/send-attempts/WebhookSendAttemptTable.vue

@ -0,0 +1,397 @@
<script setup lang="ts">
import type { VxeGridListeners, VxeGridProps } from '@abp/ui';
import type { MenuInfo } from 'ant-design-vue/es/menu/src/interface';
import type { VbenFormProps } from '@vben/common-ui';
import type { WebhookSendRecordDto } from '../../types/sendAttempts';
import { defineAsyncComponent, h, ref } from 'vue';
import { useAccess } from '@vben/access';
import { useVbenDrawer } from '@vben/common-ui';
import { createIconifyIcon } from '@vben/icons';
import { $t } from '@vben/locales';
import { formatToDateTime } from '@abp/core';
import { useHttpStatusCodeMap } from '@abp/request';
import { useTenantsApi } from '@abp/saas';
import { useVbenVxeGrid } from '@abp/ui';
import {
DeleteOutlined,
EditOutlined,
EllipsisOutlined,
} from '@ant-design/icons-vue';
import { Button, Dropdown, Menu, message, Modal, Tag } from 'ant-design-vue';
import debounce from 'lodash.debounce';
import { useSendAttemptsApi } from '../../api/useSendAttemptsApi';
import { WebhooksSendAttemptsPermissions } from '../../constants/permissions';
defineOptions({
name: 'WebhookSendAttemptTable',
});
const MenuItem = Menu.Item;
const SendMessageIcon = createIconifyIcon('ant-design:send-outlined');
const { hasAccessByCodes } = useAccess();
const { getPagedListApi: getTenantsApi } = useTenantsApi();
const {
bulkDeleteApi,
bulkReSendApi,
cancel,
deleteApi,
getPagedListApi,
reSendApi,
} = useSendAttemptsApi();
const { getHttpStatusColor, httpStatusCodeMap } = useHttpStatusCodeMap();
const tenantFilter = ref<string>();
const selectedKeys = ref<string[]>([]);
const httpStatusOptions = Object.keys(httpStatusCodeMap).map((key) => {
return {
label: httpStatusCodeMap[Number(key)],
value: key,
};
});
const formOptions: VbenFormProps = {
collapsed: true,
collapsedRows: 3,
commonConfig: {
componentProps: {
class: 'w-full',
},
},
fieldMappingTime: [
[
'creationTime',
['beginCreationTime', 'endCreationTime'],
['YYYY-MM-DD 00:00:00', 'YYYY-MM-DD 23:59:59'],
],
],
schema: [
{
component: 'ApiSelect',
componentProps: () => {
return {
allowClear: true,
api: getTenantsApi,
filterOption: false,
labelField: 'normalizedName',
onSearch: debounce((filter?: string) => {
tenantFilter.value = filter;
}, 500),
params: {
filter: tenantFilter.value || undefined,
},
resultField: 'items',
showSearch: true,
valueField: 'id',
};
},
fieldName: 'tenantId',
label: $t('WebhooksManagement.DisplayName:TenantId'),
},
{
component: 'RangePicker',
fieldName: 'creationTime',
label: $t('WebhooksManagement.DisplayName:CreationTime'),
},
{
component: 'Select',
componentProps: {
allowClear: true,
options: [
{
label: $t('WebhooksManagement.ResponseState:Successed'),
value: 'true',
},
{
label: $t('WebhooksManagement.ResponseState:Failed'),
value: 'false',
},
],
},
fieldName: 'state',
label: $t('WebhooksManagement.DisplayName:State'),
},
{
component: 'Select',
componentProps: {
options: httpStatusOptions,
},
fieldName: 'responseStatusCode',
label: $t('WebhooksManagement.DisplayName:ResponseStatusCode'),
},
{
component: 'Input',
componentProps: {
allowClear: true,
},
fieldName: 'filter',
formItemClass: 'col-span-2 items-baseline',
label: $t('AbpUi.Search'),
},
],
//
showCollapseButton: true,
//
submitOnEnter: true,
};
const gridOptions: VxeGridProps<WebhookSendRecordDto> = {
columns: [
{
align: 'center',
type: 'checkbox',
width: 40,
},
{
align: 'left',
field: 'tenantId',
minWidth: 150,
title: $t('WebhooksManagement.DisplayName:TenantId'),
},
{
align: 'left',
field: 'responseStatusCode',
minWidth: 150,
slots: { default: 'responseStatusCode' },
title: $t('WebhooksManagement.DisplayName:ResponseStatusCode'),
},
{
align: 'left',
field: 'creationTime',
formatter({ cellValue }) {
return cellValue ? formatToDateTime(cellValue) : '';
},
minWidth: 120,
title: $t('WebhooksManagement.DisplayName:CreationTime'),
},
{
align: 'center',
field: 'response',
minWidth: 300,
title: $t('WebhooksManagement.DisplayName:Response'),
},
{
field: 'action',
fixed: 'right',
slots: { default: 'action' },
title: $t('AbpUi.Actions'),
width: 220,
},
],
exportConfig: {},
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getPagedListApi({
maxResultCount: page.pageSize,
skipCount: (page.currentPage - 1) * page.pageSize,
...formValues,
});
},
},
response: {
total: 'totalCount',
list: 'items',
},
},
toolbarConfig: {
custom: true,
export: true,
// import: true,
refresh: true,
zoom: true,
},
};
const gridEvents: VxeGridListeners<WebhookSendRecordDto> = {
checkboxAll: (params) => {
selectedKeys.value = params.records.map((record) => record.id);
},
checkboxChange: (params) => {
selectedKeys.value = params.records.map((record) => record.id);
},
};
const [Grid, gridApi] = useVbenVxeGrid({
formOptions,
gridEvents,
gridOptions,
});
const [WebhookSendAttemptDrawer, drawerApi] = useVbenDrawer({
connectedComponent: defineAsyncComponent(
() => import('./WebhookSendAttemptDrawer.vue'),
),
});
function onUpdate(row: WebhookSendRecordDto) {
drawerApi.setData(row);
drawerApi.open();
}
function onDelete(row: WebhookSendRecordDto) {
Modal.confirm({
centered: true,
content: $t('AbpUi.ItemWillBeDeletedMessageWithFormat', [
$t('WebhooksManagement.SelectedItems'),
]),
onCancel: () => {
cancel();
},
onOk: async () => {
await deleteApi(row.id);
message.success($t('AbpUi.DeletedSuccessfully'));
await gridApi.query();
},
title: $t('AbpUi.AreYouSure'),
});
}
async function onMenuClick(row: WebhookSendRecordDto, info: MenuInfo) {
switch (info.key) {
case 'send': {
await onSend(row);
break;
}
}
}
async function onSend(row: WebhookSendRecordDto) {
Modal.confirm({
centered: true,
content: $t('WebhooksManagement.ItemWillBeResendMessageWithFormat', [
$t('WebhooksManagement.SelectedItems'),
]),
onOk: async () => {
try {
gridApi.setLoading(true);
await reSendApi(row.id);
message.success($t('WebhooksManagement.SuccessfullySent'));
gridApi.query();
} finally {
gridApi.setLoading(false);
}
},
title: $t('AbpUi.AreYouSure'),
});
}
async function onSendMany(keys: string[]) {
Modal.confirm({
centered: true,
content: `${$t('WebhooksManagement.ItemWillBeResendMessageWithFormat', [$t('WebhooksManagement.SelectedItems')])}`,
onOk: async () => {
try {
gridApi.setLoading(true);
await bulkReSendApi({ recordIds: keys });
message.success($t('WebhooksManagement.SuccessfullySent'));
//
gridApi.query();
} finally {
gridApi.setLoading(false);
}
},
title: $t('AbpUi.AreYouSure'),
});
}
async function onDeleteMany(keys: string[]) {
Modal.confirm({
centered: true,
content: `${$t('AbpUi.ItemWillBeDeletedMessageWithFormat', [$t('WebhooksManagement.SelectedItems')])}`,
onOk: async () => {
try {
gridApi.setLoading(true);
await bulkDeleteApi({ recordIds: keys });
message.success($t('AbpUi.DeletedSuccessfully'));
// key
selectedKeys.value = selectedKeys.value.filter(
(key) => !keys.includes(key),
);
//
gridApi.query();
} finally {
gridApi.setLoading(false);
}
},
title: $t('AbpUi.AreYouSure'),
});
}
</script>
<template>
<Grid :table-title="$t('WebhooksManagement.SendAttempts')">
<template #toolbar-tools>
<div v-if="selectedKeys.length > 0" class="flex flex-row gap-2">
<Button
v-if="hasAccessByCodes([WebhooksSendAttemptsPermissions.Resend])"
:icon="h(SendMessageIcon)"
type="primary"
@click="onSendMany(selectedKeys)"
>
{{ $t('WebhooksManagement.Resend') }}
</Button>
<Button
v-if="hasAccessByCodes([WebhooksSendAttemptsPermissions.Delete])"
:icon="h(DeleteOutlined)"
type="primary"
danger
@click="onDeleteMany(selectedKeys)"
>
{{ $t('AbpUi.Delete') }}
</Button>
</div>
</template>
<template #responseStatusCode="{ row }">
<Tag :color="getHttpStatusColor(row.responseStatusCode)">
{{ httpStatusCodeMap[row.responseStatusCode] }}
</Tag>
</template>
<template #action="{ row }">
<div class="flex flex-row">
<Button
v-if="hasAccessByCodes([WebhooksSendAttemptsPermissions.Default])"
:icon="h(EditOutlined)"
type="link"
@click="onUpdate(row)"
>
{{ $t('AbpUi.Edit') }}
</Button>
<Button
v-if="hasAccessByCodes([WebhooksSendAttemptsPermissions.Delete])"
:icon="h(DeleteOutlined)"
danger
type="link"
@click="onDelete(row)"
>
{{ $t('AbpUi.Delete') }}
</Button>
<Dropdown>
<template #overlay>
<Menu @click="(info) => onMenuClick(row, info)">
<MenuItem
v-if="
hasAccessByCodes([WebhooksSendAttemptsPermissions.Resend])
"
key="send"
>
<div class="flex flex-row items-center gap-[4px]">
<SendMessageIcon color="green" />
{{ $t('WebhooksManagement.Resend') }}
</div>
</MenuItem>
</Menu>
</template>
<Button :icon="h(EllipsisOutlined)" type="link" />
</Dropdown>
</div>
</template>
</Grid>
<WebhookSendAttemptDrawer />
</template>
<style lang="scss" scoped></style>

309
apps/vben5/packages/@abp/webhooks/src/components/subscriptions/WebhookSubscriptionModal.vue

@ -0,0 +1,309 @@
<script setup lang="ts">
import type { TenantDto } from '@abp/saas';
import type { PropertyInfo } from '@abp/ui';
import type { FormInstance } from 'ant-design-vue';
import type {
WebhookAvailableGroupDto,
WebhookSubscriptionDto,
} from '../../types';
import { defineEmits, defineOptions, ref, toValue, useTemplateRef } from 'vue';
import { useAccess } from '@vben/access';
import { useVbenModal } from '@vben/common-ui';
import { $t } from '@vben/locales';
import { isNullOrWhiteSpace } from '@abp/core';
import { useTenantsApi } from '@abp/saas';
import { PropertyTable } from '@abp/ui';
import {
Checkbox,
Form,
Input,
InputNumber,
InputPassword,
message,
Select,
Tabs,
Textarea,
Tooltip,
} from 'ant-design-vue';
import debounce from 'lodash.debounce';
import { useSubscriptionsApi } from '../../api/useSubscriptionsApi';
defineOptions({
name: 'WebhookSubscriptionModal',
});
const emits = defineEmits<{
(event: 'change', data: WebhookSubscriptionDto): void;
}>();
const FormItem = Form.Item;
const FormItemRest = Form.ItemRest;
const SelectGroup = Select.OptGroup;
const SelectOption = Select.Option;
const TabPane = Tabs.TabPane;
const defaultModel: WebhookSubscriptionDto = {
creationTime: new Date(),
displayName: '',
extraProperties: {},
id: '',
isActive: true,
isStatic: false,
webhooks: [],
webhookUri: '',
};
type TabActiveKey = 'basic' | 'headers';
const isEditModel = ref(false);
const activeTabKey = ref<TabActiveKey>('basic');
const form = useTemplateRef<FormInstance>('form');
const formModel = ref<WebhookSubscriptionDto>({ ...defaultModel });
const webhookGroups = ref<WebhookAvailableGroupDto[]>([]);
const tenants = ref<TenantDto[]>([]);
const { hasAccessByCodes } = useAccess();
const { getPagedListApi: getTenantsApi } = useTenantsApi();
const { cancel, createApi, getAllAvailableWebhooksApi, getApi, updateApi } =
useSubscriptionsApi();
const [Modal, modalApi] = useVbenModal({
class: 'w-1/2',
draggable: true,
fullscreenButton: false,
onCancel() {
modalApi.close();
},
onClosed() {
cancel('WebhookSubscriptionModal has closed!');
},
onConfirm: async () => {
await form.value?.validate();
const input = toValue(formModel);
const api = isEditModel.value
? updateApi(formModel.value.id, input)
: createApi(input);
try {
modalApi.setState({ submitting: true });
const res = await api;
emits('change', res);
message.success($t('AbpUi.SavedSuccessfully'));
modalApi.close();
} finally {
modalApi.setState({ submitting: false });
}
},
onOpenChange: async (isOpen: boolean) => {
if (isOpen) {
isEditModel.value = false;
activeTabKey.value = 'basic';
formModel.value = { ...defaultModel };
modalApi.setState({
loading: true,
showConfirmButton: true,
title: $t('WebhooksManagement.Subscriptions:AddNew'),
});
try {
const { id } = modalApi.getData<WebhookSubscriptionDto>();
await onInit();
!isNullOrWhiteSpace(id) && (await onGet(id));
} finally {
modalApi.setState({ loading: false });
}
}
},
title: $t('WebhooksManagement.Subscriptions:AddNew'),
});
async function onGet(id: string) {
isEditModel.value = true;
const dto = await getApi(id);
formModel.value = dto;
modalApi.setState({
showConfirmButton: !dto.isStatic,
title: $t('WebhooksManagement.Subscriptions:Edit'),
});
}
async function onInit() {
const [webhookGroupRes, tenantRes] = await Promise.all([
getAllAvailableWebhooksApi(),
onInitTenants(),
]);
webhookGroups.value = webhookGroupRes.items;
tenants.value = tenantRes;
}
async function onInitTenants(filter?: string) {
if (!hasAccessByCodes(['AbpSaas.Tenants'])) {
return [];
}
const { items } = await getTenantsApi({ filter });
return items;
}
function onWebhooksFilter(onputValue: string, option: any) {
if (option.displayname) {
return option.displayname.includes(onputValue);
}
if (option.label) {
return option.label.includes(onputValue);
}
return true;
}
const onTenantsSearch = debounce(async (input?: string) => {
const tenantRes = await onInitTenants(input);
tenants.value = tenantRes;
}, 500);
function onPropChange(prop: PropertyInfo) {
formModel.value.headers ??= {};
formModel.value.headers[prop.key] = prop.value;
}
function onPropDelete(prop: PropertyInfo) {
formModel.value.headers ??= {};
delete formModel.value.headers[prop.key];
}
</script>
<template>
<Modal>
<Form
ref="form"
:label-col="{ span: 6 }"
:model="formModel"
:wrapper-col="{ span: 18 }"
>
<Tabs v-model:active-key="activeTabKey">
<TabPane key="basic" :tab="$t('WebhooksManagement.BasicInfo')">
<FormItem
name="isActive"
:label="$t('WebhooksManagement.DisplayName:IsActive')"
:extra="$t('WebhooksManagement.Description:IsActive')"
>
<Checkbox
:disabled="formModel.isStatic"
v-model:checked="formModel.isActive"
>
{{ $t('WebhooksManagement.DisplayName:IsActive') }}
</Checkbox>
</FormItem>
<FormItem
:label="$t('WebhooksManagement.DisplayName:WebhookUri')"
:extra="$t('WebhooksManagement.Description:WebhookUri')"
name="webhookUri"
required
>
<Input
v-model:value="formModel.webhookUri"
:disabled="formModel.isStatic"
autocomplete="off"
allow-clear
/>
</FormItem>
<FormItem
name="webhooks"
:label="$t('WebhooksManagement.DisplayName:Webhooks')"
:extra="$t('WebhooksManagement.Description:Webhooks')"
>
<Select
:disabled="formModel.isStatic"
allow-clear
mode="multiple"
v-model:value="formModel.webhooks"
:filter-option="onWebhooksFilter"
>
<SelectGroup
v-for="group in webhookGroups"
:key="group.name"
:label="group.displayName"
>
<SelectOption
v-for="option in group.webhooks"
:key="option.name"
:value="option.name"
:displayname="option.displayName"
>
<Tooltip placement="right">
<template #title>
{{ option.description }}
</template>
{{ option.displayName }}
</Tooltip>
</SelectOption>
</SelectGroup>
</Select>
</FormItem>
<FormItem
v-if="hasAccessByCodes(['AbpSaas.Tenants'])"
:label="$t('WebhooksManagement.DisplayName:TenantId')"
:extra="$t('WebhooksManagement.Description:TenantId')"
name="tenantId"
>
<Select
v-model:value="formModel.tenantId"
:disabled="formModel.isStatic"
:options="tenants"
:field-names="{ label: 'normalizedName', value: 'id' }"
:filter-option="false"
@search="onTenantsSearch"
show-search
allow-clear
/>
</FormItem>
<FormItem
:label="$t('WebhooksManagement.DisplayName:TimeoutDuration')"
:extra="$t('WebhooksManagement.Description:TimeoutDuration')"
name="timeoutDuration"
>
<InputNumber
class="w-full"
v-model:value="formModel.timeoutDuration"
:disabled="formModel.isStatic"
:min="10"
:max="300"
autocomplete="off"
/>
</FormItem>
<FormItem
:label="$t('WebhooksManagement.DisplayName:Secret')"
:extra="$t('WebhooksManagement.Description:Secret')"
name="secret"
>
<InputPassword
v-model:value="formModel.secret"
:disabled="formModel.isStatic"
autocomplete="off"
allow-clear
/>
</FormItem>
<FormItem
:label="$t('WebhooksManagement.DisplayName:Description')"
name="description"
>
<Textarea
v-model:value="formModel.description"
:disabled="formModel.isStatic"
:auto-size="{ minRows: 3 }"
autocomplete="off"
allow-clear
/>
</FormItem>
</TabPane>
<TabPane
key="headers"
:tab="$t('WebhooksManagement.DisplayName:Headers')"
>
<FormItemRest>
<PropertyTable
:data="formModel.headers"
:disabled="formModel.isStatic"
@change="onPropChange"
@delete="onPropDelete"
/>
</FormItemRest>
</TabPane>
</Tabs>
</Form>
</Modal>
</template>
<style scoped></style>

317
apps/vben5/packages/@abp/webhooks/src/components/subscriptions/WebhookSubscriptionTable.vue

@ -0,0 +1,317 @@
<script setup lang="ts">
import type { VxeGridListeners, VxeGridProps } from '@abp/ui';
import type { VbenFormProps } from '@vben/common-ui';
import type { WebhookSubscriptionDto } from '../../types/subscriptions';
import { defineAsyncComponent, h, ref } from 'vue';
import { useAccess } from '@vben/access';
import { useVbenModal } from '@vben/common-ui';
import { $t } from '@vben/locales';
import { formatToDateTime } from '@abp/core';
import { useTenantsApi } from '@abp/saas';
import { useVbenVxeGrid } from '@abp/ui';
import {
CheckOutlined,
CloseOutlined,
DeleteOutlined,
EditOutlined,
PlusOutlined,
} from '@ant-design/icons-vue';
import { Button, message, Modal, Tag } from 'ant-design-vue';
import debounce from 'lodash.debounce';
import { useSubscriptionsApi } from '../../api/useSubscriptionsApi';
import { WebhookSubscriptionPermissions } from '../../constants/permissions';
defineOptions({
name: 'WebhookSubscriptionTable',
});
const { hasAccessByCodes } = useAccess();
const { getPagedListApi: getTenantsApi } = useTenantsApi();
const { cancel, deleteApi, getAllAvailableWebhooksApi, getPagedListApi } =
useSubscriptionsApi();
const tenantFilter = ref<string>();
const selectedKeys = ref<string[]>([]);
const formOptions: VbenFormProps = {
collapsed: true,
collapsedRows: 2,
commonConfig: {
componentProps: {
class: 'w-full',
},
},
fieldMappingTime: [
[
'creationTime',
['beginCreationTime', 'endCreationTime'],
['YYYY-MM-DD 00:00:00', 'YYYY-MM-DD 23:59:59'],
],
],
schema: [
{
component: 'ApiSelect',
componentProps: () => {
return {
allowClear: true,
api: getTenantsApi,
filterOption: false,
labelField: 'normalizedName',
onSearch: debounce((filter?: string) => {
tenantFilter.value = filter;
}, 500),
params: {
filter: tenantFilter.value || undefined,
},
resultField: 'items',
showSearch: true,
valueField: 'id',
};
},
fieldName: 'tenantId',
label: $t('WebhooksManagement.DisplayName:TenantId'),
},
{
component: 'ApiSelect',
componentProps: {
allowClear: true,
api: onInitWebhooks,
filterOption: (onputValue: string, option: any) => {
return option.label.includes(onputValue);
},
showSearch: true,
},
fieldName: 'webhooks',
label: $t('WebhooksManagement.DisplayName:Webhooks'),
},
{
component: 'RangePicker',
fieldName: 'creationTime',
label: $t('WebhooksManagement.DisplayName:CreationTime'),
},
{
component: 'Input',
fieldName: 'webhookUri',
formItemClass: 'col-span-2 items-baseline',
label: $t('WebhooksManagement.DisplayName:WebhookUri'),
},
{
component: 'Checkbox',
fieldName: 'isActive',
label: $t('WebhooksManagement.DisplayName:IsActive'),
},
{
component: 'Input',
componentProps: {
allowClear: true,
},
fieldName: 'filter',
formItemClass: 'col-span-3 items-baseline',
label: $t('AbpUi.Search'),
},
],
//
showCollapseButton: true,
//
submitOnEnter: true,
};
const gridOptions: VxeGridProps<WebhookSubscriptionDto> = {
columns: [
{
align: 'center',
fixed: 'left',
type: 'checkbox',
width: 40,
},
{
align: 'left',
field: 'isActive',
fixed: 'left',
minWidth: 150,
slots: { default: 'isActive' },
title: $t('WebhooksManagement.DisplayName:IsActive'),
},
{
align: 'left',
field: 'tenantId',
fixed: 'left',
minWidth: 150,
title: $t('WebhooksManagement.DisplayName:TenantId'),
},
{
align: 'left',
field: 'webhookUri',
fixed: 'left',
minWidth: 300,
title: $t('WebhooksManagement.DisplayName:WebhookUri'),
},
{
align: 'left',
field: 'description',
minWidth: 150,
title: $t('WebhooksManagement.DisplayName:Description'),
},
{
align: 'left',
field: 'creationTime',
formatter({ cellValue }) {
return cellValue ? formatToDateTime(cellValue) : '';
},
minWidth: 120,
title: $t('WebhooksManagement.DisplayName:CreationTime'),
},
{
align: 'center',
field: 'webhooks',
minWidth: 300,
slots: { default: 'webhooks' },
title: $t('WebhooksManagement.DisplayName:Webhooks'),
},
{
field: 'action',
fixed: 'right',
slots: { default: 'action' },
title: $t('AbpUi.Actions'),
width: 200,
},
],
exportConfig: {},
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getPagedListApi({
maxResultCount: page.pageSize,
skipCount: (page.currentPage - 1) * page.pageSize,
...formValues,
});
},
},
response: {
total: 'totalCount',
list: 'items',
},
},
toolbarConfig: {
custom: true,
export: true,
// import: true,
refresh: true,
zoom: true,
},
};
const gridEvents: VxeGridListeners<WebhookSubscriptionDto> = {
checkboxAll: (params) => {
selectedKeys.value = params.records.map((record) => record.id);
},
checkboxChange: (params) => {
selectedKeys.value = params.records.map((record) => record.id);
},
};
const [Grid, gridApi] = useVbenVxeGrid({
formOptions,
gridEvents,
gridOptions,
});
const [WebhookSubscriptionModal, modalApi] = useVbenModal({
connectedComponent: defineAsyncComponent(
() => import('./WebhookSubscriptionModal.vue'),
),
});
async function onInitWebhooks() {
if (hasAccessByCodes([WebhookSubscriptionPermissions.Default])) {
const { items } = await getAllAvailableWebhooksApi();
return items.map((group) => {
return {
label: group.displayName,
options: group.webhooks.map((p) => {
return {
label: p.displayName,
value: p.name,
};
}),
value: group.name,
};
});
}
return [];
}
function onCreate() {
modalApi.setData({});
modalApi.open();
}
function onUpdate(row: WebhookSubscriptionDto) {
modalApi.setData(row);
modalApi.open();
}
function onDelete(row: WebhookSubscriptionDto) {
Modal.confirm({
centered: true,
content: $t('AbpUi.ItemWillBeDeletedMessageWithFormat', [row.title]),
onCancel: () => {
cancel();
},
onOk: async () => {
await deleteApi(row.id);
message.success($t('AbpUi.DeletedSuccessfully'));
await gridApi.query();
},
title: $t('AbpUi.AreYouSure'),
});
}
</script>
<template>
<Grid :table-title="$t('WebhooksManagement.Subscriptions')">
<template #toolbar-tools>
<Button :icon="h(PlusOutlined)" type="primary" @click="onCreate">
{{ $t('WebhooksManagement.Subscriptions:AddNew') }}
</Button>
</template>
<template #isActive="{ row }">
<div class="flex flex-row justify-center">
<CheckOutlined v-if="row.isActive" class="text-green-500" />
<CloseOutlined v-else class="text-red-500" />
</div>
</template>
<template #webhooks="{ row }">
<div class="flex flex-row justify-center gap-1">
<template v-for="webhook in row.webhooks" :key="webhook">
<Tag color="blue">{{ webhook }}</Tag>
</template>
</div>
</template>
<template #action="{ row }">
<div class="flex flex-row">
<Button :icon="h(EditOutlined)" type="link" @click="onUpdate(row)">
{{ $t('AbpUi.Edit') }}
</Button>
<Button
:icon="h(DeleteOutlined)"
danger
type="link"
@click="onDelete(row)"
>
{{ $t('AbpUi.Delete') }}
</Button>
</div>
</template>
</Grid>
<WebhookSubscriptionModal @change="() => gridApi.query()" />
</template>
<style lang="scss" scoped></style>

41
apps/vben5/packages/@abp/webhooks/src/constants/permissions.ts

@ -0,0 +1,41 @@
/** 分组权限 */
export const GroupDefinitionsPermissions = {
/** 新增 */
Create: 'AbpWebhooks.GroupDefinitions.Create',
Default: 'AbpWebhooks.GroupDefinitions',
/** 删除 */
Delete: 'AbpWebhooks.GroupDefinitions.Delete',
/** 更新 */
Update: 'AbpWebhooks.GroupDefinitions.Update',
};
/** Webhook定义权限 */
export const WebhookDefinitionsPermissions = {
/** 新增 */
Create: 'AbpWebhooks.Definitions.Create',
Default: 'AbpWebhooks.Definitions',
/** 删除 */
Delete: 'AbpWebhooks.Definitions.Delete',
/** 更新 */
Update: 'AbpWebhooks.Definitions.Update',
};
/** Webhook订阅权限 */
export const WebhookSubscriptionPermissions = {
/** 新增 */
Create: 'AbpWebhooks.Subscriptions.Create',
Default: 'AbpWebhooks.Subscriptions',
/** 删除 */
Delete: 'AbpWebhooks.Subscriptions.Delete',
/** 更新 */
Update: 'AbpWebhooks.Subscriptions.Update',
};
/** Webhook发送记录权限 */
export const WebhooksSendAttemptsPermissions = {
Default: 'AbpWebhooks.SendAttempts',
/** 删除 */
Delete: 'AbpWebhooks.SendAttempts.Delete',
/** 更新 */
Resend: 'AbpWebhooks.SendAttempts.Resend',
};

3
apps/vben5/packages/@abp/webhooks/src/index.ts

@ -0,0 +1,3 @@
export * from './api';
export * from './components';
export * from './types';

40
apps/vben5/packages/@abp/webhooks/src/types/definitions.ts

@ -0,0 +1,40 @@
import type { IHasConcurrencyStamp, IHasExtraProperties } from '@abp/core';
interface WebhookDefinitionDto extends IHasExtraProperties {
description?: string;
displayName: string;
groupName: string;
isEnabled: boolean;
isStatic: boolean;
name: string;
requiredFeatures?: string[];
}
interface WebhookDefinitionCreateOrUpdateDto extends IHasExtraProperties {
description?: string;
displayName: string;
isEnabled: boolean;
requiredFeatures?: string[];
}
interface WebhookDefinitionCreateDto
extends WebhookDefinitionCreateOrUpdateDto {
groupName: string;
name: string;
}
interface WebhookDefinitionUpdateDto
extends IHasConcurrencyStamp,
WebhookDefinitionCreateOrUpdateDto {}
interface WebhookDefinitionGetListInput {
filter?: string;
groupName?: string;
}
export type {
WebhookDefinitionCreateDto,
WebhookDefinitionDto,
WebhookDefinitionGetListInput,
WebhookDefinitionUpdateDto,
};

31
apps/vben5/packages/@abp/webhooks/src/types/groups.ts

@ -0,0 +1,31 @@
import type { IHasConcurrencyStamp, IHasExtraProperties } from '@abp/core';
interface WebhookGroupDefinitionDto extends IHasExtraProperties {
displayName: string;
isStatic: boolean;
name: string;
}
interface WebhookGroupDefinitionCreateOrUpdateDto extends IHasExtraProperties {
displayName: string;
}
interface WebhookGroupDefinitionCreateDto
extends WebhookGroupDefinitionCreateOrUpdateDto {
name: string;
}
interface WebhookGroupDefinitionUpdateDto
extends IHasConcurrencyStamp,
WebhookGroupDefinitionCreateOrUpdateDto {}
interface WebhookGroupDefinitionGetListInput {
filter?: string;
}
export type {
WebhookGroupDefinitionCreateDto,
WebhookGroupDefinitionDto,
WebhookGroupDefinitionGetListInput,
WebhookGroupDefinitionUpdateDto,
};

4
apps/vben5/packages/@abp/webhooks/src/types/index.ts

@ -0,0 +1,4 @@
export * from './definitions';
export * from './groups';
export * from './sendAttempts';
export * from './subscriptions';

51
apps/vben5/packages/@abp/webhooks/src/types/sendAttempts.ts

@ -0,0 +1,51 @@
import type { EntityDto, PagedAndSortedResultRequestDto } from '@abp/core';
import { HttpStatusCode } from '@abp/request';
interface WebhookEventRecordDto extends EntityDto<string> {
creationTime: string;
data?: string;
tenantId?: string;
webhookName: string;
}
interface WebhookSendRecordDto extends EntityDto<string> {
creationTime: string;
lastModificationTime?: string;
requestHeaders?: Record<string, string>;
response?: string;
responseHeaders?: Record<string, string>;
responseStatusCode?: HttpStatusCode;
sendExactSameData: boolean;
tenantId?: string;
webhookEvent: WebhookEventRecordDto;
webhookEventId: string;
webhookSubscriptionId: string;
}
interface WebhookSendRecordDeleteManyInput {
recordIds: string[];
}
interface WebhookSendRecordResendManyInput {
recordIds: string[];
}
interface WebhookSendRecordGetListInput extends PagedAndSortedResultRequestDto {
beginCreationTime?: Date;
endCreationTime?: Date;
filter?: string;
responseStatusCode?: HttpStatusCode;
state?: boolean;
subscriptionId?: string;
tenantId?: string;
webhookEventId?: string;
}
export type {
WebhookEventRecordDto,
WebhookSendRecordDeleteManyInput,
WebhookSendRecordDto,
WebhookSendRecordGetListInput,
WebhookSendRecordResendManyInput,
};

73
apps/vben5/packages/@abp/webhooks/src/types/subscriptions.ts

@ -0,0 +1,73 @@
import type {
CreationAuditedEntityDto,
IHasConcurrencyStamp,
PagedAndSortedResultRequestDto,
} from '@abp/core';
interface WebhookSubscriptionDto
extends CreationAuditedEntityDto<string>,
IHasConcurrencyStamp {
description?: string;
headers?: Record<string, string>;
isActive: boolean;
secret?: string;
tenantId?: string;
timeoutDuration?: number;
webhooks: string[];
webhookUri: string;
}
interface WebhookSubscriptionCreateOrUpdateDto {
description?: string;
headers?: Record<string, string>;
isActive: boolean;
secret?: string;
tenantId?: string;
timeoutDuration?: number;
webhooks: string[];
webhookUri: string;
}
type WebhookSubscriptionCreateDto = WebhookSubscriptionCreateOrUpdateDto;
interface WebhookSubscriptionUpdateDto
extends IHasConcurrencyStamp,
WebhookSubscriptionCreateOrUpdateDto {}
interface WebhookSubscriptionDeleteManyInput {
recordIds: string[];
}
interface WebhookSubscriptionGetListInput
extends PagedAndSortedResultRequestDto {
beginCreationTime?: Date;
endCreationTime?: Date;
filter?: string;
isActive?: boolean;
secret?: string;
tenantId?: string;
webhooks?: string;
webhookUri?: string;
}
interface WebhookAvailableDto {
description?: string;
displayName: string;
name: string;
}
interface WebhookAvailableGroupDto {
displayName: string;
name: string;
webhooks: WebhookAvailableDto[];
}
export type {
WebhookAvailableDto,
WebhookAvailableGroupDto,
WebhookSubscriptionCreateDto,
WebhookSubscriptionDeleteManyInput,
WebhookSubscriptionDto,
WebhookSubscriptionGetListInput,
WebhookSubscriptionUpdateDto,
};

6
apps/vben5/packages/@abp/webhooks/tsconfig.json

@ -0,0 +1,6 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@vben/tsconfig/web.json",
"include": ["src"],
"exclude": ["node_modules"]
}
Loading…
Cancel
Save