diff --git a/.github/workflows/release-doc.yml b/.github/workflows/release-doc.yml index ea7fb697f21..22f3605c984 100644 --- a/.github/workflows/release-doc.yml +++ b/.github/workflows/release-doc.yml @@ -4,6 +4,7 @@ name: Release "on": + pull_request: push: branches: - master diff --git a/docs/modules/ROOT/images/modifying-node-properties-example.png b/docs/modules/ROOT/images/modifying-node-properties-example.png new file mode 100644 index 00000000000..82d044ad61c Binary files /dev/null and b/docs/modules/ROOT/images/modifying-node-properties-example.png differ diff --git a/docs/modules/ROOT/pages/operations/index.adoc b/docs/modules/ROOT/pages/operations/index.adoc index 8dacd13ab43..ee7afb22815 100644 --- a/docs/modules/ROOT/pages/operations/index.adoc +++ b/docs/modules/ROOT/pages/operations/index.adoc @@ -17,4 +17,4 @@ * xref:operations/rest-requests.adoc[] -- HTTP REST commands. * xref:operations/browser-rbac-count.adoc[] -- Manual refresh of counts due to RBAC. * xref:operations/product-analytics.adoc[] -- Configure consent for anonymous usage statistics. - +* xref:operations/on-canvas-operations.adoc[] -- On-Canvas Operations. diff --git a/docs/modules/ROOT/pages/operations/on-canvas-operations.adoc b/docs/modules/ROOT/pages/operations/on-canvas-operations.adoc new file mode 100644 index 00000000000..38268ba8b8e --- /dev/null +++ b/docs/modules/ROOT/pages/operations/on-canvas-operations.adoc @@ -0,0 +1,20 @@ +:description: On-Canvas Operations + + +[[on-canvas-operations]] += On-Canvas Operations + +== Double-Clicking the Canvas + +*Double clicking the white area* on canvas will automatically generate a new node with + +1. an auto-incremented ID, i.e. (the max ID of nodes on canvas) + 1 +2. a label of "Undefined" +3. the node caption of "New Node" + +== Editing the Node Properties in Inspector Panel + +*Clicking the property value* in node inspector panel will make that property editable and un-clicking the text box +(by for example clicking somewhere else) will persist the new value into Neo4J database: + +image:modifying-node-properties-example.png[width=600] diff --git a/e2e_tests/integration/viz.spec.ts b/e2e_tests/integration/viz.spec.ts index c063dadc1b7..848348f957d 100644 --- a/e2e_tests/integration/viz.spec.ts +++ b/e2e_tests/integration/viz.spec.ts @@ -257,6 +257,13 @@ describe('Viz rendering', () => { 'Undefined' ) + cy.get('[data-testid="nodeGroups"]') + .contains('New Node') + .trigger('mouseover') + .get('[data-testid="viz-details-pane-properties-table"]') + .find('td:nth-child(4)') + .should('have.text', 'description') + cy.executeCommand('MATCH (n) DETACH DELETE n') }) }) diff --git a/src/browser/modules/Sidebar/__snapshots__/GuideDrawer.test.tsx.snap b/src/browser/modules/Sidebar/__snapshots__/GuideDrawer.test.tsx.snap index fdc724e488a..9816124a6b4 100644 --- a/src/browser/modules/Sidebar/__snapshots__/GuideDrawer.test.tsx.snap +++ b/src/browser/modules/Sidebar/__snapshots__/GuideDrawer.test.tsx.snap @@ -3,15 +3,15 @@ exports[`GuideDrawer renders guide slide when a guide is selected 1`] = `

@@ -48,54 +48,54 @@ exports[`GuideDrawer renders guide slide when a guide is selected 1`] = ` exports[`GuideDrawer renders list view including Remote Guides list 1`] = `

Neo4j Browser Guides

You can also access Browser guides by running :guide [guide name] in the code editor.
Built-in guides
  • :guide intro
    Navigating Neo4j Browser @@ -103,17 +103,17 @@ exports[`GuideDrawer renders list view including Remote Guides list 1`] = `
  • :guide concepts
    Property graph model concepts @@ -121,17 +121,17 @@ exports[`GuideDrawer renders list view including Remote Guides list 1`] = `
  • :guide cypher
    Cypher basics - create, match, delete @@ -139,17 +139,17 @@ exports[`GuideDrawer renders list view including Remote Guides list 1`] = `
  • :guide movie-graph
    Queries and recommendations with Cypher - movie use case} @@ -157,17 +157,17 @@ exports[`GuideDrawer renders list view including Remote Guides list 1`] = `
  • :guide northwind-graph
    Translate and import relation data into graph @@ -176,29 +176,29 @@ exports[`GuideDrawer renders list view including Remote Guides list 1`] = `
  • Remote Guides
  • Title
  • Neo4j Browser Guides

    You can also access Browser guides by running :guide [guide name] in the code editor.
    Built-in guides
  • :guide intro
    Navigating Neo4j Browser @@ -283,17 +283,17 @@ exports[`GuideDrawer renders list view without Remote Guides 1`] = `
  • :guide concepts
    Property graph model concepts @@ -301,17 +301,17 @@ exports[`GuideDrawer renders list view without Remote Guides 1`] = `
  • :guide cypher
    Cypher basics - create, match, delete @@ -319,17 +319,17 @@ exports[`GuideDrawer renders list view without Remote Guides 1`] = `
  • :guide movie-graph
    Queries and recommendations with Cypher - movie use case} @@ -337,17 +337,17 @@ exports[`GuideDrawer renders list view without Remote Guides 1`] = `
  • :guide northwind-graph
    Translate and import relation data into graph @@ -356,10 +356,10 @@ exports[`GuideDrawer renders list view without Remote Guides 1`] = `
  • Browser Settings

    Test åäö settings
    Product Analytics
    diff --git a/src/browser/modules/Stream/CypherFrame/ErrorsView/__snapshots__/ErrorsStatusbar.test.tsx.snap b/src/browser/modules/Stream/CypherFrame/ErrorsView/__snapshots__/ErrorsStatusbar.test.tsx.snap index 3c58f5993c6..f6b1acdda1e 100644 --- a/src/browser/modules/Stream/CypherFrame/ErrorsView/__snapshots__/ErrorsStatusbar.test.tsx.snap +++ b/src/browser/modules/Stream/CypherFrame/ErrorsView/__snapshots__/ErrorsStatusbar.test.tsx.snap @@ -3,14 +3,14 @@ exports[`ErrorsStatusbar displays error 1`] = `
    diff --git a/src/browser/modules/Stream/CypherFrame/RelatableView/__snapshots__/relatable-view.test.tsx.snap b/src/browser/modules/Stream/CypherFrame/RelatableView/__snapshots__/relatable-view.test.tsx.snap index 33007397161..170d78dc2b3 100644 --- a/src/browser/modules/Stream/CypherFrame/RelatableView/__snapshots__/relatable-view.test.tsx.snap +++ b/src/browser/modules/Stream/CypherFrame/RelatableView/__snapshots__/relatable-view.test.tsx.snap @@ -3,10 +3,10 @@ exports[`RelatableViews RelatableView displays bodyMessage if no rows 1`] = `
    (no changes, no records)
    @@ -17,10 +17,10 @@ exports[`RelatableViews RelatableView displays bodyMessage if no rows 1`] = ` exports[`RelatableViews RelatableView does not crash if key is empty string 1`] = `
    "String value" @@ -89,10 +89,10 @@ exports[`RelatableViews RelatableView does not crash if key is empty string 1`] exports[`RelatableViews RelatableView does not display bodyMessage if rows, and escapes HTML 1`] = `
    "String with HTML <strong>in</strong> it" @@ -161,7 +161,7 @@ exports[`RelatableViews RelatableView does not display bodyMessage if rows, and exports[`RelatableViews TableStatusbar displays no statusBarMessage 1`] = `
    `; @@ -169,10 +169,10 @@ exports[`RelatableViews TableStatusbar displays no statusBarMessage 1`] = ` exports[`RelatableViews TableStatusbar displays statusBarMessage 1`] = `
    Started streaming 1 records after 5 ms and completed after 10 ms. diff --git a/src/browser/modules/Stream/CypherFrame/VisualizationView/PropertiesPanelContent/DetailsPane.tsx b/src/browser/modules/Stream/CypherFrame/VisualizationView/PropertiesPanelContent/DetailsPane.tsx index d1ce432d8ea..c0431e241c8 100644 --- a/src/browser/modules/Stream/CypherFrame/VisualizationView/PropertiesPanelContent/DetailsPane.tsx +++ b/src/browser/modules/Stream/CypherFrame/VisualizationView/PropertiesPanelContent/DetailsPane.tsx @@ -30,7 +30,8 @@ export const DETAILS_PANE_STEP_SIZE = 1000 export function DetailsPane({ vizItem, graphStyle, - nodeInspectorWidth + nodeInspectorWidth, + onGraphInteraction }: DetailsPaneProps): JSX.Element { const [maxPropertiesCount, setMaxPropertiesCount] = useState( DETAILS_PANE_STEP_SIZE @@ -70,6 +71,9 @@ export function DetailsPane({ relType: vizItem.item.type }} graphStyle={graphStyle} + onGraphInteraction={onGraphInteraction} + sourceNodeId={vizItem.item.source.id} + targetNodeId={vizItem.item.target.id} /> )} {vizItem.type === 'node' && @@ -82,6 +86,8 @@ export function DetailsPane({ label, propertyKeys: vizItem.item.propertyList.map(p => p.key) }} + onGraphInteraction={onGraphInteraction} + nodeId={vizItem.item.id} /> ) })} @@ -93,6 +99,7 @@ export function DetailsPane({ moreStep={DETAILS_PANE_STEP_SIZE} totalNumItems={allItemProperties.length} nodeInspectorWidth={nodeInspectorWidth} + onGraphInteraction={onGraphInteraction} /> diff --git a/src/browser/modules/Stream/CypherFrame/VisualizationView/PropertiesPanelContent/StyleableNodeLabel.tsx b/src/browser/modules/Stream/CypherFrame/VisualizationView/PropertiesPanelContent/StyleableNodeLabel.tsx index a3f510ab537..ea3fce06304 100644 --- a/src/browser/modules/Stream/CypherFrame/VisualizationView/PropertiesPanelContent/StyleableNodeLabel.tsx +++ b/src/browser/modules/Stream/CypherFrame/VisualizationView/PropertiesPanelContent/StyleableNodeLabel.tsx @@ -21,7 +21,11 @@ import React from 'react' import { Popup } from 'semantic-ui-react' import { StyledLabelChip } from 'neo4j-arc/common' -import { GraphStyleModel } from 'neo4j-arc/graph-visualization' +import { + GraphInteractionCallBack, + GraphStyleModel, + NODE_LABEL_UPDATE +} from 'neo4j-arc/graph-visualization' import { GrassEditor } from './GrassEditor' @@ -34,11 +38,15 @@ export type StyleableNodeLabelProps = { graphStyle: GraphStyleModel /* The total number of nodes in returned graph */ allNodesCount?: number | null + onGraphInteraction?: GraphInteractionCallBack + nodeId?: string } export function StyleableNodeLabel({ graphStyle, selectedLabel, - allNodesCount + allNodesCount, + onGraphInteraction = () => undefined, + nodeId }: StyleableNodeLabelProps): JSX.Element { const labels = selectedLabel.label === '*' ? [] : [selectedLabel.label] const graphStyleForLabel = graphStyle.forNode({ @@ -48,26 +56,40 @@ export function StyleableNodeLabel({ selectedLabel.label === '*' ? allNodesCount : selectedLabel.count return ( - - {`${selectedLabel.label}${count || count === 0 ? ` (${count})` : ''}`} - +
    + onGraphInteraction(NODE_LABEL_UPDATE, { + nodeId: nodeId, + oldLabel: labels[0], + newLabel: e.currentTarget.textContent + }) } > - - + + {`${selectedLabel.label}${ + count || count === 0 ? ` (${count})` : '' + }`} + + } + > + + +
    ) } diff --git a/src/browser/modules/Stream/CypherFrame/VisualizationView/PropertiesPanelContent/StyleableRelType.tsx b/src/browser/modules/Stream/CypherFrame/VisualizationView/PropertiesPanelContent/StyleableRelType.tsx index e79bfdc0ba2..a5ac847a924 100644 --- a/src/browser/modules/Stream/CypherFrame/VisualizationView/PropertiesPanelContent/StyleableRelType.tsx +++ b/src/browser/modules/Stream/CypherFrame/VisualizationView/PropertiesPanelContent/StyleableRelType.tsx @@ -21,44 +21,67 @@ import React from 'react' import { Popup } from 'semantic-ui-react' import { StyledRelationshipChip } from 'neo4j-arc/common' -import { GraphStyleModel } from 'neo4j-arc/graph-visualization' +import { + GraphInteractionCallBack, + GraphStyleModel, + REL_TYPE_UPDATE +} from 'neo4j-arc/graph-visualization' import { GrassEditor } from './GrassEditor' export type StyleableRelTypeProps = { graphStyle: GraphStyleModel selectedRelType: { relType: string; propertyKeys: string[]; count?: number } + onGraphInteraction?: GraphInteractionCallBack + sourceNodeId?: string + targetNodeId?: string } export function StyleableRelType({ selectedRelType, - graphStyle + graphStyle, + onGraphInteraction = () => undefined, + sourceNodeId, + targetNodeId }: StyleableRelTypeProps): JSX.Element { const styleForRelType = graphStyle.forRelationship({ type: selectedRelType.relType }) return ( - - {selectedRelType.count !== undefined - ? `${selectedRelType.relType} (${selectedRelType.count})` - : `${selectedRelType.relType}`} - +
    + onGraphInteraction(REL_TYPE_UPDATE, { + sourceNodeId: sourceNodeId, + targetNodeId: targetNodeId, + oldType: selectedRelType.relType, + newType: e.currentTarget.textContent + }) } - wide > - - + + {selectedRelType.count !== undefined + ? `${selectedRelType.relType} (${selectedRelType.count})` + : `${selectedRelType.relType}`} + + } + wide + > + + +
    ) } diff --git a/src/browser/modules/Stream/CypherFrame/VisualizationView/VisualizationView.tsx b/src/browser/modules/Stream/CypherFrame/VisualizationView/VisualizationView.tsx index e3381dadb3d..d51ac07fde0 100644 --- a/src/browser/modules/Stream/CypherFrame/VisualizationView/VisualizationView.tsx +++ b/src/browser/modules/Stream/CypherFrame/VisualizationView/VisualizationView.tsx @@ -28,7 +28,11 @@ import { GraphInteractionCallBack, GraphModel, GraphVisualizer, - NODE_ON_CANVAS_CREATE + REL_ON_CANVAS_CREATE, + NODE_ON_CANVAS_CREATE, + NODE_PROP_UPDATE, + NODE_LABEL_UPDATE, + REL_TYPE_UPDATE } from 'neo4j-arc/graph-visualization' import { StyledVisContainer } from './VisualizationView.styled' @@ -277,6 +281,123 @@ LIMIT ${maxNewNeighbours}` } onGraphInteraction: GraphInteractionCallBack = (event, properties) => { + if (event == REL_TYPE_UPDATE) { + if (properties == null) { + throw new Error( + 'A property map with sourceNodeId, targetNodeId, oldType, and newType keys are required' + ) + } + + const sourceNodeId = properties['sourceNodeId'] + const targetNodeId = properties['targetNodeId'] + const oldType = properties['oldType'] + const newType = properties['newType'] + + const type = `\`${oldType}\`` + + const query = `MATCH (source)-[rel:${type}]->(target) WHERE ID(source) = ${sourceNodeId} AND ID(target) = ${targetNodeId} CALL apoc.refactor.setType(rel, '${newType}') YIELD input, output RETURN input, output;` + + this.props.bus.self( + CYPHER_REQUEST, + { + query, + params: { sourceNodeId, targetNodeId, oldType, newType }, + queryType: NEO4J_BROWSER_USER_ACTION_QUERY + }, + (response: any) => { + if (!response.success) { + throw new Error(response.error) + } + } + ) + } + + if (event == NODE_LABEL_UPDATE) { + if (properties == null) { + throw new Error( + 'A property map with nodeId, oldLabel, and newLabel keys are required' + ) + } + + const nodeId = properties['nodeId'] + const oldLabel = `\`${properties['oldLabel']}\`` + const newLabel = `\`${properties['newLabel']}\`` + + const query = `MATCH(n) WHERE ID(n) = ${nodeId} REMOVE n:${oldLabel} SET n:${newLabel}` + console.log(query) + + this.props.bus.self( + CYPHER_REQUEST, + { + query, + params: { nodeId, oldLabel, newLabel }, + queryType: NEO4J_BROWSER_USER_ACTION_QUERY + }, + (response: any) => { + if (!response.success) { + throw new Error(response.error) + } + } + ) + } + + if (event == NODE_PROP_UPDATE) { + if (properties == null) { + throw new Error( + 'A property map with nodeId, propKey, and propVal keys are required' + ) + } + + const nodeId = properties['nodeId'] + const propKey = properties['propKey'] + const propVal = properties['propVal'] + + const query = `MATCH (n) WHERE ID(n) = ${nodeId} SET n.${propKey} = "${propVal}"` + console.log(query) + + this.props.bus.self( + CYPHER_REQUEST, + { + query, + params: { nodeId, propKey, propVal }, + queryType: NEO4J_BROWSER_USER_ACTION_QUERY + }, + (response: any) => { + if (!response.success) { + throw new Error(response.error) + } + } + ) + } + + if (event == REL_ON_CANVAS_CREATE) { + if (properties == null) { + throw new Error( + 'A property map with sourceNodeId, targetNodeId, and type keys are required' + ) + } + + const sourceNodeId = properties['sourceNodeId'] + const targetNodeId = properties['targetNodeId'] + const type = properties['type'] + + const query = `MATCH (source), (target) WHERE ID(source) = ${sourceNodeId} AND ID(target) = ${targetNodeId} CREATE (source)-[r:\`${type}\` {name: "new link"}]->(target) RETURN type(r);` + + this.props.bus.self( + CYPHER_REQUEST, + { + query, + params: { sourceNodeId, targetNodeId, type }, + queryType: NEO4J_BROWSER_USER_ACTION_QUERY + }, + (response: any) => { + if (!response.success) { + throw new Error(response.error) + } + } + ) + } + if (event == NODE_ON_CANVAS_CREATE) { if (properties == null) { throw new Error( @@ -286,18 +407,19 @@ LIMIT ${maxNewNeighbours}` const id = properties['id'] const name = properties['name'] + const description = properties['description'] const variableName = `node${id}` const labels = (properties['labels'] as string[]) .map(label => `\`${label}\``) .join(':') - const query = `CREATE (${variableName}:${labels} { id: ${id}, name: "${name}" });` + const query = `CREATE (${variableName}:${labels} { id: ${id}, name: "${name}", description: "${description}" });` this.props.bus.self( CYPHER_REQUEST, { query, - params: { labels, id, name }, + params: { labels, id, name, description }, queryType: NEO4J_BROWSER_USER_ACTION_QUERY }, (response: any) => { diff --git a/src/browser/modules/Stream/CypherFrame/__snapshots__/AsciiView.test.tsx.snap b/src/browser/modules/Stream/CypherFrame/__snapshots__/AsciiView.test.tsx.snap index ec5111e4766..d35300c8287 100644 --- a/src/browser/modules/Stream/CypherFrame/__snapshots__/AsciiView.test.tsx.snap +++ b/src/browser/modules/Stream/CypherFrame/__snapshots__/AsciiView.test.tsx.snap @@ -3,10 +3,10 @@ exports[`AsciiViews AsciiStatusbar displays statusBarMessage if no rows 1`] = `
    Completed after 10 ms.
    @@ -17,17 +17,17 @@ exports[`AsciiViews AsciiStatusbar displays statusBarMessage if no rows 1`] = ` exports[`AsciiViews AsciiStatusbar displays statusBarMessage if no rows 2`] = `
    Max column width:
    (no changes, no records)
    @@ -56,10 +56,10 @@ exports[`AsciiViews AsciiView displays bodyMessage if no rows 1`] = ` exports[`AsciiViews AsciiView does not display bodyMessage if rows 1`] = `
           ╒═══╕
     │x  │
    diff --git a/src/browser/modules/Stream/CypherFrame/__snapshots__/CodeView.test.tsx.snap b/src/browser/modules/Stream/CypherFrame/__snapshots__/CodeView.test.tsx.snap
    index b0e94bbd504..ae872021f9f 100644
    --- a/src/browser/modules/Stream/CypherFrame/__snapshots__/CodeView.test.tsx.snap
    +++ b/src/browser/modules/Stream/CypherFrame/__snapshots__/CodeView.test.tsx.snap
    @@ -14,7 +14,7 @@ exports[`CodeViews CodeStatusbar displays statusBarMessage 1`] = `
         class="sc-jcFjpl cdUBmZ"
       >
         
    Started streaming 1 records after 5 ms and completed after 10 ms. diff --git a/src/browser/modules/Stream/__snapshots__/SchemaFrame.test.tsx.snap b/src/browser/modules/Stream/__snapshots__/SchemaFrame.test.tsx.snap index 96c747d53b3..0fb029bfe80 100644 --- a/src/browser/modules/Stream/__snapshots__/SchemaFrame.test.tsx.snap +++ b/src/browser/modules/Stream/__snapshots__/SchemaFrame.test.tsx.snap @@ -7,16 +7,16 @@ exports[`SchemaFrame renders empty 1`] = ` >
    @@ -24,10 +24,10 @@ exports[`SchemaFrame renders empty 1`] = ` @@ -35,33 +35,33 @@ exports[`SchemaFrame renders empty 1`] = `
    Indexes
    None
    @@ -69,24 +69,24 @@ exports[`SchemaFrame renders empty 1`] = ` @@ -121,46 +121,46 @@ exports[`SchemaFrame renders empty for Neo4j >= 4.0 1`] = ` >
    Constraint Name Type EntityType LabelsOrTypes Properties
    None
    @@ -168,62 +168,62 @@ exports[`SchemaFrame renders empty for Neo4j >= 4.0 1`] = `
    Index Name Type Uniqueness EntityType LabelsOrTypes Properties State
    None
    @@ -231,24 +231,24 @@ exports[`SchemaFrame renders empty for Neo4j >= 4.0 1`] = ` @@ -283,16 +283,16 @@ exports[`SchemaFrame renders results for Neo4j < 4.0 1`] = ` >
    Constraint Name Type EntityType LabelsOrTypes Properties
    None
    @@ -300,10 +300,10 @@ exports[`SchemaFrame renders results for Neo4j < 4.0 1`] = ` @@ -311,13 +311,13 @@ exports[`SchemaFrame renders results for Neo4j < 4.0 1`] = `
    Indexes
    ON :Movie(released) ONLINE
    @@ -325,10 +325,10 @@ exports[`SchemaFrame renders results for Neo4j < 4.0 1`] = ` @@ -365,46 +365,46 @@ exports[`SchemaFrame renders results for Neo4j >= 4.2 1`] = ` >
    Constraints
    ON ( book:Book ) ASSERT book.isbn IS UNIQUE
    @@ -412,34 +412,34 @@ exports[`SchemaFrame renders results for Neo4j >= 4.2 1`] = ` @@ -447,33 +447,33 @@ exports[`SchemaFrame renders results for Neo4j >= 4.2 1`] = `
    Index Name Type Uniqueness EntityType LabelsOrTypes Properties State
    node_label_property [ "released" ] ONLINE
    @@ -481,32 +481,32 @@ exports[`SchemaFrame renders results for Neo4j >= 4.2 1`] = `
    Constraint Name Type EntityType LabelsOrTypes Properties
    constraint_550b2518 UNIQUE node [ "Movie" ] [ "released" diff --git a/src/neo4j-arc/common/components/PropertiesTable/PropertiesTable.tsx b/src/neo4j-arc/common/components/PropertiesTable/PropertiesTable.tsx index 7a2455e3a66..0dba941a653 100644 --- a/src/neo4j-arc/common/components/PropertiesTable/PropertiesTable.tsx +++ b/src/neo4j-arc/common/components/PropertiesTable/PropertiesTable.tsx @@ -31,17 +31,31 @@ import { import { ClipboardCopier } from '../ClipboardCopier' import { ShowMoreOrAll } from '../ShowMoreOrAll/ShowMoreOrAll' import { VizItemProperty } from 'neo4j-arc/common' +import { + GraphInteractionCallBack, + NODE_PROP_UPDATE +} from '../../../graph-visualization' export const ELLIPSIS = '\u2026' export const WIDE_VIEW_THRESHOLD = 900 export const MAX_LENGTH_NARROW = 150 export const MAX_LENGTH_WIDE = 300 type ExpandableValueProps = { + nodeId: string + propKey: string value: string width: number type: string + onGraphInteraction: GraphInteractionCallBack } -function ExpandableValue({ value, width, type }: ExpandableValueProps) { +function ExpandableValue({ + nodeId, + propKey, + value, + width, + type, + onGraphInteraction +}: ExpandableValueProps) { const [expanded, setExpanded] = useState(false) const maxLength = @@ -56,7 +70,17 @@ function ExpandableValue({ value, width, type }: ExpandableValueProps) { valueShown += valueIsTrimmed ? ELLIPSIS : '' return ( - <> +
    + onGraphInteraction(NODE_PROP_UPDATE, { + nodeId: nodeId, + propKey: propKey, + propVal: e.currentTarget.textContent + }) + } + > {type.startsWith('Array') && '['} {valueIsTrimmed && ( @@ -65,7 +89,7 @@ function ExpandableValue({ value, width, type }: ExpandableValueProps) { )} {type.startsWith('Array') && ']'} - +
    ) } @@ -75,14 +99,23 @@ type PropertiesViewProps = { totalNumItems: number moreStep: number nodeInspectorWidth: number + onGraphInteraction?: GraphInteractionCallBack } export const PropertiesTable = ({ visibleProperties, totalNumItems, onMoreClick, moreStep, - nodeInspectorWidth + nodeInspectorWidth, + onGraphInteraction }: PropertiesViewProps): JSX.Element => { + let id = '' + for (let i = 0; i < visibleProperties.length; i++) { + if (visibleProperties[i].key == '') { + id = visibleProperties[i].value + } + } + return ( <> @@ -95,9 +128,12 @@ export const PropertiesTable = ({ undefined)} /> diff --git a/src/neo4j-arc/graph-visualization/GraphVisualizer/DefaultPanelContent/DefaultDetailsPane.tsx b/src/neo4j-arc/graph-visualization/GraphVisualizer/DefaultPanelContent/DefaultDetailsPane.tsx index 0c8363018f5..02d10efb76b 100644 --- a/src/neo4j-arc/graph-visualization/GraphVisualizer/DefaultPanelContent/DefaultDetailsPane.tsx +++ b/src/neo4j-arc/graph-visualization/GraphVisualizer/DefaultPanelContent/DefaultDetailsPane.tsx @@ -26,17 +26,20 @@ import { PaneBody, PaneHeader, PaneTitle, PaneWrapper } from './styled' import { NodeLabel } from './NodeLabel' import { RelType } from './RelType' import { GraphStyleModel } from '../../models/GraphStyle' +import { GraphInteractionCallBack } from '../Graph/GraphEventHandlerModel' export const DETAILS_PANE_STEP_SIZE = 1000 export type DetailsPaneProps = { vizItem: NodeItem | RelationshipItem graphStyle: GraphStyleModel nodeInspectorWidth: number + onGraphInteraction?: GraphInteractionCallBack } export function DefaultDetailsPane({ vizItem, graphStyle, - nodeInspectorWidth + nodeInspectorWidth, + onGraphInteraction }: DetailsPaneProps): JSX.Element { const [maxPropertiesCount, setMaxPropertiesCount] = useState( DETAILS_PANE_STEP_SIZE @@ -76,6 +79,9 @@ export function DefaultDetailsPane({ relType: vizItem.item.type }} graphStyle={graphStyle} + onGraphInteraction={onGraphInteraction} + sourceNodeId={vizItem.item.source.id} + targetNodeId={vizItem.item.target.id} /> )} {vizItem.type === 'node' && @@ -99,6 +105,7 @@ export function DefaultDetailsPane({ moreStep={DETAILS_PANE_STEP_SIZE} totalNumItems={allItemProperties.length} nodeInspectorWidth={nodeInspectorWidth} + onGraphInteraction={onGraphInteraction} /> diff --git a/src/neo4j-arc/graph-visualization/GraphVisualizer/DefaultPanelContent/RelType.tsx b/src/neo4j-arc/graph-visualization/GraphVisualizer/DefaultPanelContent/RelType.tsx index 8b4cbd62969..662d4c6cac3 100644 --- a/src/neo4j-arc/graph-visualization/GraphVisualizer/DefaultPanelContent/RelType.tsx +++ b/src/neo4j-arc/graph-visualization/GraphVisualizer/DefaultPanelContent/RelType.tsx @@ -21,28 +21,54 @@ import React from 'react' import { GraphStyleModel } from '../../models/GraphStyle' import { NonClickableRelTypeChip } from './styled' +import { + GraphInteractionCallBack, + REL_TYPE_UPDATE +} from '../Graph/GraphEventHandlerModel' export type RelTypeProps = { graphStyle: GraphStyleModel selectedRelType: { relType: string; propertyKeys: string[]; count?: number } + onGraphInteraction?: GraphInteractionCallBack + sourceNodeId?: string + targetNodeId?: string +} +RelType.defaultProps = { + onGraphInteraction: () => undefined } export function RelType({ selectedRelType, - graphStyle + graphStyle, + onGraphInteraction = () => undefined, + sourceNodeId, + targetNodeId }: RelTypeProps): JSX.Element { const styleForRelType = graphStyle.forRelationship({ type: selectedRelType.relType }) return ( - + onGraphInteraction(REL_TYPE_UPDATE, { + sourceNodeId: sourceNodeId, + targetNodeId: targetNodeId, + oldType: selectedRelType.relType, + newType: e.currentTarget.textContent + }) + } > - {selectedRelType.count !== undefined - ? `${selectedRelType.relType} (${selectedRelType.count})` - : `${selectedRelType.relType}`} - + + {selectedRelType.count !== undefined + ? `${selectedRelType.relType} (${selectedRelType.count})` + : `${selectedRelType.relType}`} + + ) } diff --git a/src/neo4j-arc/graph-visualization/GraphVisualizer/Graph/GraphEventHandlerModel.ts b/src/neo4j-arc/graph-visualization/GraphVisualizer/Graph/GraphEventHandlerModel.ts index aa15f6afc54..3ba4a6f935b 100644 --- a/src/neo4j-arc/graph-visualization/GraphVisualizer/Graph/GraphEventHandlerModel.ts +++ b/src/neo4j-arc/graph-visualization/GraphVisualizer/Graph/GraphEventHandlerModel.ts @@ -31,6 +31,10 @@ import { import { Visualization } from './visualization/Visualization' export const NODE_ON_CANVAS_CREATE = 'NODE_ON_CANVAS_CREATE' +export const NODE_PROP_UPDATE = 'NODE_PROP_UPDATE' +export const NODE_LABEL_UPDATE = 'NODE_LABEL_UPDATE' +export const REL_ON_CANVAS_CREATE = 'REL_ON_CANVAS_CREATE' +export const REL_TYPE_UPDATE = 'REL_TYPE_UPDATE' export type GraphInteraction = | 'NODE_EXPAND' @@ -38,6 +42,10 @@ export type GraphInteraction = | 'NODE_DISMISSED' | 'NODE_ON_CANVAS_CREATE' | typeof NODE_ON_CANVAS_CREATE + | typeof NODE_PROP_UPDATE + | typeof NODE_LABEL_UPDATE + | typeof REL_ON_CANVAS_CREATE + | typeof REL_TYPE_UPDATE export type GraphInteractionCallBack = ( event: GraphInteraction, @@ -54,6 +62,9 @@ export class GraphEventHandlerModel { onGraphInteraction: GraphInteractionCallBack selectedItem: NodeModel | RelationshipModel | null + private altCreatedRelSourceNode: any + private altCreatedRelTargetNode: any + constructor( graph: GraphModel, visualization: Visualization, @@ -72,6 +83,9 @@ export class GraphEventHandlerModel { this.onGraphInteraction = onGraphInteraction ?? (() => undefined) this.onGraphModelChange = onGraphModelChange + + this.altCreatedRelSourceNode = null + this.altCreatedRelTargetNode = null } graphModelChanged(): void { @@ -239,8 +253,14 @@ export class GraphEventHandlerModel { new NodeModel( newId.toString(), ['Undefined'], - { name: 'New Node' }, - { name: 'string' } + { + name: 'New Node', + description: 'New Node' + }, + { + name: 'string', + description: 'string' + } ) ]) this.visualization.update({ updateNodes: true, updateRelationships: true }) @@ -249,6 +269,7 @@ export class GraphEventHandlerModel { this.onGraphInteraction(NODE_ON_CANVAS_CREATE, { id: newId, name: 'New Node', + description: 'New Node', labels: ['Undefined'] }) } @@ -263,6 +284,56 @@ export class GraphEventHandlerModel { }) } + nodeAltDown(node: NodeModel): void { + if (!node) { + return + } + + if ( + this.altCreatedRelSourceNode == null && + this.altCreatedRelTargetNode == null + ) { + this.altCreatedRelSourceNode = node + } else if ( + this.altCreatedRelSourceNode != null && + this.altCreatedRelTargetNode == null + ) { + this.altCreatedRelTargetNode = node + + const maxId: number = Math.max( + ...this.graph + .relationships() + .map(relationship => parseInt(relationship.id)) + ) + const newId = maxId + 1 + + const altCreatedRel: RelationshipModel = new RelationshipModel( + newId.toString(), + this.altCreatedRelSourceNode, + this.altCreatedRelTargetNode, + newId.toString(), + { name: 'new link' }, + { name: 'string' } + ) + + this.graph.addRelationships([altCreatedRel]) + this.visualization.update({ + updateNodes: true, + updateRelationships: true + }) + this.graphModelChanged() + + this.onGraphInteraction(REL_ON_CANVAS_CREATE, { + type: newId, + sourceNodeId: this.altCreatedRelSourceNode.id, + targetNodeId: this.altCreatedRelTargetNode.id + }) + + this.altCreatedRelSourceNode = null + this.altCreatedRelTargetNode = null + } + } + bindEventHandlers(): void { this.visualization .on('nodeMouseOver', this.onNodeMouseOver.bind(this)) @@ -278,6 +349,7 @@ export class GraphEventHandlerModel { .on('nodeClicked', this.nodeClicked.bind(this)) .on('nodeDblClicked', this.nodeDblClicked.bind(this)) .on('nodeUnlock', this.nodeUnlock.bind(this)) + .on('nodeAltDown', this.nodeAltDown.bind(this)) this.onItemMouseOut() } } diff --git a/src/neo4j-arc/graph-visualization/GraphVisualizer/Graph/visualization/mouseEventHandlers.ts b/src/neo4j-arc/graph-visualization/GraphVisualizer/Graph/visualization/mouseEventHandlers.ts index a65eb8e8f30..ffb28ae3689 100644 --- a/src/neo4j-arc/graph-visualization/GraphVisualizer/Graph/visualization/mouseEventHandlers.ts +++ b/src/neo4j-arc/graph-visualization/GraphVisualizer/Graph/visualization/mouseEventHandlers.ts @@ -66,6 +66,12 @@ export const nodeEventHandlers = ( trigger('nodeMouseOut', node) } + const onNodeMouseDown = (_event: KeyboardEvent, node: NodeModel) => { + if (_event.altKey || _event.ctrlKey || _event.metaKey || _event.shiftKey) { + trigger('nodeAltDown', node) + } + } + const dragstarted = (event: D3DragEvent) => { initialDragPosition = [event.x, event.y] restartedSimulation = false @@ -115,6 +121,7 @@ export const nodeEventHandlers = ( .on('mouseout', onNodeMouseOut) .on('click', onNodeClick) .on('dblclick', onNodeDblClick) + .on('mousedown', onNodeMouseDown) } export const relationshipEventHandlers = ( diff --git a/src/neo4j-arc/graph-visualization/GraphVisualizer/GraphVisualizer.tsx b/src/neo4j-arc/graph-visualization/GraphVisualizer/GraphVisualizer.tsx index b67b1360344..87bc6c09ad6 100644 --- a/src/neo4j-arc/graph-visualization/GraphVisualizer/GraphVisualizer.tsx +++ b/src/neo4j-arc/graph-visualization/GraphVisualizer/GraphVisualizer.tsx @@ -297,6 +297,7 @@ export class GraphVisualizer extends Component< }} DetailsPaneOverride={this.props.DetailsPaneOverride} OverviewPaneOverride={this.props.OverviewPaneOverride} + onGraphInteraction={this.props.onGraphInteraction} /> ) diff --git a/src/neo4j-arc/graph-visualization/GraphVisualizer/NodeInspectorPanel.tsx b/src/neo4j-arc/graph-visualization/GraphVisualizer/NodeInspectorPanel.tsx index 734d813b09e..12d9572c1e7 100644 --- a/src/neo4j-arc/graph-visualization/GraphVisualizer/NodeInspectorPanel.tsx +++ b/src/neo4j-arc/graph-visualization/GraphVisualizer/NodeInspectorPanel.tsx @@ -38,6 +38,7 @@ import { Resizable } from 're-resizable' import { GraphStats } from '../utils/mapper' import { GraphStyleModel } from '../models/GraphStyle' import { VizItem } from '../types' +import { GraphInteractionCallBack } from './Graph/GraphEventHandlerModel' interface NodeInspectorPanelProps { expanded: boolean @@ -51,6 +52,7 @@ interface NodeInspectorPanelProps { width: number DetailsPaneOverride?: React.FC OverviewPaneOverride?: React.FC + onGraphInteraction?: GraphInteractionCallBack } export const defaultPanelWidth = (): number => @@ -68,7 +70,8 @@ export class NodeInspectorPanel extends Component { toggleExpanded, width, DetailsPaneOverride, - OverviewPaneOverride + OverviewPaneOverride, + onGraphInteraction } = this.props const relevantItems = ['node', 'relationship'] const hoveringNodeOrRelationship = @@ -120,6 +123,7 @@ export class NodeInspectorPanel extends Component { vizItem={shownEl} graphStyle={graphStyle} nodeInspectorWidth={width} + onGraphInteraction={onGraphInteraction} /> ) : ( + item: Pick< + RelationshipModel, + 'id' | 'type' | 'propertyList' | 'source' | 'target' + > } type CanvasItem = { diff --git a/src/neo4j-arc/package.json b/src/neo4j-arc/package.json index 205b8232367..012832938da 100644 --- a/src/neo4j-arc/package.json +++ b/src/neo4j-arc/package.json @@ -6,7 +6,7 @@ "license": "GPL-3.0", "typings": "dist/neo4j-arc.d.ts", "scripts": { - "build": "rollup -c --failAfterWarnings", + "build": "rollup -c", "test": "tsc --noEmit", "prepublishOnly": "npm run test && npm run build" },