fix(editor): Render HTML in the log view (#17586)

This commit is contained in:
Suguru Inoue
2025-07-24 10:21:41 +02:00
committed by GitHub
parent 4713827813
commit 46635c5941
6 changed files with 89 additions and 26 deletions

View File

@@ -1 +1,6 @@
<svg viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg"><path fill="currentColor" d="M1.63636 0H8.18182C9.08556 0 9.81818 0.732625 9.81818 1.63636C9.81818 2.5401 9.08556 3.27273 8.18182 3.27273H1.63636C0.732626 3.27273 0 2.5401 0 1.63636C0 0.732625 0.732625 0 1.63636 0ZM1.63636 1.09091C1.33512 1.09091 1.09091 1.33512 1.09091 1.63636C1.09091 1.93761 1.33512 2.18182 1.63636 2.18182H8.18182C8.48306 2.18182 8.72727 1.93761 8.72727 1.63636C8.72727 1.33512 8.48306 1.09091 8.18182 1.09091H1.63636Z M7.09091 4.36353H11.4545C12.3583 4.36353 13.0909 5.09615 13.0909 5.99989C13.0909 6.90363 12.3583 7.63625 11.4545 7.63625H7.09091C6.18717 7.63625 5.45454 6.90363 5.45454 5.99989C5.45454 5.09615 6.18717 4.36353 7.09091 4.36353ZM7.09091 5.45443C6.78966 5.45443 6.54545 5.69864 6.54545 5.99989C6.54545 6.30114 6.78966 6.54534 7.09091 6.54534H11.4545C11.7558 6.54534 12 6.30114 12 5.99989C12 5.69864 11.7558 5.45443 11.4545 5.45443H7.09091Z M7.09091 8.72729H11.4545C12.3583 8.72729 13.0909 9.45992 13.0909 10.3637C13.0909 11.2674 12.3583 12 11.4545 12H7.09091C6.18717 12 5.45454 11.2674 5.45454 10.3637C5.45454 9.45992 6.18717 8.72729 7.09091 8.72729ZM7.09091 9.8182C6.78966 9.8182 6.54545 10.0624 6.54545 10.3637C6.54545 10.6649 6.78966 10.9091 7.09091 10.9091H11.4545C11.7558 10.9091 12 10.6649 12 10.3637C12 10.0624 11.7558 9.8182 11.4545 9.8182H7.09091Z" /></svg>
<svg viewBox="0 -1 14 14" xmlns="http://www.w3.org/2000/svg">
<path
fill="currentColor"
d="M1.63636 0H8.18182C9.08556 0 9.81818 0.732625 9.81818 1.63636C9.81818 2.5401 9.08556 3.27273 8.18182 3.27273H1.63636C0.732626 3.27273 0 2.5401 0 1.63636C0 0.732625 0.732625 0 1.63636 0ZM1.63636 1.09091C1.33512 1.09091 1.09091 1.33512 1.09091 1.63636C1.09091 1.93761 1.33512 2.18182 1.63636 2.18182H8.18182C8.48306 2.18182 8.72727 1.93761 8.72727 1.63636C8.72727 1.33512 8.48306 1.09091 8.18182 1.09091H1.63636Z M7.09091 4.36353H11.4545C12.3583 4.36353 13.0909 5.09615 13.0909 5.99989C13.0909 6.90363 12.3583 7.63625 11.4545 7.63625H7.09091C6.18717 7.63625 5.45454 6.90363 5.45454 5.99989C5.45454 5.09615 6.18717 4.36353 7.09091 4.36353ZM7.09091 5.45443C6.78966 5.45443 6.54545 5.69864 6.54545 5.99989C6.54545 6.30114 6.78966 6.54534 7.09091 6.54534H11.4545C11.7558 6.54534 12 6.30114 12 5.99989C12 5.69864 11.7558 5.45443 11.4545 5.45443H7.09091Z M7.09091 8.72729H11.4545C12.3583 8.72729 13.0909 9.45992 13.0909 10.3637C13.0909 11.2674 12.3583 12 11.4545 12H7.09091C6.18717 12 5.45454 11.2674 5.45454 10.3637C5.45454 9.45992 6.18717 8.72729 7.09091 8.72729ZM7.09091 9.8182C6.78966 9.8182 6.54545 10.0624 6.54545 10.3637C6.54545 10.6649 6.78966 10.9091 7.09091 10.9091H11.4545C11.7558 10.9091 12 10.6649 12 10.3637C12 10.0624 11.7558 9.8182 11.4545 9.8182H7.09091Z"
/>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -3,7 +3,6 @@ import BoltFilled from './custom/bolt-filled.svg';
import Continue from './custom/Continue.svg';
import EmptyOutput from './custom/EmptyOutput.svg';
import GripLinesVertical from './custom/grip-lines-vertical.svg';
import Json from './custom/json.svg';
import PopOut from './custom/pop-out.svg';
import Retry from './custom/Retry.svg';
import RunOnce from './custom/RunOnce.svg';
@@ -36,6 +35,7 @@ import IconLucideBell from '~icons/lucide/bell';
import IconLucideBook from '~icons/lucide/book';
import IconLucideBot from '~icons/lucide/bot';
import IconLucideBox from '~icons/lucide/box';
import IconLucideBraces from '~icons/lucide/braces';
import IconLucideBrain from '~icons/lucide/brain';
import IconLucideBug from '~icons/lucide/bug';
import IconLucideCalculator from '~icons/lucide/calculator';
@@ -208,7 +208,7 @@ export const deprecatedIconSet = {
'status-warning': StatusWarning,
'vector-square': VectorSquare,
schema: Schema,
json: Json,
json: IconLucideBraces,
binary: Binary,
text: Text,
toolbox: Toolbox,
@@ -415,7 +415,7 @@ export const updatedIconSet = {
'retry-on-fail': Retry,
'execute-once': RunOnce,
schema: Schema,
json: Json,
json: IconLucideBraces,
binary: Binary,
text: Text,
toolbox: Toolbox,

View File

@@ -130,14 +130,18 @@ export const defaultNodeDescriptions = Object.values(defaultNodeTypes).map(
({ type }) => type.description,
) as INodeTypeDescription[];
const nodeTypes = mock<INodeTypes>({
getByName(nodeType) {
return defaultNodeTypes[nodeType].type;
},
getByNameAndVersion(nodeType: string, version?: number): INodeType {
return NodeHelpers.getVersionedNodeType(defaultNodeTypes[nodeType].type, version);
},
});
export function createMockNodeTypes(data: INodeTypeData) {
return mock<INodeTypes>({
getByName(nodeType) {
return data[nodeType].type;
},
getByNameAndVersion(nodeType: string, version?: number): INodeType {
return NodeHelpers.getVersionedNodeType(data[nodeType].type, version);
},
});
}
const nodeTypes = createMockNodeTypes(defaultNodeTypes);
export function createTestWorkflowObject({
id = uuid(),
@@ -148,6 +152,7 @@ export function createTestWorkflowObject({
staticData = {},
settings = {},
pinData = {},
...rest
}: {
id?: string;
name?: string;
@@ -157,6 +162,7 @@ export function createTestWorkflowObject({
staticData?: IDataObject;
settings?: IWorkflowSettings;
pinData?: IPinData;
nodeTypes?: INodeTypes;
} = {}) {
return new Workflow({
id,
@@ -167,7 +173,7 @@ export function createTestWorkflowObject({
staticData,
settings,
pinData,
nodeTypes,
nodeTypes: rest.nodeTypes ?? nodeTypes,
});
}

View File

@@ -112,3 +112,8 @@ Object.defineProperty(HTMLCanvasElement.prototype, 'getContext', {
writable: true,
value: vi.fn(),
});
Object.defineProperty(HTMLElement.prototype, 'scrollTo', {
writable: true,
value: vi.fn(),
});

View File

@@ -721,7 +721,6 @@ onMounted(() => {
});
if (props.paneType === 'output') {
setDisplayMode();
activatePane();
}
@@ -1231,6 +1230,10 @@ function init() {
if (isNDVV2.value) {
pageSize.value = RUN_DATA_DEFAULT_PAGE_SIZE;
}
if (props.paneType === 'output') {
setDisplayMode();
}
}
function closeBinaryDataDisplay() {
@@ -1337,14 +1340,14 @@ function enableNode() {
}
}
const shouldDisplayHtml = computed(
() =>
node.value?.type === HTML_NODE_TYPE &&
node.value.parameters.operation === 'generateHtmlTemplate',
);
function setDisplayMode() {
if (!activeNode.value) return;
const shouldDisplayHtml =
activeNode.value.type === HTML_NODE_TYPE &&
activeNode.value.parameters.operation === 'generateHtmlTemplate';
if (shouldDisplayHtml) {
if (shouldDisplayHtml.value) {
emit('displayModeChange', 'html');
}
}
@@ -1461,10 +1464,7 @@ defineExpose({ enterEditMode });
:value="displayMode"
:has-binary-data="binaryData.length > 0"
:pane-type="paneType"
:node-generates-html="
activeNode?.type === HTML_NODE_TYPE &&
activeNode.parameters.operation === 'generateHtmlTemplate'
"
:node-generates-html="shouldDisplayHtml"
:has-renderable-data="hasParsedAiContent"
@change="onDisplayModeChange"
/>

View File

@@ -1,19 +1,23 @@
import { fireEvent, within } from '@testing-library/vue';
import { fireEvent, waitFor, within } from '@testing-library/vue';
import { renderComponent } from '@/__tests__/render';
import LogDetailsPanel from './LogDetailsPanel.vue';
import { createRouter, createWebHistory } from 'vue-router';
import { createTestingPinia, type TestingPinia } from '@pinia/testing';
import { h } from 'vue';
import {
createMockNodeTypes,
createTestNode,
createTestTaskData,
createTestWorkflow,
createTestWorkflowObject,
defaultNodeTypes,
mockLoadedNodeType,
} from '@/__tests__/mocks';
import { LOG_DETAILS_PANEL_STATE } from '@/features/logs/logs.constants';
import type { LogEntry } from '../logs.types';
import { createTestLogEntry } from '../__test__/mocks';
import { NodeConnectionTypes } from 'n8n-workflow';
import { HTML_NODE_TYPE } from '@/constants';
describe('LogDetailsPanel', () => {
let pinia: TestingPinia;
@@ -182,4 +186,47 @@ describe('LogDetailsPanel', () => {
),
).toBeInTheDocument();
});
it('should render output data in HTML mode for HTML node', async () => {
const nodeA = createTestNode({ name: 'A' });
const nodeB = createTestNode({
name: 'B',
type: HTML_NODE_TYPE,
});
const runDataA = createTestTaskData({ data: { [NodeConnectionTypes.Main]: [[{ json: {} }]] } });
const runDataB = createTestTaskData({
data: { [NodeConnectionTypes.Main]: [[{ json: { html: '<h1>Hi!</h1>' } }]] },
source: [{ previousNode: 'A' }],
});
const workflow = createTestWorkflowObject({
nodes: [nodeA, nodeB],
nodeTypes: createMockNodeTypes({
...defaultNodeTypes,
[HTML_NODE_TYPE]: mockLoadedNodeType(HTML_NODE_TYPE),
}),
});
const execution = { resultData: { runData: { A: [runDataA], B: [runDataB] } } };
const logA = createLogEntry({ node: nodeA, runData: runDataA, workflow, execution });
const logB = createLogEntry({ node: nodeB, runData: runDataB, workflow, execution });
// HACK: Setting parameters after creating workflow because validation removes parameters that are not define in node types.
nodeB.parameters = { operation: 'generateHtmlTemplate' };
const props = {
isOpen: true,
panels: LOG_DETAILS_PANEL_STATE.BOTH,
collapsingInputTableColumnName: null,
collapsingOutputTableColumnName: null,
};
const rendered = render({ ...props, logEntry: logB });
await waitFor(() => expect(rendered.container.querySelectorAll('iframe')).toHaveLength(1));
await rendered.rerender({ ...props, logEntry: logA });
await waitFor(() => expect(rendered.container.querySelectorAll('iframe')).toHaveLength(0));
// Re-selecting node B should render HTML again
await rendered.rerender({ ...props, logEntry: logB });
await waitFor(() => expect(rendered.container.querySelectorAll('iframe')).toHaveLength(1));
});
});