Skip to content

Commit

Permalink
feat(core): Node hints(warnings) system (#8954)
Browse files Browse the repository at this point in the history
  • Loading branch information
michael-radency committed May 13, 2024
1 parent 4d2115c commit da6088d
Show file tree
Hide file tree
Showing 7 changed files with 265 additions and 6 deletions.
17 changes: 15 additions & 2 deletions packages/core/src/WorkflowExecute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,15 @@ import type {
WorkflowExecuteMode,
CloseFunction,
StartNodeData,
NodeExecutionHint,
} from 'n8n-workflow';
import {
LoggerProxy as Logger,
WorkflowOperationError,
NodeHelpers,
NodeConnectionType,
ApplicationError,
NodeExecutionOutput,
} from 'n8n-workflow';
import get from 'lodash/get';
import * as NodeExecuteFunctions from './NodeExecuteFunctions';
Expand Down Expand Up @@ -807,6 +809,7 @@ export class WorkflowExecute {
// Variables which hold temporary data for each node-execution
let executionData: IExecuteData;
let executionError: ExecutionBaseError | undefined;
let executionHints: NodeExecutionHint[] = [];
let executionNode: INode;
let nodeSuccessData: INodeExecutionData[][] | null | undefined;
let runIndex: number;
Expand All @@ -828,7 +831,7 @@ export class WorkflowExecute {
let lastExecutionTry = '';
let closeFunction: Promise<void> | undefined;

return new PCancelable(async (resolve, reject, onCancel) => {
return new PCancelable(async (resolve, _reject, onCancel) => {
// Let as many nodes listen to the abort signal, without getting the MaxListenersExceededWarning
setMaxListeners(Infinity, this.abortController.signal);

Expand Down Expand Up @@ -896,6 +899,7 @@ export class WorkflowExecute {

nodeSuccessData = null;
executionError = undefined;
executionHints = [];
executionData =
this.runExecutionData.executionData!.nodeExecutionStack.shift() as IExecuteData;
executionNode = executionData.node;
Expand Down Expand Up @@ -1059,8 +1063,16 @@ export class WorkflowExecute {
this.mode,
this.abortController.signal,
);

nodeSuccessData = runNodeData.data;

if (nodeSuccessData instanceof NodeExecutionOutput) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
const hints: NodeExecutionHint[] = nodeSuccessData.getHints();
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
executionHints.push(...hints);
}

if (nodeSuccessData && executionData.node.onError === 'continueErrorOutput') {
// If errorOutput is activated check all the output items for error data.
// If any is found, route them to the last output as that will be the
Expand Down Expand Up @@ -1244,7 +1256,7 @@ export class WorkflowExecute {
if (!inputData) {
return;
}
inputData.forEach((item, itemIndex) => {
inputData.forEach((_item, itemIndex) => {
pairedItem.push({
item: itemIndex,
input: inputIndex,
Expand Down Expand Up @@ -1296,6 +1308,7 @@ export class WorkflowExecute {
}

taskData = {
hints: executionHints,
startTime,
executionTime: new Date().getTime() - startTime,
source: !executionData.source ? [] : executionData.source.main,
Expand Down
19 changes: 18 additions & 1 deletion packages/core/test/WorkflowExecute.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import type { IRun, WorkflowTestData } from 'n8n-workflow';
import { ApplicationError, createDeferredPromise, Workflow } from 'n8n-workflow';
import {
ApplicationError,
createDeferredPromise,
NodeExecutionOutput,
Workflow,
} from 'n8n-workflow';
import { WorkflowExecute } from '@/WorkflowExecute';

import * as Helpers from './helpers';
Expand Down Expand Up @@ -192,4 +197,16 @@ describe('WorkflowExecute', () => {
});
}
});

describe('WorkflowExecute, NodeExecutionOutput type test', () => {
//TODO Add more tests here when execution hints are added to some node types
const nodeExecutionOutput = new NodeExecutionOutput(
[[{ json: { data: 123 } }]],
[{ message: 'TEXT HINT' }],
);

expect(nodeExecutionOutput).toBeInstanceOf(NodeExecutionOutput);
expect(nodeExecutionOutput[0][0].json.data).toEqual(123);
expect(nodeExecutionOutput.getHints()[0].message).toEqual('TEXT HINT');
});
});
65 changes: 65 additions & 0 deletions packages/editor-ui/src/components/RunData.vue
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,15 @@
</div>
<slot name="before-data" />
<n8n-callout
v-for="hint in getNodeHints()"
:key="hint.message"
:class="$style.hintCallout"
:theme="hint.type || 'info'"
>
<n8n-text v-html="hint.message" size="small"></n8n-text>
</n8n-callout>
<div
v-if="maxOutputIndex > 0 && branches.length > 1"
:class="$style.tabs"
Expand Down Expand Up @@ -557,6 +566,7 @@ import type {
INodeTypeDescription,
IRunData,
IRunExecutionData,
NodeHint,
} from 'n8n-workflow';
import { NodeHelpers, NodeConnectionType } from 'n8n-workflow';
Expand Down Expand Up @@ -824,6 +834,15 @@ export default defineComponent({
hasRunError(): boolean {
return Boolean(this.node && this.workflowRunData?.[this.node.name]?.[this.runIndex]?.error);
},
executionHints(): NodeHint[] {
if (this.hasNodeRun) {
const hints = this.node && this.workflowRunData?.[this.node.name]?.[this.runIndex]?.hints;
if (hints) return hints;
}
return [];
},
workflowExecution(): IExecutionResponse | null {
return this.workflowsStore.getWorkflowExecution;
},
Expand Down Expand Up @@ -1083,6 +1102,46 @@ export default defineComponent({
}
return [];
},
shouldHintBeDisplayed(hint: NodeHint): boolean {
const { location, whenToDisplay } = hint;
if (location) {
if (location === 'ndv') {
return true;
}
if (location === 'inputPane' && this.paneType === 'input') {
return true;
}
if (location === 'outputPane' && this.paneType === 'output') {
return true;
}
return false;
}
if (whenToDisplay === 'afterExecution' && !this.hasNodeRun) {
return false;
}
if (whenToDisplay === 'beforeExecution' && this.hasNodeRun) {
return false;
}
return true;
},
getNodeHints(): NodeHint[] {
if (this.node && this.nodeType) {
const workflow = this.workflowsStore.getCurrentWorkflow();
const workflowNode = workflow.getNode(this.node.name);
if (workflowNode) {
const executionHints = this.executionHints;
const nodeHints = NodeHelpers.getNodeHints(workflow, workflowNode, this.nodeType);
return executionHints.concat(nodeHints).filter(this.shouldHintBeDisplayed);
}
}
return [];
},
onItemHover(itemIndex: number | null) {
if (itemIndex === null) {
this.$emit('itemHover', null);
Expand Down Expand Up @@ -1788,6 +1847,12 @@ export default defineComponent({
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
.hintCallout {
margin-bottom: var(--spacing-xs);
margin-left: var(--spacing-s);
margin-right: var(--spacing-s);
}
</style>
<style lang="scss" scoped>
Expand Down
2 changes: 2 additions & 0 deletions packages/nodes-base/utils/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const NODE_RAN_MULTIPLE_TIMES_WARNING =
"This node ran multiple times - once for each input item. You can change this by setting 'execute once' in the node settings. <a href='https://docs.n8n.io/flow-logic/looping/#executing-nodes-once' target='_blank'>More Info</a>";
28 changes: 27 additions & 1 deletion packages/workflow/src/Interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -392,7 +392,7 @@ export interface IGetExecuteTriggerFunctions {
}

export interface IRunNodeResponse {
data: INodeExecutionData[][] | null | undefined;
data: INodeExecutionData[][] | NodeExecutionOutput | null | undefined;
closeFunction?: CloseFunction;
}
export interface IGetExecuteFunctions {
Expand Down Expand Up @@ -1423,6 +1423,20 @@ export interface SupplyData {
closeFunction?: CloseFunction;
}

export class NodeExecutionOutput extends Array {
private hints: NodeExecutionHint[];

constructor(data: INodeExecutionData[][], hints: NodeExecutionHint[] = []) {
super();
this.push(...data);
this.hints = hints;
}

public getHints(): NodeExecutionHint[] {
return this.hints;
}
}

export interface INodeType {
description: INodeTypeDescription;
supplyData?(this: IAllExecuteFunctions, itemIndex: number): Promise<SupplyData>;
Expand Down Expand Up @@ -1745,9 +1759,20 @@ export interface INodeTypeDescription extends INodeTypeBaseDescription {
}
| boolean;
extendsCredential?: string;
hints?: NodeHint[];
__loadOptionsMethods?: string[]; // only for validation during build
}

export type NodeHint = {
message: string;
type?: 'info' | 'warning' | 'danger';
location?: 'outputPane' | 'inputPane' | 'ndv';
displayCondition?: string;
whenToDisplay?: 'always' | 'beforeExecution' | 'afterExecution';
};

export type NodeExecutionHint = Omit<NodeHint, 'whenToDisplay' | 'displayCondition'>;

export interface INodeHookDescription {
method: string;
}
Expand Down Expand Up @@ -1929,6 +1954,7 @@ export interface ITaskData {
data?: ITaskDataConnections;
inputOverride?: ITaskDataConnections;
error?: ExecutionError;
hints?: NodeExecutionHint[];
source: Array<ISourceData | null>; // Is an array as nodes have multiple inputs
metadata?: ITaskMetadata;
}
Expand Down
45 changes: 45 additions & 0 deletions packages/workflow/src/NodeHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import type {
INodeInputConfiguration,
GenericValue,
DisplayCondition,
NodeHint,
} from './Interfaces';
import {
isFilterValue,
Expand Down Expand Up @@ -1120,6 +1121,50 @@ export function getNodeInputs(
}
}

export function getNodeHints(
workflow: Workflow,
node: INode,
nodeTypeData: INodeTypeDescription,
): NodeHint[] {
const hints: NodeHint[] = [];

if (nodeTypeData?.hints?.length) {
for (const hint of nodeTypeData.hints) {
if (hint.displayCondition) {
try {
const display = (workflow.expression.getSimpleParameterValue(
node,
hint.displayCondition,
'internal',
{},
) || false) as boolean;

if (typeof display !== 'boolean') {
console.warn(
`Condition was not resolved as boolean in '${node.name}' node for hint: `,
hint.message,
);
continue;
}

if (display) {
hints.push(hint);
}
} catch (e) {
console.warn(
`Could not calculate display condition in '${node.name}' node for hint: `,
hint.message,
);
}
} else {
hints.push(hint);
}
}
}

return hints;
}

export function getNodeOutputs(
workflow: Workflow,
node: INode,
Expand Down

0 comments on commit da6088d

Please sign in to comment.