@revenexx/studio-integrations 3.0.2 → 3.1.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 (36) hide show
  1. package/CHANGELOG.md +20 -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 +14 -5
  5. package/dist/runtime/components/Integrations/NodeInspector.vue +1 -1
  6. package/dist/runtime/components/Integrations/TemplateFlowPreview.vue +1 -1
  7. package/dist/runtime/components/Integrations/TemplateInstallStep.vue +28 -10
  8. package/dist/runtime/composables/useIntegrationsAudit.d.ts +7 -2
  9. package/dist/runtime/composables/useIntegrationsCredentials.d.ts +18 -3
  10. package/dist/runtime/composables/useIntegrationsCredentials.js +11 -6
  11. package/dist/runtime/composables/useIntegrationsRuns.d.ts +1 -1
  12. package/dist/runtime/composables/useIntegrationsRuns.js +8 -4
  13. package/dist/runtime/composables/useIntegrationsSchedules.js +6 -3
  14. package/dist/runtime/composables/useIntegrationsSecrets.d.ts +6 -5
  15. package/dist/runtime/composables/useIntegrationsSecrets.js +3 -2
  16. package/dist/runtime/composables/useIntegrationsTemplates.d.ts +1 -1
  17. package/dist/runtime/composables/useIntegrationsTemplates.js +5 -6
  18. package/dist/runtime/composables/useIntegrationsWorkflows.js +4 -4
  19. package/dist/runtime/composables/useNodeRetirement.js +5 -5
  20. package/dist/runtime/i18n/messages/de.json +18 -4
  21. package/dist/runtime/i18n/messages/en.json +18 -4
  22. package/dist/runtime/pages/integrations/audit.vue +43 -9
  23. package/dist/runtime/pages/integrations/credentials.vue +3 -2
  24. package/dist/runtime/pages/integrations/runs.vue +3 -1
  25. package/dist/runtime/pages/integrations/secrets.vue +3 -1
  26. package/dist/runtime/pages/integrations/templates/[slug].vue +1 -1
  27. package/dist/runtime/types/integrations.d.ts +9 -6
  28. package/dist/runtime/utils/apiEnvelope.d.ts +15 -5
  29. package/dist/runtime/utils/apiEnvelope.js +11 -8
  30. package/dist/runtime/utils/credentialTest.d.ts +17 -0
  31. package/dist/runtime/utils/credentialTest.js +14 -0
  32. package/dist/runtime/utils/runEnvelope.d.ts +22 -24
  33. package/dist/runtime/utils/runEnvelope.js +2 -4
  34. package/package.json +1 -1
  35. package/dist/runtime/data/integrationWorkflowSamples.d.ts +0 -18
  36. package/dist/runtime/data/integrationWorkflowSamples.js +0 -318
@@ -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",
@@ -198,7 +207,7 @@
198
207
  "integration.credentials.list.action.reconnect.aria": "{name} erneut verbinden",
199
208
  "integration.credentials.list.dialog.action.back": "Zurück",
200
209
  "integration.credentials.list.dialog.description.create": "Wählen Sie einen Typ und geben Sie die Zugangsdaten ein.",
201
- "integration.credentials.list.dialog.description.edit": "Aktualisieren Sie die Zugangsdaten. Lassen Sie Secret-Felder leer, um den aktuellen Wert zu behalten.",
210
+ "integration.credentials.list.dialog.description.edit": "Aktualisieren Sie die Zugangsdaten. Lassen Sie Secret-Felder leer, um den aktuellen Wert zu behalten — das geht nicht, wenn sich der Server oder eine andere Angabe ändert.",
202
211
  "integration.credentials.list.dialog.field.name.placeholder": "z. B. Produktions-SMTP",
203
212
  "integration.credentials.list.empty": "Noch keine Zugangsdaten — mit „{action}“ das erste System verbinden.",
204
213
  "integration.credentials.list.head.title": "Zugangsdaten · Revenexx Cockpit",
@@ -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",
@@ -294,6 +306,7 @@
294
306
  "integration.credentials.toast.created": "Zugangsdaten erstellt",
295
307
  "integration.credentials.toast.deleted": "Zugangsdaten gelöscht",
296
308
  "integration.credentials.toast.updated": "Zugangsdaten aktualisiert",
309
+ "integration.credentials.toast.updatedReconnect": "Zugangsdaten aktualisiert — verbinden Sie sie neu, um sie wieder zu nutzen",
297
310
  "integration.credentials.typePicker.empty": "Kein Zugangsdatentyp entspricht „{query}“.",
298
311
  "integration.credentials.typePicker.search.aria": "Nach einer App suchen",
299
312
  "integration.credentials.typePicker.search.placeholder": "Nach einer App suchen…",
