mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-20 03:12:15 +00:00
feat(editor): Add workflow evaluation run views (no-changelog) (#12258)
This commit is contained in:
@@ -0,0 +1,194 @@
|
||||
<script setup lang="ts">
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||
import { computed, onMounted, ref, useCssModule } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useCanvasOperations } from '@/composables/useCanvasOperations';
|
||||
import { useCanvasMapping } from '@/composables/useCanvasMapping';
|
||||
import { createEventBus, N8nTooltip } from 'n8n-design-system';
|
||||
import type { CanvasConnectionPort, CanvasEventBusEvents, CanvasNodeData } from '@/types';
|
||||
import { useVueFlow } from '@vue-flow/core';
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const nodeTypesStore = useNodeTypesStore();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const locale = useI18n();
|
||||
|
||||
const { resetWorkspace, initializeWorkspace } = useCanvasOperations({ router });
|
||||
|
||||
const eventBus = createEventBus<CanvasEventBusEvents>();
|
||||
const style = useCssModule();
|
||||
const uuid = crypto.randomUUID();
|
||||
const props = defineProps<{
|
||||
modelValue: Array<{ name: string }>;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: Array<{ name: string }>];
|
||||
}>();
|
||||
|
||||
const isLoading = ref(true);
|
||||
|
||||
const workflowId = computed(() => route.params.name as string);
|
||||
const testId = computed(() => route.params.testId as string);
|
||||
const workflow = computed(() => workflowsStore.getWorkflowById(workflowId.value));
|
||||
const workflowObject = computed(() => workflowsStore.getCurrentWorkflow(true));
|
||||
const canvasId = computed(() => `${uuid}-${testId.value}`);
|
||||
|
||||
const { onNodesInitialized, fitView, zoomTo } = useVueFlow({ id: canvasId.value });
|
||||
const nodes = computed(() => {
|
||||
return workflow.value.nodes ?? [];
|
||||
});
|
||||
const connections = computed(() => workflow.value.connections);
|
||||
|
||||
const { nodes: mappedNodes, connections: mappedConnections } = useCanvasMapping({
|
||||
nodes,
|
||||
connections,
|
||||
workflowObject,
|
||||
});
|
||||
async function loadData() {
|
||||
workflowsStore.resetState();
|
||||
resetWorkspace();
|
||||
const loadingPromise = Promise.all([
|
||||
nodeTypesStore.getNodeTypes(),
|
||||
workflowsStore.fetchWorkflow(workflowId.value),
|
||||
]);
|
||||
await loadingPromise;
|
||||
initializeWorkspace(workflow.value);
|
||||
disableAllNodes();
|
||||
}
|
||||
function getNodeNameById(id: string) {
|
||||
return mappedNodes.value.find((node) => node.id === id)?.data?.name;
|
||||
}
|
||||
function updateNodeClasses(nodeIds: string[], isPinned: boolean) {
|
||||
eventBus.emit('nodes:action', {
|
||||
ids: nodeIds,
|
||||
action: 'update:node:class',
|
||||
payload: {
|
||||
className: style.pinnedNode,
|
||||
add: isPinned,
|
||||
},
|
||||
});
|
||||
eventBus.emit('nodes:action', {
|
||||
ids: nodeIds,
|
||||
action: 'update:node:class',
|
||||
payload: {
|
||||
className: style.notPinnedNode,
|
||||
add: !isPinned,
|
||||
},
|
||||
});
|
||||
}
|
||||
function disableAllNodes() {
|
||||
const ids = mappedNodes.value.map((node) => node.id);
|
||||
updateNodeClasses(ids, false);
|
||||
|
||||
const pinnedNodes = props.modelValue
|
||||
.map((node) => {
|
||||
const matchedNode = mappedNodes.value.find(
|
||||
(mappedNode) => mappedNode?.data?.name === node.name,
|
||||
);
|
||||
return matchedNode?.id ?? null;
|
||||
})
|
||||
.filter((n) => n !== null);
|
||||
|
||||
if (pinnedNodes.length > 0) {
|
||||
updateNodeClasses(pinnedNodes, true);
|
||||
}
|
||||
}
|
||||
function onPinButtonClick(data: CanvasNodeData) {
|
||||
const nodeName = getNodeNameById(data.id);
|
||||
if (!nodeName) return;
|
||||
|
||||
const isPinned = props.modelValue.some((node) => node.name === nodeName);
|
||||
const updatedNodes = isPinned
|
||||
? props.modelValue.filter((node) => node.name !== nodeName)
|
||||
: [...props.modelValue, { name: nodeName }];
|
||||
|
||||
emit('update:modelValue', updatedNodes);
|
||||
updateNodeClasses([data.id], !isPinned);
|
||||
}
|
||||
function isPinButtonVisible(outputs: CanvasConnectionPort[]) {
|
||||
return outputs.length === 1;
|
||||
}
|
||||
|
||||
onNodesInitialized(async () => {
|
||||
await fitView();
|
||||
isLoading.value = false;
|
||||
await zoomTo(0.7, { duration: 400 });
|
||||
});
|
||||
onMounted(loadData);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="$style.container">
|
||||
<N8nSpinner v-if="isLoading" size="xlarge" type="dots" :class="$style.spinner" />
|
||||
<Canvas
|
||||
:id="canvasId"
|
||||
:loading="isLoading"
|
||||
:class="{ [$style.canvas]: true }"
|
||||
:nodes="mappedNodes"
|
||||
:connections="mappedConnections"
|
||||
:show-bug-reporting-button="false"
|
||||
:read-only="true"
|
||||
:event-bus="eventBus"
|
||||
>
|
||||
<template #nodeToolbar="{ data, outputs }">
|
||||
<div :class="$style.pinButtonContainer">
|
||||
<N8nTooltip v-if="isPinButtonVisible(outputs)" placement="left">
|
||||
<template #content>
|
||||
{{ locale.baseText('testDefinition.edit.nodesPinning.pinButtonTooltip') }}
|
||||
</template>
|
||||
<n8n-icon-button
|
||||
type="tertiary"
|
||||
size="large"
|
||||
icon="thumbtack"
|
||||
:class="$style.pinButton"
|
||||
@click="onPinButtonClick(data)"
|
||||
/>
|
||||
</N8nTooltip>
|
||||
</div>
|
||||
</template>
|
||||
</Canvas>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" module>
|
||||
.container {
|
||||
width: 100vw;
|
||||
height: 100%;
|
||||
}
|
||||
.pinButtonContainer {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
bottom: 100%;
|
||||
}
|
||||
|
||||
.pinButton {
|
||||
cursor: pointer;
|
||||
color: var(--canvas-node--border-color);
|
||||
border: none;
|
||||
}
|
||||
.notPinnedNode,
|
||||
.pinnedNode {
|
||||
:global(.n8n-node-icon) > div {
|
||||
filter: contrast(40%) brightness(1.5) grayscale(100%);
|
||||
}
|
||||
}
|
||||
.pinnedNode {
|
||||
--canvas-node--border-color: hsla(247, 49%, 55%, 1);
|
||||
|
||||
:global(.n8n-node-icon) > div {
|
||||
filter: contrast(40%) brightness(1.5) grayscale(100%);
|
||||
}
|
||||
}
|
||||
.spinner {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user