@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.
- package/CHANGELOG.md +428 -0
- package/README.md +48 -4
- package/dist/module.d.mts +17 -1
- package/dist/module.json +1 -1
- package/dist/module.mjs +10 -2
- package/dist/runtime/components/Integrations/CredentialCreateDialog.vue +43 -41
- package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +123 -50
- package/dist/runtime/components/Integrations/CredentialRevenexxSource.vue +108 -71
- package/dist/runtime/components/Integrations/CredentialTypePicker.vue +12 -15
- package/dist/runtime/components/Integrations/DeadLettersPanel.vue +18 -17
- package/dist/runtime/components/Integrations/EventTypeCombobox.d.vue.ts +1 -0
- package/dist/runtime/components/Integrations/EventTypeCombobox.vue +7 -4
- package/dist/runtime/components/Integrations/EventTypeCombobox.vue.d.ts +1 -0
- package/dist/runtime/components/Integrations/ExpressionInput.d.vue.ts +4 -0
- package/dist/runtime/components/Integrations/ExpressionInput.vue +67 -37
- package/dist/runtime/components/Integrations/ExpressionInput.vue.d.ts +4 -0
- package/dist/runtime/components/Integrations/GroupFrameView.vue +30 -20
- package/dist/runtime/components/Integrations/Layout.vue +48 -4
- package/dist/runtime/components/Integrations/NodeConfigDynamicField.vue +23 -23
- package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.vue +11 -13
- package/dist/runtime/components/Integrations/NodeConfigField.d.vue.ts +2 -0
- package/dist/runtime/components/Integrations/NodeConfigField.vue +187 -93
- package/dist/runtime/components/Integrations/NodeConfigField.vue.d.ts +2 -0
- package/dist/runtime/components/Integrations/NodeInspector.d.vue.ts +6 -0
- package/dist/runtime/components/Integrations/NodeInspector.vue +1157 -376
- package/dist/runtime/components/Integrations/NodeInspector.vue.d.ts +6 -0
- package/dist/runtime/components/Integrations/NodeLogo.d.vue.ts +12 -0
- package/dist/runtime/components/Integrations/NodeLogo.vue +2 -30
- package/dist/runtime/components/Integrations/NodeLogo.vue.d.ts +12 -0
- package/dist/runtime/components/Integrations/NodeMark.d.vue.ts +15 -0
- package/dist/runtime/components/Integrations/NodeMark.vue +79 -0
- package/dist/runtime/components/Integrations/NodeMark.vue.d.ts +15 -0
- package/dist/runtime/components/Integrations/NodeOutputSchema.d.vue.ts +21 -0
- package/dist/runtime/components/Integrations/NodeOutputSchema.vue +43 -0
- package/dist/runtime/components/Integrations/NodeOutputSchema.vue.d.ts +21 -0
- package/dist/runtime/components/Integrations/NodePalette.vue +122 -55
- package/dist/runtime/components/Integrations/QueryBuilder.vue +46 -38
- package/dist/runtime/components/Integrations/RunActionsMenu.vue +38 -12
- package/dist/runtime/components/Integrations/RunDetailPanel.vue +114 -62
- package/dist/runtime/components/Integrations/RunPayloadInput.vue +27 -13
- package/dist/runtime/components/Integrations/RunResponseModal.vue +11 -10
- package/dist/runtime/components/Integrations/RunsTable.d.vue.ts +0 -1
- package/dist/runtime/components/Integrations/RunsTable.vue +38 -16
- package/dist/runtime/components/Integrations/RunsTable.vue.d.ts +0 -1
- package/dist/runtime/components/Integrations/SftpAccountImportDialog.vue +8 -7
- package/dist/runtime/components/Integrations/Sidebar.vue +14 -10
- package/dist/runtime/components/Integrations/StickyNoteView.d.vue.ts +14 -4
- package/dist/runtime/components/Integrations/StickyNoteView.vue +38 -23
- package/dist/runtime/components/Integrations/StickyNoteView.vue.d.ts +14 -4
- package/dist/runtime/components/Integrations/TemplateCard.d.vue.ts +18 -0
- package/dist/runtime/components/Integrations/TemplateCard.vue +119 -0
- package/dist/runtime/components/Integrations/TemplateCard.vue.d.ts +18 -0
- package/dist/runtime/components/Integrations/TemplateFlowPreview.d.vue.ts +2 -2
- package/dist/runtime/components/Integrations/TemplateFlowPreview.vue +16 -18
- package/dist/runtime/components/Integrations/TemplateFlowPreview.vue.d.ts +2 -2
- package/dist/runtime/components/Integrations/WorkflowContextMenu.vue +34 -33
- package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +52 -67
- package/dist/runtime/components/Integrations/WorkflowDuplicateDialog.vue +8 -7
- package/dist/runtime/components/Integrations/WorkflowEdgeView.d.vue.ts +18 -4
- package/dist/runtime/components/Integrations/WorkflowEdgeView.vue +29 -11
- package/dist/runtime/components/Integrations/WorkflowEdgeView.vue.d.ts +18 -4
- package/dist/runtime/components/Integrations/WorkflowEditor.d.vue.ts +20 -1
- package/dist/runtime/components/Integrations/WorkflowEditor.vue +240 -116
- package/dist/runtime/components/Integrations/WorkflowEditor.vue.d.ts +20 -1
- package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue +28 -43
- package/dist/runtime/components/Integrations/WorkflowIconPicker.d.vue.ts +19 -1
- package/dist/runtime/components/Integrations/WorkflowIconPicker.vue +17 -7
- package/dist/runtime/components/Integrations/WorkflowIconPicker.vue.d.ts +19 -1
- package/dist/runtime/components/Integrations/WorkflowNodeView.d.vue.ts +4 -0
- package/dist/runtime/components/Integrations/WorkflowNodeView.vue +198 -56
- package/dist/runtime/components/Integrations/WorkflowNodeView.vue.d.ts +4 -0
- package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +56 -59
- package/dist/runtime/components/Integrations/WorkflowSettingsDialog.vue +28 -21
- package/dist/runtime/components/Integrations/WorkflowTagsInput.d.vue.ts +1 -0
- package/dist/runtime/components/Integrations/WorkflowTagsInput.vue +15 -9
- package/dist/runtime/components/Integrations/WorkflowTagsInput.vue.d.ts +1 -0
- package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +15 -10
- package/dist/runtime/components/Integrations/WorkflowVariablesDialog.vue +50 -36
- package/dist/runtime/components/Integrations/WorkflowsTable.vue +126 -48
- package/dist/runtime/composables/integrationsApi.js +4 -2
- package/dist/runtime/composables/useCredentialTypes.js +8 -6
- package/dist/runtime/composables/useIntegrationsAudit.js +4 -3
- package/dist/runtime/composables/useIntegrationsCredentials.js +10 -8
- package/dist/runtime/composables/useIntegrationsDeadLetters.js +7 -6
- package/dist/runtime/composables/useIntegrationsFocus.d.ts +12 -0
- package/dist/runtime/composables/useIntegrationsLocale.d.ts +26 -0
- package/dist/runtime/composables/useIntegrationsLocale.js +8 -0
- package/dist/runtime/composables/useIntegrationsNodes.js +7 -5
- package/dist/runtime/composables/useIntegrationsRealtime.d.ts +149 -0
- package/dist/runtime/composables/useIntegrationsRealtime.js +80 -0
- package/dist/runtime/composables/useIntegrationsRevisions.js +9 -7
- package/dist/runtime/composables/useIntegrationsRuns.d.ts +1 -1
- package/dist/runtime/composables/useIntegrationsRuns.js +14 -7
- package/dist/runtime/composables/useIntegrationsSchedules.d.ts +4 -0
- package/dist/runtime/composables/useIntegrationsSchedules.js +10 -3
- package/dist/runtime/composables/useIntegrationsSecrets.js +14 -9
- package/dist/runtime/composables/useIntegrationsT.d.ts +35 -0
- package/dist/runtime/composables/useIntegrationsT.js +9 -0
- package/dist/runtime/composables/useIntegrationsTemplates.d.ts +3 -3
- package/dist/runtime/composables/useIntegrationsTemplates.js +28 -16
- package/dist/runtime/composables/useIntegrationsTriggers.js +10 -8
- package/dist/runtime/composables/useIntegrationsWorkflows.js +13 -7
- package/dist/runtime/composables/useLoadGate.d.ts +17 -0
- package/dist/runtime/composables/useLoadGate.js +8 -0
- package/dist/runtime/composables/useNodeConfigResolve.js +2 -1
- package/dist/runtime/data/branchReorder.d.ts +37 -0
- package/dist/runtime/data/branchReorder.js +71 -0
- package/dist/runtime/data/handleId.d.ts +23 -0
- package/dist/runtime/data/handleId.js +6 -0
- package/dist/runtime/data/integrationNodeCatalog.d.ts +41 -0
- package/dist/runtime/data/integrationNodeCatalog.js +14 -0
- package/dist/runtime/data/integrationTemplates.d.ts +48 -0
- package/dist/runtime/data/integrationTemplates.js +21 -0
- package/dist/runtime/data/integrationWorkflowSamples.js +1 -1
- package/dist/runtime/data/nodeIcons.d.ts +2 -0
- package/dist/runtime/data/nodeIcons.js +73 -0
- package/dist/runtime/data/nodeInputData.d.ts +16 -4
- package/dist/runtime/data/nodeInputData.js +4 -0
- package/dist/runtime/data/payloadForm.d.ts +9 -2
- package/dist/runtime/data/payloadForm.js +29 -4
- package/dist/runtime/data/portTone.d.ts +17 -0
- package/dist/runtime/data/portTone.js +7 -0
- package/dist/runtime/data/webhookAuth.d.ts +18 -0
- package/dist/runtime/data/webhookAuth.js +19 -0
- package/dist/runtime/data/workflowIconCatalog.js +4 -6
- package/dist/runtime/i18n/messages/de.json +1207 -0
- package/dist/runtime/i18n/messages/en.json +1207 -0
- package/dist/runtime/i18n/translate.d.ts +83 -0
- package/dist/runtime/i18n/translate.js +41 -0
- package/dist/runtime/pages/integrations/audit.vue +140 -37
- package/dist/runtime/pages/integrations/credentials/oauth-complete.vue +9 -6
- package/dist/runtime/pages/integrations/credentials.vue +180 -64
- package/dist/runtime/pages/integrations/index.vue +91 -92
- package/dist/runtime/pages/integrations/runs.vue +33 -21
- package/dist/runtime/pages/integrations/schedules.vue +88 -23
- package/dist/runtime/pages/integrations/secrets.vue +57 -35
- package/dist/runtime/pages/integrations/templates/[slug].vue +119 -88
- package/dist/runtime/pages/integrations/templates/index.vue +82 -103
- package/dist/runtime/pages/integrations/workflows/[id].vue +344 -91
- package/dist/runtime/pages/integrations/workflows/index.vue +23 -5
- package/dist/runtime/plugins/locale.d.ts +10 -0
- package/dist/runtime/plugins/locale.js +6 -0
- package/dist/runtime/plugins/realtime.client.d.ts +32 -0
- package/dist/runtime/plugins/realtime.client.js +4 -0
- package/dist/runtime/types/integrations.d.ts +65 -6
- package/dist/runtime/utils/apiError.d.ts +47 -0
- package/dist/runtime/utils/apiError.js +32 -0
- package/dist/runtime/utils/cockpitSources.d.ts +153 -0
- package/dist/runtime/utils/cockpitSources.js +14 -0
- package/dist/runtime/utils/credentialState.d.ts +24 -1
- package/dist/runtime/utils/credentialState.js +26 -11
- package/dist/runtime/utils/loadGate.d.ts +61 -0
- package/dist/runtime/utils/loadGate.js +22 -0
- package/dist/runtime/utils/localize.d.ts +46 -4
- package/dist/runtime/utils/localize.js +34 -7
- package/dist/runtime/utils/notify.d.ts +59 -0
- package/dist/runtime/utils/notify.js +21 -0
- package/dist/runtime/utils/pollBudget.d.ts +72 -0
- package/dist/runtime/utils/pollBudget.js +17 -0
- package/dist/runtime/utils/revenexxCredentialSource.d.ts +26 -0
- package/dist/runtime/utils/revenexxCredentialSource.js +8 -0
- package/dist/runtime/utils/schedule.d.ts +116 -0
- package/dist/runtime/utils/schedule.js +219 -0
- package/dist/runtime/utils/statusStyle.d.ts +26 -2
- package/dist/runtime/utils/statusStyle.js +9 -1
- package/dist/runtime/utils/validationFindings.d.ts +101 -0
- package/dist/runtime/utils/validationFindings.js +54 -0
- package/dist/runtime/utils/whenIdle.d.ts +42 -0
- package/dist/runtime/utils/whenIdle.js +12 -0
- package/dist/types.d.mts +2 -6
- 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
|
-
|
|
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
|
-
|
|
45
|
+
const key = actionKeys[a];
|
|
46
|
+
return key ? t(key) : a.replace(".", " ");
|
|
30
47
|
}
|
|
31
|
-
function prettyType(
|
|
32
|
-
|
|
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: "
|
|
61
|
+
label: t("integration.audit.table.column.resource"),
|
|
44
62
|
getter: (e) => e.resource_type,
|
|
45
|
-
options: resourceTypes.map((
|
|
63
|
+
options: resourceTypes.map((rt) => ({ value: rt, label: prettyType(rt) }))
|
|
46
64
|
},
|
|
47
65
|
{
|
|
48
66
|
key: "action",
|
|
49
|
-
label: "
|
|
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: "
|
|
56
|
-
{ key: "resource", label: "
|
|
57
|
-
{ key: "actor", label: "
|
|
58
|
-
{ key: "when", label: "
|
|
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(
|
|
62
|
-
if (
|
|
63
|
-
navigator.clipboard.writeText(
|
|
64
|
-
toast.success("
|
|
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="
|
|
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
|
-
:
|
|
152
|
+
:pagination="false"
|
|
153
|
+
:search="{ placeholder: t('integration.audit.search.placeholder') }"
|
|
82
154
|
:filter-tabs="filterTabs"
|
|
83
155
|
:loading="loading"
|
|
84
|
-
empty-text="
|
|
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 ? "
|
|
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"
|
|
115
|
-
|
|
116
|
-
|
|
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">
|
|
125
|
-
<div v-if="selected.resource_id" class="flex justify-between gap-2"><span class="text-muted-foreground">
|
|
126
|
-
<div class="flex justify-between gap-2"><span class="text-muted-foreground">
|
|
127
|
-
<div class="flex justify-between gap-2"><span class="text-muted-foreground">
|
|
128
|
-
<div v-if="selected.tenant_id" class="flex justify-between gap-2"><span class="text-muted-foreground">
|
|
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">
|
|
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
|
-
</
|
|
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
|
|
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
|
-
|
|
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
|
-
<
|
|
21
|
-
|
|
22
|
-
</
|
|
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 ? "
|
|
27
|
+
{{ closing ? t("integration.credentials.oauthComplete.hint.open") : t("integration.credentials.oauthComplete.hint.closing") }}
|
|
25
28
|
</p>
|
|
26
29
|
</div>
|
|
27
|
-
</
|
|
30
|
+
</main>
|
|
28
31
|
</template>
|