diff --git a/docs/modules/ROOT/content-nav.adoc b/docs/modules/ROOT/content-nav.adoc
index 2d3fa12808e..2552c8fc64c 100644
--- a/docs/modules/ROOT/content-nav.adoc
+++ b/docs/modules/ROOT/content-nav.adoc
@@ -30,3 +30,4 @@
* xref:internals/index.adoc[Internals]
** xref:internals/graph-modelling.adoc[]
+** xref:internals/graph-interactions.adoc[]
diff --git a/docs/modules/ROOT/images/neo4j-browser.drawio b/docs/modules/ROOT/images/neo4j-browser.drawio
new file mode 100644
index 00000000000..012567d14b3
--- /dev/null
+++ b/docs/modules/ROOT/images/neo4j-browser.drawio
@@ -0,0 +1,253 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/modules/ROOT/images/neo4j-browser.png b/docs/modules/ROOT/images/neo4j-browser.png
index f8d4927af76..535f3d6726c 100644
Binary files a/docs/modules/ROOT/images/neo4j-browser.png and b/docs/modules/ROOT/images/neo4j-browser.png differ
diff --git a/docs/modules/ROOT/pages/internals/graph-interactions.adoc b/docs/modules/ROOT/pages/internals/graph-interactions.adoc
new file mode 100644
index 00000000000..3ca5508ed29
--- /dev/null
+++ b/docs/modules/ROOT/pages/internals/graph-interactions.adoc
@@ -0,0 +1,130 @@
+:description: How graph interations take effect on displayed graph and backing database
+
+
+[[user-interactions]]
+= How Graph Interations Take Effect on Displayed Graph and Backing Database
+
+We define *graph interactions* as any https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent[mouse] and
+https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent[keyboard] events on the
+link:../../visual-tour/index.html#frame-views[_Graph_ frame view]
+
+There are basically 3 components involved in handling users' interactions:
+
+1. *GraphEventHandlerModel* contains implementations on how to operate on the displayed graph for various interactions
+2. *Visualization* triggers the implementations via D3 callbacks
+3. *VisualizationView* is involved if a certain type of user interaction involves database changes
+
+[NOTE]
+====
+
+In the case 3. above, _VisualizationView_
+https://react.dev/learn/passing-data-deeply-with-context#the-problem-with-passing-props[prop-drilling] a callback
+called **GraphInteractionCallBack** all they way to the _GraphEventHandlerModel_. Since database operations should not
+be defined in neo4j-arc module, _GraphEventHandlerModel_ can simply pass any argument to the callback function and let
+the upper level component (recall the component diagram in link:../index.html[overview section]) _VisualizationView_ handle
+the database connection, query issuing, and response handling, etc.
+
+====
+
+== How to Implement a User Interaction
+
+1. Implement an event handler function in GraphEventHandlerModel
+2. Bind the handler function in `GraphEventHandlerModel.bindEventHandlers()`
+3. Trigger the handler function in `Visualization`
+4. If the interaction involves database changes, add the corresponding logic to `GraphInteractionCallBack`, and
+5. trigger the `GraphInteractionCallBack` in the handler function
+
+For example, let's say we'd like to support easy on-graph editing by allowing us to create a new node when we double
+click on canvas. We will follow the steps above by first defining the event handler function:
+
+[source,typescript]
+----
+ onCanvasDblClicked(): void {
+ this.graph.addNodes([ new NodeModel(...) ])
+ this.visualization.update({ updateNodes: true, updateRelationships: true })
+ this.graphModelChanged()
+
+ this.onGraphInteraction(NODE_ON_CANVAS_CREATE, { id: newNodeId, name: 'New Node', labels: ['Undefined'] })
+ }
+----
+
+[NOTE]
+====
+
+When we add a new node to the graph, it is important to update the visual of the graph using `visualization.update()`
+and the stats panel with `graphModelChanged()`
+
+We would also like to persist the new node to database so that refreshing the page or re-login shall still render this
+node. Therefore we invoke `onGraphInteraction()` above. The details of this method will be disucssed below
+
+====
+
+Next, we bind the function so that `Visualization.ts` component can delegte any canvas double click callbacks to this
+function
+
+[source,typescript]
+----
+bindEventHandlers(): void {
+ this.visualization
+ ...
+ .on('canvasDblClicked', this.onCanvasDblClicked.bind(this))
+ ...
+ this.onItemMouseOut()
+}
+----
+
+When `Visualization.ts` recrives a canvas double click from user, it invoke the event handler function via
+https://www.d3indepth.com/selections/#event-handling[D3 event handlers]:
+
+[source,typescript]
+----
+this.rect = this.baseGroup
+ .append('rect')
+ ...
+ .on('dblclick', () => {
+ if (!this.draw) {
+ return this.trigger('canvasDblClicked')
+ }
+ })
+----
+
+This is how we trigger the handler function in `Visualization`.
+
+As we've mentioned earlier, we would like to persist this new node to database. They way to do that is by modifying
+*VisualizationView.onGraphInteraction()* method:
+
+[source,typescript]
+----
+ onGraphInteraction: GraphInteractionCallBack = (event, properties) => {
+ if (event == NODE_ON_CANVAS_CREATE) {
+ if (properties == null) {
+ throw new Error(
+ 'A property map with id, name, and labels keys are required'
+ )
+ }
+
+ const id = properties['id']
+ const name = properties['name']
+ const variableName = `node${id}`
+ const labels = (properties['labels'] as string[]).map(label => `\`${label}\``).join(':')
+
+ const query = `CREATE (${variableName}:${labels} { id: ${id}, name: "${name}" });`
+
+ this.props.bus.self(
+ CYPHER_REQUEST,
+ {
+ query,
+ params: { labels, id, name },
+ queryType: NEO4J_BROWSER_USER_ACTION_QUERY
+ },
+ (response: any) => {
+ if (!response.success) {
+ throw new Error(response.error)
+ }
+ }
+ )
+ }
+ }
+----
+
+The complete implementation is in https://github.com/QubitPi/neo4j-browser/pull/7[this PR] as a reference
diff --git a/docs/modules/ROOT/pages/internals/index.adoc b/docs/modules/ROOT/pages/internals/index.adoc
index 034d5a6b4af..f81fdccf10c 100644
--- a/docs/modules/ROOT/pages/internals/index.adoc
+++ b/docs/modules/ROOT/pages/internals/index.adoc
@@ -10,19 +10,23 @@ The Neo4J Browser is logically composed of 2 parts:
2. A user-interface that combines the graph rendering (supported by the graphing module), database, and user
interaction together
-The graphing is based on D3 but pretty much developed from scratch by building everything on top of D3 and implementing
-its own layout, coloring, and link drawing. For example, the calculation of arrow (e.g. node links) between nodes
-uses some very complicated math along with very prelimiary MDN standard basic shap specification.
+The graphing is based on D3 and implements its own layout, coloring, and link drawing. For example, the calculation of
+arrow, i.e. links, between nodes uses some
+https://github.com/QubitPi/neo4j-browser/blob/master/src/neo4j-arc/graph-visualization/utils/ArcArrow.ts[very complicated math]
+along with very prelimiary MDN standard basic shap specification.
[WARNING]
====
-We will not have any TypeDoc documentation, because the TypeScript version used in Neo4J Browser is not supprted by it
+We will not have any TypeDoc documentation, because the TypeScript version used in Neo4J Browser is not supprted by the
+TypeDoc
====
== Component Diagram (WIP)
-image:neo4j-browser.png[width=300]
+image:neo4j-browser.png[width=900]
+* The orange triangle labled with "On Canvas Interaction" is discussed in detail in
+ link:internals/graph-interactions[graph interactions] section
* Sentry.io is initialized in the top index.tsx
* AppInit.tsx is responsible for several initializations:
+
diff --git a/e2e_tests/integration/viz.spec.ts b/e2e_tests/integration/viz.spec.ts
index 66fe6aca2c4..c063dadc1b7 100644
--- a/e2e_tests/integration/viz.spec.ts
+++ b/e2e_tests/integration/viz.spec.ts
@@ -241,4 +241,22 @@ describe('Viz rendering', () => {
cy.get('#svg-vis').trigger('wheel', { deltaY: 3000, shiftKey: true })
cy.get(`[aria-label="zoom-out"]`).should('be.disabled')
})
+
+ it('can create a new node by double clicking the canvas', () => {
+ cy.executeCommand(':clear')
+ cy.executeCommand(`CREATE (a:TestLabel {name: 'testNode'}) RETURN a`, {
+ parseSpecialCharSequences: false
+ })
+
+ cy.get('[data-testid="graphCanvas"]')
+ .trigger('click', 200, 200, { force: true })
+ .trigger('dblclick', 200, 200, { force: true })
+
+ cy.get('[data-testid="nodeGroups"]', { timeout: 5000 }).contains('New Node')
+ cy.get('[data-testid="vizInspector"]', { timeout: 5000 }).contains(
+ 'Undefined'
+ )
+
+ cy.executeCommand('MATCH (n) DETACH DELETE n')
+ })
})
diff --git a/src/browser/modules/Stream/CypherFrame/VisualizationView/VisualizationView.tsx b/src/browser/modules/Stream/CypherFrame/VisualizationView/VisualizationView.tsx
index 877147680ab..e3381dadb3d 100644
--- a/src/browser/modules/Stream/CypherFrame/VisualizationView/VisualizationView.tsx
+++ b/src/browser/modules/Stream/CypherFrame/VisualizationView/VisualizationView.tsx
@@ -24,7 +24,12 @@ import { withBus } from 'react-suber'
import { Action, Dispatch } from 'redux'
import { Bus } from 'suber'
-import { GraphModel, GraphVisualizer } from 'neo4j-arc/graph-visualization'
+import {
+ GraphInteractionCallBack,
+ GraphModel,
+ GraphVisualizer,
+ NODE_ON_CANVAS_CREATE
+} from 'neo4j-arc/graph-visualization'
import { StyledVisContainer } from './VisualizationView.styled'
import { resultHasTruncatedFields } from 'browser/modules/Stream/CypherFrame/helpers'
@@ -271,6 +276,39 @@ LIMIT ${maxNewNeighbours}`
this.autoCompleteRelationships([], this.graph.nodes(), true)
}
+ onGraphInteraction: GraphInteractionCallBack = (event, properties) => {
+ if (event == NODE_ON_CANVAS_CREATE) {
+ if (properties == null) {
+ throw new Error(
+ 'A property map with id, name, and labels keys are required'
+ )
+ }
+
+ const id = properties['id']
+ const name = properties['name']
+ const variableName = `node${id}`
+ const labels = (properties['labels'] as string[])
+ .map(label => `\`${label}\``)
+ .join(':')
+
+ const query = `CREATE (${variableName}:${labels} { id: ${id}, name: "${name}" });`
+
+ this.props.bus.self(
+ CYPHER_REQUEST,
+ {
+ query,
+ params: { labels, id, name },
+ queryType: NEO4J_BROWSER_USER_ACTION_QUERY
+ },
+ (response: any) => {
+ if (!response.success) {
+ throw new Error(response.error)
+ }
+ }
+ )
+ }
+ }
+
render(): React.ReactNode {
if (!this.state.nodes.length) return null
@@ -309,6 +347,7 @@ LIMIT ${maxNewNeighbours}`
OverviewPaneOverride={OverviewPane}
useGeneratedDefaultColors={false}
initialZoomToFit
+ onGraphInteraction={this.onGraphInteraction}
/>
)
diff --git a/src/neo4j-arc/graph-visualization/GraphVisualizer/Graph/GraphEventHandlerModel.ts b/src/neo4j-arc/graph-visualization/GraphVisualizer/Graph/GraphEventHandlerModel.ts
index 04f65c1f502..aa15f6afc54 100644
--- a/src/neo4j-arc/graph-visualization/GraphVisualizer/Graph/GraphEventHandlerModel.ts
+++ b/src/neo4j-arc/graph-visualization/GraphVisualizer/Graph/GraphEventHandlerModel.ts
@@ -30,10 +30,14 @@ import {
} from '../../utils/mapper'
import { Visualization } from './visualization/Visualization'
+export const NODE_ON_CANVAS_CREATE = 'NODE_ON_CANVAS_CREATE'
+
export type GraphInteraction =
| 'NODE_EXPAND'
| 'NODE_UNPINNED'
| 'NODE_DISMISSED'
+ | 'NODE_ON_CANVAS_CREATE'
+ | typeof NODE_ON_CANVAS_CREATE
export type GraphInteractionCallBack = (
event: GraphInteraction,
@@ -225,6 +229,30 @@ export class GraphEventHandlerModel {
this.deselectItem()
}
+ onCanvasDblClicked(): void {
+ const maxId: number = Math.max(
+ ...this.graph.nodes().map(node => parseInt(node.id))
+ )
+ const newId = maxId + 1
+
+ this.graph.addNodes([
+ new NodeModel(
+ newId.toString(),
+ ['Undefined'],
+ { name: 'New Node' },
+ { name: 'string' }
+ )
+ ])
+ this.visualization.update({ updateNodes: true, updateRelationships: true })
+ this.graphModelChanged()
+
+ this.onGraphInteraction(NODE_ON_CANVAS_CREATE, {
+ id: newId,
+ name: 'New Node',
+ labels: ['Undefined']
+ })
+ }
+
onItemMouseOut(): void {
this.onItemMouseOver({
type: 'canvas',
@@ -245,6 +273,7 @@ export class GraphEventHandlerModel {
.on('relMouseOut', this.onItemMouseOut.bind(this))
.on('relationshipClicked', this.onRelationshipClicked.bind(this))
.on('canvasClicked', this.onCanvasClicked.bind(this))
+ .on('canvasDblClicked', this.onCanvasDblClicked.bind(this))
.on('nodeClose', this.nodeClose.bind(this))
.on('nodeClicked', this.nodeClicked.bind(this))
.on('nodeDblClicked', this.nodeDblClicked.bind(this))
diff --git a/src/neo4j-arc/graph-visualization/GraphVisualizer/Graph/visualization/Visualization.ts b/src/neo4j-arc/graph-visualization/GraphVisualizer/Graph/visualization/Visualization.ts
index d536715ce7b..8fdfee786fa 100644
--- a/src/neo4j-arc/graph-visualization/GraphVisualizer/Graph/visualization/Visualization.ts
+++ b/src/neo4j-arc/graph-visualization/GraphVisualizer/Graph/visualization/Visualization.ts
@@ -101,6 +101,7 @@ export class Visualization {
.attr('width', '100%')
.attr('height', '100%')
.attr('transform', 'scale(1)')
+ .attr('data-testid', 'graphCanvas')
// Background click event
// Check if panning is ongoing
.on('click', () => {
@@ -108,6 +109,11 @@ export class Visualization {
return this.trigger('canvasClicked')
}
})
+ .on('dblclick', () => {
+ if (!this.draw) {
+ return this.trigger('canvasDblClicked')
+ }
+ })
this.container = this.baseGroup.append('g')
this.geometry = new GraphGeometryModel(style)
@@ -166,6 +172,7 @@ export class Visualization {
const nodeGroups = this.container
.selectAll('g.node')
.attr('transform', d => `translate(${d.x},${d.y})`)
+ .attr('data-testid', 'nodeGroups')
nodeRenderer.forEach(renderer => nodeGroups.call(renderer.onTick, this))
diff --git a/src/neo4j-arc/graph-visualization/index.ts b/src/neo4j-arc/graph-visualization/index.ts
index 3d85b722eb6..ed0130a0f71 100644
--- a/src/neo4j-arc/graph-visualization/index.ts
+++ b/src/neo4j-arc/graph-visualization/index.ts
@@ -33,3 +33,6 @@ export { measureText } from './utils/textMeasurement'
export { GraphVisualizer } from './GraphVisualizer/GraphVisualizer'
export type { DetailsPaneProps } from './GraphVisualizer/DefaultPanelContent/DefaultDetailsPane'
export type { OverviewPaneProps } from './GraphVisualizer/DefaultPanelContent/DefaultOverviewPane'
+
+export { NODE_ON_CANVAS_CREATE } from './GraphVisualizer/Graph/GraphEventHandlerModel'
+export type { GraphInteractionCallBack } from './GraphVisualizer/Graph/GraphEventHandlerModel'