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

Add indicator for invalid node properties #752

Merged
merged 25 commits into from Jul 24, 2020
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
4cc1afb
Add error message on attempt to create circular references
marthacryan Jul 9, 2020
6a46d38
Add indicator for invalid node properties
marthacryan Jul 13, 2020
c217b78
Add ability to customize type of error message
marthacryan Jul 14, 2020
7a8a38f
Dismiss validatione error on creating valid link
marthacryan Jul 14, 2020
2f33fb7
Fix small style issue
marthacryan Jul 14, 2020
51524a2
Update icon and position
vabarbosa Jul 14, 2020
a9562d0
Merge branch 'master' of github.com:elyra-ai/elyra into prevent-circular
marthacryan Jul 20, 2020
7640d06
Merge branch 'prevent-circular' of github.com:marthacryan/elyra into …
marthacryan Jul 20, 2020
6c455df
Move error info into interface
marthacryan Jul 20, 2020
98efab3
Remove indicator when creating new node
marthacryan Jul 21, 2020
d39e256
Merge branch 'master' of github.com:elyra-ai/elyra into op-err-indicator
marthacryan Jul 21, 2020
9f195de
Fix small issue with state
marthacryan Jul 21, 2020
958fcfc
Add validation check to export
marthacryan Jul 21, 2020
52ffb97
Merge with new version of canvas
marthacryan Jul 22, 2020
1a3a063
Move link validation to beforeEditActionHander
marthacryan Jul 22, 2020
983b269
Remove unnecessary function
marthacryan Jul 22, 2020
bd4d5db
Merge with prevent-circular
marthacryan Jul 22, 2020
82bf7e6
Remove duplicate functions
marthacryan Jul 22, 2020
b41ff4c
Change name of validation functions
marthacryan Jul 22, 2020
f06f5a4
Change name of function for consistency
marthacryan Jul 22, 2020
c6e4549
Change variable name and colors
marthacryan Jul 22, 2020
24f241d
Correct description of function
marthacryan Jul 22, 2020
e67ae03
Change color of invalid selected node outline
marthacryan Jul 22, 2020
772b97d
Merge branch 'op-err-indicator' of github.com:marthacryan/elyra into …
marthacryan Jul 22, 2020
d85abfa
Add validation of circular references
marthacryan Jul 23, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/pipeline-editor/package.json
Expand Up @@ -34,6 +34,9 @@
"@elyra/application": "^1.0.0-rc.1",
"@elyra/canvas": "8.0.32",
"@elyra/ui-components": "^1.0.0-rc.1",
"@material-ui/core": "^4.1.2",
"@material-ui/icons": "^4.2.1",
"@material-ui/lab": "^4.0.0-alpha.18",
"@jupyterlab/application": "^2.0.2",
"@jupyterlab/apputils": "^2.0.2",
"@jupyterlab/docregistry": "^2.0.2",
Expand Down
237 changes: 229 additions & 8 deletions packages/pipeline-editor/src/PipelineEditorWidget.tsx
Expand Up @@ -29,7 +29,8 @@ import {
exportPipelineIcon,
pipelineIcon,
savePipelineIcon,
showFormDialog
showFormDialog,
errorIcon
} from '@elyra/ui-components';

import { JupyterFrontEnd } from '@jupyterlab/application';
Expand All @@ -45,6 +46,10 @@ import { notebookIcon } from '@jupyterlab/ui-components';

import { toArray } from '@lumino/algorithm';
import { IDragEvent } from '@lumino/dragdrop';
import { Collapse, IconButton } from '@material-ui/core';
import CloseIcon from '@material-ui/icons/Close';
import Alert from '@material-ui/lab/Alert';
import { Color } from '@material-ui/lab/Alert';

import 'carbon-components/css/carbon-components.min.css';
import '@elyra/canvas/dist/common-canvas.min.css';
Expand Down Expand Up @@ -78,17 +83,26 @@ const NodeProperties = (properties: any): React.ReactElement => {
} else if (typeof value === 'boolean') {
value = value ? 'Yes' : 'No';
}
let tooltipTextClass = '';
if (key == 'Error') {
tooltipTextClass = 'elyra-tooltipError';
}
return (
<React.Fragment key={idx}>
<dd>{key}</dd>
<dt>{value}</dt>
<dd className={tooltipTextClass}>{key}</dd>
<dt className={tooltipTextClass}>{value}</dt>
</React.Fragment>
);
})}
</dl>
);
};

interface IValidationError {
errorMessage: string;
errorSeverity: Color;
}

