@chrysb/alphaclaw 0.8.1-beta.0 → 0.8.1-beta.2
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/lib/public/js/components/routes/webhooks-route.js +1 -1
- package/lib/public/js/components/webhooks/create-webhook-modal/index.js +176 -0
- package/lib/public/js/components/webhooks/helpers.js +106 -0
- package/lib/public/js/components/webhooks/index.js +148 -0
- package/lib/public/js/components/webhooks/request-history/index.js +241 -0
- package/lib/public/js/components/webhooks/request-history/use-request-history.js +167 -0
- package/lib/public/js/components/webhooks/webhook-detail/index.js +386 -0
- package/lib/public/js/components/webhooks/webhook-detail/use-webhook-detail.js +277 -0
- package/lib/public/js/components/webhooks/webhook-list/index.js +96 -0
- package/lib/public/js/components/webhooks/webhook-list/use-webhook-list.js +30 -0
- package/lib/public/js/lib/api.js +35 -1
- package/lib/server/db/webhooks/index.js +144 -0
- package/lib/server/db/webhooks/schema.js +13 -0
- package/lib/server/init/register-server-routes.js +19 -0
- package/lib/server/oauth-callback-middleware.js +34 -0
- package/lib/server/routes/proxy.js +2 -0
- package/lib/server/routes/webhooks.js +126 -18
- package/lib/server/webhook-middleware.js +6 -1
- package/lib/server.js +12 -0
- package/package.json +1 -1
- package/lib/public/js/components/webhooks.js +0 -1259
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { useCallback, useMemo, useState } from "https://esm.sh/preact/hooks";
|
|
2
|
+
import {
|
|
3
|
+
fetchWebhookRequest,
|
|
4
|
+
fetchWebhookRequests,
|
|
5
|
+
} from "../../../lib/api.js";
|
|
6
|
+
import { usePolling } from "../../../hooks/usePolling.js";
|
|
7
|
+
import { showToast } from "../../toast.js";
|
|
8
|
+
|
|
9
|
+
export const useRequestHistory = ({
|
|
10
|
+
selectedHookName = "",
|
|
11
|
+
effectiveAuthMode = "headers",
|
|
12
|
+
webhookUrl = "",
|
|
13
|
+
webhookUrlWithQueryToken = "",
|
|
14
|
+
bearerTokenValue = "",
|
|
15
|
+
}) => {
|
|
16
|
+
const [statusFilter, setStatusFilter] = useState("all");
|
|
17
|
+
const [expandedRows, setExpandedRows] = useState(() => new Set());
|
|
18
|
+
const [replayingRequestId, setReplayingRequestId] = useState(null);
|
|
19
|
+
const [debugLoadingRequestId, setDebugLoadingRequestId] = useState(null);
|
|
20
|
+
const [debugRequest, setDebugRequest] = useState(null);
|
|
21
|
+
|
|
22
|
+
const requestsPoll = usePolling(
|
|
23
|
+
async () => {
|
|
24
|
+
if (!selectedHookName) return { requests: [] };
|
|
25
|
+
const data = await fetchWebhookRequests(selectedHookName, {
|
|
26
|
+
limit: 25,
|
|
27
|
+
offset: 0,
|
|
28
|
+
status: statusFilter,
|
|
29
|
+
});
|
|
30
|
+
return data;
|
|
31
|
+
},
|
|
32
|
+
5000,
|
|
33
|
+
{ enabled: !!selectedHookName },
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const requests = requestsPoll.data?.requests || [];
|
|
37
|
+
|
|
38
|
+
const handleRequestRowToggle = useCallback((id, isOpen) => {
|
|
39
|
+
setExpandedRows((prev) => {
|
|
40
|
+
const next = new Set(prev);
|
|
41
|
+
if (isOpen) next.add(id);
|
|
42
|
+
else next.delete(id);
|
|
43
|
+
return next;
|
|
44
|
+
});
|
|
45
|
+
}, []);
|
|
46
|
+
|
|
47
|
+
const handleSetStatusFilter = useCallback(
|
|
48
|
+
(filter) => {
|
|
49
|
+
setStatusFilter(filter);
|
|
50
|
+
setExpandedRows(new Set());
|
|
51
|
+
setTimeout(() => requestsPoll.refresh(), 0);
|
|
52
|
+
},
|
|
53
|
+
[requestsPoll.refresh],
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const resetState = useCallback(() => {
|
|
57
|
+
setStatusFilter("all");
|
|
58
|
+
setExpandedRows(new Set());
|
|
59
|
+
setDebugRequest(null);
|
|
60
|
+
setDebugLoadingRequestId(null);
|
|
61
|
+
setReplayingRequestId(null);
|
|
62
|
+
}, []);
|
|
63
|
+
|
|
64
|
+
const handleCopyRequestField = useCallback(async (value, label) => {
|
|
65
|
+
try {
|
|
66
|
+
await navigator.clipboard.writeText(String(value || ""));
|
|
67
|
+
showToast(`${label} copied`, "success");
|
|
68
|
+
} catch {
|
|
69
|
+
showToast(
|
|
70
|
+
`Could not copy ${String(label || "value").toLowerCase()}`,
|
|
71
|
+
"error",
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
}, []);
|
|
75
|
+
|
|
76
|
+
const requestUrl = useMemo(() => {
|
|
77
|
+
return effectiveAuthMode === "query" ? webhookUrlWithQueryToken : webhookUrl;
|
|
78
|
+
}, [effectiveAuthMode, webhookUrl, webhookUrlWithQueryToken]);
|
|
79
|
+
|
|
80
|
+
const requestHeaders = useMemo(() => {
|
|
81
|
+
const headers = { "Content-Type": "application/json" };
|
|
82
|
+
if (effectiveAuthMode === "headers") {
|
|
83
|
+
headers.Authorization = bearerTokenValue;
|
|
84
|
+
}
|
|
85
|
+
return headers;
|
|
86
|
+
}, [bearerTokenValue, effectiveAuthMode]);
|
|
87
|
+
|
|
88
|
+
const handleReplayRequest = useCallback(
|
|
89
|
+
async (item) => {
|
|
90
|
+
if (!item || replayingRequestId === item.id) return;
|
|
91
|
+
if (item.payloadTruncated) {
|
|
92
|
+
showToast("Cannot replay a truncated payload", "warning");
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
setReplayingRequestId(item.id);
|
|
96
|
+
try {
|
|
97
|
+
const response = await fetch(requestUrl, {
|
|
98
|
+
method: "POST",
|
|
99
|
+
headers: requestHeaders,
|
|
100
|
+
body: String(item.payload || ""),
|
|
101
|
+
});
|
|
102
|
+
const bodyText = await response.text();
|
|
103
|
+
let body = null;
|
|
104
|
+
try {
|
|
105
|
+
body = bodyText ? JSON.parse(bodyText) : null;
|
|
106
|
+
} catch {
|
|
107
|
+
body = null;
|
|
108
|
+
}
|
|
109
|
+
const errorMessage =
|
|
110
|
+
body?.ok === false
|
|
111
|
+
? body?.error || "Webhook rejected"
|
|
112
|
+
: !response.ok
|
|
113
|
+
? body?.error || bodyText || `HTTP ${response.status}`
|
|
114
|
+
: "";
|
|
115
|
+
if (errorMessage) {
|
|
116
|
+
showToast(`Replay failed: ${errorMessage}`, "error");
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
showToast("Request replayed", "success");
|
|
120
|
+
setTimeout(() => requestsPoll.refresh(), 0);
|
|
121
|
+
} catch (err) {
|
|
122
|
+
showToast(err.message || "Could not replay request", "error");
|
|
123
|
+
} finally {
|
|
124
|
+
setReplayingRequestId(null);
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
[replayingRequestId, requestHeaders, requestUrl, requestsPoll.refresh],
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
const handleAskAgentToDebug = useCallback(
|
|
131
|
+
async (item) => {
|
|
132
|
+
if (!selectedHookName || !item?.id || debugLoadingRequestId === item.id)
|
|
133
|
+
return;
|
|
134
|
+
try {
|
|
135
|
+
setDebugLoadingRequestId(item.id);
|
|
136
|
+
const data = await fetchWebhookRequest(selectedHookName, item.id);
|
|
137
|
+
setDebugRequest(data?.request || item);
|
|
138
|
+
} catch (err) {
|
|
139
|
+
showToast(err.message || "Could not load webhook request details", "error");
|
|
140
|
+
} finally {
|
|
141
|
+
setDebugLoadingRequestId(null);
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
[debugLoadingRequestId, selectedHookName],
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
state: {
|
|
149
|
+
requests,
|
|
150
|
+
statusFilter,
|
|
151
|
+
expandedRows,
|
|
152
|
+
replayingRequestId,
|
|
153
|
+
debugLoadingRequestId,
|
|
154
|
+
debugRequest,
|
|
155
|
+
},
|
|
156
|
+
actions: {
|
|
157
|
+
refreshRequests: requestsPoll.refresh,
|
|
158
|
+
handleRequestRowToggle,
|
|
159
|
+
handleSetStatusFilter,
|
|
160
|
+
handleReplayRequest,
|
|
161
|
+
handleCopyRequestField,
|
|
162
|
+
handleAskAgentToDebug,
|
|
163
|
+
setDebugRequest,
|
|
164
|
+
resetState,
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
};
|
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
import { h } from "https://esm.sh/preact";
|
|
2
|
+
import htm from "https://esm.sh/htm";
|
|
3
|
+
import { ActionButton } from "../../action-button.js";
|
|
4
|
+
import { Badge } from "../../badge.js";
|
|
5
|
+
import { ConfirmDialog } from "../../confirm-dialog.js";
|
|
6
|
+
import { showToast } from "../../toast.js";
|
|
7
|
+
import { formatDateTime } from "../helpers.js";
|
|
8
|
+
import { RequestHistory } from "../request-history/index.js";
|
|
9
|
+
import { useWebhookDetail } from "./use-webhook-detail.js";
|
|
10
|
+
|
|
11
|
+
const html = htm.bind(h);
|
|
12
|
+
|
|
13
|
+
export const WebhookDetail = ({
|
|
14
|
+
selectedHookName = "",
|
|
15
|
+
onBackToList = () => {},
|
|
16
|
+
onRestartRequired = () => {},
|
|
17
|
+
onOpenFile = () => {},
|
|
18
|
+
}) => {
|
|
19
|
+
const { state, actions } = useWebhookDetail({
|
|
20
|
+
selectedHookName,
|
|
21
|
+
onBackToList,
|
|
22
|
+
onRestartRequired,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const {
|
|
26
|
+
authMode,
|
|
27
|
+
selectedWebhook,
|
|
28
|
+
isWebhookLoading,
|
|
29
|
+
webhookLoadError,
|
|
30
|
+
selectedWebhookManaged,
|
|
31
|
+
selectedDeliveryAgentName,
|
|
32
|
+
selectedDeliveryChannel,
|
|
33
|
+
webhookUrl,
|
|
34
|
+
oauthCallbackUrl,
|
|
35
|
+
hasOauthCallback,
|
|
36
|
+
webhookUrlWithQueryToken,
|
|
37
|
+
authHeaderValue,
|
|
38
|
+
bearerTokenValue,
|
|
39
|
+
effectiveAuthMode,
|
|
40
|
+
activeCurlCommand,
|
|
41
|
+
deleting,
|
|
42
|
+
showDeleteConfirm,
|
|
43
|
+
deleteTransformDir,
|
|
44
|
+
sendingTestWebhook,
|
|
45
|
+
rotatingOauthCallback,
|
|
46
|
+
showRotateOauthConfirm,
|
|
47
|
+
} = state;
|
|
48
|
+
|
|
49
|
+
return html`
|
|
50
|
+
<div class="space-y-4">
|
|
51
|
+
<div class="bg-surface border border-border rounded-xl p-4 space-y-4">
|
|
52
|
+
<div>
|
|
53
|
+
<h2 class="font-semibold text-sm">
|
|
54
|
+
${selectedWebhook?.path || `/hooks/${selectedHookName}`}
|
|
55
|
+
</h2>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
${isWebhookLoading
|
|
59
|
+
? html`<div class="bg-black/20 border border-border rounded-lg p-3">
|
|
60
|
+
<p class="text-xs text-gray-500">Loading webhook details...</p>
|
|
61
|
+
</div>`
|
|
62
|
+
: webhookLoadError
|
|
63
|
+
? html`<div class="bg-black/20 border border-border rounded-lg p-3">
|
|
64
|
+
<p class="text-xs text-red-300">
|
|
65
|
+
${webhookLoadError?.message || "Could not load webhook details"}
|
|
66
|
+
</p>
|
|
67
|
+
</div>`
|
|
68
|
+
: hasOauthCallback
|
|
69
|
+
? null
|
|
70
|
+
: html`<div class="bg-black/20 border border-border rounded-lg p-3 space-y-4">
|
|
71
|
+
${selectedWebhookManaged
|
|
72
|
+
? null
|
|
73
|
+
: html`
|
|
74
|
+
<div class="space-y-2">
|
|
75
|
+
<p class="text-xs text-gray-500">Auth mode</p>
|
|
76
|
+
<div class="flex items-center gap-2">
|
|
77
|
+
<button
|
|
78
|
+
class="text-xs px-2 py-1 rounded border transition-colors ${authMode ===
|
|
79
|
+
"headers"
|
|
80
|
+
? "border-cyan-400 text-cyan-200 bg-cyan-400/10"
|
|
81
|
+
: "border-border text-gray-400 hover:text-gray-200"}"
|
|
82
|
+
onclick=${() => actions.setAuthMode("headers")}
|
|
83
|
+
>
|
|
84
|
+
Headers
|
|
85
|
+
</button>
|
|
86
|
+
<button
|
|
87
|
+
class="text-xs px-2 py-1 rounded border transition-colors ${authMode ===
|
|
88
|
+
"query"
|
|
89
|
+
? "border-cyan-400 text-cyan-200 bg-cyan-400/10"
|
|
90
|
+
: "border-border text-gray-400 hover:text-gray-200"}"
|
|
91
|
+
onclick=${() => actions.setAuthMode("query")}
|
|
92
|
+
>
|
|
93
|
+
Query string
|
|
94
|
+
</button>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
`}
|
|
98
|
+
<div class="space-y-2">
|
|
99
|
+
<p class="text-xs text-gray-500">Webhook URL</p>
|
|
100
|
+
<div class="flex items-center gap-2">
|
|
101
|
+
<input
|
|
102
|
+
type="text"
|
|
103
|
+
readonly
|
|
104
|
+
value=${effectiveAuthMode === "query"
|
|
105
|
+
? webhookUrlWithQueryToken
|
|
106
|
+
: webhookUrl}
|
|
107
|
+
class="h-8 flex-1 bg-black/30 border border-border rounded-lg px-3 text-xs text-gray-200 outline-none font-mono"
|
|
108
|
+
/>
|
|
109
|
+
<button
|
|
110
|
+
class="h-8 text-xs px-2.5 rounded-lg ac-btn-secondary shrink-0"
|
|
111
|
+
onclick=${async () => {
|
|
112
|
+
try {
|
|
113
|
+
await navigator.clipboard.writeText(
|
|
114
|
+
effectiveAuthMode === "query"
|
|
115
|
+
? webhookUrlWithQueryToken
|
|
116
|
+
: webhookUrl,
|
|
117
|
+
);
|
|
118
|
+
showToast("Webhook URL copied", "success");
|
|
119
|
+
} catch {
|
|
120
|
+
showToast("Could not copy URL", "error");
|
|
121
|
+
}
|
|
122
|
+
}}
|
|
123
|
+
>
|
|
124
|
+
Copy
|
|
125
|
+
</button>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
${selectedWebhookManaged
|
|
129
|
+
? null
|
|
130
|
+
: effectiveAuthMode === "headers"
|
|
131
|
+
? html`
|
|
132
|
+
<div class="space-y-2">
|
|
133
|
+
<p class="text-xs text-gray-500">Auth headers</p>
|
|
134
|
+
<div class="flex items-center gap-2">
|
|
135
|
+
<input
|
|
136
|
+
type="text"
|
|
137
|
+
readonly
|
|
138
|
+
value=${authHeaderValue}
|
|
139
|
+
class="h-8 flex-1 bg-black/30 border border-border rounded-lg px-3 text-xs text-gray-200 outline-none font-mono"
|
|
140
|
+
/>
|
|
141
|
+
<button
|
|
142
|
+
class="h-8 text-xs px-2.5 rounded-lg ac-btn-secondary shrink-0"
|
|
143
|
+
onclick=${async () => {
|
|
144
|
+
try {
|
|
145
|
+
await navigator.clipboard.writeText(
|
|
146
|
+
bearerTokenValue,
|
|
147
|
+
);
|
|
148
|
+
showToast("Bearer token copied", "success");
|
|
149
|
+
} catch {
|
|
150
|
+
showToast("Could not copy bearer token", "error");
|
|
151
|
+
}
|
|
152
|
+
}}
|
|
153
|
+
>
|
|
154
|
+
Copy
|
|
155
|
+
</button>
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
`
|
|
159
|
+
: html`
|
|
160
|
+
<p class="text-xs text-yellow-300">
|
|
161
|
+
Always use auth headers when possible. Query string is
|
|
162
|
+
less secure.
|
|
163
|
+
</p>
|
|
164
|
+
`}
|
|
165
|
+
</div>`}
|
|
166
|
+
|
|
167
|
+
${isWebhookLoading || webhookLoadError || selectedWebhookManaged || !hasOauthCallback
|
|
168
|
+
? null
|
|
169
|
+
: html`
|
|
170
|
+
<div class="bg-black/20 border border-border rounded-lg p-3 space-y-2">
|
|
171
|
+
<div class="flex items-center gap-2">
|
|
172
|
+
<p class="text-xs text-gray-500">OAuth Callback URL</p>
|
|
173
|
+
${hasOauthCallback
|
|
174
|
+
? html`<${Badge} tone="neutral">OAuth alias</${Badge}>`
|
|
175
|
+
: null}
|
|
176
|
+
</div>
|
|
177
|
+
<div class="flex items-center gap-2">
|
|
178
|
+
<input
|
|
179
|
+
type="text"
|
|
180
|
+
readonly
|
|
181
|
+
value=${hasOauthCallback ? oauthCallbackUrl : "Not enabled"}
|
|
182
|
+
class="h-8 flex-1 bg-black/30 border border-border rounded-lg px-3 text-xs text-gray-200 outline-none font-mono"
|
|
183
|
+
/>
|
|
184
|
+
${hasOauthCallback
|
|
185
|
+
? html`
|
|
186
|
+
<button
|
|
187
|
+
class="h-8 text-xs px-2.5 rounded-lg ac-btn-secondary shrink-0"
|
|
188
|
+
onclick=${async () => {
|
|
189
|
+
try {
|
|
190
|
+
await navigator.clipboard.writeText(
|
|
191
|
+
oauthCallbackUrl,
|
|
192
|
+
);
|
|
193
|
+
showToast("OAuth callback URL copied", "success");
|
|
194
|
+
} catch {
|
|
195
|
+
showToast("Could not copy URL", "error");
|
|
196
|
+
}
|
|
197
|
+
}}
|
|
198
|
+
>
|
|
199
|
+
Copy
|
|
200
|
+
</button>
|
|
201
|
+
`
|
|
202
|
+
: null}
|
|
203
|
+
</div>
|
|
204
|
+
<div class="flex items-center justify-start gap-3 flex-wrap">
|
|
205
|
+
<div class="flex items-center gap-2">
|
|
206
|
+
<button
|
|
207
|
+
class="h-8 text-xs px-2.5 rounded-lg ac-btn-secondary disabled:opacity-60"
|
|
208
|
+
onclick=${() => {
|
|
209
|
+
if (rotatingOauthCallback) return;
|
|
210
|
+
actions.setShowRotateOauthConfirm(true);
|
|
211
|
+
}}
|
|
212
|
+
disabled=${rotatingOauthCallback}
|
|
213
|
+
>
|
|
214
|
+
${rotatingOauthCallback ? "Rotating..." : "Rotate"}
|
|
215
|
+
</button>
|
|
216
|
+
</div>
|
|
217
|
+
<p class="text-xs text-yellow-300">
|
|
218
|
+
Keep this URL private. Rotate if exposed.
|
|
219
|
+
</p>
|
|
220
|
+
</div>
|
|
221
|
+
</div>
|
|
222
|
+
`}
|
|
223
|
+
|
|
224
|
+
<div class="bg-black/20 border border-border rounded-lg p-3 space-y-2">
|
|
225
|
+
<p class="text-xs text-gray-500">Deliver to</p>
|
|
226
|
+
<p class="text-xs text-gray-200 font-mono ">
|
|
227
|
+
${selectedDeliveryAgentName}${" "}
|
|
228
|
+
<span class="text-xs text-gray-500 font-mono"
|
|
229
|
+
>(${selectedDeliveryChannel})</span
|
|
230
|
+
>
|
|
231
|
+
</p>
|
|
232
|
+
</div>
|
|
233
|
+
|
|
234
|
+
<div class="bg-black/20 border border-border rounded-lg p-3 space-y-2">
|
|
235
|
+
<p class="text-xs text-gray-500">Test webhook</p>
|
|
236
|
+
<div class="flex flex-col gap-2 sm:flex-row sm:items-center">
|
|
237
|
+
<input
|
|
238
|
+
type="text"
|
|
239
|
+
readonly
|
|
240
|
+
value=${activeCurlCommand}
|
|
241
|
+
class="h-8 w-full sm:flex-1 sm:min-w-0 bg-black/30 border border-border rounded-lg px-3 text-xs text-gray-200 outline-none font-mono overflow-x-auto scrollbar-hidden"
|
|
242
|
+
/>
|
|
243
|
+
<div class="grid grid-cols-2 gap-2 w-full sm:w-auto sm:flex sm:items-center">
|
|
244
|
+
<button
|
|
245
|
+
class="h-8 text-xs px-2.5 rounded-lg ac-btn-secondary w-full sm:w-auto sm:shrink-0"
|
|
246
|
+
onclick=${async () => {
|
|
247
|
+
try {
|
|
248
|
+
await navigator.clipboard.writeText(activeCurlCommand);
|
|
249
|
+
showToast("curl command copied", "success");
|
|
250
|
+
} catch {
|
|
251
|
+
showToast("Could not copy curl command", "error");
|
|
252
|
+
}
|
|
253
|
+
}}
|
|
254
|
+
>
|
|
255
|
+
Copy
|
|
256
|
+
</button>
|
|
257
|
+
<button
|
|
258
|
+
class="h-8 text-xs px-2.5 rounded-lg ac-btn-secondary w-full sm:w-auto sm:shrink-0 disabled:opacity-60"
|
|
259
|
+
onclick=${actions.handleSendTestWebhook}
|
|
260
|
+
disabled=${sendingTestWebhook}
|
|
261
|
+
>
|
|
262
|
+
${sendingTestWebhook ? "Sending..." : "Send"}
|
|
263
|
+
</button>
|
|
264
|
+
</div>
|
|
265
|
+
</div>
|
|
266
|
+
</div>
|
|
267
|
+
|
|
268
|
+
<div class="bg-black/20 border border-border rounded-lg p-3">
|
|
269
|
+
<div class="flex items-center gap-2 text-xs text-gray-300">
|
|
270
|
+
<span class="text-gray-500">Transform:</span>
|
|
271
|
+
${selectedWebhook?.transformPath
|
|
272
|
+
? html`<button
|
|
273
|
+
type="button"
|
|
274
|
+
class="ac-tip-link flex-1 min-w-0 truncate block text-left font-mono"
|
|
275
|
+
title=${selectedWebhook.transformPath}
|
|
276
|
+
onclick=${() => onOpenFile(selectedWebhook.transformPath)}
|
|
277
|
+
>
|
|
278
|
+
${selectedWebhook.transformPath}
|
|
279
|
+
</button>`
|
|
280
|
+
: html`<code class="flex-1 min-w-0 truncate block">—</code>`}
|
|
281
|
+
<span
|
|
282
|
+
class=${`ml-auto inline-flex items-center gap-1 px-1.5 py-0.5 rounded border font-sans ${
|
|
283
|
+
selectedWebhook?.transformExists
|
|
284
|
+
? "border-green-500/30 text-green-300 bg-green-500/10"
|
|
285
|
+
: "border-yellow-500/30 text-yellow-300 bg-yellow-500/10"
|
|
286
|
+
}`}
|
|
287
|
+
>
|
|
288
|
+
<span class="font-sans text-sm leading-none">
|
|
289
|
+
${selectedWebhook?.transformExists ? "✓" : "!"}
|
|
290
|
+
</span>
|
|
291
|
+
${selectedWebhook?.transformExists ? null : html`<span>missing</span>`}
|
|
292
|
+
</span>
|
|
293
|
+
</div>
|
|
294
|
+
</div>
|
|
295
|
+
|
|
296
|
+
<div class="flex items-center justify-between gap-3">
|
|
297
|
+
<p class="text-xs text-gray-600">
|
|
298
|
+
Created: ${formatDateTime(selectedWebhook?.createdAt)}
|
|
299
|
+
</p>
|
|
300
|
+
${selectedWebhookManaged
|
|
301
|
+
? null
|
|
302
|
+
: html`<${ActionButton}
|
|
303
|
+
onClick=${() => {
|
|
304
|
+
if (deleting) return;
|
|
305
|
+
actions.setDeleteTransformDir(true);
|
|
306
|
+
actions.setShowDeleteConfirm(true);
|
|
307
|
+
}}
|
|
308
|
+
disabled=${deleting}
|
|
309
|
+
loading=${deleting}
|
|
310
|
+
tone="danger"
|
|
311
|
+
size="sm"
|
|
312
|
+
idleLabel="Delete"
|
|
313
|
+
loadingLabel="Deleting..."
|
|
314
|
+
className="shrink-0 px-2.5 py-1"
|
|
315
|
+
/>`}
|
|
316
|
+
</div>
|
|
317
|
+
</div>
|
|
318
|
+
|
|
319
|
+
${selectedWebhookManaged && !isWebhookLoading && !webhookLoadError
|
|
320
|
+
? html`
|
|
321
|
+
<div class="rounded-lg border border-yellow-500/30 bg-yellow-500/10 p-3">
|
|
322
|
+
<p class="text-xs text-yellow-200">
|
|
323
|
+
This webhook is managed by Gmail Watch setup and cannot be
|
|
324
|
+
deleted or edited from this page.
|
|
325
|
+
</p>
|
|
326
|
+
</div>
|
|
327
|
+
`
|
|
328
|
+
: null}
|
|
329
|
+
<${RequestHistory}
|
|
330
|
+
selectedHookName=${selectedHookName}
|
|
331
|
+
selectedWebhook=${selectedWebhook}
|
|
332
|
+
effectiveAuthMode=${effectiveAuthMode}
|
|
333
|
+
webhookUrl=${webhookUrl}
|
|
334
|
+
webhookUrlWithQueryToken=${webhookUrlWithQueryToken}
|
|
335
|
+
bearerTokenValue=${bearerTokenValue}
|
|
336
|
+
/>
|
|
337
|
+
<${ConfirmDialog}
|
|
338
|
+
visible=${showRotateOauthConfirm &&
|
|
339
|
+
!!selectedHookName &&
|
|
340
|
+
!selectedWebhookManaged &&
|
|
341
|
+
hasOauthCallback}
|
|
342
|
+
title="Rotate OAuth callback?"
|
|
343
|
+
message="Rotating will generate a new callback URL and immediately invalidate the current URL."
|
|
344
|
+
confirmLabel="Rotate callback URL"
|
|
345
|
+
confirmLoadingLabel="Rotating..."
|
|
346
|
+
confirmLoading=${rotatingOauthCallback}
|
|
347
|
+
cancelLabel="Cancel"
|
|
348
|
+
onCancel=${() => {
|
|
349
|
+
if (rotatingOauthCallback) return;
|
|
350
|
+
actions.setShowRotateOauthConfirm(false);
|
|
351
|
+
}}
|
|
352
|
+
onConfirm=${actions.handleRotateOauthCallback}
|
|
353
|
+
/>
|
|
354
|
+
<${ConfirmDialog}
|
|
355
|
+
visible=${showDeleteConfirm &&
|
|
356
|
+
!!selectedHookName &&
|
|
357
|
+
!selectedWebhookManaged}
|
|
358
|
+
title="Delete webhook?"
|
|
359
|
+
message=${`This removes "/hooks/${selectedHookName}" from openclaw.json.`}
|
|
360
|
+
details=${html`
|
|
361
|
+
<div class="rounded-lg border border-border bg-black/20 p-3">
|
|
362
|
+
<label class="flex items-center gap-2 text-xs text-gray-300 select-none">
|
|
363
|
+
<input
|
|
364
|
+
type="checkbox"
|
|
365
|
+
checked=${deleteTransformDir}
|
|
366
|
+
onInput=${(event) =>
|
|
367
|
+
actions.setDeleteTransformDir(!!event.target.checked)}
|
|
368
|
+
/>
|
|
369
|
+
Also delete <code>hooks/transforms/${selectedHookName}</code>
|
|
370
|
+
</label>
|
|
371
|
+
</div>
|
|
372
|
+
`}
|
|
373
|
+
confirmLabel="Delete webhook"
|
|
374
|
+
confirmLoadingLabel="Deleting..."
|
|
375
|
+
confirmLoading=${deleting}
|
|
376
|
+
cancelLabel="Cancel"
|
|
377
|
+
onCancel=${() => {
|
|
378
|
+
if (deleting) return;
|
|
379
|
+
actions.setDeleteTransformDir(true);
|
|
380
|
+
actions.setShowDeleteConfirm(false);
|
|
381
|
+
}}
|
|
382
|
+
onConfirm=${actions.handleDeleteConfirmed}
|
|
383
|
+
/>
|
|
384
|
+
</div>
|
|
385
|
+
`;
|
|
386
|
+
};
|