From 235891c6639841cf98ee144a9b136c23e1fd11b7 Mon Sep 17 00:00:00 2001
From: Evan Louie <evan.louie@outlook.com>
Date: Thu, 8 Feb 2018 01:18:01 -0800
Subject: [PATCH] Service Catalog (#49)

* listable catalog stuff

* Add instance and binding

* Add secret info to binding

* Concurrent service bindings

* immutable refactor

* wip

* flexbox plans

* provision

* update provision

* selectable chart versions (#39)

* selectable chart versions

- select chart versions from chart versions list
- adds route /chart/:repo/:chart/versions/:version to drill down on
  version details
- chartview is rendered based on the selected chart version type (latest by
  default) instead of the chart type

* style show all link correctly

* listable catalog stuff

* wip

* stashing

* card lists

* working provision

* sample parameters for provision

* fixed routes

* add CardContainer

* status message styling

* added tags to class cards

* working plan card images

* grid based cards

* catalog sync

* fixed card widths & removed template code from containers

* fixed Card infinite width growth

* retrieve name and bullets from service plan metadata

* update catalog fetch time in brokerview

* remove string[] from body

* move components to own folders

* Cleanup

- Migrated several `fetch` queries to use axios
- Changed the AppRepoForm appear in a modal instead of its own URL
- Refactored repo dispatch code from container to actions
- Moved Card css into separate CSS file
- Changed nulls to `undefined`
---
 .gitignore                                    |   1 +
 package.json                                  |   7 +-
 src/actions/catalog.ts                        | 138 ++++
 src/actions/index.ts                          |   4 +
 src/actions/releases.ts                       |   0
 src/actions/repos.ts                          |  76 ++
 src/components/AppRepoList/AppRepoButton.tsx  | 101 +++
 src/components/AppRepoList/index.tsx          |  56 ++
 src/components/BrokerView/index.tsx           | 143 ++++
 src/components/Card/Card.css                  |  55 ++
 src/components/Card/index.tsx                 |  44 ++
 .../ChartView/ChartDeployButton.tsx           |   4 +-
 src/components/ClassList/index.tsx            |  62 ++
 src/components/ClassView/index.tsx            |  83 +++
 src/components/Dashboard/Dashboard.tsx        |   2 +-
 src/components/ProvisionButton/index.tsx      | 208 ++++++
 src/components/ServiceBrokerList/index.tsx    |  31 +
 src/components/ServiceCatalog/index.tsx       |  43 ++
 src/components/ServiceList/index.tsx          |  53 ++
 src/components/Sidebar/Sidebar.tsx            |  23 +-
 src/components/SyncButton/index.tsx           |  49 ++
 src/containers/BrokerView.ts                  |  56 ++
 src/containers/ClassListContainer.ts          |  48 ++
 src/containers/ClassView.ts                   |  49 ++
 src/containers/RepoListContainer.ts           |  28 +
 src/containers/Root.tsx                       |  35 +-
 src/containers/ServiceCatalogContainer.ts     |  28 +
 src/containers/ServiceListContainer.ts        |  23 +
 src/reducers/catalog.ts                       |  63 ++
 src/reducers/index.ts                         |   4 +
 src/reducers/repos.ts                         |  65 ++
 src/shared/AppRepository.ts                   |  39 ++
 src/shared/HelmRelease.ts                     |  40 ++
 src/shared/ServiceCatalog.ts                  | 322 +++++++++
 src/shared/types.ts                           | 173 +++++
 src/shared/url.ts                             |  20 +
 tsconfig.json                                 |   4 +-
 tslint.json                                   |   4 +-
 yarn.lock                                     | 647 +++++++++---------
 39 files changed, 2495 insertions(+), 336 deletions(-)
 create mode 100644 src/actions/catalog.ts
 create mode 100644 src/actions/releases.ts
 create mode 100644 src/actions/repos.ts
 create mode 100644 src/components/AppRepoList/AppRepoButton.tsx
 create mode 100644 src/components/AppRepoList/index.tsx
 create mode 100644 src/components/BrokerView/index.tsx
 create mode 100644 src/components/Card/Card.css
 create mode 100644 src/components/Card/index.tsx
 create mode 100644 src/components/ClassList/index.tsx
 create mode 100644 src/components/ClassView/index.tsx
 create mode 100644 src/components/ProvisionButton/index.tsx
 create mode 100644 src/components/ServiceBrokerList/index.tsx
 create mode 100644 src/components/ServiceCatalog/index.tsx
 create mode 100644 src/components/ServiceList/index.tsx
 create mode 100644 src/components/SyncButton/index.tsx
 create mode 100644 src/containers/BrokerView.ts
 create mode 100644 src/containers/ClassListContainer.ts
 create mode 100644 src/containers/ClassView.ts
 create mode 100644 src/containers/RepoListContainer.ts
 create mode 100644 src/containers/ServiceCatalogContainer.ts
 create mode 100644 src/containers/ServiceListContainer.ts
 create mode 100644 src/reducers/catalog.ts
 create mode 100644 src/reducers/repos.ts
 create mode 100644 src/shared/AppRepository.ts
 create mode 100644 src/shared/HelmRelease.ts
 create mode 100644 src/shared/ServiceCatalog.ts

diff --git a/.gitignore b/.gitignore
index 8181d999893..c9de38e3571 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,6 +15,7 @@
 .env.development.local
 .env.test.local
 .env.production.local
+.vscode
 
 npm-debug.log*
 yarn-debug.log*
diff --git a/package.json b/package.json
index f788683c423..b98eda7588c 100644
--- a/package.json
+++ b/package.json
@@ -3,6 +3,7 @@
   "version": "0.1.0",
   "private": true,
   "dependencies": {
+    "axios": "^0.17.1",
     "enzyme": "^3.3.0",
     "enzyme-adapter-react-16": "^1.1.1",
     "raf": "^3.4.0",
@@ -14,7 +15,7 @@
     "react-redux": "^5.0.6",
     "react-router-dom": "^4.2.2",
     "react-router-redux": "^5.0.0-alpha.9",
-    "react-scripts-ts": "2.12.0",
+    "react-scripts-ts": "^2.13.0",
     "react-test-renderer": "^16.2.0",
     "redux": "^3.7.2",
     "redux-thunk": "^2.2.0",
@@ -49,8 +50,8 @@
     "@types/react-router-dom": "^4.2.3",
     "@types/react-router-redux": "^5.0.11",
     "@types/react-test-renderer": "^16.0.0",
-    "husky": "^0.15.0-rc.2",
-    "lint-staged": "^6.0.0",
+    "husky": "^0.14.3",
+    "lint-staged": "^6.1.0",
     "prettier": "^1.10.2",
     "tslint": "^5.9.1",
     "tslint-config-prettier": "^1.6.0",
diff --git a/src/actions/catalog.ts b/src/actions/catalog.ts
new file mode 100644
index 00000000000..7cb4dc7affb
--- /dev/null
+++ b/src/actions/catalog.ts
@@ -0,0 +1,138 @@
+import { Dispatch } from "redux";
+import { createAction, getReturnOfExpression } from "typesafe-actions";
+
+import {
+  IServiceBinding,
+  IServiceBroker,
+  IServiceClass,
+  IServiceInstance,
+  IServicePlan,
+  ServiceCatalog,
+} from "../shared/ServiceCatalog";
+import { IStoreState } from "../shared/types";
+
+export const checkCatalogInstall = createAction("CHECK_INSTALL");
+export const installed = createAction("INSTALLED");
+export const notInstalled = createAction("_NOT_INSTALLED");
+export const requestBrokers = createAction("REQUEST_BROKERS");
+export const receiveBrokers = createAction("RECEIVE_BROKERS", (brokers: IServiceBroker[]) => ({
+  brokers,
+  type: "RECEIVE_BROKERS",
+}));
+export const requestPlans = createAction("REQUEST_PLANS");
+export const receivePlans = createAction("RECEIVE_PLANS", (plans: IServicePlan[]) => ({
+  plans,
+  type: "RECEIVE_PLANS",
+}));
+export const requestInstances = createAction("REQUEST_INSTANCES");
+export const receiveInstances = createAction(
+  "RECEIVE_INSTANCES",
+  (instances: IServiceInstance[]) => ({ type: "RECEIVE_INSTANCES", instances }),
+);
+export const requestBindings = createAction("REQUEST_BINDINGS");
+export const receiveBindings = createAction("RECEIVE_BINDINGS", (bindings: IServiceBinding[]) => ({
+  bindings,
+  type: "RECEIVE_BINDINGS",
+}));
+export const requestClasses = createAction("REQUEST_PLANS");
+export const receiveClasses = createAction("RECEIVE_CLASSES", (classes: IServiceClass[]) => ({
+  classes,
+  type: "RECEIVE_CLASSES",
+}));
+
+const actions = [
+  checkCatalogInstall,
+  installed,
+  notInstalled,
+  requestBrokers,
+  receiveBrokers,
+  requestPlans,
+  receivePlans,
+  requestInstances,
+  receiveInstances,
+  requestBindings,
+  receiveBindings,
+  requestClasses,
+  receiveClasses,
+].map(getReturnOfExpression);
+
+export function provision(
+  releaseName: string,
+  namespace: string,
+  className: string,
+  planName: string,
+  parameters: {},
+) {
+  return async (dispatch: Dispatch<IStoreState>) => {
+    return ServiceCatalog.provisionInstance(
+      releaseName,
+      namespace,
+      className,
+      planName,
+      parameters,
+    );
+  };
+}
+
+export function sync(broker: IServiceBroker) {
+  return async (dispatch: Dispatch<IStoreState>) => {
+    return ServiceCatalog.syncBroker(broker);
+  };
+}
+
+export type ServiceCatalogAction = typeof actions[number];
+
+export function getBindings() {
+  return async (dispatch: Dispatch<IStoreState>) => {
+    dispatch(requestBindings());
+    const bindings = await ServiceCatalog.getServiceBindings();
+    dispatch(receiveBindings(bindings));
+    return bindings;
+  };
+}
+
+export function getBrokers() {
+  return async (dispatch: Dispatch<IStoreState>) => {
+    dispatch(requestBrokers());
+    const brokers = await ServiceCatalog.getServiceBrokers();
+    dispatch(receiveBrokers(brokers));
+    return brokers;
+  };
+}
+
+export function getClasses() {
+  return async (dispatch: Dispatch<IStoreState>) => {
+    dispatch(requestClasses());
+    const classes = await ServiceCatalog.getServiceClasses();
+    dispatch(receiveClasses(classes));
+    return classes;
+  };
+}
+
+export function getInstances() {
+  return async (dispatch: Dispatch<IStoreState>) => {
+    dispatch(requestInstances());
+    const instances = await ServiceCatalog.getServiceInstances();
+    dispatch(receiveInstances(instances));
+    return instances;
+  };
+}
+
+export function getPlans() {
+  return async (dispatch: Dispatch<IStoreState>) => {
+    dispatch(requestPlans());
+    const plans = await ServiceCatalog.getServicePlans();
+    dispatch(receivePlans(plans));
+    return plans;
+  };
+}
+
+export function getCatalog() {
+  return async (dispatch: Dispatch<IStoreState>) => {
+    dispatch(getBindings());
+    dispatch(getBrokers());
+    dispatch(getClasses());
+    dispatch(getInstances());
+    dispatch(getPlans());
+  };
+}
diff --git a/src/actions/index.ts b/src/actions/index.ts
index 4a6edbabda3..e5c048a29ca 100644
--- a/src/actions/index.ts
+++ b/src/actions/index.ts
@@ -1,5 +1,9 @@
+import * as catalog from "./catalog";
 import * as charts from "./charts";
+import * as repos from "./repos";
 
 export default {
+  catalog,
   charts,
+  repos,
 };
diff --git a/src/actions/releases.ts b/src/actions/releases.ts
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/src/actions/repos.ts b/src/actions/repos.ts
new file mode 100644
index 00000000000..85224763fb5
--- /dev/null
+++ b/src/actions/repos.ts
@@ -0,0 +1,76 @@
+import { createAction, getReturnOfExpression } from "typesafe-actions";
+
+import { Dispatch } from "react-redux";
+import { AppRepository } from "../shared/AppRepository";
+import { IAppRepository, IStoreState } from "../shared/types";
+
+export const addRepo = createAction("ADD_REPO");
+export const addedRepo = createAction("ADDED_REPO", (added: IAppRepository) => ({
+  added,
+  type: "ADDED_REPO",
+}));
+export const requestRepos = createAction("REQUEST_REPOS");
+export const receiveRepos = createAction("RECEIVE_REPOS", (repos: IAppRepository[]) => {
+  return {
+    repos,
+    type: "RECEIVE_REPOS",
+  };
+});
+export const showForm = createAction("SHOW_FORM");
+export const hideForm = createAction("HIDE_FORM");
+export const resetForm = createAction("RESET_FORM");
+export const submitForm = createAction("SUBMIT_FROM");
+export const updateForm = createAction(
+  "UPDATE_FORM",
+  (values: { name?: string; namespace?: string; url?: string }) => {
+    return {
+      type: "UPDATE_FORM",
+      values,
+    };
+  },
+);
+export const redirect = createAction("REDIRECT", (path: string) => ({ type: "REDIRECT", path }));
+export const redirected = createAction("REDIRECTED");
+
+const allActions = [
+  addRepo,
+  addedRepo,
+  requestRepos,
+  receiveRepos,
+  resetForm,
+  submitForm,
+  updateForm,
+  showForm,
+  hideForm,
+  redirect,
+  redirected,
+].map(getReturnOfExpression);
+export type AppReposAction = typeof allActions[number];
+
+export const deleteRepo = (name: string, namespace: string = "kubeapps") => {
+  return async (dispatch: Dispatch<IStoreState>) => {
+    await AppRepository.delete(name, namespace);
+    dispatch(requestRepos());
+    const repos = await AppRepository.list();
+    dispatch(receiveRepos(repos.items));
+    return repos;
+  };
+};
+
+export const fetchRepos = () => {
+  return async (dispatch: Dispatch<IStoreState>) => {
+    dispatch(requestRepos());
+    const repos = await AppRepository.list();
+    dispatch(receiveRepos(repos.items));
+    return repos;
+  };
+};
+
+export const installRepo = (name: string, url: string, namespace: string) => {
+  return async (dispatch: Dispatch<IStoreState>) => {
+    dispatch(addRepo());
+    const added = await AppRepository.create(name, url, namespace);
+    dispatch(addedRepo(added));
+    return added;
+  };
+};
diff --git a/src/components/AppRepoList/AppRepoButton.tsx b/src/components/AppRepoList/AppRepoButton.tsx
new file mode 100644
index 00000000000..99c1ead8fc3
--- /dev/null
+++ b/src/components/AppRepoList/AppRepoButton.tsx
@@ -0,0 +1,101 @@
+import * as React from "react";
+import * as Modal from "react-modal";
+import { Redirect } from "react-router";
+
+interface IAppRepoFormProps {
+  name: string;
+  url: string;
+  message?: string;
+  redirectTo?: string;
+  install: (name: string, url: string) => Promise<any>;
+  update: (values: { name?: string; url?: string }) => void;
+  onAfterInstall?: () => Promise<any>;
+}
+
+export const AppRepoForm = (props: IAppRepoFormProps) => {
+  const { name, url, update, install, onAfterInstall } = props;
+  const handleInstallClick = async () => {
+    await install(name, url);
+    if (onAfterInstall) {
+      await onAfterInstall();
+    }
+  };
+  const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) =>
+    update({ name: e.target.value });
+  const handleURLChange = (e: React.ChangeEvent<HTMLInputElement>) =>
+    update({ url: e.target.value });
+  return (
+    <div className="app-repo-form">
+      <h1>Add an App Repository</h1>
+      <label>
+        Name:
+        <input type="text" value={name} onChange={handleNameChange} />
+      </label>
+      <label>
+        URL:
+        <input type="text" value={url} onChange={handleURLChange} />
+      </label>
+      <button className="button button-primary" onClick={handleInstallClick}>
+        Install Repo
+      </button>
+      {props.redirectTo && <Redirect to={props.redirectTo} />}
+    </div>
+  );
+};
+
+interface IAppRepoAddButtonProps {
+  install: (name: string, url: string) => Promise<any>;
+  redirectTo?: string;
+}
+interface IAppRepoAddButtonState {
+  error?: string;
+  modalIsOpen: boolean;
+  name: string;
+  url: string;
+}
+
+export class AppRepoAddButton extends React.Component<
+  IAppRepoAddButtonProps,
+  IAppRepoAddButtonState
+> {
+  public state = {
+    error: undefined,
+    modalIsOpen: false,
+    name: "",
+    url: "",
+  };
+
+  public render() {
+    const { redirectTo, install } = this.props;
+    const { name, url } = this.state;
+    return (
+      <div className="AppRepoAddButton">
+        <button className="button button-primary" onClick={this.openModal}>
+          Add App Repository
+        </button>
+        <Modal
+          isOpen={this.state.modalIsOpen}
+          onRequestClose={this.closeModal}
+          contentLabel="Modal"
+        >
+          {this.state.error && (
+            <div className="container padding-v-bigger bg-action">{this.state.error}</div>
+          )}
+          <AppRepoForm
+            name={name}
+            url={url}
+            update={this.updateValues}
+            install={install}
+            onAfterInstall={this.closeModal}
+          />
+        </Modal>
+        {redirectTo && <Redirect to={redirectTo} />}
+      </div>
+    );
+  }
+
+  private closeModal = async () => this.setState({ modalIsOpen: false });
+  private openModal = async () => this.setState({ modalIsOpen: true });
+  private updateValues = async (values: { name: string; url: string }) =>
+    this.setState({ ...values });
+}
diff --git a/src/components/AppRepoList/index.tsx b/src/components/AppRepoList/index.tsx
new file mode 100644
index 00000000000..dc5da7c6d76
--- /dev/null
+++ b/src/components/AppRepoList/index.tsx
@@ -0,0 +1,56 @@
+import * as React from "react";
+
+import { IAppRepository } from "../../shared/types";
+import { AppRepoAddButton } from "./AppRepoButton";
+
+export interface IAppRepoListProps {
+  repos: IAppRepository[];
+  fetchRepos: () => Promise<any>;
+  deleteRepo: (name: string) => Promise<any>;
+  install: (name: string, url: string) => Promise<any>;
+}
+
+export class AppRepoList extends React.Component<IAppRepoListProps> {
+  public componentDidMount() {
+    this.props.fetchRepos();
+  }
+
+  public render() {
+    const { repos, install } = this.props;
+    return (
+      <div className="app-repo-list">
+        <h1>App Repositories</h1>
+        <table>
+          <thead>
+            <tr>
+              <th>Repo</th>
+              <th>URL</th>
+              <th>Actions</th>
+            </tr>
+          </thead>
+          <tbody>
+            {repos.map(repo => {
+              return (
+                <tr key={repo.metadata.name}>
+                  <td>{repo.metadata.name}</td>
+                  <td>{repo.spec && repo.spec.url}</td>
+                  <td>
+                    <button
+                      className="button button-secondary"
+                      onClick={this.handleDeleteClick(repo.metadata.name)}
+                    >
+                      Delete
+                    </button>
+                  </td>
+                </tr>
+              );
+            })}
+          </tbody>
+        </table>
+        <AppRepoAddButton install={install} />
+      </div>
+    );
+  }
+
+  private handleDeleteClick = (repoName: string) => () => this.props.deleteRepo(repoName);
+}
diff --git a/src/components/BrokerView/index.tsx b/src/components/BrokerView/index.tsx
new file mode 100644
index 00000000000..af8c8edce28
--- /dev/null
+++ b/src/components/BrokerView/index.tsx
@@ -0,0 +1,143 @@
+import * as React from "react";
+import { Link } from "react-router-dom";
+
+import {
+  IServiceBinding,
+  IServiceBroker,
+  IServiceClass,
+  IServiceInstance,
+  IServicePlan,
+} from "../../shared/ServiceCatalog";
+import { Card, CardContainer } from "../Card";
+import SyncButton from "../SyncButton";
+
+export interface IBrokerViewProps {
+  bindings: IServiceBinding[];
+  broker: IServiceBroker | undefined;
+  classes: IServiceClass[];
+  getCatalog: () => Promise<any>;
+  instances: IServiceInstance[];
+  plans: IServicePlan[];
+  sync: (broker: IServiceBroker) => Promise<any>;
+}
+
+export class BrokerView extends React.PureComponent<IBrokerViewProps> {
+  public async componentDidMount() {
+    this.props.getCatalog();
+  }
+
+  public render() {
+    const { bindings, broker, instances } = this.props;
+
+    return (
+      <div className="broker">
+        {broker && (
+          <div>
+            <h1>{broker.metadata.name}</h1>
+            <div>Catalog last updated at {broker.status.lastCatalogRetrievalTime}</div>
+            <div
+              style={{
+                display: "flex",
+                flexDirection: "row",
+              }}
+            >
+              <Link to={window.location.pathname + "/classes"}>
+                <button className="button button-primary">Provision New Service</button>
+              </Link>
+              <SyncButton sync={this.props.sync} broker={broker} />
+            </div>
+            <h3>Service Instances</h3>
+            <p>Most recent statuses for your brokers:</p>
+            <table>
+              <thead>
+                <tr>
+                  <th>Instance</th>
+                  <th>Status</th>
+                  <th>Message</th>
+                </tr>
+              </thead>
+              <tbody>
+                {instances.map(instance => {
+                  const conditions = [...instance.status.conditions];
+                  const status = conditions.shift(); // first in list is most recent
+                  const reason = status ? status.reason : "";
+                  const message = status ? status.message : "";
+
+                  return (
+                    <tr key={instance.metadata.uid}>
+                      <td key={instance.metadata.name}>
+                        <strong>
+                          {instance.metadata.namespace}/{instance.metadata.name}
+                        </strong>
+                      </td>
+                      <td key={reason}>
+                        <code>{reason}</code>
+                      </td>
+                      <td key={message}>
+                        <code>{message}</code>
+                      </td>
+                    </tr>
+                  );
+                })}
+              </tbody>
+            </table>
+
+            <h3>Bindings</h3>
+            <CardContainer>
+              {bindings.length > 0 &&
+                bindings.map(binding => {
+                  const {
+                    instanceRef,
+                    secretName,
+                    secretDatabase,
+                    secretHost,
+                    secretPassword,
+                    secretPort,
+                    secretUsername,
+                  } = binding.spec;
+                  const statuses: Array<[string, string | undefined]> = [
+                    ["Instance", instanceRef.name],
+                    ["Secret", secretName],
+                    ["Database", secretDatabase],
+                    ["Host", secretHost],
+                    ["Password", secretPassword],
+                    ["Port", secretPort],
+                    ["Username", secretUsername],
+                  ];
+                  const body = (
+                    <div style={{ display: "flex", flexWrap: "wrap", flexDirection: "column" }}>
+                      {statuses.map(statusPair => {
+                        const [key, value] = statusPair;
+                        return (
+                          <div key={key} style={{ display: "flex" }}>
+                            <strong key={key} style={{ flex: "0 0 5em" }}>
+                              {key}:
+                            </strong>
+                            <code
+                              key={value || "null"}
+                              style={{ flex: "1 1", wordBreak: "break-all" }}
+                            >
+                              {value}
+                            </code>
+                          </div>
+                        );
+                      })}
+                    </div>
+                  );
+                  const card = (
+                    <Card
+                      key={binding.metadata.name}
+                      header={binding.metadata.name}
+                      body={body}
+                      button={<span />}
+                    />
+                  );
+                  return card;
+                })}
+            </CardContainer>
+          </div>
+        )}
+      </div>
+    );
+  }
+}
diff --git a/src/components/Card/Card.css b/src/components/Card/Card.css
new file mode 100644
index 00000000000..322a6a42a89
--- /dev/null
+++ b/src/components/Card/Card.css
@@ -0,0 +1,55 @@
+/* Margin: 0.5em */
+
+.CardContainer {
+  display: flex;
+  flex-wrap: wrap;
+  margin: -0.5em;
+  padding: 1em 0;
+}
+
+.Card {
+  border-bottom: 1px solid #f2f2f0;
+  border-radius: 2px;
+  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
+  display: grid;
+  flex: 0 1 0;
+  grid-template-areas:
+    "title title title icon"
+    "body body body body"
+    "notes button button button";
+  grid-template-columns: 5fr 1fr 2fr 1fr;
+  grid-template-rows: auto auto auto;
+  margin: 0.5em;
+  min-width: 25em;
+  padding: 1em;
+}
+
+.Card__header {
+  grid-area: title;
+  color: #333333;
+  margin: 0;
+}
+
+.Card__body {
+  grid-area: body;
+  color: #666666;
+  margin: 1em 0;
+}
+
+.Card__notes {
+  grid-area: notes;
+}
+
+.Card__button {
+  grid-area: button;
+  justify-self: end;
+  align-self: end;
+}
+
+.Card__icon {
+  grid-area: icon;
+}
+
+.Card__img {
+  max-width: 100%;
+}
diff --git a/src/components/Card/index.tsx b/src/components/Card/index.tsx
new file mode 100644
index 00000000000..9d49b51047b
--- /dev/null
+++ b/src/components/Card/index.tsx
@@ -0,0 +1,44 @@
+import * as React from "react";
+import { Link } from "react-router-dom";
+
+import "./Card.css";
+
+export interface ICardProps {
+  header: string | JSX.Element | JSX.Element[];
+  body: string | JSX.Element | JSX.Element[];
+  button?: JSX.Element;
+  buttonText?: string | JSX.Element;
+  onClick?: () => (...args: any[]) => Promise<any>;
+  linkTo?: string;
+  notes?: JSX.Element;
+  icon?: string;
+}
+
+export const CardContainer = (props: any) => {
+  return <div className="CardContainer">{props.children}</div>;
+};
+
+export const Card = (props: ICardProps) => {
+  const { header, body, buttonText, onClick, linkTo, notes, icon } = props;
+  let button = props.button ? (
+    props.button
+  ) : (
+    <button onClick={onClick} className="button button-primary" style={{ width: "fit-content" }}>
+      {buttonText}
+    </button>
+  );
+  if (linkTo) {
+    button = <Link to={linkTo}>{button}</Link>;
+  }
+  return (
+    <div className="Card">
+      <h5 className="Card__header">{header}</h5>
+      <div className="Card__body">{body}</div>
+      <div className="Card__notes">{notes}</div>
+      <div className="Card__button">{button}</div>
+      <div className="Card__icon">
+        <img className="Card__img" src={icon} />
+      </div>
+    </div>
+  );
+};
diff --git a/src/components/ChartView/ChartDeployButton.tsx b/src/components/ChartView/ChartDeployButton.tsx
index 56d94b9f77a..0ac5a9c98e8 100644
--- a/src/components/ChartView/ChartDeployButton.tsx
+++ b/src/components/ChartView/ChartDeployButton.tsx
@@ -27,12 +27,12 @@ interface IChartDeployButtonState {
   namespace: string;
   values: string;
   valuesModified: boolean;
-  error: string | null;
+  error?: string;
 }
 
 class ChartDeployButton extends React.Component<IChartDeployButtonProps, IChartDeployButtonState> {
   public state: IChartDeployButtonState = {
-    error: null,
+    error: undefined,
     isDeploying: false,
     modalIsOpen: false,
     namespace: "default",
diff --git a/src/components/ClassList/index.tsx b/src/components/ClassList/index.tsx
new file mode 100644
index 00000000000..eeb25985b9a
--- /dev/null
+++ b/src/components/ClassList/index.tsx
@@ -0,0 +1,62 @@
+import * as React from "react";
+
+import { IServiceBroker, IServiceClass } from "../../shared/ServiceCatalog";
+import { Card, CardContainer } from "../Card";
+
+export interface IClassListProps {
+  broker: IServiceBroker | undefined;
+  classes: IServiceClass[];
+  getBrokers: () => Promise<IServiceBroker[]>;
+  getClasses: () => Promise<IServiceClass[]>;
+}
+
+export class ClassList extends React.Component<IClassListProps> {
+  public componentDidMount() {
+    this.props.getBrokers();
+    this.props.getClasses();
+  }
+
+  public render() {
+    const { classes } = this.props;
+    return (
+      <div>
+        <h2>Classes</h2>
+        <p>Types of services available from this broker</p>
+        <CardContainer>
+          {/* {classes.map(svcClass => { */}
+          {classes
+            .sort((a, b) => a.spec.externalName.localeCompare(b.spec.externalName))
+            .map(svcClass => {
+              const tags = svcClass.spec.tags.reduce<string>((joined, tag) => {
+                return `${joined} ${tag},`;
+              }, "");
+              const { spec } = svcClass;
+              const { externalMetadata } = spec;
+              const name = externalMetadata ? externalMetadata.displayName : spec.externalName;
+              const description = externalMetadata
+                ? externalMetadata.longDescription
+                : spec.description;
+              const imageUrl = externalMetadata && externalMetadata.imageUrl;
+
+              const card = (
+                <Card
+                  key={svcClass.metadata.uid}
+                  header={name}
+                  icon={imageUrl}
+                  body={description}
+                  buttonText="View Plans"
+                  linkTo={`${location.pathname}/${svcClass.spec.externalName}`}
+                  notes={
+                    <span style={{ fontSize: "small" }}>
+                      <strong>Tags:</strong> {tags}
+                    </span>
+                  }
+                />
+              );
+              return card;
+            })}
+        </CardContainer>
+      </div>
+    );
+  }
+}
diff --git a/src/components/ClassView/index.tsx b/src/components/ClassView/index.tsx
new file mode 100644
index 00000000000..6f54c2d6799
--- /dev/null
+++ b/src/components/ClassView/index.tsx
@@ -0,0 +1,83 @@
+import * as React from "react";
+import { RouterAction } from "react-router-redux";
+
+import { IServiceClass, IServicePlan } from "../../shared/ServiceCatalog";
+import { Card, CardContainer } from "../Card";
+import ProvisionButton from "../ProvisionButton";
+
+interface IClassViewProps {
+  classes: IServiceClass[];
+  classname: string;
+  getCatalog: () => Promise<any>;
+  plans: IServicePlan[];
+  provision: (
+    instanceName: string,
+    namespace: string,
+    className: string,
+    planName: string,
+    parameters: {},
+  ) => Promise<any>;
+  push: (location: string) => RouterAction;
+  svcClass: IServiceClass | undefined;
+}
+
+export class ClassView extends React.Component<IClassViewProps> {
+  public componentDidMount() {
+    this.props.getCatalog();
+  }
+
+  public render() {
+    const { classes, classname, plans, provision, push, svcClass } = this.props;
+    const classPlans = svcClass
+      ? plans.filter(plan => plan.spec.clusterServiceClassRef.name === svcClass.metadata.name)
+      : [];
+
+    return (
+      <div className="class-view">
+        <h3>Plans: {classname}</h3>
+        <p>Service Plans available for provisioning under {classname}</p>
+        <CardContainer>
+          {svcClass &&
+            classPlans.map(plan => {
+              const serviceClass = classes.find(
+                potential => potential.metadata.name === plan.spec.clusterServiceClassRef.name,
+              );
+              const { spec } = plan;
+              const { externalMetadata } = spec;
+              const name = externalMetadata ? externalMetadata.displayName : spec.externalName;
+              const description =
+                externalMetadata && externalMetadata.bullets
+                  ? externalMetadata.bullets
+                  : [spec.description];
+              const free = plan.spec.free ? <span>Free ✓</span> : undefined;
+              const bullets = (
+                <div>
+                  <ul>{description.map(bullet => <li key={bullet}>{bullet}</li>)}</ul>
+                </div>
+              );
+
+              const card = (
+                <Card
+                  key={plan.spec.externalID}
+                  header={name}
+                  body={bullets}
+                  notes={free}
+                  button={
+                    <ProvisionButton
+                      selectedClass={serviceClass}
+                      selectedPlan={plan}
+                      plans={plans}
+                      classes={classes}
+                      provision={provision}
+                      push={push}
+                    />
+                  }
+                />
+              );
+              return card;
+            })}
+        </CardContainer>
+      </div>
+    );
+  }
+}
diff --git a/src/components/Dashboard/Dashboard.tsx b/src/components/Dashboard/Dashboard.tsx
index c5f0b4ffca7..7d3a0e5ac32 100644
--- a/src/components/Dashboard/Dashboard.tsx
+++ b/src/components/Dashboard/Dashboard.tsx
@@ -13,7 +13,7 @@ class Dashboard extends React.Component {
           <div>No Apps installed</div>
           <div className="padding-normal">
             <Link className="button button-primary" to="/charts">
-              deploy one
+              Deploy Chart
             </Link>
           </div>
         </main>
diff --git a/src/components/ProvisionButton/index.tsx b/src/components/ProvisionButton/index.tsx
new file mode 100644
index 00000000000..ab27c5540c5
--- /dev/null
+++ b/src/components/ProvisionButton/index.tsx
@@ -0,0 +1,208 @@
+import * as React from "react";
+import AceEditor from "react-ace";
+import * as Modal from "react-modal";
+import { RouterAction } from "react-router-redux";
+
+import { IServiceClass, IServicePlan } from "../../shared/ServiceCatalog";
+
+import "brace/mode/json";
+import "brace/theme/xcode";
+
+interface IProvisionButtonProps {
+  plans: IServicePlan[];
+  classes: IServiceClass[];
+  selectedClass?: IServiceClass;
+  selectedPlan?: IServicePlan;
+  provision: (
+    releaseName: string,
+    namespace: string,
+    className: string,
+    planName: string,
+    parameters: {},
+  ) => Promise<{}>;
+  push: (location: string) => RouterAction;
+}
+
+interface IProvisionButtonState {
+  isProvisioning: boolean;
+  modalIsOpen: boolean;
+  // deployment options
+  releaseName: string;
+  namespace: string;
+  selectedPlan: IServicePlan | undefined;
+  selectedClass: IServiceClass | undefined;
+  parameters: string;
+  error?: string;
+}
+
+class ProvisionButton extends React.Component<IProvisionButtonProps, IProvisionButtonState> {
+  public state: IProvisionButtonState = {
+    error: undefined,
+    isProvisioning: false,
+    modalIsOpen: false,
+    namespace: "default",
+    parameters: JSON.stringify({ resourceGroup: "default", location: "eastus" }, undefined, 2),
+    releaseName: "",
+    selectedClass: this.props.selectedClass,
+    selectedPlan: this.props.selectedPlan,
+  };
+
+  public render() {
+    const { plans, classes } = this.props;
+    const { selectedClass, selectedPlan } = this.state;
+    return (
+      <div className="ProvisionButton">
+        {this.state.isProvisioning && <div>Provisioning...</div>}
+        <button
+          className="button button-primary"
+          onClick={this.openModel}
+          disabled={this.state.isProvisioning}
+        >
+          Provision
+        </button>
+        <Modal
+          isOpen={this.state.modalIsOpen}
+          onRequestClose={this.closeModal}
+          contentLabel="Modal"
+        >
+          {this.state.error && (
+            <div className="container padding-v-bigger bg-action">{this.state.error}</div>
+          )}
+          <form onSubmit={this.handleProvision}>
+            <div>
+              <label htmlFor="releaseName">Name</label>
+              <input
+                id="releaseName"
+                onChange={this.handleReleaseNameChange}
+                value={this.state.releaseName}
+                required={true}
+              />
+            </div>
+            <div>
+              <label htmlFor="namespace">Namespace</label>
+              <input
+                name="namespace"
+                onChange={this.handleNamespaceChange}
+                value={this.state.namespace}
+              />
+            </div>
+            <div>
+              <label htmlFor="classes">Classes</label>
+              <select onChange={this.onClassChange}>
+                {classes.map(c => (
+                  <option
+                    key={c.spec.externalName}
+                    selected={c.metadata.name === (selectedClass && selectedClass.metadata.name)}
+                    value={c.spec.externalName}
+                  >
+                    {c.spec.externalName}
+                  </option>
+                ))}
+              </select>
+            </div>
+            <div>
+              <label htmlFor="plans">Plans</label>
+              <select onChange={this.onPlanChange}>
+                {plans
+                  .filter(
+                    plan =>
+                      plan.spec.clusterServiceClassRef.name ===
+                      (selectedClass && selectedClass.metadata.name),
+                  )
+                  .map(p => (
+                    <option
+                      key={p.spec.externalName}
+                      value={p.spec.externalName}
+                      selected={p.metadata.name === (selectedPlan && selectedPlan.metadata.name)}
+                    >
+                      {p.spec.externalName}
+                    </option>
+                  ))}
+              </select>
+            </div>
+            <div style={{ marginBottom: "1em" }}>
+              <label htmlFor="values">Parameters (JSON)</label>
+              <AceEditor
+                mode="json"
+                theme="xcode"
+                name="values"
+                width="100%"
+                height="200px"
+                onChange={this.handleParametersChange}
+                setOptions={{ showPrintMargin: false }}
+                value={this.state.parameters}
+              />
+            </div>
+            <div>
+              <button className="button button-primary" type="submit">
+                Submit
+              </button>
+              <button className="button" onClick={this.closeModal}>
+                Cancel
+              </button>
+            </div>
+          </form>
+        </Modal>
+      </div>
+    );
+  }
+
+  public openModel = () => {
+    this.setState({
+      modalIsOpen: true,
+    });
+  };
+
+  public closeModal = () => {
+    this.setState({
+      modalIsOpen: false,
+    });
+  };
+
+  public handleProvision = async (e: React.FormEvent<HTMLFormElement>) => {
+    e.preventDefault();
+    const { provision, push } = this.props;
+    this.setState({ isProvisioning: true });
+    const { releaseName, namespace, selectedClass, selectedPlan, parameters } = this.state;
+
+    try {
+      const parametersObject = JSON.parse(parameters);
+      if (selectedClass && selectedPlan) {
+        await provision(
+          releaseName,
+          namespace,
+          selectedClass.spec.externalName,
+          selectedPlan.spec.externalName,
+          parametersObject,
+        );
+        push(`/services`);
+      }
+    } catch (err) {
+      this.setState({ isProvisioning: false, error: err.toString() });
+    }
+  };
+
+  public handleReleaseNameChange = (e: React.FormEvent<HTMLInputElement>) => {
+    this.setState({ releaseName: e.currentTarget.value });
+  };
+  public handleNamespaceChange = (e: React.FormEvent<HTMLInputElement>) => {
+    this.setState({ namespace: e.currentTarget.value });
+  };
+  public handleParametersChange = (parameter: string) => {
+    this.setState({ parameters: parameter });
+  };
+
+  public onClassChange = (e: React.ChangeEvent<HTMLSelectElement>) =>
+    this.setState({
+      selectedClass:
+        this.props.classes.find(svcClass => svcClass.spec.externalName === e.target.value) ||
+        undefined,
+    });
+  public onPlanChange = (e: React.ChangeEvent<HTMLSelectElement>) =>
+    this.setState({
+      selectedPlan:
+        this.props.plans.find(plan => plan.spec.externalName === e.target.value) || undefined,
+    });
+}
+
+export default ProvisionButton;
diff --git a/src/components/ServiceBrokerList/index.tsx b/src/components/ServiceBrokerList/index.tsx
new file mode 100644
index 00000000000..e2b62aaa757
--- /dev/null
+++ b/src/components/ServiceBrokerList/index.tsx
@@ -0,0 +1,31 @@
+import * as React from "react";
+
+import { IServiceBroker } from "../../shared/ServiceCatalog";
+import { Card, CardContainer } from "../Card";
+
+interface IServiceBrokerListProps {
+  brokers: IServiceBroker[];
+}
+
+export const ServiceBrokerList = (props: IServiceBrokerListProps) => {
+  const { brokers } = props;
+  return (
+    <div className="service-broker-list">
+      <h3>My Brokers</h3>
+      <CardContainer>
+        {brokers.map(broker => {
+          const card = (
+            <Card
+              key={broker.metadata.uid}
+              header={broker.metadata.name}
+              body={broker.spec.url}
+              buttonText="View"
+              linkTo={`/services/brokers/${broker.metadata.name}`}
+            />
+          );
+          return card;
+        })}
+      </CardContainer>
+    </div>
+  );
+};
diff --git a/src/components/ServiceCatalog/index.tsx b/src/components/ServiceCatalog/index.tsx
new file mode 100644
index 00000000000..4884fa5a705
--- /dev/null
+++ b/src/components/ServiceCatalog/index.tsx
@@ -0,0 +1,43 @@
+import * as React from "react";
+import { Link } from "react-router-dom";
+
+import { ServiceBrokerList } from "../../components/ServiceBrokerList";
+import { IServiceCatalogState } from "../../reducers/catalog";
+
+export interface IServiceCatalogDispatch {
+  checkCatalogInstalled: () => Promise<boolean>;
+  getCatalog: () => Promise<any>;
+}
+
+export class ServiceCatalogView extends React.Component<
+  IServiceCatalogDispatch & IServiceCatalogState
+> {
+  public async componentDidMount() {
+    this.props.checkCatalogInstalled();
+    this.props.getCatalog();
+  }
+
+  public render() {
+    const { brokers, isInstalled } = this.props;
+
+    return (
+      <div className="service-list-container">
+        <h1>Service Catalog</h1>
+        {!isInstalled ? (
+          <div>
+            <p>Service Catalog not installed.</p>
+            <div className="padding-normal">
+              <Link className="button button-primary" to={`/charts`}>
+                Install Catalog
+              </Link>
+            </div>
+          </div>
+        ) : (
+          <div>
+            <ServiceBrokerList brokers={brokers} />
+          </div>
+        )}
+      </div>
+    );
+  }
+}
diff --git a/src/components/ServiceList/index.tsx b/src/components/ServiceList/index.tsx
new file mode 100644
index 00000000000..ce0ca54c942
--- /dev/null
+++ b/src/components/ServiceList/index.tsx
@@ -0,0 +1,53 @@
+import * as React from "react";
+import { IServiceBroker, IServiceClass, IServicePlan } from "../../shared/ServiceCatalog";
+
+interface IServiceListProps {
+  brokers: IServiceBroker[];
+  classes: IServiceClass[];
+  plans: IServicePlan[];
+  getCatalog: () => Promise<any>;
+}
+
+export class ServiceList extends React.Component<IServiceListProps> {
+  public async componentDidMount() {
+    this.props.getCatalog();
+  }
+
+  public render() {
+    const { brokers, classes, plans } = this.props;
+    return (
+      <div className="service-list-container">
+        <h2>Brokers</h2>
+        <dl>
+          {brokers.length > 0 &&
+            brokers.map(broker => {
+              return [
+                <dt key={broker.metadata.name}>{broker.metadata.name}</dt>,
+                <dd key={broker.spec.url}>{broker.spec.url}</dd>,
+              ];
+            })}
+        </dl>
+        <h2>Classes</h2>
+        <dl>
+          {classes.length > 0 &&
+            classes.map(serviceClass => {
+              return [
+                <dt key={serviceClass.spec.externalName}>{serviceClass.spec.externalName}</dt>,
+                <dd key={serviceClass.spec.description}>{serviceClass.spec.description}</dd>,
+              ];
+            })}
+        </dl>
+        <h2>Plans</h2>
+        <dl>
+          {plans.length > 0 &&
+            plans.map(plan => {
+              return [
+                <dt key={plan.spec.externalName}>{plan.spec.externalName}</dt>,
+                <dd key={plan.spec.description}>{plan.spec.description}</dd>,
+              ];
+            })}
+        </dl>
+      </div>
+    );
+  }
+}
diff --git a/src/components/Sidebar/Sidebar.tsx b/src/components/Sidebar/Sidebar.tsx
index 769a5227d53..317e02e14db 100644
--- a/src/components/Sidebar/Sidebar.tsx
+++ b/src/components/Sidebar/Sidebar.tsx
@@ -4,6 +4,22 @@ import { Link } from "react-router-dom";
 import placeholder from "../../placeholder.png";
 import "./Sidebar.css";
 
+const sidebarItem = (props: { to: string; text: string; imageUrl?: string }) => {
+  const { to, text, imageUrl } = props;
+  const imageSrc: string = imageUrl || placeholder;
+
+  return (
+    <li className="padding-v-normal">
+      <Link to={to}>
+        <img src={imageSrc} height="48" />
+        <div className="type-small">
+          <span>{text}</span>
+        </div>
+      </Link>
+    </li>
+  );
+};
+
 class Sidebar extends React.Component {
   public render() {
     return (
@@ -19,10 +35,9 @@ class Sidebar extends React.Component {
             <img src={placeholder} height="48" />
             <div className="type-small">Functions</div>
           </li>
-          <li className="padding-v-normal">
-            <img src={placeholder} height="48" />
-            <div className="type-small">Service Catalog</div>
-          </li>
+          {sidebarItem({ to: "/charts", text: "Charts" })}
+          {sidebarItem({ to: "/services", text: "Service Catalog" })}
+          {sidebarItem({ to: "/repos", text: "App Repositories" })}
         </ul>
       </aside>
     );
diff --git a/src/components/SyncButton/index.tsx b/src/components/SyncButton/index.tsx
new file mode 100644
index 00000000000..703902996fa
--- /dev/null
+++ b/src/components/SyncButton/index.tsx
@@ -0,0 +1,49 @@
+import * as React from "react";
+import { IServiceBroker } from "../../shared/ServiceCatalog";
+
+interface ISyncButtonProps {
+  broker: IServiceBroker;
+  sync: (broker: IServiceBroker) => Promise<{}>;
+}
+
+interface ISyncButtonState {
+  broker: IServiceBroker | undefined;
+  isSyncing: boolean;
+  error: string | undefined;
+}
+
+class SyncButton extends React.Component<ISyncButtonProps, ISyncButtonState> {
+  public state: ISyncButtonState = {
+    broker: this.props.broker,
+    error: undefined,
+    isSyncing: false,
+  };
+
+  public handleSync = async () => {
+    const { sync, broker } = this.props;
+    this.setState({ isSyncing: true });
+
+    try {
+      await sync(broker).then(async () => this.setState({ isSyncing: false }));
+    } catch (err) {
+      this.setState({ isSyncing: false, error: err.toString() });
+    }
+  };
+
+  public render() {
+    return (
+      <div className="SyncButton">
+        {this.state.isSyncing && <div>Syncing...</div>}
+        <button
+          className="button button-primary"
+          disabled={this.state.isSyncing}
+          onClick={this.handleSync}
+        >
+          Sync
+        </button>
+      </div>
+    );
+  }
+}
+
+export default SyncButton;
diff --git a/src/containers/BrokerView.ts b/src/containers/BrokerView.ts
new file mode 100644
index 00000000000..ed90d72ac2b
--- /dev/null
+++ b/src/containers/BrokerView.ts
@@ -0,0 +1,56 @@
+import { connect } from "react-redux";
+import { Dispatch } from "redux";
+
+import actions from "../actions";
+import { BrokerView } from "../components/BrokerView";
+import { IServiceBroker } from "../shared/ServiceCatalog";
+import { IStoreState } from "../shared/types";
+
+interface IRouteProps {
+  match: {
+    params: {
+      brokerName: string;
+    };
+  };
+}
+
+function mapStateToProps({ catalog }: IStoreState, { match: { params } }: IRouteProps) {
+  const broker =
+    catalog.brokers.find(
+      potental => !!potental.metadata.name.match(new RegExp(params.brokerName, "i")),
+    ) || undefined;
+  const plans = broker
+    ? catalog.plans.filter(
+        plan => !!plan.spec.clusterServiceBrokerName.match(new RegExp(broker.metadata.name, "i")),
+      )
+    : [];
+  const classes = broker
+    ? catalog.classes.filter(
+        serviceClass =>
+          !!serviceClass.spec.clusterServiceBrokerName.match(new RegExp(broker.metadata.name, "i")),
+      )
+    : [];
+  const instances = broker ? catalog.instances : [];
+  const bindings = broker ? catalog.bindings : [];
+  return {
+    bindings,
+    broker,
+    classes,
+    instances,
+    plans,
+  };
+}
+
+function mapDispatchToProps(dispatch: Dispatch<IStoreState>) {
+  return {
+    getCatalog: async () => {
+      dispatch(actions.catalog.getCatalog());
+    },
+    sync: async (broker: IServiceBroker) => {
+      await dispatch(actions.catalog.sync(broker));
+      await dispatch(actions.catalog.getCatalog());
+    },
+  };
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(BrokerView);
diff --git a/src/containers/ClassListContainer.ts b/src/containers/ClassListContainer.ts
new file mode 100644
index 00000000000..edd09806ccc
--- /dev/null
+++ b/src/containers/ClassListContainer.ts
@@ -0,0 +1,48 @@
+import { connect } from "react-redux";
+import { Dispatch } from "redux";
+
+import actions from "../actions";
+import { ClassList } from "../components/ClassList";
+import { IStoreState } from "../shared/types";
+
+interface IRouteProps {
+  match: {
+    params: {
+      brokerName: string;
+      className: string;
+    };
+  };
+}
+
+function mapStateToProps({ catalog }: IStoreState, props: IRouteProps) {
+  const broker =
+    catalog.brokers.find(
+      potental => !!potental.metadata.name.match(new RegExp(props.match.params.brokerName, "i")),
+    ) || undefined;
+  const classes = broker
+    ? catalog.classes.filter(
+        serviceClass =>
+          !!serviceClass.spec.clusterServiceBrokerName.match(new RegExp(broker.metadata.name, "i")),
+      )
+    : [];
+
+  return {
+    broker,
+    classes,
+  };
+}
+
+function mapDispatchToProps(dispatch: Dispatch<IStoreState>) {
+  return {
+    getBrokers: async () => {
+      const brokers = await dispatch(actions.catalog.getBrokers());
+      return brokers;
+    },
+    getClasses: async () => {
+      const classes = await dispatch(actions.catalog.getClasses());
+      return classes;
+    },
+  };
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(ClassList);
diff --git a/src/containers/ClassView.ts b/src/containers/ClassView.ts
new file mode 100644
index 00000000000..aef0b79cb7d
--- /dev/null
+++ b/src/containers/ClassView.ts
@@ -0,0 +1,49 @@
+import { connect } from "react-redux";
+import { push } from "react-router-redux";
+import { Dispatch } from "redux";
+
+import actions from "../actions";
+import { ClassView } from "../components/ClassView";
+import { IStoreState } from "../shared/types";
+
+interface IRouteProps {
+  match: {
+    params: {
+      brokerName: string;
+      className: string;
+    };
+  };
+}
+
+function mapStateToProps({ catalog }: IStoreState, { match: { params } }: IRouteProps) {
+  const svcClass =
+    catalog.classes.find(
+      potential => !!potential.spec.externalName.match(new RegExp(params.className, "i")),
+    ) || undefined;
+  return {
+    classes: catalog.classes,
+    classname: params.className,
+    plans: catalog.plans,
+    svcClass,
+  };
+}
+
+function mapDispatchToProps(dispatch: Dispatch<IStoreState>) {
+  return {
+    getCatalog: async () => {
+      dispatch(actions.catalog.getCatalog());
+    },
+    provision: async (
+      instanceName: string,
+      namespace: string,
+      className: string,
+      planName: string,
+      parameters: {},
+    ) => {
+      dispatch(actions.catalog.provision(instanceName, namespace, className, planName, parameters));
+    },
+    push: (location: string) => dispatch(push(location)),
+  };
+}
+
+export const ClassViewContainer = connect(mapStateToProps, mapDispatchToProps)(ClassView);
diff --git a/src/containers/RepoListContainer.ts b/src/containers/RepoListContainer.ts
new file mode 100644
index 00000000000..2ecc43c6c87
--- /dev/null
+++ b/src/containers/RepoListContainer.ts
@@ -0,0 +1,28 @@
+import { connect } from "react-redux";
+import { Dispatch } from "redux";
+
+import actions from "../actions";
+import { AppRepoList } from "../components/AppRepoList";
+import { IStoreState } from "../shared/types";
+
+function mapStateToProps({ repos }: IStoreState) {
+  return {
+    repos: repos.repos,
+  };
+}
+
+function mapDispatchToProps(dispatch: Dispatch<IStoreState>) {
+  return {
+    deleteRepo: async (name: string, namespace: string = "kubeapps") => {
+      return dispatch(actions.repos.deleteRepo(name, namespace));
+    },
+    fetchRepos: async () => {
+      return dispatch(actions.repos.fetchRepos());
+    },
+    install: async (name: string, url: string, namespace: string = "kubeapps") => {
+      return dispatch(actions.repos.installRepo(name, url, namespace));
+    },
+  };
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(AppRepoList);
diff --git a/src/containers/Root.tsx b/src/containers/Root.tsx
index a54a0c0deb3..014da0af2ab 100644
--- a/src/containers/Root.tsx
+++ b/src/containers/Root.tsx
@@ -1,7 +1,7 @@
 import createHistory from "history/createBrowserHistory";
 import * as React from "react";
 import { Provider } from "react-redux";
-import { Route } from "react-router";
+import { Route, RouteComponentProps } from "react-router";
 import { ConnectedRouter } from "react-router-redux";
 
 import Dashboard from "../components/Dashboard";
@@ -10,25 +10,40 @@ import configureStore from "../store";
 import ChartList from "./ChartListContainer";
 import ChartView from "./ChartViewContainer";
 
+import BrokerView from "./BrokerView";
+import ClassListContainer from "./ClassListContainer";
+import { ClassViewContainer } from "./ClassView";
+import RepoListContainer from "./RepoListContainer";
+import ServiceCatalogContainer from "./ServiceCatalogContainer";
+
 const history = createHistory();
 const store = configureStore(history);
 
 class Root extends React.Component {
+  public static exactRoutes: {
+    [route: string]: React.ComponentType<RouteComponentProps<any>> | React.ComponentType<any>;
+  } = {
+    "/": Dashboard,
+    "/charts": ChartList,
+    "/charts/:repo": ChartList,
+    "/charts/:repo/:id": ChartView,
+    "/charts/:repo/:id/versions/:version": ChartView,
+    "/repos": RepoListContainer,
+    "/services": ServiceCatalogContainer,
+    "/services/brokers/:brokerName/classes": ClassListContainer,
+    "/services/brokers/:brokerName/classes/:className": ClassViewContainer,
+    "/services/brokers/:name": BrokerView,
+  };
+
   public render() {
     return (
       <Provider store={store}>
         <ConnectedRouter history={history}>
           <Layout>
             <section className="routes">
-              <Route exact={true} path="/" component={Dashboard} />
-              <Route exact={true} path="/charts" component={ChartList} />
-              <Route exact={true} path="/charts/:repo" component={ChartList} />
-              <Route exact={true} path="/charts/:repo/:id" component={ChartView} />
-              <Route
-                exact={true}
-                path="/charts/:repo/:id/versions/:version"
-                component={ChartView}
-              />
+              {Object.keys(Root.exactRoutes).map(route => (
+                <Route exact={true} path={route} component={Root.exactRoutes[route]} />
+              ))}
             </section>
           </Layout>
         </ConnectedRouter>
diff --git a/src/containers/ServiceCatalogContainer.ts b/src/containers/ServiceCatalogContainer.ts
new file mode 100644
index 00000000000..aaaee4f7e71
--- /dev/null
+++ b/src/containers/ServiceCatalogContainer.ts
@@ -0,0 +1,28 @@
+import { connect } from "react-redux";
+import { Dispatch } from "redux";
+
+import actions from "../actions";
+import { ServiceCatalogView } from "../components/ServiceCatalog";
+import { ServiceCatalog } from "../shared/ServiceCatalog";
+import { IStoreState } from "../shared/types";
+
+function mapStateToProps({ catalog }: IStoreState) {
+  return {
+    ...catalog,
+  };
+}
+
+function mapDispatchToProps(dispatch: Dispatch<IStoreState>) {
+  return {
+    checkCatalogInstalled: async () => {
+      const isInstalled = await ServiceCatalog.isCatalogInstalled();
+      isInstalled
+        ? dispatch(actions.catalog.installed())
+        : dispatch(actions.catalog.notInstalled());
+      return isInstalled;
+    },
+    getCatalog: () => dispatch(actions.catalog.getCatalog()),
+  };
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(ServiceCatalogView);
diff --git a/src/containers/ServiceListContainer.ts b/src/containers/ServiceListContainer.ts
new file mode 100644
index 00000000000..bc0f896d0aa
--- /dev/null
+++ b/src/containers/ServiceListContainer.ts
@@ -0,0 +1,23 @@
+import { connect } from "react-redux";
+import { Dispatch } from "redux";
+
+import actions from "../actions";
+import { ServiceList } from "../components/ServiceList";
+import { IStoreState } from "../shared/types";
+
+function mapStateToProps({ catalog }: IStoreState) {
+  const { brokers, classes, plans } = catalog;
+  return {
+    brokers,
+    classes,
+    plans,
+  };
+}
+
+function mapDispatchToProps(dispatch: Dispatch<IStoreState>) {
+  return {
+    getCatalog: async () => dispatch(actions.catalog.getCatalog()),
+  };
+}
+
+export const ServiceListContainer = connect(mapStateToProps, mapDispatchToProps)(ServiceList);
diff --git a/src/reducers/catalog.ts b/src/reducers/catalog.ts
new file mode 100644
index 00000000000..7ad514f58d4
--- /dev/null
+++ b/src/reducers/catalog.ts
@@ -0,0 +1,63 @@
+import { getType } from "typesafe-actions";
+
+import actions from "../actions";
+import { ServiceCatalogAction } from "../actions/catalog";
+import {
+  IServiceBinding,
+  IServiceBroker,
+  IServiceClass,
+  IServiceInstance,
+  IServicePlan,
+} from "../shared/ServiceCatalog";
+
+export interface IServiceCatalogState {
+  bindings: IServiceBinding[];
+  brokers: IServiceBroker[];
+  classes: IServiceClass[];
+  instances: IServiceInstance[];
+  isChecking: boolean;
+  isInstalled: boolean;
+  plans: IServicePlan[];
+}
+
+const initialState: IServiceCatalogState = {
+  bindings: [],
+  brokers: [],
+  classes: [],
+  instances: [],
+  isChecking: true,
+  isInstalled: false,
+  plans: [],
+};
+
+export const catalogReducer = (
+  state: IServiceCatalogState = initialState,
+  action: ServiceCatalogAction,
+): IServiceCatalogState => {
+  const { catalog } = actions;
+  switch (action.type) {
+    case getType(catalog.installed):
+      return { ...state, isChecking: false, isInstalled: true };
+    case getType(catalog.notInstalled):
+      return { ...state, isChecking: false, isInstalled: false };
+    case getType(catalog.checkCatalogInstall):
+      return { ...state, isChecking: true };
+    case getType(catalog.receiveBrokers):
+      const { brokers } = action;
+      return { ...state, brokers };
+    case getType(catalog.receiveBindings):
+      const { bindings } = action;
+      return { ...state, bindings };
+    case getType(catalog.receiveClasses):
+      const { classes } = action;
+      return { ...state, classes };
+    case getType(catalog.receiveInstances):
+      const { instances } = action;
+      return { ...state, instances };
+    case getType(catalog.receivePlans):
+      const { plans } = action;
+      return { ...state, plans };
+    default:
+      return { ...state };
+  }
+};
diff --git a/src/reducers/index.ts b/src/reducers/index.ts
index 03c6f3cdbfb..2ebdce86af4 100644
--- a/src/reducers/index.ts
+++ b/src/reducers/index.ts
@@ -2,10 +2,14 @@ import { routerReducer } from "react-router-redux";
 import { combineReducers } from "redux";
 
 import { IStoreState } from "../shared/types";
+import { catalogReducer } from "./catalog";
 import chartsReducer from "./charts";
+import { reposReducer } from "./repos";
 
 const rootReducer = combineReducers<IStoreState>({
+  catalog: catalogReducer,
   charts: chartsReducer,
+  repos: reposReducer,
   router: routerReducer,
 });
 
diff --git a/src/reducers/repos.ts b/src/reducers/repos.ts
new file mode 100644
index 00000000000..ca7030488cf
--- /dev/null
+++ b/src/reducers/repos.ts
@@ -0,0 +1,65 @@
+import { getType } from "typesafe-actions";
+
+import actions from "../actions";
+import { AppReposAction } from "../actions/repos";
+import { IAppRepository } from "../shared/types";
+
+export interface IAppRepositoryState {
+  addingRepo: boolean;
+  lastAdded?: IAppRepository;
+  isFetching: boolean;
+  repos: IAppRepository[];
+  selected?: IAppRepository;
+  form: {
+    name: string;
+    namespace: string;
+    url: string;
+    show: boolean;
+  };
+  redirectTo?: string;
+}
+
+const initialState: IAppRepositoryState = {
+  addingRepo: false,
+  form: {
+    name: "",
+    namespace: "",
+    show: false,
+    url: "",
+  },
+  isFetching: false,
+  repos: [],
+};
+
+export const reposReducer = (
+  state: IAppRepositoryState = initialState,
+  action: AppReposAction,
+): IAppRepositoryState => {
+  switch (action.type) {
+    case getType(actions.repos.receiveRepos):
+      const { repos } = action;
+      return { ...state, isFetching: false, repos };
+    case getType(actions.repos.requestRepos):
+      return { ...state, isFetching: true };
+    case getType(actions.repos.addRepo):
+      return { ...state, addingRepo: true };
+    case getType(actions.repos.addedRepo):
+      const { added } = action;
+      return { ...state, addingRepo: false, lastAdded: added, repos: [...state.repos, added] };
+    case getType(actions.repos.resetForm):
+      return { ...state, form: { ...state.form, name: "", namespace: "", url: "" } };
+    case getType(actions.repos.updateForm):
+      const { values } = action;
+      return { ...state, form: { ...state.form, ...values } };
+    case getType(actions.repos.showForm):
+      return { ...state, form: { ...state.form, show: true } };
+    case getType(actions.repos.hideForm):
+      return { ...state, form: { ...state.form, show: false } };
+    case getType(actions.repos.redirect):
+      return { ...state, redirectTo: action.path };
+    case getType(actions.repos.redirected):
+      return { ...state, redirectTo: undefined };
+    default:
+      return state;
+  }
+};
diff --git a/src/shared/AppRepository.ts b/src/shared/AppRepository.ts
new file mode 100644
index 00000000000..625849dc72f
--- /dev/null
+++ b/src/shared/AppRepository.ts
@@ -0,0 +1,39 @@
+import axios from "axios";
+
+import { IAppRepository, IAppRepositoryList } from "./types";
+
+export class AppRepository {
+  public static async list() {
+    const { data } = await axios.get<IAppRepositoryList>(
+      `${AppRepository.APIEndpoint}/apprepositories`,
+    );
+    return data;
+  }
+
+  public static async delete(name: string, namespace: string = "default") {
+    const { data } = await axios.delete(AppRepository.getSelfLink(name, namespace));
+    return data;
+  }
+
+  public static async create(name: string, url: string, namespace: string = "default") {
+    const { data } = await axios.post<IAppRepository>(
+      `${AppRepository.APIEndpoint}/namespaces/${namespace}/apprepositories`,
+      {
+        apiVersion: "kubeapps.com/v1alpha1",
+        kind: "AppRepository",
+        metadata: {
+          name,
+          namespace,
+        },
+        spec: { type: "helm", url },
+      },
+    );
+    return data;
+  }
+
+  // private static serviceCatalogURL: string = "https://svc-catalog-charts.storage.googleapis.com";
+  private static APIEndpoint: string = "/api/kube/apis/kubeapps.com/v1alpha1";
+  private static getSelfLink(name: string, namespace: string = "default"): string {
+    return `${AppRepository.APIEndpoint}/namespaces/${namespace}/apprepositories/${name}`;
+  }
+}
diff --git a/src/shared/HelmRelease.ts b/src/shared/HelmRelease.ts
new file mode 100644
index 00000000000..9d013501cd2
--- /dev/null
+++ b/src/shared/HelmRelease.ts
@@ -0,0 +1,40 @@
+import axios from "axios";
+import { IChart } from "./types";
+
+export class HelmRelease {
+  public static async create(chart: IChart, releaseName: string, namespace: string) {
+    const endpoint = HelmRelease.getResourceLink(namespace);
+    const { data } = await axios.post(endpoint, {
+      data: {
+        apiVersion: "helm.bitnami.com/v1",
+        kind: "HelmRelease",
+        metadata: {
+          releaseName,
+        },
+        spec: {
+          chartName: chart.attributes.name,
+          repoUrl: chart.attributes.repo.url,
+          version: chart.relationships.latestChartVersion.data.version,
+        },
+      },
+    });
+    return data;
+  }
+
+  public static async delete(selfLink: string) {
+    const { data } = await axios.delete(selfLink);
+    return data;
+  }
+
+  // private static getSelfLink(name: string, namespace: string) {
+  //   return `/api/kube/apis/helm.bitnami.com/v1/namespaces/${namespace}/helmreleases/${name}`;
+  // }
+
+  private static getResourceLink(namespace?: string) {
+    if (namespace) {
+      return `/api/kube/apis/helm.bitnami.com/v1/namespaces/${namespace}/helmreleases`;
+    } else {
+      return `/api/kube/apis/helm.bitnami.com/v1/helmreleases`;
+    }
+  }
+}
diff --git a/src/shared/ServiceCatalog.ts b/src/shared/ServiceCatalog.ts
new file mode 100644
index 00000000000..3f47fbea489
--- /dev/null
+++ b/src/shared/ServiceCatalog.ts
@@ -0,0 +1,322 @@
+import axios from "axios";
+
+import * as urls from "../shared/url";
+import { AppRepository } from "./AppRepository";
+import { IAppRepository, IK8sList, IStatus } from "./types";
+
+export class ServiceCatalog {
+  public static async getCatalogRepo(): Promise<IAppRepository | undefined> {
+    const repos = await AppRepository.list();
+    const svcRepo = repos.items.find(repo => {
+      return !!(
+        repo.spec &&
+        repo.spec.url &&
+        repo.spec.url === "https://svc-catalog-charts.storage.googleapis.com"
+      );
+    });
+
+    return svcRepo;
+  }
+
+  public static async installCatalog(name: string, namespace: string) {
+    const url = "https://svc-catalog-charts.storage.googleapis.com";
+    await AppRepository.create(name, url, namespace);
+  }
+
+  public static async getServiceClasses() {
+    return ServiceCatalog.getItems<IServiceClass>("/clusterserviceclasses");
+  }
+
+  public static async getServiceBrokers() {
+    return ServiceCatalog.getItems<IServiceBroker>("/clusterservicebrokers");
+  }
+
+  public static async getServicePlans() {
+    return ServiceCatalog.getItems<IServicePlan>("/clusterserviceplans");
+  }
+
+  public static async getServiceBindings(): Promise<IServiceBinding[]> {
+    const bindings = await ServiceCatalog.getItems<IServiceBinding>("/servicebindings");
+
+    // initiate with undefined secrets
+    for (const binding of bindings) {
+      binding.spec = {
+        ...binding.spec,
+        secretDatabase: undefined,
+        secretHost: undefined,
+        secretPassword: undefined,
+        secretPort: undefined,
+        secretUsername: undefined,
+      };
+    }
+
+    return Promise.all(
+      bindings.map(binding => {
+        const { secretName } = binding.spec;
+        const { namespace } = binding.metadata;
+        return axios
+          .get<IK8sApiSecretResponse>(ServiceCatalog.secretEndpoint(namespace) + secretName)
+          .then(response => {
+            const { database, host, password, port, username } = response.data.data;
+            const spec = {
+              ...binding.spec,
+              secretDatabase: atob(database),
+              secretHost: atob(host),
+              secretPassword: atob(password),
+              secretPort: atob(port),
+              secretUsername: atob(username),
+            };
+            return { ...binding, spec };
+          })
+          .catch(err => {
+            // return with undefined secrets
+            return { ...binding };
+          });
+      }),
+    );
+  }
+
+  public static async provisionInstance(
+    releaseName: string,
+    namespace: string,
+    className: string,
+    planName: string,
+    parameters: {},
+  ) {
+    const { data } = await axios.post<IStatus>(
+      urls.api.serviceinstances.create(namespace),
+      {
+        apiVersion: "servicecatalog.k8s.io/v1beta1",
+        kind: "ServiceInstance",
+        metadata: {
+          name: releaseName,
+        },
+        spec: {
+          clusterServiceClassExternalName: className,
+          clusterServicePlanExternalName: planName,
+          parameters,
+        },
+      },
+      {
+        validateStatus: statusCode => true,
+      },
+    );
+
+    if (data.status === "Failure") {
+      throw new Error(data.message);
+    }
+
+    return data;
+  }
+
+  public static async syncBroker(broker: IServiceBroker) {
+    const { data } = await axios.patch<IStatus>(
+      urls.api.clusterservicebrokers.sync(broker),
+      {
+        spec: {
+          relistRequests: broker.spec.relistRequests + 1,
+        },
+      },
+      {
+        headers: { "Content-Type": "application/merge-patch+json" },
+        validateStatus: statusCode => true,
+      },
+    );
+
+    if (data.status === "Failure") {
+      throw new Error(data.message);
+    }
+
+    return data;
+  }
+
+  public static async getServiceInstances() {
+    return ServiceCatalog.getItems<IServiceInstance>("/serviceinstances");
+  }
+
+  public static async isCatalogInstalled(): Promise<boolean> {
+    try {
+      const { status } = await axios.get(ServiceCatalog.endpoint);
+      return status === 200;
+    } catch (err) {
+      return false;
+    }
+  }
+
+  private static endpoint: string = "/api/kube/apis/servicecatalog.k8s.io/v1beta1";
+
+  private static secretEndpoint = (namespace: string = "default") =>
+    `/api/kube/api/v1/namespaces/${namespace}/secrets/`;
+
+  private static async getItems<T>(endpoint: string): Promise<T[]> {
+    const response = await axios.get<IK8sList<T, {}>>(ServiceCatalog.endpoint + endpoint);
+    // const response = await axios.get<IK8sApiListResponse<T>>(ServiceCatalog.endpoint + endpoint);
+    const json = response.data;
+    return json.items;
+  }
+}
+
+interface IK8sApiSecretResponse {
+  kind: string;
+  apiVersion: string;
+  metadata: {
+    selfLink: string;
+    resourceVersion: string;
+  };
+  data: {
+    database: string;
+    host: string;
+    password: string;
+    port: string;
+    username: string;
+  };
+}
+
+export interface IK8sApiListResponse<T> {
+  kind: string;
+  apiVersion: string;
+  metadata: {
+    selfLink: string;
+    resourceVersion: string;
+  };
+  items: T[];
+}
+
+export interface IServiceClass {
+  metadata: {
+    creationTimestamp: string;
+    name: string;
+    resourceVersion: string;
+    selfLink: string;
+    uid: string;
+  };
+  spec: {
+    bindable: boolean;
+    binding_retrievable: boolean;
+    clusterServiceBrokerName: string;
+    description: string;
+    externalID: string;
+    externalName: string;
+    planUpdatable: boolean;
+    tags: string[];
+    externalMetadata?: {
+      displayName: string;
+      documentationUrl: string;
+      imageUrl: string;
+      longDescription: string;
+      supportUrl: string;
+    };
+  };
+  status: {
+    removedFromBrokerCatalog: boolean;
+  };
+}
+
+export interface ICondition {
+  type: string;
+  status: string;
+  lastTransitionTime: string;
+  reason: string;
+  message: string;
+}
+
+export interface IServiceBroker {
+  metadata: {
+    name: string;
+    selfLink: string;
+    uid: string;
+    resourceVersion: string;
+    generation: number;
+    creationTimestamp: string;
+    finalizers: string[];
+  };
+  spec: {
+    url: string;
+    authInfo: any; // Look into
+    relistBehavior: string;
+    relistDuration: string;
+    relistRequests: number;
+  };
+  status: {
+    conditions: ICondition[];
+    reconciledGeneration: number;
+    lastCatalogRetrievalTime: string;
+  };
+}
+
+export interface IServicePlan {
+  metadata: {
+    name: string;
+    selfLink: string;
+    uid: string;
+    resourceVersion: string;
+    creationTimestamp: string;
+  };
+  spec: {
+    clusterServiceBrokerName: string;
+    externalName: string;
+    externalID: string;
+    description: string;
+    externalMetadata?: {
+      displayName: string;
+      bullets: string[];
+    };
+    free: boolean;
+    clusterServiceClassRef: {
+      name: string;
+    };
+  };
+  status: {
+    removedFromBrokerCatalog: boolean;
+  };
+}
+
+export interface IServiceInstance {
+  metadata: {
+    name: string;
+    namespace: string;
+    selfLink: string;
+    uid: string;
+    resourceVersion: string;
+    creationTimestamp: string;
+    finalizers: string[];
+    generation: number;
+  };
+  spec: {
+    clusterServiceClassExternalName: string;
+    clusterServicePlanExternalName: string;
+    externalID: string;
+    clusterServicePlanRef: {
+      name: string;
+    };
+    clusterServiceClassRef: {
+      name: string;
+    };
+  };
+  status: { conditions: ICondition[] };
+}
+
+export interface IServiceBinding {
+  metadata: {
+    name: string;
+    selfLink: string;
+    uid: string;
+    resourceVersion: string;
+    creationTimestamp: string;
+    finalizers: string[];
+    generation: number;
+    namespace: string;
+  };
+  spec: {
+    externalID: string;
+    instanceRef: {
+      name: string;
+    };
+    secretName: string | undefined;
+    secretDatabase: string | undefined;
+    secretHost: string | undefined;
+    secretPassword: string | undefined;
+    secretPort: string | undefined;
+    secretUsername: string | undefined;
+  };
+}
diff --git a/src/shared/types.ts b/src/shared/types.ts
index 059928c9cd9..9d59c66f9ae 100644
--- a/src/shared/types.ts
+++ b/src/shared/types.ts
@@ -1,3 +1,6 @@
+import { IServiceCatalogState } from "../reducers/catalog";
+import { IAppRepositoryState } from "../reducers/repos";
+
 export interface IChartVersion {
   id: string;
   attributes: IChartVersionAttributes;
@@ -53,5 +56,175 @@ export interface IChartState {
 }
 
 export interface IStoreState {
+  catalog: IServiceCatalogState;
   charts: IChartState;
+  repos: IAppRepositoryState;
+}
+
+interface IK8sResource {
+  apiVersion: string;
+  kind: string;
+}
+
+/** @see https://github.com/kubernetes/community/blob/master/contributors/devel/api-conventions.md#objects */
+export interface IK8sObject<M, SP, ST> extends IK8sResource {
+  metadata: {
+    annotations?: { [key: string]: string };
+    creationTimestamp?: string;
+    deletionTimestamp?: string | null;
+    generation?: number;
+    labels?: { [key: string]: string };
+    name: string;
+    namespace: string;
+    resourceVersion?: string;
+    uid: string;
+    selfLink?: string; // Not in docs, but seems to exist everywhere
+  } & M;
+  spec?: SP;
+  status?: ST;
+}
+
+/** @see https://github.com/kubernetes/community/blob/master/contributors/devel/api-conventions.md#lists-and-simple-kinds */
+export interface IK8sList<I, M> extends IK8sResource {
+  items: I[];
+  metadata?: {
+    resourceVersion?: string;
+    selfLink?: string; // Not in docs, but seems to exist everywhere
+  } & M;
+}
+
+export interface IAppRepository
+  extends IK8sObject<
+      {
+        clusterName: string;
+        creationTimestamp: string;
+        deletionGracePeriodSeconds: string | null;
+        deletionTimestamp: string | null;
+        resourceVersion: string;
+        selfLink: string;
+      },
+      { type: string; url: string },
+      undefined
+    > {}
+
+export interface IAppRepositoryList
+  extends IK8sList<
+      IAppRepository,
+      {
+        continue: string;
+        resourceVersion: string;
+        selfLink: string;
+      }
+    > {}
+
+export interface IClusterServicePlan
+  extends IK8sObject<
+      {
+        namespace: undefined;
+        selfLink: string;
+        resourceVersion: string;
+        creationTimestamp: string;
+      },
+      {
+        clusterServiceBrokerName: string;
+        externalName: string;
+        externalID: string;
+        description: string;
+        free: boolean;
+        clusterServiceClassRef: { [key: string]: string };
+      },
+      { removedFromBrokerCatalog: boolean }
+    > {}
+
+export interface IClusterServicePlanList
+  extends IK8sList<IClusterServicePlan, { selfLink: string; resourceVersion: string }> {}
+
+export interface IClusterServiceClass
+  extends IK8sObject<
+      {
+        creationTimestamp: string;
+        namespace: undefined;
+        resourceVersion: string;
+        selfLink: string;
+      },
+      {
+        clusterServiceBrokerName: string;
+        externalName: string;
+        externalID: string;
+        description: string;
+        bindable: boolean;
+        binding_retrievable: string;
+        planUpdatable: string;
+        tags: string[];
+      },
+      { removedFromBrokerCatalog: boolean }
+    > {}
+
+export interface IClusterServiceClassList
+  extends IK8sList<IClusterServiceClass, { selfLink: string; resourceVersion: string }> {}
+
+interface ICondition {
+  type: string;
+  status: string;
+  lastTransitionTime: string;
+  reason: string;
+  message: string;
+}
+
+/**
+ * @property authInfo Unknown what kind of auth types their are
+ */
+export interface IClusterServiceBroker
+  extends IK8sObject<
+      {
+        resourceVersion: string;
+        generation: number;
+        creationTimestamp: string;
+        finalizers: string[];
+      },
+      {
+        url: string;
+        authInfo: any;
+        relistBehavior: string;
+        relistDuration: string;
+        relistRequests: number;
+      },
+      {
+        conditions: ICondition[];
+        reconciledGeneration: number;
+        lastCatalogRetrievalTime: string;
+      }
+    > {}
+
+export interface IClusterServiceBrokerList
+  extends IK8sList<IClusterServiceBroker, { selfLink: string; resourceVersion: string }> {}
+
+/** @see https://github.com/kubernetes/community/blob/master/contributors/devel/api-conventions.md#response-status-kind */
+export interface IStatus extends IK8sResource {
+  kind: "Status";
+  status: "Success" | "Failure";
+  message: string;
+  reason:
+    | "BadRequest"
+    | "Unauthorized"
+    | "Forbidden"
+    | "NotFound"
+    | "AlreadyExists"
+    | "Conflict"
+    | "Invalid"
+    | "Timeout"
+    | "ServerTimeout"
+    | "MethodNotAllowed"
+    | "InternalError";
+  details?: {
+    kind?: string;
+    name?: string;
+    causes?: IStatusCause[] | string;
+  };
+}
+
+interface IStatusCause {
+  field: string;
+  message: string;
+  reason: string;
 }
diff --git a/src/shared/url.ts b/src/shared/url.ts
index 729413c4e41..e77ed3d6b52 100644
--- a/src/shared/url.ts
+++ b/src/shared/url.ts
@@ -1,3 +1,4 @@
+import { IServiceBroker } from "./ServiceCatalog";
 import { IChartVersion } from "./types";
 
 export const app = {
@@ -21,8 +22,27 @@ export const api = {
     listVersions: (id: string) => `${api.charts.get(id)}/versions`,
   },
 
+  // /api/kube exposes kubectl add ?watch=true
   helmreleases: {
     create: (namespace = "default") =>
       `/api/kube/apis/helm.bitnami.com/v1/namespaces/${namespace}/helmreleases`,
   },
+
+  apprepostories: {
+    base: `/api/kube/apis/kubeapps.com/v1alpha1`,
+    create: (namespace = "default") =>
+      `${api.apprepostories.base}/namespaces/${namespace}/apprepositories`,
+  },
+
+  serviceinstances: {
+    base: `/api/kube/apis/servicecatalog.k8s.io/v1beta1`,
+    create: (namespace = "default") =>
+      `${api.serviceinstances.base}/namespaces/${namespace}/serviceinstances`,
+  },
+
+  clusterservicebrokers: {
+    base: `/api/kube/apis/servicecatalog.k8s.io/v1beta1`,
+    sync: (broker: IServiceBroker) =>
+      `${api.clusterservicebrokers.base}/clusterservicebrokers/${broker.metadata.name}`,
+  },
 };
diff --git a/tsconfig.json b/tsconfig.json
index 85973541495..25095b4c0bb 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,9 +1,9 @@
 {
   "compilerOptions": {
+    "baseUrl": "src",
     "outDir": "build/dist",
     "module": "esnext",
-    "target": "es5",
-    "lib": ["es6", "dom"],
+    "target": "es6",
     "sourceMap": true,
     "allowJs": true,
     "jsx": "react",
diff --git a/tslint.json b/tslint.json
index 1e9f337d964..4e6c651683c 100644
--- a/tslint.json
+++ b/tslint.json
@@ -2,6 +2,8 @@
   "defaultSeverity": "error",
   "extends": ["tslint:recommended", "tslint-react", "tslint-config-prettier"],
   "jsRules": {},
-  "rules": {},
+  "rules": {
+    "no-console": false
+  },
   "rulesDirectory": []
 }
diff --git a/yarn.lock b/yarn.lock
index b62eda00a62..481e7cbb110 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3,8 +3,8 @@
 
 
 "@babel/code-frame@^7.0.0-beta.35":
-  version "7.0.0-beta.38"
-  resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.0.0-beta.38.tgz#c0af5930617e55e050336838e3a3670983b0b2b2"
+  version "7.0.0-beta.39"
+  resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.0.0-beta.39.tgz#91c90bb65207fc5a55128cb54956ded39e850457"
   dependencies:
     chalk "^2.0.0"
     esutils "^2.0.2"
@@ -21,8 +21,8 @@
     "@types/enzyme" "*"
 
 "@types/enzyme@*", "@types/enzyme@^3.1.6":
-  version "3.1.7"
-  resolved "https://registry.yarnpkg.com/@types/enzyme/-/enzyme-3.1.7.tgz#358a28dda8df6841db665659278437d04571a094"
+  version "3.1.8"
+  resolved "https://registry.yarnpkg.com/@types/enzyme/-/enzyme-3.1.8.tgz#e0a4492994fafb2fccc1726f8b4d9960097a4a8c"
   dependencies:
     "@types/cheerio" "*"
     "@types/react" "*"
@@ -32,12 +32,12 @@
   resolved "https://registry.yarnpkg.com/@types/history/-/history-4.6.2.tgz#12cfaba693ba20f114ed5765467ff25fdf67ddb0"
 
 "@types/jest@^22.0.0":
-  version "22.0.1"
-  resolved "https://registry.yarnpkg.com/@types/jest/-/jest-22.0.1.tgz#6370a6d60cce3845e4cd5d00bf65f654264685bc"
+  version "22.1.1"
+  resolved "https://registry.yarnpkg.com/@types/jest/-/jest-22.1.1.tgz#231d7c60ed130200af9e96c82469ed25b59a7ea2"
 
 "@types/node@*", "@types/node@^9.3.0":
-  version "9.3.0"
-  resolved "https://registry.yarnpkg.com/@types/node/-/node-9.3.0.tgz#3a129cda7c4e5df2409702626892cb4b96546dd5"
+  version "9.4.0"
+  resolved "https://registry.yarnpkg.com/@types/node/-/node-9.4.0.tgz#b85a0bcf1e1cc84eb4901b7e96966aedc6f078d1"
 
 "@types/react-dom@^16.0.3":
   version "16.0.3"
@@ -76,8 +76,8 @@
     redux "^3.7.2"
 
 "@types/react-router@*", "@types/react-router@^4.0.20":
-  version "4.0.20"
-  resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-4.0.20.tgz#3404f54e44bba2239ea4320ea701d86d92f05486"
+  version "4.0.21"
+  resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-4.0.21.tgz#d6c7ea3b45dba02eb8f869629f3b7d7b6e9a7938"
   dependencies:
     "@types/history" "*"
     "@types/react" "*"
@@ -89,10 +89,10 @@
     "@types/react" "*"
 
 "@types/react@*", "@types/react@^16.0.34":
-  version "16.0.34"
-  resolved "https://registry.yarnpkg.com/@types/react/-/react-16.0.34.tgz#7a8f795afd8a404a9c4af9539b24c75d3996914e"
+  version "16.0.36"
+  resolved "https://registry.yarnpkg.com/@types/react/-/react-16.0.36.tgz#ceb5639013bdb92a94147883052e69bb2c22c69b"
 
-abab@^1.0.3:
+abab@^1.0.3, abab@^1.0.4:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/abab/-/abab-1.0.4.tgz#5faad9c2c07f60dd76770f71cf025b62a63cfd4e"
 
@@ -119,7 +119,7 @@ acorn-globals@^3.1.0:
   dependencies:
     acorn "^4.0.4"
 
-acorn-globals@^4.0.0:
+acorn-globals@^4.1.0:
   version "4.1.0"
   resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-4.1.0.tgz#ab716025dbe17c54d3ef81d32ece2b2d99fe2538"
   dependencies:
@@ -129,9 +129,9 @@ acorn@^4.0.3, acorn@^4.0.4:
   version "4.0.13"
   resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.13.tgz#105495ae5361d697bd195c825192e1ad7f253787"
 
-acorn@^5.0.0, acorn@^5.1.2:
-  version "5.3.0"
-  resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.3.0.tgz#7446d39459c54fb49a80e6ee6478149b940ec822"
+acorn@^5.0.0, acorn@^5.3.0:
+  version "5.4.1"
+  resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.4.1.tgz#fdc58d9d17f4a4e98d102ded826a9b9759125102"
 
 address@1.0.3, address@^1.0.1:
   version "1.0.3"
@@ -173,11 +173,11 @@ amdefine@>=0.0.4:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5"
 
-ansi-align@^1.1.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-1.1.0.tgz#2f0c1658829739add5ebb15e6b0c6e3423f016ba"
+ansi-align@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-2.0.0.tgz#c36aeccba563b89ceb556f3690f0b1d9e3547f7f"
   dependencies:
-    string-width "^1.0.1"
+    string-width "^2.0.0"
 
 ansi-escapes@^1.0.0, ansi-escapes@^1.4.0:
   version "1.4.0"
@@ -344,6 +344,10 @@ async-each@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d"
 
+async-limiter@~1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.0.tgz#78faed8c3d074ab81f22b4e985d79e8738f720f8"
+
 async@^1.4.0, async@^1.5.2:
   version "1.5.2"
   resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a"
@@ -392,6 +396,13 @@ aws4@^1.2.1, aws4@^1.6.0:
   version "1.6.0"
   resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e"
 
+axios@^0.17.1:
+  version "0.17.1"
+  resolved "https://registry.yarnpkg.com/axios/-/axios-0.17.1.tgz#2d8e3e5d0bdbd7327f91bc814f5c57660f81824d"
+  dependencies:
+    follow-redirects "^1.2.5"
+    is-buffer "^1.1.5"
+
 babel-code-frame@6.26.0, babel-code-frame@^6.11.0, babel-code-frame@^6.22.0, babel-code-frame@^6.26.0:
   version "6.26.0"
   resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b"
@@ -425,8 +436,8 @@ babel-core@^6.0.0, babel-core@^6.24.1, babel-core@^6.26.0:
     source-map "^0.5.6"
 
 babel-generator@^6.18.0, babel-generator@^6.26.0:
-  version "6.26.0"
-  resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.26.0.tgz#ac1ae20070b79f6e3ca1d3269613053774f20dc5"
+  version "6.26.1"
+  resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.26.1.tgz#1844408d3b8f0d35a404ea7ac180f087a601bd90"
   dependencies:
     babel-messages "^6.23.0"
     babel-runtime "^6.26.0"
@@ -434,7 +445,7 @@ babel-generator@^6.18.0, babel-generator@^6.26.0:
     detect-indent "^4.0.0"
     jsesc "^1.3.0"
     lodash "^4.17.4"
-    source-map "^0.5.6"
+    source-map "^0.5.7"
     trim-right "^1.0.1"
 
 babel-helpers@^6.24.1:
@@ -659,19 +670,17 @@ boom@5.x.x:
   dependencies:
     hoek "4.x.x"
 
-boxen@^0.6.0:
-  version "0.6.0"
-  resolved "https://registry.yarnpkg.com/boxen/-/boxen-0.6.0.tgz#8364d4248ac34ff0ef1b2f2bf49a6c60ce0d81b6"
+boxen@^1.2.1:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/boxen/-/boxen-1.3.0.tgz#55c6c39a8ba58d9c61ad22cd877532deb665a20b"
   dependencies:
-    ansi-align "^1.1.0"
-    camelcase "^2.1.0"
-    chalk "^1.1.1"
+    ansi-align "^2.0.0"
+    camelcase "^4.0.0"
+    chalk "^2.0.1"
     cli-boxes "^1.0.0"
-    filled-array "^1.0.0"
-    object-assign "^4.0.1"
-    repeating "^2.0.0"
-    string-width "^1.0.1"
-    widest-line "^1.0.0"
+    string-width "^2.0.0"
+    term-size "^1.2.0"
+    widest-line "^2.0.0"
 
 brace-expansion@^1.0.0, brace-expansion@^1.1.7:
   version "1.1.8"
@@ -834,7 +843,7 @@ camelcase@^1.0.2:
   version "1.2.1"
   resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-1.2.1.tgz#9bb5304d2e0b56698b2c758b08a3eaa9daa58a39"
 
-camelcase@^2.0.0, camelcase@^2.1.0:
+camelcase@^2.0.0:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f"
 
@@ -842,7 +851,7 @@ camelcase@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-3.0.0.tgz#32fc4b9fcdaf845fcdf7e73bb97cac2261f0ab0a"
 
-camelcase@^4.1.0:
+camelcase@^4.0.0, camelcase@^4.1.0:
   version "4.1.0"
   resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd"
 
@@ -856,12 +865,12 @@ caniuse-api@^1.5.2:
     lodash.uniq "^4.5.0"
 
 caniuse-db@^1.0.30000529, caniuse-db@^1.0.30000634, caniuse-db@^1.0.30000639:
-  version "1.0.30000793"
-  resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30000793.tgz#3c00c66e423a7a1907c7dd96769a78b2afa8a72e"
+  version "1.0.30000802"
+  resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30000802.tgz#f7dca342da1c12cf84ff2c80432e24c7e1cc36d9"
 
 caniuse-lite@^1.0.30000748, caniuse-lite@^1.0.30000792:
-  version "1.0.30000792"
-  resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000792.tgz#d0cea981f8118f3961471afbb43c9a1e5bbf0332"
+  version "1.0.30000802"
+  resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000802.tgz#807ead8e2b59a3b90f26702fb0b220623ad9f287"
 
 capture-stack-trace@^1.0.0:
   version "1.0.0"
@@ -1092,7 +1101,11 @@ commander@2.12.x:
   version "2.12.2"
   resolved "https://registry.yarnpkg.com/commander/-/commander-2.12.2.tgz#0f5946c427ed9ec0d91a46bb9def53e54650e555"
 
-commander@^2.11.0, commander@^2.12.1, commander@^2.9.0, commander@~2.13.0:
+commander@^2.11.0, commander@^2.12.1, commander@^2.9.0:
+  version "2.14.0"
+  resolved "https://registry.yarnpkg.com/commander/-/commander-2.14.0.tgz#7b25325963e6aace20d3a9285b09379b0c2208b5"
+
+commander@~2.13.0:
   version "2.13.0"
   resolved "https://registry.yarnpkg.com/commander/-/commander-2.13.0.tgz#6964bca67685df7c1f1430c584f07d7597885b9c"
 
@@ -1118,19 +1131,16 @@ concat-map@0.0.1:
   version "0.0.1"
   resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
 
-configstore@^2.0.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/configstore/-/configstore-2.1.0.tgz#737a3a7036e9886102aa6099e47bb33ab1aba1a1"
+configstore@^3.0.0:
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/configstore/-/configstore-3.1.1.tgz#094ee662ab83fad9917678de114faaea8fcdca90"
   dependencies:
-    dot-prop "^3.0.0"
+    dot-prop "^4.1.0"
     graceful-fs "^4.1.2"
-    mkdirp "^0.5.0"
-    object-assign "^4.0.1"
-    os-tmpdir "^1.0.0"
-    osenv "^0.1.0"
-    uuid "^2.0.1"
-    write-file-atomic "^1.1.2"
-    xdg-basedir "^2.0.0"
+    make-dir "^1.0.0"
+    unique-string "^1.0.0"
+    write-file-atomic "^2.0.0"
+    xdg-basedir "^3.0.0"
 
 connect-history-api-fallback@^1.3.0:
   version "1.5.0"
@@ -1154,7 +1164,7 @@ content-disposition@0.5.2:
   version "0.5.2"
   resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4"
 
-content-type-parser@^1.0.1:
+content-type-parser@^1.0.1, content-type-parser@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/content-type-parser/-/content-type-parser-1.0.2.tgz#caabe80623e63638b2502fd4c7f12ff4ce2352e7"
 
@@ -1230,7 +1240,7 @@ create-ecdh@^4.0.0:
     bn.js "^4.1.0"
     elliptic "^6.0.0"
 
-create-error-class@^3.0.1:
+create-error-class@^3.0.0:
   version "3.0.2"
   resolved "https://registry.yarnpkg.com/create-error-class/-/create-error-class-3.0.2.tgz#06be7abef947a3f14a30fd610671d401bca8b7b6"
   dependencies:
@@ -1292,6 +1302,10 @@ crypto-browserify@^3.11.0:
     randombytes "^2.0.0"
     randomfill "^1.0.3"
 
+crypto-random-string@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-1.0.0.tgz#a230f64f568310e1498009940790ec99545bca7e"
+
 css-color-names@0.0.4:
   version "0.0.4"
   resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-0.0.4.tgz#808adc2e79cf84738069b646cb20ec27beb629e0"
@@ -1452,6 +1466,10 @@ deep-is@~0.1.3:
   version "0.1.3"
   resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34"
 
+deepmerge@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-2.0.1.tgz#25c1c24f110fb914f80001b925264dd77f3f4312"
+
 default-require-extensions@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/default-require-extensions/-/default-require-extensions-1.0.0.tgz#f37ea15d3e13ffd9b437d33e1a75b5fb97874cb8"
@@ -1560,7 +1578,7 @@ dns-equal@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/dns-equal/-/dns-equal-1.0.0.tgz#b39e7f1da6eb0a75ba9c17324b34753c47e0654d"
 
-dns-packet@^1.0.1:
+dns-packet@^1.3.1:
   version "1.3.1"
   resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-1.3.1.tgz#12aa426981075be500b910eedcd0b47dd7deda5a"
   dependencies:
@@ -1593,8 +1611,8 @@ dom-urls@^1.1.0:
     urijs "^1.16.1"
 
 domain-browser@^1.1.1:
-  version "1.1.7"
-  resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.1.7.tgz#867aa4b093faa05f1de08c06f4d7b21fdf8698bc"
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda"
 
 domelementtype@1, domelementtype@^1.3.0:
   version "1.3.0"
@@ -1642,9 +1660,9 @@ domutils@^1.5.1:
     dom-serializer "0"
     domelementtype "1"
 
-dot-prop@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-3.0.0.tgz#1b708af094a49c9a0e7dbcad790aba539dac1177"
+dot-prop@^4.1.0:
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-4.2.0.tgz#1f19e0c2e1aa0e32797c49799f2837ac6af69c57"
   dependencies:
     is-obj "^1.0.0"
 
@@ -1652,11 +1670,9 @@ dotenv@4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-4.0.0.tgz#864ef1379aced55ce6f95debecdce179f7a0cd1d"
 
-duplexer2@^0.1.4:
+duplexer3@^0.1.4:
   version "0.1.4"
-  resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1"
-  dependencies:
-    readable-stream "^2.0.2"
+  resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2"
 
 duplexer@^0.1.1:
   version "0.1.1"
@@ -1673,8 +1689,8 @@ ee-first@1.1.1:
   resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
 
 electron-to-chromium@^1.2.7, electron-to-chromium@^1.3.30:
-  version "1.3.31"
-  resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.31.tgz#00d832cba9fe2358652b0c48a8816c8e3a037e9f"
+  version "1.3.32"
+  resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.32.tgz#11d0684c0840e003c4be8928f8ac5f35dbc2b4e6"
 
 elegant-spinner@^1.0.1:
   version "1.0.1"
@@ -1697,8 +1713,8 @@ emojis-list@^2.0.0:
   resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389"
 
 encodeurl@~1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.1.tgz#79e3d58655346909fe6f0f45a5de68103b294d20"
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
 
 encoding@^0.1.11:
   version "0.1.12"
@@ -1817,8 +1833,8 @@ es6-map@^0.1.3:
     event-emitter "~0.3.5"
 
 es6-promise@^4.0.5:
-  version "4.2.2"
-  resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.2.tgz#f722d7769af88bd33bc13ec6605e1f92966b82d9"
+  version "4.2.4"
+  resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.4.tgz#dc4221c2b16518760bd8c39a52d8f356fc00ed29"
 
 es6-set@~0.1.5:
   version "0.1.5"
@@ -1963,18 +1979,6 @@ execa@^0.8.0:
     signal-exit "^3.0.0"
     strip-eof "^1.0.0"
 
-execa@^0.9.0:
-  version "0.9.0"
-  resolved "https://registry.yarnpkg.com/execa/-/execa-0.9.0.tgz#adb7ce62cf985071f60580deb4a88b9e34712d01"
-  dependencies:
-    cross-spawn "^5.0.1"
-    get-stream "^3.0.0"
-    is-stream "^1.1.0"
-    npm-run-path "^2.0.0"
-    p-finally "^1.0.0"
-    signal-exit "^3.0.0"
-    strip-eof "^1.0.0"
-
 exenv@^1.2.0:
   version "1.2.2"
   resolved "https://registry.yarnpkg.com/exenv/-/exenv-1.2.2.tgz#2ae78e85d9894158670b03d47bec1f03bd91bb9d"
@@ -2178,10 +2182,6 @@ fill-range@^2.1.0:
     repeat-element "^1.1.2"
     repeat-string "^1.5.2"
 
-filled-array@^1.0.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/filled-array/-/filled-array-1.1.0.tgz#c3c4f6c663b923459a9aa29912d2d031f1507f84"
-
 finalhandler@1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.0.tgz#ce0b6855b45853e791b2fcc680046d88253dd7f5"
@@ -2219,6 +2219,12 @@ flatten@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.2.tgz#dae46a9d78fbe25292258cc1e780a41d95c03782"
 
+follow-redirects@^1.2.5:
+  version "1.4.1"
+  resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.4.1.tgz#d8120f4518190f55aac65bb6fc7b85fcd666d6aa"
+  dependencies:
+    debug "^3.1.0"
+
 for-in@^1.0.1:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
@@ -2412,6 +2418,12 @@ glob@^7.0.3, glob@^7.0.5, glob@^7.1.1:
     once "^1.3.0"
     path-is-absolute "^1.0.0"
 
+global-dirs@^0.1.0:
+  version "0.1.1"
+  resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-0.1.1.tgz#b319c0dd4607f353f3be9cca4c72fc148c49f445"
+  dependencies:
+    ini "^1.3.4"
+
 global-modules@1.0.0, global-modules@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-1.0.0.tgz#6d770f0eb523ac78164d72b5e71a8877265cc3ea"
@@ -2455,24 +2467,20 @@ globby@^6.1.0:
     pify "^2.0.0"
     pinkie-promise "^2.0.0"
 
-got@^5.0.0:
-  version "5.7.1"
-  resolved "https://registry.yarnpkg.com/got/-/got-5.7.1.tgz#5f81635a61e4a6589f180569ea4e381680a51f35"
+got@^6.7.1:
+  version "6.7.1"
+  resolved "https://registry.yarnpkg.com/got/-/got-6.7.1.tgz#240cd05785a9a18e561dc1b44b41c763ef1e8db0"
   dependencies:
-    create-error-class "^3.0.1"
-    duplexer2 "^0.1.4"
+    create-error-class "^3.0.0"
+    duplexer3 "^0.1.4"
+    get-stream "^3.0.0"
     is-redirect "^1.0.0"
     is-retry-allowed "^1.0.0"
     is-stream "^1.0.0"
     lowercase-keys "^1.0.0"
-    node-status-codes "^1.0.0"
-    object-assign "^4.0.1"
-    parse-json "^2.1.0"
-    pinkie-promise "^2.0.0"
-    read-all-stream "^3.0.0"
-    readable-stream "^2.0.5"
-    timed-out "^3.0.0"
-    unzip-response "^1.0.2"
+    safe-buffer "^5.0.1"
+    timed-out "^4.0.0"
+    unzip-response "^2.0.1"
     url-parse-lax "^1.0.0"
 
 graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9:
@@ -2655,7 +2663,7 @@ html-comment-regex@^1.1.0:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/html-comment-regex/-/html-comment-regex-1.1.1.tgz#668b93776eaae55ebde8f3ad464b307a4963625e"
 
-html-encoding-sniffer@^1.0.1:
+html-encoding-sniffer@^1.0.1, html-encoding-sniffer@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz#e70d84b94da53aa375e11fe3a351be6642ca46f8"
   dependencies:
@@ -2723,8 +2731,8 @@ http-errors@1.6.2, http-errors@~1.6.2:
     statuses ">= 1.3.1 < 2"
 
 http-parser-js@>=0.4.0:
-  version "0.4.9"
-  resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.4.9.tgz#ea1a04fb64adff0242e9974f297dd4c3cad271e1"
+  version "0.4.10"
+  resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.4.10.tgz#92c9c1374c35085f75db359ec56cc257cbb93fa4"
 
 http-proxy-middleware@~0.17.4:
   version "0.17.4"
@@ -2762,18 +2770,13 @@ https-browserify@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73"
 
-husky@^0.15.0-rc.2:
-  version "0.15.0-rc.2"
-  resolved "https://registry.yarnpkg.com/husky/-/husky-0.15.0-rc.2.tgz#5fac1eabf0ead19645eb3dc8bd14bc14fe16068e"
+husky@^0.14.3:
+  version "0.14.3"
+  resolved "https://registry.yarnpkg.com/husky/-/husky-0.14.3.tgz#c69ed74e2d2779769a17ba8399b54ce0b63c12c3"
   dependencies:
-    cosmiconfig "^4.0.0"
-    execa "^0.9.0"
-    is-ci "^1.1.0"
-    pkg-dir "^2.0.0"
-    pupa "^1.0.0"
-    read-pkg "^3.0.0"
-    run-node "^0.2.0"
-    slash "^1.0.0"
+    is-ci "^1.0.10"
+    normalize-path "^1.0.0"
+    strip-indent "^2.0.0"
 
 iconv-lite@0.4.19, iconv-lite@^0.4.17, iconv-lite@~0.4.13:
   version "0.4.19"
@@ -2793,6 +2796,10 @@ ieee754@^1.1.4:
   version "1.1.8"
   resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.8.tgz#be33d40ac10ef1926701f6f08a2d86fbfd1ad3e4"
 
+import-lazy@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-2.1.0.tgz#05698e3d45c88e8d7e9d92cb0584e77f096f3e43"
+
 import-local@^0.1.1:
   version "0.1.1"
   resolved "https://registry.yarnpkg.com/import-local/-/import-local-0.1.1.tgz#b1179572aacdc11c6a91009fb430dbcab5f668a8"
@@ -2931,7 +2938,7 @@ is-callable@^1.1.1, is-callable@^1.1.3:
   version "1.1.3"
   resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.3.tgz#86eb75392805ddc33af71c92a0eedf74ee7604b2"
 
-is-ci@^1.0.10, is-ci@^1.1.0:
+is-ci@^1.0.10:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-1.1.0.tgz#247e4162e7860cebbdaf30b774d6b0ac7dcfe7a5"
   dependencies:
@@ -3013,6 +3020,13 @@ is-hexadecimal@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/is-hexadecimal/-/is-hexadecimal-1.0.1.tgz#6e084bbc92061fbb0971ec58b6ce6d404e24da69"
 
+is-installed-globally@^0.1.0:
+  version "0.1.0"
+  resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.1.0.tgz#0dfd98f5a9111716dd535dda6492f67bf3d25a80"
+  dependencies:
+    global-dirs "^0.1.0"
+    is-path-inside "^1.0.0"
+
 is-npm@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-1.0.0.tgz#f2fb63a65e4905b406c86072765a1a4dc793b9f4"
@@ -3585,8 +3599,8 @@ jest@20.0.4:
     jest-cli "^20.0.4"
 
 js-base64@^2.1.9:
-  version "2.4.1"
-  resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.4.1.tgz#e02813181cd53002888e918935467acb2910e596"
+  version "2.4.3"
+  resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.4.3.tgz#2e545ec2b0f2957f41356510205214e98fad6582"
 
 js-tokens@^3.0.0, js-tokens@^3.0.2:
   version "3.0.2"
@@ -3611,33 +3625,35 @@ jsbn@~0.1.0:
   resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
 
 jsdom@^11.5.1:
-  version "11.5.1"
-  resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-11.5.1.tgz#5df753b8d0bca20142ce21f4f6c039f99a992929"
+  version "11.6.2"
+  resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-11.6.2.tgz#25d1ef332d48adf77fc5221fe2619967923f16bb"
   dependencies:
-    abab "^1.0.3"
-    acorn "^5.1.2"
-    acorn-globals "^4.0.0"
+    abab "^1.0.4"
+    acorn "^5.3.0"
+    acorn-globals "^4.1.0"
     array-equal "^1.0.0"
     browser-process-hrtime "^0.1.2"
-    content-type-parser "^1.0.1"
+    content-type-parser "^1.0.2"
     cssom ">= 0.3.2 < 0.4.0"
     cssstyle ">= 0.2.37 < 0.3.0"
     domexception "^1.0.0"
     escodegen "^1.9.0"
-    html-encoding-sniffer "^1.0.1"
+    html-encoding-sniffer "^1.0.2"
     left-pad "^1.2.0"
     nwmatcher "^1.4.3"
-    parse5 "^3.0.2"
-    pn "^1.0.0"
+    parse5 "4.0.0"
+    pn "^1.1.0"
     request "^2.83.0"
-    request-promise-native "^1.0.3"
-    sax "^1.2.1"
-    symbol-tree "^3.2.1"
+    request-promise-native "^1.0.5"
+    sax "^1.2.4"
+    symbol-tree "^3.2.2"
     tough-cookie "^2.3.3"
+    w3c-hr-time "^1.0.1"
     webidl-conversions "^4.0.2"
-    whatwg-encoding "^1.0.1"
-    whatwg-url "^6.3.0"
-    xml-name-validator "^2.0.1"
+    whatwg-encoding "^1.0.3"
+    whatwg-url "^6.4.0"
+    ws "^4.0.0"
+    xml-name-validator "^3.0.0"
 
 jsdom@^9.12.0:
   version "9.12.0"
@@ -3758,20 +3774,16 @@ klaw@^1.0.0:
   optionalDependencies:
     graceful-fs "^4.1.9"
 
-latest-version@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-2.0.0.tgz#56f8d6139620847b8017f8f1f4d78e211324168b"
+latest-version@^3.0.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-3.1.0.tgz#a205383fea322b33b5ae3b18abee0dc2f356ee15"
   dependencies:
-    package-json "^2.0.0"
+    package-json "^4.0.0"
 
 lazy-cache@^1.0.3:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-1.0.4.tgz#a1d78fc3a50474cb80845d3b3b6e1da49a446e8e"
 
-lazy-req@^1.1.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/lazy-req/-/lazy-req-1.1.0.tgz#bdaebead30f8d824039ce0ce149d4daa07ba1fac"
-
 lcid@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835"
@@ -3793,9 +3805,9 @@ levn@~0.3.0:
     prelude-ls "~1.1.2"
     type-check "~0.3.2"
 
-lint-staged@^6.0.0:
-  version "6.0.1"
-  resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-6.0.1.tgz#855f2993ab4a265430e2fd9828427e648d65e6b4"
+lint-staged@^6.1.0:
+  version "6.1.0"
+  resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-6.1.0.tgz#28f600c10a6cbd249ceb003118a1552e53544a93"
   dependencies:
     app-root-path "^2.0.0"
     chalk "^2.1.0"
@@ -3885,15 +3897,6 @@ load-json-file@^2.0.0:
     pify "^2.0.0"
     strip-bom "^3.0.0"
 
-load-json-file@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b"
-  dependencies:
-    graceful-fs "^4.1.2"
-    parse-json "^4.0.0"
-    pify "^3.0.0"
-    strip-bom "^3.0.0"
-
 loader-runner@^2.3.0:
   version "2.3.0"
   resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.3.0.tgz#f482aea82d543e07921700d5a46ef26fdac6b8a2"
@@ -3923,8 +3926,8 @@ locate-path@^2.0.0:
     path-exists "^3.0.0"
 
 lodash-es@^4.2.0, lodash-es@^4.2.1:
-  version "4.17.4"
-  resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.4.tgz#dcc1d7552e150a0640073ba9cb31d70f032950e7"
+  version "4.17.5"
+  resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.5.tgz#9fc6e737b1c4d151d8f9cae2247305d552ce748f"
 
 lodash._reinterpolate@~3.0.0:
   version "3.0.0"
@@ -3955,8 +3958,8 @@ lodash.isequal@^4.1.1:
   resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
 
 lodash.isfunction@^3.0.8:
-  version "3.0.8"
-  resolved "https://registry.yarnpkg.com/lodash.isfunction/-/lodash.isfunction-3.0.8.tgz#4db709fc81bc4a8fd7127a458a5346c5cdce2c6b"
+  version "3.0.9"
+  resolved "https://registry.yarnpkg.com/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz#06de25df4db327ac931981d1bdb067e5af68d051"
 
 lodash.isstring@^4.0.1:
   version "4.0.1"
@@ -3992,8 +3995,8 @@ lodash.uniq@^4.5.0:
   resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
 
 "lodash@>=3.5 <5", lodash@^4.13.1, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.2, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0:
-  version "4.17.4"
-  resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae"
+  version "4.17.5"
+  resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.5.tgz#99a92d65c0272debe8c96b6057bc8fbfa3bed511"
 
 log-symbols@^1.0.2:
   version "1.0.2"
@@ -4054,6 +4057,12 @@ macaddress@^0.2.8:
   version "0.2.8"
   resolved "https://registry.yarnpkg.com/macaddress/-/macaddress-0.2.8.tgz#5904dc537c39ec6dbefeae902327135fa8511f12"
 
+make-dir@^1.0.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.1.0.tgz#19b4369fe48c116f53c2af95ad102c0e39e85d51"
+  dependencies:
+    pify "^3.0.0"
+
 makeerror@1.0.x:
   version "1.0.11"
   resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.11.tgz#e01a5c9109f2af79660e4e8b9587790184f5a96c"
@@ -4171,8 +4180,8 @@ mime@^1.4.1, mime@^1.5.0:
   resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
 
 mimic-fn@^1.0.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.1.0.tgz#e667783d92e89dbd342818b5230b9d62a672ad18"
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022"
 
 minimalistic-assert@^1.0.0:
   version "1.0.0"
@@ -4206,7 +4215,7 @@ minimist@~0.0.1:
   version "0.0.10"
   resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf"
 
-mkdirp@0.5.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0, mkdirp@~0.5.1:
+mkdirp@0.5.x, "mkdirp@>=0.5 0", mkdirp@^0.5.1, mkdirp@~0.5.0, mkdirp@~0.5.1:
   version "0.5.1"
   resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903"
   dependencies:
@@ -4221,11 +4230,11 @@ multicast-dns-service-types@^1.1.0:
   resolved "https://registry.yarnpkg.com/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz#899f11d9686e5e05cb91b35d5f0e63b773cfc901"
 
 multicast-dns@^6.0.1:
-  version "6.2.2"
-  resolved "https://registry.yarnpkg.com/multicast-dns/-/multicast-dns-6.2.2.tgz#300b6133361f8aaaf2b8d1248e85c363fe5b95a0"
+  version "6.2.3"
+  resolved "https://registry.yarnpkg.com/multicast-dns/-/multicast-dns-6.2.3.tgz#a0ec7bd9055c4282f790c3c82f4e28db3b31b229"
   dependencies:
-    dns-packet "^1.0.1"
-    thunky "^0.1.0"
+    dns-packet "^1.3.1"
+    thunky "^1.0.2"
 
 mute-stream@0.0.7:
   version "0.0.7"
@@ -4270,9 +4279,9 @@ node-fetch@^1.0.1:
     encoding "^0.1.11"
     is-stream "^1.0.1"
 
-node-forge@0.6.33:
-  version "0.6.33"
-  resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.6.33.tgz#463811879f573d45155ad6a9f43dc296e8e85ebc"
+node-forge@0.7.1:
+  version "0.7.1"
+  resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.7.1.tgz#9da611ea08982f4b94206b3beb4cc9665f20c300"
 
 node-int64@^0.4.0:
   version "0.4.0"
@@ -4331,10 +4340,6 @@ node-pre-gyp@^0.6.36, node-pre-gyp@^0.6.39:
     tar "^2.2.1"
     tar-pack "^3.4.0"
 
-node-status-codes@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/node-status-codes/-/node-status-codes-1.0.0.tgz#5ae5541d024645d32a58fcddc9ceecea7ae3ac2f"
-
 nomnom@~1.6.2:
   version "1.6.2"
   resolved "https://registry.yarnpkg.com/nomnom/-/nomnom-1.6.2.tgz#84a66a260174408fc5b77a18f888eccc44fb6971"
@@ -4358,6 +4363,10 @@ normalize-package-data@^2.3.2, normalize-package-data@^2.3.4:
     semver "2 || 3 || 4 || 5"
     validate-npm-package-license "^3.0.1"
 
+normalize-path@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-1.0.0.tgz#32d0e472f91ff345701c15a8311018d3b0a90379"
+
 normalize-path@^2.0.0, normalize-path@^2.0.1:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9"
@@ -4579,7 +4588,7 @@ os-tmpdir@^1.0.0, os-tmpdir@^1.0.1, os-tmpdir@~1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
 
-osenv@^0.1.0, osenv@^0.1.4:
+osenv@^0.1.4:
   version "0.1.4"
   resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.4.tgz#42fe6d5953df06c8064be6f176c3d05aaaa34644"
   dependencies:
@@ -4610,11 +4619,11 @@ p-try@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3"
 
-package-json@^2.0.0:
-  version "2.4.0"
-  resolved "https://registry.yarnpkg.com/package-json/-/package-json-2.4.0.tgz#0d15bd67d1cbbddbb2ca222ff2edb86bcb31a8bb"
+package-json@^4.0.0:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/package-json/-/package-json-4.0.1.tgz#8869a0401253661c4c4ca3da6c2121ed555f5eed"
   dependencies:
-    got "^5.0.0"
+    got "^6.7.1"
     registry-auth-token "^3.0.1"
     registry-url "^3.0.3"
     semver "^5.1.0"
@@ -4659,7 +4668,7 @@ parse-glob@^3.0.4:
     is-extglob "^1.0.0"
     is-glob "^2.0.0"
 
-parse-json@^2.1.0, parse-json@^2.2.0:
+parse-json@^2.2.0:
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9"
   dependencies:
@@ -4676,11 +4685,15 @@ parse-passwd@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6"
 
+parse5@4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/parse5/-/parse5-4.0.0.tgz#6d78656e3da8d78b4ec0b906f7c08ef1dfe3f608"
+
 parse5@^1.5.1:
   version "1.5.1"
   resolved "https://registry.yarnpkg.com/parse5/-/parse5-1.5.1.tgz#9b7f3b0de32be78dc2401b17573ccaf0f6f59d94"
 
-parse5@^3.0.1, parse5@^3.0.2:
+parse5@^3.0.1:
   version "3.0.3"
   resolved "https://registry.yarnpkg.com/parse5/-/parse5-3.0.3.tgz#042f792ffdd36851551cf4e9e066b3874ab45b5c"
   dependencies:
@@ -4744,12 +4757,6 @@ path-type@^2.0.0:
   dependencies:
     pify "^2.0.0"
 
-path-type@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f"
-  dependencies:
-    pify "^3.0.0"
-
 pbkdf2@^3.0.3:
   version "3.0.14"
   resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.14.tgz#a35e13c64799b06ce15320f459c230e68e73bade"
@@ -4792,7 +4799,7 @@ pkg-dir@^2.0.0:
   dependencies:
     find-up "^2.1.0"
 
-pn@^1.0.0:
+pn@^1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/pn/-/pn-1.1.0.tgz#e2f4cef0e219f463c179ab37463e4e1ecdccbafb"
 
@@ -5081,8 +5088,8 @@ postcss@^5.0.10, postcss@^5.0.11, postcss@^5.0.12, postcss@^5.0.13, postcss@^5.0
     supports-color "^3.2.3"
 
 postcss@^6.0.0, postcss@^6.0.1, postcss@^6.0.13:
-  version "6.0.16"
-  resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.16.tgz#112e2fe2a6d2109be0957687243170ea5589e146"
+  version "6.0.17"
+  resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.17.tgz#e259a051ca513f81e9afd0c21f7f82eda50c65c5"
   dependencies:
     chalk "^2.3.0"
     source-map "^0.6.1"
@@ -5205,10 +5212,6 @@ punycode@^2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.0.tgz#5f863edc89b96db09074bad7947bf09056ca4e7d"
 
-pupa@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/pupa/-/pupa-1.0.0.tgz#9a9568a5af7e657b8462a6e9d5328743560ceff6"
-
 q@^1.1.2:
   version "1.5.1"
   resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"
@@ -5295,8 +5298,8 @@ raw-body@2.3.2:
     unpipe "1.0.0"
 
 rc@^1.0.1, rc@^1.1.6, rc@^1.1.7:
-  version "1.2.4"
-  resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.4.tgz#a0f606caae2a3b862bbd0ef85482c0125b315fa3"
+  version "1.2.5"
+  resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.5.tgz#275cd687f6e3b36cc756baa26dfee80a790301fd"
   dependencies:
     deep-extend "~0.4.0"
     ini "~1.3.0"
@@ -5349,8 +5352,8 @@ react-error-overlay@^3.0.0:
   resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-3.0.0.tgz#c2bc8f4d91f1375b3dad6d75265d51cd5eeaf655"
 
 react-markdown@^3.1.4:
-  version "3.1.4"
-  resolved "https://registry.yarnpkg.com/react-markdown/-/react-markdown-3.1.4.tgz#4f37aa5ac4f78f64f93b41b12ad0c08e92eb1680"
+  version "3.1.5"
+  resolved "https://registry.yarnpkg.com/react-markdown/-/react-markdown-3.1.5.tgz#b1de8e3d421d31b8e1a679c66380fff20ee9a096"
   dependencies:
     prop-types "^15.6.0"
     remark-parse "^4.0.0"
@@ -5359,8 +5362,8 @@ react-markdown@^3.1.4:
     xtend "^4.0.1"
 
 react-modal@^3.1.11:
-  version "3.1.11"
-  resolved "https://registry.yarnpkg.com/react-modal/-/react-modal-3.1.11.tgz#95c8223fcee7013258ad2d149c38c9f870c89958"
+  version "3.1.12"
+  resolved "https://registry.yarnpkg.com/react-modal/-/react-modal-3.1.12.tgz#e80ab4e553ce946a6c96faf85eb31e0f9bd07470"
   dependencies:
     exenv "^1.2.0"
     prop-types "^15.5.10"
@@ -5417,9 +5420,9 @@ react-router@^4.2.0:
     prop-types "^15.5.4"
     warning "^3.0.0"
 
-react-scripts-ts@2.12.0:
-  version "2.12.0"
-  resolved "https://registry.yarnpkg.com/react-scripts-ts/-/react-scripts-ts-2.12.0.tgz#3ee9e7f507dbfa8b07dcb8476f86af3131b38491"
+react-scripts-ts@^2.13.0:
+  version "2.13.0"
+  resolved "https://registry.yarnpkg.com/react-scripts-ts/-/react-scripts-ts-2.13.0.tgz#d5493035e6d521030f0d3e5e8e5b5872f7df1a28"
   dependencies:
     autoprefixer "7.1.6"
     case-sensitive-paths-webpack-plugin "2.1.1"
@@ -5443,10 +5446,9 @@ react-scripts-ts@2.12.0:
     sw-precache-webpack-plugin "0.11.4"
     ts-jest "^20.0.7"
     ts-loader "^2.3.7"
+    tsconfig-paths-webpack-plugin "^2.0.0"
     tslint "^5.7.0"
-    tslint-loader "^3.5.3"
     tslint-react "^3.2.0"
-    typescript "^2.6.2"
     url-loader "0.6.2"
     webpack "3.8.1"
     webpack-dev-server "2.9.4"
@@ -5472,13 +5474,6 @@ react@^16.2.0:
     object-assign "^4.1.1"
     prop-types "^15.6.0"
 
-read-all-stream@^3.0.0:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/read-all-stream/-/read-all-stream-3.1.0.tgz#35c3e177f2078ef789ee4bfafa4373074eaef4fa"
-  dependencies:
-    pinkie-promise "^2.0.0"
-    readable-stream "^2.0.0"
-
 read-pkg-up@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02"
@@ -5509,14 +5504,6 @@ read-pkg@^2.0.0:
     normalize-package-data "^2.3.2"
     path-type "^2.0.0"
 
-read-pkg@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-3.0.0.tgz#9cbc686978fee65d16c00e2b19c237fcf6e38389"
-  dependencies:
-    load-json-file "^4.0.0"
-    normalize-package-data "^2.3.2"
-    path-type "^3.0.0"
-
 readable-stream@1.0:
   version "1.0.34"
   resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c"
@@ -5526,7 +5513,7 @@ readable-stream@1.0:
     isarray "0.0.1"
     string_decoder "~0.10.x"
 
-readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.2.9, readable-stream@^2.3.3:
+readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.2.9, readable-stream@^2.3.3:
   version "2.3.3"
   resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.3.tgz#368f2512d79f9d46fdfc71349ae7878bbc1eb95c"
   dependencies:
@@ -5610,8 +5597,8 @@ regexpu-core@^1.0.0:
     regjsparser "^0.1.4"
 
 registry-auth-token@^3.0.1:
-  version "3.3.1"
-  resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-3.3.1.tgz#fb0d3289ee0d9ada2cbb52af5dfe66cb070d3006"
+  version "3.3.2"
+  resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-3.3.2.tgz#851fd49038eecb586911115af845260eec983f20"
   dependencies:
     rc "^1.1.6"
     safe-buffer "^5.0.1"
@@ -5694,7 +5681,7 @@ request-promise-core@1.1.1:
   dependencies:
     lodash "^4.13.1"
 
-request-promise-native@^1.0.3:
+request-promise-native@^1.0.5:
   version "1.0.5"
   resolved "https://registry.yarnpkg.com/request-promise-native/-/request-promise-native-1.0.5.tgz#5281770f68e0c9719e5163fd3fab482215f4fda5"
   dependencies:
@@ -5831,7 +5818,7 @@ right-align@^0.1.1:
   dependencies:
     align-text "^0.1.1"
 
-rimraf@2, rimraf@^2.2.8, rimraf@^2.4.4, rimraf@^2.5.1, rimraf@^2.6.1:
+rimraf@2, rimraf@^2.2.8, rimraf@^2.5.1, rimraf@^2.6.1:
   version "2.6.2"
   resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36"
   dependencies:
@@ -5857,10 +5844,6 @@ run-async@^2.2.0:
   dependencies:
     is-promise "^2.1.0"
 
-run-node@^0.2.0:
-  version "0.2.0"
-  resolved "https://registry.yarnpkg.com/run-node/-/run-node-0.2.0.tgz#b26e942e94205dedbe532cddf0fd1dbd56649af6"
-
 rx-lite-aggregates@^4.0.8:
   version "4.0.8"
   resolved "https://registry.yarnpkg.com/rx-lite-aggregates/-/rx-lite-aggregates-4.0.8.tgz#753b87a89a11c95467c4ac1626c4efc4e05c67be"
@@ -5893,7 +5876,7 @@ sane@~1.6.0:
     walker "~1.0.5"
     watch "~0.10.0"
 
-sax@^1.2.1, sax@~1.2.1:
+sax@^1.2.1, sax@^1.2.4, sax@~1.2.1:
   version "1.2.4"
   resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
 
@@ -5908,10 +5891,10 @@ select-hose@^2.0.0:
   resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca"
 
 selfsigned@^1.9.1:
-  version "1.10.1"
-  resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-1.10.1.tgz#bf8cb7b83256c4551e31347c6311778db99eec52"
+  version "1.10.2"
+  resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-1.10.2.tgz#b4449580d99929b65b10a48389301a6592088758"
   dependencies:
-    node-forge "0.6.33"
+    node-forge "0.7.1"
 
 semver-diff@^2.0.0:
   version "2.1.0"
@@ -5987,8 +5970,8 @@ setprototypeof@1.1.0:
   resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656"
 
 sha.js@^2.4.0, sha.js@^2.4.8:
-  version "2.4.9"
-  resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.9.tgz#98f64880474b74f4a38b8da9d3c0f2d104633e7d"
+  version "2.4.10"
+  resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.10.tgz#b1fde5cd7d11a5626638a07c604ab909cfa31f9b"
   dependencies:
     inherits "^2.0.1"
     safe-buffer "^5.0.1"
@@ -6028,10 +6011,6 @@ slice-ansi@0.0.4:
   version "0.0.4"
   resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-0.0.4.tgz#edbf8903f66f7ce2f8eafd6ceed65e264c831b35"
 
-slide@^1.1.5:
-  version "1.1.6"
-  resolved "https://registry.yarnpkg.com/slide/-/slide-1.1.6.tgz#56eb027d65b4d2dce6cb2e2d32c4d4afc9e1d707"
-
 sntp@1.x.x:
   version "1.0.9"
   resolved "https://registry.yarnpkg.com/sntp/-/sntp-1.0.9.tgz#6541184cc90aeea6c6e7b35e2659082443c66198"
@@ -6087,12 +6066,12 @@ source-map-support@^0.4.15, source-map-support@^0.4.4:
     source-map "^0.5.6"
 
 source-map-support@^0.5.0:
-  version "0.5.2"
-  resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.2.tgz#1a6297fd5b2e762b39688c7fc91233b60984f0a5"
+  version "0.5.3"
+  resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.3.tgz#2b3d5fff298cfa4d1afd7d4352d569e9a0158e76"
   dependencies:
     source-map "^0.6.0"
 
-source-map@0.5.x, source-map@^0.5.3, source-map@^0.5.6, source-map@~0.5.1, source-map@~0.5.6:
+source-map@0.5.x, source-map@^0.5.3, source-map@^0.5.6, source-map@^0.5.7, source-map@~0.5.1, source-map@~0.5.6:
   version "0.5.7"
   resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
 
@@ -6244,8 +6223,8 @@ string_decoder@~0.10.x:
   resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94"
 
 stringify-object@^3.2.0:
-  version "3.2.1"
-  resolved "https://registry.yarnpkg.com/stringify-object/-/stringify-object-3.2.1.tgz#2720c2eff940854c819f6ee252aaeb581f30624d"
+  version "3.2.2"
+  resolved "https://registry.yarnpkg.com/stringify-object/-/stringify-object-3.2.2.tgz#9853052e5a88fb605a44cd27445aa257ad7ffbcd"
   dependencies:
     get-own-enumerable-property-symbols "^2.0.1"
     is-obj "^1.0.1"
@@ -6287,7 +6266,11 @@ strip-indent@^1.0.1:
   dependencies:
     get-stdin "^4.0.1"
 
-strip-json-comments@~2.0.1:
+strip-indent@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-2.0.0.tgz#5ef8db295d01e6ed6cbf7aab96998d7822527b68"
+
+strip-json-comments@^2.0.1, strip-json-comments@~2.0.1:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
 
@@ -6347,8 +6330,8 @@ sw-precache-webpack-plugin@0.11.4:
     uglify-js "^3.0.13"
 
 sw-precache@^5.1.1:
-  version "5.2.0"
-  resolved "https://registry.yarnpkg.com/sw-precache/-/sw-precache-5.2.0.tgz#eb6225ce580ceaae148194578a0ad01ab7ea199c"
+  version "5.2.1"
+  resolved "https://registry.yarnpkg.com/sw-precache/-/sw-precache-5.2.1.tgz#06134f319eec68f3b9583ce9a7036b1c119f7179"
   dependencies:
     dom-urls "^1.1.0"
     es6-promise "^4.0.5"
@@ -6359,7 +6342,7 @@ sw-precache@^5.1.1:
     mkdirp "^0.5.1"
     pretty-bytes "^4.0.2"
     sw-toolbox "^3.4.0"
-    update-notifier "^1.0.3"
+    update-notifier "^2.3.0"
 
 sw-toolbox@^3.4.0:
   version "3.6.0"
@@ -6377,10 +6360,10 @@ symbol-observable@^0.2.2:
   resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-0.2.4.tgz#95a83db26186d6af7e7a18dbd9760a2f86d08f40"
 
 symbol-observable@^1.0.3:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.1.0.tgz#5c68fd8d54115d9dfb72a84720549222e8db9b32"
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"
 
-symbol-tree@^3.2.1:
+symbol-tree@^3.2.1, symbol-tree@^3.2.2:
   version "3.2.2"
   resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.2.tgz#ae27db38f660a7ae2e1c3b7d1bc290819b8519e6"
 
@@ -6409,6 +6392,12 @@ tar@^2.2.1:
     fstream "^1.0.2"
     inherits "2"
 
+term-size@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/term-size/-/term-size-1.2.0.tgz#458b83887f288fc56d6fffbfad262e26638efa69"
+  dependencies:
+    execa "^0.7.0"
+
 test-exclude@^4.1.1:
   version "4.1.1"
   resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-4.1.1.tgz#4d84964b0966b0087ecc334a2ce002d3d9341e26"
@@ -6431,21 +6420,21 @@ through@^2.3.6:
   version "2.3.8"
   resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
 
-thunky@^0.1.0:
-  version "0.1.0"
-  resolved "https://registry.yarnpkg.com/thunky/-/thunky-0.1.0.tgz#bf30146824e2b6e67b0f2d7a4ac8beb26908684e"
+thunky@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.0.2.tgz#a862e018e3fb1ea2ec3fce5d55605cf57f247371"
 
 time-stamp@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/time-stamp/-/time-stamp-2.0.0.tgz#95c6a44530e15ba8d6f4a3ecb8c3a3fac46da357"
 
-timed-out@^3.0.0:
-  version "3.1.3"
-  resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-3.1.3.tgz#95860bfcc5c76c277f8f8326fd0f5b2e20eba217"
+timed-out@^4.0.0:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-4.0.1.tgz#f32eacac5a175bea25d7fab565ab3ed8741ef56f"
 
 timers-browserify@^2.0.4:
-  version "2.0.4"
-  resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.4.tgz#96ca53f4b794a5e7c0e1bd7cc88a372298fa01e6"
+  version "2.0.6"
+  resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.6.tgz#241e76927d9ca05f4d959819022f5b3664b64bae"
   dependencies:
     setimmediate "^1.0.4"
 
@@ -6523,8 +6512,8 @@ ts-jest@^20.0.7:
     yargs "^8.0.1"
 
 ts-jest@^22.0.1:
-  version "22.0.1"
-  resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-22.0.1.tgz#48942936a466c2e76e259b02e2f1356f1839afc3"
+  version "22.0.3"
+  resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-22.0.3.tgz#4d30f42c1b26a78d438ad908f9b0bcb4b1e6e32b"
   dependencies:
     babel-core "^6.24.1"
     babel-plugin-istanbul "^4.1.4"
@@ -6535,7 +6524,7 @@ ts-jest@^22.0.1:
     jest-config "^22.0.1"
     pkg-dir "^2.0.0"
     source-map-support "^0.5.0"
-    yargs "^10.0.3"
+    yargs "^11.0.0"
 
 ts-loader@^2.3.7:
   version "2.3.7"
@@ -6546,23 +6535,28 @@ ts-loader@^2.3.7:
     loader-utils "^1.0.2"
     semver "^5.0.1"
 
+tsconfig-paths-webpack-plugin@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-2.0.0.tgz#7652dc684bb3206c8e7e446831ca01cbf4d11772"
+  dependencies:
+    chalk "^2.3.0"
+    tsconfig-paths "^3.1.1"
+
+tsconfig-paths@^3.1.1:
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.1.1.tgz#368478faed54a247f2c8e59e44cb81418b2ae515"
+  dependencies:
+    deepmerge "^2.0.1"
+    strip-bom "^3.0.0"
+    strip-json-comments "^2.0.1"
+
 tslib@^1.8.0, tslib@^1.8.1:
   version "1.9.0"
   resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.0.tgz#e37a86fda8cbbaf23a057f473c9f4dc64e5fc2e8"
 
 tslint-config-prettier@^1.6.0:
-  version "1.6.0"
-  resolved "https://registry.yarnpkg.com/tslint-config-prettier/-/tslint-config-prettier-1.6.0.tgz#fec1ee8fb07e8f033c63fed6b135af997f31962a"
-
-tslint-loader@^3.5.3:
-  version "3.5.3"
-  resolved "https://registry.yarnpkg.com/tslint-loader/-/tslint-loader-3.5.3.tgz#343f74122d94f356b689457d3f59f64a69ab606f"
-  dependencies:
-    loader-utils "^1.0.2"
-    mkdirp "^0.5.1"
-    object-assign "^4.1.1"
-    rimraf "^2.4.4"
-    semver "^5.3.0"
+  version "1.7.0"
+  resolved "https://registry.yarnpkg.com/tslint-config-prettier/-/tslint-config-prettier-1.7.0.tgz#1588f794c6863ba31420e8b1d14ae91326063815"
 
 tslint-react@^3.2.0, tslint-react@^3.4.0:
   version "3.4.0"
@@ -6588,8 +6582,8 @@ tslint@^5.7.0, tslint@^5.9.1:
     tsutils "^2.12.1"
 
 tsutils@^2.12.1, tsutils@^2.13.1:
-  version "2.19.1"
-  resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.19.1.tgz#76d7ebdea9d7a7bf4a05f50ead3701b0168708d7"
+  version "2.21.0"
+  resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.21.0.tgz#43466a2283a0abce64e2209bc732ad72f8a04fab"
   dependencies:
     tslib "^1.8.1"
 
@@ -6625,16 +6619,16 @@ typesafe-actions@^1.1.2:
   resolved "https://registry.yarnpkg.com/typesafe-actions/-/typesafe-actions-1.1.2.tgz#af88ede3ee254be425c3e0e02de11b182830ae48"
 
 typescript@^2.6.2:
-  version "2.6.2"
-  resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.6.2.tgz#3c5b6fd7f6de0914269027f03c0946758f7673a4"
+  version "2.7.1"
+  resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.7.1.tgz#bb3682c2c791ac90e7c6210b26478a8da085c359"
 
 ua-parser-js@^0.7.9:
   version "0.7.17"
   resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.17.tgz#e9ec5f9498b9ec910e7ae3ac626a805c4d09ecac"
 
 uglify-js@3.3.x, uglify-js@^3.0.13:
-  version "3.3.8"
-  resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.3.8.tgz#51e9a5db73afb53ac98603d08224edcd0be45fd8"
+  version "3.3.9"
+  resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.3.9.tgz#33869666c8ab7f7658ce3d22f0f1ced40097d33a"
   dependencies:
     commander "~2.13.0"
     source-map "~0.6.1"
@@ -6664,6 +6658,10 @@ uid-number@^0.0.6:
   version "0.0.6"
   resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81"
 
+ultron@~1.1.0:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.1.1.tgz#9fe1536a10a664a65266a1e3ccf85fd36302bc9c"
+
 underscore@~1.4.4:
   version "1.4.4"
   resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.4.4.tgz#61a6a32010622afa07963bf325203cf12239d604"
@@ -6701,6 +6699,12 @@ uniqs@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/uniqs/-/uniqs-2.0.0.tgz#ffede4b36b25290696e6e165d4a59edb998e6b02"
 
+unique-string@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-1.0.0.tgz#9e1057cca851abb93398f8b33ae187b99caec11a"
+  dependencies:
+    crypto-random-string "^1.0.0"
+
 unist-util-is@^2.1.1:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-2.1.1.tgz#0c312629e3f960c66e931e812d3d80e77010947b"
@@ -6729,22 +6733,23 @@ unpipe@1.0.0, unpipe@~1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
 
-unzip-response@^1.0.2:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/unzip-response/-/unzip-response-1.0.2.tgz#b984f0877fc0a89c2c773cc1ef7b5b232b5b06fe"
+unzip-response@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/unzip-response/-/unzip-response-2.0.1.tgz#d2f0f737d16b0615e72a6935ed04214572d56f97"
 
-update-notifier@^1.0.3:
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-1.0.3.tgz#8f92c515482bd6831b7c93013e70f87552c7cf5a"
+update-notifier@^2.3.0:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-2.3.0.tgz#4e8827a6bb915140ab093559d7014e3ebb837451"
   dependencies:
-    boxen "^0.6.0"
-    chalk "^1.0.0"
-    configstore "^2.0.0"
+    boxen "^1.2.1"
+    chalk "^2.0.1"
+    configstore "^3.0.0"
+    import-lazy "^2.1.0"
+    is-installed-globally "^0.1.0"
     is-npm "^1.0.0"
-    latest-version "^2.0.0"
-    lazy-req "^1.1.0"
+    latest-version "^3.0.0"
     semver-diff "^2.0.0"
-    xdg-basedir "^2.0.0"
+    xdg-basedir "^3.0.0"
 
 upper-case@^1.1.1:
   version "1.1.3"
@@ -6811,7 +6816,7 @@ utils-merge@1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
 
-uuid@^2.0.1, uuid@^2.0.2:
+uuid@^2.0.2:
   version "2.0.3"
   resolved "https://registry.yarnpkg.com/uuid/-/uuid-2.0.3.tgz#67e2e863797215530dff318e5bf9dcebfd47b21a"
 
@@ -6871,6 +6876,12 @@ vm-browserify@0.0.4:
   dependencies:
     indexof "0.0.1"
 
+w3c-hr-time@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.1.tgz#82ac2bff63d950ea9e3189a58a65625fedf19045"
+  dependencies:
+    browser-process-hrtime "^0.1.2"
+
 walker@~1.0.5:
   version "1.0.7"
   resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.7.tgz#2f7f9b8fd10d677262b18a884e28d19618e028fb"
@@ -7003,7 +7014,7 @@ websocket-extensions@>=0.1.1:
   version "0.1.3"
   resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.3.tgz#5d2ff22977003ec687a4b87073dfbbac146ccf29"
 
-whatwg-encoding@^1.0.1:
+whatwg-encoding@^1.0.1, whatwg-encoding@^1.0.3:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-1.0.3.tgz#57c235bc8657e914d24e1a397d3c82daee0a6ba3"
   dependencies:
@@ -7020,7 +7031,7 @@ whatwg-url@^4.3.0:
     tr46 "~0.0.3"
     webidl-conversions "^3.0.0"
 
-whatwg-url@^6.3.0:
+whatwg-url@^6.4.0:
   version "6.4.0"
   resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-6.4.0.tgz#08fdf2b9e872783a7a1f6216260a1d66cc722e08"
   dependencies:
@@ -7052,11 +7063,11 @@ wide-align@^1.1.0:
   dependencies:
     string-width "^1.0.2"
 
-widest-line@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-1.0.0.tgz#0c09c85c2a94683d0d7eaf8ee097d564bf0e105c"
+widest-line@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-2.0.0.tgz#0142a4e8a243f8882c0233aa0e0281aa76152273"
   dependencies:
-    string-width "^1.0.1"
+    string-width "^2.1.1"
 
 window-size@0.1.0:
   version "0.1.0"
@@ -7092,13 +7103,21 @@ wrappy@1:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
 
-write-file-atomic@^1.1.2:
-  version "1.3.4"
-  resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-1.3.4.tgz#f807a4f0b1d9e913ae7a48112e6cc3af1991b45f"
+write-file-atomic@^2.0.0:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-2.3.0.tgz#1ff61575c2e2a4e8e510d6fa4e243cce183999ab"
   dependencies:
     graceful-fs "^4.1.11"
     imurmurhash "^0.1.4"
-    slide "^1.1.5"
+    signal-exit "^3.0.2"
+
+ws@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/ws/-/ws-4.0.0.tgz#bfe1da4c08eeb9780b986e0e4d10eccd7345999f"
+  dependencies:
+    async-limiter "~1.0.0"
+    safe-buffer "~5.1.0"
+    ultron "~1.1.0"
 
 x-is-function@^1.0.4:
   version "1.0.4"
@@ -7108,11 +7127,9 @@ x-is-string@^0.1.0:
   version "0.1.0"
   resolved "https://registry.yarnpkg.com/x-is-string/-/x-is-string-0.1.0.tgz#474b50865af3a49a9c4657f05acd145458f77d82"
 
-xdg-basedir@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-2.0.0.tgz#edbc903cc385fc04523d966a335504b5504d1bd2"
-  dependencies:
-    os-homedir "^1.0.0"
+xdg-basedir@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-3.0.0.tgz#496b2cc109eca8dbacfe2dc72b603c17c5870ad4"
 
 xml-char-classes@^1.0.0:
   version "1.0.0"
@@ -7122,6 +7139,10 @@ xml-name-validator@^2.0.1:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-2.0.1.tgz#4d8b8f1eccd3419aa362061becef515e1e559635"
 
+xml-name-validator@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a"
+
 xtend@^4.0.0, xtend@^4.0.1:
   version "4.0.1"
   resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af"
@@ -7152,15 +7173,15 @@ yargs-parser@^7.0.0:
   dependencies:
     camelcase "^4.1.0"
 
-yargs-parser@^8.1.0:
-  version "8.1.0"
-  resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-8.1.0.tgz#f1376a33b6629a5d063782944da732631e966950"
+yargs-parser@^9.0.2:
+  version "9.0.2"
+  resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-9.0.2.tgz#9ccf6a43460fe4ed40a9bb68f48d43b8a68cc077"
   dependencies:
     camelcase "^4.1.0"
 
-yargs@^10.0.3:
-  version "10.1.1"
-  resolved "https://registry.yarnpkg.com/yargs/-/yargs-10.1.1.tgz#5fe1ea306985a099b33492001fa19a1e61efe285"
+yargs@^11.0.0:
+  version "11.0.0"
+  resolved "https://registry.yarnpkg.com/yargs/-/yargs-11.0.0.tgz#c052931006c5eee74610e5fc0354bedfd08a201b"
   dependencies:
     cliui "^4.0.0"
     decamelize "^1.1.1"
@@ -7173,7 +7194,7 @@ yargs@^10.0.3:
     string-width "^2.0.0"
     which-module "^2.0.0"
     y18n "^3.2.1"
-    yargs-parser "^8.1.0"
+    yargs-parser "^9.0.2"
 
 yargs@^6.6.0:
   version "6.6.0"