@revenexx/studio-integrations 0.7.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/CHANGELOG.md +633 -0
  2. package/dist/module.json +1 -1
  3. package/dist/module.mjs +2 -1
  4. package/dist/runtime/components/Integrations/CredentialCreateDialog.vue +4 -1
  5. package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +8 -2
  6. package/dist/runtime/components/Integrations/DeadLettersPanel.vue +2 -6
  7. package/dist/runtime/components/Integrations/ExpressionInput.vue +5 -0
  8. package/dist/runtime/components/Integrations/NodeConfigDynamicField.d.vue.ts +3 -0
  9. package/dist/runtime/components/Integrations/NodeConfigDynamicField.vue +16 -3
  10. package/dist/runtime/components/Integrations/NodeConfigDynamicField.vue.d.ts +3 -0
  11. package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.vue +1 -0
  12. package/dist/runtime/components/Integrations/NodeConfigField.d.vue.ts +3 -0
  13. package/dist/runtime/components/Integrations/NodeConfigField.vue +2 -0
  14. package/dist/runtime/components/Integrations/NodeConfigField.vue.d.ts +3 -0
  15. package/dist/runtime/components/Integrations/NodeInspector.d.vue.ts +2 -2
  16. package/dist/runtime/components/Integrations/NodeInspector.vue +135 -10
  17. package/dist/runtime/components/Integrations/NodeInspector.vue.d.ts +2 -2
  18. package/dist/runtime/components/Integrations/NodeOutputSchema.d.vue.ts +2 -1
  19. package/dist/runtime/components/Integrations/NodeOutputSchema.vue +34 -8
  20. package/dist/runtime/components/Integrations/NodeOutputSchema.vue.d.ts +2 -1
  21. package/dist/runtime/components/Integrations/NodePalette.vue +12 -8
  22. package/dist/runtime/components/Integrations/RunDetailPanel.vue +9 -6
  23. package/dist/runtime/components/Integrations/RunsTable.vue +2 -2
  24. package/dist/runtime/components/Integrations/StickyNoteView.vue +45 -11
  25. package/dist/runtime/components/Integrations/SurfaceCard.d.vue.ts +50 -0
  26. package/dist/runtime/components/Integrations/SurfaceCard.vue +55 -0
  27. package/dist/runtime/components/Integrations/SurfaceCard.vue.d.ts +50 -0
  28. package/dist/runtime/components/Integrations/TemplateCard.vue +3 -3
  29. package/dist/runtime/components/Integrations/TemplateFlowPreview.vue +12 -3
  30. package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +5 -4
  31. package/dist/runtime/components/Integrations/WorkflowEditor.d.vue.ts +16 -0
  32. package/dist/runtime/components/Integrations/WorkflowEditor.vue +79 -12
  33. package/dist/runtime/components/Integrations/WorkflowEditor.vue.d.ts +16 -0
  34. package/dist/runtime/components/Integrations/WorkflowHistoryPanel.d.vue.ts +6 -0
  35. package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue +13 -3
  36. package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue.d.ts +6 -0
  37. package/dist/runtime/components/Integrations/WorkflowNodeView.d.vue.ts +13 -0
  38. package/dist/runtime/components/Integrations/WorkflowNodeView.vue +95 -29
  39. package/dist/runtime/components/Integrations/WorkflowNodeView.vue.d.ts +13 -0
  40. package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +11 -10
  41. package/dist/runtime/components/Integrations/WorkflowSettingsDialog.d.vue.ts +16 -17
  42. package/dist/runtime/components/Integrations/WorkflowSettingsDialog.vue +37 -26
  43. package/dist/runtime/components/Integrations/WorkflowSettingsDialog.vue.d.ts +16 -17
  44. package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +4 -5
  45. package/dist/runtime/components/Integrations/WorkflowVariablesDialog.vue +53 -14
  46. package/dist/runtime/components/Integrations/WorkflowsTable.vue +9 -4
  47. package/dist/runtime/composables/useIntegrationsCredentials.d.ts +11 -0
  48. package/dist/runtime/composables/useIntegrationsCredentials.js +2 -2
  49. package/dist/runtime/composables/useIntegrationsDeadLetters.d.ts +9 -2
  50. package/dist/runtime/composables/useIntegrationsDeadLetters.js +27 -18
  51. package/dist/runtime/composables/useIntegrationsNodes.js +11 -1
  52. package/dist/runtime/composables/useIntegrationsRuns.d.ts +30 -1
  53. package/dist/runtime/composables/useIntegrationsRuns.js +70 -29
  54. package/dist/runtime/composables/useIntegrationsSchedules.d.ts +7 -14
  55. package/dist/runtime/composables/useIntegrationsSchedules.js +80 -35
  56. package/dist/runtime/composables/useIntegrationsTriggers.d.ts +30 -3
  57. package/dist/runtime/composables/useIntegrationsTriggers.js +18 -7
  58. package/dist/runtime/composables/useIntegrationsWorkflows.d.ts +10 -1
  59. package/dist/runtime/composables/useIntegrationsWorkflows.js +25 -4
  60. package/dist/runtime/composables/useNodeConfigResolve.js +2 -1
  61. package/dist/runtime/data/integrationNodeCatalog.d.ts +22 -1
  62. package/dist/runtime/data/integrationNodeCatalog.js +8 -4
  63. package/dist/runtime/data/loopBody.d.ts +129 -0
  64. package/dist/runtime/data/loopBody.js +76 -0
  65. package/dist/runtime/data/nodeInputData.d.ts +58 -1
  66. package/dist/runtime/data/nodeInputData.js +30 -5
  67. package/dist/runtime/data/triggerGlyphs.d.ts +47 -0
  68. package/dist/runtime/data/triggerGlyphs.js +11 -0
  69. package/dist/runtime/i18n/messages/de.json +62 -33
  70. package/dist/runtime/i18n/messages/en.json +32 -3
  71. package/dist/runtime/pages/integrations/credentials.vue +4 -1
  72. package/dist/runtime/pages/integrations/index.vue +32 -34
  73. package/dist/runtime/pages/integrations/runs.vue +22 -10
  74. package/dist/runtime/pages/integrations/schedules.vue +64 -7
  75. package/dist/runtime/pages/integrations/templates/[slug].vue +13 -5
  76. package/dist/runtime/pages/integrations/workflows/[id].vue +193 -104
  77. package/dist/runtime/types/integrations.d.ts +64 -1
  78. package/dist/runtime/utils/cockpitSources.d.ts +22 -3
  79. package/dist/runtime/utils/credentialState.d.ts +6 -1
  80. package/dist/runtime/utils/credentialState.js +1 -1
  81. package/dist/runtime/utils/localize.d.ts +34 -0
  82. package/dist/runtime/utils/localize.js +22 -7
  83. package/dist/runtime/utils/pollBudget.d.ts +1 -1
  84. package/dist/runtime/utils/servedPaths.d.ts +22 -0
  85. package/dist/runtime/utils/servedPaths.js +3 -0
  86. package/dist/runtime/utils/statusStyle.d.ts +52 -0
  87. package/dist/runtime/utils/statusStyle.js +26 -0
  88. package/package.json +1 -1
