changeset 195:ce6984e324a1

Add jvm-io component Reviewed-by: jkang Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2017-September/024903.html
author Andrew Azores <aazores@redhat.com>
date Tue, 12 Sep 2017 12:53:42 -0400
parents 1a1cf7a34a05
children a9b83aee75b0
files mock-api/endpoints/jvm-io.endpoint.js src/app/components/jvm-info/en.locale.yaml src/app/components/jvm-info/jvm-info.html src/app/components/jvm-info/jvm-io/en.locale.yaml src/app/components/jvm-info/jvm-io/jvm-io.component.js src/app/components/jvm-info/jvm-io/jvm-io.controller.js src/app/components/jvm-info/jvm-io/jvm-io.controller.spec.js src/app/components/jvm-info/jvm-io/jvm-io.html src/app/components/jvm-info/jvm-io/jvm-io.routing.js src/app/components/jvm-info/jvm-io/jvm-io.routing.spec.js src/app/components/jvm-info/jvm-io/jvm-io.service.js src/app/components/jvm-info/jvm-io/jvm-io.service.spec.js
diffstat 12 files changed, 872 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mock-api/endpoints/jvm-io.endpoint.js	Tue Sep 12 12:53:42 2017 -0400
@@ -0,0 +1,32 @@
+function jvmIo (server) {
+  var _ = require('lodash');
+  server.init('jvmIo');
+
+  server.app.get('/jvm-io/0.0.1/jvms/:jvmId', function (req, res, next) {
+    server.logRequest('jvm-io', req);
+
+    var jvmId = req.params.jvmId;
+
+    var response = [
+      {
+        agentId: 'foo-agentId',
+        jvmId: jvmId,
+        timeStamp: { $numberLong: Date.now().toString() },
+        charactersRead: { $numberLong: _.floor((Math.random() * 10000000)).toString() },
+        charactersWritten: { $numberLong: _.floor((Math.random() * 10000000)).toString() },
+        readSysCalls: { $numberLong: _.floor((Math.random() * 25000)).toString() },
+        writeSysCalls: { $numberLong: _.floor((Math.random() * 120000)).toString() }
+      }
+    ];
+
+    res.setHeader('Content-Type', 'application/json');
+    res.send(JSON.stringify(
+      {
+        response: response
+      }
+    ));
+    next();
+  });
+}
+
+module.exports = jvmIo;
--- a/src/app/components/jvm-info/en.locale.yaml	Mon Sep 11 13:59:43 2017 -0400
+++ b/src/app/components/jvm-info/en.locale.yaml	Tue Sep 12 12:53:42 2017 -0400
@@ -34,3 +34,4 @@
     NONE: None
     MEMORY: Memory Usage
     GC: Garbage Collection
+    IO: File I/O
--- a/src/app/components/jvm-info/jvm-info.html	Mon Sep 11 13:59:43 2017 -0400
+++ b/src/app/components/jvm-info/jvm-info.html	Tue Sep 12 12:53:42 2017 -0400
@@ -133,6 +133,7 @@
         <option value="" translate>jvmInfo.subview.NONE</option>
         <option value="jvmMemory" translate>jvmInfo.subview.MEMORY</option>
         <option value="jvmGc" translate>jvmInfo.subview.GC</option>
