@revenexx/studio-integrations 3.7.0 → 3.8.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 +21 -0
- package/dist/module.json +1 -1
- package/dist/runtime/components/Integrations/CredentialCreateDialog.vue +3 -1
- package/dist/runtime/components/Integrations/DeadLettersPanel.vue +2 -1
- package/dist/runtime/components/Integrations/NodeConfigDynamicField.vue +37 -17
- package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.d.vue.ts +2 -0
- package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.vue +5 -2
- package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.vue.d.ts +2 -0
- package/dist/runtime/components/Integrations/NodeConfigField.vue +1 -0
- package/dist/runtime/components/Integrations/RunResponseModal.d.vue.ts +6 -0
- package/dist/runtime/components/Integrations/RunResponseModal.vue +36 -5
- package/dist/runtime/components/Integrations/RunResponseModal.vue.d.ts +6 -0
- package/dist/runtime/components/Integrations/Sidebar.vue +22 -5
- package/dist/runtime/components/Integrations/TemplateFlowPreview.vue +3 -3
- package/dist/runtime/components/Integrations/WorkflowEditor.vue +32 -27
- package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +1 -1
- package/dist/runtime/components/Integrations/WorkflowsTable.vue +9 -14
- package/dist/runtime/composables/useIntegrationsSummary.d.ts +29 -0
- package/dist/runtime/composables/useIntegrationsSummary.js +37 -0
- package/dist/runtime/composables/useNodeConfigResolve.d.ts +25 -2
- package/dist/runtime/composables/useNodeConfigResolve.js +11 -2
- package/dist/runtime/composables/useOAuthConnect.d.ts +5 -0
- package/dist/runtime/composables/useOAuthConnect.js +3 -1
- package/dist/runtime/i18n/messages/de.json +4 -3
- package/dist/runtime/i18n/messages/en.json +4 -3
- package/dist/runtime/pages/integrations/runs.vue +2 -1
- package/dist/runtime/pages/integrations/workflows/[id].vue +39 -2
- package/dist/runtime/utils/canvasFit.d.ts +59 -0
- package/dist/runtime/utils/canvasFit.js +78 -0
- package/dist/runtime/utils/executionMode.d.ts +18 -0
- package/dist/runtime/utils/executionMode.js +17 -0
- package/dist/runtime/utils/optionSearch.d.ts +24 -0
- package/dist/runtime/utils/optionSearch.js +31 -0
- package/dist/runtime/utils/runFailure.d.ts +37 -0
- package/dist/runtime/utils/runFailure.js +16 -0
- package/package.json +1 -1
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How much the tenant has, counted over the whole tenant (`GET /summary`) — the
|
|
3
|
+
* figures the navigation shows beside its entries (PO-617).
|
|
4
|
+
*
|
|
5
|
+
* Kept apart from the pages' own state on purpose. The badges used to be the
|
|
6
|
+
* length of whatever the last page had loaded: the editor puts the one workflow
|
|
7
|
+
* it opened into the workflows list, so a fresh visit to a workflow read "1"
|
|
8
|
+
* beside Workflows, and a page nobody had opened yet read nothing at all. These
|
|
9
|
+
* counts are asked for on their own and no page writes to them, so opening a
|
|
10
|
+
* page cannot change what a badge says.
|
|
11
|
+
*
|
|
12
|
+
* Each count means what its page counts: `runsInFlight` is the runs summary's
|
|
13
|
+
* `in_flight`, `schedulesActive` the schedules list's `live_count`.
|
|
14
|
+
*
|
|
15
|
+
* Exported through the module's auto-imports, so the cockpit's own sidebar can
|
|
16
|
+
* read the same figures (IN-85).
|
|
17
|
+
*/
|
|
18
|
+
export interface IntegrationsSummary {
|
|
19
|
+
workflows: number;
|
|
20
|
+
runsInFlight: number;
|
|
21
|
+
schedulesActive: number;
|
|
22
|
+
credentials: number;
|
|
23
|
+
secrets: number;
|
|
24
|
+
}
|
|
25
|
+
export declare function toIntegrationsSummary(res: unknown): IntegrationsSummary | null;
|
|
26
|
+
export declare function useIntegrationsSummary(): {
|
|
27
|
+
summary: any;
|
|
28
|
+
load: () => Promise<IntegrationsSummary | null>;
|
|
29
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { useIntegrationsApi } from "./integrationsApi.js";
|
|
2
|
+
function count(value) {
|
|
3
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
|
|
4
|
+
}
|
|
5
|
+
export function toIntegrationsSummary(res) {
|
|
6
|
+
const data = res?.data;
|
|
7
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) return null;
|
|
8
|
+
const d = data;
|
|
9
|
+
return {
|
|
10
|
+
workflows: count(d.workflows),
|
|
11
|
+
runsInFlight: count(d.runs_in_flight),
|
|
12
|
+
schedulesActive: count(d.schedules_active),
|
|
13
|
+
credentials: count(d.credentials),
|
|
14
|
+
secrets: count(d.secrets)
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export function useIntegrationsSummary() {
|
|
18
|
+
const api = useIntegrationsApi();
|
|
19
|
+
const summary = useState("integrations.summary", () => null);
|
|
20
|
+
const turn = useState("integrations.summary.turn", () => 0);
|
|
21
|
+
const forTenant = useState("integrations.summary.tenant", () => "");
|
|
22
|
+
async function load() {
|
|
23
|
+
const mine = ++turn.value;
|
|
24
|
+
const tenant = api.tenant.value;
|
|
25
|
+
if (tenant !== forTenant.value) {
|
|
26
|
+
summary.value = null;
|
|
27
|
+
forTenant.value = tenant;
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
const res = await api.get("/summary");
|
|
31
|
+
if (mine === turn.value) summary.value = toIntegrationsSummary(res);
|
|
32
|
+
} catch {
|
|
33
|
+
}
|
|
34
|
+
return summary.value;
|
|
35
|
+
}
|
|
36
|
+
return { summary, load };
|
|
37
|
+
}
|
|
@@ -8,8 +8,11 @@
|
|
|
8
8
|
* body `{ target, config, locale? }`. `target` is a config field key
|
|
9
9
|
* (→ `{ options }`), `'*'` (→ `{ fields }`) or `'outputs'` (→ `{ outputs }`).
|
|
10
10
|
*
|
|
11
|
-
* `dependsOn` given (options / dynamic-schema): those keys
|
|
12
|
-
*
|
|
11
|
+
* `dependsOn` given (options / dynamic-schema): those keys form the cache-key
|
|
12
|
+
* subset sent as `config`, and — except the ones named in `optional` — gate the
|
|
13
|
+
* query (must be set as literals). An optional setting the node reads when it is
|
|
14
|
+
* there, a credential above all, is sent along without holding the list back
|
|
15
|
+
* (IN-80).
|
|
13
16
|
* `dependsOn` omitted (outputs): no gate, and the whole `config` keys the query
|
|
14
17
|
* and is sent — so any config edit re-resolves.
|
|
15
18
|
*/
|
|
@@ -22,12 +25,25 @@ import type { NodeConfigResolveError, NodeConfigResolveResult } from '../types/i
|
|
|
22
25
|
* dependency still holding an expression can't drive resolution yet.
|
|
23
26
|
*/
|
|
24
27
|
export declare function isResolvableValue(v: unknown): boolean;
|
|
28
|
+
/**
|
|
29
|
+
* The settings of `dependsOn` that may stay empty: each one a sibling field the
|
|
30
|
+
* node declares and does not require. They are sent with the question but do
|
|
31
|
+
* not hold it back — a WSDL behind a login is read with the credential when one
|
|
32
|
+
* is chosen, and a public one is read without (IN-80). A key no sibling declares
|
|
33
|
+
* keeps gating, as before: nothing says it may be left out.
|
|
34
|
+
*/
|
|
35
|
+
export declare function optionalDependencies(dependsOn: string[] | undefined, siblings: Array<{
|
|
36
|
+
key: string;
|
|
37
|
+
required?: boolean;
|
|
38
|
+
}> | undefined): string[];
|
|
25
39
|
export declare function useNodeConfigResolve(opts: {
|
|
26
40
|
slug: MaybeRefOrGetter<string>;
|
|
27
41
|
version: MaybeRefOrGetter<string>;
|
|
28
42
|
target: MaybeRefOrGetter<string>;
|
|
29
43
|
config: MaybeRefOrGetter<Record<string, unknown>>;
|
|
30
44
|
dependsOn?: MaybeRefOrGetter<string[] | undefined>;
|
|
45
|
+
/** Keys of `dependsOn` that are sent but do not gate: the settings that may stay empty. */
|
|
46
|
+
optional?: MaybeRefOrGetter<string[] | undefined>;
|
|
31
47
|
enabled?: MaybeRefOrGetter<boolean>;
|
|
32
48
|
/** Type-to-search term, forwarded to loadOptions (PE-334). Keys the query. */
|
|
33
49
|
search?: MaybeRefOrGetter<string | undefined>;
|
|
@@ -35,6 +51,13 @@ export declare function useNodeConfigResolve(opts: {
|
|
|
35
51
|
data: Ref<NodeConfigResolveResult | null>;
|
|
36
52
|
error: Ref<NodeConfigResolveError | null>;
|
|
37
53
|
isFetching: Ref<boolean>;
|
|
54
|
+
/**
|
|
55
|
+
* The answer on screen is not yet the answer to what was last asked: a request
|
|
56
|
+
* is out, or a change (a typed term above all) is still inside the debounce.
|
|
57
|
+
* `isFetching` alone is false for those 300 ms, so a list narrowed locally
|
|
58
|
+
* would say "nothing matches" before the platform had even been asked.
|
|
59
|
+
*/
|
|
60
|
+
isPending: Ref<boolean>;
|
|
38
61
|
hasData: Ref<boolean>;
|
|
39
62
|
gated: Ref<boolean>;
|
|
40
63
|
unsatisfied: Ref<string[]>;
|
|
@@ -41,6 +41,13 @@ function classifyError(err) {
|
|
|
41
41
|
function isAbortError(err) {
|
|
42
42
|
return err instanceof DOMException ? err.name === "AbortError" : err?.name === "AbortError";
|
|
43
43
|
}
|
|
44
|
+
export function optionalDependencies(dependsOn, siblings) {
|
|
45
|
+
if (!dependsOn?.length || !siblings?.length) return [];
|
|
46
|
+
return dependsOn.filter((key) => {
|
|
47
|
+
const sibling = siblings.find((f) => f.key === key);
|
|
48
|
+
return sibling !== void 0 && sibling.required !== true;
|
|
49
|
+
});
|
|
50
|
+
}
|
|
44
51
|
const DEBOUNCE_MS = 300;
|
|
45
52
|
const STALE_MS = 3e4;
|
|
46
53
|
export function useNodeConfigResolve(opts) {
|
|
@@ -51,7 +58,8 @@ export function useNodeConfigResolve(opts) {
|
|
|
51
58
|
const hasData = ref(false);
|
|
52
59
|
const unsatisfied = computed(() => {
|
|
53
60
|
const cfg = toValue(opts.config);
|
|
54
|
-
const
|
|
61
|
+
const optional = new Set(toValue(opts.optional) ?? []);
|
|
62
|
+
const gateKeys = (toValue(opts.dependsOn) ?? []).filter((k) => !optional.has(k));
|
|
55
63
|
return gateKeys.filter((k) => !isResolvableValue(cfg[k]));
|
|
56
64
|
});
|
|
57
65
|
const gated = computed(() => unsatisfied.value.length > 0);
|
|
@@ -77,6 +85,7 @@ export function useNodeConfigResolve(opts) {
|
|
|
77
85
|
debounceTimer = null;
|
|
78
86
|
}, DEBOUNCE_MS);
|
|
79
87
|
});
|
|
88
|
+
const isPending = computed(() => isFetching.value || isEnabled.value && subsetKey.value !== debouncedKey.value);
|
|
80
89
|
let controller = null;
|
|
81
90
|
let lastFetchedKey = null;
|
|
82
91
|
let lastFetchedAt = 0;
|
|
@@ -147,5 +156,5 @@ export function useNodeConfigResolve(opts) {
|
|
|
147
156
|
if (debounceTimer) clearTimeout(debounceTimer);
|
|
148
157
|
if (controller) controller.abort();
|
|
149
158
|
});
|
|
150
|
-
return { data, error, isFetching, hasData, gated, unsatisfied, isEnabled, refetch };
|
|
159
|
+
return { data, error, isFetching, isPending, hasData, gated, unsatisfied, isEnabled, refetch };
|
|
151
160
|
}
|
|
@@ -35,6 +35,11 @@ export type OAuthConnectOutcome = 'completed' | 'closed';
|
|
|
35
35
|
* genuinely differ: the list refreshes the list, the detail dialog refreshes one
|
|
36
36
|
* credential and tells its parent, and the create dialog says nothing at all because the
|
|
37
37
|
* operator is still looking at the form. Only the waiting is shared.
|
|
38
|
+
*
|
|
39
|
+
* **`open` throws when the browser refuses the window** (a popup blocker, a hardened
|
|
40
|
+
* profile). The error's message is already the operator-facing sentence — what
|
|
41
|
+
* happened and what to do — so every caller's existing `catch` that toasts
|
|
42
|
+
* `errorReason(e, …)` shows it as it stands (PO-399).
|
|
38
43
|
*/
|
|
39
44
|
export declare function useOAuthConnect(onSettled: (outcome: OAuthConnectOutcome) => void | Promise<void>): {
|
|
40
45
|
open: (authorizeUrl: string) => void;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { onUnmounted } from "vue";
|
|
2
|
+
import { useIntegrationsT } from "./useIntegrationsT.js";
|
|
2
3
|
export function useOAuthConnect(onSettled) {
|
|
4
|
+
const t = useIntegrationsT();
|
|
3
5
|
let release = null;
|
|
4
6
|
function settle(outcome) {
|
|
5
7
|
if (!release) return;
|
|
@@ -10,7 +12,7 @@ export function useOAuthConnect(onSettled) {
|
|
|
10
12
|
function open(authorizeUrl) {
|
|
11
13
|
release?.();
|
|
12
14
|
const popup = window.open(authorizeUrl, "oauth-connect", "width=600,height=720");
|
|
13
|
-
if (!popup)
|
|
15
|
+
if (!popup) throw new Error(t("integration.credentials.error.popupBlocked"));
|
|
14
16
|
const onMessage = (ev) => {
|
|
15
17
|
if (ev.origin !== window.location.origin) return;
|
|
16
18
|
if (ev.data?.type === "integrations-oauth-complete") settle("completed");
|
|
@@ -203,6 +203,7 @@
|
|
|
203
203
|
"integration.credentials.error.loadTypes": "Zugangsdatentypen konnten nicht geladen werden.",
|
|
204
204
|
"integration.credentials.error.noAuthorizeUrl": "Keine Autorisierungs-URL erhalten.",
|
|
205
205
|
"integration.credentials.error.oauthConnect": "OAuth-Verbindung konnte nicht gestartet werden.",
|
|
206
|
+
"integration.credentials.error.popupBlocked": "Der Browser hat das Fenster für die Anmeldung beim Anbieter blockiert. Erlauben Sie Pop-ups für diese Seite und versuchen Sie es erneut.",
|
|
206
207
|
"integration.credentials.error.save": "Zugangsdaten konnten nicht gespeichert werden.",
|
|
207
208
|
"integration.credentials.error.testFailed": "Verbindungstest fehlgeschlagen.",
|
|
208
209
|
"integration.credentials.list.action.connect": "Verbinden",
|
|
@@ -1031,6 +1032,9 @@
|
|
|
1031
1032
|
"integration.runs.response.async.startedWithId": "Run #{id} wurde gestartet.",
|
|
1032
1033
|
"integration.runs.response.description.async": "Im Hintergrund gestartet",
|
|
1033
1034
|
"integration.runs.response.description.sync": "Synchrones Run-Ergebnis",
|
|
1035
|
+
"integration.runs.response.failure.run": "Run ist fehlgeschlagen.",
|
|
1036
|
+
"integration.runs.response.failure.runWithId": "Run #{id} ist fehlgeschlagen.",
|
|
1037
|
+
"integration.runs.response.failure.step": "Fehlgeschlagen bei Schritt „{step}“.",
|
|
1034
1038
|
"integration.runs.response.following": "Warten, bis der Run fertig ist…",
|
|
1035
1039
|
"integration.runs.response.loading.async": "Wird gestartet…",
|
|
1036
1040
|
"integration.runs.response.loading.sync": "Wird ausgeführt…",
|
|
@@ -1415,9 +1419,6 @@
|
|
|
1415
1419
|
"integration.workflows.table.menu.action.pause": "Pausieren",
|
|
1416
1420
|
"integration.workflows.table.menu.actions.aria": "Aktionen für {name}",
|
|
1417
1421
|
"integration.workflows.table.menu.actions.label": "Aktionen",
|
|
1418
|
-
"integration.workflows.table.mode.async": "async",
|
|
1419
|
-
"integration.workflows.table.mode.caller": "Aufrufer",
|
|
1420
|
-
"integration.workflows.table.mode.sync": "sync",
|
|
1421
1422
|
"integration.workflows.table.search.placeholder": "Workflows durchsuchen…",
|
|
1422
1423
|
"integration.workflows.table.state.empty": "Noch keine Workflows.",
|
|
1423
1424
|
"integration.workflows.table.status.build.building": "Wird erstellt…",
|
|
@@ -203,6 +203,7 @@
|
|
|
203
203
|
"integration.credentials.error.loadTypes": "Could not load credential types.",
|
|
204
204
|
"integration.credentials.error.noAuthorizeUrl": "No authorize URL returned.",
|
|
205
205
|
"integration.credentials.error.oauthConnect": "Could not start OAuth connect.",
|
|
206
|
+
"integration.credentials.error.popupBlocked": "The browser blocked the window for signing in at the provider. Allow pop-ups for this site, then try again.",
|
|
206
207
|
"integration.credentials.error.save": "Could not save credential.",
|
|
207
208
|
"integration.credentials.error.testFailed": "Connection test failed.",
|
|
208
209
|
"integration.credentials.list.action.connect": "Connect",
|
|
@@ -1031,6 +1032,9 @@
|
|
|
1031
1032
|
"integration.runs.response.async.startedWithId": "Run #{id} started.",
|
|
1032
1033
|
"integration.runs.response.description.async": "Started in the background",
|
|
1033
1034
|
"integration.runs.response.description.sync": "Synchronous run result",
|
|
1035
|
+
"integration.runs.response.failure.run": "Run failed.",
|
|
1036
|
+
"integration.runs.response.failure.runWithId": "Run #{id} failed.",
|
|
1037
|
+
"integration.runs.response.failure.step": "Failed at step “{step}”.",
|
|
1034
1038
|
"integration.runs.response.following": "Waiting for the run to finish…",
|
|
1035
1039
|
"integration.runs.response.loading.async": "Starting…",
|
|
1036
1040
|
"integration.runs.response.loading.sync": "Running…",
|
|
@@ -1415,9 +1419,6 @@
|
|
|
1415
1419
|
"integration.workflows.table.menu.action.pause": "Pause",
|
|
1416
1420
|
"integration.workflows.table.menu.actions.aria": "Actions for {name}",
|
|
1417
1421
|
"integration.workflows.table.menu.actions.label": "Actions",
|
|
1418
|
-
"integration.workflows.table.mode.async": "async",
|
|
1419
|
-
"integration.workflows.table.mode.caller": "caller",
|
|
1420
|
-
"integration.workflows.table.mode.sync": "sync",
|
|
1421
1422
|
"integration.workflows.table.search.placeholder": "Search workflows…",
|
|
1422
1423
|
"integration.workflows.table.state.empty": "No workflows yet.",
|
|
1423
1424
|
"integration.workflows.table.status.build.building": "Building…",
|
|
@@ -8,6 +8,7 @@ import { createPollBudget, SAFETY_POLL_MS } from "../../utils/pollBudget";
|
|
|
8
8
|
import { deadLetterStatusClass, deadLetterStatusLabel } from "../../utils/statusStyle";
|
|
9
9
|
import { errorReason } from "../../utils/apiError";
|
|
10
10
|
import { formatDateTime } from "../../utils/formatDateTime";
|
|
11
|
+
import { triggerTypeLabel } from "../../data/integrationNodeCatalog";
|
|
11
12
|
const t = useIntegrationsT();
|
|
12
13
|
useHead({ title: () => t("integration.runs.list.head.title") });
|
|
13
14
|
const route = useRoute();
|
|
@@ -225,7 +226,7 @@ function workflowNameOf(dl) {
|
|
|
225
226
|
<div class="text-sm break-words">{{ dl.reason || t("integration.deadLetters.reason.fallback") }}</div>
|
|
226
227
|
<div class="mt-0.5 flex flex-wrap items-center gap-x-3 gap-y-0.5 text-xs text-muted-foreground font-mono">
|
|
227
228
|
<span>#{{ dl.id }}</span>
|
|
228
|
-
<span v-if="dl.source">{{ dl.source }}</span>
|
|
229
|
+
<span v-if="dl.source">{{ triggerTypeLabel(dl.source) }}</span>
|
|
229
230
|
<span v-if="dl.trigger_handle">↳ {{ dl.trigger_handle }}</span>
|
|
230
231
|
<span>{{ formatDateTime(dl.created_at) }}</span>
|
|
231
232
|
</div>
|
|
@@ -42,6 +42,7 @@ import { createPollBudget, SAFETY_POLL_MS } from "../../../utils/pollBudget";
|
|
|
42
42
|
import { BUILD_SETTLE_POLL_MS, BUILD_SETTLE_TIMEOUT_MS, createBuildSettle, isBuildUnsettled } from "../../../utils/buildSettle";
|
|
43
43
|
import { splitRunOutputs } from "../../../utils/runOutputs";
|
|
44
44
|
import { isTerminalRun, unwrapRun } from "../../../utils/runEnvelope";
|
|
45
|
+
import { isFailedRun, reasonText, runFailureFromSteps } from "../../../utils/runFailure";
|
|
45
46
|
import { findingsSummary, groupFindingsByNode, nodeDisplayLabel } from "../../../utils/validationFindings";
|
|
46
47
|
import { payloadFieldsFromBlob } from "../../../data/payloadForm";
|
|
47
48
|
import { eventTopicOf } from "../../../data/eventTrigger";
|
|
@@ -49,6 +50,7 @@ import { isShapeIssue, saveRefusalLines } from "../../../data/saveRefusal";
|
|
|
49
50
|
import WorkflowIconView from "../../../components/Integrations/WorkflowIconView.vue";
|
|
50
51
|
import { DEFAULT_RUN_DIRECTION, DEFAULT_RUN_SORT } from "../../../utils/runFeedQuery";
|
|
51
52
|
import { keepOpenBeside } from "../../../utils/dialog";
|
|
53
|
+
import { executionModeLabel } from "../../../utils/executionMode";
|
|
52
54
|
const t = useIntegrationsT();
|
|
53
55
|
const route = useRoute();
|
|
54
56
|
const router = useRouter();
|
|
@@ -323,7 +325,7 @@ async function onRevisionRestored() {
|
|
|
323
325
|
draft.value = null;
|
|
324
326
|
settleBuildStatus();
|
|
325
327
|
}
|
|
326
|
-
const { trigger: triggerRun, loadForWorkflow, fetchRun, getRun, retryRun } = useIntegrationsRuns();
|
|
328
|
+
const { trigger: triggerRun, loadForWorkflow, fetchRun, getRun, getSteps, retryRun } = useIntegrationsRuns();
|
|
327
329
|
const running = ref(false);
|
|
328
330
|
const waitingMode = ref(null);
|
|
329
331
|
const payloadText = ref("{}");
|
|
@@ -338,6 +340,35 @@ const followingRun = ref(false);
|
|
|
338
340
|
const runModalMode = ref("sync");
|
|
339
341
|
const runRefused = ref(false);
|
|
340
342
|
const runModalRunId = ref(null);
|
|
343
|
+
const runFailure = ref(null);
|
|
344
|
+
let runFailureFor = null;
|
|
345
|
+
const RUN_FAILURE_REREAD_MS = 1500;
|
|
346
|
+
async function loadRunFailure(runId, fallbackCause) {
|
|
347
|
+
if (!workflow.value || runFailureFor === runId) return;
|
|
348
|
+
runFailureFor = runId;
|
|
349
|
+
runFailure.value = { step: null, cause: fallbackCause };
|
|
350
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
351
|
+
if (attempt > 0) await new Promise((resolve) => setTimeout(resolve, RUN_FAILURE_REREAD_MS));
|
|
352
|
+
if (runFailureFor !== runId || !workflow.value) return;
|
|
353
|
+
try {
|
|
354
|
+
const res = await getSteps(workflow.value.id, runId, false);
|
|
355
|
+
if (runFailureFor !== runId) return;
|
|
356
|
+
const found = runFailureFromSteps(
|
|
357
|
+
res?.steps,
|
|
358
|
+
shownBlob.value?.nodes ?? [],
|
|
359
|
+
fallbackCause
|
|
360
|
+
);
|
|
361
|
+
runFailure.value = found;
|
|
362
|
+
if (found.step) return;
|
|
363
|
+
} catch {
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
watch(runObject, (run2) => {
|
|
368
|
+
const id2 = extractRunId(run2);
|
|
369
|
+
if (!id2 || !isTerminalRun(run2) || !isFailedRun(run2)) return;
|
|
370
|
+
void loadRunFailure(id2, reasonText(run2?.error));
|
|
371
|
+
});
|
|
341
372
|
function extractRunId(res) {
|
|
342
373
|
if (res && typeof res === "object" && "id" in res) {
|
|
343
374
|
const id2 = res.id;
|
|
@@ -421,6 +452,8 @@ async function run(mode) {
|
|
|
421
452
|
syncResponse.value = null;
|
|
422
453
|
syncError.value = null;
|
|
423
454
|
runRefused.value = false;
|
|
455
|
+
runFailure.value = null;
|
|
456
|
+
runFailureFor = null;
|
|
424
457
|
if (mode === "sync") {
|
|
425
458
|
try {
|
|
426
459
|
syncResponse.value = await triggerRun(workflow.value.id, payload, "sync");
|
|
@@ -429,6 +462,9 @@ async function run(mode) {
|
|
|
429
462
|
} catch (e) {
|
|
430
463
|
await reportRunFailure(e, syncErrorMessage);
|
|
431
464
|
runModalRunId.value = bodyRunId(e);
|
|
465
|
+
if (runModalRunId.value && e instanceof IntegrationsApiError && e.status === 422) {
|
|
466
|
+
void loadRunFailure(runModalRunId.value, reasonText(e.body?.error));
|
|
467
|
+
}
|
|
432
468
|
} finally {
|
|
433
469
|
syncLoading.value = false;
|
|
434
470
|
void loadRuns(1);
|
|
@@ -755,7 +791,7 @@ const EXECUTION_MODES = computed(() => [
|
|
|
755
791
|
{ value: "caller_decides", label: t("integration.editor.executionMode.callerDecides.label"), hint: t("integration.editor.executionMode.callerDecides.hint") }
|
|
756
792
|
]);
|
|
757
793
|
function modeLabel(m) {
|
|
758
|
-
return
|
|
794
|
+
return executionModeLabel(m, executionModeLabel("async_only"));
|
|
759
795
|
}
|
|
760
796
|
const tagSuggestions = computed(() => {
|
|
761
797
|
const all = /* @__PURE__ */ new Set();
|
|
@@ -1318,6 +1354,7 @@ async function confirmDeactivate() {
|
|
|
1318
1354
|
:error="syncError"
|
|
1319
1355
|
:run-id="runModalRunId"
|
|
1320
1356
|
:refused="runRefused"
|
|
1357
|
+
:failure="runFailure"
|
|
1321
1358
|
@update:open="(v) => syncOpen = v"
|
|
1322
1359
|
@view-run="viewRunFromModal"
|
|
1323
1360
|
/>
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where on the canvas a fit may put the drawing, given what floats over it.
|
|
3
|
+
*
|
|
4
|
+
* The Map, *Add node* and the control pane float **over** the canvas rather than
|
|
5
|
+
* beside it, and a plain `fitView` centres the drawing in the whole canvas — so a
|
|
6
|
+
* card can land under a panel, where it takes no pointer (PO-660). This works out
|
|
7
|
+
* the part of the canvas no shown panel covers and turns it into the per-side
|
|
8
|
+
* padding `fitView` takes, so the canvas library still does the fit and the
|
|
9
|
+
* animation; it is only told which rectangle to fit into.
|
|
10
|
+
*
|
|
11
|
+
* Pure on purpose: everything is a rectangle in screen pixels, measured by the
|
|
12
|
+
* caller, so the geometry is provable without a browser (`canvasFit.test.ts`).
|
|
13
|
+
*/
|
|
14
|
+
/** A rectangle in screen pixels, edges rather than a size. */
|
|
15
|
+
export interface Box {
|
|
16
|
+
left: number;
|
|
17
|
+
top: number;
|
|
18
|
+
right: number;
|
|
19
|
+
bottom: number;
|
|
20
|
+
}
|
|
21
|
+
/** The drawing's extent in the workflow's own coordinates. */
|
|
22
|
+
export interface DrawingSize {
|
|
23
|
+
width: number;
|
|
24
|
+
height: number;
|
|
25
|
+
}
|
|
26
|
+
/** The per-side padding `fitView` accepts, as pixel strings. */
|
|
27
|
+
export interface FitPadding {
|
|
28
|
+
top: `${number}px`;
|
|
29
|
+
right: `${number}px`;
|
|
30
|
+
bottom: `${number}px`;
|
|
31
|
+
left: `${number}px`;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Every maximal rectangle of `canvas` that none of `panels` covers.
|
|
35
|
+
*
|
|
36
|
+
* Each panel splits each free rectangle it overlaps into what lies left, right,
|
|
37
|
+
* above and below it; a rectangle another one contains is dropped. A panel
|
|
38
|
+
* outside the canvas, or one with no area, changes nothing.
|
|
39
|
+
*/
|
|
40
|
+
export declare function freeRectangles(canvas: Box, panels: Box[]): Box[];
|
|
41
|
+
/**
|
|
42
|
+
* The free rectangle the drawing is fitted into: of those that show it nearly as
|
|
43
|
+
* large as the best one does (`CENTRAL_SHARE`), the one whose centre lies nearest
|
|
44
|
+
* the canvas's centre — so the drawing sits in the middle of the canvas where the
|
|
45
|
+
* panels allow it, rather than tucked into a strip beside one of them.
|
|
46
|
+
*
|
|
47
|
+
* A rectangle narrower or lower than `MIN_SIDE` is passed over while a larger
|
|
48
|
+
* one exists. When the panels leave nothing larger, the drawing still goes into
|
|
49
|
+
* the best of what is left and is shown smaller — the answer the ticket chose
|
|
50
|
+
* over collapsing a panel the author opened. Only when no free room exists at
|
|
51
|
+
* all is the whole canvas used.
|
|
52
|
+
*/
|
|
53
|
+
export declare function fitArea(canvas: Box, panels: Box[], drawing: DrawingSize, minZoom: number, maxZoom: number): Box;
|
|
54
|
+
/**
|
|
55
|
+
* The padding that makes `fitView` fit into `area` rather than into `canvas`:
|
|
56
|
+
* the distance from each canvas edge to the area's edge, plus the editor's usual
|
|
57
|
+
* margin inside the area.
|
|
58
|
+
*/
|
|
59
|
+
export declare function paddingFor(canvas: Box, area: Box): FitPadding;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
const PADDING = 0.2;
|
|
2
|
+
const MIN_SIDE = 160;
|
|
3
|
+
function width(b) {
|
|
4
|
+
return b.right - b.left;
|
|
5
|
+
}
|
|
6
|
+
function height(b) {
|
|
7
|
+
return b.bottom - b.top;
|
|
8
|
+
}
|
|
9
|
+
function overlaps(a, b) {
|
|
10
|
+
return a.left < b.right && b.left < a.right && a.top < b.bottom && b.top < a.bottom;
|
|
11
|
+
}
|
|
12
|
+
function contains(outer, inner) {
|
|
13
|
+
return outer.left <= inner.left && outer.top <= inner.top && outer.right >= inner.right && outer.bottom >= inner.bottom;
|
|
14
|
+
}
|
|
15
|
+
function sidePadding(length) {
|
|
16
|
+
return Math.floor((length - length / (1 + PADDING)) * 0.5);
|
|
17
|
+
}
|
|
18
|
+
export function freeRectangles(canvas, panels) {
|
|
19
|
+
let free = [canvas];
|
|
20
|
+
for (const raw of panels) {
|
|
21
|
+
const panel = {
|
|
22
|
+
left: Math.max(raw.left, canvas.left),
|
|
23
|
+
top: Math.max(raw.top, canvas.top),
|
|
24
|
+
right: Math.min(raw.right, canvas.right),
|
|
25
|
+
bottom: Math.min(raw.bottom, canvas.bottom)
|
|
26
|
+
};
|
|
27
|
+
if (width(panel) <= 0 || height(panel) <= 0) continue;
|
|
28
|
+
const next = [];
|
|
29
|
+
for (const r of free) {
|
|
30
|
+
if (!overlaps(r, panel)) {
|
|
31
|
+
next.push(r);
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (panel.left > r.left) next.push({ ...r, right: panel.left });
|
|
35
|
+
if (panel.right < r.right) next.push({ ...r, left: panel.right });
|
|
36
|
+
if (panel.top > r.top) next.push({ ...r, bottom: panel.top });
|
|
37
|
+
if (panel.bottom < r.bottom) next.push({ ...r, top: panel.bottom });
|
|
38
|
+
}
|
|
39
|
+
free = next.filter((r, i) => !next.some((o, j) => j !== i && contains(o, r) && (!contains(r, o) || j < i)));
|
|
40
|
+
}
|
|
41
|
+
return free;
|
|
42
|
+
}
|
|
43
|
+
function zoomInto(area, drawing, minZoom, maxZoom) {
|
|
44
|
+
const w = width(area) - 2 * sidePadding(width(area));
|
|
45
|
+
const h = height(area) - 2 * sidePadding(height(area));
|
|
46
|
+
const zoom = Math.min(w / Math.max(drawing.width, 1), h / Math.max(drawing.height, 1));
|
|
47
|
+
return Math.min(maxZoom, Math.max(minZoom, zoom));
|
|
48
|
+
}
|
|
49
|
+
const CENTRAL_SHARE = 0.75;
|
|
50
|
+
export function fitArea(canvas, panels, drawing, minZoom, maxZoom) {
|
|
51
|
+
const free = freeRectangles(canvas, panels).filter((r) => width(r) > 0 && height(r) > 0);
|
|
52
|
+
const roomy = free.filter((r) => width(r) >= MIN_SIDE && height(r) >= MIN_SIDE);
|
|
53
|
+
const usable = roomy.length ? roomy : free;
|
|
54
|
+
if (!usable.length) return canvas;
|
|
55
|
+
const zooms = usable.map((r) => zoomInto(r, drawing, minZoom, maxZoom));
|
|
56
|
+
const floor = Math.max(...zooms) * CENTRAL_SHARE - 1e-6;
|
|
57
|
+
const cx = (canvas.left + canvas.right) / 2;
|
|
58
|
+
const cy = (canvas.top + canvas.bottom) / 2;
|
|
59
|
+
const offCentre = (r) => Math.hypot((r.left + r.right) / 2 - cx, (r.top + r.bottom) / 2 - cy);
|
|
60
|
+
let best = null;
|
|
61
|
+
for (const [i, r] of usable.entries()) {
|
|
62
|
+
if (zooms[i] < floor) continue;
|
|
63
|
+
if (best === null || offCentre(r) < offCentre(best) - 1e-6 || Math.abs(offCentre(r) - offCentre(best)) <= 1e-6 && width(r) * height(r) > width(best) * height(best)) {
|
|
64
|
+
best = r;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return best ?? canvas;
|
|
68
|
+
}
|
|
69
|
+
export function paddingFor(canvas, area) {
|
|
70
|
+
const padX = sidePadding(width(area));
|
|
71
|
+
const padY = sidePadding(height(area));
|
|
72
|
+
return {
|
|
73
|
+
top: `${area.top - canvas.top + padY}px`,
|
|
74
|
+
right: `${canvas.right - area.right + padX}px`,
|
|
75
|
+
bottom: `${canvas.bottom - area.bottom + padY}px`,
|
|
76
|
+
left: `${area.left - canvas.left + padX}px`
|
|
77
|
+
};
|
|
78
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { WorkflowExecutionMode } from '../types/integrations.js';
|
|
2
|
+
/**
|
|
3
|
+
* The message key that names an execution mode in a compact badge (PO-615).
|
|
4
|
+
*
|
|
5
|
+
* Two surfaces say it: the Mode column of the workflows list and the badge beside
|
|
6
|
+
* the name in a workflow's header. They used to read two sets of keys, and so the
|
|
7
|
+
* list wrote "ASYNC" where the header wrote "Asynchron" for the same workflow. One
|
|
8
|
+
* function keeps the word in one place — the lesson `triggerTypeLabel` was written
|
|
9
|
+
* on. Caller decides has no shorter form than its choice label, so both surfaces
|
|
10
|
+
* use that.
|
|
11
|
+
*
|
|
12
|
+
* A mode the studio does not know yet — or none at all — has no key: the caller
|
|
13
|
+
* decides what an unknown mode reads as, because the list shows a dash where the
|
|
14
|
+
* header falls back to the default mode.
|
|
15
|
+
*/
|
|
16
|
+
export declare function executionModeLabelKey(mode: WorkflowExecutionMode | string | null | undefined): string | null;
|
|
17
|
+
/** The short word for an execution mode in the reader's language, or `fallback` for one without a key. */
|
|
18
|
+
export declare function executionModeLabel(mode: WorkflowExecutionMode | string | null | undefined, fallback?: string): string;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { useIntegrationsT } from "../composables/useIntegrationsT.js";
|
|
2
|
+
export function executionModeLabelKey(mode) {
|
|
3
|
+
switch (mode) {
|
|
4
|
+
case "async_only":
|
|
5
|
+
return "integration.editor.executionMode.async.short";
|
|
6
|
+
case "sync_only":
|
|
7
|
+
return "integration.editor.executionMode.sync.short";
|
|
8
|
+
case "caller_decides":
|
|
9
|
+
return "integration.editor.executionMode.callerDecides.label";
|
|
10
|
+
default:
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export function executionModeLabel(mode, fallback = "\u2014") {
|
|
15
|
+
const key = executionModeLabelKey(mode);
|
|
16
|
+
return key ? useIntegrationsT()(key) : fallback;
|
|
17
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Narrowing a list of options to what an operator typed, and ordering what is
|
|
3
|
+
* left by how well it matches (PO-650).
|
|
4
|
+
*
|
|
5
|
+
* An option has a label the operator reads and a value the platform stores —
|
|
6
|
+
* often a code (`EN-US`, `DE`). A term is first a guess at the start of one of
|
|
7
|
+
* them, and only after that something the label merely contains: "EN" is meant
|
|
8
|
+
* as English, not as the middle of "Armenisch". So the order is
|
|
9
|
+
*
|
|
10
|
+
* 1. the value starts with the term,
|
|
11
|
+
* 2. the label starts with it,
|
|
12
|
+
* 3. a word of the label starts with it,
|
|
13
|
+
* 4. the label or the value contains it,
|
|
14
|
+
* 5. every word of it is in the label, in any order ("Roth Carl"),
|
|
15
|
+
*
|
|
16
|
+
* and an option that matches none of them is not offered. Within a rank the
|
|
17
|
+
* options keep the order they came in, which is the order the platform chose.
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* The options that match `term`, best match first. An empty term (or one of
|
|
21
|
+
* blanks) leaves the list as it is; a term nothing matches leaves it empty —
|
|
22
|
+
* never the whole list, which is what an operator would read as "matches".
|
|
23
|
+
*/
|
|
24
|
+
export declare function narrowOptions<T>(options: readonly T[], term: string, labelOf: (option: T) => string, valueOf: (option: T) => string): T[];
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
var Rank = /* @__PURE__ */ ((Rank2) => {
|
|
2
|
+
Rank2[Rank2["ValuePrefix"] = 0] = "ValuePrefix";
|
|
3
|
+
Rank2[Rank2["LabelPrefix"] = 1] = "LabelPrefix";
|
|
4
|
+
Rank2[Rank2["WordPrefix"] = 2] = "WordPrefix";
|
|
5
|
+
Rank2[Rank2["Contains"] = 3] = "Contains";
|
|
6
|
+
Rank2[Rank2["AllWords"] = 4] = "AllWords";
|
|
7
|
+
return Rank2;
|
|
8
|
+
})(Rank || {});
|
|
9
|
+
function words(label) {
|
|
10
|
+
return label.split(/[^\p{L}\p{N}]+/u).filter(Boolean);
|
|
11
|
+
}
|
|
12
|
+
function rankOf(label, value, term) {
|
|
13
|
+
if (value.startsWith(term)) return 0 /* ValuePrefix */;
|
|
14
|
+
if (label.startsWith(term)) return 1 /* LabelPrefix */;
|
|
15
|
+
if (words(label).some((word) => word.startsWith(term))) return 2 /* WordPrefix */;
|
|
16
|
+
if (label.includes(term) || value.includes(term)) return 3 /* Contains */;
|
|
17
|
+
const termWords = words(term);
|
|
18
|
+
if (termWords.length > 1 && termWords.every((word) => label.includes(word))) return 4 /* AllWords */;
|
|
19
|
+
return void 0;
|
|
20
|
+
}
|
|
21
|
+
export function narrowOptions(options, term, labelOf, valueOf) {
|
|
22
|
+
const needle = term.trim().toLocaleLowerCase();
|
|
23
|
+
if (!needle) return [...options];
|
|
24
|
+
const ranked = [];
|
|
25
|
+
options.forEach((option, index) => {
|
|
26
|
+
const rank = rankOf(labelOf(option).toLocaleLowerCase(), valueOf(option).toLocaleLowerCase(), needle);
|
|
27
|
+
if (rank !== void 0) ranked.push({ option, rank, index });
|
|
28
|
+
});
|
|
29
|
+
ranked.sort((a, b) => a.rank - b.rank || a.index - b.index);
|
|
30
|
+
return ranked.map((r) => r.option);
|
|
31
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { RunStepRow } from '../types/integrations.js';
|
|
2
|
+
/**
|
|
3
|
+
* Why a run failed, in the two parts an author reads it by (PO-690): the step
|
|
4
|
+
* it failed at, named the way the canvas names it, and the cause in the step's
|
|
5
|
+
* own words.
|
|
6
|
+
*
|
|
7
|
+
* Either may be missing. The step is unknown until the run's steps have been read,
|
|
8
|
+
* and stays unknown where they cannot be; the cause then falls back to what the run
|
|
9
|
+
* itself recorded, which is less precise but is still the platform's reason.
|
|
10
|
+
*/
|
|
11
|
+
export interface RunFailure {
|
|
12
|
+
step: string | null;
|
|
13
|
+
cause: string | null;
|
|
14
|
+
}
|
|
15
|
+
/** A node as the blob carries it, enough to look a step's node up by id and name it. */
|
|
16
|
+
export interface FailureNode {
|
|
17
|
+
id: string;
|
|
18
|
+
name?: unknown;
|
|
19
|
+
nodeSlug: string;
|
|
20
|
+
nodeVersion?: string;
|
|
21
|
+
}
|
|
22
|
+
/** Whether a run, as the platform reports it, ended in failure. */
|
|
23
|
+
export declare function isFailedRun(run: {
|
|
24
|
+
status?: unknown;
|
|
25
|
+
} | null | undefined): boolean;
|
|
26
|
+
/** A reason that is text with something in it, or nothing. */
|
|
27
|
+
export declare function reasonText(value: unknown): string | null;
|
|
28
|
+
/**
|
|
29
|
+
* The failure read off a run's steps, with `fallbackCause` where no step says why.
|
|
30
|
+
*
|
|
31
|
+
* The LAST failed row is the one taken: a step retried by the engine leaves a row
|
|
32
|
+
* per attempt, and the final one is the failure that ended the run. A failed step
|
|
33
|
+
* is named after its node on the canvas; a node no longer on the canvas — the run
|
|
34
|
+
* is of a saved version since edited — is named the way the catalogue names its
|
|
35
|
+
* kind, never by the generated id.
|
|
36
|
+
*/
|
|
37
|
+
export declare function runFailureFromSteps(steps: readonly RunStepRow[] | null | undefined, nodes: readonly FailureNode[], fallbackCause: string | null): RunFailure;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { nodeDisplayName } from "./nodeDisplayName.js";
|
|
2
|
+
import { stepError } from "./stepError.js";
|
|
3
|
+
const FAILED_RUN = /* @__PURE__ */ new Set(["failed", "terminated", "timed_out"]);
|
|
4
|
+
export function isFailedRun(run) {
|
|
5
|
+
return FAILED_RUN.has(String(run?.status ?? "").toLowerCase());
|
|
6
|
+
}
|
|
7
|
+
export function reasonText(value) {
|
|
8
|
+
return typeof value === "string" && value.trim() !== "" ? value.trim() : null;
|
|
9
|
+
}
|
|
10
|
+
export function runFailureFromSteps(steps, nodes, fallbackCause) {
|
|
11
|
+
const failed = [...steps ?? []].reverse().find((s) => String(s.status).toLowerCase() === "failed");
|
|
12
|
+
if (!failed) return { step: null, cause: fallbackCause };
|
|
13
|
+
const node = nodes.find((n) => n.id === failed.node_id);
|
|
14
|
+
const step = node ? nodeDisplayName(node) : nodeDisplayName({ nodeSlug: failed.node_slug });
|
|
15
|
+
return { step, cause: stepError(failed.error)?.message ?? fallbackCause };
|
|
16
|
+
}
|