@@ -1095,10 +1108,10 @@
1095
1108
  "integration.secrets.dialog.create.title": "Secret anlegen",
1096
1109
  "integration.secrets.dialog.delete.body": "wird endgültig entfernt. Workflows, die darauf verweisen, funktionieren beim nächsten Run nicht mehr.",
1097
1110
  "integration.secrets.dialog.delete.title": "Secret löschen?",
1098
- "integration.secrets.error.invalidKey": "Der Schlüssel muss dem Muster ^[A-Z0-9_]+$ entsprechen.",
1111
+ "integration.secrets.error.invalidKey": "Der Schlüssel muss dem Muster ^[A-Za-z0-9_-]+$ entsprechen und darf höchstens 191 Zeichen lang sein.",
1099
1112
  "integration.secrets.error.loadFailed": "Secrets konnten nicht geladen werden.",
1100
1113
  "integration.secrets.error.valueRequired": "Der Wert ist erforderlich.",
1101
- "integration.secrets.form.error.invalidKey": "Der Schlüssel muss ^[A-Z0-9_]+$ entsprechen (Großbuchstaben, Ziffern, Unterstriche).",
1114
+ "integration.secrets.form.error.invalidKey": "Der Schlüssel muss ^[A-Za-z0-9_-]+$ entsprechen (Buchstaben, Ziffern, Unterstriche, Bindestriche).",
1102
1115
  "integration.secrets.form.error.saveFailed": "Das Secret konnte nicht gespeichert werden.",
1103
1116
  "integration.secrets.form.key.label": "Schlüssel",
1104
1117
  "integration.secrets.form.key.patternLabel": "Muster:",
@@ -1155,6 +1168,7 @@
1155
1168
  "integration.templates.detail.install.secretsDescriptionSuffix": ".",
1156
1169
  "integration.templates.detail.install.secretsHeader": "Secrets",
1157
1170
  "integration.templates.detail.install.selectLater": "Später auswählen",
1171
+ "integration.templates.detail.install.settingLabel": "Node {id} · {setting}",
1158
1172
  "integration.templates.detail.install.state.loading": "Es wird geprüft, was diese Vorlage benötigt…",
1159
1173
  "integration.templates.detail.install.submit": "Installieren",
1160
1174
  "integration.templates.detail.install.submitting": "Wird eingerichtet…",
@@ -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",
@@ -198,7 +207,7 @@
198
207
  "integration.credentials.list.action.reconnect.aria": "Reconnect {name}",
199
208
  "integration.credentials.list.dialog.action.back": "Back",
200
209
  "integration.credentials.list.dialog.description.create": "Pick a type and fill in its access data.",
201
- "integration.credentials.list.dialog.description.edit": "Update the credential. Leave secret fields blank to keep the current value.",
210
+ "integration.credentials.list.dialog.description.edit": "Update the credential. Leave secret fields blank to keep the current value — not possible when the server or any other setting changes.",
202
211
  "integration.credentials.list.dialog.field.name.placeholder": "e.g. Production SMTP",
203
212
  "integration.credentials.list.empty": "No credentials yet — hit “{action}” to connect your first system.",
204
213
  "integration.credentials.list.head.title": "Credentials · Revenexx Cockpit",
@@ -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",
@@ -294,6 +306,7 @@
294
306
  "integration.credentials.toast.created": "Credential created",
295
307
  "integration.credentials.toast.deleted": "Credential deleted",
296
308
  "integration.credentials.toast.updated": "Credential updated",
309
+ "integration.credentials.toast.updatedReconnect": "Credential updated — reconnect it to use it again",
297
310
  "integration.credentials.typePicker.empty": "No credential type matches “{query}”.",
298
311
  "integration.credentials.typePicker.search.aria": "Search for an app",
299
312
  "integration.credentials.typePicker.search.placeholder": "Search for an app…",
@@ -1095,10 +1108,10 @@
1095
1108
  "integration.secrets.dialog.create.title": "Create a secret",
1096
1109
  "integration.secrets.dialog.delete.body": "will be permanently removed. Workflows that reference it will break the next time they run.",
1097
1110
  "integration.secrets.dialog.delete.title": "Delete secret?",
1098
- "integration.secrets.error.invalidKey": "Key must match ^[A-Z0-9_]+$",
1111
+ "integration.secrets.error.invalidKey": "Key must match ^[A-Za-z0-9_-]+$ and be at most 191 characters",
1099
1112
  "integration.secrets.error.loadFailed": "Could not load secrets.",
1100
1113
  "integration.secrets.error.valueRequired": "Value is required.",
1101
- "integration.secrets.form.error.invalidKey": "Key must match ^[A-Z0-9_]+$ (uppercase letters, digits, underscores).",
1114
+ "integration.secrets.form.error.invalidKey": "Key must match ^[A-Za-z0-9_-]+$ (letters, digits, underscores, hyphens).",
1102
1115
  "integration.secrets.form.error.saveFailed": "Could not save the secret.",
