Interact with z/OS using a mobile device with Zowe and Flutter

This tutorial shows you how to build a Flutter application for iOS and Android that can interact with IBM Z mainframe systems via Zowe. These interactions include: listing data sets, editing data set content, deleting data sets, submitting data sets as jobs, displaying PDS members, listing jobs, and displaying job output files.

TSO, ISPF, and z/OS Unix are the interfaces that have traditionally been used to interact with z/OS. These tools are very strong and have been used for decades consistently by IBM mainframe users, however they can be challenging for new users — especially users with GUI backgrounds. Newer users are far more accustomed to graphical interfaces than to the combination of green text and black screen. In this tutorial, you will learn how to create a modern mobile app that can control a mainframe. In the process, you will use:

  • The Flutter framework to create an app that works on mobile
  • Material design widgets to create a modern-looking app
  • Provider to manage your data between states
  • Zowe as a gateway to the mainframe

IBM Z systems are among IBM’s most popular offerings — solid proof that mainframes continue to play a key role in business technology today.

Zowe is an open source project created to host technologies that benefit the IBM Z platform, and its participants include IBM Z community members from around the world.

Zowe provides:

  • A web interface that is very similar to the desktop GUIs of popular operating systems
  • A command-line interface (CLI) that allows developers to interact with a mainframe from the comfort of their local PC command prompt
  • An API mediation layer that provides a single point of access for mainframe service REST APIs

Flutter is a fairly new technology created by Google. It is used for building mobile, web, and desktop applications that can be compiled natively with a single codebase. It uses the Dart programming language.

Prerequisites

  • A basic knowledge of software development
  • Access to Zowe
  • Dart and Flutter

To complete this tutorial, you will need to have access to Zowe. If you don’t have access, you can get an account on the tutorial system by following this guide by Jessielaine Punongbayan, published on the Zowe channel on Medium.

To get started with Dart and Flutter installation, check out the official guide.

Estimated time

You should be able to finish this tutorial in about 60 – 90 minutes, depending on your familiarity with app development.

Steps

Here are the steps required to complete this tutorial. All of the code provided below can also be found here.

  1. Creating a new Flutter project
  2. Adding dependencies
  3. Project structure
  4. Discovering REST APIs and defining base URLs
  5. Creating models
  6. Authentication
  7. Routing
  8. Using data sets
  9. Completing the code
  10. Running the app
1

Creating a new Flutter project

Let’s get started! First, you need to start a new project using Flutter. To do this, open your terminal, navigate to your project directory, and execute flutter create <project_name>. This will create a new Flutter project for you.

flutter create zowe_flutter

If you have any problems with this step, it is most likely because you don’t have Flutter registered in your environment variables. You can check the installation guide for this if you haven’t already done so.

2

Adding dependencies

In Flutter, you install packages by adding them as dependencies. These dependencies are stored in the pubspec.yaml file.

The main packages that you will use are:

  • http — to make http requests to Zowe REST API
  • provider — to manage your states
  • dynamic_tabs — to have a routing with tabs on the bottom of the screen

To install them, open pubspec.yaml and update the dependencies segment as follows:

dependencies:
  flutter:
    sdk: flutter
  http: ^0.12.0
  provider: ^3.1.0+1
  dynamic_tabs: ^1.0.2+3
  cupertino_icons: ^0.1.2

Every time you make changes to the dependencies, you must let Flutter know that it needs to check the file and install any new packages that have been added to the file. You can do this by running flutter pub get on your terminal.

3

Project structure

To keep your code file structure clean, you should keep your files in separate directories.

Figure 1. File directories

File directories

You will create the following directories:

  • models — stores your model files; models are Dart class representations of the results from the Zowe API
  • providers — stores provider files, which are used to manage your states in screens
  • screens — stores your screen files, which are the main visual components of your app
  • services — stores service files, which can be used by any component
  • widgets — stores widgets, which are small general-use components

In addition, you will create enums.dart to store enumerations such as file allocation units or data set organizations, since they are predefined options. Also, router.dart links paths to your screens.

4

Discovering REST APIs and defining base URLs

One of the components of Zowe is a catalog of REST APIs, which is the main focus of this tutorial. By using these REST APIs, you can work with some z/OS services such as data sets and jobs.

