Get started with the Zowe WebUi, Part 2

This tutorial is the second part in a series. Part 1 showed you how to install prerequisites and set up a local development environment. If you have not set up your local development environment yet, please review Part 1.

Create a user database browser app on zLUX

Next, you will create and add your own app to Zowe.

The rest of this tutorial contains code snippets and descriptions that you can piece together to build a complete app. It builds off the project skeleton code found at the github project repo.

  1. Construct an app skeleton
  2. Build your first dataservice
  3. Add your first widget
  4. Add Zowe app-to-app communication

1. Construct an app skeleton

Download the skeleton code from the project repository. Next, move the project into the zlux source folder created in the prerequisite tutorial.

If you look within this repository, you’ll see that a few boilerplate files already exist to help you get your first app running quickly. The structure of this repository follows the guidelines for Zowe app filesystem layout, which you can read more about in this wiki.

Define your first plugin

So, where do you start when making an app? In the Zowe framework, an app is a plugin of type “application.” Every plugin is bound by its pluginDefinition.json file, which describes what properties it has. Let’s start by creating this file.

Make a file, pluginDefinition.json, at the root of the workshop-user-browser-app folder. The file should contain the following:

{
  "identifier": "org.openmainframe.zowe.workshop-user-browser",
  "apiVersion": "1.0.0",
  "pluginVersion": "0.0.1",
  "pluginType": "application",
  "webContent": {
    "framework": "angular2",
    "launchDefinition": {
      "pluginShortNameKey": "userBrowser",
      "pluginShortNameDefault": "User Browser",
      "imageSrc": "assets/icon.png"
    },
    "descriptionKey": "userBrowserDescription",
    "descriptionDefault": "Browse Employees in System",
    "isSingleWindowApp": true,
    "defaultWindowStyle": {
      "width": 1300,
      "height": 500
    }
  }
}

You might wonder why you’d choose the particular values that are put into this file. A description of each can be found in the wiki.

Of the many attributes here, you should be aware of the following:

  • Your app has the unique identifier of org.openmainframe.zowe.workshop-user-browser, which can be used to refer to it when running Zowe.
  • The app has a webContent attribute, because it will have a UI component visible in a browser.
    • The webContent section states that the app’s code will conform to Zowe’s angular app structure, due to it stating "framework": "angular2".
    • The app has certain characteristics that the user will see, such as:
      • The default window size (defaultWindowStyle)
      • An app icon that you provided in workshop-user-browser-app/webClient/src/assets/icon.png
      • You should see it in the browser as an app named User Browser, with the value pluginShortNameDefault

Construct a simple Angular UI

Angular apps for Zowe are structured such that the source code exists within webClient/src/app. In here, you can create modules, components, templates, and services in whatever hierarchy you like. For the app you are making here, however, you’ll keep it simple by adding just three files:

  • userbrowser.module.ts
  • userbrowser-component.html
  • userbrowser-component.ts

Let’s start by just building a shell of an app that can display some simple content. Fill in each file with the following contents.

userbrowser.module.ts
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { HttpModule } from '@angular/http'

import { UserBrowserComponent } from './userbrowser-component'

@NgModule({
  imports: [FormsModule, ReactiveFormsModule, CommonModule],
  declarations: [UserBrowserComponent],
  exports: [UserBrowserComponent],
  entryComponents: [UserBrowserComponent]
})
export class UserBrowserModule {}
userbrowser-component.html
<div class="parent col-11" id="userbrowserPluginUI">
{{simpleText}}
</div>

<div class="userbrowser-spinner-position">
  <i class="fa fa-spinner fa-spin fa-3x" *ngIf="resultNotReady"></i>
</div>
userbrowser-component.ts
import {
  Component,
  ViewChild,
  ElementRef,
  OnInit,
  AfterViewInit,
  Inject,
  SimpleChange
} from '@angular/core'
import { Observable } from 'rxjs/Observable'
import { Http, Response } from '@angular/http'
import 'rxjs/add/operator/catch'
import 'rxjs/add/operator/map'
import 'rxjs/add/operator/debounceTime'