1103
1116
  "integration.secrets.form.key.label": "Key",
1104
1117
  "integration.secrets.form.key.patternLabel": "Pattern:",
@@ -1155,6 +1168,7 @@
1155
1168
  "integration.templates.detail.install.secretsDescriptionSuffix": "page.",
1156
1169
  "integration.templates.detail.install.secretsHeader": "Secrets",
1157
1170
  "integration.templates.detail.install.selectLater": "Select later",
1171
+ "integration.templates.detail.install.settingLabel": "node {id} · {setting}",
1158
1172
  "integration.templates.detail.install.state.loading": "Checking what this template needs…",
1159
1173
  "integration.templates.detail.install.submit": "Install",
1160
1174
  "integration.templates.detail.install.submitting": "Setting up…",
@@ -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">
@@ -86,7 +86,7 @@ const triggerChips = computed(() => (tpl.value?.blob.triggers ?? []).map((trig)
86
86
  const when = describeCron(String(trig.config.cron)) ?? String(trig.config.cron);
87
87
  label = `${t("integration.templates.detail.trigger.schedule")} \xB7 ${when}${trig.config.timezone ? ` (${trig.config.timezone})` : ""}`;
88
88
  }
89
- if (trig.type === "webhook" && trig.config?.path) label = `${t("integration.templates.trigger.webhook")} \xB7 ${trig.config.path}`;
89
+ if (trig.type === "webhook" && trig.config?.method) label = `${t("integration.templates.trigger.webhook")} \xB7 ${String(trig.config.method).toUpperCase()}`;
90
90
  if (trig.type === "manual") label = t("integration.templates.trigger.manual");
91
91
  return { key: trig.id, icon: glyphForTrigger(trig.type), label };
92
92
  }));