There is an API Catalog in the Zowe application framework where REST APIs can be discovered. If you are using the Zowe tutorial system, you can access the Zowe Web Desktop app at https://<your_ip_address>/ZLUX/plugins/org.zowe.zlux.bootstrap/web/ and authenticate yourself with your tutorial system credentials. After that, just like Windows, you can start the API Catalog from the “start” menu on the left.

Figure 2. Accessing the API Catalog

Accessing the API Catalog

By browsing through the API Catalog, you can see details about specific API services — for example, the IBM z/OS Datasets API is shown in Figure 3. You will check and define these endpoints in your app in order to interact with z/OS.

Figure 3. IBM z/OS Datasets API details

IBM z/OS Datasets API details

Each API has different URLs, so you need to define them in your program. Let’s create a file called api.dart in the services directory.

import 'dart:io';
import 'package:http/io_client.dart';

class ApiService {
  static const AUTH_ENDPOINT = 'https://192.86.32.67:8544/auth';
  static const DATA_SET_ENDPOINT = 'https://192.86.32.67:8547/api/v1/datasets';
  static const JOB_ENDPOINT = 'https://192.86.32.67:7554/api/v1/jobs';

  static HttpClient httpClient = new HttpClient()
    ..badCertificateCallback =
        ((X509Certificate cert, String host, int port) => true);
  static IOClient ioClient = new IOClient(httpClient);
}

By defining these endpoints, you will be able to use them as ApiService.AUTH_ENDPOINT in your providers later on.

5

Creating models

REST APIs from Zowe return responses in JSON format. In order to parse them and remove repetition, you can create models for the APIs you use. Let’s continue with the Data Sets API from the previous section. Below where you can see the base URL, there is a link named External documentation — you can click on it or go directly to https://<your_ip_address>/swagger-ui.html#/Data_Sets_APIs to see the REST API details.

Now with this Swagger UI, things should be pretty straightforward. You can now see what request you can make, what response you can expect, or what status codes are defined. You can expand each API to see more of these details. It’s beautiful, isn’t it?

Figure 4. Data Sets APIs

Data Sets APIs

Now, all you have to do is create new model classes that match the responses you will receive. Create a directory called models under the lib directory if you haven’t already done so. Remember, model files are classes that match the JSON responses from the REST APIs, and nothing more complex than that!

Here is your model for the data sets. The upper section contains the data set fields and bottom section is the constructor.

class DataSet {
  int allocatedSize;
  String allocationUnit;
  int averageBlock;
  int blockSize;
  String catalogName;
  String creationDate;
  String dataSetOrganization;
  String deviceType;
  int directoryBlocks;
  String expirationDate;
  bool migrated;
  String name;
  int primary;
  String recordFormat;
  int recordLength;
  int secondary;
  int used;
  String volumeSerial;

  DataSet({
    this.allocatedSize,
    this.allocationUnit,
    this.averageBlock,
    this.blockSize,
    this.catalogName,
    this.creationDate,
    this.dataSetOrganization,
    this.deviceType,
    this.directoryBlocks,
    this.expirationDate,
    this.migrated,
    this.name,
    this.primary,
    this.recordFormat,
    this.recordLength,
    this.secondary,
    this.used,
    this.volumeSerial,
  });
}

This constructor can seem a bit odd if you’re seeing Dart code for the first time. In generic languages, the constructor is a function that takes class fields as arguments, and in the constructor body these arguments are matched with fields. This is possible in Dart as well, but as in our example, you have a larger number of fields. If you put them as positional arguments, it can be very difficult to remember the order, so giving named arguments can make the job easier. If, for example, your constructor is MyClass(int a), then you can instantiate your class as MyClass(1). If your constructor is MyClass({int a}), then you need to instantiate your class as MyClass(a: 1). You have to explicity name your arguments, and by adding this keyword as MyClass({this.a}) you don’t need to write a body for your constructor because it will match value a with your field value a in the class, and you will still instantiate as MyClass(a:1).

You can also implement to functions toJson and fromJson. toJson converts a data set instance to JSON format, and fromJson creates a data set instance from JSON, which you use to create your model instances directly from the responses.

