fix(editor): Cannot expand sub execution log if it finished with an error (#16236)

Co-authored-by: Csaba Tuncsik <csaba.tuncsik@gmail.com>
This commit is contained in:
Suguru Inoue
2025-06-16 12:09:45 +02:00
committed by GitHub
parent 701816aae1
commit 3fcabd40b3
6 changed files with 77 additions and 22 deletions

View File

@@ -31,7 +31,6 @@
"@codemirror/view": "^6.26.3", "@codemirror/view": "^6.26.3",
"@dagrejs/dagre": "^1.1.4", "@dagrejs/dagre": "^1.1.4",
"@lezer/common": "1.1.0", "@lezer/common": "1.1.0",
"@n8n/rest-api-client": "workspace:*",
"@n8n/api-types": "workspace:*", "@n8n/api-types": "workspace:*",
"@n8n/chat": "workspace:*", "@n8n/chat": "workspace:*",
"@n8n/codemirror-lang": "workspace:*", "@n8n/codemirror-lang": "workspace:*",
@@ -41,6 +40,7 @@
"@n8n/design-system": "workspace:*", "@n8n/design-system": "workspace:*",
"@n8n/i18n": "workspace:*", "@n8n/i18n": "workspace:*",
"@n8n/permissions": "workspace:*", "@n8n/permissions": "workspace:*",
"@n8n/rest-api-client": "workspace:*",
"@n8n/stores": "workspace:*", "@n8n/stores": "workspace:*",
"@n8n/utils": "workspace:*", "@n8n/utils": "workspace:*",
"@replit/codemirror-indentation-markers": "^6.5.3", "@replit/codemirror-indentation-markers": "^6.5.3",

View File

@@ -8,7 +8,7 @@ import { useI18n } from '@n8n/i18n';
import { I18nT } from 'vue-i18n'; import { I18nT } from 'vue-i18n';
import { toDayMonth, toTime } from '@/utils/formatters/dateFormatter'; import { toDayMonth, toTime } from '@/utils/formatters/dateFormatter';
import LogsViewNodeName from '@/features/logs/components/LogsViewNodeName.vue'; import LogsViewNodeName from '@/features/logs/components/LogsViewNodeName.vue';
import { getSubtreeTotalConsumedTokens } from '@/features/logs/logs.utils'; import { getSubtreeTotalConsumedTokens, hasSubExecution } from '@/features/logs/logs.utils';
import { useTimestamp } from '@vueuse/core'; import { useTimestamp } from '@vueuse/core';
import type { LatestNodeInfo, LogEntry } from '@/features/logs/logs.types'; import type { LatestNodeInfo, LogEntry } from '@/features/logs/logs.types';
@@ -70,9 +70,7 @@ const subtreeConsumedTokens = computed(() =>
props.shouldShowTokenCountColumn ? getSubtreeTotalConsumedTokens(props.data, false) : undefined, props.shouldShowTokenCountColumn ? getSubtreeTotalConsumedTokens(props.data, false) : undefined,
); );
const hasChildren = computed( const hasChildren = computed(() => props.data.children.length > 0 || hasSubExecution(props.data));
() => props.data.children.length > 0 || !!props.data.runData?.metadata?.subExecution,
);
function isLastChild(level: number) { function isLastChild(level: number) {
let parent = props.data.parent; let parent = props.data.parent;

View File

@@ -4,7 +4,12 @@ import { Workflow, type IRunExecutionData } from 'n8n-workflow';
import { useWorkflowsStore } from '@/stores/workflows.store'; import { useWorkflowsStore } from '@/stores/workflows.store';
import { useNodeHelpers } from '@/composables/useNodeHelpers'; import { useNodeHelpers } from '@/composables/useNodeHelpers';
import { useThrottleFn } from '@vueuse/core'; import { useThrottleFn } from '@vueuse/core';
import { createLogTree, deepToRaw, mergeStartData } from '@/features/logs/logs.utils'; import {
createLogTree,
deepToRaw,
findSubExecutionLocator,
mergeStartData,
} from '@/features/logs/logs.utils';
import { parse } from 'flatted'; import { parse } from 'flatted';
import { useToast } from '@/composables/useToast'; import { useToast } from '@/composables/useToast';
import type { LatestNodeInfo, LogEntry } from '../logs.types'; import type { LatestNodeInfo, LogEntry } from '../logs.types';
@@ -68,15 +73,14 @@ export function useLogsExecutionData() {
} }
async function loadSubExecution(logEntry: LogEntry) { async function loadSubExecution(logEntry: LogEntry) {
const executionId = logEntry.runData?.metadata?.subExecution?.executionId; const locator = findSubExecutionLocator(logEntry);
const workflowId = logEntry.runData?.metadata?.subExecution?.workflowId;
if (!execData.value?.data || !executionId || !workflowId) { if (!execData.value?.data || locator === undefined) {
return; return;
} }
try { try {
const subExecution = await workflowsStore.fetchExecutionDataById(executionId); const subExecution = await workflowsStore.fetchExecutionDataById(locator.executionId);
const data = subExecution?.data const data = subExecution?.data
? (parse(subExecution.data as unknown as string) as IRunExecutionData) ? (parse(subExecution.data as unknown as string) as IRunExecutionData)
: undefined; : undefined;
@@ -85,8 +89,8 @@ export function useLogsExecutionData() {
throw Error('Data is missing'); throw Error('Data is missing');
} }
subWorkflowExecData.value[executionId] = data; subWorkflowExecData.value[locator.executionId] = data;
subWorkflows.value[workflowId] = new Workflow({ subWorkflows.value[locator.workflowId] = new Workflow({
...subExecution.workflowData, ...subExecution.workflowData,
nodeTypes: workflowsStore.getNodeTypes(), nodeTypes: workflowsStore.getNodeTypes(),
}); });

View File

@@ -9,6 +9,7 @@ import {
createLogTree, createLogTree,
deepToRaw, deepToRaw,
findSelectedLogEntry, findSelectedLogEntry,
findSubExecutionLocator,
getDefaultCollapsedEntries, getDefaultCollapsedEntries,
getTreeNodeData, getTreeNodeData,
mergeStartData, mergeStartData,
@@ -1271,3 +1272,43 @@ describe(restoreChatHistory, () => {
]); ]);
}); });
}); });
describe(findSubExecutionLocator, () => {
it('should return undefined if given log entry has no related sub execution', () => {
const found = findSubExecutionLocator(
createTestLogEntry({
runData: createTestTaskData({
metadata: {},
}),
}),
);
expect(found).toBe(undefined);
});
it('should find workflowId and executionId in metadata', () => {
const found = findSubExecutionLocator(
createTestLogEntry({
runData: createTestTaskData({
metadata: { subExecution: { workflowId: 'w0', executionId: 'e0' } },
}),
}),
);
expect(found).toEqual({ workflowId: 'w0', executionId: 'e0' });
});
it('should find workflowId and executionId in error object', () => {
const found = findSubExecutionLocator(
createTestLogEntry({
runData: createTestTaskData({
error: {
errorResponse: { workflowId: 'w1', executionId: 'e1' },
} as unknown as ExecutionError,
}),
}),
);
expect(found).toEqual({ workflowId: 'w1', executionId: 'e1' });
});
});

