@revenexx/studio-integrations 3.0.3 → 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.
- package/CHANGELOG.md +12 -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/TemplateFlowPreview.vue +1 -1
- package/dist/runtime/components/Integrations/TemplateInstallStep.vue +28 -10
- 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.d.ts +1 -1
- 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 +1 -1
- package/dist/runtime/composables/useIntegrationsTemplates.js +5 -6
- package/dist/runtime/composables/useIntegrationsWorkflows.js +4 -4
- package/dist/runtime/composables/useNodeRetirement.js +5 -5
- package/dist/runtime/i18n/messages/de.json +16 -3
- package/dist/runtime/i18n/messages/en.json +16 -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 +1 -1
- package/dist/runtime/types/integrations.d.ts +9 -6
- 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/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,17 @@
|
|
|
1
1
|
# @revenexx/studio-integrations
|
|
2
2
|
|
|
3
|
+
## 3.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 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.
|
|
8
|
+
|
|
9
|
+
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.
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 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.
|
|
14
|
+
|
|
3
15
|
## 3.0.3
|
|
4
16
|
|
|
5
17
|
### 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;
|
|
@@ -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",
|
|
@@ -29,6 +29,7 @@ const installing = ref(false);
|
|
|
29
29
|
const error = ref(null);
|
|
30
30
|
const name = ref("");
|
|
31
31
|
const bindings = reactive({});
|
|
32
|
+
const slotKey = (nodeId, configKey) => `${nodeId}\0${configKey}`;
|
|
32
33
|
let run = 0;
|
|
33
34
|
watch(() => props.slug, async (slug) => {
|
|
34
35
|
const mine = ++run;
|
|
@@ -68,16 +69,30 @@ const secretChecks = computed(() => {
|
|
|
68
69
|
present: secretKeys.value.includes(key)
|
|
69
70
|
}));
|
|
70
71
|
});
|
|
71
|
-
function
|
|
72
|
-
|
|
72
|
+
function slotsFor(usedBy) {
|
|
73
|
+
const all = credentialReqs.value.flatMap((r) => r.usedBy);
|
|
74
|
+
const seen = /* @__PURE__ */ new Set();
|
|
75
|
+
return usedBy.filter((u) => {
|
|
76
|
+
const key = slotKey(u.nodeId, u.configKey);
|
|
77
|
+
if (seen.has(key)) return false;
|
|
78
|
+
seen.add(key);
|
|
79
|
+
return true;
|
|
80
|
+
}).map((u) => ({
|
|
81
|
+
key: slotKey(u.nodeId, u.configKey),
|
|
82
|
+
nodeId: u.nodeId,
|
|
83
|
+
configKey: u.configKey,
|
|
84
|
+
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 })
|
|
85
|
+
}));
|
|
73
86
|
}
|
|
74
87
|
async function confirmInstall() {
|
|
75
88
|
installing.value = true;
|
|
76
89
|
error.value = null;
|
|
77
90
|
try {
|
|
78
91
|
const credentialBindings = {};
|
|
79
|
-
for (const [
|
|
80
|
-
if (credId
|
|
92
|
+
for (const [key, credId] of Object.entries(bindings)) {
|
|
93
|
+
if (!credId || credId === SELECT_LATER) continue;
|
|
94
|
+
const [nodeId, configKey] = key.split("\0");
|
|
95
|
+
(credentialBindings[nodeId] ??= {})[configKey] = credId;
|
|
81
96
|
}
|
|
82
97
|
const id = await instantiate(props.slug, {
|
|
83
98
|
name: name.value.trim(),
|
|
@@ -98,7 +113,8 @@ async function confirmInstall() {
|
|
|
98
113
|
<div class="space-y-4 py-1 max-h-[28rem] overflow-y-auto pr-1">
|
|
99
114
|
<div class="space-y-1.5">
|
|
100
115
|
<Label for="tpl-install-name">{{ t("integration.templates.detail.install.nameLabel") }}</Label>
|
|
101
|
-
|
|
116
|
+
<!-- 255: the platform's limit on a workflow's name (InstantiateTemplateRequest). -->
|
|
117
|
+
<Input id="tpl-install-name" v-model="name" class="bg-white" :maxlength="255" :placeholder="t('integration.templates.detail.install.namePlaceholder')" />
|
|
102
118
|
</div>
|
|
103
119
|
|
|
104
120
|
<div v-if="loading" class="flex items-center gap-2 py-4 text-sm text-muted-foreground">
|
|
@@ -131,15 +147,17 @@ async function confirmInstall() {
|
|
|
131
147
|
<NuxtLink to="/integrations/credentials" class="text-primary hover:underline">{{ t("integration.templates.detail.install.createOneLink") }}</NuxtLink>
|
|
132
148
|
{{ t("integration.templates.detail.install.orInstallLater") }}
|
|
133
149
|
</p>
|
|
134
|
-
<div v-for="
|
|
135
|
-
|
|
150
|
+
<div v-for="(slot, si) in slotsFor(req.usedBy)" v-else :key="slot.key" class="space-y-1">
|
|
151
|
+
<!-- By position, not by joining two free strings: `a-b`+`c` and `a`+`b-c`
|
|
152
|
+
would share an id, and a label would name the wrong select. -->
|
|
153
|
+
<Label :for="`tpl-cred-${req.slug}-${si}`" class="text-xs text-muted-foreground font-mono">{{ slot.label }}</Label>
|
|
136
154
|
<Select
|
|
137
|
-
:model-value="bindings[
|
|
155
|
+
:model-value="bindings[slot.key] ?? SELECT_LATER"
|
|
138
156
|
@update:model-value="(v) => {
|
|
139
|
-
bindings[
|
|
157
|
+
bindings[slot.key] = v ?? SELECT_LATER;
|
|
140
158
|
}"
|
|
141
159
|
>
|
|
142
|
-
<SelectTrigger :id="`tpl-cred-${
|
|
160
|
+
<SelectTrigger :id="`tpl-cred-${req.slug}-${si}`" class="bg-white">
|
|
143
161
|
<SelectValue :placeholder="t('integration.templates.detail.install.selectLater')" />
|
|
144
162
|
</SelectTrigger>
|
|
145
163
|
<SelectContent>
|
|
@@ -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
|
}
|
|
@@ -44,7 +44,7 @@ export declare function useIntegrationsRuns(): {
|
|
|
44
44
|
latestLoading: any;
|
|
45
45
|
loadLatestPerWorkflow: (force?: boolean) => Promise<WorkflowRun[]>;
|
|
46
46
|
lastRunFor: (workflowId: number) => WorkflowRun | null;
|
|
47
|
-
trigger: (workflowId: number | string, payload?: Record<string, unknown>, mode?: "
|
|
47
|
+
trigger: (workflowId: number | string, payload?: Record<string, unknown>, mode?: "sync" | "async") => Promise<unknown>;
|
|
48
48
|
cancelRun: (workflowId: number | string, runId: number | string, reason?: string) => Promise<unknown>;
|
|
49
49
|
terminateRun: (workflowId: number | string, runId: number | string, reason?: string) => Promise<unknown>;
|
|
50
50
|
retryRun: (workflowId: number | string, runId: number | string) => Promise<unknown>;
|
|
@@ -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;
|
|
@@ -14,7 +14,7 @@ export declare function useIntegrationsTemplates(): {
|
|
|
14
14
|
requirements: (slug: string) => Promise<TemplateRequirements | null>;
|
|
15
15
|
instantiate: (slug: string, opts?: {
|
|
16
16
|
name?: string;
|
|
17
|
-
credentialBindings?: Record<string, string
|
|
17
|
+
credentialBindings?: Record<string, string | Record<string, string>>;
|
|
18
18
|
}) => Promise<number | null>;
|
|
19
19
|
install: (slug: string) => Promise<number | null>;
|
|
20
20
|
};
|
|
@@ -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,
|
|
@@ -184,10 +183,10 @@ export function useIntegrationsTemplates() {
|
|
|
184
183
|
const body = {};
|
|
185
184
|
if (opts.name) body.name = opts.name;
|
|
186
185
|
const bindings = Object.fromEntries(
|
|
187
|
-
Object.entries(opts.credentialBindings ?? {}).filter(([,
|
|
186
|
+
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
187
|
);
|
|
189
188
|
if (Object.keys(bindings).length) body.credential_bindings = bindings;
|
|
190
|
-
const created =
|
|
189
|
+
const created = bareRecord(await api.post(`/templates/${slug}/instantiate`, body));
|
|
191
190
|
return Number(created.id);
|
|
192
191
|
}
|
|
193
192
|
async function install(slug) {
|
|
@@ -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 {
|
|
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(
|
|
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(
|
|
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(
|
|
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 {
|
|
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 =
|
|
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
|
|
22
|
+
return bareRecord(await api.post(`${versionPath(slug, version)}/retire`, {
|
|
23
23
|
confirm: `${slug}@${version}`,
|
|
24
24
|
...reason ? { reason } : {}
|
|
25
|
-
})
|
|
25
|
+
}));
|
|
26
26
|
}
|
|
27
27
|
async function restore(slug, version) {
|
|
28
|
-
return
|
|
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
|
|
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",
|
|
@@ -1096,10 +1108,10 @@
|
|
|
1096
1108
|
"integration.secrets.dialog.create.title": "Secret anlegen",
|
|
1097
1109
|
"integration.secrets.dialog.delete.body": "wird endgültig entfernt. Workflows, die darauf verweisen, funktionieren beim nächsten Run nicht mehr.",
|
|
1098
1110
|
"integration.secrets.dialog.delete.title": "Secret löschen?",
|
|
1099
|
-
"integration.secrets.error.invalidKey": "Der Schlüssel muss dem Muster ^[A-
|
|
1111
|
+
"integration.secrets.error.invalidKey": "Der Schlüssel muss dem Muster ^[A-Za-z0-9_-]+$ entsprechen und darf höchstens 191 Zeichen lang sein.",
|
|
1100
1112
|
"integration.secrets.error.loadFailed": "Secrets konnten nicht geladen werden.",
|
|
1101
1113
|
"integration.secrets.error.valueRequired": "Der Wert ist erforderlich.",
|
|
1102
|
-
"integration.secrets.form.error.invalidKey": "Der Schlüssel muss ^[A-
|
|
1114
|
+
"integration.secrets.form.error.invalidKey": "Der Schlüssel muss ^[A-Za-z0-9_-]+$ entsprechen (Buchstaben, Ziffern, Unterstriche, Bindestriche).",
|
|
1103
1115
|
"integration.secrets.form.error.saveFailed": "Das Secret konnte nicht gespeichert werden.",
|
|
1104
1116
|
"integration.secrets.form.key.label": "Schlüssel",
|
|
1105
1117
|
"integration.secrets.form.key.patternLabel": "Muster:",
|
|
@@ -1156,6 +1168,7 @@
|
|
|
1156
1168
|
"integration.templates.detail.install.secretsDescriptionSuffix": ".",
|
|
1157
1169
|
"integration.templates.detail.install.secretsHeader": "Secrets",
|
|
1158
1170
|
"integration.templates.detail.install.selectLater": "Später auswählen",
|
|
1171
|
+
"integration.templates.detail.install.settingLabel": "Node {id} · {setting}",
|
|
1159
1172
|
"integration.templates.detail.install.state.loading": "Es wird geprüft, was diese Vorlage benötigt…",
|
|
1160
1173
|
"integration.templates.detail.install.submit": "Installieren",
|
|
1161
1174
|
"integration.templates.detail.install.submitting": "Wird eingerichtet…",
|