@revenexx/studio-integrations 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/CHANGELOG.md +520 -0
  2. package/README.md +3 -1
  3. package/dist/module.json +1 -1
  4. package/dist/runtime/components/Integrations/CredentialCreateDialog.vue +6 -18
  5. package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +13 -23
  6. package/dist/runtime/components/Integrations/CredentialRevenexxSource.vue +7 -6
  7. package/dist/runtime/components/Integrations/DeadLettersPanel.vue +15 -4
  8. package/dist/runtime/components/Integrations/ExpressionInput.d.vue.ts +4 -0
  9. package/dist/runtime/components/Integrations/ExpressionInput.vue +77 -22
  10. package/dist/runtime/components/Integrations/ExpressionInput.vue.d.ts +4 -0
  11. package/dist/runtime/components/Integrations/GroupFrameView.vue +5 -4
  12. package/dist/runtime/components/Integrations/LoadingAnnouncement.d.vue.ts +38 -0
  13. package/dist/runtime/components/Integrations/LoadingAnnouncement.vue +16 -0
  14. package/dist/runtime/components/Integrations/LoadingAnnouncement.vue.d.ts +38 -0
  15. package/dist/runtime/components/Integrations/NodeConfigField.vue +23 -0
  16. package/dist/runtime/components/Integrations/NodeInspector.d.vue.ts +2 -2
  17. package/dist/runtime/components/Integrations/NodeInspector.vue +25 -32
  18. package/dist/runtime/components/Integrations/NodeInspector.vue.d.ts +2 -2
  19. package/dist/runtime/components/Integrations/RunActionsMenu.vue +5 -3
  20. package/dist/runtime/components/Integrations/RunDetailPanel.vue +5 -17
  21. package/dist/runtime/components/Integrations/RunResponseModal.d.vue.ts +30 -8
  22. package/dist/runtime/components/Integrations/RunResponseModal.vue +92 -24
  23. package/dist/runtime/components/Integrations/RunResponseModal.vue.d.ts +30 -8
  24. package/dist/runtime/components/Integrations/RunsTable.d.vue.ts +61 -3
  25. package/dist/runtime/components/Integrations/RunsTable.vue +143 -20
  26. package/dist/runtime/components/Integrations/RunsTable.vue.d.ts +61 -3
  27. package/dist/runtime/components/Integrations/SftpAccountImportDialog.vue +10 -1
  28. package/dist/runtime/components/Integrations/StickyNoteView.vue +1 -1
  29. package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +18 -3
  30. package/dist/runtime/components/Integrations/WorkflowDeleteDialog.d.vue.ts +18 -0
  31. package/dist/runtime/components/Integrations/WorkflowDeleteDialog.vue +70 -0
  32. package/dist/runtime/components/Integrations/WorkflowDeleteDialog.vue.d.ts +18 -0
  33. package/dist/runtime/components/Integrations/WorkflowDuplicateDialog.vue +2 -1
  34. package/dist/runtime/components/Integrations/WorkflowEdgeView.vue +4 -4
  35. package/dist/runtime/components/Integrations/WorkflowEditor.vue +39 -28
  36. package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue +17 -6
  37. package/dist/runtime/components/Integrations/WorkflowNodeView.vue +17 -21
  38. package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +8 -11
  39. package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +6 -7
  40. package/dist/runtime/components/Integrations/WorkflowVariablesDialog.vue +7 -7
  41. package/dist/runtime/components/Integrations/WorkflowsTable.vue +10 -48
  42. package/dist/runtime/components/Integrations/canvasSides.d.ts +48 -0
  43. package/dist/runtime/components/Integrations/canvasSides.js +22 -0
  44. package/dist/runtime/composables/useCredentialTypes.js +2 -6
  45. package/dist/runtime/composables/useIntegrationsAudit.js +2 -2
  46. package/dist/runtime/composables/useIntegrationsCredentials.js +4 -8
  47. package/dist/runtime/composables/useIntegrationsDeadLetters.d.ts +4 -0
  48. package/dist/runtime/composables/useIntegrationsDeadLetters.js +16 -1
  49. package/dist/runtime/composables/useIntegrationsNodes.js +2 -2
  50. package/dist/runtime/composables/useIntegrationsRevisions.js +2 -6
  51. package/dist/runtime/composables/useIntegrationsRuns.d.ts +4 -15
  52. package/dist/runtime/composables/useIntegrationsRuns.js +49 -34
  53. package/dist/runtime/composables/useIntegrationsSchedules.js +2 -2
  54. package/dist/runtime/composables/useIntegrationsSecrets.js +2 -6
  55. package/dist/runtime/composables/useIntegrationsTemplates.js +2 -5
  56. package/dist/runtime/composables/useIntegrationsTriggers.js +2 -2
  57. package/dist/runtime/composables/useIntegrationsWorkflows.js +4 -7
  58. package/dist/runtime/composables/useNodeConfigResolve.js +2 -2
  59. package/dist/runtime/composables/useOAuthConnect.d.ts +42 -0
  60. package/dist/runtime/composables/useOAuthConnect.js +33 -0
  61. package/dist/runtime/data/expressionEditor.d.ts +17 -5
  62. package/dist/runtime/data/expressionEditor.js +11 -5
  63. package/dist/runtime/data/handleId.d.ts +61 -1
  64. package/dist/runtime/data/handleId.js +8 -1
  65. package/dist/runtime/data/payloadForm.js +3 -2
  66. package/dist/runtime/i18n/messages/de.json +30 -11
  67. package/dist/runtime/i18n/messages/en.json +27 -8
  68. package/dist/runtime/pages/integrations/audit.vue +7 -6
  69. package/dist/runtime/pages/integrations/credentials.vue +9 -21
  70. package/dist/runtime/pages/integrations/index.vue +70 -12
  71. package/dist/runtime/pages/integrations/runs.vue +116 -7
  72. package/dist/runtime/pages/integrations/secrets.vue +2 -1
  73. package/dist/runtime/pages/integrations/templates/[slug].vue +2 -1
  74. package/dist/runtime/pages/integrations/templates/index.vue +18 -15
  75. package/dist/runtime/pages/integrations/workflows/[id].vue +177 -28
  76. package/dist/runtime/types/integrations.d.ts +5 -0
  77. package/dist/runtime/utils/apiError.d.ts +21 -1
  78. package/dist/runtime/utils/apiError.js +1 -1
  79. package/dist/runtime/utils/clipboard.d.ts +33 -0
  80. package/dist/runtime/utils/clipboard.js +11 -0
  81. package/dist/runtime/utils/deepClone.d.ts +41 -0
  82. package/dist/runtime/utils/deepClone.js +3 -0
  83. package/dist/runtime/utils/formatDuration.d.ts +44 -0
  84. package/dist/runtime/utils/formatDuration.js +13 -0
  85. package/dist/runtime/utils/nodeDisplayName.d.ts +22 -0
  86. package/dist/runtime/utils/nodeDisplayName.js +6 -0
  87. package/dist/runtime/utils/notify.d.ts +1 -1
  88. package/dist/runtime/utils/runEnvelope.d.ts +31 -0
  89. package/dist/runtime/utils/runEnvelope.js +12 -0
  90. package/dist/runtime/utils/runFeedQuery.d.ts +135 -0
  91. package/dist/runtime/utils/runFeedQuery.js +66 -0
  92. package/dist/runtime/utils/runOutputs.d.ts +33 -0
  93. package/dist/runtime/utils/runOutputs.js +10 -0
  94. package/dist/runtime/utils/statusStyle.d.ts +24 -0
  95. package/dist/runtime/utils/statusStyle.js +1 -0
  96. package/dist/runtime/utils/validationFindings.d.ts +26 -3
  97. package/dist/runtime/utils/validationFindings.js +18 -3
  98. package/package.json +1 -1
