@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
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,259 @@
|
|
|
1
1
|
# @revenexx/studio-integrations
|
|
2
2
|
|
|
3
|
+
## 1.1.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 77ea454: A workflow's Runs tab no longer puts the operator back on the newest page while
|
|
8
|
+
they are paging. The tab's own refresh used to consult the page that last arrived
|
|
9
|
+
rather than the page that was asked for, so a refresh falling due between "show me
|
|
10
|
+
the older runs" and those runs arriving won the race and undid the click. It now
|
|
11
|
+
leaves a pending page alone, and a refresh that was already on its way no longer
|
|
12
|
+
draws over the page the operator asked for.
|
|
13
|
+
- ddb95eb: The `{ }` button beside a node setting is no longer announced as its own glyph.
|
|
14
|
+
It now carries a name saying what it does and what it opens ("Insert expression —
|
|
15
|
+
available data for this setting", and the German equivalent), reports whether the
|
|
16
|
+
list of available data is open, and points at that list while it is there.
|
|
17
|
+
|
|
18
|
+
It can also be operated from the keyboard at all now: it acted on a mouse press
|
|
19
|
+
alone, so Enter and Space did nothing, and Escape while it had focus closed the whole
|
|
20
|
+
node configuration dialog instead of the list. Nothing changes for a pointer.
|
|
21
|
+
|
|
22
|
+
- ec8f025: A run row no longer offers actions the platform will refuse. A run that has been
|
|
23
|
+
accepted but not started yet has no execution to act on, and the API answers a
|
|
24
|
+
cancel, a terminate or a resume for it with a 409 — so the row now withholds all
|
|
25
|
+
three, the way the run's own record already does, instead of turning the click into
|
|
26
|
+
an error toast.
|
|
27
|
+
|
|
28
|
+
Two German words on the same menu are fixed with it: the row's "Retry" and the
|
|
29
|
+
record's were shipped untranslated, and the entry that opens a run's replay said
|
|
30
|
+
"Wiederholen", which reads as an offer to run it again. They now read "Erneut
|
|
31
|
+
ausführen" and "Wiedergabe" — the word the replay surface itself already uses.
|
|
32
|
+
|
|
33
|
+
- 0de6228: A run's outcome is no longer cut off or drawn over the workflow name beside it. The
|
|
34
|
+
Status column in both lists of runs — the Runs page and the dashboard's latest runs —
|
|
35
|
+
was a fixed width measured against the English words, so the German
|
|
36
|
+
"fehlgeschlagen" ran out of its cell; the dashboard's own column was narrower still,
|
|
37
|
+
and even "erfolgreich" overflowed it. Both now share one track that keeps a floor and
|
|
38
|
+
grows to the widest word on the page, in whatever language the studio is read.
|
|
39
|
+
|
|
40
|
+
## 1.1.0
|
|
41
|
+
|
|
42
|
+
### Minor Changes
|
|
43
|
+
|
|
44
|
+
- c8f121b: The Schedules page shows the firing the platform actually holds, and the run it
|
|
45
|
+
really performed.
|
|
46
|
+
|
|
47
|
+
Both times in that list were made up. There was no telemetry to read, so the page
|
|
48
|
+
computed them from the schedule's own identity — arithmetic over the workflow id
|
|
49
|
+
and the trigger id — which told the truth about the _shape_ of the answer and
|
|
50
|
+
nothing about the value. A schedule that fires nightly at 03:00 could read "in
|
|
51
|
+
18m", and one that had never run could report a last run.
|
|
52
|
+
|
|
53
|
+
They are now the platform's: the next firing it holds, with the offset of the
|
|
54
|
+
schedule's own zone available as the cell's title, and the last run it really
|
|
55
|
+
performed. The header's live count and the timezones the filter offers are its
|
|
56
|
+
counts too, taken over the tenant rather than over the rows on screen — so
|
|
57
|
+
narrowing the list no longer shrinks the menu that narrows it.
|
|
58
|
+
|
|
59
|
+
Two promises changed with them.
|
|
60
|
+
|
|
61
|
+
A paused schedule used to state neither time, which looked like one rule and was
|
|
62
|
+
really an artefact of both being invented from the same flag. It has no next
|
|
63
|
+
firing, because the platform holds none for it — and it keeps the run it performed
|
|
64
|
+
before it was paused. Forgetting that is how a paused schedule reads as one that
|
|
65
|
+
never ran.
|
|
66
|
+
|
|
67
|
+
"A distance further out than a day is stated as the date and time instead" was a
|
|
68
|
+
promise with nothing to prove it: the fabricated times stayed inside twelve hours,
|
|
69
|
+
so no fixture could produce the day-scale form. A weekly schedule reaches it, and
|
|
70
|
+
it now has a test.
|
|
71
|
+
|
|
72
|
+
What the page still cannot say is _why_ a schedule will not fire. The platform
|
|
73
|
+
distinguishes six reasons beyond "somebody paused it" — the workflow is off, its
|
|
74
|
+
bundle never built, the expression cannot be read, the zone is not a real one, the
|
|
75
|
+
rhythm can never come round — and the page shows the absence of a next run and the
|
|
76
|
+
live/paused badge, which covers the first and leaves the others looking like it.
|
|
77
|
+
|
|
78
|
+
Review then found four more things a fully mocked test suite could not see, and
|
|
79
|
+
they are user-visible.
|
|
80
|
+
|
|
81
|
+
A schedule carrying no zone of its own showed a blank timezone and could be found
|
|
82
|
+
under none of the zones the filter offers — the platform counts such a row under
|
|
83
|
+
its own default and narrows to it there, so the studio puts that default back
|
|
84
|
+
rather than showing nothing.
|
|
85
|
+
|
|
86
|
+
A run the platform has created and no worker has started now says so, instead of
|
|
87
|
+
reading as a schedule that has never run: when a run _started_ is recorded only
|
|
88
|
+
once something picks it up.
|
|
89
|
+
|
|
90
|
+
A read the platform refuses says so. Until now the page fell through to the words
|
|
91
|
+
a tenant with no schedules gets, which on this page is the opposite state — an
|
|
92
|
+
operator told there are no schedules stops looking, and a tenant whose list merely
|
|
93
|
+
could not be reached is the one where something may be silently not firing.
|
|
94
|
+
|
|
95
|
+
And a date further out than a day is drawn in the schedule's own timezone, so it
|
|
96
|
+
no longer contradicts the zone stated beside it — or the exact moment on its own
|
|
97
|
+
hover — by an hour or two.
|
|
98
|
+
|
|
99
|
+
- 82884b7: The Runs page, its Missed runs tab and the Dashboard read the platform's own
|
|
100
|
+
lists and figures instead of assembling them in the browser.
|
|
101
|
+
|
|
102
|
+
Runs are a per-workflow resource, so the tenant-wide views used to collect them:
|
|
103
|
+
a request per workflow, paged to the end of each one's history — up to a hundred
|
|
104
|
+
pages of a hundred rows apiece — and then ordered, filtered and counted here. A
|
|
105
|
+
tenant's ordering and totals were therefore only ever as complete as that
|
|
106
|
+
collecting, and nothing said how far back the page reached. The same went for the
|
|
107
|
+
missed firings and for the four figures on the landing page, four of which could
|
|
108
|
+
not start until one shared list of workflows had arrived.
|
|
109
|
+
|
|
110
|
+
The platform serves all of it now. One request for the runs, ordered by it; one
|
|
111
|
+
for the counts, taken over the tenant rather than over what happened to load; one
|
|
112
|
+
for the latest run of each workflow, which is what the Workflows list and the
|
|
113
|
+
dashboard's two panels actually want. The dashboard no longer loads a run list at
|
|
114
|
+
all.
|
|
115
|
+
|
|
116
|
+
Three things visibly change rather than merely getting faster:
|
|
117
|
+
|
|
118
|
+
The trigger column and its filter tell runs apart. Every row claimed to have been
|
|
119
|
+
started by hand, because the payload carried nothing about what started it, so
|
|
120
|
+
that menu had one populated option. The platform reports it, so the column is
|
|
121
|
+
true and narrowing by it works.
|
|
122
|
+
|
|
123
|
+
The Workflows list stops being able to say "never run" about a workflow that
|
|
124
|
+
simply ran a while ago. That cell used to pick the newest run out of the tenant's
|
|
125
|
+
whole history held in memory, which only worked while all of it was loaded.
|
|
126
|
+
|
|
127
|
+
The counts are the tenant's. The Missed runs badge, the runs in flight and the
|
|
128
|
+
failed figure are counted by the platform under the same narrowing as the list
|
|
129
|
+
beside them, so a figure and its rows can no longer disagree.
|
|
130
|
+
|
|
131
|
+
What is still bounded is depth: the Runs list reaches the most recent hundred
|
|
132
|
+
runs, and narrowing reaches what is on the page. Paging past that needs the shared
|
|
133
|
+
studio table to be able to report a narrowing back to the page, which it cannot
|
|
134
|
+
yet — that is the remaining half of PO-231.
|
|
135
|
+
|
|
136
|
+
- 59133ea: The canvas says which steps a loop repeats.
|
|
137
|
+
|
|
138
|
+
An iterator draws its two outputs — Item and Done — like any other pair of ports,
|
|
139
|
+
so nothing said that everything hanging off Item runs once per element while
|
|
140
|
+
everything off Done runs once, afterwards. Somebody who had not drawn the workflow
|
|
141
|
+
could not see where the repeating part began or how far it reached.
|
|
142
|
+
|
|
143
|
+
Every step a loop repeats now carries a mark saying so, and naming the loop; a
|
|
144
|
+
step inside two loops names both, outermost first. The loop itself carries the
|
|
145
|
+
number of steps it repeats — including zero, which is what a loop nothing is wired
|
|
146
|
+
to yet looks like. Selecting the loop draws its whole body as part of the
|
|
147
|
+
selection, for as long as the selection lasts — and selecting any step of the body
|
|
148
|
+
does the same from the other end, drawing the loop and everything that repeats
|
|
149
|
+
alongside it. The marks are on all three canvases
|
|
150
|
+
that draw a workflow — the editor, the run replay and the revision preview — and
|
|
151
|
+
they follow a rewiring at once, before anything is saved.
|
|
152
|
+
|
|
153
|
+
What is marked is what the engine will actually repeat, which is not always the
|
|
154
|
+
body as it was drawn: the repeating part has no end anywhere in the workflow, only
|
|
155
|
+
a reach, so a step wired to the last step of the body runs once per element even
|
|
156
|
+
when it was meant to run once afterwards. A loop's Output tab now counts the steps
|
|
157
|
+
it repeats, names them, and says which of them are wired after the loop as well —
|
|
158
|
+
reported rather than refused, because a body that rejoins the main flow can be
|
|
159
|
+
exactly what its author wanted.
|
|
160
|
+
|
|
161
|
+
The connections themselves are unchanged: they carry importance, never meaning,
|
|
162
|
+
and colour on them stays reserved for a run that actually happened.
|
|
163
|
+
|
|
164
|
+
### Patch Changes
|
|
165
|
+
|
|
166
|
+
- 5385146: A missed run's state is said in the studio's own words.
|
|
167
|
+
|
|
168
|
+
The badge on a missed run showed the state as the platform spells it — `PENDING`,
|
|
169
|
+
`REPLAYED`, `DISCARDED` — while the badge on a run row one tab away said
|
|
170
|
+
"succeeded" or "running" in the operator's language. A German interface therefore
|
|
171
|
+
read English in this one list, and any state the platform starts recording
|
|
172
|
+
tomorrow would have appeared on screen untranslated with nothing to notice it.
|
|
173
|
+
|
|
174
|
+
Both lists that carry the entry — the tenant-wide Missed runs tab and the panel
|
|
175
|
+
inside a workflow's editor — now say the state through the message catalogue: a
|
|
176
|
+
firing still waiting, one replayed, one discarded, in English and in German. A
|
|
177
|
+
state the studio has no word for says exactly that and keeps the platform's own
|
|
178
|
+
value alongside it, so an operator can quote what arrived without the studio
|
|
179
|
+
passing it off as a word somebody chose.
|
|
180
|
+
|
|
181
|
+
The colours of those badges are unchanged, and the two surfaces still tint them
|
|
182
|
+
differently — that is a separate finding, not this one.
|
|
183
|
+
|
|
184
|
+
- 630523f: A missed run's state is drawn the same way on both lists.
|
|
185
|
+
|
|
186
|
+
The same entry was coloured differently depending on where it was opened. Beside
|
|
187
|
+
the tenant's runs it read the cockpit-wide status palette, where a waiting firing
|
|
188
|
+
is tinted like a run that is about to start and the other two states are absent
|
|
189
|
+
altogether — so _replayed_ and _discarded_ were both grey. Inside a workflow the
|
|
190
|
+
panel carried a palette of its own: amber for waiting, green for replayed, grey
|
|
191
|
+
for discarded.
|
|
192
|
+
|
|
193
|
+
Both lists now draw a state from one place: waiting asks for attention, replayed
|
|
194
|
+
reads as finished well, discarded and anything the studio has no word for read as
|
|
195
|
+
over. The colours are the shared palette's, so this is one more vocabulary mapped
|
|
196
|
+
into the studio's tones rather than a second set of colours — and the word and the
|
|
197
|
+
colour for a state now come from a single map, which is what stops the next state
|
|
198
|
+
being added to one and forgotten in the other.
|
|
199
|
+
|
|
200
|
+
Visible change: on the tenant-wide _Missed runs_ tab a waiting firing turns from
|
|
201
|
+
blue to amber and a replayed one from grey to green. The panel inside a workflow
|
|
202
|
+
keeps the colours it had, give or take the shared palette's border and text tints.
|
|
203
|
+
|
|
204
|
+
- 6e1a42e: A run's outcome is drawn the same way wherever it is shown.
|
|
205
|
+
|
|
206
|
+
A run terminated by an operator read _failed_ in a list and _failed_ at its own
|
|
207
|
+
record — in two colours. The word went through the studio's vocabulary, where the
|
|
208
|
+
platform's `terminated` becomes _failed_, and the colour did not: the palette was
|
|
209
|
+
handed the platform's word, which it has no branch for, so the badge fell back to
|
|
210
|
+
the grey a cancelled run wears. Clicking the row took the operator from a red
|
|
211
|
+
badge to a grey one for the same run.
|
|
212
|
+
|
|
213
|
+
The colour is now decided from the same word as the label, everywhere a run's or a
|
|
214
|
+
step's state is drawn: the runs list, the dashboard's latest runs, a run's record,
|
|
215
|
+
its timeline and the replay. Nothing changes on the surfaces that were already
|
|
216
|
+
right — a row's status is mapped before anything draws it — and a run whose state
|
|
217
|
+
the platform reports as `terminated` or `timed_out` now looks like the failure the
|
|
218
|
+
badge says it is.
|
|
219
|
+
|
|
220
|
+
A cancelled run was affected in the same way, in the _word_ rather than the
|
|
221
|
+
colour: the platform spells a cancellation the American way, so a run cancelled
|
|
222
|
+
from its row read _cancelled_ on the row and _canceled_ at its own record — and in
|
|
223
|
+
German the record said it in English. It now reads the studio's word wherever it
|
|
224
|
+
is shown.
|
|
225
|
+
|
|
226
|
+
One of those surfaces was worse in German than in English: the timeline's step
|
|
227
|
+
lightbox derived its colour from the word it had already translated, so a failed
|
|
228
|
+
step was drawn neutral for a German reader and red for an English one. The colour
|
|
229
|
+
is now taken from the status itself.
|
|
230
|
+
|
|
231
|
+
`statusClass` is unchanged and still exports the studio-wide status palette;
|
|
232
|
+
`runStatusClass` is the run-vocabulary door to it.
|
|
233
|
+
|
|
234
|
+
- e69e407: A run is called a Run in German too.
|
|
235
|
+
|
|
236
|
+
The German interface used two words for one object. Three quarters of the
|
|
237
|
+
catalogue said _Run_ — the sidebar item, the editor's Runs tab, the Run button, the
|
|
238
|
+
breadcrumb, every audit entry — while 29 values said _Ausführung_, including the
|
|
239
|
+
tab sitting directly beside the one that said _Runs_, and two sentences that used
|
|
240
|
+
both words in the same breath ("Noch keine Ausführungen — im Editor auf Run
|
|
241
|
+
klicken").
|
|
242
|
+
|
|
243
|
+
Those 29 now say _Run_: the editor's _Verpasste Runs_ tab, the dashboard's _Letzte
|
|
244
|
+
Runs_ panel and its missed-run count, the schedules table's _Letzter Run_ and
|
|
245
|
+
_Nächster Run_, and the toasts and hints around starting, retrying and replaying
|
|
246
|
+
one.
|
|
247
|
+
|
|
248
|
+
_Ausführung_ stays where it names the act rather than the object —
|
|
249
|
+
_Ausführungsmodus_, _Ausführungsverhalten_, _Ausführungsverlauf_,
|
|
250
|
+
_Hintergrundausführung_, and the settings section _Ausführung_. Their English says
|
|
251
|
+
_Execution_ too, which is the test for the next one: if the English says _run_, the
|
|
252
|
+
German says _Run_. The rule is now written down in `docs/interface-language.md`
|
|
253
|
+
rather than living in one catalogue entry's description.
|
|
254
|
+
|
|
255
|
+
No English copy changed, and no key was added or removed.
|
|
256
|
+
|
|
3
257
|
## 1.0.0
|
|
4
258
|
|
|
5
259
|
### Major Changes
|
package/dist/module.json
CHANGED
|
@@ -3,6 +3,7 @@ import { onMounted, ref, watch } from "vue";
|
|
|
3
3
|
import { RefreshCircle as Loader2, Refresh as ReplayIcon, TrashBinMinimalistic as TrashBin } from "#solar-icons";
|
|
4
4
|
import { Badge, Button } from "@revenexx/studio";
|
|
5
5
|
import { toast } from "vue-sonner";
|
|
6
|
+
import { deadLetterStatusClass, deadLetterStatusLabel } from "../../utils/statusStyle";
|
|
6
7
|
const props = defineProps({
|
|
7
8
|
workflowId: { type: Number, required: true }
|
|
8
9
|
});
|
|
@@ -62,11 +63,6 @@ async function onDiscard(dl) {
|
|
|
62
63
|
busyId.value = null;
|
|
63
64
|
}
|
|
64
65
|
}
|
|
65
|
-
function statusClass(status) {
|
|
66
|
-
if (status === "pending") return "bg-amber-500/15 text-amber-700 border-amber-500/25";
|
|
67
|
-
if (status === "replayed") return "bg-emerald-500/15 text-emerald-700 border-emerald-500/25";
|
|
68
|
-
return "bg-slate-500/15 text-slate-600 border-slate-500/20";
|
|
69
|
-
}
|
|
70
66
|
function fmt(iso) {
|
|
71
67
|
if (!iso) return "\u2014";
|
|
72
68
|
try {
|
|
@@ -117,7 +113,7 @@ function fmt(iso) {
|
|
|
117
113
|
:key="dl.id"
|
|
118
114
|
class="flex items-start gap-3 px-3 py-2.5"
|
|
119
115
|
>
|
|
120
|
-
<Badge variant="outline" :class="['border text-xs font-medium uppercase shrink-0 mt-0.5',
|
|
116
|
+
<Badge variant="outline" :class="['border text-xs font-medium uppercase shrink-0 mt-0.5', deadLetterStatusClass(dl.status)]">{{ deadLetterStatusLabel(dl.status) }}</Badge>
|
|
121
117
|
<div class="min-w-0 flex-1">
|
|
122
118
|
<div class="text-sm break-words">{{ dl.reason || t("integration.deadLetters.reason.fallback") }}</div>
|
|
123
119
|
<div class="mt-0.5 flex flex-wrap items-center gap-x-3 gap-y-0.5 text-xs text-muted-foreground font-mono">
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import { computed, inject, onBeforeUnmount, onMounted, ref, watch } from "vue";
|
|
2
|
+
import { computed, inject, onBeforeUnmount, onMounted, ref, useId, watch } from "vue";
|
|
3
3
|
import { Editor, EditorContent } from "@tiptap/vue-3";
|
|
4
4
|
import { Node } from "@tiptap/core";
|
|
5
5
|
import Document from "@tiptap/extension-document";
|
|
@@ -158,10 +158,16 @@ const filteredSections = computed(() => {
|
|
|
158
158
|
});
|
|
159
159
|
const open = ref(false);
|
|
160
160
|
const wrapRef = ref(null);
|
|
161
|
+
const triggerRef = ref(null);
|
|
162
|
+
const listOpen = computed(() => open.value && hasSuggestions.value);
|
|
163
|
+
const pickerId = useId();
|
|
161
164
|
function toggle() {
|
|
162
165
|
if (!open.value && !hasSuggestions.value) return;
|
|
163
166
|
open.value = !open.value;
|
|
164
167
|
}
|
|
168
|
+
function onTriggerClick(e) {
|
|
169
|
+
if (e.detail === 0) toggle();
|
|
170
|
+
}
|
|
165
171
|
function onFocusOut(e) {
|
|
166
172
|
if (wrapRef.value && e.relatedTarget instanceof globalThis.Node && wrapRef.value.contains(e.relatedTarget)) return;
|
|
167
173
|
open.value = false;
|
|
@@ -173,6 +179,13 @@ function onKeydown(e) {
|
|
|
173
179
|
open.value = false;
|
|
174
180
|
}
|
|
175
181
|
}
|
|
182
|
+
function onPickerKeydown(e) {
|
|
183
|
+
if (e.key !== "Escape" || !open.value) return;
|
|
184
|
+
e.stopPropagation();
|
|
185
|
+
e.preventDefault();
|
|
186
|
+
open.value = false;
|
|
187
|
+
triggerRef.value?.focus();
|
|
188
|
+
}
|
|
176
189
|
function onDocMousedown(e) {
|
|
177
190
|
if (wrapRef.value && !wrapRef.value.contains(e.target)) open.value = false;
|
|
178
191
|
}
|
|
@@ -188,7 +201,11 @@ function insert(expr) {
|
|
|
188
201
|
</script>
|
|
189
202
|
|
|
190
203
|
<template>
|
|
191
|
-
|
|
204
|
+
<!-- `keydown` on the widget rather than on the editor alone: with focus on the
|
|
205
|
+
trigger, Escape used to reach the configuration lightbox and close the whole
|
|
206
|
+
dialog. The editor keeps its own handler, which stops the event before it
|
|
207
|
+
arrives here. -->
|
|
208
|
+
<div ref="wrapRef" class="relative" @focusout="onFocusOut" @keydown="onKeydown">
|
|
192
209
|
<div class="flex items-center gap-1">
|
|
193
210
|
<div
|
|
194
211
|
class="expr-editor relative h-8 min-w-0 flex-1 overflow-hidden rounded-md border bg-white px-2 text-sm transition focus-within:outline-2 focus-within:-outline-offset-1 focus-within:outline-ring"
|
|
@@ -200,12 +217,22 @@ function insert(expr) {
|
|
|
200
217
|
>{{ placeholder }}</span>
|
|
201
218
|
<EditorContent :editor="editor" class="h-full" @keydown="onKeydown" />
|
|
202
219
|
</div>
|
|
220
|
+
<!-- The name says what the button does AND what it opens, because the glyph
|
|
221
|
+
says neither; `aria-expanded` says whether the list is there, and
|
|
222
|
+
`aria-controls` only while it is — an id pointing at nothing is worse
|
|
223
|
+
than no link. See the docblock on `listOpen` for why this is not the
|
|
224
|
+
shared popover. -->
|
|
203
225
|
<button
|
|
226
|
+
ref="triggerRef"
|
|
204
227
|
type="button"
|
|
205
228
|
:title="t('integration.nodeConfig.expression.insertTitle')"
|
|
229
|
+
:aria-label="t('integration.nodeConfig.expression.insertAria')"
|
|
230
|
+
:aria-expanded="listOpen"
|
|
231
|
+
:aria-controls="listOpen ? pickerId : void 0"
|
|
206
232
|
class="flex h-8 shrink-0 items-center rounded-md border border-black/[0.08] bg-white px-1.5 font-mono text-sm text-muted-foreground transition hover:text-foreground hover:bg-muted/30"
|
|
207
|
-
:class="
|
|
233
|
+
:class="listOpen ? 'ring-2 ring-emerald-500/40' : ''"
|
|
208
234
|
@mousedown.prevent="toggle"
|
|
235
|
+
@click="onTriggerClick"
|
|
209
236
|
>
|
|
210
237
|
{{ triggerLabel }}
|
|
211
238
|
</button>
|
|
@@ -224,8 +251,10 @@ function insert(expr) {
|
|
|
224
251
|
here and in the panel beside the form — and a popup nothing can address
|
|
225
252
|
is a popup nothing can test either. -->
|
|
226
253
|
<div
|
|
227
|
-
v-if="
|
|
254
|
+
v-if="listOpen"
|
|
255
|
+
:id="pickerId"
|
|
228
256
|
role="group"
|
|
257
|
+
@keydown="onPickerKeydown"
|
|
229
258
|
:aria-label="t('integration.nodeConfig.expression.picker.aria')"
|
|
230
259
|
class="absolute right-0 top-full z-50 mt-1 flex max-h-72 w-80 flex-col rounded-md border border-black/[0.08] bg-white text-sm shadow-lg"
|
|
231
260
|
>
|
|
@@ -38,6 +38,8 @@ import {
|
|
|
38
38
|
variableExpr
|
|
39
39
|
} from "../../data/nodeInputData";
|
|
40
40
|
import { portDotClass } from "../../data/portTone";
|
|
41
|
+
import { computeLoopBodies, manifestIterates } from "../../data/loopBody";
|
|
42
|
+
import { nodeDisplayLabel } from "../../utils/validationFindings";
|
|
41
43
|
import { authChoiceFromConfig, configPatchForAuthChoice } from "../../data/webhookAuth";
|
|
42
44
|
import {
|
|
43
45
|
dataTypeOf,
|
|
@@ -772,6 +774,22 @@ const outputView = ref("schema");
|
|
|
772
774
|
const selfInputs = computed(() => inputPortsForManifest(manifest.value));
|
|
773
775
|
const selfSample = computed(() => draftNode.value ? sampleFor(draftNode.value.id) : void 0);
|
|
774
776
|
const selfPorts = computed(() => draftNode.value ? withSampledFields(portsForNode(draftNode.value, manifest.value, props.dynamicOutputs?.[draftNode.value.id]), selfSample.value) : []);
|
|
777
|
+
const loopBody = computed(() => {
|
|
778
|
+
if (!draftNode.value || !manifestIterates(manifest.value)) return null;
|
|
779
|
+
const nodes = props.nodes ?? [];
|
|
780
|
+
const [body] = computeLoopBodies([draftNode.value.id], props.edges ?? []);
|
|
781
|
+
if (!body) return null;
|
|
782
|
+
const labelOf = (id) => {
|
|
783
|
+
const n = nodes.find((x) => x.id === id);
|
|
784
|
+
return n ? nodeDisplayLabel(n) : id;
|
|
785
|
+
};
|
|
786
|
+
const inBody = (ids) => nodes.filter((n) => ids.has(n.id)).map((n) => labelOf(n.id));
|
|
787
|
+
return {
|
|
788
|
+
size: body.nodeIds.size,
|
|
789
|
+
names: inBody(body.nodeIds),
|
|
790
|
+
joined: inBody(body.joinedWithDone)
|
|
791
|
+
};
|
|
792
|
+
});
|
|
775
793
|
const selfSampleJson = computed(() => selfSample.value ? JSON.stringify(selfSample.value, null, 2) : "");
|
|
776
794
|
const selfPinned = computed(() => !!draftNode.value && !!pinnedSamples.value[draftNode.value.id]);
|
|
777
795
|
watch(selfSample, (s) => {
|
|
@@ -1937,6 +1955,28 @@ async function runTestStep() {
|
|
|
1937
1955
|
rather than a permanent column — it is the last thing done, if it
|
|
1938
1956
|
is done at all, and it needs the width when it is. -->
|
|
1939
1957
|
<div v-else-if="tab === 'output' && draftNode" class="mx-auto w-full max-w-5xl space-y-5">
|
|
1958
|
+
<!-- An iterator's own tab leads with what it repeats (PO-277): the
|
|
1959
|
+
ports below say what leaves Item and Done, and this says how much
|
|
1960
|
+
of the workflow hangs off the first of them. Above the port
|
|
1961
|
+
schema on purpose — the count is the thing a reader came here to
|
|
1962
|
+
check, and it is the one statement the canvas cannot make. -->
|
|
1963
|
+
<section v-if="loopBody" class="space-y-1.5 rounded-md border border-black/[0.06] bg-white p-3">
|
|
1964
|
+
<span class="text-mono text-muted-foreground">{{ t("integration.nodeInspector.output.loopBody.heading") }}</span>
|
|
1965
|
+
<p class="text-sm text-foreground/80">
|
|
1966
|
+
{{ t("integration.nodeInspector.output.loopBody.size", loopBody.size) }}
|
|
1967
|
+
</p>
|
|
1968
|
+
<p v-if="loopBody.names.length" class="text-xs text-muted-foreground">
|
|
1969
|
+
{{ t("integration.nodeInspector.output.loopBody.names", { names: loopBody.names.join(", ") }) }}
|
|
1970
|
+
</p>
|
|
1971
|
+
<!-- Amber, not rose, and not a validation finding: a body wired
|
|
1972
|
+
into the Done side may be exactly what the author wanted, so
|
|
1973
|
+
this states what the engine will do and leaves the judgement
|
|
1974
|
+
to them. Routing it through the findings would refuse an
|
|
1975
|
+
activation that is legitimate. -->
|
|
1976
|
+
<p v-if="loopBody.joined.length" class="text-xs text-amber-700">
|
|
1977
|
+
{{ t("integration.nodeInspector.output.loopBody.joined", loopBody.joined.length, { names: loopBody.joined.join(", ") }) }}
|
|
1978
|
+
</p>
|
|
1979
|
+
</section>
|
|
1940
1980
|
<section class="space-y-2">
|
|
1941
1981
|
<div class="flex items-center justify-between gap-2">
|
|
1942
1982
|
<p class="text-xs text-muted-foreground">
|
|
@@ -23,9 +23,10 @@ function viewDetails() {
|
|
|
23
23
|
function viewReplay() {
|
|
24
24
|
void router.push({ path: `/integrations/workflows/${props.run.workflowId}`, query: { run: props.run.id, view: "replay" } });
|
|
25
25
|
}
|
|
26
|
-
const
|
|
26
|
+
const hasExecution = computed(() => !!props.run.temporalRunId);
|
|
27
|
+
const isActive = computed(() => (props.run.status === "running" || props.run.status === "queued") && hasExecution.value);
|
|
27
28
|
const isTerminal = computed(() => ["succeeded", "failed", "cancelled"].includes(props.run.status));
|
|
28
|
-
const canResume = computed(() => props.run.status === "failed");
|
|
29
|
+
const canResume = computed(() => props.run.status === "failed" && hasExecution.value);
|
|
29
30
|
const hasAny = computed(() => isActive.value || isTerminal.value);
|
|
30
31
|
const busy = ref(false);
|
|
31
32
|
function actionLabel(a) {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { computed, onUnmounted, ref, watch } from "vue";
|
|
3
3
|
import { Download as DownloadIcon, RefreshCircle as RefreshIcon } from "#solar-icons";
|
|
4
4
|
import { Badge, Button, Dialog, DialogContent, DialogHeader, DialogTitle } from "@revenexx/studio";
|
|
5
|
-
import {
|
|
5
|
+
import { runStatusClass, runStatusLabel } from "../../utils/statusStyle";
|
|
6
6
|
import { IntegrationsApiError } from "../../utils/apiError";
|
|
7
7
|
import { createPollBudget, SAFETY_POLL_MS } from "../../utils/pollBudget";
|
|
8
8
|
const props = defineProps({
|
|
@@ -270,9 +270,12 @@ function timelineBarClass(row) {
|
|
|
270
270
|
}
|
|
271
271
|
}
|
|
272
272
|
const timelineDetail = ref(null);
|
|
273
|
+
function timelineRowStatusWord(row) {
|
|
274
|
+
if (row.durationMs == null) return "running";
|
|
275
|
+
return row.ok ? "succeeded" : "failed";
|
|
276
|
+
}
|
|
273
277
|
function timelineRowStatus(row) {
|
|
274
|
-
|
|
275
|
-
return t(row.ok ? "integration.runs.status.succeeded" : "integration.runs.status.failed");
|
|
278
|
+
return runStatusLabel(timelineRowStatusWord(row));
|
|
276
279
|
}
|
|
277
280
|
function fmtClock(ms) {
|
|
278
281
|
try {
|
|
@@ -583,7 +586,7 @@ async function confirmAction() {
|
|
|
583
586
|
<div>
|
|
584
587
|
<div class="text-[10px] uppercase tracking-wide text-muted-foreground">{{ t("integration.runs.detail.field.status") }}</div>
|
|
585
588
|
<div class="mt-0.5">
|
|
586
|
-
<Badge variant="outline" :class="['border text-xs font-medium uppercase',
|
|
589
|
+
<Badge variant="outline" :class="['border text-xs font-medium uppercase', runStatusClass(dStatus ?? run.status)]">
|
|
587
590
|
{{ runStatusLabel(dStatus ?? run.status) }}
|
|
588
591
|
</Badge>
|
|
589
592
|
</div>
|
|
@@ -699,7 +702,7 @@ async function confirmAction() {
|
|
|
699
702
|
class="w-full flex items-center gap-3 px-3 py-2 text-left"
|
|
700
703
|
@click="toggleStep(i)"
|
|
701
704
|
>
|
|
702
|
-
<Badge variant="outline" :class="['border text-xs font-medium uppercase shrink-0',
|
|
705
|
+
<Badge variant="outline" :class="['border text-xs font-medium uppercase shrink-0', runStatusClass(st.status)]">{{ runStatusLabel(st.status) }}</Badge>
|
|
703
706
|
<div class="min-w-0 flex-1">
|
|
704
707
|
<div class="font-mono text-xs truncate">{{ st.node_slug || st.node_id }}</div>
|
|
705
708
|
<div class="text-[10px] text-muted-foreground truncate">
|
|
@@ -802,7 +805,7 @@ async function confirmAction() {
|
|
|
802
805
|
<Badge
|
|
803
806
|
v-if="timelineDetail"
|
|
804
807
|
variant="outline"
|
|
805
|
-
:class="['border text-xs font-medium uppercase',
|
|
808
|
+
:class="['border text-xs font-medium uppercase', runStatusClass(timelineRowStatusWord(timelineDetail))]"
|
|
806
809
|
>{{ timelineRowStatus(timelineDetail) }}</Badge>
|
|
807
810
|
<span v-if="timelineDetail" class="text-sm tabular-nums text-muted-foreground">{{ fmtMs(timelineDetail.durationMs) }}</span>
|
|
808
811
|
</DialogTitle>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { computed, onMounted, onUnmounted, ref } from "vue";
|
|
3
3
|
import { Badge } from "@revenexx/studio";
|
|
4
|
-
import { RUN_STATUS_WORDS, normalisedRunStatus,
|
|
4
|
+
import { RUN_STATUS_COLUMN_WIDTH, RUN_STATUS_WORDS, normalisedRunStatus, runStatusClass, runStatusLabel } from "../../utils/statusStyle";
|
|
5
5
|
import { TRIGGER_TYPE_WORDS, triggerTypeLabel } from "../../data/integrationNodeCatalog";
|
|
6
6
|
const props = defineProps({
|
|
7
7
|
runs: { type: Array, required: true },
|
|
@@ -70,7 +70,11 @@ const columns = computed(() => [
|
|
|
70
70
|
// Sorted and searched by the word on screen, not by the raw field: a column
|
|
71
71
|
// with no `searchGetter` is searched through its `sortGetter`, so a German
|
|
72
72
|
// reader typing the status they can see has to find the row that shows it.
|
|
73
|
-
|
|
73
|
+
// One floor for both lists of runs, this one and the dashboard's, rather than a
|
|
74
|
+
// number guessed twice — see `RUN_STATUS_COLUMN_WIDTH` for how it was chosen and
|
|
75
|
+
// why the track cannot be elastic. At the 100px it used to be, the German
|
|
76
|
+
// "fehlgeschlagen" ran out of its cell and under the workflow name (PO-362).
|
|
77
|
+
{ key: "status", label: t("integration.runs.table.column.status"), width: RUN_STATUS_COLUMN_WIDTH, sortable: true, sortGetter: (r) => runStatusLabel(r.status) },
|
|
74
78
|
...props.showWorkflow ? [{ key: "workflow", label: t("integration.runs.table.column.workflow"), sortable: true, sortGetter: (r) => r.workflowName.toLowerCase(), searchGetter: (r) => r.workflowName }] : [],
|
|
75
79
|
// Fixed floor wide enough for a full UUID, flexible above it — header and
|
|
76
80
|
// body rows are separate grids, so a content-sized track (max-content)
|
|
@@ -99,7 +103,15 @@ const columns = computed(() => [
|
|
|
99
103
|
@row-click="(r) => openRun(r)"
|
|
100
104
|
>
|
|
101
105
|
<template #cell-status="{ row: run }">
|
|
102
|
-
|
|
106
|
+
<!-- `max-w-full` is the net, not the fix: a word wider than the column above is
|
|
107
|
+
then clipped inside its own cell instead of being drawn over the one beside
|
|
108
|
+
it, and the `title` keeps it readable. A hard clip, not an ellipsis — the
|
|
109
|
+
badge is an inline-flex box, where `text-overflow` does not apply. -->
|
|
110
|
+
<Badge
|
|
111
|
+
variant="outline"
|
|
112
|
+
:title="runStatusLabel(run.status)"
|
|
113
|
+
:class="['border text-xs font-medium uppercase max-w-full', runStatusClass(run.status)]"
|
|
114
|
+
>
|
|
103
115
|
{{ runStatusLabel(run.status) }}
|
|
104
116
|
</Badge>
|
|
105
117
|
</template>
|
|
@@ -49,6 +49,7 @@ import WorkflowEdgeView from "./WorkflowEdgeView.vue";
|
|
|
49
49
|
import { layoutWorkflowNodes } from "./workflowLayout";
|
|
50
50
|
import { TRIGGER_TEMPLATES, findManifest } from "../../data/integrationNodeCatalog";
|
|
51
51
|
import { isTerminalManifest, portsForNode } from "../../data/nodeInputData";
|
|
52
|
+
import { computeLoopBodies, iteratorNodeIds, nodeLoopData } from "../../data/loopBody";
|
|
52
53
|
import { portFromHandleId } from "../../data/handleId";
|
|
53
54
|
import { groupFindingsByNode } from "../../utils/validationFindings";
|
|
54
55
|
import { reorderBranchSubtrees } from "../../data/branchReorder";
|
|
@@ -89,6 +90,29 @@ watch(() => JSON.stringify(invalidNodeFields.value), () => {
|
|
|
89
90
|
if (dragInFlight) return;
|
|
90
91
|
syncFromBlob();
|
|
91
92
|
});
|
|
93
|
+
const selectedLoopBody = computed(() => {
|
|
94
|
+
const selected = new Set(flow.getSelectedNodes.value.map((n) => n.id));
|
|
95
|
+
if (!selected.size) return /* @__PURE__ */ new Set();
|
|
96
|
+
const loops = iteratorNodeIds(blob.value.nodes);
|
|
97
|
+
if (!loops.length) return /* @__PURE__ */ new Set();
|
|
98
|
+
const out = /* @__PURE__ */ new Set();
|
|
99
|
+
for (const body of computeLoopBodies(loops, blob.value.edges)) {
|
|
100
|
+
const touched = selected.has(body.iteratorId) || [...body.nodeIds].some((id) => selected.has(id));
|
|
101
|
+
if (!touched) continue;
|
|
102
|
+
out.add(body.iteratorId);
|
|
103
|
+
for (const id of body.nodeIds) out.add(id);
|
|
104
|
+
}
|
|
105
|
+
return out;
|
|
106
|
+
});
|
|
107
|
+
provide("workflow-loop-highlight", selectedLoopBody);
|
|
108
|
+
watch(() => iteratorNodeIds(blob.value.nodes).join("|"), () => {
|
|
109
|
+
if (dragInFlight) return;
|
|
110
|
+
syncFromBlob();
|
|
111
|
+
});
|
|
112
|
+
watch(useIntegrationsUiLocale(), () => {
|
|
113
|
+
if (dragInFlight) return;
|
|
114
|
+
syncFromBlob();
|
|
115
|
+
});
|
|
92
116
|
const dynamicOutputNodes = computed(() => blob.value.nodes.filter((n) => {
|
|
93
117
|
const m = findManifest(n.nodeSlug, n.nodeVersion);
|
|
94
118
|
return (m?.outputs ?? []).some((o) => o.resolveOutputs === true);
|
|
@@ -229,6 +253,7 @@ function bornClass(id) {
|
|
|
229
253
|
return bornIds.value.has(id) ? "node-pop-in" : void 0;
|
|
230
254
|
}
|
|
231
255
|
function syncFromBlob() {
|
|
256
|
+
const loops = nodeLoopData(blob.value.nodes, blob.value.edges);
|
|
232
257
|
const stickies = [...blob.value.stickies ?? []].sort((a, b) => stickyArea(b) - stickyArea(a));
|
|
233
258
|
const collapsedBy = collapsedGroupOf();
|
|
234
259
|
const groupNodes = (blob.value.groups ?? []).map((g, i) => {
|
|
@@ -284,7 +309,8 @@ function syncFromBlob() {
|
|
|
284
309
|
data: {
|
|
285
310
|
...n,
|
|
286
311
|
__resolvedOutputs: dynamicOutputs.value[n.id],
|
|
287
|
-
__invalidFields: invalidNodeFields.value[n.id]
|
|
312
|
+
__invalidFields: invalidNodeFields.value[n.id],
|
|
313
|
+
__loop: loops.get(n.id)
|
|
288
314
|
}
|
|
289
315
|
}))
|
|
290
316
|
];
|
|
@@ -18,6 +18,7 @@ import GroupFrameView from "./GroupFrameView.vue";
|
|
|
18
18
|
import WorkflowEdgeView from "./WorkflowEdgeView.vue";
|
|
19
19
|
import NodeDynamicOutputsResolver from "./NodeDynamicOutputsResolver.vue";
|
|
20
20
|
import { findManifest } from "../../data/integrationNodeCatalog";
|
|
21
|
+
import { iteratorNodeIds, nodeLoopData } from "../../data/loopBody";
|
|
21
22
|
import { useIntegrationsRevisions } from "../../composables/useIntegrationsRevisions";
|
|
22
23
|
const props = defineProps({
|
|
23
24
|
workflow: { type: Object, required: true },
|
|
@@ -142,6 +143,7 @@ function buildPreview(blob) {
|
|
|
142
143
|
}
|
|
143
144
|
return "default";
|
|
144
145
|
};
|
|
146
|
+
const loops = nodeLoopData(blob.nodes, blob.edges);
|
|
145
147
|
const stickies = [...blob.stickies ?? []].sort((a, b) => stickyArea(b) - stickyArea(a));
|
|
146
148
|
previewNodes.value = [
|
|
147
149
|
...previewGroupNodes(blob, nodePos),
|
|
@@ -167,7 +169,7 @@ function buildPreview(blob) {
|
|
|
167
169
|
type: "workflow",
|
|
168
170
|
position: n.position ?? { x: 240, y: 80 },
|
|
169
171
|
draggable: false,
|
|
170
|
-
data: { ...n, __resolvedOutputs: dynamicOutputs.value[n.id] }
|
|
172
|
+
data: { ...n, __resolvedOutputs: dynamicOutputs.value[n.id], __loop: loops.get(n.id) }
|
|
171
173
|
}))
|
|
172
174
|
];
|
|
173
175
|
previewEdges.value = blob.edges.map((e) => {
|
|
@@ -245,6 +247,12 @@ async function restoreRevision(rev) {
|
|
|
245
247
|
watch(() => props.workflow.revision, () => {
|
|
246
248
|
if (selected.value === null) buildPreview(props.workflow.blob);
|
|
247
249
|
});
|
|
250
|
+
watch(() => iteratorNodeIds(previewBlob.value.nodes).join("|"), () => {
|
|
251
|
+
buildPreview(previewBlob.value);
|
|
252
|
+
});
|
|
253
|
+
watch(useIntegrationsUiLocale(), () => {
|
|
254
|
+
buildPreview(previewBlob.value);
|
|
255
|
+
});
|
|
248
256
|
function formatDateTime(iso) {
|
|
249
257
|
if (!iso) return "\u2014";
|
|
250
258
|
try {
|