@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.
Files changed (93) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +7 -0
  3. package/dist/index.d.ts +12 -0
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +9 -0
  6. package/dist/lib/application/useContentTypeOptions/index.d.ts +18 -0
  7. package/dist/lib/application/useContentTypeOptions/index.d.ts.map +1 -0
  8. package/dist/lib/application/useContentTypeOptions/index.js +40 -0
  9. package/dist/lib/application/useWebhookDeliveries/index.d.ts +19 -0
  10. package/dist/lib/application/useWebhookDeliveries/index.d.ts.map +1 -0
  11. package/dist/lib/application/useWebhookDeliveries/index.js +48 -0
  12. package/dist/lib/application/useWebhookEndpoints/index.d.ts +12 -0
  13. package/dist/lib/application/useWebhookEndpoints/index.d.ts.map +1 -0
  14. package/dist/lib/application/useWebhookEndpoints/index.js +26 -0
  15. package/dist/lib/application/useWebhookEvents/index.d.ts +9 -0
  16. package/dist/lib/application/useWebhookEvents/index.d.ts.map +1 -0
  17. package/dist/lib/application/useWebhookEvents/index.js +20 -0
  18. package/dist/lib/application/useWebhookMutations/index.d.ts +25 -0
  19. package/dist/lib/application/useWebhookMutations/index.d.ts.map +1 -0
  20. package/dist/lib/application/useWebhookMutations/index.js +79 -0
  21. package/dist/lib/application/useWorkspaceOptions/index.d.ts +9 -0
  22. package/dist/lib/application/useWorkspaceOptions/index.d.ts.map +1 -0
  23. package/dist/lib/application/useWorkspaceOptions/index.js +19 -0
  24. package/dist/lib/domain/contentTypeChoices/index.d.ts +65 -0
  25. package/dist/lib/domain/contentTypeChoices/index.d.ts.map +1 -0
  26. package/dist/lib/domain/contentTypeChoices/index.js +70 -0
  27. package/dist/lib/domain/headerRules/index.d.ts +37 -0
  28. package/dist/lib/domain/headerRules/index.d.ts.map +1 -0
  29. package/dist/lib/domain/headerRules/index.js +66 -0
  30. package/dist/lib/domain/types/contentTypeOption/index.d.ts +14 -0
  31. package/dist/lib/domain/types/contentTypeOption/index.d.ts.map +1 -0
  32. package/dist/lib/domain/types/contentTypeOption/index.js +1 -0
  33. package/dist/lib/domain/types/webhook/index.d.ts +102 -0
  34. package/dist/lib/domain/types/webhook/index.d.ts.map +1 -0
  35. package/dist/lib/domain/types/webhook/index.js +18 -0
  36. package/dist/lib/domain/types/workspaceOption/index.d.ts +16 -0
  37. package/dist/lib/domain/types/workspaceOption/index.d.ts.map +1 -0
  38. package/dist/lib/domain/types/workspaceOption/index.js +1 -0
  39. package/dist/lib/infrastructure/httpWebhookGateway/index.d.ts +11 -0
  40. package/dist/lib/infrastructure/httpWebhookGateway/index.d.ts.map +1 -0
  41. package/dist/lib/infrastructure/httpWebhookGateway/index.js +138 -0
  42. package/dist/lib/infrastructure/serverMessageOf/index.d.ts +16 -0
  43. package/dist/lib/infrastructure/serverMessageOf/index.d.ts.map +1 -0
  44. package/dist/lib/infrastructure/serverMessageOf/index.js +24 -0
  45. package/dist/lib/infrastructure/webhookGateway/index.d.ts +62 -0
  46. package/dist/lib/infrastructure/webhookGateway/index.d.ts.map +1 -0
  47. package/dist/lib/infrastructure/webhookGateway/index.js +1 -0
  48. package/dist/lib/infrastructure/webhookMapper/index.d.ts +104 -0
  49. package/dist/lib/infrastructure/webhookMapper/index.d.ts.map +1 -0
  50. package/dist/lib/infrastructure/webhookMapper/index.js +49 -0
  51. package/dist/lib/infrastructure/webhooksKeys/index.d.ts +35 -0
  52. package/dist/lib/infrastructure/webhooksKeys/index.d.ts.map +1 -0
  53. package/dist/lib/infrastructure/webhooksKeys/index.js +23 -0
  54. package/dist/lib/presentation/components/DeliveryStatusBadge/index.d.ts +6 -0
  55. package/dist/lib/presentation/components/DeliveryStatusBadge/index.d.ts.map +1 -0
  56. package/dist/lib/presentation/components/DeliveryStatusBadge/index.js +30 -0
  57. package/dist/lib/presentation/components/RevealWebhookSecretDialog/index.d.ts +25 -0
  58. package/dist/lib/presentation/components/RevealWebhookSecretDialog/index.d.ts.map +1 -0
  59. package/dist/lib/presentation/components/RevealWebhookSecretDialog/index.js +107 -0
  60. package/dist/lib/presentation/components/WebhookDeliveriesPanel/DeliveryDetailSheet/index.d.ts +19 -0
  61. package/dist/lib/presentation/components/WebhookDeliveriesPanel/DeliveryDetailSheet/index.d.ts.map +1 -0
  62. package/dist/lib/presentation/components/WebhookDeliveriesPanel/DeliveryDetailSheet/index.js +72 -0
  63. package/dist/lib/presentation/components/WebhookDeliveriesPanel/index.d.ts +12 -0
  64. package/dist/lib/presentation/components/WebhookDeliveriesPanel/index.d.ts.map +1 -0
  65. package/dist/lib/presentation/components/WebhookDeliveriesPanel/index.js +132 -0
  66. package/dist/lib/presentation/components/WebhookForm/index.d.ts +46 -0
  67. package/dist/lib/presentation/components/WebhookForm/index.d.ts.map +1 -0
  68. package/dist/lib/presentation/components/WebhookForm/index.js +412 -0
  69. package/dist/lib/presentation/components/WebhooksEmpty/index.d.ts +5 -0
  70. package/dist/lib/presentation/components/WebhooksEmpty/index.d.ts.map +1 -0
  71. package/dist/lib/presentation/components/WebhooksEmpty/index.js +23 -0
  72. package/dist/lib/presentation/components/WebhooksNoAccess/index.d.ts +3 -0
  73. package/dist/lib/presentation/components/WebhooksNoAccess/index.d.ts.map +1 -0
  74. package/dist/lib/presentation/components/WebhooksNoAccess/index.js +18 -0
  75. package/dist/lib/presentation/components/WebhooksSkeleton/index.d.ts +20 -0
  76. package/dist/lib/presentation/components/WebhooksSkeleton/index.d.ts.map +1 -0
  77. package/dist/lib/presentation/components/WebhooksSkeleton/index.js +45 -0
  78. package/dist/lib/presentation/components/WebhooksTable/index.d.ts +12 -0
  79. package/dist/lib/presentation/components/WebhooksTable/index.d.ts.map +1 -0
  80. package/dist/lib/presentation/components/WebhooksTable/index.js +79 -0
  81. package/dist/lib/presentation/pages/WebhookDetailPage/index.d.ts +10 -0
  82. package/dist/lib/presentation/pages/WebhookDetailPage/index.d.ts.map +1 -0
  83. package/dist/lib/presentation/pages/WebhookDetailPage/index.js +191 -0
  84. package/dist/lib/presentation/pages/WebhookEditorPage/index.d.ts +28 -0
  85. package/dist/lib/presentation/pages/WebhookEditorPage/index.d.ts.map +1 -0
  86. package/dist/lib/presentation/pages/WebhookEditorPage/index.js +149 -0
  87. package/dist/lib/presentation/pages/WebhooksPage/index.d.ts +11 -0
  88. package/dist/lib/presentation/pages/WebhooksPage/index.d.ts.map +1 -0
  89. package/dist/lib/presentation/pages/WebhooksPage/index.js +65 -0
  90. package/dist/lib/utils/webhooksPlugin/index.d.ts +35 -0
  91. package/dist/lib/utils/webhooksPlugin/index.d.ts.map +1 -0
  92. package/dist/lib/utils/webhooksPlugin/index.js +88 -0
  93. package/package.json +46 -0
