payment-kit 1.22.29 → 1.22.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/api/src/libs/util.ts +3 -3
- package/api/src/queues/event.ts +3 -12
- package/api/src/queues/webhook.ts +34 -5
- package/api/src/routes/events.ts +196 -0
- package/blocklet.yml +1 -1
- package/package.json +6 -6
- package/src/components/section/header.tsx +3 -2
- package/src/components/webhook/attempts.tsx +29 -4
package/api/src/libs/util.ts
CHANGED
|
@@ -163,10 +163,10 @@ export function tryWithTimeout(asyncFn: Function, timeout = 5000) {
|
|
|
163
163
|
}
|
|
164
164
|
|
|
165
165
|
// simple exponential delay: 2^retryCount
|
|
166
|
-
export const getNextRetry = (retryCount: number) => {
|
|
166
|
+
export const getNextRetry = (retryCount: number, eventCreatedAt?: Date) => {
|
|
167
167
|
const delay = 2 ** retryCount;
|
|
168
|
-
const
|
|
169
|
-
return
|
|
168
|
+
const baseTime = eventCreatedAt ? dayjs(eventCreatedAt).unix() : dayjs().unix();
|
|
169
|
+
return baseTime + delay;
|
|
170
170
|
};
|
|
171
171
|
|
|
172
172
|
export const getWebhookJobId = (eventId: string, webhookId: string) => {
|
package/api/src/queues/event.ts
CHANGED
|
@@ -3,11 +3,10 @@ import { Op } from 'sequelize';
|
|
|
3
3
|
import { events } from '../libs/event';
|
|
4
4
|
import logger from '../libs/logger';
|
|
5
5
|
import createQueue from '../libs/queue';
|
|
6
|
-
import { getWebhookJobId } from '../libs/util';
|
|
7
6
|
import { Event } from '../store/models/event';
|
|
8
7
|
import { WebhookAttempt } from '../store/models/webhook-attempt';
|
|
9
8
|
import { WebhookEndpoint } from '../store/models/webhook-endpoint';
|
|
10
|
-
import {
|
|
9
|
+
import { addWebhookJob } from './webhook';
|
|
11
10
|
|
|
12
11
|
type EventJob = {
|
|
13
12
|
eventId: string;
|
|
@@ -52,16 +51,8 @@ export const handleEvent = async (job: EventJob) => {
|
|
|
52
51
|
|
|
53
52
|
// we should only push webhook if it's not successfully attempted before
|
|
54
53
|
if (attemptCount === 0) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
if (!exist) {
|
|
58
|
-
logger.info(`Scheduling attempt for event ${event.id} and webhook ${webhook.id}`, job);
|
|
59
|
-
webhookQueue.push({
|
|
60
|
-
id: jobId,
|
|
61
|
-
job: { eventId: event.id, webhookId: webhook.id },
|
|
62
|
-
persist: false,
|
|
63
|
-
});
|
|
64
|
-
}
|
|
54
|
+
logger.info(`Scheduling attempt for event ${event.id} and webhook ${webhook.id}`, job);
|
|
55
|
+
await addWebhookJob(event.id, webhook.id, { persist: false });
|
|
65
56
|
}
|
|
66
57
|
});
|
|
67
58
|
|
|
@@ -110,13 +110,11 @@ export const handleWebhook = async (job: WebhookJob) => {
|
|
|
110
110
|
|
|
111
111
|
if (retryCount < MAX_RETRY_COUNT) {
|
|
112
112
|
process.nextTick(() => {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
job: { eventId: event.id, webhookId: webhook.id },
|
|
116
|
-
runAt: getNextRetry(retryCount),
|
|
113
|
+
addWebhookJob(event.id, webhook.id, {
|
|
114
|
+
runAt: getNextRetry(retryCount, event.created_at),
|
|
117
115
|
persist: false,
|
|
116
|
+
skipExistCheck: true,
|
|
118
117
|
});
|
|
119
|
-
logger.info('scheduled webhook job', { ...job, retryCount });
|
|
120
118
|
});
|
|
121
119
|
} else {
|
|
122
120
|
await event.decrement('pending_webhooks');
|
|
@@ -183,3 +181,34 @@ export const webhookQueue = createQueue<WebhookJob>({
|
|
|
183
181
|
webhookQueue.on('failed', ({ id, job, error }) => {
|
|
184
182
|
logger.error('webhook job failed', { id, job, error });
|
|
185
183
|
});
|
|
184
|
+
|
|
185
|
+
export async function addWebhookJob(
|
|
186
|
+
eventId: string,
|
|
187
|
+
webhookId: string,
|
|
188
|
+
options: {
|
|
189
|
+
runAt?: number;
|
|
190
|
+
persist?: boolean;
|
|
191
|
+
skipExistCheck?: boolean;
|
|
192
|
+
} = {}
|
|
193
|
+
) {
|
|
194
|
+
const { runAt, persist = false, skipExistCheck = false } = options;
|
|
195
|
+
const jobId = getWebhookJobId(eventId, webhookId);
|
|
196
|
+
|
|
197
|
+
if (!skipExistCheck) {
|
|
198
|
+
const exist = await webhookQueue.get(jobId);
|
|
199
|
+
if (exist) {
|
|
200
|
+
logger.info('Webhook job already exists, skipping', { eventId, webhookId, jobId });
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
webhookQueue.push({
|
|
206
|
+
id: jobId,
|
|
207
|
+
job: { eventId, webhookId },
|
|
208
|
+
runAt,
|
|
209
|
+
persist,
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
logger.info('Webhook job added to queue', { eventId, webhookId, jobId, runAt, persist });
|
|
213
|
+
return true;
|
|
214
|
+
}
|
package/api/src/routes/events.ts
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import { Router } from 'express';
|
|
2
2
|
import Joi from 'joi';
|
|
3
3
|
import type { WhereOptions } from 'sequelize';
|
|
4
|
+
import { Op } from 'sequelize';
|
|
4
5
|
|
|
5
6
|
import { createListParamSchema, getOrder } from '../libs/api';
|
|
6
7
|
import { authenticate } from '../libs/security';
|
|
7
8
|
import { Event } from '../store/models/event';
|
|
8
9
|
import { blocklet } from '../libs/auth';
|
|
9
10
|
import logger from '../libs/logger';
|
|
11
|
+
import { addWebhookJob } from '../queues/webhook';
|
|
12
|
+
import { WebhookEndpoint } from '../store/models/webhook-endpoint';
|
|
13
|
+
import { Subscription } from '../store/models/subscription';
|
|
10
14
|
|
|
11
15
|
const router = Router();
|
|
12
16
|
const auth = authenticate<Event>({ component: true, roles: ['owner', 'admin'] });
|
|
@@ -55,6 +59,157 @@ router.get('/', auth, async (req, res) => {
|
|
|
55
59
|
}
|
|
56
60
|
});
|
|
57
61
|
|
|
62
|
+
const retryWebhooksSchema = createListParamSchema<{
|
|
63
|
+
eventType?: string;
|
|
64
|
+
objectType?: string;
|
|
65
|
+
objectId?: string;
|
|
66
|
+
objectIds?: string;
|
|
67
|
+
eventIds?: string;
|
|
68
|
+
subscriptionStatus?: string;
|
|
69
|
+
latestOnly?: boolean;
|
|
70
|
+
}>({
|
|
71
|
+
eventType: Joi.string().empty(''),
|
|
72
|
+
objectType: Joi.string().empty(''),
|
|
73
|
+
objectId: Joi.string().empty(''),
|
|
74
|
+
objectIds: Joi.string().empty(''),
|
|
75
|
+
eventIds: Joi.string().empty(''),
|
|
76
|
+
subscriptionStatus: Joi.string().empty(''),
|
|
77
|
+
latestOnly: Joi.boolean().optional(),
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
router.get('/retry-webhooks', auth, async (req, res) => {
|
|
81
|
+
try {
|
|
82
|
+
const { eventType, objectType, objectId, objectIds, eventIds, subscriptionStatus, latestOnly } =
|
|
83
|
+
await retryWebhooksSchema.validateAsync(req.query, {
|
|
84
|
+
stripUnknown: true,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const where: WhereOptions<Event> = { livemode: req.livemode };
|
|
88
|
+
let targetObjectIds: string[] = [];
|
|
89
|
+
|
|
90
|
+
// Handle subscription status filter
|
|
91
|
+
if (subscriptionStatus) {
|
|
92
|
+
const subscriptions = await Subscription.findAll({
|
|
93
|
+
where: {
|
|
94
|
+
status: subscriptionStatus,
|
|
95
|
+
livemode: req.livemode,
|
|
96
|
+
},
|
|
97
|
+
attributes: ['id'],
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
if (subscriptions.length === 0) {
|
|
101
|
+
return res.json({
|
|
102
|
+
message: `No subscriptions found with status: ${subscriptionStatus}`,
|
|
103
|
+
scheduled: 0,
|
|
104
|
+
eventsProcessed: 0,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
targetObjectIds = subscriptions.map((sub) => sub.id);
|
|
109
|
+
where.object_type = 'subscription';
|
|
110
|
+
where.object_id = { [Op.in]: targetObjectIds };
|
|
111
|
+
|
|
112
|
+
logger.info(`Found ${targetObjectIds.length} subscriptions with status: ${subscriptionStatus}`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Handle explicit object filters
|
|
116
|
+
if (objectType) {
|
|
117
|
+
where.object_type = objectType;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (objectId) {
|
|
121
|
+
where.object_id = objectId;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (objectIds) {
|
|
125
|
+
const ids = objectIds
|
|
126
|
+
.split(',')
|
|
127
|
+
.map((x) => x.trim())
|
|
128
|
+
.filter(Boolean);
|
|
129
|
+
where.object_id = ids.length > 1 ? { [Op.in]: ids } : ids[0];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (eventType) {
|
|
133
|
+
where.type = eventType;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (eventIds) {
|
|
137
|
+
const ids = eventIds
|
|
138
|
+
.split(',')
|
|
139
|
+
.map((x) => x.trim())
|
|
140
|
+
.filter(Boolean);
|
|
141
|
+
where.id = ids.length > 1 ? { [Op.in]: ids } : ids[0];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
let events = await Event.findAll({
|
|
145
|
+
where,
|
|
146
|
+
order: [['created_at', 'DESC']],
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
if (events.length === 0) {
|
|
150
|
+
return res.json({
|
|
151
|
+
message: 'No events found matching the criteria',
|
|
152
|
+
scheduled: 0,
|
|
153
|
+
eventsProcessed: 0,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// If latestOnly is true, group by object_id and keep only the latest event for each
|
|
158
|
+
if (latestOnly) {
|
|
159
|
+
const eventsByObject = new Map<string, Event>();
|
|
160
|
+
events.forEach((event) => {
|
|
161
|
+
if (!eventsByObject.has(event.object_id)) {
|
|
162
|
+
eventsByObject.set(event.object_id, event);
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
events = Array.from(eventsByObject.values());
|
|
166
|
+
logger.info(`Filtered to ${events.length} latest events (from ${eventsByObject.size} unique objects)`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const webhooks = await WebhookEndpoint.findAll({
|
|
170
|
+
where: { status: 'enabled', livemode: req.livemode },
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
if (webhooks.length === 0) {
|
|
174
|
+
return res.json({
|
|
175
|
+
message: 'No enabled webhook endpoints found',
|
|
176
|
+
scheduled: 0,
|
|
177
|
+
eventsProcessed: events.length,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
let scheduled = 0;
|
|
182
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
183
|
+
for (const event of events) {
|
|
184
|
+
const eventWebhooks = webhooks.filter((webhook) => webhook.enabled_events.includes(event.type));
|
|
185
|
+
|
|
186
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
187
|
+
for (const webhook of eventWebhooks) {
|
|
188
|
+
// eslint-disable-next-line no-await-in-loop
|
|
189
|
+
const added = await addWebhookJob(event.id, webhook.id, { persist: false });
|
|
190
|
+
if (added) {
|
|
191
|
+
scheduled += 1;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
logger.info('Batch webhook retry completed', {
|
|
197
|
+
eventsProcessed: events.length,
|
|
198
|
+
webhooksScheduled: scheduled,
|
|
199
|
+
criteria: { eventType, objectType, objectId, subscriptionStatus, latestOnly },
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
return res.json({
|
|
203
|
+
message: `Successfully scheduled ${scheduled} webhooks for retry across ${events.length} events`,
|
|
204
|
+
scheduled,
|
|
205
|
+
eventsProcessed: events.length,
|
|
206
|
+
});
|
|
207
|
+
} catch (err: any) {
|
|
208
|
+
logger.error('Failed to batch retry webhooks', err);
|
|
209
|
+
return res.status(500).json({ error: `Failed to retry webhooks: ${err.message}` });
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
|
|
58
213
|
router.get('/:id', auth, async (req, res) => {
|
|
59
214
|
try {
|
|
60
215
|
const doc = await Event.findOne({
|
|
@@ -77,4 +232,45 @@ router.get('/:id', auth, async (req, res) => {
|
|
|
77
232
|
}
|
|
78
233
|
});
|
|
79
234
|
|
|
235
|
+
router.post('/:id/retry-webhooks', auth, async (req, res) => {
|
|
236
|
+
try {
|
|
237
|
+
const event = await Event.findOne({
|
|
238
|
+
where: { id: req.params.id },
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
if (!event) {
|
|
242
|
+
return res.status(404).json({ error: 'Event not found' });
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const webhooks = await WebhookEndpoint.findAll({
|
|
246
|
+
where: { status: 'enabled', livemode: event.livemode },
|
|
247
|
+
});
|
|
248
|
+
const eventWebhooks = webhooks.filter((webhook) => webhook.enabled_events.includes(event.type));
|
|
249
|
+
|
|
250
|
+
if (eventWebhooks.length === 0) {
|
|
251
|
+
return res.json({ message: 'No enabled webhook endpoints found for this event type', scheduled: 0 });
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
let scheduled = 0;
|
|
255
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
256
|
+
for (const webhook of eventWebhooks) {
|
|
257
|
+
// eslint-disable-next-line no-await-in-loop
|
|
258
|
+
const added = await addWebhookJob(event.id, webhook.id, { persist: false });
|
|
259
|
+
if (added) {
|
|
260
|
+
scheduled += 1;
|
|
261
|
+
logger.info('Manually scheduled webhook retry', { eventId: event.id, webhookId: webhook.id });
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return res.json({
|
|
266
|
+
message: `Successfully scheduled ${scheduled} webhooks for retry`,
|
|
267
|
+
scheduled,
|
|
268
|
+
total: eventWebhooks.length,
|
|
269
|
+
});
|
|
270
|
+
} catch (err: any) {
|
|
271
|
+
logger.error('Failed to retry webhooks for event', err);
|
|
272
|
+
return res.status(500).json({ error: `Failed to retry webhooks: ${err.message}` });
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
|
|
80
276
|
export default router;
|
package/blocklet.yml
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "payment-kit",
|
|
3
|
-
"version": "1.22.
|
|
3
|
+
"version": "1.22.30",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"dev": "blocklet dev --open",
|
|
6
6
|
"lint": "tsc --noEmit && eslint src api/src --ext .mjs,.js,.jsx,.ts,.tsx",
|
|
@@ -57,9 +57,9 @@
|
|
|
57
57
|
"@blocklet/error": "^0.3.4",
|
|
58
58
|
"@blocklet/js-sdk": "^1.17.4-beta-20251204-152224-243ff54f",
|
|
59
59
|
"@blocklet/logger": "^1.17.4-beta-20251204-152224-243ff54f",
|
|
60
|
-
"@blocklet/payment-broker-client": "1.22.
|
|
61
|
-
"@blocklet/payment-react": "1.22.
|
|
62
|
-
"@blocklet/payment-vendor": "1.22.
|
|
60
|
+
"@blocklet/payment-broker-client": "1.22.30",
|
|
61
|
+
"@blocklet/payment-react": "1.22.30",
|
|
62
|
+
"@blocklet/payment-vendor": "1.22.30",
|
|
63
63
|
"@blocklet/sdk": "^1.17.4-beta-20251204-152224-243ff54f",
|
|
64
64
|
"@blocklet/ui-react": "^3.2.11",
|
|
65
65
|
"@blocklet/uploader": "^0.3.13",
|
|
@@ -129,7 +129,7 @@
|
|
|
129
129
|
"devDependencies": {
|
|
130
130
|
"@abtnode/types": "^1.17.4-beta-20251204-152224-243ff54f",
|
|
131
131
|
"@arcblock/eslint-config-ts": "^0.3.3",
|
|
132
|
-
"@blocklet/payment-types": "1.22.
|
|
132
|
+
"@blocklet/payment-types": "1.22.30",
|
|
133
133
|
"@types/cookie-parser": "^1.4.9",
|
|
134
134
|
"@types/cors": "^2.8.19",
|
|
135
135
|
"@types/debug": "^4.1.12",
|
|
@@ -176,5 +176,5 @@
|
|
|
176
176
|
"parser": "typescript"
|
|
177
177
|
}
|
|
178
178
|
},
|
|
179
|
-
"gitHead": "
|
|
179
|
+
"gitHead": "78cac71748e6836069582ed293945b41b7c7af66"
|
|
180
180
|
}
|
|
@@ -4,11 +4,12 @@ import type { ReactNode } from 'react';
|
|
|
4
4
|
type Props = {
|
|
5
5
|
title: string | ReactNode;
|
|
6
6
|
children?: ReactNode;
|
|
7
|
+
action?: ReactNode;
|
|
7
8
|
mb?: number;
|
|
8
9
|
mt?: number;
|
|
9
10
|
};
|
|
10
11
|
|
|
11
|
-
export default function SectionHeader({ title, children = null, mb = 1.5, mt = 1.5 }: Props) {
|
|
12
|
+
export default function SectionHeader({ title, children = null, action = null, mb = 1.5, mt = 1.5 }: Props) {
|
|
12
13
|
return (
|
|
13
14
|
<Stack
|
|
14
15
|
className="section-header"
|
|
@@ -33,7 +34,7 @@ export default function SectionHeader({ title, children = null, mb = 1.5, mt = 1
|
|
|
33
34
|
component="div">
|
|
34
35
|
{title}
|
|
35
36
|
</Typography>
|
|
36
|
-
{children}
|
|
37
|
+
{action || children}
|
|
37
38
|
</Stack>
|
|
38
39
|
);
|
|
39
40
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/* eslint-disable react/no-unstable-nested-components */
|
|
2
2
|
import CodeBlock from '@arcblock/ux/lib/CodeBlock';
|
|
3
|
+
import Toast from '@arcblock/ux/lib/Toast';
|
|
3
4
|
import { api, formatTime } from '@blocklet/payment-react';
|
|
4
5
|
import type { Paginated, TEvent, TWebhookAttemptExpanded } from '@blocklet/payment-types';
|
|
5
|
-
import { CheckCircleOutlined, ErrorOutlined } from '@mui/icons-material';
|
|
6
|
+
import { CheckCircleOutlined, ErrorOutlined, RefreshOutlined } from '@mui/icons-material';
|
|
6
7
|
import {
|
|
7
8
|
Box,
|
|
8
9
|
Button,
|
|
@@ -17,7 +18,7 @@ import {
|
|
|
17
18
|
Stack,
|
|
18
19
|
Typography,
|
|
19
20
|
} from '@mui/material';
|
|
20
|
-
import { useInfiniteScroll } from 'ahooks';
|
|
21
|
+
import { useInfiniteScroll, useRequest } from 'ahooks';
|
|
21
22
|
import React, { useEffect, useState } from 'react';
|
|
22
23
|
|
|
23
24
|
import { isEmpty } from 'lodash';
|
|
@@ -51,7 +52,7 @@ type Props = {
|
|
|
51
52
|
};
|
|
52
53
|
|
|
53
54
|
export default function WebhookAttempts({ event_id = '', webhook_endpoint_id = '', event = undefined }: Props) {
|
|
54
|
-
const { data, loadMore, loadingMore, loading } = useInfiniteScroll<Paginated<TWebhookAttemptExpanded>>(
|
|
55
|
+
const { data, loadMore, loadingMore, loading, reload } = useInfiniteScroll<Paginated<TWebhookAttemptExpanded>>(
|
|
55
56
|
(d) => {
|
|
56
57
|
const size = 15;
|
|
57
58
|
const page = d ? Math.ceil(d.list.length / size) + 1 : 1;
|
|
@@ -70,6 +71,20 @@ export default function WebhookAttempts({ event_id = '', webhook_endpoint_id = '
|
|
|
70
71
|
>(null);
|
|
71
72
|
const groupedAttempts = groupAttemptsByDate(attempts);
|
|
72
73
|
|
|
74
|
+
const { loading: retrying, run: retryWebhook } = useRequest(
|
|
75
|
+
(eventId: string) => api.post(`/api/events/${eventId}/retry-webhooks`).then((res) => res.data),
|
|
76
|
+
{
|
|
77
|
+
manual: true,
|
|
78
|
+
onSuccess: (result) => {
|
|
79
|
+
Toast.success(result.message || 'Webhook scheduled for retry');
|
|
80
|
+
reload();
|
|
81
|
+
},
|
|
82
|
+
onError: (err: any) => {
|
|
83
|
+
Toast.error(err.response?.data?.error || 'Failed to retry webhook');
|
|
84
|
+
},
|
|
85
|
+
}
|
|
86
|
+
);
|
|
87
|
+
|
|
73
88
|
useEffect(() => {
|
|
74
89
|
if (!selected && data?.list.length) {
|
|
75
90
|
setSelected(data.list[0] as TWebhookAttemptExpanded);
|
|
@@ -161,7 +176,17 @@ export default function WebhookAttempts({ event_id = '', webhook_endpoint_id = '
|
|
|
161
176
|
}}>
|
|
162
177
|
{selected && (
|
|
163
178
|
<Stack direction="column" spacing={2} sx={{ pt: 3, pl: 3, borderLeft: '1px solid', borderColor: 'divider' }}>
|
|
164
|
-
<
|
|
179
|
+
<Stack direction="row" sx={{ justifyContent: 'space-between', alignItems: 'center' }}>
|
|
180
|
+
<Typography variant="h6">{event_id ? selected.endpoint.url : selected.event.type}</Typography>
|
|
181
|
+
<Button
|
|
182
|
+
variant="outlined"
|
|
183
|
+
size="small"
|
|
184
|
+
startIcon={<RefreshOutlined />}
|
|
185
|
+
onClick={() => retryWebhook(selected.event_id)}
|
|
186
|
+
disabled={retrying}>
|
|
187
|
+
{retrying ? 'Retrying...' : 'Retry'}
|
|
188
|
+
</Button>
|
|
189
|
+
</Stack>
|
|
165
190
|
<Box>
|
|
166
191
|
<Typography variant="h6">Response ({selected.response_status})</Typography>
|
|
167
192
|
{/* @ts-ignore */}
|