@revenexx/studio-integrations 0.5.1 → 0.7.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 (171) hide show
  1. package/CHANGELOG.md +428 -0
  2. package/README.md +48 -4
  3. package/dist/module.d.mts +17 -1
  4. package/dist/module.json +1 -1
  5. package/dist/module.mjs +10 -2
  6. package/dist/runtime/components/Integrations/CredentialCreateDialog.vue +43 -41
  7. package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +123 -50
  8. package/dist/runtime/components/Integrations/CredentialRevenexxSource.vue +108 -71
  9. package/dist/runtime/components/Integrations/CredentialTypePicker.vue +12 -15
  10. package/dist/runtime/components/Integrations/DeadLettersPanel.vue +18 -17
  11. package/dist/runtime/components/Integrations/EventTypeCombobox.d.vue.ts +1 -0
  12. package/dist/runtime/components/Integrations/EventTypeCombobox.vue +7 -4
  13. package/dist/runtime/components/Integrations/EventTypeCombobox.vue.d.ts +1 -0
  14. package/dist/runtime/components/Integrations/ExpressionInput.d.vue.ts +4 -0
  15. package/dist/runtime/components/Integrations/ExpressionInput.vue +67 -37
  16. package/dist/runtime/components/Integrations/ExpressionInput.vue.d.ts +4 -0
  17. package/dist/runtime/components/Integrations/GroupFrameView.vue +30 -20
  18. package/dist/runtime/components/Integrations/Layout.vue +48 -4
  19. package/dist/runtime/components/Integrations/NodeConfigDynamicField.vue +23 -23
  20. package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.vue +11 -13
  21. package/dist/runtime/components/Integrations/NodeConfigField.d.vue.ts +2 -0
  22. package/dist/runtime/components/Integrations/NodeConfigField.vue +187 -93
  23. package/dist/runtime/components/Integrations/NodeConfigField.vue.d.ts +2 -0
  24. package/dist/runtime/components/Integrations/NodeInspector.d.vue.ts +6 -0
  25. package/dist/runtime/components/Integrations/NodeInspector.vue +1157 -376
  26. package/dist/runtime/components/Integrations/NodeInspector.vue.d.ts +6 -0
  27. package/dist/runtime/components/Integrations/NodeLogo.d.vue.ts +12 -0
  28. package/dist/runtime/components/Integrations/NodeLogo.vue +2 -30
  29. package/dist/runtime/components/Integrations/NodeLogo.vue.d.ts +12 -0
  30. package/dist/runtime/components/Integrations/NodeMark.d.vue.ts +15 -0
  31. package/dist/runtime/components/Integrations/NodeMark.vue +79 -0
  32. package/dist/runtime/components/Integrations/NodeMark.vue.d.ts +15 -0
  33. package/dist/runtime/components/Integrations/NodeOutputSchema.d.vue.ts +21 -0
  34. package/dist/runtime/components/Integrations/NodeOutputSchema.vue +43 -0
  35. package/dist/runtime/components/Integrations/NodeOutputSchema.vue.d.ts +21 -0
  36. package/dist/runtime/components/Integrations/NodePalette.vue +122 -55
  37. package/dist/runtime/components/Integrations/QueryBuilder.vue +46 -38
  38. package/dist/runtime/components/Integrations/RunActionsMenu.vue +38 -12
  39. package/dist/runtime/components/Integrations/RunDetailPanel.vue +114 -62
  40. package/dist/runtime/components/Integrations/RunPayloadInput.vue +27 -13
  41. package/dist/runtime/components/Integrations/RunResponseModal.vue +11 -10
  42. package/dist/runtime/components/Integrations/RunsTable.d.vue.ts +0 -1
  43. package/dist/runtime/components/Integrations/RunsTable.vue +38 -16
  44. package/dist/runtime/components/Integrations/RunsTable.vue.d.ts +0 -1
  45. package/dist/runtime/components/Integrations/SftpAccountImportDialog.vue +8 -7
  46. package/dist/runtime/components/Integrations/Sidebar.vue +14 -10
  47. package/dist/runtime/components/Integrations/StickyNoteView.d.vue.ts +14 -4
  48. package/dist/runtime/components/Integrations/StickyNoteView.vue +38 -23
  49. package/dist/runtime/components/Integrations/StickyNoteView.vue.d.ts +14 -4
  50. package/dist/runtime/components/Integrations/TemplateCard.d.vue.ts +18 -0
  51. package/dist/runtime/components/Integrations/TemplateCard.vue +119 -0
  52. package/dist/runtime/components/Integrations/TemplateCard.vue.d.ts +18 -0
  53. package/dist/runtime/components/Integrations/TemplateFlowPreview.d.vue.ts +2 -2
  54. package/dist/runtime/components/Integrations/TemplateFlowPreview.vue +16 -18
  55. package/dist/runtime/components/Integrations/TemplateFlowPreview.vue.d.ts +2 -2
  56. package/dist/runtime/components/Integrations/WorkflowContextMenu.vue +34 -33
  57. package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +52 -67
  58. package/dist/runtime/components/Integrations/WorkflowDuplicateDialog.vue +8 -7
  59. package/dist/runtime/components/Integrations/WorkflowEdgeView.d.vue.ts +18 -4
  60. package/dist/runtime/components/Integrations/WorkflowEdgeView.vue +29 -11
  61. package/dist/runtime/components/Integrations/WorkflowEdgeView.vue.d.ts +18 -4
  62. package/dist/runtime/components/Integrations/WorkflowEditor.d.vue.ts +20 -1
  63. package/dist/runtime/components/Integrations/WorkflowEditor.vue +240 -116
  64. package/dist/runtime/components/Integrations/WorkflowEditor.vue.d.ts +20 -1
  65. package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue +28 -43
  66. package/dist/runtime/components/Integrations/WorkflowIconPicker.d.vue.ts +19 -1
  67. package/dist/runtime/components/Integrations/WorkflowIconPicker.vue +17 -7
  68. package/dist/runtime/components/Integrations/WorkflowIconPicker.vue.d.ts +19 -1
  69. package/dist/runtime/components/Integrations/WorkflowNodeView.d.vue.ts +4 -0
  70. package/dist/runtime/components/Integrations/WorkflowNodeView.vue +198 -56
  71. package/dist/runtime/components/Integrations/WorkflowNodeView.vue.d.ts +4 -0
  72. package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +56 -59
  73. package/dist/runtime/components/Integrations/WorkflowSettingsDialog.vue +28 -21
  74. package/dist/runtime/components/Integrations/WorkflowTagsInput.d.vue.ts +1 -0
  75. package/dist/runtime/components/Integrations/WorkflowTagsInput.vue +15 -9
  76. package/dist/runtime/components/Integrations/WorkflowTagsInput.vue.d.ts +1 -0
  77. package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +15 -10
  78. package/dist/runtime/components/Integrations/WorkflowVariablesDialog.vue +50 -36
  79. package/dist/runtime/components/Integrations/WorkflowsTable.vue +126 -48
  80. package/dist/runtime/composables/integrationsApi.js +4 -2
  81. package/dist/runtime/composables/useCredentialTypes.js +8 -6
  82. package/dist/runtime/composables/useIntegrationsAudit.js +4 -3
  83. package/dist/runtime/composables/useIntegrationsCredentials.js +10 -8
  84. package/dist/runtime/composables/useIntegrationsDeadLetters.js +7 -6
  85. package/dist/runtime/composables/useIntegrationsFocus.d.ts +12 -0
  86. package/dist/runtime/composables/useIntegrationsLocale.d.ts +26 -0
  87. package/dist/runtime/composables/useIntegrationsLocale.js +8 -0
  88. package/dist/runtime/composables/useIntegrationsNodes.js +7 -5
  89. package/dist/runtime/composables/useIntegrationsRealtime.d.ts +149 -0
  90. package/dist/runtime/composables/useIntegrationsRealtime.js +80 -0
  91. package/dist/runtime/composables/useIntegrationsRevisions.js +9 -7
  92. package/dist/runtime/composables/useIntegrationsRuns.d.ts +1 -1
  93. package/dist/runtime/composables/useIntegrationsRuns.js +14 -7
  94. package/dist/runtime/composables/useIntegrationsSchedules.d.ts +4 -0
  95. package/dist/runtime/composables/useIntegrationsSchedules.js +10 -3
  96. package/dist/runtime/composables/useIntegrationsSecrets.js +14 -9
  97. package/dist/runtime/composables/useIntegrationsT.d.ts +35 -0
  98. package/dist/runtime/composables/useIntegrationsT.js +9 -0
  99. package/dist/runtime/composables/useIntegrationsTemplates.d.ts +3 -3
  100. package/dist/runtime/composables/useIntegrationsTemplates.js +28 -16
  101. package/dist/runtime/composables/useIntegrationsTriggers.js +10 -8
  102. package/dist/runtime/composables/useIntegrationsWorkflows.js +13 -7
  103. package/dist/runtime/composables/useLoadGate.d.ts +17 -0
  104. package/dist/runtime/composables/useLoadGate.js +8 -0
  105. package/dist/runtime/composables/useNodeConfigResolve.js +2 -1
  106. package/dist/runtime/data/branchReorder.d.ts +37 -0
  107. package/dist/runtime/data/branchReorder.js +71 -0
  108. package/dist/runtime/data/handleId.d.ts +23 -0
  109. package/dist/runtime/data/handleId.js +6 -0
  110. package/dist/runtime/data/integrationNodeCatalog.d.ts +41 -0
  111. package/dist/runtime/data/integrationNodeCatalog.js +14 -0
  112. package/dist/runtime/data/integrationTemplates.d.ts +48 -0
  113. package/dist/runtime/data/integrationTemplates.js +21 -0
  114. package/dist/runtime/data/integrationWorkflowSamples.js +1 -1
  115. package/dist/runtime/data/nodeIcons.d.ts +2 -0
  116. package/dist/runtime/data/nodeIcons.js +73 -0
  117. package/dist/runtime/data/nodeInputData.d.ts +16 -4
  118. package/dist/runtime/data/nodeInputData.js +4 -0
  119. package/dist/runtime/data/payloadForm.d.ts +9 -2
  120. package/dist/runtime/data/payloadForm.js +29 -4
  121. package/dist/runtime/data/portTone.d.ts +17 -0
  122. package/dist/runtime/data/portTone.js +7 -0
  123. package/dist/runtime/data/webhookAuth.d.ts +18 -0
  124. package/dist/runtime/data/webhookAuth.js +19 -0
  125. package/dist/runtime/data/workflowIconCatalog.js +4 -6
  126. package/dist/runtime/i18n/messages/de.json +1207 -0
  127. package/dist/runtime/i18n/messages/en.json +1207 -0
  128. package/dist/runtime/i18n/translate.d.ts +83 -0
  129. package/dist/runtime/i18n/translate.js +41 -0
  130. package/dist/runtime/pages/integrations/audit.vue +140 -37
  131. package/dist/runtime/pages/integrations/credentials/oauth-complete.vue +9 -6
  132. package/dist/runtime/pages/integrations/credentials.vue +180 -64
  133. package/dist/runtime/pages/integrations/index.vue +91 -92
  134. package/dist/runtime/pages/integrations/runs.vue +33 -21
  135. package/dist/runtime/pages/integrations/schedules.vue +88 -23
  136. package/dist/runtime/pages/integrations/secrets.vue +57 -35
  137. package/dist/runtime/pages/integrations/templates/[slug].vue +119 -88
  138. package/dist/runtime/pages/integrations/templates/index.vue +82 -103
  139. package/dist/runtime/pages/integrations/workflows/[id].vue +344 -91
  140. package/dist/runtime/pages/integrations/workflows/index.vue +23 -5
  141. package/dist/runtime/plugins/locale.d.ts +10 -0
  142. package/dist/runtime/plugins/locale.js +6 -0
  143. package/dist/runtime/plugins/realtime.client.d.ts +32 -0
  144. package/dist/runtime/plugins/realtime.client.js +4 -0
  145. package/dist/runtime/types/integrations.d.ts +65 -6
  146. package/dist/runtime/utils/apiError.d.ts +47 -0
  147. package/dist/runtime/utils/apiError.js +32 -0
  148. package/dist/runtime/utils/cockpitSources.d.ts +153 -0
  149. package/dist/runtime/utils/cockpitSources.js +14 -0
  150. package/dist/runtime/utils/credentialState.d.ts +24 -1
  151. package/dist/runtime/utils/credentialState.js +26 -11
  152. package/dist/runtime/utils/loadGate.d.ts +61 -0
  153. package/dist/runtime/utils/loadGate.js +22 -0
  154. package/dist/runtime/utils/localize.d.ts +46 -4
  155. package/dist/runtime/utils/localize.js +34 -7
  156. package/dist/runtime/utils/notify.d.ts +59 -0
  157. package/dist/runtime/utils/notify.js +21 -0
  158. package/dist/runtime/utils/pollBudget.d.ts +72 -0
  159. package/dist/runtime/utils/pollBudget.js +17 -0
  160. package/dist/runtime/utils/revenexxCredentialSource.d.ts +26 -0
  161. package/dist/runtime/utils/revenexxCredentialSource.js +8 -0
  162. package/dist/runtime/utils/schedule.d.ts +116 -0
  163. package/dist/runtime/utils/schedule.js +219 -0
  164. package/dist/runtime/utils/statusStyle.d.ts +26 -2
  165. package/dist/runtime/utils/statusStyle.js +9 -1
  166. package/dist/runtime/utils/validationFindings.d.ts +101 -0
  167. package/dist/runtime/utils/validationFindings.js +54 -0
  168. package/dist/runtime/utils/whenIdle.d.ts +42 -0
  169. package/dist/runtime/utils/whenIdle.js +12 -0
  170. package/dist/types.d.mts +2 -6
  171. package/package.json +10 -3