@@ -0,0 +1,79 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Link } from 'react-router-dom';
3
+ import { defineMessages, useIntl } from 'react-intl';
4
+ import { Badge, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@orthacms/design-system';
5
+ import { DeliveryStatusBadge } from '../DeliveryStatusBadge';
6
+ const messages = defineMessages({
7
+ caption: {
8
+ id: 'webhooks.table.caption',
9
+ defaultMessage: 'Configured webhook endpoints'
10
+ },
11
+ name: { id: 'webhooks.table.name', defaultMessage: 'Name' },
12
+ url: { id: 'webhooks.table.url', defaultMessage: 'URL' },
13
+ events: { id: 'webhooks.table.events', defaultMessage: 'Sends' },
14
+ state: { id: 'webhooks.table.state', defaultMessage: 'State' },
15
+ lastDelivery: {
16
+ id: 'webhooks.table.lastDelivery',
17
+ defaultMessage: 'Last delivery'
18
+ },
19
+ active: { id: 'webhooks.table.active', defaultMessage: 'Active' },
20
+ paused: { id: 'webhooks.table.paused', defaultMessage: 'Paused' },
21
+ autoDisabled: {
22
+ id: 'webhooks.table.autoDisabled',
23
+ defaultMessage: 'Stopped after failures'
24
+ },
25
+ allEvents: { id: 'webhooks.table.allEvents', defaultMessage: 'All events' },
26
+ someEvents: {
27
+ id: 'webhooks.table.someEvents',
28
+ defaultMessage: '{count, plural, one {# event} other {# events}}'
29
+ },
30
+ allWorkspaces: {
31
+ id: 'webhooks.table.allWorkspaces',
32
+ defaultMessage: 'All workspaces'
33
+ },
34
+ someWorkspaces: {
35
+ id: 'webhooks.table.someWorkspaces',
36
+ defaultMessage: '{count, plural, one {# workspace} other {# workspaces}}'
37
+ },
38
+ someTypes: {
39
+ id: 'webhooks.table.someTypes',
40
+ defaultMessage: '{count, plural, one {# type} other {# types}}'
41
+ },
42
+ never: { id: 'webhooks.table.never', defaultMessage: 'Nothing sent yet' }
43
+ });
44
+ /**
45
+ * The endpoint list.
46
+ *
47
+ * The filter columns read as chips rather than as lists, because the question
48
+ * from the list is "roughly what does this one take?" — the exact set belongs on
49
+ * the detail page, where it can be edited.
50
+ */
51
+ export function WebhooksTable({ endpoints }) {
52
+ const intl = useIntl();
53
+ return (_jsx("div", { className: "mt-4 overflow-hidden rounded-xl border bg-card shadow-xs", children: _jsxs(Table, { children: [_jsx("caption", { className: "sr-only", children: intl.formatMessage(messages.caption) }), _jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: intl.formatMessage(messages.name) }), _jsx(TableHead, { children: intl.formatMessage(messages.url) }), _jsx(TableHead, { children: intl.formatMessage(messages.events) }), _jsx(TableHead, { children: intl.formatMessage(messages.state) }), _jsx(TableHead, { children: intl.formatMessage(messages.lastDelivery) })] }) }), _jsx(TableBody, { children: endpoints.map((endpoint) => (_jsxs(TableRow, { children: [_jsx(TableCell, { className: "font-medium", children: _jsx(Link, { to: `/webhooks/${endpoint.id}`, className: "underline-offset-4 hover:underline focus-visible:underline", children: endpoint.name }) }), _jsx(TableCell, { className: "max-w-[24rem] truncate font-mono text-xs text-muted-foreground", children: endpoint.url }), _jsx(TableCell, { children: _jsxs("div", { className: "flex flex-wrap gap-1", children: [_jsx(Badge, { variant: "outline", children: endpoint.eventKinds.length === 0
54
+ ? intl.formatMessage(messages.allEvents)
55
+ : intl.formatMessage(messages.someEvents, {
56
+ count: endpoint.eventKinds
57
+ .length
58
+ }) }), _jsx(Badge, { variant: "outline", children: endpoint.allWorkspaces
59
+ ? intl.formatMessage(messages.allWorkspaces)
60
+ : intl.formatMessage(messages.someWorkspaces, {
61
+ count: endpoint
62
+ .workspaceIds.length
63
+ }) }), endpoint.contentTypes.length > 0 ? (_jsx(Badge, { variant: "outline", children: intl.formatMessage(messages.someTypes, {
64
+ count: endpoint.contentTypes
65
+ .length
66
+ }) })) : null] }) }), _jsx(TableCell, { children: endpoint.enabled ? (_jsx(Badge, { variant: "secondary", children: intl.formatMessage(messages.active) })) : (
67
+ // An endpoint the server switched off is a
68
+ // different situation from one someone
69
+ // paused, and the operator has to be able
70
+ // to tell them apart at a glance.
71
+ _jsx(Badge, { variant: endpoint.disabledReason
72
+ ? 'destructive'
73
+ : 'outline', children: intl.formatMessage(endpoint.disabledReason
74
+ ? messages.autoDisabled
75
+ : messages.paused) })) }), _jsx(TableCell, { className: "text-sm text-muted-foreground", children: endpoint.lastDelivery ? (_jsxs("span", { className: "flex items-center gap-2", children: [_jsx(DeliveryStatusBadge, { status: endpoint.lastDelivery.status }), _jsx("span", { children: intl.formatDate(endpoint.lastDelivery.createdAt, {
76
+ dateStyle: 'medium',
77
+ timeStyle: 'short'
78
+ }) })] })) : (intl.formatMessage(messages.never)) })] }, endpoint.id))) })] }) }));
79
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * One endpoint: its configuration, and its delivery log.
3
+ *
4
+ * The two tabs answer the two questions someone comes here with — "what does
5
+ * this send?" and "did it arrive?" — and the second is where almost all the
6
+ * time is spent, which is why the log polls itself rather than waiting to be
7
+ * refreshed.
8
+ */
9
+ export declare function WebhookDetailPage(): import("react").JSX.Element;
10
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/lib/presentation/pages/WebhookDetailPage/index.tsx"],"names":[],"mappings":"AAgJA;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,gCAsRhC"}
@@ -0,0 +1,191 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import { useNavigate, useParams } from 'react-router-dom';
4
+ import { defineMessages, useIntl } from 'react-intl';
5
+ import { Pencil, Send, Trash2, TriangleAlert, Webhook } from 'lucide-react';
6
+ import { PageTopBar } from '@orthacms/shell-admin';
7
+ import { useHasPermission } from '@orthacms/identity-admin';
8
+ import { useDocumentTitle } from '@orthacms/utils-admin';
9
+ import { Alert, AlertDescription, Button, ConfirmDialog, Container, ContainerHeader, Spinner, Tabs, TabsContent, TabsList, TabsTrigger, toast } from '@orthacms/design-system';
10
+ import { useWebhookEndpoint } from '../../../application/useWebhookEndpoints';
11
+ import { useDeleteWebhook, useRotateWebhookSecret, useTestWebhook } from '../../../application/useWebhookMutations';
12
+ import { RevealWebhookSecretDialog } from '../../components/RevealWebhookSecretDialog';
13
+ import { WebhookDeliveriesPanel } from '../../components/WebhookDeliveriesPanel';
14
+ import { WebhooksNoAccess } from '../../components/WebhooksNoAccess';
15
+ const messages = defineMessages({
16
+ listTitle: { id: 'webhooks.page.title', defaultMessage: 'Webhooks' },
17
+ settings: {
18
+ id: 'webhooks.detail.settings',
19
+ defaultMessage: 'Settings'
20
+ },
21
+ deliveries: {
22
+ id: 'webhooks.detail.deliveries',
23
+ defaultMessage: 'Deliveries'
24
+ },
25
+ edit: { id: 'webhooks.detail.edit', defaultMessage: 'Edit' },
26
+ test: { id: 'webhooks.detail.test', defaultMessage: 'Send test' },
27
+ rotate: {
28
+ id: 'webhooks.detail.rotate',
29
+ defaultMessage: 'Rotate secret'
30
+ },
31
+ remove: { id: 'webhooks.detail.remove', defaultMessage: 'Delete' },
32
+ url: { id: 'webhooks.detail.url', defaultMessage: 'URL' },
33
+ secret: { id: 'webhooks.detail.secret', defaultMessage: 'Signing secret' },
34
+ headers: {
35
+ id: 'webhooks.detail.headers',
36
+ defaultMessage: 'Custom headers'
37
+ },
38
+ noHeaders: {
39
+ id: 'webhooks.detail.noHeaders',
40
+ defaultMessage: 'None'
41
+ },
42
+ secretValue: {
43
+ id: 'webhooks.detail.secretValue',
44
+ defaultMessage: 'Ends in {hint}'
45
+ },
46
+ events: { id: 'webhooks.detail.events', defaultMessage: 'Events' },
47
+ allEvents: {
48
+ id: 'webhooks.detail.allEvents',
49
+ defaultMessage: 'All events'
50
+ },
51
+ workspaces: {
52
+ id: 'webhooks.detail.workspaces',
53
+ defaultMessage: 'Workspaces'
54
+ },
55
+ allWorkspaces: {
56
+ id: 'webhooks.detail.allWorkspaces',
57
+ defaultMessage: 'All workspaces'
58
+ },
59
+ contentTypes: {
60
+ id: 'webhooks.detail.contentTypes',
61
+ defaultMessage: 'Content types'
62
+ },
63
+ allContentTypes: {
64
+ id: 'webhooks.detail.allContentTypes',
65
+ defaultMessage: 'All content types'
66
+ },
67
+ state: { id: 'webhooks.detail.state', defaultMessage: 'State' },
68
+ active: { id: 'webhooks.detail.active', defaultMessage: 'Active' },
69
+ paused: { id: 'webhooks.detail.paused', defaultMessage: 'Paused' },
70
+ notFound: {
71
+ id: 'webhooks.detail.notFound',
72
+ defaultMessage: 'That webhook no longer exists.'
73
+ },
74
+ back: { id: 'webhooks.detail.back', defaultMessage: 'Back to webhooks' },
75
+ deleted: {
76
+ id: 'webhooks.detail.deleted',
77
+ defaultMessage: 'Webhook deleted'
78
+ },
79
+ deleteFailed: {
80
+ id: 'webhooks.detail.deleteFailed',
81
+ defaultMessage: 'Couldn’t delete the webhook. Please try again.'
82
+ },
83
+ confirmDeleteTitle: {
84
+ id: 'webhooks.detail.confirmDeleteTitle',
85
+ defaultMessage: 'Delete this webhook?'
86
+ },
87
+ confirmDeleteBody: {
88
+ id: 'webhooks.detail.confirmDeleteBody',
89
+ defaultMessage: 'Deliveries stop immediately and the delivery log goes with it. This cannot be undone.'
90
+ },
91
+ confirmDelete: {
92
+ id: 'webhooks.detail.confirmDelete',
93
+ defaultMessage: 'Delete webhook'
94
+ },
95
+ cancel: { id: 'webhooks.detail.cancel', defaultMessage: 'Cancel' },
96
+ testOk: {
97
+ id: 'webhooks.detail.testOk',
98
+ defaultMessage: 'Receiver answered {status} in {ms} ms'
99
+ },
100
+ testFailed: {
101
+ id: 'webhooks.detail.testFailed',
102
+ defaultMessage: 'Test delivery failed: {reason}'
103
+ },
104
+ autoDisabled: {
105
+ id: 'webhooks.detail.autoDisabled',
106
+ defaultMessage: '{reason} Fix the receiver, then switch the webhook back on from Edit.'
107
+ }
108
+ });
109
+ /** One labelled fact in the settings summary. */
110
+ function Fact({ label, children }) {
111
+ return (_jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("dt", { className: "text-xs text-muted-foreground", children: label }), _jsx("dd", { className: "text-sm", children: children })] }));
112
+ }
113
+ /**
114
+ * One endpoint: its configuration, and its delivery log.
115
+ *
116
+ * The two tabs answer the two questions someone comes here with — "what does
117
+ * this send?" and "did it arrive?" — and the second is where almost all the
118
+ * time is spent, which is why the log polls itself rather than waiting to be
119
+ * refreshed.
120
+ */
121
+ export function WebhookDetailPage() {
122
+ const intl = useIntl();
123
+ const navigate = useNavigate();
124
+ const { id = '' } = useParams();
125
+ const canRead = useHasPermission('webhooks:read');
126
+ const canManage = useHasPermission('webhooks:manage');
127
+ const [secret, setSecret] = useState(null);
128
+ const [confirmDelete, setConfirmDelete] = useState(false);
129
+ const { data: endpoint, isPending, isError } = useWebhookEndpoint(id, canRead && id.length > 0);
130
+ const remove = useDeleteWebhook();
131
+ const rotate = useRotateWebhookSecret();
132
+ const test = useTestWebhook();
133
+ useDocumentTitle(endpoint?.name ?? intl.formatMessage(messages.listTitle));
134
+ if (!canRead) {
135
+ return (_jsx(Container, { children: _jsx(WebhooksNoAccess, {}) }));
136
+ }
137
+ if (isPending) {
138
+ return (_jsx(Container, { children: _jsx("div", { className: "flex justify-center py-16", children: _jsx(Spinner, {}) }) }));
139
+ }
140
+ if (isError || !endpoint) {
141
+ return (_jsx(Container, { children: _jsx(Alert, { variant: "destructive", role: "alert", className: "mt-8", children: _jsxs(AlertDescription, { className: "flex items-center justify-between gap-4", children: [intl.formatMessage(messages.notFound), _jsx(Button, { size: "sm", variant: "outline", onClick: () => navigate('/webhooks'), children: intl.formatMessage(messages.back) })] }) }) }));
142
+ }
143
+ return (_jsxs(_Fragment, { children: [_jsx(PageTopBar, { icon: Webhook, crumbs: [
144
+ {
145
+ key: 'webhooks',
146
+ label: intl.formatMessage(messages.listTitle),
147
+ to: '/webhooks'
148
+ },
149
+ { key: endpoint.id, label: endpoint.name }
150
+ ] }), _jsxs(Container, { children: [_jsx(ContainerHeader, { title: endpoint.name, subtitle: endpoint.url, actions: canManage ? (_jsxs("div", { className: "flex gap-2", children: [_jsxs(Button, { variant: "outline", disabled: test.isPending, onClick: () => test.mutate(endpoint.id, {
151
+ onSuccess: (result) => {
152
+ if (result.ok) {
153
+ toast.success(intl.formatMessage(messages.testOk, {
154
+ status: result.statusCode,
155
+ ms: result.durationMs
156
+ }));
157
+ }
158
+ else {
159
+ toast.error(intl.formatMessage(messages.testFailed, {
160
+ reason: result.error ??
161
+ `HTTP ${result.statusCode}`
162
+ }));
163
+ }
164
+ }
165
+ }), children: [_jsx(Send, { "aria-hidden": true }), intl.formatMessage(messages.test)] }), _jsxs(Button, { variant: "outline", onClick: () => navigate(`/webhooks/${endpoint.id}/edit`), children: [_jsx(Pencil, { "aria-hidden": true }), intl.formatMessage(messages.edit)] })] })) : undefined }), endpoint.disabledReason ? (_jsxs(Alert, { variant: "destructive", role: "alert", className: "mt-4", children: [_jsx(TriangleAlert, { "aria-hidden": true }), _jsx(AlertDescription, { children: intl.formatMessage(messages.autoDisabled, {
166
+ reason: endpoint.disabledReason
167
+ }) })] })) : null, _jsxs(Tabs, { defaultValue: "deliveries", className: "mt-6", children: [_jsxs(TabsList, { children: [_jsx(TabsTrigger, { value: "deliveries", children: intl.formatMessage(messages.deliveries) }), _jsx(TabsTrigger, { value: "settings", children: intl.formatMessage(messages.settings) })] }), _jsx(TabsContent, { value: "deliveries", className: "mt-4", children: _jsx(WebhookDeliveriesPanel, { endpointId: endpoint.id, canRedeliver: canManage }) }), _jsxs(TabsContent, { value: "settings", className: "mt-4", children: [_jsxs("dl", { className: "grid gap-4 sm:grid-cols-2", children: [_jsx(Fact, { label: intl.formatMessage(messages.url), children: _jsx("span", { className: "font-mono text-xs break-all", children: endpoint.url }) }), _jsx(Fact, { label: intl.formatMessage(messages.state), children: intl.formatMessage(endpoint.enabled
168
+ ? messages.active
169
+ : messages.paused) }), _jsx(Fact, { label: intl.formatMessage(messages.events), children: endpoint.eventKinds.length === 0
170
+ ? intl.formatMessage(messages.allEvents)
171
+ : endpoint.eventKinds.join(', ') }), _jsx(Fact, { label: intl.formatMessage(messages.contentTypes), children: endpoint.contentTypes.length === 0
172
+ ? intl.formatMessage(messages.allContentTypes)
173
+ : endpoint.contentTypes.join(', ') }), _jsx(Fact, { label: intl.formatMessage(messages.workspaces), children: endpoint.allWorkspaces
174
+ ? intl.formatMessage(messages.allWorkspaces)
175
+ : endpoint.workspaceIds.length }), _jsx(Fact, { label: intl.formatMessage(messages.headers), children: _jsx("span", { className: "font-mono text-xs break-all", children: Object.keys(endpoint.headers).length === 0
176
+ ? intl.formatMessage(messages.noHeaders)
177
+ : Object.keys(endpoint.headers).join(', ') }) }), _jsx(Fact, { label: intl.formatMessage(messages.secret), children: _jsx("span", { className: "font-mono text-xs", children: intl.formatMessage(messages.secretValue, {
178
+ hint: endpoint.secretHint
179
+ }) }) })] }), canManage ? (_jsxs("div", { className: "mt-8 flex flex-wrap gap-2 border-t pt-4", children: [_jsx(Button, { variant: "outline", disabled: rotate.isPending, onClick: () => rotate.mutate(endpoint.id, {
180
+ onSuccess: ({ secret: minted }) => setSecret(minted)
181
+ }), children: intl.formatMessage(messages.rotate) }), _jsxs(Button, { variant: "destructive", onClick: () => setConfirmDelete(true), children: [_jsx(Trash2, { "aria-hidden": true }), intl.formatMessage(messages.remove)] })] })) : null] })] })] }), _jsx(RevealWebhookSecretDialog, { secret: secret, rotated: true, open: secret !== null, onOpenChange: (next) => {
182
+ if (!next)
183
+ setSecret(null);
184
+ } }), _jsx(ConfirmDialog, { open: confirmDelete, onOpenChange: setConfirmDelete, title: intl.formatMessage(messages.confirmDeleteTitle), description: intl.formatMessage(messages.confirmDeleteBody), confirmLabel: intl.formatMessage(messages.confirmDelete), cancelLabel: intl.formatMessage(messages.cancel), confirmVariant: "destructive", busy: remove.isPending, onConfirm: () => remove.mutate(endpoint.id, {
185
+ onSuccess: () => {
186
+ toast.success(intl.formatMessage(messages.deleted));
187
+ navigate('/webhooks');
188
+ },
189
+ onError: () => toast.error(intl.formatMessage(messages.deleteFailed))
190
+ }) })] }));
191
+ }
@@ -0,0 +1,28 @@
1
+ /** Props for {@link WebhookEditorPage}. */
2
+ export type WebhookEditorPageProps = {
3
+ /**
4
+ * `'create'` mounts a blank form; `'edit'` (the default) loads the endpoint
5
+ * named by the route.
6
+ *
7
+ * One component for both, as in the alarms rule editor: the two forms are
8
+ * the same form. The only differences are what seeds it and where saving
9
+ * goes afterwards, and a second page would duplicate the whole subscription
10
+ * surface for that.
11
+ */
12
+ mode?: 'create' | 'edit';
13
+ };
14
+ /**
15
+ * Creates or edits one endpoint, as a page rather than a dialog.
16
+ *
17
+ * The form asks eight questions across three filters, two of which are pickers
18
+ * that open their own popovers; inside a modal that meant a scrolling box with
19
+ * popovers portalled into it to survive the scroll lock. On a page the two
20
+ * halves — where deliveries go, and what is worth sending — sit side by side,
21
+ * and the browser's own back button is the way out.
22
+ *
23
+ * Creating ends on the one-time secret: it exists nowhere else from that moment
24
+ * on, so the reveal opens here and only then does the page move to the new
25
+ * endpoint.
26
+ */
27
+ export declare function WebhookEditorPage({ mode }?: WebhookEditorPageProps): import("react").JSX.Element;
28
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/lib/presentation/pages/WebhookEditorPage/index.tsx"],"names":[],"mappings":"AA6DA,2CAA2C;AAC3C,MAAM,MAAM,sBAAsB,GAAG;IACjC;;;;;;;;OAQG;IACH,IAAI,CAAC,EAAE,QAAQ,GAAG,MAAM,CAAC;CAC5B,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,wBAAgB,iBAAiB,CAAC,EAC9B,IAAa,EAChB,GAAE,sBAA2B,+BAsL7B"}
@@ -0,0 +1,149 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import { defineMessages, useIntl } from 'react-intl';
4
+ import { useNavigate, useParams } from 'react-router-dom';
5
+ import { Webhook } from 'lucide-react';
6
+ import { PageTopBar } from '@orthacms/shell-admin';
7
+ import { useHasPermission } from '@orthacms/identity-admin';
8
+ import { useDocumentTitle } from '@orthacms/utils-admin';
9
+ import { Alert, AlertDescription, Button, Container, ContainerHeader, Spinner, toast } from '@orthacms/design-system';
10
+ import { useWebhookEndpoint } from '../../../application/useWebhookEndpoints';
11
+ import { useWebhookEvents } from '../../../application/useWebhookEvents';
12
+ import { useWorkspaceOptions } from '../../../application/useWorkspaceOptions';
13
+ import { useContentTypeOptions } from '../../../application/useContentTypeOptions';
14
+ import { useCreateWebhook, useUpdateWebhook } from '../../../application/useWebhookMutations';
15
+ import { serverMessageOf } from '../../../infrastructure/serverMessageOf';
16
+ import { WebhookForm } from '../../components/WebhookForm';
17
+ import { RevealWebhookSecretDialog } from '../../components/RevealWebhookSecretDialog';
18
+ import { WebhooksNoAccess } from '../../components/WebhooksNoAccess';
19
+ const messages = defineMessages({
20
+ listTitle: { id: 'webhooks.page.title', defaultMessage: 'Webhooks' },
21
+ createTitle: {
22
+ id: 'webhooks.editor.createTitle',
23
+ defaultMessage: 'New webhook'
24
+ },
25
+ editTitle: {
26
+ id: 'webhooks.editor.editTitle',
27
+ defaultMessage: 'Edit webhook'
28
+ },
29
+ subtitle: {
30
+ id: 'webhooks.editor.subtitle',
31
+ defaultMessage: 'Choose where deliveries go and which changes are worth sending.'
32
+ },
33
+ created: { id: 'webhooks.page.created', defaultMessage: 'Webhook created' },
34
+ saved: { id: 'webhooks.detail.saved', defaultMessage: 'Webhook updated' },
35
+ createFailed: {
36
+ id: 'webhooks.page.createFailed',
37
+ defaultMessage: 'Couldn’t create the webhook. Please try again.'
38
+ },
39
+ saveFailed: {
40
+ id: 'webhooks.detail.saveFailed',
41
+ defaultMessage: 'Couldn’t save the webhook. Please try again.'
42
+ },
43
+ notFound: {
44
+ id: 'webhooks.detail.notFound',
45
+ defaultMessage: 'That webhook no longer exists.'
46
+ },
47
+ back: { id: 'webhooks.detail.back', defaultMessage: 'Back to webhooks' }
48
+ });
49
+ /**
50
+ * Creates or edits one endpoint, as a page rather than a dialog.
51
+ *
52
+ * The form asks eight questions across three filters, two of which are pickers
53
+ * that open their own popovers; inside a modal that meant a scrolling box with
54
+ * popovers portalled into it to survive the scroll lock. On a page the two
55
+ * halves — where deliveries go, and what is worth sending — sit side by side,
56
+ * and the browser's own back button is the way out.
57
+ *
58
+ * Creating ends on the one-time secret: it exists nowhere else from that moment
59
+ * on, so the reveal opens here and only then does the page move to the new
60
+ * endpoint.
61
+ */
62
+ export function WebhookEditorPage({ mode = 'edit' } = {}) {
63
+ const intl = useIntl();
64
+ const navigate = useNavigate();
65
+ const { id = '' } = useParams();
66
+ const isCreate = mode === 'create';
67
+ const canRead = useHasPermission('webhooks:read');
68
+ const canManage = useHasPermission('webhooks:manage');
69
+ const [formError, setFormError] = useState(null);
70
+ const [created, setCreated] = useState(null);
71
+ const { data: endpoint, isPending, isError } = useWebhookEndpoint(id, canRead && !isCreate && id.length > 0);
72
+ // The pickers are the page now, so unlike the dialog they load with it.
73
+ const { data: events } = useWebhookEvents(canManage);
74
+ const { data: workspaces } = useWorkspaceOptions(canManage);
75
+ const { data: contentTypes } = useContentTypeOptions(canManage);
76
+ const create = useCreateWebhook();
77
+ const update = useUpdateWebhook();
78
+ const title = intl.formatMessage(isCreate ? messages.createTitle : messages.editTitle);
79
+ useDocumentTitle(title);
80
+ const crumbs = [
81
+ {
82
+ key: 'webhooks',
83
+ label: intl.formatMessage(messages.listTitle),
84
+ to: '/webhooks'
85
+ },
86
+ ...(isCreate
87
+ ? []
88
+ : [
89
+ {
90
+ key: 'endpoint',
91
+ label: endpoint?.name ?? title,
92
+ to: `/webhooks/${id}`
93
+ }
94
+ ]),
95
+ { key: 'editor', label: title }
96
+ ];
97
+ // `webhooks:manage` and not merely `webhooks:read`: this page only exists
98
+ // to write. Reaching it by URL without the key gets the same answer the nav
99
+ // gives, rather than a form whose Save would be refused.
100
+ if (!canRead || !canManage) {
101
+ return (_jsxs(_Fragment, { children: [_jsx(PageTopBar, { icon: Webhook, crumbs: crumbs }), _jsxs(Container, { children: [_jsx(ContainerHeader, { title: title }), _jsx(WebhooksNoAccess, {})] })] }));
102
+ }
103
+ if (!isCreate && isPending) {
104
+ return (_jsx(Container, { children: _jsx("div", { className: "flex justify-center py-16", children: _jsx(Spinner, {}) }) }));
105
+ }
106
+ if (!isCreate && (isError || !endpoint)) {
107
+ return (_jsxs(_Fragment, { children: [_jsx(PageTopBar, { icon: Webhook, crumbs: crumbs }), _jsx(Container, { children: _jsx(Alert, { variant: "destructive", role: "alert", className: "mt-8", children: _jsxs(AlertDescription, { className: "flex items-center justify-between gap-4", children: [intl.formatMessage(messages.notFound), _jsx(Button, { size: "sm", variant: "outline", onClick: () => navigate('/webhooks'), children: intl.formatMessage(messages.back) })] }) }) })] }));
108
+ }
109
+ const leave = () => navigate(isCreate ? '/webhooks' : `/webhooks/${id}`, { replace: true });
110
+ return (_jsxs(_Fragment, { children: [_jsx(PageTopBar, { icon: Webhook, crumbs: crumbs }), _jsxs(Container, { children: [_jsx(ContainerHeader, { title: title, subtitle: intl.formatMessage(messages.subtitle) }), _jsx("div", { className: "mt-6", children: _jsx(WebhookForm, { endpoint: isCreate ? undefined : endpoint, events: events ?? [], workspaces: workspaces ?? [], contentTypes: contentTypes ?? [], pending: create.isPending || update.isPending, error: formError, onCancel: leave, onSubmit: (input) => {
111
+ setFormError(null);
112
+ // A refused URL comes back with a message written
113
+ // for whoever typed it; show that rather than a
114
+ // generic failure, and stay on the form so it can
115
+ // be corrected in place.
116
+ const onError = (error) => setFormError(serverMessageOf(error) ??
117
+ intl.formatMessage(isCreate
118
+ ? messages.createFailed
119
+ : messages.saveFailed));
120
+ if (isCreate) {
121
+ create.mutate(input, {
122
+ onSuccess: ({ endpoint: made, secret }) => {
123
+ toast.success(intl.formatMessage(messages.created));
124
+ // The secret exists nowhere else from
125
+ // here on, so the page waits on the
126
+ // reveal rather than navigating past
127
+ // it.
128
+ setCreated({ id: made.id, secret });
129
+ },
130
+ onError
131
+ });
132
+ return;
133
+ }
134
+ update.mutate({ id, input }, {
135
+ onSuccess: () => {
136
+ toast.success(intl.formatMessage(messages.saved));
137
+ navigate(`/webhooks/${id}`, {
138
+ replace: true
139
+ });
140
+ },
141
+ onError
142
+ });
143
+ } }) })] }), _jsx(RevealWebhookSecretDialog, { secret: created?.secret ?? null, open: created !== null, onOpenChange: (next) => {
144
+ if (next || !created)
145
+ return;
146
+ navigate(`/webhooks/${created.id}`, { replace: true });
147
+ setCreated(null);
148
+ } })] }));
149
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * The global webhooks page (`/webhooks`, in the sidebar's directory group — no
3
+ * workspace context).
4
+ *
5
+ * Everything here is gated on `webhooks:read`, and both keys are
6
+ * administrator-only: an endpoint spans every workspace it names and holds a
7
+ * signing secret, so this is deployment configuration rather than an editorial
8
+ * surface.
9
+ */
10
+ export declare function WebhooksPage(): import("react").JSX.Element;
11
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/lib/presentation/pages/WebhooksPage/index.tsx"],"names":[],"mappings":"AAqCA;;;;;;;;GAQG;AACH,wBAAgB,YAAY,gCAuG3B"}
@@ -0,0 +1,65 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { defineMessages, useIntl } from 'react-intl';
3
+ import { useNavigate } from 'react-router-dom';
4
+ import { Plus, Webhook } from 'lucide-react';
5
+ import { PageTopBar } from '@orthacms/shell-admin';
6
+ import { useHasPermission } from '@orthacms/identity-admin';
7
+ import { useDocumentTitle } from '@orthacms/utils-admin';
8
+ import { Alert, AlertDescription, Button, Container, ContainerHeader } from '@orthacms/design-system';
9
+ import { useWebhookEndpoints } from '../../../application/useWebhookEndpoints';
10
+ import { WebhooksTable } from '../../components/WebhooksTable';
11
+ import { WebhooksEmpty } from '../../components/WebhooksEmpty';
12
+ import { WebhooksNoAccess } from '../../components/WebhooksNoAccess';
13
+ import { WebhooksSkeleton } from '../../components/WebhooksSkeleton';
14
+ const messages = defineMessages({
15
+ title: { id: 'webhooks.page.title', defaultMessage: 'Webhooks' },
16
+ subtitle: {
17
+ id: 'webhooks.page.subtitle',
18
+ defaultMessage: 'Endpoints this CMS notifies when content changes.'
19
+ },
20
+ create: { id: 'webhooks.page.create', defaultMessage: 'New webhook' },
21
+ error: {
22
+ id: 'webhooks.page.error',
23
+ defaultMessage: 'Couldn’t load webhooks. Please try again.'
24
+ },
25
+ retry: { id: 'webhooks.page.retry', defaultMessage: 'Retry' },
26
+ count: {
27
+ id: 'webhooks.page.count',
28
+ defaultMessage: '{count, plural, one {# webhook} other {# webhooks}}'
29
+ }
30
+ });
31
+ /**
32
+ * The global webhooks page (`/webhooks`, in the sidebar's directory group — no
33
+ * workspace context).
34
+ *
35
+ * Everything here is gated on `webhooks:read`, and both keys are
36
+ * administrator-only: an endpoint spans every workspace it names and holds a
37
+ * signing secret, so this is deployment configuration rather than an editorial
38
+ * surface.
39
+ */
40
+ export function WebhooksPage() {
41
+ const intl = useIntl();
42
+ useDocumentTitle(intl.formatMessage(messages.title));
43
+ const canRead = useHasPermission('webhooks:read');
44
+ const canManage = useHasPermission('webhooks:manage');
45
+ const navigate = useNavigate();
46
+ const { data, isPending, isError, refetch } = useWebhookEndpoints(canRead);
47
+ if (!canRead) {
48
+ return (_jsxs(_Fragment, { children: [_jsx(PageTopBar, { icon: Webhook, crumbs: [
49
+ {
50
+ key: 'webhooks',
51
+ label: intl.formatMessage(messages.title)
52
+ }
53
+ ] }), _jsxs(Container, { children: [_jsx(ContainerHeader, { title: intl.formatMessage(messages.title) }), _jsx(WebhooksNoAccess, {})] })] }));
54
+ }
55
+ return (_jsxs(_Fragment, { children: [_jsx(PageTopBar, { icon: Webhook, crumbs: [
56
+ {
57
+ key: 'webhooks',
58
+ label: intl.formatMessage(messages.title)
59
+ }
60
+ ] }), _jsxs(Container, { children: [_jsx(ContainerHeader, { title: intl.formatMessage(messages.title), subtitle: intl.formatMessage(messages.subtitle), actions: canManage && (data?.length ?? 0) > 0 ? (_jsxs(Button, { onClick: () => navigate('/webhooks/new'), children: [_jsx(Plus, {}), intl.formatMessage(messages.create)] })) : undefined }), isPending ? _jsx(WebhooksSkeleton, {}) : null, isError ? (_jsx(Alert, { variant: "destructive", role: "alert", className: "mt-4", children: _jsxs(AlertDescription, { className: "flex items-center justify-between gap-4", children: [intl.formatMessage(messages.error), _jsx(Button, { size: "sm", variant: "outline", onClick: () => void refetch(), children: intl.formatMessage(messages.retry) })] }) })) : null, data && data.length === 0 ? (_jsx(WebhooksEmpty, { onCreate: canManage
61
+ ? () => navigate('/webhooks/new')
62
+ : undefined })) : null, data && data.length > 0 ? (_jsxs(_Fragment, { children: [_jsx(WebhooksTable, { endpoints: data }), _jsx("p", { className: "mt-3 text-sm text-muted-foreground", children: intl.formatMessage(messages.count, {
63
+ count: data.length
64
+ }) })] })) : null] })] }));
65
+ }
@@ -0,0 +1,35 @@
1
+ import type { AdminPlugin } from '@orthacms/bootstrap-admin';
2
+ /**
3
+ * Admin-side webhooks plugin shape. A thin alias of {@link AdminPlugin}, kept
4
+ * named so future config has a home.
5
+ */
6
+ export type WebhooksAdminPlugin = AdminPlugin;
7
+ /**
8
+ * Creates the admin-side webhooks plugin.
9
+ *
10
+ * It owns the global webhook-management feature — `/webhooks`,
11
+ * `/webhooks/new`, `/webhooks/:id` and `/webhooks/:id/edit` — rendered inside
12
+ * the shell's authenticated layout, plus an
13
+ * entry in the **global** sidebar's `directory` group alongside Workspaces,
14
+ * Members and API tokens — reachable without selecting a workspace, because an
15
+ * endpoint is not scoped to one.
16
+ *
17
+ * The nav row and both pages gate on `webhooks:read`, which is
18
+ * administrator-only: the rows hold a signing secret and reach across every
19
+ * workspace they name.
20
+ *
21
+ * @example
22
+ * ```typescript
23
+ * createAdmin({
24
+ * plugins: [
25
+ * IdentityPlugin(),
26
+ * ShellPlugin(),
27
+ * WorkspacesPlugin(),
28
+ * ApiTokensPlugin(),
29
+ * WebhooksPlugin(),
30
+ * ],
31
+ * });
32
+ * ```
33
+ */
34
+ export declare function WebhooksPlugin(): WebhooksAdminPlugin;
35
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/utils/webhooksPlugin/index.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AA0B7D;;;GAGG;AACH,MAAM,MAAM,mBAAmB,GAAG,WAAW,CAAC;AAE9C;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,cAAc,IAAI,mBAAmB,CA2DpD"}