@revenexx/studio-integrations 1.0.0 → 1.1.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/CHANGELOG.md +254 -0
- package/dist/module.json +1 -1
- package/dist/runtime/components/Integrations/DeadLettersPanel.vue +2 -6
- package/dist/runtime/components/Integrations/ExpressionInput.vue +33 -4
- package/dist/runtime/components/Integrations/NodeInspector.vue +40 -0
- package/dist/runtime/components/Integrations/RunActionsMenu.vue +3 -2
- package/dist/runtime/components/Integrations/RunDetailPanel.vue +9 -6
- package/dist/runtime/components/Integrations/RunsTable.vue +15 -3
- package/dist/runtime/components/Integrations/WorkflowEditor.vue +27 -1
- package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue +9 -1
- package/dist/runtime/components/Integrations/WorkflowNodeView.d.vue.ts +13 -0
- package/dist/runtime/components/Integrations/WorkflowNodeView.vue +38 -3
- package/dist/runtime/components/Integrations/WorkflowNodeView.vue.d.ts +13 -0
- package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +8 -6
- package/dist/runtime/components/Integrations/WorkflowsTable.vue +9 -4
- package/dist/runtime/composables/useIntegrationsDeadLetters.d.ts +9 -2
- package/dist/runtime/composables/useIntegrationsDeadLetters.js +27 -18
- package/dist/runtime/composables/useIntegrationsRuns.d.ts +30 -1
- package/dist/runtime/composables/useIntegrationsRuns.js +70 -29
- package/dist/runtime/composables/useIntegrationsSchedules.d.ts +7 -15
- package/dist/runtime/composables/useIntegrationsSchedules.js +80 -35
- package/dist/runtime/composables/useIntegrationsTriggers.d.ts +8 -1
- package/dist/runtime/data/loopBody.d.ts +129 -0
- package/dist/runtime/data/loopBody.js +76 -0
- package/dist/runtime/i18n/messages/de.json +46 -33
- package/dist/runtime/i18n/messages/en.json +13 -0
- package/dist/runtime/pages/integrations/index.vue +29 -19
- package/dist/runtime/pages/integrations/runs.vue +22 -10
- package/dist/runtime/pages/integrations/schedules.vue +64 -7
- package/dist/runtime/pages/integrations/workflows/[id].vue +22 -8
- package/dist/runtime/types/integrations.d.ts +39 -0
- package/dist/runtime/utils/statusStyle.d.ts +76 -0
- package/dist/runtime/utils/statusStyle.js +27 -0
- package/package.json +1 -1
|
@@ -9,6 +9,19 @@ interface NodeData extends WorkflowNode {
|
|
|
9
9
|
* injected by the editor the same non-persisting way. Read-only canvases
|
|
10
10
|
* (run replay, history preview) never pass them, so nothing is marked there. */
|
|
11
11
|
__invalidFields?: string[];
|
|
12
|
+
/**
|
|
13
|
+
* Where this node stands with respect to an iterator's loop body (PO-277),
|
|
14
|
+
* computed per sync from the blob's edges (`data/loopBody.ts`) and injected
|
|
15
|
+
* like the two above. Unlike them it reaches ALL THREE canvases: what repeats
|
|
16
|
+
* is a property of the workflow rather than of editing it, and a run replay is
|
|
17
|
+
* where the repetition is most real.
|
|
18
|
+
*/
|
|
19
|
+
__loop?: {
|
|
20
|
+
/** The iterators whose body this node is in, outermost first, by label. */
|
|
21
|
+
inside?: string[];
|
|
22
|
+
/** Set on an iterator itself: how many nodes hang off its `item` port. */
|
|
23
|
+
bodySize?: number;
|
|
24
|
+
};
|
|
12
25
|
}
|
|
13
26
|
type __VLS_Props = {
|
|
14
27
|
id: string;
|
|
@@ -3,7 +3,7 @@ import { computed, inject, nextTick, ref, watch } from "vue";
|
|
|
3
3
|
import { Handle, Position, useVueFlow } from "@vue-flow/core";
|
|
4
4
|
import { NodeToolbar } from "@vue-flow/node-toolbar";
|
|
5
5
|
import * as Lucide from "lucide-vue-next";
|
|
6
|
-
import { Copy, DangerTriangle as AlertTriangle, RefreshCircle as Loader2, TrashBinMinimalistic as TrashBin } from "#solar-icons";
|
|
6
|
+
import { Copy, DangerTriangle as AlertTriangle, RefreshCircle as Loader2, Repeat, TrashBinMinimalistic as TrashBin } from "#solar-icons";
|
|
7
7
|
import { NODE_CATEGORIES, findManifest, isVersionExecutable } from "../../data/integrationNodeCatalog";
|
|
8
8
|
import { isTerminalManifest } from "../../data/nodeInputData";
|
|
9
9
|
import { resolveDisplayName, resolveLocalized } from "../../utils/localize";
|
|
@@ -22,6 +22,12 @@ const title = computed(
|
|
|
22
22
|
);
|
|
23
23
|
const invalidFields = computed(() => props.data.__invalidFields ?? []);
|
|
24
24
|
const invalidLabel = computed(() => t("integration.canvas.node.badge.invalidConfig", invalidFields.value.length, { fields: invalidFields.value.join(", ") }));
|
|
25
|
+
const insideLoops = computed(() => props.data.__loop?.inside ?? []);
|
|
26
|
+
const repeatsLabel = computed(() => t("integration.canvas.node.badge.repeats", insideLoops.value.length, { iterators: insideLoops.value.join(", ") }));
|
|
27
|
+
const bodySize = computed(() => props.data.__loop?.bodySize);
|
|
28
|
+
const bodySizeLabel = computed(() => t("integration.canvas.node.badge.loopBody", bodySize.value ?? 0));
|
|
29
|
+
const loopHighlight = inject("workflow-loop-highlight", null);
|
|
30
|
+
const inSelectedLoop = computed(() => !!loopHighlight?.value.has(props.id));
|
|
25
31
|
const notes = computed(() => (props.data.notes ?? "").trim());
|
|
26
32
|
const noteOpen = ref(false);
|
|
27
33
|
const noteReady = ref(false);
|
|
@@ -264,7 +270,7 @@ function remove() {
|
|
|
264
270
|
@mouseenter="onHoverEnter"
|
|
265
271
|
@mouseleave="onHoverLeave"
|
|
266
272
|
:class="[
|
|
267
|
-
selected ? 'border-emerald-500/50 shadow-[0_0_0_2px_rgba(16,185,129,0.18)]' : invalidFields.length ? 'border-amber-500/70 shadow-[0_0_0_2px_rgba(245,158,11,0.22)] hover:shadow-md' : 'border-black/[0.08] shadow-sm hover:shadow-md hover:border-black/[0.16]',
|
|
273
|
+
selected ? 'border-emerald-500/50 shadow-[0_0_0_2px_rgba(16,185,129,0.18)]' : invalidFields.length ? 'border-amber-500/70 shadow-[0_0_0_2px_rgba(245,158,11,0.22)] hover:shadow-md' : inSelectedLoop ? 'border-slate-400/80 shadow-[0_0_0_2px_rgba(100,116,139,0.20)]' : 'border-black/[0.08] shadow-sm hover:shadow-md hover:border-black/[0.16]',
|
|
268
274
|
showPortLabels ? 'pb-3.5' : ''
|
|
269
275
|
]"
|
|
270
276
|
>
|
|
@@ -275,7 +281,7 @@ function remove() {
|
|
|
275
281
|
a bare count at a corner is a riddle, so each carries its own title;
|
|
276
282
|
`nodrag` keeps that hover from starting a card drag. -->
|
|
277
283
|
<span
|
|
278
|
-
v-if="invalidFields.length || unbuildableVersion"
|
|
284
|
+
v-if="invalidFields.length || unbuildableVersion || insideLoops.length || bodySize !== void 0"
|
|
279
285
|
class="nodrag absolute -left-2 -top-2 z-10 flex items-center gap-1"
|
|
280
286
|
>
|
|
281
287
|
<span
|
|
@@ -305,6 +311,35 @@ function remove() {
|
|
|
305
311
|
>
|
|
306
312
|
<AlertTriangle :size="9" weight="Bold" />
|
|
307
313
|
</span>
|
|
314
|
+
<!-- Repetition (PO-277). Slate, not amber or rose: a loop is how the
|
|
315
|
+
workflow is built, not something wrong with it. The glyph is the
|
|
316
|
+
mark; the count next to it is the depth, printed from 2 up because
|
|
317
|
+
"repeats" is the news at one loop and "repeats twice over" is the
|
|
318
|
+
news above it. Both marks carry their sentence as their accessible
|
|
319
|
+
name — a bare arrow at a corner is a riddle. -->
|
|
320
|
+
<span
|
|
321
|
+
v-if="insideLoops.length"
|
|
322
|
+
role="img"
|
|
323
|
+
:aria-label="repeatsLabel"
|
|
324
|
+
:title="repeatsLabel"
|
|
325
|
+
class="flex cursor-help items-center gap-0.5 rounded-full bg-slate-600 px-1.5 py-0.5 text-[9px] font-bold leading-none text-white shadow-sm"
|
|
326
|
+
>
|
|
327
|
+
<Repeat :size="9" weight="Bold" />
|
|
328
|
+
<template v-if="insideLoops.length > 1">{{ insideLoops.length }}</template>
|
|
329
|
+
</span>
|
|
330
|
+
<!-- The loop itself, and how much of the canvas it repeats. Outlined
|
|
331
|
+
rather than filled: it names the same fact from the other end, and two
|
|
332
|
+
solid slate pills side by side on a nested iterator would read as one. -->
|
|
333
|
+
<span
|
|
334
|
+
v-if="bodySize !== void 0"
|
|
335
|
+
role="img"
|
|
336
|
+
:aria-label="bodySizeLabel"
|
|
337
|
+
:title="bodySizeLabel"
|
|
338
|
+
class="flex cursor-help items-center gap-0.5 rounded-full border border-slate-300 bg-white px-1.5 py-0.5 text-[9px] font-bold leading-none text-slate-600 shadow-sm"
|
|
339
|
+
>
|
|
340
|
+
<Repeat :size="9" weight="Bold" />
|
|
341
|
+
{{ bodySize }}
|
|
342
|
+
</span>
|
|
308
343
|
</span>
|
|
309
344
|
<!-- Always-visible edit affordance: single click opens the config lightbox.
|
|
310
345
|
(Double-click on the whole card works too.) Hidden in read-only hosts
|
|
@@ -9,6 +9,19 @@ interface NodeData extends WorkflowNode {
|
|
|
9
9
|
* injected by the editor the same non-persisting way. Read-only canvases
|
|
10
10
|
* (run replay, history preview) never pass them, so nothing is marked there. */
|
|
11
11
|
__invalidFields?: string[];
|
|
12
|
+
/**
|
|
13
|
+
* Where this node stands with respect to an iterator's loop body (PO-277),
|
|
14
|
+
* computed per sync from the blob's edges (`data/loopBody.ts`) and injected
|
|
15
|
+
* like the two above. Unlike them it reaches ALL THREE canvases: what repeats
|
|
16
|
+
* is a property of the workflow rather than of editing it, and a run replay is
|
|
17
|
+
* where the repetition is most real.
|
|
18
|
+
*/
|
|
19
|
+
__loop?: {
|
|
20
|
+
/** The iterators whose body this node is in, outermost first, by label. */
|
|
21
|
+
inside?: string[];
|
|
22
|
+
/** Set on an iterator itself: how many nodes hang off its `item` port. */
|
|
23
|
+
bodySize?: number;
|
|
24
|
+
};
|
|
12
25
|
}
|
|
13
26
|
type __VLS_Props = {
|
|
14
27
|
id: string;
|
|
@@ -24,9 +24,10 @@ import StickyNoteView from "./StickyNoteView.vue";
|
|
|
24
24
|
import GroupFrameView from "./GroupFrameView.vue";
|
|
25
25
|
import WorkflowEdgeView from "./WorkflowEdgeView.vue";
|
|
26
26
|
import { findManifest } from "../../data/integrationNodeCatalog";
|
|
27
|
+
import { nodeLoopData } from "../../data/loopBody";
|
|
27
28
|
import { triggerGlyph } from "../../data/triggerGlyphs";
|
|
28
29
|
import { SAFETY_POLL_MS } from "../../utils/pollBudget";
|
|
29
|
-
import {
|
|
30
|
+
import { runStatusClass, runStatusLabel } from "../../utils/statusStyle";
|
|
30
31
|
const props = defineProps({
|
|
31
32
|
workflow: { type: Object, required: true },
|
|
32
33
|
initialRunId: { type: [String, null], required: false },
|
|
@@ -236,6 +237,7 @@ function replayNodeClass(id, type) {
|
|
|
236
237
|
const active = activeStep.value?.node_id === id ? " replay-node-active" : "";
|
|
237
238
|
return `${tone}${reached ? "" : " replay-unreached"}${active}`;
|
|
238
239
|
}
|
|
240
|
+
const loops = computed(() => nodeLoopData(props.workflow.blob.nodes, props.workflow.blob.edges));
|
|
239
241
|
const vfNodes = computed(() => {
|
|
240
242
|
const blob = props.workflow.blob;
|
|
241
243
|
const triggerPos = (id, i) => blob.triggerPositions?.[id] ?? { x: -40, y: 60 + i * 140 };
|
|
@@ -274,7 +276,7 @@ const vfNodes = computed(() => {
|
|
|
274
276
|
position: n.position ?? { x: 240, y: 80 },
|
|
275
277
|
draggable: false,
|
|
276
278
|
class: replayNodeClass(n.id, "workflow"),
|
|
277
|
-
data: { ...n, __resolvedOutputs: dynamicOutputs.value[n.id] }
|
|
279
|
+
data: { ...n, __resolvedOutputs: dynamicOutputs.value[n.id], __loop: loops.value.get(n.id) }
|
|
278
280
|
}))
|
|
279
281
|
];
|
|
280
282
|
});
|
|
@@ -471,7 +473,7 @@ watch(() => props.initialRunId, (id) => {
|
|
|
471
473
|
<div class="absolute top-3 left-3 z-10 flex items-center gap-2 rounded-lg border border-black/[0.08] bg-white/95 px-2.5 py-1.5 text-sm shadow-sm backdrop-blur-sm">
|
|
472
474
|
<Play :size="13" class="text-muted-foreground" />
|
|
473
475
|
<span class="font-medium">{{ selectedRunId ? t("integration.replay.canvas.runLabel", { id: selectedRunId }) : t("integration.replay.canvas.pickRun") }}</span>
|
|
474
|
-
<Badge v-if="runStatus" variant="outline" class="border text-xs font-medium uppercase" :class="
|
|
476
|
+
<Badge v-if="runStatus" variant="outline" class="border text-xs font-medium uppercase" :class="runStatusClass(runStatus)">
|
|
475
477
|
{{ runStatusLabel(runStatus) }}
|
|
476
478
|
</Badge>
|
|
477
479
|
<Badge v-if="stepsSource" variant="outline" class="border text-xs font-medium uppercase text-muted-foreground">
|
|
@@ -535,7 +537,7 @@ watch(() => props.initialRunId, (id) => {
|
|
|
535
537
|
<template v-else>{{ t("integration.replay.debug.noStepSelected") }}</template>
|
|
536
538
|
</div>
|
|
537
539
|
</div>
|
|
538
|
-
<Badge v-if="activeStep" variant="outline" class="shrink-0 border text-xs font-medium uppercase" :class="
|
|
540
|
+
<Badge v-if="activeStep" variant="outline" class="shrink-0 border text-xs font-medium uppercase" :class="runStatusClass(activeStep.status)">
|
|
539
541
|
{{ runStatusLabel(activeStep.status) }}
|
|
540
542
|
</Badge>
|
|
541
543
|
</div>
|
|
@@ -613,8 +615,8 @@ watch(() => props.initialRunId, (id) => {
|
|
|
613
615
|
<component :is="triggerIcon(r.triggerType)" :size="12" class="shrink-0 text-muted-foreground" />
|
|
614
616
|
<span class="truncate">#{{ r.id }}</span>
|
|
615
617
|
</span>
|
|
616
|
-
<Badge variant="outline" class="shrink-0 border text-xs font-medium uppercase" :class="
|
|
617
|
-
{{ r.status }}
|
|
618
|
+
<Badge variant="outline" class="shrink-0 border text-xs font-medium uppercase" :class="runStatusClass(r.status)">
|
|
619
|
+
{{ runStatusLabel(r.status) }}
|
|
618
620
|
</Badge>
|
|
619
621
|
</div>
|
|
620
622
|
<div class="mt-0.5 flex items-center justify-between gap-2 text-sm text-muted-foreground">
|
|
@@ -50,13 +50,18 @@ watch(groupByTags, (on) => {
|
|
|
50
50
|
} catch {
|
|
51
51
|
}
|
|
52
52
|
});
|
|
53
|
-
const {
|
|
53
|
+
const {
|
|
54
|
+
latestPerWorkflow,
|
|
55
|
+
latestLoaded: runsLoaded,
|
|
56
|
+
loadLatestPerWorkflow,
|
|
57
|
+
lastRunFor: latestRunFor
|
|
58
|
+
} = useIntegrationsRuns();
|
|
54
59
|
onMounted(() => {
|
|
55
|
-
void
|
|
60
|
+
void loadLatestPerWorkflow();
|
|
56
61
|
});
|
|
57
62
|
function lastRunFor(id) {
|
|
58
|
-
void
|
|
59
|
-
return
|
|
63
|
+
void latestPerWorkflow.value.length;
|
|
64
|
+
return latestRunFor(id);
|
|
60
65
|
}
|
|
61
66
|
function runBadge(status) {
|
|
62
67
|
switch (status) {
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import type { DeadLetterRow } from '../types/integrations.js';
|
|
2
|
-
/**
|
|
2
|
+
/**
|
|
3
|
+
* A dead letter as the tenant-wide list serves it: the platform now reports the
|
|
4
|
+
* workflow's name on the row, where the fan-out had to inject it from the loop
|
|
5
|
+
* variable.
|
|
6
|
+
*/
|
|
3
7
|
export interface GlobalDeadLetterRow extends DeadLetterRow {
|
|
4
|
-
|
|
8
|
+
workflow_name?: string | null;
|
|
5
9
|
}
|
|
6
10
|
export declare function useIntegrationsDeadLetters(): {
|
|
7
11
|
list: (workflowId: number | string, opts?: {
|
|
@@ -18,5 +22,8 @@ export declare function useIntegrationsDeadLetters(): {
|
|
|
18
22
|
loadGlobal: (force?: boolean) => Promise<GlobalDeadLetterRow[]>;
|
|
19
23
|
globalItems: any;
|
|
20
24
|
globalLoading: any;
|
|
25
|
+
globalLoaded: any;
|
|
26
|
+
globalHasMore: any;
|
|
27
|
+
summary: any;
|
|
21
28
|
pendingCount: import("vue").ComputedRef<any>;
|
|
22
29
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { computed } from "vue";
|
|
2
2
|
import { useIntegrationsApi } from "./integrationsApi.js";
|
|
3
3
|
import { useLoadGate } from "./useLoadGate.js";
|
|
4
|
+
const EMPTY_SUMMARY = { total: 0, by_status: {} };
|
|
4
5
|
export function useIntegrationsDeadLetters() {
|
|
5
6
|
const api = useIntegrationsApi();
|
|
6
7
|
async function list(workflowId, opts = {}) {
|
|
@@ -18,34 +19,42 @@ export function useIntegrationsDeadLetters() {
|
|
|
18
19
|
}
|
|
19
20
|
const replay = (id) => api.post(`/dead-letters/${id}/replay`, {});
|
|
20
21
|
const discard = (id) => api.post(`/dead-letters/${id}/discard`, {});
|
|
21
|
-
const { workflows, load: loadWorkflows, loaded: workflowsLoaded } = useIntegrationsWorkflows();
|
|
22
22
|
const globalItems = useState("integrations.deadletters.global", () => []);
|
|
23
23
|
const globalLoading = useState("integrations.deadletters.global.loading", () => false);
|
|
24
24
|
const globalLoaded = useState("integrations.deadletters.global.loaded", () => false);
|
|
25
25
|
const gate = useLoadGate("integrations.deadletters.global", { loading: globalLoading, loaded: globalLoaded });
|
|
26
|
-
const
|
|
26
|
+
const summary = useState("integrations.deadletters.summary", () => ({ ...EMPTY_SUMMARY }));
|
|
27
|
+
const globalHasMore = useState("integrations.deadletters.global.hasMore", () => false);
|
|
28
|
+
const pendingCount = computed(() => summary.value.by_status.pending ?? 0);
|
|
27
29
|
async function loadGlobal(force = false) {
|
|
28
30
|
if (!await gate.claim(force)) return globalItems.value;
|
|
29
31
|
try {
|
|
30
|
-
await
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
return out;
|
|
42
|
-
}));
|
|
43
|
-
globalItems.value = per.flatMap((p) => p.status === "fulfilled" ? p.value : []).sort((a, b) => new Date(b.created_at ?? 0).getTime() - new Date(a.created_at ?? 0).getTime());
|
|
44
|
-
globalLoaded.value = workflowsLoaded.value && per.every((p) => p.status === "fulfilled");
|
|
32
|
+
const [page, counts] = await Promise.all([
|
|
33
|
+
api.get("/dead-letters?page=1&per_page=100"),
|
|
34
|
+
api.get("/dead-letters/summary")
|
|
35
|
+
]);
|
|
36
|
+
globalItems.value = page?.data ?? [];
|
|
37
|
+
globalHasMore.value = page?.meta?.has_more ?? false;
|
|
38
|
+
summary.value = counts?.data ?? { ...EMPTY_SUMMARY };
|
|
39
|
+
globalLoaded.value = true;
|
|
40
|
+
} catch (e) {
|
|
41
|
+
globalLoaded.value = false;
|
|
42
|
+
throw e;
|
|
45
43
|
} finally {
|
|
46
44
|
gate.release();
|
|
47
45
|
}
|
|
48
46
|
return globalItems.value;
|
|
49
47
|
}
|
|
50
|
-
return {
|
|
48
|
+
return {
|
|
49
|
+
list,
|
|
50
|
+
replay,
|
|
51
|
+
discard,
|
|
52
|
+
loadGlobal,
|
|
53
|
+
globalItems,
|
|
54
|
+
globalLoading,
|
|
55
|
+
globalLoaded,
|
|
56
|
+
globalHasMore,
|
|
57
|
+
summary,
|
|
58
|
+
pendingCount
|
|
59
|
+
};
|
|
51
60
|
}
|
|
@@ -1,13 +1,36 @@
|
|
|
1
1
|
import type { RunStatus, RunStepRow, WorkflowRun } from '../types/integrations.js';
|
|
2
|
+
interface RunSummary {
|
|
3
|
+
total: number;
|
|
4
|
+
in_flight: number;
|
|
5
|
+
by_status: Record<string, number>;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Which page of the tenant's list to read.
|
|
9
|
+
*
|
|
10
|
+
* Only the page: `/runs` also narrows by status, trigger kind, workflow, window
|
|
11
|
+
* and text, and none of that is driven from here yet. The shared DataTable
|
|
12
|
+
* narrows and searches what it holds, and it cannot report a filter selection
|
|
13
|
+
* back to the page under `serverMode` — so a narrowing sent to the platform
|
|
14
|
+
* would have nothing to set it. That is the remaining half of PO-231, and the
|
|
15
|
+
* parameters are waiting on the endpoint for it.
|
|
16
|
+
*/
|
|
17
|
+
export interface RunFeedQuery {
|
|
18
|
+
page: number;
|
|
19
|
+
perPage: number;
|
|
20
|
+
}
|
|
2
21
|
export declare function useIntegrationsRuns(): {
|
|
3
22
|
runs: any;
|
|
4
23
|
loading: any;
|
|
5
24
|
error: any;
|
|
6
25
|
loaded: any;
|
|
7
26
|
total: import("vue").ComputedRef<any>;
|
|
8
|
-
failedCount: import("vue").ComputedRef<
|
|
27
|
+
failedCount: import("vue").ComputedRef<number>;
|
|
9
28
|
runningCount: import("vue").ComputedRef<any>;
|
|
29
|
+
summary: any;
|
|
30
|
+
hasMore: any;
|
|
31
|
+
query: any;
|
|
10
32
|
load: (force?: boolean) => Promise<WorkflowRun[]>;
|
|
33
|
+
loadSummary: () => Promise<RunSummary>;
|
|
11
34
|
loadForWorkflow: (workflowId: number, workflowName: string, opts?: {
|
|
12
35
|
reconcile?: boolean;
|
|
13
36
|
page?: number;
|
|
@@ -24,6 +47,11 @@ export declare function useIntegrationsRuns(): {
|
|
|
24
47
|
}) => WorkflowRun[];
|
|
25
48
|
get: (id: string) => WorkflowRun | null;
|
|
26
49
|
fetchRun: (workflowId: number | string, runId: number | string, workflowName: string) => Promise<WorkflowRun>;
|
|
50
|
+
latestPerWorkflow: any;
|
|
51
|
+
latestLoaded: any;
|
|
52
|
+
latestLoading: any;
|
|
53
|
+
loadLatestPerWorkflow: (force?: boolean) => Promise<WorkflowRun[]>;
|
|
54
|
+
lastRunFor: (workflowId: number) => WorkflowRun | null;
|
|
27
55
|
trigger: (workflowId: number | string, payload?: Record<string, unknown>, mode?: "async" | "sync") => Promise<unknown>;
|
|
28
56
|
cancelRun: (workflowId: number | string, runId: number | string, reason?: string) => Promise<unknown>;
|
|
29
57
|
terminateRun: (workflowId: number | string, runId: number | string, reason?: string) => Promise<unknown>;
|
|
@@ -49,3 +77,4 @@ export declare function useIntegrationsRuns(): {
|
|
|
49
77
|
}>;
|
|
50
78
|
getStackTrace: (workflowId: number | string, runId: number | string) => Promise<string>;
|
|
51
79
|
};
|
|
80
|
+
export {};
|
|
@@ -3,6 +3,7 @@ import { useIntegrationsApi } from "./integrationsApi.js";
|
|
|
3
3
|
import { useIntegrationsT } from "./useIntegrationsT.js";
|
|
4
4
|
import { IntegrationsApiError } from "../utils/apiError.js";
|
|
5
5
|
import { useLoadGate } from "./useLoadGate.js";
|
|
6
|
+
const EMPTY_SUMMARY = { total: 0, in_flight: 0, by_status: {} };
|
|
6
7
|
function mapStatus(status) {
|
|
7
8
|
switch (status) {
|
|
8
9
|
case "completed":
|
|
@@ -31,22 +32,30 @@ export function useIntegrationsRuns() {
|
|
|
31
32
|
const error = useState("integrations.runs.error", () => null);
|
|
32
33
|
const loaded = useState("integrations.runs.loaded", () => false);
|
|
33
34
|
const reconciling = useState("integrations.runs.reconciling", () => false);
|
|
35
|
+
const summary = useState("integrations.runs.summary", () => ({ ...EMPTY_SUMMARY }));
|
|
36
|
+
const hasMore = useState("integrations.runs.hasMore", () => false);
|
|
37
|
+
const query = useState("integrations.runs.query", () => ({
|
|
38
|
+
page: 1,
|
|
39
|
+
perPage: 100
|
|
40
|
+
}));
|
|
34
41
|
const gate = useLoadGate("integrations.runs", { loading, loaded });
|
|
35
42
|
const SYNC_LIMIT = 5;
|
|
36
43
|
const api = useIntegrationsApi();
|
|
37
44
|
const t = useIntegrationsT();
|
|
38
|
-
|
|
39
|
-
function mapRun(r, workflowName) {
|
|
45
|
+
function mapRun(r, workflowName = "") {
|
|
40
46
|
const startedMs = r.started_at ? new Date(r.started_at).getTime() : null;
|
|
41
47
|
const finishedMs = r.completed_at ? new Date(r.completed_at).getTime() : null;
|
|
42
48
|
return {
|
|
43
49
|
id: String(r.id),
|
|
44
50
|
workflowId: r.workflow_id,
|
|
45
|
-
workflowName,
|
|
51
|
+
workflowName: r.workflow_name ?? workflowName,
|
|
46
52
|
status: mapStatus(r.status),
|
|
47
53
|
temporalRunId: r.temporal_run_id ?? null,
|
|
48
|
-
triggerId: "",
|
|
49
|
-
|
|
54
|
+
triggerId: r.trigger_handle ?? "",
|
|
55
|
+
// The platform reports what started the run. This was hardcoded to
|
|
56
|
+
// `manual` because the payload carried nothing, which left the trigger
|
|
57
|
+
// column and its filter with one populated value in production.
|
|
58
|
+
triggerType: r.trigger_type ?? "manual",
|
|
50
59
|
startedAt: r.started_at ?? "",
|
|
51
60
|
finishedAt: r.completed_at ?? null,
|
|
52
61
|
// The list carries the reason a run failed; dropping it here was why every
|
|
@@ -63,25 +72,14 @@ export function useIntegrationsRuns() {
|
|
|
63
72
|
if (!await gate.claim(force)) return runs.value;
|
|
64
73
|
error.value = null;
|
|
65
74
|
try {
|
|
66
|
-
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
`/workflows/${wf.id}/runs?page=${page}&per_page=100`
|
|
75
|
-
);
|
|
76
|
-
out.push(...(res?.data ?? []).map((r) => mapRun(r, wf.name)));
|
|
77
|
-
const last = res?.meta?.last_page ?? 1;
|
|
78
|
-
if (page >= last) break;
|
|
79
|
-
page++;
|
|
80
|
-
}
|
|
81
|
-
return out;
|
|
82
|
-
})
|
|
83
|
-
);
|
|
84
|
-
runs.value = perWorkflow.flatMap((p) => p.status === "fulfilled" ? p.value : []).sort((a, b) => new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime());
|
|
75
|
+
const { page: pageNumber, perPage } = query.value;
|
|
76
|
+
const [page, counts] = await Promise.all([
|
|
77
|
+
api.get(`/runs?page=${pageNumber}&per_page=${perPage}`),
|
|
78
|
+
api.get("/runs/summary")
|
|
79
|
+
]);
|
|
80
|
+
runs.value = (page?.data ?? []).map((r) => mapRun(r));
|
|
81
|
+
hasMore.value = page?.meta?.has_more ?? false;
|
|
82
|
+
summary.value = counts?.data ?? { ...EMPTY_SUMMARY };
|
|
85
83
|
if (!reconciling.value) {
|
|
86
84
|
reconciling.value = true;
|
|
87
85
|
try {
|
|
@@ -99,9 +97,10 @@ export function useIntegrationsRuns() {
|
|
|
99
97
|
reconciling.value = false;
|
|
100
98
|
}
|
|
101
99
|
}
|
|
102
|
-
loaded.value =
|
|
100
|
+
loaded.value = true;
|
|
103
101
|
} catch (e) {
|
|
104
102
|
error.value = e instanceof IntegrationsApiError ? e.message : e?.message ?? t("integration.runs.error.loadFailed");
|
|
103
|
+
loaded.value = false;
|
|
105
104
|
} finally {
|
|
106
105
|
gate.release();
|
|
107
106
|
}
|
|
@@ -137,7 +136,40 @@ export function useIntegrationsRuns() {
|
|
|
137
136
|
let result = [...runs.value];
|
|
138
137
|
if (filter?.workflowId !== void 0) result = result.filter((r) => r.workflowId === filter.workflowId);
|
|
139
138
|
if (filter?.status) result = result.filter((r) => r.status === filter.status);
|
|
140
|
-
return result
|
|
139
|
+
return result;
|
|
140
|
+
}
|
|
141
|
+
async function loadSummary() {
|
|
142
|
+
try {
|
|
143
|
+
const res = await api.get("/runs/summary");
|
|
144
|
+
summary.value = res?.data ?? { ...EMPTY_SUMMARY };
|
|
145
|
+
} catch (e) {
|
|
146
|
+
error.value = e instanceof IntegrationsApiError ? e.message : e?.message ?? t("integration.runs.error.loadFailed");
|
|
147
|
+
}
|
|
148
|
+
return summary.value;
|
|
149
|
+
}
|
|
150
|
+
const latestPerWorkflow = useState("integrations.runs.latest", () => []);
|
|
151
|
+
const latestLoaded = useState("integrations.runs.latest.loaded", () => false);
|
|
152
|
+
const latestLoading = useState("integrations.runs.latest.loading", () => false);
|
|
153
|
+
const latestGate = useLoadGate("integrations.runs.latest", {
|
|
154
|
+
loading: latestLoading,
|
|
155
|
+
loaded: latestLoaded
|
|
156
|
+
});
|
|
157
|
+
async function loadLatestPerWorkflow(force = false) {
|
|
158
|
+
if (!await latestGate.claim(force)) return latestPerWorkflow.value;
|
|
159
|
+
try {
|
|
160
|
+
const res = await api.get("/runs/latest-per-workflow");
|
|
161
|
+
latestPerWorkflow.value = (res?.data ?? []).map((r) => mapRun(r));
|
|
162
|
+
latestLoaded.value = true;
|
|
163
|
+
} catch (e) {
|
|
164
|
+
error.value = e instanceof IntegrationsApiError ? e.message : e?.message ?? t("integration.runs.error.loadFailed");
|
|
165
|
+
latestLoaded.value = false;
|
|
166
|
+
} finally {
|
|
167
|
+
latestGate.release();
|
|
168
|
+
}
|
|
169
|
+
return latestPerWorkflow.value;
|
|
170
|
+
}
|
|
171
|
+
function lastRunFor(workflowId) {
|
|
172
|
+
return latestPerWorkflow.value.find((r) => r.workflowId === workflowId) ?? null;
|
|
141
173
|
}
|
|
142
174
|
function get(id) {
|
|
143
175
|
return runs.value.find((r) => r.id === id) ?? null;
|
|
@@ -175,9 +207,9 @@ export function useIntegrationsRuns() {
|
|
|
175
207
|
return api.get(`/workflows/${workflowId}/runs/${runId}/history${suffix}`);
|
|
176
208
|
};
|
|
177
209
|
const getStackTrace = (workflowId, runId) => api.get(`/workflows/${workflowId}/runs/${runId}/stack-trace`).then((r) => r.stack_trace);
|
|
178
|
-
const total = computed(() =>
|
|
179
|
-
const failedCount = computed(() =>
|
|
180
|
-
const runningCount = computed(() =>
|
|
210
|
+
const total = computed(() => summary.value.total);
|
|
211
|
+
const failedCount = computed(() => Object.entries(summary.value.by_status).filter(([status]) => mapStatus(status) === "failed").reduce((sum, [, count]) => sum + count, 0));
|
|
212
|
+
const runningCount = computed(() => summary.value.in_flight);
|
|
181
213
|
return {
|
|
182
214
|
runs,
|
|
183
215
|
loading,
|
|
@@ -186,11 +218,20 @@ export function useIntegrationsRuns() {
|
|
|
186
218
|
total,
|
|
187
219
|
failedCount,
|
|
188
220
|
runningCount,
|
|
221
|
+
summary,
|
|
222
|
+
hasMore,
|
|
223
|
+
query,
|
|
189
224
|
load,
|
|
225
|
+
loadSummary,
|
|
190
226
|
loadForWorkflow,
|
|
191
227
|
list,
|
|
192
228
|
get,
|
|
193
229
|
fetchRun,
|
|
230
|
+
latestPerWorkflow,
|
|
231
|
+
latestLoaded,
|
|
232
|
+
latestLoading,
|
|
233
|
+
loadLatestPerWorkflow,
|
|
234
|
+
lastRunFor,
|
|
194
235
|
trigger,
|
|
195
236
|
cancelRun,
|
|
196
237
|
terminateRun,
|
|
@@ -1,19 +1,11 @@
|
|
|
1
1
|
import type { ScheduleEntry } from '../types/integrations.js';
|
|
2
|
-
/**
|
|
3
|
-
* Derived view — every trigger of type `schedule` across all workflows, as
|
|
4
|
-
* `useIntegrationsTriggers` holds them: read out of the workflow list, which carries
|
|
5
|
-
* each workflow's triggers, rather than fetched per workflow (PO-236).
|
|
6
|
-
* Next/last-run timestamps are mocked deterministically (no telemetry
|
|
7
|
-
* endpoint yet).
|
|
8
|
-
*
|
|
9
|
-
* Unordered on purpose: the page's table sorts, by its own default and by
|
|
10
|
-
* whichever column the operator clicks, so an order established here would be
|
|
11
|
-
* replaced before anyone saw it.
|
|
12
|
-
*/
|
|
13
2
|
export declare function useIntegrationsSchedules(): {
|
|
14
|
-
entries:
|
|
15
|
-
total: import("vue").ComputedRef<
|
|
16
|
-
activeCount: import("vue").ComputedRef<
|
|
3
|
+
entries: any;
|
|
4
|
+
total: import("vue").ComputedRef<any>;
|
|
5
|
+
activeCount: import("vue").ComputedRef<any>;
|
|
6
|
+
timezonesInUse: import("vue").ComputedRef<any>;
|
|
17
7
|
loading: any;
|
|
18
|
-
|
|
8
|
+
loaded: any;
|
|
9
|
+
error: any;
|
|
10
|
+
load: (force?: boolean) => Promise<ScheduleEntry[]>;
|
|
19
11
|
};
|