@revenexx/studio-integrations 0.7.0 → 1.1.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 +633 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +2 -1
- package/dist/runtime/components/Integrations/CredentialCreateDialog.vue +4 -1
- package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +8 -2
- package/dist/runtime/components/Integrations/DeadLettersPanel.vue +2 -6
- package/dist/runtime/components/Integrations/ExpressionInput.vue +5 -0
- package/dist/runtime/components/Integrations/NodeConfigDynamicField.d.vue.ts +3 -0
- package/dist/runtime/components/Integrations/NodeConfigDynamicField.vue +16 -3
- package/dist/runtime/components/Integrations/NodeConfigDynamicField.vue.d.ts +3 -0
- package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.vue +1 -0
- package/dist/runtime/components/Integrations/NodeConfigField.d.vue.ts +3 -0
- package/dist/runtime/components/Integrations/NodeConfigField.vue +2 -0
- package/dist/runtime/components/Integrations/NodeConfigField.vue.d.ts +3 -0
- package/dist/runtime/components/Integrations/NodeInspector.d.vue.ts +2 -2
- package/dist/runtime/components/Integrations/NodeInspector.vue +135 -10
- package/dist/runtime/components/Integrations/NodeInspector.vue.d.ts +2 -2
- package/dist/runtime/components/Integrations/NodeOutputSchema.d.vue.ts +2 -1
- package/dist/runtime/components/Integrations/NodeOutputSchema.vue +34 -8
- package/dist/runtime/components/Integrations/NodeOutputSchema.vue.d.ts +2 -1
- package/dist/runtime/components/Integrations/NodePalette.vue +12 -8
- package/dist/runtime/components/Integrations/RunDetailPanel.vue +9 -6
- package/dist/runtime/components/Integrations/RunsTable.vue +2 -2
- package/dist/runtime/components/Integrations/StickyNoteView.vue +45 -11
- package/dist/runtime/components/Integrations/SurfaceCard.d.vue.ts +50 -0
- package/dist/runtime/components/Integrations/SurfaceCard.vue +55 -0
- package/dist/runtime/components/Integrations/SurfaceCard.vue.d.ts +50 -0
- package/dist/runtime/components/Integrations/TemplateCard.vue +3 -3
- package/dist/runtime/components/Integrations/TemplateFlowPreview.vue +12 -3
- package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +5 -4
- package/dist/runtime/components/Integrations/WorkflowEditor.d.vue.ts +16 -0
- package/dist/runtime/components/Integrations/WorkflowEditor.vue +79 -12
- package/dist/runtime/components/Integrations/WorkflowEditor.vue.d.ts +16 -0
- package/dist/runtime/components/Integrations/WorkflowHistoryPanel.d.vue.ts +6 -0
- package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue +13 -3
- package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue.d.ts +6 -0
- package/dist/runtime/components/Integrations/WorkflowNodeView.d.vue.ts +13 -0
- package/dist/runtime/components/Integrations/WorkflowNodeView.vue +95 -29
- package/dist/runtime/components/Integrations/WorkflowNodeView.vue.d.ts +13 -0
- package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +11 -10
- package/dist/runtime/components/Integrations/WorkflowSettingsDialog.d.vue.ts +16 -17
- package/dist/runtime/components/Integrations/WorkflowSettingsDialog.vue +37 -26
- package/dist/runtime/components/Integrations/WorkflowSettingsDialog.vue.d.ts +16 -17
- package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +4 -5
- package/dist/runtime/components/Integrations/WorkflowVariablesDialog.vue +53 -14
- package/dist/runtime/components/Integrations/WorkflowsTable.vue +9 -4
- package/dist/runtime/composables/useIntegrationsCredentials.d.ts +11 -0
- package/dist/runtime/composables/useIntegrationsCredentials.js +2 -2
- package/dist/runtime/composables/useIntegrationsDeadLetters.d.ts +9 -2
- package/dist/runtime/composables/useIntegrationsDeadLetters.js +27 -18
- package/dist/runtime/composables/useIntegrationsNodes.js +11 -1
- package/dist/runtime/composables/useIntegrationsRuns.d.ts +30 -1
- package/dist/runtime/composables/useIntegrationsRuns.js +70 -29
- package/dist/runtime/composables/useIntegrationsSchedules.d.ts +7 -14
- package/dist/runtime/composables/useIntegrationsSchedules.js +80 -35
- package/dist/runtime/composables/useIntegrationsTriggers.d.ts +30 -3
- package/dist/runtime/composables/useIntegrationsTriggers.js +18 -7
- package/dist/runtime/composables/useIntegrationsWorkflows.d.ts +10 -1
- package/dist/runtime/composables/useIntegrationsWorkflows.js +25 -4
- package/dist/runtime/composables/useNodeConfigResolve.js +2 -1
- package/dist/runtime/data/integrationNodeCatalog.d.ts +22 -1
- package/dist/runtime/data/integrationNodeCatalog.js +8 -4
- package/dist/runtime/data/loopBody.d.ts +129 -0
- package/dist/runtime/data/loopBody.js +76 -0
- package/dist/runtime/data/nodeInputData.d.ts +58 -1
- package/dist/runtime/data/nodeInputData.js +30 -5
- package/dist/runtime/data/triggerGlyphs.d.ts +47 -0
- package/dist/runtime/data/triggerGlyphs.js +11 -0
- package/dist/runtime/i18n/messages/de.json +62 -33
- package/dist/runtime/i18n/messages/en.json +32 -3
- package/dist/runtime/pages/integrations/credentials.vue +4 -1
- package/dist/runtime/pages/integrations/index.vue +32 -34
- package/dist/runtime/pages/integrations/runs.vue +22 -10
- package/dist/runtime/pages/integrations/schedules.vue +64 -7
- package/dist/runtime/pages/integrations/templates/[slug].vue +13 -5
- package/dist/runtime/pages/integrations/workflows/[id].vue +193 -104
- package/dist/runtime/types/integrations.d.ts +64 -1
- package/dist/runtime/utils/cockpitSources.d.ts +22 -3
- package/dist/runtime/utils/credentialState.d.ts +6 -1
- package/dist/runtime/utils/credentialState.js +1 -1
- package/dist/runtime/utils/localize.d.ts +34 -0
- package/dist/runtime/utils/localize.js +22 -7
- package/dist/runtime/utils/pollBudget.d.ts +1 -1
- package/dist/runtime/utils/servedPaths.d.ts +22 -0
- package/dist/runtime/utils/servedPaths.js +3 -0
- package/dist/runtime/utils/statusStyle.d.ts +52 -0
- package/dist/runtime/utils/statusStyle.js +26 -0
- package/package.json +1 -1
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { computed, ref, watch } from "vue";
|
|
3
|
+
import { AltArrowLeft as ArrowLeft, Bolt as Zap, Magnifier as Search } from "#solar-icons";
|
|
4
|
+
import { triggerGlyph } from "../../data/triggerGlyphs";
|
|
3
5
|
import * as Lucide from "lucide-vue-next";
|
|
4
6
|
import { INTEGRATION_NODE_CATALOG, NODE_CATEGORIES, TRIGGER_TEMPLATES } from "../../data/integrationNodeCatalog";
|
|
5
|
-
import { nodeIcon } from "../../data/nodeIcons";
|
|
6
7
|
import { resolveLocalized } from "../../utils/localize";
|
|
7
8
|
const { load: loadNodes } = useIntegrationsNodes();
|
|
8
9
|
onMounted(() => {
|
|
@@ -13,7 +14,6 @@ const emit = defineEmits(["insert"]);
|
|
|
13
14
|
const search = ref("");
|
|
14
15
|
const searchInput = ref(null);
|
|
15
16
|
defineExpose({ focusSearch: () => searchInput.value?.focus() });
|
|
16
|
-
const iconFor = nodeIcon;
|
|
17
17
|
const CORE_GROUP_BY_CATEGORY = {
|
|
18
18
|
control: "Flow",
|
|
19
19
|
transform: "Data",
|
|
@@ -76,11 +76,14 @@ function isIntegration(m) {
|
|
|
76
76
|
function makeGroup(label) {
|
|
77
77
|
return { label, children: /* @__PURE__ */ new Map(), leaves: [], count: 0 };
|
|
78
78
|
}
|
|
79
|
+
function isExecutable(m) {
|
|
80
|
+
return m.__hasBundle !== false;
|
|
81
|
+
}
|
|
79
82
|
const latestManifests = computed(() => {
|
|
80
83
|
const seen = /* @__PURE__ */ new Set();
|
|
81
84
|
const out = [];
|
|
82
85
|
for (const m of INTEGRATION_NODE_CATALOG) {
|
|
83
|
-
if (seen.has(m.slug)) continue;
|
|
86
|
+
if (seen.has(m.slug) || !isExecutable(m)) continue;
|
|
84
87
|
seen.add(m.slug);
|
|
85
88
|
out.push(m);
|
|
86
89
|
}
|
|
@@ -89,6 +92,7 @@ const latestManifests = computed(() => {
|
|
|
89
92
|
const versionsBySlug = computed(() => {
|
|
90
93
|
const map = /* @__PURE__ */ new Map();
|
|
91
94
|
for (const m of INTEGRATION_NODE_CATALOG) {
|
|
95
|
+
if (!isExecutable(m)) continue;
|
|
92
96
|
const list = map.get(m.slug);
|
|
93
97
|
if (list) list.push(m);
|
|
94
98
|
else map.set(m.slug, [m]);
|
|
@@ -282,14 +286,14 @@ const searching = computed(() => search.value.trim().length > 0);
|
|
|
282
286
|
:title="t('integration.canvas.palette.action.back')"
|
|
283
287
|
@click="popLevel"
|
|
284
288
|
>
|
|
285
|
-
<
|
|
289
|
+
<ArrowLeft :size="14" />
|
|
286
290
|
</button>
|
|
287
291
|
<div class="min-w-0 flex-1 truncate text-mono text-muted-foreground">
|
|
288
292
|
{{ levelTitle }}
|
|
289
293
|
</div>
|
|
290
294
|
</div>
|
|
291
295
|
<div class="relative">
|
|
292
|
-
<
|
|
296
|
+
<Search :size="12" class="absolute left-2 top-1/2 -translate-y-1/2 text-muted-foreground" />
|
|
293
297
|
<input
|
|
294
298
|
ref="searchInput"
|
|
295
299
|
v-model="search"
|
|
@@ -333,7 +337,7 @@ const searching = computed(() => search.value.trim().length > 0);
|
|
|
333
337
|
@click="insertTrigger(tpl)"
|
|
334
338
|
>
|
|
335
339
|
<span class="flex size-6 shrink-0 items-center justify-center rounded-md bg-emerald-100 text-emerald-600">
|
|
336
|
-
<component :is="
|
|
340
|
+
<component :is="triggerGlyph(tpl.type)" :size="13" />
|
|
337
341
|
</span>
|
|
338
342
|
<div class="min-w-0">
|
|
339
343
|
<div class="truncate text-sm font-medium">{{ resolveLocalized(tpl.name) }}</div>
|
|
@@ -397,7 +401,7 @@ const searching = computed(() => search.value.trim().length > 0);
|
|
|
397
401
|
@click="insertTrigger(tpl)"
|
|
398
402
|
>
|
|
399
403
|
<span class="flex size-6 shrink-0 items-center justify-center rounded-md bg-emerald-100 text-emerald-600">
|
|
400
|
-
<component :is="
|
|
404
|
+
<component :is="triggerGlyph(tpl.type)" :size="13" />
|
|
401
405
|
</span>
|
|
402
406
|
<div class="min-w-0">
|
|
403
407
|
<div class="truncate text-sm font-medium">{{ resolveLocalized(tpl.name) }}</div>
|
|
@@ -417,7 +421,7 @@ const searching = computed(() => search.value.trim().length > 0);
|
|
|
417
421
|
@click="pushPath(['__triggers'])"
|
|
418
422
|
>
|
|
419
423
|
<span class="flex size-6 shrink-0 items-center justify-center rounded-md bg-slate-100 text-emerald-600/80">
|
|
420
|
-
<
|
|
424
|
+
<Zap :size="13" />
|
|
421
425
|
</span>
|
|
422
426
|
<span class="min-w-0 flex-1 truncate text-sm font-medium">{{ t("integration.canvas.palette.group.triggers") }}</span>
|
|
423
427
|
<span class="text-sm tabular-nums text-muted-foreground">{{ TRIGGER_TEMPLATES.length }}</span>
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { computed, onUnmounted, ref, watch } from "vue";
|
|
3
3
|
import { Download as DownloadIcon, RefreshCircle as RefreshIcon } from "#solar-icons";
|
|
4
4
|
import { Badge, Button, Dialog, DialogContent, DialogHeader, DialogTitle } from "@revenexx/studio";
|
|
5
|
-
import {
|
|
5
|
+
import { runStatusClass, runStatusLabel } from "../../utils/statusStyle";
|
|
6
6
|
import { IntegrationsApiError } from "../../utils/apiError";
|
|
7
7
|
import { createPollBudget, SAFETY_POLL_MS } from "../../utils/pollBudget";
|
|
8
8
|
const props = defineProps({
|
|
@@ -270,9 +270,12 @@ function timelineBarClass(row) {
|
|
|
270
270
|
}
|
|
271
271
|
}
|
|
272
272
|
const timelineDetail = ref(null);
|
|
273
|
+
function timelineRowStatusWord(row) {
|
|
274
|
+
if (row.durationMs == null) return "running";
|
|
275
|
+
return row.ok ? "succeeded" : "failed";
|
|
276
|
+
}
|
|
273
277
|
function timelineRowStatus(row) {
|
|
274
|
-
|
|
275
|
-
return t(row.ok ? "integration.runs.status.succeeded" : "integration.runs.status.failed");
|
|
278
|
+
return runStatusLabel(timelineRowStatusWord(row));
|
|
276
279
|
}
|
|
277
280
|
function fmtClock(ms) {
|
|
278
281
|
try {
|
|
@@ -583,7 +586,7 @@ async function confirmAction() {
|
|
|
583
586
|
<div>
|
|
584
587
|
<div class="text-[10px] uppercase tracking-wide text-muted-foreground">{{ t("integration.runs.detail.field.status") }}</div>
|
|
585
588
|
<div class="mt-0.5">
|
|
586
|
-
<Badge variant="outline" :class="['border text-xs font-medium uppercase',
|
|
589
|
+
<Badge variant="outline" :class="['border text-xs font-medium uppercase', runStatusClass(dStatus ?? run.status)]">
|
|
587
590
|
{{ runStatusLabel(dStatus ?? run.status) }}
|
|
588
591
|
</Badge>
|
|
589
592
|
</div>
|
|
@@ -699,7 +702,7 @@ async function confirmAction() {
|
|
|
699
702
|
class="w-full flex items-center gap-3 px-3 py-2 text-left"
|
|
700
703
|
@click="toggleStep(i)"
|
|
701
704
|
>
|
|
702
|
-
<Badge variant="outline" :class="['border text-xs font-medium uppercase shrink-0',
|
|
705
|
+
<Badge variant="outline" :class="['border text-xs font-medium uppercase shrink-0', runStatusClass(st.status)]">{{ runStatusLabel(st.status) }}</Badge>
|
|
703
706
|
<div class="min-w-0 flex-1">
|
|
704
707
|
<div class="font-mono text-xs truncate">{{ st.node_slug || st.node_id }}</div>
|
|
705
708
|
<div class="text-[10px] text-muted-foreground truncate">
|
|
@@ -802,7 +805,7 @@ async function confirmAction() {
|
|
|
802
805
|
<Badge
|
|
803
806
|
v-if="timelineDetail"
|
|
804
807
|
variant="outline"
|
|
805
|
-
:class="['border text-xs font-medium uppercase',
|
|
808
|
+
:class="['border text-xs font-medium uppercase', runStatusClass(timelineRowStatusWord(timelineDetail))]"
|
|
806
809
|
>{{ timelineRowStatus(timelineDetail) }}</Badge>
|
|
807
810
|
<span v-if="timelineDetail" class="text-sm tabular-nums text-muted-foreground">{{ fmtMs(timelineDetail.durationMs) }}</span>
|
|
808
811
|
</DialogTitle>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { computed, onMounted, onUnmounted, ref } from "vue";
|
|
3
3
|
import { Badge } from "@revenexx/studio";
|
|
4
|
-
import { RUN_STATUS_WORDS, normalisedRunStatus,
|
|
4
|
+
import { RUN_STATUS_WORDS, normalisedRunStatus, runStatusClass, runStatusLabel } from "../../utils/statusStyle";
|
|
5
5
|
import { TRIGGER_TYPE_WORDS, triggerTypeLabel } from "../../data/integrationNodeCatalog";
|
|
6
6
|
const props = defineProps({
|
|
7
7
|
runs: { type: Array, required: true },
|
|
@@ -99,7 +99,7 @@ const columns = computed(() => [
|
|
|
99
99
|
@row-click="(r) => openRun(r)"
|
|
100
100
|
>
|
|
101
101
|
<template #cell-status="{ row: run }">
|
|
102
|
-
<Badge variant="outline" :class="['border text-xs font-medium uppercase',
|
|
102
|
+
<Badge variant="outline" :class="['border text-xs font-medium uppercase', runStatusClass(run.status)]">
|
|
103
103
|
{{ runStatusLabel(run.status) }}
|
|
104
104
|
</Badge>
|
|
105
105
|
</template>
|
|
@@ -100,24 +100,58 @@ function onHoverLeave() {
|
|
|
100
100
|
}
|
|
101
101
|
const toolbarVisible = computed(() => editable.value && !editing.value && (hovered.value || !!props.selected));
|
|
102
102
|
function escapeHtml(s) {
|
|
103
|
-
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
103
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
104
104
|
}
|
|
105
105
|
function inline(s) {
|
|
106
106
|
return s.replace(/`([^`\n]+)`/g, '<code class="rounded bg-black/[0.07] px-1 py-px font-mono text-[0.85em]">$1</code>').replace(/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer" class="font-medium underline decoration-current/40 underline-offset-2 hover:decoration-current">$1</a>').replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>").replace(/(^|[^*])\*([^*\n]+)\*(?!\*)/g, "$1<em>$2</em>");
|
|
107
107
|
}
|
|
108
|
+
const NOTE_HEADINGS = [
|
|
109
|
+
{ tag: "h2", cls: "m-0 text-base font-semibold" },
|
|
110
|
+
{ tag: "h3", cls: "m-0 text-sm font-semibold" },
|
|
111
|
+
{ tag: "h4", cls: "m-0 text-sm font-medium" }
|
|
112
|
+
];
|
|
113
|
+
const HEADING_RE = /^(#{1,3})\s+(.+)$/;
|
|
114
|
+
const UL_RE = /^[-*]\s+(.+)$/;
|
|
115
|
+
const OL_RE = /^(\d+)\.\s+(.+)$/;
|
|
116
|
+
function renderList(tag, items) {
|
|
117
|
+
const li = items.map(({ marker, text }) => {
|
|
118
|
+
const numeric = tag === "ol" ? " tabular-nums" : "";
|
|
119
|
+
const hidden = tag === "ul" ? ' aria-hidden="true"' : "";
|
|
120
|
+
const ordinal = tag === "ol" ? ` value="${Number.parseInt(marker, 10)}"` : "";
|
|
121
|
+
return `<li${ordinal} class="flex gap-1.5"><span${hidden} class="select-none${numeric}">${marker}</span><span class="min-w-0">${text}</span></li>`;
|
|
122
|
+
}).join("");
|
|
123
|
+
return `<${tag} role="list" class="m-0 list-none p-0 pl-1">${li}</${tag}>`;
|
|
124
|
+
}
|
|
108
125
|
function renderMarkdownLite(src) {
|
|
109
|
-
|
|
110
|
-
|
|
126
|
+
const lines = escapeHtml(src).split("\n");
|
|
127
|
+
const out = [];
|
|
128
|
+
let i = 0;
|
|
129
|
+
while (i < lines.length) {
|
|
130
|
+
const line = lines[i] ?? "";
|
|
131
|
+
const h = HEADING_RE.exec(line);
|
|
111
132
|
if (h) {
|
|
112
|
-
const cls = h[1]
|
|
113
|
-
|
|
133
|
+
const { tag, cls } = NOTE_HEADINGS[(h[1] ?? "#").length - 1] ?? NOTE_HEADINGS[2];
|
|
134
|
+
out.push(`<${tag} class="${cls}">${inline(h[2] ?? "")}</${tag}>`);
|
|
135
|
+
i++;
|
|
136
|
+
continue;
|
|
114
137
|
}
|
|
115
|
-
const
|
|
116
|
-
if (
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
138
|
+
const listRe = UL_RE.test(line) ? UL_RE : OL_RE.test(line) ? OL_RE : null;
|
|
139
|
+
if (listRe) {
|
|
140
|
+
const tag = listRe === UL_RE ? "ul" : "ol";
|
|
141
|
+
const items = [];
|
|
142
|
+
while (i < lines.length) {
|
|
143
|
+
const m = listRe.exec(lines[i] ?? "");
|
|
144
|
+
if (!m) break;
|
|
145
|
+
items.push({ marker: tag === "ul" ? "\u2022" : `${m[1]}.`, text: inline(m[m.length - 1] ?? "") });
|
|
146
|
+
i++;
|
|
147
|
+
}
|
|
148
|
+
out.push(renderList(tag, items));
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
out.push(line ? `<div>${inline(line)}</div>` : "<div> </div>");
|
|
152
|
+
i++;
|
|
153
|
+
}
|
|
154
|
+
return out.join("");
|
|
121
155
|
}
|
|
122
156
|
const rendered = computed(() => renderMarkdownLite(props.data.sticky.content));
|
|
123
157
|
function onViewMouseDown(e) {
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One shortcut into a surface the *host* provides — Import & Export, the SFTP
|
|
3
|
+
* server, the event stream, inbound webhooks. None of the four is a page of this
|
|
4
|
+
* module: they are the cockpit's, and this card is the studio's landing page
|
|
5
|
+
* pointing at them.
|
|
6
|
+
*
|
|
7
|
+
* So the card has two states, and which one it is in is not its own decision:
|
|
8
|
+
*
|
|
9
|
+
* - **`available`** — the host has the surface. The card is a link and behaves
|
|
10
|
+
* exactly as it did before PO-223.
|
|
11
|
+
* - **not available** — the card stays on screen, looks the same, and is a button
|
|
12
|
+
* that explains where the surface lives instead of leading into a route the host
|
|
13
|
+
* does not serve. Before PO-223 it was a link into nothing: in the playground
|
|
14
|
+
* and in the standalone dev studio all four led to a 404.
|
|
15
|
+
*
|
|
16
|
+
* **Staying on screen is the point**, and it is the call the managed credential
|
|
17
|
+
* sources already made (`CredentialRevenexxSource.vue`, PO-222): a control that
|
|
18
|
+
* vanishes leaves nothing to read the explanation from, and the reader cannot tell
|
|
19
|
+
* a broken feature from one that belongs somewhere else.
|
|
20
|
+
*
|
|
21
|
+
* **No `aria-label`, on purpose** — like `TemplateCard`, the card takes its name
|
|
22
|
+
* from what it shows. The state is carried by the *role*: "Import & Export,
|
|
23
|
+
* button" instead of "link" is what says to a screen reader that this does not
|
|
24
|
+
* navigate, and it says it without announcing something no sighted reader can see
|
|
25
|
+
* (`specs/accessibility.md` AC-1 names the on-screen text as the source of a name).
|
|
26
|
+
*
|
|
27
|
+
* Who decides `available`, and why it is the router rather than a guess about the
|
|
28
|
+
* host, is `docs/host-surfaces.md`.
|
|
29
|
+
*
|
|
30
|
+
* The two branches draw the same body twice rather than sharing it: `as-child`
|
|
31
|
+
* wants a real element as its child, and the alternatives — a wrapper element
|
|
32
|
+
* around the card, or a second component for eight lines — cost either the grid
|
|
33
|
+
* layout or a file. **Keep the two bodies identical.**
|
|
34
|
+
*/
|
|
35
|
+
import type { Component } from 'vue';
|
|
36
|
+
type __VLS_Props = {
|
|
37
|
+
/** What the surface is called, in the reader's language. */
|
|
38
|
+
label: string;
|
|
39
|
+
/** One line on what it is for. Shown in both states. */
|
|
40
|
+
hint: string;
|
|
41
|
+
icon: Component;
|
|
42
|
+
/** Where the surface lives in a host that has it. */
|
|
43
|
+
to: string;
|
|
44
|
+
/** Whether this host has it. See the comment above; no default on purpose. */
|
|
45
|
+
available: boolean;
|
|
46
|
+
/** What the popover says when it does not. */
|
|
47
|
+
note: string;
|
|
48
|
+
};
|
|
49
|
+
declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
50
|
+
export default _default;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { ArrowRight } from "#solar-icons";
|
|
3
|
+
import { Popover, PopoverContent, PopoverTrigger } from "@revenexx/studio";
|
|
4
|
+
defineProps({
|
|
5
|
+
label: { type: String, required: true },
|
|
6
|
+
hint: { type: String, required: true },
|
|
7
|
+
icon: { type: null, required: true },
|
|
8
|
+
to: { type: String, required: true },
|
|
9
|
+
available: { type: Boolean, required: true },
|
|
10
|
+
note: { type: String, required: true }
|
|
11
|
+
});
|
|
12
|
+
const CARD_CLASS = "group rounded-lg border border-black/[0.06] bg-white p-3 transition hover:border-black/20";
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<template>
|
|
16
|
+
<NuxtLink v-if="available" :to="to" :class="CARD_CLASS">
|
|
17
|
+
<div class="flex items-center gap-2">
|
|
18
|
+
<span class="flex size-6 items-center justify-center rounded-md bg-slate-100 text-slate-500">
|
|
19
|
+
<component :is="icon" :size="12" />
|
|
20
|
+
</span>
|
|
21
|
+
<span class="min-w-0 flex-1 truncate text-sm font-semibold">{{ label }}</span>
|
|
22
|
+
<ArrowRight :size="11" class="text-muted-foreground transition-transform group-hover:translate-x-0.5" />
|
|
23
|
+
</div>
|
|
24
|
+
<p class="mt-2 text-sm text-muted-foreground">
|
|
25
|
+
{{ hint }}
|
|
26
|
+
</p>
|
|
27
|
+
</NuxtLink>
|
|
28
|
+
|
|
29
|
+
<Popover v-else>
|
|
30
|
+
<PopoverTrigger as-child>
|
|
31
|
+
<!-- No `id` here: `PopoverTrigger` puts reka-ui's generated `triggerId` on
|
|
32
|
+
its child and `PopoverContentImpl` points the opened dialog's
|
|
33
|
+
`aria-labelledby` at exactly that id, so the popover is named by this
|
|
34
|
+
card. Ours would replace it (reka's `Slot` merges with the child's
|
|
35
|
+
props winning) and leave the dialog naming nothing. -->
|
|
36
|
+
<button type="button" :class="[CARD_CLASS, 'w-full text-left']">
|
|
37
|
+
<div class="flex items-center gap-2">
|
|
38
|
+
<span class="flex size-6 items-center justify-center rounded-md bg-slate-100 text-slate-500">
|
|
39
|
+
<component :is="icon" :size="12" />
|
|
40
|
+
</span>
|
|
41
|
+
<span class="min-w-0 flex-1 truncate text-sm font-semibold">{{ label }}</span>
|
|
42
|
+
<ArrowRight :size="11" class="text-muted-foreground transition-transform group-hover:translate-x-0.5" />
|
|
43
|
+
</div>
|
|
44
|
+
<p class="mt-2 text-sm text-muted-foreground">
|
|
45
|
+
{{ hint }}
|
|
46
|
+
</p>
|
|
47
|
+
</button>
|
|
48
|
+
</PopoverTrigger>
|
|
49
|
+
<PopoverContent align="start" class="w-72 rounded-lg border border-black/[0.06] bg-white p-3 shadow-lg">
|
|
50
|
+
<p class="text-sm text-muted-foreground">
|
|
51
|
+
{{ note }}
|
|
52
|
+
</p>
|
|
53
|
+
</PopoverContent>
|
|
54
|
+
</Popover>
|
|
55
|
+
</template>
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One shortcut into a surface the *host* provides — Import & Export, the SFTP
|
|
3
|
+
* server, the event stream, inbound webhooks. None of the four is a page of this
|
|
4
|
+
* module: they are the cockpit's, and this card is the studio's landing page
|
|
5
|
+
* pointing at them.
|
|
6
|
+
*
|
|
7
|
+
* So the card has two states, and which one it is in is not its own decision:
|
|
8
|
+
*
|
|
9
|
+
* - **`available`** — the host has the surface. The card is a link and behaves
|
|
10
|
+
* exactly as it did before PO-223.
|
|
11
|
+
* - **not available** — the card stays on screen, looks the same, and is a button
|
|
12
|
+
* that explains where the surface lives instead of leading into a route the host
|
|
13
|
+
* does not serve. Before PO-223 it was a link into nothing: in the playground
|
|
14
|
+
* and in the standalone dev studio all four led to a 404.
|
|
15
|
+
*
|
|
16
|
+
* **Staying on screen is the point**, and it is the call the managed credential
|
|
17
|
+
* sources already made (`CredentialRevenexxSource.vue`, PO-222): a control that
|
|
18
|
+
* vanishes leaves nothing to read the explanation from, and the reader cannot tell
|
|
19
|
+
* a broken feature from one that belongs somewhere else.
|
|
20
|
+
*
|
|
21
|
+
* **No `aria-label`, on purpose** — like `TemplateCard`, the card takes its name
|
|
22
|
+
* from what it shows. The state is carried by the *role*: "Import & Export,
|
|
23
|
+
* button" instead of "link" is what says to a screen reader that this does not
|
|
24
|
+
* navigate, and it says it without announcing something no sighted reader can see
|
|
25
|
+
* (`specs/accessibility.md` AC-1 names the on-screen text as the source of a name).
|
|
26
|
+
*
|
|
27
|
+
* Who decides `available`, and why it is the router rather than a guess about the
|
|
28
|
+
* host, is `docs/host-surfaces.md`.
|
|
29
|
+
*
|
|
30
|
+
* The two branches draw the same body twice rather than sharing it: `as-child`
|
|
31
|
+
* wants a real element as its child, and the alternatives — a wrapper element
|
|
32
|
+
* around the card, or a second component for eight lines — cost either the grid
|
|
33
|
+
* layout or a file. **Keep the two bodies identical.**
|
|
34
|
+
*/
|
|
35
|
+
import type { Component } from 'vue';
|
|
36
|
+
type __VLS_Props = {
|
|
37
|
+
/** What the surface is called, in the reader's language. */
|
|
38
|
+
label: string;
|
|
39
|
+
/** One line on what it is for. Shown in both states. */
|
|
40
|
+
hint: string;
|
|
41
|
+
icon: Component;
|
|
42
|
+
/** Where the surface lives in a host that has it. */
|
|
43
|
+
to: string;
|
|
44
|
+
/** Whether this host has it. See the comment above; no default on purpose. */
|
|
45
|
+
available: boolean;
|
|
46
|
+
/** What the popover says when it does not. */
|
|
47
|
+
note: string;
|
|
48
|
+
};
|
|
49
|
+
declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
50
|
+
export default _default;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { computed, resolveComponent } from "vue";
|
|
3
|
-
import { RefreshCircle as Loader2
|
|
3
|
+
import { RefreshCircle as Loader2 } from "#solar-icons";
|
|
4
|
+
import { triggerGlyph as glyphForTrigger } from "../../data/triggerGlyphs";
|
|
4
5
|
import { templateCategoryLabel, templatePackageLabel } from "../../data/integrationTemplates";
|
|
5
6
|
import TemplateFlowPreview from "./TemplateFlowPreview.vue";
|
|
6
7
|
const props = defineProps({
|
|
@@ -17,7 +18,6 @@ const full = computed(() => props.density === "full");
|
|
|
17
18
|
const banner = computed(
|
|
18
19
|
() => full.value ? props.template.images.find((i) => i.category === "banner")?.src ?? null : null
|
|
19
20
|
);
|
|
20
|
-
const TRIGGER_ICON = { schedule: Clock, webhook: Webhook, manual: Play, event: Zap };
|
|
21
21
|
const TRIGGER_LABEL_KEY = {
|
|
22
22
|
schedule: "integration.templates.card.trigger.schedule",
|
|
23
23
|
webhook: "integration.templates.trigger.webhook",
|
|
@@ -104,7 +104,7 @@ const chipSize = computed(() => full.value ? "px-2 py-0.5 text-xs" : "px-1.5 py-
|
|
|
104
104
|
:key="triggerType"
|
|
105
105
|
class="inline-flex items-center gap-1 whitespace-nowrap rounded-full bg-slate-100 px-2 py-0.5 uppercase"
|
|
106
106
|
>
|
|
107
|
-
<component :is="
|
|
107
|
+
<component :is="glyphForTrigger(triggerType)" :size="10" />
|
|
108
108
|
{{ triggerTypeLabel(triggerType) }}
|
|
109
109
|
</span>
|
|
110
110
|
<span class="flex-1" />
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import { computed } from "vue";
|
|
2
|
+
import { computed, onMounted } from "vue";
|
|
3
3
|
import * as Lucide from "lucide-vue-next";
|
|
4
|
+
import { triggerGlyph } from "../../data/triggerGlyphs";
|
|
4
5
|
import { findManifest, NODE_CATEGORIES, TRIGGER_TEMPLATES } from "../../data/integrationNodeCatalog";
|
|
5
6
|
import { resolveLocalized } from "../../utils/localize";
|
|
6
7
|
import { describeCron } from "../../utils/schedule";
|
|
@@ -12,6 +13,10 @@ const props = defineProps({
|
|
|
12
13
|
showErrorBranches: { type: Boolean, required: false, default: false }
|
|
13
14
|
});
|
|
14
15
|
const t = useIntegrationsT();
|
|
16
|
+
const { load: loadNodeCatalog } = useIntegrationsNodes();
|
|
17
|
+
onMounted(() => {
|
|
18
|
+
void loadNodeCatalog();
|
|
19
|
+
});
|
|
15
20
|
const STOP_SLUG = "revenexx:stop-and-error";
|
|
16
21
|
const NODE_W = 220;
|
|
17
22
|
const NODE_H = 58;
|
|
@@ -37,7 +42,11 @@ const boxes = computed(() => {
|
|
|
37
42
|
subtitle,
|
|
38
43
|
type: t2.type,
|
|
39
44
|
manifest: null,
|
|
40
|
-
|
|
45
|
+
// The trigger's glyph from the one map, like every other trigger box. This
|
|
46
|
+
// resolved `TRIGGER_TEMPLATES`' icon name ('Zap', 'Play', …) out of Lucide
|
|
47
|
+
// instead — the same route the palette's tiles took — so the preview drew a
|
|
48
|
+
// Lucide bolt where the card it previews draws Solar's (PO-329).
|
|
49
|
+
icon: triggerGlyph(t2.type)
|
|
41
50
|
});
|
|
42
51
|
});
|
|
43
52
|
blob.nodes.forEach((n, i) => {
|
|
@@ -162,7 +171,7 @@ const bounds = computed(() => {
|
|
|
162
171
|
>
|
|
163
172
|
<div class="flex flex-1 items-center gap-2.5 px-3 py-2">
|
|
164
173
|
<span class="flex size-8 shrink-0 items-center justify-center rounded-md bg-emerald-500 text-white shadow-sm">
|
|
165
|
-
<component :is="b.icon" :size="15"
|
|
174
|
+
<component :is="b.icon" :size="15" />
|
|
166
175
|
</span>
|
|
167
176
|
<div class="min-w-0 flex-1">
|
|
168
177
|
<div class="truncate text-sm font-semibold leading-tight text-foreground">{{ b.label }}</div>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { computed, ref, watch } from "vue";
|
|
3
|
-
import { RefreshCircle as Loader2, MagicStick as Sparkles } from "#solar-icons";
|
|
3
|
+
import { RefreshCircle as Loader2, Magnifier as Search, MagicStick as Sparkles } from "#solar-icons";
|
|
4
|
+
import { triggerGlyph } from "../../data/triggerGlyphs";
|
|
4
5
|
import * as Lucide from "lucide-vue-next";
|
|
5
6
|
import {
|
|
6
7
|
Badge,
|
|
@@ -179,7 +180,7 @@ async function useTemplate(slug) {
|
|
|
179
180
|
<SelectValue>
|
|
180
181
|
<span class="flex min-w-0 items-center gap-2">
|
|
181
182
|
<span class="flex size-6 shrink-0 items-center justify-center rounded-md bg-emerald-100 text-emerald-600">
|
|
182
|
-
<component :is="
|
|
183
|
+
<component :is="triggerGlyph(selectedTrigger?.type)" :size="13" />
|
|
183
184
|
</span>
|
|
184
185
|
<span class="shrink-0 text-sm font-medium">{{ resolveLocalized(selectedTrigger?.name) }}</span>
|
|
185
186
|
<span class="truncate text-sm text-muted-foreground">{{ resolveLocalized(selectedTrigger?.description) }}</span>
|
|
@@ -190,7 +191,7 @@ async function useTemplate(slug) {
|
|
|
190
191
|
<SelectItem v-for="t in TRIGGER_TEMPLATES" :key="t.id" :value="t.type">
|
|
191
192
|
<span class="flex items-center gap-2.5 py-0.5">
|
|
192
193
|
<span class="flex size-7 shrink-0 items-center justify-center rounded-md bg-emerald-100 text-emerald-600">
|
|
193
|
-
<component :is="
|
|
194
|
+
<component :is="triggerGlyph(t.type)" :size="14" />
|
|
194
195
|
</span>
|
|
195
196
|
<span class="min-w-0">
|
|
196
197
|
<span class="block text-sm font-medium">{{ resolveLocalized(t.name) }}</span>
|
|
@@ -250,7 +251,7 @@ async function useTemplate(slug) {
|
|
|
250
251
|
<!-- ── From template ─────────────────────────────────────────────────── -->
|
|
251
252
|
<div v-else class="-m-1 max-h-[65vh] space-y-3 overflow-y-auto p-1">
|
|
252
253
|
<div class="relative">
|
|
253
|
-
<
|
|
254
|
+
<Search :size="12" class="absolute left-2 top-1/2 -translate-y-1/2 text-muted-foreground" />
|
|
254
255
|
<input
|
|
255
256
|
v-model="templateSearch"
|
|
256
257
|
type="text"
|
|
@@ -4,6 +4,22 @@ import '@vue-flow/minimap/dist/style.css';
|
|
|
4
4
|
import type { Workflow, WorkflowBlob } from '../../types/integrations.js';
|
|
5
5
|
type __VLS_Props = {
|
|
6
6
|
workflow: Workflow;
|
|
7
|
+
/**
|
|
8
|
+
* Unsaved work the page is still holding for this workflow, read ONCE, at
|
|
9
|
+
* mount (behaviour specs/workflow-save.md).
|
|
10
|
+
*
|
|
11
|
+
* The editor is mounted per tab, so a trip to the Runs tab and back seeded a
|
|
12
|
+
* fresh working copy from the SAVED blob while the page still held the draft:
|
|
13
|
+
* the canvas showed one graph, the header said "Unsaved changes" about
|
|
14
|
+
* another, and Save wrote the one nobody could see. Seeding from the draft is
|
|
15
|
+
* what makes a move inside the workflow keep the work.
|
|
16
|
+
*
|
|
17
|
+
* Deliberately not watched. The page's draft is a copy of what this editor
|
|
18
|
+
* last emitted, so following it would re-seed the canvas — and drop the
|
|
19
|
+
* selection, the undo stack and any open dialog with it — on the user's own
|
|
20
|
+
* keystrokes.
|
|
21
|
+
*/
|
|
22
|
+
draft?: WorkflowBlob | null;
|
|
7
23
|
/** Busy flag for the control-pane Play button (a run is being triggered). */
|
|
8
24
|
runBusy?: boolean;
|
|
9
25
|
/** The run is queued behind a bundle build — busy, but worth saying why. */
|