export const commandIDs = {
openPipelineEditor: 'pipeline-editor:open',
openDocManager: 'docmanager:open',
Expand Down Expand Up @@ -149,6 +163,16 @@ export namespace PipelineEditor {
*/
propertiesInfo: any;

/*
* Whether the warning for invalid operations is visible.
*/
showValidationError: boolean;

/*
* Error to present for an invalid operation
*/
validationError: IValidationError;

/**
* Whether pipeline is empty.
*/
Expand Down Expand Up @@ -182,11 +206,19 @@ export class PipelineEditor extends React.Component<
this.contextMenuHandler = this.contextMenuHandler.bind(this);
this.clickActionHandler = this.clickActionHandler.bind(this);
this.editActionHandler = this.editActionHandler.bind(this);
this.beforeEditActionHandler = this.beforeEditActionHandler.bind(this);
this.tipHandler = this.tipHandler.bind(this);

this.nodesConnected = this.nodesConnected.bind(this);

this.state = {
showPropertiesDialog: false,
propertiesInfo: {},
showValidationError: false,
validationError: {
errorMessage: '',
errorSeverity: 'error'
},
emptyPipeline: Utils.isEmptyPipeline(
this.canvasController.getPipelineFlow()
)
Expand All @@ -204,6 +236,27 @@ export class PipelineEditor extends React.Component<

render(): React.ReactElement {
const style = { height: '100%' };
const validationAlert = (
<Collapse in={this.state.showValidationError}>
<Alert
severity={this.state.validationError.errorSeverity}
action={
<IconButton
aria-label="close"
color="inherit"
size="small"
onClick={(): void => {
this.setState({ showValidationError: false });
}}
>
<CloseIcon fontSize="inherit" />
</IconButton>
}
>
{this.state.validationError.errorMessage}
</Alert>
</Collapse>
);
const emptyCanvasContent = (
<div>
<dragDropIcon.react tag="div" elementPosition="center" height="120px" />
Expand Down Expand Up @@ -298,6 +351,7 @@ export class PipelineEditor extends React.Component<

return (
<div style={style} ref={this.node}>
{validationAlert}
<IntlProvider
key="IntlProvider1"
locale={'en'}
Expand All @@ -308,6 +362,7 @@ export class PipelineEditor extends React.Component<
contextMenuHandler={this.contextMenuHandler}
clickActionHandler={this.clickActionHandler}
editActionHandler={this.editActionHandler}
beforeEditActionHandler={this.beforeEditActionHandler}
tipHandler={this.tipHandler}
toolbarConfig={toolbarConfig}
config={canvasConfig}
Expand Down Expand Up @@ -364,18 +419,25 @@ export class PipelineEditor extends React.Component<
node_props.parameterDef.current_parameters.include_subdirectories =
app_data.include_subdirectories;

this.setState({ showPropertiesDialog: true, propertiesInfo: node_props });
this.setState({
showValidationError: false,
showPropertiesDialog: true,
propertiesInfo: node_props
});
}

applyPropertyChanges(propertySet: any, appData: any): void {
console.log('Applying changes to properties');
const app_data = this.canvasController.getNode(appData.id).app_data;
const node = this.canvasController.getNode(appData.id);
const app_data = node.app_data;

app_data.runtime_image = propertySet.runtime_image;
app_data.outputs = propertySet.outputs;
app_data.env_vars = propertySet.env_vars;
app_data.dependencies = propertySet.dependencies;
app_data.include_subdirectories = propertySet.include_subdirectories;
this.validateAllNodes();
this.updateModel();
}

closePropertiesDialog(): void {
Expand Down Expand Up @@ -425,10 +487,64 @@ export class PipelineEditor extends React.Component<
}
}

/*
* Checks if there is a path from sourceNode to targetNode in the graph.
*/
nodesConnected(
sourceNode: string,
targetNode: string,
links: any[]
): boolean {
if (
links.find((value: any, index: number) => {
return value.srcNodeId == sourceNode && value.trgNodeId == targetNode;
})
) {
return true;
} else {
for (const link of links) {
if (
link.srcNodeId == sourceNode &&
this.nodesConnected(link.trgNodeId, targetNode, links)
) {
return true;
}
}
}
return false;
}

beforeEditActionHandler(data: any): any {
// Checks validity of links before adding
if (
data.editType == 'linkNodes' &&
this.nodesConnected(
data.targetNodes[0].id,
data.nodes[0].id,
this.canvasController.getLinks()
)
) {
this.setState({
validationError: {
errorMessage: 'Invalid operation: circular references in pipeline.',
errorSeverity: 'error'
},
showValidationError: true
});
// Don't proceed with adding the link if invalid.
return null;
} else {
return data;
}
}

/*
* Handles creating new nodes in the canvas
*/
editActionHandler(data: any): void {
this.setState({
showValidationError: false
});
if (data && data.editType) {
console.log(`Handling action: ${data.editType}`);

Expand Down Expand Up @@ -472,6 +588,10 @@ export class PipelineEditor extends React.Component<
const propsInfo = this.propertiesInfo.parameterDef.uihints.parameter_info;
const tooltipProps: any = {};

if (appData.invalidNodeError != null) {
tooltipProps['Error'] = appData.invalidNodeError;
}

propsInfo.forEach(
(info: { parameter_ref: string; label: { default: string } }) => {
tooltipProps[info.label.default] = appData[info.parameter_ref] || '';
Expand Down Expand Up @@ -540,6 +660,7 @@ export class PipelineEditor extends React.Component<
] = this.propertiesInfo.parameterDef.current_parameters.include_subdirectories;

this.canvasController.editActionHandler(data);
this.setState({ showValidationError: false });

position += 20;
}
Expand Down Expand Up @@ -575,6 +696,18 @@ export class PipelineEditor extends React.Component<
}

async handleExportPipeline(): Promise<void> {
// Warn user if the pipeline has invalid nodes
if (!this.validateAllNodes()) {
this.setState({
showValidationError: true,
validationError: {
errorMessage:
'Invalid pipeline: Some nodes have missing or invalid properties.',
errorSeverity: 'error'
}
});
return;
}
const runtimes = await PipelineService.getRuntimes();
const dialogOptions: Partial<Dialog.IOptions<any>> = {
title: 'Export pipeline',
Expand Down Expand Up @@ -681,16 +814,104 @@ export class PipelineEditor extends React.Component<
}
});
}
} else {
// in this case, pipeline version is current
this.canvasController.setPipelineFlow(pipelineJson);
}
}
this.setState({ emptyPipeline: Utils.isEmptyPipeline(pipelineJson) });
this.canvasController.setPipelineFlow(pipelineJson);
this.validateAllNodes();
});
}

