How I Built… the APOC User Guide Graph App

Adam Cowley
May 5, 2020 · 5 min read
The new APOC User Guide Graph App

At the end of last week, I tweeted about a soft-release of the new APOC User Guide Graph App. This has been one of the many Graph App based experiments that I have been working on over the past couple of months so it’s nice to have a version out in the wild.

I thought I’d take some time to write up how I built the Graph App and give you an overview of some of the features.

The idea behind the Graph App was to take the existing documentation and add some extra pieces of functionality to make it more interactive. At Neo4j, the majority of our docs and user guides are written with Asciidoc. As part of the build process, this builds XML, HTML and PDF files.

The original plan was to somehow take the XML files, load them in and wrap them in Vue.js components but this proved to be problematic. In the end, the easier route was to load the HTML files using and extract the relevant DOM elements using JavaScript, manipulate the links and then add them to the <body> of the Graph App.

Table of Contents

The navigation on the left hand side loads in the Table of Contents using the fetch API and adds the HTML into its own “virtual” DOM — basically a HTML element.

const url = `${this.root}/toc.html`fetch(url)
.then(res => res.text())
.then(html => {
this.toc = document.createElement('body')
this.toc.innerHTML = html
})
.finally(() => this.loading = false)

Once that was loaded, I could use querySelectorAll to find all of the links and load pass them into an array for the template to pick up.

computed: {
// ...
chapters() {
if ( !this.toc ) return []
return Array.from(this.toc.querySelectorAll('.chapter a'))
.map(a => ({
class: a.parentElement.className,
to: { path: appendIndex(a), },
text: a.innerHTML,
children: Array.from(a.parentElement.parentElement.querySelectorAll('.section a')).map(a => ({
class: a.parentElement.className,
to: { path: appendIndex(a), },
text: a.innerHTML,
})),
}))
},
}

The nice thing about computed properties in Vue.js is that it makes it really easy to manipulate variables — this made the search a 5 minute job. After creating an input element bound to a v-model, I could take the value from that and use it to filter down the chapters property based on whether the term was mentioned in the chapter link or it’s child sections.

chapterResults() {
if ( this.search === '' ) return this.chapters
return this.chapters.filter(chapter =>
chapter.text.toLowerCase().includes(this.search.toLowerCase())
|| chapter.children.find(child => child.text.toLowerCase().includes(this.search.toLowerCase()))
)
.map(chapter => ({
...chapter,
children: chapter.children.filter(section => section.text.toLowerCase().includes(this.search.toLowerCase()))
}))
},

Hacking the Content

The content was a little more tricky, but I took the same approach of loading the contents of a file using fetch and manipulating the content after it was loaded into a virtual element using document.createElement.

When a link on the sidebar is clicked, the path of the file is added to the hash of the page, and a Document component picks this up via Vue Router. From there, there is a bit of work to do to correct the links (for example relative links and adding a target="_blank" attribute to any external links), then adding a listener to prevent the default action and pass a navigation event to Vue Router.

Running Cypher in Neo4j Browser with a single click

For any cypher code blocks, you can click the Run in Browser button to copy the code to Neo4j Browser. This takes advantage of the Deep Link functionality available in Neo4j Desktop.

Handy buttons to copy the code snippet to the clipboard or run the cypher query in Neo4j Browser

Under the hood, this is pretty simple. The button simply takes the content from the code block, then sets window.location.href to a URL similar to the following to instruct Neo4j Desktop to open up the Neo4j Browser graph app with the arg portion (in this case, a URL encoded version of a Cypher statement) pre-populated into the browser bar.

neo4j-desktop://graphapps/neo4j-browser?cmd=edit&arg=MATCH%20%28n%29%20RETURN%20count%28n%29%20AS%20count

Some of the code blocks are also quite complex — for that reason it made sense to add a Copy to Clipboard button to each code element in case the user needs to copy the snippet into a project or share it with someone else.

const copyToClipboard = code => {
const textarea = document.createElement('textarea')
textarea.value = cleanCode(code)
textarea.setAttribute('readonly', '');
textarea.style.position = 'absolute';
textarea.style.left = '-9999px';
document.body.appendChild(textarea)
textarea.select()
document.execCommand('copy')
document.body.removeChild(textarea)
}

Publishing to npm

The Graph App has now been published to npm. The major benefit to this is that Neo4j Desktop will now keep this up to date. If there is a change to the documentation or we decide to add some functionality, all we need to do is run npm publish to upload it to npm.

Neo4j Desktop will then regularly check for updates and install them as long as the app is valid for the current version of the Neo4j Desktop API.

To install the Graph App now, simply paste the following URL into the Install form at the bottom of the Graph Apps pane in Neo4j Desktop:

https://registry.npmjs.org/@graphapps/apoc

It’s a simple app but I hope that you find it useful. I’d love to hear your feedback, so if there is anything that you love or think is missing from the app you can catch me on Twitter or post a message on the Neo4j Community site.

If you are interested in the other Graph Apps available, you can check out the Graph App Gallery or if you would like to build your own Graph App there is a developer guide at neo4j.com/developer/graph-app-development.

Happy Graphing!
- Adam

Neo4j Developer Blog

Developer Content around Graph Databases, Neo4j, Cypher…