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
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.
- Creating a new Flutter project
- Adding dependencies
- Project structure
- Discovering REST APIs and defining base URLs
- Creating models
- Authentication
- Routing
- Using data sets
- Completing the code
- Running the app
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.
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.
Project structure
To keep your code file structure clean, you should keep your files in separate directories.
Figure 1. File directories
You will create the following directories:
models
— stores your model files; models are Dart class representations of the results from the Zowe APIproviders
— stores provider files, which are used to manage your states in screensscreens
— stores your screen files, which are the main visual components of your appservices
— stores service files, which can be used by any componentwidgets
— 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.
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
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
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.
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
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.
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
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
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
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:
- The data set list can be fetched with a non-empty result.
- The data set list can be fetched with an empty result.
- The data set list can be currently loading.
- 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.
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.
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
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
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
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.