import {
  Angular2InjectionTokens,
  Angular2PluginWindowActions,
  Angular2PluginWindowEvents
} from 'pluginlib/inject-resources'

@Component({
  selector: 'userbrowser',
  templateUrl: 'userbrowser-component.html',
  styleUrls: ['userbrowser-component.css']
})
export class UserBrowserComponent implements OnInit, AfterViewInit {
  private simpleText: string
  private resultNotReady: boolean = false

  constructor(
    private element: ElementRef,
    private http: Http,
    @Inject(Angular2InjectionTokens.LOGGER) private log: ZLUX.ComponentLogger,
    @Inject(Angular2InjectionTokens.PLUGIN_DEFINITION)
    private pluginDefinition: ZLUX.ContainerPluginDefinition,
    @Inject(Angular2InjectionTokens.WINDOW_ACTIONS)
    private windowAction: Angular2PluginWindowActions,
    @Inject(Angular2InjectionTokens.WINDOW_EVENTS)
    private windowEvents: Angular2PluginWindowEvents
  ) {
    this.log.info(`User Browser constructor called`)
  }

  ngOnInit(): void {
    this.simpleText = `Hello World!`
    this.log.info(`App has initialized`)
  }

  ngAfterViewInit(): void {}
}

Package your web app

You have now created the source for a Zowe app that should open up in the desktop with a greeting to the planet. Before you can use it, however, you have to transpile the TypeScript and package the app. This will require a few build tools first. You’ll need to make an NPM package in order to facilitate this.

Let’s create a package.json file within workshop-user-browser-app/webClient. While a package.json can be created through other means such as npm init, and packages can be added via commands such as npm install --save-dev typescript@2.9.0, you can save time by just pasting these contents in:

{
  "name": "workshop-user-browser",
  "version": "0.0.1",
  "scripts": {
    "start": "webpack --progress --colors --watch",
    "build": "webpack --progress --colors",
    "lint": "tslint -c tslint.json \"src/**/*.ts\""
  },
  "private": true,
  "dependencies": {},
  "devDependencies": {
    "@angular/animations": "~6.0.9",
    "@angular/common": "~6.0.9",
    "@angular/compiler": "~6.0.9",
    "@angular/core": "~6.0.9",
    "@angular/forms": "~6.0.9",
    "@angular/http": "~6.0.9",
    "@angular/platform-browser": "~6.0.9",
    "@angular/platform-browser-dynamic": "~6.0.9",
    "@angular/router": "~6.0.9",
    "@zlux/grid": "git+https://github.com/zowe/zlux-grid",
    "@zlux/widgets": "git+https://github.com/zowe/zlux-widgets",
    "angular2-template-loader": "~0.6.2",
    "copy-webpack-plugin": "~4.5.2",
    "core-js": "~2.5.7",
    "css-loader": "~1.0.0",
    "exports-loader": "~0.7.0",
    "file-loader": "~1.1.11",
    "html-loader": "~0.5.5",
    "rxjs": "~6.2.2",
    "rxjs-compat": "~6.2.2",
    "source-map-loader": "~0.2.3",
    "ts-loader": "~4.4.2",
    "tslint": "~5.10.0",
    "typescript": "~2.9.0",
    "webpack": "~4.0.0",
    "webpack-cli": "~3.0.0",
    "webpack-config": "~7.5.0",
    "zone.js": "~0.8.26"
  }
}

Before you can build, you need to tell your system where your example server is located. While you could provide the explicit path to the server in your project, creating an environmental variable with this location will speed up future projects.

To add an environmental variable on a Unix-based machine:

  1. cd ~
  2. nano .bash_profile
  3. Add export MVD_DESKTOP_DIR=/Users/<user-name>/path/to/zlux/zlux-app-manager/virtual-desktop/
  4. Save and exit
  5. source ~/.bash_profile

Now you’re really ready to build. Set up your system to automatically perform these steps every time you make updates to the app.

  1. Open up a command prompt to workshop-user-browser-app/webClient.
  2. Execute npm install.
  3. Execute npm run start.

