@revenexx/studio-integrations 1.4.1 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +297 -0
- package/README.md +2 -1
- package/dist/module.json +1 -1
- package/dist/runtime/components/Integrations/CredentialCreateDialog.vue +6 -6
- package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +134 -131
- package/dist/runtime/components/Integrations/CredentialLogo.vue +1 -1
- package/dist/runtime/components/Integrations/CredentialRevenexxSource.vue +1 -1
- package/dist/runtime/components/Integrations/CredentialTypePicker.vue +6 -6
- package/dist/runtime/components/Integrations/ExpressionInput.d.vue.ts +2 -0
- package/dist/runtime/components/Integrations/ExpressionInput.vue +90 -11
- package/dist/runtime/components/Integrations/ExpressionInput.vue.d.ts +2 -0
- package/dist/runtime/components/Integrations/GroupFrameView.vue +121 -121
- package/dist/runtime/components/Integrations/NodeConfigField.vue +20 -5
- package/dist/runtime/components/Integrations/NodeInspector.vue +44 -41
- package/dist/runtime/components/Integrations/NodePalette.vue +52 -37
- package/dist/runtime/components/Integrations/RunActionsMenu.vue +6 -5
- package/dist/runtime/components/Integrations/RunDetailPanel.d.vue.ts +7 -0
- package/dist/runtime/components/Integrations/RunDetailPanel.vue +14 -2
- package/dist/runtime/components/Integrations/RunDetailPanel.vue.d.ts +7 -0
- package/dist/runtime/components/Integrations/RunsTable.vue +72 -70
- package/dist/runtime/components/Integrations/SftpAccountImportDialog.vue +20 -6
- package/dist/runtime/components/Integrations/StickyNoteView.vue +1 -0
- package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +10 -6
- package/dist/runtime/components/Integrations/WorkflowEditor.d.vue.ts +0 -34
- package/dist/runtime/components/Integrations/WorkflowEditor.vue +226 -277
- package/dist/runtime/components/Integrations/WorkflowEditor.vue.d.ts +0 -34
- package/dist/runtime/components/Integrations/WorkflowExportDialog.d.vue.ts +6 -0
- package/dist/runtime/components/Integrations/WorkflowExportDialog.vue +71 -0
- package/dist/runtime/components/Integrations/WorkflowExportDialog.vue.d.ts +6 -0
- package/dist/runtime/components/Integrations/WorkflowImportFileStep.d.vue.ts +8 -0
- package/dist/runtime/components/Integrations/WorkflowImportFileStep.vue +95 -0
- package/dist/runtime/components/Integrations/WorkflowImportFileStep.vue.d.ts +8 -0
- package/dist/runtime/components/Integrations/WorkflowNodeView.vue +5 -4
- package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +0 -7
- package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +2 -2
- package/dist/runtime/components/Integrations/WorkflowVariablesDialog.vue +2 -3
- package/dist/runtime/components/Integrations/WorkflowsTable.vue +14 -8
- package/dist/runtime/composables/useIntegrationsNodes.js +4 -1
- package/dist/runtime/composables/useIntegrationsWorkflows.d.ts +3 -1
- package/dist/runtime/composables/useIntegrationsWorkflows.js +5 -22
- package/dist/runtime/composables/useNow.d.ts +17 -0
- package/dist/runtime/composables/useNow.js +16 -0
- package/dist/runtime/composables/useWorkflowFile.d.ts +21 -0
- package/dist/runtime/composables/useWorkflowFile.js +63 -0
- package/dist/runtime/data/expressionEditor.d.ts +11 -1
- package/dist/runtime/data/expressionEditor.js +2 -1
- package/dist/runtime/data/nodeIcons.d.ts +14 -0
- package/dist/runtime/data/nodeIcons.js +57 -14
- package/dist/runtime/data/nodeRunData.js +1 -1
- package/dist/runtime/data/payloadForm.js +1 -1
- package/dist/runtime/i18n/messages/de.json +47 -11
- package/dist/runtime/i18n/messages/en.json +47 -11
- package/dist/runtime/pages/integrations/audit.vue +3 -3
- package/dist/runtime/pages/integrations/credentials/oauth-complete.vue +2 -2
- package/dist/runtime/pages/integrations/credentials.vue +17 -5
- package/dist/runtime/pages/integrations/index.vue +5 -10
- package/dist/runtime/pages/integrations/schedules.vue +24 -13
- package/dist/runtime/pages/integrations/templates/index.vue +40 -40
- package/dist/runtime/pages/integrations/workflows/[id].vue +324 -324
- package/dist/runtime/types/integrations.d.ts +13 -0
- package/dist/runtime/utils/cockpitSources.d.ts +68 -1
- package/dist/runtime/utils/credentialState.d.ts +15 -7
- package/dist/runtime/utils/credentialState.js +4 -12
- package/dist/runtime/utils/localize.d.ts +12 -0
- package/dist/runtime/utils/localize.js +1 -1
- package/dist/runtime/utils/paletteOrder.d.ts +29 -0
- package/dist/runtime/utils/paletteOrder.js +46 -0
- package/dist/runtime/utils/relativeTime.d.ts +29 -0
- package/dist/runtime/utils/relativeTime.js +23 -0
- package/dist/runtime/utils/runFeedQuery.d.ts +12 -6
- package/dist/runtime/utils/runFeedQuery.js +3 -18
- package/dist/runtime/utils/settingValue.d.ts +33 -0
- package/dist/runtime/utils/settingValue.js +8 -0
- package/dist/runtime/utils/statusStyle.d.ts +8 -8
- package/dist/runtime/utils/statusStyle.js +4 -1
- package/dist/runtime/utils/workflowFile.d.ts +119 -0
- package/dist/runtime/utils/workflowFile.js +192 -0
- package/package.json +10 -5
- package/dist/runtime/utils/errorPathAdvice.d.ts +0 -60
- package/dist/runtime/utils/errorPathAdvice.js +0 -19
|
@@ -84,9 +84,10 @@ async function runAction(a) {
|
|
|
84
84
|
busy.value = false;
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
|
-
function
|
|
88
|
-
|
|
89
|
-
|
|
87
|
+
function confirmStop(a) {
|
|
88
|
+
const question = a === "cancel" ? t("integration.runs.toast.cancel.confirm", { id: props.run.id }) : t("integration.runs.toast.terminate.confirm", { id: props.run.id });
|
|
89
|
+
toast(question, {
|
|
90
|
+
action: { label: actionLabel(a), onClick: () => void runAction(a) }
|
|
90
91
|
});
|
|
91
92
|
}
|
|
92
93
|
</script>
|
|
@@ -104,8 +105,8 @@ function confirmTerminate() {
|
|
|
104
105
|
<DropdownMenuItem @select="viewDetails">{{ t("integration.runs.menu.viewDetails") }}</DropdownMenuItem>
|
|
105
106
|
<DropdownMenuItem @select="viewReplay">{{ t("integration.runs.menu.replay") }}</DropdownMenuItem>
|
|
106
107
|
<DropdownMenuSeparator v-if="hasAny" />
|
|
107
|
-
<DropdownMenuItem v-if="isActive" @select="
|
|
108
|
-
<DropdownMenuItem v-if="isActive" class="text-rose-600" @select="
|
|
108
|
+
<DropdownMenuItem v-if="isActive" @select="confirmStop('cancel')">{{ actionLabel("cancel") }}</DropdownMenuItem>
|
|
109
|
+
<DropdownMenuItem v-if="isActive" class="text-rose-600" @select="confirmStop('terminate')">{{ actionLabel("terminate") }}</DropdownMenuItem>
|
|
109
110
|
<DropdownMenuSeparator v-if="isActive && (canResume || isTerminal)" />
|
|
110
111
|
<DropdownMenuItem v-if="canResume" @select="runAction('resume')">{{ actionLabel("resume") }}</DropdownMenuItem>
|
|
111
112
|
<DropdownMenuItem v-if="isTerminal" @select="runAction('retry')">{{ actionLabel("retry") }}</DropdownMenuItem>
|
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
import type { WorkflowRun } from '../../types/integrations.js';
|
|
2
|
+
import type { OutputNode } from '../../utils/runOutputs.js';
|
|
2
3
|
type __VLS_Props = {
|
|
3
4
|
run: WorkflowRun;
|
|
4
5
|
/** Which surface renders: the overview or the steps+history timeline. */
|
|
5
6
|
view?: 'details' | 'timeline';
|
|
7
|
+
/**
|
|
8
|
+
* The workflow's nodes, which the result is split against and named by. The
|
|
9
|
+
* published blob's, as the Replay view next to this one draws them.
|
|
10
|
+
*/
|
|
11
|
+
nodes?: readonly OutputNode[];
|
|
6
12
|
};
|
|
7
13
|
declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
8
14
|
changed: () => any;
|
|
9
15
|
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
10
16
|
onChanged?: (() => any) | undefined;
|
|
11
17
|
}>, {
|
|
18
|
+
nodes: readonly OutputNode[];
|
|
12
19
|
view: "details" | "timeline";
|
|
13
20
|
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
14
21
|
export default _default;
|
|
@@ -7,9 +7,11 @@ import { IntegrationsApiError, errorReason } from "../../utils/apiError";
|
|
|
7
7
|
import { createPollBudget, SAFETY_POLL_MS } from "../../utils/pollBudget";
|
|
8
8
|
import { formatDuration } from "../../utils/formatDuration";
|
|
9
9
|
import { unwrapRun } from "../../utils/runEnvelope";
|
|
10
|
+
import { splitRunOutputs } from "../../utils/runOutputs";
|
|
10
11
|
const props = defineProps({
|
|
11
12
|
run: { type: Object, required: true },
|
|
12
|
-
view: { type: String, required: false, default: "details" }
|
|
13
|
+
view: { type: String, required: false, default: "details" },
|
|
14
|
+
nodes: { type: Array, required: false, default: () => [] }
|
|
13
15
|
});
|
|
14
16
|
const emit = defineEmits(["changed"]);
|
|
15
17
|
const t = useIntegrationsT();
|
|
@@ -83,6 +85,7 @@ const temporalWorkflowId = computed(() => pickStr(runData.value, ["temporal_work
|
|
|
83
85
|
const temporalRunId = computed(() => pickStr(runData.value, ["temporal_run_id", "temporalRunId"]) ?? props.run.temporalRunId ?? null);
|
|
84
86
|
const hasTemporal = computed(() => !!temporalWorkflowId.value && !!temporalRunId.value);
|
|
85
87
|
const runResult = computed(() => pick(runData.value, ["result"]));
|
|
88
|
+
const resultOutputs = computed(() => splitRunOutputs(runResult.value, props.nodes));
|
|
86
89
|
const MAX_RESULT_LINKS = 50;
|
|
87
90
|
const resultLinks = computed(() => {
|
|
88
91
|
const out = [];
|
|
@@ -685,7 +688,16 @@ async function confirmAction() {
|
|
|
685
688
|
{{ link.label }}
|
|
686
689
|
</a>
|
|
687
690
|
</div>
|
|
688
|
-
|
|
691
|
+
<!-- One block per step, each clamped and scrolling on its own (AC-3). -->
|
|
692
|
+
<div v-if="resultOutputs?.length" class="space-y-3">
|
|
693
|
+
<div v-for="out in resultOutputs" :key="out.nodeId" class="min-w-0">
|
|
694
|
+
<h5 class="mb-1 text-xs font-medium text-foreground">
|
|
695
|
+
{{ out.name }}
|
|
696
|
+
</h5>
|
|
697
|
+
<pre class="rounded-md bg-muted/40 p-3 text-xs overflow-auto max-h-[28rem]">{{ json(out.value) }}</pre>
|
|
698
|
+
</div>
|
|
699
|
+
</div>
|
|
700
|
+
<pre v-else class="rounded-md bg-muted/40 p-3 text-xs overflow-x-auto max-h-[28rem]">{{ json(runResult) }}</pre>
|
|
689
701
|
</section>
|
|
690
702
|
|
|
691
703
|
<!-- Stack Trace (non-terminal runs only, lazy) -->
|
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
import type { WorkflowRun } from '../../types/integrations.js';
|
|
2
|
+
import type { OutputNode } from '../../utils/runOutputs.js';
|
|
2
3
|
type __VLS_Props = {
|
|
3
4
|
run: WorkflowRun;
|
|
4
5
|
/** Which surface renders: the overview or the steps+history timeline. */
|
|
5
6
|
view?: 'details' | 'timeline';
|
|
7
|
+
/**
|
|
8
|
+
* The workflow's nodes, which the result is split against and named by. The
|
|
9
|
+
* published blob's, as the Replay view next to this one draws them.
|
|
10
|
+
*/
|
|
11
|
+
nodes?: readonly OutputNode[];
|
|
6
12
|
};
|
|
7
13
|
declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
8
14
|
changed: () => any;
|
|
9
15
|
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
10
16
|
onChanged?: (() => any) | undefined;
|
|
11
17
|
}>, {
|
|
18
|
+
nodes: readonly OutputNode[];
|
|
12
19
|
view: "details" | "timeline";
|
|
13
20
|
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
14
21
|
export default _default;
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import { computed,
|
|
2
|
+
import { computed, ref } from "vue";
|
|
3
3
|
import {
|
|
4
4
|
Badge,
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
Select,
|
|
6
|
+
SelectContent,
|
|
7
|
+
SelectItem,
|
|
8
|
+
SelectTrigger,
|
|
9
|
+
SelectValue
|
|
10
10
|
} from "@revenexx/studio";
|
|
11
11
|
import { DEFAULT_RUN_DIRECTION, DEFAULT_RUN_SORT } from "../../utils/runFeedQuery";
|
|
12
12
|
import { RUN_STATUS_COLUMN_WIDTH, RUN_STATUS_WORDS, normalisedRunStatus, runStatusClass, runStatusLabel } from "../../utils/statusStyle";
|
|
13
13
|
import { formatDuration } from "../../utils/formatDuration";
|
|
14
|
+
import { relativeTime } from "../../utils/relativeTime";
|
|
15
|
+
import { useNow } from "../../composables/useNow";
|
|
14
16
|
import { TRIGGER_TYPE_WORDS, triggerTypeLabel } from "../../data/integrationNodeCatalog";
|
|
15
17
|
const props = defineProps({
|
|
16
18
|
runs: { type: Array, required: true },
|
|
@@ -31,31 +33,10 @@ const page = defineModel("page", { type: Number, ...{ default: 1 } });
|
|
|
31
33
|
const t = useIntegrationsT();
|
|
32
34
|
const router = useRouter();
|
|
33
35
|
const emptyText = computed(() => props.emptyText ?? (anyNarrowing.value || searchText.value ? t("integration.runs.table.empty.narrowed") : t("integration.runs.table.empty")));
|
|
34
|
-
const now =
|
|
35
|
-
let tick = null;
|
|
36
|
-
onMounted(() => {
|
|
37
|
-
tick = setInterval(() => {
|
|
38
|
-
now.value = Date.now();
|
|
39
|
-
}, 3e3);
|
|
40
|
-
});
|
|
41
|
-
onUnmounted(() => {
|
|
42
|
-
if (tick) {
|
|
43
|
-
clearInterval(tick);
|
|
44
|
-
tick = null;
|
|
45
|
-
}
|
|
46
|
-
});
|
|
36
|
+
const now = useNow();
|
|
47
37
|
function openRun(r) {
|
|
48
38
|
void router.push({ path: `/integrations/workflows/${r.workflowId}`, query: { run: r.id } });
|
|
49
39
|
}
|
|
50
|
-
function formatRelative(iso) {
|
|
51
|
-
if (!iso) return "\u2014";
|
|
52
|
-
const ms = Date.now() - new Date(iso).getTime();
|
|
53
|
-
if (Number.isNaN(ms)) return "\u2014";
|
|
54
|
-
if (ms < 6e4) return t("integration.common.time.secondsAgo", Math.max(1, Math.floor(ms / 1e3)));
|
|
55
|
-
if (ms < 36e5) return t("integration.common.time.minutesAgo", Math.floor(ms / 6e4));
|
|
56
|
-
if (ms < 864e5) return t("integration.common.time.hoursAgo", Math.floor(ms / 36e5));
|
|
57
|
-
return t("integration.common.time.daysAgo", Math.floor(ms / 864e5));
|
|
58
|
-
}
|
|
59
40
|
function liveDuration(run) {
|
|
60
41
|
if (!run.startedAt) return null;
|
|
61
42
|
const start = Date.parse(run.startedAt);
|
|
@@ -85,8 +66,9 @@ function setStatus(word) {
|
|
|
85
66
|
function setTrigger(kind) {
|
|
86
67
|
emit("narrow", { triggerType: kind ?? "" });
|
|
87
68
|
}
|
|
88
|
-
const
|
|
89
|
-
const
|
|
69
|
+
const ALL = "all";
|
|
70
|
+
const statusChoice = computed(() => chosenStatus.value[0] ?? ALL);
|
|
71
|
+
const triggerChoice = computed(() => props.triggerType || ALL);
|
|
90
72
|
const searchText = ref("");
|
|
91
73
|
function onTableState(state) {
|
|
92
74
|
searchText.value = state.search;
|
|
@@ -117,10 +99,10 @@ const sortedColumn = computed(() => {
|
|
|
117
99
|
return entry ? entry[0] : null;
|
|
118
100
|
});
|
|
119
101
|
const serverModels = computed(() => props.serverMode ? {
|
|
120
|
-
|
|
102
|
+
sort: tableSort.value,
|
|
121
103
|
"onUpdate:sort": onTableSort,
|
|
122
104
|
// 1-based here, as the platform pages; the shared table counts from zero.
|
|
123
|
-
|
|
105
|
+
page: page.value - 1,
|
|
124
106
|
"onUpdate:page": (p) => {
|
|
125
107
|
page.value = p + 1;
|
|
126
108
|
}
|
|
@@ -171,46 +153,66 @@ const columns = computed(() => [
|
|
|
171
153
|
(`filterTabs`) cannot be used here, because it compares the rows in hand and
|
|
172
154
|
those are one page.
|
|
173
155
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
156
|
+
It is no longer the same gesture as the other lists. Theirs tick values and
|
|
157
|
+
apply them together; this one picks one value per dimension and applies it on
|
|
158
|
+
the spot, because each pick is a request to the platform and staging them
|
|
159
|
+
would only delay the same round trip. So the shared narrowing helper does not
|
|
160
|
+
reach this page and `runs.spec.ts` carries its own — said in both places
|
|
161
|
+
rather than left to be discovered (`e2e/support/studio-table.ts`).
|
|
162
|
+
|
|
163
|
+
What is still NOT copied is the count beside each option: the built-in counts
|
|
164
|
+
the rows in hand, and counting a page here would put a number next to
|
|
165
|
+
"failed" that describes this page rather than the history the option narrows.
|
|
181
166
|
-->
|
|
182
167
|
<template v-if="serverMode" #filter-menu>
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
168
|
+
<!--
|
|
169
|
+
Two selects rather than the sub-menus this slot used to hold. Since
|
|
170
|
+
`studio-shared` 0.3.2 the filter trigger opens a popover instead of a
|
|
171
|
+
dropdown menu, and a `DropdownMenuSub` outside a menu root has nothing to
|
|
172
|
+
attach to: it renders nothing at all, so the narrowing was there in the
|
|
173
|
+
markup and gone from the screen. A select is what the new surface is built
|
|
174
|
+
for — it sizes `[data-slot=select-trigger]` to its full width — and it
|
|
175
|
+
keeps what the menu promised, the widest setting included.
|
|
176
|
+
-->
|
|
177
|
+
<div class="space-y-1">
|
|
178
|
+
<span class="text-xs font-medium text-muted-foreground">{{ t("integration.runs.table.column.status") }}</span>
|
|
179
|
+
<Select
|
|
180
|
+
:model-value="statusChoice"
|
|
181
|
+
@update:model-value="(v) => setStatus(v === ALL ? null : String(v))"
|
|
182
|
+
>
|
|
183
|
+
<SelectTrigger :aria-label="t('integration.runs.table.column.status')" class="h-8 text-sm">
|
|
184
|
+
<SelectValue />
|
|
185
|
+
</SelectTrigger>
|
|
186
|
+
<SelectContent>
|
|
187
|
+
<SelectItem :value="ALL">
|
|
188
|
+
{{ t("integration.runs.table.filter.all") }}
|
|
189
|
+
</SelectItem>
|
|
190
|
+
<SelectItem v-for="word in RUN_STATUS_WORDS" :key="word" :value="word">
|
|
191
|
+
{{ runStatusLabel(word) }}
|
|
192
|
+
</SelectItem>
|
|
193
|
+
</SelectContent>
|
|
194
|
+
</Select>
|
|
195
|
+
</div>
|
|
198
196
|
|
|
199
|
-
<
|
|
200
|
-
<
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
<
|
|
206
|
-
|
|
207
|
-
</
|
|
208
|
-
<
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
197
|
+
<div class="space-y-1">
|
|
198
|
+
<span class="text-xs font-medium text-muted-foreground">{{ t("integration.runs.table.column.trigger") }}</span>
|
|
199
|
+
<Select
|
|
200
|
+
:model-value="triggerChoice"
|
|
201
|
+
@update:model-value="(v) => setTrigger(v === ALL ? null : String(v))"
|
|
202
|
+
>
|
|
203
|
+
<SelectTrigger :aria-label="t('integration.runs.table.column.trigger')" class="h-8 text-sm">
|
|
204
|
+
<SelectValue />
|
|
205
|
+
</SelectTrigger>
|
|
206
|
+
<SelectContent>
|
|
207
|
+
<SelectItem :value="ALL">
|
|
208
|
+
{{ t("integration.runs.table.filter.all") }}
|
|
209
|
+
</SelectItem>
|
|
210
|
+
<SelectItem v-for="kind in TRIGGER_TYPE_WORDS" :key="kind" :value="kind">
|
|
211
|
+
{{ triggerTypeLabel(kind) }}
|
|
212
|
+
</SelectItem>
|
|
213
|
+
</SelectContent>
|
|
214
|
+
</Select>
|
|
215
|
+
</div>
|
|
214
216
|
</template>
|
|
215
217
|
|
|
216
218
|
<template #cell-status="{ row: run }">
|
|
@@ -243,7 +245,7 @@ const columns = computed(() => [
|
|
|
243
245
|
</template>
|
|
244
246
|
|
|
245
247
|
<template #cell-started="{ row: run }">
|
|
246
|
-
<span class="text-sm text-muted-foreground tabular-nums">{{
|
|
248
|
+
<span class="text-sm text-muted-foreground tabular-nums">{{ relativeTime(run.startedAt, now) }}</span>
|
|
247
249
|
</template>
|
|
248
250
|
|
|
249
251
|
<template #cell-duration="{ row: run }">
|
|
@@ -17,13 +17,24 @@ const sftp = typeof useConsoleSftp === "function" ? useConsoleSftp() : {
|
|
|
17
17
|
const { create } = useIntegrationsCredentials();
|
|
18
18
|
const busyId = ref(null);
|
|
19
19
|
const error = ref(null);
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
const loadPending = ref(false);
|
|
21
|
+
let openCycle = 0;
|
|
22
|
+
watch(open, async (o) => {
|
|
23
|
+
if (!o) return;
|
|
24
|
+
const cycle = ++openCycle;
|
|
25
|
+
error.value = null;
|
|
26
|
+
loadPending.value = true;
|
|
27
|
+
try {
|
|
28
|
+
await sftp.load();
|
|
29
|
+
} finally {
|
|
30
|
+
if (cycle === openCycle) loadPending.value = false;
|
|
24
31
|
}
|
|
25
32
|
});
|
|
26
33
|
const activeAccounts = computed(() => sftp.accounts.value.filter((a) => a.status === "active" && a.login_username));
|
|
34
|
+
const waiting = computed(() => (loadPending.value || sftp.loading.value) && !activeAccounts.value.length);
|
|
35
|
+
const loadFailed = computed(
|
|
36
|
+
() => !waiting.value && !activeAccounts.value.length && sftp.activeTenantSlug.value && sftp.error?.value ? t("integration.credentials.sftpImport.state.loadFailed", { message: sftp.error.value }) : null
|
|
37
|
+
);
|
|
27
38
|
async function importAccount(account) {
|
|
28
39
|
const tenant = sftp.activeTenantSlug.value;
|
|
29
40
|
if (!tenant || !account.login_username) return;
|
|
@@ -69,13 +80,16 @@ async function importAccount(account) {
|
|
|
69
80
|
limit written down there (PO-251). -->
|
|
70
81
|
<LoadingAnnouncement
|
|
71
82
|
:label="t('integration.credentials.sftpImport.title')"
|
|
72
|
-
:loading="
|
|
83
|
+
:loading="waiting"
|
|
73
84
|
:text="t('integration.credentials.sftpImport.state.loading')"
|
|
74
85
|
/>
|
|
75
86
|
|
|
76
|
-
<div v-if="
|
|
87
|
+
<div v-if="waiting" aria-hidden="true" class="space-y-2">
|
|
77
88
|
<div v-for="n in 3" :key="`sk-${n}`" class="h-9 rounded-md bg-black/[0.07] animate-pulse" />
|
|
78
89
|
</div>
|
|
90
|
+
<p v-else-if="loadFailed" class="py-4 text-center text-sm text-rose-600">
|
|
91
|
+
{{ loadFailed }}
|
|
92
|
+
</p>
|
|
79
93
|
<p v-else-if="!activeAccounts.length" class="py-4 text-center text-sm text-muted-foreground">
|
|
80
94
|
{{ t("integration.credentials.sftpImport.empty") }}
|
|
81
95
|
</p>
|
|
@@ -212,6 +212,7 @@ function onViewMouseDown(e) {
|
|
|
212
212
|
class="h-full w-full overflow-hidden whitespace-pre-wrap break-words px-3 py-2"
|
|
213
213
|
@mousedown="onViewMouseDown"
|
|
214
214
|
>
|
|
215
|
+
<!-- eslint-disable-next-line vue/no-v-html -- renderMarkdownLite escapes the note before it adds a tag -->
|
|
215
216
|
<div v-if="data.sticky.content" v-html="rendered" />
|
|
216
217
|
<!-- The hint is an instruction, not content, so it goes with the rest of the
|
|
217
218
|
affordances: on a read-only canvas it would name the one gesture
|
|
@@ -4,7 +4,6 @@ import { AltArrowLeft as ArrowLeft, RefreshCircle as Loader2, Magnifier as Searc
|
|
|
4
4
|
import { triggerGlyph } from "../../data/triggerGlyphs";
|
|
5
5
|
import * as Lucide from "lucide-vue-next";
|
|
6
6
|
import {
|
|
7
|
-
Badge,
|
|
8
7
|
Button,
|
|
9
8
|
Dialog,
|
|
10
9
|
DialogContent,
|
|
@@ -23,6 +22,7 @@ import { TRIGGER_TEMPLATES } from "../../data/integrationNodeCatalog";
|
|
|
23
22
|
import LoadingAnnouncement from "./LoadingAnnouncement.vue";
|
|
24
23
|
import TemplateCard from "./TemplateCard.vue";
|
|
25
24
|
import TemplateInstallStep from "./TemplateInstallStep.vue";
|
|
25
|
+
import WorkflowImportFileStep from "./WorkflowImportFileStep.vue";
|
|
26
26
|
import WorkflowIconPicker from "./WorkflowIconPicker.vue";
|
|
27
27
|
import WorkflowTagsInput from "./WorkflowTagsInput.vue";
|
|
28
28
|
import { resolveLocalized } from "../../utils/localize";
|
|
@@ -35,7 +35,8 @@ const { templates, loading: templatesLoading, error: templatesError, load: loadT
|
|
|
35
35
|
const mode = ref("blank");
|
|
36
36
|
const modeTabs = computed(() => [
|
|
37
37
|
{ key: "blank", label: t("integration.workflows.create.tab.blank") },
|
|
38
|
-
{ key: "template", label: t("integration.workflows.create.tab.template") }
|
|
38
|
+
{ key: "template", label: t("integration.workflows.create.tab.template") },
|
|
39
|
+
{ key: "file", label: t("integration.workflows.create.tab.file") }
|
|
39
40
|
]);
|
|
40
41
|
function iconFor(name2) {
|
|
41
42
|
if (!name2) return Lucide.Box;
|
|
@@ -200,14 +201,14 @@ async function installed(id) {
|
|
|
200
201
|
</SelectValue>
|
|
201
202
|
</SelectTrigger>
|
|
202
203
|
<SelectContent>
|
|
203
|
-
<SelectItem v-for="
|
|
204
|
+
<SelectItem v-for="tpl in TRIGGER_TEMPLATES" :key="tpl.id" :value="tpl.type">
|
|
204
205
|
<span class="flex items-center gap-2.5 py-0.5">
|
|
205
206
|
<span class="flex size-7 shrink-0 items-center justify-center rounded-md bg-emerald-100 text-emerald-600">
|
|
206
|
-
<component :is="triggerGlyph(
|
|
207
|
+
<component :is="triggerGlyph(tpl.type)" :size="14" />
|
|
207
208
|
</span>
|
|
208
209
|
<span class="min-w-0">
|
|
209
|
-
<span class="block text-sm font-medium">{{ resolveLocalized(
|
|
210
|
-
<span class="block text-sm text-muted-foreground">{{ resolveLocalized(
|
|
210
|
+
<span class="block text-sm font-medium">{{ resolveLocalized(tpl.name) }}</span>
|
|
211
|
+
<span class="block text-sm text-muted-foreground">{{ resolveLocalized(tpl.description) }}</span>
|
|
211
212
|
</span>
|
|
212
213
|
</span>
|
|
213
214
|
</SelectItem>
|
|
@@ -260,6 +261,9 @@ async function installed(id) {
|
|
|
260
261
|
</div>
|
|
261
262
|
</form>
|
|
262
263
|
|
|
264
|
+
<!-- ── From a file ────────────────────────────────────────────────────── -->
|
|
265
|
+
<WorkflowImportFileStep v-else-if="mode === 'file'" @created="installed" @cancel="open = false" />
|
|
266
|
+
|
|
263
267
|
<!-- ── From template ─────────────────────────────────────────────────── -->
|
|
264
268
|
<div v-else class="-m-1 max-h-[65vh] space-y-3 overflow-y-auto p-1">
|
|
265
269
|
<!-- The install step, once a template is picked: the same one the
|
|
@@ -42,42 +42,8 @@ type __VLS_Props = {
|
|
|
42
42
|
* id that is on the canvas is in the blob by construction.
|
|
43
43
|
*/
|
|
44
44
|
declare function revealNode(id: string): void;
|
|
45
|
-
/**
|
|
46
|
-
* Point at a node without opening it — for a reader who has to look at the
|
|
47
|
-
* card's *surroundings* rather than at its settings.
|
|
48
|
-
*
|
|
49
|
-
* The deliberate difference from {@link revealNode} is the missing selection,
|
|
50
|
-
* and with it the missing settings lightbox. That is the whole reason there are
|
|
51
|
-
* two functions: the validation banner sends someone to a field at fault, and
|
|
52
|
-
* the dialog is where that field is. The error-path advice sends them to a port
|
|
53
|
-
* with nothing on it, and the question they have to answer — where should this
|
|
54
|
-
* failure go? — is answered by the graph around the card, which a dialog over
|
|
55
|
-
* the canvas covers up.
|
|
56
|
-
*
|
|
57
|
-
* Marked with the same pulse a newly created card gets, because the job is the
|
|
58
|
-
* same one: say which of thirty cards is the one meant.
|
|
59
|
-
*/
|
|
60
|
-
declare function showNodeOnCanvas(id: string): void;
|
|
61
|
-
/**
|
|
62
|
-
* Hang a Stop-and-Error step off a node's error output and wire it up
|
|
63
|
-
* (specs/workflow-advice.md AC-3).
|
|
64
|
-
*
|
|
65
|
-
* The advice strip can tell an author their error output leads nowhere; on its
|
|
66
|
-
* own that is a reprimand without a remedy, because the repair — drop a node,
|
|
67
|
-
* pick the right one, connect it, write an expression quoting the failing step's
|
|
68
|
-
* message — is four steps of tedium they have to already know. Every template we
|
|
69
|
-
* ship does exactly this, twenty-five times over, so the shape is settled and
|
|
70
|
-
* worth one click.
|
|
71
|
-
*
|
|
72
|
-
* Leaves the result unsaved and unopened, like every other insertion here: the
|
|
73
|
-
* new step is highlighted and the author decides whether to keep it. Deleting it
|
|
74
|
-
* is the undo, because this editor has no undo stack to hook into.
|
|
75
|
-
*/
|
|
76
|
-
declare function attachErrorStop(sourceNodeId: string, port: string): void;
|
|
77
45
|
declare const _default: import("vue").DefineComponent<__VLS_Props, {
|
|
78
46
|
revealNode: typeof revealNode;
|
|
79
|
-
showNodeOnCanvas: typeof showNodeOnCanvas;
|
|
80
|
-
attachErrorStop: typeof attachErrorStop;
|
|
81
47
|
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
82
48
|
run: () => any;
|
|
83
49
|
toggleActive: () => any;
|