@revenexx/studio-integrations 1.1.1 → 1.3.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 +638 -0
- package/README.md +3 -1
- package/dist/module.json +1 -1
- package/dist/runtime/components/Integrations/CredentialCreateDialog.vue +6 -18
- package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +13 -23
- package/dist/runtime/components/Integrations/CredentialRevenexxSource.vue +7 -6
- package/dist/runtime/components/Integrations/DeadLettersPanel.vue +15 -4
- package/dist/runtime/components/Integrations/ExpressionInput.d.vue.ts +4 -0
- package/dist/runtime/components/Integrations/ExpressionInput.vue +45 -19
- package/dist/runtime/components/Integrations/ExpressionInput.vue.d.ts +4 -0
- package/dist/runtime/components/Integrations/GroupFrameView.vue +5 -4
- package/dist/runtime/components/Integrations/LoadingAnnouncement.d.vue.ts +38 -0
- package/dist/runtime/components/Integrations/LoadingAnnouncement.vue +16 -0
- package/dist/runtime/components/Integrations/LoadingAnnouncement.vue.d.ts +38 -0
- package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.vue +10 -2
- package/dist/runtime/components/Integrations/NodeConfigField.d.vue.ts +5 -1
- package/dist/runtime/components/Integrations/NodeConfigField.vue +88 -1
- package/dist/runtime/components/Integrations/NodeConfigField.vue.d.ts +5 -1
- package/dist/runtime/components/Integrations/NodeInspector.d.vue.ts +7 -3
- package/dist/runtime/components/Integrations/NodeInspector.vue +125 -54
- package/dist/runtime/components/Integrations/NodeInspector.vue.d.ts +7 -3
- package/dist/runtime/components/Integrations/NodeVersionMoveDialog.d.vue.ts +21 -0
- package/dist/runtime/components/Integrations/NodeVersionMoveDialog.vue +133 -0
- package/dist/runtime/components/Integrations/NodeVersionMoveDialog.vue.d.ts +21 -0
- package/dist/runtime/components/Integrations/RunActionsMenu.vue +2 -1
- package/dist/runtime/components/Integrations/RunDetailPanel.vue +5 -17
- package/dist/runtime/components/Integrations/RunResponseModal.d.vue.ts +30 -8
- package/dist/runtime/components/Integrations/RunResponseModal.vue +92 -24
- package/dist/runtime/components/Integrations/RunResponseModal.vue.d.ts +30 -8
- package/dist/runtime/components/Integrations/RunsTable.d.vue.ts +61 -3
- package/dist/runtime/components/Integrations/RunsTable.vue +129 -18
- package/dist/runtime/components/Integrations/RunsTable.vue.d.ts +61 -3
- package/dist/runtime/components/Integrations/SftpAccountImportDialog.vue +10 -1
- package/dist/runtime/components/Integrations/StateNamespaceCreateDialog.d.vue.ts +16 -0
- package/dist/runtime/components/Integrations/StateNamespaceCreateDialog.vue +111 -0
- package/dist/runtime/components/Integrations/StateNamespaceCreateDialog.vue.d.ts +16 -0
- package/dist/runtime/components/Integrations/StickyNoteView.vue +1 -1
- package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +18 -3
- package/dist/runtime/components/Integrations/WorkflowDeleteDialog.d.vue.ts +18 -0
- package/dist/runtime/components/Integrations/WorkflowDeleteDialog.vue +70 -0
- package/dist/runtime/components/Integrations/WorkflowDeleteDialog.vue.d.ts +18 -0
- package/dist/runtime/components/Integrations/WorkflowDuplicateDialog.vue +2 -1
- package/dist/runtime/components/Integrations/WorkflowEdgeView.vue +4 -4
- package/dist/runtime/components/Integrations/WorkflowEditor.vue +78 -30
- package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue +17 -6
- package/dist/runtime/components/Integrations/WorkflowNodeView.vue +17 -21
- package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +8 -11
- package/dist/runtime/components/Integrations/WorkflowStateNamespacesDialog.d.vue.ts +13 -0
- package/dist/runtime/components/Integrations/WorkflowStateNamespacesDialog.vue +216 -0
- package/dist/runtime/components/Integrations/WorkflowStateNamespacesDialog.vue.d.ts +13 -0
- package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +6 -7
- package/dist/runtime/components/Integrations/WorkflowVariablesDialog.vue +7 -7
- package/dist/runtime/components/Integrations/WorkflowsTable.vue +10 -48
- package/dist/runtime/components/Integrations/canvasSides.d.ts +48 -0
- package/dist/runtime/components/Integrations/canvasSides.js +22 -0
- package/dist/runtime/composables/useCredentialTypes.js +2 -6
- package/dist/runtime/composables/useIntegrationsAudit.js +2 -2
- package/dist/runtime/composables/useIntegrationsCredentials.js +4 -8
- package/dist/runtime/composables/useIntegrationsDeadLetters.d.ts +4 -0
- package/dist/runtime/composables/useIntegrationsDeadLetters.js +16 -1
- package/dist/runtime/composables/useIntegrationsNodes.js +2 -2
- package/dist/runtime/composables/useIntegrationsRevisions.js +2 -6
- package/dist/runtime/composables/useIntegrationsRuns.d.ts +4 -15
- package/dist/runtime/composables/useIntegrationsRuns.js +49 -34
- package/dist/runtime/composables/useIntegrationsSchedules.js +2 -2
- package/dist/runtime/composables/useIntegrationsSecrets.js +2 -6
- package/dist/runtime/composables/useIntegrationsTemplates.js +7 -5
- package/dist/runtime/composables/useIntegrationsTriggers.js +2 -2
- package/dist/runtime/composables/useIntegrationsWorkflows.js +10 -7
- package/dist/runtime/composables/useNodeConfigResolve.js +2 -2
- package/dist/runtime/composables/useOAuthConnect.d.ts +42 -0
- package/dist/runtime/composables/useOAuthConnect.js +33 -0
- package/dist/runtime/data/expressionEditor.d.ts +17 -5
- package/dist/runtime/data/expressionEditor.js +11 -5
- package/dist/runtime/data/handleId.d.ts +61 -1
- package/dist/runtime/data/handleId.js +8 -1
- package/dist/runtime/data/integrationNodeCatalog.d.ts +25 -0
- package/dist/runtime/data/integrationNodeCatalog.js +8 -0
- package/dist/runtime/data/nodeVersionMove.d.ts +72 -0
- package/dist/runtime/data/nodeVersionMove.js +65 -0
- package/dist/runtime/data/payloadForm.js +3 -2
- package/dist/runtime/i18n/messages/de.json +79 -12
- package/dist/runtime/i18n/messages/en.json +79 -12
- package/dist/runtime/pages/integrations/audit.vue +7 -6
- package/dist/runtime/pages/integrations/credentials.vue +9 -21
- package/dist/runtime/pages/integrations/index.vue +55 -8
- package/dist/runtime/pages/integrations/runs.vue +116 -7
- package/dist/runtime/pages/integrations/secrets.vue +2 -1
- package/dist/runtime/pages/integrations/templates/[slug].vue +2 -1
- package/dist/runtime/pages/integrations/templates/index.vue +18 -15
- package/dist/runtime/pages/integrations/workflows/[id].vue +169 -24
- package/dist/runtime/pages/integrations/workflows/index.vue +23 -4
- package/dist/runtime/types/integrations.d.ts +49 -1
- package/dist/runtime/utils/apiError.d.ts +21 -1
- package/dist/runtime/utils/apiError.js +1 -1
- package/dist/runtime/utils/buildSettle.d.ts +68 -0
- package/dist/runtime/utils/buildSettle.js +29 -0
- package/dist/runtime/utils/clipboard.d.ts +33 -0
- package/dist/runtime/utils/clipboard.js +11 -0
- package/dist/runtime/utils/deepClone.d.ts +41 -0
- package/dist/runtime/utils/deepClone.js +3 -0
- package/dist/runtime/utils/formatDuration.d.ts +44 -0
- package/dist/runtime/utils/formatDuration.js +13 -0
- package/dist/runtime/utils/nodeDisplayName.d.ts +22 -0
- package/dist/runtime/utils/nodeDisplayName.js +6 -0
- package/dist/runtime/utils/notify.d.ts +1 -1
- package/dist/runtime/utils/runEnvelope.d.ts +31 -0
- package/dist/runtime/utils/runEnvelope.js +12 -0
- package/dist/runtime/utils/runFeedQuery.d.ts +135 -0
- package/dist/runtime/utils/runFeedQuery.js +66 -0
- package/dist/runtime/utils/runOutputs.d.ts +33 -0
- package/dist/runtime/utils/runOutputs.js +10 -0
- package/dist/runtime/utils/settingCondition.d.ts +48 -0
- package/dist/runtime/utils/settingCondition.js +84 -0
- package/dist/runtime/utils/stateNamespaceName.d.ts +23 -0
- package/dist/runtime/utils/stateNamespaceName.js +8 -0
- package/dist/runtime/utils/validationFindings.d.ts +26 -3
- package/dist/runtime/utils/validationFindings.js +18 -3
- package/package.json +1 -1
|
@@ -1,16 +1,12 @@
|
|
|
1
1
|
import { ref } from "vue";
|
|
2
2
|
import { useIntegrationsApi } from "./integrationsApi.js";
|
|
3
3
|
import { useIntegrationsT } from "./useIntegrationsT.js";
|
|
4
|
-
import {
|
|
4
|
+
import { errorReason } from "../utils/apiError.js";
|
|
5
5
|
export function useIntegrationsRevisions() {
|
|
6
6
|
const api = useIntegrationsApi();
|
|
7
7
|
const t = useIntegrationsT();
|
|
8
8
|
const loading = ref(false);
|
|
9
9
|
const error = ref(null);
|
|
10
|
-
function captureError(e, fallback) {
|
|
11
|
-
if (e instanceof IntegrationsApiError) return e.message;
|
|
12
|
-
return e?.message ?? fallback;
|
|
13
|
-
}
|
|
14
10
|
async function list(workflowId, page = 1, perPage = 25) {
|
|
15
11
|
loading.value = true;
|
|
16
12
|
error.value = null;
|
|
@@ -18,7 +14,7 @@ export function useIntegrationsRevisions() {
|
|
|
18
14
|
const res = await api.get(`/workflows/${workflowId}/revisions?page=${page}&per_page=${perPage}`);
|
|
19
15
|
return { revisions: res.data ?? [], meta: res.meta };
|
|
20
16
|
} catch (e) {
|
|
21
|
-
error.value =
|
|
17
|
+
error.value = errorReason(e, t("integration.revisions.error.loadFailed"));
|
|
22
18
|
throw e;
|
|
23
19
|
} finally {
|
|
24
20
|
loading.value = false;
|
|
@@ -1,23 +1,11 @@
|
|
|
1
1
|
import type { RunStatus, RunStepRow, WorkflowRun } from '../types/integrations.js';
|
|
2
|
+
import type { RunFeedQuery } from '../utils/runFeedQuery.js';
|
|
3
|
+
export type { RunFeedQuery };
|
|
2
4
|
interface RunSummary {
|
|
3
5
|
total: number;
|
|
4
6
|
in_flight: number;
|
|
5
7
|
by_status: Record<string, number>;
|
|
6
8
|
}
|
|
7
|
-
/**
|
|
8
|
-
* Which page of the tenant's list to read.
|
|
9
|
-
*
|
|
10
|
-
* Only the page: `/runs` also narrows by status, trigger kind, workflow, window
|
|
11
|
-
* and text, and none of that is driven from here yet. The shared DataTable
|
|
12
|
-
* narrows and searches what it holds, and it cannot report a filter selection
|
|
13
|
-
* back to the page under `serverMode` — so a narrowing sent to the platform
|
|
14
|
-
* would have nothing to set it. That is the remaining half of PO-231, and the
|
|
15
|
-
* parameters are waiting on the endpoint for it.
|
|
16
|
-
*/
|
|
17
|
-
export interface RunFeedQuery {
|
|
18
|
-
page: number;
|
|
19
|
-
perPage: number;
|
|
20
|
-
}
|
|
21
9
|
export declare function useIntegrationsRuns(): {
|
|
22
10
|
runs: any;
|
|
23
11
|
loading: any;
|
|
@@ -29,6 +17,8 @@ export declare function useIntegrationsRuns(): {
|
|
|
29
17
|
summary: any;
|
|
30
18
|
hasMore: any;
|
|
31
19
|
query: any;
|
|
20
|
+
setQuery: (patch: Partial<RunFeedQuery>) => Promise<WorkflowRun[]>;
|
|
21
|
+
narrowedTotal: any;
|
|
32
22
|
load: (force?: boolean) => Promise<WorkflowRun[]>;
|
|
33
23
|
loadSummary: () => Promise<RunSummary>;
|
|
34
24
|
loadForWorkflow: (workflowId: number, workflowName: string, opts?: {
|
|
@@ -77,4 +67,3 @@ export declare function useIntegrationsRuns(): {
|
|
|
77
67
|
}>;
|
|
78
68
|
getStackTrace: (workflowId: number | string, runId: number | string) => Promise<string>;
|
|
79
69
|
};
|
|
80
|
-
export {};
|
|
@@ -1,31 +1,17 @@
|
|
|
1
1
|
import { computed } from "vue";
|
|
2
2
|
import { useIntegrationsApi } from "./integrationsApi.js";
|
|
3
3
|
import { useIntegrationsT } from "./useIntegrationsT.js";
|
|
4
|
-
import {
|
|
4
|
+
import { errorReason } from "../utils/apiError.js";
|
|
5
5
|
import { useLoadGate } from "./useLoadGate.js";
|
|
6
|
+
import {
|
|
7
|
+
DEFAULT_RUN_DIRECTION,
|
|
8
|
+
DEFAULT_RUN_SORT,
|
|
9
|
+
mapRunStatus,
|
|
10
|
+
runFeedNarrowingString,
|
|
11
|
+
runFeedQueryString,
|
|
12
|
+
withRunFeedPatch
|
|
13
|
+
} from "../utils/runFeedQuery.js";
|
|
6
14
|
const EMPTY_SUMMARY = { total: 0, in_flight: 0, by_status: {} };
|
|
7
|
-
function mapStatus(status) {
|
|
8
|
-
switch (status) {
|
|
9
|
-
case "completed":
|
|
10
|
-
return "succeeded";
|
|
11
|
-
case "pending":
|
|
12
|
-
return "queued";
|
|
13
|
-
case "terminated":
|
|
14
|
-
return "failed";
|
|
15
|
-
case "timed_out":
|
|
16
|
-
return "failed";
|
|
17
|
-
case "running":
|
|
18
|
-
return "running";
|
|
19
|
-
case "failed":
|
|
20
|
-
return "failed";
|
|
21
|
-
case "cancelled":
|
|
22
|
-
return "cancelled";
|
|
23
|
-
// An unknown/unexpected status must never surface as a stuck `running` —
|
|
24
|
-
// fail closed so a run the backend can't classify is visibly terminal.
|
|
25
|
-
default:
|
|
26
|
-
return "failed";
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
15
|
export function useIntegrationsRuns() {
|
|
30
16
|
const runs = useState("integrations.runs", () => []);
|
|
31
17
|
const loading = useState("integrations.runs.loading", () => false);
|
|
@@ -36,9 +22,20 @@ export function useIntegrationsRuns() {
|
|
|
36
22
|
const hasMore = useState("integrations.runs.hasMore", () => false);
|
|
37
23
|
const query = useState("integrations.runs.query", () => ({
|
|
38
24
|
page: 1,
|
|
39
|
-
perPage:
|
|
25
|
+
perPage: 25,
|
|
26
|
+
status: [],
|
|
27
|
+
triggerType: "",
|
|
28
|
+
search: "",
|
|
29
|
+
sort: DEFAULT_RUN_SORT,
|
|
30
|
+
direction: DEFAULT_RUN_DIRECTION
|
|
40
31
|
}));
|
|
32
|
+
const narrowedTotal = useState("integrations.runs.narrowedTotal", () => 0);
|
|
33
|
+
const summaryWrite = useState("integrations.runs.summaryWrite", () => 0);
|
|
41
34
|
const gate = useLoadGate("integrations.runs", { loading, loaded });
|
|
35
|
+
function claimSummary() {
|
|
36
|
+
const mine = ++summaryWrite.value;
|
|
37
|
+
return () => mine === summaryWrite.value;
|
|
38
|
+
}
|
|
42
39
|
const SYNC_LIMIT = 5;
|
|
43
40
|
const api = useIntegrationsApi();
|
|
44
41
|
const t = useIntegrationsT();
|
|
@@ -49,7 +46,7 @@ export function useIntegrationsRuns() {
|
|
|
49
46
|
id: String(r.id),
|
|
50
47
|
workflowId: r.workflow_id,
|
|
51
48
|
workflowName: r.workflow_name ?? workflowName,
|
|
52
|
-
status:
|
|
49
|
+
status: mapRunStatus(r.status),
|
|
53
50
|
temporalRunId: r.temporal_run_id ?? null,
|
|
54
51
|
triggerId: r.trigger_handle ?? "",
|
|
55
52
|
// The platform reports what started the run. This was hardcoded to
|
|
@@ -72,14 +69,24 @@ export function useIntegrationsRuns() {
|
|
|
72
69
|
if (!await gate.claim(force)) return runs.value;
|
|
73
70
|
error.value = null;
|
|
74
71
|
try {
|
|
75
|
-
const
|
|
72
|
+
const narrowing = runFeedNarrowingString(query.value);
|
|
73
|
+
const counted = claimSummary();
|
|
76
74
|
const [page, counts] = await Promise.all([
|
|
77
|
-
api.get(`/runs
|
|
78
|
-
api.get(
|
|
75
|
+
api.get(`/runs?${runFeedQueryString(query.value)}`),
|
|
76
|
+
api.get(`/runs/summary${narrowing ? `?${narrowing}` : ""}`)
|
|
79
77
|
]);
|
|
80
78
|
runs.value = (page?.data ?? []).map((r) => mapRun(r));
|
|
81
79
|
hasMore.value = page?.meta?.has_more ?? false;
|
|
82
|
-
|
|
80
|
+
narrowedTotal.value = counts?.data?.total ?? 0;
|
|
81
|
+
if (narrowing) {
|
|
82
|
+
const refreshed = claimSummary();
|
|
83
|
+
void api.get("/runs/summary").then((tenantWide) => {
|
|
84
|
+
if (refreshed()) summary.value = tenantWide?.data ?? { ...EMPTY_SUMMARY };
|
|
85
|
+
}).catch(() => {
|
|
86
|
+
});
|
|
87
|
+
} else if (counted()) {
|
|
88
|
+
summary.value = counts?.data ?? { ...EMPTY_SUMMARY };
|
|
89
|
+
}
|
|
83
90
|
if (!reconciling.value) {
|
|
84
91
|
reconciling.value = true;
|
|
85
92
|
try {
|
|
@@ -99,7 +106,7 @@ export function useIntegrationsRuns() {
|
|
|
99
106
|
}
|
|
100
107
|
loaded.value = true;
|
|
101
108
|
} catch (e) {
|
|
102
|
-
error.value = e
|
|
109
|
+
error.value = errorReason(e, t("integration.runs.error.loadFailed"));
|
|
103
110
|
loaded.value = false;
|
|
104
111
|
} finally {
|
|
105
112
|
gate.release();
|
|
@@ -139,11 +146,12 @@ export function useIntegrationsRuns() {
|
|
|
139
146
|
return result;
|
|
140
147
|
}
|
|
141
148
|
async function loadSummary() {
|
|
149
|
+
const current = claimSummary();
|
|
142
150
|
try {
|
|
143
151
|
const res = await api.get("/runs/summary");
|
|
144
|
-
summary.value = res?.data ?? { ...EMPTY_SUMMARY };
|
|
152
|
+
if (current()) summary.value = res?.data ?? { ...EMPTY_SUMMARY };
|
|
145
153
|
} catch (e) {
|
|
146
|
-
error.value = e
|
|
154
|
+
if (current()) error.value = errorReason(e, t("integration.runs.error.loadFailed"));
|
|
147
155
|
}
|
|
148
156
|
return summary.value;
|
|
149
157
|
}
|
|
@@ -161,7 +169,7 @@ export function useIntegrationsRuns() {
|
|
|
161
169
|
latestPerWorkflow.value = (res?.data ?? []).map((r) => mapRun(r));
|
|
162
170
|
latestLoaded.value = true;
|
|
163
171
|
} catch (e) {
|
|
164
|
-
error.value = e
|
|
172
|
+
error.value = errorReason(e, t("integration.runs.error.loadFailed"));
|
|
165
173
|
latestLoaded.value = false;
|
|
166
174
|
} finally {
|
|
167
175
|
latestGate.release();
|
|
@@ -208,8 +216,13 @@ export function useIntegrationsRuns() {
|
|
|
208
216
|
};
|
|
209
217
|
const getStackTrace = (workflowId, runId) => api.get(`/workflows/${workflowId}/runs/${runId}/stack-trace`).then((r) => r.stack_trace);
|
|
210
218
|
const total = computed(() => summary.value.total);
|
|
211
|
-
const failedCount = computed(() => Object.entries(summary.value.by_status).filter(([status]) =>
|
|
219
|
+
const failedCount = computed(() => Object.entries(summary.value.by_status).filter(([status]) => mapRunStatus(status) === "failed").reduce((sum, [, count]) => sum + count, 0));
|
|
212
220
|
const runningCount = computed(() => summary.value.in_flight);
|
|
221
|
+
async function setQuery(patch) {
|
|
222
|
+
const next = withRunFeedPatch(query.value, patch);
|
|
223
|
+
query.value = next;
|
|
224
|
+
return load(true);
|
|
225
|
+
}
|
|
213
226
|
return {
|
|
214
227
|
runs,
|
|
215
228
|
loading,
|
|
@@ -221,6 +234,8 @@ export function useIntegrationsRuns() {
|
|
|
221
234
|
summary,
|
|
222
235
|
hasMore,
|
|
223
236
|
query,
|
|
237
|
+
setQuery,
|
|
238
|
+
narrowedTotal,
|
|
224
239
|
load,
|
|
225
240
|
loadSummary,
|
|
226
241
|
loadForWorkflow,
|
|
@@ -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 { useLoadGate } from "./useLoadGate.js";
|
|
5
|
-
import {
|
|
5
|
+
import { errorReason } from "../utils/apiError.js";
|
|
6
6
|
import { scheduleTimezone } from "../utils/schedule.js";
|
|
7
7
|
export function useIntegrationsSchedules() {
|
|
8
8
|
const api = useIntegrationsApi();
|
|
@@ -70,7 +70,7 @@ export function useIntegrationsSchedules() {
|
|
|
70
70
|
meta.value = res?.meta ?? { total: 0, live_count: 0, timezones: [] };
|
|
71
71
|
loaded.value = true;
|
|
72
72
|
} catch (e) {
|
|
73
|
-
error.value = e
|
|
73
|
+
error.value = errorReason(e, t("integration.schedules.error.loadFailed"));
|
|
74
74
|
loaded.value = false;
|
|
75
75
|
} finally {
|
|
76
76
|
gate.release();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { computed } from "vue";
|
|
2
2
|
import { useIntegrationsApi } from "./integrationsApi.js";
|
|
3
3
|
import { useIntegrationsT } from "./useIntegrationsT.js";
|
|
4
|
-
import {
|
|
4
|
+
import { errorReason } from "../utils/apiError.js";
|
|
5
5
|
import { useLoadGate } from "./useLoadGate.js";
|
|
6
6
|
const KEY_PATTERN = /^[A-Z0-9_]+$/;
|
|
7
7
|
export function useIntegrationsSecrets() {
|
|
@@ -26,10 +26,6 @@ export function useIntegrationsSecrets() {
|
|
|
26
26
|
function toRecord(r) {
|
|
27
27
|
return { key: r.key ?? "", created_at: r.created_at ?? "", updated_at: r.updated_at ?? "" };
|
|
28
28
|
}
|
|
29
|
-
function captureError(e, fallback) {
|
|
30
|
-
if (e instanceof IntegrationsApiError) return e.message;
|
|
31
|
-
return e?.message ?? fallback;
|
|
32
|
-
}
|
|
33
29
|
async function load(force = false) {
|
|
34
30
|
if (!await gate.claim(force)) return secrets.value;
|
|
35
31
|
error.value = null;
|
|
@@ -37,7 +33,7 @@ export function useIntegrationsSecrets() {
|
|
|
37
33
|
secrets.value = normalize(await api.get("/secrets"));
|
|
38
34
|
loaded.value = true;
|
|
39
35
|
} catch (e) {
|
|
40
|
-
error.value =
|
|
36
|
+
error.value = errorReason(e, t("integration.secrets.error.loadFailed"));
|
|
41
37
|
} finally {
|
|
42
38
|
gate.release();
|
|
43
39
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { computed } from "vue";
|
|
2
2
|
import { useIntegrationsApi } from "./integrationsApi.js";
|
|
3
|
-
import {
|
|
3
|
+
import { errorReason } from "../utils/apiError.js";
|
|
4
4
|
import { resolveLocalized } from "../utils/localize.js";
|
|
5
5
|
import { useLoadGate } from "./useLoadGate.js";
|
|
6
6
|
const BLOB_VERSION = "v0-draft";
|
|
@@ -70,6 +70,11 @@ function applyDetail(base, api) {
|
|
|
70
70
|
edges: def.edges ?? [],
|
|
71
71
|
triggerPositions: def.triggerPositions ?? {},
|
|
72
72
|
variables: def.variables,
|
|
73
|
+
// Enumerated key by key like every block above it, so one missing here is
|
|
74
|
+
// one lost in this direction. Harmless today — a template becomes a workflow
|
|
75
|
+
// server-side, so this blob only feeds the preview — but the preview is
|
|
76
|
+
// where an author would look to see what a template remembers.
|
|
77
|
+
state: def.state,
|
|
73
78
|
triggers
|
|
74
79
|
};
|
|
75
80
|
const md = resolveLocalized(api.description ?? "") || base.descriptionMd;
|
|
@@ -100,9 +105,6 @@ export function useIntegrationsTemplates() {
|
|
|
100
105
|
function unwrap(res) {
|
|
101
106
|
return res?.data ?? res;
|
|
102
107
|
}
|
|
103
|
-
function captureError(e, fallback) {
|
|
104
|
-
return e instanceof IntegrationsApiError ? e.message : e?.message ?? fallback;
|
|
105
|
-
}
|
|
106
108
|
async function load(force = false) {
|
|
107
109
|
if (!await gate.claim(force)) return templates.value;
|
|
108
110
|
error.value = null;
|
|
@@ -112,7 +114,7 @@ export function useIntegrationsTemplates() {
|
|
|
112
114
|
rawDetails.value = {};
|
|
113
115
|
loaded.value = true;
|
|
114
116
|
} catch (e) {
|
|
115
|
-
error.value =
|
|
117
|
+
error.value = errorReason(e, t("integration.templates.state.error.fallback"));
|
|
116
118
|
rawList.value = [];
|
|
117
119
|
rawDetails.value = {};
|
|
118
120
|
loaded.value = true;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useIntegrationsApi } from "./integrationsApi.js";
|
|
2
2
|
import { useIntegrationsT } from "./useIntegrationsT.js";
|
|
3
|
-
import {
|
|
3
|
+
import { errorReason } from "../utils/apiError.js";
|
|
4
4
|
import { useLoadGate } from "./useLoadGate.js";
|
|
5
5
|
export function tenantTriggers(workflows, byWorkflow) {
|
|
6
6
|
return workflows.flatMap((wf) => (byWorkflow[wf.id] ?? []).flatMap((t) => {
|
|
@@ -40,7 +40,7 @@ export function useIntegrationsTriggers() {
|
|
|
40
40
|
error.value = workflowsError.value;
|
|
41
41
|
loaded.value = workflowsLoaded.value && !workflowsError.value;
|
|
42
42
|
} catch (e) {
|
|
43
|
-
error.value = e
|
|
43
|
+
error.value = errorReason(e, t("integration.triggers.error.loadFailed"));
|
|
44
44
|
} finally {
|
|
45
45
|
gate.release();
|
|
46
46
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { computed } from "vue";
|
|
2
2
|
import { useIntegrationsApi } from "./integrationsApi.js";
|
|
3
3
|
import { useIntegrationsT } from "./useIntegrationsT.js";
|
|
4
|
-
import {
|
|
4
|
+
import { deepClone } from "../utils/deepClone.js";
|
|
5
|
+
import { errorReason } from "../utils/apiError.js";
|
|
5
6
|
import { useLoadGate } from "./useLoadGate.js";
|
|
6
7
|
const BLOB_VERSION = "v0-draft";
|
|
7
8
|
function toCockpit(api) {
|
|
@@ -19,6 +20,7 @@ function toCockpit(api) {
|
|
|
19
20
|
nodes: apiBlob.nodes ?? [],
|
|
20
21
|
edges: apiBlob.edges ?? [],
|
|
21
22
|
variables: apiBlob.variables ?? void 0,
|
|
23
|
+
state: apiBlob.state ?? void 0,
|
|
22
24
|
triggerPositions: apiBlob.triggerPositions ?? {},
|
|
23
25
|
stickies: apiBlob.stickies ?? [],
|
|
24
26
|
groups: apiBlob.groups ?? [],
|
|
@@ -63,6 +65,11 @@ function toPayload(meta) {
|
|
|
63
65
|
...b.groups?.length ? { groups: b.groups } : {},
|
|
64
66
|
// variables were silently dropped before — the schema always allowed them.
|
|
65
67
|
...b.variables && Object.keys(b.variables).length ? { variables: b.variables } : {},
|
|
68
|
+
// Same shape, and the same trap: both mappers enumerate blob keys, so a
|
|
69
|
+
// block missing from either one is lost in that direction. `state` is the
|
|
70
|
+
// declaration that makes the run grant reachable at all (PO-374) — losing
|
|
71
|
+
// it on save would take the workflow's memory away without a message.
|
|
72
|
+
...b.state?.length ? { state: b.state } : {},
|
|
66
73
|
...b.tags?.length ? { tags: b.tags } : {},
|
|
67
74
|
...b.icon ? { icon: b.icon } : {}
|
|
68
75
|
},
|
|
@@ -89,10 +96,6 @@ export function useIntegrationsWorkflows() {
|
|
|
89
96
|
function unwrap(res) {
|
|
90
97
|
return res?.data ?? res;
|
|
91
98
|
}
|
|
92
|
-
function captureError(e, fallback) {
|
|
93
|
-
if (e instanceof IntegrationsApiError) return e.message;
|
|
94
|
-
return e?.message ?? fallback;
|
|
95
|
-
}
|
|
96
99
|
function remember(api2, into) {
|
|
97
100
|
if (api2.triggers !== void 0) into[Number(api2.id)] = api2.triggers;
|
|
98
101
|
}
|
|
@@ -123,7 +126,7 @@ export function useIntegrationsWorkflows() {
|
|
|
123
126
|
workflows.value = ingestList(list2);
|
|
124
127
|
loaded.value = true;
|
|
125
128
|
} catch (e) {
|
|
126
|
-
error.value =
|
|
129
|
+
error.value = errorReason(e, t("integration.workflows.error.loadFailed"));
|
|
127
130
|
loaded.value = false;
|
|
128
131
|
} finally {
|
|
129
132
|
gate.release();
|
|
@@ -157,7 +160,7 @@ export function useIntegrationsWorkflows() {
|
|
|
157
160
|
async function duplicate(id, name) {
|
|
158
161
|
const source = await get(id);
|
|
159
162
|
if (!source) throw new Error(t("integration.workflows.error.notFound"));
|
|
160
|
-
const blob =
|
|
163
|
+
const blob = deepClone(source.blob);
|
|
161
164
|
const handleMap = /* @__PURE__ */ new Map();
|
|
162
165
|
for (const trigger of blob.triggers ?? []) {
|
|
163
166
|
const fresh = crypto.randomUUID();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { computed, onScopeDispose, ref, toValue, watch } from "vue";
|
|
2
2
|
import { isVersionExecutable } from "../data/integrationNodeCatalog.js";
|
|
3
3
|
import { useIntegrationsApi } from "./integrationsApi.js";
|
|
4
|
-
import { IntegrationsApiError } from "../utils/apiError.js";
|
|
4
|
+
import { IntegrationsApiError, errorReason } from "../utils/apiError.js";
|
|
5
5
|
export function isResolvableValue(v) {
|
|
6
6
|
if (v === void 0 || v === null) return false;
|
|
7
7
|
if (typeof v === "string") {
|
|
@@ -17,7 +17,7 @@ export function isResolvableValue(v) {
|
|
|
17
17
|
function classifyError(err) {
|
|
18
18
|
const status = err instanceof IntegrationsApiError ? err.status : void 0;
|
|
19
19
|
const t = useIntegrationsT();
|
|
20
|
-
const message = err
|
|
20
|
+
const message = errorReason(err, t("integration.nodeConfig.resolve.error.generic"));
|
|
21
21
|
let kind;
|
|
22
22
|
switch (status) {
|
|
23
23
|
case 409:
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How an OAuth connect attempt ended.
|
|
3
|
+
*
|
|
4
|
+
* `completed` is the provider's own answer, relayed by the landing page. `closed` is
|
|
5
|
+
* the operator shutting the window — which says nothing about whether the
|
|
6
|
+
* authorisation went through, so the caller re-reads rather than announcing anything.
|
|
7
|
+
*/
|
|
8
|
+
export type OAuthConnectOutcome = 'completed' | 'closed';
|
|
9
|
+
/**
|
|
10
|
+
* Connecting a credential by OAuth: the popup, and the two ways of finding out it is
|
|
11
|
+
* over.
|
|
12
|
+
*
|
|
13
|
+
* The studio opens the provider's authorisation page in a separate window rather than
|
|
14
|
+
* navigating, so that a half-filled credential form is still there afterwards. Two
|
|
15
|
+
* things can then end the attempt, and both have to be watched, because neither
|
|
16
|
+
* implies the other:
|
|
17
|
+
*
|
|
18
|
+
* - The integrations callback redirects to `/integrations/credentials/oauth-complete`,
|
|
19
|
+
* which posts `integrations-oauth-complete` back to us. This is the success path,
|
|
20
|
+
* and it is the only one that knows the authorisation actually happened.
|
|
21
|
+
* - The operator closes the window instead — cancelling, or completing it somewhere
|
|
22
|
+
* the message could not come back from. Nothing is posted, so the only signal is
|
|
23
|
+
* `popup.closed`, which cannot be subscribed to and has to be polled.
|
|
24
|
+
*
|
|
25
|
+
* **This needs the integrations-side `oauth_return_url` and the provider redirect to
|
|
26
|
+
* be configured**; without them the callback never reaches the landing page and only
|
|
27
|
+
* the close path ever fires. The prerequisite is written out in `oauth-complete.vue`.
|
|
28
|
+
* It lives on the composable rather than at a call site because it is the reason the
|
|
29
|
+
* handshake has two paths at all, and a reader who only opens one of the three callers
|
|
30
|
+
* would otherwise never meet it — which is exactly what happened while this was written
|
|
31
|
+
* out three times and the prerequisite recorded in one of them.
|
|
32
|
+
*
|
|
33
|
+
* `onSettled` is called once, whichever path fires, with what is known. The caller
|
|
34
|
+
* re-reads the credential and decides what to say, because that is where the three
|
|
35
|
+
* genuinely differ: the list refreshes the list, the detail dialog refreshes one
|
|
36
|
+
* credential and tells its parent, and the create dialog says nothing at all because the
|
|
37
|
+
* operator is still looking at the form. Only the waiting is shared.
|
|
38
|
+
*/
|
|
39
|
+
export declare function useOAuthConnect(onSettled: (outcome: OAuthConnectOutcome) => void | Promise<void>): {
|
|
40
|
+
open: (authorizeUrl: string) => void;
|
|
41
|
+
cancel: () => void;
|
|
42
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { onUnmounted } from "vue";
|
|
2
|
+
export function useOAuthConnect(onSettled) {
|
|
3
|
+
let release = null;
|
|
4
|
+
function settle(outcome) {
|
|
5
|
+
if (!release) return;
|
|
6
|
+
release();
|
|
7
|
+
release = null;
|
|
8
|
+
void onSettled(outcome);
|
|
9
|
+
}
|
|
10
|
+
function open(authorizeUrl) {
|
|
11
|
+
release?.();
|
|
12
|
+
const popup = window.open(authorizeUrl, "oauth-connect", "width=600,height=720");
|
|
13
|
+
if (!popup) return;
|
|
14
|
+
const onMessage = (ev) => {
|
|
15
|
+
if (ev.origin !== window.location.origin) return;
|
|
16
|
+
if (ev.data?.type === "integrations-oauth-complete") settle("completed");
|
|
17
|
+
};
|
|
18
|
+
window.addEventListener("message", onMessage);
|
|
19
|
+
const poll = window.setInterval(() => {
|
|
20
|
+
if (popup.closed) settle("closed");
|
|
21
|
+
}, 1e3);
|
|
22
|
+
release = () => {
|
|
23
|
+
window.clearInterval(poll);
|
|
24
|
+
window.removeEventListener("message", onMessage);
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function cancel() {
|
|
28
|
+
release?.();
|
|
29
|
+
release = null;
|
|
30
|
+
}
|
|
31
|
+
onUnmounted(cancel);
|
|
32
|
+
return { open, cancel };
|
|
33
|
+
}
|
|
@@ -12,11 +12,23 @@ export interface InlineNodeJSON {
|
|
|
12
12
|
export declare const EXPR_TOKEN_NODE = "exprToken";
|
|
13
13
|
/** Raw text → inline content: text nodes with one atom pill per token. */
|
|
14
14
|
export declare function textToInlineContent(text: string): InlineNodeJSON[];
|
|
15
|
-
/**
|
|
16
|
-
|
|
15
|
+
/**
|
|
16
|
+
* One paragraph per line, or one paragraph for the whole value.
|
|
17
|
+
*
|
|
18
|
+
* A field the node declared `multiline` keeps its newlines: each line becomes a
|
|
19
|
+
* paragraph, which is the only structure a TipTap doc has for one. Every other
|
|
20
|
+
* field is single-line and stays a single paragraph — the editor swallows Enter
|
|
21
|
+
* there, so nothing can create a second one from inside.
|
|
22
|
+
*/
|
|
23
|
+
type LineMode = {
|
|
24
|
+
multiline?: boolean;
|
|
25
|
+
};
|
|
26
|
+
/** Raw text → a full TipTap doc: one paragraph, or one per line when multiline. */
|
|
27
|
+
export declare function textToDoc(text: string, { multiline }?: LineMode): InlineNodeJSON;
|
|
17
28
|
/** TipTap doc JSON → raw text (pills serialize back to their `${{ … }}` token).
|
|
18
|
-
* Multiple paragraphs
|
|
19
|
-
*
|
|
20
|
-
export declare function docToText(doc: InlineNodeJSON | null | undefined): string;
|
|
29
|
+
* Multiple paragraphs are the lines of a multiline field; in a single-line
|
|
30
|
+
* field they can only have arrived by paste, and collapse to single spaces. */
|
|
31
|
+
export declare function docToText(doc: InlineNodeJSON | null | undefined, { multiline }?: LineMode): string;
|
|
21
32
|
/** True when any TEXT node still carries a complete raw token (needs re-pilling). */
|
|
22
33
|
export declare function hasRawTokenText(doc: InlineNodeJSON | null | undefined): boolean;
|
|
34
|
+
export {};
|
|
@@ -11,11 +11,17 @@ export function textToInlineContent(text) {
|
|
|
11
11
|
if (cursor < text.length) out.push({ type: "text", text: text.slice(cursor) });
|
|
12
12
|
return out;
|
|
13
13
|
}
|
|
14
|
-
export function textToDoc(text) {
|
|
15
|
-
const
|
|
16
|
-
return {
|
|
14
|
+
export function textToDoc(text, { multiline = false } = {}) {
|
|
15
|
+
const lines = multiline ? text.split("\n") : [text];
|
|
16
|
+
return {
|
|
17
|
+
type: "doc",
|
|
18
|
+
content: lines.map((line) => {
|
|
19
|
+
const content = textToInlineContent(line);
|
|
20
|
+
return content.length ? { type: "paragraph", content } : { type: "paragraph" };
|
|
21
|
+
})
|
|
22
|
+
};
|
|
17
23
|
}
|
|
18
|
-
export function docToText(doc) {
|
|
24
|
+
export function docToText(doc, { multiline = false } = {}) {
|
|
19
25
|
if (!doc) return "";
|
|
20
26
|
const inlineText = (p) => {
|
|
21
27
|
let s = "";
|
|
@@ -26,7 +32,7 @@ export function docToText(doc) {
|
|
|
26
32
|
}
|
|
27
33
|
return s;
|
|
28
34
|
};
|
|
29
|
-
return (doc.content ?? []).filter((n) => n.type === "paragraph").map(inlineText).join(" ");
|
|
35
|
+
return (doc.content ?? []).filter((n) => n.type === "paragraph").map(inlineText).join(multiline ? "\n" : " ");
|
|
30
36
|
}
|
|
31
37
|
export function hasRawTokenText(doc) {
|
|
32
38
|
if (!doc) return false;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* The shape of a canvas handle id — writing one, and reading the port name back out.
|
|
3
3
|
*
|
|
4
4
|
* Each node on the canvas renders four connection points per port, one per
|
|
5
5
|
* side, so that an edge can leave and arrive without crossing a card body. The
|
|
@@ -19,5 +19,65 @@
|
|
|
19
19
|
* how to read an id, and because that is what makes it testable: the editor is
|
|
20
20
|
* an SFC, out of reach of the unit suite, and the defect above shipped once
|
|
21
21
|
* already without anything going red.
|
|
22
|
+
*
|
|
23
|
+
* **Both directions live here, and that is PO-258.** The reader was here alone while
|
|
24
|
+
* twelve places across six components assembled an id with a template string and five
|
|
25
|
+
* of them declared the side union again — so the two halves of one contract agreed by
|
|
26
|
+
* spelling the same thing the same way rather than by construction. Nothing was wrong
|
|
27
|
+
* on screen; what it cost is that adding a fifth dot position would have had the
|
|
28
|
+
* writers produce `paid__centre` while the reader, which never heard of it, handed
|
|
29
|
+
* `paid__centre` back as a port name. That is not nullish, so the `?? 'default'` behind
|
|
30
|
+
* every call site cannot catch it, and the editor would have persisted an output no
|
|
31
|
+
* node has.
|
|
32
|
+
*
|
|
33
|
+
* **What happens elsewhere when a fifth position is added, and that is PO-396.** This
|
|
34
|
+
* module owns what a side *is*; what the canvas does with one — where the library draws
|
|
35
|
+
* it, what faces it, where along an edge it sits — lives in
|
|
36
|
+
* `components/Integrations/canvasSides.ts`, which is separate for one reason worth
|
|
37
|
+
* keeping: `Position` is Vue Flow's, and this file has no imports, so its round trip can
|
|
38
|
+
* be proved without the canvas library.
|
|
39
|
+
*
|
|
40
|
+
* Those three answers used to be ternary chains inside the components, each ending in an
|
|
41
|
+
* `else`, and two of them were written more than once: the position map in the node card
|
|
42
|
+
* and the trigger card, and the opposite-side map in the editor and in both read-only
|
|
43
|
+
* canvases — where it was called `opposite` rather than `oppositeSide`, which is why a
|
|
44
|
+
* search by name found one of the three. An `else` absorbs a new name
|
|
45
|
+
* silently — a `centre` dot drawn on the left, with `right` called its opposite — and
|
|
46
|
+
* every one of those is a plausible answer for *some* side, so nothing would look wrong.
|
|
47
|
+
* They are now `Record<Side, …>` maps: adding a name to `SIDES` below stops all three
|
|
48
|
+
* compiling until they say what it means. Still more than one edit, and none of them
|
|
49
|
+
* can be forgotten, which is the better half of the trade.
|
|
22
50
|
*/
|
|
51
|
+
/**
|
|
52
|
+
* The side suffix, anchored at the end. The four sides are a closed set, which
|
|
53
|
+
* is what lets the name in front of them be read whole: a port name is not
|
|
54
|
+
* restricted to a safe alphabet: `portsForNode` builds one with `String(v)` over
|
|
55
|
+
* the config values a person types into a switch node's cases, and a
|
|
56
|
+
* server-resolved dynamic output is named whatever the remote system calls its
|
|
57
|
+
* fields — where a double underscore is a naming convention rather than an
|
|
58
|
+
* accident. So a name may well contain `__` itself, and taking the FIRST one as
|
|
59
|
+
* the separator would hand back a truncated name that no node has a port for.
|
|
60
|
+
* Truncated is worse than empty: it is not nullish, so a caller's
|
|
61
|
+
* `?? 'default'` cannot catch it either.
|
|
62
|
+
*/
|
|
63
|
+
/** Every side a connection dot is rendered on. The closed set the parse anchors on. */
|
|
64
|
+
export declare const SIDES: readonly ["top", "right", "bottom", "left"];
|
|
65
|
+
export type Side = (typeof SIDES)[number];
|
|
23
66
|
export declare function portFromHandleId(handleId: string | null | undefined): string | null;
|
|
67
|
+
/**
|
|
68
|
+
* The id of an output dot: the port's own name, then the side.
|
|
69
|
+
*
|
|
70
|
+
* The name is passed through as it is. A port name is not restricted to a safe
|
|
71
|
+
* alphabet — `portsForNode` builds one with `String(v)` over what a person types into a
|
|
72
|
+
* switch node's cases — and sanitising it here would produce an id the reader cannot
|
|
73
|
+
* turn back into the name the blob stores.
|
|
74
|
+
*/
|
|
75
|
+
export declare function outputHandleId(port: string, side: Side): string;
|
|
76
|
+
/**
|
|
77
|
+
* The id of the single input dot on a given side.
|
|
78
|
+
*
|
|
79
|
+
* The separator sits at the front with no name before it, which is exactly what
|
|
80
|
+
* `portFromHandleId` reads as "no port": a node has one logical input, so there is no
|
|
81
|
+
* name to carry. A group frame's input is this on the top side.
|
|
82
|
+
*/
|
|
83
|
+
export declare function inputHandleId(side: Side): string;
|
|
@@ -1,6 +1,13 @@
|
|
|
1
|
-
const
|
|
1
|
+
export const SIDES = ["top", "right", "bottom", "left"];
|
|
2
|
+
const SIDE_SUFFIX = new RegExp(`__(?:${SIDES.join("|")})$`);
|
|
2
3
|
export function portFromHandleId(handleId) {
|
|
3
4
|
if (!handleId) return null;
|
|
4
5
|
const name = handleId.replace(SIDE_SUFFIX, "");
|
|
5
6
|
return name.startsWith("__") || !name ? null : name;
|
|
6
7
|
}
|
|
8
|
+
export function outputHandleId(port, side) {
|
|
9
|
+
return `${port}__${side}`;
|
|
10
|
+
}
|
|
11
|
+
export function inputHandleId(side) {
|
|
12
|
+
return `__in__${side}`;
|
|
13
|
+
}
|