factory DataSet.fromJson(Map<String, dynamic> json) => DataSet(
        allocatedSize: json["allocatedSize"],
        allocationUnit: json["allocationUnit"],
        averageBlock: json["averageBlock"],
        blockSize: json["blockSize"],
        catalogName: json["catalogName"],
        creationDate: json["creationDate"],
        dataSetOrganization: json["dataSetOrganization"],
        deviceType: json["deviceType"],
        directoryBlocks: json["directoryBlocks"],
        expirationDate: json["expirationDate"],
        migrated: json["migrated"],
        name: json["name"],
        primary: json["primary"],
        recordFormat: json["recordFormat"],
        recordLength: json["recordLength"],
        secondary: json["secondary"],
        used: json["used"],
        volumeSerial: json["volumeSerial"],
      );

  Map<String, dynamic> toJson() => {
        "allocatedSize": allocatedSize,
        "allocationUnit": allocationUnit,
        "averageBlock": averageBlock,
        "blockSize": blockSize,
        "catalogName": catalogName,
        "creationDate": creationDate,
        "dataSetOrganization": dataSetOrganization,
        "deviceType": deviceType,
        "directoryBlocks": directoryBlocks,
        "expirationDate": expirationDate,
        "migrated": migrated,
        "name": name,
        "primary": primary,
        "recordFormat": recordFormat,
        "recordLength": recordLength,
        "secondary": secondary,
        "used": used,
        "volumeSerial": volumeSerial,
      };

Next, create a User model that you will use in authentication:

class User {
  String userId;
  String token;

  User({this.userId, this.token});

  User.initial()
      : userId = '',
        token = '';
}

In order to keep this tutorial from running long, we will not explicitly write any other models. You can try to create them yourself or get them from the repo.

6

Authentication

If you haven’t already done so, create directories for providers, screens, and widgets, as well as a file called router.dart under lib. Then create auth.dart under providers and login_screen.dart under screens.

Before proceeding, let’s talk about Provider.

In Flutter, literally everything is a widget. And this app is basically a tree of widgets with MyApp from main.dart sitting at the root. When you talk about a tree, you think about parents and children. So if you’re wondering How do I share data from parents to children? — this is exactly the issue Provider aims to address. Providers can be created in any widgets to store data and emit data change events. And any widget that is directly or indirectly below that widget can listen for these changes.

Figure 5 illustrates a portion of the app’s widget tree. Pay close attention to AuthProvider/HomePage, which is linked to, and above, both the DataSetList and JobList screens. This means you can access authentication details from these screens. You can get your token to make calls to see if you are authenticated or not. For more information, see Simple app state management.

Figure 5. App widget tree

App widget tree

You will use basic authentication for the REST API, meaning you add the encrypted value of your username:password combination. For more info about authentication, see Zowe Authentication & Authorization API.

In your AuthProvider, you will add the following code to the file:

import 'dart:convert';
import 'package:flutter/widgets.dart';
import 'package:zowe_flutter/models/user.dart';
import 'package:zowe_flutter/services/api.dart';

enum AuthStatus { Unauthenticated, Authenticating, Authenticated }

class AuthProvider with ChangeNotifier {
  AuthStatus _status = AuthStatus.Unauthenticated;
  User _user = User.initial();

  AuthProvider.instance();

  AuthStatus get status => _status;
  User get user => _user;

  Future<bool> login(String userId, String password) async {
    _status = AuthStatus.Authenticating;
    notifyListeners();

    // Data that will be sent
    var data = {'username': userId, 'password': password};

    // Tell the server we are sending a json body
    Map<String, String> headers = {
      'Content-Type': 'application/json',
    };

    // Send credentials as JSON and asnyc wait for the response.
    var response = await ApiService.ioClient.post(ApiService.AUTH_ENDPOINT,
        headers: headers, body: jsonEncode(data));

    // Parse success field to determine if auth was successful.
    bool success = json.decode(response.body)['success'];
    if (!success) {
      _status = AuthStatus.Unauthenticated;
      notifyListeners();
      return false;
    }

    // Create auth token and create User object.
    String rawUser = userId + ':' + password;
    List<int> bytes = utf8.encode(rawUser);
    String base64User = base64.encode(bytes);

    _user = User(userId: userId, token: base64User);
    _status = AuthStatus.Authenticated;
    notifyListeners();
    return true;
  }

