Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core): Node hints(warnings) system #8954

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
88880e1
:zap: warnings fe setup
michael-radency Mar 22, 2024
304a36f
:zap: warnings be setup
michael-radency Mar 22, 2024
ade42db
Merge branch 'master' of https://github.com/n8n-io/n8n into node-1238…
michael-radency Mar 22, 2024
ab5e015
:zap: airtable search looping support with warning message
michael-radency Mar 22, 2024
776a1d8
Merge branch 'master' of https://github.com/n8n-io/n8n into node-1238…
michael-radency Mar 22, 2024
6d0e39d
:zap:multiple messages styles fix
michael-radency Mar 22, 2024
aa71c14
:zap: test fix
michael-radency Mar 22, 2024
383a1d9
Merge branch 'master' of https://github.com/n8n-io/n8n into node-1238…
michael-radency Mar 22, 2024
e18818c
Merge branch 'master' of https://github.com/n8n-io/n8n into node-1238…
michael-radency Mar 23, 2024
1feb795
Merge branch 'master' of https://github.com/n8n-io/n8n into node-1238…
michael-radency Mar 28, 2024
91f9770
Merge branch 'master' of https://github.com/n8n-io/n8n into node-1238…
michael-radency May 7, 2024
7c7dd70
reverted changes to airtable node
michael-radency May 7, 2024
8f52503
:zap: updated implementation of execution based warnings
michael-radency May 7, 2024
8d67e33
Merge branch 'master' of https://github.com/n8n-io/n8n into node-1238…
michael-radency May 7, 2024
24bdc59
Merge branch 'master' of https://github.com/n8n-io/n8n into node-1238…
michael-radency May 8, 2024
17244a8
warnings css update
michael-radency May 8, 2024
fb831b5
Merge branch 'master' of https://github.com/n8n-io/n8n into node-1238…
michael-radency May 9, 2024
607bb18
:zap: processing hints from node property in runData, getNodeHints he…
michael-radency May 9, 2024
208a068
:zap: NodeExecutionOutput accepts NodeExecutionHint array instead of …
michael-radency May 9, 2024
c34579e
:zap: cleanup
michael-radency May 9, 2024
782859b
Merge branch 'master' of https://github.com/n8n-io/n8n into node-1238…
michael-radency May 13, 2024
6ac75d6
getNodeHints helper tests
michael-radency May 13, 2024
75704a7
Merge branch 'master' of https://github.com/n8n-io/n8n into node-1238…
michael-radency May 13, 2024
ba140ca
NodeExecutionOutput type test
michael-radency May 13, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading
Loading