View File

@@ -10,6 +10,8 @@ import {
type Workflow, type Workflow,
type INode, type INode,
type ISourceData, type ISourceData,
parseErrorMetadata,
type RelatedExecution,
} from 'n8n-workflow'; } from 'n8n-workflow';
import type { LogEntry, LogEntrySelection, LogTreeCreationContext } from './logs.types'; import type { LogEntry, LogEntrySelection, LogTreeCreationContext } from './logs.types';
import { isProxy, isReactive, isRef, toRaw } from 'vue'; import { isProxy, isReactive, isRef, toRaw } from 'vue';
@@ -70,13 +72,13 @@ function getChildNodes(
runIndex: number | undefined, runIndex: number | undefined,
context: LogTreeCreationContext, context: LogTreeCreationContext,
) { ) {
if (hasSubExecution(treeNode)) { const subExecutionLocator = findSubExecutionLocator(treeNode);
const workflowId = treeNode.runData?.metadata?.subExecution?.workflowId;
const executionId = treeNode.runData?.metadata?.subExecution?.executionId;
const workflow = workflowId ? context.workflows[workflowId] : undefined;
const subWorkflowRunData = executionId ? context.subWorkflowData[executionId] : undefined;
if (!workflow || !subWorkflowRunData || !executionId) { if (subExecutionLocator !== undefined) {
const workflow = context.workflows[subExecutionLocator.workflowId];
const subWorkflowRunData = context.subWorkflowData[subExecutionLocator.executionId];
if (!workflow || !subWorkflowRunData) {
return []; return [];
} }
@@ -85,7 +87,7 @@ function getChildNodes(
parent: treeNode, parent: treeNode,
depth: context.depth + 1, depth: context.depth + 1,
workflow, workflow,
executionId, executionId: subExecutionLocator.executionId,
data: subWorkflowRunData, data: subWorkflowRunData,
}); });
} }
@@ -434,7 +436,17 @@ export function mergeStartData(
} }
export function hasSubExecution(entry: LogEntry): boolean { export function hasSubExecution(entry: LogEntry): boolean {
return !!entry.runData?.metadata?.subExecution; return findSubExecutionLocator(entry) !== undefined;
}
export function findSubExecutionLocator(entry: LogEntry): RelatedExecution | undefined {
const metadata = entry.runData?.metadata?.subExecution;
if (metadata) {
return { workflowId: metadata.workflowId, executionId: metadata.executionId };
}
return parseErrorMetadata(entry.runData?.error)?.subExecution;
} }
export function getDefaultCollapsedEntries(entries: LogEntry[]): Record<string, boolean> { export function getDefaultCollapsedEntries(entries: LogEntry[]): Record<string, boolean> {

View File

@@ -38,7 +38,7 @@ catalog:
xml2js: 0.6.2 xml2js: 0.6.2
xss: 1.0.15 xss: 1.0.15
zod: 3.24.1 zod: 3.24.1
'zod-to-json-schema': 3.23.3 zod-to-json-schema: 3.23.3
'@langchain/core': 0.3.48 '@langchain/core': 0.3.48
'@langchain/openai': 0.5.0 '@langchain/openai': 0.5.0
'@langchain/anthropic': 0.3.21 '@langchain/anthropic': 0.3.21
@@ -66,4 +66,4 @@ catalogs:
vue-tsc: ^2.2.8 vue-tsc: ^2.2.8
vue-markdown-render: ^2.2.1 vue-markdown-render: ^2.2.1
highlight.js: ^11.8.0 highlight.js: ^11.8.0
'element-plus': 2.4.3 element-plus: 2.4.3