  bool logout() {
    // Clear user data
    _user = null;
    _status = AuthStatus.Unauthenticated;
    notifyListeners();
    return true;
  }
}

If you break down the class, it stores User instance and Authentication status. It is pretty self explanatory. This class also has a login function.

var response = await ApiService.ioClient.post(ApiService.AUTH_ENDPOINT,
        headers: headers, body: jsonEncode(data));

This will now benefit from your ApiService, using the endpoint that you defined there as well as the HttpClient you created there.

// Create auth token and create User object.
String rawUser = userId + ':' + password;
List<int> bytes = utf8.encode(rawUser);
String base64User = base64.encode(bytes);

 _user = User(userId: userId, token: base64User);
_status = AuthStatus.Authenticated;
notifyListeners();

The upper section here creates and stores the token that will be used for the other request to prove that you are actually authenticated to the server. The lower section updates the user object and status, and also notifies listeners. As you have seen with Provider, this notifyListeners function actually tells listeners about the change so they can rebuild themselves. Without this function, they would not know if there have been any changes in the state.

Now let’s create the LoginScreen to see how you can make better use of Provider.

import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:provider/provider.dart';

import 'package:zowe_flutter/providers/auth.dart';

class LoginScreen extends StatefulWidget {
  @override
  _LoginScreenState createState() => _LoginScreenState();
}

class _LoginScreenState extends State<LoginScreen> {
  TextStyle style = TextStyle(fontFamily: 'Montserrat', fontSize: 20.0);
  TextEditingController _id;
  TextEditingController _password;
  final _formKey = GlobalKey<FormState>();
  final _key = GlobalKey<ScaffoldState>();

  @override
  void initState() {
    super.initState();
    _id = TextEditingController(text: "");
    _password = TextEditingController(text: "");
  }

  @override
  Widget build(BuildContext context) {
    final auth = Provider.of<AuthProvider>(context);
    return Scaffold(
      key: _key,
      appBar: AppBar(
        title: Text("Zowe Flutter"),
      ),
      body: Form(
        key: _formKey,
        child: Center(
          child: ListView(
            shrinkWrap: true,
            children: <Widget>[
              Padding(
                padding: const EdgeInsets.all(16.0),
                child: Image(
                  image: AssetImage('zowe.png'),
                ),
              ),
              Padding(
                padding: const EdgeInsets.all(16.0),
                child: TextFormField(
                  controller: _id,
                  validator: (value) =>
                      (value.isEmpty) ? "Please enter your user ID!" : null,
                  style: style,
                  decoration: InputDecoration(
                      prefixIcon: Icon(Icons.person),
                      labelText: "User ID",
                      border: OutlineInputBorder()),
                ),
              ),
              Padding(
                padding: const EdgeInsets.all(16.0),
                child: TextFormField(
                  controller: _password,
                  validator: (value) =>
                      (value.isEmpty) ? "Please enter your password!" : null,
                  style: style,
                  decoration: InputDecoration(
                      prefixIcon: Icon(Icons.lock),
                      labelText: "Password",
                      border: OutlineInputBorder()),
                      obscureText: true,
                ),
              ),
              auth.status == AuthStatus.Authenticating
                  ? Center(child: CircularProgressIndicator())
                  : Padding(
                      padding: const EdgeInsets.symmetric(horizontal: 16.0),
                      child: Material(
                        elevation: 5.0,
                        borderRadius: BorderRadius.circular(30.0),
                        color: Colors.blue,
                        child: MaterialButton(
                          onPressed: () async {
                            if (_formKey.currentState.validate()) {
                              if (!await auth.login(_id.text, _password.text))
                                _key.currentState.showSnackBar(SnackBar(
                                  content: Text("Something is wrong"),
                                ));
                            }
                          },
                          child: Text(
                            "Sign In",
                            style: style.copyWith(
                                color: Colors.white,
                                fontWeight: FontWeight.bold),
                          ),
                        ),
                      ),
                    ),
              SizedBox(height: 20),
            ],
          ),
        ),
      ),
    );
  }
}

