Skip to content

Commit

Permalink
Merge pull request #340 from XpressAI/paul/attached-literals
Browse files Browse the repository at this point in the history
Allow attaching Literals to their target nodes
  • Loading branch information
MFA-X-AI authored Jul 15, 2024
2 parents 13605d2 + 05d5081 commit a464313
Show file tree
Hide file tree
Showing 17 changed files with 203 additions and 71 deletions.
10 changes: 7 additions & 3 deletions src/commands/NodeActionCommands.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -785,7 +785,11 @@ export function addNodeActionCommands(
});
return null;
}


const connections = Object.values(selected_node.ports)
.map((p: CustomPortModel) => Object.keys(p.links).length)
.reduce((a, b) => a+b);

const literalType = selected_node["extras"]["type"];
let oldValue = selected_node.getPorts()["out-0"].getOptions()["label"];

Expand All @@ -794,8 +798,8 @@ export function addNodeActionCommands(
}

const updateTitle = `Update ${literalType}`;
let nodeData: CustomNodeModelOptions = {color: selected_node["color"], type: selected_node["extras"]["type"]}
let updatedContent = await handleLiteralInput(selected_node["name"], nodeData, oldValue, literalType, updateTitle);
let nodeData: CustomNodeModelOptions = {color: selected_node["color"], type: selected_node["extras"]["type"], extras: {attached: selected_node["extras"]["attached"]}}
let updatedContent = await handleLiteralInput(selected_node["name"], nodeData, oldValue, literalType, updateTitle, connections);

