@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.
Files changed (39) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/module.json +1 -1
  3. package/dist/runtime/components/Integrations/CredentialCreateDialog.vue +3 -2
  4. package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +3 -2
  5. package/dist/runtime/components/Integrations/NodeInspector.vue +1 -1
  6. package/dist/runtime/components/Integrations/RunDetailPanel.vue +3 -2
  7. package/dist/runtime/components/Integrations/TemplateFlowPreview.vue +1 -1
  8. package/dist/runtime/components/Integrations/TemplateInstallStep.vue +55 -33
  9. package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +14 -2
  10. package/dist/runtime/composables/useIntegrationsAudit.d.ts +7 -2
  11. package/dist/runtime/composables/useIntegrationsCredentials.d.ts +11 -3
  12. package/dist/runtime/composables/useIntegrationsCredentials.js +9 -5
  13. package/dist/runtime/composables/useIntegrationsRuns.js +8 -4
  14. package/dist/runtime/composables/useIntegrationsSchedules.js +6 -3
  15. package/dist/runtime/composables/useIntegrationsSecrets.d.ts +6 -5
  16. package/dist/runtime/composables/useIntegrationsSecrets.js +3 -2
  17. package/dist/runtime/composables/useIntegrationsTemplates.d.ts +3 -1
  18. package/dist/runtime/composables/useIntegrationsTemplates.js +20 -15
  19. package/dist/runtime/composables/useIntegrationsWorkflows.js +4 -4
  20. package/dist/runtime/composables/useNodeRetirement.js +5 -5
  21. package/dist/runtime/i18n/messages/de.json +20 -3
  22. package/dist/runtime/i18n/messages/en.json +20 -3
  23. package/dist/runtime/pages/integrations/audit.vue +43 -9
  24. package/dist/runtime/pages/integrations/credentials.vue +3 -2
  25. package/dist/runtime/pages/integrations/runs.vue +3 -1
  26. package/dist/runtime/pages/integrations/secrets.vue +3 -1
  27. package/dist/runtime/pages/integrations/templates/[slug].vue +22 -23
  28. package/dist/runtime/types/integrations.d.ts +25 -7
  29. package/dist/runtime/utils/apiEnvelope.d.ts +15 -5
  30. package/dist/runtime/utils/apiEnvelope.js +11 -8
  31. package/dist/runtime/utils/credentialTest.d.ts +17 -0
  32. package/dist/runtime/utils/credentialTest.js +14 -0
  33. package/dist/runtime/utils/runEnvelope.d.ts +22 -24
  34. package/dist/runtime/utils/runEnvelope.js +2 -4
  35. package/dist/runtime/utils/stepError.d.ts +13 -0
  36. package/dist/runtime/utils/stepError.js +24 -0
  37. package/package.json +1 -1
  38. package/dist/runtime/data/integrationWorkflowSamples.d.ts +0 -18
  39. package/dist/runtime/data/integrationWorkflowSamples.js +0 -318
@@ -3,7 +3,7 @@ import { useIntegrationsApi } from "./integrationsApi.js";
3
3
  import { useIntegrationsT } from "./useIntegrationsT.js";
4
4
  import { assignTriggerHandles, pausedTriggers } from "../utils/workflowFile.js";
5
5
  import { errorReason } from "../utils/apiError.js";
6
- import { unwrapList, unwrapRecord } from "../utils/apiEnvelope.js";
6
+ import { bareRecord, unwrapList } from "../utils/apiEnvelope.js";
7
7
  import { useLoadGate } from "./useLoadGate.js";
8
8
  import { eventConfigForPlatform } from "../data/eventTrigger.js";
9
9
  const BLOB_VERSION = "v0-draft";
@@ -140,7 +140,7 @@ export function useIntegrationsWorkflows() {
140
140
  }