I will not go into detail about Widgets — if you want to learn about them, please see Introduction to widgets. Instead, I want to draw your attention to the line final auth = Provider.of<AuthProvider>(context);. Do you remember our widget tree above and how AuthProvider was above our LoginScreen? Well, this is how easy it is to access Provider from any widget. With this, you can access your login function or other auth data, and that is exactly what you do when you click the login button.

The login screen should look like this:

Figure 6. Login screen

Login screen shot

7

Routing

You can create routes with names in Flutter. Add the following code to router.dart, which you created earlier in this tutorial:

import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:zowe_flutter/screens/dashboard_screen.dart';
import 'package:zowe_flutter/screens/data_set_content_screen.dart';
import 'package:zowe_flutter/screens/data_set_create_screen.dart';
import 'package:zowe_flutter/screens/data_set_members_list_screen.dart';
import 'package:zowe_flutter/screens/login_screen.dart';

const String initialRoute = 'login';

class Router {
  static Map<String, WidgetBuilder> buildRoutes(BuildContext context) {
    return <String, WidgetBuilder>{
      'login': (BuildContext context) => LoginScreen(),
      'dashboard': (BuildContext context) => DashboardScreen(),
      'dataSetContent': (BuildContext context) => DataSetContentScreen(),
      'dataSetMembers': (BuildContext context) => DataSetMembersListScreen(),
      'dataSetCreate': (BuildContext context) => DataSetCreateScreen(),
    };
  }
}

If you have red underlined lines, don’t worry. You might not have all of the screens yet. You can simply comment them out. For now, you are only interested in the login and dashboard paths.

Now you need to let your main app know about the routes described, so let’s update main.dart:

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:zowe_flutter/providers/auth.dart';
import 'package:zowe_flutter/router.dart';
import 'package:zowe_flutter/screens/dashboard_screen.dart';
import 'package:zowe_flutter/screens/login_screen.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: HomePage(),
      routes: Router.buildRoutes(context),
    );
  }
}

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (_) => AuthProvider.instance(),
      child: Consumer(
        builder: (context, AuthProvider user, _) {
          switch (user.status) {
            case AuthStatus.Unauthenticated:
            case AuthStatus.Authenticating:
              return LoginScreen();
            case AuthStatus.Authenticated:
              return DashboardScreen();
          }
        },
      ),
    );
  }
}

As you can see, you can describe your routes on the MaterialApp widget by adding routes: Router.buildRoutes(context). Also, this code…

return ChangeNotifierProvider(
      create: (_) => AuthProvider.instance(),
      child: Consumer(
        builder: (context, AuthProvider user, _) {
            ....
       }
    )
);

…enables you to access AuthProvider in the section below. Again, if you recall the widget tree, this is how you attach a provider to a widget. And because of the provider, it is possible to know if the user is authenticated or not so that the user can be redirected to a dashboard or login screen.

You will use dashboard as your navigation screen, which means every other operation except login will be a child of this widget.

Using the dynamic tabs package, you can create routes with tabs as follows:

return DynamicTabScaffold.adaptive(
        routes: Router.buildRoutes(context),
        persistIndex: true,
        maxTabs: 2,
        tabs: <DynamicTab>[
          DynamicTab(
            child: DataSetListScreen(),
            tab: BottomNavigationBarItem(
              title: Text("List Data Sets"),
              icon: Icon(Icons.view_list),
            ),
            tag: "dataSetList", // Must Be Unique
          ),
          DynamicTab(
            child: DataSetCreateScreen(),
            tab: BottomNavigationBarItem(
              title: Text("Create Data Set"),
              icon: Icon(Icons.add_box),
            ),
            tag: "dataSetCreate", // Must Be Unique
          )
      ]
);

Here, you use the routes you created once again.

Figure 7. Navigation tabs at the bottom of the page

Navigation tabs at the bottom of the page

8

Using data sets

Now let’s create a widgets directory and then create alert_widget.dart and loading_widget.dart below that. These will be general-use widgets. You will show a loading widget when you are fetching data from an API, and an alert widget when, for example, an error occurs or the result is empty.

The loading widget will be static as follows:

import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';

class LoadingWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      child: Center(
        child: CircularProgressIndicator(),
      ),
    );
  }
}

