@revenexx/studio-integrations 2.0.0 → 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 +135 -0
- package/README.md +1 -0
- 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 +7 -26
- package/dist/runtime/components/Integrations/StickyNoteView.vue +1 -0
- package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +10 -6
- package/dist/runtime/components/Integrations/WorkflowEditor.vue +224 -225
- 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 +42 -0
- package/dist/runtime/i18n/messages/en.json +42 -0
- 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/index.vue +5 -10
- package/dist/runtime/pages/integrations/schedules.vue +13 -1
- package/dist/runtime/pages/integrations/templates/index.vue +40 -40
- package/dist/runtime/pages/integrations/workflows/[id].vue +324 -244
- package/dist/runtime/types/integrations.d.ts +13 -0
- package/dist/runtime/utils/credentialState.d.ts +0 -7
- package/dist/runtime/utils/credentialState.js +0 -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 +8 -3
|
@@ -4,7 +4,8 @@ import { AltArrowLeft as ArrowLeft, Bolt as Zap, Magnifier as Search } from "#so
|
|
|
4
4
|
import { triggerGlyph } from "../../data/triggerGlyphs";
|
|
5
5
|
import * as Lucide from "lucide-vue-next";
|
|
6
6
|
import { INTEGRATION_NODE_CATALOG, NODE_CATEGORIES, TRIGGER_TEMPLATES } from "../../data/integrationNodeCatalog";
|
|
7
|
-
import { resolveLocalized } from "../../utils/localize";
|
|
7
|
+
import { resolveLocalized, studioLocale } from "../../utils/localize";
|
|
8
|
+
import { orderFolderLeaves, orderFolders } from "../../utils/paletteOrder";
|
|
8
9
|
const { load: loadNodes } = useIntegrationsNodes();
|
|
9
10
|
onMounted(() => {
|
|
10
11
|
void loadNodes();
|
|
@@ -14,29 +15,39 @@ const emit = defineEmits(["insert"]);
|
|
|
14
15
|
const search = ref("");
|
|
15
16
|
const searchInput = ref(null);
|
|
16
17
|
defineExpose({ focusSearch: () => searchInput.value?.focus() });
|
|
17
|
-
const
|
|
18
|
+
const CORE_FOLDERS = {
|
|
19
|
+
Flow: { message: "integration.canvas.palette.folder.flow", icon: "Split", color: "bg-slate-100 text-rose-600/80" },
|
|
20
|
+
Data: { message: "integration.canvas.palette.folder.data", icon: "Database", color: "bg-slate-100 text-amber-700/80" },
|
|
21
|
+
"Text & Formats": { message: "integration.canvas.palette.folder.textAndFormats", icon: "Type", color: "bg-slate-100 text-orange-600/80" },
|
|
22
|
+
Files: { message: "integration.canvas.palette.folder.files", icon: "Files", color: "bg-slate-100 text-yellow-700/80" },
|
|
23
|
+
Web: { message: "integration.canvas.palette.folder.web", icon: "Globe", color: "bg-slate-100 text-sky-600/80" },
|
|
24
|
+
Messaging: { message: "integration.canvas.palette.folder.messaging", icon: "MessageCircle", color: "bg-slate-100 text-violet-600/80" },
|
|
25
|
+
"FTP / SFTP": { message: "integration.canvas.palette.folder.ftpSftp", icon: "FolderSync", color: "bg-slate-100 text-teal-600/80" },
|
|
26
|
+
Actions: { message: "integration.canvas.palette.folder.actions", icon: "Wand2", color: "bg-slate-100 text-sky-600/80" },
|
|
27
|
+
Other: { message: "integration.canvas.palette.folder.misc", icon: "FolderOpen", color: "bg-slate-100 text-slate-500" }
|
|
28
|
+
};
|
|
29
|
+
const CORE_FOLDER_BY_CATEGORY = {
|
|
18
30
|
control: "Flow",
|
|
19
31
|
transform: "Data",
|
|
20
32
|
io: "Data",
|
|
21
|
-
action: "Actions"
|
|
22
|
-
trigger: "Triggers"
|
|
33
|
+
action: "Actions"
|
|
23
34
|
};
|
|
24
35
|
const CORE_LABEL_ALIAS = {
|
|
25
36
|
"Flow control": "Flow",
|
|
26
|
-
"Files": "
|
|
27
|
-
|
|
28
|
-
"Network": "Messaging"
|
|
29
|
-
};
|
|
30
|
-
const CORE_GROUP_META = {
|
|
31
|
-
"Data": { icon: "Database", color: "bg-slate-100 text-amber-700/80" },
|
|
32
|
-
"Flow": { icon: "Split", color: "bg-slate-100 text-rose-600/80" },
|
|
33
|
-
"Messaging": { icon: "MessageCircle", color: "bg-slate-100 text-violet-600/80" },
|
|
34
|
-
"Actions": { icon: "Wand2", color: "bg-slate-100 text-sky-600/80" }
|
|
37
|
+
"Files & I/O": "Files",
|
|
38
|
+
Network: "Web"
|
|
35
39
|
};
|
|
36
|
-
function coreGroupIcon(
|
|
37
|
-
const meta =
|
|
40
|
+
function coreGroupIcon(key) {
|
|
41
|
+
const meta = key ? CORE_FOLDERS[key] : void 0;
|
|
38
42
|
return meta && Lucide[meta.icon] || Lucide.FolderOpen;
|
|
39
43
|
}
|
|
44
|
+
function coreFolder(m) {
|
|
45
|
+
const own = m.groups?.[0];
|
|
46
|
+
const ownKey = own ? resolveLocalized(own, "en") : "";
|
|
47
|
+
const key = ownKey && (CORE_LABEL_ALIAS[ownKey] ?? ownKey) || CORE_FOLDER_BY_CATEGORY[m.category] || "Other";
|
|
48
|
+
const known = CORE_FOLDERS[key];
|
|
49
|
+
return { key, label: known ? t(known.message) : resolveLocalized(own) };
|
|
50
|
+
}
|
|
40
51
|
const VERB_PREFIX = /^(create|get|list|update|delete|search|send|sync|upsert|cancel)-(.+)$/;
|
|
41
52
|
function titleize(kebab) {
|
|
42
53
|
return kebab.split("-").map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join(" ");
|
|
@@ -58,7 +69,7 @@ function groupPath(m) {
|
|
|
58
69
|
if (m.groups?.length) {
|
|
59
70
|
const path = m.groups.map((g) => resolveLocalized(g)).filter(Boolean);
|
|
60
71
|
if (isIntegration(m) || !path.length) return path;
|
|
61
|
-
return [
|
|
72
|
+
return [coreFolder(m).label, ...path.slice(1)];
|
|
62
73
|
}
|
|
63
74
|
if (isAppPackage(m.__package)) {
|
|
64
75
|
const app = packageAppLabel(m.__package);
|
|
@@ -68,10 +79,10 @@ function groupPath(m) {
|
|
|
68
79
|
const verb = VERB_PREFIX.exec(rest);
|
|
69
80
|
return verb ? [app, titleize(verb[2])] : [app];
|
|
70
81
|
}
|
|
71
|
-
return [
|
|
82
|
+
return [coreFolder(m).label];
|
|
72
83
|
}
|
|
73
84
|
function isIntegration(m) {
|
|
74
|
-
return isAppPackage(m.__package) ||
|
|
85
|
+
return isAppPackage(m.__package) || hasLogo(m);
|
|
75
86
|
}
|
|
76
87
|
function makeGroup(label) {
|
|
77
88
|
return { label, children: /* @__PURE__ */ new Map(), leaves: [], count: 0 };
|
|
@@ -124,32 +135,36 @@ const tree = computed(() => {
|
|
|
124
135
|
const root = isIntegration(m) ? apps : core;
|
|
125
136
|
let cursor = root;
|
|
126
137
|
cursor.count++;
|
|
127
|
-
for (const label of path) {
|
|
128
|
-
if (!cursor.children.has(label))
|
|
138
|
+
for (const [depth, label] of path.entries()) {
|
|
139
|
+
if (!cursor.children.has(label)) {
|
|
140
|
+
const group = makeGroup(label);
|
|
141
|
+
if (root === core && depth === 0) group.key = coreFolder(m).key;
|
|
142
|
+
cursor.children.set(label, group);
|
|
143
|
+
}
|
|
129
144
|
cursor = cursor.children.get(label);
|
|
130
145
|
cursor.count++;
|
|
131
146
|
}
|
|
132
147
|
cursor.leaves.push(m);
|
|
133
148
|
}
|
|
149
|
+
const locale = studioLocale();
|
|
150
|
+
const order = (g) => {
|
|
151
|
+
g.leaves = orderFolderLeaves(g.leaves);
|
|
152
|
+
g.children = orderFolders(g.children, locale);
|
|
153
|
+
for (const child of g.children.values()) order(child);
|
|
154
|
+
};
|
|
155
|
+
order(apps);
|
|
156
|
+
order(core);
|
|
134
157
|
return { apps, core };
|
|
135
158
|
});
|
|
136
|
-
const appRows = computed(() =>
|
|
137
|
-
const rows = Array.from(tree.value.apps.children.entries());
|
|
138
|
-
const sftpIdx = rows.findIndex(([label]) => label === "SFTP");
|
|
139
|
-
const ftpIdx = rows.findIndex(([label]) => label === "FTP");
|
|
140
|
-
if (ftpIdx !== -1 && sftpIdx !== -1 && sftpIdx !== ftpIdx + 1) {
|
|
141
|
-
const [sftp] = rows.splice(sftpIdx, 1);
|
|
142
|
-
rows.splice(rows.findIndex(([label]) => label === "FTP") + 1, 0, sftp);
|
|
143
|
-
}
|
|
144
|
-
return rows;
|
|
145
|
-
});
|
|
159
|
+
const appRows = computed(() => Array.from(tree.value.apps.children.entries()));
|
|
146
160
|
function subtreeManifests(g) {
|
|
147
161
|
const out = [...g.leaves];
|
|
148
162
|
for (const child of g.children.values()) out.push(...subtreeManifests(child));
|
|
149
163
|
return out;
|
|
150
164
|
}
|
|
151
165
|
function groupLogoManifest(g) {
|
|
152
|
-
|
|
166
|
+
const all = subtreeManifests(g);
|
|
167
|
+
return all.find(hasLogo) ?? all.find((m) => (m.images ?? []).length > 0) ?? null;
|
|
153
168
|
}
|
|
154
169
|
const stack = ref([]);
|
|
155
170
|
const direction = ref("push");
|
|
@@ -259,10 +274,10 @@ function insertManifest(leaf) {
|
|
|
259
274
|
function insertTrigger(tpl) {
|
|
260
275
|
emit("insert", { kind: "trigger", type: tpl.type });
|
|
261
276
|
}
|
|
262
|
-
function categoryColor(
|
|
263
|
-
const meta =
|
|
277
|
+
function categoryColor(key) {
|
|
278
|
+
const meta = key ? CORE_FOLDERS[key] : void 0;
|
|
264
279
|
if (meta) return meta.color;
|
|
265
|
-
const cat = NODE_CATEGORIES.find((c) =>
|
|
280
|
+
const cat = NODE_CATEGORIES.find((c) => CORE_FOLDER_BY_CATEGORY[String(c.key)] === key);
|
|
266
281
|
return cat?.color ?? "bg-slate-100 text-slate-500";
|
|
267
282
|
}
|
|
268
283
|
function nodeCategoryColor(m) {
|
|
@@ -397,7 +412,7 @@ const searching = computed(() => search.value.trim().length > 0);
|
|
|
397
412
|
draggable="true"
|
|
398
413
|
:title="resolveLocalized(tpl.name)"
|
|
399
414
|
@dragstart="onTriggerDragStart(tpl, $event)"
|
|
400
|
-
|
|
415
|
+
@dragend="onDragEnd"
|
|
401
416
|
@click="insertTrigger(tpl)"
|
|
402
417
|
>
|
|
403
418
|
<span class="flex size-6 shrink-0 items-center justify-center rounded-md bg-emerald-100 text-emerald-600">
|
|
@@ -434,8 +449,8 @@ const searching = computed(() => search.value.trim().length > 0);
|
|
|
434
449
|
class="flex w-full items-center gap-2 rounded-md border border-transparent px-2 py-1.5 text-left transition hover:border-black/[0.08] hover:bg-muted/40"
|
|
435
450
|
@click="pushPath(['__core', label])"
|
|
436
451
|
>
|
|
437
|
-
<span class="flex size-6 shrink-0 items-center justify-center rounded-md" :class="categoryColor(
|
|
438
|
-
<component :is="coreGroupIcon(
|
|
452
|
+
<span class="flex size-6 shrink-0 items-center justify-center rounded-md" :class="categoryColor(group.key)">
|
|
453
|
+
<component :is="coreGroupIcon(group.key)" :size="13" />
|
|
439
454
|
</span>
|
|
440
455
|
<span class="min-w-0 flex-1 truncate text-sm font-medium">{{ label }}</span>
|
|
441
456
|
<span class="text-sm tabular-nums text-muted-foreground">{{ group.count }}</span>
|
|
@@ -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,5 +1,5 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import { computed,
|
|
2
|
+
import { computed, ref } from "vue";
|
|
3
3
|
import {
|
|
4
4
|
Badge,
|
|
5
5
|
Select,
|
|
@@ -11,6 +11,8 @@ import {
|
|
|
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);
|
|
@@ -118,10 +99,10 @@ const sortedColumn = computed(() => {
|
|
|
118
99
|
return entry ? entry[0] : null;
|
|
119
100
|
});
|
|
120
101
|
const serverModels = computed(() => props.serverMode ? {
|
|
121
|
-
|
|
102
|
+
sort: tableSort.value,
|
|
122
103
|
"onUpdate:sort": onTableSort,
|
|
123
104
|
// 1-based here, as the platform pages; the shared table counts from zero.
|
|
124
|
-
|
|
105
|
+
page: page.value - 1,
|
|
125
106
|
"onUpdate:page": (p) => {
|
|
126
107
|
page.value = p + 1;
|
|
127
108
|
}
|
|
@@ -264,7 +245,7 @@ const columns = computed(() => [
|
|
|
264
245
|
</template>
|
|
265
246
|
|
|
266
247
|
<template #cell-started="{ row: run }">
|
|
267
|
-
<span class="text-sm text-muted-foreground tabular-nums">{{
|
|
248
|
+
<span class="text-sm text-muted-foreground tabular-nums">{{ relativeTime(run.startedAt, now) }}</span>
|
|
268
249
|
</template>
|
|
269
250
|
|
|
270
251
|
<template #cell-duration="{ row: run }">
|
|
@@ -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
|