@revenexx/studio-integrations 1.1.0 → 1.2.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 +520 -0
- package/README.md +3 -1
- package/dist/module.json +1 -1
- package/dist/runtime/components/Integrations/CredentialCreateDialog.vue +6 -18
- package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +13 -23
- package/dist/runtime/components/Integrations/CredentialRevenexxSource.vue +7 -6
- package/dist/runtime/components/Integrations/DeadLettersPanel.vue +15 -4
- package/dist/runtime/components/Integrations/ExpressionInput.d.vue.ts +4 -0
- package/dist/runtime/components/Integrations/ExpressionInput.vue +77 -22
- package/dist/runtime/components/Integrations/ExpressionInput.vue.d.ts +4 -0
- package/dist/runtime/components/Integrations/GroupFrameView.vue +5 -4
- package/dist/runtime/components/Integrations/LoadingAnnouncement.d.vue.ts +38 -0
- package/dist/runtime/components/Integrations/LoadingAnnouncement.vue +16 -0
- package/dist/runtime/components/Integrations/LoadingAnnouncement.vue.d.ts +38 -0
- package/dist/runtime/components/Integrations/NodeConfigField.vue +23 -0
- package/dist/runtime/components/Integrations/NodeInspector.d.vue.ts +2 -2
- package/dist/runtime/components/Integrations/NodeInspector.vue +25 -32
- package/dist/runtime/components/Integrations/NodeInspector.vue.d.ts +2 -2
- package/dist/runtime/components/Integrations/RunActionsMenu.vue +5 -3
- package/dist/runtime/components/Integrations/RunDetailPanel.vue +5 -17
- package/dist/runtime/components/Integrations/RunResponseModal.d.vue.ts +30 -8
- package/dist/runtime/components/Integrations/RunResponseModal.vue +92 -24
- package/dist/runtime/components/Integrations/RunResponseModal.vue.d.ts +30 -8
- package/dist/runtime/components/Integrations/RunsTable.d.vue.ts +61 -3
- package/dist/runtime/components/Integrations/RunsTable.vue +143 -20
- package/dist/runtime/components/Integrations/RunsTable.vue.d.ts +61 -3
- package/dist/runtime/components/Integrations/SftpAccountImportDialog.vue +10 -1
- package/dist/runtime/components/Integrations/StickyNoteView.vue +1 -1
- package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +18 -3
- package/dist/runtime/components/Integrations/WorkflowDeleteDialog.d.vue.ts +18 -0
- package/dist/runtime/components/Integrations/WorkflowDeleteDialog.vue +70 -0
- package/dist/runtime/components/Integrations/WorkflowDeleteDialog.vue.d.ts +18 -0
- package/dist/runtime/components/Integrations/WorkflowDuplicateDialog.vue +2 -1
- package/dist/runtime/components/Integrations/WorkflowEdgeView.vue +4 -4
- package/dist/runtime/components/Integrations/WorkflowEditor.vue +39 -28
- package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue +17 -6
- package/dist/runtime/components/Integrations/WorkflowNodeView.vue +17 -21
- package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +8 -11
- package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +6 -7
- package/dist/runtime/components/Integrations/WorkflowVariablesDialog.vue +7 -7
- package/dist/runtime/components/Integrations/WorkflowsTable.vue +10 -48
- package/dist/runtime/components/Integrations/canvasSides.d.ts +48 -0
- package/dist/runtime/components/Integrations/canvasSides.js +22 -0
- package/dist/runtime/composables/useCredentialTypes.js +2 -6
- package/dist/runtime/composables/useIntegrationsAudit.js +2 -2
- package/dist/runtime/composables/useIntegrationsCredentials.js +4 -8
- package/dist/runtime/composables/useIntegrationsDeadLetters.d.ts +4 -0
- package/dist/runtime/composables/useIntegrationsDeadLetters.js +16 -1
- package/dist/runtime/composables/useIntegrationsNodes.js +2 -2
- package/dist/runtime/composables/useIntegrationsRevisions.js +2 -6
- package/dist/runtime/composables/useIntegrationsRuns.d.ts +4 -15
- package/dist/runtime/composables/useIntegrationsRuns.js +49 -34
- package/dist/runtime/composables/useIntegrationsSchedules.js +2 -2
- package/dist/runtime/composables/useIntegrationsSecrets.js +2 -6
- package/dist/runtime/composables/useIntegrationsTemplates.js +2 -5
- package/dist/runtime/composables/useIntegrationsTriggers.js +2 -2
- package/dist/runtime/composables/useIntegrationsWorkflows.js +4 -7
- package/dist/runtime/composables/useNodeConfigResolve.js +2 -2
- package/dist/runtime/composables/useOAuthConnect.d.ts +42 -0
- package/dist/runtime/composables/useOAuthConnect.js +33 -0
- package/dist/runtime/data/expressionEditor.d.ts +17 -5
- package/dist/runtime/data/expressionEditor.js +11 -5
- package/dist/runtime/data/handleId.d.ts +61 -1
- package/dist/runtime/data/handleId.js +8 -1
- package/dist/runtime/data/payloadForm.js +3 -2
- package/dist/runtime/i18n/messages/de.json +30 -11
- package/dist/runtime/i18n/messages/en.json +27 -8
- package/dist/runtime/pages/integrations/audit.vue +7 -6
- package/dist/runtime/pages/integrations/credentials.vue +9 -21
- package/dist/runtime/pages/integrations/index.vue +70 -12
- package/dist/runtime/pages/integrations/runs.vue +116 -7
- package/dist/runtime/pages/integrations/secrets.vue +2 -1
- package/dist/runtime/pages/integrations/templates/[slug].vue +2 -1
- package/dist/runtime/pages/integrations/templates/index.vue +18 -15
- package/dist/runtime/pages/integrations/workflows/[id].vue +177 -28
- package/dist/runtime/types/integrations.d.ts +5 -0
- package/dist/runtime/utils/apiError.d.ts +21 -1
- package/dist/runtime/utils/apiError.js +1 -1
- package/dist/runtime/utils/clipboard.d.ts +33 -0
- package/dist/runtime/utils/clipboard.js +11 -0
- package/dist/runtime/utils/deepClone.d.ts +41 -0
- package/dist/runtime/utils/deepClone.js +3 -0
- package/dist/runtime/utils/formatDuration.d.ts +44 -0
- package/dist/runtime/utils/formatDuration.js +13 -0
- package/dist/runtime/utils/nodeDisplayName.d.ts +22 -0
- package/dist/runtime/utils/nodeDisplayName.js +6 -0
- package/dist/runtime/utils/notify.d.ts +1 -1
- package/dist/runtime/utils/runEnvelope.d.ts +31 -0
- package/dist/runtime/utils/runEnvelope.js +12 -0
- package/dist/runtime/utils/runFeedQuery.d.ts +135 -0
- package/dist/runtime/utils/runFeedQuery.js +66 -0
- package/dist/runtime/utils/runOutputs.d.ts +33 -0
- package/dist/runtime/utils/runOutputs.js +10 -0
- package/dist/runtime/utils/statusStyle.d.ts +24 -0
- package/dist/runtime/utils/statusStyle.js +1 -0
- package/dist/runtime/utils/validationFindings.d.ts +26 -3
- package/dist/runtime/utils/validationFindings.js +18 -3
- package/package.json +1 -1
|
@@ -1,19 +1,36 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { computed, onMounted, onUnmounted, ref } from "vue";
|
|
3
|
-
import {
|
|
4
|
-
|
|
3
|
+
import {
|
|
4
|
+
Badge,
|
|
5
|
+
DropdownMenuItem,
|
|
6
|
+
DropdownMenuSeparator,
|
|
7
|
+
DropdownMenuSub,
|
|
8
|
+
DropdownMenuSubContent,
|
|
9
|
+
DropdownMenuSubTrigger
|
|
10
|
+
} from "@revenexx/studio";
|
|
11
|
+
import { DEFAULT_RUN_DIRECTION, DEFAULT_RUN_SORT } from "../../utils/runFeedQuery";
|
|
12
|
+
import { RUN_STATUS_COLUMN_WIDTH, RUN_STATUS_WORDS, normalisedRunStatus, runStatusClass, runStatusLabel } from "../../utils/statusStyle";
|
|
13
|
+
import { formatDuration } from "../../utils/formatDuration";
|
|
5
14
|
import { TRIGGER_TYPE_WORDS, triggerTypeLabel } from "../../data/integrationNodeCatalog";
|
|
6
15
|
const props = defineProps({
|
|
7
16
|
runs: { type: Array, required: true },
|
|
8
17
|
loading: { type: Boolean, required: false, default: false },
|
|
9
18
|
emptyText: { type: String, required: false },
|
|
10
19
|
pagination: { type: Boolean, required: false, default: true },
|
|
11
|
-
showWorkflow: { type: Boolean, required: false, default: true }
|
|
20
|
+
showWorkflow: { type: Boolean, required: false, default: true },
|
|
21
|
+
serverMode: { type: Boolean, required: false, default: false },
|
|
22
|
+
total: { type: Number, required: false, default: 0 },
|
|
23
|
+
perPage: { type: Number, required: false, default: 25 },
|
|
24
|
+
status: { type: Array, required: false },
|
|
25
|
+
triggerType: { type: String, required: false, default: "" },
|
|
26
|
+
sort: { type: null, required: false, default: DEFAULT_RUN_SORT },
|
|
27
|
+
direction: { type: String, required: false, default: DEFAULT_RUN_DIRECTION }
|
|
12
28
|
});
|
|
13
|
-
const emit = defineEmits(["changed"]);
|
|
29
|
+
const emit = defineEmits(["changed", "narrow", "search", "page-size", "reorder"]);
|
|
30
|
+
const page = defineModel("page", { type: Number, ...{ default: 1 } });
|
|
14
31
|
const t = useIntegrationsT();
|
|
15
32
|
const router = useRouter();
|
|
16
|
-
const emptyText = computed(() => props.emptyText ?? t("integration.runs.table.empty"));
|
|
33
|
+
const emptyText = computed(() => props.emptyText ?? (anyNarrowing.value || searchText.value ? t("integration.runs.table.empty.narrowed") : t("integration.runs.table.empty")));
|
|
17
34
|
const now = ref(Date.now());
|
|
18
35
|
let tick = null;
|
|
19
36
|
onMounted(() => {
|
|
@@ -39,12 +56,6 @@ function formatRelative(iso) {
|
|
|
39
56
|
if (ms < 864e5) return t("integration.common.time.hoursAgo", Math.floor(ms / 36e5));
|
|
40
57
|
return t("integration.common.time.daysAgo", Math.floor(ms / 864e5));
|
|
41
58
|
}
|
|
42
|
-
function formatDuration(ms) {
|
|
43
|
-
if (ms == null) return "\u2014";
|
|
44
|
-
if (ms < 1e3) return `${ms}ms`;
|
|
45
|
-
if (ms < 6e4) return `${(ms / 1e3).toFixed(2)}s`;
|
|
46
|
-
return `${(ms / 6e4).toFixed(1)}m`;
|
|
47
|
-
}
|
|
48
59
|
function liveDuration(run) {
|
|
49
60
|
if (!run.startedAt) return null;
|
|
50
61
|
const start = Date.parse(run.startedAt);
|
|
@@ -52,7 +63,7 @@ function liveDuration(run) {
|
|
|
52
63
|
const end = run.finishedAt ? Date.parse(run.finishedAt) : now.value;
|
|
53
64
|
return end - start;
|
|
54
65
|
}
|
|
55
|
-
const filterTabs = computed(() => props.pagination ? [
|
|
66
|
+
const filterTabs = computed(() => props.pagination && !props.serverMode ? [
|
|
56
67
|
{
|
|
57
68
|
key: "status",
|
|
58
69
|
label: t("integration.runs.table.column.status"),
|
|
@@ -66,21 +77,73 @@ const filterTabs = computed(() => props.pagination ? [
|
|
|
66
77
|
options: TRIGGER_TYPE_WORDS.map((v) => ({ value: v, label: triggerTypeLabel(v) }))
|
|
67
78
|
}
|
|
68
79
|
] : void 0);
|
|
80
|
+
const chosenStatus = computed(() => props.status ?? []);
|
|
81
|
+
const anyNarrowing = computed(() => chosenStatus.value.length > 0 || props.triggerType !== "");
|
|
82
|
+
function setStatus(word) {
|
|
83
|
+
emit("narrow", { status: word ? [word] : [] });
|
|
84
|
+
}
|
|
85
|
+
function setTrigger(kind) {
|
|
86
|
+
emit("narrow", { triggerType: kind ?? "" });
|
|
87
|
+
}
|
|
88
|
+
const statusApplied = computed(() => chosenStatus.value[0] ? runStatusLabel(chosenStatus.value[0]) : "");
|
|
89
|
+
const triggerApplied = computed(() => props.triggerType ? triggerTypeLabel(props.triggerType) : "");
|
|
90
|
+
const searchText = ref("");
|
|
91
|
+
function onTableState(state) {
|
|
92
|
+
searchText.value = state.search;
|
|
93
|
+
emit("search", state.search);
|
|
94
|
+
if (state.pageSize !== props.perPage) emit("page-size", state.pageSize);
|
|
95
|
+
}
|
|
96
|
+
const tableSort = computed(() => ({
|
|
97
|
+
column: sortedColumn.value,
|
|
98
|
+
direction: sortedColumn.value ? props.direction : null
|
|
99
|
+
}));
|
|
100
|
+
function onTableSort(next) {
|
|
101
|
+
const sort = next.column ? SERVER_SORTS[next.column] : void 0;
|
|
102
|
+
if (!sort || !next.direction) {
|
|
103
|
+
emit("reorder", { sort: DEFAULT_RUN_SORT, direction: DEFAULT_RUN_DIRECTION });
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
emit("reorder", { sort, direction: next.direction });
|
|
107
|
+
}
|
|
108
|
+
const SERVER_SORTS = {
|
|
109
|
+
started: "started_at",
|
|
110
|
+
duration: "duration"
|
|
111
|
+
};
|
|
112
|
+
function sortableColumn(key) {
|
|
113
|
+
return props.serverMode && key in SERVER_SORTS;
|
|
114
|
+
}
|
|
115
|
+
const sortedColumn = computed(() => {
|
|
116
|
+
const entry = Object.entries(SERVER_SORTS).find(([, value]) => value === props.sort);
|
|
117
|
+
return entry ? entry[0] : null;
|
|
118
|
+
});
|
|
119
|
+
const serverModels = computed(() => props.serverMode ? {
|
|
120
|
+
"sort": tableSort.value,
|
|
121
|
+
"onUpdate:sort": onTableSort,
|
|
122
|
+
// 1-based here, as the platform pages; the shared table counts from zero.
|
|
123
|
+
"page": page.value - 1,
|
|
124
|
+
"onUpdate:page": (p) => {
|
|
125
|
+
page.value = p + 1;
|
|
126
|
+
}
|
|
127
|
+
} : {});
|
|
69
128
|
const columns = computed(() => [
|
|
70
129
|
// Sorted and searched by the word on screen, not by the raw field: a column
|
|
71
130
|
// with no `searchGetter` is searched through its `sortGetter`, so a German
|
|
72
131
|
// reader typing the status they can see has to find the row that shows it.
|
|
73
|
-
|
|
74
|
-
|
|
132
|
+
// One floor for both lists of runs, this one and the dashboard's, rather than a
|
|
133
|
+
// number guessed twice — see `RUN_STATUS_COLUMN_WIDTH` for how it was chosen and
|
|
134
|
+
// why the track cannot be elastic. At the 100px it used to be, the German
|
|
135
|
+
// "fehlgeschlagen" ran out of its cell and under the workflow name (PO-362).
|
|
136
|
+
{ key: "status", label: t("integration.runs.table.column.status"), width: RUN_STATUS_COLUMN_WIDTH, sortable: sortableColumn("status"), sortGetter: (r) => runStatusLabel(r.status) },
|
|
137
|
+
...props.showWorkflow ? [{ key: "workflow", label: t("integration.runs.table.column.workflow"), sortable: sortableColumn("workflow"), sortGetter: (r) => r.workflowName.toLowerCase(), searchGetter: (r) => r.workflowName }] : [],
|
|
75
138
|
// Fixed floor wide enough for a full UUID, flexible above it — header and
|
|
76
139
|
// body rows are separate grids, so a content-sized track (max-content)
|
|
77
140
|
// would misalign them. The flex share also lets the table fill the width.
|
|
78
141
|
{ key: "id", label: t("integration.runs.table.column.id"), width: "minmax(280px,1fr)", searchGetter: (r) => `${r.temporalRunId ?? ""} ${r.id}` },
|
|
79
142
|
// Same rule as the status column above: the word on screen is what sorts and
|
|
80
143
|
// what a search has to match.
|
|
81
|
-
{ key: "trigger", label: t("integration.runs.table.column.trigger"), width: "120px", sortable:
|
|
82
|
-
{ key: "started", label: t("integration.runs.table.column.started"), width: "120px", sortable:
|
|
83
|
-
{ key: "duration", label: t("integration.runs.table.column.duration"), width: "100px", sortable:
|
|
144
|
+
{ key: "trigger", label: t("integration.runs.table.column.trigger"), width: "120px", sortable: sortableColumn("trigger"), sortGetter: (r) => triggerTypeLabel(r.triggerType) },
|
|
145
|
+
{ key: "started", label: t("integration.runs.table.column.started"), width: "120px", sortable: sortableColumn("started"), sortGetter: (r) => r.startedAt },
|
|
146
|
+
{ key: "duration", label: t("integration.runs.table.column.duration"), width: "100px", sortable: sortableColumn("duration"), sortGetter: (r) => r.durationMs ?? -1 },
|
|
84
147
|
{ key: "actions", label: "", width: "52px" }
|
|
85
148
|
]);
|
|
86
149
|
</script>
|
|
@@ -90,16 +153,76 @@ const columns = computed(() => [
|
|
|
90
153
|
:data="runs"
|
|
91
154
|
:columns="columns"
|
|
92
155
|
:row-key="(r) => r.id"
|
|
93
|
-
:default-sort="{ column: 'started', direction: 'desc' }"
|
|
94
156
|
:search="{ placeholder: t('integration.runs.table.search.placeholder') }"
|
|
95
157
|
:filter-tabs="filterTabs"
|
|
158
|
+
:filter="serverMode ? { width: 'w-72', active: anyNarrowing } : false"
|
|
159
|
+
:server-mode="serverMode"
|
|
160
|
+
:total="total"
|
|
96
161
|
:loading="loading"
|
|
97
|
-
:pagination="pagination"
|
|
162
|
+
:pagination="serverMode ? { pageSize: perPage } : pagination"
|
|
98
163
|
:empty-text="emptyText"
|
|
164
|
+
v-bind="serverModels"
|
|
165
|
+
@state-change="onTableState"
|
|
99
166
|
@row-click="(r) => openRun(r)"
|
|
100
167
|
>
|
|
168
|
+
<!--
|
|
169
|
+
The narrowing, where a server-mode surface has to put it: the shared table
|
|
170
|
+
owns the popover, this page owns the selection. Its own in-memory control
|
|
171
|
+
(`filterTabs`) cannot be used here, because it compares the rows in hand and
|
|
172
|
+
those are one page.
|
|
173
|
+
|
|
174
|
+
Built to the same shape as that control on purpose — a dimension per
|
|
175
|
+
sub-menu, "All" first, one value at a time — so this list is narrowed by the
|
|
176
|
+
same gesture as the five that narrow what they hold, and one test helper
|
|
177
|
+
still addresses all six (`e2e/support/studio-table.ts`). What is NOT copied
|
|
178
|
+
is the count beside each option: the built-in counts the rows in hand, and
|
|
179
|
+
counting a page here would put a number next to "failed" that describes this
|
|
180
|
+
page rather than the history the option narrows.
|
|
181
|
+
-->
|
|
182
|
+
<template v-if="serverMode" #filter-menu>
|
|
183
|
+
<DropdownMenuSub>
|
|
184
|
+
<DropdownMenuSubTrigger class="gap-2">
|
|
185
|
+
<span class="flex-1">{{ t("integration.runs.table.column.status") }}</span>
|
|
186
|
+
<span v-if="statusApplied" class="text-xs text-primary">{{ statusApplied }}</span>
|
|
187
|
+
</DropdownMenuSubTrigger>
|
|
188
|
+
<DropdownMenuSubContent class="w-48">
|
|
189
|
+
<DropdownMenuItem @select="setStatus(null)">
|
|
190
|
+
{{ t("integration.runs.table.filter.all") }}
|
|
191
|
+
</DropdownMenuItem>
|
|
192
|
+
<DropdownMenuSeparator />
|
|
193
|
+
<DropdownMenuItem v-for="word in RUN_STATUS_WORDS" :key="word" @select="setStatus(word)">
|
|
194
|
+
{{ runStatusLabel(word) }}
|
|
195
|
+
</DropdownMenuItem>
|
|
196
|
+
</DropdownMenuSubContent>
|
|
197
|
+
</DropdownMenuSub>
|
|
198
|
+
|
|
199
|
+
<DropdownMenuSub>
|
|
200
|
+
<DropdownMenuSubTrigger class="gap-2">
|
|
201
|
+
<span class="flex-1">{{ t("integration.runs.table.column.trigger") }}</span>
|
|
202
|
+
<span v-if="triggerApplied" class="text-xs text-primary">{{ triggerApplied }}</span>
|
|
203
|
+
</DropdownMenuSubTrigger>
|
|
204
|
+
<DropdownMenuSubContent class="w-48">
|
|
205
|
+
<DropdownMenuItem @select="setTrigger(null)">
|
|
206
|
+
{{ t("integration.runs.table.filter.all") }}
|
|
207
|
+
</DropdownMenuItem>
|
|
208
|
+
<DropdownMenuSeparator />
|
|
209
|
+
<DropdownMenuItem v-for="kind in TRIGGER_TYPE_WORDS" :key="kind" @select="setTrigger(kind)">
|
|
210
|
+
{{ triggerTypeLabel(kind) }}
|
|
211
|
+
</DropdownMenuItem>
|
|
212
|
+
</DropdownMenuSubContent>
|
|
213
|
+
</DropdownMenuSub>
|
|
214
|
+
</template>
|
|
215
|
+
|
|
101
216
|
<template #cell-status="{ row: run }">
|
|
102
|
-
|
|
217
|
+
<!-- `max-w-full` is the net, not the fix: a word wider than the column above is
|
|
218
|
+
then clipped inside its own cell instead of being drawn over the one beside
|
|
219
|
+
it, and the `title` keeps it readable. A hard clip, not an ellipsis — the
|
|
220
|
+
badge is an inline-flex box, where `text-overflow` does not apply. -->
|
|
221
|
+
<Badge
|
|
222
|
+
variant="outline"
|
|
223
|
+
:title="runStatusLabel(run.status)"
|
|
224
|
+
:class="['border text-xs font-medium uppercase max-w-full', runStatusClass(run.status)]"
|
|
225
|
+
>
|
|
103
226
|
{{ runStatusLabel(run.status) }}
|
|
104
227
|
</Badge>
|
|
105
228
|
</template>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { WorkflowRun } from '../../types/integrations.js';
|
|
1
|
+
import type { RunStatus, TriggerType, WorkflowRun } from '../../types/integrations.js';
|
|
2
|
+
import type { RunSort, RunSortDirection } from '../../utils/runFeedQuery.js';
|
|
2
3
|
type __VLS_Props = {
|
|
3
4
|
runs: WorkflowRun[];
|
|
4
5
|
loading?: boolean;
|
|
@@ -7,14 +8,71 @@ type __VLS_Props = {
|
|
|
7
8
|
pagination?: boolean;
|
|
8
9
|
/** false inside a workflow — the context makes the column redundant. */
|
|
9
10
|
showWorkflow?: boolean;
|
|
11
|
+
/**
|
|
12
|
+
* The owning list reads its page and its narrowing from the platform.
|
|
13
|
+
*
|
|
14
|
+
* The table then renders the rows it is handed and reports what the operator
|
|
15
|
+
* asked for, instead of narrowing and slicing an array. That is what lets the
|
|
16
|
+
* list reach past the newest hundred runs at all (PO-369).
|
|
17
|
+
*/
|
|
18
|
+
serverMode?: boolean;
|
|
19
|
+
/** How many runs match the narrowing. The pager's "of 7"; server mode only. */
|
|
20
|
+
total?: number;
|
|
21
|
+
/**
|
|
22
|
+
* How many rows a page holds — the SAME number the page asks the platform for.
|
|
23
|
+
*
|
|
24
|
+
* The shared table works its footer's "showing 1–25 of 140" out from its own
|
|
25
|
+
* page size, so a table left on its default while the request asks for another
|
|
26
|
+
* number states a range that is simply wrong: it counted twenty where
|
|
27
|
+
* twenty-five had arrived.
|
|
28
|
+
*/
|
|
29
|
+
perPage?: number;
|
|
30
|
+
/** The narrowing in force. Owned by the page, drawn here. */
|
|
31
|
+
status?: RunStatus[];
|
|
32
|
+
triggerType?: TriggerType | '';
|
|
33
|
+
/** The order the platform is reading the list in. Server mode only. */
|
|
34
|
+
sort?: RunSort;
|
|
35
|
+
direction?: RunSortDirection;
|
|
10
36
|
};
|
|
11
|
-
|
|
37
|
+
type __VLS_PublicProps = __VLS_Props & {
|
|
38
|
+
/** 1-based, as the platform pages. The shared table counts from zero. */
|
|
39
|
+
'page'?: number;
|
|
40
|
+
};
|
|
41
|
+
declare const _default: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
42
|
+
search: (text: string) => any;
|
|
12
43
|
changed: () => any;
|
|
13
|
-
|
|
44
|
+
narrow: (patch: {
|
|
45
|
+
status?: RunStatus[];
|
|
46
|
+
triggerType?: TriggerType | "";
|
|
47
|
+
}) => any;
|
|
48
|
+
"page-size": (size: number) => any;
|
|
49
|
+
reorder: (order: {
|
|
50
|
+
sort: RunSort;
|
|
51
|
+
direction: RunSortDirection;
|
|
52
|
+
}) => any;
|
|
53
|
+
"update:page": (value: number) => any;
|
|
54
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
55
|
+
onSearch?: ((text: string) => any) | undefined;
|
|
14
56
|
onChanged?: (() => any) | undefined;
|
|
57
|
+
onNarrow?: ((patch: {
|
|
58
|
+
status?: RunStatus[];
|
|
59
|
+
triggerType?: TriggerType | "";
|
|
60
|
+
}) => any) | undefined;
|
|
61
|
+
"onPage-size"?: ((size: number) => any) | undefined;
|
|
62
|
+
onReorder?: ((order: {
|
|
63
|
+
sort: RunSort;
|
|
64
|
+
direction: RunSortDirection;
|
|
65
|
+
}) => any) | undefined;
|
|
66
|
+
"onUpdate:page"?: ((value: number) => any) | undefined;
|
|
15
67
|
}>, {
|
|
68
|
+
sort: RunSort;
|
|
16
69
|
loading: boolean;
|
|
70
|
+
total: number;
|
|
71
|
+
direction: RunSortDirection;
|
|
72
|
+
triggerType: TriggerType | "";
|
|
73
|
+
perPage: number;
|
|
17
74
|
pagination: boolean;
|
|
18
75
|
showWorkflow: boolean;
|
|
76
|
+
serverMode: boolean;
|
|
19
77
|
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
20
78
|
export default _default;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { computed, ref, watch } from "vue";
|
|
3
3
|
import { RefreshCircle as Loader2 } from "#solar-icons";
|
|
4
4
|
import { Badge, Button, Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@revenexx/studio";
|
|
5
|
+
import LoadingAnnouncement from "./LoadingAnnouncement.vue";
|
|
5
6
|
import { useIntegrationsCredentials } from "../../composables/useIntegrationsCredentials";
|
|
6
7
|
const open = defineModel("open", { type: Boolean, ...{ default: false } });
|
|
7
8
|
const emit = defineEmits(["created"]);
|
|
@@ -64,7 +65,15 @@ async function importAccount(account) {
|
|
|
64
65
|
</DialogDescription>
|
|
65
66
|
</DialogHeader>
|
|
66
67
|
|
|
67
|
-
|
|
68
|
+
<!-- Mounts with the dialog, like the one in the create dialog and with the same
|
|
69
|
+
limit written down there (PO-251). -->
|
|
70
|
+
<LoadingAnnouncement
|
|
71
|
+
:label="t('integration.credentials.sftpImport.title')"
|
|
72
|
+
:loading="sftp.loading.value && !activeAccounts.length"
|
|
73
|
+
:text="t('integration.credentials.sftpImport.state.loading')"
|
|
74
|
+
/>
|
|
75
|
+
|
|
76
|
+
<div v-if="sftp.loading.value && !activeAccounts.length" aria-hidden="true" class="space-y-2">
|
|
68
77
|
<div v-for="n in 3" :key="`sk-${n}`" class="h-9 rounded-md bg-black/[0.07] animate-pulse" />
|
|
69
78
|
</div>
|
|
70
79
|
<p v-else-if="!activeAccounts.length" class="py-4 text-center text-sm text-muted-foreground">
|
|
@@ -171,7 +171,7 @@ function onViewMouseDown(e) {
|
|
|
171
171
|
v-for="(c, i) in STICKY_COLORS"
|
|
172
172
|
:key="i"
|
|
173
173
|
type="button"
|
|
174
|
-
class="size-
|
|
174
|
+
class="size-6 rounded-full border transition hover:scale-110"
|
|
175
175
|
:class="(data.sticky.color ?? 0) === i ? 'ring-2 ring-black/25 ring-offset-1' : ''"
|
|
176
176
|
:style="{ backgroundColor: c.bg, borderColor: c.border }"
|
|
177
177
|
:title="t(c.nameKey)"
|
|
@@ -21,10 +21,12 @@ import {
|
|
|
21
21
|
} from "@revenexx/studio";
|
|
22
22
|
import { toast } from "vue-sonner";
|
|
23
23
|
import { TRIGGER_TEMPLATES } from "../../data/integrationNodeCatalog";
|
|
24
|
+
import LoadingAnnouncement from "./LoadingAnnouncement.vue";
|
|
24
25
|
import TemplateCard from "./TemplateCard.vue";
|
|
25
26
|
import WorkflowIconPicker from "./WorkflowIconPicker.vue";
|
|
26
27
|
import WorkflowTagsInput from "./WorkflowTagsInput.vue";
|
|
27
28
|
import { resolveLocalized } from "../../utils/localize";
|
|
29
|
+
import { errorReason } from "../../utils/apiError";
|
|
28
30
|
const open = defineModel("open", { type: Boolean, ...{ default: false } });
|
|
29
31
|
const t = useIntegrationsT();
|
|
30
32
|
const router = useRouter();
|
|
@@ -102,7 +104,7 @@ async function submit() {
|
|
|
102
104
|
open.value = false;
|
|
103
105
|
await router.push(`/integrations/workflows/${created.id}`);
|
|
104
106
|
} catch (e) {
|
|
105
|
-
error.value = e
|
|
107
|
+
error.value = errorReason(e, t("integration.workflows.create.error.generic"));
|
|
106
108
|
} finally {
|
|
107
109
|
submitting.value = false;
|
|
108
110
|
}
|
|
@@ -126,7 +128,7 @@ async function useTemplate(slug) {
|
|
|
126
128
|
open.value = false;
|
|
127
129
|
await router.push(`/integrations/workflows/${id}`);
|
|
128
130
|
} catch (e) {
|
|
129
|
-
toast.error(e
|
|
131
|
+
toast.error(errorReason(e, t("integration.workflows.create.toast.templateError")));
|
|
130
132
|
} finally {
|
|
131
133
|
instantiatingSlug.value = null;
|
|
132
134
|
}
|
|
@@ -143,6 +145,19 @@ async function useTemplate(slug) {
|
|
|
143
145
|
<!-- Switcher: the same TabBar the studio surfaces use. -->
|
|
144
146
|
<TabBar v-model="mode" :tabs="modeTabs" class="-mt-1 shrink-0" />
|
|
145
147
|
|
|
148
|
+
<!-- Beside the switcher rather than inside the template half, and that is the
|
|
149
|
+
whole point: the dialog opens on the blank half, so a region placed in the
|
|
150
|
+
other branch would be inserted at the moment the reader switches to it —
|
|
151
|
+
with the catalogue already on its way and its own text already set, which is
|
|
152
|
+
a live region nothing was listening to. Out here it is in the dialog from
|
|
153
|
+
the moment it opens, and the text changes under a reader who is already
|
|
154
|
+
there (PO-251). -->
|
|
155
|
+
<LoadingAnnouncement
|
|
156
|
+
:label="t('integration.workflows.create.tab.template')"
|
|
157
|
+
:loading="mode === 'template' && templatesLoading && !templates.length"
|
|
158
|
+
:text="t('integration.workflows.create.template.state.loading')"
|
|
159
|
+
/>
|
|
160
|
+
|
|
146
161
|
<!-- ── Blank ─────────────────────────────────────────────────────────── -->
|
|
147
162
|
<!-- -m-1/p-1 keeps the selection rings clear of the scroll clipping. -->
|
|
148
163
|
<form v-if="mode === 'blank'" class="-m-1 max-h-[65vh] space-y-3 overflow-y-auto p-1" @submit.prevent="submit">
|
|
@@ -261,7 +276,7 @@ async function useTemplate(slug) {
|
|
|
261
276
|
>
|
|
262
277
|
</div>
|
|
263
278
|
|
|
264
|
-
<div v-if="templatesLoading && !templates.length" class="grid gap-2 p-1 sm:grid-cols-2">
|
|
279
|
+
<div v-if="templatesLoading && !templates.length" aria-hidden="true" class="grid gap-2 p-1 sm:grid-cols-2">
|
|
265
280
|
<div v-for="n in 6" :key="`sk-tpl-${n}`" class="rounded-lg border border-black/[0.06] bg-white p-3 space-y-2">
|
|
266
281
|
<div class="flex items-center gap-2">
|
|
267
282
|
<div class="size-7 rounded-md bg-black/[0.07] animate-pulse" />
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export type DeletableWorkflow = {
|
|
2
|
+
id: number;
|
|
3
|
+
name: string;
|
|
4
|
+
};
|
|
5
|
+
type __VLS_Props = {
|
|
6
|
+
workflow: DeletableWorkflow | null;
|
|
7
|
+
};
|
|
8
|
+
type __VLS_PublicProps = __VLS_Props & {
|
|
9
|
+
'open'?: boolean;
|
|
10
|
+
};
|
|
11
|
+
declare const _default: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
12
|
+
"update:open": (value: boolean) => any;
|
|
13
|
+
deleted: (workflow: DeletableWorkflow) => any;
|
|
14
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
15
|
+
"onUpdate:open"?: ((value: boolean) => any) | undefined;
|
|
16
|
+
onDeleted?: ((workflow: DeletableWorkflow) => any) | undefined;
|
|
17
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
18
|
+
export default _default;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { 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
|
+
} from "@revenexx/studio";
|
|
13
|
+
import { useIntegrationsWorkflows } from "../../composables/useIntegrationsWorkflows";
|
|
14
|
+
import { errorReason } from "../../utils/apiError";
|
|
15
|
+
const open = defineModel("open", { type: Boolean, ...{ default: false } });
|
|
16
|
+
const props = defineProps({
|
|
17
|
+
workflow: { type: [Object, null], required: true }
|
|
18
|
+
});
|
|
19
|
+
const emit = defineEmits(["deleted"]);
|
|
20
|
+
const t = useIntegrationsT();
|
|
21
|
+
const { remove } = useIntegrationsWorkflows();
|
|
22
|
+
const busy = ref(false);
|
|
23
|
+
const error = ref(null);
|
|
24
|
+
watch(open, (o) => {
|
|
25
|
+
if (o) error.value = null;
|
|
26
|
+
});
|
|
27
|
+
async function confirm() {
|
|
28
|
+
const wf = props.workflow;
|
|
29
|
+
if (!wf || busy.value) return;
|
|
30
|
+
busy.value = true;
|
|
31
|
+
error.value = null;
|
|
32
|
+
try {
|
|
33
|
+
await remove(wf.id);
|
|
34
|
+
open.value = false;
|
|
35
|
+
emit("deleted", wf);
|
|
36
|
+
} catch (e) {
|
|
37
|
+
error.value = errorReason(e, t("integration.workflows.delete.error.generic"));
|
|
38
|
+
} finally {
|
|
39
|
+
busy.value = false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
</script>
|
|
43
|
+
|
|
44
|
+
<template>
|
|
45
|
+
<AlertDialog :open="open" @update:open="(v) => {
|
|
46
|
+
if (!v && !busy) open = false;
|
|
47
|
+
}">
|
|
48
|
+
<AlertDialogContent>
|
|
49
|
+
<AlertDialogHeader>
|
|
50
|
+
<AlertDialogTitle>{{ t("integration.workflows.delete.dialog.title") }}</AlertDialogTitle>
|
|
51
|
+
<AlertDialogDescription>
|
|
52
|
+
<span v-if="props.workflow">
|
|
53
|
+
<span class="font-medium text-foreground">{{ props.workflow.name }}</span>
|
|
54
|
+
{{ t("integration.workflows.delete.dialog.description") }}
|
|
55
|
+
</span>
|
|
56
|
+
</AlertDialogDescription>
|
|
57
|
+
</AlertDialogHeader>
|
|
58
|
+
<p v-if="error" class="text-sm text-rose-600">{{ error }}</p>
|
|
59
|
+
<AlertDialogFooter>
|
|
60
|
+
<Button variant="outline" :disabled="busy" @click="open = false">
|
|
61
|
+
{{ t("integration.common.action.cancel") }}
|
|
62
|
+
</Button>
|
|
63
|
+
<Button class="bg-destructive text-white hover:bg-destructive/90" :disabled="busy" @click="confirm">
|
|
64
|
+
<Loader2 v-if="busy" :size="14" class="animate-spin" />
|
|
65
|
+
{{ busy ? t("integration.workflows.delete.action.deleting") : t("integration.common.action.delete") }}
|
|
66
|
+
</Button>
|
|
67
|
+
</AlertDialogFooter>
|
|
68
|
+
</AlertDialogContent>
|
|
69
|
+
</AlertDialog>
|
|
70
|
+
</template>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export type DeletableWorkflow = {
|
|
2
|
+
id: number;
|
|
3
|
+
name: string;
|
|
4
|
+
};
|
|
5
|
+
type __VLS_Props = {
|
|
6
|
+
workflow: DeletableWorkflow | null;
|
|
7
|
+
};
|
|
8
|
+
type __VLS_PublicProps = __VLS_Props & {
|
|
9
|
+
'open'?: boolean;
|
|
10
|
+
};
|
|
11
|
+
declare const _default: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
12
|
+
"update:open": (value: boolean) => any;
|
|
13
|
+
deleted: (workflow: DeletableWorkflow) => any;
|
|
14
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
15
|
+
"onUpdate:open"?: ((value: boolean) => any) | undefined;
|
|
16
|
+
onDeleted?: ((workflow: DeletableWorkflow) => any) | undefined;
|
|
17
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
18
|
+
export default _default;
|
|
@@ -3,6 +3,7 @@ import { computed, ref, watch } from "vue";
|
|
|
3
3
|
import { RefreshCircle as Loader2 } from "#solar-icons";
|
|
4
4
|
import { Button, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, Input } from "@revenexx/studio";
|
|
5
5
|
import { useIntegrationsWorkflows } from "../../composables/useIntegrationsWorkflows";
|
|
6
|
+
import { errorReason } from "../../utils/apiError";
|
|
6
7
|
const open = defineModel("open", { type: Boolean, ...{ default: false } });
|
|
7
8
|
const props = defineProps({
|
|
8
9
|
workflow: { type: [Object, null], required: true }
|
|
@@ -29,7 +30,7 @@ async function submit() {
|
|
|
29
30
|
open.value = false;
|
|
30
31
|
emit("duplicated", copy);
|
|
31
32
|
} catch (e) {
|
|
32
|
-
error.value = e
|
|
33
|
+
error.value = errorReason(e, t("integration.workflows.duplicate.error.generic"));
|
|
33
34
|
} finally {
|
|
34
35
|
busy.value = false;
|
|
35
36
|
}
|
|
@@ -146,20 +146,20 @@ function spliceNode() {
|
|
|
146
146
|
>
|
|
147
147
|
<button
|
|
148
148
|
type="button"
|
|
149
|
-
class="flex size-
|
|
149
|
+
class="flex size-6 items-center justify-center rounded text-muted-foreground transition hover:bg-muted/60 hover:text-foreground"
|
|
150
150
|
:title="t('integration.canvas.edge.action.insertNode')"
|
|
151
151
|
@click.stop="spliceNode"
|
|
152
152
|
>
|
|
153
|
-
<Plus :size="
|
|
153
|
+
<Plus :size="13" />
|
|
154
154
|
</button>
|
|
155
155
|
<div class="h-3.5 w-px bg-black/10" />
|
|
156
156
|
<button
|
|
157
157
|
type="button"
|
|
158
|
-
class="flex size-
|
|
158
|
+
class="flex size-6 items-center justify-center rounded text-muted-foreground transition hover:bg-rose-50 hover:text-rose-600"
|
|
159
159
|
:title="t('integration.canvas.edge.action.delete')"
|
|
160
160
|
@click.stop="removeEdge"
|
|
161
161
|
>
|
|
162
|
-
<TrashBin :size="
|
|
162
|
+
<TrashBin :size="13" />
|
|
163
163
|
</button>
|
|
164
164
|
</div>
|
|
165
165
|
</EdgeLabelRenderer>
|