However, you will add a few parameters to the alert widget, such as message, color, icon, and whether it should display a “Go back” button:

import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';

class AlertWidget extends StatelessWidget {
  final String message;
  final Color color;
  final IconData icon;
  final bool back;
  AlertWidget({this.message, this.color, this.icon, this.back});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Icon(
              icon,
              color: color,
              size: 64,
            ),
            Padding(
              child: Text(
                message,
                style: TextStyle(
                  fontSize: 36,
                  fontFamily: 'Montserrat',
                ),
                textAlign: TextAlign.center,
              ),
              padding: EdgeInsets.all(16),
            ),
            back ? Padding(
              child: RaisedButton(
                child: Text('Back'),
                onPressed: () => Navigator.pop(context),
              ),
              padding: EdgeInsets.all(16),
            ) : Container(),
          ],
        ),
      ),
    );
  } 
}

Let’s create enums.dart under lib, and define the Status and ActionStatus enums. Status will be the status of the main fetching operation, and ActionStatus will be the status of save, delete, or submit within a list.

enum Status { Loading, Success, Error, Empty }
enum ActionStatus { Idle, Working }

Next, create data_set_list_screen.dart under screens and data_set_list.dart under providers.

DataSetListProvider will store a list of data sets, a status, an action status, and a filter string. It will also have functions such as getDataSets, deleteDataSet, and few others as seen below:

import 'dart:convert';

import 'package:flutter/widgets.dart';
import 'package:http/http.dart';
import 'package:zowe_flutter/enums.dart';
import 'package:zowe_flutter/models/data_set.dart';
import 'package:zowe_flutter/models/response_status_message.dart';
import 'package:zowe_flutter/services/api.dart';

class DataSetListProvider with ChangeNotifier{
  Status _status = Status.Loading;
  ActionStatus _actionStatus = ActionStatus.Idle;
  List<DataSet> _dataSetList = [];
  String _filter = "";

  DataSetListProvider.initial({String filterString, String authToken}) {
    getDataSets(filterString: filterString, authToken: authToken);
  }

  Status get status => _status;
  ActionStatus get actionStatus => _actionStatus;
  List<DataSet> get dataSetList => _dataSetList;
  String get filter => _filter;

  /// Get list of data sets with filter.
  Future<ResponseStatusMessage> getDataSets({String filterString, String authToken}) async {
    _status = Status.Loading;
    _filter = filterString;
    notifyListeners();

    // if filter is empty don't bother with http request
    if (filterString == '') {
      _status = Status.Success;
      _dataSetList = [];
      notifyListeners();
      return ResponseStatusMessage(status: 'Success', message: 'Data set list fetched', error: false);
    }

    Map<String, String> headers = {
      'Content-Type': 'application/json',
      'Authorization': 'Basic ' + authToken,
    };

    final url = '${ApiService.DATA_SET_ENDPOINT}/$filterString';
    Response response = await ApiService.ioClient.get(url, headers: headers);
    var jsonBody = json.decode(response.body);

    if (response.statusCode >= 400) {
      _status = Status.Error;
      _dataSetList = [];
      notifyListeners();
      return ResponseStatusMessage.fromJson(jsonBody);
    }


    Iterable items = jsonBody['items'];
    List<DataSet> dataSets = items.map((item) => DataSet.fromJson(item)).toList();

    _status = Status.Success;
    _dataSetList = dataSets;

    if (dataSets.length == 0) {
      _status = Status.Empty;
    }
    notifyListeners();

    return ResponseStatusMessage(status: 'Success', message: 'Data set list fetched', error: false);
  }

  /// Delete data set
  Future<ResponseStatusMessage> deleteDataSet({String dataSetName, String authToken}) async {
    _actionStatus = ActionStatus.Working;
    notifyListeners();

    Map<String, String> headers = {
      'Content-Type': 'application/json',
      'Authorization': 'Basic ' + authToken,
    };

    final url = '${ApiService.DATA_SET_ENDPOINT}/$dataSetName';
    var response = await ApiService.ioClient.delete(url, headers: headers);

    _actionStatus = ActionStatus.Idle;
    if (response.statusCode >= 400) {
      var jsonBody = json.decode(response.body);  // it is empty if successful
      return ResponseStatusMessage.fromJson(jsonBody);
    }

    _dataSetList.removeWhere((ds) => ds.name == dataSetName);
    notifyListeners();
    return ResponseStatusMessage(status: 'Success', message: 'Data set deleted', error: false);
  }

