@revenexx/studio-integrations 2.4.0 → 2.5.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 +30 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +30 -2
- package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +50 -14
- package/dist/runtime/components/Integrations/CredentialRevenexxSource.vue +27 -18
- package/dist/runtime/components/Integrations/ExpressionInput.vue +7 -4
- package/dist/runtime/components/Integrations/NodeInspector.vue +6 -2
- package/dist/runtime/components/Integrations/NodePalette.vue +5 -4
- package/dist/runtime/components/Integrations/NodeRetireDialog.d.vue.ts +19 -0
- package/dist/runtime/components/Integrations/NodeRetireDialog.vue +131 -0
- package/dist/runtime/components/Integrations/NodeRetireDialog.vue.d.ts +19 -0
- package/dist/runtime/components/Integrations/NodeVersionsPanel.d.vue.ts +5 -0
- package/dist/runtime/components/Integrations/NodeVersionsPanel.vue +134 -0
- package/dist/runtime/components/Integrations/NodeVersionsPanel.vue.d.ts +5 -0
- package/dist/runtime/components/Integrations/TemplateFlowPreview.vue +3 -2
- package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +4 -3
- package/dist/runtime/components/Integrations/WorkflowEditor.vue +4 -4
- package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +5 -4
- package/dist/runtime/composables/useIntegrationsCredentials.d.ts +6 -1
- package/dist/runtime/composables/useIntegrationsCredentials.js +6 -3
- package/dist/runtime/composables/useIntegrationsNodes.js +3 -0
- package/dist/runtime/composables/useNodeRetirement.d.ts +48 -0
- package/dist/runtime/composables/useNodeRetirement.js +31 -0
- package/dist/runtime/data/integrationNodeCatalog.d.ts +10 -1
- package/dist/runtime/data/integrationNodeCatalog.js +8 -5
- package/dist/runtime/data/triggerGlyphs.d.ts +8 -3
- package/dist/runtime/data/triggerGlyphs.js +2 -2
- package/dist/runtime/i18n/messages/de.json +28 -1
- package/dist/runtime/i18n/messages/en.json +28 -1
- package/dist/runtime/types/integrations.d.ts +7 -0
- package/dist/runtime/utils/cockpitSources.d.ts +10 -3
- package/dist/runtime/utils/credentialEdit.d.ts +16 -0
- package/dist/runtime/utils/credentialEdit.js +7 -0
- package/package.json +1 -1
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed, onMounted, ref, watch } from "vue";
|
|
3
|
+
import { RefreshCircle as Loader2 } from "#solar-icons";
|
|
4
|
+
import { Badge, Button, Card, CardContent, CardHeader, CardTitle } from "@revenexx/studio";
|
|
5
|
+
import { useIntegrationsT } from "../../composables/useIntegrationsT";
|
|
6
|
+
import { useNodeRetirement } from "../../composables/useNodeRetirement";
|
|
7
|
+
import { errorReason } from "../../utils/apiError";
|
|
8
|
+
import NodeRetireDialog, {} from "./NodeRetireDialog.vue";
|
|
9
|
+
const props = defineProps({
|
|
10
|
+
packageName: { type: [String, null], required: true }
|
|
11
|
+
});
|
|
12
|
+
const t = useIntegrationsT();
|
|
13
|
+
const { versionsOfPackage, restore } = useNodeRetirement();
|
|
14
|
+
const versions = ref([]);
|
|
15
|
+
const loading = ref(false);
|
|
16
|
+
const error = ref(null);
|
|
17
|
+
const restoring = ref(null);
|
|
18
|
+
const retireTarget = ref(null);
|
|
19
|
+
const retireOpen = ref(false);
|
|
20
|
+
const nodes = computed(() => {
|
|
21
|
+
const bySlug = /* @__PURE__ */ new Map();
|
|
22
|
+
for (const v of versions.value) {
|
|
23
|
+
const entry = bySlug.get(v.slug) ?? { slug: v.slug, name: v.name, versions: [] };
|
|
24
|
+
entry.versions.push(v);
|
|
25
|
+
bySlug.set(v.slug, entry);
|
|
26
|
+
}
|
|
27
|
+
return [...bySlug.values()];
|
|
28
|
+
});
|
|
29
|
+
async function load() {
|
|
30
|
+
if (!props.packageName) {
|
|
31
|
+
versions.value = [];
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
loading.value = true;
|
|
35
|
+
error.value = null;
|
|
36
|
+
try {
|
|
37
|
+
versions.value = await versionsOfPackage(props.packageName);
|
|
38
|
+
} catch (e) {
|
|
39
|
+
error.value = errorReason(e, t("integration.nodeVersions.error.load"));
|
|
40
|
+
} finally {
|
|
41
|
+
loading.value = false;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function askRetire(v) {
|
|
45
|
+
retireTarget.value = { slug: v.slug, version: v.version, name: v.name };
|
|
46
|
+
retireOpen.value = true;
|
|
47
|
+
}
|
|
48
|
+
async function doRestore(v) {
|
|
49
|
+
const key = `${v.slug}@${v.version}`;
|
|
50
|
+
restoring.value = key;
|
|
51
|
+
error.value = null;
|
|
52
|
+
try {
|
|
53
|
+
await restore(v.slug, v.version);
|
|
54
|
+
await load();
|
|
55
|
+
} catch (e) {
|
|
56
|
+
error.value = errorReason(e, t("integration.nodeVersions.restore.error.generic"));
|
|
57
|
+
} finally {
|
|
58
|
+
restoring.value = null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
watch(() => props.packageName, () => void load());
|
|
62
|
+
onMounted(() => void load());
|
|
63
|
+
</script>
|
|
64
|
+
|
|
65
|
+
<template>
|
|
66
|
+
<div class="space-y-4" data-testid="node-versions-panel">
|
|
67
|
+
<p v-if="!packageName" class="text-sm text-muted-foreground">
|
|
68
|
+
{{ t("integration.nodeVersions.empty.noPackage") }}
|
|
69
|
+
</p>
|
|
70
|
+
<p v-else-if="error" class="text-sm text-rose-600">{{ error }}</p>
|
|
71
|
+
<div v-if="loading" class="flex items-center gap-2 text-sm text-muted-foreground">
|
|
72
|
+
<Loader2 :size="14" class="animate-spin" />
|
|
73
|
+
{{ t("integration.nodeVersions.loading") }}
|
|
74
|
+
</div>
|
|
75
|
+
<p v-else-if="packageName && !error && !nodes.length" class="text-sm text-muted-foreground">
|
|
76
|
+
{{ t("integration.nodeVersions.empty.noNodes") }}
|
|
77
|
+
</p>
|
|
78
|
+
|
|
79
|
+
<Card v-for="node in nodes" :key="node.slug" :data-testid="`node-versions-${node.slug}`">
|
|
80
|
+
<CardHeader class="pb-2">
|
|
81
|
+
<CardTitle class="flex flex-wrap items-baseline gap-2 text-base">
|
|
82
|
+
<span>{{ node.name }}</span>
|
|
83
|
+
<span class="font-mono text-xs font-normal text-muted-foreground">{{ node.slug }}</span>
|
|
84
|
+
</CardTitle>
|
|
85
|
+
</CardHeader>
|
|
86
|
+
<CardContent>
|
|
87
|
+
<table class="w-full text-sm">
|
|
88
|
+
<thead class="text-left text-muted-foreground">
|
|
89
|
+
<tr>
|
|
90
|
+
<th class="py-1 pr-3 font-medium">{{ t("integration.nodeVersions.column.version") }}</th>
|
|
91
|
+
<th class="py-1 pr-3 font-medium">{{ t("integration.nodeVersions.column.state") }}</th>
|
|
92
|
+
<th class="py-1" />
|
|
93
|
+
</tr>
|
|
94
|
+
</thead>
|
|
95
|
+
<tbody>
|
|
96
|
+
<tr v-for="v in node.versions" :key="v.version" class="border-t border-border" :data-testid="`node-version-${v.version}`">
|
|
97
|
+
<td class="py-2 pr-3 font-mono">v{{ v.version }}</td>
|
|
98
|
+
<td class="py-2 pr-3">
|
|
99
|
+
<Badge v-if="v.retired" variant="outline" class="text-xs font-medium border-amber-500/30 text-amber-700">
|
|
100
|
+
{{ t("integration.nodeVersions.badge.retired") }}
|
|
101
|
+
</Badge>
|
|
102
|
+
<!-- Not retired is not the same as offered: a version whose code was
|
|
103
|
+
never built is left out of the palette too (PO-338). -->
|
|
104
|
+
<Badge v-else-if="v.has_bundle === false" variant="outline" class="text-xs font-medium border-slate-400/40 text-slate-600">
|
|
105
|
+
{{ t("integration.nodeVersions.badge.notBuilt") }}
|
|
106
|
+
</Badge>
|
|
107
|
+
<Badge v-else variant="outline" class="text-xs font-medium border-emerald-500/30 text-emerald-700">
|
|
108
|
+
{{ t("integration.nodeVersions.badge.offered") }}
|
|
109
|
+
</Badge>
|
|
110
|
+
<span v-if="v.retired && v.retirement_reason" class="ml-2 text-muted-foreground">{{ v.retirement_reason }}</span>
|
|
111
|
+
</td>
|
|
112
|
+
<td class="py-2 text-right">
|
|
113
|
+
<Button
|
|
114
|
+
v-if="v.retired"
|
|
115
|
+
variant="outline"
|
|
116
|
+
size="sm"
|
|
117
|
+
:disabled="restoring === `${v.slug}@${v.version}`"
|
|
118
|
+
@click="doRestore(v)"
|
|
119
|
+
>
|
|
120
|
+
{{ t("integration.nodeVersions.action.restore") }}
|
|
121
|
+
</Button>
|
|
122
|
+
<Button v-else variant="outline" size="sm" class="text-destructive" @click="askRetire(v)">
|
|
123
|
+
{{ t("integration.nodeVersions.action.retire") }}
|
|
124
|
+
</Button>
|
|
125
|
+
</td>
|
|
126
|
+
</tr>
|
|
127
|
+
</tbody>
|
|
128
|
+
</table>
|
|
129
|
+
</CardContent>
|
|
130
|
+
</Card>
|
|
131
|
+
|
|
132
|
+
<NodeRetireDialog v-model:open="retireOpen" :target="retireTarget" @retired="load" />
|
|
133
|
+
</div>
|
|
134
|
+
</template>
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
type __VLS_Props = {
|
|
2
|
+
packageName: string | null;
|
|
3
|
+
};
|
|
4
|
+
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>;
|
|
5
|
+
export default _default;
|
|
@@ -106,6 +106,7 @@ const EDGE_STROKE = {
|
|
|
106
106
|
trigger: "#94a3b8",
|
|
107
107
|
error: "#cbd5e1"
|
|
108
108
|
};
|
|
109
|
+
const triggerChipClass = NODE_CATEGORIES.find((c) => c.key === "trigger")?.color;
|
|
109
110
|
const bounds = computed(() => {
|
|
110
111
|
if (!boxes.value.length) return { x: 0, y: 0, w: 1, h: 1 };
|
|
111
112
|
const minX = Math.min(...boxes.value.map((b) => b.x)) - props.padding;
|
|
@@ -165,8 +166,8 @@ const bounds = computed(() => {
|
|
|
165
166
|
class="flex h-full w-full flex-col rounded-lg border-2 border-emerald-400/40 bg-gradient-to-br from-emerald-50 to-white shadow-sm"
|
|
166
167
|
>
|
|
167
168
|
<div class="flex flex-1 items-center gap-2.5 px-3 py-2">
|
|
168
|
-
<span class="flex size-8 shrink-0 items-center justify-center rounded-md
|
|
169
|
-
<component :is="b.icon" :size="
|
|
169
|
+
<span class="flex size-8 shrink-0 items-center justify-center rounded-md" :class="triggerChipClass">
|
|
170
|
+
<component :is="b.icon" :size="18" />
|
|
170
171
|
</span>
|
|
171
172
|
<div class="min-w-0 flex-1">
|
|
172
173
|
<div class="truncate text-sm font-semibold leading-tight text-foreground">{{ b.label }}</div>
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
SelectValue,
|
|
19
19
|
Textarea
|
|
20
20
|
} from "@revenexx/studio";
|
|
21
|
-
import { TRIGGER_TEMPLATES } from "../../data/integrationNodeCatalog";
|
|
21
|
+
import { NODE_CATEGORIES, TRIGGER_TEMPLATES } from "../../data/integrationNodeCatalog";
|
|
22
22
|
import LoadingAnnouncement from "./LoadingAnnouncement.vue";
|
|
23
23
|
import TemplateCard from "./TemplateCard.vue";
|
|
24
24
|
import TemplateInstallStep from "./TemplateInstallStep.vue";
|
|
@@ -61,6 +61,7 @@ const executionModes = computed(() => [
|
|
|
61
61
|
{ key: "caller_decides", label: t("integration.workflows.create.executionMode.callerDecides.label"), hint: t("integration.workflows.create.executionMode.callerDecides.hint"), icon: "SlidersHorizontal" }
|
|
62
62
|
]);
|
|
63
63
|
const selectedTrigger = computed(() => TRIGGER_TEMPLATES.find((tt) => tt.type === triggerType.value) ?? null);
|
|
64
|
+
const triggerChipClass = NODE_CATEGORIES.find((c) => c.key === "trigger")?.color;
|
|
64
65
|
const selectedMode = computed(() => executionModes.value.find((m) => m.key === executionMode.value) ?? executionModes.value[0]);
|
|
65
66
|
watch(open, (v) => {
|
|
66
67
|
if (!v) return;
|
|
@@ -196,7 +197,7 @@ async function installed(id) {
|
|
|
196
197
|
<SelectTrigger id="wf-trigger" class="w-full bg-white">
|
|
197
198
|
<SelectValue>
|
|
198
199
|
<span class="flex min-w-0 items-center gap-2">
|
|
199
|
-
<span class="flex size-6 shrink-0 items-center justify-center rounded-md
|
|
200
|
+
<span class="flex size-6 shrink-0 items-center justify-center rounded-md" :class="triggerChipClass">
|
|
200
201
|
<component :is="triggerGlyph(selectedTrigger?.type)" :size="13" />
|
|
201
202
|
</span>
|
|
202
203
|
<span class="shrink-0 text-sm font-medium">{{ resolveLocalized(selectedTrigger?.name) }}</span>
|
|
@@ -207,7 +208,7 @@ async function installed(id) {
|
|
|
207
208
|
<SelectContent>
|
|
208
209
|
<SelectItem v-for="tpl in TRIGGER_TEMPLATES" :key="tpl.id" :value="tpl.type">
|
|
209
210
|
<span class="flex items-center gap-2.5 py-0.5">
|
|
210
|
-
<span class="flex size-7 shrink-0 items-center justify-center rounded-md
|
|
211
|
+
<span class="flex size-7 shrink-0 items-center justify-center rounded-md" :class="triggerChipClass">
|
|
211
212
|
<component :is="triggerGlyph(tpl.type)" :size="14" />
|
|
212
213
|
</span>
|
|
213
214
|
<span class="min-w-0">
|
|
@@ -58,7 +58,8 @@ import { deepClone } from "../../utils/deepClone";
|
|
|
58
58
|
import { copyToClipboard } from "../../utils/clipboard";
|
|
59
59
|
import { groupFindingsByNode, findingsByNodeField } from "../../utils/validationFindings";
|
|
60
60
|
import { reorderBranchSubtrees } from "../../data/branchReorder";
|
|
61
|
-
import {
|
|
61
|
+
import { resolveLocalized } from "../../utils/localize";
|
|
62
|
+
import { nodeDisplayName } from "../../utils/nodeDisplayName";
|
|
62
63
|
const props = defineProps({
|
|
63
64
|
workflow: { type: Object, required: true },
|
|
64
65
|
draft: { type: [Object, null], required: false },
|
|
@@ -873,9 +874,8 @@ const selectedUpstreamOutputs = computed(() => {
|
|
|
873
874
|
const port = portsForNode(source, findManifest(source.nodeSlug, source.nodeVersion), dynamicOutputs.value[source.id]).find((p) => p.name === e.from.output);
|
|
874
875
|
return [{
|
|
875
876
|
nodeId: source.id,
|
|
876
|
-
//
|
|
877
|
-
|
|
878
|
-
nodeName: resolveDisplayName(source.name) || source.nodeSlug,
|
|
877
|
+
// Called what its card calls it, not by its slug (PO-601).
|
|
878
|
+
nodeName: nodeDisplayName(source),
|
|
879
879
|
output: e.from.output,
|
|
880
880
|
fields: port?.fields.map((f) => f.name)
|
|
881
881
|
}];
|
|
@@ -7,7 +7,7 @@ import { SIDE_POSITION } from "./canvasSides";
|
|
|
7
7
|
import { outputHandleId } from "../../data/handleId";
|
|
8
8
|
import { triggerGlyph as glyphForTrigger } from "../../data/triggerGlyphs";
|
|
9
9
|
import { resolveDisplayName } from "../../utils/localize";
|
|
10
|
-
import { triggerTypeLabel } from "../../data/integrationNodeCatalog";
|
|
10
|
+
import { NODE_CATEGORIES, triggerTypeLabel } from "../../data/integrationNodeCatalog";
|
|
11
11
|
import { describeCron } from "../../utils/schedule";
|
|
12
12
|
const props = defineProps({
|
|
13
13
|
id: { type: String, required: true },
|
|
@@ -18,6 +18,7 @@ defineOptions({ inheritAttrs: false });
|
|
|
18
18
|
defineEmits(["updateNodeInternals"]);
|
|
19
19
|
const t = useIntegrationsT();
|
|
20
20
|
const icon = computed(() => glyphForTrigger(props.data.type));
|
|
21
|
+
const chipClass = NODE_CATEGORIES.find((c) => c.key === "trigger")?.color;
|
|
21
22
|
const subtitle = computed(() => {
|
|
22
23
|
const c = props.data.config;
|
|
23
24
|
if (props.data.type === "schedule") return describeCron(c.cron) ?? c.cron ?? t("integration.canvas.trigger.subtitle.noSchedule");
|
|
@@ -72,10 +73,10 @@ const OUT_SIDES = ["bottom"];
|
|
|
72
73
|
@mouseleave="onHoverLeave"
|
|
73
74
|
>
|
|
74
75
|
<!-- Same type scale as WorkflowNodeView so trigger and node cards read
|
|
75
|
-
identically (icon chip size-8
|
|
76
|
+
identically (icon chip size-8 with an 18px glyph, subtitle plain text-sm, mt-0.5). -->
|
|
76
77
|
<div class="flex items-center gap-2.5 px-3 py-2.5">
|
|
77
|
-
<span class="flex size-8 shrink-0 items-center justify-center rounded-md
|
|
78
|
-
<component :is="icon" :size="
|
|
78
|
+
<span class="flex size-8 shrink-0 items-center justify-center rounded-md" :class="chipClass">
|
|
79
|
+
<component :is="icon" :size="18" />
|
|
79
80
|
</span>
|
|
80
81
|
<div class="min-w-0 flex-1">
|
|
81
82
|
<div class="text-sm font-semibold text-foreground leading-tight truncate">
|
|
@@ -27,6 +27,11 @@ export interface CredentialTestResult {
|
|
|
27
27
|
ok: boolean;
|
|
28
28
|
message?: string;
|
|
29
29
|
error?: string;
|
|
30
|
+
/**
|
|
31
|
+
* Whether the platform kept this answer as the credential's state. `false` for a
|
|
32
|
+
* test of an edit that tried values other than the stored ones (PO-535).
|
|
33
|
+
*/
|
|
34
|
+
recorded?: boolean;
|
|
30
35
|
}
|
|
31
36
|
export declare function useIntegrationsCredentials(): {
|
|
32
37
|
credentials: any;
|
|
@@ -49,6 +54,6 @@ export declare function useIntegrationsCredentials(): {
|
|
|
49
54
|
}) => Promise<Credential>;
|
|
50
55
|
remove: (id: string) => Promise<void>;
|
|
51
56
|
testInline: (credentialTypeSlug: string, config: Record<string, unknown>) => Promise<CredentialTestResult>;
|
|
52
|
-
test: (id: string) => Promise<CredentialTestResult>;
|
|
57
|
+
test: (id: string, config?: Record<string, unknown>) => Promise<CredentialTestResult>;
|
|
53
58
|
oauthAuthorizeUrl: (id: string) => Promise<string>;
|
|
54
59
|
};
|
|
@@ -53,10 +53,13 @@ export function useIntegrationsCredentials() {
|
|
|
53
53
|
return { ok: false, error: errorReason(e, t("integration.credentials.error.testFailed")) };
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
|
-
async function test(id) {
|
|
56
|
+
async function test(id, config) {
|
|
57
57
|
try {
|
|
58
|
-
const res = await api.post(
|
|
59
|
-
|
|
58
|
+
const res = await api.post(
|
|
59
|
+
`/credentials/${encodeURIComponent(id)}/test`,
|
|
60
|
+
config ? { config } : void 0
|
|
61
|
+
);
|
|
62
|
+
return res?.ok === false ? res : { ok: true, message: res?.message, recorded: res?.recorded };
|
|
60
63
|
} catch (e) {
|
|
61
64
|
return { ok: false, error: errorReason(e, t("integration.credentials.error.testFailed")) };
|
|
62
65
|
}
|
|
@@ -31,6 +31,9 @@ export function useIntegrationsNodes() {
|
|
|
31
31
|
// was never built can be drawn but not executed, so the palette must
|
|
32
32
|
// be able to leave it out (PO-338). Missing field = assume built.
|
|
33
33
|
__hasBundle: n.has_bundle !== false,
|
|
34
|
+
// Retired by its publisher: still drawn for the workflows already on
|
|
35
|
+
// it, offered nowhere new (PO-549). Missing field = not retired.
|
|
36
|
+
__retired: n.retired === true,
|
|
34
37
|
// The package's own order, which the palette falls back to inside a
|
|
35
38
|
// folder (PO-537). Null and absent both mean "not recorded".
|
|
36
39
|
__packagePosition: typeof n.package_position === "number" ? n.package_position : void 0
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A publisher withdrawing a version of its own node from new use, and taking
|
|
3
|
+
* that back (PO-549; behaviour in specs/node-versions.md).
|
|
4
|
+
*
|
|
5
|
+
* Retiring is not deleting. The workflows already built on the version — in any
|
|
6
|
+
* tenant, since a public package's nodes sit in workflows its publisher cannot
|
|
7
|
+
* see — keep drawing, saving and running; what a retired version loses is the
|
|
8
|
+
* offer, and the platform refuses any save that would newly use it. The platform
|
|
9
|
+
* decides who may do this (the organisation the slug is namespaced under) and
|
|
10
|
+
* insists on the version being named in `confirm`, whatever this screen does.
|
|
11
|
+
*/
|
|
12
|
+
/** One registered version of a node, as the catalogue lists it. */
|
|
13
|
+
export interface PublishedNodeVersion {
|
|
14
|
+
slug: string;
|
|
15
|
+
version: string;
|
|
16
|
+
name: string;
|
|
17
|
+
package: {
|
|
18
|
+
name: string;
|
|
19
|
+
version: string;
|
|
20
|
+
label?: string | null;
|
|
21
|
+
} | null;
|
|
22
|
+
has_bundle?: boolean;
|
|
23
|
+
retired?: boolean;
|
|
24
|
+
retired_at?: string | null;
|
|
25
|
+
retirement_reason?: string | null;
|
|
26
|
+
}
|
|
27
|
+
/** How widely one version is used, across every tenant — counts only. */
|
|
28
|
+
export interface NodeVersionUsage {
|
|
29
|
+
workflows: number;
|
|
30
|
+
active_workflows: number;
|
|
31
|
+
tenants: number;
|
|
32
|
+
templates: number;
|
|
33
|
+
}
|
|
34
|
+
interface RetirementAnswer {
|
|
35
|
+
slug: string;
|
|
36
|
+
version: string;
|
|
37
|
+
retired: boolean;
|
|
38
|
+
retired_at: string | null;
|
|
39
|
+
retirement_reason: string | null;
|
|
40
|
+
usage: NodeVersionUsage;
|
|
41
|
+
}
|
|
42
|
+
export declare function useNodeRetirement(): {
|
|
43
|
+
versionsOfPackage: (packageName: string) => Promise<PublishedNodeVersion[]>;
|
|
44
|
+
usage: (slug: string, version: string) => Promise<NodeVersionUsage>;
|
|
45
|
+
retire: (slug: string, version: string, reason: string | null) => Promise<RetirementAnswer>;
|
|
46
|
+
restore: (slug: string, version: string) => Promise<RetirementAnswer>;
|
|
47
|
+
};
|
|
48
|
+
export {};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { IntegrationsApiError } from "../utils/apiError.js";
|
|
2
|
+
import { unwrapList, unwrapRecord } from "../utils/apiEnvelope.js";
|
|
3
|
+
import { useIntegrationsApi } from "./integrationsApi.js";
|
|
4
|
+
function versionPath(slug, version) {
|
|
5
|
+
return `/nodes/${encodeURIComponent(slug)}/${encodeURIComponent(version)}`;
|
|
6
|
+
}
|
|
7
|
+
export function useNodeRetirement() {
|
|
8
|
+
const api = useIntegrationsApi();
|
|
9
|
+
async function versionsOfPackage(packageName) {
|
|
10
|
+
const all = unwrapList(await api.get("/nodes"));
|
|
11
|
+
return all.filter((n) => n.package?.name === packageName);
|
|
12
|
+
}
|
|
13
|
+
async function usage(slug, version) {
|
|
14
|
+
const answer = unwrapRecord(await api.get(`${versionPath(slug, version)}/usage`), ["usage"]);
|
|
15
|
+
const counts = answer?.usage;
|
|
16
|
+
if (!counts || typeof counts.workflows !== "number") {
|
|
17
|
+
throw new IntegrationsApiError(200, answer, "");
|
|
18
|
+
}
|
|
19
|
+
return counts;
|
|
20
|
+
}
|
|
21
|
+
async function retire(slug, version, reason) {
|
|
22
|
+
return unwrapRecord(await api.post(`${versionPath(slug, version)}/retire`, {
|
|
23
|
+
confirm: `${slug}@${version}`,
|
|
24
|
+
...reason ? { reason } : {}
|
|
25
|
+
}), ["usage"]);
|
|
26
|
+
}
|
|
27
|
+
async function restore(slug, version) {
|
|
28
|
+
return unwrapRecord(await api.del(`${versionPath(slug, version)}/retire`), ["usage"]);
|
|
29
|
+
}
|
|
30
|
+
return { versionsOfPackage, usage, retire, restore };
|
|
31
|
+
}
|
|
@@ -72,6 +72,14 @@ export declare function triggerTypeLabel(type: TriggerType | string | null | und
|
|
|
72
72
|
* executable, because the alternative is a studio that hides working nodes.
|
|
73
73
|
*/
|
|
74
74
|
export declare function isVersionExecutable(slug: string, version: string): boolean;
|
|
75
|
+
/**
|
|
76
|
+
* Whether this version is one to offer: its code was built, and its publisher
|
|
77
|
+
* has not retired it (PO-338, PO-549). What the palette inserts and what a
|
|
78
|
+
* move points at. Narrower than {@link isVersionExecutable} on purpose — a
|
|
79
|
+
* retired version still runs for the workflows already on it, it is only no
|
|
80
|
+
* longer a choice.
|
|
81
|
+
*/
|
|
82
|
+
export declare function isOffered(m: NodeManifest): boolean;
|
|
75
83
|
/**
|
|
76
84
|
* The newest version of this node that can be executed, or `null` when none can.
|
|
77
85
|
*
|
|
@@ -92,7 +100,8 @@ export declare function newestExecutableVersion(slug: string): string | null;
|
|
|
92
100
|
*
|
|
93
101
|
* A version whose code was never built is left out, for the reason the palette
|
|
94
102
|
* leaves it out: it is not a choice, it is a dead end with a version number
|
|
95
|
-
* (PO-338).
|
|
103
|
+
* (PO-338). So is one its publisher retired, which the platform would refuse
|
|
104
|
+
* to newly pin (PO-549). The version a step is pinned to is therefore not guaranteed to be in
|
|
96
105
|
* here — which is exactly the state the move exists to get out of.
|
|
97
106
|
*/
|
|
98
107
|
export declare function executableVersionsOf(slug: string): NodeManifest[];
|
|
@@ -30,17 +30,20 @@ export function isVersionExecutable(slug, version) {
|
|
|
30
30
|
const exact = INTEGRATION_NODE_CATALOG.find((m) => m.slug === slug && m.version === version);
|
|
31
31
|
return exact ? exact.__hasBundle !== false : true;
|
|
32
32
|
}
|
|
33
|
+
export function isOffered(m) {
|
|
34
|
+
return m.__hasBundle !== false && m.__retired !== true;
|
|
35
|
+
}
|
|
33
36
|
export function newestExecutableVersion(slug) {
|
|
34
|
-
const hit = INTEGRATION_NODE_CATALOG.find((m) => m.slug === slug && m
|
|
37
|
+
const hit = INTEGRATION_NODE_CATALOG.find((m) => m.slug === slug && isOffered(m));
|
|
35
38
|
return hit?.version ?? null;
|
|
36
39
|
}
|
|
37
40
|
export function executableVersionsOf(slug) {
|
|
38
|
-
return INTEGRATION_NODE_CATALOG.filter((m) => m.slug === slug && m
|
|
41
|
+
return INTEGRATION_NODE_CATALOG.filter((m) => m.slug === slug && isOffered(m));
|
|
39
42
|
}
|
|
40
43
|
export function newerExecutableVersions(slug, version) {
|
|
41
|
-
const
|
|
42
|
-
const at =
|
|
43
|
-
return at < 0 ? [] :
|
|
44
|
+
const built = INTEGRATION_NODE_CATALOG.filter((m) => m.slug === slug && m.__hasBundle !== false);
|
|
45
|
+
const at = built.findIndex((m) => m.version === version);
|
|
46
|
+
return at < 0 ? [] : built.slice(0, at).filter(isOffered);
|
|
44
47
|
}
|
|
45
48
|
export function findManifest(slug, version) {
|
|
46
49
|
const matches = INTEGRATION_NODE_CATALOG.filter((m) => m.slug === slug);
|
|
@@ -22,6 +22,11 @@ import type { TriggerType } from '../types/integrations.js';
|
|
|
22
22
|
* ('Play', 'Clock', 'Webhook', 'Zap') were Lucide names by habit rather than by
|
|
23
23
|
* contract. The node-type icon map (`nodeIcons.ts`) is a different thing and
|
|
24
24
|
* stays Lucide deliberately — see `docs/icons.md`.
|
|
25
|
+
*
|
|
26
|
+
* `manual` is a person, not the play triangle. The play glyph is the Run button's,
|
|
27
|
+
* and on a trigger card — white on the filled chip — it read as a second Run button
|
|
28
|
+
* that did nothing when pressed (PO-606). A trigger's glyph names who or what fires
|
|
29
|
+
* it; it is a label, so it must not borrow an action's shape.
|
|
25
30
|
*/
|
|
26
31
|
export declare const TRIGGER_GLYPHS: Record<TriggerType, Component>;
|
|
27
32
|
/**
|
|
@@ -32,9 +37,9 @@ export declare const TRIGGER_GLYPHS: Record<TriggerType, Component>;
|
|
|
32
37
|
* the studio has no word for yet — `templates/[slug].vue` is built for exactly
|
|
33
38
|
* that case and says so.
|
|
34
39
|
*
|
|
35
|
-
* Such a kind falls back to the **bolt**, not to the manual
|
|
36
|
-
* what the templates surfaces did before this map existed.
|
|
37
|
-
* "
|
|
40
|
+
* Such a kind falls back to the **bolt**, not to the manual trigger's glyph, which
|
|
41
|
+
* is what the templates surfaces did before this map existed. That glyph asserts
|
|
42
|
+
* "a person starts this", and for an unknown kind that is a claim rather than a
|
|
38
43
|
* gap; the bolt says only that something fires it.
|
|
39
44
|
*
|
|
40
45
|
* `Object.hasOwn`, not `?? Bolt` on its own: a plain object inherits
|
|
@@ -170,9 +170,11 @@
|
|
|
170
170
|
"integration.credentials.detail.field.typeSlug": "Typ-Slug",
|
|
171
171
|
"integration.credentials.detail.field.updated": "Aktualisiert",
|
|
172
172
|
"integration.credentials.detail.field.version": "Version",
|
|
173
|
+
"integration.credentials.detail.state.testedAt": "· geprüft {time}",
|
|
173
174
|
"integration.credentials.detail.state.untestedHint": "Testen Sie die Verbindung, um zu prüfen, ob diese Zugangsdaten tatsächlich funktionieren.",
|
|
174
175
|
"integration.credentials.detail.tab.credentials": "Zugangsdaten",
|
|
175
176
|
"integration.credentials.detail.tab.details": "Details",
|
|
177
|
+
"integration.credentials.detail.test.editNotSupported": "Der Dienst kann nur gespeicherte Werte testen, diese Werte wurden also nicht geprüft. Speichern Sie sie, um sie zu testen.",
|
|
176
178
|
"integration.credentials.error.delete": "Zugangsdaten konnten nicht gelöscht werden.",
|
|
177
179
|
"integration.credentials.error.loadFailed": "Zugangsdaten konnten nicht geladen werden.",
|
|
178
180
|
"integration.credentials.error.loadTypes": "Zugangsdatentypen konnten nicht geladen werden.",
|
|
@@ -220,6 +222,7 @@
|
|
|
220
222
|
"integration.credentials.revenexxSource.error.createSmtp": "Die Mail-Zugangsdaten konnten nicht erstellt werden.",
|
|
221
223
|
"integration.credentials.revenexxSource.error.loadResources": "revenexx-Ressourcen konnten nicht geladen werden.",
|
|
222
224
|
"integration.credentials.revenexxSource.error.mailCredentialGone": "Diese Mail-Zugangsdaten sind nicht mehr vorhanden.",
|
|
225
|
+
"integration.credentials.revenexxSource.error.mailCredentialNoSecret": "Diese Mail-Zugangsdaten haben keinen Key, deshalb wurde nichts übernommen. Wählen Sie andere aus oder prüfen Sie diese unter Mail.",
|
|
223
226
|
"integration.credentials.revenexxSource.error.mailCredentialNotReturned": "Die Mail-Zugangsdaten wurden nicht zurückgegeben — bitte aus der Liste wählen, sobald sie erscheinen.",
|
|
224
227
|
"integration.credentials.revenexxSource.error.noTenant": "Kein Mandant ausgewählt.",
|
|
225
228
|
"integration.credentials.revenexxSource.error.provisioningFailed": "Bereitstellung fehlgeschlagen.",
|
|
@@ -1350,5 +1353,29 @@
|
|
|
1350
1353
|
"integration.workflows.variables.row.aria": "Variable {key}",
|
|
1351
1354
|
"integration.workflows.variables.state.empty": "Noch keine Variablen — fügen Sie die erste unten hinzu.",
|
|
1352
1355
|
"integration.workflows.variables.toast.copied": "Ausdruck kopiert",
|
|
1353
|
-
"integration.workflows.variables.toast.copyFailed": "Kopieren fehlgeschlagen"
|
|
1356
|
+
"integration.workflows.variables.toast.copyFailed": "Kopieren fehlgeschlagen",
|
|
1357
|
+
"integration.nodeVersions.retire.dialog.title": "Diese Version ausmustern?",
|
|
1358
|
+
"integration.nodeVersions.retire.dialog.description": "wird nicht mehr angeboten: Niemand kann sie mehr in einen Workflow einfügen. Workflows, die sie schon nutzen, laufen weiter.",
|
|
1359
|
+
"integration.nodeVersions.retire.confirm.label": "Zum Fortfahren {version} eintippen",
|
|
1360
|
+
"integration.nodeVersions.retire.confirm.aria": "Auszumusternde Version, ausgeschrieben",
|
|
1361
|
+
"integration.nodeVersions.retire.reason.label": "Grund (optional)",
|
|
1362
|
+
"integration.nodeVersions.retire.reason.placeholder": "z. B. der Anbieter hat API v1 abgeschaltet",
|
|
1363
|
+
"integration.nodeVersions.retire.usage.none": "Kein Workflow und kein Template nutzt diese Version.",
|
|
1364
|
+
"integration.nodeVersions.retire.usage.counts": "Genutzt von {workflows} Workflow(s), davon {active} aktiv, in {tenants} Mandant(en), und von {templates} Template(s).",
|
|
1365
|
+
"integration.nodeVersions.retire.usage.keepWorking": "Sie laufen unverändert weiter. Die Version wird nur nicht mehr angeboten und kann keinem Workflow neu hinzugefügt werden.",
|
|
1366
|
+
"integration.nodeVersions.retire.action.check": "Nutzung prüfen",
|
|
1367
|
+
"integration.nodeVersions.retire.action.retire": "Version ausmustern",
|
|
1368
|
+
"integration.nodeVersions.retire.error.generic": "Die Version konnte nicht ausgemustert werden.",
|
|
1369
|
+
"integration.nodeVersions.restore.error.generic": "Die Version konnte nicht wiederhergestellt werden.",
|
|
1370
|
+
"integration.nodeVersions.error.load": "Die Node-Versionen konnten nicht geladen werden.",
|
|
1371
|
+
"integration.nodeVersions.empty.noPackage": "Diese Integration hat noch kein Node-Paket veröffentlicht.",
|
|
1372
|
+
"integration.nodeVersions.empty.noNodes": "Dieses Paket registriert keine Nodes.",
|
|
1373
|
+
"integration.nodeVersions.loading": "Node-Versionen werden geladen…",
|
|
1374
|
+
"integration.nodeVersions.column.version": "Version",
|
|
1375
|
+
"integration.nodeVersions.column.state": "Status",
|
|
1376
|
+
"integration.nodeVersions.badge.retired": "Ausgemustert",
|
|
1377
|
+
"integration.nodeVersions.badge.offered": "Angeboten",
|
|
1378
|
+
"integration.nodeVersions.badge.notBuilt": "Nicht gebaut",
|
|
1379
|
+
"integration.nodeVersions.action.retire": "Ausmustern",
|
|
1380
|
+
"integration.nodeVersions.action.restore": "Wiederherstellen"
|
|
1354
1381
|
}
|
|
@@ -170,9 +170,11 @@
|
|
|
170
170
|
"integration.credentials.detail.field.typeSlug": "Type slug",
|
|
171
171
|
"integration.credentials.detail.field.updated": "Updated",
|
|
172
172
|
"integration.credentials.detail.field.version": "Version",
|
|
173
|
+
"integration.credentials.detail.state.testedAt": "· verified {time}",
|
|
173
174
|
"integration.credentials.detail.state.untestedHint": "Run a test to verify these credentials actually connect.",
|
|
174
175
|
"integration.credentials.detail.tab.credentials": "Credentials",
|
|
175
176
|
"integration.credentials.detail.tab.details": "Details",
|
|
177
|
+
"integration.credentials.detail.test.editNotSupported": "The service can test only what is saved, so these values were not tried. Save them to test them.",
|
|
176
178
|
"integration.credentials.error.delete": "Could not delete credential.",
|
|
177
179
|
"integration.credentials.error.loadFailed": "Could not load credentials.",
|
|
178
180
|
"integration.credentials.error.loadTypes": "Could not load credential types.",
|
|
@@ -220,6 +222,7 @@
|
|
|
220
222
|
"integration.credentials.revenexxSource.error.createSmtp": "Could not create the mail credential.",
|
|
221
223
|
"integration.credentials.revenexxSource.error.loadResources": "Could not load revenexx resources.",
|
|
222
224
|
"integration.credentials.revenexxSource.error.mailCredentialGone": "That mail credential is no longer there.",
|
|
225
|
+
"integration.credentials.revenexxSource.error.mailCredentialNoSecret": "That mail credential has no key, so nothing was copied. Pick another one, or check this one in Mail.",
|
|
223
226
|
"integration.credentials.revenexxSource.error.mailCredentialNotReturned": "The mail credential was not returned — pick it from the list once it appears.",
|
|
224
227
|
"integration.credentials.revenexxSource.error.noTenant": "No tenant selected.",
|
|
225
228
|
"integration.credentials.revenexxSource.error.provisioningFailed": "Provisioning failed.",
|
|
@@ -1350,5 +1353,29 @@
|
|
|
1350
1353
|
"integration.workflows.variables.row.aria": "Variable {key}",
|
|
1351
1354
|
"integration.workflows.variables.state.empty": "No variables yet — add the first one below.",
|
|
1352
1355
|
"integration.workflows.variables.toast.copied": "Expression copied",
|
|
1353
|
-
"integration.workflows.variables.toast.copyFailed": "Could not copy"
|
|
1356
|
+
"integration.workflows.variables.toast.copyFailed": "Could not copy",
|
|
1357
|
+
"integration.nodeVersions.retire.dialog.title": "Retire this version?",
|
|
1358
|
+
"integration.nodeVersions.retire.dialog.description": "stops being offered: nobody can add it to a workflow any more. Workflows that already use it keep working.",
|
|
1359
|
+
"integration.nodeVersions.retire.confirm.label": "Type {version} to continue",
|
|
1360
|
+
"integration.nodeVersions.retire.confirm.aria": "Version to retire, typed out",
|
|
1361
|
+
"integration.nodeVersions.retire.reason.label": "Reason (optional)",
|
|
1362
|
+
"integration.nodeVersions.retire.reason.placeholder": "e.g. the vendor switched off API v1",
|
|
1363
|
+
"integration.nodeVersions.retire.usage.none": "No workflow and no template uses this version.",
|
|
1364
|
+
"integration.nodeVersions.retire.usage.counts": "Used by {workflows} workflow(s), {active} of them active, in {tenants} tenant(s), and by {templates} template(s).",
|
|
1365
|
+
"integration.nodeVersions.retire.usage.keepWorking": "They keep working as they are. The version is only no longer offered, and cannot be added to a workflow again.",
|
|
1366
|
+
"integration.nodeVersions.retire.action.check": "Check usage",
|
|
1367
|
+
"integration.nodeVersions.retire.action.retire": "Retire version",
|
|
1368
|
+
"integration.nodeVersions.retire.error.generic": "The version could not be retired.",
|
|
1369
|
+
"integration.nodeVersions.restore.error.generic": "The version could not be restored.",
|
|
1370
|
+
"integration.nodeVersions.error.load": "The node versions could not be loaded.",
|
|
1371
|
+
"integration.nodeVersions.empty.noPackage": "This integration has not published a node package yet.",
|
|
1372
|
+
"integration.nodeVersions.empty.noNodes": "This package registers no nodes.",
|
|
1373
|
+
"integration.nodeVersions.loading": "Loading node versions…",
|
|
1374
|
+
"integration.nodeVersions.column.version": "Version",
|
|
1375
|
+
"integration.nodeVersions.column.state": "State",
|
|
1376
|
+
"integration.nodeVersions.badge.retired": "Retired",
|
|
1377
|
+
"integration.nodeVersions.badge.offered": "Offered",
|
|
1378
|
+
"integration.nodeVersions.badge.notBuilt": "Not built",
|
|
1379
|
+
"integration.nodeVersions.action.retire": "Retire",
|
|
1380
|
+
"integration.nodeVersions.action.restore": "Restore"
|
|
1354
1381
|
}
|
|
@@ -425,6 +425,13 @@ export interface NodeManifest {
|
|
|
425
425
|
* does not make every node disappear.
|
|
426
426
|
*/
|
|
427
427
|
__hasBundle?: boolean;
|
|
428
|
+
/**
|
|
429
|
+
* Whether the publisher retired this version (editor-side enrichment from
|
|
430
|
+
* `GET /nodes`; not manifest data, PO-549). A retired version still draws and
|
|
431
|
+
* runs for the workflows already on it, but is offered nowhere new, and the
|
|
432
|
+
* platform refuses a save that newly uses it.
|
|
433
|
+
*/
|
|
434
|
+
__retired?: boolean;
|
|
428
435
|
}
|
|
429
436
|
export interface SecretRecord {
|
|
430
437
|
key: string;
|
|
@@ -205,14 +205,21 @@ export interface CockpitSftpAccount {
|
|
|
205
205
|
/**
|
|
206
206
|
* The members of the cockpit's `MailCredential` this module reads.
|
|
207
207
|
*
|
|
208
|
-
* The SMTP pair is optional on the wire
|
|
209
|
-
*
|
|
210
|
-
*
|
|
208
|
+
* The SMTP pair is optional on the wire, and the mail engine leaves it out: its
|
|
209
|
+
* `show` and create answers carry the name and the key. So the login is read the
|
|
210
|
+
* way the cockpit's Mail tab reads it — `smtp_username` or else the name, and
|
|
211
|
+
* `smtp_password` or else the key — and a credential with neither secret copies
|
|
212
|
+
* nothing rather than an empty password (PO-602).
|
|
211
213
|
*/
|
|
212
214
|
export interface CockpitMailCredential {
|
|
213
215
|
uuid: string;
|
|
214
216
|
name: string;
|
|
215
217
|
type?: string;
|
|
218
|
+
/**
|
|
219
|
+
* The credential's secret. The mail engine authenticates SMTP by it alone, and
|
|
220
|
+
* where `smtp_password` is absent it is the password (PO-602).
|
|
221
|
+
*/
|
|
222
|
+
key?: string;
|
|
216
223
|
smtp_username?: string;
|
|
217
224
|
smtp_password?: string;
|
|
218
225
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { CredentialTypeField } from '../types/integrations.js';
|
|
2
|
+
/**
|
|
3
|
+
* Whether the values an edit would send differ from what the credential holds, as
|
|
4
|
+
* far as the studio can see it (PO-535).
|
|
5
|
+
*
|
|
6
|
+
* `payload` is what a save would send: every non-secret field, and only the secrets
|
|
7
|
+
* that were typed — a blank secret means "keep the stored one" and is never sent.
|
|
8
|
+
* So a secret in the payload is a change by definition, since the stored one is never
|
|
9
|
+
* shown; a non-secret is a change when it reads differently from `public_config`.
|
|
10
|
+
*
|
|
11
|
+
* The platform judges this again on the merged values before it records a test, so
|
|
12
|
+
* this only decides whether to ask for a test of the edit at all — which takes the
|
|
13
|
+
* right to change the credential, where a test of the stored one takes only the
|
|
14
|
+
* right to read it.
|
|
15
|
+
*/
|
|
16
|
+
export declare function credentialEditDiffers(fields: CredentialTypeField[], payload: Record<string, unknown>, publicConfig: Record<string, unknown>): boolean;
|