@revenexx/studio-integrations 3.0.3 → 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 +24 -0
- package/dist/module.json +1 -1
- package/dist/runtime/components/Integrations/CredentialCreateDialog.vue +3 -2
- package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +3 -2
- package/dist/runtime/components/Integrations/NodeInspector.vue +1 -1
- package/dist/runtime/components/Integrations/RunDetailPanel.vue +3 -2
- package/dist/runtime/components/Integrations/TemplateFlowPreview.vue +1 -1
- package/dist/runtime/components/Integrations/TemplateInstallStep.vue +55 -33
- package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +14 -2
- package/dist/runtime/composables/useIntegrationsAudit.d.ts +7 -2
- package/dist/runtime/composables/useIntegrationsCredentials.d.ts +11 -3
- package/dist/runtime/composables/useIntegrationsCredentials.js +9 -5
- package/dist/runtime/composables/useIntegrationsRuns.js +8 -4
- package/dist/runtime/composables/useIntegrationsSchedules.js +6 -3
- package/dist/runtime/composables/useIntegrationsSecrets.d.ts +6 -5
- package/dist/runtime/composables/useIntegrationsSecrets.js +3 -2
- package/dist/runtime/composables/useIntegrationsTemplates.d.ts +3 -1
- package/dist/runtime/composables/useIntegrationsTemplates.js +20 -15
- package/dist/runtime/composables/useIntegrationsWorkflows.js +4 -4
- package/dist/runtime/composables/useNodeRetirement.js +5 -5
- package/dist/runtime/i18n/messages/de.json +20 -3
- package/dist/runtime/i18n/messages/en.json +20 -3
- package/dist/runtime/pages/integrations/audit.vue +43 -9
- package/dist/runtime/pages/integrations/credentials.vue +3 -2
- package/dist/runtime/pages/integrations/runs.vue +3 -1
- package/dist/runtime/pages/integrations/secrets.vue +3 -1
- package/dist/runtime/pages/integrations/templates/[slug].vue +22 -23
- package/dist/runtime/types/integrations.d.ts +25 -7
- package/dist/runtime/utils/apiEnvelope.d.ts +15 -5
- package/dist/runtime/utils/apiEnvelope.js +11 -8
- package/dist/runtime/utils/credentialTest.d.ts +17 -0
- package/dist/runtime/utils/credentialTest.js +14 -0
- package/dist/runtime/utils/runEnvelope.d.ts +22 -24
- package/dist/runtime/utils/runEnvelope.js +2 -4
- package/dist/runtime/utils/stepError.d.ts +13 -0
- package/dist/runtime/utils/stepError.js +24 -0
- package/package.json +1 -1
- package/dist/runtime/data/integrationWorkflowSamples.d.ts +0 -18
- package/dist/runtime/data/integrationWorkflowSamples.js +0 -318
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
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
|
+
|
|
15
|
+
## 3.1.0
|
|
16
|
+
|
|
17
|
+
### Minor Changes
|
|
18
|
+
|
|
19
|
+
- 23bb093: The audit log offers every kind of change and of thing a tenant's log can hold — a cancelled or resumed run, a retired or restored node version, deleted stored state — and names a trigger by its kind and workflow and a run by its workflow, since the platform records no name for either. A connection test the platform could not run (its test broker was busy, or the reader may not try changed values) now reads "Not tested" instead of a failed connection. A secret key may be anything the platform stores — lower case and hyphens included, up to 191 characters; a runs search is cut to the platform's 200 characters instead of being refused; an install name stops at 255. Installing a template wires a step with two credential settings setting by setting,, a template's industry outside the six the studio used to know is kept and found by search, and a template started by a webhook says which method it answers rather than a path it never carries. An answer the schedules page cannot read is reported as a failed load, not as a tenant with no schedules.
|
|
20
|
+
|
|
21
|
+
For hosts: single workflows, runs and credentials are now read only in the bare shape the Integrations service sends; a `data`-wrapped answer is refused. `instantiate()` also accepts `{ nodeId: { settingKey: credentialId } }` beside a bare id. Three exported types widen to what the service sends — `WorkflowTemplate.industries` is `string[]`, `CredentialType.icon` and `description` may be `null`, and a credential field option's `label` may be localised.
|
|
22
|
+
|
|
23
|
+
### Patch Changes
|
|
24
|
+
|
|
25
|
+
- ef3f3eb: A dead letter's `reason` is typed as the string the platform always sends (empty when nothing was recorded) and its `trigger_id` as the number it is. Rotating a webhook's endpoint secret reads the platform's answer in the one shape it has ever sent. Nothing else a host sees changes: the rest of this release brings the browser tests' mock in line with what the Integrations service actually answers.
|
|
26
|
+
|
|
3
27
|
## 3.0.3
|
|
4
28
|
|
|
5
29
|
### Patch Changes
|
package/dist/module.json
CHANGED
|
@@ -23,6 +23,7 @@ import { resolveLocalized } from "../../utils/localize";
|
|
|
23
23
|
import { fromModelValue, toItemValue, toModelValue } from "../../utils/selectOptions";
|
|
24
24
|
import { errorReason } from "../../utils/apiError";
|
|
25
25
|
import { keepOpenBeside } from "../../utils/dialog";
|
|
26
|
+
import { testOutcomeView } from "../../utils/credentialTest";
|
|
26
27
|
const props = defineProps({
|
|
27
28
|
open: { type: Boolean, required: true },
|
|
28
29
|
credentialType: { type: [String, Array], required: false }
|
|
@@ -247,12 +248,12 @@ async function submit() {
|
|
|
247
248
|
<div
|
|
248
249
|
v-if="testResult"
|
|
249
250
|
class="rounded-md px-3 py-2 text-sm"
|
|
250
|
-
:class="testResult.
|
|
251
|
+
:class="testOutcomeView(testResult, t).toneClass"
|
|
251
252
|
>
|
|
252
253
|
<!-- The type's own sentence, either way. This is the surface where it is
|
|
253
254
|
worth the most: nothing is stored yet, so the operator is deciding
|
|
254
255
|
whether these access data reach what they meant (specs/credential-test.md). -->
|
|
255
|
-
{{ testResult
|
|
256
|
+
{{ testOutcomeView(testResult, t).text }}
|
|
256
257
|
</div>
|
|
257
258
|
</div>
|
|
258
259
|
|
|
@@ -39,6 +39,7 @@ import { hasRevenexxSource } from "../../utils/revenexxCredentialSource";
|
|
|
39
39
|
import { resolveLocalized } from "../../utils/localize";
|
|
40
40
|
import { fromModelValue, toItemValue, toModelValue } from "../../utils/selectOptions";
|
|
41
41
|
import { keepOpenBeside } from "../../utils/dialog";
|
|
42
|
+
import { testOutcomeView } from "../../utils/credentialTest";
|
|
42
43
|
const props = defineProps({
|
|
43
44
|
open: { type: Boolean, required: true },
|
|
44
45
|
credential: { type: [Object, null], required: true },
|
|
@@ -375,9 +376,9 @@ async function connect() {
|
|
|
375
376
|
<div
|
|
376
377
|
v-if="liveTest"
|
|
377
378
|
class="rounded-md px-3 py-2 text-sm"
|
|
378
|
-
:class="liveTest.
|
|
379
|
+
:class="testOutcomeView(liveTest, t).toneClass"
|
|
379
380
|
>
|
|
380
|
-
{{ liveTest
|
|
381
|
+
{{ testOutcomeView(liveTest, t).text }}
|
|
381
382
|
</div>
|
|
382
383
|
|
|
383
384
|
</div>
|
|
@@ -472,7 +472,7 @@ async function rotateEndpointSecret() {
|
|
|
472
472
|
const api2 = useIntegrationsApi();
|
|
473
473
|
const res = await api2.post(`/workflows/${props.workflowId}/triggers/${wt.id}/rotate-secret`, {});
|
|
474
474
|
const body = res ?? {};
|
|
475
|
-
const secret = body.inbound_secret ??
|
|
475
|
+
const secret = body.inbound_secret ?? null;
|
|
476
476
|
if (secret) {
|
|
477
477
|
rotatedSecret.value = secret;
|
|
478
478
|
rotateOpen.value = true;
|
|
@@ -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)">
|
|
@@ -30,7 +30,7 @@ const boxes = computed(() => {
|
|
|
30
30
|
triggers.forEach((t2, i) => {
|
|
31
31
|
const pos = blob.triggerPositions?.[t2.id] ?? { x: 40, y: 40 + i * 160 };
|
|
32
32
|
const template = TRIGGER_TEMPLATES.find((tt) => tt.type === t2.type);
|
|
33
|
-
const subtitle = t2.type === "schedule" && t2.config?.cron ? describeCron(String(t2.config.cron)) ?? String(t2.config.cron) : t2.type === "webhook" && t2.config?.
|
|
33
|
+
const subtitle = t2.type === "schedule" && t2.config?.cron ? describeCron(String(t2.config.cron)) ?? String(t2.config.cron) : t2.type === "webhook" && t2.config?.method ? String(t2.config.method).toUpperCase() : resolveLocalized(template?.description ?? "");
|
|
34
34
|
out.push({
|
|
35
35
|
key: `t-${t2.id}`,
|
|
36
36
|
kind: "trigger",
|
|
@@ -20,64 +20,77 @@ 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);
|
|
30
31
|
const name = ref("");
|
|
31
32
|
const bindings = reactive({});
|
|
33
|
+
const slotKey = (nodeId, configKey) => `${nodeId}\0${configKey}`;
|
|
32
34
|
let run = 0;
|
|
33
35
|
watch(() => props.slug, async (slug) => {
|
|
34
36
|
const mine = ++run;
|
|
35
37
|
loading.value = true;
|
|
36
38
|
reqs.value = null;
|
|
39
|
+
reqsError.value = null;
|
|
37
40
|
error.value = null;
|
|
38
41
|
name.value = "";
|
|
39
42
|
for (const k of Object.keys(bindings)) delete bindings[k];
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
+
}
|
|
46
55
|
if (mine !== run) return;
|
|
47
56
|
reqs.value = answer;
|
|
57
|
+
reqsError.value = failure;
|
|
48
58
|
loading.value = false;
|
|
49
59
|
}, { immediate: true });
|
|
50
60
|
watch(loading, (v) => emit("update:loading", v), { immediate: true });
|
|
51
61
|
watch(installing, (v) => emit("update:installing", v));
|
|
52
62
|
onBeforeUnmount(() => emit("update:loading", false));
|
|
53
|
-
const tpl = computed(() => get(props.slug));
|
|
54
63
|
const credentialReqs = computed(() => reqs.value?.credentialTypes ?? []);
|
|
55
64
|
const secretReqs = computed(() => reqs.value?.secrets ?? []);
|
|
56
|
-
const secretChecks = computed(() => {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
key
|
|
67
|
-
|
|
68
|
-
|
|
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
|
+
})));
|
|
70
|
+
function slotsFor(usedBy) {
|
|
71
|
+
const all = credentialReqs.value.flatMap((r) => r.usedBy);
|
|
72
|
+
const seen = /* @__PURE__ */ new Set();
|
|
73
|
+
return usedBy.filter((u) => {
|
|
74
|
+
const key = slotKey(u.nodeId, u.configKey);
|
|
75
|
+
if (seen.has(key)) return false;
|
|
76
|
+
seen.add(key);
|
|
77
|
+
return true;
|
|
78
|
+
}).map((u) => ({
|
|
79
|
+
key: slotKey(u.nodeId, u.configKey),
|
|
80
|
+
nodeId: u.nodeId,
|
|
81
|
+
configKey: u.configKey,
|
|
82
|
+
label: new Set(all.filter((o) => o.nodeId === u.nodeId).map((o) => o.configKey)).size > 1 ? t("integration.templates.detail.install.settingLabel", { id: u.nodeId, setting: u.configKey }) : t("integration.templates.detail.install.nodeLabel", { id: u.nodeId })
|
|
69
83
|
}));
|
|
70
|
-
});
|
|
71
|
-
function nodesFor(usedBy) {
|
|
72
|
-
return [...new Set(usedBy.map((u) => u.nodeId))];
|
|
73
84
|
}
|
|
74
85
|
async function confirmInstall() {
|
|
75
86
|
installing.value = true;
|
|
76
87
|
error.value = null;
|
|
77
88
|
try {
|
|
78
89
|
const credentialBindings = {};
|
|
79
|
-
for (const [
|
|
80
|
-
if (credId
|
|
90
|
+
for (const [key, credId] of Object.entries(bindings)) {
|
|
91
|
+
if (!credId || credId === SELECT_LATER) continue;
|
|
92
|
+
const [nodeId, configKey] = key.split("\0");
|
|
93
|
+
(credentialBindings[nodeId] ??= {})[configKey] = credId;
|
|
81
94
|
}
|
|
82
95
|
const id = await instantiate(props.slug, {
|
|
83
96
|
name: name.value.trim(),
|
|
@@ -98,7 +111,8 @@ async function confirmInstall() {
|
|
|
98
111
|
<div class="space-y-4 py-1 max-h-[28rem] overflow-y-auto pr-1">
|
|
99
112
|
<div class="space-y-1.5">
|
|
100
113
|
<Label for="tpl-install-name">{{ t("integration.templates.detail.install.nameLabel") }}</Label>
|
|
101
|
-
|
|
114
|
+
<!-- 255: the platform's limit on a workflow's name (InstantiateTemplateRequest). -->
|
|
115
|
+
<Input id="tpl-install-name" v-model="name" class="bg-white" :maxlength="255" :placeholder="t('integration.templates.detail.install.namePlaceholder')" />
|
|
102
116
|
</div>
|
|
103
117
|
|
|
104
118
|
<div v-if="loading" class="flex items-center gap-2 py-4 text-sm text-muted-foreground">
|
|
@@ -112,7 +126,13 @@ async function confirmInstall() {
|
|
|
112
126
|
<div class="text-mono text-muted-foreground border-b border-black/[0.06] pb-1.5">
|
|
113
127
|
{{ t("integration.templates.detail.credentials.title") }}
|
|
114
128
|
</div>
|
|
115
|
-
|
|
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">
|
|
116
136
|
{{ t("integration.templates.detail.install.noCredentials") }}
|
|
117
137
|
</p>
|
|
118
138
|
<div v-for="req in credentialReqs" :key="req.slug" class="space-y-2">
|
|
@@ -131,15 +151,17 @@ async function confirmInstall() {
|
|
|
131
151
|
<NuxtLink to="/integrations/credentials" class="text-primary hover:underline">{{ t("integration.templates.detail.install.createOneLink") }}</NuxtLink>
|
|
132
152
|
{{ t("integration.templates.detail.install.orInstallLater") }}
|
|
133
153
|
</p>
|
|
134
|
-
<div v-for="
|
|
135
|
-
|
|
154
|
+
<div v-for="(slot, si) in slotsFor(req.usedBy)" v-else :key="slot.key" class="space-y-1">
|
|
155
|
+
<!-- By position, not by joining two free strings: `a-b`+`c` and `a`+`b-c`
|
|
156
|
+
would share an id, and a label would name the wrong select. -->
|
|
157
|
+
<Label :for="`tpl-cred-${req.slug}-${si}`" class="text-xs text-muted-foreground font-mono">{{ slot.label }}</Label>
|
|
136
158
|
<Select
|
|
137
|
-
:model-value="bindings[
|
|
159
|
+
:model-value="bindings[slot.key] ?? SELECT_LATER"
|
|
138
160
|
@update:model-value="(v) => {
|
|
139
|
-
bindings[
|
|
161
|
+
bindings[slot.key] = v ?? SELECT_LATER;
|
|
140
162
|
}"
|
|
141
163
|
>
|
|
142
|
-
<SelectTrigger :id="`tpl-cred-${
|
|
164
|
+
<SelectTrigger :id="`tpl-cred-${req.slug}-${si}`" class="bg-white">
|
|
143
165
|
<SelectValue :placeholder="t('integration.templates.detail.install.selectLater')" />
|
|
144
166
|
</SelectTrigger>
|
|
145
167
|
<SelectContent>
|
|
@@ -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">
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
export type AuditResourceType = 'credential' | 'secret' | 'workflow' | 'trigger' | 'workflow_run';
|
|
2
|
-
export type AuditAction = 'create' | 'update' | 'delete' | 'run.started' | 'run.finished' | 'run.failed';
|
|
3
1
|
export interface AuditEntry {
|
|
4
2
|
id: number;
|
|
5
3
|
org_id?: string;
|
|
@@ -8,12 +6,19 @@ export interface AuditEntry {
|
|
|
8
6
|
/** As the actor's sign-in stated it when the entry was written; null for the platform, and for entries from before names were recorded (PO-609). */
|
|
9
7
|
actor_name?: string | null;
|
|
10
8
|
actor_email?: string | null;
|
|
9
|
+
/** Whether the platform wrote the entry with no person behind it, and by which route it arrived. Sent on every row; not shown yet. */
|
|
10
|
+
via_system?: boolean;
|
|
11
|
+
channel?: string;
|
|
11
12
|
action: string;
|
|
12
13
|
resource_type: string;
|
|
13
14
|
resource_id?: string;
|
|
14
15
|
metadata?: Record<string, unknown> | null;
|
|
15
16
|
created_at: string;
|
|
16
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* The narrowing the feed itself accepts. The page sends none of it today — it
|
|
20
|
+
* narrows in the browser over what it has loaded (specs/audit-log.md `## Gaps`).
|
|
21
|
+
*/
|
|
17
22
|
export interface AuditFilters {
|
|
18
23
|
sub?: string;
|
|
19
24
|
resource_type?: string;
|
|
@@ -8,11 +8,13 @@ import { IntegrationsApiError } from '../utils/apiError.js';
|
|
|
8
8
|
* GET /credentials[?type=slug] → list
|
|
9
9
|
* POST /credentials → create { credential_type_slug, name, config }
|
|
10
10
|
* GET /credentials/{id} → show (masked)
|
|
11
|
-
* PATCH /credentials/{id} → update { name?, config? } (blank secret = keep
|
|
11
|
+
* PATCH /credentials/{id} → update { name?, config? } (blank secret = keep,
|
|
12
|
+
* unless the same edit changes any other value:
|
|
13
|
+
* then 422 at config.<secret>, PO-765)
|
|
12
14
|
* DELETE /credentials/{id} → remove
|
|
13
15
|
* POST /credentials/test → inline test { credential_type_slug, config }
|
|
14
|
-
* POST /credentials/{id}/test → test
|
|
15
|
-
* POST /credentials/{id}/oauth/authorize-url → { authorize_url
|
|
16
|
+
* POST /credentials/{id}/test → test { config? } → { ok, message, recorded, last_test_at, last_test_ok }
|
|
17
|
+
* POST /credentials/{id}/oauth/authorize-url → { authorize_url }
|
|
16
18
|
*/
|
|
17
19
|
/**
|
|
18
20
|
* `message` is what the credential type said about the test, whichever way it went —
|
|
@@ -33,6 +35,12 @@ export interface CredentialTestResult {
|
|
|
33
35
|
* test of an edit that tried values other than the stored ones (PO-535).
|
|
34
36
|
*/
|
|
35
37
|
recorded?: boolean;
|
|
38
|
+
/**
|
|
39
|
+
* The test did not happen, so it says nothing about the credential: the platform's
|
|
40
|
+
* test broker had no room for it, or the reader may not try changed values (IN-22).
|
|
41
|
+
* Drawn neither as a pass nor as a failure.
|
|
42
|
+
*/
|
|
43
|
+
untested?: true;
|
|
36
44
|
/**
|
|
37
45
|
* The refusal the service answered instead of testing, held as it arrived: values it
|
|
38
46
|
* would not try as given — a stored secret kept for a changed server (PO-765). A form
|
|
@@ -2,7 +2,7 @@ import { computed } from "vue";
|
|
|
2
2
|
import { useIntegrationsApi } from "./integrationsApi.js";
|
|
3
3
|
import { useIntegrationsT } from "./useIntegrationsT.js";
|
|
4
4
|
import { IntegrationsApiError, errorReason } from "../utils/apiError.js";
|
|
5
|
-
import {
|
|
5
|
+
import { bareRecord, unwrapList } from "../utils/apiEnvelope.js";
|
|
6
6
|
import { useLoadGate } from "./useLoadGate.js";
|
|
7
7
|
export function useIntegrationsCredentials() {
|
|
8
8
|
const credentials = useState("integrations.credentials", () => []);
|
|
@@ -33,15 +33,15 @@ export function useIntegrationsCredentials() {
|
|
|
33
33
|
return credentials.value.filter((c) => c.credential_type_slug === slug);
|
|
34
34
|
}
|
|
35
35
|
async function get(id) {
|
|
36
|
-
return
|
|
36
|
+
return bareRecord(await api.get(`/credentials/${encodeURIComponent(id)}`));
|
|
37
37
|
}
|
|
38
38
|
async function create(input) {
|
|
39
|
-
const created =
|
|
39
|
+
const created = bareRecord(await api.post("/credentials", input));
|
|
40
40
|
await load(true);
|
|
41
41
|
return created;
|
|
42
42
|
}
|
|
43
43
|
async function update(id, input) {
|
|
44
|
-
const updated =
|
|
44
|
+
const updated = bareRecord(await api.patch(`/credentials/${encodeURIComponent(id)}`, input));
|
|
45
45
|
await load(true);
|
|
46
46
|
return updated;
|
|
47
47
|
}
|
|
@@ -63,15 +63,19 @@ export function useIntegrationsCredentials() {
|
|
|
63
63
|
`/credentials/${encodeURIComponent(id)}/test`,
|
|
64
64
|
config ? { config } : void 0
|
|
65
65
|
);
|
|
66
|
+
if (!config && res?.ok === false && res.recorded === false) return { ...res, untested: true };
|
|
66
67
|
return res?.ok === false ? res : { ok: true, message: res?.message, recorded: res?.recorded };
|
|
67
68
|
} catch (e) {
|
|
69
|
+
if (config && e instanceof IntegrationsApiError && e.status === 403) {
|
|
70
|
+
return { ok: false, untested: true, error: t("integration.credentials.shared.test.notAllowed") };
|
|
71
|
+
}
|
|
68
72
|
const refusal = e instanceof IntegrationsApiError && e.status === 422 ? e : void 0;
|
|
69
73
|
return { ok: false, error: errorReason(e, t("integration.credentials.error.testFailed")), refusal };
|
|
70
74
|
}
|
|
71
75
|
}
|
|
72
76
|
async function oauthAuthorizeUrl(id) {
|
|
73
77
|
const res = await api.post(`/credentials/${encodeURIComponent(id)}/oauth/authorize-url`);
|
|
74
|
-
const authorizeUrl =
|
|
78
|
+
const authorizeUrl = bareRecord(res).authorize_url;
|
|
75
79
|
if (!authorizeUrl) throw new IntegrationsApiError(0, res, t("integration.credentials.error.noAuthorizeUrl"));
|
|
76
80
|
return authorizeUrl;
|
|
77
81
|
}
|
|
@@ -2,7 +2,7 @@ import { computed } from "vue";
|
|
|
2
2
|
import { useIntegrationsApi } from "./integrationsApi.js";
|
|
3
3
|
import { useIntegrationsT } from "./useIntegrationsT.js";
|
|
4
4
|
import { errorReason } from "../utils/apiError.js";
|
|
5
|
-
import {
|
|
5
|
+
import { bareRecord } from "../utils/apiEnvelope.js";
|
|
6
6
|
import { RUN_OWN_FIELDS } from "../utils/runEnvelope.js";
|
|
7
7
|
import { useLoadGate } from "./useLoadGate.js";
|
|
8
8
|
import {
|
|
@@ -58,6 +58,10 @@ export function useIntegrationsRuns() {
|
|
|
58
58
|
// The platform reports what started the run. This was hardcoded to
|
|
59
59
|
// `manual` because the payload carried nothing, which left the trigger
|
|
60
60
|
// column and its filter with one populated value in production.
|
|
61
|
+
// Null — the trigger was deleted, or the run predates trigger_handle, or the
|
|
62
|
+
// row came from a per-workflow read, which sends no trigger at all — is read
|
|
63
|
+
// as `manual` here although the platform said nothing (IN-9, api-contract
|
|
64
|
+
// runs.feed F6 and runs.execution F2).
|
|
61
65
|
triggerType: r.trigger_type ?? "manual",
|
|
62
66
|
startedAt: r.started_at ?? "",
|
|
63
67
|
finishedAt: r.completed_at ?? null,
|
|
@@ -187,11 +191,11 @@ export function useIntegrationsRuns() {
|
|
|
187
191
|
async function trigger(workflowId, payload = {}, mode) {
|
|
188
192
|
const body = { trigger_payload: payload };
|
|
189
193
|
if (mode) body.mode = mode;
|
|
190
|
-
return
|
|
194
|
+
return bareRecord(await api.post(`/workflows/${workflowId}/runs`, body), RUN_OWN_FIELDS);
|
|
191
195
|
}
|
|
192
196
|
async function runAction(workflowId, runId, action, reason) {
|
|
193
197
|
const body = reason ? { reason } : {};
|
|
194
|
-
return
|
|
198
|
+
return bareRecord(await api.post(`/workflows/${workflowId}/runs/${runId}/${action}`, body), RUN_OWN_FIELDS);
|
|
195
199
|
}
|
|
196
200
|
const cancelRun = (workflowId, runId, reason) => runAction(workflowId, runId, "cancel", reason);
|
|
197
201
|
const terminateRun = (workflowId, runId, reason) => runAction(workflowId, runId, "terminate", reason);
|
|
@@ -199,7 +203,7 @@ export function useIntegrationsRuns() {
|
|
|
199
203
|
const resumeRun = (workflowId, runId, reason) => runAction(workflowId, runId, "resume", reason);
|
|
200
204
|
const getRun = (workflowId, runId) => api.get(`/workflows/${workflowId}/runs/${runId}`);
|
|
201
205
|
async function fetchRun(workflowId, runId, workflowName) {
|
|
202
|
-
const apiRun =
|
|
206
|
+
const apiRun = bareRecord(await getRun(workflowId, runId), RUN_OWN_FIELDS);
|
|
203
207
|
return mapRun(apiRun, workflowName);
|
|
204
208
|
}
|
|
205
209
|
const getDetails = (workflowId, runId) => api.get(`/workflows/${workflowId}/runs/${runId}/details`);
|
|
@@ -2,7 +2,8 @@ import { computed } from "vue";
|
|
|
2
2
|
import { useIntegrationsApi } from "./integrationsApi.js";
|
|
3
3
|
import { useIntegrationsT } from "./useIntegrationsT.js";
|
|
4
4
|
import { useLoadGate } from "./useLoadGate.js";
|
|
5
|
-
import { errorReason } from "../utils/apiError.js";
|
|
5
|
+
import { errorReason, IntegrationsApiError } from "../utils/apiError.js";
|
|
6
|
+
import { unwrapList } from "../utils/apiEnvelope.js";
|
|
6
7
|
import { scheduleTimezone } from "../utils/schedule.js";
|
|
7
8
|
export function useIntegrationsSchedules() {
|
|
8
9
|
const api = useIntegrationsApi();
|
|
@@ -66,8 +67,10 @@ export function useIntegrationsSchedules() {
|
|
|
66
67
|
error.value = null;
|
|
67
68
|
try {
|
|
68
69
|
const res = await api.get("/schedules");
|
|
69
|
-
|
|
70
|
-
|
|
70
|
+
const rows = unwrapList(res);
|
|
71
|
+
if (!res?.meta) throw new IntegrationsApiError(200, res, "");
|
|
72
|
+
entries.value = rows.map(mapEntry);
|
|
73
|
+
meta.value = res.meta;
|
|
71
74
|
loaded.value = true;
|
|
72
75
|
} catch (e) {
|
|
73
76
|
error.value = errorReason(e, t("integration.schedules.error.loadFailed"));
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import type { SecretRecord } from '../types/integrations.js';
|
|
2
|
+
export declare const SECRET_KEY_MAX = 191;
|
|
2
3
|
/**
|
|
3
4
|
* Per-tenant encrypted key/value store, backed by the Integrations service
|
|
4
|
-
* (`/api/v1/secrets`). The backend NEVER returns
|
|
5
|
-
* timestamps — so the list is keys-only by design.
|
|
5
|
+
* (`/api/v1/secrets`). The backend NEVER returns a value.
|
|
6
6
|
*
|
|
7
|
-
* GET /secrets →
|
|
8
|
-
* POST /secrets → upsert
|
|
9
|
-
*
|
|
7
|
+
* GET /secrets → { data: [{ key, created_at, updated_at }] }, key-ascending
|
|
8
|
+
* POST /secrets → upsert { key, value } — 201 created, 200 replaced, bare record
|
|
9
|
+
* PATCH /secrets/{key} → replace the value (404 unknown key) — not used by this UI
|
|
10
|
+
* DELETE /secrets/{key} → 204, or 404 for a key this tenant does not hold
|
|
10
11
|
*/
|
|
11
12
|
export declare function useIntegrationsSecrets(): {
|
|
12
13
|
secrets: any;
|
|
@@ -4,7 +4,8 @@ import { useIntegrationsT } from "./useIntegrationsT.js";
|
|
|
4
4
|
import { errorReason } from "../utils/apiError.js";
|
|
5
5
|
import { unwrapList } from "../utils/apiEnvelope.js";
|
|
6
6
|
import { useLoadGate } from "./useLoadGate.js";
|
|
7
|
-
const KEY_PATTERN = /^[
|
|
7
|
+
const KEY_PATTERN = /^[\w-]+$/;
|
|
8
|
+
export const SECRET_KEY_MAX = 191;
|
|
8
9
|
export function useIntegrationsSecrets() {
|
|
9
10
|
const secrets = useState("integrations.secrets", () => []);
|
|
10
11
|
const loading = useState("integrations.secrets.loading", () => false);
|
|
@@ -14,7 +15,7 @@ export function useIntegrationsSecrets() {
|
|
|
14
15
|
const api = useIntegrationsApi();
|
|
15
16
|
const t = useIntegrationsT();
|
|
16
17
|
function isValidKey(key) {
|
|
17
|
-
return KEY_PATTERN.test(key);
|
|
18
|
+
return KEY_PATTERN.test(key) && [...key].length <= SECRET_KEY_MAX;
|
|
18
19
|
}
|
|
19
20
|
function normalize(res) {
|
|
20
21
|
const obj = res;
|
|
@@ -12,9 +12,11 @@ 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
|
-
credentialBindings?: Record<string, string
|
|
19
|
+
credentialBindings?: Record<string, string | Record<string, string>>;
|
|
18
20
|
}) => Promise<number | null>;
|
|
19
21
|
install: (slug: string) => Promise<number | null>;
|
|
20
22
|
};
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { computed } from "vue";
|
|
2
2
|
import { useIntegrationsApi } from "./integrationsApi.js";
|
|
3
3
|
import { errorReason } from "../utils/apiError.js";
|
|
4
|
-
import { unwrapList, unwrapRecord } from "../utils/apiEnvelope.js";
|
|
4
|
+
import { bareRecord, unwrapList, unwrapRecord } from "../utils/apiEnvelope.js";
|
|
5
5
|
import { resolveLocalized } from "../utils/localize.js";
|
|
6
6
|
import { useLoadGate } from "./useLoadGate.js";
|
|
7
7
|
import { createDetailLoader, keyedBatch } from "../utils/detailLoader.js";
|
|
8
8
|
const BLOB_VERSION = "v0-draft";
|
|
9
|
-
const INDUSTRY_VALUES = ["mechanical-engineering", "chemicals", "wholesale", "manufacturing", "retail", "any"];
|
|
10
9
|
function plainExcerpt(md) {
|
|
11
10
|
for (const raw of md.split("\n")) {
|
|
12
11
|
const line = raw.trim();
|
|
@@ -21,7 +20,7 @@ function publisherFor(slug, vendors) {
|
|
|
21
20
|
return { name: ns, kind };
|
|
22
21
|
}
|
|
23
22
|
function mapIndustries(arr) {
|
|
24
|
-
return (arr ?? []).filter((i) =>
|
|
23
|
+
return (arr ?? []).filter((i) => typeof i === "string" && i.trim() !== "").map((i) => i.trim());
|
|
25
24
|
}
|
|
26
25
|
function emptyBlob(name) {
|
|
27
26
|
return { nodeManifestVersion: BLOB_VERSION, name, nodes: [], edges: [], triggerPositions: {}, triggers: [] };
|
|
@@ -58,7 +57,7 @@ function mapListItem(api) {
|
|
|
58
57
|
function applyDetail(base, api) {
|
|
59
58
|
const def = api.definition ?? {};
|
|
60
59
|
const triggers = (api.triggers ?? []).map((t) => ({
|
|
61
|
-
id: t.handle
|
|
60
|
+
id: t.handle,
|
|
62
61
|
type: t.type,
|
|
63
62
|
name: typeof t.name === "string" ? t.name : void 0,
|
|
64
63
|
active: t.active,
|
|
@@ -166,16 +165,22 @@ export function useIntegrationsTemplates() {
|
|
|
166
165
|
if (!wanted.length) return;
|
|
167
166
|
remember(await loader.many(wanted));
|
|
168
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
|
+
}
|
|
169
181
|
async function requirements(slug) {
|
|
170
182
|
try {
|
|
171
|
-
|
|
172
|
-
return {
|
|
173
|
-
...raw,
|
|
174
|
-
credentialTypes: (raw.credentialTypes ?? []).map((c) => ({
|
|
175
|
-
...c,
|
|
176
|
-
name: c.name ? resolveLocalized(c.name) : c.name
|
|
177
|
-
}))
|
|
178
|
-
};
|
|
183
|
+
return await checkRequirements(slug);
|
|
179
184
|
} catch {
|
|
180
185
|
return null;
|
|
181
186
|
}
|
|
@@ -184,10 +189,10 @@ export function useIntegrationsTemplates() {
|
|
|
184
189
|
const body = {};
|
|
185
190
|
if (opts.name) body.name = opts.name;
|
|
186
191
|
const bindings = Object.fromEntries(
|
|
187
|
-
Object.entries(opts.credentialBindings ?? {}).filter(([,
|
|
192
|
+
Object.entries(opts.credentialBindings ?? {}).map(([nodeId, b]) => [nodeId, typeof b === "string" ? b : Object.fromEntries(Object.entries(b).filter(([, id]) => id))]).filter(([, b]) => typeof b === "string" ? b !== "" : Object.keys(b).length > 0)
|
|
188
193
|
);
|
|
189
194
|
if (Object.keys(bindings).length) body.credential_bindings = bindings;
|
|
190
|
-
const created =
|
|
195
|
+
const created = bareRecord(await api.post(`/templates/${slug}/instantiate`, body));
|
|
191
196
|
return Number(created.id);
|
|
192
197
|
}
|
|
193
198
|
async function install(slug) {
|
|
@@ -195,5 +200,5 @@ export function useIntegrationsTemplates() {
|
|
|
195
200
|
}
|
|
196
201
|
const total = computed(() => templates.value.length);
|
|
197
202
|
const startingPoints = computed(() => templates.value.slice(0, 4));
|
|
198
|
-
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 };
|
|
199
204
|
}
|