@@ -1,15 +1,17 @@
1
1
  <script setup>
2
2
  import { computed, ref, watch } from "vue";
3
- import TemplateFlowPreview from "../../components/Integrations/TemplateFlowPreview.vue";
3
+ import TemplateCard from "../../components/Integrations/TemplateCard.vue";
4
4
  import WorkflowCreateDialog from "../../components/Integrations/WorkflowCreateDialog.vue";
5
5
  import { Pulse as Activity, DangerTriangle as AlertTriangle, ArrowRight, Box as Boxes, ClockCircle as Clock, Database, Layers, AddCircle as Plus, Radio, MagicStick as Sparkles, LinkRound as Webhook, Programming as Workflow } from "#solar-icons";
6
6
  import { Badge, Button } from "@revenexx/studio";
7
- useHead({ title: "Dashboard \xB7 Integration Studio" });
7
+ import { runStatusLabel, statusClass } from "../../utils/statusStyle";
8
+ const t = useIntegrationsT();
9
+ useHead({ title: () => t("integration.dashboard.head.title") });
8
10
  const { workflows, activeCount: activeWorkflows, loading: workflowsLoading, load: loadWorkflows } = useIntegrationsWorkflows();
9
11
  const { runs, runningCount, failedCount, loading: runsLoading, load: loadRuns } = useIntegrationsRuns();
