Skip to content

Developing a Panorama Module

Sam Habiel edited this page Aug 14, 2017 · 6 revisions

Constructing a Panorama Module

I am Sam Habiel, the creator of the Panorama Framework. When I use 'I', you will know who is talking. Alexis Carlson wrote a bunch of code to help me, and I may refer to him separately when he does something differently.

Panorama is a Web-UI framework for use with the EMRs VistA (formerly known as DHCP) and its cousin RPMS. It is based on QEWD.js. If you like it but want to use it with other applications/EMRs, visit http://qewdjs.com/. Panorama is expressly written for VistA/RPMS, and won't work for other EMRs.

This is a simple tutorial for creating a new module for VistA/RPMS using the Panorama Framework. This example is based on a real application I am developing, and thus the name dba for database administration. Your application can be of course named anything. I will be using a file I created in VistA for Panorama, but you can substitute any file you want.

When developing Panorama, a capability which makes development easier is to export NODE_ENV to production. This enables copying of routines to your M database and web assets to the qewd/www/ewd-vista/ directory. This copying happens only upon application start-up--it doesn't take up any user time. This means that when I make a change in the user side Javascript file, I have to restart QEWD in order to get my changes, and then I have to CTRL-F5 refresh my browser to do a cold refresh. Alexis doesn't do that--instead, he creates symlinks to the files that are currently being developed, and unsets NODE_ENV. This way, he does not have to restart QEWD to copy the assets over. He still has to CTRL-F5 refresh the browser though.

Pre-requisites

I don't teach you how to develop in node.js over here. You have to know that already.

Panorama is a framework for providing a Web Interface for VistA or RPMS. This means you you need to have a VistA or RPMS instance up and running on Caché or GT.M. The install instructions tell you how to configure the qewd.js file to talk to Caché or GT.M.

Panorama uses ES6 features: specifically: let, arrow functions, Object.keys/ Object.values/Object.assign.

My development environment

I typically develop Panorama using Chrome or Chromium and vim as the editor. Vim uses ESLint with the Syntastic Plugin for linting my Javascript Code. You are free to use any editor and debugger you want.

In order to debug code, I start with just doing console.log of the offending code. More advanced debugging is done by running node --inspect qewd.js, which gives you a couple of URLs, one for the master process, and one for the worker process. Copy the URL for the worker process to Chrome or Chromium for debugging. Once you do that, you will see a list of modules on your left hand pane. Panorama lazy loads modules, so you probably have to visit the module on your web page to get its source code to show up. Once you do that, you can open the JS files and place breakpoints.

QEWD Folder Structure

If you follow the install instructions, you should have a folder structure that looks like this:

home directory
|
|- qewd
|-- www/
|-- node_moudles/
|--- qewd
|--- ewd-vista
|--- ewd-vista-bedboard
|--- ewd-vista-fileman

There is nothing in Panorama that requires you to put qewd in your home directory--it's just a convention. You can place it anywhere you like. What is important is that all the ewd-vista* modules need to be under qewd/node_modules/, and they all need to be siblings of one another.

Creating a New Module

Create a folder under qewd/node_modules, with your module name. In this example, it's "ewd-vista-dba". It has to start with "ewd-vista-".

In that folder create a package.json file that looks like this.

{
  "name": "ewd-vista-dba",
  "engines": {
    "node": ">=6"
  },
  "version": "0.0.1",
  "description": "EWD VistA: DBA Module",
  "main": "index.js",
  "author": "Sam Habiel",
  "license": "Apache-2.0",
  "repository": {
    "type": "git",
    "url": "git+https://github.com/shabiel/ewd-vista-dba.git"
  },
  "devDependencies": {
    "eslint": "",
    "eslint-plugin-json": ""
  },
  "ewdVista": {
    "parentModule": "ewd-vista",
    "name": "Database Administration",
    "htmlName": "dba",
    "sortOrder": "50",
    "securityKey": "XUMGR"
  }
}

