@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
@@ -0,0 +1,83 @@
1
+ /**
2
+ * The studio's own interface strings, and the function that reads one.
3
+ *
4
+ * **Why the module carries a catalogue at all.** `@revenexx/studio-integrations`
5
+ * is published. It renders inside the cockpit, which has vue-i18n and a
6
+ * catalogue of its own, and it renders inside the playground host and any other
7
+ * consumer, which have neither. Its chrome — every heading, button, empty state
8
+ * and toast in `runtime/` — is the module's text, not the host's, so the module
9
+ * ships it. A host cannot be asked to carry `integration.*` keys it has never
10
+ * seen, and the module cannot reach into a catalogue it does not own.
11
+ *
12
+ * **Why not vue-i18n.** Adding it here would either duplicate the host's
13
+ * instance or force every consumer to install it. The resolution rules a flat
14
+ * catalogue actually needs are small — a fallback chain, `{named}`
15
+ * interpolation and the `singular | plural` pipe — and they are below, tested,
16
+ * in eighty lines. The message format is deliberately vue-i18n's, so the
17
+ * catalogue can be handed to the cockpit's tooling (or to a translator working
18
+ * from it) without a conversion step.
19
+ *
20
+ * **Where the language comes from.** {@link studioLocale}, which is the seam
21
+ * PO-229 built for the API's bilingual text — the host sets it once with
22
+ * `useIntegrationsLocale().setLocale()` or `studioIntegrations.locale`, and both
23
+ * halves of a screen follow the same value. Reading it here is also what makes
24
+ * the chrome reactive: every call site is inside a template or a `computed()`,
25
+ * so each becomes a subscriber to the language without a line changing.
26
+ */
27
+ /** The languages the module ships. Anything else falls back to English. */
28
+ export declare const MESSAGES: Readonly<Record<string, Record<string, string>>>;
29
+ /** The language every key is guaranteed to exist in. */
30
+ export declare const FALLBACK_LOCALE = "en";
31
+ /** Interpolation values for a message with `{placeholders}`. */
32
+ export type TranslateNamed = Record<string, unknown>;
33
+ /**
34
+ * The call shapes a caller may use — vue-i18n's, so that moving a key between
35
+ * this catalogue and the cockpit's is a copy and not a rewrite.
36
+ */
37
+ export interface Translate {
38
+ (key: string): string;
39
+ (key: string, named: TranslateNamed): string;
40
+ (key: string, plural: number): string;
41
+ (key: string, plural: number, named: TranslateNamed): string;
42
+ }
43
+ /**
44
+ * Which catalogue answers for a BCP-47 tag: the exact tag, then the language
45
+ * without its region (so `de-CH` reads `de` rather than English), then English.
46
+ */
47
+ export declare function messagesFor(locale: string): Record<string, string>;
48
+ /**
49
+ * The raw message for a key, or `undefined`.
50
+ *
51
+ * An empty value counts as missing — a translator clearing a cell must not
52
+ * blank the UI, it must fall through to English.
53
+ */
54
+ export declare function lookup(key: string, locale: string): string | undefined;
55
+ /** Replace `{name}` with the value given for it. An unknown name is left alone. */
56
+ export declare function interpolate(message: string, values: TranslateNamed): string;
57
+ /**
58
+ * Pick a branch out of `singular | plural`.
59
+ *
60
+ * vue-i18n's default rule, which is what the catalogue is written against: two
61
+ * branches select on `count === 1`, three add a zero branch. Both languages the
62
+ * module ships have two plural forms; the third branch exists so a catalogue
63
+ * that wants "no runs / one run / n runs" can say so without new machinery.
64
+ *
65
+ * A message without a pipe is returned whole — passing a count to a key that
66
+ * has no plural form is harmless, the reverse is not.
67
+ */
68
+ export declare function selectPlural(message: string, count: number): string;
69
+ /**
70
+ * Resolve a key in a named language. `translate` is this, in the studio's.
71
+ *
72
+ * A key with no message anywhere is returned unchanged, which is the same
73
+ * promise the host bridge in `@revenexx/studio-shared` makes: a missing key is
74
+ * a visible defect on screen and never a blank, and never a crash.
75
+ */
76
+ export declare function translateIn(locale: string, key: string, ...args: unknown[]): string;
77
+ /**
78
+ * Translate a key in the language the host set.
79
+ *
80
+ * Reads {@link studioLocale} on every call rather than closing over it, so a
81
+ * `computed()` that translated once re-runs on a language switch.
82
+ */
83
+ export declare const translate: Translate;
@@ -0,0 +1,41 @@
1
+ import { studioLocale } from "../utils/localize.js";
2
+ import de from "./messages/de.json" with { type: "json" };
3
+ import en from "./messages/en.json" with { type: "json" };
4
+ export const MESSAGES = Object.freeze({
5
+ en,
6
+ de
7
+ });
8
+ export const FALLBACK_LOCALE = "en";
9
+ export function messagesFor(locale) {
10
+ const wanted = (locale || "").toLowerCase();
11
+ const base = wanted.split("-")[0];
12
+ return MESSAGES[wanted] ?? MESSAGES[base] ?? MESSAGES[FALLBACK_LOCALE];
13
+ }
14
+ export function lookup(key, locale) {
15
+ const primary = messagesFor(locale)[key];
16
+ if (primary) return primary;
17
+ const fallback = MESSAGES[FALLBACK_LOCALE][key];
18
+ return fallback || void 0;
19
+ }
20
+ export function interpolate(message, values) {
21
+ if (!message.includes("{")) return message;
22
+ return message.replace(/\{(\w+)\}/g, (whole, name) => name in values ? String(values[name]) : whole);
23
+ }
24
+ export function selectPlural(message, count) {
25
+ if (!message.includes("|")) return message;
26
+ const branches = message.split("|").map((part) => part.trim());
27
+ if (branches.length === 2) return branches[count === 1 ? 0 : 1];
28
+ if (branches.length >= 3) return branches[count === 0 ? 0 : count === 1 ? 1 : 2];
29
+ return branches[0];
30
+ }
31
+ export function translateIn(locale, key, ...args) {
32
+ const message = lookup(key, locale);
33
+ if (message === void 0) return key;
34
+ const [first, second] = args;
35
+ const count = typeof first === "number" ? first : void 0;
36
+ const named = typeof first === "object" && first !== null ? first : second;
37
+ const branch = count === void 0 ? message : selectPlural(message, count);
38
+ const values = count === void 0 ? named ?? {} : { n: count, count, ...named ?? {} };
39
+ return interpolate(branch, values);
40
+ }
41
+ export const translate = ((key, ...args) => translateIn(studioLocale(), key, ...args));
@@ -11,7 +11,8 @@ import {
11
11
  } from "#solar-icons";
