@revenexx/studio-integrations 3.1.0 → 3.2.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 +12 -0
- package/dist/module.json +1 -1
- package/dist/runtime/components/Integrations/RunDetailPanel.vue +3 -2
- package/dist/runtime/components/Integrations/TemplateInstallStep.vue +28 -24
- package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +14 -2
- package/dist/runtime/composables/useIntegrationsRuns.d.ts +1 -1
- package/dist/runtime/composables/useIntegrationsTemplates.d.ts +2 -0
- package/dist/runtime/composables/useIntegrationsTemplates.js +15 -9
- package/dist/runtime/i18n/messages/de.json +4 -0
- package/dist/runtime/i18n/messages/en.json +4 -0
- package/dist/runtime/pages/integrations/templates/[slug].vue +21 -22
- package/dist/runtime/types/integrations.d.ts +16 -1
- package/dist/runtime/utils/stepError.d.ts +13 -0
- package/dist/runtime/utils/stepError.js +24 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @revenexx/studio-integrations
|
|
2
2
|
|
|
3
|
+
## 3.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 60e6143: A template whose requirements the platform could not answer for is no longer shown as ready to install. The template page's setup rail and the install step now say they could not check what the template needs, with the platform's reason, instead of "Everything is ready to install" and "This template needs no credentials"; installing stays possible. While the answer is still on its way, the rail claims nothing.
|
|
8
|
+
|
|
9
|
+
For hosts: `useIntegrationsTemplates()` gains `checkRequirements(slug)`, which rejects with the platform's reason where `requirements(slug)` resolves to `null`, and `uncheckedReason(error)`, which turns that rejection into the sentence the studio shows. `requirements` still behaves as before but is now deprecated: its `null` cannot tell "could not check" from "needs nothing", so a host that reads it as an empty answer shows an unchecked template as ready. Switch to `checkRequirements`.
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- e62ae58: A failed step now says why it failed in one sentence. The run record's step list showed the whole error the platform sends, as a block of JSON; it now shows the message. The replay's debug panel shows the message and the failure's type, and the places in the node's own code where it failed as a list that stays folded until opened.
|
|
14
|
+
|
|
3
15
|
## 3.1.0
|
|
4
16
|
|
|
5
17
|
### Minor Changes
|
package/dist/module.json
CHANGED
|
@@ -4,6 +4,7 @@ import { Download as DownloadIcon, RefreshCircle as RefreshIcon } from "#solar-i
|
|
|
4
4
|
import { Badge, Button, Dialog, DialogContent, DialogHeader, DialogTitle } from "@revenexx/studio";
|
|
5
5
|
import { runStatusClass, runStatusLabel } from "../../utils/statusStyle";
|
|
6
6
|
import { isSkippedStep, skipReasonMessage, stepStatusLabel } from "../../utils/skippedStep";
|
|
7
|
+
import { stepError } from "../../utils/stepError";
|
|
7
8
|
import { IntegrationsApiError, errorReason } from "../../utils/apiError";
|
|
8
9
|
import { createPollBudget, SAFETY_POLL_MS } from "../../utils/pollBudget";
|
|
9
10
|
import { formatDuration } from "../../utils/formatDuration";
|
|
@@ -764,8 +765,8 @@ async function confirmAction() {
|
|
|
764
765
|
<span class="text-xs text-muted-foreground tabular-nums shrink-0">{{ fmtMs(st.duration_ms) }}</span>
|
|
765
766
|
<span v-if="!isSkippedStep(st)" class="text-[10px] text-muted-foreground shrink-0">{{ expandedStep === stepKey(st, i) ? "\u25BC" : "\u25B6" }}</span>
|
|
766
767
|
</button>
|
|
767
|
-
<div v-if="st.error" class="px-3 pb-2 -mt-1">
|
|
768
|
-
<div class="rounded-md border border-rose-500/20 bg-rose-500/5 px-2 py-1 text-xs text-rose-700 break-words">{{ st.error }}</div>
|
|
768
|
+
<div v-if="stepError(st.error)" class="px-3 pb-2 -mt-1">
|
|
769
|
+
<div class="rounded-md border border-rose-500/20 bg-rose-500/5 px-2 py-1 text-xs text-rose-700 break-words whitespace-pre-wrap" data-testid="step-error">{{ stepError(st.error).message }}</div>
|
|
769
770
|
</div>
|
|
770
771
|
<div v-if="expandedStep === stepKey(st, i)" class="px-3 pb-3 space-y-2">
|
|
771
772
|
<template v-if="hasPayload(st)">
|
|
@@ -20,10 +20,11 @@ const props = defineProps({
|
|
|
20
20
|
});
|
|
21
21
|
const emit = defineEmits(["installed", "update:loading", "update:installing"]);
|
|
22
22
|
const t = useIntegrationsT();
|
|
23
|
-
const { get, load, fetchDetail,
|
|
23
|
+
const { get, load, fetchDetail, checkRequirements, uncheckedReason, instantiate } = useIntegrationsTemplates();
|
|
24
24
|
const { keys: secretKeys, load: loadSecrets } = useIntegrationsSecrets();
|
|
25
25
|
const { ofType, refresh: refreshCredentials } = useIntegrationsCredentials();
|
|
26
26
|
const reqs = ref(null);
|
|
27
|
+
const reqsError = ref(null);
|
|
27
28
|
const loading = ref(true);
|
|
28
29
|
const installing = ref(false);
|
|
29
30
|
const error = ref(null);
|
|
@@ -35,40 +36,37 @@ watch(() => props.slug, async (slug) => {
|
|
|
35
36
|
const mine = ++run;
|
|
36
37
|
loading.value = true;
|
|
37
38
|
reqs.value = null;
|
|
39
|
+
reqsError.value = null;
|
|
38
40
|
error.value = null;
|
|
39
41
|
name.value = "";
|
|
40
42
|
for (const k of Object.keys(bindings)) delete bindings[k];
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
43
|
+
let answer = null;
|
|
44
|
+
let failure = null;
|
|
45
|
+
try {
|
|
46
|
+
await load();
|
|
47
|
+
if (mine !== run) return;
|
|
48
|
+
if (name.value === "") name.value = get(slug)?.name ?? "";
|
|
49
|
+
await Promise.all([fetchDetail(slug), refreshCredentials(), loadSecrets()]);
|
|
50
|
+
if (mine !== run) return;
|
|
51
|
+
answer = await checkRequirements(slug);
|
|
52
|
+
} catch (e) {
|
|
53
|
+
failure = uncheckedReason(e);
|
|
54
|
+
}
|
|
47
55
|
if (mine !== run) return;
|
|
48
56
|
reqs.value = answer;
|
|
57
|
+
reqsError.value = failure;
|
|
49
58
|
loading.value = false;
|
|
50
59
|
}, { immediate: true });
|
|
51
60
|
watch(loading, (v) => emit("update:loading", v), { immediate: true });
|
|
52
61
|
watch(installing, (v) => emit("update:installing", v));
|
|
53
62
|
onBeforeUnmount(() => emit("update:loading", false));
|
|
54
|
-
const tpl = computed(() => get(props.slug));
|
|
55
63
|
const credentialReqs = computed(() => reqs.value?.credentialTypes ?? []);
|
|
56
64
|
const secretReqs = computed(() => reqs.value?.secrets ?? []);
|
|
57
|
-
const secretChecks = computed(() => {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
label: s.key ?? t("integration.templates.detail.secrets.chosenAtInstall"),
|
|
63
|
-
present: s.exists || (s.key ? secretKeys.value.includes(s.key) : false)
|
|
64
|
-
}));
|
|
65
|
-
}
|
|
66
|
-
return (tpl.value?.requiredSecrets ?? []).map((key) => ({
|
|
67
|
-
key,
|
|
68
|
-
label: key,
|
|
69
|
-
present: secretKeys.value.includes(key)
|
|
70
|
-
}));
|
|
71
|
-
});
|
|
65
|
+
const secretChecks = computed(() => secretReqs.value.map((s) => ({
|
|
66
|
+
key: s.key,
|
|
67
|
+
label: s.key ?? t("integration.templates.detail.secrets.chosenAtInstall"),
|
|
68
|
+
present: s.exists || (s.key ? secretKeys.value.includes(s.key) : false)
|
|
69
|
+
})));
|
|
72
70
|
function slotsFor(usedBy) {
|
|
73
71
|
const all = credentialReqs.value.flatMap((r) => r.usedBy);
|
|
74
72
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -128,7 +126,13 @@ async function confirmInstall() {
|
|
|
128
126
|
<div class="text-mono text-muted-foreground border-b border-black/[0.06] pb-1.5">
|
|
129
127
|
{{ t("integration.templates.detail.credentials.title") }}
|
|
130
128
|
</div>
|
|
131
|
-
|
|
129
|
+
<!-- Two lines, not one sentence around {reason}: the platform's message
|
|
130
|
+
ends in a full stop or not, and a template cannot know which. -->
|
|
131
|
+
<div v-if="reqsError" class="rounded-md border-l-2 border-amber-500/40 bg-amber-500/5 px-2.5 py-1.5 text-sm text-amber-700" data-testid="template-install-unchecked">
|
|
132
|
+
<p>{{ t("integration.templates.detail.setup.unchecked", { reason: reqsError }) }}</p>
|
|
133
|
+
<p>{{ t("integration.templates.detail.install.stillInstallable") }}</p>
|
|
134
|
+
</div>
|
|
135
|
+
<p v-else-if="!credentialReqs.length" class="text-sm text-muted-foreground italic">
|
|
132
136
|
{{ t("integration.templates.detail.install.noCredentials") }}
|
|
133
137
|
</p>
|
|
134
138
|
<div v-for="req in credentialReqs" :key="req.slug" class="space-y-2">
|
|
@@ -31,6 +31,7 @@ import { formatDuration } from "../../utils/formatDuration";
|
|
|
31
31
|
import { SAFETY_POLL_MS } from "../../utils/pollBudget";
|
|
32
32
|
import { runStatusClass, runStatusLabel } from "../../utils/statusStyle";
|
|
33
33
|
import { executedNodeIds, isSkippedStep, skipReasonMessage, stepStatusLabel } from "../../utils/skippedStep";
|
|
34
|
+
import { frameLabel, stepError } from "../../utils/stepError";
|
|
34
35
|
import { errorReason } from "../../utils/apiError";
|
|
35
36
|
const props = defineProps({
|
|
36
37
|
workflow: { type: Object, required: true },
|
|
@@ -108,6 +109,7 @@ const playing = ref(false);
|
|
|
108
109
|
let playTimer = null;
|
|
109
110
|
const safeCursor = computed(() => steps.value.length === 0 ? 0 : Math.min(cursor.value, steps.value.length - 1));
|
|
110
111
|
const activeStep = computed(() => steps.value[safeCursor.value]);
|
|
112
|
+
const activeError = computed(() => stepError(activeStep.value?.error));
|
|
111
113
|
function seek(i) {
|
|
112
114
|
playing.value = false;
|
|
113
115
|
cursor.value = Math.max(0, Math.min(i, steps.value.length - 1));
|
|
@@ -529,9 +531,19 @@ watch(() => props.initialRunId, (id) => {
|
|
|
529
531
|
<code class="rounded bg-black/[0.05] px-1.5 py-0.5 font-mono">{{ activeStep.branch }}</code>
|
|
530
532
|
</div>
|
|
531
533
|
|
|
532
|
-
<div v-if="
|
|
534
|
+
<div v-if="activeError" class="text-sm" data-testid="replay-step-error">
|
|
533
535
|
<div class="text-mono text-muted-foreground mb-1">{{ t("integration.replay.debug.field.error") }}</div>
|
|
534
|
-
<
|
|
536
|
+
<div class="max-h-40 overflow-auto rounded-md border border-rose-500/20 bg-rose-500/5 p-2 text-sm text-rose-700 break-words whitespace-pre-wrap">{{ activeError.message }}</div>
|
|
537
|
+
<div v-if="activeError.type" class="mt-1 font-mono text-xs text-muted-foreground">{{ activeError.type }}</div>
|
|
538
|
+
<!-- Where the node's own code failed (PO-447): for whoever maintains the
|
|
539
|
+
node, so folded rather than beside the reason (IN-12). Keyed on the step so
|
|
540
|
+
it folds again on the next failed one. -->
|
|
541
|
+
<details v-if="activeError.frames?.length" :key="safeCursor" class="mt-1">
|
|
542
|
+
<summary class="cursor-pointer text-xs text-muted-foreground">{{ t("integration.replay.debug.field.frames") }}</summary>
|
|
543
|
+
<ol class="mt-1 space-y-0.5 font-mono text-xs">
|
|
544
|
+
<li v-for="(frame, fi) in activeError.frames" :key="fi">{{ frameLabel(frame) }}</li>
|
|
545
|
+
</ol>
|
|
546
|
+
</details>
|
|
535
547
|
</div>
|
|
536
548
|
|
|
537
549
|
<div v-if="activeStep.input !== void 0 && activeStep.input !== null" class="text-sm">
|
|
@@ -44,7 +44,7 @@ export declare function useIntegrationsRuns(): {
|
|
|
44
44
|
latestLoading: any;
|
|
45
45
|
loadLatestPerWorkflow: (force?: boolean) => Promise<WorkflowRun[]>;
|
|
46
46
|
lastRunFor: (workflowId: number) => WorkflowRun | null;
|
|
47
|
-
trigger: (workflowId: number | string, payload?: Record<string, unknown>, mode?: "
|
|
47
|
+
trigger: (workflowId: number | string, payload?: Record<string, unknown>, mode?: "async" | "sync") => Promise<unknown>;
|
|
48
48
|
cancelRun: (workflowId: number | string, runId: number | string, reason?: string) => Promise<unknown>;
|
|
49
49
|
terminateRun: (workflowId: number | string, runId: number | string, reason?: string) => Promise<unknown>;
|
|
50
50
|
retryRun: (workflowId: number | string, runId: number | string) => Promise<unknown>;
|
|
@@ -12,6 +12,8 @@ export declare function useIntegrationsTemplates(): {
|
|
|
12
12
|
fetchDetail: (slug: string) => Promise<WorkflowTemplate | null>;
|
|
13
13
|
ensureDetails: (slugs: string[], _concurrency?: number) => Promise<void>;
|
|
14
14
|
requirements: (slug: string) => Promise<TemplateRequirements | null>;
|
|
15
|
+
checkRequirements: (slug: string) => Promise<TemplateRequirements>;
|
|
16
|
+
uncheckedReason: (err: unknown) => string;
|
|
15
17
|
instantiate: (slug: string, opts?: {
|
|
16
18
|
name?: string;
|
|
17
19
|
credentialBindings?: Record<string, string | Record<string, string>>;
|
|
@@ -165,16 +165,22 @@ export function useIntegrationsTemplates() {
|
|
|
165
165
|
if (!wanted.length) return;
|
|
166
166
|
remember(await loader.many(wanted));
|
|
167
167
|
}
|
|
168
|
+
async function checkRequirements(slug) {
|
|
169
|
+
const raw = unwrapRecord(await api.get(`/templates/${slug}/requirements`));
|
|
170
|
+
return {
|
|
171
|
+
...raw,
|
|
172
|
+
credentialTypes: (raw.credentialTypes ?? []).map((c) => ({
|
|
173
|
+
...c,
|
|
174
|
+
name: c.name ? resolveLocalized(c.name) : c.name
|
|
175
|
+
}))
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
function uncheckedReason(err) {
|
|
179
|
+
return errorReason(err, t("integration.templates.detail.setup.uncheckedFallback"));
|
|
180
|
+
}
|
|
168
181
|
async function requirements(slug) {
|
|
169
182
|
try {
|
|
170
|
-
|
|
171
|
-
return {
|
|
172
|
-
...raw,
|
|
173
|
-
credentialTypes: (raw.credentialTypes ?? []).map((c) => ({
|
|
174
|
-
...c,
|
|
175
|
-
name: c.name ? resolveLocalized(c.name) : c.name
|
|
176
|
-
}))
|
|
177
|
-
};
|
|
183
|
+
return await checkRequirements(slug);
|
|
178
184
|
} catch {
|
|
179
185
|
return null;
|
|
180
186
|
}
|
|
@@ -194,5 +200,5 @@ export function useIntegrationsTemplates() {
|
|
|
194
200
|
}
|
|
195
201
|
const total = computed(() => templates.value.length);
|
|
196
202
|
const startingPoints = computed(() => templates.value.slice(0, 4));
|
|
197
|
-
return { templates, loaded, loading, error, total, startingPoints, load, list, get, fetchDetail, ensureDetails, requirements, instantiate, install };
|
|
203
|
+
return { templates, loaded, loading, error, total, startingPoints, load, list, get, fetchDetail, ensureDetails, requirements, checkRequirements, uncheckedReason, instantiate, install };
|
|
198
204
|
}
|
|
@@ -920,6 +920,7 @@
|
|
|
920
920
|
"integration.replay.debug.field.config": "Konfiguration",
|
|
921
921
|
"integration.replay.debug.field.duration": "Dauer",
|
|
922
922
|
"integration.replay.debug.field.error": "Fehler",
|
|
923
|
+
"integration.replay.debug.field.frames": "Stelle im Code des Nodes",
|
|
923
924
|
"integration.replay.debug.field.input": "Eingabe",
|
|
924
925
|
"integration.replay.debug.field.output": "Ausgabe",
|
|
925
926
|
"integration.replay.debug.field.started": "Gestartet",
|
|
@@ -1172,6 +1173,7 @@
|
|
|
1172
1173
|
"integration.templates.detail.install.state.loading": "Es wird geprüft, was diese Vorlage benötigt…",
|
|
1173
1174
|
"integration.templates.detail.install.submit": "Installieren",
|
|
1174
1175
|
"integration.templates.detail.install.submitting": "Wird eingerichtet…",
|
|
1176
|
+
"integration.templates.detail.install.stillInstallable": "Sie können sie trotzdem installieren und die Zugangsdaten im Editor verbinden.",
|
|
1175
1177
|
"integration.templates.detail.publisher.kind.community": "Community",
|
|
1176
1178
|
"integration.templates.detail.publisher.kind.partner": "Partner",
|
|
1177
1179
|
"integration.templates.detail.publisher.kind.revenexx": "Offiziell",
|
|
@@ -1189,6 +1191,8 @@
|
|
|
1189
1191
|
"integration.templates.detail.setup.missingCredentials": "{n} Zugangsdatensatz zu verbinden | {n} Zugangsdatensätze zu verbinden",
|
|
1190
1192
|
"integration.templates.detail.setup.missingSecrets": "{n} Secret fehlt | {n} Secrets fehlen",
|
|
1191
1193
|
"integration.templates.detail.setup.ready": "Alles ist bereit für die Installation.",
|
|
1194
|
+
"integration.templates.detail.setup.unchecked": "Es ließ sich nicht prüfen, was diese Vorlage benötigt: {reason}",
|
|
1195
|
+
"integration.templates.detail.setup.uncheckedFallback": "Die Plattform hat nicht geantwortet.",
|
|
1192
1196
|
"integration.templates.detail.state.loading": "Vorlage wird geladen…",
|
|
1193
1197
|
"integration.templates.detail.state.notFound": "Vorlage nicht gefunden.",
|
|
1194
1198
|
"integration.templates.detail.steps.empty": "Für diese Vorlage gibt es noch keine Schritt-für-Schritt-Übersicht.",
|
|
@@ -920,6 +920,7 @@
|
|
|
920
920
|
"integration.replay.debug.field.config": "Config",
|
|
921
921
|
"integration.replay.debug.field.duration": "Duration",
|
|
922
922
|
"integration.replay.debug.field.error": "Error",
|
|
923
|
+
"integration.replay.debug.field.frames": "Where in the node's code",
|
|
923
924
|
"integration.replay.debug.field.input": "Input",
|
|
924
925
|
"integration.replay.debug.field.output": "Output",
|
|
925
926
|
"integration.replay.debug.field.started": "Started",
|
|
@@ -1172,6 +1173,7 @@
|
|
|
1172
1173
|
"integration.templates.detail.install.state.loading": "Checking what this template needs…",
|
|
1173
1174
|
"integration.templates.detail.install.submit": "Install",
|
|
1174
1175
|
"integration.templates.detail.install.submitting": "Setting up…",
|
|
1176
|
+
"integration.templates.detail.install.stillInstallable": "You can still install it and connect its credentials in the editor.",
|
|
1175
1177
|
"integration.templates.detail.publisher.kind.community": "Community",
|
|
1176
1178
|
"integration.templates.detail.publisher.kind.partner": "Partner",
|
|
1177
1179
|
"integration.templates.detail.publisher.kind.revenexx": "Official",
|
|
@@ -1189,6 +1191,8 @@
|
|
|
1189
1191
|
"integration.templates.detail.setup.missingCredentials": "{n} credential to connect | {n} credentials to connect",
|
|
1190
1192
|
"integration.templates.detail.setup.missingSecrets": "{n} secret missing | {n} secrets missing",
|
|
1191
1193
|
"integration.templates.detail.setup.ready": "Everything is ready to install.",
|
|
1194
|
+
"integration.templates.detail.setup.unchecked": "Could not check what this template needs: {reason}",
|
|
1195
|
+
"integration.templates.detail.setup.uncheckedFallback": "The platform did not answer.",
|
|
1192
1196
|
"integration.templates.detail.state.loading": "Loading template…",
|
|
1193
1197
|
"integration.templates.detail.state.notFound": "Template not found.",
|
|
1194
1198
|
"integration.templates.detail.steps.empty": "No step-by-step outline for this template yet.",
|
|
@@ -22,7 +22,7 @@ import { keepOpenBeside } from "../../../utils/dialog";
|
|
|
22
22
|
const t = useIntegrationsT();
|
|
23
23
|
const route = useRoute();
|
|
24
24
|
const router = useRouter();
|
|
25
|
-
const { get, load, loaded, fetchDetail,
|
|
25
|
+
const { get, load, loaded, fetchDetail, checkRequirements, uncheckedReason } = useIntegrationsTemplates();
|
|
26
26
|
const { load: loadNodeCatalog, findManifest, resolveLocalized } = useIntegrationsNodes();
|
|
27
27
|
const { keys: secretKeys, load: loadSecrets } = useIntegrationsSecrets();
|
|
28
28
|
const { ofType, load: loadCredentials } = useIntegrationsCredentials();
|
|
@@ -30,10 +30,15 @@ const slug = computed(() => String(route.params.slug));
|
|
|
30
30
|
const tpl = computed(() => get(slug.value));
|
|
31
31
|
useHead({ title: () => t("integration.templates.detail.head.title", { name: tpl.value?.name ?? t("integration.templates.detail.head.fallbackName") }) });
|
|
32
32
|
const reqs = ref(null);
|
|
33
|
+
const reqsError = ref(null);
|
|
33
34
|
onMounted(async () => {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
try {
|
|
36
|
+
await load();
|
|
37
|
+
await Promise.all([fetchDetail(slug.value), loadNodeCatalog(false, { summary: true }), loadCredentials(), loadSecrets()]);
|
|
38
|
+
reqs.value = await checkRequirements(slug.value);
|
|
39
|
+
} catch (e) {
|
|
40
|
+
reqsError.value = uncheckedReason(e);
|
|
41
|
+
}
|
|
37
42
|
});
|
|
38
43
|
const descriptionHtml = computed(() => renderTemplateDescription(tpl.value?.descriptionMd));
|
|
39
44
|
const steps = computed(() => {
|
|
@@ -104,24 +109,14 @@ const variableColumns = computed(() => [
|
|
|
104
109
|
]);
|
|
105
110
|
const credentialReqs = computed(() => reqs.value?.credentialTypes ?? []);
|
|
106
111
|
const secretReqs = computed(() => reqs.value?.secrets ?? []);
|
|
107
|
-
const secretChecks = computed(() => {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
label: s.key ?? t("integration.templates.detail.secrets.chosenAtInstall"),
|
|
113
|
-
present: s.exists || (s.key ? secretKeys.value.includes(s.key) : false)
|
|
114
|
-
}));
|
|
115
|
-
}
|
|
116
|
-
return (tpl.value?.requiredSecrets ?? []).map((key) => ({
|
|
117
|
-
key,
|
|
118
|
-
label: key,
|
|
119
|
-
present: secretKeys.value.includes(key)
|
|
120
|
-
}));
|
|
121
|
-
});
|
|
112
|
+
const secretChecks = computed(() => secretReqs.value.map((s) => ({
|
|
113
|
+
key: s.key,
|
|
114
|
+
label: s.key ?? t("integration.templates.detail.secrets.chosenAtInstall"),
|
|
115
|
+
present: s.exists || (s.key ? secretKeys.value.includes(s.key) : false)
|
|
116
|
+
})));
|
|
122
117
|
const missingSecrets = computed(() => secretChecks.value.filter((s) => !s.present).length);
|
|
123
118
|
const missingCredentials = computed(() => credentialReqs.value.filter((c) => !ofType(c.slug).length).length);
|
|
124
|
-
const setupReady = computed(() => missingSecrets.value === 0 && missingCredentials.value === 0);
|
|
119
|
+
const setupReady = computed(() => reqs.value !== null && missingSecrets.value === 0 && missingCredentials.value === 0);
|
|
125
120
|
const packageLabel = computed(() => tpl.value ? templatePackageLabel(tpl.value) : null);
|
|
126
121
|
const categoryLabel = computed(() => templateCategoryLabel(tpl.value?.category ?? ""));
|
|
127
122
|
const installOpen = ref(false);
|
|
@@ -298,11 +293,15 @@ function levelDot(level) {
|
|
|
298
293
|
point `aria-labelledby` at. Copy this pattern only for a panel
|
|
299
294
|
that genuinely has none. -->
|
|
300
295
|
<div role="group" :aria-label="t('integration.templates.detail.setup.ariaLabel')" class="rounded-lg border border-black/[0.06] bg-white p-3 space-y-2.5">
|
|
301
|
-
<div v-if="
|
|
296
|
+
<div v-if="reqsError" class="flex items-start gap-1.5 text-sm text-amber-700" data-testid="template-setup-unchecked">
|
|
297
|
+
<ShieldAlert :size="14" class="mt-0.5 shrink-0" />
|
|
298
|
+
{{ t("integration.templates.detail.setup.unchecked", { reason: reqsError }) }}
|
|
299
|
+
</div>
|
|
300
|
+
<div v-else-if="setupReady" class="flex items-center gap-1.5 text-sm text-emerald-700">
|
|
302
301
|
<CheckCircle2 :size="14" />
|
|
303
302
|
{{ t("integration.templates.detail.setup.ready") }}
|
|
304
303
|
</div>
|
|
305
|
-
<div v-else class="text-sm text-amber-700 space-y-0.5">
|
|
304
|
+
<div v-else-if="reqs" class="text-sm text-amber-700 space-y-0.5">
|
|
306
305
|
<div v-if="missingCredentials" class="flex items-center gap-1.5">
|
|
307
306
|
<Key :size="12" />
|
|
308
307
|
{{ t("integration.templates.detail.setup.missingCredentials", missingCredentials) }}
|
|
@@ -698,6 +698,17 @@ export interface WorkflowRunNodeStep {
|
|
|
698
698
|
attempt: number;
|
|
699
699
|
durationMs: number | null;
|
|
700
700
|
}
|
|
701
|
+
export interface RunStepFrame {
|
|
702
|
+
file: string;
|
|
703
|
+
line: number;
|
|
704
|
+
column: number;
|
|
705
|
+
name?: string | null;
|
|
706
|
+
}
|
|
707
|
+
export interface RunStepError {
|
|
708
|
+
message: string;
|
|
709
|
+
type?: string;
|
|
710
|
+
frames?: RunStepFrame[];
|
|
711
|
+
}
|
|
701
712
|
export interface RunStepRow {
|
|
702
713
|
node_id: string;
|
|
703
714
|
node_slug: string;
|
|
@@ -716,7 +727,11 @@ export interface RunStepRow {
|
|
|
716
727
|
port: string;
|
|
717
728
|
} | null;
|
|
718
729
|
branch?: string | null;
|
|
719
|
-
|
|
730
|
+
/**
|
|
731
|
+
* Why the step failed. The platform sends the object; a string is what older
|
|
732
|
+
* rows and older mocks carry. Read it through `utils/stepError.ts`, never print it.
|
|
733
|
+
*/
|
|
734
|
+
error?: RunStepError | string | null;
|
|
720
735
|
input?: unknown;
|
|
721
736
|
output?: unknown;
|
|
722
737
|
/** Resolved node config for this step (present with include_payloads). */
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { RunStepError, RunStepFrame } from '../types/integrations.js';
|
|
2
|
+
/**
|
|
3
|
+
* A failed step's error as an operator reads it (IN-12).
|
|
4
|
+
*
|
|
5
|
+
* The platform sends it as an object — the message, the failure's type, and where
|
|
6
|
+
* the node package's own code was when it failed (`frames`, PO-447). The studio used
|
|
7
|
+
* to read it as text, and a template prints an object as JSON. A row from before the
|
|
8
|
+
* object, or a mock that still answers a string, is read as the message itself, so
|
|
9
|
+
* no shape ever reaches the screen as a dump.
|
|
10
|
+
*/
|
|
11
|
+
export declare function stepError(error: unknown): RunStepError | null;
|
|
12
|
+
/** One frame as a reader names it: `src/nodes/Foo.ts:12:5` and the function, where known. */
|
|
13
|
+
export declare function frameLabel(frame: RunStepFrame): string;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export function stepError(error) {
|
|
2
|
+
if (typeof error === "string") return error.trim() === "" ? null : { message: error };
|
|
3
|
+
if (error === null || typeof error !== "object") return null;
|
|
4
|
+
const raw = error;
|
|
5
|
+
const message = typeof raw.message === "string" && raw.message.trim() !== "" ? raw.message : null;
|
|
6
|
+
if (message === null) return null;
|
|
7
|
+
return {
|
|
8
|
+
message,
|
|
9
|
+
...typeof raw.type === "string" && raw.type.trim() !== "" ? { type: raw.type } : {},
|
|
10
|
+
...Array.isArray(raw.frames) ? { frames: raw.frames.filter(isFrame).map(withName) } : {}
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export function frameLabel(frame) {
|
|
14
|
+
const at = `${frame.file}:${frame.line}:${frame.column}`;
|
|
15
|
+
return frame.name ? `${frame.name} (${at})` : at;
|
|
16
|
+
}
|
|
17
|
+
function isFrame(value) {
|
|
18
|
+
if (value === null || typeof value !== "object") return false;
|
|
19
|
+
const f = value;
|
|
20
|
+
return typeof f.file === "string" && typeof f.line === "number" && typeof f.column === "number";
|
|
21
|
+
}
|
|
22
|
+
function withName(frame) {
|
|
23
|
+
return typeof frame.name === "string" && frame.name.trim() !== "" ? frame : { file: frame.file, line: frame.line, column: frame.column };
|
|
24
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@revenexx/studio-integrations",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Integration Studio (core: workflows, node editor, runs, schedules, credentials, secrets, audit) as a standalone, publishable Nuxt module.",
|
|
6
6
|
"repository": {
|