@revenexx/studio-integrations 1.1.1 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +638 -0
- package/README.md +3 -1
- package/dist/module.json +1 -1
- package/dist/runtime/components/Integrations/CredentialCreateDialog.vue +6 -18
- package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +13 -23
- package/dist/runtime/components/Integrations/CredentialRevenexxSource.vue +7 -6
- package/dist/runtime/components/Integrations/DeadLettersPanel.vue +15 -4
- package/dist/runtime/components/Integrations/ExpressionInput.d.vue.ts +4 -0
- package/dist/runtime/components/Integrations/ExpressionInput.vue +45 -19
- package/dist/runtime/components/Integrations/ExpressionInput.vue.d.ts +4 -0
- package/dist/runtime/components/Integrations/GroupFrameView.vue +5 -4
- package/dist/runtime/components/Integrations/LoadingAnnouncement.d.vue.ts +38 -0
- package/dist/runtime/components/Integrations/LoadingAnnouncement.vue +16 -0
- package/dist/runtime/components/Integrations/LoadingAnnouncement.vue.d.ts +38 -0
- package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.vue +10 -2
- package/dist/runtime/components/Integrations/NodeConfigField.d.vue.ts +5 -1
- package/dist/runtime/components/Integrations/NodeConfigField.vue +88 -1
- package/dist/runtime/components/Integrations/NodeConfigField.vue.d.ts +5 -1
- package/dist/runtime/components/Integrations/NodeInspector.d.vue.ts +7 -3
- package/dist/runtime/components/Integrations/NodeInspector.vue +125 -54
- package/dist/runtime/components/Integrations/NodeInspector.vue.d.ts +7 -3
- package/dist/runtime/components/Integrations/NodeVersionMoveDialog.d.vue.ts +21 -0
- package/dist/runtime/components/Integrations/NodeVersionMoveDialog.vue +133 -0
- package/dist/runtime/components/Integrations/NodeVersionMoveDialog.vue.d.ts +21 -0
- package/dist/runtime/components/Integrations/RunActionsMenu.vue +2 -1
- package/dist/runtime/components/Integrations/RunDetailPanel.vue +5 -17
- package/dist/runtime/components/Integrations/RunResponseModal.d.vue.ts +30 -8
- package/dist/runtime/components/Integrations/RunResponseModal.vue +92 -24
- package/dist/runtime/components/Integrations/RunResponseModal.vue.d.ts +30 -8
- package/dist/runtime/components/Integrations/RunsTable.d.vue.ts +61 -3
- package/dist/runtime/components/Integrations/RunsTable.vue +129 -18
- package/dist/runtime/components/Integrations/RunsTable.vue.d.ts +61 -3
- package/dist/runtime/components/Integrations/SftpAccountImportDialog.vue +10 -1
- package/dist/runtime/components/Integrations/StateNamespaceCreateDialog.d.vue.ts +16 -0
- package/dist/runtime/components/Integrations/StateNamespaceCreateDialog.vue +111 -0
- package/dist/runtime/components/Integrations/StateNamespaceCreateDialog.vue.d.ts +16 -0
- package/dist/runtime/components/Integrations/StickyNoteView.vue +1 -1
- package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +18 -3
- package/dist/runtime/components/Integrations/WorkflowDeleteDialog.d.vue.ts +18 -0
- package/dist/runtime/components/Integrations/WorkflowDeleteDialog.vue +70 -0
- package/dist/runtime/components/Integrations/WorkflowDeleteDialog.vue.d.ts +18 -0
- package/dist/runtime/components/Integrations/WorkflowDuplicateDialog.vue +2 -1
- package/dist/runtime/components/Integrations/WorkflowEdgeView.vue +4 -4
- package/dist/runtime/components/Integrations/WorkflowEditor.vue +78 -30
- package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue +17 -6
- package/dist/runtime/components/Integrations/WorkflowNodeView.vue +17 -21
- package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +8 -11
- package/dist/runtime/components/Integrations/WorkflowStateNamespacesDialog.d.vue.ts +13 -0
- package/dist/runtime/components/Integrations/WorkflowStateNamespacesDialog.vue +216 -0
- package/dist/runtime/components/Integrations/WorkflowStateNamespacesDialog.vue.d.ts +13 -0
- package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +6 -7
- package/dist/runtime/components/Integrations/WorkflowVariablesDialog.vue +7 -7
- package/dist/runtime/components/Integrations/WorkflowsTable.vue +10 -48
- package/dist/runtime/components/Integrations/canvasSides.d.ts +48 -0
- package/dist/runtime/components/Integrations/canvasSides.js +22 -0
- package/dist/runtime/composables/useCredentialTypes.js +2 -6
- package/dist/runtime/composables/useIntegrationsAudit.js +2 -2
- package/dist/runtime/composables/useIntegrationsCredentials.js +4 -8
- package/dist/runtime/composables/useIntegrationsDeadLetters.d.ts +4 -0
- package/dist/runtime/composables/useIntegrationsDeadLetters.js +16 -1
- package/dist/runtime/composables/useIntegrationsNodes.js +2 -2
- package/dist/runtime/composables/useIntegrationsRevisions.js +2 -6
- package/dist/runtime/composables/useIntegrationsRuns.d.ts +4 -15
- package/dist/runtime/composables/useIntegrationsRuns.js +49 -34
- package/dist/runtime/composables/useIntegrationsSchedules.js +2 -2
- package/dist/runtime/composables/useIntegrationsSecrets.js +2 -6
- package/dist/runtime/composables/useIntegrationsTemplates.js +7 -5
- package/dist/runtime/composables/useIntegrationsTriggers.js +2 -2
- package/dist/runtime/composables/useIntegrationsWorkflows.js +10 -7
- package/dist/runtime/composables/useNodeConfigResolve.js +2 -2
- package/dist/runtime/composables/useOAuthConnect.d.ts +42 -0
- package/dist/runtime/composables/useOAuthConnect.js +33 -0
- package/dist/runtime/data/expressionEditor.d.ts +17 -5
- package/dist/runtime/data/expressionEditor.js +11 -5
- package/dist/runtime/data/handleId.d.ts +61 -1
- package/dist/runtime/data/handleId.js +8 -1
- package/dist/runtime/data/integrationNodeCatalog.d.ts +25 -0
- package/dist/runtime/data/integrationNodeCatalog.js +8 -0
- package/dist/runtime/data/nodeVersionMove.d.ts +72 -0
- package/dist/runtime/data/nodeVersionMove.js +65 -0
- package/dist/runtime/data/payloadForm.js +3 -2
- package/dist/runtime/i18n/messages/de.json +79 -12
- package/dist/runtime/i18n/messages/en.json +79 -12
- package/dist/runtime/pages/integrations/audit.vue +7 -6
- package/dist/runtime/pages/integrations/credentials.vue +9 -21
- package/dist/runtime/pages/integrations/index.vue +55 -8
- package/dist/runtime/pages/integrations/runs.vue +116 -7
- package/dist/runtime/pages/integrations/secrets.vue +2 -1
- package/dist/runtime/pages/integrations/templates/[slug].vue +2 -1
- package/dist/runtime/pages/integrations/templates/index.vue +18 -15
- package/dist/runtime/pages/integrations/workflows/[id].vue +169 -24
- package/dist/runtime/pages/integrations/workflows/index.vue +23 -4
- package/dist/runtime/types/integrations.d.ts +49 -1
- package/dist/runtime/utils/apiError.d.ts +21 -1
- package/dist/runtime/utils/apiError.js +1 -1
- package/dist/runtime/utils/buildSettle.d.ts +68 -0
- package/dist/runtime/utils/buildSettle.js +29 -0
- package/dist/runtime/utils/clipboard.d.ts +33 -0
- package/dist/runtime/utils/clipboard.js +11 -0
- package/dist/runtime/utils/deepClone.d.ts +41 -0
- package/dist/runtime/utils/deepClone.js +3 -0
- package/dist/runtime/utils/formatDuration.d.ts +44 -0
- package/dist/runtime/utils/formatDuration.js +13 -0
- package/dist/runtime/utils/nodeDisplayName.d.ts +22 -0
- package/dist/runtime/utils/nodeDisplayName.js +6 -0
- package/dist/runtime/utils/notify.d.ts +1 -1
- package/dist/runtime/utils/runEnvelope.d.ts +31 -0
- package/dist/runtime/utils/runEnvelope.js +12 -0
- package/dist/runtime/utils/runFeedQuery.d.ts +135 -0
- package/dist/runtime/utils/runFeedQuery.js +66 -0
- package/dist/runtime/utils/runOutputs.d.ts +33 -0
- package/dist/runtime/utils/runOutputs.js +10 -0
- package/dist/runtime/utils/settingCondition.d.ts +48 -0
- package/dist/runtime/utils/settingCondition.js +84 -0
- package/dist/runtime/utils/stateNamespaceName.d.ts +23 -0
- package/dist/runtime/utils/stateNamespaceName.js +8 -0
- package/dist/runtime/utils/validationFindings.d.ts +26 -3
- package/dist/runtime/utils/validationFindings.js +18 -3
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from "vue";
|
|
3
|
-
import { AltArrowLeft as ArrowLeft, AltArrowRight as Chevron,
|
|
3
|
+
import { AltArrowLeft as ArrowLeft, AltArrowRight as Chevron, RefreshCircle as Loader2, MenuDots as MoreHorizontal, Play, Refresh as RetryIcon, Diskette as Save, SidebarMinimalistic as RailIcon } from "#solar-icons";
|
|
4
4
|
import {
|
|
5
5
|
AlertDialog,
|
|
6
6
|
AlertDialogCancel,
|
|
@@ -16,6 +16,12 @@ import {
|
|
|
16
16
|
DialogDescription,
|
|
17
17
|
DialogHeader,
|
|
18
18
|
DialogTitle,
|
|
19
|
+
DropdownMenu,
|
|
20
|
+
DropdownMenuContent,
|
|
21
|
+
DropdownMenuItem,
|
|
22
|
+
DropdownMenuLabel,
|
|
23
|
+
DropdownMenuSeparator,
|
|
24
|
+
DropdownMenuTrigger,
|
|
19
25
|
Select,
|
|
20
26
|
SelectContent,
|
|
21
27
|
SelectItem,
|
|
@@ -24,16 +30,21 @@ import {
|
|
|
24
30
|
Switch,
|
|
25
31
|
Textarea
|
|
26
32
|
} from "@revenexx/studio";
|
|
33
|
+
import { deepClone } from "../../../utils/deepClone";
|
|
27
34
|
import WorkflowEditor from "../../../components/Integrations/WorkflowEditor.vue";
|
|
28
35
|
import WorkflowHistoryPanel from "../../../components/Integrations/WorkflowHistoryPanel.vue";
|
|
29
36
|
import WorkflowRunReplayPanel from "../../../components/Integrations/WorkflowRunReplayPanel.vue";
|
|
30
37
|
import WorkflowSettingsDialog from "../../../components/Integrations/WorkflowSettingsDialog.vue";
|
|
38
|
+
import WorkflowDeleteDialog from "../../../components/Integrations/WorkflowDeleteDialog.vue";
|
|
31
39
|
import WorkflowDuplicateDialog from "../../../components/Integrations/WorkflowDuplicateDialog.vue";
|
|
32
40
|
import RunsTable from "../../../components/Integrations/RunsTable.vue";
|
|
33
41
|
import RunDetailPanel from "../../../components/Integrations/RunDetailPanel.vue";
|
|
34
42
|
import { toast } from "vue-sonner";
|
|
35
|
-
import { IntegrationsApiError, apiValidationIssues } from "../../../utils/apiError";
|
|
43
|
+
import { IntegrationsApiError, apiValidationIssues, errorReason } from "../../../utils/apiError";
|
|
36
44
|
import { createPollBudget, SAFETY_POLL_MS } from "../../../utils/pollBudget";
|
|
45
|
+
import { BUILD_SETTLE_POLL_MS, BUILD_SETTLE_TIMEOUT_MS, createBuildSettle, isBuildUnsettled } from "../../../utils/buildSettle";
|
|
46
|
+
import { splitRunOutputs } from "../../../utils/runOutputs";
|
|
47
|
+
import { isTerminalRun, unwrapRun } from "../../../utils/runEnvelope";
|
|
37
48
|
import { findingsSummary, groupFindingsByNode, nodeDisplayLabel } from "../../../utils/validationFindings";
|
|
38
49
|
import { payloadFieldsFromBlob } from "../../../data/payloadForm";
|
|
39
50
|
import WorkflowIconView from "../../../components/Integrations/WorkflowIconView.vue";
|
|
@@ -62,11 +73,11 @@ function draftFrom() {
|
|
|
62
73
|
executionMode: w.execution_mode ?? "async_only",
|
|
63
74
|
tags: [...w.blob.tags ?? []],
|
|
64
75
|
icon: w.blob.icon ?? null,
|
|
65
|
-
blob:
|
|
76
|
+
blob: deepClone(w.blob)
|
|
66
77
|
};
|
|
67
78
|
}
|
|
68
79
|
function payloadBlob(d) {
|
|
69
|
-
const blob =
|
|
80
|
+
const blob = deepClone(d.blob);
|
|
70
81
|
blob.name = d.name;
|
|
71
82
|
if (d.description) blob.description = d.description;
|
|
72
83
|
else delete blob.description;
|
|
@@ -114,8 +125,8 @@ const reauthCredentials = computed(() => {
|
|
|
114
125
|
const buildWorking = computed(() => workflow.value?.buildStatus === "building" || workflow.value?.buildStatus === "pending");
|
|
115
126
|
const buildFailed = computed(() => workflow.value?.buildStatus === "failed");
|
|
116
127
|
const waitingForBuild = ref(false);
|
|
117
|
-
const BUILD_WAIT_POLL_MS =
|
|
118
|
-
const BUILD_WAIT_TIMEOUT_MS =
|
|
128
|
+
const BUILD_WAIT_POLL_MS = BUILD_SETTLE_POLL_MS;
|
|
129
|
+
const BUILD_WAIT_TIMEOUT_MS = BUILD_SETTLE_TIMEOUT_MS;
|
|
119
130
|
async function waitForBuildReady() {
|
|
120
131
|
waitingForBuild.value = true;
|
|
121
132
|
try {
|
|
@@ -138,6 +149,14 @@ async function waitForBuildReady() {
|
|
|
138
149
|
waitingForBuild.value = false;
|
|
139
150
|
}
|
|
140
151
|
}
|
|
152
|
+
const buildSettle = createBuildSettle();
|
|
153
|
+
function settleBuildStatus() {
|
|
154
|
+
void buildSettle.settle({
|
|
155
|
+
unsettled: () => !!workflow.value && isBuildUnsettled(workflow.value.buildStatus),
|
|
156
|
+
read: () => get(workflow.value.id)
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
onUnmounted(() => buildSettle.stop());
|
|
141
160
|
const buildNotice = computed(() => {
|
|
142
161
|
switch (workflow.value?.buildStatus) {
|
|
143
162
|
case "building":
|
|
@@ -159,6 +178,7 @@ watch(id, async () => {
|
|
|
159
178
|
draft.value = null;
|
|
160
179
|
warningsExpanded.value = false;
|
|
161
180
|
if (tab.value === "runs") void loadRuns(1);
|
|
181
|
+
settleBuildStatus();
|
|
162
182
|
}, { immediate: true });
|
|
163
183
|
function onChange(blob) {
|
|
164
184
|
const d = draftFrom();
|
|
@@ -212,12 +232,25 @@ async function onDuplicated(copy) {
|
|
|
212
232
|
toast.success(t("integration.editor.toast.duplicated", { name: copy.name }));
|
|
213
233
|
await leaveTo(`/integrations/workflows/${copy.id}`);
|
|
214
234
|
}
|
|
235
|
+
const deleteOpen = ref(false);
|
|
236
|
+
const deleteTarget = ref(null);
|
|
237
|
+
function askDelete() {
|
|
238
|
+
if (!workflow.value) return;
|
|
239
|
+
deleteTarget.value = { id: workflow.value.id, name: workflow.value.name };
|
|
240
|
+
deleteOpen.value = true;
|
|
241
|
+
}
|
|
242
|
+
async function onDeleted(wf) {
|
|
243
|
+
draft.value = null;
|
|
244
|
+
toast.success(t("integration.editor.toast.deleted", { name: wf.name }));
|
|
245
|
+
await leaveTo("/integrations/workflows");
|
|
246
|
+
}
|
|
215
247
|
async function onRevisionRestored() {
|
|
216
248
|
if (!workflow.value) return;
|
|
217
249
|
await get(workflow.value.id);
|
|
218
250
|
draft.value = null;
|
|
251
|
+
settleBuildStatus();
|
|
219
252
|
}
|
|
220
|
-
const { trigger: triggerRun, loadForWorkflow, fetchRun, retryRun } = useIntegrationsRuns();
|
|
253
|
+
const { trigger: triggerRun, loadForWorkflow, fetchRun, getRun, retryRun } = useIntegrationsRuns();
|
|
221
254
|
const running = ref(false);
|
|
222
255
|
const waitingMode = ref(null);
|
|
223
256
|
const payloadText = ref("{}");
|
|
@@ -227,6 +260,8 @@ const syncOpen = ref(false);
|
|
|
227
260
|
const syncLoading = ref(false);
|
|
228
261
|
const syncResponse = ref(null);
|
|
229
262
|
const syncError = ref(null);
|
|
263
|
+
const runObject = ref(null);
|
|
264
|
+
const followingRun = ref(false);
|
|
230
265
|
const runModalMode = ref("sync");
|
|
231
266
|
const runModalRunId = ref(null);
|
|
232
267
|
function extractRunId(res) {
|
|
@@ -236,6 +271,13 @@ function extractRunId(res) {
|
|
|
236
271
|
}
|
|
237
272
|
return null;
|
|
238
273
|
}
|
|
274
|
+
function asRunObject(res) {
|
|
275
|
+
if (!res || typeof res !== "object" || Array.isArray(res)) return null;
|
|
276
|
+
const o = res;
|
|
277
|
+
return "id" in o && "status" in o ? o : null;
|
|
278
|
+
}
|
|
279
|
+
const runOutputs = computed(() => splitRunOutputs(runObject.value?.result, shownBlob.value?.nodes ?? []));
|
|
280
|
+
const runResult = computed(() => runObject.value ? runObject.value.result : syncResponse.value);
|
|
239
281
|
function viewRunFromModal(id2) {
|
|
240
282
|
syncOpen.value = false;
|
|
241
283
|
void router.push({ query: { ...route.query, run: id2 } });
|
|
@@ -244,9 +286,8 @@ function syncErrorMessage(e) {
|
|
|
244
286
|
if (e instanceof IntegrationsApiError) {
|
|
245
287
|
if (e.status === 504) return t("integration.editor.run.error.syncTimeout");
|
|
246
288
|
if (e.status === 422 && e.body?.status === "failed") return t("integration.editor.run.error.syncFailed");
|
|
247
|
-
return e.message || t("integration.editor.run.error.syncFailed");
|
|
248
289
|
}
|
|
249
|
-
return e
|
|
290
|
+
return errorReason(e, t("integration.editor.run.error.syncFailed"));
|
|
250
291
|
}
|
|
251
292
|
function refusedForSettings(e) {
|
|
252
293
|
const issues = apiValidationIssues(e);
|
|
@@ -289,6 +330,8 @@ async function run(mode) {
|
|
|
289
330
|
}
|
|
290
331
|
runModalMode.value = mode;
|
|
291
332
|
runModalRunId.value = null;
|
|
333
|
+
stopFollowingRun();
|
|
334
|
+
runObject.value = null;
|
|
292
335
|
syncOpen.value = true;
|
|
293
336
|
syncLoading.value = true;
|
|
294
337
|
syncResponse.value = null;
|
|
@@ -297,6 +340,7 @@ async function run(mode) {
|
|
|
297
340
|
try {
|
|
298
341
|
syncResponse.value = await triggerRun(workflow.value.id, payload, "sync");
|
|
299
342
|
runModalRunId.value = extractRunId(syncResponse.value);
|
|
343
|
+
runObject.value = asRunObject(syncResponse.value);
|
|
300
344
|
} catch (e) {
|
|
301
345
|
await reportRunFailure(e, syncErrorMessage);
|
|
302
346
|
if (e instanceof IntegrationsApiError) {
|
|
@@ -312,8 +356,10 @@ async function run(mode) {
|
|
|
312
356
|
try {
|
|
313
357
|
const res = await triggerRun(workflow.value.id, payload, "async");
|
|
314
358
|
runModalRunId.value = extractRunId(res);
|
|
359
|
+
runObject.value = asRunObject(res);
|
|
360
|
+
startFollowingRun();
|
|
315
361
|
} catch (e) {
|
|
316
|
-
await reportRunFailure(e, (err) => err
|
|
362
|
+
await reportRunFailure(e, (err) => errorReason(err, t("integration.editor.run.error.couldNotStart")));
|
|
317
363
|
} finally {
|
|
318
364
|
running.value = false;
|
|
319
365
|
syncLoading.value = false;
|
|
@@ -419,6 +465,61 @@ function startRunsPoll() {
|
|
|
419
465
|
}, RUNS_POLL_MS);
|
|
420
466
|
}
|
|
421
467
|
onUnmounted(stopRunsPoll);
|
|
468
|
+
const FOLLOW_POLL_MS = 3e3;
|
|
469
|
+
const followBudget = createPollBudget({ intervalMs: FOLLOW_POLL_MS, liveIntervalMs: SAFETY_POLL_MS });
|
|
470
|
+
const FOLLOW_MAX_MISSES = 3;
|
|
471
|
+
let followTimer = null;
|
|
472
|
+
let followMisses = 0;
|
|
473
|
+
let followSeq = 0;
|
|
474
|
+
function stopFollowingRun() {
|
|
475
|
+
if (followTimer) {
|
|
476
|
+
clearInterval(followTimer);
|
|
477
|
+
followTimer = null;
|
|
478
|
+
}
|
|
479
|
+
followSeq++;
|
|
480
|
+
followingRun.value = false;
|
|
481
|
+
}
|
|
482
|
+
function countFollowMiss() {
|
|
483
|
+
if (++followMisses >= FOLLOW_MAX_MISSES) stopFollowingRun();
|
|
484
|
+
}
|
|
485
|
+
async function readFollowedRun() {
|
|
486
|
+
const id2 = runModalRunId.value;
|
|
487
|
+
if (!workflow.value || !id2) return stopFollowingRun();
|
|
488
|
+
const seq = ++followSeq;
|
|
489
|
+
try {
|
|
490
|
+
const raw = await getRun(workflow.value.id, id2);
|
|
491
|
+
followBudget.spend();
|
|
492
|
+
if (seq !== followSeq) return;
|
|
493
|
+
const run2 = asRunObject(unwrapRun(raw));
|
|
494
|
+
if (!run2) return countFollowMiss();
|
|
495
|
+
followMisses = 0;
|
|
496
|
+
runObject.value = run2;
|
|
497
|
+
if (isTerminalRun(run2)) stopFollowingRun();
|
|
498
|
+
} catch {
|
|
499
|
+
if (seq !== followSeq) return;
|
|
500
|
+
countFollowMiss();
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
function startFollowingRun() {
|
|
504
|
+
stopFollowingRun();
|
|
505
|
+
if (!runModalRunId.value || isTerminalRun(runObject.value)) return;
|
|
506
|
+
followMisses = 0;
|
|
507
|
+
followingRun.value = true;
|
|
508
|
+
void readFollowedRun();
|
|
509
|
+
followTimer = setInterval(() => {
|
|
510
|
+
if (followBudget.due(realtimeDelivering.value)) void readFollowedRun();
|
|
511
|
+
}, FOLLOW_POLL_MS);
|
|
512
|
+
}
|
|
513
|
+
watch(runTick, () => {
|
|
514
|
+
if (!followingRun.value) return;
|
|
515
|
+
const eventRun = lastRunEvent.value?.topicId;
|
|
516
|
+
if (eventRun && eventRun !== String(runModalRunId.value ?? "")) return;
|
|
517
|
+
void readFollowedRun();
|
|
518
|
+
});
|
|
519
|
+
watch(syncOpen, (open) => {
|
|
520
|
+
if (!open) stopFollowingRun();
|
|
521
|
+
});
|
|
522
|
+
onUnmounted(stopFollowingRun);
|
|
422
523
|
watch(runTick, () => {
|
|
423
524
|
if (tab.value !== "runs" || runsWanted.value !== 1 || runsLoading.value) return;
|
|
424
525
|
const eventWorkflow = workflowIdOf(lastRunEvent.value);
|
|
@@ -455,7 +556,7 @@ async function loadRuns(page = runsWanted.value, opts = {}) {
|
|
|
455
556
|
runsWanted.value = res.page;
|
|
456
557
|
} catch (e) {
|
|
457
558
|
if (seq !== runsSeq) return;
|
|
458
|
-
runsError.value = e
|
|
559
|
+
runsError.value = errorReason(e, t("integration.editor.runs.error.load"));
|
|
459
560
|
runsWanted.value = runsPage.value;
|
|
460
561
|
} finally {
|
|
461
562
|
if (!opts.silent && seq === runsLoadingSeq) runsLoading.value = false;
|
|
@@ -510,7 +611,7 @@ async function loadOpenRun() {
|
|
|
510
611
|
try {
|
|
511
612
|
openRun.value = await fetchRun(workflow.value.id, openRunId.value, workflow.value.name);
|
|
512
613
|
} catch (e) {
|
|
513
|
-
openRunError.value = e
|
|
614
|
+
openRunError.value = errorReason(e, t("integration.editor.run.error.loadOpenRun"));
|
|
514
615
|
}
|
|
515
616
|
}
|
|
516
617
|
watch(openRunId, () => {
|
|
@@ -536,7 +637,7 @@ async function retryOpenRun() {
|
|
|
536
637
|
toast.success(newId && newId !== openRun.value.id ? t("integration.editor.retry.toast.started", { id: newId }) : t("integration.editor.retry.toast.requested"));
|
|
537
638
|
void loadRuns(1, { silent: true });
|
|
538
639
|
} catch (e) {
|
|
539
|
-
toast.error(e
|
|
640
|
+
toast.error(errorReason(e, t("integration.editor.retry.error.generic")));
|
|
540
641
|
} finally {
|
|
541
642
|
retryBusy.value = false;
|
|
542
643
|
}
|
|
@@ -589,7 +690,7 @@ async function onToggleActive() {
|
|
|
589
690
|
toast.error(t("integration.editor.activate.error.validationIssues", issues.length));
|
|
590
691
|
return;
|
|
591
692
|
}
|
|
592
|
-
toast.error(e
|
|
693
|
+
toast.error(errorReason(e, t("integration.editor.activate.error.generic")));
|
|
593
694
|
}
|
|
594
695
|
}
|
|
595
696
|
async function save() {
|
|
@@ -606,6 +707,7 @@ async function save() {
|
|
|
606
707
|
executionMode: d.executionMode
|
|
607
708
|
});
|
|
608
709
|
draft.value = null;
|
|
710
|
+
settleBuildStatus();
|
|
609
711
|
} catch (e) {
|
|
610
712
|
const issues = apiValidationIssues(e);
|
|
611
713
|
if (issues.length && workflow.value.active) {
|
|
@@ -617,7 +719,7 @@ async function save() {
|
|
|
617
719
|
});
|
|
618
720
|
deactivationOpen.value = true;
|
|
619
721
|
} else {
|
|
620
|
-
saveError.value = e
|
|
722
|
+
saveError.value = errorReason(e, t("integration.editor.save.error.generic"));
|
|
621
723
|
}
|
|
622
724
|
} finally {
|
|
623
725
|
saving.value = false;
|
|
@@ -642,9 +744,10 @@ async function confirmDeactivate() {
|
|
|
642
744
|
draft.value = null;
|
|
643
745
|
deactivationOpen.value = false;
|
|
644
746
|
toast.success(t("integration.editor.deactivate.toast.savedPaused"));
|
|
747
|
+
settleBuildStatus();
|
|
645
748
|
} catch (e) {
|
|
646
749
|
deactivationOpen.value = false;
|
|
647
|
-
saveError.value = e
|
|
750
|
+
saveError.value = errorReason(e, t("integration.editor.save.error.generic"));
|
|
648
751
|
} finally {
|
|
649
752
|
saving.value = false;
|
|
650
753
|
}
|
|
@@ -747,10 +850,33 @@ function formatDateTime(iso) {
|
|
|
747
850
|
{{ workflow.buildStatus === "building" ? t("integration.editor.build.chip.building") : t("integration.editor.build.chip.queued") }}
|
|
748
851
|
</span>
|
|
749
852
|
<span v-if="isDirty" class="text-sm text-amber-600">{{ t("integration.editor.status.unsavedChanges") }}</span>
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
853
|
+
<!-- What is done to the WORKFLOW, as opposed to the draft in front
|
|
854
|
+
of it: the buttons beside this menu discard and save an
|
|
855
|
+
afternoon's work, and these two make and unmake the thing that
|
|
856
|
+
work belongs to. Two reasons they are a menu rather than two more
|
|
857
|
+
buttons in a row of five. Deleting cannot be undone and has no
|
|
858
|
+
business a mis-click away from Save; and a lone Delete under a
|
|
859
|
+
Duplicate that stayed a button reads as an oversight rather than
|
|
860
|
+
as a grouping — the menu only holds together while it holds both.
|
|
861
|
+
Icon-only, so it carries a name of its own. -->
|
|
862
|
+
<DropdownMenu>
|
|
863
|
+
<DropdownMenuTrigger as-child>
|
|
864
|
+
<Button variant="outline" size="icon" class="size-7" :disabled="saving" :aria-label="t('integration.editor.action.more.aria')">
|
|
865
|
+
<MoreHorizontal :size="12" />
|
|
866
|
+
</Button>
|
|
867
|
+
</DropdownMenuTrigger>
|
|
868
|
+
<DropdownMenuContent align="end">
|
|
869
|
+
<DropdownMenuLabel>{{ t("integration.editor.action.more.label") }}</DropdownMenuLabel>
|
|
870
|
+
<DropdownMenuSeparator />
|
|
871
|
+
<DropdownMenuItem @select="duplicateOpen = true">
|
|
872
|
+
{{ t("integration.editor.action.duplicate.label") }}
|
|
873
|
+
</DropdownMenuItem>
|
|
874
|
+
<DropdownMenuSeparator />
|
|
875
|
+
<DropdownMenuItem class="text-destructive focus:text-destructive" @select="askDelete">
|
|
876
|
+
{{ t("integration.common.action.delete") }}
|
|
877
|
+
</DropdownMenuItem>
|
|
878
|
+
</DropdownMenuContent>
|
|
879
|
+
</DropdownMenu>
|
|
754
880
|
<Button variant="outline" class="h-7 text-sm" :disabled="!isDirty || saving" :title="t('integration.editor.action.discard.title')" @click="discardChanges">
|
|
755
881
|
{{ t("integration.editor.action.discard.label") }}
|
|
756
882
|
</Button>
|
|
@@ -898,7 +1024,7 @@ function formatDateTime(iso) {
|
|
|
898
1024
|
:draft="draft?.blob ?? null"
|
|
899
1025
|
:run-busy="running || syncLoading || waitingForBuild"
|
|
900
1026
|
:run-waiting="waitingForBuild"
|
|
901
|
-
:overlay-open="settingsOpen || duplicateOpen || syncOpen || runPayloadOpen || deactivationOpen || leaveOpen"
|
|
1027
|
+
:overlay-open="settingsOpen || duplicateOpen || deleteOpen || syncOpen || runPayloadOpen || deactivationOpen || leaveOpen"
|
|
902
1028
|
@change="onChange"
|
|
903
1029
|
@run="runFromEditor"
|
|
904
1030
|
@toggle-active="onToggleActive"
|
|
@@ -1032,7 +1158,10 @@ function formatDateTime(iso) {
|
|
|
1032
1158
|
:open="syncOpen"
|
|
1033
1159
|
:loading="syncLoading"
|
|
1034
1160
|
:mode="runModalMode"
|
|
1035
|
-
:
|
|
1161
|
+
:run="runObject"
|
|
1162
|
+
:outputs="runOutputs"
|
|
1163
|
+
:result="runResult"
|
|
1164
|
+
:following="followingRun"
|
|
1036
1165
|
:error="syncError"
|
|
1037
1166
|
:run-id="runModalRunId"
|
|
1038
1167
|
@update:open="(v) => syncOpen = v"
|
|
@@ -1049,6 +1178,8 @@ function formatDateTime(iso) {
|
|
|
1049
1178
|
@apply="applySettings"
|
|
1050
1179
|
/>
|
|
1051
1180
|
|
|
1181
|
+
<WorkflowDeleteDialog v-model:open="deleteOpen" :workflow="deleteTarget" @deleted="onDeleted" />
|
|
1182
|
+
|
|
1052
1183
|
<WorkflowDuplicateDialog v-model:open="duplicateOpen" :workflow="duplicateTarget" @duplicated="onDuplicated" />
|
|
1053
1184
|
|
|
1054
1185
|
<!-- Leaving the workflow with unsaved work.
|
|
@@ -1060,7 +1191,18 @@ function formatDateTime(iso) {
|
|
|
1060
1191
|
afternoon this dialog exists to protect.
|
|
1061
1192
|
`as-child` so the studio's own Button is that element rather than a second one
|
|
1062
1193
|
beside it. Escape means the same thing, through `update:open`; a click beside
|
|
1063
|
-
the dialog does nothing at all, which an alert dialog is entitled to.
|
|
1194
|
+
the dialog does nothing at all, which an alert dialog is entitled to.
|
|
1195
|
+
|
|
1196
|
+
The class list on that Button is the price of `as-child`: `AlertDialogCancel`
|
|
1197
|
+
applies the outline variant to whatever it renders, and every dialog in this
|
|
1198
|
+
module draws the answer on the right as a filled one. So the two variants meet
|
|
1199
|
+
on one element and `tailwind-merge` decides between them per group — which it
|
|
1200
|
+
cannot do where the outline variant names a group the default one does not, and
|
|
1201
|
+
those are the five that have to be said here. Left to itself the merge produced
|
|
1202
|
+
a background from the outline variant and a text colour from the default, which
|
|
1203
|
+
resolve to the same colour in this theme: the label was invisible until hovered
|
|
1204
|
+
(PO-415). Nothing marks this answer as the one holding the keyboard otherwise —
|
|
1205
|
+
`focus-visible` does not match after the click that opened the dialog. -->
|
|
1064
1206
|
<AlertDialog :open="leaveOpen" @update:open="(v) => {
|
|
1065
1207
|
if (!v) answerLeave(false);
|
|
1066
1208
|
}">
|
|
@@ -1076,7 +1218,10 @@ function formatDateTime(iso) {
|
|
|
1076
1218
|
{{ t("integration.editor.leave.dialog.discard") }}
|
|
1077
1219
|
</Button>
|
|
1078
1220
|
<AlertDialogCancel as-child>
|
|
1079
|
-
<Button
|
|
1221
|
+
<Button
|
|
1222
|
+
class="border-transparent bg-primary text-primary-foreground hover:bg-primary/90 hover:text-primary-foreground dark:border-transparent dark:bg-primary dark:hover:bg-primary/90"
|
|
1223
|
+
@click="answerLeave(false)"
|
|
1224
|
+
>
|
|
1080
1225
|
{{ t("integration.editor.leave.dialog.stay") }}
|
|
1081
1226
|
</Button>
|
|
1082
1227
|
</AlertDialogCancel>
|
|
@@ -3,15 +3,30 @@ import { AddCircle as Plus } from "#solar-icons";
|
|
|
3
3
|
import { Button } from "@revenexx/studio";
|
|
4
4
|
import WorkflowsTable from "../../../components/Integrations/WorkflowsTable.vue";
|
|
5
5
|
import WorkflowCreateDialog from "../../../components/Integrations/WorkflowCreateDialog.vue";
|
|
6
|
+
import { createBuildSettle, isBuildUnsettled } from "../../../utils/buildSettle";
|
|
6
7
|
const t = useIntegrationsT();
|
|
7
8
|
useHead({ title: () => t("integration.workflows.list.head.title") });
|
|
8
9
|
const { workflows, loading, error, load } = useIntegrationsWorkflows();
|
|
9
|
-
onMounted(() => {
|
|
10
|
-
|
|
10
|
+
onMounted(async () => {
|
|
11
|
+
await load();
|
|
12
|
+
settleBuildStates();
|
|
13
|
+
});
|
|
14
|
+
let onScreen = true;
|
|
15
|
+
const buildSettle = createBuildSettle();
|
|
16
|
+
function settleBuildStates() {
|
|
17
|
+
if (!onScreen) return;
|
|
18
|
+
void buildSettle.settle({
|
|
19
|
+
unsettled: () => workflows.value.some((wf) => isBuildUnsettled(wf.buildStatus)),
|
|
20
|
+
read: () => load(true)
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
onUnmounted(() => {
|
|
24
|
+
onScreen = false;
|
|
25
|
+
buildSettle.stop();
|
|
11
26
|
});
|
|
12
27
|
const { buildTick } = useIntegrationsRealtime();
|
|
13
28
|
watch(buildTick, () => {
|
|
14
|
-
void load(true);
|
|
29
|
+
void load(true).then(() => settleBuildStates());
|
|
15
30
|
});
|
|
16
31
|
const loadFailed = computed(() => !!error && !!error.value && !workflows.value.length);
|
|
17
32
|
const createOpen = ref(false);
|
|
@@ -43,7 +58,11 @@ const createOpen = ref(false);
|
|
|
43
58
|
</Button>
|
|
44
59
|
</template>
|
|
45
60
|
</StateCard>
|
|
46
|
-
|
|
61
|
+
<!-- `loading` is the composable's app-wide flag and goes up for every read, the
|
|
62
|
+
build watch's included — so passing it straight through would flip the table
|
|
63
|
+
between its skeleton and its empty text every few seconds while a build
|
|
64
|
+
runs. What the table's skeleton means is "nothing to show yet". -->
|
|
65
|
+
<WorkflowsTable v-else :workflows="workflows" :loading="loading && !workflows.length" />
|
|
47
66
|
</div>
|
|
48
67
|
|
|
49
68
|
<WorkflowCreateDialog v-model:open="createOpen" />
|
|
@@ -158,11 +158,28 @@ export interface WorkflowIcon {
|
|
|
158
158
|
/** Image URL, e.g. a node-package logo (kind=image). */
|
|
159
159
|
src?: string;
|
|
160
160
|
}
|
|
161
|
+
/** The four things a workflow can remember between runs (PO-374). */
|
|
162
|
+
export type WorkflowStateRole = 'mapping' | 'cursor' | 'dedupe' | 'digest';
|
|
163
|
+
/** `private` belongs to this workflow; `shared` is tenant-wide and outlives it. */
|
|
164
|
+
export type WorkflowStateVisibility = 'private' | 'shared';
|
|
165
|
+
/**
|
|
166
|
+
* One state namespace a workflow declares it may reach. The engine mints the
|
|
167
|
+
* run's grant from exactly this list, so an undeclared namespace is not
|
|
168
|
+
* undocumented — it is refused at run time.
|
|
169
|
+
*/
|
|
170
|
+
export interface WorkflowStateNamespace {
|
|
171
|
+
namespace: string;
|
|
172
|
+
role: WorkflowStateRole;
|
|
173
|
+
/** Absent means `private`, which is what the engine's schema defaults to. */
|
|
174
|
+
visibility?: WorkflowStateVisibility;
|
|
175
|
+
}
|
|
161
176
|
export interface WorkflowBlob {
|
|
162
177
|
nodeManifestVersion: string;
|
|
163
178
|
name: LocalizedString;
|
|
164
179
|
description?: LocalizedString;
|
|
165
180
|
variables?: Record<string, WorkflowVariable>;
|
|
181
|
+
/** State namespaces this workflow may reach at run time (BlobDef `state`). */
|
|
182
|
+
state?: WorkflowStateNamespace[];
|
|
166
183
|
triggers: WorkflowTrigger[];
|
|
167
184
|
nodes: WorkflowNode[];
|
|
168
185
|
edges: WorkflowEdge[];
|
|
@@ -247,11 +264,25 @@ export interface NodeManifestOutput {
|
|
|
247
264
|
* publishes them. Author-time schema only, never live run data. */
|
|
248
265
|
fields?: string[] | Record<string, unknown>;
|
|
249
266
|
}
|
|
250
|
-
export type ConfigFieldType = 'string' | 'number' | 'boolean' | 'select' | 'multiselect' | 'object' | 'array' | 'expression' | 'secret-ref' | 'credentials-ref'
|
|
267
|
+
export type ConfigFieldType = 'string' | 'number' | 'boolean' | 'select' | 'multiselect' | 'object' | 'array' | 'expression' | 'secret-ref' | 'credentials-ref'
|
|
268
|
+
/** Names one of the workflow's declared state namespaces (PO-374). */
|
|
269
|
+
| 'state-ref' | 'dynamic-schema';
|
|
251
270
|
export interface ConfigFieldOption {
|
|
252
271
|
value: string | number | boolean;
|
|
253
272
|
label: LocalizedString;
|
|
254
273
|
}
|
|
274
|
+
/** The comparison words a settings condition may use. One vocabulary with the
|
|
275
|
+
* condition and filter nodes, so an author who has met these words in one
|
|
276
|
+
* place does not learn a second set for the other. */
|
|
277
|
+
export type SettingConditionOperator = 'equals' | 'notEquals' | 'contains' | 'notContains' | 'startsWith' | 'endsWith' | 'greaterThan' | 'greaterThanOrEqual' | 'lessThan' | 'lessThanOrEqual' | 'exists' | 'notExists' | 'isEmpty' | 'isNotEmpty';
|
|
278
|
+
/** A condition on another setting's value, deciding whether this one applies. */
|
|
279
|
+
export interface SettingCondition {
|
|
280
|
+
/** The `key` of another setting at the same level. */
|
|
281
|
+
key: string;
|
|
282
|
+
op: SettingConditionOperator;
|
|
283
|
+
/** Left out by the four operators that read presence rather than content. */
|
|
284
|
+
value?: string | number | boolean;
|
|
285
|
+
}
|
|
255
286
|
export interface ConfigField {
|
|
256
287
|
key: string;
|
|
257
288
|
label: LocalizedString;
|
|
@@ -261,6 +292,11 @@ export interface ConfigField {
|
|
|
261
292
|
default?: unknown;
|
|
262
293
|
placeholder?: LocalizedString;
|
|
263
294
|
expressionAllowed?: boolean;
|
|
295
|
+
/** The setting takes several lines — a mail body, a list of instructions one
|
|
296
|
+
* per line — so it is drawn with room for them. Read for `string` and
|
|
297
|
+
* `expression` fields; every other type ignores it (a dropdown has no
|
|
298
|
+
* multi-line meaning, and the JSON fallback is already a text area). */
|
|
299
|
+
multiline?: boolean;
|
|
264
300
|
options?: ConfigFieldOption[];
|
|
265
301
|
/** When true, this field's `options` are resolved at author-time via
|
|
266
302
|
* config:resolve with target === this field's `key` (see useNodeConfigResolve). */
|
|
@@ -273,8 +309,20 @@ export interface ConfigField {
|
|
|
273
309
|
* (e.g. 'revenexx:business-central'). An array lets a node accept more than
|
|
274
310
|
* one credential type; the picker merges the tenant's credentials across all. */
|
|
275
311
|
credentialType?: string | string[];
|
|
312
|
+
/**
|
|
313
|
+
* Only for `type: 'state-ref'` (PO-374): which kind of state namespace the
|
|
314
|
+
* field accepts. The picker lists the workflow's declarations of that role;
|
|
315
|
+
* the stored value is the namespace NAME.
|
|
316
|
+
*/
|
|
317
|
+
stateRole?: WorkflowStateRole;
|
|
276
318
|
/** keys of other config fields this field's options/schema depend on. */
|
|
277
319
|
dependsOn?: string[];
|
|
320
|
+
/** When this setting applies at all (PO-410): another setting's key, an
|
|
321
|
+
* operator, and where the operator takes one, the value to compare against.
|
|
322
|
+
* Drawn only while the condition holds — see utils/settingCondition. Not
|
|
323
|
+
* `dependsOn`, which decides what a setting resolves to rather than whether
|
|
324
|
+
* it is drawn. */
|
|
325
|
+
showIf?: SettingCondition;
|
|
278
326
|
validation?: {
|
|
279
327
|
pattern?: string;
|
|
280
328
|
min?: number;
|
|
@@ -60,5 +60,25 @@ export declare function apiFieldErrors(err: unknown): Record<string, string>;
|
|
|
60
60
|
* one — and it counts things no form draws. A surface that shows every field-keyed
|
|
61
61
|
* message at its field does not need the number at all; what it needs is the reason
|
|
62
62
|
* (PO-275, reported from use).
|
|
63
|
+
*
|
|
64
|
+
* **It answers for a load as well as a refusal, and that is why it is not called
|
|
65
|
+
* `refusalReason` any more.** Six composables carried a byte-identical private
|
|
66
|
+
* `captureError` for the load case, and forty-odd call sites spelled the cast inline —
|
|
67
|
+
* one contract, three implementations, and only this one knew about the suffix. The
|
|
68
|
+
* strip is a no-op on a load failure, because nothing but a `422` carries a count; what
|
|
69
|
+
* the load case gains is the two lines above it, which it never had: a whitespace-only
|
|
70
|
+
* message now falls back instead of drawing blank.
|
|
71
|
+
*
|
|
72
|
+
* The `instanceof IntegrationsApiError` branch every `captureError` opened with is gone
|
|
73
|
+
* rather than moved. It read `if (e instanceof IntegrationsApiError) return e.message`
|
|
74
|
+
* above a line that reaches `e.message` anyway — the class extends `Error`, and `??`
|
|
75
|
+
* does not fire on a non-empty string — so it never changed an answer for any input.
|
|
76
|
+
*
|
|
77
|
+
* Three other sites kept that shape with this function in the `else`, and there it stopped
|
|
78
|
+
* being harmless the moment the two branches differed: `integrationsApi` wraps **every**
|
|
79
|
+
* failure in `IntegrationsApiError`, so the guard always won and the count was never
|
|
80
|
+
* stripped on a node-config resolve, a schedules load or a run's detail panel. They are
|
|
81
|
+
* gone too. The shape is worth recognising: an `instanceof` guard in front of this
|
|
82
|
+
* function reintroduces exactly the split it exists to remove.
|
|
63
83
|
*/
|
|
64
|
-
export declare function
|
|
84
|
+
export declare function errorReason(err: unknown, fallback: string): string;
|
|
@@ -33,7 +33,7 @@ export function apiFieldErrors(err) {
|
|
|
33
33
|
}
|
|
34
34
|
return out;
|
|
35
35
|
}
|
|
36
|
-
export function
|
|
36
|
+
export function errorReason(err, fallback) {
|
|
37
37
|
const raw = err?.message?.trim();
|
|
38
38
|
if (!raw) return fallback;
|
|
39
39
|
return raw.replace(/\s*\(and \d+ more errors?\)\s*$/, "").trim() || fallback;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Watching a queued bundle build out, on any surface that reports one.
|
|
3
|
+
*
|
|
4
|
+
* A save queues a build, and until the platform has finished it the workflow cannot
|
|
5
|
+
* run. Both surfaces that say so read the state off the workflow row — the editor's
|
|
6
|
+
* chip beside Save, the build column in the workflows list — so neither of them
|
|
7
|
+
* learns that a build finished unless the row is read again.
|
|
8
|
+
*
|
|
9
|
+
* Two things can make that happen. A realtime build report, which is the fast path
|
|
10
|
+
* and is not always there: the topics have no producer in every environment, the
|
|
11
|
+
* connection can be off for a tenant, and a socket can drop in the second after the
|
|
12
|
+
* save. And re-reading the row, which is this — the floor under the fast path, and
|
|
13
|
+
* the only one of the two that works everywhere today.
|
|
14
|
+
*
|
|
15
|
+
* The editor has had it since PO-398, where a chip that never cleared was reported
|
|
16
|
+
* as "saving does not work"; the list kept the same defect for as long as it had no
|
|
17
|
+
* loop of its own (PO-416). So the loop lives here rather than in either surface:
|
|
18
|
+
* one interval, one ceiling, and one rule about which call wins, for both.
|
|
19
|
+
*
|
|
20
|
+
* WHAT A CALLER SUPPLIES is what differs between the two surfaces, and it is only
|
|
21
|
+
* ever these two things: whether anything is still unsettled — one workflow in the
|
|
22
|
+
* editor, any row in the list — and how to read it again. `settle()` decides
|
|
23
|
+
* nothing about the answer; the caller's own state is what every surface renders
|
|
24
|
+
* from.
|
|
25
|
+
*
|
|
26
|
+
* A GENERATION RATHER THAN A BUSY FLAG. Two saves in quick succession must not
|
|
27
|
+
* leave the second one unwatched, and the earlier loop cannot be relied on to cover
|
|
28
|
+
* it: a read already in flight can land after the newer save and put the pre-save
|
|
29
|
+
* row back into the cache, after which the older loop reads `ready` and stops. So
|
|
30
|
+
* the newest call wins and every older loop retires at its next turn.
|
|
31
|
+
*
|
|
32
|
+
* THE CEILING is not a timeout in the usual sense — nothing is reported when it is
|
|
33
|
+
* reached. A build is seconds; a build still queued after five minutes is a state
|
|
34
|
+
* worth leaving on screen rather than asking about forever, and the operator's own
|
|
35
|
+
* reload starts a fresh watch (which is why arriving on an unsettled row starts one).
|
|
36
|
+
*/
|
|
37
|
+
/** Between reads. Same cadence the editor's queued-run wait uses. */
|
|
38
|
+
export declare const BUILD_SETTLE_POLL_MS = 2500;
|
|
39
|
+
/** After this, the state stands as last read and nothing asks again. */
|
|
40
|
+
export declare const BUILD_SETTLE_TIMEOUT_MS: number;
|
|
41
|
+
/**
|
|
42
|
+
* Whether a build state is one that changes on its own.
|
|
43
|
+
*
|
|
44
|
+
* `ready` and `failed` are where a build lands; anything else — including a row
|
|
45
|
+
* whose state the platform did not send — is nothing to wait for.
|
|
46
|
+
*/
|
|
47
|
+
export declare function isBuildUnsettled(status?: string | null): boolean;
|
|
48
|
+
export interface BuildSettleWatch {
|
|
49
|
+
/** Whether anything is still worth waiting for. Read fresh on every turn. */
|
|
50
|
+
unsettled: () => boolean;
|
|
51
|
+
/** Reads the state again. Its own failures are its business — see below. */
|
|
52
|
+
read: () => Promise<unknown>;
|
|
53
|
+
}
|
|
54
|
+
export interface BuildSettleOptions {
|
|
55
|
+
pollMs?: number;
|
|
56
|
+
timeoutMs?: number;
|
|
57
|
+
}
|
|
58
|
+
export interface BuildSettle {
|
|
59
|
+
/**
|
|
60
|
+
* Reads until nothing is unsettled, or until the ceiling. Resolves either way and
|
|
61
|
+
* throws nothing: a read that fails leaves the state as it stands and the next
|
|
62
|
+
* turn tries again, which is the same answer a surface gives its reader anyway.
|
|
63
|
+
*/
|
|
64
|
+
settle: (watch: BuildSettleWatch) => Promise<void>;
|
|
65
|
+
/** Retires whatever is watching — for a surface being left. */
|
|
66
|
+
stop: () => void;
|
|
67
|
+
}
|
|
68
|
+
export declare function createBuildSettle(options?: BuildSettleOptions): BuildSettle;
|