if (!updatedContent) {
// handle case where Cancel was clicked or an error occurred
Expand Down
8 changes: 4 additions & 4 deletions src/components/link/CustomLinkFactory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,15 @@ namespace S {

function addHover(model: TriangleLinkModel | ParameterLinkModel){
return (() => {
document.querySelector(`div.port[data-nodeid='${model.getSourcePort().getNode().getID()}'][data-name='${model.getSourcePort().getName()}']>div>div`).classList.add("hover");
document.querySelector(`div.port[data-nodeid="${model.getTargetPort().getNode().getID()}"][data-name='${model.getTargetPort().getName()}']>div>div`).classList.add("hover");
document.querySelector(`div.port[data-nodeid='${model.getSourcePort().getNode().getID()}'][data-name='${model.getSourcePort().getName()}']>div>div`)?.classList?.add("hover");
document.querySelector(`div.port[data-nodeid="${model.getTargetPort().getNode().getID()}"][data-name='${model.getTargetPort().getName()}']>div>div`)?.classList?.add("hover");
});
}

function removeHover(model: TriangleLinkModel | ParameterLinkModel){
return () => {
document.querySelector(`div.port[data-nodeid='${model.getSourcePort().getNode().getID()}'][data-name='${model.getSourcePort().getName()}']>div>div`).classList.remove("hover");
document.querySelector(`div.port[data-nodeid="${model.getTargetPort().getNode().getID()}"][data-name='${model.getTargetPort().getName()}']>div>div`).classList.remove("hover");
document.querySelector(`div.port[data-nodeid='${model.getSourcePort().getNode().getID()}'][data-name='${model.getSourcePort().getName()}']>div>div`)?.classList?.remove("hover");
document.querySelector(`div.port[data-nodeid="${model.getTargetPort().getNode().getID()}"][data-name='${model.getTargetPort().getName()}']>div>div`)?.classList?.remove("hover");
}
}

Expand Down
45 changes: 26 additions & 19 deletions src/components/node/CustomNodeWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,18 @@ const CommentNode = ({ node }) => {
);
};

const PortsComponent = ({node, engine, app}) => {
const renderPort = (port) => {
return <CustomPortLabel engine={engine} port={port} key={port.getID()} node={node} app={app} />
};
return (
<S.Ports>
<S.PortsContainer>{_.map(node.getInPorts(), renderPort)}</S.PortsContainer>
<S.PortsContainer>{_.map(node.getOutPorts(), renderPort)}</S.PortsContainer>
</S.Ports>
)
}

const ParameterNode = ({ node, engine, app }) => {
const handleEditParameter = () => {
const nodeName = node.getOptions()["name"];
Expand All @@ -201,6 +213,10 @@ const ParameterNode = ({ node, engine, app }) => {
app.commands.execute(commandIDs.editNode);
};

if(node.getOptions().extras['attached']){
return <></>;
}

return (
<S.Node
borderColor={node.getOptions().extras["borderColor"]}
Expand All @@ -214,15 +230,12 @@ const ParameterNode = ({ node, engine, app }) => {
{/* <S.IconContainer>{getNodeIcon('parameter')}</S.IconContainer> */}
<S.TitleName>{node.getOptions().name}</S.TitleName>
</S.Title>
<S.Ports>
<S.PortsContainer>{_.map(node.getInPorts(), port => <CustomPortLabel engine={engine} port={port} key={port.getID()} node={node} />)}</S.PortsContainer>
<S.PortsContainer>{_.map(node.getOutPorts(), port => <CustomPortLabel engine={engine} port={port} key={port.getID()} node={node} />)}</S.PortsContainer>
</S.Ports>
<PortsComponent node={node} engine={engine} app={app}/>
</S.Node>
);
};

const StartFinishNode = ({ node, engine, handleDeletableNode }) => (
const StartFinishNode = ({ node, engine, handleDeletableNode, app }) => (
<S.Node
borderColor={node.getOptions().extras["borderColor"]}
data-default-node-name={node.getOptions().name}
Expand All @@ -237,14 +250,11 @@ const StartFinishNode = ({ node, engine, handleDeletableNode }) => (
<Toggle className='lock' checked={node.isLocked() ?? false} onChange={event => handleDeletableNode('nodeDeletable', event)} />
</label>
</S.Title>
<S.Ports>
<S.PortsContainer>{_.map(node.getInPorts(), port => <CustomPortLabel engine={engine} port={port} key={port.getID()} node={node} />)}</S.PortsContainer>
<S.PortsContainer>{_.map(node.getOutPorts(), port => <CustomPortLabel engine={engine} port={port} key={port.getID()} node={node} />)}</S.PortsContainer>
</S.Ports>
<PortsComponent node={node} engine={engine} app={app}/>
</S.Node>
);

const WorkflowNode = ({ node, engine, handleDeletableNode }) => {
const WorkflowNode = ({ node, engine, app, handleDeletableNode }) => {
const elementRef = React.useRef<HTMLElement>(null);
return (
<div style={{ position: "relative" }}>
Expand All @@ -265,16 +275,13 @@ const WorkflowNode = ({ node, engine, handleDeletableNode }) => {
<Toggle className='lock' checked={node.isLocked() ?? false} onChange={event => handleDeletableNode('nodeDeletable', event)} />
</label>
</S.Title>
<S.Ports>
<S.PortsContainer>{_.map(node.getInPorts(), port => <CustomPortLabel engine={engine} port={port} key={port.getID()} node={node} />)}</S.PortsContainer>
<S.PortsContainer>{_.map(node.getOutPorts(), port => <CustomPortLabel engine={engine} port={port} key={port.getID()} node={node} />)}</S.PortsContainer>
</S.Ports>
<PortsComponent node={node} engine={engine} app={app}/>
</S.WorkflowNode>
</div>
);
};

const ComponentLibraryNode = ({ node, engine, shell, handleDeletableNode }) => {
const ComponentLibraryNode = ({ node, engine, shell, app, handleDeletableNode }) => {
const [showDescription, setShowDescription] = React.useState(false);
const [descriptionStr, setDescriptionStr] = React.useState("");
const elementRef = React.useRef<HTMLElement>(null);
Expand Down Expand Up @@ -330,10 +337,7 @@ const ComponentLibraryNode = ({ node, engine, shell, handleDeletableNode }) => {
<Toggle className='description' name='Description' checked={showDescription ?? false} onChange={handleDescription} />
</label>
</S.Title>
<S.Ports>
<S.PortsContainer>{_.map(node.getInPorts(), port => <CustomPortLabel engine={engine} port={port} key={port.getID()} node={node} />)}</S.PortsContainer>
<S.PortsContainer>{_.map(node.getOutPorts(), port => <CustomPortLabel engine={engine} port={port} key={port.getID()} node={node} />)}</S.PortsContainer>
</S.Ports>
<PortsComponent node={node} engine={engine} app={app}/>
</S.Node>
{(node.getOptions().extras["tip"] != undefined && node.getOptions().extras["tip"] != "") ?
<ReactTooltip
Expand Down Expand Up @@ -407,6 +411,7 @@ export class CustomNodeWidget extends React.Component<DefaultNodeProps> {
return <WorkflowNode
node={node}
engine={engine}
app={app}
handleDeletableNode={this.handleDeletableNode}
/>;
}
Expand All @@ -415,6 +420,7 @@ export class CustomNodeWidget extends React.Component<DefaultNodeProps> {
return <StartFinishNode
node={node}
engine={engine}
app={app}
handleDeletableNode={this.handleDeletableNode}
/>;
}
Expand All @@ -423,6 +429,7 @@ export class CustomNodeWidget extends React.Component<DefaultNodeProps> {
node={node}
engine={engine}
shell={shell}
app={app}
handleDeletableNode={this.handleDeletableNode}
/>;
}
Expand Down
130 changes: 113 additions & 17 deletions src/components/port/CustomPortLabel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@ import * as React from 'react';
import { DiagramEngine, PortWidget } from '@projectstorm/react-diagrams-core';
import { DefaultNodeModel, DefaultPortModel } from "@projectstorm/react-diagrams";
import styled from '@emotion/styled';
import { JupyterFrontEnd } from "@jupyterlab/application";
import { commandIDs } from "../../commands/CommandIDs";
import Color from "colorjs.io";

export interface CustomPortLabelProps {
port: DefaultPortModel;
engine: DiagramEngine;
node: DefaultNodeModel;
app: JupyterFrontEnd;
}

namespace S {
Expand All @@ -24,7 +28,7 @@ namespace S {
max-width: 40ch;
`;

export const SymbolContainer = styled.div<{ symbolType: string; selected: boolean; isOutPort: boolean }>`
export const SymbolContainer = styled.div<{ symbolType: string; selected: boolean; isOutPort: boolean; attachedColor?: string }>`
width: 15px;
height: 15px;
background: ${(p) => (p.selected ? 'oklch(1 0 0 / 0.5)' : 'oklch(50% 0 0 / 0.2)')};
Expand All @@ -39,6 +43,18 @@ namespace S {
background: rgb(192, 255, 0);
box-shadow: ${(p) => p.selected ? '' : 'inset'} 0 4px 8px rgb(0 0 0 / 0.5);
}
&.attached {
padding: 0 2px 0 3px;
border-radius: 20px;
border: 0;
background: ${(p) => p.attachedColor};
&:hover, &.hover {
background: rgb(192, 255, 0);
box-shadow: ${(p) => p.selected ? '' : 'inset'} 0 4px 8px rgb(0 0 0 / 0.5);
}
}
`;

export const Symbol = styled.div<{ selected: boolean; isOutPort: boolean }>`
Expand All @@ -49,7 +65,7 @@ namespace S {
padding:${(p) => (p.isOutPort ? '2px 0px 0px 2px' : '2px 2px 0px 0px')};
`;

export const Port = styled.div<{ isOutPort: boolean, hasLinks: boolean }>`
export const Port = styled.div<{ isOutPort: boolean, hasLinks: boolean; attachedColor?: string}>`
width: 15px;
height: 15px;
background: ${(p) => p.hasLinks ? 'oklch(1 0 0 / 0.5)' : 'oklch(50% 0 0 / 0.2)'};
Expand All @@ -73,9 +89,42 @@ namespace S {
stroke-linecap: round;
stroke-linejoin: round;
}
&.attached {
padding: 0 2px 0 3px;
border-radius: 20px;
border: 0;
background: ${(p) => p.attachedColor};
&:hover, &.hover {
background: rgb(192, 255, 0);
box-shadow: ${(p) => p.hasLinks ? '' : 'inset'} 0 4px 8px rgb(0 0 0 / 0.5);
}
}
`;
}

const PortLabel = ({nodeType, port}) => {
const LITERAL_SECRET = "Literal Secret";

let labelText = port.getOptions().label.replace('▶', '').trim();

let attached = false;
if(port.getOptions().in){
Object.values(port.links).forEach(link => {
if(link['sourcePort']['parent']['name'].startsWith('Literal ') && link['sourcePort']['parent']['extras']['attached']){
attached = true;
const label = link['sourcePort']['parent']['name'] === LITERAL_SECRET ? "*****" : link['sourcePort']['options']['label']
labelText += ": "+(label.length > 18 ? label.substring(0, 15)+"..." : label)
}
})
}

return (
<S.Label style={{ textAlign: (!port.getOptions().in && port.getOptions().label === '▶') ? 'right' : 'left', cursor: attached ? 'pointer' : 'inherit' }}>
{nodeType === LITERAL_SECRET ? "*****" : labelText}
</S.Label>
);
}

export class CustomPortLabel extends React.Component<CustomPortLabelProps> {
render() {
let portName = this.props.port.getOptions().name;
Expand Down Expand Up @@ -123,8 +172,36 @@ export class CustomPortLabel extends React.Component<CustomPortLabelProps> {
/* Workaround for Arguments being set up as triangle ports in other places */
!this.props.node['name'].match('Argument \(.+?\):');

let dblClickHandler = () => {};
let attachedColor = null;
if(this.props.port.getOptions().in){
Object.values(this.props.port.links).forEach(link => {
if(link['sourcePort']['parent']['name'].startsWith('Literal ') && link['sourcePort']['parent']['extras']['attached']){
attachedColor = link['sourcePort']['parent']['options']['color'];

dblClickHandler = () => {
this.props.engine.getModel().clearSelection();
link['sourcePort']['parent'].setSelected(true);
this.props.app.commands.execute(commandIDs.editNode);
}
}
})
}

if(attachedColor != null){
const color = new Color(attachedColor);
color.alpha = 0.75;
color.oklch.c *= 1.2;
const color1 = color.to('oklch').toString()
color.oklch.c *= 1.2;
color.oklch.l /= 2;
const color2 = color.to('oklch').toString()

attachedColor = `linear-gradient(${color1}, ${color2})`;
}

const port = (
<S.Port isOutPort={!isIn} hasLinks={hasLinks}>
<S.Port isOutPort={!isIn} hasLinks={hasLinks} className={attachedColor ? 'attached' : null} attachedColor={attachedColor}>
{!isTrianglePort ? null : (isIn ?
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" >
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
Expand All @@ -148,7 +225,7 @@ export class CustomPortLabel extends React.Component<CustomPortLabelProps> {


const symbol = (
<S.SymbolContainer symbolType={symbolLabel} selected={portHasLink} isOutPort={isOutPort}>
<S.SymbolContainer symbolType={symbolLabel} selected={portHasLink} isOutPort={isOutPort} className={attachedColor ? 'attached' : null} attachedColor={attachedColor}>
<S.Symbol isOutPort={isOutPort} selected={portHasLink}>
{symbolLabel}
</S.Symbol>
Expand All @@ -159,40 +236,59 @@ export class CustomPortLabel extends React.Component<CustomPortLabelProps> {
function addHover(port: DefaultPortModel) {
return (() => {
for (let linksKey in port.getLinks()) {
document.querySelector(`g[data-linkid='${linksKey}']`).classList.add("hover");
document.querySelector(`g[data-linkid='${linksKey}']`)?.classList.add("hover");
const model = port.getLinks()[linksKey]
if(model.getSourcePort() != null)
document.querySelector(`div.port[data-nodeid="${model.getSourcePort().getNode().getID()}"][data-name='${model.getSourcePort().getName()}']>div>div`).classList.add("hover");
document.querySelector(`div.port[data-nodeid="${model.getSourcePort().getNode().getID()}"][data-name='${model.getSourcePort().getName()}']>div>div`)?.classList.add("hover");
if(model.getTargetPort() != null)
document.querySelector(`div.port[data-nodeid="${model.getTargetPort().getNode().getID()}"][data-name='${model.getTargetPort().getName()}']>div>div`).classList.add("hover");
document.querySelector(`div.port[data-nodeid="${model.getTargetPort().getNode().getID()}"][data-name='${model.getTargetPort().getName()}']>div>div`)?.classList.add("hover");
if(attachedColor != null){
if(model.getSourcePort() != null){
Object.values(model.getSourcePort().getNode().getPorts()).forEach(p => {
Object.values(p.getLinks()).forEach(l => {
if(model.getTargetPort() != null)
document.querySelector(`div.port[data-nodeid="${l.getTargetPort().getNode().getID()}"][data-name='${l.getTargetPort().getName()}']>div>div`)?.classList.add("hover");
})
})
}
}
}
});
}

function removeHover(port: DefaultPortModel) {
return () => {
for (let linksKey in port.getLinks()) {
document.querySelector(`g[data-linkid='${linksKey}']`).classList.remove("hover");
document.querySelector(`g[data-linkid='${linksKey}']`)?.classList.remove("hover");
const model = port.getLinks()[linksKey]
if(model.getSourcePort() != null)
document.querySelector(`div.port[data-nodeid="${model.getSourcePort().getNode().getID()}"][data-name='${model.getSourcePort().getName()}']>div>div`).classList.remove("hover");
document.querySelector(`div.port[data-nodeid="${model.getSourcePort().getNode().getID()}"][data-name='${model.getSourcePort().getName()}']>div>div`)?.classList.remove("hover");
if(model.getTargetPort() != null)
document.querySelector(`div.port[data-nodeid="${model.getTargetPort().getNode().getID()}"][data-name='${model.getTargetPort().getName()}']>div>div`).classList.remove("hover");
document.querySelector(`div.port[data-nodeid="${model.getTargetPort().getNode().getID()}"][data-name='${model.getTargetPort().getName()}']>div>div`)?.classList.remove("hover");
if(attachedColor != null){
if(model.getSourcePort() != null){
Object.values(model.getSourcePort().getNode().getPorts()).forEach(p => {
Object.values(p.getLinks()).forEach(l => {
if(model.getTargetPort() != null)
document.querySelector(`div.port[data-nodeid="${l.getTargetPort().getNode().getID()}"][data-name='${l.getTargetPort().getName()}']>div>div`)?.classList.remove("hover");
})
})
}
}
}
};
}

const label = (
<S.Label style={{ textAlign: (!this.props.port.getOptions().in && this.props.port.getOptions().label === '▶') ? 'right' : 'left' }}>
{nodeType === "Literal Secret" ? "*****" : this.props.port.getOptions().label.replace('▶', '').trim()}
</S.Label>);
const label = <PortLabel port={this.props.port} nodeType={nodeType} />

return (
<S.PortLabel>
<S.PortLabel onMouseOver={addHover(this.props.port)}
onMouseOut={removeHover(this.props.port)}
onDoubleClick={dblClickHandler}
>
{this.props.port.getOptions().in ? null : label}
<PortWidget engine={this.props.engine} port={this.props.port}>
<div onMouseOver={addHover(this.props.port)}
onMouseOut={removeHover(this.props.port)}>
<div>
{symbolLabel == null ? port : symbol}
</div>
</PortWidget>
Expand Down
3 changes: 3 additions & 0 deletions src/components/port/CustomPortModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ export class CustomPortModel extends DefaultPortModel {
}

canLinkToPort(port: CustomPortModel): boolean {
// No self connections allowed
if(port === this) return false;

if (port instanceof DefaultPortModel) {
if(this.options.in === port.getOptions().in){
port.getNode().getOptions().extras["borderColor"]="red";
Expand Down
Loading

0 comments on commit a464313

Please sign in to comment.