/**
* Adds an error decoration if a node has any invalid properties.
*
* @param node - canvas node object to validate
*
* @returns true if the node is valid.
*/
validateNode(node: any): boolean {
node.app_data.invalidNodeError = this.validateProperties(node);
if (node.app_data.invalidNodeError != null) {
this.canvasController.setNodeDecorations(node.id, [
{
id: 'error',
image: IconUtil.encode(errorIcon),
outline: false,
class_name: 'elyra-canvasErrorIcon',
position: 'topLeft',
x_pos: 20,
y_pos: 3
}
]);
const pipelineId = this.canvasController.getPrimaryPipelineId();
const stylePipelineObj: any = {};
stylePipelineObj[pipelineId] = [node.id];
const styleSpec = {
body: { default: 'stroke: var(--jp-error-color1);' },
selection_outline: { default: 'stroke: var(--jp-error-color1);' },
label: { default: 'fill: var(--jp-error-color1);' }
};
this.canvasController.setObjectsStyle(stylePipelineObj, styleSpec, true);
return false;
} else {
this.canvasController.setNodeDecorations(node.id, []);
return true;
}
}

/**
* Validates the properties of a given node.
*
* @param node: node to check properties for
*
* @returns a warning message to display in the tooltip
* if there are invalid properties. If there are none,
* returns null.
*/
validateProperties(node: any): string {
if (
node.app_data.runtime_image == null ||
node.app_data.runtime_image == ''
) {
return 'no runtime image.';
} else {
return null;
}
}

/**
* Validates the properties of all nodes in the pipeline.
* Updates the decorations / style of all nodes.
*
* @returns true if the pipeline is valid.
*/
validateAllNodes(): boolean {
let validPipeline = true;
// Reset any existing flagged nodes' style
this.canvasController.removeAllStyles(true);
const pipelineId = this.canvasController.getPrimaryPipelineId();
for (const node of this.canvasController.getNodes(pipelineId)) {
if (!this.validateNode(node)) {
validPipeline = false;
}
}
return validPipeline;
}

async handleRunPipeline(): Promise<void> {
// Check that all nodes are valid
if (!this.validateAllNodes()) {
this.setState({
showValidationError: true,
validationError: {
errorMessage:
'Invalid pipeline: Some nodes have missing or invalid properties.',
errorSeverity: 'error'
}
});
return;
}

const runtimes = await PipelineService.getRuntimes();
const dialogOptions: Partial<Dialog.IOptions<any>> = {
title: 'Run pipeline',
Expand Down
4 changes: 4 additions & 0 deletions packages/pipeline-editor/style/index.css
Expand Up @@ -20,6 +20,10 @@
text-align: center;
}

.elyra-tooltipError {
color: var(--jp-error-color1);
}

.elyra-PipelineEditor .properties-modal {
position: absolute;
}
Expand Down