@revenexx/studio-integrations 0.6.0 → 1.0.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 +643 -0
- package/README.md +31 -0
- package/dist/module.d.mts +17 -1
- package/dist/module.json +1 -1
- package/dist/module.mjs +12 -3
- package/dist/runtime/components/Integrations/CredentialCreateDialog.vue +32 -28
- package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +45 -38
- package/dist/runtime/components/Integrations/CredentialRevenexxSource.vue +58 -58
- package/dist/runtime/components/Integrations/CredentialTypePicker.vue +10 -9
- package/dist/runtime/components/Integrations/DeadLettersPanel.vue +18 -17
- package/dist/runtime/components/Integrations/EventTypeCombobox.vue +4 -3
- package/dist/runtime/components/Integrations/ExpressionInput.vue +21 -15
- package/dist/runtime/components/Integrations/GroupFrameView.vue +29 -19
- package/dist/runtime/components/Integrations/Layout.vue +28 -3
- package/dist/runtime/components/Integrations/NodeConfigDynamicField.d.vue.ts +3 -0
- package/dist/runtime/components/Integrations/NodeConfigDynamicField.vue +30 -16
- package/dist/runtime/components/Integrations/NodeConfigDynamicField.vue.d.ts +3 -0
- package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.vue +10 -8
- package/dist/runtime/components/Integrations/NodeConfigField.d.vue.ts +3 -0
- package/dist/runtime/components/Integrations/NodeConfigField.vue +45 -42
- package/dist/runtime/components/Integrations/NodeConfigField.vue.d.ts +3 -0
- package/dist/runtime/components/Integrations/NodeInspector.d.vue.ts +2 -2
- package/dist/runtime/components/Integrations/NodeInspector.vue +315 -221
- package/dist/runtime/components/Integrations/NodeInspector.vue.d.ts +2 -2
- package/dist/runtime/components/Integrations/NodeOutputSchema.d.vue.ts +2 -1
- package/dist/runtime/components/Integrations/NodeOutputSchema.vue +37 -10
- package/dist/runtime/components/Integrations/NodeOutputSchema.vue.d.ts +2 -1
- package/dist/runtime/components/Integrations/NodePalette.vue +40 -35
- package/dist/runtime/components/Integrations/QueryBuilder.vue +44 -43
- package/dist/runtime/components/Integrations/RunActionsMenu.vue +36 -12
- package/dist/runtime/components/Integrations/RunDetailPanel.vue +114 -62
- package/dist/runtime/components/Integrations/RunPayloadInput.vue +14 -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 +82 -34
- package/dist/runtime/components/Integrations/StickyNoteView.vue.d.ts +14 -4
- package/dist/runtime/components/Integrations/SurfaceCard.d.vue.ts +50 -0
- package/dist/runtime/components/Integrations/SurfaceCard.vue +55 -0
- package/dist/runtime/components/Integrations/SurfaceCard.vue.d.ts +50 -0
- package/dist/runtime/components/Integrations/TemplateCard.vue +18 -8
- package/dist/runtime/components/Integrations/TemplateFlowPreview.vue +22 -12
- package/dist/runtime/components/Integrations/WorkflowContextMenu.vue +34 -33
- package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +41 -39
- package/dist/runtime/components/Integrations/WorkflowDuplicateDialog.vue +8 -7
- package/dist/runtime/components/Integrations/WorkflowEdgeView.vue +6 -4
- package/dist/runtime/components/Integrations/WorkflowEditor.d.vue.ts +16 -0
- package/dist/runtime/components/Integrations/WorkflowEditor.vue +131 -87
- package/dist/runtime/components/Integrations/WorkflowEditor.vue.d.ts +16 -0
- package/dist/runtime/components/Integrations/WorkflowHistoryPanel.d.vue.ts +6 -0
- package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue +27 -24
- package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue.d.ts +6 -0
- package/dist/runtime/components/Integrations/WorkflowIconPicker.vue +7 -6
- package/dist/runtime/components/Integrations/WorkflowNodeView.vue +76 -46
- package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +55 -43
- package/dist/runtime/components/Integrations/WorkflowSettingsDialog.d.vue.ts +16 -17
- package/dist/runtime/components/Integrations/WorkflowSettingsDialog.vue +55 -43
- package/dist/runtime/components/Integrations/WorkflowSettingsDialog.vue.d.ts +16 -17
- package/dist/runtime/components/Integrations/WorkflowTagsInput.vue +9 -8
- package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +15 -13
- package/dist/runtime/components/Integrations/WorkflowVariablesDialog.vue +79 -39
- package/dist/runtime/components/Integrations/WorkflowsTable.vue +83 -45
- package/dist/runtime/composables/integrationsApi.js +4 -2
- package/dist/runtime/composables/useCredentialTypes.js +4 -2
- package/dist/runtime/composables/useIntegrationsCredentials.d.ts +11 -0
- package/dist/runtime/composables/useIntegrationsCredentials.js +8 -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 +14 -2
- 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 +3 -1
- package/dist/runtime/composables/useIntegrationsSchedules.d.ts +3 -2
- package/dist/runtime/composables/useIntegrationsSecrets.js +4 -2
- 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 +16 -8
- package/dist/runtime/composables/useIntegrationsTriggers.d.ts +23 -3
- package/dist/runtime/composables/useIntegrationsTriggers.js +21 -8
- package/dist/runtime/composables/useIntegrationsWorkflows.d.ts +10 -1
- package/dist/runtime/composables/useIntegrationsWorkflows.js +30 -7
- package/dist/runtime/composables/useNodeConfigResolve.js +4 -2
- package/dist/runtime/data/integrationNodeCatalog.d.ts +63 -1
- package/dist/runtime/data/integrationNodeCatalog.js +22 -4
- package/dist/runtime/data/integrationTemplates.d.ts +9 -0
- package/dist/runtime/data/integrationTemplates.js +7 -1
- package/dist/runtime/data/nodeInputData.d.ts +58 -1
- package/dist/runtime/data/nodeInputData.js +30 -5
- package/dist/runtime/data/payloadForm.js +2 -1
- package/dist/runtime/data/triggerGlyphs.d.ts +47 -0
- package/dist/runtime/data/triggerGlyphs.js +11 -0
- package/dist/runtime/data/workflowIconCatalog.js +2 -1
- package/dist/runtime/i18n/messages/de.json +1224 -0
- package/dist/runtime/i18n/messages/en.json +1224 -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 +52 -34
- package/dist/runtime/pages/integrations/credentials/oauth-complete.vue +3 -2
- package/dist/runtime/pages/integrations/credentials.vue +73 -52
- package/dist/runtime/pages/integrations/index.vue +66 -73
- package/dist/runtime/pages/integrations/runs.vue +33 -21
- package/dist/runtime/pages/integrations/schedules.vue +49 -28
- package/dist/runtime/pages/integrations/secrets.vue +34 -34
- package/dist/runtime/pages/integrations/templates/[slug].vue +89 -72
- package/dist/runtime/pages/integrations/templates/index.vue +35 -27
- package/dist/runtime/pages/integrations/workflows/[id].vue +351 -197
- package/dist/runtime/pages/integrations/workflows/index.vue +10 -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 +25 -1
- package/dist/runtime/utils/cockpitSources.d.ts +22 -3
- package/dist/runtime/utils/credentialState.d.ts +30 -2
- package/dist/runtime/utils/credentialState.js +26 -11
- package/dist/runtime/utils/localize.d.ts +52 -5
- package/dist/runtime/utils/localize.js +47 -9
- package/dist/runtime/utils/pollBudget.d.ts +72 -0
- package/dist/runtime/utils/pollBudget.js +17 -0
- package/dist/runtime/utils/schedule.d.ts +0 -25
- package/dist/runtime/utils/schedule.js +37 -25
- package/dist/runtime/utils/servedPaths.d.ts +22 -0
- package/dist/runtime/utils/servedPaths.js +3 -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 +2 -2
- package/dist/runtime/utils/validationFindings.js +4 -3
- package/dist/types.d.mts +2 -6
- package/package.json +3 -2
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import { RefreshCircle as Loader2, Refresh as ReplayIcon, TrashBinMinimalistic as
|
|
2
|
+
import { RefreshCircle as Loader2, Refresh as ReplayIcon, TrashBinMinimalistic as TrashBin } from "#solar-icons";
|
|
3
3
|
import { Badge, Button } from "@revenexx/studio";
|
|
4
4
|
import { toast } from "vue-sonner";
|
|
5
5
|
import RunsTable from "../../components/Integrations/RunsTable.vue";
|
|
6
|
+
import { createPollBudget, SAFETY_POLL_MS } from "../../utils/pollBudget";
|
|
6
7
|
import { statusClass } from "../../utils/statusStyle";
|
|
7
|
-
|
|
8
|
+
const t = useIntegrationsT();
|
|
9
|
+
useHead({ title: () => t("integration.runs.list.head.title") });
|
|
8
10
|
const route = useRoute();
|
|
9
11
|
const { runs, loading, load } = useIntegrationsRuns();
|
|
10
12
|
const {
|
|
@@ -17,18 +19,28 @@ const {
|
|
|
17
19
|
} = useIntegrationsDeadLetters();
|
|
18
20
|
const tab = ref(route.query.tab === "missed" ? "missed" : "runs");
|
|
19
21
|
const tabs = computed(() => [
|
|
20
|
-
{ key: "runs", label: "
|
|
22
|
+
{ key: "runs", label: t("integration.runs.tabs.runs.label") },
|
|
21
23
|
// "Missed runs": trigger firings that never became a run (dead letters).
|
|
22
|
-
{ key: "missed", label: "
|
|
24
|
+
{ key: "missed", label: t("integration.deadLetters.tabs.missed.label"), badge: pendingCount.value || null }
|
|
23
25
|
]);
|
|
26
|
+
const POLL_MS = 3e3;
|
|
24
27
|
const hasActiveRuns = computed(() => runs.value.some((r) => r.status === "running" || r.status === "queued"));
|
|
28
|
+
const { delivering, runTick, workflowTick } = useIntegrationsRealtime();
|
|
29
|
+
const budget = createPollBudget({ intervalMs: POLL_MS, liveIntervalMs: SAFETY_POLL_MS });
|
|
30
|
+
function reload() {
|
|
31
|
+
budget.spend();
|
|
32
|
+
void load(true);
|
|
33
|
+
}
|
|
34
|
+
watch([runTick, workflowTick], () => {
|
|
35
|
+
reload();
|
|
36
|
+
});
|
|
25
37
|
let pollTimer = null;
|
|
26
38
|
onMounted(() => {
|
|
27
39
|
void load(true);
|
|
28
40
|
void loadGlobal(true);
|
|
29
41
|
pollTimer = setInterval(() => {
|
|
30
|
-
if (hasActiveRuns.value && !loading.value) void load(true);
|
|
31
|
-
},
|
|
42
|
+
if (hasActiveRuns.value && !loading.value && budget.due(delivering.value)) void load(true);
|
|
43
|
+
}, POLL_MS);
|
|
32
44
|
});
|
|
33
45
|
onUnmounted(() => {
|
|
34
46
|
if (pollTimer) {
|
|
@@ -42,11 +54,11 @@ async function onReplay(dl) {
|
|
|
42
54
|
try {
|
|
43
55
|
const res = await replay(dl.id);
|
|
44
56
|
const newId = res && typeof res === "object" && "id" in res ? String(res.id) : null;
|
|
45
|
-
toast.success(newId ?
|
|
57
|
+
toast.success(newId ? t("integration.deadLetters.toast.replay.started", { id: newId }) : t("integration.deadLetters.toast.replay.done"));
|
|
46
58
|
await loadGlobal(true);
|
|
47
|
-
|
|
59
|
+
reload();
|
|
48
60
|
} catch (e) {
|
|
49
|
-
toast.error(e?.message ?? "
|
|
61
|
+
toast.error(e?.message ?? t("integration.deadLetters.toast.replay.error"));
|
|
50
62
|
} finally {
|
|
51
63
|
busyId.value = null;
|
|
52
64
|
}
|
|
@@ -55,10 +67,10 @@ async function onDiscard(dl) {
|
|
|
55
67
|
busyId.value = dl.id;
|
|
56
68
|
try {
|
|
57
69
|
await discard(dl.id);
|
|
58
|
-
toast.success("
|
|
70
|
+
toast.success(t("integration.deadLetters.toast.discard.done"));
|
|
59
71
|
await loadGlobal(true);
|
|
60
72
|
} catch (e) {
|
|
61
|
-
toast.error(e?.message ?? "
|
|
73
|
+
toast.error(e?.message ?? t("integration.deadLetters.toast.discard.error"));
|
|
62
74
|
} finally {
|
|
63
75
|
busyId.value = null;
|
|
64
76
|
}
|
|
@@ -76,25 +88,25 @@ function fmt(iso) {
|
|
|
76
88
|
<template>
|
|
77
89
|
<IntegrationsLayout>
|
|
78
90
|
<PageHeader
|
|
79
|
-
title="
|
|
80
|
-
description="
|
|
91
|
+
:title="t('integration.runs.list.header.title')"
|
|
92
|
+
:description="t('integration.runs.list.header.description')"
|
|
81
93
|
/>
|
|
82
94
|
|
|
83
95
|
<TabBar v-model="tab" :tabs="tabs" class="px-4" />
|
|
84
96
|
|
|
85
97
|
<div class="p-4 space-y-4">
|
|
86
|
-
<RunsTable v-if="tab === 'runs'" :runs="runs" :loading="loading" @changed="
|
|
98
|
+
<RunsTable v-if="tab === 'runs'" :runs="runs" :loading="loading" @changed="reload" />
|
|
87
99
|
|
|
88
100
|
<!-- Missed runs — trigger firings that never produced a run (the
|
|
89
101
|
workflow wasn't runnable when its schedule/event fired). -->
|
|
90
102
|
<template v-else>
|
|
91
103
|
<div class="flex items-center justify-between">
|
|
92
104
|
<p class="text-sm text-muted-foreground">
|
|
93
|
-
|
|
105
|
+
{{ t("integration.deadLetters.list.description") }}
|
|
94
106
|
</p>
|
|
95
107
|
<Button variant="outline" size="sm" class="h-7 text-sm" :disabled="missedLoading" @click="() => loadGlobal(true)">
|
|
96
108
|
<Loader2 v-if="missedLoading" :size="12" class="animate-spin" />
|
|
97
|
-
|
|
109
|
+
{{ t("integration.common.action.refresh") }}
|
|
98
110
|
</Button>
|
|
99
111
|
</div>
|
|
100
112
|
|
|
@@ -109,7 +121,7 @@ function fmt(iso) {
|
|
|
109
121
|
</div>
|
|
110
122
|
</div>
|
|
111
123
|
<div v-else-if="!missed.length" class="rounded-lg border border-black/[0.06] bg-white p-6 text-center text-sm text-muted-foreground">
|
|
112
|
-
|
|
124
|
+
{{ t("integration.deadLetters.state.empty") }}
|
|
113
125
|
</div>
|
|
114
126
|
|
|
115
127
|
<div v-else class="rounded-lg border border-black/[0.06] bg-white divide-y divide-black/[0.04]">
|
|
@@ -123,7 +135,7 @@ function fmt(iso) {
|
|
|
123
135
|
<NuxtLink :to="`/integrations/workflows/${dl.workflow_id}`" class="text-sm font-medium truncate transition-colors hover:text-primary">
|
|
124
136
|
{{ dl.workflowName }}
|
|
125
137
|
</NuxtLink>
|
|
126
|
-
<div class="text-sm break-words">{{ dl.reason || "
|
|
138
|
+
<div class="text-sm break-words">{{ dl.reason || t("integration.deadLetters.reason.fallback") }}</div>
|
|
127
139
|
<div class="mt-0.5 flex flex-wrap items-center gap-x-3 gap-y-0.5 text-xs text-muted-foreground font-mono">
|
|
128
140
|
<span>#{{ dl.id }}</span>
|
|
129
141
|
<span v-if="dl.source">{{ dl.source }}</span>
|
|
@@ -135,11 +147,11 @@ function fmt(iso) {
|
|
|
135
147
|
<Button variant="outline" size="sm" class="h-7 text-sm" :disabled="busyId === dl.id" @click="onReplay(dl)">
|
|
136
148
|
<Loader2 v-if="busyId === dl.id" :size="12" class="animate-spin" />
|
|
137
149
|
<ReplayIcon v-else :size="12" />
|
|
138
|
-
|
|
150
|
+
{{ t("integration.deadLetters.action.replay") }}
|
|
139
151
|
</Button>
|
|
140
152
|
<Button variant="ghost" size="sm" class="h-7 text-sm text-rose-600 hover:bg-rose-500/10" :disabled="busyId === dl.id" @click="onDiscard(dl)">
|
|
141
|
-
<
|
|
142
|
-
|
|
153
|
+
<TrashBin :size="12" />
|
|
154
|
+
{{ t("integration.deadLetters.action.discard") }}
|
|
143
155
|
</Button>
|
|
144
156
|
</div>
|
|
145
157
|
</div>
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
import { ClockCircle as Clock, Globus as Globe } from "#solar-icons";
|
|
3
3
|
import { Badge } from "@revenexx/studio";
|
|
4
4
|
import { cadenceRank, describeCron } from "../../utils/schedule";
|
|
5
|
-
|
|
5
|
+
const t = useIntegrationsT();
|
|
6
|
+
useHead({ title: () => t("integration.schedules.head.title") });
|
|
6
7
|
const router = useRouter();
|
|
7
8
|
const { entries, activeCount, loading, load } = useIntegrationsSchedules();
|
|
8
9
|
onMounted(() => {
|
|
@@ -11,18 +12,38 @@ onMounted(() => {
|
|
|
11
12
|
function formatRelative(iso) {
|
|
12
13
|
if (!iso) return "\u2014";
|
|
13
14
|
const ms = new Date(iso).getTime() - Date.now();
|
|
14
|
-
if (Math.abs(ms) < 6e4) return ms > 0 ? "
|
|
15
|
+
if (Math.abs(ms) < 6e4) return ms > 0 ? t("integration.schedules.time.inLessThanMinute") : t("integration.common.time.justNow");
|
|
15
16
|
if (Math.abs(ms) < 36e5) {
|
|
16
17
|
const mins = Math.round(Math.abs(ms) / 6e4);
|
|
17
|
-
return ms > 0 ?
|
|
18
|
+
return ms > 0 ? t("integration.schedules.time.inMinutes", mins) : t("integration.common.time.minutesAgo", mins);
|
|
18
19
|
}
|
|
19
20
|
if (Math.abs(ms) < 864e5) {
|
|
20
21
|
const hrs = Math.round(Math.abs(ms) / 36e5);
|
|
21
|
-
return ms > 0 ?
|
|
22
|
+
return ms > 0 ? t("integration.schedules.time.inHours", hrs) : t("integration.common.time.hoursAgo", hrs);
|
|
22
23
|
}
|
|
23
24
|
return new Date(iso).toLocaleString(void 0, { month: "short", day: "2-digit", hour: "2-digit", minute: "2-digit" });
|
|
24
25
|
}
|
|
25
|
-
const
|
|
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(() => [
|
|
26
47
|
// Two columns share the slack, and the sentence gets the larger share of it:
|
|
27
48
|
// "Weekdays, hourly from 09:00 to 17:00" is what this page is for, while a
|
|
28
49
|
// workflow name is a label the reader recognises from its first few words.
|
|
@@ -31,26 +52,26 @@ const columns = [
|
|
|
31
52
|
// The expression stays in the search text although the column no longer shows
|
|
32
53
|
// it: support quotes expressions, and a row has to be findable by the string
|
|
33
54
|
// someone was given.
|
|
34
|
-
{ key: "workflow", label: "
|
|
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) ?? ""}` },
|
|
35
56
|
// Sorted by how often a schedule fires. The expression's characters would be
|
|
36
57
|
// meaningless here — the column shows a sentence.
|
|
37
|
-
{ key: "cron", label: "
|
|
38
|
-
{ key: "timezone", label: "
|
|
39
|
-
{ key: "nextRun", label: "
|
|
40
|
-
{ key: "lastRun", label: "
|
|
41
|
-
{ key: "status", label: "
|
|
42
|
-
];
|
|
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
|
+
]);
|
|
43
64
|
</script>
|
|
44
65
|
|
|
45
66
|
<template>
|
|
46
67
|
<IntegrationsLayout>
|
|
47
68
|
<PageHeader
|
|
48
|
-
title="
|
|
49
|
-
description="
|
|
69
|
+
:title="t('integration.schedules.header.title')"
|
|
70
|
+
:description="t('integration.schedules.header.description')"
|
|
50
71
|
>
|
|
51
72
|
<template #actions>
|
|
52
73
|
<span class="text-sm text-muted-foreground">
|
|
53
|
-
<strong class="text-foreground">{{ activeCount }}</strong> active
|
|
74
|
+
<strong class="text-foreground">{{ activeCount }}</strong> {{ t("integration.schedules.stat.active") }}
|
|
54
75
|
</span>
|
|
55
76
|
</template>
|
|
56
77
|
</PageHeader>
|
|
@@ -62,9 +83,9 @@ const columns = [
|
|
|
62
83
|
:columns="columns"
|
|
63
84
|
:row-key="(e2) => e2.id"
|
|
64
85
|
:default-sort="{ column: 'nextRun', direction: 'asc' }"
|
|
65
|
-
:search="{ placeholder: '
|
|
66
|
-
:filter="
|
|
67
|
-
empty-text="
|
|
86
|
+
:search="{ placeholder: t('integration.schedules.search.placeholder') }"
|
|
87
|
+
:filter-tabs="filterTabs"
|
|
88
|
+
:empty-text="t('integration.schedules.state.empty')"
|
|
68
89
|
@row-click="(e2) => router.push(`/integrations/workflows/${e2.workflowId}`)"
|
|
69
90
|
>
|
|
70
91
|
<template #cell-workflow="{ row: e }">
|
|
@@ -92,17 +113,17 @@ const columns = [
|
|
|
92
113
|
at several named times is a long sentence, and half of one is
|
|
93
114
|
the wrong half. Its title is the expression, so there is no
|
|
94
115
|
tooltip left to carry the rest of the words. -->
|
|
95
|
-
<div v-if="describeCron(e.cron)" aria-label="
|
|
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">
|
|
96
117
|
{{ describeCron(e.cron) }}
|
|
97
118
|
</div>
|
|
98
119
|
<template v-else-if="e.cron">
|
|
99
|
-
<div aria-label="
|
|
100
|
-
|
|
120
|
+
<div :aria-label="t('integration.schedules.cell.whenItFires.aria')" class="text-sm truncate">
|
|
121
|
+
{{ t("integration.schedules.cell.customSchedule") }}
|
|
101
122
|
</div>
|
|
102
|
-
<code aria-label="
|
|
123
|
+
<code :aria-label="t('integration.schedules.cell.cronExpression.aria')" class="block font-mono text-xs text-muted-foreground truncate">{{ e.cron }}</code>
|
|
103
124
|
</template>
|
|
104
|
-
<div v-else aria-label="
|
|
105
|
-
|
|
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") }}
|
|
106
127
|
</div>
|
|
107
128
|
</div>
|
|
108
129
|
</template>
|
|
@@ -123,19 +144,19 @@ const columns = [
|
|
|
123
144
|
`specs/accessibility.md`).
|
|
124
145
|
-->
|
|
125
146
|
<template #cell-nextRun="{ row: e }">
|
|
126
|
-
<span aria-label="
|
|
147
|
+
<span :aria-label="t('integration.schedules.table.column.nextRun')" class="text-sm tabular-nums">{{ formatRelative(e.nextRunAt) }}</span>
|
|
127
148
|
</template>
|
|
128
149
|
|
|
129
150
|
<template #cell-lastRun="{ row: e }">
|
|
130
|
-
<span aria-label="
|
|
151
|
+
<span :aria-label="t('integration.schedules.table.column.lastRun')" class="text-sm tabular-nums text-muted-foreground">{{ formatRelative(e.lastRunAt) }}</span>
|
|
131
152
|
</template>
|
|
132
153
|
|
|
133
154
|
<template #cell-status="{ row: e }">
|
|
134
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">
|
|
135
|
-
live
|
|
156
|
+
{{ t("integration.schedules.status.live") }}
|
|
136
157
|
</Badge>
|
|
137
158
|
<Badge v-else variant="outline" class="text-xs font-medium uppercase">
|
|
138
|
-
paused
|
|
159
|
+
{{ t("integration.schedules.status.paused") }}
|
|
139
160
|
</Badge>
|
|
140
161
|
</template>
|
|
141
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
|
|
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
|
-
|
|
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 = "
|
|
49
|
+
formError.value = t("integration.secrets.form.error.invalidKey");
|
|
49
50
|
return;
|
|
50
51
|
}
|
|
51
52
|
if (!newValue.value) {
|
|
52
|
-
formError.value = "
|
|
53
|
+
formError.value = t("integration.secrets.error.valueRequired");
|
|
53
54
|
return;
|
|
54
55
|
}
|
|
55
56
|
saving.value = true;
|
|
@@ -79,39 +80,39 @@ function formatDate(iso) {
|
|
|
79
80
|
return iso;
|
|
80
81
|
}
|
|
81
82
|
}
|
|
82
|
-
const columns = [
|
|
83
|
-
{ key: "key", label: "
|
|
84
|
-
{ key: "created", label: "
|
|
85
|
-
{ key: "updated", label: "
|
|
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="
|
|
94
|
-
description="
|
|
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
|
-
|
|
102
|
+
{{ t("integration.secrets.action.new") }}
|
|
102
103
|
</Button>
|
|
103
104
|
</DialogTrigger>
|
|
104
105
|
<DialogContent>
|
|
105
106
|
<DialogHeader>
|
|
106
|
-
<DialogTitle>
|
|
107
|
+
<DialogTitle>{{ t("integration.secrets.dialog.create.title") }}</DialogTitle>
|
|
107
108
|
<DialogDescription class="text-sm">
|
|
108
|
-
|
|
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">
|
|
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
|
-
|
|
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">
|
|
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,7 +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"
|
|
139
|
-
:aria-label="showValue ? '
|
|
140
|
+
:aria-label="showValue ? t('integration.secrets.form.value.hide') : t('integration.secrets.form.value.show')"
|
|
140
141
|
@click="showValue = !showValue"
|
|
141
142
|
>
|
|
142
143
|
<component :is="showValue ? EyeOff : Eye" :size="14" />
|
|
@@ -151,11 +152,11 @@ const columns = [
|
|
|
151
152
|
|
|
152
153
|
<DialogFooter>
|
|
153
154
|
<Button variant="outline" :disabled="saving" @click="createOpen = false">
|
|
154
|
-
|
|
155
|
+
{{ t("integration.common.action.cancel") }}
|
|
155
156
|
</Button>
|
|
156
157
|
<Button :disabled="saving" @click="submitCreate">
|
|
157
158
|
<Loader2 v-if="saving" :size="14" class="animate-spin" />
|
|
158
|
-
{{ saving ? "
|
|
159
|
+
{{ saving ? t("integration.common.state.saving") : t("integration.common.action.save") }}
|
|
159
160
|
</Button>
|
|
160
161
|
</DialogFooter>
|
|
161
162
|
</DialogContent>
|
|
@@ -173,9 +174,8 @@ const columns = [
|
|
|
173
174
|
:columns="columns"
|
|
174
175
|
:row-key="(s2) => s2.key"
|
|
175
176
|
:default-sort="{ column: 'key', direction: 'asc' }"
|
|
176
|
-
:search="{ placeholder: '
|
|
177
|
-
:
|
|
178
|
-
empty-text="No secrets yet."
|
|
177
|
+
:search="{ placeholder: t('integration.secrets.search.placeholder') }"
|
|
178
|
+
:empty-text="t('integration.secrets.state.empty')"
|
|
179
179
|
>
|
|
180
180
|
<template #cell-key="{ row: s }">
|
|
181
181
|
<div class="flex items-center gap-2 min-w-0">
|
|
@@ -184,7 +184,7 @@ const columns = [
|
|
|
184
184
|
</div>
|
|
185
185
|
<div class="min-w-0">
|
|
186
186
|
<div class="font-mono text-sm truncate">{{ s.key }}</div>
|
|
187
|
-
<div class="text-sm text-muted-foreground">
|
|
187
|
+
<div class="text-sm text-muted-foreground">{{ t("integration.secrets.cell.masked") }}</div>
|
|
188
188
|
</div>
|
|
189
189
|
</div>
|
|
190
190
|
</template>
|
|
@@ -204,7 +204,7 @@ const columns = [
|
|
|
204
204
|
<component
|
|
205
205
|
:is="s.created_at ? 'time' : 'span'"
|
|
206
206
|
:datetime="s.created_at || void 0"
|
|
207
|
-
:aria-label="
|
|
207
|
+
:aria-label="t('integration.secrets.cell.created.aria', { key: s.key, date: formatDate(s.created_at) })"
|
|
208
208
|
class="text-sm tabular-nums whitespace-nowrap text-muted-foreground"
|
|
209
209
|
>{{ formatDate(s.created_at) }}</component>
|
|
210
210
|
</template>
|
|
@@ -213,7 +213,7 @@ const columns = [
|
|
|
213
213
|
<component
|
|
214
214
|
:is="s.updated_at ? 'time' : 'span'"
|
|
215
215
|
:datetime="s.updated_at || void 0"
|
|
216
|
-
:aria-label="
|
|
216
|
+
:aria-label="t('integration.secrets.cell.updated.aria', { key: s.key, date: formatDate(s.updated_at) })"
|
|
217
217
|
class="text-sm tabular-nums whitespace-nowrap text-muted-foreground"
|
|
218
218
|
>{{ formatDate(s.updated_at) }}</component>
|
|
219
219
|
</template>
|
|
@@ -222,16 +222,16 @@ const columns = [
|
|
|
222
222
|
<div @click.stop>
|
|
223
223
|
<DropdownMenu>
|
|
224
224
|
<DropdownMenuTrigger as-child>
|
|
225
|
-
<Button variant="ghost" size="icon" class="size-7" :aria-label="
|
|
225
|
+
<Button variant="ghost" size="icon" class="size-7" :aria-label="t('integration.secrets.cell.actions.aria', { key: s.key })">
|
|
226
226
|
<MoreHorizontal :size="14" />
|
|
227
227
|
</Button>
|
|
228
228
|
</DropdownMenuTrigger>
|
|
229
229
|
<DropdownMenuContent align="end">
|
|
230
|
-
<DropdownMenuLabel>
|
|
230
|
+
<DropdownMenuLabel>{{ t("integration.secrets.menu.label") }}</DropdownMenuLabel>
|
|
231
231
|
<DropdownMenuSeparator />
|
|
232
232
|
<DropdownMenuItem class="text-destructive focus:text-destructive" @select="pendingDelete = s">
|
|
233
|
-
<
|
|
234
|
-
|
|
233
|
+
<TrashBin :size="14" />
|
|
234
|
+
{{ t("integration.common.action.delete") }}
|
|
235
235
|
</DropdownMenuItem>
|
|
236
236
|
</DropdownMenuContent>
|
|
237
237
|
</DropdownMenu>
|
|
@@ -245,21 +245,21 @@ const columns = [
|
|
|
245
245
|
}">
|
|
246
246
|
<AlertDialogContent>
|
|
247
247
|
<AlertDialogHeader>
|
|
248
|
-
<AlertDialogTitle>
|
|
248
|
+
<AlertDialogTitle>{{ t("integration.secrets.dialog.delete.title") }}</AlertDialogTitle>
|
|
249
249
|
<AlertDialogDescription>
|
|
250
250
|
<span v-if="pendingDelete">
|
|
251
251
|
<span class="font-mono font-medium text-foreground">{{ pendingDelete.key }}</span>
|
|
252
|
-
|
|
252
|
+
{{ t("integration.secrets.dialog.delete.body") }}
|
|
253
253
|
</span>
|
|
254
254
|
</AlertDialogDescription>
|
|
255
255
|
</AlertDialogHeader>
|
|
256
256
|
<AlertDialogFooter>
|
|
257
257
|
<Button variant="outline" :disabled="deleting" @click="pendingDelete = null">
|
|
258
|
-
|
|
258
|
+
{{ t("integration.common.action.cancel") }}
|
|
259
259
|
</Button>
|
|
260
260
|
<Button class="bg-destructive text-white hover:bg-destructive/90" :disabled="deleting" @click="confirmDelete">
|
|
261
261
|
<Loader2 v-if="deleting" :size="14" class="animate-spin" />
|
|
262
|
-
{{ deleting ? "
|
|
262
|
+
{{ deleting ? t("integration.common.state.deleting") : t("integration.common.action.delete") }}
|
|
263
263
|
</Button>
|
|
264
264
|
</AlertDialogFooter>
|
|
265
265
|
</AlertDialogContent>
|