@revenexx/studio-integrations 0.5.0 → 0.6.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 +201 -0
- package/README.md +17 -4
- package/dist/module.json +1 -1
- package/dist/runtime/components/Integrations/CredentialCreateDialog.vue +15 -14
- package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +91 -19
- package/dist/runtime/components/Integrations/CredentialRevenexxSource.vue +58 -21
- package/dist/runtime/components/Integrations/CredentialTypePicker.vue +4 -8
- package/dist/runtime/components/Integrations/EventTypeCombobox.d.vue.ts +1 -0
- package/dist/runtime/components/Integrations/EventTypeCombobox.vue +4 -2
- package/dist/runtime/components/Integrations/EventTypeCombobox.vue.d.ts +1 -0
- package/dist/runtime/components/Integrations/ExpressionInput.d.vue.ts +4 -0
- package/dist/runtime/components/Integrations/ExpressionInput.vue +54 -25
- package/dist/runtime/components/Integrations/ExpressionInput.vue.d.ts +4 -0
- package/dist/runtime/components/Integrations/GroupFrameView.vue +12 -12
- package/dist/runtime/components/Integrations/Layout.vue +21 -2
- package/dist/runtime/components/Integrations/NodeConfigDynamicField.vue +10 -11
- package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.vue +2 -5
- package/dist/runtime/components/Integrations/NodeConfigField.d.vue.ts +2 -0
- package/dist/runtime/components/Integrations/NodeConfigField.vue +160 -67
- package/dist/runtime/components/Integrations/NodeConfigField.vue.d.ts +2 -0
- package/dist/runtime/components/Integrations/NodeInspector.d.vue.ts +6 -0
- package/dist/runtime/components/Integrations/NodeInspector.vue +1075 -303
- package/dist/runtime/components/Integrations/NodeInspector.vue.d.ts +6 -0
- package/dist/runtime/components/Integrations/NodeLogo.d.vue.ts +12 -0
- package/dist/runtime/components/Integrations/NodeLogo.vue +2 -30
- package/dist/runtime/components/Integrations/NodeLogo.vue.d.ts +12 -0
- package/dist/runtime/components/Integrations/NodeMark.d.vue.ts +15 -0
- package/dist/runtime/components/Integrations/NodeMark.vue +79 -0
- package/dist/runtime/components/Integrations/NodeMark.vue.d.ts +15 -0
- package/dist/runtime/components/Integrations/NodeOutputSchema.d.vue.ts +21 -0
- package/dist/runtime/components/Integrations/NodeOutputSchema.vue +42 -0
- package/dist/runtime/components/Integrations/NodeOutputSchema.vue.d.ts +21 -0
- package/dist/runtime/components/Integrations/NodePalette.vue +100 -34
- package/dist/runtime/components/Integrations/QueryBuilder.vue +8 -1
- package/dist/runtime/components/Integrations/RunActionsMenu.vue +3 -1
- package/dist/runtime/components/Integrations/RunPayloadInput.vue +17 -4
- package/dist/runtime/components/Integrations/StickyNoteView.vue +2 -1
- package/dist/runtime/components/Integrations/TemplateCard.d.vue.ts +18 -0
- package/dist/runtime/components/Integrations/TemplateCard.vue +109 -0
- package/dist/runtime/components/Integrations/TemplateCard.vue.d.ts +18 -0
- package/dist/runtime/components/Integrations/TemplateFlowPreview.d.vue.ts +2 -2
- package/dist/runtime/components/Integrations/TemplateFlowPreview.vue +7 -10
- package/dist/runtime/components/Integrations/TemplateFlowPreview.vue.d.ts +2 -2
- package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +21 -37
- package/dist/runtime/components/Integrations/WorkflowEdgeView.d.vue.ts +18 -4
- package/dist/runtime/components/Integrations/WorkflowEdgeView.vue +23 -7
- package/dist/runtime/components/Integrations/WorkflowEdgeView.vue.d.ts +18 -4
- package/dist/runtime/components/Integrations/WorkflowEditor.d.vue.ts +22 -1
- package/dist/runtime/components/Integrations/WorkflowEditor.vue +170 -48
- package/dist/runtime/components/Integrations/WorkflowEditor.vue.d.ts +22 -1
- package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue +4 -20
- package/dist/runtime/components/Integrations/WorkflowIconPicker.d.vue.ts +19 -1
- package/dist/runtime/components/Integrations/WorkflowIconPicker.vue +11 -2
- package/dist/runtime/components/Integrations/WorkflowIconPicker.vue.d.ts +19 -1
- package/dist/runtime/components/Integrations/WorkflowNodeView.d.vue.ts +4 -0
- package/dist/runtime/components/Integrations/WorkflowNodeView.vue +189 -46
- package/dist/runtime/components/Integrations/WorkflowNodeView.vue.d.ts +4 -0
- package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +4 -20
- package/dist/runtime/components/Integrations/WorkflowSettingsDialog.vue +15 -9
- package/dist/runtime/components/Integrations/WorkflowTagsInput.d.vue.ts +1 -0
- package/dist/runtime/components/Integrations/WorkflowTagsInput.vue +7 -2
- package/dist/runtime/components/Integrations/WorkflowTagsInput.vue.d.ts +1 -0
- package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +4 -2
- package/dist/runtime/components/Integrations/WorkflowVariablesDialog.vue +30 -17
- package/dist/runtime/components/Integrations/WorkflowsTable.vue +47 -7
- package/dist/runtime/composables/useCredentialTypes.js +4 -4
- package/dist/runtime/composables/useIntegrationsAudit.js +4 -3
- package/dist/runtime/composables/useIntegrationsCredentials.js +4 -4
- package/dist/runtime/composables/useIntegrationsDeadLetters.js +7 -6
- package/dist/runtime/composables/useIntegrationsNodes.js +4 -4
- package/dist/runtime/composables/useIntegrationsRuns.js +11 -6
- package/dist/runtime/composables/useIntegrationsSchedules.d.ts +4 -0
- package/dist/runtime/composables/useIntegrationsSchedules.js +10 -3
- package/dist/runtime/composables/useIntegrationsSecrets.js +10 -7
- package/dist/runtime/composables/useIntegrationsTemplates.d.ts +1 -1
- package/dist/runtime/composables/useIntegrationsTemplates.js +12 -8
- package/dist/runtime/composables/useIntegrationsTriggers.js +6 -6
- package/dist/runtime/composables/useIntegrationsWorkflows.js +8 -4
- package/dist/runtime/composables/useLoadGate.d.ts +17 -0
- package/dist/runtime/composables/useLoadGate.js +8 -0
- package/dist/runtime/data/branchReorder.d.ts +37 -0
- package/dist/runtime/data/branchReorder.js +71 -0
- package/dist/runtime/data/handleId.d.ts +23 -0
- package/dist/runtime/data/handleId.js +6 -0
- package/dist/runtime/data/integrationTemplates.d.ts +39 -0
- package/dist/runtime/data/integrationTemplates.js +15 -0
- package/dist/runtime/data/integrationWorkflowSamples.js +1 -1
- package/dist/runtime/data/nodeIcons.d.ts +2 -0
- package/dist/runtime/data/nodeIcons.js +73 -0
- package/dist/runtime/data/nodeInputData.d.ts +16 -4
- package/dist/runtime/data/nodeInputData.js +4 -0
- package/dist/runtime/data/payloadForm.d.ts +9 -2
- package/dist/runtime/data/payloadForm.js +27 -3
- package/dist/runtime/data/portTone.d.ts +17 -0
- package/dist/runtime/data/portTone.js +7 -0
- package/dist/runtime/data/webhookAuth.d.ts +18 -0
- package/dist/runtime/data/webhookAuth.js +19 -0
- package/dist/runtime/data/workflowIconCatalog.js +2 -5
- package/dist/runtime/pages/integrations/audit.vue +91 -6
- package/dist/runtime/pages/integrations/credentials/oauth-complete.vue +6 -4
- package/dist/runtime/pages/integrations/credentials.vue +119 -21
- package/dist/runtime/pages/integrations/index.vue +49 -44
- package/dist/runtime/pages/integrations/runs.vue +1 -1
- package/dist/runtime/pages/integrations/schedules.vue +51 -7
- package/dist/runtime/pages/integrations/secrets.vue +29 -7
- package/dist/runtime/pages/integrations/templates/[slug].vue +52 -30
- package/dist/runtime/pages/integrations/templates/index.vue +57 -86
- package/dist/runtime/pages/integrations/workflows/[id].vue +243 -25
- package/dist/runtime/pages/integrations/workflows/index.vue +15 -2
- package/dist/runtime/types/integrations.d.ts +65 -6
- package/dist/runtime/utils/apiError.d.ts +47 -0
- package/dist/runtime/utils/apiError.js +32 -0
- package/dist/runtime/utils/cockpitSources.d.ts +153 -0
- package/dist/runtime/utils/cockpitSources.js +14 -0
- package/dist/runtime/utils/loadGate.d.ts +61 -0
- package/dist/runtime/utils/loadGate.js +22 -0
- package/dist/runtime/utils/localize.d.ts +29 -0
- package/dist/runtime/utils/localize.js +4 -0
- package/dist/runtime/utils/notify.d.ts +59 -0
- package/dist/runtime/utils/notify.js +21 -0
- package/dist/runtime/utils/revenexxCredentialSource.d.ts +26 -0
- package/dist/runtime/utils/revenexxCredentialSource.js +8 -0
- package/dist/runtime/utils/schedule.d.ts +141 -0
- package/dist/runtime/utils/schedule.js +207 -0
- package/dist/runtime/utils/validationFindings.d.ts +101 -0
- package/dist/runtime/utils/validationFindings.js +53 -0
- package/dist/runtime/utils/whenIdle.d.ts +42 -0
- package/dist/runtime/utils/whenIdle.js +12 -0
- package/package.json +8 -2
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { computed, ref, watch } from "vue";
|
|
3
|
-
import { Magnifier as Search,
|
|
3
|
+
import { Magnifier as Search, Layers, DangerTriangle as AlertTriangle } from "#solar-icons";
|
|
4
4
|
import { Input, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@revenexx/studio";
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
5
|
+
import TemplateCard from "../../../components/Integrations/TemplateCard.vue";
|
|
6
|
+
import { templateCategoryLabel, templatePackageLabel } from "../../../data/integrationTemplates";
|
|
7
7
|
useHead({ title: "Templates \xB7 Integration Studio" });
|
|
8
8
|
const { templates, loading, error, loaded, load, ensureDetails } = useIntegrationsTemplates();
|
|
9
9
|
const { load: loadNodeCatalog } = useIntegrationsNodes();
|
|
@@ -15,16 +15,13 @@ const search = ref("");
|
|
|
15
15
|
const activeCategory = ref("all");
|
|
16
16
|
const activeLevel = ref("all");
|
|
17
17
|
const sortBy = ref("name");
|
|
18
|
-
const CATEGORY_LABELS = Object.fromEntries(
|
|
19
|
-
TEMPLATE_CATEGORIES.map((c) => [c.key, c.label])
|
|
20
|
-
);
|
|
21
|
-
function categoryLabel(key) {
|
|
22
|
-
return CATEGORY_LABELS[key] ?? key.charAt(0).toUpperCase() + key.slice(1);
|
|
23
|
-
}
|
|
24
18
|
const categories = computed(() => {
|
|
25
19
|
const counts = /* @__PURE__ */ new Map();
|
|
26
|
-
for (const t of templates.value)
|
|
27
|
-
|
|
20
|
+
for (const t of templates.value) {
|
|
21
|
+
if (!t.category) continue;
|
|
22
|
+
counts.set(t.category, (counts.get(t.category) ?? 0) + 1);
|
|
23
|
+
}
|
|
24
|
+
return [...counts.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).map(([key, count]) => ({ key, label: templateCategoryLabel(key), count }));
|
|
28
25
|
});
|
|
29
26
|
const LEVELS = ["beginner", "intermediate", "advanced"];
|
|
30
27
|
const LEVEL_RANK = { beginner: 0, intermediate: 1, advanced: 2 };
|
|
@@ -35,7 +32,7 @@ const filtered = computed(() => {
|
|
|
35
32
|
if (activeLevel.value !== "all") items = items.filter((t) => t.level === activeLevel.value);
|
|
36
33
|
if (q) {
|
|
37
34
|
items = items.filter(
|
|
38
|
-
(t) => t.name.toLowerCase().includes(q) || t.description.toLowerCase().includes(q) || t.vendors.some((v) => v.includes(q)) || (
|
|
35
|
+
(t) => t.name.toLowerCase().includes(q) || t.description.toLowerCase().includes(q) || t.vendors.some((v) => v.toLowerCase().includes(q)) || (templatePackageLabel(t) ?? "").toLowerCase().includes(q) || t.industries.some((i) => i.toLowerCase().includes(q))
|
|
39
36
|
);
|
|
40
37
|
}
|
|
41
38
|
const sorted = [...items];
|
|
@@ -47,21 +44,8 @@ const filtered = computed(() => {
|
|
|
47
44
|
watch(() => templates.value.length, () => {
|
|
48
45
|
if (templates.value.length) void ensureDetails(templates.value.map((t) => t.slug));
|
|
49
46
|
});
|
|
50
|
-
const
|
|
51
|
-
const
|
|
52
|
-
function levelDot(level) {
|
|
53
|
-
return level === "beginner" ? "bg-emerald-500" : level === "intermediate" ? "bg-amber-500" : "bg-rose-500";
|
|
54
|
-
}
|
|
55
|
-
function bannerFor(tpl) {
|
|
56
|
-
return tpl.images.find((i) => i.category === "banner")?.src ?? null;
|
|
57
|
-
}
|
|
58
|
-
function packageLabel(tpl) {
|
|
59
|
-
const pkg = tpl.package;
|
|
60
|
-
if (!pkg) return null;
|
|
61
|
-
const tail = (pkg.name.split("/").pop() ?? pkg.name).replace(/^integrations-nodes-/, "");
|
|
62
|
-
if (tail === "core") return null;
|
|
63
|
-
return pkg.label ?? tail.split("-").map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join(" ");
|
|
64
|
-
}
|
|
47
|
+
const awaitingCatalogue = computed(() => loading.value && !templates.value.length);
|
|
48
|
+
const statusText = computed(() => awaitingCatalogue.value ? "Loading templates\u2026" : "");
|
|
65
49
|
</script>
|
|
66
50
|
|
|
67
51
|
<template>
|
|
@@ -96,7 +80,7 @@ function packageLabel(tpl) {
|
|
|
96
80
|
</div>
|
|
97
81
|
|
|
98
82
|
<Select v-model="activeLevel">
|
|
99
|
-
<SelectTrigger class="h-7 data-[size=default]:h-7 min-h-0 py-0 w-36 rounded-full bg-white text-sm">
|
|
83
|
+
<SelectTrigger aria-label="Filter by level" class="h-7 data-[size=default]:h-7 min-h-0 py-0 w-36 rounded-full bg-white text-sm">
|
|
100
84
|
<SelectValue />
|
|
101
85
|
</SelectTrigger>
|
|
102
86
|
<SelectContent>
|
|
@@ -105,7 +89,7 @@ function packageLabel(tpl) {
|
|
|
105
89
|
</SelectContent>
|
|
106
90
|
</Select>
|
|
107
91
|
<Select v-model="sortBy">
|
|
108
|
-
<SelectTrigger class="h-7 data-[size=default]:h-7 min-h-0 py-0 w-40 rounded-full bg-white text-sm">
|
|
92
|
+
<SelectTrigger aria-label="Sort templates" class="h-7 data-[size=default]:h-7 min-h-0 py-0 w-40 rounded-full bg-white text-sm">
|
|
109
93
|
<SelectValue />
|
|
110
94
|
</SelectTrigger>
|
|
111
95
|
<SelectContent>
|
|
@@ -116,15 +100,40 @@ function packageLabel(tpl) {
|
|
|
116
100
|
</Select>
|
|
117
101
|
<div class="relative w-60">
|
|
118
102
|
<Search :size="12" class="absolute left-2 top-1/2 -translate-y-1/2 text-muted-foreground" />
|
|
119
|
-
<Input v-model="search" placeholder="Search templates, vendors…" class="h-7 min-h-0 py-0 rounded-full pl-7 text-sm bg-white" />
|
|
103
|
+
<Input v-model="search" aria-label="Search templates and vendors" placeholder="Search templates, vendors…" class="h-7 min-h-0 py-0 rounded-full pl-7 text-sm bg-white" />
|
|
120
104
|
</div>
|
|
121
105
|
</div>
|
|
122
106
|
</template>
|
|
123
107
|
</PageHeader>
|
|
124
108
|
|
|
125
109
|
<div class="p-4">
|
|
126
|
-
<!--
|
|
127
|
-
|
|
110
|
+
<!-- What the page is doing, in words (specs/templates.md AC-9).
|
|
111
|
+
The shimmer says "still working" only to somebody who can see it, and
|
|
112
|
+
six blank tiles carry no text, no name and no state — so waiting and
|
|
113
|
+
"nothing is registered" were the same silence, which are the two states
|
|
114
|
+
that ask for opposite reactions.
|
|
115
|
+
|
|
116
|
+
The region is OUTSIDE the v-if chain below and always in the DOM, with
|
|
117
|
+
only its text changing. A live region that appears together with its
|
|
118
|
+
content is a region assistive technology may never have been watching:
|
|
119
|
+
what is announced is a change inside an existing region.
|
|
120
|
+
|
|
121
|
+
Named, because it is not the only `role="status"` on this page: every
|
|
122
|
+
card draws one while its flow miniature loads ("Loading the flow"). Two
|
|
123
|
+
unnamed status regions on one page are two things a reader — and a test —
|
|
124
|
+
cannot tell apart, and this one is about the gallery rather than a card. -->
|
|
125
|
+
<p role="status" aria-live="polite" aria-label="Template gallery" class="sr-only">{{ statusText }}</p>
|
|
126
|
+
|
|
127
|
+
<!-- The placeholders are taken out of the accessibility tree rather than
|
|
128
|
+
marked busy. `aria-busy` only asks assistive technology to defer
|
|
129
|
+
exposing changes, and on a role-less div that is not a live region NVDA
|
|
130
|
+
and JAWS effectively ignore it — it would have expressed the state
|
|
131
|
+
without keeping anything from being walked. A placeholder carries no
|
|
132
|
+
information, so there is nothing to lose by removing it, and the region
|
|
133
|
+
above says in words what this area means. Which is also why the named
|
|
134
|
+
"Templates" group below is absent while this branch renders: the set of
|
|
135
|
+
cards does not exist yet, rather than existing and being empty. -->
|
|
136
|
+
<div v-if="awaitingCatalogue" aria-hidden="true" class="grid gap-3 sm:grid-cols-2 xl:grid-cols-3">
|
|
128
137
|
<div v-for="n in 6" :key="`sk-${n}`" class="rounded-lg border border-black/[0.06] bg-white p-0 overflow-hidden">
|
|
129
138
|
<div class="h-36 bg-black/[0.04] animate-pulse" />
|
|
130
139
|
<div class="p-4 space-y-2">
|
|
@@ -135,8 +144,15 @@ function packageLabel(tpl) {
|
|
|
135
144
|
</div>
|
|
136
145
|
</div>
|
|
137
146
|
|
|
138
|
-
<!-- Fetch failed
|
|
139
|
-
|
|
147
|
+
<!-- Fetch failed. `role="alert"` because this block is how the wait ends for
|
|
148
|
+
somebody who was told through the region above to hold on: it is
|
|
149
|
+
announced when it is inserted, so the sequence is "Loading templates…"
|
|
150
|
+
then the reason, rather than "Loading templates…" then silence. An alert
|
|
151
|
+
rather than a copy of the text in the status region, which would put the
|
|
152
|
+
same sentence in the page twice and read it out twice to anybody walking
|
|
153
|
+
it. The other two empty states are not alerts: nothing had spoken before
|
|
154
|
+
them, and their copy is where the reader is going anyway. -->
|
|
155
|
+
<div v-else-if="error && !templates.length" role="alert" class="rounded-lg border border-rose-500/20 bg-rose-500/5 p-4 text-sm text-rose-700 flex items-start gap-2">
|
|
140
156
|
<AlertTriangle :size="15" class="mt-0.5 shrink-0" />
|
|
141
157
|
<div>
|
|
142
158
|
<div class="font-medium">Couldn't load templates.</div>
|
|
@@ -155,61 +171,16 @@ function packageLabel(tpl) {
|
|
|
155
171
|
</p>
|
|
156
172
|
</div>
|
|
157
173
|
|
|
158
|
-
<!--
|
|
159
|
-
|
|
160
|
-
|
|
174
|
+
<!-- The group is named so the cards can be addressed as a set; a card names
|
|
175
|
+
itself from what it shows, which TemplateCard says more about. -->
|
|
176
|
+
<div v-else role="group" aria-label="Templates" class="grid gap-3 sm:grid-cols-2 xl:grid-cols-3">
|
|
177
|
+
<TemplateCard
|
|
161
178
|
v-for="tpl in filtered"
|
|
162
179
|
:key="tpl.slug"
|
|
180
|
+
:template="tpl"
|
|
181
|
+
density="full"
|
|
163
182
|
:to="`/integrations/templates/${tpl.slug}`"
|
|
164
|
-
|
|
165
|
-
>
|
|
166
|
-
<!-- Live workflow preview (or the registry banner, when one ships) -->
|
|
167
|
-
<div class="relative h-36 shrink-0 border-b border-black/[0.06] bg-slate-50/70 [background-image:radial-gradient(circle,rgba(15,23,42,0.07)_1px,transparent_1px)] [background-size:14px_14px]">
|
|
168
|
-
<img
|
|
169
|
-
v-if="bannerFor(tpl)"
|
|
170
|
-
:src="bannerFor(tpl)"
|
|
171
|
-
alt=""
|
|
172
|
-
class="h-full w-full object-cover"
|
|
173
|
-
>
|
|
174
|
-
<TemplateFlowPreview v-else-if="tpl.detailLoaded" :blob="tpl.blob" :height="144" class="px-2" />
|
|
175
|
-
<div v-else class="flex h-full items-center justify-center">
|
|
176
|
-
<Loader2 :size="16" class="animate-spin text-muted-foreground/50" />
|
|
177
|
-
</div>
|
|
178
|
-
<div class="absolute left-2 top-2 flex items-center gap-1.5">
|
|
179
|
-
<span class="inline-flex items-center whitespace-nowrap rounded-full bg-slate-100 px-2 py-0.5 uppercase text-xs font-medium text-slate-700">
|
|
180
|
-
{{ categoryLabel(tpl.category) }}
|
|
181
|
-
</span>
|
|
182
|
-
<span
|
|
183
|
-
v-if="packageLabel(tpl)"
|
|
184
|
-
class="inline-flex items-center whitespace-nowrap rounded-full bg-violet-100 px-2 py-0.5 uppercase text-xs font-medium text-violet-700"
|
|
185
|
-
>
|
|
186
|
-
{{ packageLabel(tpl) }}
|
|
187
|
-
</span>
|
|
188
|
-
</div>
|
|
189
|
-
</div>
|
|
190
|
-
|
|
191
|
-
<div class="flex min-h-0 flex-1 flex-col p-4">
|
|
192
|
-
<div class="text-sm font-semibold text-foreground line-clamp-1">{{ tpl.name }}</div>
|
|
193
|
-
<p class="mt-1 flex-1 text-sm text-muted-foreground line-clamp-2">{{ tpl.description }}</p>
|
|
194
|
-
|
|
195
|
-
<div class="mt-3 flex items-center gap-2 text-xs text-muted-foreground">
|
|
196
|
-
<span
|
|
197
|
-
v-for="t in tpl.triggerTypes"
|
|
198
|
-
:key="t"
|
|
199
|
-
class="inline-flex items-center gap-1 whitespace-nowrap rounded-full bg-slate-100 px-2 py-0.5 uppercase"
|
|
200
|
-
>
|
|
201
|
-
<component :is="TRIGGER_ICON[t] ?? Zap" :size="10" />
|
|
202
|
-
{{ TRIGGER_LABEL[t] ?? t }}
|
|
203
|
-
</span>
|
|
204
|
-
<span class="flex-1" />
|
|
205
|
-
<span class="inline-flex items-center gap-1 whitespace-nowrap rounded-full bg-slate-100 px-2 py-0.5 uppercase">
|
|
206
|
-
<span class="size-1.5 rounded-full" :class="levelDot(tpl.level)" />
|
|
207
|
-
{{ tpl.level }}
|
|
208
|
-
</span>
|
|
209
|
-
<span v-if="tpl.publisher.kind !== 'revenexx'" class="inline-flex items-center rounded-full bg-slate-100 px-2 py-0.5 uppercase">{{ tpl.publisher.name }}</span>
|
|
210
|
-
</div>
|
|
211
|
-
</div>
|
|
212
|
-
</NuxtLink>
|
|
183
|
+
/>
|
|
213
184
|
</div>
|
|
214
185
|
|
|
215
186
|
<div v-if="templates.length && !filtered.length" class="mt-4 rounded-lg border border-black/[0.06] bg-white p-8 text-center text-sm text-muted-foreground">
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import { computed, onMounted, onUnmounted, ref, watch } from "vue";
|
|
2
|
+
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from "vue";
|
|
3
3
|
import { AltArrowLeft as ArrowLeft, AltArrowRight as Chevron, Copy as CopyIcon, Pause as PauseIcon, RefreshCircle as Loader2, Play, Refresh as RetryIcon, Diskette as Save } from "#solar-icons";
|
|
4
4
|
import {
|
|
5
|
+
AlertDialog,
|
|
6
|
+
AlertDialogContent,
|
|
7
|
+
AlertDialogDescription,
|
|
8
|
+
AlertDialogFooter,
|
|
9
|
+
AlertDialogHeader,
|
|
10
|
+
AlertDialogTitle,
|
|
5
11
|
Badge,
|
|
6
12
|
Button,
|
|
7
13
|
Dialog,
|
|
@@ -24,7 +30,8 @@ import WorkflowDuplicateDialog from "../../../components/Integrations/WorkflowDu
|
|
|
24
30
|
import RunsTable from "../../../components/Integrations/RunsTable.vue";
|
|
25
31
|
import RunDetailPanel from "../../../components/Integrations/RunDetailPanel.vue";
|
|
26
32
|
import { toast } from "vue-sonner";
|
|
27
|
-
import { IntegrationsApiError } from "../../../utils/apiError";
|
|
33
|
+
import { IntegrationsApiError, apiValidationIssues } from "../../../utils/apiError";
|
|
34
|
+
import { findingsSummary, groupFindingsByNode, nodeDisplayLabel } from "../../../utils/validationFindings";
|
|
28
35
|
import { payloadFieldsFromBlob } from "../../../data/payloadForm";
|
|
29
36
|
import WorkflowIconView from "../../../components/Integrations/WorkflowIconView.vue";
|
|
30
37
|
const route = useRoute();
|
|
@@ -46,6 +53,24 @@ const dirtyBlob = ref(null);
|
|
|
46
53
|
const isDirty = computed(() => dirtyBlob.value !== null);
|
|
47
54
|
const saving = ref(false);
|
|
48
55
|
const saveError = ref(null);
|
|
56
|
+
const validationWarnings = computed(() => workflow.value?.warnings);
|
|
57
|
+
const isInvalid = computed(() => (validationWarnings.value?.length ?? 0) > 0);
|
|
58
|
+
const WARN_COLLAPSED = 3;
|
|
59
|
+
const warningsExpanded = ref(false);
|
|
60
|
+
const warningGroups = computed(() => groupFindingsByNode(validationWarnings.value ?? [], workflow.value?.blob.nodes ?? []));
|
|
61
|
+
const warningSummary = computed(() => findingsSummary(validationWarnings.value?.length ?? 0, warningGroups.value.filter((g) => g.nodeId).length));
|
|
62
|
+
const editorRef = ref(null);
|
|
63
|
+
async function revealNode(nodeId) {
|
|
64
|
+
tab.value = "editor";
|
|
65
|
+
await nextTick();
|
|
66
|
+
editorRef.value?.revealNode(nodeId);
|
|
67
|
+
}
|
|
68
|
+
const { load: loadNodeCatalog } = useIntegrationsNodes();
|
|
69
|
+
onMounted(() => {
|
|
70
|
+
void loadNodeCatalog();
|
|
71
|
+
});
|
|
72
|
+
const deactivationOpen = ref(false);
|
|
73
|
+
const deactivationIssues = ref([]);
|
|
49
74
|
const { load: loadCredentials, needsReauth } = useIntegrationsCredentials();
|
|
50
75
|
onMounted(() => {
|
|
51
76
|
void loadCredentials();
|
|
@@ -54,11 +79,33 @@ const reauthCredentials = computed(() => {
|
|
|
54
79
|
const blob = JSON.stringify(dirtyBlob.value ?? workflow.value?.blob ?? {});
|
|
55
80
|
return needsReauth.value.filter((c) => blob.includes(c.id));
|
|
56
81
|
});
|
|
57
|
-
const buildBlocked = computed(() => {
|
|
58
|
-
const s = workflow.value?.buildStatus;
|
|
59
|
-
return s === "pending" || s === "building" || s === "failed";
|
|
60
|
-
});
|
|
61
82
|
const buildWorking = computed(() => workflow.value?.buildStatus === "building" || workflow.value?.buildStatus === "pending");
|
|
83
|
+
const buildFailed = computed(() => workflow.value?.buildStatus === "failed");
|
|
84
|
+
const waitingForBuild = ref(false);
|
|
85
|
+
const BUILD_WAIT_POLL_MS = 2500;
|
|
86
|
+
const BUILD_WAIT_TIMEOUT_MS = 5 * 6e4;
|
|
87
|
+
async function waitForBuildReady() {
|
|
88
|
+
waitingForBuild.value = true;
|
|
89
|
+
try {
|
|
90
|
+
const started = Date.now();
|
|
91
|
+
while (Date.now() - started < BUILD_WAIT_TIMEOUT_MS) {
|
|
92
|
+
await new Promise((resolve) => setTimeout(resolve, BUILD_WAIT_POLL_MS));
|
|
93
|
+
if (!workflow.value) return false;
|
|
94
|
+
const fresh = await get(workflow.value.id);
|
|
95
|
+
const status = fresh?.buildStatus;
|
|
96
|
+
if (status === "pending" || status === "building") continue;
|
|
97
|
+
if (status === "failed") {
|
|
98
|
+
toast.error("The workflow bundle failed to build \u2014 the run was not started.");
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
toast.error("The bundle build is taking unusually long \u2014 try the run again once it is ready.");
|
|
104
|
+
return false;
|
|
105
|
+
} finally {
|
|
106
|
+
waitingForBuild.value = false;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
62
109
|
const buildNotice = computed(() => {
|
|
63
110
|
switch (workflow.value?.buildStatus) {
|
|
64
111
|
case "building":
|
|
@@ -78,6 +125,7 @@ watch(id, async () => {
|
|
|
78
125
|
const w = await get(id.value);
|
|
79
126
|
if (!w) await router.push("/integrations/workflows");
|
|
80
127
|
dirtyBlob.value = null;
|
|
128
|
+
warningsExpanded.value = false;
|
|
81
129
|
if (tab.value === "runs") void loadRuns(1);
|
|
82
130
|
}, { immediate: true });
|
|
83
131
|
function onChange(blob) {
|
|
@@ -101,6 +149,7 @@ async function onRevisionRestored() {
|
|
|
101
149
|
}
|
|
102
150
|
const { trigger: triggerRun, loadForWorkflow, fetchRun, retryRun } = useIntegrationsRuns();
|
|
103
151
|
const running = ref(false);
|
|
152
|
+
const waitingMode = ref(null);
|
|
104
153
|
const payloadText = ref("{}");
|
|
105
154
|
const payloadError = ref(null);
|
|
106
155
|
const primaryMode = computed(() => workflow.value?.execution_mode === "sync_only" ? "sync" : "async");
|
|
@@ -129,6 +178,29 @@ function syncErrorMessage(e) {
|
|
|
129
178
|
}
|
|
130
179
|
return e?.message || "Sync run failed";
|
|
131
180
|
}
|
|
181
|
+
function refusedForSettings(e) {
|
|
182
|
+
const issues = apiValidationIssues(e);
|
|
183
|
+
if (!issues.length) return null;
|
|
184
|
+
const groups = groupFindingsByNode(issues, workflow.value?.blob.nodes ?? []);
|
|
185
|
+
const named = groups.filter((g) => g.nodeId);
|
|
186
|
+
const summary = findingsSummary(issues.length, named.length);
|
|
187
|
+
const parts = [
|
|
188
|
+
...named.map((g) => `${g.label} (${g.fields.join(", ")})`),
|
|
189
|
+
...groups.filter((g) => !g.nodeId).flatMap((g) => g.messages)
|
|
190
|
+
];
|
|
191
|
+
const where = parts.length ? ` \u2014 ${parts.join("; ")}` : "";
|
|
192
|
+
const line = `Can\u2019t run \u2014 ${summary}${where}`;
|
|
193
|
+
return /[.!?]$/.test(line) ? line : `${line}.`;
|
|
194
|
+
}
|
|
195
|
+
async function reportRunFailure(e, fallback) {
|
|
196
|
+
const settings = refusedForSettings(e);
|
|
197
|
+
if (!settings) {
|
|
198
|
+
syncError.value = fallback(e);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
syncError.value = settings;
|
|
202
|
+
if (workflow.value) await get(workflow.value.id);
|
|
203
|
+
}
|
|
132
204
|
async function run(mode) {
|
|
133
205
|
if (!workflow.value) return;
|
|
134
206
|
let payload;
|
|
@@ -139,6 +211,12 @@ async function run(mode) {
|
|
|
139
211
|
return;
|
|
140
212
|
}
|
|
141
213
|
payloadError.value = null;
|
|
214
|
+
if (buildWorking.value) {
|
|
215
|
+
waitingMode.value = mode;
|
|
216
|
+
const ready = await waitForBuildReady();
|
|
217
|
+
waitingMode.value = null;
|
|
218
|
+
if (!ready) return;
|
|
219
|
+
}
|
|
142
220
|
runModalMode.value = mode;
|
|
143
221
|
runModalRunId.value = null;
|
|
144
222
|
syncOpen.value = true;
|
|
@@ -150,7 +228,7 @@ async function run(mode) {
|
|
|
150
228
|
syncResponse.value = await triggerRun(workflow.value.id, payload, "sync");
|
|
151
229
|
runModalRunId.value = extractRunId(syncResponse.value);
|
|
152
230
|
} catch (e) {
|
|
153
|
-
|
|
231
|
+
await reportRunFailure(e, syncErrorMessage);
|
|
154
232
|
if (e instanceof IntegrationsApiError) {
|
|
155
233
|
runModalRunId.value = extractRunId(e.body?.run);
|
|
156
234
|
}
|
|
@@ -165,7 +243,7 @@ async function run(mode) {
|
|
|
165
243
|
const res = await triggerRun(workflow.value.id, payload, "async");
|
|
166
244
|
runModalRunId.value = extractRunId(res);
|
|
167
245
|
} catch (e) {
|
|
168
|
-
|
|
246
|
+
await reportRunFailure(e, (err) => err?.message || "Could not start the run.");
|
|
169
247
|
} finally {
|
|
170
248
|
running.value = false;
|
|
171
249
|
syncLoading.value = false;
|
|
@@ -412,6 +490,25 @@ async function saveWorkflowMeta(meta) {
|
|
|
412
490
|
savingMeta.value = false;
|
|
413
491
|
}
|
|
414
492
|
}
|
|
493
|
+
async function onToggleActive() {
|
|
494
|
+
const wf = workflow.value;
|
|
495
|
+
if (!wf) return;
|
|
496
|
+
if (!wf.active && isInvalid.value) {
|
|
497
|
+
toast.error(`Can\u2019t activate \u2014 ${validationWarnings.value?.length} setting${validationWarnings.value?.length === 1 ? "" : "s"} still need${validationWarnings.value?.length === 1 ? "s" : ""} fixing (see the banner above).`);
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
try {
|
|
501
|
+
await toggleActive(wf.id);
|
|
502
|
+
} catch (e) {
|
|
503
|
+
const issues = apiValidationIssues(e);
|
|
504
|
+
if (issues.length) {
|
|
505
|
+
await get(wf.id);
|
|
506
|
+
toast.error(`Can\u2019t activate \u2014 ${issues.length} validation issue${issues.length === 1 ? "" : "s"} to fix.`);
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
toast.error(e.message || "Could not change the workflow status.");
|
|
510
|
+
}
|
|
511
|
+
}
|
|
415
512
|
async function save() {
|
|
416
513
|
if (!workflow.value || !dirtyBlob.value) return;
|
|
417
514
|
saving.value = true;
|
|
@@ -426,6 +523,42 @@ async function save() {
|
|
|
426
523
|
});
|
|
427
524
|
dirtyBlob.value = null;
|
|
428
525
|
} catch (e) {
|
|
526
|
+
const issues = apiValidationIssues(e);
|
|
527
|
+
if (issues.length && workflow.value.active) {
|
|
528
|
+
const nodes = workflow.value.blob.nodes;
|
|
529
|
+
deactivationIssues.value = issues.map((i) => {
|
|
530
|
+
const m = /^blob\.nodes\.(\d+)\b/.exec(i.path);
|
|
531
|
+
const node = m ? nodes[Number(m[1])] : void 0;
|
|
532
|
+
return { ...i, where: node ? nodeDisplayLabel(node) : null };
|
|
533
|
+
});
|
|
534
|
+
deactivationOpen.value = true;
|
|
535
|
+
} else {
|
|
536
|
+
saveError.value = e.message || "Save failed.";
|
|
537
|
+
}
|
|
538
|
+
} finally {
|
|
539
|
+
saving.value = false;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
async function confirmDeactivate() {
|
|
543
|
+
if (!workflow.value || !dirtyBlob.value) {
|
|
544
|
+
deactivationOpen.value = false;
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
saving.value = true;
|
|
548
|
+
try {
|
|
549
|
+
await update(workflow.value.id, {
|
|
550
|
+
name: workflow.value.name,
|
|
551
|
+
description: workflow.value.description,
|
|
552
|
+
blob: dirtyBlob.value,
|
|
553
|
+
active: false,
|
|
554
|
+
executionMode: workflow.value.execution_mode
|
|
555
|
+
});
|
|
556
|
+
await get(workflow.value.id);
|
|
557
|
+
dirtyBlob.value = null;
|
|
558
|
+
deactivationOpen.value = false;
|
|
559
|
+
toast.success("Saved as paused \u2014 fix the highlighted settings, then activate again.");
|
|
560
|
+
} catch (e) {
|
|
561
|
+
deactivationOpen.value = false;
|
|
429
562
|
saveError.value = e.message || "Save failed.";
|
|
430
563
|
} finally {
|
|
431
564
|
saving.value = false;
|
|
@@ -513,8 +646,9 @@ function formatDateTime(iso) {
|
|
|
513
646
|
:variant="workflow.active ? 'outline' : 'default'"
|
|
514
647
|
class="h-7 text-sm"
|
|
515
648
|
:class="workflow.active ? 'text-amber-700 hover:text-amber-800' : 'bg-emerald-600 text-white hover:bg-emerald-700'"
|
|
516
|
-
:
|
|
517
|
-
|
|
649
|
+
:disabled="!workflow.active && isInvalid"
|
|
650
|
+
:title="workflow.active ? 'Pause this workflow' : isInvalid ? `Fix ${validationWarnings?.length} node setting(s) before activating` : 'Activate this workflow'"
|
|
651
|
+
@click="onToggleActive"
|
|
518
652
|
>
|
|
519
653
|
<component :is="workflow.active ? PauseIcon : Play" :size="12" />
|
|
520
654
|
{{ workflow.active ? "Pause" : "Activate" }}
|
|
@@ -540,6 +674,51 @@ function formatDateTime(iso) {
|
|
|
540
674
|
</NuxtLink>.
|
|
541
675
|
</div>
|
|
542
676
|
|
|
677
|
+
<!-- Manifest validation (PO-186): an inactive workflow with warnings can't
|
|
678
|
+
be activated. The banner names what's incomplete; the Activate button
|
|
679
|
+
stays disabled until the next save recomputes an empty warnings set.
|
|
680
|
+
Amber (not red) — the work is saved fine, it just isn't runnable yet.
|
|
681
|
+
One line per NODE, listing its fields (PO-209): the messages behind
|
|
682
|
+
them ride along as the line's title, and the line leads to the node. -->
|
|
683
|
+
<div
|
|
684
|
+
v-if="!openRunId && !workflow.active && isInvalid"
|
|
685
|
+
class="border-b border-amber-500/20 bg-amber-500/5 px-4 py-2 text-sm text-amber-800"
|
|
686
|
+
>
|
|
687
|
+
<p class="font-medium">
|
|
688
|
+
This workflow can’t be activated yet — {{ warningSummary }}:
|
|
689
|
+
</p>
|
|
690
|
+
<ul
|
|
691
|
+
class="mt-1 space-y-0.5"
|
|
692
|
+
:class="warningsExpanded ? 'max-h-36 overflow-y-auto pr-1' : ''"
|
|
693
|
+
>
|
|
694
|
+
<li
|
|
695
|
+
v-for="(g, i) in warningsExpanded ? warningGroups : warningGroups.slice(0, WARN_COLLAPSED)"
|
|
696
|
+
:key="`${g.nodeId ?? 'unattributed'}-${i}`"
|
|
697
|
+
class="flex gap-1.5"
|
|
698
|
+
>
|
|
699
|
+
<span class="select-none text-amber-500">•</span>
|
|
700
|
+
<!-- Attributed to a node: the whole line jumps there and opens its
|
|
701
|
+
settings. Otherwise there is nowhere to go — state the finding. -->
|
|
702
|
+
<button
|
|
703
|
+
v-if="g.nodeId"
|
|
704
|
+
type="button"
|
|
705
|
+
class="text-left underline decoration-amber-700/30 underline-offset-2 hover:decoration-amber-800"
|
|
706
|
+
:title="g.messages.join('\n')"
|
|
707
|
+
:aria-label="`Fix ${g.fields.length} setting${g.fields.length === 1 ? '' : 's'} in ${g.label}`"
|
|
708
|
+
@click="revealNode(g.nodeId)"
|
|
709
|
+
>
|
|
710
|
+
<span class="font-medium">{{ g.label }}</span> — {{ g.fields.join(", ") }}
|
|
711
|
+
</button>
|
|
712
|
+
<span v-else>{{ g.messages.join(" ") }}</span>
|
|
713
|
+
</li>
|
|
714
|
+
<li v-if="!warningsExpanded && warningGroups.length > WARN_COLLAPSED" class="pl-3">
|
|
715
|
+
<button type="button" class="text-amber-700/90 underline underline-offset-2 hover:text-amber-900" @click="warningsExpanded = true">
|
|
716
|
+
…and {{ warningGroups.length - WARN_COLLAPSED }} more — open a node to fix its config.
|
|
717
|
+
</button>
|
|
718
|
+
</li>
|
|
719
|
+
</ul>
|
|
720
|
+
</div>
|
|
721
|
+
|
|
543
722
|
<!-- Only the FAILED build state banners (its message matters); the
|
|
544
723
|
working states live as the header chip — and inside a run the
|
|
545
724
|
workflow's build state is noise, so nothing shows at all. -->
|
|
@@ -569,15 +748,23 @@ function formatDateTime(iso) {
|
|
|
569
748
|
</template>
|
|
570
749
|
|
|
571
750
|
<!-- Editor tab — fills the remaining height inside the panel via
|
|
572
|
-
the flex wrapper in the studio layout.
|
|
751
|
+
the flex wrapper in the studio layout.
|
|
752
|
+
|
|
753
|
+
overlay-open: every lightbox this page owns renders as a sibling of
|
|
754
|
+
the tabs, so the editor stays mounted underneath one and would keep
|
|
755
|
+
its keyboard shortcuts live (Delete deleting the selected node behind
|
|
756
|
+
the dialog — PO-208). Add new page-level dialogs to this list. -->
|
|
573
757
|
<div v-else-if="tab === 'editor'" class="flex-1 min-h-0">
|
|
574
758
|
<WorkflowEditor
|
|
575
759
|
:key="editorResetKey"
|
|
760
|
+
ref="editorRef"
|
|
576
761
|
:workflow="workflow"
|
|
577
|
-
:run-busy="running || syncLoading"
|
|
762
|
+
:run-busy="running || syncLoading || waitingForBuild"
|
|
763
|
+
:run-waiting="waitingForBuild"
|
|
764
|
+
:overlay-open="settingsOpen || duplicateOpen || syncOpen || runPayloadOpen || deactivationOpen"
|
|
578
765
|
@change="onChange"
|
|
579
766
|
@run="runFromEditor"
|
|
580
|
-
@toggle-active="
|
|
767
|
+
@toggle-active="onToggleActive"
|
|
581
768
|
@open-settings="settingsOpen = true"
|
|
582
769
|
/>
|
|
583
770
|
</div>
|
|
@@ -669,36 +856,36 @@ function formatDateTime(iso) {
|
|
|
669
856
|
<template v-if="workflow.execution_mode === 'caller_decides'">
|
|
670
857
|
<Button
|
|
671
858
|
variant="outline"
|
|
672
|
-
:disabled="running || syncLoading ||
|
|
673
|
-
:title="
|
|
859
|
+
:disabled="running || syncLoading || waitingForBuild || buildFailed"
|
|
860
|
+
:title="buildFailed ? 'The workflow bundle failed to build.' : void 0"
|
|
674
861
|
class="h-8 text-sm"
|
|
675
862
|
@click="runFromPayloadDialog('sync')"
|
|
676
863
|
>
|
|
677
|
-
<Loader2 v-if="syncLoading" :size="14" class="animate-spin" />
|
|
864
|
+
<Loader2 v-if="syncLoading || waitingMode === 'sync'" :size="14" class="animate-spin" />
|
|
678
865
|
<Play v-else :size="14" />
|
|
679
|
-
Run (sync)
|
|
866
|
+
{{ waitingMode === "sync" ? "Waiting for build\u2026" : "Run (sync)" }}
|
|
680
867
|
</Button>
|
|
681
868
|
<Button
|
|
682
|
-
:disabled="running || syncLoading ||
|
|
683
|
-
:title="
|
|
869
|
+
:disabled="running || syncLoading || waitingForBuild || buildFailed"
|
|
870
|
+
:title="buildFailed ? 'The workflow bundle failed to build.' : void 0"
|
|
684
871
|
class="h-8 text-sm"
|
|
685
872
|
@click="runFromPayloadDialog('async')"
|
|
686
873
|
>
|
|
687
|
-
<Loader2 v-if="running" :size="14" class="animate-spin" />
|
|
874
|
+
<Loader2 v-if="running || waitingMode === 'async'" :size="14" class="animate-spin" />
|
|
688
875
|
<Play v-else :size="14" />
|
|
689
|
-
Run (async)
|
|
876
|
+
{{ waitingMode === "async" ? "Waiting for build\u2026" : "Run (async)" }}
|
|
690
877
|
</Button>
|
|
691
878
|
</template>
|
|
692
879
|
<Button
|
|
693
880
|
v-else
|
|
694
|
-
:disabled="running || syncLoading ||
|
|
695
|
-
:title="
|
|
881
|
+
:disabled="running || syncLoading || waitingForBuild || buildFailed"
|
|
882
|
+
:title="buildFailed ? 'The workflow bundle failed to build.' : void 0"
|
|
696
883
|
class="h-8 text-sm"
|
|
697
884
|
@click="runFromPayloadDialog(primaryMode)"
|
|
698
885
|
>
|
|
699
|
-
<Loader2 v-if="running || syncLoading" :size="14" class="animate-spin" />
|
|
886
|
+
<Loader2 v-if="running || syncLoading || waitingForBuild" :size="14" class="animate-spin" />
|
|
700
887
|
<Play v-else :size="14" />
|
|
701
|
-
Run ({
|
|
888
|
+
{{ waitingForBuild ? "Waiting for build\u2026" : `Run (${primaryMode})` }}
|
|
702
889
|
</Button>
|
|
703
890
|
</div>
|
|
704
891
|
</DialogContent>
|
|
@@ -729,5 +916,36 @@ function formatDateTime(iso) {
|
|
|
729
916
|
/>
|
|
730
917
|
|
|
731
918
|
<WorkflowDuplicateDialog v-model:open="duplicateOpen" :workflow="duplicateTarget" @duplicated="onDuplicated" />
|
|
919
|
+
|
|
920
|
+
<!-- Active-save blocked by manifest validation: keep the work by saving it
|
|
921
|
+
as paused. Cancelling persists nothing (the 422 already rolled back). -->
|
|
922
|
+
<AlertDialog :open="deactivationOpen" @update:open="(v) => {
|
|
923
|
+
if (!v) deactivationOpen = false;
|
|
924
|
+
}">
|
|
925
|
+
<AlertDialogContent>
|
|
926
|
+
<AlertDialogHeader>
|
|
927
|
+
<AlertDialogTitle>Can’t keep this workflow active</AlertDialogTitle>
|
|
928
|
+
<AlertDialogDescription>
|
|
929
|
+
These node settings must be valid while the workflow is active. Save it as
|
|
930
|
+
<span class="font-medium">paused</span> to keep your changes, then fix them and activate again — or cancel to discard this save.
|
|
931
|
+
</AlertDialogDescription>
|
|
932
|
+
</AlertDialogHeader>
|
|
933
|
+
<ul class="max-h-48 space-y-0.5 overflow-y-auto rounded-md border border-amber-500/20 bg-amber-500/5 px-3 py-2 text-sm text-amber-800">
|
|
934
|
+
<li v-for="(w, i) in deactivationIssues" :key="`${w.path}-${i}`" class="flex gap-1.5">
|
|
935
|
+
<span class="select-none text-amber-500">•</span>
|
|
936
|
+
<span><span v-if="w.where" class="font-medium">{{ w.where }}:</span> {{ w.message }}</span>
|
|
937
|
+
</li>
|
|
938
|
+
</ul>
|
|
939
|
+
<AlertDialogFooter>
|
|
940
|
+
<Button variant="outline" :disabled="saving" @click="deactivationOpen = false">
|
|
941
|
+
Cancel
|
|
942
|
+
</Button>
|
|
943
|
+
<Button class="bg-emerald-600 text-white hover:bg-emerald-700" :disabled="saving" @click="confirmDeactivate">
|
|
944
|
+
<Loader2 v-if="saving" :size="12" class="animate-spin" />
|
|
945
|
+
Save as paused
|
|
946
|
+
</Button>
|
|
947
|
+
</AlertDialogFooter>
|
|
948
|
+
</AlertDialogContent>
|
|
949
|
+
</AlertDialog>
|
|
732
950
|
</IntegrationsLayout>
|
|
733
951
|
</template>
|
|
@@ -4,10 +4,11 @@ import { Button } from "@revenexx/studio";
|
|
|
4
4
|
import WorkflowsTable from "../../../components/Integrations/WorkflowsTable.vue";
|
|
5
5
|
import WorkflowCreateDialog from "../../../components/Integrations/WorkflowCreateDialog.vue";
|
|
6
6
|
useHead({ title: "Workflows \xB7 Revenexx Cockpit" });
|
|
7
|
-
const { workflows, loading, load } = useIntegrationsWorkflows();
|
|
7
|
+
const { workflows, loading, error, load } = useIntegrationsWorkflows();
|
|
8
8
|
onMounted(() => {
|
|
9
9
|
void load();
|
|
10
10
|
});
|
|
11
|
+
const loadFailed = computed(() => !!error && !!error.value && !workflows.value.length);
|
|
11
12
|
const createOpen = ref(false);
|
|
12
13
|
</script>
|
|
13
14
|
|
|
@@ -25,7 +26,19 @@ const createOpen = ref(false);
|
|
|
25
26
|
</PageHeader>
|
|
26
27
|
|
|
27
28
|
<div class="p-4">
|
|
28
|
-
<
|
|
29
|
+
<StateCard
|
|
30
|
+
v-if="loadFailed"
|
|
31
|
+
variant="error"
|
|
32
|
+
title="Could not load workflows"
|
|
33
|
+
:description="error ?? void 0"
|
|
34
|
+
>
|
|
35
|
+
<template #actions>
|
|
36
|
+
<Button variant="outline" class="h-8 text-sm" @click="load(true)">
|
|
37
|
+
Try again
|
|
38
|
+
</Button>
|
|
39
|
+
</template>
|
|
40
|
+
</StateCard>
|
|
41
|
+
<WorkflowsTable v-else :workflows="workflows" :loading="loading" />
|
|
29
42
|
</div>
|
|
30
43
|
|
|
31
44
|
<WorkflowCreateDialog v-model:open="createOpen" />
|