@@ -4,7 +4,7 @@ import { Badge, Button } from "@revenexx/studio";
4
4
  import { toast } from "vue-sonner";
5
5
  import RunsTable from "../../components/Integrations/RunsTable.vue";
6
6
  import { createPollBudget, SAFETY_POLL_MS } from "../../utils/pollBudget";
7
- import { statusClass } from "../../utils/statusStyle";
7
+ import { deadLetterStatusClass, deadLetterStatusLabel } from "../../utils/statusStyle";
8
8
  const t = useIntegrationsT();
9
9
  useHead({ title: () => t("integration.runs.list.head.title") });
10
10
  const route = useRoute();
@@ -37,7 +37,7 @@ watch([runTick, workflowTick], () => {
37
37
  let pollTimer = null;
38
38
  onMounted(() => {
39
39
  void load(true);
40
- void loadGlobal(true);
40
+ void reloadMissedRuns();
41
41
  pollTimer = setInterval(() => {
42
42
  if (hasActiveRuns.value && !loading.value && budget.due(delivering.value)) void load(true);
43
43
  }, POLL_MS);
@@ -49,31 +49,43 @@ onUnmounted(() => {
49
49
  }
50
50
  });
51
51
  const busyId = ref(null);
52
+ async function reloadMissedRuns() {
53
+ try {
54
+ await loadGlobal(true);
55
+ } catch {
56
+ }
57
+ }
52
58
  async function onReplay(dl) {
53
59
  busyId.value = dl.id;
54
60
  try {
55
61
  const res = await replay(dl.id);
56
62
  const newId = res && typeof res === "object" && "id" in res ? String(res.id) : null;
57
63
  toast.success(newId ? t("integration.deadLetters.toast.replay.started", { id: newId }) : t("integration.deadLetters.toast.replay.done"));
58
- await loadGlobal(true);
59
- reload();
60
64
  } catch (e) {
61
65
  toast.error(e?.message ?? t("integration.deadLetters.toast.replay.error"));
62
- } finally {
63
66
  busyId.value = null;
67
+ return;
64
68
  }
69
+ await reloadMissedRuns();
70
+ reload();
71
+ busyId.value = null;
65
72
  }
66
73
  async function onDiscard(dl) {
67
74
  busyId.value = dl.id;
68
75
  try {
69
76
  await discard(dl.id);
70
77
  toast.success(t("integration.deadLetters.toast.discard.done"));
71
- await loadGlobal(true);
72
78
  } catch (e) {
73
79
  toast.error(e?.message ?? t("integration.deadLetters.toast.discard.error"));
74
- } finally {
75
80
  busyId.value = null;
81
+ return;
76
82
  }
83
+ await reloadMissedRuns();
84
+ busyId.value = null;
85
+ }
86
+ function workflowNameOf(dl) {
87
+ const name = dl.workflow_name;
88
+ return typeof name === "string" && name.trim() !== "" ? name : t("integration.deadLetters.workflow.fallback", { id: dl.workflow_id });
77
89
  }
78
90
  function fmt(iso) {
79
91
  if (!iso) return "\u2014";
@@ -104,7 +116,7 @@ function fmt(iso) {
104
116
  <p class="text-sm text-muted-foreground">
105
117
  {{ t("integration.deadLetters.list.description") }}
106
118
  </p>
107
- <Button variant="outline" size="sm" class="h-7 text-sm" :disabled="missedLoading" @click="() => loadGlobal(true)">
119
+ <Button variant="outline" size="sm" class="h-7 text-sm" :disabled="missedLoading" @click="() => reloadMissedRuns()">
108
120
  <Loader2 v-if="missedLoading" :size="12" class="animate-spin" />
109
121
  {{ t("integration.common.action.refresh") }}
110
122
  </Button>
@@ -130,10 +142,10 @@ function fmt(iso) {
130
142
  :key="dl.id"
131
143
  class="flex items-start gap-3 px-3 py-2.5"
132
144
  >
133
- <Badge variant="outline" :class="['border text-xs font-medium uppercase shrink-0 mt-0.5', statusClass(dl.status)]">{{ dl.status }}</Badge>
145
+ <Badge variant="outline" :class="['border text-xs font-medium uppercase shrink-0 mt-0.5', deadLetterStatusClass(dl.status)]">{{ deadLetterStatusLabel(dl.status) }}</Badge>
134
146
  <div class="min-w-0 flex-1">
135
147
  <NuxtLink :to="`/integrations/workflows/${dl.workflow_id}`" class="text-sm font-medium truncate transition-colors hover:text-primary">
136
- {{ dl.workflowName }}
148
+ {{ workflowNameOf(dl) }}
137
149
  </NuxtLink>
138
150
  <div class="text-sm break-words">{{ dl.reason || t("integration.deadLetters.reason.fallback") }}</div>
139
151
  <div class="mt-0.5 flex flex-wrap items-center gap-x-3 gap-y-0.5 text-xs text-muted-foreground font-mono">
@@ -2,14 +2,23 @@
2
2
  import { ClockCircle as Clock, Globus as Globe } from "#solar-icons";
3
3
  import { Badge } from "@revenexx/studio";
4
4
  import { cadenceRank, describeCron } from "../../utils/schedule";
5
+ import { runStatusLabel } from "../../utils/statusStyle";
5
6
  const t = useIntegrationsT();
6
7
  useHead({ title: () => t("integration.schedules.head.title") });
7
8
  const router = useRouter();
8
- const { entries, activeCount, loading, load } = useIntegrationsSchedules();
9
+ const { entries, activeCount, timezonesInUse, loading, error, load } = useIntegrationsSchedules();
9
10
  onMounted(() => {
10
11
  void load(true);
11
12
  });
12
- function formatRelative(iso) {
13
+ function inScheduleZone(iso, timezone) {
14
+ const parts = { month: "short", day: "2-digit", hour: "2-digit", minute: "2-digit" };
15
+ try {
16
+ return new Date(iso).toLocaleString(void 0, { ...parts, timeZone: timezone });
17
+ } catch {
18
+ return new Date(iso).toLocaleString(void 0, parts);
19
+ }
20
+ }
21
+ function formatRelative(iso, timezone) {
13
22
  if (!iso) return "\u2014";
14
23
  const ms = new Date(iso).getTime() - Date.now();
15
24
  if (Math.abs(ms) < 6e4) return ms > 0 ? t("integration.schedules.time.inLessThanMinute") : t("integration.common.time.justNow");
@@ -21,7 +30,11 @@ function formatRelative(iso) {
21
30
  const hrs = Math.round(Math.abs(ms) / 36e5);
22
31
  return ms > 0 ? t("integration.schedules.time.inHours", hrs) : t("integration.common.time.hoursAgo", hrs);
23
32
  }
24
- return new Date(iso).toLocaleString(void 0, { month: "short", day: "2-digit", hour: "2-digit", minute: "2-digit" });
33
+ return inScheduleZone(iso, timezone);
34
+ }
35
+ function lastRunText(e) {
36
+ if (e.lastRunAt) return formatRelative(e.lastRunAt, e.timezone);
37
+ return e.lastRunStatus ? runStatusLabel(e.lastRunStatus) : "\u2014";
25
38
  }
26
39
  const filterTabs = computed(() => [
27
40
  {
@@ -40,7 +53,10 @@ const filterTabs = computed(() => [
40
53
  key: "timezone",
41
54
  label: t("integration.schedules.table.column.timezone"),
42
55
  getter: (e) => e.timezone,
43
- options: [...new Set(entries.value.map((e) => e.timezone))].sort().map((tz) => ({ value: tz, label: tz }))
56
+ // The zones the platform reports as in use across the tenant, not the ones
57
+ // visible in the rows: with the list narrowed, deriving them from the rows
58
+ // would shrink the menu to what the previous choice left standing.
59
+ options: timezonesInUse.value.map((tz) => ({ value: tz, label: tz }))
44
60
  }
45
61
  ]);
46
62
  const columns = computed(() => [
@@ -76,7 +92,25 @@ const columns = computed(() => [
76
92
  </template>
77
93
  </PageHeader>
78
94
 
95
+ <!--
96
+ A refused read says so. Without this the page fell through to the table's
97
+ own empty text, so a 503 and a tenant with no schedules were one screen —
98
+ and the message it needed was already written and translated
99
+ (`specs/schedules.md` AC-13). An alert rather than a bare div, so it is
100
+ announced and not only visible.
101
+ -->
102
+ <div v-if="error" role="alert" class="m-4 rounded-md border border-rose-500/20 bg-rose-500/5 px-3 py-2 text-sm text-rose-700">
103
+ {{ error }}
104
+ </div>
105
+
79
106
  <div class="p-4">
107
+ <!--
108
+ The empty text answers the error too, so the table cannot go on saying
109
+ "No schedules yet" underneath a banner explaining that there was no
110
+ answer to count. Two renderings of one fact rather than two facts: the
111
+ banner carries the platform's own words, which may be specific, and this
112
+ is why the list is blank.
113
+ -->
80
114
  <DataTable
81
115
  :loading="loading"
82
116
  :data="entries"
@@ -85,7 +119,7 @@ const columns = computed(() => [
85
119
  :default-sort="{ column: 'nextRun', direction: 'asc' }"
86
120
  :search="{ placeholder: t('integration.schedules.search.placeholder') }"
87
121
  :filter-tabs="filterTabs"
88
- :empty-text="t('integration.schedules.state.empty')"
122
+ :empty-text="error ? t('integration.schedules.error.loadFailed') : t('integration.schedules.state.empty')"
89
123
  @row-click="(e2) => router.push(`/integrations/workflows/${e2.workflowId}`)"
90
124
  >
91
125
  <template #cell-workflow="{ row: e }">
@@ -143,12 +177,35 @@ const columns = computed(() => [
143
177
  semantics in the shared table (PO-233, and a gap in
144
178
  `specs/accessibility.md`).
145
179
  -->
180
+ <!--
181
+ The instant the platform holds, shown as a distance from now, with the
182
+ moment in the schedule's own zone as its title: "in 6d" answers when,
183
+ and "2026-08-24T08:30:00+02:00" answers which side of a clock change it
184
+ falls on. Neither is worked out here — a browser doing summer-time
185
+ arithmetic that a wrong answer makes worse than no answer is exactly
186
+ what this replaced.
187
+ -->
146
188
  <template #cell-nextRun="{ row: e }">
147
- <span :aria-label="t('integration.schedules.table.column.nextRun')" class="text-sm tabular-nums">{{ formatRelative(e.nextRunAt) }}</span>
189
+ <span
190
+ :aria-label="t('integration.schedules.table.column.nextRun')"
191
+ :title="e.nextRunLocal ?? void 0"
192
+ class="text-sm tabular-nums"
193
+ >{{ formatRelative(e.nextRunAt, e.timezone) }}</span>
148
194
  </template>
149
195
 
196
+ <!--
197
+ The last run carries a title too, and it is the instant rather than a
198
+ zone reading: the platform reports no local form for a run the way it
199
+ does for a firing, and inventing one here is what this page stopped
200
+ doing. Without it a run further back than a day was readable to the
201
+ minute and to no greater precision than the cell's own text.
202
+ -->
150
203
  <template #cell-lastRun="{ row: e }">
151
- <span :aria-label="t('integration.schedules.table.column.lastRun')" class="text-sm tabular-nums text-muted-foreground">{{ formatRelative(e.lastRunAt) }}</span>
204
+ <span
205
+ :aria-label="t('integration.schedules.table.column.lastRun')"
206
+ :title="e.lastRunAt ?? void 0"
207
+ class="text-sm tabular-nums text-muted-foreground"
208
+ >{{ lastRunText(e) }}</span>
152
209
  </template>
153
210
 
154
211
  <template #cell-status="{ row: e }">
@@ -1,7 +1,8 @@
1
1
  <script setup>
2
2
  import { computed, reactive, ref } from "vue";
3
3
  import { marked } from "marked";
4
- import { AltArrowRight as Chevron, Bolt as Zap, CheckCircle as CheckCircle2, ClockCircle as Clock, Global as Webhook, Key, Play, RefreshCircle as Loader2, Shield as ShieldAlert, User } from "#solar-icons";
4
+ import { AltArrowRight as Chevron, CheckCircle as CheckCircle2, Key, RefreshCircle as Loader2, Shield as ShieldAlert, User } from "#solar-icons";
5
+ import { triggerGlyph as glyphForTrigger } from "../../../data/triggerGlyphs";
5
6
  import {
6
7
  Badge,
7
8
  Button,
@@ -90,7 +91,6 @@ function firstSentence(text) {
90
91
  const idx = text.indexOf(". ");
91
92
  return idx === -1 ? text : text.slice(0, idx + 1);
92
93
  }
93
- const TRIGGER_ICON = { schedule: Clock, webhook: Webhook, manual: Play, event: Zap };
94
94
  const triggerChips = computed(() => (tpl.value?.blob.triggers ?? []).map((trig) => {
95
95
  let label = trig.type.charAt(0).toUpperCase() + trig.type.slice(1);
96
96
  if (trig.type === "schedule" && trig.config?.cron) {
@@ -99,7 +99,7 @@ const triggerChips = computed(() => (tpl.value?.blob.triggers ?? []).map((trig)
99
99
  }
100
100
  if (trig.type === "webhook" && trig.config?.path) label = `${t("integration.templates.trigger.webhook")} \xB7 ${trig.config.path}`;
101
101
  if (trig.type === "manual") label = t("integration.templates.trigger.manual");
102
- return { key: trig.id, icon: TRIGGER_ICON[trig.type] ?? Zap, label };
102
+ return { key: trig.id, icon: glyphForTrigger(trig.type), label };
103
103
  }));
104
104
  const variables = computed(() => Object.entries(tpl.value?.blob.variables ?? {}).map(([key, v]) => ({
105
105
  key,
@@ -206,8 +206,16 @@ function levelDot(level) {
206
206
  </PageHeader>
207
207
 
208
208
  <div class="p-4 space-y-4">
209
- <!-- Hero: the live workflow preview on the editor's dotted canvas -->
210
- <div class="relative overflow-hidden rounded-lg border border-black/[0.06] bg-slate-50/70 [background-image:radial-gradient(circle,rgba(15,23,42,0.07)_1px,transparent_1px)] [background-size:14px_14px]">
209
+ <!-- Hero: the live workflow preview on the editor's dotted canvas.
210
+ Named like the two panels below it, with `aria-label` because it
211
+ carries no heading to point at: it was the largest thing on the page
212
+ and the only one with no name at all, which also left it the one
213
+ surface drawing the miniature that nothing could address (PO-247). -->
214
+ <div
215
+ role="region"
216
+ :aria-label="t('integration.templates.detail.flow.ariaLabel')"
217
+ class="relative overflow-hidden rounded-lg border border-black/[0.06] bg-slate-50/70 [background-image:radial-gradient(circle,rgba(15,23,42,0.07)_1px,transparent_1px)] [background-size:14px_14px]"
218
+ >
211
219
  <TemplateFlowPreview v-if="tpl.detailLoaded" :blob="tpl.blob" :height="400" class="px-4" />
212
220
  <div v-else class="flex h-[400px] items-center justify-center">
213
221
  <Loader2 :size="18" class="animate-spin text-muted-foreground/50" />