1. 程式人生 > >Getting started with the Zowe WebUi

Getting started with the Zowe WebUi

Learning objectives

This tutorial walks you through the process of adding new apps to the Zowe WebUi, and teaches you how to communicate with other parts of Zowe. By the end of this tutorial, you will:

  1. Know how to create an app that shows up on the Desktop
  2. Know how to create a dataservice that implements a simple REST API
  3. Be introduced to Typescript programming
  4. Be introduced to simple Angular web development
  5. Have experience in working with the Zowe app framework
  6. Become familiar with one of the Zowe app widgets, the grid widget

Prerequisites

Before following the tutorial, please navigate to https://www-01.ibm.com/events/wwe/ast/mtm/zowe.nsf/enrollall?openform

and register for a username and password to access the mainframe. You will need this to login to your system and complete the tutorial.

Users of Zowe can interact with the zLux WebUi by pointing their browser at their mainframe system. This UI will include all the apps installed on the Zowe mainframe system currently. While it is possible to develop new apps and deploy them on your mainframe directly, it is often easier to create a local instance of the WebUi (known as zLUX) on your desktop. From here, you can play with the example-server, make changes to apps already installed, and create our own applications.

Estimated time

Completing this tutorial should take about 45 minutes.

Stand up a local version of the zLUX example server

1. Acquire the source code

To get started, first clone or download the github capstone repository, https://github.com/zowe/zlux. Make sure that you have your SSH key set up and added to github.

git clone --recursive [email protected]:zowe/zlux.git
cd zlux
git submodule foreach "git checkout master"
cd zlux-build

At this point, you’ll have the latest code from each repository on your system. Continue from within the zlux-example-server folder.

2. Build zLUX apps

Note when building, NPM is used. The version of NPM needed for the build to succeed should be at least 5.4. You can update NPM by executing npm install -g npm.

zLUX apps can contain server and/or web components. The web components must be built, as webpack is involved in optimized packaging and server components are also likely to need building if they require external dependencies from NPM, use native code, or are written in typescript.

Under zlux-build run:

//Windows
build.bat

//Otherwise
build.sh

This will take some time to complete.

Note: You will need to have ant and ant-contrib installed.

3. Deploy server configuration files

Since you are running the zLUX Proxy Server separate from ZSS, you must ensure that the ZSS installation has its configuration deployed. You can accomplish this by navigating to zlux-build and running:

ant deploy

4. Run the server

At this point, all server files have been configured and apps built, so ZSS and the app server are ready to run.

From the system with the zLUX Proxy Server, start it with a few parameters to hook it to ZSS. Make sure to replace “8542” with your zss port:

cd ../zlux-example-server/bin

// Windows:
`nodeServer.bat -h 192.86.32.67 -P 8542 -p 5000`

// Others:
`nodeServer.sh -h 192.86.32.67 -P 8542 -p 5000`

Valid parameters for nodeServer are as follows:

  • -h: Specifies the hostname where ZSS can be found. Use as -h \<hostname\>.
  • -P: Specifies the port where ZSS can be found. Use as -P \<port\>. This overrides zssPort from the configuration file.
  • -p: Specifies the HTTP port to be used by the zLUX Proxy Server. Use as -p <port>. This overrides node.http.port from the configuration file.
  • -s: Specifies the HTTPS port to be used by the zLUX Proxy Server. Use as -s <port>. This overrides node.https.port from the configuration file.
  • –noChild: If specified, tells the server to ignore and skip spawning of child processes defined as node.childProcesses in the configuration file.

When the zLUX Proxy Server has started, one of the last messages you will see as bootstrapping completes is that the server is listening on the HTTP/s port. At this time, you should be able to use the server.

5. Connect in a browser

Next, navigate to the zLUX Proxy Server which can be found at:

http://localhost:5000/ZLUX/plugins/com.rs.mvd/web/

Once there, you should be greeted with a login screen and a few example apps in the taskbar at the bottom of the screen. You can login into the mainframe with the credentials:

  • Username: Refer to prereq
  • Password: Refer to prereq

Create a user database browser app on zLUX

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

The rest of the 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.

Constructing 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 on this wiki if you need.

Defining 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 again 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

Constructing 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 3 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 {}
}

Packaging Your web app

At this time, you’ve made 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 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 [email protected], 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.git",
    "@zlux/widgets": "git+https://github.com/zowe/zlux-widgets.git",
    "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 first 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-script 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.

Adding your app to the desktop

At this point, your workshop-user-browser-app folder contains files for an app that could be added to a Zowe instance. You’ll add this to your own Zowe instance. First, ensure that the Zowe app server is not running. Then, navigate to the instance’s root folder, /zlux-example-server.

Within, you’ll see a folder, plugins. Take a look at one of the files within the folder. You can see that these are JSON files with the attributes identifier and pluginLocation. These files are what we call Plugin Locators, since they point to a plugin to be included into the server.

Now you can make one yourself. Make a file, /zlux-example-server/plugins/org.openmainframe.zowe.workshop-user-browser.json, with these contents:

{
  "identifier": "org.openmainframe.zowe.workshop-user-browser",
  "pluginLocation": "../../workshop-user-browser-app"
}

When the server runs, it will check for these sorts of files in pluginsDir, a location known to the server via its specification in the server configuration file. In your case, this is /zlux-example-server/deploy/instance/ZLUX/plugins/.

You could place the JSON directly into that location, but the recommended way to place content into the deploy area is via running the server deployment process. Simply:

  1. Open up a (second) command prompt to zlux-build
  2. ant deploy

Now you’re ready to run the server and see your app:

  1. cd /zlux-example-server/bin
  2. ./nodeServer.sh
  3. Open your browser to https://hostname:port
  4. Login with your credentials
  5. 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.

Building 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.

  1. Create a file, 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.

Working 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.

Adding 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-script 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
    }
],

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
    }
  ],
  "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 determines both the name seen in logs as well as the URL this can be accessed at. 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/ 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.

Adding 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.

Adding 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 ? RocketMVD.uriBroker.pluginRESTUri(this.pluginDefinition.getBasePlugin(), 'table', `${this.filter.type}/${this.filter.value}`) : RocketMVD.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-script 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, Adding your dataservice to the app, 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-script 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.

Adding 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 task to accomplish 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 (as 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 do either task? App-to-app communication! Apps can communicate with other apps in a few ways, but can be categorized into two interaction groups:

  1. Launching an app with a context of what it should do
  2. Messaging 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, or any of the currently open instances of the target app

Adding 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 Adding Your app to the desktop section, you need to move the app files to a location where they can be included in your zlux-example-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.git

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

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

3. Next, navigate to the zlux-example-server:

  • create a new file under /zlux-example-server/plugins/org.openmainframe.zowe.workshop-starter.json.
  • Edit the file to contain:
{
  "identifier": "org.openmainframe.zowe.workshop-starter",
  "pluginLocation": "../../workshop-starter-app"
}

4. Make sure the ./nodeServer is stopped before running ant deploy under zlux-build.

5. Restart the ./nodeServer under zlux-example-server/bin with the appropriate parameters passed in.

6. Refresh the browser and verify that the app with a Green S is present in zLUX.

Enabling 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 that 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!

Calling 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 to 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 = RocketMVD.dispatcher.makeAction(
  'org.openmainframe.zowe.workshop-user-browser.actions.submitselections',
  'Sorts user table in app that has it',
  RocketMVD.dispatcher.constants.ActionTargetMode.PluginFindAnyOrCreate,
  RocketMVD.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 = RocketMVD.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;
    }

    RocketMVD.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">