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.tsfile: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 anactionobject 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 theactionobject to react differently in each of them.The function also receives an
eventApiobject through which it can listen to changes in the records to which the action is being applied; and asignalobject that indicates when this particular surfacing of the command is being removed.In our implementation, we subscribe to the
recordscontextchangeevent, 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
recordscontextchangeevent before we run oursetEnabled()code, so we specifydispatchNow: trueto 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
signalto the event listener so that it unsubscribes automatically from therecordscontextchangeevent 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 buildThis compiles the plug-in and bundles it into a
distfolder 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/configurationdirectory, and then navigate to thefragments/opal-servicesdirectory that it contains.If the
opal-servicesdirectory does not already contain apluginssubdirectory, create one.Create a directory for your plug-in inside the
pluginsdirectory (for example,plugins/plugin-basic), and copy the contents of thedistfolder into this folder.Run the following toolkit commands to update the deployed i2 Analyze server:
setup -t stopLiberty setup -t deployLiberty setup -t startLibertyStop 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