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

View File

@@ -8,7 +8,7 @@ import { useI18n } from '@n8n/i18n';
import { I18nT } from 'vue-i18n';
import { toDayMonth, toTime } from '@/utils/formatters/dateFormatter';
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 type { LatestNodeInfo, LogEntry } from '@/features/logs/logs.types';
@@ -70,9 +70,7 @@ const subtreeConsumedTokens = computed(() =>
props.shouldShowTokenCountColumn ? getSubtreeTotalConsumedTokens(props.data, false) : undefined,
);
const hasChildren = computed(
() => props.data.children.length > 0 || !!props.data.runData?.metadata?.subExecution,
);
const hasChildren = computed(() => props.data.children.length > 0 || hasSubExecution(props.data));
function isLastChild(level: number) {
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 { useNodeHelpers } from '@/composables/useNodeHelpers';
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 { useToast } from '@/composables/useToast';
import type { LatestNodeInfo, LogEntry } from '../logs.types';
@@ -68,15 +73,14 @@ export function useLogsExecutionData() {
}
async function loadSubExecution(logEntry: LogEntry) {
const executionId = logEntry.runData?.metadata?.subExecution?.executionId;
const workflowId = logEntry.runData?.metadata?.subExecution?.workflowId;
const locator = findSubExecutionLocator(logEntry);
if (!execData.value?.data || !executionId || !workflowId) {
if (!execData.value?.data || locator === undefined) {
return;
}
try {
const subExecution = await workflowsStore.fetchExecutionDataById(executionId);
const subExecution = await workflowsStore.fetchExecutionDataById(locator.executionId);
const data = subExecution?.data
? (parse(subExecution.data as unknown as string) as IRunExecutionData)
: undefined;
@@ -85,8 +89,8 @@ export function useLogsExecutionData() {
throw Error('Data is missing');
}
subWorkflowExecData.value[executionId] = data;
subWorkflows.value[workflowId] = new Workflow({
subWorkflowExecData.value[locator.executionId] = data;
subWorkflows.value[locator.workflowId] = new Workflow({
...subExecution.workflowData,
nodeTypes: workflowsStore.getNodeTypes(),
});

View File

@@ -9,6 +9,7 @@ import {
createLogTree,
deepToRaw,
findSelectedLogEntry,
findSubExecutionLocator,
getDefaultCollapsedEntries,
getTreeNodeData,
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 INode,
type ISourceData,
parseErrorMetadata,
type RelatedExecution,
} from 'n8n-workflow';
import type { LogEntry, LogEntrySelection, LogTreeCreationContext } from './logs.types';
import { isProxy, isReactive, isRef, toRaw } from 'vue';
@@ -70,13 +72,13 @@ function getChildNodes(
runIndex: number | undefined,
context: LogTreeCreationContext,
) {
if (hasSubExecution(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;
const subExecutionLocator = findSubExecutionLocator(treeNode);
if (!workflow || !subWorkflowRunData || !executionId) {
if (subExecutionLocator !== undefined) {
const workflow = context.workflows[subExecutionLocator.workflowId];
const subWorkflowRunData = context.subWorkflowData[subExecutionLocator.executionId];
if (!workflow || !subWorkflowRunData) {
return [];
}
@@ -85,7 +87,7 @@ function getChildNodes(
parent: treeNode,
depth: context.depth + 1,
workflow,
executionId,
executionId: subExecutionLocator.executionId,
data: subWorkflowRunData,
});
}
@@ -434,7 +436,17 @@ export function mergeStartData(
}
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> {

View File

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