@revenexx/studio-integrations 3.7.1 → 3.8.1
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 +21 -0
- package/dist/module.json +1 -1
- package/dist/runtime/components/Integrations/CredentialCreateDialog.vue +3 -1
- package/dist/runtime/components/Integrations/DeadLettersPanel.vue +2 -1
- package/dist/runtime/components/Integrations/GroupFrameView.vue +17 -6
- package/dist/runtime/components/Integrations/NodeConfigDynamicField.vue +37 -17
- package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.d.vue.ts +2 -0
- package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.vue +5 -2
- package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.vue.d.ts +2 -0
- package/dist/runtime/components/Integrations/NodeConfigField.vue +1 -0
- package/dist/runtime/components/Integrations/RunResponseModal.d.vue.ts +6 -0
- package/dist/runtime/components/Integrations/RunResponseModal.vue +36 -5
- package/dist/runtime/components/Integrations/RunResponseModal.vue.d.ts +6 -0
- package/dist/runtime/components/Integrations/Sidebar.vue +22 -5
- package/dist/runtime/components/Integrations/StickyNoteView.vue +22 -9
- package/dist/runtime/components/Integrations/TemplateFlowPreview.vue +3 -3
- package/dist/runtime/components/Integrations/WorkflowEdgeView.vue +3 -2
- package/dist/runtime/components/Integrations/WorkflowEditor.vue +32 -27
- package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +1 -1
- package/dist/runtime/components/Integrations/WorkflowsTable.vue +9 -14
- package/dist/runtime/composables/useCanvasTarget.d.ts +15 -0
- package/dist/runtime/composables/useCanvasTarget.js +10 -0
- package/dist/runtime/composables/useIntegrationsSummary.d.ts +29 -0
- package/dist/runtime/composables/useIntegrationsSummary.js +37 -0
- package/dist/runtime/composables/useNodeConfigResolve.d.ts +25 -2
- package/dist/runtime/composables/useNodeConfigResolve.js +11 -2
- package/dist/runtime/composables/useOAuthConnect.d.ts +5 -0
- package/dist/runtime/composables/useOAuthConnect.js +3 -1
- package/dist/runtime/i18n/messages/de.json +4 -3
- package/dist/runtime/i18n/messages/en.json +4 -3
- package/dist/runtime/pages/integrations/runs.vue +2 -1
- package/dist/runtime/pages/integrations/workflows/[id].vue +39 -2
- package/dist/runtime/utils/canvasFit.d.ts +59 -0
- package/dist/runtime/utils/canvasFit.js +78 -0
- package/dist/runtime/utils/executionMode.d.ts +18 -0
- package/dist/runtime/utils/executionMode.js +17 -0
- package/dist/runtime/utils/optionSearch.d.ts +24 -0
- package/dist/runtime/utils/optionSearch.js +31 -0
- package/dist/runtime/utils/runFailure.d.ts +37 -0
- package/dist/runtime/utils/runFailure.js +16 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# @revenexx/studio-integrations
|
|
2
2
|
|
|
3
|
+
## 3.8.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- f5c169c: The insert and delete buttons on a connection, and the corners a sticky note and a group frame are resized by, are now at least 24 pixels on screen at every zoom level. They used to shrink with the canvas: a workflow of eighteen steps opens at 58 %, and there they measured 14, 11.6 and 9.3 pixels. The connection's buttons now keep their size on screen, as the node's toolbar already did. The resize corners look the same as before, and the area that reacts to the pointer reaches outward past the note or frame, so a bigger target covers none of the content. The pencil and the note badge on a node card are not changed here.
|
|
8
|
+
|
|
9
|
+
## 3.8.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- 0933441: A trigger kind is named in the reader's language wherever the studio shows one — "Zeitplan", "Manuell" in German, "Schedule", "Manual" in English. The workflows list, the trigger card on the canvas, the template miniature and the origin of a missed run used to print the platform's own word ("schedule", "Trigger · manual") in every language. The workflows list's Mode column and its filter now use the same word as the workflow's header: "Async", "Sync" and "Caller decides" in English, "Asynchron", "Synchron" and "Aufrufer entscheidet" in German, where the list used to show "async", "sync" and "caller" (in German "Aufrufer"). The filter's option names change with it, which is why this is a minor. The list sorts both columns by the word it shows. The three `integration.workflows.table.mode.*` message keys are gone; the list reads `integration.editor.executionMode.*`.
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- 78b99ec: A setting whose options the platform resolves is no longer held back by a setting it depends on that the node does not require. That setting is sent with the question when it is filled in, and left out when it is empty — so SOAP Request's operation list reads a WSDL behind a login with the chosen credential, and a public WSDL still without one. A setting the node requires still holds the list back and says so.
|
|
18
|
+
- b3001f3: When the browser refuses to open the window for connecting a credential at the provider — a pop-up blocker, a hardened profile — the studio now says so and says what to do: "The browser blocked the window for signing in at the provider. Allow pop-ups for this site, then try again." Before, Connect and Reconnect did nothing visible in the credentials list, a credential's detail view, and the New-credential dialog straight after saving. The New-credential dialog now also reports an authorisation address the service could not hand back, which it used to drop silently. New message key `integration.credentials.error.popupBlocked`.
|
|
19
|
+
- a3bbf46: The badges beside Workflows, Runs, Schedules, Credentials and Secrets in the navigation now show the tenant's counts — the same figure on every page — instead of the length of whatever the last page loaded. They are read from the Integrations service's new `GET /v1/summary`, which a host's own sidebar can read too through `useIntegrationsSummary()`.
|
|
20
|
+
- 8e21845: Typing into a setting whose options come from the platform now narrows its list or says that nothing matches, instead of showing every option again, and puts the option whose code or name starts with what was typed first. While the answer to a typed term is awaited the options stay where they are and cannot be chosen, and the field shows that it is asking, so a click no longer lands on the option below the one aimed at.
|
|
21
|
+
- 9e773ec: A workflow opens, and *Fit* and *Tidy up* fit it, into the part of the canvas that *Add node*, the Map and the control pane leave free, wherever each is docked or floated, so no step lands under a panel where it cannot be clicked; a panel collapsed to its pill takes no room (PO-660). The view a workflow opens on used to avoid only the control pane.
|
|
22
|
+
- 5861e33: A run started from the editor that fails now says so in the run dialog: instead of a green "Run started" confirmation and "The run returned nothing", it shows that the run failed, the step it failed at (named as on the canvas) and the cause, with "View run" for the details. This holds for both the waiting and the immediate mode.
|
|
23
|
+
|
|
3
24
|
## 3.7.1
|
|
4
25
|
|
|
5
26
|
### Patch Changes
|
package/dist/module.json
CHANGED
|
@@ -137,7 +137,9 @@ async function submit() {
|
|
|
137
137
|
await load(true);
|
|
138
138
|
toast.success(t("integration.credentials.toast.created"));
|
|
139
139
|
if (isOAuthAuthcode(pickedType.value)) {
|
|
140
|
-
|
|
140
|
+
connect(created.id).catch((e) => {
|
|
141
|
+
toast.error(errorReason(e, t("integration.credentials.error.oauthConnect")));
|
|
142
|
+
});
|
|
141
143
|
}
|
|
142
144
|
emit("created", created.id);
|
|
143
145
|
emit("update:open", false);
|
|
@@ -7,6 +7,7 @@ import LoadingAnnouncement from "./LoadingAnnouncement.vue";
|
|
|
7
7
|
import { deadLetterStatusClass, deadLetterStatusLabel } from "../../utils/statusStyle";
|
|
8
8
|
import { errorReason } from "../../utils/apiError";
|
|
9
9
|
import { formatDateTime } from "../../utils/formatDateTime";
|
|
10
|
+
import { triggerTypeLabel } from "../../data/integrationNodeCatalog";
|
|
10
11
|
const props = defineProps({
|
|
11
12
|
workflowId: { type: Number, required: true }
|
|
12
13
|
});
|
|
@@ -122,7 +123,7 @@ async function onDiscard(dl) {
|
|
|
122
123
|
<div class="text-sm break-words">{{ dl.reason || t("integration.deadLetters.reason.fallback") }}</div>
|
|
123
124
|
<div class="mt-0.5 flex flex-wrap items-center gap-x-3 gap-y-0.5 text-xs text-muted-foreground font-mono">
|
|
124
125
|
<span>#{{ dl.id }}</span>
|
|
125
|
-
<span v-if="dl.source">{{ dl.source }}</span>
|
|
126
|
+
<span v-if="dl.source">{{ triggerTypeLabel(dl.source) }}</span>
|
|
126
127
|
<span v-if="dl.trigger_handle">↳ {{ dl.trigger_handle }}</span>
|
|
127
128
|
<span>{{ formatDateTime(dl.created_at) }}</span>
|
|
128
129
|
</div>
|
|
@@ -6,6 +6,7 @@ import { ChevronsDownUp, ChevronsUpDown, Ungroup } from "lucide-vue-next";
|
|
|
6
6
|
import { TrashBinMinimalistic as TrashBin } from "#solar-icons";
|
|
7
7
|
import { inputHandleId, outputHandleId } from "../../data/handleId";
|
|
8
8
|
import { resolveDisplayName } from "../../utils/localize";
|
|
9
|
+
import { useCanvasTarget } from "../../composables/useCanvasTarget";
|
|
9
10
|
const props = defineProps({
|
|
10
11
|
id: { type: String, required: true },
|
|
11
12
|
data: { type: Object, required: true },
|
|
@@ -65,6 +66,8 @@ const resizing = ref(false);
|
|
|
65
66
|
const liveW = ref(0);
|
|
66
67
|
const liveH = ref(0);
|
|
67
68
|
let resizeStart = { x: 0, y: 0, w: 0, h: 0 };
|
|
69
|
+
const RESIZE_CORNER = 20;
|
|
70
|
+
const resizeTarget = useCanvasTarget(RESIZE_CORNER);
|
|
68
71
|
const frameW = computed(() => resizing.value ? liveW.value : props.data.rect.width);
|
|
69
72
|
const frameH = computed(() => resizing.value ? liveH.value : props.data.rect.height);
|
|
70
73
|
function onResizeStart(e) {
|
|
@@ -215,18 +218,26 @@ function onResizeEnd() {
|
|
|
215
218
|
<!-- Bottom-right resize handle (editable frames only). -->
|
|
216
219
|
<div
|
|
217
220
|
v-if="editable"
|
|
218
|
-
class="nodrag pointer-events-auto absolute
|
|
219
|
-
:style="{
|
|
221
|
+
class="nodrag pointer-events-auto absolute cursor-se-resize"
|
|
222
|
+
:style="{
|
|
223
|
+
color: color.chip,
|
|
224
|
+
width: `${resizeTarget}px`,
|
|
225
|
+
height: `${resizeTarget}px`,
|
|
226
|
+
right: `${RESIZE_CORNER - resizeTarget}px`,
|
|
227
|
+
bottom: `${RESIZE_CORNER - resizeTarget}px`
|
|
228
|
+
}"
|
|
220
229
|
:title="t('integration.canvas.group.action.resize')"
|
|
221
230
|
@pointerdown="onResizeStart"
|
|
222
231
|
@pointermove="onResizeMove"
|
|
223
232
|
@pointerup="onResizeEnd"
|
|
224
233
|
@pointercancel="onResizeEnd"
|
|
225
234
|
>
|
|
226
|
-
<
|
|
227
|
-
<
|
|
228
|
-
|
|
229
|
-
|
|
235
|
+
<div class="flex size-5 items-end justify-end p-1">
|
|
236
|
+
<svg viewBox="0 0 8 8" class="size-2.5" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round">
|
|
237
|
+
<path d="M7 1 1 7" />
|
|
238
|
+
<path d="M7 4.5 4.5 7" />
|
|
239
|
+
</svg>
|
|
240
|
+
</div>
|
|
230
241
|
</div>
|
|
231
242
|
</div>
|
|
232
243
|
</div>
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { computed, nextTick, ref, watch } from "vue";
|
|
3
3
|
import { isVersionExecutable } from "../../data/integrationNodeCatalog";
|
|
4
|
-
import { ChevronDown } from "lucide-vue-next";
|
|
4
|
+
import { ChevronDown, LoaderCircle } from "lucide-vue-next";
|
|
5
5
|
import { Input } from "@revenexx/studio";
|
|
6
|
+
import { optionalDependencies } from "../../composables/useNodeConfigResolve";
|
|
6
7
|
import { resolveLocalized } from "../../utils/localize";
|
|
8
|
+
import { narrowOptions } from "../../utils/optionSearch";
|
|
7
9
|
const props = defineProps({
|
|
8
10
|
slug: { type: String, required: true },
|
|
9
11
|
version: { type: String, required: true },
|
|
@@ -23,6 +25,7 @@ const q = useNodeConfigResolve({
|
|
|
23
25
|
target: () => props.field.key,
|
|
24
26
|
config: () => props.config,
|
|
25
27
|
dependsOn: () => props.field.dependsOn,
|
|
28
|
+
optional: () => optionalDependencies(props.field.dependsOn, props.siblingFields),
|
|
26
29
|
search: () => focused.value ? search.value : ""
|
|
27
30
|
});
|
|
28
31
|
const unsatisfiedLabels = computed(() => q.unsatisfied.value.map((key) => {
|
|
@@ -72,13 +75,10 @@ function onComboClick() {
|
|
|
72
75
|
listDismissed.value = false;
|
|
73
76
|
}
|
|
74
77
|
const visibleOptions = computed(() => {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const hits = options.value.filter(
|
|
78
|
-
(o) => (resolveLocalized(o.label) || "").toLowerCase().includes(term) || String(o.value).toLowerCase().includes(term)
|
|
79
|
-
);
|
|
80
|
-
return hits.length ? hits : options.value;
|
|
78
|
+
if (!focused.value) return options.value;
|
|
79
|
+
return narrowOptions(options.value, search.value, (o) => resolveLocalized(o.label) || "", (o) => String(o.value));
|
|
81
80
|
});
|
|
81
|
+
const answerPending = computed(() => listOpen.value && q.isPending.value);
|
|
82
82
|
function onComboFocus() {
|
|
83
83
|
search.value = "";
|
|
84
84
|
listDismissed.value = false;
|
|
@@ -88,6 +88,7 @@ function onComboBlur() {
|
|
|
88
88
|
focused.value = false;
|
|
89
89
|
}
|
|
90
90
|
function pick(value) {
|
|
91
|
+
if (answerPending.value) return;
|
|
91
92
|
emit("update:modelValue", value);
|
|
92
93
|
search.value = "";
|
|
93
94
|
focused.value = false;
|
|
@@ -126,8 +127,10 @@ function toggleMulti(optValue) {
|
|
|
126
127
|
<p class="mt-1 text-sm text-amber-600">{{ t("integration.nodeConfig.dynamicField.gated", { keys: unsatisfiedLabels.join(", ") }) }}</p>
|
|
127
128
|
</template>
|
|
128
129
|
|
|
129
|
-
<!-- first load: no
|
|
130
|
-
|
|
130
|
+
<!-- first load: no answer yet. Not "no options": a search that found nothing
|
|
131
|
+
empties the list too, and swapping the field out then would take the focus
|
|
132
|
+
from an operator correcting their term (AC-13) -->
|
|
133
|
+
<template v-else-if="q.isFetching.value && !q.hasData.value">
|
|
131
134
|
<div :class="disabledControlClass">{{ t("integration.nodeConfig.dynamicField.resolving") }}</div>
|
|
132
135
|
</template>
|
|
133
136
|
|
|
@@ -198,26 +201,43 @@ function toggleMulti(optValue) {
|
|
|
198
201
|
@keydown.down="onComboArrowDown"
|
|
199
202
|
@blur="onComboBlur"
|
|
200
203
|
/>
|
|
201
|
-
|
|
204
|
+
<!-- waiting is shown in the field, not as a row of the list: a row that
|
|
205
|
+
comes and goes moves every option under the pointer (AC-14) -->
|
|
206
|
+
<LoaderCircle
|
|
207
|
+
v-if="answerPending"
|
|
208
|
+
:size="14"
|
|
209
|
+
role="status"
|
|
210
|
+
:aria-label="search ? t('integration.nodeConfig.dynamicField.searching') : t('integration.nodeConfig.dynamicField.resolving')"
|
|
211
|
+
class="absolute right-2 top-1/2 -translate-y-1/2 animate-spin text-muted-foreground"
|
|
212
|
+
/>
|
|
213
|
+
<ChevronDown v-else :size="14" class="pointer-events-none absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground" />
|
|
202
214
|
<div
|
|
203
215
|
v-if="listOpen"
|
|
216
|
+
:aria-busy="answerPending"
|
|
204
217
|
class="absolute left-0 right-0 top-full z-30 mt-1 max-h-56 overflow-y-auto rounded-md border border-black/[0.08] bg-white py-1 shadow-md"
|
|
205
218
|
>
|
|
206
|
-
<div v-if="q.isFetching.value" class="px-2.5 py-1.5 text-sm text-muted-foreground">
|
|
207
|
-
{{ search ? t("integration.nodeConfig.dynamicField.searching") : t("integration.nodeConfig.dynamicField.resolving") }}
|
|
208
|
-
</div>
|
|
209
219
|
<button
|
|
210
220
|
v-for="opt in visibleOptions"
|
|
211
221
|
:key="String(opt.value)"
|
|
212
222
|
type="button"
|
|
213
|
-
|
|
214
|
-
|
|
223
|
+
:aria-disabled="answerPending || void 0"
|
|
224
|
+
class="flex w-full items-center px-2.5 py-1.5 text-left text-sm"
|
|
225
|
+
:class="[
|
|
226
|
+
String(opt.value) === currentValue ? 'bg-emerald-500/10 text-emerald-800' : '',
|
|
227
|
+
answerPending ? 'cursor-wait opacity-60' : 'hover:bg-muted/50'
|
|
228
|
+
]"
|
|
215
229
|
@mousedown.prevent="pick(String(opt.value))"
|
|
216
230
|
>
|
|
217
231
|
<span class="min-w-0 truncate">{{ resolveLocalized(opt.label) || String(opt.value) }}</span>
|
|
218
232
|
</button>
|
|
219
|
-
|
|
220
|
-
|
|
233
|
+
<!-- with nothing to show, the one row says why: still asking, or nothing matches -->
|
|
234
|
+
<div v-if="visibleOptions.length === 0" class="px-2.5 py-1.5 text-sm text-muted-foreground">
|
|
235
|
+
<template v-if="answerPending">
|
|
236
|
+
{{ search ? t("integration.nodeConfig.dynamicField.searching") : t("integration.nodeConfig.dynamicField.resolving") }}
|
|
237
|
+
</template>
|
|
238
|
+
<template v-else>
|
|
239
|
+
{{ search.trim() ? t("integration.nodeConfig.dynamicField.noMatch", { query: search.trim() }) : t("integration.nodeConfig.dynamicField.empty") }}
|
|
240
|
+
</template>
|
|
221
241
|
</div>
|
|
222
242
|
</div>
|
|
223
243
|
</div>
|
|
@@ -7,6 +7,8 @@ type __VLS_Props = {
|
|
|
7
7
|
staticKeys: string[];
|
|
8
8
|
secretKeys: string[];
|
|
9
9
|
depth?: number;
|
|
10
|
+
/** The settings beside this one, so an optional `dependsOn` does not gate (IN-80). */
|
|
11
|
+
siblingFields?: ConfigField[];
|
|
10
12
|
};
|
|
11
13
|
declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
12
14
|
set: (key: string, value: unknown) => any;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { computed } from "vue";
|
|
3
3
|
import { Button } from "@revenexx/studio";
|
|
4
4
|
import { IntegrationsApiError } from "../../utils/apiError";
|
|
5
|
+
import { optionalDependencies } from "../../composables/useNodeConfigResolve";
|
|
5
6
|
import { resolveLocalized } from "../../utils/localize";
|
|
6
7
|
import { applyingFields, staleKeys } from "../../utils/settingCondition";
|
|
7
8
|
const props = defineProps({
|
|
@@ -11,7 +12,8 @@ const props = defineProps({
|
|
|
11
12
|
config: { type: Object, required: true },
|
|
12
13
|
staticKeys: { type: Array, required: true },
|
|
13
14
|
secretKeys: { type: Array, required: true },
|
|
14
|
-
depth: { type: Number, required: false }
|
|
15
|
+
depth: { type: Number, required: false },
|
|
16
|
+
siblingFields: { type: Array, required: false }
|
|
15
17
|
});
|
|
16
18
|
const emit = defineEmits(["set"]);
|
|
17
19
|
const t = useIntegrationsT();
|
|
@@ -22,7 +24,8 @@ const q = useNodeConfigResolve({
|
|
|
22
24
|
version: () => props.version,
|
|
23
25
|
target: () => "*",
|
|
24
26
|
config: () => props.config,
|
|
25
|
-
dependsOn: () => props.field.dependsOn
|
|
27
|
+
dependsOn: () => props.field.dependsOn,
|
|
28
|
+
optional: () => optionalDependencies(props.field.dependsOn, props.siblingFields)
|
|
26
29
|
});
|
|
27
30
|
const resolvedFields = computed(() => q.data.value?.fields ?? []);
|
|
28
31
|
const applyingResolvedFields = computed(() => applyingFields(resolvedFields.value, props.config));
|
|
@@ -7,6 +7,8 @@ type __VLS_Props = {
|
|
|
7
7
|
staticKeys: string[];
|
|
8
8
|
secretKeys: string[];
|
|
9
9
|
depth?: number;
|
|
10
|
+
/** The settings beside this one, so an optional `dependsOn` does not gate (IN-80). */
|
|
11
|
+
siblingFields?: ConfigField[];
|
|
10
12
|
};
|
|
11
13
|
declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
12
14
|
set: (key: string, value: unknown) => any;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { RunOutput } from '../../utils/runOutputs.js';
|
|
2
|
+
import { type RunFailure } from '../../utils/runFailure.js';
|
|
2
3
|
/**
|
|
3
4
|
* Result surface for a manual run, sync and async alike.
|
|
4
5
|
*
|
|
@@ -44,6 +45,11 @@ type __VLS_Props = {
|
|
|
44
45
|
* which has the status code; `error` alone cannot tell the two apart.
|
|
45
46
|
*/
|
|
46
47
|
refused?: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Why the run failed — the step, named as the canvas names it, and the cause —
|
|
50
|
+
* as far as the page has found out. Read only for a run that failed (PO-690).
|
|
51
|
+
*/
|
|
52
|
+
failure?: RunFailure | null;
|
|
47
53
|
};
|
|
48
54
|
declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
49
55
|
"update:open": (args_0: boolean) => any;
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
import { formatDuration } from "../../utils/formatDuration";
|
|
13
13
|
import { runStatusClass, runStatusLabel } from "../../utils/statusStyle";
|
|
14
14
|
import { isTerminalRun } from "../../utils/runEnvelope";
|
|
15
|
+
import { isFailedRun } from "../../utils/runFailure";
|
|
15
16
|
const props = defineProps({
|
|
16
17
|
open: { type: Boolean, required: true },
|
|
17
18
|
loading: { type: Boolean, required: false },
|
|
@@ -22,7 +23,8 @@ const props = defineProps({
|
|
|
22
23
|
following: { type: Boolean, required: false },
|
|
23
24
|
error: { type: [String, null], required: false },
|
|
24
25
|
runId: { type: [String, null], required: false },
|
|
25
|
-
refused: { type: Boolean, required: false }
|
|
26
|
+
refused: { type: Boolean, required: false },
|
|
27
|
+
failure: { type: [Object, null], required: false }
|
|
26
28
|
});
|
|
27
29
|
const emit = defineEmits(["update:open", "view-run"]);
|
|
28
30
|
const t = useIntegrationsT();
|
|
@@ -40,6 +42,8 @@ const ended = computed(() => {
|
|
|
40
42
|
if (props.mode !== "async") return true;
|
|
41
43
|
return isTerminalRun(props.run);
|
|
42
44
|
});
|
|
45
|
+
const failed = computed(() => ended.value && isFailedRun(props.run));
|
|
46
|
+
const cancelled = computed(() => ["cancelled", "canceled"].includes(status.value.toLowerCase()));
|
|
43
47
|
const returnedNothing = computed(() => props.result == null || typeof props.result === "object" && !Array.isArray(props.result) && !Object.keys(props.result).length);
|
|
44
48
|
const refused = computed(() => !props.loading && !!props.refused);
|
|
45
49
|
const unconfirmed = computed(() => !props.loading && !refused.value && props.mode === "async" && !!props.error && !runNumber.value);
|
|
@@ -94,8 +98,16 @@ function pretty(value) {
|
|
|
94
98
|
>
|
|
95
99
|
{{ error }}
|
|
96
100
|
</DialogDescription>
|
|
97
|
-
<div v-else class="rounded-md border border-rose-500/20 bg-rose-500/5 px-3 py-2 text-rose-700">
|
|
98
|
-
{{ error }}
|
|
101
|
+
<div v-else role="alert" class="rounded-md border border-rose-500/20 bg-rose-500/5 px-3 py-2 text-rose-700">
|
|
102
|
+
<p>{{ error }}</p>
|
|
103
|
+
<!-- A waiting run the engine failed ships the run, so it exists and has
|
|
104
|
+
a failed step and a reason like any other (PO-690). -->
|
|
105
|
+
<p v-if="runId && failure?.step" class="mt-1">
|
|
106
|
+
{{ t("integration.runs.response.failure.step", { step: failure.step }) }}
|
|
107
|
+
</p>
|
|
108
|
+
<p v-if="runId && failure?.cause" class="mt-1 whitespace-pre-wrap break-words">
|
|
109
|
+
{{ failure.cause }}
|
|
110
|
+
</p>
|
|
99
111
|
</div>
|
|
100
112
|
<div v-if="runId" class="mt-3 flex justify-end">
|
|
101
113
|
<Button class="h-8 text-sm" @click="emit('view-run', runId)">
|
|
@@ -107,8 +119,25 @@ function pretty(value) {
|
|
|
107
119
|
<template v-else>
|
|
108
120
|
<!-- The confirmation an async press earns: the run exists and is named,
|
|
109
121
|
whatever the studio manages to read back about it afterwards. -->
|
|
122
|
+
<!-- A run that failed says so in place of the confirmation, rather than
|
|
123
|
+
beside it: green "started" above red "failed" is two answers (PO-690). -->
|
|
124
|
+
<div
|
|
125
|
+
v-if="failed"
|
|
126
|
+
role="alert"
|
|
127
|
+
class="rounded-md border border-rose-500/20 bg-rose-500/5 px-3 py-2 text-rose-700"
|
|
128
|
+
>
|
|
129
|
+
<p class="font-medium">
|
|
130
|
+
{{ runNumber ? t("integration.runs.response.failure.runWithId", { id: runNumber }) : t("integration.runs.response.failure.run") }}
|
|
131
|
+
</p>
|
|
132
|
+
<p v-if="failure?.step" class="mt-1">
|
|
133
|
+
{{ t("integration.runs.response.failure.step", { step: failure.step }) }}
|
|
134
|
+
</p>
|
|
135
|
+
<p v-if="failure?.cause" class="mt-1 whitespace-pre-wrap break-words">
|
|
136
|
+
{{ failure.cause }}
|
|
137
|
+
</p>
|
|
138
|
+
</div>
|
|
110
139
|
<div
|
|
111
|
-
v-if="mode === 'async'"
|
|
140
|
+
v-else-if="mode === 'async'"
|
|
112
141
|
class="flex items-center gap-2 rounded-md border border-emerald-500/20 bg-emerald-500/5 px-3 py-2 text-emerald-700"
|
|
113
142
|
>
|
|
114
143
|
<CheckCircle2 :size="15" class="shrink-0" />
|
|
@@ -148,7 +177,9 @@ function pretty(value) {
|
|
|
148
177
|
<!-- Nothing at all where the run has not ended: the studio has stopped
|
|
149
178
|
looking, which is not the same as the run having finished, and the
|
|
150
179
|
confirmation and the status line above already say where it stands. -->
|
|
151
|
-
|
|
180
|
+
<!-- A failed run has said why above; "returned nothing" under it would read
|
|
181
|
+
as the explanation, and a `null` result printed whole as the answer. -->
|
|
182
|
+
<template v-else-if="ended && !failed && !cancelled">
|
|
152
183
|
<p v-if="returnedNothing" class="mt-3 py-3 text-muted-foreground">
|
|
153
184
|
{{ t("integration.runs.response.nothingReturned") }}
|
|
154
185
|
</p>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { RunOutput } from '../../utils/runOutputs.js';
|
|
2
|
+
import { type RunFailure } from '../../utils/runFailure.js';
|
|
2
3
|
/**
|
|
3
4
|
* Result surface for a manual run, sync and async alike.
|
|
4
5
|
*
|
|
@@ -44,6 +45,11 @@ type __VLS_Props = {
|
|
|
44
45
|
* which has the status code; `error` alone cannot tell the two apart.
|
|
45
46
|
*/
|
|
46
47
|
refused?: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Why the run failed — the step, named as the canvas names it, and the cause —
|
|
50
|
+
* as far as the page has found out. Read only for a run that failed (PO-690).
|
|
51
|
+
*/
|
|
52
|
+
failure?: RunFailure | null;
|
|
47
53
|
};
|
|
48
54
|
declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
49
55
|
"update:open": (args_0: boolean) => any;
|
|
@@ -1,12 +1,29 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
+
import { computed, onMounted, watch } from "vue";
|
|
2
3
|
import { Pulse as Activity, ClockCircle as Clock, Home, Key as KeyRound, Layers, ShieldCheck, ShieldKeyhole as Credentials, Programming as Workflow } from "#solar-icons";
|
|
3
4
|
import SidebarNav from "@revenexx/studio-shared/components/Shared/SidebarNav.vue";
|
|
4
5
|
const t = useIntegrationsT();
|
|
6
|
+
const route = useRoute();
|
|
7
|
+
const { summary, load: loadSummary } = useIntegrationsSummary();
|
|
8
|
+
onMounted(loadSummary);
|
|
9
|
+
watch(() => route.path, () => {
|
|
10
|
+
void loadSummary();
|
|
11
|
+
});
|
|
5
12
|
const { workflows } = useIntegrationsWorkflows();
|
|
6
13
|
const { runningCount } = useIntegrationsRuns();
|
|
7
14
|
const { activeCount: schedulesActive } = useIntegrationsSchedules();
|
|
8
15
|
const { total: credentialsTotal } = useIntegrationsCredentials();
|
|
9
16
|
const { total: secretsTotal } = useIntegrationsSecrets();
|
|
17
|
+
const { tenant } = useIntegrationsApi();
|
|
18
|
+
watch(
|
|
19
|
+
() => [tenant.value, workflows.value.length, runningCount.value, schedulesActive.value, credentialsTotal.value, secretsTotal.value],
|
|
20
|
+
() => {
|
|
21
|
+
void loadSummary();
|
|
22
|
+
}
|
|
23
|
+
);
|
|
24
|
+
function badge(count) {
|
|
25
|
+
return count || null;
|
|
26
|
+
}
|
|
10
27
|
const sections = computed(() => [
|
|
11
28
|
{
|
|
12
29
|
key: "pinned",
|
|
@@ -21,12 +38,12 @@ const sections = computed(() => [
|
|
|
21
38
|
// translator will look for it rather than only in a review comment.
|
|
22
39
|
label: t("integration.nav.section.integrations"),
|
|
23
40
|
items: [
|
|
24
|
-
{ label: t("integration.nav.item.workflows"), to: "/integrations/workflows", icon: Workflow, badge:
|
|
41
|
+
{ label: t("integration.nav.item.workflows"), to: "/integrations/workflows", icon: Workflow, badge: badge(summary.value?.workflows) },
|
|
25
42
|
{ label: t("integration.nav.item.templates"), to: "/integrations/templates", icon: Layers },
|
|
26
|
-
{ label: t("integration.nav.item.runs"), to: "/integrations/runs", icon: Activity, badge:
|
|
27
|
-
{ label: t("integration.nav.item.schedules"), to: "/integrations/schedules", icon: Clock, badge:
|
|
28
|
-
{ label: t("integration.nav.item.credentials"), to: "/integrations/credentials", icon: Credentials, badge:
|
|
29
|
-
{ label: t("integration.nav.item.secrets"), to: "/integrations/secrets", icon: KeyRound, badge:
|
|
43
|
+
{ label: t("integration.nav.item.runs"), to: "/integrations/runs", icon: Activity, badge: badge(summary.value?.runsInFlight) },
|
|
44
|
+
{ label: t("integration.nav.item.schedules"), to: "/integrations/schedules", icon: Clock, badge: badge(summary.value?.schedulesActive) },
|
|
45
|
+
{ label: t("integration.nav.item.credentials"), to: "/integrations/credentials", icon: Credentials, badge: badge(summary.value?.credentials) },
|
|
46
|
+
{ label: t("integration.nav.item.secrets"), to: "/integrations/secrets", icon: KeyRound, badge: badge(summary.value?.secrets) },
|
|
30
47
|
{ label: t("integration.nav.item.audit"), to: "/integrations/audit", icon: ShieldCheck }
|
|
31
48
|
]
|
|
32
49
|
}
|
|
@@ -3,6 +3,7 @@ import { computed, nextTick, ref, watch } from "vue";
|
|
|
3
3
|
import { Position, useVueFlow } from "@vue-flow/core";
|
|
4
4
|
import { NodeToolbar } from "@vue-flow/node-toolbar";
|
|
5
5
|
import { TrashBinMinimalistic as TrashBin } from "#solar-icons";
|
|
6
|
+
import { useCanvasTarget } from "../../composables/useCanvasTarget";
|
|
6
7
|
const props = defineProps({
|
|
7
8
|
id: { type: String, required: true },
|
|
8
9
|
data: { type: Object, required: true },
|
|
@@ -34,6 +35,8 @@ const MIN_HEIGHT = 80;
|
|
|
34
35
|
const width = ref(props.data.sticky.size?.width ?? DEFAULT_WIDTH);
|
|
35
36
|
const height = ref(props.data.sticky.size?.height ?? DEFAULT_HEIGHT);
|
|
36
37
|
const resizing = ref(false);
|
|
38
|
+
const RESIZE_CORNER = 16;
|
|
39
|
+
const resizeTarget = useCanvasTarget(RESIZE_CORNER);
|
|
37
40
|
watch(() => props.data.sticky.size, (s) => {
|
|
38
41
|
if (resizing.value) return;
|
|
39
42
|
width.value = s?.width ?? DEFAULT_WIDTH;
|
|
@@ -191,7 +194,7 @@ function onViewMouseDown(e) {
|
|
|
191
194
|
|
|
192
195
|
<!-- Card -->
|
|
193
196
|
<div
|
|
194
|
-
class="relative flex flex-col
|
|
197
|
+
class="relative flex flex-col rounded-lg border text-sm leading-snug text-stone-700 shadow-sm"
|
|
195
198
|
:class="[
|
|
196
199
|
selected ? 'ring-2 ring-emerald-400/60' : '',
|
|
197
200
|
editing ? '' : 'cursor-grab active:cursor-grabbing'
|
|
@@ -209,7 +212,7 @@ function onViewMouseDown(e) {
|
|
|
209
212
|
<!-- View mode -->
|
|
210
213
|
<div
|
|
211
214
|
v-if="!editing"
|
|
212
|
-
class="h-full w-full overflow-hidden whitespace-pre-wrap break-words px-3 py-2"
|
|
215
|
+
class="h-full w-full overflow-hidden rounded-[inherit] whitespace-pre-wrap break-words px-3 py-2"
|
|
213
216
|
@mousedown="onViewMouseDown"
|
|
214
217
|
>
|
|
215
218
|
<!-- eslint-disable-next-line vue/no-v-html -- renderMarkdownLite escapes the note before it adds a tag -->
|
|
@@ -228,7 +231,7 @@ function onViewMouseDown(e) {
|
|
|
228
231
|
ref="textareaEl"
|
|
229
232
|
v-model="draft"
|
|
230
233
|
:aria-label="t('integration.canvas.sticky.textarea.aria')"
|
|
231
|
-
class="nodrag nowheel h-full w-full resize-none bg-transparent px-3 py-2 text-sm leading-snug text-stone-700 outline-none placeholder:text-stone-400 focus:ring-2 focus:ring-inset focus:ring-stone-500/40"
|
|
234
|
+
class="nodrag nowheel h-full w-full resize-none rounded-[inherit] bg-transparent px-3 py-2 text-sm leading-snug text-stone-700 outline-none placeholder:text-stone-400 focus:ring-2 focus:ring-inset focus:ring-stone-500/40"
|
|
232
235
|
:placeholder="t('integration.canvas.sticky.textarea.placeholder')"
|
|
233
236
|
spellcheck="false"
|
|
234
237
|
@blur="commitEdit"
|
|
@@ -239,20 +242,30 @@ function onViewMouseDown(e) {
|
|
|
239
242
|
|
|
240
243
|
<!-- Bottom-right resize handle (hand-rolled; min 150x80). Gone on a
|
|
241
244
|
read-only canvas: a handle that cannot resize is the same lie as a bin
|
|
242
|
-
that cannot delete.
|
|
245
|
+
that cannot delete. The hit area reaches past the note's corner (see
|
|
246
|
+
`resizeTarget`) — which is why the card no longer clips its overflow —
|
|
247
|
+
and the drawn corner sits in its top-left, exactly where it always sat. -->
|
|
243
248
|
<div
|
|
244
249
|
v-if="editable"
|
|
245
|
-
class="nodrag absolute
|
|
250
|
+
class="nodrag absolute cursor-se-resize"
|
|
251
|
+
:style="{
|
|
252
|
+
width: `${resizeTarget}px`,
|
|
253
|
+
height: `${resizeTarget}px`,
|
|
254
|
+
right: `${RESIZE_CORNER - resizeTarget}px`,
|
|
255
|
+
bottom: `${RESIZE_CORNER - resizeTarget}px`
|
|
256
|
+
}"
|
|
246
257
|
:title="t('integration.canvas.sticky.action.resize')"
|
|
247
258
|
@pointerdown="onResizeStart"
|
|
248
259
|
@pointermove="onResizeMove"
|
|
249
260
|
@pointerup="onResizeEnd"
|
|
250
261
|
@pointercancel="onResizeEnd"
|
|
251
262
|
>
|
|
252
|
-
<
|
|
253
|
-
<
|
|
254
|
-
|
|
255
|
-
|
|
263
|
+
<div class="flex size-4 items-end justify-end p-0.5 text-stone-400/80">
|
|
264
|
+
<svg viewBox="0 0 8 8" class="size-2.5" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round">
|
|
265
|
+
<path d="M7 1 1 7" />
|
|
266
|
+
<path d="M7 4.5 4.5 7" />
|
|
267
|
+
</svg>
|
|
268
|
+
</div>
|
|
256
269
|
</div>
|
|
257
270
|
</div>
|
|
258
271
|
</template>
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { computed, onMounted } from "vue";
|
|
3
3
|
import { nodeIcon } from "../../data/nodeIcons";
|
|
4
4
|
import { triggerGlyph } from "../../data/triggerGlyphs";
|
|
5
|
-
import { findManifest, NODE_CATEGORIES, TRIGGER_TEMPLATES } from "../../data/integrationNodeCatalog";
|
|
5
|
+
import { findManifest, NODE_CATEGORIES, TRIGGER_TEMPLATES, triggerTypeLabel } from "../../data/integrationNodeCatalog";
|
|
6
6
|
import { resolveLocalized } from "../../utils/localize";
|
|
7
7
|
import { describeCron } from "../../utils/schedule";
|
|
8
8
|
import IntegrationsNodeMark from "./NodeMark.vue";
|
|
@@ -38,7 +38,7 @@ const boxes = computed(() => {
|
|
|
38
38
|
y: pos.y,
|
|
39
39
|
w: TRIG_W,
|
|
40
40
|
h: TRIG_H,
|
|
41
|
-
label: typeof t2.name === "string" && t2.name ||
|
|
41
|
+
label: typeof t2.name === "string" && t2.name || triggerTypeLabel(t2.type),
|
|
42
42
|
subtitle,
|
|
43
43
|
type: t2.type,
|
|
44
44
|
manifest: null,
|
|
@@ -175,7 +175,7 @@ const bounds = computed(() => {
|
|
|
175
175
|
</div>
|
|
176
176
|
</div>
|
|
177
177
|
<div class="rounded-b-md border-t border-emerald-500/15 bg-emerald-500/[0.04] px-3 py-1 text-mono font-semibold text-emerald-700/80">
|
|
178
|
-
{{ t("integration.templates.preview.triggerLabel") }} · {{ b.type }}
|
|
178
|
+
{{ t("integration.templates.preview.triggerLabel") }} · {{ triggerTypeLabel(b.type) }}
|
|
179
179
|
</div>
|
|
180
180
|
</div>
|
|
181
181
|
|
|
@@ -92,6 +92,7 @@ function onHoverLeave() {
|
|
|
92
92
|
}, 200);
|
|
93
93
|
}
|
|
94
94
|
const toolsVisible = computed(() => edgeActions !== null && !props.data?.replay && (hovered.value || isSelected.value));
|
|
95
|
+
const unzoom = computed(() => 1 / (flow.viewport.value.zoom || 1));
|
|
95
96
|
function removeEdge() {
|
|
96
97
|
flow.removeEdges([props.id]);
|
|
97
98
|
}
|
|
@@ -128,7 +129,7 @@ function spliceNode() {
|
|
|
128
129
|
<div
|
|
129
130
|
class="absolute pointer-events-none select-none rounded-md border px-1.5 py-0.5 text-mono shadow-sm"
|
|
130
131
|
:style="{
|
|
131
|
-
transform: `translate(-50%, -50%) translate(${path[1]}px, ${path[2] - (toolsVisible ? 16 : 0)}px)`,
|
|
132
|
+
transform: `translate(-50%, -50%) translate(${path[1]}px, ${path[2] - (toolsVisible ? 16 * unzoom : 0)}px)`,
|
|
132
133
|
color: color.label,
|
|
133
134
|
background: color.bg,
|
|
134
135
|
borderColor: color.stroke + '55'
|
|
@@ -142,7 +143,7 @@ function spliceNode() {
|
|
|
142
143
|
<EdgeLabelRenderer v-if="toolsVisible">
|
|
143
144
|
<div
|
|
144
145
|
class="absolute z-20 flex items-center gap-0.5 rounded-md border border-black/[0.08] bg-white p-0.5 shadow-md"
|
|
145
|
-
:style="{ transform: `translate(-50%, -50%) translate(${path[1]}px, ${path[2] + (props.data?.label ? 14 : 0)}px)`, pointerEvents: 'all' }"
|
|
146
|
+
:style="{ transform: `translate(-50%, -50%) translate(${path[1]}px, ${path[2] + (props.data?.label ? 14 * unzoom : 0)}px) scale(${unzoom})`, pointerEvents: 'all' }"
|
|
146
147
|
@mouseenter="onHoverEnter"
|
|
147
148
|
@mouseleave="onHoverLeave"
|
|
148
149
|
>
|