@revenexx/studio-integrations 0.5.1 → 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 +20 -1
- package/dist/runtime/components/Integrations/WorkflowEditor.vue +167 -46
- package/dist/runtime/components/Integrations/WorkflowEditor.vue.d.ts +20 -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 +196 -8
- 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();
|
|
@@ -100,6 +125,7 @@ watch(id, async () => {
|
|
|
100
125
|
const w = await get(id.value);
|
|
101
126
|
if (!w) await router.push("/integrations/workflows");
|
|
102
127
|
dirtyBlob.value = null;
|
|
128
|
+
warningsExpanded.value = false;
|
|
103
129
|
if (tab.value === "runs") void loadRuns(1);
|
|
104
130
|
}, { immediate: true });
|
|
105
131
|
function onChange(blob) {
|
|
@@ -152,6 +178,29 @@ function syncErrorMessage(e) {
|
|
|
152
178
|
}
|
|
153
179
|
return e?.message || "Sync run failed";
|
|
154
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
|
+
}
|
|
155
204
|
async function run(mode) {
|
|
156
205
|
if (!workflow.value) return;
|
|
157
206
|
let payload;
|
|
@@ -179,7 +228,7 @@ async function run(mode) {
|
|
|
179
228
|
syncResponse.value = await triggerRun(workflow.value.id, payload, "sync");
|
|
180
229
|
runModalRunId.value = extractRunId(syncResponse.value);
|
|
181
230
|
} catch (e) {
|
|
182
|
-
|
|
231
|
+
await reportRunFailure(e, syncErrorMessage);
|
|
183
232
|
if (e instanceof IntegrationsApiError) {
|
|
184
233
|
runModalRunId.value = extractRunId(e.body?.run);
|
|
185
234
|
}
|
|
@@ -194,7 +243,7 @@ async function run(mode) {
|
|
|
194
243
|
const res = await triggerRun(workflow.value.id, payload, "async");
|
|
195
244
|
runModalRunId.value = extractRunId(res);
|
|
196
245
|
} catch (e) {
|
|
197
|
-
|
|
246
|
+
await reportRunFailure(e, (err) => err?.message || "Could not start the run.");
|
|
198
247
|
} finally {
|
|
199
248
|
running.value = false;
|
|
200
249
|
syncLoading.value = false;
|
|
@@ -441,6 +490,25 @@ async function saveWorkflowMeta(meta) {
|
|
|
441
490
|
savingMeta.value = false;
|
|
442
491
|
}
|
|
443
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
|
+
}
|
|
444
512
|
async function save() {
|
|
445
513
|
if (!workflow.value || !dirtyBlob.value) return;
|
|
446
514
|
saving.value = true;
|
|
@@ -455,6 +523,42 @@ async function save() {
|
|
|
455
523
|
});
|
|
456
524
|
dirtyBlob.value = null;
|
|
457
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;
|
|
458
562
|
saveError.value = e.message || "Save failed.";
|
|
459
563
|
} finally {
|
|
460
564
|
saving.value = false;
|
|
@@ -542,8 +646,9 @@ function formatDateTime(iso) {
|
|
|
542
646
|
:variant="workflow.active ? 'outline' : 'default'"
|
|
543
647
|
class="h-7 text-sm"
|
|
544
648
|
:class="workflow.active ? 'text-amber-700 hover:text-amber-800' : 'bg-emerald-600 text-white hover:bg-emerald-700'"
|
|
545
|
-
:
|
|
546
|
-
|
|
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"
|
|
547
652
|
>
|
|
548
653
|
<component :is="workflow.active ? PauseIcon : Play" :size="12" />
|
|
549
654
|
{{ workflow.active ? "Pause" : "Activate" }}
|
|
@@ -569,6 +674,51 @@ function formatDateTime(iso) {
|
|
|
569
674
|
</NuxtLink>.
|
|
570
675
|
</div>
|
|
571
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
|
+
|
|
572
722
|
<!-- Only the FAILED build state banners (its message matters); the
|
|
573
723
|
working states live as the header chip — and inside a run the
|
|
574
724
|
workflow's build state is noise, so nothing shows at all. -->
|
|
@@ -598,16 +748,23 @@ function formatDateTime(iso) {
|
|
|
598
748
|
</template>
|
|
599
749
|
|
|
600
750
|
<!-- Editor tab — fills the remaining height inside the panel via
|
|
601
|
-
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. -->
|
|
602
757
|
<div v-else-if="tab === 'editor'" class="flex-1 min-h-0">
|
|
603
758
|
<WorkflowEditor
|
|
604
759
|
:key="editorResetKey"
|
|
760
|
+
ref="editorRef"
|
|
605
761
|
:workflow="workflow"
|
|
606
762
|
:run-busy="running || syncLoading || waitingForBuild"
|
|
607
763
|
:run-waiting="waitingForBuild"
|
|
764
|
+
:overlay-open="settingsOpen || duplicateOpen || syncOpen || runPayloadOpen || deactivationOpen"
|
|
608
765
|
@change="onChange"
|
|
609
766
|
@run="runFromEditor"
|
|
610
|
-
@toggle-active="
|
|
767
|
+
@toggle-active="onToggleActive"
|
|
611
768
|
@open-settings="settingsOpen = true"
|
|
612
769
|
/>
|
|
613
770
|
</div>
|
|
@@ -759,5 +916,36 @@ function formatDateTime(iso) {
|
|
|
759
916
|
/>
|
|
760
917
|
|
|
761
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>
|
|
762
950
|
</IntegrationsLayout>
|
|
763
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" />
|