changeset 245:2eb2ae0f3b3f

Transition project to Angular 4 Hybrid Reviewed-by: jkang Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2017-October/025266.html
author Andrew Azores <aazores@redhat.com>
date Fri, 13 Oct 2017 14:05:09 -0400
parents 63df0ca94b6b
children 1ad5abbce4fc
files karma.conf.js package.json src/app/ang-app.module.js src/app/app-root.component.js src/app/app-root.controller.js src/app/app-root.controller.spec.js src/app/app-root.html src/app/app.controller.js src/app/app.controller.spec.js src/app/app.module.js src/app/app.module.spec.js src/app/app.module.ts src/app/app.routing.js src/app/app.routing.spec.js src/app/auth-interceptor.factory.js src/app/auth-interceptor.factory.spec.js src/app/components/about/about.controller.spec.js src/app/components/jvm-info/jvm-gc/jvm-gc.controller.spec.js src/app/components/jvm-info/jvm-info.controller.spec.js src/app/components/jvm-info/jvm-memory/jvm-memory.controller.spec.js src/app/components/jvm-info/kill-vm.service.spec.js src/app/components/jvm-list/jvm-list.controller.spec.js src/app/index.html src/app/shared/config/config.module.js src/app/shared/config/config.module.spec.js src/app/shared/filters/big-int-to-string.filter.js src/app/shared/filters/big-int-to-string.filter.spec.js src/app/shared/filters/extract-class.pipe.spec.ts src/app/shared/filters/extract-class.pipe.ts src/app/shared/filters/filters.module.ts src/app/shared/filters/format-bytes.pipe.spec.ts src/app/shared/filters/format-bytes.pipe.ts src/app/shared/filters/metric-to-big-int.filter.spec.js src/app/shared/filters/metric.d.ts src/app/shared/services/ajs-upgraded-providers.ts src/app/shared/services/command-channel.service.js src/app/shared/services/command-channel.service.spec.js src/app/shared/services/extract-class.service.js src/app/shared/services/extract-class.service.spec.js src/app/shared/services/local-storage.service.js src/app/shared/services/local-storage.service.spec.js src/app/shared/services/metric-to-big-int.service.js src/app/shared/services/metric-to-big-int.service.spec.js src/app/shared/services/multichart.service.js src/app/shared/services/multichart.service.spec.js src/app/shared/services/sanitize.service.js src/app/shared/services/sanitize.service.spec.js src/app/shared/services/scale-bytes.service.js src/app/shared/services/scale-bytes.service.spec.js src/app/shared/services/services.module.js src/app/shared/services/services.module.ts src/app/shared/services/websocket-factory.service.js src/gateway-url-helper.js src/main.ts src/rx-subject.stub.ts src/tests.webpack.js tsconfig.json tslint.json webpack.config.js
diffstat 59 files changed, 1445 insertions(+), 787 deletions(-) [+]
line wrap: on
line diff
--- a/karma.conf.js	Fri Oct 13 11:39:51 2017 -0400
+++ b/karma.conf.js	Fri Oct 13 14:05:09 2017 -0400
@@ -25,11 +25,12 @@
  * exception statement from your version.
  */
 