OK, after the first execution of the transpilation and packaging concludes, you should have workshop-user-browser-app/web populated with files that can be served by the Zowe app server.

Add your app to the desktop

At this point, your workshop-user-browser-app folder contains files for an app that can be added to a Zowe instance. You’ll add this to your own Zowe instance. Now you’re ready to run the server and see your app:

  1. cd ~/my-zowe/zlux-app-server/bin
  2. ./install-app.sh /path-to-plugin/workshop-user-browser-app
  3. ./appServer.sh
  4. Open your browser to https://hostname:port.
  5. Login with your credentials.
  6. Open the app on the bottom of the page with the green ‘U’ icon.

Do you see your “Hello World” message from this earlier step? If so, you’re in good shape! Now, let’s add some content to the app.

2. Build your first dataservice

An app can have one or more dataservices. A dataservice is a REST or Websocket endpoint that can be added to the Zowe app server.

To demonstrate the use of a dataservice, you can add one to this app. The app needs to display a list of users, filtered by some value. Ordinarily, this sort of data would be contained within a database, where you can get rows in bulk and filter them in some manner. Likewise, retrieval of database contents is a task that is easily representable via a REST API, so let’s make one…

Create a file named workshop-user-browser-app/nodeServer/ts/tablehandler.ts and add the following contents:

import { Response, Request } from 'express'
import * as table from './usertable'
import { Router } from 'express-serve-static-core'

const express = require('express')
const Promise = require('bluebird')

class UserTableDataservice {
  private context: any
  private router: Router

  constructor(context: any) {
    this.context = context
    let router = express.Router()

    router.use(function noteRequest(req: Request, res: Response, next: any) {
      context.logger.info('Saw request, method=' + req.method)
      next()
    })

    router.get('/', function(req: Request, res: Response) {
      res.status(200).json({ greeting: 'hello' })
    })

    this.router = router
  }

  getRouter(): Router {
    return this.router
  }
}

exports.tableRouter = function(context): Router {
  return new Promise(function(resolve, reject) {
    let dataservice = new UserTableDataservice(context)
    resolve(dataservice.getRouter())
  })
}

This is boilerplate for making a dataservice. You lightly wrap ExpressJS routers in a Promise-based structure where you can associate a router with a particular URL space, which you will see later. If you were to attach this to the server and do a GET on the associated root URL, you’d receive the {"greeting":"hello"} message.

Work with ExpressJS

Let’s move beyond “Hello World” and access this user table.

1. Within workshop-user-browser-app/nodeServer/ts/tablehandler.ts, add a function for returning the rows of the user table.

const MY_VERSION = '0.0.1'
const METADATA_SCHEMA_VERSION = '1.0'
function respondWithRows(rows: Array<Array<string>>, res: Response): void {
  let rowObjects = rows.map(row => {
    return {
      firstname: row[table.columns.firstname],
      mi: row[table.columns.mi],
      lastname: row[table.columns.lastname],
      email: row[table.columns.email],
      location: row[table.columns.location],
      department: row[table.columns.department]
    }
  })

  let responseBody = {
    _docType: 'org.openmainframe.zowe.workshop-user-browser.user-table',
    _metaDataVersion: MY_VERSION,
    metadata: table.metadata,
    resultMetaDataSchemaVersion: '1.0',
    rows: rowObjects
  }
  res.status(200).json(responseBody)
}

Because you reference the usertable file via import, you are able to refer to its metadata and columns attributes here. This respondWithRows function expects an array of rows, so you can improve the router to call this function with some rows so that you can present them back to the user.

