@revenexx/studio-integrations 2.3.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 +36 -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/WorkflowContextMenu.d.vue.ts +2 -0
- package/dist/runtime/components/Integrations/WorkflowContextMenu.vue +3 -2
- package/dist/runtime/components/Integrations/WorkflowContextMenu.vue.d.ts +2 -0
- package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +4 -3
- package/dist/runtime/components/Integrations/WorkflowEditor.d.vue.ts +6 -3
- package/dist/runtime/components/Integrations/WorkflowEditor.vue +7 -27
- package/dist/runtime/components/Integrations/WorkflowEditor.vue.d.ts +6 -3
- 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 +30 -1
- package/dist/runtime/i18n/messages/en.json +30 -1
- package/dist/runtime/pages/integrations/workflows/[id].vue +29 -12
- 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
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,41 @@
|
|
|
1
1
|
# @revenexx/studio-integrations
|
|
2
2
|
|
|
3
|
+
## 2.5.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 2d78623: Testing a credential on its detail view now tries the values in the form when they differ from what is stored (PO-535). It used to test the stored access data whatever had been typed, and recorded that answer, so a working new key read as failing on the list until it was saved. A test of an unsaved edit no longer changes the credential's state; with nothing changed, the test is a test of the stored credential as before. Once a test has run on the detail view, its result is the only answer shown: the credential's state at the top stands aside until the view is opened again. The time beside that state is now translated (German read "verified vor 1 Min."). The detail view gains a **Cancel** that closes it and leaves the stored values as they were. Saving changed access data now tests what was saved, so the state follows a key that was just fixed; a save that only renames asks the provider nothing. Against an Integrations API without it, a test of typed values says that they were not tried rather than showing the stored values' answer as theirs. Needs the Integrations API with PO-535, which takes an optional `config` on `POST /credentials/{id}/test`. `test()` from `useIntegrationsCredentials` gains that optional `config`, and its result an optional `recorded`.
|
|
8
|
+
- 0aa67e6: A publisher can see and retire the versions of the nodes a package publishes (PO-549).
|
|
9
|
+
|
|
10
|
+
- New `IntegrationsNodeVersionsPanel` component, taking a `package-name`. It lists
|
|
11
|
+
every node that package registers with each of its versions, marking which are
|
|
12
|
+
offered and which are retired, with the reason. The cockpit mounts it as the
|
|
13
|
+
_Nodes_ tab of one of _My integrations_.
|
|
14
|
+
- Retiring a version takes two steps. The publisher types it out as
|
|
15
|
+
`<slug>@<version>`, then sees how widely it is used (workflows, how many are
|
|
16
|
+
active, tenants, templates) before retiring it. A retired version can be restored
|
|
17
|
+
without typing.
|
|
18
|
+
- The palette no longer offers a version its publisher retired, and neither does
|
|
19
|
+
moving a node to another version. A workflow already pinned to a retired version
|
|
20
|
+
still draws and runs, and its step still says a newer version is available.
|
|
21
|
+
- The panel marks a version that was registered but never built as _Not built_,
|
|
22
|
+
not _Offered_.
|
|
23
|
+
|
|
24
|
+
Needs an Integrations service that reports `retired` on `GET /nodes` and serves
|
|
25
|
+
`/nodes/{slug}/{version}/retire` and `/usage` (revenexx/integrations#275).
|
|
26
|
+
|
|
27
|
+
### Patch Changes
|
|
28
|
+
|
|
29
|
+
- aabada7: A toast sent from the studio now reaches the host's `<Toaster>` when the host's Vite dev server pre-bundles `@revenexx/studio` (PO-276). Vite inlined `vue-sonner` into that pre-bundle while the module's transpiled runtime imported the package itself, so the two held separate stores and no message ever appeared — with nothing in the console. The module now adds `vue-sonner` to `vite.optimizeDeps.include` whenever the host can resolve it, keeping whatever the host listed there. Hosts that already pre-bundle it need no change.
|
|
30
|
+
- 1f6e49c: The `{ }` mapping list is as wide as its setting and shows every entry in full, and a step nobody renamed is called what its card calls it instead of its slug (PO-601).
|
|
31
|
+
- b816ff6: A manual trigger no longer looks like a second Run button (PO-606). Its glyph is now a person instead of the play triangle — the Run button's glyph — on every surface that draws a trigger: canvas card, node palette, inspector, create dialog, template card and detail page. And a trigger's glyph now sits on the same light chip a node's does — on the canvas card and in a template's flow preview instead of a filled green one, so nothing on the card reads as pressable, and in the node palette and the new-workflow dialog instead of a pale green one, so a trigger looks the same from picking it to placing it. The green border, background and footer still mark it as a trigger.
|
|
32
|
+
|
|
33
|
+
## 2.4.0
|
|
34
|
+
|
|
35
|
+
### Minor Changes
|
|
36
|
+
|
|
37
|
+
- 22eb158: Run in the workflow editor no longer starts the saved version while the canvas shows unsaved changes (PO-567). A changed setting used to be run as its old value, with nothing on screen to say so. Run now stands in the header beside Save rather than in the canvas's control pane, and while there is unsaved work it reads **Save & run** (German **Speichern & Run**): the press saves first and runs the workflow as it then stands. A save that is refused, or waits on the question whether to keep the work as paused, starts no run. The canvas's right-click menu names the save the same way. The header's dispatch badge now shows the mode as entered in the settings, as the name beside it already did, because that is the mode the next run goes out in. `WorkflowEditor` loses its `runWaiting` prop and gains `runSaves`.
|
|
38
|
+
|
|
3
39
|
## 2.3.0
|
|
4
40
|
|
|
5
41
|
### Minor Changes
|
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readdirSync, statSync, existsSync } from 'node:fs';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
|
-
import { defineNuxtModule, createResolver, addPlugin, hasNuxtModule, installModule, addComponentsDir, addImportsDir, extendPages } from '@nuxt/kit';
|
|
2
|
+
import { join, dirname } from 'node:path';
|
|
3
|
+
import { defineNuxtModule, createResolver, addPlugin, hasNuxtModule, installModule, extendViteConfig, addComponentsDir, addImportsDir, extendPages } from '@nuxt/kit';
|
|
4
4
|
|
|
5
5
|
function toSegment(name) {
|
|
6
6
|
if (name.startsWith("[...")) return `:${name.slice(4, -1)}(.*)*`;
|
|
@@ -23,6 +23,29 @@ function scanRoutes(dir, baseUrl, baseName) {
|
|
|
23
23
|
return routes;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
const SHARED_PEERS_TO_PRE_BUNDLE = ["vue-sonner"];
|
|
27
|
+
function resolvableSharedPeers(rootDir, exists = existsSync) {
|
|
28
|
+
return SHARED_PEERS_TO_PRE_BUNDLE.filter((pkg) => {
|
|
29
|
+
for (let dir = rootDir; ; dir = dirname(dir)) {
|
|
30
|
+
if (exists(join(dir, "node_modules", pkg, "package.json"))) {
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
if (dirname(dir) === dir) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
function withPreBundled(include, peers) {
|
|
40
|
+
const merged = [...include ?? []];
|
|
41
|
+
for (const pkg of peers) {
|
|
42
|
+
if (!merged.includes(pkg)) {
|
|
43
|
+
merged.push(pkg);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return merged;
|
|
47
|
+
}
|
|
48
|
+
|
|
26
49
|
const module$1 = defineNuxtModule({
|
|
27
50
|
meta: {
|
|
28
51
|
name: "@revenexx/studio-integrations",
|
|
@@ -58,6 +81,11 @@ const module$1 = defineNuxtModule({
|
|
|
58
81
|
new RegExp(runtime.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
|
|
59
82
|
);
|
|
60
83
|
nuxt.options.build.transpile.push(runtime);
|
|
84
|
+
const peers = resolvableSharedPeers(nuxt.options.rootDir);
|
|
85
|
+
extendViteConfig((config) => {
|
|
86
|
+
config.optimizeDeps ??= {};
|
|
87
|
+
config.optimizeDeps.include = withPreBundled(config.optimizeDeps.include, peers);
|
|
88
|
+
});
|
|
61
89
|
addComponentsDir({ path: join(runtime, "components"), pathPrefix: true });
|
|
62
90
|
addImportsDir(join(runtime, "composables"));
|
|
63
91
|
addPlugin(join(runtime, "plugins", "realtime.client"));
|
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
import { toast } from "../../utils/notify";
|
|
33
33
|
import { apiRefusal, errorReason } from "../../utils/apiError";
|
|
34
34
|
import { credentialState } from "../../utils/credentialState";
|
|
35
|
+
import { credentialEditDiffers } from "../../utils/credentialEdit";
|
|
35
36
|
import { relativeTime } from "../../utils/relativeTime";
|
|
36
37
|
import { useNow } from "../../composables/useNow";
|
|
37
38
|
import { hasRevenexxSource } from "../../utils/revenexxCredentialSource";
|
|
@@ -98,9 +99,11 @@ function formatDateTime(iso) {
|
|
|
98
99
|
return String(iso);
|
|
99
100
|
}
|
|
100
101
|
}
|
|
102
|
+
let viewOpening = 0;
|
|
101
103
|
watch(
|
|
102
104
|
[() => props.open, () => props.credential?.id],
|
|
103
105
|
() => {
|
|
106
|
+
viewOpening++;
|
|
104
107
|
if (!props.open || !props.credential) return;
|
|
105
108
|
tab.value = "credentials";
|
|
106
109
|
form.name = props.credential.name;
|
|
@@ -122,26 +125,41 @@ function buildConfigPayload() {
|
|
|
122
125
|
}
|
|
123
126
|
async function runTest() {
|
|
124
127
|
if (!props.credential) return;
|
|
128
|
+
const opening = viewOpening;
|
|
125
129
|
testing.value = true;
|
|
126
130
|
liveTest.value = null;
|
|
127
131
|
clearRefusal();
|
|
128
132
|
try {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
133
|
+
const payload = buildConfigPayload();
|
|
134
|
+
const edited = credentialEditDiffers(fieldsOf(props.type), payload, props.credential.public_config ?? {});
|
|
135
|
+
const result = await test(props.credential.id, edited ? payload : void 0);
|
|
136
|
+
if (result.recorded !== false) {
|
|
137
|
+
await load(true);
|
|
138
|
+
emit("changed");
|
|
139
|
+
}
|
|
140
|
+
if (opening !== viewOpening) return;
|
|
141
|
+
liveTest.value = edited && result.recorded === void 0 && !result.error ? { ok: false, error: t("integration.credentials.detail.test.editNotSupported") } : result;
|
|
132
142
|
} finally {
|
|
133
|
-
testing.value = false;
|
|
143
|
+
if (opening === viewOpening) testing.value = false;
|
|
134
144
|
}
|
|
135
145
|
}
|
|
146
|
+
async function testSaved(id) {
|
|
147
|
+
await test(id);
|
|
148
|
+
emit("changed");
|
|
149
|
+
}
|
|
136
150
|
async function save() {
|
|
137
151
|
if (!props.credential || !form.name.trim()) return;
|
|
138
152
|
saving.value = true;
|
|
139
153
|
clearRefusal();
|
|
154
|
+
const id = props.credential.id;
|
|
155
|
+
const payload = buildConfigPayload();
|
|
156
|
+
const edited = credentialEditDiffers(fieldsOf(props.type), payload, props.credential.public_config ?? {});
|
|
140
157
|
try {
|
|
141
|
-
await update(
|
|
158
|
+
await update(id, { name: form.name.trim(), config: payload });
|
|
142
159
|
emit("changed");
|
|
143
160
|
emit("update:open", false);
|
|
144
161
|
toast.success(t("integration.credentials.toast.updated"));
|
|
162
|
+
if (edited) void testSaved(id);
|
|
145
163
|
} catch (e) {
|
|
146
164
|
saveRefusal.value = e;
|
|
147
165
|
void focusFirstRefusedField();
|
|
@@ -209,13 +227,18 @@ async function connect() {
|
|
|
209
227
|
pane was the whole tab, so nothing about the spacing around them moves. -->
|
|
210
228
|
<template v-if="tab === 'credentials'">
|
|
211
229
|
<div class="space-y-5 max-h-[62vh] overflow-y-auto pr-1 pt-4">
|
|
212
|
-
<!-- Connection state — the headline answer: are these credentials correct?
|
|
213
|
-
|
|
230
|
+
<!-- Connection state — the headline answer: are these credentials correct?
|
|
231
|
+
Until the operator tests from here: from then on the result below is
|
|
232
|
+
the only answer on screen. The state is about the stored values and a
|
|
233
|
+
test may have tried the typed ones, and two answers side by side — red
|
|
234
|
+
above, green below — were read as the studio contradicting itself, not
|
|
235
|
+
as two questions (PO-535). -->
|
|
236
|
+
<div v-if="!liveTest" class="rounded-lg border p-3" :class="state.toneClass">
|
|
214
237
|
<div class="flex items-center gap-2">
|
|
215
238
|
<CheckCircle2 v-if="state.key === 'connected'" :size="16" class="shrink-0" />
|
|
216
239
|
<XCircle v-else-if="state.key === 'failed'" :size="16" class="shrink-0" />
|
|
217
240
|
<span class="font-medium">{{ state.label }}</span>
|
|
218
|
-
<span v-if="state.at" class="text-sm opacity-70"
|
|
241
|
+
<span v-if="state.at" class="text-sm opacity-70">{{ t("integration.credentials.detail.state.testedAt", { time: relativeTime(state.at, now) }) }}</span>
|
|
219
242
|
<div class="flex-1" />
|
|
220
243
|
<Button
|
|
221
244
|
v-if="isOAuthAuthcode"
|
|
@@ -229,19 +252,26 @@ async function connect() {
|
|
|
229
252
|
{{ t("integration.credentials.detail.action.reconnect") }}
|
|
230
253
|
</Button>
|
|
231
254
|
</div>
|
|
232
|
-
<!-- The stored sentence
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
The freshly returned one wins, being whole (specs/credential-test.md
|
|
255
|
+
<!-- The stored sentence. Never beside a fresh one: the whole block stands
|
|
256
|
+
aside once a test has run here, and the reload that follows a test puts
|
|
257
|
+
the same sentence in it — printed twice, and past the length the
|
|
258
|
+
platform truncates at, twice differently (specs/credential-test.md
|
|
237
259
|
AC-1). -->
|
|
238
|
-
<p v-if="state.message
|
|
260
|
+
<p v-if="state.message" class="mt-1 text-sm break-words opacity-90">
|
|
239
261
|
{{ state.message }}
|
|
240
262
|
</p>
|
|
241
263
|
<p v-else-if="state.key === 'untested'" class="mt-1 text-sm opacity-80">
|
|
242
264
|
{{ t("integration.credentials.detail.state.untestedHint") }}
|
|
243
265
|
</p>
|
|
244
266
|
</div>
|
|
267
|
+
<!-- The state stood aside for a test result, and took the way to reconnect
|
|
268
|
+
with it; a three-legged credential keeps that way open. -->
|
|
269
|
+
<div v-else-if="isOAuthAuthcode" class="flex justify-end">
|
|
270
|
+
<Button size="sm" variant="outline" :disabled="connecting" @click="connect">
|
|
271
|
+
<Loader2 v-if="connecting" :size="13" class="mr-1 animate-spin" />
|
|
272
|
+
{{ t("integration.credentials.detail.action.reconnect") }}
|
|
273
|
+
</Button>
|
|
274
|
+
</div>
|
|
245
275
|
|
|
246
276
|
<!-- Editable fields -->
|
|
247
277
|
<div class="space-y-3">
|
|
@@ -359,6 +389,12 @@ async function connect() {
|
|
|
359
389
|
|
|
360
390
|
<div class="flex items-center gap-2 pt-3">
|
|
361
391
|
<div class="flex-1" />
|
|
392
|
+
<!-- Leaves everything as stored: what was typed goes, since the form is
|
|
393
|
+
seeded afresh each time the view opens. The one click back after a test
|
|
394
|
+
of a new key failed, where the ✕ does not say it discards anything. -->
|
|
395
|
+
<Button variant="ghost" :disabled="saving" @click="emit('update:open', false)">
|
|
396
|
+
{{ t("integration.common.action.cancel") }}
|
|
397
|
+
</Button>
|
|
362
398
|
<Button variant="outline" :disabled="testing || saving" @click="runTest">
|
|
363
399
|
<Loader2 v-if="testing" :size="14" class="mr-1 animate-spin" />
|
|
364
400
|
<TestIcon v-else :size="14" class="mr-1" />
|
|
@@ -130,23 +130,39 @@ async function applySftp() {
|
|
|
130
130
|
applying.value = false;
|
|
131
131
|
}
|
|
132
132
|
}
|
|
133
|
+
function smtpLogin(cred) {
|
|
134
|
+
return { user: cred.smtp_username || cred.name, password: cred.smtp_password || cred.key || "" };
|
|
135
|
+
}
|
|
136
|
+
function applyMailLogin(cred) {
|
|
137
|
+
const login = smtpLogin(cred);
|
|
138
|
+
if (!login.password) {
|
|
139
|
+
toast.error(t("integration.credentials.revenexxSource.error.mailCredentialNoSecret"));
|
|
140
|
+
emit("apply", { user: "", password: "" });
|
|
141
|
+
applied.value = null;
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
emit("apply", {
|
|
145
|
+
host: mailSmtpHost,
|
|
146
|
+
port: mailSmtpPort,
|
|
147
|
+
user: login.user,
|
|
148
|
+
password: login.password,
|
|
149
|
+
secure: mailSmtpPort === 465
|
|
150
|
+
});
|
|
151
|
+
applied.value = `${login.user}@${mailSmtpHost}:${mailSmtpPort}`;
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
133
154
|
async function applySmtp() {
|
|
134
155
|
if (!mail) return;
|
|
156
|
+
const id = selectedId.value;
|
|
135
157
|
applying.value = true;
|
|
136
158
|
try {
|
|
137
|
-
const cred = await mail.getCredential(
|
|
159
|
+
const cred = await mail.getCredential(id);
|
|
160
|
+
if (selectedId.value !== id) return;
|
|
138
161
|
if (!cred) {
|
|
139
162
|
toast.error(t("integration.credentials.revenexxSource.error.mailCredentialGone"));
|
|
140
163
|
return;
|
|
141
164
|
}
|
|
142
|
-
|
|
143
|
-
host: mailSmtpHost,
|
|
144
|
-
port: mailSmtpPort,
|
|
145
|
-
user: cred.smtp_username ?? "",
|
|
146
|
-
password: cred.smtp_password ?? "",
|
|
147
|
-
secure: mailSmtpPort === 465
|
|
148
|
-
});
|
|
149
|
-
applied.value = `${cred.smtp_username}@${mailSmtpHost}:${mailSmtpPort}`;
|
|
165
|
+
applyMailLogin(cred);
|
|
150
166
|
} catch (e) {
|
|
151
167
|
toast.error(errorReason(e, t("integration.credentials.revenexxSource.error.readMailCredential")));
|
|
152
168
|
} finally {
|
|
@@ -218,18 +234,11 @@ async function createSmtp() {
|
|
|
218
234
|
toast.error(t("integration.credentials.revenexxSource.error.mailCredentialNotReturned"));
|
|
219
235
|
return;
|
|
220
236
|
}
|
|
221
|
-
|
|
222
|
-
host: mailSmtpHost,
|
|
223
|
-
port: mailSmtpPort,
|
|
224
|
-
user: cred.smtp_username ?? "",
|
|
225
|
-
password: cred.smtp_password ?? "",
|
|
226
|
-
secure: mailSmtpPort === 465
|
|
227
|
-
});
|
|
228
|
-
applied.value = `${cred.smtp_username}@${mailSmtpHost}:${mailSmtpPort}`;
|
|
237
|
+
const copied = applyMailLogin(cred);
|
|
229
238
|
smtpCreds.value = [{ uuid: cred.uuid, name: cred.name }, ...smtpCreds.value];
|
|
230
239
|
selectedId.value = cred.uuid;
|
|
231
240
|
createOpen.value = false;
|
|
232
|
-
toast.success(t("integration.credentials.revenexxSource.toast.smtpCreated"));
|
|
241
|
+
if (copied) toast.success(t("integration.credentials.revenexxSource.toast.smtpCreated"));
|
|
233
242
|
} catch (e) {
|
|
234
243
|
toast.error(errorReason(e, t("integration.credentials.revenexxSource.error.createSmtp")));
|
|
235
244
|
} finally {
|
|
@@ -352,7 +352,10 @@ function insert(expr) {
|
|
|
352
352
|
<span class="min-w-0 truncate font-mono" :class="preview.misses ? 'text-amber-600' : 'text-emerald-700'">{{ preview.resolved }}</span>
|
|
353
353
|
</p>
|
|
354
354
|
|
|
355
|
-
<!--
|
|
355
|
+
<!-- As wide as the setting, and every line wraps (PO-601): entries of one step
|
|
356
|
+
differ only at their end — `invoices.count`, `invoices.invoices` — and a
|
|
357
|
+
fixed 20rem with `truncate` cut exactly that part off.
|
|
358
|
+
Named, because the same expressions are offered twice on this surface —
|
|
356
359
|
here and in the panel beside the form — and a popup nothing can address
|
|
357
360
|
is a popup nothing can test either. -->
|
|
358
361
|
<div
|
|
@@ -360,7 +363,7 @@ function insert(expr) {
|
|
|
360
363
|
:id="pickerId"
|
|
361
364
|
role="group"
|
|
362
365
|
:aria-label="t('integration.nodeConfig.expression.picker.aria')"
|
|
363
|
-
class="absolute right-0 top-full z-50 mt-1 flex max-h-72 w-80 flex-col rounded-md border border-black/[0.08] bg-white text-sm shadow-lg"
|
|
366
|
+
class="absolute right-0 top-full z-50 mt-1 flex max-h-72 w-full min-w-80 flex-col rounded-md border border-black/[0.08] bg-white text-sm shadow-lg"
|
|
364
367
|
@keydown="onPickerKeydown"
|
|
365
368
|
>
|
|
366
369
|
<div class="shrink-0 border-b border-black/[0.06] p-1">
|
|
@@ -387,10 +390,10 @@ function insert(expr) {
|
|
|
387
390
|
class="block w-full px-2 py-1 text-left transition hover:bg-muted/50"
|
|
388
391
|
@mousedown.prevent="insert(item.expr)"
|
|
389
392
|
>
|
|
390
|
-
<div class="
|
|
393
|
+
<div class="break-words text-muted-foreground">
|
|
391
394
|
{{ item.label }}
|
|
392
395
|
</div>
|
|
393
|
-
<div class="
|
|
396
|
+
<div class="break-all font-mono text-emerald-700">
|
|
394
397
|
{{ item.expr }}
|
|
395
398
|
</div>
|
|
396
399
|
</button>
|
|
@@ -52,6 +52,7 @@ import {
|
|
|
52
52
|
valueAtPath
|
|
53
53
|
} from "../../data/nodeRunData";
|
|
54
54
|
import { resolveDisplayName, resolveLocalized } from "../../utils/localize";
|
|
55
|
+
import { nodeDisplayName } from "../../utils/nodeDisplayName";
|
|
55
56
|
import { describeRecurrence, parseCron, scheduleTimezone, toCron } from "../../utils/schedule";
|
|
56
57
|
import { errorReason } from "../../utils/apiError";
|
|
57
58
|
const t = useIntegrationsT();
|
|
@@ -616,7 +617,10 @@ const sampleContext = computed(() => {
|
|
|
616
617
|
});
|
|
617
618
|
provide("integrationsSampleContext", sampleContext);
|
|
618
619
|
provide("integrationsNodeNames", computed(
|
|
619
|
-
() =>
|
|
620
|
+
() => (
|
|
621
|
+
// A pill names an unnamed step as its card does, not by its slug (PO-601).
|
|
622
|
+
Object.fromEntries((props.nodes ?? []).map((n) => [n.id, nodeDisplayName(n)]))
|
|
623
|
+
)
|
|
620
624
|
));
|
|
621
625
|
const upstreamSections = computed(() => {
|
|
622
626
|
if (!isNode.value) return [];
|
|
@@ -624,7 +628,7 @@ const upstreamSections = computed(() => {
|
|
|
624
628
|
const m = findManifest(node.nodeSlug, node.nodeVersion);
|
|
625
629
|
return {
|
|
626
630
|
id: node.id,
|
|
627
|
-
name:
|
|
631
|
+
name: nodeDisplayName(node),
|
|
628
632
|
manifest: m,
|
|
629
633
|
direct,
|
|
630
634
|
ports: withSampledFields(portsForNode(node, m, props.dynamicOutputs?.[node.id]), sampleFor(node.id))
|
|
@@ -3,7 +3,7 @@ import { computed, ref, watch } from "vue";
|
|
|
3
3
|
import { AltArrowLeft as ArrowLeft, Bolt as Zap, Magnifier as Search } from "#solar-icons";
|
|
4
4
|
import { triggerGlyph } from "../../data/triggerGlyphs";
|
|
5
5
|
import * as Lucide from "lucide-vue-next";
|
|
6
|
-
import { INTEGRATION_NODE_CATALOG, NODE_CATEGORIES, TRIGGER_TEMPLATES } from "../../data/integrationNodeCatalog";
|
|
6
|
+
import { INTEGRATION_NODE_CATALOG, isOffered, NODE_CATEGORIES, TRIGGER_TEMPLATES } from "../../data/integrationNodeCatalog";
|
|
7
7
|
import { resolveLocalized, studioLocale } from "../../utils/localize";
|
|
8
8
|
import { orderFolderLeaves, orderFolders } from "../../utils/paletteOrder";
|
|
9
9
|
const { load: loadNodes } = useIntegrationsNodes();
|
|
@@ -88,7 +88,7 @@ function makeGroup(label) {
|
|
|
88
88
|
return { label, children: /* @__PURE__ */ new Map(), leaves: [], count: 0 };
|
|
89
89
|
}
|
|
90
90
|
function isExecutable(m) {
|
|
91
|
-
return m
|
|
91
|
+
return isOffered(m);
|
|
92
92
|
}
|
|
93
93
|
const latestManifests = computed(() => {
|
|
94
94
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -274,6 +274,7 @@ function insertManifest(leaf) {
|
|
|
274
274
|
function insertTrigger(tpl) {
|
|
275
275
|
emit("insert", { kind: "trigger", type: tpl.type });
|
|
276
276
|
}
|
|
277
|
+
const triggerChipClass = NODE_CATEGORIES.find((c) => c.key === "trigger")?.color;
|
|
277
278
|
function categoryColor(key) {
|
|
278
279
|
const meta = key ? CORE_FOLDERS[key] : void 0;
|
|
279
280
|
if (meta) return meta.color;
|
|
@@ -351,7 +352,7 @@ const searching = computed(() => search.value.trim().length > 0);
|
|
|
351
352
|
@dragend="onDragEnd"
|
|
352
353
|
@click="insertTrigger(tpl)"
|
|
353
354
|
>
|
|
354
|
-
<span class="flex size-6 shrink-0 items-center justify-center rounded-md
|
|
355
|
+
<span class="flex size-6 shrink-0 items-center justify-center rounded-md" :class="triggerChipClass">
|
|
355
356
|
<component :is="triggerGlyph(tpl.type)" :size="13" />
|
|
356
357
|
</span>
|
|
357
358
|
<div class="min-w-0">
|
|
@@ -415,7 +416,7 @@ const searching = computed(() => search.value.trim().length > 0);
|
|
|
415
416
|
@dragend="onDragEnd"
|
|
416
417
|
@click="insertTrigger(tpl)"
|
|
417
418
|
>
|
|
418
|
-
<span class="flex size-6 shrink-0 items-center justify-center rounded-md
|
|
419
|
+
<span class="flex size-6 shrink-0 items-center justify-center rounded-md" :class="triggerChipClass">
|
|
419
420
|
<component :is="triggerGlyph(tpl.type)" :size="13" />
|
|
420
421
|
</span>
|
|
421
422
|
<div class="min-w-0">
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export type RetirableVersion = {
|
|
2
|
+
slug: string;
|
|
3
|
+
version: string;
|
|
4
|
+
name: string;
|
|
5
|
+
};
|
|
6
|
+
type __VLS_Props = {
|
|
7
|
+
target: RetirableVersion | null;
|
|
8
|
+
};
|
|
9
|
+
type __VLS_PublicProps = __VLS_Props & {
|
|
10
|
+
'open'?: boolean;
|
|
11
|
+
};
|
|
12
|
+
declare const _default: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
13
|
+
"update:open": (value: boolean) => any;
|
|
14
|
+
retired: (target: RetirableVersion) => any;
|
|
15
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
16
|
+
"onUpdate:open"?: ((value: boolean) => any) | undefined;
|
|
17
|
+
onRetired?: ((target: RetirableVersion) => any) | undefined;
|
|
18
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
19
|
+
export default _default;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed, ref, watch } from "vue";
|
|
3
|
+
import { RefreshCircle as Loader2 } from "#solar-icons";
|
|
4
|
+
import {
|
|
5
|
+
AlertDialog,
|
|
6
|
+
AlertDialogContent,
|
|
7
|
+
AlertDialogDescription,
|
|
8
|
+
AlertDialogFooter,
|
|
9
|
+
AlertDialogHeader,
|
|
10
|
+
AlertDialogTitle,
|
|
11
|
+
Button,
|
|
12
|
+
Input
|
|
13
|
+
} from "@revenexx/studio";
|
|
14
|
+
import { useIntegrationsT } from "../../composables/useIntegrationsT";
|
|
15
|
+
import { useNodeRetirement } from "../../composables/useNodeRetirement";
|
|
16
|
+
import { errorReason } from "../../utils/apiError";
|
|
17
|
+
const open = defineModel("open", { type: Boolean, ...{ default: false } });
|
|
18
|
+
const props = defineProps({
|
|
19
|
+
target: { type: [Object, null], required: true }
|
|
20
|
+
});
|
|
21
|
+
const emit = defineEmits(["retired"]);
|
|
22
|
+
const t = useIntegrationsT();
|
|
23
|
+
const { usage: readUsage, retire } = useNodeRetirement();
|
|
24
|
+
const typed = ref("");
|
|
25
|
+
const reason = ref("");
|
|
26
|
+
const step = ref("confirm");
|
|
27
|
+
const usage = ref(null);
|
|
28
|
+
const busy = ref(false);
|
|
29
|
+
const error = ref(null);
|
|
30
|
+
const expected = computed(() => props.target ? `${props.target.slug}@${props.target.version}` : "");
|
|
31
|
+
const typedMatches = computed(() => typed.value.trim() === expected.value);
|
|
32
|
+
const PLACEHOLDER = "\0";
|
|
33
|
+
const confirmPrompt = computed(() => {
|
|
34
|
+
const [before = "", after = ""] = t("integration.nodeVersions.retire.confirm.label", { version: PLACEHOLDER }).split(PLACEHOLDER);
|
|
35
|
+
return { before, after };
|
|
36
|
+
});
|
|
37
|
+
watch(open, (o) => {
|
|
38
|
+
if (!o) return;
|
|
39
|
+
typed.value = "";
|
|
40
|
+
reason.value = "";
|
|
41
|
+
step.value = "confirm";
|
|
42
|
+
usage.value = null;
|
|
43
|
+
error.value = null;
|
|
44
|
+
});
|
|
45
|
+
async function check() {
|
|
46
|
+
const target = props.target;
|
|
47
|
+
if (!target || !typedMatches.value || busy.value) return;
|
|
48
|
+
busy.value = true;
|
|
49
|
+
error.value = null;
|
|
50
|
+
try {
|
|
51
|
+
usage.value = await readUsage(target.slug, target.version);
|
|
52
|
+
step.value = "review";
|
|
53
|
+
} catch (e) {
|
|
54
|
+
error.value = errorReason(e, t("integration.nodeVersions.retire.error.generic"));
|
|
55
|
+
} finally {
|
|
56
|
+
busy.value = false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
async function confirm() {
|
|
60
|
+
const target = props.target;
|
|
61
|
+
if (!target || busy.value) return;
|
|
62
|
+
busy.value = true;
|
|
63
|
+
error.value = null;
|
|
64
|
+
try {
|
|
65
|
+
await retire(target.slug, target.version, reason.value.trim() || null);
|
|
66
|
+
open.value = false;
|
|
67
|
+
emit("retired", target);
|
|
68
|
+
} catch (e) {
|
|
69
|
+
error.value = errorReason(e, t("integration.nodeVersions.retire.error.generic"));
|
|
70
|
+
} finally {
|
|
71
|
+
busy.value = false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
</script>
|
|
75
|
+
|
|
76
|
+
<template>
|
|
77
|
+
<AlertDialog :open="open" @update:open="(v) => {
|
|
78
|
+
if (!v && !busy) open = false;
|
|
79
|
+
}">
|
|
80
|
+
<AlertDialogContent>
|
|
81
|
+
<AlertDialogHeader>
|
|
82
|
+
<AlertDialogTitle>{{ t("integration.nodeVersions.retire.dialog.title") }}</AlertDialogTitle>
|
|
83
|
+
<AlertDialogDescription>
|
|
84
|
+
<span v-if="props.target">
|
|
85
|
+
<span class="font-medium text-foreground font-mono">{{ expected }}</span>
|
|
86
|
+
{{ t("integration.nodeVersions.retire.dialog.description") }}
|
|
87
|
+
</span>
|
|
88
|
+
</AlertDialogDescription>
|
|
89
|
+
</AlertDialogHeader>
|
|
90
|
+
|
|
91
|
+
<form v-if="step === 'confirm'" class="space-y-3" @submit.prevent="check">
|
|
92
|
+
<label class="block space-y-1 text-sm">
|
|
93
|
+
<span>{{ confirmPrompt.before }}<code class="rounded bg-muted px-1 py-0.5 font-mono text-xs text-foreground">{{ expected }}</code>{{ confirmPrompt.after }}</span>
|
|
94
|
+
<Input v-model="typed" autocomplete="off" spellcheck="false" class="font-mono" :aria-label="t('integration.nodeVersions.retire.confirm.aria')" />
|
|
95
|
+
</label>
|
|
96
|
+
<label class="block space-y-1 text-sm">
|
|
97
|
+
<span>{{ t("integration.nodeVersions.retire.reason.label") }}</span>
|
|
98
|
+
<Input v-model="reason" :placeholder="t('integration.nodeVersions.retire.reason.placeholder')" />
|
|
99
|
+
</label>
|
|
100
|
+
</form>
|
|
101
|
+
|
|
102
|
+
<div v-else-if="usage" class="space-y-2 text-sm" data-testid="node-retire-usage">
|
|
103
|
+
<p v-if="usage.workflows === 0 && usage.templates === 0">
|
|
104
|
+
{{ t("integration.nodeVersions.retire.usage.none") }}
|
|
105
|
+
</p>
|
|
106
|
+
<template v-else>
|
|
107
|
+
<p class="font-medium">
|
|
108
|
+
{{ t("integration.nodeVersions.retire.usage.counts", { workflows: usage.workflows, active: usage.active_workflows, tenants: usage.tenants, templates: usage.templates }) }}
|
|
109
|
+
</p>
|
|
110
|
+
<p class="text-muted-foreground">{{ t("integration.nodeVersions.retire.usage.keepWorking") }}</p>
|
|
111
|
+
</template>
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
<p v-if="error" class="text-sm text-rose-600">{{ error }}</p>
|
|
115
|
+
|
|
116
|
+
<AlertDialogFooter>
|
|
117
|
+
<Button variant="outline" :disabled="busy" @click="open = false">
|
|
118
|
+
{{ t("integration.common.action.cancel") }}
|
|
119
|
+
</Button>
|
|
120
|
+
<Button v-if="step === 'confirm'" :disabled="!typedMatches || busy" @click="check">
|
|
121
|
+
<Loader2 v-if="busy" :size="14" class="animate-spin" />
|
|
122
|
+
{{ t("integration.nodeVersions.retire.action.check") }}
|
|
123
|
+
</Button>
|
|
124
|
+
<Button v-else class="bg-destructive text-white hover:bg-destructive/90" :disabled="busy" @click="confirm">
|
|
125
|
+
<Loader2 v-if="busy" :size="14" class="animate-spin" />
|
|
126
|
+
{{ t("integration.nodeVersions.retire.action.retire") }}
|
|
127
|
+
</Button>
|
|
128
|
+
</AlertDialogFooter>
|
|
129
|
+
</AlertDialogContent>
|
|
130
|
+
</AlertDialog>
|
|
131
|
+
</template>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export type RetirableVersion = {
|
|
2
|
+
slug: string;
|
|
3
|
+
version: string;
|
|
4
|
+
name: string;
|
|
5
|
+
};
|
|
6
|
+
type __VLS_Props = {
|
|
7
|
+
target: RetirableVersion | null;
|
|
8
|
+
};
|
|
9
|
+
type __VLS_PublicProps = __VLS_Props & {
|
|
10
|
+
'open'?: boolean;
|
|
11
|
+
};
|
|
12
|
+
declare const _default: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
13
|
+
"update:open": (value: boolean) => any;
|
|
14
|
+
retired: (target: RetirableVersion) => any;
|
|
15
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
16
|
+
"onUpdate:open"?: ((value: boolean) => any) | undefined;
|
|
17
|
+
onRetired?: ((target: RetirableVersion) => any) | undefined;
|
|
18
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
19
|
+
export default _default;
|
|
@@ -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;
|