10
- const { popular, loading: templatesLoading, load: loadTemplates, ensureDetails } = useIntegrationsTemplates();
11
- watch(() => popular.value.map((t) => t.slug).join(","), () => {
12
- if (popular.value.length) void ensureDetails(popular.value.map((t) => t.slug));
12
+ const { startingPoints, loading: templatesLoading, load: loadTemplates, ensureDetails } = useIntegrationsTemplates();
13
+ watch(() => startingPoints.value.map((t2) => t2.slug).join(","), () => {
14
+ if (startingPoints.value.length) void ensureDetails(startingPoints.value.map((t2) => t2.slug));
13
15
  }, { immediate: true });
14
16
  const { activeCount: schedulesActive, load: loadSchedules } = useIntegrationsSchedules();
15
17
  const { pendingCount: missedRuns, globalLoading: missedLoading, loadGlobal: loadMissedRuns } = useIntegrationsDeadLetters();
@@ -24,107 +26,104 @@ onMounted(() => {
24
26
  void loadSchedules();
25
27
  void loadMissedRuns();
26
28
  });
27
- const latestPerWorkflow = computed(() => {
29
+ const latestByWorkflow = computed(() => {
28
30
  const seen = /* @__PURE__ */ new Set();
29
31
  const out = [];
30
32
  for (const r of runs.value) {
31
33
  if (seen.has(r.workflowId)) continue;
32
34
  seen.add(r.workflowId);
33
35
  out.push(r);
34
- if (out.length >= 10) break;
35
36
  }
36
37
  return out;
37
38
  });
38
- const failedRecent = computed(() => runs.value.filter((r) => r.status === "failed").slice(0, 3));
39
+ const latestPerWorkflow = computed(() => latestByWorkflow.value.slice(0, 10));
40
+ const failedRecent = computed(() => latestByWorkflow.value.filter((r) => r.status === "failed").slice(0, 3));
39
41
  function formatRelative(iso) {
40
42
  if (!iso) return "\u2014";
41
43
  const ms = Date.now() - new Date(iso).getTime();
42
- if (ms < 6e4) return "just now";
43
- if (ms < 36e5) return `${Math.floor(ms / 6e4)}m ago`;
44
- if (ms < 864e5) return `${Math.floor(ms / 36e5)}h ago`;
45
- return `${Math.floor(ms / 864e5)}d ago`;
46
- }
47
- function statusClass(status) {
48
- switch (status) {
49
- case "succeeded":
50
- return "bg-emerald-500/15 text-emerald-700 border-emerald-500/20";
51
- case "running":
52
- return "bg-sky-500/15 text-sky-700 border-sky-500/20";
53
- case "failed":
54
- return "bg-rose-500/15 text-rose-700 border-rose-500/20";
55
- case "queued":
56
- return "bg-amber-500/15 text-amber-700 border-amber-500/20";
57
- case "cancelled":
58
- return "bg-slate-500/15 text-slate-700 border-slate-500/20";
59
- }
44
+ if (ms < 6e4) return t("integration.common.time.justNow");
45
+ if (ms < 36e5) return t("integration.common.time.minutesAgo", Math.floor(ms / 6e4));
46
+ if (ms < 864e5) return t("integration.common.time.hoursAgo", Math.floor(ms / 36e5));
47
+ return t("integration.common.time.daysAgo", Math.floor(ms / 864e5));
60
48
  }
61
49
  const kpis = computed(() => [
62
50
  {
63
- label: "Active workflows",
51
+ key: "activeWorkflows",
52
+ label: t("integration.dashboard.kpi.activeWorkflows.label"),
64
53
  value: activeWorkflows.value,
65
- sub: `${workflows.value.length - activeWorkflows.value} paused or drafts`,
54
+ // A plural key, so the count is the second argument. Called without one it
55
+ // would put the raw "a | b" on screen.
56
+ sub: t("integration.dashboard.kpi.activeWorkflows.sub", workflows.value.length - activeWorkflows.value),
66
57
  icon: Workflow,
67
58
  tile: "bg-emerald-500 text-white"
68
59
  },
69
60
  {
70
- label: "Running now",
61
+ key: "runningNow",
62
+ label: t("integration.dashboard.kpi.runningNow.label"),
71
63
  value: runningCount.value,
72
- sub: "live executions in flight",
64
+ sub: t("integration.dashboard.kpi.runningNow.sub"),
73
65
  icon: Activity,
74
66
  tile: "bg-sky-500 text-white"
75
67
  },
76
68
  {
77
- label: "Scheduled",
69
+ key: "scheduled",
70
+ label: t("integration.dashboard.kpi.scheduled.label"),
78
71
  value: schedulesActive.value,
79
- sub: "cron triggers active",
72
+ sub: t("integration.dashboard.kpi.scheduled.sub"),
80
73
  icon: Clock,
81
74
  tile: "bg-violet-500 text-white"
82
75
  },
83
76
  {
84
- label: "Missed runs",
77
+ key: "missedRuns",
78
+ label: t("integration.dashboard.kpi.missedRuns.label"),
85
79
  value: missedRuns.value,
86
- sub: "trigger firings without a run",
80
+ sub: t("integration.dashboard.kpi.missedRuns.sub"),
87
81
  icon: AlertTriangle,
88
82
  tile: missedRuns.value ? "bg-amber-500 text-white" : "bg-slate-300 text-white",
89
83
  to: "/integrations/runs?tab=missed"
90
84
  }
91
85
  ]);
92
- const surfaces = [
93
- { label: "Import & Export", to: "/integrations/io", icon: Database, hint: "Profiles that move catalogue and entity data in and out." },
94
- { label: "SFTP Server", to: "/integrations/sftp", icon: Boxes, hint: "Managed SFTP endpoints for file-based exchange." },
95
- { label: "Event Stream", to: "/integrations/events", icon: Radio, hint: "Live platform events that workflows subscribe to." },
96
- { label: "Webhooks", to: "/integrations/webhooks", icon: Webhook, hint: "Inbound HTTP endpoints that trigger workflows." }
97
- ];
86
+ const surfaces = computed(() => [
87
+ { label: t("integration.dashboard.surface.io.label"), to: "/integrations/io", icon: Database, hint: t("integration.dashboard.surface.io.hint") },
88
+ { label: t("integration.dashboard.surface.sftp.label"), to: "/integrations/sftp", icon: Boxes, hint: t("integration.dashboard.surface.sftp.hint") },
89
+ { label: t("integration.dashboard.surface.events.label"), to: "/integrations/events", icon: Radio, hint: t("integration.dashboard.surface.events.hint") },
90
+ { label: t("integration.dashboard.surface.webhooks.label"), to: "/integrations/webhooks", icon: Webhook, hint: t("integration.dashboard.surface.webhooks.hint") }
91
+ ]);
98
92
  </script>
99
93
 
100
94
  <template>
101
95
  <IntegrationsLayout>
102
96
  <PageHeader
103
- title="Dashboard"
104
- description="Everything that connects Revenue Cloud to the outside world."
97
+ :title="t('integration.dashboard.header.title')"
98
+ :description="t('integration.dashboard.header.description')"
105
99
  >
106
100
  <template #actions>
107
101
  <NuxtLink to="/integrations/templates">
108
102
  <Button variant="outline" class="h-7 text-sm">
109
103
  <Layers :size="12" />
110
- Browse templates
104
+ {{ t("integration.dashboard.action.browseTemplates") }}
111
105
  </Button>
112
106
  </NuxtLink>
113
107
  <Button class="h-7 text-sm" @click="createOpen = true">
114
108
  <Plus :size="14" />
115
- New workflow
109
+ {{ t("integration.dashboard.action.newWorkflow") }}
116
110
  </Button>
117
111
  </template>
118
112
  </PageHeader>
119
113
 
120
114
  <div class="p-4 space-y-4">
121
- <!-- KPI strip -->
122
- <div class="grid gap-2 sm:grid-cols-2 lg:grid-cols-4">
115
+ <!-- KPI strip. Each tile carries its figure in its own name: the Missed-runs
116
+ tile is a link, and a link takes its name from its label instead of its
117
+ contents — named "Missed runs" alone it would announce less than the
118
+ three tiles beside it. -->
119
+ <div role="group" :aria-label="t('integration.dashboard.kpi.aria.group')" class="grid gap-2 sm:grid-cols-2 lg:grid-cols-4">
123
120
  <component
124
121
  :is="k.to ? NuxtLinkComp : 'div'"
125
122
  v-for="k in kpis"
126
- :key="k.label"
123
+ :key="k.key"
127
124
  :to="k.to"
125
+ :role="k.to ? void 0 : 'group'"
126
+ :aria-label="`${k.label}: ${k.value}`"
128
127
  class="block rounded-lg border border-black/[0.06] bg-white p-3"
129
128
  :class="k.to ? 'transition hover:border-black/20' : ''"
130
129
  >
@@ -148,30 +147,34 @@ const surfaces = [
148
147
  </component>
149
148
  </div>
150
149
 
151
- <!-- Two-column: recent runs + needs attention + popular templates -->
150
+ <!-- Two-column: recent runs + needs attention + templates to start from -->
152
151
  <div class="grid gap-4 lg:grid-cols-3">
153
152
  <!-- Recent runs ─ takes 2 cols -->
154
- <div class="lg:col-span-2 rounded-lg border border-black/[0.06] bg-white">
153
+ <div role="group" :aria-label="t('integration.dashboard.runs.title')" class="lg:col-span-2 rounded-lg border border-black/[0.06] bg-white">
155
154
  <div class="flex items-center justify-between border-b border-black/[0.06] px-3 py-2">
156
155
  <div class="flex items-center gap-2">
157
156
  <span class="flex size-6 items-center justify-center rounded-md bg-slate-100 text-slate-500">
158
157
  <Activity :size="12" />
159
158
  </span>
160
- <h3 class="text-sm font-semibold">Latest executions</h3>
159
+ <h3 class="text-sm font-semibold">{{ t("integration.dashboard.runs.title") }}</h3>
161
160
  </div>
162
161
  <NuxtLink to="/integrations/runs" class="text-sm text-muted-foreground hover:text-foreground inline-flex items-center gap-1">
163
- View all <ArrowRight :size="11" />
162
+ {{ t("integration.common.action.viewAll") }} <ArrowRight :size="11" />
164
163
  </NuxtLink>
165
164
  </div>
166
165
  <div>
167
- <div
166
+ <!-- An entry is a link, not a div that answers a click: that is what
167
+ makes it reachable by keyboard, and it is what gives it a name —
168
+ the values it shows. A name written onto a plain row would be
169
+ heard by nobody, because ARIA does not name a generic element. -->
170
+ <NuxtLink
168
171
  v-for="r in latestPerWorkflow"
169
172
  :key="r.id"
170
- class="grid grid-cols-[90px_minmax(0,1fr)_100px_70px] items-center gap-2 border-b border-black/[0.04] px-3 py-2 text-sm last:border-b-0 cursor-pointer hover:bg-muted/30"
171
- @click="$router.push({ path: `/integrations/workflows/${r.workflowId}`, query: { run: r.id } })"
173
+ :to="{ path: `/integrations/workflows/${r.workflowId}`, query: { run: r.id } }"
174
+ class="grid grid-cols-[90px_minmax(0,1fr)_100px_70px] items-center gap-2 border-b border-black/[0.04] px-3 py-2 text-sm last:border-b-0 hover:bg-muted/30"
172
175
  >
173
176
  <Badge variant="outline" :class="['border text-xs font-medium uppercase', statusClass(r.status)]">
174
- {{ r.status }}
177
+ {{ runStatusLabel(r.status) }}
175
178
  </Badge>
176
179
  <div class="truncate">
177
180
  <div class="font-medium text-foreground truncate">{{ r.workflowName }}</div>
@@ -183,7 +186,7 @@ const surfaces = [
183
186
  <div class="text-sm text-muted-foreground tabular-nums text-right">
184
187
  {{ r.durationMs == null ? "\u2026" : `${(r.durationMs / 1e3).toFixed(2)}s` }}
185
188
  </div>
186
- </div>
189
+ </NuxtLink>
187
190
  <template v-if="latestLoading && !latestPerWorkflow.length">
188
191
  <div
189
192
  v-for="n in 6"
@@ -200,19 +203,19 @@ const surfaces = [
200
203
  </div>
201
204
  </template>
202
205
  <div v-else-if="!latestPerWorkflow.length" class="px-3 py-8 text-center text-sm text-muted-foreground">
203
- No runs yet.
206
+ {{ t("integration.dashboard.runs.empty") }}
204
207
  </div>
205
208
  </div>
206
209
  </div>
207
210
 
208
211
  <!-- Needs attention -->
209
- <div class="rounded-lg border border-black/[0.06] bg-white">
212
+ <div role="group" :aria-label="t('integration.dashboard.attention.title')" class="rounded-lg border border-black/[0.06] bg-white">
210
213
  <div class="flex items-center justify-between border-b border-black/[0.06] px-3 py-2">
211
214
  <div class="flex items-center gap-2">
212
215
  <span class="flex size-6 items-center justify-center rounded-md bg-slate-100 text-slate-500">
213
216
  <AlertTriangle :size="12" />
214
217
  </span>
215
- <h3 class="text-sm font-semibold">Needs attention</h3>
218
+ <h3 class="text-sm font-semibold">{{ t("integration.dashboard.attention.title") }}</h3>
216
219
  </div>
217
220
  </div>
218
221
  <div v-if="(missedLoading || latestLoading) && !missedRuns && !failedRecent.length" class="space-y-2 p-3">
@@ -225,28 +228,32 @@ const surfaces = [
225
228
  to="/integrations/runs?tab=missed"
226
229
  class="block space-y-1 border-b border-black/[0.04] px-3 py-2 text-sm hover:bg-muted/30"
227
230
  >
228
- <div class="font-medium">{{ missedRuns }} missed run{{ missedRuns === 1 ? "" : "s" }}</div>
231
+ <div class="font-medium">{{ t("integration.dashboard.attention.missedRuns", missedRuns) }}</div>
229
232
  <div class="text-sm text-amber-700">
230
- Triggers fired while their workflow couldn't run — review &amp; replay.
233
+ {{ t("integration.dashboard.attention.missedRuns.hint") }}
231
234
  </div>
232
235
  </NuxtLink>
233
- <div
236
+ <!-- A link, for the same reason as the entries next door — and like
237
+ the waiting-firings entry above it, which was one all along. -->
238
+ <NuxtLink
234
239
  v-for="r in failedRecent"
235
240
  :key="r.id"
236
- class="space-y-1 border-b border-black/[0.04] px-3 py-2 text-sm last:border-b-0 cursor-pointer hover:bg-muted/30"
237
- @click="$router.push(`/integrations/workflows/${r.workflowId}`)"
241
+ :to="`/integrations/workflows/${r.workflowId}`"
242
+ class="block space-y-1 border-b border-black/[0.04] px-3 py-2 text-sm last:border-b-0 hover:bg-muted/30"
238
243
  >
239
244
  <div class="font-medium truncate">{{ r.workflowName }}</div>
240
245
  <div class="text-sm text-rose-700 truncate" :title="r.errorMessage">
241
- {{ r.errorMessage ?? "Failed." }}
246
+ {{ r.errorMessage ?? t("integration.dashboard.attention.failed") }}
242
247
  </div>
248
+ <!-- When it failed is when it ended, not when it set off: a run
249
+ that broke after an hour would otherwise be dated an hour early. -->
243
250
  <div class="text-sm text-muted-foreground tabular-nums">
244
- {{ formatRelative(r.startedAt) }}
251
+ {{ formatRelative(r.finishedAt ?? r.startedAt) }}
245
252
  </div>
246
- </div>
253
+ </NuxtLink>
247
254
  </div>
248
255
  <div v-else class="px-3 py-6 text-center text-sm text-muted-foreground">
249
- All workflows healthy.
256
+ {{ t("integration.dashboard.attention.healthy") }}
250
257
  </div>
251
258
  </div>
252
259
  </div>
@@ -272,49 +279,41 @@ const surfaces = [
272
279
  </NuxtLink>
273
280
  </div>
274
281
 
275
- <!-- Popular templates -->
276
- <div class="rounded-lg border border-black/[0.06] bg-white">
282
+ <!-- Named for the offer it makes, not for a ranking nothing measures
283
+ (PO-238): every template's install count was nought, so the sort that
284
+ produced this list compared nothing at all. The wording deliberately
285
+ echoes the create-workflow dialog's "Start from template" tab — the same
286
+ offer, so the same words, rather than a second name for it. -->
287
+ <div role="group" :aria-label="t('integration.dashboard.templates.title')" class="rounded-lg border border-black/[0.06] bg-white">
277
288
  <div class="flex items-center justify-between border-b border-black/[0.06] px-3 py-2">
278
289
  <div class="flex items-center gap-2">
279
290
  <span class="flex size-6 items-center justify-center rounded-md bg-slate-100 text-slate-500">
280
291
  <Sparkles :size="12" />
281
292
  </span>
282
- <h3 class="text-sm font-semibold">Popular templates</h3>
293
+ <h3 class="text-sm font-semibold">{{ t("integration.dashboard.templates.title") }}</h3>
283
294
  </div>
284
295
  <NuxtLink to="/integrations/templates" class="text-sm text-muted-foreground hover:text-foreground inline-flex items-center gap-1">
285
- See all <ArrowRight :size="11" />
296
+ {{ t("integration.common.action.seeAll") }} <ArrowRight :size="11" />
286
297
  </NuxtLink>
287
298
  </div>
288
- <div v-if="templatesLoading && !popular.length" class="grid gap-2 p-3 sm:grid-cols-2 lg:grid-cols-4">
299
+ <div v-if="templatesLoading && !startingPoints.length" class="grid gap-2 p-3 sm:grid-cols-2 lg:grid-cols-4">
289
300
  <div v-for="n in 4" :key="`sk-tpl-${n}`" class="rounded-md border border-black/[0.06] bg-white p-3 space-y-2">
290
301
  <div class="h-5 w-20 rounded bg-black/[0.07] animate-pulse" />
291
302
  <div class="h-3 w-3/4 rounded bg-black/[0.07] animate-pulse" />
292
303
  <div class="h-3 w-1/2 rounded bg-black/[0.05] animate-pulse" />
293
304
  </div>
294
305
  </div>
295
- <div v-else-if="!popular.length" class="px-3 py-6 text-center text-sm text-muted-foreground">
296
- No templates registered yet.
306
+ <div v-else-if="!startingPoints.length" class="px-3 py-6 text-center text-sm text-muted-foreground">
307
+ {{ t("integration.dashboard.templates.empty") }}
297
308
  </div>
298
309
  <div v-else class="grid gap-2 p-3 sm:grid-cols-2 lg:grid-cols-4">
299
- <NuxtLink
300
- v-for="tpl in popular"
310
+ <TemplateCard
311
+ v-for="tpl in startingPoints"
301
312
  :key="tpl.slug"
313
+ :template="tpl"
314
+ density="compact"
302
315
  :to="`/integrations/templates/${tpl.slug}`"
303
- class="flex flex-col overflow-hidden rounded-md border border-black/[0.06] bg-white transition hover:border-black/20 hover:shadow-sm"
304
- >
305
- <div class="relative h-20 shrink-0 border-b border-black/[0.06] bg-slate-50/70 [background-image:radial-gradient(circle,rgba(15,23,42,0.07)_1px,transparent_1px)] [background-size:12px_12px]">
306
- <TemplateFlowPreview v-if="tpl.detailLoaded" :blob="tpl.blob" :height="80" class="px-1" />
307
- <span class="absolute left-1.5 top-1.5 inline-flex items-center whitespace-nowrap rounded-full bg-slate-100 px-1.5 py-px text-[11px] font-medium uppercase text-slate-700">
308
- {{ tpl.category }}
309
- </span>
310
- </div>
311
- <div class="flex min-h-0 flex-1 flex-col p-3">
312
- <div class="text-sm font-medium truncate">{{ tpl.name }}</div>
313
- <p class="mt-1 text-sm text-muted-foreground line-clamp-2">
314
- {{ tpl.description }}
315
- </p>
316
- </div>
317
- </NuxtLink>
316
+ />
318
317
  </div>
319
318
  </div>
320
319
  </div>
@@ -1,10 +1,12 @@
1
1
  <script setup>
2
- import { RefreshCircle as Loader2, Refresh as ReplayIcon, TrashBinMinimalistic as Trash2 } from "#solar-icons";
2
+ import { RefreshCircle as Loader2, Refresh as ReplayIcon, TrashBinMinimalistic as TrashBin } from "#solar-icons";
3
3
  import { Badge, Button } from "@revenexx/studio";
4
4
  import { toast } from "vue-sonner";
5
5
  import RunsTable from "../../components/Integrations/RunsTable.vue";
6
+ import { createPollBudget, SAFETY_POLL_MS } from "../../utils/pollBudget";
6
7
  import { statusClass } from "../../utils/statusStyle";
7
- useHead({ title: "Runs \xB7 Integration Studio" });
8
+ const t = useIntegrationsT();
9
+ useHead({ title: () => t("integration.runs.list.head.title") });
8
10
  const route = useRoute();
9
11
  const { runs, loading, load } = useIntegrationsRuns();
10
12
  const {
@@ -17,18 +19,28 @@ const {
17
19
  } = useIntegrationsDeadLetters();
18
20
  const tab = ref(route.query.tab === "missed" ? "missed" : "runs");
19
21
  const tabs = computed(() => [
20
- { key: "runs", label: "Runs" },
22
+ { key: "runs", label: t("integration.runs.tabs.runs.label") },
21
23
  // "Missed runs": trigger firings that never became a run (dead letters).
22
- { key: "missed", label: "Missed runs", badge: pendingCount.value || null }
24
+ { key: "missed", label: t("integration.deadLetters.tabs.missed.label"), badge: pendingCount.value || null }
23
25
  ]);
26
+ const POLL_MS = 3e3;
24
27
  const hasActiveRuns = computed(() => runs.value.some((r) => r.status === "running" || r.status === "queued"));
28
+ const { delivering, runTick, workflowTick } = useIntegrationsRealtime();
29
+ const budget = createPollBudget({ intervalMs: POLL_MS, liveIntervalMs: SAFETY_POLL_MS });
30
+ function reload() {
31
+ budget.spend();
32
+ void load(true);
33
+ }
34
+ watch([runTick, workflowTick], () => {
35
+ reload();
36
+ });
25
37
  let pollTimer = null;
26
38
  onMounted(() => {
27
39
  void load(true);
28
40
  void loadGlobal(true);
29
41
  pollTimer = setInterval(() => {
30
- if (hasActiveRuns.value) void load(true);
31
- }, 3e3);
42
+ if (hasActiveRuns.value && !loading.value && budget.due(delivering.value)) void load(true);
43
+ }, POLL_MS);
32
44
  });
33
45
  onUnmounted(() => {
34
46
  if (pollTimer) {
@@ -42,11 +54,11 @@ async function onReplay(dl) {
42
54
  try {
43
55
  const res = await replay(dl.id);
44
56
  const newId = res && typeof res === "object" && "id" in res ? String(res.id) : null;
45
- toast.success(newId ? `Replayed \u2014 started run #${newId}` : "Replayed");
57
+ toast.success(newId ? t("integration.deadLetters.toast.replay.started", { id: newId }) : t("integration.deadLetters.toast.replay.done"));
46
58
  await loadGlobal(true);
47
- void load(true);
59
+ reload();
48
60
  } catch (e) {
49
- toast.error(e?.message ?? "Could not replay.");
61
+ toast.error(e?.message ?? t("integration.deadLetters.toast.replay.error"));
50
62
  } finally {
51
63
  busyId.value = null;
52
64
  }
@@ -55,10 +67,10 @@ async function onDiscard(dl) {
55
67
  busyId.value = dl.id;
56
68
  try {
57
69
  await discard(dl.id);
58
- toast.success("Discarded");
70
+ toast.success(t("integration.deadLetters.toast.discard.done"));
59
71
  await loadGlobal(true);
60
72
  } catch (e) {
61
- toast.error(e?.message ?? "Could not discard.");
73
+ toast.error(e?.message ?? t("integration.deadLetters.toast.discard.error"));
62
74
  } finally {
63
75
  busyId.value = null;
64
76
  }
@@ -76,25 +88,25 @@ function fmt(iso) {
76
88
  <template>
77
89
  <IntegrationsLayout>
78
90
  <PageHeader
79
- title="Runs"
80
- description="Execution history across every workflow — sourced from Temporal."
91
+ :title="t('integration.runs.list.header.title')"
92
+ :description="t('integration.runs.list.header.description')"
81
93
  />
82
94
 
83
95
  <TabBar v-model="tab" :tabs="tabs" class="px-4" />
84
96
 
85
97
  <div class="p-4 space-y-4">
86
- <RunsTable v-if="tab === 'runs'" :runs="runs" :loading="loading" @changed="() => load(true)" />
98
+ <RunsTable v-if="tab === 'runs'" :runs="runs" :loading="loading" @changed="reload" />
87
99
 
88
100
  <!-- Missed runs — trigger firings that never produced a run (the
89
101
  workflow wasn't runnable when its schedule/event fired). -->
90
102
  <template v-else>
91
103
  <div class="flex items-center justify-between">
92
104
  <p class="text-sm text-muted-foreground">
93
- Triggers that fired while their workflow couldn't run (bundle building or failed). Replay starts a fresh run from the captured payload.
105
+ {{ t("integration.deadLetters.list.description") }}
94
106
  </p>
95
107
  <Button variant="outline" size="sm" class="h-7 text-sm" :disabled="missedLoading" @click="() => loadGlobal(true)">
96
108
  <Loader2 v-if="missedLoading" :size="12" class="animate-spin" />
97
- Refresh
109
+ {{ t("integration.common.action.refresh") }}
98
110
  </Button>
99
111
  </div>
100
112
 
@@ -109,7 +121,7 @@ function fmt(iso) {
109
121
  </div>
110
122
  </div>
111
123
  <div v-else-if="!missed.length" class="rounded-lg border border-black/[0.06] bg-white p-6 text-center text-sm text-muted-foreground">
112
- No missed runs. Every trigger firing made it into a run.
124
+ {{ t("integration.deadLetters.state.empty") }}
113
125
  </div>
114
126
 
115
127
  <div v-else class="rounded-lg border border-black/[0.06] bg-white divide-y divide-black/[0.04]">
@@ -123,7 +135,7 @@ function fmt(iso) {
123
135
  <NuxtLink :to="`/integrations/workflows/${dl.workflow_id}`" class="text-sm font-medium truncate transition-colors hover:text-primary">
124
136
  {{ dl.workflowName }}
125
137
  </NuxtLink>
126
- <div class="text-sm break-words">{{ dl.reason || "No reason recorded." }}</div>
138
+ <div class="text-sm break-words">{{ dl.reason || t("integration.deadLetters.reason.fallback") }}</div>
127
139
  <div class="mt-0.5 flex flex-wrap items-center gap-x-3 gap-y-0.5 text-xs text-muted-foreground font-mono">
128
140
  <span>#{{ dl.id }}</span>
129
141
  <span v-if="dl.source">{{ dl.source }}</span>
@@ -135,11 +147,11 @@ function fmt(iso) {
135
147
  <Button variant="outline" size="sm" class="h-7 text-sm" :disabled="busyId === dl.id" @click="onReplay(dl)">
136
148
  <Loader2 v-if="busyId === dl.id" :size="12" class="animate-spin" />
137
149
  <ReplayIcon v-else :size="12" />
138
- Replay
150
+ {{ t("integration.deadLetters.action.replay") }}
139
151
  </Button>
140
152
  <Button variant="ghost" size="sm" class="h-7 text-sm text-rose-600 hover:bg-rose-500/10" :disabled="busyId === dl.id" @click="onDiscard(dl)">
141
- <Trash2 :size="12" />
142
- Discard
153
+ <TrashBin :size="12" />
154
+ {{ t("integration.deadLetters.action.discard") }}
143
155
  </Button>
144
156
  </div>
145
157
  </div>