2. Update the UserTableDataservice constructor, modifying and expanding upon the router.

  constructor(context: any){
    this.context = context;
    let router = express.Router();
    router.use(function noteRequest(req: Request,res: Response,next: any) {
      context.logger.info('Saw request, method='+req.method);
      next();
    });
    router.get('/',function(req: Request,res: Response) {
      respondWithRows(table.rows,res);
    });

    router.get('/:filter/:filterValue',function(req: Request,res: Response) {
      let column = table.columns[req.params.filter];
      if (column===undefined) {
        res.status(400).json({"error":"Invalid filter specified"});
        return;
      }
      let matches = table.rows.filter(row=> row[column] == req.params.filterValue);
      respondWithRows(matches,res);
    });

    this.router = router;
  }

Zowe’s use of ExpressJS routers allows you to quickly assign functions to HTTP calls such as GET, PUT, POST, DELETE, or even websockets, and provides you with easy parsing and filtering of the HTTP requests so that there is very little involved in making a good API for your users.

This REST API now allows for two GET calls to be made: one to root / and the other to /filter/value. The behavior here is as defined in the ExpressJS documentation for routers, where the URL is parameterized to give you arguments that you can feed into your function for filtering the user table rows before giving the result to respondWithRows for sending back to the caller.

Add your dataservice to the plugin definition

Now that the dataservice is made, you need to add it to your plugin’s definition so that the server is aware of it, and build it so that the server can run it.

1. Open up a (third) command prompt to workshop-user-browser-app/nodeServer.

2. Install dependencies, npm install.

3. Invoke the NPM build process, npm run start.

4. Edit workshop-user-browser-app/pluginDefinition.json, adding a new attribute that declares dataservices.

"dataServices": [
    {
      "type": "router",
      "name": "table",
      "serviceLookupMethod": "external",
      "fileName": "tablehandler.js",
      "routerFactory": "tableRouter",
      "dependenciesIncluded": true,
      "version": "1.0.0"
    }
],

Your full pluginDefinition.json should now be:

{
  "identifier": "org.openmainframe.zowe.workshop-user-browser",
  "apiVersion": "1.0.0",
  "pluginVersion": "0.0.1",
  "pluginType": "application",
  "dataServices": [
    {
      "type": "router",
      "name": "table",
      "serviceLookupMethod": "external",
      "fileName": "tablehandler.js",
      "routerFactory": "tableRouter",
      "dependenciesIncluded": true,
      "version": "1.0.0"
    }
  ],
  "webContent": {
    "framework": "angular2",
    "launchDefinition": {
      "pluginShortNameKey": "userBrowser",
      "pluginShortNameDefault": "User Browser",
      "imageSrc": "assets/icon.png"
    },
    "descriptionKey": "userBrowserDescription",
    "descriptionDefault": "Browse Employees in System",
    "isSingleWindowApp": true,
    "defaultWindowStyle": {
      "width": 1300,
      "height": 500
    }
  }
}

The dataservice you have specified here has a few interesting attributes. First, it is listed as type: router because there are different types of dataservices that can be made to suit the need. Second, the name is table, which determines both the name seen in logs as well as the URL where this can be accessed. Finally, fileName and routerFactory point to the file within workshop-user-browser-app/lib where the code can be invoked, and the function that returns the ExpressJS router, respectively.

5. Restart the server (as was done when adding the app initially) to load this new dataservice. This is not always needed but it’s done here for educational purposes.

6. Access https://host:port/ZLUX/plugins/org.openmainframe.zowe.workshop-user-browser/services/table/1.0.0 to see the dataservice in action. It should return all the rows in the user table, as you did a GET to the root / URL that you just coded.

3. Add your first widget

Now that you can get this data from the server’s new REST API, you need to make improvements to the web content of the app to visualize this. This means not only calling this API from the app, but presenting it in a way that is easy to read and extract information.

Add your dataservice to the app

