datocms-plugin-record-bin 2.0.0 → 3.0.1
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/README.md +127 -11
- package/build/assets/index-BnrW9Ts8.js +15 -0
- package/build/assets/index-aWCW2c0n.css +1 -0
- package/build/index.html +13 -1
- package/index.html +12 -0
- package/package.json +24 -18
- package/src/entrypoints/BinOutlet.tsx +262 -37
- package/src/entrypoints/ConfigScreen.tsx +939 -38
- package/src/entrypoints/ErrorModal.tsx +86 -2
- package/src/index.tsx +73 -28
- package/src/react-app-env.d.ts +1 -1
- package/src/types/types.ts +36 -8
- package/src/utils/binCleanup.test.ts +107 -0
- package/src/utils/binCleanup.ts +71 -23
- package/src/utils/debugLogger.ts +27 -0
- package/src/utils/deployProviders.test.ts +33 -0
- package/src/utils/deployProviders.ts +28 -0
- package/src/utils/getDeploymentUrlFromParameters.test.ts +26 -0
- package/src/utils/getDeploymentUrlFromParameters.ts +21 -0
- package/src/utils/getRuntimeMode.test.ts +57 -0
- package/src/utils/getRuntimeMode.ts +23 -0
- package/src/utils/lambdaLessCapture.test.ts +218 -0
- package/src/utils/lambdaLessCapture.ts +160 -0
- package/src/utils/lambdaLessCleanup.test.ts +125 -0
- package/src/utils/lambdaLessCleanup.ts +69 -0
- package/src/utils/lambdaLessRestore.test.ts +248 -0
- package/src/utils/lambdaLessRestore.ts +159 -0
- package/src/utils/recordBinModel.ts +108 -0
- package/src/utils/recordBinPayload.test.ts +103 -0
- package/src/utils/recordBinPayload.ts +136 -0
- package/src/utils/recordBinWebhook.test.ts +253 -0
- package/src/utils/recordBinWebhook.ts +305 -0
- package/src/utils/render.tsx +17 -8
- package/src/utils/restoreError.test.ts +112 -0
- package/src/utils/restoreError.ts +221 -0
- package/src/utils/verifyLambdaHealth.test.ts +248 -0
- package/src/utils/verifyLambdaHealth.ts +422 -0
- package/vite.config.ts +11 -0
- package/build/asset-manifest.json +0 -13
- package/build/static/css/main.10f29737.css +0 -2
- package/build/static/css/main.10f29737.css.map +0 -1
- package/build/static/js/main.53795e3b.js +0 -3
- package/build/static/js/main.53795e3b.js.LICENSE.txt +0 -47
- package/build/static/js/main.53795e3b.js.map +0 -1
- package/src/entrypoints/InstallationModal.tsx +0 -107
- package/src/entrypoints/PreInstallConfig.tsx +0 -28
- package/src/utils/attemptVercelInitialization.ts +0 -16
|
@@ -1,64 +1,965 @@
|
|
|
1
1
|
import { RenderConfigScreenCtx } from "datocms-plugin-sdk";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
import {
|
|
3
|
+
Button,
|
|
4
|
+
Canvas,
|
|
5
|
+
Dropdown,
|
|
6
|
+
DropdownMenu,
|
|
7
|
+
DropdownOption,
|
|
8
|
+
Form,
|
|
9
|
+
Section,
|
|
10
|
+
SwitchField,
|
|
11
|
+
TextField,
|
|
12
|
+
} from "datocms-react-ui";
|
|
13
|
+
import { CSSProperties, useEffect, useState } from "react";
|
|
14
|
+
import {
|
|
15
|
+
automaticBinCleanupObject,
|
|
16
|
+
LambdaConnectionState,
|
|
17
|
+
} from "../types/types";
|
|
18
|
+
import {
|
|
19
|
+
DEPLOY_PROVIDER_OPTIONS,
|
|
20
|
+
DeployProvider,
|
|
21
|
+
PLUGIN_README_URL,
|
|
22
|
+
} from "../utils/deployProviders";
|
|
23
|
+
import { createDebugLogger, isDebugEnabled } from "../utils/debugLogger";
|
|
24
|
+
import { getDeploymentUrlFromParameters } from "../utils/getDeploymentUrlFromParameters";
|
|
25
|
+
import { getRuntimeMode, RuntimeMode } from "../utils/getRuntimeMode";
|
|
26
|
+
import {
|
|
27
|
+
ensureRecordBinWebhook,
|
|
28
|
+
getRecordBinWebhookSyncErrorDetails,
|
|
29
|
+
isRecordBinWebhookSyncError,
|
|
30
|
+
removeRecordBinWebhook,
|
|
31
|
+
RecordBinWebhookSyncError,
|
|
32
|
+
} from "../utils/recordBinWebhook";
|
|
33
|
+
import {
|
|
34
|
+
buildConnectedLambdaConnectionState,
|
|
35
|
+
buildDisconnectedLambdaConnectionState,
|
|
36
|
+
getLambdaConnectionErrorDetails,
|
|
37
|
+
LambdaHealthCheckError,
|
|
38
|
+
verifyLambdaHealth,
|
|
39
|
+
} from "../utils/verifyLambdaHealth";
|
|
40
|
+
|
|
41
|
+
const DEFAULT_CONNECTION_ERROR_SUMMARY =
|
|
42
|
+
"Could not validate the Record Bin lambda deployment.";
|
|
43
|
+
|
|
44
|
+
const getConnectionErrorSummary = (
|
|
45
|
+
connection?: LambdaConnectionState
|
|
46
|
+
): string => {
|
|
47
|
+
if (!connection || connection.status !== "disconnected") {
|
|
48
|
+
return "";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return connection.errorMessage || DEFAULT_CONNECTION_ERROR_SUMMARY;
|
|
52
|
+
};
|
|
5
53
|
|
|
6
54
|
export default function ConfigScreen({ ctx }: { ctx: RenderConfigScreenCtx }) {
|
|
55
|
+
const initialConnectionState = (ctx.plugin.attributes.parameters.lambdaConnection ??
|
|
56
|
+
undefined) as LambdaConnectionState | undefined;
|
|
57
|
+
const initialRuntimeMode = getRuntimeMode(ctx.plugin.attributes.parameters);
|
|
58
|
+
const initialDeploymentUrl = getDeploymentUrlFromParameters(
|
|
59
|
+
ctx.plugin.attributes.parameters
|
|
60
|
+
);
|
|
61
|
+
const initialDebugEnabled = isDebugEnabled(ctx.plugin.attributes.parameters);
|
|
62
|
+
const initialNumberOfDays = String(
|
|
63
|
+
(ctx.plugin.attributes.parameters?.automaticBinCleanup as automaticBinCleanupObject)
|
|
64
|
+
?.numberOfDays ?? "30"
|
|
65
|
+
);
|
|
66
|
+
const hasInitialConnectionErrorDetails =
|
|
67
|
+
initialDeploymentUrl.trim().length > 0 &&
|
|
68
|
+
initialConnectionState?.status === "disconnected" &&
|
|
69
|
+
Boolean(
|
|
70
|
+
initialConnectionState.errorCode ||
|
|
71
|
+
initialConnectionState.errorMessage ||
|
|
72
|
+
initialConnectionState.httpStatus ||
|
|
73
|
+
initialConnectionState.responseSnippet
|
|
74
|
+
);
|
|
75
|
+
|
|
7
76
|
const [numberOfDays, setNumberOfDays] = useState(
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
77
|
+
initialNumberOfDays
|
|
78
|
+
);
|
|
79
|
+
const [debugEnabled, setDebugEnabled] = useState(initialDebugEnabled);
|
|
80
|
+
const [runtimeModeSelection, setRuntimeModeSelection] = useState<RuntimeMode>(
|
|
81
|
+
initialRuntimeMode
|
|
12
82
|
);
|
|
83
|
+
const [savedFormValues, setSavedFormValues] = useState({
|
|
84
|
+
numberOfDays: initialNumberOfDays,
|
|
85
|
+
debugEnabled: initialDebugEnabled,
|
|
86
|
+
runtimeMode: initialRuntimeMode,
|
|
87
|
+
});
|
|
13
88
|
const [isLoading, setLoading] = useState(false);
|
|
89
|
+
const [isDisconnecting, setIsDisconnecting] = useState(false);
|
|
90
|
+
const [isConnecting, setIsConnecting] = useState(false);
|
|
14
91
|
const [error, setError] = useState("");
|
|
92
|
+
const [isHealthChecking, setIsHealthChecking] = useState(false);
|
|
93
|
+
const [deploymentUrlInput, setDeploymentUrlInput] = useState(
|
|
94
|
+
initialDeploymentUrl
|
|
95
|
+
);
|
|
96
|
+
const [activeDeploymentUrl, setActiveDeploymentUrl] = useState(
|
|
97
|
+
initialDeploymentUrl
|
|
98
|
+
);
|
|
99
|
+
const [connectionState, setConnectionState] = useState<
|
|
100
|
+
LambdaConnectionState | undefined
|
|
101
|
+
>(initialConnectionState);
|
|
102
|
+
const [connectionErrorSummary, setConnectionErrorSummary] = useState(
|
|
103
|
+
hasInitialConnectionErrorDetails
|
|
104
|
+
? getConnectionErrorSummary(initialConnectionState)
|
|
105
|
+
: ""
|
|
106
|
+
);
|
|
107
|
+
const [connectionErrorDetails, setConnectionErrorDetails] = useState<string[]>(
|
|
108
|
+
hasInitialConnectionErrorDetails
|
|
109
|
+
? getLambdaConnectionErrorDetails(initialConnectionState)
|
|
110
|
+
: []
|
|
111
|
+
);
|
|
112
|
+
const [showConnectionDetails, setShowConnectionDetails] = useState(false);
|
|
113
|
+
const [showAdvancedSettings, setShowAdvancedSettings] = useState(false);
|
|
114
|
+
const debugLogger = createDebugLogger(debugEnabled, "ConfigScreen");
|
|
115
|
+
|
|
116
|
+
const persistPluginParameters = async (updates: Record<string, unknown>) => {
|
|
117
|
+
await ctx.updatePluginParameters({
|
|
118
|
+
...ctx.plugin.attributes.parameters,
|
|
119
|
+
...updates,
|
|
120
|
+
});
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const clearConnectionErrorState = () => {
|
|
124
|
+
setConnectionErrorSummary("");
|
|
125
|
+
setConnectionErrorDetails([]);
|
|
126
|
+
setShowConnectionDetails(false);
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const applyDisconnectedState = (state: LambdaConnectionState) => {
|
|
130
|
+
setConnectionState(state);
|
|
131
|
+
setConnectionErrorSummary(getConnectionErrorSummary(state));
|
|
132
|
+
setConnectionErrorDetails(getLambdaConnectionErrorDetails(state));
|
|
133
|
+
setShowConnectionDetails(false);
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const applyWebhookSyncErrorState = (
|
|
137
|
+
error: RecordBinWebhookSyncError,
|
|
138
|
+
operation: "connect" | "disconnect"
|
|
139
|
+
) => {
|
|
140
|
+
setConnectionErrorSummary(error.message);
|
|
141
|
+
setConnectionErrorDetails(
|
|
142
|
+
getRecordBinWebhookSyncErrorDetails(error, operation)
|
|
143
|
+
);
|
|
144
|
+
setShowConnectionDetails(false);
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const canManageWebhooks =
|
|
148
|
+
ctx.currentRole?.meta?.final_permissions?.can_manage_webhooks === true;
|
|
149
|
+
|
|
150
|
+
useEffect(() => {
|
|
151
|
+
let isCancelled = false;
|
|
152
|
+
debugLogger.log("Config screen mounted", {
|
|
153
|
+
initialDebugEnabled,
|
|
154
|
+
hasInitialConnectionState: !!initialConnectionState,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const runHealthCheck = async () => {
|
|
158
|
+
setIsHealthChecking(true);
|
|
159
|
+
|
|
160
|
+
if (initialRuntimeMode !== "lambda") {
|
|
161
|
+
debugLogger.log(
|
|
162
|
+
"Skipping lambda health check because Lambda-full mode is not selected"
|
|
163
|
+
);
|
|
164
|
+
if (!isCancelled) {
|
|
165
|
+
setIsHealthChecking(false);
|
|
166
|
+
}
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const configuredDeploymentUrl = getDeploymentUrlFromParameters(
|
|
171
|
+
ctx.plugin.attributes.parameters
|
|
172
|
+
);
|
|
173
|
+
if (!isCancelled) {
|
|
174
|
+
setDeploymentUrlInput(configuredDeploymentUrl);
|
|
175
|
+
setActiveDeploymentUrl(configuredDeploymentUrl);
|
|
176
|
+
}
|
|
177
|
+
debugLogger.log("Running lambda health check", {
|
|
178
|
+
phase: "config_mount",
|
|
179
|
+
deploymentUrl: configuredDeploymentUrl,
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
if (!configuredDeploymentUrl.trim()) {
|
|
183
|
+
debugLogger.log(
|
|
184
|
+
"Skipping lambda health check because no deployment URL is configured"
|
|
185
|
+
);
|
|
186
|
+
if (!isCancelled) {
|
|
187
|
+
setConnectionState(undefined);
|
|
188
|
+
clearConnectionErrorState();
|
|
189
|
+
setIsHealthChecking(false);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
await persistPluginParameters({
|
|
194
|
+
lambdaConnection: null,
|
|
195
|
+
runtimeMode: runtimeModeSelection,
|
|
196
|
+
lambdaFullMode: runtimeModeSelection === "lambda",
|
|
197
|
+
});
|
|
198
|
+
debugLogger.log("Cleared lambda connection state without URL");
|
|
199
|
+
} catch (persistError) {
|
|
200
|
+
debugLogger.warn(
|
|
201
|
+
"Failed to clear lambda connection state without URL",
|
|
202
|
+
persistError
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
try {
|
|
210
|
+
const verificationResult = await verifyLambdaHealth({
|
|
211
|
+
baseUrl: configuredDeploymentUrl,
|
|
212
|
+
environment: ctx.environment,
|
|
213
|
+
phase: "config_mount",
|
|
214
|
+
debug: debugEnabled,
|
|
215
|
+
});
|
|
216
|
+
debugLogger.log("Lambda health check succeeded", verificationResult);
|
|
217
|
+
|
|
218
|
+
const connectedState = buildConnectedLambdaConnectionState(
|
|
219
|
+
verificationResult.endpoint,
|
|
220
|
+
verificationResult.checkedAt,
|
|
221
|
+
"config_mount"
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
if (!isCancelled) {
|
|
225
|
+
setConnectionState(connectedState);
|
|
226
|
+
clearConnectionErrorState();
|
|
227
|
+
setDeploymentUrlInput(verificationResult.normalizedBaseUrl);
|
|
228
|
+
setActiveDeploymentUrl(verificationResult.normalizedBaseUrl);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
try {
|
|
232
|
+
await persistPluginParameters({
|
|
233
|
+
deploymentURL: verificationResult.normalizedBaseUrl,
|
|
234
|
+
vercelURL: verificationResult.normalizedBaseUrl,
|
|
235
|
+
lambdaConnection: connectedState,
|
|
236
|
+
runtimeMode: runtimeModeSelection,
|
|
237
|
+
lambdaFullMode: runtimeModeSelection === "lambda",
|
|
238
|
+
});
|
|
239
|
+
debugLogger.log("Persisted connected lambda state on mount");
|
|
240
|
+
} catch (persistError) {
|
|
241
|
+
debugLogger.warn(
|
|
242
|
+
"Failed to persist connected lambda state on mount",
|
|
243
|
+
persistError
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
} catch (healthCheckError) {
|
|
247
|
+
debugLogger.warn("Lambda health check failed on mount", healthCheckError);
|
|
248
|
+
const disconnectedState = buildDisconnectedLambdaConnectionState(
|
|
249
|
+
healthCheckError,
|
|
250
|
+
configuredDeploymentUrl,
|
|
251
|
+
"config_mount"
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
if (!isCancelled) {
|
|
255
|
+
applyDisconnectedState(disconnectedState);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
try {
|
|
259
|
+
await persistPluginParameters({
|
|
260
|
+
lambdaConnection: disconnectedState,
|
|
261
|
+
runtimeMode: runtimeModeSelection,
|
|
262
|
+
lambdaFullMode: runtimeModeSelection === "lambda",
|
|
263
|
+
});
|
|
264
|
+
debugLogger.log("Persisted disconnected lambda state on mount");
|
|
265
|
+
} catch (persistError) {
|
|
266
|
+
debugLogger.warn(
|
|
267
|
+
"Failed to persist disconnected lambda state on mount",
|
|
268
|
+
persistError
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
} finally {
|
|
272
|
+
if (!isCancelled) {
|
|
273
|
+
setIsHealthChecking(false);
|
|
274
|
+
}
|
|
275
|
+
debugLogger.log("Lambda health check on mount finished");
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
runHealthCheck();
|
|
280
|
+
|
|
281
|
+
return () => {
|
|
282
|
+
isCancelled = true;
|
|
283
|
+
debugLogger.log("Config screen unmounted");
|
|
284
|
+
};
|
|
285
|
+
}, []);
|
|
286
|
+
|
|
287
|
+
const connectLambdaHandler = async () => {
|
|
288
|
+
if (runtimeModeSelection !== "lambda") {
|
|
289
|
+
await ctx.alert(
|
|
290
|
+
"Enable 'Also save records deleted from the API' before connecting a lambda deployment."
|
|
291
|
+
);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const candidateUrl = deploymentUrlInput.trim();
|
|
296
|
+
if (!candidateUrl) {
|
|
297
|
+
setConnectionErrorSummary("Enter your lambda deployment URL.");
|
|
298
|
+
setConnectionErrorDetails([]);
|
|
299
|
+
setShowConnectionDetails(false);
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
debugLogger.log("Connecting lambda function from config", { candidateUrl });
|
|
304
|
+
setIsConnecting(true);
|
|
305
|
+
clearConnectionErrorState();
|
|
306
|
+
|
|
307
|
+
try {
|
|
308
|
+
const verificationResult = await verifyLambdaHealth({
|
|
309
|
+
baseUrl: candidateUrl,
|
|
310
|
+
environment: ctx.environment,
|
|
311
|
+
phase: "config_connect",
|
|
312
|
+
debug: debugEnabled,
|
|
313
|
+
});
|
|
314
|
+
debugLogger.log("Lambda connect health check succeeded", verificationResult);
|
|
315
|
+
|
|
316
|
+
const webhookSyncResult = await ensureRecordBinWebhook({
|
|
317
|
+
currentUserAccessToken: ctx.currentUserAccessToken,
|
|
318
|
+
canManageWebhooks,
|
|
319
|
+
environment: ctx.environment,
|
|
320
|
+
lambdaBaseUrl: verificationResult.normalizedBaseUrl,
|
|
321
|
+
});
|
|
322
|
+
debugLogger.log("Record Bin webhook synchronized on connect", {
|
|
323
|
+
action: webhookSyncResult.action,
|
|
324
|
+
webhookId: webhookSyncResult.webhookId,
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
const connectedState = buildConnectedLambdaConnectionState(
|
|
328
|
+
verificationResult.endpoint,
|
|
329
|
+
verificationResult.checkedAt,
|
|
330
|
+
"config_connect"
|
|
331
|
+
);
|
|
332
|
+
setConnectionState(connectedState);
|
|
333
|
+
setDeploymentUrlInput(verificationResult.normalizedBaseUrl);
|
|
334
|
+
setActiveDeploymentUrl(verificationResult.normalizedBaseUrl);
|
|
335
|
+
clearConnectionErrorState();
|
|
336
|
+
|
|
337
|
+
await persistPluginParameters({
|
|
338
|
+
deploymentURL: verificationResult.normalizedBaseUrl,
|
|
339
|
+
vercelURL: verificationResult.normalizedBaseUrl,
|
|
340
|
+
lambdaConnection: connectedState,
|
|
341
|
+
runtimeMode: runtimeModeSelection,
|
|
342
|
+
lambdaFullMode: runtimeModeSelection === "lambda",
|
|
343
|
+
});
|
|
344
|
+
debugLogger.log("Persisted connected lambda state from config connect");
|
|
345
|
+
ctx.notice("Lambda function connected successfully.");
|
|
346
|
+
} catch (connectError) {
|
|
347
|
+
if (connectError instanceof LambdaHealthCheckError) {
|
|
348
|
+
debugLogger.warn("Lambda connect health check failed", connectError);
|
|
349
|
+
const disconnectedState = buildDisconnectedLambdaConnectionState(
|
|
350
|
+
connectError,
|
|
351
|
+
candidateUrl,
|
|
352
|
+
"config_connect"
|
|
353
|
+
);
|
|
354
|
+
applyDisconnectedState(disconnectedState);
|
|
355
|
+
|
|
356
|
+
try {
|
|
357
|
+
await persistPluginParameters({
|
|
358
|
+
lambdaConnection: disconnectedState,
|
|
359
|
+
runtimeMode: runtimeModeSelection,
|
|
360
|
+
lambdaFullMode: runtimeModeSelection === "lambda",
|
|
361
|
+
});
|
|
362
|
+
debugLogger.log("Persisted disconnected lambda state from connect");
|
|
363
|
+
} catch (persistError) {
|
|
364
|
+
debugLogger.warn(
|
|
365
|
+
"Failed to persist disconnected lambda state from connect",
|
|
366
|
+
persistError
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
} else if (isRecordBinWebhookSyncError(connectError)) {
|
|
370
|
+
debugLogger.warn("Record Bin webhook synchronization failed", connectError);
|
|
371
|
+
applyWebhookSyncErrorState(connectError, "connect");
|
|
372
|
+
} else {
|
|
373
|
+
debugLogger.error(
|
|
374
|
+
"Unexpected error while connecting lambda function",
|
|
375
|
+
connectError
|
|
376
|
+
);
|
|
377
|
+
setConnectionErrorSummary("Unexpected error while connecting lambda.");
|
|
378
|
+
setConnectionErrorDetails([
|
|
379
|
+
"Unexpected error while connecting lambda.",
|
|
380
|
+
`Failure details: ${connectError instanceof Error ? connectError.message : "Unknown error"}`,
|
|
381
|
+
]);
|
|
382
|
+
setShowConnectionDetails(false);
|
|
383
|
+
}
|
|
384
|
+
} finally {
|
|
385
|
+
setIsConnecting(false);
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
const disconnectCurrentLambdaHandler = async () => {
|
|
390
|
+
const previousActiveDeploymentUrl = activeDeploymentUrl;
|
|
391
|
+
|
|
392
|
+
debugLogger.log("Disconnecting current lambda function", {
|
|
393
|
+
activeDeploymentUrl,
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
setIsDisconnecting(true);
|
|
397
|
+
clearConnectionErrorState();
|
|
398
|
+
|
|
399
|
+
let webhookWasRemoved = false;
|
|
400
|
+
|
|
401
|
+
try {
|
|
402
|
+
const webhookRemovalResult = await removeRecordBinWebhook({
|
|
403
|
+
currentUserAccessToken: ctx.currentUserAccessToken,
|
|
404
|
+
canManageWebhooks,
|
|
405
|
+
environment: ctx.environment,
|
|
406
|
+
});
|
|
407
|
+
webhookWasRemoved = webhookRemovalResult.action === "deleted";
|
|
408
|
+
debugLogger.log("Record Bin webhook synchronized on disconnect", {
|
|
409
|
+
action: webhookRemovalResult.action,
|
|
410
|
+
webhookId: webhookRemovalResult.webhookId,
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
await persistPluginParameters({
|
|
414
|
+
deploymentURL: "",
|
|
415
|
+
vercelURL: "",
|
|
416
|
+
lambdaConnection: null,
|
|
417
|
+
runtimeMode: runtimeModeSelection,
|
|
418
|
+
lambdaFullMode: runtimeModeSelection === "lambda",
|
|
419
|
+
});
|
|
420
|
+
setDeploymentUrlInput("");
|
|
421
|
+
setActiveDeploymentUrl("");
|
|
422
|
+
setConnectionState(undefined);
|
|
423
|
+
clearConnectionErrorState();
|
|
424
|
+
debugLogger.log("Current lambda function disconnected");
|
|
425
|
+
ctx.notice("Current lambda function has been disconnected.");
|
|
426
|
+
} catch (disconnectError) {
|
|
427
|
+
if (webhookWasRemoved && previousActiveDeploymentUrl.trim()) {
|
|
428
|
+
try {
|
|
429
|
+
const webhookRestoreResult = await ensureRecordBinWebhook({
|
|
430
|
+
currentUserAccessToken: ctx.currentUserAccessToken,
|
|
431
|
+
canManageWebhooks,
|
|
432
|
+
environment: ctx.environment,
|
|
433
|
+
lambdaBaseUrl: previousActiveDeploymentUrl,
|
|
434
|
+
});
|
|
435
|
+
debugLogger.warn(
|
|
436
|
+
"Restored Record Bin webhook after disconnect failure",
|
|
437
|
+
webhookRestoreResult
|
|
438
|
+
);
|
|
439
|
+
} catch (restoreError) {
|
|
440
|
+
debugLogger.error(
|
|
441
|
+
"Failed to restore Record Bin webhook after disconnect failure",
|
|
442
|
+
restoreError
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
if (isRecordBinWebhookSyncError(disconnectError)) {
|
|
448
|
+
debugLogger.warn(
|
|
449
|
+
"Failed to synchronize Record Bin webhook on disconnect",
|
|
450
|
+
disconnectError
|
|
451
|
+
);
|
|
452
|
+
applyWebhookSyncErrorState(disconnectError, "disconnect");
|
|
453
|
+
} else {
|
|
454
|
+
debugLogger.warn(
|
|
455
|
+
"Failed to disconnect current lambda function",
|
|
456
|
+
disconnectError
|
|
457
|
+
);
|
|
458
|
+
setConnectionErrorSummary("Could not disconnect the current lambda.");
|
|
459
|
+
setConnectionErrorDetails([
|
|
460
|
+
"Could not disconnect the current lambda function.",
|
|
461
|
+
`Failure details: ${disconnectError instanceof Error ? disconnectError.message : "Unknown error"}`,
|
|
462
|
+
]);
|
|
463
|
+
setShowConnectionDetails(false);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
await ctx.alert("Could not disconnect the current lambda function.");
|
|
467
|
+
} finally {
|
|
468
|
+
setIsDisconnecting(false);
|
|
469
|
+
}
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
const handleDeployProviderClick = (provider: DeployProvider) => {
|
|
473
|
+
const option = DEPLOY_PROVIDER_OPTIONS.find(
|
|
474
|
+
(candidate) => candidate.provider === provider
|
|
475
|
+
);
|
|
476
|
+
if (!option) {
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
debugLogger.log("Opening deploy helper from config", { provider });
|
|
481
|
+
window.open(option.url, "_blank", "noreferrer");
|
|
482
|
+
};
|
|
15
483
|
|
|
16
484
|
const deletionHandler = async () => {
|
|
17
|
-
const userInput = parseInt(numberOfDays as string);
|
|
485
|
+
const userInput = parseInt(numberOfDays as string, 10);
|
|
486
|
+
debugLogger.log("Saving plugin settings", {
|
|
487
|
+
numberOfDays,
|
|
488
|
+
parsedNumberOfDays: userInput,
|
|
489
|
+
debugEnabled,
|
|
490
|
+
runtimeModeSelection,
|
|
491
|
+
});
|
|
492
|
+
|
|
18
493
|
if (isNaN(userInput)) {
|
|
19
|
-
setError("Days must be an
|
|
494
|
+
setError("Days must be an integer number");
|
|
495
|
+
debugLogger.warn("Cannot save settings: numberOfDays is not a number");
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const hasConnectedLambdaForSave =
|
|
500
|
+
runtimeModeSelection !== "lambda" ||
|
|
501
|
+
(activeDeploymentUrl.trim().length > 0 &&
|
|
502
|
+
connectionState?.status === "connected" &&
|
|
503
|
+
!isHealthChecking &&
|
|
504
|
+
!isConnecting);
|
|
505
|
+
if (!hasConnectedLambdaForSave) {
|
|
506
|
+
await ctx.alert(
|
|
507
|
+
"Cannot save while 'Also save records deleted from the API' is enabled unless the Lambda URL is connected and ping status is Connected."
|
|
508
|
+
);
|
|
20
509
|
return;
|
|
21
510
|
}
|
|
22
511
|
|
|
23
512
|
setLoading(true);
|
|
24
513
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
});
|
|
514
|
+
try {
|
|
515
|
+
let persistedDeploymentUrl = activeDeploymentUrl.trim();
|
|
516
|
+
let persistedConnectionState = connectionState ?? null;
|
|
29
517
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
518
|
+
if (runtimeModeSelection === "lambdaless" && persistedDeploymentUrl) {
|
|
519
|
+
debugLogger.log(
|
|
520
|
+
"Switching to Lambda-less mode: attempting to remove managed webhook and clear lambda URL"
|
|
521
|
+
);
|
|
522
|
+
try {
|
|
523
|
+
const webhookRemovalResult = await removeRecordBinWebhook({
|
|
524
|
+
currentUserAccessToken: ctx.currentUserAccessToken,
|
|
525
|
+
canManageWebhooks,
|
|
526
|
+
environment: ctx.environment,
|
|
527
|
+
});
|
|
528
|
+
debugLogger.log(
|
|
529
|
+
"Managed Record Bin webhook synchronized while switching to Lambda-less mode",
|
|
530
|
+
webhookRemovalResult
|
|
531
|
+
);
|
|
532
|
+
} catch (webhookRemovalError) {
|
|
533
|
+
debugLogger.warn(
|
|
534
|
+
"Could not remove managed webhook while switching to Lambda-less mode",
|
|
535
|
+
webhookRemovalError
|
|
536
|
+
);
|
|
537
|
+
await ctx.notice(
|
|
538
|
+
"Runtime was switched to Lambda-less, but the managed webhook could not be removed automatically. If you see duplicate captures, remove the webhook manually."
|
|
539
|
+
);
|
|
540
|
+
}
|
|
33
541
|
|
|
34
|
-
|
|
542
|
+
persistedDeploymentUrl = "";
|
|
543
|
+
persistedConnectionState = null;
|
|
544
|
+
setDeploymentUrlInput("");
|
|
545
|
+
setActiveDeploymentUrl("");
|
|
546
|
+
setConnectionState(undefined);
|
|
547
|
+
clearConnectionErrorState();
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
await persistPluginParameters({
|
|
551
|
+
debug: debugEnabled,
|
|
552
|
+
automaticBinCleanup: { numberOfDays: userInput, timeStamp: "" },
|
|
553
|
+
runtimeMode: runtimeModeSelection,
|
|
554
|
+
lambdaFullMode: runtimeModeSelection === "lambda",
|
|
555
|
+
deploymentURL: persistedDeploymentUrl,
|
|
556
|
+
vercelURL: persistedDeploymentUrl,
|
|
557
|
+
lambdaConnection: persistedConnectionState,
|
|
558
|
+
});
|
|
559
|
+
debugLogger.log("Plugin settings saved", {
|
|
560
|
+
numberOfDays: userInput,
|
|
561
|
+
runtimeModeSelection,
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
ctx.notice(
|
|
565
|
+
`Settings saved. Runtime mode: ${runtimeModeSelection === "lambda" ? "Lambda-full" : "Lambda-less"}. All records older than ${numberOfDays} days in the bin will be daily deleted. Debug logging is ${debugEnabled ? "enabled" : "disabled"}.`
|
|
566
|
+
);
|
|
567
|
+
setSavedFormValues({
|
|
568
|
+
numberOfDays: String(userInput),
|
|
569
|
+
debugEnabled,
|
|
570
|
+
runtimeMode: runtimeModeSelection,
|
|
571
|
+
});
|
|
572
|
+
} catch (saveError) {
|
|
573
|
+
debugLogger.warn("Failed to save plugin settings", saveError);
|
|
574
|
+
await ctx.alert("Could not save plugin settings.");
|
|
575
|
+
} finally {
|
|
576
|
+
setLoading(false);
|
|
577
|
+
}
|
|
35
578
|
};
|
|
36
579
|
|
|
580
|
+
const isLambdaFullModeEnabled = runtimeModeSelection === "lambda";
|
|
581
|
+
const pingIndicator = isHealthChecking || isConnecting
|
|
582
|
+
? {
|
|
583
|
+
label: "Checking ping...",
|
|
584
|
+
color: "var(--warning-color)",
|
|
585
|
+
}
|
|
586
|
+
: connectionState?.status === "connected"
|
|
587
|
+
? {
|
|
588
|
+
label: "Connected (ping successful)",
|
|
589
|
+
color: "var(--notice-color)",
|
|
590
|
+
}
|
|
591
|
+
: connectionState?.status === "disconnected"
|
|
592
|
+
? {
|
|
593
|
+
label: "Disconnected (ping failed)",
|
|
594
|
+
color: "var(--alert-color)",
|
|
595
|
+
}
|
|
596
|
+
: activeDeploymentUrl
|
|
597
|
+
? {
|
|
598
|
+
label: "Connection pending",
|
|
599
|
+
color: "var(--light-body-color)",
|
|
600
|
+
}
|
|
601
|
+
: {
|
|
602
|
+
label: "Disconnected (no lambda URL configured)",
|
|
603
|
+
color: "var(--light-body-color)",
|
|
604
|
+
};
|
|
605
|
+
const hasActiveDeploymentUrl = activeDeploymentUrl.trim().length > 0;
|
|
606
|
+
const connectButtonLabel = isConnecting
|
|
607
|
+
? hasActiveDeploymentUrl
|
|
608
|
+
? "Changing Lambda URL..."
|
|
609
|
+
: "Connecting..."
|
|
610
|
+
: hasActiveDeploymentUrl
|
|
611
|
+
? "Change Lambda URL"
|
|
612
|
+
: "Connect";
|
|
613
|
+
const disconnectButtonLabel = isDisconnecting ? "Disconnecting..." : "Disconnect";
|
|
614
|
+
const lambdaActionButtonStyle = {
|
|
615
|
+
width: "100%",
|
|
616
|
+
height: "40px",
|
|
617
|
+
fontSize: "var(--font-size-m)",
|
|
618
|
+
fontWeight: 500,
|
|
619
|
+
lineHeight: "1",
|
|
620
|
+
padding: "0 var(--spacing-m)",
|
|
621
|
+
display: "inline-flex",
|
|
622
|
+
alignItems: "center",
|
|
623
|
+
justifyContent: "center",
|
|
624
|
+
boxSizing: "border-box",
|
|
625
|
+
flex: "1 1 0",
|
|
626
|
+
whiteSpace: "nowrap",
|
|
627
|
+
};
|
|
628
|
+
const cardStyle = {
|
|
629
|
+
border: "1px solid var(--border-color)",
|
|
630
|
+
borderRadius: "6px",
|
|
631
|
+
background: "#fff",
|
|
632
|
+
padding: "var(--spacing-l)",
|
|
633
|
+
marginBottom: "var(--spacing-l)",
|
|
634
|
+
textAlign: "left",
|
|
635
|
+
};
|
|
636
|
+
const subtleTextStyle = {
|
|
637
|
+
margin: 0,
|
|
638
|
+
color: "var(--light-body-color)",
|
|
639
|
+
fontSize: "var(--font-size-xs)",
|
|
640
|
+
};
|
|
641
|
+
const infoTextStyle = {
|
|
642
|
+
marginTop: 0,
|
|
643
|
+
marginBottom: "var(--spacing-s)",
|
|
644
|
+
color: "var(--base-body-color)",
|
|
645
|
+
fontSize: "var(--font-size-s)",
|
|
646
|
+
};
|
|
647
|
+
const advancedSettingsStyle = {
|
|
648
|
+
display: "flex",
|
|
649
|
+
flexDirection: "column",
|
|
650
|
+
gap: "var(--spacing-m)",
|
|
651
|
+
};
|
|
652
|
+
const switchFieldNoHintGapStyle = {
|
|
653
|
+
"--spacing-s": "0",
|
|
654
|
+
} as CSSProperties;
|
|
655
|
+
const switchFieldNoHintGapStyleWithExtraSpacing = {
|
|
656
|
+
...switchFieldNoHintGapStyle,
|
|
657
|
+
marginBottom: "0.25rem",
|
|
658
|
+
} as CSSProperties;
|
|
659
|
+
const lambdaSetupDisabled =
|
|
660
|
+
isConnecting || isDisconnecting || isHealthChecking || isLoading;
|
|
661
|
+
const hasUnsavedChanges =
|
|
662
|
+
numberOfDays !== savedFormValues.numberOfDays ||
|
|
663
|
+
debugEnabled !== savedFormValues.debugEnabled ||
|
|
664
|
+
runtimeModeSelection !== savedFormValues.runtimeMode;
|
|
665
|
+
const canSaveWithLambdaMode =
|
|
666
|
+
!isLambdaFullModeEnabled ||
|
|
667
|
+
(hasActiveDeploymentUrl &&
|
|
668
|
+
connectionState?.status === "connected" &&
|
|
669
|
+
!isHealthChecking &&
|
|
670
|
+
!isConnecting);
|
|
671
|
+
const lambdaSaveBlockReason = !isLambdaFullModeEnabled
|
|
672
|
+
? ""
|
|
673
|
+
: !hasActiveDeploymentUrl
|
|
674
|
+
? "To save with API capture enabled, connect a Lambda URL first."
|
|
675
|
+
: isHealthChecking || isConnecting
|
|
676
|
+
? "Wait for the Lambda ping check to finish."
|
|
677
|
+
: connectionState?.status !== "connected"
|
|
678
|
+
? "To save with API capture enabled, Lambda status must be Connected."
|
|
679
|
+
: "";
|
|
680
|
+
|
|
37
681
|
return (
|
|
38
682
|
<Canvas ctx={ctx}>
|
|
39
|
-
<
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
683
|
+
<div
|
|
684
|
+
style={{
|
|
685
|
+
maxWidth: "760px",
|
|
686
|
+
margin: "0 auto",
|
|
687
|
+
}}
|
|
688
|
+
>
|
|
689
|
+
{isLambdaFullModeEnabled && (
|
|
690
|
+
<div style={cardStyle}>
|
|
691
|
+
<h2
|
|
692
|
+
style={{
|
|
693
|
+
marginTop: 0,
|
|
694
|
+
marginBottom: "var(--spacing-s)",
|
|
695
|
+
fontSize: "var(--font-size-l)",
|
|
696
|
+
}}
|
|
697
|
+
>
|
|
698
|
+
Lambda setup
|
|
699
|
+
</h2>
|
|
700
|
+
<p style={infoTextStyle}>
|
|
701
|
+
<strong>Current URL:</strong>{" "}
|
|
702
|
+
<span style={{ wordBreak: "break-all" }}>
|
|
703
|
+
{activeDeploymentUrl || "No lambda function connected."}
|
|
704
|
+
</span>
|
|
705
|
+
</p>
|
|
706
|
+
<p
|
|
707
|
+
style={{
|
|
708
|
+
display: "flex",
|
|
709
|
+
justifyContent: "flex-start",
|
|
710
|
+
alignItems: "center",
|
|
711
|
+
gap: "var(--spacing-s)",
|
|
712
|
+
marginTop: 0,
|
|
713
|
+
marginBottom: "var(--spacing-s)",
|
|
714
|
+
fontSize: "var(--font-size-s)",
|
|
715
|
+
color: "var(--light-body-color)",
|
|
716
|
+
}}
|
|
717
|
+
>
|
|
718
|
+
<span
|
|
719
|
+
aria-hidden="true"
|
|
720
|
+
style={{
|
|
721
|
+
display: "inline-block",
|
|
722
|
+
width: "10px",
|
|
723
|
+
height: "10px",
|
|
724
|
+
borderRadius: "999px",
|
|
725
|
+
background: pingIndicator.color,
|
|
726
|
+
}}
|
|
727
|
+
/>
|
|
728
|
+
<span>{pingIndicator.label}</span>
|
|
729
|
+
</p>
|
|
730
|
+
<p style={{ ...subtleTextStyle, marginBottom: "var(--spacing-l)" }}>
|
|
731
|
+
Status is based on the `/api/datocms/plugin-health` ping.
|
|
732
|
+
</p>
|
|
733
|
+
|
|
734
|
+
<TextField
|
|
735
|
+
name="deploymentURL"
|
|
736
|
+
id="deploymentURL"
|
|
737
|
+
label="Lambda URL"
|
|
738
|
+
value={deploymentUrlInput}
|
|
739
|
+
placeholder="https://record-bin.example.com/"
|
|
740
|
+
onChange={(newValue) => {
|
|
741
|
+
setDeploymentUrlInput(newValue);
|
|
742
|
+
clearConnectionErrorState();
|
|
743
|
+
}}
|
|
744
|
+
/>
|
|
745
|
+
|
|
746
|
+
<div
|
|
747
|
+
style={{
|
|
748
|
+
display: "flex",
|
|
749
|
+
alignItems: "center",
|
|
750
|
+
flexWrap: "nowrap",
|
|
751
|
+
width: "100%",
|
|
752
|
+
gap: "var(--spacing-s)",
|
|
753
|
+
marginTop: "var(--spacing-l)",
|
|
754
|
+
}}
|
|
755
|
+
>
|
|
756
|
+
<Dropdown
|
|
757
|
+
style={{ flex: "1 1 0" }}
|
|
758
|
+
renderTrigger={({ onClick }) => (
|
|
759
|
+
<Button
|
|
760
|
+
buttonType="muted"
|
|
761
|
+
onClick={onClick}
|
|
762
|
+
disabled={
|
|
763
|
+
isConnecting || isHealthChecking || isDisconnecting
|
|
764
|
+
}
|
|
765
|
+
style={lambdaActionButtonStyle}
|
|
766
|
+
>
|
|
767
|
+
Deploy lambda
|
|
768
|
+
</Button>
|
|
769
|
+
)}
|
|
770
|
+
>
|
|
771
|
+
<DropdownMenu alignment="left">
|
|
772
|
+
{DEPLOY_PROVIDER_OPTIONS.map((option) => (
|
|
773
|
+
<DropdownOption
|
|
774
|
+
key={option.provider}
|
|
775
|
+
onClick={() => handleDeployProviderClick(option.provider)}
|
|
776
|
+
>
|
|
777
|
+
{option.label}
|
|
778
|
+
</DropdownOption>
|
|
779
|
+
))}
|
|
780
|
+
</DropdownMenu>
|
|
781
|
+
</Dropdown>
|
|
782
|
+
<Button
|
|
783
|
+
onClick={disconnectCurrentLambdaHandler}
|
|
784
|
+
buttonType="negative"
|
|
785
|
+
disabled={
|
|
786
|
+
isDisconnecting || isHealthChecking || !activeDeploymentUrl.trim()
|
|
787
|
+
}
|
|
788
|
+
style={lambdaActionButtonStyle}
|
|
789
|
+
>
|
|
790
|
+
{disconnectButtonLabel}
|
|
791
|
+
</Button>
|
|
792
|
+
<Button
|
|
793
|
+
buttonType="primary"
|
|
794
|
+
onClick={connectLambdaHandler}
|
|
795
|
+
disabled={isConnecting || isHealthChecking || isDisconnecting}
|
|
796
|
+
style={lambdaActionButtonStyle}
|
|
797
|
+
>
|
|
798
|
+
{connectButtonLabel}
|
|
799
|
+
</Button>
|
|
800
|
+
</div>
|
|
801
|
+
</div>
|
|
802
|
+
)}
|
|
803
|
+
|
|
804
|
+
{isLambdaFullModeEnabled && connectionErrorSummary && (
|
|
805
|
+
<div
|
|
806
|
+
style={{
|
|
807
|
+
border: "1px solid rgba(var(--alert-color-rgb-components), 0.5)",
|
|
808
|
+
borderRadius: "6px",
|
|
809
|
+
background: "rgba(var(--alert-color-rgb-components), 0.08)",
|
|
810
|
+
padding: "var(--spacing-m)",
|
|
811
|
+
marginBottom: "var(--spacing-m)",
|
|
812
|
+
}}
|
|
813
|
+
>
|
|
814
|
+
<p style={{ marginTop: 0, marginBottom: "var(--spacing-s)" }}>
|
|
815
|
+
{connectionErrorSummary}
|
|
816
|
+
</p>
|
|
817
|
+
{connectionErrorDetails.length > 0 && (
|
|
818
|
+
<Button
|
|
819
|
+
buttonType="muted"
|
|
820
|
+
buttonSize="s"
|
|
821
|
+
onClick={() => setShowConnectionDetails((current) => !current)}
|
|
822
|
+
>
|
|
823
|
+
{showConnectionDetails ? "Hide details" : "Show details"}
|
|
824
|
+
</Button>
|
|
825
|
+
)}
|
|
826
|
+
</div>
|
|
827
|
+
)}
|
|
828
|
+
|
|
829
|
+
{isLambdaFullModeEnabled &&
|
|
830
|
+
showConnectionDetails &&
|
|
831
|
+
connectionErrorDetails.length > 0 && (
|
|
832
|
+
<div
|
|
833
|
+
style={{
|
|
834
|
+
border: "1px solid rgba(var(--alert-color-rgb-components), 0.5)",
|
|
835
|
+
borderRadius: "6px",
|
|
836
|
+
background: "#fff",
|
|
837
|
+
padding: "var(--spacing-m)",
|
|
838
|
+
marginBottom: "var(--spacing-l)",
|
|
839
|
+
textAlign: "left",
|
|
840
|
+
}}
|
|
841
|
+
>
|
|
842
|
+
{connectionErrorDetails.map((detail, index) => (
|
|
843
|
+
<p key={`config-health-error-${index}`}>{detail}</p>
|
|
844
|
+
))}
|
|
845
|
+
</div>
|
|
846
|
+
)}
|
|
847
|
+
|
|
848
|
+
<h2
|
|
849
|
+
style={{
|
|
850
|
+
marginTop: 0,
|
|
851
|
+
marginBottom: "var(--spacing-s)",
|
|
852
|
+
fontSize: "var(--font-size-l)",
|
|
51
853
|
}}
|
|
52
|
-
/>
|
|
53
|
-
<Button
|
|
54
|
-
onClick={deletionHandler}
|
|
55
|
-
fullWidth
|
|
56
|
-
buttonType={isLoading ? "muted" : "primary"}
|
|
57
|
-
disabled={isLoading}
|
|
58
854
|
>
|
|
59
|
-
|
|
60
|
-
</
|
|
61
|
-
|
|
855
|
+
Bin cleanup settings
|
|
856
|
+
</h2>
|
|
857
|
+
<Form>
|
|
858
|
+
<TextField
|
|
859
|
+
error={error}
|
|
860
|
+
required
|
|
861
|
+
name="numberOfDays"
|
|
862
|
+
id="numberOfDays"
|
|
863
|
+
label="Delete trashed records older than (days)"
|
|
864
|
+
value={numberOfDays}
|
|
865
|
+
onChange={(event) => {
|
|
866
|
+
setNumberOfDays(event);
|
|
867
|
+
setError("");
|
|
868
|
+
}}
|
|
869
|
+
/>
|
|
870
|
+
<Section
|
|
871
|
+
title="Advanced settings"
|
|
872
|
+
collapsible={{
|
|
873
|
+
isOpen: showAdvancedSettings,
|
|
874
|
+
onToggle: () => setShowAdvancedSettings((current) => !current),
|
|
875
|
+
}}
|
|
876
|
+
>
|
|
877
|
+
<div style={advancedSettingsStyle}>
|
|
878
|
+
<div style={switchFieldNoHintGapStyleWithExtraSpacing}>
|
|
879
|
+
<SwitchField
|
|
880
|
+
name="debug"
|
|
881
|
+
id="debug"
|
|
882
|
+
label="Enable debug logs"
|
|
883
|
+
hint="When enabled, plugin events and requests are logged to the browser console."
|
|
884
|
+
value={debugEnabled}
|
|
885
|
+
onChange={(newValue) => setDebugEnabled(newValue)}
|
|
886
|
+
/>
|
|
887
|
+
</div>
|
|
888
|
+
<div style={switchFieldNoHintGapStyle}>
|
|
889
|
+
<SwitchField
|
|
890
|
+
name="lambdaMode"
|
|
891
|
+
id="lambdaMode"
|
|
892
|
+
label="Also save records deleted from the API"
|
|
893
|
+
hint="If you do not know what Serverless Functions are, keep this disabled"
|
|
894
|
+
value={isLambdaFullModeEnabled}
|
|
895
|
+
switchInputProps={{
|
|
896
|
+
disabled: lambdaSetupDisabled,
|
|
897
|
+
}}
|
|
898
|
+
onChange={(newValue) => {
|
|
899
|
+
setRuntimeModeSelection(newValue ? "lambda" : "lambdaless");
|
|
900
|
+
clearConnectionErrorState();
|
|
901
|
+
}}
|
|
902
|
+
/>
|
|
903
|
+
</div>
|
|
904
|
+
<p style={subtleTextStyle}>
|
|
905
|
+
<a
|
|
906
|
+
href={`${PLUGIN_README_URL}#runtime-modes`}
|
|
907
|
+
target="_blank"
|
|
908
|
+
rel="noreferrer"
|
|
909
|
+
>
|
|
910
|
+
Runtime mode guide and differences
|
|
911
|
+
</a>
|
|
912
|
+
</p>
|
|
913
|
+
{isLambdaFullModeEnabled && (
|
|
914
|
+
<p style={subtleTextStyle}>
|
|
915
|
+
To capture API deletions, connect a Lambda function above.
|
|
916
|
+
</p>
|
|
917
|
+
)}
|
|
918
|
+
{runtimeModeSelection === "lambdaless" && hasActiveDeploymentUrl && (
|
|
919
|
+
<p style={subtleTextStyle}>
|
|
920
|
+
Lambda is currently connected. Click Save to complete the switch to
|
|
921
|
+
Lambda-less and remove the managed webhook.
|
|
922
|
+
</p>
|
|
923
|
+
)}
|
|
924
|
+
{lambdaSaveBlockReason && (
|
|
925
|
+
<p
|
|
926
|
+
style={{
|
|
927
|
+
...subtleTextStyle,
|
|
928
|
+
color: "var(--alert-color)",
|
|
929
|
+
}}
|
|
930
|
+
>
|
|
931
|
+
{lambdaSaveBlockReason}
|
|
932
|
+
</p>
|
|
933
|
+
)}
|
|
934
|
+
</div>
|
|
935
|
+
</Section>
|
|
936
|
+
{!showAdvancedSettings && lambdaSaveBlockReason && (
|
|
937
|
+
<p
|
|
938
|
+
style={{
|
|
939
|
+
...subtleTextStyle,
|
|
940
|
+
marginTop: "var(--spacing-s)",
|
|
941
|
+
color: "var(--alert-color)",
|
|
942
|
+
}}
|
|
943
|
+
>
|
|
944
|
+
Open Advanced settings to configure API capture before saving.
|
|
945
|
+
</p>
|
|
946
|
+
)}
|
|
947
|
+
<Button
|
|
948
|
+
onClick={deletionHandler}
|
|
949
|
+
fullWidth
|
|
950
|
+
buttonType={isLoading ? "muted" : "primary"}
|
|
951
|
+
disabled={
|
|
952
|
+
isLoading ||
|
|
953
|
+
isDisconnecting ||
|
|
954
|
+
isConnecting ||
|
|
955
|
+
!canSaveWithLambdaMode ||
|
|
956
|
+
!hasUnsavedChanges
|
|
957
|
+
}
|
|
958
|
+
>
|
|
959
|
+
Save
|
|
960
|
+
</Button>
|
|
961
|
+
</Form>
|
|
962
|
+
</div>
|
|
62
963
|
</Canvas>
|
|
63
964
|
);
|
|
64
965
|
}
|