12
12
  import { Badge, Button } from "@revenexx/studio";
13
13
  import { toast } from "vue-sonner";
14
- useHead({ title: "Audit Log \xB7 Integration Studio" });
14
+ const t = useIntegrationsT();
15
+ useHead({ title: () => t("integration.audit.head.title") });
15
16
  const { entries, loading, error, hasMore, load, loadMore } = useIntegrationsAudit();
16
17
  const resourceTypes = ["credential", "secret", "workflow", "trigger", "workflow_run"];
17
18
  const actions = ["create", "update", "delete", "run.started", "run.finished", "run.failed"];
@@ -25,11 +26,28 @@ function actionTone(a) {
25
26
  if (a === "delete" || a === "run.failed") return "bg-rose-500/10 text-rose-700 border-rose-500/20";
26
27
  return "bg-violet-500/10 text-violet-700 border-violet-500/20";
27
28
  }
29
+ const resourceTypeKeys = {
30
+ credential: "integration.audit.resourceType.credential",
31
+ secret: "integration.audit.resourceType.secret",
32
+ workflow: "integration.audit.resourceType.workflow",
33
+ trigger: "integration.audit.resourceType.trigger",
34
+ workflow_run: "integration.audit.resourceType.workflowRun"
35
+ };
36
+ const actionKeys = {
37
+ "create": "integration.audit.action.create",
38
+ "update": "integration.audit.action.update",
39
+ "delete": "integration.audit.action.delete",
40
+ "run.started": "integration.audit.action.runStarted",
41
+ "run.finished": "integration.audit.action.runFinished",
42
+ "run.failed": "integration.audit.action.runFailed"
43
+ };
28
44
  function prettyAction(a) {
29
- return a.replace(".", " ");
45
+ const key = actionKeys[a];
46
+ return key ? t(key) : a.replace(".", " ");
30
47
  }