Here it is again, with annotations. (// -- you can't copy this file. Use the one above.)

{
  "name": "ewd-vista-dba", // This is the module name. Panorama uses this internally.
  "engines": {             // This says that you need Node.js 6 or above. Always true for Panorama.
    "node": ">=6"
  },
  "version": "0.0.1",      // Not used by Panorama, but used by npm.
  "description": "EWD VistA: DBA Module", // ditto
  "author": "Sam Habiel",
  "license": "Apache-2.0",
  "repository": {
    "type": "git",
    "url": "git+https://github.com/shabiel/ewd-vista-dba.git"
  },
  "devDependencies": {
    "eslint": "",
    "eslint-plugin-json": ""
  },
  "ewdVista": {
    "parentModule": "ewd-vista", // This is used to construct menus
    "name": "Database Administration", // This is the menu text
    "htmlName": "dba",                 // This is the name of the css and js file and main js object that has the prep method.
    "sortOrder": "50",                 // This is the position of the menu relative to other modules
    "securityKey": "XUMGR"             // This is the security key. Users who don't hold it cannot get in.
  }
}

I need this security key in my application--so I will be keeping it--but if you are a new to VistA or RPMS, you may as well keep this blank.

If the htmlName contains dashes or starts with numbers, you cannot use it as a javascript object name. In that case, you need to use 'clientModuleName' as an additional property. E.g.

    "htmlName": "dba-module",
    "clientModuleName": "dbaModule"

The next step is to create the supporting files folders: You need to create index.js and then create www/assets/javascripts/dba.js and www/assets/stylesheets/dba.css. (dba is the htmlName). Create a fragments folder and put a file it called dba.html. (This last name doesn't matter really, as you will load the files from there--Panorama won't do it for you based on a naming convention).

Here's the output of the tree command from the ewd-vista-dba folder:

├── fragments
│   └── dba.html
├── index.js
├── package.json
└── www
    └── assets
        ├── javascripts
        │   └── dba.js
        └── stylesheets
            └── dba.css

At this point, you should be able to open Panorama, and see the the application in the menu. If you restart/reload the QEWD node application, you will see this:

menu

If you click on the menu, you won't see anything (actually, you may see that calling dba.prep fails if you have the console open as we haven't created that method yet).

User Interface

At this point, the next step is to add the dba.prep method to dba.js.

var dba = {}; // main object!!!

dba.prep = function(EWD) {
  console.log("I got loaded");
};

Pay attention to the naming of the main object. This the name supplied by htmlName or clientModuleName in package.json . If clientModuleName is supplied, it overrides the htmlName. So if clientModuleName is dbaMod and the htmlName is dba-object, then Panorama will try to load object dbaMod in the javascript file named dba-object.js. In our case, since we just supplied an htmlName of dba, our file is dba.js and our object in the file is dba.

At this point, restart QEWD, log-in, and click on Database Administration. If you look at the console after clicking, you will see the message.

console

If you switch to the network tab, you will see that we loaded dba.css and dba.js only when we clicked on the database administration menu.

network

At this point, theoretically, you can do whatever you want with the webpage since Panorama is executing prep in your new module. However, we need to now write code to access the database to present something useful on your application.

Backend module code

Add this code to the previously empty file, index.js:

'use strict';

let vista = require('ewd-vista');

module.exports          = {};
module.exports.handlers = {};

module.exports.init = function() {
  vista.init.call(this);
};

module.exports.beforeHandler = vista.beforeHandler;

// Get Numberspace Data
module.exports.handlers.getNumberspaceData = function(messageObj, session, send, finished) {
  finished({'serverKey': messageObj.params.clientKey + ' from Server'});
};

Let's talk about init first. You SHOULD call vista.init, assigning this to the current object. The vista object is normally initialized; however, if you stop QEWD and start it again, it won't be; and you risk not having parts of your code fail since the symbol table and various helpers are not around to invoke.