141
141
  async function get(id, { strict = false } = {}) {
142
142
  try {
143
- const wf = ingest(unwrapRecord(await api.get(`/workflows/${id}`)));
143
+ const wf = ingest(bareRecord(await api.get(`/workflows/${id}`)));
144
144
  upsertCache(wf);
145
145
  return wf;
146
146
  } catch (e) {
@@ -149,7 +149,7 @@ export function useIntegrationsWorkflows() {
149
149
  }
150
150
  }
151
151
  async function create(input) {
152
- const wf = ingest(unwrapRecord(await api.post("/workflows", toPayload({
152
+ const wf = ingest(bareRecord(await api.post("/workflows", toPayload({
153
153
  name: input.name,
154
154
  description: input.description,
155
155
  blob: input.blob,
@@ -182,7 +182,7 @@ export function useIntegrationsWorkflows() {
182
182
  throw lastError instanceof Error ? lastError : new Error(t("integration.workflows.error.duplicateFailed"));
183
183
  }
184
184
  async function update(id, patch) {
185
- const wf = ingest(unwrapRecord(await api.put(`/workflows/${id}`, toPayload({
185
+ const wf = ingest(bareRecord(await api.put(`/workflows/${id}`, toPayload({
186
186
  name: patch.name,
187
187
  description: patch.description,
188
188
  blob: patch.blob,
@@ -1,5 +1,5 @@
1
1
  import { IntegrationsApiError } from "../utils/apiError.js";
2
- import { unwrapList, unwrapRecord } from "../utils/apiEnvelope.js";
2
+ import { bareRecord, unwrapList } from "../utils/apiEnvelope.js";
3
3
  import { useIntegrationsApi } from "./integrationsApi.js";
4
4
  function versionPath(slug, version) {
5
5
  return `/nodes/${encodeURIComponent(slug)}/${encodeURIComponent(version)}`;
@@ -11,7 +11,7 @@ export function useNodeRetirement() {
11
11
  return all.filter((n) => n.package?.name === packageName);
12
12
  }
13
13
  async function usage(slug, version) {
14
- const answer = unwrapRecord(await api.get(`${versionPath(slug, version)}/usage`), ["usage"]);
14
+ const answer = bareRecord(await api.get(`${versionPath(slug, version)}/usage`));
15
15
  const counts = answer?.usage;
16
16
  if (!counts || typeof counts.workflows !== "number") {
17
17
  throw new IntegrationsApiError(200, answer, "");
@@ -19,13 +19,13 @@ export function useNodeRetirement() {
19
19
  return counts;
20
20
  }
21
21
  async function retire(slug, version, reason) {
22
- return unwrapRecord(await api.post(`${versionPath(slug, version)}/retire`, {
22
+ return bareRecord(await api.post(`${versionPath(slug, version)}/retire`, {
23
23
  confirm: `${slug}@${version}`,
24
24
  ...reason ? { reason } : {}
25
- }), ["usage"]);
25
+ }));
26
26
  }
27
27
  async function restore(slug, version) {
28
- return unwrapRecord(await api.del(`${versionPath(slug, version)}/retire`), ["usage"]);
28
+ return bareRecord(await api.del(`${versionPath(slug, version)}/retire`));
29
29
  }
30
30
  return { versionsOfPackage, usage, retire, restore };
31
31
  }
@@ -3,8 +3,12 @@
3
3
  "integration.audit.action.create": "erstellt",
4
4
  "integration.audit.action.delete": "gelöscht",
5
5
  "integration.audit.action.loadMore": "Weitere laden",
6
+ "integration.audit.action.restore": "wiederhergestellt",
7
+ "integration.audit.action.retire": "ausgemustert",
8
+ "integration.audit.action.runCancelled": "Run abgebrochen",
6
9
  "integration.audit.action.runFailed": "Run fehlgeschlagen",
7
10
  "integration.audit.action.runFinished": "Run beendet",
11
+ "integration.audit.action.runResumed": "Run fortgesetzt",
8
12
  "integration.audit.action.runStarted": "Run gestartet",
9
13
  "integration.audit.action.update": "aktualisiert",
10
14
  "integration.audit.actor.system": "System",
@@ -14,13 +18,18 @@
14
18
  "integration.audit.detail.id": "ID",
15
19
  "integration.audit.detail.tenant": "Mandant",
16
20
  "integration.audit.head.title": "Audit-Log · Integration Studio",
17
- "integration.audit.header.description": "Wer hat was geändert, und wann. Nur anfügend, pro Mandant — Zugangsdaten, Secrets, Workflows, Trigger und Runs.",
21
+ "integration.audit.header.description": "Wer hat was geändert, und wann. Nur anfügend, pro Mandant — Zugangsdaten, Secrets, Workflows, Trigger, Runs, Node-Versionen und gespeicherter Zustand.",
18
22
  "integration.audit.header.title": "Audit-Log",
19
23
  "integration.audit.panel.close.aria": "Details schließen",
20
24
  "integration.audit.panel.dismiss.aria": "Details schließen",
21
25
  "integration.audit.panel.title.aria": "Details zum Audit-Eintrag",
26
+ "integration.audit.resource.run": "Run von Workflow #{workflow}",
27
+ "integration.audit.resource.trigger": "{type}-Trigger von Workflow #{workflow}",
22
28
  "integration.audit.resourceType.credential": "Zugangsdaten",
29
+ "integration.audit.resourceType.nodeVersion": "Node-Version",
23
30
  "integration.audit.resourceType.secret": "Secret",
31
+ "integration.audit.resourceType.stateEntry": "Zustandseintrag",
32
+ "integration.audit.resourceType.stateNamespace": "Zustands-Namespace",
24
33
  "integration.audit.resourceType.trigger": "Trigger",
25
34
  "integration.audit.resourceType.workflow": "Workflow",
26
35
  "integration.audit.resourceType.workflowRun": "Workflow-Run",
@@ -285,6 +294,9 @@
285
294
  "integration.credentials.shared.field.select.placeholder": "Auswählen",
286
295
  "integration.credentials.shared.oauthHint": "Nach dem Speichern öffnet sich ein Fenster, um die Verbindung mit dem Anbieter herzustellen.",
287
296
  "integration.credentials.shared.test.failed": "Verbindung fehlgeschlagen.",
297
+ "integration.credentials.shared.test.notAllowed": "Nur wer diese Zugangsdaten ändern darf, kann Änderungen daran testen.",
298
+ "integration.credentials.shared.test.notTested": "Nicht getestet — die Plattform konnte den Test nicht ausführen. Über die Verbindung ist nichts bekannt.",
299
+ "integration.credentials.shared.test.notTestedBecause": "Nicht getestet — {reason}",
288
300
  "integration.credentials.shared.test.ok": "Verbindung ok.",
289
301
  "integration.credentials.state.connected": "Verbunden",
290
302
  "integration.credentials.state.failed": "Fehlgeschlagen",
@@ -908,6 +920,7 @@
908
920
  "integration.replay.debug.field.config": "Konfiguration",
909
921
  "integration.replay.debug.field.duration": "Dauer",
910
922
  "integration.replay.debug.field.error": "Fehler",
923
+ "integration.replay.debug.field.frames": "Stelle im Code des Nodes",
911
924
  "integration.replay.debug.field.input": "Eingabe",
912
925
  "integration.replay.debug.field.output": "Ausgabe",
913
926
  "integration.replay.debug.field.started": "Gestartet",
@@ -1096,10 +1109,10 @@
1096
1109
  "integration.secrets.dialog.create.title": "Secret anlegen",
1097
1110
  "integration.secrets.dialog.delete.body": "wird endgültig entfernt. Workflows, die darauf verweisen, funktionieren beim nächsten Run nicht mehr.",
1098
1111
  "integration.secrets.dialog.delete.title": "Secret löschen?",
1099
- "integration.secrets.error.invalidKey": "Der Schlüssel muss dem Muster ^[A-Z0-9_]+$ entsprechen.",
1112
+ "integration.secrets.error.invalidKey": "Der Schlüssel muss dem Muster ^[A-Za-z0-9_-]+$ entsprechen und darf höchstens 191 Zeichen lang sein.",
1100
1113
  "integration.secrets.error.loadFailed": "Secrets konnten nicht geladen werden.",
1101
1114
  "integration.secrets.error.valueRequired": "Der Wert ist erforderlich.",
1102
- "integration.secrets.form.error.invalidKey": "Der Schlüssel muss ^[A-Z0-9_]+$ entsprechen (Großbuchstaben, Ziffern, Unterstriche).",
1115
+ "integration.secrets.form.error.invalidKey": "Der Schlüssel muss ^[A-Za-z0-9_-]+$ entsprechen (Buchstaben, Ziffern, Unterstriche, Bindestriche).",
1103
1116
  "integration.secrets.form.error.saveFailed": "Das Secret konnte nicht gespeichert werden.",
1104
1117
  "integration.secrets.form.key.label": "Schlüssel",
1105
1118
  "integration.secrets.form.key.patternLabel": "Muster:",
@@ -1156,9 +1169,11 @@
1156
1169
  "integration.templates.detail.install.secretsDescriptionSuffix": ".",
1157
1170
  "integration.templates.detail.install.secretsHeader": "Secrets",
1158
1171
  "integration.templates.detail.install.selectLater": "Später auswählen",
1172
+ "integration.templates.detail.install.settingLabel": "Node {id} · {setting}",
1159
1173
  "integration.templates.detail.install.state.loading": "Es wird geprüft, was diese Vorlage benötigt…",
1160
1174
  "integration.templates.detail.install.submit": "Installieren",
1161
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.",
1162
1177
  "integration.templates.detail.publisher.kind.community": "Community",
1163
1178
  "integration.templates.detail.publisher.kind.partner": "Partner",
1164
1179
  "integration.templates.detail.publisher.kind.revenexx": "Offiziell",
@@ -1176,6 +1191,8 @@
1176
1191
  "integration.templates.detail.setup.missingCredentials": "{n} Zugangsdatensatz zu verbinden | {n} Zugangsdatensätze zu verbinden",
1177
1192
  "integration.templates.detail.setup.missingSecrets": "{n} Secret fehlt | {n} Secrets fehlen",
1178
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.",
1179
1196
  "integration.templates.detail.state.loading": "Vorlage wird geladen…",
1180
1197
  "integration.templates.detail.state.notFound": "Vorlage nicht gefunden.",
1181
1198
  "integration.templates.detail.steps.empty": "Für diese Vorlage gibt es noch keine Schritt-für-Schritt-Übersicht.",
@@ -3,8 +3,12 @@
3
3
  "integration.audit.action.create": "create",
4
4
  "integration.audit.action.delete": "delete",
5
5
  "integration.audit.action.loadMore": "Load more",
6
+ "integration.audit.action.restore": "restore",
7
+ "integration.audit.action.retire": "retire",
8
+ "integration.audit.action.runCancelled": "run cancelled",
6
9
  "integration.audit.action.runFailed": "run failed",
7
10
  "integration.audit.action.runFinished": "run finished",
11
+ "integration.audit.action.runResumed": "run resumed",
8
12
  "integration.audit.action.runStarted": "run started",
9
13
  "integration.audit.action.update": "update",
10
14
  "integration.audit.actor.system": "system",
@@ -14,13 +18,18 @@
14
18
  "integration.audit.detail.id": "ID",
15
19
  "integration.audit.detail.tenant": "Tenant",
16
20
  "integration.audit.head.title": "Audit Log · Integration Studio",
17
- "integration.audit.header.description": "Who changed what, and when. Append-only, per-tenant — credentials, secrets, workflows, triggers and runs.",
21
+ "integration.audit.header.description": "Who changed what, and when. Append-only, per-tenant — credentials, secrets, workflows, triggers, runs, node versions and stored state.",
18
22
  "integration.audit.header.title": "Audit Log",
19
23
  "integration.audit.panel.close.aria": "Close entry details",
20
24
  "integration.audit.panel.dismiss.aria": "Dismiss entry details",
21
25
  "integration.audit.panel.title.aria": "Audit entry details",
26
+ "integration.audit.resource.run": "Run of workflow #{workflow}",
27
+ "integration.audit.resource.trigger": "{type} trigger of workflow #{workflow}",
22
28
  "integration.audit.resourceType.credential": "credential",
29
+ "integration.audit.resourceType.nodeVersion": "node version",
23
30
  "integration.audit.resourceType.secret": "secret",
31
+ "integration.audit.resourceType.stateEntry": "state entry",
32
+ "integration.audit.resourceType.stateNamespace": "state namespace",
24
33
  "integration.audit.resourceType.trigger": "trigger",
25
34
  "integration.audit.resourceType.workflow": "workflow",
26
35
  "integration.audit.resourceType.workflowRun": "workflow run",
@@ -285,6 +294,9 @@
285
294
  "integration.credentials.shared.field.select.placeholder": "Pick one",
286
295
  "integration.credentials.shared.oauthHint": "After saving, a window opens to connect with the provider.",
287
296
  "integration.credentials.shared.test.failed": "Connection failed.",
297
+ "integration.credentials.shared.test.notAllowed": "Only someone who may change this credential can test changes to it.",
298
+ "integration.credentials.shared.test.notTested": "Not tested — the platform could not run the test. Nothing is known about the connection.",
299
+ "integration.credentials.shared.test.notTestedBecause": "Not tested — {reason}",
288
300
  "integration.credentials.shared.test.ok": "Connection ok.",
289
301
  "integration.credentials.state.connected": "Connected",
290
302
  "integration.credentials.state.failed": "Failed",
@@ -908,6 +920,7 @@
908
920
  "integration.replay.debug.field.config": "Config",
909
921
  "integration.replay.debug.field.duration": "Duration",
910
922
  "integration.replay.debug.field.error": "Error",
923
+ "integration.replay.debug.field.frames": "Where in the node's code",
911
924
  "integration.replay.debug.field.input": "Input",
912
925
  "integration.replay.debug.field.output": "Output",
913
926
  "integration.replay.debug.field.started": "Started",
@@ -1096,10 +1109,10 @@
1096
1109
  "integration.secrets.dialog.create.title": "Create a secret",
1097
1110
  "integration.secrets.dialog.delete.body": "will be permanently removed. Workflows that reference it will break the next time they run.",
1098
1111
  "integration.secrets.dialog.delete.title": "Delete secret?",
1099
- "integration.secrets.error.invalidKey": "Key must match ^[A-Z0-9_]+$",
1112
+ "integration.secrets.error.invalidKey": "Key must match ^[A-Za-z0-9_-]+$ and be at most 191 characters",
1100
1113
  "integration.secrets.error.loadFailed": "Could not load secrets.",
1101
1114
  "integration.secrets.error.valueRequired": "Value is required.",
1102
- "integration.secrets.form.error.invalidKey": "Key must match ^[A-Z0-9_]+$ (uppercase letters, digits, underscores).",
1115
+ "integration.secrets.form.error.invalidKey": "Key must match ^[A-Za-z0-9_-]+$ (letters, digits, underscores, hyphens).",
1103
1116
  "integration.secrets.form.error.saveFailed": "Could not save the secret.",
1104
1117
  "integration.secrets.form.key.label": "Key",
1105
1118
  "integration.secrets.form.key.patternLabel": "Pattern:",
@@ -1156,9 +1169,11 @@
1156
1169
  "integration.templates.detail.install.secretsDescriptionSuffix": "page.",
1157
1170
  "integration.templates.detail.install.secretsHeader": "Secrets",
1158
1171
  "integration.templates.detail.install.selectLater": "Select later",
1172
+ "integration.templates.detail.install.settingLabel": "node {id} · {setting}",
1159
1173
  "integration.templates.detail.install.state.loading": "Checking what this template needs…",
1160
1174
  "integration.templates.detail.install.submit": "Install",
1161
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.",
1162
1177
  "integration.templates.detail.publisher.kind.community": "Community",
1163
1178
  "integration.templates.detail.publisher.kind.partner": "Partner",
1164
1179
  "integration.templates.detail.publisher.kind.revenexx": "Official",
@@ -1176,6 +1191,8 @@
1176
1191
  "integration.templates.detail.setup.missingCredentials": "{n} credential to connect | {n} credentials to connect",
1177
1192
  "integration.templates.detail.setup.missingSecrets": "{n} secret missing | {n} secrets missing",
1178
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.",
1179
1196
  "integration.templates.detail.state.loading": "Loading template…",
1180
1197
  "integration.templates.detail.state.notFound": "Template not found.",
1181
1198
  "integration.templates.detail.steps.empty": "No step-by-step outline for this template yet.",
@@ -7,20 +7,33 @@ import {
7
7
  Key as Secret,
8
8
  Programming as Workflow,
9
9
  Bolt as Trigger,
10
- Play as Run
10
+ Play as Run,
11
+ Box as NodeVersion,
12
+ Database as StateNamespace,
13
+ Document as StateEntry
11
14
  } from "#solar-icons";
12
15
  import { Badge, Button } from "@revenexx/studio";
13
16
  import { copyToClipboard } from "../../utils/clipboard";
14
17
  import { isBareIdentifier, personLabel } from "../../utils/personLabel";
18
+ import { triggerTypeLabel } from "../../data/integrationNodeCatalog";
15
19
  const t = useIntegrationsT();
16
20
  useHead({ title: () => t("integration.audit.head.title") });
17
21
  const { entries, loading, error, hasMore, load, loadMore } = useIntegrationsAudit();
18
- const resourceTypes = ["credential", "secret", "workflow", "trigger", "workflow_run"];
19
- const actions = ["create", "update", "delete", "run.started", "run.finished", "run.failed"];
22
+ const resourceTypes = ["credential", "secret", "workflow", "trigger", "workflow_run", "node_version", "state_namespace", "state_entry"];
23
+ const actions = ["create", "update", "delete", "run.started", "run.finished", "run.failed", "run.cancelled", "run.resumed", "retire", "restore"];
20
24
  onMounted(() => {
21
25
  void load();
22
26
  });
23
- const resourceIcon = { credential: Credential, secret: Secret, workflow: Workflow, trigger: Trigger, workflow_run: Run };
27
+ const resourceIcon = {
28
+ credential: Credential,
29
+ secret: Secret,
30
+ workflow: Workflow,
31
+ trigger: Trigger,
32
+ workflow_run: Run,
33
+ node_version: NodeVersion,
34
+ state_namespace: StateNamespace,
35
+ state_entry: StateEntry
36
+ };
24
37
  function actionTone(a) {
25
38
  if (a === "create" || a === "run.finished") return "bg-emerald-500/10 text-emerald-700 border-emerald-500/20";
26
39
  if (a === "update") return "bg-amber-500/10 text-amber-700 border-amber-500/20";
@@ -32,7 +45,10 @@ const resourceTypeKeys = {
32
45
  secret: "integration.audit.resourceType.secret",
33
46
  workflow: "integration.audit.resourceType.workflow",
34
47
  trigger: "integration.audit.resourceType.trigger",
35
- workflow_run: "integration.audit.resourceType.workflowRun"
48
+ workflow_run: "integration.audit.resourceType.workflowRun",
49
+ node_version: "integration.audit.resourceType.nodeVersion",
50
+ state_namespace: "integration.audit.resourceType.stateNamespace",
51
+ state_entry: "integration.audit.resourceType.stateEntry"
36
52
  };
37
53
  const actionKeys = {
38
54
  create: "integration.audit.action.create",
@@ -40,7 +56,11 @@ const actionKeys = {
40
56
  delete: "integration.audit.action.delete",
41
57
  "run.started": "integration.audit.action.runStarted",
42
58
  "run.finished": "integration.audit.action.runFinished",
43
- "run.failed": "integration.audit.action.runFailed"
59
+ "run.failed": "integration.audit.action.runFailed",
60
+ "run.cancelled": "integration.audit.action.runCancelled",
61
+ "run.resumed": "integration.audit.action.runResumed",
62
+ retire: "integration.audit.action.retire",
63
+ restore: "integration.audit.action.restore"
44
64
  };
45
65
  function prettyAction(a) {
46
66
  const key = actionKeys[a];
@@ -51,7 +71,18 @@ function prettyType(rt) {
51
71
  return key ? t(key) : rt.replace("_", " ");
52
72
  }
53
73
  function resourceName(e) {
54
- return e.metadata?.name || e.resource_id || "\u2014";
74
+ const m = e.metadata ?? {};
75
+ const text = (v) => typeof v === "string" && v !== "" ? v : void 0;
76
+ const workflow = m.workflow_id != null ? String(m.workflow_id) : void 0;
77
+ const name = text(m.name);
78
+ if (name) return name;
79
+ if (e.resource_type === "trigger" && workflow && text(m.type)) {
80
+ return t("integration.audit.resource.trigger", { type: triggerTypeLabel(text(m.type)), workflow });
81
+ }
82
+ if (e.resource_type === "workflow_run" && workflow) return t("integration.audit.resource.run", { workflow });
83
+ if (e.resource_type === "state_namespace" && text(m.namespace)) return text(m.namespace);
84
+ if (e.resource_type === "state_entry" && text(m.key)) return [text(m.namespace), text(m.key)].filter(Boolean).join(" \xB7 ");
85
+ return e.resource_id || "\u2014";
55
86
  }
56
87
  function fmtDate(iso) {
57
88
  return iso ? new Date(iso).toLocaleString() : "\u2014";
@@ -68,18 +99,21 @@ function actorIsId(e) {
68
99
  function actorIsPerson(e) {
69
100
  return !!(e.actor_name || e.actor_email);
70
101
  }
102
+ function withHeld(known, of) {
103
+ return [...known, ...[...new Set(entries.value.map(of))].filter((v) => v && !known.includes(v)).sort()];
104
+ }
71
105
  const filterTabs = computed(() => [
72
106
  {
73
107
  key: "resource",
74
108
  label: t("integration.audit.table.column.resource"),
75
109
  getter: (e) => e.resource_type,
76
- options: resourceTypes.map((rt) => ({ value: rt, label: prettyType(rt) }))
110
+ options: withHeld(resourceTypes, (e) => e.resource_type).map((rt) => ({ value: rt, label: prettyType(rt) }))
77
111
  },
78
112
  {
79
113
  key: "action",
80
114
  label: t("integration.audit.table.column.action"),
81
115
  getter: (e) => e.action,
82
- options: actions.map((a) => ({ value: a, label: prettyAction(a) }))
116
+ options: withHeld(actions, (e) => e.action).map((a) => ({ value: a, label: prettyAction(a) }))
83
117
  }
84
118
  ]);
85
119
  const columns = computed(() => [
@@ -37,6 +37,7 @@ import { hasRevenexxSource } from "../../utils/revenexxCredentialSource";
37
37
  import { resolveLocalized } from "../../utils/localize";
38
38
  import { fromModelValue, toItemValue, toModelValue } from "../../utils/selectOptions";
39
39
  import { keepOpenBeside } from "../../utils/dialog";
40
+ import { testOutcomeView } from "../../utils/credentialTest";
40
41
  const t = useIntegrationsT();
41
42
  useHead({ title: () => t("integration.credentials.list.head.title") });
42
43
  const { credentials, loading, error, load, create, update, remove, testInline, test, oauthAuthorizeUrl } = useIntegrationsCredentials();
@@ -462,12 +463,12 @@ async function confirmDelete() {
462
463
  <div
463
464
  v-if="testResult"
464
465
  class="rounded-md px-3 py-2 text-sm"
465
- :class="testResult.ok ? 'bg-emerald-500/10 text-emerald-700' : 'bg-rose-500/10 text-rose-700'"
466
+ :class="testOutcomeView(testResult, t).toneClass"
466
467
  >
467
468
  <!-- What the credential type said, whichever way the test went. The fixed
468
469
  sentences are the fallback for a type that reports nothing, not the
469
470
  first choice (specs/credential-test.md). -->
470
- {{ testResult.ok ? testResult.message ?? t("integration.credentials.shared.test.ok") : testResult.error ?? testResult.message ?? t("integration.credentials.shared.test.failed") }}
471
+ {{ testOutcomeView(testResult, t).text }}
471
472
  </div>
472
473
 
473
474
  </div>
@@ -41,7 +41,9 @@ function onReorder(order) {
41
41
  void setQuery(order);
42
42
  }
43
43
  let searchTimer;
44
- function onSearch(text) {
44
+ const SEARCH_MAX = 200;
45
+ function onSearch(typed) {
46
+ const text = [...typed].slice(0, SEARCH_MAX).join("");
45
47
  clearTimeout(searchTimer);
46
48
  if (text === query.value.search) return;
47
49
  searchTimer = setTimeout(() => {
@@ -26,6 +26,7 @@ import {
26
26
  } from "@revenexx/studio";
27
27
  import { errorReason } from "../../utils/apiError";
28
28
  import { keepOpenBeside } from "../../utils/dialog";
29
+ import { SECRET_KEY_MAX } from "../../composables/useIntegrationsSecrets";
29
30
  const t = useIntegrationsT();
30
31
  useHead({ title: () => t("integration.secrets.head.title") });
31
32
  const { secrets, loading, error, isValidKey, upsert, remove, load } = useIntegrationsSecrets();
@@ -121,9 +122,10 @@ const columns = computed(() => [
121
122
  placeholder="STRIPE_API_KEY"
122
123
  class="font-mono bg-white"
123
124
  autocomplete="off"
125
+ :maxlength="SECRET_KEY_MAX"
124
126
  />
125
127
  <p class="text-sm text-muted-foreground">
126
- {{ t("integration.secrets.form.key.patternLabel") }} <code class="font-mono">^[A-Z0-9_]+$</code>
128
+ {{ t("integration.secrets.form.key.patternLabel") }} <code class="font-mono">^[A-Za-z0-9_-]+$</code>
127
129
  </p>
128
130
  </div>
129
131
  <div class="space-y-1.5">
@@ -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, requirements } = useIntegrationsTemplates();
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
- await load();
35
- await Promise.all([fetchDetail(slug.value), loadNodeCatalog(false, { summary: true }), loadCredentials(), loadSecrets()]);
36
- reqs.value = await requirements(slug.value);
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(() => {
@@ -86,7 +91,7 @@ const triggerChips = computed(() => (tpl.value?.blob.triggers ?? []).map((trig)
86
91
  const when = describeCron(String(trig.config.cron)) ?? String(trig.config.cron);
87
92
  label = `${t("integration.templates.detail.trigger.schedule")} \xB7 ${when}${trig.config.timezone ? ` (${trig.config.timezone})` : ""}`;
88
93
  }
89
- if (trig.type === "webhook" && trig.config?.path) label = `${t("integration.templates.trigger.webhook")} \xB7 ${trig.config.path}`;
94
+ if (trig.type === "webhook" && trig.config?.method) label = `${t("integration.templates.trigger.webhook")} \xB7 ${String(trig.config.method).toUpperCase()}`;
90
95
  if (trig.type === "manual") label = t("integration.templates.trigger.manual");
91
96
  return { key: trig.id, icon: glyphForTrigger(trig.type), label };
92
97
  }));
@@ -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
- const fromApi = secretReqs.value;
109
- if (fromApi.length) {
110
- return fromApi.map((s) => ({
111
- key: s.key,
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="setupReady" class="flex items-center gap-1.5 text-sm text-emerald-700">
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) }}
@@ -472,7 +472,7 @@ export interface CredentialTypeField {
472
472
  label: LocalizedString;
473
473
  required?: boolean;
474
474
  options?: Array<{
475
- label: string;
475
+ label: LocalizedString;
476
476
  value: string | number | boolean;
477
477
  }>;
478
478
  validation?: {
@@ -495,8 +495,10 @@ export interface CredentialType {
495
495
  slug: string;
496
496
  version: string;
497
497
  name: LocalizedString;
498
- description?: LocalizedString;
499
- icon?: string;
498
+ /** Always sent; null where the descriptor has none. */
499
+ description?: LocalizedString | null;
500
+ /** Always sent; null where the descriptor has none. */
501
+ icon?: string | null;
500
502
  /** Signed logo/brand image URLs from the API (prefer `category: 'logo'`). */
501
503
  images?: CredentialTypeImage[];
502
504
  auth_kind: AuthKind;
@@ -629,7 +631,8 @@ export interface WorkflowTemplate {
629
631
  * under `sync` is a name invented by the reader's side of the wire.
630
632
  */
631
633
  category: TemplateCategory | '';
632
- industries: Array<'mechanical-engineering' | 'chemicals' | 'wholesale' | 'manufacturing' | 'retail' | 'any'>;
634
+ /** Free-form registry tags; `any` is the one the studio words itself. */
635
+ industries: string[];
633
636
  /** Vendor slugs this template wires together (used to compute prerequisites). */
634
637
  vendors: string[];
635
638
  /** Required secret keys; UI shows which are missing. */
@@ -695,6 +698,17 @@ export interface WorkflowRunNodeStep {
695
698
  attempt: number;
696
699
  durationMs: number | null;
697
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
+ }
698
712
  export interface RunStepRow {
699
713
  node_id: string;
700
714
  node_slug: string;
@@ -713,7 +727,11 @@ export interface RunStepRow {
713
727
  port: string;
714
728
  } | null;
715
729
  branch?: string | null;
716
- error?: string | null;
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;
717
735
  input?: unknown;
718
736
  output?: unknown;
719
737
  /** Resolved node config for this step (present with include_payloads). */
@@ -725,10 +743,10 @@ export interface RunStepRow {
725
743
  export interface DeadLetterRow {
726
744
  id: number;
727
745
  workflow_id: number;
728
- trigger_id: string | null;
746
+ trigger_id: number | null;
729
747
  source: string | null;
730
748
  trigger_handle: string | null;
731
- reason: string | null;
749
+ reason: string;
732
750
  status: 'pending' | 'replayed' | 'discarded' | string;
733
751
  created_at: string | null;
734
752
  updated_at: string | null;
@@ -1,9 +1,10 @@
1
1
  /**
2
- * One record, from `{ data: record }` or from a bare record.
2
+ * One record, out of the `{ data: record }` a wrapping endpoint sends — the
3
+ * template endpoints and `GET /credential-types/{slug}` (see the table above).
3
4
  *
4
- * Throws when the response carries an envelope with nothing in it, or no body at
5
- * all — the case the old expression turned into an object with none of the
6
- * caller's fields, which every page downstream drew as empty.
5
+ * Throws when the body carries no `data`, a null one, or no body at all — the
6
+ * case the old expression turned into an object with none of the caller's
7
+ * fields, which every page downstream drew as empty.
7
8
  *
8
9
  * **The thrown message is deliberately empty.** `errorReason(err, fallback)` in
9
10
  * `apiError.ts` falls back to the caller's own translated sentence when the
@@ -11,7 +12,16 @@
11
12
  * not load this" copy rather than as an untranslated technical string in the UI.
12
13
  * The response that caused it rides along in `body`.
13
14
  */
14
- export declare function unwrapRecord<T>(res: unknown, ownFields?: readonly string[]): T;
15
+ export declare function unwrapRecord<T>(res: unknown): T;
16
+ /**
17
+ * One record that IS the body — every single-record endpoint outside templates.
18
+ *
19
+ * Throws, the way `unwrapRecord` does, for no body at all and for a body shaped
20
+ * like an envelope: a `data` key and none of `ownFields`. No version of these
21
+ * endpoints sent one. `ownFields` is for a record that may carry a `data` field of
22
+ * its own — a run (`RUN_OWN_FIELDS`) — so that one is not mistaken for a wrapper.
23
+ */
24
+ export declare function bareRecord<T>(res: unknown, ownFields?: readonly string[]): T;
15
25
  /**
16
26
  * A list, from `{ data: [ … ] }` or from a bare array.
17
27
  *
@@ -1,14 +1,17 @@
1
1
  import { IntegrationsApiError } from "./apiError.js";
2
- export function unwrapRecord(res, ownFields = []) {
3
- if (res === null || res === void 0) {
2
+ export function unwrapRecord(res) {
3
+ const data = res?.data;
4
+ if (data === null || data === void 0 || typeof data !== "object" || Array.isArray(data)) {
4
5
  throw new IntegrationsApiError(200, res, "");
5
6
  }
6
- if (typeof res === "object" && "data" in res && !ownFields.some((f) => f in res)) {
7
- const data = res.data;
8
- if (data === null || data === void 0) {
9
- throw new IntegrationsApiError(200, res, "");
10
- }
11
- return data;
7
+ return data;
8
+ }
9
+ export function bareRecord(res, ownFields = []) {
10
+ if (res === null || res === void 0 || typeof res !== "object" || Array.isArray(res)) {
11
+ throw new IntegrationsApiError(200, res, "");
12
+ }
13
+ if ("data" in res && !ownFields.some((f) => f in res)) {
14
+ throw new IntegrationsApiError(200, res, "");
12
15
  }
13
16
  return res;
14
17
  }