+        <option value="jvmIo" translate>jvmInfo.subview.IO</option>
       </select>
     </div>
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/app/components/jvm-info/jvm-io/en.locale.yaml	Tue Sep 12 12:53:42 2017 -0400
@@ -0,0 +1,25 @@
+jvmIo:
+
+  REFRESH_RATE_LABEL: Refresh Rate
+  MAX_DATA_AGE_LABEL: Max Data Age
+
+  refresh:
+    DISABLED: Disabled
+    SECONDS: '{SECONDS, plural, =0{0 Seconds} one{1 Second} other{# Seconds}}{DEFAULT, select, true{ (Default)} other{}}'
+
+  dataAge:
+    SECONDS: '{SECONDS, plural, =0{0 Seconds} one{1 Second} other{# Seconds}}{DEFAULT, select, true{ (Default)} other{}}'
+    MINUTES: '{MINUTES, plural, =0{0 Minutes} one{1 Minute} other{# Minutes}}{DEFAULT, select, true{ (Default)} other{}}'
+
+  chart:
+    TITLE: JVM I/O
+    X_LABEL: Timestamp
+    Y1_LABEL: Characters
+    Y2_LABEL: System Calls
+
+  metrics:
+    timestamp: Date
+    charactersRead: Characters Read
+    charactersWritten: Characters Written
+    readSysCalls: Read System Calls
+    writeSysCalls: Write System Calls
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/app/components/jvm-info/jvm-io/jvm-io.component.js	Tue Sep 12 12:53:42 2017 -0400
@@ -0,0 +1,43 @@
+/**
+ * Copyright 2012-2017 Red Hat, Inc.
+ *
+ * Thermostat is distributed under the GNU General Public License,
+ * version 2 or any later version (with a special exception described
+ * below, commonly known as the "Classpath Exception").
+ *
+ * A copy of GNU General Public License (GPL) is included in this
+ * distribution, in the file COPYING.
+ *
+ * Linking Thermostat code with other modules is making a combined work
+ * based on Thermostat.  Thus, the terms and conditions of the GPL
+ * cover the whole combination.
+ *
+ * As a special exception, the copyright holders of Thermostat give you
+ * permission to link this code with independent modules to produce an
+ * executable, regardless of the license terms of these independent
+ * modules, and to copy and distribute the resulting executable under
+ * terms of your choice, provided that you also meet, for each linked
+ * independent module, the terms and conditions of the license of that
+ * module.  An independent module is a module which is not derived from
+ * or based on Thermostat code.  If you modify Thermostat, you may
+ * extend this exception to your version of the software, but you are
+ * not obligated to do so.  If you do not wish to do so, delete this
+ * exception statement from your version.
+ */
+
+import controller from './jvm-io.controller.js';
+import service from './jvm-io.service.js';
+
+export default angular
+  .module('jvmIo.component', [
+    controller,
+    service
+  ])
+  .component('jvmIo', {
+    bindings: {
+      jvmId: '<'
+    },
+    controller: 'JvmIoController',
+    template: require('./jvm-io.html')
+  })
+  .name;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/app/components/jvm-info/jvm-io/jvm-io.controller.js	Tue Sep 12 12:53:42 2017 -0400
@@ -0,0 +1,192 @@
+/**
+ * Copyright 2012-2017 Red Hat, Inc.
+ *
+ * Thermostat is distributed under the GNU General Public License,
+ * version 2 or any later version (with a special exception described
+ * below, commonly known as the "Classpath Exception").
+ *
+ * A copy of GNU General Public License (GPL) is included in this
+ * distribution, in the file COPYING.
+ *
+ * Linking Thermostat code with other modules is making a combined work
+ * based on Thermostat.  Thus, the terms and conditions of the GPL
+ * cover the whole combination.
+ *
+ * As a special exception, the copyright holders of Thermostat give you
+ * permission to link this code with independent modules to produce an
+ * executable, regardless of the license terms of these independent
+ * modules, and to copy and distribute the resulting executable under
+ * terms of your choice, provided that you also meet, for each linked
+ * independent module, the terms and conditions of the license of that
+ * module.  An independent module is a module which is not derived from
+ * or based on Thermostat code.  If you modify Thermostat, you may
+ * extend this exception to your version of the software, but you are
+ * not obligated to do so.  If you do not wish to do so, delete this
+ * exception statement from your version.
+ */
+
+import 'c3';
+import service from './jvm-io.service.js';
+import services from 'shared/services/services.module.js';
+import filters from 'shared/filters/filters.module.js';
+
+class JvmIoController {
+  constructor (jvmIoService, $interval, $translate, dateFilter, DATE_FORMAT, metricToNumberFilter) {
+    'ngInject';
+    this._svc = jvmIoService;
+    this._interval = $interval;
+    this._translate = $translate;
+    this._dateFilter = dateFilter;
+    this._dateFormat = DATE_FORMAT;
+    this._metricToNumber = metricToNumberFilter;
+
+    this._refreshRate = 1000;
+    this._dataAgeLimit = 30000;
+    this._makeChartConfig().then(() => this._start());
+  }
+
+  $onDestroy () {
+    this._stop();
+  }
+
+  _makeChartConfig () {
+    return this._translate([
+      'jvmIo.chart.X_LABEL',
+      'jvmIo.chart.Y1_LABEL',
+      'jvmIo.chart.Y2_LABEL',
+
+      'jvmIo.metrics.timestamp',
+      'jvmIo.metrics.charactersRead',
+      'jvmIo.metrics.charactersWritten',
+      'jvmIo.metrics.readSysCalls',
+      'jvmIo.metrics.writeSysCalls',
+    ]).then(translations => {
+      this.config = {
+        type: 'line',
+        chartId: 'jvm-io-chart',
+        grid: {
+          y: {
+            show: true
+          }
+        },
+        axis: {
+          x: {
+            label: translations['jvmIo.chart.X_LABEL'],
+            type: 'timeseries',
+            localtime: false,
+            tick: {
+              format: timestamp => this._dateFilter(timestamp, this._dateFormat.time.medium),
+              count: 5
+            }
+          },
+          y: {
+            label: translations['jvmIo.chart.Y1_LABEL'],
+            tick: {
+              format: d => d
+            }
+          },
+          y2: {
+            show: true,
+            label: translations['jvmIo.chart.Y2_LABEL'],
+            tick: {
+              format: d => d
+            }
+          }
+        },
+        tooltip: {
+          format: {
+            title: x => x,
+            value: y => y
+          }
+        },
+        data: {
+          x: translations['jvmIo.metrics.timestamp'],
+          rows: [
+            [
+              translations['jvmIo.metrics.timestamp'],
+              translations['jvmIo.metrics.charactersRead'],
+              translations['jvmIo.metrics.charactersWritten'],
+              translations['jvmIo.metrics.readSysCalls'],
+              translations['jvmIo.metrics.writeSysCalls']
+            ]
+          ]
+        }
+      };
+      this.config.data.axes = {};
+      this.config.data.axes[translations['jvmIo.metrics.charactersRead']] = 'y';
+      this.config.data.axes[translations['jvmIo.metrics.charactersWritten']] = 'y';
+      this.config.data.axes[translations['jvmIo.metrics.readSysCalls']] = 'y2';
+      this.config.data.axes[translations['jvmIo.metrics.writeSysCalls']] = 'y2';
+    });
+  }
+
+  set refreshRate (val) {
+    this._stop();
+    this._refreshRate = parseInt(val);
+    if (this._refreshRate > 0) {
+      this._start();
+    }
+  }
+
+  get refreshRate () {
+    return this._refreshRate.toString();
+  }
+
+  set dataAgeLimit (val) {
+    this._dataAgeLimit = val;
+    this._trimData();
+  }
+
+  get dataAgeLimit () {
+    return this._dataAgeLimit.toString();
+  }
+
+  _start () {
+    this._stop();
+    this._refresh = this._interval(() => this._update(), this._refreshRate);
+    this._update();
+  }
+
+  _stop () {
+    if (angular.isDefined(this._refresh)) {
+      this._interval.cancel(this._refresh);
+      delete this._refresh;
+    }
+  }
+
+  _trimData () {
+    let now = Date.now();
+    let limit = now - this._dataAgeLimit;
+    while (this.config.data.rows.length > 1 && this.config.data.rows[1][0] < limit) {
+      this.config.data.rows.splice(1, 1);
+    }
+  }
+
+  _update () {
+    if (!angular.isDefined(this.jvmId)) {
+      return;
+    }
+    this._svc.getJvmIoData(this.jvmId).then(res => {
+      let update = res.data.response[0];
+      this.config.data.rows.push([
+        this._metricToNumber(update.timeStamp),
+        this._metricToNumber(update.charactersRead),
+        this._metricToNumber(update.charactersWritten),
+        this._metricToNumber(update.readSysCalls),
+        this._metricToNumber(update.writeSysCalls),
+      ]);
+      this._trimData();
+    });
+  }
+}
+
+export default angular
+  .module('jvmIo.controller', [
+    service,
+    services,
+    filters,
+    'patternfly',
+    'patternfly.charts'
+  ])
+  .controller('JvmIoController', JvmIoController)
+  .name;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/app/components/jvm-info/jvm-io/jvm-io.controller.spec.js	Tue Sep 12 12:53:42 2017 -0400
@@ -0,0 +1,263 @@
+/**
+ * Copyright 2012-2017 Red Hat, Inc.
+ *
+ * Thermostat is distributed under the GNU General Public License,
+ * version 2 or any later version (with a special exception described
+ * below, commonly known as the "Classpath Exception").
+ *
+ * A copy of GNU General Public License (GPL) is included in this
+ * distribution, in the file COPYING.
+ *
+ * Linking Thermostat code with other modules is making a combined work
+ * based on Thermostat.  Thus, the terms and conditions of the GPL
+ * cover the whole combination.
+ *
+ * As a special exception, the copyright holders of Thermostat give you
+ * permission to link this code with independent modules to produce an
+ * executable, regardless of the license terms of these independent
+ * modules, and to copy and distribute the resulting executable under
+ * terms of your choice, provided that you also meet, for each linked
+ * independent module, the terms and conditions of the license of that
+ * module.  An independent module is a module which is not derived from
+ * or based on Thermostat code.  If you modify Thermostat, you may
+ * extend this exception to your version of the software, but you are
+ * not obligated to do so.  If you do not wish to do so, delete this
+ * exception statement from your version.
+ */
+
+import controllerModule from './jvm-io.controller.js';
+
+describe('JvmIoController', () => {
+
+  let ctrl, svc, interval, translations, translate,
+    dateFilter, dateFormat, metricToNumberFilter;
+  beforeEach(() => {
+    angular.mock.module(controllerModule);
+    angular.mock.inject($controller => {
+      'ngInject';
+      let svcPromise = sinon.stub();
+      svc = {
+        getJvmIoData: sinon.stub().returns({
+          then: svcPromise
+        }),
+        promise: svcPromise
+      };
+      interval = sinon.stub().returns('intervalMock');
+      interval.cancel = sinon.spy();
+
+      translations = {
+        'jvmIo.metrics.timestamp': 'date',
+        'jvmIo.metrics.charactersRead': 'characters read',
+        'jvmIo.metrics.charactersWritten': 'characters written',
+        'jvmIo.metrics.readSysCalls': 'read sys calls',
+        'jvmIo.metrics.writeSysCalls': 'write sys calls'
+      };
+      let translateThenThen = sinon.stub().yields();
+      translate = sinon.stub().returns({
+        then: sinon.stub().yields(translations).returns({
+          then: translateThenThen
+        })
+      });
+
+      dateFilter = sinon.stub().returns('dateFilterMock');
+      dateFormat = {
+        time: {
+          medium: 'dateFormatMedium'
+        }
+      };
+      metricToNumberFilter = sinon.stub().callsFake(v => parseInt(v.$numberLong));
+
+      ctrl = $controller('JvmIoController', {
+        jvmIoService: svc,
+        $interval: interval,
+        $translate: translate,
+        dateFilter: dateFilter,
+        DATE_FORMAT: dateFormat,
+        metricToNumberFilter: metricToNumberFilter
+      });
+      ctrl.jvmId = 'foo-jvmId';
+    });
+  });
+
+  it('should exist', () => {
+    should.exist(ctrl);
+  });
+
+  describe('init', () => {
+    it('should set refresh rate to 1 second', () => {
+      ctrl.refreshRate.should.equal('1000');
+    });
+
+    it('should set data age limit to 30 seconds', () => {
+      ctrl.dataAgeLimit.should.equal('30000');
+    });
+
+    it('should use translations', () => {
+      translate.should.be.calledWith([
+        'jvmIo.chart.X_LABEL',
+        'jvmIo.chart.Y1_LABEL',
+        'jvmIo.chart.Y2_LABEL',
+
+        'jvmIo.metrics.timestamp',
+        'jvmIo.metrics.charactersRead',
+        'jvmIo.metrics.charactersWritten',
+        'jvmIo.metrics.readSysCalls',
+        'jvmIo.metrics.writeSysCalls',
+      ]);
+    });
+  });
+
+  describe('chart config', () => {
+    it('should format x-axis ticks', () => {
+      let fn = ctrl.config.axis.x.tick.format;
+      fn(100).should.equal('dateFilterMock');
+    });
+
+    it('should provide an identity y-axis tick format function', () => {
+      let fn = ctrl.config.axis.y.tick.format;
+      fn(100).should.equal(100);
+      fn(200).should.equal(200);
+    });
+
+    it('should provide an identity y2-axis tick format function', () => {
+      let fn = ctrl.config.axis.y2.tick.format;
+      fn(100).should.equal(100);
+      fn(200).should.equal(200);
+    });
+
+    it('should provide identity tooltip format functions', () => {
+      let title = ctrl.config.tooltip.format.title;
+      title(100).should.equal(100);
+      title(200).should.equal(200);
+      let value = ctrl.config.tooltip.format.value;
+      value(100).should.equal(100);
+      value(200).should.equal(200);
+    });
+  });
+
+  describe('$onDestroy', () => {
+    it('should do nothing if controller is not started', () => {
+      ctrl._stop();
+      interval.cancel.should.be.calledOnce();
+      ctrl.$onDestroy();
+      interval.cancel.should.be.calledOnce();
+    });
+
+    it('should stop the controller if already started', () => {
+      ctrl._start();
+      interval.cancel.should.be.calledOnce();
+      ctrl.$onDestroy();
+      interval.cancel.should.be.calledTwice();
+    });
+  });
+
+  describe('refreshRate', () => {
+    it('should set interval and disable if <= 0', () => {
+      interval.should.be.calledOnce();
+      ctrl.refreshRate = 10000;
+      interval.cancel.should.be.calledOnce();
+      interval.should.be.calledTwice();
+      interval.secondCall.should.be.calledWith(sinon.match.func, 10000);
+      ctrl.refreshRate = -1;
+      interval.cancel.should.be.calledTwice();
+      interval.should.be.calledTwice();
+    });
+
+    it('should reflect changes in getter', () => {
+      ctrl.refreshRate.should.equal('1000');
+      ctrl.refreshRate = 2000;
+      ctrl.refreshRate.should.equal('2000');
+    });
+  });
+
+  describe('dataAgeLimit', () => {
+    it('should cause a data trim on change', () => {
+      sinon.spy(ctrl, '_trimData');
+      ctrl._trimData.should.not.be.called;
+      ctrl.dataAgeLimit = 10000;
+      ctrl._trimData.should.be.calledOnce();
+      ctrl._trimData.restore();
+    });
+
+    it('should reflect changes in getter', () => {
+      ctrl.dataAgeLimit.should.equal('30000');
+      ctrl.dataAgeLimit = 10000;
+      ctrl.dataAgeLimit.should.equal('10000');
+    });
+  });
+
+  describe('_start', () => {
+    it('should perform updates on an interval', () => {
+      interval.should.be.calledOnce();
+      ctrl._start();
+      interval.should.be.calledTwice();
+      interval.secondCall.should.be.calledWith(sinon.match.func, 1000);
+
+      let func = interval.args[1][0];
+      svc.getJvmIoData.should.be.calledOnce();
+      func();
+      svc.getJvmIoData.should.be.calledTwice();
+    });
+  });
+
+  describe('_stop', () => {
+    it('should do nothing if not started', () => {
+      interval.cancel.should.not.be.called();
+      ctrl._stop();
+      interval.cancel.should.be.calledOnce();
+      interval.cancel.should.be.calledWith('intervalMock');
+      ctrl._stop();
+      interval.cancel.should.be.calledOnce();
+    });
+  });
+
+  describe('_update', () => {
+    it('should do nothing if jvmId not yet bound', () => {
+      svc.getJvmIoData.should.not.be.called();
+      delete ctrl.jvmId;
+      ctrl._update();
+      svc.getJvmIoData.should.not.be.called();
+    });
+
+    it('should add update data to chart data', () => {
+      ctrl._update();
+      svc.promise.should.be.calledOnce();
+      svc.promise.should.be.calledWith(sinon.match.func);
+      let stamp = Date.now();
+      svc.promise.args[0][0]({
+        data: {
+          response: [{
+            timeStamp: { $numberLong: stamp.toString() },
+            charactersRead: { $numberLong: '1000000' },
+            charactersWritten: { $numberLong: '500000' },
+            readSysCalls: { $numberLong: '100' },
+            writeSysCalls: { $numberLong: '50' }
+          }]
+        }
+      });
+      ctrl.config.data.rows.should.deepEqual([
+        ['date', 'characters read', 'characters written', 'read sys calls', 'write sys calls'],
+        [stamp, 1000000, 500000, 100, 50]
+      ]);
+    });
+  });
+
+  describe('_trimData', () => {
+    it('should remove datasets older than dataAgeLimit', () => {
+      let now = Date.now();
+      let expired = now - 60000;
+      ctrl.config.data.rows.push([
+        expired, 1, 1, 1, 1
+      ]);
+      ctrl.config.data.rows.push([
+        now, 2, 2, 2, 2
+      ]);
+      ctrl._trimData();
+      ctrl.config.data.rows.should.deepEqual([
+        ['date', 'characters read', 'characters written', 'read sys calls', 'write sys calls'],
+        [now, 2, 2, 2, 2]
+      ]);
+    });
+  });
+
+});
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/app/components/jvm-info/jvm-io/jvm-io.html	Tue Sep 12 12:53:42 2017 -0400
@@ -0,0 +1,45 @@
+<div class="container-fluid">
+
+  <div class="row" style="margin-top:2vh">
+    <div class="col-xs-12 col-md-3">
+      <label for="refreshCombo" class="label label-info" translate>jvmIo.REFRESH_RATE_LABEL</label>
+      <select name="refreshCombo" class="combobox form-control" ng-model="$ctrl.refreshRate">
+        <option value="-1" translate>jvmIo.refresh.DISABLED</option>
+        <option value="1000" selected translate="jvmIo.refresh.SECONDS" translate-values="{ SECONDS: 1, DEFAULT: true }" translate-interpolation="messageformat"></option>
+        <option value="2000" translate="jvmIo.refresh.SECONDS" translate-values="{ SECONDS: 2 }" translate-interpolation="messageformat"></option>
+        <option value="5000" translate="jvmIo.refresh.SECONDS" translate-values="{ SECONDS: 5 }" translate-interpolation="messageformat"></option>
+        <option value="10000" translate="jvmIo.refresh.SECONDS" translate-values="{ SECONDS: 10 }" translate-interpolation="messageformat"></option>
+        <option value="30000" translate="jvmIo.refresh.SECONDS" translate-values="{ SECONDS: 30 }" translate-interpolation="messageformat"></option>
+      </select>
+    </div>
+
+    <div class="col-xs-12 col-md-3">
+      <label for="dataAgeCombo" class="label label-info" translate>jvmIo.MAX_DATA_AGE_LABEL</label>
+      <select name="dataAgeCombo" class="combobox form-control" ng-model="$ctrl.dataAgeLimit">
+        <option value="10000" translate="jvmIo.dataAge.SECONDS" translate-values="{ SECONDS: 10 }" translate-interpolation="messageformat"></option>
+        <option value="30000" selected translate="jvmIo.dataAge.SECONDS" translate-values="{ SECONDS: 30, DEFAULT: true }" translate-interpolation="messageformat"></option>
+        <option value="60000" translate="jvmIo.dataAge.MINUTES" translate-values="{ MINUTES: 1 }" translate-interpolation="messageformat"></option>
+        <option value="300000" translate="jvmIo.dataAge.MINUTES" translate-values="{ MINUTES: 5 }" translate-interpolation="messageformat"></option>
+        <option value="900000" translate="jvmIo.dataAge.MINUTES" translate-values="{ MINUTES: 15 }" translate-interpolation="messageformat"></option>
+      </select>
+    </div>
+  </div><!-- row -->
+
+  <div class="row row-cards-pf">
+    <div class="container-fluid container-cards-pf">
+
+      <div class="col-md-12">
+        <div class="card-pf card-pf-view">
+          <div class="card-pf-heading">
+            <label class="card-pf-title" translate>jvmIo.chart.TITLE</label>
+          </div>
+          <div ng-if="$ctrl.config" class="card-pf-body text-center">
+            <pf-c3-chart id="jvm-io-chart" config="$ctrl.config"></pf-c3-chart>
+          </div>
+        </div><!-- card-pf -->
+      </div><!-- col -->
+    </div><!-- container-cards-pf -->
+  </div><!-- row-cards-pf -->
+
+
+</div><!-- container-fluid -->
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/app/components/jvm-info/jvm-io/jvm-io.routing.js	Tue Sep 12 12:53:42 2017 -0400
@@ -0,0 +1,61 @@
+/**
+ * Copyright 2012-2017 Red Hat, Inc.
+ *
+ * Thermostat is distributed under the GNU General Public License,
+ * version 2 or any later version (with a special exception described
+ * below, commonly known as the "Classpath Exception").
+ *
+ * A copy of GNU General Public License (GPL) is included in this
+ * distribution, in the file COPYING.
+ *
+ * Linking Thermostat code with other modules is making a combined work
+ * based on Thermostat.  Thus, the terms and conditions of the GPL
+ * cover the whole combination.
+ *
+ * As a special exception, the copyright holders of Thermostat give you
+ * permission to link this code with independent modules to produce an
+ * executable, regardless of the license terms of these independent
+ * modules, and to copy and distribute the resulting executable under
+ * terms of your choice, provided that you also meet, for each linked
+ * independent module, the terms and conditions of the license of that
+ * module.  An independent module is a module which is not derived from
+ * or based on Thermostat code.  If you modify Thermostat, you may
+ * extend this exception to your version of the software, but you are
+ * not obligated to do so.  If you do not wish to do so, delete this
+ * exception statement from your version.
+ */
+
+function config ($stateProvider) {
+  'ngInject';
+
+  $stateProvider.state('jvmInfo.jvmIo', {
+    url: '/jvm-io',
+    component: 'jvmIo',
+    resolve: {
+      lazyLoad: ($q, $ocLazyLoad) => {
+        'ngInject';
+        return $q(resolve => {
+          require.ensure(['./jvm-io.component.js'], () => {
+            let module = require('./jvm-io.component.js');
+            $ocLazyLoad.load({ name: module.default });
+            resolve(module);
+          });
+        });
+      },
+      jvmId: $stateParams => {
+        'ngInject';
+        return $stateParams.jvmId;
+      }
+    }
+  });
+}
+
+export { config };
+
+export default angular
+  .module('jvmIo.routing', [
+    'ui.router',
+    'oc.lazyLoad'
+  ])
+  .config(config)
+  .name;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/app/components/jvm-info/jvm-io/jvm-io.routing.spec.js	Tue Sep 12 12:53:42 2017 -0400
@@ -0,0 +1,84 @@
+/**
+ * Copyright 2012-2017 Red Hat, Inc.
+ *
+ * Thermostat is distributed under the GNU General Public License,
+ * version 2 or any later version (with a special exception described
+ * below, commonly known as the "Classpath Exception").
+ *
+ * A copy of GNU General Public License (GPL) is included in this
+ * distribution, in the file COPYING.
+ *
+ * Linking Thermostat code with other modules is making a combined work
+ * based on Thermostat.  Thus, the terms and conditions of the GPL
+ * cover the whole combination.
+ *
+ * As a special exception, the copyright holders of Thermostat give you
+ * permission to link this code with independent modules to produce an
+ * executable, regardless of the license terms of these independent
+ * modules, and to copy and distribute the resulting executable under
+ * terms of your choice, provided that you also meet, for each linked
+ * independent module, the terms and conditions of the license of that
+ * module.  An independent module is a module which is not derived from
+ * or based on Thermostat code.  If you modify Thermostat, you may
+ * extend this exception to your version of the software, but you are
+ * not obligated to do so.  If you do not wish to do so, delete this
+ * exception statement from your version.
+ */
+
+describe('JvmIoRouting', () => {
+
+  let module = require('./jvm-io.routing.js');
+  let stateProvider, args, q, ocLazyLoad;
+  beforeEach(() => {
+    angular.mock.module(module.default);
+    stateProvider = {
+      state: sinon.spy()
+    };
+    module.config(stateProvider);
+    args = stateProvider.state.args[0];
+    q = sinon.spy();
+    ocLazyLoad = {
+      load: sinon.spy()
+    };
+  });
+
+  describe('stateProvider', () => {
+    it('should call $stateProvider.state', () => {
+      stateProvider.state.should.be.calledOnce();
+    });
+
+    it('should define a \'jvmInfo.jvmIo\' state', () => {
+      args[0].should.equal('jvmInfo.jvmIo');
+    });
+
+    it('should map to /jvm-io', () => {
+      args[1].url.should.equal('/jvm-io');
+    });
+
+    it('resolve should load jvm-io component', done => {
+      let resolveFn = args[1].resolve.lazyLoad[2];
+      resolveFn.should.be.a.Function();
+      resolveFn(q, ocLazyLoad);
+      q.should.be.calledOnce();
+
+      let deferred = q.args[0][0];
+      deferred.should.be.a.Function();
+
+      let resolve = sinon.stub().callsFake(val => {
+        let mod = require('./jvm-io.component.js');
+        ocLazyLoad.load.should.be.calledWith({ name: mod.default });
+        val.should.equal(mod);
+        done();
+      });
+      deferred(resolve);
+    });
+  });
+
+  describe('resolve params', () => {
+    it('should resolve jvmId state param', () => {
+      let fn = args[1].resolve.jvmId[1];
+      fn({ jvmId: 'foo-id' }).should.equal('foo-id');
+    });
+  });
+
+});
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/app/components/jvm-info/jvm-io/jvm-io.service.js	Tue Sep 12 12:53:42 2017 -0400
@@ -0,0 +1,47 @@
+/**
+ * Copyright 2012-2017 Red Hat, Inc.
+ *
+ * Thermostat is distributed under the GNU General Public License,
+ * version 2 or any later version (with a special exception described
+ * below, commonly known as the "Classpath Exception").
+ *
+ * A copy of GNU General Public License (GPL) is included in this
+ * distribution, in the file COPYING.
+ *
+ * Linking Thermostat code with other modules is making a combined work
+ * based on Thermostat.  Thus, the terms and conditions of the GPL
+ * cover the whole combination.
+ *
+ * As a special exception, the copyright holders of Thermostat give you
+ * permission to link this code with independent modules to produce an
+ * executable, regardless of the license terms of these independent
+ * modules, and to copy and distribute the resulting executable under
+ * terms of your choice, provided that you also meet, for each linked
+ * independent module, the terms and conditions of the license of that
+ * module.  An independent module is a module which is not derived from
+ * or based on Thermostat code.  If you modify Thermostat, you may
+ * extend this exception to your version of the software, but you are
+ * not obligated to do so.  If you do not wish to do so, delete this
+ * exception statement from your version.
+ */
+
+import config from 'shared/config/config.module.js';
+import urlJoin from 'url-join';
+
+class JvmIoService {
+  constructor ($http, gatewayUrl) {
+    'ngInject';
+    this._http = $http;
+    this._gatewayUrl = gatewayUrl;
+  }
+
+  getJvmIoData (jvmId) {
+    let params = { sort: '-timeStamp' };
+    return this._http.get(urlJoin(this._gatewayUrl, 'jvm-io', '0.0.1', 'jvms', jvmId), { params: params });
+  }
+}
+
+export default angular
+  .module('jvmIo.service', [config])
+  .service('jvmIoService', JvmIoService)
+  .name;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/app/components/jvm-info/jvm-io/jvm-io.service.spec.js	Tue Sep 12 12:53:42 2017 -0400
@@ -0,0 +1,78 @@
+/**
+ * Copyright 2012-2017 Red Hat, Inc.
+ *
+ * Thermostat is distributed under the GNU General Public License,
+ * version 2 or any later version (with a special exception described
+ * below, commonly known as the "Classpath Exception").
+ *
+ * A copy of GNU General Public License (GPL) is included in this
+ * distribution, in the file COPYING.
+ *
+ * Linking Thermostat code with other modules is making a combined work
+ * based on Thermostat.  Thus, the terms and conditions of the GPL
+ * cover the whole combination.
+ *
+ * As a special exception, the copyright holders of Thermostat give you
+ * permission to link this code with independent modules to produce an
+ * executable, regardless of the license terms of these independent
+ * modules, and to copy and distribute the resulting executable under
+ * terms of your choice, provided that you also meet, for each linked
+ * independent module, the terms and conditions of the license of that
+ * module.  An independent module is a module which is not derived from
+ * or based on Thermostat code.  If you modify Thermostat, you may
+ * extend this exception to your version of the software, but you are
+ * not obligated to do so.  If you do not wish to do so, delete this
+ * exception statement from your version.
+ */
+
+describe('JvmIoService', () => {
+
+  beforeEach(() => {
+    angular.mock.module('configModule', $provide => {
+      'ngInject';
+      $provide.constant('gatewayUrl', 'http://example.com:1234');
+    });
+
+    angular.mock.module('jvmIo.service');
+  });
+
+  let httpBackend, scope, svc;
+  beforeEach(inject(($httpBackend, $rootScope, jvmIoService) => {
+    'ngInject';
+    httpBackend = $httpBackend;
+
+    scope = $rootScope;
+    svc = jvmIoService;
+  }));
+
+  afterEach(() => {
+    httpBackend.verifyNoOutstandingExpectation();
+    httpBackend.verifyNoOutstandingRequest();
+  });
+
+  it('should exist', () => {
+    should.exist(svc);
+  });
+
+  describe('getJvmIodata (jvmId)', () => {
+    it('should resolve mock data', done => {
+      let expected = {
+        timeStamp: { $numberLong: Date.now().toString() },
+        charactersRead: { $numberLong: '100000' },
+        charactersWritten: { $numberLong: '50000' },
+        readSysCalls: { $numberLong: '100' },
+        writeSysCalls: { $numberLong: '50' }
+      };
+      httpBackend.when('GET', 'http://example.com:1234/jvm-io/0.0.1/jvms/foo-jvmId?sort=-timeStamp')
+        .respond(expected);
+      svc.getJvmIoData('foo-jvmId').then(res => {
+        res.data.should.deepEqual(expected);
+        done();
+      });
+      httpBackend.expectGET('http://example.com:1234/jvm-io/0.0.1/jvms/foo-jvmId?sort=-timeStamp');
+      httpBackend.flush();
+      scope.$apply();
+    });
+  });
+
+});