@@ -0,0 +1,22 @@
1
+ import { Position } from "@vue-flow/core";
2
+ export const SIDE_POSITION = {
3
+ top: Position.Top,
4
+ right: Position.Right,
5
+ bottom: Position.Bottom,
6
+ left: Position.Left
7
+ };
8
+ export const OPPOSITE_SIDE = {
9
+ top: "bottom",
10
+ right: "left",
11
+ bottom: "top",
12
+ left: "right"
13
+ };
14
+ const SIDE_SPREAD_AXIS = {
15
+ top: "left",
16
+ right: "top",
17
+ bottom: "left",
18
+ left: "top"
19
+ };
20
+ export function distributedStyle(side, i, total) {
21
+ return { [SIDE_SPREAD_AXIS[side]]: `${(i + 1) / (total + 1) * 100}%` };
22
+ }
@@ -1,6 +1,6 @@
1
1
  import { useIntegrationsApi } from "./integrationsApi.js";
2
2
  import { useIntegrationsT } from "./useIntegrationsT.js";
3
- import { IntegrationsApiError } from "../utils/apiError.js";
3
+ import { errorReason } from "../utils/apiError.js";
4
4
  import { useLoadGate } from "./useLoadGate.js";
5
5
  export function useCredentialTypes() {
6
6
  const types = useState("integrations.credentialTypes", () => []);
@@ -16,10 +16,6 @@ export function useCredentialTypes() {
16
16
  if (Array.isArray(obj?.data)) return obj.data;
17
17
  return [];
18
18
  }
19
- function captureError(e, fallback) {
20
- if (e instanceof IntegrationsApiError) return e.message;
21
- return e?.message ?? fallback;
22
- }
23
19
  async function load(force = false) {
24
20
  if (!await gate.claim(force)) return types.value;
25
21
  error.value = null;
@@ -27,7 +23,7 @@ export function useCredentialTypes() {
27
23
  types.value = normalize(await api.get("/credential-types"));
28
24
  loaded.value = true;
29
25
  } catch (e) {
30
- error.value = captureError(e, t("integration.credentials.error.loadTypes"));
26
+ error.value = errorReason(e, t("integration.credentials.error.loadTypes"));
31
27
  } finally {
32
28
  gate.release();
33
29
  }
@@ -1,5 +1,5 @@
1
1
  import { useIntegrationsApi } from "./integrationsApi.js";
2
- import { IntegrationsApiError } from "../utils/apiError.js";
2
+ import { errorReason } from "../utils/apiError.js";
3
3
  import { useLoadGate } from "./useLoadGate.js";
4
4
  export function useIntegrationsAudit() {
5
5
  const api = useIntegrationsApi();
@@ -28,7 +28,7 @@ export function useIntegrationsAudit() {
28
28
  entries.value = append ? [...entries.value, ...data] : data;
29
29
  nextCursor.value = res.meta?.next_cursor ?? null;
30
30
  } catch (e) {
31
- error.value = e instanceof IntegrationsApiError ? e.message : e?.message ?? "Could not load the audit log.";
31
+ error.value = errorReason(e, "Could not load the audit log.");
32
32
  } finally {
33
33
  gate.release();
34
34
  }
@@ -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 { IntegrationsApiError } from "../utils/apiError.js";
4
+ import { IntegrationsApiError, errorReason } from "../utils/apiError.js";
5
5
  import { useLoadGate } from "./useLoadGate.js";
6
6
  export function useIntegrationsCredentials() {
7
7
  const credentials = useState("integrations.credentials", () => []);
@@ -20,10 +20,6 @@ export function useIntegrationsCredentials() {
20
20
  function unwrap(res) {
21
21
  return res?.data ?? res;
22
22
  }
23
- function captureError(e, fallback) {
24
- if (e instanceof IntegrationsApiError) return e.message;
25
- return e?.message ?? fallback;
26
- }
27
23
  async function load(force = false) {
28
24
  if (!await gate.claim(force)) return credentials.value;
29
25
  error.value = null;
@@ -31,7 +27,7 @@ export function useIntegrationsCredentials() {
31
27
  credentials.value = normalize(await api.get("/credentials"));
32
28
  loaded.value = true;
33
29
  } catch (e) {
34
- error.value = captureError(e, t("integration.credentials.error.loadFailed"));
30
+ error.value = errorReason(e, t("integration.credentials.error.loadFailed"));
35
31
  } finally {
36
32
  gate.release();
37
33
  }
@@ -62,7 +58,7 @@ export function useIntegrationsCredentials() {
62
58
  const res = await api.post("/credentials/test", { credential_type_slug: credentialTypeSlug, config });
63
59
  return res?.ok === false ? res : { ok: true, message: res?.message };
64
60
  } catch (e) {
65
- return { ok: false, error: captureError(e, t("integration.credentials.error.testFailed")) };
61
+ return { ok: false, error: errorReason(e, t("integration.credentials.error.testFailed")) };
66
62
  }
67
63
  }
68
64
  async function test(id) {
@@ -70,7 +66,7 @@ export function useIntegrationsCredentials() {
70
66
  const res = await api.post(`/credentials/${encodeURIComponent(id)}/test`);
71
67
  return res?.ok === false ? res : { ok: true, message: res?.message };
72
68
  } catch (e) {
73
- return { ok: false, error: captureError(e, t("integration.credentials.error.testFailed")) };
69
+ return { ok: false, error: errorReason(e, t("integration.credentials.error.testFailed")) };
74
70
  }
75
71
  }
76
72
  async function oauthAuthorizeUrl(id) {
@@ -24,6 +24,10 @@ export declare function useIntegrationsDeadLetters(): {
24
24
  globalLoading: any;
25
25
  globalLoaded: any;
26
26
  globalHasMore: any;
27
+ globalPage: any;
28
+ globalLastPage: import("vue").ComputedRef<number>;
29
+ goGlobalPage: (page: number) => Promise<GlobalDeadLetterRow[]>;
30
+ resetGlobalPage: () => void;
27
31
  summary: any;
28
32
  pendingCount: import("vue").ComputedRef<any>;
29
33
  };
@@ -1,6 +1,7 @@
1
1
  import { computed } from "vue";
2
2
  import { useIntegrationsApi } from "./integrationsApi.js";
3
3
  import { useLoadGate } from "./useLoadGate.js";
4
+ import { runFeedLastPage } from "../utils/runFeedQuery.js";
4
5
  const EMPTY_SUMMARY = { total: 0, by_status: {} };
5
6
  export function useIntegrationsDeadLetters() {
6
7
  const api = useIntegrationsApi();
@@ -25,12 +26,15 @@ export function useIntegrationsDeadLetters() {
25
26
  const gate = useLoadGate("integrations.deadletters.global", { loading: globalLoading, loaded: globalLoaded });
26
27
  const summary = useState("integrations.deadletters.summary", () => ({ ...EMPTY_SUMMARY }));
27
28
  const globalHasMore = useState("integrations.deadletters.global.hasMore", () => false);
29
+ const GLOBAL_PER_PAGE = 25;
30
+ const globalPage = useState("integrations.deadletters.global.page", () => 1);
31
+ const globalLastPage = computed(() => runFeedLastPage(summary.value.total, GLOBAL_PER_PAGE));
28
32
  const pendingCount = computed(() => summary.value.by_status.pending ?? 0);
29
33
  async function loadGlobal(force = false) {
30
34
  if (!await gate.claim(force)) return globalItems.value;
31
35
  try {
32
36
  const [page, counts] = await Promise.all([
33
- api.get("/dead-letters?page=1&per_page=100"),
37
+ api.get(`/dead-letters?page=${globalPage.value}&per_page=${GLOBAL_PER_PAGE}`),
34
38
  api.get("/dead-letters/summary")
35
39
  ]);
36
40
  globalItems.value = page?.data ?? [];
@@ -45,6 +49,13 @@ export function useIntegrationsDeadLetters() {
45
49
  }
46
50
  return globalItems.value;
47
51
  }
52
+ async function goGlobalPage(page) {
53
+ globalPage.value = Math.min(Math.max(1, page), globalLastPage.value);
54
+ return loadGlobal(true);
55
+ }
56
+ function resetGlobalPage() {
57
+ globalPage.value = 1;
58
+ }
48
59
  return {
49
60
  list,
50
61
  replay,
@@ -54,6 +65,10 @@ export function useIntegrationsDeadLetters() {
54
65
  globalLoading,
55
66
  globalLoaded,
56
67
  globalHasMore,
68
+ globalPage,
69
+ globalLastPage,
70
+ goGlobalPage,
71
+ resetGlobalPage,
57
72
  summary,
58
73
  pendingCount
59
74
  };
@@ -2,7 +2,7 @@ import { applyNodeCatalog, findManifest } from "../data/integrationNodeCatalog.j
2
2
  import { resolveLocalized } from "../utils/localize.js";
3
3
  import { useIntegrationsApi } from "./integrationsApi.js";
4
4
  import { useIntegrationsT } from "./useIntegrationsT.js";
5
- import { IntegrationsApiError } from "../utils/apiError.js";
5
+ import { errorReason } from "../utils/apiError.js";
6
6
  import { useLoadGate } from "./useLoadGate.js";
7
7
  const RETRY_COOLDOWN_MS = 3e4;
8
8
  export function useIntegrationsNodes() {
@@ -39,7 +39,7 @@ export function useIntegrationsNodes() {
39
39
  applyNodeCatalog([]);
40
40
  loaded.value = false;
41
41
  failedAt.value = Date.now();
42
- error.value = e instanceof IntegrationsApiError ? e.message : e?.message ?? t("integration.nodes.error.loadFailed");
42
+ error.value = errorReason(e, t("integration.nodes.error.loadFailed"));
43
43
  } finally {
44
44
  gate.release();
45
45
  }
@@ -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 { IntegrationsApiError } from "../utils/apiError.js";
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 = captureError(e, t("integration.revisions.error.loadFailed"));
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 { IntegrationsApiError } from "../utils/apiError.js";
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: 100
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: mapStatus(r.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 { page: pageNumber, perPage } = query.value;
72
+ const narrowing = runFeedNarrowingString(query.value);
73
+ const counted = claimSummary();
76
74
  const [page, counts] = await Promise.all([
77
- api.get(`/runs?page=${pageNumber}&per_page=${perPage}`),
78
- api.get("/runs/summary")
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
- summary.value = counts?.data ?? { ...EMPTY_SUMMARY };
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 instanceof IntegrationsApiError ? e.message : e?.message ?? t("integration.runs.error.loadFailed");
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 instanceof IntegrationsApiError ? e.message : e?.message ?? t("integration.runs.error.loadFailed");
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 instanceof IntegrationsApiError ? e.message : e?.message ?? t("integration.runs.error.loadFailed");
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]) => mapStatus(status) === "failed").reduce((sum, [, count]) => sum + count, 0));
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 { IntegrationsApiError } from "../utils/apiError.js";
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 instanceof IntegrationsApiError ? e.message : e?.message ?? t("integration.schedules.error.loadFailed");
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 { IntegrationsApiError } from "../utils/apiError.js";
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 = captureError(e, t("integration.secrets.error.loadFailed"));
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 { IntegrationsApiError } from "../utils/apiError.js";
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";
@@ -100,9 +100,6 @@ export function useIntegrationsTemplates() {
100
100
  function unwrap(res) {
101
101
  return res?.data ?? res;
102
102
  }
103
- function captureError(e, fallback) {
104
- return e instanceof IntegrationsApiError ? e.message : e?.message ?? fallback;
105
- }
106
103
  async function load(force = false) {
107
104
  if (!await gate.claim(force)) return templates.value;
108
105
  error.value = null;
@@ -112,7 +109,7 @@ export function useIntegrationsTemplates() {
112
109
  rawDetails.value = {};
113
110
  loaded.value = true;
114
111
  } catch (e) {
115
- error.value = captureError(e, t("integration.templates.state.error.fallback"));
112
+ error.value = errorReason(e, t("integration.templates.state.error.fallback"));
116
113
  rawList.value = [];
117
114
  rawDetails.value = {};
118
115
  loaded.value = true;
@@ -1,6 +1,6 @@
1
1
  import { useIntegrationsApi } from "./integrationsApi.js";
2
2
  import { useIntegrationsT } from "./useIntegrationsT.js";
3
- import { IntegrationsApiError } from "../utils/apiError.js";
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 instanceof IntegrationsApiError ? e.message : e?.message ?? t("integration.triggers.error.loadFailed");
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 { IntegrationsApiError } from "../utils/apiError.js";
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) {
@@ -89,10 +90,6 @@ export function useIntegrationsWorkflows() {
89
90
  function unwrap(res) {
90
91
  return res?.data ?? res;
91
92
  }
92
- function captureError(e, fallback) {
93
- if (e instanceof IntegrationsApiError) return e.message;
94
- return e?.message ?? fallback;
95
- }
96
93
  function remember(api2, into) {
97
94
  if (api2.triggers !== void 0) into[Number(api2.id)] = api2.triggers;
98
95
  }
@@ -123,7 +120,7 @@ export function useIntegrationsWorkflows() {
123
120
  workflows.value = ingestList(list2);
124
121
  loaded.value = true;
125
122
  } catch (e) {
126
- error.value = captureError(e, t("integration.workflows.error.loadFailed"));
123
+ error.value = errorReason(e, t("integration.workflows.error.loadFailed"));
127
124
  loaded.value = false;
128
125
  } finally {
129
126
  gate.release();
@@ -157,7 +154,7 @@ export function useIntegrationsWorkflows() {
157
154
  async function duplicate(id, name) {
158
155
  const source = await get(id);
159
156
  if (!source) throw new Error(t("integration.workflows.error.notFound"));
160
- const blob = JSON.parse(JSON.stringify(source.blob));
157
+ const blob = deepClone(source.blob);
161
158
  const handleMap = /* @__PURE__ */ new Map();
162
159
  for (const trigger of blob.triggers ?? []) {
163
160
  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 instanceof IntegrationsApiError ? err.message : err?.message ?? t("integration.nodeConfig.resolve.error.generic");
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
+ }