Creating a production-ready plug-in with the Create Notebook Plug-in tool
This tutorial takes you through creating a production-ready plug-in for the i2 Web Client using a project scaffold created with the create-notebook-plugin
tool.
Prerequisites
To follow the tutorial, you must install at least version 18 of Node.js on your development machine. For downloads and more information about Node.js, visit the project website at https://nodejs.org.
This tutorial also requires a running instance of i2 Analyze on which you have permission to use the i2 Web Client.
Create a plug-in
Run the following command on the command line:
npm create @i2analyze/notebook-plugin
This command installs and executes @i2analyze/create-notebook-plugin, a tool designed for the purpose of creating plug-in projects for the i2 Web Client.
When you run the tool, you'll see a number of options.
For now, use the default value for each option by pressing Enter
at each prompt.
$ create-notebook-plugin
Plugin-name: (My plug-in): My plug-in
Plugin-description: (My plug-in description): My plug-in description
Server URL: (http://localhost:9082/opal): http://localhost:9082/opal
Run git init?: (no) No
Run npm install?: (no) No
$
The tool creates a directory for the plug-in project whose name is derived from the name that you provide.
With the default values, the directory is named my-plug-in
.
When the project is ready, run the following commands to install dependencies and start the development server and the plug-in proxy:
cd my-plug-in
npm install
npm run dev
The plug-in project is now running!
In your browser, navigate to the Web Client at http://localhost:4000/opal/
and check that the plug-in has loaded correctly.
Look for a new button in the application ribbon, and a new message in the developer tools console:
My plug-in running
If you see this message, then the plug-in is working correctly.
The Create Notebook Plug-in tool configures the project folder for TypeScript, ESLint, and prettier.
It also creates devproxy.json
and plugin.json
manifests, and adds scripts to allow for development and production builds.
The latter use webpack
to create a bundle that's deployable to an i2 Analyze server.
Add a ribbon command to the plug-in
At this stage, the plug-in sends some basic information to the browser's developer tools console, and adds a button that does nothing to the application ribbon. Next, we'll replace the generated code with some calls to the i2 Web Client API that modify the button to open geospatial locations in Google Maps.
In
src/entrypoint.ts
, make the following changes.Change the first line from:
import { NotebookStatic } from '@i2analyze/notebook-sdk';
to:
import { commands, data, records, NotebookStatic } from '@i2analyze/notebook-sdk';
Change:
name: 'My plug-in';
to:
name: 'View on map';
In the
createCommand()
call, change:type: 'unscoped';
to:
type: 'records';
And change:
onExecute() {
to:
onExecute(payload: commands.IRecordsContext) {
Create this helper function at the top level of the
entrypoint.ts
file:function findGeospatialValue(chartRecords: data.IReadOnlyCollection<records.IChartRecord>) { for (const record of chartRecords) { for (const propertyType of record.itemType.propertyTypes) { if (propertyType.logicalType === 'geospatial') { const property = record.getProperty(propertyType); if (property !== undefined && !record.isValueUnfetched(property)) { return property as data.IGeoPoint; } } } } return undefined; }
This function performs a simple scan of the records that it receives, and returns the first non-empty geospatial property that it finds.
We can now use the helper function in the implementation of
onExecute()
, replacing the default code that the project was created with. We'll take the latitude and longitude to a new Google Maps window:onExecute(payload: commands.IRecordsContext) { const property = findGeospatialValue(payload.records); if (!property) { return; } window.open( `https://www.google.com/maps/@${property.latitude},${property.longitude},18z`, "_blank" ); },
Reload the Web Client, select an element with a geospatial property, and try the command. Google Maps should open at the relevant location.
Make the command sensitive to selection
There's a problem with our command: The button is enabled even if the selection doesn't actually contain a geospatial property. That would be misleading for a user, but we can fix it by taking control of the surfacing of the command in the user interface.
After the
onExecute()
parameter tocreateCommand()
, replace theonSurface()
function definition:onSurface(action, eventApi, signal) { eventApi.addEventListener( "recordscontextchange", (context) => { action.setEnabled(!!findGeospatialValue(context.records)); }, { dispatchNow: true, signal } ); },
There are a few things going on here:
The Web Client calls our
onSurface()
function with anaction
object that represents the user interface control to which the command is bound each time it is surfaced. A single command might be surfaced in several places, and you can use theaction
object to react differently in each of them.The function also receives an
eventApi
object through which it can listen to changes in the records to which the action is being applied; and asignal
object that indicates when this particular surfacing of the command is being removed.In our implementation, we subscribe to the
recordscontextchange
event, which tells us when the current records change. When they do change, we set the enabled state of the action to a value based on whether there is a geospatial value in the current records.Event listeners are normally invoked when the event occurs. However, we don't want to wait for a
recordscontextchange
event before we run oursetEnabled()
code, so we specifydispatchNow: true
to invoke the callback immediately, without waiting for the event. This in turn causes our action to be enabled or disabled correctly, right away.We forward the
signal
to the event listener so that it unsubscribes automatically from therecordscontextchange
event when the action is unsurfaced.
Reload the Web Client. The button in the application ribbon is now enabled only when a geospatial property exists in the selection.
Use the command in more than one place
As well as adding it to the ribbon, we can add exactly the same command to the chart pop-up menu with a single line of code.
After the existing call to
surfaceCommands()
, add:api.commands.chartPopupMenu.surfaceCommands(viewOnMap);
Reload the Web Client. The same command, with the same enablement rules, is now present in the chart's pop-up menu.
Deploy the plug-in
To deploy our plug-in on the server, we should rebuild it ready for deployment using a production build. We then need to add it to the server configuration, and then redeploy the server.
Note: If you follow this procedure in a deployment that provides high availability, you must complete each step on every Liberty server in your environment before you move to the next step.
Build a production version of the plug-in by executing the following command:
npm run build
This compiles the plug-in and bundles it into a
dist
folder in the project. That folder contains everything that you need to copy to the server.On the server that hosts the i2 Analyze deployment, find the
toolkit/configuration
directory, and then navigate to thefragments/opal-services
directory that it contains.If the
opal-services
directory does not already contain aplugins
subdirectory, create one.Create a directory for your plug-in inside the
plugins
directory (for example,plugins/plugin-basic
), and copy the contents of thedist
folder into this folder.Run the following toolkit commands to update the deployed i2 Analyze server:
setup -t stopLiberty setup -t deployLiberty setup -t startLiberty
Stop the development proxy, and use the browser to navigate to your real server address. You'll find that your plug-in was successfully deployed.
Next steps
By following the procedure in this tutorial, you've used the create-notebook-plugin
tool to create an i2 Web Client plug-in that, though basic, is fully integrated with the application.
It displays actions in the user interface, it responds to changes in selection, and it sends data from chart records to an external application.
Most real plug-ins go further than this one by adding a tool view that presents users with a custom interface for performing more complex tasks. You can see the fundamentals of how tool views work by following one of the examples that use popular UI frameworks to implement a tool view.
You can instruct create-notebook-plugin
to create such a plug-in by specifying a template
that uses React to implement a skeleton tool view:
npm create @i2analyze/notebook-plugin --template=plugin-with-toolview