+var webpackConfig = require('./webpack.config');
 module.exports = function (config) {
   config.set({
     basePath: '',
 
-    frameworks: ['mocha', 'should-sinon', 'sinon', 'should'],
+    frameworks: ['mocha', 'should-sinon', 'sinon', 'should', 'karma-typescript'],
 
     files: [
       'src/app/components/auth/keycloak.stub.js',
@@ -41,7 +42,7 @@
       'src/tests.webpack.js': ['webpack', 'sourcemap']
     },
 
-    reporters: ['mocha', 'beep', 'junit', 'coverage-istanbul'],
+    reporters: ['mocha', 'beep', 'junit', 'coverage-istanbul', 'karma-typescript'],
 
     junitReporter: {
       outputDir: 'test-reports'
@@ -66,7 +67,11 @@
 
     browsers: ['PhantomJS'],
 
-    webpack: require('./webpack.config'),
+    webpack: {
+      module: webpackConfig.module,
+      resolve: webpackConfig.resolve,
+      devtool: webpackConfig.devtool
+    },
 
     webpackMiddleware: {
       noInfo: 'errors-only'
--- a/package.json	Fri Oct 13 11:39:51 2017 -0400
+++ b/package.json	Fri Oct 13 14:05:09 2017 -0400
@@ -10,6 +10,15 @@
   "author": "",
   "license": "",
   "devDependencies": {
+    "@angular/core": "^4.4.2",
+    "@angular/platform-browser": "^4.4.2",
+    "@angular/upgrade": "^4.4.2",
+    "@types/angular": "^1.6.32",
+    "@types/big.js": "^3.2.0",
+    "@types/core-js": "^0.9.43",
+    "@types/mocha": "^2.2.43",
+    "@types/should": "^11.2.0",
+    "@types/sinon": "^2.3.5",
     "@uirouter/angularjs": "^1.0.0",
     "angular-mocks": "1.5.*",
     "angular-patternfly": "^4.4.1",
@@ -49,29 +58,38 @@
     "karma-should-sinon": "^1.0.0",
     "karma-sinon": "^1.0.5",
     "karma-sourcemap-loader": "^0.3.7",
+    "karma-typescript": "^3.0.7",
     "karma-webpack": "^2.0.3",
     "keycloak-js": "^3.2.0",
     "mkdirp": "^0.5.1",
     "mocha": "^3.2.0",
     "mocha-junit-reporter": "^1.13.0",
     "mocha-multi": "^0.11.0",
+    "ngx-bootstrap": "^1.9.3",
     "node-sass": "^4.5.2",
     "null-loader": "^0.1.1",
     "nyc": "^10.2.0",
     "oclazyload": "^1.1.0",
+    "patternfly": "^3.27.4",
+    "patternfly-ng": "^0.10.1",
     "phantomjs-prebuilt": "^2.1.14",
     "protractor": "^5.1.2",
     "raw-loader": "^0.5.1",
+    "reflect-metadata": "^0.1.10",
     "rimraf": "^2.6.1",
     "sass-loader": "^6.0.5",
     "should": "^11.2.1",
     "sinon": "^2.1.0",
     "style-loader": "^0.16.1",
+    "ts-loader": "^2.3.7",
+    "tslint": "^5.7.0",
+    "typescript": "^2.5.2",
     "url": "^0.11.0",
     "url-join": "^2.0.1",
     "webpack": "^2.6.1",
     "webpack-dev-server": "^2.4.2",
-    "yaml-loader": "^0.5.0"
+    "yaml-loader": "^0.5.0",
+    "zone.js": "^0.8.17"
   },
   "scripts": {
     "clean": "rimraf checkstyle coverage dist test-reports",
@@ -80,13 +98,15 @@
     "prelint": "mkdirp checkstyle",
     "lint-js": "eslint --ext '!.spec.js' -c .eslintrc.yaml  src || npm run lint-js-checkstyle",
     "lint-js-checkstyle": "eslint -c .eslintrc.yaml -f checkstyle -o checkstyle/checkstyle-src.xml src",
+    "lint-ts": "tslint --exclude 'src/app/**/*.spec.ts' --project ./tsconfig.json --type-check 'src/app/**/*.ts' || npm run lint-ts-checkstyle",
+    "lint-ts-checkstyle": "tslint --force --exclude 'src/app/**/*.spec.ts' --project ./tsconfig.json --type-check 'src/app/**/*.ts' -o checkstyle/checkstyle-ts-src.xml -t checkstyle 'src/app/**/*.ts'",
     "lint-html": "htmlhint -c .htmlhintrc 'src/**/*.html' || npm run lint-html-checkstyle",
     "lint-html-checkstyle": "htmlhint -c .htmlhintrc -f checkstyle 'src/**/*.html' > checkstyle/checkstyle-html.xml",
     "lint-tests": "eslint -c .eslintrc.test.yaml src --ext .spec.js || npm run lint-test-checkstyle",
     "lint-tests-checkstyle": "eslint -c .eslintrc.test.yaml --ext .spec.js -f checkstyle -o checkstyle/checkstyle-test.xml src",
     "lint-itests": "eslint -c .eslintrc.test.yaml integration-test --ext .spec.js || npm run lint-itest-checkstyle",
     "lint-itests-checkstyle": "eslint -c .eslintrc.test.yaml --ext .spec.js -f checkstyle -o checkstyle/checkstyle-itest.xml src",
-    "lint": "npm run lint-js && npm run lint-html",
+    "lint": "npm run lint-ts && npm run lint-js && npm run lint-html",
     "prebuild": "npm run clean && npm run license-check && npm run lint",
     "build": "webpack --bail --progress --profile",
     "postbuild": "npm test",
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/app/ang-app.module.js	Fri Oct 13 14:05:09 2017 -0400
@@ -0,0 +1,105 @@
+/**
+ * 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 'angular-patternfly';
+import '@uirouter/angularjs';
+import angularTranslate from 'angular-translate';
+import 'angular-translate-interpolation-messageformat';
+import 'oclazyload';
+import 'bootstrap';
+import 'bootstrap-switch';
+
+import 'angular-patternfly/node_modules/patternfly/node_modules/jquery/dist/jquery.js';
+import 'angular-patternfly/node_modules/patternfly/node_modules/datatables.net/js/jquery.dataTables.js';
+import 'angular-patternfly/node_modules/patternfly/node_modules/datatables.net-select/js/dataTables.select.js';
+import 'angularjs-datatables/dist/angular-datatables.min.js';
+import 'angularjs-datatables/dist/plugins/select/angular-datatables.select.min.js';
+
+import {default as authModule, config as authModSetup} from 'components/auth/auth.module.js';
+
+require.ensure([], () => {
+  require('angular-patternfly/node_modules/datatables.net-dt/css/jquery.dataTables.css');
+  require('angular-patternfly/node_modules/patternfly/dist/css/patternfly.css');
+  require('angular-patternfly/node_modules/patternfly/dist/css/patternfly-additions.css');
+  require('bootstrap-switch/dist/css/bootstrap3/bootstrap-switch.css');
+  require('scss/app.scss');
+});
+
+const angApp = 'appModule';
+export default angApp;
+
+/* istanbul ignore next */
+function initializeApplication () {
+  require('shared/services/services.module.js').init();
+  require('shared/config/config.module.js').init();
+  let authInterceptorFactory = require('./auth-interceptor.factory.js').default;
+  return angular
+    .module(angApp, [
+      'ui.router',
+      'ui.bootstrap',
+      'patternfly',
+      'patternfly.navigation',
+      'patternfly.table',
+      angularTranslate,
+      authModule,
+      authInterceptorFactory,
+      // non-core modules
+      require('./app.routing.js').default,
+      require('./app-root.component.js').default
+    ])
+    .config($httpProvider => {
+      'ngInject';
+      $httpProvider.interceptors.push(authInterceptorFactory);
+    })
+    .config($translateProvider => {
+      'ngInject';
+      $translateProvider
+        .useSanitizeValueStrategy('escapeParameters')
+        .addInterpolation('$translateMessageFormatInterpolation')
+        .registerAvailableLanguageKeys(['en'], {
+          'en_*': 'en'
+        })
+        .fallbackLanguage('en')
+        .determinePreferredLanguage();
+
+      let req = require.context('./', true, /\/([a-z]{2})\.locale\.yaml$/);
+      req.keys().map(key => {
+        let lang = /\/([a-z]{2})\.locale\.yaml$/.exec(key)[1];
+        let translations = req(key);
+        $translateProvider.translations(lang, translations);
+      });
+    })
+    .name;
+}
+
+/* istanbul ignore next */
+export function doInit () {
+  return new Promise((resolve, reject) => {
+    let appModule = initializeApplication();
+    authModSetup(process.env.NODE_ENV, () => resolve());
+  });
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/app/app-root.component.js	Fri Oct 13 14:05:09 2017 -0400
@@ -0,0 +1,36 @@
+/**
+ * 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 './app-root.controller.js';
+
+export default angular
+  .module('appRoot.component', [controller])
+  .component('appRoot', {
+    controller: 'AppRootController',
+    template: require('./app-root.html')
+  })
+  .name;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/app/app-root.controller.js	Fri Oct 13 14:05:09 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.
+ */
+
+import authModule from 'components/auth/auth.module.js';
+
+class AppRootController {
+  constructor ($scope, environment, authService, $translate) {
+    'ngInject';
+    this.env = environment;
+    this._authService = authService;
+    this._translate = $translate;
+
+    $scope.$on('userLoginChanged', () => this._updateUsernameLabel());
+  }
+
+  $onInit () {
+    angular.element(document.querySelector('#logoutButton')).removeAttr('hidden');
+    if (this.env !== 'production') {
+      angular.element(document.querySelector('#envHeader')).removeAttr('hidden');
+    }
+
+    this._updateUsernameLabel();
+
+    this._translate([
+      'navbar.states.JVM_LISTING',
+      'navbar.states.MULTICHARTS'
+    ]).then(translations => {
+      this.navigationItems = [
+        {
+          title: translations['navbar.states.JVM_LISTING'],
+          iconClass: 'fa pficon-domain',
+          uiSref: 'jvmList'
+        },
+        {
+          title: translations['navbar.states.MULTICHARTS'],
+          iconClass: 'fa pficon-trend-up',
+          uiSref: 'multichart'
+        }
+      ];
+    });
+  }
+
+  get loginStatus () {
+    return this._authService.status();
+  }
+
+  logout () {
+    return this._authService.logout();
+  }
+
+  _updateUsernameLabel () {
+    this.username = this._authService.username;
+  }
+
+}
+
+export default angular
+  .module('appRoot.controller', [authModule])
+  .controller('AppRootController', AppRootController)
+  .name;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/app/app-root.controller.spec.js	Fri Oct 13 14:05:09 2017 -0400
@@ -0,0 +1,170 @@
+/**
+ * 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 { default as authModule, config } from './components/auth/auth.module.js';
+import controllerModule from './app-root.controller.js';
+
+describe('AppRootController', () => {
+
+  beforeEach(angular.mock.module($provide => {
+    'ngInject';
+    $provide.value('$transitions', { onBefore: angular.noop });
+
+    let localStorage = {
+      getItem: sinon.stub(),
+      hasItem: sinon.stub(),
+      removeItem: sinon.spy(),
+      setItem: sinon.spy(),
+      clear: sinon.spy()
+    };
+    $provide.value('localStorage', localStorage);
+  }));
+
+  beforeEach(() => {
+    angular.mock.module(authModule);
+    config();
+    angular.mock.module(controllerModule);
+  });
+
+  ['testing', 'development', 'production'].forEach(env => {
+    describe(env, () => {
+      let ctrl, scope, authService, translate;
+      beforeEach(inject($controller => {
+        'ngInject';
+
+        scope = { $on: sinon.spy() };
+        authService = {
+          status: sinon.stub().returns(true),
+          login: sinon.spy(),
+          logout: sinon.spy()
+        };
+        translate = sinon.stub().returns({
+          then: sinon.stub().yields(
+            {
+              'navbar.states.JVM_LISTING': 'JVM Listing',
+              'navbar.states.MULTICHARTS': 'Multicharts'
+            }
+          )
+        });
+
+        ctrl = $controller('AppRootController', {
+          environment: env,
+          $scope: scope,
+          authService: authService,
+          $translate: translate
+        });
+        ctrl.$onInit();
+      }));
+
+      it('should set loginStatus', () => {
+        ctrl.loginStatus.should.be.True();
+        authService.status.should.be.calledOnce();
+      });
+    });
+  });
+
+  describe('logout()', () => {
+    let ctrl, scope, authService, translate;
+    beforeEach(inject($controller => {
+      'ngInject';
+
+      scope = { $on: sinon.spy() };
+      authService = {
+        status: sinon.stub().returns(true),
+        login: sinon.spy(),
+        logout: sinon.spy()
+      };
+      translate = sinon.stub().returns({
+        then: sinon.stub().yields(
+          {
+            'navbar.states.JVM_LISTING': 'JVM Listing',
+            'navbar.states.MULTICHARTS': 'Multicharts'
+          }
+        )
+      });
+
+      ctrl = $controller('AppRootController', {
+        environment: 'testing',
+        $scope: scope,
+        authService: authService,
+        $translate: translate
+      });
+      ctrl.$onInit();
+    }));
+
+    it('should delegate to AuthService', () => {
+      authService.logout.should.not.be.called();
+      ctrl.logout();
+      authService.logout.should.be.calledOnce();
+    });
+  });
+
+  describe('username', () => {
+    let rootScope, scope, ctrl, authService, translate;
+    beforeEach(inject(($controller, $rootScope) => {
+      'ngInject';
+
+      rootScope = $rootScope;
+      scope = $rootScope.$new();
+      authService = {
+        status: sinon.stub().returns(true),
+        login: sinon.spy(),
+        logout: sinon.spy(),
+        username: 'fake-username'
+      };
+      translate = sinon.stub().returns({
+        then: sinon.stub().yields(
+          {
+            'navbar.states.JVM_LISTING': 'JVM Listing',
+            'navbar.states.MULTICHARTS': 'Multicharts'
+          }
+        )
+      });
+
+      ctrl = $controller('AppRootController', {
+        $scope: scope,
+        environment: 'testing',
+        authService: authService,
+        $translate: translate
+      });
+      ctrl.$onInit();
+    }));
+
+    it('should be set on init', () => {
+      ctrl.should.have.ownProperty('username');
+      ctrl.username.should.equal(authService.username);
+    });
+
+    it('should be set on userLoginChanged according to authService username', () => {
+      authService.username = 'new-username';
+      rootScope.$broadcast('userLoginChanged');
+      ctrl.should.have.ownProperty('username');
+      ctrl.username.should.equal(authService.username);
+    });
+  });
+
+});
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/app/app-root.html	Fri Oct 13 14:05:09 2017 -0400
@@ -0,0 +1,97 @@
+<div class="" ng-controller="AppRootController as $ctrl">
+
+  <div ng-if="!$ctrl.loginStatus">
+
+    <nav class="navbar navbar-pf-vertical">
+
+      <div class="navbar-header">
+        <a ui-sref="landing" class="navbar-brand">
+          <img class="navbar-brand-icon" src="~images/thermostat_logo_white_600px.png" height="57" alt="Thermostat"/>
+        </a>
+        <p hidden class="navbar-text label label-info infotip">{{$ctrl.env}}</p>
+      </div>
+
+      <nav class="collapse navbar-collapse">
+        <ul class="nav navbar-nav navbar-right navbar-iconic navbar-utility">
+          <li class="dropdown">
+            <a class="dropdown-toggle nav-item-iconic" id="infoDropdown" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
+              <span translate-attr="{title: 'navbar.HELP'}" class="fa pficon-help"></span>
+              <span class="caret"></span>
+            </a>
+            <ul class="dropdown-menu" aria-labelledby="infoDropdown">
+              <li><a translate-attr="{href: 'navbar.HELP_URL'}" translate>navbar.HELP</a></li>
+              <li><a ui-sref="about" translate>navbar.ABOUT</a></li>
+            </ul>
+          </li>
+        </ul>
+      </nav>
+
+    </nav>
+
+    <div class="container-pf-nav-pf-vertical-with-tertiary">
+      <div class="pf-framework-content">
+        <main ui-view class="main pf-framework-view">
+        </main>
+      </div>
+    </div>
+
+  </div><!--!$ctrl.loginStatus-->
+
+  <div ng-if="$ctrl.loginStatus">
+    <div class="layout-pf layout-pf-fixed faux-layout">
+      <pf-vertical-navigation
+         items="$ctrl.navigationItems"
+         show-badges="true"
+         pinnable-menus="true"
+         update-active-items-on-click="true"
+         >
+         <div class="navbar-header">
+           <a ui-sref="landing" class="navbar-brand">
+             <img id="brandLogoImg" class="navbar-brand-icon" src="~images/thermostat_logo_white_600px.png" height="57" alt="Thermostat"/>
+           </a>
+        <p hidden id="envHeader" class="navbar-text label label-info infotip">{{$ctrl.env}}</p>
+         </div>
+
+         <div>
+           <ul class="nav navbar-nav navbar-right navbar-iconic">
+
+             <li class="dropdown">
+             </li>
+             <li class="dropdown">
+               <a class="dropdown-toggle nav-item-iconic" id="helpMenu" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
+                 <span title="Help" class="fa pficon-help"></span>
+                 <span class="caret"></span>
+               </a>
+               <ul class="dropdown-menu" aria-labelledby="helpMenu">
+                 <li><a translate-attr="{href: 'navbar.HELP_URL'}" translate>navbar.HELP</a></li>
+                 <li><a ui-sref="about" translate>navbar.ABOUT</a></li>
+               </ul>
+             </li>
+
+             <li ng-if="$ctrl.loginStatus" class="dropdown">
+               <a class="dropdown-toggle nav-item-iconic" id="userMenu" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
+                 <span translate-attr="{title: 'navbar.USERNAME'}" class="fa pficon-user"></span>
+                 {{$ctrl.username}}
+                 <span class="caret"></span>
+               </a>
+               <ul class="dropdown-menu" aria-labelledby="userMenu">
+                 <li><a ui-sref="user-prefs" translate>navbar.USER_PREFS</a></li>
+                 <li><a id="logoutButton" ng-click="$ctrl.logout()" style="cursor: pointer; cursor: hand" translate>navbar.LOGOUT</a></li>
+               </ul>
+             </li>
+
+           </ul>
+         </div>
+
+         </pf-vertical-navigation>
+         <div class="container-fluid container-cards-pf container-pf-nav-pf-vertical">
+           <div class="pf-framework-content">
+             <main ui-view class="main pf-framework-view">
+             </main>
+           </div>
+         </div>
+    </div>
+  </div><!--$ctrl.loginStatus-->
+
+</div>
+
--- a/src/app/app.controller.js	Fri Oct 13 11:39:51 2017 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,85 +0,0 @@
-/**
- * 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 authModule from 'components/auth/auth.module.js';
-
-class AppController {
-  constructor ($scope, environment, authService, $translate) {
-    'ngInject';
-    this.env = environment;
-    this._authService = authService;
-    this._translate = $translate;
-
-    $scope.$on('userLoginChanged', () => this._updateUsernameLabel());
-  }
-
-  $onInit () {
-    angular.element('logoutButton').removeAttr('hidden');
-    if (this.env !== 'production') {
-      angular.element('envHeader').removeAttr('hidden');
-    }
-
-    this._updateUsernameLabel();
-
-    this._translate([
-      'navbar.states.JVM_LISTING',
-      'navbar.states.MULTICHARTS'
-    ]).then(translations => {
-      this.navigationItems = [
-        {
-          title: translations['navbar.states.JVM_LISTING'],
-          iconClass: 'fa pficon-domain',
-          uiSref: 'jvmList'
-        },
-        {
-          title: translations['navbar.states.MULTICHARTS'],
-          iconClass: 'fa pficon-trend-up',
-          uiSref: 'multichart'
-        }
-      ];
-    });
-  }
-
-  get loginStatus () {
-    return this._authService.status();
-  }
-
-  logout () {
-    return this._authService.logout();
-  }
-
-  _updateUsernameLabel () {
-    this.username = this._authService.username;
-  }
-
-}
-
-let name = 'AppController';
-export default angular
-  .module(name, [authModule])
-  .controller(name, AppController)
-  .name;
--- a/src/app/app.controller.spec.js	Fri Oct 13 11:39:51 2017 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,163 +0,0 @@
-/**
- * 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('AppController', () => {
-
-  beforeEach(angular.mock.module($provide => {
-    'ngInject';
-    $provide.value('$transitions', { onBefore: angular.noop });
-
-    let localStorage = {
-      getItem: sinon.stub(),
-      hasItem: sinon.stub(),
-      removeItem: sinon.spy(),
-      setItem: sinon.spy(),
-      clear: sinon.spy()
-    };
-    $provide.value('localStorage', localStorage);
-  }));
-
-  beforeEach(angular.mock.module('AppController'));
-
-  ['testing', 'development', 'production'].forEach(env => {
-    describe(env, () => {
-      let ctrl, scope, authService, translate;
-      beforeEach(inject($controller => {
-        'ngInject';
-
-        scope = { $on: sinon.spy() };
-        authService = {
-          status: sinon.stub().returns(true),
-          login: sinon.spy(),
-          logout: sinon.spy()
-        };
-        translate = sinon.stub().returns({
-          then: sinon.stub().yields(
-            {
-              'navbar.states.JVM_LISTING': 'JVM Listing',
-              'navbar.states.MULTICHARTS': 'Multicharts'
-            }
-          )
-        });
-
-        ctrl = $controller('AppController', {
-          environment: env,
-          $scope: scope,
-          authService: authService,
-          $translate: translate
-        });
-        ctrl.$onInit();
-      }));
-
-      it('should set loginStatus', () => {
-        ctrl.loginStatus.should.be.True();
-        authService.status.should.be.calledOnce();
-      });
-    });
-  });
-
-  describe('logout()', () => {
-    let ctrl, scope, authService, translate;
-    beforeEach(inject($controller => {
-      'ngInject';
-
-      scope = { $on: sinon.spy() };
-      authService = {
-        status: sinon.stub().returns(true),
-        login: sinon.spy(),
-        logout: sinon.spy()
-      };
-      translate = sinon.stub().returns({
-        then: sinon.stub().yields(
-          {
-            'navbar.states.JVM_LISTING': 'JVM Listing',
-            'navbar.states.MULTICHARTS': 'Multicharts'
-          }
-        )
-      });
-
-      ctrl = $controller('AppController', {
-        environment: 'testing',
-        $scope: scope,
-        authService: authService,
-        $translate: translate
-      });
-      ctrl.$onInit();
-    }));
-
-    it('should delegate to AuthService', () => {
-      authService.logout.should.not.be.called();
-      ctrl.logout();
-      authService.logout.should.be.calledOnce();
-    });
-  });
-
-  describe('username', () => {
-    let rootScope, scope, ctrl, authService, translate;
-    beforeEach(inject(($controller, $rootScope) => {
-      'ngInject';
-
-      rootScope = $rootScope;
-      scope = $rootScope.$new();
-      authService = {
-        status: sinon.stub().returns(true),
-        login: sinon.spy(),
-        logout: sinon.spy(),
-        username: 'fake-username'
-      };
-      translate = sinon.stub().returns({
-        then: sinon.stub().yields(
-          {
-            'navbar.states.JVM_LISTING': 'JVM Listing',
-            'navbar.states.MULTICHARTS': 'Multicharts'
-          }
-        )
-      });
-
-      ctrl = $controller('AppController', {
-        $scope: scope,
-        environment: 'testing',
-        authService: authService,
-        $translate: translate
-      });
-      ctrl.$onInit();
-    }));
-
-    it('should be set on init', () => {
-      ctrl.should.have.ownProperty('username');
-      ctrl.username.should.equal(authService.username);
-    });
-
-    it('should be set on userLoginChanged according to authService username', () => {
-      authService.username = 'new-username';
-      rootScope.$broadcast('userLoginChanged');
-      ctrl.should.have.ownProperty('username');
-      ctrl.username.should.equal(authService.username);
-    });
-  });
-
-});
--- a/src/app/app.module.js	Fri Oct 13 11:39:51 2017 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,115 +0,0 @@
-/**
- * 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 'angular-patternfly';
-import '@uirouter/angularjs';
-import angularTranslate from 'angular-translate';
-import 'angular-translate-interpolation-messageformat';
-import 'oclazyload';
-import 'bootstrap';
-import 'bootstrap-switch';
-
-import 'angular-patternfly/node_modules/patternfly/node_modules/jquery/dist/jquery.js';
-import 'angular-patternfly/node_modules/patternfly/node_modules/datatables.net/js/jquery.dataTables.js';
-import 'angular-patternfly/node_modules/patternfly/node_modules/datatables.net-select/js/dataTables.select.js';
-import 'angularjs-datatables/dist/angular-datatables.min.js';
-import 'angularjs-datatables/dist/plugins/select/angular-datatables.select.min.js';
-
-import {default as authModule, config as authModBootstrap} from 'components/auth/auth.module.js';
-import authInterceptorFactory from './auth-interceptor.factory.js';
-
-require.ensure([], () => {
-  require('angular-patternfly/node_modules/datatables.net-dt/css/jquery.dataTables.css');
-  require('angular-patternfly/node_modules/patternfly/dist/css/patternfly.css');
-  require('angular-patternfly/node_modules/patternfly/dist/css/patternfly-additions.css');
-  require('bootstrap-switch/dist/css/bootstrap3/bootstrap-switch.css');
-  require('scss/app.scss');
-});
-
-function initializeApplication () {
-  return angular
-    .module('appModule', [
-      'ui.router',
-      'ui.bootstrap',
-      'patternfly',
-      'patternfly.navigation',
-      'patternfly.table',
-      angularTranslate,
-      authModule,
-      // non-core modules
-      require('./app.routing.js').default,
-      require('./app.controller.js').default,
-      authInterceptorFactory
-    ])
-    .config($httpProvider => {
-      'ngInject';
-      $httpProvider.interceptors.push(authInterceptorFactory);
-    })
-    .config($translateProvider => {
-      'ngInject';
-      $translateProvider
-        .useSanitizeValueStrategy('escapeParameters')
-        .addInterpolation('$translateMessageFormatInterpolation')
-        .registerAvailableLanguageKeys(['en'], {
-          'en_*': 'en'
-        })
-        .fallbackLanguage('en')
-        .determinePreferredLanguage();
-
-      let req = require.context('./', true, /\/([a-z]{2})\.locale\.yaml$/);
-      req.keys().map(key => {
-        let lang = /\/([a-z]{2})\.locale\.yaml$/.exec(key)[1];
-        let translations = req(key);
-        $translateProvider.translations(lang, translations);
-      });
-    })
-    .name;
-}
-
-/* istanbul ignore next */
-if (window.tmsGatewayUrl) {
-  let appModule = initializeApplication();
-  authModBootstrap(process.env.NODE_ENV, () => angular.element(() => angular.bootstrap(document, [appModule])));
-} else {
-  $.get('/gatewayurl')
-    .done(res => {
-      window.tmsGatewayUrl = res.gatewayUrl;
-    })
-    .fail(() => {
-      let url = require('url');
-      let parsed = url.parse(window.location.href);
-      let gateway = {
-        protocol: parsed.protocol,
-        host: parsed.host
-      };
-      window.tmsGatewayUrl = url.format(gateway);
-    })
-    .always(() => {
-      let appModule = initializeApplication();
-      authModBootstrap(process.env.NODE_ENV, () => angular.element(() => angular.bootstrap(document, [appModule])));
-    });
-}
--- a/src/app/app.module.spec.js	Fri Oct 13 11:39:51 2017 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,79 +0,0 @@
-/**
- * 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 './components/auth/auth.module.js';
-
-describe('AppModule', () => {
-
-  beforeEach(angular.mock.module('appModule'));
-
-  let state;
-  beforeEach(angular.mock.module($provide => {
-    'ngInject';
-
-    state = {
-      go: sinon.spy(),
-      href: sinon.spy(),
-      current: {
-        name: 'fooState'
-      },
-      params: {}
-    };
-
-    $provide.value('$state', state);
-  }));
-
-  // this is actually provided by the auth.module pseudo-module - see auth.module.spec.js
-  describe('auth bootstrap', () => {
-
-    let svc;
-    beforeEach(() => {
-      let mockKeycloakProvider = require('./components/auth/keycloak.stub.js');
-      config('production', () => {}, mockKeycloakProvider());
-
-      inject(authService => {
-        'ngInject';
-        svc = authService;
-      });
-    });
-
-    it('should provide an authService', () => {
-      should.exist(svc);
-
-      svc.should.have.property('status');
-      svc.should.have.property('login');
-      svc.should.have.property('logout');
-      svc.should.have.property('refresh');
-
-      svc.status.should.be.a.Function();
-      svc.login.should.be.a.Function();
-      svc.logout.should.be.a.Function();
-      svc.refresh.should.be.a.Function();
-    });
-  });
-
-});
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/app/app.module.ts	Fri Oct 13 14:05:09 2017 -0400
@@ -0,0 +1,68 @@
+/**
+ * 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 * as angular from "angular";
+import { default as angApp, doInit } from "./ang-app.module.js";
+
+import {
+  NgModule,
+  Inject,
+  forwardRef
+} from "@angular/core";
+
+import { BrowserModule } from "@angular/platform-browser";
+import { HttpClientModule } from "@angular/common/http";
+import { UpgradeModule } from "@angular/upgrade/static";
+import { UpgradeAdapter } from "@angular/upgrade";
+
+import { ServicesModule } from "./shared/services/services.module";
+import { FiltersModule } from "./shared/filters/filters.module";
+
+const upgradeAdapter = new UpgradeAdapter(forwardRef(() => AppModule));
+@NgModule({
+  declarations: [],
+  imports: [
+    // Angular core modules
+    BrowserModule,
+    HttpClientModule,
+    UpgradeModule,
+
+    ServicesModule,
+    FiltersModule,
+  ]
+})
+export class AppModule {
+
+  constructor(@Inject(UpgradeModule) private upgradeModule: UpgradeModule) {}
+
+  public ngDoBootstrap() {
+    doInit().then(() => {
+      this.upgradeModule.bootstrap(document.body, [angApp], { strictDi: true });
+    });
+  }
+
+}
--- a/src/app/app.routing.js	Fri Oct 13 11:39:51 2017 -0400
+++ b/src/app/app.routing.js	Fri Oct 13 14:05:09 2017 -0400
@@ -79,4 +79,4 @@
 appRouter.run(transitionHook);
 export default appRouter.name;
 
-export { errorRouter, errorRouting, transitionHook };
+export { defaultState, errorRouter, errorRouting, transitionHook };
--- a/src/app/app.routing.spec.js	Fri Oct 13 11:39:51 2017 -0400
+++ b/src/app/app.routing.spec.js	Fri Oct 13 14:05:09 2017 -0400
@@ -144,6 +144,14 @@
         fn({ name: 'login' }).should.be.false();
       });
 
+      it('should match non-auth\'d transitions', () => {
+        authSvc.status = () => false;
+        transitions.onBefore.args[1][0].should.have.ownProperty('to');
+        let fn = transitions.onBefore.args[1][0].to;
+        fn.should.be.a.Function();
+        fn({ name: 'foo' }).should.be.true();
+      });
+
       it('should provide a transition function', () => {
         transitions.onBefore.args[1][1].should.be.a.Function();
       });
@@ -177,4 +185,13 @@
     });
   });
 
+  describe('defaultState', () => {
+    it('should add a \'default\' state which redirects to jvmList', () => {
+      stateProvider.state.should.be.calledOnce();
+      module.defaultState(stateProvider);
+      stateProvider.state.should.be.calledTwice();
+      stateProvider.state.secondCall.should.be.calledWithMatch('default', { redirectTo: 'jvmList' });
+    });
+  });
+
 });
--- a/src/app/auth-interceptor.factory.js	Fri Oct 13 11:39:51 2017 -0400
+++ b/src/app/auth-interceptor.factory.js	Fri Oct 13 14:05:09 2017 -0400
@@ -26,11 +26,15 @@
  */
 
 import authModule from 'components/auth/auth.module.js';
+import servicesModule from 'shared/services/services.module.js';
 
 let name = 'authInterceptorFactory';
 
 export default angular
-  .module(name, [authModule])
+  .module(name, [
+    authModule,
+    servicesModule
+  ])
   .factory(name, ($q, authService) => {
     'ngInject';
     return {
--- a/src/app/auth-interceptor.factory.spec.js	Fri Oct 13 11:39:51 2017 -0400
+++ b/src/app/auth-interceptor.factory.spec.js	Fri Oct 13 14:05:09 2017 -0400
@@ -25,10 +25,13 @@
  * exception statement from your version.
  */
 
+import factoryModule from './auth-interceptor.factory.js';
+
 describe('authInterceptorFactory', () => {
 
   let authSvc, refreshPromise, interceptor;
   beforeEach(() => {
+    angular.mock.module(factoryModule);
     angular.mock.module('authModule', $provide => {
       'ngInject';
 
--- a/src/app/components/about/about.controller.spec.js	Fri Oct 13 11:39:51 2017 -0400
+++ b/src/app/components/about/about.controller.spec.js	Fri Oct 13 14:05:09 2017 -0400
@@ -26,11 +26,14 @@
  */
 
 import controllerModule from './about.controller.js';
+import { default as servicesModule, init as initServices } from 'shared/services/services.module.js';
 
 describe('AboutController', () => {
 
   let ctrl, authSvc;
   beforeEach(() => {
+    angular.mock.module(servicesModule);
+    initServices();
     angular.mock.module(controllerModule);
     angular.mock.inject($controller => {
       'ngInject';
--- a/src/app/components/jvm-info/jvm-gc/jvm-gc.controller.spec.js	Fri Oct 13 11:39:51 2017 -0400
+++ b/src/app/components/jvm-info/jvm-gc/jvm-gc.controller.spec.js	Fri Oct 13 14:05:09 2017 -0400
@@ -25,49 +25,56 @@
  * exception statement from your version.
  */
 
-describe('JvmGcController', () => {
+import controllerModule from './jvm-gc.controller.js';
+import filtersModule from 'shared/filters/filters.module.js';
+import { default as servicesModule, init as initServices } from 'shared/services/services.module.js';
 
-  beforeEach(angular.mock.module('app.filters'));
-  beforeEach(angular.mock.module('jvmGc.controller'));
+describe('JvmGcController', () => {
 
   let interval, dateFilterStub, dateFormatSpy, svc, promise, ctrl, translate, sanitizeService;
-  beforeEach(inject(($controller) => {
-    'ngInject';
+  beforeEach(() => {
+    angular.mock.module(filtersModule);
+    angular.mock.module(servicesModule);
+    initServices();
+    angular.mock.module(controllerModule);
+    angular.mock.inject(($controller) => {
+      'ngInject';
 
-    dateFilterStub = sinon.stub().returns('mockDate');
-    dateFormatSpy = {
-      time: {
-        medium: sinon.spy()
-      }
-    };
+      dateFilterStub = sinon.stub().returns('mockDate');
+      dateFormatSpy = {
+        time: {
+          medium: sinon.spy()
+        }
+      };
 
-    interval = sinon.stub().returns('interval-sentinel');
-    interval.cancel = sinon.spy();
+      interval = sinon.stub().returns('interval-sentinel');
+      interval.cancel = sinon.spy();
 
-    promise = { then: sinon.spy() };
-    svc = { getJvmGcData: sinon.stub().returns(promise) };
-
-    sanitizeService = { sanitize: sinon.spy() };
+      promise = { then: sinon.spy() };
+      svc = { getJvmGcData: sinon.stub().returns(promise) };
 
-    translate = sinon.stub().returns({
-      then: sinon.stub().yields({
-        'jvmGc.chart.UNITS': 'microseconds',
-        'jvmGc.chart.X_AXIS_LABEL': 'timestamp',
-        'jvmGc.chart.Y_AXIS_LABEL': 'elapsed'
-      })
-    });
+      sanitizeService = { sanitize: sinon.spy() };
+
+      translate = sinon.stub().returns({
+        then: sinon.stub().yields({
+          'jvmGc.chart.UNITS': 'microseconds',
+          'jvmGc.chart.X_AXIS_LABEL': 'timestamp',
+          'jvmGc.chart.Y_AXIS_LABEL': 'elapsed'
+        })
+      });
 
-    ctrl = $controller('JvmGcController', {
-      $stateParams: { jvmId: 'foo-jvmId' },
-      $interval: interval,
-      dateFilter: dateFilterStub,
-      DATE_FORMAT: dateFormatSpy,
-      jvmGcService: svc,
-      sanitizeService: sanitizeService,
-      $translate: translate
+      ctrl = $controller('JvmGcController', {
+        $stateParams: { jvmId: 'foo-jvmId' },
+        $interval: interval,
+        dateFilter: dateFilterStub,
+        DATE_FORMAT: dateFormatSpy,
+        jvmGcService: svc,
+        sanitizeService: sanitizeService,
+        $translate: translate
+      });
+      ctrl.$onInit();
     });
-    ctrl.$onInit();
-  }));
+  });
 
   it('should exist', () => {
     should.exist(ctrl);
--- a/src/app/components/jvm-info/jvm-info.controller.spec.js	Fri Oct 13 11:39:51 2017 -0400
+++ b/src/app/components/jvm-info/jvm-info.controller.spec.js	Fri Oct 13 14:05:09 2017 -0400
@@ -25,12 +25,14 @@
  * exception statement from your version.
  */
 
+import controllerModule from './jvm-info.controller.js';
+
 describe('JvmInfoController', () => {
 
-  beforeEach(angular.mock.module('jvmInfo.controller'));
+  beforeEach(angular.mock.module(controllerModule));
 
   let state, jvmInfoService, killVmService, ctrl, infoPromise, killPromise, systemInfoService, systemInfoPromise, translate;
-  beforeEach(inject($controller => {
+  beforeEach(angular.mock.inject($controller => {
     'ngInject';
 
     state = { go: sinon.spy() };
--- a/src/app/components/jvm-info/jvm-memory/jvm-memory.controller.spec.js	Fri Oct 13 11:39:51 2017 -0400
+++ b/src/app/components/jvm-info/jvm-memory/jvm-memory.controller.spec.js	Fri Oct 13 14:05:09 2017 -0400
@@ -25,46 +25,54 @@
  * exception statement from your version.
  */
 
-describe('JvmMemory controller', () => {
+import controllerModule from './jvm-memory.controller.js';
+import filtersModule from 'shared/filters/filters.module.js';
+import { default as servicesModule, init as initServices } from 'shared/services/services.module.js';
 
-  beforeEach(angular.mock.module('jvmMemory.controller'));
+describe('JvmMemory controller', () => {
 
   let interval, memSvc, scaleSvc, promise, ctrl, sanitizeSvc;
-  beforeEach(inject($controller => {
-    'ngInject';
+  beforeEach(() => {
+    angular.mock.module(filtersModule);
+    angular.mock.module(servicesModule);
+    initServices();
+    angular.mock.module(controllerModule);
+    angular.mock.inject($controller => {
+      'ngInject';
 
-    interval = sinon.stub().returns('interval-sentinel');
-    interval.cancel = sinon.spy();
+      interval = sinon.stub().returns('interval-sentinel');
+      interval.cancel = sinon.spy();
 
-    promise = {
-      then: sinon.spy()
-    };
-    memSvc = {
-      getJvmMemory: sinon.stub().returns(promise)
-    };
-    scaleSvc = {
-      format: sinon.stub().returns({
-        scale: 1024 * 1024,
-        unit: 'MiB'
-      })
-    };
+      promise = {
+        then: sinon.spy()
+      };
+      memSvc = {
+        getJvmMemory: sinon.stub().returns(promise)
+      };
+      scaleSvc = {
+        format: sinon.stub().returns({
+          scale: 1024 * 1024,
+          unit: 'MiB'
+        })
+      };
 
-    sanitizeSvc = {
-      sanitize: sinon.stub().returns('sanitized-mock')
-    };
+      sanitizeSvc = {
+        sanitize: sinon.stub().returns('sanitized-mock')
+      };
 
-    ctrl = $controller('JvmMemoryController', {
-      $stateParams: { jvmId: 'foo-jvmId' },
-      $interval: interval,
-      jvmMemoryService: memSvc,
-      scaleBytesService: scaleSvc,
-      sanitizeService: sanitizeSvc
+      ctrl = $controller('JvmMemoryController', {
+        $stateParams: { jvmId: 'foo-jvmId' },
+        $interval: interval,
+        jvmMemoryService: memSvc,
+        scaleBytesService: scaleSvc,
+        sanitizeService: sanitizeSvc
+      });
+      ctrl.$onInit();
+
+      sinon.spy(ctrl, '_update');
+      sinon.spy(ctrl, '_stop');
     });
-    ctrl.$onInit();
-
-    sinon.spy(ctrl, '_update');
-    sinon.spy(ctrl, '_stop');
-  }));
+  });
 
   afterEach(() => {
     ctrl._update.restore();
--- a/src/app/components/jvm-info/kill-vm.service.spec.js	Fri Oct 13 11:39:51 2017 -0400
+++ b/src/app/components/jvm-info/kill-vm.service.spec.js	Fri Oct 13 14:05:09 2017 -0400
@@ -25,6 +25,7 @@
  * exception statement from your version.
  */
 
+import serviceModule from './kill-vm.service.js';
 import servicesModule from 'shared/services/services.module.js';
 
 describe('KillVmService', () => {
@@ -36,6 +37,7 @@
       'ngInject';
       $provide.value('commandChannelService', commandChannel);
     });
+    angular.mock.module(serviceModule);
     angular.mock.inject((killVmService, $rootScope, $q) => {
       'ngInject';
       scope = $rootScope;
--- a/src/app/components/jvm-list/jvm-list.controller.spec.js	Fri Oct 13 11:39:51 2017 -0400
+++ b/src/app/components/jvm-list/jvm-list.controller.spec.js	Fri Oct 13 14:05:09 2017 -0400
@@ -25,9 +25,11 @@
  * exception statement from your version.
  */
 
+import controllerModule from './jvm-list.controller.js';
+
 describe('JvmListController', () => {
 
-  beforeEach(angular.mock.module('jvmList.controller'));
+  beforeEach(angular.mock.module(controllerModule));
 
   let rootScope, controller, jvmListSvc, systemInfoSvc, promise, location, state, timeout, translate;
 
@@ -76,7 +78,7 @@
     };
   };
 
-  beforeEach(inject(($q, $rootScope, $controller) => {
+  beforeEach(angular.mock.inject(($q, $rootScope, $controller) => {
     'ngInject';
     rootScope = $rootScope;
     sinon.stub(angular, 'element').withArgs('#aliveOnlyState').returns({
--- a/src/app/index.html	Fri Oct 13 11:39:51 2017 -0400
+++ b/src/app/index.html	Fri Oct 13 14:05:09 2017 -0400
@@ -11,102 +11,7 @@
   </head>
   <body id="pf-app" class="pf-body apf-body ng-cloak">
 
-    <div class="" ng-controller="AppController as $ctrl">
-
-      <div ng-if="!$ctrl.loginStatus">
-
-        <nav class="navbar navbar-pf-vertical">
-
-          <div class="navbar-header">
-            <a ui-sref="default" style="cursor: pointer; cursor: hand" class="navbar-brand">
-              <img class="navbar-brand-icon" src="~images/thermostat_logo_white_600px.png" height="57" alt="Thermostat"/>
-            </a>
-            <p hidden class="navbar-text label label-info infotip">{{$ctrl.env}}</p>
-          </div>
-
-          <nav class="collapse navbar-collapse">
-            <ul class="nav navbar-nav navbar-right navbar-iconic navbar-utility">
-              <li class="dropdown">
-                <a class="dropdown-toggle nav-item-iconic" id="infoDropdown" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
-                  <span translate-attr="{title: 'navbar.HELP'}" class="fa pficon-help"></span>
-                  <span class="caret"></span>
-                </a>
-                <ul class="dropdown-menu" aria-labelledby="infoDropdown">
-                  <li><a translate-attr="{href: 'navbar.HELP_URL'}" translate>navbar.HELP</a></li>
-                  <li><a ui-sref="about" translate>navbar.ABOUT</a></li>
-                </ul>
-              </li>
-            </ul>
-          </nav>
-
-        </nav>
-
-        <div class="container-pf-nav-pf-vertical-with-tertiary">
-          <div class="pf-framework-content">
-            <main ui-view class="main pf-framework-view">
-            </main>
-          </div>
-        </div>
-
-      </div><!--!$ctrl.loginStatus-->
-
-      <div ng-if="$ctrl.loginStatus">
-        <div class="layout-pf layout-pf-fixed faux-layout">
-          <pf-vertical-navigation
-             items="$ctrl.navigationItems"
-             show-badges="true"
-             pinnable-menus="true"
-             update-active-items-on-click="true"
-             >
-             <div class="navbar-header">
-               <a ui-sref="default" style="cursor: pointer; cursor: hand" class="navbar-brand">
-                 <img id="brandLogoImg" class="navbar-brand-icon" src="~images/thermostat_logo_white_600px.png" height="57" alt="Thermostat"/>
-               </a>
-            <p hidden id="envHeader" class="navbar-text label label-info infotip">{{$ctrl.env}}</p>
-             </div>
-
-             <div>
-               <ul class="nav navbar-nav navbar-right navbar-iconic">
-
-                 <li class="dropdown">
-                 </li>
-                 <li class="dropdown">
-                   <a class="dropdown-toggle nav-item-iconic" id="helpMenu" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
-                     <span title="Help" class="fa pficon-help"></span>
-                     <span class="caret"></span>
-                   </a>
-                   <ul class="dropdown-menu" aria-labelledby="helpMenu">
-                     <li><a translate-attr="{href: 'navbar.HELP_URL'}" translate>navbar.HELP</a></li>
-                     <li><a ui-sref="about" translate>navbar.ABOUT</a></li>
-                   </ul>
-                 </li>
-
-                 <li ng-if="$ctrl.loginStatus" class="dropdown">
-                   <a class="dropdown-toggle nav-item-iconic" id="userMenu" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
-                     <span translate-attr="{title: 'navbar.USERNAME'}" class="fa pficon-user"></span>
-                     {{$ctrl.username}}
-                     <span class="caret"></span>
-                   </a>
-                   <ul class="dropdown-menu" aria-labelledby="userMenu">
-                     <li><a ui-sref="user-prefs" translate>navbar.USER_PREFS</a></li>
-                     <li><a id="logoutButton" ng-click="$ctrl.logout()" style="cursor: pointer; cursor: hand" translate>navbar.LOGOUT</a></li>
-                   </ul>
-                 </li>
-
-               </ul>
-             </div>
-
-             </pf-vertical-navigation>
-             <div class="container-fluid container-cards-pf container-pf-nav-pf-vertical">
-               <div class="pf-framework-content">
-                 <main ui-view class="main pf-framework-view">
-                 </main>
-               </div>
-             </div>
-        </div>
-      </div><!--$ctrl.loginStatus-->
-
-    </div>
+    <app-root></app-root>
 
   </body>
 </html>
--- a/src/app/shared/config/config.module.js	Fri Oct 13 11:39:51 2017 -0400
+++ b/src/app/shared/config/config.module.js	Fri Oct 13 14:05:09 2017 -0400
@@ -27,7 +27,7 @@
 
 import * as url from 'url';
 
-export function cmdChanUrl (gatewayUrl) {
+export function cmdChanUrl (gatewayUrl = window.tmsGatewayUrl) {
   if (!gatewayUrl) {
     throw new Error('gatewayUrl could not be determined');
   }
@@ -43,10 +43,15 @@
   return url.format(parsed);
 };
 
-export default angular
-  .module('configModule', [])
-  .constant('environment', process.env.NODE_ENV)
-  .constant('debug', process.env.DEBUG)
-  .value('gatewayUrl', window.tmsGatewayUrl)
-  .value('commandChannelUrl', cmdChanUrl(window.tmsGatewayUrl))
-  .name;
+let modName = 'configModule';
+let mod = angular.module(modName, []);
+export default modName;
+
+/* istanbul ignore next */
+export function init () {
+  mod
+    .constant('environment', process.env.NODE_ENV)
+    .constant('debug', process.env.DEBUG)
+    .value('gatewayUrl', window.tmsGatewayUrl)
+    .factory('commandChannelUrl', cmdChanUrl);
+}
--- a/src/app/shared/config/config.module.spec.js	Fri Oct 13 11:39:51 2017 -0400
+++ b/src/app/shared/config/config.module.spec.js	Fri Oct 13 14:05:09 2017 -0400
@@ -27,96 +27,11 @@
 
 describe('ConfigModule', () => {
 
-  beforeEach(() => {
-    angular.mock.module('configModule');
-  });
-
-  describe('environment', () => {
-    let _environment;
-    beforeEach(inject(environment => {
-      'ngInject';
-
-      _environment = environment;
-    }));
-
-    it('should be exported', () => {
-      should.exist(_environment);
-    });
-
-    it('should be readonly', done => {
-      try {
-        _environment.foo = 'bar';
-      } catch (e) {
-        e.message.should.equal('Attempted to assign to readonly property.');
-        done();
-      }
-    });
-  });
-
-  describe('debug', () => {
-    let _debug;
-    beforeEach(inject(debug => {
-      'ngInject';
-
-      _debug = debug;
-    }));
-
-    it('should be exported', () => {
-      should.exist(_debug);
-    });
-
-    it('should be readonly', done => {
-      try {
-        _debug.foo = 'bar';
-      } catch (e) {
-        e.message.should.equal('Attempted to assign to readonly property.');
-        done();
-      }
-    });
-  });
-
-  describe('gatewayUrl', () => {
-    let _gatewayUrl;
-    beforeEach(inject(gatewayUrl => {
-      'ngInject';
-
-      _gatewayUrl = gatewayUrl;
-    }));
-
-    it('should be exported', () => {
-      should.exist(_gatewayUrl);
-    });
-
-    it('should be readonly', done => {
-      try {
-        _gatewayUrl.foo = 'bar';
-      } catch (e) {
-        e.message.should.equal('Attempted to assign to readonly property.');
-        done();
-      }
-    });
-  });
-
   describe('commandChannelUrl', () => {
     let fn = require('./config.module.js').cmdChanUrl;
-    let _commandChannelUrl;
-    beforeEach(inject(commandChannelUrl => {
-      'ngInject';
 
-      _commandChannelUrl = commandChannelUrl;
-    }));
-
-    it('should be exported', () => {
-      should.exist(_commandChannelUrl);
-    });
-
-    it('should be readonly', done => {
-      try {
-        _commandChannelUrl.foo = 'bar';
-      } catch (e) {
-        e.message.should.equal('Attempted to assign to readonly property.');
-        done();
-      }
+    it('should use window.tmsGatewayUrl if none specified', () => {
+      fn().should.equal('ws://localhost:8888/');
     });
 
     it('should yield ws:// URL when gateway URL is http://', () => {
@@ -127,9 +42,9 @@
       fn('https://example.com:8888/').should.equal('wss://example.com:8888/');
     });
 
-    it('should throw error when gateway URL is undefined', done => {
+    it('should throw error when gateway URL is falsy', done => {
       try {
-        fn(undefined);
+        fn(null);
       } catch (e) {
         e.message.should.equal('gatewayUrl could not be determined');
         done();
--- a/src/app/shared/filters/big-int-to-string.filter.js	Fri Oct 13 11:39:51 2017 -0400
+++ b/src/app/shared/filters/big-int-to-string.filter.js	Fri Oct 13 14:05:09 2017 -0400
@@ -32,7 +32,7 @@
  * @param {Number}
  * @returns {String}
  */
-function filterProvider () {
+export function filterProvider () {
   return val => {
     val = val || 0;
     return val.toFixed();
--- a/src/app/shared/filters/big-int-to-string.filter.spec.js	Fri Oct 13 11:39:51 2017 -0400
+++ b/src/app/shared/filters/big-int-to-string.filter.spec.js	Fri Oct 13 14:05:09 2017 -0400
@@ -26,16 +26,18 @@
  */
 
 import big from 'big.js';
+import filtersModule from './filters.module.js';
 
 describe('bigIntToString filter', () => {
 
-  beforeEach(angular.mock.module('app.filters'));
-
   let fn;
-  beforeEach(angular.mock.inject(bigIntToStringFilter => {
-    'ngInject';
-    fn = bigIntToStringFilter;
-  }));
+  beforeEach(() => {
+    angular.mock.module(filtersModule);
+    angular.mock.inject(bigIntToStringFilter => {
+      'ngInject';
+      fn = bigIntToStringFilter;
+    });
+  });
 
   it('should convert big int to string', () => {
     fn(big(100)).should.equal('100');
@@ -46,7 +48,7 @@
   });
 
   it('should fail on object input', () => {
-    (() => fn({ foo: 'bar' })).should.throw(/undefined is not a constructor/);
+    (() => fn({ foo: 'bar' })).should.throw();
   });
 
   it('should treat undefined as 0', () => {
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/app/shared/filters/extract-class.pipe.spec.ts	Fri Oct 13 14:05:09 2017 -0400
@@ -0,0 +1,51 @@
+/**
+ * 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 * as sinon from 'sinon';
+
+import { ExtractClassPipe } from './extract-class.pipe';
+import { ExtractClassService } from "../services/extract-class.service";
+
+describe('ExtractClassPipe', () => {
+
+  let pipe: ExtractClassPipe;
+  let svc: ExtractClassService;
+  beforeEach(() => {
+    svc = {
+      extract: sinon.stub().returns('svc-result')
+    };
+    pipe = new ExtractClassPipe(svc);
+  });
+
+  it('should delegate to service', () => {
+    svc.extract.should.not.be.called();
+    pipe.transform('foo', true).should.equal('svc-result');
+    svc.extract.should.be.calledOnce();
+    svc.extract.should.be.calledWith('foo', true);
+  });
+
+});
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/app/shared/filters/extract-class.pipe.ts	Fri Oct 13 14:05:09 2017 -0400
@@ -0,0 +1,44 @@
+/**
+ * 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 {
+  Inject,
+  Pipe,
+  PipeTransform,
+} from "@angular/core";
+import { ExtractClassService } from "../services/extract-class.service";
+
+@Pipe({
+  name: "extractClass",
+})
+export class ExtractClassPipe implements PipeTransform {
+  constructor(@Inject(ExtractClassService) private svc: ExtractClassService) {}
+
+  public transform(value: string, includePkg: boolean): string {
+    return this.svc.extract(value, includePkg);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/app/shared/filters/filters.module.ts	Fri Oct 13 14:05:09 2017 -0400
@@ -0,0 +1,45 @@
+/**
+ * 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 { NgModule } from "@angular/core";
+import { ServicesModule } from "../services/services.module";
+
+import { ExtractClassPipe } from "./extract-class.pipe";
+import { FormatBytesPipe } from "./format-bytes.pipe";
+
+@NgModule({
+  declarations: [
+    ExtractClassPipe,
+    FormatBytesPipe,
+  ],
+  exports: [
+    ExtractClassPipe,
+    FormatBytesPipe,
+  ],
+  imports: [ ServicesModule ],
+})
+export class FiltersModule {}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/app/shared/filters/format-bytes.pipe.spec.ts	Fri Oct 13 14:05:09 2017 -0400
@@ -0,0 +1,58 @@
+/**
+ * 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 * as sinon from 'sinon';
+
+import { FormatBytesPipe } from './format-bytes.pipe';
+import { Metric } from './metric';
+import { ScaleBytesService } from '../services/scale-bytes.service'
+
+describe('FormatBytesPipe', () => {
+
+  let pipe: FormatBytesPipe;
+  let svc: ScaleBytesService;
+  beforeEach(() => {
+    svc = {
+      format: sinon.stub().returns({
+        result: 100,
+        scale: 2,
+        unit: 'KiB'
+      }),
+      metricToBigInt: null,
+      sizes: ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'],
+    };
+    pipe = new FormatBytesPipe(svc);
+  });
+
+  it('should delegate to service', () => {
+    svc.format.should.not.be.called();
+    pipe.transform({ $numberLong: '2' }).should.equal('100 KiB');
+    svc.format.should.be.calledOnce();
+    svc.format.should.be.calledWithMatch({ $numberLong: '2' });
+  });
+
+});
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/app/shared/filters/format-bytes.pipe.ts	Fri Oct 13 14:05:09 2017 -0400
@@ -0,0 +1,46 @@
+/**
+ * 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 {
+  Inject,
+  Pipe,
+  PipeTransform,
+} from "@angular/core";
+import { ScaleBytesService } from "../services/scale-bytes.service";
+import { Metric } from "./metric";
+
+@Pipe({
+  name: "formatBytes",
+})
+export class FormatBytesPipe implements PipeTransform {
+  constructor(@Inject(ScaleBytesService) private svc: ScaleBytesService) {}
+
+  public transform(value: Metric): string {
+    const scale = this.svc.format(value);
+    return scale.result + " " + scale.unit;
+  }
+}
--- a/src/app/shared/filters/metric-to-big-int.filter.spec.js	Fri Oct 13 11:39:51 2017 -0400
+++ b/src/app/shared/filters/metric-to-big-int.filter.spec.js	Fri Oct 13 14:05:09 2017 -0400
@@ -26,12 +26,16 @@
  */
 
 import big from 'big.js';
+import { default as servicesModule, init as initServices } from 'shared/services/services.module.js';
+import filtersModule from './filters.module.js';
 
 describe('metricToBigInt filter', () => {
 
   let fn;
   beforeEach(() => {
-    angular.mock.module('app.filters');
+    angular.mock.module(servicesModule);
+    initServices();
+    angular.mock.module(filtersModule);
     angular.mock.inject(metricToBigIntFilter => {
       'ngInject';
       fn = metricToBigIntFilter;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/app/shared/filters/metric.d.ts	Fri Oct 13 14:05:09 2017 -0400
@@ -0,0 +1,32 @@
+/**
+ * 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.
+ */
+
+export type Metric = IMongoLong;
+
+export interface IMongoLong {
+  $numberLong: string;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/app/shared/services/ajs-upgraded-providers.ts	Fri Oct 13 14:05:09 2017 -0400
@@ -0,0 +1,82 @@
+/**
+ * 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 { CommandChannelService } from "./command-channel.service.js";
+export const commandChannelServiceProvider = {
+  deps: ["$injector"],
+  provide: CommandChannelService,
+  useClass: CommandChannelService,
+};
+
+import { ExtractClassService } from "./extract-class.service.js";
+export const extractClassServiceProvider = {
+  deps: ["$injector"],
+  provide: ExtractClassService,
+  useClass: ExtractClassService,
+};
+
+import { LocalStorageService } from "./local-storage.service.js";
+export const localStorageServiceProvider = {
+  deps: ["$injector"],
+  provide: LocalStorageService,
+  useClass: LocalStorageService,
+};
+
+import { MetricToBigIntService } from "./metric-to-big-int.service.js";
+export const metricToBigIntServiceProvider = {
+  deps: ["$injector"],
+  provide: MetricToBigIntService,
+  useClass: MetricToBigIntService,
+};
+
+import { MultichartService } from "./multichart.service.js";
+export const multichartServiceProvider = {
+  deps: ["$injector"],
+  provide: MultichartService,
+  useClass: MultichartService,
+};
+
+import { SanitizeService } from "./sanitize.service.js";
+export const sanitizeServiceProvider = {
+  deps: ["$injector"],
+  provide: SanitizeService,
+  useClass: SanitizeService,
+};
+
+import { ScaleBytesService } from "./scale-bytes.service.js";
+export const scaleBytesServiceProvider = {
+  deps: ["$injector"],
+  provide: ScaleBytesService,
+  useClass: ScaleBytesService,
+};
+
+import { WebSocketFactory } from "./websocket-factory.service.js";
+export const webSocketFactoryProvider = {
+  deps: ["$injector"],
+  provide: WebSocketFactory,
+  useClass: WebSocketFactory,
+};
--- a/src/app/shared/services/command-channel.service.js	Fri Oct 13 11:39:51 2017 -0400
+++ b/src/app/shared/services/command-channel.service.js	Fri Oct 13 14:05:09 2017 -0400
@@ -30,7 +30,7 @@
 
 const CLIENT_REQUEST_TYPE = 2;
 
-class CommandChannelService {
+export class CommandChannelService {
   constructor ($q, authService, commandChannelUrl, webSocketFactory, $translate) {
     'ngInject';
     this._sequence = 1;
@@ -122,6 +122,8 @@
   }
 }
 
-angular
-  .module(servicesModule)
-  .service('commandChannelService', CommandChannelService);
+export function init () {
+  angular
+    .module(servicesModule)
+    .service('commandChannelService', CommandChannelService);
+}
--- a/src/app/shared/services/command-channel.service.spec.js	Fri Oct 13 11:39:51 2017 -0400
+++ b/src/app/shared/services/command-channel.service.spec.js	Fri Oct 13 14:05:09 2017 -0400
@@ -25,7 +25,7 @@
  * exception statement from your version.
  */
 
-import servicesModule from 'shared/services/services.module.js';
+import { default as servicesModule, init as initServices } from 'shared/services/services.module.js';
 import configModule from 'shared/config/config.module.js';
 
 describe('CommandChannelService', () => {
@@ -66,6 +66,7 @@
       $provide.constant('commandChannelUrl', 'ws://foo-host:1234');
     });
     angular.mock.module(servicesModule);
+    initServices();
     angular.mock.module($provide => {
       'ngInject';
       $provide.value('authService', authService);
--- a/src/app/shared/services/extract-class.service.js	Fri Oct 13 11:39:51 2017 -0400
+++ b/src/app/shared/services/extract-class.service.js	Fri Oct 13 14:05:09 2017 -0400
@@ -25,7 +25,7 @@
  * exception statement from your version.
  */
 
-class ExtractClassService {
+export class ExtractClassService {
   extract (fullClassName = '', includePkg = false) {
 
     if (fullClassName.indexOf('.') === -1) {
@@ -57,6 +57,8 @@
   }
 }
 
-angular
-  .module('app.services')
-  .service('extractClassService', ExtractClassService);
+export function init () {
+  angular
+    .module('app.services')
+    .service('extractClassService', ExtractClassService);
+}
--- a/src/app/shared/services/extract-class.service.spec.js	Fri Oct 13 11:39:51 2017 -0400
+++ b/src/app/shared/services/extract-class.service.spec.js	Fri Oct 13 14:05:09 2017 -0400
@@ -25,16 +25,19 @@
  * exception statement from your version.
  */
 
+import { default as servicesModule, init as initServices } from 'shared/services/services.module.js';
+
 describe('ExtractClassService', () => {
 
   let svc;
-
-  beforeEach(angular.mock.module('app.services'));
-
-  beforeEach(inject(extractClassService => {
-    'ngInject';
-    svc = extractClassService;
-  }));
+  beforeEach(() => {
+    angular.mock.module(servicesModule);
+    initServices();
+    angular.mock.inject(extractClassService => {
+      'ngInject';
+      svc = extractClassService;
+    });
+  });
 
   it('should return early if class name is bare', () => {
     svc.extract('className').should.equal('className');
--- a/src/app/shared/services/local-storage.service.js	Fri Oct 13 11:39:51 2017 -0400
+++ b/src/app/shared/services/local-storage.service.js	Fri Oct 13 14:05:09 2017 -0400
@@ -27,7 +27,7 @@
 
 import servicesModule from 'shared/services/services.module.js';
 
-class LocalStorageService {
+export class LocalStorageService {
 
   constructor ($window) {
     'ngInject';
@@ -57,6 +57,8 @@
 
 }
 
-angular
-  .module(servicesModule)
-  .service('localStorage', LocalStorageService);
+export function init () {
+  angular
+    .module(servicesModule)
+    .service('localStorage', LocalStorageService);
+}
--- a/src/app/shared/services/local-storage.service.spec.js	Fri Oct 13 11:39:51 2017 -0400
+++ b/src/app/shared/services/local-storage.service.spec.js	Fri Oct 13 14:05:09 2017 -0400
@@ -25,7 +25,7 @@
  * exception statement from your version.
  */
 
-import servicesModule from 'shared/services/services.module.js';
+import { default as servicesModule, init as initServices } from 'shared/services/services.module.js';
 
 describe('localStorage', () => {
 
@@ -47,6 +47,7 @@
     });
 
     angular.mock.module(servicesModule);
+    initServices();
 
     angular.mock.inject(localStorage => {
       'ngInject';
--- a/src/app/shared/services/metric-to-big-int.service.js	Fri Oct 13 11:39:51 2017 -0400
+++ b/src/app/shared/services/metric-to-big-int.service.js	Fri Oct 13 14:05:09 2017 -0400
@@ -34,7 +34,7 @@
  * @param {Object} metric e.g., '{ $numberLong: metric }'
  * @return {Big number Object}
  */
-class MetricToBigIntService {
+export class MetricToBigIntService {
   constructor () {
     this.big = big;
   }
@@ -51,6 +51,8 @@
   }
 }
 
-angular
-  .module('app.services')
-  .service('metricToBigIntService', MetricToBigIntService);
+export function init () {
+  angular
+    .module('app.services')
+    .service('metricToBigIntService', MetricToBigIntService);
+}
--- a/src/app/shared/services/metric-to-big-int.service.spec.js	Fri Oct 13 11:39:51 2017 -0400
+++ b/src/app/shared/services/metric-to-big-int.service.spec.js	Fri Oct 13 14:05:09 2017 -0400
@@ -26,18 +26,20 @@
  */
 
 import big from 'big.js';
+import { default as servicesModule, init as initServices } from 'shared/services/services.module.js';
 
 describe('MetricToBigIntService', () => {
 
   let svc;
-
-  beforeEach(angular.mock.module('app.services'));
-
-  beforeEach(inject(metricToBigIntService => {
-    'ngInject';
-    svc = metricToBigIntService;
-    sinon.spy(svc, 'big');
-  }));
+  beforeEach(() => {
+    angular.mock.module(servicesModule);
+    initServices();
+    angular.mock.inject(metricToBigIntService => {
+      'ngInject';
+      svc = metricToBigIntService;
+      sinon.spy(svc, 'big');
+    });
+  });
 
   afterEach(() => {
     svc.big.restore();
--- a/src/app/shared/services/multichart.service.js	Fri Oct 13 11:39:51 2017 -0400
+++ b/src/app/shared/services/multichart.service.js	Fri Oct 13 14:05:09 2017 -0400
@@ -28,7 +28,7 @@
 import servicesModule from 'shared/services/services.module.js';
 import _ from 'lodash';
 
-class MultiChartService {
+export class MultichartService {
   constructor ($q, $translate) {
     this.q = $q;
     this.translate = $translate;
@@ -174,6 +174,8 @@
   }
 }
 
-angular
-  .module(servicesModule)
-  .service('multichartService', MultiChartService);
+export function init () {
+  angular
+    .module(servicesModule)
+    .service('multichartService', MultichartService);
+}
--- a/src/app/shared/services/multichart.service.spec.js	Fri Oct 13 11:39:51 2017 -0400
+++ b/src/app/shared/services/multichart.service.spec.js	Fri Oct 13 14:05:09 2017 -0400
@@ -25,13 +25,14 @@
  * exception statement from your version.
  */
 
-import servicesModule from 'shared/services/services.module.js';
+import { default as servicesModule, init as initServices } from 'shared/services/services.module.js';
 
 describe('MultichartService', () => {
 
   let svc, translate;
   beforeEach(() => {
     angular.mock.module(servicesModule);
+    initServices();
     translate = sinon.stub().returns({
       then: sinon.stub().yields()
     });
--- a/src/app/shared/services/sanitize.service.js	Fri Oct 13 11:39:51 2017 -0400
+++ b/src/app/shared/services/sanitize.service.js	Fri Oct 13 14:05:09 2017 -0400
@@ -27,7 +27,7 @@
 
 import servicesModule from 'shared/services/services.module.js';
 
-class SanitizeService {
+export class SanitizeService {
   sanitize (str) {
     if (!str) {
       return;
@@ -41,6 +41,8 @@
   }
 }
 
-angular
-  .module(servicesModule)
-  .service('sanitizeService', SanitizeService);
+export function init () {
+  angular
+    .module(servicesModule)
+    .service('sanitizeService', SanitizeService);
+}
--- a/src/app/shared/services/sanitize.service.spec.js	Fri Oct 13 11:39:51 2017 -0400
+++ b/src/app/shared/services/sanitize.service.spec.js	Fri Oct 13 14:05:09 2017 -0400
@@ -25,13 +25,14 @@
  * exception statement from your version.
  */
 
-import servicesModule from 'shared/services/services.module.js';
+import { default as servicesModule, init as initServices } from 'shared/services/services.module.js';
 
 describe('SanitizeService', () => {
 
   let svc;
   beforeEach(() => {
     angular.mock.module(servicesModule);
+    initServices();
     angular.mock.inject(sanitizeService => {
       'ngInject';
       svc = sanitizeService;
--- a/src/app/shared/services/scale-bytes.service.js	Fri Oct 13 11:39:51 2017 -0400
+++ b/src/app/shared/services/scale-bytes.service.js	Fri Oct 13 14:05:09 2017 -0400
@@ -25,7 +25,7 @@
  * exception statement from your version.
  */
 
-class ScaleBytesService {
+export class ScaleBytesService {
   constructor (metricToBigIntService) {
     'ngInject';
     this.metricToBigInt = metricToBigIntService;
@@ -66,6 +66,8 @@
   }
 }
 
-angular
-  .module('app.services')
-  .service('scaleBytesService', ScaleBytesService);
+export function init () {
+  angular
+    .module('app.services')
+    .service('scaleBytesService', ScaleBytesService);
+}
--- a/src/app/shared/services/scale-bytes.service.spec.js	Fri Oct 13 11:39:51 2017 -0400
+++ b/src/app/shared/services/scale-bytes.service.spec.js	Fri Oct 13 14:05:09 2017 -0400
@@ -25,15 +25,19 @@
  * exception statement from your version.
  */
 
-describe('ScaleBytesService', () => {
+import { default as servicesModule, init as initServices } from 'shared/services/services.module.js';
 
-  beforeEach(angular.mock.module('app.services'));
+describe('ScaleBytesService', () => {
 
   let svc;
-  beforeEach(angular.mock.inject(scaleBytesService => {
-    'ngInject';
-    svc = scaleBytesService;
-  }));
+  beforeEach(() => {
+    angular.mock.module(servicesModule);
+    initServices();
+    angular.mock.inject(scaleBytesService => {
+      'ngInject';
+      svc = scaleBytesService;
+    });
+  });
 
   it('should exist', () => {
     should.exist(svc);
--- a/src/app/shared/services/services.module.js	Fri Oct 13 11:39:51 2017 -0400
+++ b/src/app/shared/services/services.module.js	Fri Oct 13 14:05:09 2017 -0400
@@ -35,5 +35,10 @@
   ])
   .name;
 
-let req = require.context('./', true, /\.service\.js/);
-req.keys().map(req);
+export function init () {
+  let req = require.context('./', true, /\.service\.js/);
+  req.keys().map(key => {
+    let mod = req(key);
+    mod.init();
+  });
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/app/shared/services/services.module.ts	Fri Oct 13 14:05:09 2017 -0400
@@ -0,0 +1,55 @@
+/**
+ * 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 { NgModule } from "@angular/core";
+
+import {
+  commandChannelServiceProvider,
+  extractClassServiceProvider,
+  localStorageServiceProvider,
+  metricToBigIntServiceProvider,
+  multichartServiceProvider,
+  sanitizeServiceProvider,
+  scaleBytesServiceProvider,
+  webSocketFactoryProvider,
+} from "./ajs-upgraded-providers";
+
+@NgModule({
+  declarations: [],
+  imports: [],
+  providers: [
+    commandChannelServiceProvider,
+    extractClassServiceProvider,
+    localStorageServiceProvider,
+    metricToBigIntServiceProvider,
+    multichartServiceProvider,
+    sanitizeServiceProvider,
+    scaleBytesServiceProvider,
+    webSocketFactoryProvider,
+  ],
+})
+export class ServicesModule {}
--- a/src/app/shared/services/websocket-factory.service.js	Fri Oct 13 11:39:51 2017 -0400
+++ b/src/app/shared/services/websocket-factory.service.js	Fri Oct 13 14:05:09 2017 -0400
@@ -28,7 +28,7 @@
 import servicesModule from './services.module.js';
 
 /* istanbul ignore next */
-class WebSocketFactory {
+export class WebSocketFactory {
   createSocket (connectUrl) {
     if ('WebSocket' in window) {
       return new WebSocket(connectUrl);
@@ -40,6 +40,8 @@
   }
 }
 
-angular
-  .module(servicesModule)
-  .service('webSocketFactory', WebSocketFactory);
+export function init () {
+  angular
+    .module(servicesModule)
+    .service('webSocketFactory', WebSocketFactory);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/gateway-url-helper.js	Fri Oct 13 14:05:09 2017 -0400
@@ -0,0 +1,45 @@
+/**
+ * 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.
+ */
+
+export function DetermineGatewayUrl () {
+  return new Promise(resolve => {
+    $.get('/gatewayurl')
+      .done(res => {
+        window.tmsGatewayUrl = res.gatewayUrl;
+      })
+      .fail(() => {
+        let url = require('url');
+        let parsed = url.parse(window.location.href);
+        let gateway = {
+          protocol: parsed.protocol,
+          host: parsed.host
+        };
+        window.tmsGatewayUrl = url.format(gateway);
+      })
+      .always(() => resolve());
+  });
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main.ts	Fri Oct 13 14:05:09 2017 -0400
@@ -0,0 +1,40 @@
+/**
+ * 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 './rx-subject.stub.ts';
+import 'zone.js';
+import 'reflect-metadata';
+
+import { UpgradeModule } from '@angular/upgrade/static';
+import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
+import { AppModule } from './app/app.module';
+
+import { DetermineGatewayUrl } from './gateway-url-helper.js';
+
+DetermineGatewayUrl().then(() => {
+  platformBrowserDynamic().bootstrapModule(AppModule);
+});
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/rx-subject.stub.ts	Fri Oct 13 14:05:09 2017 -0400
@@ -0,0 +1,35 @@
+/**
+ * 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 { Operator } from 'rxjs/Operator';
+import { Observable } from 'rxjs/Observable';
+
+declare module 'rxjs/Subject' {
+  interface Subject<T> {
+    lift<R>(operator: Operator<T, R>): Observable<R>;
+  }
+}
--- a/src/tests.webpack.js	Fri Oct 13 11:39:51 2017 -0400
+++ b/src/tests.webpack.js	Fri Oct 13 14:05:09 2017 -0400
@@ -25,12 +25,36 @@
  * exception statement from your version.
  */
 
+import 'core-js/client/core.js';
+import 'zone.js/dist/zone.js';
+import 'zone.js/dist/long-stack-trace-zone.js';
+import 'zone.js/dist/proxy.js';
+import 'zone.js/dist/sync-test.js';
+import 'zone.js/dist/mocha-patch.js';
+import 'zone.js/dist/async-test.js';
+import 'zone.js/dist/fake-async-test.js';
+
 import 'angular';
+import 'angular-patternfly';
+import '@uirouter/angularjs';
+import angularTranslate from 'angular-translate';
+import 'angular-translate-interpolation-messageformat';
+import 'oclazyload';
+
 import 'angular-mocks/angular-mocks';
-import 'babel-polyfill';
+
+import { getTestBed } from "@angular/core/testing";
+import {
+  BrowserDynamicTestingModule,
+  platformBrowserDynamicTesting
+} from "@angular/platform-browser-dynamic/testing";
+
+getTestBed().initTestEnvironment(
+  BrowserDynamicTestingModule,
+  platformBrowserDynamicTesting()
+);
 
 window.tmsGatewayUrl = 'http://localhost:8888/';
 
-const context = require.context('./app', true, /\.js$/);
-
-context.keys().forEach(context);
+const testsContext = require.context('./app', true, /\.spec\.(js|ts)$/);
+testsContext.keys().forEach(testsContext);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tsconfig.json	Fri Oct 13 14:05:09 2017 -0400
@@ -0,0 +1,22 @@
+{
+  "files": [
+    "src/rx-subject.stub.ts",
+    "src/main.ts"
+  ],
+  "compilerOptions": {
+    "outDir": "./dist",
+    "noImplicitAny": true,
+    "target": "es5",
+    "experimentalDecorators": true,
+    "allowJs": true,
+    "allowSyntheticDefaultImports": true,
+    "sourceMap": true,
+    "lib": [
+      "es2015",
+      "dom"
+    ]
+  },
+  "exclude": [
+    "node_modules"
+  ]
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tslint.json	Fri Oct 13 14:05:09 2017 -0400
@@ -0,0 +1,9 @@
+{
+    "defaultSeverity": "error",
+    "extends": [
+        "tslint:recommended"
+    ],
+    "jsRules": {},
+    "rules": {},
+    "rulesDirectory": []
+}
--- a/webpack.config.js	Fri Oct 13 11:39:51 2017 -0400
+++ b/webpack.config.js	Fri Oct 13 14:05:09 2017 -0400
@@ -41,7 +41,7 @@
   var config = {};
 
   config.entry = isTest ? void 0 : {
-    app: './src/app/app.module.js'
+    app: './src/main.ts'
   };
 
   config.resolve = {
@@ -59,11 +59,12 @@
       'scss': 'assets/scss',
       'shared': path.resolve(__dirname, 'src', 'app', 'shared'),
       'templates': 'shared/templates'
-    }
+    },
+    extensions: [ '.ts', '.js' ]
   };
 
   config.output = isTest ? {} : {
-    path: __dirname + '/dist',
+    path: path.resolve(__dirname, 'dist'),
     filename: '[name].bundle.js',
     chunkFilename: '[name].bundle.js'
   };
@@ -82,6 +83,10 @@
       loader: 'babel-loader',
       exclude: /node_modules/
     }, {
+      test: /\.ts$/,
+      use: 'ts-loader?silent=true',
+      exclude: /node_modules/
+    }, {
       test: /\.scss$/,
       loader: ['style-loader', 'css-loader', 'sass-loader'],
       exclude: /node_modules/
@@ -101,6 +106,15 @@
       test: /^(?!.*\.spec\.js$).*\.js$/,
       include: __dirname + '/src/app/',
       loaders: ['istanbul-instrumenter-loader', 'babel-loader']
+    }, {
+      test: /^(?!.*\.spec\.ts$).*\.ts$/,
+      include: __dirname + '/src/app/',
+      exclude: /(node_modules|\.spec\.ts$)/,
+      loader: 'istanbul-instrumenter-loader',
+      enforce: 'post',
+      options: {
+        esModules: true
+      }
     }]
   };