-
Notifications
You must be signed in to change notification settings - Fork 4
Developing 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.
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.
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.
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.
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:
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).
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.
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.
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.
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);
});
};
$('#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 writethis.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));
});
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});
};
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.