mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-22 04:10:01 +00:00
refactor(editor): Move editor-ui and design-system to frontend dir (no-changelog) (#13564)
This commit is contained in:
@@ -0,0 +1,274 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Ref } from 'vue';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import {
|
||||
type AIResult,
|
||||
createAiData,
|
||||
getReferencedData,
|
||||
getTreeNodeData,
|
||||
type TreeNode,
|
||||
} from '@/components/RunDataAi/utils';
|
||||
import type { IAiData, INodeUi } from '@/Interface';
|
||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import NodeIcon from '@/components/NodeIcon.vue';
|
||||
import RunDataAiContent from './RunDataAiContent.vue';
|
||||
import { ElTree } from 'element-plus';
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
import type { Workflow } from 'n8n-workflow';
|
||||
|
||||
export interface Props {
|
||||
node: INodeUi;
|
||||
runIndex?: number;
|
||||
slim?: boolean;
|
||||
workflow: Workflow;
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), { runIndex: 0 });
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const nodeTypesStore = useNodeTypesStore();
|
||||
const selectedRun: Ref<IAiData[]> = ref([]);
|
||||
|
||||
const i18n = useI18n();
|
||||
|
||||
function isTreeNodeSelected(node: TreeNode) {
|
||||
return selectedRun.value.some((run) => run.node === node.node && run.runIndex === node.runIndex);
|
||||
}
|
||||
|
||||
function toggleTreeItem(node: { expanded: boolean }) {
|
||||
node.expanded = !node.expanded;
|
||||
}
|
||||
|
||||
function onItemClick(data: TreeNode) {
|
||||
const matchingRun = aiData.value?.find(
|
||||
(run) => run.node === data.node && run.runIndex === data.runIndex,
|
||||
);
|
||||
if (!matchingRun) {
|
||||
selectedRun.value = [];
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedNodeRun = workflowsStore.getWorkflowResultDataByNodeName(data.node)?.[
|
||||
data.runIndex
|
||||
];
|
||||
if (!selectedNodeRun) {
|
||||
return;
|
||||
}
|
||||
selectedRun.value = [
|
||||
{
|
||||
node: data.node,
|
||||
runIndex: data.runIndex,
|
||||
data: getReferencedData(selectedNodeRun, true, true),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function getNodeType(nodeName: string) {
|
||||
const node = workflowsStore.getNodeByName(nodeName);
|
||||
if (!node) {
|
||||
return null;
|
||||
}
|
||||
const nodeType = nodeTypesStore.getNodeType(node?.type);
|
||||
|
||||
return nodeType;
|
||||
}
|
||||
|
||||
function selectFirst() {
|
||||
if (executionTree.value.length && executionTree.value[0].children.length) {
|
||||
onItemClick(executionTree.value[0].children[0]);
|
||||
}
|
||||
}
|
||||
|
||||
const aiData = computed<AIResult[]>(() =>
|
||||
createAiData(props.node.name, props.workflow, workflowsStore.getWorkflowResultDataByNodeName),
|
||||
);
|
||||
|
||||
const executionTree = computed<TreeNode[]>(() =>
|
||||
getTreeNodeData(props.node.name, props.workflow, aiData.value),
|
||||
);
|
||||
|
||||
watch(() => props.runIndex, selectFirst, { immediate: true });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="$style.container">
|
||||
<template v-if="aiData.length > 0">
|
||||
<div :class="{ [$style.tree]: true, [$style.slim]: slim }">
|
||||
<ElTree
|
||||
:data="executionTree"
|
||||
:props="{ label: 'node' }"
|
||||
default-expand-all
|
||||
:indent="12"
|
||||
:expand-on-click-node="false"
|
||||
data-test-id="lm-chat-logs-tree"
|
||||
@node-click="onItemClick"
|
||||
>
|
||||
<template #default="{ node, data }">
|
||||
<div
|
||||
:class="{
|
||||
[$style.treeNode]: true,
|
||||
[$style.isSelected]: isTreeNodeSelected(data),
|
||||
}"
|
||||
:data-tree-depth="data.depth"
|
||||
:style="{ '--item-depth': data.depth }"
|
||||
>
|
||||
<button
|
||||
v-if="data.children.length"
|
||||
:class="$style.treeToggle"
|
||||
@click="toggleTreeItem(node)"
|
||||
>
|
||||
<font-awesome-icon :icon="node.expanded ? 'angle-down' : 'angle-right'" />
|
||||
</button>
|
||||
<n8n-tooltip :disabled="!slim" placement="right">
|
||||
<template #content>
|
||||
{{ node.label }}
|
||||
</template>
|
||||
<span :class="$style.leafLabel">
|
||||
<NodeIcon
|
||||
:node-type="getNodeType(data.node)!"
|
||||
:size="17"
|
||||
:class="$style.nodeIcon"
|
||||
/>
|
||||
<span v-if="!slim" v-text="node.label" />
|
||||
</span>
|
||||
</n8n-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
</ElTree>
|
||||
</div>
|
||||
<div :class="$style.runData">
|
||||
<div v-if="selectedRun.length === 0" :class="$style.empty">
|
||||
<n8n-text size="large">
|
||||
{{
|
||||
i18n.baseText('ndv.output.ai.empty', {
|
||||
interpolate: {
|
||||
node: props.node.name,
|
||||
},
|
||||
})
|
||||
}}
|
||||
</n8n-text>
|
||||
</div>
|
||||
<div
|
||||
v-for="(data, index) in selectedRun"
|
||||
:key="`${data.node}__${data.runIndex}__index`"
|
||||
data-test-id="lm-chat-logs-entry"
|
||||
>
|
||||
<RunDataAiContent :input-data="data" :content-index="index" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else :class="$style.noData">{{ i18n.baseText('ndv.output.ai.waiting') }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" module>
|
||||
.treeToggle {
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
padding: 0 var(--spacing-3xs);
|
||||
margin: 0 calc(-1 * var(--spacing-3xs));
|
||||
cursor: pointer;
|
||||
}
|
||||
.leafLabel {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-3xs);
|
||||
}
|
||||
.noData {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
color: var(--color-text-light);
|
||||
}
|
||||
.empty {
|
||||
padding: var(--spacing-l);
|
||||
}
|
||||
.title {
|
||||
font-size: var(--font-size-s);
|
||||
margin-bottom: var(--spacing-xs);
|
||||
}
|
||||
.tree {
|
||||
flex-shrink: 0;
|
||||
min-width: 8rem;
|
||||
height: 100%;
|
||||
|
||||
padding-right: var(--spacing-xs);
|
||||
padding-left: var(--spacing-2xs);
|
||||
&.slim {
|
||||
min-width: auto;
|
||||
}
|
||||
}
|
||||
.runData {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
.container {
|
||||
height: 100%;
|
||||
padding: 0 var(--spacing-xs);
|
||||
display: flex;
|
||||
|
||||
:global(.el-tree > .el-tree-node) {
|
||||
position: relative;
|
||||
&:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 2rem;
|
||||
bottom: 1.2rem;
|
||||
left: 0.75rem;
|
||||
width: 0.125rem;
|
||||
background-color: var(--color-foreground-base);
|
||||
}
|
||||
}
|
||||
:global(.el-tree-node__expand-icon) {
|
||||
display: none;
|
||||
}
|
||||
:global(.el-tree) {
|
||||
margin-left: calc(-1 * var(--spacing-xs));
|
||||
}
|
||||
:global(.el-tree-node__content) {
|
||||
margin-left: var(--spacing-xs);
|
||||
}
|
||||
}
|
||||
.nodeIcon {
|
||||
padding: var(--spacing-3xs) var(--spacing-3xs);
|
||||
border-radius: var(--border-radius-base);
|
||||
margin-right: var(--spacing-4xs);
|
||||
}
|
||||
.isSelected {
|
||||
background-color: var(--color-foreground-base);
|
||||
}
|
||||
.treeNode {
|
||||
display: inline-flex;
|
||||
border-radius: var(--border-radius-base);
|
||||
align-items: center;
|
||||
padding-right: var(--spacing-3xs);
|
||||
margin: var(--spacing-4xs) 0;
|
||||
font-size: var(--font-size-2xs);
|
||||
color: var(--color-text-dark);
|
||||
margin-bottom: var(--spacing-3xs);
|
||||
cursor: pointer;
|
||||
|
||||
&.isSelected {
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
&:hover {
|
||||
background-color: var(--color-foreground-base);
|
||||
}
|
||||
&[data-tree-depth='0'] {
|
||||
margin-left: calc(-1 * var(--spacing-2xs));
|
||||
}
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
margin: auto;
|
||||
background-color: var(--color-foreground-base);
|
||||
height: 0.125rem;
|
||||
left: 0.75rem;
|
||||
width: calc(var(--item-depth) * 0.625rem);
|
||||
margin-top: var(--spacing-3xs);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user