  /// Submit data set as a job
  Future<ResponseStatusMessage> submitAsJob({String dataSetName, String authToken}) async {
    _actionStatus = ActionStatus.Working;
    notifyListeners();

    Map<String, String> headers = {
      'Content-Type': 'application/json',
      'Authorization': 'Basic ' + authToken,
    };

    Object requestBody = {
      'file': "'$dataSetName'"
    };
    String requestBodyJson = json.encode(requestBody);

    final url = '${ApiService.JOB_ENDPOINT}/dataset';
    var response = await ApiService.ioClient.post(url, headers: headers, body: requestBodyJson);
    _actionStatus = ActionStatus.Idle;

    // Error occured
    if (response.statusCode >= 400) {
      var jsonBody = json.decode(response.body);  // it is empty if successful
      return ResponseStatusMessage.fromJson(jsonBody);
    }

    notifyListeners();
    return ResponseStatusMessage(status: 'Success', message: 'Job is submitted!', error: false);
  }
}

For DataSetListScreen, you will have four different states:

  1. The data set list can be fetched with a non-empty result.
  2. The data set list can be fetched with an empty result.
  3. The data set list can be currently loading.
  4. An error has occurred.

You simply need to separate these conditions with a switch-case statement:

    switch (dataSetListProvider.status) {
            case Status.Success:
              return Scaffold(
                body: Column(
                  mainAxisAlignment: MainAxisAlignment.start,
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: <Widget>[
                    Padding(
                      padding: const EdgeInsets.all(32.0),
                      child: TextField(
                        controller: _filter,
                        decoration: InputDecoration(
                          hintText: "Filter data sets",
                          suffixIcon: IconButton(
                            onPressed: () => dataSetListProvider.getDataSets(
                              filterString: _filter.text,
                              authToken: auth.user.token,
                            ),
                            icon: Icon(Icons.search),
                          ),
                        ),
                      ),
                    ),
                    Expanded(
                      child: ListView.builder(
                      padding: EdgeInsets.all(16.0),
                      itemCount: dataSetListProvider.dataSetList.length,
                      itemBuilder: (context, index) => DataSetItem(
                          dataSet: dataSetListProvider.dataSetList[index]),
                      )
                    ),
                  ],
                ),
              ); 
            case Status.Loading:
              return LoadingWidget();
            case Status.Empty:
              return AlertWidget(
                message: 'Nothing to display.',
                color: Colors.amber,
                icon: Icons.assistant,
                back: false,
              );
            case Status.Error:
            default:
              return AlertWidget(
                message: 'An error occured!',
                color: Colors.redAccent,
                icon: Icons.warning,
                back: false,
              );
          }
        }

As you can see, the “success” state consists of two parts: one for user input, which consists of a text field and a search button, and one for displaying data sets. These data sets are built with data from dataSetListProvider. The results are rendered into a list view with a DataSetItem widget for each.

Each data set item renders data set organization, name, and actions. To create them, you pass DataSet instances to them:

class DataSetItem extends StatelessWidget {
  final DataSet dataSet;

  DataSetItem({this.dataSet});