Next: beforeHandler. You MUST assign this module's beforeHandler to vista.beforeHandler. This does runtime security checking. This checks EVERY SINGLE CALL to the backend to check that you are authenticated and authorized. The authorization check is done using the security keys assigned to the module.

Next: the handlers object. The way QEWD works as a framework is that it communicates to modules using named messages. To create a new message type, all you have to do is add a new function to the handlers object with a signature of 4 parameters. The 4 parameters are:

  • messageObj: The message being sent from the browser.
  • session: an object which makes it easy to check the current session.
  • send: a method to send intermediate results in a long running operation.
  • finished: a method to send final results.

At this point, let's see if we can actually call this. Modify dba.js to show this:

var dba = {};

dba.prep = function(EWD) {
  let messageObj = {
    service: 'ewd-vista-dba', // The name of the module in package.json
    type: 'getNumberspaceData', // The name of the handler
    params: {
      clientKey: 'hello from client'
    }
  };

  EWD.send(messageObj, function(responseObj) {
    $('#main-content').html(responseObj.message.serverKey);
  });
};

First Result

$('#main-content') is the main drawing div in Panorama. What the above shows is a summary of how you communicate with QEWD. You send a message with EWD.send() and handle the response in the callback.

Now, let's try to load the VistA Title global ^DIC(3.1). Change getNumberspaceData in index.js to as follows:

module.exports.handlers.getNumberspaceData = function(messageObj, session, send, finished) {
  let titleFile = this.db.use('DIC','3.1');
  let data      = titleFile.getDocument();
  finished({'serverKey': data});
};

A few items to explain:

  • this.db is responsible for all database communication. this.db.function lets you execute M functions; this.db.procedure lets you execute M procedures. this.db.use is like the M instrinic function $NAME -- it gives you a reference you can use later to access the global containing the data. Due to a bug with cache.node (it doesn't perform float to M String conversions properly), you need to make sure that if you use decimals in subscripts to quote them as if they are strings. Thus the reason '3.1' is quoted. Once Intersystems tells me that that is fixed, you won't need to quote the numbers anymore. However, keep in mind that M represents decimals without a leading zero. So if you want a .1 node in a global, you should grab it as '.1'.
  • this.db.use accepts a series of subscripts to construct a global name. If you want ^PS(50.6,0), you would write this.db.use('PS','50.6',0).
  • Once you have a reference to a global using this.db.use, you use .getDocument to get all the data under that global. QEWD provides this feature to obtain all the data in a global as a JSON document.

We need to change the front end dba.js to stringify the resulting JSON document:

EWD.send(messageObj, function(responseObj) {
  $('#main-content').html(JSON.stringify(responseObj.message.serverKey));
});

Title File Global

That data is too disoraganized to be useful, as we are grabbing the global with everything, including indexes, and the zero data dictionary nodes.

So rather than getting everything, let's loop through the global and grab the zero nodes:

module.exports.handlers.getNumberspaceData = function(messageObj, session, send, finished) {
  let titleFile = this.db.use('DIC','3.1');
  let data = new Array();
  titleFile.forEachChild((ien,node) => {
    let zeroNode = node.$(0);
    if (zeroNode.exists) data.push(zeroNode.value);
  });
  finished({'serverKey': data});
};

Title File Zero Nodes

Let's explain the code: The M FOR loop is done using node.forEachChild, where node is a global reference obtained using this.db.use. .forEachChild accepts a call back with two parameters: the for loop subscript (here called ien), and the node variable is the global plus the loop subscript (here called node). So if ien is 1, then node is ^DIC(3.1,1).

Let's talk about the .$ operator. .$ on a node lets you traverse down subscripts. So node.$(0) on a node of ^DIC(3.1,1) will obtain node ^DIC(3.1,1,0). In the example above, we assign that to the variable zeroNode.

.exists is the same as $data in M. It checks to see whether a node exists.

.value gets you the value of the node. Unlike M, grabbing a non-existent value does not cause an error. You will just get back an empty string.