@revenexx/studio-integrations 0.5.0 → 0.6.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 +201 -0
- package/README.md +17 -4
- package/dist/module.json +1 -1
- package/dist/runtime/components/Integrations/CredentialCreateDialog.vue +15 -14
- package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +91 -19
- package/dist/runtime/components/Integrations/CredentialRevenexxSource.vue +58 -21
- package/dist/runtime/components/Integrations/CredentialTypePicker.vue +4 -8
- package/dist/runtime/components/Integrations/EventTypeCombobox.d.vue.ts +1 -0
- package/dist/runtime/components/Integrations/EventTypeCombobox.vue +4 -2
- 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 +54 -25
- package/dist/runtime/components/Integrations/ExpressionInput.vue.d.ts +4 -0
- package/dist/runtime/components/Integrations/GroupFrameView.vue +12 -12
- package/dist/runtime/components/Integrations/Layout.vue +21 -2
- package/dist/runtime/components/Integrations/NodeConfigDynamicField.vue +10 -11
- package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.vue +2 -5
- package/dist/runtime/components/Integrations/NodeConfigField.d.vue.ts +2 -0
- package/dist/runtime/components/Integrations/NodeConfigField.vue +160 -67
- 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 +1075 -303
- 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 +42 -0
- package/dist/runtime/components/Integrations/NodeOutputSchema.vue.d.ts +21 -0
- package/dist/runtime/components/Integrations/NodePalette.vue +100 -34
- package/dist/runtime/components/Integrations/QueryBuilder.vue +8 -1
- package/dist/runtime/components/Integrations/RunActionsMenu.vue +3 -1
- package/dist/runtime/components/Integrations/RunPayloadInput.vue +17 -4
- package/dist/runtime/components/Integrations/StickyNoteView.vue +2 -1
- package/dist/runtime/components/Integrations/TemplateCard.d.vue.ts +18 -0
- package/dist/runtime/components/Integrations/TemplateCard.vue +109 -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 +7 -10
- package/dist/runtime/components/Integrations/TemplateFlowPreview.vue.d.ts +2 -2
- package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +21 -37
- package/dist/runtime/components/Integrations/WorkflowEdgeView.d.vue.ts +18 -4
- package/dist/runtime/components/Integrations/WorkflowEdgeView.vue +23 -7
- package/dist/runtime/components/Integrations/WorkflowEdgeView.vue.d.ts +18 -4
- package/dist/runtime/components/Integrations/WorkflowEditor.d.vue.ts +22 -1
- package/dist/runtime/components/Integrations/WorkflowEditor.vue +170 -48
- package/dist/runtime/components/Integrations/WorkflowEditor.vue.d.ts +22 -1
- package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue +4 -20
- package/dist/runtime/components/Integrations/WorkflowIconPicker.d.vue.ts +19 -1
- package/dist/runtime/components/Integrations/WorkflowIconPicker.vue +11 -2
- 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 +189 -46
- package/dist/runtime/components/Integrations/WorkflowNodeView.vue.d.ts +4 -0
- package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +4 -20
- package/dist/runtime/components/Integrations/WorkflowSettingsDialog.vue +15 -9
- package/dist/runtime/components/Integrations/WorkflowTagsInput.d.vue.ts +1 -0
- package/dist/runtime/components/Integrations/WorkflowTagsInput.vue +7 -2
- package/dist/runtime/components/Integrations/WorkflowTagsInput.vue.d.ts +1 -0
- package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +4 -2
- package/dist/runtime/components/Integrations/WorkflowVariablesDialog.vue +30 -17
- package/dist/runtime/components/Integrations/WorkflowsTable.vue +47 -7
- package/dist/runtime/composables/useCredentialTypes.js +4 -4
- package/dist/runtime/composables/useIntegrationsAudit.js +4 -3
- package/dist/runtime/composables/useIntegrationsCredentials.js +4 -4
- package/dist/runtime/composables/useIntegrationsDeadLetters.js +7 -6
- package/dist/runtime/composables/useIntegrationsNodes.js +4 -4
- package/dist/runtime/composables/useIntegrationsRuns.js +11 -6
- package/dist/runtime/composables/useIntegrationsSchedules.d.ts +4 -0
- package/dist/runtime/composables/useIntegrationsSchedules.js +10 -3
- package/dist/runtime/composables/useIntegrationsSecrets.js +10 -7
- package/dist/runtime/composables/useIntegrationsTemplates.d.ts +1 -1
- package/dist/runtime/composables/useIntegrationsTemplates.js +12 -8
- package/dist/runtime/composables/useIntegrationsTriggers.js +6 -6
- package/dist/runtime/composables/useIntegrationsWorkflows.js +8 -4
- package/dist/runtime/composables/useLoadGate.d.ts +17 -0
- package/dist/runtime/composables/useLoadGate.js +8 -0
- 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/integrationTemplates.d.ts +39 -0
- package/dist/runtime/data/integrationTemplates.js +15 -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 +27 -3
- 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 +2 -5
- package/dist/runtime/pages/integrations/audit.vue +91 -6
- package/dist/runtime/pages/integrations/credentials/oauth-complete.vue +6 -4
- package/dist/runtime/pages/integrations/credentials.vue +119 -21
- package/dist/runtime/pages/integrations/index.vue +49 -44
- package/dist/runtime/pages/integrations/runs.vue +1 -1
- package/dist/runtime/pages/integrations/schedules.vue +51 -7
- package/dist/runtime/pages/integrations/secrets.vue +29 -7
- package/dist/runtime/pages/integrations/templates/[slug].vue +52 -30
- package/dist/runtime/pages/integrations/templates/index.vue +57 -86
- package/dist/runtime/pages/integrations/workflows/[id].vue +243 -25
- package/dist/runtime/pages/integrations/workflows/index.vue +15 -2
- 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/loadGate.d.ts +61 -0
- package/dist/runtime/utils/loadGate.js +22 -0
- package/dist/runtime/utils/localize.d.ts +29 -0
- package/dist/runtime/utils/localize.js +4 -0
- package/dist/runtime/utils/notify.d.ts +59 -0
- package/dist/runtime/utils/notify.js +21 -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 +141 -0
- package/dist/runtime/utils/schedule.js +207 -0
- package/dist/runtime/utils/validationFindings.d.ts +101 -0
- package/dist/runtime/utils/validationFindings.js +53 -0
- package/dist/runtime/utils/whenIdle.d.ts +42 -0
- package/dist/runtime/utils/whenIdle.js +12 -0
- package/package.json +8 -2
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { computed, ref, watch } from "vue";
|
|
3
|
-
import
|
|
3
|
+
import TemplateCard from "../../components/Integrations/TemplateCard.vue";
|
|
4
4
|
import WorkflowCreateDialog from "../../components/Integrations/WorkflowCreateDialog.vue";
|
|
5
5
|
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";
|
|
6
6
|
import { Badge, Button } from "@revenexx/studio";
|
|
7
7
|
useHead({ title: "Dashboard \xB7 Integration Studio" });
|
|
8
8
|
const { workflows, activeCount: activeWorkflows, loading: workflowsLoading, load: loadWorkflows } = useIntegrationsWorkflows();
|
|
9
9
|
const { runs, runningCount, failedCount, loading: runsLoading, load: loadRuns } = useIntegrationsRuns();
|
|
10
|
-
const {
|
|
11
|
-
watch(() =>
|
|
12
|
-
if (
|
|
10
|
+
const { startingPoints, loading: templatesLoading, load: loadTemplates, ensureDetails } = useIntegrationsTemplates();
|
|
11
|
+
watch(() => startingPoints.value.map((t) => t.slug).join(","), () => {
|
|
12
|
+
if (startingPoints.value.length) void ensureDetails(startingPoints.value.map((t) => t.slug));
|
|
13
13
|
}, { immediate: true });
|
|
14
14
|
const { activeCount: schedulesActive, load: loadSchedules } = useIntegrationsSchedules();
|
|
15
15
|
const { pendingCount: missedRuns, globalLoading: missedLoading, loadGlobal: loadMissedRuns } = useIntegrationsDeadLetters();
|
|
@@ -24,18 +24,18 @@ onMounted(() => {
|
|
|
24
24
|
void loadSchedules();
|
|
25
25
|
void loadMissedRuns();
|
|
26
26
|
});
|
|
27
|
-
const
|
|
27
|
+
const latestByWorkflow = computed(() => {
|
|
28
28
|
const seen = /* @__PURE__ */ new Set();
|
|
29
29
|
const out = [];
|
|
30
30
|
for (const r of runs.value) {
|
|
31
31
|
if (seen.has(r.workflowId)) continue;
|
|
32
32
|
seen.add(r.workflowId);
|
|
33
33
|
out.push(r);
|
|
34
|
-
if (out.length >= 10) break;
|
|
35
34
|
}
|
|
36
35
|
return out;
|
|
37
36
|
});
|
|
38
|
-
const
|
|
37
|
+
const latestPerWorkflow = computed(() => latestByWorkflow.value.slice(0, 10));
|
|
38
|
+
const failedRecent = computed(() => latestByWorkflow.value.filter((r) => r.status === "failed").slice(0, 3));
|
|
39
39
|
function formatRelative(iso) {
|
|
40
40
|
if (!iso) return "\u2014";
|
|
41
41
|
const ms = Date.now() - new Date(iso).getTime();
|
|
@@ -76,7 +76,7 @@ const kpis = computed(() => [
|
|
|
76
76
|
{
|
|
77
77
|
label: "Scheduled",
|
|
78
78
|
value: schedulesActive.value,
|
|
79
|
-
sub: "
|
|
79
|
+
sub: "scheduled triggers active",
|
|
80
80
|
icon: Clock,
|
|
81
81
|
tile: "bg-violet-500 text-white"
|
|
82
82
|
},
|
|
@@ -118,13 +118,18 @@ const surfaces = [
|
|
|
118
118
|
</PageHeader>
|
|
119
119
|
|
|
120
120
|
<div class="p-4 space-y-4">
|
|
121
|
-
<!-- KPI strip
|
|
122
|
-
|
|
121
|
+
<!-- KPI strip. Each tile carries its figure in its own name: the Missed-runs
|
|
122
|
+
tile is a link, and a link takes its name from its label instead of its
|
|
123
|
+
contents — named "Missed runs" alone it would announce less than the
|
|
124
|
+
three tiles beside it. -->
|
|
125
|
+
<div role="group" aria-label="Key figures" class="grid gap-2 sm:grid-cols-2 lg:grid-cols-4">
|
|
123
126
|
<component
|
|
124
127
|
:is="k.to ? NuxtLinkComp : 'div'"
|
|
125
128
|
v-for="k in kpis"
|
|
126
129
|
:key="k.label"
|
|
127
130
|
:to="k.to"
|
|
131
|
+
:role="k.to ? void 0 : 'group'"
|
|
132
|
+
:aria-label="`${k.label}: ${k.value}`"
|
|
128
133
|
class="block rounded-lg border border-black/[0.06] bg-white p-3"
|
|
129
134
|
:class="k.to ? 'transition hover:border-black/20' : ''"
|
|
130
135
|
>
|
|
@@ -148,10 +153,10 @@ const surfaces = [
|
|
|
148
153
|
</component>
|
|
149
154
|
</div>
|
|
150
155
|
|
|
151
|
-
<!-- Two-column: recent runs + needs attention +
|
|
156
|
+
<!-- Two-column: recent runs + needs attention + templates to start from -->
|
|
152
157
|
<div class="grid gap-4 lg:grid-cols-3">
|
|
153
158
|
<!-- Recent runs ─ takes 2 cols -->
|
|
154
|
-
<div class="lg:col-span-2 rounded-lg border border-black/[0.06] bg-white">
|
|
159
|
+
<div role="group" aria-label="Latest executions" class="lg:col-span-2 rounded-lg border border-black/[0.06] bg-white">
|
|
155
160
|
<div class="flex items-center justify-between border-b border-black/[0.06] px-3 py-2">
|
|
156
161
|
<div class="flex items-center gap-2">
|
|
157
162
|
<span class="flex size-6 items-center justify-center rounded-md bg-slate-100 text-slate-500">
|
|
@@ -164,11 +169,15 @@ const surfaces = [
|
|
|
164
169
|
</NuxtLink>
|
|
165
170
|
</div>
|
|
166
171
|
<div>
|
|
167
|
-
|
|
172
|
+
<!-- An entry is a link, not a div that answers a click: that is what
|
|
173
|
+
makes it reachable by keyboard, and it is what gives it a name —
|
|
174
|
+
the values it shows. A name written onto a plain row would be
|
|
175
|
+
heard by nobody, because ARIA does not name a generic element. -->
|
|
176
|
+
<NuxtLink
|
|
168
177
|
v-for="r in latestPerWorkflow"
|
|
169
178
|
:key="r.id"
|
|
170
|
-
|
|
171
|
-
|
|
179
|
+
:to="{ path: `/integrations/workflows/${r.workflowId}`, query: { run: r.id } }"
|
|
180
|
+
class="grid grid-cols-[90px_minmax(0,1fr)_100px_70px] items-center gap-2 border-b border-black/[0.04] px-3 py-2 text-sm last:border-b-0 hover:bg-muted/30"
|
|
172
181
|
>
|
|
173
182
|
<Badge variant="outline" :class="['border text-xs font-medium uppercase', statusClass(r.status)]">
|
|
174
183
|
{{ r.status }}
|
|
@@ -183,7 +192,7 @@ const surfaces = [
|
|
|
183
192
|
<div class="text-sm text-muted-foreground tabular-nums text-right">
|
|
184
193
|
{{ r.durationMs == null ? "\u2026" : `${(r.durationMs / 1e3).toFixed(2)}s` }}
|
|
185
194
|
</div>
|
|
186
|
-
</
|
|
195
|
+
</NuxtLink>
|
|
187
196
|
<template v-if="latestLoading && !latestPerWorkflow.length">
|
|
188
197
|
<div
|
|
189
198
|
v-for="n in 6"
|
|
@@ -206,7 +215,7 @@ const surfaces = [
|
|
|
206
215
|
</div>
|
|
207
216
|
|
|
208
217
|
<!-- Needs attention -->
|
|
209
|
-
<div class="rounded-lg border border-black/[0.06] bg-white">
|
|
218
|
+
<div role="group" aria-label="Needs attention" class="rounded-lg border border-black/[0.06] bg-white">
|
|
210
219
|
<div class="flex items-center justify-between border-b border-black/[0.06] px-3 py-2">
|
|
211
220
|
<div class="flex items-center gap-2">
|
|
212
221
|
<span class="flex size-6 items-center justify-center rounded-md bg-slate-100 text-slate-500">
|
|
@@ -230,20 +239,24 @@ const surfaces = [
|
|
|
230
239
|
Triggers fired while their workflow couldn't run — review & replay.
|
|
231
240
|
</div>
|
|
232
241
|
</NuxtLink>
|
|
233
|
-
|
|
242
|
+
<!-- A link, for the same reason as the entries next door — and like
|
|
243
|
+
the waiting-firings entry above it, which was one all along. -->
|
|
244
|
+
<NuxtLink
|
|
234
245
|
v-for="r in failedRecent"
|
|
235
246
|
:key="r.id"
|
|
236
|
-
|
|
237
|
-
|
|
247
|
+
:to="`/integrations/workflows/${r.workflowId}`"
|
|
248
|
+
class="block space-y-1 border-b border-black/[0.04] px-3 py-2 text-sm last:border-b-0 hover:bg-muted/30"
|
|
238
249
|
>
|
|
239
250
|
<div class="font-medium truncate">{{ r.workflowName }}</div>
|
|
240
251
|
<div class="text-sm text-rose-700 truncate" :title="r.errorMessage">
|
|
241
252
|
{{ r.errorMessage ?? "Failed." }}
|
|
242
253
|
</div>
|
|
254
|
+
<!-- When it failed is when it ended, not when it set off: a run
|
|
255
|
+
that broke after an hour would otherwise be dated an hour early. -->
|
|
243
256
|
<div class="text-sm text-muted-foreground tabular-nums">
|
|
244
|
-
{{ formatRelative(r.startedAt) }}
|
|
257
|
+
{{ formatRelative(r.finishedAt ?? r.startedAt) }}
|
|
245
258
|
</div>
|
|
246
|
-
</
|
|
259
|
+
</NuxtLink>
|
|
247
260
|
</div>
|
|
248
261
|
<div v-else class="px-3 py-6 text-center text-sm text-muted-foreground">
|
|
249
262
|
All workflows healthy.
|
|
@@ -272,49 +285,41 @@ const surfaces = [
|
|
|
272
285
|
</NuxtLink>
|
|
273
286
|
</div>
|
|
274
287
|
|
|
275
|
-
<!--
|
|
276
|
-
|
|
288
|
+
<!-- Named for the offer it makes, not for a ranking nothing measures
|
|
289
|
+
(PO-238): every template's install count was nought, so the sort that
|
|
290
|
+
produced this list compared nothing at all. The wording deliberately
|
|
291
|
+
echoes the create-workflow dialog's "Start from template" tab — the same
|
|
292
|
+
offer, so the same words, rather than a second name for it. -->
|
|
293
|
+
<div role="group" aria-label="Start from a template" class="rounded-lg border border-black/[0.06] bg-white">
|
|
277
294
|
<div class="flex items-center justify-between border-b border-black/[0.06] px-3 py-2">
|
|
278
295
|
<div class="flex items-center gap-2">
|
|
279
296
|
<span class="flex size-6 items-center justify-center rounded-md bg-slate-100 text-slate-500">
|
|
280
297
|
<Sparkles :size="12" />
|
|
281
298
|
</span>
|
|
282
|
-
<h3 class="text-sm font-semibold">
|
|
299
|
+
<h3 class="text-sm font-semibold">Start from a template</h3>
|
|
283
300
|
</div>
|
|
284
301
|
<NuxtLink to="/integrations/templates" class="text-sm text-muted-foreground hover:text-foreground inline-flex items-center gap-1">
|
|
285
302
|
See all <ArrowRight :size="11" />
|
|
286
303
|
</NuxtLink>
|
|
287
304
|
</div>
|
|
288
|
-
<div v-if="templatesLoading && !
|
|
305
|
+
<div v-if="templatesLoading && !startingPoints.length" class="grid gap-2 p-3 sm:grid-cols-2 lg:grid-cols-4">
|
|
289
306
|
<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">
|
|
290
307
|
<div class="h-5 w-20 rounded bg-black/[0.07] animate-pulse" />
|
|
291
308
|
<div class="h-3 w-3/4 rounded bg-black/[0.07] animate-pulse" />
|
|
292
309
|
<div class="h-3 w-1/2 rounded bg-black/[0.05] animate-pulse" />
|
|
293
310
|
</div>
|
|
294
311
|
</div>
|
|
295
|
-
<div v-else-if="!
|
|
312
|
+
<div v-else-if="!startingPoints.length" class="px-3 py-6 text-center text-sm text-muted-foreground">
|
|
296
313
|
No templates registered yet.
|
|
297
314
|
</div>
|
|
298
315
|
<div v-else class="grid gap-2 p-3 sm:grid-cols-2 lg:grid-cols-4">
|
|
299
|
-
<
|
|
300
|
-
v-for="tpl in
|
|
316
|
+
<TemplateCard
|
|
317
|
+
v-for="tpl in startingPoints"
|
|
301
318
|
:key="tpl.slug"
|
|
319
|
+
:template="tpl"
|
|
320
|
+
density="compact"
|
|
302
321
|
:to="`/integrations/templates/${tpl.slug}`"
|
|
303
|
-
|
|
304
|
-
>
|
|
305
|
-
<div class="relative h-20 shrink-0 border-b border-black/[0.06] bg-slate-50/70 [background-image:radial-gradient(circle,rgba(15,23,42,0.07)_1px,transparent_1px)] [background-size:12px_12px]">
|
|
306
|
-
<TemplateFlowPreview v-if="tpl.detailLoaded" :blob="tpl.blob" :height="80" class="px-1" />
|
|
307
|
-
<span class="absolute left-1.5 top-1.5 inline-flex items-center whitespace-nowrap rounded-full bg-slate-100 px-1.5 py-px text-[11px] font-medium uppercase text-slate-700">
|
|
308
|
-
{{ tpl.category }}
|
|
309
|
-
</span>
|
|
310
|
-
</div>
|
|
311
|
-
<div class="flex min-h-0 flex-1 flex-col p-3">
|
|
312
|
-
<div class="text-sm font-medium truncate">{{ tpl.name }}</div>
|
|
313
|
-
<p class="mt-1 text-sm text-muted-foreground line-clamp-2">
|
|
314
|
-
{{ tpl.description }}
|
|
315
|
-
</p>
|
|
316
|
-
</div>
|
|
317
|
-
</NuxtLink>
|
|
322
|
+
/>
|
|
318
323
|
</div>
|
|
319
324
|
</div>
|
|
320
325
|
</div>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { ClockCircle as Clock, Globus as Globe } from "#solar-icons";
|
|
3
3
|
import { Badge } from "@revenexx/studio";
|
|
4
|
+
import { cadenceRank, describeCron } from "../../utils/schedule";
|
|
4
5
|
useHead({ title: "Schedules \xB7 Integration Studio" });
|
|
5
6
|
const router = useRouter();
|
|
6
7
|
const { entries, activeCount, loading, load } = useIntegrationsSchedules();
|
|
@@ -22,8 +23,18 @@ function formatRelative(iso) {
|
|
|
22
23
|
return new Date(iso).toLocaleString(void 0, { month: "short", day: "2-digit", hour: "2-digit", minute: "2-digit" });
|
|
23
24
|
}
|
|
24
25
|
const columns = [
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
// Two columns share the slack, and the sentence gets the larger share of it:
|
|
27
|
+
// "Weekdays, hourly from 09:00 to 17:00" is what this page is for, while a
|
|
28
|
+
// workflow name is a label the reader recognises from its first few words.
|
|
29
|
+
// Both flex, so no width is left unused at the end of the row.
|
|
30
|
+
//
|
|
31
|
+
// The expression stays in the search text although the column no longer shows
|
|
32
|
+
// it: support quotes expressions, and a row has to be findable by the string
|
|
33
|
+
// someone was given.
|
|
34
|
+
{ key: "workflow", label: "Workflow", width: "minmax(180px, 1fr)", sortable: true, sortGetter: (e) => e.workflowName.toLowerCase(), searchGetter: (e) => `${e.workflowName} ${e.cron ?? ""} ${e.timezone} ${describeCron(e.cron) ?? ""}` },
|
|
35
|
+
// Sorted by how often a schedule fires. The expression's characters would be
|
|
36
|
+
// meaningless here — the column shows a sentence.
|
|
37
|
+
{ key: "cron", label: "When", width: "minmax(260px, 1.4fr)", sortable: true, sortGetter: (e) => cadenceRank(e.cron) },
|
|
27
38
|
{ key: "timezone", label: "Timezone", width: "140px", sortable: true, sortGetter: (e) => e.timezone },
|
|
28
39
|
{ key: "nextRun", label: "Next run", width: "140px", sortable: true, sortGetter: (e) => e.nextRunAt ?? "" },
|
|
29
40
|
{ key: "lastRun", label: "Last run", width: "140px", sortable: true, sortGetter: (e) => e.lastRunAt ?? "" },
|
|
@@ -35,7 +46,7 @@ const columns = [
|
|
|
35
46
|
<IntegrationsLayout>
|
|
36
47
|
<PageHeader
|
|
37
48
|
title="Schedules"
|
|
38
|
-
description="Every
|
|
49
|
+
description="Every scheduled trigger across your workflows. Click a row to open the workflow."
|
|
39
50
|
>
|
|
40
51
|
<template #actions>
|
|
41
52
|
<span class="text-sm text-muted-foreground">
|
|
@@ -51,7 +62,7 @@ const columns = [
|
|
|
51
62
|
:columns="columns"
|
|
52
63
|
:row-key="(e2) => e2.id"
|
|
53
64
|
:default-sort="{ column: 'nextRun', direction: 'asc' }"
|
|
54
|
-
:search="{ placeholder: 'Search by workflow,
|
|
65
|
+
:search="{ placeholder: 'Search by workflow, schedule, timezone\u2026' }"
|
|
55
66
|
:filter="true"
|
|
56
67
|
empty-text="No schedules yet. Add a schedule trigger to any workflow."
|
|
57
68
|
@row-click="(e2) => router.push(`/integrations/workflows/${e2.workflowId}`)"
|
|
@@ -67,8 +78,33 @@ const columns = [
|
|
|
67
78
|
</div>
|
|
68
79
|
</template>
|
|
69
80
|
|
|
81
|
+
<!--
|
|
82
|
+
The sentence is the answer, and the expression is not on the page: the
|
|
83
|
+
question this column asks is "when does this run", which an expression
|
|
84
|
+
answers in a language the reader may not speak. It is still one hover
|
|
85
|
+
away — support quotes expressions — and it still stands alone in the one
|
|
86
|
+
case where nothing else would be true: a schedule the studio cannot say
|
|
87
|
+
in words, which is one no picker built either.
|
|
88
|
+
-->
|
|
70
89
|
<template #cell-cron="{ row: e }">
|
|
71
|
-
<
|
|
90
|
+
<div class="min-w-0">
|
|
91
|
+
<!-- The sentence wraps rather than truncating: a schedule that fires
|
|
92
|
+
at several named times is a long sentence, and half of one is
|
|
93
|
+
the wrong half. Its title is the expression, so there is no
|
|
94
|
+
tooltip left to carry the rest of the words. -->
|
|
95
|
+
<div v-if="describeCron(e.cron)" aria-label="When it fires" :title="e.cron ?? void 0" class="text-sm leading-snug">
|
|
96
|
+
{{ describeCron(e.cron) }}
|
|
97
|
+
</div>
|
|
98
|
+
<template v-else-if="e.cron">
|
|
99
|
+
<div aria-label="When it fires" class="text-sm truncate">
|
|
100
|
+
Custom schedule
|
|
101
|
+
</div>
|
|
102
|
+
<code aria-label="Cron expression" class="block font-mono text-xs text-muted-foreground truncate">{{ e.cron }}</code>
|
|
103
|
+
</template>
|
|
104
|
+
<div v-else aria-label="When it fires" class="text-sm text-muted-foreground truncate">
|
|
105
|
+
No schedule set
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
72
108
|
</template>
|
|
73
109
|
|
|
74
110
|
<template #cell-timezone="{ row: e }">
|
|
@@ -78,12 +114,20 @@ const columns = [
|
|
|
78
114
|
</div>
|
|
79
115
|
</template>
|
|
80
116
|
|
|
117
|
+
<!--
|
|
118
|
+
The two time cells carry the name their column header states, so each is
|
|
119
|
+
addressable by what it is rather than by how it is typeset. It is not the
|
|
120
|
+
accessible name a screen reader hears — the cell is a plain span, and
|
|
121
|
+
ARIA prohibits naming a generic element — that needs row and cell
|
|
122
|
+
semantics in the shared table (PO-233, and a gap in
|
|
123
|
+
`specs/accessibility.md`).
|
|
124
|
+
-->
|
|
81
125
|
<template #cell-nextRun="{ row: e }">
|
|
82
|
-
<span class="text-sm tabular-nums">{{ formatRelative(e.nextRunAt) }}</span>
|
|
126
|
+
<span aria-label="Next run" class="text-sm tabular-nums">{{ formatRelative(e.nextRunAt) }}</span>
|
|
83
127
|
</template>
|
|
84
128
|
|
|
85
129
|
<template #cell-lastRun="{ row: e }">
|
|
86
|
-
<span class="text-sm tabular-nums text-muted-foreground">{{ formatRelative(e.lastRunAt) }}</span>
|
|
130
|
+
<span aria-label="Last run" class="text-sm tabular-nums text-muted-foreground">{{ formatRelative(e.lastRunAt) }}</span>
|
|
87
131
|
</template>
|
|
88
132
|
|
|
89
133
|
<template #cell-status="{ row: e }">
|
|
@@ -71,18 +71,18 @@ async function confirmDelete() {
|
|
|
71
71
|
deleting.value = false;
|
|
72
72
|
pendingDelete.value = null;
|
|
73
73
|
}
|
|
74
|
-
function
|
|
74
|
+
function formatDate(iso) {
|
|
75
75
|
if (!iso) return "\u2014";
|
|
76
76
|
try {
|
|
77
|
-
return new Date(iso).
|
|
77
|
+
return new Date(iso).toLocaleDateString(void 0, { month: "short", day: "2-digit", year: "numeric" });
|
|
78
78
|
} catch {
|
|
79
79
|
return iso;
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
82
|
const columns = [
|
|
83
83
|
{ key: "key", label: "Key", sortable: true, sortGetter: (s) => s.key.toLowerCase() },
|
|
84
|
-
{ key: "created", label: "Created", width: "160px", sortable: true, sortGetter: (s) => s.created_at },
|
|
85
|
-
{ key: "updated", label: "Updated", width: "160px", sortable: true, sortGetter: (s) => s.updated_at },
|
|
84
|
+
{ key: "created", label: "Created", width: "160px", sortable: true, sortGetter: (s) => s.created_at, searchGetter: () => "" },
|
|
85
|
+
{ key: "updated", label: "Updated", width: "160px", sortable: true, sortGetter: (s) => s.updated_at, searchGetter: () => "" },
|
|
86
86
|
{ key: "actions", label: "", width: "40px", toggleable: false }
|
|
87
87
|
];
|
|
88
88
|
</script>
|
|
@@ -136,6 +136,7 @@ const columns = [
|
|
|
136
136
|
<button
|
|
137
137
|
type="button"
|
|
138
138
|
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 ? 'Hide value' : 'Show value'"
|
|
139
140
|
@click="showValue = !showValue"
|
|
140
141
|
>
|
|
141
142
|
<component :is="showValue ? EyeOff : Eye" :size="14" />
|
|
@@ -188,19 +189,40 @@ const columns = [
|
|
|
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="`Created for ${s.key}: ${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="`Updated for ${s.key}: ${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="`Actions for ${s.key}`">
|
|
204
226
|
<MoreHorizontal :size="14" />
|
|
205
227
|
</Button>
|
|
206
228
|
</DropdownMenuTrigger>
|
|
@@ -20,8 +20,9 @@ import {
|
|
|
20
20
|
SelectValue
|
|
21
21
|
} from "@revenexx/studio";
|
|
22
22
|
import TemplateFlowPreview from "../../../components/Integrations/TemplateFlowPreview.vue";
|
|
23
|
-
import
|
|
24
|
-
import {
|
|
23
|
+
import NodeMark from "../../../components/Integrations/NodeMark.vue";
|
|
24
|
+
import { templateCategoryLabel, templatePackageLabel } from "../../../data/integrationTemplates";
|
|
25
|
+
import { describeCron } from "../../../utils/schedule";
|
|
25
26
|
const SELECT_LATER = "__later__";
|
|
26
27
|
const route = useRoute();
|
|
27
28
|
const router = useRouter();
|
|
@@ -91,7 +92,10 @@ function firstSentence(text) {
|
|
|
91
92
|
const TRIGGER_ICON = { schedule: Clock, webhook: Webhook, manual: Play, event: Zap };
|
|
92
93
|
const triggerChips = computed(() => (tpl.value?.blob.triggers ?? []).map((t) => {
|
|
93
94
|
let label = t.type.charAt(0).toUpperCase() + t.type.slice(1);
|
|
94
|
-
if (t.type === "schedule" && t.config?.cron)
|
|
95
|
+
if (t.type === "schedule" && t.config?.cron) {
|
|
96
|
+
const when = describeCron(String(t.config.cron)) ?? String(t.config.cron);
|
|
97
|
+
label = `Schedule \xB7 ${when}${t.config.timezone ? ` (${t.config.timezone})` : ""}`;
|
|
98
|
+
}
|
|
95
99
|
if (t.type === "webhook" && t.config?.path) label = `Webhook \xB7 ${t.config.path}`;
|
|
96
100
|
if (t.type === "manual") label = "Run on demand";
|
|
97
101
|
return { key: t.id, icon: TRIGGER_ICON[t.type] ?? Zap, label };
|
|
@@ -104,7 +108,9 @@ const variables = computed(() => Object.entries(tpl.value?.blob.variables ?? {})
|
|
|
104
108
|
const variableColumns = [
|
|
105
109
|
{ key: "key", label: "Variable", width: "minmax(0,180px)", sortable: true },
|
|
106
110
|
{ key: "description", label: "Description" },
|
|
107
|
-
|
|
111
|
+
// Stored as `default` in the blob, but it *is* the value the workflow runs
|
|
112
|
+
// with — see `specs/workflow-variables.md`.
|
|
113
|
+
{ key: "default", label: "Value", width: "minmax(0,200px)" }
|
|
108
114
|
];
|
|
109
115
|
const credentialReqs = computed(() => reqs.value?.credentialTypes ?? []);
|
|
110
116
|
const secretReqs = computed(() => reqs.value?.secrets ?? []);
|
|
@@ -126,17 +132,8 @@ const secretChecks = computed(() => {
|
|
|
126
132
|
const missingSecrets = computed(() => secretChecks.value.filter((s) => !s.present).length);
|
|
127
133
|
const missingCredentials = computed(() => credentialReqs.value.filter((c) => !ofType(c.slug).length).length);
|
|
128
134
|
const setupReady = computed(() => missingSecrets.value === 0 && missingCredentials.value === 0);
|
|
129
|
-
const packageLabel = computed(() =>
|
|
130
|
-
|
|
131
|
-
if (!pkg) return null;
|
|
132
|
-
const tail = (pkg.name.split("/").pop() ?? pkg.name).replace(/^integrations-nodes-/, "");
|
|
133
|
-
if (tail === "core") return null;
|
|
134
|
-
return pkg.label ?? tail.split("-").map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join(" ");
|
|
135
|
-
});
|
|
136
|
-
const categoryLabel = computed(() => {
|
|
137
|
-
const cat = tpl.value?.category;
|
|
138
|
-
return TEMPLATE_CATEGORIES.find((c) => c.key === cat)?.label ?? (cat ? cat.charAt(0).toUpperCase() + cat.slice(1) : "");
|
|
139
|
-
});
|
|
135
|
+
const packageLabel = computed(() => tpl.value ? templatePackageLabel(tpl.value) : null);
|
|
136
|
+
const categoryLabel = computed(() => templateCategoryLabel(tpl.value?.category ?? ""));
|
|
140
137
|
const installOpen = ref(false);
|
|
141
138
|
const installing = ref(false);
|
|
142
139
|
const installError = ref(null);
|
|
@@ -207,7 +204,7 @@ function levelDot(level) {
|
|
|
207
204
|
<Loader2 :size="18" class="animate-spin text-muted-foreground/50" />
|
|
208
205
|
</div>
|
|
209
206
|
<div class="absolute left-3 top-3 flex items-center gap-1.5">
|
|
210
|
-
<span class="inline-flex items-center rounded-full bg-slate-100 px-2 py-0.5 uppercase text-xs font-medium text-slate-700">
|
|
207
|
+
<span v-if="categoryLabel" class="inline-flex items-center rounded-full bg-slate-100 px-2 py-0.5 uppercase text-xs font-medium text-slate-700">
|
|
211
208
|
{{ categoryLabel }}
|
|
212
209
|
</span>
|
|
213
210
|
<span class="inline-flex items-center gap-1 rounded-full bg-slate-100 px-2 py-0.5 uppercase text-xs font-medium text-slate-700">
|
|
@@ -233,8 +230,13 @@ function levelDot(level) {
|
|
|
233
230
|
<div class="grid gap-4 lg:grid-cols-3">
|
|
234
231
|
<div class="lg:col-span-2 space-y-4">
|
|
235
232
|
<!-- About: the registry description, rendered -->
|
|
236
|
-
<div
|
|
237
|
-
|
|
233
|
+
<div
|
|
234
|
+
v-if="descriptionHtml"
|
|
235
|
+
role="region"
|
|
236
|
+
aria-labelledby="tpl-about-heading"
|
|
237
|
+
class="rounded-lg border border-black/[0.06] bg-white p-4"
|
|
238
|
+
>
|
|
239
|
+
<h3 id="tpl-about-heading" class="text-sm font-semibold mb-2">About this template</h3>
|
|
238
240
|
<div
|
|
239
241
|
class="text-sm leading-relaxed text-foreground/90 space-y-2 [&_h1]:text-sm [&_h1]:font-semibold [&_h2]:text-sm [&_h2]:font-semibold [&_h3]:text-sm [&_h3]:font-semibold [&_ol]:list-decimal [&_ol]:pl-5 [&_ul]:list-disc [&_ul]:pl-5 [&_li]:mt-1 [&_code]:rounded [&_code]:bg-slate-100 [&_code]:px-1 [&_code]:py-px [&_code]:font-mono [&_code]:text-[13px] [&_a]:text-primary [&_a]:underline [&_strong]:font-semibold"
|
|
240
242
|
v-html="descriptionHtml"
|
|
@@ -242,8 +244,12 @@ function levelDot(level) {
|
|
|
242
244
|
</div>
|
|
243
245
|
|
|
244
246
|
<!-- Step by step, humanized from the actual nodes -->
|
|
245
|
-
<div
|
|
246
|
-
|
|
247
|
+
<div
|
|
248
|
+
role="region"
|
|
249
|
+
aria-labelledby="tpl-steps-heading"
|
|
250
|
+
class="rounded-lg border border-black/[0.06] bg-white p-4"
|
|
251
|
+
>
|
|
252
|
+
<h3 id="tpl-steps-heading" class="text-sm font-semibold mb-3">Step by step</h3>
|
|
247
253
|
<ol v-if="steps.length" class="space-y-2.5">
|
|
248
254
|
<li
|
|
249
255
|
v-for="(step, i) in steps"
|
|
@@ -254,7 +260,7 @@ function levelDot(level) {
|
|
|
254
260
|
{{ i + 1 }}
|
|
255
261
|
</span>
|
|
256
262
|
<span class="flex size-7 shrink-0 items-center justify-center rounded-md bg-white ring-1 ring-black/[0.08]">
|
|
257
|
-
<
|
|
263
|
+
<NodeMark :manifest="step.manifest" :size="15" :badge="false" />
|
|
258
264
|
</span>
|
|
259
265
|
<span class="min-w-0 pt-0.5">
|
|
260
266
|
<span class="block text-sm font-medium text-foreground">{{ step.label }}</span>
|
|
@@ -268,8 +274,17 @@ function levelDot(level) {
|
|
|
268
274
|
</div>
|
|
269
275
|
|
|
270
276
|
<!-- Variables the operator can tune after install -->
|
|
271
|
-
|
|
272
|
-
|
|
277
|
+
<!-- Named by its own heading rather than by a copy of it: two strings
|
|
278
|
+
saying the same thing drift, and the test only holds one of them.
|
|
279
|
+
`role` stays because this is a div — a bare <section> would be a
|
|
280
|
+
region on its own, this one has to say so. -->
|
|
281
|
+
<div
|
|
282
|
+
v-if="variables.length"
|
|
283
|
+
role="region"
|
|
284
|
+
aria-labelledby="tpl-variables-heading"
|
|
285
|
+
class="rounded-lg border border-black/[0.06] bg-white p-4"
|
|
286
|
+
>
|
|
287
|
+
<h3 id="tpl-variables-heading" class="text-sm font-semibold mb-1">Variables you can tune</h3>
|
|
273
288
|
<p class="text-sm text-muted-foreground mb-3">Set once after install, used across the workflow.</p>
|
|
274
289
|
<DataTable
|
|
275
290
|
:data="variables"
|
|
@@ -296,7 +311,11 @@ function levelDot(level) {
|
|
|
296
311
|
<!-- Setup rail -->
|
|
297
312
|
<div class="space-y-3">
|
|
298
313
|
<!-- Readiness + CTA -->
|
|
299
|
-
|
|
314
|
+
<!-- `aria-label` deliberately, unlike the two panels below: this one
|
|
315
|
+
shows no heading of its own, so there is no visible string to
|
|
316
|
+
point `aria-labelledby` at. Copy this pattern only for a panel
|
|
317
|
+
that genuinely has none. -->
|
|
318
|
+
<div role="group" aria-label="Setup readiness" class="rounded-lg border border-black/[0.06] bg-white p-3 space-y-2.5">
|
|
300
319
|
<div v-if="setupReady" class="flex items-center gap-1.5 text-sm text-emerald-700">
|
|
301
320
|
<CheckCircle2 :size="14" />
|
|
302
321
|
Everything is ready to install.
|
|
@@ -349,8 +368,11 @@ function levelDot(level) {
|
|
|
349
368
|
</div>
|
|
350
369
|
</div>
|
|
351
370
|
|
|
352
|
-
|
|
353
|
-
|
|
371
|
+
<!-- Named by the label it shows, like the variables panel above: an
|
|
372
|
+
invented name is a second string saying the same thing, and it is
|
|
373
|
+
what a screen reader hears instead of what is written. -->
|
|
374
|
+
<div v-if="credentialReqs.length" role="group" aria-labelledby="tpl-credentials-label" class="rounded-lg border border-black/[0.06] bg-white p-3 space-y-2">
|
|
375
|
+
<div id="tpl-credentials-label" class="flex items-center gap-1.5 text-mono text-muted-foreground">
|
|
354
376
|
<Key :size="11" /> Credentials
|
|
355
377
|
</div>
|
|
356
378
|
<ul class="space-y-1">
|
|
@@ -378,9 +400,9 @@ function levelDot(level) {
|
|
|
378
400
|
<NuxtLink to="/integrations/credentials" class="block text-sm text-primary hover:underline">Manage credentials →</NuxtLink>
|
|
379
401
|
</div>
|
|
380
402
|
|
|
381
|
-
<div v-if="secretChecks.length" class="rounded-lg border border-black/[0.06] bg-white p-3 space-y-2">
|
|
403
|
+
<div v-if="secretChecks.length" role="group" aria-labelledby="tpl-secrets-label" class="rounded-lg border border-black/[0.06] bg-white p-3 space-y-2">
|
|
382
404
|
<div class="flex items-center gap-2">
|
|
383
|
-
<span class="text-mono text-muted-foreground">Required secrets</span>
|
|
405
|
+
<span id="tpl-secrets-label" class="text-mono text-muted-foreground">Required secrets</span>
|
|
384
406
|
<span v-if="missingSecrets > 0" class="flex items-center gap-1 text-sm text-amber-700">
|
|
385
407
|
<ShieldAlert :size="11" />
|
|
386
408
|
{{ missingSecrets }} missing
|
|
@@ -472,14 +494,14 @@ function levelDot(level) {
|
|
|
472
494
|
first, or install now and wire it up in the editor.
|
|
473
495
|
</p>
|
|
474
496
|
<div v-for="nodeId in nodesFor(req.usedBy)" v-else :key="nodeId" class="space-y-1">
|
|
475
|
-
<Label class="text-xs text-muted-foreground font-mono">node {{ nodeId }}</Label>
|
|
497
|
+
<Label :for="`tpl-cred-${nodeId}`" class="text-xs text-muted-foreground font-mono">node {{ nodeId }}</Label>
|
|
476
498
|
<Select
|
|
477
499
|
:model-value="bindings[nodeId] ?? SELECT_LATER"
|
|
478
500
|
@update:model-value="(v2) => {
|
|
479
501
|
bindings[nodeId] = v2 ?? SELECT_LATER;
|
|
480
502
|
}"
|
|
481
503
|
>
|
|
482
|
-
<SelectTrigger class="bg-white">
|
|
504
|
+
<SelectTrigger :id="`tpl-cred-${nodeId}`" class="bg-white">
|
|
483
505
|
<SelectValue placeholder="Select later" />
|
|
484
506
|
</SelectTrigger>
|
|
485
507
|
<SelectContent>
|