  @override
  Widget build(BuildContext context) {
    final dsListProvider = Provider.of<DataSetListProvider>(context);
    final authProvider = Provider.of<AuthProvider>(context);
    final active = dataSet.dataSetOrganization != null && dsListProvider.actionStatus == ActionStatus.Idle;
    return ListTile(
      title: Text(dataSet.name),
      trailing: Row(
        mainAxisSize: MainAxisSize.min,
        children: <Widget>[
          IconButton(
            icon: Icon(Icons.remove_red_eye),
            color: Colors.black,
            onPressed: !active ? null : () async {
              switch (dataSet.dataSetOrganization) {
                case 'PO':
                case 'PO_E':
                  Navigator.pushNamed(context, 'dataSetMembers', arguments: dataSet.name);
                  break;
                case 'PS':
                  final result = await Navigator.pushNamed(context, 'dataSetContent', arguments: dataSet.name);
                  print(result);
                  if (result == 'refresh') {
                    dsListProvider.getDataSets(
                      filterString: dsListProvider.filter,
                      authToken: authProvider.user.token,
                    );
                  }
                  break;
                default:
              }
            },
            tooltip: 'Display',
          ),
          IconButton(
            icon: Icon(Icons.send),
            color: Colors.green,
            onPressed: !active || dataSet.dataSetOrganization != 'PS' ? null : () async {
              ResponseStatusMessage response = await dsListProvider.submitAsJob(
                dataSetName: dataSet.name, 
                authToken: authProvider.user.token
              );

              if (response.error) {
                Scaffold.of(context).showSnackBar(SnackBar(
                  content: Text('${response.status}: ${response.message}', style: TextStyle(color: Colors.white)),
                  backgroundColor: Colors.red,
                ));
              } else {
                Scaffold.of(context).showSnackBar(SnackBar(
                  content: Text('${response.message}', style: TextStyle(color: Colors.white)),
                  backgroundColor: Colors.green,
                ));
              }
            },
            tooltip: 'Submit as a job',
          ),
          IconButton(
            icon: Icon(Icons.delete_forever),
            color: Colors.redAccent,
            onPressed: !active ? null : () async {
              ResponseStatusMessage response = await dsListProvider.deleteDataSet(
                dataSetName: dataSet.name,
                authToken: authProvider.user.token,
              );

              if (response.error) {
                Scaffold.of(context).showSnackBar(SnackBar(
                  content: Text('${response.status}: ${response.message}', style: TextStyle(color: Colors.white)),
                  backgroundColor: Colors.red,
                ));
              } else {
                Scaffold.of(context).showSnackBar(SnackBar(
                  content: Text('${response.message}', style: TextStyle(color: Colors.white)),
                  backgroundColor: Colors.green,
                ));
              }
            },
            tooltip: 'Delete',
          ),
        ],
      ),
      leading: Text(dataSet.dataSetOrganization ?? '-', style: TextStyle(fontWeight: FontWeight.bold),),
      subtitle:
          Text('RECF: ${dataSet.recordFormat} | RECL: ${dataSet.recordLength}'),
      dense: false,
      enabled: dataSet.dataSetOrganization != null,
      isThreeLine: true,
    );
  }

Again, this child widget can access the providers you saw earlier. By being able to use authProvider, you can use the user ID to navigate to other routes which is required for API calls — such as getting data set content.

9

Completing the code

The code listings in this tutorial do not cover all of the widgets, but once again all can be accessed at any time in this repo.

10

Running the app

Now that everything is complete, it is time to run your application to see the result. First, you need to run iOS Simulator for MacOS or Android Emulator for other operating systems. Alternatively, you can plug in your own mobile device via a USB port, and turn on debug mode. Either way, Flutter will be aware of the connected devices. After you make sure your virtual or physical device is properly connected, execute the flutter run command on the terminal while in the root folder of the project.

Figure 8. A successful run command output

A successful run command output

Now you can filter and list data sets in the Z on the go with your mobile app. You can display them, submit them as a job, or delete them.

Figure 9. Data set filter, list, and actions

Data set filter, list, and actions

You can also create a new data set with a UI form, anywhere, on your mobile device. You can adjust every feature of the data set, from organization to record size.

Another great thing about this app is job management at a basic level. You can check the status of a job anywhere, or purge jobs if you need to.

Figure 10. Job listing and actions

Job listing and actions

Summary

Congrats! You have now successfully created a mobile app that can connect to a mainframe and interact with its data sets and jobs. But that’s not all — now that you’ve discovered Zowe, you can go on and create any other kind of client app with any technology stack you like by consuming REST APIs.

By completing this tutorial, you have:

  • installed Dart and Flutter on your machine
  • launched a Zowe web app
  • discovered REST APIs
  • learned the basics of Provider state management
  • disproven the myth that mainframes are no longer useful

Don’t forget that Zowe is not limited to REST APIs. If you’re curious about it, you can check out Zowe’s official website. To connect with the community, join the Open Mainframe Project Slack channel.