@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,7 +1,9 @@
1
1
  <script setup>
2
2
  import { ClockCircle as Clock, Globus as Globe } from "#solar-icons";
3
3
  import { Badge } from "@revenexx/studio";
4
- useHead({ title: "Schedules \xB7 Integration Studio" });
4
+ import { cadenceRank, describeCron } from "../../utils/schedule";
5
+ const t = useIntegrationsT();
6
+ useHead({ title: () => t("integration.schedules.head.title") });
5
7
  const router = useRouter();
6
8
  const { entries, activeCount, loading, load } = useIntegrationsSchedules();
7
9
  onMounted(() => {
@@ -10,36 +12,66 @@ onMounted(() => {
10
12
  function formatRelative(iso) {
11
13
  if (!iso) return "\u2014";
12
14
  const ms = new Date(iso).getTime() - Date.now();
13
- if (Math.abs(ms) < 6e4) return ms > 0 ? "in <1m" : "just now";
15
+ if (Math.abs(ms) < 6e4) return ms > 0 ? t("integration.schedules.time.inLessThanMinute") : t("integration.common.time.justNow");
14
16
  if (Math.abs(ms) < 36e5) {
15
17
  const mins = Math.round(Math.abs(ms) / 6e4);
16
- return ms > 0 ? `in ${mins}m` : `${mins}m ago`;
18
+ return ms > 0 ? t("integration.schedules.time.inMinutes", mins) : t("integration.common.time.minutesAgo", mins);
17
19
  }
18
20
  if (Math.abs(ms) < 864e5) {
19
21
  const hrs = Math.round(Math.abs(ms) / 36e5);
20
- return ms > 0 ? `in ${hrs}h` : `${hrs}h ago`;
22
+ return ms > 0 ? t("integration.schedules.time.inHours", hrs) : t("integration.common.time.hoursAgo", hrs);
21
23
  }
22
24
  return new Date(iso).toLocaleString(void 0, { month: "short", day: "2-digit", hour: "2-digit", minute: "2-digit" });
23
25
  }
24
- const columns = [
25
- { key: "workflow", label: "Workflow", sortable: true, sortGetter: (e) => e.workflowName.toLowerCase(), searchGetter: (e) => `${e.workflowName} ${e.cron} ${e.timezone}` },
26
- { key: "cron", label: "Cron", width: "180px", sortable: true, sortGetter: (e) => e.cron },
27
- { key: "timezone", label: "Timezone", width: "140px", sortable: true, sortGetter: (e) => e.timezone },
28
- { key: "nextRun", label: "Next run", width: "140px", sortable: true, sortGetter: (e) => e.nextRunAt ?? "" },
29
- { key: "lastRun", label: "Last run", width: "140px", sortable: true, sortGetter: (e) => e.lastRunAt ?? "" },
30
- { key: "status", label: "Status", width: "80px", sortable: true, sortGetter: (e) => e.active ? "live" : "paused" }
31
- ];
26
+ const filterTabs = computed(() => [
27
+ {
28
+ key: "status",
29
+ label: t("integration.schedules.table.column.status"),
30
+ // The flag, not the word: a stored value has to survive a language switch, or the
31
+ // narrowed list empties itself when the host changes language. Only the labels are
32
+ // translated (see `credentials.vue` for the whole reasoning).
33
+ getter: (e) => e.active ? "live" : "paused",
34
+ options: [
35
+ { value: "live", label: t("integration.schedules.status.live") },
36
+ { value: "paused", label: t("integration.schedules.status.paused") }
37
+ ]
38
+ },
39
+ {
40
+ key: "timezone",
41
+ label: t("integration.schedules.table.column.timezone"),
42
+ getter: (e) => e.timezone,
43
+ options: [...new Set(entries.value.map((e) => e.timezone))].sort().map((tz) => ({ value: tz, label: tz }))
44
+ }
45
+ ]);
46
+ const columns = computed(() => [
47
+ // Two columns share the slack, and the sentence gets the larger share of it:
48
+ // "Weekdays, hourly from 09:00 to 17:00" is what this page is for, while a
49
+ // workflow name is a label the reader recognises from its first few words.
50
+ // Both flex, so no width is left unused at the end of the row.
51
+ //
52
+ // The expression stays in the search text although the column no longer shows
53
+ // it: support quotes expressions, and a row has to be findable by the string
54
+ // someone was given.
55
+ { key: "workflow", label: t("integration.schedules.table.column.workflow"), width: "minmax(180px, 1fr)", sortable: true, sortGetter: (e) => e.workflowName.toLowerCase(), searchGetter: (e) => `${e.workflowName} ${e.cron ?? ""} ${e.timezone} ${describeCron(e.cron) ?? ""}` },
56
+ // Sorted by how often a schedule fires. The expression's characters would be
57
+ // meaningless here — the column shows a sentence.
58
+ { key: "cron", label: t("integration.schedules.table.column.when"), width: "minmax(260px, 1.4fr)", sortable: true, sortGetter: (e) => cadenceRank(e.cron) },
59
+ { key: "timezone", label: t("integration.schedules.table.column.timezone"), width: "140px", sortable: true, sortGetter: (e) => e.timezone },
60
+ { key: "nextRun", label: t("integration.schedules.table.column.nextRun"), width: "140px", sortable: true, sortGetter: (e) => e.nextRunAt ?? "" },
61
+ { key: "lastRun", label: t("integration.schedules.table.column.lastRun"), width: "140px", sortable: true, sortGetter: (e) => e.lastRunAt ?? "" },
62
+ { key: "status", label: t("integration.schedules.table.column.status"), width: "80px", sortable: true, sortGetter: (e) => e.active ? "live" : "paused" }
63
+ ]);
32
64
  </script>
33
65
 
34
66
  <template>
35
67
  <IntegrationsLayout>
36
68
  <PageHeader
37
- title="Schedules"
38
- description="Every cron-driven trigger across your workflows. Click a row to open the workflow."
69
+ :title="t('integration.schedules.header.title')"
70
+ :description="t('integration.schedules.header.description')"
39
71
  >
40
72
  <template #actions>
41
73
  <span class="text-sm text-muted-foreground">
42
- <strong class="text-foreground">{{ activeCount }}</strong> active
74
+ <strong class="text-foreground">{{ activeCount }}</strong> {{ t("integration.schedules.stat.active") }}
43
75
  </span>
44
76
  </template>
45
77
  </PageHeader>
@@ -51,9 +83,9 @@ const columns = [
51
83
  :columns="columns"
52
84
  :row-key="(e2) => e2.id"
53
85
  :default-sort="{ column: 'nextRun', direction: 'asc' }"
54
- :search="{ placeholder: 'Search by workflow, cron, timezone\u2026' }"
55
- :filter="true"
56
- empty-text="No schedules yet. Add a schedule trigger to any workflow."
86
+ :search="{ placeholder: t('integration.schedules.search.placeholder') }"
87
+ :filter-tabs="filterTabs"
88
+ :empty-text="t('integration.schedules.state.empty')"
57
89
  @row-click="(e2) => router.push(`/integrations/workflows/${e2.workflowId}`)"
58
90
  >
59
91
  <template #cell-workflow="{ row: e }">
@@ -67,8 +99,33 @@ const columns = [
67
99
  </div>
68
100
  </template>
69
101
 
102
+ <!--
103
+ The sentence is the answer, and the expression is not on the page: the
104
+ question this column asks is "when does this run", which an expression
105
+ answers in a language the reader may not speak. It is still one hover
106
+ away — support quotes expressions — and it still stands alone in the one
107
+ case where nothing else would be true: a schedule the studio cannot say
108
+ in words, which is one no picker built either.
109
+ -->
70
110
  <template #cell-cron="{ row: e }">
71
- <code class="font-mono text-sm truncate">{{ e.cron }}</code>
111
+ <div class="min-w-0">
112
+ <!-- The sentence wraps rather than truncating: a schedule that fires
113
+ at several named times is a long sentence, and half of one is
114
+ the wrong half. Its title is the expression, so there is no
115
+ tooltip left to carry the rest of the words. -->
116
+ <div v-if="describeCron(e.cron)" :aria-label="t('integration.schedules.cell.whenItFires.aria')" :title="e.cron ?? void 0" class="text-sm leading-snug">
117
+ {{ describeCron(e.cron) }}
118
+ </div>
119
+ <template v-else-if="e.cron">
120
+ <div :aria-label="t('integration.schedules.cell.whenItFires.aria')" class="text-sm truncate">
121
+ {{ t("integration.schedules.cell.customSchedule") }}
122
+ </div>
123
+ <code :aria-label="t('integration.schedules.cell.cronExpression.aria')" class="block font-mono text-xs text-muted-foreground truncate">{{ e.cron }}</code>
124
+ </template>
125
+ <div v-else :aria-label="t('integration.schedules.cell.whenItFires.aria')" class="text-sm text-muted-foreground truncate">
126
+ {{ t("integration.schedules.cell.noSchedule") }}
127
+ </div>
128
+ </div>
72
129
  </template>
73
130
 
74
131
  <template #cell-timezone="{ row: e }">
@@ -78,20 +135,28 @@ const columns = [
78
135
  </div>
79
136
  </template>
80
137
 
138
+ <!--
139
+ The two time cells carry the name their column header states, so each is
140
+ addressable by what it is rather than by how it is typeset. It is not the
141
+ accessible name a screen reader hears — the cell is a plain span, and
142
+ ARIA prohibits naming a generic element — that needs row and cell
143
+ semantics in the shared table (PO-233, and a gap in
144
+ `specs/accessibility.md`).
145
+ -->
81
146
  <template #cell-nextRun="{ row: e }">
82
- <span class="text-sm tabular-nums">{{ formatRelative(e.nextRunAt) }}</span>
147
+ <span :aria-label="t('integration.schedules.table.column.nextRun')" class="text-sm tabular-nums">{{ formatRelative(e.nextRunAt) }}</span>
83
148
  </template>
84
149
 
85
150
  <template #cell-lastRun="{ row: e }">
86
- <span class="text-sm tabular-nums text-muted-foreground">{{ formatRelative(e.lastRunAt) }}</span>
151
+ <span :aria-label="t('integration.schedules.table.column.lastRun')" class="text-sm tabular-nums text-muted-foreground">{{ formatRelative(e.lastRunAt) }}</span>
87
152
  </template>
88
153
 
89
154
  <template #cell-status="{ row: e }">
90
155
  <Badge v-if="e.active" variant="outline" class="bg-emerald-500/10 border-emerald-500/20 text-emerald-700 text-xs font-medium uppercase">
91
- live
156
+ {{ t("integration.schedules.status.live") }}
92
157
  </Badge>
93
158
  <Badge v-else variant="outline" class="text-xs font-medium uppercase">
94
- paused
159
+ {{ t("integration.schedules.status.paused") }}
95
160
  </Badge>
96
161
  </template>
97
162
  </DataTable>
@@ -1,5 +1,5 @@
1
1
  <script setup>
2
- import { Eye, EyeClosed as EyeOff, Key as KeyRound, RefreshCircle as Loader2, MenuDots as MoreHorizontal, AddCircle as Plus, TrashBinMinimalistic as Trash2 } from "#solar-icons";
2
+ import { Eye, EyeClosed as EyeOff, Key as KeyRound, RefreshCircle as Loader2, MenuDots as MoreHorizontal, AddCircle as Plus, TrashBinMinimalistic as TrashBin } from "#solar-icons";
3
3
  import {
4
4
  AlertDialog,
5
5
  AlertDialogContent,
@@ -24,7 +24,8 @@ import {
24
24
  Input,
25
25
  Label
26
26
  } from "@revenexx/studio";
27
- useHead({ title: "Secrets \xB7 Revenexx Cockpit" });
27
+ const t = useIntegrationsT();
28
+ useHead({ title: () => t("integration.secrets.head.title") });
28
29
  const { secrets, loading, error, isValidKey, upsert, remove, load } = useIntegrationsSecrets();
29
30
  onMounted(() => {
30
31
  void load();
@@ -45,11 +46,11 @@ function openCreate() {
45
46
  async function submitCreate() {
46
47
  const key = newKey.value.trim();
47
48
  if (!isValidKey(key)) {
48
- formError.value = "Key must match ^[A-Z0-9_]+$ (uppercase letters, digits, underscores).";
49
+ formError.value = t("integration.secrets.form.error.invalidKey");
49
50
  return;
50
51
  }
51
52
  if (!newValue.value) {
52
- formError.value = "Value is required.";
53
+ formError.value = t("integration.secrets.error.valueRequired");
53
54
  return;
54
55
  }
55
56
  saving.value = true;
@@ -71,47 +72,47 @@ async function confirmDelete() {
71
72
  deleting.value = false;
72
73
  pendingDelete.value = null;
73
74
  }
74
- function formatDateTime(iso) {
75
+ function formatDate(iso) {
75
76
  if (!iso) return "\u2014";
76
77
  try {
77
- return new Date(iso).toLocaleString(void 0, { month: "short", day: "2-digit", year: "numeric", hour: "2-digit", minute: "2-digit" });
78
+ return new Date(iso).toLocaleDateString(void 0, { month: "short", day: "2-digit", year: "numeric" });
78
79
  } catch {
79
80
  return iso;
80
81
  }
81
82
  }
82
- const columns = [
83
- { key: "key", label: "Key", sortable: true, sortGetter: (s) => s.key.toLowerCase() },
84
- { key: "created", label: "Created", width: "160px", sortable: true, sortGetter: (s) => s.created_at },
85
- { key: "updated", label: "Updated", width: "160px", sortable: true, sortGetter: (s) => s.updated_at },
83
+ const columns = computed(() => [
84
+ { key: "key", label: t("integration.secrets.table.column.key"), sortable: true, sortGetter: (s) => s.key.toLowerCase() },
85
+ { key: "created", label: t("integration.secrets.table.column.created"), width: "160px", sortable: true, sortGetter: (s) => s.created_at, searchGetter: () => "" },
86
+ { key: "updated", label: t("integration.secrets.table.column.updated"), width: "160px", sortable: true, sortGetter: (s) => s.updated_at, searchGetter: () => "" },
86
87
  { key: "actions", label: "", width: "40px", toggleable: false }
87
- ];
88
+ ]);
88
89
  </script>
89
90
 
90
91
  <template>
91
92
  <IntegrationsLayout>
92
93
  <PageHeader
93
- title="Secrets"
94
- description="Per-tenant encrypted key/value store. Values are encrypted at rest and never returned by the API."
94
+ :title="t('integration.secrets.header.title')"
95
+ :description="t('integration.secrets.header.description')"
95
96
  >
96
97
  <template #actions>
97
98
  <Dialog v-model:open="createOpen">
98
99
  <DialogTrigger as-child>
99
100
  <Button class="h-7 text-sm" @click="openCreate">
100
101
  <Plus :size="14" />
101
- New secret
102
+ {{ t("integration.secrets.action.new") }}
102
103
  </Button>
103
104
  </DialogTrigger>
104
105
  <DialogContent>
105
106
  <DialogHeader>
106
- <DialogTitle>Create a secret</DialogTitle>
107
+ <DialogTitle>{{ t("integration.secrets.dialog.create.title") }}</DialogTitle>
107
108
  <DialogDescription class="text-sm">
108
- Once stored, the value is encrypted with AES-256-GCM and never echoed back. You can overwrite it later, but to read it you have to know it.
109
+ {{ t("integration.secrets.dialog.create.description") }}
109
110
  </DialogDescription>
110
111
  </DialogHeader>
111
112
 
112
113
  <div class="space-y-4 py-2">
113
114
  <div class="space-y-1.5">
114
- <Label for="sec-key">Key</Label>
115
+ <Label for="sec-key">{{ t("integration.secrets.form.key.label") }}</Label>
115
116
  <Input
116
117
  id="sec-key"
117
118
  v-model="newKey"
@@ -120,11 +121,11 @@ const columns = [
120
121
  autocomplete="off"
121
122
  />
122
123
  <p class="text-sm text-muted-foreground">
123
- Pattern: <code class="font-mono">^[A-Z0-9_]+$</code>
124
+ {{ t("integration.secrets.form.key.patternLabel") }} <code class="font-mono">^[A-Z0-9_]+$</code>
124
125
  </p>
125
126
  </div>
126
127
  <div class="space-y-1.5">
127
- <Label for="sec-value">Value</Label>
128
+ <Label for="sec-value">{{ t("integration.secrets.form.value.label") }}</Label>
128
129
  <div class="relative">
129
130
  <Input
130
131
  id="sec-value"
@@ -136,6 +137,7 @@ const columns = [
136
137
  <button
137
138
  type="button"
138
139
  class="absolute right-1 top-1/2 -translate-y-1/2 flex size-7 items-center justify-center rounded text-muted-foreground hover:text-foreground"
140
+ :aria-label="showValue ? t('integration.secrets.form.value.hide') : t('integration.secrets.form.value.show')"
139
141
  @click="showValue = !showValue"
140
142
  >
141
143
  <component :is="showValue ? EyeOff : Eye" :size="14" />
@@ -150,11 +152,11 @@ const columns = [
150
152
 
151
153
  <DialogFooter>
152
154
  <Button variant="outline" :disabled="saving" @click="createOpen = false">
153
- Cancel
155
+ {{ t("integration.common.action.cancel") }}
154
156
  </Button>
155
157
  <Button :disabled="saving" @click="submitCreate">
156
158
  <Loader2 v-if="saving" :size="14" class="animate-spin" />
157
- {{ saving ? "Saving\u2026" : "Save" }}
159
+ {{ saving ? t("integration.common.state.saving") : t("integration.common.action.save") }}
158
160
  </Button>
159
161
  </DialogFooter>
160
162
  </DialogContent>
@@ -172,9 +174,8 @@ const columns = [
172
174
  :columns="columns"
173
175
  :row-key="(s2) => s2.key"
174
176
  :default-sort="{ column: 'key', direction: 'asc' }"
175
- :search="{ placeholder: 'Search keys\u2026' }"
176
- :filter="true"
177
- empty-text="No secrets yet."
177
+ :search="{ placeholder: t('integration.secrets.search.placeholder') }"
178
+ :empty-text="t('integration.secrets.state.empty')"
178
179
  >
179
180
  <template #cell-key="{ row: s }">
180
181
  <div class="flex items-center gap-2 min-w-0">
@@ -183,33 +184,54 @@ const columns = [
183
184
  </div>
184
185
  <div class="min-w-0">
185
186
  <div class="font-mono text-sm truncate">{{ s.key }}</div>
186
- <div class="text-sm text-muted-foreground">Value masked — write-only</div>
187
+ <div class="text-sm text-muted-foreground">{{ t("integration.secrets.cell.masked") }}</div>
187
188
  </div>
188
189
  </div>
189
190
  </template>
190
191
 
192
+ <!--
193
+ `time` carries the full timestamp the cell no longer prints, and the
194
+ label says which date this is and whose — the table is a div grid, so
195
+ a date read on its own is otherwise a number without a column.
196
+
197
+ A date the service did not send is a plain span: `time` promises a
198
+ machine-readable moment, either in `datetime` or as its own text, and
199
+ the dash is neither — `datetime=""` would be an empty promise instead
200
+ of none. The label stays either way, because it is what ties the cell
201
+ to its row and column.
202
+ -->
191
203
  <template #cell-created="{ row: s }">
192
- <span class="text-sm tabular-nums text-muted-foreground">{{ formatDateTime(s.created_at) }}</span>
204
+ <component
205
+ :is="s.created_at ? 'time' : 'span'"
206
+ :datetime="s.created_at || void 0"
207
+ :aria-label="t('integration.secrets.cell.created.aria', { key: s.key, date: formatDate(s.created_at) })"
208
+ class="text-sm tabular-nums whitespace-nowrap text-muted-foreground"
209
+ >{{ formatDate(s.created_at) }}</component>
193
210
  </template>
194
211
 
195
212
  <template #cell-updated="{ row: s }">
196
- <span class="text-sm tabular-nums text-muted-foreground">{{ formatDateTime(s.updated_at) }}</span>
213
+ <component
214
+ :is="s.updated_at ? 'time' : 'span'"
215
+ :datetime="s.updated_at || void 0"
216
+ :aria-label="t('integration.secrets.cell.updated.aria', { key: s.key, date: formatDate(s.updated_at) })"
217
+ class="text-sm tabular-nums whitespace-nowrap text-muted-foreground"
218
+ >{{ formatDate(s.updated_at) }}</component>
197
219
  </template>
198
220
 
199
221
  <template #cell-actions="{ row: s }">
200
222
  <div @click.stop>
201
223
  <DropdownMenu>
202
224
  <DropdownMenuTrigger as-child>
203
- <Button variant="ghost" size="icon" class="size-7">
225
+ <Button variant="ghost" size="icon" class="size-7" :aria-label="t('integration.secrets.cell.actions.aria', { key: s.key })">
204
226
  <MoreHorizontal :size="14" />
205
227
  </Button>
206
228
  </DropdownMenuTrigger>
207
229
  <DropdownMenuContent align="end">
208
- <DropdownMenuLabel>Actions</DropdownMenuLabel>
230
+ <DropdownMenuLabel>{{ t("integration.secrets.menu.label") }}</DropdownMenuLabel>
209
231
  <DropdownMenuSeparator />
210
232
  <DropdownMenuItem class="text-destructive focus:text-destructive" @select="pendingDelete = s">
211
- <Trash2 :size="14" />
212
- Delete
233
+ <TrashBin :size="14" />
234
+ {{ t("integration.common.action.delete") }}
213
235
  </DropdownMenuItem>
214
236
  </DropdownMenuContent>
215
237
  </DropdownMenu>
@@ -223,21 +245,21 @@ const columns = [
223
245
  }">
224
246
  <AlertDialogContent>
225
247
  <AlertDialogHeader>
226
- <AlertDialogTitle>Delete secret?</AlertDialogTitle>
248
+ <AlertDialogTitle>{{ t("integration.secrets.dialog.delete.title") }}</AlertDialogTitle>
227
249
  <AlertDialogDescription>
228
250
  <span v-if="pendingDelete">
229
251
  <span class="font-mono font-medium text-foreground">{{ pendingDelete.key }}</span>
230
- will be permanently removed. Workflows that reference it will break the next time they run.
252
+ {{ t("integration.secrets.dialog.delete.body") }}
231
253
  </span>
232
254
  </AlertDialogDescription>
233
255
  </AlertDialogHeader>
234
256
  <AlertDialogFooter>
235
257
  <Button variant="outline" :disabled="deleting" @click="pendingDelete = null">
236
- Cancel
258
+ {{ t("integration.common.action.cancel") }}
237
259
  </Button>
238
260
  <Button class="bg-destructive text-white hover:bg-destructive/90" :disabled="deleting" @click="confirmDelete">
239
261
  <Loader2 v-if="deleting" :size="14" class="animate-spin" />
240
- {{ deleting ? "Deleting\u2026" : "Delete" }}
262
+ {{ deleting ? t("integration.common.state.deleting") : t("integration.common.action.delete") }}
241
263
  </Button>
242
264
  </AlertDialogFooter>
243
265
  </AlertDialogContent>