Let’s make some edits to userbrowser-component.ts, replacing the UserBrowserComponent class’s ngOnInit method with a call to get the user table, and defining ngAfterViewInit:

  ngOnInit(): void {
    this.resultNotReady = true;
    this.log.info(`Calling own dataservice to get user listing for filter=${JSON.stringify(this.filter)}`);
    let uri = this.filter ? ZoweZLUX.uriBroker.pluginRESTUri(this.pluginDefinition.getBasePlugin(), 'table', `${this.filter.type}/${this.filter.value}`) : ZoweZLUX.uriBroker.pluginRESTUri(this.pluginDefinition.getBasePlugin(), 'table',null);
    setTimeout(()=> {
    this.log.info(`Sending GET request to ${uri}`);
    this.http.get(uri).map(res=>res.json()).subscribe(
      data=>{
        this.log.info(`Successful GET, data=${JSON.stringify(data)}`);
        this.columnMetaData = data.metadata;
        this.unfilteredRows = data.rows.map(x=>Object.assign({},x));
        this.rows = this.unfilteredRows;
        this.showGrid = true;
        this.resultNotReady = false;
      },
      error=>{
        this.log.warn(`Error from GET. error=${error}`);
        this.error_msg = error;
        this.resultNotReady = false;
      }
    );
    },100);
  }

  ngAfterViewInit(): void {
    // the flex table div is not on the dom at this point
    // have to calculate the height for the table by subtracting all
    // the height of all fixed items from their container
    let fixedElems = this.element.nativeElement.querySelectorAll('div.include-in-calculation');
    let height = 0;
    fixedElems.forEach(function (elem, i) {
      height += elem.clientHeight;
    });
    this.windowEvents.resized.subscribe(() => {
      if (this.grid) {
        this.grid.updateRowsPerPage();
      }
    });
  }

You may have noticed that you’re referring to several instance variables that you haven’t declared yet. Let’s add those within the UserBrowserComponent class, too, above the constructor.

  private showGrid: boolean = false;
  private columnMetaData: any = null;
  private unfilteredRows: any = null;
  private rows: any = null;
  private selectedRows: any[];
  private query: string;
  private error_msg: any;
  private url: string;
  private filter:any;

Hopefully, you are still running the command in the first command prompt, npm run start, which will rebuild your web content for the app whenever you make changes. You may see some errors, which you will clear up by adding the next portion of the app.

Introducing ZLUX Grid

When ngOnInit runs, it will call out to the REST dataservice and put the table row results into your cache, but you haven’t yet visualized this in any way. You need to improve your HTML a bit to do that, and rather than reinvent the wheel, you luckily have a table visualization library that you can rely on — ZLUX Grid.

If you inspect package.json in the webClient folder, you’ll see that you’ve already included @zlux/grid as a dependency — as a link to one of the Zowe GitHub repositories, so it should have been pulled into the node_modules folder during the npm install operation. Now you just need to include it in the Angular code to make use of it. This comes in two steps:

1. Edit webClient/src/app/userbrowser.module.ts, adding import statements for the zLUX widgets above and within the @NgModule statement:

