@orthacms/webhooks-admin 0.5.0
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/LICENSE +21 -0
- package/README.md +7 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/lib/application/useContentTypeOptions/index.d.ts +18 -0
- package/dist/lib/application/useContentTypeOptions/index.d.ts.map +1 -0
- package/dist/lib/application/useContentTypeOptions/index.js +40 -0
- package/dist/lib/application/useWebhookDeliveries/index.d.ts +19 -0
- package/dist/lib/application/useWebhookDeliveries/index.d.ts.map +1 -0
- package/dist/lib/application/useWebhookDeliveries/index.js +48 -0
- package/dist/lib/application/useWebhookEndpoints/index.d.ts +12 -0
- package/dist/lib/application/useWebhookEndpoints/index.d.ts.map +1 -0
- package/dist/lib/application/useWebhookEndpoints/index.js +26 -0
- package/dist/lib/application/useWebhookEvents/index.d.ts +9 -0
- package/dist/lib/application/useWebhookEvents/index.d.ts.map +1 -0
- package/dist/lib/application/useWebhookEvents/index.js +20 -0
- package/dist/lib/application/useWebhookMutations/index.d.ts +25 -0
- package/dist/lib/application/useWebhookMutations/index.d.ts.map +1 -0
- package/dist/lib/application/useWebhookMutations/index.js +79 -0
- package/dist/lib/application/useWorkspaceOptions/index.d.ts +9 -0
- package/dist/lib/application/useWorkspaceOptions/index.d.ts.map +1 -0
- package/dist/lib/application/useWorkspaceOptions/index.js +19 -0
- package/dist/lib/domain/contentTypeChoices/index.d.ts +65 -0
- package/dist/lib/domain/contentTypeChoices/index.d.ts.map +1 -0
- package/dist/lib/domain/contentTypeChoices/index.js +70 -0
- package/dist/lib/domain/headerRules/index.d.ts +37 -0
- package/dist/lib/domain/headerRules/index.d.ts.map +1 -0
- package/dist/lib/domain/headerRules/index.js +66 -0
- package/dist/lib/domain/types/contentTypeOption/index.d.ts +14 -0
- package/dist/lib/domain/types/contentTypeOption/index.d.ts.map +1 -0
- package/dist/lib/domain/types/contentTypeOption/index.js +1 -0
- package/dist/lib/domain/types/webhook/index.d.ts +102 -0
- package/dist/lib/domain/types/webhook/index.d.ts.map +1 -0
- package/dist/lib/domain/types/webhook/index.js +18 -0
- package/dist/lib/domain/types/workspaceOption/index.d.ts +16 -0
- package/dist/lib/domain/types/workspaceOption/index.d.ts.map +1 -0
- package/dist/lib/domain/types/workspaceOption/index.js +1 -0
- package/dist/lib/infrastructure/httpWebhookGateway/index.d.ts +11 -0
- package/dist/lib/infrastructure/httpWebhookGateway/index.d.ts.map +1 -0
- package/dist/lib/infrastructure/httpWebhookGateway/index.js +138 -0
- package/dist/lib/infrastructure/serverMessageOf/index.d.ts +16 -0
- package/dist/lib/infrastructure/serverMessageOf/index.d.ts.map +1 -0
- package/dist/lib/infrastructure/serverMessageOf/index.js +24 -0
- package/dist/lib/infrastructure/webhookGateway/index.d.ts +62 -0
- package/dist/lib/infrastructure/webhookGateway/index.d.ts.map +1 -0
- package/dist/lib/infrastructure/webhookGateway/index.js +1 -0
- package/dist/lib/infrastructure/webhookMapper/index.d.ts +104 -0
- package/dist/lib/infrastructure/webhookMapper/index.d.ts.map +1 -0
- package/dist/lib/infrastructure/webhookMapper/index.js +49 -0
- package/dist/lib/infrastructure/webhooksKeys/index.d.ts +35 -0
- package/dist/lib/infrastructure/webhooksKeys/index.d.ts.map +1 -0
- package/dist/lib/infrastructure/webhooksKeys/index.js +23 -0
- package/dist/lib/presentation/components/DeliveryStatusBadge/index.d.ts +6 -0
- package/dist/lib/presentation/components/DeliveryStatusBadge/index.d.ts.map +1 -0
- package/dist/lib/presentation/components/DeliveryStatusBadge/index.js +30 -0
- package/dist/lib/presentation/components/RevealWebhookSecretDialog/index.d.ts +25 -0
- package/dist/lib/presentation/components/RevealWebhookSecretDialog/index.d.ts.map +1 -0
- package/dist/lib/presentation/components/RevealWebhookSecretDialog/index.js +107 -0
- package/dist/lib/presentation/components/WebhookDeliveriesPanel/DeliveryDetailSheet/index.d.ts +19 -0
- package/dist/lib/presentation/components/WebhookDeliveriesPanel/DeliveryDetailSheet/index.d.ts.map +1 -0
- package/dist/lib/presentation/components/WebhookDeliveriesPanel/DeliveryDetailSheet/index.js +72 -0
- package/dist/lib/presentation/components/WebhookDeliveriesPanel/index.d.ts +12 -0
- package/dist/lib/presentation/components/WebhookDeliveriesPanel/index.d.ts.map +1 -0
- package/dist/lib/presentation/components/WebhookDeliveriesPanel/index.js +132 -0
- package/dist/lib/presentation/components/WebhookForm/index.d.ts +46 -0
- package/dist/lib/presentation/components/WebhookForm/index.d.ts.map +1 -0
- package/dist/lib/presentation/components/WebhookForm/index.js +412 -0
- package/dist/lib/presentation/components/WebhooksEmpty/index.d.ts +5 -0
- package/dist/lib/presentation/components/WebhooksEmpty/index.d.ts.map +1 -0
- package/dist/lib/presentation/components/WebhooksEmpty/index.js +23 -0
- package/dist/lib/presentation/components/WebhooksNoAccess/index.d.ts +3 -0
- package/dist/lib/presentation/components/WebhooksNoAccess/index.d.ts.map +1 -0
- package/dist/lib/presentation/components/WebhooksNoAccess/index.js +18 -0
- package/dist/lib/presentation/components/WebhooksSkeleton/index.d.ts +20 -0
- package/dist/lib/presentation/components/WebhooksSkeleton/index.d.ts.map +1 -0
- package/dist/lib/presentation/components/WebhooksSkeleton/index.js +45 -0
- package/dist/lib/presentation/components/WebhooksTable/index.d.ts +12 -0
- package/dist/lib/presentation/components/WebhooksTable/index.d.ts.map +1 -0
- package/dist/lib/presentation/components/WebhooksTable/index.js +79 -0
- package/dist/lib/presentation/pages/WebhookDetailPage/index.d.ts +10 -0
- package/dist/lib/presentation/pages/WebhookDetailPage/index.d.ts.map +1 -0
- package/dist/lib/presentation/pages/WebhookDetailPage/index.js +191 -0
- package/dist/lib/presentation/pages/WebhookEditorPage/index.d.ts +28 -0
- package/dist/lib/presentation/pages/WebhookEditorPage/index.d.ts.map +1 -0
- package/dist/lib/presentation/pages/WebhookEditorPage/index.js +149 -0
- package/dist/lib/presentation/pages/WebhooksPage/index.d.ts +11 -0
- package/dist/lib/presentation/pages/WebhooksPage/index.d.ts.map +1 -0
- package/dist/lib/presentation/pages/WebhooksPage/index.js +65 -0
- package/dist/lib/utils/webhooksPlugin/index.d.ts +35 -0
- package/dist/lib/utils/webhooksPlugin/index.d.ts.map +1 -0
- package/dist/lib/utils/webhooksPlugin/index.js +88 -0
- package/package.json +46 -0
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
3
|
+
import { defineMessages, useIntl } from 'react-intl';
|
|
4
|
+
import { Alert, AlertDescription, Button, Checkbox, ConfirmDialog, Input, InputField, Label, MultiSelect, Switch } from '@orthacms/design-system';
|
|
5
|
+
import { Plus, X } from 'lucide-react';
|
|
6
|
+
import { fromHeaderRows, rejectionFor, toHeaderRows } from '../../../domain/headerRules';
|
|
7
|
+
import { addContentTypeNames, contentTypeChoices, grantedContentTypes, parseContentTypeNames } from '../../../domain/contentTypeChoices';
|
|
8
|
+
const messages = defineMessages({
|
|
9
|
+
target: {
|
|
10
|
+
id: 'webhooks.form.target',
|
|
11
|
+
defaultMessage: 'Where deliveries go'
|
|
12
|
+
},
|
|
13
|
+
subscription: {
|
|
14
|
+
id: 'webhooks.form.subscription',
|
|
15
|
+
defaultMessage: 'What this endpoint receives'
|
|
16
|
+
},
|
|
17
|
+
name: { id: 'webhooks.form.name', defaultMessage: 'Name' },
|
|
18
|
+
namePlaceholder: {
|
|
19
|
+
id: 'webhooks.form.namePlaceholder',
|
|
20
|
+
defaultMessage: 'Rebuild the storefront'
|
|
21
|
+
},
|
|
22
|
+
url: { id: 'webhooks.form.url', defaultMessage: 'URL' },
|
|
23
|
+
urlHint: {
|
|
24
|
+
id: 'webhooks.form.urlHint',
|
|
25
|
+
defaultMessage: 'Must be https:// and reachable on the public internet.'
|
|
26
|
+
},
|
|
27
|
+
workspaces: {
|
|
28
|
+
id: 'webhooks.form.workspaces',
|
|
29
|
+
defaultMessage: 'Workspaces'
|
|
30
|
+
},
|
|
31
|
+
allWorkspaces: {
|
|
32
|
+
id: 'webhooks.form.allWorkspaces',
|
|
33
|
+
defaultMessage: 'All workspaces, including ones created later'
|
|
34
|
+
},
|
|
35
|
+
workspacesHint: {
|
|
36
|
+
id: 'webhooks.form.workspacesHint',
|
|
37
|
+
defaultMessage: 'A record that belongs to no workspace is only sent to endpoints that take them all.'
|
|
38
|
+
},
|
|
39
|
+
workspacesPlaceholder: {
|
|
40
|
+
id: 'webhooks.form.workspacesPlaceholder',
|
|
41
|
+
defaultMessage: 'Choose workspaces'
|
|
42
|
+
},
|
|
43
|
+
events: { id: 'webhooks.form.events', defaultMessage: 'Events' },
|
|
44
|
+
allEvents: {
|
|
45
|
+
id: 'webhooks.form.allEvents',
|
|
46
|
+
defaultMessage: 'Every event, including ones added later'
|
|
47
|
+
},
|
|
48
|
+
eventsPlaceholder: {
|
|
49
|
+
id: 'webhooks.form.eventsPlaceholder',
|
|
50
|
+
defaultMessage: 'Choose events'
|
|
51
|
+
},
|
|
52
|
+
contentTypes: {
|
|
53
|
+
id: 'webhooks.form.contentTypes',
|
|
54
|
+
defaultMessage: 'Content types'
|
|
55
|
+
},
|
|
56
|
+
allContentTypes: {
|
|
57
|
+
id: 'webhooks.form.allContentTypes',
|
|
58
|
+
defaultMessage: 'Every content type, including ones added later'
|
|
59
|
+
},
|
|
60
|
+
contentTypesPlaceholder: {
|
|
61
|
+
id: 'webhooks.form.contentTypesPlaceholder',
|
|
62
|
+
defaultMessage: 'Choose content types'
|
|
63
|
+
},
|
|
64
|
+
contentTypesHint: {
|
|
65
|
+
id: 'webhooks.form.contentTypesHint',
|
|
66
|
+
defaultMessage: 'Pick from the types this build defines, or add one by machine name — a type you are about to create can be subscribed to before it exists.'
|
|
67
|
+
},
|
|
68
|
+
noContentTypes: {
|
|
69
|
+
id: 'webhooks.form.noContentTypes',
|
|
70
|
+
defaultMessage: 'Nothing chosen yet, so this endpoint still receives every type.'
|
|
71
|
+
},
|
|
72
|
+
contentTypeSearch: {
|
|
73
|
+
id: 'webhooks.form.contentTypeSearch',
|
|
74
|
+
defaultMessage: 'Search, or type a machine name'
|
|
75
|
+
},
|
|
76
|
+
addContentType: {
|
|
77
|
+
id: 'webhooks.form.addContentType',
|
|
78
|
+
defaultMessage: 'Add “{name}”'
|
|
79
|
+
},
|
|
80
|
+
unknownContentType: {
|
|
81
|
+
id: 'webhooks.form.unknownContentType',
|
|
82
|
+
defaultMessage: '{name} (not defined here)'
|
|
83
|
+
},
|
|
84
|
+
contentTypesNeedWorkspace: {
|
|
85
|
+
id: 'webhooks.form.contentTypesNeedWorkspace',
|
|
86
|
+
defaultMessage: 'Choose a workspace first — which types exist depends on what those workspaces were granted.'
|
|
87
|
+
},
|
|
88
|
+
contentTypesNoneGranted: {
|
|
89
|
+
id: 'webhooks.form.contentTypesNoneGranted',
|
|
90
|
+
defaultMessage: 'The chosen workspaces were granted no content types, so there is nothing to pick from — a machine name can still be typed into the picker.'
|
|
91
|
+
},
|
|
92
|
+
contentTypesScoped: {
|
|
93
|
+
id: 'webhooks.form.contentTypesScoped',
|
|
94
|
+
defaultMessage: 'Listing only what the chosen workspaces were granted.'
|
|
95
|
+
},
|
|
96
|
+
status: { id: 'webhooks.form.status', defaultMessage: 'Status' },
|
|
97
|
+
enabled: { id: 'webhooks.form.enabled', defaultMessage: 'Enabled' },
|
|
98
|
+
disabled: { id: 'webhooks.form.disabled', defaultMessage: 'Disabled' },
|
|
99
|
+
enabledHint: {
|
|
100
|
+
id: 'webhooks.form.enabledHint',
|
|
101
|
+
defaultMessage: 'Deliveries are sent as content changes.'
|
|
102
|
+
},
|
|
103
|
+
disabledHint: {
|
|
104
|
+
id: 'webhooks.form.disabledHint',
|
|
105
|
+
defaultMessage: 'Nothing is sent while this is off.'
|
|
106
|
+
},
|
|
107
|
+
disableTitle: {
|
|
108
|
+
id: 'webhooks.form.disableTitle',
|
|
109
|
+
defaultMessage: 'Turn this endpoint off?'
|
|
110
|
+
},
|
|
111
|
+
disableBody: {
|
|
112
|
+
id: 'webhooks.form.disableBody',
|
|
113
|
+
defaultMessage: 'Events that happen while it is off are not queued for later — there is nothing to catch up on when you turn it back on. Deliveries already waiting are closed as failed.'
|
|
114
|
+
},
|
|
115
|
+
disableConfirm: {
|
|
116
|
+
id: 'webhooks.form.disableConfirm',
|
|
117
|
+
defaultMessage: 'Turn it off'
|
|
118
|
+
},
|
|
119
|
+
cancelDisable: {
|
|
120
|
+
id: 'webhooks.form.cancelDisable',
|
|
121
|
+
defaultMessage: 'Keep it on'
|
|
122
|
+
},
|
|
123
|
+
headers: {
|
|
124
|
+
id: 'webhooks.form.headers',
|
|
125
|
+
defaultMessage: 'Custom headers'
|
|
126
|
+
},
|
|
127
|
+
headersHint: {
|
|
128
|
+
id: 'webhooks.form.headersHint',
|
|
129
|
+
defaultMessage: 'Sent with every delivery — an Authorization header, say, if the receiver wants one of its own on top of the signature.'
|
|
130
|
+
},
|
|
131
|
+
headerName: {
|
|
132
|
+
id: 'webhooks.form.headerName',
|
|
133
|
+
defaultMessage: 'Header name'
|
|
134
|
+
},
|
|
135
|
+
headerValue: {
|
|
136
|
+
id: 'webhooks.form.headerValue',
|
|
137
|
+
defaultMessage: 'Header value'
|
|
138
|
+
},
|
|
139
|
+
addHeader: { id: 'webhooks.form.addHeader', defaultMessage: 'Add header' },
|
|
140
|
+
removeHeader: {
|
|
141
|
+
id: 'webhooks.form.removeHeader',
|
|
142
|
+
defaultMessage: 'Remove {name}'
|
|
143
|
+
},
|
|
144
|
+
headerReserved: {
|
|
145
|
+
id: 'webhooks.form.headerReserved',
|
|
146
|
+
defaultMessage: '“{name}” is reserved for the delivery’s own metadata and can’t be set here.'
|
|
147
|
+
},
|
|
148
|
+
headerMalformed: {
|
|
149
|
+
id: 'webhooks.form.headerMalformed',
|
|
150
|
+
defaultMessage: '“{name}” is not a valid header name.'
|
|
151
|
+
},
|
|
152
|
+
save: { id: 'webhooks.form.save', defaultMessage: 'Save' },
|
|
153
|
+
create: { id: 'webhooks.form.create', defaultMessage: 'Create webhook' },
|
|
154
|
+
cancel: { id: 'webhooks.form.cancel', defaultMessage: 'Cancel' },
|
|
155
|
+
nameRequired: {
|
|
156
|
+
id: 'webhooks.form.nameRequired',
|
|
157
|
+
defaultMessage: 'Give the webhook a name.'
|
|
158
|
+
},
|
|
159
|
+
urlRequired: {
|
|
160
|
+
id: 'webhooks.form.urlRequired',
|
|
161
|
+
defaultMessage: 'Enter the URL deliveries should be posted to.'
|
|
162
|
+
},
|
|
163
|
+
noWorkspaces: {
|
|
164
|
+
id: 'webhooks.form.noWorkspaces',
|
|
165
|
+
defaultMessage: 'Choose at least one workspace, or switch on “All workspaces”. As it stands this endpoint would receive nothing.'
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
/** The values a fresh dialog opens with. */
|
|
169
|
+
function emptyValues() {
|
|
170
|
+
return {
|
|
171
|
+
name: '',
|
|
172
|
+
url: '',
|
|
173
|
+
enabled: true,
|
|
174
|
+
// Defaults to false: reaching across every workspace should be
|
|
175
|
+
// something someone chose, not something they forgot to narrow.
|
|
176
|
+
allWorkspaces: false,
|
|
177
|
+
workspaceIds: [],
|
|
178
|
+
// Defaults to true, for the opposite reason: an endpoint that takes no
|
|
179
|
+
// events is not a safer endpoint, it is a broken one.
|
|
180
|
+
allEvents: true,
|
|
181
|
+
eventKinds: [],
|
|
182
|
+
// Same reasoning as events: a subscription narrowed to nothing is not
|
|
183
|
+
// a safer one, so the default is the whole registry.
|
|
184
|
+
allContentTypes: true,
|
|
185
|
+
contentTypes: [],
|
|
186
|
+
headers: []
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
/** An existing endpoint, as the form edits it. */
|
|
190
|
+
function valuesOf(endpoint) {
|
|
191
|
+
return {
|
|
192
|
+
name: endpoint.name,
|
|
193
|
+
url: endpoint.url,
|
|
194
|
+
enabled: endpoint.enabled,
|
|
195
|
+
allWorkspaces: endpoint.allWorkspaces,
|
|
196
|
+
workspaceIds: endpoint.workspaceIds,
|
|
197
|
+
allEvents: endpoint.eventKinds.length === 0,
|
|
198
|
+
eventKinds: endpoint.eventKinds,
|
|
199
|
+
allContentTypes: endpoint.contentTypes.length === 0,
|
|
200
|
+
contentTypes: endpoint.contentTypes,
|
|
201
|
+
headers: toHeaderRows(endpoint.headers)
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Create and edit an endpoint.
|
|
206
|
+
*
|
|
207
|
+
* The three filters are each an "All…" toggle over a picker, and the toggle is
|
|
208
|
+
* not cosmetic: it is the difference between "everything, including what does
|
|
209
|
+
* not exist yet" and an enumerated list that silently stops covering the next
|
|
210
|
+
* workspace or event kind. Turning one on sends **no** value for that filter,
|
|
211
|
+
* which is exactly how the server reads "all".
|
|
212
|
+
*/
|
|
213
|
+
export function WebhookForm({ endpoint, events, workspaces, contentTypes, pending, error, onSubmit, onCancel }) {
|
|
214
|
+
const intl = useIntl();
|
|
215
|
+
const [values, setValues] = useState(emptyValues);
|
|
216
|
+
const [touched, setTouched] = useState(false);
|
|
217
|
+
// Switching an endpoint off is not undoable in the way a form field is:
|
|
218
|
+
// what happens while it is off is simply not recorded, so the switch asks.
|
|
219
|
+
const [confirmDisable, setConfirmDisable] = useState(false);
|
|
220
|
+
// Rows need a key that survives editing the name — using the name itself
|
|
221
|
+
// would remount the input on every keystroke and lose the caret.
|
|
222
|
+
const nextRowKey = useRef(0);
|
|
223
|
+
// Seed once the endpoint arrives — the page mounts before its query
|
|
224
|
+
// resolves. Keyed on the id rather than the object, so a background refetch
|
|
225
|
+
// cannot discard edits in progress by re-seeding from the server's copy.
|
|
226
|
+
// Depending on the id rather than the object is deliberate: a background
|
|
227
|
+
// refetch hands back a new object with the same contents, and re-seeding on
|
|
228
|
+
// that would discard whatever is half-typed. `useRef` holds the endpoint so
|
|
229
|
+
// the effect can read it without listing it.
|
|
230
|
+
const latest = useRef(endpoint);
|
|
231
|
+
latest.current = endpoint;
|
|
232
|
+
const endpointId = endpoint?.id;
|
|
233
|
+
useEffect(() => {
|
|
234
|
+
const seed = latest.current;
|
|
235
|
+
if (!seed)
|
|
236
|
+
return;
|
|
237
|
+
setValues(valuesOf(seed));
|
|
238
|
+
setTouched(false);
|
|
239
|
+
}, [endpointId]);
|
|
240
|
+
const eventOptions = useMemo(() => events
|
|
241
|
+
// `ping` is what the Send-test button produces; it is not
|
|
242
|
+
// something an endpoint subscribes to.
|
|
243
|
+
.filter((event) => event.kind !== 'ping')
|
|
244
|
+
.map((event) => ({ value: event.kind, label: event.label })), [events]);
|
|
245
|
+
const workspaceOptions = useMemo(() => workspaces.map((workspace) => ({
|
|
246
|
+
value: workspace.id,
|
|
247
|
+
label: workspace.name
|
|
248
|
+
})), [workspaces]);
|
|
249
|
+
// The registry's types plus any selected name it does not know. The unknown
|
|
250
|
+
// ones are labelled as such rather than hidden: an endpoint subscribed to a
|
|
251
|
+
// type that has been removed, or to one with a typo in its name, is
|
|
252
|
+
// receiving nothing and this is the only place that shows.
|
|
253
|
+
// Narrowed to what the chosen workspaces were actually granted: a delivery
|
|
254
|
+
// needs the event's workspace *and* its type to match, so a type none of
|
|
255
|
+
// them can hold would never fire here.
|
|
256
|
+
const offeredContentTypes = useMemo(() => grantedContentTypes(contentTypes, workspaces, {
|
|
257
|
+
allWorkspaces: values.allWorkspaces,
|
|
258
|
+
workspaceIds: values.workspaceIds
|
|
259
|
+
}), [contentTypes, workspaces, values.allWorkspaces, values.workspaceIds]);
|
|
260
|
+
const contentTypeOptions = useMemo(() => contentTypeChoices(offeredContentTypes, values.contentTypes).map((choice) => ({
|
|
261
|
+
value: choice.value,
|
|
262
|
+
label: choice.known
|
|
263
|
+
? choice.label
|
|
264
|
+
: intl.formatMessage(messages.unknownContentType, {
|
|
265
|
+
name: choice.value
|
|
266
|
+
})
|
|
267
|
+
})), [offeredContentTypes, values.contentTypes, intl]);
|
|
268
|
+
// Which types are on offer is a question about the workspaces, so it cannot
|
|
269
|
+
// be asked before they are chosen. An endpoint that already carries a type
|
|
270
|
+
// filter is the exception — hiding it would leave a saved subscription
|
|
271
|
+
// invisible and uneditable.
|
|
272
|
+
const workspaceChosen = values.allWorkspaces || values.workspaceIds.length > 0;
|
|
273
|
+
const contentTypesAnswerable = workspaceChosen || values.contentTypes.length > 0;
|
|
274
|
+
// Chosen workspaces that were granted nothing: the picker is empty for a
|
|
275
|
+
// reason worth stating, rather than looking broken.
|
|
276
|
+
const noGrants = workspaceChosen &&
|
|
277
|
+
!values.allWorkspaces &&
|
|
278
|
+
offeredContentTypes.length === 0;
|
|
279
|
+
const addTypedContentTypes = (raw) => {
|
|
280
|
+
const names = parseContentTypeNames(raw);
|
|
281
|
+
if (names.length === 0)
|
|
282
|
+
return;
|
|
283
|
+
setValues((prev) => ({
|
|
284
|
+
...prev,
|
|
285
|
+
contentTypes: addContentTypeNames(prev.contentTypes, names)
|
|
286
|
+
}));
|
|
287
|
+
};
|
|
288
|
+
const setHeaders = (next) => setValues((prev) => ({ ...prev, headers: next }));
|
|
289
|
+
// Named rows only: an untouched empty row is someone about to type.
|
|
290
|
+
const headerErrors = values.headers.map((row) => {
|
|
291
|
+
if (row.name.trim().length === 0)
|
|
292
|
+
return null;
|
|
293
|
+
const rejection = rejectionFor(row.name);
|
|
294
|
+
if (!rejection)
|
|
295
|
+
return null;
|
|
296
|
+
return intl.formatMessage(rejection === 'reserved'
|
|
297
|
+
? messages.headerReserved
|
|
298
|
+
: messages.headerMalformed, { name: row.name.trim() });
|
|
299
|
+
});
|
|
300
|
+
const headersValid = headerErrors.every((error) => error === null);
|
|
301
|
+
const nameError = touched && values.name.trim().length === 0
|
|
302
|
+
? intl.formatMessage(messages.nameRequired)
|
|
303
|
+
: undefined;
|
|
304
|
+
const urlError = touched && values.url.trim().length === 0
|
|
305
|
+
? intl.formatMessage(messages.urlRequired)
|
|
306
|
+
: undefined;
|
|
307
|
+
// Not a hard error — the server accepts it — but an endpoint scoped to no
|
|
308
|
+
// workspace receives nothing, which is never what someone meant.
|
|
309
|
+
const emptyWorkspaces = !values.allWorkspaces && values.workspaceIds.length === 0;
|
|
310
|
+
const submit = () => {
|
|
311
|
+
setTouched(true);
|
|
312
|
+
if (values.name.trim().length === 0 ||
|
|
313
|
+
values.url.trim().length === 0 ||
|
|
314
|
+
// The server refuses these too; stopping here keeps the message
|
|
315
|
+
// beside the row that caused it.
|
|
316
|
+
!headersValid) {
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
onSubmit({
|
|
320
|
+
name: values.name.trim(),
|
|
321
|
+
url: values.url.trim(),
|
|
322
|
+
enabled: values.enabled,
|
|
323
|
+
allWorkspaces: values.allWorkspaces,
|
|
324
|
+
workspaceIds: values.allWorkspaces ? [] : values.workspaceIds,
|
|
325
|
+
// An empty list is how "all" is spelled on the wire, for both.
|
|
326
|
+
eventKinds: values.allEvents ? [] : values.eventKinds,
|
|
327
|
+
contentTypes: values.allContentTypes ? [] : values.contentTypes,
|
|
328
|
+
headers: fromHeaderRows(values.headers)
|
|
329
|
+
});
|
|
330
|
+
};
|
|
331
|
+
return (
|
|
332
|
+
// Two columns on a wide screen, one on a narrow one. The split is the
|
|
333
|
+
// two questions an endpoint answers — where a delivery goes, and what
|
|
334
|
+
// is worth sending — which is also the order they are decided in.
|
|
335
|
+
_jsxs("div", { className: "grid max-w-5xl gap-x-12 gap-y-8 lg:grid-cols-2", children: [_jsxs("section", { className: "flex flex-col gap-4", children: [_jsx("h2", { className: "text-sm font-medium text-muted-foreground", children: intl.formatMessage(messages.target) }), _jsx(InputField, { id: "webhook-name", label: intl.formatMessage(messages.name), value: values.name, placeholder: intl.formatMessage(messages.namePlaceholder), error: nameError, onChange: (event) => setValues((prev) => ({
|
|
336
|
+
...prev,
|
|
337
|
+
name: event.target.value
|
|
338
|
+
})) }), _jsx(InputField, { id: "webhook-url", label: intl.formatMessage(messages.url), value: values.url, type: "url", placeholder: "https://example.com/hooks/ortha", description: intl.formatMessage(messages.urlHint), error: urlError, onChange: (event) => setValues((prev) => ({
|
|
339
|
+
...prev,
|
|
340
|
+
url: event.target.value
|
|
341
|
+
})) }), _jsxs("fieldset", { className: "flex flex-col gap-2", children: [_jsx("legend", { className: "text-sm font-medium", children: intl.formatMessage(messages.status) }), _jsxs("div", { className: "flex items-center gap-3", children: [_jsx(Switch, { id: "webhook-enabled", checked: values.enabled, onCheckedChange: (checked) => {
|
|
342
|
+
// Turning it on is harmless and immediate;
|
|
343
|
+
// turning it off loses whatever happens next,
|
|
344
|
+
// so only that direction asks.
|
|
345
|
+
if (checked) {
|
|
346
|
+
setValues((prev) => ({
|
|
347
|
+
...prev,
|
|
348
|
+
enabled: true
|
|
349
|
+
}));
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
setConfirmDisable(true);
|
|
353
|
+
} }), _jsx(Label, { htmlFor: "webhook-enabled", className: "text-sm font-normal", children: intl.formatMessage(values.enabled
|
|
354
|
+
? messages.enabled
|
|
355
|
+
: messages.disabled) })] }), _jsx("p", { className: "text-xs text-muted-foreground", children: intl.formatMessage(values.enabled
|
|
356
|
+
? messages.enabledHint
|
|
357
|
+
: messages.disabledHint) })] }), _jsxs("fieldset", { className: "flex flex-col gap-2", children: [_jsx("legend", { className: "text-sm font-medium", children: intl.formatMessage(messages.headers) }), values.headers.map((row, index) => (_jsxs("div", { className: "flex flex-col gap-1", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Label, { htmlFor: `webhook-header-name-${row.key}`, className: "sr-only", children: intl.formatMessage(messages.headerName) }), _jsx(Input, { id: `webhook-header-name-${row.key}`, value: row.name, placeholder: "Authorization", className: "font-mono text-sm", "aria-invalid": headerErrors[index] !== null
|
|
358
|
+
? true
|
|
359
|
+
: undefined, onChange: (event) => setHeaders(values.headers.map((candidate) => candidate.key === row.key
|
|
360
|
+
? {
|
|
361
|
+
...candidate,
|
|
362
|
+
name: event.target
|
|
363
|
+
.value
|
|
364
|
+
}
|
|
365
|
+
: candidate)) }), _jsx(Label, { htmlFor: `webhook-header-value-${row.key}`, className: "sr-only", children: intl.formatMessage(messages.headerValue) }), _jsx(Input, { id: `webhook-header-value-${row.key}`, value: row.value, placeholder: "Bearer \u2026", className: "font-mono text-sm", onChange: (event) => setHeaders(values.headers.map((candidate) => candidate.key === row.key
|
|
366
|
+
? {
|
|
367
|
+
...candidate,
|
|
368
|
+
value: event.target
|
|
369
|
+
.value
|
|
370
|
+
}
|
|
371
|
+
: candidate)) }), _jsx(Button, { type: "button", variant: "ghost", size: "icon", "aria-label": intl.formatMessage(messages.removeHeader, {
|
|
372
|
+
name: row.name.trim() ||
|
|
373
|
+
intl.formatMessage(messages.headerName)
|
|
374
|
+
}), onClick: () => setHeaders(values.headers.filter((candidate) => candidate.key !== row.key)), children: _jsx(X, { "aria-hidden": true }) })] }), headerErrors[index] ? (_jsx("p", { role: "alert", className: "text-xs text-destructive", children: headerErrors[index] })) : null] }, row.key))), _jsx("div", { children: _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: () => setHeaders([
|
|
375
|
+
...values.headers,
|
|
376
|
+
{
|
|
377
|
+
key: `row-${nextRowKey.current++}`,
|
|
378
|
+
name: '',
|
|
379
|
+
value: ''
|
|
380
|
+
}
|
|
381
|
+
]), children: [_jsx(Plus, {}), intl.formatMessage(messages.addHeader)] }) }), _jsx("p", { className: "text-xs text-muted-foreground", children: intl.formatMessage(messages.headersHint) })] })] }), _jsxs("section", { className: "flex flex-col gap-4", children: [_jsx("h2", { className: "text-sm font-medium text-muted-foreground", children: intl.formatMessage(messages.subscription) }), _jsxs("fieldset", { className: "flex flex-col gap-2", children: [_jsx("legend", { className: "text-sm font-medium", children: intl.formatMessage(messages.workspaces) }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Checkbox, { id: "webhook-all-workspaces", checked: values.allWorkspaces, onCheckedChange: (checked) => setValues((prev) => ({
|
|
382
|
+
...prev,
|
|
383
|
+
allWorkspaces: checked === true
|
|
384
|
+
})) }), _jsx(Label, { htmlFor: "webhook-all-workspaces", className: "text-sm font-normal", children: intl.formatMessage(messages.allWorkspaces) })] }), !values.allWorkspaces ? (_jsxs(_Fragment, { children: [_jsx(Label, { htmlFor: "webhook-workspaces", className: "sr-only", children: intl.formatMessage(messages.workspaces) }), _jsx(MultiSelect, { id: "webhook-workspaces", options: workspaceOptions, value: values.workspaceIds, onChange: (next) => setValues((prev) => ({
|
|
385
|
+
...prev,
|
|
386
|
+
workspaceIds: next
|
|
387
|
+
})), placeholder: intl.formatMessage(messages.workspacesPlaceholder) })] })) : null, _jsx("p", { className: "text-xs text-muted-foreground", children: intl.formatMessage(messages.workspacesHint) }), emptyWorkspaces ? (_jsx("p", { className: "text-xs text-destructive", children: intl.formatMessage(messages.noWorkspaces) })) : null] }), _jsxs("fieldset", { className: "flex flex-col gap-2", children: [_jsx("legend", { className: "text-sm font-medium", children: intl.formatMessage(messages.events) }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Checkbox, { id: "webhook-all-events", checked: values.allEvents, onCheckedChange: (checked) => setValues((prev) => ({
|
|
388
|
+
...prev,
|
|
389
|
+
allEvents: checked === true
|
|
390
|
+
})) }), _jsx(Label, { htmlFor: "webhook-all-events", className: "text-sm font-normal", children: intl.formatMessage(messages.allEvents) })] }), !values.allEvents ? (_jsxs(_Fragment, { children: [_jsx(Label, { htmlFor: "webhook-events", className: "sr-only", children: intl.formatMessage(messages.events) }), _jsx(MultiSelect, { id: "webhook-events", options: eventOptions, value: values.eventKinds, onChange: (next) => setValues((prev) => ({
|
|
391
|
+
...prev,
|
|
392
|
+
eventKinds: next
|
|
393
|
+
})), placeholder: intl.formatMessage(messages.eventsPlaceholder) })] })) : null] }), _jsxs("fieldset", { className: "flex flex-col gap-2", children: [_jsx("legend", { className: "text-sm font-medium", children: intl.formatMessage(messages.contentTypes) }), !contentTypesAnswerable ? (_jsx("p", { className: "text-xs text-muted-foreground", children: intl.formatMessage(messages.contentTypesNeedWorkspace) })) : null, contentTypesAnswerable ? (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Checkbox, { id: "webhook-all-content-types", checked: values.allContentTypes, onCheckedChange: (checked) => setValues((prev) => ({
|
|
394
|
+
...prev,
|
|
395
|
+
allContentTypes: checked === true
|
|
396
|
+
})) }), _jsx(Label, { htmlFor: "webhook-all-content-types", className: "text-sm font-normal", children: intl.formatMessage(messages.allContentTypes) })] })) : null, contentTypesAnswerable && !values.allContentTypes ? (_jsxs(_Fragment, { children: [_jsx(Label, { htmlFor: "webhook-content-types", className: "sr-only", children: intl.formatMessage(messages.contentTypes) }), _jsx(MultiSelect, { id: "webhook-content-types", options: contentTypeOptions, value: values.contentTypes, onChange: (next) => setValues((prev) => ({
|
|
397
|
+
...prev,
|
|
398
|
+
contentTypes: next
|
|
399
|
+
})), placeholder: intl.formatMessage(messages.contentTypesPlaceholder), searchPlaceholder: intl.formatMessage(messages.contentTypeSearch),
|
|
400
|
+
// The search box doubles as the way in for
|
|
401
|
+
// a name the registry has no row for — one
|
|
402
|
+
// control instead of a picker beside a
|
|
403
|
+
// second field that meant the same thing.
|
|
404
|
+
onCreate: addTypedContentTypes, createLabel: (name) => intl.formatMessage(messages.addContentType, { name }) }), noGrants ? (_jsx("p", { className: "text-xs text-muted-foreground", children: intl.formatMessage(messages.contentTypesNoneGranted) })) : null, !noGrants && values.contentTypes.length === 0 ? (_jsx("p", { className: "text-xs text-muted-foreground", children: intl.formatMessage(messages.noContentTypes) })) : null] })) : null, contentTypesAnswerable && !noGrants ? (_jsxs("p", { className: "text-xs text-muted-foreground", children: [intl.formatMessage(messages.contentTypesHint), !values.allContentTypes &&
|
|
405
|
+
!values.allWorkspaces &&
|
|
406
|
+
values.workspaceIds.length > 0
|
|
407
|
+
? ` ${intl.formatMessage(messages.contentTypesScoped)}`
|
|
408
|
+
: ''] })) : null] })] }), _jsxs("div", { className: "flex flex-col gap-4 lg:col-span-2", children: [error ? (_jsx(Alert, { variant: "destructive", role: "alert", children: _jsx(AlertDescription, { children: error }) })) : null, _jsxs("div", { className: "flex items-center gap-2 border-t pt-4", children: [_jsx(Button, { onClick: submit, disabled: pending, children: intl.formatMessage(endpoint ? messages.save : messages.create) }), _jsx(Button, { variant: "ghost", onClick: onCancel, children: intl.formatMessage(messages.cancel) })] })] }), _jsx(ConfirmDialog, { open: confirmDisable, onOpenChange: setConfirmDisable, title: intl.formatMessage(messages.disableTitle), description: intl.formatMessage(messages.disableBody), confirmLabel: intl.formatMessage(messages.disableConfirm), cancelLabel: intl.formatMessage(messages.cancelDisable), confirmVariant: "destructive", onConfirm: () => {
|
|
409
|
+
setValues((prev) => ({ ...prev, enabled: false }));
|
|
410
|
+
setConfirmDisable(false);
|
|
411
|
+
} })] }));
|
|
412
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/lib/presentation/components/WebhooksEmpty/index.tsx"],"names":[],"mappings":"AAoBA,yFAAyF;AACzF,wBAAgB,aAAa,CAAC,EAAE,QAAQ,EAAE,EAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;CAAE,+BAmBpE"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { defineMessages, useIntl } from 'react-intl';
|
|
3
|
+
import { Plus, Webhook } from 'lucide-react';
|
|
4
|
+
import { Button } from '@orthacms/design-system';
|
|
5
|
+
const messages = defineMessages({
|
|
6
|
+
title: {
|
|
7
|
+
id: 'webhooks.empty.title',
|
|
8
|
+
defaultMessage: 'No webhooks yet'
|
|
9
|
+
},
|
|
10
|
+
description: {
|
|
11
|
+
id: 'webhooks.empty.description',
|
|
12
|
+
defaultMessage: 'Add an endpoint to have this CMS notify another system when content changes — a site rebuild, a cache purge, a search re-index.'
|
|
13
|
+
},
|
|
14
|
+
create: {
|
|
15
|
+
id: 'webhooks.empty.create',
|
|
16
|
+
defaultMessage: 'New webhook'
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
/** The empty state: nothing is configured yet. Offers the create action when allowed. */
|
|
20
|
+
export function WebhooksEmpty({ onCreate }) {
|
|
21
|
+
const intl = useIntl();
|
|
22
|
+
return (_jsxs("div", { className: "mt-8 flex flex-col items-center gap-3 text-center", children: [_jsx(Webhook, { className: "size-10 text-muted-foreground", "aria-hidden": true }), _jsx("h2", { className: "text-lg font-medium", children: intl.formatMessage(messages.title) }), _jsx("p", { className: "max-w-md text-sm text-muted-foreground", children: intl.formatMessage(messages.description) }), onCreate ? (_jsxs(Button, { className: "mt-2", onClick: onCreate, children: [_jsx(Plus, {}), intl.formatMessage(messages.create)] })) : null] }));
|
|
23
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/lib/presentation/components/WebhooksNoAccess/index.tsx"],"names":[],"mappings":"AAeA,2DAA2D;AAC3D,wBAAgB,gBAAgB,gCAgB/B"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { defineMessages, useIntl } from 'react-intl';
|
|
3
|
+
import { ShieldAlert } from 'lucide-react';
|
|
4
|
+
const messages = defineMessages({
|
|
5
|
+
title: {
|
|
6
|
+
id: 'webhooks.noAccess.title',
|
|
7
|
+
defaultMessage: 'You don’t have access to webhooks'
|
|
8
|
+
},
|
|
9
|
+
description: {
|
|
10
|
+
id: 'webhooks.noAccess.description',
|
|
11
|
+
defaultMessage: 'Ask an administrator for the “webhooks:read” permission. Webhooks reach across every workspace and hold a signing secret, so both viewing and managing them are administrator-only.'
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
/** Shown when the signed-in user lacks `webhooks:read`. */
|
|
15
|
+
export function WebhooksNoAccess() {
|
|
16
|
+
const intl = useIntl();
|
|
17
|
+
return (_jsxs("div", { className: "mt-8 flex flex-col items-center gap-3 text-center", children: [_jsx(ShieldAlert, { className: "size-10 text-muted-foreground", "aria-hidden": true }), _jsx("h2", { className: "text-lg font-medium", children: intl.formatMessage(messages.title) }), _jsx("p", { className: "max-w-md text-sm text-muted-foreground", children: intl.formatMessage(messages.description) })] }));
|
|
18
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The table placeholder shown while the list loads. Built from the real `Table`
|
|
3
|
+
* primitives inside the same card frame the loaded table uses, so the columns
|
|
4
|
+
* line up and nothing reflows on swap. Owns the single `role="status"`
|
|
5
|
+
* announcement; the table itself is `aria-hidden`.
|
|
6
|
+
*/
|
|
7
|
+
export declare function WebhooksSkeleton({ rows }: {
|
|
8
|
+
rows?: number;
|
|
9
|
+
}): import("react").JSX.Element;
|
|
10
|
+
/**
|
|
11
|
+
* Full-page placeholder for the lazy route's `Suspense` fallback.
|
|
12
|
+
*
|
|
13
|
+
* The chrome — the top bar and the container header — is **real**, not
|
|
14
|
+
* skeleton: it is the page's identity, so it must be there from the first paint
|
|
15
|
+
* rather than popping in when the chunk resolves. Only the table is a skeleton,
|
|
16
|
+
* which is exactly what the page then swaps for its own pending state, so the
|
|
17
|
+
* chunk boundary is invisible.
|
|
18
|
+
*/
|
|
19
|
+
export declare function WebhooksPageSkeleton(): import("react").JSX.Element;
|
|
20
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/lib/presentation/components/WebhooksSkeleton/index.tsx"],"names":[],"mappings":"AA4BA;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,EAAE,IAAQ,EAAE,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,+BAyD/D;AAED;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,gCAuBnC"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { defineMessages, useIntl } from 'react-intl';
|
|
3
|
+
import { Webhook } from 'lucide-react';
|
|
4
|
+
import { PageTopBar } from '@orthacms/shell-admin';
|
|
5
|
+
import { Container, ContainerHeader, Skeleton, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@orthacms/design-system';
|
|
6
|
+
/** Intl descriptors for the webhook skeletons, co-located here. */
|
|
7
|
+
const messages = defineMessages({
|
|
8
|
+
title: { id: 'webhooks.page.title', defaultMessage: 'Webhooks' },
|
|
9
|
+
subtitle: {
|
|
10
|
+
id: 'webhooks.page.subtitle',
|
|
11
|
+
defaultMessage: 'Endpoints this CMS notifies when content changes.'
|
|
12
|
+
},
|
|
13
|
+
loading: {
|
|
14
|
+
id: 'webhooks.skeleton.loading',
|
|
15
|
+
defaultMessage: 'Loading webhooks…'
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
/**
|
|
19
|
+
* The table placeholder shown while the list loads. Built from the real `Table`
|
|
20
|
+
* primitives inside the same card frame the loaded table uses, so the columns
|
|
21
|
+
* line up and nothing reflows on swap. Owns the single `role="status"`
|
|
22
|
+
* announcement; the table itself is `aria-hidden`.
|
|
23
|
+
*/
|
|
24
|
+
export function WebhooksSkeleton({ rows = 4 }) {
|
|
25
|
+
const intl = useIntl();
|
|
26
|
+
return (_jsxs("div", { role: "status", children: [_jsx("span", { className: "sr-only", children: intl.formatMessage(messages.loading) }), _jsx("div", { "aria-hidden": true, className: "mt-4 overflow-hidden rounded-xl border bg-card shadow-xs", children: _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: _jsx(Skeleton, { className: "h-4 w-24" }) }), _jsx(TableHead, { children: _jsx(Skeleton, { className: "h-4 w-40" }) }), _jsx(TableHead, { children: _jsx(Skeleton, { className: "h-4 w-28" }) }), _jsx(TableHead, { children: _jsx(Skeleton, { className: "h-4 w-20" }) }), _jsx(TableHead, { children: _jsx(Skeleton, { className: "h-4 w-24" }) })] }) }), _jsx(TableBody, { children: Array.from({ length: rows }).map((_, index) => (_jsxs(TableRow, { children: [_jsx(TableCell, { children: _jsx(Skeleton, { className: "h-4 w-32" }) }), _jsx(TableCell, { children: _jsx(Skeleton, { className: "h-4 w-56" }) }), _jsx(TableCell, { children: _jsx(Skeleton, { className: "h-5 w-24 rounded-full" }) }), _jsx(TableCell, { children: _jsx(Skeleton, { className: "h-5 w-16 rounded-full" }) }), _jsx(TableCell, { children: _jsx(Skeleton, { className: "h-4 w-24" }) })] }, index))) })] }) })] }));
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Full-page placeholder for the lazy route's `Suspense` fallback.
|
|
30
|
+
*
|
|
31
|
+
* The chrome — the top bar and the container header — is **real**, not
|
|
32
|
+
* skeleton: it is the page's identity, so it must be there from the first paint
|
|
33
|
+
* rather than popping in when the chunk resolves. Only the table is a skeleton,
|
|
34
|
+
* which is exactly what the page then swaps for its own pending state, so the
|
|
35
|
+
* chunk boundary is invisible.
|
|
36
|
+
*/
|
|
37
|
+
export function WebhooksPageSkeleton() {
|
|
38
|
+
const intl = useIntl();
|
|
39
|
+
return (_jsxs(_Fragment, { children: [_jsx(PageTopBar, { icon: Webhook, crumbs: [
|
|
40
|
+
{
|
|
41
|
+
key: 'webhooks',
|
|
42
|
+
label: intl.formatMessage(messages.title)
|
|
43
|
+
}
|
|
44
|
+
] }), _jsxs(Container, { children: [_jsx(ContainerHeader, { title: intl.formatMessage(messages.title), subtitle: intl.formatMessage(messages.subtitle) }), _jsx(WebhooksSkeleton, {})] })] }));
|
|
45
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { WebhookEndpoint } from '../../../domain/types/webhook';
|
|
2
|
+
/**
|
|
3
|
+
* The endpoint list.
|
|
4
|
+
*
|
|
5
|
+
* The filter columns read as chips rather than as lists, because the question
|
|
6
|
+
* from the list is "roughly what does this one take?" — the exact set belongs on
|
|
7
|
+
* the detail page, where it can be edited.
|
|
8
|
+
*/
|
|
9
|
+
export declare function WebhooksTable({ endpoints }: {
|
|
10
|
+
endpoints: WebhookEndpoint[];
|
|
11
|
+
}): import("react").JSX.Element;
|
|
12
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/lib/presentation/components/WebhooksTable/index.tsx"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AA2CrE;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,EAAE,SAAS,EAAE,EAAE;IAAE,SAAS,EAAE,eAAe,EAAE,CAAA;CAAE,+BAwI5E"}
|