To give a 3rd party applications the possibility to know, what active graph is and where it is contained, we provide a context.
First of all, context expose to the apps all data, that could be viable for them to operate - configurations, projects, graphs.
Context itself is an immutable structure which is generated on demand, but all context changes are interceptable, so the apps could react to context changes immediately.
Check out Changelog to follow API changes.
We provide a Neo4j Desktop Canary channel with nightly builds of Neo4j Desktop so developers can verify that their graph application works in the coming release.
See https://neo4j.com/download-center/ -> Neo4j Desktop -> Pre-releases.
Neo4j Desktop have Development mode
.
To enable it, open Settings
pane in Sidebar and toggle switch in Developer tools
section.
When development mode is enabled, additional app is added to the list of other apps: Development App
.
Developer needs to setup entry point and application root directory for Graph App in settings.
Settings:
- Entry Point
- Supported formats:
- File: load
.html
file directly from filesystem- Example:
file:///Users/me/work/graph-app/index.html
- Example:
- HTTP: load arbitrary
URL
- Example:
http://localhost:3000
- Example:
- File: load
- Supported formats:
- Root Path
- Example:
/Users/me/work/graph-app
- Example:
Note: settings are not saved between Neo4j Desktop restarts.
Available examples:
To prepare the graphQL connection, you'll need a set of libraries, we recommend using these
yarn add apollo-client apollo-link apollo-link-http apollo-link-ws
For managing both subscriptions and queries, passing custom headers, you'll need these
yarn add apollo-link-context apollo-utilities
The following code shows how to initialize the connection to the graphql server
// All the necessary information is available in the url, as searchParams
const url = new URL(window.location.href);
// Url of a graphQL endpoint
const apiEndpoint = url.searchParams.get('neo4jDesktopApiUrl');
// Parse API endpoint URL to get scheme-less
const apiEndpointUrl = new URL(apiEndpoint)
const apiEndpointNoScheme = `${apiEndpointUrl.host}${
apiEndpointUrl.pathname ? apiEndpointUrl.pathname : ''
}`
// A Desktop generated token to verify the provided appId
const apiClientId = url.searchParams.get('neo4jDesktopGraphAppClientId');
// http link to execute graphQL queries
const httpLink = createHttpLink({
uri: apiEndpoint,
});
// websocket link to be able to subscribe to the Desktop events
const wsLink = new WebSocketLink({
uri: `ws://${apiEndpointNoScheme}`,
options: {
reconnect: true,
connectionParams: {
ClientId: apiClientId
}
}
});
// link needed to inject custom headers to every query,
// so that Neo4j Desktop could identify the graph app and give as much data as it can
const authLink = setContext((_, {headers}) => {
return {
headers: {
...headers,
ClientId: apiClientId
}
}
});
// general link, used to choose a correct endpoint between the operation types
const link = split(
// split based on operation type
({query}) => {
const {kind, operation} = getMainDefinition(query);
return kind === 'OperationDefinition' && operation === 'subscription';
},
wsLink,
authLink.concat(httpLink),
);
// at this point client is ready to use
const client = new ApolloClient({
link: link,
cache: new InMemoryCache()
});
After the client has been initialized, you can execute queries and start subscriptions to the Desktop GraphQL server. The format of the output data is almost the same as it was before, when contextAPI was in place.
client.query({
query: /* your query */
}).then(({data}) => {
// do something with data here
});
const observable = client.subscribe({
query: /* subscribe query */
});
observable.subscribe(({data}) => {
// do something with data here, this callback will be triggered every time Desktop has its data changed
});
Extended example you can find here
Also the fallback API is still being injected into the graph apps, so that you could still use the API in the old way.
Note that fallback API is deprecated and will be removed in a future Neo4j Desktop release (the exact date is not known yet, wait for the announcement).
/**
* If application can run in multiple environments, detect that we are in Desktop.
*/
if (window.neo4jDesktopApi) {
// API will be available in global `window` variable `neo4jDesktopApi`.
// Listen for context changes
neo4jDesktopApi.onContextUpdate((event, newContext, oldContext) => {
if (event.type === "...") {
// do something if event is of specific type
}
// check context changes and apply them
});
// Get current context.
// Should be used if application requires current context when it starts.
neo4jDesktopApi.getContext().then(context => {
// initialize application with context
});
}
Graph applications should be distributed as a valid npm package file,
where dist
folder contains a default app entry point index.html
.
To install a self updating graph application, you enter a link to a npm style repository for the graph application.
Example for neo4j-browser: http://neo.jfrog.io/neo/api/npm/npm/neo4j-browser
.
This application will self update on every release (see below for how to display release notes).
or
Pack and upload a .tgz
file (following the structure explained above) and enter the URL to it in the graph application sidebar. This will be downloaded once and never updated.
or
Host a web application yourself and "install" it by inserting the URL to it in the graph application sidebar.
Manifest file manifest.json
should be placed in /dist
folder and contain information about the Graph App. Including Neo4j Desktop API version that is used.
Note: You can either specify explicit apiVersion
or semver range.
The fallback is using the package.json
file if no manifest file is found.
Example:
{
"name": "my-graph-app",
"version": "1.0.0",
"description": "(desktop)-[:LOVES]->(apps)",
"homepage": "http://neo4j.com",
"neo4jDesktop": {
"apiVersion": "^1.2.0"
}
}
For packages graph applications
Neo4j Desktop scans manifest.json
for the fields neo4jDesktop
, name
, description
, icons
, and homepage
to show the values of these fields
on the UI.
To customize the look of the graph app inside Neo4j Desktop - include an icon to the distribution and add icons
property to the manifest.json
.
Note: The paths should be relative to the location of the manifest file where they are specified:
- If they are specified in
package.json
, their src should be relative to the graph app root. - If they are specified in
dist/manifest.json
their src should be relative to thedist/
folder.
Icon type could be any image type, or inline data URI. Example:
{
"name": "my-graph-app",
"description": "(desktop)-[:LOVES]->(apps)",
"icons": [
{
"src": "./my-image.png",
"type": "png"
},
{
"src": "./my-vector-image.svg",
"type": "svg"
},
{
"src": "data:image/svg+xml;base64,[data]",
"type": "data"
}
],
"homepage": "http://neo4j.com"
}
For online/hosted graph applications
Neo4j Desktop looks for a manifest.json
in the web root and look for a <name>
tag to derive graph app name. The icon will be fetched from the <link rel="icon">
tag if it exists.
The fallback is using the documents <title>
tag if no manifest file is found.
Include release-notes.md
on the same level as package.json
to have Neo4j Desktop display your applications release notes when it's updated.
Notes:
- Ensure that
neo4jDesktop.apiVersion
is properly configured. - Ensure that package have proper structure.
If a graph application needs to use a privileged API (such as bundled Java or Node.js script execution), the app has to check and request the appropriate permission. Declare that your app needs a permission by listing the permission in the app manifest and then request that the user approve each permission at runtime.
To declare that your app needs a permission, put a permissions
field in your app manifest.json
Example:
{
"name": "my-graph-app",
"permissions": ["backgroundProcess", "allGraphs", "activeGraph"]
}
Currently available permissions:
Permission | Description |
---|---|
activeGraph | Gives access to the active Graph data. This is a default permission granted on app install. |
allGraphs | Gives access to all the configured Graphs. |
backgroundProcess | Gives access to executeJava and executeNode API. |
Explain why the app needs permissions
To help the user understand why your app needs a permission, add usage description to the list of permissions in form of map-like object:
{
"name": "my-graph-app",
"permissions": [
"activeGraph",
{
"backgroundProcess": "Allow background processes to see output of demo Java class",
"allGraphs": "Another usage description here"
}
]
}
To check if you have a permission, call the checkPermission()
method.
window.neo4jDesktopApi.checkPermission("backgroundProcess").then(granted => {
if (!granted) {
// Permission is not granted.
}
});
If your app doesn't already have the permission it needs, call requestPermission()
method to request the appropriate permission from user. Only permissions declared in app manifest can be requested.
window.neo4jDesktopApi.requestPermission("backgroundProcess").then(granted => {
if (granted) {
// Permission granted.
} else {
// Permission denied.
}
});
If the permission has not been already granted, the system dialog box is shown:
A graph application may specify database plugins required for the graph app to work. It is applicable for any locally installed and on-premise graph apps.
To specify the dependencies, add pluginDependencies
section to manifest.json
file.
For example:
{
"name": "my-graph-app",
"pluginDependencies": [
"org.neo4j.procedure/apoc",
"org.neo4j/graph-algorithms-algo",
"org.neo4j/neo4j-graphql"
]
}
A dependency format is Maven coordinates of a plugin: groupdId/artifactId
. A compatible plugin version is resolved at runtime. Neo4j Desktop automatically installs the required plugins to all the databases in a project having the graph application enabled.
OS | Location |
---|---|
macOS | ~/Library/Application Support/Neo4j Desktop |
Windows pre 1.0.19 | %APPDATA%/Neo4j Desktop |
Windows post 1.0.19 | %USERPROFILE%/.Neo4jDesktop |
Linux | ~/.config/Neo4j Desktop |
Note: API is under development and it can be changed, based on user feedback.
Note: API definition is presented using TypeScript syntax.
export interface DesktopApi {
/**
* API version.
*/
version: string;
/**
* Asynchronously get current context.
*/
getContext: () => Promise<Context>;
/**
* Register callback to receive context updates when events are happening.
*/
onContextUpdate: (
callback: (event: Event, newContext: Context, oldContext: Context) => void
) => void;
/**
* Execute any jar, bundled inside you app package or given path. Will return wrapped process with API provided
*/
executeJava: (parameters: JavaParameters) => Promise<Process>;
/**
* Execute any node script, bundled inside you app package or given path.
* Will return wrapped process with API provided
*/
executeNode: (
filePath: string,
args: [string],
options: ExecOptions
) => Promise<Process>;
/**
* Requests a permission from user.
* Only permissions declared in `manifest.json` can be requested.
*
* Permissions types:
*
* activeGraph access active Graph data
* allGraphs access to all the configured Graphs
* backgroundProcess execute background Java and Node.js processes
*
* @param permission
* @return true if permission is granted
*/
requestPermission: (permission: PermissionType) => Promise<boolean>;
/**
* Check if a permission is granted.
* @param permission
*/
checkPermission: (permission: PermissionType) => Promise<boolean>;
/**
* Asynchronously get kerberos ticket for given service principal
* Service principal can be found in context
*/
getKerberosTicket: (
servicePrincipal: string
) => Promise<KerberosTicketResult>;
/**
* Register callback to receive arguments updates
* paramsString - pures string with params. ex.: cmd=play&args=music
* params - parsed params Map ([key: string]: string | null)
*
* How to call graph-app with param - neo4j://GraphAppName?cmd=play&args=music
*/
onArgumentsChange: (
callback: (paramsString: string, params: ParamsMap) => void
) => void;
getJwtToken: (dbId: string) => Promise<JWTTokenResult>;
}
export interface ParamsMap {
[key: string]: string | null;
}
export type PermissionType = "activeGraph" | "allGraphs" | "backgroundProcess";
//---------------
// Java
//---------------
export type JavaParameters =
/**
* Specify class or jar that should be executed.
* Path to a .jar file can either be relative to App Path or absolute.
* Example: 'Main'
* Example: './test.jar'
*/
{ [x in "class" | "jar"]: string } & {
/**
* JVM arguments.
* Example: ['-DmyProperty=value', '-Xdebug']
*/
options: string[];
/**
* Jar's that will be added to classpath.
* Example: ['./test.jar', '/opt/lib/test.jar']
*/
classpath: string[];
/**
* Argument passed to a main.
* Example: ['one', 'two', 'three']
*/
arguments: string[];
};
type ProcessStatus = "RUNNING" | "STOPPED" | "KILLED";
interface Process {
/**
* Stop the process tree gracefully, if fails - kill the process tree forcefully
*/
stop(): Promise<boolean>;
/**
* Get the actual status of the process
*/
status(): Promise<ProcessStatus>;
/**
* Get the list of PIDs for whole process tree
*/
getProcessTreeIds(): Promise<number[]>;
/**
* Listen to process-related errors (e.g. not being able to start)
*/
onError(listener: (error: Error) => void): void;
/**
* Listen to process exit event. Provides the status which was assigned the last.
*/
onExit(listener: (status: ProcessStatus) => void): void;
/**
* Attach to the stdout stream
*/
addOutListener(listener: (data: string) => void): void;
/**
* Attach to the stderr stream
*/
addErrListener(listener: (errData: string) => void): void;
}
//---------------
// Node
//---------------
interface EnvOptions {
[key: string]: string;
}
export interface ExecOptions {
cwd?: string;
env?: EnvOptions;
}
//---------------
// Kerberos
//---------------
interface KerberosTicketResult {
ticket?: string;
error?: string;
}
//---------------
// JWT
//---------------
export interface JWTTokenResult {
token?: string;
error?: string;
}
//---------------
// Context
//---------------
export interface Context {
global: {
settings: Settings;
online: boolean;
};
projects: Project[];
activationKeys: GraphAppLicense[];
}
export interface GraphAppLicense {
featureName: string;
expirationDate: string;
activationVersion: string;
featureVersion: string;
registrant: string;
organization: string;
email: string;
signature: string;
}
export interface Settings {
allowSendStats: boolean;
allowSendReports: boolean;
allowStoreCredentials: boolean;
}
export interface Project {
id: string;
name: string;
graphs: Graph[];
}
export interface Graph {
id: string;
name: string;
description: string;
status: "ACTIVE" | "INACTIVE";
connection: GraphLocalConnection | GraphRemoteConnection;
}
export interface GraphLocalConnection {
type: "LOCAL";
databaseType: "neo4j";
databaseStatus: GraphLocalConnectionStatus;
info: {
version: string;
edition: string;
};
configuration: GraphLocalConnectionConfiguration;
}
export interface GraphLocalConnectionConfiguration {
path: string;
protocols: {
bolt: {
enabled: boolean;
host: string;
port: number;
tlsLevel: "OPTIONAL" | "REQUIRED" | "DISABLED";
username?: string;
password?: string;
url: string;
};
http: {
enabled: boolean;
host: string;
port: number;
url: string;
};
https: {
enabled: boolean;
host: string;
port: number;
url: string;
};
};
authenticationMethods?: {
kerberos: {
enabled: boolean;
};
};
}
export type GraphLocalConnectionStatus =
| "STOPPED"
| "STOPPING"
| "STARTING"
| "RESTARTING"
| "RUNNING"
| "UNKNOWN"
| "NEW"
| "CREATING"
| "REMOVING"
| "UPGRADING"
| "MISSING";
export interface GraphRemoteConnection {
type: "REMOTE";
databaseType: "neo4j";
databaseStatus: GraphRemoteConnectionStatus;
info: {
version: "UNKNOWN" | string;
edition: "UNKNOWN" | string;
};
configuration: GraphRemoteConnectionProtocols;
}
export interface GraphRemoteConnectionProtocols {
protocols: {
bolt: {
enabled: boolean;
host: string;
port: number;
tlsLevel: "OPTIONAL" | "REQUIRED" | "DISABLED";
username?: string;
password?: string;
url: string;
};
http: {
enabled: boolean;
host: string;
port: number;
url: string;
};
https: {
enabled: boolean;
host: string;
port: number;
url: string;
};
};
authenticationMethods?: {
kerberos: {
enabled: boolean;
servicePrincipal?: string;
};
};
}
export type GraphRemoteConnectionStatus =
| "UNKNOWN"
| "NEW"
| "CREATING"
| "REMOVING"
| "ACTIVATING"
| "AVAILABLE"
| "UPDATING"
| "NOT_AVAILABLE"
| "INVALID_PASSWORD"
| "DEACTIVATING"
| "DEACTIVATED";
//---------------
// Events
//---------------
export type Event =
| ApplicationOnlineEvent
| ApplicationOfflineEvent
| ProjectCreatedEvent
| ProjectRemovedEvent
| ProjectRenamedEvent
| GraphActiveEvent
| GraphInactiveEvent
| DatabaseCreatedEvent
| DatabaseStartedEvent
| DatabaseStoppedEvent
| DatabaseRenamedEvent
| DatabaseRemovedEvent
| DatabaseUpdatedEvent
| DatabaseUpgradedEvent
| DatabaseSettingsSavedEvent
| RemoteConnectionCreatedEvent
| RemoteConnectionRemovedEvent
| RemoteConnectionActivatedEvent
| RemoteConnectionDeactivatedEvent;
interface ApplicationOnlineEvent {
type: "APPLICATION_ONLINE";
}
interface ApplicationOfflineEvent {
type: "APPLICATION_OFFLINE";
}
interface ProjectCreatedEvent {
type: "PROJECT_CREATED";
id: string;
name: string;
}
interface ProjectRemovedEvent {
type: "PROJECT_REMOVED";
id: string;
}
interface ProjectRenamedEvent {
type: "PROJECT_RENAMED";
id: string;
name: string;
}
interface GraphActiveEvent {
type: "GRAPH_ACTIVE";
id: string;
}
interface GraphInactiveEvent {
type: "GRAPH_INACTIVE";
id: string;
}
interface DatabaseCreatedEvent {
type: "DATABASE_CREATED";
id: string;
projectId: string;
name: string;
description: string;
status: GraphLocalConnectionStatus;
version: string;
edition: "community" | "enterprise";
}
interface DatabaseStartedEvent {
type: "DATABASE_STARTED";
id: string;
}
interface DatabaseStoppedEvent {
type: "DATABASE_STOPPED";
id: string;
}
interface DatabaseRenamedEvent {
type: "DATABASE_RENAMED";
id: string;
name: string;
}
interface DatabaseRemovedEvent {
type: "DATABASE_REMOVED";
id: string;
}
interface DatabaseUpdatedEvent {
type: "DATABASE_UPDATED";
id: string;
database: {
description: string;
};
}
interface DatabaseUpgradedEvent {
type: "DATABASE_UPGRADED";
id: string;
version: string;
}
interface DatabaseSettingsSavedEvent {
type: "DATABASE_SETTINGS_SAVED";
id: string;
}
interface RemoteConnectionCreatedEvent {
type: "REMOTE_CONNECTION_CREATED";
id: string;
}
interface RemoteConnectionRemovedEvent {
type: "REMOTE_CONNECTION_REMOVED";
id: string;
}
interface RemoteConnectionActivatedEvent {
type: "REMOTE_CONNECTION_ACTIVATED";
id: string;
}
interface RemoteConnectionDeactivatedEvent {
type: "REMOTE_CONNECTION_DEACTIVATED";
id: string;
}