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