@revenexx/studio-integrations 1.1.0 → 1.2.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 +520 -0
- package/README.md +3 -1
- package/dist/module.json +1 -1
- package/dist/runtime/components/Integrations/CredentialCreateDialog.vue +6 -18
- package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +13 -23
- package/dist/runtime/components/Integrations/CredentialRevenexxSource.vue +7 -6
- package/dist/runtime/components/Integrations/DeadLettersPanel.vue +15 -4
- package/dist/runtime/components/Integrations/ExpressionInput.d.vue.ts +4 -0
- package/dist/runtime/components/Integrations/ExpressionInput.vue +77 -22
- package/dist/runtime/components/Integrations/ExpressionInput.vue.d.ts +4 -0
- package/dist/runtime/components/Integrations/GroupFrameView.vue +5 -4
- package/dist/runtime/components/Integrations/LoadingAnnouncement.d.vue.ts +38 -0
- package/dist/runtime/components/Integrations/LoadingAnnouncement.vue +16 -0
- package/dist/runtime/components/Integrations/LoadingAnnouncement.vue.d.ts +38 -0
- package/dist/runtime/components/Integrations/NodeConfigField.vue +23 -0
- package/dist/runtime/components/Integrations/NodeInspector.d.vue.ts +2 -2
- package/dist/runtime/components/Integrations/NodeInspector.vue +25 -32
- package/dist/runtime/components/Integrations/NodeInspector.vue.d.ts +2 -2
- package/dist/runtime/components/Integrations/RunActionsMenu.vue +5 -3
- package/dist/runtime/components/Integrations/RunDetailPanel.vue +5 -17
- package/dist/runtime/components/Integrations/RunResponseModal.d.vue.ts +30 -8
- package/dist/runtime/components/Integrations/RunResponseModal.vue +92 -24
- package/dist/runtime/components/Integrations/RunResponseModal.vue.d.ts +30 -8
- package/dist/runtime/components/Integrations/RunsTable.d.vue.ts +61 -3
- package/dist/runtime/components/Integrations/RunsTable.vue +143 -20
- package/dist/runtime/components/Integrations/RunsTable.vue.d.ts +61 -3
- package/dist/runtime/components/Integrations/SftpAccountImportDialog.vue +10 -1
- package/dist/runtime/components/Integrations/StickyNoteView.vue +1 -1
- package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +18 -3
- package/dist/runtime/components/Integrations/WorkflowDeleteDialog.d.vue.ts +18 -0
- package/dist/runtime/components/Integrations/WorkflowDeleteDialog.vue +70 -0
- package/dist/runtime/components/Integrations/WorkflowDeleteDialog.vue.d.ts +18 -0
- package/dist/runtime/components/Integrations/WorkflowDuplicateDialog.vue +2 -1
- package/dist/runtime/components/Integrations/WorkflowEdgeView.vue +4 -4
- package/dist/runtime/components/Integrations/WorkflowEditor.vue +39 -28
- package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue +17 -6
- package/dist/runtime/components/Integrations/WorkflowNodeView.vue +17 -21
- package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +8 -11
- package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +6 -7
- package/dist/runtime/components/Integrations/WorkflowVariablesDialog.vue +7 -7
- package/dist/runtime/components/Integrations/WorkflowsTable.vue +10 -48
- package/dist/runtime/components/Integrations/canvasSides.d.ts +48 -0
- package/dist/runtime/components/Integrations/canvasSides.js +22 -0
- package/dist/runtime/composables/useCredentialTypes.js +2 -6
- package/dist/runtime/composables/useIntegrationsAudit.js +2 -2
- package/dist/runtime/composables/useIntegrationsCredentials.js +4 -8
- package/dist/runtime/composables/useIntegrationsDeadLetters.d.ts +4 -0
- package/dist/runtime/composables/useIntegrationsDeadLetters.js +16 -1
- package/dist/runtime/composables/useIntegrationsNodes.js +2 -2
- package/dist/runtime/composables/useIntegrationsRevisions.js +2 -6
- package/dist/runtime/composables/useIntegrationsRuns.d.ts +4 -15
- package/dist/runtime/composables/useIntegrationsRuns.js +49 -34
- package/dist/runtime/composables/useIntegrationsSchedules.js +2 -2
- package/dist/runtime/composables/useIntegrationsSecrets.js +2 -6
- package/dist/runtime/composables/useIntegrationsTemplates.js +2 -5
- package/dist/runtime/composables/useIntegrationsTriggers.js +2 -2
- package/dist/runtime/composables/useIntegrationsWorkflows.js +4 -7
- package/dist/runtime/composables/useNodeConfigResolve.js +2 -2
- package/dist/runtime/composables/useOAuthConnect.d.ts +42 -0
- package/dist/runtime/composables/useOAuthConnect.js +33 -0
- package/dist/runtime/data/expressionEditor.d.ts +17 -5
- package/dist/runtime/data/expressionEditor.js +11 -5
- package/dist/runtime/data/handleId.d.ts +61 -1
- package/dist/runtime/data/handleId.js +8 -1
- package/dist/runtime/data/payloadForm.js +3 -2
- package/dist/runtime/i18n/messages/de.json +30 -11
- package/dist/runtime/i18n/messages/en.json +27 -8
- package/dist/runtime/pages/integrations/audit.vue +7 -6
- package/dist/runtime/pages/integrations/credentials.vue +9 -21
- package/dist/runtime/pages/integrations/index.vue +70 -12
- package/dist/runtime/pages/integrations/runs.vue +116 -7
- package/dist/runtime/pages/integrations/secrets.vue +2 -1
- package/dist/runtime/pages/integrations/templates/[slug].vue +2 -1
- package/dist/runtime/pages/integrations/templates/index.vue +18 -15
- package/dist/runtime/pages/integrations/workflows/[id].vue +177 -28
- package/dist/runtime/types/integrations.d.ts +5 -0
- package/dist/runtime/utils/apiError.d.ts +21 -1
- package/dist/runtime/utils/apiError.js +1 -1
- package/dist/runtime/utils/clipboard.d.ts +33 -0
- package/dist/runtime/utils/clipboard.js +11 -0
- package/dist/runtime/utils/deepClone.d.ts +41 -0
- package/dist/runtime/utils/deepClone.js +3 -0
- package/dist/runtime/utils/formatDuration.d.ts +44 -0
- package/dist/runtime/utils/formatDuration.js +13 -0
- package/dist/runtime/utils/nodeDisplayName.d.ts +22 -0
- package/dist/runtime/utils/nodeDisplayName.js +6 -0
- package/dist/runtime/utils/notify.d.ts +1 -1
- package/dist/runtime/utils/runEnvelope.d.ts +31 -0
- package/dist/runtime/utils/runEnvelope.js +12 -0
- package/dist/runtime/utils/runFeedQuery.d.ts +135 -0
- package/dist/runtime/utils/runFeedQuery.js +66 -0
- package/dist/runtime/utils/runOutputs.d.ts +33 -0
- package/dist/runtime/utils/runOutputs.js +10 -0
- package/dist/runtime/utils/statusStyle.d.ts +24 -0
- package/dist/runtime/utils/statusStyle.js +1 -0
- package/dist/runtime/utils/validationFindings.d.ts +26 -3
- package/dist/runtime/utils/validationFindings.js +18 -3
- package/package.json +1 -1
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { computed, ref, watch } from "vue";
|
|
3
|
+
import LoadingAnnouncement from "../../components/Integrations/LoadingAnnouncement.vue";
|
|
3
4
|
import SurfaceCard from "../../components/Integrations/SurfaceCard.vue";
|
|
4
5
|
import TemplateCard from "../../components/Integrations/TemplateCard.vue";
|
|
5
6
|
import WorkflowCreateDialog from "../../components/Integrations/WorkflowCreateDialog.vue";
|
|
6
7
|
import { Pulse as Activity, DangerTriangle as AlertTriangle, ArrowRight, Box as Boxes, ClockCircle as Clock, Database, Layers, AddCircle as Plus, Radio, MagicStick as Sparkles, LinkRound as Webhook, Programming as Workflow } from "#solar-icons";
|
|
7
8
|
import { Badge, Button } from "@revenexx/studio";
|
|
9
|
+
import { formatDuration } from "../../utils/formatDuration";
|
|
8
10
|
import { servedPaths } from "../../utils/servedPaths";
|
|
9
|
-
import { runStatusClass, runStatusLabel } from "../../utils/statusStyle";
|
|
11
|
+
import { RUN_STATUS_COLUMN_WIDTH, runStatusClass, runStatusLabel } from "../../utils/statusStyle";
|
|
12
|
+
const LATEST_RUN_COLUMNS = `${RUN_STATUS_COLUMN_WIDTH} minmax(0,1fr) 100px 70px`;
|
|
10
13
|
const t = useIntegrationsT();
|
|
11
14
|
useHead({ title: () => t("integration.dashboard.head.title") });
|
|
12
15
|
const { workflows, activeCount: activeWorkflows, loading: workflowsLoading, load: loadWorkflows } = useIntegrationsWorkflows();
|
|
@@ -21,10 +24,19 @@ const { startingPoints, loading: templatesLoading, load: loadTemplates, ensureDe
|
|
|
21
24
|
watch(() => startingPoints.value.map((t2) => t2.slug).join(","), () => {
|
|
22
25
|
if (startingPoints.value.length) void ensureDetails(startingPoints.value.map((t2) => t2.slug));
|
|
23
26
|
}, { immediate: true });
|
|
24
|
-
const { activeCount: schedulesActive, load: loadSchedules } = useIntegrationsSchedules();
|
|
27
|
+
const { activeCount: schedulesActive, loading: schedulesLoading, load: loadSchedules } = useIntegrationsSchedules();
|
|
25
28
|
const { pendingCount: missedRuns, globalLoading: missedLoading, loadGlobal: loadMissedRuns } = useIntegrationsDeadLetters();
|
|
26
|
-
const
|
|
29
|
+
const kpiWaits = computed(() => ({
|
|
30
|
+
activeWorkflows: workflowsLoading.value && !workflows.value.length,
|
|
31
|
+
runningNow: runsLoading.value && !latestRunPerWorkflow.value.length,
|
|
32
|
+
scheduled: schedulesLoading.value && schedulesActive.value === 0,
|
|
33
|
+
missedRuns: missedLoading.value && missedRuns.value === 0
|
|
34
|
+
}));
|
|
27
35
|
const latestLoading = computed(() => runsLoading.value && !latestRunPerWorkflow.value.length);
|
|
36
|
+
const awaitingKpis = computed(() => Object.values(kpiWaits.value).some(Boolean));
|
|
37
|
+
const awaitingLatestRuns = computed(() => latestLoading.value && !latestRunPerWorkflow.value.length);
|
|
38
|
+
const awaitingAttention = computed(() => (missedLoading.value || latestLoading.value) && !missedRuns.value && !failedRecent.value.length);
|
|
39
|
+
const awaitingStartingPoints = computed(() => templatesLoading.value && !startingPoints.value.length);
|
|
28
40
|
const createOpen = ref(false);
|
|
29
41
|
const NuxtLinkComp = resolveComponent("NuxtLink");
|
|
30
42
|
onMounted(() => {
|
|
@@ -50,6 +62,7 @@ function formatRelative(iso) {
|
|
|
50
62
|
const kpis = computed(() => [
|
|
51
63
|
{
|
|
52
64
|
key: "activeWorkflows",
|
|
65
|
+
waiting: kpiWaits.value.activeWorkflows,
|
|
53
66
|
label: t("integration.dashboard.kpi.activeWorkflows.label"),
|
|
54
67
|
value: activeWorkflows.value,
|
|
55
68
|
// A plural key, so the count is the second argument. Called without one it
|
|
@@ -60,6 +73,7 @@ const kpis = computed(() => [
|
|
|
60
73
|
},
|
|
61
74
|
{
|
|
62
75
|
key: "runningNow",
|
|
76
|
+
waiting: kpiWaits.value.runningNow,
|
|
63
77
|
label: t("integration.dashboard.kpi.runningNow.label"),
|
|
64
78
|
value: runningCount.value,
|
|
65
79
|
sub: t("integration.dashboard.kpi.runningNow.sub"),
|
|
@@ -68,6 +82,7 @@ const kpis = computed(() => [
|
|
|
68
82
|
},
|
|
69
83
|
{
|
|
70
84
|
key: "scheduled",
|
|
85
|
+
waiting: kpiWaits.value.scheduled,
|
|
71
86
|
label: t("integration.dashboard.kpi.scheduled.label"),
|
|
72
87
|
value: schedulesActive.value,
|
|
73
88
|
sub: t("integration.dashboard.kpi.scheduled.sub"),
|
|
@@ -76,6 +91,7 @@ const kpis = computed(() => [
|
|
|
76
91
|
},
|
|
77
92
|
{
|
|
78
93
|
key: "missedRuns",
|
|
94
|
+
waiting: kpiWaits.value.missedRuns,
|
|
79
95
|
label: t("integration.dashboard.kpi.missedRuns.label"),
|
|
80
96
|
value: missedRuns.value,
|
|
81
97
|
sub: t("integration.dashboard.kpi.missedRuns.sub"),
|
|
@@ -123,13 +139,22 @@ function surfaceCards() {
|
|
|
123
139
|
contents — named "Missed runs" alone it would announce less than the
|
|
124
140
|
three tiles beside it. -->
|
|
125
141
|
<div role="group" :aria-label="t('integration.dashboard.kpi.aria.group')" class="grid gap-2 sm:grid-cols-2 lg:grid-cols-4">
|
|
142
|
+
<!-- Four panels on this page wait on four sources, so each says so under its
|
|
143
|
+
own name (PO-251). The tiles' own names drop their figure while this is
|
|
144
|
+
true: a tile named "Active workflows: 0" from an empty list does not read
|
|
145
|
+
as waiting, it reads as an answer. -->
|
|
146
|
+
<LoadingAnnouncement
|
|
147
|
+
:label="t('integration.dashboard.kpi.aria.group')"
|
|
148
|
+
:loading="awaitingKpis"
|
|
149
|
+
:text="t('integration.dashboard.kpi.state.loading')"
|
|
150
|
+
/>
|
|
126
151
|
<component
|
|
127
152
|
:is="k.to ? NuxtLinkComp : 'div'"
|
|
128
153
|
v-for="k in kpis"
|
|
129
154
|
:key="k.key"
|
|
130
155
|
:to="k.to"
|
|
131
156
|
:role="k.to ? void 0 : 'group'"
|
|
132
|
-
:aria-label="`${k.label}: ${k.value}`"
|
|
157
|
+
:aria-label="k.waiting ? k.label : `${k.label}: ${k.value}`"
|
|
133
158
|
class="block rounded-lg border border-black/[0.06] bg-white p-3"
|
|
134
159
|
:class="k.to ? 'transition hover:border-black/20' : ''"
|
|
135
160
|
>
|
|
@@ -138,7 +163,7 @@ function surfaceCards() {
|
|
|
138
163
|
<div class="text-mono text-muted-foreground truncate">
|
|
139
164
|
{{ k.label }}
|
|
140
165
|
</div>
|
|
141
|
-
<div v-if="
|
|
166
|
+
<div v-if="k.waiting" aria-hidden="true" class="my-1.5 h-6 w-12 rounded bg-black/[0.07] animate-pulse" />
|
|
142
167
|
<div v-else class="text-2xl font-semibold tabular-nums text-[#0B0522]">
|
|
143
168
|
{{ k.value }}
|
|
144
169
|
</div>
|
|
@@ -168,6 +193,11 @@ function surfaceCards() {
|
|
|
168
193
|
{{ t("integration.common.action.viewAll") }} <ArrowRight :size="11" />
|
|
169
194
|
</NuxtLink>
|
|
170
195
|
</div>
|
|
196
|
+
<LoadingAnnouncement
|
|
197
|
+
:label="t('integration.dashboard.runs.title')"
|
|
198
|
+
:loading="awaitingLatestRuns"
|
|
199
|
+
:text="t('integration.dashboard.runs.state.loading')"
|
|
200
|
+
/>
|
|
171
201
|
<div>
|
|
172
202
|
<!-- An entry is a link, not a div that answers a click: that is what
|
|
173
203
|
makes it reachable by keyboard, and it is what gives it a name —
|
|
@@ -177,9 +207,18 @@ function surfaceCards() {
|
|
|
177
207
|
v-for="r in latestPerWorkflow"
|
|
178
208
|
:key="r.id"
|
|
179
209
|
:to="{ path: `/integrations/workflows/${r.workflowId}`, query: { run: r.id } }"
|
|
180
|
-
class="grid
|
|
210
|
+
class="grid items-center gap-2 border-b border-black/[0.04] px-3 py-2 text-sm last:border-b-0 hover:bg-muted/30"
|
|
211
|
+
:style="{ gridTemplateColumns: LATEST_RUN_COLUMNS }"
|
|
181
212
|
>
|
|
182
|
-
|
|
213
|
+
<!-- The runs list's own track, not a second guess: this panel was the
|
|
214
|
+
narrower of the two at 90px, so three German words ran out of it,
|
|
215
|
+
"erfolgreich" included (PO-362). `max-w-full` is the net behind it,
|
|
216
|
+
and it clips rather than ellipsing — see the runs list. -->
|
|
217
|
+
<Badge
|
|
218
|
+
variant="outline"
|
|
219
|
+
:title="runStatusLabel(r.status)"
|
|
220
|
+
:class="['border text-xs font-medium uppercase max-w-full', runStatusClass(r.status)]"
|
|
221
|
+
>
|
|
183
222
|
{{ runStatusLabel(r.status) }}
|
|
184
223
|
</Badge>
|
|
185
224
|
<div class="truncate">
|
|
@@ -189,15 +228,24 @@ function surfaceCards() {
|
|
|
189
228
|
<div class="text-sm text-muted-foreground tabular-nums truncate">
|
|
190
229
|
{{ formatRelative(r.startedAt) }}
|
|
191
230
|
</div>
|
|
231
|
+
<!-- The ellipsis is this panel's own answer and not the formatter's em
|
|
232
|
+
dash: an entry here is the latest run of a workflow, so a missing
|
|
233
|
+
duration means it is still going, where the runs table counts up a
|
|
234
|
+
live figure instead. The figure itself is the shared spelling
|
|
235
|
+
(PO-383) — this cell used to divide by a thousand on the spot and
|
|
236
|
+
print two decimals, which said `0.90s` for 900 ms and `63.00s` for a
|
|
237
|
+
step the run record called `1m 3s`. -->
|
|
192
238
|
<div class="text-sm text-muted-foreground tabular-nums text-right">
|
|
193
|
-
{{ r.durationMs == null ? "\u2026" :
|
|
239
|
+
{{ r.durationMs == null ? "\u2026" : formatDuration(r.durationMs) }}
|
|
194
240
|
</div>
|
|
195
241
|
</NuxtLink>
|
|
196
|
-
<template v-if="
|
|
242
|
+
<template v-if="awaitingLatestRuns">
|
|
197
243
|
<div
|
|
198
244
|
v-for="n in 6"
|
|
199
245
|
:key="`sk-run-${n}`"
|
|
200
|
-
|
|
246
|
+
aria-hidden="true"
|
|
247
|
+
class="grid items-center gap-2 border-b border-black/[0.04] px-3 py-2.5 last:border-b-0"
|
|
248
|
+
:style="{ gridTemplateColumns: LATEST_RUN_COLUMNS }"
|
|
201
249
|
>
|
|
202
250
|
<div class="h-5 w-16 rounded bg-black/[0.07] animate-pulse" />
|
|
203
251
|
<div class="space-y-1.5">
|
|
@@ -224,7 +272,12 @@ function surfaceCards() {
|
|
|
224
272
|
<h3 class="text-sm font-semibold">{{ t("integration.dashboard.attention.title") }}</h3>
|
|
225
273
|
</div>
|
|
226
274
|
</div>
|
|
227
|
-
<
|
|
275
|
+
<LoadingAnnouncement
|
|
276
|
+
:label="t('integration.dashboard.attention.title')"
|
|
277
|
+
:loading="awaitingAttention"
|
|
278
|
+
:text="t('integration.dashboard.attention.state.loading')"
|
|
279
|
+
/>
|
|
280
|
+
<div v-if="awaitingAttention" aria-hidden="true" class="space-y-2 p-3">
|
|
228
281
|
<div v-for="n in 3" :key="`sk-att-${n}`" class="h-3 rounded bg-black/[0.07] animate-pulse" :class="n === 1 ? 'w-2/3' : n === 2 ? 'w-1/2' : 'w-3/5'" />
|
|
229
282
|
</div>
|
|
230
283
|
<div v-else-if="missedRuns || failedRecent.length">
|
|
@@ -294,7 +347,12 @@ function surfaceCards() {
|
|
|
294
347
|
{{ t("integration.common.action.seeAll") }} <ArrowRight :size="11" />
|
|
295
348
|
</NuxtLink>
|
|
296
349
|
</div>
|
|
297
|
-
<
|
|
350
|
+
<LoadingAnnouncement
|
|
351
|
+
:label="t('integration.dashboard.templates.title')"
|
|
352
|
+
:loading="awaitingStartingPoints"
|
|
353
|
+
:text="t('integration.dashboard.templates.state.loading')"
|
|
354
|
+
/>
|
|
355
|
+
<div v-if="awaitingStartingPoints" aria-hidden="true" class="grid gap-2 p-3 sm:grid-cols-2 lg:grid-cols-4">
|
|
298
356
|
<div v-for="n in 4" :key="`sk-tpl-${n}`" class="rounded-md border border-black/[0.06] bg-white p-3 space-y-2">
|
|
299
357
|
<div class="h-5 w-20 rounded bg-black/[0.07] animate-pulse" />
|
|
300
358
|
<div class="h-3 w-3/4 rounded bg-black/[0.07] animate-pulse" />
|
|
@@ -2,21 +2,52 @@
|
|
|
2
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
|
+
import LoadingAnnouncement from "../../components/Integrations/LoadingAnnouncement.vue";
|
|
5
6
|
import RunsTable from "../../components/Integrations/RunsTable.vue";
|
|
6
7
|
import { createPollBudget, SAFETY_POLL_MS } from "../../utils/pollBudget";
|
|
7
8
|
import { deadLetterStatusClass, deadLetterStatusLabel } from "../../utils/statusStyle";
|
|
9
|
+
import { errorReason } from "../../utils/apiError";
|
|
8
10
|
const t = useIntegrationsT();
|
|
9
11
|
useHead({ title: () => t("integration.runs.list.head.title") });
|
|
10
12
|
const route = useRoute();
|
|
11
|
-
const { runs, loading, load } = useIntegrationsRuns();
|
|
13
|
+
const { runs, loading, error, load, query, setQuery, narrowedTotal } = useIntegrationsRuns();
|
|
12
14
|
const {
|
|
13
15
|
globalItems: missed,
|
|
14
16
|
globalLoading: missedLoading,
|
|
15
17
|
pendingCount,
|
|
18
|
+
globalPage: missedPage,
|
|
19
|
+
globalLastPage: missedLastPage,
|
|
20
|
+
goGlobalPage,
|
|
21
|
+
resetGlobalPage,
|
|
16
22
|
loadGlobal,
|
|
17
23
|
replay,
|
|
18
|
-
discard
|
|
24
|
+
discard,
|
|
25
|
+
summary: missedSummary
|
|
19
26
|
} = useIntegrationsDeadLetters();
|
|
27
|
+
const page = computed({
|
|
28
|
+
get: () => query.value.page,
|
|
29
|
+
// Only when it actually moved. The shared table returns to its first page
|
|
30
|
+
// whenever the search or the page size changes, and reports that as a page
|
|
31
|
+
// change too — so an unguarded setter would send a second request for the page
|
|
32
|
+
// the list is already on, beside the one the search itself asks for.
|
|
33
|
+
set: (next) => {
|
|
34
|
+
if (next !== query.value.page) void setQuery({ page: next });
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
function onNarrow(patch) {
|
|
38
|
+
void setQuery(patch);
|
|
39
|
+
}
|
|
40
|
+
function onReorder(order) {
|
|
41
|
+
void setQuery(order);
|
|
42
|
+
}
|
|
43
|
+
let searchTimer;
|
|
44
|
+
function onSearch(text) {
|
|
45
|
+
clearTimeout(searchTimer);
|
|
46
|
+
if (text === query.value.search) return;
|
|
47
|
+
searchTimer = setTimeout(() => {
|
|
48
|
+
void setQuery({ search: text });
|
|
49
|
+
}, 250);
|
|
50
|
+
}
|
|
20
51
|
const tab = ref(route.query.tab === "missed" ? "missed" : "runs");
|
|
21
52
|
const tabs = computed(() => [
|
|
22
53
|
{ key: "runs", label: t("integration.runs.tabs.runs.label") },
|
|
@@ -36,7 +67,8 @@ watch([runTick, workflowTick], () => {
|
|
|
36
67
|
});
|
|
37
68
|
let pollTimer = null;
|
|
38
69
|
onMounted(() => {
|
|
39
|
-
void
|
|
70
|
+
void setQuery({ search: "", page: 1 });
|
|
71
|
+
resetGlobalPage();
|
|
40
72
|
void reloadMissedRuns();
|
|
41
73
|
pollTimer = setInterval(() => {
|
|
42
74
|
if (hasActiveRuns.value && !loading.value && budget.due(delivering.value)) void load(true);
|
|
@@ -47,8 +79,15 @@ onUnmounted(() => {
|
|
|
47
79
|
clearInterval(pollTimer);
|
|
48
80
|
pollTimer = null;
|
|
49
81
|
}
|
|
82
|
+
clearTimeout(searchTimer);
|
|
50
83
|
});
|
|
51
84
|
const busyId = ref(null);
|
|
85
|
+
async function goMissedPage(page2) {
|
|
86
|
+
try {
|
|
87
|
+
await goGlobalPage(page2);
|
|
88
|
+
} catch {
|
|
89
|
+
}
|
|
90
|
+
}
|
|
52
91
|
async function reloadMissedRuns() {
|
|
53
92
|
try {
|
|
54
93
|
await loadGlobal(true);
|
|
@@ -62,7 +101,7 @@ async function onReplay(dl) {
|
|
|
62
101
|
const newId = res && typeof res === "object" && "id" in res ? String(res.id) : null;
|
|
63
102
|
toast.success(newId ? t("integration.deadLetters.toast.replay.started", { id: newId }) : t("integration.deadLetters.toast.replay.done"));
|
|
64
103
|
} catch (e) {
|
|
65
|
-
toast.error(e
|
|
104
|
+
toast.error(errorReason(e, t("integration.deadLetters.toast.replay.error")));
|
|
66
105
|
busyId.value = null;
|
|
67
106
|
return;
|
|
68
107
|
}
|
|
@@ -76,7 +115,7 @@ async function onDiscard(dl) {
|
|
|
76
115
|
await discard(dl.id);
|
|
77
116
|
toast.success(t("integration.deadLetters.toast.discard.done"));
|
|
78
117
|
} catch (e) {
|
|
79
|
-
toast.error(e
|
|
118
|
+
toast.error(errorReason(e, t("integration.deadLetters.toast.discard.error")));
|
|
80
119
|
busyId.value = null;
|
|
81
120
|
return;
|
|
82
121
|
}
|
|
@@ -107,7 +146,49 @@ function fmt(iso) {
|
|
|
107
146
|
<TabBar v-model="tab" :tabs="tabs" class="px-4" />
|
|
108
147
|
|
|
109
148
|
<div class="p-4 space-y-4">
|
|
110
|
-
|
|
149
|
+
<!-- Above the tab chain on purpose: the missed-runs half is drawn by a
|
|
150
|
+
`v-else`, so a region placed inside it would mount together with the
|
|
151
|
+
placeholders it speaks for, and a live region that arrives with its own
|
|
152
|
+
content is one nothing was listening to. Out here it is on the page from
|
|
153
|
+
the first render and only its text moves (PO-251). -->
|
|
154
|
+
<LoadingAnnouncement
|
|
155
|
+
:label="t('integration.deadLetters.tabs.missed.label')"
|
|
156
|
+
:loading="tab === 'missed' && missedLoading && !missed.length"
|
|
157
|
+
:text="t('integration.deadLetters.state.loading')"
|
|
158
|
+
/>
|
|
159
|
+
|
|
160
|
+
<!--
|
|
161
|
+
A refused read, said plainly. Without this the page kept the rows it had
|
|
162
|
+
while `query` had already moved on, so a failed page turn drew rows 1–25
|
|
163
|
+
under a footer reading "Showing 26–50", and a refused narrowing left the
|
|
164
|
+
old rows under the new selection — both reading as an answer.
|
|
165
|
+
-->
|
|
166
|
+
<div
|
|
167
|
+
v-if="tab === 'runs' && error"
|
|
168
|
+
class="rounded-md border border-rose-500/20 bg-rose-500/5 px-3 py-2 text-sm text-rose-700"
|
|
169
|
+
role="alert"
|
|
170
|
+
>
|
|
171
|
+
{{ error }}
|
|
172
|
+
</div>
|
|
173
|
+
|
|
174
|
+
<RunsTable
|
|
175
|
+
v-if="tab === 'runs'"
|
|
176
|
+
v-model:page="page"
|
|
177
|
+
:runs="runs"
|
|
178
|
+
:loading="loading"
|
|
179
|
+
server-mode
|
|
180
|
+
:total="narrowedTotal"
|
|
181
|
+
:per-page="query.perPage"
|
|
182
|
+
:status="query.status"
|
|
183
|
+
:trigger-type="query.triggerType"
|
|
184
|
+
:sort="query.sort"
|
|
185
|
+
:direction="query.direction"
|
|
186
|
+
@narrow="onNarrow"
|
|
187
|
+
@search="onSearch"
|
|
188
|
+
@page-size="(size) => setQuery({ perPage: size })"
|
|
189
|
+
@reorder="onReorder"
|
|
190
|
+
@changed="reload"
|
|
191
|
+
/>
|
|
111
192
|
|
|
112
193
|
<!-- Missed runs — trigger firings that never produced a run (the
|
|
113
194
|
workflow wasn't runnable when its schedule/event fired). -->
|
|
@@ -122,7 +203,7 @@ function fmt(iso) {
|
|
|
122
203
|
</Button>
|
|
123
204
|
</div>
|
|
124
205
|
|
|
125
|
-
<div v-if="missedLoading && !missed.length"><div class="rounded-lg border border-black/[0.06] bg-white divide-y divide-black/[0.04]">
|
|
206
|
+
<div v-if="missedLoading && !missed.length" aria-hidden="true"><div class="rounded-lg border border-black/[0.06] bg-white divide-y divide-black/[0.04]">
|
|
126
207
|
<div v-for="n in 4" :key="`sk-${n}`" class="flex items-start gap-3 px-3 py-3">
|
|
127
208
|
<div class="h-5 w-16 shrink-0 rounded bg-black/[0.07] animate-pulse" />
|
|
128
209
|
<div class="min-w-0 flex-1 space-y-2">
|
|
@@ -168,6 +249,34 @@ function fmt(iso) {
|
|
|
168
249
|
</div>
|
|
169
250
|
</div>
|
|
170
251
|
</div>
|
|
252
|
+
|
|
253
|
+
<!--
|
|
254
|
+
Server pagination — the endpoint pages, so the whole backlog is
|
|
255
|
+
reachable. It used to read one page of a hundred and offer no way past
|
|
256
|
+
it, while `has_more` came back on every read and was shown to nobody
|
|
257
|
+
(PO-369).
|
|
258
|
+
-->
|
|
259
|
+
<div v-if="missedLastPage > 1" class="flex items-center justify-between text-sm text-muted-foreground">
|
|
260
|
+
<span class="tabular-nums">
|
|
261
|
+
{{ t("integration.deadLetters.pagination.summary", { page: missedPage, lastPage: missedLastPage, total: missedSummary.total }) }}
|
|
262
|
+
</span>
|
|
263
|
+
<div class="flex gap-2">
|
|
264
|
+
<Button
|
|
265
|
+
variant="outline" size="sm" class="h-7 text-sm"
|
|
266
|
+
:disabled="missedPage <= 1 || missedLoading"
|
|
267
|
+
@click="goMissedPage(missedPage - 1)"
|
|
268
|
+
>
|
|
269
|
+
{{ t("integration.deadLetters.pagination.newer") }}
|
|
270
|
+
</Button>
|
|
271
|
+
<Button
|
|
272
|
+
variant="outline" size="sm" class="h-7 text-sm"
|
|
273
|
+
:disabled="missedPage >= missedLastPage || missedLoading"
|
|
274
|
+
@click="goMissedPage(missedPage + 1)"
|
|
275
|
+
>
|
|
276
|
+
{{ t("integration.deadLetters.pagination.older") }}
|
|
277
|
+
</Button>
|
|
278
|
+
</div>
|
|
279
|
+
</div>
|
|
171
280
|
</template>
|
|
172
281
|
</div>
|
|
173
282
|
</IntegrationsLayout>
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
Input,
|
|
25
25
|
Label
|
|
26
26
|
} from "@revenexx/studio";
|
|
27
|
+
import { errorReason } from "../../utils/apiError";
|
|
27
28
|
const t = useIntegrationsT();
|
|
28
29
|
useHead({ title: () => t("integration.secrets.head.title") });
|
|
29
30
|
const { secrets, loading, error, isValidKey, upsert, remove, load } = useIntegrationsSecrets();
|
|
@@ -58,7 +59,7 @@ async function submitCreate() {
|
|
|
58
59
|
await upsert(key, newValue.value);
|
|
59
60
|
createOpen.value = false;
|
|
60
61
|
} catch (e) {
|
|
61
|
-
formError.value = e.
|
|
62
|
+
formError.value = errorReason(e, t("integration.secrets.form.error.saveFailed"));
|
|
62
63
|
} finally {
|
|
63
64
|
saving.value = false;
|
|
64
65
|
}
|
|
@@ -24,6 +24,7 @@ import TemplateFlowPreview from "../../../components/Integrations/TemplateFlowPr
|
|
|
24
24
|
import NodeMark from "../../../components/Integrations/NodeMark.vue";
|
|
25
25
|
import { templateCategoryLabel, templatePackageLabel } from "../../../data/integrationTemplates";
|
|
26
26
|
import { describeCron } from "../../../utils/schedule";
|
|
27
|
+
import { errorReason } from "../../../utils/apiError";
|
|
27
28
|
const SELECT_LATER = "__later__";
|
|
28
29
|
const t = useIntegrationsT();
|
|
29
30
|
const route = useRoute();
|
|
@@ -165,7 +166,7 @@ async function confirmInstall() {
|
|
|
165
166
|
});
|
|
166
167
|
if (id) await router.push(`/integrations/workflows/${id}`);
|
|
167
168
|
} catch (e) {
|
|
168
|
-
installError.value = e
|
|
169
|
+
installError.value = errorReason(e, t("integration.templates.detail.install.error.fallback"));
|
|
169
170
|
} finally {
|
|
170
171
|
installing.value = false;
|
|
171
172
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { computed, ref, watch } from "vue";
|
|
3
3
|
import { Magnifier as Search, Layers, DangerTriangle as AlertTriangle } from "#solar-icons";
|
|
4
4
|
import { Input, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@revenexx/studio";
|
|
5
|
+
import LoadingAnnouncement from "../../../components/Integrations/LoadingAnnouncement.vue";
|
|
5
6
|
import TemplateCard from "../../../components/Integrations/TemplateCard.vue";
|
|
6
7
|
import { templateCategoryLabel, templatePackageLabel } from "../../../data/integrationTemplates";
|
|
7
8
|
const t = useIntegrationsT();
|
|
@@ -55,7 +56,6 @@ watch(() => templates.value.length, () => {
|
|
|
55
56
|
if (templates.value.length) void ensureDetails(templates.value.map((t2) => t2.slug));
|
|
56
57
|
});
|
|
57
58
|
const awaitingCatalogue = computed(() => loading.value && !templates.value.length);
|
|
58
|
-
const statusText = computed(() => awaitingCatalogue.value ? t("integration.templates.gallery.state.loading") : "");
|
|
59
59
|
</script>
|
|
60
60
|
|
|
61
61
|
<template>
|
|
@@ -115,22 +115,25 @@ const statusText = computed(() => awaitingCatalogue.value ? t("integration.templ
|
|
|
115
115
|
</PageHeader>
|
|
116
116
|
|
|
117
117
|
<div class="p-4">
|
|
118
|
-
<!-- What the page is doing, in words (specs/templates.md AC-9).
|
|
119
|
-
|
|
120
|
-
six blank tiles carry no text, no name and no state — so waiting and
|
|
121
|
-
"nothing is registered" were the same silence, which are the two states
|
|
122
|
-
that ask for opposite reactions.
|
|
118
|
+
<!-- What the page is doing, in words (specs/templates.md AC-9). The mechanics are
|
|
119
|
+
in LoadingAnnouncement; three things about this page in particular are not.
|
|
123
120
|
|
|
124
|
-
The region is
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
what is announced is a change inside an existing region.
|
|
121
|
+
The region is NAMED because every card draws a status region of its own while
|
|
122
|
+
its flow miniature loads, so an unnamed one here would be indistinguishable
|
|
123
|
+
from six of those.
|
|
128
124
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
125
|
+
Only the WAIT gets words. The empty registry and a filter without a match say
|
|
126
|
+
which they are in copy the reader arrives at or already has focus in, so
|
|
127
|
+
announcing them too would tell each of them twice.
|
|
128
|
+
|
|
129
|
+
A refused catalogue IS announced, but not from here: it is said by the block
|
|
130
|
+
that shows it, which carries `role="alert"`. Saying it here as well would put
|
|
131
|
+
the same sentence twice in the DOM, heard twice by anybody walking the page. -->
|
|
132
|
+
<LoadingAnnouncement
|
|
133
|
+
:label="t('integration.templates.gallery.state.regionLabel')"
|
|
134
|
+
:loading="awaitingCatalogue"
|
|
135
|
+
:text="t('integration.templates.gallery.state.loading')"
|
|
136
|
+
/>
|
|
134
137
|
|
|
135
138
|
<!-- The placeholders are taken out of the accessibility tree rather than
|
|
136
139
|
marked busy. `aria-busy` only asks assistive technology to defer
|