@@ -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. */
@@ -725,10 +728,10 @@ export interface RunStepRow {
725
728
  export interface DeadLetterRow {
726
729
  id: number;
727
730
  workflow_id: number;
728
- trigger_id: string | null;
731
+ trigger_id: number | null;
729
732
  source: string | null;
730
733
  trigger_handle: string | null;
731
- reason: string | null;
734
+ reason: string;
732
735
  status: 'pending' | 'replayed' | 'discarded' | string;
733
736
  created_at: string | null;
734
737
  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
  }
@@ -0,0 +1,17 @@
1
+ import type { CredentialTestResult } from '../composables/useIntegrationsCredentials.js';
2
+ /** What a connection test came to, as the three surfaces that run one draw it. */
3
+ export interface TestOutcomeView {
4
+ kind: 'ok' | 'failed' | 'untested';
5
+ /** The tone of the box: green, red, or neutral for a test that did not happen. */
6
+ toneClass: string;
7
+ text: string;
8
+ }
9
+ /**
10
+ * One reading of a test result for every surface (specs/credential-test.md).
11
+ *
12
+ * Three answers, not two: a test the platform could not run — its test broker had no
13
+ * room, or the reader may not try an edit — said nothing about the credential, and
14
+ * drawing it red told the operator the connection was broken (IN-22). The words are
15
+ * the platform's where it sent any, the studio's fixed sentence otherwise.
16
+ */
17
+ export declare function testOutcomeView(result: CredentialTestResult, t: (key: string, params?: Record<string, unknown>) => string): TestOutcomeView;
@@ -0,0 +1,14 @@
1
+ export function testOutcomeView(result, t) {
2
+ if (result.ok) {
3
+ return { kind: "ok", toneClass: "bg-emerald-500/10 text-emerald-700", text: result.message ?? t("integration.credentials.shared.test.ok") };
4
+ }
5
+ if (result.untested) {
6
+ const reason = result.error ?? result.message;
7
+ return {
8
+ kind: "untested",
9
+ toneClass: "bg-slate-500/10 text-slate-700",
10
+ text: reason ? t("integration.credentials.shared.test.notTestedBecause", { reason }) : t("integration.credentials.shared.test.notTested")
11
+ };
12
+ }
13
+ return { kind: "failed", toneClass: "bg-rose-500/10 text-rose-700", text: result.error ?? result.message ?? t("integration.credentials.shared.test.failed") };
14
+ }
@@ -1,28 +1,5 @@
1
1
  /** A run as the API sends it, before anything has been read out of it. */
2
2
  export type RunLike = Record<string, unknown>;
3
- /**
4
- * The run out of a single-run response, enveloped or not.
5
- *
6
- * `GET /workflows/{id}/runs/{runId}` answers the run itself, and the studio's own
7
- * mock and some callers wrap it in `{ data: … }`. So the envelope has to be
8
- * optional — but unwrapping on the mere presence of a `data` key is wrong: a run
9
- * that ever carried one would be unwrapped into it, and the caller would be
10
- * handed a field instead of a run. Hence the two guards: a body that already
11
- * looks like a run (`status`, `temporal_run_id`) is the run, whatever else it has
12
- * on it.
13
- *
14
- * The two guards are `RUN_OWN_FIELDS`, exported because `unwrapRecord` reads the
15
- * same endpoint from `useIntegrationsRuns.ts` and has to apply the same rule —
16
- * one list, two readers, rather than the two literals that used to sit here
17
- * (PO-394). What stays different is deliberate: this one answers `null` where
18
- * `unwrapRecord` throws, because a caller following a run has to count an
19
- * unusable answer as a failed read rather than have it raised at it.
20
- *
21
- * This is deliberately generous about the *rest* of the shape. It answers "where
22
- * is the run in this body", not "is this a usable run" — the caller decides that,
23
- * because a caller following a run has to be able to count an unusable answer as
24
- * a failed read rather than sit on it.
25
- */
26
3
  /**
27
4
  * The raw platform statuses a run does not come back from.
28
5
  *
@@ -37,7 +14,28 @@ export declare function isTerminalRun(run: {
37
14
  } | null | undefined): boolean;
38
15
  /**
39
16
  * The fields that mark a body as the run itself rather than an envelope around
40
- * one. A run carrying its own `data` field would otherwise be unwrapped into it.
17
+ * one. A run carrying its own `data` field would otherwise be taken for an envelope.
41
18
  */
42
19
  export declare const RUN_OWN_FIELDS: readonly ["status", "temporal_run_id"];
20
+ /**
21
+ * The run out of a single-run response — the body itself.
22
+ *
23
+ * `GET /workflows/{id}/runs/{runId}` answers the run bare, and no version of it
24
+ * sent an envelope. Until IN-22 this accepted `{ data: run }` as well, because the
25
+ * studio's own mocks wrapped it; that tolerance is what let them stay wrong, so a
26
+ * body shaped like an envelope is now no run at all. "Shaped like an envelope"
27
+ * needs care: a run may carry a `data` field of its own, so a body that already
28
+ * looks like a run (`status`, `temporal_run_id`) is the run, whatever else it has
29
+ * on it.
30
+ *
31
+ * Those two guards are `RUN_OWN_FIELDS`, exported because `bareRecord` reads the
32
+ * same endpoint from `useIntegrationsRuns.ts` and has to apply the same rule —
33
+ * one list, two readers, rather than the two literals that used to sit here
34
+ * (PO-394). What stays different is deliberate: this one answers `null` where
35
+ * `bareRecord` throws, because a caller following a run has to count an
36
+ * unusable answer as a failed read rather than have it raised at it.
37
+ *
38
+ * This is deliberately generous about the *rest* of the shape. It answers "is
39
+ * there a run in this body", not "is this a usable run" — the caller decides that.
40
+ */
43
41
  export declare function unwrapRun(res: unknown): RunLike | null;
@@ -4,10 +4,8 @@ export function isTerminalRun(run) {
4
4
  }
5
5
  export const RUN_OWN_FIELDS = ["status", "temporal_run_id"];
6
6
  export function unwrapRun(res) {
7
- if (!res || typeof res !== "object") return null;
7
+ if (!res || typeof res !== "object" || Array.isArray(res)) return null;
8
8
  const o = res;
9
- if ("data" in o && o.data && typeof o.data === "object" && !RUN_OWN_FIELDS.some((f) => f in o)) {
10
- return o.data;
11
- }
9
+ if ("data" in o && !RUN_OWN_FIELDS.some((f) => f in o)) return null;
12
10
  return o;
13
11
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@revenexx/studio-integrations",
3
- "version": "3.0.2",
3
+ "version": "3.1.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": {
@@ -1,18 +0,0 @@
1
- import type { Connection, ConnectionTemplate, Workflow } from '../types/integrations.js';
2
- /**
3
- * Seed data for the workflow list. Built strictly with the manifests the
4
- * backend currently has fixtures for (`rest-get`, `control-if`,
5
- * `control-switch`) and the trigger output port the reference workflow
6
- * blobs use (`default` — see
7
- * `playground/integrations/tests/Fixtures/WorkflowBlobs/*.json`).
8
- *
9
- * If you change a manifest's `slug` / `version`, change it here too.
10
- */
11
- export declare const SAMPLE_WORKFLOWS: Workflow[];
12
- export declare const SAMPLE_SECRETS: {
13
- key: string;
14
- created_at: string;
15
- updated_at: string;
16
- }[];
17
- export declare const CONNECTION_TEMPLATES: ConnectionTemplate[];
18
- export declare const SAMPLE_CONNECTIONS: Connection[];