import { ZluxGridModule } from '@zlux/grid';
import { ZluxPopupWindowModule, ZluxButtonModule } from '@zlux/widgets'
//...
@NgModule({
imports: [FormsModule, HttpModule, ReactiveFormsModule, CommonModule, ZluxGridModule, ZluxPopupWindowModule, ZluxButtonModule],
//...

The full file should now be:

*
  This Angular module definition will pull all of your Angular files together to form a coherent app
*/

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { HttpModule } from '@angular/http';
import { ZluxGridModule } from '@zlux/grid';
import { ZluxPopupWindowModule, ZluxButtonModule } from '@zlux/widgets'

import { UserBrowserComponent } from './userbrowser-component';

@NgModule({
  imports: [FormsModule, HttpModule, ReactiveFormsModule, CommonModule, ZluxGridModule, ZluxPopupWindowModule, ZluxButtonModule],
  declarations: [UserBrowserComponent],
  exports: [UserBrowserComponent],
  entryComponents: [UserBrowserComponent]
})
export class UserBrowserModule { }

2. Edit userbrowser-component.html within the same folder. Previously, it was just meant for presenting a “Hello World” message, so you should add some style to accommodate the zlux-grid element that you will also add to this template via a tag.

<!-- In this HTML file, an Angular template should be placed that will work together with your Angular component to make a dynamic, modern UI. -->

<div class="parent col-11" id="userbrowserPluginUI">
  <div class="fixed-height-child include-in-calculation">
      <button type="button" class="wide-button btn btn-default" value="Send">
        Submit Selected Users
      </button>
  </div>
  <div class="fixed-height-child height-40" *ngIf="!showGrid && !viewConfig">
    <div class="">
      <p class="alert-danger">{{error_msg}}</p>
    </div>
  </div>
  <div class="container variable-height-child" *ngIf="showGrid">
    <zlux-grid [columns]="columnMetaData | zluxTableMetadataToColumns"
    [rows]="rows"
    [paginator]="true"
    selectionMode="multiple"
    selectionWay="checkbox"
    [scrollableHorizontal]="true"
    (selectionChange)="onTableSelectionChange($event)"
    #grid></zlux-grid>
  </div>
  <div class="fixed-height-child include-in-calculation" style="height: 20px; order: 3"></div>
</div>

<div class="userbrowser-spinner-position">
  <i class="fa fa-spinner fa-spin fa-3x" *ngIf="resultNotReady"></i>
</div>

Note the key functions of this template:

  • There’s a button that when clicked will submit selected users (from the grid). You will implement this ability later.
  • You can show or hide the grid based on a variable ngIf="showGrid" so that you can wait to show the grid until there is data to present.
  • The zlux-grid tag pulls the ZLUX Grid widget into your app, and it has many variables that can be set for visualization, as well as functions and modes.
    • You can allow the columns, rows, and metadata to be set dynamically by using the square bracket [ ] template syntax, and allow your code to be informed when the user selection of rows changes via (selectionChange)="onTableSelectionChange($event)".

3. Make a small modification to userbrowser-component.ts to add the grid variable, and set up the aforementioned table selection event listener, both within the UserBrowserComponent class:

@ViewChild('grid') grid; //above the constructor

onTableSelectionChange(rows: any[]):void{
    this.selectedRows = rows;
}

The previous section, Build your first dataservice, set the variables that are fed into the ZLUX Grid widget, so at this point the app should be updated with the ability to present a list of users in a grid.

If you are still running npm run start in a command prompt, it should now show that the app has been successfully built, and that means you are ready to see the results. Reload your browser’s webpage and open the user browser app once more. Do you see the list of users in columns and rows that can be sorted and selected? If so, great — you’ve built a simple yet useful app within Zowe! Let’s move on to the last portion of the app tutorial where you’ll hook the starter app and the user browser app together to accomplish a task.

4. Add Zowe app-to-app communication

Apps in Zowe can be useful and provide insight all by themselves, but a big part of using the Zowe Desktop is that apps are able to keep track of and share context through user interaction. This is done in order to accomplish a complex task by simple and intuitive means by having the foreground app request an app that’s best suited for a specific task; the app accomplishes that task with some context as to the data and purpose.

In this tutorial, you’re trying to not just find a list of employees in a company (which was accomplished in the last step where the grid was added and populated with the REST API), but to also filter that list find those employees who are best suited to the task you need done. So, your user browser app needs to be enhanced with two new abilities:

  • Filter the user list to show only those users that meet the filter
  • Send the subset of users selected in the list back to the app that requested a user list

How can you accomplish either of these tasks? App-to-app communication! Apps can communicate with other apps in a few ways, but can be categorized into two interaction groups:

  1. Launch an app with a context of what it should do
  2. Message an app that’s already open to send a request or an alert

In either case, the app framework provides actions as the objects to perform the communication. Actions not only define what form of communication should happen, but between which apps. Actions are issued from one app, and are fulfilled by a target app. But because there may be more than one instance/window of an app open, there are target modes:

  • Open a new app window, where the message context is delivered in the form of a launch context
  • Message a particular instance, or any of the currently open instances, of the target app

Add the starter app

In order to facilitate app-to-app communication, you need another app to communicate with. A starter app is provided which can be found on GitHub.

As you did previously in the Add your app to the desktop section, you need to move the app files to a location where they can be included in your zlux-app-server. You then need to add to the plugins folder in the example server and redeploy.

1. Clone or download the starter app under the zlux folder:

  • git clone https://github.com/zowe/workshop-starter-app

2. Navigate to the starter app and build it as before:

  • Install packages with cd webClient and then npm install.
  • Build the project using npm run start.

3. Next, add the workshop-starter-app plugin to the zlux-app-server:

Make sure the ./appServer is stopped before running cd ~/my-zowe/zlux-app-server/bin ./install-app.sh /path-to-plugin/workshop-starter-app

4. Restart the ./appServer under zlux-app-server/bin with the appropriate parameters passed in.

5. Refresh the browser and verify that the app with a green S is present in zLUX.

Enable communication

You’ve already done the work of setting up the app’s HTML and Angular definitions, so in order to make your app compatible with app-to-app communication, it only needs to listen for, act upon, and issue Zowe app actions. Let’s make edits to the TypeScript component to do that. Edit the UserBrowserComponent class’s constructor within userbrowser-component.ts in order to listen for the launch context:

  constructor(
    private element: ElementRef,
    private http: Http,
    @Inject(Angular2InjectionTokens.LOGGER) private log: ZLUX.ComponentLogger,
    @Inject(Angular2InjectionTokens.PLUGIN_DEFINITION) private pluginDefinition: ZLUX.ContainerPluginDefinition,
    @Inject(Angular2InjectionTokens.WINDOW_ACTIONS) private windowAction: Angular2PluginWindowActions,
    @Inject(Angular2InjectionTokens.WINDOW_EVENTS) private windowEvents: Angular2PluginWindowEvents,
    //Now, if this is not null, you're provided with some context of what to do on launch.
    @Inject(Angular2InjectionTokens.LAUNCH_METADATA) private launchMetadata: any,
  ) {
    this.log.info(`User Browser constructor called`);

    //NOW: if provided with some startup context, act upon it... otherwise just load all.
    //Step: after making the grid... you add this to show that you can instruct an app to narrow its scope on open
    this.log.info(`Launch metadata provided=${JSON.stringify(launchMetadata)}`);
    if (launchMetadata != null && launchMetadata.data) {
    /* The message will always be an object, but format can be specific. The format you are using here is in the starter app:
      https://github.com/zowe/workshop-starter-app/blob/master/webClient/src/app/workshopstarter-component.ts#L177
    */
      switch (launchMetadata.data.type) {
      case 'load':
        if (launchMetadata.data.filter) {
          this.filter = launchMetadata.data.filter;
        }
        break;
      default:
        this.log.warn(`Unknown launchMetadata type`);
      }
    } else {
      this.log.info(`Skipping launching in a context due to missing or malformed launchMetadata object`);
    }
}

Then, add a new method on the class ‘provideZLUXDispatcherCallbacks’, which is a web-framework-independent way to allow the Zowe apps to register for event listening of actions.

  /*
 You might expect to see a JSON here, but the format can be specific depending on the action - see the starter app to see the format that is sent for the workshop:
  https://github.com/zowe/workshop-starter-app/blob/master/webClient/src/app/workshopstarter-component.ts#L225
  */
  zluxOnMessage(eventContext: any): Promise<any> {
    return new Promise((resolve,reject)=> {
      if (!eventContext || !eventContext.data) {
        return reject('Event context missing or malformed');
      }
      switch (eventContext.data.type) {
      case 'filter':
        let filterParms = eventContext.data.parameters;
        this.log.info(`Messaged to filter table by column=${filterParms.column}, value=${filterParms.value}`);

        for (let i = 0; i < this.columnMetaData.columnMetaData.length; i++) {
          if (this.columnMetaData.columnMetaData[i].columnIdentifier == filterParms.column) {
            //ensure it is a valid column
            this.rows = this.unfilteredRows.filter((row)=> {
              if (row[filterParms.column]===filterParms.value) {
                return true;
              } else {
                return false;
              }
            });
            break;
          }
        }
        resolve();
        break;
      default:
        reject('Event context missing or unknown data.type');
      };
    });
  }


  provideZLUXDispatcherCallbacks(): ZLUX.ApplicationCallbacks {
    return {
      onMessage: (eventContext: any): Promise<any> => {
        return this.zluxOnMessage(eventContext);
      }
    }
}

At this point, the app should build successfully. Upon reloading the Zowe page in your browser, you should see that if you open the starter app (the app with the green S) and click the Find Users from Lookup Directory button, it should open up the user browser app with a smaller, filtered list of employees rather than the unfiltered list you see when you open the app manually.

You can also see that once this app has been opened, the starter app’s button, Filter Results to Those Nearby, becomes enabled and you can click on it to see the open User Browser app’s listing become filtered even more — this time using the browser’s Geolocation API to instruct the user browser app to filter to those employees who are closest to you.

Call back to the starter app

You’re almost finished now! The app can visualize data from a REST API, and can be instructed by other apps to filter that data according to the situation. However, in order to complete this tutorial you need the app communication to go in the other direction: Inform the starter app which employees you have chosen in the table.

This time, you will edit provideZLUXDispatcherCallbacks to issue actions rather than listen for them. You need to target the starter app, since it is the app that expects to receive a message about which employees should be assigned a task. If that app is given an employee listing that contains employees with the wrong job titles, the operation will be rejected as invalid, so you can ensure that you get the right result through a combination of filtering and sending a subset of the filtered users back to the starter app.

Add a private instance variable to the UserBrowserComponent class:

 private submitSelectionAction: ZLUX.Action;

Then, create the Action template within the constructor:

this.submitSelectionAction = ZoweZLUX.dispatcher.makeAction(
  'org.openmainframe.zowe.workshop-user-browser.actions.submitselections',
  'Sorts user table in app that has it',
  ZoweZLUX.dispatcher.constants.ActionTargetMode.PluginFindAnyOrCreate,
  ZoweZLUX.dispatcher.constants.ActionType.Message,
  'org.openmainframe.zowe.workshop-starter',
  { data: { op: 'deref', source: 'event', path: ['data'] } }
)

So you’ve made an action that targets an open window of the starter app and provides it with an object with a data attribute. You can now populate this object for the message to send to the app by getting the results from ZLUX Grid (this.selectedRows will be populated from this.onTableSelectionChange).

For the final change to this file, add a new method to the class:

  submitSelectedUsers() {
    let plugin = ZoweZLUX.PluginManager.getPlugin("org.openmainframe.zowe.workshop-starter");
    if (!plugin) {
      this.log.warn(`Cannot request Workshop Starter App... It was not in the current environment!`);
      return;
    }

    ZoweZLUX.dispatcher.invokeAction(this.submitSelectionAction,
      {'data':{
         'type':'loadusers',
         'value':this.selectedRows
      }}
    );
}

And then invoke this via a button click action, which you can add into the Angular template, userbrowser-component.html, by changing the button tag for Submit Selected Users to:

<button type="button" class="wide-button btn btn-default" (click)="submitSelectedUsers()" value="Send">

Check that the app builds successfully. If it does, you have now built the app for the tutorial! Try it out:

  1. Open the starter app.
  2. Click the Find Users from Lookup Directory button.
    • You should see a filtered list of users in your user app.
  3. Click the Filter Results to Those Nearby button on the starter app.
    • You should now see the list filtered further to include only one geography.
  4. Select some users to send back to the starter app.
  5. Click the Submit Selected Users button on the user browser app.
    • The starter app should print a confirmation message indicating success.

Summary

And that’s it! If you look back to the beginning of this tutorial, you should notice that we’ve covered all aspects of building the app — REST APIs, persistent settings storage, creating Angular apps, and using widgets within them — as well as having one app communicate with another. Hopefully, you have learned a lot about app building from this experience, but if you have questions or want to learn more, please visit https://zowe.github.io/docs-site/ for more details!

References

Zowe.org

Zowe documentation

Background on the Zowe open source project

Zowe trial registration

Sean Grady
Nakul Manchanda