31
- function prettyType(t) {
32
- return t.replace("_", " ");
48
+ function prettyType(rt) {
49
+ const key = resourceTypeKeys[rt];
50
+ return key ? t(key) : rt.replace("_", " ");
33
51
  }
34
52
  function resourceName(e) {
35
53
  return e.metadata?.name || e.resource_id || "\u2014";
@@ -37,51 +55,105 @@ function resourceName(e) {
37
55
  function fmtDate(iso) {
38
56
  return iso ? new Date(iso).toLocaleString() : "\u2014";
39
57
  }
40
- const filterTabs = [
58
+ const filterTabs = computed(() => [
41
59
  {
42
60
  key: "resource",
43
- label: "Resource",
61
+ label: t("integration.audit.table.column.resource"),
44
62
  getter: (e) => e.resource_type,
45
- options: resourceTypes.map((t) => ({ value: t, label: prettyType(t) }))
63
+ options: resourceTypes.map((rt) => ({ value: rt, label: prettyType(rt) }))
46
64
  },
47
65
  {
48
66
  key: "action",
49
- label: "Action",
67
+ label: t("integration.audit.table.column.action"),
50
68
  getter: (e) => e.action,
51
69
  options: actions.map((a) => ({ value: a, label: prettyAction(a) }))
52
70
  }
53
- ];
54
- const columns = [
55
- { key: "action", label: "Action", width: "130px", sortable: true, sortGetter: (e) => e.action },
56
- { key: "resource", label: "Resource", sortable: true, sortGetter: (e) => `${e.resource_type} ${resourceName(e)}`, searchGetter: (e) => `${e.resource_type} ${resourceName(e)} ${e.action} ${e.actor_sub ?? ""}` },
57
- { key: "actor", label: "Actor", width: "170px", sortable: true, sortGetter: (e) => e.actor_sub ?? "" },
58
- { key: "when", label: "When", width: "180px", sortable: true, sortGetter: (e) => e.created_at }
59
- ];
71
+ ]);
72
+ const columns = computed(() => [
73
+ { key: "action", label: t("integration.audit.table.column.action"), width: "130px", sortable: true, sortGetter: (e) => e.action },
74
+ { key: "resource", label: t("integration.audit.table.column.resource"), sortable: true, sortGetter: (e) => `${e.resource_type} ${resourceName(e)}`, searchGetter: (e) => `${e.resource_type} ${resourceName(e)} ${e.action} ${e.actor_sub ?? ""}` },
75
+ { key: "actor", label: t("integration.audit.table.column.actor"), width: "170px", sortable: true, sortGetter: (e) => e.actor_sub ?? "" },
76
+ { key: "when", label: t("integration.audit.table.column.when"), width: "180px", sortable: true, sortGetter: (e) => e.created_at }
77
+ ]);
60
78
  const selected = ref(null);
61
- function copy(t) {
62
- if (t && navigator.clipboard) {
63
- navigator.clipboard.writeText(t);
64
- toast.success("Copied");
79
+ function copy(t2) {
80
+ if (t2 && navigator.clipboard) {
81
+ navigator.clipboard.writeText(t2);
82
+ toast.success(t("integration.audit.toast.copied"));
65
83
  }
66
84
  }
85
+ const panelEl = ref(null);
86
+ let opener = null;
87
+ function tabStops() {
88
+ const root = panelEl.value;
89
+ if (!root) return [];
90
+ const sel = 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
91
+ return [...root.querySelectorAll(sel)].filter((el) => el.offsetParent !== null);
92
+ }
93
+ function onKeydown(e) {
94
+ if (e.key === "Escape") {
95
+ selected.value = null;
96
+ return;
97
+ }
98
+ if (e.key !== "Tab") return;
99
+ const root = panelEl.value;
100
+ if (!root) return;
101
+ const stops = tabStops();
102
+ if (!stops.length) {
103
+ e.preventDefault();
104
+ root.focus();
105
+ return;
106
+ }
107
+ const first = stops[0];
108
+ const last = stops[stops.length - 1];
109
+ const active = document.activeElement;
110
+ const inside = active instanceof Node && root.contains(active);
111
+ if (e.shiftKey && (!inside || active === first || active === root)) {
112
+ e.preventDefault();
113
+ last.focus();
114
+ } else if (!e.shiftKey && (!inside || active === last)) {
115
+ e.preventDefault();
116
+ first.focus();
117
+ }
118
+ }
119
+ watch(selected, async (entry, previous) => {
120
+ if (entry) {
121
+ document.addEventListener("keydown", onKeydown);
122
+ if (!previous) opener = document.activeElement;
123
+ await nextTick();
124
+ panelEl.value?.focus();
125
+ } else {
126
+ document.removeEventListener("keydown", onKeydown);
127
+ if (opener?.isConnected) opener.focus();
128
+ opener = null;
129
+ }
130
+ });
131
+ onUnmounted(() => document.removeEventListener("keydown", onKeydown));
67
132
  </script>
68
133
 
69
134
  <template>
70
135
  <IntegrationsLayout>
71
- <PageHeader title="Audit Log" description="Who changed what, and when. Append-only, per-tenant — credentials, secrets, workflows, triggers and runs." />
136
+ <PageHeader :title="t('integration.audit.header.title')" :description="t('integration.audit.header.description')" />
72
137
 
73
138
  <div class="p-4 space-y-3">
74
139
  <p v-if="error" class="text-sm text-rose-700">{{ error }}</p>
75
140
 
141
+ <!-- `:pagination="false"` — one pagination, not two. The feed is loaded a
142
+ page at a time by "Load more" below; the table's own pager (on by
143
+ default, twenty rows) would then hold the entries just fetched behind a
144
+ control nobody asked for, so the operator presses "Load more" and sees
145
+ nothing change. `templates/[slug].vue` and `workflows/[id].vue` switch
146
+ it off for the same reason. -->
76
147
  <DataTable
77
148
  :data="entries"
78
149
  :columns="columns"
79
150
  :row-key="(e) => e.id"
80
151
  :default-sort="{ column: 'when', direction: 'desc' }"
81
- :search="{ placeholder: 'Search action, resource, actor\u2026' }"
152
+ :pagination="false"
153
+ :search="{ placeholder: t('integration.audit.search.placeholder') }"
82
154
  :filter-tabs="filterTabs"
83
155
  :loading="loading"
84
- empty-text="No audit entries match."
156
+ :empty-text="t('integration.audit.state.empty')"
85
157
  @row-click="(e) => selected = e"
86
158
  >
87
159
  <template #cell-action="{ row }">
@@ -97,7 +169,7 @@ function copy(t) {
97
169
  </div>
98
170
  </template>
99
171
  <template #cell-actor="{ row }">
100
- <span class="text-sm font-mono text-muted-foreground truncate">{{ row.actor_sub ?? "system" }}</span>
172
+ <span class="text-sm font-mono text-muted-foreground truncate">{{ row.actor_sub ?? t("integration.audit.actor.system") }}</span>
101
173
  </template>
102
174
  <template #cell-when="{ row }">
103
175
  <span class="text-sm text-muted-foreground">{{ fmtDate(row.created_at) }}</span>
@@ -105,42 +177,73 @@ function copy(t) {
105
177
  </DataTable>
106
178
 
107
179
  <div v-if="hasMore" class="flex justify-center pt-1">
108
- <Button variant="outline" size="sm" :disabled="loading" @click="loadMore()">{{ loading ? "Loading\u2026" : "Load more" }}</Button>
180
+ <Button variant="outline" size="sm" :disabled="loading" @click="loadMore()">{{ loading ? t("integration.common.state.loading") : t("integration.audit.action.loadMore") }}</Button>
109
181
  </div>
110
182
  </div>
111
183
 
112
184
  <!-- Detail flyout -->
113
185
  <Transition name="drawer">
114
- <div v-if="selected" class="fixed inset-0 z-50 flex" @keydown.esc="selected = null">
115
- <div class="flex-1 bg-black/20 backdrop-blur-[1px]" @click="selected = null" />
116
- <aside class="w-full max-w-md overflow-y-auto border-l border-black/[0.06] bg-white shadow-xl">
186
+ <div v-if="selected" class="fixed inset-0 z-50 flex">
187
+ <!-- Clicking away dismisses the panel, and that is a pointer gesture by
188
+ design: the two routes that do not need a pointer — Escape and
189
+ `Close entry details` — are both inside the panel, where the Tab trap
190
+ keeps the focus. So this element is deliberately out of reach.
191
+ `tabindex="-1"` says so at the element rather than leaving it to the
192
+ trap, and `aria-modal` on the panel beside it already keeps the whole
193
+ overlay out of what a screen reader is offered. It stays a button and
194
+ keeps its name so that the day the trap changes, the fallback is a
195
+ named control rather than a clickable div. -->
196
+ <button
197
+ type="button"
198
+ tabindex="-1"
199
+ :aria-label="t('integration.audit.panel.dismiss.aria')"
200
+ class="flex-1 bg-black/20 backdrop-blur-[1px]"
201
+ @click="selected = null"
202
+ />
203
+ <!-- A dialog rather than a complementary landmark (PO-245). It covers the
204
+ page, holds the focus and closes on Escape, which is what a dialog is;
205
+ announcing it as supporting content beside the page while trapping the
206
+ focus inside it would be two answers to one question. `aria-modal` is
207
+ what tells a screen reader not to wander into what the overlay hides.
208
+ `tabindex="-1"` is what makes the container focusable at all, so
209
+ opening the panel can put the operator in it — see `onKeydown` above.
210
+ Still named: the shell has an <aside> for the sidebar, and this panel
211
+ needs to be tellable from it and addressable by a test. -->
212
+ <div
213
+ ref="panelEl"
214
+ role="dialog"
215
+ aria-modal="true"
216
+ :aria-label="t('integration.audit.panel.title.aria')"
217
+ tabindex="-1"
218
+ class="drawer-panel w-full max-w-md overflow-y-auto border-l border-black/[0.06] bg-white shadow-xl outline-none"
219
+ >
117
220
  <div class="sticky top-0 z-10 flex items-center gap-2 border-b border-black/[0.06] bg-white/90 backdrop-blur px-4 py-3">
118
221
  <Badge variant="outline" :class="['border text-xs font-medium uppercase', actionTone(selected.action)]">{{ prettyAction(selected.action) }}</Badge>
119
222
  <span class="text-sm font-semibold text-[#0B0522] capitalize">{{ prettyType(selected.resource_type) }}</span>
120
- <Button variant="ghost" size="sm" class="ml-auto h-7 w-7 p-0" @click="selected = null"><Close :size="15" /></Button>
223
+ <Button variant="ghost" size="sm" :aria-label="t('integration.audit.panel.close.aria')" class="ml-auto h-7 w-7 p-0" @click="selected = null"><Close :size="15" /></Button>
121
224
  </div>
122
225
  <div class="p-4 space-y-4 text-sm">
123
226
  <div class="space-y-1.5">
124
- <div class="flex justify-between gap-2"><span class="text-muted-foreground">Resource</span><span class="font-medium text-right">{{ resourceName(selected) }}</span></div>
125
- <div v-if="selected.resource_id" class="flex justify-between gap-2"><span class="text-muted-foreground">ID</span><code class="font-mono text-xs">{{ selected.resource_id }}</code></div>
126
- <div class="flex justify-between gap-2"><span class="text-muted-foreground">Actor</span><code class="font-mono text-xs">{{ selected.actor_sub ?? "system" }}</code></div>
127
- <div class="flex justify-between gap-2"><span class="text-muted-foreground">When</span><span>{{ fmtDate(selected.created_at) }}</span></div>
128
- <div v-if="selected.tenant_id" class="flex justify-between gap-2"><span class="text-muted-foreground">Tenant</span><span>{{ selected.tenant_id }}</span></div>
227
+ <div class="flex justify-between gap-2"><span class="text-muted-foreground">{{ t("integration.audit.table.column.resource") }}</span><span class="font-medium text-right">{{ resourceName(selected) }}</span></div>
228
+ <div v-if="selected.resource_id" class="flex justify-between gap-2"><span class="text-muted-foreground">{{ t("integration.audit.detail.id") }}</span><code class="font-mono text-xs">{{ selected.resource_id }}</code></div>
229
+ <div class="flex justify-between gap-2"><span class="text-muted-foreground">{{ t("integration.audit.table.column.actor") }}</span><code class="font-mono text-xs">{{ selected.actor_sub ?? t("integration.audit.actor.system") }}</code></div>
230
+ <div class="flex justify-between gap-2"><span class="text-muted-foreground">{{ t("integration.audit.table.column.when") }}</span><span>{{ fmtDate(selected.created_at) }}</span></div>
231
+ <div v-if="selected.tenant_id" class="flex justify-between gap-2"><span class="text-muted-foreground">{{ t("integration.audit.detail.tenant") }}</span><span>{{ selected.tenant_id }}</span></div>
129
232
  </div>
130
233
  <div v-if="selected.metadata && Object.keys(selected.metadata).length">
131
234
  <div class="mb-1 flex items-center justify-between">
132
- <span class="text-[11px] font-medium uppercase tracking-wide text-muted-foreground">Details</span>
133
- <button class="inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground" @click="copy(JSON.stringify(selected.metadata, null, 2))"><CopyIcon :size="12" /> copy</button>
235
+ <span class="text-[11px] font-medium uppercase tracking-wide text-muted-foreground">{{ t("integration.audit.detail.detailsHeading") }}</span>
236
+ <button class="inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground" @click="copy(JSON.stringify(selected.metadata, null, 2))"><CopyIcon :size="12" /> {{ t("integration.audit.action.copy") }}</button>
134
237
  </div>
135
238
  <pre class="overflow-x-auto rounded-md bg-[#0B0522] p-3 text-xs leading-relaxed text-[#E6E1F5]"><code>{{ JSON.stringify(selected.metadata, null, 2) }}</code></pre>
136
239
  </div>
137
240
  </div>
138
- </aside>
241
+ </div>
139
242
  </div>
140
243
  </Transition>
141
244
  </IntegrationsLayout>
142
245
  </template>
143
246
 
144
247
  <style scoped>
145
- .drawer-enter-active,.drawer-leave-active{transition:opacity .2s ease}.drawer-enter-active aside,.drawer-leave-active aside{transition:transform .25s cubic-bezier(.22,1,.36,1)}.drawer-enter-from,.drawer-leave-to{opacity:0}.drawer-enter-from aside,.drawer-leave-to aside{transform:translateX(100%)}
248
+ .drawer-enter-active,.drawer-leave-active{transition:opacity .2s ease}.drawer-enter-active .drawer-panel,.drawer-leave-active .drawer-panel{transition:transform .25s cubic-bezier(.22,1,.36,1)}.drawer-enter-from,.drawer-leave-to{opacity:0}.drawer-enter-from .drawer-panel,.drawer-leave-to .drawer-panel{transform:translateX(100%)}
146
249
  </style>
@@ -1,6 +1,7 @@
1
1
  <script setup>
2
2
  import { onMounted, ref } from "vue";
3
3
  definePageMeta({ layout: false });
4
+ const t = useIntegrationsT();
4
5
  const closing = ref(true);
5
6
  onMounted(() => {
6
7
  try {
@@ -15,14 +16,16 @@ onMounted(() => {
15
16
  </script>
16
17
 
17
18
  <template>
18
- <div class="flex min-h-screen items-center justify-center bg-white text-center">
19
+ <!-- Own landmark and heading: this popup renders without the studio shell
20
+ (`layout: false`), so it would otherwise be the one page with neither. -->
21
+ <main class="flex min-h-screen items-center justify-center bg-white text-center">
19
22
  <div class="space-y-2">
20
- <p class="text-sm font-medium text-foreground">
21
- Connection complete
22
- </p>
23
+ <h1 class="text-sm font-medium text-foreground">
24
+ {{ t("integration.credentials.oauthComplete.title") }}
25
+ </h1>
23
26
  <p class="text-sm text-muted-foreground">
24
- {{ closing ? "You can close this window." : "Done \u2014 closing\u2026" }}
27
+ {{ closing ? t("integration.credentials.oauthComplete.hint.open") : t("integration.credentials.oauthComplete.hint.closing") }}
25
28
  </p>
26
29
  </div>
27
- </div>
30
+ </main>
28
31
  </template>