@revenexx/studio-integrations 1.0.0 → 1.1.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 +217 -0
- package/dist/module.json +1 -1
- package/dist/runtime/components/Integrations/DeadLettersPanel.vue +2 -6
- package/dist/runtime/components/Integrations/NodeInspector.vue +40 -0
- package/dist/runtime/components/Integrations/RunDetailPanel.vue +9 -6
- package/dist/runtime/components/Integrations/RunsTable.vue +2 -2
- 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 +42 -30
- package/dist/runtime/i18n/messages/en.json +12 -0
- package/dist/runtime/pages/integrations/index.vue +16 -17
- package/dist/runtime/pages/integrations/runs.vue +22 -10
- package/dist/runtime/pages/integrations/schedules.vue +64 -7
- package/dist/runtime/types/integrations.d.ts +39 -0
- package/dist/runtime/utils/statusStyle.d.ts +52 -0
- package/dist/runtime/utils/statusStyle.js +26 -0
- package/package.json +1 -1
|
@@ -2,14 +2,23 @@
|
|
|
2
2
|
import { ClockCircle as Clock, Globus as Globe } from "#solar-icons";
|
|
3
3
|
import { Badge } from "@revenexx/studio";
|
|
4
4
|
import { cadenceRank, describeCron } from "../../utils/schedule";
|
|
5
|
+
import { runStatusLabel } from "../../utils/statusStyle";
|
|
5
6
|
const t = useIntegrationsT();
|
|
6
7
|
useHead({ title: () => t("integration.schedules.head.title") });
|
|
7
8
|
const router = useRouter();
|
|
8
|
-
const { entries, activeCount, loading, load } = useIntegrationsSchedules();
|
|
9
|
+
const { entries, activeCount, timezonesInUse, loading, error, load } = useIntegrationsSchedules();
|
|
9
10
|
onMounted(() => {
|
|
10
11
|
void load(true);
|
|
11
12
|
});
|
|
12
|
-
function
|
|
13
|
+
function inScheduleZone(iso, timezone) {
|
|
14
|
+
const parts = { month: "short", day: "2-digit", hour: "2-digit", minute: "2-digit" };
|
|
15
|
+
try {
|
|
16
|
+
return new Date(iso).toLocaleString(void 0, { ...parts, timeZone: timezone });
|
|
17
|
+
} catch {
|
|
18
|
+
return new Date(iso).toLocaleString(void 0, parts);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function formatRelative(iso, timezone) {
|
|
13
22
|
if (!iso) return "\u2014";
|
|
14
23
|
const ms = new Date(iso).getTime() - Date.now();
|
|
15
24
|
if (Math.abs(ms) < 6e4) return ms > 0 ? t("integration.schedules.time.inLessThanMinute") : t("integration.common.time.justNow");
|
|
@@ -21,7 +30,11 @@ function formatRelative(iso) {
|
|
|
21
30
|
const hrs = Math.round(Math.abs(ms) / 36e5);
|
|
22
31
|
return ms > 0 ? t("integration.schedules.time.inHours", hrs) : t("integration.common.time.hoursAgo", hrs);
|
|
23
32
|
}
|
|
24
|
-
return
|
|
33
|
+
return inScheduleZone(iso, timezone);
|
|
34
|
+
}
|
|
35
|
+
function lastRunText(e) {
|
|
36
|
+
if (e.lastRunAt) return formatRelative(e.lastRunAt, e.timezone);
|
|
37
|
+
return e.lastRunStatus ? runStatusLabel(e.lastRunStatus) : "\u2014";
|
|
25
38
|
}
|
|
26
39
|
const filterTabs = computed(() => [
|
|
27
40
|
{
|
|
@@ -40,7 +53,10 @@ const filterTabs = computed(() => [
|
|
|
40
53
|
key: "timezone",
|
|
41
54
|
label: t("integration.schedules.table.column.timezone"),
|
|
42
55
|
getter: (e) => e.timezone,
|
|
43
|
-
|
|
56
|
+
// The zones the platform reports as in use across the tenant, not the ones
|
|
57
|
+
// visible in the rows: with the list narrowed, deriving them from the rows
|
|
58
|
+
// would shrink the menu to what the previous choice left standing.
|
|
59
|
+
options: timezonesInUse.value.map((tz) => ({ value: tz, label: tz }))
|
|
44
60
|
}
|
|
45
61
|
]);
|
|
46
62
|
const columns = computed(() => [
|
|
@@ -76,7 +92,25 @@ const columns = computed(() => [
|
|
|
76
92
|
</template>
|
|
77
93
|
</PageHeader>
|
|
78
94
|
|
|
95
|
+
<!--
|
|
96
|
+
A refused read says so. Without this the page fell through to the table's
|
|
97
|
+
own empty text, so a 503 and a tenant with no schedules were one screen —
|
|
98
|
+
and the message it needed was already written and translated
|
|
99
|
+
(`specs/schedules.md` AC-13). An alert rather than a bare div, so it is
|
|
100
|
+
announced and not only visible.
|
|
101
|
+
-->
|
|
102
|
+
<div v-if="error" role="alert" class="m-4 rounded-md border border-rose-500/20 bg-rose-500/5 px-3 py-2 text-sm text-rose-700">
|
|
103
|
+
{{ error }}
|
|
104
|
+
</div>
|
|
105
|
+
|
|
79
106
|
<div class="p-4">
|
|
107
|
+
<!--
|
|
108
|
+
The empty text answers the error too, so the table cannot go on saying
|
|
109
|
+
"No schedules yet" underneath a banner explaining that there was no
|
|
110
|
+
answer to count. Two renderings of one fact rather than two facts: the
|
|
111
|
+
banner carries the platform's own words, which may be specific, and this
|
|
112
|
+
is why the list is blank.
|
|
113
|
+
-->
|
|
80
114
|
<DataTable
|
|
81
115
|
:loading="loading"
|
|
82
116
|
:data="entries"
|
|
@@ -85,7 +119,7 @@ const columns = computed(() => [
|
|
|
85
119
|
:default-sort="{ column: 'nextRun', direction: 'asc' }"
|
|
86
120
|
:search="{ placeholder: t('integration.schedules.search.placeholder') }"
|
|
87
121
|
:filter-tabs="filterTabs"
|
|
88
|
-
:empty-text="t('integration.schedules.state.empty')"
|
|
122
|
+
:empty-text="error ? t('integration.schedules.error.loadFailed') : t('integration.schedules.state.empty')"
|
|
89
123
|
@row-click="(e2) => router.push(`/integrations/workflows/${e2.workflowId}`)"
|
|
90
124
|
>
|
|
91
125
|
<template #cell-workflow="{ row: e }">
|
|
@@ -143,12 +177,35 @@ const columns = computed(() => [
|
|
|
143
177
|
semantics in the shared table (PO-233, and a gap in
|
|
144
178
|
`specs/accessibility.md`).
|
|
145
179
|
-->
|
|
180
|
+
<!--
|
|
181
|
+
The instant the platform holds, shown as a distance from now, with the
|
|
182
|
+
moment in the schedule's own zone as its title: "in 6d" answers when,
|
|
183
|
+
and "2026-08-24T08:30:00+02:00" answers which side of a clock change it
|
|
184
|
+
falls on. Neither is worked out here — a browser doing summer-time
|
|
185
|
+
arithmetic that a wrong answer makes worse than no answer is exactly
|
|
186
|
+
what this replaced.
|
|
187
|
+
-->
|
|
146
188
|
<template #cell-nextRun="{ row: e }">
|
|
147
|
-
<span
|
|
189
|
+
<span
|
|
190
|
+
:aria-label="t('integration.schedules.table.column.nextRun')"
|
|
191
|
+
:title="e.nextRunLocal ?? void 0"
|
|
192
|
+
class="text-sm tabular-nums"
|
|
193
|
+
>{{ formatRelative(e.nextRunAt, e.timezone) }}</span>
|
|
148
194
|
</template>
|
|
149
195
|
|
|
196
|
+
<!--
|
|
197
|
+
The last run carries a title too, and it is the instant rather than a
|
|
198
|
+
zone reading: the platform reports no local form for a run the way it
|
|
199
|
+
does for a firing, and inventing one here is what this page stopped
|
|
200
|
+
doing. Without it a run further back than a day was readable to the
|
|
201
|
+
minute and to no greater precision than the cell's own text.
|
|
202
|
+
-->
|
|
150
203
|
<template #cell-lastRun="{ row: e }">
|
|
151
|
-
<span
|
|
204
|
+
<span
|
|
205
|
+
:aria-label="t('integration.schedules.table.column.lastRun')"
|
|
206
|
+
:title="e.lastRunAt ?? void 0"
|
|
207
|
+
class="text-sm tabular-nums text-muted-foreground"
|
|
208
|
+
>{{ lastRunText(e) }}</span>
|
|
152
209
|
</template>
|
|
153
210
|
|
|
154
211
|
<template #cell-status="{ row: e }">
|
|
@@ -339,6 +339,18 @@ export interface NodeManifest {
|
|
|
339
339
|
/** Empty marks a terminal node (PO-201): the run ends there, so the canvas
|
|
340
340
|
* draws no output handle and no edge may leave it. */
|
|
341
341
|
outputs: NodeManifestOutput[];
|
|
342
|
+
/**
|
|
343
|
+
* When true, the node runs whatever is wired to its `item` output once per
|
|
344
|
+
* element, and its `done` output once afterwards — so the canvas marks that
|
|
345
|
+
* part as repeating (PO-277, `specs/editor-loop-body.md`).
|
|
346
|
+
*
|
|
347
|
+
* **Nothing publishes it yet** (later phase; harmless now, like
|
|
348
|
+
* `resolveOutputs` above). The worker derives the same fact at bundle-build
|
|
349
|
+
* time by duck-typing the implementation, and no field of `GET /nodes` carries
|
|
350
|
+
* it, so until one does the editor falls back to the published iterator's slug
|
|
351
|
+
* — `ITERATOR_SLUGS` in `data/loopBody.ts`, which this field is here to retire.
|
|
352
|
+
*/
|
|
353
|
+
iteration?: boolean;
|
|
342
354
|
config?: ConfigField[];
|
|
343
355
|
/** Source package (editor-side enrichment from GET /nodes; not manifest data). */
|
|
344
356
|
__package?: {
|
|
@@ -453,6 +465,12 @@ export interface WebhookEntry {
|
|
|
453
465
|
lastFiredAt: string | null;
|
|
454
466
|
callCount24h: number;
|
|
455
467
|
}
|
|
468
|
+
/**
|
|
469
|
+
* Why a schedule holds no next firing. Mirrors the conditions under which the
|
|
470
|
+
* platform's scheduler would never select it, so a paused schedule and one whose
|
|
471
|
+
* expression nobody can read stop reading the same way.
|
|
472
|
+
*/
|
|
473
|
+
export type ScheduleNotFiringReason = 'trigger_paused' | 'workflow_inactive' | 'workflow_not_ready' | 'no_cron' | 'invalid_cron' | 'unknown_timezone' | 'unsatisfiable';
|
|
456
474
|
export interface ScheduleEntry {
|
|
457
475
|
id: string;
|
|
458
476
|
workflowId: number;
|
|
@@ -462,8 +480,29 @@ export interface ScheduleEntry {
|
|
|
462
480
|
cron: string | null;
|
|
463
481
|
timezone: string;
|
|
464
482
|
active: boolean;
|
|
483
|
+
/** Whether the workflow the schedule belongs to is active. */
|
|
484
|
+
workflowActive: boolean;
|
|
485
|
+
/**
|
|
486
|
+
* The next firing the platform holds, as an instant, or `null` when it holds
|
|
487
|
+
* none. Not computed here: a browser working it out from the expression has to
|
|
488
|
+
* do summer-time arithmetic that a wrong answer makes worse than no answer.
|
|
489
|
+
*/
|
|
465
490
|
nextRunAt: string | null;
|
|
491
|
+
/** The same moment in the schedule's own zone, offset included. */
|
|
492
|
+
nextRunLocal: string | null;
|
|
493
|
+
/** When the schedule last actually ran — `null` when it never has. */
|
|
466
494
|
lastRunAt: string | null;
|
|
495
|
+
/** How that run ended. */
|
|
496
|
+
lastRunStatus: string | null;
|
|
497
|
+
/**
|
|
498
|
+
* When the scheduler last DECIDED to fire this schedule. Stamped before the
|
|
499
|
+
* run exists, so it can differ from `lastRunAt` — a firing that was dispatched
|
|
500
|
+
* and then lost.
|
|
501
|
+
*/
|
|
502
|
+
lastDispatchAt: string | null;
|
|
503
|
+
/** False whenever the platform would not fire this schedule. */
|
|
504
|
+
holdsFirings: boolean;
|
|
505
|
+
notFiringReason: ScheduleNotFiringReason | null;
|
|
467
506
|
}
|
|
468
507
|
export type TemplateCategory = 'sync' | 'notification' | 'enrichment' | 'fulfilment' | 'reporting' | 'finance' | 'commerce' | 'sales';
|
|
469
508
|
export interface WorkflowTemplate {
|
|
@@ -42,4 +42,56 @@ export declare const RUN_STATUS_WORDS: readonly ["running", "succeeded", "failed
|
|
|
42
42
|
* never disagree about which status they mean.
|
|
43
43
|
*/
|
|
44
44
|
export declare function normalisedRunStatus(status: string | null | undefined): string;
|
|
45
|
+
/**
|
|
46
|
+
* The classes a run's status badge is drawn with — the colour half of
|
|
47
|
+
* {@link runStatusLabel}, and it reads the same word.
|
|
48
|
+
*
|
|
49
|
+
* `statusClass` on its own is handed whatever the platform wrote, and the map it
|
|
50
|
+
* looks in knows the studio's vocabulary rather than Temporal's: `terminated` and
|
|
51
|
+
* `timed_out` are in no branch of it, so a run the operator terminated was drawn
|
|
52
|
+
* neutral — the colour a *cancelled* run wears — while its badge said "failed".
|
|
53
|
+
* Two runs reading the same, in two colours, because the word went through
|
|
54
|
+
* {@link normalisedRunStatus} and the colour did not.
|
|
55
|
+
*
|
|
56
|
+
* So this normalises first. Every surface drawing a run's state calls it rather
|
|
57
|
+
* than `statusClass`, which stays what it is: one map from a status *word* to a
|
|
58
|
+
* tone, for the many vocabularies across the cockpit that already speak it.
|
|
59
|
+
*/
|
|
60
|
+
export declare function runStatusClass(status: string | null | undefined): string;
|
|
45
61
|
export declare function runStatusLabel(status: string | null | undefined): string;
|
|
62
|
+
/**
|
|
63
|
+
* A missed run's state, as a word written for the operator.
|
|
64
|
+
*
|
|
65
|
+
* The counterpart to {@link runStatusLabel} for the entry a trigger firing leaves
|
|
66
|
+
* when its workflow could not run. A state the catalogue does not know is named as
|
|
67
|
+
* unknown and carries the platform's own value inside that frame, so an operator
|
|
68
|
+
* can still quote what arrived without the studio claiming the word as one of its
|
|
69
|
+
* own. Nothing in, nothing out — the same answer `runStatusLabel` gives, for a
|
|
70
|
+
* column that is never actually empty.
|
|
71
|
+
*
|
|
72
|
+
* **The match is exact, unlike `runStatusLabel`'s**, and that is the point rather
|
|
73
|
+
* than an omission. Everything else that reads this field compares it strictly —
|
|
74
|
+
* whether an entry offers replay and discard, and whether it counts towards the
|
|
75
|
+
* firings still waiting — so a value differing by case or a stray space is one the
|
|
76
|
+
* studio does not act on. Saying "waiting" for it would promise an action that is
|
|
77
|
+
* not offered; saying it is unknown is what the rest of the studio already
|
|
78
|
+
* believes.
|
|
79
|
+
*
|
|
80
|
+
* The frame carries the value with its surrounding whitespace off and nothing else
|
|
81
|
+
* touched. Not verbatim, twice over, and neither is worth chasing: the badge is
|
|
82
|
+
* HTML, which collapses whitespace, and it draws its text uppercased — so an
|
|
83
|
+
* operator reads the letters that arrived rather than the casing, exactly as they
|
|
84
|
+
* do for the three states the studio does have words for.
|
|
85
|
+
*/
|
|
86
|
+
export declare function deadLetterStatusLabel(status: string | null | undefined): string;
|
|
87
|
+
/**
|
|
88
|
+
* The classes a missed run's state badge is drawn with, on every surface that lists
|
|
89
|
+
* one.
|
|
90
|
+
*
|
|
91
|
+
* The colour half of {@link deadLetterStatusLabel}, reading the same map by the same
|
|
92
|
+
* exact match, so the two cannot disagree about which states the studio knows: a
|
|
93
|
+
* state named as unknown is drawn as unknown. Tones come from the shared palette,
|
|
94
|
+
* so this is one more vocabulary mapped into the studio's colours rather than a
|
|
95
|
+
* second set of colours.
|
|
96
|
+
*/
|
|
97
|
+
export declare function deadLetterStatusClass(status: string | null | undefined): string;
|
|
@@ -89,13 +89,39 @@ export function normalisedRunStatus(status) {
|
|
|
89
89
|
case "terminated":
|
|
90
90
|
case "timed_out":
|
|
91
91
|
return "failed";
|
|
92
|
+
// Temporal spells a cancellation the American way and the studio does not, so
|
|
93
|
+
// without this the *word* half of the promise fails on the surfaces that read
|
|
94
|
+
// the platform's answer directly: the row said `cancelled`, the record
|
|
95
|
+
// `canceled`, and in German the record said it in English.
|
|
96
|
+
case "canceled":
|
|
97
|
+
return "cancelled";
|
|
92
98
|
default:
|
|
93
99
|
return s;
|
|
94
100
|
}
|
|
95
101
|
}
|
|
102
|
+
export function runStatusClass(status) {
|
|
103
|
+
return statusClass(normalisedRunStatus(status));
|
|
104
|
+
}
|
|
96
105
|
export function runStatusLabel(status) {
|
|
97
106
|
const normalized = normalisedRunStatus(status);
|
|
98
107
|
if (!normalized) return "";
|
|
99
108
|
if (!RUN_STATUS_WORD_SET.has(normalized)) return normalized;
|
|
100
109
|
return useIntegrationsT()(`integration.runs.status.${normalized}`);
|
|
101
110
|
}
|
|
111
|
+
const MISSED_RUN_STATUS_TONE = /* @__PURE__ */ new Map([
|
|
112
|
+
["pending", "warning"],
|
|
113
|
+
["replayed", "success"],
|
|
114
|
+
["discarded", "neutral"]
|
|
115
|
+
]);
|
|
116
|
+
export function deadLetterStatusLabel(status) {
|
|
117
|
+
if (!status) return "";
|
|
118
|
+
const raw = String(status);
|
|
119
|
+
if (!MISSED_RUN_STATUS_TONE.has(raw)) {
|
|
120
|
+
return useIntegrationsT()("integration.deadLetters.status.unknown", { status: raw.trim() || raw });
|
|
121
|
+
}
|
|
122
|
+
return useIntegrationsT()(`integration.deadLetters.status.${raw}`);
|
|
123
|
+
}
|
|
124
|
+
export function deadLetterStatusClass(status) {
|
|
125
|
+
const tone = status ? MISSED_RUN_STATUS_TONE.get(String(status)) : void 0;
|
|
126
|
+
return toneClass(tone ?? "neutral");
|
|
127
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@revenexx/studio-integrations",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Integration Studio (core: workflows, node editor, runs, schedules, credentials, secrets, audit) as a standalone, publishable Nuxt module.",
|
|
6
6
|
"repository": {
|