diff --git a/ui/.bowerrc b/ui/.bowerrc
new file mode 100644
index 000000000..959e1696e
--- /dev/null
+++ b/ui/.bowerrc
@@ -0,0 +1,4 @@
+{
+ "directory": "bower_components",
+ "analytics": false
+}
diff --git a/ui/.editorconfig b/ui/.editorconfig
new file mode 100644
index 000000000..219985c22
--- /dev/null
+++ b/ui/.editorconfig
@@ -0,0 +1,20 @@
+# EditorConfig helps developers define and maintain consistent
+# coding styles between different editors and IDEs
+# editorconfig.org
+
+root = true
+
+
+[*]
+end_of_line = lf
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
+indent_style = space
+indent_size = 2
+
+[*.hbs]
+insert_final_newline = false
+
+[*.{diff,md}]
+trim_trailing_whitespace = false
diff --git a/ui/.ember-cli b/ui/.ember-cli
new file mode 100644
index 000000000..322549174
--- /dev/null
+++ b/ui/.ember-cli
@@ -0,0 +1,10 @@
+{
+ /**
+ Ember CLI sends analytics information by default. The data is completely
+ anonymous, but there are times when you might want to disable this behavior.
+
+ Setting `disableAnalytics` to true will prevent any data from being sent.
+ */
+ "disableAnalytics": true,
+ "output-path": "../pkg/web_ui"
+}
diff --git a/ui/.eslintrc.js b/ui/.eslintrc.js
new file mode 100644
index 000000000..7784839c1
--- /dev/null
+++ b/ui/.eslintrc.js
@@ -0,0 +1,24 @@
+module.exports = {
+ root: true,
+ parserOptions: {
+ ecmaVersion: 2017,
+ sourceType: 'module',
+ "ecmaFeatures": {
+ "experimentalObjectRestSpread": true
+ }
+ },
+ extends: 'eslint:recommended',
+ env: {
+ browser: true,
+ es6: true,
+ },
+ rules: {
+ "no-unused-vars": ["error", { "ignoreRestSiblings": true }]
+ },
+ globals: {
+ base64js: true,
+ TextEncoderLite: true,
+ TextDecoderLite: true,
+ Duration: true
+ }
+};
diff --git a/ui/.gitignore b/ui/.gitignore
new file mode 100644
index 000000000..8fa39a63c
--- /dev/null
+++ b/ui/.gitignore
@@ -0,0 +1,23 @@
+# See https://help.github.com/ignore-files/ for more about ignoring files.
+
+# compiled output
+/dist
+/tmp
+
+# dependencies
+/node_modules
+/bower_components
+
+# misc
+/.sass-cache
+/connect.lock
+/coverage/*
+/libpeerconnection.log
+npm-debug.log*
+yarn-error.log
+testem.log
+
+# ember-try
+.node_modules.ember-try/
+bower.json.ember-try
+package.json.ember-try
diff --git a/ui/.travis.yml b/ui/.travis.yml
new file mode 100644
index 000000000..d98801699
--- /dev/null
+++ b/ui/.travis.yml
@@ -0,0 +1,25 @@
+---
+language: node_js
+node_js:
+ - "4"
+
+sudo: false
+
+cache:
+ directories:
+ - $HOME/.npm
+ - $HOME/.cache # includes bowers cache
+
+before_install:
+ - npm config set spin false
+ - npm install -g bower
+ - bower --version
+ - npm install phantomjs-prebuilt
+ - node_modules/phantomjs-prebuilt/bin/phantomjs --version
+
+install:
+ - npm install
+ - bower install
+
+script:
+ - npm test
diff --git a/ui/.watchmanconfig b/ui/.watchmanconfig
new file mode 100644
index 000000000..e7834e3e4
--- /dev/null
+++ b/ui/.watchmanconfig
@@ -0,0 +1,3 @@
+{
+ "ignore_dirs": ["tmp", "dist"]
+}
diff --git a/ui/README.md b/ui/README.md
new file mode 100644
index 000000000..ae4964a23
--- /dev/null
+++ b/ui/README.md
@@ -0,0 +1,45 @@
+# vault
+
+This README outlines the details of collaborating on this Ember application.
+A short introduction of this app could easily go here.
+
+## Prerequisites
+
+You will need the following things properly installed on your computer.
+
+* [Git](https://git-scm.com/)
+* [Node.js](https://nodejs.org/) (with NPM)
+* [Bower](https://bower.io/)
+* [Ember CLI](https://ember-cli.com/)
+
+
+## Running / Development
+
+* `ember serve`
+* Visit your app at [http://localhost:4200](http://localhost:4200).
+
+### Code Generators
+
+Make use of the many generators for code, try `ember help generate` for more details
+
+### Running Tests
+
+* `ember test`
+* `ember test --server`
+
+### Building
+
+* `ember build` (development)
+* `ember build --environment production` (production)
+
+### Deploying
+
+Specify what it takes to deploy your app.
+
+## Further Reading / Useful Links
+
+* [ember.js](http://emberjs.com/)
+* [ember-cli](https://ember-cli.com/)
+* Development Browser Extensions
+ * [ember inspector for chrome](https://chrome.google.com/webstore/detail/ember-inspector/bmdblncegkenkacieihfhpjfppoconhi)
+ * [ember inspector for firefox](https://addons.mozilla.org/en-US/firefox/addon/ember-inspector/)
diff --git a/ui/app/adapters/application.js b/ui/app/adapters/application.js
new file mode 100644
index 000000000..df076ae8f
--- /dev/null
+++ b/ui/app/adapters/application.js
@@ -0,0 +1,86 @@
+import Ember from 'ember';
+import DS from 'ember-data';
+import fetch from 'fetch';
+
+const POLLING_URL_PATTERNS = ['sys/seal-status', 'sys/health', 'sys/replication/status'];
+
+export default DS.RESTAdapter.extend({
+ auth: Ember.inject.service(),
+
+ flashMessages: Ember.inject.service(),
+
+ namespace: 'v1/sys',
+
+ shouldReloadAll() {
+ return true;
+ },
+
+ shouldReloadRecord() {
+ return true;
+ },
+
+ shouldBackgroundReloadRecord() {
+ return false;
+ },
+
+ _preRequest(url, options) {
+ const token = this.get('auth.currentToken');
+ if (token && !options.unauthenticated) {
+ options.headers = Ember.assign(options.headers || {}, {
+ 'X-Vault-Token': token,
+ });
+ if (options.wrapTTL) {
+ Ember.assign(options.headers, { 'X-Vault-Wrap-TTL': options.wrapTTL });
+ }
+ }
+ const isPolling = POLLING_URL_PATTERNS.some(str => url.includes(str));
+ if (!isPolling) {
+ this.get('auth').setLastFetch(Date.now());
+ }
+ if (this.get('auth.shouldRenew')) {
+ this.get('auth').renew();
+ }
+ options.timeout = 60000;
+ return options;
+ },
+
+ ajax(url, type, options = {}) {
+ let opts = this._preRequest(url, options);
+
+ return this._super(url, type, opts).then((...args) => {
+ const [resp] = args;
+ if (resp && resp.warnings) {
+ const flash = this.get('flashMessages');
+ resp.warnings.forEach(message => {
+ flash.info(message);
+ });
+ }
+ return Ember.RSVP.resolve(...args);
+ });
+ },
+
+ // for use on endpoints that don't return JSON responses
+ rawRequest(url, type, options = {}) {
+ let opts = this._preRequest(url, options);
+ return fetch(url, {
+ method: type | 'GET',
+ headers: opts.headers | {},
+ }).then(response => {
+ if (response.status >= 200 && response.status < 300) {
+ return Ember.RSVP.resolve(response);
+ } else {
+ return Ember.RSVP.reject();
+ }
+ });
+ },
+
+ handleResponse(status, headers, payload, requestData) {
+ const returnVal = this._super(...arguments);
+ // ember data errors don't have the status code, so we add it here
+ if (returnVal instanceof DS.AdapterError) {
+ Ember.set(returnVal, 'httpStatus', status);
+ Ember.set(returnVal, 'path', requestData.url);
+ }
+ return returnVal;
+ },
+});
diff --git a/ui/app/adapters/auth-config/_base.js b/ui/app/adapters/auth-config/_base.js
new file mode 100644
index 000000000..f2cd6780e
--- /dev/null
+++ b/ui/app/adapters/auth-config/_base.js
@@ -0,0 +1,38 @@
+import ApplicationAdapter from '../application';
+
+export default ApplicationAdapter.extend({
+ namespace: '/v1/auth',
+
+ pathForType(modelType) {
+ // we want the last part of the path
+ const type = modelType.split('/').pop();
+ if (type === 'identity-whitelist' || type === 'roletag-blacklist') {
+ return `tidy/${type}`;
+ }
+ return type;
+ },
+
+ buildURL(modelName, id, snapshot) {
+ const backendId = id ? id : snapshot.belongsTo('backend').id;
+ let url = `${this.get('namespace')}/${backendId}/config`;
+ // aws has a lot more config endpoints
+ if (modelName.includes('aws')) {
+ url = `${url}/${this.pathForType(modelName)}`;
+ }
+ return url;
+ },
+
+ createRecord(store, type, snapshot) {
+ const id = snapshot.belongsTo('backend').id;
+ return this._super(...arguments).then(() => {
+ return { id };
+ });
+ },
+
+ updateRecord(store, type, snapshot) {
+ const id = snapshot.belongsTo('backend').id;
+ return this._super(...arguments).then(() => {
+ return { id };
+ });
+ },
+});
diff --git a/ui/app/adapters/auth-config/aws/client.js b/ui/app/adapters/auth-config/aws/client.js
new file mode 100644
index 000000000..6d61a851d
--- /dev/null
+++ b/ui/app/adapters/auth-config/aws/client.js
@@ -0,0 +1,2 @@
+import AuthConfig from '../_base';
+export default AuthConfig.extend();
diff --git a/ui/app/adapters/auth-config/aws/identity-whitelist.js b/ui/app/adapters/auth-config/aws/identity-whitelist.js
new file mode 100644
index 000000000..6d61a851d
--- /dev/null
+++ b/ui/app/adapters/auth-config/aws/identity-whitelist.js
@@ -0,0 +1,2 @@
+import AuthConfig from '../_base';
+export default AuthConfig.extend();
diff --git a/ui/app/adapters/auth-config/aws/roletag-blacklist.js b/ui/app/adapters/auth-config/aws/roletag-blacklist.js
new file mode 100644
index 000000000..6d61a851d
--- /dev/null
+++ b/ui/app/adapters/auth-config/aws/roletag-blacklist.js
@@ -0,0 +1,2 @@
+import AuthConfig from '../_base';
+export default AuthConfig.extend();
diff --git a/ui/app/adapters/auth-config/gcp.js b/ui/app/adapters/auth-config/gcp.js
new file mode 100644
index 000000000..21f5624ac
--- /dev/null
+++ b/ui/app/adapters/auth-config/gcp.js
@@ -0,0 +1,2 @@
+import AuthConfig from './_base';
+export default AuthConfig.extend();
diff --git a/ui/app/adapters/auth-config/github.js b/ui/app/adapters/auth-config/github.js
new file mode 100644
index 000000000..21f5624ac
--- /dev/null
+++ b/ui/app/adapters/auth-config/github.js
@@ -0,0 +1,2 @@
+import AuthConfig from './_base';
+export default AuthConfig.extend();
diff --git a/ui/app/adapters/auth-config/kubernetes.js b/ui/app/adapters/auth-config/kubernetes.js
new file mode 100644
index 000000000..21f5624ac
--- /dev/null
+++ b/ui/app/adapters/auth-config/kubernetes.js
@@ -0,0 +1,2 @@
+import AuthConfig from './_base';
+export default AuthConfig.extend();
diff --git a/ui/app/adapters/auth-config/ldap.js b/ui/app/adapters/auth-config/ldap.js
new file mode 100644
index 000000000..21f5624ac
--- /dev/null
+++ b/ui/app/adapters/auth-config/ldap.js
@@ -0,0 +1,2 @@
+import AuthConfig from './_base';
+export default AuthConfig.extend();
diff --git a/ui/app/adapters/auth-config/okta.js b/ui/app/adapters/auth-config/okta.js
new file mode 100644
index 000000000..21f5624ac
--- /dev/null
+++ b/ui/app/adapters/auth-config/okta.js
@@ -0,0 +1,2 @@
+import AuthConfig from './_base';
+export default AuthConfig.extend();
diff --git a/ui/app/adapters/auth-config/radius.js b/ui/app/adapters/auth-config/radius.js
new file mode 100644
index 000000000..21f5624ac
--- /dev/null
+++ b/ui/app/adapters/auth-config/radius.js
@@ -0,0 +1,2 @@
+import AuthConfig from './_base';
+export default AuthConfig.extend();
diff --git a/ui/app/adapters/auth-method.js b/ui/app/adapters/auth-method.js
new file mode 100644
index 000000000..03d564290
--- /dev/null
+++ b/ui/app/adapters/auth-method.js
@@ -0,0 +1,41 @@
+import Ember from 'ember';
+import ApplicationAdapter from './application';
+import DS from 'ember-data';
+
+export default ApplicationAdapter.extend({
+ url(path) {
+ const url = `${this.buildURL()}/auth`;
+ return path ? url + '/' + path : url;
+ },
+
+ // used in updateRecord on the model#tune action
+ pathForType() {
+ return 'mounts/auth';
+ },
+
+ findAll() {
+ return this.ajax(this.url(), 'GET').catch(e => {
+ if (e instanceof DS.AdapterError) {
+ Ember.set(e, 'policyPath', 'sys/auth');
+ }
+ throw e;
+ });
+ },
+
+ createRecord(store, type, snapshot) {
+ const serializer = store.serializerFor(type.modelName);
+ const data = serializer.serialize(snapshot);
+ const path = snapshot.attr('path');
+
+ return this.ajax(this.url(path), 'POST', { data }).then(() => {
+ // ember data doesn't like 204s if it's not a DELETE
+ return {
+ data: Ember.assign({}, data, { path: path + '/', id: path }),
+ };
+ });
+ },
+
+ urlForDeleteRecord(id, modelName, snapshot) {
+ return this.url(snapshot.id);
+ },
+});
diff --git a/ui/app/adapters/capabilities.js b/ui/app/adapters/capabilities.js
new file mode 100644
index 000000000..5c110f2ba
--- /dev/null
+++ b/ui/app/adapters/capabilities.js
@@ -0,0 +1,26 @@
+import ApplicationAdapter from './application';
+import DS from 'ember-data';
+import Ember from 'ember';
+
+export default ApplicationAdapter.extend({
+ pathForType() {
+ return 'capabilities-self';
+ },
+
+ findRecord(store, type, id) {
+ return this.ajax(this.buildURL(type), 'POST', { data: { path: id } }).catch(e => {
+ if (e instanceof DS.AdapterError) {
+ Ember.set(e, 'policyPath', 'sys/capabilities-self');
+ }
+ throw e;
+ });
+ },
+
+ queryRecord(store, type, query) {
+ const { id } = query;
+ return this.findRecord(store, type, id).then(resp => {
+ resp.path = id;
+ return resp;
+ });
+ },
+});
diff --git a/ui/app/adapters/cluster.js b/ui/app/adapters/cluster.js
new file mode 100644
index 000000000..b8bc36eaa
--- /dev/null
+++ b/ui/app/adapters/cluster.js
@@ -0,0 +1,187 @@
+import Ember from 'ember';
+import ApplicationAdapter from './application';
+import DS from 'ember-data';
+
+const { AdapterError } = DS;
+const { assert, inject } = Ember;
+
+const ENDPOINTS = ['health', 'seal-status', 'tokens', 'token', 'seal', 'unseal', 'init', 'capabilities-self'];
+
+const REPLICATION_ENDPOINTS = {
+ reindex: 'reindex',
+ recover: 'recover',
+ status: 'status',
+
+ primary: ['enable', 'disable', 'demote', 'secondary-token', 'revoke-secondary'],
+
+ secondary: ['enable', 'disable', 'promote', 'update-primary'],
+};
+
+const REPLICATION_MODES = ['dr', 'performance'];
+export default ApplicationAdapter.extend({
+ version: inject.service(),
+ shouldBackgroundReloadRecord() {
+ return true;
+ },
+ findRecord(store, type, id, snapshot) {
+ let fetches = {
+ health: this.health(),
+ sealStatus: this.sealStatus().catch(e => e),
+ };
+ if (this.get('version.isEnterprise')) {
+ fetches.replicationStatus = this.replicationStatus().catch(e => e);
+ }
+ return Ember.RSVP.hash(fetches).then(({ health, sealStatus, replicationStatus }) => {
+ let ret = {
+ id,
+ name: snapshot.attr('name'),
+ };
+ ret = Ember.assign(ret, health);
+ if (sealStatus instanceof AdapterError === false) {
+ ret = Ember.assign(ret, { nodes: [sealStatus] });
+ }
+ if (replicationStatus && replicationStatus instanceof AdapterError === false) {
+ ret = Ember.assign(ret, replicationStatus.data);
+ }
+ return Ember.RSVP.resolve(ret);
+ });
+ },
+
+ pathForType(type) {
+ return type === 'cluster' ? 'clusters' : Ember.String.pluralize(type);
+ },
+
+ health() {
+ return this.ajax(this.urlFor('health'), 'GET', {
+ data: { standbycode: 200, sealedcode: 200, uninitcode: 200, drsecondarycode: 200 },
+ unauthenticated: true,
+ });
+ },
+
+ features() {
+ return this.ajax(`${this.buildURL()}/license/features`, 'GET', {
+ unauthenticated: true,
+ });
+ },
+
+ sealStatus() {
+ return this.ajax(this.urlFor('seal-status'), 'GET', { unauthenticated: true });
+ },
+
+ seal() {
+ return this.ajax(this.urlFor('seal'), 'PUT');
+ },
+
+ unseal(data) {
+ return this.ajax(this.urlFor('unseal'), 'PUT', {
+ data,
+ unauthenticated: true,
+ });
+ },
+
+ initCluster(data) {
+ return this.ajax(this.urlFor('init'), 'PUT', {
+ data,
+ unauthenticated: true,
+ });
+ },
+
+ authenticate({ backend, data }) {
+ const { token, password, username, path } = data;
+ const url = this.urlForAuth(backend, username, path);
+ const verb = backend === 'token' ? 'GET' : 'POST';
+ let options = {
+ unauthenticated: true,
+ };
+ if (backend === 'token') {
+ options.headers = {
+ 'X-Vault-Token': token,
+ };
+ } else {
+ options.data = token ? { token, password } : { password };
+ }
+
+ return this.ajax(url, verb, options);
+ },
+
+ urlFor(endpoint) {
+ if (!ENDPOINTS.includes(endpoint)) {
+ throw new Error(
+ `Calls to a ${endpoint} endpoint are not currently allowed in the vault cluster adapater`
+ );
+ }
+ return `${this.buildURL()}/${endpoint}`;
+ },
+
+ urlForAuth(type, username, path) {
+ const authBackend = type.toLowerCase();
+ const authURLs = {
+ github: 'login',
+ userpass: `login/${encodeURIComponent(username)}`,
+ ldap: `login/${encodeURIComponent(username)}`,
+ okta: `login/${encodeURIComponent(username)}`,
+ token: 'lookup-self',
+ };
+ const urlSuffix = authURLs[authBackend];
+ const urlPrefix = path && authBackend !== 'token' ? path : authBackend;
+ if (!urlSuffix) {
+ throw new Error(`There is no auth url for ${type}.`);
+ }
+ return `/v1/auth/${urlPrefix}/${urlSuffix}`;
+ },
+
+ urlForReplication(replicationMode, clusterMode, endpoint) {
+ let suffix;
+ const errString = `Calls to replication ${endpoint} endpoint are not currently allowed in the vault cluster adapater`;
+ if (clusterMode) {
+ assert(errString, REPLICATION_ENDPOINTS[clusterMode].includes(endpoint));
+ suffix = `${replicationMode}/${clusterMode}/${endpoint}`;
+ } else {
+ assert(errString, REPLICATION_ENDPOINTS[endpoint]);
+ suffix = `${endpoint}`;
+ }
+ return `${this.buildURL()}/replication/${suffix}`;
+ },
+
+ replicationStatus() {
+ return this.ajax(`${this.buildURL()}/replication/status`, 'GET', { unauthenticated: true });
+ },
+
+ replicationDrPromote(data, options) {
+ const verb = options && options.checkStatus ? 'GET' : 'PUT';
+ return this.ajax(`${this.buildURL()}/replication/dr/secondary/promote`, verb, {
+ data,
+ unauthenticated: true,
+ });
+ },
+
+ generateDrOperationToken(data, options) {
+ const verb = options && options.checkStatus ? 'GET' : 'PUT';
+ let url = `${this.buildURL()}/replication/dr/secondary/generate-operation-token/`;
+ if (!data || data.pgp_key || data.otp) {
+ // start the generation
+ url = url + 'attempt';
+ } else {
+ // progress the operation
+ url = url + 'update';
+ }
+ return this.ajax(url, verb, {
+ data,
+ unauthenticated: true,
+ });
+ },
+
+ replicationAction(action, replicationMode, clusterMode, data) {
+ assert(
+ `${replicationMode} is an unsupported replication mode.`,
+ replicationMode && REPLICATION_MODES.includes(replicationMode)
+ );
+
+ const url =
+ action === 'recover' || action === 'reindex'
+ ? this.urlForReplication(replicationMode, null, action)
+ : this.urlForReplication(replicationMode, clusterMode, action);
+
+ return this.ajax(url, 'POST', { data });
+ },
+});
diff --git a/ui/app/adapters/iam-credential.js b/ui/app/adapters/iam-credential.js
new file mode 100644
index 000000000..fbd90a4cf
--- /dev/null
+++ b/ui/app/adapters/iam-credential.js
@@ -0,0 +1,26 @@
+import ApplicationAdapter from './application';
+
+export default ApplicationAdapter.extend({
+ namespace: 'v1',
+
+ url(role, isSTS) {
+ if (isSTS) {
+ return `/v1/${role.backend}/sts/${role.name}`;
+ }
+ return `/v1/${role.backend}/creds/${role.name}`;
+ },
+
+ createRecord(store, type, snapshot) {
+ const isSTS = snapshot.attr('withSTS');
+ const options = isSTS ? { data: { ttl: snapshot.attr('ttl') } } : {};
+ const method = isSTS ? 'POST' : 'GET';
+ const role = snapshot.attr('role');
+ const url = this.url(role, isSTS);
+
+ return this.ajax(url, method, options).then(response => {
+ response.id = snapshot.id;
+ response.modelName = type.modelName;
+ store.pushPayload(type.modelName, response);
+ });
+ },
+});
diff --git a/ui/app/adapters/identity/base.js b/ui/app/adapters/identity/base.js
new file mode 100644
index 000000000..6a6ed9fd2
--- /dev/null
+++ b/ui/app/adapters/identity/base.js
@@ -0,0 +1,23 @@
+import ApplicationAdapater from '../application';
+
+export default ApplicationAdapater.extend({
+ namespace: 'v1',
+ pathForType(type) {
+ return type;
+ },
+
+ urlForQuery() {
+ return this._super(...arguments) + '?list=true';
+ },
+
+ query(store, type) {
+ return this.ajax(this.buildURL(type.modelName, null, null, 'query'), 'GET');
+ },
+
+ buildURL(modelName, id, snapshot, requestType, query) {
+ if (requestType === 'createRecord') {
+ return this._super(...arguments);
+ }
+ return this._super(`${modelName}/id`, id, snapshot, requestType, query);
+ },
+});
diff --git a/ui/app/adapters/identity/entity-alias.js b/ui/app/adapters/identity/entity-alias.js
new file mode 100644
index 000000000..5f9b19fc1
--- /dev/null
+++ b/ui/app/adapters/identity/entity-alias.js
@@ -0,0 +1,3 @@
+import IdentityAdapter from './base';
+
+export default IdentityAdapter.extend();
diff --git a/ui/app/adapters/identity/entity-merge.js b/ui/app/adapters/identity/entity-merge.js
new file mode 100644
index 000000000..f087244c5
--- /dev/null
+++ b/ui/app/adapters/identity/entity-merge.js
@@ -0,0 +1,17 @@
+import IdentityAdapter from './base';
+
+export default IdentityAdapter.extend({
+ buildURL() {
+ // first arg is modelName which we're hardcoding in the call to _super.
+ let [, ...args] = arguments;
+ return this._super('identity/entity/merge', ...args);
+ },
+
+ createRecord(store, type, snapshot) {
+ return this._super(...arguments).then(() => {
+ // return the `to` id here so we can redirect to it on success
+ // (and because ember _loves_ 204s for createRecord)
+ return { id: snapshot.attr('toEntityId') };
+ });
+ },
+});
diff --git a/ui/app/adapters/identity/entity.js b/ui/app/adapters/identity/entity.js
new file mode 100644
index 000000000..7aecbfcd9
--- /dev/null
+++ b/ui/app/adapters/identity/entity.js
@@ -0,0 +1,18 @@
+import IdentityAdapter from './base';
+
+export default IdentityAdapter.extend({
+ lookup(store, data) {
+ let url = `/${this.urlPrefix()}/identity/lookup/entity`;
+ return this.ajax(url, 'POST', { data }).then(response => {
+ // unsuccessful lookup is a 204
+ if (!response) return;
+ let modelName = 'identity/entity';
+ store.push(
+ store
+ .serializerFor(modelName)
+ .normalizeResponse(store, store.modelFor(modelName), response, response.data.id, 'findRecord')
+ );
+ return response;
+ });
+ },
+});
diff --git a/ui/app/adapters/identity/group-alias.js b/ui/app/adapters/identity/group-alias.js
new file mode 100644
index 000000000..5f9b19fc1
--- /dev/null
+++ b/ui/app/adapters/identity/group-alias.js
@@ -0,0 +1,3 @@
+import IdentityAdapter from './base';
+
+export default IdentityAdapter.extend();
diff --git a/ui/app/adapters/identity/group.js b/ui/app/adapters/identity/group.js
new file mode 100644
index 000000000..bd12d8ceb
--- /dev/null
+++ b/ui/app/adapters/identity/group.js
@@ -0,0 +1,18 @@
+import IdentityAdapter from './base';
+
+export default IdentityAdapter.extend({
+ lookup(store, data) {
+ let url = `/${this.urlPrefix()}/identity/lookup/group`;
+ return this.ajax(url, 'POST', { data }).then(response => {
+ // unsuccessful lookup is a 204
+ if (!response) return;
+ let modelName = 'identity/group';
+ store.push(
+ store
+ .serializerFor(modelName)
+ .normalizeResponse(store, store.modelFor(modelName), response, response.data.id, 'findRecord')
+ );
+ return response;
+ });
+ },
+});
diff --git a/ui/app/adapters/lease.js b/ui/app/adapters/lease.js
new file mode 100644
index 000000000..4b2044159
--- /dev/null
+++ b/ui/app/adapters/lease.js
@@ -0,0 +1,57 @@
+import ApplicationAdapter from './application';
+
+export default ApplicationAdapter.extend({
+ revokePrefix(prefix) {
+ let url = this.buildURL() + '/leases/revoke-prefix/' + prefix;
+ url = url.replace(/\/$/, '');
+ return this.ajax(url, 'PUT');
+ },
+
+ forceRevokePrefix(prefix) {
+ let url = this.buildURL() + '/leases/revoke-prefix/' + prefix;
+ url = url.replace(/\/$/, '');
+ return this.ajax(url, 'PUT');
+ },
+
+ renew(lease_id, interval) {
+ let url = this.buildURL() + '/leases/renew';
+ return this.ajax(url, 'PUT', {
+ data: {
+ lease_id,
+ interval,
+ },
+ });
+ },
+
+ deleteRecord(store, type, snapshot) {
+ const lease_id = snapshot.id;
+ return this.ajax(this.buildURL() + '/leases/revoke', 'PUT', {
+ data: {
+ lease_id,
+ },
+ });
+ },
+
+ queryRecord(store, type, query) {
+ const { lease_id } = query;
+ return this.ajax(this.buildURL() + '/leases/lookup', 'PUT', {
+ data: {
+ lease_id,
+ },
+ });
+ },
+
+ query(store, type, query) {
+ const prefix = query.prefix || '';
+ return this.ajax(this.buildURL() + '/leases/lookup/' + prefix, 'GET', {
+ data: {
+ list: true,
+ },
+ }).then(resp => {
+ if (prefix) {
+ resp.prefix = prefix;
+ }
+ return resp;
+ });
+ },
+});
diff --git a/ui/app/adapters/mount-filter-config.js b/ui/app/adapters/mount-filter-config.js
new file mode 100644
index 000000000..597eedb0d
--- /dev/null
+++ b/ui/app/adapters/mount-filter-config.js
@@ -0,0 +1,28 @@
+import ApplicationAdapter from './application';
+
+export default ApplicationAdapter.extend({
+ url(id) {
+ return `${this.buildURL()}/replication/performance/primary/mount-filter/${id}`;
+ },
+
+ findRecord(store, type, id) {
+ return this.ajax(this.url(id), 'GET').then(resp => {
+ resp.id = id;
+ return resp;
+ });
+ },
+
+ createRecord(store, type, snapshot) {
+ return this.ajax(this.url(snapshot.id), 'PUT', {
+ data: this.serialize(snapshot),
+ });
+ },
+
+ updateRecord() {
+ return this.createRecord(...arguments);
+ },
+
+ deleteRecord(store, type, snapshot) {
+ return this.ajax(this.url(snapshot.id), 'DELETE');
+ },
+});
diff --git a/ui/app/adapters/node.js b/ui/app/adapters/node.js
new file mode 100644
index 000000000..c0a6b722e
--- /dev/null
+++ b/ui/app/adapters/node.js
@@ -0,0 +1,3 @@
+import ApplicationAdapter from './application';
+
+export default ApplicationAdapter.extend();
diff --git a/ui/app/adapters/pki-ca-certificate-sign.js b/ui/app/adapters/pki-ca-certificate-sign.js
new file mode 100644
index 000000000..fedb0b7d6
--- /dev/null
+++ b/ui/app/adapters/pki-ca-certificate-sign.js
@@ -0,0 +1,8 @@
+import Adapter from './pki';
+
+export default Adapter.extend({
+ url(_, snapshot) {
+ const backend = snapshot.attr('backend');
+ return `/v1/${backend}/root/sign-intermediate`;
+ },
+});
diff --git a/ui/app/adapters/pki-ca-certificate.js b/ui/app/adapters/pki-ca-certificate.js
new file mode 100644
index 000000000..f9910220a
--- /dev/null
+++ b/ui/app/adapters/pki-ca-certificate.js
@@ -0,0 +1,62 @@
+import ApplicationAdapter from './application';
+
+export default ApplicationAdapter.extend({
+ namespace: 'v1',
+
+ defaultSerializer: 'ssh',
+
+ url(snapshot, action) {
+ const { backend, caType, type } = snapshot.attributes();
+ if (action === 'sign-intermediate') {
+ return `/v1/${backend}/root/sign-intermediate`;
+ }
+ if (action === 'set-signed-intermediate') {
+ return `/v1/${backend}/intermediate/set-signed`;
+ }
+ if (action === 'upload') {
+ return `/v1/${backend}/config/ca`;
+ }
+ return `/v1/${backend}/${caType}/generate/${type}`;
+ },
+
+ createRecordOrUpdate(store, type, snapshot, requestType) {
+ const serializer = store.serializerFor(this.get('defaultSerializer'));
+ const isUpload = snapshot.attr('uploadPemBundle');
+ const isSetSignedIntermediate = snapshot.adapterOptions.method === 'setSignedIntermediate';
+ let action = snapshot.adapterOptions.method === 'signIntermediate' ? 'sign-intermediate' : null;
+ let data;
+ if (isUpload) {
+ action = 'upload';
+ data = { pem_bundle: snapshot.attr('pemBundle') };
+ } else if (isSetSignedIntermediate) {
+ action = 'set-signed-intermediate';
+ data = { certificate: snapshot.attr('certificate') };
+ } else {
+ data = serializer.serialize(snapshot, requestType);
+ }
+
+ return this.ajax(this.url(snapshot, action), 'POST', { data }).then(response => {
+ // uploading CA, setting signed intermediate cert, and attempting to generate
+ // a new CA if one exists, all return a 204
+ if (!response) {
+ response = {};
+ }
+ response.id = snapshot.id;
+ response.modelName = type.modelName;
+ store.pushPayload(type.modelName, response);
+ });
+ },
+
+ createRecord() {
+ return this.createRecordOrUpdate(...arguments);
+ },
+
+ updateRecord() {
+ return this.createRecordOrUpdate(...arguments);
+ },
+
+ deleteRecord(store, type, snapshot) {
+ const backend = snapshot.attr('backend');
+ return this.ajax(`/v1/${backend}/root`, 'DELETE');
+ },
+});
diff --git a/ui/app/adapters/pki-certificate-sign.js b/ui/app/adapters/pki-certificate-sign.js
new file mode 100644
index 000000000..eb5ca2615
--- /dev/null
+++ b/ui/app/adapters/pki-certificate-sign.js
@@ -0,0 +1,10 @@
+import Adapter from './pki';
+
+export default Adapter.extend({
+ url(role, snapshot) {
+ if (snapshot.attr('signVerbatim') === true) {
+ return `/v1/${role.backend}/sign-verbatim/${role.name}`;
+ }
+ return `/v1/${role.backend}/sign/${role.name}`;
+ },
+});
diff --git a/ui/app/adapters/pki-certificate.js b/ui/app/adapters/pki-certificate.js
new file mode 100644
index 000000000..8ec9553b2
--- /dev/null
+++ b/ui/app/adapters/pki-certificate.js
@@ -0,0 +1,66 @@
+import Ember from 'ember';
+import Adapter from './pki';
+
+export default Adapter.extend({
+ url(role) {
+ return `/v1/${role.backend}/issue/${role.name}`;
+ },
+
+ urlFor(backend, id) {
+ let url = `${this.buildURL()}/${backend}/certs`;
+ if (id) {
+ url = `${this.buildURL()}/${backend}/cert/${id}`;
+ }
+ return url;
+ },
+
+ optionsForQuery(id) {
+ let data = {};
+ if (!id) {
+ data['list'] = true;
+ }
+ return { data };
+ },
+
+ fetchByQuery(store, query) {
+ const { backend, id } = query;
+ return this.ajax(this.urlFor(backend, id), 'GET', this.optionsForQuery(id)).then(resp => {
+ const data = {
+ backend,
+ };
+ if (id) {
+ data.serial_number = id;
+ data.id = id;
+ data.id_for_nav = `cert/${id}`;
+ }
+ return Ember.assign({}, resp, data);
+ });
+ },
+
+ query(store, type, query) {
+ return this.fetchByQuery(store, query);
+ },
+
+ queryRecord(store, type, query) {
+ return this.fetchByQuery(store, query);
+ },
+
+ updateRecord(store, type, snapshot) {
+ if (snapshot.adapterOptions.method !== 'revoke') {
+ return;
+ }
+ const id = snapshot.id;
+ const backend = snapshot.record.get('backend');
+ const data = {
+ serial_number: id,
+ };
+ return this.ajax(`${this.buildURL()}/${backend}/revoke`, 'POST', { data }).then(resp => {
+ const data = {
+ id,
+ serial_number: id,
+ backend,
+ };
+ return Ember.assign({}, resp, data);
+ });
+ },
+});
diff --git a/ui/app/adapters/pki-config.js b/ui/app/adapters/pki-config.js
new file mode 100644
index 000000000..157274471
--- /dev/null
+++ b/ui/app/adapters/pki-config.js
@@ -0,0 +1,122 @@
+import ApplicationAdapter from './application';
+import DS from 'ember-data';
+import Ember from 'ember';
+
+export default ApplicationAdapter.extend({
+ namespace: 'v1',
+
+ defaultSerializer: 'config',
+
+ urlFor(backend, section) {
+ const urls = {
+ tidy: `/v1/${backend}/tidy`,
+ urls: `/v1/${backend}/config/urls`,
+ crl: `/v1/${backend}/config/crl`,
+ };
+ return urls[section];
+ },
+
+ createOrUpdate(store, type, snapshot) {
+ const url = this.urlFor(snapshot.record.get('backend'), snapshot.adapterOptions.method);
+ const serializer = store.serializerFor(this.get('defaultSerializer'));
+ if (!url) {
+ return;
+ }
+ const data = snapshot.adapterOptions.fields.reduce((data, field) => {
+ let attr = snapshot.attr(field);
+ if (attr) {
+ serializer.serializeAttribute(snapshot, data, field, attr);
+ } else {
+ data[serializer.keyForAttribute(field)] = attr;
+ }
+ return data;
+ }, {});
+ return this.ajax(url, 'POST', { data });
+ },
+
+ createRecord() {
+ return this.createOrUpdate(...arguments);
+ },
+
+ updateRecord() {
+ return this.createOrUpdate(...arguments, 'update');
+ },
+
+ fetchSection(backendPath, section) {
+ const sections = ['cert', 'urls', 'crl', 'tidy'];
+ if (!section || !sections.includes(section)) {
+ const error = new DS.AdapterError();
+ Ember.set(error, 'httpStatus', 404);
+ throw error;
+ }
+ return this[`fetch${Ember.String.capitalize(section)}`](backendPath);
+ },
+
+ id(backendPath) {
+ return backendPath + '-config-ca';
+ },
+
+ fetchCert(backendPath) {
+ // these are all un-authed so using `fetch` directly works
+ const derURL = `/v1/${backendPath}/ca`;
+ const pemURL = `${derURL}/pem`;
+ const chainURL = `${derURL}_chain`;
+
+ return Ember.RSVP.hash({
+ backend: backendPath,
+ id: this.id(backendPath),
+ der: this.rawRequest(derURL, { unauthenticate: true }).then(response => response.blob()),
+ pem: this.rawRequest(pemURL, { unauthenticate: true }).then(response => response.text()),
+ ca_chain: this.rawRequest(chainURL, { unauthenticate: true }).then(response => response.text()),
+ });
+ },
+
+ fetchUrls(backendPath) {
+ const url = `/v1/${backendPath}/config/urls`;
+ const id = this.id(backendPath);
+ return this.ajax(url, 'GET')
+ .then(resp => {
+ resp.id = id;
+ resp.backend = backendPath;
+ return resp;
+ })
+ .catch(e => {
+ if (e.httpStatus === 404) {
+ return Ember.RSVP.resolve({ id });
+ } else {
+ throw e;
+ }
+ });
+ },
+
+ fetchCrl(backendPath) {
+ const url = `/v1/${backendPath}/config/crl`;
+ const id = this.id(backendPath);
+ return this.ajax(url, 'GET')
+ .then(resp => {
+ resp.id = id;
+ resp.backend = backendPath;
+ return resp;
+ })
+ .catch(e => {
+ if (e.httpStatus === 404) {
+ return { id };
+ } else {
+ throw e;
+ }
+ });
+ },
+
+ fetchTidy(backendPath) {
+ const id = this.id(backendPath);
+ return Ember.RSVP.resolve({ id, backend: backendPath });
+ },
+
+ queryRecord(store, type, query) {
+ const { backend, section } = query;
+ return this.fetchSection(backend, section).then(resp => {
+ resp.backend = backend;
+ return resp;
+ });
+ },
+});
diff --git a/ui/app/adapters/pki.js b/ui/app/adapters/pki.js
new file mode 100644
index 000000000..bcebb8273
--- /dev/null
+++ b/ui/app/adapters/pki.js
@@ -0,0 +1,24 @@
+import ApplicationAdapter from './application';
+import Ember from 'ember';
+
+export default ApplicationAdapter.extend({
+ namespace: 'v1',
+
+ defaultSerializer: 'ssh',
+
+ url(/*role*/) {
+ Ember.assert('Override the `url` method to extend the SSH adapter', false);
+ },
+
+ createRecord(store, type, snapshot, requestType) {
+ const serializer = store.serializerFor(type.modelName);
+ const data = serializer.serialize(snapshot, requestType);
+ const role = snapshot.attr('role');
+
+ return this.ajax(this.url(role, snapshot), 'POST', { data }).then(response => {
+ response.id = snapshot.id;
+ response.modelName = type.modelName;
+ store.pushPayload(type.modelName, response);
+ });
+ },
+});
diff --git a/ui/app/adapters/policy.js b/ui/app/adapters/policy.js
new file mode 100644
index 000000000..5846e31d9
--- /dev/null
+++ b/ui/app/adapters/policy.js
@@ -0,0 +1,35 @@
+import Ember from 'ember';
+import ApplicationAdapter from './application';
+
+export default ApplicationAdapter.extend({
+ namespace: 'v1/sys',
+ pathForType(type) {
+ let path = type.replace('policy', 'policies');
+ return path;
+ },
+
+ createOrUpdate(store, type, snapshot) {
+ const serializer = store.serializerFor('policy');
+ const data = serializer.serialize(snapshot);
+ const name = snapshot.attr('name');
+
+ return this.ajax(this.buildURL(type.modelName, name), 'PUT', { data }).then(() => {
+ // doing this to make it like a Vault response - ember data doesn't like 204s if it's not a DELETE
+ return {
+ data: Ember.assign({}, snapshot.record.toJSON(), { id: name }),
+ };
+ });
+ },
+
+ createRecord() {
+ return this.createOrUpdate(...arguments);
+ },
+
+ updateRecord() {
+ return this.createOrUpdate(...arguments);
+ },
+
+ query(store, type) {
+ return this.ajax(this.buildURL(type.modelName), 'GET', { data: { list: true } });
+ },
+});
diff --git a/ui/app/adapters/policy/acl.js b/ui/app/adapters/policy/acl.js
new file mode 100644
index 000000000..ea0f1a321
--- /dev/null
+++ b/ui/app/adapters/policy/acl.js
@@ -0,0 +1,3 @@
+import PolicyAdapter from '../policy';
+
+export default PolicyAdapter.extend();
diff --git a/ui/app/adapters/policy/egp.js b/ui/app/adapters/policy/egp.js
new file mode 100644
index 000000000..ea0f1a321
--- /dev/null
+++ b/ui/app/adapters/policy/egp.js
@@ -0,0 +1,3 @@
+import PolicyAdapter from '../policy';
+
+export default PolicyAdapter.extend();
diff --git a/ui/app/adapters/policy/rgp.js b/ui/app/adapters/policy/rgp.js
new file mode 100644
index 000000000..ea0f1a321
--- /dev/null
+++ b/ui/app/adapters/policy/rgp.js
@@ -0,0 +1,3 @@
+import PolicyAdapter from '../policy';
+
+export default PolicyAdapter.extend();
diff --git a/ui/app/adapters/role-aws.js b/ui/app/adapters/role-aws.js
new file mode 100644
index 000000000..67f50ef77
--- /dev/null
+++ b/ui/app/adapters/role-aws.js
@@ -0,0 +1,69 @@
+import ApplicationAdapter from './application';
+import Ember from 'ember';
+
+export default ApplicationAdapter.extend({
+ namespace: 'v1',
+
+ createOrUpdate(store, type, snapshot, requestType) {
+ const serializer = store.serializerFor(type.modelName);
+ const data = serializer.serialize(snapshot, requestType);
+ const { id } = snapshot;
+ let url = this.urlForRole(snapshot.record.get('backend'), id);
+
+ return this.ajax(url, 'POST', { data });
+ },
+
+ createRecord() {
+ return this.createOrUpdate(...arguments);
+ },
+
+ updateRecord() {
+ return this.createOrUpdate(...arguments, 'update');
+ },
+
+ deleteRecord(store, type, snapshot) {
+ const { id } = snapshot;
+ return this.ajax(this.urlForRole(snapshot.record.get('backend'), id), 'DELETE');
+ },
+
+ pathForType() {
+ return 'roles';
+ },
+
+ urlForRole(backend, id) {
+ let url = `${this.buildURL()}/${backend}/roles`;
+ if (id) {
+ url = url + '/' + id;
+ }
+ return url;
+ },
+
+ optionsForQuery(id) {
+ let data = {};
+ if (!id) {
+ data['list'] = true;
+ }
+ return { data };
+ },
+
+ fetchByQuery(store, query) {
+ const { id, backend } = query;
+ return this.ajax(this.urlForRole(backend, id), 'GET', this.optionsForQuery(id)).then(resp => {
+ const data = {
+ id,
+ name: id,
+ backend,
+ };
+
+ return Ember.assign({}, resp, data);
+ });
+ },
+
+ query(store, type, query) {
+ return this.fetchByQuery(store, query);
+ },
+
+ queryRecord(store, type, query) {
+ return this.fetchByQuery(store, query);
+ },
+});
diff --git a/ui/app/adapters/role-pki.js b/ui/app/adapters/role-pki.js
new file mode 100644
index 000000000..67f50ef77
--- /dev/null
+++ b/ui/app/adapters/role-pki.js
@@ -0,0 +1,69 @@
+import ApplicationAdapter from './application';
+import Ember from 'ember';
+
+export default ApplicationAdapter.extend({
+ namespace: 'v1',
+
+ createOrUpdate(store, type, snapshot, requestType) {
+ const serializer = store.serializerFor(type.modelName);
+ const data = serializer.serialize(snapshot, requestType);
+ const { id } = snapshot;
+ let url = this.urlForRole(snapshot.record.get('backend'), id);
+
+ return this.ajax(url, 'POST', { data });
+ },
+
+ createRecord() {
+ return this.createOrUpdate(...arguments);
+ },
+
+ updateRecord() {
+ return this.createOrUpdate(...arguments, 'update');
+ },
+
+ deleteRecord(store, type, snapshot) {
+ const { id } = snapshot;
+ return this.ajax(this.urlForRole(snapshot.record.get('backend'), id), 'DELETE');
+ },
+
+ pathForType() {
+ return 'roles';
+ },
+
+ urlForRole(backend, id) {
+ let url = `${this.buildURL()}/${backend}/roles`;
+ if (id) {
+ url = url + '/' + id;
+ }
+ return url;
+ },
+
+ optionsForQuery(id) {
+ let data = {};
+ if (!id) {
+ data['list'] = true;
+ }
+ return { data };
+ },
+
+ fetchByQuery(store, query) {
+ const { id, backend } = query;
+ return this.ajax(this.urlForRole(backend, id), 'GET', this.optionsForQuery(id)).then(resp => {
+ const data = {
+ id,
+ name: id,
+ backend,
+ };
+
+ return Ember.assign({}, resp, data);
+ });
+ },
+
+ query(store, type, query) {
+ return this.fetchByQuery(store, query);
+ },
+
+ queryRecord(store, type, query) {
+ return this.fetchByQuery(store, query);
+ },
+});
diff --git a/ui/app/adapters/role-ssh.js b/ui/app/adapters/role-ssh.js
new file mode 100644
index 000000000..6f33ec4d3
--- /dev/null
+++ b/ui/app/adapters/role-ssh.js
@@ -0,0 +1,96 @@
+import ApplicationAdapter from './application';
+import Ember from 'ember';
+
+export default ApplicationAdapter.extend({
+ namespace: 'v1',
+
+ defaultSerializer: 'role',
+
+ createOrUpdate(store, type, snapshot, requestType) {
+ const serializer = store.serializerFor(type.modelName);
+ const data = serializer.serialize(snapshot, requestType);
+ const { id } = snapshot;
+ let url = this.urlForRole(snapshot.record.get('backend'), id);
+
+ return this.ajax(url, 'POST', { data });
+ },
+
+ createRecord() {
+ return this.createOrUpdate(...arguments);
+ },
+
+ updateRecord() {
+ return this.createOrUpdate(...arguments, 'update');
+ },
+
+ deleteRecord(store, type, snapshot) {
+ const { id } = snapshot;
+ return this.ajax(this.urlForRole(snapshot.record.get('backend'), id), 'DELETE');
+ },
+
+ pathForType() {
+ return 'roles';
+ },
+
+ urlForRole(backend, id) {
+ let url = `${this.buildURL()}/${backend}/roles`;
+ if (id) {
+ url = url + '/' + id;
+ }
+ return url;
+ },
+
+ optionsForQuery(id) {
+ let data = {};
+ if (!id) {
+ data['list'] = true;
+ }
+ return { data };
+ },
+
+ fetchByQuery(store, query) {
+ const { id, backend } = query;
+ let zeroAddressAjax = Ember.RSVP.resolve();
+ const queryAjax = this.ajax(this.urlForRole(backend, id), 'GET', this.optionsForQuery(id));
+ if (!id) {
+ zeroAddressAjax = this.findAllZeroAddress(store, query);
+ }
+
+ return Ember.RSVP.allSettled([queryAjax, zeroAddressAjax]).then(results => {
+ // query result 404d, so throw the adapterError
+ if (!results[0].value) {
+ throw results[0].reason;
+ }
+ let resp = {
+ id,
+ name: id,
+ backend,
+ };
+
+ results.forEach(result => {
+ if (result.value) {
+ if (result.value.data.roles) {
+ resp = Ember.assign({}, resp, { zero_address_roles: result.value.data.roles });
+ } else {
+ resp = Ember.assign({}, resp, result.value);
+ }
+ }
+ });
+ return resp;
+ });
+ },
+
+ findAllZeroAddress(store, query) {
+ const { backend } = query;
+ const url = `/v1/${backend}/config/zeroaddress`;
+ return this.ajax(url, 'GET');
+ },
+
+ query(store, type, query) {
+ return this.fetchByQuery(store, query);
+ },
+
+ queryRecord(store, type, query) {
+ return this.fetchByQuery(store, query);
+ },
+});
diff --git a/ui/app/adapters/secret-cubbyhole.js b/ui/app/adapters/secret-cubbyhole.js
new file mode 100644
index 000000000..389f10783
--- /dev/null
+++ b/ui/app/adapters/secret-cubbyhole.js
@@ -0,0 +1,61 @@
+import Ember from 'ember';
+import ApplicationAdapter from './application';
+
+export default ApplicationAdapter.extend({
+ namespace: 'v1',
+
+ createOrUpdate(store, type, snapshot) {
+ const serializer = store.serializerFor(type.modelName);
+ const data = serializer.serialize(snapshot);
+ const { id } = snapshot;
+
+ return this.ajax(this.urlForSecret(snapshot.attr('backend'), id), 'POST', { data });
+ },
+
+ createRecord() {
+ return this.createOrUpdate(...arguments);
+ },
+
+ updateRecord() {
+ return this.createOrUpdate(...arguments);
+ },
+
+ deleteRecord(store, type, snapshot) {
+ const { id } = snapshot;
+ return this.ajax(this.urlForSecret(snapshot.attr('backend'), id), 'DELETE');
+ },
+
+ urlForSecret(backend, id) {
+ let url = this.buildURL() + '/' + backend + '/';
+ if (!Ember.isEmpty(id)) {
+ url = url + id;
+ }
+
+ return url;
+ },
+
+ optionsForQuery(id, action) {
+ let data = {};
+ if (action === 'query') {
+ data['list'] = true;
+ }
+
+ return { data };
+ },
+
+ fetchByQuery(query, action) {
+ const { id, backend } = query;
+ return this.ajax(this.urlForSecret(backend, id), 'GET', this.optionsForQuery(id, action)).then(resp => {
+ resp.id = id;
+ return resp;
+ });
+ },
+
+ query(store, type, query) {
+ return this.fetchByQuery(query, 'query');
+ },
+
+ queryRecord(store, type, query) {
+ return this.fetchByQuery(query, 'queryRecord');
+ },
+});
diff --git a/ui/app/adapters/secret-engine.js b/ui/app/adapters/secret-engine.js
new file mode 100644
index 000000000..4c483f990
--- /dev/null
+++ b/ui/app/adapters/secret-engine.js
@@ -0,0 +1,102 @@
+import Ember from 'ember';
+import ApplicationAdapter from './application';
+import DS from 'ember-data';
+
+export default ApplicationAdapter.extend({
+ url(path) {
+ const url = `${this.buildURL()}/mounts`;
+ return path ? url + '/' + path : url;
+ },
+
+ pathForType(type) {
+ let path;
+ switch (type) {
+ case 'cluster':
+ path = 'clusters';
+ break;
+ case 'secret-engine':
+ path = 'mounts';
+ break;
+ default:
+ path = Ember.String.pluralize(type);
+ break;
+ }
+ return path;
+ },
+
+ query() {
+ return this.ajax(this.url(), 'GET').catch(e => {
+ if (e instanceof DS.AdapterError) {
+ Ember.set(e, 'policyPath', 'sys/mounts');
+ }
+
+ throw e;
+ });
+ },
+
+ createRecord(store, type, snapshot) {
+ const serializer = store.serializerFor(type.modelName);
+ const data = serializer.serialize(snapshot);
+ const path = snapshot.attr('path');
+
+ return this.ajax(this.url(path), 'POST', { data }).then(() => {
+ // ember data doesn't like 204s if it's not a DELETE
+ return {
+ data: Ember.assign({}, data, { path: path + '/', id: path }),
+ };
+ });
+ },
+
+ findRecord(store, type, path, snapshot) {
+ if (snapshot.attr('type') === 'ssh') {
+ return this.ajax(`/v1/${path}/config/ca`, 'GET');
+ }
+ return;
+ },
+
+ queryRecord(store, type, query) {
+ if (query.type === 'aws') {
+ return this.ajax(`/v1/${query.backend}/config/lease`, 'GET').then(resp => {
+ resp.path = query.backend + '/';
+ return resp;
+ });
+ }
+ return;
+ },
+
+ updateRecord(store, type, snapshot) {
+ const { apiPath, options, adapterMethod } = snapshot.adapterOptions;
+ if (adapterMethod) {
+ return this[adapterMethod](...arguments);
+ }
+ if (apiPath) {
+ const serializer = store.serializerFor(type.modelName);
+ const data = serializer.serialize(snapshot);
+ const path = snapshot.id;
+ return this.ajax(`/v1/${path}/${apiPath}`, options.isDelete ? 'DELETE' : 'POST', { data });
+ }
+ },
+
+ saveAWSRoot(store, type, snapshot) {
+ let { data } = snapshot.adapterOptions;
+ const path = snapshot.id;
+ return this.ajax(`/v1/${path}/config/root`, 'POST', { data });
+ },
+
+ saveAWSLease(store, type, snapshot) {
+ let { data } = snapshot.adapterOptions;
+ const path = snapshot.id;
+ return this.ajax(`/v1/${path}/config/lease`, 'POST', { data });
+ },
+
+ saveZeroAddressConfig(store, type, snapshot) {
+ const path = snapshot.id;
+ const roles = store.peekAll('role-ssh').filterBy('zeroAddress').mapBy('id').join(',');
+ const url = `/v1/${path}/config/zeroaddress`;
+ const data = { roles };
+ if (roles === '') {
+ return this.ajax(url, 'DELETE');
+ }
+ return this.ajax(url, 'POST', { data });
+ },
+});
diff --git a/ui/app/adapters/secret.js b/ui/app/adapters/secret.js
new file mode 100644
index 000000000..d056dc58a
--- /dev/null
+++ b/ui/app/adapters/secret.js
@@ -0,0 +1,86 @@
+import Ember from 'ember';
+import ApplicationAdapter from './application';
+const { computed } = Ember;
+
+export default ApplicationAdapter.extend({
+ namespace: 'v1',
+
+ headers: computed(function() {
+ return {
+ 'X-Vault-Kv-Client': 'v1',
+ };
+ }),
+
+ createOrUpdate(store, type, snapshot) {
+ const serializer = store.serializerFor(type.modelName);
+ const data = serializer.serialize(snapshot);
+ const { id } = snapshot;
+
+ return this.ajax(this.urlForSecret(snapshot.attr('backend'), id), 'POST', {
+ data: { data },
+ });
+ },
+
+ createRecord() {
+ return this.createOrUpdate(...arguments);
+ },
+
+ updateRecord() {
+ return this.createOrUpdate(...arguments);
+ },
+
+ deleteRecord(store, type, snapshot) {
+ const { id } = snapshot;
+ return this.ajax(this.urlForSecret(snapshot.attr('backend'), id), 'DELETE');
+ },
+
+ urlForSecret(backend, id, infix = 'data') {
+ let url = `${this.buildURL()}/${backend}/${infix}/`;
+ if (!Ember.isEmpty(id)) {
+ url = url + id;
+ }
+
+ return url;
+ },
+
+ optionsForQuery(id, action) {
+ let data = {};
+ if (action === 'query') {
+ data['list'] = true;
+ }
+
+ return { data };
+ },
+
+ urlForQuery(query) {
+ let { id, backend } = query;
+ return this.urlForSecret(backend, id, 'metadata');
+ },
+
+ urlForQueryRecord(query) {
+ let { id, backend } = query;
+ return this.urlForSecret(backend, id);
+ },
+
+ query(store, type, query) {
+ return this.ajax(
+ this.urlForQuery(query, type.modelName),
+ 'GET',
+ this.optionsForQuery(query.id, 'query')
+ ).then(resp => {
+ resp.id = query.id;
+ return resp;
+ });
+ },
+
+ queryRecord(store, type, query) {
+ return this.ajax(
+ this.urlForQueryRecord(query, type.modelName),
+ 'GET',
+ this.optionsForQuery(query.id, 'queryRecord')
+ ).then(resp => {
+ resp.id = query.id;
+ return resp;
+ });
+ },
+});
diff --git a/ui/app/adapters/ssh-otp-credential.js b/ui/app/adapters/ssh-otp-credential.js
new file mode 100644
index 000000000..99b13e38a
--- /dev/null
+++ b/ui/app/adapters/ssh-otp-credential.js
@@ -0,0 +1,7 @@
+import SSHAdapter from './ssh';
+
+export default SSHAdapter.extend({
+ url(role) {
+ return `/v1/${role.backend}/creds/${role.name}`;
+ },
+});
diff --git a/ui/app/adapters/ssh-sign.js b/ui/app/adapters/ssh-sign.js
new file mode 100644
index 000000000..a5e8cbdcd
--- /dev/null
+++ b/ui/app/adapters/ssh-sign.js
@@ -0,0 +1,7 @@
+import SSHAdapter from './ssh';
+
+export default SSHAdapter.extend({
+ url(role) {
+ return `/v1/${role.backend}/sign/${role.name}`;
+ },
+});
diff --git a/ui/app/adapters/ssh.js b/ui/app/adapters/ssh.js
new file mode 100644
index 000000000..d275a43e6
--- /dev/null
+++ b/ui/app/adapters/ssh.js
@@ -0,0 +1,24 @@
+import ApplicationAdapter from './application';
+import Ember from 'ember';
+
+export default ApplicationAdapter.extend({
+ namespace: 'v1',
+
+ defaultSerializer: 'ssh',
+
+ url(/*role*/) {
+ Ember.assert('Override the `url` method to extend the SSH adapter', false);
+ },
+
+ createRecord(store, type, snapshot, requestType) {
+ const serializer = store.serializerFor(type.modelName);
+ const data = serializer.serialize(snapshot, requestType);
+ const role = snapshot.attr('role');
+
+ return this.ajax(this.url(role), 'POST', { data }).then(response => {
+ response.id = snapshot.id;
+ response.modelName = type.modelName;
+ store.pushPayload(type.modelName, response);
+ });
+ },
+});
diff --git a/ui/app/adapters/tools.js b/ui/app/adapters/tools.js
new file mode 100644
index 000000000..0432c756d
--- /dev/null
+++ b/ui/app/adapters/tools.js
@@ -0,0 +1,23 @@
+import ApplicationAdapter from './application';
+
+const WRAPPING_ENDPOINTS = ['lookup', 'wrap', 'unwrap', 'rewrap'];
+const TOOLS_ENDPOINTS = ['random', 'hash'];
+
+export default ApplicationAdapter.extend({
+ toolUrlFor(action) {
+ const isWrapping = WRAPPING_ENDPOINTS.includes(action);
+ const isTool = TOOLS_ENDPOINTS.includes(action);
+ const prefix = isWrapping ? 'wrapping' : 'tools';
+ if (!isWrapping && !isTool) {
+ throw new Error(`Calls to a ${action} endpoint are not currently allowed in the tool adapter`);
+ }
+ return `${this.buildURL()}/${prefix}/${action}`;
+ },
+
+ toolAction(action, data, options = {}) {
+ const { wrapTTL } = options;
+ const url = this.toolUrlFor(action);
+ const ajaxOptions = wrapTTL ? { data, wrapTTL } : { data };
+ return this.ajax(url, 'POST', ajaxOptions);
+ },
+});
diff --git a/ui/app/adapters/transit-key.js b/ui/app/adapters/transit-key.js
new file mode 100644
index 000000000..520595300
--- /dev/null
+++ b/ui/app/adapters/transit-key.js
@@ -0,0 +1,114 @@
+import Ember from 'ember';
+import ApplicationAdapter from './application';
+
+export default ApplicationAdapter.extend({
+ namespace: 'v1',
+
+ createOrUpdate(store, type, snapshot, requestType) {
+ const serializer = store.serializerFor(type.modelName);
+ const data = serializer.serialize(snapshot, requestType);
+ const { id } = snapshot;
+ let url = this.urlForSecret(snapshot.record.get('backend'), id);
+
+ if (requestType === 'update') {
+ url = url + '/config';
+ }
+
+ return this.ajax(url, 'POST', { data });
+ },
+
+ createRecord() {
+ return this.createOrUpdate(...arguments);
+ },
+
+ updateRecord() {
+ return this.createOrUpdate(...arguments, 'update');
+ },
+
+ deleteRecord(store, type, snapshot) {
+ const { id } = snapshot;
+ return this.ajax(this.urlForSecret(snapshot.record.get('backend'), id), 'DELETE');
+ },
+
+ pathForType(type) {
+ let path;
+ switch (type) {
+ case 'cluster':
+ path = 'clusters';
+ break;
+ case 'secret-engine':
+ path = 'secrets';
+ break;
+ default:
+ path = Ember.String.pluralize(type);
+ break;
+ }
+ return path;
+ },
+
+ urlForSecret(backend, id) {
+ let url = `${this.buildURL()}/${backend}/keys/`;
+ if (id) {
+ url += id;
+ }
+ return url;
+ },
+
+ urlForAction(action, backend, id, param) {
+ let urlBase = `${this.buildURL()}/${backend}/${action}`;
+ // these aren't key-specific
+ if (action === 'hash' || action === 'random') {
+ return urlBase;
+ }
+ if (action === 'datakey' && param) {
+ // datakey action has `wrapped` or `plaintext` as part of the url
+ return `${urlBase}/${param}/${id}`;
+ }
+ if (action === 'export' && param) {
+ let [type, version] = param;
+ const exportBase = `${urlBase}/${type}-key/${id}`;
+ return version ? `${exportBase}/${version}` : exportBase;
+ }
+ return `${urlBase}/${id}`;
+ },
+
+ optionsForQuery(id) {
+ let data = {};
+ if (!id) {
+ data['list'] = true;
+ }
+ return { data };
+ },
+
+ fetchByQuery(query) {
+ const { id, backend } = query;
+ return this.ajax(this.urlForSecret(backend, id), 'GET', this.optionsForQuery(id)).then(resp => {
+ resp.id = id;
+ return resp;
+ });
+ },
+
+ query(store, type, query) {
+ return this.fetchByQuery(query);
+ },
+
+ queryRecord(store, type, query) {
+ return this.fetchByQuery(query);
+ },
+
+ // rotate, encrypt, decrypt, sign, verify, hmac, rewrap, datakey
+ keyAction(action, { backend, id, payload }, options = {}) {
+ const verb = action === 'export' ? 'GET' : 'POST';
+ const { wrapTTL } = options;
+ if (action === 'rotate') {
+ return this.ajax(this.urlForSecret(backend, id) + '/rotate', verb);
+ }
+ const { param } = payload;
+
+ delete payload.param;
+ return this.ajax(this.urlForAction(action, backend, id, param), verb, {
+ data: payload,
+ wrapTTL,
+ });
+ },
+});
diff --git a/ui/app/app.js b/ui/app/app.js
new file mode 100644
index 000000000..dde117785
--- /dev/null
+++ b/ui/app/app.js
@@ -0,0 +1,16 @@
+import Ember from 'ember';
+import Resolver from './resolver';
+import loadInitializers from 'ember-load-initializers';
+import config from './config/environment';
+
+let App;
+
+App = Ember.Application.extend({
+ modulePrefix: config.modulePrefix,
+ podModulePrefix: config.podModulePrefix,
+ Resolver,
+});
+
+loadInitializers(App, config.modulePrefix);
+
+export default App;
diff --git a/ui/app/components/.gitkeep b/ui/app/components/.gitkeep
new file mode 100644
index 000000000..e69de29bb
diff --git a/ui/app/components/auth-config-form/config.js b/ui/app/components/auth-config-form/config.js
new file mode 100644
index 000000000..8dee9b840
--- /dev/null
+++ b/ui/app/components/auth-config-form/config.js
@@ -0,0 +1,32 @@
+import Ember from 'ember';
+import { task } from 'ember-concurrency';
+import DS from 'ember-data';
+
+const { inject } = Ember;
+
+const AuthConfigBase = Ember.Component.extend({
+ tagName: '',
+ model: null,
+
+ flashMessages: inject.service(),
+
+ saveModel: task(function*() {
+ try {
+ yield this.get('model').save();
+ } catch (err) {
+ // AdapterErrors are handled by the error-message component
+ // in the form
+ if (err instanceof DS.AdapterError === false) {
+ throw err;
+ }
+ return;
+ }
+ this.get('flashMessages').success('The configuration was saved successfully.');
+ }),
+});
+
+AuthConfigBase.reopenClass({
+ positionalParams: ['model'],
+});
+
+export default AuthConfigBase;
diff --git a/ui/app/components/auth-config-form/options.js b/ui/app/components/auth-config-form/options.js
new file mode 100644
index 000000000..73a2dd063
--- /dev/null
+++ b/ui/app/components/auth-config-form/options.js
@@ -0,0 +1,22 @@
+import AuthConfigComponent from './config';
+import { task } from 'ember-concurrency';
+import DS from 'ember-data';
+
+export default AuthConfigComponent.extend({
+ saveModel: task(function*() {
+ const model = this.get('model');
+ let data = model.get('config').toJSON();
+ data.description = model.get('description');
+ try {
+ yield model.tune(data);
+ } catch (err) {
+ // AdapterErrors are handled by the error-message component
+ // in the form
+ if (err instanceof DS.AdapterError === false) {
+ throw err;
+ }
+ return;
+ }
+ this.get('flashMessages').success('The configuration options were saved successfully.');
+ }),
+});
diff --git a/ui/app/components/auth-form.js b/ui/app/components/auth-form.js
new file mode 100644
index 000000000..a5a95846f
--- /dev/null
+++ b/ui/app/components/auth-form.js
@@ -0,0 +1,63 @@
+import Ember from 'ember';
+import { supportedAuthBackends } from 'vault/helpers/supported-auth-backends';
+const BACKENDS = supportedAuthBackends();
+
+export default Ember.Component.extend({
+ classNames: ['auth-form'],
+ routing: Ember.inject.service('-routing'),
+ auth: Ember.inject.service(),
+ flashMessages: Ember.inject.service(),
+ didRender() {
+ // on very narrow viewports the active tab may be overflowed, so we scroll it into view here
+ this.$('li.is-active').get(0).scrollIntoView();
+ },
+
+ cluster: null,
+ redirectTo: null,
+
+ selectedAuthType: 'token',
+ selectedAuthBackend: Ember.computed('selectedAuthType', function() {
+ return BACKENDS.findBy('type', this.get('selectedAuthType'));
+ }),
+
+ providerComponentName: Ember.computed('selectedAuthBackend.type', function() {
+ const type = Ember.String.dasherize(this.get('selectedAuthBackend.type'));
+ return `auth-form/${type}`;
+ }),
+
+ handleError(e) {
+ this.set('loading', false);
+ this.set('error', `Authentication failed: ${e.errors.join('.')}`);
+ },
+
+ actions: {
+ doSubmit(data) {
+ this.setProperties({
+ loading: true,
+ error: null,
+ });
+ const targetRoute = this.get('redirectTo') || 'vault.cluster';
+ //const {password, token, username} = data;
+ const backend = this.get('selectedAuthBackend.type');
+ const path = this.get('customPath');
+ if (this.get('useCustomPath') && path) {
+ data.path = path;
+ }
+ const clusterId = this.get('cluster.id');
+ this.get('auth').authenticate({ clusterId, backend, data }).then(
+ ({ isRoot }) => {
+ this.set('loading', false);
+ const transition = this.get('routing.router').transitionTo(targetRoute);
+ if (isRoot) {
+ transition.followRedirects().then(() => {
+ this.get('flashMessages').warning(
+ 'You have logged in with a root token. As a security precaution, this root token will not be stored by your browser and you will need to re-authenticate after the window is closed or refreshed.'
+ );
+ });
+ }
+ },
+ (...errArgs) => this.handleError(...errArgs)
+ );
+ },
+ },
+});
diff --git a/ui/app/components/auth-info.js b/ui/app/components/auth-info.js
new file mode 100644
index 000000000..ba44e5c71
--- /dev/null
+++ b/ui/app/components/auth-info.js
@@ -0,0 +1,32 @@
+import Ember from 'ember';
+
+export default Ember.Component.extend({
+ auth: Ember.inject.service(),
+
+ routing: Ember.inject.service('-routing'),
+
+ transitionToRoute: function() {
+ var router = this.get('routing.router');
+ router.transitionTo.apply(router, arguments);
+ },
+
+ classNames: 'user-menu auth-info',
+
+ isRenewing: Ember.computed.or('fakeRenew', 'auth.isRenewing'),
+
+ actions: {
+ renewToken() {
+ this.set('fakeRenew', true);
+ Ember.run.later(() => {
+ this.set('fakeRenew', false);
+ this.get('auth').renew();
+ }, 200);
+ },
+
+ revokeToken() {
+ this.get('auth').revokeCurrentToken().then(() => {
+ this.transitionToRoute('vault.cluster.logout');
+ });
+ },
+ },
+});
diff --git a/ui/app/components/b64-toggle.js b/ui/app/components/b64-toggle.js
new file mode 100644
index 000000000..a84995e84
--- /dev/null
+++ b/ui/app/components/b64-toggle.js
@@ -0,0 +1,128 @@
+import Ember from 'ember';
+import { encodeString, decodeString } from 'vault/utils/b64';
+
+const { computed, get, set } = Ember;
+const B64 = 'base64';
+const UTF8 = 'utf-8';
+export default Ember.Component.extend({
+ tagName: 'button',
+ attributeBindings: ['type'],
+ type: 'button',
+ classNames: ['button', 'b64-toggle'],
+ classNameBindings: ['isInput:is-input:is-textarea'],
+
+ /*
+ * Whether or not the toggle is associated with an input.
+ * Also bound to `is-input` and `is-textarea` classes
+ * Defaults to true
+ *
+ * @public
+ * @type boolean
+ */
+
+ isInput: true,
+
+ /*
+ * The value that will be mutated when the encoding is toggled
+ *
+ * @public
+ * @type string
+ */
+ value: null,
+
+ /*
+ * The encoding of `value` when the component is initialized.
+ * Defaults to 'utf-8'.
+ * Possible values: 'utf-8' and 'base64'
+ *
+ * @public
+ * @type string
+ */
+ initialEncoding: UTF8,
+
+ /*
+ * A cached version of value - used to determine if the input has changed since encoding.
+ *
+ * @private
+ * @type string
+ */
+ _value: '',
+
+ /*
+ * The current encoding of `value`.
+ * Possible values: 'utf-8' and 'base64'
+ *
+ * @private
+ * @type string
+ */
+ currentEncoding: '',
+
+ /*
+ * The encoding when we last mutated `value`.
+ * Possible values: 'utf-8' and 'base64'
+ *
+ * @private
+ * @type string
+ */
+ lastEncoding: '',
+
+ /*
+ * Is the value known to be base64-encoded.
+ *
+ * @private
+ * @type boolean
+ */
+ isBase64: computed.equal('currentEncoding', B64),
+
+ /*
+ * Does the current value match the cached _value, i.e. has the input been mutated since we encoded.
+ *
+ * @private
+ * @type boolean
+ */
+ valuesMatch: computed('value', '_value', function() {
+ const { value, _value } = this.getProperties('value', '_value');
+ const anyBlank = Ember.isBlank(value) || Ember.isBlank(_value);
+ return !anyBlank && value === _value;
+ }),
+
+ init() {
+ this._super(...arguments);
+ const initial = get(this, 'initialEncoding');
+ set(this, 'currentEncoding', initial);
+ if (initial === B64) {
+ set(this, '_value', get(this, 'value'));
+ set(this, 'lastEncoding', B64);
+ }
+ },
+
+ didReceiveAttrs() {
+ // if there's no value, reset encoding
+ if (get(this, 'value') === '') {
+ set(this, 'currentEncoding', UTF8);
+ return;
+ }
+ // the value has changed after we transformed it so we reset currentEncoding
+ if (get(this, 'isBase64') && !get(this, 'valuesMatch')) {
+ set(this, 'currentEncoding', UTF8);
+ }
+ // the value changed back to one we previously had transformed
+ if (get(this, 'lastEncoding') === B64 && get(this, 'valuesMatch')) {
+ set(this, 'currentEncoding', B64);
+ }
+ },
+
+ click() {
+ let val = get(this, 'value');
+ const isUTF8 = get(this, 'currentEncoding') === UTF8;
+ if (!val) {
+ return;
+ }
+ let newVal = isUTF8 ? encodeString(val) : decodeString(val);
+ const encoding = isUTF8 ? B64 : UTF8;
+ set(this, 'value', newVal);
+ set(this, '_value', newVal);
+ set(this, 'lastEncoding', encoding);
+ set(this, 'currentEncoding', encoding);
+ },
+});
diff --git a/ui/app/components/config-pki-ca.js b/ui/app/components/config-pki-ca.js
new file mode 100644
index 000000000..6eed7d2da
--- /dev/null
+++ b/ui/app/components/config-pki-ca.js
@@ -0,0 +1,203 @@
+import Ember from 'ember';
+
+const { computed, inject } = Ember;
+
+export default Ember.Component.extend({
+ classNames: 'config-pki-ca',
+ store: inject.service('store'),
+ flashMessages: inject.service(),
+
+ /*
+ * @param boolean
+ * @private
+ * bool that gets flipped if you have a CA cert and click the Replace Cert button
+ */
+ replaceCA: false,
+
+ /*
+ * @param boolean
+ * @private
+ * bool that gets flipped if you push the click the "Set signed intermediate" button
+ */
+ setSignedIntermediate: false,
+
+ /*
+ * @param boolean
+ * @private
+ * bool that gets flipped if you push the click the "Set signed intermediate" button
+ */
+ signIntermediate: false,
+
+ /*
+ * @param boolean
+ * @private
+ *
+ * true when there's no CA cert currently configured
+ */
+ needsConfig: computed.not('config.pem'),
+
+ /*
+ * @param DS.Model
+ * @private
+ *
+ * a `pki-ca-certificate` model used to back the form when uploading or creating a CA cert
+ * created and set on `init`, and unloaded on willDestroy
+ *
+ */
+ model: null,
+
+ /*
+ * @param DS.Model
+ * @public
+ *
+ * a `pki-config` model - passed in in the component useage
+ *
+ */
+ config: null,
+
+ /*
+ * @param Function
+ * @public
+ *
+ * function that gets called to refresh the config model
+ *
+ */
+ onRefresh: () => {},
+
+ loading: false,
+
+ willDestroy() {
+ const ca = this.get('model');
+ if (ca) {
+ ca.unloadRecord();
+ }
+ this._super(...arguments);
+ },
+
+ createOrReplaceModel(modelType) {
+ const ca = this.get('model');
+ const config = this.get('config');
+ const store = this.get('store');
+ const backend = config.get('backend');
+ if (ca) {
+ ca.unloadRecord();
+ }
+ const caCert = store.createRecord(modelType || 'pki-ca-certificate', {
+ id: `${backend}-ca-cert`,
+ backend,
+ });
+ this.set('model', caCert);
+ },
+
+ /*
+ * @private
+ * @returns array
+ *
+ * When a CA is configured, we let them download
+ * the CA in der, pem, and the CA Chain in pem (if one exists)
+ *
+ * This array provides the text and download hrefs for those links.
+ *
+ */
+ downloadHrefs: computed('config', 'config.{backend,pem,caChain,der}', function() {
+ const config = this.get('config');
+ const { backend, pem, caChain, der } = config.getProperties('backend', 'pem', 'caChain', 'der');
+
+ if (!pem) {
+ return [];
+ }
+ const pemFile = new File([pem], { type: 'text/plain' });
+ const links = [
+ {
+ display: 'Download CA Certificate in PEM format',
+ name: `${backend}_ca.pem`,
+ url: URL.createObjectURL(pemFile),
+ },
+ {
+ display: 'Download CA Certificate in DER format',
+ name: `${backend}_ca.der`,
+ url: URL.createObjectURL(der),
+ },
+ ];
+ if (caChain) {
+ const caChainFile = new File([caChain], { type: 'text/plain' });
+ links.push({
+ display: 'Download CA Certificate Chain',
+ name: `${backend}_ca_chain.pem`,
+ url: URL.createObjectURL(caChainFile),
+ });
+ }
+ return links;
+ }),
+
+ actions: {
+ saveCA(method) {
+ this.set('loading', true);
+ const model = this.get('model');
+ const isUpload = this.get('model.uploadPemBundle');
+ model
+ .save({ adapterOptions: { method } })
+ .then(m => {
+ if (method === 'setSignedIntermediate' || isUpload) {
+ this.send('refresh');
+ this.get('flashMessages').success('The certificate for this backend has been updated.');
+ } else if (!m.get('certificate') && !m.get('csr')) {
+ // if there's no certificate, it wasn't generated and the generation was a noop
+ this.get('flashMessages').warning(
+ 'You tried to generate a new root CA, but one currently exists. To replace the existing one, delete it first and then generate again.'
+ );
+ }
+ })
+ .finally(() => {
+ this.set('loading', false);
+ });
+ },
+ deleteCA() {
+ this.set('loading', true);
+ const model = this.get('model');
+ const backend = model.get('backend');
+ //TODO Is there better way to do this? This forces the saved state so Ember Data will make a server call.
+ model.send('pushedData');
+ model
+ .destroyRecord()
+ .then(() => {
+ this.get('flashMessages').success(
+ `The CA key for ${backend} has been deleted. The old CA certificate will still be accessible for reading until a new certificate/key are generated or uploaded.`
+ );
+ })
+ .finally(() => {
+ this.set('loading', false);
+ this.send('refresh');
+ this.createOrReplaceModel();
+ });
+ },
+ refresh() {
+ this.setProperties({
+ setSignedIntermediate: false,
+ signIntermediate: false,
+ replaceCA: false,
+ });
+ this.get('onRefresh')();
+ },
+ toggleReplaceCA() {
+ if (!this.get('replaceCA')) {
+ this.createOrReplaceModel();
+ }
+ this.toggleProperty('replaceCA');
+ },
+ toggleVal(name, val) {
+ if (!name) {
+ return;
+ }
+ const model = name === 'signIntermediate' ? 'pki-ca-certificate-sign' : null;
+ if (!this.get(name)) {
+ this.createOrReplaceModel(model);
+ }
+ if (val !== undefined) {
+ this.set(name, val);
+ } else {
+ this.toggleProperty(name);
+ }
+ },
+ },
+});
diff --git a/ui/app/components/config-pki.js b/ui/app/components/config-pki.js
new file mode 100644
index 000000000..dd3a2165b
--- /dev/null
+++ b/ui/app/components/config-pki.js
@@ -0,0 +1,66 @@
+import Ember from 'ember';
+
+const { get, inject } = Ember;
+
+export default Ember.Component.extend({
+ classNames: 'config-pki',
+ flashMessages: inject.service(),
+
+ /*
+ *
+ * @param String
+ * @public
+ * String corresponding to the route parameter for the current section
+ *
+ */
+
+ section: null,
+
+ /*
+ * @param DS.Model
+ * @public
+ *
+ * a `pki-config` model - passed in in the component useage
+ *
+ */
+ config: null,
+
+ /*
+ * @param Function
+ * @public
+ *
+ * function that gets called to refresh the config model
+ *
+ */
+ onRefresh: () => {},
+
+ loading: false,
+
+ actions: {
+ save(section) {
+ this.set('loading', true);
+ const config = this.get('config');
+ config
+ .save({
+ adapterOptions: {
+ method: section,
+ fields: get(config, `${section}Attrs`).map(attr => attr.name),
+ },
+ })
+ .then(() => {
+ this.get('flashMessages').success(`The ${section} config for this backend has been updated.`);
+ // attrs aren't persistent for Tidy
+ if (section === 'tidy') {
+ config.rollbackAttributes();
+ }
+ this.send('refresh');
+ })
+ .finally(() => {
+ this.set('loading', false);
+ });
+ },
+ refresh() {
+ this.get('onRefresh')();
+ },
+ },
+});
diff --git a/ui/app/components/confirm-action.js b/ui/app/components/confirm-action.js
new file mode 100644
index 000000000..ca6fdf519
--- /dev/null
+++ b/ui/app/components/confirm-action.js
@@ -0,0 +1,57 @@
+import Ember from 'ember';
+import hbs from 'htmlbars-inline-precompile';
+
+export default Ember.Component.extend({
+ tagName: 'span',
+ classNames: ['confirm-action'],
+ layout: hbs`
+ {{#if showConfirm ~}}
+
+ {{if disabled disabledMessage confirmMessage}}
+ {{confirmButtonText}}
+ {{cancelButtonText}}
+
+ {{else}}
+
+ {{yield}}
+
+ {{~/if}}
+ `,
+
+ disabled: false,
+ disabledMessage: 'Complete the form to complete this action',
+ showConfirm: false,
+ messageClasses: 'is-size-8 has-text-grey',
+ confirmButtonClasses: 'is-danger is-outlined button',
+ containerClasses: '',
+ buttonClasses: 'button',
+ buttonText: 'Delete',
+ confirmMessage: 'Are you sure you want to do this?',
+ confirmButtonText: 'Delete',
+ cancelButtonClasses: 'button',
+ cancelButtonText: 'Cancel',
+ // the action to take when we confirm
+ onConfirmAction: null,
+
+ actions: {
+ toggleConfirm() {
+ this.toggleProperty('showConfirm');
+ },
+
+ onConfirm() {
+ const confirmAction = this.get('onConfirmAction');
+
+ if (typeof confirmAction !== 'function') {
+ throw new Error('confirm-action components expects `onConfirmAction` attr to be a function');
+ } else {
+ confirmAction();
+ this.toggleProperty('showConfirm');
+ }
+ },
+ },
+});
diff --git a/ui/app/components/doc-link.js b/ui/app/components/doc-link.js
new file mode 100644
index 000000000..892cb2ffe
--- /dev/null
+++ b/ui/app/components/doc-link.js
@@ -0,0 +1,19 @@
+import Ember from 'ember';
+import hbs from 'htmlbars-inline-precompile';
+
+const { Component, computed } = Ember;
+
+export default Component.extend({
+ tagName: 'a',
+ attributeBindings: ['target', 'rel', 'href'],
+
+ layout: hbs`{{yield}}`,
+
+ target: '_blank',
+ rel: 'noreferrer noopener',
+
+ path: '/',
+ href: computed('path', function() {
+ return `https://www.vaultproject.io/docs${this.get('path')}`;
+ }),
+});
diff --git a/ui/app/components/download-button.js b/ui/app/components/download-button.js
new file mode 100644
index 000000000..e4761a76c
--- /dev/null
+++ b/ui/app/components/download-button.js
@@ -0,0 +1,32 @@
+import Ember from 'ember';
+import hbs from 'htmlbars-inline-precompile';
+
+const { computed } = Ember;
+
+export default Ember.Component.extend({
+ layout: hbs`{{actionText}}`,
+ tagName: 'a',
+ role: 'button',
+ attributeBindings: ['role', 'download', 'href'],
+ download: computed('filename', 'extension', function() {
+ return `${this.get('filename')}-${new Date().toISOString()}.${this.get('extension')}`;
+ }),
+
+ href: computed('data', 'mime', 'stringify', function() {
+ let data = this.get('data');
+ const mime = this.get('mime');
+ if (this.get('stringify')) {
+ data = JSON.stringify(data, null, 2);
+ }
+
+ const file = new File([data], { type: mime });
+ return window.URL.createObjectURL(file);
+ }),
+
+ actionText: 'Download',
+ data: null,
+ filename: null,
+ mime: 'text/plain',
+ extension: 'txt',
+ stringify: false,
+});
diff --git a/ui/app/components/edition-badge.js b/ui/app/components/edition-badge.js
new file mode 100644
index 000000000..956e37257
--- /dev/null
+++ b/ui/app/components/edition-badge.js
@@ -0,0 +1,15 @@
+import Ember from 'ember';
+
+export default Ember.Component.extend({
+ tagName: 'span',
+ classNames: 'badge edition-badge',
+ abbreviation: Ember.computed('edition', function() {
+ const edition = this.get('edition');
+ if (edition == 'Enterprise') {
+ return 'Ent';
+ } else {
+ return edition;
+ }
+ }),
+ attributeBindings: ['edition:aria-label'],
+});
diff --git a/ui/app/components/flash-message.js b/ui/app/components/flash-message.js
new file mode 100644
index 000000000..02753442d
--- /dev/null
+++ b/ui/app/components/flash-message.js
@@ -0,0 +1,17 @@
+import Ember from 'ember';
+import FlashMessage from 'ember-cli-flash/components/flash-message';
+
+const { computed, getWithDefault } = Ember;
+
+export default FlashMessage.extend({
+ // override alertType to get Bulma specific prefix
+ //https://github.com/poteto/ember-cli-flash/blob/master/addon/components/flash-message.js#L35
+ alertType: computed('flash.type', {
+ get() {
+ const flashType = getWithDefault(this, 'flash.type', '');
+ let prefix = 'notification has-border is-';
+
+ return `${prefix}${flashType}`;
+ },
+ }),
+});
diff --git a/ui/app/components/flex-table-column.js b/ui/app/components/flex-table-column.js
new file mode 100644
index 000000000..8c57d221e
--- /dev/null
+++ b/ui/app/components/flex-table-column.js
@@ -0,0 +1,7 @@
+import Ember from 'ember';
+
+export default Ember.Component.extend({
+ classNames: 'column',
+ header: null,
+ content: null,
+});
diff --git a/ui/app/components/form-field-groups.js b/ui/app/components/form-field-groups.js
new file mode 100644
index 000000000..86c32730d
--- /dev/null
+++ b/ui/app/components/form-field-groups.js
@@ -0,0 +1,31 @@
+import Ember from 'ember';
+
+const { computed } = Ember;
+
+export default Ember.Component.extend({
+ tagName: '',
+
+ /*
+ * @public String
+ * A whitelist of groups to include in the render
+ */
+ renderGroup: computed(function() {
+ return null;
+ }),
+
+ /*
+ * @public DS.Model
+ * model to be passed down to form-field component
+ * if `fieldGroups` is present on the model then it will be iterated over and
+ * groups of `form-field` components will be rendered
+ *
+ */
+ model: null,
+
+ /*
+ * @public Function
+ * onChange handler that will get set on the form-field component
+ *
+ */
+ onChange: () => {},
+});
diff --git a/ui/app/components/form-field.js b/ui/app/components/form-field.js
new file mode 100644
index 000000000..db91f3a0a
--- /dev/null
+++ b/ui/app/components/form-field.js
@@ -0,0 +1,110 @@
+import Ember from 'ember';
+import { capitalize } from 'vault/helpers/capitalize';
+import { humanize } from 'vault/helpers/humanize';
+import { dasherize } from 'vault/helpers/dasherize';
+const { computed } = Ember;
+
+export default Ember.Component.extend({
+ 'data-test-field': true,
+ classNames: ['field'],
+
+ /*
+ * @public Function
+ * called whenever a value on the model changes via the component
+ *
+ */
+ onChange() {},
+
+ /*
+ * @public
+ * @param Object
+ * in the form of
+ * {
+ * name: "foo",
+ * options: {
+ * label: "Foo",
+ * defaultValue: "",
+ * editType: "ttl",
+ * helpText: "This will be in a tooltip"
+ * },
+ * type: "boolean"
+ * }
+ *
+ * this is usually derived from ember model `attributes` lookup,
+ * and all members of `attr.options` are optional
+ *
+ */
+ attr: null,
+
+ /*
+ * @private
+ * @param string
+ * Computed property used in the label element next to the form element
+ *
+ */
+ labelString: computed('attr.name', 'attr.options.label', function() {
+ const label = this.get('attr.options.label');
+ const name = this.get('attr.name');
+ if (label) {
+ return label;
+ }
+ if (name) {
+ return capitalize([humanize([dasherize([name])])]);
+ }
+ }),
+
+ // both the path to mutate on the model, and the path to read the value from
+ /*
+ * @private
+ * @param string
+ *
+ * Computed property used to set values on the passed model
+ *
+ */
+ valuePath: computed('attr.name', 'attr.options.fieldValue', function() {
+ return this.get('attr.options.fieldValue') || this.get('attr.name');
+ }),
+
+ /*
+ *
+ * @public
+ * @param DS.Model
+ *
+ * the Ember Data model that `attr` is defined on
+ */
+ model: null,
+
+ /*
+ * @private
+ * @param object
+ *
+ * Used by the pgp-file component when an attr is editType of 'file'
+ */
+ file: { value: '' },
+ emptyData: '{\n}',
+
+ actions: {
+ setFile(_, keyFile) {
+ const path = this.get('valuePath');
+ const { value } = keyFile;
+ this.get('model').set(path, value);
+ this.get('onChange')(path, value);
+ this.set('file', keyFile);
+ },
+
+ setAndBroadcast(path, value) {
+ this.get('model').set(path, value);
+ this.get('onChange')(path, value);
+ },
+
+ codemirrorUpdated(path, value, codemirror) {
+ codemirror.performLint();
+ const hasErrors = codemirror.state.lint.marked.length > 0;
+
+ if (!hasErrors) {
+ this.get('model').set(path, JSON.parse(value));
+ this.get('onChange')(path, JSON.parse(value));
+ }
+ },
+ },
+});
diff --git a/ui/app/components/generate-credentials.js b/ui/app/components/generate-credentials.js
new file mode 100644
index 000000000..bab049ab7
--- /dev/null
+++ b/ui/app/components/generate-credentials.js
@@ -0,0 +1,133 @@
+import Ember from 'ember';
+
+const { get, computed } = Ember;
+
+const MODEL_TYPES = {
+ 'ssh-sign': {
+ model: 'ssh-sign',
+ },
+ 'ssh-creds': {
+ model: 'ssh-otp-credential',
+ title: 'Generate SSH Credentials',
+ generatedAttr: 'key',
+ },
+ 'aws-creds': {
+ model: 'iam-credential',
+ title: 'Generate IAM Credentials',
+ generateWithoutInput: true,
+ backIsListLink: true,
+ },
+ 'aws-sts': {
+ model: 'iam-credential',
+ title: 'Generate IAM Credentials with STS',
+ generatedAttr: 'accessKey',
+ },
+ 'pki-issue': {
+ model: 'pki-certificate',
+ title: 'Issue Certificate',
+ generatedAttr: 'certificate',
+ },
+ 'pki-sign': {
+ model: 'pki-certificate-sign',
+ title: 'Sign Certificate',
+ generatedAttr: 'certificate',
+ },
+};
+
+export default Ember.Component.extend({
+ store: Ember.inject.service(),
+ routing: Ember.inject.service('-routing'),
+ // set on the component
+ backend: null,
+ action: null,
+ role: null,
+
+ model: null,
+ loading: false,
+ emptyData: '{\n}',
+
+ modelForType() {
+ const type = this.get('options');
+ if (type) {
+ return type.model;
+ }
+ // if we don't have a mode for that type then redirect them back to the backend list
+ const router = this.get('routing.router');
+ router.transitionTo.call(router, 'vault.cluster.secrets.backend.list-root', this.get('model.backend'));
+ },
+
+ options: computed('action', 'backend.type', function() {
+ const action = this.get('action') || 'creds';
+ return MODEL_TYPES[`${this.get('backend.type')}-${action}`];
+ }),
+
+ init() {
+ this._super(...arguments);
+ this.createOrReplaceModel();
+ this.maybeGenerate();
+ },
+
+ willDestroy() {
+ this.get('model').unloadRecord();
+ this._super(...arguments);
+ },
+
+ createOrReplaceModel() {
+ const modelType = this.modelForType();
+ const model = this.get('model');
+ const roleModel = this.get('role');
+ if (!modelType) {
+ return;
+ }
+ if (model) {
+ model.unloadRecord();
+ }
+ const attrs = {
+ role: roleModel,
+ id: `${get(roleModel, 'backend')}-${get(roleModel, 'name')}`,
+ };
+ if (this.get('action') === 'sts') {
+ attrs.withSTS = true;
+ }
+ const newModel = this.get('store').createRecord(modelType, attrs);
+ this.set('model', newModel);
+ },
+
+ /*
+ *
+ * @function maybeGenerate
+ *
+ * This method is called on `init`. If there is no input requried (as is the case for AWS IAM creds)
+ * then the `create` action is triggered right away.
+ *
+ */
+ maybeGenerate() {
+ if (this.get('backend.type') !== 'aws' || this.get('action') === 'sts') {
+ return;
+ }
+ // for normal IAM creds - there's no input, so just generate right away
+ this.send('create');
+ },
+
+ actions: {
+ create() {
+ this.set('loading', true);
+ this.model.save().finally(() => {
+ this.set('loading', false);
+ });
+ },
+
+ codemirrorUpdated(attr, val, codemirror) {
+ codemirror.performLint();
+ const hasErrors = codemirror.state.lint.marked.length > 0;
+
+ if (!hasErrors) {
+ Ember.set(this.get('model'), attr, JSON.parse(val));
+ }
+ },
+
+ newModel() {
+ this.createOrReplaceModel();
+ },
+ },
+});
diff --git a/ui/app/components/home-link.js b/ui/app/components/home-link.js
new file mode 100644
index 000000000..7eab8f799
--- /dev/null
+++ b/ui/app/components/home-link.js
@@ -0,0 +1,25 @@
+import Ember from 'ember';
+import hbs from 'htmlbars-inline-precompile';
+
+const { computed } = Ember;
+
+export default Ember.Component.extend({
+ layout: hbs`
+ {{#if hasBlock}}
+ {{yield}}
+ {{else}}
+ {{text}}
+ {{/if}}
+
+ `,
+
+ tagName: '',
+
+ text: computed(function() {
+ return 'home';
+ }),
+
+ computedClasses: computed('classNames', function() {
+ return this.get('classNames').join(' ');
+ }),
+});
diff --git a/ui/app/components/i-con.js b/ui/app/components/i-con.js
new file mode 100644
index 000000000..aedcb1895
--- /dev/null
+++ b/ui/app/components/i-con.js
@@ -0,0 +1,47 @@
+import Ember from 'ember';
+import hbs from 'htmlbars-inline-precompile';
+
+const { computed } = Ember;
+const GLYPHS_WITH_SVG_TAG = [
+ 'folder',
+ 'file',
+ 'perf-replication',
+ 'role',
+ 'information-reversed',
+ 'true',
+ 'false',
+];
+
+export default Ember.Component.extend({
+ layout: hbs`
+ {{#if excludeSVG}}
+ {{partial partialName}}
+ {{else}}
+
+ {{partial partialName}}
+
+ {{/if}}
+ `,
+
+ tagName: 'span',
+ excludeIconClass: false,
+ classNameBindings: ['excludeIconClass::icon'],
+ classNames: ['has-current-color-fill'],
+
+ attributeBindings: ['aria-label', 'aria-hidden'],
+
+ glyph: null,
+
+ excludeSVG: computed('glyph', function() {
+ return GLYPHS_WITH_SVG_TAG.includes(this.get('glyph'));
+ }),
+
+ size: computed(function() {
+ return 12;
+ }),
+
+ partialName: computed('glyph', function() {
+ const glyph = this.get('glyph');
+ return `svg/icons/${Ember.String.camelize(glyph)}`;
+ }),
+});
diff --git a/ui/app/components/identity/edit-form.js b/ui/app/components/identity/edit-form.js
new file mode 100644
index 000000000..64d222848
--- /dev/null
+++ b/ui/app/components/identity/edit-form.js
@@ -0,0 +1,68 @@
+import Ember from 'ember';
+import { task } from 'ember-concurrency';
+import { humanize } from 'vault/helpers/humanize';
+
+const { computed } = Ember;
+export default Ember.Component.extend({
+ model: null,
+ mode: 'create',
+ /*
+ * @param Function
+ * @public
+ *
+ * Optional param to call a function upon successfully mounting a backend
+ *
+ */
+ onSave: () => {},
+
+ cancelLink: computed('mode', 'model', function() {
+ let { model, mode } = this.getProperties('model', 'mode');
+ let key = `${mode}-${model.get('identityType')}`;
+ let routes = {
+ 'create-entity': 'vault.cluster.access.identity',
+ 'edit-entity': 'vault.cluster.access.identity.show',
+ 'merge-entity-merge': 'vault.cluster.access.identity',
+ 'create-entity-alias': 'vault.cluster.access.identity.aliases',
+ 'edit-entity-alias': 'vault.cluster.access.identity.aliases.show',
+ 'create-group': 'vault.cluster.access.identity',
+ 'edit-group': 'vault.cluster.access.identity.show',
+ 'create-group-alias': 'vault.cluster.access.identity.aliases',
+ 'edit-group-alias': 'vault.cluster.access.identity.aliases.show',
+ };
+
+ return routes[key];
+ }),
+
+ getMessage(model) {
+ let mode = this.get('mode');
+ let typeDisplay = humanize([model.get('identityType')]);
+ if (mode === 'merge') {
+ return 'Successfully merged entities';
+ }
+ if (model.get('id')) {
+ return `Successfully saved ${typeDisplay} ${model.id}.`;
+ }
+ return `Successfully saved ${typeDisplay}.`;
+ },
+
+ save: task(function*() {
+ let model = this.get('model');
+ let message = this.getMessage(model);
+
+ try {
+ yield model.save();
+ } catch (err) {
+ // err will display via model state
+ return;
+ }
+ this.get('flashMessages').success(message);
+ yield this.get('onSave')(model);
+ }).drop(),
+
+ willDestroy() {
+ let model = this.get('model');
+ if (!model.isDestroyed || !model.isDestroying) {
+ model.rollbackAttributes();
+ }
+ },
+});
diff --git a/ui/app/components/identity/lookup-input.js b/ui/app/components/identity/lookup-input.js
new file mode 100644
index 000000000..9dbc9cb16
--- /dev/null
+++ b/ui/app/components/identity/lookup-input.js
@@ -0,0 +1,75 @@
+import Ember from 'ember';
+import { task } from 'ember-concurrency';
+import { underscore } from 'vault/helpers/underscore';
+
+const { inject } = Ember;
+
+export default Ember.Component.extend({
+ store: inject.service(),
+ flashMessages: inject.service(),
+ routing: inject.service('-routing'),
+
+ // Public API - either 'entity' or 'group'
+ // this will determine which adapter is used to make the lookup call
+ type: 'entity',
+
+ param: 'alias name',
+ paramValue: null,
+ aliasMountAccessor: null,
+
+ authMethods: null,
+
+ init() {
+ this._super(...arguments);
+ this.get('store').findAll('auth-method').then(methods => {
+ this.set('authMethods', methods);
+ this.set('aliasMountAccessor', methods.get('firstObject.accessor'));
+ });
+ },
+
+ adapter() {
+ let type = this.get('type');
+ let store = this.get('store');
+ return store.adapterFor(`identity/${type}`);
+ },
+
+ data() {
+ let { param, paramValue, aliasMountAccessor } = this.getProperties(
+ 'param',
+ 'paramValue',
+ 'aliasMountAccessor'
+ );
+ let data = {};
+
+ data[underscore([param])] = paramValue;
+ if (param === 'alias name') {
+ data.alias_mount_accessor = aliasMountAccessor;
+ }
+ return data;
+ },
+
+ lookup: task(function*() {
+ let flash = this.get('flashMessages');
+ let type = this.get('type');
+ let store = this.get('store');
+ let { param, paramValue } = this.getProperties('param', 'paramValue');
+ let response;
+ try {
+ response = yield this.adapter().lookup(store, this.data());
+ } catch (err) {
+ flash.danger(
+ `We encountered an error attempting the ${type} lookup: ${err.message || err.errors.join('')}.`
+ );
+ return;
+ }
+ if (response) {
+ return this.get('routing.router').transitionTo(
+ 'vault.cluster.access.identity.show',
+ response.id,
+ 'details'
+ );
+ } else {
+ flash.danger(`We were unable to find an identity ${type} with a "${param}" of "${paramValue}".`);
+ }
+ }),
+});
diff --git a/ui/app/components/info-table-row.js b/ui/app/components/info-table-row.js
new file mode 100644
index 000000000..914573614
--- /dev/null
+++ b/ui/app/components/info-table-row.js
@@ -0,0 +1,30 @@
+import Ember from 'ember';
+
+export default Ember.Component.extend({
+ classNames: ['info-table-row'],
+ isVisible: Ember.computed.or('alwaysRender', 'value'),
+
+ /*
+ * @param boolean
+ * indicates if the component content should be always be rendered.
+ * when false, the value of `value` will be used to determine if the component should render
+ */
+ alwaysRender: false,
+
+ /*
+ * @param string
+ * the display name for the value
+ *
+ */
+ label: null,
+
+ /*
+ *
+ * the value of the data passed in - by default the content of the component will only show if there is a value
+ */
+ value: null,
+
+ valueIsBoolean: Ember.computed('value', function() {
+ return Ember.typeOf(this.get('value')) === 'boolean';
+ }),
+});
diff --git a/ui/app/components/info-tooltip.js b/ui/app/components/info-tooltip.js
new file mode 100644
index 000000000..f7e144d45
--- /dev/null
+++ b/ui/app/components/info-tooltip.js
@@ -0,0 +1,7 @@
+import Ember from 'ember';
+
+export default Ember.Component.extend({
+ 'data-test-component': 'info-tooltip',
+ tagName: 'span',
+ classNames: ['is-inline-block'],
+});
diff --git a/ui/app/components/json-editor.js b/ui/app/components/json-editor.js
new file mode 100644
index 000000000..7b1a1c669
--- /dev/null
+++ b/ui/app/components/json-editor.js
@@ -0,0 +1,28 @@
+import IvyCodemirrorComponent from './ivy-codemirror';
+import Ember from 'ember';
+
+const { assign } = Ember;
+const JSON_EDITOR_DEFAULTS = {
+ // IMPORTANT: `gutters` must come before `lint` since the presence of
+ // `gutters` is cached internally when `lint` is toggled
+ gutters: ['CodeMirror-lint-markers'],
+ tabSize: 2,
+ mode: 'application/json',
+ lineNumbers: true,
+ lint: { lintOnChange: false },
+ theme: 'hashi',
+ readOnly: false,
+};
+
+export default IvyCodemirrorComponent.extend({
+ 'data-test-component': 'json-editor',
+ updateCodeMirrorOptions() {
+ const options = assign({}, JSON_EDITOR_DEFAULTS, this.get('options'));
+
+ if (options) {
+ Object.keys(options).forEach(function(option) {
+ this.updateCodeMirrorOption(option, options[option]);
+ }, this);
+ }
+ },
+});
diff --git a/ui/app/components/key-value-header.js b/ui/app/components/key-value-header.js
new file mode 100644
index 000000000..998c72240
--- /dev/null
+++ b/ui/app/components/key-value-header.js
@@ -0,0 +1,85 @@
+import Ember from 'ember';
+import utils from 'vault/lib/key-utils';
+
+export default Ember.Component.extend({
+ tagName: 'nav',
+ classNames: 'key-value-header breadcrumb',
+ ariaLabel: 'breadcrumbs',
+ attributeBindings: ['ariaLabel:aria-label', 'aria-hidden'],
+
+ baseKey: null,
+ path: null,
+ showCurrent: true,
+ linkToPaths: true,
+
+ stripTrailingSlash(str) {
+ return str[str.length - 1] === '/' ? str.slice(0, -1) : str;
+ },
+
+ currentPath: Ember.computed('mode', 'path', 'showCurrent', function() {
+ const mode = this.get('mode');
+ const path = this.get('path');
+ const showCurrent = this.get('showCurrent');
+ if (!mode || showCurrent === false) {
+ return path;
+ }
+ return `vault.cluster.secrets.backend.${mode}`;
+ }),
+
+ secretPath: Ember.computed('baseKey', 'baseKey.display', 'baseKey.id', 'root', 'showCurrent', function() {
+ let crumbs = [];
+ const root = this.get('root');
+ const baseKey = this.get('baseKey.display') || this.get('baseKey.id');
+ const baseKeyModel = this.get('baseKey.id');
+
+ if (root) {
+ crumbs.push(root);
+ }
+
+ if (!baseKey) {
+ return crumbs;
+ }
+
+ const path = this.get('path');
+ const currentPath = this.get('currentPath');
+ const showCurrent = this.get('showCurrent');
+ const ancestors = utils.ancestorKeysForKey(baseKey);
+ const parts = utils.keyPartsForKey(baseKey);
+ if (!ancestors) {
+ crumbs.push({
+ label: baseKey,
+ text: this.stripTrailingSlash(baseKey),
+ path: currentPath,
+ model: baseKeyModel,
+ });
+
+ if (!showCurrent) {
+ crumbs.pop();
+ }
+
+ return crumbs;
+ }
+
+ ancestors.forEach((ancestor, index) => {
+ crumbs.push({
+ label: parts[index],
+ text: this.stripTrailingSlash(parts[index]),
+ path: path,
+ model: ancestor,
+ });
+ });
+
+ crumbs.push({
+ label: utils.keyWithoutParentKey(baseKey),
+ text: this.stripTrailingSlash(utils.keyWithoutParentKey(baseKey)),
+ path: currentPath,
+ model: baseKeyModel,
+ });
+
+ if (!showCurrent) {
+ crumbs.pop();
+ }
+
+ return crumbs;
+ }),
+});
diff --git a/ui/app/components/key-version-select.js b/ui/app/components/key-version-select.js
new file mode 100644
index 000000000..e3ac4fb5c
--- /dev/null
+++ b/ui/app/components/key-version-select.js
@@ -0,0 +1,5 @@
+import Ember from 'ember';
+
+export default Ember.Component.extend({
+ tagName: '',
+});
diff --git a/ui/app/components/kv-object-editor.js b/ui/app/components/kv-object-editor.js
new file mode 100644
index 000000000..0603998ea
--- /dev/null
+++ b/ui/app/components/kv-object-editor.js
@@ -0,0 +1,67 @@
+import Ember from 'ember';
+import KVObject from 'vault/lib/kv-object';
+
+const { assert, Component, computed, guidFor } = Ember;
+
+export default Component.extend({
+ 'data-test-component': 'kv-object-editor',
+ classNames: ['field', 'form-section'],
+ // public API
+ // Ember Object to mutate
+ value: null,
+ label: null,
+ helpText: null,
+ // onChange will be called with the changed Value
+ onChange() {},
+
+ init() {
+ this._super(...arguments);
+ const data = KVObject.create({ content: [] }).fromJSON(this.get('value'));
+ this.set('kvData', data);
+ this.addRow();
+ },
+
+ kvData: null,
+
+ kvDataAsJSON: computed('kvData', 'kvData.[]', function() {
+ return this.get('kvData').toJSON();
+ }),
+
+ kvDataIsAdvanced: computed('kvData', 'kvData.[]', function() {
+ return this.get('kvData').isAdvanced();
+ }),
+
+ kvHasDuplicateKeys: computed('kvData', 'kvData.@each.name', function() {
+ let data = this.get('kvData');
+ return data.uniqBy('name').length !== data.get('length');
+ }),
+
+ addRow() {
+ let data = this.get('kvData');
+ let newObj = { name: '', value: '' };
+ if (!Ember.isNone(data.findBy('name', ''))) {
+ return;
+ }
+ guidFor(newObj);
+ data.addObject(newObj);
+ },
+ actions: {
+ addRow() {
+ this.addRow();
+ },
+
+ updateRow() {
+ let data = this.get('kvData');
+ this.get('onChange')(data.toJSON());
+ },
+
+ deleteRow(object, index) {
+ let data = this.get('kvData');
+ let oldObj = data.objectAt(index);
+
+ assert('object guids match', guidFor(oldObj) === guidFor(object));
+ data.removeAt(index);
+ this.get('onChange')(data.toJSON());
+ },
+ },
+});
diff --git a/ui/app/components/link-to.js b/ui/app/components/link-to.js
new file mode 100644
index 000000000..872a2ffac
--- /dev/null
+++ b/ui/app/components/link-to.js
@@ -0,0 +1,7 @@
+import Ember from 'ember';
+
+Ember.LinkComponent.reopen({
+ activeClass: 'is-active',
+});
+
+export default Ember.LinkComponent;
diff --git a/ui/app/components/linked-block.js b/ui/app/components/linked-block.js
new file mode 100644
index 000000000..cdbaa6c09
--- /dev/null
+++ b/ui/app/components/linked-block.js
@@ -0,0 +1,35 @@
+import Ember from 'ember';
+import hbs from 'htmlbars-inline-precompile';
+
+let LinkedBlockComponent = Ember.Component.extend({
+ layout: hbs`{{yield}}`,
+
+ classNames: 'linked-block',
+
+ routing: Ember.inject.service('-routing'),
+ queryParams: null,
+
+ click(event) {
+ const $target = this.$(event.target);
+ const isAnchorOrButton =
+ $target.is('a') ||
+ $target.is('button') ||
+ $target.closest('button', event.currentTarget).length > 0 ||
+ $target.closest('a', event.currentTarget).length > 0;
+ if (!isAnchorOrButton) {
+ const router = this.get('routing.router');
+ const params = this.get('params');
+ const queryParams = this.get('queryParams');
+ if (queryParams) {
+ params.push({ queryParams });
+ }
+ router.transitionTo.apply(router, params);
+ }
+ },
+});
+
+LinkedBlockComponent.reopenClass({
+ positionalParams: 'params',
+});
+
+export default LinkedBlockComponent;
diff --git a/ui/app/components/list-pagination.js b/ui/app/components/list-pagination.js
new file mode 100644
index 000000000..163a253b9
--- /dev/null
+++ b/ui/app/components/list-pagination.js
@@ -0,0 +1,37 @@
+import Ember from 'ember';
+import { range } from 'ember-composable-helpers/helpers/range';
+
+const { computed } = Ember;
+
+export default Ember.Component.extend({
+ classNames: ['box', 'is-shadowless', 'list-pagination'],
+ page: null,
+ lastPage: null,
+ link: null,
+ model: null,
+ // number of links to show on each side of page
+ spread: 2,
+ hasNext: computed('page', 'lastPage', function() {
+ return this.get('page') < this.get('lastPage');
+ }),
+ hasPrevious: computed('page', 'lastPage', function() {
+ return this.get('page') > 1;
+ }),
+
+ segmentLinks: computed.gt('lastPage', 10),
+
+ pageRange: computed('page', 'lastPage', function() {
+ const { spread, page, lastPage } = this.getProperties('spread', 'page', 'lastPage');
+
+ let lower = Math.max(2, page - spread);
+ let upper = Math.min(lastPage - 1, lower + spread * 2);
+ // we're closer to lastPage than the spread
+ if (upper - lower < 5) {
+ lower = upper - 4;
+ }
+ if (lastPage <= 10) {
+ return range([1, lastPage, true]);
+ }
+ return range([lower, upper, true]);
+ }),
+});
diff --git a/ui/app/components/logo-splash.js b/ui/app/components/logo-splash.js
new file mode 100644
index 000000000..48e3f15c1
--- /dev/null
+++ b/ui/app/components/logo-splash.js
@@ -0,0 +1,8 @@
+import Ember from 'ember';
+
+const { inject } = Ember;
+
+export default Ember.Component.extend({
+ tagName: '',
+ version: inject.service(),
+});
diff --git a/ui/app/components/menu-sidebar.js b/ui/app/components/menu-sidebar.js
new file mode 100644
index 000000000..0382ad413
--- /dev/null
+++ b/ui/app/components/menu-sidebar.js
@@ -0,0 +1,15 @@
+import Ember from 'ember';
+
+export default Ember.Component.extend({
+ classNames: ['column', 'is-sidebar'],
+ classNameBindings: ['isActive:is-active'],
+ isActive: false,
+ actions: {
+ openMenu() {
+ this.set('isActive', true);
+ },
+ closeMenu() {
+ this.set('isActive', false);
+ },
+ },
+});
diff --git a/ui/app/components/message-error.js b/ui/app/components/message-error.js
new file mode 100644
index 000000000..04e9f2294
--- /dev/null
+++ b/ui/app/components/message-error.js
@@ -0,0 +1,32 @@
+import Ember from 'ember';
+
+export default Ember.Component.extend({
+ tagName: '',
+ model: null,
+ errors: [],
+ errorMessage: null,
+
+ displayErrors: Ember.computed(
+ 'errorMessage',
+ 'model.isError',
+ 'model.adapterError.errors.@each',
+ 'errors',
+ 'errors.@each',
+ function() {
+ const errorMessage = this.get('errorMessage');
+ const errors = this.get('errors');
+ const modelIsError = this.get('model.isError');
+ if (errorMessage) {
+ return [errorMessage];
+ }
+
+ if (errors && errors.length > 0) {
+ return errors;
+ }
+
+ if (modelIsError) {
+ return this.get('model.adapterError.errors');
+ }
+ }
+ ),
+});
diff --git a/ui/app/components/message-in-page.js b/ui/app/components/message-in-page.js
new file mode 100644
index 000000000..f25076d90
--- /dev/null
+++ b/ui/app/components/message-in-page.js
@@ -0,0 +1,20 @@
+import Ember from 'ember';
+
+import { messageTypes } from 'vault/helpers/message-types';
+const { computed } = Ember;
+
+export default Ember.Component.extend({
+ type: null,
+
+ classNameBindings: ['containerClass'],
+
+ containerClass: computed('type', function() {
+ return 'message ' + messageTypes([this.get('type')]).class;
+ }),
+
+ alertType: computed('type', function() {
+ return messageTypes([this.get('type')]);
+ }),
+
+ messageClass: 'message-body',
+});
diff --git a/ui/app/components/mount-accessor-select.js b/ui/app/components/mount-accessor-select.js
new file mode 100644
index 000000000..bdd5d7c97
--- /dev/null
+++ b/ui/app/components/mount-accessor-select.js
@@ -0,0 +1,32 @@
+import Ember from 'ember';
+import { task } from 'ember-concurrency';
+
+const { inject } = Ember;
+
+export default Ember.Component.extend({
+ store: inject.service(),
+
+ // Public API
+ //value for the external mount selector
+ value: null,
+ onChange: () => {},
+
+ init() {
+ this._super(...arguments);
+ this.get('authMethods').perform();
+ },
+
+ authMethods: task(function*() {
+ let methods = yield this.get('store').findAll('auth-method');
+ if (!this.get('value')) {
+ this.set('value', methods.get('firstObject.accessor'));
+ }
+ return methods;
+ }).drop(),
+
+ actions: {
+ change(value) {
+ this.get('onChange')(value);
+ },
+ },
+});
diff --git a/ui/app/components/mount-backend-form.js b/ui/app/components/mount-backend-form.js
new file mode 100644
index 000000000..d9c48dfd6
--- /dev/null
+++ b/ui/app/components/mount-backend-form.js
@@ -0,0 +1,137 @@
+import Ember from 'ember';
+import { task } from 'ember-concurrency';
+import { methods } from 'vault/helpers/mountable-auth-methods';
+
+const { inject } = Ember;
+const METHODS = methods();
+
+export default Ember.Component.extend({
+ store: inject.service(),
+ flashMessages: inject.service(),
+ routing: inject.service('-routing'),
+
+ /*
+ * @param Function
+ * @public
+ *
+ * Optional param to call a function upon successfully mounting a backend
+ *
+ */
+ onMountSuccess: () => {},
+ onConfigError: () => {},
+ /*
+ * @param String
+ * @public
+ * the type of backend we want to mount
+ * defaults to `auth`
+ *
+ */
+ mountType: 'auth',
+
+ /*
+ *
+ * @param DS.Model
+ * @private
+ * Ember Data model corresponding to the `mountType`.
+ * Created and set during `init`
+ *
+ */
+ mountModel: null,
+
+ init() {
+ this._super(...arguments);
+ const type = this.get('mountType');
+ const modelType = type === 'secret' ? 'secret-engine' : 'auth-method';
+ const model = this.get('store').createRecord(modelType);
+ this.set('mountModel', model);
+ this.changeConfigModel(model.get('type'));
+ },
+
+ willDestroy() {
+ // if unsaved, we want to unload so it doesn't show up in the auth mount list
+ this.get('mountModel').rollbackAttributes();
+ },
+
+ getConfigModelType(methodType) {
+ let noConfig = ['approle'];
+ if (noConfig.includes(methodType)) {
+ return;
+ }
+ if (methodType === 'aws') {
+ return 'auth-config/aws/client';
+ }
+ return `auth-config/${methodType}`;
+ },
+
+ changeConfigModel(methodType) {
+ const mount = this.get('mountModel');
+ const configRef = mount.hasMany('authConfigs').value();
+ const currentConfig = configRef.get('firstObject');
+ if (currentConfig) {
+ // rollbackAttributes here will remove the the config model from the store
+ // because `isNew` will be true
+ currentConfig.rollbackAttributes();
+ }
+ const configType = this.getConfigModelType(methodType);
+ if (!configType) return;
+ const config = this.get('store').createRecord(configType);
+ config.set('backend', mount);
+ },
+
+ checkPathChange(type) {
+ const mount = this.get('mountModel');
+ const currentPath = mount.get('path');
+ // if the current path matches a type (meaning the user hasn't altered it),
+ // change it here to match the new type
+ const isUnchanged = METHODS.findBy('type', currentPath);
+ if (isUnchanged) {
+ mount.set('path', type);
+ }
+ },
+
+ mountBackend: task(function*() {
+ const mountModel = this.get('mountModel');
+ const { type, path } = mountModel.getProperties('type', 'path');
+ try {
+ yield mountModel.save();
+ } catch (err) {
+ // err will display via model state
+ return;
+ }
+ this.get('flashMessages').success(
+ `Successfully mounted ${type} ${this.get('mountType')} method at ${path}.`
+ );
+ yield this.get('saveConfig').perform(mountModel);
+ }).drop(),
+
+ saveConfig: task(function*(mountModel) {
+ const configRef = mountModel.hasMany('authConfigs').value();
+ const config = configRef.get('firstObject');
+ const { type, path } = mountModel.getProperties('type', 'path');
+ try {
+ if (config && Object.keys(config.changedAttributes()).length) {
+ yield config.save();
+ this.get('flashMessages').success(
+ `The config for ${type} ${this.get('mountType')} method at ${path} was saved successfully.`
+ );
+ }
+ yield this.get('onMountSuccess')();
+ } catch (err) {
+ this.get('flashMessages').danger(
+ `There was an error saving the configuration for ${type} ${this.get(
+ 'mountType'
+ )} method at ${path}. ${err.errors.join(' ')}`
+ );
+ yield this.get('onConfigError')(mountModel.id);
+ }
+ }).drop(),
+
+ actions: {
+ onTypeChange(path, value) {
+ if (path === 'type') {
+ this.changeConfigModel(value);
+ this.checkPathChange(value);
+ }
+ },
+ },
+});
diff --git a/ui/app/components/mount-filter-config-list.js b/ui/app/components/mount-filter-config-list.js
new file mode 100644
index 000000000..848d62c93
--- /dev/null
+++ b/ui/app/components/mount-filter-config-list.js
@@ -0,0 +1,26 @@
+import Ember from 'ember';
+
+const { get, set } = Ember;
+
+export default Ember.Component.extend({
+ config: null,
+ mounts: null,
+
+ // singleton mounts are not eligible for per-mount-filtering
+ singletonMountTypes: ['cubbyhole', 'system', 'token', 'identity'],
+
+ actions: {
+ addOrRemovePath(path, e) {
+ let config = get(this, 'config') || [];
+ let paths = get(config, 'paths').slice();
+
+ if (e.target.checked) {
+ paths.addObject(path);
+ } else {
+ paths.removeObject(path);
+ }
+
+ set(config, 'paths', paths);
+ },
+ },
+});
diff --git a/ui/app/components/navigate-input.js b/ui/app/components/navigate-input.js
new file mode 100644
index 000000000..f56c22750
--- /dev/null
+++ b/ui/app/components/navigate-input.js
@@ -0,0 +1,190 @@
+import Ember from 'ember';
+import utils from 'vault/lib/key-utils';
+import keys from 'vault/lib/keycodes';
+import FocusOnInsertMixin from 'vault/mixins/focus-on-insert';
+
+const routeFor = function(type, mode) {
+ const MODES = {
+ secrets: 'vault.cluster.secrets.backend',
+ 'secrets-cert': 'vault.cluster.secrets.backend',
+ 'policy-show': 'vault.cluster.policy',
+ 'policy-list': 'vault.cluster.policies',
+ leases: 'vault.cluster.access.leases',
+ };
+ let useSuffix = true;
+ const typeVal = mode === 'secrets' || mode === 'leases' ? type : type.replace('-root', '');
+ const modeKey = mode + '-' + typeVal;
+ const modeVal = MODES[modeKey] || MODES[mode];
+ if (modeKey === 'policy-list') {
+ useSuffix = false;
+ }
+
+ return useSuffix ? modeVal + '.' + typeVal : modeVal;
+};
+
+export default Ember.Component.extend(FocusOnInsertMixin, {
+ classNames: ['navigate-filter'],
+
+ // these get passed in from the outside
+ // actions that get passed in
+ filterFocusDidChange: null,
+ filterDidChange: null,
+ mode: 'secrets',
+ shouldNavigateTree: false,
+ extraNavParams: null,
+
+ baseKey: null,
+ filter: null,
+ filterMatchesKey: null,
+ firstPartialMatch: null,
+
+ routing: Ember.inject.service('-routing'),
+
+ transitionToRoute: function() {
+ var router = this.get('routing.router');
+ router.transitionTo.apply(router, arguments);
+ },
+
+ shouldFocus: false,
+
+ focusFilter: Ember.observer('filter', function() {
+ if (!this.get('filter')) return;
+ Ember.run.schedule('afterRender', this, 'forceFocus');
+ }).on('didInsertElement'),
+
+ keyForNav(key) {
+ if (this.get('mode') !== 'secrets-cert') {
+ return key;
+ }
+ return `cert/${key}`;
+ },
+ onEnter: function(val) {
+ let baseKey = this.get('baseKey');
+ let mode = this.get('mode');
+ let extraParams = this.get('extraNavParams');
+ if (mode.startsWith('secrets') && (!val || val === baseKey)) {
+ return;
+ }
+ if (this.get('filterMatchesKey') && !utils.keyIsFolder(val)) {
+ let params = [routeFor('show', mode), extraParams, this.keyForNav(val)].compact();
+ this.transitionToRoute(...params);
+ } else {
+ if (mode === 'policies') {
+ return;
+ }
+ let route = routeFor('create', mode);
+ if (baseKey) {
+ this.transitionToRoute(route, this.keyForNav(baseKey), {
+ queryParams: {
+ initialKey: val.replace(this.keyForNav(baseKey), ''),
+ },
+ });
+ } else {
+ this.transitionToRoute(route + '-root', {
+ queryParams: {
+ initialKey: this.keyForNav(val),
+ },
+ });
+ }
+ }
+ },
+
+ // pop to the nearest parentKey or to the root
+ onEscape: function(val) {
+ var key = utils.parentKeyForKey(val) || '';
+ this.get('filterDidChange')(key);
+ this.filterUpdated(key);
+ },
+
+ onTab: function(event) {
+ var firstPartialMatch = this.get('firstPartialMatch.id');
+ if (!firstPartialMatch) {
+ return;
+ }
+ event.preventDefault();
+ this.get('filterDidChange')(firstPartialMatch);
+ this.filterUpdated(firstPartialMatch);
+ },
+
+ // as you type, navigates through the k/v tree
+ filterUpdated: function(val) {
+ var mode = this.get('mode');
+ if (mode === 'policies' || !this.get('shouldNavigateTree')) {
+ this.filterUpdatedNoNav(val, mode);
+ return;
+ }
+ // select the key to nav to, assumed to be a folder
+ var key = val ? val.trim() : '';
+ var isFolder = utils.keyIsFolder(key);
+
+ if (!isFolder) {
+ // nav to the closest parentKey (or the root)
+ key = utils.parentKeyForKey(val) || '';
+ }
+
+ const pageFilter = val.replace(key, '');
+ this.navigate(this.keyForNav(key), mode, pageFilter);
+ },
+
+ navigate(key, mode, pageFilter) {
+ const route = routeFor(key ? 'list' : 'list-root', mode);
+ let args = [route];
+ if (key) {
+ args.push(key);
+ }
+ if (pageFilter && !utils.keyIsFolder(pageFilter)) {
+ args.push({
+ queryParams: {
+ page: 1,
+ pageFilter,
+ },
+ });
+ } else {
+ args.push({
+ queryParams: {
+ page: 1,
+ pageFilter: null,
+ },
+ });
+ }
+ this.transitionToRoute(...args);
+ },
+
+ filterUpdatedNoNav: function(val, mode) {
+ var key = val ? val.trim() : null;
+ this.transitionToRoute(routeFor('list-root', mode), {
+ queryParams: {
+ pageFilter: key,
+ page: 1,
+ },
+ });
+ },
+
+ actions: {
+ handleInput: function(event) {
+ var filter = event.target.value;
+ this.get('filterDidChange')(filter);
+ Ember.run.debounce(this, 'filterUpdated', filter, 200);
+ },
+
+ setFilterFocused: function(isFocused) {
+ this.get('filterFocusDidChange')(isFocused);
+ },
+
+ handleKeyPress: function(val, event) {
+ if (event.keyCode === keys.TAB) {
+ this.onTab(event);
+ }
+ },
+
+ handleKeyUp: function(val, event) {
+ var keyCode = event.keyCode;
+ if (keyCode === keys.ENTER) {
+ this.onEnter(val);
+ }
+ if (keyCode === keys.ESC) {
+ this.onEscape(val);
+ }
+ },
+ },
+});
diff --git a/ui/app/components/not-found.js b/ui/app/components/not-found.js
new file mode 100644
index 000000000..5d76dd583
--- /dev/null
+++ b/ui/app/components/not-found.js
@@ -0,0 +1,12 @@
+import Ember from 'ember';
+
+const { computed, inject } = Ember;
+
+export default Ember.Component.extend({
+ // public
+ model: null,
+
+ tagName: '',
+ routing: inject.service('-routing'),
+ path: computed.alias('routing.router.currentURL'),
+});
diff --git a/ui/app/components/pgp-file.js b/ui/app/components/pgp-file.js
new file mode 100644
index 000000000..43735afce
--- /dev/null
+++ b/ui/app/components/pgp-file.js
@@ -0,0 +1,74 @@
+import Ember from 'ember';
+
+const { set } = Ember;
+const BASE_64_REGEX = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/gi;
+
+export default Ember.Component.extend({
+ classNames: ['box', 'is-fullwidth', 'is-marginless', 'is-shadowless'],
+ key: null,
+ index: null,
+ onChange: () => {},
+
+ /*
+ * @public
+ * @param String
+ * Text to use as the label for the file input
+ * If null, a default will be rendered
+ */
+ label: null,
+
+ /*
+ * @public
+ * @param String
+ * Text to use as help under the file input
+ * If null, a default will be rendered
+ */
+ fileHelpText: null,
+
+ /*
+ * @public
+ * @param String
+ * Text to use as help under the textarea in text-input mode
+ * If null, a default will be rendered
+ */
+ textareaHelpText: null,
+
+ readFile(file) {
+ const reader = new FileReader();
+ reader.onload = () => this.setPGPKey(reader.result, file.name);
+ // this gives us a base64-encoded string which is important in the onload
+ reader.readAsDataURL(file);
+ },
+
+ setPGPKey(dataURL, filename) {
+ const b64File = dataURL.split(',')[1].trim();
+ const decoded = atob(b64File).trim();
+
+ // If a b64-encoded file was uploaded, then after decoding, it
+ // will still be b64.
+ // If after decoding it's not b64, we want
+ // the original as it was only encoded when we used `readAsDataURL`.
+ const fileData = decoded.match(BASE_64_REGEX) ? decoded : b64File;
+ this.get('onChange')(this.get('index'), { value: fileData, fileName: filename });
+ },
+
+ actions: {
+ pickedFile(e) {
+ const { files } = e.target;
+ if (!files.length) {
+ return;
+ }
+ for (let i = 0, len = files.length; i < len; i++) {
+ this.readFile(files[i]);
+ }
+ },
+ updateData(e) {
+ const key = this.get('key');
+ set(key, 'value', e.target.value);
+ this.get('onChange')(this.get('index'), this.get('key'));
+ },
+ clearKey() {
+ this.get('onChange')(this.get('index'), { value: '' });
+ },
+ },
+});
diff --git a/ui/app/components/pgp-list.js b/ui/app/components/pgp-list.js
new file mode 100644
index 000000000..b8aed1c78
--- /dev/null
+++ b/ui/app/components/pgp-list.js
@@ -0,0 +1,20 @@
+import Ember from 'ember';
+
+export default Ember.Component.extend({
+ onDataUpdate: () => {},
+ listData: Ember.computed('listLength', function() {
+ let num = this.get('listLength');
+ if (num) {
+ num = parseInt(num, 10);
+ }
+ return Array(num || 0).fill(null).map(() => ({ value: '' }));
+ }),
+ listLength: 0,
+ actions: {
+ setKey(index, key) {
+ let listData = this.get('listData');
+ listData.replace(index, 1, key);
+ this.get('onDataUpdate')(listData.compact().map(k => k.value));
+ },
+ },
+});
diff --git a/ui/app/components/pki-cert-popup.js b/ui/app/components/pki-cert-popup.js
new file mode 100644
index 000000000..64faaeb59
--- /dev/null
+++ b/ui/app/components/pki-cert-popup.js
@@ -0,0 +1,17 @@
+import Ember from 'ember';
+
+export default Ember.Component.extend({
+ /*
+ * @public
+ * @param DS.Model
+ *
+ * the pki-certificate model
+ */
+ item: null,
+
+ actions: {
+ delete(item) {
+ item.save({ adapterOptions: { method: 'revoke' } });
+ },
+ },
+});
diff --git a/ui/app/components/pki-cert-show.js b/ui/app/components/pki-cert-show.js
new file mode 100644
index 000000000..4a2e09c45
--- /dev/null
+++ b/ui/app/components/pki-cert-show.js
@@ -0,0 +1,9 @@
+import RoleEdit from './role-edit';
+
+export default RoleEdit.extend({
+ actions: {
+ delete() {
+ this.get('model').save({ adapterOptions: { method: 'revoke' } });
+ },
+ },
+});
diff --git a/ui/app/components/popup-menu.js b/ui/app/components/popup-menu.js
new file mode 100644
index 000000000..e3ac4fb5c
--- /dev/null
+++ b/ui/app/components/popup-menu.js
@@ -0,0 +1,5 @@
+import Ember from 'ember';
+
+export default Ember.Component.extend({
+ tagName: '',
+});
diff --git a/ui/app/components/replication-actions.js b/ui/app/components/replication-actions.js
new file mode 100644
index 000000000..2e1b22a90
--- /dev/null
+++ b/ui/app/components/replication-actions.js
@@ -0,0 +1,58 @@
+import Ember from 'ember';
+import ReplicationActions from 'vault/mixins/replication-actions';
+
+const { computed } = Ember;
+
+const DEFAULTS = {
+ token: null,
+ primary_api_addr: null,
+ primary_cluster_addr: null,
+ errors: [],
+ id: null,
+ replicationMode: null,
+};
+
+export default Ember.Component.extend(ReplicationActions, DEFAULTS, {
+ replicationMode: null,
+ selectedAction: null,
+ tagName: 'form',
+
+ didReceiveAttrs() {
+ this._super(...arguments);
+ },
+
+ model: null,
+ cluster: computed.alias('model'),
+ loading: false,
+ onSubmit: null,
+
+ reset() {
+ if (!this || this.isDestroyed || this.isDestroying) {
+ return;
+ }
+ this.setProperties(DEFAULTS);
+ },
+
+ replicationDisplayMode: computed('replicationMode', function() {
+ const replicationMode = this.get('replicationMode');
+ if (replicationMode === 'dr') {
+ return 'DR';
+ }
+ if (replicationMode === 'performance') {
+ return 'Performance';
+ }
+ }),
+
+ actions: {
+ onSubmit() {
+ return this.submitHandler(...arguments);
+ },
+ clear() {
+ this.reset();
+ this.setProperties({
+ token: null,
+ id: null,
+ });
+ },
+ },
+});
diff --git a/ui/app/components/replication-mode-summary.js b/ui/app/components/replication-mode-summary.js
new file mode 100644
index 000000000..6c26d98ec
--- /dev/null
+++ b/ui/app/components/replication-mode-summary.js
@@ -0,0 +1,51 @@
+import Ember from 'ember';
+import { hrefTo } from 'vault/helpers/href-to';
+const { computed, get, getProperties } = Ember;
+
+const replicationAttr = function(attr) {
+ return computed('mode', `cluster.{dr,performance}.${attr}`, function() {
+ const { mode, cluster } = getProperties(this, 'mode', 'cluster');
+ return get(cluster, `${mode}.${attr}`);
+ });
+};
+export default Ember.Component.extend({
+ version: Ember.inject.service(),
+ classNames: ['level', 'box-label'],
+ classNameBindings: ['isMenu:is-mobile'],
+ attributeBindings: ['href', 'target'],
+ display: 'banner',
+ isMenu: computed.equal('display', 'menu'),
+ href: computed('display', 'mode', 'replicationEnabled', 'version.hasPerfReplication', function() {
+ const display = this.get('display');
+ const mode = this.get('mode');
+ if (mode === 'performance' && display === 'menu' && this.get('version.hasPerfReplication') === false) {
+ return 'https://www.hashicorp.com/products/vault';
+ }
+ if (this.get('replicationEnabled') || display === 'menu') {
+ return hrefTo(this, 'vault.cluster.replication.mode.index', this.get('cluster.name'), mode);
+ }
+ return null;
+ }),
+ target: computed('isPerformance', 'version.hasPerfReplication', function() {
+ if (this.get('isPerformance') && this.get('version.hasPerfReplication') === false) {
+ return '_blank';
+ }
+ return null;
+ }),
+ isPerformance: computed.equal('mode', 'performance'),
+ replicationEnabled: replicationAttr('replicationEnabled'),
+ replicationUnsupported: replicationAttr('replicationUnsupported'),
+ replicationDisabled: replicationAttr('replicationDisabled'),
+ syncProgressPercent: replicationAttr('syncProgressPercent'),
+ syncProgress: replicationAttr('syncProgress'),
+ secondaryId: replicationAttr('secondaryId'),
+ modeForUrl: replicationAttr('modeForUrl'),
+ clusterIdDisplay: replicationAttr('clusterIdDisplay'),
+ mode: null,
+ cluster: null,
+ partialName: computed('display', function() {
+ return this.get('display') === 'menu'
+ ? 'partials/replication/replication-mode-summary-menu'
+ : 'partials/replication/replication-mode-summary';
+ }),
+});
diff --git a/ui/app/components/replication-secondaries.js b/ui/app/components/replication-secondaries.js
new file mode 100644
index 000000000..c13704b15
--- /dev/null
+++ b/ui/app/components/replication-secondaries.js
@@ -0,0 +1,19 @@
+import Ember from 'ember';
+
+const { computed } = Ember;
+
+export default Ember.Component.extend({
+ cluster: null,
+ replicationMode: null,
+ secondaries: null,
+ onRevoke: Function.prototype,
+
+ addRoute: computed('replicationMode', function() {}),
+ revokeRoute: computed('replicationMode', function() {}),
+
+ actions: {
+ onConfirmRevoke() {
+ this.get('onRevoke')(...arguments);
+ },
+ },
+});
diff --git a/ui/app/components/replication-summary.js b/ui/app/components/replication-summary.js
new file mode 100644
index 000000000..35ae26fe1
--- /dev/null
+++ b/ui/app/components/replication-summary.js
@@ -0,0 +1,80 @@
+import Ember from 'ember';
+import decodeConfigFromJWT from 'vault/utils/decode-config-from-jwt';
+import ReplicationActions from 'vault/mixins/replication-actions';
+
+const { computed, get } = Ember;
+
+const DEFAULTS = {
+ mode: 'primary',
+ token: null,
+ id: null,
+ loading: false,
+ errors: [],
+ primary_api_addr: null,
+ primary_cluster_addr: null,
+ ca_file: null,
+ ca_path: null,
+ replicationMode: 'dr',
+};
+
+export default Ember.Component.extend(ReplicationActions, DEFAULTS, {
+ didReceiveAttrs() {
+ this._super(...arguments);
+ const initialReplicationMode = this.get('initialReplicationMode');
+ if (initialReplicationMode) {
+ this.set('replicationMode', initialReplicationMode);
+ }
+ },
+ showModeSummary: false,
+ initialReplicationMode: null,
+ cluster: null,
+ version: Ember.inject.service(),
+
+ replicationAttrs: computed.alias('cluster.replicationAttrs'),
+
+ tokenIncludesAPIAddr: computed('token', function() {
+ const config = decodeConfigFromJWT(get(this, 'token'));
+ return config && config.addr ? true : false;
+ }),
+
+ disallowEnable: computed(
+ 'replicationMode',
+ 'version.hasPerfReplication',
+ 'mode',
+ 'tokenIncludesAPIAddr',
+ 'primary_api_addr',
+ function() {
+ const inculdesAPIAddr = this.get('tokenIncludesAPIAddr');
+ if (this.get('replicationMode') === 'performance' && this.get('version.hasPerfReplication') === false) {
+ return true;
+ }
+ if (
+ this.get('mode') !== 'secondary' ||
+ inculdesAPIAddr ||
+ (!inculdesAPIAddr && this.get('primary_api_addr'))
+ ) {
+ return false;
+ }
+
+ return true;
+ }
+ ),
+
+ reset() {
+ this.setProperties(DEFAULTS);
+ },
+
+ actions: {
+ onSubmit(/*action, mode, data, event*/) {
+ return this.submitHandler(...arguments);
+ },
+
+ clear() {
+ this.reset();
+ this.setProperties({
+ token: null,
+ id: null,
+ });
+ },
+ },
+});
diff --git a/ui/app/components/role-aws-edit.js b/ui/app/components/role-aws-edit.js
new file mode 100644
index 000000000..0b5fb5ec6
--- /dev/null
+++ b/ui/app/components/role-aws-edit.js
@@ -0,0 +1,49 @@
+import RoleEdit from './role-edit';
+import Ember from 'ember';
+
+const { get, set } = Ember;
+const SHOW_ROUTE = 'vault.cluster.secrets.backend.show';
+
+export default RoleEdit.extend({
+ useARN: false,
+ init() {
+ this._super(...arguments);
+ const arn = get(this, 'model.arn');
+ if (arn) {
+ set(this, 'useARN', true);
+ }
+ },
+
+ actions: {
+ createOrUpdate(type, event) {
+ event.preventDefault();
+
+ const modelId = this.get('model.id');
+ // prevent from submitting if there's no key
+ // maybe do something fancier later
+ if (type === 'create' && Ember.isBlank(modelId)) {
+ return;
+ }
+ // clear the policy or arn before save depending on "useARN"
+ if (get(this, 'useARN')) {
+ set(this, 'model.policy', '');
+ } else {
+ set(this, 'model.arn', '');
+ }
+
+ this.persist('save', () => {
+ this.hasDataChanges();
+ this.transitionToRoute(SHOW_ROUTE, modelId);
+ });
+ },
+
+ codemirrorUpdated(attr, val, codemirror) {
+ codemirror.performLint();
+ const hasErrors = codemirror.state.lint.marked.length > 0;
+
+ if (!hasErrors) {
+ set(this.get('model'), attr, val);
+ }
+ },
+ },
+});
diff --git a/ui/app/components/role-edit.js b/ui/app/components/role-edit.js
new file mode 100644
index 000000000..4f84c51b1
--- /dev/null
+++ b/ui/app/components/role-edit.js
@@ -0,0 +1,114 @@
+import Ember from 'ember';
+import FocusOnInsertMixin from 'vault/mixins/focus-on-insert';
+import keys from 'vault/lib/keycodes';
+
+const { get, set, computed } = Ember;
+const LIST_ROOT_ROUTE = 'vault.cluster.secrets.backend.list-root';
+const SHOW_ROUTE = 'vault.cluster.secrets.backend.show';
+
+export default Ember.Component.extend(FocusOnInsertMixin, {
+ mode: null,
+ emptyData: '{\n}',
+ onDataChange: () => {},
+ refresh: 'refresh',
+ model: null,
+ routing: Ember.inject.service('-routing'),
+ requestInFlight: computed.or('model.isLoading', 'model.isReloading', 'model.isSaving'),
+ willDestroyElement() {
+ const model = this.get('model');
+ if (get(model, 'isError')) {
+ model.rollbackAttributes();
+ }
+ },
+
+ transitionToRoute() {
+ const router = this.get('routing.router');
+ router.transitionTo.apply(router, arguments);
+ },
+
+ bindKeys: Ember.on('didInsertElement', function() {
+ Ember.$(document).on('keyup.keyEdit', this.onEscape.bind(this));
+ }),
+
+ unbindKeys: Ember.on('willDestroyElement', function() {
+ Ember.$(document).off('keyup.keyEdit');
+ }),
+
+ onEscape(e) {
+ if (e.keyCode !== keys.ESC || this.get('mode') !== 'show') {
+ return;
+ }
+ this.transitionToRoute(LIST_ROOT_ROUTE);
+ },
+
+ hasDataChanges() {
+ get(this, 'onDataChange')(get(this, 'model.hasDirtyAttributes'));
+ },
+
+ persist(method, successCallback) {
+ const model = get(this, 'model');
+ return model[method]().then(result => {
+ if (!Ember.get(result, 'didError')) {
+ successCallback(model);
+ }
+ });
+ },
+
+ actions: {
+ handleKeyDown(_, e) {
+ e.stopPropagation();
+ if (!(e.keyCode === keys.ENTER && e.metaKey)) {
+ return;
+ }
+ let $form = this.$('form');
+ if ($form.length) {
+ $form.submit();
+ }
+ $form = null;
+ },
+
+ createOrUpdate(type, event) {
+ event.preventDefault();
+
+ const modelId = this.get('model.id');
+ // prevent from submitting if there's no key
+ // maybe do something fancier later
+ if (type === 'create' && Ember.isBlank(modelId)) {
+ return;
+ }
+
+ this.persist('save', () => {
+ this.hasDataChanges();
+ this.transitionToRoute(SHOW_ROUTE, modelId);
+ });
+ },
+
+ handleChange() {
+ this.hasDataChanges();
+ },
+
+ setValue(key, event) {
+ set(get(this, 'model'), key, event.target.checked);
+ },
+
+ refresh() {
+ this.sendAction('refresh');
+ },
+
+ delete() {
+ this.persist('destroyRecord', () => {
+ this.hasDataChanges();
+ this.transitionToRoute(LIST_ROOT_ROUTE);
+ });
+ },
+
+ codemirrorUpdated(attr, val, codemirror) {
+ codemirror.performLint();
+ const hasErrors = codemirror.state.lint.marked.length > 0;
+
+ if (!hasErrors) {
+ set(this.get('model'), attr, JSON.parse(val));
+ }
+ },
+ },
+});
diff --git a/ui/app/components/role-pki-edit.js b/ui/app/components/role-pki-edit.js
new file mode 100644
index 000000000..25d06d37a
--- /dev/null
+++ b/ui/app/components/role-pki-edit.js
@@ -0,0 +1,3 @@
+import RoleEdit from './role-edit';
+
+export default RoleEdit.extend({});
diff --git a/ui/app/components/role-ssh-edit.js b/ui/app/components/role-ssh-edit.js
new file mode 100644
index 000000000..25d06d37a
--- /dev/null
+++ b/ui/app/components/role-ssh-edit.js
@@ -0,0 +1,3 @@
+import RoleEdit from './role-edit';
+
+export default RoleEdit.extend({});
diff --git a/ui/app/components/secret-edit.js b/ui/app/components/secret-edit.js
new file mode 100644
index 000000000..ea0d8b7d5
--- /dev/null
+++ b/ui/app/components/secret-edit.js
@@ -0,0 +1,251 @@
+import Ember from 'ember';
+import FocusOnInsertMixin from 'vault/mixins/focus-on-insert';
+import keys from 'vault/lib/keycodes';
+import autosize from 'autosize';
+import KVObject from 'vault/lib/kv-object';
+
+const LIST_ROUTE = 'vault.cluster.secrets.backend.list';
+const LIST_ROOT_ROUTE = 'vault.cluster.secrets.backend.list-root';
+const SHOW_ROUTE = 'vault.cluster.secrets.backend.show';
+const { get, computed } = Ember;
+
+export default Ember.Component.extend(FocusOnInsertMixin, {
+ // a key model
+ key: null,
+
+ // a value to pre-fill the key input - this is populated by the corresponding
+ // 'initialKey' queryParam
+ initialKey: null,
+
+ // set in the route's setupController hook
+ mode: null,
+
+ secretData: null,
+
+ // called with a bool indicating if there's been a change in the secretData
+ onDataChange: () => {},
+
+ // did user request advanced mode
+ preferAdvancedEdit: false,
+
+ // use a named action here so we don't have to pass one in
+ // this will bubble to the route
+ toggleAdvancedEdit: 'toggleAdvancedEdit',
+
+ codemirrorString: null,
+
+ hasLintError: false,
+
+ init() {
+ this._super(...arguments);
+ const secrets = this.get('key.secretData');
+ const data = KVObject.create({ content: [] }).fromJSON(secrets);
+ this.set('secretData', data);
+ this.set('codemirrorString', data.toJSONString());
+ if (data.isAdvanced()) {
+ this.set('preferAdvancedEdit', true);
+ }
+ this.checkRows();
+ if (this.get('mode') === 'edit') {
+ this.send('addRow');
+ }
+ },
+
+ didRender() {
+ const textareas = this.$('textarea');
+ if (textareas.length) {
+ autosize(textareas);
+ }
+ },
+
+ willDestroyElement() {
+ const key = this.get('key');
+ if (get(key, 'isError') && !key.isDestroyed) {
+ key.rollbackAttributes();
+ }
+ },
+
+ partialName: Ember.computed('mode', function() {
+ return `partials/secret-form-${this.get('mode')}`;
+ }),
+
+ routing: Ember.inject.service('-routing'),
+
+ showPrefix: computed.or('key.initialParentKey', 'key.parentKey'),
+
+ requestInFlight: computed.or('key.isLoading', 'key.isReloading', 'key.isSaving'),
+
+ buttonDisabled: computed.or(
+ 'requestInFlight',
+ 'key.isFolder',
+ 'key.didError',
+ 'key.flagsIsInvalid',
+ 'hasLintError'
+ ),
+
+ basicModeDisabled: computed('secretDataIsAdvanced', 'showAdvancedMode', function() {
+ return this.get('secretDataIsAdvanced') || this.get('showAdvancedMode') === false;
+ }),
+
+ secretDataAsJSON: computed('secretData', 'secretData.[]', function() {
+ return this.get('secretData').toJSON();
+ }),
+
+ secretDataIsAdvanced: computed('secretData', 'secretData.[]', function() {
+ return this.get('secretData').isAdvanced();
+ }),
+
+ hasDataChanges() {
+ const keyDataString = this.get('key.dataAsJSONString');
+ const sameData = this.get('secretData').toJSONString() === keyDataString;
+ if (sameData === false) {
+ this.set('lastChange', Date.now());
+ }
+
+ this.get('onDataChange')(!sameData);
+ },
+
+ showAdvancedMode: computed('preferAdvancedEdit', 'secretDataIsAdvanced', 'lastChange', function() {
+ return this.get('secretDataIsAdvanced') || this.get('preferAdvancedEdit');
+ }),
+
+ transitionToRoute() {
+ const router = this.get('routing.router');
+ router.transitionTo.apply(router, arguments);
+ },
+
+ bindKeys: Ember.on('didInsertElement', function() {
+ Ember.$(document).on('keyup.keyEdit', this.onEscape.bind(this));
+ }),
+
+ unbindKeys: Ember.on('willDestroyElement', function() {
+ Ember.$(document).off('keyup.keyEdit');
+ }),
+
+ onEscape(e) {
+ if (e.keyCode !== keys.ESC || this.get('mode') !== 'show') {
+ return;
+ }
+ const parentKey = this.get('key.parentKey');
+ if (parentKey) {
+ this.transitionToRoute(LIST_ROUTE, parentKey);
+ } else {
+ this.transitionToRoute(LIST_ROOT_ROUTE);
+ }
+ },
+
+ // successCallback is called in the context of the component
+ persistKey(method, successCallback, isCreate) {
+ let model = this.get('key');
+ const key = model.get('id');
+
+ if (isCreate && typeof model.createRecord === 'function') {
+ // create an ember data model from the proxy
+ model = model.createRecord(model.get('backend'));
+ this.set('key', model);
+ }
+
+ return model[method]().then(result => {
+ if (!Ember.get(result, 'didError')) {
+ successCallback(key);
+ }
+ });
+ },
+
+ checkRows() {
+ if (this.get('secretData').get('length') === 0) {
+ this.send('addRow');
+ }
+ },
+
+ actions: {
+ handleKeyDown(_, e) {
+ e.stopPropagation();
+ if (!(e.keyCode === keys.ENTER && e.metaKey)) {
+ return;
+ }
+ let $form = this.$('form');
+ if ($form.length) {
+ $form.submit();
+ }
+ $form = null;
+ },
+
+ handleChange() {
+ this.set('codemirrorString', this.get('secretData').toJSONString(true));
+ this.hasDataChanges();
+ },
+
+ createOrUpdateKey(type, event) {
+ event.preventDefault();
+ const newData = this.get('secretData').toJSON();
+ this.get('key').set('secretData', newData);
+
+ // prevent from submitting if there's no key
+ // maybe do something fancier later
+ if (type === 'create' && Ember.isBlank(this.get('key.id'))) {
+ return;
+ }
+
+ this.persistKey(
+ 'save',
+ key => {
+ this.hasDataChanges();
+ this.transitionToRoute(SHOW_ROUTE, key);
+ },
+ type === 'create'
+ );
+ },
+
+ deleteKey() {
+ this.persistKey('destroyRecord', () => {
+ this.transitionToRoute(LIST_ROOT_ROUTE);
+ });
+ },
+
+ refresh() {
+ this.attrs.onRefresh();
+ },
+
+ addRow() {
+ const data = this.get('secretData');
+ if (Ember.isNone(data.findBy('name', ''))) {
+ data.pushObject({ name: '', value: '' });
+ this.set('codemirrorString', data.toJSONString(true));
+ }
+ this.checkRows();
+ this.hasDataChanges();
+ },
+
+ deleteRow(name) {
+ const data = this.get('secretData');
+ const item = data.findBy('name', name);
+ if (Ember.isBlank(item.name)) {
+ return;
+ }
+ data.removeObject(item);
+ this.checkRows();
+ this.hasDataChanges();
+ this.set('codemirrorString', data.toJSONString(true));
+ this.rerender();
+ },
+
+ toggleAdvanced(bool) {
+ this.sendAction('toggleAdvancedEdit', bool);
+ },
+
+ codemirrorUpdated(val, codemirror) {
+ codemirror.performLint();
+ const noErrors = codemirror.state.lint.marked.length === 0;
+ if (noErrors) {
+ this.get('secretData').fromJSONString(val);
+ }
+ this.set('hasLintError', !noErrors);
+ this.set('codemirrorString', val);
+ },
+
+ formatJSON() {
+ this.set('codemirrorString', this.get('secretData').toJSONString(true));
+ },
+ },
+});
diff --git a/ui/app/components/secret-form-header.js b/ui/app/components/secret-form-header.js
new file mode 100644
index 000000000..1a20cb820
--- /dev/null
+++ b/ui/app/components/secret-form-header.js
@@ -0,0 +1,49 @@
+import Ember from 'ember';
+import hbs from 'htmlbars-inline-precompile';
+
+export default Ember.Component.extend({
+ key: null,
+ mode: null,
+ path: null,
+ actionClass: null,
+
+ title: Ember.computed.alias('key.keyWithoutParent'),
+
+ layout: hbs`
+
`,
+});
diff --git a/ui/app/components/secret-link.js b/ui/app/components/secret-link.js
new file mode 100644
index 000000000..70fa30827
--- /dev/null
+++ b/ui/app/components/secret-link.js
@@ -0,0 +1,43 @@
+import Ember from 'ember';
+import hbs from 'htmlbars-inline-precompile';
+import { hrefTo } from 'vault/helpers/href-to';
+const { computed } = Ember;
+
+export function linkParams({ mode, secret, queryParams }) {
+ let params;
+ const route = `vault.cluster.secrets.backend.${mode}`;
+
+ if (!secret || secret === ' ') {
+ params = [route + '-root'];
+ } else {
+ params = [route, secret];
+ }
+
+ if (queryParams) {
+ params.push(queryParams);
+ }
+
+ return params;
+}
+
+export default Ember.Component.extend({
+ mode: 'list',
+
+ secret: null,
+ queryParams: null,
+ ariaLabel: null,
+
+ linkParams: computed('mode', 'secret', 'queryParams', function() {
+ return linkParams(this.getProperties('mode', 'secret', 'queryParams'));
+ }),
+
+ attributeBindings: ['href', 'aria-label:ariaLabel'],
+
+ href: computed('linkParams', function() {
+ return hrefTo(this, ...this.get('linkParams'));
+ }),
+
+ layout: hbs`{{yield}}`,
+
+ tagName: 'a',
+});
diff --git a/ui/app/components/section-tabs.js b/ui/app/components/section-tabs.js
new file mode 100644
index 000000000..c90cb5503
--- /dev/null
+++ b/ui/app/components/section-tabs.js
@@ -0,0 +1,14 @@
+import Ember from 'ember';
+
+const SectionTabs = Ember.Component.extend({
+ tagName: '',
+
+ model: null,
+ tabType: 'authSettings',
+});
+
+SectionTabs.reopenClass({
+ positionalParams: ['model', 'tabType'],
+});
+
+export default SectionTabs;
diff --git a/ui/app/components/shamir-flow.js b/ui/app/components/shamir-flow.js
new file mode 100644
index 000000000..5df2bc38d
--- /dev/null
+++ b/ui/app/components/shamir-flow.js
@@ -0,0 +1,142 @@
+import Ember from 'ember';
+
+const { Component, inject, computed, get } = Ember;
+const { camelize } = Ember.String;
+
+const DEFAULTS = {
+ key: null,
+ loading: false,
+ errors: [],
+ threshold: null,
+ progress: null,
+ started: false,
+ generateWithPGP: false,
+ pgpKeyFile: { value: '' },
+ nonce: '',
+};
+
+export default Component.extend(DEFAULTS, {
+ tagName: '',
+ store: inject.service(),
+ formText: null,
+ fetchOnInit: false,
+ buttonText: 'Submit',
+ thresholdPath: 'required',
+ generateAction: false,
+ encoded_token: null,
+
+ init() {
+ if (this.get('fetchOnInit')) {
+ this.attemptProgress();
+ }
+ return this._super(...arguments);
+ },
+
+ onShamirSuccess: _ => _,
+ // can be overridden w/an attr
+ isComplete(data) {
+ return data.complete === true;
+ },
+
+ stopLoading() {
+ this.setProperties({
+ loading: false,
+ errors: [],
+ key: null,
+ });
+ },
+
+ reset() {
+ this.setProperties(DEFAULTS);
+ },
+
+ hasProgress: computed.gt('progress', 0),
+
+ actionSuccess(resp) {
+ const { isComplete, onShamirSuccess, thresholdPath } = this.getProperties(
+ 'isComplete',
+ 'onShamirSuccess',
+ 'thresholdPath'
+ );
+ this.stopLoading();
+ this.set('threshold', get(resp, thresholdPath));
+ this.setProperties(resp);
+ if (isComplete(resp)) {
+ this.reset();
+ onShamirSuccess(resp);
+ }
+ },
+
+ actionError(e) {
+ this.stopLoading();
+ if (e.httpStatus === 400) {
+ this.set('errors', e.errors);
+ } else {
+ throw e;
+ }
+ },
+
+ extractData(data) {
+ const isGenerate = this.get('generateAction');
+ const hasStarted = this.get('started');
+ const usePGP = this.get('generateWithPGP');
+ const nonce = this.get('nonce');
+
+ if (!isGenerate || hasStarted) {
+ if (nonce) {
+ data.nonce = nonce;
+ }
+ return data;
+ }
+
+ if (usePGP) {
+ return {
+ pgp_key: data.pgp_key,
+ };
+ }
+
+ return {
+ otp: data.otp,
+ };
+ },
+
+ attemptProgress(data) {
+ const checkStatus = data ? false : true;
+ let action = this.get('action');
+ action = action && camelize(action);
+ this.set('loading', true);
+ const adapter = this.get('store').adapterFor('cluster');
+ const method = adapter[action];
+ method
+ .call(adapter, data, { checkStatus })
+ .then(resp => this.actionSuccess(resp), (...args) => this.actionError(...args));
+ },
+
+ actions: {
+ onSubmit(data) {
+ if (!data.key) {
+ return;
+ }
+ this.attemptProgress(this.extractData(data));
+ },
+
+ startGenerate(data) {
+ this.attemptProgress(this.extractData(data));
+ },
+
+ generateOTP() {
+ const bytes = new window.Uint8Array(16);
+ window.crypto.getRandomValues(bytes);
+ this.set('otp', base64js.fromByteArray(bytes));
+ },
+
+ setKey(_, keyFile) {
+ this.set('pgp_key', keyFile.value);
+ this.set('pgpKeyFile', keyFile);
+ },
+
+ clearToken() {
+ this.set('encoded_token', null);
+ },
+ },
+});
diff --git a/ui/app/components/shamir-progress.js b/ui/app/components/shamir-progress.js
new file mode 100644
index 000000000..c470cd0a1
--- /dev/null
+++ b/ui/app/components/shamir-progress.js
@@ -0,0 +1,14 @@
+import Ember from 'ember';
+
+export default Ember.Component.extend({
+ threshold: null,
+ progress: null,
+ classNames: ['shamir-progress'],
+ progressPercent: Ember.computed('threshold', 'progress', function() {
+ const { threshold, progress } = this.getProperties('threshold', 'progress');
+ if (threshold && progress) {
+ return progress / threshold * 100;
+ }
+ return 0;
+ }),
+});
diff --git a/ui/app/components/splash-page.js b/ui/app/components/splash-page.js
new file mode 100644
index 000000000..e3ac4fb5c
--- /dev/null
+++ b/ui/app/components/splash-page.js
@@ -0,0 +1,5 @@
+import Ember from 'ember';
+
+export default Ember.Component.extend({
+ tagName: '',
+});
diff --git a/ui/app/components/splash-page/splash-content.js b/ui/app/components/splash-page/splash-content.js
new file mode 100644
index 000000000..e3ac4fb5c
--- /dev/null
+++ b/ui/app/components/splash-page/splash-content.js
@@ -0,0 +1,5 @@
+import Ember from 'ember';
+
+export default Ember.Component.extend({
+ tagName: '',
+});
diff --git a/ui/app/components/splash-page/splash-footer.js b/ui/app/components/splash-page/splash-footer.js
new file mode 100644
index 000000000..e3ac4fb5c
--- /dev/null
+++ b/ui/app/components/splash-page/splash-footer.js
@@ -0,0 +1,5 @@
+import Ember from 'ember';
+
+export default Ember.Component.extend({
+ tagName: '',
+});
diff --git a/ui/app/components/splash-page/splash-header.js b/ui/app/components/splash-page/splash-header.js
new file mode 100644
index 000000000..e3ac4fb5c
--- /dev/null
+++ b/ui/app/components/splash-page/splash-header.js
@@ -0,0 +1,5 @@
+import Ember from 'ember';
+
+export default Ember.Component.extend({
+ tagName: '',
+});
diff --git a/ui/app/components/status-menu.js b/ui/app/components/status-menu.js
new file mode 100644
index 000000000..79928a285
--- /dev/null
+++ b/ui/app/components/status-menu.js
@@ -0,0 +1,21 @@
+import Ember from 'ember';
+
+const { inject, computed } = Ember;
+
+export default Ember.Component.extend({
+ currentCluster: inject.service('current-cluster'),
+ cluster: computed.alias('currentCluster.cluster'),
+ auth: inject.service(),
+ type: 'cluster',
+ partialName: computed('type', function() {
+ return `partials/status/${this.get('type')}`;
+ }),
+ glyphName: computed('type', function() {
+ const glyphs = {
+ cluster: 'unlocked',
+ user: 'android-person',
+ replication: 'replication',
+ };
+ return glyphs[this.get('type')];
+ }),
+});
diff --git a/ui/app/components/string-list.js b/ui/app/components/string-list.js
new file mode 100644
index 000000000..277200987
--- /dev/null
+++ b/ui/app/components/string-list.js
@@ -0,0 +1,130 @@
+import Ember from 'ember';
+
+const { computed, set } = Ember;
+
+export default Ember.Component.extend({
+ 'data-test-component': 'string-list',
+ classNames: ['field', 'string-list', 'form-section'],
+
+ /*
+ * @public
+ * @param String
+ *
+ * Optional - Text displayed in the header above all of the inputs
+ *
+ */
+ label: null,
+
+ /*
+ * @public
+ * @param Function
+ *
+ * Function called when any of the inputs change
+ * accepts a single param `value` that is the
+ * result of calling `toVal()`.
+ *
+ */
+ onChange: () => {},
+
+ /*
+ * @public
+ * @param String | Array
+ * A comma-separated string or an array of strings.
+ * Defaults to an empty array.
+ *
+ */
+ inputValue: [],
+
+ /*
+ *
+ * @public
+ * @param String - ['array'|'string]
+ *
+ * Optional type for `inputValue` - defaults to `'array'`
+ * Needs to match type of `inputValue` because it is set by the component on init.
+ *
+ */
+ type: 'array',
+
+ /*
+ *
+ * @private
+ * @param Ember.ArrayProxy
+ *
+ * mutable array that contains objects in the form of
+ * {
+ * value: 'somestring',
+ * }
+ *
+ * used to track the state of values bound to the various inputs
+ *
+ */
+ inputList: computed(function() {
+ return Ember.ArrayProxy.create({
+ content: [],
+ // trim the `value` when accessing objects
+ objectAtContent: function(idx) {
+ const obj = this.get('content').objectAt(idx);
+ if (obj && obj.value) {
+ set(obj, 'value', obj.value.trim());
+ }
+ return obj;
+ },
+ });
+ }),
+
+ init() {
+ this._super(...arguments);
+ this.setType();
+ this.toList();
+ this.send('addInput');
+ },
+
+ setType() {
+ const list = this.get('inputList');
+ if (!list) {
+ return;
+ }
+ this.set('type', typeof list);
+ },
+
+ toVal() {
+ const inputs = this.get('inputList').filter(x => x.value).mapBy('value');
+ if (this.get('format') === 'string') {
+ return inputs.join(',');
+ }
+ return inputs;
+ },
+
+ toList() {
+ let input = this.get('inputValue') || [];
+ const inputList = this.get('inputList');
+ if (typeof input === 'string') {
+ input = input.split(',');
+ }
+ inputList.addObjects(input.map(value => ({ value })));
+ },
+
+ actions: {
+ inputChanged(idx, val) {
+ const inputObj = this.get('inputList').objectAt(idx);
+ const onChange = this.get('onChange');
+ set(inputObj, 'value', val);
+ onChange(this.toVal());
+ },
+
+ addInput() {
+ const inputList = this.get('inputList');
+ if (inputList.get('lastObject.value') !== '') {
+ inputList.pushObject({ value: '' });
+ }
+ },
+
+ removeInput(idx) {
+ const onChange = this.get('onChange');
+ const inputs = this.get('inputList');
+ inputs.removeObject(inputs.objectAt(idx));
+ onChange(this.toVal());
+ },
+ },
+});
diff --git a/ui/app/components/text-file.js b/ui/app/components/text-file.js
new file mode 100644
index 000000000..eba0e4d6e
--- /dev/null
+++ b/ui/app/components/text-file.js
@@ -0,0 +1,85 @@
+import Ember from 'ember';
+
+const { set } = Ember;
+
+export default Ember.Component.extend({
+ 'data-test-component': 'text-file',
+ classNames: ['box', 'is-fullwidth', 'is-marginless', 'is-shadowless'],
+ classNameBindings: ['inputOnly:is-paddingless'],
+
+ /*
+ * @public
+ * @param Object
+ * Object in the shape of:
+ * {
+ * value: 'file contents here',
+ * fileName: 'nameOfFile.txt',
+ * enterAsText: bool
+ * }
+ */
+ file: null,
+
+ index: null,
+ onChange: () => {},
+
+ /*
+ * @public
+ * @param Boolean
+ * When true, only the file input will be rendered
+ */
+ inputOnly: false,
+
+ /*
+ * @public
+ * @param String
+ * Text to use as the label for the file input
+ * If null, a default will be rendered
+ */
+ label: null,
+
+ /*
+ * @public
+ * @param String
+ * Text to use as help under the file input
+ * If null, a default will be rendered
+ */
+ fileHelpText: 'Select a file from your computer',
+
+ /*
+ * @public
+ * @param String
+ * Text to use as help under the textarea in text-input mode
+ * If null, a default will be rendered
+ */
+ textareaHelpText: 'Enter the value as text',
+
+ readFile(file) {
+ const reader = new FileReader();
+ reader.onload = () => this.setFile(reader.result, file.name);
+ reader.readAsText(file);
+ },
+
+ setFile(contents, filename) {
+ this.get('onChange')(this.get('index'), { value: contents, fileName: filename });
+ },
+
+ actions: {
+ pickedFile(e) {
+ const { files } = e.target;
+ if (!files.length) {
+ return;
+ }
+ for (let i = 0, len = files.length; i < len; i++) {
+ this.readFile(files[i]);
+ }
+ },
+ updateData(e) {
+ const file = this.get('file');
+ set(file, 'value', e.target.value);
+ this.get('onChange')(this.get('index'), this.get('file'));
+ },
+ clearFile() {
+ this.get('onChange')(this.get('index'), { value: '' });
+ },
+ },
+});
diff --git a/ui/app/components/toggle-button.js b/ui/app/components/toggle-button.js
new file mode 100644
index 000000000..56c68413a
--- /dev/null
+++ b/ui/app/components/toggle-button.js
@@ -0,0 +1,34 @@
+import Ember from 'ember';
+
+const { get, set } = Ember;
+
+export default Ember.Component.extend({
+ tagName: 'button',
+ type: 'button',
+ toggleTarget: null,
+ toggleAttr: null,
+ classNameBindings: ['buttonClass'],
+ attributeBindings: ['type'],
+ buttonClass: 'has-text-info',
+ classNames: ['button', 'is-transparent'],
+ openLabel: 'Hide options',
+ closedLabel: 'More options',
+ init() {
+ this._super(...arguments);
+ const toggleAttr = this.get('toggleAttr');
+ Ember.defineProperty(
+ this,
+ 'isOpen',
+ Ember.computed(`toggleTarget.${toggleAttr}`, () => {
+ const props = this.getProperties('toggleTarget', 'toggleAttr');
+ return Ember.get(props.toggleTarget, props.toggleAttr);
+ })
+ );
+ },
+ click() {
+ const target = this.get('toggleTarget');
+ const attr = this.get('toggleAttr');
+ const current = get(target, attr);
+ set(target, attr, !current);
+ },
+});
diff --git a/ui/app/components/token-expire-warning.js b/ui/app/components/token-expire-warning.js
new file mode 100644
index 000000000..d17911572
--- /dev/null
+++ b/ui/app/components/token-expire-warning.js
@@ -0,0 +1,31 @@
+import Ember from 'ember';
+
+export default Ember.Component.extend({
+ auth: Ember.inject.service(),
+
+ routing: Ember.inject.service('-routing'),
+
+ transitionToRoute: function() {
+ var router = this.get('routing.router');
+ router.transitionTo.apply(router, arguments);
+ },
+
+ isDismissed: false,
+
+ actions: {
+ reauthenticate() {
+ this.get('auth').deleteCurrentToken();
+ this.transitionToRoute('vault.cluster');
+ },
+
+ renewToken() {
+ const auth = this.get('auth');
+ auth.renew();
+ auth.setLastFetch(Date.now());
+ },
+
+ dismiss() {
+ this.set('isDismissed', true);
+ },
+ },
+});
diff --git a/ui/app/components/tool-actions-form.js b/ui/app/components/tool-actions-form.js
new file mode 100644
index 000000000..c89cc4191
--- /dev/null
+++ b/ui/app/components/tool-actions-form.js
@@ -0,0 +1,134 @@
+import Ember from 'ember';
+import moment from 'moment';
+
+const { get, set, computed, setProperties } = Ember;
+
+const DEFAULTS = {
+ token: null,
+ rewrap_token: null,
+ errors: [],
+ wrap_info: null,
+ creation_time: null,
+ creation_ttl: null,
+ data: '{\n}',
+ unwrap_data: null,
+ wrapTTL: null,
+ sum: null,
+ random_bytes: null,
+ input: null,
+};
+
+const WRAPPING_ENDPOINTS = ['lookup', 'wrap', 'unwrap', 'rewrap'];
+
+export default Ember.Component.extend(DEFAULTS, {
+ store: Ember.inject.service(),
+ // putting these attrs here so they don't get reset when you click back
+ //random
+ bytes: 32,
+ //hash
+ format: 'base64',
+ algorithm: 'sha2-256',
+
+ tagName: '',
+
+ didReceiveAttrs() {
+ this._super(...arguments);
+ this.checkAction();
+ },
+
+ selectedAction: null,
+
+ reset() {
+ if (this.isDestroyed || this.isDestroying) {
+ return;
+ }
+ setProperties(this, DEFAULTS);
+ },
+
+ checkAction() {
+ const currentAction = get(this, 'selectedAction');
+ const oldAction = get(this, 'oldSelectedAction');
+
+ if (currentAction !== oldAction) {
+ this.reset();
+ }
+ set(this, 'oldSelectedAction', currentAction);
+ },
+
+ dataIsEmpty: computed.match('data', new RegExp(DEFAULTS.data)),
+
+ expirationDate: computed('creation_time', 'creation_ttl', function() {
+ const { creation_time, creation_ttl } = this.getProperties('creation_time', 'creation_ttl');
+ if (!(creation_time && creation_ttl)) {
+ return null;
+ }
+ return moment(creation_time).add(moment.duration(creation_ttl, 'seconds'));
+ }),
+
+ handleError(e) {
+ set(this, 'errors', e.errors);
+ },
+
+ handleSuccess(resp, action) {
+ let props = {};
+ if (resp && resp.data && action === 'unwrap') {
+ props = Ember.assign({}, props, { unwrap_data: resp.data });
+ }
+ props = Ember.assign({}, props, resp.data);
+
+ if (resp && resp.wrap_info) {
+ const keyName = action === 'rewrap' ? 'rewrap_token' : 'token';
+ props = Ember.assign({}, props, { [keyName]: resp.wrap_info.token });
+ }
+ setProperties(this, props);
+ },
+
+ getData() {
+ const action = get(this, 'selectedAction');
+ if (WRAPPING_ENDPOINTS.includes(action)) {
+ return get(this, 'dataIsEmpty')
+ ? { token: (get(this, 'token') || '').trim() }
+ : JSON.parse(get(this, 'data'));
+ }
+ if (action === 'random') {
+ return this.getProperties('bytes');
+ }
+
+ if (action === 'hash') {
+ return this.getProperties('input', 'format', 'algorithm');
+ }
+ },
+
+ actions: {
+ doSubmit(evt) {
+ evt.preventDefault();
+ const action = get(this, 'selectedAction');
+ const wrapTTL = action === 'wrap' ? get(this, 'wrapTTL') : null;
+ const data = this.getData();
+ setProperties(this, {
+ errors: null,
+ wrap_info: null,
+ creation_time: null,
+ creation_ttl: null,
+ });
+
+ get(this, 'store')
+ .adapterFor('tools')
+ .toolAction(action, data, { wrapTTL })
+ .then(resp => this.handleSuccess(resp, action), (...errArgs) => this.handleError(...errArgs));
+ },
+
+ onClear() {
+ this.reset();
+ },
+
+ codemirrorUpdated(val, codemirror) {
+ codemirror.performLint();
+ const hasErrors = codemirror.state.lint.marked.length > 0;
+ setProperties(this, {
+ buttonDisabled: hasErrors,
+ data: val,
+ });
+ },
+ },
+});
diff --git a/ui/app/components/tool-tip.js b/ui/app/components/tool-tip.js
new file mode 100644
index 000000000..6ea25baea
--- /dev/null
+++ b/ui/app/components/tool-tip.js
@@ -0,0 +1,6 @@
+import HoverDropdown from 'ember-basic-dropdown-hover/components/basic-dropdown-hover';
+
+export default HoverDropdown.extend({
+ delay: 0,
+ horizontalPosition: 'auto-right',
+});
diff --git a/ui/app/components/transit-edit.js b/ui/app/components/transit-edit.js
new file mode 100644
index 000000000..a4a0b69a6
--- /dev/null
+++ b/ui/app/components/transit-edit.js
@@ -0,0 +1,116 @@
+import Ember from 'ember';
+import FocusOnInsertMixin from 'vault/mixins/focus-on-insert';
+import keys from 'vault/lib/keycodes';
+
+const { get, set, computed } = Ember;
+const LIST_ROOT_ROUTE = 'vault.cluster.secrets.backend.list-root';
+const SHOW_ROUTE = 'vault.cluster.secrets.backend.show';
+
+export default Ember.Component.extend(FocusOnInsertMixin, {
+ mode: null,
+ onDataChange: null,
+ refresh: 'refresh',
+ key: null,
+ routing: Ember.inject.service('-routing'),
+ requestInFlight: computed.or('key.isLoading', 'key.isReloading', 'key.isSaving'),
+ willDestroyElement() {
+ const key = this.get('key');
+ if (get(key, 'isError')) {
+ key.rollbackAttributes();
+ }
+ },
+
+ transitionToRoute() {
+ const router = this.get('routing.router');
+ router.transitionTo.apply(router, arguments);
+ },
+
+ bindKeys: Ember.on('didInsertElement', function() {
+ Ember.$(document).on('keyup.keyEdit', this.onEscape.bind(this));
+ }),
+
+ unbindKeys: Ember.on('willDestroyElement', function() {
+ Ember.$(document).off('keyup.keyEdit');
+ }),
+
+ onEscape(e) {
+ if (e.keyCode !== keys.ESC || this.get('mode') !== 'show') {
+ return;
+ }
+ this.transitionToRoute(LIST_ROOT_ROUTE);
+ },
+
+ hasDataChanges() {
+ get(this, 'onDataChange')(get(this, 'key.hasDirtyAttributes'));
+ },
+
+ persistKey(method, successCallback) {
+ const key = get(this, 'key');
+ return key[method]().then(result => {
+ if (!Ember.get(result, 'didError')) {
+ successCallback(key);
+ }
+ });
+ },
+
+ actions: {
+ handleKeyDown(_, e) {
+ e.stopPropagation();
+ if (!(e.keyCode === keys.ENTER && e.metaKey)) {
+ return;
+ }
+ let $form = this.$('form');
+ if ($form.length) {
+ $form.submit();
+ }
+ $form = null;
+ },
+
+ createOrUpdateKey(type, event) {
+ event.preventDefault();
+
+ const keyId = this.get('key.id');
+ // prevent from submitting if there's no key
+ // maybe do something fancier later
+ if (type === 'create' && Ember.isBlank(keyId)) {
+ return;
+ }
+
+ this.persistKey(
+ 'save',
+ () => {
+ this.hasDataChanges();
+ this.transitionToRoute(SHOW_ROUTE, keyId);
+ },
+ type === 'create'
+ );
+ },
+
+ handleChange() {
+ this.hasDataChanges();
+ },
+
+ setValueOnKey(key, event) {
+ set(get(this, 'key'), key, event.target.checked);
+ },
+
+ derivedChange(val) {
+ get(this, 'key').setDerived(val);
+ },
+
+ convergentEncryptionChange(val) {
+ get(this, 'key').setConvergentEncryption(val);
+ },
+
+ refresh() {
+ this.sendAction('refresh');
+ },
+
+ deleteKey() {
+ this.persistKey('destroyRecord', () => {
+ this.hasDataChanges();
+ this.transitionToRoute(LIST_ROOT_ROUTE);
+ });
+ },
+ },
+});
diff --git a/ui/app/components/transit-key-actions.js b/ui/app/components/transit-key-actions.js
new file mode 100644
index 000000000..b0a022672
--- /dev/null
+++ b/ui/app/components/transit-key-actions.js
@@ -0,0 +1,181 @@
+import Ember from 'ember';
+const { get, set } = Ember;
+
+const TRANSIT_PARAMS = {
+ algorithm: 'sha2-256',
+ bits: 256,
+ bytes: 32,
+ ciphertext: null,
+ context: null,
+ format: 'base64',
+ hmac: null,
+ input: null,
+ key_version: 0,
+ keys: null,
+ nonce: null,
+ param: 'wrapped',
+ prehashed: false,
+ plaintext: null,
+ random_bytes: null,
+ signature: null,
+ sum: null,
+ exportKeyType: null,
+ exportKeyVersion: null,
+ wrappedToken: null,
+ valid: null,
+ plaintextOriginal: null,
+ didDecode: false,
+};
+const PARAMS_FOR_ACTION = {
+ sign: ['input', 'algorithm', 'key_version', 'prehashed'],
+ verify: ['input', 'hmac', 'signature', 'algorithm', 'prehashed'],
+ hmac: ['input', 'algorithm', 'key_version'],
+ encrypt: ['plaintext', 'context', 'nonce', 'key_version'],
+ decrypt: ['ciphertext', 'context', 'nonce'],
+ rewrap: ['ciphertext', 'context', 'nonce', 'key_version'],
+};
+export default Ember.Component.extend(TRANSIT_PARAMS, {
+ store: Ember.inject.service(),
+
+ // public attrs
+ selectedAction: null,
+ key: null,
+
+ refresh: 'refresh',
+
+ init() {
+ this._super(...arguments);
+ if (get(this, 'selectedAction')) {
+ return;
+ }
+ set(this, 'selectedAction', get(this, 'key.supportedActions.firstObject'));
+ Ember.assert('`key` is required for `' + this.toString() + '`.', this.getModelInfo());
+ },
+
+ didReceiveAttrs() {
+ this._super(...arguments);
+ this.checkAction();
+ if (get(this, 'selectedAction') === 'export') {
+ this.setExportKeyDefaults();
+ }
+ },
+
+ setExportKeyDefaults() {
+ const exportKeyType = get(this, 'key.exportKeyTypes.firstObject');
+ const exportKeyVersion = get(this, 'key.validKeyVersions.lastObject');
+ this.setProperties({
+ exportKeyType,
+ exportKeyVersion,
+ });
+ },
+
+ getModelInfo() {
+ const model = get(this, 'key') || get(this, 'backend');
+ if (!model) {
+ return null;
+ }
+ const backend = get(model, 'backend') || get(model, 'id');
+ const id = get(model, 'id');
+
+ return {
+ backend,
+ id,
+ };
+ },
+
+ checkAction() {
+ const currentAction = get(this, 'selectedAction');
+ const oldAction = get(this, 'oldSelectedAction');
+
+ this.resetParams(oldAction, currentAction);
+ set(this, 'oldSelectedAction', currentAction);
+ },
+
+ resetParams(oldAction, action) {
+ let params = Ember.copy(TRANSIT_PARAMS);
+ let paramsToKeep;
+ let clearWithoutCheck =
+ !oldAction ||
+ // don't save values from datakey
+ oldAction === 'datakey' ||
+ // can rewrap signatures — using that as a ciphertext later would be problematic
+ (oldAction === 'rewrap' && !get(this, 'key.supportsEncryption'));
+
+ if (!clearWithoutCheck && action) {
+ paramsToKeep = PARAMS_FOR_ACTION[action];
+ }
+
+ if (paramsToKeep) {
+ paramsToKeep.forEach(param => delete params[param]);
+ }
+ //resets params still left in the object to defaults
+ this.setProperties(params);
+ if (action === 'export') {
+ this.setExportKeyDefaults();
+ }
+ },
+
+ handleError(e) {
+ this.set('errors', e.errors);
+ },
+
+ handleSuccess(resp, options, action) {
+ let props = {};
+ if (resp && resp.data) {
+ if (action === 'export' && resp.data.keys) {
+ const { keys, type, name } = resp.data;
+ resp.data.keys = { keys, type, name };
+ }
+ props = Ember.assign({}, props, resp.data);
+ }
+ if (options.wrapTTL) {
+ props = Ember.assign({}, props, { wrappedToken: resp.wrap_info.token });
+ }
+ this.setProperties(props);
+ if (action === 'rotate') {
+ this.sendAction('refresh');
+ }
+ },
+
+ compactData(data) {
+ return Object.keys(data).reduce((result, key) => {
+ if (data[key]) {
+ result[key] = data[key];
+ }
+ return result;
+ }, {});
+ },
+
+ actions: {
+ onActionChange(action) {
+ set(this, 'selectedAction', action);
+ this.checkAction();
+ },
+
+ onClear() {
+ this.resetParams(null, get(this, 'selectedAction'));
+ },
+
+ clearParams(params) {
+ const arr = Array.isArray(params) ? params : [params];
+ arr.forEach(param => this.set(param, null));
+ },
+
+ doSubmit(data, options = {}) {
+ const { backend, id } = this.getModelInfo();
+ const action = this.get('selectedAction');
+ let payload = data ? this.compactData(data) : null;
+ this.setProperties({
+ errors: null,
+ result: null,
+ });
+ this.get('store')
+ .adapterFor('transit-key')
+ .keyAction(action, { backend, id, payload }, options)
+ .then(
+ resp => this.handleSuccess(resp, options, action),
+ (...errArgs) => this.handleError(...errArgs)
+ );
+ },
+ },
+});
diff --git a/ui/app/components/ttl-picker.js b/ui/app/components/ttl-picker.js
new file mode 100644
index 000000000..c903ad8f4
--- /dev/null
+++ b/ui/app/components/ttl-picker.js
@@ -0,0 +1,79 @@
+import Ember from 'ember';
+
+const { computed, get, set } = Ember;
+
+export default Ember.Component.extend({
+ 'data-test-component': 'ttl-picker',
+ classNames: 'field',
+ setDefaultValue: true,
+ onChange: () => {},
+ labelText: 'TTL',
+ labelClass: '',
+ time: 30,
+ unit: 'm',
+ initialValue: null,
+ unitOptions: [
+ { label: 'seconds', value: 's' },
+ { label: 'minutes', value: 'm' },
+ { label: 'hours', value: 'h' },
+ { label: 'days', value: 'd' },
+ ],
+
+ ouputSeconds: false,
+
+ convertToSeconds(time, unit) {
+ const toSeconds = {
+ s: 1,
+ m: 60,
+ h: 3600,
+ };
+
+ return time * toSeconds[unit];
+ },
+
+ TTL: computed('time', 'unit', function() {
+ let { time, unit, outputSeconds } = this.getProperties('time', 'unit', 'outputSeconds');
+ //convert to hours
+ if (unit === 'd') {
+ time = time * 24;
+ unit = 'h';
+ }
+ const timeString = time + unit;
+ return outputSeconds ? this.convertToSeconds(time, unit) : timeString;
+ }),
+
+ didRender() {
+ this._super(...arguments);
+ if (get(this, 'setDefaultValue') === false) {
+ return;
+ }
+ get(this, 'onChange')(get(this, 'TTL'));
+ },
+
+ init() {
+ this._super(...arguments);
+ if (!get(this, 'onChange')) {
+ throw new Ember.Error('`onChange` handler is a required attr in `' + this.toString() + '`.');
+ }
+ if (get(this, 'initialValue')) {
+ this.parseAndSetTime();
+ }
+ },
+
+ parseAndSetTime() {
+ const value = get(this, 'initialValue');
+ const seconds = Ember.typeOf(value) === 'number' ? value : Duration.parse(value).seconds();
+
+ this.set('time', seconds);
+ this.set('unit', 's');
+ },
+
+ actions: {
+ changedValue(key, event) {
+ const { type, value, checked } = event.target;
+ const val = type === 'checkbox' ? checked : value;
+ set(this, key, val);
+ get(this, 'onChange')(get(this, 'TTL'));
+ },
+ },
+});
diff --git a/ui/app/components/upgrade-link.js b/ui/app/components/upgrade-link.js
new file mode 100644
index 000000000..979838c68
--- /dev/null
+++ b/ui/app/components/upgrade-link.js
@@ -0,0 +1,29 @@
+import Ember from 'ember';
+
+export default Ember.Component.extend({
+ isAnimated: false,
+ isActive: false,
+ tagName: 'span',
+ actions: {
+ openOverlay() {
+ this.set('isActive', true);
+ Ember.run.later(
+ this,
+ function() {
+ this.set('isAnimated', true);
+ },
+ 10
+ );
+ },
+ closeOverlay() {
+ this.set('isAnimated', false);
+ Ember.run.later(
+ this,
+ function() {
+ this.set('isActive', false);
+ },
+ 300
+ );
+ },
+ },
+});
diff --git a/ui/app/components/upgrade-page.js b/ui/app/components/upgrade-page.js
new file mode 100644
index 000000000..590b55dfd
--- /dev/null
+++ b/ui/app/components/upgrade-page.js
@@ -0,0 +1,12 @@
+import Ember from 'ember';
+
+const { computed } = Ember;
+
+export default Ember.Component.extend({
+ title: 'Vault Enterprise',
+ featureName: computed('title', function() {
+ let title = this.get('title');
+ return title === 'Vault Enterprise' ? 'This' : title;
+ }),
+ minimumEdition: 'Vault Enterprise',
+});
diff --git a/ui/app/components/wrap-ttl.js b/ui/app/components/wrap-ttl.js
new file mode 100644
index 000000000..7e7402a3e
--- /dev/null
+++ b/ui/app/components/wrap-ttl.js
@@ -0,0 +1,60 @@
+import Ember from 'ember';
+import hbs from 'htmlbars-inline-precompile';
+
+const { computed, get, set } = Ember;
+
+export default Ember.Component.extend({
+ // passed from outside
+ onChange: null,
+ wrapResponse: true,
+
+ ttl: null,
+
+ wrapTTL: computed('wrapResponse', 'ttl', function() {
+ const { wrapResponse, ttl } = this.getProperties('wrapResponse', 'ttl');
+ return wrapResponse ? ttl : null;
+ }),
+
+ didRender() {
+ this._super(...arguments);
+ get(this, 'onChange')(get(this, 'wrapTTL'));
+ },
+
+ init() {
+ this._super(...arguments);
+ Ember.assert(
+ '`onChange` handler is a required attr in `' + this.toString() + '`.',
+ get(this, 'onChange')
+ );
+ },
+
+ layout: hbs`
+
+
+
+
+ Wrap response
+
+
+ {{#if wrapResponse}}
+ {{ttl-picker data-test-wrap-ttl-picker=true labelText='Wrap TTL' onChange=(action (mut ttl))}}
+ {{/if}}
+
+ `,
+
+ actions: {
+ changedValue(key, event) {
+ const { type, value, checked } = event.target;
+ const val = type === 'checkbox' ? checked : value;
+ set(this, key, val);
+ get(this, 'onChange')(get(this, 'wrapTTL'));
+ },
+ },
+});
diff --git a/ui/app/controllers/.gitkeep b/ui/app/controllers/.gitkeep
new file mode 100644
index 000000000..e69de29bb
diff --git a/ui/app/controllers/application.js b/ui/app/controllers/application.js
new file mode 100644
index 000000000..5f6f6c9a9
--- /dev/null
+++ b/ui/app/controllers/application.js
@@ -0,0 +1,33 @@
+import Ember from 'ember';
+import config from '../config/environment';
+
+export default Ember.Controller.extend({
+ env: config.environment,
+ auth: Ember.inject.service(),
+ vaultVersion: Ember.inject.service('version'),
+ activeCluster: Ember.computed('auth.activeCluster', function() {
+ return this.store.peekRecord('cluster', this.get('auth.activeCluster'));
+ }),
+ activeClusterName: Ember.computed('auth.activeCluster', function() {
+ const activeCluster = this.store.peekRecord('cluster', this.get('auth.activeCluster'));
+ return activeCluster ? activeCluster.get('name') : null;
+ }),
+ showNav: Ember.computed(
+ 'activeClusterName',
+ 'auth.currentToken',
+ 'activeCluster.dr.isSecondary',
+ 'activeCluster.{needsInit,sealed}',
+ function() {
+ if (
+ this.get('activeCluster.dr.isSecondary') ||
+ this.get('activeCluster.needsInit') ||
+ this.get('activeCluster.sealed')
+ ) {
+ return false;
+ }
+ if (this.get('activeClusterName') && this.get('auth.currentToken')) {
+ return true;
+ }
+ }
+ ),
+});
diff --git a/ui/app/controllers/vault/cluster.js b/ui/app/controllers/vault/cluster.js
new file mode 100644
index 000000000..141426150
--- /dev/null
+++ b/ui/app/controllers/vault/cluster.js
@@ -0,0 +1,8 @@
+import Ember from 'ember';
+
+const { inject, Controller } = Ember;
+
+export default Controller.extend({
+ auth: inject.service(),
+ version: inject.service(),
+});
diff --git a/ui/app/controllers/vault/cluster/access/identity/aliases/add.js b/ui/app/controllers/vault/cluster/access/identity/aliases/add.js
new file mode 100644
index 000000000..e22b4416a
--- /dev/null
+++ b/ui/app/controllers/vault/cluster/access/identity/aliases/add.js
@@ -0,0 +1,5 @@
+import CreateController from '../create';
+
+export default CreateController.extend({
+ showRoute: 'vault.cluster.access.identity.aliases.show',
+});
diff --git a/ui/app/controllers/vault/cluster/access/identity/aliases/edit.js b/ui/app/controllers/vault/cluster/access/identity/aliases/edit.js
new file mode 100644
index 000000000..dc06915f0
--- /dev/null
+++ b/ui/app/controllers/vault/cluster/access/identity/aliases/edit.js
@@ -0,0 +1,3 @@
+import CreateController from './add';
+
+export default CreateController.extend();
diff --git a/ui/app/controllers/vault/cluster/access/identity/aliases/index.js b/ui/app/controllers/vault/cluster/access/identity/aliases/index.js
new file mode 100644
index 000000000..fd2e72663
--- /dev/null
+++ b/ui/app/controllers/vault/cluster/access/identity/aliases/index.js
@@ -0,0 +1,4 @@
+import Ember from 'ember';
+import ListController from 'vault/mixins/list-controller';
+
+export default Ember.Controller.extend(ListController);
diff --git a/ui/app/controllers/vault/cluster/access/identity/create.js b/ui/app/controllers/vault/cluster/access/identity/create.js
new file mode 100644
index 000000000..46d9d6437
--- /dev/null
+++ b/ui/app/controllers/vault/cluster/access/identity/create.js
@@ -0,0 +1,10 @@
+import Ember from 'ember';
+import { task } from 'ember-concurrency';
+
+export default Ember.Controller.extend({
+ showRoute: 'vault.cluster.access.identity.show',
+ showTab: 'details',
+ navToShow: task(function*(model) {
+ yield this.transitionToRoute(this.get('showRoute'), model.id, this.get('showTab'));
+ }),
+});
diff --git a/ui/app/controllers/vault/cluster/access/identity/edit.js b/ui/app/controllers/vault/cluster/access/identity/edit.js
new file mode 100644
index 000000000..43916a858
--- /dev/null
+++ b/ui/app/controllers/vault/cluster/access/identity/edit.js
@@ -0,0 +1,3 @@
+import CreateController from './create';
+
+export default CreateController.extend();
diff --git a/ui/app/controllers/vault/cluster/access/identity/index.js b/ui/app/controllers/vault/cluster/access/identity/index.js
new file mode 100644
index 000000000..fd2e72663
--- /dev/null
+++ b/ui/app/controllers/vault/cluster/access/identity/index.js
@@ -0,0 +1,4 @@
+import Ember from 'ember';
+import ListController from 'vault/mixins/list-controller';
+
+export default Ember.Controller.extend(ListController);
diff --git a/ui/app/controllers/vault/cluster/access/identity/merge.js b/ui/app/controllers/vault/cluster/access/identity/merge.js
new file mode 100644
index 000000000..43916a858
--- /dev/null
+++ b/ui/app/controllers/vault/cluster/access/identity/merge.js
@@ -0,0 +1,3 @@
+import CreateController from './create';
+
+export default CreateController.extend();
diff --git a/ui/app/controllers/vault/cluster/access/leases/index.js b/ui/app/controllers/vault/cluster/access/leases/index.js
new file mode 100644
index 000000000..cbe007a32
--- /dev/null
+++ b/ui/app/controllers/vault/cluster/access/leases/index.js
@@ -0,0 +1,9 @@
+import Ember from 'ember';
+
+export default Ember.Controller.extend({
+ actions: {
+ lookupLease(id) {
+ this.transitionToRoute('vault.cluster.access.leases.show', id);
+ },
+ },
+});
diff --git a/ui/app/controllers/vault/cluster/access/leases/list-root.js b/ui/app/controllers/vault/cluster/access/leases/list-root.js
new file mode 100644
index 000000000..ea1fedd0a
--- /dev/null
+++ b/ui/app/controllers/vault/cluster/access/leases/list-root.js
@@ -0,0 +1 @@
+export { default } from './list';
diff --git a/ui/app/controllers/vault/cluster/access/leases/list.js b/ui/app/controllers/vault/cluster/access/leases/list.js
new file mode 100644
index 000000000..955192bbc
--- /dev/null
+++ b/ui/app/controllers/vault/cluster/access/leases/list.js
@@ -0,0 +1,77 @@
+import Ember from 'ember';
+import utils from 'vault/lib/key-utils';
+
+export default Ember.Controller.extend({
+ flashMessages: Ember.inject.service(),
+ clusterController: Ember.inject.controller('vault.cluster'),
+ queryParams: {
+ page: 'page',
+ pageFilter: 'pageFilter',
+ },
+
+ page: 1,
+ pageFilter: null,
+ filter: null,
+
+ backendCrumb: Ember.computed(function() {
+ return {
+ label: 'leases',
+ text: 'leases',
+ path: 'vault.cluster.access.leases.list-root',
+ model: this.get('clusterController.model.name'),
+ };
+ }),
+
+ isLoading: false,
+
+ filterMatchesKey: Ember.computed('filter', 'model', 'model.[]', function() {
+ var filter = this.get('filter');
+ var content = this.get('model');
+ return !!(content.length && content.findBy('id', filter));
+ }),
+
+ firstPartialMatch: Ember.computed('filter', 'model', 'model.[]', 'filterMatchesKey', function() {
+ var filter = this.get('filter');
+ var content = this.get('model');
+ var filterMatchesKey = this.get('filterMatchesKey');
+ var re = new RegExp('^' + filter);
+ return filterMatchesKey
+ ? null
+ : content.find(function(key) {
+ return re.test(key.get('id'));
+ });
+ }),
+
+ filterIsFolder: Ember.computed('filter', function() {
+ return !!utils.keyIsFolder(this.get('filter'));
+ }),
+
+ actions: {
+ setFilter(val) {
+ this.set('filter', val);
+ },
+
+ setFilterFocus(bool) {
+ this.set('filterFocused', bool);
+ },
+
+ revokePrefix(prefix, isForce) {
+ const adapter = this.model.store.adapterFor('lease');
+ const method = isForce ? 'forceRevokePrefix' : 'revokePrefix';
+ const fn = adapter[method];
+ fn
+ .call(adapter, prefix)
+ .then(() => {
+ return this.transitionToRoute('vault.cluster.access.leases.list-root').then(() => {
+ this.get('flashMessages').success(`All of the leases under ${prefix} will be revoked.`);
+ });
+ })
+ .catch(e => {
+ const errString = e.errors.join('.');
+ this.get('flashMessages').danger(
+ `There was an error attempting to revoke the prefix: ${prefix}. ${errString}.`
+ );
+ });
+ },
+ },
+});
diff --git a/ui/app/controllers/vault/cluster/access/leases/show.js b/ui/app/controllers/vault/cluster/access/leases/show.js
new file mode 100644
index 000000000..74bff6913
--- /dev/null
+++ b/ui/app/controllers/vault/cluster/access/leases/show.js
@@ -0,0 +1,42 @@
+import Ember from 'ember';
+
+export default Ember.Controller.extend({
+ clusterController: Ember.inject.controller('vault.cluster'),
+
+ backendCrumb: Ember.computed(function() {
+ return {
+ label: 'leases',
+ text: 'leases',
+ path: 'vault.cluster.access.leases.list-root',
+ model: this.get('clusterController.model.name'),
+ };
+ }),
+
+ flashMessages: Ember.inject.service(),
+
+ actions: {
+ revokeLease(model) {
+ return model.destroyRecord().then(() => {
+ return this.transitionToRoute('vault.cluster.access.leases.list-root');
+ });
+ },
+
+ renewLease(model, interval) {
+ const adapter = model.store.adapterFor('lease');
+ const flash = this.get('flashMessages');
+ adapter
+ .renew(model.id, interval)
+ .then(() => {
+ this.send('refreshModel');
+ // lol this is terrible, but there's no way to get the promise from the route refresh
+ Ember.run.next(() => {
+ flash.success(`The lease ${model.id} was successfully renewed.`);
+ });
+ })
+ .catch(e => {
+ const errString = e.errors.join('.');
+ flash.danger(`There was an error renewing the lease: ${errString}`);
+ });
+ },
+ },
+});
diff --git a/ui/app/controllers/vault/cluster/access/methods.js b/ui/app/controllers/vault/cluster/access/methods.js
new file mode 100644
index 000000000..51ba59bb8
--- /dev/null
+++ b/ui/app/controllers/vault/cluster/access/methods.js
@@ -0,0 +1,25 @@
+import Ember from 'ember';
+import { task } from 'ember-concurrency';
+
+export default Ember.Controller.extend({
+ queryParams: {
+ page: 'page',
+ pageFilter: 'pageFilter',
+ },
+
+ page: 1,
+ pageFilter: null,
+ filter: null,
+
+ disableMethod: task(function*(method) {
+ const { type, path } = method.getProperties('type', 'path');
+ try {
+ yield method.destroyRecord();
+ this.get('flashMessages').success(`The ${type} auth method at ${path} has been disabled.`);
+ } catch (err) {
+ this.get('flashMessages').danger(
+ `There was an error disabling auth method at ${path}: ${err.errors.join(' ')}.`
+ );
+ }
+ }).drop(),
+});
diff --git a/ui/app/controllers/vault/cluster/auth.js b/ui/app/controllers/vault/cluster/auth.js
new file mode 100644
index 000000000..6cc7ac0af
--- /dev/null
+++ b/ui/app/controllers/vault/cluster/auth.js
@@ -0,0 +1,11 @@
+import Ember from 'ember';
+import { supportedAuthBackends } from 'vault/helpers/supported-auth-backends';
+
+export default Ember.Controller.extend({
+ queryParams: ['with'],
+ with: Ember.computed(function() {
+ return supportedAuthBackends()[0].type;
+ }),
+
+ redirectTo: null,
+});
diff --git a/ui/app/controllers/vault/cluster/init.js b/ui/app/controllers/vault/cluster/init.js
new file mode 100644
index 000000000..c04dd306d
--- /dev/null
+++ b/ui/app/controllers/vault/cluster/init.js
@@ -0,0 +1,71 @@
+import Ember from 'ember';
+
+const DEFAULTS = {
+ keyData: null,
+ secret_shares: null,
+ secret_threshold: null,
+ pgp_keys: null,
+ use_pgp: false,
+ loading: false,
+};
+
+export default Ember.Controller.extend(DEFAULTS, {
+ reset() {
+ this.setProperties(DEFAULTS);
+ },
+
+ initSuccess(resp) {
+ this.set('loading', false);
+ this.set('keyData', resp);
+ },
+
+ initError(e) {
+ this.set('loading', false);
+ if (e.httpStatus === 400) {
+ this.set('errors', e.errors);
+ } else {
+ throw e;
+ }
+ },
+
+ keyFilename: Ember.computed('model.name', function() {
+ return `vault-cluster-${this.get('model.name')}`;
+ }),
+
+ actions: {
+ initCluster(data) {
+ if (data.secret_shares) {
+ data.secret_shares = parseInt(data.secret_shares);
+ }
+ if (data.secret_threshold) {
+ data.secret_threshold = parseInt(data.secret_threshold);
+ }
+ if (!data.use_pgp) {
+ delete data.pgp_keys;
+ }
+ if (!data.use_pgp_for_root) {
+ delete data.root_token_pgp_key;
+ }
+
+ delete data.use_pgp;
+ delete data.use_pgp_for_root;
+ const store = this.model.store;
+ this.setProperties({
+ loading: true,
+ errors: null,
+ });
+ store
+ .adapterFor('cluster')
+ .initCluster(data)
+ .then(resp => this.initSuccess(resp), (...errArgs) => this.initError(...errArgs));
+ },
+
+ setKeys(data) {
+ this.set('pgp_keys', data);
+ },
+
+ setRootKey([key]) {
+ this.set('root_token_pgp_key', key);
+ },
+ },
+});
diff --git a/ui/app/controllers/vault/cluster/policies/create.js b/ui/app/controllers/vault/cluster/policies/create.js
new file mode 100644
index 000000000..537d0d022
--- /dev/null
+++ b/ui/app/controllers/vault/cluster/policies/create.js
@@ -0,0 +1,19 @@
+import Ember from 'ember';
+import PolicyEditController from 'vault/mixins/policy-edit-controller';
+
+export default Ember.Controller.extend(PolicyEditController, {
+ showFileUpload: false,
+ file: null,
+
+ actions: {
+ setPolicyFromFile(index, fileInfo) {
+ let { value, fileName } = fileInfo;
+ let model = this.get('model');
+ model.set('policy', value);
+ if (!model.get('name')) {
+ model.set('name', fileName);
+ }
+ this.set('showFileUpload', false);
+ },
+ },
+});
diff --git a/ui/app/controllers/vault/cluster/policies/index.js b/ui/app/controllers/vault/cluster/policies/index.js
new file mode 100644
index 000000000..8d02e0ad3
--- /dev/null
+++ b/ui/app/controllers/vault/cluster/policies/index.js
@@ -0,0 +1,68 @@
+import Ember from 'ember';
+let { inject } = Ember;
+
+export default Ember.Controller.extend({
+ flashMessages: inject.service(),
+
+ queryParams: {
+ page: 'page',
+ pageFilter: 'pageFilter',
+ },
+
+ filter: null,
+ page: 1,
+ pageFilter: null,
+
+ filterFocused: false,
+
+ // set via the route `loading` action
+ isLoading: false,
+
+ filterMatchesKey: Ember.computed('filter', 'model', 'model.[]', function() {
+ var filter = this.get('filter');
+ var content = this.get('model');
+ return !!(content && content.length && content.findBy('id', filter));
+ }),
+
+ firstPartialMatch: Ember.computed('filter', 'model', 'model.[]', 'filterMatchesKey', function() {
+ var filter = this.get('filter');
+ var content = this.get('model');
+ if (!content) {
+ return;
+ }
+ var filterMatchesKey = this.get('filterMatchesKey');
+ var re = new RegExp('^' + filter);
+ return filterMatchesKey
+ ? null
+ : content.find(function(key) {
+ return re.test(key.get('id'));
+ });
+ }),
+
+ actions: {
+ setFilter: function(val) {
+ this.set('filter', val);
+ },
+ setFilterFocus: function(bool) {
+ this.set('filterFocused', bool);
+ },
+ deletePolicy(model) {
+ let policyType = model.get('policyType');
+ let name = model.id;
+ let flash = this.get('flashMessages');
+ model
+ .destroyRecord()
+ .then(() => {
+ flash.success(`${policyType.toUpperCase()} policy "${name}" was successfully deleted.`);
+ // this will clear the dataset cache on the store
+ this.send('willTransition');
+ })
+ .catch(e => {
+ let errors = e.errors ? e.errors.join('') : e.message;
+ flash.danger(
+ `There was an error deleting the ${policyType.toUpperCase()} policy "${name}": ${errors}.`
+ );
+ });
+ },
+ },
+});
diff --git a/ui/app/controllers/vault/cluster/policy/edit.js b/ui/app/controllers/vault/cluster/policy/edit.js
new file mode 100644
index 000000000..93c6cefe0
--- /dev/null
+++ b/ui/app/controllers/vault/cluster/policy/edit.js
@@ -0,0 +1,4 @@
+import Ember from 'ember';
+import PolicyEditController from 'vault/mixins/policy-edit-controller';
+
+export default Ember.Controller.extend(PolicyEditController);
diff --git a/ui/app/controllers/vault/cluster/replication-dr-promote.js b/ui/app/controllers/vault/cluster/replication-dr-promote.js
new file mode 100644
index 000000000..578007d98
--- /dev/null
+++ b/ui/app/controllers/vault/cluster/replication-dr-promote.js
@@ -0,0 +1,6 @@
+import Ember from 'ember';
+
+export default Ember.Controller.extend({
+ queryParams: ['action'],
+ action: '',
+});
diff --git a/ui/app/controllers/vault/cluster/replication.js b/ui/app/controllers/vault/cluster/replication.js
new file mode 100644
index 000000000..80a7e9d40
--- /dev/null
+++ b/ui/app/controllers/vault/cluster/replication.js
@@ -0,0 +1,126 @@
+import Ember from 'ember';
+
+const DEFAULTS = {
+ token: null,
+ id: null,
+ loading: false,
+ errors: [],
+ showFilterConfig: false,
+ primary_api_addr: null,
+ primary_cluster_addr: null,
+ filterConfig: {
+ mode: 'whitelist',
+ paths: [],
+ },
+};
+
+export default Ember.Controller.extend(DEFAULTS, {
+ store: Ember.inject.service(),
+ rm: Ember.inject.service('replication-mode'),
+ replicationMode: Ember.computed.alias('rm.mode'),
+
+ submitError(e) {
+ if (e.errors) {
+ this.set('errors', e.errors);
+ } else {
+ throw e;
+ }
+ },
+
+ saveFilterConfig() {
+ const config = this.get('filterConfig');
+ const id = this.get('id');
+ config.id = id;
+ const configRecord = this.get('store').createRecord('mount-filter-config', config);
+ return configRecord.save().catch(e => this.submitError(e));
+ },
+
+ reset() {
+ this.setProperties(DEFAULTS);
+ },
+
+ submitSuccess(resp, action) {
+ const cluster = this.get('model');
+ const store = this.get('store');
+ if (!cluster) {
+ return;
+ }
+
+ if (resp && resp.wrap_info) {
+ this.set('token', resp.wrap_info.token);
+ }
+ if (action === 'secondary-token') {
+ this.setProperties({
+ loading: false,
+ primary_api_addr: null,
+ primary_cluster_addr: null,
+ });
+ return cluster;
+ }
+ this.reset();
+ return store
+ .adapterFor('cluster')
+ .replicationStatus()
+ .then(status => {
+ return store.pushPayload('cluster', status);
+ })
+ .finally(() => {
+ this.set('loading', false);
+ });
+ },
+
+ submitHandler(action, clusterMode, data, event) {
+ const replicationMode = this.get('replicationMode');
+ let saveFilterConfig;
+ if (event && event.preventDefault) {
+ event.preventDefault();
+ }
+ if (data && Ember.isPresent(data.saveFilterConfig)) {
+ saveFilterConfig = data.saveFilterConfig;
+ delete data.saveFilterConfig;
+ }
+ this.setProperties({
+ loading: true,
+ errors: [],
+ });
+ if (data) {
+ data = Object.keys(data).reduce((newData, key) => {
+ var val = data[key];
+ if (Ember.isPresent(val)) {
+ newData[key] = val;
+ }
+ return newData;
+ }, {});
+ }
+
+ return this.get('store')
+ .adapterFor('cluster')
+ .replicationAction(action, replicationMode, clusterMode, data)
+ .then(
+ resp => {
+ if (saveFilterConfig) {
+ return this.saveFilterConfig().then(() => {
+ return this.submitSuccess(resp, action, clusterMode);
+ });
+ } else {
+ return this.submitSuccess(resp, action, clusterMode);
+ }
+ },
+ (...args) => this.submitError(...args)
+ );
+ },
+
+ actions: {
+ onSubmit(/*action, mode, data, event*/) {
+ return this.submitHandler(...arguments);
+ },
+
+ clear() {
+ this.reset();
+ this.setProperties({
+ token: null,
+ id: null,
+ });
+ },
+ },
+});
diff --git a/ui/app/controllers/vault/cluster/replication/index.js b/ui/app/controllers/vault/cluster/replication/index.js
new file mode 100644
index 000000000..4966bce8f
--- /dev/null
+++ b/ui/app/controllers/vault/cluster/replication/index.js
@@ -0,0 +1,3 @@
+import Controller from './replication-mode';
+
+export default Controller.extend();
diff --git a/ui/app/controllers/vault/cluster/replication/mode.js b/ui/app/controllers/vault/cluster/replication/mode.js
new file mode 100644
index 000000000..4966bce8f
--- /dev/null
+++ b/ui/app/controllers/vault/cluster/replication/mode.js
@@ -0,0 +1,3 @@
+import Controller from './replication-mode';
+
+export default Controller.extend();
diff --git a/ui/app/controllers/vault/cluster/replication/mode/index.js b/ui/app/controllers/vault/cluster/replication/mode/index.js
new file mode 100644
index 000000000..b0d0fc2ee
--- /dev/null
+++ b/ui/app/controllers/vault/cluster/replication/mode/index.js
@@ -0,0 +1,3 @@
+import Controller from '../replication-mode';
+
+export default Controller.extend();
diff --git a/ui/app/controllers/vault/cluster/replication/mode/manage.js b/ui/app/controllers/vault/cluster/replication/mode/manage.js
new file mode 100644
index 000000000..b0d0fc2ee
--- /dev/null
+++ b/ui/app/controllers/vault/cluster/replication/mode/manage.js
@@ -0,0 +1,3 @@
+import Controller from '../replication-mode';
+
+export default Controller.extend();
diff --git a/ui/app/controllers/vault/cluster/replication/mode/secondaries.js b/ui/app/controllers/vault/cluster/replication/mode/secondaries.js
new file mode 100644
index 000000000..b0d0fc2ee
--- /dev/null
+++ b/ui/app/controllers/vault/cluster/replication/mode/secondaries.js
@@ -0,0 +1,3 @@
+import Controller from '../replication-mode';
+
+export default Controller.extend();
diff --git a/ui/app/controllers/vault/cluster/replication/mode/secondaries/add.js b/ui/app/controllers/vault/cluster/replication/mode/secondaries/add.js
new file mode 100644
index 000000000..01da58dff
--- /dev/null
+++ b/ui/app/controllers/vault/cluster/replication/mode/secondaries/add.js
@@ -0,0 +1,3 @@
+import ReplicationController from '../../../replication';
+
+export default ReplicationController.extend();
diff --git a/ui/app/controllers/vault/cluster/replication/mode/secondaries/config-create.js b/ui/app/controllers/vault/cluster/replication/mode/secondaries/config-create.js
new file mode 100644
index 000000000..092ead178
--- /dev/null
+++ b/ui/app/controllers/vault/cluster/replication/mode/secondaries/config-create.js
@@ -0,0 +1 @@
+export { default } from './config-edit';
diff --git a/ui/app/controllers/vault/cluster/replication/mode/secondaries/config-edit.js b/ui/app/controllers/vault/cluster/replication/mode/secondaries/config-edit.js
new file mode 100644
index 000000000..4ff135647
--- /dev/null
+++ b/ui/app/controllers/vault/cluster/replication/mode/secondaries/config-edit.js
@@ -0,0 +1,52 @@
+import Ember from 'ember';
+
+const CONFIG_DEFAULTS = {
+ mode: 'whitelist',
+ paths: [],
+};
+
+export default Ember.Controller.extend({
+ flashMessages: Ember.inject.service(),
+ rm: Ember.inject.service('replication-mode'),
+ replicationMode: Ember.computed.alias('rm.mode'),
+ actions: {
+ resetConfig(config) {
+ if (config.get('isNew')) {
+ config.setProperties(CONFIG_DEFAULTS);
+ } else {
+ config.rollbackAttributes();
+ }
+ },
+
+ saveConfig(config, isDelete) {
+ const flash = this.get('flashMessages');
+ const id = config.id;
+ const redirectArgs = isDelete
+ ? [
+ 'vault.cluster.replication.mode.secondaries',
+ this.model.cluster.get('name'),
+ this.get('replicationMode'),
+ ]
+ : ['vault.cluster.replication.mode.secondaries.config-show', id];
+ const modelMethod = isDelete ? config.destroyRecord : config.save;
+
+ modelMethod
+ .call(config)
+ .then(() => {
+ this.transitionToRoute(...redirectArgs).followRedirects().then(() => {
+ flash.success(
+ `The performance mount filter config for the secondary ${id} was successfully ${isDelete
+ ? 'deleted'
+ : 'saved'}.`
+ );
+ });
+ })
+ .catch(e => {
+ const errString = e.errors.join('.');
+ flash.error(
+ `There was an error ${isDelete ? 'deleting' : 'saving'} the config for ${id}: ${errString}`
+ );
+ });
+ },
+ },
+});
diff --git a/ui/app/controllers/vault/cluster/replication/mode/secondaries/config-show.js b/ui/app/controllers/vault/cluster/replication/mode/secondaries/config-show.js
new file mode 100644
index 000000000..f0dddae6f
--- /dev/null
+++ b/ui/app/controllers/vault/cluster/replication/mode/secondaries/config-show.js
@@ -0,0 +1,6 @@
+import Ember from 'ember';
+
+export default Ember.Controller.extend({
+ rm: Ember.inject.service('replication-mode'),
+ replicationMode: Ember.computed.alias('rm.mode'),
+});
diff --git a/ui/app/controllers/vault/cluster/replication/mode/secondaries/index.js b/ui/app/controllers/vault/cluster/replication/mode/secondaries/index.js
new file mode 100644
index 000000000..01da58dff
--- /dev/null
+++ b/ui/app/controllers/vault/cluster/replication/mode/secondaries/index.js
@@ -0,0 +1,3 @@
+import ReplicationController from '../../../replication';
+
+export default ReplicationController.extend();
diff --git a/ui/app/controllers/vault/cluster/replication/mode/secondaries/revoke.js b/ui/app/controllers/vault/cluster/replication/mode/secondaries/revoke.js
new file mode 100644
index 000000000..9be89e9d3
--- /dev/null
+++ b/ui/app/controllers/vault/cluster/replication/mode/secondaries/revoke.js
@@ -0,0 +1 @@
+export { default } from '../../../replication';
diff --git a/ui/app/controllers/vault/cluster/replication/replication-mode.js b/ui/app/controllers/vault/cluster/replication/replication-mode.js
new file mode 100644
index 000000000..f0dddae6f
--- /dev/null
+++ b/ui/app/controllers/vault/cluster/replication/replication-mode.js
@@ -0,0 +1,6 @@
+import Ember from 'ember';
+
+export default Ember.Controller.extend({
+ rm: Ember.inject.service('replication-mode'),
+ replicationMode: Ember.computed.alias('rm.mode'),
+});
diff --git a/ui/app/controllers/vault/cluster/response-wrapping.js b/ui/app/controllers/vault/cluster/response-wrapping.js
new file mode 100644
index 000000000..612c01b88
--- /dev/null
+++ b/ui/app/controllers/vault/cluster/response-wrapping.js
@@ -0,0 +1,9 @@
+import Ember from 'ember';
+
+export default Ember.Controller.extend({
+ queryParams: {
+ selectedAction: 'action',
+ },
+
+ selectedAction: 'wrap',
+});
diff --git a/ui/app/controllers/vault/cluster/secrets/backend/actions-root.js b/ui/app/controllers/vault/cluster/secrets/backend/actions-root.js
new file mode 100644
index 000000000..0b230db57
--- /dev/null
+++ b/ui/app/controllers/vault/cluster/secrets/backend/actions-root.js
@@ -0,0 +1 @@
+export { default } from './actions';
diff --git a/ui/app/controllers/vault/cluster/secrets/backend/actions.js b/ui/app/controllers/vault/cluster/secrets/backend/actions.js
new file mode 100644
index 000000000..81393e4b6
--- /dev/null
+++ b/ui/app/controllers/vault/cluster/secrets/backend/actions.js
@@ -0,0 +1,20 @@
+import Ember from 'ember';
+import BackendCrumbMixin from 'vault/mixins/backend-crumb';
+
+export default Ember.Controller.extend(BackendCrumbMixin, {
+ queryParams: {
+ selectedAction: 'action',
+ },
+
+ actions: {
+ refresh: function() {
+ // closure actions don't bubble to routes,
+ // so we have to manually bubble here
+ this.send('refreshModel');
+ },
+
+ hasChanges(hasChanges) {
+ this.send('hasDataChanges', hasChanges);
+ },
+ },
+});
diff --git a/ui/app/controllers/vault/cluster/secrets/backend/create-root.js b/ui/app/controllers/vault/cluster/secrets/backend/create-root.js
new file mode 100644
index 000000000..28d5c8ccc
--- /dev/null
+++ b/ui/app/controllers/vault/cluster/secrets/backend/create-root.js
@@ -0,0 +1 @@
+export { default } from './create';
diff --git a/ui/app/controllers/vault/cluster/secrets/backend/create.js b/ui/app/controllers/vault/cluster/secrets/backend/create.js
new file mode 100644
index 000000000..383aaa025
--- /dev/null
+++ b/ui/app/controllers/vault/cluster/secrets/backend/create.js
@@ -0,0 +1,17 @@
+import Ember from 'ember';
+import BackendCrumbMixin from 'vault/mixins/backend-crumb';
+
+export default Ember.Controller.extend(BackendCrumbMixin, {
+ queryParams: ['initialKey'],
+
+ initialKey: '',
+
+ actions: {
+ refresh: function() {
+ this.send('refreshModel');
+ },
+ hasChanges(hasChanges) {
+ this.send('hasDataChanges', hasChanges);
+ },
+ },
+});
diff --git a/ui/app/controllers/vault/cluster/secrets/backend/credentials-root.js b/ui/app/controllers/vault/cluster/secrets/backend/credentials-root.js
new file mode 100644
index 000000000..cbf926c3a
--- /dev/null
+++ b/ui/app/controllers/vault/cluster/secrets/backend/credentials-root.js
@@ -0,0 +1 @@
+export { default } from './credentials';
diff --git a/ui/app/controllers/vault/cluster/secrets/backend/credentials.js b/ui/app/controllers/vault/cluster/secrets/backend/credentials.js
new file mode 100644
index 000000000..bb0f79adf
--- /dev/null
+++ b/ui/app/controllers/vault/cluster/secrets/backend/credentials.js
@@ -0,0 +1,9 @@
+import Ember from 'ember';
+
+export default Ember.Controller.extend({
+ queryParams: ['action'],
+ action: '',
+ reset() {
+ this.set('action', '');
+ },
+});
diff --git a/ui/app/controllers/vault/cluster/secrets/backend/edit-root.js b/ui/app/controllers/vault/cluster/secrets/backend/edit-root.js
new file mode 100644
index 000000000..58cfd6a1c
--- /dev/null
+++ b/ui/app/controllers/vault/cluster/secrets/backend/edit-root.js
@@ -0,0 +1 @@
+export { default } from './edit';
diff --git a/ui/app/controllers/vault/cluster/secrets/backend/edit.js b/ui/app/controllers/vault/cluster/secrets/backend/edit.js
new file mode 100644
index 000000000..35e181111
--- /dev/null
+++ b/ui/app/controllers/vault/cluster/secrets/backend/edit.js
@@ -0,0 +1,16 @@
+import Ember from 'ember';
+import BackendCrumbMixin from 'vault/mixins/backend-crumb';
+
+export default Ember.Controller.extend(BackendCrumbMixin, {
+ actions: {
+ refresh: function() {
+ // closure actions don't bubble to routes,
+ // so we have to manually bubble here
+ this.send('refreshModel');
+ },
+
+ hasChanges(hasChanges) {
+ this.send('hasDataChanges', hasChanges);
+ },
+ },
+});
diff --git a/ui/app/controllers/vault/cluster/secrets/backend/list-root.js b/ui/app/controllers/vault/cluster/secrets/backend/list-root.js
new file mode 100644
index 000000000..ea1fedd0a
--- /dev/null
+++ b/ui/app/controllers/vault/cluster/secrets/backend/list-root.js
@@ -0,0 +1 @@
+export { default } from './list';
diff --git a/ui/app/controllers/vault/cluster/secrets/backend/list.js b/ui/app/controllers/vault/cluster/secrets/backend/list.js
new file mode 100644
index 000000000..a113f5cb1
--- /dev/null
+++ b/ui/app/controllers/vault/cluster/secrets/backend/list.js
@@ -0,0 +1,73 @@
+import Ember from 'ember';
+import utils from 'vault/lib/key-utils';
+import BackendCrumbMixin from 'vault/mixins/backend-crumb';
+
+export default Ember.Controller.extend(BackendCrumbMixin, {
+ flashMessages: Ember.inject.service(),
+ queryParams: ['page', 'pageFilter', 'tab'],
+
+ tab: '',
+ page: 1,
+ pageFilter: null,
+ filterFocused: false,
+
+ // set via the route `loading` action
+ isLoading: false,
+
+ filterMatchesKey: Ember.computed('filter', 'model', 'model.[]', function() {
+ var filter = this.get('filter');
+ var content = this.get('model');
+ return !!(content.length && content.findBy('id', filter));
+ }),
+
+ firstPartialMatch: Ember.computed('filter', 'model', 'model.[]', 'filterMatchesKey', function() {
+ var filter = this.get('filter');
+ var content = this.get('model');
+ var filterMatchesKey = this.get('filterMatchesKey');
+ var re = new RegExp('^' + filter);
+ return filterMatchesKey
+ ? null
+ : content.find(function(key) {
+ return re.test(key.get('id'));
+ });
+ }),
+
+ filterIsFolder: Ember.computed('filter', function() {
+ return !!utils.keyIsFolder(this.get('filter'));
+ }),
+
+ actions: {
+ setFilter(val) {
+ this.set('filter', val);
+ },
+
+ setFilterFocus(bool) {
+ this.set('filterFocused', bool);
+ },
+
+ chooseAction(action) {
+ this.set('selectedAction', action);
+ },
+
+ toggleZeroAddress(item, backend) {
+ item.toggleProperty('zeroAddress');
+ this.set('loading-' + item.id, true);
+ backend
+ .saveZeroAddressConfig()
+ .catch(e => {
+ item.set('zeroAddress', false);
+ this.get('flashMessages').danger(e.message);
+ })
+ .finally(() => {
+ this.set('loading-' + item.id, false);
+ });
+ },
+
+ delete(item) {
+ const name = item.id;
+ item.destroyRecord().then(() => {
+ this.get('flashMessages').success(`${name} was successfully deleted.`);
+ });
+ },
+ },
+});
diff --git a/ui/app/controllers/vault/cluster/secrets/backend/show-root.js b/ui/app/controllers/vault/cluster/secrets/backend/show-root.js
new file mode 100644
index 000000000..98bd0c1a0
--- /dev/null
+++ b/ui/app/controllers/vault/cluster/secrets/backend/show-root.js
@@ -0,0 +1 @@
+export { default } from './show';
diff --git a/ui/app/controllers/vault/cluster/secrets/backend/show.js b/ui/app/controllers/vault/cluster/secrets/backend/show.js
new file mode 100644
index 000000000..d72f6bdc8
--- /dev/null
+++ b/ui/app/controllers/vault/cluster/secrets/backend/show.js
@@ -0,0 +1,21 @@
+import Ember from 'ember';
+import BackendCrumbMixin from 'vault/mixins/backend-crumb';
+
+export default Ember.Controller.extend(BackendCrumbMixin, {
+ queryParams: ['tab'],
+ tab: '',
+ reset() {
+ this.set('tab', '');
+ },
+ actions: {
+ refresh: function() {
+ // closure actions don't bubble to routes,
+ // so we have to manually bubble here
+ this.send('refreshModel');
+ },
+
+ hasChanges(hasChanges) {
+ this.send('hasDataChanges', hasChanges);
+ },
+ },
+});
diff --git a/ui/app/controllers/vault/cluster/secrets/backend/sign-root.js b/ui/app/controllers/vault/cluster/secrets/backend/sign-root.js
new file mode 100644
index 000000000..d034c1595
--- /dev/null
+++ b/ui/app/controllers/vault/cluster/secrets/backend/sign-root.js
@@ -0,0 +1 @@
+export { default } from './sign';
diff --git a/ui/app/controllers/vault/cluster/secrets/backend/sign.js b/ui/app/controllers/vault/cluster/secrets/backend/sign.js
new file mode 100644
index 000000000..5e5e3bd47
--- /dev/null
+++ b/ui/app/controllers/vault/cluster/secrets/backend/sign.js
@@ -0,0 +1,36 @@
+import Ember from 'ember';
+const { get, set } = Ember;
+
+export default Ember.Controller.extend({
+ store: Ember.inject.service(),
+ loading: false,
+ emptyData: '{\n}',
+ actions: {
+ sign() {
+ this.set('loading', true);
+ this.model.save().finally(() => {
+ this.set('loading', false);
+ });
+ },
+
+ codemirrorUpdated(attr, val, codemirror) {
+ codemirror.performLint();
+ const hasErrors = codemirror.state.lint.marked.length > 0;
+
+ if (!hasErrors) {
+ set(this.get('model'), attr, JSON.parse(val));
+ }
+ },
+
+ newModel() {
+ const model = this.get('model');
+ const roleModel = model.get('role');
+ model.unloadRecord();
+ const newModel = this.get('store').createRecord('ssh-sign', {
+ role: roleModel,
+ id: `${get(roleModel, 'backend')}-${get(roleModel, 'name')}`,
+ });
+ this.set('model', newModel);
+ },
+ },
+});
diff --git a/ui/app/controllers/vault/cluster/secrets/backends.js b/ui/app/controllers/vault/cluster/secrets/backends.js
new file mode 100644
index 000000000..31195d10f
--- /dev/null
+++ b/ui/app/controllers/vault/cluster/secrets/backends.js
@@ -0,0 +1,27 @@
+import Ember from 'ember';
+import { supportedSecretBackends } from 'vault/helpers/supported-secret-backends';
+const { computed, Controller } = Ember;
+const LINKED_BACKENDS = supportedSecretBackends();
+
+export default Controller.extend({
+ displayableBackends: computed.filterBy('model', 'shouldIncludeInList'),
+
+ supportedBackends: computed('displayableBackends', 'displayableBackends.[]', function() {
+ return (this.get('displayableBackends') || [])
+ .filter(backend => LINKED_BACKENDS.includes(backend.get('type')))
+ .sortBy('id');
+ }),
+
+ unsupportedBackends: computed(
+ 'displayableBackends',
+ 'displayableBackends.[]',
+ 'supportedBackends',
+ 'supportedBackends.[]',
+ function() {
+ return (this.get('displayableBackends') || [])
+ .slice()
+ .removeObjects(this.get('supportedBackends'))
+ .sortBy('id');
+ }
+ ),
+});
diff --git a/ui/app/controllers/vault/cluster/settings/auth/enable.js b/ui/app/controllers/vault/cluster/settings/auth/enable.js
new file mode 100644
index 000000000..945a3bf89
--- /dev/null
+++ b/ui/app/controllers/vault/cluster/settings/auth/enable.js
@@ -0,0 +1,12 @@
+import Ember from 'ember';
+
+export default Ember.Controller.extend({
+ actions: {
+ onMountSuccess: function() {
+ return this.transitionToRoute('vault.cluster.access.methods');
+ },
+ onConfigError: function(modelId) {
+ return this.transitionToRoute('vault.cluster.settings.auth.configure', modelId);
+ },
+ },
+});
diff --git a/ui/app/controllers/vault/cluster/settings/configure-secret-backend.js b/ui/app/controllers/vault/cluster/settings/configure-secret-backend.js
new file mode 100644
index 000000000..a2401f0ad
--- /dev/null
+++ b/ui/app/controllers/vault/cluster/settings/configure-secret-backend.js
@@ -0,0 +1,67 @@
+import Ember from 'ember';
+
+const CONFIG_ATTRS = {
+ // ssh
+ configured: false,
+
+ // aws root config
+ iamEndpoint: null,
+ stsEndpoint: null,
+ accessKey: null,
+ secretKey: null,
+ region: '',
+};
+
+export default Ember.Controller.extend(CONFIG_ATTRS, {
+ queryParams: ['tab'],
+ tab: '',
+ flashMessages: Ember.inject.service(),
+ loading: false,
+ reset() {
+ this.get('model').rollbackAttributes();
+ this.setProperties(CONFIG_ATTRS);
+ },
+ actions: {
+ saveConfig(options = { delete: false }) {
+ const isDelete = options.delete;
+ if (this.get('model.type') === 'ssh') {
+ this.set('loading', true);
+ this.get('model').saveCA({ isDelete }).then(() => {
+ this.set('loading', false);
+ this.send('refreshRoute');
+ this.set('configured', !isDelete);
+ if (isDelete) {
+ this.get('flashMessages').success('SSH Certificate Authority Configuration deleted!');
+ } else {
+ this.get('flashMessages').success('SSH Certificate Authority Configuration saved!');
+ }
+ });
+ }
+ },
+
+ save(method, data) {
+ this.set('loading', true);
+ const hasData = Object.keys(data).some(key => {
+ return Ember.isPresent(data[key]);
+ });
+ if (!hasData) {
+ return;
+ }
+ this.get('model')
+ .save({
+ adapterOptions: {
+ adapterMethod: method,
+ data,
+ },
+ })
+ .then(() => {
+ this.get('model').send('pushedData');
+ this.reset();
+ this.get('flashMessages').success('The backend configuration saved successfully!');
+ })
+ .finally(() => {
+ this.set('loading', false);
+ });
+ },
+ },
+});
diff --git a/ui/app/controllers/vault/cluster/settings/mount-secret-backend.js b/ui/app/controllers/vault/cluster/settings/mount-secret-backend.js
new file mode 100644
index 000000000..f23db482a
--- /dev/null
+++ b/ui/app/controllers/vault/cluster/settings/mount-secret-backend.js
@@ -0,0 +1,143 @@
+import Ember from 'ember';
+import { supportedSecretBackends } from 'vault/helpers/supported-secret-backends';
+
+const SUPPORTED_BACKENDS = supportedSecretBackends();
+
+const { computed } = Ember;
+
+export default Ember.Controller.extend({
+ mountTypes: [
+ { label: 'AWS', value: 'aws' },
+ { label: 'Cassandra', value: 'cassandra' },
+ { label: 'Consul', value: 'consul' },
+ { label: 'Databases', value: 'database' },
+ { label: 'KV', value: 'kv' },
+ { label: 'MongoDB', value: 'mongodb' },
+ { label: 'MS SQL', value: 'mssql', deprecated: true },
+ { label: 'MySQL', value: 'mysql', deprecated: true },
+ { label: 'Nomad', value: 'nomad' },
+ { label: 'PKI', value: 'pki' },
+ { label: 'PostgreSQL', value: 'postgresql', deprecated: true },
+ { label: 'RabbitMQ', value: 'rabbitmq' },
+ { label: 'SSH', value: 'ssh' },
+ { label: 'Transit', value: 'transit' },
+ { label: 'TOTP', value: 'totp' },
+ ],
+
+ selectedType: null,
+ selectedPath: null,
+ description: null,
+ default_lease_ttl: null,
+ max_lease_ttl: null,
+ force_no_cache: null,
+ showConfig: false,
+ local: false,
+ sealWrap: false,
+ versioned: true,
+
+ selection: computed('selectedType', function() {
+ return this.get('mountTypes').findBy('value', this.get('selectedType'));
+ }),
+
+ flashMessages: Ember.inject.service(),
+
+ reset() {
+ const defaultBackend = this.get('mountTypes.firstObject.value');
+ this.setProperties({
+ selectedPath: defaultBackend,
+ selectedType: defaultBackend,
+ description: null,
+ default_lease_ttl: null,
+ max_lease_ttl: null,
+ force_no_cache: null,
+ local: false,
+ showConfig: false,
+ sealWrap: false,
+ versioned: true,
+ });
+ },
+
+ init() {
+ this._super(...arguments);
+ this.reset();
+ },
+
+ actions: {
+ onTypeChange(val) {
+ const { selectedPath, selectedType } = this.getProperties('selectedPath', 'selectedType');
+ this.set('selectedType', val);
+ if (selectedPath === selectedType) {
+ this.set('selectedPath', val);
+ }
+ },
+
+ toggleShowConfig() {
+ this.toggleProperty('showConfig');
+ },
+
+ mountBackend() {
+ const {
+ selectedPath: path,
+ selectedType: type,
+ description,
+ default_lease_ttl,
+ force_no_cache,
+ local,
+ max_lease_ttl,
+ sealWrap,
+ versioned,
+ } = this.getProperties(
+ 'selectedPath',
+ 'selectedType',
+ 'description',
+ 'default_lease_ttl',
+ 'force_no_cache',
+ 'local',
+ 'max_lease_ttl',
+ 'sealWrap',
+ 'versioned'
+ );
+ const currentModel = this.get('model');
+ if (currentModel && currentModel.rollbackAttributes) {
+ currentModel.rollbackAttributes();
+ }
+ let attrs = {
+ path,
+ type,
+ description,
+ local,
+ sealWrap,
+ };
+
+ if (this.get('showConfig')) {
+ attrs.config = {
+ default_lease_ttl,
+ max_lease_ttl,
+ force_no_cache,
+ };
+ }
+
+ if (type === 'kv' && versioned) {
+ attrs.options = {
+ versioned: 'true',
+ };
+ }
+
+ const model = this.store.createRecord('secret-engine', attrs);
+
+ this.set('model', model);
+ model.save().then(() => {
+ this.reset();
+ let transition;
+ if (SUPPORTED_BACKENDS.includes(type)) {
+ transition = this.transitionToRoute('vault.cluster.secrets.backend.index', path);
+ } else {
+ transition = this.transitionToRoute('vault.cluster.secrets.backends');
+ }
+ transition.followRedirects().then(() => {
+ this.get('flashMessages').success(`Successfully mounted '${type}' at '${path}'!`);
+ });
+ });
+ },
+ },
+});
diff --git a/ui/app/controllers/vault/cluster/settings/seal.js b/ui/app/controllers/vault/cluster/settings/seal.js
new file mode 100644
index 000000000..53017f39e
--- /dev/null
+++ b/ui/app/controllers/vault/cluster/settings/seal.js
@@ -0,0 +1,15 @@
+import Ember from 'ember';
+
+export default Ember.Controller.extend({
+ auth: Ember.inject.service(),
+
+ actions: {
+ seal() {
+ return this.model.cluster.store.adapterFor('cluster').seal().then(() => {
+ this.model.cluster.get('leaderNode').set('sealed', true);
+ this.get('auth').deleteCurrentToken();
+ return this.transitionToRoute('vault.cluster');
+ });
+ },
+ },
+});
diff --git a/ui/app/controllers/vault/cluster/unseal.js b/ui/app/controllers/vault/cluster/unseal.js
new file mode 100644
index 000000000..d20b38125
--- /dev/null
+++ b/ui/app/controllers/vault/cluster/unseal.js
@@ -0,0 +1,14 @@
+import Ember from 'ember';
+
+export default Ember.Controller.extend({
+ actions: {
+ transitionToCluster() {
+ return this.get('model').reload().then(() => {
+ return this.transitionToRoute('vault.cluster', this.get('model.name'));
+ });
+ },
+ isUnsealed(data) {
+ return data.sealed === false;
+ },
+ },
+});
diff --git a/ui/app/helpers/.gitkeep b/ui/app/helpers/.gitkeep
new file mode 100644
index 000000000..e69de29bb
diff --git a/ui/app/helpers/add.js b/ui/app/helpers/add.js
new file mode 100644
index 000000000..0f2a78a20
--- /dev/null
+++ b/ui/app/helpers/add.js
@@ -0,0 +1,7 @@
+import Ember from 'ember';
+
+export function add(params) {
+ return params.reduce((sum, param) => parseInt(param, 0) + sum, 0);
+}
+
+export default Ember.Helper.helper(add);
diff --git a/ui/app/helpers/aws-regions.js b/ui/app/helpers/aws-regions.js
new file mode 100644
index 000000000..5901e157d
--- /dev/null
+++ b/ui/app/helpers/aws-regions.js
@@ -0,0 +1,25 @@
+import Ember from 'ember';
+
+//list from http://docs.aws.amazon.com/general/latest/gr/rande.html#sts_region
+const REGIONS = [
+ 'us-east-1',
+ 'us-east-2',
+ 'us-west-1',
+ 'us-west-2',
+ 'ca-central-1',
+ 'ap-south-1',
+ 'ap-northeast-1',
+ 'ap-northeast-2',
+ 'ap-southeast-1',
+ 'ap-southeast-2',
+ 'eu-central-1',
+ 'eu-west-1',
+ 'eu-west-2',
+ 'sa-east-1',
+];
+
+export function regions() {
+ return REGIONS.slice(0);
+}
+
+export default Ember.Helper.helper(regions);
diff --git a/ui/app/helpers/coerce-eq.js b/ui/app/helpers/coerce-eq.js
new file mode 100644
index 000000000..c53ef23a7
--- /dev/null
+++ b/ui/app/helpers/coerce-eq.js
@@ -0,0 +1,8 @@
+/*jshint eqeqeq: false */
+import Ember from 'ember';
+
+export function coerceEq(params) {
+ return params[0] == params[1];
+}
+
+export default Ember.Helper.helper(coerceEq);
diff --git a/ui/app/helpers/has-feature.js b/ui/app/helpers/has-feature.js
new file mode 100644
index 000000000..f260b7fd7
--- /dev/null
+++ b/ui/app/helpers/has-feature.js
@@ -0,0 +1,32 @@
+import Ember from 'ember';
+const { Helper, inject, observer } = Ember;
+
+const FEATURES = [
+ 'HSM',
+ 'Performance Replication',
+ 'DR Replication',
+ 'MFA',
+ 'Sentinel',
+ 'AWS KMS Autounseal',
+ 'GCP CKMS Autounseal',
+ 'Seal Wrapping',
+ 'Control Groups',
+];
+
+export function hasFeature(featureName, features) {
+ if (!FEATURES.includes(featureName)) {
+ Ember.assert(`${featureName} is not one of the available values for Vault Enterprise features.`, false);
+ return false;
+ }
+ return features ? features.includes(featureName) : false;
+}
+
+export default Helper.extend({
+ version: inject.service(),
+ onFeaturesChange: observer('version.features.[]', function() {
+ this.recompute();
+ }),
+ compute([featureName]) {
+ return hasFeature(featureName, this.get('version.features'));
+ },
+});
diff --git a/ui/app/helpers/includes.js b/ui/app/helpers/includes.js
new file mode 100644
index 000000000..7e460fc24
--- /dev/null
+++ b/ui/app/helpers/includes.js
@@ -0,0 +1,7 @@
+import Ember from 'ember';
+
+export function includes([haystack, needle]) {
+ return haystack.includes(needle);
+}
+
+export default Ember.Helper.helper(includes);
diff --git a/ui/app/helpers/is-active-route.js b/ui/app/helpers/is-active-route.js
new file mode 100644
index 000000000..1f5a0709a
--- /dev/null
+++ b/ui/app/helpers/is-active-route.js
@@ -0,0 +1,34 @@
+import Ember from 'ember';
+
+const { Helper, inject, observer } = Ember;
+
+const exact = (a, b) => a === b;
+const startsWith = (a, b) => a.indexOf(b) === 0;
+
+export default Helper.extend({
+ routing: inject.service('-routing'),
+
+ onRouteChange: observer('routing.router.currentURL', 'routing.router.currentRouteName', function() {
+ this.recompute();
+ }),
+
+ compute([routeName, model], { isExact }) {
+ const router = this.get('routing.router');
+ const currentRoute = router.get('currentRouteName');
+ let currentURL = router.get('currentURL');
+ // if we have any query params we want to discard them
+ currentURL = currentURL.split('?')[0];
+ const comparator = isExact ? exact : startsWith;
+ if (!currentRoute) {
+ return false;
+ }
+ if (Ember.isArray(routeName)) {
+ return routeName.some(name => comparator(currentRoute, name));
+ } else if (model) {
+ // slice off the rootURL from the generated route
+ return comparator(currentURL, router.generate(routeName, model).slice(router.rootURL.length - 1));
+ } else {
+ return comparator(currentRoute, routeName);
+ }
+ },
+});
diff --git a/ui/app/helpers/is-version.js b/ui/app/helpers/is-version.js
new file mode 100644
index 000000000..85bd65242
--- /dev/null
+++ b/ui/app/helpers/is-version.js
@@ -0,0 +1,16 @@
+import Ember from 'ember';
+const { Helper, inject, observer } = Ember;
+
+export default Helper.extend({
+ version: inject.service(),
+ onFeaturesChange: observer('version.version', function() {
+ this.recompute();
+ }),
+ compute([sku]) {
+ if (sku !== 'OSS' && sku !== 'Enterprise') {
+ Ember.assert(`${sku} is not one of the available values for Vault versions.`, false);
+ return false;
+ }
+ return this.get(`version.is${sku}`);
+ },
+});
diff --git a/ui/app/helpers/jsonify.js b/ui/app/helpers/jsonify.js
new file mode 100644
index 000000000..259c8d470
--- /dev/null
+++ b/ui/app/helpers/jsonify.js
@@ -0,0 +1,7 @@
+import Ember from 'ember';
+
+export function jsonify([target]) {
+ return JSON.parse(target);
+}
+
+export default Ember.Helper.helper(jsonify);
diff --git a/ui/app/helpers/message-types.js b/ui/app/helpers/message-types.js
new file mode 100644
index 000000000..a42e8f2f1
--- /dev/null
+++ b/ui/app/helpers/message-types.js
@@ -0,0 +1,34 @@
+import Ember from 'ember';
+
+const MESSAGE_TYPES = {
+ info: {
+ class: 'is-info',
+ glyphClass: 'has-text-info',
+ glyph: 'information-circled',
+ text: 'Info',
+ },
+ success: {
+ class: 'is-success',
+ glyphClass: 'has-text-success',
+ glyph: 'checkmark-circled',
+ text: 'Success',
+ },
+ danger: {
+ class: 'is-danger',
+ glyphClass: 'has-text-danger',
+ glyph: 'close-circled',
+ text: 'Error',
+ },
+ warning: {
+ class: 'is-highlight',
+ glyphClass: 'has-text-highlight',
+ glyph: 'alert-circled',
+ text: 'Attention',
+ },
+};
+
+export function messageTypes([type]) {
+ return MESSAGE_TYPES[type];
+}
+
+export default Ember.Helper.helper(messageTypes);
diff --git a/ui/app/helpers/mountable-auth-methods.js b/ui/app/helpers/mountable-auth-methods.js
new file mode 100644
index 000000000..42433132a
--- /dev/null
+++ b/ui/app/helpers/mountable-auth-methods.js
@@ -0,0 +1,60 @@
+import Ember from 'ember';
+
+const MOUNTABLE_AUTH_METHODS = [
+ {
+ displayName: 'AppRole',
+ value: 'approle',
+ type: 'approle',
+ },
+ {
+ displayName: 'AWS',
+ value: 'aws',
+ type: 'aws',
+ },
+ {
+ displayName: 'Google Cloud',
+ value: 'gcp',
+ type: 'gcp',
+ },
+ {
+ displayName: 'Kubernetes',
+ value: 'kubernetes',
+ type: 'kubernetes',
+ },
+ {
+ displayName: 'GitHub',
+ value: 'github',
+ type: 'github',
+ },
+ {
+ displayName: 'LDAP',
+ value: 'ldap',
+ type: 'ldap',
+ },
+ {
+ displayName: 'Okta',
+ value: 'okta',
+ type: 'okta',
+ },
+ {
+ displayName: 'RADIUS',
+ value: 'radius',
+ type: 'radius',
+ },
+ {
+ displayName: 'TLS Certificates',
+ value: 'cert',
+ type: 'cert',
+ },
+ {
+ displayName: 'Username & Password',
+ value: 'userpass',
+ type: 'userpass',
+ },
+];
+
+export function methods() {
+ return MOUNTABLE_AUTH_METHODS;
+}
+
+export default Ember.Helper.helper(methods);
diff --git a/ui/app/helpers/nav-to-route.js b/ui/app/helpers/nav-to-route.js
new file mode 100644
index 000000000..93d348511
--- /dev/null
+++ b/ui/app/helpers/nav-to-route.js
@@ -0,0 +1,15 @@
+import Ember from 'ember';
+
+const { Helper, inject } = Ember;
+
+export default Helper.extend({
+ routing: inject.service('-routing'),
+
+ compute([routeName, ...models], { replace = false }) {
+ return () => {
+ const router = this.get('routing.router');
+ const method = replace ? router.replaceWith : router.transitionTo;
+ return method.call(router, routeName, ...models);
+ };
+ },
+});
diff --git a/ui/app/helpers/options-for-backend.js b/ui/app/helpers/options-for-backend.js
new file mode 100644
index 000000000..da626cb57
--- /dev/null
+++ b/ui/app/helpers/options-for-backend.js
@@ -0,0 +1,84 @@
+import Ember from 'ember';
+
+const DEFAULT_DISPLAY = {
+ searchPlaceholder: 'Filter secrets',
+ item: 'secret',
+ create: 'Create secret',
+ navigateTree: true,
+ editComponent: 'secret-edit',
+ listItemPartial: 'partials/secret-list/item',
+};
+const SECRET_BACKENDS = {
+ aws: {
+ displayName: 'AWS',
+ searchPlaceholder: 'Filter roles',
+ item: 'role',
+ create: 'Create role',
+ navigateTree: false,
+ editComponent: 'role-aws-edit',
+ listItemPartial: 'partials/secret-list/aws-role-item',
+ },
+ pki: {
+ displayName: 'PKI',
+ navigateTree: false,
+ listItemPartial: 'partials/secret-list/pki-role-item',
+ tabs: [
+ {
+ name: 'roles',
+ label: 'Roles',
+ searchPlaceholder: 'Filter roles',
+ item: 'role',
+ create: 'Create role',
+ editComponent: 'role-pki-edit',
+ },
+ {
+ name: 'certs',
+ modelPrefix: 'cert/',
+ label: 'Certificates',
+ searchPlaceholder: 'Filter certificates',
+ item: 'certificates',
+ create: 'Create role',
+ tab: 'certs',
+ listItemPartial: 'partials/secret-list/pki-cert-item',
+ editComponent: 'pki-cert-show',
+ },
+ ],
+ },
+ ssh: {
+ displayName: 'SSH',
+ searchPlaceholder: 'Filter roles',
+ item: 'role',
+ create: 'Create role',
+ navigateTree: false,
+ editComponent: 'role-ssh-edit',
+ listItemPartial: 'partials/secret-list/ssh-role-item',
+ },
+ transit: {
+ searchPlaceholder: 'Filter keys',
+ item: 'key',
+ create: 'Create encryption key',
+ navigateTree: false,
+ editComponent: 'transit-edit',
+ listItemPartial: 'partials/secret-list/item',
+ },
+};
+
+export function optionsForBackend([backend, tab]) {
+ const selected = SECRET_BACKENDS[backend];
+ let backendOptions;
+
+ if (selected && selected.tabs) {
+ let tabData =
+ selected.tabs.findBy('name', tab) || selected.tabs.findBy('modelPrefix', tab) || selected.tabs[0];
+ backendOptions = Ember.assign({}, selected, tabData);
+ } else if (selected) {
+ backendOptions = selected;
+ } else {
+ backendOptions = Ember.assign({}, DEFAULT_DISPLAY, {
+ displayName: backend === 'kv' ? 'KV' : Ember.String.capitalize(backend),
+ });
+ }
+ return backendOptions;
+}
+
+export default Ember.Helper.helper(optionsForBackend);
diff --git a/ui/app/helpers/reduce-to-array.js b/ui/app/helpers/reduce-to-array.js
new file mode 100644
index 000000000..bca0c1978
--- /dev/null
+++ b/ui/app/helpers/reduce-to-array.js
@@ -0,0 +1,16 @@
+import Ember from 'ember';
+
+export function reduceToArray(params) {
+ return params.reduce(function(result, param) {
+ if (Ember.isNone(param)) {
+ return result;
+ }
+ if (Ember.typeOf(param) === 'array') {
+ return result.concat(param);
+ } else {
+ return result.concat([param]);
+ }
+ }, []);
+}
+
+export default Ember.Helper.helper(reduceToArray);
diff --git a/ui/app/helpers/replication-action-for-mode.js b/ui/app/helpers/replication-action-for-mode.js
new file mode 100644
index 000000000..8abd86c7e
--- /dev/null
+++ b/ui/app/helpers/replication-action-for-mode.js
@@ -0,0 +1,19 @@
+import Ember from 'ember';
+const ACTIONS = {
+ performance: {
+ primary: ['disable', 'demote', 'recover', 'reindex'],
+ secondary: ['disable', 'promote', 'update-primary', 'recover', 'reindex'],
+ bootstrapping: ['disable', 'recover', 'reindex'],
+ },
+ dr: {
+ primary: ['disable', 'recover', 'reindex', 'demote'],
+ secondary: ['promote'],
+ bootstrapping: ['disable', 'recover', 'reindex'],
+ },
+};
+
+export function replicationActionForMode([replicationMode, clusterMode] /*, hash*/) {
+ return Ember.get(ACTIONS, `${replicationMode}.${clusterMode}`);
+}
+
+export default Ember.Helper.helper(replicationActionForMode);
diff --git a/ui/app/helpers/set-flash-message.js b/ui/app/helpers/set-flash-message.js
new file mode 100644
index 000000000..be5126065
--- /dev/null
+++ b/ui/app/helpers/set-flash-message.js
@@ -0,0 +1,13 @@
+import Ember from 'ember';
+
+const { Helper, inject } = Ember;
+
+export default Helper.extend({
+ flashMessages: inject.service(),
+
+ compute([message, type]) {
+ return () => {
+ this.get('flashMessages')[type || 'success'](message);
+ };
+ },
+});
diff --git a/ui/app/helpers/sha2-digest-sizes.js b/ui/app/helpers/sha2-digest-sizes.js
new file mode 100644
index 000000000..3eb1624ff
--- /dev/null
+++ b/ui/app/helpers/sha2-digest-sizes.js
@@ -0,0 +1,9 @@
+import Ember from 'ember';
+
+const SHA2_DIGEST_SIZES = ['sha2-224', 'sha2-256', 'sha2-384', 'sha2-512'];
+
+export function sha2DigestSizes() {
+ return SHA2_DIGEST_SIZES;
+}
+
+export default Ember.Helper.helper(sha2DigestSizes);
diff --git a/ui/app/helpers/stringify.js b/ui/app/helpers/stringify.js
new file mode 100644
index 000000000..a49427025
--- /dev/null
+++ b/ui/app/helpers/stringify.js
@@ -0,0 +1,10 @@
+import Ember from 'ember';
+
+export function stringify([target], { skipFormat }) {
+ if (skipFormat) {
+ return JSON.stringify(target);
+ }
+ return JSON.stringify(target, null, 2);
+}
+
+export default Ember.Helper.helper(stringify);
diff --git a/ui/app/helpers/supported-auth-backends.js b/ui/app/helpers/supported-auth-backends.js
new file mode 100644
index 000000000..2db68af54
--- /dev/null
+++ b/ui/app/helpers/supported-auth-backends.js
@@ -0,0 +1,40 @@
+import Ember from 'ember';
+
+const SUPPORTED_AUTH_BACKENDS = [
+ {
+ type: 'token',
+ description: 'Token authentication.',
+ tokenPath: 'id',
+ displayNamePath: 'display_name',
+ },
+ {
+ type: 'userpass',
+ description: 'A simple username and password backend.',
+ tokenPath: 'client_token',
+ displayNamePath: 'metadata.username',
+ },
+ {
+ type: 'LDAP',
+ description: 'LDAP authentication.',
+ tokenPath: 'client_token',
+ displayNamePath: 'metadata.username',
+ },
+ {
+ type: 'Okta',
+ description: 'Authenticate with your Okta username and password.',
+ tokenPath: 'client_token',
+ displayNamePath: 'metadata.username',
+ },
+ {
+ type: 'GitHub',
+ description: 'GitHub authentication.',
+ tokenPath: 'client_token',
+ displayNamePath: ['metadata.org', 'metadata.username'],
+ },
+];
+
+export function supportedAuthBackends() {
+ return SUPPORTED_AUTH_BACKENDS;
+}
+
+export default Ember.Helper.helper(supportedAuthBackends);
diff --git a/ui/app/helpers/supported-secret-backends.js b/ui/app/helpers/supported-secret-backends.js
new file mode 100644
index 000000000..b2f8dc78e
--- /dev/null
+++ b/ui/app/helpers/supported-secret-backends.js
@@ -0,0 +1,9 @@
+import Ember from 'ember';
+
+const SUPPORTED_SECRET_BACKENDS = ['aws', 'cubbyhole', 'generic', 'kv', 'pki', 'ssh', 'transit'];
+
+export function supportedSecretBackends() {
+ return SUPPORTED_SECRET_BACKENDS;
+}
+
+export default Ember.Helper.helper(supportedSecretBackends);
diff --git a/ui/app/helpers/tabs-for-auth-section.js b/ui/app/helpers/tabs-for-auth-section.js
new file mode 100644
index 000000000..ca83443f9
--- /dev/null
+++ b/ui/app/helpers/tabs-for-auth-section.js
@@ -0,0 +1,61 @@
+import Ember from 'ember';
+
+const TABS_FOR_SETTINGS = {
+ aws: [
+ {
+ label: 'Client',
+ routeParams: ['vault.cluster.settings.auth.configure.section', 'client'],
+ },
+ {
+ label: 'Identity Whitelist Tidy',
+ routeParams: ['vault.cluster.settings.auth.configure.section', 'identity-whitelist'],
+ },
+ {
+ label: 'Role Tag Blacklist Tidy',
+ routeParams: ['vault.cluster.settings.auth.configure.section', 'roletag-blacklist'],
+ },
+ ],
+ github: [
+ {
+ label: 'Configuration',
+ routeParams: ['vault.cluster.settings.auth.configure.section', 'configuration'],
+ },
+ ],
+ gcp: [
+ {
+ label: 'Configuration',
+ routeParams: ['vault.cluster.settings.auth.configure.section', 'configuration'],
+ },
+ ],
+ kubernetes: [
+ {
+ label: 'Configuration',
+ routeParams: ['vault.cluster.settings.auth.configure.section', 'configuration'],
+ },
+ ],
+};
+
+const TABS_FOR_SHOW = {};
+
+export function tabsForAuthSection([methodType, sectionType = 'authSettings']) {
+ let tabs;
+
+ if (sectionType === 'authSettings') {
+ tabs = (TABS_FOR_SETTINGS[methodType] || []).slice();
+ tabs.push({
+ label: 'Method Options',
+ routeParams: ['vault.cluster.settings.auth.configure.section', 'options'],
+ });
+ return tabs;
+ }
+
+ tabs = (TABS_FOR_SHOW[methodType] || []).slice();
+ tabs.push({
+ label: 'Configuration',
+ routeParams: ['vault.cluster.access.method.section', 'configuration'],
+ });
+
+ return tabs;
+}
+
+export default Ember.Helper.helper(tabsForAuthSection);
diff --git a/ui/app/helpers/tabs-for-identity-show.js b/ui/app/helpers/tabs-for-identity-show.js
new file mode 100644
index 000000000..a1c808ca1
--- /dev/null
+++ b/ui/app/helpers/tabs-for-identity-show.js
@@ -0,0 +1,21 @@
+import Ember from 'ember';
+
+export const TABS = {
+ entity: ['details', 'aliases', 'policies', 'groups', 'metadata'],
+ 'entity-alias': ['details', 'metadata'],
+ //group will be used in the model hook of the route
+ group: ['details', 'aliases', 'policies', 'members', 'metadata'],
+ 'group-internal': ['details', 'policies', 'members', 'metadata'],
+ 'group-external': ['details', 'aliases', 'policies', 'members', 'metadata'],
+ 'group-alias': ['details'],
+};
+
+export function tabsForIdentityShow([modelType, groupType]) {
+ let key = modelType;
+ if (groupType) {
+ key = `${key}-${groupType}`;
+ }
+ return TABS[key];
+}
+
+export default Ember.Helper.helper(tabsForIdentityShow);
diff --git a/ui/app/helpers/to-label.js b/ui/app/helpers/to-label.js
new file mode 100644
index 000000000..a1691a795
--- /dev/null
+++ b/ui/app/helpers/to-label.js
@@ -0,0 +1,10 @@
+import Ember from 'ember';
+import { capitalize } from 'vault/helpers/capitalize';
+import { humanize } from 'vault/helpers/humanize';
+import { dasherize } from 'vault/helpers/dasherize';
+
+export function toLabel(val) {
+ return capitalize([humanize([dasherize(val)])]);
+}
+
+export default Ember.Helper.helper(toLabel);
diff --git a/ui/app/helpers/tools-actions.js b/ui/app/helpers/tools-actions.js
new file mode 100644
index 000000000..83b5bae4a
--- /dev/null
+++ b/ui/app/helpers/tools-actions.js
@@ -0,0 +1,9 @@
+import Ember from 'ember';
+
+const TOOLS_ACTIONS = ['wrap', 'lookup', 'unwrap', 'rewrap', 'random', 'hash'];
+
+export function toolsActions() {
+ return TOOLS_ACTIONS;
+}
+
+export default Ember.Helper.helper(toolsActions);
diff --git a/ui/app/index.html b/ui/app/index.html
new file mode 100644
index 000000000..6de475b6b
--- /dev/null
+++ b/ui/app/index.html
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+ Vault
+
+
+ {{content-for "head"}}
+
+
+
+
+ {{content-for "head-footer"}}
+
+
+ {{content-for "body"}}
+
+
+
+
+ {{content-for "body-footer"}}
+
+
diff --git a/ui/app/initializers/disable-ember-inspector.js b/ui/app/initializers/disable-ember-inspector.js
new file mode 100644
index 000000000..7cb22c3bd
--- /dev/null
+++ b/ui/app/initializers/disable-ember-inspector.js
@@ -0,0 +1,11 @@
+import config from '../config/environment';
+
+export default {
+ name: 'ember-inspect-disable',
+ initialize: function() {
+ if (config.environment === 'production') {
+ // disables ember inspector
+ window.NO_EMBER_DEBUG = true;
+ }
+ },
+};
diff --git a/ui/app/lib/key-utils.js b/ui/app/lib/key-utils.js
new file mode 100644
index 000000000..e5c7166b9
--- /dev/null
+++ b/ui/app/lib/key-utils.js
@@ -0,0 +1,47 @@
+function keyIsFolder(key) {
+ return key ? !!key.match(/\/$/) : false;
+}
+
+function keyPartsForKey(key) {
+ if (!key) {
+ return null;
+ }
+ var isFolder = keyIsFolder(key);
+ var parts = key.split('/');
+ if (isFolder) {
+ parts.pop();
+ }
+ return parts.length > 1 ? parts : null;
+}
+
+function parentKeyForKey(key) {
+ var parts = keyPartsForKey(key);
+ if (!parts) {
+ return null;
+ }
+ return parts.slice(0, -1).join('/') + '/';
+}
+
+function keyWithoutParentKey(key) {
+ return key ? key.replace(parentKeyForKey(key), '') : null;
+}
+
+function ancestorKeysForKey(key) {
+ var ancestors = [],
+ parentKey = parentKeyForKey(key);
+
+ while (parentKey) {
+ ancestors.unshift(parentKey);
+ parentKey = parentKeyForKey(parentKey);
+ }
+
+ return ancestors.length ? ancestors : null;
+}
+
+export default {
+ keyIsFolder,
+ keyPartsForKey,
+ parentKeyForKey,
+ keyWithoutParentKey,
+ ancestorKeysForKey,
+};
diff --git a/ui/app/lib/keycodes.js b/ui/app/lib/keycodes.js
new file mode 100644
index 000000000..872468021
--- /dev/null
+++ b/ui/app/lib/keycodes.js
@@ -0,0 +1,11 @@
+// a map of keyCode for use in keyboard event handlers
+export default {
+ ENTER: 13,
+ ESC: 27,
+ TAB: 9,
+ LEFT: 37,
+ UP: 38,
+ RIGHT: 39,
+ DOWN: 40,
+ T: 116,
+};
diff --git a/ui/app/lib/kv-object.js b/ui/app/lib/kv-object.js
new file mode 100644
index 000000000..4ac1fda47
--- /dev/null
+++ b/ui/app/lib/kv-object.js
@@ -0,0 +1,48 @@
+import Ember from 'ember';
+
+export default Ember.ArrayProxy.extend({
+ fromJSON(json) {
+ const contents = Object.keys(json || []).map(key => {
+ let obj = {
+ name: key,
+ value: json[key],
+ };
+ Ember.guidFor(obj);
+ return obj;
+ });
+ this.setObjects(
+ contents.sort((a, b) => {
+ if (a.name === '') {
+ return 1;
+ }
+ if (b.name === '') {
+ return -1;
+ }
+ return a.name.localeCompare(b.name);
+ })
+ );
+ return this;
+ },
+
+ fromJSONString(jsonString) {
+ return this.fromJSON(JSON.parse(jsonString));
+ },
+
+ toJSON(includeBlanks = false) {
+ return this.reduce((obj, item) => {
+ if (!includeBlanks && item.value === '' && item.name === '') {
+ return obj;
+ }
+ obj[item.name || ''] = item.value || '';
+ return obj;
+ }, {});
+ },
+
+ toJSONString(includeBlanks) {
+ return JSON.stringify(this.toJSON(includeBlanks), null, 2);
+ },
+
+ isAdvanced() {
+ return this.any(item => typeof item.value !== 'string');
+ },
+});
diff --git a/ui/app/lib/local-storage.js b/ui/app/lib/local-storage.js
new file mode 100644
index 000000000..86556835c
--- /dev/null
+++ b/ui/app/lib/local-storage.js
@@ -0,0 +1,18 @@
+export default {
+ getItem(key) {
+ var item = window.localStorage.getItem(key);
+ return item && JSON.parse(item);
+ },
+
+ setItem(key, val) {
+ window.localStorage.setItem(key, JSON.stringify(val));
+ },
+
+ removeItem(key) {
+ return window.localStorage.removeItem(key);
+ },
+
+ keys() {
+ return Object.keys(window.localStorage);
+ },
+};
diff --git a/ui/app/lib/memory-storage.js b/ui/app/lib/memory-storage.js
new file mode 100644
index 000000000..b702afd31
--- /dev/null
+++ b/ui/app/lib/memory-storage.js
@@ -0,0 +1,20 @@
+let cache = {};
+
+export default {
+ getItem(key) {
+ var item = cache[key];
+ return item && JSON.parse(item);
+ },
+
+ setItem(key, val) {
+ cache[key] = JSON.stringify(val);
+ },
+
+ removeItem(key) {
+ delete cache[key];
+ },
+
+ keys() {
+ return Object.keys(cache);
+ },
+};
diff --git a/ui/app/lib/token-storage.js b/ui/app/lib/token-storage.js
new file mode 100644
index 000000000..69ee4deb7
--- /dev/null
+++ b/ui/app/lib/token-storage.js
@@ -0,0 +1,16 @@
+import localStorageWrapper from './local-storage';
+import memoryStorage from './memory-storage';
+
+export default function(type) {
+ if (type === 'memory') {
+ return memoryStorage;
+ }
+ let storage;
+ try {
+ window.localStorage.getItem('test');
+ storage = localStorageWrapper;
+ } catch (e) {
+ storage = memoryStorage;
+ }
+ return storage;
+}
diff --git a/ui/app/mixins/backend-crumb.js b/ui/app/mixins/backend-crumb.js
new file mode 100644
index 000000000..8409d60f2
--- /dev/null
+++ b/ui/app/mixins/backend-crumb.js
@@ -0,0 +1,18 @@
+import Ember from 'ember';
+
+export default Ember.Mixin.create({
+ backendCrumb: Ember.computed('backend', function() {
+ const backend = this.get('backend');
+
+ if (backend === undefined) {
+ throw new Error('backend-crumb mixin requires backend to be set');
+ }
+
+ return {
+ label: backend,
+ text: backend,
+ path: 'vault.cluster.secrets.backend.list-root',
+ model: backend,
+ };
+ }),
+});
diff --git a/ui/app/mixins/cluster-route.js b/ui/app/mixins/cluster-route.js
new file mode 100644
index 000000000..e04288ae4
--- /dev/null
+++ b/ui/app/mixins/cluster-route.js
@@ -0,0 +1,67 @@
+import Ember from 'ember';
+
+const { get } = Ember;
+const INIT = 'vault.cluster.init';
+const UNSEAL = 'vault.cluster.unseal';
+const AUTH = 'vault.cluster.auth';
+const CLUSTER = 'vault.cluster';
+const DR_REPLICATION_SECONDARY = 'vault.cluster.replication-dr-promote';
+
+export { INIT, UNSEAL, AUTH, CLUSTER, DR_REPLICATION_SECONDARY };
+
+export default Ember.Mixin.create({
+ auth: Ember.inject.service(),
+
+ transitionToTargetRoute() {
+ const targetRoute = this.targetRouteName();
+ if (targetRoute && targetRoute !== this.routeName) {
+ return this.transitionTo(targetRoute);
+ }
+ return Ember.RSVP.resolve();
+ },
+
+ beforeModel() {
+ return this.transitionToTargetRoute();
+ },
+
+ clusterModel() {
+ return this.modelFor(CLUSTER);
+ },
+
+ authToken() {
+ return get(this, 'auth.currentToken');
+ },
+
+ hasKeyData() {
+ return !!get(this.controllerFor(INIT), 'keyData');
+ },
+
+ targetRouteName() {
+ const cluster = this.clusterModel();
+ const isAuthed = this.authToken();
+ if (get(cluster, 'needsInit')) {
+ return INIT;
+ }
+ if (this.hasKeyData() && this.routeName !== UNSEAL && this.routeName !== AUTH) {
+ return INIT;
+ }
+ if (get(cluster, 'sealed')) {
+ return UNSEAL;
+ }
+ if (get(cluster, 'dr.isSecondary')) {
+ return DR_REPLICATION_SECONDARY;
+ }
+ if (!isAuthed) {
+ return AUTH;
+ }
+ if (
+ (!get(cluster, 'needsInit') && this.routeName === INIT) ||
+ (!get(cluster, 'sealed') && this.routeName === UNSEAL) ||
+ (!get(cluster, 'dr.isSecondary') && this.routeName === DR_REPLICATION_SECONDARY) ||
+ (isAuthed && this.routeName === AUTH)
+ ) {
+ return CLUSTER;
+ }
+ return null;
+ },
+});
diff --git a/ui/app/mixins/focus-on-insert.js b/ui/app/mixins/focus-on-insert.js
new file mode 100644
index 000000000..97f4871e1
--- /dev/null
+++ b/ui/app/mixins/focus-on-insert.js
@@ -0,0 +1,28 @@
+import Ember from 'ember';
+
+export default Ember.Mixin.create({
+ // selector passed to `this.$()` to find the element to focus
+ // defaults to `'input'`
+ focusOnInsertSelector: null,
+ shouldFocus: true,
+
+ // uses Ember.on so that we don't have to worry about calling _super if
+ // didInsertElement is overridden
+ focusOnInsert: Ember.on('didInsertElement', function() {
+ Ember.run.schedule('afterRender', this, 'focusOnInsertFocus');
+ }),
+
+ focusOnInsertFocus() {
+ if (this.get('shouldFocus') === false) {
+ return;
+ }
+ this.forceFocus();
+ },
+
+ forceFocus() {
+ var $selector = this.$(this.get('focusOnInsertSelector') || 'input').first();
+ if (!$selector.is(':focus')) {
+ $selector.focus();
+ }
+ },
+});
diff --git a/ui/app/mixins/list-controller.js b/ui/app/mixins/list-controller.js
new file mode 100644
index 000000000..039d24dc1
--- /dev/null
+++ b/ui/app/mixins/list-controller.js
@@ -0,0 +1,42 @@
+import Ember from 'ember';
+
+export default Ember.Mixin.create({
+ queryParams: {
+ page: 'page',
+ pageFilter: 'pageFilter',
+ },
+
+ page: 1,
+ pageFilter: null,
+ filter: null,
+
+ isLoading: false,
+
+ filterMatchesKey: Ember.computed('filter', 'model', 'model.[]', function() {
+ var filter = this.get('filter');
+ var content = this.get('model');
+ return !!(content.length && content.findBy('id', filter));
+ }),
+
+ firstPartialMatch: Ember.computed('filter', 'model', 'model.[]', 'filterMatchesKey', function() {
+ var filter = this.get('filter');
+ var content = this.get('model');
+ var filterMatchesKey = this.get('filterMatchesKey');
+ var re = new RegExp('^' + filter);
+ return filterMatchesKey
+ ? null
+ : content.find(function(key) {
+ return re.test(key.get('id'));
+ });
+ }),
+
+ actions: {
+ setFilter(val) {
+ this.set('filter', val);
+ },
+
+ setFilterFocus(bool) {
+ this.set('filterFocused', bool);
+ },
+ },
+});
diff --git a/ui/app/mixins/list-route.js b/ui/app/mixins/list-route.js
new file mode 100644
index 000000000..16e5248d4
--- /dev/null
+++ b/ui/app/mixins/list-route.js
@@ -0,0 +1,12 @@
+import Ember from 'ember';
+
+export default Ember.Mixin.create({
+ queryParams: {
+ page: {
+ refreshModel: true,
+ },
+ pageFilter: {
+ refreshModel: true,
+ },
+ },
+});
diff --git a/ui/app/mixins/model-boundary-route.js b/ui/app/mixins/model-boundary-route.js
new file mode 100644
index 000000000..d38dc8a08
--- /dev/null
+++ b/ui/app/mixins/model-boundary-route.js
@@ -0,0 +1,58 @@
+// meant for use mixed-in to a Route file
+//
+// When a route is deactivated, this mixin clears the Ember Data store of
+// models of type specified by the required param `modelType`.
+//
+// example:
+// Using this as with a modelType of `datacenter` on the infrastructure
+// route will cause all `datacenter` models to get unloaded when the
+// infrastructure route is navigated away from.
+
+import Ember from 'ember';
+
+export default Ember.Mixin.create({
+ modelType: null,
+ modelTypes: null,
+
+ verifyProps: Ember.on('init', function() {
+ var modelType = this.get('modelType');
+ var modelTypes = this.get('modelTypes');
+ Ember.warn(
+ 'No `modelType` or `modelTypes` specified for `' +
+ this.toString() +
+ '`. Check to make sure you still need to use the `model-boundary-route` mixin.',
+ Ember.isPresent(modelType) || Ember.isPresent(modelTypes),
+ { id: 'model-boundary-init' }
+ );
+
+ Ember.warn(
+ 'Expected `model-boundary-route` to be used on an Ember.Route, not `' + this.toString() + '`.',
+ this instanceof Ember.Route,
+ { id: 'mode-boundary-is-route' }
+ );
+ }),
+
+ clearModelCache: Ember.on('deactivate', function() {
+ var modelType = this.get('modelType');
+ var modelTypes = this.get('modelTypes');
+
+ if (!modelType && !modelTypes) {
+ Ember.warn(
+ 'Attempted to clear store clear store cache when leaving `' +
+ this.routeName +
+ '`, but no `modelType` or `modelTypes` was specified.',
+ Ember.isPresent(modelType),
+ { id: 'model-boundary-clear' }
+ );
+ return;
+ }
+ if (modelType) {
+ this.store.unloadAll(modelType);
+ }
+ if (modelTypes) {
+ modelTypes.forEach(type => {
+ this.store.unloadAll(type);
+ });
+ }
+ }),
+});
diff --git a/ui/app/mixins/policy-edit-controller.js b/ui/app/mixins/policy-edit-controller.js
new file mode 100644
index 000000000..69f425f74
--- /dev/null
+++ b/ui/app/mixins/policy-edit-controller.js
@@ -0,0 +1,40 @@
+import Ember from 'ember';
+
+let { inject } = Ember;
+
+export default Ember.Mixin.create({
+ flashMessages: inject.service(),
+ actions: {
+ deletePolicy(model) {
+ let policyType = model.get('policyType');
+ let name = model.get('name');
+ let flash = this.get('flashMessages');
+ model
+ .destroyRecord()
+ .then(() => {
+ flash.success(`${policyType.toUpperCase()} policy "${name}" was successfully deleted.`);
+ return this.transitionToRoute('vault.cluster.policies', policyType);
+ })
+ .catch(e => {
+ let errors = e.errors ? e.errors.join('') : e.message;
+ flash.danger(
+ `There was an error deleting the ${policyType.toUpperCase()} policy "${name}": ${errors}.`
+ );
+ });
+ },
+
+ savePolicy(model) {
+ let flash = this.get('flashMessages');
+ let policyType = model.get('policyType');
+ let name = model.get('name');
+ model.save().then(m => {
+ flash.success(`${policyType.toUpperCase()} policy "${name}" was successfully saved.`);
+ return this.transitionToRoute('vault.cluster.policy.show', m.get('policyType'), m.get('name'));
+ });
+ },
+
+ setModelName(model, e) {
+ model.set('name', e.target.value.toLowerCase());
+ },
+ },
+});
diff --git a/ui/app/mixins/replication-actions.js b/ui/app/mixins/replication-actions.js
new file mode 100644
index 000000000..0fe0466b2
--- /dev/null
+++ b/ui/app/mixins/replication-actions.js
@@ -0,0 +1,101 @@
+import Ember from 'ember';
+const { inject, computed } = Ember;
+
+export default Ember.Mixin.create({
+ store: inject.service(),
+ routing: inject.service('-routing'),
+ router: computed.alias('routing.router'),
+ submitHandler(action, clusterMode, data, event) {
+ let replicationMode = (data && data.replicationMode) || this.get('replicationMode');
+ if (event && event.preventDefault) {
+ event.preventDefault();
+ }
+ this.setProperties({
+ loading: true,
+ errors: [],
+ });
+ if (data) {
+ data = Object.keys(data).reduce((newData, key) => {
+ var val = data[key];
+ if (Ember.isPresent(val)) {
+ newData[key] = val;
+ }
+ return newData;
+ }, {});
+ delete data.replicationMode;
+ }
+
+ return this.get('store')
+ .adapterFor('cluster')
+ .replicationAction(action, replicationMode, clusterMode, data)
+ .then(
+ resp => {
+ return this.submitSuccess(resp, action, clusterMode);
+ },
+ (...args) => this.submitError(...args)
+ );
+ },
+
+ submitSuccess(resp, action, mode) {
+ const cluster = this.get('cluster');
+ const replicationMode = this.get('selectedReplicationMode') || this.get('replicationMode');
+ const store = this.get('store');
+ if (!cluster) {
+ return;
+ }
+
+ if (resp && resp.wrap_info) {
+ this.set('token', resp.wrap_info.token);
+ }
+ if (action === 'secondary-token') {
+ this.setProperties({
+ loading: false,
+ primary_api_addr: null,
+ primary_cluster_addr: null,
+ });
+ return cluster;
+ }
+ this.reset();
+ if (action === 'enable') {
+ // do something to show model is pending
+ cluster.set(
+ replicationMode,
+ store.createFragment('replication-attributes', {
+ mode: 'bootstrapping',
+ })
+ );
+ if (mode === 'secondary' && replicationMode === 'performance') {
+ // if we're enabing a secondary, there could be mount filtering,
+ // so we should unload all of the backends
+ store.unloadAll('secret-engine');
+ }
+ }
+ const router = this.get('router');
+ if (action === 'disable') {
+ return router.transitionTo.call(router, 'vault.cluster.replication.mode', replicationMode);
+ }
+ return cluster
+ .reload()
+ .then(() => {
+ cluster.rollbackAttributes();
+ if (action === 'enable') {
+ return router.transitionTo.call(router, 'vault.cluster.replication.mode', replicationMode);
+ }
+
+ if (mode === 'secondary' && replicationMode === 'dr') {
+ return router.transitionTo.call(router, 'vault.cluster');
+ }
+ })
+ .finally(() => {
+ this.set('loading', false);
+ });
+ },
+
+ submitError(e) {
+ if (e.errors) {
+ this.set('errors', e.errors);
+ } else {
+ throw e;
+ }
+ },
+});
diff --git a/ui/app/mixins/unload-model-route.js b/ui/app/mixins/unload-model-route.js
new file mode 100644
index 000000000..87a397edd
--- /dev/null
+++ b/ui/app/mixins/unload-model-route.js
@@ -0,0 +1,22 @@
+import Ember from 'ember';
+
+// removes Ember Data records from the cache when the model
+// changes or you move away from the current route
+export default Ember.Mixin.create({
+ modelPath: 'model',
+ unloadModel() {
+ const model = this.controller.get(this.get('modelPath'));
+ if (!model || !model.unloadRecord) {
+ return;
+ }
+ this.store.unloadRecord(model);
+ model.destroy();
+ },
+
+ actions: {
+ willTransition() {
+ this.unloadModel();
+ return true;
+ },
+ },
+});
diff --git a/ui/app/mixins/unsaved-model-route.js b/ui/app/mixins/unsaved-model-route.js
new file mode 100644
index 000000000..89f5c2297
--- /dev/null
+++ b/ui/app/mixins/unsaved-model-route.js
@@ -0,0 +1,28 @@
+import Ember from 'ember';
+const { get } = Ember;
+
+// this mixin relies on `unload-model-route` also being used
+export default Ember.Mixin.create({
+ actions: {
+ willTransition(transition) {
+ const model = this.controller.get('model');
+ if (!model) {
+ return true;
+ }
+ if (get(model, 'hasDirtyAttributes')) {
+ if (
+ window.confirm(
+ 'You have unsaved changes. Navigating away will discard these changes. Are you sure you want to discard your changes?'
+ )
+ ) {
+ this.unloadModel();
+ return true;
+ } else {
+ transition.abort();
+ return false;
+ }
+ }
+ return true;
+ },
+ },
+});
diff --git a/ui/app/models/.gitkeep b/ui/app/models/.gitkeep
new file mode 100644
index 000000000..e69de29bb
diff --git a/ui/app/models/auth-config.js b/ui/app/models/auth-config.js
new file mode 100644
index 000000000..88befe73b
--- /dev/null
+++ b/ui/app/models/auth-config.js
@@ -0,0 +1,6 @@
+import DS from 'ember-data';
+const { belongsTo } = DS;
+
+export default DS.Model.extend({
+ backend: belongsTo('auth-method', { readOnly: true, async: false }),
+});
diff --git a/ui/app/models/auth-config/approle.js b/ui/app/models/auth-config/approle.js
new file mode 100644
index 000000000..66657e064
--- /dev/null
+++ b/ui/app/models/auth-config/approle.js
@@ -0,0 +1,2 @@
+import AuthConfig from '../auth-config';
+export default AuthConfig.extend({});
diff --git a/ui/app/models/auth-config/aws/client.js b/ui/app/models/auth-config/aws/client.js
new file mode 100644
index 000000000..0bb13bb96
--- /dev/null
+++ b/ui/app/models/auth-config/aws/client.js
@@ -0,0 +1,33 @@
+import Ember from 'ember';
+import DS from 'ember-data';
+import AuthConfig from '../../auth-config';
+import fieldToAttrs from 'vault/utils/field-to-attrs';
+
+const { attr } = DS;
+const { computed } = Ember;
+
+export default AuthConfig.extend({
+ secretKey: attr('string'),
+ accessKey: attr('string'),
+ endpoint: attr('string', {
+ label: 'EC2 Endpoint',
+ }),
+ iamEndpoint: attr('string', {
+ label: 'IAM Endpoint',
+ }),
+ stsEndpoint: attr('string', {
+ label: 'STS Endpoint',
+ }),
+ iamServerIdHeaderValue: attr('string', {
+ label: 'IAM Server ID Header Value',
+ }),
+
+ fieldGroups: computed(function() {
+ const groups = [
+ { default: ['accessKey', 'secretKey'] },
+ { 'AWS Options': ['endpoint', 'iamEndpoint', 'stsEndpoint', 'iamServerIdHeaderValue'] },
+ ];
+
+ return fieldToAttrs(this, groups);
+ }),
+});
diff --git a/ui/app/models/auth-config/aws/identity-whitelist.js b/ui/app/models/auth-config/aws/identity-whitelist.js
new file mode 100644
index 000000000..a9c065da7
--- /dev/null
+++ b/ui/app/models/auth-config/aws/identity-whitelist.js
@@ -0,0 +1,2 @@
+import Tidy from './tidy';
+export default Tidy.extend();
diff --git a/ui/app/models/auth-config/aws/roletag-blacklist.js b/ui/app/models/auth-config/aws/roletag-blacklist.js
new file mode 100644
index 000000000..a9c065da7
--- /dev/null
+++ b/ui/app/models/auth-config/aws/roletag-blacklist.js
@@ -0,0 +1,2 @@
+import Tidy from './tidy';
+export default Tidy.extend();
diff --git a/ui/app/models/auth-config/aws/tidy.js b/ui/app/models/auth-config/aws/tidy.js
new file mode 100644
index 000000000..daf3949e4
--- /dev/null
+++ b/ui/app/models/auth-config/aws/tidy.js
@@ -0,0 +1,22 @@
+import DS from 'ember-data';
+import Ember from 'ember';
+import { expandAttributeMeta } from 'vault/utils/field-to-attrs';
+import AuthConfig from '../../auth-config';
+
+const { attr } = DS;
+const { computed } = Ember;
+
+export default AuthConfig.extend({
+ safetyBuffer: attr({
+ defaultValue: '72h',
+ editType: 'ttl',
+ }),
+
+ disablePeriodicTidy: attr('boolean', {
+ defaultValue: false,
+ }),
+
+ attrs: computed(function() {
+ return expandAttributeMeta(this, ['safetyBuffer', 'disablePeriodicTidy']);
+ }),
+});
diff --git a/ui/app/models/auth-config/cert.js b/ui/app/models/auth-config/cert.js
new file mode 100644
index 000000000..66657e064
--- /dev/null
+++ b/ui/app/models/auth-config/cert.js
@@ -0,0 +1,2 @@
+import AuthConfig from '../auth-config';
+export default AuthConfig.extend({});
diff --git a/ui/app/models/auth-config/gcp.js b/ui/app/models/auth-config/gcp.js
new file mode 100644
index 000000000..6d306e4f0
--- /dev/null
+++ b/ui/app/models/auth-config/gcp.js
@@ -0,0 +1,26 @@
+import Ember from 'ember';
+import DS from 'ember-data';
+
+import AuthConfig from '../auth-config';
+import fieldToAttrs from 'vault/utils/field-to-attrs';
+
+const { attr } = DS;
+const { computed } = Ember;
+
+export default AuthConfig.extend({
+ credentials: attr('string', {
+ editType: 'file',
+ }),
+
+ googleCertsEndpoint: attr('string'),
+
+ fieldGroups: computed(function() {
+ const groups = [
+ { default: ['credentials'] },
+ {
+ 'Google Cloud Options': ['googleCertsEndpoint'],
+ },
+ ];
+ return fieldToAttrs(this, groups);
+ }),
+});
diff --git a/ui/app/models/auth-config/github.js b/ui/app/models/auth-config/github.js
new file mode 100644
index 000000000..4ffa743b8
--- /dev/null
+++ b/ui/app/models/auth-config/github.js
@@ -0,0 +1,26 @@
+import Ember from 'ember';
+import DS from 'ember-data';
+
+import AuthConfig from '../auth-config';
+import fieldToAttrs from 'vault/utils/field-to-attrs';
+
+const { attr } = DS;
+const { computed } = Ember;
+
+export default AuthConfig.extend({
+ organization: attr('string'),
+ baseUrl: attr('string', {
+ label: 'Base URL',
+ }),
+
+ fieldGroups: computed(function() {
+ const groups = [
+ { default: ['organization'] },
+ {
+ 'GitHub Options': ['baseUrl'],
+ },
+ ];
+
+ return fieldToAttrs(this, groups);
+ }),
+});
diff --git a/ui/app/models/auth-config/kubernetes.js b/ui/app/models/auth-config/kubernetes.js
new file mode 100644
index 000000000..f7a40214e
--- /dev/null
+++ b/ui/app/models/auth-config/kubernetes.js
@@ -0,0 +1,45 @@
+import Ember from 'ember';
+import DS from 'ember-data';
+
+import AuthConfig from '../auth-config';
+import fieldToAttrs from 'vault/utils/field-to-attrs';
+
+const { attr } = DS;
+const { computed } = Ember;
+
+export default AuthConfig.extend({
+ kubernetesHost: attr('string', {
+ label: 'Kubernetes Host',
+ helpText:
+ 'Host must be a host string, a host:port pair, or a URL to the base of the Kubernetes API server',
+ }),
+
+ kubernetesCaCert: attr('string', {
+ label: 'Kubernetes CA Certificate',
+ editType: 'file',
+ helpText: 'PEM encoded CA cert for use by the TLS client used to talk with the Kubernetes API',
+ }),
+
+ tokenReviewerJwt: attr('string', {
+ label: 'Token Reviewer JWT',
+ helpText:
+ 'A service account JWT used to access the TokenReview API to validate other JWTs during login. If not set the JWT used for login will be used to access the API',
+ }),
+
+ pemKeys: attr({
+ label: 'Service account verification keys',
+ editType: 'stringArray',
+ }),
+
+ fieldGroups: computed(function() {
+ const groups = [
+ {
+ default: ['kubernetesHost', 'kubernetesCaCert'],
+ },
+ {
+ 'Kubernetes Options': ['tokenReviewerJwt', 'pemKeys'],
+ },
+ ];
+ return fieldToAttrs(this, groups);
+ }),
+});
diff --git a/ui/app/models/auth-config/ldap.js b/ui/app/models/auth-config/ldap.js
new file mode 100644
index 000000000..d2db7b180
--- /dev/null
+++ b/ui/app/models/auth-config/ldap.js
@@ -0,0 +1,116 @@
+import Ember from 'ember';
+import DS from 'ember-data';
+
+import AuthConfig from '../auth-config';
+import fieldToAttrs from 'vault/utils/field-to-attrs';
+
+const { attr } = DS;
+const { computed } = Ember;
+
+export default AuthConfig.extend({
+ url: attr('string', {
+ label: 'URL',
+ }),
+ starttls: attr('boolean', {
+ defaultValue: false,
+ label: 'Issue StartTLS command after establishing an unencrypted connection',
+ }),
+ tlsMinVersion: attr('string', {
+ label: 'Minimum TLS Version',
+ defaultValue: 'tls12',
+ possibleValues: ['tls10', 'tls11', 'tls12'],
+ }),
+
+ tlsMaxVersion: attr('string', {
+ label: 'Maximum TLS Version',
+ defaultValue: 'tls12',
+ possibleValues: ['tls10', 'tls11', 'tls12'],
+ }),
+ insecureTls: attr('boolean', {
+ defaultValue: false,
+ label: 'Skip LDAP server SSL certificate verification',
+ }),
+ certificate: attr('string', {
+ label: 'CA certificate to verify LDAP server certificate',
+ editType: 'file',
+ }),
+
+ binddn: attr('string', {
+ label: 'Name of Object to bind (binddn)',
+ helpText: 'Used when performing user search. Example: cn=vault,ou=Users,dc=example,dc=com',
+ }),
+ bindpass: attr('string', {
+ label: 'Password',
+ helpText: 'Used along with binddn when performing user search',
+ }),
+
+ userdn: attr('string', {
+ label: 'User DN',
+ helpText: 'Base DN under which to perform user search. Example: ou=Users,dc=example,dc=com',
+ }),
+ userattr: attr('string', {
+ label: 'User Attribute',
+ defaultValue: 'cn',
+ helpText:
+ 'Attribute on user attribute object matching the username passed when authenticating. Examples: sAMAccountName, cn, uid',
+ }),
+ discoverdn: attr('boolean', {
+ defaultValue: false,
+ label: 'Use anonymous bind to discover the bind DN of a user',
+ }),
+ denyNullBind: attr('boolean', {
+ defaultValue: true,
+ label: 'Prevent users from bypassing authentication when providing an empty password',
+ }),
+ upndomain: attr('string', {
+ label: 'User Principal (UPN) Domain',
+ helpText:
+ 'The userPrincipalDomain used to construct the UPN string for the authenticating user. The constructed UPN will appear as [username]@UPNDomain. Example: example.com, which will cause vault to bind as username@example.com.',
+ }),
+
+ groupfilter: attr('string', {
+ label: 'Group Filter',
+ helpText:
+ 'Go template used when constructing the group membership query. The template can access the following context variables: [UserDN, Username]. The default is (|(memberUid={{.Username}})(member={{.UserDN}})(uniqueMember={{.UserDN}})), which is compatible with several common directory schemas. To support nested group resolution for Active Directory, instead use the following query: (&(objectClass=group)(member:1.2.840.113556.1.4.1941:={{.UserDN}}))',
+ }),
+ groupdn: attr('string', {
+ label: 'Group DN',
+ helpText:
+ 'LDAP search base for group membership search. This can be the root containing either groups or users. Example: ou=Groups,dc=example,dc=com',
+ }),
+ groupattr: attr('string', {
+ label: 'Group Attribute',
+ defaultValue: 'cn',
+
+ helpText:
+ 'LDAP attribute to follow on objects returned by groupfilter in order to enumerate user group membership. Examples: for groupfilter queries returning group objects, use: cn. For queries returning user objects, use: memberOf. The default is cn.',
+ }),
+
+ fieldGroups: computed(function() {
+ const groups = [
+ {
+ default: ['url'],
+ },
+ {
+ 'LDAP Options': [
+ 'starttls',
+ 'insecureTls',
+ 'discoverdn',
+ 'denyNullBind',
+ 'tlsMinVersion',
+ 'tlsMaxVersion',
+ 'certificate',
+ 'userattr',
+ 'upndomain',
+ ],
+ },
+ {
+ 'Customize User Search': ['binddn', 'userdn', 'bindpass'],
+ },
+ {
+ 'Customize Group Membership Search': ['groupfilter', 'groupattr', 'groupdn'],
+ },
+ ];
+ return fieldToAttrs(this, groups);
+ }),
+});
diff --git a/ui/app/models/auth-config/okta.js b/ui/app/models/auth-config/okta.js
new file mode 100644
index 000000000..969db3d91
--- /dev/null
+++ b/ui/app/models/auth-config/okta.js
@@ -0,0 +1,41 @@
+import Ember from 'ember';
+import DS from 'ember-data';
+import AuthConfig from '../auth-config';
+import fieldToAttrs from 'vault/utils/field-to-attrs';
+
+const { attr } = DS;
+const { computed } = Ember;
+
+export default AuthConfig.extend({
+ orgName: attr('string', {
+ label: 'Organization Name',
+ helpText: 'Name of the organization to be used in the Okta API',
+ }),
+ apiToken: attr('string', {
+ label: 'API Token',
+ helpText:
+ 'Okta API token. This is required to query Okta for user group membership. If this is not supplied only locally configured groups will be enabled.',
+ }),
+ baseUrl: attr('string', {
+ label: 'Base URL',
+ helpText:
+ 'If set, will be used as the base domain for API requests. Examples are okta.com, oktapreview.com, and okta-emea.com',
+ }),
+ bypassOktaMfa: attr('boolean', {
+ defaultValue: false,
+ label: 'Bypass Okta MFA',
+ helpText:
+ "Useful if Vault's built-in MFA mechanisms. Will also cause certain other statuses to be ignored, such as PASSWORD_EXPIRED",
+ }),
+ fieldGroups: computed(function() {
+ const groups = [
+ {
+ default: ['orgName'],
+ },
+ {
+ Options: ['apiToken', 'baseUrl', 'bypassOktaMfa'],
+ },
+ ];
+ return fieldToAttrs(this, groups);
+ }),
+});
diff --git a/ui/app/models/auth-config/radius.js b/ui/app/models/auth-config/radius.js
new file mode 100644
index 000000000..5dc9addc6
--- /dev/null
+++ b/ui/app/models/auth-config/radius.js
@@ -0,0 +1,42 @@
+import Ember from 'ember';
+import DS from 'ember-data';
+import AuthConfig from '../auth-config';
+import fieldToAttrs from 'vault/utils/field-to-attrs';
+
+const { attr } = DS;
+const { computed } = Ember;
+
+export default AuthConfig.extend({
+ host: attr('string'),
+
+ port: attr('number', {
+ defaultValue: 1812,
+ }),
+
+ secret: attr('string'),
+
+ unregisteredUserPolicies: attr('string', {
+ label: 'Policies for unregistered users',
+ }),
+
+ dialTimeout: attr('number', {
+ defaultValue: 10,
+ }),
+
+ nasPort: attr('number', {
+ defaultValue: 10,
+ label: 'NAS Port',
+ }),
+
+ fieldGroups: computed(function() {
+ const groups = [
+ {
+ default: ['host', 'secret'],
+ },
+ {
+ Options: ['port', 'nasPort', 'dialTimeout', 'unregisteredUserPolicies'],
+ },
+ ];
+ return fieldToAttrs(this, groups);
+ }),
+});
diff --git a/ui/app/models/auth-config/userpass.js b/ui/app/models/auth-config/userpass.js
new file mode 100644
index 000000000..66657e064
--- /dev/null
+++ b/ui/app/models/auth-config/userpass.js
@@ -0,0 +1,2 @@
+import AuthConfig from '../auth-config';
+export default AuthConfig.extend({});
diff --git a/ui/app/models/auth-method.js b/ui/app/models/auth-method.js
new file mode 100644
index 000000000..adeb1f931
--- /dev/null
+++ b/ui/app/models/auth-method.js
@@ -0,0 +1,113 @@
+import Ember from 'ember';
+import DS from 'ember-data';
+import { fragment } from 'ember-data-model-fragments/attributes';
+import { queryRecord } from 'ember-computed-query';
+import { methods } from 'vault/helpers/mountable-auth-methods';
+import fieldToAttrs, { expandAttributeMeta } from 'vault/utils/field-to-attrs';
+import { memberAction } from 'ember-api-actions';
+
+const { attr, hasMany } = DS;
+const { computed } = Ember;
+
+const METHODS = methods();
+
+const configPath = function configPath(strings, key) {
+ return function(...values) {
+ return `${strings[0]}${values[key]}${strings[1]}`;
+ };
+};
+export default DS.Model.extend({
+ authConfigs: hasMany('auth-config', { polymorphic: true, inverse: 'backend', async: false }),
+ path: attr('string', {
+ defaultValue: METHODS[0].value,
+ }),
+ accessor: attr('string'),
+ name: attr('string'),
+ type: attr('string', {
+ defaultValue: METHODS[0].value,
+ possibleValues: METHODS,
+ }),
+ description: attr('string', {
+ editType: 'textarea',
+ }),
+ config: fragment('mount-config', { defaultValue: {} }),
+ local: attr('boolean'),
+ sealWrap: attr('boolean'),
+
+ // used when the `auth` prefix is important,
+ // currently only when setting perf mount filtering
+ apiPath: computed('path', function() {
+ return `auth/${this.get('path')}`;
+ }),
+ localDisplay: computed('local', function() {
+ return this.get('local') ? 'local' : 'replicated';
+ }),
+
+ tuneAttrs: computed(function() {
+ return expandAttributeMeta(this, ['description', 'config.{defaultLeaseTtl,maxLeaseTtl}']);
+ }),
+
+ //sys/mounts/auth/[auth-path]/tune.
+ tune: memberAction({
+ path: 'tune',
+ type: 'post',
+ urlType: 'updateRecord',
+ }),
+
+ formFields: [
+ 'type',
+ 'path',
+ 'description',
+ 'accessor',
+ 'local',
+ 'sealWrap',
+ 'config.{defaultLeaseTtl,maxLeaseTtl}',
+ ],
+
+ formFieldGroups: [
+ { default: ['type', 'path'] },
+ { 'Method Options': ['description', 'local', 'sealWrap', 'config.{defaultLeaseTtl,maxLeaseTtl}'] },
+ ],
+
+ attrs: computed('formFields', function() {
+ return expandAttributeMeta(this, this.get('formFields'));
+ }),
+
+ fieldGroups: computed('formFieldGroups', function() {
+ return fieldToAttrs(this, this.get('formFieldGroups'));
+ }),
+
+ configPathTmpl: computed('type', function() {
+ const type = this.get('type');
+ if (type === 'aws') {
+ return configPath`auth/${0}/config/client`;
+ } else {
+ return configPath`auth/${0}/config`;
+ }
+ }),
+
+ configPath: queryRecord(
+ 'capabilities',
+ context => {
+ const { id, configPathTmpl } = context.getProperties('id', 'configPathTmpl');
+ return {
+ id: configPathTmpl(id),
+ };
+ },
+ 'id',
+ 'configPathTmpl'
+ ),
+ deletePath: queryRecord(
+ 'capabilities',
+ context => {
+ const { id } = context.get('id');
+ return {
+ id: `sys/auth/${id}`,
+ };
+ },
+ 'id'
+ ),
+ canDisable: computed.alias('deletePath.canDelete'),
+
+ canEdit: computed.alias('configPath.canUpdate'),
+});
diff --git a/ui/app/models/capabilities.js b/ui/app/models/capabilities.js
new file mode 100644
index 000000000..72af44e83
--- /dev/null
+++ b/ui/app/models/capabilities.js
@@ -0,0 +1,54 @@
+// This model represents the capabilities on a given `path`
+// `path` is also the primaryId
+// https://www.vaultproject.io/docs/concepts/policies.html#capabilities
+
+import Ember from 'ember';
+import DS from 'ember-data';
+
+const { attr } = DS;
+
+const SUDO_PATHS = [
+ 'sys/seal',
+ 'sys/replication/performance/primary/secondary-token',
+ 'sys/replication/dr/primary/secondary-token',
+ 'sys/replication/reindex',
+ 'sys/leases/lookup/',
+];
+
+const SUDO_PATH_PREFIXES = ['sys/leases/revoke-prefix', 'sys/leases/revoke-force'];
+
+export { SUDO_PATHS, SUDO_PATH_PREFIXES };
+
+const computedCapability = function(capability) {
+ return Ember.computed('path', 'capabilities', 'capabilities.[]', function() {
+ const capabilities = this.get('capabilities');
+ const path = this.get('path');
+ if (!capabilities) {
+ return false;
+ }
+ if (capabilities.includes('root')) {
+ return true;
+ }
+ if (capabilities.includes('deny')) {
+ return false;
+ }
+ // if the path is sudo protected, they'll need sudo + the appropriate capability
+ if (SUDO_PATHS.includes(path) || SUDO_PATH_PREFIXES.find(item => item.startsWith(path))) {
+ return capabilities.includes('sudo') && capabilities.includes(capability);
+ }
+ return capabilities.includes(capability);
+ });
+};
+
+export default DS.Model.extend({
+ path: attr('string'),
+ capabilities: attr('array'),
+ canSudo: computedCapability('sudo'),
+ canRead: computedCapability('read'),
+ canCreate: computedCapability('create'),
+ canUpdate: computedCapability('update'),
+ canDelete: computedCapability('delete'),
+ canList: computedCapability('list'),
+ allowedParameters: attr(),
+ deniedParameters: attr(),
+});
diff --git a/ui/app/models/cluster.js b/ui/app/models/cluster.js
new file mode 100644
index 000000000..673e9f91f
--- /dev/null
+++ b/ui/app/models/cluster.js
@@ -0,0 +1,103 @@
+import Ember from 'ember';
+import DS from 'ember-data';
+import { fragment } from 'ember-data-model-fragments/attributes';
+const { hasMany, attr } = DS;
+const { computed, get, inject } = Ember;
+const { alias, gte, not } = computed;
+
+export default DS.Model.extend({
+ version: inject.service(),
+
+ nodes: hasMany('nodes', { async: false }),
+ name: attr('string'),
+ status: attr('string'),
+
+ needsInit: computed('nodes', 'nodes.[]', function() {
+ // needs init if no nodes are initialized
+ return this.get('nodes').isEvery('initialized', false);
+ }),
+
+ type: computed(function() {
+ return this.constructor.modelName;
+ }),
+
+ unsealed: computed('nodes', 'nodes.[]', 'nodes.@each.sealed', function() {
+ // unsealed if there's at least one unsealed node
+ return !!this.get('nodes').findBy('sealed', false);
+ }),
+
+ sealed: not('unsealed'),
+
+ leaderNode: computed('nodes', 'nodes.[]', function() {
+ const nodes = this.get('nodes');
+ if (nodes.get('length') === 1) {
+ return nodes.get('firstObject');
+ } else {
+ return nodes.findBy('isLeader');
+ }
+ }),
+
+ sealThreshold: alias('leaderNode.sealThreshold'),
+ sealProgress: alias('leaderNode.progress'),
+ hasProgress: gte('sealProgress', 1),
+
+ //replication mode - will only ever be 'unsupported'
+ //otherwise the particular mode will have the relevant mode attr through replication-attributes
+ mode: attr('string'),
+ allReplicationDisabled: computed.and('{dr,performance}.replicationDisabled'),
+
+ anyReplicationEnabled: computed.or('{dr,performance}.replicationEnabled'),
+
+ stateDisplay(state) {
+ if (!state) {
+ return null;
+ }
+ const defaultDisp = 'Synced';
+ const displays = {
+ 'stream-wals': 'Streaming',
+ 'merkle-diff': 'Determining sync status',
+ 'merkle-sync': 'Syncing',
+ };
+
+ return displays[state] || defaultDisp;
+ },
+
+ drStateDisplay: computed('dr.state', function() {
+ return this.stateDisplay(this.get('dr.state'));
+ }),
+
+ performanceStateDisplay: computed('performance.state', function() {
+ return this.stateDisplay(this.get('performance.state'));
+ }),
+
+ stateGlyph(state) {
+ const glyph = 'checkmark';
+
+ const glyphs = {
+ 'stream-wals': 'android-sync',
+ 'merkle-diff': 'android-sync',
+ 'merkle-sync': null,
+ };
+
+ return glyphs[state] || glyph;
+ },
+
+ drStateGlyph: computed('dr.state', function() {
+ return this.stateGlyph(this.get('dr.state'));
+ }),
+
+ performanceStateGlyph: computed('performance.state', function() {
+ return this.stateGlyph(this.get('performance.state'));
+ }),
+
+ dr: fragment('replication-attributes'),
+ performance: fragment('replication-attributes'),
+ // this service exposes what mode the UI is currently viewing
+ // replicationAttrs will then return the relevant `replication-attributes` fragment
+ rm: Ember.inject.service('replication-mode'),
+ replicationMode: computed.alias('rm.mode'),
+ replicationAttrs: computed('dr.mode', 'performance.mode', 'replicationMode', function() {
+ const replicationMode = this.get('replicationMode');
+ return replicationMode ? get(this, replicationMode) : null;
+ }),
+});
diff --git a/ui/app/models/iam-credential.js b/ui/app/models/iam-credential.js
new file mode 100644
index 000000000..420065e1d
--- /dev/null
+++ b/ui/app/models/iam-credential.js
@@ -0,0 +1,55 @@
+import DS from 'ember-data';
+import Ember from 'ember';
+const { attr } = DS;
+const { computed, get } = Ember;
+const CREATE_FIELDS = ['ttl'];
+
+const DISPLAY_FIELDS = ['accessKey', 'secretKey', 'securityToken', 'leaseId', 'renewable', 'leaseDuration'];
+export default DS.Model.extend({
+ role: attr('object', {
+ readOnly: true,
+ }),
+
+ withSTS: attr('boolean', {
+ readOnly: true,
+ }),
+
+ ttl: attr({
+ editType: 'ttl',
+ defaultValue: '1h',
+ }),
+ leaseId: attr('string'),
+ renewable: attr('boolean'),
+ leaseDuration: attr('number'),
+ accessKey: attr('string'),
+ secretKey: attr('string'),
+ securityToken: attr('string'),
+
+ attrs: computed('accessKey', function() {
+ let keys = this.get('accessKey') ? DISPLAY_FIELDS.slice(0) : CREATE_FIELDS.slice(0);
+ get(this.constructor, 'attributes').forEach((meta, name) => {
+ const index = keys.indexOf(name);
+ if (index === -1) {
+ return;
+ }
+ keys.replace(index, 1, {
+ type: meta.type,
+ name,
+ options: meta.options,
+ });
+ });
+ return keys;
+ }),
+
+ toCreds: computed('accessKey', 'secretKey', 'securityToken', 'leaseId', function() {
+ const props = this.getProperties('accessKey', 'secretKey', 'securityToken', 'leaseId');
+ const propsWithVals = Object.keys(props).reduce((ret, prop) => {
+ if (props[prop]) {
+ ret[prop] = props[prop];
+ return ret;
+ }
+ return ret;
+ }, {});
+ return JSON.stringify(propsWithVals, null, 2);
+ }),
+});
diff --git a/ui/app/models/identity/_base.js b/ui/app/models/identity/_base.js
new file mode 100644
index 000000000..e4e5fdd33
--- /dev/null
+++ b/ui/app/models/identity/_base.js
@@ -0,0 +1,19 @@
+import Ember from 'ember';
+import DS from 'ember-data';
+import { expandAttributeMeta } from 'vault/utils/field-to-attrs';
+
+const { assert, computed } = Ember;
+export default DS.Model.extend({
+ formFields: computed(function() {
+ return assert('formFields should be overridden', false);
+ }),
+
+ fields: computed('formFields', 'formFields.[]', function() {
+ return expandAttributeMeta(this, this.get('formFields'));
+ }),
+
+ identityType: computed(function() {
+ let modelType = this.constructor.modelName.split('/')[1];
+ return modelType;
+ }),
+});
diff --git a/ui/app/models/identity/entity-alias.js b/ui/app/models/identity/entity-alias.js
new file mode 100644
index 000000000..6e5dbaeaf
--- /dev/null
+++ b/ui/app/models/identity/entity-alias.js
@@ -0,0 +1,31 @@
+import IdentityModel from './_base';
+import DS from 'ember-data';
+const { attr, belongsTo } = DS;
+
+export default IdentityModel.extend({
+ formFields: ['name', 'mountAccessor', 'metadata'],
+ entity: belongsTo('identity/entity', { readOnly: true, async: false }),
+
+ name: attr('string'),
+ canonicalId: attr('string'),
+ mountAccessor: attr('string', {
+ label: 'Auth Backend',
+ editType: 'mountAccessor',
+ }),
+ metadata: attr('object', {
+ editType: 'kv',
+ }),
+ mountPath: attr('string', {
+ readOnly: true,
+ }),
+ mountType: attr('string', {
+ readOnly: true,
+ }),
+ creationTime: attr('string', {
+ readOnly: true,
+ }),
+ lastUpdateTime: attr('string', {
+ readOnly: true,
+ }),
+ mergedFromCanonicalIds: attr(),
+});
diff --git a/ui/app/models/identity/entity-merge.js b/ui/app/models/identity/entity-merge.js
new file mode 100644
index 000000000..c66293818
--- /dev/null
+++ b/ui/app/models/identity/entity-merge.js
@@ -0,0 +1,18 @@
+import IdentityModel from './_base';
+import DS from 'ember-data';
+const { attr } = DS;
+
+export default IdentityModel.extend({
+ formFields: ['toEntityId', 'fromEntityIds', 'force'],
+ toEntityId: attr('string', {
+ label: 'Entity to merge to',
+ }),
+ fromEntityIds: attr({
+ label: 'Entities to merge from',
+ editType: 'stringArray',
+ }),
+ force: attr('boolean', {
+ label: 'Keep MFA secrets from the "to" entity if there are merge conflicts',
+ defaultValue: false,
+ }),
+});
diff --git a/ui/app/models/identity/entity.js b/ui/app/models/identity/entity.js
new file mode 100644
index 000000000..77b685318
--- /dev/null
+++ b/ui/app/models/identity/entity.js
@@ -0,0 +1,31 @@
+import IdentityModel from './_base';
+import DS from 'ember-data';
+const { attr, hasMany } = DS;
+
+export default IdentityModel.extend({
+ formFields: ['name', 'policies', 'metadata'],
+ name: attr('string'),
+ mergedEntityIds: attr(),
+ metadata: attr('object', {
+ editType: 'kv',
+ }),
+ policies: attr({
+ editType: 'stringArray',
+ }),
+ creationTime: attr('string', {
+ readOnly: true,
+ }),
+ lastUpdateTime: attr('string', {
+ readOnly: true,
+ }),
+ aliases: hasMany('identity/entity-alias', { async: false, readOnly: true }),
+ groupIds: attr({
+ readOnly: true,
+ }),
+ directGroupIds: attr({
+ readOnly: true,
+ }),
+ inheritedGroupIds: attr({
+ readOnly: true,
+ }),
+});
diff --git a/ui/app/models/identity/group-alias.js b/ui/app/models/identity/group-alias.js
new file mode 100644
index 000000000..14af8c110
--- /dev/null
+++ b/ui/app/models/identity/group-alias.js
@@ -0,0 +1,29 @@
+import IdentityModel from './_base';
+import DS from 'ember-data';
+const { attr, belongsTo } = DS;
+
+export default IdentityModel.extend({
+ formFields: ['name', 'mountAccessor'],
+ group: belongsTo('identity/group', { readOnly: true, async: false }),
+
+ name: attr('string'),
+ canonicalId: attr('string'),
+
+ mountPath: attr('string', {
+ readOnly: true,
+ }),
+ mountType: attr('string', {
+ readOnly: true,
+ }),
+ mountAccessor: attr('string', {
+ label: 'Auth Backend',
+ editType: 'mountAccessor',
+ }),
+
+ creationTime: attr('string', {
+ readOnly: true,
+ }),
+ lastUpdateTime: attr('string', {
+ readOnly: true,
+ }),
+});
diff --git a/ui/app/models/identity/group.js b/ui/app/models/identity/group.js
new file mode 100644
index 000000000..37212c0c9
--- /dev/null
+++ b/ui/app/models/identity/group.js
@@ -0,0 +1,55 @@
+import Ember from 'ember';
+import IdentityModel from './_base';
+import DS from 'ember-data';
+
+const { computed } = Ember;
+const { attr, belongsTo } = DS;
+
+export default IdentityModel.extend({
+ formFields: computed('type', function() {
+ let fields = ['name', 'type', 'policies', 'metadata'];
+ if (this.get('type') === 'internal') {
+ return fields.concat(['memberGroupIds', 'memberEntityIds']);
+ }
+ return fields;
+ }),
+ name: attr('string'),
+ type: attr('string', {
+ defaultValue: 'internal',
+ possibleValues: ['internal', 'external'],
+ }),
+ creationTime: attr('string', {
+ readOnly: true,
+ }),
+ lastUpdateTime: attr('string', {
+ readOnly: true,
+ }),
+ metadata: attr('object', {
+ editType: 'kv',
+ }),
+ policies: attr({
+ editType: 'stringArray',
+ }),
+ memberGroupIds: attr({
+ label: 'Member Group IDs',
+ editType: 'stringArray',
+ }),
+ memberEntityIds: attr({
+ label: 'Member Entity IDs',
+ editType: 'stringArray',
+ }),
+ hasMembers: computed(
+ 'memberEntityIds',
+ 'memberEntityIds.[]',
+ 'memberGroupIds',
+ 'memberGroupIds.[]',
+ function() {
+ let { memberEntityIds, memberGroupIds } = this.getProperties('memberEntityIds', 'memberGroupIds');
+ let numEntities = (memberEntityIds && memberEntityIds.get('length')) || 0;
+ let numGroups = (memberGroupIds && memberGroupIds.get('length')) || 0;
+ return numEntities + numGroups > 0;
+ }
+ ),
+
+ alias: belongsTo('identity/group-alias', { async: false, readOnly: true }),
+});
diff --git a/ui/app/models/key-mixin.js b/ui/app/models/key-mixin.js
new file mode 100644
index 000000000..694e8ce06
--- /dev/null
+++ b/ui/app/models/key-mixin.js
@@ -0,0 +1,44 @@
+import Ember from 'ember';
+import utils from '../lib/key-utils';
+
+export default Ember.Mixin.create({
+ flags: null,
+
+ initialParentKey: null,
+
+ isCreating: Ember.computed('initialParentKey', function() {
+ return this.get('initialParentKey') != null;
+ }),
+
+ isFolder: Ember.computed('id', function() {
+ return utils.keyIsFolder(this.get('id'));
+ }),
+
+ keyParts: Ember.computed('id', function() {
+ return utils.keyPartsForKey(this.get('id'));
+ }),
+
+ parentKey: Ember.computed('id', 'isCreating', {
+ get: function() {
+ return this.get('isCreating') ? this.get('initialParentKey') : utils.parentKeyForKey(this.get('id'));
+ },
+ set: function(_, value) {
+ return value;
+ },
+ }),
+
+ keyWithoutParent: Ember.computed('id', 'parentKey', {
+ get: function() {
+ var key = this.get('id');
+ return key ? key.replace(this.get('parentKey'), '') : null;
+ },
+ set: function(_, value) {
+ if (value && value.trim()) {
+ this.set('id', this.get('parentKey') + value);
+ } else {
+ this.set('id', null);
+ }
+ return value;
+ },
+ }),
+});
diff --git a/ui/app/models/lease.js b/ui/app/models/lease.js
new file mode 100644
index 000000000..4f0991188
--- /dev/null
+++ b/ui/app/models/lease.js
@@ -0,0 +1,25 @@
+import Ember from 'ember';
+import DS from 'ember-data';
+import KeyMixin from './key-mixin';
+const { attr } = DS;
+
+/* sample response
+{
+ "id": "auth/token/create/25c75065466dfc5f920525feafe47502c4c9915c",
+ "issue_time": "2017-04-30T10:18:11.228946471-04:00",
+ "expire_time": "2017-04-30T11:18:11.228946708-04:00",
+ "last_renewal": null,
+ "renewable": true,
+ "ttl": 3558
+}
+
+*/
+
+export default DS.Model.extend(KeyMixin, {
+ issueTime: attr('string'),
+ expireTime: attr('string'),
+ lastRenewal: attr('string'),
+ renewable: attr('boolean'),
+ ttl: attr('number'),
+ isAuthLease: Ember.computed.match('id', /^auth/),
+});
diff --git a/ui/app/models/mount-config.js b/ui/app/models/mount-config.js
new file mode 100644
index 000000000..35bbf19f1
--- /dev/null
+++ b/ui/app/models/mount-config.js
@@ -0,0 +1,13 @@
+import attr from 'ember-data/attr';
+import Fragment from 'ember-data-model-fragments/fragment';
+
+export default Fragment.extend({
+ defaultLeaseTtl: attr({
+ label: 'Default Lease TTL',
+ editType: 'ttl',
+ }),
+ maxLeaseTtl: attr({
+ label: 'Max Lease TTL',
+ editType: 'ttl',
+ }),
+});
diff --git a/ui/app/models/mount-filter-config.js b/ui/app/models/mount-filter-config.js
new file mode 100644
index 000000000..b0234a749
--- /dev/null
+++ b/ui/app/models/mount-filter-config.js
@@ -0,0 +1,13 @@
+import DS from 'ember-data';
+const { attr } = DS;
+
+export default DS.Model.extend({
+ mode: attr('string', {
+ defaultValue: 'whitelist',
+ }),
+ paths: attr('array', {
+ defaultValue: function() {
+ return [];
+ },
+ }),
+});
diff --git a/ui/app/models/mount-options.js b/ui/app/models/mount-options.js
new file mode 100644
index 000000000..49bffe2ff
--- /dev/null
+++ b/ui/app/models/mount-options.js
@@ -0,0 +1,6 @@
+import attr from 'ember-data/attr';
+import Fragment from 'ember-data-model-fragments/fragment';
+
+export default Fragment.extend({
+ versioned: attr('string'),
+});
diff --git a/ui/app/models/node.js b/ui/app/models/node.js
new file mode 100644
index 000000000..5d9b648c9
--- /dev/null
+++ b/ui/app/models/node.js
@@ -0,0 +1,38 @@
+import Ember from 'ember';
+import DS from 'ember-data';
+const { attr } = DS;
+
+const { computed } = Ember;
+const { equal, and, alias } = computed;
+
+export default DS.Model.extend({
+ name: attr('string'),
+ //https://www.vaultproject.io/docs/http/sys-health.html
+ initialized: attr('boolean'),
+ sealed: attr('boolean'),
+ isSealed: alias('sealed'),
+ standby: attr('boolean'),
+ isActive: equal('standby', false),
+ clusterName: attr('string'),
+ clusterId: attr('string'),
+
+ isLeader: and('initialized', 'isActive'),
+
+ //https://www.vaultproject.io/docs/http/sys-seal-status.html
+ //The "t" parameter is the threshold, and "n" is the number of shares.
+ t: attr('number'),
+ n: attr('number'),
+ progress: attr('number'),
+ sealThreshold: alias('t'),
+ sealNumShares: alias('n'),
+ version: attr('string'),
+
+ //https://www.vaultproject.io/docs/http/sys-leader.html
+ haEnabled: attr('boolean'),
+ isSelf: attr('boolean'),
+ leaderAddress: attr('string'),
+
+ type: Ember.computed(function() {
+ return this.constructor.modelName;
+ }),
+});
diff --git a/ui/app/models/pki-ca-certificate-sign.js b/ui/app/models/pki-ca-certificate-sign.js
new file mode 100644
index 000000000..25617eec9
--- /dev/null
+++ b/ui/app/models/pki-ca-certificate-sign.js
@@ -0,0 +1,77 @@
+import Ember from 'ember';
+import DS from 'ember-data';
+import Certificate from './pki-certificate-sign';
+
+const { attr } = DS;
+const { computed } = Ember;
+
+export default Certificate.extend({
+ backend: attr('string', {
+ readOnly: true,
+ }),
+ useCsrValues: attr('boolean', {
+ defaultValue: false,
+ label: 'Use CSR values',
+ }),
+ maxPathLength: attr('number', {
+ defaultValue: -1,
+ }),
+ permittedDnsNames: attr('string', {
+ label: 'Permitted DNS domains',
+ }),
+ ou: attr({
+ label: 'OU (OrganizationalUnit)',
+ editType: 'stringArray',
+ }),
+ organization: attr({
+ editType: 'stringArray',
+ }),
+ country: attr({
+ editType: 'stringArray',
+ }),
+ locality: attr({
+ editType: 'stringArray',
+ label: 'Locality/City',
+ }),
+ province: attr({
+ editType: 'stringArray',
+ label: 'Province/State',
+ }),
+ streetAddress: attr({
+ editType: 'stringArray',
+ }),
+ postalCode: attr({
+ editType: 'stringArray',
+ }),
+
+ fieldGroups: computed('useCsrValues', function() {
+ const options = [
+ {
+ Options: [
+ 'altNames',
+ 'ipSans',
+ 'ttl',
+ 'excludeCnFromSans',
+ 'maxPathLength',
+ 'permittedDnsNames',
+ 'ou',
+ 'organization',
+ 'otherSans',
+ ],
+ },
+ {
+ 'Address Options': ['country', 'locality', 'province', 'streetAddress', 'postalCode'],
+ },
+ ];
+ let groups = [
+ {
+ default: ['csr', 'commonName', 'format', 'useCsrValues'],
+ },
+ ];
+ if (this.get('useCsrValues') === false) {
+ groups = groups.concat(options);
+ }
+
+ return this.fieldsToAttrs(Ember.copy(groups, true));
+ }),
+});
diff --git a/ui/app/models/pki-ca-certificate.js b/ui/app/models/pki-ca-certificate.js
new file mode 100644
index 000000000..472339bb0
--- /dev/null
+++ b/ui/app/models/pki-ca-certificate.js
@@ -0,0 +1,154 @@
+import Certificate from './pki-certificate';
+import Ember from 'ember';
+import DS from 'ember-data';
+import { queryRecord } from 'ember-computed-query';
+
+const { computed } = Ember;
+const { attr } = DS;
+
+export default Certificate.extend({
+ DISPLAY_FIELDS: [
+ 'csr',
+ 'certificate',
+ 'expiration',
+ 'issuingCa',
+ 'caChain',
+ 'privateKey',
+ 'privateKeyType',
+ 'serialNumber',
+ ],
+ backend: attr('string', {
+ readOnly: true,
+ }),
+
+ caType: attr('string', {
+ possibleValues: ['root', 'intermediate'],
+ defaultValue: 'root',
+ label: 'CA Type',
+ readOnly: true,
+ }),
+ uploadPemBundle: attr('boolean', {
+ label: 'Upload PEM bundle',
+ readOnly: true,
+ }),
+ pemBundle: attr('string', {
+ label: 'PEM bundle',
+ editType: 'file',
+ }),
+
+ fieldDefinition: computed('caType', 'uploadPemBundle', function() {
+ const type = this.get('caType');
+ const isUpload = this.get('uploadPemBundle');
+ let groups = [{ default: ['caType', 'uploadPemBundle'] }];
+ if (isUpload) {
+ groups[0].default.push('pemBundle');
+ } else {
+ groups[0].default.push('type', 'commonName');
+ if (type === 'root') {
+ groups.push({
+ Options: [
+ 'altNames',
+ 'ipSans',
+ 'ttl',
+ 'format',
+ 'privateKeyFormat',
+ 'keyType',
+ 'keyBits',
+ 'maxPathLength',
+ 'permittedDnsNames',
+ 'excludeCnFromSans',
+ 'ou',
+ 'organization',
+ 'otherSans',
+ ],
+ });
+ }
+ if (type === 'intermediate') {
+ groups.push({
+ Options: [
+ 'altNames',
+ 'ipSans',
+ 'format',
+ 'privateKeyFormat',
+ 'keyType',
+ 'keyBits',
+ 'excludeCnFromSans',
+ 'ou',
+ 'organization',
+ 'otherSans',
+ ],
+ });
+ }
+ }
+ groups.push({
+ 'Address Options': ['country', 'locality', 'province', 'streetAddress', 'postalCode'],
+ });
+
+ return groups;
+ }),
+
+ type: attr('string', {
+ possibleValues: ['internal', 'exported'],
+ defaultValue: 'internal',
+ }),
+ ou: attr({
+ label: 'OU (OrganizationalUnit)',
+ editType: 'stringArray',
+ }),
+ organization: attr({
+ editType: 'stringArray',
+ }),
+ country: attr({
+ editType: 'stringArray',
+ }),
+ locality: attr({
+ editType: 'stringArray',
+ label: 'Locality/City',
+ }),
+ province: attr({
+ editType: 'stringArray',
+ label: 'Province/State',
+ }),
+ streetAddress: attr({
+ editType: 'stringArray',
+ }),
+ postalCode: attr({
+ editType: 'stringArray',
+ }),
+
+ keyType: attr('string', {
+ possibleValues: ['rsa', 'ec'],
+ defaultValue: 'rsa',
+ }),
+ keyBits: attr('number', {
+ defaultValue: 2048,
+ }),
+ privateKeyFormat: attr('string', {
+ possibleValues: ['', 'der', 'pem', 'pkcs8'],
+ defaultValue: '',
+ }),
+ maxPathLength: attr('number', {
+ defaultValue: -1,
+ }),
+ permittedDnsNames: attr('string', {
+ label: 'Permitted DNS domains',
+ }),
+
+ csr: attr('string', {
+ editType: 'textarea',
+ label: 'CSR',
+ }),
+ expiration: attr(),
+
+ deletePath: queryRecord(
+ 'capabilities',
+ context => {
+ const { backend } = context.getProperties('backend');
+ return {
+ id: `${backend}/root`,
+ };
+ },
+ 'backend'
+ ),
+ canDeleteRoot: computed.and('deletePath.canDelete', 'deletePath.canSudo'),
+});
diff --git a/ui/app/models/pki-certificate-sign.js b/ui/app/models/pki-certificate-sign.js
new file mode 100644
index 000000000..9fa95dc9e
--- /dev/null
+++ b/ui/app/models/pki-certificate-sign.js
@@ -0,0 +1,32 @@
+import Ember from 'ember';
+import DS from 'ember-data';
+import Certificate from './pki-certificate';
+
+const { attr } = DS;
+const { computed } = Ember;
+
+export default Certificate.extend({
+ signVerbatim: attr('boolean', {
+ readOnly: true,
+ defaultValue: false,
+ }),
+
+ csr: attr('string', {
+ label: 'Certificate Signing Request (CSR)',
+ editType: 'textarea',
+ }),
+
+ fieldGroups: computed('signVerbatim', function() {
+ const options = { Options: ['altNames', 'ipSans', 'ttl', 'excludeCnFromSans', 'otherSans'] };
+ const groups = [
+ {
+ default: ['csr', 'commonName', 'format', 'signVerbatim'],
+ },
+ ];
+ if (this.get('signVerbatim') === false) {
+ groups.push(options);
+ }
+
+ return this.fieldsToAttrs(Ember.copy(groups, true));
+ }),
+});
diff --git a/ui/app/models/pki-certificate.js b/ui/app/models/pki-certificate.js
new file mode 100644
index 000000000..75be9f8b0
--- /dev/null
+++ b/ui/app/models/pki-certificate.js
@@ -0,0 +1,159 @@
+import Ember from 'ember';
+import DS from 'ember-data';
+import { queryRecord } from 'ember-computed-query';
+
+const { computed, get } = Ember;
+const { attr } = DS;
+
+export default DS.Model.extend({
+ idPrefix: 'cert/',
+
+ backend: attr('string', {
+ readOnly: true,
+ }),
+ //the id prefixed with `cert/` so we can use it as the *secret param for the secret show route
+ idForNav: attr('string', {
+ readOnly: true,
+ }),
+ DISPLAY_FIELDS: [
+ 'certificate',
+ 'issuingCa',
+ 'caChain',
+ 'privateKey',
+ 'privateKeyType',
+ 'serialNumber',
+ 'revocationTime',
+ ],
+ role: attr('object', {
+ readOnly: true,
+ }),
+
+ revocationTime: attr('number'),
+ commonName: attr('string', {
+ label: 'Common Name',
+ }),
+
+ altNames: attr('string', {
+ label: 'DNS/Email Subject Alternative Names (SANs)',
+ }),
+
+ ipSans: attr('string', {
+ label: 'IP Subject Alternative Names (SANs)',
+ }),
+ otherSans: attr({
+ editType: 'stringArray',
+ label: 'Other SANs',
+ helpText:
+ 'The format is the same as OpenSSL: ;: where the only current valid type is UTF8',
+ }),
+
+ ttl: attr({
+ label: 'TTL',
+ editType: 'ttl',
+ }),
+
+ format: attr('string', {
+ defaultValue: 'pem',
+ possibleValues: ['pem', 'der', 'pem_bundle'],
+ }),
+
+ excludeCnFromSans: attr('boolean', {
+ label: 'Exclude Common Name from Subject Alternative Names (SANs)',
+ defaultValue: false,
+ }),
+
+ certificate: attr('string'),
+ issuingCa: attr('string', {
+ label: 'Issuing CA',
+ }),
+ caChain: attr('string', {
+ label: 'CA chain',
+ }),
+ privateKey: attr('string'),
+ privateKeyType: attr('string'),
+ serialNumber: attr('string'),
+
+ fieldsToAttrs(fieldGroups) {
+ const attrMap = get(this.constructor, 'attributes');
+ return fieldGroups.map(group => {
+ const groupKey = Object.keys(group)[0];
+ const groupMembers = group[groupKey];
+ const fields = groupMembers.map(field => {
+ var meta = attrMap.get(field);
+ return {
+ type: meta.type,
+ name: meta.name,
+ options: meta.options,
+ };
+ });
+ return { [groupKey]: fields };
+ });
+ },
+
+ fieldDefinition: computed(function() {
+ const groups = [
+ { default: ['commonName', 'format'] },
+ { Options: ['altNames', 'ipSans', 'ttl', 'excludeCnFromSans', 'otherSans'] },
+ ];
+ return groups;
+ }),
+
+ fieldGroups: computed('fieldDefinition', function() {
+ return this.fieldsToAttrs(this.get('fieldDefinition'));
+ }),
+
+ attrs: computed('certificate', 'csr', function() {
+ let keys = this.get('certificate') || this.get('csr') ? this.DISPLAY_FIELDS.slice(0) : [];
+ const attrMap = get(this.constructor, 'attributes');
+ keys = keys.map(key => {
+ let meta = attrMap.get(key);
+ return {
+ type: meta.type,
+ name: meta.name,
+ options: meta.options,
+ };
+ });
+ return keys;
+ }),
+
+ toCreds: computed(
+ 'certificate',
+ 'issuingCa',
+ 'caChain',
+ 'privateKey',
+ 'privateKeyType',
+ 'revocationTime',
+ 'serialNumber',
+ function() {
+ const props = this.getProperties(
+ 'certificate',
+ 'issuingCa',
+ 'caChain',
+ 'privateKey',
+ 'privateKeyType',
+ 'revocationTime',
+ 'serialNumber'
+ );
+ const propsWithVals = Object.keys(props).reduce((ret, prop) => {
+ if (props[prop]) {
+ ret[prop] = props[prop];
+ return ret;
+ }
+ return ret;
+ }, {});
+ return JSON.stringify(propsWithVals, null, 2);
+ }
+ ),
+
+ revokePath: queryRecord(
+ 'capabilities',
+ context => {
+ const { backend } = context.getProperties('backend');
+ return {
+ id: `${backend}/revoke`,
+ };
+ },
+ 'backend'
+ ),
+ canRevoke: computed.alias('revokePath.canUpdate'),
+});
diff --git a/ui/app/models/pki-config.js b/ui/app/models/pki-config.js
new file mode 100644
index 000000000..c471db069
--- /dev/null
+++ b/ui/app/models/pki-config.js
@@ -0,0 +1,69 @@
+import Ember from 'ember';
+import DS from 'ember-data';
+
+const { attr } = DS;
+const { computed, get } = Ember;
+
+export default DS.Model.extend({
+ backend: attr('string'),
+ der: attr(),
+ pem: attr('string'),
+ caChain: attr('string'),
+ attrList(keys) {
+ const attrMap = get(this.constructor, 'attributes');
+ keys = keys.map(key => {
+ let meta = attrMap.get(key);
+ return {
+ type: meta.type,
+ name: meta.name,
+ options: meta.options,
+ };
+ });
+ return keys;
+ },
+
+ //urls
+ urlsAttrs: computed(function() {
+ let keys = ['issuingCertificates', 'crlDistributionPoints', 'ocspServers'];
+ return this.attrList(keys);
+ }),
+ issuingCertificates: attr({
+ editType: 'stringArray',
+ }),
+ crlDistributionPoints: attr({
+ label: 'CRL Distribution Points',
+ editType: 'stringArray',
+ }),
+ ocspServers: attr({
+ label: 'OCSP Servers',
+ editType: 'stringArray',
+ }),
+
+ //tidy
+ tidyAttrs: computed(function() {
+ let keys = ['tidyCertStore', 'tidyRevocationList', 'safetyBuffer'];
+ return this.attrList(keys);
+ }),
+ tidyCertStore: attr('boolean', {
+ defaultValue: false,
+ label: 'Tidy the Certificate Store',
+ }),
+ tidyRevocationList: attr('boolean', {
+ defaultValue: false,
+ label: 'Tidy the Revocation List (CRL)',
+ }),
+ safetyBuffer: attr({
+ defaultValue: '72h',
+ editType: 'ttl',
+ }),
+
+ crlAttrs: computed(function() {
+ let keys = ['expiry'];
+ return this.attrList(keys);
+ }),
+ //crl
+ expiry: attr({
+ defaultValue: '72h',
+ editType: 'ttl',
+ }),
+});
diff --git a/ui/app/models/policy.js b/ui/app/models/policy.js
new file mode 100644
index 000000000..785aa6237
--- /dev/null
+++ b/ui/app/models/policy.js
@@ -0,0 +1,46 @@
+import DS from 'ember-data';
+import Ember from 'ember';
+import { queryRecord } from 'ember-computed-query';
+
+let { attr } = DS;
+let { computed } = Ember;
+
+export default DS.Model.extend({
+ name: attr('string'),
+ policy: attr('string'),
+ policyType: computed(function() {
+ return this.constructor.modelName.split('/')[1];
+ }),
+
+ updatePath: queryRecord(
+ 'capabilities',
+ context => {
+ const { policyType, id } = context.getProperties('policyType', 'id');
+ if (!policyType && id) {
+ return;
+ }
+ return {
+ id: `sys/${policyType}/policies/${id}`,
+ };
+ },
+ 'id',
+ 'policyType'
+ ),
+ canDelete: computed.alias('updatePath.canDelete'),
+ canEdit: computed.alias('updatePath.canUpdate'),
+ canRead: computed.alias('updatePath.canRead'),
+ format: computed('policy', function() {
+ let policy = this.get('policy');
+ let isJSON;
+ try {
+ let parsed = JSON.parse(policy);
+ if (parsed) {
+ isJSON = true;
+ }
+ } catch (e) {
+ // can't parse JSON
+ isJSON = false;
+ }
+ return isJSON ? 'json' : 'hcl';
+ }),
+});
diff --git a/ui/app/models/policy/acl.js b/ui/app/models/policy/acl.js
new file mode 100644
index 000000000..fd700b574
--- /dev/null
+++ b/ui/app/models/policy/acl.js
@@ -0,0 +1,3 @@
+import PolicyModel from '../policy';
+
+export default PolicyModel.extend();
diff --git a/ui/app/models/policy/egp.js b/ui/app/models/policy/egp.js
new file mode 100644
index 000000000..d6dece528
--- /dev/null
+++ b/ui/app/models/policy/egp.js
@@ -0,0 +1,17 @@
+import DS from 'ember-data';
+import Ember from 'ember';
+
+import PolicyModel from './rgp';
+import { expandAttributeMeta } from 'vault/utils/field-to-attrs';
+
+let { attr } = DS;
+let { computed } = Ember;
+
+export default PolicyModel.extend({
+ paths: attr({
+ editType: 'stringArray',
+ }),
+ additionalAttrs: computed(function() {
+ return expandAttributeMeta(this, ['enforcementLevel', 'paths']);
+ }),
+});
diff --git a/ui/app/models/policy/rgp.js b/ui/app/models/policy/rgp.js
new file mode 100644
index 000000000..361d669b3
--- /dev/null
+++ b/ui/app/models/policy/rgp.js
@@ -0,0 +1,19 @@
+import DS from 'ember-data';
+import Ember from 'ember';
+
+import PolicyModel from '../policy';
+import { expandAttributeMeta } from 'vault/utils/field-to-attrs';
+
+let { attr } = DS;
+let { computed } = Ember;
+
+export default PolicyModel.extend({
+ enforcementLevel: attr('string', {
+ possibleValues: ['advisory', 'soft-mandatory', 'hard-mandatory'],
+ defaultValue: 'hard-mandatory',
+ }),
+
+ additionalAttrs: computed(function() {
+ return expandAttributeMeta(this, ['enforcementLevel']);
+ }),
+});
diff --git a/ui/app/models/replication-attributes.js b/ui/app/models/replication-attributes.js
new file mode 100644
index 000000000..a2a981e3a
--- /dev/null
+++ b/ui/app/models/replication-attributes.js
@@ -0,0 +1,77 @@
+import Ember from 'ember';
+import attr from 'ember-data/attr';
+import Fragment from 'ember-data-model-fragments/fragment';
+
+const { computed } = Ember;
+const { match } = computed;
+
+export default Fragment.extend({
+ clusterId: attr('string'),
+ clusterIdDisplay: computed('mode', function() {
+ const clusterId = this.get('clusterId');
+ return clusterId ? clusterId.split('-')[0] : null;
+ }),
+ mode: attr('string'),
+ replicationDisabled: match('mode', /disabled|unsupported/),
+ replicationUnsupported: match('mode', /unsupported/),
+ replicationEnabled: computed.not('replicationDisabled'),
+
+ // primary attrs
+ isPrimary: match('mode', /primary/),
+
+ knownSecondaries: attr('array'),
+
+ // secondary attrs
+ isSecondary: match('mode', /secondary/),
+
+ modeForUrl: computed('mode', function() {
+ const mode = this.get('mode');
+ return mode === 'bootstrapping'
+ ? 'bootstrapping'
+ : (this.get('isSecondary') && 'secondary') || (this.get('isPrimary') && 'primary');
+ }),
+ secondaryId: attr('string'),
+ primaryClusterAddr: attr('string'),
+ knownPrimaryClusterAddrs: attr('array'),
+ state: attr('string'), //stream-wal, merkle-diff, merkle-sync, idle
+ lastRemoteWAL: attr('number'),
+
+ // attrs on primary and secondary
+ lastWAL: attr('number'),
+ merkleRoot: attr('string'),
+ merkleSyncProgress: attr('object'),
+ syncProgress: computed('state', 'merkleSyncProgress', function() {
+ const { state, merkleSyncProgress } = this.getProperties('state', 'merkleSyncProgress');
+ if (state !== 'merkle-sync' || !merkleSyncProgress) {
+ return null;
+ }
+ const { sync_total_keys, sync_progress } = merkleSyncProgress;
+ return {
+ progress: sync_progress,
+ total: sync_total_keys,
+ };
+ }).volatile(),
+
+ syncProgressPercent: computed('syncProgress', function() {
+ const syncProgress = this.get('syncProgress');
+ if (!syncProgress) {
+ return null;
+ }
+ const { progress, total } = syncProgress;
+
+ return Math.floor(100 * (progress / total));
+ }),
+
+ modeDisplay: computed('mode', function() {
+ const displays = {
+ disabled: 'Disabled',
+ unknown: 'Unknown',
+ bootstrapping: 'Bootstrapping',
+ primary: 'Primary',
+ secondary: 'Secondary',
+ unsupported: 'Not supported',
+ };
+
+ return displays[this.get('mode')] || 'Disabled';
+ }),
+});
diff --git a/ui/app/models/role-aws.js b/ui/app/models/role-aws.js
new file mode 100644
index 000000000..a1d6e0738
--- /dev/null
+++ b/ui/app/models/role-aws.js
@@ -0,0 +1,81 @@
+import Ember from 'ember';
+import DS from 'ember-data';
+import { queryRecord } from 'ember-computed-query';
+
+const { attr } = DS;
+const { computed, get } = Ember;
+
+const CREATE_FIELDS = ['name', 'policy', 'arn'];
+export default DS.Model.extend({
+ backend: attr('string', {
+ readOnly: true,
+ }),
+ name: attr('string', {
+ label: 'Role name',
+ fieldValue: 'id',
+ readOnly: true,
+ }),
+ arn: attr('string', {
+ helpText: '',
+ }),
+ policy: attr('string', {
+ helpText: '',
+ widget: 'json',
+ }),
+ attrs: computed(function() {
+ let keys = CREATE_FIELDS.slice(0);
+ get(this.constructor, 'attributes').forEach((meta, name) => {
+ const index = keys.indexOf(name);
+ if (index === -1) {
+ return;
+ }
+ keys.replace(index, 1, {
+ type: meta.type,
+ name,
+ options: meta.options,
+ });
+ });
+ return keys;
+ }),
+
+ updatePath: queryRecord(
+ 'capabilities',
+ context => {
+ const { backend, id } = context.getProperties('backend', 'id');
+ return {
+ id: `${backend}/roles/${id}`,
+ };
+ },
+ 'id',
+ 'backend'
+ ),
+ canDelete: computed.alias('updatePath.canDelete'),
+ canEdit: computed.alias('updatePath.canUpdate'),
+ canRead: computed.alias('updatePath.canRead'),
+
+ generatePath: queryRecord(
+ 'capabilities',
+ context => {
+ const { backend, id } = context.getProperties('backend', 'id');
+ return {
+ id: `${backend}/creds/${id}`,
+ };
+ },
+ 'id',
+ 'backend'
+ ),
+ canGenerate: computed.alias('generatePath.canUpdate'),
+
+ stsPath: queryRecord(
+ 'capabilities',
+ context => {
+ const { backend, id } = context.getProperties('backend', 'id');
+ return {
+ id: `${backend}/sts/${id}`,
+ };
+ },
+ 'id',
+ 'backend'
+ ),
+ canGenerateSTS: computed.alias('stsPath.canUpdate'),
+});
diff --git a/ui/app/models/role-pki.js b/ui/app/models/role-pki.js
new file mode 100644
index 000000000..0a7251d78
--- /dev/null
+++ b/ui/app/models/role-pki.js
@@ -0,0 +1,221 @@
+import Ember from 'ember';
+import DS from 'ember-data';
+import { queryRecord } from 'ember-computed-query';
+
+const { attr } = DS;
+const { computed, get } = Ember;
+
+export default DS.Model.extend({
+ backend: attr('string', {
+ readOnly: true,
+ }),
+ name: attr('string', {
+ label: 'Role name',
+ fieldValue: 'id',
+ readOnly: true,
+ }),
+ keyType: attr('string', {
+ possibleValues: ['rsa', 'ec'],
+ }),
+ ttl: attr({
+ label: 'TTL',
+ editType: 'ttl',
+ }),
+ maxTtl: attr({
+ label: 'Max TTL',
+ editType: 'ttl',
+ }),
+ allowLocalhost: attr('boolean', {}),
+ allowedDomains: attr('string', {}),
+ allowBareDomains: attr('boolean', {}),
+ allowSubdomains: attr('boolean', {}),
+ allowGlobDomains: attr('boolean', {}),
+ allowAnyName: attr('boolean', {}),
+ enforceHostnames: attr('boolean', {}),
+ allowIpSans: attr('boolean', {
+ defaultValue: true,
+ label: 'Allow clients to request IP Subject Alternative Names (SANs)',
+ }),
+ allowedOtherSans: attr({
+ editType: 'stringArray',
+ label: 'Allowed Other SANs',
+ }),
+ serverFlag: attr('boolean', {
+ defaultValue: true,
+ }),
+ clientFlag: attr('boolean', {
+ defaultValue: true,
+ }),
+ codeSigningFlag: attr('boolean', {}),
+ emailProtectionFlag: attr('boolean', {}),
+ keyBits: attr('number', {
+ defaultValue: 2048,
+ }),
+ keyUsage: attr('string', {
+ defaultValue: 'DigitalSignature,KeyAgreement,KeyEncipherment',
+ editType: 'stringArray',
+ }),
+ useCsrCommonName: attr('boolean', {
+ label: 'Use CSR common name',
+ defaultValue: true,
+ }),
+ useCsrSans: attr('boolean', {
+ defaultValue: true,
+ label: 'Use CSR subject alternative names (SANs)',
+ }),
+ ou: attr({
+ label: 'OU (OrganizationalUnit)',
+ editType: 'stringArray',
+ }),
+ organization: attr({
+ editType: 'stringArray',
+ }),
+ country: attr({
+ editType: 'stringArray',
+ }),
+ locality: attr({
+ editType: 'stringArray',
+ label: 'Locality/City',
+ }),
+ province: attr({
+ editType: 'stringArray',
+ label: 'Province/State',
+ }),
+ streetAddress: attr({
+ editType: 'stringArray',
+ }),
+ postalCode: attr({
+ editType: 'stringArray',
+ }),
+ generateLease: attr('boolean', {}),
+ noStore: attr('boolean', {}),
+
+ updatePath: queryRecord(
+ 'capabilities',
+ context => {
+ const { backend, id } = context.getProperties('backend', 'id');
+ return {
+ id: `${backend}/roles/${id}`,
+ };
+ },
+ 'id',
+ 'backend'
+ ),
+ canDelete: computed.alias('updatePath.canDelete'),
+ canEdit: computed.alias('updatePath.canUpdate'),
+ canRead: computed.alias('updatePath.canRead'),
+
+ generatePath: queryRecord(
+ 'capabilities',
+ context => {
+ const { backend, id } = context.getProperties('backend', 'id');
+ return {
+ id: `${backend}/issue/${id}`,
+ };
+ },
+ 'id',
+ 'backend'
+ ),
+ canGenerate: computed.alias('generatePath.canUpdate'),
+
+ signPath: queryRecord(
+ 'capabilities',
+ context => {
+ const { backend, id } = context.getProperties('backend', 'id');
+ return {
+ id: `${backend}/sign/${id}`,
+ };
+ },
+ 'id',
+ 'backend'
+ ),
+ canSign: computed.alias('signPath.canUpdate'),
+
+ signVerbatimPath: queryRecord(
+ 'capabilities',
+ context => {
+ const { backend, id } = context.getProperties('backend', 'id');
+ return {
+ id: `${backend}/sign-verbatim/${id}`,
+ };
+ },
+ 'id',
+ 'backend'
+ ),
+ canSignVerbatim: computed.alias('signVerbatimPath.canUpdate'),
+
+ /*
+ * this hydrates the map in `fieldGroups` so that it contains
+ * the actual field information, not just the name of the field
+ */
+ fieldsToAttrs(fieldGroups) {
+ const attrMap = get(this.constructor, 'attributes');
+ return fieldGroups.map(group => {
+ const groupKey = Object.keys(group)[0];
+ const groupMembers = group[groupKey];
+ const fields = groupMembers.map(field => {
+ var meta = attrMap.get(field);
+ return {
+ type: meta.type,
+ name: meta.name,
+ options: meta.options,
+ };
+ });
+ return { [groupKey]: fields };
+ });
+ },
+
+ /*
+ * returns an array of objects that list attributes so that the form can be programmatically generated
+ * the attributes are pulled from the model's attribute hash
+ *
+ * The keys will be used to label each section of the form.
+ * the 'default' key contains fields that are outside of any grouping
+ *
+ * returns an array of objects:
+ *
+ * [
+ * {'default': [ { type: 'string', name: 'keyType', options: { label: 'Key Type'}}]},
+ * {'Options': [{ type: 'boolean', name: 'allowAnyName', options: {}}]}
+ * ]
+ *
+ *
+ */
+ fieldGroups: computed(function() {
+ const groups = [
+ { default: ['name', 'keyType'] },
+ {
+ Options: [
+ 'keyBits',
+ 'ttl',
+ 'maxTtl',
+ 'allowAnyName',
+ 'enforceHostnames',
+ 'allowIpSans',
+ 'useCsrCommonName',
+ 'useCsrSans',
+ 'ou',
+ 'organization',
+ 'keyUsage',
+ 'allowedOtherSans',
+ ],
+ },
+ {
+ 'Address Options': ['country', 'locality', 'province', 'streetAddress', 'postalCode'],
+ },
+ {
+ 'Domain Handling': [
+ 'allowLocalhost',
+ 'allowBareDomains',
+ 'allowSubdomains',
+ 'allowGlobDomains',
+ 'allowedDomains',
+ ],
+ },
+ { 'Extended Key Usage': ['serverFlag', 'clientFlag', 'codeSigningFlag', 'emailProtectionFlag'] },
+ { Advanced: ['generateLease', 'noStore'] },
+ ];
+
+ return this.fieldsToAttrs(Ember.copy(groups, true));
+ }),
+});
diff --git a/ui/app/models/role-ssh.js b/ui/app/models/role-ssh.js
new file mode 100644
index 000000000..22b2c2cb4
--- /dev/null
+++ b/ui/app/models/role-ssh.js
@@ -0,0 +1,193 @@
+import Ember from 'ember';
+import DS from 'ember-data';
+import { queryRecord } from 'ember-computed-query';
+
+const { attr } = DS;
+const { computed, get } = Ember;
+
+// these arrays define the order in which the fields will be displayed
+// see
+// https://github.com/hashicorp/vault/blob/master/builtin/logical/ssh/path_roles.go#L542 for list of fields for each key type
+const OTP_FIELDS = [
+ 'name',
+ 'keyType',
+ 'defaultUser',
+ 'adminUser',
+ 'port',
+ 'allowedUsers',
+ 'cidrList',
+ 'excludeCidrList',
+];
+const CA_FIELDS = [
+ 'name',
+ 'keyType',
+ 'allowUserCertificates',
+ 'allowHostCertificates',
+ 'defaultUser',
+ 'allowedUsers',
+ 'allowedDomains',
+ 'ttl',
+ 'maxTtl',
+ 'allowedCriticalOptions',
+ 'defaultCriticalOptions',
+ 'allowedExtensions',
+ 'defaultExtensions',
+ 'allowBareDomains',
+ 'allowSubdomains',
+ 'allowUserKeyIds',
+ 'keyIdFormat',
+];
+export default DS.Model.extend({
+ zeroAddress: attr('boolean', {
+ readOnly: true,
+ }),
+ backend: attr('string', {
+ readOnly: true,
+ }),
+ name: attr('string', {
+ label: 'Role name',
+ fieldValue: 'id',
+ readOnly: true,
+ }),
+ keyType: attr('string', {
+ possibleValues: ['ca', 'otp'],
+ }),
+ adminUser: attr('string', {
+ helpText: 'Username of the admin user at the remote host',
+ }),
+ defaultUser: attr('string', {
+ helpText: "Username to use when one isn't specified",
+ }),
+ allowedUsers: attr('string', {
+ helpText:
+ 'Create a whitelist of users that can use this key (e.g. `admin, dev`, or use `*` to allow all)',
+ }),
+ allowedDomains: attr('string', {
+ helpText:
+ 'List of domains for which a client can request a certificate (e.g. `example.com`, or `*` to allow all)',
+ }),
+ cidrList: attr('string', {
+ label: 'CIDR list',
+ helpText: 'List of CIDR blocks for which this role is applicable',
+ }),
+ excludeCidrList: attr('string', {
+ label: 'Exclude CIDR list',
+ helpText: 'List of CIDR blocks that are not accepted by this role',
+ }),
+ port: attr('number', {
+ defaultValue: 22,
+ helpText: 'Port number for the SSH connection (default is `22`)',
+ }),
+ ttl: attr({
+ label: 'TTL',
+ editType: 'ttl',
+ }),
+ maxTtl: attr({
+ label: 'Max TTL',
+ editType: 'ttl',
+ }),
+ allowedCriticalOptions: attr('string', {
+ helpText: 'List of critical options that certificates have when signed',
+ }),
+ defaultCriticalOptions: attr('object', {
+ helpText: 'Map of critical options certificates should have if none are provided when signing',
+ }),
+ allowedExtensions: attr('string', {
+ helpText: 'List of extensions that certificates can have when signed',
+ }),
+ defaultExtensions: attr('object', {
+ helpText: 'Map of extensions certificates should have if none are provided when signing',
+ }),
+ allowUserCertificates: attr('boolean', {
+ helpText: 'Specifies if certificates are allowed to be signed for us as a user',
+ }),
+ allowHostCertificates: attr('boolean', {
+ helpText: 'Specifies if certificates are allowed to be signed for us as a host',
+ }),
+ allowBareDomains: attr('boolean', {
+ helpText:
+ 'Specifies if host certificates that are requested are allowed to use the base domains listed in Allowed Domains',
+ }),
+ allowSubdomains: attr('boolean', {
+ helpText:
+ 'Specifies if host certificates that are requested are allowed to be subdomains of those listed in Allowed Domains',
+ }),
+ allowUserKeyIds: attr('boolean', {
+ label: 'Allow user key IDs',
+ helpText: 'Specifies if users can override the key ID for a signed certificate with the "key_id" field',
+ }),
+ keyIdFormat: attr('string', {
+ label: 'Key ID format',
+ helpText: 'When supplied, this value specifies a custom format for the key id of a signed certificate',
+ }),
+
+ attrsForKeyType: computed('keyType', function() {
+ const keyType = this.get('keyType');
+ let keys = keyType === 'ca' ? CA_FIELDS.slice(0) : OTP_FIELDS.slice(0);
+ get(this.constructor, 'attributes').forEach((meta, name) => {
+ const index = keys.indexOf(name);
+ if (index === -1) {
+ return;
+ }
+ keys.replace(index, 1, {
+ type: meta.type,
+ name,
+ options: meta.options,
+ });
+ });
+ return keys;
+ }),
+
+ updatePath: queryRecord(
+ 'capabilities',
+ context => {
+ const { backend, id } = context.getProperties('backend', 'id');
+ return {
+ id: `${backend}/roles/${id}`,
+ };
+ },
+ 'id',
+ 'backend'
+ ),
+ canDelete: computed.alias('updatePath.canDelete'),
+ canEdit: computed.alias('updatePath.canUpdate'),
+ canRead: computed.alias('updatePath.canRead'),
+
+ generatePath: queryRecord(
+ 'capabilities',
+ context => {
+ const { backend, id } = context.getProperties('backend', 'id');
+ return {
+ id: `${backend}/creds/${id}`,
+ };
+ },
+ 'id',
+ 'backend'
+ ),
+ canGenerate: computed.alias('generatePath.canUpdate'),
+
+ signPath: queryRecord(
+ 'capabilities',
+ context => {
+ const { backend, id } = context.getProperties('backend', 'id');
+ return {
+ id: `${backend}/sign/${id}`,
+ };
+ },
+ 'id',
+ 'backend'
+ ),
+ canSign: computed.alias('signPath.canUpdate'),
+
+ zeroAddressPath: queryRecord(
+ 'capabilities',
+ context => {
+ const { backend } = context.getProperties('backend');
+ return {
+ id: `${backend}/config/zeroaddress`,
+ };
+ },
+ 'backend'
+ ),
+ canEditZeroAddress: computed.alias('zeroAddressPath.canUpdate'),
+});
diff --git a/ui/app/models/secret-cubbyhole.js b/ui/app/models/secret-cubbyhole.js
new file mode 100644
index 000000000..fcbf908ca
--- /dev/null
+++ b/ui/app/models/secret-cubbyhole.js
@@ -0,0 +1,3 @@
+import Secret from './secret';
+
+export default Secret.extend();
diff --git a/ui/app/models/secret-engine.js b/ui/app/models/secret-engine.js
new file mode 100644
index 000000000..3a1a2d25a
--- /dev/null
+++ b/ui/app/models/secret-engine.js
@@ -0,0 +1,83 @@
+import Ember from 'ember';
+import DS from 'ember-data';
+import { queryRecord } from 'ember-computed-query';
+import { fragment } from 'ember-data-model-fragments/attributes';
+
+const { attr } = DS;
+const { computed } = Ember;
+
+//identity will be managed separately and the inclusion
+//of the system backend is an implementation detail
+const LIST_EXCLUDED_BACKENDS = ['system', 'identity'];
+
+export default DS.Model.extend({
+ path: attr('string'),
+ accessor: attr('string'),
+ name: attr('string'),
+ type: attr('string'),
+ description: attr('string'),
+ config: attr('object'),
+ options: fragment('mount-options'),
+ local: attr('boolean'),
+ sealWrap: attr('boolean'),
+ isVersioned: computed.alias('options.versioned'),
+
+ shouldIncludeInList: computed('type', function() {
+ return !LIST_EXCLUDED_BACKENDS.includes(this.get('type'));
+ }),
+
+ localDisplay: Ember.computed('local', function() {
+ return this.get('local') ? 'local' : 'replicated';
+ }),
+
+ // ssh specific ones
+ privateKey: attr('string'),
+ publicKey: attr('string'),
+ generateSigningKey: attr('boolean', {
+ defaultValue: true,
+ }),
+
+ saveCA(options) {
+ if (this.get('type') !== 'ssh') {
+ return;
+ }
+ if (options.isDelete) {
+ this.setProperties({
+ privateKey: null,
+ publicKey: null,
+ generateSigningKey: false,
+ });
+ }
+ return this.save({
+ adapterOptions: {
+ options: options,
+ apiPath: 'config/ca',
+ attrsToSend: ['privateKey', 'publicKey', 'generateSigningKey'],
+ },
+ });
+ },
+
+ saveZeroAddressConfig() {
+ return this.save({
+ adapterOptions: {
+ adapterMethod: 'saveZeroAddressConfig',
+ },
+ });
+ },
+
+ zeroAddressPath: queryRecord(
+ 'capabilities',
+ context => {
+ const { id } = context.getProperties('backend', 'id');
+ return {
+ id: `${id}/config/zeroaddress`,
+ };
+ },
+ 'id'
+ ),
+ canEditZeroAddress: computed.alias('zeroAddressPath.canUpdate'),
+
+ // aws backend attrs
+ lease: attr('string'),
+ leaseMax: attr('string'),
+});
diff --git a/ui/app/models/secret.js b/ui/app/models/secret.js
new file mode 100644
index 000000000..3a1783cca
--- /dev/null
+++ b/ui/app/models/secret.js
@@ -0,0 +1,26 @@
+import Ember from 'ember';
+import DS from 'ember-data';
+import KeyMixin from './key-mixin';
+const { attr } = DS;
+const { computed } = Ember;
+
+export default DS.Model.extend(KeyMixin, {
+ auth: attr('string'),
+ lease_duration: attr('number'),
+ lease_id: attr('string'),
+ renewable: attr('boolean'),
+
+ secretData: attr('object'),
+
+ dataAsJSONString: computed('secretData', function() {
+ return JSON.stringify(this.get('secretData'), null, 2);
+ }),
+
+ isAdvancedFormat: computed('secretData', function() {
+ const data = this.get('secretData');
+ return Object.keys(data).some(key => typeof data[key] !== 'string');
+ }),
+
+ helpText: attr('string'),
+ backend: attr('string'),
+});
diff --git a/ui/app/models/ssh-otp-credential.js b/ui/app/models/ssh-otp-credential.js
new file mode 100644
index 000000000..13bb2d2ff
--- /dev/null
+++ b/ui/app/models/ssh-otp-credential.js
@@ -0,0 +1,38 @@
+import DS from 'ember-data';
+import Ember from 'ember';
+const { attr } = DS;
+const { computed, get } = Ember;
+const CREATE_FIELDS = ['username', 'ip'];
+
+const DISPLAY_FIELDS = ['username', 'ip', 'key', 'keyType', 'port'];
+export default DS.Model.extend({
+ role: attr('object', {
+ readOnly: true,
+ }),
+ ip: attr('string', {
+ label: 'IP Address',
+ }),
+ username: attr('string'),
+ key: attr('string'),
+ keyType: attr('string'),
+ port: attr('number'),
+ attrs: computed('key', function() {
+ let keys = this.get('key') ? DISPLAY_FIELDS.slice(0) : CREATE_FIELDS.slice(0);
+ get(this.constructor, 'attributes').forEach((meta, name) => {
+ const index = keys.indexOf(name);
+ if (index === -1) {
+ return;
+ }
+ keys.replace(index, 1, {
+ type: meta.type,
+ name,
+ options: meta.options,
+ });
+ });
+ return keys;
+ }),
+ toCreds: computed('key', function() {
+ // todo: would this be better copied as an SSH command?
+ return this.get('key');
+ }),
+});
diff --git a/ui/app/models/ssh-sign.js b/ui/app/models/ssh-sign.js
new file mode 100644
index 000000000..66e54dfcb
--- /dev/null
+++ b/ui/app/models/ssh-sign.js
@@ -0,0 +1,61 @@
+import DS from 'ember-data';
+import Ember from 'ember';
+const { attr } = DS;
+const { computed, get } = Ember;
+const CREATE_FIELDS = [
+ 'publicKey',
+ 'keyId',
+ 'validPrincipals',
+ 'certType',
+ 'criticalOptions',
+ 'extension',
+ 'ttl',
+];
+
+const DISPLAY_FIELDS = ['signedKey', 'leaseId', 'renewable', 'leaseDuration', 'serialNumber'];
+
+export default DS.Model.extend({
+ role: attr('object', {
+ readOnly: true,
+ }),
+ publicKey: attr('string'),
+ ttl: attr({
+ label: 'TTL',
+ editType: 'ttl',
+ }),
+ validPrincipals: attr('string'),
+ certType: attr('string', {
+ defaultValue: 'user',
+ label: 'Certificate Type',
+ possibleValues: ['user', 'host'],
+ }),
+ keyId: attr('string', {
+ label: 'Key ID',
+ }),
+ criticalOptions: attr('object'),
+ extension: attr('object'),
+
+ leaseId: attr('string', {
+ label: 'Lease ID',
+ }),
+ renewable: attr('boolean'),
+ leaseDuration: attr('number'),
+ serialNumber: attr('string'),
+ signedKey: attr('string'),
+
+ attrs: computed('signedKey', function() {
+ let keys = this.get('signedKey') ? DISPLAY_FIELDS.slice(0) : CREATE_FIELDS.slice(0);
+ get(this.constructor, 'attributes').forEach((meta, name) => {
+ const index = keys.indexOf(name);
+ if (index === -1) {
+ return;
+ }
+ keys.replace(index, 1, {
+ type: meta.type,
+ name,
+ options: meta.options,
+ });
+ });
+ return keys;
+ }),
+});
diff --git a/ui/app/models/test-form-model.js b/ui/app/models/test-form-model.js
new file mode 100644
index 000000000..e46d668ef
--- /dev/null
+++ b/ui/app/models/test-form-model.js
@@ -0,0 +1,9 @@
+// this model is just used for integration tests
+//
+
+import AuthMethodModel from './auth-method';
+import { fragment } from 'ember-data-model-fragments/attributes';
+
+export default AuthMethodModel.extend({
+ otherConfig: fragment('mount-config', { defaultValue: {} }),
+});
diff --git a/ui/app/models/transit-key.js b/ui/app/models/transit-key.js
new file mode 100644
index 000000000..6895db28b
--- /dev/null
+++ b/ui/app/models/transit-key.js
@@ -0,0 +1,139 @@
+import Ember from 'ember';
+import DS from 'ember-data';
+import clamp from 'vault/utils/clamp';
+import { queryRecord } from 'ember-computed-query';
+
+const { attr } = DS;
+const { computed, get, set } = Ember;
+
+const ACTION_VALUES = {
+ encrypt: 'supportsEncryption',
+ decrypt: 'supportsDecryption',
+ datakey: 'supportsEncryption',
+ rewrap: 'supportsEncryption',
+ sign: 'supportsSigning',
+ hmac: true,
+ verify: true,
+ export: 'exportable',
+};
+
+export default DS.Model.extend({
+ type: attr('string', {
+ defaultValue: 'aes256-gcm96',
+ }),
+ name: attr('string'),
+ deletionAllowed: attr('boolean'),
+ derived: attr('boolean'),
+ exportable: attr('boolean'),
+ minDecryptionVersion: attr('number', {
+ defaultValue: 1,
+ }),
+ minEncryptionVersion: attr('number', {
+ defaultValue: 0,
+ }),
+ latestVersion: attr('number'),
+ keys: attr('object'),
+ convergentEncryption: attr('boolean'),
+ convergentEncryptionVersion: attr('number'),
+
+ supportsSigning: attr('boolean'),
+ supportsEncryption: attr('boolean'),
+ supportsDecryption: attr('boolean'),
+ supportsDerivation: attr('boolean'),
+
+ setConvergentEncryption(val) {
+ if (val === true) {
+ set(this, 'derived', val);
+ }
+ set(this, 'convergentEncryption', val);
+ },
+
+ setDerived(val) {
+ if (val === false) {
+ set(this, 'convergentEncryption', val);
+ }
+ set(this, 'derived', val);
+ },
+
+ supportedActions: computed('type', function() {
+ return Object.keys(ACTION_VALUES).filter(name => {
+ const isSupported = ACTION_VALUES[name];
+ if (typeof isSupported === 'boolean') {
+ return isSupported;
+ }
+ return get(this, isSupported);
+ });
+ }),
+
+ canDelete: computed('deletionAllowed', 'lastLoadTS', function() {
+ const deleteAttrChanged = Boolean(this.changedAttributes().deletionAllowed);
+ return get(this, 'deletionAllowed') && deleteAttrChanged === false;
+ }),
+
+ keyVersions: computed('validKeyVersions', function() {
+ let maxVersion = Math.max(...get(this, 'validKeyVersions'));
+ let versions = [];
+ while (maxVersion > 0) {
+ versions.unshift(maxVersion);
+ maxVersion--;
+ }
+ return versions;
+ }),
+
+ encryptionKeyVersions: computed('keyVerisons', 'minDecryptionVersion', 'latestVersion', function() {
+ const { keyVersions, minDecryptionVersion } = this.getProperties('keyVersions', 'minDecryptionVersion');
+
+ return keyVersions
+ .filter(version => {
+ return version >= minDecryptionVersion;
+ })
+ .reverse();
+ }),
+
+ keysForEncryption: computed('minEncryptionVersion', 'latestVersion', function() {
+ let { minEncryptionVersion, latestVersion } = this.getProperties('minEncryptionVersion', 'latestVersion');
+ let minVersion = clamp(minEncryptionVersion - 1, 0, latestVersion);
+ let versions = [];
+ while (latestVersion > minVersion) {
+ versions.push(latestVersion);
+ latestVersion--;
+ }
+ return versions;
+ }),
+
+ validKeyVersions: computed('keys', function() {
+ return Object.keys(get(this, 'keys'));
+ }),
+
+ exportKeyTypes: computed('exportable', 'type', function() {
+ let types = ['hmac'];
+ if (this.get('supportsSigning')) {
+ types.unshift('signing');
+ }
+ if (this.get('supportsEncryption')) {
+ types.unshift('encryption');
+ }
+ return types;
+ }),
+
+ backend: attr('string', {
+ readOnly: true,
+ }),
+
+ rotatePath: queryRecord(
+ 'capabilities',
+ context => {
+ const { backend, id } = context.getProperties('backend', 'id');
+ if (!backend && id) {
+ return;
+ }
+ return {
+ id: `${backend}/keys/${id}/rotate`,
+ };
+ },
+ 'id',
+ 'backend'
+ ),
+
+ canRotate: computed.alias('rotatePath.canUpdate'),
+});
diff --git a/ui/app/resolver.js b/ui/app/resolver.js
new file mode 100644
index 000000000..2fb563d6c
--- /dev/null
+++ b/ui/app/resolver.js
@@ -0,0 +1,3 @@
+import Resolver from 'ember-resolver';
+
+export default Resolver;
diff --git a/ui/app/router.js b/ui/app/router.js
new file mode 100644
index 000000000..f3c65df00
--- /dev/null
+++ b/ui/app/router.js
@@ -0,0 +1,126 @@
+import Ember from 'ember';
+import config from './config/environment';
+
+const Router = Ember.Router.extend({
+ location: config.locationType,
+ rootURL: config.rootURL,
+});
+
+Router.map(function() {
+ this.route('vault', { path: '/' }, function() {
+ this.route('cluster', { path: '/:cluster_name' }, function() {
+ this.route('auth');
+ this.route('init');
+ this.route('logout');
+ this.route('settings', function() {
+ this.route('index', { path: '/' });
+ this.route('seal');
+ this.route('auth', function() {
+ this.route('index', { path: '/' });
+ this.route('enable');
+ this.route('configure', { path: '/configure/:method' }, function() {
+ this.route('index', { path: '/' });
+ this.route('section', { path: '/:section_name' });
+ });
+ });
+ this.route('mount-secret-backend');
+ this.route('configure-secret-backend', { path: '/secrets/configure/:backend' }, function() {
+ this.route('index', { path: '/' });
+ this.route('section', { path: '/:section_name' });
+ });
+ });
+ this.route('unseal');
+ this.route('tools', function() {
+ this.route('tool', { path: '/:selectedAction' });
+ });
+ this.route('access', function() {
+ this.route('methods', { path: '/' });
+ this.route('method', { path: '/:path' }, function() {
+ this.route('index', { path: '/' });
+ this.route('section', { path: '/:section_name' });
+ });
+ this.route('leases', function() {
+ // lookup
+ this.route('index', { path: '/' });
+ // lookup prefix
+ // revoke prefix + revoke force
+ this.route('list-root', { path: '/list/' });
+ this.route('list', { path: '/list/*prefix' });
+ //renew + revoke
+ this.route('show', { path: '/show/*lease_id' });
+ });
+ // the outer identity route handles group and entity items
+ this.route('identity', { path: '/identity/:item_type' }, function() {
+ this.route('index', { path: '/' });
+ this.route('create');
+ this.route('merge');
+ this.route('edit', { path: '/edit/:item_id' });
+ this.route('show', { path: '/:item_id/:section' });
+ this.route('aliases', function() {
+ this.route('index', { path: '/' });
+ this.route('add', { path: '/add/:item_id' });
+ this.route('edit', { path: '/edit/:item_alias_id' });
+ this.route('show', { path: '/:item_alias_id/:section' });
+ });
+ });
+ });
+ this.route('secrets', function() {
+ this.route('backends', { path: '/' });
+ this.route('backend', { path: '/:backend' }, function() {
+ this.route('index', { path: '/' });
+ // because globs / params can't be empty,
+ // we have to special-case ids of '' with thier own routes
+ this.route('list-root', { path: '/list/' });
+ this.route('create-root', { path: '/create/' });
+ this.route('show-root', { path: '/show/' });
+ this.route('edit-root', { path: '/edit/' });
+
+ this.route('list', { path: '/list/*secret' });
+ this.route('show', { path: '/show/*secret' });
+ this.route('create', { path: '/create/*secret' });
+ this.route('edit', { path: '/edit/*secret' });
+
+ this.route('credentials-root', { path: '/credentials/' });
+ this.route('credentials', { path: '/credentials/*secret' });
+
+ // ssh sign
+ this.route('sign-root', { path: '/sign/' });
+ this.route('sign', { path: '/sign/*secret' });
+ // transit-specific routes
+ this.route('actions-root', { path: '/actions/' });
+ this.route('actions', { path: '/actions/*secret' });
+ });
+ });
+ this.route('policies', { path: '/policies/:type' }, function() {
+ this.route('index', { path: '/' });
+ this.route('create', { path: '/create' });
+ });
+ this.route('policy', { path: '/policy/:type' }, function() {
+ this.route('show', { path: '/:policy_name' });
+ this.route('edit', { path: '/:policy_name/edit' });
+ });
+ this.route('replication-dr-promote');
+ this.route('replication', function() {
+ this.route('index', { path: '/' });
+ this.route('mode', { path: '/:replication_mode' }, function() {
+ //details
+ this.route('index', { path: '/' });
+ this.route('manage');
+ this.route('secondaries', function() {
+ this.route('add', { path: '/add' });
+ this.route('revoke', { path: '/revoke' });
+ this.route('config-show', { path: '/config/show/:secondary_id' });
+ this.route('config-edit', { path: '/config/edit/:secondary_id' });
+ this.route('config-create', { path: '/config/create/:secondary_id' });
+ });
+ });
+ });
+
+ this.route('response-wrapping');
+ this.route('not-found', { path: '/*path' });
+ });
+ this.route('not-found', { path: '/*path' });
+ });
+});
+
+export default Router;
diff --git a/ui/app/routes/.gitkeep b/ui/app/routes/.gitkeep
new file mode 100644
index 000000000..e69de29bb
diff --git a/ui/app/routes/application.js b/ui/app/routes/application.js
new file mode 100644
index 000000000..d9da2f88a
--- /dev/null
+++ b/ui/app/routes/application.js
@@ -0,0 +1,9 @@
+import Ember from 'ember';
+
+export default Ember.Route.extend({
+ actions: {
+ willTransition() {
+ window.scrollTo(0, 0);
+ },
+ },
+});
diff --git a/ui/app/routes/vault.js b/ui/app/routes/vault.js
new file mode 100644
index 000000000..9a2326ec0
--- /dev/null
+++ b/ui/app/routes/vault.js
@@ -0,0 +1,33 @@
+import Ember from 'ember';
+const SPLASH_DELAY = Ember.testing ? 0 : 300;
+
+export default Ember.Route.extend({
+ version: Ember.inject.service(),
+ beforeModel() {
+ return this.get('version').fetchVersion();
+ },
+ model() {
+ // hardcode single cluster
+ const fixture = {
+ data: {
+ id: '1',
+ type: 'cluster',
+ attributes: {
+ name: 'vault',
+ },
+ },
+ };
+ this.store.push(fixture);
+ return new Ember.RSVP.Promise(resolve => {
+ Ember.run.later(() => {
+ resolve(this.store.peekAll('cluster'));
+ }, SPLASH_DELAY);
+ });
+ },
+
+ redirect(model, transition) {
+ if (model.get('length') === 1 && transition.targetName === 'vault.index') {
+ return this.transitionTo('vault.cluster', model.get('firstObject.name'));
+ }
+ },
+});
diff --git a/ui/app/routes/vault/cluster.js b/ui/app/routes/vault/cluster.js
new file mode 100644
index 000000000..9b86deb70
--- /dev/null
+++ b/ui/app/routes/vault/cluster.js
@@ -0,0 +1,72 @@
+import Ember from 'ember';
+import ClusterRoute from 'vault/mixins/cluster-route';
+import ModelBoundaryRoute from 'vault/mixins/model-boundary-route';
+
+const POLL_INTERVAL_MS = 10000;
+const { inject } = Ember;
+
+export default Ember.Route.extend(ModelBoundaryRoute, ClusterRoute, {
+ store: inject.service(),
+ auth: inject.service(),
+ currentCluster: Ember.inject.service(),
+ modelTypes: ['node', 'secret', 'secret-engine'],
+
+ getClusterId(params) {
+ const { cluster_name } = params;
+ const cluster = this.modelFor('vault').findBy('name', cluster_name);
+ return cluster ? cluster.get('id') : null;
+ },
+
+ beforeModel() {
+ const params = this.paramsFor(this.routeName);
+ const id = this.getClusterId(params);
+ if (id) {
+ return this.get('auth').setCluster(id);
+ } else {
+ return Ember.RSVP.reject({ httpStatus: 404, message: 'not found', path: params.cluster_name });
+ }
+ },
+
+ model(params) {
+ const id = this.getClusterId(params);
+ return this.get('store').findRecord('cluster', id);
+ },
+
+ stopPoll: Ember.on('deactivate', function() {
+ Ember.run.cancel(this.get('timer'));
+ }),
+
+ poll() {
+ // when testing, the polling loop causes promises to never settle so acceptance tests hang
+ // to get around that, we just disable the poll in tests
+ return Ember.testing
+ ? null
+ : Ember.run.later(() => {
+ this.controller.get('model').reload().then(
+ () => {
+ this.set('timer', this.poll());
+ return this.transitionToTargetRoute();
+ },
+ () => {
+ this.set('timer', this.poll());
+ }
+ );
+ }, POLL_INTERVAL_MS);
+ },
+
+ afterModel(model) {
+ this.get('currentCluster').setCluster(model);
+ this._super(...arguments);
+ this.poll();
+ return this.transitionToTargetRoute();
+ },
+
+ actions: {
+ error(e) {
+ if (e.httpStatus === 503 && e.errors[0] === 'Vault is sealed') {
+ this.refresh();
+ }
+ return true;
+ },
+ },
+});
diff --git a/ui/app/routes/vault/cluster/access/identity.js b/ui/app/routes/vault/cluster/access/identity.js
new file mode 100644
index 000000000..ad1545d19
--- /dev/null
+++ b/ui/app/routes/vault/cluster/access/identity.js
@@ -0,0 +1,15 @@
+import Ember from 'ember';
+
+const MODEL_FROM_PARAM = {
+ entities: 'entity',
+ groups: 'group',
+};
+
+export default Ember.Route.extend({
+ model(params) {
+ let model = MODEL_FROM_PARAM[params.item_type];
+
+ //TODO 404 behavior;
+ return model;
+ },
+});
diff --git a/ui/app/routes/vault/cluster/access/identity/aliases/add.js b/ui/app/routes/vault/cluster/access/identity/aliases/add.js
new file mode 100644
index 000000000..5d9af4ba3
--- /dev/null
+++ b/ui/app/routes/vault/cluster/access/identity/aliases/add.js
@@ -0,0 +1,11 @@
+import Ember from 'ember';
+
+export default Ember.Route.extend({
+ model(params) {
+ let itemType = this.modelFor('vault.cluster.access.identity');
+ let modelType = `identity/${itemType}-alias`;
+ return this.store.createRecord(modelType, {
+ canonicalId: params.item_id,
+ });
+ },
+});
diff --git a/ui/app/routes/vault/cluster/access/identity/aliases/edit.js b/ui/app/routes/vault/cluster/access/identity/aliases/edit.js
new file mode 100644
index 000000000..15e9b36dc
--- /dev/null
+++ b/ui/app/routes/vault/cluster/access/identity/aliases/edit.js
@@ -0,0 +1,9 @@
+import Ember from 'ember';
+
+export default Ember.Route.extend({
+ model(params) {
+ let itemType = this.modelFor('vault.cluster.access.identity');
+ let modelType = `identity/${itemType}-alias`;
+ return this.store.findRecord(modelType, params.item_alias_id);
+ },
+});
diff --git a/ui/app/routes/vault/cluster/access/identity/aliases/index.js b/ui/app/routes/vault/cluster/access/identity/aliases/index.js
new file mode 100644
index 000000000..027cc1f7d
--- /dev/null
+++ b/ui/app/routes/vault/cluster/access/identity/aliases/index.js
@@ -0,0 +1,36 @@
+import Ember from 'ember';
+import ListRoute from 'vault/mixins/list-route';
+
+export default Ember.Route.extend(ListRoute, {
+ model(params) {
+ let itemType = this.modelFor('vault.cluster.access.identity');
+ let modelType = `identity/${itemType}-alias`;
+ return this.store
+ .lazyPaginatedQuery(modelType, {
+ responsePath: 'data.keys',
+ page: params.page,
+ pageFilter: params.pageFilter,
+ size: 100,
+ })
+ .catch(err => {
+ if (err.httpStatus === 404) {
+ return [];
+ } else {
+ throw err;
+ }
+ });
+ },
+ setupController(controller) {
+ this._super(...arguments);
+ controller.set('identityType', this.modelFor('vault.cluster.access.identity'));
+ },
+ actions: {
+ willTransition(transition) {
+ window.scrollTo(0, 0);
+ if (transition.targetName !== this.routeName) {
+ this.store.clearAllDatasets();
+ }
+ return true;
+ },
+ },
+});
diff --git a/ui/app/routes/vault/cluster/access/identity/aliases/show.js b/ui/app/routes/vault/cluster/access/identity/aliases/show.js
new file mode 100644
index 000000000..21318791e
--- /dev/null
+++ b/ui/app/routes/vault/cluster/access/identity/aliases/show.js
@@ -0,0 +1,30 @@
+import Ember from 'ember';
+import DS from 'ember-data';
+import { TABS } from 'vault/helpers/tabs-for-identity-show';
+
+export default Ember.Route.extend({
+ model(params) {
+ let { section } = params;
+ let itemType = this.modelFor('vault.cluster.access.identity') + '-alias';
+ let tabs = TABS[itemType];
+ let modelType = `identity/${itemType}`;
+ if (!tabs.includes(section)) {
+ const error = new DS.AdapterError();
+ Ember.set(error, 'httpStatus', 404);
+ throw error;
+ }
+ // TODO peekRecord here to see if we have the record already
+ return Ember.RSVP.hash({
+ model: this.store.findRecord(modelType, params.item_alias_id),
+ section,
+ });
+ },
+
+ setupController(controller, resolvedModel) {
+ let { model, section } = resolvedModel;
+ controller.setProperties({
+ model,
+ section,
+ });
+ },
+});
diff --git a/ui/app/routes/vault/cluster/access/identity/create.js b/ui/app/routes/vault/cluster/access/identity/create.js
new file mode 100644
index 000000000..a147b9deb
--- /dev/null
+++ b/ui/app/routes/vault/cluster/access/identity/create.js
@@ -0,0 +1,9 @@
+import Ember from 'ember';
+
+export default Ember.Route.extend({
+ model() {
+ let itemType = this.modelFor('vault.cluster.access.identity');
+ let modelType = `identity/${itemType}`;
+ return this.store.createRecord(modelType);
+ },
+});
diff --git a/ui/app/routes/vault/cluster/access/identity/edit.js b/ui/app/routes/vault/cluster/access/identity/edit.js
new file mode 100644
index 000000000..df1e5327e
--- /dev/null
+++ b/ui/app/routes/vault/cluster/access/identity/edit.js
@@ -0,0 +1,9 @@
+import Ember from 'ember';
+
+export default Ember.Route.extend({
+ model(params) {
+ let itemType = this.modelFor('vault.cluster.access.identity');
+ let modelType = `identity/${itemType}`;
+ return this.store.findRecord(modelType, params.item_id);
+ },
+});
diff --git a/ui/app/routes/vault/cluster/access/identity/index.js b/ui/app/routes/vault/cluster/access/identity/index.js
new file mode 100644
index 000000000..200061ed7
--- /dev/null
+++ b/ui/app/routes/vault/cluster/access/identity/index.js
@@ -0,0 +1,38 @@
+import Ember from 'ember';
+import ListRoute from 'vault/mixins/list-route';
+
+export default Ember.Route.extend(ListRoute, {
+ model(params) {
+ let itemType = this.modelFor('vault.cluster.access.identity');
+ let modelType = `identity/${itemType}`;
+ return this.store
+ .lazyPaginatedQuery(modelType, {
+ responsePath: 'data.keys',
+ page: params.page,
+ pageFilter: params.pageFilter,
+ size: 100,
+ })
+ .catch(err => {
+ if (err.httpStatus === 404) {
+ return [];
+ } else {
+ throw err;
+ }
+ });
+ },
+
+ setupController(controller) {
+ this._super(...arguments);
+ controller.set('identityType', this.modelFor('vault.cluster.access.identity'));
+ },
+
+ actions: {
+ willTransition(transition) {
+ window.scrollTo(0, 0);
+ if (transition.targetName !== this.routeName) {
+ this.store.clearAllDatasets();
+ }
+ return true;
+ },
+ },
+});
diff --git a/ui/app/routes/vault/cluster/access/identity/merge.js b/ui/app/routes/vault/cluster/access/identity/merge.js
new file mode 100644
index 000000000..5274646dc
--- /dev/null
+++ b/ui/app/routes/vault/cluster/access/identity/merge.js
@@ -0,0 +1,15 @@
+import Ember from 'ember';
+import UnloadModelRoute from 'vault/mixins/unload-model-route';
+
+export default Ember.Route.extend(UnloadModelRoute, {
+ beforeModel() {
+ let itemType = this.modelFor('vault.cluster.access.identity');
+ if (itemType !== 'entity') {
+ return this.transitionTo('vault.cluster.access.identity');
+ }
+ },
+ model() {
+ let modelType = `identity/entity-merge`;
+ return this.store.createRecord(modelType);
+ },
+});
diff --git a/ui/app/routes/vault/cluster/access/identity/show.js b/ui/app/routes/vault/cluster/access/identity/show.js
new file mode 100644
index 000000000..bb4d43277
--- /dev/null
+++ b/ui/app/routes/vault/cluster/access/identity/show.js
@@ -0,0 +1,37 @@
+import Ember from 'ember';
+import DS from 'ember-data';
+import { TABS } from 'vault/helpers/tabs-for-identity-show';
+
+export default Ember.Route.extend({
+ model(params) {
+ let { section } = params;
+ let itemType = this.modelFor('vault.cluster.access.identity');
+ let tabs = TABS[itemType];
+ let modelType = `identity/${itemType}`;
+ if (!tabs.includes(section)) {
+ const error = new DS.AdapterError();
+ Ember.set(error, 'httpStatus', 404);
+ throw error;
+ }
+ // TODO peekRecord here to see if we have the record already
+ return Ember.RSVP.hash({
+ model: this.store.findRecord(modelType, params.item_id),
+ section,
+ });
+ },
+
+ afterModel(resolvedModel) {
+ let { section, model } = resolvedModel;
+ if (model.get('identityType') === 'group' && model.get('type') === 'internal' && section === 'aliases') {
+ return this.transitionTo('vault.cluster.access.identity.show', model.id, 'details');
+ }
+ },
+
+ setupController(controller, resolvedModel) {
+ let { model, section } = resolvedModel;
+ controller.setProperties({
+ model,
+ section,
+ });
+ },
+});
diff --git a/ui/app/routes/vault/cluster/access/leases.js b/ui/app/routes/vault/cluster/access/leases.js
new file mode 100644
index 000000000..019c1fdd5
--- /dev/null
+++ b/ui/app/routes/vault/cluster/access/leases.js
@@ -0,0 +1,8 @@
+import Ember from 'ember';
+import ClusterRoute from 'vault/mixins/cluster-route';
+
+export default Ember.Route.extend(ClusterRoute, {
+ model() {
+ return this.store.findRecord('capabilities', 'sys/leases/lookup/');
+ },
+});
diff --git a/ui/app/routes/vault/cluster/access/leases/index.js b/ui/app/routes/vault/cluster/access/leases/index.js
new file mode 100644
index 000000000..f423ca2b6
--- /dev/null
+++ b/ui/app/routes/vault/cluster/access/leases/index.js
@@ -0,0 +1,14 @@
+import Ember from 'ember';
+
+export default Ember.Route.extend({
+ beforeModel(transition) {
+ if (
+ this.modelFor('vault.cluster.access.leases').get('canList') &&
+ transition.targetName === this.routeName
+ ) {
+ return this.replaceWith('vault.cluster.access.leases.list-root');
+ } else {
+ return;
+ }
+ },
+});
diff --git a/ui/app/routes/vault/cluster/access/leases/list-root.js b/ui/app/routes/vault/cluster/access/leases/list-root.js
new file mode 100644
index 000000000..ea1fedd0a
--- /dev/null
+++ b/ui/app/routes/vault/cluster/access/leases/list-root.js
@@ -0,0 +1 @@
+export { default } from './list';
diff --git a/ui/app/routes/vault/cluster/access/leases/list.js b/ui/app/routes/vault/cluster/access/leases/list.js
new file mode 100644
index 000000000..e92bfcc67
--- /dev/null
+++ b/ui/app/routes/vault/cluster/access/leases/list.js
@@ -0,0 +1,102 @@
+import Ember from 'ember';
+
+export default Ember.Route.extend({
+ queryParams: {
+ page: {
+ refreshModel: true,
+ },
+ pageFilter: {
+ refreshModel: true,
+ },
+ },
+
+ templateName: 'vault/cluster/access/leases/list',
+
+ model(params) {
+ const prefix = params.prefix || '';
+ if (this.modelFor('vault.cluster.access.leases').get('canList')) {
+ return Ember.RSVP.hash({
+ leases: this.store
+ .lazyPaginatedQuery('lease', {
+ prefix,
+ responsePath: 'data.keys',
+ page: params.page,
+ pageFilter: params.pageFilter,
+ size: 100,
+ })
+ .then(model => {
+ this.set('has404', false);
+ return model;
+ })
+ .catch(err => {
+ if (err.httpStatus === 404 && prefix === '') {
+ return [];
+ } else {
+ throw err;
+ }
+ }),
+ capabilities: Ember.RSVP.hash({
+ revokePrefix: this.store.findRecord('capabilities', `sys/leases/revoke-prefix/${prefix}`),
+ forceRevokePrefix: this.store.findRecord('capabilities', `sys/leases/revoke-force/${prefix}`),
+ }),
+ });
+ }
+ },
+
+ setupController(controller, model) {
+ const params = this.paramsFor(this.routeName);
+ const prefix = params.prefix ? params.prefix : '';
+ const has404 = this.get('has404');
+ controller.set('hasModel', true);
+ controller.setProperties({
+ model: model.leases,
+ capabilities: model.capabilities,
+ baseKey: { id: prefix },
+ has404,
+ });
+ if (!has404) {
+ const pageFilter = params.pageFilter;
+ let filter;
+ if (prefix) {
+ filter = prefix + (pageFilter || '');
+ } else if (pageFilter) {
+ filter = pageFilter;
+ }
+ controller.setProperties({
+ filter: filter || '',
+ page: model.leases.get('meta.currentPage'),
+ });
+ }
+ },
+
+ resetController(controller, isExiting) {
+ this._super(...arguments);
+ if (isExiting) {
+ controller.set('filter', '');
+ }
+ },
+
+ actions: {
+ error(error, transition) {
+ const { prefix } = this.paramsFor(this.routeName);
+
+ Ember.set(error, 'keyId', prefix);
+ const hasModel = this.controllerFor(this.routeName).get('hasModel');
+ // only swallow the error if we have a previous model
+ if (hasModel && error.httpStatus === 404) {
+ this.set('has404', true);
+ transition.abort();
+ } else {
+ return true;
+ }
+ },
+
+ willTransition(transition) {
+ window.scrollTo(0, 0);
+ if (transition.targetName !== this.routeName) {
+ this.store.clearAllDatasets();
+ }
+ return true;
+ },
+ },
+});
diff --git a/ui/app/routes/vault/cluster/access/leases/show.js b/ui/app/routes/vault/cluster/access/leases/show.js
new file mode 100644
index 000000000..4d05b5c38
--- /dev/null
+++ b/ui/app/routes/vault/cluster/access/leases/show.js
@@ -0,0 +1,54 @@
+import Ember from 'ember';
+import UnloadModelRoute from 'vault/mixins/unload-model-route';
+
+import utils from 'vault/lib/key-utils';
+
+export default Ember.Route.extend(UnloadModelRoute, {
+ beforeModel() {
+ const { lease_id: leaseId } = this.paramsFor(this.routeName);
+ const parentKey = utils.parentKeyForKey(leaseId);
+ if (utils.keyIsFolder(leaseId)) {
+ if (parentKey) {
+ return this.transitionTo('vault.cluster.access.leases.list', parentKey);
+ } else {
+ return this.transitionTo('vault.cluster.access.leases.list-root');
+ }
+ }
+ },
+
+ model(params) {
+ const { lease_id } = params;
+ return Ember.RSVP.hash({
+ lease: this.store.queryRecord('lease', {
+ lease_id,
+ }),
+ capabilities: Ember.RSVP.hash({
+ renew: this.store.findRecord('capabilities', 'sys/leases/renew'),
+ revoke: this.store.findRecord('capabilities', 'sys/leases/revoke'),
+ leases: this.modelFor('vault.cluster.access.leases'),
+ }),
+ });
+ },
+
+ setupController(controller, model) {
+ this._super(...arguments);
+ const { lease_id: leaseId } = this.paramsFor(this.routeName);
+ controller.setProperties({
+ model: model.lease,
+ capabilities: model.capabilities,
+ baseKey: { id: leaseId },
+ });
+ },
+
+ actions: {
+ error(error) {
+ const { lease_id } = this.paramsFor(this.routeName);
+ Ember.set(error, 'keyId', lease_id);
+ return true;
+ },
+
+ refreshModel() {
+ this.refresh();
+ },
+ },
+});
diff --git a/ui/app/routes/vault/cluster/access/method.js b/ui/app/routes/vault/cluster/access/method.js
new file mode 100644
index 000000000..0291f08ca
--- /dev/null
+++ b/ui/app/routes/vault/cluster/access/method.js
@@ -0,0 +1,17 @@
+import Ember from 'ember';
+import DS from 'ember-data';
+
+export default Ember.Route.extend({
+ model(params) {
+ const { path } = params;
+ return this.store.findAll('auth-method').then(modelArray => {
+ const model = modelArray.findBy('id', path);
+ if (!model) {
+ const error = new DS.AdapterError();
+ Ember.set(error, 'httpStatus', 404);
+ throw error;
+ }
+ return model;
+ });
+ },
+});
diff --git a/ui/app/routes/vault/cluster/access/method/index.js b/ui/app/routes/vault/cluster/access/method/index.js
new file mode 100644
index 000000000..4bcfbd1f5
--- /dev/null
+++ b/ui/app/routes/vault/cluster/access/method/index.js
@@ -0,0 +1,7 @@
+import Ember from 'ember';
+
+export default Ember.Route.extend({
+ beforeModel() {
+ return this.transitionTo('vault.cluster.access.method.section', 'configuration');
+ },
+});
diff --git a/ui/app/routes/vault/cluster/access/method/section.js b/ui/app/routes/vault/cluster/access/method/section.js
new file mode 100644
index 000000000..2c94c6153
--- /dev/null
+++ b/ui/app/routes/vault/cluster/access/method/section.js
@@ -0,0 +1,20 @@
+import Ember from 'ember';
+import DS from 'ember-data';
+
+export default Ember.Route.extend({
+ model(params) {
+ const { section_name: section } = params;
+ if (section !== 'configuration') {
+ const error = new DS.AdapterError();
+ Ember.set(error, 'httpStatus', 404);
+ throw error;
+ }
+ return this.modelFor('vault.cluster.access.method');
+ },
+
+ setupController(controller) {
+ const { section_name: section } = this.paramsFor(this.routeName);
+ this._super(...arguments);
+ controller.set('section', section);
+ },
+});
diff --git a/ui/app/routes/vault/cluster/access/methods.js b/ui/app/routes/vault/cluster/access/methods.js
new file mode 100644
index 000000000..84e53dccf
--- /dev/null
+++ b/ui/app/routes/vault/cluster/access/methods.js
@@ -0,0 +1,16 @@
+import Ember from 'ember';
+
+export default Ember.Route.extend({
+ queryParams: {
+ page: {
+ refreshModel: true,
+ },
+ pageFilter: {
+ refreshModel: true,
+ },
+ },
+
+ model() {
+ return this.store.findAll('auth-method');
+ },
+});
diff --git a/ui/app/routes/vault/cluster/auth.js b/ui/app/routes/vault/cluster/auth.js
new file mode 100644
index 000000000..875583f71
--- /dev/null
+++ b/ui/app/routes/vault/cluster/auth.js
@@ -0,0 +1 @@
+export { default } from './cluster-route-base';
diff --git a/ui/app/routes/vault/cluster/cluster-route-base.js b/ui/app/routes/vault/cluster/cluster-route-base.js
new file mode 100644
index 000000000..f8e876d83
--- /dev/null
+++ b/ui/app/routes/vault/cluster/cluster-route-base.js
@@ -0,0 +1,15 @@
+// this is the base route for
+// all of the CLUSTER_ROUTES that are states before you can use vault
+//
+import Ember from 'ember';
+import ClusterRoute from 'vault/mixins/cluster-route';
+
+export default Ember.Route.extend(ClusterRoute, {
+ model() {
+ return this.modelFor('vault.cluster');
+ },
+
+ resetController(controller) {
+ controller.reset && controller.reset();
+ },
+});
diff --git a/ui/app/routes/vault/cluster/index.js b/ui/app/routes/vault/cluster/index.js
new file mode 100644
index 000000000..0b5995841
--- /dev/null
+++ b/ui/app/routes/vault/cluster/index.js
@@ -0,0 +1,7 @@
+import Ember from 'ember';
+
+export default Ember.Route.extend({
+ beforeModel() {
+ return this.transitionTo('vault.cluster.secrets');
+ },
+});
diff --git a/ui/app/routes/vault/cluster/init.js b/ui/app/routes/vault/cluster/init.js
new file mode 100644
index 000000000..875583f71
--- /dev/null
+++ b/ui/app/routes/vault/cluster/init.js
@@ -0,0 +1 @@
+export { default } from './cluster-route-base';
diff --git a/ui/app/routes/vault/cluster/logout.js b/ui/app/routes/vault/cluster/logout.js
new file mode 100644
index 000000000..0ddc7c110
--- /dev/null
+++ b/ui/app/routes/vault/cluster/logout.js
@@ -0,0 +1,16 @@
+import Ember from 'ember';
+import ModelBoundaryRoute from 'vault/mixins/model-boundary-route';
+
+export default Ember.Route.extend(ModelBoundaryRoute, {
+ auth: Ember.inject.service(),
+ flashMessages: Ember.inject.service(),
+
+ modelTypes: ['secret', 'secret-engine'],
+
+ beforeModel() {
+ this.get('auth').deleteCurrentToken();
+ this.clearModelCache();
+ this.replaceWith('vault.cluster');
+ this.get('flashMessages').clearMessages();
+ },
+});
diff --git a/ui/app/routes/vault/cluster/policies.js b/ui/app/routes/vault/cluster/policies.js
new file mode 100644
index 000000000..401e74c64
--- /dev/null
+++ b/ui/app/routes/vault/cluster/policies.js
@@ -0,0 +1,23 @@
+import Ember from 'ember';
+import ClusterRoute from 'vault/mixins/cluster-route';
+
+const ALLOWED_TYPES = ['acl', 'egp', 'rgp'];
+const { inject } = Ember;
+
+export default Ember.Route.extend(ClusterRoute, {
+ version: inject.service(),
+
+ beforeModel() {
+ return this.get('version').fetchFeatures().then(() => {
+ return this._super(...arguments);
+ });
+ },
+
+ model(params) {
+ let policyType = params.type;
+ if (!ALLOWED_TYPES.includes(policyType)) {
+ return this.transitionTo(this.routeName, ALLOWED_TYPES[0]);
+ }
+ return {};
+ },
+});
diff --git a/ui/app/routes/vault/cluster/policies/create.js b/ui/app/routes/vault/cluster/policies/create.js
new file mode 100644
index 000000000..92c438f37
--- /dev/null
+++ b/ui/app/routes/vault/cluster/policies/create.js
@@ -0,0 +1,25 @@
+import Ember from 'ember';
+import UnloadModelRoute from 'vault/mixins/unload-model-route';
+import UnsavedModelRoute from 'vault/mixins/unsaved-model-route';
+
+const { inject } = Ember;
+export default Ember.Route.extend(UnloadModelRoute, UnsavedModelRoute, {
+ version: inject.service(),
+ model() {
+ let policyType = this.policyType();
+
+ if (!this.get('version.features').includes('Sentinel') && policyType !== 'acl') {
+ return this.transitionTo('vault.cluster.policies', policyType);
+ }
+ return this.store.createRecord(`policy/${policyType}`, {});
+ },
+
+ setupController(controller) {
+ this._super(...arguments);
+ controller.set('policyType', this.policyType());
+ },
+
+ policyType() {
+ return this.paramsFor('vault.cluster.policies').type;
+ },
+});
diff --git a/ui/app/routes/vault/cluster/policies/index.js b/ui/app/routes/vault/cluster/policies/index.js
new file mode 100644
index 000000000..50ff37289
--- /dev/null
+++ b/ui/app/routes/vault/cluster/policies/index.js
@@ -0,0 +1,79 @@
+import Ember from 'ember';
+import ClusterRoute from 'vault/mixins/cluster-route';
+
+const { inject } = Ember;
+
+export default Ember.Route.extend(ClusterRoute, {
+ version: inject.service(),
+ queryParams: {
+ page: {
+ refreshModel: true,
+ },
+ pageFilter: {
+ refreshModel: true,
+ },
+ },
+
+ shouldReturnEmptyModel(policyType, version) {
+ return policyType !== 'acl' && (version.get('isOSS') || !version.get('features').includes('Sentinel'));
+ },
+
+ model(params) {
+ let policyType = this.policyType();
+ if (this.shouldReturnEmptyModel(policyType, this.get('version'))) {
+ return;
+ }
+ return this.store
+ .lazyPaginatedQuery(`policy/${policyType}`, {
+ page: params.page,
+ pageFilter: params.pageFilter,
+ responsePath: 'data.keys',
+ size: 100,
+ })
+ .catch(err => {
+ // acls will never be empty, but sentinel policies can be
+ if (err.httpStatus === 404 && this.policyType() !== 'acl') {
+ return [];
+ } else {
+ throw err;
+ }
+ });
+ },
+
+ setupController(controller, model) {
+ const params = this.paramsFor(this.routeName);
+ if (!model) {
+ controller.setProperties({
+ model: null,
+ policyType: this.policyType(),
+ });
+ return;
+ }
+ controller.setProperties({
+ model,
+ filter: params.pageFilter || '',
+ page: model.get('meta.currentPage') || 1,
+ policyType: this.policyType(),
+ });
+ },
+
+ resetController(controller, isExiting) {
+ this._super(...arguments);
+ if (isExiting) {
+ controller.set('filter', '');
+ }
+ },
+ actions: {
+ willTransition(transition) {
+ window.scrollTo(0, 0);
+ if (!transition || transition.targetName !== this.routeName) {
+ this.store.clearAllDatasets();
+ }
+ return true;
+ },
+ },
+
+ policyType() {
+ return this.paramsFor('vault.cluster.policies').type;
+ },
+});
diff --git a/ui/app/routes/vault/cluster/policy.js b/ui/app/routes/vault/cluster/policy.js
new file mode 100644
index 000000000..168b05e7d
--- /dev/null
+++ b/ui/app/routes/vault/cluster/policy.js
@@ -0,0 +1,24 @@
+import Ember from 'ember';
+import ClusterRoute from 'vault/mixins/cluster-route';
+
+const ALLOWED_TYPES = ['acl', 'egp', 'rgp'];
+const { inject } = Ember;
+
+export default Ember.Route.extend(ClusterRoute, {
+ version: inject.service(),
+ beforeModel() {
+ return this.get('version').fetchFeatures().then(() => {
+ return this._super(...arguments);
+ });
+ },
+ model(params) {
+ let policyType = params.type;
+ if (!ALLOWED_TYPES.includes(policyType)) {
+ return this.transitionTo('vault.cluster.policies', ALLOWED_TYPES[0]);
+ }
+ if (!this.get('version.features').includes('Sentinel') && policyType !== 'acl') {
+ return this.transitionTo('vault.cluster.policies', policyType);
+ }
+ return {};
+ },
+});
diff --git a/ui/app/routes/vault/cluster/policy/edit.js b/ui/app/routes/vault/cluster/policy/edit.js
new file mode 100644
index 000000000..c0743fc75
--- /dev/null
+++ b/ui/app/routes/vault/cluster/policy/edit.js
@@ -0,0 +1,4 @@
+import UnsavedModelRoute from 'vault/mixins/unsaved-model-route';
+import ShowRoute from './show';
+
+export default ShowRoute.extend(UnsavedModelRoute);
diff --git a/ui/app/routes/vault/cluster/policy/index.js b/ui/app/routes/vault/cluster/policy/index.js
new file mode 100644
index 000000000..2e6e24994
--- /dev/null
+++ b/ui/app/routes/vault/cluster/policy/index.js
@@ -0,0 +1,7 @@
+import Ember from 'ember';
+
+export default Ember.Route.extend({
+ beforeModel() {
+ return this.transitionTo('vault.cluster.policies', 'acl');
+ },
+});
diff --git a/ui/app/routes/vault/cluster/policy/show.js b/ui/app/routes/vault/cluster/policy/show.js
new file mode 100644
index 000000000..f6a34066e
--- /dev/null
+++ b/ui/app/routes/vault/cluster/policy/show.js
@@ -0,0 +1,32 @@
+import Ember from 'ember';
+import UnloadModelRoute from 'vault/mixins/unload-model-route';
+
+export default Ember.Route.extend(UnloadModelRoute, {
+ beforeModel() {
+ const params = this.paramsFor(this.routeName);
+ let policyType = this.policyType();
+ if (policyType === 'acl' && params.policy_name === 'root') {
+ return this.transitionTo('vault.cluster.policies', 'acl');
+ }
+ },
+
+ model(params) {
+ let type = this.policyType();
+ return Ember.RSVP.hash({
+ policy: this.store.findRecord(`policy/${type}`, params.policy_name),
+ capabilities: this.store.findRecord('capabilities', `sys/policies/${type}/${params.policy_name}`),
+ });
+ },
+
+ setupController(controller, model) {
+ controller.setProperties({
+ model: model.policy,
+ capabilities: model.capabilities,
+ policyType: this.policyType(),
+ });
+ },
+
+ policyType() {
+ return this.paramsFor('vault.cluster.policy').type;
+ },
+});
diff --git a/ui/app/routes/vault/cluster/replication-dr-promote.js b/ui/app/routes/vault/cluster/replication-dr-promote.js
new file mode 100644
index 000000000..967e8ab5b
--- /dev/null
+++ b/ui/app/routes/vault/cluster/replication-dr-promote.js
@@ -0,0 +1,10 @@
+import Ember from 'ember';
+import Base from './cluster-route-base';
+
+export default Base.extend({
+ replicationMode: Ember.inject.service(),
+ beforeModel() {
+ this._super(...arguments);
+ this.get('replicationMode').setMode('dr');
+ },
+});
diff --git a/ui/app/routes/vault/cluster/replication.js b/ui/app/routes/vault/cluster/replication.js
new file mode 100644
index 000000000..083bca7bb
--- /dev/null
+++ b/ui/app/routes/vault/cluster/replication.js
@@ -0,0 +1,36 @@
+import Ember from 'ember';
+import ClusterRoute from 'vault/mixins/cluster-route';
+const { inject } = Ember;
+
+export default Ember.Route.extend(ClusterRoute, {
+ version: inject.service(),
+
+ beforeModel() {
+ return this.get('version').fetchFeatures().then(() => {
+ return this._super(...arguments);
+ });
+ },
+
+ model() {
+ return this.modelFor('vault.cluster');
+ },
+
+ afterModel(model) {
+ return Ember.RSVP
+ .hash({
+ canEnablePrimary: this.store
+ .findRecord('capabilities', 'sys/replication/primary/enable')
+ .then(c => c.get('canUpdate')),
+ canEnableSecondary: this.store
+ .findRecord('capabilities', 'sys/replication/secondary/enable')
+ .then(c => c.get('canUpdate')),
+ })
+ .then(({ canEnablePrimary, canEnableSecondary }) => {
+ Ember.setProperties(model, {
+ canEnablePrimary,
+ canEnableSecondary,
+ });
+ return model;
+ });
+ },
+});
diff --git a/ui/app/routes/vault/cluster/replication/index.js b/ui/app/routes/vault/cluster/replication/index.js
new file mode 100644
index 000000000..ca621413a
--- /dev/null
+++ b/ui/app/routes/vault/cluster/replication/index.js
@@ -0,0 +1,11 @@
+import Ember from 'ember';
+
+export default Ember.Route.extend({
+ replicationMode: Ember.inject.service(),
+ beforeModel() {
+ this.get('replicationMode').setMode(null);
+ },
+ model() {
+ return this.modelFor('vault.cluster.replication');
+ },
+});
diff --git a/ui/app/routes/vault/cluster/replication/mode.js b/ui/app/routes/vault/cluster/replication/mode.js
new file mode 100644
index 000000000..810c12c7e
--- /dev/null
+++ b/ui/app/routes/vault/cluster/replication/mode.js
@@ -0,0 +1,25 @@
+import Ember from 'ember';
+
+const SUPPORTED_REPLICATION_MODES = ['dr', 'performance'];
+
+export default Ember.Route.extend({
+ replicationMode: Ember.inject.service(),
+
+ beforeModel() {
+ const replicationMode = this.paramsFor(this.routeName).replication_mode;
+ if (!SUPPORTED_REPLICATION_MODES.includes(replicationMode)) {
+ return this.transitionTo('vault.cluster.replication');
+ } else {
+ return this._super(...arguments);
+ }
+ },
+
+ model() {
+ return this.modelFor('vault.cluster.replication');
+ },
+
+ setReplicationMode: Ember.on('activate', 'enter', function() {
+ const replicationMode = this.paramsFor(this.routeName).replication_mode;
+ this.get('replicationMode').setMode(replicationMode);
+ }),
+});
diff --git a/ui/app/routes/vault/cluster/replication/mode/index.js b/ui/app/routes/vault/cluster/replication/mode/index.js
new file mode 100644
index 000000000..72dabdd5c
--- /dev/null
+++ b/ui/app/routes/vault/cluster/replication/mode/index.js
@@ -0,0 +1,12 @@
+import Ember from 'ember';
+
+export default Ember.Route.extend({
+ replicationMode: Ember.inject.service(),
+ beforeModel() {
+ const replicationMode = this.paramsFor('vault.cluster.replication.mode').replication_mode;
+ this.get('replicationMode').setMode(replicationMode);
+ },
+ model() {
+ return this.modelFor('vault.cluster.replication.mode');
+ },
+});
diff --git a/ui/app/routes/vault/cluster/replication/mode/manage.js b/ui/app/routes/vault/cluster/replication/mode/manage.js
new file mode 100644
index 000000000..83e48e3fc
--- /dev/null
+++ b/ui/app/routes/vault/cluster/replication/mode/manage.js
@@ -0,0 +1,46 @@
+import Ember from 'ember';
+import { replicationActionForMode } from 'vault/helpers/replication-action-for-mode';
+
+const pathForAction = (action, replicationMode, clusterMode) => {
+ let path;
+ if (action === 'reindex' || action === 'recover') {
+ path = `sys/replication/${action}`;
+ } else {
+ path = `sys/replication/${replicationMode}/${clusterMode}/${action}`;
+ }
+ return path;
+};
+
+export default Ember.Route.extend({
+ store: Ember.inject.service(),
+ model() {
+ const store = this.get('store');
+ const model = this.modelFor('vault.cluster.replication.mode');
+
+ const replicationMode = this.paramsFor('vault.cluster.replication.mode').replication_mode;
+ const clusterMode = model.get(replicationMode).get('modeForUrl');
+ const actions = replicationActionForMode([replicationMode, clusterMode]);
+ return Ember.RSVP
+ .all(
+ actions.map(action => {
+ return store.findRecord('capabilities', pathForAction(action)).then(capability => {
+ model.set(`can${Ember.String.camelize(action)}`, capability.get('canUpdate'));
+ });
+ })
+ )
+ .then(() => {
+ return model;
+ });
+ },
+
+ beforeModel() {
+ const model = this.modelFor('vault.cluster.replication.mode');
+ const replicationMode = this.paramsFor('vault.cluster.replication.mode').replication_mode;
+ if (
+ model.get(replicationMode).get('replicationDisabled') ||
+ model.get(replicationMode).get('replicationUnsupported')
+ ) {
+ return this.transitionTo('vault.cluster.replication.mode', replicationMode);
+ }
+ },
+});
diff --git a/ui/app/routes/vault/cluster/replication/mode/secondaries.js b/ui/app/routes/vault/cluster/replication/mode/secondaries.js
new file mode 100644
index 000000000..30b1d2e4b
--- /dev/null
+++ b/ui/app/routes/vault/cluster/replication/mode/secondaries.js
@@ -0,0 +1,35 @@
+import Ember from 'ember';
+
+export default Ember.Route.extend({
+ model() {
+ const replicationMode = this.paramsFor('vault.cluster.replication.mode').replication_mode;
+
+ return Ember.RSVP
+ .hash({
+ cluster: this.modelFor('vault.cluster.replication.mode'),
+ canAddSecondary: this.store
+ .findRecord('capabilities', `sys/replication/${replicationMode}/primary/secondary-token`)
+ .then(c => c.get('canUpdate')),
+ canRevokeSecondary: this.store
+ .findRecord('capabilities', `sys/replication/${replicationMode}/primary/revoke-secondary`)
+ .then(c => c.get('canUpdate')),
+ })
+ .then(({ cluster, canAddSecondary, canRevokeSecondary }) => {
+ Ember.setProperties(cluster, {
+ canRevokeSecondary,
+ canAddSecondary,
+ });
+ return cluster;
+ });
+ },
+ afterModel(model) {
+ const replicationMode = this.paramsFor('vault.cluster.replication.mode').replication_mode;
+ if (
+ !model.get(`${replicationMode}.isPrimary`) ||
+ model.get(`${replicationMode}.replicationDisabled`) ||
+ model.get(`${replicationMode}.replicationUnsupported`)
+ ) {
+ return this.transitionTo('vault.cluster.replication.mode', replicationMode);
+ }
+ },
+});
diff --git a/ui/app/routes/vault/cluster/replication/mode/secondaries/add.js b/ui/app/routes/vault/cluster/replication/mode/secondaries/add.js
new file mode 100644
index 000000000..2436e0da0
--- /dev/null
+++ b/ui/app/routes/vault/cluster/replication/mode/secondaries/add.js
@@ -0,0 +1,27 @@
+import Ember from 'ember';
+import Base from '../../replication-base';
+
+export default Base.extend({
+ model() {
+ return Ember.RSVP.hash({
+ cluster: this.modelFor('vault.cluster.replication.mode.secondaries'),
+ mounts: this.fetchMounts(),
+ });
+ },
+
+ redirect(model) {
+ const replicationMode = this.get('replicationMode');
+ if (!model.cluster.get(`${replicationMode}.isPrimary`) || !model.cluster.get('canAddSecondary')) {
+ return this.transitionTo('vault.cluster.replication.mode', model.cluster.get('name'), replicationMode);
+ }
+ },
+
+ setupController(controller, model) {
+ controller.set('model', model.cluster);
+ controller.set('mounts', model.mounts);
+ },
+
+ resetController(controller) {
+ controller.reset();
+ },
+});
diff --git a/ui/app/routes/vault/cluster/replication/mode/secondaries/config-create.js b/ui/app/routes/vault/cluster/replication/mode/secondaries/config-create.js
new file mode 100644
index 000000000..67dfcf505
--- /dev/null
+++ b/ui/app/routes/vault/cluster/replication/mode/secondaries/config-create.js
@@ -0,0 +1,54 @@
+import Ember from 'ember';
+import Base from '../../replication-base';
+
+export default Base.extend({
+ flashMessages: Ember.inject.service(),
+
+ modelPath: 'model.config',
+
+ findOrCreate(id) {
+ const flash = this.get('flashMessages');
+ return this.store
+ .findRecord('mount-filter-config', id)
+ .then(() => {
+ // if we find a record, transition to the edit view
+ return this.transitionTo('vault.cluster.replication.mode.secondaries.config-edit', id)
+ .followRedirects()
+ .then(() => {
+ flash.info(
+ `${id} already had a mount filter config, so we loaded the config edit screen for you.`
+ );
+ });
+ })
+ .catch(e => {
+ if (e.httpStatus === 404) {
+ return this.store.createRecord('mount-filter-config', {
+ id,
+ });
+ } else {
+ throw e;
+ }
+ });
+ },
+
+ redirect(model) {
+ const cluster = model.cluster;
+ const replicationMode = this.get('replicationMode');
+ if (
+ !this.get('version.hasPerfReplication') ||
+ replicationMode !== 'performance' ||
+ !cluster.get(`${replicationMode}.isPrimary`) ||
+ !cluster.get('canAddSecondary')
+ ) {
+ return this.transitionTo('vault.cluster.replication.mode', cluster.get('name'), replicationMode);
+ }
+ },
+
+ model(params) {
+ return Ember.RSVP.hash({
+ cluster: this.modelFor('vault.cluster.replication.mode'),
+ config: this.findOrCreate(params.secondary_id),
+ mounts: this.fetchMounts(),
+ });
+ },
+});
diff --git a/ui/app/routes/vault/cluster/replication/mode/secondaries/config-edit.js b/ui/app/routes/vault/cluster/replication/mode/secondaries/config-edit.js
new file mode 100644
index 000000000..4c82b19e9
--- /dev/null
+++ b/ui/app/routes/vault/cluster/replication/mode/secondaries/config-edit.js
@@ -0,0 +1,27 @@
+import Ember from 'ember';
+import Base from '../../replication-base';
+
+export default Base.extend({
+ modelPath: 'model.config',
+
+ model(params) {
+ return Ember.RSVP.hash({
+ cluster: this.modelFor('vault.cluster.replication.mode.secondaries'),
+ config: this.store.findRecord('mount-filter-config', params.secondary_id),
+ mounts: this.fetchMounts(),
+ });
+ },
+
+ redirect(model) {
+ const cluster = model.cluster;
+ const replicationMode = this.get('replicationMode');
+ if (
+ !this.get('version.hasPerfReplication') ||
+ replicationMode !== 'performance' ||
+ !cluster.get(`${replicationMode}.isPrimary`) ||
+ !cluster.get('canAddSecondary')
+ ) {
+ return this.transitionTo('vault.cluster.replication.mode', cluster.get('name'), replicationMode);
+ }
+ },
+});
diff --git a/ui/app/routes/vault/cluster/replication/mode/secondaries/config-show.js b/ui/app/routes/vault/cluster/replication/mode/secondaries/config-show.js
new file mode 100644
index 000000000..44d164ff8
--- /dev/null
+++ b/ui/app/routes/vault/cluster/replication/mode/secondaries/config-show.js
@@ -0,0 +1,32 @@
+import Ember from 'ember';
+import Base from '../../replication-base';
+
+export default Base.extend({
+ modelPath: 'model.config',
+
+ model(params) {
+ const id = params.secondary_id;
+ return Ember.RSVP.hash({
+ cluster: this.modelFor('vault.cluster.replication'),
+ config: this.store.findRecord('mount-filter-config', id).catch(e => {
+ if (e.httpStatus === 404) {
+ // return an empty obj to let them nav to create
+ return Ember.RSVP.resolve({ id });
+ } else {
+ throw e;
+ }
+ }),
+ });
+ },
+ redirect(model) {
+ const cluster = model.cluster;
+ const replicationMode = this.paramsFor('vault.cluster.replication.mode').replication_mode;
+ if (
+ !this.get('version.hasPerfReplication') ||
+ replicationMode !== 'performance' ||
+ !cluster.get(`${replicationMode}.isPrimary`)
+ ) {
+ return this.transitionTo('vault.cluster.replication.mode', cluster.get('name'), replicationMode);
+ }
+ },
+});
diff --git a/ui/app/routes/vault/cluster/replication/mode/secondaries/revoke.js b/ui/app/routes/vault/cluster/replication/mode/secondaries/revoke.js
new file mode 100644
index 000000000..0ceaf8ea0
--- /dev/null
+++ b/ui/app/routes/vault/cluster/replication/mode/secondaries/revoke.js
@@ -0,0 +1,18 @@
+import Base from '../../replication-base';
+
+export default Base.extend({
+ model() {
+ return this.modelFor('vault.cluster.replication.secondaries');
+ },
+
+ redirect(model) {
+ const replicationMode = this.get('replicationMode');
+ if (!model.get(`${replicationMode}.isPrimary`) || !model.get('canRevokeSecondary')) {
+ return this.transitionTo('vault.cluster.replication', model.get('name'));
+ }
+ },
+
+ resetController(controller) {
+ controller.reset();
+ },
+});
diff --git a/ui/app/routes/vault/cluster/replication/replication-base.js b/ui/app/routes/vault/cluster/replication/replication-base.js
new file mode 100644
index 000000000..9749412ce
--- /dev/null
+++ b/ui/app/routes/vault/cluster/replication/replication-base.js
@@ -0,0 +1,20 @@
+import Ember from 'ember';
+import UnloadModelRouteMixin from 'vault/mixins/unload-model-route';
+
+export default Ember.Route.extend(UnloadModelRouteMixin, {
+ modelPath: 'model.config',
+ fetchMounts() {
+ return Ember.RSVP
+ .hash({
+ mounts: this.store.findAll('secret-engine'),
+ auth: this.store.findAll('auth-method'),
+ })
+ .then(({ mounts, auth }) => {
+ return Ember.RSVP.resolve(mounts.toArray().concat(auth.toArray()));
+ });
+ },
+
+ version: Ember.inject.service(),
+ rm: Ember.inject.service('replication-mode'),
+ replicationMode: Ember.computed.alias('rm.mode'),
+});
diff --git a/ui/app/routes/vault/cluster/secrets.js b/ui/app/routes/vault/cluster/secrets.js
new file mode 100644
index 000000000..d86c74ead
--- /dev/null
+++ b/ui/app/routes/vault/cluster/secrets.js
@@ -0,0 +1,8 @@
+import Ember from 'ember';
+import ClusterRoute from 'vault/mixins/cluster-route';
+
+export default Ember.Route.extend(ClusterRoute, {
+ model() {
+ return this.store.query('secret-engine', {});
+ },
+});
diff --git a/ui/app/routes/vault/cluster/secrets/backend.js b/ui/app/routes/vault/cluster/secrets/backend.js
new file mode 100644
index 000000000..58b73d607
--- /dev/null
+++ b/ui/app/routes/vault/cluster/secrets/backend.js
@@ -0,0 +1,20 @@
+import Ember from 'ember';
+const { inject } = Ember;
+export default Ember.Route.extend({
+ flashMessages: inject.service(),
+ beforeModel(transition) {
+ const target = transition.targetName;
+ const { backend } = this.paramsFor(this.routeName);
+ const backendModel = this.store.peekRecord('secret-engine', backend);
+ const type = backendModel && backendModel.get('type');
+ if (type === 'kv' && backendModel.get('isVersioned')) {
+ this.get('flashMessages').stickyInfo(
+ `"${backend}" is a versioned kv secrets engine. The Vault UI does not currently support the additional versioning features. All actions taken through the UI in this engine will operate on the most recent version of a secret.`
+ );
+ }
+
+ if (target === this.routeName) {
+ return this.replaceWith('vault.cluster.secrets.backend.list-root', backend);
+ }
+ },
+});
diff --git a/ui/app/routes/vault/cluster/secrets/backend/actions.js b/ui/app/routes/vault/cluster/secrets/backend/actions.js
new file mode 100644
index 000000000..a33e06d93
--- /dev/null
+++ b/ui/app/routes/vault/cluster/secrets/backend/actions.js
@@ -0,0 +1,30 @@
+import EditBase from './secret-edit';
+import utils from 'vault/lib/key-utils';
+
+export default EditBase.extend({
+ queryParams: {
+ selectedAction: {
+ replace: true,
+ },
+ },
+
+ templateName: 'vault/cluster/secrets/backend/transitActionsLayout',
+
+ beforeModel() {
+ const { secret } = this.paramsFor(this.routeName);
+ const parentKey = utils.parentKeyForKey(secret);
+ const { backend } = this.paramsFor('vault.cluster.secrets.backend');
+ if (this.backendType(backend) !== 'transit') {
+ if (parentKey) {
+ return this.transitionTo('vault.cluster.secrets.backend.show', parentKey);
+ } else {
+ return this.transitionTo('vault.cluster.secrets.backend.show-root');
+ }
+ }
+ },
+ setupController(controller, model) {
+ this._super(...arguments);
+ const { selectedAction } = this.paramsFor(this.routeName);
+ controller.set('selectedAction', selectedAction || model.secret.get('supportedActions.firstObject'));
+ },
+});
diff --git a/ui/app/routes/vault/cluster/secrets/backend/create-root.js b/ui/app/routes/vault/cluster/secrets/backend/create-root.js
new file mode 100644
index 000000000..28d5c8ccc
--- /dev/null
+++ b/ui/app/routes/vault/cluster/secrets/backend/create-root.js
@@ -0,0 +1 @@
+export { default } from './create';
diff --git a/ui/app/routes/vault/cluster/secrets/backend/create.js b/ui/app/routes/vault/cluster/secrets/backend/create.js
new file mode 100644
index 000000000..a3bf26844
--- /dev/null
+++ b/ui/app/routes/vault/cluster/secrets/backend/create.js
@@ -0,0 +1,52 @@
+import Ember from 'ember';
+import EditBase from './secret-edit';
+import KeyMixin from 'vault/models/key-mixin';
+
+var SecretProxy = Ember.Object.extend(KeyMixin, {
+ store: null,
+
+ toModel() {
+ return this.getProperties('id', 'secretData', 'backend');
+ },
+
+ createRecord(backend) {
+ let modelType = 'secret';
+ if (backend === 'cubbyhole') {
+ modelType = modelType + '-cubbyhole';
+ }
+ return this.store.createRecord(modelType, this.toModel());
+ },
+});
+
+export default EditBase.extend({
+ createModel(transition, parentKey) {
+ const { backend } = this.paramsFor('vault.cluster.secrets.backend');
+ const modelType = this.modelType(backend);
+ if (modelType === 'role-ssh') {
+ return this.store.createRecord(modelType, { keyType: 'ca' });
+ }
+ if (modelType !== 'secret' && modelType !== 'secret-cubbyhole') {
+ return this.store.createRecord(modelType);
+ }
+ const key = transition.queryParams.initialKey || '';
+ const model = SecretProxy.create({
+ initialParentKey: parentKey,
+ store: this.store,
+ });
+
+ if (key) {
+ // have to set this after so that it will be
+ // computed properly in the template (it's dependent on `initialParentKey`)
+ model.set('keyWithoutParent', key);
+ }
+ return model;
+ },
+
+ model(params, transition) {
+ const parentKey = params.secret ? params.secret : '';
+ return Ember.RSVP.hash({
+ secret: this.createModel(transition, parentKey),
+ capabilities: this.capabilities(parentKey),
+ });
+ },
+});
diff --git a/ui/app/routes/vault/cluster/secrets/backend/credentials-root.js b/ui/app/routes/vault/cluster/secrets/backend/credentials-root.js
new file mode 100644
index 000000000..cbf926c3a
--- /dev/null
+++ b/ui/app/routes/vault/cluster/secrets/backend/credentials-root.js
@@ -0,0 +1 @@
+export { default } from './credentials';
diff --git a/ui/app/routes/vault/cluster/secrets/backend/credentials.js b/ui/app/routes/vault/cluster/secrets/backend/credentials.js
new file mode 100644
index 000000000..034117161
--- /dev/null
+++ b/ui/app/routes/vault/cluster/secrets/backend/credentials.js
@@ -0,0 +1,52 @@
+import Ember from 'ember';
+import UnloadModel from 'vault/mixins/unload-model-route';
+
+const SUPPORTED_DYNAMIC_BACKENDS = ['ssh', 'aws', 'pki'];
+
+export default Ember.Route.extend(UnloadModel, {
+ templateName: 'vault/cluster/secrets/backend/credentials',
+
+ backendModel() {
+ const backend = this.paramsFor('vault.cluster.secrets.backend').backend;
+ return this.store.peekRecord('secret-engine', backend);
+ },
+
+ pathQuery(role, backend) {
+ const type = this.backendModel().get('type');
+ if (type === 'pki') {
+ return `${backend}/issue/${role}`;
+ }
+ return `${backend}/creds/${role}`;
+ },
+
+ model(params) {
+ const role = params.secret;
+ const backendModel = this.backendModel();
+ const backend = backendModel.get('id');
+
+ if (!SUPPORTED_DYNAMIC_BACKENDS.includes(backendModel.get('type'))) {
+ return this.transitionTo('vault.cluster.secrets.backend.list-root', backend);
+ }
+ return this.store
+ .queryRecord('capabilities', { id: this.pathQuery(role, backend) })
+ .then(capabilities => {
+ if (!capabilities.get('canUpdate')) {
+ return this.transitionTo('vault.cluster.secrets.backend.list-root', backend);
+ }
+ return Ember.RSVP.resolve({
+ backend,
+ id: role,
+ name: role,
+ });
+ });
+ },
+
+ setupController(controller) {
+ this._super(...arguments);
+ controller.set('backend', this.backendModel());
+ },
+
+ resetController(controller) {
+ controller.reset();
+ },
+});
diff --git a/ui/app/routes/vault/cluster/secrets/backend/edit-root.js b/ui/app/routes/vault/cluster/secrets/backend/edit-root.js
new file mode 100644
index 000000000..58cfd6a1c
--- /dev/null
+++ b/ui/app/routes/vault/cluster/secrets/backend/edit-root.js
@@ -0,0 +1 @@
+export { default } from './edit';
diff --git a/ui/app/routes/vault/cluster/secrets/backend/edit.js b/ui/app/routes/vault/cluster/secrets/backend/edit.js
new file mode 100644
index 000000000..69b9f2e0b
--- /dev/null
+++ b/ui/app/routes/vault/cluster/secrets/backend/edit.js
@@ -0,0 +1,3 @@
+import EditBase from './secret-edit';
+
+export default EditBase.extend();
diff --git a/ui/app/routes/vault/cluster/secrets/backend/index.js b/ui/app/routes/vault/cluster/secrets/backend/index.js
new file mode 100644
index 000000000..67782f890
--- /dev/null
+++ b/ui/app/routes/vault/cluster/secrets/backend/index.js
@@ -0,0 +1,7 @@
+import Ember from 'ember';
+
+export default Ember.Route.extend({
+ beforeModel() {
+ return this.replaceWith('vault.cluster.secrets.backend.list-root');
+ },
+});
diff --git a/ui/app/routes/vault/cluster/secrets/backend/list-root.js b/ui/app/routes/vault/cluster/secrets/backend/list-root.js
new file mode 100644
index 000000000..ea1fedd0a
--- /dev/null
+++ b/ui/app/routes/vault/cluster/secrets/backend/list-root.js
@@ -0,0 +1 @@
+export { default } from './list';
diff --git a/ui/app/routes/vault/cluster/secrets/backend/list.js b/ui/app/routes/vault/cluster/secrets/backend/list.js
new file mode 100644
index 000000000..78ee1a3da
--- /dev/null
+++ b/ui/app/routes/vault/cluster/secrets/backend/list.js
@@ -0,0 +1,168 @@
+import Ember from 'ember';
+import { supportedSecretBackends } from 'vault/helpers/supported-secret-backends';
+
+const SUPPORTED_BACKENDS = supportedSecretBackends();
+
+export default Ember.Route.extend({
+ queryParams: {
+ page: {
+ refreshModel: true,
+ },
+ pageFilter: {
+ refreshModel: true,
+ },
+ tab: {
+ refreshModel: true,
+ },
+ },
+
+ templateName: 'vault/cluster/secrets/backend/list',
+
+ beforeModel() {
+ const { backend } = this.paramsFor('vault.cluster.secrets.backend');
+ const backendModel = this.store.peekRecord('secret-engine', backend);
+ const type = backendModel && backendModel.get('type');
+ if (!type || !SUPPORTED_BACKENDS.includes(type)) {
+ return this.transitionTo('vault.cluster.secrets');
+ }
+ },
+
+ capabilities(secret) {
+ const { backend } = this.paramsFor('vault.cluster.secrets.backend');
+ const path = backend + '/' + secret;
+ return this.store.findRecord('capabilities', path);
+ },
+
+ getModelType(backend, tab) {
+ const types = {
+ transit: 'transit-key',
+ ssh: 'role-ssh',
+ aws: 'role-aws',
+ cubbyhole: 'secret-cubbyhole',
+ pki: tab === 'certs' ? 'pki-certificate' : 'role-pki',
+ };
+ const backendModel = this.store.peekRecord('secret-engine', backend);
+ return types[backendModel.get('type')] || 'secret';
+ },
+
+ model(params) {
+ const secret = params.secret ? params.secret : '';
+ const { backend } = this.paramsFor('vault.cluster.secrets.backend');
+ const backends = this.modelFor('vault.cluster.secrets').mapBy('id');
+ return Ember.RSVP.hash({
+ secrets: this.store
+ .lazyPaginatedQuery(this.getModelType(backend, params.tab), {
+ id: secret,
+ backend,
+ responsePath: 'data.keys',
+ page: params.page,
+ pageFilter: params.pageFilter,
+ size: 100,
+ })
+ .then(model => {
+ this.set('has404', false);
+ return model;
+ })
+ .catch(err => {
+ if (backends.includes(backend) && err.httpStatus === 404 && secret === '') {
+ return [];
+ } else {
+ throw err;
+ }
+ }),
+ capabilities: this.capabilities(secret),
+ });
+ },
+
+ afterModel(model) {
+ const { tab } = this.paramsFor(this.routeName);
+ const { backend } = this.paramsFor('vault.cluster.secrets.backend');
+ if (!tab || tab !== 'certs') {
+ return;
+ }
+ return Ember.RSVP
+ .all(
+ // these ids are treated specially by vault's api, but it's also
+ // possible that there is no certificate for them in order to know,
+ // we fetch them specifically on the list page, and then unload the
+ // records if there is no `certificate` attribute on the resultant model
+ ['ca', 'crl', 'ca_chain'].map(id => this.store.queryRecord('pki-certificate', { id, backend }))
+ )
+ .then(
+ results => {
+ results.rejectBy('certificate').forEach(record => record.unloadRecord());
+ return model;
+ },
+ () => {
+ return model;
+ }
+ );
+ },
+
+ setupController(controller, model) {
+ const secretParams = this.paramsFor(this.routeName);
+ const secret = secretParams.secret ? secretParams.secret : '';
+ const { backend } = this.paramsFor('vault.cluster.secrets.backend');
+ const backendModel = this.store.peekRecord('secret-engine', backend);
+ const has404 = this.get('has404');
+ controller.set('hasModel', true);
+ controller.setProperties({
+ model: model.secrets,
+ capabilities: model.capabilities,
+ baseKey: { id: secret },
+ has404,
+ backend,
+ backendModel,
+ backendType: backendModel.get('type'),
+ });
+ if (!has404) {
+ const pageFilter = secretParams.pageFilter;
+ let filter;
+ if (secret) {
+ filter = secret + (pageFilter || '');
+ } else if (pageFilter) {
+ filter = pageFilter;
+ }
+ controller.setProperties({
+ filter: filter || '',
+ page: model.secrets.get('meta.currentPage') || 1,
+ });
+ }
+ },
+
+ resetController(controller, isExiting) {
+ this._super(...arguments);
+ if (isExiting) {
+ controller.set('filter', '');
+ }
+ },
+
+ actions: {
+ error(error, transition) {
+ const { secret } = this.paramsFor(this.routeName);
+ const { backend } = this.paramsFor('vault.cluster.secrets.backend');
+ const backends = this.modelFor('vault.cluster.secrets').mapBy('id');
+
+ Ember.set(error, 'secret', secret);
+ Ember.set(error, 'isRoot', true);
+ Ember.set(error, 'hasBackend', backends.includes(backend));
+ Ember.set(error, 'backend', backend);
+ const hasModel = this.controllerFor(this.routeName).get('hasModel');
+ // only swallow the error if we have a previous model
+ if (hasModel && error.httpStatus === 404) {
+ this.set('has404', true);
+ transition.abort();
+ } else {
+ return true;
+ }
+ },
+
+ willTransition(transition) {
+ window.scrollTo(0, 0);
+ if (transition.targetName !== this.routeName) {
+ this.store.clearAllDatasets();
+ }
+ return true;
+ },
+ },
+});
diff --git a/ui/app/routes/vault/cluster/secrets/backend/secret-edit.js b/ui/app/routes/vault/cluster/secrets/backend/secret-edit.js
new file mode 100644
index 000000000..2583125b2
--- /dev/null
+++ b/ui/app/routes/vault/cluster/secrets/backend/secret-edit.js
@@ -0,0 +1,140 @@
+import Ember from 'ember';
+import utils from 'vault/lib/key-utils';
+import UnloadModelRoute from 'vault/mixins/unload-model-route';
+
+export default Ember.Route.extend(UnloadModelRoute, {
+ capabilities(secret) {
+ const { backend } = this.paramsFor('vault.cluster.secrets.backend');
+ let path;
+ if (backend === 'transit') {
+ path = backend + '/keys/' + secret;
+ } else if (backend === 'ssh' || backend === 'aws') {
+ path = backend + '/roles/' + secret;
+ } else {
+ path = backend + '/' + secret;
+ }
+ return this.store.findRecord('capabilities', path);
+ },
+
+ backendType(path) {
+ return this.store.peekRecord('secret-engine', path).get('type');
+ },
+
+ templateName: 'vault/cluster/secrets/backend/secretEditLayout',
+
+ beforeModel() {
+ // currently there is no recursive delete for folders in vault, so there's no need to 'edit folders'
+ // perhaps in the future we could recurse _for_ users, but for now, just kick them
+ // back to the list
+ const { secret } = this.paramsFor(this.routeName);
+ const parentKey = utils.parentKeyForKey(secret);
+ const mode = this.routeName.split('.').pop();
+ if (mode === 'edit' && utils.keyIsFolder(secret)) {
+ if (parentKey) {
+ return this.transitionTo('vault.cluster.secrets.backend.list', parentKey);
+ } else {
+ return this.transitionTo('vault.cluster.secrets.backend.list-root');
+ }
+ }
+ },
+
+ modelType(backend, secret) {
+ const models = {
+ transit: 'transit-key',
+ ssh: 'role-ssh',
+ aws: 'role-aws',
+ cubbyhole: 'secret-cubbyhole',
+ pki: secret && secret.startsWith('cert/') ? 'pki-certificate' : 'role-pki',
+ };
+ return models[this.backendType(backend)] || 'secret';
+ },
+
+ model(params) {
+ let { secret } = params;
+ const { backend } = this.paramsFor('vault.cluster.secrets.backend');
+ const modelType = this.modelType(backend, secret);
+
+ if (!secret) {
+ secret = '\u0020';
+ }
+ if (modelType === 'pki-certificate') {
+ secret = secret.replace('cert/', '');
+ }
+ return Ember.RSVP.hash({
+ secret: this.store.queryRecord(modelType, { id: secret, backend }),
+ capabilities: this.capabilities(secret),
+ });
+ },
+
+ setupController(controller, model) {
+ this._super(...arguments);
+ const { secret } = this.paramsFor(this.routeName);
+ const { backend } = this.paramsFor('vault.cluster.secrets.backend');
+ const preferAdvancedEdit =
+ this.controllerFor('vault.cluster.secrets.backend').get('preferAdvancedEdit') || false;
+ const backendType = this.backendType(backend);
+ model.secret.setProperties({ backend });
+ controller.setProperties({
+ model: model.secret,
+ capabilities: model.capabilities,
+ baseKey: { id: secret },
+ // mode will be 'show', 'edit', 'create'
+ mode: this.routeName.split('.').pop().replace('-root', ''),
+ backend,
+ preferAdvancedEdit,
+ backendType,
+ });
+ },
+
+ resetController(controller) {
+ if (controller.reset && typeof controller.reset === 'function') {
+ controller.reset();
+ }
+ },
+
+ actions: {
+ error(error) {
+ const { secret } = this.paramsFor(this.routeName);
+ const { backend } = this.paramsFor('vault.cluster.secrets.backend');
+ const backends = this.modelFor('vault.cluster.secrets').mapBy('id');
+ Ember.set(error, 'keyId', backend + '/' + secret);
+ Ember.set(error, 'backend', backend);
+ Ember.set(error, 'hasBackend', backends.includes(backend));
+ return true;
+ },
+
+ refreshModel() {
+ this.refresh();
+ },
+
+ willTransition(transition) {
+ const mode = this.routeName.split('.').pop();
+ if (mode === 'show') {
+ return transition;
+ }
+ if (this.get('hasChanges')) {
+ if (
+ window.confirm(
+ 'You have unsaved changes. Navigating away will discard these changes. Are you sure you want to discard your changes?'
+ )
+ ) {
+ this.unloadModel();
+ this.set('hasChanges', false);
+ return transition;
+ } else {
+ transition.abort();
+ return false;
+ }
+ }
+ },
+
+ hasDataChanges(hasChanges) {
+ this.set('hasChanges', hasChanges);
+ },
+
+ toggleAdvancedEdit(bool) {
+ this.controller.set('preferAdvancedEdit', bool);
+ this.controllerFor('vault.cluster.secrets.backend').set('preferAdvancedEdit', bool);
+ },
+ },
+});
diff --git a/ui/app/routes/vault/cluster/secrets/backend/show-root.js b/ui/app/routes/vault/cluster/secrets/backend/show-root.js
new file mode 100644
index 000000000..98bd0c1a0
--- /dev/null
+++ b/ui/app/routes/vault/cluster/secrets/backend/show-root.js
@@ -0,0 +1 @@
+export { default } from './show';
diff --git a/ui/app/routes/vault/cluster/secrets/backend/show.js b/ui/app/routes/vault/cluster/secrets/backend/show.js
new file mode 100644
index 000000000..69b9f2e0b
--- /dev/null
+++ b/ui/app/routes/vault/cluster/secrets/backend/show.js
@@ -0,0 +1,3 @@
+import EditBase from './secret-edit';
+
+export default EditBase.extend();
diff --git a/ui/app/routes/vault/cluster/secrets/backend/sign-root.js b/ui/app/routes/vault/cluster/secrets/backend/sign-root.js
new file mode 100644
index 000000000..d034c1595
--- /dev/null
+++ b/ui/app/routes/vault/cluster/secrets/backend/sign-root.js
@@ -0,0 +1 @@
+export { default } from './sign';
diff --git a/ui/app/routes/vault/cluster/secrets/backend/sign.js b/ui/app/routes/vault/cluster/secrets/backend/sign.js
new file mode 100644
index 000000000..b4329d527
--- /dev/null
+++ b/ui/app/routes/vault/cluster/secrets/backend/sign.js
@@ -0,0 +1,45 @@
+import Ember from 'ember';
+import UnloadModel from 'vault/mixins/unload-model-route';
+
+export default Ember.Route.extend(UnloadModel, {
+ templateName: 'vault/cluster/secrets/backend/sign',
+
+ backendModel() {
+ const backend = this.paramsFor('vault.cluster.secrets.backend').backend;
+ return this.store.peekRecord('secret-engine', backend);
+ },
+
+ pathQuery(role, backend) {
+ return {
+ id: `${backend}/sign/${role}`,
+ };
+ },
+
+ model(params) {
+ const role = params.secret;
+ const backendModel = this.backendModel();
+ const backend = backendModel.get('id');
+
+ if (backendModel.get('type') !== 'ssh') {
+ return this.transitionTo('vault.cluster.secrets.backend.list-root', backend);
+ }
+ return this.store.queryRecord('capabilities', this.pathQuery(role, backend)).then(capabilities => {
+ if (!capabilities.get('canUpdate')) {
+ return this.transitionTo('vault.cluster.secrets.backend.list-root', backend);
+ }
+ return this.store.createRecord('ssh-sign', {
+ role: {
+ backend,
+ id: role,
+ name: role,
+ },
+ id: `${backend}-${role}`,
+ });
+ });
+ },
+
+ setupController(controller) {
+ this._super(...arguments);
+ controller.set('backend', this.backendModel());
+ },
+});
diff --git a/ui/app/routes/vault/cluster/settings.js b/ui/app/routes/vault/cluster/settings.js
new file mode 100644
index 000000000..212fac094
--- /dev/null
+++ b/ui/app/routes/vault/cluster/settings.js
@@ -0,0 +1,8 @@
+import Ember from 'ember';
+import ClusterRoute from 'vault/mixins/cluster-route';
+
+export default Ember.Route.extend(ClusterRoute, {
+ model() {
+ return {};
+ },
+});
diff --git a/ui/app/routes/vault/cluster/settings/auth/configure.js b/ui/app/routes/vault/cluster/settings/auth/configure.js
new file mode 100644
index 000000000..c644c98e3
--- /dev/null
+++ b/ui/app/routes/vault/cluster/settings/auth/configure.js
@@ -0,0 +1,22 @@
+import Ember from 'ember';
+import DS from 'ember-data';
+
+import { methods } from 'vault/helpers/mountable-auth-methods';
+
+const METHODS = methods();
+
+export default Ember.Route.extend({
+ model() {
+ const { method } = this.paramsFor(this.routeName);
+ return this.store.findAll('auth-method').then(() => {
+ const model = this.store.peekRecord('auth-method', method);
+ const modelType = model && model.get('type');
+ if (!model || (modelType !== 'token' && !METHODS.findBy('type', modelType))) {
+ const error = new DS.AdapterError();
+ Ember.set(error, 'httpStatus', 404);
+ throw error;
+ }
+ return model;
+ });
+ },
+});
diff --git a/ui/app/routes/vault/cluster/settings/auth/configure/index.js b/ui/app/routes/vault/cluster/settings/auth/configure/index.js
new file mode 100644
index 000000000..438ffeb14
--- /dev/null
+++ b/ui/app/routes/vault/cluster/settings/auth/configure/index.js
@@ -0,0 +1,12 @@
+import Ember from 'ember';
+import { tabsForAuthSection } from 'vault/helpers/tabs-for-auth-section';
+
+const { get } = Ember;
+
+export default Ember.Route.extend({
+ beforeModel() {
+ const type = this.modelFor('vault.cluster.settings.auth.configure').get('type');
+ const section = get(tabsForAuthSection([type]), 'firstObject.routeParams.lastObject');
+ return this.transitionTo('vault.cluster.settings.auth.configure.section', section);
+ },
+});
diff --git a/ui/app/routes/vault/cluster/settings/auth/configure/section.js b/ui/app/routes/vault/cluster/settings/auth/configure/section.js
new file mode 100644
index 000000000..cfcdfd2ef
--- /dev/null
+++ b/ui/app/routes/vault/cluster/settings/auth/configure/section.js
@@ -0,0 +1,80 @@
+import Ember from 'ember';
+import DS from 'ember-data';
+import UnloadModelRoute from 'vault/mixins/unload-model-route';
+
+const { RSVP } = Ember;
+export default Ember.Route.extend(UnloadModelRoute, {
+ modelPath: 'model.model',
+ modelType(backendType, section) {
+ const MODELS = {
+ 'aws-client': 'auth-config/aws/client',
+ 'aws-identity-whitelist': 'auth-config/aws/identity-whitelist',
+ 'aws-roletag-blacklist': 'auth-config/aws/roletag-blacklist',
+ 'github-configuration': 'auth-config/github',
+ 'gcp-configuration': 'auth-config/gcp',
+ 'kubernetes-configuration': 'auth-config/kubernetes',
+ 'ldap-configuration': 'auth-config/ldap',
+ 'okta-configuration': 'auth-config/okta',
+ 'radius-configuration': 'auth-config/radius',
+ };
+ return MODELS[`${backendType}-${section}`];
+ },
+
+ model(params) {
+ const backend = this.modelFor('vault.cluster.settings.auth.configure');
+ const { section_name: section } = params;
+ if (section === 'options') {
+ return RSVP.hash({
+ model: backend,
+ section,
+ });
+ }
+ const modelType = this.modelType(backend.get('type'), section);
+ if (!modelType) {
+ const error = new DS.AdapterError();
+ Ember.set(error, 'httpStatus', 404);
+ throw error;
+ }
+ const model = this.store.peekRecord(modelType, backend.id);
+ if (model) {
+ return RSVP.hash({
+ model,
+ section,
+ });
+ }
+ return this.store
+ .findRecord(modelType, backend.id)
+ .then(config => {
+ config.set('backend', backend);
+ return RSVP.hash({
+ model: config,
+ section,
+ });
+ })
+ .catch(e => {
+ let config;
+ // if you haven't saved a config, the API 404s, so create one here to edit and return it
+ if (e.httpStatus === 404) {
+ config = this.store.createRecord(modelType, {
+ id: backend.id,
+ });
+ config.set('backend', backend);
+
+ return RSVP.hash({
+ model: config,
+ section,
+ });
+ }
+ throw e;
+ });
+ },
+
+ actions: {
+ willTransition() {
+ if (this.currentModel.model.constructor.modelName !== 'auth-method') {
+ this.unloadModel();
+ return true;
+ }
+ },
+ },
+});
diff --git a/ui/app/routes/vault/cluster/settings/auth/index.js b/ui/app/routes/vault/cluster/settings/auth/index.js
new file mode 100644
index 000000000..0e8ce4270
--- /dev/null
+++ b/ui/app/routes/vault/cluster/settings/auth/index.js
@@ -0,0 +1,7 @@
+import Ember from 'ember';
+
+export default Ember.Route.extend({
+ beforeModel() {
+ return this.replaceWith('vault.cluster.settings.auth.enable');
+ },
+});
diff --git a/ui/app/routes/vault/cluster/settings/configure-secret-backend.js b/ui/app/routes/vault/cluster/settings/configure-secret-backend.js
new file mode 100644
index 000000000..3c6217dff
--- /dev/null
+++ b/ui/app/routes/vault/cluster/settings/configure-secret-backend.js
@@ -0,0 +1,65 @@
+import Ember from 'ember';
+import DS from 'ember-data';
+
+const CONFIGURABLE_BACKEND_TYPES = ['aws', 'ssh', 'pki'];
+
+export default Ember.Route.extend({
+ model() {
+ const { backend } = this.paramsFor(this.routeName);
+ return this.store.query('secret-engine', {}).then(() => {
+ const model = this.store.peekRecord('secret-engine', backend);
+ if (!model || !CONFIGURABLE_BACKEND_TYPES.includes(model.get('type'))) {
+ const error = new DS.AdapterError();
+ Ember.set(error, 'httpStatus', 404);
+ throw error;
+ }
+ return this.store.findRecord('secret-engine', backend).then(
+ () => {
+ return model;
+ },
+ () => {
+ return model;
+ }
+ );
+ });
+ },
+
+ afterModel(model, transition) {
+ const type = model.get('type');
+ if (type === 'pki') {
+ if (transition.targetName === this.routeName) {
+ return this.transitionTo(`${this.routeName}.section`, 'cert');
+ } else {
+ return;
+ }
+ }
+ if (type === 'aws') {
+ return this.store
+ .queryRecord('secret-engine', {
+ backend: model.id,
+ type,
+ })
+ .then(() => model, () => model);
+ }
+ return model;
+ },
+
+ setupController(controller, model) {
+ if (model.get('publicKey')) {
+ controller.set('configured', true);
+ }
+ return this._super(...arguments);
+ },
+
+ resetController(controller, isExiting) {
+ if (isExiting) {
+ controller.reset();
+ }
+ },
+
+ actions: {
+ refreshRoute() {
+ this.refresh();
+ },
+ },
+});
diff --git a/ui/app/routes/vault/cluster/settings/configure-secret-backend/index.js b/ui/app/routes/vault/cluster/settings/configure-secret-backend/index.js
new file mode 100644
index 000000000..245a2d4fe
--- /dev/null
+++ b/ui/app/routes/vault/cluster/settings/configure-secret-backend/index.js
@@ -0,0 +1,10 @@
+import Ember from 'ember';
+
+export default Ember.Route.extend({
+ beforeModel(transition) {
+ const type = this.modelFor('vault.cluster.settings.configure-secret-backend').get('type');
+ if (type === 'pki' && transition.targetName === this.routeName) {
+ return this.transitionTo('vault.cluster.settings.configure-secret-backend.section', 'cert');
+ }
+ },
+});
diff --git a/ui/app/routes/vault/cluster/settings/configure-secret-backend/section.js b/ui/app/routes/vault/cluster/settings/configure-secret-backend/section.js
new file mode 100644
index 000000000..4e0b30e79
--- /dev/null
+++ b/ui/app/routes/vault/cluster/settings/configure-secret-backend/section.js
@@ -0,0 +1,41 @@
+import Ember from 'ember';
+import DS from 'ember-data';
+
+const SECTIONS_FOR_TYPE = {
+ pki: ['cert', 'urls', 'crl', 'tidy'],
+};
+export default Ember.Route.extend({
+ fetchModel() {
+ const { section_name: sectionName } = this.paramsFor(this.routeName);
+ const backendModel = this.modelFor('vault.cluster.settings.configure-secret-backend');
+ const modelType = `${backendModel.get('type')}-config`;
+ return this.store
+ .queryRecord(modelType, {
+ backend: backendModel.id,
+ section: sectionName,
+ })
+ .then(model => {
+ model.set('backendType', backendModel.get('type'));
+ model.set('section', sectionName);
+ return model;
+ });
+ },
+
+ model(params) {
+ const { section_name: sectionName } = params;
+ const backendModel = this.modelFor('vault.cluster.settings.configure-secret-backend');
+ const sections = SECTIONS_FOR_TYPE[backendModel.get('type')];
+ const hasSection = sections.includes(sectionName);
+ if (!backendModel || !hasSection) {
+ const error = new DS.AdapterError();
+ Ember.set(error, 'httpStatus', 404);
+ throw error;
+ }
+ return this.fetchModel();
+ },
+
+ setupController(controller) {
+ this._super(...arguments);
+ controller.set('onRefresh', () => this.fetchModel());
+ },
+});
diff --git a/ui/app/routes/vault/cluster/settings/index.js b/ui/app/routes/vault/cluster/settings/index.js
new file mode 100644
index 000000000..779430929
--- /dev/null
+++ b/ui/app/routes/vault/cluster/settings/index.js
@@ -0,0 +1,10 @@
+import Ember from 'ember';
+
+export default Ember.Route.extend({
+ beforeModel: function(transition) {
+ if (transition.targetName === this.routeName) {
+ transition.abort();
+ this.replaceWith('vault.cluster.settings.mount-secret-backend');
+ }
+ },
+});
diff --git a/ui/app/routes/vault/cluster/settings/mount-secret-backend.js b/ui/app/routes/vault/cluster/settings/mount-secret-backend.js
new file mode 100644
index 000000000..42208967e
--- /dev/null
+++ b/ui/app/routes/vault/cluster/settings/mount-secret-backend.js
@@ -0,0 +1,14 @@
+import Ember from 'ember';
+import UnloadModelRoute from 'vault/mixins/unload-model-route';
+import UnsavedModelRoute from 'vault/mixins/unsaved-model-route';
+
+export default Ember.Route.extend(UnloadModelRoute, UnsavedModelRoute, {
+ // intentionally blank - we don't want a model until one is
+ // created via the form in the controller
+ model() {
+ return {};
+ },
+ activate() {
+ this.store.unloadAll('secret-engine');
+ },
+});
diff --git a/ui/app/routes/vault/cluster/settings/seal.js b/ui/app/routes/vault/cluster/settings/seal.js
new file mode 100644
index 000000000..d64a10305
--- /dev/null
+++ b/ui/app/routes/vault/cluster/settings/seal.js
@@ -0,0 +1,10 @@
+import Ember from 'ember';
+
+export default Ember.Route.extend({
+ model() {
+ return Ember.RSVP.hash({
+ cluster: this.modelFor('vault.cluster'),
+ seal: this.store.findRecord('capabilities', 'sys/seal'),
+ });
+ },
+});
diff --git a/ui/app/routes/vault/cluster/tools.js b/ui/app/routes/vault/cluster/tools.js
new file mode 100644
index 000000000..18552ac11
--- /dev/null
+++ b/ui/app/routes/vault/cluster/tools.js
@@ -0,0 +1,8 @@
+import Ember from 'ember';
+import ClusterRoute from 'vault/mixins/cluster-route';
+
+export default Ember.Route.extend(ClusterRoute, {
+ model() {
+ return this.modelFor('vault.cluster');
+ },
+});
diff --git a/ui/app/routes/vault/cluster/tools/index.js b/ui/app/routes/vault/cluster/tools/index.js
new file mode 100644
index 000000000..8ecc01051
--- /dev/null
+++ b/ui/app/routes/vault/cluster/tools/index.js
@@ -0,0 +1,14 @@
+import Ember from 'ember';
+import { toolsActions } from 'vault/helpers/tools-actions';
+
+export default Ember.Route.extend({
+ currentCluster: Ember.inject.service(),
+ beforeModel(transition) {
+ const currentCluster = this.get('currentCluster.cluster.name');
+ const supportedActions = toolsActions();
+ if (transition.targetName === this.routeName) {
+ transition.abort();
+ return this.replaceWith('vault.cluster.tools.tool', currentCluster, supportedActions[0]);
+ }
+ },
+});
diff --git a/ui/app/routes/vault/cluster/tools/tool.js b/ui/app/routes/vault/cluster/tools/tool.js
new file mode 100644
index 000000000..b031d0d0c
--- /dev/null
+++ b/ui/app/routes/vault/cluster/tools/tool.js
@@ -0,0 +1,20 @@
+import Ember from 'ember';
+import { toolsActions } from 'vault/helpers/tools-actions';
+
+export default Ember.Route.extend({
+ beforeModel(transition) {
+ const supportedActions = toolsActions();
+ const { selectedAction } = this.paramsFor(this.routeName);
+ if (!selectedAction || !supportedActions.includes(selectedAction)) {
+ transition.abort();
+ return this.transitionTo(this.routeName, supportedActions[0]);
+ }
+ },
+ model() {},
+ actions: {
+ didTransition() {
+ const params = this.paramsFor(this.routeName);
+ this.controller.setProperties(params);
+ },
+ },
+});
diff --git a/ui/app/routes/vault/cluster/unseal.js b/ui/app/routes/vault/cluster/unseal.js
new file mode 100644
index 000000000..875583f71
--- /dev/null
+++ b/ui/app/routes/vault/cluster/unseal.js
@@ -0,0 +1 @@
+export { default } from './cluster-route-base';
diff --git a/ui/app/serializers/application.js b/ui/app/serializers/application.js
new file mode 100644
index 000000000..64826849f
--- /dev/null
+++ b/ui/app/serializers/application.js
@@ -0,0 +1,62 @@
+import DS from 'ember-data';
+import Ember from 'ember';
+const { decamelize } = Ember.String;
+
+export default DS.JSONSerializer.extend({
+ keyForAttribute: function(attr) {
+ return decamelize(attr);
+ },
+
+ normalizeItems(payload) {
+ if (payload.data && payload.data.keys && Array.isArray(payload.data.keys)) {
+ let models = payload.data.keys.map(key => {
+ let pk = this.get('primaryKey') || 'id';
+ return { [pk]: key };
+ });
+ return models;
+ }
+ Ember.assign(payload, payload.data);
+ delete payload.data;
+ return payload;
+ },
+
+ pushPayload(store, payload) {
+ const transformedPayload = this.normalizeResponse(
+ store,
+ store.modelFor(payload.modelName),
+ payload,
+ payload.id,
+ 'findRecord'
+ );
+ return store.push(transformedPayload);
+ },
+
+ normalizeResponse(store, primaryModelClass, payload, id, requestType) {
+ const responseJSON = this.normalizeItems(payload);
+ if (id && !responseJSON.id) {
+ responseJSON.id = id;
+ }
+ return this._super(store, primaryModelClass, responseJSON, id, requestType);
+ },
+
+ serializeAttribute(snapshot, json, key, attributes) {
+ const val = snapshot.attr(key);
+ const valHasNotChanged = Ember.isNone(snapshot.changedAttributes()[key]);
+ const valIsBlank = Ember.isBlank(val);
+ if (attributes.options.readOnly) {
+ return;
+ }
+ if (attributes.type === 'object' && val && Object.keys(val).length > 0 && valHasNotChanged) {
+ return;
+ }
+ if (valIsBlank && valHasNotChanged) {
+ return;
+ }
+
+ this._super(snapshot, json, key, attributes);
+ },
+
+ serializeBelongsTo(snapshot, json) {
+ return json;
+ },
+});
diff --git a/ui/app/serializers/auth-method.js b/ui/app/serializers/auth-method.js
new file mode 100644
index 000000000..77b2a7fb6
--- /dev/null
+++ b/ui/app/serializers/auth-method.js
@@ -0,0 +1,24 @@
+import ApplicationSerializer from './application';
+
+export default ApplicationSerializer.extend({
+ normalizeBackend(path, backend) {
+ let struct = {};
+ for (let attribute in backend) {
+ struct[attribute] = backend[attribute];
+ }
+ // strip the trailing slash off of the path so we
+ // can navigate to it without getting `//` in the url
+ struct.id = path.slice(0, -1);
+ struct.path = path;
+ return struct;
+ },
+
+ normalizeResponse(store, primaryModelClass, payload, id, requestType) {
+ const isCreate = requestType === 'createRecord';
+ const backends = isCreate
+ ? payload.data
+ : Object.keys(payload.data).map(id => this.normalizeBackend(id, payload[id]));
+
+ return this._super(store, primaryModelClass, backends, id, requestType);
+ },
+});
diff --git a/ui/app/serializers/capabilities.js b/ui/app/serializers/capabilities.js
new file mode 100644
index 000000000..c9212ea93
--- /dev/null
+++ b/ui/app/serializers/capabilities.js
@@ -0,0 +1,20 @@
+import DS from 'ember-data';
+
+export default DS.RESTSerializer.extend({
+ primaryKey: 'path',
+
+ normalizeResponse(store, primaryModelClass, payload, id, requestType) {
+ // queryRecord will already have set this, and we won't have an id here
+ if (id) {
+ payload.path = id;
+ }
+ const response = {
+ [primaryModelClass.modelName]: payload,
+ };
+ return this._super(store, primaryModelClass, response, id, requestType);
+ },
+
+ modelNameFromPayloadKey() {
+ return 'capabilities';
+ },
+});
diff --git a/ui/app/serializers/cluster.js b/ui/app/serializers/cluster.js
new file mode 100644
index 000000000..5c552381a
--- /dev/null
+++ b/ui/app/serializers/cluster.js
@@ -0,0 +1,34 @@
+import Ember from 'ember';
+import DS from 'ember-data';
+
+const { decamelize } = Ember.String;
+
+export default DS.RESTSerializer.extend(DS.EmbeddedRecordsMixin, {
+ keyForAttribute: function(attr) {
+ return decamelize(attr);
+ },
+
+ attrs: {
+ nodes: { embedded: 'always' },
+ },
+
+ pushPayload(store, payload) {
+ const transformedPayload = this.normalizeResponse(
+ store,
+ store.modelFor('cluster'),
+ payload,
+ null,
+ 'findAll'
+ );
+ return store.push(transformedPayload);
+ },
+
+ normalizeResponse(store, primaryModelClass, payload, id, requestType) {
+ // FIXME when multiple clusters lands
+ const transformedPayload = {
+ clusters: Ember.assign({ id: '1' }, payload.data || payload),
+ };
+
+ return this._super(store, primaryModelClass, transformedPayload, id, requestType);
+ },
+});
diff --git a/ui/app/serializers/config.js b/ui/app/serializers/config.js
new file mode 100644
index 000000000..80314d93c
--- /dev/null
+++ b/ui/app/serializers/config.js
@@ -0,0 +1,29 @@
+import DS from 'ember-data';
+import Ember from 'ember';
+const { decamelize } = Ember.String;
+
+export default DS.RESTSerializer.extend({
+ keyForAttribute: function(attr) {
+ return decamelize(attr);
+ },
+
+ normalizeAll(payload) {
+ if (payload.data) {
+ const data = Ember.assign({}, payload, payload.data);
+ return [data];
+ }
+ return [payload];
+ },
+
+ normalizeResponse(store, primaryModelClass, payload, id, requestType) {
+ const records = this.normalizeAll(payload);
+ const { modelName } = primaryModelClass;
+ let transformedPayload = { [modelName]: records };
+ // just return the single object because ember is picky
+ if (requestType === 'queryRecord') {
+ transformedPayload = { [modelName]: records[0] };
+ }
+
+ return this._super(store, primaryModelClass, transformedPayload, id, requestType);
+ },
+});
diff --git a/ui/app/serializers/iam-credential.js b/ui/app/serializers/iam-credential.js
new file mode 100644
index 000000000..3b0943f0e
--- /dev/null
+++ b/ui/app/serializers/iam-credential.js
@@ -0,0 +1,34 @@
+import DS from 'ember-data';
+import Ember from 'ember';
+const { decamelize } = Ember.String;
+
+export default DS.RESTSerializer.extend({
+ keyForAttribute: function(attr) {
+ return decamelize(attr);
+ },
+
+ pushPayload(store, payload) {
+ const transformedPayload = this.normalizeResponse(
+ store,
+ store.modelFor(payload.modelName),
+ payload,
+ payload.id,
+ 'findRecord'
+ );
+ return store.push(transformedPayload);
+ },
+
+ normalizeItems(payload) {
+ Ember.assign(payload, payload.data);
+ delete payload.data;
+ return payload;
+ },
+
+ normalizeResponse(store, primaryModelClass, payload, id, requestType) {
+ const responseJSON = this.normalizeItems(payload);
+ const { modelName } = primaryModelClass;
+ let transformedPayload = { [modelName]: responseJSON };
+ let ret = this._super(store, primaryModelClass, transformedPayload, id, requestType);
+ return ret;
+ },
+});
diff --git a/ui/app/serializers/identity/entity.js b/ui/app/serializers/identity/entity.js
new file mode 100644
index 000000000..03aee2693
--- /dev/null
+++ b/ui/app/serializers/identity/entity.js
@@ -0,0 +1,8 @@
+import DS from 'ember-data';
+import ApplicationSerializer from '../application';
+
+export default ApplicationSerializer.extend(DS.EmbeddedRecordsMixin, {
+ attrs: {
+ aliases: { embedded: 'always' },
+ },
+});
diff --git a/ui/app/serializers/identity/group.js b/ui/app/serializers/identity/group.js
new file mode 100644
index 000000000..c2df2fdf8
--- /dev/null
+++ b/ui/app/serializers/identity/group.js
@@ -0,0 +1,25 @@
+import DS from 'ember-data';
+import ApplicationSerializer from '../application';
+
+export default ApplicationSerializer.extend(DS.EmbeddedRecordsMixin, {
+ attrs: {
+ alias: { embedded: 'always' },
+ },
+
+ normalizeFindRecordResponse(store, primaryModelClass, payload) {
+ if (payload.alias && Object.keys(payload.alias).length === 0) {
+ delete payload.alias;
+ }
+ return this._super(...arguments);
+ },
+
+ serialize() {
+ let json = this._super(...arguments);
+ delete json.alias;
+ if (json.type === 'external') {
+ delete json.member_entity_ids;
+ delete json.member_group_ids;
+ }
+ return json;
+ },
+});
diff --git a/ui/app/serializers/lease.js b/ui/app/serializers/lease.js
new file mode 100644
index 000000000..41765bc20
--- /dev/null
+++ b/ui/app/serializers/lease.js
@@ -0,0 +1,32 @@
+import DS from 'ember-data';
+import Ember from 'ember';
+const { decamelize } = Ember.String;
+
+export default DS.RESTSerializer.extend({
+ keyForAttribute: function(attr) {
+ return decamelize(attr);
+ },
+
+ normalizeAll(payload) {
+ if (payload.data.keys && Array.isArray(payload.data.keys)) {
+ const records = payload.data.keys.map(record => {
+ const fullPath = payload.prefix ? payload.prefix + record : record;
+ return { id: fullPath };
+ });
+ return records;
+ }
+ return [payload.data];
+ },
+
+ normalizeResponse(store, primaryModelClass, payload, id, requestType) {
+ const records = this.normalizeAll(payload);
+ const { modelName } = primaryModelClass;
+ let transformedPayload = { [modelName]: records };
+ // just return the single object because ember is picky
+ if (requestType === 'queryRecord') {
+ transformedPayload = { [modelName]: records[0] };
+ }
+
+ return this._super(store, primaryModelClass, transformedPayload, id, requestType);
+ },
+});
diff --git a/ui/app/serializers/mount-config.js b/ui/app/serializers/mount-config.js
new file mode 100644
index 000000000..16289da25
--- /dev/null
+++ b/ui/app/serializers/mount-config.js
@@ -0,0 +1,2 @@
+import ApplicationSerializer from './application';
+export default ApplicationSerializer.extend();
diff --git a/ui/app/serializers/mount-filter-config.js b/ui/app/serializers/mount-filter-config.js
new file mode 100644
index 000000000..1effd5741
--- /dev/null
+++ b/ui/app/serializers/mount-filter-config.js
@@ -0,0 +1,16 @@
+import DS from 'ember-data';
+import Ember from 'ember';
+const { decamelize } = Ember.String;
+
+export default DS.RESTSerializer.extend({
+ keyForAttribute: function(attr) {
+ return decamelize(attr);
+ },
+
+ normalizeResponse(store, primaryModelClass, payload, id, requestType) {
+ const { modelName } = primaryModelClass;
+ payload.data.id = id;
+ const transformedPayload = { [modelName]: payload.data };
+ return this._super(store, primaryModelClass, transformedPayload, id, requestType);
+ },
+});
diff --git a/ui/app/serializers/node.js b/ui/app/serializers/node.js
new file mode 100644
index 000000000..582031350
--- /dev/null
+++ b/ui/app/serializers/node.js
@@ -0,0 +1,47 @@
+import Ember from 'ember';
+import DS from 'ember-data';
+
+const { decamelize } = Ember.String;
+
+export default DS.RESTSerializer.extend(DS.EmbeddedRecordsMixin, {
+ keyForAttribute: function(attr) {
+ return decamelize(attr);
+ },
+
+ pushPayload(store, payload) {
+ const transformedPayload = this.normalizeResponse(
+ store,
+ store.modelFor('node'),
+ payload,
+ null,
+ 'findAll'
+ );
+ return store.push(transformedPayload);
+ },
+
+ nodeFromObject(name, payload) {
+ const nodeObj = payload.nodes[name];
+ return Ember.assign(nodeObj, {
+ name,
+ id: name,
+ });
+ },
+
+ normalizeResponse(store, primaryModelClass, payload, id, requestType) {
+ // payload looks like:
+ // { "nodes": { "name": { "sealed": "true" }}}
+
+ const nodes = payload.nodes
+ ? Object.keys(payload.nodes).map(name => this.nodeFromObject(name, payload))
+ : [Ember.assign(payload, { id: '1' })];
+
+ const transformedPayload = { nodes: nodes };
+
+ return this._super(store, primaryModelClass, transformedPayload, id, requestType);
+ },
+
+ normalize(model, hash, prop) {
+ hash.id = '1';
+ return this._super(model, hash, prop);
+ },
+});
diff --git a/ui/app/serializers/pki-certificate.js b/ui/app/serializers/pki-certificate.js
new file mode 100644
index 000000000..4588eb238
--- /dev/null
+++ b/ui/app/serializers/pki-certificate.js
@@ -0,0 +1,63 @@
+import DS from 'ember-data';
+import Ember from 'ember';
+const { decamelize } = Ember.String;
+
+export default DS.RESTSerializer.extend({
+ keyForAttribute: function(attr) {
+ return decamelize(attr);
+ },
+
+ pushPayload(store, payload) {
+ const transformedPayload = this.normalizeResponse(
+ store,
+ store.modelFor(payload.modelName),
+ payload,
+ payload.id,
+ 'findRecord'
+ );
+ return store.push(transformedPayload);
+ },
+
+ normalizeItems(payload) {
+ if (payload.data && payload.data.keys && Array.isArray(payload.data.keys)) {
+ let ret = payload.data.keys.map(key => {
+ let model = {
+ id_for_nav: `cert/${key}`,
+ id: key,
+ };
+ if (payload.backend) {
+ model.backend = payload.backend;
+ }
+ return model;
+ });
+ return ret;
+ }
+ Ember.assign(payload, payload.data);
+ delete payload.data;
+ return payload;
+ },
+
+ normalizeResponse(store, primaryModelClass, payload, id, requestType) {
+ const responseJSON = this.normalizeItems(payload);
+ const { modelName } = primaryModelClass;
+ let transformedPayload = { [modelName]: responseJSON };
+ return this._super(store, primaryModelClass, transformedPayload, id, requestType);
+ },
+
+ serializeAttribute(snapshot, json, key, attributes) {
+ const val = snapshot.attr(key);
+ const valHasNotChanged = Ember.isNone(snapshot.changedAttributes()[key]);
+ const valIsBlank = Ember.isBlank(val);
+ if (attributes.options.readOnly) {
+ return;
+ }
+ if (attributes.type === 'object' && val && Object.keys(val).length > 0 && valHasNotChanged) {
+ return;
+ }
+ if (valIsBlank && valHasNotChanged) {
+ return;
+ }
+
+ this._super(snapshot, json, key, attributes);
+ },
+});
diff --git a/ui/app/serializers/policy.js b/ui/app/serializers/policy.js
new file mode 100644
index 000000000..9646bbb94
--- /dev/null
+++ b/ui/app/serializers/policy.js
@@ -0,0 +1,18 @@
+import ApplicationSerializer from './application';
+
+export default ApplicationSerializer.extend({
+ primaryKey: 'name',
+
+ normalizePolicies(payload) {
+ const data = payload.data.keys ? payload.data.keys.map(name => ({ name })) : payload.data;
+ return data;
+ },
+
+ normalizeResponse(store, primaryModelClass, payload, id, requestType) {
+ const nullResponses = ['deleteRecord'];
+ let normalizedPayload = nullResponses.includes(requestType)
+ ? { name: id }
+ : this.normalizePolicies(payload);
+ return this._super(store, primaryModelClass, normalizedPayload, id, requestType);
+ },
+});
diff --git a/ui/app/serializers/policy/acl.js b/ui/app/serializers/policy/acl.js
new file mode 100644
index 000000000..ea1e2c990
--- /dev/null
+++ b/ui/app/serializers/policy/acl.js
@@ -0,0 +1,3 @@
+import PolicySerializer from '../policy';
+
+export default PolicySerializer.extend();
diff --git a/ui/app/serializers/policy/egp.js b/ui/app/serializers/policy/egp.js
new file mode 100644
index 000000000..ea1e2c990
--- /dev/null
+++ b/ui/app/serializers/policy/egp.js
@@ -0,0 +1,3 @@
+import PolicySerializer from '../policy';
+
+export default PolicySerializer.extend();
diff --git a/ui/app/serializers/policy/rgp.js b/ui/app/serializers/policy/rgp.js
new file mode 100644
index 000000000..ea1e2c990
--- /dev/null
+++ b/ui/app/serializers/policy/rgp.js
@@ -0,0 +1,3 @@
+import PolicySerializer from '../policy';
+
+export default PolicySerializer.extend();
diff --git a/ui/app/serializers/replication-attributes.js b/ui/app/serializers/replication-attributes.js
new file mode 100644
index 000000000..558e1a95e
--- /dev/null
+++ b/ui/app/serializers/replication-attributes.js
@@ -0,0 +1,10 @@
+import Ember from 'ember';
+import DS from 'ember-data';
+
+const { decamelize } = Ember.String;
+
+export default DS.RESTSerializer.extend({
+ keyForAttribute: function(attr) {
+ return decamelize(attr);
+ },
+});
diff --git a/ui/app/serializers/role-aws.js b/ui/app/serializers/role-aws.js
new file mode 100644
index 000000000..41878b351
--- /dev/null
+++ b/ui/app/serializers/role-aws.js
@@ -0,0 +1,71 @@
+import DS from 'ember-data';
+import Ember from 'ember';
+const { decamelize } = Ember.String;
+
+export default DS.RESTSerializer.extend({
+ keyForAttribute: function(attr) {
+ return decamelize(attr);
+ },
+
+ extractLazyPaginatedData(payload) {
+ let ret;
+ ret = payload.data.keys.map(key => {
+ let model = {
+ id: key,
+ };
+ if (payload.backend) {
+ model.backend = payload.backend;
+ }
+ return model;
+ });
+ return ret;
+ },
+
+ normalizeItems(payload) {
+ if (payload.data.keys && Array.isArray(payload.data.keys)) {
+ return payload.data.keys;
+ }
+ Ember.assign(payload, payload.data);
+ delete payload.data;
+ return [payload];
+ },
+ modelNameFromPayloadKey(payloadType) {
+ return payloadType;
+ },
+
+ normalizeResponse(store, primaryModelClass, payload, id, requestType) {
+ const nullResponses = ['updateRecord', 'createRecord', 'deleteRecord'];
+ const responseJSON = nullResponses.includes(requestType) ? { id } : this.normalizeItems(payload);
+ const { modelName } = primaryModelClass;
+ let transformedPayload = { [modelName]: responseJSON };
+ // just return the single object because ember is picky
+ if (requestType === 'queryRecord') {
+ transformedPayload = { [modelName]: responseJSON[0] };
+ }
+
+ return this._super(store, primaryModelClass, transformedPayload, id, requestType);
+ },
+
+ serializeAttribute(snapshot, json, key, attributes) {
+ const val = snapshot.attr(key);
+ if (attributes.options.readOnly) {
+ return;
+ }
+ if (
+ attributes.type === 'object' &&
+ val &&
+ Object.keys(val).length > 0 &&
+ Ember.isNone(snapshot.changedAttributes()[key])
+ ) {
+ return;
+ }
+ if (Ember.isBlank(val) && Ember.isNone(snapshot.changedAttributes()[key])) {
+ return;
+ }
+
+ this._super(snapshot, json, key, attributes);
+ },
+ serialize() {
+ return this._super(...arguments);
+ },
+});
diff --git a/ui/app/serializers/role-pki.js b/ui/app/serializers/role-pki.js
new file mode 100644
index 000000000..af9f7c23d
--- /dev/null
+++ b/ui/app/serializers/role-pki.js
@@ -0,0 +1,66 @@
+import DS from 'ember-data';
+import Ember from 'ember';
+const { decamelize } = Ember.String;
+
+export default DS.RESTSerializer.extend({
+ keyForAttribute: function(attr) {
+ return decamelize(attr);
+ },
+
+ normalizeItems(payload) {
+ if (payload.data.keys && Array.isArray(payload.data.keys)) {
+ let ret = payload.data.keys.map(key => {
+ let model = {
+ id: key,
+ };
+ if (payload.backend) {
+ model.backend = payload.backend;
+ }
+ return model;
+ });
+ return ret;
+ }
+ Ember.assign(payload, payload.data);
+ delete payload.data;
+ return [payload];
+ },
+ modelNameFromPayloadKey(payloadType) {
+ return payloadType;
+ },
+
+ normalizeResponse(store, primaryModelClass, payload, id, requestType) {
+ const nullResponses = ['updateRecord', 'createRecord', 'deleteRecord'];
+ const responseJSON = nullResponses.includes(requestType) ? { id } : this.normalizeItems(payload);
+ const { modelName } = primaryModelClass;
+ let transformedPayload = { [modelName]: responseJSON };
+ // just return the single object because ember is picky
+ if (requestType === 'queryRecord') {
+ transformedPayload = { [modelName]: responseJSON[0] };
+ }
+
+ return this._super(store, primaryModelClass, transformedPayload, id, requestType);
+ },
+
+ serializeAttribute(snapshot, json, key, attributes) {
+ const val = snapshot.attr(key);
+ if (attributes.options.readOnly) {
+ return;
+ }
+ if (
+ attributes.type === 'object' &&
+ val &&
+ Object.keys(val).length > 0 &&
+ Ember.isNone(snapshot.changedAttributes()[key])
+ ) {
+ return;
+ }
+ if (Ember.isBlank(val) && Ember.isNone(snapshot.changedAttributes()[key])) {
+ return;
+ }
+
+ this._super(snapshot, json, key, attributes);
+ },
+ serialize() {
+ return this._super(...arguments);
+ },
+});
diff --git a/ui/app/serializers/role-ssh.js b/ui/app/serializers/role-ssh.js
new file mode 100644
index 000000000..d36af6b2f
--- /dev/null
+++ b/ui/app/serializers/role-ssh.js
@@ -0,0 +1,3 @@
+import RoleSerializer from './role';
+
+export default RoleSerializer.extend();
diff --git a/ui/app/serializers/role.js b/ui/app/serializers/role.js
new file mode 100644
index 000000000..b8f86a1d2
--- /dev/null
+++ b/ui/app/serializers/role.js
@@ -0,0 +1,80 @@
+import DS from 'ember-data';
+import Ember from 'ember';
+const { decamelize } = Ember.String;
+
+export default DS.RESTSerializer.extend({
+ keyForAttribute: function(attr) {
+ return decamelize(attr);
+ },
+
+ extractLazyPaginatedData(payload) {
+ let ret;
+ if (payload.zero_address_roles) {
+ payload.zero_address_roles.forEach(role => {
+ // mutate key_info object to add zero_address info
+ payload.data.key_info[role].zero_address = true;
+ });
+ }
+ ret = payload.data.keys.map(key => {
+ let model = {
+ id: key,
+ key_type: payload.data.key_info[key].key_type,
+ zero_address: payload.data.key_info[key].zero_address,
+ };
+ if (payload.backend) {
+ model.backend = payload.backend;
+ }
+ return model;
+ });
+ delete payload.data.key_info;
+ return ret;
+ },
+
+ normalizeItems(payload) {
+ if (payload.data.keys && Array.isArray(payload.data.keys)) {
+ return payload.data.keys;
+ }
+ Ember.assign(payload, payload.data);
+ delete payload.data;
+ return [payload];
+ },
+ modelNameFromPayloadKey(payloadType) {
+ return payloadType;
+ },
+
+ normalizeResponse(store, primaryModelClass, payload, id, requestType) {
+ const nullResponses = ['updateRecord', 'createRecord', 'deleteRecord'];
+ const responseJSON = nullResponses.includes(requestType) ? { id } : this.normalizeItems(payload);
+ const { modelName } = primaryModelClass;
+ let transformedPayload = { [modelName]: responseJSON };
+ // just return the single object because ember is picky
+ if (requestType === 'queryRecord') {
+ transformedPayload = { [modelName]: responseJSON[0] };
+ }
+
+ return this._super(store, primaryModelClass, transformedPayload, id, requestType);
+ },
+
+ serializeAttribute(snapshot, json, key, attributes) {
+ const val = snapshot.attr(key);
+ if (attributes.options.readOnly) {
+ return;
+ }
+ if (
+ attributes.type === 'object' &&
+ val &&
+ Object.keys(val).length > 0 &&
+ Ember.isNone(snapshot.changedAttributes()[key])
+ ) {
+ return;
+ }
+ if (Ember.isBlank(val) && Ember.isNone(snapshot.changedAttributes()[key])) {
+ return;
+ }
+
+ this._super(snapshot, json, key, attributes);
+ },
+ serialize() {
+ return this._super(...arguments);
+ },
+});
diff --git a/ui/app/serializers/secret-cubbyhole.js b/ui/app/serializers/secret-cubbyhole.js
new file mode 100644
index 000000000..079c9bf84
--- /dev/null
+++ b/ui/app/serializers/secret-cubbyhole.js
@@ -0,0 +1,42 @@
+import DS from 'ember-data';
+import Ember from 'ember';
+const { decamelize } = Ember.String;
+
+export default DS.RESTSerializer.extend({
+ keyForAttribute: function(attr) {
+ return decamelize(attr);
+ },
+
+ normalizeSecrets(payload) {
+ if (payload.data.keys && Array.isArray(payload.data.keys)) {
+ const secrets = payload.data.keys.map(secret => {
+ let fullSecretPath = payload.id ? payload.id + secret : secret;
+ if (!fullSecretPath) {
+ fullSecretPath = '\u0020';
+ }
+ return { id: fullSecretPath };
+ });
+ return secrets;
+ }
+ payload.secret_data = payload.data;
+ delete payload.data;
+ return [payload];
+ },
+
+ normalizeResponse(store, primaryModelClass, payload, id, requestType) {
+ const nullResponses = ['updateRecord', 'createRecord', 'deleteRecord'];
+ const secrets = nullResponses.includes(requestType) ? { id } : this.normalizeSecrets(payload);
+ const { modelName } = primaryModelClass;
+ let transformedPayload = { [modelName]: secrets };
+ // just return the single object because ember is picky
+ if (requestType === 'queryRecord') {
+ transformedPayload = { [modelName]: secrets[0] };
+ }
+
+ return this._super(store, primaryModelClass, transformedPayload, id, requestType);
+ },
+
+ serialize(snapshot) {
+ return snapshot.attr('secretData');
+ },
+});
diff --git a/ui/app/serializers/secret-engine.js b/ui/app/serializers/secret-engine.js
new file mode 100644
index 000000000..c14495549
--- /dev/null
+++ b/ui/app/serializers/secret-engine.js
@@ -0,0 +1,51 @@
+import DS from 'ember-data';
+import Ember from 'ember';
+const { decamelize } = Ember.String;
+
+export default DS.RESTSerializer.extend({
+ keyForAttribute: function(attr) {
+ return decamelize(attr);
+ },
+ normalizeBackend(path, backend) {
+ let struct = {};
+ for (let attribute in backend) {
+ struct[attribute] = backend[attribute];
+ }
+ //queryRecord adds path to the response
+ if (path !== null && !struct.path) {
+ struct.path = path;
+ }
+
+ if (struct.data) {
+ struct = Ember.assign({}, struct, struct.data);
+ delete struct.data;
+ }
+ // strip the trailing slash off of the path so we
+ // can navigate to it without getting `//` in the url
+ struct.id = struct.path.slice(0, -1);
+ return struct;
+ },
+
+ normalizeResponse(store, primaryModelClass, payload, id, requestType) {
+ const isCreate = requestType === 'createRecord';
+ const isFind = requestType === 'findRecord';
+ const isQueryRecord = requestType === 'queryRecord';
+ let backends;
+ if (isCreate) {
+ backends = payload.data;
+ } else if (isFind) {
+ backends = this.normalizeBackend(id + '/', payload.data);
+ } else if (isQueryRecord) {
+ backends = this.normalizeBackend(null, payload);
+ } else {
+ backends = Object.keys(payload.data).map(id => this.normalizeBackend(id, payload[id]));
+ }
+
+ const transformedPayload = { [primaryModelClass.modelName]: backends };
+ return this._super(store, primaryModelClass, transformedPayload, id, requestType);
+ },
+
+ serialize() {
+ return this._super(...arguments);
+ },
+});
diff --git a/ui/app/serializers/secret.js b/ui/app/serializers/secret.js
new file mode 100644
index 000000000..91b1984e9
--- /dev/null
+++ b/ui/app/serializers/secret.js
@@ -0,0 +1,42 @@
+import DS from 'ember-data';
+import Ember from 'ember';
+const { decamelize } = Ember.String;
+
+export default DS.RESTSerializer.extend({
+ keyForAttribute: function(attr) {
+ return decamelize(attr);
+ },
+
+ normalizeSecrets(payload) {
+ if (payload.data.keys && Array.isArray(payload.data.keys)) {
+ const secrets = payload.data.keys.map(secret => {
+ let fullSecretPath = payload.id ? payload.id + secret : secret;
+ if (!fullSecretPath) {
+ fullSecretPath = '\u0020';
+ }
+ return { id: fullSecretPath };
+ });
+ return secrets;
+ }
+ payload.secret_data = payload.data.data;
+ delete payload.data;
+ return [payload];
+ },
+
+ normalizeResponse(store, primaryModelClass, payload, id, requestType) {
+ const nullResponses = ['updateRecord', 'createRecord', 'deleteRecord'];
+ const secrets = nullResponses.includes(requestType) ? { id } : this.normalizeSecrets(payload);
+ const { modelName } = primaryModelClass;
+ let transformedPayload = { [modelName]: secrets };
+ // just return the single object because ember is picky
+ if (requestType === 'queryRecord') {
+ transformedPayload = { [modelName]: secrets[0] };
+ }
+
+ return this._super(store, primaryModelClass, transformedPayload, id, requestType);
+ },
+
+ serialize(snapshot) {
+ return snapshot.attr('secretData');
+ },
+});
diff --git a/ui/app/serializers/ssh.js b/ui/app/serializers/ssh.js
new file mode 100644
index 000000000..a49f045af
--- /dev/null
+++ b/ui/app/serializers/ssh.js
@@ -0,0 +1,54 @@
+import DS from 'ember-data';
+import Ember from 'ember';
+const { decamelize } = Ember.String;
+
+export default DS.RESTSerializer.extend({
+ keyForAttribute: function(attr) {
+ return decamelize(attr);
+ },
+
+ pushPayload(store, payload) {
+ const transformedPayload = this.normalizeResponse(
+ store,
+ store.modelFor(payload.modelName),
+ payload,
+ payload.id,
+ 'findRecord'
+ );
+ return store.push(transformedPayload);
+ },
+
+ normalizeItems(payload) {
+ Ember.assign(payload, payload.data);
+ delete payload.data;
+ return payload;
+ },
+
+ normalizeResponse(store, primaryModelClass, payload, id, requestType) {
+ const responseJSON = this.normalizeItems(payload);
+ const { modelName } = primaryModelClass;
+ let transformedPayload = { [modelName]: responseJSON };
+ let ret = this._super(store, primaryModelClass, transformedPayload, id, requestType);
+ return ret;
+ },
+
+ serializeAttribute(snapshot, json, key, attributes) {
+ const val = snapshot.attr(key);
+ if (attributes.options.readOnly) {
+ return;
+ }
+ if (
+ attributes.type === 'object' &&
+ val &&
+ Object.keys(val).length > 0 &&
+ Ember.isNone(snapshot.changedAttributes()[key])
+ ) {
+ return;
+ }
+ if (Ember.isBlank(val) && Ember.isNone(snapshot.changedAttributes()[key])) {
+ return;
+ }
+
+ this._super(snapshot, json, key, attributes);
+ },
+});
diff --git a/ui/app/serializers/transit-key.js b/ui/app/serializers/transit-key.js
new file mode 100644
index 000000000..5ace36a78
--- /dev/null
+++ b/ui/app/serializers/transit-key.js
@@ -0,0 +1,49 @@
+import DS from 'ember-data';
+import Ember from 'ember';
+const { decamelize } = Ember.String;
+
+export default DS.RESTSerializer.extend({
+ primaryKey: 'name',
+
+ keyForAttribute: function(attr) {
+ return decamelize(attr);
+ },
+
+ normalizeSecrets(payload) {
+ if (payload.data.keys && Array.isArray(payload.data.keys)) {
+ const secrets = payload.data.keys.map(secret => ({ name: secret }));
+ return secrets;
+ }
+ Ember.assign(payload, payload.data);
+ delete payload.data;
+ return [payload];
+ },
+
+ normalizeResponse(store, primaryModelClass, payload, id, requestType) {
+ const nullResponses = ['updateRecord', 'createRecord', 'deleteRecord'];
+ const secrets = nullResponses.includes(requestType) ? { name: id } : this.normalizeSecrets(payload);
+ const { modelName } = primaryModelClass;
+ let transformedPayload = { [modelName]: secrets };
+ // just return the single object because ember is picky
+ if (requestType === 'queryRecord') {
+ transformedPayload = { [modelName]: secrets[0] };
+ }
+
+ return this._super(store, primaryModelClass, transformedPayload, id, requestType);
+ },
+
+ serialize(snapshot, requestType) {
+ if (requestType === 'update') {
+ const min_decryption_version = snapshot.attr('minDecryptionVersion');
+ const min_encryption_version = snapshot.attr('minEncryptionVersion');
+ const deletion_allowed = snapshot.attr('deletionAllowed');
+ return {
+ min_decryption_version,
+ min_encryption_version,
+ deletion_allowed,
+ };
+ } else {
+ return this._super(...arguments);
+ }
+ },
+});
diff --git a/ui/app/services/auth.js b/ui/app/services/auth.js
new file mode 100644
index 000000000..19cb8cc9c
--- /dev/null
+++ b/ui/app/services/auth.js
@@ -0,0 +1,294 @@
+import Ember from 'ember';
+import getStorage from '../lib/token-storage';
+import ENV from 'vault/config/environment';
+import { supportedAuthBackends } from 'vault/helpers/supported-auth-backends';
+
+const { get, isArray, computed, getOwner } = Ember;
+
+const TOKEN_SEPARATOR = '☃';
+const TOKEN_PREFIX = 'vault-';
+const ROOT_PREFIX = '🗝';
+const IDLE_TIMEOUT_MS = 3 * 60e3;
+const BACKENDS = supportedAuthBackends();
+
+export { TOKEN_SEPARATOR, TOKEN_PREFIX, ROOT_PREFIX };
+
+export default Ember.Service.extend({
+ expirationCalcTS: null,
+ init() {
+ this._super(...arguments);
+ this.checkForRootToken();
+ },
+
+ clusterAdapter() {
+ return getOwner(this).lookup('adapter:cluster');
+ },
+
+ tokens: computed(function() {
+ return this.getTokensFromStorage() || [];
+ }),
+
+ generateTokenName({ backend, clusterId }, policies) {
+ return (policies || []).includes('root')
+ ? `${TOKEN_PREFIX}${ROOT_PREFIX}${TOKEN_SEPARATOR}${clusterId}`
+ : `${TOKEN_PREFIX}${backend}${TOKEN_SEPARATOR}${clusterId}`;
+ },
+
+ backendFromTokenName(tokenName) {
+ return tokenName.includes(`${TOKEN_PREFIX}${ROOT_PREFIX}`)
+ ? 'token'
+ : tokenName.slice(TOKEN_PREFIX.length).split(TOKEN_SEPARATOR)[0];
+ },
+
+ storage(tokenName) {
+ if (
+ tokenName &&
+ tokenName.indexOf(`${TOKEN_PREFIX}${ROOT_PREFIX}`) === 0 &&
+ this.environment() !== 'development'
+ ) {
+ return getStorage('memory');
+ } else {
+ return getStorage();
+ }
+ },
+
+ environment() {
+ return ENV.environment;
+ },
+
+ setCluster(clusterId) {
+ this.set('activeCluster', clusterId);
+ },
+
+ ajax(url, method, options) {
+ const defaults = {
+ url,
+ method,
+ dataType: 'json',
+ headers: {
+ 'X-Vault-Token': this.get('currentToken'),
+ },
+ };
+ return Ember.$.ajax(Ember.assign(defaults, options));
+ },
+
+ renewCurrentToken() {
+ const url = '/v1/auth/token/renew-self';
+ return this.ajax(url, 'POST');
+ },
+
+ revokeCurrentToken() {
+ const url = '/v1/auth/token/revoke-self';
+ return this.ajax(url, 'POST');
+ },
+
+ calculateExpiration(resp, creationTime) {
+ const creationTTL = resp.creation_ttl || resp.lease_duration;
+ const leaseMilli = creationTTL ? creationTTL * 1e3 : null;
+ const tokenIssueEpoch = resp.creation_time ? resp.creation_time * 1e3 : creationTime || Date.now();
+ const tokenExpirationEpoch = tokenIssueEpoch + leaseMilli;
+ const expirationData = {
+ tokenIssueEpoch,
+ tokenExpirationEpoch,
+ leaseMilli,
+ };
+ this.set('expirationCalcTS', Date.now());
+ return expirationData;
+ },
+
+ persistAuthData() {
+ const [firstArg, resp] = arguments;
+ let tokens = this.get('tokens');
+ let tokenName;
+ let options;
+ let backend;
+ if (typeof firstArg === 'string') {
+ tokenName = firstArg;
+ backend = this.backendFromTokenName(tokenName);
+ } else {
+ options = firstArg;
+ backend = options.backend;
+ }
+
+ const currentBackend = BACKENDS.findBy('type', backend);
+ let displayName;
+ if (isArray(currentBackend.displayNamePath)) {
+ displayName = currentBackend.displayNamePath.map(name => get(resp, name)).join('/');
+ } else {
+ displayName = get(resp, currentBackend.displayNamePath);
+ }
+
+ const { policies, renewable } = resp;
+ let data = {
+ displayName,
+ backend: currentBackend,
+ token: resp.client_token || get(resp, currentBackend.tokenPath),
+ policies,
+ renewable,
+ };
+
+ tokenName = this.generateTokenName(
+ {
+ backend,
+ clusterId: (options && options.clusterId) || this.get('activeCluster'),
+ },
+ resp.policies
+ );
+
+ if (resp.renewable) {
+ Ember.assign(data, this.calculateExpiration(resp));
+ }
+
+ if (!data.displayName) {
+ data.displayName = get(this.getTokenData(tokenName) || {}, 'displayName');
+ }
+ tokens.addObject(tokenName);
+ this.set('tokens', tokens);
+ this.set('allowExpiration', false);
+ this.setTokenData(tokenName, data);
+ return Ember.RSVP.resolve({
+ token: tokenName,
+ isRoot: policies.includes('root'),
+ });
+ },
+
+ setTokenData(token, data) {
+ this.storage(token).setItem(token, data);
+ },
+
+ getTokenData(token) {
+ return this.storage(token).getItem(token);
+ },
+
+ removeTokenData(token) {
+ return this.storage(token).removeItem(token);
+ },
+
+ tokenExpirationDate: computed('currentTokenName', 'expirationCalcTS', function() {
+ const tokenName = this.get('currentTokenName');
+ if (!tokenName) {
+ return;
+ }
+ const { tokenExpirationEpoch } = this.getTokenData(tokenName);
+ const expirationDate = new Date(0);
+ return tokenExpirationEpoch ? expirationDate.setUTCMilliseconds(tokenExpirationEpoch) : null;
+ }),
+
+ tokenExpired: computed(function() {
+ const expiration = this.get('tokenExpirationDate');
+ return expiration ? Date.now() >= expiration : null;
+ }).volatile(),
+
+ renewAfterEpoch: computed('currentTokenName', 'expirationCalcTS', function() {
+ const tokenName = this.get('currentTokenName');
+ const data = this.getTokenData(tokenName);
+ if (!tokenName || !data) {
+ return null;
+ }
+ const { leaseMilli, tokenIssueEpoch, renewable } = data;
+ return data && renewable ? Math.floor(leaseMilli / 2) + tokenIssueEpoch : null;
+ }),
+
+ renew() {
+ const tokenName = this.get('currentTokenName');
+ const currentlyRenewing = this.get('isRenewing');
+ if (currentlyRenewing) {
+ return;
+ }
+ this.set('isRenewing', true);
+ return this.renewCurrentToken().then(
+ resp => {
+ this.set('isRenewing', false);
+ return this.persistAuthData(tokenName, resp.data || resp.auth);
+ },
+ e => {
+ this.set('isRenewing', false);
+ throw e;
+ }
+ );
+ },
+
+ shouldRenew: computed(function() {
+ const now = Date.now();
+ const lastFetch = this.get('lastFetch');
+ const renewTime = this.get('renewAfterEpoch');
+ if (this.get('tokenExpired') || this.get('allowExpiration') || !renewTime) {
+ return false;
+ }
+ if (lastFetch && now - lastFetch >= IDLE_TIMEOUT_MS) {
+ this.set('allowExpiration', true);
+ return false;
+ }
+ if (now >= renewTime) {
+ return true;
+ }
+ return false;
+ }).volatile(),
+
+ setLastFetch(timestamp) {
+ this.set('lastFetch', timestamp);
+ },
+
+ getTokensFromStorage(filterFn) {
+ return this.storage().keys().reject(key => {
+ return key.indexOf(TOKEN_PREFIX) !== 0 || (filterFn && filterFn(key));
+ });
+ },
+
+ checkForRootToken() {
+ if (this.environment() === 'development') {
+ return;
+ }
+ this.getTokensFromStorage().forEach(key => {
+ const data = this.getTokenData(key);
+ if (data.policies.includes('root')) {
+ this.removeTokenData(key);
+ }
+ });
+ },
+
+ authenticate(/*{clusterId, backend, data}*/) {
+ const [options] = arguments;
+ const adapter = this.clusterAdapter();
+
+ return adapter.authenticate(options).then(resp => {
+ return this.persistAuthData(options, resp.auth || resp.data);
+ });
+ },
+
+ deleteCurrentToken() {
+ const tokenName = this.get('currentTokenName');
+ this.deleteToken(tokenName);
+ this.removeTokenData(tokenName);
+ },
+
+ deleteToken(tokenName) {
+ const tokenNames = this.get('tokens').without(tokenName);
+ this.removeTokenData(tokenName);
+ this.set('tokens', tokenNames);
+ },
+
+ currentTokenName: computed('activeCluster', 'tokens.[]', function() {
+ const regex = new RegExp(this.get('activeCluster'));
+ return this.get('tokens').find(key => regex.test(key));
+ }),
+
+ currentToken: computed('currentTokenName', function() {
+ const name = this.get('currentTokenName');
+ const data = name && this.getTokenData(name);
+ return name && data ? data.token : null;
+ }),
+
+ authData: computed('currentTokenName', function() {
+ const token = this.get('currentTokenName');
+ if (!token) {
+ return;
+ }
+ const backend = this.backendFromTokenName(token);
+ const stored = this.getTokenData(token);
+
+ return Ember.assign(stored, {
+ backend: BACKENDS.findBy('type', backend),
+ });
+ }),
+});
diff --git a/ui/app/services/current-cluster.js b/ui/app/services/current-cluster.js
new file mode 100644
index 000000000..e1bf4ba14
--- /dev/null
+++ b/ui/app/services/current-cluster.js
@@ -0,0 +1,9 @@
+import Ember from 'ember';
+
+export default Ember.Service.extend({
+ cluster: null,
+
+ setCluster(cluster) {
+ this.set('cluster', cluster);
+ },
+});
diff --git a/ui/app/services/flash-messages.js b/ui/app/services/flash-messages.js
new file mode 100644
index 000000000..ae0258579
--- /dev/null
+++ b/ui/app/services/flash-messages.js
@@ -0,0 +1,10 @@
+import FlashMessages from 'ember-cli-flash/services/flash-messages';
+
+export default FlashMessages.extend({
+ stickyInfo(message) {
+ return this.info(message, {
+ sticky: true,
+ priority: 300,
+ });
+ },
+});
diff --git a/ui/app/services/replication-mode.js b/ui/app/services/replication-mode.js
new file mode 100644
index 000000000..c5a1fc9f9
--- /dev/null
+++ b/ui/app/services/replication-mode.js
@@ -0,0 +1,13 @@
+import Ember from 'ember';
+
+export default Ember.Service.extend({
+ mode: null,
+
+ getMode() {
+ this.get('mode');
+ },
+
+ setMode(mode) {
+ this.set('mode', mode);
+ },
+});
diff --git a/ui/app/services/store.js b/ui/app/services/store.js
new file mode 100644
index 000000000..bc092f923
--- /dev/null
+++ b/ui/app/services/store.js
@@ -0,0 +1,172 @@
+import DS from 'ember-data';
+import Ember from 'ember';
+import clamp from 'vault/utils/clamp';
+
+const { assert, computed, get, set } = Ember;
+
+export function normalizeModelName(modelName) {
+ return Ember.String.dasherize(modelName);
+}
+
+export function keyForCache(query) {
+ /*eslint no-unused-vars: ["error", { "ignoreRestSiblings": true }]*/
+ // we want to ignore size, page, responsePath, and pageFilter in the cacheKey
+ const { size, page, responsePath, pageFilter, ...queryForCache } = query;
+ const cacheKeyObject = Object.keys(queryForCache).sort().reduce((result, key) => {
+ result[key] = queryForCache[key];
+ return result;
+ }, {});
+ return JSON.stringify(cacheKeyObject);
+}
+
+export default DS.Store.extend({
+ // this is a map of map that stores the caches
+ lazyCaches: computed(function() {
+ return new Map();
+ }),
+
+ setLazyCacheForModel(modelName, key, value) {
+ const cacheKey = keyForCache(key);
+ const cache = this.lazyCacheForModel(modelName) || new Map();
+ cache.set(cacheKey, value);
+ const lazyCaches = this.get('lazyCaches');
+ const modelKey = normalizeModelName(modelName);
+ lazyCaches.set(modelKey, cache);
+ },
+
+ getLazyCacheForModel(modelName, key) {
+ const cacheKey = keyForCache(key);
+ const modelCache = this.lazyCacheForModel(modelName);
+ if (modelCache) {
+ return modelCache.get(cacheKey);
+ }
+ },
+
+ lazyCacheForModel(modelName) {
+ return this.get('lazyCaches').get(normalizeModelName(modelName));
+ },
+
+ // This is the public interface for the store extension - to be used just
+ // like `Store.query`. Special handling of the response is controlled by
+ // `query.pageFilter`, `query.page`, and `query.size`.
+
+ // Required attributes of the `query` argument are:
+ // responsePath: a string indicating the location on the response where
+ // the array of items will be found
+ // page: the page number to return
+ // size: the size of the page
+ // pageFilter: a string that will be used to do a fuzzy match against the
+ // results, this is done pre-pagination
+ lazyPaginatedQuery(modelType, query /*, options*/) {
+ const adapter = this.adapterFor(modelType);
+ const modelName = normalizeModelName(modelType);
+ const dataCache = this.getDataset(modelName, query);
+ const responsePath = query.responsePath;
+ assert('responsePath is required', responsePath);
+ assert('page is required', typeof query.page === 'number');
+ assert('size is required', query.size);
+
+ if (dataCache) {
+ return Ember.RSVP.resolve(this.fetchPage(modelName, query));
+ }
+ return adapter
+ .query(this, { modelName }, query)
+ .then(response => {
+ const serializer = this.serializerFor(modelName);
+ const datasetHelper = serializer.extractLazyPaginatedData;
+ const dataset = datasetHelper
+ ? datasetHelper.call(serializer, response)
+ : get(response, responsePath);
+ set(response, responsePath, null);
+ this.storeDataset(modelName, query, response, dataset);
+ return this.fetchPage(modelName, query);
+ })
+ .catch(function(e) {
+ throw e;
+ });
+ },
+
+ filterData(filter, dataset) {
+ let newData = dataset || [];
+ if (filter) {
+ newData = dataset.filter(function(item) {
+ const id = item.id || item;
+ return id.toLowerCase().includes(filter.toLowerCase());
+ });
+ }
+ return newData;
+ },
+
+ // reconstructs the original form of the response from the server
+ // with an additional `meta` block
+ //
+ // the meta block includes:
+ // currentPage, lastPage, nextPage, prevPage, total, filteredTotal
+ constructResponse(modelName, query) {
+ const { pageFilter, responsePath, size, page } = query;
+ let { response, dataset } = this.getDataset(modelName, query);
+ response = Ember.copy(response, true);
+ const data = this.filterData(pageFilter, dataset);
+
+ const lastPage = Math.ceil(data.length / size);
+ const currentPage = clamp(page, 1, lastPage);
+ const end = currentPage * size;
+ const start = end - size;
+ const slicedDataSet = data.slice(start, end);
+
+ set(response, responsePath || '', slicedDataSet);
+
+ response.meta = {
+ currentPage,
+ lastPage,
+ nextPage: clamp(currentPage + 1, 1, lastPage),
+ prevPage: clamp(currentPage - 1, 1, lastPage),
+ total: get(dataset, 'length') || 0,
+ filteredTotal: get(data, 'length') || 0,
+ };
+
+ return response;
+ },
+
+ // pushes records into the store and returns the result
+ fetchPage(modelName, query) {
+ const response = this.constructResponse(modelName, query);
+ this.unloadAll(modelName);
+ this.push(
+ this.serializerFor(modelName).normalizeResponse(this, this.modelFor(modelName), response, null, 'query')
+ );
+ const model = this.peekAll(modelName);
+ model.set('meta', response.meta);
+ return model;
+ },
+
+ // get cached data
+ getDataset(modelName, query) {
+ return this.getLazyCacheForModel(modelName, query);
+ },
+
+ // store data cache as { response, dataset}
+ // also populated `lazyCaches` attribute
+ storeDataset(modelName, query, response, array) {
+ const dataSet = {
+ response,
+ dataset: array,
+ };
+ this.setLazyCacheForModel(modelName, query, dataSet);
+ },
+
+ clearDataset(modelName) {
+ let cacheList = this.get('lazyCaches');
+ if (!cacheList.size) return;
+ if (modelName && cacheList.has(modelName)) {
+ cacheList.delete(modelName);
+ return;
+ }
+ cacheList.clear();
+ this.set('lazyCaches', cacheList);
+ },
+
+ clearAllDatasets() {
+ this.clearDataset();
+ },
+});
diff --git a/ui/app/services/version.js b/ui/app/services/version.js
new file mode 100644
index 000000000..8236d82e3
--- /dev/null
+++ b/ui/app/services/version.js
@@ -0,0 +1,68 @@
+import Ember from 'ember';
+import { task } from 'ember-concurrency';
+
+const { Service, inject, computed } = Ember;
+
+const hasFeature = featureKey => {
+ return computed('features', 'features.[]', function() {
+ const features = this.get('features');
+ if (!features) {
+ return false;
+ }
+ return features.includes(featureKey);
+ });
+};
+export default Service.extend({
+ _features: null,
+ features: computed.readOnly('_features'),
+ version: null,
+ store: inject.service(),
+
+ hasPerfReplication: hasFeature('Performance Replication'),
+
+ hasDRReplication: hasFeature('DR Replication'),
+
+ isEnterprise: computed.match('version', /\+\w+$/),
+
+ isOSS: computed.not('isEnterprise'),
+
+ setVersion(resp) {
+ this.set('version', resp.version);
+ },
+
+ setFeatures(resp) {
+ if (!resp.features) {
+ return;
+ }
+ this.set('_features', resp.features);
+ },
+
+ getVersion: task(function*() {
+ if (this.get('version')) {
+ return;
+ }
+ let response = yield this.get('store').adapterFor('cluster').health();
+ this.setVersion(response);
+ return;
+ }),
+
+ getFeatures: task(function*() {
+ if (this.get('features.length') || this.get('isOSS')) {
+ return;
+ }
+ try {
+ let response = yield this.get('store').adapterFor('cluster').features();
+ this.setFeatures(response);
+ return;
+ } catch (err) {
+ throw err;
+ }
+ }).keepLatest(),
+
+ fetchVersion: function() {
+ return this.get('getVersion').perform();
+ },
+ fetchFeatures: function() {
+ return this.get('getFeatures').perform();
+ },
+});
diff --git a/ui/app/styles/app.scss b/ui/app/styles/app.scss
new file mode 100644
index 000000000..fe76b668a
--- /dev/null
+++ b/ui/app/styles/app.scss
@@ -0,0 +1,2 @@
+@import "ember-basic-dropdown";
+@import "./core";
diff --git a/ui/app/styles/components/auth-form.scss b/ui/app/styles/components/auth-form.scss
new file mode 100644
index 000000000..27e0229b8
--- /dev/null
+++ b/ui/app/styles/components/auth-form.scss
@@ -0,0 +1,5 @@
+.auth-form {
+ @extend .box;
+ @extend .is-bottomless;
+ padding: 0;
+}
diff --git a/ui/app/styles/components/b64-toggle.scss b/ui/app/styles/components/b64-toggle.scss
new file mode 100644
index 000000000..676e9365e
--- /dev/null
+++ b/ui/app/styles/components/b64-toggle.scss
@@ -0,0 +1,13 @@
+.b64-toggle {
+ padding: 0.75rem;
+ font-size: $size-9;
+}
+.b64-toggle.is-input {
+ box-shadow: none;
+}
+.b64-toggle.is-textarea {
+ @extend .is-compact;
+ position: absolute;
+ bottom: 0.25rem;
+ right: 0.25rem;
+}
diff --git a/ui/app/styles/components/badge.scss b/ui/app/styles/components/badge.scss
new file mode 100644
index 000000000..de6001b55
--- /dev/null
+++ b/ui/app/styles/components/badge.scss
@@ -0,0 +1,24 @@
+.badge {
+ border: 1px solid $grey;
+ border-radius: 3px;
+ color: $grey-dark;
+ display: inline-block;
+ font-size: $size-9;
+ line-height: $size-7;
+ text-transform: uppercase;
+ margin-left: 0.5rem;
+ opacity: 0.6;
+ padding: 0.1rem 0.3rem;
+
+ .navbar &,
+ .navbar-sections &,
+ .upgrade-overlay & {
+ border-color: $grey-light;
+ color: $white;
+ }
+
+ .title & {
+ font-weight: $font-weight-normal;
+ vertical-align: middle;
+ }
+}
diff --git a/ui/app/styles/components/box-label.scss b/ui/app/styles/components/box-label.scss
new file mode 100644
index 000000000..c536c26b6
--- /dev/null
+++ b/ui/app/styles/components/box-label.scss
@@ -0,0 +1,42 @@
+label.box-label {
+ cursor: pointer;
+}
+.box-label {
+ @extend .box;
+ @extend .is-centered;
+ @extend .is-gapless;
+ border-radius: 3px;
+ text-decoration: none;
+ width: 100%;
+
+ > div:first-child {
+ flex-grow: 1;
+ }
+
+ &.is-column {
+ @extend .is-flex-column;
+ }
+ &.is-selected {
+ box-shadow: 0 0 0 1px $blue;
+ }
+
+ input[type=radio] {
+ display: none;
+ }
+
+ input[type=radio] + label {
+ border: 1px solid $grey;
+ border-radius: 50%;
+ cursor: pointer;
+ display: block;
+ margin: 1rem auto 0;
+ height: 1rem;
+ width: 1rem;
+ }
+
+ input[type=radio]:checked + label {
+ background: $blue;
+ border: 1px solid $blue;
+ box-shadow: inset 0 0 0 0.15rem $white;
+ }
+}
diff --git a/ui/app/styles/components/codemirror.scss b/ui/app/styles/components/codemirror.scss
new file mode 100644
index 000000000..37f773a04
--- /dev/null
+++ b/ui/app/styles/components/codemirror.scss
@@ -0,0 +1,173 @@
+$light-grey: #dde3e7;
+$light-gray: #a4a4a4;
+$light-grey-blue: #6c7b81;
+$dark-grey: #788290;
+$faded-gray: #eaeaea;
+// Product colors
+$atlas: #127eff;
+$vagrant: #2f88f7;
+$consul: #69499a;
+$terraform: #822ff7;
+$serf: #dd4e58;
+$packer: #1ddba3;
+
+// Our colors
+$gray: lighten($black, 89%);
+$red: #ff3d3d;
+$green: #39b54a;
+$dark-gray: #535f73;
+
+$gutter-grey: #2a2f36;
+
+.CodeMirror-lint-tooltip {
+ background-color: #f9f9fa;
+ border: 1px solid $light-gray;
+ border-radius: 0;
+ color: lighten($black, 13%);
+ font-family: $family-monospace;
+ font-size: 13px;
+ padding: 7px 8px 9px;
+}
+
+.cm-s-hashi {
+ &.CodeMirror {
+ background-color: $black !important;
+ color: #cfd2d1 !important;
+ border: none;
+ font-family: $family-monospace;
+ -webkit-font-smoothing: auto;
+ line-height: 1.4;
+ }
+
+ .CodeMirror-gutters {
+ color: $dark-grey;
+ background-color: $gutter-grey;
+ border: none;
+ }
+
+ .CodeMirror-cursor {
+ border-left: solid thin #f8f8f0;
+ }
+
+ .CodeMirror-linenumber {
+ color: #6d8a88;
+ }
+
+ &.CodeMirror-focused div.CodeMirror-selected {
+ background: rgba(255, 255, 255, 0.10);
+ }
+
+ .CodeMirror-line::selection,
+ .CodeMirror-line > span::selection,
+ .CodeMirror-line > span > span::selection {
+ background: rgba(255, 255, 255, 0.10);
+ }
+
+ .CodeMirror-line::-moz-selection,
+ .CodeMirror-line > span::-moz-selection,
+ .CodeMirror-line > span > span::-moz-selection {
+ background: rgba(255, 255, 255, 0.10);
+ }
+
+ span.cm-comment {
+ color: $light-grey;
+ }
+
+ span.cm-string,
+ span.cm-string-2 {
+ color: $packer;
+ }
+
+ span.cm-number {
+ color: $serf;
+ }
+
+ span.cm-variable {
+ color: lighten($consul, 20%);
+ }
+
+ span.cm-variable-2 {
+ color: lighten($consul, 20%);
+ }
+
+ span.cm-def {
+ color: $packer;
+ }
+
+ span.cm-operator {
+ color: $gray;
+ }
+ span.cm-keyword {
+ color: $yellow;
+ }
+
+ span.cm-atom {
+ color: $serf;
+ }
+
+ span.cm-meta {
+ color: $packer;
+ }
+
+ span.cm-tag {
+ color: $packer;
+ }
+
+ span.cm-attribute {
+ color: #9fca56;
+ }
+
+ span.cm-qualifier {
+ color: #9fca56;
+ }
+
+ span.cm-property {
+ color: lighten($consul, 20%);
+ }
+
+ span.cm-variable-3 {
+ color: #9fca56;
+ }
+
+ span.cm-builtin {
+ color: #9fca56;
+ }
+
+ .CodeMirror-activeline-background {
+ background: #101213;
+ }
+
+ .CodeMirror-matchingbracket {
+ text-decoration: underline;
+ color: white !important;
+ }
+}
+
+.readonly-codemirror {
+ .CodeMirror-cursors {
+ display: none;
+ }
+
+ .cm-s-hashi {
+ span {
+ color: $light-grey;
+ }
+
+ span.cm-string,
+ span.cm-string-2 {
+ color: $faded-gray;
+ }
+
+ span.cm-number {
+ color: lighten($dark-gray, 30%);
+ }
+
+ span.cm-property {
+ color: white;
+ }
+
+ span.cm-variable-2 {
+ color: $light-grey-blue;
+ }
+ }
+}
diff --git a/ui/app/styles/components/confirm.scss b/ui/app/styles/components/confirm.scss
new file mode 100644
index 000000000..d18a91f58
--- /dev/null
+++ b/ui/app/styles/components/confirm.scss
@@ -0,0 +1,23 @@
+.confirm-action > span {
+ @include from($tablet) {
+ align-items: center;
+ display: flex;
+ }
+
+ * {
+ margin-left: $size-8;
+ }
+
+ .confirm-action-text:not('.is-block') {
+ text-align: right;
+
+ @include until($tablet) {
+ display: block;
+ margin-bottom: $size-8;
+ text-align: left;
+ }
+ }
+ .confirm-action-text.is-block {
+ text-align: left;
+ }
+}
diff --git a/ui/app/styles/components/form-section.scss b/ui/app/styles/components/form-section.scss
new file mode 100644
index 000000000..721bd04d5
--- /dev/null
+++ b/ui/app/styles/components/form-section.scss
@@ -0,0 +1,9 @@
+.form-section {
+ padding-top: 1.75rem;
+ box-shadow: 0 -1px 0 0 rgba($black, 0.1);
+}
+
+.field:first-child .form-section {
+ padding: 0;
+ box-shadow: none;
+}
diff --git a/ui/app/styles/components/global-flash.scss b/ui/app/styles/components/global-flash.scss
new file mode 100644
index 000000000..d1c059778
--- /dev/null
+++ b/ui/app/styles/components/global-flash.scss
@@ -0,0 +1,24 @@
+.global-flash {
+ position: fixed;
+ @include until($desktop) {
+ position: -webkit-sticky;
+ position: sticky;
+ top: 0;
+ bottom: auto;
+ margin: 0 auto;
+ width: 95%;
+ }
+ width: 450px;
+ bottom: 0;
+ left: 0;
+ margin: 10px;
+ z-index: 1;
+
+ .notification {
+ box-shadow: 0 0 25px rgba($black, 0.2);
+ margin: 20px;
+ @include until($desktop) {
+ margin: 1rem 0;
+ }
+ }
+}
diff --git a/ui/app/styles/components/info-table-row.scss b/ui/app/styles/components/info-table-row.scss
new file mode 100644
index 000000000..73a0762d1
--- /dev/null
+++ b/ui/app/styles/components/info-table-row.scss
@@ -0,0 +1,57 @@
+.info-table-row {
+ box-shadow: 0 1px 0 $grey-light;
+ margin: 0 $size-6;
+
+ @include from($tablet) {
+ display: flex;
+ }
+
+ &.thead {
+ box-shadow: 0 1px 0 $grey-light, 0 -1px 0 $grey-light;
+ margin: 0;
+ padding: 0 $size-6;
+
+ .column {
+ padding: 0.5rem 0.75rem;
+ }
+ }
+
+ .column {
+ &.info-table-row-edit {
+ padding-bottom: 0.3rem;
+ padding-top: 0.3rem;
+ }
+
+ @include until($tablet) {
+ padding: 0;
+ }
+
+ &:first-child {
+ padding-left: 0;
+
+ @include until($tablet) {
+ padding: $size-8 0 0;
+ }
+ }
+
+ &:last-child {
+ padding-right: 0;
+
+ @include until($tablet) {
+ padding: 0 0 $size-8;
+ }
+ }
+
+ textarea {
+ min-height: 35px;
+ }
+ }
+}
+
+.info-table-row-header {
+ margin: 0;
+
+ @include until($tablet) {
+ display: none;
+ }
+}
diff --git a/ui/app/styles/components/init-illustration.scss b/ui/app/styles/components/init-illustration.scss
new file mode 100644
index 000000000..5cc72e28e
--- /dev/null
+++ b/ui/app/styles/components/init-illustration.scss
@@ -0,0 +1,5 @@
+.init-illustration {
+ position: absolute;
+ left: calc(50% - 100px);
+ top: -94px;
+}
diff --git a/ui/app/styles/components/input-hint.scss b/ui/app/styles/components/input-hint.scss
new file mode 100644
index 000000000..6e9e688e2
--- /dev/null
+++ b/ui/app/styles/components/input-hint.scss
@@ -0,0 +1,5 @@
+.input-hint {
+ padding: 0 $size-9;
+ font-size: $size-8;
+ color: $grey;
+}
diff --git a/ui/app/styles/components/linked-block.scss b/ui/app/styles/components/linked-block.scss
new file mode 100644
index 000000000..dfa6d7c8d
--- /dev/null
+++ b/ui/app/styles/components/linked-block.scss
@@ -0,0 +1,16 @@
+.linked-block {
+ cursor: pointer;
+ &:hover,
+ &:focus {
+ position: relative;
+ box-shadow: $box-link-hover-shadow;
+ }
+ &:active {
+ position: relative;
+ box-shadow: $box-link-active-shadow;
+ }
+}
+
+.linked-block .columns {
+ @extend .is-flex-center;
+}
diff --git a/ui/app/styles/components/list-pagination.scss b/ui/app/styles/components/list-pagination.scss
new file mode 100644
index 000000000..c012d7e41
--- /dev/null
+++ b/ui/app/styles/components/list-pagination.scss
@@ -0,0 +1,78 @@
+.list-pagination {
+ @extend .has-slim-padding;
+ position: relative;
+ top: 1px;
+ background-color: $grey-lighter;
+ margin-bottom: $size-4;
+
+ a {
+ text-decoration: none;
+ height: 1.5rem;
+ min-width: 1.5rem;
+ border: none;
+ }
+ a.pagination-link {
+ width: 3ch;
+ }
+ a:not('.is-current'):hover {
+ text-decoration: underline;
+ color: $blue;
+ }
+ a.is-current {
+ background-color: $grey;
+ }
+ .pagination {
+ justify-content: center;
+ }
+ .pagination-list {
+ flex-grow: 0;
+ }
+ .pagination-ellipsis {
+ margin: 0;
+ padding-left: 0;
+ padding-right: 0;
+ }
+}
+.list-pagination .pagination-previous,
+.list-pagination .pagination-next {
+ @extend .button;
+ @extend .is-primary;
+ @extend .is-outlined;
+ @extend .is-compact;
+ background: $white;
+ max-width: 8rem;
+
+ @include until($tablet) {
+ max-width: 2rem;
+ padding-left: 0;
+ padding-right: 0;
+ }
+
+ .pagination-next-label,
+ .pagination-previous-label {
+ @include until($tablet) {
+ display: none;
+ }
+ }
+
+ .icon {
+ height: 1em;
+ width: 1em;
+ vertical-align: middle;
+
+ &:last-child:not(:first-child),
+ &:first-child:not(:last-child) {
+ margin: -0.1em 0 0;
+ }
+ }
+
+ .button .icon {
+ margin: 0;
+ }
+}
+
+.list-pagination .pagination-next {
+ @include until($tablet) {
+ order: 3;
+ }
+}
diff --git a/ui/app/styles/components/loader.scss b/ui/app/styles/components/loader.scss
new file mode 100644
index 000000000..df2121b17
--- /dev/null
+++ b/ui/app/styles/components/loader.scss
@@ -0,0 +1,3 @@
+.loader-inner-page {
+ height: 60vh;
+}
diff --git a/ui/app/styles/components/message-in-page.scss b/ui/app/styles/components/message-in-page.scss
new file mode 100644
index 000000000..936e2273e
--- /dev/null
+++ b/ui/app/styles/components/message-in-page.scss
@@ -0,0 +1,10 @@
+.message-in-page {
+ margin-bottom: 2rem;
+ position: relative;
+
+ .close-button {
+ position: absolute;
+ right: 1rem;
+ top: $size-10;
+ }
+}
diff --git a/ui/app/styles/components/page-header.scss b/ui/app/styles/components/page-header.scss
new file mode 100644
index 000000000..5557130a2
--- /dev/null
+++ b/ui/app/styles/components/page-header.scss
@@ -0,0 +1,24 @@
+.page-header {
+ padding-bottom: $size-10;
+ padding-top: $size-4;
+
+ .level {
+ align-items: flex-end;
+ }
+ .level-left,
+ .level-right {
+ flex-grow: 1;
+ flex-shrink: 1;
+ }
+ .level-right {
+ justify-content: flex-end;
+ }
+
+ .title {
+ margin-top: $size-1;
+ }
+
+ .breadcrumb + .level .title {
+ margin-top: $size-4;
+ }
+}
diff --git a/ui/app/styles/components/popup-menu.scss b/ui/app/styles/components/popup-menu.scss
new file mode 100644
index 000000000..8e2406737
--- /dev/null
+++ b/ui/app/styles/components/popup-menu.scss
@@ -0,0 +1,75 @@
+.popup-menu-content {
+ border-radius: 2px;
+ margin: -2px 0 0 0;
+ & > .box {
+ border-radius: 2px;
+ padding: 0;
+ position: relative;
+ width: 175px;
+ @include css-top-arrow(8px, $white, 1px, $blue, 155px);
+ }
+ &.is-wide > .box {
+ width: 200px;
+ @include css-top-arrow(8px, $white, 1px, $blue, 178px);
+ }
+
+ .confirm-action span .button {
+ display: block;
+ margin: .25rem auto;
+ width: 95%;
+ }
+}
+.popup-menu-trigger {
+ width: 3rem;
+ height: 2rem;
+}
+.popup-menu-trigger.is-active {
+ &,
+ &:active,
+ &:focus {
+ box-shadow: 0 0 0 1px $blue;
+ }
+}
+
+.ember-basic-dropdown-content--left.popup-menu {
+ margin: 0px 0 0 -8px;
+}
+
+.popup-menu-content .menu {
+ button.link,
+ a {
+ border-radius: $menu-item-radius;
+ color: $menu-item-color;
+ font-size: $size-7;
+ font-weight: $font-weight-semibold;
+ display: block;
+ padding: $size-9 $size-6;
+ box-shadow: none;
+ border: none;
+ background: transparent;
+ height: auto;
+ width: 100%;
+ text-align: left;
+
+ &:hover {
+ background-color: $menu-item-hover-background-color;
+ color: $menu-item-hover-color;
+ }
+
+ &.is-active {
+ background-color: $menu-item-active-background-color;
+ color: $menu-item-active-color;
+ }
+ }
+}
+.popup-menu-content .menu-list {
+ margin: 0.1rem;
+}
+.popup-menu-content .menu-label {
+ background: $grey-lighter;
+ font-size: $size-8;
+ letter-spacing: 0;
+ margin: 0;
+ padding: $size-10 calc(#{$size-6} + .1rem);
+ text-transform: none;
+}
diff --git a/ui/app/styles/components/role-item.scss b/ui/app/styles/components/role-item.scss
new file mode 100644
index 000000000..9663deba0
--- /dev/null
+++ b/ui/app/styles/components/role-item.scss
@@ -0,0 +1,4 @@
+.role-item-details {
+ float: left;
+ margin-left: 8px;
+}
diff --git a/ui/app/styles/components/shamir-progress.scss b/ui/app/styles/components/shamir-progress.scss
new file mode 100644
index 000000000..4bb418328
--- /dev/null
+++ b/ui/app/styles/components/shamir-progress.scss
@@ -0,0 +1,11 @@
+.shamir-progress {
+ .shamir-progress-progress {
+ display: inline-block;
+ margin-right: $size-8;
+ }
+ .progress {
+ box-shadow: 0 0 0 4px $progress-bar-background-color;
+ display: inline;
+ width: 150px;
+ }
+}
diff --git a/ui/app/styles/components/sidebar.scss b/ui/app/styles/components/sidebar.scss
new file mode 100644
index 000000000..5e6e21353
--- /dev/null
+++ b/ui/app/styles/components/sidebar.scss
@@ -0,0 +1,85 @@
+.is-sidebar {
+ border-right: $base-border;
+ display: flex;
+ flex: 1;
+ margin: 0.75rem 0.75rem 0.75rem 0;
+ padding: 0 0 0 0.75rem;
+
+ @include until($tablet) {
+ background-color: $white;
+ bottom: 0;
+ left: -1.5rem;
+ margin: 0;
+ max-width: 300px;
+ padding-left: 0;
+ position: absolute;
+ right: $size-2;
+ transform: translateX(-100%);
+ transition: transform $speed;
+ top: -3rem;
+ z-index: 5;
+ }
+
+ &.is-active {
+ @include until($tablet) {
+ transform: translateX(0);
+ }
+ }
+
+ .menu-toggle {
+ color: $blue;
+ cursor: pointer;
+ display: none;
+
+ @include until($tablet) {
+ display: block;
+ margin-left: $size-8;
+ position: absolute;
+ top: 4.2rem;
+ left: 100%;
+ }
+
+ .button {
+ min-width: 0;
+ }
+ }
+
+ .menu {
+ flex: 1;
+ padding-top: 5.25rem;
+ position: relative;
+
+ @include until($tablet) {
+ padding-top: $size-6;
+ }
+ }
+
+ .menu-label {
+ color: $grey-light;
+ font-size: $size-small;
+ line-height: 1;
+ margin-bottom: $size-8;
+ padding-left: $size-5;
+ }
+
+ .menu-list {
+ border-top: $base-border;
+ padding: $size-9 0;
+
+ a {
+ color: $grey-dark;
+ padding-left: $size-5;
+ transition: 250ms border-width;
+
+ &.is-active {
+ color: $blue;
+ background-color: $dark-white;
+ border-right: 4px solid $blue;
+ }
+
+ &:hover {
+ background-color: $dark-white;
+ }
+ }
+ }
+}
diff --git a/ui/app/styles/components/status-menu.scss b/ui/app/styles/components/status-menu.scss
new file mode 100644
index 000000000..c299bcd51
--- /dev/null
+++ b/ui/app/styles/components/status-menu.scss
@@ -0,0 +1,104 @@
+.status-menu-content {
+ max-width: 360px;
+ min-width: 280px;
+ border-radius: 3px;
+ margin: 0 -17px 0 0;
+ will-change: transform, opacity;
+
+ .card {
+ position: relative;
+ border-radius: 2px;
+ border: $base-border;
+ box-shadow: 0 0 4px rgba($black, 0.21);
+ color: $black;
+ @include css-top-arrow(8px, $grey-lighter, 1px, $grey, 100%, -25px);
+ .card-content {
+ padding: $size-6 $size-3;
+ }
+ }
+
+ .card-header-title,
+ .menu-label {
+ background: $grey-lighter;
+ color: $grey-dark;
+ font-size: $size-7;
+ font-weight: normal;
+ letter-spacing: 0;
+ margin: 0;
+ padding: 8px $size-3;
+ text-transform: uppercase;
+ }
+
+ .replication-card {
+ @include css-top-arrow(8px, $grey-lighter, 1px, $grey, 100%, -98px);
+ .box-label {
+ box-shadow: none;
+ }
+ a.is-active,
+ a:hover {
+ box-shadow: 0 0 0 1px $blue;
+ }
+ }
+
+ &.ember-basic-dropdown-content--below.ember-basic-dropdown--transitioning-in {
+ animation: drop-fade-above .15s;
+ }
+ &.ember-basic-dropdown-content--below.ember-basic-dropdown--transitioning-out {
+ animation: drop-fade-above .15s reverse;
+ }
+ &.ember-basic-dropdown-content--above.ember-basic-dropdown--transitioning-in {
+ animation: drop-fade-below .15s;
+ }
+ &.ember-basic-dropdown-content--above.ember-basic-dropdown--transitioning-out {
+ animation: drop-fade-below .15s reverse;
+ }
+}
+.status-menu-content-replication {
+ margin-top: 5px;
+}
+
+.is-status-chevron {
+ line-height: 0;
+ padding: 0.25em 0 0.25em 0.25em;
+}
+
+.status-menu-user-trigger {
+ height: 1.25em;
+ width: 1.25em;
+ background: $white-bis;
+ box-shadow: 0 0 0 2px $dark-grey, 0 0 0 3px $grey-light;
+ margin-right: 0.25rem;
+
+ .icon {
+ min-width: 0;
+ }
+}
+
+.status-menu-content .menu {
+ button.link,
+ a {
+ font-size: $size-6;
+ font-weight: 500;
+ padding: $size-9 $size-3;
+ border-radius: $menu-item-radius;
+ color: $menu-item-color;
+ display: block;
+ padding: $size-9 $size-3;
+ box-shadow: none;
+ border: none;
+ background: transparent;
+ height: auto;
+ width: 100%;
+ text-align: left;
+
+ &:hover {
+ background-color: $menu-item-hover-background-color;
+ color: $menu-item-hover-color;
+ }
+
+ &.is-active {
+ background-color: $menu-item-active-background-color;
+ color: $menu-item-active-color;
+ }
+ }
+}
diff --git a/ui/app/styles/components/sub-nav.scss b/ui/app/styles/components/sub-nav.scss
new file mode 100644
index 000000000..5c2f4447e
--- /dev/null
+++ b/ui/app/styles/components/sub-nav.scss
@@ -0,0 +1,31 @@
+.sub-nav {
+ &.tabs {
+ background: $grey-lighter;
+ padding: 0 1.25rem;
+ ul {
+ border-color: transparent;
+ }
+ a {
+ color: $grey-dark;
+ font-weight: $font-weight-semibold;
+ text-decoration: none;
+ padding: 1.5rem 1rem;
+ border-bottom: 2px solid transparent;
+ transition: border-color $speed;
+ }
+ a:hover,
+ a:active {
+ border-color: $grey-light;
+ }
+ li:focus {
+ box-shadow: none;
+ }
+ li.is-active a {
+ border-color: $blue;
+ color: $blue;
+ }
+ .ember-basic-dropdown-trigger {
+ outline: none;
+ }
+ }
+}
diff --git a/ui/app/styles/components/tool-tip.scss b/ui/app/styles/components/tool-tip.scss
new file mode 100644
index 000000000..a971e6e9b
--- /dev/null
+++ b/ui/app/styles/components/tool-tip.scss
@@ -0,0 +1,47 @@
+.tool-tip {
+ font-size: $size-7;
+ text-transform: none;
+ margin: 8px 0px 0 -4px;
+ .box {
+ position: relative;
+ color: $white;
+ width: 200px;
+ background: $grey;
+ padding: 0.5rem;
+ line-height: 1.4;
+ }
+ @include css-top-arrow(8px, $grey, 1px, $grey-dark, 20px);
+ &.ember-basic-dropdown-content--below.ember-basic-dropdown--transitioning-in {
+ animation: drop-fade-above .15s;
+ }
+ &.ember-basic-dropdown-content--below.ember-basic-dropdown--transitioning-out {
+ animation: drop-fade-above .15s reverse;
+ }
+ &.ember-basic-dropdown-content--above.ember-basic-dropdown--transitioning-in {
+ animation: drop-fade-below .15s;
+ }
+ &.ember-basic-dropdown-content--above.ember-basic-dropdown--transitioning-out {
+ animation: drop-fade-below .15s reverse;
+ }
+}
+
+.ember-basic-dropdown-content--left.tool-tip {
+ margin: 8px 0 0 -11px;
+}
+.tool-tip-trigger {
+ border: none;
+ border-radius: 20px;
+ height: 18px;
+ width: 18px;
+ outline: none;
+ box-shadow: none;
+ cursor: pointer;
+ padding: 0;
+ olor: $grey-dark;
+ margin-left: 8px;
+}
+
+.b-checkbox .tool-tip-trigger {
+ position: relative;
+ top: -3px;
+}
diff --git a/ui/app/styles/components/upgrade-overlay.scss b/ui/app/styles/components/upgrade-overlay.scss
new file mode 100644
index 000000000..5ec71408e
--- /dev/null
+++ b/ui/app/styles/components/upgrade-overlay.scss
@@ -0,0 +1,59 @@
+.upgrade-overlay {
+ font-size: 1rem;
+ opacity: 0;
+ text-align: left;
+ transition: opacity $speed-slow;
+ will-change: opacity;
+
+ &.is-animated {
+ opacity: 1;
+ }
+
+ .modal-background {
+ background-image: url("/ui/vault-hex.svg"), linear-gradient(90deg, #191A1C, #1B212D);
+ opacity: 0.97;
+ }
+
+ .modal-content {
+ overflow: auto;
+ overflow-x: hidden;
+ transform: translateY(20%) scale(0.9);
+ transition: transform $speed-slow;
+ will-change: transform;
+ }
+
+ &.is-animated {
+ .modal-content {
+ transform: translateY(0) scale(1);
+ }
+ }
+
+ .upgrade-overlay-title {
+ border-bottom: 1px solid $grey;
+ padding-bottom: $size-10;
+ }
+
+ .upgrade-overlay-icon svg {
+ height: 24px;
+ width: auto;
+ }
+
+ .columns {
+ margin-bottom: $size-4;
+ margin-top: $size-4;
+ }
+
+ .column {
+ display: flex;
+
+ .box {
+ border-radius: $radius;
+ box-shadow: inset 0 0 0 1px $grey;
+ width: 100%;
+ }
+ }
+
+ li {
+ list-style: inside disc;
+ }
+}
diff --git a/ui/app/styles/core.scss b/ui/app/styles/core.scss
new file mode 100644
index 000000000..405ed24b4
--- /dev/null
+++ b/ui/app/styles/core.scss
@@ -0,0 +1,66 @@
+// Start with Bulma variables as a foundation
+@import "bulma/sass/utilities/initial-variables";
+
+// Override variables where appropriate
+@import "./utils/bulma_variables";
+
+// Utils
+@import "./utils/mixins";
+@import "./utils/animations";
+
+// Bring in the rest of Bulma
+@import "bulma/bulma";
+@import "bulma/switch";
+@import "bulma/bulma-radio-checkbox";
+
+// Override Bulma details where appropriate
+@import "./core/generic";
+@import "./core/box";
+@import "./core/breadcrumb";
+@import "./core/bulma-radio-checkboxes";
+@import "./core/buttons";
+@import "./core/footer";
+@import "./core/forms";
+@import "./core/helpers";
+@import "./core/hero";
+@import "./core/level";
+@import "./core/menu";
+@import "./core/message";
+@import "./core/navbar";
+@import "./core/notification";
+@import "./core/progress";
+@import "./core/switch";
+@import "./core/tables";
+@import "./core/tabs";
+@import "./core/tags";
+@import "./core/title";
+
+// bulma additions
+@import "./core/gradients";
+@import "./core/layout";
+@import "./core/lists";
+
+@import "./components/auth-form";
+@import "./components/badge";
+@import "./components/b64-toggle";
+@import "./components/box-label";
+@import "./components/codemirror";
+@import "./components/confirm";
+@import "./components/form-section";
+@import "./components/global-flash";
+@import "./components/init-illustration";
+@import "./components/info-table-row";
+@import "./components/input-hint";
+@import "./components/linked-block";
+@import "./components/list-pagination";
+@import "./components/loader";
+@import "./components/message-in-page";
+@import "./components/page-header";
+@import "./components/popup-menu";
+@import "./components/role-item";
+@import "./components/shamir-progress";
+@import "./components/sidebar";
+@import "./components/status-menu";
+@import "./components/sub-nav";
+@import "./components/tool-tip";
+@import "./components/upgrade-overlay";
diff --git a/ui/app/styles/core/box.scss b/ui/app/styles/core/box.scss
new file mode 100644
index 000000000..14cc4f45a
--- /dev/null
+++ b/ui/app/styles/core/box.scss
@@ -0,0 +1,23 @@
+.box {
+ box-shadow: 0 0 0 1px rgba($grey-dark, 0.3);
+}
+.box.is-fullwidth {
+ padding-left: 0;
+ padding-right: 0;
+}
+.box.no-padding-bottom {
+ padding-bottom: 0;
+}
+.box.has-slim-padding {
+ padding: 9px 0;
+}
+.box.has-glow {
+ box-shadow: 0 2px 4px 0 rgba($grey-dark, 0.5);
+}
+.box.is-rounded {
+ border-radius: 3px;
+}
+
+.box.no-top-shadow {
+ box-shadow: inset 0 -1px 0 0 rgba($black, 0.1);
+}
diff --git a/ui/app/styles/core/breadcrumb.scss b/ui/app/styles/core/breadcrumb.scss
new file mode 100644
index 000000000..fde1d5906
--- /dev/null
+++ b/ui/app/styles/core/breadcrumb.scss
@@ -0,0 +1,56 @@
+.breadcrumb {
+ -ms-user-select: text;
+ -webkit-user-select: text;
+ user-select: text;
+ height: 1.5rem;
+ margin: 0;
+ overflow-x: auto;
+
+ &:not(:last-child) {
+ margin: 0;
+ }
+
+ .is-sidebar + .column & {
+ @include until($tablet) {
+ margin-left: $size-2;
+ }
+ }
+
+ li {
+ & + li::before {
+ display: none;
+ }
+
+ &:first-child {
+ .sep {
+ margin-left: 0;
+ }
+ }
+ }
+
+ a {
+ line-height: 1;
+ padding: 0 $size-11 0 0;
+ text-decoration: none;
+
+ &:hover {
+ color: $blue;
+ }
+ }
+
+ .sep {
+ display: inline-block;
+ color: transparent;
+ margin: 0.15rem 0.4rem 0 0.5rem;
+ overflow: hidden;
+ width: 0.5rem;
+
+ &::before {
+ color: $blue;
+ content: "❮";
+ font-size: 1rem;
+ line-height: 1;
+ opacity: 0.33;
+ }
+ }
+}
diff --git a/ui/app/styles/core/bulma-radio-checkboxes.scss b/ui/app/styles/core/bulma-radio-checkboxes.scss
new file mode 100644
index 000000000..8d7f944cb
--- /dev/null
+++ b/ui/app/styles/core/bulma-radio-checkboxes.scss
@@ -0,0 +1,15 @@
+.b-checkbox input[type="checkbox"]:checked + label::before {
+ border-color: $blue;
+}
+
+.b-checkbox input[type="checkbox"]:checked + label::after,
+.b-checkbox input[type="radio"]:checked + label::after {
+ font-family: $family-monospace;
+ /*checkmark from ionicons*/
+ content: url('data:image/svg+xml; utf8, ');
+}
+
+.b-checkbox.no-label input[type="checkbox"] + label {
+ position: absolute;
+ top: 0;
+}
diff --git a/ui/app/styles/core/buttons.scss b/ui/app/styles/core/buttons.scss
new file mode 100644
index 000000000..ef23caf78
--- /dev/null
+++ b/ui/app/styles/core/buttons.scss
@@ -0,0 +1,187 @@
+$button-box-shadow-standard: 0 3px 1px 0 rgba($black, 0.12);
+
+.button {
+ box-shadow: $button-box-shadow-standard;
+ border: 1px solid $grey-light;
+ color: $grey-dark;
+ display: inline-block;
+ font-size: $size-small;
+ font-weight: $font-weight-bold;
+ height: 2.5rem;
+ line-height: 1.6;
+ min-width: 6rem;
+ padding: $size-10 $size-8;
+ text-decoration: none;
+ transition: background-color $speed, border-color $speed, box-shadow $speed, color $speed;
+ vertical-align: middle;
+
+ &.is-icon {
+ padding: 0.25rem $size-3;
+ }
+
+ &:active,
+ &.is-active,
+ &:focus,
+ &.is-focused {
+ border-color: darken($grey-light, 10%);
+ box-shadow: $button-box-shadow-standard;
+ color: darken($grey-dark, 10%);
+ }
+
+ &.is-inverted.is-outlined {
+ box-shadow: none;
+ }
+
+ &.is-transparent {
+ color: currentColor;
+ background: none;
+ border: none;
+ box-shadow: none;
+ min-width: auto;
+ padding: 0;
+ }
+
+ @each $name, $pair in $colors {
+ $color: nth($pair, 1);
+ @if $name == "primary" {
+ $color: $blue;
+ }
+ $color-invert: nth($pair, 2);
+
+ &.is-#{$name} {
+ border-color: $color;
+ background-color: $color;
+ color: $color-invert;
+
+ &:hover,
+ &.is-hovered {
+ background-color: darken($color, 5%);
+ border-color: darken($color, 5%);
+ }
+
+ &:active,
+ &.is-active {
+ background-color: darken($color, 10%);
+ border-color: darken($color, 10%);
+ box-shadow: none;
+ }
+
+ &:focus,
+ &.is-focused {
+ border-color: darken($color, 10%);
+ box-shadow: $button-box-shadow-standard;
+ }
+
+ &.is-outlined {
+ border-color: $color;
+ color: $color;
+ background-color: transparent;
+
+ &.is-important {
+ border-color: $color;
+ }
+
+ &:hover,
+ &.is-hovered,
+ &:focus,
+ &.is-focused {
+ background-color: transparent;
+ border-color: darken($color, 10%);
+ color: $color;
+ }
+
+ &:active,
+ &.is-active {
+ background-color: transparent;
+ border-color: darken($color, 10%);
+ color: darken($color, 10%);
+ }
+ }
+
+ &.is-inverted.is-outlined {
+ border-color: rgba($color-invert, 0.5);
+ color: rgba($color-invert, 0.9);
+
+ &:hover,
+ &.is-hovered,
+ &:focus,
+ &.is-focused {
+ background-color: transparent;
+ border-color: $color-invert;
+ color: $color-invert;
+ }
+
+ &:active,
+ &.is-active {
+ background-color: rgba($color-invert, 0.2);
+ border-color: $color-invert;
+ color: $color-invert;
+ box-shadow: none;
+ }
+ }
+ }
+ }
+
+ &.is-ghost {
+ background-color: transparent;
+ border-color: transparent;
+ box-shadow: none;
+ color: $blue;
+
+ &:hover {
+ background-color: $grey-lighter;
+ }
+ }
+
+ &.is-compact {
+ height: 2rem;
+ padding: $size-11 $size-8;
+ }
+
+ .has-text-info & {
+ font-weight: $font-weight-semibold;
+
+ .icon {
+ vertical-align: middle;
+ }
+ }
+
+ &.is-more-icon,
+ &.tool-tip-trigger {
+ color: $black;
+ min-width: auto;
+ }
+
+ &.has-icon-left,
+ &.has-icon-right {
+ .icon {
+ height: 16px;
+ min-width: auto;
+ width: 16px;
+ }
+ }
+
+ &.has-icon-left {
+ .icon {
+ &,
+ &:first-child:last-child {
+ margin-left: -$size-10;
+ }
+ }
+ }
+
+ &.has-icon-right {
+ .icon {
+ &,
+ &:first-child:last-child {
+ margin-left: $size-11;
+ margin-right: -$size-10;
+ }
+ }
+ }
+}
+
+.button .icon.auto-width {
+ width: auto;
+ margin: 0 !important;
+}
diff --git a/ui/app/styles/core/footer.scss b/ui/app/styles/core/footer.scss
new file mode 100644
index 000000000..b176a8366
--- /dev/null
+++ b/ui/app/styles/core/footer.scss
@@ -0,0 +1,9 @@
+.footer {
+ border-top: $base-border;
+ padding: $size-3 1.5rem;
+
+ span:not(:first-child) {
+ display: inline-block;
+ padding: 0 0.5rem;
+ }
+}
diff --git a/ui/app/styles/core/forms.scss b/ui/app/styles/core/forms.scss
new file mode 100644
index 000000000..7fa28d5fd
--- /dev/null
+++ b/ui/app/styles/core/forms.scss
@@ -0,0 +1,272 @@
+label {
+ cursor: pointer;
+ &.is-select-list {
+ padding: 10px 0px;
+ &:hover {
+ color: $blue;
+ }
+ }
+}
+
+.label {
+ color: $grey-darker;
+ text-transform: uppercase;
+ font-size: $size-small;
+}
+
+.is-label,
+.b-checkbox .is-label {
+ color: $grey-darker;
+ display: inline-block;
+ font-size: $size-small;
+ font-weight: $font-weight-bold;
+
+ &:not(:last-child) {
+ margin-bottom: 0.25rem;
+ }
+ // Sizes
+ &.is-small {
+ font-size: $size-small;
+ }
+ &.is-medium {
+ font-size: $size-medium;
+ }
+ &.is-large {
+ font-size: $size-large;
+ }
+
+ &::before,
+ &::after {
+ transform: translateY(0.2em);
+ }
+
+ &::before {
+ border-color: $grey-light;
+ }
+}
+
+.b-checkbox .is-label {
+ display: inline;
+ margin-left: $size-10;
+}
+
+.help {
+ &.is-danger {
+ font-weight: $weight-bold;
+ }
+}
+
+.is-help {
+ font-size: $size-small;
+ margin-top: 0.25rem;
+ @each $name, $pair in $colors {
+ $color: nth($pair, 1);
+ &.is-#{$name} {
+ color: $color;
+ }
+ }
+}
+
+.input,
+.textarea,
+.select select {
+ @include until($desktop) {
+ font-size: 16px;
+ }
+ &::placeholder {
+ opacity: 0.5;
+ }
+ border-color: $grey-light;
+ color: $grey-dark;
+ padding-left: $size-8;
+ padding-right: $size-8;
+
+ .has-background-grey-lighter & {
+ background-color: $white;
+ }
+}
+
+.input,
+.select select,
+.control.has-icons-left .icon,
+.control.has-icons-right .icon {
+ height: 2.5rem;
+}
+
+.input.variable {
+ font-family: $family-monospace;
+}
+.input[disabled],
+.textarea[disabled] {
+ border-color: $grey-lighter;
+ background-color: $white-ter;
+ color: $grey-light;
+}
+
+.control {
+ max-width: 100%;
+
+ // Modifiers
+ &.has-icons-left,
+ &.has-icons-right {
+ .input,
+ .select select {
+ padding-left: 2rem;
+
+ &:focus,
+ &.is-focused,
+ &:active,
+ &.is-active {
+ & ~ .icon {
+ color: currentColor;
+ }
+ }
+ }
+ }
+ &.has-checkbox-right {
+ label.checkbox {
+ display: inline-flex;
+ height: 2.5em;
+ position: absolute;
+ top: 0;
+ right: 0;
+ justify-content: flex-end;
+ margin-left: auto;
+ }
+ .input,
+ .select select {
+ padding-right: 10em;
+ }
+ }
+}
+
+.input,
+.textarea {
+ box-shadow: none;
+
+ @each $name, $pair in $colors {
+ $color: nth($pair, 1);
+ $color-invert: findColorInvert($color);
+ $color-bg: nth($pair, 2);
+ &.is-#{$name} {
+ border-color: $color;
+ background-color: $color-bg;
+ &.is-inverted {
+ border-color: rgba($color-invert, 0.3);
+ background-color: $color-bg;
+ &::placeholder {
+ color: rgba($color-invert, 0.3);
+ }
+ &:focus,
+ &.is-focused,
+ &:active,
+ &.is-active {
+ border-color: rgba($color-invert, 1);
+ background-color: $color-bg;
+ color: $color-invert;
+ &::placeholder {
+ color: rgba($color-invert, 0.7);
+ }
+ }
+ }
+ }
+ }
+ &:focus,
+ &.is-focused,
+ &:active,
+ &.is-active {
+ background-color: $input-focus-background-color;
+ }
+}
+
+.select {
+ &:not(.is-multiple)::after {
+ border-width: 2px;
+ margin-top: 0;
+ transform: rotate(-45deg) translateY(-50%);
+ }
+}
+
+.field:not(:last-child) {
+ margin-bottom: 1.5rem;
+}
+.field-body .field {
+ margin-bottom: 0;
+}
+.field.has-addons {
+ flex-wrap: wrap;
+ .control {
+ .button,
+ .checkbox,
+ .input,
+ .select select {
+ border-radius: 0;
+ &:hover,
+ &.is-hovered {
+ z-index: 2;
+ }
+ &:focus,
+ &.is-focused,
+ &:active,
+ &.is-active {
+ z-index: 3;
+ &:hover {
+ z-index: 4;
+ }
+ }
+ }
+ &:first-of-type {
+ flex-grow: 1;
+ .button,
+ .checkbox,
+ .input,
+ .select select {
+ border-bottom-left-radius: $input-radius;
+ border-top-left-radius: $input-radius;
+ }
+ }
+ &:last-child {
+ .button,
+ .checkbox,
+ .input,
+ .select select {
+ border-bottom-right-radius: $input-radius;
+ border-top-right-radius: $input-radius;
+ }
+ }
+ }
+ & > .label {
+ flex-grow: 1;
+ flex-shrink: 0;
+ width: 100%;
+ }
+ .checkbox {
+ @include input;
+ background-color: $dark-white;
+ @each $name, $pair in $colors {
+ $color: nth($pair, 1);
+ &.is-#{$name} {
+ border-color: $color;
+ }
+ }
+ input {
+ margin-right: 0.5em;
+ }
+ }
+}
+
+.file-icon,
+.file-label {
+ color: $grey-dark;
+}
+.file-cta {
+ background: $white-bis;
+ position: relative;
+}
+.file-delete-button {
+ @extend .button;
+ @extend .is-transparent;
+ color: $grey;
+ position: absolute;
+ right: 5px;
+}
diff --git a/ui/app/styles/core/generic.scss b/ui/app/styles/core/generic.scss
new file mode 100644
index 000000000..358b6dd32
--- /dev/null
+++ b/ui/app/styles/core/generic.scss
@@ -0,0 +1,45 @@
+// seriously ? yes. seriously.
+a {
+ text-decoration: underline;
+ &:hover,
+ &:active,
+ &:focus {
+ position: relative;
+ }
+}
+.menu a {
+ text-decoration: none;
+}
+code {
+ padding: 0;
+ font-size: 1em;
+}
+.icon {
+ min-width: 1.5rem;
+ vertical-align: middle;
+}
+code,
+pre {
+ font-smoothing: inherit;
+ -webkit-font-smoothing: inherit;
+}
+
+input::-webkit-outer-spin-button,
+input::-webkit-inner-spin-button {
+ -webkit-appearance: none;
+ margin: 0;
+}
+
+.link {
+ background: transparent;
+ border: 0;
+ color: $blue;
+ cursor: pointer;
+ display: inline;
+ font: inherit;
+ line-height: normal;
+ margin: 0;
+ padding: 0;
+ text-decoration: underline;
+ -moz-user-select: text;
+}
diff --git a/ui/app/styles/core/gradients.scss b/ui/app/styles/core/gradients.scss
new file mode 100644
index 000000000..56b409463
--- /dev/null
+++ b/ui/app/styles/core/gradients.scss
@@ -0,0 +1,3 @@
+.has-dark-grey-gradient {
+ background: linear-gradient(to right, darken($grey-dark, 25%), $grey-dark);
+}
diff --git a/ui/app/styles/core/helpers.scss b/ui/app/styles/core/helpers.scss
new file mode 100644
index 000000000..57d7259f9
--- /dev/null
+++ b/ui/app/styles/core/helpers.scss
@@ -0,0 +1,134 @@
+.is-invisible {
+ visibility: hidden;
+}
+.is-underline {
+ text-decoration: underline;
+}
+.is-sideless {
+ box-shadow: 0 2px 0 -1px $grey-light, 0 -2px 0 -1px $grey-light;
+}
+.is-bottomless {
+ box-shadow: 0 -1px 0 0 $grey-light;
+}
+.is-borderless {
+ border: none !important;
+}
+.is-relative {
+ position: relative;
+}
+.is-fullwidth {
+ width: 100%;
+}
+.is-in-bottom-right {
+ position: absolute;
+ bottom: 1rem;
+ right: 1rem;
+ z-index: 10;
+}
+
+.has-background-transition {
+ transition: background-color $easing $speed;
+}
+.is-flex-column {
+ display: flex;
+ flex-direction: column;
+}
+.is-flex-v-centered {
+ display: flex;
+ align-items: center;
+ align-self: center;
+ justify-content: center;
+}
+.is-flex-v-centered-tablet {
+ @include tablet {
+ display: flex;
+ align-items: center;
+ align-self: center;
+ justify-content: center;
+ }
+}
+.is-flex-center {
+ display: flex;
+ align-items: center;
+}
+.is-flex-1 {
+ flex: 1;
+}
+.is-flex-wrap {
+ flex-flow: row wrap;
+}
+.is-flex-end {
+ display: flex !important;
+ justify-content: flex-end;
+}
+.is-flex-full {
+ flex-basis: 100%;
+}
+.is-no-flex-grow {
+ flex-grow: 0 !important;
+}
+.is-auto-width {
+ width: auto;
+}
+
+.is-flex-between,
+.is-grouped-split {
+ display: flex;
+ justify-content: space-between !important;
+}
+.has-default-border {
+ border: 1px solid $grey !important;
+}
+.has-no-pointer {
+ pointer-events: none;
+}
+.has-pointer {
+ cursor: pointer;
+}
+.has-short-padding {
+ padding: 0.25rem 1.25rem;
+}
+
+.is-sideless.has-short-padding {
+ padding: 0.25rem 1.25rem;
+}
+.has-current-color-fill {
+ &,
+ & svg {
+ fill: currentColor;
+ }
+}
+.is-word-break {
+ overflow-wrap: break-word;
+ word-wrap: break-word;
+ word-break: break-word;
+ white-space: pre-wrap;
+}
+.is-font-mono {
+ font-family: $family-monospace;
+}
+.is-size-8 {
+ font-size: $size-8 !important;
+}
+.is-size-9 {
+ font-size: $size-9 !important;
+}
+@each $name, $shade in $shades {
+ .has-background-#{$name} {
+ background: $shade !important;
+ }
+}
+.has-background-transparent {
+ background: transparent !important;
+}
+@each $name, $pair in $colors {
+ $color: nth($pair, 1);
+ .has-background-#{$name} {
+ background: $color !important;
+ }
+}
+.is-optional {
+ color: $grey;
+ font-size: $size-8;
+ text-transform: lowercase;
+}
diff --git a/ui/app/styles/core/hero.scss b/ui/app/styles/core/hero.scss
new file mode 100644
index 000000000..bf040a64f
--- /dev/null
+++ b/ui/app/styles/core/hero.scss
@@ -0,0 +1,7 @@
+.hero-body {
+ padding: 3rem 2rem 1rem;
+
+ svg {
+ margin-bottom: $size-10;
+ }
+}
diff --git a/ui/app/styles/core/layout.scss b/ui/app/styles/core/layout.scss
new file mode 100644
index 000000000..49b099327
--- /dev/null
+++ b/ui/app/styles/core/layout.scss
@@ -0,0 +1,29 @@
+.ember-application > .ember-view {
+ display: flex;
+ flex-direction: column;
+}
+.page-container {
+ min-height: 100vh;
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-end;
+}
+
+.section {
+ display: flex;
+ flex-grow: 1;
+ flex-direction: column;
+ padding-top: 0;
+ padding-bottom: 0;
+
+ > .container {
+ display: flex;
+ flex-grow: 1;
+ flex-direction: column;
+ width: 100%;
+
+ > .columns {
+ flex-grow: 1;
+ }
+ }
+}
diff --git a/ui/app/styles/core/level.scss b/ui/app/styles/core/level.scss
new file mode 100644
index 000000000..ffe88fb49
--- /dev/null
+++ b/ui/app/styles/core/level.scss
@@ -0,0 +1,15 @@
+.level:not(:last-child) {
+ @include vault-block;
+}
+
+.level.has-padded-items {
+ .level-item {
+ flex: 0 1 auto;
+ }
+ .level-item {
+ padding-right: 1.5rem;
+ }
+ .level-item.is-fixed-25 {
+ flex-basis: 25%;
+ }
+}
diff --git a/ui/app/styles/core/lists.scss b/ui/app/styles/core/lists.scss
new file mode 100644
index 000000000..26e0da6a9
--- /dev/null
+++ b/ui/app/styles/core/lists.scss
@@ -0,0 +1,10 @@
+.sep {
+ display: inline-flex;
+ align-items: center;
+ &:before {
+ font-size: $size-5;
+ color: $white-ter;
+ content: '|';
+ position: relative;
+ }
+}
diff --git a/ui/app/styles/core/menu.scss b/ui/app/styles/core/menu.scss
new file mode 100644
index 000000000..81fb5de04
--- /dev/null
+++ b/ui/app/styles/core/menu.scss
@@ -0,0 +1,11 @@
+.column .menu-list a {
+ border-radius: 0;
+ border-right: 0 solid transparent;
+ font-weight: $font-weight-semibold;
+
+ &:hover,
+ &.is-active {
+ color: $menu-item-hover-background-color;
+ background-color: $menu-item-active-color;
+ }
+}
diff --git a/ui/app/styles/core/message.scss b/ui/app/styles/core/message.scss
new file mode 100644
index 000000000..f9007a535
--- /dev/null
+++ b/ui/app/styles/core/message.scss
@@ -0,0 +1,38 @@
+.message {
+ &.is-list {
+ margin: $size-10 0;
+ }
+
+ &.is-warning {
+ .message-body {
+ color: $dark-yellow;
+ padding: 0.75rem 1.25rem;
+ }
+ }
+
+ &.is-highlight {
+ background: $light-yellow;
+ .message-body {
+ border: none;
+ box-shadow: 0 0 0 1px $orange;
+ color: $dark-yellow;
+ }
+ .has-text-highlight,
+ .close-button {
+ color: $orange;
+ }
+ .title,
+ code {
+ background: none;
+ color: inherit;
+ }
+ .content .button {
+ border-color: $orange;
+ color: $dark-yellow;
+ }
+ }
+
+ .content {
+ margin-bottom: 0;
+ }
+}
diff --git a/ui/app/styles/core/navbar.scss b/ui/app/styles/core/navbar.scss
new file mode 100644
index 000000000..b750221ae
--- /dev/null
+++ b/ui/app/styles/core/navbar.scss
@@ -0,0 +1,41 @@
+.navbar {
+ align-items: center;
+ height: 3.5rem;
+}
+
+.navbar-end {
+ .navbar-item {
+ padding: 0.5em 0.75em;
+
+ .button {
+ display: inline-flex;
+ min-width: 2.5rem;
+ }
+ }
+}
+
+a.navbar-item {
+ color: $white-ter;
+ &:hover,
+ &:active {
+ color: $white;
+ background: transparent;
+ }
+}
+
+.navbar-brand {
+ margin: 0 0 0 $size-10;
+}
+
+.navbar-sections {
+ justify-content: flex-start;
+ background-color: $grey;
+
+ @include from($tablet) {
+ background-color: transparent;
+ position: absolute;
+ left: 3.5em;
+ top: 0;
+ height: 3.25rem;
+ }
+}
diff --git a/ui/app/styles/core/notification.scss b/ui/app/styles/core/notification.scss
new file mode 100644
index 000000000..44447c8cc
--- /dev/null
+++ b/ui/app/styles/core/notification.scss
@@ -0,0 +1,58 @@
+.notification {
+ padding: 1rem 2.5rem 1rem 1.5rem;
+ .title {
+ font-weight: $weight-bold;
+ }
+ &.has-border {
+ border: 1px solid currentColor;
+ border-left-width: 10px;
+ }
+
+ @each $name, $pair in $colors {
+ $color: nth($pair, 1);
+ $color-invert: nth($pair, 2);
+ $color-lightning: max((100% - lightness($color)) - 2%, 0%);
+ $color-luminance: colorLuminance($color);
+ $darken-percentage: $color-luminance * 70%;
+ $desaturate-percentage: $color-luminance * 30%;
+ &.is-#{$name} {
+ background-color: lighten($color, $color-lightning);
+ border-color: $color;
+ color: desaturate(
+ darken($color, $darken-percentage),
+ $desaturate-percentage
+ );
+ .delete {
+ color: $color;
+ }
+ .title {
+ color: $color-invert;
+ margin-bottom: .5rem;
+ }
+ }
+ }
+
+ &.is-warning {
+ .title,
+ .delete {
+ color: $dark-yellow;
+ }
+ border-color: $orange;
+ color: $dark-yellow;
+ }
+
+ & > .delete {
+ &:before,
+ &:after {
+ content: none;
+ }
+ position: absolute;
+ background-color: transparent;
+ border: none;
+ color: currentColor;
+ right: 0.5rem;
+ top: 0.5rem;
+ height: 1.5rem;
+ width: 1.5rem;
+ }
+}
diff --git a/ui/app/styles/core/progress.scss b/ui/app/styles/core/progress.scss
new file mode 100644
index 000000000..3454e73bd
--- /dev/null
+++ b/ui/app/styles/core/progress.scss
@@ -0,0 +1,20 @@
+.progress[value]::-webkit-progress-bar,
+.progress[value]::-webkit-progress-value {
+ border-radius: 2px;
+}
+.progress {
+ border-radius: 0;
+ margin-bottom: 0;
+ &.is-small {
+ height: 0.5rem;
+ }
+ &.is-narrow {
+ width: 30px;
+ }
+}
+.progress.is-rounded {
+ border-radius: 2px;
+}
+.progress.is-bordered {
+ box-shadow: 0 0 0 4px $progress-bar-background-color;
+}
diff --git a/ui/app/styles/core/switch.scss b/ui/app/styles/core/switch.scss
new file mode 100644
index 000000000..12e30d4a8
--- /dev/null
+++ b/ui/app/styles/core/switch.scss
@@ -0,0 +1,27 @@
+.switch[type="checkbox"] {
+ &.is-small {
+ + label {
+ font-size: $size-9;
+ font-weight: bold;
+ padding-left: $size-9 * 2.5;
+ margin: 0 0.25rem;
+ &::before {
+ top: $size-9 / 5;
+ height: $size-9;
+ width: $size-9 * 2;
+ }
+ &::after {
+ width: $size-9 * 0.68;
+ height: $size-9 * 0.68;
+ left: .15rem;
+ top: $size-9 / 2.5;
+ }
+ }
+ &:checked + label::after {
+ left: ($size-9 * 2) - ($size-9 * 0.9);
+ }
+ }
+}
+.switch[type="checkbox"]:focus + label {
+ box-shadow: 0 0 1px $blue;
+}
diff --git a/ui/app/styles/core/tables.scss b/ui/app/styles/core/tables.scss
new file mode 100644
index 000000000..d43e5ef9f
--- /dev/null
+++ b/ui/app/styles/core/tables.scss
@@ -0,0 +1,41 @@
+.table {
+ thead,
+ .thead {
+ background: $grey-lighter;
+ box-shadow: 0 1px 0 0 $grey-light, 0 -1px 0 0 $grey-light;
+
+ th,
+ .th {
+ text-transform: uppercase;
+ font-size: $size-8;
+ color: $grey-dark;
+ font-weight: normal;
+ padding: 0.5rem 1.5rem;
+ border-width: 0 0 1px 0;
+ border-color: $grey-light;
+ }
+ }
+
+ .thead {
+ box-shadow: 0 -1px 0 0 $grey-light;
+ }
+ td,
+ .td {
+ font-size: $size-6;
+ color: $black;
+ padding: 1rem 1.5rem;
+ border-width: 0 0 1px 0;
+ border-color: $grey-light;
+ border-style: solid;
+ }
+ tr:hover {
+ background: rgba($grey-lighter, .4);
+ }
+}
+.table tbody tr:last-child td,
+.table tbody tr:last-child th {
+ border-bottom-width: inherit;
+}
+.table.has-expanded-borders {
+ border-collapse: inherit;
+}
diff --git a/ui/app/styles/core/tabs.scss b/ui/app/styles/core/tabs.scss
new file mode 100644
index 000000000..27b5d1c1f
--- /dev/null
+++ b/ui/app/styles/core/tabs.scss
@@ -0,0 +1,48 @@
+.tabs.tabs-subnav {
+ align-items: center;
+ height: 3.5rem;
+ margin-bottom: 0;
+ padding: $size-10 $size-10;
+
+ @include until($tablet) {
+ background-color: $grey;
+ flex: 0 0 100%;
+ height: 3rem;
+ }
+
+ ul {
+ border-bottom: none;
+ }
+
+ a,
+ .link {
+ align-items: center;
+ border-bottom: none;
+ color: $white-ter;
+ cursor: pointer;
+ display: inline-flex;
+ font-weight: $font-weight-semibold;
+ line-height: 1;
+ padding: $size-10 $size-6;
+ text-decoration: none;
+ transition: color $speed, background-color $speed;
+
+ &:hover {
+ color: $white;
+ }
+ }
+ .sep:before {
+ position: relative;
+ left: -0.75em;
+ }
+
+ .is-active a,
+ .is-active .link {
+ color: $white;
+ background: rgba($black, 0.25);
+ border-radius: 4px;
+ @include until($tablet) {
+ background: rgba($grey-dark, 0.75);
+ }
+ }
+}
diff --git a/ui/app/styles/core/tags.scss b/ui/app/styles/core/tags.scss
new file mode 100644
index 000000000..99eb6be52
--- /dev/null
+++ b/ui/app/styles/core/tags.scss
@@ -0,0 +1,28 @@
+.tag:not(body) {
+ background-color: lighten($grey-light, 17%);
+ border-radius: 2px;
+ height: auto;
+ padding: 0 0.75em;
+ margin-right: 0.5rem;
+ font-weight: normal;
+ code {
+ color: $grey;
+ }
+}
+.tag.is-outlined {
+ border: 1px solid currentColor;
+}
+.tag.is-inverted {
+ border-color: $grey;
+ background: none;
+ code {
+ color: $grey-dark;
+ }
+}
+.tag.is-bold {
+ font-weight: bold;
+}
+
+.tag.is-small {
+ height: auto;
+}
diff --git a/ui/app/styles/core/title.scss b/ui/app/styles/core/title.scss
new file mode 100644
index 000000000..be110bb64
--- /dev/null
+++ b/ui/app/styles/core/title.scss
@@ -0,0 +1,13 @@
+.title:not(:last-child),
+.subtitle:not(:last-child) {
+ margin-bottom: 1rem;
+}
+
+.title {
+ font-weight: $font-weight-bold;
+
+ > a {
+ color: $black;
+ text-decoration: none;
+ }
+}
diff --git a/ui/app/styles/utils/_bulma_variables.scss b/ui/app/styles/utils/_bulma_variables.scss
new file mode 100644
index 000000000..5ee88490b
--- /dev/null
+++ b/ui/app/styles/utils/_bulma_variables.scss
@@ -0,0 +1,83 @@
+$vault-grey: #6a7786;
+// ui colors
+$grey-light: #bbc4d1;
+$grey-lighter: #f9f9f9;
+$grey: #929dab;
+$grey-dark: $vault-grey;
+$blue: #1563ff;
+
+//
+$orange: #f9bb2d;
+$light-orange: rgb(255, 254, 218);
+$light-yellow: #fffbee;
+$dark-yellow: #614903;
+$light-red: #fff5f8;
+$dark-red: #c84034;
+$light-blue: #e2eafa;
+$dark-blue: #1563ff;
+$light-blue: rgb(218, 234, 247);
+$dark-green: #36ae40;
+$light-green: rgb(244, 250, 236);
+$light-white: #f9f8fe;
+$dark-white: #f9f9f9;
+$white-ter: rgba($white, .5);
+$white-bis: $dark-white;
+
+// Primary colors
+$success: $dark-green;
+$danger: $dark-red;
+$warning: $light-yellow;
+$primary: $grey-dark;
+$code: $grey-dark;
+$code-background: transparent;
+$info: $dark-blue;
+$light: $grey-lighter;
+$border: $grey-light;
+
+$hr-margin: 1rem 0;
+
+//typography
+$family-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
+$family-primary: $family-sans;
+$body-size: 14px;
+$size-3: (24/14) + 0rem;
+$size-7: (13/14) + 0rem;
+$size-8: (12/14) + 0rem;
+$size-9: 0.75rem;
+$size-10: 0.5rem;
+$size-11: 0.25rem;
+$size-small: $size-8;
+$font-weight-normal: 400;
+$font-weight-semibold: 600;
+$font-weight-bold: 700;
+
+//input
+$input-background-color: $dark-white;
+$input-focus-background-color: $white;
+$input-border-color: $grey;
+
+$radius: 2px;
+
+//box
+$box-radius: 0;
+$box-shadow: 0 0 0 1px rgba($black, 0.1);
+
+$link: $blue;
+$text: $black;
+
+$breadcrumb-item-color: $blue;
+$breadcrumb-item-separator-color: rgba($blue, 0.5);
+$breadcrumb-item-active-color: $black;
+
+$navbar-background-color: transparent;
+
+$menu-item-hover-background-color: $blue;
+$menu-item-hover-color: $white;
+
+$progress-bar-background-color: lighten($grey-light, 15%);
+
+$base-border: 1px solid $grey-light;
+
+// animations
+$speed: 150ms;
+$speed-slow: $speed * 2;
diff --git a/ui/app/styles/utils/animations.scss b/ui/app/styles/utils/animations.scss
new file mode 100644
index 000000000..8c7c3b2d7
--- /dev/null
+++ b/ui/app/styles/utils/animations.scss
@@ -0,0 +1,36 @@
+@mixin keyframes($name) {
+ @-webkit-keyframes #{$name} {
+ @content;
+ }
+ @-moz-keyframes #{$name} {
+ @content;
+ }
+ @-ms-keyframes #{$name} {
+ @content;
+ }
+ @keyframes #{$name} {
+ @content;
+ }
+}
+
+@include keyframes(drop-fade-below) {
+ 0% {
+ opacity: 0;
+ transform: translateY(-1rem);
+ }
+ 100% {
+ opacity: 1;
+ transform: translateY(0px);
+ }
+}
+
+@include keyframes(drop-fade-above) {
+ 0% {
+ opacity: 0;
+ transform: translateY(1rem);
+ }
+ 100% {
+ opacity: 1;
+ transform: translateY(0px);
+ }
+}
diff --git a/ui/app/styles/utils/mixins.scss b/ui/app/styles/utils/mixins.scss
new file mode 100644
index 000000000..97f3cc012
--- /dev/null
+++ b/ui/app/styles/utils/mixins.scss
@@ -0,0 +1,45 @@
+@mixin css-top-arrow($size, $color, $border-width, $border-color, $left: 50%, $left-offset: 0px) {
+ & {
+ border: 1px solid $border-color;
+ }
+
+ &:after,
+ &:before {
+ bottom: 100%;
+ border: solid transparent;
+ content: " ";
+ height: 0;
+ width: 0;
+ position: absolute;
+ pointer-events: none;
+ }
+
+ &:after {
+ border-color: rgba($color, 0);
+ border-bottom-color: $color;
+ border-width: $size;
+ left: calc(#{$left} + #{$left-offset});
+ margin-left: -$size;
+ }
+ &:before {
+ border-color: rgba($border-color, 0);
+ border-bottom-color: $border-color;
+ border-width: $size + round(1.41421356 * $border-width);
+ left: calc(#{$left} + #{$left-offset});
+ margin-left: -($size + round(1.41421356 * $border-width));
+ }
+
+ @at-root .ember-basic-dropdown-content--left#{&} {
+ &:after,
+ &:before {
+ left: auto;
+ right: calc(#{$left} + #{$left-offset} - #{$size});
+ }
+ }
+}
+
+@mixin vault-block {
+ &:not(:last-child) {
+ margin-bottom: (5/14) + 0rem;
+ }
+}
diff --git a/ui/app/templates/application.hbs b/ui/app/templates/application.hbs
new file mode 100644
index 000000000..fb6884ce5
--- /dev/null
+++ b/ui/app/templates/application.hbs
@@ -0,0 +1,139 @@
+
+ {{#if showNav}}
+
+ {{/if}}
+
+ {{#each flashMessages.queue as |flash|}}
+ {{#flash-message data-test-flash-message=true flash=flash as |component flash close|}}
+ {{#if flash.componentName}}
+ {{component flash.componentName content=flash.content}}
+ {{else}}
+
+ {{get (message-types flash.type) "text"}}
+
+
+ {{flash.message}}
+
+
+ {{i-con excludeIconClass=true glyph="close" aria-label="Close"}}
+
+ {{/if}}
+ {{/flash-message}}
+ {{/each}}
+
+ {{#if showNav}}
+
+
+ {{component
+ (if
+ (or
+ (is-after (now interval=1000) auth.tokenExpirationDate)
+ (and activeClusterName auth.currentToken)
+ )
+ 'token-expire-warning'
+ null
+ )
+ }}
+ {{#unless (and
+ activeClusterName
+ auth.currentToken
+ (is-after (now interval=1000) auth.tokenExpirationDate)
+ )
+ }}
+ {{outlet}}
+ {{/unless}}
+
+
+ {{else}}
+ {{outlet}}
+ {{/if}}
+
+
+ {{#if (eq env "development") }}
+
+
+ {{i-con glyph="wand" class="type-icon"}}Local Development
+
+
+ {{/if}}
+
+
diff --git a/ui/app/templates/components/.gitkeep b/ui/app/templates/components/.gitkeep
new file mode 100644
index 000000000..e69de29bb
diff --git a/ui/app/templates/components/auth-config-form/config.hbs b/ui/app/templates/components/auth-config-form/config.hbs
new file mode 100644
index 000000000..9271bb65a
--- /dev/null
+++ b/ui/app/templates/components/auth-config-form/config.hbs
@@ -0,0 +1,17 @@
+
diff --git a/ui/app/templates/components/auth-config-form/options.hbs b/ui/app/templates/components/auth-config-form/options.hbs
new file mode 100644
index 000000000..cee9da999
--- /dev/null
+++ b/ui/app/templates/components/auth-config-form/options.hbs
@@ -0,0 +1,13 @@
+
diff --git a/ui/app/templates/components/auth-form.hbs b/ui/app/templates/components/auth-form.hbs
new file mode 100644
index 000000000..506a942ec
--- /dev/null
+++ b/ui/app/templates/components/auth-form.hbs
@@ -0,0 +1,45 @@
+
+
+
+
+ {{message-error errorMessage=error}}
+ {{component providerComponentName onSubmit=(action 'doSubmit') }}
+
+ {{#unless (eq selectedAuthBackend.type "token")}}
+ {{toggle-button toggleTarget=this toggleAttr="useCustomPath"}}
+
+ {{#if useCustomPath}}
+
+ Mount path
+
+
+
+
+
+ If this backend was mounted using a non-default path, enter it here.
+
+ {{/if}}
+
+ {{/unless}}
+
+
+
+
+ Sign In
+
+
diff --git a/ui/app/templates/components/auth-form/git-hub.hbs b/ui/app/templates/components/auth-form/git-hub.hbs
new file mode 100644
index 000000000..a87e98afc
--- /dev/null
+++ b/ui/app/templates/components/auth-form/git-hub.hbs
@@ -0,0 +1,17 @@
+
diff --git a/ui/app/templates/components/auth-form/ldap.hbs b/ui/app/templates/components/auth-form/ldap.hbs
new file mode 100644
index 000000000..1bcd301f2
--- /dev/null
+++ b/ui/app/templates/components/auth-form/ldap.hbs
@@ -0,0 +1 @@
+{{partial "partials/userpass-form"}}
diff --git a/ui/app/templates/components/auth-form/okta.hbs b/ui/app/templates/components/auth-form/okta.hbs
new file mode 100644
index 000000000..1bcd301f2
--- /dev/null
+++ b/ui/app/templates/components/auth-form/okta.hbs
@@ -0,0 +1 @@
+{{partial "partials/userpass-form"}}
diff --git a/ui/app/templates/components/auth-form/token.hbs b/ui/app/templates/components/auth-form/token.hbs
new file mode 100644
index 000000000..7f89628f9
--- /dev/null
+++ b/ui/app/templates/components/auth-form/token.hbs
@@ -0,0 +1,17 @@
+
diff --git a/ui/app/templates/components/auth-form/userpass.hbs b/ui/app/templates/components/auth-form/userpass.hbs
new file mode 100644
index 000000000..1bcd301f2
--- /dev/null
+++ b/ui/app/templates/components/auth-form/userpass.hbs
@@ -0,0 +1 @@
+{{partial "partials/userpass-form"}}
diff --git a/ui/app/templates/components/auth-info.hbs b/ui/app/templates/components/auth-info.hbs
new file mode 100644
index 000000000..80e0d4f9e
--- /dev/null
+++ b/ui/app/templates/components/auth-info.hbs
@@ -0,0 +1,56 @@
+
+
+
+
diff --git a/ui/app/templates/components/auth-method/configuration.hbs b/ui/app/templates/components/auth-method/configuration.hbs
new file mode 100644
index 000000000..67cbae8c5
--- /dev/null
+++ b/ui/app/templates/components/auth-method/configuration.hbs
@@ -0,0 +1,9 @@
+
+ {{#each model.attrs as |attr|}}
+ {{#if (eq attr.type "object")}}
+ {{info-table-row alwaysRender=true label=(or attr.options.label (to-label attr.name)) value=(stringify (get model attr.name))}}
+ {{else}}
+ {{info-table-row alwaysRender=true label=(or attr.options.label (to-label attr.name)) value=(get model attr.name)}}
+ {{/if}}
+ {{/each}}
+
diff --git a/ui/app/templates/components/b64-toggle.hbs b/ui/app/templates/components/b64-toggle.hbs
new file mode 100644
index 000000000..c52f4f4ff
--- /dev/null
+++ b/ui/app/templates/components/b64-toggle.hbs
@@ -0,0 +1,5 @@
+{{#if isBase64}}
+ Decode from base64
+{{else}}
+ Encode to base64
+{{/if}}
diff --git a/ui/app/templates/components/backend-configure.hbs b/ui/app/templates/components/backend-configure.hbs
new file mode 100644
index 000000000..e69de29bb
diff --git a/ui/app/templates/components/config-pki-ca.hbs b/ui/app/templates/components/config-pki-ca.hbs
new file mode 100644
index 000000000..ed61c6961
--- /dev/null
+++ b/ui/app/templates/components/config-pki-ca.hbs
@@ -0,0 +1,174 @@
+{{#if replaceCA}}
+ {{message-error model=model}}
+
+ {{#if needsConfig}}
+ Configure CA Certificate
+ {{else}}
+ Replace CA Certificate
+ {{/if}}
+
+ {{#if (or model.certificate model.csr)}}
+ {{#each model.attrs as |attr|}}
+ {{info-table-row data-test-table-row label=(capitalize (or attr.options.label (humanize (dasherize attr.name)))) value=(get model attr.name)}}
+ {{/each}}
+
+
+ {{#copy-button
+ clipboardText=(or model.certificate model.csr)
+ class="button"
+ buttonType="button"
+ success=(action (set-flash-message (concat (if model.certificate "Certificate" "CSR") " copied!")))
+ }}
+ Copy {{if model.certificate "Certificate" "CSR"}}
+ {{/copy-button}}
+
+
+
+ Back
+
+
+
+ {{else}}
+
+ {{/if}}
+{{else if signIntermediate}}
+ {{#if (or model.certificate)}}
+ {{#message-in-page data-test-warning type="warning"}}
+ If using this for an Intermediate CA in Vault, copy the certificate below and write it to the PKI mount being used as an intermediate using the `Set signed Intermediate` endpoint.
+ {{/message-in-page}}
+ {{#each model.attrs as |attr|}}
+ {{info-table-row data-test-table-row label=(capitalize (or attr.options.label (humanize (dasherize attr.name)))) value=(get model attr.name)}}
+ {{/each}}
+
+
+ {{#copy-button
+ clipboardText=model.certificate
+ class="button"
+ buttonType="button"
+ success=(action (set-flash-message "Certificate copied!"))
+ }}
+ Copy Certificate
+ {{/copy-button}}
+
+
+
+ Back
+
+
+
+ {{else}}
+ {{message-error model=model}}
+ Sign intermediate
+
+ {{/if}}
+{{else if setSignedIntermediate}}
+ {{message-error model=model}}
+ Set signed intermediate
+
+ Submit a signed CA certificate corresponding to a generated private key.
+
+
+{{else}}
+
+ This is the default CA certificate used in Vault. It is not used for self-signed certificates or if you have a signed intermediate CA certificate with a generated key.
+
+ {{#each downloadHrefs as |dl|}}
+
+ {{/each}}
+
+
+
+
+ {{#if needsConfig}}
+ Configure CA
+ {{else}}
+ Replace CA
+ {{/if}}
+
+
+ {{#if config.pem}}
+
+
+ Sign intermediate
+
+
+ {{/if}}
+
+
+ Set signed intermediate
+
+
+
+{{/if}}
diff --git a/ui/app/templates/components/config-pki.hbs b/ui/app/templates/components/config-pki.hbs
new file mode 100644
index 000000000..c1f140429
--- /dev/null
+++ b/ui/app/templates/components/config-pki.hbs
@@ -0,0 +1,26 @@
+{{#if (eq section "tidy")}}
+
+ You can tidy up the backend storage and/or CRL by removing certificates that have expired and are past a certain buffer period beyond their expiration time.
+
+{{else if (eq section "crl")}}
+
+ Certificate Revocation List (CRL) Config
+
+
+ Set the duration for which the generated CRL should be marked valid.
+
+{{/if}}
+
+{{message-error model=config}}
+
diff --git a/ui/app/templates/components/edition-badge.hbs b/ui/app/templates/components/edition-badge.hbs
new file mode 100644
index 000000000..0f8aac4ca
--- /dev/null
+++ b/ui/app/templates/components/edition-badge.hbs
@@ -0,0 +1 @@
+{{abbreviation}}
diff --git a/ui/app/templates/components/flex-table-column.hbs b/ui/app/templates/components/flex-table-column.hbs
new file mode 100644
index 000000000..210430eda
--- /dev/null
+++ b/ui/app/templates/components/flex-table-column.hbs
@@ -0,0 +1,14 @@
+
+
diff --git a/ui/app/templates/components/form-field-groups.hbs b/ui/app/templates/components/form-field-groups.hbs
new file mode 100644
index 000000000..3fdc4ca44
--- /dev/null
+++ b/ui/app/templates/components/form-field-groups.hbs
@@ -0,0 +1,29 @@
+{{#each model.fieldGroups as |fieldGroup|}}
+ {{#each-in fieldGroup as |group fields|}}
+ {{#if (or (not renderGroup) (and renderGroup (eq group renderGroup)))}}
+ {{#if (eq group "default")}}
+ {{#each fields as |attr|}}
+ {{#unless (and (not-eq mode 'create') (eq attr.name "name"))}}
+ {{form-field data-test-field attr=attr model=model onChange=onChange}}
+ {{/unless}}
+ {{/each}}
+ {{else}}
+ {{toggle-button
+ class="is-block"
+ toggleAttr=(concat "show" (camelize group))
+ toggleTarget=this
+ openLabel=(concat "Hide " group)
+ closedLabel=group
+ data-test-toggle-group=group
+ }}
+ {{#if (get this (concat "show" (camelize group)))}}
+
+ {{#each fields as |attr|}}
+ {{form-field data-test-field attr=attr model=model}}
+ {{/each}}
+
+ {{/if}}
+ {{/if}}
+ {{/if}}
+ {{/each-in}}
+{{/each}}
diff --git a/ui/app/templates/components/form-field.hbs b/ui/app/templates/components/form-field.hbs
new file mode 100644
index 000000000..cbe64c422
--- /dev/null
+++ b/ui/app/templates/components/form-field.hbs
@@ -0,0 +1,114 @@
+{{#unless (or (and attr.options.editType (not-eq attr.options.editType "textarea")) (eq attr.type "boolean"))}}
+
+ {{labelString}}
+ {{#if attr.options.helpText}}
+ {{#info-tooltip}}
+
+ {{attr.options.helpText}}
+
+ {{/info-tooltip}}
+ {{/if}}
+
+{{/unless}}
+{{#if attr.options.possibleValues}}
+
+
+
+ {{#each attr.options.possibleValues as |val|}}
+
+ {{or val.displayName val}}
+
+ {{/each}}
+
+
+
+{{else if (eq attr.options.editType 'mountAccessor')}}
+ {{mount-accessor-select
+ name=attr.name
+ label=labelString
+ warning=attr.options.warning
+ helpText=attr.options.helpText
+ value=(get model valuePath)
+ onChange=(action "setAndBroadcast" valuePath)
+ }}
+{{else if (eq attr.options.editType 'kv')}}
+ {{kv-object-editor
+ value=(get model valuePath)
+ onChange=(action "setAndBroadcast" valuePath)
+ label=labelString
+ warning=attr.options.warning
+ helpText=attr.options.helpText
+ }}
+{{else if (eq attr.options.editType 'file')}}
+ {{text-file
+ index=''
+ file=file
+ onChange=(action 'setFile')
+ warning=attr.options.warning
+ label=labelString
+ }}
+{{else if (eq attr.options.editType 'ttl')}}
+ {{ttl-picker
+ initialValue=(or (get model valuePath) attr.options.defaultValue)
+ labelText=labelString
+ warning=attr.options.warning
+ setDefaultValue=false
+ onChange=(action (action "setAndBroadcast" valuePath))
+ }}
+{{else if (eq attr.options.editType 'stringArray')}}
+ {{string-list
+ label=labelString
+ warning=attr.options.warning
+ helpText=attr.options.helpText
+ inputValue=(get model valuePath)
+ onChange=(action (action "setAndBroadcast" valuePath))
+ }}
+{{else if (or (eq attr.type 'number') (eq attr.type 'string'))}}
+
+ {{#if (eq attr.options.editType 'textarea')}}
+
+ {{else}}
+
+ {{/if}}
+
+{{else if (eq attr.type 'boolean')}}
+
+
+
+ {{labelString}}
+ {{#if attr.options.helpText}}
+ {{#info-tooltip}}
+ {{attr.options.helpText}}
+ {{/info-tooltip}}
+ {{/if}}
+
+
+{{else if (eq attr.type 'object')}}
+ {{json-editor
+ value=(if (get model valuePath) (stringify (get model valuePath)) emptyData)
+ valueUpdated=(action "codemirrorUpdated" attr.name)
+ }}
+{{/if}}
diff --git a/ui/app/templates/components/generate-credentials.hbs b/ui/app/templates/components/generate-credentials.hbs
new file mode 100644
index 000000000..1253502c0
--- /dev/null
+++ b/ui/app/templates/components/generate-credentials.hbs
@@ -0,0 +1,133 @@
+
+
+{{#if (or options.generateWithoutInput (get model options.generatedAttr))}}
+ {{#if loading}}
+ {{partial "partials/loading"}}
+ {{else}}
+
+ {{message-error model=model}}
+ {{#unless model.isError}}
+ {{#message-in-page type="warning" data-test-warning=true}}
+ You will not be able to access this information later, so please copy the information below.
+ {{/message-in-page}}
+ {{/unless}}
+ {{#each model.attrs as |attr|}}
+ {{#if (eq attr.type "object")}}
+ {{info-table-row label=(capitalize (or attr.options.label (humanize (dasherize attr.name)))) value=(stringify (get model attr.name))}}
+ {{else}}
+ {{info-table-row label=(capitalize (or attr.options.label (humanize (dasherize attr.name)))) value=(get model attr.name)}}
+ {{/if}}
+ {{/each}}
+
+
+
+ {{#copy-button
+ clipboardText=model.toCreds
+ class="button is-primary"
+ buttonType="button"
+ success=(action (set-flash-message "Credentials copied!"))
+ }}
+ Copy credentials
+ {{/copy-button}}
+
+ {{#if model.leaseId}}
+
+ {{#copy-button
+ clipboardText=model.leaseId
+ class="button"
+ buttonType="button"
+ success=(action (set-flash-message "Lease ID copied!"))
+ }}
+ Copy Lease ID
+ {{/copy-button}}
+
+ {{/if}}
+
+ {{#if options.backIsListLink}}
+ {{#link-to
+ "vault.cluster.secrets.backend.list-root"
+ backend.id
+ data-test-secret-generate-back=true
+ class="button"
+ }}
+ Back
+ {{/link-to}}
+ {{else}}
+
+ Back
+
+ {{/if}}
+
+
+ {{/if}}
+{{else}}
+
+{{/if}}
diff --git a/ui/app/templates/components/identity/edit-form.hbs b/ui/app/templates/components/identity/edit-form.hbs
new file mode 100644
index 000000000..8ed98941f
--- /dev/null
+++ b/ui/app/templates/components/identity/edit-form.hbs
@@ -0,0 +1,33 @@
+
diff --git a/ui/app/templates/components/identity/entity-nav.hbs b/ui/app/templates/components/identity/entity-nav.hbs
new file mode 100644
index 000000000..9d81b9d33
--- /dev/null
+++ b/ui/app/templates/components/identity/entity-nav.hbs
@@ -0,0 +1,44 @@
+
+
+
+
+
+ {{identity/lookup-input type=identityType}}
+
+
+
diff --git a/ui/app/templates/components/identity/item-alias/alias-details.hbs b/ui/app/templates/components/identity/item-alias/alias-details.hbs
new file mode 100644
index 000000000..d7ecb17c5
--- /dev/null
+++ b/ui/app/templates/components/identity/item-alias/alias-details.hbs
@@ -0,0 +1,29 @@
+{{info-table-row label="Name" value=model.name }}
+{{info-table-row label="ID" value=model.id }}
+{{#info-table-row label=(if (eq model.identityType "entity-alias") "Entity ID" "Group ID") value=model.canonicalId}}
+
+ {{model.canonicalId}}
+
+{{/info-table-row}}
+{{info-table-row label="Merged from Entity ID" value=model.mergedFromCanonicalIds}}
+{{#info-table-row label="Mount" value=model.mountAccessor }}
+
+ {{model.mountPath}}
+
+ {{model.mountType}}
+ {{model.mountAccessor}}
+
+
+{{/info-table-row}}
+{{#info-table-row label="Created" value=model.creationTime}}
+
+ {{moment-format model.creationTime 'MMM DD, YYYY [at] h:mm a'}}
+
+{{/info-table-row}}
+{{#info-table-row label="Last Updated" value=model.lastUpdateTime}}
+
+ {{moment-format model.lastUpdateTime 'MMM DD, YYYY [at] h:mm a'}}
+
+{{/info-table-row}}
diff --git a/ui/app/templates/components/identity/item-alias/alias-metadata.hbs b/ui/app/templates/components/identity/item-alias/alias-metadata.hbs
new file mode 100644
index 000000000..efa6658b8
--- /dev/null
+++ b/ui/app/templates/components/identity/item-alias/alias-metadata.hbs
@@ -0,0 +1,26 @@
+{{#each-in model.metadata as |key value|}}
+
+
+
+ {{key}}
+
+
+ {{value}}
+
+
+
+
+
+{{else}}
+
+
+
+
+
+ There is no metadata associated with {{model.name}}.
+
+
+
+
+
+{{/each-in}}
diff --git a/ui/app/templates/components/identity/item-aliases.hbs b/ui/app/templates/components/identity/item-aliases.hbs
new file mode 100644
index 000000000..f972272eb
--- /dev/null
+++ b/ui/app/templates/components/identity/item-aliases.hbs
@@ -0,0 +1,36 @@
+{{#each (if model.alias.id (array model.alias) model.aliases) as |item|}}
+ {{#linked-block
+ "vault.cluster.access.identity.aliases.show"
+ item.id
+ "details"
+ class="box is-sideless is-marginless"
+ }}
+
+ {{/linked-block}}
+{{else}}
+
+
+
+
+
+ There are no {{model.identityType}} aliases for {{model.name}}.
+
+
+
+
+
+{{/each}}
diff --git a/ui/app/templates/components/identity/item-details.hbs b/ui/app/templates/components/identity/item-details.hbs
new file mode 100644
index 000000000..75ba63cd1
--- /dev/null
+++ b/ui/app/templates/components/identity/item-details.hbs
@@ -0,0 +1,22 @@
+{{info-table-row label="Name" value=model.name }}
+{{info-table-row label="Type" value=model.type }}
+{{info-table-row label="ID" value=model.id }}
+{{#info-table-row label="Merged Ids" value=model.mergedEntityIds }}
+
+ {{#each model.mergedEntityIds as |id|}}
+
+ {{id}}
+
+ {{/each}}
+
+{{/info-table-row}}
+{{#info-table-row label="Created" value=model.creationTime}}
+
+ {{moment-format model.creationTime 'MMM DD, YYYY [at] h:mm a'}}
+
+{{/info-table-row}}
+{{#info-table-row label="Last Updated" value=model.lastUpdateTime}}
+
+ {{moment-format model.lastUpdateTime 'MMM DD, YYYY [at] h:mm a'}}
+
+{{/info-table-row}}
diff --git a/ui/app/templates/components/identity/item-groups.hbs b/ui/app/templates/components/identity/item-groups.hbs
new file mode 100644
index 000000000..149bf0ea2
--- /dev/null
+++ b/ui/app/templates/components/identity/item-groups.hbs
@@ -0,0 +1,38 @@
+{{#if model.groupIds}}
+ {{#each model.directGroupIds as |gid|}}
+ {{i-con
+ glyph='folder'
+ size=14
+ class="has-text-grey-light"
+ }}{{gid}}
+ {{/each}}
+ {{#each model.inheritedGroupIds as |gid|}}
+ {{#linked-block
+ "vault.cluster.access.identity.show" "groups" gid "details"
+ class="box is-sideless is-marginless"
+ }}
+ {{i-con
+ glyph='folder'
+ size=14
+ class="has-text-grey-light"
+ }}{{gid}}
+ inherited
+ {{/linked-block}}
+ {{/each}}
+{{else}}
+
+
+
+
+
+ {{model.name}} is not a member of any groups.
+
+
+
+
+
+{{/if}}
diff --git a/ui/app/templates/components/identity/item-members.hbs b/ui/app/templates/components/identity/item-members.hbs
new file mode 100644
index 000000000..f31f16f62
--- /dev/null
+++ b/ui/app/templates/components/identity/item-members.hbs
@@ -0,0 +1,32 @@
+{{#if model.hasMembers}}
+ {{#each model.memberGroupIds as |gid|}}
+ {{i-con
+ glyph='folder'
+ size=14
+ class="has-text-grey-light"
+ }}{{gid}}
+ {{/each}}
+ {{#each model.memberEntityIds as |gid|}}
+ {{i-con
+ glyph='role'
+ size=14
+ class="has-text-grey-light"
+ }}{{gid}}
+ {{/each}}
+{{else}}
+
+
+
+
+
+ There are no members in this group.
+
+
+
+
+
+{{/if}}
diff --git a/ui/app/templates/components/identity/item-metadata.hbs b/ui/app/templates/components/identity/item-metadata.hbs
new file mode 100644
index 000000000..efa6658b8
--- /dev/null
+++ b/ui/app/templates/components/identity/item-metadata.hbs
@@ -0,0 +1,26 @@
+{{#each-in model.metadata as |key value|}}
+
+
+
+ {{key}}
+
+
+ {{value}}
+
+
+
+
+
+{{else}}
+
+
+
+
+
+ There is no metadata associated with {{model.name}}.
+
+
+
+
+
+{{/each-in}}
diff --git a/ui/app/templates/components/identity/item-policies.hbs b/ui/app/templates/components/identity/item-policies.hbs
new file mode 100644
index 000000000..2211c16f7
--- /dev/null
+++ b/ui/app/templates/components/identity/item-policies.hbs
@@ -0,0 +1,32 @@
+{{#each model.policies as |item|}}
+ {{#linked-block
+ "vault.cluster.policy.show"
+ "acl"
+ item
+ class="box is-sideless is-marginless"
+ }}
+
+ {{/linked-block}}
+{{else}}
+
+
+
+
+
+ There are no policies associated with {{model.name}}.
+
+
+
+
+
+{{/each}}
+
diff --git a/ui/app/templates/components/identity/lookup-input.hbs b/ui/app/templates/components/identity/lookup-input.hbs
new file mode 100644
index 000000000..527df5676
--- /dev/null
+++ b/ui/app/templates/components/identity/lookup-input.hbs
@@ -0,0 +1,37 @@
+
diff --git a/ui/app/templates/components/info-table-row.hbs b/ui/app/templates/components/info-table-row.hbs
new file mode 100644
index 000000000..7021a3397
--- /dev/null
+++ b/ui/app/templates/components/info-table-row.hbs
@@ -0,0 +1,18 @@
+{{#if (or alwaysRender value)}}
+
+ {{label}}
+
+
+ {{#if hasBlock}}
+ {{yield}}
+ {{else if valueIsBoolean}}
+ {{#if value}}
+ {{i-con glyph="true" size=15}} Yes
+ {{else}}
+ {{i-con glyph="false" size=15}} No
+ {{/if}}
+ {{else}}
+ {{value}}
+ {{/if}}
+
+{{/if}}
diff --git a/ui/app/templates/components/info-tooltip.hbs b/ui/app/templates/components/info-tooltip.hbs
new file mode 100644
index 000000000..0a2085f1a
--- /dev/null
+++ b/ui/app/templates/components/info-tooltip.hbs
@@ -0,0 +1,16 @@
+{{#tool-tip renderInPlace=true as |d|}}
+ {{#d.trigger tagName="button" class=(concat "tool-tip-trigger button") data-test-tool-tip-trigger=true}}
+ {{i-con
+ glyph="information-reversed"
+ class="auto-width"
+ size=16
+ aria-label="help"
+ excludeIconClass=true
+ }}
+ {{/d.trigger}}
+ {{#d.content class="tool-tip"}}
+
+ {{yield}}
+
+ {{/d.content}}
+{{/tool-tip}}
diff --git a/ui/app/templates/components/key-value-header.hbs b/ui/app/templates/components/key-value-header.hbs
new file mode 100644
index 000000000..272f4296d
--- /dev/null
+++ b/ui/app/templates/components/key-value-header.hbs
@@ -0,0 +1,14 @@
+
+ {{#each secretPath as |path index|}}
+
+ /
+ {{#if linkToPaths}}
+
+ {{path.text}}
+
+ {{else}}
+ {{path.text}}
+ {{/if}}
+
+ {{/each}}
+
diff --git a/ui/app/templates/components/key-version-select.hbs b/ui/app/templates/components/key-version-select.hbs
new file mode 100644
index 000000000..7983a0fd0
--- /dev/null
+++ b/ui/app/templates/components/key-version-select.hbs
@@ -0,0 +1,26 @@
+{{#if (gt key.keysForEncryption.length 1)}}
+
+
Key version
+
+
+
+ {{#each key.keysForEncryption as |version|}}
+ {{#if (eq version key.latestVersion)}}
+
+ {{version}} (latest)
+
+ {{else}}
+
+ {{version}}
+
+ {{/if}}
+ {{/each}}
+
+
+
+
+{{/if}}
diff --git a/ui/app/templates/components/kv-object-editor.hbs b/ui/app/templates/components/kv-object-editor.hbs
new file mode 100644
index 000000000..275bf9754
--- /dev/null
+++ b/ui/app/templates/components/kv-object-editor.hbs
@@ -0,0 +1,57 @@
+{{#if label}}
+
+ {{label}}
+ {{#if helpText}}
+ {{#info-tooltip}}
+ {{helpText}}
+ {{/info-tooltip}}
+ {{/if}}
+
+{{/if}}
+{{#each kvData as |row index|}}
+
+
+ {{input
+ data-test-kv-key=true
+ value=row.name
+ placeholder="key"
+ change=(action "updateRow" row index)
+ class="input"
+ }}
+
+
+ {{textarea
+ data-test-kv-value=true
+ name=row.name
+ change=(action "updateRow" row index)
+ value=row.value
+ wrap="off"
+ class="input"
+ placeholder="value"
+ rows=1
+ }}
+
+
+ {{#if (eq kvData.length (inc index))}}
+
+ Add
+
+ {{else}}
+
+ {{i-con size=22 glyph='trash-a' excludeIconClass=true class="is-large has-text-grey-light"}}
+
+ {{/if}}
+
+
+{{/each}}
+{{#if kvHasDuplicateKeys}}
+ {{#message-in-page type="warning" class="is-marginless" data-test-duplicate-error-warnings=true}}
+ More than one key shares the same name. Please be sure to have unique key names or some data may be lost when saving.
+ {{/message-in-page}}
+{{/if}}
diff --git a/ui/app/templates/components/list-pagination.hbs b/ui/app/templates/components/list-pagination.hbs
new file mode 100644
index 000000000..b156e7675
--- /dev/null
+++ b/ui/app/templates/components/list-pagination.hbs
@@ -0,0 +1,107 @@
+{{#with (compact (flatten (array link model))) as |params|}}
+
+{{/with}}
diff --git a/ui/app/templates/components/logo-splash.hbs b/ui/app/templates/components/logo-splash.hbs
new file mode 100644
index 000000000..01ff28935
--- /dev/null
+++ b/ui/app/templates/components/logo-splash.hbs
@@ -0,0 +1,7 @@
+
+
+
+ {{partial "svg/vault-edition-logo"}}
+
+
+
diff --git a/ui/app/templates/components/menu-sidebar.hbs b/ui/app/templates/components/menu-sidebar.hbs
new file mode 100644
index 000000000..67203a5b0
--- /dev/null
+++ b/ui/app/templates/components/menu-sidebar.hbs
@@ -0,0 +1,21 @@
+
diff --git a/ui/app/templates/components/message-error.hbs b/ui/app/templates/components/message-error.hbs
new file mode 100644
index 000000000..e3244b258
--- /dev/null
+++ b/ui/app/templates/components/message-error.hbs
@@ -0,0 +1,7 @@
+{{#if displayErrors.length}}
+ {{#each displayErrors as |error|}}
+ {{#message-in-page type="danger" data-test-error=true}}
+ {{error}}
+ {{/message-in-page}}
+ {{/each}}
+{{/if}}
diff --git a/ui/app/templates/components/message-in-page.hbs b/ui/app/templates/components/message-in-page.hbs
new file mode 100644
index 000000000..be3d60501
--- /dev/null
+++ b/ui/app/templates/components/message-in-page.hbs
@@ -0,0 +1,18 @@
+
+
+
+ {{i-con
+ glyph=alertType.glyph
+ class=alertType.glyphClass
+ size=22
+ excludeIconClass=true
+ }}
+
+
+
+ {{alertType.text}}
+ {{yield}}
+
+
+
+
diff --git a/ui/app/templates/components/mount-accessor-select.hbs b/ui/app/templates/components/mount-accessor-select.hbs
new file mode 100644
index 000000000..01eec0ca5
--- /dev/null
+++ b/ui/app/templates/components/mount-accessor-select.hbs
@@ -0,0 +1,41 @@
+{{#if label}}
+
+ {{label}}
+ {{#if helpText}}
+ {{#info-tooltip}}
+ {{helpText}}
+ {{/info-tooltip}}
+ {{/if}}
+
+{{/if}}
+{{#if authMethods.isRunning}}
+
+
+
+{{else if authMethods.last.value}}
+
+
+
+ {{#each authMethods.last.value as |method|}}
+
+ {{method.path}} ({{method.type}})
+
+ {{/each}}
+
+
+
+{{else}}
+
+{{/if}}
+
diff --git a/ui/app/templates/components/mount-backend-form.hbs b/ui/app/templates/components/mount-backend-form.hbs
new file mode 100644
index 000000000..81eb4fd4a
--- /dev/null
+++ b/ui/app/templates/components/mount-backend-form.hbs
@@ -0,0 +1,30 @@
+
+
diff --git a/ui/app/templates/components/mount-filter-config-list.hbs b/ui/app/templates/components/mount-filter-config-list.hbs
new file mode 100644
index 000000000..ac64e5bf2
--- /dev/null
+++ b/ui/app/templates/components/mount-filter-config-list.hbs
@@ -0,0 +1,79 @@
+
+ Mount filter mode
+
+
+
+
+ {{#each (reduce-to-array "whitelist" "blacklist") as |mode|}}
+
+ {{capitalize mode}}
+
+ {{/each}}
+
+
+
+ {{#if (eq config.mode "blacklist")}}
+ Selected mounts will be excluded when replicating to the secondary {{or id config.id}}
.
+ {{else}}
+ Only the selected mounts will be replicated to the secondary {{or id config.id}}
.
+ {{/if}}
+
+
+
+ Filtered Mounts
+
+
+{{#each mounts as |mount|}}
+ {{#unless (or mount.local (includes singletonMountTypes mount.type))}}
+ {{!-- auth mounts have apiPath, secret mounts use path --}}
+ {{#with (or mount.apiPath mount.path) as |path| }}
+
+
+
+
+ {{path}}
+
+
+
+
+ {{/with}}
+ {{/unless}}
+{{/each}}
diff --git a/ui/app/templates/components/navigate-input.hbs b/ui/app/templates/components/navigate-input.hbs
new file mode 100644
index 000000000..e1d1874be
--- /dev/null
+++ b/ui/app/templates/components/navigate-input.hbs
@@ -0,0 +1,17 @@
+
+
+ {{input
+ value=filter
+ placeholder=(or placeholder "Filter keys")
+ class="filter input"
+ disabled=disabled
+ key-up=(action "handleKeyUp")
+ key-down=(action "handleKeyPress")
+ input=(action "handleInput")
+ focus-in=(action "setFilterFocused" true)
+ focus-out=(action "setFilterFocused" false)
+ }}
+
+ {{i-con glyph="ios-search-strong" class="is-left has-text-grey" size=18}}
+
+
diff --git a/ui/app/templates/components/not-found.hbs b/ui/app/templates/components/not-found.hbs
new file mode 100644
index 000000000..7c4005513
--- /dev/null
+++ b/ui/app/templates/components/not-found.hbs
@@ -0,0 +1,15 @@
+
+
+
+
Sorry, we were unable to find any content at {{or model.path path}}
.
+
Double check the url or go back {{home-link text="home"}}.
+
+
diff --git a/ui/app/templates/components/pgp-file.hbs b/ui/app/templates/components/pgp-file.hbs
new file mode 100644
index 000000000..8526d9e37
--- /dev/null
+++ b/ui/app/templates/components/pgp-file.hbs
@@ -0,0 +1,77 @@
+
+
+
+ {{#if label}}
+ {{label}}
+ {{else}}
+ PGP KEY {{if (not-eq index '') (inc index)}}
+ {{/if}}
+
+
+
+
+
+
+ Enter as text
+
+
+
+
+
+ {{#if key.enterAsText}}
+
+
+
+
+ {{#if textareaHelpText}}
+ {{textareaHelpText}}
+ {{else}}
+ Enter a base64-encoded key
+ {{/if}}
+
+ {{else}}
+
+
+
+
+
+
+ {{i-con glyph="document" size=16}}
+
+
+ {{#if key.fileName}}
+ {{key.fileName}}
+ {{else}}
+ Choose a file…
+ {{/if}}
+
+ {{#if key.fileName}}
+
+ {{i-con glyph="close" size=16}}
+
+ {{/if}}
+
+
+
+
+
+ {{#if fileHelpText}}
+ {{fileHelpText}}
+ {{else}}
+ Select a PGP key from your computer
+ {{/if}}
+
+ {{/if}}
+
diff --git a/ui/app/templates/components/pgp-list.hbs b/ui/app/templates/components/pgp-list.hbs
new file mode 100644
index 000000000..f4052c0dc
--- /dev/null
+++ b/ui/app/templates/components/pgp-list.hbs
@@ -0,0 +1,7 @@
+{{#each listData as |key index|}}
+ {{pgp-file key=key index=index onChange=(action 'setKey')}}
+{{else}}
+
+ Enter a number of Key Shares to enter PGP keys.
+
+{{/each}}
diff --git a/ui/app/templates/components/pki-cert-popup.hbs b/ui/app/templates/components/pki-cert-popup.hbs
new file mode 100644
index 000000000..90eb720f1
--- /dev/null
+++ b/ui/app/templates/components/pki-cert-popup.hbs
@@ -0,0 +1,29 @@
+{{#popup-menu name="role-aws-nav" contentClass="is-wide"}}
+
+{{/popup-menu}}
diff --git a/ui/app/templates/components/pki-cert-show.hbs b/ui/app/templates/components/pki-cert-show.hbs
new file mode 100644
index 000000000..56d817e67
--- /dev/null
+++ b/ui/app/templates/components/pki-cert-show.hbs
@@ -0,0 +1,61 @@
+
+
+
+ {{message-error model=model}}
+ {{#each model.attrs as |attr|}}
+ {{#if (eq attr.type "object")}}
+ {{info-table-row label=(capitalize (or attr.options.label (humanize (dasherize attr.name)))) value=(stringify (get model attr.name))}}
+ {{else}}
+ {{info-table-row label=(capitalize (or attr.options.label (humanize (dasherize attr.name)))) value=(get model attr.name)}}
+ {{/if}}
+ {{/each}}
+
+
+
+
+ {{#copy-button
+ clipboardText=model.toCreds
+ class="button is-primary"
+ buttonType="button"
+ success=(action (set-flash-message "Credentials copied!"))
+ }}
+ Copy credentials
+ {{/copy-button}}
+
+
+ {{#link-to
+ "vault.cluster.secrets.backend.list-root"
+ (query-params tab="certs")
+ class="button"
+ }}
+ Back
+ {{/link-to}}
+
+
+ {{#if (and (not model.revocationTime) model.canRevoke)}}
+ {{#confirm-action
+ buttonClasses="button"
+ onConfirmAction=(action "delete")
+ confirmMessage=(concat "Are you sure you want to revoke " model.id "?")
+ cancelButtonText="Cancel"
+ confirmButtonText="Revoke"
+ }}
+ Revoke
+ {{/confirm-action}}
+ {{/if}}
+
diff --git a/ui/app/templates/components/popup-menu.hbs b/ui/app/templates/components/popup-menu.hbs
new file mode 100644
index 000000000..12f07223e
--- /dev/null
+++ b/ui/app/templates/components/popup-menu.hbs
@@ -0,0 +1,15 @@
+{{#basic-dropdown class="popup-menu" horizontalPosition="auto-right" verticalPosition="below" onOpen=onOpen as |d|}}
+ {{#d.trigger tagName="button" class=(concat "popup-menu-trigger button is-transparent " (if d.isOpen "is-active")) data-test-popup-menu-trigger=true}}
+ {{i-con
+ glyph="more"
+ class="has-text-black auto-width"
+ size=16
+ aria-label="More options"
+ }}
+ {{/d.trigger}}
+ {{#d.content class=(concat "popup-menu-content " contentClass)}}
+
+ {{yield}}
+
+ {{/d.content}}
+{{/basic-dropdown}}
diff --git a/ui/app/templates/components/replication-actions.hbs b/ui/app/templates/components/replication-actions.hbs
new file mode 100644
index 000000000..08c0c031c
--- /dev/null
+++ b/ui/app/templates/components/replication-actions.hbs
@@ -0,0 +1,5 @@
+{{message-error errors=errors}}
+{{#if loading}}
+{{else}}
+ {{partial (concat 'partials/replication/' selectedAction)}}
+{{/if}}
diff --git a/ui/app/templates/components/replication-mode-summary.hbs b/ui/app/templates/components/replication-mode-summary.hbs
new file mode 100644
index 000000000..d3fb34c19
--- /dev/null
+++ b/ui/app/templates/components/replication-mode-summary.hbs
@@ -0,0 +1 @@
+{{partial partialName}}
diff --git a/ui/app/templates/components/replication-summary.hbs b/ui/app/templates/components/replication-summary.hbs
new file mode 100644
index 000000000..3d0b2bad3
--- /dev/null
+++ b/ui/app/templates/components/replication-summary.hbs
@@ -0,0 +1,24 @@
+{{#if (not version.hasDRReplication)}}
+ {{upgrade-page title="Replication"}}
+{{else if (or cluster.allReplicationDisabled cluster.replicationAttrs.replicationDisabled)}}
+ {{partial 'partials/replication/enable'}}
+{{else if showModeSummary}}
+ {{partial 'partials/replication/mode-summary'}}
+{{else}}
+ {{#if (eq replicationAttrs.mode 'initializing')}}
+ The cluster is initializing replication. This may take some time.
+ {{else}}
+ {{info-table-row label="Mode" value=replicationAttrs.mode}}
+ {{info-table-row label="Replication set" value=replicationAttrs.clusterId}}
+ {{info-table-row label="Secondary ID" value=replicationAttrs.secondaryId}}
+ {{info-table-row label="State" value=replicationAttrs.state}}
+ {{info-table-row label="Primary cluster address" value=replicationAttrs.primaryClusterAddr}}
+ {{info-table-row label="Replication state" value=replicationAttrs.replicationState}}
+ {{info-table-row label="Last WAL entry" value=replicationAttrs.lastWAL}}
+ {{info-table-row label="Last Remote WAL entry" value=replicationAttrs.lastRemoteWAL}}
+ {{info-table-row label="Merkle root index" value=replicationAttrs.merkleRoot}}
+ {{#if replicationAttrs.syncProgress}}
+ {{info-table-row label="Sync progress" value=(concat replicationAttrs.syncProgress.progress '/' replicationAttrs.syncProgress.total)}}
+ {{/if}}
+ {{/if}}
+{{/if}}
diff --git a/ui/app/templates/components/role-aws-edit.hbs b/ui/app/templates/components/role-aws-edit.hbs
new file mode 100644
index 000000000..cae7480e2
--- /dev/null
+++ b/ui/app/templates/components/role-aws-edit.hbs
@@ -0,0 +1,74 @@
+
+
+{{#if (or (eq mode 'edit') (eq mode 'create'))}}
+ {{partial 'partials/role-aws/form'}}
+{{else}}
+ {{partial 'partials/role-aws/show'}}
+{{/if}}
diff --git a/ui/app/templates/components/role-pki-edit.hbs b/ui/app/templates/components/role-pki-edit.hbs
new file mode 100644
index 000000000..f73ec1884
--- /dev/null
+++ b/ui/app/templates/components/role-pki-edit.hbs
@@ -0,0 +1,76 @@
+
+
+{{#if (or (eq mode 'edit') (eq mode 'create'))}}
+ {{partial 'partials/role-pki/form'}}
+{{else}}
+ {{partial 'partials/role-pki/show'}}
+{{/if}}
diff --git a/ui/app/templates/components/role-ssh-edit.hbs b/ui/app/templates/components/role-ssh-edit.hbs
new file mode 100644
index 000000000..efdc98210
--- /dev/null
+++ b/ui/app/templates/components/role-ssh-edit.hbs
@@ -0,0 +1,70 @@
+
+
+{{#if (or (eq mode 'edit') (eq mode 'create'))}}
+ {{partial 'partials/role-ssh/form'}}
+{{else}}
+ {{partial 'partials/role-ssh/show'}}
+{{/if}}
diff --git a/ui/app/templates/components/secret-edit.hbs b/ui/app/templates/components/secret-edit.hbs
new file mode 100644
index 000000000..679937d99
--- /dev/null
+++ b/ui/app/templates/components/secret-edit.hbs
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+ JSON
+
+ {{#if (and (not-eq mode 'create') (or capabilities.canUpdate capabilities.canDelete))}}
+
+ {{input
+ id="edit"
+ type="checkbox"
+ name="navToEdit"
+ class="switch is-rounded is-success is-small"
+ checked=(eq mode 'edit')
+ change=(action (nav-to-route (concat 'vault.cluster.secrets.backend.' (if (eq mode 'show') 'edit' 'show')) key.id replace=true) )
+ }}
+ Edit
+
+ {{/if}}
+
+
+
+
+{{partial partialName}}
diff --git a/ui/app/templates/components/section-tabs.hbs b/ui/app/templates/components/section-tabs.hbs
new file mode 100644
index 000000000..842eca60f
--- /dev/null
+++ b/ui/app/templates/components/section-tabs.hbs
@@ -0,0 +1,17 @@
+{{#with (tabs-for-auth-section model.type tabType) as |tabs|}}
+ {{#if tabs.length}}
+
+
+
+ {{#each tabs as |tab|}}
+ {{#link-to params=tab.routeParams tagName="li"}}
+
+ {{tab.label}}
+
+ {{/link-to}}
+ {{/each}}
+
+
+
+ {{/if}}
+{{/with}}
diff --git a/ui/app/templates/components/shamir-flow.hbs b/ui/app/templates/components/shamir-flow.hbs
new file mode 100644
index 000000000..5c5d22403
--- /dev/null
+++ b/ui/app/templates/components/shamir-flow.hbs
@@ -0,0 +1,160 @@
+{{#if encoded_token}}
+
+
+
+
+ Encoded Operation Token
+
+ {{encoded_token}}
+
+
+
+ If you entered a One Time Password, you can use the Vault CLI to decode the Token:
+
+
+
+
+ DR Operation Token Command
+
+
+ vault generate-root -otp="[enter your otp here]" -decode="{{encoded_token}}"
+
+
+
+ {{#copy-button
+ clipboardText=(concat 'vault generate-root -otp="
" -decode="' encoded_token '"')
+ class="button is-compact"
+ buttonType="button"
+ success=(action (set-flash-message 'CLI command copied!'))
+ }}
+ Copy CLI command
+ {{/copy-button}}
+
+
+ {{#copy-button
+ clipboardText=encoded_token
+ class="button is-primary"
+ buttonType="button"
+ success=(action (set-flash-message 'OTP copied!'))
+ }}
+ Copy Encoded Operation Token
+ {{/copy-button}}
+
+ Clear Token
+
+
+{{else if (and generateAction (not started))}}
+
+{{else}}
+
+ {{message-error errors=errors}}
+
+
+
+
+
+
+ {{if generateAction "Generate Token" buttonText}}
+
+
+
+ {{#if (or started hasProgress)}}
+ {{shamir-progress
+ threshold=threshold
+ progress=progress
+ }}
+ {{/if}}
+
+
+
+{{/if}}
diff --git a/ui/app/templates/components/shamir-progress.hbs b/ui/app/templates/components/shamir-progress.hbs
new file mode 100644
index 000000000..63f9f14be
--- /dev/null
+++ b/ui/app/templates/components/shamir-progress.hbs
@@ -0,0 +1,8 @@
+
+
+
+ {{progress}} / {{threshold}} keys provided
+
+
+
+
diff --git a/ui/app/templates/components/splash-page.hbs b/ui/app/templates/components/splash-page.hbs
new file mode 100644
index 000000000..4f04020cb
--- /dev/null
+++ b/ui/app/templates/components/splash-page.hbs
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+ {{partial "svg/vault-edition-logo"}}
+
+ {{yield (hash header=(component 'splash-page/splash-header'))}}
+
+
+ {{yield (hash content=(component 'splash-page/splash-content'))}}
+
+ {{yield (hash footer=(component 'splash-page/splash-content')) }}
+
+
+
diff --git a/ui/app/templates/components/splash-page/splash-content.hbs b/ui/app/templates/components/splash-page/splash-content.hbs
new file mode 100644
index 000000000..889d9eead
--- /dev/null
+++ b/ui/app/templates/components/splash-page/splash-content.hbs
@@ -0,0 +1 @@
+{{yield}}
diff --git a/ui/app/templates/components/splash-page/splash-footer.hbs b/ui/app/templates/components/splash-page/splash-footer.hbs
new file mode 100644
index 000000000..889d9eead
--- /dev/null
+++ b/ui/app/templates/components/splash-page/splash-footer.hbs
@@ -0,0 +1 @@
+{{yield}}
diff --git a/ui/app/templates/components/splash-page/splash-header.hbs b/ui/app/templates/components/splash-page/splash-header.hbs
new file mode 100644
index 000000000..889d9eead
--- /dev/null
+++ b/ui/app/templates/components/splash-page/splash-header.hbs
@@ -0,0 +1 @@
+{{yield}}
diff --git a/ui/app/templates/components/status-menu.hbs b/ui/app/templates/components/status-menu.hbs
new file mode 100644
index 000000000..f3ac08fcc
--- /dev/null
+++ b/ui/app/templates/components/status-menu.hbs
@@ -0,0 +1,31 @@
+{{#basic-dropdown-hover horizontalPosition="auto-right" verticalPosition="below" as |d|}}
+ {{#d.trigger tagName=(if (eq type "replication") "span" "button") class=(if (eq type "replication") "" "button is-transparent")}}
+ {{#if (eq type "user")}}
+
+ {{i-con glyph="chevron-down" aria-hidden="true" size=8 class="has-text-white auto-width is-status-chevron"}}
+ {{else if (eq type "replication")}}
+
+ Replication
+ {{i-con glyph="chevron-down" aria-hidden="true" excludeIconClass=true size=8 class="auto-width is-status-chevron is-marginless"}}
+
+ {{else}}
+ {{i-con
+ glyph=glyphName
+ class="has-text-white auto-width"
+ size=16
+ aria-label=ariaLabel
+ }}
+ {{i-con glyph="chevron-down" aria-hidden="true" size=8 class="has-text-white auto-width is-status-chevron"}}
+ {{/if}}
+ {{/d.trigger}}
+ {{#d.content class=(concat "status-menu-content status-menu-content-" type)}}
+ {{partial partialName}}
+ {{/d.content}}
+{{/basic-dropdown-hover}}
diff --git a/ui/app/templates/components/string-list.hbs b/ui/app/templates/components/string-list.hbs
new file mode 100644
index 000000000..d0a15625c
--- /dev/null
+++ b/ui/app/templates/components/string-list.hbs
@@ -0,0 +1,42 @@
+{{#if label}}
+
+ {{label}}
+ {{#if helpText}}
+ {{#info-tooltip}}
+ {{helpText}}
+ {{/info-tooltip}}
+ {{/if}}
+
+{{/if}}
+{{#if warning}}
+ {{#message-in-page type="warning"}}
+ {{warning}}
+ {{/message-in-page}}
+{{/if}}
+{{#each inputList as |data index|}}
+
+
+ {{input
+ data-test-string-list-input=index
+ type="text"
+ class="input"
+ value=data.value
+ name=(concat elementId "-" index)
+ id=(concat elementId "-" index)
+ key-up=(action "inputChanged" index)
+ change=(action (action "inputChanged" index) value="target.value")
+ }}
+
+
+ {{#if (eq (inc index) inputList.length)}}
+
+ Add
+
+ {{else}}
+
+ {{i-con size=22 glyph='trash-a' excludeIconClass=true class="is-large has-text-grey-light"}}
+
+ {{/if}}
+
+
+{{/each}}
diff --git a/ui/app/templates/components/text-file.hbs b/ui/app/templates/components/text-file.hbs
new file mode 100644
index 000000000..c2fadf850
--- /dev/null
+++ b/ui/app/templates/components/text-file.hbs
@@ -0,0 +1,71 @@
+{{#unless inputOnly}}
+
+
+
+ {{#if label}}
+ {{label}}
+ {{else}}
+ File
+ {{/if}}
+
+
+
+
+
+
+ Enter as text
+
+
+
+
+{{/unless}}
+
+ {{#if file.enterAsText}}
+
+
+
+
+ {{textareaHelpText}}
+
+ {{else}}
+
+
+
+
+
+
+ {{i-con glyph="document" size=16}}
+
+
+ {{#if file.fileName}}
+ {{file.fileName}}
+ {{else}}
+ Choose a file…
+ {{/if}}
+
+ {{#if file.fileName}}
+
+ {{i-con glyph="close" size=16}}
+
+ {{/if}}
+
+
+
+
+
+ {{fileHelpText}}
+
+ {{/if}}
+
diff --git a/ui/app/templates/components/toggle-button.hbs b/ui/app/templates/components/toggle-button.hbs
new file mode 100644
index 000000000..bd1fb85ab
--- /dev/null
+++ b/ui/app/templates/components/toggle-button.hbs
@@ -0,0 +1,5 @@
+{{#if isOpen}}
+ {{i-con glyph='chevron-up' exludeIconClass=true}} {{openLabel}}
+{{else}}
+ {{i-con glyph='chevron-down' exludeIconClass=true}} {{closedLabel}}
+{{/if}}
diff --git a/ui/app/templates/components/token-expire-warning.hbs b/ui/app/templates/components/token-expire-warning.hbs
new file mode 100644
index 000000000..4dfe0785a
--- /dev/null
+++ b/ui/app/templates/components/token-expire-warning.hbs
@@ -0,0 +1,28 @@
+{{#unless (and isDismissed (is-before (now interval=1000) auth.tokenExpirationDate))}}
+ {{#if (is-after (now interval=1000) auth.tokenExpirationDate)}}
+ {{#message-in-page type="danger"}}
+
+
+ Your auth token expired at {{moment-format auth.tokenExpirationDate 'YYYY-MM-DD HH:mm:ss'}}.
+
+
+
+ Reauthenticate
+
+ {{/message-in-page}}
+ {{else if auth.allowExpiration}}
+ {{#message-in-page type="warning"}}
+
+
+ We've determined you are inactive, and have stopped auto-renewing your current auth token.
+ Your token will expire in {{moment-from-now auth.tokenExpirationDate interval=1000 hideSuffix=true}} at
+ {{moment-format auth.tokenExpirationDate 'YYYY-MM-DD HH:mm:ss'}}
+
+
Resume auto-renewal
+
+
+ {{i-con size=12 glyph="close"}}
+
+ {{/message-in-page}}
+ {{/if}}
+{{/unless}}
diff --git a/ui/app/templates/components/tool-actions-form.hbs b/ui/app/templates/components/tool-actions-form.hbs
new file mode 100644
index 000000000..93a5e9654
--- /dev/null
+++ b/ui/app/templates/components/tool-actions-form.hbs
@@ -0,0 +1,4 @@
+{{message-error errors=errors}}
+
diff --git a/ui/app/templates/components/tool-tip.hbs b/ui/app/templates/components/tool-tip.hbs
new file mode 100644
index 000000000..4526b6424
--- /dev/null
+++ b/ui/app/templates/components/tool-tip.hbs
@@ -0,0 +1,27 @@
+{{#basic-dropdown-hover
+ renderInPlace=renderInPlace
+ verticalPosition=verticalPosition
+ horizontalPosition=horizontalPosition
+ matchTriggerWidth=matchTriggerWidth
+ triggerComponent=triggerComponent
+ contentComponent=contentComponent
+ calculatePosition=calculatePosition
+ calculateInPlacePosition=calculateInPlacePosition as |dd|}}
+ {{yield (assign
+ dd
+ (hash
+ trigger=(component dd.trigger
+ onMouseDown=(action "prevent")
+ onMouseEnter=(action "open")
+ onMouseLeave=(action "close")
+ onBlur=(action "close")
+ )
+ content=(component dd.content
+ onMouseEnter=(action "open")
+ onMouseLeave=(action "close")
+ onFocus=(action "open")
+ onBlur=(action "close")
+ )
+ )
+ )}}
+{{/basic-dropdown-hover}}
diff --git a/ui/app/templates/components/transit-edit.hbs b/ui/app/templates/components/transit-edit.hbs
new file mode 100644
index 000000000..3c5f68433
--- /dev/null
+++ b/ui/app/templates/components/transit-edit.hbs
@@ -0,0 +1,54 @@
+
+
+{{partial (concat 'partials/transit-form-' mode)}}
diff --git a/ui/app/templates/components/transit-key-action/datakey.hbs b/ui/app/templates/components/transit-key-action/datakey.hbs
new file mode 100644
index 000000000..a408f1467
--- /dev/null
+++ b/ui/app/templates/components/transit-key-action/datakey.hbs
@@ -0,0 +1,123 @@
+
diff --git a/ui/app/templates/components/transit-key-action/decrypt.hbs b/ui/app/templates/components/transit-key-action/decrypt.hbs
new file mode 100644
index 000000000..93f5654bb
--- /dev/null
+++ b/ui/app/templates/components/transit-key-action/decrypt.hbs
@@ -0,0 +1,76 @@
+
+ {{#if (and plaintext ciphertext)}}
+
+
+
+ Plaintext
+
+
+ {{textarea id="plaintext" value=plaintext readonly=true class="textarea" data-test-transit-input="plaintext"}}
+ {{b64-toggle value=plaintext isInput=false initialEncoding="base64" data-test-transit-b64-toggle="plaintext"}}
+
+
+
+
+
+ {{#copy-button
+ clipboardTarget="#plaintext"
+ class="button is-primary"
+ buttonType="button"
+ success=(action (set-flash-message 'Plaintext copied!'))
+ }}
+ Copy
+ {{/copy-button}}
+
+
+
+ Back
+
+
+
+ {{else}}
+
+
+
Ciphertext
+
+ {{textarea id="ciphertext" name="ciphertext" value=ciphertext class="textarea" data-test-transit-input="ciphertext"}}
+
+
+ {{#if key.derived}}
+
+
+ Context
+
+
+
+ {{input type="text" id="context" value=context class="input" data-test-transit-input="context"}}
+
+
+ {{b64-toggle value=context data-test-transit-b64-toggle="context"}}
+
+
+
+ {{/if}}
+ {{#if (eq key.convergentEncryptionVersion 1)}}
+
+
Nonce
+
+
+ {{input type="text" id="nonce" value=nonce class="input" data-test-transit-input="nonce"}}
+
+
+ {{b64-toggle value=nonce data-test-transit-b64-toggle="nonce"}}
+
+
+
+ {{/if}}
+
+
+ {{/if}}
+
diff --git a/ui/app/templates/components/transit-key-action/encrypt.hbs b/ui/app/templates/components/transit-key-action/encrypt.hbs
new file mode 100644
index 000000000..8316972e9
--- /dev/null
+++ b/ui/app/templates/components/transit-key-action/encrypt.hbs
@@ -0,0 +1,83 @@
+
+ {{#if (and plaintext ciphertext)}}
+
+
+
Ciphertext
+
+ {{ciphertext}}
+
+
+
+
+
+ {{#copy-button
+ clipboardTarget="#ciphertext"
+ class="button is-primary"
+ buttonType="button"
+ success=(action (set-flash-message 'Ciphertext copied!'))
+ }}
+ Copy
+ {{/copy-button}}
+
+
+
+ Back
+
+
+
+ {{else}}
+
+ {{key-version-select
+ key=key
+ onVersionChange=(action (mut key_version))
+ key_version=key_version
+ }}
+
+
+ Plaintext
+
+
+ {{textarea id="plaintext" value=plaintext class="textarea" data-test-transit-input="plaintext"}}
+ {{b64-toggle value=plaintext isInput=false data-test-transit-b64-toggle="plaintext"}}
+
+
+ {{#if key.derived}}
+
+
+ Context
+
+
+
+ {{input type="text" id="context" value=context class="input" data-test-transit-input="context"}}
+
+
+ {{b64-toggle value=context data-test-transit-b64-toggle="context"}}
+
+
+
+ {{/if}}
+ {{#if (eq key.convergentEncryptionVersion 1)}}
+
+
+
+ Nonce
+
+
+ {{b64-toggle value=nonce data-test-transit-b64-toggle="nonce"}}
+
+
+
+ {{input type="text" id="nonce" value=nonce class="input" data-test-transit-input="nonce"}}
+
+
+ {{/if}}
+
+
+ {{/if}}
+
diff --git a/ui/app/templates/components/transit-key-action/export.hbs b/ui/app/templates/components/transit-key-action/export.hbs
new file mode 100644
index 000000000..579ae1f95
--- /dev/null
+++ b/ui/app/templates/components/transit-key-action/export.hbs
@@ -0,0 +1,103 @@
+
+ {{#if (or keys wrappedToken) }}
+
+
+ {{#if wrapTTL}}
+
Wrapped Key
+
+ {{wrappedToken}}
+
+ {{else}}
+
Exported Key
+ {{json-editor
+ value=(stringify keys)
+ options=(hash
+ readOnly=true
+ )
+ }}
+ {{/if}}
+
+
+
+
+ {{#copy-button
+ clipboardText=(if wrapTTL wrappedToken (stringify keys))
+ class="button is-primary"
+ buttonType="button"
+ success=(action (set-flash-message (if wrapTTL 'Wrapped key copied!' 'Exported key copied!')))
+ }}
+ Copy
+ {{/copy-button}}
+
+
+
+ Back
+
+
+
+ {{else}}
+
+
+
Key type
+
+
+
+ {{#each key.exportKeyTypes as |currOption|}}
+
+ {{currOption}}
+
+ {{/each}}
+
+
+
+
+
+
+ {{input type="checkbox" name="exportVersion" id="exportVersion" class="styled" checked=exportVersion}}
+
+ Export a single version
+
+
+ {{#if exportVersion}}
+
+
Version
+
+
+
+ {{#each key.validKeyVersions as |versionOption|}}
+
+ {{versionOption}}
+ {{#if (eq key.validKeyVersions.lastObject versionOption)}}
+ (latest)
+ {{/if}}
+
+ {{/each}}
+
+
+
+
+ {{/if}}
+
+ {{wrap-ttl onChange=(action (mut wrapTTL))}}
+
+
+ {{/if}}
+
diff --git a/ui/app/templates/components/transit-key-action/hmac.hbs b/ui/app/templates/components/transit-key-action/hmac.hbs
new file mode 100644
index 000000000..06d4931d1
--- /dev/null
+++ b/ui/app/templates/components/transit-key-action/hmac.hbs
@@ -0,0 +1,71 @@
+
+ {{#if hmac}}
+
+
+
+ {{#copy-button
+ clipboardTarget="#hmac"
+ class="button is-primary"
+ buttonType="button"
+ success=(action (set-flash-message 'HMAC copied!'))
+ }}
+ Copy
+ {{/copy-button}}
+
+
+
+ Back
+
+
+
+ {{else}}
+
+ {{key-version-select
+ key=key
+ onVersionChange=(action (mut key_version))
+ key_version=key_version
+ }}
+
+
+ Input
+
+
+ {{textarea id="input" name="input" value=input class="textarea" data-test-transit-input="input"}}
+ {{b64-toggle value=input isInput=false data-test-transit-b64-toggle="input"}}
+
+
+
+
Algorithm
+
+
+
+ {{#each (sha2-digest-sizes) as |algo|}}
+
+ {{algo}}
+
+ {{/each}}
+
+
+
+
+
+
+ {{/if}}
+
diff --git a/ui/app/templates/components/transit-key-action/rewrap.hbs b/ui/app/templates/components/transit-key-action/rewrap.hbs
new file mode 100644
index 000000000..daff9f9ac
--- /dev/null
+++ b/ui/app/templates/components/transit-key-action/rewrap.hbs
@@ -0,0 +1,55 @@
+
+
+ {{key-version-select
+ key=key
+ onVersionChange=(action (mut key_version))
+ key_version=key_version
+ }}
+
+
Ciphertext
+
+ {{textarea name="ciphertext" class="textarea" id="ciphertext" value=ciphertext}}
+
+
+ {{#if key.derived}}
+
+
+ Context
+
+
+
+ {{input type="text" id="context" value=context class="input" data-test-transit-input="context"}}
+
+
+ {{b64-toggle value=context data-test-transit-b64-toggle="context"}}
+
+
+
+ {{/if}}
+ {{#if (eq key.convergentEncryptionVersion 1)}}
+
+
Nonce
+
+
+ {{input type="text" id="nonce" value=nonce class="input" data-test-transit-input="nonce"}}
+
+
+ {{b64-toggle value=nonce data-test-transit-b64-toggle="nonce"}}
+
+
+
+ {{/if}}
+
+
+
+
+ Submitting this form will update the ciphertext
in-place.
+
+
+
+
+ Rewrap
+
+
+
+
diff --git a/ui/app/templates/components/transit-key-action/sign.hbs b/ui/app/templates/components/transit-key-action/sign.hbs
new file mode 100644
index 000000000..d2a1b4b72
--- /dev/null
+++ b/ui/app/templates/components/transit-key-action/sign.hbs
@@ -0,0 +1,96 @@
+
+ {{#if signature}}
+
+
+
Signature
+
+ {{signature}}
+
+
+
+
+
+ {{#copy-button
+ clipboardTarget="#signature"
+ class="button is-primary"
+ buttonType="button"
+ success=(action (set-flash-message 'Signature copied!'))
+ }}
+ Copy
+ {{/copy-button}}
+
+
+
+ Back
+
+
+
+ {{else}}
+
+ {{key-version-select
+ key=key
+ onVersionChange=(action (mut key_version))
+ key_version=key_version
+ }}
+
+
+ Input
+
+
+ {{textarea id="input" name="input" value=input class="textarea" data-test-transit-input="input"}}
+ {{b64-toggle value=input isInput=false data-test-transit-b64-toggle="input"}}
+
+
+ {{#if key.derived}}
+
+
+ Context
+
+
+
+ {{input type="text" id="context" value=context class="input" data-test-transit-input="context"}}
+
+
+ {{b64-toggle value=context data-test-transit-b64-toggle="context"}}
+
+
+
+ {{/if}}
+
+
+
+ Algorithm
+
+
+
+ {{input id="prehashed" type="checkbox" name="prehashed" class="switch is-rounded is-success is-small" checked=prehashed }}
+ Prehashed
+
+
+
+
+
+
+ {{#each (sha2-digest-sizes) as |algo|}}
+
+ {{algo}}
+
+ {{/each}}
+
+
+
+
+
+
+ {{/if}}
+
diff --git a/ui/app/templates/components/transit-key-action/verify.hbs b/ui/app/templates/components/transit-key-action/verify.hbs
new file mode 100644
index 000000000..20e9dc186
--- /dev/null
+++ b/ui/app/templates/components/transit-key-action/verify.hbs
@@ -0,0 +1,155 @@
+
+ {{#if (not-eq valid null)}}
+
+
Verified
+
+
+
+
+ {{i-con glyph=(if valid 'checkmark' 'close') }}
+ The input is {{if valid 'valid' 'not valid'}} for the given {{if signature 'signature' 'hmac'}}.
+
+
+
+
+
+
+
+ Back
+
+
+ {{else}}
+
+
+
+ Input
+
+
+ {{textarea id="input" name="input" value=input class="textarea" data-test-transit-input="input"}}
+ {{b64-toggle value=input isInput=false data-test-transit-b64-toggle="input"}}
+
+
+ {{#if (and key.supportsSigning key.derived (not hmac))}}
+
+
+ Context
+
+
+
+ {{input type="text" id="context" value=context class="input" data-test-transit-input="context"}}
+
+
+ {{b64-toggle value=context data-test-transit-b64-toggle="context"}}
+
+
+
+ {{/if}}
+ {{#if key.supportsSigning}}
+
+
+
+
Verification Type
+
+
+
+ {{#each (array 'Signature' 'HMAC') as |type|}}
+
+ {{type}}
+
+ {{/each}}
+
+
+
+
+
+
+
+ Algorithm
+
+
+ {{#unless (eq verification 'HMAC')}}
+
+ {{input id="prehashed" type="checkbox" name="prehashed" class="switch is-rounded is-success is-small" checked=prehashed }}
+ Prehashed
+
+ {{/unless}}
+
+
+
+
+
+ {{#each (sha2-digest-sizes) as |algo|}}
+
+ {{algo}}
+
+ {{/each}}
+
+
+
+
+
+
+ {{#if (or (and verification (eq verification 'HMAC')) hmac)}}
+
+
HMAC
+
+ {{textarea class="textarea" id="hmac" value=hmac}}
+
+
+ {{else}}
+
+
Signature
+
+ {{textarea id="signature" class="textarea" value=signature}}
+
+
+ {{/if}}
+
+
+ {{else}}
+
+
HMAC
+
+ {{textarea class="textarea" id="hmac" value=hmac}}
+
+
+
+
Algorithm
+
+
+
+ {{#each (array 'sha2-224' 'sha2-256' 'sha2-384' 'sha2-512') as |algo|}}
+
+ {{algo}}
+
+ {{/each}}
+
+
+
+
+ {{/if}}
+
+
+ {{/if}}
+
diff --git a/ui/app/templates/components/transit-key-actions.hbs b/ui/app/templates/components/transit-key-actions.hbs
new file mode 100644
index 000000000..2ba1fbdc5
--- /dev/null
+++ b/ui/app/templates/components/transit-key-actions.hbs
@@ -0,0 +1,26 @@
+{{#if (eq selectedAction 'rotate')}}
+ {{#if key.canRotate}}
+
+
+ {{#confirm-action
+ buttonClasses="button"
+ onConfirmAction=(action "doSubmit")
+ confirmMessage=(concat 'Are you sure you want to rotate "' key.id '"?')
+ confirmButtonText="Confirm rotation"
+ cancelButtonText="Cancel"
+ messageClasses="is-block-mobile has-text-grey"
+ data-test-transit-key-rotate=true
+ }}
+ Rotate encryption key
+ {{/confirm-action}}
+
+
+ {{/if}}
+{{else}}
+ {{message-error errors=errors}}
+ {{#if selectedAction}}
+
+ {{partial (concat 'components/transit-key-action/' selectedAction)}}
+
+ {{/if}}
+{{/if}}
diff --git a/ui/app/templates/components/ttl-picker.hbs b/ui/app/templates/components/ttl-picker.hbs
new file mode 100644
index 000000000..ce6ec531f
--- /dev/null
+++ b/ui/app/templates/components/ttl-picker.hbs
@@ -0,0 +1,30 @@
+{{labelText}}
+
+
+
+
+
+
+
+ {{#each unitOptions as |unitOption|}}
+
+ {{unitOption.label}}
+
+ {{/each}}
+
+
+
+
diff --git a/ui/app/templates/components/upgrade-link.hbs b/ui/app/templates/components/upgrade-link.hbs
new file mode 100644
index 000000000..611a67d37
--- /dev/null
+++ b/ui/app/templates/components/upgrade-link.hbs
@@ -0,0 +1,7 @@
+
+ {{yield}}
+
+
+{{#ember-wormhole to="modal-wormhole"}}
+ {{partial "partials/upgrade-overlay"}}
+{{/ember-wormhole}}
diff --git a/ui/app/templates/components/upgrade-page.hbs b/ui/app/templates/components/upgrade-page.hbs
new file mode 100644
index 000000000..c5c9039f7
--- /dev/null
+++ b/ui/app/templates/components/upgrade-page.hbs
@@ -0,0 +1,20 @@
+
+
+
+
+
+ {{featureName}} is a {{minimumEdition}} feature.
+
+ You can upgrade to {{minimumEdition}} to unlock additional collaboration and security features
+
+
+ {{#upgrade-link linkClass="button is-ghost has-icon-right" data-test-upgrade-link="true"}}
+ Vault Enterprise
+ {{i-con glyph="chevron-right"}}
+ {{/upgrade-link}}
+
+
diff --git a/ui/app/templates/loading.hbs b/ui/app/templates/loading.hbs
new file mode 100644
index 000000000..a21f00dc4
--- /dev/null
+++ b/ui/app/templates/loading.hbs
@@ -0,0 +1 @@
+{{logo-splash}}
diff --git a/ui/app/templates/partials/backend-details.hbs b/ui/app/templates/partials/backend-details.hbs
new file mode 100644
index 000000000..65fe91827
--- /dev/null
+++ b/ui/app/templates/partials/backend-details.hbs
@@ -0,0 +1,34 @@
+
+ {{#if backend.description}}
+
+
+
Description
+
+ {{capitalize backend.description}}.
+
+
+
+ {{/if}}
+
+
+
Default TTL
+
+ {{backend.config.default_lease_ttl}}
+
+
+
+
+
+
Max TTL
+
+ {{backend.config.max_lease_ttl}}
+
+
+
+
+
+
Replicated behavior
+ {{backend.localDisplay}}
+
+
+
diff --git a/ui/app/templates/partials/form-field-from-model.hbs b/ui/app/templates/partials/form-field-from-model.hbs
new file mode 100644
index 000000000..43acae746
--- /dev/null
+++ b/ui/app/templates/partials/form-field-from-model.hbs
@@ -0,0 +1,59 @@
+
+ {{#unless (or attr.options.editType (eq attr.type 'boolean'))}}
+
+ {{capitalize (or attr.options.label (humanize (dasherize attr.name)))}}
+ {{#if attr.options.helpText}}
+ {{#info-tooltip}}
+ {{attr.options.helpText}}
+ {{/info-tooltip}}
+ {{/if}}
+
+ {{/unless}}
+ {{#if attr.options.possibleValues}}
+
+
+
+ {{#each attr.options.possibleValues as |val|}}
+
+ {{val}}
+
+ {{/each}}
+
+
+
+ {{else if (eq attr.options.editType 'ttl')}}
+ {{ttl-picker initialValue=(or (get model attr.name) attr.options.defaultValue) labelText=(if attr.options.label attr.options.label (humanize (dasherize attr.name))) setDefaultValue=false onChange=(action (mut (get model attr.name)))}}
+ {{else if (or (eq attr.type 'number') (eq attr.type 'string'))}}
+
+ {{input id=attr.name value=(get model (or attr.options.fieldValue attr.name)) class="input" data-test-input=attr.name}}
+
+ {{else if (eq attr.type 'boolean')}}
+
+
+
+ {{capitalize (or attr.options.label (humanize (dasherize attr.name)))}}
+ {{#if attr.options.helpText}}
+ {{#info-tooltip}}
+ {{attr.options.helpText}}
+ {{/info-tooltip}}
+ {{/if}}
+
+
+ {{else if (eq attr.type 'object')}}
+ {{json-editor
+ value=(if (get model attr.name) (stringify (get model attr.name)) emptyData)
+ valueUpdated=(action "codemirrorUpdated" attr.name)
+ }}
+ {{/if}}
+
diff --git a/ui/app/templates/partials/form-field-groups-loop.hbs b/ui/app/templates/partials/form-field-groups-loop.hbs
new file mode 100644
index 000000000..2509a4073
--- /dev/null
+++ b/ui/app/templates/partials/form-field-groups-loop.hbs
@@ -0,0 +1,27 @@
+{{#each model.fieldGroups as |fieldGroup|}}
+ {{#each-in fieldGroup as |group fields|}}
+ {{#if (eq group "default")}}
+ {{#each fields as |attr|}}
+ {{#unless (and (not-eq mode 'create') (eq attr.name "name"))}}
+ {{form-field data-test-field attr=attr model=model}}
+ {{/unless}}
+ {{/each}}
+ {{else}}
+ {{toggle-button
+ class="is-block"
+ toggleAttr=(concat "show" (camelize group))
+ toggleTarget=this
+ openLabel=(concat "Hide " group)
+ closedLabel=group
+ data-test-toggle-group=group
+ }}
+ {{#if (get this (concat "show" (camelize group)))}}
+
+ {{#each fields as |attr|}}
+ {{form-field data-test-field attr=attr model=model}}
+ {{/each}}
+
+ {{/if}}
+ {{/if}}
+ {{/each-in}}
+{{/each}}
diff --git a/ui/app/templates/partials/loading.hbs b/ui/app/templates/partials/loading.hbs
new file mode 100644
index 000000000..fcf87707a
--- /dev/null
+++ b/ui/app/templates/partials/loading.hbs
@@ -0,0 +1,14 @@
+
diff --git a/ui/app/templates/partials/replication/demote.hbs b/ui/app/templates/partials/replication/demote.hbs
new file mode 100644
index 000000000..5b3013dbf
--- /dev/null
+++ b/ui/app/templates/partials/replication/demote.hbs
@@ -0,0 +1,42 @@
+
+ Demote cluster
+
+
+ {{#if (and
+ (eq replicationMode 'dr')
+ (not model.performance.replicationDisabled)
+ )
+ }}
+
+ Caution : Demoting this DR primary cluster
+ would result in a DR secondary and in that mode Vault is read-only. This
+ cluster is also currently operating as a Performance
+ {{capitalize model.performance.modeForUrl}}, demoting it will leave your
+ replication setup without a performance primary cluster until a new
+ cluster is promoted.
+
+ {{/if}}
+
+ Demote this {{replicationDisplayMode}} primary cluster to a {{replicationDisplayMode}} secondary. The resulting secondary cluster will not
+ attempt to connect to a primary, but will maintain knowledge of its cluster
+ ID and can be reconnected to the same set of replication clusters without
+ wiping local storage.
+
+
+ In order to connect the resulting secondary to a new primary, use the Update primary
action.
+
+
+
+
+ {{#confirm-action
+ buttonClasses="button is-primary"
+ onConfirmAction=(action "onSubmit" "demote" model.replicationAttrs.modeForUrl)
+ confirmMessage="Are you sure you want to demote this cluster?"
+ confirmButtonText="Demote cluster"
+ cancelButtonText="Cancel"
+ data-test-replication-demote=true
+ }}
+ Demote cluster
+ {{/confirm-action}}
+
+
diff --git a/ui/app/templates/partials/replication/disable.hbs b/ui/app/templates/partials/replication/disable.hbs
new file mode 100644
index 000000000..be81e0ca1
--- /dev/null
+++ b/ui/app/templates/partials/replication/disable.hbs
@@ -0,0 +1,66 @@
+
+ Disable replication
+
+
+
+ Disable {{replicationMode}} replication entirely on the cluster.
+ {{#if model.replicationAttrs.isPrimary}}
+ Any secondaries will no longer be able to connect.
+ {{else if (eq model.replicationAttrs.modeForUrl 'bootstrapping')}}
+
+ Since the cluster is currently bootstrapping, we need to know which mode to disable.
+ Be sure to choose it below.
+
+ Replication cluster mode
+
+
+
+
+ {{#each (reduce-to-array 'primary' 'secondary') as |modeOption|}}
+
+ {{modeOption}}
+
+ {{/each}}
+
+
+
+ {{else}}
+ The cluster will no longer be able to connect to the primary.
+ {{/if}}
+
+
+ Caution : re-enabling this node as a primary or secondary will
+ change its cluster ID.
+
+
+ In the secondary case this means a wipe of the
+ underlying storage when connected to a primary, and in the primary case,
+ secondaries connecting back to the cluster (even if they have connected
+ before) will require a wipe of the underlying storage.
+
+
+
+
+ {{#confirm-action
+ onConfirmAction=(action
+ "onSubmit"
+ "disable"
+ (if
+ (eq model.replicationAttrs.modeForUrl 'bootstrapping')
+ mode
+ model.replicationAttrs.modeForUrl
+ )
+ )
+ buttonClasses="button is-primary"
+ confirmMessage=(concat "Are you sure you want to disable replication on this cluster?")
+ confirmButtonText="Disable"
+ cancelButtonText="Cancel"
+ data-test-disable-replication=true
+ }}
+ Disable replication
+ {{/confirm-action}}
+
+
diff --git a/ui/app/templates/partials/replication/enable.hbs b/ui/app/templates/partials/replication/enable.hbs
new file mode 100644
index 000000000..cf91fd2ce
--- /dev/null
+++ b/ui/app/templates/partials/replication/enable.hbs
@@ -0,0 +1,258 @@
+
+
+
+
+ {{message-error errors=errors}}
+ {{#if initialReplicationMode}}
+ {{#if (eq initialReplicationMode 'dr')}}
+
+ {{i-con class="has-text-grey is-medium" glyph="replication" size=24}}
+ Disaster Recovery (DR) Replication
+
+
+ DR is designed to protect against catastrophic failure of entire clusters. Secondaries do not forward service requests (until they are elected and become a new primary).
+
+ {{else if (eq initialReplicationMode 'performance')}}
+
+ {{i-con class="has-text-grey is-medium" glyph="perf-replication" size=20}}
+ Performance Replication
+
+ {{#if (not version.hasPerfReplication)}}
+
+ Performance Replication is a feature of {{#upgrade-link}}Vault Enterprise Premium{{/upgrade-link}}
+
+ {{else}}
+
+ Performance replication scales workloads horizontally across clusters to make requests faster. Local secondaries handle read requests but forward writes to the primary to be handled.
+
+ {{/if}}
+ {{/if}}
+ {{else}}
+
+
+ Type of Replication
+
+ In both Performance and Disaster Recovery (DR) Replication, secondaries share the underlying configuration, policies, and supporting secrets as their primary cluster.
+
+
+
+
+
+
+
+ DR is designed to protect against catastrophic failure of entire clusters. Secondaries do not forward service requests (until they are elected and become a new primary).
+
+
+
+ {{radio-button
+ value="dr"
+ groupValue=replicationMode
+ name="replication-mode"
+ radioId="dr"
+ }}
+
+
+
+
+
+
+
+
+ {{#if (not version.hasPerfReplication)}}
+
+ Performance Replication is a feature of {{#upgrade-link}}Vault Enterprise Premium{{/upgrade-link}}
+
+ {{else}}
+
+ Performance replication scales workloads horizontally across clusters to make requests faster. Local secondaries handle read requests but forward writes to the primary to be handled.
+
+ {{/if}}
+
+
+ {{#if version.hasPerfReplication}}
+ {{radio-button
+ value="performance"
+ groupValue=replicationMode
+ name="replication-mode"
+ classNames="box columns is-centered"
+ radioId="performance"
+ }}
+
+ {{/if}}
+
+
+
+
+ {{/if}}
+
+
+
+ Cluster mode
+
+
+
+
+ {{#each (array 'primary' 'secondary') as |modeOption|}}
+
+ {{modeOption}}
+
+ {{/each}}
+
+
+ {{#if (eq mode 'secondary')}}
+
+ Caution : this will immediately clear all data in this cluster!
+
+ {{/if}}
+
+ {{#if (eq mode 'primary')}}
+ {{#if cluster.canEnablePrimary}}
+
+
+ Primary cluster address (optional)
+
+
+ {{input class="input" id="primary_cluster_addr" name="primary_cluster_addr" value=primary_cluster_addr}}
+
+
+ Overrides the cluster address that the primary gives to secondary nodes.
+
+
+ {{else}}
+
+ The token you are using is not authorized to enable primary replication.
+
+ {{/if}}
+ {{else}}
+ {{#if cluster.canEnableSecondary}}
+ {{#if (and
+ (eq replicationMode 'dr')
+ (not cluster.performance.replicationDisabled)
+ version.hasPerfReplication
+ )
+ }}
+
+ {{toggle-button
+ toggleTarget=this
+ toggleAttr='showExplanation'
+ openLabel="Disable Performance Replication in order to enable this cluster as a DR secondary."
+ closedLabel="Disable Performance Replication in order to enable this cluster as a DR secondary."
+ class="has-text-danger"
+ }}
+ {{#if showExplanation}}
+
+ When running as a DR secondary Vault is read only.
+ For this reason, we don't allow other replication modes to operate at the same time. This cluster is also
+ currently operating as a Performance {{capitalize cluster.performance.modeForUrl}}.
+
+ {{/if}}
+
+ {{else}}
+
+
+ Secondary activation token
+
+
+ {{textarea value=token id="secondary-token" name="secondary-token" class="textarea"}}
+
+
+
+
+ Primary API address {{#unless (and token (not tokenIncludesAPIAddr))}}(optional) {{/unless}}
+
+
+ {{input value=primary_api_addr id="primary_api_addr" name="primary_api_addr" class="input"}}
+
+
+ {{#if (and token (not tokenIncludesAPIAddr))}}
+ The supplied token does not contain an embedded address for the primary cluster. Please enter the primary cluster's API address (normal Vault address).
+ {{else}}
+ Set this to the API address (normal Vault address) to override the
+ value embedded in the token.
+ {{/if}}
+
+
+
+
+ CA file (optional)
+
+
+ {{input value=ca_file id="ca_file" name="ca_file" class="input"}}
+
+
+ Specifies the path to a CA root file (PEM format) that the secondary can use when unwrapping the token from the primary.
+
+
+
+
+ CA path (optional)
+
+
+ {{input value=ca_path id="ca_path" name="ca_file" class="input"}}
+
+
+ Specifies the path to a CA root directory containing PEM-format files that the secondary can use when unwrapping the token from the primary.
+
+
+
+ Note: If both CA file
and CA path
are not given, they default to system CA roots.
+
+ {{/if}}
+ {{else}}
+
The token you are using is not authorized to enable secondary replication.
+ {{/if}}
+ {{/if}}
+
+ {{#if (or (and (eq mode 'primary') cluster.canEnablePrimary) (and (eq mode 'secondary') cluster.canEnableSecondary))}}
+
+
+
+ Enable replication
+
+
+
+ {{/if}}
+
diff --git a/ui/app/templates/partials/replication/mode-summary.hbs b/ui/app/templates/partials/replication/mode-summary.hbs
new file mode 100644
index 000000000..bb8e3d3d0
--- /dev/null
+++ b/ui/app/templates/partials/replication/mode-summary.hbs
@@ -0,0 +1,32 @@
+
+
+
+
+ {{i-con class="has-text-grey is-medium" glyph="replication" size=20}}
+ Disaster Recovery (DR)
+
+ {{replication-mode-summary
+ mode="dr"
+ cluster=cluster
+ tagName=(if cluster.dr.replicationEnabled 'a' 'div')
+ }}
+
+
+
+ {{i-con class="has-text-grey is-medium" glyph="perf-replication" size=20}}
+ Performance
+
+ {{replication-mode-summary
+ mode="performance"
+ cluster=cluster
+ tagName=(if cluster.performance.replicationEnabled 'a' 'div')
+ }}
+
diff --git a/ui/app/templates/partials/replication/promote.hbs b/ui/app/templates/partials/replication/promote.hbs
new file mode 100644
index 000000000..9d0927f33
--- /dev/null
+++ b/ui/app/templates/partials/replication/promote.hbs
@@ -0,0 +1,77 @@
+{{#if (and (eq replicationMode 'dr') (eq model.replicationAttrs.modeForUrl 'secondary'))}}
+
+
+ This cluster is currently running as a DR Replication Secondary.
+ Promote the cluster to a primary by entering DR Operation Token.
+
+
+
+ DR Operation Token
+
+
+ {{input class="input" id="dr_operation_token" name="dr_operation_token" value=dr_operation_token}}
+
+
+
+
+ Primary cluster address (optional)
+
+
+ {{input class="input" id="primary_cluster_addr" name="primary_cluster_addr" value=primary_cluster_addr}}
+
+
+ Overrides the cluster address that the primary gives to secondary nodes.
+
+
+
+
+
+
+ {{#confirm-action
+ buttonClasses="button is-primary"
+ onConfirmAction=(action "onSubmit" "promote" model.replicationAttrs.modeForUrl (hash dr_operation_token=dr_operation_token primary_cluster_addr=primary_cluster_addr))
+ confirmMessage="Are you sure you want to promote this cluster?"
+ confirmButtonText="Promote cluster"
+ cancelButtonText="Cancel"
+ }}
+ Promote cluster
+ {{/confirm-action}}
+
+
+
+{{else}}
+
+ Promote cluster
+
+
+
Promote the cluster to primary.
+
+ Caution : Vault replication is not designed for active-active usage and enabling two primaries should never be done, as it can lead to data loss if they or their secondaries are ever reconnected.
+ If the cluster has a primary, be sure to demote it before promoting a secondary.
+
+
+
+ Primary cluster address (optional)
+
+
+ {{input class="input" id="primary_cluster_addr" name="primary_cluster_addr" value=primary_cluster_addr}}
+
+
+ Overrides the cluster address that the primary gives to secondary nodes.
+
+
+
+
+
+ {{#confirm-action
+ buttonClasses="button is-primary"
+ onConfirmAction=(action "onSubmit" "promote" model.replicationAttrs.modeForUrl (hash primary_cluster_addr=primary_cluster_addr))
+ confirmMessage="Are you sure you want to promote this cluster?"
+ confirmButtonText="Promote cluster"
+ cancelButtonText="Cancel"
+ }}
+ Promote cluster
+ {{/confirm-action}}
+
+
+{{/if}}
diff --git a/ui/app/templates/partials/replication/recover.hbs b/ui/app/templates/partials/replication/recover.hbs
new file mode 100644
index 000000000..603edd50d
--- /dev/null
+++ b/ui/app/templates/partials/replication/recover.hbs
@@ -0,0 +1,23 @@
+
+ Recover
+
+
+
+ Attempt recovery if replication is in a bad state, for instance if an error
+ has caused replication to stop syncing.
+
+
+
+
+ {{#confirm-action
+ buttonClasses="button is-primary"
+ onConfirmAction=(action "onSubmit" "recover")
+ confirmMessage="Are you sure you want to initiate cluster recovery?"
+ confirmButtonText="Begin recovery"
+ cancelButtonText="Cancel"
+ }}
+ Recover
+ {{/confirm-action}}
+
+
+
diff --git a/ui/app/templates/partials/replication/reindex.hbs b/ui/app/templates/partials/replication/reindex.hbs
new file mode 100644
index 000000000..3735a7185
--- /dev/null
+++ b/ui/app/templates/partials/replication/reindex.hbs
@@ -0,0 +1,22 @@
+
+ Reindex
+
+
+
+ Reindex the local data storage. This can cause a very long delay depending
+ on the number and size of objects in the data store.
+
+
+
+
+ {{#confirm-action
+ buttonClasses="button is-primary"
+ onConfirmAction=(action "onSubmit" "reindex")
+ confirmMessage="Are you sure you want to initiate cluster reindex?"
+ confirmButtonText="Begin reindex"
+ cancelButtonText="Cancel"
+ }}
+ Reindex
+ {{/confirm-action}}
+
+
diff --git a/ui/app/templates/partials/replication/replication-mode-summary-menu.hbs b/ui/app/templates/partials/replication/replication-mode-summary-menu.hbs
new file mode 100644
index 000000000..db4d76dc1
--- /dev/null
+++ b/ui/app/templates/partials/replication/replication-mode-summary-menu.hbs
@@ -0,0 +1,38 @@
+
+
+ {{#if (or replicationUnsupported (and (eq mode 'performance') (not version.hasPerfReplication)))}}
+
+ Upgrade to Vault Enterprise Premium to use Performance Replication.
+
+ {{else if replicationEnabled}}
+
+ {{capitalize modeForUrl}}
+
+ {{#if secondaryId}}
+
+
+ {{secondaryId}}
+
+
+ {{/if}}
+
+
+ {{clusterIdDisplay}}
+
+
+ {{else}}
+ Enable
+ {{/if}}
+
+
+
+ {{#if replicationEnabled}}
+ {{#if (get cluster (concat mode 'StateGlyph'))}}
+ {{i-con size=14 glyph=(get cluster (concat mode 'StateGlyph')) class="has-text-info" aria-label=(concat mode 'StateDisplay')}}
+ {{else if syncProgress}}
+
+ {{syncProgress.progress}} of {{syncProgress.total}} keys
+
+ {{/if}}
+ {{/if}}
+
diff --git a/ui/app/templates/partials/replication/replication-mode-summary.hbs b/ui/app/templates/partials/replication/replication-mode-summary.hbs
new file mode 100644
index 000000000..4377a49cd
--- /dev/null
+++ b/ui/app/templates/partials/replication/replication-mode-summary.hbs
@@ -0,0 +1,53 @@
+
+
+ {{#if (and (eq mode 'performance') (not version.hasPerfReplication))}}
+
+ Performance Replication is a feature of Vault Enterprise Premium.
+
+
+ {{#upgrade-link linkClass="button is-ghost has-icon-right"}}
+ Learn more
+ {{i-con glyph="chevron-right"}}
+ {{/upgrade-link}}
+
+ {{else if replicationEnabled}}
+
+ Enabled
+
+
+ {{capitalize modeForUrl}}
+
+ {{#if secondaryId}}
+
+
+ {{secondaryId}}
+
+
+ {{/if}}
+
+
+ {{clusterIdDisplay}}
+
+
+ {{else}}
+
+ {{#if (eq mode 'dr')}}
+ DR is designed to protect against catastrophic failure of entire clusters. Secondaries do not forward service requests (until they are elected and become a new primary).
+ {{else}}
+ Performance replication scales workloads horizontally across clusters to make requests faster. Local secondaries handle read requests but forward writes to the primary to be handled.
+ {{/if}}
+
+ {{/if}}
+
+
+
+ {{#if replicationDisabled}}
+
+ Enable
+
+ {{else if (eq mode 'dr')}}
+ {{cluster.drReplicationStateDisplay}}
+ {{else if (eq mode 'performance')}}
+ {{cluster.perfReplicationStateDisplay}}
+ {{/if}}
+
diff --git a/ui/app/templates/partials/replication/update-primary.hbs b/ui/app/templates/partials/replication/update-primary.hbs
new file mode 100644
index 000000000..bfc372d82
--- /dev/null
+++ b/ui/app/templates/partials/replication/update-primary.hbs
@@ -0,0 +1,140 @@
+{{#if (and (eq replicationMode 'dr') (eq model.replicationAttrs.modeForUrl 'secondary'))}}
+
+
+ Change a secondary cluster’s assigned primary cluster using a secondary
+ activation token. This does not wipe all data in the cluster.
+
+
+
+ DR Operation Token
+
+
+ {{input class="input" id="dr_operation_token" name="dr_operation_token" value=dr_operation_token}}
+
+
+
+
+ Secondary activation token
+
+
+ {{textarea value=token id="secondary-token" name="secondary-token" class="textarea"}}
+
+
+
+
+ Primary API address (optional)
+
+
+ {{input class="input" value=primary_api_addr id="primary_api_addr" name="primary_api_addr"}}
+
+
+ Set this to the API address (normal Vault address) to override the
+ value embedded in the token.
+
+
+
+
+ CA file (optional)
+
+
+ {{input value=ca_file id="ca_file" name="ca_file" class="input"}}
+
+
+ Specifies the path to a CA root file (PEM format) that the secondary can use when unwrapping the token from the primary.
+
+
+
+
+ CA path (optional)
+
+
+ {{input value=ca_path id="ca_path" name="ca_file" class="input"}}
+
+
+ Specifies the path to a CA root directory containing PEM-format files that the secondary can use when unwrapping the token from the primary.
+
+
+
+
+
+
+
+ {{#confirm-action
+ buttonClasses="button is-primary"
+ onConfirmAction=(action "onSubmit" "update-primary" model.replicationAttrs.modeForUrl (hash dr_operation_token=dr_operation_token token=token primary_api_addr=primary_api_addr ca_path=ca_path ca_file=ca_file))
+ confirmMessage="Are you sure you want to update this cluster's primary?"
+ confirmButtonText="Update primary"
+ cancelButtonText="Cancel"
+ }}
+ Update primary
+ {{/confirm-action}}
+
+
+
+{{else}}
+
+ Update primary
+
+
+
+ Change a secondary cluster’s assigned primary cluster using a secondary
+ activation token. This does not wipe all data in the cluster.
+
+
+
+
+ Secondary activation token
+
+
+ {{textarea value=token id="secondary-token" name="secondary-token" class="textarea"}}
+
+
+
+
+ Primary API address (optional)
+
+
+ {{input class="input" value=primary_api_addr id="primary_api_addr" name="primary_api_addr"}}
+
+
+ Set this to the API address (normal Vault address) to override the
+ value embedded in the token.
+
+
+
+
+ CA file (optional)
+
+
+ {{input value=ca_file id="ca_file" name="ca_file" class="input"}}
+
+
+ Specifies the path to a CA root file (PEM format) that the secondary can use when unwrapping the token from the primary.
+
+
+
+
+ CA path (optional)
+
+
+ {{input value=ca_path id="ca_path" name="ca_file" class="input"}}
+
+
+ Specifies the path to a CA root directory containing PEM-format files that the secondary can use when unwrapping the token from the primary.
+
+
+
+
+
+ {{#confirm-action
+ buttonClasses="button is-primary"
+ onConfirmAction=(action "onSubmit" "update-primary" model.replicationAttrs.modeForUrl (hash token=token primary_api_addr=primary_api_addr ca_path=ca_path ca_file=ca_file))
+ confirmMessage="Are you sure you want to update this cluster's primary?"
+ confirmButtonText="Update primary"
+ cancelButtonText="Cancel"
+ }}
+ Update primary
+ {{/confirm-action}}
+
+
+{{/if}}
diff --git a/ui/app/templates/partials/role-aws/form.hbs b/ui/app/templates/partials/role-aws/form.hbs
new file mode 100644
index 000000000..c67565198
--- /dev/null
+++ b/ui/app/templates/partials/role-aws/form.hbs
@@ -0,0 +1,88 @@
+{{message-error model=model}}
+
+
+ {{#if (eq mode 'create')}}
+
+
+ Role Name
+
+
+ {{input id="name" value=model.id class="input" data-test-input="name"}}
+
+
+ {{/if}}
+
+
+
+ {{#if useARN}}
+
+ ARN
+
+ {{else}}
+
+ Policy
+
+ {{/if}}
+
+
+
+ {{input
+ data-test-aws-toggle-use-arn=true
+ id="use-arn"
+ type="checkbox"
+ name="use-arn"
+ class="switch is-rounded is-success is-small"
+ checked=useARN
+ }}
+ Use Amazon Resource Name
+
+
+
+
+ {{#if useARN}}
+ {{input id="arn" value=model.arn class="input" data-test-input="arn"}}
+ {{else}}
+ {{json-editor
+ value=(if model.policy (stringify (jsonify model.policy)) emptyData)
+ valueUpdated=(action "codemirrorUpdated" "policy")
+ }}
+ {{/if}}
+
+
+
+
+
+ {{#if capabilities.canCreate}}
+
+ {{#if (eq mode 'create')}}
+ Create role
+ {{else if (eq mode 'edit')}}
+ Save
+ {{/if}}
+
+ {{/if}}
+ {{#secret-link
+ mode=(if (eq mode "create") "list" "show")
+ class="button"
+ secret=model.id
+ }}
+ Cancel
+ {{/secret-link}}
+
+ {{#if (and (eq mode 'edit') model.canDelete)}}
+ {{#confirm-action
+ buttonClasses="button"
+ onConfirmAction=(action "delete")
+ confirmMessage=(concat "Are you sure you want to delete " model.id "?")
+ cancelButtonText="Cancel"
+ }}
+ Delete
+ {{/confirm-action}}
+ {{/if}}
+
+
diff --git a/ui/app/templates/partials/role-aws/popup-menu.hbs b/ui/app/templates/partials/role-aws/popup-menu.hbs
new file mode 100644
index 000000000..365ba3d55
--- /dev/null
+++ b/ui/app/templates/partials/role-aws/popup-menu.hbs
@@ -0,0 +1,76 @@
+{{#popup-menu name="role-aws-nav" contentClass="is-wide"}}
+
+{{/popup-menu}}
diff --git a/ui/app/templates/partials/role-aws/show.hbs b/ui/app/templates/partials/role-aws/show.hbs
new file mode 100644
index 000000000..9f396d59b
--- /dev/null
+++ b/ui/app/templates/partials/role-aws/show.hbs
@@ -0,0 +1,11 @@
+
+ {{#each model.attrs as |attr|}}
+ {{#if (eq attr.name "policy")}}
+ {{#info-table-row label=(capitalize (or attr.options.label (humanize (dasherize attr.name)))) value=model.policy}}
+
{{stringify (jsonify model.policy)}}
+ {{/info-table-row}}
+ {{else}}
+ {{info-table-row label=(capitalize (or attr.options.label (humanize (dasherize attr.name)))) value=(get model attr.name)}}
+ {{/if}}
+ {{/each}}
+
diff --git a/ui/app/templates/partials/role-pki/form.hbs b/ui/app/templates/partials/role-pki/form.hbs
new file mode 100644
index 000000000..2f3c9e82a
--- /dev/null
+++ b/ui/app/templates/partials/role-pki/form.hbs
@@ -0,0 +1,42 @@
+{{message-error model=model}}
+
+
+ {{partial "partials/form-field-groups-loop"}}
+
+
+
+ {{#if capabilities.canCreate}}
+
+ {{#if (eq mode 'create')}}
+ Create role
+ {{else if (eq mode 'edit')}}
+ Save
+ {{/if}}
+
+ {{/if}}
+ {{#secret-link
+ mode=(if (eq mode "create") "list" "show")
+ class="button"
+ secret=model.id
+ }}
+ Cancel
+ {{/secret-link}}
+
+ {{#if (and (eq mode 'edit') model.canDelete)}}
+ {{#confirm-action
+ data-test-role-delete
+ buttonClasses="button"
+ onConfirmAction=(action "delete")
+ confirmMessage=(concat "Are you sure you want to delete " model.id "?")
+ cancelButtonText="Cancel"
+ }}
+ Delete
+ {{/confirm-action}}
+ {{/if}}
+
+
diff --git a/ui/app/templates/partials/role-pki/popup-menu.hbs b/ui/app/templates/partials/role-pki/popup-menu.hbs
new file mode 100644
index 000000000..3e3d41569
--- /dev/null
+++ b/ui/app/templates/partials/role-pki/popup-menu.hbs
@@ -0,0 +1,76 @@
+{{#popup-menu name="role-aws-nav" contentClass="is-wide"}}
+
+{{/popup-menu}}
diff --git a/ui/app/templates/partials/role-pki/show.hbs b/ui/app/templates/partials/role-pki/show.hbs
new file mode 100644
index 000000000..b5062b0b3
--- /dev/null
+++ b/ui/app/templates/partials/role-pki/show.hbs
@@ -0,0 +1,20 @@
+
+ {{#each model.fieldGroups as |fieldGroup|}}
+ {{#each-in fieldGroup as |group fields|}}
+ {{#if (or (eq group "default") (eq group "Options"))}}
+ {{#each fields as |attr|}}
+ {{info-table-row label=(capitalize (or attr.options.label (humanize (dasherize attr.name)))) value=(get model attr.name)}}
+ {{/each}}
+ {{else}}
+
+
+ {{group}}
+
+ {{#each fields as |attr|}}
+ {{info-table-row alwaysRender=true label=(capitalize (or attr.options.label (humanize (dasherize attr.name)))) value=(get model attr.name)}}
+ {{/each}}
+
+ {{/if}}
+ {{/each-in}}
+ {{/each}}
+
diff --git a/ui/app/templates/partials/role-ssh/form.hbs b/ui/app/templates/partials/role-ssh/form.hbs
new file mode 100644
index 000000000..cd03090df
--- /dev/null
+++ b/ui/app/templates/partials/role-ssh/form.hbs
@@ -0,0 +1,61 @@
+{{message-error model=model}}
+
+
+ {{#with (if (eq model.keyType 'otp') 3 4) as |numRequired|}}
+ {{#each (take numRequired model.attrsForKeyType) as |attr|}}
+ {{#unless (and (eq mode 'edit') (eq attr.name 'name'))}}
+ {{partial "partials/form-field-from-model"}}
+ {{/unless}}
+ {{/each}}
+ {{toggle-button
+ toggleAttr="showOptions"
+ toggleTarget=this
+ openLabel="Hide options"
+ closedLabel="More options"
+ data-test-toggle-more="true"
+ }}
+ {{#if showOptions}}
+
+ {{#each (drop numRequired model.attrsForKeyType) as |attr|}}
+ {{partial "partials/form-field-from-model"}}
+ {{/each}}
+
+ {{/if}}
+ {{/with}}
+
+
+
+ {{#if capabilities.canCreate}}
+
+ {{#if (eq mode 'create')}}
+ Create role
+ {{else if (eq mode 'edit')}}
+ Save
+ {{/if}}
+
+ {{/if}}
+ {{#secret-link
+ mode=(if (eq mode "create") "list" "show")
+ class="button"
+ secret=model.id
+ }}
+ Cancel
+ {{/secret-link}}
+
+ {{#if (and (eq mode 'edit') model.canDelete)}}
+ {{#confirm-action
+ buttonClasses="button"
+ onConfirmAction=(action "delete")
+ confirmMessage=(concat "Are you sure you want to delete " model.id "?")
+ cancelButtonText="Cancel"
+ }}
+ Delete
+ {{/confirm-action}}
+ {{/if}}
+
+
diff --git a/ui/app/templates/partials/role-ssh/popup-menu.hbs b/ui/app/templates/partials/role-ssh/popup-menu.hbs
new file mode 100644
index 000000000..d90b47444
--- /dev/null
+++ b/ui/app/templates/partials/role-ssh/popup-menu.hbs
@@ -0,0 +1,95 @@
+{{#popup-menu name="role-ssh-nav"}}
+
+{{/popup-menu}}
diff --git a/ui/app/templates/partials/role-ssh/show.hbs b/ui/app/templates/partials/role-ssh/show.hbs
new file mode 100644
index 000000000..0caee6f21
--- /dev/null
+++ b/ui/app/templates/partials/role-ssh/show.hbs
@@ -0,0 +1,9 @@
+
+ {{#each model.attrsForKeyType as |attr|}}
+ {{#if (eq attr.type "object")}}
+ {{info-table-row label=(capitalize (or attr.options.label (humanize (dasherize attr.name)))) value=(stringify (get model attr.name))}}
+ {{else}}
+ {{info-table-row label=(capitalize (or attr.options.label (humanize (dasherize attr.name)))) value=(get model attr.name)}}
+ {{/if}}
+ {{/each}}
+
diff --git a/ui/app/templates/partials/secret-backend-settings/aws.hbs b/ui/app/templates/partials/secret-backend-settings/aws.hbs
new file mode 100644
index 000000000..ac23e95db
--- /dev/null
+++ b/ui/app/templates/partials/secret-backend-settings/aws.hbs
@@ -0,0 +1,120 @@
+
+
+
+ {{#link-to "vault.cluster.settings.configure-secret-backend" model.id (query-params tab='') tagName="li"}}
+
+ Dynamic IAM Root Credentials
+
+ {{/link-to}}
+
+ {{#link-to "vault.cluster.settings.configure-secret-backend" model.id (query-params tab='leases') tagName="li"}}
+
+ Leases
+
+ {{/link-to}}
+
+
+
+{{#if (eq tab "leases")}}
+
+
+ {{message-error model=model}}
+
+ If you do not supply lease settings, we will use the default values in AWS.
+
+
+ {{ttl-picker labelText="Lease" initialValue=model.lease onChange=(action (mut model.lease))}}
+ {{ttl-picker labelText="Maximum Lease" initialValue=model.leaseMax onChange=(action (mut model.leaseMax))}}
+
+
+ Save
+
+
+
+{{else}}
+
+
+
+ Note: the client uses the official AWS SDK and will use the specified credentials, environment credentials, shared file credentials, or IAM role/ECS task credentials in that order.
+
+
+
+
+
+ Access Key
+
+
+ {{input type="text" id="access" name="access" class="input" value=accessKey data-test-aws-input="accessKey"}}
+
+
+
+
+
+ Secret Key
+
+
+ {{input type="text" id="secret" name="secret" class="input" value=secretKey data-test-aws-input="secretKey"}}
+
+
+
+ {{toggle-button
+ toggleAttr="showOptions"
+ toggleTarget=this
+ openLabel="Hide options"
+ closedLabel="More options"
+ }}
+ {{#if showOptions}}
+
+
+
+ Region
+
+
+
+
+
+ {{#each (aws-regions) as |val|}}
+ {{val}}
+ {{/each}}
+
+
+
+
+
+
+ IAM Endpoint
+
+
+ {{input type="text" id="iam" name="iam" class="input" value=iamEndpoint}}
+
+
+
+
+ STS Endpoint
+
+
+ {{input type="text" id="sts" name="sts" class="input" value=stsEndpoint}}
+
+
+
+ {{/if}}
+
+
+
+ Save
+
+
+
+{{/if}}
+
diff --git a/ui/app/templates/partials/secret-backend-settings/pki.hbs b/ui/app/templates/partials/secret-backend-settings/pki.hbs
new file mode 100644
index 000000000..b9a3d030a
--- /dev/null
+++ b/ui/app/templates/partials/secret-backend-settings/pki.hbs
@@ -0,0 +1,21 @@
+
+
+
+ {{#each (array 'cert' 'urls' 'crl' 'tidy') as |section|}}
+ {{#link-to 'vault.cluster.settings.configure-secret-backend.section' section tagName="li" activeClass="is-active"}}
+ {{#link-to 'vault.cluster.settings.configure-secret-backend.section' section}}
+ {{#if (eq section 'cert')}}
+ CA Certificate
+ {{else if (eq section 'urls')}}
+ URLs
+ {{else if (eq section 'crl')}}
+ CRL
+ {{else if (eq section 'tidy')}}
+ Tidy
+ {{/if}}
+ {{/link-to}}
+ {{/link-to}}
+ {{/each}}
+
+
+
diff --git a/ui/app/templates/partials/secret-backend-settings/ssh.hbs b/ui/app/templates/partials/secret-backend-settings/ssh.hbs
new file mode 100644
index 000000000..72f7dfea7
--- /dev/null
+++ b/ui/app/templates/partials/secret-backend-settings/ssh.hbs
@@ -0,0 +1,84 @@
+{{#if configured}}
+
+
+
+ Public Key
+
+
+ {{textarea
+ name="publicKey"
+ id="publicKey"
+ class="textarea"
+ value=model.publicKey
+ readonly=true
+ data-test-ssh-input="public-key"
+ }}
+
+
+
+
+
+ {{#copy-button
+ clipboardTarget="#publicKey"
+ class="button is-primary"
+ buttonType="button"
+ success=(action (set-flash-message "Public Key copied!"))
+ }}
+ Copy
+ {{/copy-button}}
+
+
+ {{#confirm-action
+ buttonClasses="button"
+ onConfirmAction=(action "saveConfig" (hash delete=true))
+ confirmMessage=(concat "This will delete the current CA keys associated with this backend. Are you sure you want to delete them?")
+ cancelButtonText="Cancel"
+ }}
+ Delete
+ {{/confirm-action}}
+
+
+{{else}}
+
+
+
+
+ Private Key
+
+
+ {{textarea name="privateKey" id="privateKey" class="input" value=model.privateKey}}
+
+
+
+
+ Public Key
+
+
+ {{textarea name="publicKey" id="publicKey" class="input" value=model.publicKey}}
+
+
+
+
+
+ Generate Signing Key
+ {{#info-tooltip}}
+ Specifies if Vault should generate the signing key pair internally
+ {{/info-tooltip}}
+
+
+
+
+
+{{/if}}
diff --git a/ui/app/templates/partials/secret-edit-display.hbs b/ui/app/templates/partials/secret-edit-display.hbs
new file mode 100644
index 000000000..7f44152de
--- /dev/null
+++ b/ui/app/templates/partials/secret-edit-display.hbs
@@ -0,0 +1,52 @@
+{{#unless key.isFolder}}
+ {{#if showAdvancedMode}}
+ {{json-editor
+ value=codemirrorString
+ valueUpdated=(action "codemirrorUpdated")
+ onFocusOut=(action "formatJSON")
+ }}
+ {{else}}
+ {{#each secretData as |secret index|}}
+
+
+ {{input
+ data-test-secret-key=true
+ value=secret.name
+ placeholder="key"
+ change="handleChange"
+ class="input"
+ }}
+
+
+ {{textarea
+ data-test-secret-value=true
+ name=secret.name
+ key-down="handleKeyDown"
+ change="handleChange"
+ value=secret.value
+ wrap="off"
+ class="input"
+ placeholder="value"
+ rows=1
+ }}
+
+
+ {{#if (eq secretData.length (inc index))}}
+
+ Add
+
+ {{else}}
+
+ {{i-con size=22 glyph='trash-a' excludeIconClass=true class="is-large has-text-grey-light"}}
+
+ {{/if}}
+
+
+ {{/each}}
+ {{/if}}
+{{/unless}}
diff --git a/ui/app/templates/partials/secret-form-create.hbs b/ui/app/templates/partials/secret-form-create.hbs
new file mode 100644
index 000000000..2f858f587
--- /dev/null
+++ b/ui/app/templates/partials/secret-form-create.hbs
@@ -0,0 +1,58 @@
+{{message-error model=key}}
+
+
+
Path for this secret
+
+ {{#if (not-eq key.initialParentKey '') }}
+ {{! need this to prevent a shift in the layout before we transition when saving }}
+ {{#if key.isCreating}}
+
+
+ {{key.initialParentKey}}
+
+
+ {{else}}
+
+
+ {{key.parentKey}}
+
+
+ {{/if}}
+ {{/if}}
+
+ {{input data-test-secret-path=true id="kv-key" class="input" value=key.keyWithoutParent}}
+
+
+ {{#if key.isFolder}}
+
+ The secret path may not end in /
+
+ {{/if}}
+
+
+ {{partial "partials/secret-edit-display"}}
+
+
+
+ {{#if capabilities.canCreate}}
+
+ Save
+
+ {{/if}}
+
+
+ {{#secret-link
+ mode="list"
+ secret=key.initialParentKey
+ class="button"
+ }}
+ Cancel
+ {{/secret-link}}
+
+
+
diff --git a/ui/app/templates/partials/secret-form-edit.hbs b/ui/app/templates/partials/secret-form-edit.hbs
new file mode 100644
index 000000000..fabb751fa
--- /dev/null
+++ b/ui/app/templates/partials/secret-form-edit.hbs
@@ -0,0 +1,96 @@
+{{#if key.isError}}
+ {{#each key.adapterError.errors as |error|}}
+
+ {{error}}
+
+ {{/each}}
+{{/if}}
+
+
+ {{#if key.didError}}
+
+
+ We were unable to save your changes because the key has changed since the page loaded. Reload the page or click the button below to try again.
+
+
+ Reload {{key.id}}
+
+
+ {{/if}}
+
+ {{#unless showAdvancedMode}}
+
+ {{/unless}}
+ {{#if capabilities.canUpdate}}
+ {{partial "partials/secret-edit-display"}}
+ {{else}}
+
+
+
Your current token does not have capabilities to update this secret.
+
+ {{#secret-link
+ mode=(if key.isFolder "list" "show")
+ secret=key.id
+ class="button"
+ }}
+ Back to {{key.id}}
+ {{/secret-link}}
+
+
+
+ {{/if}}
+
+
+
+ {{#unless key.isFolder}}
+ {{#if capabilities.canUpdate}}
+
+
+ Save
+
+
+ {{/if}}
+ {{/unless}}
+
+ {{#secret-link
+ mode=(if key.isFolder "list" "show")
+ secret=key.id
+ class="button"
+ }}
+ Cancel
+ {{/secret-link}}
+
+
+ {{#if capabilities.canDelete}}
+ {{#confirm-action
+ buttonClasses="button"
+ onConfirmAction=(action "deleteKey")
+ confirmMessage=(if key.isFolder
+ (concat "Are you sure you want to delete " key.id " and all its contents?")
+ (concat "Are you sure you want to delete " key.id "?")
+ )
+ cancelButtonText="Cancel"
+ data-test-secret-delete="true"
+ }}
+ {{#if key.isFolder}}
+ Delete folder
+ {{else}}
+ Delete secret
+ {{/if}}
+ {{/confirm-action}}
+ {{/if}}
+
+
diff --git a/ui/app/templates/partials/secret-form-show.hbs b/ui/app/templates/partials/secret-form-show.hbs
new file mode 100644
index 000000000..6622ff48f
--- /dev/null
+++ b/ui/app/templates/partials/secret-form-show.hbs
@@ -0,0 +1,22 @@
+{{#if showAdvancedMode}}
+ {{json-editor
+ value=key.dataAsJSONString
+ options=(hash
+ readOnly=true
+ )
+ }}
+{{else}}
+
+ {{#each-in key.secretData as |key value|}}
+ {{info-table-row label=key value=value alwaysRender=true}}
+ {{/each-in}}
+{{/if}}
diff --git a/ui/app/templates/partials/secret-list/aws-role-item.hbs b/ui/app/templates/partials/secret-list/aws-role-item.hbs
new file mode 100644
index 000000000..9a83e7149
--- /dev/null
+++ b/ui/app/templates/partials/secret-list/aws-role-item.hbs
@@ -0,0 +1,37 @@
+{{#linked-block
+ (concat
+ "vault.cluster.secrets.backend."
+ "credentials"
+ (if (not item.id) "-root")
+ )
+ item.id
+ class="box is-sideless is-marginless"
+ data-test-secret-link=item.id
+}}
+
+
+ {{#link-to
+ (concat
+ "vault.cluster.secrets.backend."
+ "credentials"
+ (if (not item.id) "-root")
+ )
+ item.id
+ class="has-text-black has-text-weight-semibold"
+ }}
+ {{i-con
+ glyph="role"
+ size=14
+ class="has-text-grey-light is-pulled-left"
+ }}
+
+ {{if (eq item.id ' ') '(self)' (or item.keyWithoutParent item.id)}}
+
+ {{/link-to}}
+
+
+ {{partial 'partials/role-pki/popup-menu'}}
+
+
+{{/linked-block}}
+
diff --git a/ui/app/templates/partials/secret-list/item.hbs b/ui/app/templates/partials/secret-list/item.hbs
new file mode 100644
index 000000000..8baa37315
--- /dev/null
+++ b/ui/app/templates/partials/secret-list/item.hbs
@@ -0,0 +1,28 @@
+{{#linked-block
+ (concat
+ "vault.cluster.secrets.backend."
+ (if item.isFolder "list" "show")
+ (if (not item.id) "-root")
+ )
+ item.id
+ class="box is-sideless is-marginless"
+ data-test-secret-link=item.id
+}}
+
+
+ {{#secret-link
+ mode=(if item.isFolder "list" "show")
+ secret=item.id
+ class="has-text-black has-text-weight-semibold"
+ }}
+ {{i-con
+ glyph=(if item.isFolder 'folder' 'file')
+ size=14
+ class="has-text-grey-light"
+ }}{{if (eq item.id ' ') '(self)' (or item.keyWithoutParent item.id)}}
+ {{/secret-link}}
+
+
+
+
+{{/linked-block}}
diff --git a/ui/app/templates/partials/secret-list/pki-cert-item.hbs b/ui/app/templates/partials/secret-list/pki-cert-item.hbs
new file mode 100644
index 000000000..d2a32e6ec
--- /dev/null
+++ b/ui/app/templates/partials/secret-list/pki-cert-item.hbs
@@ -0,0 +1,37 @@
+{{#linked-block
+ (concat
+ "vault.cluster.secrets.backend."
+ "show"
+ (if (not item.id) "-root")
+ )
+ item.idForNav
+ class="box is-sideless is-marginless linked-block"
+ data-test-secret-link=item.id
+ tagName="div"
+}}
+
+
+ {{#link-to
+ (concat
+ "vault.cluster.secrets.backend."
+ "show"
+ (if (not item.id) "-root")
+ )
+ item.idForNav
+ class="has-text-black has-text-weight-semibold"
+ }}
+ {{i-con
+ glyph="file"
+ size=14
+ class="has-text-grey-light is-pulled-left"
+ }}
+
+ {{if (eq item.id " ") "(self)" (or item.keyWithoutParent item.id)}}
+
+ {{/link-to}}
+
+
+ {{pki-cert-popup item=item}}
+
+
+{{/linked-block}}
diff --git a/ui/app/templates/partials/secret-list/pki-role-item.hbs b/ui/app/templates/partials/secret-list/pki-role-item.hbs
new file mode 100644
index 000000000..7c5624185
--- /dev/null
+++ b/ui/app/templates/partials/secret-list/pki-role-item.hbs
@@ -0,0 +1,40 @@
+{{#linked-block
+ (concat
+ "vault.cluster.secrets.backend."
+ "credentials"
+ (if (not item.id) "-root")
+ )
+ item.backend
+ item.id
+ queryParams=(hash action="issue")
+ class="box is-sideless is-marginless"
+ data-test-secret-link=item.id
+ tagName="div"
+}}
+
+
+ {{#link-to
+ (concat
+ "vault.cluster.secrets.backend."
+ "credentials"
+ (if (not item.id) "-root")
+ )
+ item.id
+ (query-params action="issue")
+ class="has-text-black has-text-weight-semibold"
+ }}
+ {{i-con
+ glyph="role"
+ size=14
+ class="has-text-grey-light is-pulled-left"
+ }}
+
+ {{if (eq item.id " ") "(self)" (or item.keyWithoutParent item.id)}}
+
+ {{/link-to}}
+
+
+ {{partial "partials/role-pki/popup-menu"}}
+
+
+{{/linked-block}}
diff --git a/ui/app/templates/partials/secret-list/ssh-role-item.hbs b/ui/app/templates/partials/secret-list/ssh-role-item.hbs
new file mode 100644
index 000000000..0928ab6ae
--- /dev/null
+++ b/ui/app/templates/partials/secret-list/ssh-role-item.hbs
@@ -0,0 +1,44 @@
+{{#linked-block
+ (concat
+ "vault.cluster.secrets.backend."
+ (if (eq item.keyType "ca") "sign" "credentials")
+ (if (not item.id) "-root")
+ )
+ item.id
+ class="box is-sideless is-marginless"
+ data-test-secret-link=item.id
+}}
+
+
+ {{#link-to
+ (concat
+ "vault.cluster.secrets.backend."
+ (if (eq item.keyType "ca") "sign" "credentials")
+ (if (not item.id) "-root")
+ )
+ item.id
+ class="has-text-black has-text-weight-semibold"
+ }}
+ {{i-con
+ glyph="role"
+ size=14
+ class="has-text-grey-light is-pulled-left"
+ }}
+
+ {{if (eq item.id ' ') '(self)' (or item.keyWithoutParent item.id)}}
+
+ {{item.keyType}}
+ {{#if item.zeroAddress}}
+ Zero-Address
+ {{/if}}
+
+ {{/link-to}}
+
+
+ {{#if (eq backendType 'ssh')}}
+ {{partial 'partials/role-ssh/popup-menu'}}
+ {{/if}}
+
+
+{{/linked-block}}
+
diff --git a/ui/app/templates/partials/status/cluster.hbs b/ui/app/templates/partials/status/cluster.hbs
new file mode 100644
index 000000000..91d0c7b5b
--- /dev/null
+++ b/ui/app/templates/partials/status/cluster.hbs
@@ -0,0 +1,12 @@
+
+
+
+
+
If you suspect your data has been compromised, you can seal your vault to prevent access to your secrets.
+
+
+
diff --git a/ui/app/templates/partials/status/replication.hbs b/ui/app/templates/partials/status/replication.hbs
new file mode 100644
index 000000000..be58af87e
--- /dev/null
+++ b/ui/app/templates/partials/status/replication.hbs
@@ -0,0 +1,28 @@
+
+
+
diff --git a/ui/app/templates/partials/status/user.hbs b/ui/app/templates/partials/status/user.hbs
new file mode 100644
index 000000000..575aaeab2
--- /dev/null
+++ b/ui/app/templates/partials/status/user.hbs
@@ -0,0 +1,3 @@
+{{#if (and cluster.name auth.currentToken)}}
+ {{auth-info activeClusterName=cluster.name}}
+{{/if}}
diff --git a/ui/app/templates/partials/tools/hash.hbs b/ui/app/templates/partials/tools/hash.hbs
new file mode 100644
index 000000000..6c93f454e
--- /dev/null
+++ b/ui/app/templates/partials/tools/hash.hbs
@@ -0,0 +1,100 @@
+
+
+{{#if sum}}
+
+
+
+ {{#copy-button
+ clipboardText=sum
+ class="button is-primary"
+ buttonType="button"
+ success=(action (set-flash-message 'Hashed data copied!'))
+ }}
+ Copy
+ {{/copy-button}}
+
+
+
+ Back
+
+
+
+{{else}}
+
+
+
+ Input
+
+
+ {{textarea id="input" name="input" value=input class="textarea" data-test-tools-input="hash-input"}}
+ {{b64-toggle value=input isInput=false data-test-tools-b64-toggle="input"}}
+
+
+
+
+
+
Algorithm
+
+
+
+ {{#each (sha2-digest-sizes) as |algo|}}
+
+ {{algo}}
+
+ {{/each}}
+
+
+
+
+
+
Output format
+
+
+
+ {{#each (reduce-to-array 'base64' 'hex') as |formatOption|}}
+
+ {{formatOption}}
+
+ {{/each}}
+
+
+
+
+
+
+
+
+{{/if}}
diff --git a/ui/app/templates/partials/tools/lookup.hbs b/ui/app/templates/partials/tools/lookup.hbs
new file mode 100644
index 000000000..875281d3d
--- /dev/null
+++ b/ui/app/templates/partials/tools/lookup.hbs
@@ -0,0 +1,49 @@
+
+
+{{#if (or creation_time creation_ttl)}}
+
+ {{info-table-row label="Creation time" value=creation_time data-test-tools="token-lookup-row"}}
+ {{info-table-row label="Creation TTL" value=creation_ttl data-test-tools="token-lookup-row"}}
+ {{#if expirationDate}}
+ {{info-table-row label="Expiration date" value=expirationDate data-test-tools="token-lookup-row"}}
+ {{info-table-row label="Expires in" value=(moment-from-now expirationDate interval=1000 hideSuffix=true) data-test-tools="token-lookup-row"}}
+ {{/if}}
+
+
+{{else}}
+
+
+
Wrapping token
+
+ {{input
+ value=token
+ class="input"
+ id="token"
+ name="token"
+ data-test-tools-input="wrapping-token"
+ }}
+
+
+
+
+{{/if}}
diff --git a/ui/app/templates/partials/tools/random.hbs b/ui/app/templates/partials/tools/random.hbs
new file mode 100644
index 000000000..709aea780
--- /dev/null
+++ b/ui/app/templates/partials/tools/random.hbs
@@ -0,0 +1,79 @@
+
+
+{{#if random_bytes}}
+
+ Random bytes
+ {{random_bytes}}
+
+
+
+ {{#copy-button
+ clipboardText=random_bytes
+ class="button is-primary"
+ buttonType="button"
+ success=(action (set-flash-message 'Random bytes copied!'))
+ }}
+ Copy
+ {{/copy-button}}
+
+
+
+ Back
+
+
+
+{{else}}
+
+
+
+
+
+ Number of bytes
+
+
+ {{input id="bytes" class="input" value=bytes data-test-tools-input="bytes"}}
+
+
+
+
+ Output format
+
+
+
+
+ {{#each (array 'base64' 'hex') as |formatOption|}}
+
+ {{formatOption}}
+
+ {{/each}}
+
+
+
+
+
+
+
+
+{{/if}}
diff --git a/ui/app/templates/partials/tools/rewrap.hbs b/ui/app/templates/partials/tools/rewrap.hbs
new file mode 100644
index 000000000..e3bb30c89
--- /dev/null
+++ b/ui/app/templates/partials/tools/rewrap.hbs
@@ -0,0 +1,66 @@
+
+
+{{#if rewrap_token}}
+
+
+
Rewrapped token
+
+ {{textarea
+ value=rewrap_token
+ readonly=true
+ class="textarea"
+ id="wrap-info"
+ name="wrap-info"
+ data-test-tools-input="rewrapped-token"
+ }}
+
+
+
+
+
+ {{#copy-button
+ clipboardText=rewrap_token
+ class="button is-primary"
+ buttonType="button"
+ success=(action (set-flash-message 'Token copied!'))
+ }}
+ Copy
+ {{/copy-button}}
+
+
+
+ Back
+
+
+
+{{else}}
+
+
+
Wrapping token
+
+ {{input
+ value=token
+ class="input"
+ id="token"
+ name="token"
+ data-test-tools-input="wrapping-token"
+ }}
+
+
+
+
+{{/if}}
diff --git a/ui/app/templates/partials/tools/unwrap.hbs b/ui/app/templates/partials/tools/unwrap.hbs
new file mode 100644
index 000000000..fb1ecf428
--- /dev/null
+++ b/ui/app/templates/partials/tools/unwrap.hbs
@@ -0,0 +1,66 @@
+
+
+{{#if unwrap_data}}
+
+
+
+ Unwrapped data
+
+
+ {{json-editor
+ value=(stringify unwrap_data)
+ options=(hash
+ readOnly=true
+ )
+ }}
+
+
+
+
+
+ {{#copy-button
+ clipboardText=(stringify unwrap_data)
+ class="button is-primary"
+ buttonType="button"
+ success=(action (set-flash-message 'Data copied!'))
+ }}
+ Copy
+ {{/copy-button}}
+
+
+
+ Back
+
+
+
+{{else}}
+
+
+
Wrapping token
+
+ {{input
+ value=token
+ class="input"
+ id="token"
+ name="token"
+ data-test-tools-input="wrapping-token"
+ }}
+
+
+
+
+{{/if}}
diff --git a/ui/app/templates/partials/tools/wrap.hbs b/ui/app/templates/partials/tools/wrap.hbs
new file mode 100644
index 000000000..47643ca97
--- /dev/null
+++ b/ui/app/templates/partials/tools/wrap.hbs
@@ -0,0 +1,64 @@
+
+
+{{#if token}}
+
+
+
Wrapping token
+
+ {{textarea
+ value=token
+ readonly=true
+ class="textarea"
+ id="wrap-info"
+ name="wrap-info"
+ data-test-tools-input="wrapping-token"
+ }}
+
+
+
+
+
+ {{#copy-button
+ clipboardText=token
+ class="button is-primary"
+ buttonType="button"
+ success=(action (set-flash-message 'Token copied!'))
+ }}
+ Copy
+ {{/copy-button}}
+
+
+
+ Back
+
+
+
+{{else}}
+
+
+
Data to wrap (json-formatted)
+
+ {{json-editor
+ value=data
+ valueUpdated=(action "codemirrorUpdated")
+ }}
+
+
+ {{ttl-picker labelText='Wrap TTL' onChange=(action (mut wrapTTL))}}
+
+
+{{/if}}
diff --git a/ui/app/templates/partials/transit-form-create.hbs b/ui/app/templates/partials/transit-form-create.hbs
new file mode 100644
index 000000000..5d90a8fc3
--- /dev/null
+++ b/ui/app/templates/partials/transit-form-create.hbs
@@ -0,0 +1,118 @@
+{{message-error model=key}}
+
+
+
+ Name
+ {{input id="key-name" value=key.id class="input" data-test-transit-key-name=true}}
+
+
+
Type
+
+
+
+
+ aes256-gcm96
+
+
+ chacha20-poly1305
+
+
+ ecdsa-p256
+
+
+ ed25519
+
+
+ rsa-2048
+
+
+ rsa-4096
+
+
+
+
+
+
+ {{#if (or
+ (eq key.type "aes256-gcm96")
+ (eq key.type "chacha20-poly1305")
+ (eq key.type "ed25519")
+ )
+ }}
+
+ {{/if}}
+ {{#if (or
+ (eq key.type "aes256-gcm96")
+ (eq key.type "chacha20-poly1305")
+ )
+ }}
+
+
+
+
+ Enable convergent encryption
+
+
+
+ {{/if}}
+
+
+ {{#if capabilities.canCreate}}
+
+
+ Create encryption key
+
+
+ {{/if}}
+
+ {{#secret-link
+ mode="list"
+ class="button"
+ }}
+ Cancel
+ {{/secret-link}}
+
+
+
diff --git a/ui/app/templates/partials/transit-form-edit.hbs b/ui/app/templates/partials/transit-form-edit.hbs
new file mode 100644
index 000000000..ba79137b2
--- /dev/null
+++ b/ui/app/templates/partials/transit-form-edit.hbs
@@ -0,0 +1,103 @@
+{{message-error model=key}}
+
+
+
+
+
+
+ Allow deletion
+
+
+
+
+
Minimum decryption version
+
+
+
+ {{#each key.keyVersions as |version|}}
+
+ {{version}}
+
+ {{/each}}
+
+
+
+
+ The minimum decryption version required to reverse transformations performed with the encryption key. Results from lower key versions
+ may be rewrapped with the new key version using the rewrap
action.
+
+
+
+
Minimum encryption version
+
+
+
+
+ Latest
(currently {{key.latestVersion}})
+
+ {{#each key.encryptionKeyVersions as |version|}}
+
+ {{version}}
+
+ {{/each}}
+
+
+
+
+ The minimum version of the key that can be used to encrypt plaintext, sign payloads, or generate HMACs.
+ You will be able to specify which version of the key to use for each of these actions. The value specified here must be greater than or equal to that specified in the Minimum Decryption Version selection above.
+
+
+
+
+
+ {{#if capabilities.canUpdate}}
+
+
+ Update encryption key
+
+
+ {{/if}}
+
+ {{#secret-link
+ mode="show"
+ secret=key.id
+ class="button"
+ }}
+ Cancel
+ {{/secret-link}}
+
+
+ {{#if (and key.canDelete capabilities.canDelete)}}
+ {{#confirm-action
+ buttonClasses="button"
+ onConfirmAction=(action "deleteKey")
+ confirmMessage=(concat "Are you sure you want to delete " key.id "?")
+ cancelButtonText="Cancel"
+ }}
+ Delete encryption key
+ {{/confirm-action}}
+ {{/if}}
+
+
diff --git a/ui/app/templates/partials/transit-form-show.hbs b/ui/app/templates/partials/transit-form-show.hbs
new file mode 100644
index 000000000..4996a065a
--- /dev/null
+++ b/ui/app/templates/partials/transit-form-show.hbs
@@ -0,0 +1,177 @@
+
+
+
+
+ {{#secret-link
+ secret=key.id
+ mode="show"
+ replace=true
+ data-test-transit-link="details"
+ }}
+ Details
+ {{/secret-link}}
+
+
+
+ {{#secret-link
+ secret=key.id
+ mode="show"
+ replace=true
+ queryParams=(query-params tab='versions')
+ data-test-transit-link="versions"
+ }}
+ Versions
+ {{/secret-link}}
+
+
+
+
+
+{{#if (eq tab 'versions')}}
+ {{#if (or
+ (eq key.type "aes256-gcm96")
+ (eq key.type "chacha20-poly1305")
+ )
+ }}
+
+ {{#each-in key.keys as |version creationTimestamp|}}
+
+
+
+ {{version}}
+ {{#if (coerce-eq key.minDecryptionVersion version)}}
+
(current minimum decryption version)
+ {{/if}}
+
+
+
+
+ {{moment-format (unix creationTimestamp) 'MMM DD, YYYY hh:mm:ss A'}}
+
+
+ {{moment-format (unix creationTimestamp) }}
+
+
+
+
+ {{/each-in}}
+ {{else}}
+
+ {{#each-in key.keys as |version meta|}}
+
+
+
+
+
+
+
+ {{version}}
+ {{#if (coerce-eq key.minDecryptionVersion version)}}
+
(current minimum decryption version)
+ {{/if}}
+
+
+
+
+
+
+ {{moment-format meta.creation_time 'MMM DD, YYYY hh:mm:ss A'}}
+
+
+ {{moment-format meta.creation_time}}
+
+
+
+
+
+
+
+
+ {{i-con glyph="more" size=18 aria-label=(concat backend.path ' details')}}
+
+
+
+
+
+ {{#if (get this (concat version '-open'))}}
+
+
+
+
+ Public Key
+
+
{{meta.public_key}}
+
+
+ {{#copy-button
+ clipboardText=meta.public_key
+ class="button"
+ buttonType="button"
+ success=(action (set-flash-message (concat 'Public key for version ' version ' copied!')))
+ }}
+ Copy
+ {{/copy-button}}
+
+
+
+
+
+ {{/if}}
+ {{/each-in}}
+ {{/if}}
+
+ {{transit-key-actions
+ key=key
+ selectedAction='rotate'
+ capabilities=capabilities
+ }}
+
+{{else}}
+ {{info-table-row label="Type" value=key.type}}
+ {{info-table-row label="Deletion allowed" value=(stringify key.deletionAllowed)}}
+
+ {{#if key.derived}}
+ {{info-table-row label="Derived" value=key.derived}}
+ {{info-table-row label="Convergent encryption" value=key.convergentEncryption}}
+ {{/if}}
+{{/if}}
diff --git a/ui/app/templates/partials/upgrade-overlay.hbs b/ui/app/templates/partials/upgrade-overlay.hbs
new file mode 100644
index 000000000..678a92de2
--- /dev/null
+++ b/ui/app/templates/partials/upgrade-overlay.hbs
@@ -0,0 +1,78 @@
+
+
+
+
+ Vault Enterprise
+
+
+ Collaborate on secrets management and access
+
+
+ Vault Enterprise has features to help unify disparate users and roles,
+ use collaboration workflows, and disaster recovery for system recovery,
+ provide governance over secrets management and access with multi-factor
+ authentication. Choose the plan that is right for your team.
+
+
+
+
+
+
+
+ Pro
+ {{#unless (is-version "OSS")}}
+ {{#unless version.hasPerfReplication}}
+ {{edition-badge edition="Current"}}
+ {{/unless}}
+ {{/unless}}
+
+
+
+ {{partial "svg/edition-icon-pro"}}
+
+
+
+ All Open Source features
+ Disaster Recovery Replication
+ Cluster management
+ Init and unseal workflow
+ GCP Cloud KMS Auto-unseal
+ Silver support: 9x5 support w/SLA
+
+
+
+
+
+
+
+
+ Premium
+
+
+
+ {{partial "svg/edition-icon-premium"}}
+
+
+
+ All Pro features
+ Performance Replication
+ HSM Autounseal
+ Mount Filters
+ Multi-Factor Authentication
+ Sentinel Integration
+ Control Groups
+ Seal Wrap / FIPS 140-2 Compliance
+ Gold support: 24x7 support w/SLA
+
+
+
+
+
+
+ Request Info
+ {{i-con glyph="chevron-right"}}
+
+
+
+
+
diff --git a/ui/app/templates/partials/userpass-form.hbs b/ui/app/templates/partials/userpass-form.hbs
new file mode 100644
index 000000000..ec1945a29
--- /dev/null
+++ b/ui/app/templates/partials/userpass-form.hbs
@@ -0,0 +1,28 @@
+
+
+
Username
+
+ {{input
+ value=username
+ name="username"
+ id="username"
+ class="input"
+ }}
+
+
+
+
Password
+
+ {{input
+ value=password
+ name="password"
+ id="password"
+ type="password"
+ class="input"
+ }}
+
+
+
diff --git a/ui/app/templates/svg/edition-icon-premium.hbs b/ui/app/templates/svg/edition-icon-premium.hbs
new file mode 100644
index 000000000..772fda857
--- /dev/null
+++ b/ui/app/templates/svg/edition-icon-premium.hbs
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ui/app/templates/svg/edition-icon-pro.hbs b/ui/app/templates/svg/edition-icon-pro.hbs
new file mode 100644
index 000000000..c108d1c3a
--- /dev/null
+++ b/ui/app/templates/svg/edition-icon-pro.hbs
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ui/app/templates/svg/hashicorp-logo.hbs b/ui/app/templates/svg/hashicorp-logo.hbs
new file mode 100644
index 000000000..47c283313
--- /dev/null
+++ b/ui/app/templates/svg/hashicorp-logo.hbs
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/ui/app/templates/svg/icons/alert-circled.hbs b/ui/app/templates/svg/icons/alert-circled.hbs
new file mode 100644
index 000000000..331d6d3f0
--- /dev/null
+++ b/ui/app/templates/svg/icons/alert-circled.hbs
@@ -0,0 +1 @@
+
diff --git a/ui/app/templates/svg/icons/alert.hbs b/ui/app/templates/svg/icons/alert.hbs
new file mode 100644
index 000000000..69ebb096a
--- /dev/null
+++ b/ui/app/templates/svg/icons/alert.hbs
@@ -0,0 +1 @@
+
diff --git a/ui/app/templates/svg/icons/android-folder-open.hbs b/ui/app/templates/svg/icons/android-folder-open.hbs
new file mode 100644
index 000000000..f1c668bab
--- /dev/null
+++ b/ui/app/templates/svg/icons/android-folder-open.hbs
@@ -0,0 +1 @@
+
diff --git a/ui/app/templates/svg/icons/android-folder.hbs b/ui/app/templates/svg/icons/android-folder.hbs
new file mode 100644
index 000000000..e64916df9
--- /dev/null
+++ b/ui/app/templates/svg/icons/android-folder.hbs
@@ -0,0 +1 @@
+
diff --git a/ui/app/templates/svg/icons/android-person.hbs b/ui/app/templates/svg/icons/android-person.hbs
new file mode 100644
index 000000000..211977bbd
--- /dev/null
+++ b/ui/app/templates/svg/icons/android-person.hbs
@@ -0,0 +1 @@
+
diff --git a/ui/app/templates/svg/icons/android-sync.hbs b/ui/app/templates/svg/icons/android-sync.hbs
new file mode 100644
index 000000000..39d936113
--- /dev/null
+++ b/ui/app/templates/svg/icons/android-sync.hbs
@@ -0,0 +1 @@
+
diff --git a/ui/app/templates/svg/icons/checkmark-circled.hbs b/ui/app/templates/svg/icons/checkmark-circled.hbs
new file mode 100644
index 000000000..c8bfac62b
--- /dev/null
+++ b/ui/app/templates/svg/icons/checkmark-circled.hbs
@@ -0,0 +1 @@
+
diff --git a/ui/app/templates/svg/icons/checkmark.hbs b/ui/app/templates/svg/icons/checkmark.hbs
new file mode 100644
index 000000000..75f955703
--- /dev/null
+++ b/ui/app/templates/svg/icons/checkmark.hbs
@@ -0,0 +1 @@
+
diff --git a/ui/app/templates/svg/icons/chevron-down.hbs b/ui/app/templates/svg/icons/chevron-down.hbs
new file mode 100644
index 000000000..aa22de866
--- /dev/null
+++ b/ui/app/templates/svg/icons/chevron-down.hbs
@@ -0,0 +1 @@
+
diff --git a/ui/app/templates/svg/icons/chevron-left.hbs b/ui/app/templates/svg/icons/chevron-left.hbs
new file mode 100644
index 000000000..3a5ee52c6
--- /dev/null
+++ b/ui/app/templates/svg/icons/chevron-left.hbs
@@ -0,0 +1 @@
+
diff --git a/ui/app/templates/svg/icons/chevron-right.hbs b/ui/app/templates/svg/icons/chevron-right.hbs
new file mode 100644
index 000000000..adaf018d5
--- /dev/null
+++ b/ui/app/templates/svg/icons/chevron-right.hbs
@@ -0,0 +1 @@
+
diff --git a/ui/app/templates/svg/icons/chevron-up.hbs b/ui/app/templates/svg/icons/chevron-up.hbs
new file mode 100644
index 000000000..7fe3a4767
--- /dev/null
+++ b/ui/app/templates/svg/icons/chevron-up.hbs
@@ -0,0 +1 @@
+
diff --git a/ui/app/templates/svg/icons/clipboard.hbs b/ui/app/templates/svg/icons/clipboard.hbs
new file mode 100644
index 000000000..0cf1cce11
--- /dev/null
+++ b/ui/app/templates/svg/icons/clipboard.hbs
@@ -0,0 +1 @@
+
diff --git a/ui/app/templates/svg/icons/close-circled.hbs b/ui/app/templates/svg/icons/close-circled.hbs
new file mode 100644
index 000000000..638b01e99
--- /dev/null
+++ b/ui/app/templates/svg/icons/close-circled.hbs
@@ -0,0 +1 @@
+
diff --git a/ui/app/templates/svg/icons/close-round.hbs b/ui/app/templates/svg/icons/close-round.hbs
new file mode 100644
index 000000000..03190c424
--- /dev/null
+++ b/ui/app/templates/svg/icons/close-round.hbs
@@ -0,0 +1 @@
+
diff --git a/ui/app/templates/svg/icons/close.hbs b/ui/app/templates/svg/icons/close.hbs
new file mode 100644
index 000000000..4c2f2d1a8
--- /dev/null
+++ b/ui/app/templates/svg/icons/close.hbs
@@ -0,0 +1 @@
+
diff --git a/ui/app/templates/svg/icons/cube.hbs b/ui/app/templates/svg/icons/cube.hbs
new file mode 100644
index 000000000..eaa45143d
--- /dev/null
+++ b/ui/app/templates/svg/icons/cube.hbs
@@ -0,0 +1 @@
+
diff --git a/ui/app/templates/svg/icons/document.hbs b/ui/app/templates/svg/icons/document.hbs
new file mode 100644
index 000000000..ac74873a9
--- /dev/null
+++ b/ui/app/templates/svg/icons/document.hbs
@@ -0,0 +1 @@
+
diff --git a/ui/app/templates/svg/icons/false.hbs b/ui/app/templates/svg/icons/false.hbs
new file mode 100644
index 000000000..631cc1548
--- /dev/null
+++ b/ui/app/templates/svg/icons/false.hbs
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/ui/app/templates/svg/icons/file.hbs b/ui/app/templates/svg/icons/file.hbs
new file mode 100644
index 000000000..0789224dd
--- /dev/null
+++ b/ui/app/templates/svg/icons/file.hbs
@@ -0,0 +1,3 @@
+
+
+
diff --git a/ui/app/templates/svg/icons/folder.hbs b/ui/app/templates/svg/icons/folder.hbs
new file mode 100644
index 000000000..d153f9f6a
--- /dev/null
+++ b/ui/app/templates/svg/icons/folder.hbs
@@ -0,0 +1,3 @@
+
+
+
diff --git a/ui/app/templates/svg/icons/grid.hbs b/ui/app/templates/svg/icons/grid.hbs
new file mode 100644
index 000000000..dc4943fc6
--- /dev/null
+++ b/ui/app/templates/svg/icons/grid.hbs
@@ -0,0 +1,2 @@
+
+
diff --git a/ui/app/templates/svg/icons/hashicorp.hbs b/ui/app/templates/svg/icons/hashicorp.hbs
new file mode 100644
index 000000000..0c4599430
--- /dev/null
+++ b/ui/app/templates/svg/icons/hashicorp.hbs
@@ -0,0 +1 @@
+
diff --git a/ui/app/templates/svg/icons/information-circled.hbs b/ui/app/templates/svg/icons/information-circled.hbs
new file mode 100644
index 000000000..49a0f159c
--- /dev/null
+++ b/ui/app/templates/svg/icons/information-circled.hbs
@@ -0,0 +1 @@
+
diff --git a/ui/app/templates/svg/icons/information-reversed.hbs b/ui/app/templates/svg/icons/information-reversed.hbs
new file mode 100644
index 000000000..528d04ecd
--- /dev/null
+++ b/ui/app/templates/svg/icons/information-reversed.hbs
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/ui/app/templates/svg/icons/information.hbs b/ui/app/templates/svg/icons/information.hbs
new file mode 100644
index 000000000..cd5718d44
--- /dev/null
+++ b/ui/app/templates/svg/icons/information.hbs
@@ -0,0 +1 @@
+
diff --git a/ui/app/templates/svg/icons/ios-search-strong.hbs b/ui/app/templates/svg/icons/ios-search-strong.hbs
new file mode 100644
index 000000000..46763f979
--- /dev/null
+++ b/ui/app/templates/svg/icons/ios-search-strong.hbs
@@ -0,0 +1,2 @@
+
+
diff --git a/ui/app/templates/svg/icons/ios-search.hbs b/ui/app/templates/svg/icons/ios-search.hbs
new file mode 100644
index 000000000..7844b9af7
--- /dev/null
+++ b/ui/app/templates/svg/icons/ios-search.hbs
@@ -0,0 +1 @@
+
diff --git a/ui/app/templates/svg/icons/key.hbs b/ui/app/templates/svg/icons/key.hbs
new file mode 100644
index 000000000..aeecbb7ad
--- /dev/null
+++ b/ui/app/templates/svg/icons/key.hbs
@@ -0,0 +1,2 @@
+
+
diff --git a/ui/app/templates/svg/icons/list.hbs b/ui/app/templates/svg/icons/list.hbs
new file mode 100644
index 000000000..a598ebbc9
--- /dev/null
+++ b/ui/app/templates/svg/icons/list.hbs
@@ -0,0 +1 @@
+
diff --git a/ui/app/templates/svg/icons/locked.hbs b/ui/app/templates/svg/icons/locked.hbs
new file mode 100644
index 000000000..5bef339e2
--- /dev/null
+++ b/ui/app/templates/svg/icons/locked.hbs
@@ -0,0 +1 @@
+
diff --git a/ui/app/templates/svg/icons/log-in.hbs b/ui/app/templates/svg/icons/log-in.hbs
new file mode 100644
index 000000000..ca34c02f8
--- /dev/null
+++ b/ui/app/templates/svg/icons/log-in.hbs
@@ -0,0 +1 @@
+
diff --git a/ui/app/templates/svg/icons/log-out.hbs b/ui/app/templates/svg/icons/log-out.hbs
new file mode 100644
index 000000000..3ae858d27
--- /dev/null
+++ b/ui/app/templates/svg/icons/log-out.hbs
@@ -0,0 +1 @@
+
diff --git a/ui/app/templates/svg/icons/loop.hbs b/ui/app/templates/svg/icons/loop.hbs
new file mode 100644
index 000000000..c872bd369
--- /dev/null
+++ b/ui/app/templates/svg/icons/loop.hbs
@@ -0,0 +1 @@
+
diff --git a/ui/app/templates/svg/icons/more.hbs b/ui/app/templates/svg/icons/more.hbs
new file mode 100644
index 000000000..57d4583cf
--- /dev/null
+++ b/ui/app/templates/svg/icons/more.hbs
@@ -0,0 +1 @@
+
diff --git a/ui/app/templates/svg/icons/perf-replication.hbs b/ui/app/templates/svg/icons/perf-replication.hbs
new file mode 100644
index 000000000..98e99c567
--- /dev/null
+++ b/ui/app/templates/svg/icons/perf-replication.hbs
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/ui/app/templates/svg/icons/power.hbs b/ui/app/templates/svg/icons/power.hbs
new file mode 100644
index 000000000..3b4245297
--- /dev/null
+++ b/ui/app/templates/svg/icons/power.hbs
@@ -0,0 +1 @@
+
diff --git a/ui/app/templates/svg/icons/radio-waves.hbs b/ui/app/templates/svg/icons/radio-waves.hbs
new file mode 100644
index 000000000..00a520e44
--- /dev/null
+++ b/ui/app/templates/svg/icons/radio-waves.hbs
@@ -0,0 +1,2 @@
+
+
diff --git a/ui/app/templates/svg/icons/replication.hbs b/ui/app/templates/svg/icons/replication.hbs
new file mode 100644
index 000000000..4fbc5a850
--- /dev/null
+++ b/ui/app/templates/svg/icons/replication.hbs
@@ -0,0 +1,2 @@
+
+
diff --git a/ui/app/templates/svg/icons/reply.hbs b/ui/app/templates/svg/icons/reply.hbs
new file mode 100644
index 000000000..9436f85d4
--- /dev/null
+++ b/ui/app/templates/svg/icons/reply.hbs
@@ -0,0 +1 @@
+
diff --git a/ui/app/templates/svg/icons/role.hbs b/ui/app/templates/svg/icons/role.hbs
new file mode 100644
index 000000000..e6ee37843
--- /dev/null
+++ b/ui/app/templates/svg/icons/role.hbs
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ui/app/templates/svg/icons/shuffle.hbs b/ui/app/templates/svg/icons/shuffle.hbs
new file mode 100644
index 000000000..4a386c30d
--- /dev/null
+++ b/ui/app/templates/svg/icons/shuffle.hbs
@@ -0,0 +1,2 @@
+
+
diff --git a/ui/app/templates/svg/icons/trash-a.hbs b/ui/app/templates/svg/icons/trash-a.hbs
new file mode 100644
index 000000000..89372ac92
--- /dev/null
+++ b/ui/app/templates/svg/icons/trash-a.hbs
@@ -0,0 +1 @@
+
diff --git a/ui/app/templates/svg/icons/true.hbs b/ui/app/templates/svg/icons/true.hbs
new file mode 100644
index 000000000..43469dbfa
--- /dev/null
+++ b/ui/app/templates/svg/icons/true.hbs
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/ui/app/templates/svg/icons/unlocked.hbs b/ui/app/templates/svg/icons/unlocked.hbs
new file mode 100644
index 000000000..01cc126bb
--- /dev/null
+++ b/ui/app/templates/svg/icons/unlocked.hbs
@@ -0,0 +1 @@
+
diff --git a/ui/app/templates/svg/icons/vault.hbs b/ui/app/templates/svg/icons/vault.hbs
new file mode 100644
index 000000000..5e5f220dc
--- /dev/null
+++ b/ui/app/templates/svg/icons/vault.hbs
@@ -0,0 +1 @@
+
diff --git a/ui/app/templates/svg/icons/wand.hbs b/ui/app/templates/svg/icons/wand.hbs
new file mode 100644
index 000000000..2c9a8ea5f
--- /dev/null
+++ b/ui/app/templates/svg/icons/wand.hbs
@@ -0,0 +1 @@
+
diff --git a/ui/app/templates/svg/initialize.hbs b/ui/app/templates/svg/initialize.hbs
new file mode 100644
index 000000000..d8cc987b6
--- /dev/null
+++ b/ui/app/templates/svg/initialize.hbs
@@ -0,0 +1,963 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ui/app/templates/svg/vault-edition-logo.hbs b/ui/app/templates/svg/vault-edition-logo.hbs
new file mode 100644
index 000000000..2e96a4818
--- /dev/null
+++ b/ui/app/templates/svg/vault-edition-logo.hbs
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+ {{#if (is-version "Enterprise")}}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{/if}}
+
diff --git a/ui/app/templates/svg/vault-enterprise.hbs b/ui/app/templates/svg/vault-enterprise.hbs
new file mode 100644
index 000000000..8ee0442f3
--- /dev/null
+++ b/ui/app/templates/svg/vault-enterprise.hbs
@@ -0,0 +1,8 @@
+
+
+
+
+
diff --git a/ui/app/templates/svg/vault-logo.hbs b/ui/app/templates/svg/vault-logo.hbs
new file mode 100644
index 000000000..efc2811f0
--- /dev/null
+++ b/ui/app/templates/svg/vault-logo.hbs
@@ -0,0 +1,3 @@
+
+
+
diff --git a/ui/app/templates/vault/cluster.hbs b/ui/app/templates/vault/cluster.hbs
new file mode 100644
index 000000000..c24cd6895
--- /dev/null
+++ b/ui/app/templates/vault/cluster.hbs
@@ -0,0 +1 @@
+{{outlet}}
diff --git a/ui/app/templates/vault/cluster/access.hbs b/ui/app/templates/vault/cluster/access.hbs
new file mode 100644
index 000000000..be96cf4f8
--- /dev/null
+++ b/ui/app/templates/vault/cluster/access.hbs
@@ -0,0 +1,27 @@
+
+ {{#menu-sidebar title="Access" class="is-3" data-test-sidebar=true}}
+
+ {{#link-to "vault.cluster.access.methods" data-test-link=true current-when="vault.cluster.access.methods vault.cluster.access.method"}}
+ Auth Methods
+ {{/link-to}}
+
+
+ {{#link-to "vault.cluster.access.identity" "entities" data-test-link=true }}
+ Entities
+ {{/link-to}}
+
+
+ {{#link-to "vault.cluster.access.identity" "groups" data-test-link=true }}
+ Groups
+ {{/link-to}}
+
+
+ {{#link-to "vault.cluster.access.leases" data-test-link=true}}
+ Leases
+ {{/link-to}}
+
+ {{/menu-sidebar}}
+
+ {{outlet}}
+
+
diff --git a/ui/app/templates/vault/cluster/access/identity/aliases/add.hbs b/ui/app/templates/vault/cluster/access/identity/aliases/add.hbs
new file mode 100644
index 000000000..5b41e4cfa
--- /dev/null
+++ b/ui/app/templates/vault/cluster/access/identity/aliases/add.hbs
@@ -0,0 +1,11 @@
+
+
+{{identity/edit-form model=model onSave=(perform navToShow)}}
diff --git a/ui/app/templates/vault/cluster/access/identity/aliases/edit.hbs b/ui/app/templates/vault/cluster/access/identity/aliases/edit.hbs
new file mode 100644
index 000000000..a13fc9e44
--- /dev/null
+++ b/ui/app/templates/vault/cluster/access/identity/aliases/edit.hbs
@@ -0,0 +1,11 @@
+
+
+{{identity/edit-form mode="edit" model=model onSave=(perform navToShow)}}
diff --git a/ui/app/templates/vault/cluster/access/identity/aliases/index.hbs b/ui/app/templates/vault/cluster/access/identity/aliases/index.hbs
new file mode 100644
index 000000000..7d715af32
--- /dev/null
+++ b/ui/app/templates/vault/cluster/access/identity/aliases/index.hbs
@@ -0,0 +1,41 @@
+{{identity/entity-nav identityType=identityType}}
+{{#if model.meta.total}}
+ {{#each model as |item|}}
+
+ {{i-con
+ glyph="role"
+ size=14
+ class="has-text-grey-light"
+ }}
+
+ {{item.id}}
+
+
+ {{/each}}
+{{else}}
+
+
+
+
+
+ There are currently no {{identityType}} aliases.
+
+
+
+
+
+{{/if}}
+{{#if (gt model.meta.lastPage 1) }}
+ {{list-pagination
+ page=model.meta.currentPage
+ lastPage=model.meta.lastPage
+ link="vault.cluster.access.identity.aliases.index"
+ }}
+{{/if}}
diff --git a/ui/app/templates/vault/cluster/access/identity/aliases/show.hbs b/ui/app/templates/vault/cluster/access/identity/aliases/show.hbs
new file mode 100644
index 000000000..24f7d9cdf
--- /dev/null
+++ b/ui/app/templates/vault/cluster/access/identity/aliases/show.hbs
@@ -0,0 +1,39 @@
+
+
+
+
+ {{#each (tabs-for-identity-show model.identityType) as |tab|}}
+ {{#link-to "vault.cluster.access.identity.aliases.show" model.id tab tagName="li"}}
+
+ {{capitalize tab}}
+
+ {{/link-to}}
+ {{/each}}
+
+
+
+{{component (concat 'identity/item-alias/alias-' section) model=model}}
diff --git a/ui/app/templates/vault/cluster/access/identity/create.hbs b/ui/app/templates/vault/cluster/access/identity/create.hbs
new file mode 100644
index 000000000..3fce9e8d3
--- /dev/null
+++ b/ui/app/templates/vault/cluster/access/identity/create.hbs
@@ -0,0 +1,11 @@
+
+
+{{identity/edit-form model=model onSave=(perform navToShow)}}
diff --git a/ui/app/templates/vault/cluster/access/identity/edit.hbs b/ui/app/templates/vault/cluster/access/identity/edit.hbs
new file mode 100644
index 000000000..a13fc9e44
--- /dev/null
+++ b/ui/app/templates/vault/cluster/access/identity/edit.hbs
@@ -0,0 +1,11 @@
+
+
+{{identity/edit-form mode="edit" model=model onSave=(perform navToShow)}}
diff --git a/ui/app/templates/vault/cluster/access/identity/index.hbs b/ui/app/templates/vault/cluster/access/identity/index.hbs
new file mode 100644
index 000000000..c38c5b860
--- /dev/null
+++ b/ui/app/templates/vault/cluster/access/identity/index.hbs
@@ -0,0 +1,41 @@
+{{identity/entity-nav identityType=identityType}}
+{{#if model.meta.total}}
+ {{#each model as |item|}}
+
+ {{i-con
+ glyph="role"
+ size=14
+ class="has-text-grey-light"
+ }}
+
+ {{item.id}}
+
+
+ {{/each}}
+{{else}}
+
+
+
+
+
+ There are currently no {{pluralize identityType}}.
+
+
+
+
+
+{{/if}}
+{{#if (gt model.meta.lastPage 1) }}
+ {{list-pagination
+ page=model.meta.currentPage
+ lastPage=model.meta.lastPage
+ link="vault.cluster.access.identity.index"
+ }}
+{{/if}}
diff --git a/ui/app/templates/vault/cluster/access/identity/merge.hbs b/ui/app/templates/vault/cluster/access/identity/merge.hbs
new file mode 100644
index 000000000..064692ce9
--- /dev/null
+++ b/ui/app/templates/vault/cluster/access/identity/merge.hbs
@@ -0,0 +1,11 @@
+
+
+{{identity/edit-form mode="merge" model=model onSave=(perform navToShow)}}
diff --git a/ui/app/templates/vault/cluster/access/identity/show.hbs b/ui/app/templates/vault/cluster/access/identity/show.hbs
new file mode 100644
index 000000000..41afa9796
--- /dev/null
+++ b/ui/app/templates/vault/cluster/access/identity/show.hbs
@@ -0,0 +1,45 @@
+
+
+
+
+ {{#each (tabs-for-identity-show model.identityType model.type) as |tab|}}
+ {{#link-to "vault.cluster.access.identity.show" model.id tab tagName="li"}}
+
+ {{capitalize tab}}
+
+ {{/link-to}}
+ {{/each}}
+
+
+
+{{component (concat 'identity/item-' section) model=model}}
diff --git a/ui/app/templates/vault/cluster/access/leases/error.hbs b/ui/app/templates/vault/cluster/access/leases/error.hbs
new file mode 100644
index 000000000..24ebae834
--- /dev/null
+++ b/ui/app/templates/vault/cluster/access/leases/error.hbs
@@ -0,0 +1,49 @@
+
+
+{{#unless (or (eq model.httpStatus 400) (eq model.httpStatus 404))}}
+ {{model.message}}
+{{/unless}}
+
+
+
+
+
+ {{#if (eq model.httpStatus 400)}}
+
+ {{i-con glyph='close'}}
+ {{model.keyId}}
is not a valid lease ID
+
+ {{else if (eq model.httpStatus 404)}}
+
+ Unable to find lease for the id
: {{model.keyId}}
. Try going back to the
+ {{#link-to "vault.cluster.access.leases"}}lookup{{/link-to}}
+ and re-entering the id
.
+
+ {{else if (eq model.httpStatus 403)}}
+
+ You don't have access to {{model.keyId}}
. If you think you've reached this page in error, please contact your administrator.
+
+ {{else}}
+ {{#each model.errors as |error|}}
+
+ {{error}}
+
+ {{/each}}
+ {{/if}}
+
+
+
+
+
+ {{#link-to "vault.cluster.access.leases" class="button"}}
+ Back
+ {{/link-to}}
+
diff --git a/ui/app/templates/vault/cluster/access/leases/index.hbs b/ui/app/templates/vault/cluster/access/leases/index.hbs
new file mode 100644
index 000000000..5ffc30202
--- /dev/null
+++ b/ui/app/templates/vault/cluster/access/leases/index.hbs
@@ -0,0 +1,28 @@
+
+
+
+
+
+
Lease ID
+
+ {{input value=leaseId id="lease-id" class="input"}}
+
+
+ If you know the id
of a lease, enter it above to lookup details of the lease.
+
+
+
+
+
diff --git a/ui/app/templates/vault/cluster/access/leases/list.hbs b/ui/app/templates/vault/cluster/access/leases/list.hbs
new file mode 100644
index 000000000..526ec305e
--- /dev/null
+++ b/ui/app/templates/vault/cluster/access/leases/list.hbs
@@ -0,0 +1,149 @@
+
+
+
+
+ {{navigate-input
+ filterFocusDidChange=(action "setFilterFocus")
+ filterDidChange=(action "setFilter")
+ filter=filter
+ filterMatchesKey=filterMatchesKey
+ firstPartialMatch=firstPartialMatch
+ baseKey=(get baseKey "id")
+ shouldNavigateTree=true
+ mode='leases'
+ placeholder='Filter leases'
+ }}
+ {{#if filterFocused}}
+
+
+ {{#if filterMatchesKey}}
+ {{#unless filterIsFolder}}
+
+ ENTER to go to see details
+
+ {{/unless}}
+ {{/if}}
+ {{#if firstPartialMatch}}
+
+ TAB to complete
+
+ {{/if}}
+ {{/if}}
+
+
+
+
+{{#if model.meta.total}}
+ {{#each model as |item|}}
+
+ {{i-con
+ glyph=(if item.isFolder 'folder' 'document')
+ size=14
+ class="has-text-grey-light"
+ }}
+
+ {{or item.keyWithoutParent item.id}}
+
+
+ {{else}}
+
+ There are no leases matching {{filter}}
+
+ {{/each}}
+{{else}}
+
+
+
+
+
+ {{#if (eq baseKey.id '')}}
+ There are currently no leases.
+ {{else}}
+ {{#if filterIsFolder}}
+ {{#if (eq filter baseKey.id)}}
+ There are no leases under {{filter}}
.
+ {{else}}
+ We couldn't find a prefix matching {{filter}}
.
+ {{/if}}
+ {{/if}}
+ {{/if}}
+
+
+
+
+
+{{/if}}
+{{#if (gt model.meta.lastPage 1) }}
+ {{list-pagination
+ page=model.meta.currentPage
+ lastPage=model.meta.lastPage
+ link=(concat "vault.cluster.access.leases.list" (if (not baseKey.id) "-root"))
+ model=(compact (array (if baseKey.id baseKey.id)))
+ }}
+{{/if}}
diff --git a/ui/app/templates/vault/cluster/access/leases/show.hbs b/ui/app/templates/vault/cluster/access/leases/show.hbs
new file mode 100644
index 000000000..5e4307d6a
--- /dev/null
+++ b/ui/app/templates/vault/cluster/access/leases/show.hbs
@@ -0,0 +1,83 @@
+
+
+ {{#info-table-row label="Issue time" value=model.issueTime}}
+ {{moment-format model.issueTime 'MMM DD, YYYY hh:mm:ss A'}}
+
+
+ {{model.issueTime}}
+
+ {{/info-table-row}}
+ {{info-table-row label="Renewable" value=model.renewable}}
+ {{#info-table-row label="Last renewal" value=model.lastRenewal}}
+ {{moment-format model.lastRenewal 'MMM DD, YYYY hh:mm:ss A'}}
+
+
+ {{model.lastRenewal}}
+
+ {{/info-table-row}}
+ {{#if model.expireTime}}
+ {{#info-table-row label="Expiration time" value=model.expireTime}}
+ {{moment-format model.expireTime 'MMM DD, YYYY hh:mm:ss A'}}
+
+
+ {{model.expireTime}}
+
+ {{/info-table-row}}
+ {{info-table-row label="Expires in" value=(moment-from-now model.expireTime interval=1000 hideSuffix=true)}}
+ {{/if}}
+ {{info-table-row label="TTL" value=model.ttl}}
+
+{{#if (and (not model.isAuthLease) model.renewable capabilities.renew.canUpdate)}}
+
+
Renew Lease
+
+ {{ttl-picker
+ labelText="Interval"
+ labelClass="is-label"
+ onChange=(action (mut interval))
+ outputSeconds=true
+ }}
+
+
+
+{{/if}}
+
+
{{!-- needs to be here to push over the button --}}
+ {{#if capabilities.revoke.canUpdate}}
+ {{#confirm-action
+ onConfirmAction=(action "revokeLease" model)
+ confirmMessage= (concat "Are you sure you want to revoke this lease?")
+ confirmButtonText="Confirm revocation"
+ cancelButtonText="Cancel"
+ data-test-lease-revoke=true
+ }}
+ Revoke lease
+ {{/confirm-action}}
+ {{/if}}
+
diff --git a/ui/app/templates/vault/cluster/access/method/section.hbs b/ui/app/templates/vault/cluster/access/method/section.hbs
new file mode 100644
index 000000000..2419984e7
--- /dev/null
+++ b/ui/app/templates/vault/cluster/access/method/section.hbs
@@ -0,0 +1,36 @@
+
+
+{{section-tabs model 'authShow'}}
+{{component (concat "auth-method/" section) model=model}}
+
diff --git a/ui/app/templates/vault/cluster/access/methods.hbs b/ui/app/templates/vault/cluster/access/methods.hbs
new file mode 100644
index 000000000..7212c3893
--- /dev/null
+++ b/ui/app/templates/vault/cluster/access/methods.hbs
@@ -0,0 +1,84 @@
+
+
+{{#each (sort-by "path" model) as |method|}}
+ {{#linked-block
+ "vault.cluster.access.methods"
+ class="box is-sideless is-marginless has-pointer "
+ data-test-auth-backend-link=method.id
+ }}
+
+
+
+ {{i-con glyph="folder" size=14 class="has-text-grey-light"}} {{method.path}}
+
+
+
+ {{#if (eq method.type 'plugin')}}
+ {{method.type}}: {{method.config.plugin_name}}
+ {{else}}
+ {{method.type}}
+ {{/if}}
+
+
+
+ {{method.accessor}}
+
+
+
+
+
+ {{#popup-menu name="auth-backend-nav" contentClass="is-wide"}}
+
+ {{/popup-menu}}
+
+
+
+ {{/linked-block}}
+{{/each}}
diff --git a/ui/app/templates/vault/cluster/auth.hbs b/ui/app/templates/vault/cluster/auth.hbs
new file mode 100644
index 000000000..78aab10dd
--- /dev/null
+++ b/ui/app/templates/vault/cluster/auth.hbs
@@ -0,0 +1,30 @@
+{{#splash-page as |s|}}
+ {{#s.header}}
+
+ Sign in to Vault
+
+ {{/s.header}}
+ {{#s.content}}
+
+
+
+
+ {{i-con glyph="unlocked" size=20}} {{capitalize model.name}} is {{if model.unsealed 'unsealed' 'sealed'}}
+
+
+
+
+ {{auth-form
+ cluster=model
+ redirectTo=redirectTo
+ selectedAuthType=with
+ }}
+ {{/s.content}}
+ {{#s.footer}}
+
+
+ Contact your administrator for login credentials
+
+
+ {{/s.footer}}
+{{/splash-page}}
diff --git a/ui/app/templates/vault/cluster/error.hbs b/ui/app/templates/vault/cluster/error.hbs
new file mode 100644
index 000000000..6bc05baf3
--- /dev/null
+++ b/ui/app/templates/vault/cluster/error.hbs
@@ -0,0 +1,51 @@
+{{#if (eq model.httpStatus 404)}}
+ {{not-found model=model}}
+{{else}}
+
+
+ {{#if (and
+ (eq model.httpStatus 403)
+ (eq model.policyPath 'sys/capabilities-self')
+ )
+ }}
+
+ Your auth token does not have access to {{model.policyPath}}
. Vault Enterprise uses this endpoint to determine what actions are allowed in the interface.
+
+
+ Make sure the policy for the path {{model.policyPath}}
includes capabilities = ['update']
.
+
+ {{else if (and
+ (eq model.httpStatus 403)
+ (eq model.policyPath 'sys/mounts')
+ )
+ }}
+
+ Your auth token does not have access to {{model.policyPath}}. This is necessary in order to browse secret backends.
+
+
+ Make sure the policy for the path {{model.policyPath}}
has capabilities = ['list', 'read']
.
+
+ {{else}}
+ {{#if model.message}}
+
{{model.message}}
+ {{/if}}
+ {{#each model.errors as |error|}}
+
+ {{error}}
+
+ {{/each}}
+ {{/if}}
+
+{{/if}}
diff --git a/ui/app/templates/vault/cluster/init.hbs b/ui/app/templates/vault/cluster/init.hbs
new file mode 100644
index 000000000..1eb347824
--- /dev/null
+++ b/ui/app/templates/vault/cluster/init.hbs
@@ -0,0 +1,173 @@
+{{#splash-page as |s|}}
+ {{#s.header}}
+
+ Connect to Vault and initialize
+
+ {{/s.header}}
+ {{#s.content}}
+ {{#if keyData}}
+
+
+
+
+ Vault has been initialized!
+
+
+
+
+ {{partial "svg/initialize"}}
+
+
+
+
+
+
+ Initial Root Token
+
+ {{keyData.root_token}}
+
+
+
+
+ Please securely distribute the keys below. When the Vault is re-sealed, restarted, or stopped, you must provide at least {{secret_threshold}} of these keys to unseal it again.
+ Vault does not store the master key. Without at least {{secret_threshold}} keys, your Vault will remain permanently sealed.
+
+
+ {{#each (if keyData.keys_base64 keyData.keys_base64 keyData.keys) as |key index| }}
+
+
+
+ Key {{add index 1}}
+
+ {{key}}
+
+
+ {{/each}}
+
+
+
+ {{#if model.sealed}}
+
+ {{#link-to 'vault.cluster.unseal' model.name class="button is-primary"}}
+ Continue to Unseal
+ {{/link-to}}
+
+ {{else}}
+
+ {{#link-to 'vault.cluster.auth' model.name class="button is-primary"}}
+ Continue to Authenticate
+ {{/link-to}}
+
+ {{/if}}
+ {{download-button
+ actionText="Download Keys"
+ data=keyData
+ filename=keyFilename
+ mime="application/json"
+ extension="json"
+ class="button"
+ stringify=true
+ }}
+
+
+ {{else}}
+
+
+
+
+
+ Let’s set up the initial set of master keys and the backend data store structure
+
+
+
+
+ {{partial "svg/initialize"}}
+
+
+
+ {{message-error errors=errors}}
+
+
+
+
+ Key Shares
+
+
+ {{input class="input" name="key-shares" type="number" step="1" min="1" pattern="[0-9]*" value=secret_shares}}
+
+
+ The number of key shares to split the master key into
+
+
+
+
+
+
+ Key Threshold
+
+
+ {{input class="input" name="key-threshold" type="number" step="1" min="1" pattern="[0-9]*" value=secret_threshold}}
+
+
+ The number of key shares required to reconstruct the master key
+
+
+
+
+
+ {{toggle-button
+ openLabel="Encrypt Output with PGP"
+ closedLabel="Encrypt Output with PGP"
+ toggleTarget=this
+ toggleAttr="use_pgp"
+ class="is-block"
+ }}
+ {{#if use_pgp}}
+
+
+ The output unseal keys will be encrypted and hex-encoded, in order, with the given public keys.
+
+ {{pgp-list listLength=secret_shares onDataUpdate=(action 'setKeys')}}
+
+ {{/if}}
+ {{toggle-button
+ openLabel="Encrypt Root Token with PGP"
+ closedLabel="Encrypt Root Token with PGP"
+ toggleTarget=this
+ toggleAttr="use_pgp_for_root"
+ class="is-block"
+ }}
+ {{#if use_pgp_for_root}}
+
+
+ The root unseal key will be encrypted and hex-encoded with the given public key.
+
+ {{pgp-list listLength=1 onDataUpdate=(action 'setRootKey')}}
+
+ {{/if}}
+
+
+
+
+ Initialize
+
+
+ {{/if}}
+ {{/s.content}}
+{{/splash-page}}
diff --git a/ui/app/templates/vault/cluster/loading.hbs b/ui/app/templates/vault/cluster/loading.hbs
new file mode 100644
index 000000000..b8b5ad854
--- /dev/null
+++ b/ui/app/templates/vault/cluster/loading.hbs
@@ -0,0 +1 @@
+{{partial "partials/loading"}}
diff --git a/ui/app/templates/vault/cluster/not-found.hbs b/ui/app/templates/vault/cluster/not-found.hbs
new file mode 100644
index 000000000..3ce01ab82
--- /dev/null
+++ b/ui/app/templates/vault/cluster/not-found.hbs
@@ -0,0 +1 @@
+{{not-found model=model}}
diff --git a/ui/app/templates/vault/cluster/policies.hbs b/ui/app/templates/vault/cluster/policies.hbs
new file mode 100644
index 000000000..c3f94acce
--- /dev/null
+++ b/ui/app/templates/vault/cluster/policies.hbs
@@ -0,0 +1,38 @@
+
diff --git a/ui/app/templates/vault/cluster/policies/create.hbs b/ui/app/templates/vault/cluster/policies/create.hbs
new file mode 100644
index 000000000..cc79d6f6d
--- /dev/null
+++ b/ui/app/templates/vault/cluster/policies/create.hbs
@@ -0,0 +1,110 @@
+
+
+
+
+ {{message-error model=model}}
+
+
+
+
+ Policy
+
+
+
+
+ {{input
+ id="fileUploadToggle"
+ type="checkbox"
+ name="fileUploadToggle"
+ class="switch is-rounded is-success is-small"
+ checked=showFileUpload
+ change=(toggle-action "showFileUpload" this)
+ data-test-policy-edit-toggle=true
+ }}
+ Upload file
+
+
+
+
+ {{#if showFileUpload}}
+ {{text-file
+ inputOnly=true
+ index=""
+ file=file
+ onChange=(action "setPolicyFromFile")
+ }}
+ {{else}}
+ {{ivy-codemirror
+ value=model.policy
+ id="policy"
+ valueUpdated=(action (mut model.policy))
+ options=(hash
+ lineNumbers=true
+ tabSize=2
+ mode='ruby'
+ theme='hashi'
+ extraKeys=(hash
+ Shift-Enter=(action "savePolicy" model)
+ )
+ )
+ }}
+
+
+ You can use Alt+Tab (Option+Tab on MacOS) in the code editor to skip to the next field
+
+
+ {{/if}}
+
+ {{#each model.additionalAttrs as |attr|}}
+ {{form-field data-test-field attr=attr model=model}}
+ {{/each}}
+
+
+
+
+ Create Policy
+
+
+
+ {{#link-to
+ "vault.cluster.policies"
+ replace=true
+ class="button"
+ }}
+ Cancel
+ {{/link-to}}
+
+
+
diff --git a/ui/app/templates/vault/cluster/policies/index.hbs b/ui/app/templates/vault/cluster/policies/index.hbs
new file mode 100644
index 000000000..14f86b934
--- /dev/null
+++ b/ui/app/templates/vault/cluster/policies/index.hbs
@@ -0,0 +1,171 @@
+{{#if (or (eq policyType "acl") (has-feature "Sentinel"))}}
+
+
+
+
+ {{navigate-input
+ filterFocusDidChange=(action "setFilterFocus")
+ filterDidChange=(action "setFilter")
+ filter=filter
+ filterMatchesKey=filterMatchesKey
+ firstPartialMatch=firstPartialMatch
+ extraNavParams=policyType
+ placeholder="Filter policies"
+ mode="policy"
+ }}
+ {{#if filterFocused}}
+ {{#if filterMatchesKey}}
+
+ ENTER to go to {{or pageFilter filter}}
+
+ {{/if}}
+ {{#if firstPartialMatch}}
+
+ TAB to complete {{firstPartialMatch.id}}
+
+ {{/if}}
+ {{/if}}
+
+
+
+ {{#if model.meta.total}}
+ {{#each model as |item|}}
+ {{#if (eq item.id "root")}}
+
+ {{i-con
+ glyph='file'
+ size=14
+ class="has-text-grey-light"
+ }}
+
+
{{item.id}}
+
+ The root
policy does not contain any rules but can do anything within Vault. It should be used with extreme care.
+
+
+
+ {{else}}
+ {{#linked-block
+ "vault.cluster.policy.show"
+ policyType
+ item.id
+ class="box is-sideless is-marginless"
+ data-test-policy-link=item.id
+ }}
+
+
+
+ {{#popup-menu name="policy-nav"}}
+
+ {{/popup-menu}}
+
+
+ {{/linked-block}}
+ {{/if}}
+ {{else}}
+
+
There are no policies matching {{pageFilter}}
.
+
+ {{/each}}
+ {{else}}
+
+
+
+
+
+ There are currently no {{uppercase policyType}} policies.
+
+
+
+
+
+ {{/if}}
+ {{#if (gt model.meta.lastPage 1) }}
+ {{list-pagination
+ page=model.meta.currentPage
+ lastPage=model.meta.lastPage
+ link="vault.cluster.policies.index"
+ }}
+ {{/if}}
+{{else}}
+ {{upgrade-page title="Sentinel" minimumEdition="Vault Enterprise Premium"}}
+{{/if}}
diff --git a/ui/app/templates/vault/cluster/policies/loading.hbs b/ui/app/templates/vault/cluster/policies/loading.hbs
new file mode 100644
index 000000000..b8b5ad854
--- /dev/null
+++ b/ui/app/templates/vault/cluster/policies/loading.hbs
@@ -0,0 +1 @@
+{{partial "partials/loading"}}
diff --git a/ui/app/templates/vault/cluster/policy.hbs b/ui/app/templates/vault/cluster/policy.hbs
new file mode 100644
index 000000000..7351a1dcd
--- /dev/null
+++ b/ui/app/templates/vault/cluster/policy.hbs
@@ -0,0 +1,36 @@
+
diff --git a/ui/app/templates/vault/cluster/policy/edit.hbs b/ui/app/templates/vault/cluster/policy/edit.hbs
new file mode 100644
index 000000000..850bcb8f3
--- /dev/null
+++ b/ui/app/templates/vault/cluster/policy/edit.hbs
@@ -0,0 +1,105 @@
+
+
+
+
+
+
+ Policy
+
+
+
+ {{#if (and (not-eq model.id "root") (or capabilities.canUpdate capabilities.canDelete))}}
+
+ {{input
+ id="edit"
+ type="checkbox"
+ name="navToEdit"
+ class="switch is-rounded is-success is-small"
+ checked=true
+ change=(action (nav-to-route 'vault.cluster.policy.show' model.id replace=true) )
+ data-test-policy-edit-toggle=true
+ }}
+ Edit
+
+ {{/if}}
+
+
+
+ {{message-error model=model}}
+
+ {{ivy-codemirror
+ value=model.policy
+ valueUpdated=(action (mut model.policy))
+ options=(hash
+ lineNumbers=true
+ tabSize=2
+ mode='ruby'
+ theme='hashi'
+ extraKeys=(hash
+ Shift-Enter=(action "savePolicy" model)
+ )
+ )
+ }}
+
+
+ You can use Alt+Tab (Option+Tab on MacOS) in the code editor to skip to the next field
+
+
+
+ {{#each model.additionalAttrs as |attr|}}
+ {{form-field data-test-field attr=attr model=model}}
+ {{/each}}
+
+
+ {{#if capabilities.canUpdate}}
+
+
+ Save
+
+
+ {{/if}}
+
+
+ {{#if (and (not-eq model.id "default") capabilities.canDelete)}}
+ {{#confirm-action
+ buttonClasses="button is-link is-outlined is-inverted"
+ onConfirmAction=(action "deletePolicy" model)
+ confirmMessage=(concat "Are you sure you want to delete " model.id "?")
+ data-test-policy-delete=true
+ }}
+ Delete
+ {{/confirm-action}}
+ {{/if}}
+
+
+
diff --git a/ui/app/templates/vault/cluster/policy/error.hbs b/ui/app/templates/vault/cluster/policy/error.hbs
new file mode 100644
index 000000000..dc1618250
--- /dev/null
+++ b/ui/app/templates/vault/cluster/policy/error.hbs
@@ -0,0 +1,25 @@
+ {{#if (eq model.httpStatus 404)}}
+ {{not-found model=model}}
+ {{else}}
+
+
+ {{#if model.message}}
+
{{model.message}}
+ {{/if}}
+ {{#each model.errors as |error|}}
+
{{error}}
+ {{/each}}
+
+ {{/if}}
diff --git a/ui/app/templates/vault/cluster/policy/loading.hbs b/ui/app/templates/vault/cluster/policy/loading.hbs
new file mode 100644
index 000000000..b8b5ad854
--- /dev/null
+++ b/ui/app/templates/vault/cluster/policy/loading.hbs
@@ -0,0 +1 @@
+{{partial "partials/loading"}}
diff --git a/ui/app/templates/vault/cluster/policy/show.hbs b/ui/app/templates/vault/cluster/policy/show.hbs
new file mode 100644
index 000000000..593336270
--- /dev/null
+++ b/ui/app/templates/vault/cluster/policy/show.hbs
@@ -0,0 +1,84 @@
+
+
+
+
+ Policy
+ {{#if (eq policyType "acl")}}
+ ({{uppercase model.format}} format)
+ {{/if}}
+
+
+
+ {{#if (and (not-eq model.id "root") (or capabilities.canUpdate capabilities.canDelete))}}
+
+ {{input
+ id="edit"
+ type="checkbox"
+ name="navToEdit"
+ class="switch is-rounded is-success is-small"
+ checked=false
+ change=(action (nav-to-route 'vault.cluster.policy.edit' model.id replace=true) )
+ data-test-policy-edit-toggle=true
+ }}
+ Edit
+
+ {{/if}}
+
+
+
+
+ {{ivy-codemirror
+ value=model.policy
+ options=(hash
+ readOnly=true
+ lineNumbers=true
+ tabSize=2
+ mode='ruby'
+ theme='hashi'
+ )
+ }}
+
+ {{#if model.paths}}
+
+
Paths
+
+ {{#each model.paths as |path|}}
+
+ {{path}}
+
+ {{/each}}
+
+
+ {{/if}}
+
+ {{download-button
+ classNames="link is-pulled-right"
+ actionText="Download policy"
+ extension=(if (eq policyType "acl") model.format "sentinel")
+ filename=model.name
+ data=model.policy
+ }}
+
+
+
diff --git a/ui/app/templates/vault/cluster/replication-dr-promote.hbs b/ui/app/templates/vault/cluster/replication-dr-promote.hbs
new file mode 100644
index 000000000..e5e7463f7
--- /dev/null
+++ b/ui/app/templates/vault/cluster/replication-dr-promote.hbs
@@ -0,0 +1,63 @@
+{{#splash-page as |s|}}
+ {{#s.header}}
+
+ Disaster Recovery Secondary is enabled
+
+ {{/s.header}}
+ {{#s.content}}
+
+
+
+ {{#link-to 'vault.cluster.replication-dr-promote' (query-params action='')}}
+ Operation Token
+ {{/link-to}}
+
+
+ {{#link-to 'vault.cluster.replication-dr-promote' (query-params action='promote')}}
+ Promote
+ {{/link-to}}
+
+
+ {{#link-to 'vault.cluster.replication-dr-promote' (query-params action='update')}}
+ Update Primary
+ {{/link-to}}
+
+
+
+ {{#if (eq action 'promote')}}
+
+
+
+
+ {{i-con glyph="alert-circled" size=28 excludeIconClass=true}}
+
+
+
+
+ Caution : Vault replication is not designed for active-active usage and enabling two performance primaries should never be done, as it can lead to data loss if they or their secondaries are ever reconnected.
+
+
+
+
+
+
+ {{replication-actions replicationMode="dr" selectedAction="promote" model=model}}
+ {{/if}}
+ {{#if (eq action 'update')}}
+ {{replication-actions replicationMode="dr" selectedAction="updatePrimary" model=model}}
+ {{/if}}
+ {{#unless action}}
+ {{#shamir-flow
+ action="generate-dr-operation-token"
+ buttonText="Promote cluster"
+ fetchOnInit=true
+ generateAction=true
+ }}
+
+ Generate an Operation Token by entering a portion of the master key.
+ Once all portions are entered, the generated operation token may be used to manage your Seondary Disaster Recovery cluster.
+
+ {{/shamir-flow}}
+ {{/unless}}
+ {{/s.content}}
+{{/splash-page}}
diff --git a/ui/app/templates/vault/cluster/replication/index.hbs b/ui/app/templates/vault/cluster/replication/index.hbs
new file mode 100644
index 000000000..6540915dc
--- /dev/null
+++ b/ui/app/templates/vault/cluster/replication/index.hbs
@@ -0,0 +1,21 @@
+{{#if (eq model.mode 'unsupported')}}
+
+
+
+ The current cluster configuration does not support replication.
+
+
+{{else}}
+ {{replication-summary
+ cluster=model
+ showModeSummary=true
+ }}
+{{/if}}
diff --git a/ui/app/templates/vault/cluster/replication/mode.hbs b/ui/app/templates/vault/cluster/replication/mode.hbs
new file mode 100644
index 000000000..58dbd43fe
--- /dev/null
+++ b/ui/app/templates/vault/cluster/replication/mode.hbs
@@ -0,0 +1,55 @@
+{{#if model.replicationAttrs.replicationEnabled}}
+
+
+
+{{/if}}
+{{outlet}}
diff --git a/ui/app/templates/vault/cluster/replication/mode/index.hbs b/ui/app/templates/vault/cluster/replication/mode/index.hbs
new file mode 100644
index 000000000..54f898ee7
--- /dev/null
+++ b/ui/app/templates/vault/cluster/replication/mode/index.hbs
@@ -0,0 +1,4 @@
+{{replication-summary
+ cluster=model
+ initialReplicationMode=replicationMode
+}}
diff --git a/ui/app/templates/vault/cluster/replication/mode/manage.hbs b/ui/app/templates/vault/cluster/replication/mode/manage.hbs
new file mode 100644
index 000000000..17fb025b9
--- /dev/null
+++ b/ui/app/templates/vault/cluster/replication/mode/manage.hbs
@@ -0,0 +1,11 @@
+{{#each (replication-action-for-mode replicationMode model.replicationAttrs.modeForUrl) as |replicationAction index|}}
+ {{#if (get model (concat 'can' (camelize replicationAction)))}}
+
+ {{replication-actions
+ replicationMode=replicationMode
+ model=model
+ selectedAction=replicationAction
+ }}
+
+ {{/if}}
+{{/each}}
diff --git a/ui/app/templates/vault/cluster/replication/mode/secondaries/add.hbs b/ui/app/templates/vault/cluster/replication/mode/secondaries/add.hbs
new file mode 100644
index 000000000..86e921521
--- /dev/null
+++ b/ui/app/templates/vault/cluster/replication/mode/secondaries/add.hbs
@@ -0,0 +1,90 @@
+
+
+
+ Generate a secondary token
+
+
Generate a token to enable {{replicationMode}} replication or change primaries on secondary cluster.
+
+ {{message-error errors=errors}}
+ {{#if token}}
+
+
+ Activation token
+
+
+
+
+
+
+
+ {{#copy-button
+ clipboardText=token
+ class="button is-primary"
+ buttonType="button"
+ success=(action (set-flash-message 'Activation token copied!'))
+ }}
+ Copy
+ {{/copy-button}}
+
+
+
+ Back
+
+
+
+ {{else}}
+
+
+ Secondary ID
+
+
+ {{input class="input" name="activation-token-id" id="activation-token-id" value=id data-test-replication-secondary-id=true}}
+
+
+ This will be used to identify secondary cluster once a connection has been established with the primary.
+
+
+
+ {{ttl-picker onChange=(action (mut ttl)) class="is-marginless"}}
+
+ This is the Time To Live for the generated secondary token. After this period, the generated token will no longer be valid.
+
+
+ {{#if (eq replicationMode "performance")}}
+
+ {{toggle-button
+ toggleTarget=this
+ toggleAttr='showFilterConfig'
+ closedLabel='Configure performance mount filtering'
+ openLabel='Hide performance mount filtering config'
+ data-test-replication-secondary-token-options=true
+ }}
+ {{#if showFilterConfig}}
+
+ {{mount-filter-config-list
+ config=filterConfig
+ mounts=mounts
+ id=id
+ }}
+
+ {{/if}}
+
+ {{/if}}
+
+
+
+ Generate token
+
+
+
+ {{#link-to "vault.cluster.replication.mode.secondaries" model.name replicationMode class="button"}}
+ Cancel
+ {{/link-to}}
+
+
+ {{/if}}
+
diff --git a/ui/app/templates/vault/cluster/replication/mode/secondaries/config-create.hbs b/ui/app/templates/vault/cluster/replication/mode/secondaries/config-create.hbs
new file mode 100644
index 000000000..b168a4934
--- /dev/null
+++ b/ui/app/templates/vault/cluster/replication/mode/secondaries/config-create.hbs
@@ -0,0 +1,27 @@
+
+
+ Create a mount filter config for {{model.config.id}}
+
+
+
+ {{mount-filter-config-list
+ mounts=model.mounts
+ config=model.config
+ }}
+
+
+
+ Create
+
+
+
+ {{#link-to
+ "vault.cluster.replication.mode.secondaries.config-show"
+ model.config.id
+ class="button"
+ }}
+ Cancel
+ {{/link-to}}
+
+
+
diff --git a/ui/app/templates/vault/cluster/replication/mode/secondaries/config-edit.hbs b/ui/app/templates/vault/cluster/replication/mode/secondaries/config-edit.hbs
new file mode 100644
index 000000000..f5c1085ba
--- /dev/null
+++ b/ui/app/templates/vault/cluster/replication/mode/secondaries/config-edit.hbs
@@ -0,0 +1,38 @@
+
+
+ Edit mount filter config for {{model.config.id}}
+
+
+
+ {{mount-filter-config-list
+ mounts=model.mounts
+ config=model.config
+ }}
+
+
+
+
+ Save
+
+
+
+ {{#link-to
+ "vault.cluster.replication.mode.secondaries.config-show"
+ model.config.id
+ class="button"
+ }}
+ Cancel
+ {{/link-to}}
+
+
+ {{#confirm-action
+ onConfirmAction=(action "saveConfig" model.config true)
+ confirmMessage=(concat "Are you sure you want to delete the filter config for " model.config.id "?")
+ confirmButtonText="Delete"
+ cancelButtonText="Cancel"
+ data-test-delete-mount-config=true
+ }}
+ Delete
+ {{/confirm-action}}
+
+
diff --git a/ui/app/templates/vault/cluster/replication/mode/secondaries/config-show.hbs b/ui/app/templates/vault/cluster/replication/mode/secondaries/config-show.hbs
new file mode 100644
index 000000000..565e5573c
--- /dev/null
+++ b/ui/app/templates/vault/cluster/replication/mode/secondaries/config-show.hbs
@@ -0,0 +1,56 @@
+
+
+ Mount filter config for {{model.config.id}}
+
+
+{{#if model.config.mode}}
+
+ {{info-table-row label="Mode" value=model.config.mode data-test-mount-config-mode=true}}
+ {{#info-table-row label="Paths" value=model.config.paths}}
+
+ {{#each model.config.paths as |path|}}
+
+ {{path}}
+
+ {{/each}}
+
+ {{/info-table-row}}
+
+{{else}}
+
+
+
+
+
+ The secondary {{model.config.id}}
does not currently have performance mount filtering configured.
+
+
+
+
+
+{{/if}}
+
+ {{#if model.config.mode}}
+
+ {{#link-to
+ 'vault.cluster.replication.mode.secondaries.config-edit'
+ model.config.id
+ class="button"
+ data-test-replication-link="edit-mount-config"
+ }}
+ Edit
+ {{/link-to}}
+
+ {{else}}
+
+ {{#link-to
+ 'vault.cluster.replication.mode.secondaries.config-create'
+ model.config.id
+ class="button"
+ data-test-replication-link="create-mount-config"
+ }}
+ Create
+ {{/link-to}}
+
+ {{/if}}
+
diff --git a/ui/app/templates/vault/cluster/replication/mode/secondaries/index.hbs b/ui/app/templates/vault/cluster/replication/mode/secondaries/index.hbs
new file mode 100644
index 000000000..e40e40f79
--- /dev/null
+++ b/ui/app/templates/vault/cluster/replication/mode/secondaries/index.hbs
@@ -0,0 +1,77 @@
+{{#if model.replicationAttrs.isPrimary}}
+ {{#if model.replicationAttrs.knownSecondaries.length}}
+ {{#each model.replicationAttrs.knownSecondaries as |secondary|}}
+
+
+
+ {{secondary}}
+
+
+ {{#if (or (eq replicationMode 'performance') model.canRevokeSecondary)}}
+ {{#popup-menu name="secondary-details"}}
+
+ {{/popup-menu}}
+ {{/if}}
+
+
+
+
+ {{/each}}
+ {{else}}
+
+
+
+
+
+ There are currently no known {{performanceMode}} secondary clusters associated with this cluster.
+
+
+
+
+
+ {{/if}}
+
+ {{#if model.canAddSecondary}}
+
+ {{#link-to 'vault.cluster.replication.mode.secondaries.add' model.name replicationMode class="button" data-test-secondary-add=true }}
+ Add
+ {{/link-to}}
+
+ {{/if}}
+ {{#if model.canRevokeSecondary}}
+
+ {{#link-to 'vault.cluster.replication.mode.secondaries.revoke' model.name replicationMode class="button"}}
+ Revoke
+ {{/link-to}}
+
+ {{/if}}
+
+{{/if}}
diff --git a/ui/app/templates/vault/cluster/replication/mode/secondaries/revoke.hbs b/ui/app/templates/vault/cluster/replication/mode/secondaries/revoke.hbs
new file mode 100644
index 000000000..b44ce837b
--- /dev/null
+++ b/ui/app/templates/vault/cluster/replication/mode/secondaries/revoke.hbs
@@ -0,0 +1,40 @@
+
+
+ Revoke a secondary token
+
+
+{{message-error errors=errors}}
+
+
+ Secondary ID
+
+
+ {{input class="input" name="activation-token-id" id="activation-token-id" value=id }}
+
+
+ The secondary id to revoke; given initially to generate a secondary token.
+
+
+
+
+ {{#confirm-action
+ onConfirmAction=(action "onSubmit" "revoke-secondary" "primary" (hash id=id))
+ confirmMessage=(concat "Are you sure you want to revoke " id "?")
+ disabledMessage="A secondary ID is required perform revocation."
+ buttonClasses="is-primary button"
+ confirmButtonText="Revoke"
+ cancelButtonText="Cancel"
+ showConfirm=isRevoking
+ disabled=(not id)
+ }}
+ Revoke
+ {{/confirm-action}}
+
+
+ {{#unless isRevoking}}
+ {{#link-to "vault.cluster.replication.mode.secondaries" model.name replicationMode class="button"}}
+ Cancel
+ {{/link-to}}
+ {{/unless}}
+
+
diff --git a/ui/app/templates/vault/cluster/secrets/backend/credentials.hbs b/ui/app/templates/vault/cluster/secrets/backend/credentials.hbs
new file mode 100644
index 000000000..4123d2efd
--- /dev/null
+++ b/ui/app/templates/vault/cluster/secrets/backend/credentials.hbs
@@ -0,0 +1,5 @@
+{{generate-credentials
+ role=model
+ backend=backend
+ action=action
+}}
diff --git a/ui/app/templates/vault/cluster/secrets/backend/error.hbs b/ui/app/templates/vault/cluster/secrets/backend/error.hbs
new file mode 100644
index 000000000..5a44976b6
--- /dev/null
+++ b/ui/app/templates/vault/cluster/secrets/backend/error.hbs
@@ -0,0 +1,71 @@
+
+
+
+ {{#if (eq model.httpStatus 404)}}
+
+ Unable to find secret at {{concat model.backend "/" model.secret}}
. Try going back to the
+ {{#link-to params=(if model.hasBackend
+ (reduce-to-array "vault.cluster.secrets.backend.list-root")
+ (reduce-to-array "vault.cluster.secrets")
+ )
+ }}root{{/link-to}}
+ and navigating from there.
+
+ {{else if (eq model.httpStatus 403)}}
+ {{#if (eq model.policyPath 'sys/capabilities-self') }}
+
+ Your auth token does not have access to {{model.policyPath}}
. Vault Enterprise uses this endpoint to determine what actions are allowed in the interface.
+
+
+ Make sure the policy for the path {{model.policyPath}}
includes capabilities = ['update']
.
+
+ {{else}}
+
+ You don't have access to {{if model.secret (concat model.backend '/' model.secret) (concat model.backend '/')}}
. If you think you've reached this page in error, please contact your administrator.
+
+
+ {{#if model.secret}}
+ {{#link-to "vault.cluster.secrets.backend.list-root"}}Navigate back to the root{{/link-to}}.
+ {{else}}
+ {{#home-link}}Go back home{{/home-link}}.
+ {{/if}}
+
+ {{/if}}
+ {{else}}
+ {{#if model.message}}
+
{{model.message}}
+ {{/if}}
+ {{#each model.errors as |error|}}
+
+ {{error}}
+
+ {{/each}}
+ {{/if}}
+
+
diff --git a/ui/app/templates/vault/cluster/secrets/backend/list.hbs b/ui/app/templates/vault/cluster/secrets/backend/list.hbs
new file mode 100644
index 000000000..b746f7bd0
--- /dev/null
+++ b/ui/app/templates/vault/cluster/secrets/backend/list.hbs
@@ -0,0 +1,151 @@
+{{#with (options-for-backend backendType tab) as |options|}}
+
+ {{#if options.tabs}}
+
+
+
+ {{#each options.tabs as |oTab|}}
+ {{#if oTab.tab}}
+ {{#link-to 'vault.cluster.secrets.backend.list-root' (query-params tab=oTab.tab) tagName="li" activeClass="is-active" data-test-tab=oTab.label}}
+ {{#link-to 'vault.cluster.secrets.backend.list-root' (query-params tab=oTab.tab)}}
+ {{oTab.label}}
+ {{/link-to}}
+ {{/link-to}}
+ {{else}}
+ {{#link-to 'vault.cluster.secrets.backend.list-root' (query-params tab='') tagName="li" activeClass="is-active" data-test-tab=oTab.label}}
+ {{#link-to 'vault.cluster.secrets.backend.list-root' (query-params tab='')}}
+ {{oTab.label}}
+ {{/link-to}}
+ {{/link-to}}
+ {{/if}}
+ {{/each}}
+
+
+
+ {{/if}}
+
+
+
+ {{navigate-input
+ enterpriseProduct="vault"
+ filterFocusDidChange=(action "setFilterFocus")
+ filterDidChange=(action "setFilter")
+ filter=filter
+ filterMatchesKey=filterMatchesKey
+ firstPartialMatch=firstPartialMatch
+ baseKey=(get baseKey "id")
+ shouldNavigateTree=options.navigateTree
+ placeholder=options.searchPlaceholder
+ mode=(if (eq tab 'certs') 'secrets-cert' 'secrets')
+ }}
+ {{#if filterFocused}}
+
+
+ {{#if filterMatchesKey}}
+ {{#unless filterIsFolder}}
+
+ Enter to view {{filter}}
+
+ {{/unless}}
+ {{/if}}
+ {{#if firstPartialMatch}}
+
+ Tab to autocomplete
+
+ {{/if}}
+ {{/if}}
+
+
+
+ {{#if model.meta.total}}
+ {{#each model as |item|}}
+ {{partial options.listItemPartial}}
+ {{else}}
+
+ {{#if filterFocused}}
+ There are no {{pluralize options.item}} matching {{filter}}
+ {{#if capabilities.canCreate}}, press ENTER to add one{{/if}}.
+ {{else}}
+ There are no {{pluralize options.item}} matching {{filter}}
.
+ {{/if}}
+
+ {{/each}}
+ {{else}}
+
+
+
+
+
+ {{#if (eq baseKey.id '')}}
+ There are currently no {{pluralize options.item}} in this backend.
+ {{else}}
+ {{#if filterIsFolder}}
+ {{#if (eq filter baseKey.id)}}
+ There are no {{pluralize options.item}} under {{or filter}}
.
+ {{else}}
+ We couldn't find a folder matching {{filter}}
.
+ {{/if}}
+ {{/if}}
+ {{/if}}
+
+
+
+
+
+ {{/if}}
+{{/with}}
+{{#if (gt model.meta.lastPage 1) }}
+ {{list-pagination
+ page=model.meta.currentPage
+ lastPage=model.meta.lastPage
+ link=(concat "vault.cluster.secrets.backend.list" (if (not baseKey.id) "-root"))
+ model=(compact (array backend (if baseKey.id baseKey.id)))
+ }}
+{{/if}}
diff --git a/ui/app/templates/vault/cluster/secrets/backend/secret-edit-layout.hbs b/ui/app/templates/vault/cluster/secrets/backend/secret-edit-layout.hbs
new file mode 100644
index 000000000..294e94cdf
--- /dev/null
+++ b/ui/app/templates/vault/cluster/secrets/backend/secret-edit-layout.hbs
@@ -0,0 +1,13 @@
+ {{component (get (options-for-backend backendType model.idPrefix) 'editComponent')
+ key=model
+ tab=tab
+ model=model
+ mode=mode
+ root=backendCrumb
+ capabilities=capabilities
+ onDataChange=(action "hasChanges")
+ onRefresh=(action "refresh")
+ initialKey=initialKey
+ baseKey=baseKey
+ preferAdvancedEdit=preferAdvancedEdit
+ }}
diff --git a/ui/app/templates/vault/cluster/secrets/backend/sign.hbs b/ui/app/templates/vault/cluster/secrets/backend/sign.hbs
new file mode 100644
index 000000000..61fe1c3a6
--- /dev/null
+++ b/ui/app/templates/vault/cluster/secrets/backend/sign.hbs
@@ -0,0 +1,124 @@
+
+
+{{#if model.signedKey}}
+
+ {{#message-in-page type="warning" data-test-warning=true}}
+ You will not be able to access this information later, so please copy the information below.
+ {{/message-in-page}}
+ {{#each model.attrs as |attr|}}
+ {{#if (eq attr.type "object")}}
+ {{info-table-row label=(capitalize (or attr.options.label (humanize (dasherize attr.name)))) value=(stringify (get model attr.name))}}
+ {{else}}
+ {{info-table-row label=(capitalize (or attr.options.label (humanize (dasherize attr.name)))) value=(get model attr.name)}}
+ {{/if}}
+ {{/each}}
+
+
+
+ {{#copy-button
+ clipboardText=model.signedKey
+ class="button is-primary"
+ buttonType="button"
+ success=(action (set-flash-message "Key copied!"))
+ }}
+ Copy Key
+ {{/copy-button}}
+
+ {{#if model.leaseId}}
+
+ {{#copy-button
+ clipboardText=model.leaseId
+ class="button"
+ buttonType="button"
+ success=(action (set-flash-message "Lease ID copied!"))
+ }}
+ Copy Lease ID
+ {{/copy-button}}
+
+ {{/if}}
+
+
+ Back
+
+
+
+{{else}}
+
+
+ {{message-error model=model}}
+ {{#each (take 1 model.attrs) as |attr|}}
+ {{partial "partials/form-field-from-model"}}
+ {{/each}}
+ {{toggle-button
+ toggleAttr="showOptions"
+ toggleTarget=this
+ openLabel="Hide options"
+ closedLabel="More options"
+ }}
+ {{#if showOptions}}
+
+ {{#each (drop 1 model.attrs) as |attr|}}
+ {{partial "partials/form-field-from-model"}}
+ {{/each}}
+
+ {{/if}}
+
+
+
+
+ Sign
+
+
+
+ {{#link-to
+ "vault.cluster.secrets.backend.list-root"
+ backend.id
+ class="button"
+ data-test-secret-generate-cancel=true
+ }}
+ Cancel
+ {{/link-to}}
+
+
+
+{{/if}}
diff --git a/ui/app/templates/vault/cluster/secrets/backend/transit-actions-layout.hbs b/ui/app/templates/vault/cluster/secrets/backend/transit-actions-layout.hbs
new file mode 100644
index 000000000..e58d57b98
--- /dev/null
+++ b/ui/app/templates/vault/cluster/secrets/backend/transit-actions-layout.hbs
@@ -0,0 +1,51 @@
+
+ {{#menu-sidebar title="Transit Actions" class="is-2"}}
+ {{#each model.supportedActions as |supportedAction|}}
+
+ {{#secret-link
+ mode="actions"
+ secret=model.id
+ class=(if (eq supportedAction selectedAction) "is-active")
+ queryParams=(query-params action=supportedAction)
+ data-test-transit-action-link=supportedAction
+ }}
+ {{capitalize supportedAction}}
+ {{/secret-link}}
+
+ {{/each}}
+ {{/menu-sidebar}}
+
+
+
+
diff --git a/ui/app/templates/vault/cluster/secrets/backends.hbs b/ui/app/templates/vault/cluster/secrets/backends.hbs
new file mode 100644
index 000000000..8b427b03b
--- /dev/null
+++ b/ui/app/templates/vault/cluster/secrets/backends.hbs
@@ -0,0 +1,100 @@
+
+
+{{#each supportedBackends as |backend|}}
+ {{#linked-block
+ "vault.cluster.secrets.backend.list-root"
+ backend.id
+ class=(concat
+ 'box is-sideless is-marginless has-pointer '
+ (if (get this (concat backend.accessor '-open')) 'has-background-white-bis')
+ )
+ data-test-secret-backend-link=backend.id
+ }}
+
+
+
+
+
+ {{i-con glyph="more" size=16 aria-label=(concat backend.path ' details')}}
+
+
+
+
+ {{#if (get this (concat backend.accessor '-open'))}}
+ {{partial "partials/backend-details"}}
+ {{/if}}
+ {{/linked-block}}
+{{/each}}
+{{#each unsupportedBackends as |backend|}}
+
+
+
+
+
+ {{i-con glyph="folder" size=14 class="has-text-grey-light"}} {{backend.path}}
+
+
+
+ {{#if (eq backend.type 'plugin')}}
+ {{backend.type}}: {{backend.config.plugin_name}}
+ {{else}}
+ {{backend.type}}
+ {{/if}}
+
+
+
+ {{backend.accessor}}
+
+
+
+
+
+
+ {{i-con glyph="more" size=16 aria-label=(concat backend.path ' details')}}
+
+
+
+
+ {{#if (get this (concat backend.accessor '-open'))}}
+ {{partial "partials/backend-details"}}
+ {{/if}}
+
+{{/each}}
diff --git a/ui/app/templates/vault/cluster/settings.hbs b/ui/app/templates/vault/cluster/settings.hbs
new file mode 100644
index 000000000..d8b3f9d90
--- /dev/null
+++ b/ui/app/templates/vault/cluster/settings.hbs
@@ -0,0 +1,22 @@
+
+ {{#menu-sidebar title="Settings" class="is-3"}}
+
+ {{#link-to "vault.cluster.settings.mount-secret-backend" current-when="vault.cluster.settings.mount-secret-backend vault.cluster.settings.configure-secret-backend"}}
+ Secret Engines
+ {{/link-to}}
+
+
+ {{#link-to "vault.cluster.settings.auth"}}
+ Auth Methods
+ {{/link-to}}
+
+
+ {{#link-to "vault.cluster.settings.seal"}}
+ Seal
+ {{/link-to}}
+
+ {{/menu-sidebar}}
+
+ {{outlet}}
+
+
diff --git a/ui/app/templates/vault/cluster/settings/auth/configure.hbs b/ui/app/templates/vault/cluster/settings/auth/configure.hbs
new file mode 100644
index 000000000..e58985eb2
--- /dev/null
+++ b/ui/app/templates/vault/cluster/settings/auth/configure.hbs
@@ -0,0 +1,33 @@
+
+
+{{section-tabs model}}
+{{outlet}}
diff --git a/ui/app/templates/vault/cluster/settings/auth/configure/section.hbs b/ui/app/templates/vault/cluster/settings/auth/configure/section.hbs
new file mode 100644
index 000000000..9c2c02d0c
--- /dev/null
+++ b/ui/app/templates/vault/cluster/settings/auth/configure/section.hbs
@@ -0,0 +1,5 @@
+{{#if (eq model.section "options") }}
+ {{auth-config-form/options model.model}}
+{{else}}
+ {{auth-config-form/config model.model}}
+{{/if}}
diff --git a/ui/app/templates/vault/cluster/settings/auth/enable.hbs b/ui/app/templates/vault/cluster/settings/auth/enable.hbs
new file mode 100644
index 000000000..3ec71759e
--- /dev/null
+++ b/ui/app/templates/vault/cluster/settings/auth/enable.hbs
@@ -0,0 +1,4 @@
+{{mount-backend-form
+ onMountSuccess=(action "onMountSuccess")
+ onConfigError=(action "onConfigError")
+}}
diff --git a/ui/app/templates/vault/cluster/settings/configure-secret-backend.hbs b/ui/app/templates/vault/cluster/settings/configure-secret-backend.hbs
new file mode 100644
index 000000000..d5d1c003f
--- /dev/null
+++ b/ui/app/templates/vault/cluster/settings/configure-secret-backend.hbs
@@ -0,0 +1,33 @@
+
+
+{{partial (concat "partials/secret-backend-settings/" model.type)}}
+{{outlet}}
diff --git a/ui/app/templates/vault/cluster/settings/configure-secret-backend/section.hbs b/ui/app/templates/vault/cluster/settings/configure-secret-backend/section.hbs
new file mode 100644
index 000000000..5120c39a1
--- /dev/null
+++ b/ui/app/templates/vault/cluster/settings/configure-secret-backend/section.hbs
@@ -0,0 +1,7 @@
+{{#if (eq model.backendType "pki")}}
+ {{#if (eq model.section "cert")}}
+ {{config-pki-ca config=model onRefresh=onRefresh}}
+ {{else}}
+ {{config-pki config=model onRefresh=onRefresh section=model.section}}
+ {{/if}}
+{{/if}}
diff --git a/ui/app/templates/vault/cluster/settings/mount-secret-backend.hbs b/ui/app/templates/vault/cluster/settings/mount-secret-backend.hbs
new file mode 100644
index 000000000..24fa1296a
--- /dev/null
+++ b/ui/app/templates/vault/cluster/settings/mount-secret-backend.hbs
@@ -0,0 +1,102 @@
+
+
+
+ {{message-error model=model}}
+
+
Secret backend
+
+
+
+ {{#each mountTypes as |backend|}}
+
+ {{backend.label}}{{if backend.deprecated " (deprecated)"}}
+
+ {{/each}}
+
+
+
+
+ {{#if selection.deprecated}}
+
+
+ The {{selection.label}} backend is deprecated! If you are using a SQL database backend, use the general purpose {{#doc-link path="/secrets/databases/index.html"}}Databases{{/doc-link}} backend instead.
+
+
+ {{/if}}
+
+
Path
+
+ {{input value=selectedPath class="input" id="backend-path" data-test-secret-backend-path=true}}
+
+
+
+
Description
+
+ {{textarea class="editor" value=description id="backend-description" class="textarea"}}
+
+
+ {{#if (eq selectedType "kv")}}
+
+
+ {{input type="checkbox" id="versioned" name="versioned" checked=versioned}}
+
+ Versioned
+
+
+ The KV Secrets engine can operate in versioned or non-versioned mode. Non-versioned may be upgraded to versioned later, but the opposite is not true .
+
+
+
+ {{/if}}
+
+
+ {{input type="checkbox" id="local" name="local" checked=local}}
+
+ Local
+
+
+ When replication is enabled, a local mount will not be replicated across clusters. This can only be specified at mount time .
+
+
+
+
+
+
+ {{input type="checkbox" id="sealWrap" name="sealWrap" checked=sealWrap}}
+
+ Seal Wrap
+
+
+ When enabled - if a seal supporting seal wrapping is specified in the configuration, all items in this backend will be seal wrapped. This can only be specified at mount time .
+
+
+
+
+ {{toggle-button toggleTarget=this toggleAttr="showConfig" data-test-secret-backend-options=true}}
+ {{#if showConfig}}
+
+ {{ttl-picker data-test-secret-backend-default-ttl=true labelText='Default lease TTL' onChange=(action (mut default_lease_ttl))}}
+ {{ttl-picker data-test-secret-backend-max-ttl labelText='Maximum lease TTL' onChange=(action (mut max_lease_ttl))}}
+
+ {{/if}}
+
+
+
+
+ Enable Engine
+
+
+
diff --git a/ui/app/templates/vault/cluster/settings/seal.hbs b/ui/app/templates/vault/cluster/settings/seal.hbs
new file mode 100644
index 000000000..9d657951d
--- /dev/null
+++ b/ui/app/templates/vault/cluster/settings/seal.hbs
@@ -0,0 +1,43 @@
+
+
+{{#if model.seal.canUpdate}}
+
+
+ Sealing a vault tells the Vault server to stop responding to any
+ access operations until it is unsealed again. A sealed vault throws away
+ its master key to unlock the data, so it physically is blocked from
+ responding to operations again until the Vault is unsealed again with
+ the "unseal" command or via the API.
+
+
+
+ {{#confirm-action
+ onConfirmAction=(action "seal")
+ confirmMessage=(concat "Are you sure you want to seal " model.cluster.name "?")
+ confirmButtonText="Seal"
+ buttonClasses="button is-primary"
+ cancelButtonText="Cancel"
+ data-test-seal=true
+ }}
+ Seal
+ {{/confirm-action}}
+
+{{else}}
+
+
+
+
+
The token you are currently authenticated with does not have sufficient capabilities to seal this vault.
+
+
+
+
+{{/if}}
diff --git a/ui/app/templates/vault/cluster/tools/tool.hbs b/ui/app/templates/vault/cluster/tools/tool.hbs
new file mode 100644
index 000000000..17109cb0d
--- /dev/null
+++ b/ui/app/templates/vault/cluster/tools/tool.hbs
@@ -0,0 +1,17 @@
+
+ {{#menu-sidebar title="Tools" class="is-3"}}
+ {{#each (tools-actions) as |supportedAction|}}
+
+
+ {{capitalize supportedAction}}
+
+
+ {{/each}}
+ {{/menu-sidebar}}
+
+ {{tool-actions-form selectedAction=selectedAction}}
+
+
diff --git a/ui/app/templates/vault/cluster/unseal.hbs b/ui/app/templates/vault/cluster/unseal.hbs
new file mode 100644
index 000000000..73c7ab8e8
--- /dev/null
+++ b/ui/app/templates/vault/cluster/unseal.hbs
@@ -0,0 +1,31 @@
+{{#splash-page as |s|}}
+ {{#s.header}}
+
+ Unseal Vault
+
+ {{/s.header}}
+ {{#s.content}}
+
+
+
+
+ {{i-con glyph="unlocked" size=20}} {{capitalize model.name}} is {{if model.unsealed 'unsealed' 'sealed'}}
+
+
+
+
+ {{#shamir-flow
+ action="unseal"
+ onShamirSuccess=(action 'transitionToCluster')
+ buttonText="Unseal"
+ thresholdPath="t"
+ isComplete=(action 'isUnsealed')
+ threshold=model.sealThreshold
+ progress=model.sealProgress
+ }}
+
+ Unseal the vault by entering a portion of the master key. Once all portions are entered, the vault will be unsealed.
+
+ {{/shamir-flow}}
+ {{/s.content}}
+{{/splash-page}}
diff --git a/ui/app/templates/vault/error.hbs b/ui/app/templates/vault/error.hbs
new file mode 100644
index 000000000..0a55e026c
--- /dev/null
+++ b/ui/app/templates/vault/error.hbs
@@ -0,0 +1,38 @@
+
+
+
+ {{#if (eq model.httpStatus 404)}}
+ {{not-found model=model}}
+ {{else}}
+
+
+ {{#if model.message}}
+
{{model.message}}
+ {{/if}}
+ {{#each model.errors as |error|}}
+
{{error}}
+ {{/each}}
+
+ {{/if}}
+
+
diff --git a/ui/app/templates/vault/not-found.hbs b/ui/app/templates/vault/not-found.hbs
new file mode 100644
index 000000000..1c53df4e7
--- /dev/null
+++ b/ui/app/templates/vault/not-found.hbs
@@ -0,0 +1,14 @@
+
+
+
+ {{not-found model=model}}
+
+
diff --git a/ui/app/transforms/array.js b/ui/app/transforms/array.js
new file mode 100644
index 000000000..cd1f94d51
--- /dev/null
+++ b/ui/app/transforms/array.js
@@ -0,0 +1,23 @@
+import Ember from 'ember';
+import DS from 'ember-data';
+/*
+ This should go inside a globally available place for all apps
+
+ DS.attr('array')
+*/
+export default DS.Transform.extend({
+ deserialize(value) {
+ if (Ember.isArray(value)) {
+ return Ember.A(value);
+ } else {
+ return Ember.A();
+ }
+ },
+ serialize(value) {
+ if (Ember.isArray(value)) {
+ return Ember.A(value);
+ } else {
+ return Ember.A();
+ }
+ },
+});
diff --git a/ui/app/transforms/object.js b/ui/app/transforms/object.js
new file mode 100644
index 000000000..4bf7ddd8d
--- /dev/null
+++ b/ui/app/transforms/object.js
@@ -0,0 +1,21 @@
+import $ from 'jquery';
+import DS from 'ember-data';
+/*
+ DS.attr('object')
+*/
+export default DS.Transform.extend({
+ deserialize: function(value) {
+ if (!$.isPlainObject(value)) {
+ return {};
+ } else {
+ return value;
+ }
+ },
+ serialize: function(value) {
+ if (!$.isPlainObject(value)) {
+ return {};
+ } else {
+ return value;
+ }
+ },
+});
diff --git a/ui/app/utils/b64.js b/ui/app/utils/b64.js
new file mode 100644
index 000000000..f20fe4ecc
--- /dev/null
+++ b/ui/app/utils/b64.js
@@ -0,0 +1,9 @@
+export function encodeString(string) {
+ var encoded = new TextEncoderLite('utf-8').encode(string);
+ return base64js.fromByteArray(encoded);
+}
+
+export function decodeString(b64String) {
+ var uint8array = base64js.toByteArray(b64String);
+ return new TextDecoderLite('utf-8').decode(uint8array);
+}
diff --git a/ui/app/utils/clamp.js b/ui/app/utils/clamp.js
new file mode 100644
index 000000000..4b63a502c
--- /dev/null
+++ b/ui/app/utils/clamp.js
@@ -0,0 +1,9 @@
+export default function(num, min, max) {
+ let inRangeNumber;
+ if (typeof num !== 'number') {
+ inRangeNumber = min;
+ } else {
+ inRangeNumber = num;
+ }
+ return Math.min(Math.max(inRangeNumber, min), max);
+}
diff --git a/ui/app/utils/decode-config-from-jwt.js b/ui/app/utils/decode-config-from-jwt.js
new file mode 100644
index 000000000..d0c840621
--- /dev/null
+++ b/ui/app/utils/decode-config-from-jwt.js
@@ -0,0 +1,34 @@
+import { decodeString } from 'vault/utils/b64';
+
+/*
+ * @param token - Replication Secondary Activation Token
+ * @returns config Object if successful | undefined if not
+ *
+ */
+export default function(token) {
+ if (!token) {
+ return;
+ }
+ const tokenParts = token.split('.');
+ // config is the second item in the JWT
+ let [, configB64] = tokenParts;
+ let config;
+
+ if (tokenParts.length !== 3) {
+ return;
+ }
+
+ // JWTs strip padding from their b64 parts.
+ // since we're converting to a typed array before
+ // decoding back to utf-8, we need to add any padding back
+ while (configB64.length % 4 !== 0) {
+ configB64 = configB64 + '=';
+ }
+ try {
+ config = JSON.parse(decodeString(configB64));
+ } catch (e) {
+ // swallow error
+ }
+
+ return config;
+}
diff --git a/ui/app/utils/field-to-attrs.js b/ui/app/utils/field-to-attrs.js
new file mode 100644
index 000000000..723b76957
--- /dev/null
+++ b/ui/app/utils/field-to-attrs.js
@@ -0,0 +1,101 @@
+import Ember from 'ember';
+
+/*
+ *
+ * @param modelClass DS.Model
+ * @param attributeNames Array[String]
+ * @param prefixName String
+ * @param map Map
+ * @returns Array[Object]
+ *
+ * A function that takes a model and an array of attributes
+ * and expands them in-place to an array of metadata about the attributes
+ *
+ * if passed a Model with attributes `foo` and `bar` and the array ['foo', 'bar']
+ * the returned array would take the form of:
+ *
+ * [
+ * {
+ * name: 'foo',
+ * type: 'string',
+ * options: {
+ * defaultValue: 'Foo'
+ * }
+ * },
+ * {
+ * name: 'bar',
+ * type: 'string',
+ * options: {
+ * defaultValue: 'Bar',
+ * editType: 'textarea',
+ * label: 'The Bar Field'
+ * }
+ * },
+ * ]
+ *
+ */
+
+export const expandAttributeMeta = function(modelClass, attributeNames, namePrefix, map) {
+ let fields = [];
+ // expand all attributes
+ attributeNames.forEach(field => Ember.expandProperties(field, x => fields.push(x)));
+ let attributeMap = map || new Map();
+ modelClass.eachAttribute((name, meta) => {
+ let fieldName = namePrefix ? namePrefix + name : name;
+ if (meta.isFragment) {
+ // pass the fragment and all fields that start with
+ // the fragment name down to get extracted from the Fragment
+ expandAttributeMeta(
+ Ember.get(modelClass, fieldName),
+ fields.filter(f => f.startsWith(fieldName)),
+ fieldName + '.',
+ attributeMap
+ );
+ return;
+ }
+ attributeMap.set(fieldName, meta);
+ });
+
+ // we have all of the attributes in the map now,
+ // so we'll replace each key in `fields` with the expanded meta
+ fields = fields.map(field => {
+ let meta = attributeMap.get(field);
+ const { type, options } = meta;
+ return {
+ // using field name here because it is the full path,
+ // name on the attribute meta will be relative to the fragment it's on
+ name: field,
+ type,
+ options,
+ };
+ });
+ return fields;
+};
+
+/*
+ *
+ * @param modelClass DS.Model
+ * @param fieldGroups Array[Object]
+ * @returns Array
+ *
+ * A function meant for use on an Ember Data Model
+ *
+ * The function takes a array of groups, each group
+ * being a list of attributes on the model, for example
+ * `fieldGroups` could look like this
+ *
+ * [
+ * { default: ['commonName', 'format'] },
+ * { Options: ['altNames', 'ipSans', 'ttl', 'excludeCnFromSans'] },
+ * ]
+ *
+ * The array will get mapped over producing a new array with each attribute replaced with that attribute's metadata from the attr declaration
+ */
+
+export default function(modelClass, fieldGroups) {
+ return fieldGroups.map(group => {
+ const groupKey = Object.keys(group)[0];
+ const fields = expandAttributeMeta(modelClass, group[groupKey]);
+ return { [groupKey]: fields };
+ });
+}
diff --git a/ui/bower.json b/ui/bower.json
new file mode 100644
index 000000000..b1f238a8d
--- /dev/null
+++ b/ui/bower.json
@@ -0,0 +1,12 @@
+{
+ "name": "vault",
+ "dependencies": {
+ "text-encoder-lite": "1.0.0",
+ "base64-js": "1.2.1",
+ "autosize": "3.0.17",
+ "jsonlint": "1.6.0",
+ "codemirror": "5.15.2",
+ "Duration.js": "icholy/Duration.js#golang_compatible",
+ "string.prototype.startswith": "mathiasbynens/String.prototype.startsWith"
+ }
+}
diff --git a/ui/config/environment.js b/ui/config/environment.js
new file mode 100644
index 000000000..32a9cfaca
--- /dev/null
+++ b/ui/config/environment.js
@@ -0,0 +1,60 @@
+/* jshint node: true */
+
+module.exports = function(environment) {
+ var ENV = {
+ modulePrefix: 'vault',
+ environment: environment,
+ rootURL: '/ui/',
+ locationType: 'auto',
+ EmberENV: {
+ FEATURES: {
+ // Here you can enable experimental features on an ember canary build
+ // e.g. 'with-controller': true
+ },
+ EXTEND_PROTOTYPES: {
+ // Prevent Ember Data from overriding Date.parse.
+ Date: false,
+ },
+ },
+
+ APP: {
+ // Here you can pass flags/options to your application instance
+ // when it is created
+ },
+ flashMessageDefaults: {
+ timeout: 7000,
+ sticky: false,
+ preventDuplicates: true,
+ },
+ };
+ if (environment === 'development') {
+ // ENV.APP.LOG_RESOLVER = true;
+ // ENV.APP.LOG_ACTIVE_GENERATION = true;
+ ENV.APP.LOG_TRANSITIONS = true;
+ // ENV.APP.LOG_TRANSITIONS_INTERNAL = true;
+ // ENV.APP.LOG_VIEW_LOOKUPS = true;
+ //ENV['ember-cli-mirage'] = {
+ //enabled: true
+ //};
+ }
+
+ if (environment === 'test') {
+ // Testem prefers this...
+ ENV.locationType = 'none';
+
+ // keep test console output quieter
+ ENV.APP.LOG_ACTIVE_GENERATION = false;
+ ENV.APP.LOG_VIEW_LOOKUPS = false;
+
+ ENV.APP.rootElement = '#ember-testing';
+
+ ENV['ember-cli-mirage'] = {
+ enabled: false,
+ };
+ }
+
+ if (environment === 'production') {
+ }
+
+ return ENV;
+};
diff --git a/ui/config/targets.js b/ui/config/targets.js
new file mode 100644
index 000000000..08d2ad4d6
--- /dev/null
+++ b/ui/config/targets.js
@@ -0,0 +1,10 @@
+/* eslint-env node */
+module.exports = {
+ browsers: [
+ 'ie 10',
+ 'last 2 Chrome versions',
+ 'last 2 Firefox versions',
+ 'last 2 Safari versions',
+ 'last 2 Edge versions',
+ ],
+};
diff --git a/ui/ember-cli-build.js b/ui/ember-cli-build.js
new file mode 100644
index 000000000..7a3aac357
--- /dev/null
+++ b/ui/ember-cli-build.js
@@ -0,0 +1,65 @@
+/*jshint node:true*/
+/* global require, module */
+var EmberApp = require('ember-cli/lib/broccoli/ember-app');
+
+module.exports = function(defaults) {
+ var config = defaults.project.config(EmberApp.env());
+ var app = new EmberApp(defaults, {
+ favicons: {
+ faviconsConfig: {
+ appName: 'Vault Enterprise',
+ path: config.rootURL,
+ url: null,
+ icons: {
+ android: false,
+ appleIcon: false,
+ appleStartup: false,
+ coast: false,
+ favicons: true,
+ firefox: false,
+ opengraph: false,
+ twitter: false,
+ windows: false,
+ yandex: false
+ }
+ }
+ },
+ codemirror: {
+ modes: ['javascript','ruby'],
+ keyMaps: ['sublime']
+ },
+ babel: {
+ plugins: [
+ 'transform-object-rest-spread'
+ ]
+ }
+ });
+
+ app.import('vendor/string-includes.js');
+ app.import(app.bowerDirectory + '/string.prototype.startswith/startswith.js');
+ app.import(app.bowerDirectory + '/autosize/dist/autosize.js');
+ app.import('vendor/shims/autosize.js');
+
+ app.import(app.bowerDirectory + '/jsonlint/lib/jsonlint.js');
+ app.import(app.bowerDirectory + '/codemirror/addon/lint/lint.css');
+ app.import(app.bowerDirectory + '/codemirror/addon/lint/lint.js');
+ app.import(app.bowerDirectory + '/codemirror/addon/lint/json-lint.js');
+ app.import(app.bowerDirectory + '/base64-js/base64js.min.js');
+ app.import(app.bowerDirectory + '/text-encoder-lite/index.js');
+ app.import(app.bowerDirectory + '/Duration.js/duration.js');
+
+ // Use `app.import` to add additional libraries to the generated
+ // output files.
+ //
+ // If you need to use different assets in different
+ // environments, specify an object as the first parameter. That
+ // object's keys should be the environment name and the values
+ // should be the asset to use in that environment.
+ //
+ // If the library that you are including contains AMD or ES6
+ // modules that you would like to import into your application
+ // please specify an object with the list of modules as keys
+ // along with the exports of each module as its value.
+
+ return app.toTree();
+};
diff --git a/ui/lib/.eslintrc.js b/ui/lib/.eslintrc.js
new file mode 100644
index 000000000..548ea343c
--- /dev/null
+++ b/ui/lib/.eslintrc.js
@@ -0,0 +1,6 @@
+module.exports = {
+ env: {
+ node: true,
+ browser: false,
+ },
+};
diff --git a/ui/lib/bulma/index.js b/ui/lib/bulma/index.js
new file mode 100644
index 000000000..075b56d4e
--- /dev/null
+++ b/ui/lib/bulma/index.js
@@ -0,0 +1,49 @@
+/* eslint-env node */
+'use strict';
+
+var path = require('path');
+var Funnel = require('broccoli-funnel');
+var mergeTrees = require('broccoli-merge-trees');
+
+module.exports = {
+ name: 'bulma',
+
+ isDevelopingAddon() {
+ return true;
+ },
+
+ included: function(app) {
+ this._super.included.apply(this, arguments);
+
+ // see: https://github.com/ember-cli/ember-cli/issues/3718
+ while (typeof app.import !== 'function' && app.app) {
+ app = app.app;
+ }
+
+ this.bulmaPath = path.dirname(require.resolve('bulma'));
+ this.bulmaSwitchPath = path.dirname(require.resolve('bulma-switch/switch.sass'));
+ this.bulmaCheckPath = path.dirname(require.resolve('cool-checkboxes-for-bulma.io'));
+ return app;
+ },
+
+ treeForStyles: function() {
+ var bulma = new Funnel(this.bulmaPath, {
+ srcDir: '/',
+ destDir: 'app/styles/bulma',
+ annotation: 'Funnel (bulma)',
+ });
+
+ var bulmaSwitch = new Funnel(this.bulmaSwitchPath, {
+ srcDir: '/',
+ destDir: 'app/styles/bulma',
+ annotation: 'Funnel (bulma-switch)',
+ });
+ var bulmaCheck = new Funnel(this.bulmaCheckPath, {
+ srcDir: '/',
+ destDir: 'app/styles/bulma',
+ annotation: 'Funnel (bulma-check)',
+ });
+
+ return mergeTrees([bulmaCheck, bulmaSwitch, bulma], { overwrite: true });
+ },
+};
diff --git a/ui/lib/bulma/package.json b/ui/lib/bulma/package.json
new file mode 100644
index 000000000..5dc02af6c
--- /dev/null
+++ b/ui/lib/bulma/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "bulma",
+ "keywords": [
+ "ember-addon"
+ ]
+}
diff --git a/ui/mirage/config.js b/ui/mirage/config.js
new file mode 100644
index 000000000..e4eb27aa8
--- /dev/null
+++ b/ui/mirage/config.js
@@ -0,0 +1,142 @@
+import Mirage from 'ember-cli-mirage';
+import { faker } from 'ember-cli-mirage';
+
+export default function() {
+ // These comments are here to help you get started. Feel free to delete them.
+
+ /*
+ Config (with defaults).
+
+ Note: these only affect routes defined *after* them!
+ */
+
+ // this.urlPrefix = ''; // make this `http://localhost:8080`, for example, if your API is on a different server
+ this.namespace = '/v1'; // make this `/api`, for example, if your API is namespaced
+ // this.timing = 400; // delay for each request, automatically set to 0 during testing
+
+ /*
+ Shorthand cheatsheet:
+
+ this.get('/posts');
+ this.post('/posts');
+ this.get('/posts/:id');
+ this.put('/posts/:id'); // or this.patch
+ this.del('/posts/:id');
+
+ http://www.ember-cli-mirage.com/docs/v0.2.x/shorthands/
+
+ */
+ this.post('/sys/replication/primary/enable', schema => {
+ var cluster = schema.clusters.first();
+ cluster.update('mode', 'primary');
+ return new Mirage.Response(204);
+ });
+
+ // primary_cluster_addr=(opt)
+
+ this.post('/sys/replication/primary/demote', schema => {
+ var cluster = schema.clusters.first();
+ cluster.update('mode', 'secondary');
+ return new Mirage.Response(204);
+ });
+
+ this.post('/sys/replication/primary/disable', schema => {
+ var cluster = schema.clusters.first();
+ cluster.update('mode', 'disabled');
+ return new Mirage.Response(204);
+ });
+ this.post('/sys/replication/primary/secondary-token', (schema, request) => {
+ //id=(req) ttl=(opt) (sudo)
+ var params = JSON.parse(request.requestBody);
+ var cluster = schema.clusters.first();
+
+ if (!params.id) {
+ return new Mirage.Response(400, {}, { errors: ['id must be specified'] });
+ } else {
+ var newSecondaries = (cluster.attrs.known_secondaries || []).slice();
+ newSecondaries.push(params.id);
+ cluster.update('known_secondaries', newSecondaries);
+ return new Mirage.Response(200, {}, { token: faker.random.uuid() });
+ }
+ });
+
+ this.post('/sys/replication/primary/revoke-secondary', (schema, request) => {
+ var params = JSON.parse(request.requestBody);
+ var cluster = schema.clusters.first();
+
+ if (!params.id) {
+ return new Mirage.Response(400, {}, { errors: ['id must be specified'] });
+ } else {
+ var newSecondaries = cluster.attrs.known_secondaries.without(params.id);
+ cluster.update('known_secondaries', newSecondaries);
+ return new Mirage.Response(204);
+ }
+ });
+
+ this.post('/sys/replication/secondary/enable', (schema, request) => {
+ //token=(req)
+ var params = JSON.parse(request.requestBody);
+ var cluster = schema.clusters.first();
+
+ if (!params.token) {
+ return new Mirage.Response(400, {}, { errors: ['token must be specified'] });
+ } else {
+ cluster.update('mode', 'secondary');
+ return new Mirage.Response(204);
+ }
+ });
+
+ this.post('/sys/replication/secondary/promote', schema => {
+ var cluster = schema.clusters.first();
+ cluster.update('mode', 'primary');
+ return new Mirage.Response(204);
+ });
+
+ //primary_cluster_addr=(opt)
+ this.post('/sys/replication/secondary/disable', schema => {
+ var cluster = schema.clusters.first();
+ cluster.update('mode', 'disabled');
+ return new Mirage.Response(204);
+ });
+
+ this.post('/sys/replication/secondary/update-primary', (schema, request) => {
+ //token=(req)
+ var params = JSON.parse(request.requestBody);
+
+ if (!params.token) {
+ return new Mirage.Response(400, {}, { errors: ['token must be specified'] });
+ } else {
+ return new Mirage.Response(204);
+ }
+ });
+
+ this.post('/sys/replication/recover', () => {
+ return new Mirage.Response(204);
+ });
+ this.post('/sys/replication/reindex', () => {
+ return new Mirage.Response(204);
+ });
+ //(sudo)
+ this.get('/sys/replication/status', schema => {
+ let model = schema.clusters.first();
+ return new Mirage.Response(200, {}, model);
+ }); //(unauthenticated)
+
+ // enable and auth method
+ this.post('/sys/auth/:path', (schema, request) => {
+ const { path } = JSON.parse(request.requestBody);
+ schema.authMethods.create({
+ path,
+ });
+ return new Mirage.Response(204);
+ });
+
+ // TODO making this the default is probably not desired, but there's not an
+ // easy way to do overrides currently - should this maybe just live in the
+ // relevant test with pretender stubs?
+ this.get('/sys/mounts', () => {
+ return new Mirage.Response(403, {});
+ });
+
+ this.passthrough();
+}
diff --git a/ui/mirage/fixtures/clusters.js b/ui/mirage/fixtures/clusters.js
new file mode 100644
index 000000000..f18c21ce8
--- /dev/null
+++ b/ui/mirage/fixtures/clusters.js
@@ -0,0 +1 @@
+export default [{ id: '1', name: 'vault' }];
diff --git a/ui/mirage/scenarios/default.js b/ui/mirage/scenarios/default.js
new file mode 100644
index 000000000..2d53a6d6a
--- /dev/null
+++ b/ui/mirage/scenarios/default.js
@@ -0,0 +1,14 @@
+export default function(server) {
+ /*
+ Seed your development database using your factories.
+ This data will not be loaded in your tests.
+
+ Make sure to define a factory for each model you want to create.
+ */
+
+ server.schema.clusters.create({
+ name: 'vault',
+ id: '1',
+ mode: 'disabled',
+ });
+}
diff --git a/ui/mirage/serializers/application.js b/ui/mirage/serializers/application.js
new file mode 100644
index 000000000..589c22325
--- /dev/null
+++ b/ui/mirage/serializers/application.js
@@ -0,0 +1,6 @@
+import { RestSerializer } from 'ember-cli-mirage';
+
+export default RestSerializer.extend({
+ embed: true,
+ root: false,
+});
diff --git a/ui/package.json b/ui/package.json
new file mode 100644
index 000000000..ce73996c2
--- /dev/null
+++ b/ui/package.json
@@ -0,0 +1,98 @@
+{
+ "name": "vault",
+ "version": "0.0.0",
+ "description": "The official UI for Vault by HashiCorp",
+ "directories": {
+ "doc": "doc",
+ "test": "tests"
+ },
+ "author": "",
+ "repository": "",
+ "scripts": {
+ "build": "ember build -prod",
+ "start": "export VAULT_ADDR=http://localhost:8200; ember server --proxy=$VAULT_ADDR",
+ "start2": "ember server --proxy=http://localhost:8202 --port=4202",
+ "test": "node scripts/start-vault.js & ember test",
+ "test-oss": "yarn run test -f='!enterprise'",
+ "fmt-js": "prettier-eslint --single-quote --trailing-comma es5 --print-width=110 --write {app,tests,config,lib,mirage}/**/*.js",
+ "fmt-styles": "prettier --write app/styles/**/*.*",
+ "fmt": "yarn run fmt-js && yarn run fmt-styles",
+ "precommit": "lint-staged"
+ },
+ "lint-staged": {
+ "gitDir": "../",
+ "linters": {
+ "ui/{app,tests,config,lib,mirage}/**/*.js": [
+ "prettier-eslint --single-quote --trailing-comma es5 --print-width 110 --write",
+ "git add"
+ ],
+ "ui/app/styles/**/*.*": [
+ "prettier --write",
+ "git add"
+ ]
+ }
+ },
+ "devDependencies": {
+ "babel-plugin-transform-object-rest-spread": "^6.23.0",
+ "broccoli-asset-rev": "^2.4.5",
+ "broccoli-sri-hash": "meirish/broccoli-sri-hash#rooturl",
+ "bulma": "^0.5.2",
+ "bulma-switch": "^0.0.1",
+ "cool-checkboxes-for-bulma.io": "^1.1.0",
+ "ember-ajax": "^3.0.0",
+ "ember-api-actions": "^0.1.8",
+ "ember-basic-dropdown": "^0.33.5",
+ "ember-basic-dropdown-hover": "^0.2.0",
+ "ember-cli": "~2.14.0",
+ "ember-cli-babel": "^6.3.0",
+ "ember-cli-clipboard": "^0.8.0",
+ "ember-cli-dependency-checker": "^1.3.0",
+ "ember-cli-eslint": "4",
+ "ember-cli-favicon": "1.0.0-beta.4",
+ "ember-cli-flash": "^1.5.0",
+ "ember-cli-htmlbars": "^2.0.1",
+ "ember-cli-htmlbars-inline-precompile": "^0.4.3",
+ "ember-cli-inject-live-reload": "^1.4.1",
+ "ember-cli-mirage": "^0.4.1",
+ "ember-cli-moment-shim": "2.2.1",
+ "ember-cli-page-object": "^1.13.0",
+ "ember-cli-pretender": "0.7.0",
+ "ember-cli-qunit": "^4.0.0",
+ "ember-cli-sass": "6.0.0",
+ "ember-cli-shims": "^1.1.0",
+ "ember-cli-sri": "meirish/ember-cli-sri#rooturl",
+ "ember-cli-string-helpers": "^1.5.0",
+ "ember-cli-uglify": "^1.2.0",
+ "ember-composable-helpers": "^2.0.3",
+ "ember-computed-query": "^0.1.1",
+ "ember-concurrency": "^0.8.14",
+ "ember-data": "2.12.1",
+ "ember-data-model-fragments": "2.11.x",
+ "ember-export-application-global": "^2.0.0",
+ "ember-fetch": "^3.4.3",
+ "ember-href-to": "^1.13.0",
+ "ember-load-initializers": "^1.0.0",
+ "ember-moment": "7.0.0-beta.5",
+ "ember-qunit-assert-helpers": "^0.1.3",
+ "ember-radio-button": "^1.1.1",
+ "ember-resolver": "^4.0.0",
+ "ember-sinon": "^1.0.1",
+ "ember-source": "~2.14.0",
+ "ember-test-selectors": "^0.3.6",
+ "ember-truth-helpers": "1.2.0",
+ "ivy-codemirror": "2.0.3",
+ "loader.js": "^4.2.3",
+ "normalize.css": "4.1.1",
+ "prettier": "^1.5.3",
+ "prettier-eslint-cli": "^4.2.1"
+ },
+ "engines": {
+ "node": "^4.5 || 6.* || >= 7.*"
+ },
+ "private": true,
+ "ember-addon": {
+ "paths": [
+ "lib/bulma"
+ ]
+ }
+}
diff --git a/ui/public/crossdomain.xml b/ui/public/crossdomain.xml
new file mode 100644
index 000000000..0c16a7a07
--- /dev/null
+++ b/ui/public/crossdomain.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ui/public/favicon.png b/ui/public/favicon.png
new file mode 100644
index 000000000..85ed28902
--- /dev/null
+++ b/ui/public/favicon.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a673e9ec1ac4d0fe448c0049aca2643c804b11b6ba6533cc6f5f4c56e12d8dbd
+size 16843
diff --git a/ui/public/robots.txt b/ui/public/robots.txt
new file mode 100644
index 000000000..207b72685
--- /dev/null
+++ b/ui/public/robots.txt
@@ -0,0 +1,3 @@
+# http://www.robotstxt.org
+User-agent: *
+Disallow: *
diff --git a/ui/public/vault-hex.svg b/ui/public/vault-hex.svg
new file mode 100644
index 000000000..76a6d342d
--- /dev/null
+++ b/ui/public/vault-hex.svg
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/ui/scripts/start-vault.js b/ui/scripts/start-vault.js
new file mode 100755
index 000000000..180f0dcea
--- /dev/null
+++ b/ui/scripts/start-vault.js
@@ -0,0 +1,76 @@
+#!/usr/bin/env node
+
+if(process.argv[2]){
+ process.kill(process.argv[2], 'SIGINT');
+ process.exit(0);
+}
+
+var fs = require('fs');
+var path = require('path');
+var spawn = require('child_process').spawn;
+var vault = spawn(
+ 'vault',
+ [
+ 'server',
+ '-dev',
+ '-dev-ha',
+ '-dev-transactional',
+ '-dev-leased-kv',
+ '-dev-root-token-id=root',
+ '-dev-listen-address=127.0.0.1:9200'
+ ]
+);
+
+// https://github.com/chalk/ansi-regex/blob/master/index.js
+var ansiPattern = [
+ '[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[a-zA-Z\\d]*)*)?\\u0007)',
+ '(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PRZcf-ntqry=><~]))'
+].join('|');
+
+var ANSI_REGEX = new RegExp(ansiPattern, 'g');
+
+var output = '';
+var unseal, root;
+vault.stdout.on('data', function(data) {
+ var stringData = data.toString().replace(ANSI_REGEX, '');
+ output = output + stringData;
+ console.log(stringData);
+
+ var unsealMatch = output.match(/Unseal Key\: (.+)$/m);
+ if (unsealMatch && !unseal) { unseal = unsealMatch[1] };
+ var rootMatch = output.match(/Root Token\: (.+)$/m);
+ if (rootMatch && !root) { root = rootMatch[1] };
+ if (root && unseal) {
+ fs.writeFile(
+ path.join(process.cwd(), 'tests/helpers/vault-keys.js'),
+ `export default ${JSON.stringify({ unseal:unseal, root:root }, null, 2)}`
+ );
+
+ console.log('VAULT SERVER READY');
+ }
+});
+
+vault.stderr.on('data', function(data) {
+ console.log(data.toString());
+});
+
+vault.on('close', function(code) {
+ console.log(`child process exited with code ${code}`);
+ process.exit();
+});
+vault.on('error', function(error) {
+ console.log(`child process errored: ${error}`);
+ process.exit();
+});
+
+
+var pidFile = 'vault-ui-integration-server.pid';
+process.on('SIGINT', function() {
+ vault.kill('SIGINT');
+ process.exit();
+});
+process.on('exit', function() {
+ vault.kill('SIGINT');
+});
+
+fs.writeFile(pidFile, process.pid);
diff --git a/ui/testem.js b/ui/testem.js
new file mode 100644
index 000000000..df357bd74
--- /dev/null
+++ b/ui/testem.js
@@ -0,0 +1,19 @@
+/*jshint node:true*/
+module.exports = {
+ "framework": "qunit",
+ "test_page": "tests/index.html?hidepassed",
+ "disable_watching": true,
+ "launch_in_ci": [
+ "Chrome"
+ ],
+ "launch_in_dev": [
+ "Chrome"
+ ],
+ "on_exit": "[ -e ../../vault-ui-integration-server.pid ] && node ../../scripts/start-vault.js `cat ../../vault-ui-integration-server.pid`; [ -e ../../vault-ui-integration-server.pid ] && rm ../../vault-ui-integration-server.pid",
+
+ proxies: {
+ "/v1": {
+ "target": "http://localhost:9200"
+ }
+ }
+};
diff --git a/ui/tests/.eslintrc.js b/ui/tests/.eslintrc.js
new file mode 100644
index 000000000..88d281d2b
--- /dev/null
+++ b/ui/tests/.eslintrc.js
@@ -0,0 +1,15 @@
+module.exports = {
+ env: {
+ embertest: true,
+ },
+ globals: {
+ faker: true,
+ server: true,
+ $: true,
+ authLogout: false,
+ authLogin: false,
+ pollCluster: false,
+ mountSupportedSecretBackend: false,
+ wait: true,
+ },
+};
diff --git a/ui/tests/acceptance/access/identity/index-test.js b/ui/tests/acceptance/access/identity/index-test.js
new file mode 100644
index 000000000..ed8659a65
--- /dev/null
+++ b/ui/tests/acceptance/access/identity/index-test.js
@@ -0,0 +1,16 @@
+import { test } from 'qunit';
+import moduleForAcceptance from 'vault/tests/helpers/module-for-acceptance';
+import page from 'vault/tests/pages/access/identity/index';
+
+moduleForAcceptance('Acceptance | /access/identity/entities', {
+ beforeEach() {
+ return authLogin();
+ },
+});
+
+test('it renders the page', function(assert) {
+ page.visit({ item_type: 'entities' });
+ andThen(() => {
+ assert.ok(currentRouteName(), 'vault.cluster.access.identity.index', 'navigates to the correct route');
+ });
+});
diff --git a/ui/tests/acceptance/access/methods-test.js b/ui/tests/acceptance/access/methods-test.js
new file mode 100644
index 000000000..d35c83211
--- /dev/null
+++ b/ui/tests/acceptance/access/methods-test.js
@@ -0,0 +1,18 @@
+import { test } from 'qunit';
+import moduleForAcceptance from 'vault/tests/helpers/module-for-acceptance';
+import page from 'vault/tests/pages/access/methods';
+
+moduleForAcceptance('Acceptance | /access/', {
+ beforeEach() {
+ return authLogin();
+ },
+});
+
+test('it navigates', function(assert) {
+ page.visit();
+ andThen(() => {
+ assert.ok(currentRouteName(), 'vault.cluster.access.methods', 'navigates to the correct route');
+ assert.ok(page.navLinks(0).isActive, 'the first link is active');
+ assert.equal(page.navLinks(0).text, 'Auth Methods');
+ });
+});
diff --git a/ui/tests/acceptance/auth-test.js b/ui/tests/acceptance/auth-test.js
new file mode 100644
index 000000000..361d70e77
--- /dev/null
+++ b/ui/tests/acceptance/auth-test.js
@@ -0,0 +1,27 @@
+import { test } from 'qunit';
+import moduleForAcceptance from 'vault/tests/helpers/module-for-acceptance';
+import { supportedAuthBackends } from 'vault/helpers/supported-auth-backends';
+
+moduleForAcceptance('Acceptance | auth', {
+ afterEach() {
+ return authLogout();
+ },
+});
+
+test('auth query params', function(assert) {
+ const backends = supportedAuthBackends();
+ visit('/vault/auth');
+ andThen(function() {
+ assert.equal(currentURL(), '/vault/auth');
+ });
+ backends.reverse().forEach(backend => {
+ click(`[data-test-auth-method-link="${backend.type}"]`);
+ andThen(function() {
+ assert.equal(
+ currentURL(),
+ `/vault/auth?with=${backend.type}`,
+ `has the correct URL for ${backend.type}`
+ );
+ });
+ });
+});
diff --git a/ui/tests/acceptance/aws-test.js b/ui/tests/acceptance/aws-test.js
new file mode 100644
index 000000000..78bce4573
--- /dev/null
+++ b/ui/tests/acceptance/aws-test.js
@@ -0,0 +1,106 @@
+import { test } from 'qunit';
+import moduleForAcceptance from 'vault/tests/helpers/module-for-acceptance';
+
+moduleForAcceptance('Acceptance | aws secret backend', {
+ beforeEach() {
+ return authLogin();
+ },
+ afterEach() {
+ return authLogout();
+ },
+});
+
+const POLICY = {
+ Version: '2012-10-17',
+ Statement: [
+ {
+ Effect: 'Allow',
+ Action: 'iam:*',
+ Resource: '*',
+ },
+ ],
+};
+test('aws backend', function(assert) {
+ const now = new Date().getTime();
+ const path = `aws-${now}`;
+ const roleName = 'awsrole';
+
+ mountSupportedSecretBackend(assert, 'aws', path);
+ click('[data-test-secret-backend-configure]');
+ andThen(() => {
+ assert.equal(currentURL(), `/vault/settings/secrets/configure/${path}`);
+ assert.ok(find('[data-test-aws-root-creds-form]').length, 'renders the empty root creds form');
+ assert.ok(find('[data-test-aws-link="root-creds"]').length, 'renders the root creds link');
+ assert.ok(find('[data-test-aws-link="leases"]').length, 'renders the leases config link');
+ });
+
+ fillIn('[data-test-aws-input="accessKey"]', 'foo');
+ fillIn('[data-test-aws-input="secretKey"]', 'bar');
+ click('[data-test-aws-input="root-save"]');
+ andThen(() => {
+ assert.ok(
+ find('[data-test-flash-message]').text().trim(),
+ `The backend configuration saved successfully!`
+ );
+ click('[data-test-flash-message]');
+ });
+ click('[data-test-aws-link="leases"]');
+ click('[data-test-aws-input="lease-save"]');
+
+ andThen(() => {
+ assert.ok(
+ find('[data-test-flash-message]').text().trim(),
+ `The backend configuration saved successfully!`
+ );
+ click('[data-test-flash-message]');
+ });
+
+ click('[data-test-backend-view-link]');
+ //back at the roles list
+ andThen(() => {
+ assert.equal(currentURL(), `/vault/secrets/${path}/list`, `navigates to the roles list`);
+ });
+
+ click('[ data-test-secret-create]');
+ andThen(() => {
+ assert.ok(find('[data-test-secret-header]').text().includes('AWS Role'), `aws: renders the create page`);
+ });
+
+ fillIn('[data-test-input="name"]', roleName);
+ andThen(function() {
+ find('.CodeMirror').get(0).CodeMirror.setValue(JSON.stringify(POLICY));
+ });
+
+ // save the role
+ click('[data-test-role-aws-create]');
+ andThen(() => {
+ assert.equal(
+ currentURL(),
+ `/vault/secrets/${path}/show/${roleName}`,
+ `$aws: navigates to the show page on creation`
+ );
+ });
+
+ click('[data-test-secret-root-link]');
+
+ //back at the roles list
+ andThen(() => {
+ assert.equal(currentURL(), `/vault/secrets/${path}/list`);
+ assert.ok(find(`[data-test-secret-link="${roleName}"]`).length, `aws: role shows in the list`);
+ });
+
+ //and delete
+ click(`[data-test-secret-link="${roleName}"] [data-test-popup-menu-trigger]`);
+ andThen(() => {
+ click(`[data-test-aws-role-delete="${roleName}"] button`);
+ });
+ click(`[data-test-confirm-button]`);
+
+ andThen(() => {
+ assert.equal(
+ find(`[data-test-secret-link="${roleName}"]`).length,
+ 0,
+ `aws: role is no longer in the list`
+ );
+ });
+});
diff --git a/ui/tests/acceptance/enterprise-replication-test.js b/ui/tests/acceptance/enterprise-replication-test.js
new file mode 100644
index 000000000..ad7857817
--- /dev/null
+++ b/ui/tests/acceptance/enterprise-replication-test.js
@@ -0,0 +1,196 @@
+import { test } from 'qunit';
+import moduleForAcceptance from 'vault/tests/helpers/module-for-acceptance';
+
+const disableReplication = (type, assert) => {
+ // disable performance replication
+ visit(`/vault/replication/${type}`);
+ return andThen(() => {
+ if (find('[data-test-replication-link="manage"]').length) {
+ click('[data-test-replication-link="manage"]');
+ click('[data-test-disable-replication] button');
+ click('[data-test-confirm-button]');
+ if (assert) {
+ andThen(() => {
+ assert.equal(currentURL(), `/vault/replication/${type}`, 'redirects to the replication page');
+ assert.equal(
+ // TODO better test selectors for flash messages
+ find('[data-test-flash-message-body]:contains(This cluster is having)').text().trim(),
+ 'This cluster is having replication disabled. Vault will be unavailable for a brief period and will resume service shortly.',
+ 'renders info flash when disabled'
+ );
+ click('[data-test-flash-message-body]:contains(This cluster is having)');
+ });
+ }
+ } else {
+ // do nothing, it's already off
+ }
+ });
+};
+moduleForAcceptance('Acceptance | Enterprise | replication', {
+ beforeEach() {
+ authLogin();
+ disableReplication('dr');
+ return disableReplication('performance');
+ },
+ afterEach() {
+ disableReplication('dr');
+ return disableReplication('performance');
+ },
+});
+
+test('replication', function(assert) {
+ const secondaryName = 'firstSecondary';
+ const mode = 'blacklist';
+ const mountType = 'kv';
+ let mountPath;
+
+ visit('/vault/replication');
+ andThen(function() {
+ assert.equal(currentURL(), '/vault/replication');
+ });
+
+ // enable perf replication
+ click('[data-test-replication-type-select="performance"]');
+ fillIn('[data-test-replication-cluster-mode-select]', 'primary');
+
+ click('[data-test-replication-enable]');
+ andThen(() => {
+ pollCluster();
+ });
+
+ // add a secondary with a mount filter config
+ click('[data-test-replication-link="secondaries"]');
+ click('[data-test-secondary-add]');
+ fillIn('[data-test-replication-secondary-id]', secondaryName);
+ //expand the config
+ click('[data-test-replication-secondary-token-options]');
+ fillIn('[data-test-replication-filter-mount-mode]', mode);
+ click(`[data-test-mount-filter="${mountType}"]:eq(0)`);
+ andThen(() => {
+ mountPath = find(`[data-test-mount-filter-path-for-type="${mountType}"]`).first().text().trim();
+ });
+ click('[data-test-secondary-add]');
+
+ // fetch new secondaries
+ andThen(() => {
+ pollCluster();
+ });
+
+ // click into the added secondary's mount filter config
+ click('[data-test-replication-link="secondaries"]');
+ click('[data-test-popup-menu-trigger]');
+
+ click('[data-test-replication-mount-filter-link]');
+ andThen(() => {
+ assert.equal(currentURL(), `/vault/replication/performance/secondaries/config/show/${secondaryName}`);
+ assert.ok(
+ find('[data-test-mount-config-mode]').text().trim().toLowerCase().includes(mode),
+ 'show page renders the correct mode'
+ );
+ assert.equal(
+ find('[data-test-mount-config-paths]').text().trim(),
+ mountPath,
+ 'show page renders the correct mount path'
+ );
+ });
+ // click edit
+
+ // delete config
+ click('[data-test-replication-link="edit-mount-config"]');
+ click('[data-test-delete-mount-config] button');
+ click('[data-test-confirm-button]');
+ andThen(() => {
+ assert.equal(
+ currentURL(),
+ `/vault/replication/performance/secondaries`,
+ 'redirects to the secondaries page'
+ );
+ assert.equal(
+ //TODO re-work error message test selectors
+ find('[data-test-flash-message-body]:contains(The performance mount filter)').text().trim(),
+ `The performance mount filter config for the secondary ${secondaryName} was successfully deleted.`,
+ 'renders success flash upon deletion'
+ );
+ click('[data-test-flash-message-body]:contains(The performance mount filter)');
+ });
+
+ // nav to DR
+ visit('/vault/replication/dr');
+ fillIn('[data-test-replication-cluster-mode-select]', 'secondary');
+ andThen(() => {
+ assert.ok(
+ find('[data-test-replication-enable]').is(':disabled'),
+ 'dr secondary enable is disabled when other replication modes are on'
+ );
+ });
+
+ // disable performance replication
+ disableReplication('replication', assert);
+
+ // enable dr replication
+ visit('/vault/replication/dr');
+ fillIn('[data-test-replication-cluster-mode-select]', 'primary');
+ click('button[type="submit"]');
+ andThen(() => {
+ pollCluster();
+ });
+ andThen(() => {
+ assert.ok(
+ find('[data-test-replication-title]').text().includes('Disaster Recovery'),
+ 'it displays the replication type correctly'
+ );
+ assert.ok(
+ find('[data-test-replication-mode-display]').text().includes('primary'),
+ 'it displays the cluster mode correctly'
+ );
+ });
+
+ // add dr secondary
+ click('[data-test-replication-link="secondaries"]');
+ click('[data-test-secondary-add]');
+ fillIn('[data-test-replication-secondary-id]', secondaryName);
+ click('[data-test-secondary-add]');
+ andThen(() => {
+ pollCluster();
+ });
+ click('[data-test-replication-link="secondaries"]');
+ andThen(() => {
+ assert.equal(
+ find('[data-test-secondary-name]').text().trim(),
+ secondaryName,
+ 'it displays the secondary in the list of known secondaries'
+ );
+ });
+
+ // disable dr replication
+ disableReplication('dr', assert);
+ return wait();
+});
+
+test('disabling dr primary when perf replication is enabled', function(assert) {
+ visit('/vault/replication/performance');
+ // enable perf replication
+ fillIn('[data-test-replication-cluster-mode-select]', 'primary');
+
+ click('[data-test-replication-enable]');
+ andThen(() => {
+ pollCluster();
+ });
+
+ // enable dr replication
+ visit('/vault/replication/dr');
+ fillIn('[data-test-replication-cluster-mode-select]', 'primary');
+ click('[data-test-replication-enable]');
+ andThen(() => {
+ pollCluster();
+ });
+ visit('/vault/replication/dr/manage');
+ andThen(() => {
+ assert.ok(find('[data-test-demote-warning]').length, 'displays the demotion warning');
+ });
+
+ // disable replication
+ disableReplication('performance', assert);
+ disableReplication('dr', assert);
+ return wait();
+});
diff --git a/ui/tests/acceptance/leases-test.js b/ui/tests/acceptance/leases-test.js
new file mode 100644
index 000000000..180032fab
--- /dev/null
+++ b/ui/tests/acceptance/leases-test.js
@@ -0,0 +1,134 @@
+import { test } from 'qunit';
+import moduleForAcceptance from 'vault/tests/helpers/module-for-acceptance';
+import Ember from 'ember';
+
+let adapterException;
+// testing error states is terrible in ember acceptance tests so these weird Ember bits are to work around that
+// adapted from https://github.com/emberjs/ember.js/issues/12791#issuecomment-244934786
+moduleForAcceptance('Acceptance | leases', {
+ beforeEach() {
+ adapterException = Ember.Test.adapter.exception;
+ Ember.Test.adapter.exception = () => null;
+ return authLogin();
+ },
+ afterEach() {
+ Ember.Test.adapter.exception = adapterException;
+ return authLogout();
+ },
+});
+
+const createSecret = (context, isRenewable) => {
+ const now = new Date().getTime();
+ const secretContents = { secret: 'foo' };
+ if (isRenewable) {
+ secretContents.ttl = '30h';
+ }
+ context.secret = {
+ name: isRenewable ? `renew-secret-${now}` : `secret-${now}`,
+ text: JSON.stringify(secretContents),
+ };
+ //create a secret so we have a lease (server is running in -dev-leased-kv mode)
+ visit('/vault/secrets/secret/list');
+ click('[data-test-secret-create]');
+ fillIn('[data-test-secret-path]', context.secret.name);
+ andThen(() => {
+ const codeMirror = find('.CodeMirror');
+ // UI keeps state so once we flip to json, we don't need to again
+ if (!codeMirror.length) {
+ click('[data-test-secret-json-toggle]');
+ }
+ });
+ andThen(() => {
+ find('.CodeMirror').get(0).CodeMirror.setValue(context.secret.text);
+ });
+ click('[data-test-secret-save]');
+};
+
+const navToDetail = secret => {
+ visit('/vault/access/leases/');
+ click('[data-test-lease-link="secret/"]');
+ click(`[data-test-lease-link="secret/${secret.name}/"]`);
+ click(`[data-test-lease-link]:eq(0)`);
+};
+
+test('it renders the show page', function(assert) {
+ createSecret(this);
+ navToDetail(this.secret);
+ andThen(() => {
+ assert.equal(
+ currentRouteName(),
+ 'vault.cluster.access.leases.show',
+ 'a lease for the secret is in the list'
+ );
+ assert.equal(
+ find('[data-test-lease-renew-picker]').length,
+ 0,
+ 'non-renewable lease does not render a renew picker'
+ );
+ });
+});
+
+test('it renders the show page with a picker', function(assert) {
+ createSecret(this, true);
+ navToDetail(this.secret);
+ andThen(() => {
+ assert.equal(
+ currentRouteName(),
+ 'vault.cluster.access.leases.show',
+ 'a lease for the secret is in the list'
+ );
+ assert.equal(find('[data-test-lease-renew-picker]').length, 1, 'renewable lease renders a renew picker');
+ });
+});
+
+test('it removes leases upon revocation', function(assert) {
+ createSecret(this);
+ navToDetail(this.secret);
+ click('[data-test-lease-revoke] button');
+ click('[data-test-confirm-button]');
+ andThen(() => {
+ assert.equal(
+ currentRouteName(),
+ 'vault.cluster.access.leases.list-root',
+ 'it navigates back to the leases root on revocation'
+ );
+ });
+ click('[data-test-lease-link="secret/"]');
+ andThen(() => {
+ assert.equal(
+ find(`[data-test-lease-link="secret/${this.secret.name}/"]`).length,
+ 0,
+ 'link to the lease was removed with revocation'
+ );
+ });
+});
+
+test('it removes branches when a prefix is revoked', function(assert) {
+ createSecret(this);
+ visit('/vault/access/leases/list/secret/');
+ click('[data-test-lease-revoke-prefix] button');
+ click('[data-test-confirm-button]');
+ andThen(() => {
+ assert.equal(
+ currentRouteName(),
+ 'vault.cluster.access.leases.list-root',
+ 'it navigates back to the leases root on revocation'
+ );
+ assert.equal(
+ find('[data-test-lease-link="secret/"]').length,
+ 0,
+ 'link to the prefix was removed with revocation'
+ );
+ });
+});
+
+test('lease not found', function(assert) {
+ visit('/vault/access/leases/show/not-found');
+ andThen(() => {
+ assert.equal(
+ find('[data-test-lease-error]').text().trim(),
+ 'not-found is not a valid lease ID',
+ 'it shows an error when the lease is not found'
+ );
+ });
+});
diff --git a/ui/tests/acceptance/not-found-test.js b/ui/tests/acceptance/not-found-test.js
new file mode 100644
index 000000000..05c7327ca
--- /dev/null
+++ b/ui/tests/acceptance/not-found-test.js
@@ -0,0 +1,52 @@
+import { test } from 'qunit';
+import moduleForAcceptance from 'vault/tests/helpers/module-for-acceptance';
+import Ember from 'ember';
+
+let adapterException;
+let loggerError;
+
+moduleForAcceptance('Acceptance | not-found', {
+ beforeEach() {
+ loggerError = Ember.Logger.error;
+ adapterException = Ember.Test.adapter.exception;
+ Ember.Test.adapter.exception = () => {};
+ Ember.Logger.error = () => {};
+ return authLogin();
+ },
+ afterEach() {
+ Ember.Test.adapter.exception = adapterException;
+ Ember.Logger.error = loggerError;
+ return authLogout();
+ },
+});
+
+test('top-level not-found', function(assert) {
+ visit('/404');
+ andThen(() => {
+ assert.ok(find('[data-test-not-found]').length, 'renders the not found component');
+ assert.ok(find('[data-test-header-without-nav]').length, 'renders the not found component with a header');
+ });
+});
+
+test('vault route not-found', function(assert) {
+ visit('/vault/404');
+ andThen(() => {
+ assert.ok(find('[data-test-not-found]'), 'renders the not found component');
+ assert.ok(find('[data-test-header-with-nav]').length, 'renders header with nav');
+ });
+});
+
+test('cluster route not-found', function(assert) {
+ visit('/vault/secrets/secret/404/show');
+ andThen(() => {
+ assert.ok(find('[data-test-not-found]'), 'renders the not found component');
+ assert.ok(find('[data-test-header-with-nav]').length, 'renders header with nav');
+ });
+});
+
+test('secret not-found', function(assert) {
+ visit('/vault/secrets/secret/show/404');
+ andThen(() => {
+ assert.ok(find('[data-test-secret-not-found]'), 'renders the message about the secret not being found');
+ });
+});
diff --git a/ui/tests/acceptance/policies-acl-old-test.js b/ui/tests/acceptance/policies-acl-old-test.js
new file mode 100644
index 000000000..659b916cd
--- /dev/null
+++ b/ui/tests/acceptance/policies-acl-old-test.js
@@ -0,0 +1,73 @@
+import Ember from 'ember';
+import { test } from 'qunit';
+import moduleForAcceptance from 'vault/tests/helpers/module-for-acceptance';
+import page from 'vault/tests/pages/policies/index';
+
+let adapterException;
+let loggerError;
+moduleForAcceptance('Acceptance | policies (old)', {
+ beforeEach() {
+ loggerError = Ember.Logger.error;
+ adapterException = Ember.Test.adapter.exception;
+ Ember.Test.adapter.exception = () => {};
+ Ember.Logger.error = () => {};
+ return authLogin();
+ },
+ afterEach() {
+ Ember.Test.adapter.exception = adapterException;
+ Ember.Logger.error = loggerError;
+ },
+});
+
+test('policies', function(assert) {
+ const now = new Date().getTime();
+ const policyString = 'path "*" { capabilities = ["update"]}';
+ const policyName = `Policy test ${now}`;
+ const policyLower = policyName.toLowerCase();
+
+ page.visit({ type: 'acl' });
+ // new policy creation
+ click('[data-test-policy-create-link]');
+ fillIn('[data-test-policy-input="name"]', policyName);
+ click('[data-test-policy-save]');
+ andThen(function() {
+ assert.equal(find('[data-test-error]').length, 1, 'renders error messages on save');
+ find('.CodeMirror').get(0).CodeMirror.setValue(policyString);
+ });
+ click('[data-test-policy-save]');
+ andThen(function() {
+ assert.equal(
+ currentURL(),
+ `/vault/policy/acl/${encodeURIComponent(policyLower)}`,
+ 'navigates to policy show on successful save'
+ );
+ assert.equal(
+ find('[data-test-policy-name]').text().trim(),
+ policyLower,
+ 'displays the policy name on the show page'
+ );
+ assert.equal(
+ find('[data-test-flash-message].is-info').length,
+ 0,
+ 'no flash message is displayed on save'
+ );
+ });
+ click('[data-test-policy-list-link]');
+ andThen(function() {
+ assert.equal(find(`[data-test-policy-link="${policyLower}"]`).length, 1, 'new policy shown in the list');
+ });
+
+ // policy deletion
+ click(`[data-test-policy-link="${policyLower}"]`);
+ click('[data-test-policy-edit-toggle]');
+ click('[data-test-policy-delete] button');
+ click('[data-test-confirm-button]');
+ andThen(function() {
+ assert.equal(currentURL(), `/vault/policies/acl`, 'navigates to policy list on successful deletion');
+ assert.equal(
+ find(`[data-test-policy-item="${policyLower}"]`).length,
+ 0,
+ 'deleted policy is not shown in the list'
+ );
+ });
+});
diff --git a/ui/tests/acceptance/policies-test.js b/ui/tests/acceptance/policies-test.js
new file mode 100644
index 000000000..01e8c2c6c
--- /dev/null
+++ b/ui/tests/acceptance/policies-test.js
@@ -0,0 +1,30 @@
+import { test } from 'qunit';
+import moduleForAcceptance from 'vault/tests/helpers/module-for-acceptance';
+
+moduleForAcceptance('Acceptance | policies', {
+ beforeEach() {
+ return authLogin();
+ },
+});
+
+test('it redirects to acls on unknown policy type', function(assert) {
+ visit('/vault/policy/foo/default');
+ andThen(() => {
+ assert.equal(currentRouteName(), 'vault.cluster.policies.index');
+ assert.equal(currentURL(), '/vault/policies/acl');
+ });
+
+ visit('/vault/policy/foo/default/edit');
+ andThen(() => {
+ assert.equal(currentRouteName(), 'vault.cluster.policies.index');
+ assert.equal(currentURL(), '/vault/policies/acl');
+ });
+});
+
+test('it redirects to acls on index navigation', function(assert) {
+ visit('/vault/policy/acl');
+ andThen(() => {
+ assert.equal(currentRouteName(), 'vault.cluster.policies.index');
+ assert.equal(currentURL(), '/vault/policies/acl');
+ });
+});
diff --git a/ui/tests/acceptance/policies/index-test.js b/ui/tests/acceptance/policies/index-test.js
new file mode 100644
index 000000000..e2109eef5
--- /dev/null
+++ b/ui/tests/acceptance/policies/index-test.js
@@ -0,0 +1,31 @@
+import { test } from 'qunit';
+import moduleForAcceptance from 'vault/tests/helpers/module-for-acceptance';
+import page from 'vault/tests/pages/policies/index';
+
+moduleForAcceptance('Acceptance | policies/acl', {
+ beforeEach() {
+ return authLogin();
+ },
+});
+
+test('it lists default and root acls', function(assert) {
+ page.visit({ type: 'acl' });
+ andThen(() => {
+ let policies = page.policies();
+ assert.equal(currentURL(), '/vault/policies/acl');
+ assert.ok(policies.findByName('root'), 'root policy shown in the list');
+ assert.ok(policies.findByName('default'), 'default policy shown in the list');
+ });
+});
+
+test('it navigates to show when clicking on the link', function(assert) {
+ page.visit({ type: 'acl' });
+ andThen(() => {
+ page.policies().findByName('default').click();
+ });
+
+ andThen(() => {
+ assert.equal(currentRouteName(), 'vault.cluster.policy.show');
+ assert.equal(currentURL(), '/vault/policy/acl/default');
+ });
+});
diff --git a/ui/tests/acceptance/policy-test.js b/ui/tests/acceptance/policy-test.js
new file mode 100644
index 000000000..a2cac7347
--- /dev/null
+++ b/ui/tests/acceptance/policy-test.js
@@ -0,0 +1,19 @@
+import { test } from 'qunit';
+import moduleForAcceptance from 'vault/tests/helpers/module-for-acceptance';
+
+moduleForAcceptance('Acceptance | policies', {
+ beforeEach() {
+ return authLogin();
+ },
+ afterEach() {
+ return authLogout();
+ },
+});
+
+test('it redirects to acls with unknown policy type', function(assert) {
+ visit('/vault/policies/foo');
+ andThen(() => {
+ assert.equal(currentRouteName(), 'vault.cluster.policies.index');
+ assert.equal(currentURL(), '/vault/policies/acl');
+ });
+});
diff --git a/ui/tests/acceptance/policy/edit-test.js b/ui/tests/acceptance/policy/edit-test.js
new file mode 100644
index 000000000..c09f8835a
--- /dev/null
+++ b/ui/tests/acceptance/policy/edit-test.js
@@ -0,0 +1,30 @@
+import { test } from 'qunit';
+import moduleForAcceptance from 'vault/tests/helpers/module-for-acceptance';
+import page from 'vault/tests/pages/policy/edit';
+
+moduleForAcceptance('Acceptance | policy/acl/:name/edit', {
+ beforeEach() {
+ return authLogin();
+ },
+});
+
+test('it redirects to list if navigating to root', function(assert) {
+ page.visit({ type: 'acl', name: 'root' });
+ andThen(function() {
+ assert.equal(currentURL(), '/vault/policies/acl', 'navigation to root show redirects you to policy list');
+ });
+});
+
+test('it does not show delete for default policy', function(assert) {
+ page.visit({ type: 'acl', name: 'default' });
+ andThen(function() {
+ assert.notOk(page.deleteIsPresent, 'there is no delete button');
+ });
+});
+
+test('it navigates to show when the toggle is clicked', function(assert) {
+ page.visit({ type: 'acl', name: 'default' }).toggleEdit();
+ andThen(() => {
+ assert.equal(currentURL(), '/vault/policy/acl/default', 'toggle navigates from edit to show');
+ });
+});
diff --git a/ui/tests/acceptance/policy/show-test.js b/ui/tests/acceptance/policy/show-test.js
new file mode 100644
index 000000000..15af7eaa5
--- /dev/null
+++ b/ui/tests/acceptance/policy/show-test.js
@@ -0,0 +1,23 @@
+import { test } from 'qunit';
+import moduleForAcceptance from 'vault/tests/helpers/module-for-acceptance';
+import page from 'vault/tests/pages/policy/show';
+
+moduleForAcceptance('Acceptance | policy/acl/:name', {
+ beforeEach() {
+ return authLogin();
+ },
+});
+
+test('it redirects to list if navigating to root', function(assert) {
+ page.visit({ type: 'acl', name: 'root' });
+ andThen(function() {
+ assert.equal(currentURL(), '/vault/policies/acl', 'navigation to root show redirects you to policy list');
+ });
+});
+
+test('it navigates to edit when the toggle is clicked', function(assert) {
+ page.visit({ type: 'acl', name: 'default' }).toggleEdit();
+ andThen(() => {
+ assert.equal(currentURL(), '/vault/policy/acl/default/edit', 'toggle navigates to edit page');
+ });
+});
diff --git a/ui/tests/acceptance/secrets/backend/kv/secret-test.js b/ui/tests/acceptance/secrets/backend/kv/secret-test.js
new file mode 100644
index 000000000..3667251a4
--- /dev/null
+++ b/ui/tests/acceptance/secrets/backend/kv/secret-test.js
@@ -0,0 +1,26 @@
+import { test } from 'qunit';
+import moduleForAcceptance from 'vault/tests/helpers/module-for-acceptance';
+import editPage from 'vault/tests/pages/secrets/backend/kv/edit-secret';
+import showPage from 'vault/tests/pages/secrets/backend/kv/show';
+import listPage from 'vault/tests/pages/secrets/backend/list';
+
+moduleForAcceptance('Acceptance | secrets/secret/create', {
+ beforeEach() {
+ return authLogin();
+ },
+});
+
+test('it creates a secret and redirects', function(assert) {
+ const path = `kv-${new Date().getTime()}`;
+ listPage.visitRoot({ backend: 'secret' });
+ andThen(() => {
+ assert.equal(currentRouteName(), 'vault.cluster.secrets.backend.list-root', 'navigates to the list page');
+ });
+
+ listPage.create();
+ editPage.createSecret(path, 'foo', 'bar');
+ andThen(() => {
+ assert.equal(currentRouteName(), 'vault.cluster.secrets.backend.show', 'redirects to the show page');
+ assert.ok(showPage.editIsPresent, 'shows the edit button');
+ });
+});
diff --git a/ui/tests/acceptance/secrets/backend/pki/cert-test.js b/ui/tests/acceptance/secrets/backend/pki/cert-test.js
new file mode 100644
index 000000000..d975a3a20
--- /dev/null
+++ b/ui/tests/acceptance/secrets/backend/pki/cert-test.js
@@ -0,0 +1,79 @@
+import { test } from 'qunit';
+import moduleForAcceptance from 'vault/tests/helpers/module-for-acceptance';
+import editPage from 'vault/tests/pages/secrets/backend/pki/edit-role';
+import listPage from 'vault/tests/pages/secrets/backend/list';
+import generatePage from 'vault/tests/pages/secrets/backend/pki/generate-cert';
+import showPage from 'vault/tests/pages/secrets/backend/pki/show';
+import configPage from 'vault/tests/pages/settings/configure-secret-backends/pki/section-cert';
+
+moduleForAcceptance('Acceptance | secrets/pki/list?tab=certs', {
+ beforeEach() {
+ return authLogin();
+ },
+});
+
+const CSR = `-----BEGIN CERTIFICATE REQUEST-----
+MIICdDCCAVwCAQAwDjEMMAoGA1UEAxMDbG9sMIIBIjANBgkqhkiG9w0BAQEFAAOC
+AQ8AMIIBCgKCAQEA4Dz2b/nAP/M6bqyk5mctqqYAAcoME//xPBy0wREHuZ776Pu4
+l45kDL3dPXiY8U2P9pn8WIr2KpLK6oWUfSsiG2P082bpWDL20UymkWqDhhrA4unf
+ZRq68UIDbcetlLw15YKnlNdvNZ7Qr8Se8KV0YGR/wFqI7QfS6VE3lhxZWEBUayI0
+egqOuDbXAcZTON1AZ92/F+WFSbc43iYdDk16XfAPFKhtvLr6zQQuzebAb7HG04Hc
+GhRskixxyJ8XY6XUplfsa1HcpUXE4f1GeUvq3g6ltVCSJ0p7qI9FFjV4t+DCLVVV
+LnwHUi9Vzz6i2wjMt7P6+gHR+RrOWBgRMn38fwIDAQABoCEwHwYJKoZIhvcNAQkO
+MRIwEDAOBgNVHREEBzAFggNsb2wwDQYJKoZIhvcNAQELBQADggEBAAm3AHQ1ctdV
+8HCrMOXGVLgI2cB1sFd6VYVxPBxIk812Y4wyO8Q6POE5VZNTIgMcSeIaFu5lgHNL
+Peeb54F+zEa+OJYkcWgCAX5mY/0HoML4p2bxFTSjllSpcX7ktjq4IEIY/LRpqSgc
+jgZHHRwanFfkeIOhN4Q5qJWgBPNhDAcNPE7T0M/4mxqYDqMSJvMYmC67hq1UOOug
+/QVDUDJRC1C0aDw9if+DbG/bt1V6HpMQhDIEUjzfu4zG8pcag3cJpOA8JhW1hnG0
+XA2ZOCA7s34/szr2FczXtIoKiYmv3UzPyO9/4mc0Q2+/nR4CG8NU9WW/XJCne9ID
+elRplAzrMF4=
+-----END CERTIFICATE REQUEST-----`;
+
+// mount, generate CA, nav to create role page
+const setup = (assert, action = 'issue') => {
+ const path = `pki-${new Date().getTime()}`;
+ const roleName = 'role';
+ mountSupportedSecretBackend(assert, 'pki', path);
+ configPage.visit({ backend: path }).form.generateCA();
+ editPage.visitRoot({ backend: path });
+ editPage.createRole('role', 'example.com');
+ generatePage.visit({ backend: path, id: roleName, action });
+ return path;
+};
+
+test('it issues a cert', function(assert) {
+ setup(assert);
+
+ generatePage.issueCert('foo');
+ andThen(() => {
+ assert.ok(generatePage.hasCert, 'displays the cert');
+ });
+
+ generatePage.back();
+ andThen(() => {
+ assert.notOk(generatePage.commonNameValue, 'the form is cleared');
+ });
+});
+
+test('it signs a csr', function(assert) {
+ setup(assert, 'sign');
+ generatePage.sign('common', CSR);
+ andThen(() => {
+ assert.ok(generatePage.hasCert, 'displays the cert');
+ });
+});
+
+test('it views a cert', function(assert) {
+ const path = setup(assert);
+ generatePage.issueCert('foo');
+ listPage.visitRoot({ backend: path, tab: 'certs' });
+ andThen(() => {
+ assert.ok(listPage.secrets().count > 0, 'lists certs');
+ });
+
+ listPage.secrets(0).click();
+ andThen(() => {
+ assert.equal(currentRouteName(), 'vault.cluster.secrets.backend.show', 'navigates to the show page');
+ assert.ok(showPage.hasCert, 'shows the cert');
+ });
+});
diff --git a/ui/tests/acceptance/secrets/backend/pki/list-test.js b/ui/tests/acceptance/secrets/backend/pki/list-test.js
new file mode 100644
index 000000000..107071d87
--- /dev/null
+++ b/ui/tests/acceptance/secrets/backend/pki/list-test.js
@@ -0,0 +1,46 @@
+import { test } from 'qunit';
+import moduleForAcceptance from 'vault/tests/helpers/module-for-acceptance';
+import page from 'vault/tests/pages/secrets/backend/list';
+
+moduleForAcceptance('Acceptance | secrets/pki/list', {
+ beforeEach() {
+ return authLogin();
+ },
+});
+
+const mountAndNav = assert => {
+ const path = `pki-${new Date().getTime()}`;
+ mountSupportedSecretBackend(assert, 'pki', path);
+ page.visitRoot({ backend: path });
+};
+
+test('it renders an empty list', function(assert) {
+ mountAndNav(assert);
+ andThen(() => {
+ assert.equal(currentRouteName(), 'vault.cluster.secrets.backend.list-root', 'redirects from the index');
+ assert.ok(page.createIsPresent, 'create button is present');
+ assert.ok(page.configureIsPresent, 'configure button is present');
+ assert.equal(page.tabs().count, 2, 'shows 2 tabs');
+ assert.ok(page.backendIsEmpty);
+ });
+});
+
+test('it navigates to the create page', function(assert) {
+ mountAndNav(assert);
+ page.create();
+ andThen(() => {
+ assert.equal(currentRouteName(), 'vault.cluster.secrets.backend.create-root', 'links to the create page');
+ });
+});
+
+test('it navigates to the configure page', function(assert) {
+ mountAndNav(assert);
+ page.configure();
+ andThen(() => {
+ assert.equal(
+ currentRouteName(),
+ 'vault.cluster.settings.configure-secret-backend.section',
+ 'links to the configure page'
+ );
+ });
+});
diff --git a/ui/tests/acceptance/secrets/backend/pki/role-test.js b/ui/tests/acceptance/secrets/backend/pki/role-test.js
new file mode 100644
index 000000000..3d49dad54
--- /dev/null
+++ b/ui/tests/acceptance/secrets/backend/pki/role-test.js
@@ -0,0 +1,69 @@
+import { test } from 'qunit';
+import moduleForAcceptance from 'vault/tests/helpers/module-for-acceptance';
+import editPage from 'vault/tests/pages/secrets/backend/pki/edit-role';
+import showPage from 'vault/tests/pages/secrets/backend/pki/show';
+import listPage from 'vault/tests/pages/secrets/backend/list';
+
+moduleForAcceptance('Acceptance | secrets/pki/create', {
+ beforeEach() {
+ return authLogin();
+ },
+});
+
+const mountAndNav = assert => {
+ const path = `pki-${new Date().getTime()}`;
+ mountSupportedSecretBackend(assert, 'pki', path);
+ editPage.visitRoot({ backend: path });
+ return path;
+};
+
+test('it creates a role and redirects', function(assert) {
+ const path = mountAndNav(assert);
+ editPage.createRole('role', 'example.com');
+ andThen(() => {
+ assert.equal(currentRouteName(), 'vault.cluster.secrets.backend.show', 'redirects to the show page');
+ assert.ok(showPage.editIsPresent, 'shows the edit button');
+ assert.ok(showPage.generateCertIsPresent, 'shows the generate button');
+ assert.ok(showPage.signCertIsPresent, 'shows the sign button');
+ });
+
+ showPage.visit({ backend: path, id: 'role' });
+ showPage.generateCert();
+ andThen(() => {
+ assert.equal(
+ currentRouteName(),
+ 'vault.cluster.secrets.backend.credentials',
+ 'navs to the credentials page'
+ );
+ });
+
+ showPage.visit({ backend: path, id: 'role' });
+ showPage.signCert();
+ andThen(() => {
+ assert.equal(
+ currentRouteName(),
+ 'vault.cluster.secrets.backend.credentials',
+ 'navs to the credentials page'
+ );
+ });
+
+ listPage.visitRoot({ backend: path });
+ andThen(() => {
+ assert.equal(listPage.secrets().count, 1, 'shows role in the list');
+ });
+});
+
+test('it deletes a role', function(assert) {
+ mountAndNav(assert);
+ editPage.createRole('role', 'example.com');
+ showPage.edit();
+ andThen(() => {
+ assert.equal(currentRouteName(), 'vault.cluster.secrets.backend.edit', 'navs to the edit page');
+ });
+
+ editPage.deleteRole();
+ andThen(() => {
+ assert.equal(currentRouteName(), 'vault.cluster.secrets.backend.list-root', 'redirects to list page');
+ assert.ok(listPage.backendIsEmpty, 'no roles listed');
+ });
+});
diff --git a/ui/tests/acceptance/secrets/no-access-test.js b/ui/tests/acceptance/secrets/no-access-test.js
new file mode 100644
index 000000000..af15e7cfd
--- /dev/null
+++ b/ui/tests/acceptance/secrets/no-access-test.js
@@ -0,0 +1,31 @@
+import { test } from 'qunit';
+import moduleForAcceptance from 'vault/tests/helpers/module-for-acceptance';
+import listPage from 'vault/tests/pages/secrets/backend/list';
+import { startMirage } from 'vault/initializers/ember-cli-mirage';
+import Ember from 'ember';
+
+let adapterException;
+let loggerError;
+
+moduleForAcceptance('Acceptance | secrets/secret/secret error', {
+ beforeEach() {
+ this.server = startMirage();
+ loggerError = Ember.Logger.error;
+ adapterException = Ember.Test.adapter.exception;
+ Ember.Test.adapter.exception = () => {};
+ Ember.Logger.error = () => {};
+ return authLogin();
+ },
+ afterEach() {
+ Ember.Test.adapter.exception = adapterException;
+ Ember.Logger.error = loggerError;
+ this.server.shutdown();
+ },
+});
+
+test('it shows a warning if dont have access to the secrets list', function(assert) {
+ listPage.visitRoot({ backend: 'secret' });
+ andThen(() => {
+ assert.ok(find('[data-test-sys-mounts-warning]').length, 'shows the warning for sys/mounts');
+ });
+});
diff --git a/ui/tests/acceptance/settings-test.js b/ui/tests/acceptance/settings-test.js
new file mode 100644
index 000000000..9f0345f72
--- /dev/null
+++ b/ui/tests/acceptance/settings-test.js
@@ -0,0 +1,49 @@
+import { test } from 'qunit';
+import moduleForAcceptance from 'vault/tests/helpers/module-for-acceptance';
+
+moduleForAcceptance('Acceptance | settings', {
+ beforeEach() {
+ return authLogin();
+ },
+ afterEach() {
+ return authLogout();
+ },
+});
+
+test('settings', function(assert) {
+ const now = new Date().getTime();
+ const type = 'consul';
+ const path = `path-${now}`;
+
+ // mount unsupported backend
+ visit('/vault/settings/mount-secret-backend');
+ andThen(function() {
+ assert.equal(currentURL(), '/vault/settings/mount-secret-backend');
+ });
+
+ fillIn('[data-test-secret-backend-type]', type);
+ fillIn('[data-test-secret-backend-path]', path);
+ click('[data-test-secret-backend-options]');
+
+ // set a ttl of 100s
+ fillIn('[data-test-secret-backend-default-ttl] input', 100);
+ fillIn('[data-test-secret-backend-default-ttl] select', 's');
+
+ click('[data-test-secret-backend-submit]');
+ andThen(() => {
+ assert.equal(currentURL(), `/vault/secrets`, 'redirects to secrets page');
+ assert.ok(
+ find('[data-test-flash-message]').text().trim(),
+ `Successfully mounted '${type}' at '${path}'!`
+ );
+ });
+
+ //show mount details
+ click(`[data-test-secret-backend-row="${path}"] [data-test-secret-backend-detail]`);
+ andThen(() => {
+ assert.ok(
+ find('[data-test-secret-backend-details="default-ttl"]').text().match(/100/),
+ 'displays the input ttl'
+ );
+ });
+});
diff --git a/ui/tests/acceptance/settings/auth/configure/index-test.js b/ui/tests/acceptance/settings/auth/configure/index-test.js
new file mode 100644
index 000000000..3c1101805
--- /dev/null
+++ b/ui/tests/acceptance/settings/auth/configure/index-test.js
@@ -0,0 +1,37 @@
+import { test } from 'qunit';
+import moduleForAcceptance from 'vault/tests/helpers/module-for-acceptance';
+import enablePage from 'vault/tests/pages/settings/auth/enable';
+import page from 'vault/tests/pages/settings/auth/configure/index';
+
+moduleForAcceptance('Acceptance | settings/auth/configure', {
+ beforeEach() {
+ return authLogin();
+ },
+});
+
+test('it redirects to section options when there are no other sections', function(assert) {
+ const path = `approle-${new Date().getTime()}`;
+ const type = 'approle';
+ enablePage.visit().enableAuth(type, path);
+ page.visit({ path });
+ andThen(() => {
+ assert.equal(currentRouteName(), 'vault.cluster.settings.auth.configure.section');
+ assert.equal(currentURL(), `/vault/settings/auth/configure/${path}/options`, 'loads the options route');
+ });
+});
+
+test('it redirects to the first section', function(assert) {
+ const path = `aws-${new Date().getTime()}`;
+ const type = 'aws';
+ enablePage.visit().enableAuth(type, path);
+ page.visit({ path });
+
+ andThen(() => {
+ assert.equal(currentRouteName(), 'vault.cluster.settings.auth.configure.section');
+ assert.equal(
+ currentURL(),
+ `/vault/settings/auth/configure/${path}/client`,
+ 'loads the first section for the type of auth method'
+ );
+ });
+});
diff --git a/ui/tests/acceptance/settings/auth/configure/section-test.js b/ui/tests/acceptance/settings/auth/configure/section-test.js
new file mode 100644
index 000000000..878f9c1dd
--- /dev/null
+++ b/ui/tests/acceptance/settings/auth/configure/section-test.js
@@ -0,0 +1,29 @@
+import { test } from 'qunit';
+import moduleForAcceptance from 'vault/tests/helpers/module-for-acceptance';
+import enablePage from 'vault/tests/pages/settings/auth/enable';
+import page from 'vault/tests/pages/settings/auth/configure/section';
+
+moduleForAcceptance('Acceptance | settings/auth/configure/section', {
+ beforeEach() {
+ return authLogin();
+ },
+});
+
+test('it can save options', function(assert) {
+ const path = `approle-${new Date().getTime()}`;
+ const type = 'approle';
+ const section = 'options';
+ enablePage.visit().enableAuth(type, path);
+ page.visit({ path, section });
+ andThen(() => {
+ page.fields().findByName('description').textarea('This is AppRole!');
+ page.save();
+ });
+ andThen(() => {
+ assert.equal(
+ page.flash.latestMessage,
+ `The configuration options were saved successfully.`,
+ 'success flash shows'
+ );
+ });
+});
diff --git a/ui/tests/acceptance/settings/auth/enable-test.js b/ui/tests/acceptance/settings/auth/enable-test.js
new file mode 100644
index 000000000..9cfe80ecf
--- /dev/null
+++ b/ui/tests/acceptance/settings/auth/enable-test.js
@@ -0,0 +1,34 @@
+import { test } from 'qunit';
+import moduleForAcceptance from 'vault/tests/helpers/module-for-acceptance';
+import page from 'vault/tests/pages/settings/auth/enable';
+import listPage from 'vault/tests/pages/access/methods';
+
+moduleForAcceptance('Acceptance | settings/auth/enable', {
+ beforeEach() {
+ return authLogin();
+ },
+});
+
+test('it mounts and redirects', function(assert) {
+ page.visit();
+ andThen(() => {
+ assert.equal(currentRouteName(), 'vault.cluster.settings.auth.enable');
+ });
+ // always force the new mount to the top of the list
+ const path = `approle-${new Date().getTime()}`;
+ const type = 'approle';
+ page.enableAuth(type, path);
+ andThen(() => {
+ assert.equal(
+ page.flash.latestMessage,
+ `Successfully mounted ${type} auth method at ${path}.`,
+ 'success flash shows'
+ );
+ assert.equal(
+ currentRouteName(),
+ 'vault.cluster.access.methods',
+ 'redirects to the auth backend list page'
+ );
+ assert.ok(listPage.backendLinks().findById(path), 'mount is present in the list');
+ });
+});
diff --git a/ui/tests/acceptance/settings/configure-secret-backends/pki/index-test.js b/ui/tests/acceptance/settings/configure-secret-backends/pki/index-test.js
new file mode 100644
index 000000000..dcf7ba0dd
--- /dev/null
+++ b/ui/tests/acceptance/settings/configure-secret-backends/pki/index-test.js
@@ -0,0 +1,22 @@
+import { test } from 'qunit';
+import moduleForAcceptance from 'vault/tests/helpers/module-for-acceptance';
+import page from 'vault/tests/pages/settings/configure-secret-backends/pki/index';
+
+moduleForAcceptance('Acceptance | settings/configure/secrets/pki', {
+ beforeEach() {
+ return authLogin();
+ },
+});
+
+test('it redirects to the cert section', function(assert) {
+ const path = `pki-${new Date().getTime()}`;
+ mountSupportedSecretBackend(assert, 'pki', path);
+ page.visit({ backend: path });
+ andThen(() => {
+ assert.equal(
+ currentRouteName(),
+ 'vault.cluster.settings.configure-secret-backend.section',
+ 'redirects from the index'
+ );
+ });
+});
diff --git a/ui/tests/acceptance/settings/configure-secret-backends/pki/section-cert-test.js b/ui/tests/acceptance/settings/configure-secret-backends/pki/section-cert-test.js
new file mode 100644
index 000000000..864fe0e63
--- /dev/null
+++ b/ui/tests/acceptance/settings/configure-secret-backends/pki/section-cert-test.js
@@ -0,0 +1,136 @@
+import { test } from 'qunit';
+import moduleForAcceptance from 'vault/tests/helpers/module-for-acceptance';
+import page from 'vault/tests/pages/settings/configure-secret-backends/pki/section-cert';
+
+moduleForAcceptance('Acceptance | settings/configure/secrets/pki/cert', {
+ beforeEach() {
+ return authLogin();
+ },
+});
+
+const PEM_BUNDLE = `-----BEGIN CERTIFICATE-----
+MIIDGjCCAgKgAwIBAgIUFvnhb2nQ8+KNS3SzjlfYDMHGIRgwDQYJKoZIhvcNAQEL
+BQAwDTELMAkGA1UEAxMCZmEwHhcNMTgwMTEwMTg1NDI5WhcNMTgwMjExMTg1NDU5
+WjANMQswCQYDVQQDEwJmYTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
+AN2VtBn6EMlA4aYre/xoKHxlgNDxJnfSQWfs6yF/K201qPnt4QF9AXChatbmcKVn
+OaURq+XEJrGVgF/u2lSos3NRZdhWVe8o3/sOetsGxcrd0gXAieOSmkqJjp27bYdl
+uY3WsxhyiPvdfS6xz39OehsK/YCB6qCzwB4eEfSKqbkvfDL9sLlAiOlaoHC9pczf
+6/FANKp35UDwInSwmq5vxGbnWk9zMkh5Jq6hjOWHZnVc2J8J49PYvkIM8uiHDgOE
+w71T2xM5plz6crmZnxPCOcTKIdF7NTEP2lUfiqc9lONV9X1Pi4UclLPHJf5bwTmn
+JaWgbKeY+IlF61/mgxzhC7cCAwEAAaNyMHAwDgYDVR0PAQH/BAQDAgEGMA8GA1Ud
+EwEB/wQFMAMBAf8wHQYDVR0OBBYEFLDtc6+HZN2lv60JSDAZq3+IHoq7MB8GA1Ud
+IwQYMBaAFLDtc6+HZN2lv60JSDAZq3+IHoq7MA0GA1UdEQQGMASCAmZhMA0GCSqG
+SIb3DQEBCwUAA4IBAQDVt6OddTV1MB0UvF5v4zL1bEB9bgXvWx35v/FdS+VGn/QP
+cC2c4ZNukndyHhysUEPdqVg4+up1aXm4eKXzNmGMY/ottN2pEhVEWQyoIIA1tH0e
+8Kv/bysYpHZKZuoGg5+mdlHS2p2Dh2bmYFyBLJ8vaeP83NpTs2cNHcmEvWh/D4UN
+UmYDODRN4qh9xYruKJ8i89iMGQfbdcq78dCC4JwBIx3bysC8oF4lqbTYoYNVTnAi
+LVqvLdHycEOMlqV0ecq8uMLhPVBalCmIlKdWNQFpXB0TQCsn95rCCdi7ZTsYk5zv
+Q4raFvQrZth3Cz/X5yPTtQL78oBYrmHzoQKDFJ2z
+-----END CERTIFICATE-----
+-----BEGIN RSA PRIVATE KEY-----
+MIIEogIBAAKCAQEA3ZW0GfoQyUDhpit7/GgofGWA0PEmd9JBZ+zrIX8rbTWo+e3h
+AX0BcKFq1uZwpWc5pRGr5cQmsZWAX+7aVKizc1Fl2FZV7yjf+w562wbFyt3SBcCJ
+45KaSomOnbtth2W5jdazGHKI+919LrHPf056Gwr9gIHqoLPAHh4R9IqpuS98Mv2w
+uUCI6VqgcL2lzN/r8UA0qnflQPAidLCarm/EZudaT3MySHkmrqGM5YdmdVzYnwnj
+09i+Qgzy6IcOA4TDvVPbEzmmXPpyuZmfE8I5xMoh0Xs1MQ/aVR+Kpz2U41X1fU+L
+hRyUs8cl/lvBOaclpaBsp5j4iUXrX+aDHOELtwIDAQABAoIBACLdk2Ei/9Eq7FaB
+MRkeKoCoWASIbU0dQD1iAf1bTTH554Sr8WOSj89xFqaJy9+6xk864Jleq9f1diWi
+J6h6gwH6JNRNgWgIPnX6aUpdXnH1RT6ydP/h6XUg/9fBzhIn53Jx/ewy2WsIBtJ6
+F/QoHP50VD8MMibnIaubf6fCycHhc97u4BKM2QdnAugn1sWjSiTIoYmFw/3Ej8mB
+bItLWZTg9oMASgCtDwPEstlKn7yPqirOJj+G/a+6sIcP2fynd0fISsfLZ0ovN+yW
+d3SV3orC0RNj83GVwYykqwCc/3pP0mRfX9fl8DKbXusITqUiGL8LGb+H6YDDpbNU
+5Fj7VwECgYEA5P6aIcGfCZayEJtHKlTCA2/KBkGTOP/0iNKWhntBQT/GK+bjmr+D
+GO1zR8ZFEIRdlUA5MjC9wU2AQikgFQzzmtz604Wt34fDN2NFrxq8sWN7Hjr65Fjf
+ivJ6faT5r5gcNEq3EM/GLF9oJH8M+B5ccFe9iXH8AbmZHOO0FZtYxIcCgYEA97dm
+Kj1qyuKlINXKt4KXdYMuIT+Z3G1B92wNN9TY/eJZgCJ7zlNcinUF/OFbiGgsk4t+
+P0yVMs8BENQML0TH4Gebf4HfnDFno4J1M9HDt6HSMhsLKyvFYjFvb8hF4SPrY1pF
+wW3lM3zMMzAVi8044vRrTvxfxL8QJX+1Hesye1ECgYAT5/H8Fzm8+qWV/fmMu3t2
+EwSr0I18uftG3Y+KNzKv+lw+ur50WEuMIjAQQDMGwYrlC4UtUMFeCV+p4KtSSSLw
+Bl+jfY5kzQdyTCXll9xpSy2LrjLbIMKl8Hgnbezqj7176jbJtlYSy2RhL84vz2vX
+tDjcttTiTYD62uxvqGZqBwKBgFQ3tPM9aDZL8coFBWN4cZfRHnjNT7kCKEA/KwtF
+QPSn5LfMgXz3GGo2OO/tihoJGMac0TIiDkN03y7ieLYFU1L2xoYGGIjYvxx2+PPC
+KCEhUf4Y9aYavoOQvQsq8p8FgDyJ71dAzoC/uAjbGygpgGKgqG71HHYeYxXsoh3m
+3YXRAoGAE7MBnVJWiIN5s63gGz9f9V6k1dPLfcPE1I0KMM/SDOIV0oLMsYQecTTB
+ZzkXwRCdcJARkaKulTfjby7+oGpQydP8iZr+CNKFxwf838UbhhsXHnN6rc62qzYD
+BXUV2Uwtxf+QCphnlht9muX2fsLIzDJea0JipWj1uf2H8OZsjE8=
+-----END RSA PRIVATE KEY-----`;
+
+const mountAndNav = (assert, prefix) => {
+ const path = `${prefix}pki-${new Date().getTime()}`;
+ mountSupportedSecretBackend(assert, 'pki', path);
+ page.visit({ backend: path });
+ return path;
+};
+
+test('cert config: generate', function(assert) {
+ mountAndNav(assert);
+ andThen(() => {
+ assert.equal(currentRouteName(), 'vault.cluster.settings.configure-secret-backend.section');
+ });
+
+ page.form.generateCA();
+ andThen(() => {
+ assert.ok(page.form.rows().count > 0, 'shows all of the rows');
+ assert.ok(page.form.certificateIsPresent, 'the certificate is included');
+ });
+
+ page.form.back();
+ page.form.generateCA();
+ andThen(() => {
+ assert.ok(
+ page.flash.latestMessage.includes('You tried to generate a new root CA'),
+ 'shows warning message'
+ );
+ });
+});
+
+test('cert config: upload', function(assert) {
+ mountAndNav(assert);
+ andThen(() => {
+ assert.equal(page.form.downloadLinks().count, 0, 'there are no download links');
+ });
+
+ page.form.uploadCA(PEM_BUNDLE);
+ andThen(() => {
+ assert.ok(
+ page.flash.latestMessage.startsWith('The certificate for this backend has been updated'),
+ 'flash message displays properly'
+ );
+ });
+});
+
+test('cert config: sign intermediate and set signed intermediate', function(assert) {
+ let csrVal, intermediateCert;
+ const rootPath = mountAndNav(assert, 'root-');
+ page.form.generateCA();
+
+ const intermediatePath = mountAndNav(assert, 'intermediate-');
+ page.form.generateCA('Intermediate CA', 'intermediate');
+ andThen(() => {
+ // cache csr
+ csrVal = page.form.csr;
+ });
+ page.form.back();
+
+ page.visit({ backend: rootPath });
+ page.form.signIntermediate('Intermediate CA');
+ andThen(() => {
+ page.form.csrField(csrVal).submit();
+ });
+ andThen(() => {
+ intermediateCert = page.form.certificate;
+ });
+ page.form.back();
+ page.visit({ backend: intermediatePath });
+
+ andThen(() => {
+ page.form.setSignedIntermediateBtn().signedIntermediate(intermediateCert).submit();
+ });
+ andThen(() => {
+ assert.ok(
+ page.flash.latestMessage.startsWith('The certificate for this backend has been updated'),
+ 'flash message displays properly'
+ );
+ assert.equal(page.form.downloadLinks().count, 3, 'includes the caChain download link');
+ });
+});
diff --git a/ui/tests/acceptance/settings/configure-secret-backends/pki/section-crl-test.js b/ui/tests/acceptance/settings/configure-secret-backends/pki/section-crl-test.js
new file mode 100644
index 000000000..a196fc522
--- /dev/null
+++ b/ui/tests/acceptance/settings/configure-secret-backends/pki/section-crl-test.js
@@ -0,0 +1,26 @@
+import { test } from 'qunit';
+import moduleForAcceptance from 'vault/tests/helpers/module-for-acceptance';
+import page from 'vault/tests/pages/settings/configure-secret-backends/pki/section';
+
+moduleForAcceptance('Acceptance | settings/configure/secrets/pki/crl', {
+ beforeEach() {
+ return authLogin();
+ },
+});
+
+test('it saves crl config', function(assert) {
+ const path = `pki-${new Date().getTime()}`;
+ mountSupportedSecretBackend(assert, 'pki', path);
+ page.visit({ backend: path, section: 'crl' });
+ andThen(() => {
+ assert.equal(currentRouteName(), 'vault.cluster.settings.configure-secret-backend.section');
+ });
+
+ page.form.fillInField('time', 3);
+ page.form.fillInField('unit', 'h');
+ page.form.submit();
+
+ andThen(() => {
+ assert.equal(page.lastMessage, 'The crl config for this backend has been updated.');
+ });
+});
diff --git a/ui/tests/acceptance/settings/configure-secret-backends/pki/section-tidy-test.js b/ui/tests/acceptance/settings/configure-secret-backends/pki/section-tidy-test.js
new file mode 100644
index 000000000..40c66e544
--- /dev/null
+++ b/ui/tests/acceptance/settings/configure-secret-backends/pki/section-tidy-test.js
@@ -0,0 +1,26 @@
+import { test } from 'qunit';
+import moduleForAcceptance from 'vault/tests/helpers/module-for-acceptance';
+import page from 'vault/tests/pages/settings/configure-secret-backends/pki/section';
+
+moduleForAcceptance('Acceptance | settings/configure/secrets/pki/tidy', {
+ beforeEach() {
+ return authLogin();
+ },
+});
+
+test('it saves tidy config', function(assert) {
+ const path = `pki-${new Date().getTime()}`;
+ mountSupportedSecretBackend(assert, 'pki', path);
+ page.visit({ backend: path, section: 'tidy' });
+ andThen(() => {
+ assert.equal(currentRouteName(), 'vault.cluster.settings.configure-secret-backend.section');
+ page.form.fields();
+ });
+
+ page.form.fields(0).clickLabel();
+ page.form.submit();
+
+ andThen(() => {
+ assert.equal(page.lastMessage, 'The tidy config for this backend has been updated.');
+ });
+});
diff --git a/ui/tests/acceptance/settings/configure-secret-backends/pki/section-urls-test.js b/ui/tests/acceptance/settings/configure-secret-backends/pki/section-urls-test.js
new file mode 100644
index 000000000..92b270b88
--- /dev/null
+++ b/ui/tests/acceptance/settings/configure-secret-backends/pki/section-urls-test.js
@@ -0,0 +1,38 @@
+import Ember from 'ember';
+import { test } from 'qunit';
+import moduleForAcceptance from 'vault/tests/helpers/module-for-acceptance';
+import page from 'vault/tests/pages/settings/configure-secret-backends/pki/section';
+let adapterException;
+moduleForAcceptance('Acceptance | settings/configure/secrets/pki/urls', {
+ beforeEach() {
+ adapterException = Ember.Test.adapter.exception;
+ Ember.Test.adapter.exception = () => null;
+ return authLogin();
+ },
+ afterEach() {
+ Ember.Test.adapter.exception = adapterException;
+ },
+});
+
+test('it saves urls config', function(assert) {
+ const path = `pki-${new Date().getTime()}`;
+ mountSupportedSecretBackend(assert, 'pki', path);
+ page.visit({ backend: path, section: 'urls' });
+ andThen(() => {
+ assert.equal(currentRouteName(), 'vault.cluster.settings.configure-secret-backend.section');
+ });
+
+ page.form.fields(0).input('foo').change();
+ page.form.submit();
+
+ andThen(() => {
+ assert.ok(page.form.hasError, 'shows error on invalid input');
+ });
+
+ page.form.fields(0).input('foo.example.com').change();
+ page.form.submit();
+
+ andThen(() => {
+ assert.equal(page.lastMessage, 'The urls config for this backend has been updated.');
+ });
+});
diff --git a/ui/tests/acceptance/settings/mount-secret-backend-test.js b/ui/tests/acceptance/settings/mount-secret-backend-test.js
new file mode 100644
index 000000000..fd9e85a8c
--- /dev/null
+++ b/ui/tests/acceptance/settings/mount-secret-backend-test.js
@@ -0,0 +1,42 @@
+import { test } from 'qunit';
+import moduleForAcceptance from 'vault/tests/helpers/module-for-acceptance';
+import page from 'vault/tests/pages/settings/mount-secret-backend';
+import listPage from 'vault/tests/pages/secrets/backends';
+
+moduleForAcceptance('Acceptance | settings/mount-secret-backend', {
+ beforeEach() {
+ return authLogin();
+ },
+});
+
+test('it sets the ttl corrects when mounting', function(assert) {
+ page.visit();
+ andThen(() => {
+ assert.equal(currentRouteName(), 'vault.cluster.settings.mount-secret-backend');
+ });
+ // always force the new mount to the top of the list
+ const path = `kv-${new Date().getTime()}`;
+ const defaultTTLHours = 100;
+ const maxTTLHours = 300;
+ const defaultTTLSeconds = defaultTTLHours * 60 * 60;
+ const maxTTLSeconds = maxTTLHours * 60 * 60;
+ page
+ .type('kv')
+ .path(path)
+ .toggleOptions()
+ .defaultTTLVal(defaultTTLHours)
+ .defaultTTLUnit('h')
+ .maxTTLVal(maxTTLHours)
+ .maxTTLUnit('h')
+ .submit();
+
+ listPage.visit();
+ andThen(() => {
+ listPage.links().findByPath(path).toggleDetails();
+ });
+ andThen(() => {
+ const details = listPage.links().findByPath(path);
+ assert.equal(details.defaultTTL, defaultTTLSeconds, 'shows the proper TTL');
+ assert.equal(details.maxTTL, maxTTLSeconds, 'shows the proper max TTL');
+ });
+});
diff --git a/ui/tests/acceptance/ssh-test.js b/ui/tests/acceptance/ssh-test.js
new file mode 100644
index 000000000..5e2868c0a
--- /dev/null
+++ b/ui/tests/acceptance/ssh-test.js
@@ -0,0 +1,153 @@
+import { test } from 'qunit';
+import moduleForAcceptance from 'vault/tests/helpers/module-for-acceptance';
+
+moduleForAcceptance('Acceptance | ssh secret backend', {
+ beforeEach() {
+ return authLogin();
+ },
+ afterEach() {
+ return authLogout();
+ },
+});
+
+const PUB_KEY = `ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCn9p5dHNr4aU4R2W7ln+efzO5N2Cdv/SXk6zbCcvhWcblWMjkXf802B0PbKvf6cJIzM/Xalb3qz1cK+UUjCSEAQWefk6YmfzbOikfc5EHaSKUqDdE+HlsGPvl42rjCr28qYfuYh031YfwEQGEAIEypo7OyAj+38NLbHAQxDxuaReee1YCOV5rqWGtEgl2VtP5kG+QEBza4ZfeglS85f/GGTvZC4Jq1GX+wgmFxIPnd6/mUXa4ecoR0QMfOAzzvPm4ajcNCQORfHLQKAcmiBYMiyQJoU+fYpi9CJGT1jWTmR99yBkrSg6yitI2qqXyrpwAbhNGrM0Fw0WpWxh66N9Xp meirish@Macintosh-3.local`;
+
+const ROLES = [
+ {
+ type: 'ca',
+ name: 'carole',
+ fillInCreate() {
+ click('[data-test-input="allowUserCertificates"]');
+ },
+ fillInGenerate() {
+ fillIn('[data-test-input="publicKey"]', PUB_KEY);
+ },
+ assertAfterGenerate(assert, sshPath) {
+ assert.equal(currentURL(), `/vault/secrets/${sshPath}/sign/${this.name}`, 'ca sign url is correct');
+ assert.equal(find('[data-test-row-label="Signed key"]').length, 1, 'renders the signed key');
+ assert.equal(find('[data-test-row-value="Signed key"]').length, 1, "renders the signed key's value");
+ assert.equal(find('[data-test-row-label="Serial number"]').length, 1, 'renders the serial');
+ assert.equal(find('[data-test-row-value="Serial number"]').length, 1, 'renders the serial value');
+ },
+ },
+ {
+ type: 'otp',
+ name: 'otprole',
+ fillInCreate() {
+ fillIn('[data-test-input="defaultUser"]', 'admin');
+ click('[data-test-toggle-more]');
+ fillIn('[data-test-input="cidrList"]', '1.2.3.4/32');
+ },
+ fillInGenerate() {
+ fillIn('[data-test-input="username"]', 'admin');
+ fillIn('[data-test-input="ip"]', '1.2.3.4');
+ },
+ assertAfterGenerate(assert, sshPath) {
+ assert.equal(
+ currentURL(),
+ `/vault/secrets/${sshPath}/credentials/${this.name}`,
+ 'otp credential url is correct'
+ );
+ assert.equal(find('[data-test-row-label="Key"]').length, 1, 'renders the key');
+ assert.equal(find('[data-test-row-value="Key"]').length, 1, "renders the key's value");
+ assert.equal(find('[data-test-row-label="Port"]').length, 1, 'renders the port');
+ assert.equal(find('[data-test-row-value="Port"]').length, 1, "renders the port's value");
+ },
+ },
+];
+test('ssh backend', function(assert) {
+ const now = new Date().getTime();
+ const sshPath = `ssh-${now}`;
+
+ mountSupportedSecretBackend(assert, 'ssh', sshPath);
+ click('[data-test-secret-backend-configure]');
+ andThen(() => {
+ assert.equal(currentURL(), `/vault/settings/secrets/configure/${sshPath}`);
+ assert.ok(find('[data-test-ssh-configure-form]').length, 'renders the empty configuration form');
+ });
+
+ // default has generate CA checked so we just submit the form
+ click('[data-test-ssh-input="configure-submit"]');
+ andThen(() => {
+ assert.ok(find('[data-test-ssh-input="public-key"]').length, 'a public key is fetched');
+ });
+ click('[data-test-backend-view-link]');
+
+ //back at the roles list
+ andThen(() => {
+ assert.equal(currentURL(), `/vault/secrets/${sshPath}/list`, `redirects to ssh index`);
+ });
+
+ ROLES.forEach(role => {
+ // create a role
+ click('[ data-test-secret-create]');
+ andThen(() => {
+ assert.ok(
+ find('[data-test-secret-header]').text().includes('SSH Role'),
+ `${role.type}: renders the create page`
+ );
+ });
+
+ fillIn('[data-test-input="name"]', role.name);
+ fillIn('[data-test-input="keyType"]', role.type);
+ role.fillInCreate();
+
+ // save the role
+ click('[data-test-role-ssh-create]');
+ andThen(() => {
+ assert.equal(
+ currentURL(),
+ `/vault/secrets/${sshPath}/show/${role.name}`,
+ `${role.type}: navigates to the show page on creation`
+ );
+ });
+
+ // sign a key with this role
+ click('[data-test-backend-credentials]');
+ role.fillInGenerate();
+
+ // generate creds
+ click('[data-test-secret-generate]');
+ andThen(() => {
+ role.assertAfterGenerate(assert, sshPath);
+ });
+
+ // click the "Back" button
+ click('[data-test-secret-generate-back]');
+ andThen(() => {
+ assert.ok(
+ find('[data-test-secret-generate-form]').length,
+ `${role.type}: back takes you back to the form`
+ );
+ });
+
+ click('[data-test-secret-generate-cancel]');
+ //back at the roles list
+ andThen(() => {
+ assert.equal(
+ currentURL(),
+ `/vault/secrets/${sshPath}/list`,
+ `${role.type}: cancel takes you to ssh index`
+ );
+ assert.ok(
+ find(`[data-test-secret-link="${role.name}"]`).length,
+ `${role.type}: role shows in the list`
+ );
+ });
+
+ //and delete
+ click(`[data-test-secret-link="${role.name}"] [data-test-popup-menu-trigger]`);
+ andThen(() => {
+ click(`[data-test-ssh-role-delete="${role.name}"] button`);
+ });
+ click(`[data-test-confirm-button]`);
+
+ andThen(() => {
+ assert.equal(
+ find(`[data-test-secret-link="${role.name}"]`).length,
+ 0,
+ `${role.type}: role is no longer in the list`
+ );
+ });
+ });
+});
diff --git a/ui/tests/acceptance/tools-test.js b/ui/tests/acceptance/tools-test.js
new file mode 100644
index 000000000..9c7d0b218
--- /dev/null
+++ b/ui/tests/acceptance/tools-test.js
@@ -0,0 +1,137 @@
+import { test } from 'qunit';
+import moduleForAcceptance from 'vault/tests/helpers/module-for-acceptance';
+import { toolsActions } from 'vault/helpers/tools-actions';
+
+moduleForAcceptance('Acceptance | tools', {
+ beforeEach() {
+ return authLogin();
+ },
+ afterEach() {
+ return authLogout();
+ },
+});
+
+const DATA_TO_WRAP = JSON.stringify({ tools: 'tests' });
+const TOOLS_ACTIONS = toolsActions();
+
+/*
+data-test-tools-input="wrapping-token"
+data-test-tools-input="rewrapped-token"
+data-test-tools="token-lookup-row"
+data-test-tools-action-link=supportedAction
+*/
+
+var createTokenStore = () => {
+ let token;
+ return {
+ set(val) {
+ token = val;
+ },
+ get() {
+ return token;
+ },
+ };
+};
+test('tools functionality', function(assert) {
+ var tokenStore = createTokenStore();
+ visit('/vault/tools');
+ andThen(function() {
+ assert.equal(currentURL(), '/vault/tools/wrap', 'forwards to the first action');
+ TOOLS_ACTIONS.forEach(action => {
+ assert.ok(findWithAssert(`[data-test-tools-action-link="${action}"]`), `${action} link renders`);
+ });
+ find('.CodeMirror').get(0).CodeMirror.setValue(DATA_TO_WRAP);
+ });
+
+ // wrap
+ click('[data-test-tools-submit]');
+ andThen(function() {
+ tokenStore.set(find('[data-test-tools-input="wrapping-token"]').val());
+ assert.ok(find('[data-test-tools-input="wrapping-token"]').val(), 'has a wrapping token');
+ });
+
+ //lookup
+ click('[data-test-tools-action-link="lookup"]');
+ // have to wrap this in andThen because tokenStore is sync, but fillIn is async
+ andThen(() => {
+ fillIn('[data-test-tools-input="wrapping-token"]', tokenStore.get());
+ });
+ click('[data-test-tools-submit]');
+ andThen(() => {
+ assert.ok(
+ find('[data-test-tools="token-lookup-row"]:eq(0)').text().match(/Creation time/i),
+ 'show creation time row'
+ );
+ assert.ok(
+ find('[data-test-tools="token-lookup-row"]:eq(1)').text().match(/Creation ttl/i),
+ 'show creation ttl row'
+ );
+ });
+
+ //rewrap
+ click('[data-test-tools-action-link="rewrap"]');
+ andThen(() => {
+ fillIn('[data-test-tools-input="wrapping-token"]', tokenStore.get());
+ });
+ click('[data-test-tools-submit]');
+ andThen(() => {
+ assert.ok(find('[data-test-tools-input="rewrapped-token"]').val(), 'has a new re-wrapped token');
+ assert.notEqual(
+ find('[data-test-tools-input="rewrapped-token"]').val(),
+ tokenStore.get(),
+ 're-wrapped token is not the wrapped token'
+ );
+ tokenStore.set(find('[data-test-tools-input="rewrapped-token"]').val());
+ });
+
+ //unwrap
+ click('[data-test-tools-action-link="unwrap"]');
+ andThen(() => {
+ fillIn('[data-test-tools-input="wrapping-token"]', tokenStore.get());
+ });
+ click('[data-test-tools-submit]');
+ andThen(() => {
+ assert.deepEqual(
+ JSON.parse(find('.CodeMirror').get(0).CodeMirror.getValue()),
+ JSON.parse(DATA_TO_WRAP),
+ 'unwrapped data equals input data'
+ );
+ });
+
+ //random
+ click('[data-test-tools-action-link="random"]');
+ andThen(() => {
+ assert.equal(find('[data-test-tools-input="bytes"]').val(), 32, 'defaults to 32 bytes');
+ });
+ click('[data-test-tools-submit]');
+ andThen(() => {
+ assert.ok(
+ find('[data-test-tools-input="random-bytes"]').val(),
+ 'shows the returned value of random bytes'
+ );
+ });
+
+ //hash
+ click('[data-test-tools-action-link="hash"]');
+ fillIn('[data-test-tools-input="hash-input"]', 'foo');
+ click('[data-test-tools-b64-toggle="input"]');
+ click('[data-test-tools-submit]');
+ andThen(() => {
+ assert.equal(
+ find('[data-test-tools-input="sum"]').val(),
+ 'LCa0a2j/xo/5m0U8HTBBNBNCLXBkg7+g+YpeiGJm564=',
+ 'hashes the data, encodes input'
+ );
+ });
+ click('[data-test-tools-back]');
+ fillIn('[data-test-tools-input="hash-input"]', 'e2RhdGE6ImZvbyJ9');
+
+ click('[data-test-tools-submit]');
+ andThen(() => {
+ assert.equal(
+ find('[data-test-tools-input="sum"]').val(),
+ 'JmSi2Hhbgu2WYOrcOyTqqMdym7KT3sohCwAwaMonVrc=',
+ 'hashes the data, passes b64 input through'
+ );
+ });
+});
diff --git a/ui/tests/acceptance/transit-test.js b/ui/tests/acceptance/transit-test.js
new file mode 100644
index 000000000..e97c9dc22
--- /dev/null
+++ b/ui/tests/acceptance/transit-test.js
@@ -0,0 +1,302 @@
+import { test } from 'qunit';
+import moduleForAcceptance from 'vault/tests/helpers/module-for-acceptance';
+import { encodeString } from 'vault/utils/b64';
+
+moduleForAcceptance('Acceptance | transit', {
+ beforeEach() {
+ return authLogin();
+ },
+ afterEach() {
+ return authLogout();
+ },
+});
+
+let generateTransitKeys = () => {
+ const ts = new Date().getTime();
+ const keys = [
+ {
+ name: `aes-${ts}`,
+ type: 'aes256-gcm96',
+ exportable: true,
+ supportsEncryption: true,
+ },
+ {
+ name: `aes-convergent-${ts}`,
+ type: 'aes256-gcm96',
+ convergent: true,
+ supportsEncryption: true,
+ },
+ {
+ name: `chacha-${ts}`,
+ type: 'chacha20-poly1305',
+ exportable: true,
+ supportsEncryption: true,
+ },
+ {
+ name: `chacha-convergent-${ts}`,
+ type: 'chacha20-poly1305',
+ convergent: true,
+ supportsEncryption: true,
+ },
+ {
+ name: `ecdsa-${ts}`,
+ type: 'ecdsa-p256',
+ exportable: true,
+ supportsSigning: true,
+ },
+ {
+ name: `ed25519-${ts}`,
+ type: 'ed25519',
+ derived: true,
+ supportsSigning: true,
+ },
+ {
+ name: `rsa-2048-${ts}`,
+ type: `rsa-2048`,
+ supportsSigning: true,
+ supportsEncryption: true,
+ },
+ {
+ name: `rsa-4096-${ts}`,
+ type: `rsa-4096`,
+ supportsSigning: true,
+ supportsEncryption: true,
+ },
+ ];
+
+ keys.forEach(key => {
+ click('[data-test-secret-create]');
+ fillIn('[data-test-transit-key-name]', key.name);
+ fillIn('[data-test-transit-key-type]', key.type);
+ if (key.exportable) {
+ click('[data-test-transit-key-exportable]');
+ }
+ if (key.derived) {
+ click('[data-test-transit-key-derived]');
+ }
+ if (key.convergent) {
+ click('[data-test-transit-key-convergent-encryption]');
+ }
+ click('[data-test-transit-key-create]');
+
+ // link back to the list
+ click('[data-test-secret-root-link]');
+ });
+ return keys;
+};
+
+const testEncryption = (assert, keyName) => {
+ const tests = [
+ // raw bytes for plaintext and context
+ {
+ plaintext: 'NaXud2QW7KjyK6Me9ggh+zmnCeBGdG93LQED49PtoOI=',
+ context: 'nqR8LiVgNh/lwO2rArJJE9F9DMhh0lKo4JX9DAAkCDw=',
+ encodePlaintext: false,
+ encodeContext: false,
+ decodeAfterDecrypt: false,
+ assertAfterEncrypt: key => {
+ assert.ok(
+ /vault:/.test(find('[data-test-transit-input="ciphertext"]').val()),
+ `${key}: ciphertext shows a vault-prefixed ciphertext`
+ );
+ },
+ assertBeforeDecrypt: key => {
+ assert.equal(
+ find('[data-test-transit-input="context"]').val(),
+ 'nqR8LiVgNh/lwO2rArJJE9F9DMhh0lKo4JX9DAAkCDw=',
+ `${key}: the ui shows the base64-encoded context`
+ );
+ },
+
+ assertAfterDecrypt: key => {
+ assert.equal(
+ find('[data-test-transit-input="plaintext"]').val(),
+ 'NaXud2QW7KjyK6Me9ggh+zmnCeBGdG93LQED49PtoOI=',
+ `${key}: the ui shows the base64-encoded plaintext`
+ );
+ },
+ },
+ // raw bytes for plaintext, string for context
+ {
+ plaintext: 'NaXud2QW7KjyK6Me9ggh+zmnCeBGdG93LQED49PtoOI=',
+ context: encodeString('context'),
+ encodePlaintext: false,
+ encodeContext: false,
+ decodeAfterDecrypt: false,
+ assertAfterEncrypt: key => {
+ assert.ok(
+ /vault:/.test(find('[data-test-transit-input="ciphertext"]').val()),
+ `${key}: ciphertext shows a vault-prefixed ciphertext`
+ );
+ },
+ assertBeforeDecrypt: key => {
+ assert.equal(
+ find('[data-test-transit-input="context"]').val(),
+ encodeString('context'),
+ `${key}: the ui shows the input context`
+ );
+ },
+ assertAfterDecrypt: key => {
+ assert.equal(
+ find('[data-test-transit-input="plaintext"]').val(),
+ 'NaXud2QW7KjyK6Me9ggh+zmnCeBGdG93LQED49PtoOI=',
+ `${key}: the ui shows the base64-encoded plaintext`
+ );
+ },
+ },
+ // base64 input
+ {
+ plaintext: encodeString('This is the secret'),
+ context: encodeString('context'),
+ encodePlaintext: false,
+ encodeContext: false,
+ decodeAfterDecrypt: true,
+ assertAfterEncrypt: key => {
+ assert.ok(
+ /vault:/.test(find('[data-test-transit-input="ciphertext"]').val()),
+ `${key}: ciphertext shows a vault-prefixed ciphertext`
+ );
+ },
+ assertBeforeDecrypt: key => {
+ assert.equal(
+ find('[data-test-transit-input="context"]').val(),
+ encodeString('context'),
+ `${key}: the ui shows the input context`
+ );
+ },
+ assertAfterDecrypt: key => {
+ assert.equal(
+ find('[data-test-transit-input="plaintext"]').val(),
+ 'This is the secret',
+ `${key}: the ui decodes plaintext`
+ );
+ },
+ },
+
+ // string input
+ {
+ plaintext: 'There are many secrets 🤐',
+ context: 'secret 2',
+ encodePlaintext: true,
+ encodeContext: true,
+ decodeAfterDecrypt: true,
+ assertAfterEncrypt: key => {
+ assert.ok(findWithAssert('[data-test-transit-input="ciphertext"]'), `${key}: ciphertext box shows`);
+ assert.ok(
+ /vault:/.test(find('[data-test-transit-input="ciphertext"]').val()),
+ `${key}: ciphertext shows a vault-prefixed ciphertext`
+ );
+ },
+ assertBeforeDecrypt: key => {
+ assert.equal(
+ find('[data-test-transit-input="context"]').val(),
+ encodeString('secret 2'),
+ `${key}: the ui shows the encoded context`
+ );
+ },
+ assertAfterDecrypt: key => {
+ assert.ok(findWithAssert('[data-test-transit-input="plaintext"]'), `${key}: plaintext box shows`);
+ assert.equal(
+ find('[data-test-transit-input="plaintext"]').val(),
+ 'There are many secrets 🤐',
+ `${key}: the ui decodes plaintext`
+ );
+ },
+ },
+ ];
+
+ tests.forEach(testCase => {
+ click('[data-test-transit-action-link="encrypt"]');
+ fillIn('[data-test-transit-input="plaintext"]', testCase.plaintext);
+ fillIn('[data-test-transit-input="context"]', testCase.context);
+ if (testCase.encodePlaintext) {
+ click('[data-test-transit-b64-toggle="plaintext"]');
+ }
+ if (testCase.encodeContext) {
+ click('[data-test-transit-b64-toggle="context"]');
+ }
+ click('button:contains(Encrypt)');
+ if (testCase.assertAfterEncrypt) {
+ andThen(() => testCase.assertAfterEncrypt(keyName));
+ }
+ click('[data-test-transit-action-link="decrypt"]');
+ if (testCase.assertBeforeDecrypt) {
+ andThen(() => testCase.assertBeforeDecrypt(keyName));
+ }
+ click('button:contains(Decrypt)');
+
+ if (testCase.assertAfterDecrypt) {
+ andThen(() => {
+ if (testCase.decodeAfterDecrypt) {
+ click('[data-test-transit-b64-toggle="plaintext"]');
+ andThen(() => testCase.assertAfterDecrypt(keyName));
+ } else {
+ testCase.assertAfterDecrypt(keyName);
+ }
+ });
+ }
+ });
+};
+test('transit backend', function(assert) {
+ assert.expect(50);
+ const now = new Date().getTime();
+ const transitPath = `transit-${now}`;
+
+ mountSupportedSecretBackend(assert, 'transit', transitPath);
+
+ // create a bunch of different kinds of keys
+ const transitKeys = generateTransitKeys();
+
+ transitKeys.forEach((key, index) => {
+ click(`[data-test-secret-link="${key.name}"]`);
+ if (index === 0) {
+ click('[data-test-transit-link="versions"]');
+ andThen(() => {
+ assert.equal(
+ find('[data-test-transit-key-version-row]').length,
+ 1,
+ `${key.name}: only one key version`
+ );
+ });
+ click('[data-test-transit-key-rotate] button');
+ click('[data-test-confirm-button]');
+ andThen(() => {
+ assert.equal(
+ find('[data-test-transit-key-version-row]').length,
+ 2,
+ `${key.name}: two key versions after rotate`
+ );
+ });
+ }
+ click('[data-test-transit-key-actions-link]');
+ andThen(() => {
+ assert.equal(
+ currentURL(),
+ `/vault/secrets/${transitPath}/actions/${key.name}`,
+ `${key.name}: navigates to tranist actions`
+ );
+ if (index === 0) {
+ assert.ok(
+ findWithAssert('[data-test-transit-key-version-select]'),
+ `${key.name}: the rotated key allows you to select versions`
+ );
+ }
+ if (key.exportable) {
+ assert.ok(
+ findWithAssert('[data-test-transit-action-link="export"]'),
+ `${key.name}: exportable key has a link to export action`
+ );
+ } else {
+ assert.equal(
+ find('[data-test-transit-action-link="export"]').length,
+ 0,
+ `${key.name}: non-exportable key does not link to export action`
+ );
+ }
+ if (key.convergent && key.supportsEncryption) {
+ testEncryption(assert, key.name);
+ }
+ });
+ click('[data-test-secret-root-link]');
+ });
+});
diff --git a/ui/tests/acceptance/unseal-test.js b/ui/tests/acceptance/unseal-test.js
new file mode 100644
index 000000000..d5a3c0600
--- /dev/null
+++ b/ui/tests/acceptance/unseal-test.js
@@ -0,0 +1,45 @@
+import { test } from 'qunit';
+import moduleForAcceptance from 'vault/tests/helpers/module-for-acceptance';
+import VAULT_KEYS from 'vault/tests/helpers/vault-keys';
+
+const { unseal } = VAULT_KEYS;
+
+moduleForAcceptance('Acceptance | unseal', {
+ beforeEach() {
+ return authLogin();
+ },
+ afterEach() {
+ return authLogout();
+ },
+});
+
+test('seal then unseal', function(assert) {
+ visit('/vault/settings/seal');
+ andThen(function() {
+ assert.equal(currentURL(), '/vault/settings/seal');
+ });
+
+ // seal
+ click('[data-test-seal] button');
+ click('[data-test-confirm-button]');
+ andThen(() => {
+ pollCluster();
+ });
+ andThen(function() {
+ assert.equal(currentURL(), '/vault/unseal', 'vault is on the unseal page');
+ });
+
+ // unseal
+ fillIn('[data-test-shamir-input]', unseal);
+ click('button[type="submit"]');
+ andThen(() => {
+ pollCluster();
+ });
+ andThen(() => {
+ assert.ok(
+ find('[data-test-cluster-status]').text().includes('is unsealed'),
+ 'ui indicates the vault is unsealed'
+ );
+ assert.ok(currentURL().match(/\/vault\/auth/), 'vault is ready to authenticate');
+ });
+});
diff --git a/ui/tests/helpers/auth-login.js b/ui/tests/helpers/auth-login.js
new file mode 100644
index 000000000..d42a8c55c
--- /dev/null
+++ b/ui/tests/helpers/auth-login.js
@@ -0,0 +1,11 @@
+import Ember from 'ember';
+
+export default Ember.Test.registerAsyncHelper('authLogin', function() {
+ visit('/vault/auth?with=token');
+ fillIn('[data-test-token]', 'root');
+ click('[data-test-auth-submit]');
+ // get rid of the root warning flash
+ if (find('[data-test-flash-message-body]').length) {
+ click('[data-test-flash-message-body]');
+ }
+});
diff --git a/ui/tests/helpers/auth-logout.js b/ui/tests/helpers/auth-logout.js
new file mode 100644
index 000000000..b44b7b450
--- /dev/null
+++ b/ui/tests/helpers/auth-logout.js
@@ -0,0 +1,5 @@
+import Ember from 'ember';
+
+export default Ember.Test.registerAsyncHelper('authLogout', function() {
+ visit('/vault/logout');
+});
diff --git a/ui/tests/helpers/destroy-app.js b/ui/tests/helpers/destroy-app.js
new file mode 100644
index 000000000..2541c6182
--- /dev/null
+++ b/ui/tests/helpers/destroy-app.js
@@ -0,0 +1,6 @@
+import Ember from 'ember';
+
+export default function destroyApp(application) {
+ Ember.run(application, 'destroy');
+ //server.shutdown();
+}
diff --git a/ui/tests/helpers/flash-message.js b/ui/tests/helpers/flash-message.js
new file mode 100644
index 000000000..56646e876
--- /dev/null
+++ b/ui/tests/helpers/flash-message.js
@@ -0,0 +1,3 @@
+import FlashObject from 'ember-cli-flash/flash/object';
+
+FlashObject.reopen({ init() {} });
diff --git a/ui/tests/helpers/module-for-acceptance.js b/ui/tests/helpers/module-for-acceptance.js
new file mode 100644
index 000000000..135329425
--- /dev/null
+++ b/ui/tests/helpers/module-for-acceptance.js
@@ -0,0 +1,23 @@
+import { module } from 'qunit';
+import Ember from 'ember';
+import startApp from '../helpers/start-app';
+import destroyApp from '../helpers/destroy-app';
+
+const { RSVP: { resolve } } = Ember;
+
+export default function(name, options = {}) {
+ module(name, {
+ beforeEach() {
+ this.application = startApp();
+
+ if (options.beforeEach) {
+ return options.beforeEach.apply(this, arguments);
+ }
+ },
+
+ afterEach() {
+ let afterEach = options.afterEach && options.afterEach.apply(this, arguments);
+ return resolve(afterEach).then(() => destroyApp(this.application));
+ },
+ });
+}
diff --git a/ui/tests/helpers/mount-secret-backend.js b/ui/tests/helpers/mount-secret-backend.js
new file mode 100644
index 000000000..84661ff87
--- /dev/null
+++ b/ui/tests/helpers/mount-secret-backend.js
@@ -0,0 +1,20 @@
+import Ember from 'ember';
+
+export default Ember.Test.registerAsyncHelper('mountSupportedSecretBackend', function(_, assert, type, path) {
+ visit('/vault/settings/mount-secret-backend');
+ andThen(function() {
+ assert.equal(currentURL(), '/vault/settings/mount-secret-backend');
+ });
+
+ fillIn('[data-test-secret-backend-type]', type);
+ fillIn('[data-test-secret-backend-path]', path);
+ click('[data-test-secret-backend-submit]');
+ return andThen(() => {
+ assert.equal(currentURL(), `/vault/secrets/${path}/list`, `redirects to ${path} index`);
+ assert.ok(
+ find('[data-test-flash-message]').text().trim(),
+ `Successfully mounted '${type}' at '${path}'!`
+ );
+ click('[data-test-flash-message]');
+ });
+});
diff --git a/ui/tests/helpers/poll-cluster.js b/ui/tests/helpers/poll-cluster.js
new file mode 100644
index 000000000..6d38ef4bf
--- /dev/null
+++ b/ui/tests/helpers/poll-cluster.js
@@ -0,0 +1,8 @@
+import Ember from 'ember';
+
+export default Ember.Test.registerAsyncHelper('pollCluster', function(app) {
+ const clusterRoute = app.__container__.cache['route:vault/cluster'];
+ return Ember.run(() => {
+ return clusterRoute.controller.model.reload();
+ });
+});
diff --git a/ui/tests/helpers/resolver.js b/ui/tests/helpers/resolver.js
new file mode 100644
index 000000000..319b45fc1
--- /dev/null
+++ b/ui/tests/helpers/resolver.js
@@ -0,0 +1,11 @@
+import Resolver from '../../resolver';
+import config from '../../config/environment';
+
+const resolver = Resolver.create();
+
+resolver.namespace = {
+ modulePrefix: config.modulePrefix,
+ podModulePrefix: config.podModulePrefix,
+};
+
+export default resolver;
diff --git a/ui/tests/helpers/start-app.js b/ui/tests/helpers/start-app.js
new file mode 100644
index 000000000..c64e9b273
--- /dev/null
+++ b/ui/tests/helpers/start-app.js
@@ -0,0 +1,23 @@
+import Ember from 'ember';
+import Application from '../../app';
+import config from '../../config/environment';
+
+import './auth-login';
+import './auth-logout';
+import './mount-secret-backend';
+import './poll-cluster';
+
+import registerClipboardHelpers from '../helpers/ember-cli-clipboard';
+registerClipboardHelpers();
+
+export default function startApp(attrs) {
+ let attributes = Ember.merge({}, config.APP);
+ attributes = Ember.merge(attributes, attrs); // use defaults, but you can override;
+
+ return Ember.run(() => {
+ let application = Application.create(attributes);
+ application.setupForTesting();
+ application.injectTestHelpers();
+ return application;
+ });
+}
diff --git a/ui/tests/index.html b/ui/tests/index.html
new file mode 100644
index 000000000..b3b2eb4dc
--- /dev/null
+++ b/ui/tests/index.html
@@ -0,0 +1,33 @@
+
+
+
+
+
+ Vault Tests
+
+
+
+ {{content-for "head"}}
+ {{content-for "test-head"}}
+
+
+
+
+
+ {{content-for "head-footer"}}
+ {{content-for "test-head-footer"}}
+
+
+ {{content-for "body"}}
+ {{content-for "test-body"}}
+
+
+
+
+
+
+
+ {{content-for "body-footer"}}
+ {{content-for "test-body-footer"}}
+
+
diff --git a/ui/tests/integration/.gitkeep b/ui/tests/integration/.gitkeep
new file mode 100644
index 000000000..e69de29bb
diff --git a/ui/tests/integration/components/b64-toggle-test.js b/ui/tests/integration/components/b64-toggle-test.js
new file mode 100644
index 000000000..32defe1da
--- /dev/null
+++ b/ui/tests/integration/components/b64-toggle-test.js
@@ -0,0 +1,45 @@
+import { moduleForComponent, test } from 'ember-qunit';
+import hbs from 'htmlbars-inline-precompile';
+
+moduleForComponent('b64-toggle', 'Integration | Component | b64 toggle', {
+ integration: true,
+});
+
+test('it renders', function(assert) {
+ this.render(hbs`{{b64-toggle}}`);
+ assert.equal(this.$('button').length, 1);
+});
+
+test('it toggles encoding on the passed string', function(assert) {
+ this.set('value', 'value');
+ this.render(hbs`{{b64-toggle value=value}}`);
+ this.$('button').click();
+ assert.equal(this.get('value'), btoa('value'), 'encodes to base64');
+ this.$('button').click();
+ assert.equal(this.get('value'), 'value', 'decodes from base64');
+});
+
+test('it toggles encoding starting with base64', function(assert) {
+ this.set('value', btoa('value'));
+ this.render(hbs`{{b64-toggle value=value initialEncoding='base64'}}`);
+ assert.ok(this.$('button').text().includes('Decode'), 'renders as on when in b64 mode');
+ this.$('button').click();
+ assert.equal(this.get('value'), 'value', 'decodes from base64');
+});
+
+test('it detects changes to value after encoding', function(assert) {
+ this.set('value', btoa('value'));
+ this.render(hbs`{{b64-toggle value=value initialEncoding='base64'}}`);
+ assert.ok(this.$('button').text().includes('Decode'), 'renders as on when in b64 mode');
+ this.set('value', btoa('value') + '=');
+ assert.ok(this.$('button').text().includes('Encode'), 'toggles off since value has changed');
+ this.set('value', btoa('value'));
+ assert.ok(this.$('button').text().includes('Decode'), 'toggles on since value is equal to the original');
+});
+
+test('it does not toggle when the value is empty', function(assert) {
+ this.set('value', '');
+ this.render(hbs`{{b64-toggle value=value}}`);
+ this.$('button').click();
+ assert.ok(this.$('button').text().includes('Encode'));
+});
diff --git a/ui/tests/integration/components/config-pki-ca-test.js b/ui/tests/integration/components/config-pki-ca-test.js
new file mode 100644
index 000000000..a4b6b46fe
--- /dev/null
+++ b/ui/tests/integration/components/config-pki-ca-test.js
@@ -0,0 +1,75 @@
+import Ember from 'ember';
+import { moduleForComponent, test } from 'ember-qunit';
+import hbs from 'htmlbars-inline-precompile';
+import { create } from 'ember-cli-page-object';
+import configPki from '../../pages/components/config-pki-ca';
+
+const component = create(configPki);
+
+const storeStub = Ember.Service.extend({
+ createRecord(type, args) {
+ return Ember.Object.create(args, {
+ save() {
+ return Ember.RSVP.resolve(this);
+ },
+ destroyRecord() {},
+ send() {},
+ unloadRecord() {},
+ });
+ },
+});
+
+moduleForComponent('config-pki-ca', 'Integration | Component | config pki ca', {
+ integration: true,
+ beforeEach() {
+ component.setContext(this);
+ Ember.getOwner(this).lookup('service:flash-messages').registerTypes(['success']);
+ this.register('service:store', storeStub);
+ this.inject.service('store', { as: 'storeService' });
+ },
+
+ afterEach() {
+ component.removeContext();
+ },
+});
+
+const config = function(pem) {
+ return Ember.Object.create({
+ pem: pem,
+ backend: 'pki',
+ caChain: 'caChain',
+ der: new File(['der'], { type: 'text/plain' }),
+ });
+};
+
+const setupAndRender = function(context, onRefresh) {
+ const refreshFn = onRefresh || function() {};
+ context.set('config', config());
+ context.set('onRefresh', refreshFn);
+ context.render(hbs`{{config-pki-ca onRefresh=onRefresh config=config}}`);
+};
+
+test('it renders, no pem', function(assert) {
+ setupAndRender(this);
+
+ assert.notOk(component.hasTitle, 'no title in the default state');
+ assert.equal(component.replaceCAText, 'Configure CA');
+ assert.equal(component.downloadLinks().count, 0, 'there are no download links');
+
+ component.replaceCA();
+ assert.equal(component.title, 'Configure CA Certificate');
+ component.back();
+
+ component.setSignedIntermediateBtn();
+ assert.equal(component.title, 'Set signed intermediate');
+ component.back();
+});
+
+test('it renders, with pem', function(assert) {
+ const c = config('pem');
+ this.set('config', c);
+ this.render(hbs`{{config-pki-ca config=config}}`);
+ assert.notOk(component.hasTitle, 'no title in the default state');
+ assert.equal(component.replaceCAText, 'Replace CA');
+ assert.equal(component.downloadLinks().count, 3, 'shows download links');
+});
diff --git a/ui/tests/integration/components/config-pki-test.js b/ui/tests/integration/components/config-pki-test.js
new file mode 100644
index 000000000..5aaff453c
--- /dev/null
+++ b/ui/tests/integration/components/config-pki-test.js
@@ -0,0 +1,103 @@
+import Ember from 'ember';
+import { moduleForComponent, test } from 'ember-qunit';
+import hbs from 'htmlbars-inline-precompile';
+import { create } from 'ember-cli-page-object';
+import configPki from '../../pages/components/config-pki';
+
+const component = create(configPki);
+
+moduleForComponent('config-pki', 'Integration | Component | config pki', {
+ integration: true,
+ beforeEach() {
+ component.setContext(this);
+ Ember.getOwner(this).lookup('service:flash-messages').registerTypes(['success']);
+ },
+
+ afterEach() {
+ component.removeContext();
+ },
+});
+
+const config = function(saveFn) {
+ return {
+ save: saveFn,
+ rollbackAttributes: () => {},
+ tidyAttrs: [
+ {
+ type: 'boolean',
+ name: 'tidyCertStore',
+ },
+ {
+ type: 'string',
+ name: 'anotherAttr',
+ },
+ ],
+ crlAttrs: [
+ {
+ type: 'string',
+ name: 'crl',
+ },
+ ],
+ urlsAttrs: [
+ {
+ type: 'string',
+ name: 'urls',
+ },
+ ],
+ };
+};
+
+const setupAndRender = function(context, section = 'tidy') {
+ context.set('config', config());
+ context.set('section', section);
+ context.render(hbs`{{config-pki section=section config=config}}`);
+};
+
+test('it renders tidy section', function(assert) {
+ setupAndRender(this);
+ assert.ok(component.text.startsWith('You can tidy up the backend'));
+ assert.notOk(component.hasTitle, 'No title for tidy section');
+ assert.equal(component.fields().count, 2);
+ assert.ok(component.fields(0).labelText, 'Tidy cert store');
+ assert.ok(component.fields(1).labelText, 'Another attr');
+});
+
+test('it renders crl section', function(assert) {
+ setupAndRender(this, 'crl');
+ assert.ok(component.hasTitle, 'renders the title');
+ assert.equal(component.title, 'Certificate Revocation List (CRL) Config');
+ assert.ok(component.text.startsWith('Set the duration for which the generated CRL'));
+ assert.equal(component.fields().count, 1);
+ assert.ok(component.fields(0).labelText, 'Crl');
+});
+
+test('it renders urls section', function(assert) {
+ setupAndRender(this, 'urls');
+ assert.notOk(component.hasTitle, 'No title for urls section');
+ assert.equal(component.fields().count, 1);
+ assert.ok(component.fields(0).labelText, 'urls');
+});
+
+test('it calls save with the correct arguments', function(assert) {
+ assert.expect(3);
+ const section = 'tidy';
+ this.set('onRefresh', () => {
+ assert.ok(true, 'refresh called');
+ });
+ this.set(
+ 'config',
+ config(options => {
+ assert.equal(options.adapterOptions.method, section, 'method passed to save');
+ assert.deepEqual(
+ options.adapterOptions.fields,
+ ['tidyCertStore', 'anotherAttr'],
+ 'fields passed to save'
+ );
+ return Ember.RSVP.resolve();
+ })
+ );
+ this.set('section', section);
+ this.render(hbs`{{config-pki section=section config=config onRefresh=onRefresh}}`);
+
+ component.submit();
+});
diff --git a/ui/tests/integration/components/edition-badge-test.js b/ui/tests/integration/components/edition-badge-test.js
new file mode 100644
index 000000000..988039b90
--- /dev/null
+++ b/ui/tests/integration/components/edition-badge-test.js
@@ -0,0 +1,20 @@
+import { moduleForComponent, test } from 'ember-qunit';
+import hbs from 'htmlbars-inline-precompile';
+
+moduleForComponent('edition-badge', 'Integration | Component | edition badge', {
+ integration: true,
+});
+
+test('it renders', function(assert) {
+ this.render(hbs`
+ {{edition-badge edition="Custom"}}
+ `);
+
+ assert.equal(this.$('.edition-badge').text().trim(), 'Custom', 'contains edition');
+
+ this.render(hbs`
+ {{edition-badge edition="Enterprise"}}
+ `);
+
+ assert.equal(this.$('.edition-badge').text().trim(), 'Ent', 'abbreviates Enterprise');
+});
diff --git a/ui/tests/integration/components/form-field-test.js b/ui/tests/integration/components/form-field-test.js
new file mode 100644
index 000000000..62e91227e
--- /dev/null
+++ b/ui/tests/integration/components/form-field-test.js
@@ -0,0 +1,135 @@
+import { moduleForComponent, test } from 'ember-qunit';
+import hbs from 'htmlbars-inline-precompile';
+import { create } from 'ember-cli-page-object';
+import Ember from 'ember';
+import sinon from 'sinon';
+import formFields from '../../pages/components/form-field';
+
+const component = create(formFields);
+
+moduleForComponent('form-field', 'Integration | Component | form field', {
+ integration: true,
+ beforeEach() {
+ component.setContext(this);
+ },
+
+ afterEach() {
+ component.removeContext();
+ },
+});
+
+const createAttr = (name, type, options) => {
+ return {
+ name,
+ type,
+ options,
+ };
+};
+
+const setup = function(attr) {
+ let model = Ember.Object.create({});
+ let spy = sinon.spy();
+ this.set('onChange', spy);
+ this.set('model', model);
+ this.set('attr', attr);
+ this.render(hbs`{{form-field attr=attr model=model onChange=onChange}}`);
+ return [model, spy];
+};
+
+test('it renders', function(assert) {
+ let model = Ember.Object.create({});
+ this.set('attr', { name: 'foo' });
+ this.set('model', model);
+ this.render(hbs`{{form-field attr=attr model=model}}`);
+
+ assert.equal(component.field.labelText, 'Foo', 'renders a label');
+ assert.notOk(component.hasInput, 'renders only the label');
+});
+
+test('it renders: string', function(assert) {
+ let [model, spy] = setup.call(this, createAttr('foo', 'string', { defaultValue: 'default' }));
+ assert.equal(component.field.labelText, 'Foo', 'renders a label');
+ assert.equal(component.field.inputValue, 'default', 'renders default value');
+ assert.ok(component.hasInput, 'renders input for string');
+ component.field.input('bar').change();
+
+ assert.equal(model.get('foo'), 'bar');
+ assert.ok(spy.calledWith('foo', 'bar'), 'onChange called with correct args');
+});
+
+test('it renders: boolean', function(assert) {
+ let [model, spy] = setup.call(this, createAttr('foo', 'boolean', { defaultValue: false }));
+ assert.equal(component.field.labelText, 'Foo', 'renders a label');
+ assert.notOk(component.field.inputChecked, 'renders default value');
+ assert.ok(component.hasCheckbox, 'renders a checkbox for boolean');
+ component.field.clickLabel();
+
+ assert.equal(model.get('foo'), true);
+ assert.ok(spy.calledWith('foo', true), 'onChange called with correct args');
+});
+
+test('it renders: number', function(assert) {
+ let [model, spy] = setup.call(this, createAttr('foo', 'number', { defaultValue: 5 }));
+ assert.equal(component.field.labelText, 'Foo', 'renders a label');
+ assert.equal(component.field.inputValue, 5, 'renders default value');
+ assert.ok(component.hasInput, 'renders input for number');
+ component.field.input(8).change();
+
+ assert.equal(model.get('foo'), 8);
+ assert.ok(spy.calledWith('foo', '8'), 'onChange called with correct args');
+});
+
+test('it renders: object', function(assert) {
+ setup.call(this, createAttr('foo', 'object'));
+ assert.equal(component.field.labelText, 'Foo', 'renders a label');
+ assert.ok(component.hasJSONEditor, 'renders the json editor');
+});
+
+test('it renders: editType textarea', function(assert) {
+ let [model, spy] = setup.call(
+ this,
+ createAttr('foo', 'string', { defaultValue: 'goodbye', editType: 'textarea' })
+ );
+ assert.equal(component.field.labelText, 'Foo', 'renders a label');
+ assert.ok(component.hasTextarea, 'renders a textarea');
+ assert.equal(component.field.textareaValue, 'goodbye', 'renders default value');
+ component.field.textarea('hello');
+
+ assert.equal(model.get('foo'), 'hello');
+ assert.ok(spy.calledWith('foo', 'hello'), 'onChange called with correct args');
+});
+
+test('it renders: editType file', function(assert) {
+ setup.call(this, createAttr('foo', 'string', { editType: 'file' }));
+ assert.ok(component.hasTextFile, 'renders the text-file component');
+});
+
+test('it renders: editType ttl', function(assert) {
+ let [model, spy] = setup.call(this, createAttr('foo', null, { editType: 'ttl' }));
+ assert.ok(component.hasTTLPicker, 'renders the ttl-picker component');
+ component.field.input('3');
+ component.field.select('h').change();
+
+ assert.equal(model.get('foo'), '3h');
+ assert.ok(spy.calledWith('foo', '3h'), 'onChange called with correct args');
+});
+
+test('it renders: editType stringArray', function(assert) {
+ let [model, spy] = setup.call(this, createAttr('foo', 'string', { editType: 'stringArray' }));
+ assert.ok(component.hasStringList, 'renders the string-list component');
+
+ component.field.input('array').change();
+ assert.deepEqual(model.get('foo'), ['array'], 'sets the value on the model');
+ assert.deepEqual(spy.args[0], ['foo', ['array']], 'onChange called with correct args');
+});
+
+test('it uses a passed label', function(assert) {
+ setup.call(this, createAttr('foo', 'string', { label: 'Not Foo' }));
+ assert.equal(component.field.labelText, 'Not Foo', 'renders the label from options');
+});
+
+test('it renders a help tooltip', function(assert) {
+ setup.call(this, createAttr('foo', 'string', { helpText: 'Here is some help text' }));
+ assert.ok(component.hasTooltip, 'renders the tooltip component');
+ component.tooltipTrigger();
+});
diff --git a/ui/tests/integration/components/kv-object-editor-test.js b/ui/tests/integration/components/kv-object-editor-test.js
new file mode 100644
index 000000000..4340c1bcc
--- /dev/null
+++ b/ui/tests/integration/components/kv-object-editor-test.js
@@ -0,0 +1,91 @@
+import { moduleForComponent, test } from 'ember-qunit';
+import wait from 'ember-test-helpers/wait';
+import hbs from 'htmlbars-inline-precompile';
+
+import { create } from 'ember-cli-page-object';
+import kvObjectEditor from '../../pages/components/kv-object-editor';
+
+import sinon from 'sinon';
+const component = create(kvObjectEditor);
+
+moduleForComponent('kv-object-editor', 'Integration | Component | kv object editor', {
+ integration: true,
+ beforeEach() {
+ component.setContext(this);
+ },
+
+ afterEach() {
+ component.removeContext();
+ },
+});
+
+test('it renders with no initial value', function(assert) {
+ let spy = sinon.spy();
+ this.set('onChange', spy);
+ this.render(hbs`{{kv-object-editor onChange=onChange}}`);
+ assert.equal(component.rows().count, 1, 'renders a single row');
+ component.addRow();
+ return wait().then(() => {
+ assert.equal(component.rows().count, 1, 'will only render row with a blank key');
+ });
+});
+
+test('it calls onChange when the val changes', function(assert) {
+ let spy = sinon.spy();
+ this.set('onChange', spy);
+ this.render(hbs`{{kv-object-editor onChange=onChange}}`);
+ component.rows(0).kvKey('foo').kvVal('bar');
+ wait().then(() => {
+ assert.equal(spy.callCount, 2, 'calls onChange each time change is triggered');
+ assert.deepEqual(
+ spy.lastCall.args[0],
+ { foo: 'bar' },
+ 'calls onChange with the JSON respresentation of the data'
+ );
+ });
+ component.addRow();
+ return wait().then(() => {
+ assert.equal(component.rows().count, 2, 'adds a row when there is no blank one');
+ });
+});
+
+test('it renders passed data', function(assert) {
+ let metadata = { foo: 'bar', baz: 'bop' };
+ this.set('value', metadata);
+ this.render(hbs`{{kv-object-editor value=value}}`);
+ assert.equal(
+ component.rows().count,
+ Object.keys(metadata).length + 1,
+ 'renders both rows of the metadata, plus an empty one'
+ );
+});
+
+test('it deletes a row', function(assert) {
+ let spy = sinon.spy();
+ this.set('onChange', spy);
+ this.render(hbs`{{kv-object-editor onChange=onChange}}`);
+ component.rows(0).kvKey('foo').kvVal('bar');
+ component.addRow();
+ wait().then(() => {
+ assert.equal(component.rows().count, 2);
+ assert.equal(spy.callCount, 2, 'calls onChange for editing');
+ component.rows(0).deleteRow();
+ });
+
+ return wait().then(() => {
+ assert.equal(component.rows().count, 1, 'only the blank row left');
+ assert.equal(spy.callCount, 3, 'calls onChange deleting row');
+ assert.deepEqual(spy.lastCall.args[0], {}, 'last call to onChange is an empty object');
+ });
+});
+
+test('it shows a warning if there are duplicate keys', function(assert) {
+ let metadata = { foo: 'bar', baz: 'bop' };
+ this.set('value', metadata);
+ this.render(hbs`{{kv-object-editor value=value}}`);
+ component.rows(0).kvKey('foo');
+
+ return wait().then(() => {
+ assert.ok(component.showsDuplicateError, 'duplicate keys are allowed but an error message is shown');
+ });
+});
diff --git a/ui/tests/integration/components/mount-backend-form-test.js b/ui/tests/integration/components/mount-backend-form-test.js
new file mode 100644
index 000000000..90b047b0f
--- /dev/null
+++ b/ui/tests/integration/components/mount-backend-form-test.js
@@ -0,0 +1,77 @@
+import Ember from 'ember';
+import { moduleForComponent, test } from 'ember-qunit';
+import wait from 'ember-test-helpers/wait';
+import hbs from 'htmlbars-inline-precompile';
+
+import { create } from 'ember-cli-page-object';
+import mountBackendForm from '../../pages/components/mount-backend-form';
+
+import { startMirage } from 'vault/initializers/ember-cli-mirage';
+import sinon from 'sinon';
+
+const component = create(mountBackendForm);
+
+moduleForComponent('mount-backend-form', 'Integration | Component | mount backend form', {
+ integration: true,
+ beforeEach() {
+ component.setContext(this);
+ Ember.getOwner(this).lookup('service:flash-messages').registerTypes(['success', 'danger']);
+ this.server = startMirage();
+ },
+
+ afterEach() {
+ component.removeContext();
+ this.server.shutdown();
+ },
+});
+
+test('it renders', function(assert) {
+ this.render(hbs`{{mount-backend-form}}`);
+ assert.equal(component.header, 'Enable an authentication method', 'renders auth header in default state');
+ assert.equal(component.fields().count, 2, 'renders 2 fields');
+});
+
+test('it changes path when type is changed', function(assert) {
+ this.render(hbs`{{mount-backend-form}}`);
+ assert.equal(component.pathValue, 'approle', 'defaults to approle (first in the list)');
+ component.type('aws');
+ assert.equal(component.pathValue, 'aws', 'updates to the value of the type');
+});
+
+test('it keeps path value if the user has changed it', function(assert) {
+ this.render(hbs`{{mount-backend-form}}`);
+ assert.equal(component.pathValue, 'approle', 'defaults to approle (first in the list)');
+ component.path('newpath');
+ component.type('aws');
+ assert.equal(component.pathValue, 'newpath', 'updates to the value of the type');
+});
+
+test('it calls mount success', function(assert) {
+ const spy = sinon.spy();
+ this.set('onMountSuccess', spy);
+ this.render(hbs`{{mount-backend-form onMountSuccess=onMountSuccess}}`);
+
+ component.path('foo').type('approle').submit();
+ return wait().then(() => {
+ assert.equal(this.server.db.authMethods.length, 1, 'it enables an auth method');
+ assert.ok(spy.calledOnce, 'calls the passed success method');
+ });
+});
+
+test('it calls mount mount config error', function(assert) {
+ const spy = sinon.spy();
+ const spy2 = sinon.spy();
+ this.set('onMountSuccess', spy);
+ this.set('onConfigError', spy2);
+ this.render(hbs`{{mount-backend-form onMountSuccess=onMountSuccess onConfigError=onConfigError}}`);
+
+ component.path('bar').type('kubernetes');
+ // kubernetes requires a host + a cert / pem, so only filling the host will error
+ component.fields().fillIn('kubernetesHost', 'host');
+ component.submit();
+ return wait().then(() => {
+ assert.equal(this.server.db.authMethods.length, 1, 'it still enables an auth method');
+ assert.equal(spy.callCount, 0, 'does not call the success method');
+ assert.ok(spy2.calledOnce, 'calls the passed error method');
+ });
+});
diff --git a/ui/tests/integration/components/mount-filter-config-list-test.js b/ui/tests/integration/components/mount-filter-config-list-test.js
new file mode 100644
index 000000000..0b48b10aa
--- /dev/null
+++ b/ui/tests/integration/components/mount-filter-config-list-test.js
@@ -0,0 +1,33 @@
+import { moduleForComponent, test } from 'ember-qunit';
+import hbs from 'htmlbars-inline-precompile';
+
+moduleForComponent('mount-filter-config-list', 'Integration | Component | mount filter config list', {
+ integration: true,
+});
+
+test('it renders', function(assert) {
+ this.set('config', { mode: 'whitelist', paths: [] });
+ this.set('mounts', [{ path: 'userpass/', type: 'userpass', accessor: 'userpass' }]);
+ this.render(hbs`{{mount-filter-config-list config=config mounts=mounts}}`);
+
+ assert.equal(this.$('#filter-userpass').length, 1);
+});
+
+test('it sets config.paths', function(assert) {
+ this.set('config', { mode: 'whitelist', paths: [] });
+ this.set('mounts', [{ path: 'userpass/', type: 'userpass', accessor: 'userpass' }]);
+ this.render(hbs`{{mount-filter-config-list config=config mounts=mounts}}`);
+
+ this.$('#filter-userpass').click();
+ assert.ok(this.get('config.paths').includes('userpass/'), 'adds to paths');
+
+ this.$('#filter-userpass').click();
+ assert.equal(this.get('config.paths').length, 0, 'removes from paths');
+});
+
+test('it sets config.mode', function(assert) {
+ this.set('config', { mode: 'whitelist', paths: [] });
+ this.render(hbs`{{mount-filter-config-list config=config}}`);
+ this.$('#filter-mode').val('blacklist').change();
+ assert.equal(this.get('config.mode'), 'blacklist');
+});
diff --git a/ui/tests/integration/components/pgp-file-test.js b/ui/tests/integration/components/pgp-file-test.js
new file mode 100644
index 000000000..cffb67eaa
--- /dev/null
+++ b/ui/tests/integration/components/pgp-file-test.js
@@ -0,0 +1,101 @@
+import { moduleForComponent, test } from 'ember-qunit';
+import hbs from 'htmlbars-inline-precompile';
+import Ember from 'ember';
+import wait from 'ember-test-helpers/wait';
+
+let file;
+const fileEvent = () => {
+ const data = { some: 'content' };
+ file = new File([JSON.stringify(data, null, 2)], 'file.json', { type: 'application/json' });
+ return Ember.$.Event('change', {
+ target: {
+ files: [file],
+ },
+ });
+};
+
+moduleForComponent('pgp-file', 'Integration | Component | pgp file', {
+ integration: true,
+ beforeEach: function() {
+ file = null;
+ this.lastOnChangeCall = null;
+ this.set('change', (index, key) => {
+ this.lastOnChangeCall = [index, key];
+ this.set('key', key);
+ });
+ },
+});
+
+test('it renders', function(assert) {
+ this.set('key', { value: '' });
+ this.set('index', 0);
+
+ this.render(hbs`{{pgp-file index=index key=key onChange=(action change)}}`);
+
+ assert.equal(this.$('[data-test-pgp-label]').text().trim(), 'PGP KEY 1');
+ assert.equal(this.$('[data-test-pgp-file-input-label]').text().trim(), 'Choose a file…');
+});
+
+test('it accepts files', function(assert) {
+ const key = { value: '' };
+ const event = fileEvent();
+ this.set('key', key);
+ this.set('index', 0);
+
+ this.render(hbs`{{pgp-file index=index key=key onChange=(action change)}}`);
+ this.$('[data-test-pgp-file-input]').trigger(event);
+
+ return wait().then(() => {
+ // FileReader is async, but then we need extra run loop wait to re-render
+ Ember.run.next(() => {
+ assert.equal(
+ this.$('[data-test-pgp-file-input-label]').text().trim(),
+ file.name,
+ 'the file input shows the file name'
+ );
+ assert.notDeepEqual(this.lastOnChangeCall[1].value, key.value, 'onChange was called with the new key');
+ assert.equal(this.lastOnChangeCall[0], 0, 'onChange is called with the index value');
+ this.$('[data-test-pgp-clear]').click();
+ });
+ return wait().then(() => {
+ assert.equal(this.lastOnChangeCall[1].value, key.value, 'the key gets reset when the input is cleared');
+ });
+ });
+});
+
+test('it allows for text entry', function(assert) {
+ const key = { value: '' };
+ const text = 'a really long pgp key';
+ this.set('key', key);
+ this.set('index', 0);
+
+ this.render(hbs`{{pgp-file index=index key=key onChange=(action change)}}`);
+ this.$('[data-test-text-toggle]').click();
+ assert.equal(this.$('[data-test-pgp-file-textarea]').length, 1, 'renders the textarea on toggle');
+
+ this.$('[data-test-pgp-file-textarea]').text(text).trigger('input');
+ assert.equal(this.lastOnChangeCall[1].value, text, 'the key value is passed to onChange');
+});
+
+test('toggling back and forth', function(assert) {
+ const key = { value: '' };
+ const event = fileEvent();
+ this.set('key', key);
+ this.set('index', 0);
+
+ this.render(hbs`{{pgp-file index=index key=key onChange=(action change)}}`);
+ this.$('[data-test-pgp-file-input]').trigger(event);
+ return wait().then(() => {
+ Ember.run.next(() => {
+ this.$('[data-test-text-toggle]').click();
+ wait().then(() => {
+ assert.equal(this.$('[data-test-pgp-file-textarea]').length, 1, 'renders the textarea on toggle');
+ assert.equal(
+ this.$('[data-test-pgp-file-textarea]').text().trim(),
+ this.lastOnChangeCall[1].value,
+ 'textarea shows the value of the base64d key'
+ );
+ });
+ });
+ });
+});
diff --git a/ui/tests/integration/components/replication-actions-test.js b/ui/tests/integration/components/replication-actions-test.js
new file mode 100644
index 000000000..43c75aff0
--- /dev/null
+++ b/ui/tests/integration/components/replication-actions-test.js
@@ -0,0 +1,158 @@
+import { moduleForComponent, test } from 'ember-qunit';
+import hbs from 'htmlbars-inline-precompile';
+import Ember from 'ember';
+
+const CapabilitiesStub = Ember.Object.extend({
+ canUpdate: Ember.computed('capabilities', function() {
+ return (this.get('capabilities') || []).includes('root');
+ }),
+});
+
+const storeStub = Ember.Service.extend({
+ callArgs: null,
+ capabilitiesReturnVal: null,
+ findRecord(_, path) {
+ const self = this;
+ self.set('callArgs', { path });
+ const caps = CapabilitiesStub.create({
+ path,
+ capabilities: self.get('capabilitiesReturnVal') || [],
+ });
+ return Ember.RSVP.resolve(caps);
+ },
+});
+
+moduleForComponent('replication-actions', 'Integration | Component | replication actions', {
+ integration: true,
+ beforeEach: function() {
+ this.register('service:store', storeStub);
+ this.inject.service('store', { as: 'storeService' });
+ },
+});
+
+function testAction(
+ assert,
+ replicationMode,
+ clusterMode,
+ action,
+ headerText,
+ capabilitiesPath,
+ fillInFn,
+ expectedOnSubmit
+) {
+ const testKey = `${replicationMode}-${clusterMode}-${action}`;
+ if (replicationMode) {
+ this.set('model', {
+ replicationAttrs: {
+ modeForUrl: clusterMode,
+ },
+ [replicationMode]: {
+ mode: clusterMode,
+ modeForUrl: clusterMode,
+ },
+ });
+ this.set('replicationMode', replicationMode);
+ } else {
+ this.set('model', { mode: clusterMode });
+ }
+ this.set('selectedAction', action);
+ this.set('onSubmit', (...actual) => {
+ assert.deepEqual(
+ JSON.stringify(actual),
+ JSON.stringify(expectedOnSubmit),
+ `${testKey}: submitted values match expected`
+ );
+ return Ember.RSVP.resolve();
+ });
+ this.set('storeService.capabilitiesReturnVal', ['root']);
+ this.render(
+ hbs`{{replication-actions model=model replicationMode=replicationMode selectedAction=selectedAction onSubmit=(action onSubmit)}}`
+ );
+
+ assert.equal(
+ this.$(`h4:contains(${headerText})`).length,
+ 1,
+ `${testKey}: renders the correct partial as default`
+ );
+
+ if (typeof fillInFn === 'function') {
+ fillInFn.call(this);
+ }
+ this.$('button').click();
+ this.$('button.red').click();
+}
+
+function callTest(context, assert) {
+ return function() {
+ testAction.call(context, assert, ...arguments);
+ };
+}
+
+test('actions', function(assert) {
+ const t = callTest(this, assert);
+ //TODO move to table test so we don't share the same store
+ //t('dr', 'primary', 'disable', 'Disable dr replication', 'sys/replication/dr/primary/disable', null, ['disable', 'primary']);
+ //t('performance', 'primary', 'disable', 'Disable performance replication', 'sys/replication/performance/primary/disable', null, ['disable', 'primary']);
+ t('dr', 'secondary', 'disable', 'Disable replication', 'sys/replication/dr/secondary/disable', null, [
+ 'disable',
+ 'secondary',
+ ]);
+ t(
+ 'performance',
+ 'secondary',
+ 'disable',
+ 'Disable replication',
+ 'sys/replication/performance/secondary/disable',
+ null,
+ ['disable', 'secondary']
+ );
+
+ t('dr', 'primary', 'recover', 'Recover', 'sys/replication/recover', null, ['recover']);
+ t('performance', 'primary', 'recover', 'Recover', 'sys/replication/recover', null, ['recover']);
+ t('performance', 'secondary', 'recover', 'Recover', 'sys/replication/recover', null, ['recover']);
+
+ t('dr', 'primary', 'reindex', 'Reindex', 'sys/replication/reindex', null, ['reindex']);
+ t('performance', 'primary', 'reindex', 'Reindex', 'sys/replication/reindex', null, ['reindex']);
+ t('dr', 'secondary', 'reindex', 'Reindex', 'sys/replication/reindex', null, ['reindex']);
+ t('performance', 'secondary', 'reindex', 'Reindex', 'sys/replication/reindex', null, ['reindex']);
+
+ t('dr', 'primary', 'demote', 'Demote cluster', 'sys/replication/dr/primary/demote', null, [
+ 'demote',
+ 'primary',
+ ]);
+ t(
+ 'performance',
+ 'primary',
+ 'demote',
+ 'Demote cluster',
+ 'sys/replication/performance/primary/demote',
+ null,
+ ['demote', 'primary']
+ );
+ // we don't do dr secondary promote in this component so just test perf
+ t(
+ 'performance',
+ 'secondary',
+ 'promote',
+ 'Promote cluster',
+ 'sys/replication/performance/secondary/promote',
+ function() {
+ this.$('[name="primary_cluster_addr"]').val('cluster addr').change();
+ },
+ ['promote', 'secondary', { primary_cluster_addr: 'cluster addr' }]
+ );
+
+ // don't yet update-primary for dr
+ t(
+ 'performance',
+ 'secondary',
+ 'update-primary',
+ 'Update primary',
+ 'sys/replication/performance/secondary/update-primary',
+ function() {
+ this.$('#secondary-token').val('token').change();
+ this.$('#primary_api_addr').val('addr').change();
+ },
+ ['update-primary', 'secondary', { token: 'token', primary_api_addr: 'addr' }]
+ );
+});
diff --git a/ui/tests/integration/components/shamir-flow-test.js b/ui/tests/integration/components/shamir-flow-test.js
new file mode 100644
index 000000000..e5f5fd199
--- /dev/null
+++ b/ui/tests/integration/components/shamir-flow-test.js
@@ -0,0 +1,111 @@
+import { moduleForComponent, test } from 'ember-qunit';
+import hbs from 'htmlbars-inline-precompile';
+import Ember from 'ember';
+
+let response = {
+ progress: 1,
+ required: 3,
+ complete: false,
+};
+
+let percent = () => {
+ const percent = response.progress / response.required * 100;
+ return percent.toFixed(4);
+};
+
+let adapter = {
+ foo() {
+ return Ember.RSVP.resolve(response);
+ },
+};
+
+const storeStub = Ember.Service.extend({
+ adapterFor() {
+ return adapter;
+ },
+});
+
+moduleForComponent('shamir-flow', 'Integration | Component | shamir flow', {
+ integration: true,
+ beforeEach: function() {
+ this.register('service:store', storeStub);
+ this.inject.service('store', { as: 'storeService' });
+ },
+});
+
+test('it renders', function(assert) {
+ // Set any properties with this.set('myProperty', 'value');
+ // Handle any actions with this.on('myAction', function(val) { ... });
+
+ this.render(hbs`{{shamir-flow formText="like whoa"}}`);
+
+ assert.equal(this.$('form p').text().trim(), 'like whoa', 'renders formText inline');
+
+ // Template block usage:
+ this.render(hbs`
+ {{#shamir-flow formText="like whoa"}}
+ whoa again
+ {{/shamir-flow}}
+ `);
+
+ assert.equal(this.$('.shamir-progress').length, 0, 'renders no progress bar for no progress');
+ assert.equal(this.$('form p').text().trim(), 'whoa again', 'renders the block, not formText');
+
+ this.render(hbs`
+ {{shamir-flow progress=1 threshold=5}}
+ `);
+
+ assert.equal(
+ this.$('.shamir-progress .progress').val(),
+ 1 / 5 * 100,
+ 'renders progress bar with the appropriate value'
+ );
+
+ this.set('errors', ['first error', 'this is fine']);
+ this.render(hbs`
+ {{shamir-flow errors=errors}}
+ `);
+ assert.equal(this.$('.message.is-danger').length, 2, 'renders errors');
+});
+
+test('it sends data to the passed action', function(assert) {
+ this.set('key', 'foo');
+ this.render(hbs`
+ {{shamir-flow key=key action='foo' thresholdPath='required'}}
+ `);
+ this.$('[data-test-shamir-submit]').click();
+ assert.equal(
+ this.$('.shamir-progress .progress').val(),
+ percent(),
+ 'renders progress bar with correct percent value'
+ );
+});
+
+test('it checks onComplete to call onShamirSuccess', function(assert) {
+ this.set('key', 'foo');
+ this.set('onSuccess', function() {
+ assert.ok(true, 'onShamirSuccess called');
+ });
+
+ this.set('checkComplete', function() {
+ assert.ok(true, 'onComplete called');
+ // return true so we trigger success call
+ return true;
+ });
+
+ this.render(hbs`
+ {{shamir-flow key=key action='foo' isComplete=(action checkComplete) onShamirSuccess=(action onSuccess)}}
+ `);
+ this.$('[data-test-shamir-submit]').click();
+});
+
+test('it fetches progress on init when fetchOnInit is true', function(assert) {
+ this.render(hbs`
+ {{shamir-flow action='foo' fetchOnInit=true}}
+ `);
+ assert.equal(
+ this.$('.shamir-progress .progress').val(),
+ percent(),
+ 'renders progress bar with correct percent value'
+ );
+});
diff --git a/ui/tests/integration/components/string-list-test.js b/ui/tests/integration/components/string-list-test.js
new file mode 100644
index 000000000..283667a2b
--- /dev/null
+++ b/ui/tests/integration/components/string-list-test.js
@@ -0,0 +1,118 @@
+import { moduleForComponent, test } from 'ember-qunit';
+import hbs from 'htmlbars-inline-precompile';
+
+moduleForComponent('string-list', 'Integration | Component | string list', {
+ integration: true,
+});
+
+const assertBlank = function(assert) {
+ assert.equal(this.$('[data-test-string-list-input]').length, 1, 'renders 1 input');
+ assert.equal(this.$('[data-test-string-list-input]').val(), '', 'the input is blank');
+};
+
+const assertFoo = function(assert) {
+ assert.equal(this.$('[data-test-string-list-input]').length, 2, 'renders 2 inputs');
+ assert.equal(this.$('[data-test-string-list-input="0"]').val(), 'foo', 'first input has the inputValue');
+ assert.equal(this.$('[data-test-string-list-input="1"]').val(), '', 'second input is blank');
+};
+
+const assertFooBar = function(assert) {
+ assert.equal(this.$('[data-test-string-list-input]').length, 3, 'renders 3 inputs');
+ assert.equal(this.$('[data-test-string-list-input="0"]').val(), 'foo');
+ assert.equal(this.$('[data-test-string-list-input="1"]').val(), 'bar');
+ assert.equal(this.$('[data-test-string-list-input="2"]').val(), '', 'last input is blank');
+};
+
+test('it renders the label', function(assert) {
+ this.render(hbs`{{string-list label="foo"}}`);
+ assert.equal(
+ this.$('[data-test-string-list-label]').text().trim(),
+ 'foo',
+ 'renders the label when provided'
+ );
+
+ this.render(hbs`{{string-list}}`);
+ assert.equal(this.$('[data-test-string-list-label]').length, 0, 'does not render the label');
+ assertBlank.call(this, assert);
+});
+
+test('it renders inputValue from empty string', function(assert) {
+ this.render(hbs`{{string-list inputValue=""}}`);
+ assertBlank.call(this, assert);
+});
+
+test('it renders inputValue from string with one value', function(assert) {
+ this.render(hbs`{{string-list inputValue="foo"}}`);
+ assertFoo.call(this, assert);
+});
+
+test('it renders inputValue from comma-separated string', function(assert) {
+ this.render(hbs`{{string-list inputValue="foo,bar"}}`);
+ assertFooBar.call(this, assert);
+});
+
+test('it renders inputValue from a blank array', function(assert) {
+ this.set('inputValue', []);
+ this.render(hbs`{{string-list inputValue=inputValue}}`);
+ assertBlank.call(this, assert);
+});
+
+test('it renders inputValue array with a single item', function(assert) {
+ this.set('inputValue', ['foo']);
+ this.render(hbs`{{string-list inputValue=inputValue}}`);
+ assertFoo.call(this, assert);
+});
+
+test('it renders inputValue array with a multiple items', function(assert) {
+ this.set('inputValue', ['foo', 'bar']);
+ this.render(hbs`{{string-list inputValue=inputValue}}`);
+ assertFooBar.call(this, assert);
+});
+
+test('it adds a new row only when the last row is not blank', function(assert) {
+ this.render(hbs`{{string-list inputValue=""}}`);
+ this.$('[data-test-string-list-button="add"]').click();
+ assertBlank.call(this, assert);
+ this.$('[data-test-string-list-input="0"]').val('foo').keyup();
+ this.$('[data-test-string-list-button="add"]').click();
+ assertFoo.call(this, assert);
+});
+
+test('it trims input values', function(assert) {
+ this.render(hbs`{{string-list inputValue=""}}`);
+ this.$('[data-test-string-list-input="0"]').val(' foo ').keyup();
+ assert.equal(this.$('[data-test-string-list-input="0"]').val(), 'foo');
+});
+
+test('it calls onChange with array when editing', function(assert) {
+ assert.expect(1);
+ this.set('inputValue', ['foo']);
+ this.set('onChange', function(val) {
+ assert.deepEqual(val, ['foo', 'bar'], 'calls onChange with expected value');
+ });
+ this.render(hbs`{{string-list inputValue=inputValue onChange=(action onChange)}}`);
+ this.$('[data-test-string-list-input="1"]').val('bar').keyup();
+});
+
+test('it calls onChange with string when editing', function(assert) {
+ assert.expect(1);
+ this.set('inputValue', 'foo');
+ this.set('onChange', function(val) {
+ assert.equal(val, 'foo,bar', 'calls onChange with expected value');
+ });
+ this.render(hbs`{{string-list inputValue=inputValue onChange=(action onChange)}}`);
+ this.$('[data-test-string-list-input="1"]').val('bar').keyup();
+});
+
+test('it removes a row', function(assert) {
+ this.set('inputValue', ['foo', 'bar']);
+ this.set('onChange', function(val) {
+ assert.equal(val, 'bar', 'calls onChange with expected value');
+ });
+ this.render(hbs`{{string-list inputValue=inputValue onChange=(action onChange)}}`);
+
+ this.$('[data-test-string-list-row="0"] [data-test-string-list-button="delete"]').click();
+ assert.equal(this.$('[data-test-string-list-input]').length, 2, 'renders 2 inputs');
+ assert.equal(this.$('[data-test-string-list-input="0"]').val(), 'bar');
+ assert.equal(this.$('[data-test-string-list-input="1"]').val(), '');
+});
diff --git a/ui/tests/integration/components/toggle-button-test.js b/ui/tests/integration/components/toggle-button-test.js
new file mode 100644
index 000000000..f1ccfbb6b
--- /dev/null
+++ b/ui/tests/integration/components/toggle-button-test.js
@@ -0,0 +1,30 @@
+import { moduleForComponent, test } from 'ember-qunit';
+import hbs from 'htmlbars-inline-precompile';
+
+moduleForComponent('toggle-button', 'Integration | Component | toggle button', {
+ integration: true,
+});
+
+test('toggle functionality', function(assert) {
+ this.set('toggleTarget', {});
+
+ this.render(hbs`{{toggle-button toggleTarget=toggleTarget toggleAttr="toggled"}}`);
+
+ assert.equal(this.$('button').text().trim(), 'More options', 'renders default closedLabel');
+
+ this.$('button').click();
+ assert.equal(this.get('toggleTarget.toggled'), true, 'it toggles the attr on the target');
+ assert.equal(this.$('button').text().trim(), 'Hide options', 'renders default openLabel');
+ this.$('button').click();
+ assert.equal(this.get('toggleTarget.toggled'), false, 'it toggles the attr on the target');
+
+ this.set('closedLabel', 'Open the options!');
+ this.set('openLabel', 'Close the options!');
+ this.render(
+ hbs`{{toggle-button toggleTarget=toggleTarget toggleAttr="toggled" closedLabel=closedLabel openLabel=openLabel}}`
+ );
+
+ assert.equal(this.$('button').text().trim(), 'Open the options!', 'renders passed closedLabel');
+ this.$('button').click();
+ assert.equal(this.$('button').text().trim(), 'Close the options!', 'renders passed openLabel');
+});
diff --git a/ui/tests/integration/components/transit-key-actions-test.js b/ui/tests/integration/components/transit-key-actions-test.js
new file mode 100644
index 000000000..7c66d3745
--- /dev/null
+++ b/ui/tests/integration/components/transit-key-actions-test.js
@@ -0,0 +1,227 @@
+import { moduleForComponent, test } from 'ember-qunit';
+import hbs from 'htmlbars-inline-precompile';
+import Ember from 'ember';
+import { encodeString } from 'vault/utils/b64';
+
+const storeStub = Ember.Service.extend({
+ callArgs: null,
+ keyActionReturnVal: null,
+ rootKeyActionReturnVal: null,
+ adapterFor() {
+ const self = this;
+ return {
+ keyAction(action, { backend, id, payload }, options) {
+ self.set('callArgs', { action, backend, id, payload });
+ self.set('callArgsOptions', options);
+ const rootResp = Ember.assign({}, self.get('rootKeyActionReturnVal'));
+ const resp =
+ Object.keys(rootResp).length > 0
+ ? rootResp
+ : {
+ data: Ember.assign({}, self.get('keyActionReturnVal')),
+ };
+ return Ember.RSVP.resolve(resp);
+ },
+ };
+ },
+});
+
+moduleForComponent('transit-key-actions', 'Integration | Component | transit key actions', {
+ integration: true,
+ beforeEach: function() {
+ this.register('service:store', storeStub);
+ this.inject.service('store', { as: 'storeService' });
+ },
+});
+
+test('it requires `key`', function(assert) {
+ assert.expectAssertion(
+ () => this.render(hbs`{{transit-key-actions}}`),
+ /`key` is required for/,
+ 'asserts without key'
+ );
+});
+
+test('it renders', function(assert) {
+ this.set('key', { backend: 'transit', supportedActions: ['encrypt'] });
+ this.render(hbs`{{transit-key-actions selectedAction="encrypt" key=key}}`);
+ assert.equal(this.$('[data-test-transit-action="encrypt"]').length, 1, 'renders encrypt');
+
+ this.set('key', { backend: 'transit', supportedActions: ['sign'] });
+ this.render(hbs`{{transit-key-actions selectedAction="sign" key=key}}`);
+ assert.equal(this.$('[data-test-transit-action="sign"]').length, 1, 'renders sign');
+});
+
+test('it renders: rotate', function(assert) {
+ this.set('key', { backend: 'transit', id: 'akey', supportedActions: ['rotate'] });
+ this.render(hbs`{{transit-key-actions selectedAction="rotate" key=key}}`);
+
+ assert.equal(this.$().text().trim(), '', 'renders an empty div');
+
+ this.set('key.canRotate', true);
+ assert.equal(
+ this.$('button').text().trim(),
+ 'Rotate encryption key',
+ 'renders confirm-button when key.canRotate is true'
+ );
+});
+
+function doEncrypt(assert, actions = [], keyattrs = {}) {
+ let keyDefaults = { backend: 'transit', id: 'akey', supportedActions: ['encrypt'].concat(actions) };
+
+ const key = Ember.assign({}, keyDefaults, keyattrs);
+ this.set('key', key);
+ this.set('selectedAction', 'encrypt');
+ this.set('storeService.keyActionReturnVal', { ciphertext: 'secret' });
+ this.render(hbs`{{transit-key-actions selectedAction=selectedAction key=key}}`);
+
+ this.$('#plaintext').val('plaintext').trigger('input');
+ this.$('[data-test-transit-b64-toggle="plaintext"]').click();
+ this.$('button:submit').click();
+ assert.deepEqual(
+ this.get('storeService.callArgs'),
+ {
+ action: 'encrypt',
+ backend: 'transit',
+ id: 'akey',
+ payload: {
+ plaintext: encodeString('plaintext'),
+ },
+ },
+ 'passes expected args to the adapter'
+ );
+
+ assert.equal(this.$('#ciphertext').val(), 'secret');
+}
+
+test('it encrypts', doEncrypt);
+
+test('it shows key version selection', function(assert) {
+ let keyDefaults = { backend: 'transit', id: 'akey', supportedActions: ['encrypt'].concat([]) };
+ let keyattrs = { keysForEncryption: [3, 2, 1], latestVersion: 3 };
+ const key = Ember.assign({}, keyDefaults, keyattrs);
+ this.set('key', key);
+ this.set('storeService.keyActionReturnVal', { ciphertext: 'secret' });
+ this.render(hbs`{{transit-key-actions selectedAction="encrypt" key=key}}`);
+
+ this.$('#plaintext').val('plaintext').trigger('input');
+ this.$('[data-test-transit-b64-toggle="plaintext"]').click();
+ assert.equal(this.$('#key_version').length, 1, 'it renders the key version selector');
+
+ this.$('#key_version').trigger('change');
+ this.$('button:submit').click();
+ assert.deepEqual(
+ this.get('storeService.callArgs'),
+ {
+ action: 'encrypt',
+ backend: 'transit',
+ id: 'akey',
+ payload: {
+ plaintext: encodeString('plaintext'),
+ key_version: '0',
+ },
+ },
+ 'includes key_version in the payload'
+ );
+});
+
+test('it hides key version selection', function(assert) {
+ let keyDefaults = { backend: 'transit', id: 'akey', supportedActions: ['encrypt'].concat([]) };
+ let keyattrs = { keysForEncryption: [1] };
+ const key = Ember.assign({}, keyDefaults, keyattrs);
+ this.set('key', key);
+ this.set('storeService.keyActionReturnVal', { ciphertext: 'secret' });
+ this.render(hbs`{{transit-key-actions selectedAction="encrypt" key=key}}`);
+
+ this.$('#plaintext').val('plaintext').trigger('input');
+ this.$('[data-test-transit-b64-toggle="plaintext"]').click();
+
+ assert.equal(
+ this.$('#key_version').length,
+ 0,
+ 'it does not render the selector when there is only one key'
+ );
+});
+
+test('it carries ciphertext value over to decrypt', function(assert) {
+ const plaintext = 'not so secret';
+ doEncrypt.call(this, assert, ['decrypt']);
+
+ this.set('storeService.keyActionReturnVal', { plaintext });
+ this.set('selectedAction', 'decrypt');
+ assert.equal(this.$('#ciphertext').val(), 'secret', 'keeps ciphertext value');
+
+ this.$('button:submit').click();
+ assert.equal(this.$('#plaintext').val(), plaintext, 'renders decrypted value');
+});
+
+const setupExport = function() {
+ this.set('key', {
+ backend: 'transit',
+ id: 'akey',
+ supportedActions: ['export'],
+ exportKeyTypes: ['encryption'],
+ validKeyVersions: [1],
+ });
+ this.render(hbs`{{transit-key-actions key=key}}`);
+};
+
+test('it can export a key:default behavior', function(assert) {
+ this.set('storeService.rootKeyActionReturnVal', { wrap_info: { token: 'wrapped-token' } });
+ setupExport.call(this);
+ this.$('button:submit').click();
+
+ assert.deepEqual(
+ this.get('storeService.callArgs'),
+ {
+ action: 'export',
+ backend: 'transit',
+ id: 'akey',
+ payload: {
+ param: ['encryption'],
+ },
+ },
+ 'passes expected args to the adapter'
+ );
+ assert.equal(this.get('storeService.callArgsOptions.wrapTTL'), '30m', 'passes value for wrapTTL');
+ assert.equal(this.$('#export').val(), 'wrapped-token', 'wraps by default');
+});
+
+test('it can export a key:unwrapped behavior', function(assert) {
+ const response = { keys: { a: 'key' } };
+ this.set('storeService.keyActionReturnVal', response);
+ setupExport.call(this);
+ this.$('#wrap-response').click().change();
+ this.$('button:submit').click();
+ assert.deepEqual(
+ JSON.parse(this.$('.CodeMirror').get(0).CodeMirror.getValue()),
+ response,
+ 'prints json response'
+ );
+});
+
+test('it can export a key: unwrapped, single version', function(assert) {
+ const response = { keys: { a: 'key' } };
+ this.set('storeService.keyActionReturnVal', response);
+ setupExport.call(this);
+ this.$('#wrap-response').click().change();
+ this.$('#exportVersion').click().change();
+ this.$('button:submit').click();
+ assert.deepEqual(
+ JSON.parse(this.$('.CodeMirror').get(0).CodeMirror.getValue()),
+ response,
+ 'prints json response'
+ );
+ assert.deepEqual(
+ this.get('storeService.callArgs'),
+ {
+ action: 'export',
+ backend: 'transit',
+ id: 'akey',
+ payload: {
+ param: ['encryption', 1],
+ },
+ },
+ 'passes expected args to the adapter'
+ );
+});
diff --git a/ui/tests/integration/components/upgrade-link-test.js b/ui/tests/integration/components/upgrade-link-test.js
new file mode 100644
index 000000000..8db46a7b7
--- /dev/null
+++ b/ui/tests/integration/components/upgrade-link-test.js
@@ -0,0 +1,42 @@
+import { moduleForComponent, test } from 'ember-qunit';
+import hbs from 'htmlbars-inline-precompile';
+
+moduleForComponent('upgrade-link', 'Integration | Component | upgrade link', {
+ integration: true,
+});
+
+test('it renders with overlay', function(assert) {
+ this.render(hbs`
+
+ {{#upgrade-link data-test-link}}upgrade{{/upgrade-link}}
+
+
+ `);
+
+ assert.equal(this.$('.upgrade-link-container button').text().trim(), 'upgrade', 'renders link content');
+ assert.equal(
+ this.$('#modal-wormhole .upgrade-overlay-title').text().trim(),
+ 'Vault Enterprise',
+ 'contains overlay content'
+ );
+ assert.equal(
+ this.$('#modal-wormhole a[href^="https://www.hashicorp.com/go/vault-enterprise"]').length,
+ 1,
+ 'contains info link'
+ );
+});
+
+test('it adds custom classes', function(assert) {
+ this.render(hbs`
+
+ {{#upgrade-link linkClass="button upgrade-button"}}upgrade{{/upgrade-link}}
+
+
+ `);
+
+ assert.equal(
+ this.$('.upgrade-link-container button').attr('class'),
+ 'link button upgrade-button',
+ 'adds classes to link'
+ );
+});
diff --git a/ui/tests/integration/components/upgrade-page-test.js b/ui/tests/integration/components/upgrade-page-test.js
new file mode 100644
index 000000000..43372a7a4
--- /dev/null
+++ b/ui/tests/integration/components/upgrade-page-test.js
@@ -0,0 +1,35 @@
+import { moduleForComponent, test } from 'ember-qunit';
+import hbs from 'htmlbars-inline-precompile';
+
+moduleForComponent('upgrade-page', 'Integration | Component | upgrade page', {
+ integration: true,
+});
+
+test('it renders with defaults', function(assert) {
+ this.render(hbs`
+ {{upgrade-page}}
+
+ `);
+
+ assert.equal(this.$('.page-header .title').text().trim(), 'Vault Enterprise', 'renders default title');
+ assert.equal(
+ this.$('[data-test-upgrade-feature-description]').text().trim(),
+ 'This is a Vault Enterprise feature.',
+ 'renders default description'
+ );
+ assert.equal(this.$('[data-test-upgrade-link]').length, 1, 'renders upgrade link');
+});
+
+test('it renders with custom attributes', function(assert) {
+ this.render(hbs`
+ {{upgrade-page title="Test Feature Title" featureName="Specific Feature Name" minimumEdition="Premium"}}
+
+ `);
+
+ assert.equal(this.$('.page-header .title').text().trim(), 'Test Feature Title', 'renders default title');
+ assert.equal(
+ this.$('[data-test-upgrade-feature-description]').text().trim(),
+ 'Specific Feature Name is a Premium feature.',
+ 'renders default description'
+ );
+});
diff --git a/ui/tests/integration/components/wrap-ttl-test.js b/ui/tests/integration/components/wrap-ttl-test.js
new file mode 100644
index 000000000..3d43dddc6
--- /dev/null
+++ b/ui/tests/integration/components/wrap-ttl-test.js
@@ -0,0 +1,45 @@
+import { moduleForComponent, test } from 'ember-qunit';
+import hbs from 'htmlbars-inline-precompile';
+
+moduleForComponent('wrap-ttl', 'Integration | Component | wrap ttl', {
+ integration: true,
+ beforeEach() {
+ this.lastOnChangeCall = null;
+ this.set('onChange', val => {
+ this.lastOnChangeCall = val;
+ });
+ },
+});
+
+test('it requires `onChange`', function(assert) {
+ assert.expectAssertion(
+ () => this.render(hbs`{{wrap-ttl}}`),
+ /`onChange` handler is a required attr in/,
+ 'asserts without onChange'
+ );
+});
+
+test('it renders', function(assert) {
+ this.render(hbs`{{wrap-ttl onChange=(action onChange)}}`);
+ assert.equal(this.lastOnChangeCall, '30m', 'calls onChange with 30m default on first render');
+ assert.equal(this.$('label[for=wrap-response]').text().trim(), 'Wrap response');
+});
+
+test('it nulls out value when you uncheck wrapResponse', function(assert) {
+ this.render(hbs`{{wrap-ttl onChange=(action onChange)}}`);
+ this.$('#wrap-response').click().change();
+ assert.equal(this.lastOnChangeCall, null, 'calls onChange with null');
+});
+
+test('it sends value changes to onChange handler', function(assert) {
+ this.render(hbs`{{wrap-ttl onChange=(action onChange)}}`);
+
+ this.$('[data-test-wrap-ttl-picker] input').val('20').trigger('input');
+ assert.equal(this.lastOnChangeCall, '20m', 'calls onChange correctly on time input');
+
+ this.$('#unit').val('h').change();
+ assert.equal(this.lastOnChangeCall, '20h', 'calls onChange correctly on unit change');
+
+ this.$('#unit').val('d').change();
+ assert.equal(this.lastOnChangeCall, '480h', 'converts days to hours correctly');
+});
diff --git a/ui/tests/integration/helpers/has-feature-test.js b/ui/tests/integration/helpers/has-feature-test.js
new file mode 100644
index 000000000..f91f1b0eb
--- /dev/null
+++ b/ui/tests/integration/helpers/has-feature-test.js
@@ -0,0 +1,33 @@
+import { moduleForComponent, test } from 'ember-qunit';
+import hbs from 'htmlbars-inline-precompile';
+import Ember from 'ember';
+
+const versionStub = Ember.Service.extend({
+ features: null,
+});
+
+moduleForComponent('has-feature', 'helper:has-feature', {
+ integration: true,
+ beforeEach: function() {
+ this.register('service:version', versionStub);
+ this.inject.service('version', { as: 'versionService' });
+ },
+});
+
+test('it asserts on unknown features', function(assert) {
+ assert.expectAssertion(() => {
+ this.render(hbs`{{has-feature 'New Feature'}}`);
+ }, 'New Feature is not one of the available values for Vault Enterprise features.');
+});
+
+test('it is true with existing features', function(assert) {
+ this.set('versionService.features', ['HSM']);
+ this.render(hbs`{{if (has-feature 'HSM') 'It works' null}}`);
+ assert.equal(this._element.textContent.trim(), 'It works', 'present features evaluate to true');
+});
+
+test('it is false with missing features', function(assert) {
+ this.set('versionService.features', ['MFA']);
+ this.render(hbs`{{if (has-feature 'HSM') 'It works' null}}`);
+ assert.equal(this._element.textContent.trim(), '', 'missing features evaluate to false');
+});
diff --git a/ui/tests/integration/utils/field-to-attrs-test.js b/ui/tests/integration/utils/field-to-attrs-test.js
new file mode 100644
index 000000000..dbae80b90
--- /dev/null
+++ b/ui/tests/integration/utils/field-to-attrs-test.js
@@ -0,0 +1,147 @@
+import Ember from 'ember';
+import { moduleForModel, test } from 'ember-qunit';
+import { methods } from 'vault/helpers/mountable-auth-methods';
+import fieldToAttrs, { expandAttributeMeta } from 'vault/utils/field-to-attrs';
+const METHODS = methods();
+
+moduleForModel('test-form-model', 'Integration | Util | field to attrs', {
+ needs: ['model:auth-config', 'model:mount-config'],
+});
+
+const PATH_ATTR = { type: 'string', name: 'path', options: { defaultValue: METHODS[0].value } };
+const DESCRIPTION_ATTR = { type: 'string', name: 'description', options: { editType: 'textarea' } };
+const DEFAULT_LEASE_ATTR = {
+ type: undefined,
+ name: 'config.defaultLeaseTtl',
+ options: { label: 'Default Lease TTL', editType: 'ttl' },
+};
+
+const OTHER_DEFAULT_LEASE_ATTR = {
+ type: undefined,
+ name: 'otherConfig.defaultLeaseTtl',
+ options: { label: 'Default Lease TTL', editType: 'ttl' },
+};
+const MAX_LEASE_ATTR = {
+ type: undefined,
+ name: 'config.maxLeaseTtl',
+ options: { label: 'Max Lease TTL', editType: 'ttl' },
+};
+const OTHER_MAX_LEASE_ATTR = {
+ type: undefined,
+ name: 'otherConfig.maxLeaseTtl',
+ options: { label: 'Max Lease TTL', editType: 'ttl' },
+};
+
+test('it extracts attrs', function(assert) {
+ const model = this.subject();
+ Ember.run(() => {
+ const [attr] = expandAttributeMeta(model, ['path']);
+ assert.deepEqual(attr, PATH_ATTR, 'returns attribute meta');
+ });
+});
+
+test('it extracts more than one attr', function(assert) {
+ const model = this.subject();
+ Ember.run(() => {
+ const [path, desc] = expandAttributeMeta(model, ['path', 'description']);
+ assert.deepEqual(path, PATH_ATTR, 'returns attribute meta');
+ assert.deepEqual(desc, DESCRIPTION_ATTR, 'returns attribute meta');
+ });
+});
+
+test('it extracts fieldGroups', function(assert) {
+ const model = this.subject();
+ Ember.run(() => {
+ const groups = fieldToAttrs(model, [{ default: ['path'] }, { Options: ['description'] }]);
+ const expected = [{ default: [PATH_ATTR] }, { Options: [DESCRIPTION_ATTR] }];
+ assert.deepEqual(groups, expected, 'expands all given groups');
+ });
+});
+
+test('it extracts arrays as fieldGroups', function(assert) {
+ const model = this.subject();
+ Ember.run(() => {
+ const groups = fieldToAttrs(model, [{ default: ['path', 'description'] }, { Options: ['description'] }]);
+ const expected = [{ default: [PATH_ATTR, DESCRIPTION_ATTR] }, { Options: [DESCRIPTION_ATTR] }];
+ assert.deepEqual(groups, expected, 'expands all given groups');
+ });
+});
+
+test('it extracts model-fragment attributes with brace expansion', function(assert) {
+ const model = this.subject();
+ Ember.run(() => {
+ const [attr] = expandAttributeMeta(model, ['config.{defaultLeaseTtl}']);
+ assert.deepEqual(attr, DEFAULT_LEASE_ATTR, 'properly extracts model fragment attr');
+ });
+
+ Ember.run(() => {
+ const [defaultLease, maxLease] = expandAttributeMeta(model, ['config.{defaultLeaseTtl,maxLeaseTtl}']);
+ assert.deepEqual(defaultLease, DEFAULT_LEASE_ATTR, 'properly extracts default lease');
+ assert.deepEqual(maxLease, MAX_LEASE_ATTR, 'properly extracts max lease');
+ });
+});
+
+test('it extracts model-fragment attributes with double brace expansion', function(assert) {
+ const model = this.subject();
+ Ember.run(() => {
+ const [configDefault, configMax, otherConfigDefault, otherConfigMax] = expandAttributeMeta(model, [
+ '{config,otherConfig}.{defaultLeaseTtl,maxLeaseTtl}',
+ ]);
+ assert.deepEqual(configDefault, DEFAULT_LEASE_ATTR, 'properly extracts config.defaultLeaseTTL');
+ assert.deepEqual(
+ otherConfigDefault,
+ OTHER_DEFAULT_LEASE_ATTR,
+ 'properly extracts otherConfig.defaultLeaseTTL'
+ );
+
+ assert.deepEqual(configMax, MAX_LEASE_ATTR, 'properly extracts config.maxLeaseTTL');
+ assert.deepEqual(otherConfigMax, OTHER_MAX_LEASE_ATTR, 'properly extracts otherConfig.maxLeaseTTL');
+ });
+});
+
+test('it extracts model-fragment attributes with dot notation', function(assert) {
+ const model = this.subject();
+ Ember.run(() => {
+ const [attr] = expandAttributeMeta(model, ['config.defaultLeaseTtl']);
+ assert.deepEqual(attr, DEFAULT_LEASE_ATTR, 'properly extracts model fragment attr');
+ });
+
+ Ember.run(() => {
+ const [defaultLease, maxLease] = expandAttributeMeta(model, [
+ 'config.defaultLeaseTtl',
+ 'config.maxLeaseTtl',
+ ]);
+ assert.deepEqual(defaultLease, DEFAULT_LEASE_ATTR, 'properly extracts model fragment attr');
+ assert.deepEqual(maxLease, MAX_LEASE_ATTR, 'properly extracts model fragment attr');
+ });
+});
+
+test('it extracts fieldGroups from model-fragment attributes with brace expansion', function(assert) {
+ const model = this.subject();
+ const expected = [
+ { default: [PATH_ATTR, DEFAULT_LEASE_ATTR, MAX_LEASE_ATTR] },
+ { Options: [DESCRIPTION_ATTR] },
+ ];
+ Ember.run(() => {
+ const groups = fieldToAttrs(model, [
+ { default: ['path', 'config.{defaultLeaseTtl,maxLeaseTtl}'] },
+ { Options: ['description'] },
+ ]);
+ assert.deepEqual(groups, expected, 'properly extracts fieldGroups with brace expansion');
+ });
+});
+
+test('it extracts fieldGroups from model-fragment attributes with dot notation', function(assert) {
+ const model = this.subject();
+ const expected = [
+ { default: [DEFAULT_LEASE_ATTR, PATH_ATTR, MAX_LEASE_ATTR] },
+ { Options: [DESCRIPTION_ATTR] },
+ ];
+ Ember.run(() => {
+ const groups = fieldToAttrs(model, [
+ { default: ['config.defaultLeaseTtl', 'path', 'config.maxLeaseTtl'] },
+ { Options: ['description'] },
+ ]);
+ assert.deepEqual(groups, expected, 'properly extracts fieldGroups with dot notation');
+ });
+});
diff --git a/ui/tests/pages/access/identity/index.js b/ui/tests/pages/access/identity/index.js
new file mode 100644
index 000000000..96e44360b
--- /dev/null
+++ b/ui/tests/pages/access/identity/index.js
@@ -0,0 +1,4 @@
+import { create, visitable } from 'ember-cli-page-object';
+export default create({
+ visit: visitable('/vault/access/identity/:item_type'),
+});
diff --git a/ui/tests/pages/access/methods.js b/ui/tests/pages/access/methods.js
new file mode 100644
index 000000000..39af2890b
--- /dev/null
+++ b/ui/tests/pages/access/methods.js
@@ -0,0 +1,24 @@
+import { create, attribute, visitable, collection, hasClass, text } from 'ember-cli-page-object';
+
+export default create({
+ visit: visitable('/vault/access/'),
+ navLinks: collection({
+ scope: '[data-test-sidebar]',
+ itemScope: '[data-test-link]',
+ item: {
+ isActive: hasClass('is-active'),
+ text: text(),
+ },
+ }),
+
+ backendLinks: collection({
+ itemScope: '[data-test-auth-backend-link]',
+ item: {
+ path: text('[data-test-path]'),
+ id: attribute('data-test-id', '[data-test-path]'),
+ },
+ findById(id) {
+ return this.toArray().findBy('id', id);
+ },
+ }),
+});
diff --git a/ui/tests/pages/components/config-pki-ca.js b/ui/tests/pages/components/config-pki-ca.js
new file mode 100644
index 000000000..e37c18fd4
--- /dev/null
+++ b/ui/tests/pages/components/config-pki-ca.js
@@ -0,0 +1,54 @@
+import { clickable, collection, fillable, text, selectable, isPresent } from 'ember-cli-page-object';
+
+import fields from './form-field';
+
+export default {
+ ...fields,
+ scope: '.config-pki-ca',
+ text: text('[data-test-text]'),
+ title: text('[data-test-title]'),
+
+ hasTitle: isPresent('[data-test-title]'),
+ hasError: isPresent('[data-test-error]'),
+ hasSignIntermediateForm: isPresent('[data-test-sign-intermediate-form]'),
+
+ replaceCA: clickable('[data-test-go-replace-ca]'),
+ replaceCAText: text('[data-test-go-replace-ca]'),
+ setSignedIntermediateBtn: clickable('[data-test-go-set-signed-intermediate]'),
+ signIntermediateBtn: clickable('[data-test-go-sign-intermediate]'),
+ caType: selectable('[data-test-input="caType"]'),
+ submit: clickable('[data-test-submit]'),
+ back: clickable('[data-test-back-button]'),
+
+ signedIntermediate: fillable('[data-test-signed-intermediate]'),
+ downloadLinks: collection({ itemScope: '[data-test-ca-download-link]' }),
+ rows: collection({
+ itemScope: '[data-test-table-row]',
+ }),
+ rowValues: collection({
+ itemScope: '[data-test-row-value]',
+ }),
+ csr: text('[data-test-row-value="CSR"]', { normalize: false }),
+ csrField: fillable('[data-test-input="csr"]'),
+ certificate: text('[data-test-row-value="Certificate"]', { normalize: false }),
+ certificateIsPresent: isPresent('[data-test-row-value="Certificate"]'),
+ uploadCert: clickable('[data-test-input="uploadPemBundle"]'),
+ enterCertAsText: clickable('[data-test-text-toggle]'),
+ pemBundle: fillable('[data-test-text-file-textarea="true"]'),
+ commonName: fillable('[data-test-input="commonName"]'),
+
+ generateCA(commonName = 'PKI CA', type = 'root') {
+ if (type === 'intermediate') {
+ return this.replaceCA().commonName(commonName).caType('intermediate').submit();
+ }
+ return this.replaceCA().commonName(commonName).submit();
+ },
+
+ uploadCA(pem) {
+ return this.replaceCA().uploadCert().enterCertAsText().pemBundle(pem).submit();
+ },
+
+ signIntermediate(commonName) {
+ return this.signIntermediateBtn().commonName(commonName);
+ },
+};
diff --git a/ui/tests/pages/components/config-pki.js b/ui/tests/pages/components/config-pki.js
new file mode 100644
index 000000000..46de01a56
--- /dev/null
+++ b/ui/tests/pages/components/config-pki.js
@@ -0,0 +1,13 @@
+import { clickable, fillable, text, isPresent } from 'ember-cli-page-object';
+import fields from './form-field';
+
+export default {
+ ...fields,
+ scope: '.config-pki',
+ text: text('[data-test-text]'),
+ title: text('[data-test-title]'),
+ hasTitle: isPresent('[data-test-title]'),
+ hasError: isPresent('[data-test-error]'),
+ submit: clickable('[data-test-submit]'),
+ fillInField: fillable('[data-test-field]'),
+};
diff --git a/ui/tests/pages/components/flash-message.js b/ui/tests/pages/components/flash-message.js
new file mode 100644
index 000000000..c4f98e2da
--- /dev/null
+++ b/ui/tests/pages/components/flash-message.js
@@ -0,0 +1,12 @@
+import { collection } from 'ember-cli-page-object';
+import { getter } from 'ember-cli-page-object/macros';
+
+export default {
+ latestMessage: getter(function() {
+ const count = this.messages().count;
+ return this.messages(count - 1).text;
+ }),
+ messages: collection({
+ itemScope: '[data-test-flash-message-body]',
+ }),
+};
diff --git a/ui/tests/pages/components/form-field.js b/ui/tests/pages/components/form-field.js
new file mode 100644
index 000000000..4f5c2779c
--- /dev/null
+++ b/ui/tests/pages/components/form-field.js
@@ -0,0 +1,55 @@
+import {
+ attribute,
+ focusable,
+ value,
+ clickable,
+ isPresent,
+ collection,
+ fillable,
+ text,
+ triggerable,
+} from 'ember-cli-page-object';
+import { getter } from 'ember-cli-page-object/macros';
+
+export default {
+ hasStringList: isPresent('[data-test-component=string-list]'),
+ hasTextFile: isPresent('[data-test-component=text-file]'),
+ hasTTLPicker: isPresent('[data-test-component=ttl-picker]'),
+ hasJSONEditor: isPresent('[data-test-component=json-editor]'),
+ hasSelect: isPresent('select'),
+ hasInput: isPresent('input'),
+ hasCheckbox: isPresent('input[type=checkbox]'),
+ hasTextarea: isPresent('textarea'),
+ hasTooltip: isPresent('[data-test-component=info-tooltip]'),
+ tooltipTrigger: focusable('[data-test-tool-tip-trigger]'),
+ tooltipContent: text('[data-test-help-text]'),
+
+ fields: collection({
+ itemScope: '[data-test-field]',
+ item: {
+ clickLabel: clickable('label'),
+ for: attribute('for', 'label'),
+ labelText: text('label', { multiple: true }),
+ input: fillable('input'),
+ select: fillable('select'),
+ textarea: fillable('textarea'),
+ change: triggerable('keyup', 'input'),
+ inputValue: value('input'),
+ textareaValue: value('textarea'),
+ inputChecked: attribute('checked', 'input[type=checkbox]'),
+ selectValue: value('select'),
+ },
+ findByName(name) {
+ // we use name in the label `for` attribute
+ // this is consistent across all types of fields
+ //(otherwise we'd have to use name on select or input or textarea)
+ return this.toArray().findBy('for', name);
+ },
+ fillIn(name, value) {
+ return this.findByName(name).input(value);
+ },
+ }),
+ field: getter(function() {
+ return this.fields(0);
+ }),
+};
diff --git a/ui/tests/pages/components/kv-object-editor.js b/ui/tests/pages/components/kv-object-editor.js
new file mode 100644
index 000000000..cc120d4eb
--- /dev/null
+++ b/ui/tests/pages/components/kv-object-editor.js
@@ -0,0 +1,14 @@
+import { clickable, collection, fillable, isPresent } from 'ember-cli-page-object';
+
+export default {
+ showsDuplicateError: isPresent('[data-test-duplicate-error-warnings]'),
+ addRow: clickable('[data-test-kv-add-row]'),
+ rows: collection({
+ itemScope: '[data-test-kv-row]',
+ item: {
+ kvKey: fillable('[data-test-kv-key]'),
+ kvVal: fillable('[data-test-kv-value]'),
+ deleteRow: clickable('[data-test-kv-delete-row]'),
+ },
+ }),
+};
diff --git a/ui/tests/pages/components/message-in-page.js b/ui/tests/pages/components/message-in-page.js
new file mode 100644
index 000000000..069771e56
--- /dev/null
+++ b/ui/tests/pages/components/message-in-page.js
@@ -0,0 +1,5 @@
+import { text } from 'ember-cli-page-object';
+
+export default {
+ errorText: text('[data-test-error]'),
+};
diff --git a/ui/tests/pages/components/mount-backend-form.js b/ui/tests/pages/components/mount-backend-form.js
new file mode 100644
index 000000000..3c0a1d193
--- /dev/null
+++ b/ui/tests/pages/components/mount-backend-form.js
@@ -0,0 +1,14 @@
+import { clickable, fillable, text, value } from 'ember-cli-page-object';
+import fields from './form-field';
+import errorText from './message-in-page';
+
+export default {
+ ...fields,
+ ...errorText,
+ header: text('[data-test-mount-form-header]'),
+ submit: clickable('[data-test-mount-submit]'),
+ path: fillable('[data-test-input="path"]'),
+ pathValue: value('[data-test-input="path"]'),
+ type: fillable('[data-test-input="type"]'),
+ typeValue: value('[data-test-input="type"]'),
+};
diff --git a/ui/tests/pages/policies/create.js b/ui/tests/pages/policies/create.js
new file mode 100644
index 000000000..6cad29919
--- /dev/null
+++ b/ui/tests/pages/policies/create.js
@@ -0,0 +1,4 @@
+import { create, visitable } from 'ember-cli-page-object';
+export default create({
+ visit: visitable('/vault/policies/create'),
+});
diff --git a/ui/tests/pages/policies/index.js b/ui/tests/pages/policies/index.js
new file mode 100644
index 000000000..f058be660
--- /dev/null
+++ b/ui/tests/pages/policies/index.js
@@ -0,0 +1,13 @@
+import { text, create, collection, visitable } from 'ember-cli-page-object';
+export default create({
+ visit: visitable('/vault/policies/:type'),
+ policies: collection({
+ itemScope: '[data-test-policy-item]',
+ item: {
+ name: text('[data-test-policy-name]'),
+ },
+ findByName(name) {
+ return this.toArray().findBy('name', name);
+ },
+ }),
+});
diff --git a/ui/tests/pages/policy/edit.js b/ui/tests/pages/policy/edit.js
new file mode 100644
index 000000000..8250b3d72
--- /dev/null
+++ b/ui/tests/pages/policy/edit.js
@@ -0,0 +1,6 @@
+import { clickable, create, isPresent, visitable } from 'ember-cli-page-object';
+export default create({
+ visit: visitable('/vault/policy/:type/:name/edit'),
+ deleteIsPresent: isPresent('[data-test-policy-delete]'),
+ toggleEdit: clickable('[data-test-policy-edit-toggle]'),
+});
diff --git a/ui/tests/pages/policy/show.js b/ui/tests/pages/policy/show.js
new file mode 100644
index 000000000..ef82d4ad0
--- /dev/null
+++ b/ui/tests/pages/policy/show.js
@@ -0,0 +1,5 @@
+import { clickable, create, visitable } from 'ember-cli-page-object';
+export default create({
+ visit: visitable('/vault/policy/:type/:name'),
+ toggleEdit: clickable('[data-test-policy-edit-toggle]'),
+});
diff --git a/ui/tests/pages/secrets/backend/create.js b/ui/tests/pages/secrets/backend/create.js
new file mode 100644
index 000000000..235499247
--- /dev/null
+++ b/ui/tests/pages/secrets/backend/create.js
@@ -0,0 +1,7 @@
+import { create, visitable } from 'ember-cli-page-object';
+
+export const Base = {
+ visit: visitable('/vault/secrets/:backend/create/:id'),
+ visitRoot: visitable('/vault/secrets/:backend/create'),
+};
+export default create(Base);
diff --git a/ui/tests/pages/secrets/backend/credentials.js b/ui/tests/pages/secrets/backend/credentials.js
new file mode 100644
index 000000000..9383d6009
--- /dev/null
+++ b/ui/tests/pages/secrets/backend/credentials.js
@@ -0,0 +1,8 @@
+import { create, visitable } from 'ember-cli-page-object';
+
+export const Base = {
+ visit: visitable('/vault/secrets/:backend/credentials/:id'),
+ visitRoot: visitable('/vault/secrets/:backend/credentials'),
+};
+
+export default create(Base);
diff --git a/ui/tests/pages/secrets/backend/edit.js b/ui/tests/pages/secrets/backend/edit.js
new file mode 100644
index 000000000..029e8cbcb
--- /dev/null
+++ b/ui/tests/pages/secrets/backend/edit.js
@@ -0,0 +1,6 @@
+import { create, visitable } from 'ember-cli-page-object';
+
+export default create({
+ visit: visitable('/vault/secrets/:backend/edit/:id'),
+ visitRoot: visitable('/vault/secrets/:backend/edit'),
+});
diff --git a/ui/tests/pages/secrets/backend/kv/edit-secret.js b/ui/tests/pages/secrets/backend/kv/edit-secret.js
new file mode 100644
index 000000000..1686eede2
--- /dev/null
+++ b/ui/tests/pages/secrets/backend/kv/edit-secret.js
@@ -0,0 +1,21 @@
+import { Base } from '../create';
+import { clickable, visitable, create, fillable } from 'ember-cli-page-object';
+
+export default create({
+ ...Base,
+ path: fillable('[data-test-secret-path]'),
+ secretKey: fillable('[data-test-secret-key]'),
+ secretValue: fillable('[data-test-secret-value]'),
+ save: clickable('[data-test-secret-save]'),
+ deleteBtn: clickable('[data-test-secret-delete] button'),
+ confirmBtn: clickable('[data-test-confirm-button]'),
+ visitEdit: visitable('/vault/secrets/:backend/edit/:id'),
+ visitEditRoot: visitable('/vault/secrets/:backend/edit'),
+ deleteSecret() {
+ return this.deleteBtn().confirmBtn();
+ },
+
+ createSecret(path, key, value) {
+ return this.path(path).secretKey(key).secretValue(value).save();
+ },
+});
diff --git a/ui/tests/pages/secrets/backend/kv/show.js b/ui/tests/pages/secrets/backend/kv/show.js
new file mode 100644
index 000000000..468fa8fe1
--- /dev/null
+++ b/ui/tests/pages/secrets/backend/kv/show.js
@@ -0,0 +1,11 @@
+import { Base } from '../show';
+import { create, clickable, collection, isPresent } from 'ember-cli-page-object';
+
+export default create({
+ ...Base,
+ rows: collection({
+ scope: 'data-test-row-label',
+ }),
+ edit: clickable('[data-test-secret-json-toggle]'),
+ editIsPresent: isPresent('[data-test-secret-json-toggle]'),
+});
diff --git a/ui/tests/pages/secrets/backend/list.js b/ui/tests/pages/secrets/backend/list.js
new file mode 100644
index 000000000..bd4588c11
--- /dev/null
+++ b/ui/tests/pages/secrets/backend/list.js
@@ -0,0 +1,23 @@
+import { create, collection, visitable, clickable, isPresent } from 'ember-cli-page-object';
+import { getter } from 'ember-cli-page-object/macros';
+
+export default create({
+ visit: visitable('/vault/secrets/:backend/list/:id'),
+ visitRoot: visitable('/vault/secrets/:backend/list'),
+ create: clickable('[data-test-secret-create]'),
+ createIsPresent: isPresent('[data-test-secret-create]'),
+ configure: clickable('[data-test-secret-backend-configure]'),
+ configureIsPresent: isPresent('[data-test-secret-backend-configure]'),
+
+ tabs: collection({
+ itemScope: '[data-test-tab]',
+ }),
+
+ secrets: collection({
+ itemScope: '[data-test-secret-link]',
+ }),
+
+ backendIsEmpty: getter(function() {
+ return this.secrets().count === 0;
+ }),
+});
diff --git a/ui/tests/pages/secrets/backend/pki/edit-role.js b/ui/tests/pages/secrets/backend/pki/edit-role.js
new file mode 100644
index 000000000..23935be3e
--- /dev/null
+++ b/ui/tests/pages/secrets/backend/pki/edit-role.js
@@ -0,0 +1,28 @@
+import { Base } from '../create';
+import { clickable, visitable, create, fillable } from 'ember-cli-page-object';
+
+export default create({
+ ...Base,
+ visitEdit: visitable('/vault/secrets/:backend/edit/:id'),
+ visitEditRoot: visitable('/vault/secrets/:backend/edit'),
+ toggleDomain: clickable('[data-test-toggle-group="Domain Handling"]'),
+ toggleOptions: clickable('[data-test-toggle-group="Options"]'),
+ name: fillable('[data-test-input="name"]'),
+ allowAnyName: clickable('[data-test-input="allowAnyName"]'),
+ allowedDomains: fillable('[data-test-input="allowedDomains"]'),
+ save: clickable('[data-test-role-create]'),
+ deleteBtn: clickable('[data-test-role-delete] button'),
+ confirmBtn: clickable('[data-test-confirm-button]'),
+ deleteRole() {
+ return this.deleteBtn().confirmBtn();
+ },
+
+ createRole(name, allowedDomains) {
+ return this.toggleDomain()
+ .toggleOptions()
+ .name(name)
+ .allowAnyName()
+ .allowedDomains(allowedDomains)
+ .save();
+ },
+});
diff --git a/ui/tests/pages/secrets/backend/pki/generate-cert.js b/ui/tests/pages/secrets/backend/pki/generate-cert.js
new file mode 100644
index 000000000..717a51fde
--- /dev/null
+++ b/ui/tests/pages/secrets/backend/pki/generate-cert.js
@@ -0,0 +1,23 @@
+import { Base } from '../credentials';
+import { clickable, text, value, create, fillable, isPresent } from 'ember-cli-page-object';
+
+export default create({
+ ...Base,
+ title: text('[data-test-title]'),
+ commonName: fillable('[data-test-input="commonName"]'),
+ commonNameValue: value('[data-test-input="commonName"]'),
+ csr: fillable('[data-test-input="csr"]'),
+ submit: clickable('[data-test-secret-generate]'),
+ back: clickable('[data-test-secret-generate-back]'),
+ certificate: text('[data-test-row-value="Certificate"]'),
+ toggleOptions: clickable('[data-test-toggle-group]'),
+ hasCert: isPresent('[data-test-row-value="Certificate"]'),
+ fillInField: fillable('[data-test-field]'),
+ issueCert(commonName) {
+ return this.commonName(commonName).toggleOptions().fillInField('unit', 'h').submit();
+ },
+
+ sign(commonName, csr) {
+ return this.csr(csr).commonName(commonName).toggleOptions().fillInField('unit', 'h').submit();
+ },
+});
diff --git a/ui/tests/pages/secrets/backend/pki/show.js b/ui/tests/pages/secrets/backend/pki/show.js
new file mode 100644
index 000000000..0eec10171
--- /dev/null
+++ b/ui/tests/pages/secrets/backend/pki/show.js
@@ -0,0 +1,17 @@
+import { Base } from '../show';
+import { create, clickable, collection, text, isPresent } from 'ember-cli-page-object';
+
+export default create({
+ ...Base,
+ rows: collection({
+ scope: 'data-test-row-label',
+ }),
+ certificate: text('[data-test-row-value="Certificate"]'),
+ hasCert: isPresent('[data-test-row-value="Certificate"]'),
+ edit: clickable('[data-test-edit-link]'),
+ editIsPresent: isPresent('[data-test-edit-link]'),
+ generateCert: clickable('[data-test-credentials-link]'),
+ generateCertIsPresent: isPresent('[data-test-credentials-link]'),
+ signCert: clickable('[data-test-sign-link]'),
+ signCertIsPresent: isPresent('[data-test-sign-link]'),
+});
diff --git a/ui/tests/pages/secrets/backend/show.js b/ui/tests/pages/secrets/backend/show.js
new file mode 100644
index 000000000..78acd3ae6
--- /dev/null
+++ b/ui/tests/pages/secrets/backend/show.js
@@ -0,0 +1,7 @@
+import { create, visitable } from 'ember-cli-page-object';
+
+export const Base = {
+ visit: visitable('/vault/secrets/:backend/show/:id'),
+ visitRoot: visitable('/vault/secrets/:backend/show'),
+};
+export default create(Base);
diff --git a/ui/tests/pages/secrets/backends.js b/ui/tests/pages/secrets/backends.js
new file mode 100644
index 000000000..9e3ac1899
--- /dev/null
+++ b/ui/tests/pages/secrets/backends.js
@@ -0,0 +1,17 @@
+import { create, visitable, collection, text, clickable } from 'ember-cli-page-object';
+
+export default create({
+ visit: visitable('/vault/secrets'),
+ links: collection({
+ itemScope: '[data-test-secret-backend-link]',
+ item: {
+ path: text('[data-test-secret-path]'),
+ toggleDetails: clickable('[data-test-secret-backend-detail]'),
+ defaultTTL: text('[data-test-secret-backend-details="default-ttl"]'),
+ maxTTL: text('[data-test-secret-backend-details="max-ttl"]'),
+ },
+ findByPath(path) {
+ return this.toArray().findBy('path', path + '/');
+ },
+ }),
+});
diff --git a/ui/tests/pages/settings/auth/configure/index.js b/ui/tests/pages/settings/auth/configure/index.js
new file mode 100644
index 000000000..9058ff08d
--- /dev/null
+++ b/ui/tests/pages/settings/auth/configure/index.js
@@ -0,0 +1,5 @@
+import { create, visitable } from 'ember-cli-page-object';
+
+export default create({
+ visit: visitable('/vault/settings/auth/configure/:path'),
+});
diff --git a/ui/tests/pages/settings/auth/configure/section.js b/ui/tests/pages/settings/auth/configure/section.js
new file mode 100644
index 000000000..6d6a342a4
--- /dev/null
+++ b/ui/tests/pages/settings/auth/configure/section.js
@@ -0,0 +1,10 @@
+import { create, clickable, visitable } from 'ember-cli-page-object';
+import fields from '../../../components/form-field';
+import flashMessage from '../../../components/flash-message';
+
+export default create({
+ ...fields,
+ visit: visitable('/vault/settings/auth/configure/:path/:section'),
+ flash: flashMessage,
+ save: clickable('[data-test-save-config]'),
+});
diff --git a/ui/tests/pages/settings/auth/enable.js b/ui/tests/pages/settings/auth/enable.js
new file mode 100644
index 000000000..db36c41a0
--- /dev/null
+++ b/ui/tests/pages/settings/auth/enable.js
@@ -0,0 +1,15 @@
+import { create, visitable } from 'ember-cli-page-object';
+import backendForm from '../../components/mount-backend-form';
+import flashMessages from '../../components/flash-message';
+
+export default create({
+ visit: visitable('/vault/settings/auth/enable'),
+ form: backendForm,
+ flash: flashMessages,
+ enableAuth(type, path) {
+ if (path) {
+ return this.form.path(path).type(type).submit();
+ }
+ return this.form.type(type).submit();
+ },
+});
diff --git a/ui/tests/pages/settings/configure-secret-backends/pki/index.js b/ui/tests/pages/settings/configure-secret-backends/pki/index.js
new file mode 100644
index 000000000..4126da388
--- /dev/null
+++ b/ui/tests/pages/settings/configure-secret-backends/pki/index.js
@@ -0,0 +1,5 @@
+import { create, visitable } from 'ember-cli-page-object';
+
+export default create({
+ visit: visitable('/vault/settings/secrets/configure/:backend/'),
+});
diff --git a/ui/tests/pages/settings/configure-secret-backends/pki/section-cert.js b/ui/tests/pages/settings/configure-secret-backends/pki/section-cert.js
new file mode 100644
index 000000000..c2a5b01c0
--- /dev/null
+++ b/ui/tests/pages/settings/configure-secret-backends/pki/section-cert.js
@@ -0,0 +1,10 @@
+import { create, visitable } from 'ember-cli-page-object';
+
+import ConfigPKICA from 'vault/tests/pages/components/config-pki-ca';
+import flashMessages from 'vault/tests/pages/components/flash-message';
+
+export default create({
+ visit: visitable('/vault/settings/secrets/configure/:backend/cert'),
+ form: ConfigPKICA,
+ flash: flashMessages,
+});
diff --git a/ui/tests/pages/settings/configure-secret-backends/pki/section.js b/ui/tests/pages/settings/configure-secret-backends/pki/section.js
new file mode 100644
index 000000000..cfb2d66b1
--- /dev/null
+++ b/ui/tests/pages/settings/configure-secret-backends/pki/section.js
@@ -0,0 +1,16 @@
+import { create, visitable, collection } from 'ember-cli-page-object';
+
+import { getter } from 'ember-cli-page-object/macros';
+import ConfigPKI from 'vault/tests/pages/components/config-pki';
+
+export default create({
+ visit: visitable('/vault/settings/secrets/configure/:backend/:section'),
+ form: ConfigPKI,
+ lastMessage: getter(function() {
+ const count = this.flashMessages().count;
+ return this.flashMessages(count - 1).text;
+ }),
+ flashMessages: collection({
+ itemScope: '[data-test-flash-message-body]',
+ }),
+});
diff --git a/ui/tests/pages/settings/mount-secret-backend.js b/ui/tests/pages/settings/mount-secret-backend.js
new file mode 100644
index 000000000..c4aef5b5b
--- /dev/null
+++ b/ui/tests/pages/settings/mount-secret-backend.js
@@ -0,0 +1,13 @@
+import { create, visitable, fillable, clickable } from 'ember-cli-page-object';
+
+export default create({
+ visit: visitable('/vault/settings/mount-secret-backend'),
+ type: fillable('[data-test-secret-backend-type]'),
+ path: fillable('[data-test-secret-backend-path]'),
+ submit: clickable('[data-test-secret-backend-submit]'),
+ toggleOptions: clickable('[data-test-secret-backend-options]'),
+ maxTTLVal: fillable('[data-test-ttl-value]', { scope: '[data-test-secret-backend-max-ttl]' }),
+ maxTTLUnit: fillable('[data-test-ttl-unit]', { scope: '[data-test-secret-backend-max-ttl]' }),
+ defaultTTLVal: fillable('[data-test-ttl-value]', { scope: '[data-test-secret-backend-default-ttl]' }),
+ defaultTTLUnit: fillable('[data-test-ttl-unit]', { scope: '[data-test-secret-backend-default-ttl]' }),
+});
diff --git a/ui/tests/test-helper.js b/ui/tests/test-helper.js
new file mode 100644
index 000000000..0680db4ea
--- /dev/null
+++ b/ui/tests/test-helper.js
@@ -0,0 +1,8 @@
+import resolver from './helpers/resolver';
+import './helpers/flash-message';
+
+import { setResolver } from 'ember-qunit';
+import { start } from 'ember-cli-qunit';
+
+setResolver(resolver);
+start();
diff --git a/ui/tests/unit/.gitkeep b/ui/tests/unit/.gitkeep
new file mode 100644
index 000000000..e69de29bb
diff --git a/ui/tests/unit/adapters/capabilities-test.js b/ui/tests/unit/adapters/capabilities-test.js
new file mode 100644
index 000000000..9f1ef2846
--- /dev/null
+++ b/ui/tests/unit/adapters/capabilities-test.js
@@ -0,0 +1,21 @@
+import { moduleFor, test } from 'ember-qunit';
+import Ember from 'ember';
+
+moduleFor('adapter:capabilities', 'Unit | Adapter | capabilities', {
+ needs: ['service:auth', 'service:flash-messages'],
+});
+
+test('calls the correct url', function(assert) {
+ let url, method, options;
+ let adapter = this.subject({
+ ajax: (...args) => {
+ [url, method, options] = args;
+ return Ember.RSVP.resolve();
+ },
+ });
+
+ adapter.findRecord(null, 'capabilities', 'foo');
+ assert.equal('/v1/sys/capabilities-self', url, 'calls the correct URL');
+ assert.deepEqual({ path: 'foo' }, options.data, 'data params OK');
+ assert.equal('POST', method, 'method OK');
+});
diff --git a/ui/tests/unit/adapters/cluster-test.js b/ui/tests/unit/adapters/cluster-test.js
new file mode 100644
index 000000000..d94169913
--- /dev/null
+++ b/ui/tests/unit/adapters/cluster-test.js
@@ -0,0 +1,196 @@
+import { moduleFor, test } from 'ember-qunit';
+import Ember from 'ember';
+
+moduleFor('adapter:cluster', 'Unit | Adapter | cluster', {
+ needs: ['service:auth', 'service:flash-messages', 'service:version'],
+});
+
+test('cluster api urls', function(assert) {
+ let url, method, options;
+ let adapter = this.subject({
+ ajax: (...args) => {
+ [url, method, options] = args;
+ return Ember.RSVP.resolve();
+ },
+ });
+ adapter.health();
+ assert.equal('/v1/sys/health', url, 'health url OK');
+ assert.deepEqual(
+ { standbycode: 200, sealedcode: 200, uninitcode: 200, drsecondarycode: 200 },
+ options.data,
+ 'health data params OK'
+ );
+ assert.equal('GET', method, 'health method OK');
+
+ adapter.sealStatus();
+ assert.equal('/v1/sys/seal-status', url, 'health url OK');
+ assert.equal('GET', method, 'seal-status method OK');
+
+ let data = { someData: 1 };
+ adapter.unseal(data);
+ assert.equal('/v1/sys/unseal', url, 'unseal url OK');
+ assert.equal('PUT', method, 'unseal method OK');
+ assert.deepEqual({ data, unauthenticated: true }, options, 'unseal options OK');
+
+ adapter.initCluster(data);
+ assert.equal('/v1/sys/init', url, 'init url OK');
+ assert.equal('PUT', method, 'init method OK');
+ assert.deepEqual({ data, unauthenticated: true }, options, 'init options OK');
+
+ data = { token: 'token', password: 'password', username: 'username' };
+
+ adapter.authenticate({ backend: 'token', data });
+ assert.equal('/v1/auth/token/lookup-self', url, 'auth:token url OK');
+ assert.equal('GET', method, 'auth:token method OK');
+ assert.deepEqual(
+ { headers: { 'X-Vault-Token': 'token' }, unauthenticated: true },
+ options,
+ 'auth:token options OK'
+ );
+
+ adapter.authenticate({ backend: 'github', data });
+ assert.equal('/v1/auth/github/login', url, 'auth:github url OK');
+ assert.equal('POST', method, 'auth:github method OK');
+ assert.deepEqual(
+ { data: { password: 'password', token: 'token' }, unauthenticated: true },
+ options,
+ 'auth:github options OK'
+ );
+
+ data = { token: 'token', password: 'password', username: 'username', path: 'path' };
+
+ adapter.authenticate({ backend: 'token', data });
+ assert.equal('/v1/auth/token/lookup-self', url, 'auth:token url with path OK');
+
+ adapter.authenticate({ backend: 'github', data });
+ assert.equal('/v1/auth/path/login', url, 'auth:github with path url OK');
+
+ data = { password: 'password', username: 'username' };
+
+ adapter.authenticate({ backend: 'userpass', data });
+ assert.equal('/v1/auth/userpass/login/username', url, 'auth:userpass url OK');
+ assert.equal('POST', method, 'auth:userpass method OK');
+ assert.deepEqual(
+ { data: { password: 'password' }, unauthenticated: true },
+ options,
+ 'auth:userpass options OK'
+ );
+
+ adapter.authenticate({ backend: 'LDAP', data });
+ assert.equal('/v1/auth/ldap/login/username', url, 'ldap:userpass url OK');
+ assert.equal('POST', method, 'ldap:userpass method OK');
+ assert.deepEqual(
+ { data: { password: 'password' }, unauthenticated: true },
+ options,
+ 'ldap:userpass options OK'
+ );
+
+ adapter.authenticate({ backend: 'okta', data });
+ assert.equal('/v1/auth/okta/login/username', url, 'okta:userpass url OK');
+ assert.equal('POST', method, 'ldap:userpass method OK');
+ assert.deepEqual(
+ { data: { password: 'password' }, unauthenticated: true },
+ options,
+ 'okta:userpass options OK'
+ );
+
+ // use a custom mount path
+ data = { password: 'password', username: 'username', path: 'path' };
+
+ adapter.authenticate({ backend: 'userpass', data });
+ assert.equal('/v1/auth/path/login/username', url, 'auth:userpass with path url OK');
+
+ adapter.authenticate({ backend: 'LDAP', data });
+ assert.equal('/v1/auth/path/login/username', url, 'auth:LDAP with path url OK');
+
+ adapter.authenticate({ backend: 'Okta', data });
+ assert.equal('/v1/auth/path/login/username', url, 'auth:Okta with path url OK');
+});
+
+test('cluster replication api urls', function(assert) {
+ let url, method, options;
+ let adapter = this.subject({
+ ajax: (...args) => {
+ [url, method, options] = args;
+ return Ember.RSVP.resolve();
+ },
+ });
+
+ adapter.replicationStatus();
+ assert.equal('/v1/sys/replication/status', url, 'replication:status url OK');
+ assert.equal('GET', method, 'replication:status method OK');
+ assert.deepEqual({ unauthenticated: true }, options, 'replication:status options OK');
+
+ adapter.replicationAction('recover', 'dr');
+ assert.equal('/v1/sys/replication/recover', url, 'replication: recover url OK');
+ assert.equal('POST', method, 'replication:recover method OK');
+
+ adapter.replicationAction('reindex', 'dr');
+ assert.equal('/v1/sys/replication/reindex', url, 'replication: reindex url OK');
+ assert.equal('POST', method, 'replication:reindex method OK');
+
+ adapter.replicationAction('enable', 'dr', 'primary');
+ assert.equal('/v1/sys/replication/dr/primary/enable', url, 'replication:dr primary:enable url OK');
+ assert.equal('POST', method, 'replication:primary:enable method OK');
+ adapter.replicationAction('enable', 'performance', 'primary');
+ assert.equal(
+ '/v1/sys/replication/performance/primary/enable',
+ url,
+ 'replication:performance primary:enable url OK'
+ );
+
+ adapter.replicationAction('enable', 'dr', 'secondary');
+ assert.equal('/v1/sys/replication/dr/secondary/enable', url, 'replication:dr secondary:enable url OK');
+ assert.equal('POST', method, 'replication:secondary:enable method OK');
+ adapter.replicationAction('enable', 'performance', 'secondary');
+ assert.equal(
+ '/v1/sys/replication/performance/secondary/enable',
+ url,
+ 'replication:performance secondary:enable url OK'
+ );
+
+ adapter.replicationAction('disable', 'dr', 'primary');
+ assert.equal('/v1/sys/replication/dr/primary/disable', url, 'replication:dr primary:disable url OK');
+ assert.equal('POST', method, 'replication:primary:disable method OK');
+ adapter.replicationAction('disable', 'performance', 'primary');
+ assert.equal(
+ '/v1/sys/replication/performance/primary/disable',
+ url,
+ 'replication:performance primary:disable url OK'
+ );
+
+ adapter.replicationAction('disable', 'dr', 'secondary');
+ assert.equal('/v1/sys/replication/dr/secondary/disable', url, 'replication: drsecondary:disable url OK');
+ assert.equal('POST', method, 'replication:secondary:disable method OK');
+ adapter.replicationAction('disable', 'performance', 'secondary');
+ assert.equal(
+ '/v1/sys/replication/performance/secondary/disable',
+ url,
+ 'replication: performance:disable url OK'
+ );
+
+ adapter.replicationAction('demote', 'dr', 'primary');
+ assert.equal('/v1/sys/replication/dr/primary/demote', url, 'replication: dr primary:demote url OK');
+ assert.equal('POST', method, 'replication:primary:demote method OK');
+ adapter.replicationAction('demote', 'performance', 'primary');
+ assert.equal(
+ '/v1/sys/replication/performance/primary/demote',
+ url,
+ 'replication: performance primary:demote url OK'
+ );
+
+ adapter.replicationAction('promote', 'performance', 'secondary');
+ assert.equal('POST', method, 'replication:secondary:promote method OK');
+ assert.equal(
+ '/v1/sys/replication/performance/secondary/promote',
+ url,
+ 'replication:performance secondary:promote url OK'
+ );
+
+ adapter.replicationDrPromote();
+ assert.equal('/v1/sys/replication/dr/secondary/promote', url, 'replication:dr secondary:promote url OK');
+ assert.equal('PUT', method, 'replication:dr secondary:promote method OK');
+ adapter.replicationDrPromote({}, { checkStatus: true });
+ assert.equal('/v1/sys/replication/dr/secondary/promote', url, 'replication:dr secondary:promote url OK');
+ assert.equal('GET', method, 'replication:dr secondary:promote method OK');
+});
diff --git a/ui/tests/unit/adapters/identity/_test-cases.js b/ui/tests/unit/adapters/identity/_test-cases.js
new file mode 100644
index 000000000..b769bc795
--- /dev/null
+++ b/ui/tests/unit/adapters/identity/_test-cases.js
@@ -0,0 +1,43 @@
+export const storeMVP = {
+ serializerFor() {
+ return {
+ serializeIntoHash() {},
+ };
+ },
+};
+
+export default function(modelName) {
+ return [
+ {
+ adapterMethod: 'findRecord',
+ args: [null, { modelName }, 'foo'],
+ url: `/v1/${modelName}/id/foo`,
+ method: 'GET',
+ },
+
+ {
+ adapterMethod: 'createRecord',
+ args: [storeMVP, { modelName }],
+ url: `/v1/${modelName}`,
+ method: 'POST',
+ },
+ {
+ adapterMethod: 'updateRecord',
+ args: [storeMVP, { modelName }, { id: 'foo' }],
+ url: `/v1/${modelName}/id/foo`,
+ method: 'PUT',
+ },
+ {
+ adapterMethod: 'deleteRecord',
+ args: [storeMVP, { modelName }, { id: 'foo' }],
+ url: `/v1/${modelName}/id/foo`,
+ method: 'DELETE',
+ },
+ {
+ adapterMethod: 'query',
+ args: [null, { modelName }, {}],
+ url: `/v1/${modelName}/id?list=true`,
+ method: 'GET',
+ },
+ ];
+}
diff --git a/ui/tests/unit/adapters/identity/entity-alias-test.js b/ui/tests/unit/adapters/identity/entity-alias-test.js
new file mode 100644
index 000000000..7a5bd30cb
--- /dev/null
+++ b/ui/tests/unit/adapters/identity/entity-alias-test.js
@@ -0,0 +1,41 @@
+import Pretender from 'pretender';
+import { moduleFor, test } from 'ember-qunit';
+import testCases from './_test-cases';
+
+const noop = response => {
+ return function() {
+ return [response, { 'Content-Type': 'application/json' }, JSON.stringify({})];
+ };
+};
+
+moduleFor('adapter:identity/entity-alias', 'Unit | Adapter | identity/entity-alias', {
+ needs: ['service:auth', 'service:flash-messages'],
+ beforeEach() {
+ this.server = new Pretender(function() {
+ this.post('/v1/**', noop());
+ this.put('/v1/**', noop());
+ this.get('/v1/**', noop());
+ this.delete('/v1/**', noop(204));
+ });
+ },
+ afterEach() {
+ this.server.shutdown();
+ },
+});
+
+const cases = testCases('identit/entity-alias');
+
+cases.forEach(testCase => {
+ test(`entity-alias#${testCase.adapterMethod}`, function(assert) {
+ assert.expect(2);
+ let adapter = this.subject();
+ adapter[testCase.adapterMethod](...testCase.args);
+ let { url, method } = this.server.handledRequests[0];
+ assert.equal(url, testCase.url, `${testCase.adapterMethod} calls the correct url: ${testCase.url}`);
+ assert.equal(
+ method,
+ testCase.method,
+ `${testCase.adapterMethod} uses the correct http verb: ${testCase.method}`
+ );
+ });
+});
diff --git a/ui/tests/unit/adapters/identity/entity-merge-test.js b/ui/tests/unit/adapters/identity/entity-merge-test.js
new file mode 100644
index 000000000..118731e5b
--- /dev/null
+++ b/ui/tests/unit/adapters/identity/entity-merge-test.js
@@ -0,0 +1,26 @@
+import Pretender from 'pretender';
+import { moduleFor, test } from 'ember-qunit';
+import { storeMVP } from './_test-cases';
+
+moduleFor('adapter:identity/entity-merge', 'Unit | Adapter | identity/entity-merge', {
+ needs: ['service:auth', 'service:flash-messages'],
+ beforeEach() {
+ this.server = new Pretender(function() {
+ this.post('/v1/**', response => {
+ return [response, { 'Content-Type': 'application/json' }, JSON.stringify({})];
+ });
+ });
+ },
+ afterEach() {
+ this.server.shutdown();
+ },
+});
+
+test(`entity-merge#createRecord`, function(assert) {
+ assert.expect(2);
+ let adapter = this.subject();
+ adapter.createRecord(storeMVP, { modelName: 'identity/entity-merge' }, { attr: x => x });
+ let { url, method } = this.server.handledRequests[0];
+ assert.equal(url, `/v1/identity/entity/merge`, ` calls the correct url`);
+ assert.equal(method, 'POST', `uses the correct http verb: POST`);
+});
diff --git a/ui/tests/unit/adapters/identity/entity-test.js b/ui/tests/unit/adapters/identity/entity-test.js
new file mode 100644
index 000000000..064f66e95
--- /dev/null
+++ b/ui/tests/unit/adapters/identity/entity-test.js
@@ -0,0 +1,41 @@
+import Pretender from 'pretender';
+import { moduleFor, test } from 'ember-qunit';
+import testCases from './_test-cases';
+
+const noop = response => {
+ return function() {
+ return [response, { 'Content-Type': 'application/json' }, JSON.stringify({})];
+ };
+};
+
+moduleFor('adapter:identity/entity', 'Unit | Adapter | identity/entity', {
+ needs: ['service:auth', 'service:flash-messages'],
+ beforeEach() {
+ this.server = new Pretender(function() {
+ this.post('/v1/**', noop());
+ this.put('/v1/**', noop());
+ this.get('/v1/**', noop());
+ this.delete('/v1/**', noop(204));
+ });
+ },
+ afterEach() {
+ this.server.shutdown();
+ },
+});
+
+const cases = testCases('identit/entity');
+
+cases.forEach(testCase => {
+ test(`entity#${testCase.adapterMethod}`, function(assert) {
+ assert.expect(2);
+ let adapter = this.subject();
+ adapter[testCase.adapterMethod](...testCase.args);
+ let { url, method } = this.server.handledRequests[0];
+ assert.equal(url, testCase.url, `${testCase.adapterMethod} calls the correct url: ${testCase.url}`);
+ assert.equal(
+ method,
+ testCase.method,
+ `${testCase.adapterMethod} uses the correct http verb: ${testCase.method}`
+ );
+ });
+});
diff --git a/ui/tests/unit/adapters/identity/group-alias-test.js b/ui/tests/unit/adapters/identity/group-alias-test.js
new file mode 100644
index 000000000..e1ace27f8
--- /dev/null
+++ b/ui/tests/unit/adapters/identity/group-alias-test.js
@@ -0,0 +1,41 @@
+import Pretender from 'pretender';
+import { moduleFor, test } from 'ember-qunit';
+import testCases from './_test-cases';
+
+const noop = response => {
+ return function() {
+ return [response, { 'Content-Type': 'application/json' }, JSON.stringify({})];
+ };
+};
+
+moduleFor('adapter:identity/group-alias', 'Unit | Adapter | identity/group-alias', {
+ needs: ['service:auth', 'service:flash-messages'],
+ beforeEach() {
+ this.server = new Pretender(function() {
+ this.post('/v1/**', noop());
+ this.put('/v1/**', noop());
+ this.get('/v1/**', noop());
+ this.delete('/v1/**', noop(204));
+ });
+ },
+ afterEach() {
+ this.server.shutdown();
+ },
+});
+
+const cases = testCases('identity/group-alias');
+
+cases.forEach(testCase => {
+ test(`group-alias#${testCase.adapterMethod}`, function(assert) {
+ assert.expect(2);
+ let adapter = this.subject();
+ adapter[testCase.adapterMethod](...testCase.args);
+ let { url, method } = this.server.handledRequests[0];
+ assert.equal(url, testCase.url, `${testCase.adapterMethod} calls the correct url: ${testCase.url}`);
+ assert.equal(
+ method,
+ testCase.method,
+ `${testCase.adapterMethod} uses the correct http verb: ${testCase.method}`
+ );
+ });
+});
diff --git a/ui/tests/unit/adapters/identity/group-test.js b/ui/tests/unit/adapters/identity/group-test.js
new file mode 100644
index 000000000..edc0f69bc
--- /dev/null
+++ b/ui/tests/unit/adapters/identity/group-test.js
@@ -0,0 +1,41 @@
+import Pretender from 'pretender';
+import { moduleFor, test } from 'ember-qunit';
+import testCases from './_test-cases';
+
+const noop = response => {
+ return function() {
+ return [response, { 'Content-Type': 'application/json' }, JSON.stringify({})];
+ };
+};
+
+moduleFor('adapter:identity/group', 'Unit | Adapter | identity/group', {
+ needs: ['service:auth', 'service:flash-messages'],
+ beforeEach() {
+ this.server = new Pretender(function() {
+ this.post('/v1/**', noop());
+ this.put('/v1/**', noop());
+ this.get('/v1/**', noop());
+ this.delete('/v1/**', noop(204));
+ });
+ },
+ afterEach() {
+ this.server.shutdown();
+ },
+});
+
+const cases = testCases('identit/entity');
+
+cases.forEach(testCase => {
+ test(`group#${testCase.adapterMethod}`, function(assert) {
+ assert.expect(2);
+ let adapter = this.subject();
+ adapter[testCase.adapterMethod](...testCase.args);
+ let { url, method } = this.server.handledRequests[0];
+ assert.equal(url, testCase.url, `${testCase.adapterMethod} calls the correct url: ${testCase.url}`);
+ assert.equal(
+ method,
+ testCase.method,
+ `${testCase.adapterMethod} uses the correct http verb: ${testCase.method}`
+ );
+ });
+});
diff --git a/ui/tests/unit/adapters/secret-cubbyhole-test.js b/ui/tests/unit/adapters/secret-cubbyhole-test.js
new file mode 100644
index 000000000..6805b3c7b
--- /dev/null
+++ b/ui/tests/unit/adapters/secret-cubbyhole-test.js
@@ -0,0 +1,25 @@
+import { moduleFor, test } from 'ember-qunit';
+import Ember from 'ember';
+
+moduleFor('adapter:secret-cubbyhole', 'Unit | Adapter | secret-cubbyhole', {
+ needs: ['service:auth', 'service:flash-messages'],
+});
+
+test('secret api urls', function(assert) {
+ let url, method, options;
+ let adapter = this.subject({
+ ajax: (...args) => {
+ [url, method, options] = args;
+ return Ember.RSVP.resolve({});
+ },
+ });
+
+ adapter.query({}, 'secret', { id: '', backend: 'secret' });
+ assert.equal(url, '/v1/secret/', 'query generic url OK');
+ assert.equal('GET', method, 'query generic method OK');
+ assert.deepEqual(options, { data: { list: true } }, 'query generic url OK');
+
+ adapter.queryRecord({}, 'secret', { id: 'foo', backend: 'secret' });
+ assert.equal(url, '/v1/secret/foo', 'queryRecord generic url OK');
+ assert.equal('GET', method, 'queryRecord generic method OK');
+});
diff --git a/ui/tests/unit/adapters/secret-test.js b/ui/tests/unit/adapters/secret-test.js
new file mode 100644
index 000000000..36bf05a87
--- /dev/null
+++ b/ui/tests/unit/adapters/secret-test.js
@@ -0,0 +1,25 @@
+import { moduleFor, test } from 'ember-qunit';
+import Ember from 'ember';
+
+moduleFor('adapter:secret', 'Unit | Adapter | secret', {
+ needs: ['service:auth', 'service:flash-messages'],
+});
+
+test('secret api urls', function(assert) {
+ let url, method, options;
+ let adapter = this.subject({
+ ajax: (...args) => {
+ [url, method, options] = args;
+ return Ember.RSVP.resolve({});
+ },
+ });
+
+ adapter.query({}, 'secret', { id: '', backend: 'secret' });
+ assert.equal(url, '/v1/secret/metadata/', 'query generic url OK');
+ assert.equal('GET', method, 'query generic method OK');
+ assert.deepEqual(options, { data: { list: true } }, 'query generic url OK');
+
+ adapter.queryRecord({}, 'secret', { id: 'foo', backend: 'secret' });
+ assert.equal(url, '/v1/secret/data/foo', 'queryRecord generic url OK');
+ assert.equal('GET', method, 'queryRecord generic method OK');
+});
diff --git a/ui/tests/unit/adapters/tools-test.js b/ui/tests/unit/adapters/tools-test.js
new file mode 100644
index 000000000..115a1b487
--- /dev/null
+++ b/ui/tests/unit/adapters/tools-test.js
@@ -0,0 +1,56 @@
+import { moduleFor, test } from 'ember-qunit';
+import Ember from 'ember';
+
+moduleFor('adapter:tools', 'Unit | Adapter | tools', {
+ needs: ['service:auth', 'service:flash-messages'],
+});
+
+test('wrapping api urls', function(assert) {
+ let url, method, options;
+ let adapter = this.subject({
+ ajax: (...args) => {
+ [url, method, options] = args;
+ return Ember.RSVP.resolve();
+ },
+ });
+
+ let data = { foo: 'bar' };
+ adapter.toolAction('wrap', data, { wrapTTL: '30m' });
+ assert.equal('/v1/sys/wrapping/wrap', url, 'wrapping:wrap url OK');
+ assert.equal('POST', method, 'wrapping:wrap method OK');
+ assert.deepEqual({ data: data, wrapTTL: '30m' }, options, 'wrapping:wrap options OK');
+
+ data = { token: 'token' };
+ adapter.toolAction('lookup', data);
+ assert.equal('/v1/sys/wrapping/lookup', url, 'wrapping:lookup url OK');
+ assert.equal('POST', method, 'wrapping:lookup method OK');
+ assert.deepEqual({ data }, options, 'wrapping:lookup options OK');
+
+ adapter.toolAction('unwrap', data);
+ assert.equal('/v1/sys/wrapping/unwrap', url, 'wrapping:unwrap url OK');
+ assert.equal('POST', method, 'wrapping:unwrap method OK');
+ assert.deepEqual({ data }, options, 'wrapping:unwrap options OK');
+
+ adapter.toolAction('rewrap', data);
+ assert.equal('/v1/sys/wrapping/rewrap', url, 'wrapping:rewrap url OK');
+ assert.equal('POST', method, 'wrapping:rewrap method OK');
+ assert.deepEqual({ data }, options, 'wrapping:rewrap options OK');
+});
+
+test('tools api urls', function(assert) {
+ let url, method;
+ let adapter = this.subject({
+ ajax: (...args) => {
+ [url, method] = args;
+ return Ember.RSVP.resolve();
+ },
+ });
+
+ adapter.toolAction('hash', { input: 'someBase64' });
+ assert.equal(url, '/v1/sys/tools/hash', 'sys tools hash: url OK');
+ assert.equal('POST', method, 'sys tools hash: method OK');
+
+ adapter.toolAction('random', { bytes: '32' });
+ assert.equal(url, '/v1/sys/tools/random', 'sys tools random: url OK');
+ assert.equal('POST', method, 'sys tools random: method OK');
+});
diff --git a/ui/tests/unit/adapters/transit-key-test.js b/ui/tests/unit/adapters/transit-key-test.js
new file mode 100644
index 000000000..adf79aecf
--- /dev/null
+++ b/ui/tests/unit/adapters/transit-key-test.js
@@ -0,0 +1,40 @@
+import { moduleFor, test } from 'ember-qunit';
+import Ember from 'ember';
+
+moduleFor('adapter:transit-key', 'Unit | Adapter | transit key', {
+ needs: ['service:auth', 'service:flash-messages'],
+});
+
+test('transit api urls', function(assert) {
+ let url, method, options;
+ let adapter = this.subject({
+ ajax: (...args) => {
+ [url, method, options] = args;
+ return Ember.RSVP.resolve({});
+ },
+ });
+
+ adapter.query({}, 'transit-key', { id: '', backend: 'transit' });
+ assert.equal(url, '/v1/transit/keys/', 'query list url OK');
+ assert.equal('GET', method, 'query list method OK');
+ assert.deepEqual(options, { data: { list: true } }, 'query generic url OK');
+
+ adapter.queryRecord({}, 'transit-key', { id: 'foo', backend: 'transit' });
+ assert.equal(url, '/v1/transit/keys/foo', 'queryRecord generic url OK');
+ assert.equal('GET', method, 'queryRecord generic method OK');
+
+ adapter.keyAction('rotate', { backend: 'transit', id: 'foo', payload: {} });
+ assert.equal(url, '/v1/transit/keys/foo/rotate', 'keyAction:rotate url OK');
+
+ adapter.keyAction('encrypt', { backend: 'transit', id: 'foo', payload: {} });
+ assert.equal(url, '/v1/transit/encrypt/foo', 'keyAction:encrypt url OK');
+
+ adapter.keyAction('datakey', { backend: 'transit', id: 'foo', payload: { param: 'plaintext' } });
+ assert.equal(url, '/v1/transit/datakey/plaintext/foo', 'keyAction:datakey url OK');
+
+ adapter.keyAction('export', { backend: 'transit', id: 'foo', payload: { param: ['hmac'] } });
+ assert.equal(url, '/v1/transit/export/hmac-key/foo', 'transitAction:export, no version url OK');
+
+ adapter.keyAction('export', { backend: 'transit', id: 'foo', payload: { param: ['hmac', 10] } });
+ assert.equal(url, '/v1/transit/export/hmac-key/foo/10', 'transitAction:export, with version url OK');
+});
diff --git a/ui/tests/unit/mixins/cluster-route-test.js b/ui/tests/unit/mixins/cluster-route-test.js
new file mode 100644
index 000000000..01e4d32c7
--- /dev/null
+++ b/ui/tests/unit/mixins/cluster-route-test.js
@@ -0,0 +1,79 @@
+import Ember from 'ember';
+import ClusterRouteMixin from 'vault/mixins/cluster-route';
+import { INIT, UNSEAL, AUTH, CLUSTER, DR_REPLICATION_SECONDARY } from 'vault/mixins/cluster-route';
+import { module, test } from 'qunit';
+
+module('Unit | Mixin | cluster route');
+
+function createClusterRoute(clusterModel = {}, methods = { hasKeyData: () => false, authToken: () => null }) {
+ let ClusterRouteObject = Ember.Object.extend(
+ ClusterRouteMixin,
+ Ember.assign(methods, { clusterModel: () => clusterModel })
+ );
+ return ClusterRouteObject.create();
+}
+
+test('#targetRouteName init', function(assert) {
+ let subject = createClusterRoute({ needsInit: true });
+ subject.routeName = CLUSTER;
+ assert.equal(subject.targetRouteName(), INIT, 'forwards to INIT when cluster needs init');
+
+ subject = createClusterRoute({ needsInit: false, sealed: true });
+ subject.routeName = CLUSTER;
+ assert.equal(subject.targetRouteName(), UNSEAL, 'forwards to UNSEAL if sealed and initialized');
+
+ subject = createClusterRoute({ needsInit: false });
+ subject.routeName = CLUSTER;
+ assert.equal(subject.targetRouteName(), AUTH, 'forwards to AUTH if unsealed and initialized');
+
+ subject = createClusterRoute({ dr: { isSecondary: true } });
+ subject.routeName = CLUSTER;
+ assert.equal(
+ subject.targetRouteName(),
+ DR_REPLICATION_SECONDARY,
+ 'forwards to DR_REPLICATION_SECONDARY if is a dr secondary'
+ );
+});
+
+test('#targetRouteName when #hasDataKey is true', function(assert) {
+ let subject = createClusterRoute(
+ { needsInit: false, sealed: true },
+ { hasKeyData: () => true, authToken: () => null }
+ );
+
+ subject.routeName = CLUSTER;
+ assert.equal(subject.targetRouteName(), INIT, 'still land on INIT if there are keys on the controller');
+
+ subject.routeName = UNSEAL;
+ assert.equal(subject.targetRouteName(), UNSEAL, 'allowed to proceed to unseal');
+
+ subject = createClusterRoute(
+ { needsInit: false, sealed: false },
+ { hasKeyData: () => true, authToken: () => null }
+ );
+
+ subject.routeName = AUTH;
+ assert.equal(subject.targetRouteName(), AUTH, 'allowed to proceed to auth');
+});
+
+test('#targetRouteName happy path forwards to CLUSTER route', function(assert) {
+ let subject = createClusterRoute(
+ { needsInit: false, sealed: false, dr: { isSecondary: false } },
+ { hasKeyData: () => false, authToken: () => 'a token' }
+ );
+ subject.routeName = INIT;
+ assert.equal(subject.targetRouteName(), CLUSTER, 'forwards when inited and navigating to INIT');
+
+ subject.routeName = UNSEAL;
+ assert.equal(subject.targetRouteName(), CLUSTER, 'forwards when unsealed and navigating to UNSEAL');
+
+ subject.routeName = AUTH;
+ assert.equal(subject.targetRouteName(), CLUSTER, 'forwards when authenticated and navigating to AUTH');
+
+ subject.routeName = DR_REPLICATION_SECONDARY;
+ assert.equal(
+ subject.targetRouteName(),
+ CLUSTER,
+ 'forwards when not a DR secondary and navigating to DR_REPLICATION_SECONDARY'
+ );
+});
diff --git a/ui/tests/unit/models/capabilities-test.js b/ui/tests/unit/models/capabilities-test.js
new file mode 100644
index 000000000..1caa7efbb
--- /dev/null
+++ b/ui/tests/unit/models/capabilities-test.js
@@ -0,0 +1,71 @@
+import { moduleForModel, test } from 'ember-qunit';
+import { SUDO_PATHS, SUDO_PATH_PREFIXES } from 'vault/models/capabilities';
+
+moduleForModel('capabilities', 'Unit | Model | capabilities', {
+ needs: ['transform:array'],
+});
+
+test('it exists', function(assert) {
+ let model = this.subject();
+ assert.ok(!!model);
+});
+
+test('it reads capabilities', function(assert) {
+ let model = this.subject({
+ path: 'foo',
+ capabilities: ['list', 'read'],
+ });
+
+ assert.ok(model.get('canRead'));
+ assert.ok(model.get('canList'));
+ assert.notOk(model.get('canUpdate'));
+ assert.notOk(model.get('canDelete'));
+});
+
+test('it allows everything if root is present', function(assert) {
+ let model = this.subject({
+ path: 'foo',
+ capabilities: ['root', 'deny', 'read'],
+ });
+ assert.ok(model.get('canRead'));
+ assert.ok(model.get('canCreate'));
+ assert.ok(model.get('canUpdate'));
+ assert.ok(model.get('canDelete'));
+ assert.ok(model.get('canList'));
+});
+
+test('it denies everything if deny is present', function(assert) {
+ let model = this.subject({
+ path: 'foo',
+ capabilities: ['sudo', 'deny', 'read'],
+ });
+ assert.notOk(model.get('canRead'));
+ assert.notOk(model.get('canCreate'));
+ assert.notOk(model.get('canUpdate'));
+ assert.notOk(model.get('canDelete'));
+ assert.notOk(model.get('canList'));
+});
+
+test('it requires sudo on sudo paths', function(assert) {
+ let model = this.subject({
+ path: SUDO_PATHS[0],
+ capabilities: ['sudo', 'read'],
+ });
+ assert.ok(model.get('canRead'));
+ assert.notOk(model.get('canCreate'), 'sudo requires the capability to be set as well');
+ assert.notOk(model.get('canUpdate'));
+ assert.notOk(model.get('canDelete'));
+ assert.notOk(model.get('canList'));
+});
+
+test('it requires sudo on sudo paths prefixes', function(assert) {
+ let model = this.subject({
+ path: SUDO_PATH_PREFIXES[0] + '/foo',
+ capabilities: ['sudo', 'read'],
+ });
+ assert.ok(model.get('canRead'));
+ assert.notOk(model.get('canCreate'), 'sudo requires the capability to be set as well');
+ assert.notOk(model.get('canUpdate'));
+ assert.notOk(model.get('canDelete'));
+ assert.notOk(model.get('canList'));
+});
diff --git a/ui/tests/unit/models/transit-key-test.js b/ui/tests/unit/models/transit-key-test.js
new file mode 100644
index 000000000..ef4e7e951
--- /dev/null
+++ b/ui/tests/unit/models/transit-key-test.js
@@ -0,0 +1,60 @@
+import Ember from 'ember';
+import { moduleForModel, test } from 'ember-qunit';
+
+moduleForModel('transit-key', 'Unit | Model | transit key');
+
+test('it exists', function(assert) {
+ let model = this.subject();
+ assert.ok(!!model);
+});
+
+test('supported actions', function(assert) {
+ let model = this.subject({
+ supportsEncryption: true,
+ supportsDecryption: true,
+ supportsSigning: false,
+ });
+
+ let supportedActions = model.get('supportedActions');
+ assert.deepEqual(['encrypt', 'decrypt', 'datakey', 'rewrap', 'hmac', 'verify'], supportedActions);
+});
+
+test('encryption key versions', function(assert) {
+ let done = assert.async();
+ let model = this.subject({
+ keyVersions: [1, 2, 3, 4, 5],
+ minDecryptionVersion: 1,
+ latestVersion: 5,
+ });
+ assert.deepEqual([5, 4, 3, 2, 1], model.get('encryptionKeyVersions'), 'lists all available versions');
+ Ember.run(() => {
+ model.set('minDecryptionVersion', 3);
+ assert.deepEqual(
+ [5, 4, 3],
+ model.get('encryptionKeyVersions'),
+ 'adjusts to a change in minDecryptionVersion'
+ );
+ done();
+ });
+});
+
+test('keys for encryption', function(assert) {
+ let done = assert.async();
+ let model = this.subject({
+ keyVersions: [1, 2, 3, 4, 5],
+ minDecryptionVersion: 1,
+ latestVersion: 5,
+ });
+
+ assert.deepEqual(
+ [5, 4, 3, 2, 1],
+ model.get('keysForEncryption'),
+ 'lists all available versions when no min is set'
+ );
+
+ Ember.run(() => {
+ model.set('minEncryptionVersion', 4);
+ assert.deepEqual([5, 4], model.get('keysForEncryption'), 'calculates using minEncryptionVersion');
+ done();
+ });
+});
diff --git a/ui/tests/unit/serializers/policy-test.js b/ui/tests/unit/serializers/policy-test.js
new file mode 100644
index 000000000..47f7f7c79
--- /dev/null
+++ b/ui/tests/unit/serializers/policy-test.js
@@ -0,0 +1,69 @@
+import { moduleFor, test } from 'ember-qunit';
+
+moduleFor('serializer:policy', 'Unit | Serializer | policy');
+
+const POLICY_LIST_RESPONSE = {
+ keys: ['default', 'root'],
+ policies: ['default', 'root'],
+ request_id: '3a6a3d67-dc3b-a086-2fc7-902bdc4dec3a',
+ lease_id: '',
+ renewable: false,
+ lease_duration: 0,
+ data: {
+ keys: ['default', 'root'],
+ policies: ['default', 'root'],
+ },
+ wrap_info: null,
+ warnings: null,
+ auth: null,
+};
+
+const EMBER_DATA_EXPECTS_FOR_POLICY_LIST = [{ name: 'default' }, { name: 'root' }];
+
+const POLICY_SHOW_RESPONSE = {
+ name: 'default',
+ rules:
+ '\n# Allow tokens to look up their own properties\npath "auth/token/lookup-self" {\n capabilities = ["read"]\n}\n\n# Allow tokens to renew themselves\npath "auth/token/renew-self" {\n capabilities = ["update"]\n}\n\n# Allow tokens to revoke themselves\npath "auth/token/revoke-self" {\n capabilities = ["update"]\n}\n\n# Allow a token to look up its own capabilities on a path\npath "sys/capabilities-self" {\n capabilities = ["update"]\n}\n\n# Allow a token to renew a lease via lease_id in the request body\npath "sys/renew" {\n capabilities = ["update"]\n}\n\n# Allow a token to manage its own cubbyhole\npath "cubbyhole/*" {\n capabilities = ["create", "read", "update", "delete", "list"]\n}\n\n# Allow a token to list its cubbyhole (not covered by the splat above)\npath "cubbyhole" {\n capabilities = ["list"]\n}\n\n# Allow a token to wrap arbitrary values in a response-wrapping token\npath "sys/wrapping/wrap" {\n capabilities = ["update"]\n}\n\n# Allow a token to look up the creation time and TTL of a given\n# response-wrapping token\npath "sys/wrapping/lookup" {\n capabilities = ["update"]\n}\n\n# Allow a token to unwrap a response-wrapping token. This is a convenience to\n# avoid client token swapping since this is also part of the response wrapping\n# policy.\npath "sys/wrapping/unwrap" {\n capabilities = ["update"]\n}\n',
+ request_id: '890eabf8-d418-07af-f978-928d328a7e64',
+ lease_id: '',
+ renewable: false,
+ lease_duration: 0,
+ data: {
+ name: 'default',
+ rules:
+ '\n# Allow tokens to look up their own properties\npath "auth/token/lookup-self" {\n capabilities = ["read"]\n}\n\n# Allow tokens to renew themselves\npath "auth/token/renew-self" {\n capabilities = ["update"]\n}\n\n# Allow tokens to revoke themselves\npath "auth/token/revoke-self" {\n capabilities = ["update"]\n}\n\n# Allow a token to look up its own capabilities on a path\npath "sys/capabilities-self" {\n capabilities = ["update"]\n}\n\n# Allow a token to renew a lease via lease_id in the request body\npath "sys/renew" {\n capabilities = ["update"]\n}\n\n# Allow a token to manage its own cubbyhole\npath "cubbyhole/*" {\n capabilities = ["create", "read", "update", "delete", "list"]\n}\n\n# Allow a token to list its cubbyhole (not covered by the splat above)\npath "cubbyhole" {\n capabilities = ["list"]\n}\n\n# Allow a token to wrap arbitrary values in a response-wrapping token\npath "sys/wrapping/wrap" {\n capabilities = ["update"]\n}\n\n# Allow a token to look up the creation time and TTL of a given\n# response-wrapping token\npath "sys/wrapping/lookup" {\n capabilities = ["update"]\n}\n\n# Allow a token to unwrap a response-wrapping token. This is a convenience to\n# avoid client token swapping since this is also part of the response wrapping\n# policy.\npath "sys/wrapping/unwrap" {\n capabilities = ["update"]\n}\n',
+ },
+ wrap_info: null,
+ warnings: null,
+ auth: null,
+};
+
+const EMBER_DATA_EXPECTS_FOR_POLICY_SHOW = {
+ name: 'default',
+ rules:
+ '\n# Allow tokens to look up their own properties\npath "auth/token/lookup-self" {\n capabilities = ["read"]\n}\n\n# Allow tokens to renew themselves\npath "auth/token/renew-self" {\n capabilities = ["update"]\n}\n\n# Allow tokens to revoke themselves\npath "auth/token/revoke-self" {\n capabilities = ["update"]\n}\n\n# Allow a token to look up its own capabilities on a path\npath "sys/capabilities-self" {\n capabilities = ["update"]\n}\n\n# Allow a token to renew a lease via lease_id in the request body\npath "sys/renew" {\n capabilities = ["update"]\n}\n\n# Allow a token to manage its own cubbyhole\npath "cubbyhole/*" {\n capabilities = ["create", "read", "update", "delete", "list"]\n}\n\n# Allow a token to list its cubbyhole (not covered by the splat above)\npath "cubbyhole" {\n capabilities = ["list"]\n}\n\n# Allow a token to wrap arbitrary values in a response-wrapping token\npath "sys/wrapping/wrap" {\n capabilities = ["update"]\n}\n\n# Allow a token to look up the creation time and TTL of a given\n# response-wrapping token\npath "sys/wrapping/lookup" {\n capabilities = ["update"]\n}\n\n# Allow a token to unwrap a response-wrapping token. This is a convenience to\n# avoid client token swapping since this is also part of the response wrapping\n# policy.\npath "sys/wrapping/unwrap" {\n capabilities = ["update"]\n}\n',
+};
+
+test('it transforms a list request payload', function(assert) {
+ let serializer = this.subject();
+
+ let transformedPayload = serializer.normalizePolicies(POLICY_LIST_RESPONSE);
+
+ assert.deepEqual(
+ transformedPayload,
+ EMBER_DATA_EXPECTS_FOR_POLICY_LIST,
+ 'transformed payload matches the expected payload'
+ );
+});
+
+test('it transforms a list request payload', function(assert) {
+ let serializer = this.subject();
+
+ let transformedPayload = serializer.normalizePolicies(POLICY_SHOW_RESPONSE);
+
+ assert.deepEqual(
+ transformedPayload,
+ EMBER_DATA_EXPECTS_FOR_POLICY_SHOW,
+ 'transformed payload matches the expected payload'
+ );
+});
diff --git a/ui/tests/unit/services/auth-test.js b/ui/tests/unit/services/auth-test.js
new file mode 100644
index 000000000..61483b302
--- /dev/null
+++ b/ui/tests/unit/services/auth-test.js
@@ -0,0 +1,337 @@
+import { moduleFor, test } from 'ember-qunit';
+import { TOKEN_SEPARATOR, TOKEN_PREFIX, ROOT_PREFIX } from 'vault/services/auth';
+import Ember from 'ember';
+import Pretender from 'pretender';
+
+function storage() {
+ return {
+ items: {},
+ getItem(key) {
+ var item = this.items[key];
+ return item && JSON.parse(item);
+ },
+
+ setItem(key, val) {
+ return (this.items[key] = JSON.stringify(val));
+ },
+
+ removeItem(key) {
+ delete this.items[key];
+ },
+
+ keys() {
+ return Object.keys(this.items);
+ },
+ };
+}
+
+let ROOT_TOKEN_RESPONSE = {
+ request_id: 'e6674d7f-c96f-d51f-4463-cc95f0ad307e',
+ lease_id: '',
+ renewable: false,
+ lease_duration: 0,
+ data: {
+ accessor: '1dd25306-fdb9-0f43-8169-48ad702041b0',
+ creation_time: 1477671134,
+ creation_ttl: 0,
+ display_name: 'root',
+ explicit_max_ttl: 0,
+ id: '',
+ meta: null,
+ num_uses: 0,
+ orphan: true,
+ path: 'auth/token/root',
+ policies: ['root'],
+ ttl: 0,
+ },
+ wrap_info: null,
+ warnings: null,
+ auth: null,
+};
+
+let TOKEN_NON_ROOT_RESPONSE = function() {
+ return {
+ request_id: '3ca32cd9-fd40-891d-02d5-ea23138e8642',
+ lease_id: '',
+ renewable: false,
+ lease_duration: 0,
+ data: {
+ accessor: '4ef32471-a94c-79ee-c290-aeba4d63bdc9',
+ creation_time: Math.floor(Date.now() / 1000),
+ creation_ttl: 2764800,
+ display_name: 'token',
+ explicit_max_ttl: 0,
+ id: '6d83e912-1b21-9df9-b51a-d201b709f3d5',
+ meta: null,
+ num_uses: 0,
+ orphan: false,
+ path: 'auth/token/create',
+ policies: ['default', 'userpass'],
+ renewable: true,
+ ttl: 2763327,
+ },
+ wrap_info: null,
+ warnings: null,
+ auth: null,
+ };
+};
+
+let USERPASS_RESPONSE = {
+ request_id: '7e5e8d3d-599e-6ef7-7570-f7057fc7c53d',
+ lease_id: '',
+ renewable: false,
+ lease_duration: 0,
+ data: null,
+ wrap_info: null,
+ warnings: null,
+ auth: {
+ client_token: '5313ff81-05cb-699f-29d1-b82b4e2906dc',
+ accessor: '5c5303e7-56d6-ea13-72df-d85411bd9a7d',
+ policies: ['default'],
+ metadata: {
+ username: 'matthew',
+ },
+ lease_duration: 2764800,
+ renewable: true,
+ },
+};
+
+let GITHUB_RESPONSE = {
+ request_id: '4913f9cd-a95f-d1f9-5746-4c3af4e15660',
+ lease_id: '',
+ renewable: false,
+ lease_duration: 0,
+ data: null,
+ wrap_info: null,
+ warnings: null,
+ auth: {
+ client_token: '0d39b535-598e-54d9-96e3-97493492a5f7',
+ accessor: 'd8cd894f-bedf-5ce3-f1b5-98f7c6cf8ab4',
+ policies: ['default'],
+ metadata: {
+ org: 'hashicorp',
+ username: 'meirish',
+ },
+ lease_duration: 2764800,
+ renewable: true,
+ },
+};
+
+moduleFor('service:auth', 'Unit | Service | auth', {
+ needs: ['service:flash-messages', 'adapter:cluster', 'service:version'],
+ beforeEach: function() {
+ Ember.getOwner(this).lookup('service:flash-messages').registerTypes(['warning']);
+ this.store = storage();
+ this.memStore = storage();
+ this.server = new Pretender(function() {
+ this.get('/v1/auth/token/lookup-self', function(request) {
+ let resp = Ember.copy(ROOT_TOKEN_RESPONSE, true);
+ resp.id = request.requestHeaders['X-Vault-Token'];
+ resp.data.id = request.requestHeaders['X-Vault-Token'];
+ return [200, {}, resp];
+ });
+ this.post('/v1/auth/userpass/login/:username', function(request) {
+ const { username } = request.params;
+ let resp = Ember.copy(USERPASS_RESPONSE, true);
+ resp.auth.metadata.username = username;
+ return [200, {}, resp];
+ });
+
+ this.post('/v1/auth/github/login', function() {
+ let resp = Ember.copy(GITHUB_RESPONSE, true);
+ return [200, {}, resp];
+ });
+ });
+
+ this.server.prepareBody = function(body) {
+ return body ? JSON.stringify(body) : '{"error": "not found"}';
+ };
+
+ this.server.prepareHeaders = function(headers) {
+ headers['content-type'] = 'application/javascript';
+ return headers;
+ };
+ },
+
+ afterEach: function() {
+ this.server.shutdown();
+ },
+});
+
+test('token authentication: root token', function(assert) {
+ let done = assert.async();
+ let self = this;
+ let service = this.subject({
+ storage(tokenName) {
+ if (
+ tokenName &&
+ tokenName.indexOf(`${TOKEN_PREFIX}${ROOT_PREFIX}`) === 0 &&
+ this.environment() !== 'development'
+ ) {
+ return self.memStore;
+ } else {
+ return self.store;
+ }
+ },
+ });
+ Ember.run(() => {
+ service.authenticate({ clusterId: '1', backend: 'token', data: { token: 'test' } }).then(() => {
+ const clusterTokenName = service.get('currentTokenName');
+ const clusterToken = service.get('currentToken');
+ const authData = service.get('authData');
+
+ const expectedTokenName = `${TOKEN_PREFIX}${ROOT_PREFIX}${TOKEN_SEPARATOR}1`;
+ assert.equal('test', clusterToken, 'token is saved properly');
+ assert.equal(
+ `${TOKEN_PREFIX}${ROOT_PREFIX}${TOKEN_SEPARATOR}1`,
+ clusterTokenName,
+ 'token name is saved properly'
+ );
+ assert.equal('token', authData.backend.type, 'backend is saved properly');
+ assert.equal(
+ ROOT_TOKEN_RESPONSE.data.display_name,
+ authData.displayName,
+ 'displayName is saved properly'
+ );
+ assert.ok(this.memStore.keys().includes(expectedTokenName), 'root token is stored in the memory store');
+ assert.equal(this.store.keys().length, 0, 'normal storage is empty');
+ done();
+ });
+ });
+});
+
+test('token authentication: root token in ember development environment', function(assert) {
+ let done = assert.async();
+ let self = this;
+ let service = this.subject({
+ storage(tokenName) {
+ if (
+ tokenName &&
+ tokenName.indexOf(`${TOKEN_PREFIX}${ROOT_PREFIX}`) === 0 &&
+ this.environment() !== 'development'
+ ) {
+ return self.memStore;
+ } else {
+ return self.store;
+ }
+ },
+ environment: () => 'development',
+ });
+ Ember.run(() => {
+ service.authenticate({ clusterId: '1', backend: 'token', data: { token: 'test' } }).then(() => {
+ const clusterTokenName = service.get('currentTokenName');
+ const clusterToken = service.get('currentToken');
+ const authData = service.get('authData');
+
+ const expectedTokenName = `${TOKEN_PREFIX}${ROOT_PREFIX}${TOKEN_SEPARATOR}1`;
+ assert.equal('test', clusterToken, 'token is saved properly');
+ assert.equal(
+ `${TOKEN_PREFIX}${ROOT_PREFIX}${TOKEN_SEPARATOR}1`,
+ clusterTokenName,
+ 'token name is saved properly'
+ );
+ assert.equal('token', authData.backend.type, 'backend is saved properly');
+ assert.equal(
+ ROOT_TOKEN_RESPONSE.data.display_name,
+ authData.displayName,
+ 'displayName is saved properly'
+ );
+ assert.ok(this.store.keys().includes(expectedTokenName), 'root token is stored in the store');
+ assert.equal(this.memStore.keys().length, 0, 'mem storage is empty');
+ done();
+ });
+ });
+});
+
+test('github authentication', function(assert) {
+ let done = assert.async();
+ let service = this.subject({
+ storage: type => (type === 'memory' ? this.memStore : this.store),
+ });
+
+ Ember.run(() => {
+ service.authenticate({ clusterId: '1', backend: 'GitHub', data: { token: 'test' } }).then(() => {
+ const clusterTokenName = service.get('currentTokenName');
+ const clusterToken = service.get('currentToken');
+ const authData = service.get('authData');
+ const expectedTokenName = `${TOKEN_PREFIX}GitHub${TOKEN_SEPARATOR}1`;
+
+ assert.equal(GITHUB_RESPONSE.auth.client_token, clusterToken, 'token is saved properly');
+ assert.equal(expectedTokenName, clusterTokenName, 'token name is saved properly');
+ assert.equal('GitHub', authData.backend.type, 'backend is saved properly');
+ assert.equal(
+ GITHUB_RESPONSE.auth.metadata.org + '/' + GITHUB_RESPONSE.auth.metadata.username,
+ authData.displayName,
+ 'displayName is saved properly'
+ );
+ assert.equal(this.memStore.keys().length, 0, 'mem storage is empty');
+ assert.ok(this.store.keys().includes(expectedTokenName), 'normal storage contains the token');
+ done();
+ });
+ });
+});
+
+test('userpass authentication', function(assert) {
+ let done = assert.async();
+ let service = this.subject({ storage: () => this.store });
+ Ember.run(() => {
+ service
+ .authenticate({
+ clusterId: '1',
+ backend: 'userpass',
+ data: { username: USERPASS_RESPONSE.auth.metadata.username, password: 'passoword' },
+ })
+ .then(() => {
+ const clusterTokenName = service.get('currentTokenName');
+ const clusterToken = service.get('currentToken');
+ const authData = service.get('authData');
+
+ assert.equal(USERPASS_RESPONSE.auth.client_token, clusterToken, 'token is saved properly');
+ assert.equal(
+ `${TOKEN_PREFIX}userpass${TOKEN_SEPARATOR}1`,
+ clusterTokenName,
+ 'token name is saved properly'
+ );
+ assert.equal('userpass', authData.backend.type, 'backend is saved properly');
+ assert.equal(
+ USERPASS_RESPONSE.auth.metadata.username,
+ authData.displayName,
+ 'displayName is saved properly'
+ );
+ done();
+ });
+ });
+});
+
+test('token auth expiry with non-root token', function(assert) {
+ const tokenResp = TOKEN_NON_ROOT_RESPONSE();
+ this.server.map(function() {
+ this.get('/v1/auth/token/lookup-self', function(request) {
+ let resp = Ember.copy(tokenResp, true);
+ resp.id = request.requestHeaders['X-Vault-Token'];
+ resp.data.id = request.requestHeaders['X-Vault-Token'];
+ return [200, {}, resp];
+ });
+ });
+
+ let done = assert.async();
+ let service = this.subject({ storage: () => this.store });
+ Ember.run(() => {
+ service.authenticate({ clusterId: '1', backend: 'token', data: { token: 'test' } }).then(() => {
+ const clusterTokenName = service.get('currentTokenName');
+ const clusterToken = service.get('currentToken');
+ const authData = service.get('authData');
+
+ assert.equal('test', clusterToken, 'token is saved properly');
+ assert.equal(
+ `${TOKEN_PREFIX}token${TOKEN_SEPARATOR}1`,
+ clusterTokenName,
+ 'token name is saved properly'
+ );
+ assert.equal(authData.backend.type, 'token', 'backend is saved properly');
+ assert.equal(authData.displayName, tokenResp.data.display_name, 'displayName is saved properly');
+ assert.equal(service.get('tokenExpired'), false, 'token is not expired');
+ done();
+ });
+ });
+});
diff --git a/ui/tests/unit/services/store-test.js b/ui/tests/unit/services/store-test.js
new file mode 100644
index 000000000..ab2f95fa0
--- /dev/null
+++ b/ui/tests/unit/services/store-test.js
@@ -0,0 +1,216 @@
+import { moduleFor, test } from 'ember-qunit';
+import { normalizeModelName, keyForCache } from 'vault/services/store';
+import clamp from 'vault/utils/clamp';
+import Ember from 'ember';
+
+moduleFor('service:store', 'Unit | Service | store', {
+ // Specify the other units that are required for this test.
+ needs: ['model:transit-key', 'serializer:transit-key'],
+});
+
+test('normalizeModelName', function(assert) {
+ assert.equal(normalizeModelName('oneThing'), 'one-thing', 'dasherizes modelName');
+});
+
+test('keyForCache', function(assert) {
+ const query = { id: 1 };
+ const queryWithSize = { id: 1, size: 1 };
+ assert.deepEqual(keyForCache(query), JSON.stringify(query), 'generated the correct cache key');
+ assert.deepEqual(keyForCache(queryWithSize), JSON.stringify(query), 'excludes size from query cache');
+});
+
+test('clamp', function(assert) {
+ assert.equal(clamp('foo', 0, 100), 0, 'returns the min if passed a non-number');
+ assert.equal(clamp(0, 1, 100), 1, 'returns the min when passed number is less than the min');
+ assert.equal(clamp(200, 1, 100), 100, 'returns the max passed number is greater than the max');
+ assert.equal(clamp(50, 1, 100), 50, 'returns the passed number when it is in range');
+});
+
+test('store.storeDataset', function(assert) {
+ const arr = ['one', 'two'];
+ const store = this.subject();
+ const query = { id: 1 };
+ store.storeDataset('data', query, {}, arr);
+
+ assert.deepEqual(store.getDataset('data', query).dataset, arr, 'it stores the array as .dataset');
+ assert.deepEqual(store.getDataset('data', query).response, {}, 'it stores the response as .response');
+ assert.ok(store.get('lazyCaches').has('data'), 'it stores model map');
+ assert.ok(store.get('lazyCaches').get('data').has(keyForCache(query)), 'it stores data on the model map');
+});
+
+test('store.clearDataset with a prefix', function(assert) {
+ const store = this.subject();
+ const arr = ['one', 'two'];
+ const arr2 = ['one', 'two', 'three', 'four'];
+ store.storeDataset('data', { id: 1 }, {}, arr);
+ store.storeDataset('transit-key', { id: 2 }, {}, arr2);
+ assert.equal(store.get('lazyCaches').size, 2, 'it stores both keys');
+
+ store.clearDataset('transit-key');
+ assert.equal(store.get('lazyCaches').size, 1, 'deletes one key');
+ assert.notOk(store.get('lazyCaches').has(), 'cache is no longer stored');
+});
+
+test('store.clearAllDatasets', function(assert) {
+ const store = this.subject();
+ const arr = ['one', 'two'];
+ const arr2 = ['one', 'two', 'three', 'four'];
+ store.storeDataset('data', { id: 1 }, {}, arr);
+ store.storeDataset('transit-key', { id: 2 }, {}, arr2);
+ assert.equal(store.get('lazyCaches').size, 2, 'it stores both keys');
+
+ store.clearAllDatasets();
+ assert.equal(store.get('lazyCaches').size, 0, 'deletes all of the keys');
+ assert.notOk(store.get('lazyCaches').has('transit-key'), 'first cache key is no longer stored');
+ assert.notOk(store.get('lazyCaches').has('data'), 'second cache key is no longer stored');
+});
+
+test('store.getDataset', function(assert) {
+ const arr = ['one', 'two'];
+ const store = this.subject();
+ store.storeDataset('data', { id: 1 }, {}, arr);
+
+ assert.deepEqual(store.getDataset('data', { id: 1 }), { response: {}, dataset: arr });
+});
+
+test('store.constructResponse', function(assert) {
+ const arr = ['one', 'two', 'three', 'fifteen', 'twelve'];
+ const store = this.subject();
+ store.storeDataset('data', { id: 1 }, {}, arr);
+
+ assert.deepEqual(
+ store.constructResponse('data', { id: 1, pageFilter: 't', page: 1, size: 3, responsePath: 'data' }),
+ {
+ data: ['two', 'three', 'fifteen'],
+ meta: { currentPage: 1, lastPage: 2, nextPage: 2, prevPage: 1, total: 5, filteredTotal: 4 },
+ },
+ 'it returns filtered results'
+ );
+});
+
+test('store.fetchPage', function(assert) {
+ const keys = ['zero', 'one', 'two', 'three', 'four', 'five', 'six'];
+ const data = {
+ data: {
+ keys,
+ },
+ };
+ const store = this.subject();
+ const pageSize = 2;
+ const query = {
+ size: pageSize,
+ page: 1,
+ responsePath: 'data.keys',
+ };
+ store.storeDataset('transit-key', query, data, keys);
+
+ let result;
+ Ember.run(() => {
+ result = store.fetchPage('transit-key', query);
+ });
+
+ assert.ok(result.get('length'), pageSize, 'returns the correct number of items');
+ assert.deepEqual(result.toArray().mapBy('id'), keys.slice(0, pageSize), 'returns the first page of items');
+ assert.deepEqual(
+ result.get('meta'),
+ {
+ nextPage: 2,
+ prevPage: 1,
+ currentPage: 1,
+ lastPage: 4,
+ total: 7,
+ filteredTotal: 7,
+ },
+ 'returns correct meta values'
+ );
+
+ Ember.run(() => {
+ result = store.fetchPage('transit-key', {
+ size: pageSize,
+ page: 3,
+ responsePath: 'data.keys',
+ });
+ });
+
+ const pageThreeEnd = 3 * pageSize;
+ const pageThreeStart = pageThreeEnd - pageSize;
+ assert.deepEqual(
+ result.toArray().mapBy('id'),
+ keys.slice(pageThreeStart, pageThreeEnd),
+ 'returns the third page of items'
+ );
+
+ Ember.run(() => {
+ result = store.fetchPage('transit-key', {
+ size: pageSize,
+ page: 99,
+ responsePath: 'data.keys',
+ });
+ });
+
+ assert.deepEqual(
+ result.toArray().mapBy('id'),
+ keys.slice(keys.length - 1),
+ 'returns the last page when the page value is beyond the of bounds'
+ );
+
+ Ember.run(() => {
+ result = store.fetchPage('transit-key', {
+ size: pageSize,
+ page: 0,
+ responsePath: 'data.keys',
+ });
+ });
+ assert.deepEqual(
+ result.toArray().mapBy('id'),
+ keys.slice(0, pageSize),
+ 'returns the first page when page value is under the bounds'
+ );
+});
+
+test('store.lazyPaginatedQuery', function(assert) {
+ let response = {
+ data: ['foo'],
+ };
+ const store = this.subject({
+ adapterFor() {
+ return {
+ query() {
+ return Ember.RSVP.resolve(response);
+ },
+ };
+ },
+ fetchPage() {},
+ });
+
+ const query = { page: 1, size: 1, responsePath: 'data' };
+ Ember.run(function() {
+ store.lazyPaginatedQuery('transit-key', query);
+ });
+ assert.deepEqual(
+ store.getDataset('transit-key', query),
+ { response: { data: null }, dataset: ['foo'] },
+ 'stores returned dataset'
+ );
+ assert.throws(
+ () => {
+ store.lazyPaginatedQuery('transit-key', {});
+ },
+ /responsePath is required/,
+ 'requires responsePath'
+ );
+ assert.throws(
+ () => {
+ store.lazyPaginatedQuery('transit-key', { responsePath: 'foo' });
+ },
+ /page is required/,
+ 'requires page'
+ );
+ assert.throws(
+ () => {
+ store.lazyPaginatedQuery('transit-key', { responsePath: 'foo', page: 1 });
+ },
+ /size is required/,
+ 'requires size'
+ );
+});
diff --git a/ui/tests/unit/services/version-test.js b/ui/tests/unit/services/version-test.js
new file mode 100644
index 000000000..9ad182ce1
--- /dev/null
+++ b/ui/tests/unit/services/version-test.js
@@ -0,0 +1,31 @@
+import { moduleFor, test } from 'ember-qunit';
+
+moduleFor('service:version', 'Unit | Service | version');
+
+test('setting version computes isOSS properly', function(assert) {
+ let service = this.subject();
+ service.set('version', '0.9.5');
+ assert.equal(service.get('isOSS'), true);
+ assert.equal(service.get('isEnterprise'), false);
+});
+
+test('setting version computes isEnterprise properly', function(assert) {
+ let service = this.subject();
+ service.set('version', '0.9.5+prem');
+ assert.equal(service.get('isOSS'), false);
+ assert.equal(service.get('isEnterprise'), true);
+});
+
+test('hasPerfReplication', function(assert) {
+ let service = this.subject();
+ assert.equal(service.get('hasPerfReplication'), false);
+ service.set('_features', ['Performance Replication']);
+ assert.equal(service.get('hasPerfReplication'), true);
+});
+
+test('hasDRReplication', function(assert) {
+ let service = this.subject();
+ assert.equal(service.get('hasDRReplication'), false);
+ service.set('_features', ['DR Replication']);
+ assert.equal(service.get('hasDRReplication'), true);
+});
diff --git a/ui/tests/unit/utils/decode-config-from-jwt-test.js b/ui/tests/unit/utils/decode-config-from-jwt-test.js
new file mode 100644
index 000000000..83db8d079
--- /dev/null
+++ b/ui/tests/unit/utils/decode-config-from-jwt-test.js
@@ -0,0 +1,30 @@
+import decodeConfigFromJWT from 'vault/utils/decode-config-from-jwt';
+import { module, test } from 'qunit';
+
+module('Unit | Util | decode config from jwt');
+
+const PADDING_STRIPPED_TOKEN =
+ 'eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJhZGRyIjoiaHR0cDovLzE5Mi4xNjguNTAuMTUwOjgyMDAiLCJleHAiOjE1MTczNjkwNzUsImlhdCI6MTUxNzM2NzI3NSwianRpIjoiN2IxZDZkZGUtZmViZC00ZGU1LTc0MWUtZDU2ZTg0ZTNjZDk2IiwidHlwZSI6IndyYXBwaW5nIn0.MIGIAkIB6s2zbohbxLimwhM6cg16OISK2DgoTgy1vHbTjPT8uG4hsrJndZp5COB8dX-djWjx78ZFMk-3a6Ij51su_By9xsoCQgFXV8y3DzH_YzYvdL9x38dMSWaVHpR_lpoKWsQnMvAukSchJp1FfHZQ8JcSkPu5IAVZdfwlG5esJ_ZOMxA3KIQFnA';
+const NO_PADDING_TOKEN =
+ 'eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJhZGRyIjoiaHR0cDovLzEyNy4wLjAuMTo4MjAwIiwiZXhwIjoxNTE3NDM0NDA2LCJpYXQiOjE1MTc0MzI2MDYsImp0aSI6IjBiYmI1ZWMyLWM0ODgtMzRjYi0wMzY5LTkxZmJiMjVkZTFiYSIsInR5cGUiOiJ3cmFwcGluZyJ9.MIGHAkIBAGzB5EW6PolAi2rYOzZNvfJnR902WxprtRqnSF2E2I2ye9XLGX--L7npSBjBhnd27ocQ4ZO9VhfDIFqMzu1TNiwCQT52O6xAoz9ElRrq76PjkEHO4ns5_ZgjSKXuKaqdGysHYSlry8KEjWLGQECvZWg9LQeIf35jwqeQUfyJUfmwl5r_';
+const INVALID_JSON_TOKEN = `foo.${btoa({ addr: 'http://127.0.0.1' })}.bar`;
+
+test('it decodes token with no padding', function(assert) {
+ const config = decodeConfigFromJWT(NO_PADDING_TOKEN);
+
+ assert.ok(!!config, 'config was decoded');
+ assert.ok(!!config.addr, 'config.addr is present');
+});
+
+test('it decodes token with stripped padding', function(assert) {
+ const config = decodeConfigFromJWT(PADDING_STRIPPED_TOKEN);
+
+ assert.ok(!!config, 'config was decoded');
+ assert.ok(!!config.addr, 'config.addr is present');
+});
+
+test('it returns nothing if the config is invalid JSON', function(assert) {
+ const config = decodeConfigFromJWT(INVALID_JSON_TOKEN);
+
+ assert.notOk(config, 'config is not present');
+});
diff --git a/ui/vendor/.gitkeep b/ui/vendor/.gitkeep
new file mode 100644
index 000000000..e69de29bb
diff --git a/ui/vendor/shims/autosize.js b/ui/vendor/shims/autosize.js
new file mode 100644
index 000000000..c7417b43a
--- /dev/null
+++ b/ui/vendor/shims/autosize.js
@@ -0,0 +1,9 @@
+(function() {
+ function vendorModule() {
+ 'use strict';
+
+ return { 'default': self['autosize'] };
+ }
+
+ define('autosize', [], vendorModule);
+})();
diff --git a/ui/vendor/string-includes.js b/ui/vendor/string-includes.js
new file mode 100644
index 000000000..5b9cf4b2e
--- /dev/null
+++ b/ui/vendor/string-includes.js
@@ -0,0 +1,16 @@
+// By Mozilla Contributors licensed under CC-BY-SA 2.5 (http://creativecommons.org/licenses/by-sa/2.5/)
+// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/includes#Polyfill
+if (!String.prototype.includes) {
+ String.prototype.includes = function(search, start) {
+ 'use strict';
+ if (typeof start !== 'number') {
+ start = 0;
+ }
+
+ if (start + search.length > this.length) {
+ return false;
+ } else {
+ return this.indexOf(search, start) !== -1;
+ }
+ };
+}
diff --git a/ui/yarn.lock b/ui/yarn.lock
new file mode 100644
index 000000000..1b9ff6410
--- /dev/null
+++ b/ui/yarn.lock
@@ -0,0 +1,7710 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+"@glimmer/compiler@^0.22.3":
+ version "0.22.3"
+ resolved "https://registry.yarnpkg.com/@glimmer/compiler/-/compiler-0.22.3.tgz#3aef9448460af1d320a82423323498a6ff38a0c6"
+ dependencies:
+ "@glimmer/syntax" "^0.22.3"
+ "@glimmer/util" "^0.22.3"
+ "@glimmer/wire-format" "^0.22.3"
+ simple-html-tokenizer "^0.3.0"
+
+"@glimmer/di@^0.2.0":
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/@glimmer/di/-/di-0.2.0.tgz#73bfd4a6ee4148a80bf092e8a5d29bcac9d4ce7e"
+
+"@glimmer/interfaces@^0.22.3":
+ version "0.22.3"
+ resolved "https://registry.yarnpkg.com/@glimmer/interfaces/-/interfaces-0.22.3.tgz#1c2e3289ae41a750f0c8ddcc64529b9e90dda604"
+ dependencies:
+ "@glimmer/wire-format" "^0.22.3"
+
+"@glimmer/node@^0.22.3":
+ version "0.22.3"
+ resolved "https://registry.yarnpkg.com/@glimmer/node/-/node-0.22.3.tgz#ff33eea6e65147a20c1bd1f05fdc4a6c3595c54c"
+ dependencies:
+ "@glimmer/runtime" "^0.22.3"
+ simple-dom "^0.3.0"
+
+"@glimmer/object-reference@^0.22.3":
+ version "0.22.3"
+ resolved "https://registry.yarnpkg.com/@glimmer/object-reference/-/object-reference-0.22.3.tgz#31db68c8912324c63509b1ef83213f7ad4ef312b"
+ dependencies:
+ "@glimmer/reference" "^0.22.3"
+ "@glimmer/util" "^0.22.3"
+
+"@glimmer/object@^0.22.3":
+ version "0.22.3"
+ resolved "https://registry.yarnpkg.com/@glimmer/object/-/object-0.22.3.tgz#1fc9fd7465c7d12e5b92464ad40038b595de8ed0"
+ dependencies:
+ "@glimmer/object-reference" "^0.22.3"
+ "@glimmer/util" "^0.22.3"
+
+"@glimmer/reference@^0.22.3":
+ version "0.22.3"
+ resolved "https://registry.yarnpkg.com/@glimmer/reference/-/reference-0.22.3.tgz#6f2ef8cd97fe756d89fef75f8c3c79003502a2a9"
+ dependencies:
+ "@glimmer/util" "^0.22.3"
+
+"@glimmer/resolver@^0.4.1":
+ version "0.4.1"
+ resolved "https://registry.yarnpkg.com/@glimmer/resolver/-/resolver-0.4.1.tgz#cd9644572c556e7e799de1cf8eff2b999cf5b878"
+ dependencies:
+ "@glimmer/di" "^0.2.0"
+
+"@glimmer/runtime@^0.22.3":
+ version "0.22.3"
+ resolved "https://registry.yarnpkg.com/@glimmer/runtime/-/runtime-0.22.3.tgz#b8cb28efc9cc86c406ee996f5c2cf6730620d404"
+ dependencies:
+ "@glimmer/interfaces" "^0.22.3"
+ "@glimmer/object" "^0.22.3"
+ "@glimmer/object-reference" "^0.22.3"
+ "@glimmer/reference" "^0.22.3"
+ "@glimmer/util" "^0.22.3"
+ "@glimmer/wire-format" "^0.22.3"
+
+"@glimmer/syntax@^0.22.3":
+ version "0.22.3"
+ resolved "https://registry.yarnpkg.com/@glimmer/syntax/-/syntax-0.22.3.tgz#8528d19324bf7f920f5cfd31925e452e51781b44"
+ dependencies:
+ handlebars "^4.0.6"
+ simple-html-tokenizer "^0.3.0"
+
+"@glimmer/util@^0.22.3":
+ version "0.22.3"
+ resolved "https://registry.yarnpkg.com/@glimmer/util/-/util-0.22.3.tgz#8272f50905d1bb904ee371e8ade83fd779b51508"
+
+"@glimmer/wire-format@^0.22.3":
+ version "0.22.3"
+ resolved "https://registry.yarnpkg.com/@glimmer/wire-format/-/wire-format-0.22.3.tgz#19b226d9b93ba6ee54472d9ffb1d48e7c0d80a0d"
+ dependencies:
+ "@glimmer/util" "^0.22.3"
+
+abbrev@1:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.0.tgz#d0554c2256636e2f56e7c2e5ad183f859428d81f"
+
+accepts@1.3.3, accepts@~1.3.3:
+ version "1.3.3"
+ resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.3.tgz#c3ca7434938648c3e0d9c1e328dd68b622c284ca"
+ dependencies:
+ mime-types "~2.1.11"
+ negotiator "0.6.1"
+
+acorn-jsx@^3.0.0:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-3.0.1.tgz#afdf9488fb1ecefc8348f6fb22f464e32a58b36b"
+ dependencies:
+ acorn "^3.0.4"
+
+acorn@^3.0.4:
+ version "3.3.0"
+ resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a"
+
+acorn@^4.0.3:
+ version "4.0.13"
+ resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.13.tgz#105495ae5361d697bd195c825192e1ad7f253787"
+
+acorn@^5.0.1:
+ version "5.1.1"
+ resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.1.1.tgz#53fe161111f912ab999ee887a90a0bc52822fd75"
+
+after@0.8.1:
+ version "0.8.1"
+ resolved "https://registry.yarnpkg.com/after/-/after-0.8.1.tgz#ab5d4fb883f596816d3515f8f791c0af486dd627"
+
+ajv-keywords@^1.0.0:
+ version "1.5.1"
+ resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-1.5.1.tgz#314dd0a4b3368fad3dfcdc54ede6171b886daf3c"
+
+ajv@^4.7.0, ajv@^4.9.1:
+ version "4.11.8"
+ resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.8.tgz#82ffb02b29e662ae53bdc20af15947706739c536"
+ dependencies:
+ co "^4.6.0"
+ json-stable-stringify "^1.0.1"
+
+ajv@^5.2.0:
+ version "5.2.2"
+ resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.2.2.tgz#47c68d69e86f5d953103b0074a9430dc63da5e39"
+ dependencies:
+ co "^4.6.0"
+ fast-deep-equal "^1.0.0"
+ json-schema-traverse "^0.3.0"
+ json-stable-stringify "^1.0.1"
+
+align-text@^0.1.1, align-text@^0.1.3:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/align-text/-/align-text-0.1.4.tgz#0cd90a561093f35d0a99256c22b7069433fad117"
+ dependencies:
+ kind-of "^3.0.2"
+ longest "^1.0.1"
+ repeat-string "^1.5.2"
+
+alter@~0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/alter/-/alter-0.2.0.tgz#c7588808617572034aae62480af26b1d4d1cb3cd"
+ dependencies:
+ stable "~0.1.3"
+
+amd-name-resolver@0.0.5:
+ version "0.0.5"
+ resolved "https://registry.yarnpkg.com/amd-name-resolver/-/amd-name-resolver-0.0.5.tgz#76962dac876ed3311b05d29c6a58c14e1ef3304b"
+ dependencies:
+ ensure-posix-path "^1.0.1"
+
+amd-name-resolver@0.0.6:
+ version "0.0.6"
+ resolved "https://registry.yarnpkg.com/amd-name-resolver/-/amd-name-resolver-0.0.6.tgz#d3e4ba2dfcaab1d820c1be9de947c67828cfe595"
+ dependencies:
+ ensure-posix-path "^1.0.1"
+
+amd-name-resolver@0.0.7:
+ version "0.0.7"
+ resolved "https://registry.yarnpkg.com/amd-name-resolver/-/amd-name-resolver-0.0.7.tgz#814301adfe8a2f109f6e84d5e935196efb669615"
+ dependencies:
+ ensure-posix-path "^1.0.1"
+
+amdefine@>=0.0.4:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5"
+
+ansi-escapes@^1.1.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-1.4.0.tgz#d3a8a83b319aa67793662b13e761c7911422306e"
+
+ansi-escapes@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-2.0.0.tgz#5bae52be424878dd9783e8910e3fc2922e83c81b"
+
+ansi-regex@^0.2.0, ansi-regex@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-0.2.1.tgz#0d8e946967a3d8143f93e24e298525fc1b2235f9"
+
+ansi-regex@^2.0.0, ansi-regex@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df"
+
+ansi-regex@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998"
+
+ansi-styles@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-1.1.0.tgz#eaecbf66cd706882760b2f4691582b8f55d7a7de"
+
+ansi-styles@^2.2.1:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe"
+
+ansi-styles@^3.0.0, ansi-styles@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.1.0.tgz#09c202d5c917ec23188caa5c9cb9179cd9547750"
+ dependencies:
+ color-convert "^1.0.0"
+
+ansicolors@~0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/ansicolors/-/ansicolors-0.2.1.tgz#be089599097b74a5c9c4a84a0cdbcdb62bd87aef"
+
+anymatch@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-1.3.0.tgz#a3e52fa39168c825ff57b0248126ce5a8ff95507"
+ dependencies:
+ arrify "^1.0.0"
+ micromatch "^2.1.5"
+
+aot-test-generators@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/aot-test-generators/-/aot-test-generators-0.1.0.tgz#43f0f615f97cb298d7919c1b0b4e6b7310b03cd0"
+ dependencies:
+ jsesc "^2.5.0"
+
+applause@1.2.2:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/applause/-/applause-1.2.2.tgz#a8468579e81f67397bb5634c29953bedcd0f56c0"
+ dependencies:
+ cson-parser "^1.1.0"
+ js-yaml "^3.3.0"
+ lodash "^3.10.0"
+
+aproba@^1.0.3:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.1.2.tgz#45c6629094de4e96f693ef7eab74ae079c240fc1"
+
+are-we-there-yet@~1.1.2:
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz#bb5dca382bb94f05e15194373d16fd3ba1ca110d"
+ dependencies:
+ delegates "^1.0.0"
+ readable-stream "^2.0.6"
+
+argparse@^1.0.7:
+ version "1.0.9"
+ resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.9.tgz#73d83bc263f86e97f8cc4f6bae1b0e90a7d22c86"
+ dependencies:
+ sprintf-js "~1.0.2"
+
+arr-diff@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-2.0.0.tgz#8f3b827f955a8bd669697e4a4256ac3ceae356cf"
+ dependencies:
+ arr-flatten "^1.0.1"
+
+arr-flatten@^1.0.1:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1"
+
+array-equal@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/array-equal/-/array-equal-1.0.0.tgz#8c2a5ef2472fd9ea742b04c77a75093ba2757c93"
+
+array-find-index@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1"
+
+array-flatten@1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2"
+
+array-to-error@^1.0.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/array-to-error/-/array-to-error-1.1.1.tgz#d68812926d14097a205579a667eeaf1856a44c07"
+ dependencies:
+ array-to-sentence "^1.1.0"
+
+array-to-sentence@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/array-to-sentence/-/array-to-sentence-1.1.0.tgz#c804956dafa53232495b205a9452753a258d39fc"
+
+array-union@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39"
+ dependencies:
+ array-uniq "^1.0.1"
+
+array-uniq@^1.0.1:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6"
+
+array-unique@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.2.1.tgz#a1d97ccafcbc2625cc70fadceb36a50c58b01a53"
+
+arraybuffer.slice@0.0.6:
+ version "0.0.6"
+ resolved "https://registry.yarnpkg.com/arraybuffer.slice/-/arraybuffer.slice-0.0.6.tgz#f33b2159f0532a3f3107a272c0ccfbd1ad2979ca"
+
+arrify@^1.0.0, arrify@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d"
+
+asn1@~0.2.3:
+ version "0.2.3"
+ resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.3.tgz#dac8787713c9966849fc8180777ebe9c1ddf3b86"
+
+assert-plus@1.0.0, assert-plus@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525"
+
+assert-plus@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-0.2.0.tgz#d74e1b87e7affc0db8aadb7021f3fe48101ab234"
+
+ast-traverse@~0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/ast-traverse/-/ast-traverse-0.1.1.tgz#69cf2b8386f19dcda1bb1e05d68fe359d8897de6"
+
+ast-types@0.8.12:
+ version "0.8.12"
+ resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.8.12.tgz#a0d90e4351bb887716c83fd637ebf818af4adfcc"
+
+ast-types@0.9.6:
+ version "0.9.6"
+ resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.9.6.tgz#102c9e9e9005d3e7e3829bf0c4fa24ee862ee9b9"
+
+async-disk-cache@^1.2.1:
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/async-disk-cache/-/async-disk-cache-1.3.2.tgz#ac53d6152843df202c9406e28d774362608d74dd"
+ dependencies:
+ debug "^2.1.3"
+ heimdalljs "^0.2.3"
+ istextorbinary "2.1.0"
+ mkdirp "^0.5.0"
+ rimraf "^2.5.3"
+ rsvp "^3.0.18"
+ username-sync "1.0.1"
+
+async-each@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d"
+
+async-foreach@^0.1.3:
+ version "0.1.3"
+ resolved "https://registry.yarnpkg.com/async-foreach/-/async-foreach-0.1.3.tgz#36121f845c0578172de419a97dbeb1d16ec34542"
+
+async-promise-queue@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/async-promise-queue/-/async-promise-queue-1.0.3.tgz#70c9c37635620f894978814b6c65e6e14e2573ee"
+ dependencies:
+ async "^2.4.1"
+
+async@^1.4.0, async@^1.5.0, async@^1.5.2:
+ version "1.5.2"
+ resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a"
+
+async@^2.4.1:
+ version "2.5.0"
+ resolved "https://registry.yarnpkg.com/async/-/async-2.5.0.tgz#843190fd6b7357a0b9e1c956edddd5ec8462b54d"
+ dependencies:
+ lodash "^4.14.0"
+
+async@~0.2.9:
+ version "0.2.10"
+ resolved "https://registry.yarnpkg.com/async/-/async-0.2.10.tgz#b6bbe0b0674b9d719708ca38de8c237cb526c3d1"
+
+async@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/async/-/async-1.0.0.tgz#f8fc04ca3a13784ade9e1641af98578cfbd647a9"
+
+asynckit@^0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
+
+aws-sign2@~0.6.0:
+ version "0.6.0"
+ resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.6.0.tgz#14342dd38dbcc94d0e5b87d763cd63612c0e794f"
+
+aws4@^1.2.1:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e"
+
+babel-code-frame@^6.16.0, babel-code-frame@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.22.0.tgz#027620bee567a88c32561574e7fd0801d33118e4"
+ dependencies:
+ chalk "^1.1.0"
+ esutils "^2.0.2"
+ js-tokens "^3.0.0"
+
+babel-core@^5.0.0:
+ version "5.8.38"
+ resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-5.8.38.tgz#1fcaee79d7e61b750b00b8e54f6dfc9d0af86558"
+ dependencies:
+ babel-plugin-constant-folding "^1.0.1"
+ babel-plugin-dead-code-elimination "^1.0.2"
+ babel-plugin-eval "^1.0.1"
+ babel-plugin-inline-environment-variables "^1.0.1"
+ babel-plugin-jscript "^1.0.4"
+ babel-plugin-member-expression-literals "^1.0.1"
+ babel-plugin-property-literals "^1.0.1"
+ babel-plugin-proto-to-assign "^1.0.3"
+ babel-plugin-react-constant-elements "^1.0.3"
+ babel-plugin-react-display-name "^1.0.3"
+ babel-plugin-remove-console "^1.0.1"
+ babel-plugin-remove-debugger "^1.0.1"
+ babel-plugin-runtime "^1.0.7"
+ babel-plugin-undeclared-variables-check "^1.0.2"
+ babel-plugin-undefined-to-void "^1.1.6"
+ babylon "^5.8.38"
+ bluebird "^2.9.33"
+ chalk "^1.0.0"
+ convert-source-map "^1.1.0"
+ core-js "^1.0.0"
+ debug "^2.1.1"
+ detect-indent "^3.0.0"
+ esutils "^2.0.0"
+ fs-readdir-recursive "^0.1.0"
+ globals "^6.4.0"
+ home-or-tmp "^1.0.0"
+ is-integer "^1.0.4"
+ js-tokens "1.0.1"
+ json5 "^0.4.0"
+ lodash "^3.10.0"
+ minimatch "^2.0.3"
+ output-file-sync "^1.1.0"
+ path-exists "^1.0.0"
+ path-is-absolute "^1.0.0"
+ private "^0.1.6"
+ regenerator "0.8.40"
+ regexpu "^1.3.0"
+ repeating "^1.1.2"
+ resolve "^1.1.6"
+ shebang-regex "^1.0.0"
+ slash "^1.0.0"
+ source-map "^0.5.0"
+ source-map-support "^0.2.10"
+ to-fast-properties "^1.0.0"
+ trim-right "^1.0.0"
+ try-resolve "^1.0.0"
+
+babel-core@^6.14.0, babel-core@^6.24.1:
+ version "6.25.0"
+ resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.25.0.tgz#7dd42b0463c742e9d5296deb3ec67a9322dad729"
+ dependencies:
+ babel-code-frame "^6.22.0"
+ babel-generator "^6.25.0"
+ babel-helpers "^6.24.1"
+ babel-messages "^6.23.0"
+ babel-register "^6.24.1"
+ babel-runtime "^6.22.0"
+ babel-template "^6.25.0"
+ babel-traverse "^6.25.0"
+ babel-types "^6.25.0"
+ babylon "^6.17.2"
+ convert-source-map "^1.1.0"
+ debug "^2.1.1"
+ json5 "^0.5.0"
+ lodash "^4.2.0"
+ minimatch "^3.0.2"
+ path-is-absolute "^1.0.0"
+ private "^0.1.6"
+ slash "^1.0.0"
+ source-map "^0.5.0"
+
+babel-generator@^6.25.0:
+ version "6.25.0"
+ resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.25.0.tgz#33a1af70d5f2890aeb465a4a7793c1df6a9ea9fc"
+ dependencies:
+ babel-messages "^6.23.0"
+ babel-runtime "^6.22.0"
+ babel-types "^6.25.0"
+ detect-indent "^4.0.0"
+ jsesc "^1.3.0"
+ lodash "^4.2.0"
+ source-map "^0.5.0"
+ trim-right "^1.0.1"
+
+babel-helper-builder-binary-assignment-operator-visitor@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-builder-binary-assignment-operator-visitor/-/babel-helper-builder-binary-assignment-operator-visitor-6.24.1.tgz#cce4517ada356f4220bcae8a02c2b346f9a56664"
+ dependencies:
+ babel-helper-explode-assignable-expression "^6.24.1"
+ babel-runtime "^6.22.0"
+ babel-types "^6.24.1"
+
+babel-helper-call-delegate@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-call-delegate/-/babel-helper-call-delegate-6.24.1.tgz#ece6aacddc76e41c3461f88bfc575bd0daa2df8d"
+ dependencies:
+ babel-helper-hoist-variables "^6.24.1"
+ babel-runtime "^6.22.0"
+ babel-traverse "^6.24.1"
+ babel-types "^6.24.1"
+
+babel-helper-define-map@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-define-map/-/babel-helper-define-map-6.24.1.tgz#7a9747f258d8947d32d515f6aa1c7bd02204a080"
+ dependencies:
+ babel-helper-function-name "^6.24.1"
+ babel-runtime "^6.22.0"
+ babel-types "^6.24.1"
+ lodash "^4.2.0"
+
+babel-helper-explode-assignable-expression@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-explode-assignable-expression/-/babel-helper-explode-assignable-expression-6.24.1.tgz#f25b82cf7dc10433c55f70592d5746400ac22caa"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-traverse "^6.24.1"
+ babel-types "^6.24.1"
+
+babel-helper-function-name@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz#d3475b8c03ed98242a25b48351ab18399d3580a9"
+ dependencies:
+ babel-helper-get-function-arity "^6.24.1"
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+ babel-traverse "^6.24.1"
+ babel-types "^6.24.1"
+
+babel-helper-get-function-arity@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.24.1.tgz#8f7782aa93407c41d3aa50908f89b031b1b6853d"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-types "^6.24.1"
+
+babel-helper-hoist-variables@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-hoist-variables/-/babel-helper-hoist-variables-6.24.1.tgz#1ecb27689c9d25513eadbc9914a73f5408be7a76"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-types "^6.24.1"
+
+babel-helper-optimise-call-expression@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-optimise-call-expression/-/babel-helper-optimise-call-expression-6.24.1.tgz#f7a13427ba9f73f8f4fa993c54a97882d1244257"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-types "^6.24.1"
+
+babel-helper-regex@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-regex/-/babel-helper-regex-6.24.1.tgz#d36e22fab1008d79d88648e32116868128456ce8"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-types "^6.24.1"
+ lodash "^4.2.0"
+
+babel-helper-remap-async-to-generator@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-remap-async-to-generator/-/babel-helper-remap-async-to-generator-6.24.1.tgz#5ec581827ad723fecdd381f1c928390676e4551b"
+ dependencies:
+ babel-helper-function-name "^6.24.1"
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+ babel-traverse "^6.24.1"
+ babel-types "^6.24.1"
+
+babel-helper-replace-supers@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-replace-supers/-/babel-helper-replace-supers-6.24.1.tgz#bf6dbfe43938d17369a213ca8a8bf74b6a90ab1a"
+ dependencies:
+ babel-helper-optimise-call-expression "^6.24.1"
+ babel-messages "^6.23.0"
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+ babel-traverse "^6.24.1"
+ babel-types "^6.24.1"
+
+babel-helpers@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helpers/-/babel-helpers-6.24.1.tgz#3471de9caec388e5c850e597e58a26ddf37602b2"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+
+babel-messages@^6.23.0:
+ version "6.23.0"
+ resolved "https://registry.yarnpkg.com/babel-messages/-/babel-messages-6.23.0.tgz#f3cdf4703858035b2a2951c6ec5edf6c62f2630e"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-check-es2015-constants@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-check-es2015-constants/-/babel-plugin-check-es2015-constants-6.22.0.tgz#35157b101426fd2ffd3da3f75c7d1e91835bbf8a"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-constant-folding@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-constant-folding/-/babel-plugin-constant-folding-1.0.1.tgz#8361d364c98e449c3692bdba51eff0844290aa8e"
+
+babel-plugin-dead-code-elimination@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/babel-plugin-dead-code-elimination/-/babel-plugin-dead-code-elimination-1.0.2.tgz#5f7c451274dcd7cccdbfbb3e0b85dd28121f0f65"
+
+babel-plugin-debug-macros@^0.1.10, babel-plugin-debug-macros@^0.1.11:
+ version "0.1.11"
+ resolved "https://registry.yarnpkg.com/babel-plugin-debug-macros/-/babel-plugin-debug-macros-0.1.11.tgz#6c562bf561fccd406ce14ab04f42c218cf956605"
+ dependencies:
+ semver "^5.3.0"
+
+babel-plugin-ember-modules-api-polyfill@^1.4.1:
+ version "1.4.2"
+ resolved "https://registry.yarnpkg.com/babel-plugin-ember-modules-api-polyfill/-/babel-plugin-ember-modules-api-polyfill-1.4.2.tgz#e254f8ed0ba7cf32ea6a71c4770b3568a8577402"
+ dependencies:
+ ember-rfc176-data "^0.2.0"
+
+babel-plugin-ember-modules-api-polyfill@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-ember-modules-api-polyfill/-/babel-plugin-ember-modules-api-polyfill-2.0.1.tgz#baaf26dcebe2ed1de120021bc42be29f520497b3"
+ dependencies:
+ ember-rfc176-data "^0.2.7"
+
+babel-plugin-ember-modules-api-polyfill@^2.3.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-ember-modules-api-polyfill/-/babel-plugin-ember-modules-api-polyfill-2.3.0.tgz#0c01f359658cfb9c797f705af6b09f6220205ae0"
+ dependencies:
+ ember-rfc176-data "^0.3.0"
+
+babel-plugin-eval@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-eval/-/babel-plugin-eval-1.0.1.tgz#a2faed25ce6be69ade4bfec263f70169195950da"
+
+babel-plugin-feature-flags@^0.2.1:
+ version "0.2.3"
+ resolved "https://registry.yarnpkg.com/babel-plugin-feature-flags/-/babel-plugin-feature-flags-0.2.3.tgz#81d81ed77bda2014098fa8243abcf03a551cbd4d"
+ dependencies:
+ json-stable-stringify "^1.0.1"
+
+babel-plugin-filter-imports@^0.2.0:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-filter-imports/-/babel-plugin-filter-imports-0.2.1.tgz#784f96a892f2f7ed2ccf0955688bd8916cd2e212"
+ dependencies:
+ json-stable-stringify "^1.0.1"
+
+babel-plugin-htmlbars-inline-precompile@^0.2.3:
+ version "0.2.3"
+ resolved "https://registry.yarnpkg.com/babel-plugin-htmlbars-inline-precompile/-/babel-plugin-htmlbars-inline-precompile-0.2.3.tgz#cd365e278af409bfa6be7704c4354beee742446b"
+
+babel-plugin-inline-environment-variables@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-inline-environment-variables/-/babel-plugin-inline-environment-variables-1.0.1.tgz#1f58ce91207ad6a826a8bf645fafe68ff5fe3ffe"
+
+babel-plugin-jscript@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/babel-plugin-jscript/-/babel-plugin-jscript-1.0.4.tgz#8f342c38276e87a47d5fa0a8bd3d5eb6ccad8fcc"
+
+babel-plugin-member-expression-literals@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-member-expression-literals/-/babel-plugin-member-expression-literals-1.0.1.tgz#cc5edb0faa8dc927170e74d6d1c02440021624d3"
+
+babel-plugin-property-literals@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-property-literals/-/babel-plugin-property-literals-1.0.1.tgz#0252301900192980b1c118efea48ce93aab83336"
+
+babel-plugin-proto-to-assign@^1.0.3:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/babel-plugin-proto-to-assign/-/babel-plugin-proto-to-assign-1.0.4.tgz#c49e7afd02f577bc4da05ea2df002250cf7cd123"
+ dependencies:
+ lodash "^3.9.3"
+
+babel-plugin-react-constant-elements@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/babel-plugin-react-constant-elements/-/babel-plugin-react-constant-elements-1.0.3.tgz#946736e8378429cbc349dcff62f51c143b34e35a"
+
+babel-plugin-react-display-name@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/babel-plugin-react-display-name/-/babel-plugin-react-display-name-1.0.3.tgz#754fe38926e8424a4e7b15ab6ea6139dee0514fc"
+
+babel-plugin-remove-console@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-remove-console/-/babel-plugin-remove-console-1.0.1.tgz#d8f24556c3a05005d42aaaafd27787f53ff013a7"
+
+babel-plugin-remove-debugger@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-remove-debugger/-/babel-plugin-remove-debugger-1.0.1.tgz#fd2ea3cd61a428ad1f3b9c89882ff4293e8c14c7"
+
+babel-plugin-runtime@^1.0.7:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/babel-plugin-runtime/-/babel-plugin-runtime-1.0.7.tgz#bf7c7d966dd56ecd5c17fa1cb253c9acb7e54aaf"
+
+babel-plugin-syntax-async-functions@^6.8.0:
+ version "6.13.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.13.0.tgz#cad9cad1191b5ad634bf30ae0872391e0647be95"
+
+babel-plugin-syntax-exponentiation-operator@^6.8.0:
+ version "6.13.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-syntax-exponentiation-operator/-/babel-plugin-syntax-exponentiation-operator-6.13.0.tgz#9ee7e8337290da95288201a6a57f4170317830de"
+
+babel-plugin-syntax-object-rest-spread@^6.8.0:
+ version "6.13.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz#fd6536f2bce13836ffa3a5458c4903a597bb3bf5"
+
+babel-plugin-syntax-trailing-function-commas@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-6.22.0.tgz#ba0360937f8d06e40180a43fe0d5616fff532cf3"
+
+babel-plugin-transform-async-to-generator@^6.22.0:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-async-to-generator/-/babel-plugin-transform-async-to-generator-6.24.1.tgz#6536e378aff6cb1d5517ac0e40eb3e9fc8d08761"
+ dependencies:
+ babel-helper-remap-async-to-generator "^6.24.1"
+ babel-plugin-syntax-async-functions "^6.8.0"
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-arrow-functions@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.22.0.tgz#452692cb711d5f79dc7f85e440ce41b9f244d221"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-block-scoped-functions@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-block-scoped-functions/-/babel-plugin-transform-es2015-block-scoped-functions-6.22.0.tgz#bbc51b49f964d70cb8d8e0b94e820246ce3a6141"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-block-scoping@^6.23.0:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.24.1.tgz#76c295dc3a4741b1665adfd3167215dcff32a576"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+ babel-traverse "^6.24.1"
+ babel-types "^6.24.1"
+ lodash "^4.2.0"
+
+babel-plugin-transform-es2015-classes@^6.23.0:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-classes/-/babel-plugin-transform-es2015-classes-6.24.1.tgz#5a4c58a50c9c9461e564b4b2a3bfabc97a2584db"
+ dependencies:
+ babel-helper-define-map "^6.24.1"
+ babel-helper-function-name "^6.24.1"
+ babel-helper-optimise-call-expression "^6.24.1"
+ babel-helper-replace-supers "^6.24.1"
+ babel-messages "^6.23.0"
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+ babel-traverse "^6.24.1"
+ babel-types "^6.24.1"
+
+babel-plugin-transform-es2015-computed-properties@^6.22.0:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-computed-properties/-/babel-plugin-transform-es2015-computed-properties-6.24.1.tgz#6fe2a8d16895d5634f4cd999b6d3480a308159b3"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+
+babel-plugin-transform-es2015-destructuring@^6.23.0:
+ version "6.23.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-destructuring/-/babel-plugin-transform-es2015-destructuring-6.23.0.tgz#997bb1f1ab967f682d2b0876fe358d60e765c56d"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-duplicate-keys@^6.22.0:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-duplicate-keys/-/babel-plugin-transform-es2015-duplicate-keys-6.24.1.tgz#73eb3d310ca969e3ef9ec91c53741a6f1576423e"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-types "^6.24.1"
+
+babel-plugin-transform-es2015-for-of@^6.23.0:
+ version "6.23.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-for-of/-/babel-plugin-transform-es2015-for-of-6.23.0.tgz#f47c95b2b613df1d3ecc2fdb7573623c75248691"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-function-name@^6.22.0:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-function-name/-/babel-plugin-transform-es2015-function-name-6.24.1.tgz#834c89853bc36b1af0f3a4c5dbaa94fd8eacaa8b"
+ dependencies:
+ babel-helper-function-name "^6.24.1"
+ babel-runtime "^6.22.0"
+ babel-types "^6.24.1"
+
+babel-plugin-transform-es2015-literals@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-literals/-/babel-plugin-transform-es2015-literals-6.22.0.tgz#4f54a02d6cd66cf915280019a31d31925377ca2e"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-modules-amd@^6.22.0, babel-plugin-transform-es2015-modules-amd@^6.24.0, babel-plugin-transform-es2015-modules-amd@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-amd/-/babel-plugin-transform-es2015-modules-amd-6.24.1.tgz#3b3e54017239842d6d19c3011c4bd2f00a00d154"
+ dependencies:
+ babel-plugin-transform-es2015-modules-commonjs "^6.24.1"
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+
+babel-plugin-transform-es2015-modules-commonjs@^6.23.0, babel-plugin-transform-es2015-modules-commonjs@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.24.1.tgz#d3e310b40ef664a36622200097c6d440298f2bfe"
+ dependencies:
+ babel-plugin-transform-strict-mode "^6.24.1"
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+ babel-types "^6.24.1"
+
+babel-plugin-transform-es2015-modules-systemjs@^6.23.0:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-systemjs/-/babel-plugin-transform-es2015-modules-systemjs-6.24.1.tgz#ff89a142b9119a906195f5f106ecf305d9407d23"
+ dependencies:
+ babel-helper-hoist-variables "^6.24.1"
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+
+babel-plugin-transform-es2015-modules-umd@^6.23.0:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-umd/-/babel-plugin-transform-es2015-modules-umd-6.24.1.tgz#ac997e6285cd18ed6176adb607d602344ad38468"
+ dependencies:
+ babel-plugin-transform-es2015-modules-amd "^6.24.1"
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+
+babel-plugin-transform-es2015-object-super@^6.22.0:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-object-super/-/babel-plugin-transform-es2015-object-super-6.24.1.tgz#24cef69ae21cb83a7f8603dad021f572eb278f8d"
+ dependencies:
+ babel-helper-replace-supers "^6.24.1"
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-parameters@^6.23.0:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-parameters/-/babel-plugin-transform-es2015-parameters-6.24.1.tgz#57ac351ab49caf14a97cd13b09f66fdf0a625f2b"
+ dependencies:
+ babel-helper-call-delegate "^6.24.1"
+ babel-helper-get-function-arity "^6.24.1"
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+ babel-traverse "^6.24.1"
+ babel-types "^6.24.1"
+
+babel-plugin-transform-es2015-shorthand-properties@^6.22.0:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-shorthand-properties/-/babel-plugin-transform-es2015-shorthand-properties-6.24.1.tgz#24f875d6721c87661bbd99a4622e51f14de38aa0"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-types "^6.24.1"
+
+babel-plugin-transform-es2015-spread@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-spread/-/babel-plugin-transform-es2015-spread-6.22.0.tgz#d6d68a99f89aedc4536c81a542e8dd9f1746f8d1"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-sticky-regex@^6.22.0:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-sticky-regex/-/babel-plugin-transform-es2015-sticky-regex-6.24.1.tgz#00c1cdb1aca71112cdf0cf6126c2ed6b457ccdbc"
+ dependencies:
+ babel-helper-regex "^6.24.1"
+ babel-runtime "^6.22.0"
+ babel-types "^6.24.1"
+
+babel-plugin-transform-es2015-template-literals@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-template-literals/-/babel-plugin-transform-es2015-template-literals-6.22.0.tgz#a84b3450f7e9f8f1f6839d6d687da84bb1236d8d"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-typeof-symbol@^6.23.0:
+ version "6.23.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-typeof-symbol/-/babel-plugin-transform-es2015-typeof-symbol-6.23.0.tgz#dec09f1cddff94b52ac73d505c84df59dcceb372"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-unicode-regex@^6.22.0:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-unicode-regex/-/babel-plugin-transform-es2015-unicode-regex-6.24.1.tgz#d38b12f42ea7323f729387f18a7c5ae1faeb35e9"
+ dependencies:
+ babel-helper-regex "^6.24.1"
+ babel-runtime "^6.22.0"
+ regexpu-core "^2.0.0"
+
+babel-plugin-transform-exponentiation-operator@^6.22.0:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-exponentiation-operator/-/babel-plugin-transform-exponentiation-operator-6.24.1.tgz#2ab0c9c7f3098fa48907772bb813fe41e8de3a0e"
+ dependencies:
+ babel-helper-builder-binary-assignment-operator-visitor "^6.24.1"
+ babel-plugin-syntax-exponentiation-operator "^6.8.0"
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-object-rest-spread@^6.23.0:
+ version "6.23.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-6.23.0.tgz#875d6bc9be761c58a2ae3feee5dc4895d8c7f921"
+ dependencies:
+ babel-plugin-syntax-object-rest-spread "^6.8.0"
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-regenerator@^6.22.0:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.24.1.tgz#b8da305ad43c3c99b4848e4fe4037b770d23c418"
+ dependencies:
+ regenerator-transform "0.9.11"
+
+babel-plugin-transform-strict-mode@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.24.1.tgz#d5faf7aa578a65bbe591cf5edae04a0c67020758"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-types "^6.24.1"
+
+babel-plugin-undeclared-variables-check@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/babel-plugin-undeclared-variables-check/-/babel-plugin-undeclared-variables-check-1.0.2.tgz#5cf1aa539d813ff64e99641290af620965f65dee"
+ dependencies:
+ leven "^1.0.2"
+
+babel-plugin-undefined-to-void@^1.1.6:
+ version "1.1.6"
+ resolved "https://registry.yarnpkg.com/babel-plugin-undefined-to-void/-/babel-plugin-undefined-to-void-1.1.6.tgz#7f578ef8b78dfae6003385d8417a61eda06e2f81"
+
+babel-polyfill@^6.16.0:
+ version "6.23.0"
+ resolved "https://registry.yarnpkg.com/babel-polyfill/-/babel-polyfill-6.23.0.tgz#8364ca62df8eafb830499f699177466c3b03499d"
+ dependencies:
+ babel-runtime "^6.22.0"
+ core-js "^2.4.0"
+ regenerator-runtime "^0.10.0"
+
+babel-preset-env@^1.5.1:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/babel-preset-env/-/babel-preset-env-1.6.0.tgz#2de1c782a780a0a5d605d199c957596da43c44e4"
+ dependencies:
+ babel-plugin-check-es2015-constants "^6.22.0"
+ babel-plugin-syntax-trailing-function-commas "^6.22.0"
+ babel-plugin-transform-async-to-generator "^6.22.0"
+ babel-plugin-transform-es2015-arrow-functions "^6.22.0"
+ babel-plugin-transform-es2015-block-scoped-functions "^6.22.0"
+ babel-plugin-transform-es2015-block-scoping "^6.23.0"
+ babel-plugin-transform-es2015-classes "^6.23.0"
+ babel-plugin-transform-es2015-computed-properties "^6.22.0"
+ babel-plugin-transform-es2015-destructuring "^6.23.0"
+ babel-plugin-transform-es2015-duplicate-keys "^6.22.0"
+ babel-plugin-transform-es2015-for-of "^6.23.0"
+ babel-plugin-transform-es2015-function-name "^6.22.0"
+ babel-plugin-transform-es2015-literals "^6.22.0"
+ babel-plugin-transform-es2015-modules-amd "^6.22.0"
+ babel-plugin-transform-es2015-modules-commonjs "^6.23.0"
+ babel-plugin-transform-es2015-modules-systemjs "^6.23.0"
+ babel-plugin-transform-es2015-modules-umd "^6.23.0"
+ babel-plugin-transform-es2015-object-super "^6.22.0"
+ babel-plugin-transform-es2015-parameters "^6.23.0"
+ babel-plugin-transform-es2015-shorthand-properties "^6.22.0"
+ babel-plugin-transform-es2015-spread "^6.22.0"
+ babel-plugin-transform-es2015-sticky-regex "^6.22.0"
+ babel-plugin-transform-es2015-template-literals "^6.22.0"
+ babel-plugin-transform-es2015-typeof-symbol "^6.23.0"
+ babel-plugin-transform-es2015-unicode-regex "^6.22.0"
+ babel-plugin-transform-exponentiation-operator "^6.22.0"
+ babel-plugin-transform-regenerator "^6.22.0"
+ browserslist "^2.1.2"
+ invariant "^2.2.2"
+ semver "^5.3.0"
+
+babel-register@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-register/-/babel-register-6.24.1.tgz#7e10e13a2f71065bdfad5a1787ba45bca6ded75f"
+ dependencies:
+ babel-core "^6.24.1"
+ babel-runtime "^6.22.0"
+ core-js "^2.4.0"
+ home-or-tmp "^2.0.0"
+ lodash "^4.2.0"
+ mkdirp "^0.5.1"
+ source-map-support "^0.4.2"
+
+babel-runtime@^6.18.0, babel-runtime@^6.22.0, babel-runtime@^6.23.0:
+ version "6.23.0"
+ resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.23.0.tgz#0a9489f144de70efb3ce4300accdb329e2fc543b"
+ dependencies:
+ core-js "^2.4.0"
+ regenerator-runtime "^0.10.0"
+
+babel-template@^6.24.1, babel-template@^6.25.0:
+ version "6.25.0"
+ resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.25.0.tgz#665241166b7c2aa4c619d71e192969552b10c071"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-traverse "^6.25.0"
+ babel-types "^6.25.0"
+ babylon "^6.17.2"
+ lodash "^4.2.0"
+
+babel-traverse@^6.24.1, babel-traverse@^6.25.0:
+ version "6.25.0"
+ resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.25.0.tgz#2257497e2fcd19b89edc13c4c91381f9512496f1"
+ dependencies:
+ babel-code-frame "^6.22.0"
+ babel-messages "^6.23.0"
+ babel-runtime "^6.22.0"
+ babel-types "^6.25.0"
+ babylon "^6.17.2"
+ debug "^2.2.0"
+ globals "^9.0.0"
+ invariant "^2.2.0"
+ lodash "^4.2.0"
+
+babel-types@^6.19.0, babel-types@^6.24.1, babel-types@^6.25.0:
+ version "6.25.0"
+ resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.25.0.tgz#70afb248d5660e5d18f811d91c8303b54134a18e"
+ dependencies:
+ babel-runtime "^6.22.0"
+ esutils "^2.0.2"
+ lodash "^4.2.0"
+ to-fast-properties "^1.0.1"
+
+babel5-plugin-strip-class-callcheck@^5.1.0:
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/babel5-plugin-strip-class-callcheck/-/babel5-plugin-strip-class-callcheck-5.1.0.tgz#77d4a40c8614d367b8a21a53908159806dba5f91"
+
+babel5-plugin-strip-heimdall@^5.0.2:
+ version "5.0.2"
+ resolved "https://registry.yarnpkg.com/babel5-plugin-strip-heimdall/-/babel5-plugin-strip-heimdall-5.0.2.tgz#e1fe191c34de79686564d50a86f4217b8df629c1"
+
+babylon@^5.8.38:
+ version "5.8.38"
+ resolved "https://registry.yarnpkg.com/babylon/-/babylon-5.8.38.tgz#ec9b120b11bf6ccd4173a18bf217e60b79859ffd"
+
+babylon@^6.17.2:
+ version "6.17.4"
+ resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.17.4.tgz#3e8b7402b88d22c3423e137a1577883b15ff869a"
+
+backbone@^1.1.2:
+ version "1.3.3"
+ resolved "https://registry.yarnpkg.com/backbone/-/backbone-1.3.3.tgz#4cc80ea7cb1631ac474889ce40f2f8bc683b2999"
+ dependencies:
+ underscore ">=1.8.3"
+
+backo2@1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947"
+
+balanced-match@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
+
+base64-arraybuffer@0.1.5:
+ version "0.1.5"
+ resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz#73926771923b5a19747ad666aa5cd4bf9c6e9ce8"
+
+base64id@0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/base64id/-/base64id-0.1.0.tgz#02ce0fdeee0cef4f40080e1e73e834f0b1bfce3f"
+
+basic-auth@~1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/basic-auth/-/basic-auth-1.1.0.tgz#45221ee429f7ee1e5035be3f51533f1cdfd29884"
+
+bcrypt-pbkdf@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz#63bc5dcb61331b92bc05fd528953c33462a06f8d"
+ dependencies:
+ tweetnacl "^0.14.3"
+
+better-assert@~1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/better-assert/-/better-assert-1.0.2.tgz#40866b9e1b9e0b55b481894311e68faffaebc522"
+ dependencies:
+ callsite "1.0.0"
+
+bignumber.js@^2.1.0:
+ version "2.4.0"
+ resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-2.4.0.tgz#838a992da9f9d737e0f4b2db0be62bb09dd0c5e8"
+
+binary-extensions@^1.0.0:
+ version "1.8.0"
+ resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.8.0.tgz#48ec8d16df4377eae5fa5884682480af4d95c774"
+
+"binaryextensions@1 || 2":
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/binaryextensions/-/binaryextensions-2.0.0.tgz#e597d1a7a6a3558a2d1c7241a16c99965e6aa40f"
+
+blank-object@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/blank-object/-/blank-object-1.0.2.tgz#f990793fbe9a8c8dd013fb3219420bec81d5f4b9"
+
+blob@0.0.4:
+ version "0.0.4"
+ resolved "https://registry.yarnpkg.com/blob/-/blob-0.0.4.tgz#bcf13052ca54463f30f9fc7e95b9a47630a94921"
+
+block-stream@*:
+ version "0.0.9"
+ resolved "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a"
+ dependencies:
+ inherits "~2.0.0"
+
+bluebird@^2.9.33:
+ version "2.11.0"
+ resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-2.11.0.tgz#534b9033c022c9579c56ba3b3e5a5caafbb650e1"
+
+bluebird@^3.1.1, bluebird@^3.3.5, bluebird@^3.4.6:
+ version "3.5.0"
+ resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.0.tgz#791420d7f551eea2897453a8a77653f96606d67c"
+
+bmp-js@0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/bmp-js/-/bmp-js-0.0.1.tgz#5ad0147099d13a9f38aa7b99af1d6e78666ed37f"
+
+bmp-js@0.0.3:
+ version "0.0.3"
+ resolved "https://registry.yarnpkg.com/bmp-js/-/bmp-js-0.0.3.tgz#64113e9c7cf1202b376ed607bf30626ebe57b18a"
+
+body@^5.1.0:
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/body/-/body-5.1.0.tgz#e4ba0ce410a46936323367609ecb4e6553125069"
+ dependencies:
+ continuable-cache "^0.3.1"
+ error "^7.0.0"
+ raw-body "~1.1.0"
+ safe-json-parse "~1.0.1"
+
+boolbase@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
+
+boolify@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/boolify/-/boolify-1.0.1.tgz#b5c09e17cacd113d11b7bb3ed384cc012994d86b"
+
+boom@2.x.x:
+ version "2.10.1"
+ resolved "https://registry.yarnpkg.com/boom/-/boom-2.10.1.tgz#39c8918ceff5799f83f9492a848f625add0c766f"
+ dependencies:
+ hoek "2.x.x"
+
+bower-config@^1.3.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/bower-config/-/bower-config-1.4.0.tgz#16c38c1135f8071c19f25938d61b0d8cbf18d3f1"
+ dependencies:
+ graceful-fs "^4.1.3"
+ mout "^1.0.0"
+ optimist "^0.6.1"
+ osenv "^0.1.3"
+ untildify "^2.1.0"
+
+bower-endpoint-parser@0.2.2:
+ version "0.2.2"
+ resolved "https://registry.yarnpkg.com/bower-endpoint-parser/-/bower-endpoint-parser-0.2.2.tgz#00b565adbfab6f2d35addde977e97962acbcb3f6"
+
+brace-expansion@^1.0.0, brace-expansion@^1.1.7:
+ version "1.1.8"
+ resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.8.tgz#c07b211c7c952ec1f8efd51a77ef0d1d3990a292"
+ dependencies:
+ balanced-match "^1.0.0"
+ concat-map "0.0.1"
+
+braces@^1.8.2:
+ version "1.8.5"
+ resolved "https://registry.yarnpkg.com/braces/-/braces-1.8.5.tgz#ba77962e12dff969d6b76711e914b737857bf6a7"
+ dependencies:
+ expand-range "^1.8.1"
+ preserve "^0.2.0"
+ repeat-element "^1.1.2"
+
+breakable@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/breakable/-/breakable-1.0.0.tgz#784a797915a38ead27bad456b5572cb4bbaa78c1"
+
+broccoli-asset-rev@^2.4.5:
+ version "2.5.0"
+ resolved "https://registry.yarnpkg.com/broccoli-asset-rev/-/broccoli-asset-rev-2.5.0.tgz#f5f66eac962bf9f086286921f0eaeaab6d00d819"
+ dependencies:
+ broccoli-asset-rewrite "^1.1.0"
+ broccoli-filter "^1.2.2"
+ json-stable-stringify "^1.0.0"
+ matcher-collection "^1.0.1"
+ rsvp "^3.0.6"
+
+broccoli-asset-rewrite@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/broccoli-asset-rewrite/-/broccoli-asset-rewrite-1.1.0.tgz#77a5da56157aa318c59113245e8bafb4617f8830"
+ dependencies:
+ broccoli-filter "^1.2.3"
+
+broccoli-babel-transpiler@^5.5.0, broccoli-babel-transpiler@^5.6.0, broccoli-babel-transpiler@^5.6.2:
+ version "5.7.1"
+ resolved "https://registry.yarnpkg.com/broccoli-babel-transpiler/-/broccoli-babel-transpiler-5.7.1.tgz#e10d831faed1c57e37272e4223748ba71a7926d1"
+ dependencies:
+ babel-core "^5.0.0"
+ broccoli-funnel "^1.0.0"
+ broccoli-merge-trees "^1.0.0"
+ broccoli-persistent-filter "^1.4.2"
+ clone "^0.2.0"
+ hash-for-dep "^1.0.2"
+ heimdalljs-logger "^0.1.7"
+ json-stable-stringify "^1.0.0"
+ rsvp "^3.5.0"
+ workerpool "^2.2.1"
+
+broccoli-babel-transpiler@^6.0.0:
+ version "6.1.1"
+ resolved "https://registry.yarnpkg.com/broccoli-babel-transpiler/-/broccoli-babel-transpiler-6.1.1.tgz#938f470e1ddb47047a77ef5e38f34c21de0e85a8"
+ dependencies:
+ babel-core "^6.14.0"
+ broccoli-funnel "^1.0.0"
+ broccoli-merge-trees "^1.0.0"
+ broccoli-persistent-filter "^1.4.0"
+ clone "^2.0.0"
+ hash-for-dep "^1.0.2"
+ heimdalljs-logger "^0.1.7"
+ json-stable-stringify "^1.0.0"
+ rsvp "^3.5.0"
+ workerpool "^2.2.1"
+
+broccoli-babel-transpiler@^6.1.2:
+ version "6.1.2"
+ resolved "https://registry.yarnpkg.com/broccoli-babel-transpiler/-/broccoli-babel-transpiler-6.1.2.tgz#26019c045b5ea3e44cfef62821302f9bd483cabd"
+ dependencies:
+ babel-core "^6.14.0"
+ broccoli-funnel "^1.0.0"
+ broccoli-merge-trees "^1.0.0"
+ broccoli-persistent-filter "^1.4.0"
+ clone "^2.0.0"
+ hash-for-dep "^1.0.2"
+ heimdalljs-logger "^0.1.7"
+ json-stable-stringify "^1.0.0"
+ rsvp "^3.5.0"
+ workerpool "^2.2.1"
+
+broccoli-brocfile-loader@^0.18.0:
+ version "0.18.0"
+ resolved "https://registry.yarnpkg.com/broccoli-brocfile-loader/-/broccoli-brocfile-loader-0.18.0.tgz#2e86021c805c34ffc8d29a2fb721cf273e819e4b"
+ dependencies:
+ findup-sync "^0.4.2"
+
+broccoli-builder@^0.18.3:
+ version "0.18.8"
+ resolved "https://registry.yarnpkg.com/broccoli-builder/-/broccoli-builder-0.18.8.tgz#fe54694d544c3cdfdb01028e802eeca65749a879"
+ dependencies:
+ heimdalljs "^0.2.0"
+ promise-map-series "^0.2.1"
+ quick-temp "^0.1.2"
+ rimraf "^2.2.8"
+ rsvp "^3.0.17"
+ silent-error "^1.0.1"
+
+broccoli-caching-writer@^2.2.0, broccoli-caching-writer@^2.2.1:
+ version "2.3.1"
+ resolved "https://registry.yarnpkg.com/broccoli-caching-writer/-/broccoli-caching-writer-2.3.1.tgz#b93cf58f9264f003075868db05774f4e7f25bd07"
+ dependencies:
+ broccoli-kitchen-sink-helpers "^0.2.5"
+ broccoli-plugin "1.1.0"
+ debug "^2.1.1"
+ rimraf "^2.2.8"
+ rsvp "^3.0.17"
+ walk-sync "^0.2.5"
+
+broccoli-caching-writer@^3.0.3:
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/broccoli-caching-writer/-/broccoli-caching-writer-3.0.3.tgz#0bd2c96a9738d6a6ab590f07ba35c5157d7db476"
+ dependencies:
+ broccoli-kitchen-sink-helpers "^0.3.1"
+ broccoli-plugin "^1.2.1"
+ debug "^2.1.1"
+ rimraf "^2.2.8"
+ rsvp "^3.0.17"
+ walk-sync "^0.3.0"
+
+broccoli-clean-css@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/broccoli-clean-css/-/broccoli-clean-css-1.1.0.tgz#9db143d9af7e0ae79c26e3ac5a9bb2d720ea19fa"
+ dependencies:
+ broccoli-persistent-filter "^1.1.6"
+ clean-css-promise "^0.1.0"
+ inline-source-map-comment "^1.0.5"
+ json-stable-stringify "^1.0.0"
+
+broccoli-concat@^3.2.2:
+ version "3.2.2"
+ resolved "https://registry.yarnpkg.com/broccoli-concat/-/broccoli-concat-3.2.2.tgz#86ffdc52606eb590ba9f6b894c5ec7a016f5b7b9"
+ dependencies:
+ broccoli-kitchen-sink-helpers "^0.3.1"
+ broccoli-plugin "^1.3.0"
+ broccoli-stew "^1.3.3"
+ ensure-posix-path "^1.0.2"
+ fast-sourcemap-concat "^1.0.1"
+ find-index "^1.1.0"
+ fs-extra "^1.0.0"
+ fs-tree-diff "^0.5.6"
+ lodash.merge "^4.3.0"
+ lodash.omit "^4.1.0"
+ lodash.uniq "^4.2.0"
+ walk-sync "^0.3.1"
+
+broccoli-config-loader@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/broccoli-config-loader/-/broccoli-config-loader-1.0.1.tgz#d10aaf8ebc0cb45c1da5baa82720e1d88d28c80a"
+ dependencies:
+ broccoli-caching-writer "^3.0.3"
+
+broccoli-config-replace@^1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/broccoli-config-replace/-/broccoli-config-replace-1.1.2.tgz#6ea879d92a5bad634d11329b51fc5f4aafda9c00"
+ dependencies:
+ broccoli-kitchen-sink-helpers "^0.3.1"
+ broccoli-plugin "^1.2.0"
+ debug "^2.2.0"
+ fs-extra "^0.24.0"
+
+broccoli-debug@^0.6.1, broccoli-debug@^0.6.2:
+ version "0.6.2"
+ resolved "https://registry.yarnpkg.com/broccoli-debug/-/broccoli-debug-0.6.2.tgz#4c6e89459fc3de7d5d4fc7b77e57f46019f44db1"
+ dependencies:
+ broccoli-plugin "^1.2.1"
+ fs-tree-diff "^0.5.2"
+ heimdalljs "^0.2.1"
+ heimdalljs-logger "^0.1.7"
+ minimatch "^3.0.3"
+ symlink-or-copy "^1.1.8"
+ tree-sync "^1.2.2"
+
+broccoli-favicon@1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/broccoli-favicon/-/broccoli-favicon-1.0.0.tgz#c770a5aa16032fbaf1b5c9c033f71b9cc5a5cb51"
+ dependencies:
+ bluebird "^3.3.5"
+ broccoli-caching-writer "^2.2.1"
+ favicons "^4.7.1"
+ lodash "^4.10.0"
+
+broccoli-file-creator@^1.0.0, broccoli-file-creator@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/broccoli-file-creator/-/broccoli-file-creator-1.1.1.tgz#1b35b67d215abdfadd8d49eeb69493c39e6c3450"
+ dependencies:
+ broccoli-kitchen-sink-helpers "~0.2.0"
+ broccoli-plugin "^1.1.0"
+ broccoli-writer "~0.1.1"
+ mkdirp "^0.5.1"
+ rsvp "~3.0.6"
+ symlink-or-copy "^1.0.1"
+
+broccoli-filter@^0.1.11:
+ version "0.1.14"
+ resolved "https://registry.yarnpkg.com/broccoli-filter/-/broccoli-filter-0.1.14.tgz#23cae3891ff9ebb7b4d7db00c6dcf03535daf7ad"
+ dependencies:
+ broccoli-kitchen-sink-helpers "^0.2.6"
+ broccoli-writer "^0.1.1"
+ mkdirp "^0.3.5"
+ promise-map-series "^0.2.1"
+ quick-temp "^0.1.2"
+ rsvp "^3.0.16"
+ symlink-or-copy "^1.0.1"
+ walk-sync "^0.1.3"
+
+broccoli-filter@^1.0.1, broccoli-filter@^1.2.2, broccoli-filter@^1.2.3:
+ version "1.2.4"
+ resolved "https://registry.yarnpkg.com/broccoli-filter/-/broccoli-filter-1.2.4.tgz#409afb94b9a3a6da9fac8134e91e205f40cc7330"
+ dependencies:
+ broccoli-kitchen-sink-helpers "^0.3.1"
+ broccoli-plugin "^1.0.0"
+ copy-dereference "^1.0.0"
+ debug "^2.2.0"
+ mkdirp "^0.5.1"
+ promise-map-series "^0.2.1"
+ rsvp "^3.0.18"
+ symlink-or-copy "^1.0.1"
+ walk-sync "^0.3.1"
+
+broccoli-funnel-reducer@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/broccoli-funnel-reducer/-/broccoli-funnel-reducer-1.0.0.tgz#11365b2a785aec9b17972a36df87eef24c5cc0ea"
+
+broccoli-funnel@^1.0.0, broccoli-funnel@^1.0.1, broccoli-funnel@^1.0.2, broccoli-funnel@^1.0.6, broccoli-funnel@^1.1.0, broccoli-funnel@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/broccoli-funnel/-/broccoli-funnel-1.2.0.tgz#cddc3afc5ff1685a8023488fff74ce6fb5a51296"
+ dependencies:
+ array-equal "^1.0.0"
+ blank-object "^1.0.1"
+ broccoli-plugin "^1.3.0"
+ debug "^2.2.0"
+ exists-sync "0.0.4"
+ fast-ordered-set "^1.0.0"
+ fs-tree-diff "^0.5.3"
+ heimdalljs "^0.2.0"
+ minimatch "^3.0.0"
+ mkdirp "^0.5.0"
+ path-posix "^1.0.0"
+ rimraf "^2.4.3"
+ symlink-or-copy "^1.0.0"
+ walk-sync "^0.3.1"
+
+broccoli-funnel@^2.0.0, broccoli-funnel@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/broccoli-funnel/-/broccoli-funnel-2.0.1.tgz#6823c73b675ef78fffa7ab800f083e768b51d449"
+ dependencies:
+ array-equal "^1.0.0"
+ blank-object "^1.0.1"
+ broccoli-plugin "^1.3.0"
+ debug "^2.2.0"
+ fast-ordered-set "^1.0.0"
+ fs-tree-diff "^0.5.3"
+ heimdalljs "^0.2.0"
+ minimatch "^3.0.0"
+ mkdirp "^0.5.0"
+ path-posix "^1.0.0"
+ rimraf "^2.4.3"
+ symlink-or-copy "^1.0.0"
+ walk-sync "^0.3.1"
+
+broccoli-kitchen-sink-helpers@^0.2.5, broccoli-kitchen-sink-helpers@^0.2.6, broccoli-kitchen-sink-helpers@~0.2.0:
+ version "0.2.9"
+ resolved "https://registry.yarnpkg.com/broccoli-kitchen-sink-helpers/-/broccoli-kitchen-sink-helpers-0.2.9.tgz#a5e0986ed8d76fb5984b68c3f0450d3a96e36ecc"
+ dependencies:
+ glob "^5.0.10"
+ mkdirp "^0.5.1"
+
+broccoli-kitchen-sink-helpers@^0.3.1:
+ version "0.3.1"
+ resolved "https://registry.yarnpkg.com/broccoli-kitchen-sink-helpers/-/broccoli-kitchen-sink-helpers-0.3.1.tgz#77c7c18194b9664163ec4fcee2793444926e0c06"
+ dependencies:
+ glob "^5.0.10"
+ mkdirp "^0.5.1"
+
+broccoli-lint-eslint@^4.0.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/broccoli-lint-eslint/-/broccoli-lint-eslint-4.1.0.tgz#dccfa1150dc62407cd66fd56a619273c5479a10e"
+ dependencies:
+ aot-test-generators "^0.1.0"
+ broccoli-concat "^3.2.2"
+ broccoli-persistent-filter "^1.2.0"
+ eslint "^4.0.0"
+ json-stable-stringify "^1.0.1"
+ lodash.defaultsdeep "^4.6.0"
+ md5-hex "^2.0.0"
+
+broccoli-merge-trees@^1.0.0, broccoli-merge-trees@^1.1.0, broccoli-merge-trees@^1.1.1, broccoli-merge-trees@^1.1.4:
+ version "1.2.4"
+ resolved "https://registry.yarnpkg.com/broccoli-merge-trees/-/broccoli-merge-trees-1.2.4.tgz#a001519bb5067f06589d91afa2942445a2d0fdb5"
+ dependencies:
+ broccoli-plugin "^1.3.0"
+ can-symlink "^1.0.0"
+ fast-ordered-set "^1.0.2"
+ fs-tree-diff "^0.5.4"
+ heimdalljs "^0.2.1"
+ heimdalljs-logger "^0.1.7"
+ rimraf "^2.4.3"
+ symlink-or-copy "^1.0.0"
+
+broccoli-merge-trees@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/broccoli-merge-trees/-/broccoli-merge-trees-2.0.0.tgz#10aea46dd5cebcc8b8f7d5a54f0a84a4f0bb90b9"
+ dependencies:
+ broccoli-plugin "^1.3.0"
+ merge-trees "^1.0.1"
+
+broccoli-middleware@^1.0.0-beta.8:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/broccoli-middleware/-/broccoli-middleware-1.0.0.tgz#92f4e1fb9a791ea986245a7077f35cc648dab097"
+ dependencies:
+ handlebars "^4.0.4"
+ mime "^1.2.11"
+
+broccoli-persistent-filter@^1.0.3, broccoli-persistent-filter@^1.1.6, broccoli-persistent-filter@^1.2.0, broccoli-persistent-filter@^1.4.0, broccoli-persistent-filter@^1.4.2:
+ version "1.4.2"
+ resolved "https://registry.yarnpkg.com/broccoli-persistent-filter/-/broccoli-persistent-filter-1.4.2.tgz#17af1278a25ff2556f9d7d23e115accfad3a7ce7"
+ dependencies:
+ async-disk-cache "^1.2.1"
+ async-promise-queue "^1.0.3"
+ broccoli-plugin "^1.0.0"
+ crypto "0.0.3"
+ fs-tree-diff "^0.5.2"
+ hash-for-dep "^1.0.2"
+ heimdalljs "^0.2.1"
+ heimdalljs-logger "^0.1.7"
+ mkdirp "^0.5.1"
+ promise-map-series "^0.2.1"
+ rimraf "^2.6.1"
+ rsvp "^3.0.18"
+ symlink-or-copy "^1.0.1"
+ walk-sync "^0.3.1"
+
+broccoli-persistent-filter@^1.1.5:
+ version "1.4.3"
+ resolved "https://registry.yarnpkg.com/broccoli-persistent-filter/-/broccoli-persistent-filter-1.4.3.tgz#3511bc52fc53740cda51621f58a28152d9911bc1"
+ dependencies:
+ async-disk-cache "^1.2.1"
+ async-promise-queue "^1.0.3"
+ broccoli-plugin "^1.0.0"
+ fs-tree-diff "^0.5.2"
+ hash-for-dep "^1.0.2"
+ heimdalljs "^0.2.1"
+ heimdalljs-logger "^0.1.7"
+ mkdirp "^0.5.1"
+ promise-map-series "^0.2.1"
+ rimraf "^2.6.1"
+ rsvp "^3.0.18"
+ symlink-or-copy "^1.0.1"
+ walk-sync "^0.3.1"
+
+broccoli-plugin@1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/broccoli-plugin/-/broccoli-plugin-1.1.0.tgz#73e2cfa05f8ea1e3fc1420c40c3d9e7dc724bf02"
+ dependencies:
+ promise-map-series "^0.2.1"
+ quick-temp "^0.1.3"
+ rimraf "^2.3.4"
+ symlink-or-copy "^1.0.1"
+
+broccoli-plugin@^1.0.0, broccoli-plugin@^1.1.0, broccoli-plugin@^1.2.0, broccoli-plugin@^1.2.1, broccoli-plugin@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/broccoli-plugin/-/broccoli-plugin-1.3.0.tgz#bee704a8e42da08cb58e513aaa436efb7f0ef1ee"
+ dependencies:
+ promise-map-series "^0.2.1"
+ quick-temp "^0.1.3"
+ rimraf "^2.3.4"
+ symlink-or-copy "^1.1.8"
+
+broccoli-replace@^0.12.0:
+ version "0.12.0"
+ resolved "https://registry.yarnpkg.com/broccoli-replace/-/broccoli-replace-0.12.0.tgz#36460a984c45c61731638c53068b0ab12ea8fdb7"
+ dependencies:
+ applause "1.2.2"
+ broccoli-persistent-filter "^1.2.0"
+ minimatch "^3.0.0"
+
+broccoli-sass-source-maps@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/broccoli-sass-source-maps/-/broccoli-sass-source-maps-2.0.0.tgz#7f25f9f4b296918cec6e00672c63e75abce33d45"
+ dependencies:
+ broccoli-caching-writer "^3.0.3"
+ include-path-searcher "^0.1.0"
+ mkdirp "^0.3.5"
+ node-sass "^4.1.0"
+ object-assign "^2.0.0"
+ rsvp "^3.0.6"
+
+broccoli-slow-trees@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/broccoli-slow-trees/-/broccoli-slow-trees-3.0.1.tgz#9bf2a9e2f8eb3ed3a3f2abdde988da437ccdc9b4"
+ dependencies:
+ heimdalljs "^0.2.1"
+
+broccoli-source@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/broccoli-source/-/broccoli-source-1.1.0.tgz#54f0e82c8b73f46580cbbc4f578f0b32fca8f809"
+
+broccoli-sri-hash@^2.1.0, broccoli-sri-hash@meirish/broccoli-sri-hash#rooturl:
+ version "2.1.2"
+ resolved "https://codeload.github.com/meirish/broccoli-sri-hash/tar.gz/5ebad6f345c38d45461676c7a298a0b61be4a39d"
+ dependencies:
+ broccoli-caching-writer "^2.2.0"
+ mkdirp "^0.5.1"
+ rsvp "^3.1.0"
+ sri-toolbox "^0.2.0"
+ symlink-or-copy "^1.0.1"
+
+broccoli-stew@^1.0.0, broccoli-stew@^1.2.0, broccoli-stew@^1.3.3, broccoli-stew@^1.4.0, broccoli-stew@^1.4.2, broccoli-stew@^1.5.0:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/broccoli-stew/-/broccoli-stew-1.5.0.tgz#d7af8c18511dce510e49d308a62e5977f461883c"
+ dependencies:
+ broccoli-debug "^0.6.1"
+ broccoli-funnel "^1.0.1"
+ broccoli-merge-trees "^1.0.0"
+ broccoli-persistent-filter "^1.1.6"
+ broccoli-plugin "^1.3.0"
+ chalk "^1.1.3"
+ debug "^2.4.0"
+ ensure-posix-path "^1.0.1"
+ fs-extra "^2.0.0"
+ minimatch "^3.0.2"
+ resolve "^1.1.6"
+ rsvp "^3.0.16"
+ symlink-or-copy "^1.1.8"
+ walk-sync "^0.3.0"
+
+broccoli-string-replace@^0.1.1:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/broccoli-string-replace/-/broccoli-string-replace-0.1.2.tgz#1ed92f85680af8d503023925e754e4e33676b91f"
+ dependencies:
+ broccoli-persistent-filter "^1.1.5"
+ minimatch "^3.0.3"
+
+broccoli-templater@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/broccoli-templater/-/broccoli-templater-1.0.0.tgz#7c054aacf596d1868d1a44291f9ec7b907d30ecf"
+ dependencies:
+ broccoli-filter "^0.1.11"
+ broccoli-stew "^1.2.0"
+ lodash.template "^3.3.2"
+
+broccoli-uglify-sourcemap@^1.0.0:
+ version "1.5.2"
+ resolved "https://registry.yarnpkg.com/broccoli-uglify-sourcemap/-/broccoli-uglify-sourcemap-1.5.2.tgz#04f84ab0db539031fa868ccfa563c9932d50cedb"
+ dependencies:
+ broccoli-plugin "^1.2.1"
+ debug "^2.2.0"
+ lodash.merge "^4.5.1"
+ matcher-collection "^1.0.0"
+ mkdirp "^0.5.0"
+ source-map-url "^0.3.0"
+ symlink-or-copy "^1.0.1"
+ uglify-js "^2.7.0"
+ walk-sync "^0.1.3"
+
+broccoli-unwatched-tree@^0.1.1:
+ version "0.1.3"
+ resolved "https://registry.yarnpkg.com/broccoli-unwatched-tree/-/broccoli-unwatched-tree-0.1.3.tgz#ab0fb820f613845bf67a803baad820f68b1e3aae"
+ dependencies:
+ broccoli-source "^1.1.0"
+
+broccoli-writer@^0.1.1, broccoli-writer@~0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/broccoli-writer/-/broccoli-writer-0.1.1.tgz#d4d71aa8f2afbc67a3866b91a2da79084b96ab2d"
+ dependencies:
+ quick-temp "^0.1.0"
+ rsvp "^3.0.6"
+
+browserslist@^2.1.2:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-2.2.0.tgz#5e35ec993e467c6464b8cb708447386891de9f50"
+ dependencies:
+ caniuse-lite "^1.0.30000701"
+ electron-to-chromium "^1.3.15"
+
+bser@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/bser/-/bser-2.0.0.tgz#9ac78d3ed5d915804fd87acb158bc797147a1719"
+ dependencies:
+ node-int64 "^0.4.0"
+
+buffer-alloc-unsafe@^0.1.0:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/buffer-alloc-unsafe/-/buffer-alloc-unsafe-0.1.1.tgz#ffe1f67551dd055737de253337bfe853dfab1a6a"
+
+buffer-alloc@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/buffer-alloc/-/buffer-alloc-1.1.0.tgz#05514d33bf1656d3540c684f65b1202e90eca303"
+ dependencies:
+ buffer-alloc-unsafe "^0.1.0"
+ buffer-fill "^0.1.0"
+
+buffer-equal@0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/buffer-equal/-/buffer-equal-0.0.1.tgz#91bc74b11ea405bc916bc6aa908faafa5b4aac4b"
+
+buffer-fill@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/buffer-fill/-/buffer-fill-0.1.0.tgz#ca9470e8d4d1b977fd7543f4e2ab6a7dc95101a8"
+
+build@^0.1.4:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/build/-/build-0.1.4.tgz#707fe026ffceddcacbfdcdf356eafda64f151046"
+ dependencies:
+ cssmin "0.3.x"
+ jsmin "1.x"
+ jxLoader "*"
+ moo-server "*"
+ promised-io "*"
+ timespan "2.x"
+ uglify-js "1.x"
+ walker "1.x"
+ winston "*"
+ wrench "1.3.x"
+
+builtin-modules@^1.0.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f"
+
+builtins@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/builtins/-/builtins-1.0.3.tgz#cb94faeb61c8696451db36534e1422f94f0aee88"
+
+bulma-switch@^0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/bulma-switch/-/bulma-switch-0.0.1.tgz#2de6eb7c602244de7c5efa880b3b19b8464012a9"
+
+bulma@^0.5.2:
+ version "0.5.2"
+ resolved "https://registry.yarnpkg.com/bulma/-/bulma-0.5.2.tgz#b5c4695075700b9539619555840d8f4f9f84b3a5"
+
+bytes@1:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/bytes/-/bytes-1.0.0.tgz#3569ede8ba34315fab99c3e92cb04c7220de1fa8"
+
+bytes@2.5.0:
+ version "2.5.0"
+ resolved "https://registry.yarnpkg.com/bytes/-/bytes-2.5.0.tgz#4c9423ea2d252c270c41b2bdefeff9bb6b62c06a"
+
+calculate-cache-key-for-tree@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/calculate-cache-key-for-tree/-/calculate-cache-key-for-tree-1.1.0.tgz#0c3e42c9c134f3c9de5358c0f16793627ea976d6"
+ dependencies:
+ json-stable-stringify "^1.0.1"
+
+caller-path@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-0.1.0.tgz#94085ef63581ecd3daa92444a8fe94e82577751f"
+ dependencies:
+ callsites "^0.2.0"
+
+callsite@1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/callsite/-/callsite-1.0.0.tgz#280398e5d664bd74038b6f0905153e6e8af1bc20"
+
+callsites@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/callsites/-/callsites-0.2.0.tgz#afab96262910a7f33c19a5775825c69f34e350ca"
+
+camelcase-keys@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-2.1.0.tgz#308beeaffdf28119051efa1d932213c91b8f92e7"
+ dependencies:
+ camelcase "^2.0.0"
+ map-obj "^1.0.0"
+
+camelcase-keys@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-4.1.0.tgz#214d348cc5457f39316a2c31cc3e37246325e73f"
+ dependencies:
+ camelcase "^4.1.0"
+ map-obj "^2.0.0"
+ quick-lru "^1.0.0"
+
+camelcase@^1.0.2, camelcase@^1.2.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-1.2.1.tgz#9bb5304d2e0b56698b2c758b08a3eaa9daa58a39"
+
+camelcase@^2.0.0, camelcase@^2.0.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f"
+
+camelcase@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-3.0.0.tgz#32fc4b9fcdaf845fcdf7e73bb97cac2261f0ab0a"
+
+camelcase@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd"
+
+can-symlink@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/can-symlink/-/can-symlink-1.0.0.tgz#97b607d8a84bb6c6e228b902d864ecb594b9d219"
+ dependencies:
+ tmp "0.0.28"
+
+caniuse-lite@^1.0.30000701:
+ version "1.0.30000701"
+ resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000701.tgz#9d673cf6b74dcb3d5c21d213176b011ac6a45baa"
+
+capture-exit@^1.1.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/capture-exit/-/capture-exit-1.2.0.tgz#1c5fcc489fd0ab00d4f1ac7ae1072e3173fbab6f"
+ dependencies:
+ rsvp "^3.3.3"
+
+cardinal@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/cardinal/-/cardinal-1.0.0.tgz#50e21c1b0aa37729f9377def196b5a9cec932ee9"
+ dependencies:
+ ansicolors "~0.2.1"
+ redeyed "~1.0.0"
+
+caseless@~0.11.0:
+ version "0.11.0"
+ resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.11.0.tgz#715b96ea9841593cc33067923f5ec60ebda4f7d7"
+
+caseless@~0.12.0:
+ version "0.12.0"
+ resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
+
+ceibo@~2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/ceibo/-/ceibo-2.0.0.tgz#9a61eb054a91c09934588d4e45d9dd2c3bf04eee"
+
+center-align@^0.1.1:
+ version "0.1.3"
+ resolved "https://registry.yarnpkg.com/center-align/-/center-align-0.1.3.tgz#aa0d32629b6ee972200411cbd4461c907bc2b7ad"
+ dependencies:
+ align-text "^0.1.3"
+ lazy-cache "^1.0.3"
+
+chalk@^0.5.1:
+ version "0.5.1"
+ resolved "https://registry.yarnpkg.com/chalk/-/chalk-0.5.1.tgz#663b3a648b68b55d04690d49167aa837858f2174"
+ dependencies:
+ ansi-styles "^1.1.0"
+ escape-string-regexp "^1.0.0"
+ has-ansi "^0.1.0"
+ strip-ansi "^0.3.0"
+ supports-color "^0.2.0"
+
+chalk@^1.0.0, chalk@^1.1.0, chalk@^1.1.1, chalk@^1.1.3:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98"
+ dependencies:
+ ansi-styles "^2.2.1"
+ escape-string-regexp "^1.0.2"
+ has-ansi "^2.0.0"
+ strip-ansi "^3.0.0"
+ supports-color "^2.0.0"
+
+chalk@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.0.1.tgz#dbec49436d2ae15f536114e76d14656cdbc0f44d"
+ dependencies:
+ ansi-styles "^3.1.0"
+ escape-string-regexp "^1.0.5"
+ supports-color "^4.0.0"
+
+charm@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/charm/-/charm-1.0.2.tgz#8add367153a6d9a581331052c4090991da995e35"
+ dependencies:
+ inherits "^2.0.1"
+
+cheerio@^0.19.0:
+ version "0.19.0"
+ resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-0.19.0.tgz#772e7015f2ee29965096d71ea4175b75ab354925"
+ dependencies:
+ css-select "~1.0.0"
+ dom-serializer "~0.1.0"
+ entities "~1.1.1"
+ htmlparser2 "~3.8.1"
+ lodash "^3.2.0"
+
+chokidar@1.6.1:
+ version "1.6.1"
+ resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.6.1.tgz#2f4447ab5e96e50fb3d789fd90d4c72e0e4c70c2"
+ dependencies:
+ anymatch "^1.3.0"
+ async-each "^1.0.0"
+ glob-parent "^2.0.0"
+ inherits "^2.0.1"
+ is-binary-path "^1.0.0"
+ is-glob "^2.0.0"
+ path-is-absolute "^1.0.0"
+ readdirp "^2.0.0"
+ optionalDependencies:
+ fsevents "^1.0.0"
+
+circular-json@^0.3.1:
+ version "0.3.1"
+ resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.3.1.tgz#be8b36aefccde8b3ca7aa2d6afc07a37242c0d2d"
+
+clean-base-url@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/clean-base-url/-/clean-base-url-1.0.0.tgz#c901cf0a20b972435b0eccd52d056824a4351b7b"
+
+clean-css-promise@^0.1.0:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/clean-css-promise/-/clean-css-promise-0.1.1.tgz#43f3d2c8dfcb2bf071481252cd9b76433c08eecb"
+ dependencies:
+ array-to-error "^1.0.0"
+ clean-css "^3.4.5"
+ pinkie-promise "^2.0.0"
+
+clean-css@^3.4.5:
+ version "3.4.28"
+ resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-3.4.28.tgz#bf1945e82fc808f55695e6ddeaec01400efd03ff"
+ dependencies:
+ commander "2.8.x"
+ source-map "0.4.x"
+
+cli-cursor@^1.0.1, cli-cursor@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-1.0.2.tgz#64da3f7d56a54412e59794bd62dc35295e8f2987"
+ dependencies:
+ restore-cursor "^1.0.1"
+
+cli-cursor@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5"
+ dependencies:
+ restore-cursor "^2.0.0"
+
+cli-spinners@^0.1.2:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-0.1.2.tgz#bb764d88e185fb9e1e6a2a1f19772318f605e31c"
+
+cli-table2@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/cli-table2/-/cli-table2-0.2.0.tgz#2d1ef7f218a0e786e214540562d4bd177fe32d97"
+ dependencies:
+ lodash "^3.10.1"
+ string-width "^1.0.1"
+ optionalDependencies:
+ colors "^1.1.2"
+
+cli-table@^0.3.1:
+ version "0.3.1"
+ resolved "https://registry.yarnpkg.com/cli-table/-/cli-table-0.3.1.tgz#f53b05266a8b1a0b934b3d0821e6e2dc5914ae23"
+ dependencies:
+ colors "1.0.3"
+
+cli-width@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.1.0.tgz#b234ca209b29ef66fc518d9b98d5847b00edf00a"
+
+clipboard@^1.7.1:
+ version "1.7.1"
+ resolved "https://registry.yarnpkg.com/clipboard/-/clipboard-1.7.1.tgz#360d6d6946e99a7a1fef395e42ba92b5e9b5a16b"
+ dependencies:
+ good-listener "^1.2.2"
+ select "^1.1.2"
+ tiny-emitter "^2.0.0"
+
+cliui@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/cliui/-/cliui-2.1.0.tgz#4b475760ff80264c762c3a1719032e91c7fea0d1"
+ dependencies:
+ center-align "^0.1.1"
+ right-align "^0.1.1"
+ wordwrap "0.0.2"
+
+cliui@^3.0.3, cliui@^3.2.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/cliui/-/cliui-3.2.0.tgz#120601537a916d29940f934da3b48d585a39213d"
+ dependencies:
+ string-width "^1.0.1"
+ strip-ansi "^3.0.1"
+ wrap-ansi "^2.0.0"
+
+clone-stats@^0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/clone-stats/-/clone-stats-0.0.1.tgz#b88f94a82cf38b8791d58046ea4029ad88ca99d1"
+
+clone@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/clone/-/clone-0.2.0.tgz#c6126a90ad4f72dbf5acdb243cc37724fe93fc1f"
+
+clone@^1.0.0, clone@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.2.tgz#260b7a99ebb1edfe247538175f783243cb19d149"
+
+clone@^2.0.0:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.1.tgz#d217d1e961118e3ac9a4b8bba3285553bf647cdb"
+
+co@^4.6.0:
+ version "4.6.0"
+ resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
+
+code-point-at@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
+
+coffee-script@^1.10.0:
+ version "1.12.7"
+ resolved "https://registry.yarnpkg.com/coffee-script/-/coffee-script-1.12.7.tgz#c05dae0cb79591d05b3070a8433a98c9a89ccc53"
+
+color-convert@^1.0.0:
+ version "1.9.0"
+ resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.0.tgz#1accf97dd739b983bf994d56fec8f95853641b7a"
+ dependencies:
+ color-name "^1.1.1"
+
+color-name@^1.1.1:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
+
+colors@1.0.3, colors@1.0.x:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b"
+
+colors@^1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63"
+
+combined-stream@^1.0.5, combined-stream@~1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.5.tgz#938370a57b4a51dea2c77c15d5c5fdf895164009"
+ dependencies:
+ delayed-stream "~1.0.0"
+
+commander@2.8.x:
+ version "2.8.1"
+ resolved "https://registry.yarnpkg.com/commander/-/commander-2.8.1.tgz#06be367febfda0c330aa1e2a072d3dc9762425d4"
+ dependencies:
+ graceful-readlink ">= 1.0.0"
+
+commander@2.9.0, commander@^2.5.0, commander@^2.6.0, commander@^2.9.0:
+ version "2.9.0"
+ resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4"
+ dependencies:
+ graceful-readlink ">= 1.0.0"
+
+common-tags@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/common-tags/-/common-tags-1.4.0.tgz#1187be4f3d4cf0c0427d43f74eef1f73501614c0"
+ dependencies:
+ babel-runtime "^6.18.0"
+
+commoner@~0.10.3:
+ version "0.10.8"
+ resolved "https://registry.yarnpkg.com/commoner/-/commoner-0.10.8.tgz#34fc3672cd24393e8bb47e70caa0293811f4f2c5"
+ dependencies:
+ commander "^2.5.0"
+ detective "^4.3.1"
+ glob "^5.0.15"
+ graceful-fs "^4.1.2"
+ iconv-lite "^0.4.5"
+ mkdirp "^0.5.0"
+ private "^0.1.6"
+ q "^1.1.2"
+ recast "^0.11.17"
+
+component-bind@1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/component-bind/-/component-bind-1.0.0.tgz#00c608ab7dcd93897c0009651b1d3a8e1e73bbd1"
+
+component-emitter@1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.1.2.tgz#296594f2753daa63996d2af08d15a95116c9aec3"
+
+component-emitter@1.2.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6"
+
+component-inherit@0.0.3:
+ version "0.0.3"
+ resolved "https://registry.yarnpkg.com/component-inherit/-/component-inherit-0.0.3.tgz#645fc4adf58b72b649d5cae65135619db26ff143"
+
+compressible@~2.0.10:
+ version "2.0.10"
+ resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.10.tgz#feda1c7f7617912732b29bf8cf26252a20b9eecd"
+ dependencies:
+ mime-db ">= 1.27.0 < 2"
+
+compression@^1.4.4:
+ version "1.7.0"
+ resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.0.tgz#030c9f198f1643a057d776a738e922da4373012d"
+ dependencies:
+ accepts "~1.3.3"
+ bytes "2.5.0"
+ compressible "~2.0.10"
+ debug "2.6.8"
+ on-headers "~1.0.1"
+ safe-buffer "5.1.1"
+ vary "~1.1.1"
+
+concat-map@0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
+
+concat-stream@1.5.0:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.5.0.tgz#53f7d43c51c5e43f81c8fdd03321c631be68d611"
+ dependencies:
+ inherits "~2.0.1"
+ readable-stream "~2.0.0"
+ typedarray "~0.0.5"
+
+concat-stream@^1.4.7, concat-stream@^1.5.2, concat-stream@^1.6.0:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.0.tgz#0aac662fd52be78964d5532f694784e70110acf7"
+ dependencies:
+ inherits "^2.0.3"
+ readable-stream "^2.2.2"
+ typedarray "^0.0.6"
+
+configstore@^3.0.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/configstore/-/configstore-3.1.0.tgz#45df907073e26dfa1cf4b2d52f5b60545eaa11d1"
+ dependencies:
+ dot-prop "^4.1.0"
+ graceful-fs "^4.1.2"
+ make-dir "^1.0.0"
+ unique-string "^1.0.0"
+ write-file-atomic "^2.0.0"
+ xdg-basedir "^3.0.0"
+
+console-control-strings@^1.0.0, console-control-strings@~1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e"
+
+console-ui@^1.0.2:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/console-ui/-/console-ui-1.0.3.tgz#31c524461b63422769f9e89c173495d91393721c"
+ dependencies:
+ chalk "^1.1.3"
+ inquirer "^1.2.3"
+ ora "^0.2.0"
+ through "^2.3.8"
+
+consolidate@^0.14.0:
+ version "0.14.5"
+ resolved "https://registry.yarnpkg.com/consolidate/-/consolidate-0.14.5.tgz#5a25047bc76f73072667c8cb52c989888f494c63"
+ dependencies:
+ bluebird "^3.1.1"
+
+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@~1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.2.tgz#b7d113aee7a8dd27bd21133c4dc2529df1721eed"
+
+continuable-cache@^0.3.1:
+ version "0.3.1"
+ resolved "https://registry.yarnpkg.com/continuable-cache/-/continuable-cache-0.3.1.tgz#bd727a7faed77e71ff3985ac93351a912733ad0f"
+
+convert-source-map@^1.1.0:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.5.0.tgz#9acd70851c6d5dfdd93d9282e5edf94a03ff46b5"
+
+cookie-signature@1.0.6:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
+
+cookie@0.3.1:
+ version "0.3.1"
+ resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb"
+
+cool-checkboxes-for-bulma.io@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/cool-checkboxes-for-bulma.io/-/cool-checkboxes-for-bulma.io-1.1.0.tgz#4715f5144b952b9c9d19eab5315d738359fae833"
+
+copy-dereference@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/copy-dereference/-/copy-dereference-1.0.0.tgz#6b131865420fd81b413ba994b44d3655311152b6"
+
+core-js@^1.0.0:
+ version "1.2.7"
+ resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636"
+
+core-js@^2.4.0:
+ version "2.4.1"
+ resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.4.1.tgz#4de911e667b0eae9124e34254b53aea6fc618d3e"
+
+core-object@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/core-object/-/core-object-1.1.0.tgz#86d63918733cf9da1a5aae729e62c0a88e66ad0a"
+
+core-object@^3.1.3:
+ version "3.1.3"
+ resolved "https://registry.yarnpkg.com/core-object/-/core-object-3.1.3.tgz#df399b3311bdb0c909e8aae8929fc3c1c4b25880"
+ dependencies:
+ chalk "^1.1.3"
+
+core-util-is@~1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
+
+cross-spawn@^3.0.0:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-3.0.1.tgz#1256037ecb9f0c5f79e3d6ef135e30770184b982"
+ dependencies:
+ lru-cache "^4.0.1"
+ which "^1.2.9"
+
+cross-spawn@^5.0.1, cross-spawn@^5.1.0:
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449"
+ dependencies:
+ lru-cache "^4.0.1"
+ shebang-command "^1.2.0"
+ which "^1.2.9"
+
+cryptiles@2.x.x:
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8"
+ dependencies:
+ boom "2.x.x"
+
+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"
+
+crypto@0.0.3:
+ version "0.0.3"
+ resolved "https://registry.yarnpkg.com/crypto/-/crypto-0.0.3.tgz#470a81b86be4c5ee17acc8207a1f5315ae20dbb0"
+
+cson-parser@^1.1.0:
+ version "1.3.5"
+ resolved "https://registry.yarnpkg.com/cson-parser/-/cson-parser-1.3.5.tgz#7ec675e039145533bf2a6a856073f1599d9c2d24"
+ dependencies:
+ coffee-script "^1.10.0"
+
+css-select@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/css-select/-/css-select-1.0.0.tgz#b1121ca51848dd264e2244d058cee254deeb44b0"
+ dependencies:
+ boolbase "~1.0.0"
+ css-what "1.0"
+ domutils "1.4"
+ nth-check "~1.0.0"
+
+css-what@1.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/css-what/-/css-what-1.0.0.tgz#d7cc2df45180666f99d2b14462639469e00f736c"
+
+cssmin@0.3.x:
+ version "0.3.2"
+ resolved "https://registry.yarnpkg.com/cssmin/-/cssmin-0.3.2.tgz#ddce4c547b510ae0d594a8f1fbf8aaf8e2c5c00d"
+
+currently-unhandled@^0.4.1:
+ version "0.4.1"
+ resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea"
+ dependencies:
+ array-find-index "^1.0.1"
+
+cycle@1.0.x:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/cycle/-/cycle-1.0.3.tgz#21e80b2be8580f98b468f379430662b046c34ad2"
+
+d@1:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/d/-/d-1.0.0.tgz#754bb5bfe55451da69a58b94d45f4c5b0462d58f"
+ dependencies:
+ es5-ext "^0.10.9"
+
+dag-map@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/dag-map/-/dag-map-2.0.2.tgz#9714b472de82a1843de2fba9b6876938cab44c68"
+
+dashdash@^1.12.0:
+ version "1.14.1"
+ resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
+ dependencies:
+ assert-plus "^1.0.0"
+
+debug@0.7.4:
+ version "0.7.4"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-0.7.4.tgz#06e1ea8082c2cb14e39806e22e2f6f757f92af39"
+
+debug@2.2.0, debug@~2.2.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-2.2.0.tgz#f87057e995b1a1f6ae6a4960664137bc56f039da"
+ dependencies:
+ ms "0.7.1"
+
+debug@2.3.3:
+ version "2.3.3"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-2.3.3.tgz#40c453e67e6e13c901ddec317af8986cda9eff8c"
+ dependencies:
+ ms "0.7.2"
+
+debug@2.6.7:
+ version "2.6.7"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.7.tgz#92bad1f6d05bbb6bba22cca88bcd0ec894c2861e"
+ dependencies:
+ ms "2.0.0"
+
+debug@2.6.8, debug@^2.1.0, debug@^2.1.1, debug@^2.1.3, debug@^2.2.0, debug@^2.4.0, debug@^2.6.8, debug@~2.6.7:
+ version "2.6.8"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.8.tgz#e731531ca2ede27d188222427da17821d68ff4fc"
+ dependencies:
+ ms "2.0.0"
+
+decamelize@^1.0.0, decamelize@^1.1.1, decamelize@^1.1.2:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
+
+deep-extend@~0.4.0:
+ version "0.4.2"
+ resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.4.2.tgz#48b699c27e334bf89f10892be432f6e4c7d34a7f"
+
+deep-is@~0.1.3:
+ version "0.1.3"
+ resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34"
+
+defined@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693"
+
+defs@~1.1.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/defs/-/defs-1.1.1.tgz#b22609f2c7a11ba7a3db116805c139b1caffa9d2"
+ dependencies:
+ alter "~0.2.0"
+ ast-traverse "~0.1.1"
+ breakable "~1.0.0"
+ esprima-fb "~15001.1001.0-dev-harmony-fb"
+ simple-fmt "~0.1.0"
+ simple-is "~0.2.0"
+ stringmap "~0.2.2"
+ stringset "~0.2.1"
+ tryor "~0.1.2"
+ yargs "~3.27.0"
+
+del@^2.0.2:
+ version "2.2.2"
+ resolved "https://registry.yarnpkg.com/del/-/del-2.2.2.tgz#c12c981d067846c84bcaf862cff930d907ffd1a8"
+ dependencies:
+ globby "^5.0.0"
+ is-path-cwd "^1.0.0"
+ is-path-in-cwd "^1.0.0"
+ object-assign "^4.0.1"
+ pify "^2.0.0"
+ pinkie-promise "^2.0.0"
+ rimraf "^2.2.8"
+
+delayed-stream@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
+
+delegate@^3.1.2:
+ version "3.1.3"
+ resolved "https://registry.yarnpkg.com/delegate/-/delegate-3.1.3.tgz#9a8251a777d7025faa55737bc3b071742127a9fd"
+
+delegates@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
+
+depd@1.1.0, depd@~1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.0.tgz#e1bd82c6aab6ced965b97b88b17ed3e528ca18c3"
+
+destroy@~1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80"
+
+detect-file@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/detect-file/-/detect-file-0.1.0.tgz#4935dedfd9488648e006b0129566e9386711ea63"
+ dependencies:
+ fs-exists-sync "^0.1.0"
+
+detect-indent@^3.0.0:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-3.0.1.tgz#9dc5e5ddbceef8325764b9451b02bc6d54084f75"
+ dependencies:
+ get-stdin "^4.0.1"
+ minimist "^1.1.0"
+ repeating "^1.1.0"
+
+detect-indent@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-4.0.0.tgz#f76d064352cdf43a1cb6ce619c4ee3a9475de208"
+ dependencies:
+ repeating "^2.0.0"
+
+detective@^4.3.1:
+ version "4.5.0"
+ resolved "https://registry.yarnpkg.com/detective/-/detective-4.5.0.tgz#6e5a8c6b26e6c7a254b1c6b6d7490d98ec91edd1"
+ dependencies:
+ acorn "^4.0.3"
+ defined "^1.0.0"
+
+diff@^3.1.0:
+ version "3.4.0"
+ resolved "https://registry.yarnpkg.com/diff/-/diff-3.4.0.tgz#b1d85507daf3964828de54b37d0d73ba67dda56c"
+
+diff@^3.2.0:
+ version "3.3.0"
+ resolved "https://registry.yarnpkg.com/diff/-/diff-3.3.0.tgz#056695150d7aa93237ca7e378ac3b1682b7963b9"
+
+dlv@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/dlv/-/dlv-1.1.0.tgz#fee1a7c43f63be75f3f679e85262da5f102764a7"
+
+doctrine@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.0.0.tgz#c73d8d2909d22291e1a007a395804da8b665fe63"
+ dependencies:
+ esutils "^2.0.2"
+ isarray "^1.0.0"
+
+dom-serializer@0, dom-serializer@~0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.0.tgz#073c697546ce0780ce23be4a28e293e40bc30c82"
+ dependencies:
+ domelementtype "~1.1.1"
+ entities "~1.1.1"
+
+dom-walk@^0.1.0:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/dom-walk/-/dom-walk-0.1.1.tgz#672226dc74c8f799ad35307df936aba11acd6018"
+
+domelementtype@1, domelementtype@~1.1.1:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.1.3.tgz#bd28773e2642881aec51544924299c5cd822185b"
+
+domhandler@2.3:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.3.0.tgz#2de59a0822d5027fabff6f032c2b25a2a8abe738"
+ dependencies:
+ domelementtype "1"
+
+domutils@1.4:
+ version "1.4.3"
+ resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.4.3.tgz#0865513796c6b306031850e175516baf80b72a6f"
+ dependencies:
+ domelementtype "1"
+
+domutils@1.5:
+ version "1.5.1"
+ resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf"
+ dependencies:
+ dom-serializer "0"
+ domelementtype "1"
+
+dot-prop@^4.1.0:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-4.1.1.tgz#a8493f0b7b5eeec82525b5c7587fa7de7ca859c1"
+ dependencies:
+ is-obj "^1.0.0"
+
+ecc-jsbn@~0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz#0fc73a9ed5f0d53c38193398523ef7e543777505"
+ dependencies:
+ jsbn "~0.1.0"
+
+editions@^1.1.1:
+ version "1.3.3"
+ resolved "https://registry.yarnpkg.com/editions/-/editions-1.3.3.tgz#0907101bdda20fac3cbe334c27cbd0688dc99a5b"
+
+ee-first@1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
+
+electron-to-chromium@^1.3.15:
+ version "1.3.15"
+ resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.15.tgz#08397934891cbcfaebbd18b82a95b5a481138369"
+
+ember-ajax@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/ember-ajax/-/ember-ajax-3.0.0.tgz#8f21e9da0c1d433cf879aa855fce464d517e9ab5"
+ dependencies:
+ ember-cli-babel "^6.0.0"
+
+ember-api-actions@^0.1.8:
+ version "0.1.8"
+ resolved "https://registry.yarnpkg.com/ember-api-actions/-/ember-api-actions-0.1.8.tgz#651031b9d61a320c221dd75b20f7e8f783e6393d"
+ dependencies:
+ ember-cli-babel "^6.0.0"
+
+ember-assign-helper@0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/ember-assign-helper/-/ember-assign-helper-0.1.1.tgz#217f221f37781b64657bd371d9da911768c3fbd1"
+ dependencies:
+ ember-cli-babel "^5.1.7"
+
+ember-basic-dropdown-hover@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/ember-basic-dropdown-hover/-/ember-basic-dropdown-hover-0.2.0.tgz#bbedb70a6858562bb6ad00c55c26406cac3a8264"
+ dependencies:
+ ember-assign-helper "0.1.1"
+ ember-basic-dropdown "^0.33.5"
+ ember-cli-babel "^5.1.7"
+ ember-cli-htmlbars "^1.1.1"
+
+ember-basic-dropdown@^0.33.5:
+ version "0.33.5"
+ resolved "https://registry.yarnpkg.com/ember-basic-dropdown/-/ember-basic-dropdown-0.33.5.tgz#39986d4cc6732edf43fb51eabb70790e99e8ae2c"
+ dependencies:
+ ember-cli-babel "^6.8.1"
+ ember-cli-htmlbars "^2.0.3"
+ ember-native-dom-helpers "^0.5.3"
+ ember-wormhole "^0.5.2"
+
+ember-cli-babel@5.1.10:
+ version "5.1.10"
+ resolved "https://registry.yarnpkg.com/ember-cli-babel/-/ember-cli-babel-5.1.10.tgz#d403f178aab602e1337c403c5a58c0200a8969aa"
+ dependencies:
+ broccoli-babel-transpiler "^5.6.0"
+ broccoli-funnel "^1.0.0"
+ clone "^1.0.2"
+ ember-cli-version-checker "^1.0.2"
+ resolve "^1.1.2"
+
+ember-cli-babel@^5.0.0, ember-cli-babel@^5.1.5, ember-cli-babel@^5.1.6, ember-cli-babel@^5.1.7:
+ version "5.2.4"
+ resolved "https://registry.yarnpkg.com/ember-cli-babel/-/ember-cli-babel-5.2.4.tgz#5ce4f46b08ed6f6d21e878619fb689719d6e8e13"
+ dependencies:
+ broccoli-babel-transpiler "^5.6.2"
+ broccoli-funnel "^1.0.0"
+ clone "^2.0.0"
+ ember-cli-version-checker "^1.0.2"
+ resolve "^1.1.2"
+
+ember-cli-babel@^6.0.0, ember-cli-babel@^6.3.0:
+ version "6.6.0"
+ resolved "https://registry.yarnpkg.com/ember-cli-babel/-/ember-cli-babel-6.6.0.tgz#a8362bc44841bfdf89b389f3197f104d7ba526da"
+ dependencies:
+ amd-name-resolver "0.0.6"
+ babel-plugin-debug-macros "^0.1.11"
+ babel-plugin-ember-modules-api-polyfill "^1.4.1"
+ babel-plugin-transform-es2015-modules-amd "^6.24.0"
+ babel-polyfill "^6.16.0"
+ babel-preset-env "^1.5.1"
+ broccoli-babel-transpiler "^6.0.0"
+ broccoli-debug "^0.6.2"
+ broccoli-funnel "^1.0.0"
+ broccoli-source "^1.1.0"
+ clone "^2.0.0"
+ ember-cli-version-checker "^2.0.0"
+
+ember-cli-babel@^6.0.0-beta.4, ember-cli-babel@^6.10.0, ember-cli-babel@^6.8.2:
+ version "6.11.0"
+ resolved "https://registry.yarnpkg.com/ember-cli-babel/-/ember-cli-babel-6.11.0.tgz#79cb184bac3c05bfe181ddc306bac100ab1f9493"
+ dependencies:
+ amd-name-resolver "0.0.7"
+ babel-plugin-debug-macros "^0.1.11"
+ babel-plugin-ember-modules-api-polyfill "^2.3.0"
+ babel-plugin-transform-es2015-modules-amd "^6.24.0"
+ babel-polyfill "^6.16.0"
+ babel-preset-env "^1.5.1"
+ broccoli-babel-transpiler "^6.1.2"
+ broccoli-debug "^0.6.2"
+ broccoli-funnel "^1.0.0"
+ broccoli-source "^1.1.0"
+ clone "^2.0.0"
+ ember-cli-version-checker "^2.1.0"
+ semver "^5.4.1"
+
+ember-cli-babel@^6.0.0-beta.7, ember-cli-babel@^6.1.0, ember-cli-babel@^6.6.0, ember-cli-babel@^6.8.1:
+ version "6.8.2"
+ resolved "https://registry.yarnpkg.com/ember-cli-babel/-/ember-cli-babel-6.8.2.tgz#eac2785964f4743f4c815cd53c6288f00cc087d7"
+ dependencies:
+ amd-name-resolver "0.0.7"
+ babel-plugin-debug-macros "^0.1.11"
+ babel-plugin-ember-modules-api-polyfill "^2.0.1"
+ babel-plugin-transform-es2015-modules-amd "^6.24.0"
+ babel-polyfill "^6.16.0"
+ babel-preset-env "^1.5.1"
+ broccoli-babel-transpiler "^6.1.2"
+ broccoli-debug "^0.6.2"
+ broccoli-funnel "^1.0.0"
+ broccoli-source "^1.1.0"
+ clone "^2.0.0"
+ ember-cli-version-checker "^2.0.0"
+
+ember-cli-broccoli-sane-watcher@^2.0.4:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/ember-cli-broccoli-sane-watcher/-/ember-cli-broccoli-sane-watcher-2.0.4.tgz#f43f42f75b7509c212fb926cd9aea86ae19264c6"
+ dependencies:
+ broccoli-slow-trees "^3.0.1"
+ heimdalljs "^0.2.1"
+ heimdalljs-logger "^0.1.7"
+ rsvp "^3.0.18"
+ sane "^1.1.1"
+
+ember-cli-clipboard@^0.8.0:
+ version "0.8.0"
+ resolved "https://registry.yarnpkg.com/ember-cli-clipboard/-/ember-cli-clipboard-0.8.0.tgz#c2e91290b2746c1a4903097f5d7a55406de539b1"
+ dependencies:
+ broccoli-funnel "^1.1.0"
+ clipboard "^1.7.1"
+ ember-cli-babel "^6.3.0"
+ ember-cli-htmlbars "^2.0.2"
+ fastboot-transform "0.1.1"
+
+ember-cli-dependency-checker@^1.3.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/ember-cli-dependency-checker/-/ember-cli-dependency-checker-1.4.0.tgz#2b13f977e1eea843fc1a21a001be6ca5d4ef1942"
+ dependencies:
+ chalk "^0.5.1"
+ is-git-url "^0.2.0"
+ semver "^4.1.0"
+
+ember-cli-eslint@4:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/ember-cli-eslint/-/ember-cli-eslint-4.1.0.tgz#50e43224e71849b7c03f73d5e5c4647b48993033"
+ dependencies:
+ broccoli-lint-eslint "^4.0.0"
+ ember-cli-version-checker "^2.0.0"
+ rsvp "^3.2.1"
+ walk-sync "^0.3.0"
+
+ember-cli-favicon@1.0.0-beta.4:
+ version "1.0.0-beta.4"
+ resolved "https://registry.yarnpkg.com/ember-cli-favicon/-/ember-cli-favicon-1.0.0-beta.4.tgz#8c27d47cb4124691939b3f0f7602848a265b0594"
+ dependencies:
+ broccoli-favicon "1.0.0"
+ broccoli-merge-trees "^1.1.1"
+ broccoli-replace "^0.12.0"
+ ember-cli-babel "^5.1.6"
+
+ember-cli-flash@^1.5.0:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/ember-cli-flash/-/ember-cli-flash-1.5.0.tgz#d4e0edf618376ffbf648512d92d5ff7a0f0ffb0c"
+ dependencies:
+ ember-cli-babel "^6.3.0"
+ ember-cli-htmlbars "^2.0.1"
+ ember-runtime-enumerable-includes-polyfill "^2.0.0"
+
+ember-cli-get-component-path-option@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/ember-cli-get-component-path-option/-/ember-cli-get-component-path-option-1.0.0.tgz#0d7b595559e2f9050abed804f1d8eff1b08bc771"
+
+ember-cli-get-dependency-depth@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/ember-cli-get-dependency-depth/-/ember-cli-get-dependency-depth-1.0.0.tgz#e0afecf82a2d52f00f28ab468295281aec368d11"
+
+ember-cli-htmlbars-inline-precompile@^0.4.3:
+ version "0.4.3"
+ resolved "https://registry.yarnpkg.com/ember-cli-htmlbars-inline-precompile/-/ember-cli-htmlbars-inline-precompile-0.4.3.tgz#4123f507fea6c59ba4c272ef7e713a6d55ba06c9"
+ dependencies:
+ babel-plugin-htmlbars-inline-precompile "^0.2.3"
+ ember-cli-version-checker "^2.0.0"
+ hash-for-dep "^1.0.2"
+ silent-error "^1.1.0"
+
+ember-cli-htmlbars@^1.0.10, ember-cli-htmlbars@^1.1.1:
+ version "1.3.4"
+ resolved "https://registry.yarnpkg.com/ember-cli-htmlbars/-/ember-cli-htmlbars-1.3.4.tgz#461289724b34af372a6a0c4b6635819156963353"
+ dependencies:
+ broccoli-persistent-filter "^1.0.3"
+ ember-cli-version-checker "^1.0.2"
+ hash-for-dep "^1.0.2"
+ json-stable-stringify "^1.0.0"
+ strip-bom "^2.0.0"
+
+ember-cli-htmlbars@^2.0.1:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/ember-cli-htmlbars/-/ember-cli-htmlbars-2.0.2.tgz#230a9ace7c3454b3acff2768a50f963813a90c38"
+ dependencies:
+ broccoli-persistent-filter "^1.0.3"
+ hash-for-dep "^1.0.2"
+ json-stable-stringify "^1.0.0"
+ strip-bom "^3.0.0"
+
+ember-cli-htmlbars@^2.0.2, ember-cli-htmlbars@^2.0.3:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/ember-cli-htmlbars/-/ember-cli-htmlbars-2.0.3.tgz#e116e1500dba12f29c94b05b9ec90f52cb8bb042"
+ dependencies:
+ broccoli-persistent-filter "^1.0.3"
+ hash-for-dep "^1.0.2"
+ json-stable-stringify "^1.0.0"
+ strip-bom "^3.0.0"
+
+ember-cli-inject-live-reload@^1.4.1:
+ version "1.7.0"
+ resolved "https://registry.yarnpkg.com/ember-cli-inject-live-reload/-/ember-cli-inject-live-reload-1.7.0.tgz#af94336e015336127dfb98080ad442bb233e37ed"
+
+ember-cli-is-package-missing@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/ember-cli-is-package-missing/-/ember-cli-is-package-missing-1.0.0.tgz#6e6184cafb92635dd93ca6c946b104292d4e3390"
+
+ember-cli-legacy-blueprints@^0.1.2:
+ version "0.1.5"
+ resolved "https://registry.yarnpkg.com/ember-cli-legacy-blueprints/-/ember-cli-legacy-blueprints-0.1.5.tgz#93c15ca242ec5107d62a8af7ec30f6ac538f3ad9"
+ dependencies:
+ chalk "^1.1.1"
+ ember-cli-get-component-path-option "^1.0.0"
+ ember-cli-get-dependency-depth "^1.0.0"
+ ember-cli-is-package-missing "^1.0.0"
+ ember-cli-lodash-subset "^1.0.7"
+ ember-cli-normalize-entity-name "^1.0.0"
+ ember-cli-path-utils "^1.0.0"
+ ember-cli-string-utils "^1.0.0"
+ ember-cli-test-info "^1.0.0"
+ ember-cli-valid-component-name "^1.0.0"
+ ember-cli-version-checker "^1.1.7"
+ ember-router-generator "^1.0.0"
+ exists-sync "0.0.3"
+ fs-extra "^0.24.0"
+ inflection "^1.7.1"
+ rsvp "^3.0.17"
+ silent-error "^1.0.0"
+
+ember-cli-lodash-subset@^1.0.11, ember-cli-lodash-subset@^1.0.7:
+ version "1.0.12"
+ resolved "https://registry.yarnpkg.com/ember-cli-lodash-subset/-/ember-cli-lodash-subset-1.0.12.tgz#af2e77eba5dcb0d77f3308d3a6fd7d3450f6e537"
+
+ember-cli-mirage@^0.4.1:
+ version "0.4.1"
+ resolved "https://registry.yarnpkg.com/ember-cli-mirage/-/ember-cli-mirage-0.4.1.tgz#bfdfe61e5e74dc3881ed31f12112dae1a29f0d4c"
+ dependencies:
+ broccoli-funnel "^1.0.2"
+ broccoli-merge-trees "^1.1.0"
+ broccoli-stew "^1.5.0"
+ chalk "^1.1.1"
+ ember-cli-babel "^6.8.2"
+ ember-cli-node-assets "^0.1.4"
+ ember-get-config "^0.2.2"
+ ember-inflector "^2.0.0"
+ ember-lodash "^4.17.3"
+ exists-sync "0.0.3"
+ fake-xml-http-request "^1.4.0"
+ faker "^3.0.0"
+ pretender "^1.6.1"
+ route-recognizer "^0.2.3"
+
+ember-cli-moment-shim@2.2.1:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/ember-cli-moment-shim/-/ember-cli-moment-shim-2.2.1.tgz#78870872a626177d0b04223c9eb6be0729590e61"
+ dependencies:
+ broccoli-funnel "^1.0.0"
+ broccoli-merge-trees "^1.0.0"
+ broccoli-stew "^1.0.0"
+ chalk "^1.1.1"
+ ember-cli-babel "^5.0.0"
+ exists-sync "0.0.3"
+ lodash.defaults "^4.1.0"
+ moment "^2.13.0"
+ moment-timezone "^0.5.0"
+
+ember-cli-node-assets@^0.1.4:
+ version "0.1.6"
+ resolved "https://registry.yarnpkg.com/ember-cli-node-assets/-/ember-cli-node-assets-0.1.6.tgz#6488a2949048c801ad6d9e33753c7bce32fc1146"
+ dependencies:
+ broccoli-funnel "^1.0.1"
+ broccoli-merge-trees "^1.1.1"
+ broccoli-unwatched-tree "^0.1.1"
+ debug "^2.2.0"
+ lodash "^4.5.1"
+ resolve "^1.1.7"
+
+ember-cli-node-assets@^0.2.2:
+ version "0.2.2"
+ resolved "https://registry.yarnpkg.com/ember-cli-node-assets/-/ember-cli-node-assets-0.2.2.tgz#d2d55626e7cc6619f882d7fe55751f9266022708"
+ dependencies:
+ broccoli-funnel "^1.0.1"
+ broccoli-merge-trees "^1.1.1"
+ broccoli-source "^1.1.0"
+ debug "^2.2.0"
+ lodash "^4.5.1"
+ resolve "^1.1.7"
+
+ember-cli-normalize-entity-name@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/ember-cli-normalize-entity-name/-/ember-cli-normalize-entity-name-1.0.0.tgz#0b14f7bcbc599aa117b5fddc81e4fd03c4bad5b7"
+ dependencies:
+ silent-error "^1.0.0"
+
+ember-cli-page-object@^1.13.0:
+ version "1.13.0"
+ resolved "https://registry.yarnpkg.com/ember-cli-page-object/-/ember-cli-page-object-1.13.0.tgz#9ac9342d9f90a363c429fbb14f3ad5c0be11827a"
+ dependencies:
+ ceibo "~2.0.0"
+ ember-cli-babel "^6.6.0"
+ ember-cli-node-assets "^0.2.2"
+ ember-native-dom-helpers "^0.5.3"
+ ember-test-helpers "^0.6.3"
+ jquery "^3.2.1"
+ rsvp "^4.7.0"
+
+ember-cli-path-utils@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/ember-cli-path-utils/-/ember-cli-path-utils-1.0.0.tgz#4e39af8b55301cddc5017739b77a804fba2071ed"
+
+ember-cli-preprocess-registry@^3.1.0:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/ember-cli-preprocess-registry/-/ember-cli-preprocess-registry-3.1.1.tgz#38456c21c4d2b64945850cf9ec68db6ba769288a"
+ dependencies:
+ broccoli-clean-css "^1.1.0"
+ broccoli-funnel "^1.0.0"
+ broccoli-merge-trees "^1.0.0"
+ debug "^2.2.0"
+ ember-cli-lodash-subset "^1.0.7"
+ exists-sync "0.0.3"
+ process-relative-require "^1.0.0"
+ silent-error "^1.0.0"
+
+ember-cli-pretender@0.7.0:
+ version "0.7.0"
+ resolved "https://registry.yarnpkg.com/ember-cli-pretender/-/ember-cli-pretender-0.7.0.tgz#ef56225cdd773db6dd1369912df2657d7a74b752"
+ dependencies:
+ broccoli-funnel "^1.0.6"
+ broccoli-merge-trees "^1.1.4"
+ pretender "^1.1.0"
+ resolve "^1.1.7"
+
+ember-cli-qunit@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/ember-cli-qunit/-/ember-cli-qunit-4.0.0.tgz#1f0022469a5bd64f627b8102880a25e94e533a3b"
+ dependencies:
+ broccoli-funnel "^1.0.1"
+ broccoli-merge-trees "^2.0.0"
+ ember-cli-babel "^6.0.0-beta.7"
+ ember-cli-test-loader "^2.0.0"
+ ember-cli-version-checker "^1.1.4"
+ ember-qunit "^2.1.3"
+ qunit-notifications "^0.1.1"
+ qunitjs "^2.0.1"
+ resolve "^1.1.6"
+ silent-error "^1.0.0"
+
+ember-cli-sass@6.0.0:
+ version "6.0.0"
+ resolved "https://registry.yarnpkg.com/ember-cli-sass/-/ember-cli-sass-6.0.0.tgz#31c9c8fa789c0d25aaf8e315431b7a3ec4ba0175"
+ dependencies:
+ broccoli-funnel "^1.0.0"
+ broccoli-merge-trees "^1.1.0"
+ broccoli-sass-source-maps "^2.0.0"
+ ember-cli-babel "5.1.10"
+ ember-cli-version-checker "^1.0.2"
+ merge "^1.2.0"
+
+ember-cli-shims@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/ember-cli-shims/-/ember-cli-shims-1.1.0.tgz#0e3b8a048be865b4f81cc81d397ff1eeb13f75b6"
+ dependencies:
+ ember-cli-babel "^6.0.0-beta.7"
+ ember-cli-version-checker "^1.2.0"
+ silent-error "^1.0.1"
+
+ember-cli-sri@meirish/ember-cli-sri#rooturl:
+ version "2.1.0"
+ resolved "https://codeload.github.com/meirish/ember-cli-sri/tar.gz/1c0ff776a61f09121d1ea69ce16e4653da5e1efa"
+ dependencies:
+ broccoli-sri-hash "^2.1.0"
+
+ember-cli-string-helpers@^1.5.0:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/ember-cli-string-helpers/-/ember-cli-string-helpers-1.5.0.tgz#b7c1b27ffd4bb4bf4846b3167f730f0125a96f44"
+ dependencies:
+ broccoli-funnel "^1.0.1"
+ ember-cli-babel "^6.3.0"
+
+ember-cli-string-utils@^1.0.0, ember-cli-string-utils@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/ember-cli-string-utils/-/ember-cli-string-utils-1.1.0.tgz#39b677fc2805f55173735376fcef278eaa4452a1"
+
+ember-cli-test-info@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/ember-cli-test-info/-/ember-cli-test-info-1.0.0.tgz#ed4e960f249e97523cf891e4aed2072ce84577b4"
+ dependencies:
+ ember-cli-string-utils "^1.0.0"
+
+ember-cli-test-loader@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/ember-cli-test-loader/-/ember-cli-test-loader-2.1.0.tgz#16163bae0ac32cad1af13c4ed94c6c698b54d431"
+ dependencies:
+ ember-cli-babel "^6.0.0-beta.7"
+
+ember-cli-uglify@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/ember-cli-uglify/-/ember-cli-uglify-1.2.0.tgz#3208c32b54bc2783056e8bb0d5cfe9bbaf17ffb2"
+ dependencies:
+ broccoli-uglify-sourcemap "^1.0.0"
+
+ember-cli-valid-component-name@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/ember-cli-valid-component-name/-/ember-cli-valid-component-name-1.0.0.tgz#71550ce387e0233065f30b30b1510aa2dfbe87ef"
+ dependencies:
+ silent-error "^1.0.0"
+
+ember-cli-version-checker@1.3.1, ember-cli-version-checker@^1.0.2, ember-cli-version-checker@^1.1.4, ember-cli-version-checker@^1.1.6, ember-cli-version-checker@^1.1.7, ember-cli-version-checker@^1.2.0, ember-cli-version-checker@^1.3.1:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/ember-cli-version-checker/-/ember-cli-version-checker-1.3.1.tgz#0bc2d134c830142da64bf9627a0eded10b61ae72"
+ dependencies:
+ semver "^5.3.0"
+
+ember-cli-version-checker@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/ember-cli-version-checker/-/ember-cli-version-checker-2.0.0.tgz#e1f7d8e4cdcd752ac35f1611e4daa8836db4c4c7"
+ dependencies:
+ resolve "^1.3.3"
+ semver "^5.3.0"
+
+ember-cli-version-checker@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/ember-cli-version-checker/-/ember-cli-version-checker-2.1.0.tgz#fc79a56032f3717cf844ada7cbdec1a06fedb604"
+ dependencies:
+ resolve "^1.3.3"
+ semver "^5.3.0"
+
+ember-cli@~2.14.0:
+ version "2.14.0"
+ resolved "https://registry.yarnpkg.com/ember-cli/-/ember-cli-2.14.0.tgz#9aff1414168883183e8677fa32626d1e3228ccbc"
+ dependencies:
+ amd-name-resolver "0.0.6"
+ babel-plugin-transform-es2015-modules-amd "^6.24.0"
+ bower-config "^1.3.0"
+ bower-endpoint-parser "0.2.2"
+ broccoli-babel-transpiler "^6.0.0"
+ broccoli-brocfile-loader "^0.18.0"
+ broccoli-builder "^0.18.3"
+ broccoli-concat "^3.2.2"
+ broccoli-config-loader "^1.0.0"
+ broccoli-config-replace "^1.1.2"
+ broccoli-funnel "^1.0.6"
+ broccoli-funnel-reducer "^1.0.0"
+ broccoli-merge-trees "^2.0.0"
+ broccoli-middleware "^1.0.0-beta.8"
+ broccoli-source "^1.1.0"
+ broccoli-stew "^1.2.0"
+ calculate-cache-key-for-tree "^1.0.0"
+ capture-exit "^1.1.0"
+ chalk "^1.1.3"
+ clean-base-url "^1.0.0"
+ compression "^1.4.4"
+ configstore "^3.0.0"
+ console-ui "^1.0.2"
+ core-object "^3.1.3"
+ dag-map "^2.0.2"
+ diff "^3.2.0"
+ ember-cli-broccoli-sane-watcher "^2.0.4"
+ ember-cli-get-component-path-option "^1.0.0"
+ ember-cli-is-package-missing "^1.0.0"
+ ember-cli-legacy-blueprints "^0.1.2"
+ ember-cli-lodash-subset "^1.0.11"
+ ember-cli-normalize-entity-name "^1.0.0"
+ ember-cli-preprocess-registry "^3.1.0"
+ ember-cli-string-utils "^1.0.0"
+ ember-try "^0.2.15"
+ ensure-posix-path "^1.0.2"
+ escape-string-regexp "^1.0.3"
+ execa "^0.6.0"
+ exists-sync "0.0.4"
+ exit "^0.1.2"
+ express "^4.12.3"
+ filesize "^3.1.3"
+ find-up "^2.1.0"
+ fs-extra "^3.0.0"
+ fs-tree-diff "^0.5.2"
+ get-caller-file "^1.0.0"
+ git-repo-info "^1.4.1"
+ glob "7.1.1"
+ heimdalljs "^0.2.3"
+ heimdalljs-fs-monitor "^0.1.0"
+ heimdalljs-graph "^0.3.1"
+ heimdalljs-logger "^0.1.7"
+ http-proxy "^1.9.0"
+ inflection "^1.7.0"
+ is-git-url "^0.2.0"
+ isbinaryfile "^3.0.0"
+ js-yaml "^3.6.1"
+ json-stable-stringify "^1.0.1"
+ leek "0.0.24"
+ lodash.template "^4.2.5"
+ markdown-it "^8.3.0"
+ markdown-it-terminal "0.1.0"
+ minimatch "^3.0.0"
+ morgan "^1.8.1"
+ node-modules-path "^1.0.0"
+ nopt "^3.0.6"
+ npm-package-arg "^4.1.1"
+ portfinder "^1.0.7"
+ promise-map-series "^0.2.1"
+ quick-temp "^0.1.8"
+ resolve "^1.3.0"
+ rsvp "^3.3.3"
+ sane "^1.6.0"
+ semver "^5.1.1"
+ silent-error "^1.0.0"
+ sort-package-json "^1.4.0"
+ symlink-or-copy "^1.1.8"
+ temp "0.8.3"
+ testem "^1.15.0"
+ tiny-lr "^1.0.3"
+ tree-sync "^1.2.1"
+ uuid "^3.0.0"
+ validate-npm-package-name "^3.0.0"
+ walk-sync "^0.3.0"
+ yam "0.0.22"
+
+ember-composable-helpers@^2.0.3:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/ember-composable-helpers/-/ember-composable-helpers-2.0.3.tgz#9b5e595bf5a45bc4431adfe27821f23b1d534be0"
+ dependencies:
+ broccoli-funnel "^1.0.1"
+ ember-cli-babel "^6.1.0"
+
+ember-computed-query@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/ember-computed-query/-/ember-computed-query-0.1.1.tgz#2e6debe36043c1271b5973ab19bf2f1931439ea0"
+ dependencies:
+ ember-cli-babel "^6.3.0"
+
+ember-concurrency@^0.8.14:
+ version "0.8.14"
+ resolved "https://registry.yarnpkg.com/ember-concurrency/-/ember-concurrency-0.8.14.tgz#4017133e5fbb9d088082ef6ab5b91839ed33107b"
+ dependencies:
+ babel-core "^6.24.1"
+ ember-cli-babel "^6.8.2"
+ ember-maybe-import-regenerator "^0.1.5"
+
+ember-data-model-fragments@2.11.x:
+ version "2.11.5"
+ resolved "https://registry.yarnpkg.com/ember-data-model-fragments/-/ember-data-model-fragments-2.11.5.tgz#756809c0931eab78b90aaf4973ff8010a9c251b2"
+ dependencies:
+ broccoli-file-creator "^1.1.1"
+ broccoli-merge-trees "^2.0.0"
+ ember-cli-babel "^6.0.0"
+ exists-sync "^0.0.4"
+ git-repo-info "^1.4.1"
+ npm-git-info "^1.0.3"
+
+ember-data@2.12.1:
+ version "2.12.1"
+ resolved "https://registry.yarnpkg.com/ember-data/-/ember-data-2.12.1.tgz#c06d47b14ff4956e6579b04960f62060b8ce7a70"
+ dependencies:
+ amd-name-resolver "0.0.5"
+ babel-plugin-feature-flags "^0.2.1"
+ babel-plugin-filter-imports "^0.2.0"
+ babel5-plugin-strip-class-callcheck "^5.1.0"
+ babel5-plugin-strip-heimdall "^5.0.2"
+ broccoli-babel-transpiler "^5.5.0"
+ broccoli-file-creator "^1.0.0"
+ broccoli-merge-trees "^1.0.0"
+ chalk "^1.1.1"
+ ember-cli-babel "^5.1.6"
+ ember-cli-path-utils "^1.0.0"
+ ember-cli-string-utils "^1.0.0"
+ ember-cli-test-info "^1.0.0"
+ ember-cli-version-checker "^1.1.4"
+ ember-inflector "^1.9.4"
+ ember-runtime-enumerable-includes-polyfill "^1.0.0"
+ exists-sync "0.0.3"
+ git-repo-info "^1.1.2"
+ heimdalljs "^0.3.0"
+ inflection "^1.8.0"
+ npm-git-info "^1.0.0"
+ semver "^5.1.0"
+ silent-error "^1.0.0"
+
+ember-export-application-global@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/ember-export-application-global/-/ember-export-application-global-2.0.0.tgz#8d6d7619ac8a1a3f8c43003549eb21ebed685bd2"
+ dependencies:
+ ember-cli-babel "^6.0.0-beta.7"
+
+ember-fetch@^3.4.3:
+ version "3.4.3"
+ resolved "https://registry.yarnpkg.com/ember-fetch/-/ember-fetch-3.4.3.tgz#fb8ba73148bb2399a82b037e4fdf9a953cd496ba"
+ dependencies:
+ broccoli-funnel "^1.2.0"
+ broccoli-stew "^1.4.2"
+ broccoli-templater "^1.0.0"
+ ember-cli-babel "^6.8.1"
+ node-fetch "^2.0.0-alpha.9"
+ whatwg-fetch "^2.0.3"
+
+ember-get-config@^0.2.2:
+ version "0.2.4"
+ resolved "https://registry.yarnpkg.com/ember-get-config/-/ember-get-config-0.2.4.tgz#118492a2a03d73e46004ed777928942021fe1ecd"
+ dependencies:
+ broccoli-file-creator "^1.1.1"
+ ember-cli-babel "^6.3.0"
+
+ember-href-to@^1.13.0:
+ version "1.13.0"
+ resolved "https://registry.yarnpkg.com/ember-href-to/-/ember-href-to-1.13.0.tgz#308ab4803d9d08e30a92af888cc67412a800468d"
+ dependencies:
+ ember-cli-babel "^5.1.6"
+
+ember-inflector@^1.9.4:
+ version "1.12.1"
+ resolved "https://registry.yarnpkg.com/ember-inflector/-/ember-inflector-1.12.1.tgz#d8bd2ca2f327b439720f89923fe614d46b5da1ca"
+ dependencies:
+ ember-cli-babel "^5.1.7"
+
+ember-inflector@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/ember-inflector/-/ember-inflector-2.1.0.tgz#afcb92d022a4eab58f08ff4578eafc3a1de2d09b"
+ dependencies:
+ ember-cli-babel "^6.0.0"
+
+ember-load-initializers@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/ember-load-initializers/-/ember-load-initializers-1.0.0.tgz#4919eaf06f6dfeca7e134633d8c05a6c9921e6e7"
+ dependencies:
+ ember-cli-babel "^6.0.0-beta.7"
+
+ember-lodash@^4.17.3:
+ version "4.18.0"
+ resolved "https://registry.yarnpkg.com/ember-lodash/-/ember-lodash-4.18.0.tgz#45de700d6a4f68f1cd62888d90b50aa6477b9a83"
+ dependencies:
+ broccoli-debug "^0.6.1"
+ broccoli-funnel "^2.0.1"
+ broccoli-merge-trees "^2.0.0"
+ broccoli-string-replace "^0.1.1"
+ ember-cli-babel "^6.10.0"
+ lodash-es "^4.17.4"
+
+ember-maybe-import-regenerator@^0.1.5:
+ version "0.1.6"
+ resolved "https://registry.yarnpkg.com/ember-maybe-import-regenerator/-/ember-maybe-import-regenerator-0.1.6.tgz#35d41828afa6d6a59bc0da3ce47f34c573d776ca"
+ dependencies:
+ broccoli-funnel "^1.0.1"
+ broccoli-merge-trees "^1.0.0"
+ ember-cli-babel "^6.0.0-beta.4"
+ regenerator-runtime "^0.9.5"
+
+ember-moment@7.0.0-beta.5:
+ version "7.0.0-beta.5"
+ resolved "https://registry.yarnpkg.com/ember-moment/-/ember-moment-7.0.0-beta.5.tgz#b62c144d32f6ad0acaadd588ba93f4ddeb72ba89"
+ dependencies:
+ ember-cli-babel "^5.1.6"
+
+ember-native-dom-helpers@^0.5.3:
+ version "0.5.4"
+ resolved "https://registry.yarnpkg.com/ember-native-dom-helpers/-/ember-native-dom-helpers-0.5.4.tgz#0bc1506a643fb7adc0abf1d09c44a7914459296b"
+ dependencies:
+ broccoli-funnel "^1.1.0"
+ ember-cli-babel "^6.6.0"
+
+ember-qunit-assert-helpers@^0.1.3:
+ version "0.1.3"
+ resolved "https://registry.yarnpkg.com/ember-qunit-assert-helpers/-/ember-qunit-assert-helpers-0.1.3.tgz#6ba2acf63a3c45c6f6764bc1b5cffd42942df678"
+ dependencies:
+ broccoli-filter "^1.0.1"
+ ember-cli-babel "^5.1.7"
+
+ember-qunit@^2.1.3:
+ version "2.1.4"
+ resolved "https://registry.yarnpkg.com/ember-qunit/-/ember-qunit-2.1.4.tgz#5732794e668f753d8fe1a353692ffeda73742d29"
+ dependencies:
+ ember-test-helpers "^0.6.3"
+
+ember-radio-button@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/ember-radio-button/-/ember-radio-button-1.1.1.tgz#e5ae8361ff032a4f1be91a810295e196eb2acf97"
+ dependencies:
+ ember-cli-babel "^5.1.7"
+ ember-cli-htmlbars "^1.0.10"
+
+ember-resolver@^4.0.0:
+ version "4.3.0"
+ resolved "https://registry.yarnpkg.com/ember-resolver/-/ember-resolver-4.3.0.tgz#aaf0e43646be2e7da14399a0c2e9574c2130ce69"
+ dependencies:
+ "@glimmer/resolver" "^0.4.1"
+ babel-plugin-debug-macros "^0.1.10"
+ broccoli-funnel "^1.1.0"
+ broccoli-merge-trees "^2.0.0"
+ ember-cli-babel "^6.3.0"
+ ember-cli-version-checker "1.3.1"
+ resolve "^1.3.3"
+
+ember-rfc176-data@^0.2.0:
+ version "0.2.5"
+ resolved "https://registry.yarnpkg.com/ember-rfc176-data/-/ember-rfc176-data-0.2.5.tgz#b26f62d9c03d3b02485153cf31137e089299839a"
+
+ember-rfc176-data@^0.2.7:
+ version "0.2.7"
+ resolved "https://registry.yarnpkg.com/ember-rfc176-data/-/ember-rfc176-data-0.2.7.tgz#bd355bc9b473e08096b518784170a23388bc973b"
+
+ember-rfc176-data@^0.3.0:
+ version "0.3.1"
+ resolved "https://registry.yarnpkg.com/ember-rfc176-data/-/ember-rfc176-data-0.3.1.tgz#6a5a4b8b82ec3af34f3010965fa96b936ca94519"
+
+ember-router-generator@^1.0.0:
+ version "1.2.3"
+ resolved "https://registry.yarnpkg.com/ember-router-generator/-/ember-router-generator-1.2.3.tgz#8ed2ca86ff323363120fc14278191e9e8f1315ee"
+ dependencies:
+ recast "^0.11.3"
+
+ember-runtime-enumerable-includes-polyfill@^1.0.0:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/ember-runtime-enumerable-includes-polyfill/-/ember-runtime-enumerable-includes-polyfill-1.0.4.tgz#16a7612e347a2edf07da8b2f2f09dbfee70deba0"
+ dependencies:
+ ember-cli-babel "^5.1.6"
+ ember-cli-version-checker "^1.1.6"
+
+ember-runtime-enumerable-includes-polyfill@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/ember-runtime-enumerable-includes-polyfill/-/ember-runtime-enumerable-includes-polyfill-2.0.0.tgz#6e9ba118bc909d1d7762de1b03a550d8955308a9"
+ dependencies:
+ ember-cli-babel "^6.0.0"
+ ember-cli-version-checker "^1.1.6"
+
+ember-sinon@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/ember-sinon/-/ember-sinon-1.0.1.tgz#056390eacc9367b4c3955ce1cb5a04246f8197f5"
+ dependencies:
+ broccoli-funnel "^2.0.0"
+ broccoli-merge-trees "^2.0.0"
+ ember-cli-babel "^6.3.0"
+ sinon "^3.2.1"
+
+ember-source@~2.14.0:
+ version "2.14.1"
+ resolved "https://registry.yarnpkg.com/ember-source/-/ember-source-2.14.1.tgz#4abf0b4c916f2da8bf317349df4750905df7e628"
+ dependencies:
+ "@glimmer/compiler" "^0.22.3"
+ "@glimmer/node" "^0.22.3"
+ "@glimmer/reference" "^0.22.3"
+ "@glimmer/runtime" "^0.22.3"
+ "@glimmer/util" "^0.22.3"
+ broccoli-funnel "^1.2.0"
+ broccoli-merge-trees "^2.0.0"
+ ember-cli-get-component-path-option "^1.0.0"
+ ember-cli-normalize-entity-name "^1.0.0"
+ ember-cli-path-utils "^1.0.0"
+ ember-cli-string-utils "^1.1.0"
+ ember-cli-test-info "^1.0.0"
+ ember-cli-valid-component-name "^1.0.0"
+ ember-cli-version-checker "^1.3.1"
+ handlebars "^4.0.6"
+ jquery "^3.2.1"
+ resolve "^1.3.3"
+ rsvp "^3.5.0"
+ simple-dom "^0.3.0"
+ simple-html-tokenizer "^0.4.1"
+
+ember-test-helpers@^0.6.3:
+ version "0.6.3"
+ resolved "https://registry.yarnpkg.com/ember-test-helpers/-/ember-test-helpers-0.6.3.tgz#f864cdf6f4e75f3f8768d6537785b5ab6e82d907"
+
+ember-test-selectors@^0.3.6:
+ version "0.3.6"
+ resolved "https://registry.yarnpkg.com/ember-test-selectors/-/ember-test-selectors-0.3.6.tgz#38fa62f3f82381793047fda98a37093ec891a211"
+ dependencies:
+ broccoli-stew "^1.4.0"
+ ember-cli-babel "^6.6.0"
+ ember-cli-version-checker "^2.0.0"
+
+ember-truth-helpers@1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/ember-truth-helpers/-/ember-truth-helpers-1.2.0.tgz#e63cffeaa8211882ae61a958816fded3790d065b"
+ dependencies:
+ ember-cli-babel "^5.1.5"
+
+ember-try-config@^2.0.1:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/ember-try-config/-/ember-try-config-2.1.0.tgz#e0e156229a542346a58ee6f6ad605104c98edfe0"
+ dependencies:
+ lodash "^4.6.1"
+ node-fetch "^1.3.3"
+ rsvp "^3.2.1"
+ semver "^5.1.0"
+
+ember-try@^0.2.15:
+ version "0.2.16"
+ resolved "https://registry.yarnpkg.com/ember-try/-/ember-try-0.2.16.tgz#cf7092d8a8fea9701d7faa73cbdbff37a8ada330"
+ dependencies:
+ chalk "^1.0.0"
+ cli-table2 "^0.2.0"
+ core-object "^1.1.0"
+ debug "^2.2.0"
+ ember-cli-version-checker "^1.1.6"
+ ember-try-config "^2.0.1"
+ extend "^3.0.0"
+ fs-extra "^0.26.0"
+ promise-map-series "^0.2.1"
+ resolve "^1.1.6"
+ rimraf "^2.3.2"
+ rsvp "^3.0.17"
+ semver "^5.1.0"
+
+ember-wormhole@^0.5.2:
+ version "0.5.2"
+ resolved "https://registry.yarnpkg.com/ember-wormhole/-/ember-wormhole-0.5.2.tgz#cc0ceb7db4f8b8da0fd852edc81d75cb1dcd92f1"
+ dependencies:
+ ember-cli-babel "^6.0.0"
+ ember-cli-htmlbars "^1.1.1"
+
+encodeurl@~1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.1.tgz#79e3d58655346909fe6f0f45a5de68103b294d20"
+
+encoding@^0.1.11:
+ version "0.1.12"
+ resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.12.tgz#538b66f3ee62cd1ab51ec323829d1f9480c74beb"
+ dependencies:
+ iconv-lite "~0.4.13"
+
+engine.io-client@1.8.0:
+ version "1.8.0"
+ resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-1.8.0.tgz#7b730e4127414087596d9be3c88d2bc5fdb6cf5c"
+ dependencies:
+ component-emitter "1.2.1"
+ component-inherit "0.0.3"
+ debug "2.3.3"
+ engine.io-parser "1.3.1"
+ has-cors "1.1.0"
+ indexof "0.0.1"
+ parsejson "0.0.3"
+ parseqs "0.0.5"
+ parseuri "0.0.5"
+ ws "1.1.1"
+ xmlhttprequest-ssl "1.5.3"
+ yeast "0.1.2"
+
+engine.io-parser@1.3.1:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-1.3.1.tgz#9554f1ae33107d6fbd170ca5466d2f833f6a07cf"
+ dependencies:
+ after "0.8.1"
+ arraybuffer.slice "0.0.6"
+ base64-arraybuffer "0.1.5"
+ blob "0.0.4"
+ has-binary "0.1.6"
+ wtf-8 "1.0.0"
+
+engine.io@1.8.0:
+ version "1.8.0"
+ resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-1.8.0.tgz#3eeb5f264cb75dbbec1baaea26d61f5a4eace2aa"
+ dependencies:
+ accepts "1.3.3"
+ base64id "0.1.0"
+ cookie "0.3.1"
+ debug "2.3.3"
+ engine.io-parser "1.3.1"
+ ws "1.1.1"
+
+ensure-posix-path@^1.0.0, ensure-posix-path@^1.0.1, ensure-posix-path@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/ensure-posix-path/-/ensure-posix-path-1.0.2.tgz#a65b3e42d0b71cfc585eb774f9943c8d9b91b0c2"
+
+entities@1.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/entities/-/entities-1.0.0.tgz#b2987aa3821347fcde642b24fdfc9e4fb712bf26"
+
+entities@~1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.1.tgz#6e5c2d0a5621b5dadaecef80b90edfb5cd7772f0"
+
+error-ex@^1.2.0:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.1.tgz#f855a86ce61adc4e8621c3cda21e7a7612c3a8dc"
+ dependencies:
+ is-arrayish "^0.2.1"
+
+error@^7.0.0:
+ version "7.0.2"
+ resolved "https://registry.yarnpkg.com/error/-/error-7.0.2.tgz#a5f75fff4d9926126ddac0ea5dc38e689153cb02"
+ dependencies:
+ string-template "~0.2.1"
+ xtend "~4.0.0"
+
+es5-ext@^0.10.14, es5-ext@^0.10.9, es5-ext@~0.10.14:
+ version "0.10.30"
+ resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.30.tgz#7141a16836697dbabfaaaeee41495ce29f52c939"
+ dependencies:
+ es6-iterator "2"
+ es6-symbol "~3.1"
+
+es6-iterator@2, es6-iterator@^2.0.1, es6-iterator@~2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.1.tgz#8e319c9f0453bf575d374940a655920e59ca5512"
+ dependencies:
+ d "1"
+ es5-ext "^0.10.14"
+ es6-symbol "^3.1"
+
+es6-map@^0.1.3:
+ version "0.1.5"
+ resolved "https://registry.yarnpkg.com/es6-map/-/es6-map-0.1.5.tgz#9136e0503dcc06a301690f0bb14ff4e364e949f0"
+ dependencies:
+ d "1"
+ es5-ext "~0.10.14"
+ es6-iterator "~2.0.1"
+ es6-set "~0.1.5"
+ es6-symbol "~3.1.1"
+ event-emitter "~0.3.5"
+
+es6-promise@^3.0.2:
+ version "3.3.1"
+ resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-3.3.1.tgz#a08cdde84ccdbf34d027a1451bc91d4bcd28a613"
+
+es6-promise@~4.0.3:
+ version "4.0.5"
+ resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.0.5.tgz#7882f30adde5b240ccfa7f7d78c548330951ae42"
+
+es6-set@~0.1.5:
+ version "0.1.5"
+ resolved "https://registry.yarnpkg.com/es6-set/-/es6-set-0.1.5.tgz#d2b3ec5d4d800ced818db538d28974db0a73ccb1"
+ dependencies:
+ d "1"
+ es5-ext "~0.10.14"
+ es6-iterator "~2.0.1"
+ es6-symbol "3.1.1"
+ event-emitter "~0.3.5"
+
+es6-symbol@3.1.1, es6-symbol@^3.1, es6-symbol@^3.1.1, es6-symbol@~3.1, es6-symbol@~3.1.1:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.1.tgz#bf00ef4fdab6ba1b46ecb7b629b4c7ed5715cc77"
+ dependencies:
+ d "1"
+ es5-ext "~0.10.14"
+
+es6-weak-map@^2.0.1:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/es6-weak-map/-/es6-weak-map-2.0.2.tgz#5e3ab32251ffd1538a1f8e5ffa1357772f92d96f"
+ dependencies:
+ d "1"
+ es5-ext "^0.10.14"
+ es6-iterator "^2.0.1"
+ es6-symbol "^3.1.1"
+
+escape-html@~1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
+
+escape-string-regexp@^1.0.0, escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.3, escape-string-regexp@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
+
+escope@^3.6.0:
+ version "3.6.0"
+ resolved "https://registry.yarnpkg.com/escope/-/escope-3.6.0.tgz#e01975e812781a163a6dadfdd80398dc64c889c3"
+ dependencies:
+ es6-map "^0.1.3"
+ es6-weak-map "^2.0.1"
+ esrecurse "^4.1.0"
+ estraverse "^4.1.1"
+
+eslint-scope@^3.7.1:
+ version "3.7.1"
+ resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-3.7.1.tgz#3d63c3edfda02e06e01a452ad88caacc7cdcb6e8"
+ dependencies:
+ esrecurse "^4.1.0"
+ estraverse "^4.1.1"
+
+eslint@^3.19.0:
+ version "3.19.0"
+ resolved "https://registry.yarnpkg.com/eslint/-/eslint-3.19.0.tgz#c8fc6201c7f40dd08941b87c085767386a679acc"
+ dependencies:
+ babel-code-frame "^6.16.0"
+ chalk "^1.1.3"
+ concat-stream "^1.5.2"
+ debug "^2.1.1"
+ doctrine "^2.0.0"
+ escope "^3.6.0"
+ espree "^3.4.0"
+ esquery "^1.0.0"
+ estraverse "^4.2.0"
+ esutils "^2.0.2"
+ file-entry-cache "^2.0.0"
+ glob "^7.0.3"
+ globals "^9.14.0"
+ ignore "^3.2.0"
+ imurmurhash "^0.1.4"
+ inquirer "^0.12.0"
+ is-my-json-valid "^2.10.0"
+ is-resolvable "^1.0.0"
+ js-yaml "^3.5.1"
+ json-stable-stringify "^1.0.0"
+ levn "^0.3.0"
+ lodash "^4.0.0"
+ mkdirp "^0.5.0"
+ natural-compare "^1.4.0"
+ optionator "^0.8.2"
+ path-is-inside "^1.0.1"
+ pluralize "^1.2.1"
+ progress "^1.1.8"
+ require-uncached "^1.0.2"
+ shelljs "^0.7.5"
+ strip-bom "^3.0.0"
+ strip-json-comments "~2.0.1"
+ table "^3.7.8"
+ text-table "~0.2.0"
+ user-home "^2.0.0"
+
+eslint@^4.0.0:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/eslint/-/eslint-4.2.0.tgz#a2b3184111b198e02e9c7f3cca625a5e01c56b3d"
+ dependencies:
+ ajv "^5.2.0"
+ babel-code-frame "^6.22.0"
+ chalk "^1.1.3"
+ concat-stream "^1.6.0"
+ debug "^2.6.8"
+ doctrine "^2.0.0"
+ eslint-scope "^3.7.1"
+ espree "^3.4.3"
+ esquery "^1.0.0"
+ estraverse "^4.2.0"
+ esutils "^2.0.2"
+ file-entry-cache "^2.0.0"
+ glob "^7.1.2"
+ globals "^9.17.0"
+ ignore "^3.3.3"
+ imurmurhash "^0.1.4"
+ inquirer "^3.0.6"
+ is-resolvable "^1.0.0"
+ js-yaml "^3.8.4"
+ json-stable-stringify "^1.0.1"
+ levn "^0.3.0"
+ lodash "^4.17.4"
+ minimatch "^3.0.2"
+ mkdirp "^0.5.1"
+ natural-compare "^1.4.0"
+ optionator "^0.8.2"
+ path-is-inside "^1.0.2"
+ pluralize "^4.0.0"
+ progress "^2.0.0"
+ require-uncached "^1.0.3"
+ strip-json-comments "~2.0.1"
+ table "^4.0.1"
+ text-table "~0.2.0"
+
+espree@^3.4.0, espree@^3.4.3:
+ version "3.4.3"
+ resolved "https://registry.yarnpkg.com/espree/-/espree-3.4.3.tgz#2910b5ccd49ce893c2ffffaab4fd8b3a31b82374"
+ dependencies:
+ acorn "^5.0.1"
+ acorn-jsx "^3.0.0"
+
+esprima-fb@~15001.1001.0-dev-harmony-fb:
+ version "15001.1001.0-dev-harmony-fb"
+ resolved "https://registry.yarnpkg.com/esprima-fb/-/esprima-fb-15001.1001.0-dev-harmony-fb.tgz#43beb57ec26e8cf237d3dd8b33e42533577f2659"
+
+esprima@^2.6.0:
+ version "2.7.3"
+ resolved "https://registry.yarnpkg.com/esprima/-/esprima-2.7.3.tgz#96e3b70d5779f6ad49cd032673d1c312767ba581"
+
+esprima@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.0.tgz#4499eddcd1110e0b218bacf2fa7f7f59f55ca804"
+
+esprima@~3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.0.0.tgz#53cf247acda77313e551c3aa2e73342d3fb4f7d9"
+
+esprima@~3.1.0:
+ version "3.1.3"
+ resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633"
+
+esquery@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.0.0.tgz#cfba8b57d7fba93f17298a8a006a04cda13d80fa"
+ dependencies:
+ estraverse "^4.0.0"
+
+esrecurse@^4.1.0:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.0.tgz#fa9568d98d3823f9a41d91e902dcab9ea6e5b163"
+ dependencies:
+ estraverse "^4.1.0"
+ object-assign "^4.0.1"
+
+estraverse@^4.0.0, estraverse@^4.1.0, estraverse@^4.1.1, estraverse@^4.2.0:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13"
+
+esutils@^2.0.0, esutils@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b"
+
+etag@~1.8.0:
+ version "1.8.0"
+ resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.0.tgz#6f631aef336d6c46362b51764044ce216be3c051"
+
+event-emitter@~0.3.5:
+ version "0.3.5"
+ resolved "https://registry.yarnpkg.com/event-emitter/-/event-emitter-0.3.5.tgz#df8c69eef1647923c7157b9ce83840610b02cc39"
+ dependencies:
+ d "1"
+ es5-ext "~0.10.14"
+
+eventemitter3@1.x.x:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-1.2.0.tgz#1c86991d816ad1e504750e73874224ecf3bec508"
+
+events-to-array@^1.0.1:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/events-to-array/-/events-to-array-1.1.2.tgz#2d41f563e1fe400ed4962fe1a4d5c6a7539df7f6"
+
+exec-sh@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.2.0.tgz#14f75de3f20d286ef933099b2ce50a90359cef10"
+ dependencies:
+ merge "^1.1.3"
+
+execa@^0.6.0:
+ version "0.6.3"
+ resolved "https://registry.yarnpkg.com/execa/-/execa-0.6.3.tgz#57b69a594f081759c69e5370f0d17b9cb11658fe"
+ 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"
+
+exif-parser@^0.1.9:
+ version "0.1.11"
+ resolved "https://registry.yarnpkg.com/exif-parser/-/exif-parser-0.1.11.tgz#8a97d1c9315ffd4754b6ae938ce4488d1b1a26b7"
+
+exists-stat@1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/exists-stat/-/exists-stat-1.0.0.tgz#0660e3525a2e89d9e446129440c272edfa24b529"
+
+exists-sync@0.0.3:
+ version "0.0.3"
+ resolved "https://registry.yarnpkg.com/exists-sync/-/exists-sync-0.0.3.tgz#b910000bedbb113b378b82f5f5a7638107622dcf"
+
+exists-sync@0.0.4, exists-sync@^0.0.4:
+ version "0.0.4"
+ resolved "https://registry.yarnpkg.com/exists-sync/-/exists-sync-0.0.4.tgz#9744c2c428cc03b01060db454d4b12f0ef3c8879"
+
+exit-hook@^1.0.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/exit-hook/-/exit-hook-1.1.1.tgz#f05ca233b48c05d54fff07765df8507e95c02ff8"
+
+exit@^0.1.2:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c"
+
+expand-brackets@^0.1.4:
+ version "0.1.5"
+ resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-0.1.5.tgz#df07284e342a807cd733ac5af72411e581d1177b"
+ dependencies:
+ is-posix-bracket "^0.1.0"
+
+expand-range@^1.8.1:
+ version "1.8.2"
+ resolved "https://registry.yarnpkg.com/expand-range/-/expand-range-1.8.2.tgz#a299effd335fe2721ebae8e257ec79644fc85337"
+ dependencies:
+ fill-range "^2.1.0"
+
+expand-tilde@^1.2.2:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/expand-tilde/-/expand-tilde-1.2.2.tgz#0b81eba897e5a3d31d1c3d102f8f01441e559449"
+ dependencies:
+ os-homedir "^1.0.1"
+
+express@^4.10.7, express@^4.12.3:
+ version "4.15.3"
+ resolved "https://registry.yarnpkg.com/express/-/express-4.15.3.tgz#bab65d0f03aa80c358408972fc700f916944b662"
+ dependencies:
+ accepts "~1.3.3"
+ array-flatten "1.1.1"
+ content-disposition "0.5.2"
+ content-type "~1.0.2"
+ cookie "0.3.1"
+ cookie-signature "1.0.6"
+ debug "2.6.7"
+ depd "~1.1.0"
+ encodeurl "~1.0.1"
+ escape-html "~1.0.3"
+ etag "~1.8.0"
+ finalhandler "~1.0.3"
+ fresh "0.5.0"
+ merge-descriptors "1.0.1"
+ methods "~1.1.2"
+ on-finished "~2.3.0"
+ parseurl "~1.3.1"
+ path-to-regexp "0.1.7"
+ proxy-addr "~1.1.4"
+ qs "6.4.0"
+ range-parser "~1.2.0"
+ send "0.15.3"
+ serve-static "1.12.3"
+ setprototypeof "1.0.3"
+ statuses "~1.3.1"
+ type-is "~1.6.15"
+ utils-merge "1.0.0"
+ vary "~1.1.1"
+
+extend@^3.0.0, extend@~3.0.0:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444"
+
+external-editor@^1.1.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-1.1.1.tgz#12d7b0db850f7ff7e7081baf4005700060c4600b"
+ dependencies:
+ extend "^3.0.0"
+ spawn-sync "^1.0.15"
+ tmp "^0.0.29"
+
+external-editor@^2.0.4:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-2.0.4.tgz#1ed9199da9cbfe2ef2f7a31b2fde8b0d12368972"
+ dependencies:
+ iconv-lite "^0.4.17"
+ jschardet "^1.4.2"
+ tmp "^0.0.31"
+
+extglob@^0.3.1:
+ version "0.3.2"
+ resolved "https://registry.yarnpkg.com/extglob/-/extglob-0.3.2.tgz#2e18ff3d2f49ab2765cec9023f011daa8d8349a1"
+ dependencies:
+ is-extglob "^1.0.0"
+
+extract-zip@~1.5.0:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.5.0.tgz#92ccf6d81ef70a9fa4c1747114ccef6d8688a6c4"
+ dependencies:
+ concat-stream "1.5.0"
+ debug "0.7.4"
+ mkdirp "0.5.0"
+ yauzl "2.4.1"
+
+extsprintf@1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.0.2.tgz#e1080e0658e300b06294990cc70e1502235fd550"
+
+eyes@0.1.x:
+ version "0.1.8"
+ resolved "https://registry.yarnpkg.com/eyes/-/eyes-0.1.8.tgz#62cf120234c683785d902348a800ef3e0cc20bc0"
+
+fake-xml-http-request@^1.4.0, fake-xml-http-request@^1.6.0:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/fake-xml-http-request/-/fake-xml-http-request-1.6.0.tgz#bd0ac79ae3e2660098282048a12c730a6f64d550"
+
+faker@^3.0.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/faker/-/faker-3.1.0.tgz#0f908faf4e6ec02524e54a57e432c5c013e08c9f"
+
+fast-deep-equal@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz#96256a3bc975595eb36d82e9929d060d893439ff"
+
+fast-levenshtein@~2.0.4:
+ version "2.0.6"
+ resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
+
+fast-ordered-set@^1.0.0, fast-ordered-set@^1.0.2:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/fast-ordered-set/-/fast-ordered-set-1.0.3.tgz#3fbb36634f7be79e4f7edbdb4a357dee25d184eb"
+ dependencies:
+ blank-object "^1.0.1"
+
+fast-sourcemap-concat@^1.0.1:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/fast-sourcemap-concat/-/fast-sourcemap-concat-1.1.0.tgz#a800767abed5eda02e67238ec063a709be61f9d4"
+ dependencies:
+ chalk "^0.5.1"
+ debug "^2.2.0"
+ fs-extra "^0.30.0"
+ memory-streams "^0.1.0"
+ mkdirp "^0.5.0"
+ rsvp "^3.0.14"
+ source-map "^0.4.2"
+ source-map-url "^0.3.0"
+
+fastboot-transform@0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/fastboot-transform/-/fastboot-transform-0.1.1.tgz#de55550d85644ec94cb11264c2ba883e3ea3b255"
+ dependencies:
+ broccoli-stew "^1.5.0"
+
+favicons@^4.7.1:
+ version "4.8.6"
+ resolved "https://registry.yarnpkg.com/favicons/-/favicons-4.8.6.tgz#a2b13800ab3fec2715bc8f27fa841d038d4761e2"
+ dependencies:
+ async "^1.5.0"
+ cheerio "^0.19.0"
+ clone "^1.0.2"
+ colors "^1.1.2"
+ harmony-reflect "^1.4.2"
+ image-size "^0.4.0"
+ jimp "^0.2.13"
+ jsontoxml "0.0.11"
+ merge-defaults "^0.2.1"
+ mkdirp "^0.5.1"
+ node-rest-client "^1.5.1"
+ require-directory "^2.1.1"
+ svg2png "~3.0.1"
+ through2 "^2.0.0"
+ tinycolor2 "^1.1.2"
+ to-ico "^1.1.2"
+ underscore "^1.8.3"
+ vinyl "^1.1.0"
+
+faye-websocket@~0.10.0:
+ version "0.10.0"
+ resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.10.0.tgz#4e492f8d04dfb6f89003507f6edbf2d501e7c6f4"
+ dependencies:
+ websocket-driver ">=0.5.1"
+
+fb-watchman@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.0.tgz#54e9abf7dfa2f26cd9b1636c588c1afc05de5d58"
+ dependencies:
+ bser "^2.0.0"
+
+fd-slicer@~1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.0.1.tgz#8b5bcbd9ec327c5041bf9ab023fd6750f1177e65"
+ dependencies:
+ pend "~1.2.0"
+
+figures@^1.3.5:
+ version "1.7.0"
+ resolved "https://registry.yarnpkg.com/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e"
+ dependencies:
+ escape-string-regexp "^1.0.5"
+ object-assign "^4.1.0"
+
+figures@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962"
+ dependencies:
+ escape-string-regexp "^1.0.5"
+
+file-entry-cache@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-2.0.0.tgz#c392990c3e684783d838b8c84a45d8a048458361"
+ dependencies:
+ flat-cache "^1.2.1"
+ object-assign "^4.0.1"
+
+file-type@^3.1.0, file-type@^3.8.0:
+ version "3.9.0"
+ resolved "https://registry.yarnpkg.com/file-type/-/file-type-3.9.0.tgz#257a078384d1db8087bc449d107d52a52672b9e9"
+
+filename-regex@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/filename-regex/-/filename-regex-2.0.1.tgz#c1c4b9bee3e09725ddb106b75c1e301fe2f18b26"
+
+filesize@^3.1.3:
+ version "3.5.10"
+ resolved "https://registry.yarnpkg.com/filesize/-/filesize-3.5.10.tgz#fc8fa23ddb4ef9e5e0ab6e1e64f679a24a56761f"
+
+fill-range@^2.1.0:
+ version "2.2.3"
+ resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-2.2.3.tgz#50b77dfd7e469bc7492470963699fe7a8485a723"
+ dependencies:
+ is-number "^2.1.0"
+ isobject "^2.0.0"
+ randomatic "^1.1.3"
+ repeat-element "^1.1.2"
+ repeat-string "^1.5.2"
+
+finalhandler@~1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.0.3.tgz#ef47e77950e999780e86022a560e3217e0d0cc89"
+ dependencies:
+ debug "2.6.7"
+ encodeurl "~1.0.1"
+ escape-html "~1.0.3"
+ on-finished "~2.3.0"
+ parseurl "~1.3.1"
+ statuses "~1.3.1"
+ unpipe "~1.0.0"
+
+find-index@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/find-index/-/find-index-1.1.0.tgz#53007c79cd30040d6816d79458e8837d5c5705ef"
+
+find-up@^1.0.0:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f"
+ dependencies:
+ path-exists "^2.0.0"
+ pinkie-promise "^2.0.0"
+
+find-up@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7"
+ dependencies:
+ locate-path "^2.0.0"
+
+findup-sync@0.4.3, findup-sync@^0.4.2:
+ version "0.4.3"
+ resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-0.4.3.tgz#40043929e7bc60adf0b7f4827c4c6e75a0deca12"
+ dependencies:
+ detect-file "^0.1.0"
+ is-glob "^2.0.1"
+ micromatch "^2.3.7"
+ resolve-dir "^0.1.0"
+
+fireworm@^0.7.0:
+ version "0.7.1"
+ resolved "https://registry.yarnpkg.com/fireworm/-/fireworm-0.7.1.tgz#ccf20f7941f108883fcddb99383dbe6e1861c758"
+ dependencies:
+ async "~0.2.9"
+ is-type "0.0.1"
+ lodash.debounce "^3.1.1"
+ lodash.flatten "^3.0.2"
+ minimatch "^3.0.2"
+
+flat-cache@^1.2.1:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-1.2.2.tgz#fa86714e72c21db88601761ecf2f555d1abc6b96"
+ dependencies:
+ circular-json "^0.3.1"
+ del "^2.0.2"
+ graceful-fs "^4.1.2"
+ write "^0.2.1"
+
+for-each@^0.3.2:
+ version "0.3.2"
+ resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.2.tgz#2c40450b9348e97f281322593ba96704b9abd4d4"
+ dependencies:
+ is-function "~1.0.0"
+
+for-in@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
+
+for-own@^0.1.4:
+ version "0.1.5"
+ resolved "https://registry.yarnpkg.com/for-own/-/for-own-0.1.5.tgz#5265c681a4f294dabbf17c9509b6763aa84510ce"
+ dependencies:
+ for-in "^1.0.1"
+
+forever-agent@~0.6.1:
+ version "0.6.1"
+ resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
+
+form-data@~2.1.1:
+ version "2.1.4"
+ resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.1.4.tgz#33c183acf193276ecaa98143a69e94bfee1750d1"
+ dependencies:
+ asynckit "^0.4.0"
+ combined-stream "^1.0.5"
+ mime-types "^2.1.12"
+
+formatio@1.2.0, formatio@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/formatio/-/formatio-1.2.0.tgz#f3b2167d9068c4698a8d51f4f760a39a54d818eb"
+ dependencies:
+ samsam "1.x"
+
+forwarded@~0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.0.tgz#19ef9874c4ae1c297bcf078fde63a09b66a84363"
+
+fresh@0.5.0:
+ version "0.5.0"
+ resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.0.tgz#f474ca5e6a9246d6fd8e0953cfa9b9c805afa78e"
+
+fs-exists-sync@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/fs-exists-sync/-/fs-exists-sync-0.1.0.tgz#982d6893af918e72d08dec9e8673ff2b5a8d6add"
+
+fs-extra@^0.24.0:
+ version "0.24.0"
+ resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-0.24.0.tgz#d4e4342a96675cb7846633a6099249332b539952"
+ dependencies:
+ graceful-fs "^4.1.2"
+ jsonfile "^2.1.0"
+ path-is-absolute "^1.0.0"
+ rimraf "^2.2.8"
+
+fs-extra@^0.26.0:
+ version "0.26.7"
+ resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-0.26.7.tgz#9ae1fdd94897798edab76d0918cf42d0c3184fa9"
+ dependencies:
+ graceful-fs "^4.1.2"
+ jsonfile "^2.1.0"
+ klaw "^1.0.0"
+ path-is-absolute "^1.0.0"
+ rimraf "^2.2.8"
+
+fs-extra@^0.30.0:
+ version "0.30.0"
+ resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-0.30.0.tgz#f233ffcc08d4da7d432daa449776989db1df93f0"
+ dependencies:
+ graceful-fs "^4.1.2"
+ jsonfile "^2.1.0"
+ klaw "^1.0.0"
+ path-is-absolute "^1.0.0"
+ rimraf "^2.2.8"
+
+fs-extra@^1.0.0, fs-extra@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-1.0.0.tgz#cd3ce5f7e7cb6145883fcae3191e9877f8587950"
+ dependencies:
+ graceful-fs "^4.1.2"
+ jsonfile "^2.1.0"
+ klaw "^1.0.0"
+
+fs-extra@^2.0.0:
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-2.1.2.tgz#046c70163cef9aad46b0e4a7fa467fb22d71de35"
+ dependencies:
+ graceful-fs "^4.1.2"
+ jsonfile "^2.1.0"
+
+fs-extra@^3.0.0:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-3.0.1.tgz#3794f378c58b342ea7dbbb23095109c4b3b62291"
+ dependencies:
+ graceful-fs "^4.1.2"
+ jsonfile "^3.0.0"
+ universalify "^0.1.0"
+
+fs-readdir-recursive@^0.1.0:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/fs-readdir-recursive/-/fs-readdir-recursive-0.1.2.tgz#315b4fb8c1ca5b8c47defef319d073dad3568059"
+
+fs-tree-diff@^0.5.2, fs-tree-diff@^0.5.3, fs-tree-diff@^0.5.4, fs-tree-diff@^0.5.6:
+ version "0.5.6"
+ resolved "https://registry.yarnpkg.com/fs-tree-diff/-/fs-tree-diff-0.5.6.tgz#342665749e8dca406800b672268c8f5073f3e623"
+ dependencies:
+ heimdalljs-logger "^0.1.7"
+ object-assign "^4.1.0"
+ path-posix "^1.0.0"
+ symlink-or-copy "^1.1.8"
+
+fs.realpath@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
+
+fsevents@^1.0.0:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.1.2.tgz#3282b713fb3ad80ede0e9fcf4611b5aa6fc033f4"
+ dependencies:
+ nan "^2.3.0"
+ node-pre-gyp "^0.6.36"
+
+fstream-ignore@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/fstream-ignore/-/fstream-ignore-1.0.5.tgz#9c31dae34767018fe1d249b24dada67d092da105"
+ dependencies:
+ fstream "^1.0.0"
+ inherits "2"
+ minimatch "^3.0.0"
+
+fstream@^1.0.0, fstream@^1.0.10, fstream@^1.0.2:
+ version "1.0.11"
+ resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.11.tgz#5c1fb1f117477114f0632a0eb4b71b3cb0fd3171"
+ dependencies:
+ graceful-fs "^4.1.2"
+ inherits "~2.0.0"
+ mkdirp ">=0.5 0"
+ rimraf "2"
+
+gauge@~2.7.3:
+ version "2.7.4"
+ resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7"
+ dependencies:
+ aproba "^1.0.3"
+ console-control-strings "^1.0.0"
+ has-unicode "^2.0.0"
+ object-assign "^4.1.0"
+ signal-exit "^3.0.0"
+ string-width "^1.0.1"
+ strip-ansi "^3.0.1"
+ wide-align "^1.1.0"
+
+gaze@^1.0.0:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/gaze/-/gaze-1.1.2.tgz#847224677adb8870d679257ed3388fdb61e40105"
+ dependencies:
+ globule "^1.0.0"
+
+generate-function@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/generate-function/-/generate-function-2.0.0.tgz#6858fe7c0969b7d4e9093337647ac79f60dfbe74"
+
+generate-object-property@^1.1.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/generate-object-property/-/generate-object-property-1.2.0.tgz#9c0e1c40308ce804f4783618b937fa88f99d50d0"
+ dependencies:
+ is-property "^1.0.0"
+
+get-caller-file@^1.0.0, get-caller-file@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.2.tgz#f702e63127e7e231c160a80c1554acb70d5047e5"
+
+get-stdin@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe"
+
+get-stdin@^5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-5.0.1.tgz#122e161591e21ff4c52530305693f20e6393a398"
+
+get-stream@^2.0.0:
+ version "2.3.1"
+ resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-2.3.1.tgz#5f38f93f346009666ee0150a054167f91bdd95de"
+ dependencies:
+ object-assign "^4.0.1"
+ pinkie-promise "^2.0.0"
+
+get-stream@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14"
+
+getpass@^0.1.1:
+ version "0.1.7"
+ resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa"
+ dependencies:
+ assert-plus "^1.0.0"
+
+git-repo-info@^1.1.2, git-repo-info@^1.4.1:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/git-repo-info/-/git-repo-info-1.4.1.tgz#2a072823254aaf62fcf0766007d7b6651bd41943"
+
+glob-base@^0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/glob-base/-/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4"
+ dependencies:
+ glob-parent "^2.0.0"
+ is-glob "^2.0.0"
+
+glob-parent@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-2.0.0.tgz#81383d72db054fcccf5336daa902f182f6edbb28"
+ dependencies:
+ is-glob "^2.0.0"
+
+glob@7.1.1, glob@^7.0.3, glob@^7.0.4, glob@^7.0.5, glob@^7.1.1:
+ version "7.1.1"
+ resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.1.tgz#805211df04faaf1c63a3600306cdf5ade50b2ec8"
+ dependencies:
+ fs.realpath "^1.0.0"
+ inflight "^1.0.4"
+ inherits "2"
+ minimatch "^3.0.2"
+ once "^1.3.0"
+ path-is-absolute "^1.0.0"
+
+glob@^5.0.10, glob@^5.0.15:
+ version "5.0.15"
+ resolved "https://registry.yarnpkg.com/glob/-/glob-5.0.15.tgz#1bc936b9e02f4a603fcc222ecf7633d30b8b93b1"
+ dependencies:
+ inflight "^1.0.4"
+ inherits "2"
+ minimatch "2 || 3"
+ once "^1.3.0"
+ path-is-absolute "^1.0.0"
+
+glob@^7.0.0, glob@^7.1.2, glob@~7.1.1:
+ version "7.1.2"
+ resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15"
+ dependencies:
+ fs.realpath "^1.0.0"
+ inflight "^1.0.4"
+ inherits "2"
+ minimatch "^3.0.4"
+ once "^1.3.0"
+ path-is-absolute "^1.0.0"
+
+glob@~7.0.6:
+ version "7.0.6"
+ resolved "https://registry.yarnpkg.com/glob/-/glob-7.0.6.tgz#211bafaf49e525b8cd93260d14ab136152b3f57a"
+ dependencies:
+ fs.realpath "^1.0.0"
+ inflight "^1.0.4"
+ inherits "2"
+ minimatch "^3.0.2"
+ once "^1.3.0"
+ path-is-absolute "^1.0.0"
+
+global-modules@^0.2.3:
+ version "0.2.3"
+ resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-0.2.3.tgz#ea5a3bed42c6d6ce995a4f8a1269b5dae223828d"
+ dependencies:
+ global-prefix "^0.1.4"
+ is-windows "^0.2.0"
+
+global-prefix@^0.1.4:
+ version "0.1.5"
+ resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-0.1.5.tgz#8d3bc6b8da3ca8112a160d8d496ff0462bfef78f"
+ dependencies:
+ homedir-polyfill "^1.0.0"
+ ini "^1.3.4"
+ is-windows "^0.2.0"
+ which "^1.2.12"
+
+global@~4.3.0:
+ version "4.3.2"
+ resolved "https://registry.yarnpkg.com/global/-/global-4.3.2.tgz#e76989268a6c74c38908b1305b10fc0e394e9d0f"
+ dependencies:
+ min-document "^2.19.0"
+ process "~0.5.1"
+
+globals@^6.4.0:
+ version "6.4.1"
+ resolved "https://registry.yarnpkg.com/globals/-/globals-6.4.1.tgz#8498032b3b6d1cc81eebc5f79690d8fe29fabf4f"
+
+globals@^9.0.0, globals@^9.14.0, globals@^9.17.0:
+ version "9.18.0"
+ resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a"
+
+globby@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/globby/-/globby-5.0.0.tgz#ebd84667ca0dbb330b99bcfc68eac2bc54370e0d"
+ dependencies:
+ array-union "^1.0.1"
+ arrify "^1.0.0"
+ glob "^7.0.3"
+ object-assign "^4.0.1"
+ pify "^2.0.0"
+ pinkie-promise "^2.0.0"
+
+globule@^1.0.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/globule/-/globule-1.2.0.tgz#1dc49c6822dd9e8a2fa00ba2a295006e8664bd09"
+ dependencies:
+ glob "~7.1.1"
+ lodash "~4.17.4"
+ minimatch "~3.0.2"
+
+good-listener@^1.2.2:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/good-listener/-/good-listener-1.2.2.tgz#d53b30cdf9313dffb7dc9a0d477096aa6d145c50"
+ dependencies:
+ delegate "^3.1.2"
+
+graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.3, graceful-fs@^4.1.4, graceful-fs@^4.1.6, graceful-fs@^4.1.9:
+ version "4.1.11"
+ resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658"
+
+"graceful-readlink@>= 1.0.0":
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725"
+
+growly@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081"
+
+handlebars@^4.0.4, handlebars@^4.0.6:
+ version "4.0.10"
+ resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.0.10.tgz#3d30c718b09a3d96f23ea4cc1f403c4d3ba9ff4f"
+ dependencies:
+ async "^1.4.0"
+ optimist "^0.6.1"
+ source-map "^0.4.4"
+ optionalDependencies:
+ uglify-js "^2.6"
+
+har-schema@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-1.0.5.tgz#d263135f43307c02c602afc8fe95970c0151369e"
+
+har-validator@~2.0.6:
+ version "2.0.6"
+ resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-2.0.6.tgz#cdcbc08188265ad119b6a5a7c8ab70eecfb5d27d"
+ dependencies:
+ chalk "^1.1.1"
+ commander "^2.9.0"
+ is-my-json-valid "^2.12.4"
+ pinkie-promise "^2.0.0"
+
+har-validator@~4.2.1:
+ version "4.2.1"
+ resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-4.2.1.tgz#33481d0f1bbff600dd203d75812a6a5fba002e2a"
+ dependencies:
+ ajv "^4.9.1"
+ har-schema "^1.0.5"
+
+harmony-reflect@^1.4.2:
+ version "1.5.1"
+ resolved "https://registry.yarnpkg.com/harmony-reflect/-/harmony-reflect-1.5.1.tgz#b54ca617b00cc8aef559bbb17b3d85431dc7e329"
+
+has-ansi@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-0.1.0.tgz#84f265aae8c0e6a88a12d7022894b7568894c62e"
+ dependencies:
+ ansi-regex "^0.2.0"
+
+has-ansi@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91"
+ dependencies:
+ ansi-regex "^2.0.0"
+
+has-binary@0.1.6:
+ version "0.1.6"
+ resolved "https://registry.yarnpkg.com/has-binary/-/has-binary-0.1.6.tgz#25326f39cfa4f616ad8787894e3af2cfbc7b6e10"
+ dependencies:
+ isarray "0.0.1"
+
+has-binary@0.1.7:
+ version "0.1.7"
+ resolved "https://registry.yarnpkg.com/has-binary/-/has-binary-0.1.7.tgz#68e61eb16210c9545a0a5cce06a873912fe1e68c"
+ dependencies:
+ isarray "0.0.1"
+
+has-cors@1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/has-cors/-/has-cors-1.1.0.tgz#5e474793f7ea9843d1bb99c23eef49ff126fff39"
+
+has-flag@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-2.0.0.tgz#e8207af1cc7b30d446cc70b734b5e8be18f88d51"
+
+has-unicode@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9"
+
+hash-for-dep@^1.0.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/hash-for-dep/-/hash-for-dep-1.1.2.tgz#e3347ed92960eb0bb53a2c6c2b70e36d75b7cd0c"
+ dependencies:
+ broccoli-kitchen-sink-helpers "^0.3.1"
+ heimdalljs "^0.2.3"
+ heimdalljs-logger "^0.1.7"
+ resolve "^1.1.6"
+
+hasha@~2.2.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/hasha/-/hasha-2.2.0.tgz#78d7cbfc1e6d66303fe79837365984517b2f6ee1"
+ dependencies:
+ is-stream "^1.0.1"
+ pinkie-promise "^2.0.0"
+
+hawk@~3.1.3:
+ version "3.1.3"
+ resolved "https://registry.yarnpkg.com/hawk/-/hawk-3.1.3.tgz#078444bd7c1640b0fe540d2c9b73d59678e8e1c4"
+ dependencies:
+ boom "2.x.x"
+ cryptiles "2.x.x"
+ hoek "2.x.x"
+ sntp "1.x.x"
+
+heimdalljs-fs-monitor@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/heimdalljs-fs-monitor/-/heimdalljs-fs-monitor-0.1.0.tgz#d404a65688c6714c485469ed3495da4853440272"
+ dependencies:
+ heimdalljs "^0.2.0"
+ heimdalljs-logger "^0.1.7"
+
+heimdalljs-graph@^0.3.1:
+ version "0.3.3"
+ resolved "https://registry.yarnpkg.com/heimdalljs-graph/-/heimdalljs-graph-0.3.3.tgz#ea801dbba659c8d522fe1cb83b2d605726e4918f"
+
+heimdalljs-logger@^0.1.7:
+ version "0.1.9"
+ resolved "https://registry.yarnpkg.com/heimdalljs-logger/-/heimdalljs-logger-0.1.9.tgz#d76ada4e45b7bb6f786fc9c010a68eb2e2faf176"
+ dependencies:
+ debug "^2.2.0"
+ heimdalljs "^0.2.0"
+
+heimdalljs@^0.2.0, heimdalljs@^0.2.1, heimdalljs@^0.2.3:
+ version "0.2.5"
+ resolved "https://registry.yarnpkg.com/heimdalljs/-/heimdalljs-0.2.5.tgz#6aa54308eee793b642cff9cf94781445f37730ac"
+ dependencies:
+ rsvp "~3.2.1"
+
+heimdalljs@^0.3.0:
+ version "0.3.3"
+ resolved "https://registry.yarnpkg.com/heimdalljs/-/heimdalljs-0.3.3.tgz#e92d2c6f77fd46d5bf50b610d28ad31755054d0b"
+ dependencies:
+ rsvp "~3.2.1"
+
+hoek@2.x.x:
+ version "2.16.3"
+ resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed"
+
+home-or-tmp@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-1.0.0.tgz#4b9f1e40800c3e50c6c27f781676afcce71f3985"
+ dependencies:
+ os-tmpdir "^1.0.1"
+ user-home "^1.1.1"
+
+home-or-tmp@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8"
+ dependencies:
+ os-homedir "^1.0.0"
+ os-tmpdir "^1.0.1"
+
+homedir-polyfill@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.1.tgz#4c2bbc8a758998feebf5ed68580f76d46768b4bc"
+ dependencies:
+ parse-passwd "^1.0.0"
+
+hosted-git-info@^2.1.4, hosted-git-info@^2.1.5:
+ version "2.5.0"
+ resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.5.0.tgz#6d60e34b3abbc8313062c3b798ef8d901a07af3c"
+
+htmlparser2@~3.8.1:
+ version "3.8.3"
+ resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.8.3.tgz#996c28b191516a8be86501a7d79757e5c70c1068"
+ dependencies:
+ domelementtype "1"
+ domhandler "2.3"
+ domutils "1.5"
+ entities "1.0"
+ readable-stream "1.1"
+
+http-errors@~1.6.1:
+ version "1.6.1"
+ resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.1.tgz#5f8b8ed98aca545656bf572997387f904a722257"
+ dependencies:
+ depd "1.1.0"
+ inherits "2.0.3"
+ setprototypeof "1.0.3"
+ statuses ">= 1.3.1 < 2"
+
+http-proxy@^1.13.1, http-proxy@^1.9.0:
+ version "1.16.2"
+ resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.16.2.tgz#06dff292952bf64dbe8471fa9df73066d4f37742"
+ dependencies:
+ eventemitter3 "1.x.x"
+ requires-port "1.x.x"
+
+http-signature@~1.1.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.1.1.tgz#df72e267066cd0ac67fb76adf8e134a8fbcf91bf"
+ dependencies:
+ assert-plus "^0.2.0"
+ jsprim "^1.2.2"
+ sshpk "^1.7.0"
+
+iconv-lite@^0.4.17, iconv-lite@^0.4.5, iconv-lite@~0.4.13:
+ version "0.4.18"
+ resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.18.tgz#23d8656b16aae6742ac29732ea8f0336a4789cf2"
+
+ignore@^3.2.0, ignore@^3.2.7, ignore@^3.3.3:
+ version "3.3.3"
+ resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.3.tgz#432352e57accd87ab3110e82d3fea0e47812156d"
+
+image-size@^0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/image-size/-/image-size-0.4.0.tgz#d4b4e1f61952e4cbc1cea9a6b0c915fecb707510"
+
+image-size@^0.5.0:
+ version "0.5.5"
+ resolved "https://registry.yarnpkg.com/image-size/-/image-size-0.5.5.tgz#09dfd4ab9d20e29eb1c3e80b8990378df9e3cb9c"
+
+imurmurhash@^0.1.4:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
+
+in-publish@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/in-publish/-/in-publish-2.0.0.tgz#e20ff5e3a2afc2690320b6dc552682a9c7fadf51"
+
+include-path-searcher@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/include-path-searcher/-/include-path-searcher-0.1.0.tgz#c0cf2ddfa164fb2eae07bc7ca43a7f191cb4d7bd"
+
+indent-string@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-2.1.0.tgz#8e2d48348742121b4a8218b7a137e9a52049dc80"
+ dependencies:
+ repeating "^2.0.0"
+
+indent-string@^3.1.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-3.2.0.tgz#4a5fd6d27cc332f37e5419a504dbb837105c9289"
+
+indexof@0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d"
+
+inflection@^1.7.0, inflection@^1.7.1, inflection@^1.8.0:
+ version "1.12.0"
+ resolved "https://registry.yarnpkg.com/inflection/-/inflection-1.12.0.tgz#a200935656d6f5f6bc4dc7502e1aecb703228416"
+
+inflight@^1.0.4:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
+ dependencies:
+ once "^1.3.0"
+ wrappy "1"
+
+inherits@2, inherits@2.0.3, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.0, inherits@~2.0.1, inherits@~2.0.3:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
+
+ini@^1.3.4, ini@~1.3.0:
+ version "1.3.4"
+ resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.4.tgz#0537cb79daf59b59a1a517dff706c86ec039162e"
+
+inline-source-map-comment@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/inline-source-map-comment/-/inline-source-map-comment-1.0.5.tgz#50a8a44c2a790dfac441b5c94eccd5462635faf6"
+ dependencies:
+ chalk "^1.0.0"
+ get-stdin "^4.0.1"
+ minimist "^1.1.1"
+ sum-up "^1.0.1"
+ xtend "^4.0.0"
+
+inquirer@^0.12.0:
+ version "0.12.0"
+ resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-0.12.0.tgz#1ef2bfd63504df0bc75785fff8c2c41df12f077e"
+ dependencies:
+ ansi-escapes "^1.1.0"
+ ansi-regex "^2.0.0"
+ chalk "^1.0.0"
+ cli-cursor "^1.0.1"
+ cli-width "^2.0.0"
+ figures "^1.3.5"
+ lodash "^4.3.0"
+ readline2 "^1.0.1"
+ run-async "^0.1.0"
+ rx-lite "^3.1.2"
+ string-width "^1.0.1"
+ strip-ansi "^3.0.0"
+ through "^2.3.6"
+
+inquirer@^1.2.3:
+ version "1.2.3"
+ resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-1.2.3.tgz#4dec6f32f37ef7bb0b2ed3f1d1a5c3f545074918"
+ dependencies:
+ ansi-escapes "^1.1.0"
+ chalk "^1.0.0"
+ cli-cursor "^1.0.1"
+ cli-width "^2.0.0"
+ external-editor "^1.1.0"
+ figures "^1.3.5"
+ lodash "^4.3.0"
+ mute-stream "0.0.6"
+ pinkie-promise "^2.0.0"
+ run-async "^2.2.0"
+ rx "^4.1.0"
+ string-width "^1.0.1"
+ strip-ansi "^3.0.0"
+ through "^2.3.6"
+
+inquirer@^3.0.6:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-3.2.0.tgz#45b44c2160c729d7578c54060b3eed94487bb42b"
+ dependencies:
+ ansi-escapes "^2.0.0"
+ chalk "^2.0.0"
+ cli-cursor "^2.1.0"
+ cli-width "^2.0.0"
+ external-editor "^2.0.4"
+ figures "^2.0.0"
+ lodash "^4.3.0"
+ mute-stream "0.0.7"
+ run-async "^2.2.0"
+ rx-lite "^4.0.8"
+ rx-lite-aggregates "^4.0.8"
+ string-width "^2.1.0"
+ strip-ansi "^4.0.0"
+ through "^2.3.6"
+
+interpret@^1.0.0:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.0.3.tgz#cbc35c62eeee73f19ab7b10a801511401afc0f90"
+
+invariant@^2.2.0, invariant@^2.2.2:
+ version "2.2.2"
+ resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.2.tgz#9e1f56ac0acdb6bf303306f338be3b204ae60360"
+ dependencies:
+ loose-envify "^1.0.0"
+
+invert-kv@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6"
+
+ip-regex@^1.0.1:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-1.0.3.tgz#dc589076f659f419c222039a33316f1c7387effd"
+
+ipaddr.js@1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.3.0.tgz#1e03a52fdad83a8bbb2b25cbf4998b4cffcd3dec"
+
+is-arrayish@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
+
+is-binary-path@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898"
+ dependencies:
+ binary-extensions "^1.0.0"
+
+is-buffer@^1.1.5:
+ version "1.1.5"
+ resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.5.tgz#1f3b26ef613b214b88cbca23cc6c01d87961eecc"
+
+is-builtin-module@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-1.0.0.tgz#540572d34f7ac3119f8f76c30cbc1b1e037affbe"
+ dependencies:
+ builtin-modules "^1.0.0"
+
+is-dotfile@^1.0.0:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/is-dotfile/-/is-dotfile-1.0.3.tgz#a6a2f32ffd2dfb04f5ca25ecd0f6b83cf798a1e1"
+
+is-equal-shallow@^0.1.3:
+ version "0.1.3"
+ resolved "https://registry.yarnpkg.com/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz#2238098fc221de0bcfa5d9eac4c45d638aa1c534"
+ dependencies:
+ is-primitive "^2.0.0"
+
+is-extendable@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89"
+
+is-extglob@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-1.0.0.tgz#ac468177c4943405a092fc8f29760c6ffc6206c0"
+
+is-finite@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.0.2.tgz#cc6677695602be550ef11e8b4aa6305342b6d0aa"
+ dependencies:
+ number-is-nan "^1.0.0"
+
+is-fullwidth-code-point@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb"
+ dependencies:
+ number-is-nan "^1.0.0"
+
+is-fullwidth-code-point@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f"
+
+is-function@^1.0.1, is-function@~1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/is-function/-/is-function-1.0.1.tgz#12cfb98b65b57dd3d193a3121f5f6e2f437602b5"
+
+is-git-url@^0.2.0:
+ version "0.2.3"
+ resolved "https://registry.yarnpkg.com/is-git-url/-/is-git-url-0.2.3.tgz#445200d6fbd6da028fb5e01440d9afc93f3ccb64"
+
+is-glob@^2.0.0, is-glob@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-2.0.1.tgz#d096f926a3ded5600f3fdfd91198cb0888c2d863"
+ dependencies:
+ is-extglob "^1.0.0"
+
+is-integer@^1.0.4:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/is-integer/-/is-integer-1.0.7.tgz#6bde81aacddf78b659b6629d629cadc51a886d5c"
+ dependencies:
+ is-finite "^1.0.0"
+
+is-my-json-valid@^2.10.0, is-my-json-valid@^2.12.4:
+ version "2.16.0"
+ resolved "https://registry.yarnpkg.com/is-my-json-valid/-/is-my-json-valid-2.16.0.tgz#f079dd9bfdae65ee2038aae8acbc86ab109e3693"
+ dependencies:
+ generate-function "^2.0.0"
+ generate-object-property "^1.1.0"
+ jsonpointer "^4.0.0"
+ xtend "^4.0.0"
+
+is-number@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/is-number/-/is-number-2.1.0.tgz#01fcbbb393463a548f2f466cce16dece49db908f"
+ dependencies:
+ kind-of "^3.0.2"
+
+is-number@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195"
+ dependencies:
+ kind-of "^3.0.2"
+
+is-obj@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f"
+
+is-path-cwd@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-1.0.0.tgz#d225ec23132e89edd38fda767472e62e65f1106d"
+
+is-path-in-cwd@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-path-in-cwd/-/is-path-in-cwd-1.0.0.tgz#6477582b8214d602346094567003be8a9eac04dc"
+ dependencies:
+ is-path-inside "^1.0.0"
+
+is-path-inside@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-1.0.0.tgz#fc06e5a1683fbda13de667aff717bbc10a48f37f"
+ dependencies:
+ path-is-inside "^1.0.1"
+
+is-posix-bracket@^0.1.0:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz#3334dc79774368e92f016e6fbc0a88f5cd6e6bc4"
+
+is-primitive@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/is-primitive/-/is-primitive-2.0.0.tgz#207bab91638499c07b2adf240a41a87210034575"
+
+is-promise@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa"
+
+is-property@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84"
+
+is-resolvable@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.0.0.tgz#8df57c61ea2e3c501408d100fb013cf8d6e0cc62"
+ dependencies:
+ tryit "^1.0.1"
+
+is-stream@^1.0.1, is-stream@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
+
+is-type@0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/is-type/-/is-type-0.0.1.tgz#f651d85c365d44955d14a51d8d7061f3f6b4779c"
+ dependencies:
+ core-util-is "~1.0.0"
+
+is-typedarray@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
+
+is-utf8@^0.2.0:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72"
+
+is-windows@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-0.2.0.tgz#de1aa6d63ea29dd248737b69f1ff8b8002d2108c"
+
+isarray@0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
+
+isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
+
+isbinaryfile@^3.0.0:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-3.0.2.tgz#4a3e974ec0cba9004d3fc6cde7209ea69368a621"
+
+isexe@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
+
+isobject@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89"
+ dependencies:
+ isarray "1.0.0"
+
+isstream@0.1.x, isstream@~0.1.2:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
+
+istextorbinary@2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/istextorbinary/-/istextorbinary-2.1.0.tgz#dbed2a6f51be2f7475b68f89465811141b758874"
+ dependencies:
+ binaryextensions "1 || 2"
+ editions "^1.1.1"
+ textextensions "1 || 2"
+
+ivy-codemirror@2.0.3:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/ivy-codemirror/-/ivy-codemirror-2.0.3.tgz#a5b26d343be2031dead036e2be794c46f1b157d9"
+ dependencies:
+ ember-cli-babel "^5.1.7"
+
+jimp@^0.2.13, jimp@^0.2.21:
+ version "0.2.28"
+ resolved "https://registry.yarnpkg.com/jimp/-/jimp-0.2.28.tgz#dd529a937190f42957a7937d1acc3a7762996ea2"
+ dependencies:
+ bignumber.js "^2.1.0"
+ bmp-js "0.0.3"
+ es6-promise "^3.0.2"
+ exif-parser "^0.1.9"
+ file-type "^3.1.0"
+ jpeg-js "^0.2.0"
+ load-bmfont "^1.2.3"
+ mime "^1.3.4"
+ mkdirp "0.5.1"
+ pixelmatch "^4.0.0"
+ pngjs "^3.0.0"
+ read-chunk "^1.0.1"
+ request "^2.65.0"
+ stream-to-buffer "^0.1.0"
+ tinycolor2 "^1.1.2"
+ url-regex "^3.0.0"
+
+jpeg-js@^0.1.1:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/jpeg-js/-/jpeg-js-0.1.2.tgz#135b992c0575c985cfa0f494a3227ed238583ece"
+
+jpeg-js@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/jpeg-js/-/jpeg-js-0.2.0.tgz#53e448ec9d263e683266467e9442d2c5a2ef5482"
+
+jquery@^3.2.1:
+ version "3.2.1"
+ resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.2.1.tgz#5c4d9de652af6cd0a770154a631bba12b015c787"
+
+js-base64@^2.1.8:
+ version "2.1.9"
+ resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.1.9.tgz#f0e80ae039a4bd654b5f281fc93f04a914a7fcce"
+
+js-reporters@1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/js-reporters/-/js-reporters-1.2.0.tgz#7cf2cb698196684790350d0c4ca07f4aed9ec17e"
+
+js-tokens@1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-1.0.1.tgz#cc435a5c8b94ad15acb7983140fc80182c89aeae"
+
+js-tokens@^3.0.0:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b"
+
+js-yaml@0.3.x:
+ version "0.3.7"
+ resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-0.3.7.tgz#d739d8ee86461e54b354d6a7d7d1f2ad9a167f62"
+
+js-yaml@^3.2.5, js-yaml@^3.2.7, js-yaml@^3.3.0, js-yaml@^3.5.1, js-yaml@^3.6.1, js-yaml@^3.8.4:
+ version "3.9.0"
+ resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.9.0.tgz#4ffbbf25c2ac963b8299dc74da7e3740de1c18ce"
+ dependencies:
+ argparse "^1.0.7"
+ esprima "^4.0.0"
+
+jsbn@~0.1.0:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
+
+jschardet@^1.4.2:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/jschardet/-/jschardet-1.5.0.tgz#a61f310306a5a71188e1b1acd08add3cfbb08b1e"
+
+jsesc@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-1.3.0.tgz#46c3fec8c1892b12b0833db9bc7622176dbab34b"
+
+jsesc@^2.5.0:
+ version "2.5.1"
+ resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.1.tgz#e421a2a8e20d6b0819df28908f782526b96dd1fe"
+
+jsesc@~0.5.0:
+ version "0.5.0"
+ resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d"
+
+jsmin@1.x:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/jsmin/-/jsmin-1.0.1.tgz#e7bd0dcd6496c3bf4863235bf461a3d98aa3b98c"
+
+json-schema-traverse@^0.3.0:
+ version "0.3.1"
+ resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340"
+
+json-schema@0.2.3:
+ version "0.2.3"
+ resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13"
+
+json-stable-stringify@^1.0.0, json-stable-stringify@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz#9a759d39c5f2ff503fd5300646ed445f88c4f9af"
+ dependencies:
+ jsonify "~0.0.0"
+
+json-stringify-safe@~5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
+
+json3@3.3.2:
+ version "3.3.2"
+ resolved "https://registry.yarnpkg.com/json3/-/json3-3.3.2.tgz#3c0434743df93e2f5c42aee7b19bcb483575f4e1"
+
+json5@^0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/json5/-/json5-0.4.0.tgz#054352e4c4c80c86c0923877d449de176a732c8d"
+
+json5@^0.5.0:
+ version "0.5.1"
+ resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821"
+
+jsonfile@^2.1.0:
+ version "2.4.0"
+ resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-2.4.0.tgz#3736a2b428b87bbda0cc83b53fa3d633a35c2ae8"
+ optionalDependencies:
+ graceful-fs "^4.1.6"
+
+jsonfile@^3.0.0:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-3.0.1.tgz#a5ecc6f65f53f662c4415c7675a0331d0992ec66"
+ optionalDependencies:
+ graceful-fs "^4.1.6"
+
+jsonify@~0.0.0:
+ version "0.0.0"
+ resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73"
+
+jsonpointer@^4.0.0:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-4.0.1.tgz#4fd92cb34e0e9db3c89c8622ecf51f9b978c6cb9"
+
+jsontoxml@0.0.11:
+ version "0.0.11"
+ resolved "https://registry.yarnpkg.com/jsontoxml/-/jsontoxml-0.0.11.tgz#373ab5b2070be3737a5fb3e32fd1b7b81870caa4"
+
+jsprim@^1.2.2:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.0.tgz#a3b87e40298d8c380552d8cc7628a0bb95a22918"
+ dependencies:
+ assert-plus "1.0.0"
+ extsprintf "1.0.2"
+ json-schema "0.2.3"
+ verror "1.3.6"
+
+just-extend@^1.1.26:
+ version "1.1.27"
+ resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-1.1.27.tgz#ec6e79410ff914e472652abfa0e603c03d60e905"
+
+jxLoader@*:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/jxLoader/-/jxLoader-0.1.1.tgz#0134ea5144e533b594fc1ff25ff194e235c53ecd"
+ dependencies:
+ js-yaml "0.3.x"
+ moo-server "1.3.x"
+ promised-io "*"
+ walker "1.x"
+
+kew@~0.7.0:
+ version "0.7.0"
+ resolved "https://registry.yarnpkg.com/kew/-/kew-0.7.0.tgz#79d93d2d33363d6fdd2970b335d9141ad591d79b"
+
+kind-of@^3.0.2:
+ version "3.2.2"
+ resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64"
+ dependencies:
+ is-buffer "^1.1.5"
+
+kind-of@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57"
+ dependencies:
+ is-buffer "^1.1.5"
+
+klaw@^1.0.0:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/klaw/-/klaw-1.3.1.tgz#4088433b46b3b1ba259d78785d8e96f73ba02439"
+ optionalDependencies:
+ graceful-fs "^4.1.9"
+
+lazy-cache@^1.0.3:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-1.0.4.tgz#a1d78fc3a50474cb80845d3b3b6e1da49a446e8e"
+
+lcid@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835"
+ dependencies:
+ invert-kv "^1.0.0"
+
+leek@0.0.24:
+ version "0.0.24"
+ resolved "https://registry.yarnpkg.com/leek/-/leek-0.0.24.tgz#e400e57f0e60d8ef2bd4d068dc428a54345dbcda"
+ dependencies:
+ debug "^2.1.0"
+ lodash.assign "^3.2.0"
+ rsvp "^3.0.21"
+
+leven@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/leven/-/leven-1.0.2.tgz#9144b6eebca5f1d0680169f1a6770dcea60b75c3"
+
+levn@^0.3.0, levn@~0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee"
+ dependencies:
+ prelude-ls "~1.1.2"
+ type-check "~0.3.2"
+
+linkify-it@^2.0.0:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-2.0.3.tgz#d94a4648f9b1c179d64fa97291268bdb6ce9434f"
+ dependencies:
+ uc.micro "^1.0.1"
+
+livereload-js@^2.2.2:
+ version "2.2.2"
+ resolved "https://registry.yarnpkg.com/livereload-js/-/livereload-js-2.2.2.tgz#6c87257e648ab475bc24ea257457edcc1f8d0bc2"
+
+load-bmfont@^1.2.3:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/load-bmfont/-/load-bmfont-1.3.0.tgz#bb7e7c710de6bcafcb13cb3b8c81e0c0131ecbc9"
+ dependencies:
+ buffer-equal "0.0.1"
+ mime "^1.3.4"
+ parse-bmfont-ascii "^1.0.3"
+ parse-bmfont-binary "^1.0.5"
+ parse-bmfont-xml "^1.1.0"
+ xhr "^2.0.1"
+ xtend "^4.0.0"
+
+load-json-file@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0"
+ dependencies:
+ graceful-fs "^4.1.2"
+ parse-json "^2.2.0"
+ pify "^2.0.0"
+ pinkie-promise "^2.0.0"
+ strip-bom "^2.0.0"
+
+loader.js@^4.2.3:
+ version "4.5.1"
+ resolved "https://registry.yarnpkg.com/loader.js/-/loader.js-4.5.1.tgz#c15ab15a6b8376bd4fbf7ea56f8d76cc557331da"
+
+locate-path@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e"
+ dependencies:
+ p-locate "^2.0.0"
+ path-exists "^3.0.0"
+
+lodash-es@^4.17.4:
+ version "4.17.4"
+ resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.4.tgz#dcc1d7552e150a0640073ba9cb31d70f032950e7"
+
+lodash._baseassign@^3.0.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz#8c38a099500f215ad09e59f1722fd0c52bfe0a4e"
+ dependencies:
+ lodash._basecopy "^3.0.0"
+ lodash.keys "^3.0.0"
+
+lodash._basecopy@^3.0.0:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz#8da0e6a876cf344c0ad8a54882111dd3c5c7ca36"
+
+lodash._baseflatten@^3.0.0:
+ version "3.1.4"
+ resolved "https://registry.yarnpkg.com/lodash._baseflatten/-/lodash._baseflatten-3.1.4.tgz#0770ff80131af6e34f3b511796a7ba5214e65ff7"
+ dependencies:
+ lodash.isarguments "^3.0.0"
+ lodash.isarray "^3.0.0"
+
+lodash._basetostring@^3.0.0:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/lodash._basetostring/-/lodash._basetostring-3.0.1.tgz#d1861d877f824a52f669832dcaf3ee15566a07d5"
+
+lodash._basevalues@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/lodash._basevalues/-/lodash._basevalues-3.0.0.tgz#5b775762802bde3d3297503e26300820fdf661b7"
+
+lodash._bindcallback@^3.0.0:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz#e531c27644cf8b57a99e17ed95b35c748789392e"
+
+lodash._createassigner@^3.0.0:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/lodash._createassigner/-/lodash._createassigner-3.1.1.tgz#838a5bae2fdaca63ac22dee8e19fa4e6d6970b11"
+ dependencies:
+ lodash._bindcallback "^3.0.0"
+ lodash._isiterateecall "^3.0.0"
+ lodash.restparam "^3.0.0"
+
+lodash._getnative@^3.0.0:
+ version "3.9.1"
+ resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5"
+
+lodash._isiterateecall@^3.0.0:
+ version "3.0.9"
+ resolved "https://registry.yarnpkg.com/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz#5203ad7ba425fae842460e696db9cf3e6aac057c"
+
+lodash._reinterpolate@^3.0.0, lodash._reinterpolate@~3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d"
+
+lodash._root@^3.0.0:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/lodash._root/-/lodash._root-3.0.1.tgz#fba1c4524c19ee9a5f8136b4609f017cf4ded692"
+
+lodash.assign@^3.2.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-3.2.0.tgz#3ce9f0234b4b2223e296b8fa0ac1fee8ebca64fa"
+ dependencies:
+ lodash._baseassign "^3.0.0"
+ lodash._createassigner "^3.0.0"
+ lodash.keys "^3.0.0"
+
+lodash.assign@^4.2.0:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-4.2.0.tgz#0d99f3ccd7a6d261d19bdaeb9245005d285808e7"
+
+lodash.assignin@^4.1.0:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/lodash.assignin/-/lodash.assignin-4.2.0.tgz#ba8df5fb841eb0a3e8044232b0e263a8dc6a28a2"
+
+lodash.clonedeep@^4.3.2, lodash.clonedeep@^4.4.1:
+ version "4.5.0"
+ resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
+
+lodash.debounce@^3.1.1:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-3.1.1.tgz#812211c378a94cc29d5aa4e3346cf0bfce3a7df5"
+ dependencies:
+ lodash._getnative "^3.0.0"
+
+lodash.defaults@^4.1.0:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c"
+
+lodash.defaultsdeep@^4.6.0:
+ version "4.6.0"
+ resolved "https://registry.yarnpkg.com/lodash.defaultsdeep/-/lodash.defaultsdeep-4.6.0.tgz#bec1024f85b1bd96cbea405b23c14ad6443a6f81"
+
+lodash.escape@^3.0.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/lodash.escape/-/lodash.escape-3.2.0.tgz#995ee0dc18c1b48cc92effae71a10aab5b487698"
+ dependencies:
+ lodash._root "^3.0.0"
+
+lodash.find@^4.5.1:
+ version "4.6.0"
+ resolved "https://registry.yarnpkg.com/lodash.find/-/lodash.find-4.6.0.tgz#cb0704d47ab71789ffa0de8b97dd926fb88b13b1"
+
+lodash.flatten@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-3.0.2.tgz#de1cf57758f8f4479319d35c3e9cc60c4501938c"
+ dependencies:
+ lodash._baseflatten "^3.0.0"
+ lodash._isiterateecall "^3.0.0"
+
+lodash.get@^4.4.2:
+ version "4.4.2"
+ resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99"
+
+lodash.isarguments@^3.0.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a"
+
+lodash.isarray@^3.0.0:
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-3.0.4.tgz#79e4eb88c36a8122af86f844aa9bcd851b5fbb55"
+
+lodash.keys@^3.0.0:
+ version "3.1.2"
+ resolved "https://registry.yarnpkg.com/lodash.keys/-/lodash.keys-3.1.2.tgz#4dbc0472b156be50a0b286855d1bd0b0c656098a"
+ dependencies:
+ lodash._getnative "^3.0.0"
+ lodash.isarguments "^3.0.0"
+ lodash.isarray "^3.0.0"
+
+lodash.memoize@^4.1.2:
+ version "4.1.2"
+ resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
+
+lodash.merge@^4.3.0, lodash.merge@^4.4.0, lodash.merge@^4.5.1, lodash.merge@^4.6.0:
+ version "4.6.0"
+ resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.0.tgz#69884ba144ac33fe699737a6086deffadd0f89c5"
+
+lodash.mergewith@^4.6.0:
+ version "4.6.0"
+ resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.0.tgz#150cf0a16791f5903b8891eab154609274bdea55"
+
+lodash.omit@^4.1.0:
+ version "4.5.0"
+ resolved "https://registry.yarnpkg.com/lodash.omit/-/lodash.omit-4.5.0.tgz#6eb19ae5a1ee1dd9df0b969e66ce0b7fa30b5e60"
+
+lodash.restparam@^3.0.0:
+ version "3.6.1"
+ resolved "https://registry.yarnpkg.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz#936a4e309ef330a7645ed4145986c85ae5b20805"
+
+lodash.template@^3.3.2:
+ version "3.6.2"
+ resolved "https://registry.yarnpkg.com/lodash.template/-/lodash.template-3.6.2.tgz#f8cdecc6169a255be9098ae8b0c53d378931d14f"
+ dependencies:
+ lodash._basecopy "^3.0.0"
+ lodash._basetostring "^3.0.0"
+ lodash._basevalues "^3.0.0"
+ lodash._isiterateecall "^3.0.0"
+ lodash._reinterpolate "^3.0.0"
+ lodash.escape "^3.0.0"
+ lodash.keys "^3.0.0"
+ lodash.restparam "^3.0.0"
+ lodash.templatesettings "^3.0.0"
+
+lodash.template@^4.2.5:
+ version "4.4.0"
+ resolved "https://registry.yarnpkg.com/lodash.template/-/lodash.template-4.4.0.tgz#e73a0385c8355591746e020b99679c690e68fba0"
+ dependencies:
+ lodash._reinterpolate "~3.0.0"
+ lodash.templatesettings "^4.0.0"
+
+lodash.templatesettings@^3.0.0:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/lodash.templatesettings/-/lodash.templatesettings-3.1.1.tgz#fb307844753b66b9f1afa54e262c745307dba8e5"
+ dependencies:
+ lodash._reinterpolate "^3.0.0"
+ lodash.escape "^3.0.0"
+
+lodash.templatesettings@^4.0.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/lodash.templatesettings/-/lodash.templatesettings-4.1.0.tgz#2b4d4e95ba440d915ff08bc899e4553666713316"
+ dependencies:
+ lodash._reinterpolate "~3.0.0"
+
+lodash.uniq@^4.2.0:
+ version "4.5.0"
+ resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
+
+lodash.uniqby@^4.7.0:
+ version "4.7.0"
+ resolved "https://registry.yarnpkg.com/lodash.uniqby/-/lodash.uniqby-4.7.0.tgz#d99c07a669e9e6d24e1362dfe266c67616af1302"
+
+lodash@^3.10.0, lodash@^3.10.1, lodash@^3.2.0, lodash@^3.9.3:
+ version "3.10.1"
+ resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6"
+
+lodash@^4.0.0, lodash@^4.10.0, lodash@^4.14.0, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.3.0, lodash@^4.5.1, lodash@^4.6.1, lodash@~4.17.4:
+ version "4.17.4"
+ resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae"
+
+lodash@~2.4.1:
+ version "2.4.2"
+ resolved "https://registry.yarnpkg.com/lodash/-/lodash-2.4.2.tgz#fadd834b9683073da179b3eae6d9c0d15053f73e"
+
+loglevel-colored-level-prefix@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/loglevel-colored-level-prefix/-/loglevel-colored-level-prefix-1.0.0.tgz#6a40218fdc7ae15fc76c3d0f3e676c465388603e"
+ dependencies:
+ chalk "^1.1.3"
+ loglevel "^1.4.1"
+
+loglevel@^1.4.1:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.4.1.tgz#95b383f91a3c2756fd4ab093667e4309161f2bcd"
+
+lolex@^1.6.0:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/lolex/-/lolex-1.6.0.tgz#3a9a0283452a47d7439e72731b9e07d7386e49f6"
+
+lolex@^2.1.2:
+ version "2.3.1"
+ resolved "https://registry.yarnpkg.com/lolex/-/lolex-2.3.1.tgz#3d2319894471ea0950ef64692ead2a5318cff362"
+
+longest@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097"
+
+loose-envify@^1.0.0:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.3.1.tgz#d1a8ad33fa9ce0e713d65fdd0ac8b748d478c848"
+ dependencies:
+ js-tokens "^3.0.0"
+
+loud-rejection@^1.0.0:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f"
+ dependencies:
+ currently-unhandled "^0.4.1"
+ signal-exit "^3.0.0"
+
+lru-cache@^4.0.1:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.1.tgz#622e32e82488b49279114a4f9ecf45e7cd6bba55"
+ dependencies:
+ pseudomap "^1.0.2"
+ yallist "^2.1.2"
+
+make-dir@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.0.0.tgz#97a011751e91dd87cfadef58832ebb04936de978"
+ dependencies:
+ pify "^2.3.0"
+
+make-plural@~3.0.6:
+ version "3.0.6"
+ resolved "https://registry.yarnpkg.com/make-plural/-/make-plural-3.0.6.tgz#2033a03bac290b8f3bb91258f65b9df7e8b01ca7"
+ optionalDependencies:
+ minimist "^1.2.0"
+
+makeerror@1.0.x:
+ version "1.0.11"
+ resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.11.tgz#e01a5c9109f2af79660e4e8b9587790184f5a96c"
+ dependencies:
+ tmpl "1.0.x"
+
+map-obj@^1.0.0, map-obj@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d"
+
+map-obj@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-2.0.0.tgz#a65cd29087a92598b8791257a523e021222ac1f9"
+
+markdown-it-terminal@0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/markdown-it-terminal/-/markdown-it-terminal-0.1.0.tgz#545abd8dd01c3d62353bfcea71db580b51d22bd9"
+ dependencies:
+ ansi-styles "^3.0.0"
+ cardinal "^1.0.0"
+ cli-table "^0.3.1"
+ lodash.merge "^4.6.0"
+ markdown-it "^8.3.1"
+
+markdown-it@^8.3.0, markdown-it@^8.3.1:
+ version "8.3.1"
+ resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-8.3.1.tgz#2f4b622948ccdc193d66f3ca2d43125ac4ac7323"
+ dependencies:
+ argparse "^1.0.7"
+ entities "~1.1.1"
+ linkify-it "^2.0.0"
+ mdurl "^1.0.1"
+ uc.micro "^1.0.3"
+
+matcher-collection@^1.0.0, matcher-collection@^1.0.1:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/matcher-collection/-/matcher-collection-1.0.4.tgz#2f66ae0869996f29e43d0b62c83dd1d43e581755"
+ dependencies:
+ minimatch "^3.0.2"
+
+md5-hex@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/md5-hex/-/md5-hex-2.0.0.tgz#d0588e9f1c74954492ecd24ac0ac6ce997d92e33"
+ dependencies:
+ md5-o-matic "^0.1.1"
+
+md5-o-matic@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/md5-o-matic/-/md5-o-matic-0.1.1.tgz#822bccd65e117c514fab176b25945d54100a03c3"
+
+mdurl@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e"
+
+media-typer@0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
+
+memory-streams@^0.1.0:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/memory-streams/-/memory-streams-0.1.2.tgz#273ff777ab60fec599b116355255282cca2c50c2"
+ dependencies:
+ readable-stream "~1.0.2"
+
+meow@^3.7.0:
+ version "3.7.0"
+ resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb"
+ dependencies:
+ camelcase-keys "^2.0.0"
+ decamelize "^1.1.2"
+ loud-rejection "^1.0.0"
+ map-obj "^1.0.1"
+ minimist "^1.1.3"
+ normalize-package-data "^2.3.4"
+ object-assign "^4.0.1"
+ read-pkg-up "^1.0.1"
+ redent "^1.0.0"
+ trim-newlines "^1.0.0"
+
+merge-defaults@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/merge-defaults/-/merge-defaults-0.2.1.tgz#dd42248eb96bb6a51521724321c72ff9583dde80"
+ dependencies:
+ lodash "~2.4.1"
+
+merge-descriptors@1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"
+
+merge-trees@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/merge-trees/-/merge-trees-1.0.1.tgz#ccbe674569787f9def17fd46e6525f5700bbd23e"
+ dependencies:
+ can-symlink "^1.0.0"
+ fs-tree-diff "^0.5.4"
+ heimdalljs "^0.2.1"
+ heimdalljs-logger "^0.1.7"
+ rimraf "^2.4.3"
+ symlink-or-copy "^1.0.0"
+
+merge@^1.1.3, merge@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/merge/-/merge-1.2.0.tgz#7531e39d4949c281a66b8c5a6e0265e8b05894da"
+
+messageformat-parser@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/messageformat-parser/-/messageformat-parser-1.1.0.tgz#13ba2250a76bbde8e0fca0dbb3475f95c594a90a"
+
+messageformat@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/messageformat/-/messageformat-1.0.2.tgz#908f4691f29ff28dae35c45436a24cff93402388"
+ dependencies:
+ glob "~7.0.6"
+ make-plural "~3.0.6"
+ messageformat-parser "^1.0.0"
+ nopt "~3.0.6"
+ reserved-words "^0.1.1"
+
+methods@~1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
+
+micromatch@^2.1.5, micromatch@^2.3.7:
+ version "2.3.11"
+ resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-2.3.11.tgz#86677c97d1720b363431d04d0d15293bd38c1565"
+ dependencies:
+ arr-diff "^2.0.0"
+ array-unique "^0.2.1"
+ braces "^1.8.2"
+ expand-brackets "^0.1.4"
+ extglob "^0.3.1"
+ filename-regex "^2.0.0"
+ is-extglob "^1.0.0"
+ is-glob "^2.0.1"
+ kind-of "^3.0.2"
+ normalize-path "^2.0.1"
+ object.omit "^2.0.0"
+ parse-glob "^3.0.4"
+ regex-cache "^0.4.2"
+
+"mime-db@>= 1.27.0 < 2":
+ version "1.29.0"
+ resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.29.0.tgz#48d26d235589651704ac5916ca06001914266878"
+
+mime-db@~1.27.0:
+ version "1.27.0"
+ resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.27.0.tgz#820f572296bbd20ec25ed55e5b5de869e5436eb1"
+
+mime-types@^2.1.12, mime-types@~2.1.11, mime-types@~2.1.15, mime-types@~2.1.7:
+ version "2.1.15"
+ resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.15.tgz#a4ebf5064094569237b8cf70046776d09fc92aed"
+ dependencies:
+ mime-db "~1.27.0"
+
+mime@1.3.4:
+ version "1.3.4"
+ resolved "https://registry.yarnpkg.com/mime/-/mime-1.3.4.tgz#115f9e3b6b3daf2959983cb38f149a2d40eb5d53"
+
+mime@^1.2.11, mime@^1.3.4:
+ version "1.3.6"
+ resolved "https://registry.yarnpkg.com/mime/-/mime-1.3.6.tgz#591d84d3653a6b0b4a3b9df8de5aa8108e72e5e0"
+
+mimic-fn@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.1.0.tgz#e667783d92e89dbd342818b5230b9d62a672ad18"
+
+min-document@^2.19.0:
+ version "2.19.0"
+ resolved "https://registry.yarnpkg.com/min-document/-/min-document-2.19.0.tgz#7bd282e3f5842ed295bb748cdd9f1ffa2c824685"
+ dependencies:
+ dom-walk "^0.1.0"
+
+"minimatch@2 || 3", minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.3, minimatch@^3.0.4, minimatch@~3.0.2:
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
+ dependencies:
+ brace-expansion "^1.1.7"
+
+minimatch@^2.0.3:
+ version "2.0.10"
+ resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-2.0.10.tgz#8d087c39c6b38c001b97fca7ce6d0e1e80afbac7"
+ dependencies:
+ brace-expansion "^1.0.0"
+
+minimist@0.0.8, minimist@~0.0.1:
+ version "0.0.8"
+ resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
+
+minimist@^1.1.0, minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
+
+mkdirp@0.5.0:
+ version "0.5.0"
+ resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.0.tgz#1d73076a6df986cd9344e15e71fcc05a4c9abf12"
+ dependencies:
+ minimist "0.0.8"
+
+mkdirp@0.5.1, mkdirp@0.5.x, "mkdirp@>=0.5 0", 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:
+ minimist "0.0.8"
+
+mkdirp@^0.3.5:
+ version "0.3.5"
+ resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.3.5.tgz#de3e5f8961c88c787ee1368df849ac4413eca8d7"
+
+mktemp@~0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/mktemp/-/mktemp-0.4.0.tgz#6d0515611c8a8c84e484aa2000129b98e981ff0b"
+
+moment-timezone@^0.5.0:
+ version "0.5.13"
+ resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.13.tgz#99ce5c7d827262eb0f1f702044177f60745d7b90"
+ dependencies:
+ moment ">= 2.9.0"
+
+"moment@>= 2.9.0", moment@^2.13.0:
+ version "2.18.1"
+ resolved "https://registry.yarnpkg.com/moment/-/moment-2.18.1.tgz#c36193dd3ce1c2eed2adb7c802dbbc77a81b1c0f"
+
+moo-server@*, moo-server@1.3.x:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/moo-server/-/moo-server-1.3.0.tgz#5dc79569565a10d6efed5439491e69d2392e58f1"
+
+morgan@^1.8.1:
+ version "1.8.2"
+ resolved "https://registry.yarnpkg.com/morgan/-/morgan-1.8.2.tgz#784ac7734e4a453a9c6e6e8680a9329275c8b687"
+ dependencies:
+ basic-auth "~1.1.0"
+ debug "2.6.8"
+ depd "~1.1.0"
+ on-finished "~2.3.0"
+ on-headers "~1.0.1"
+
+mout@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/mout/-/mout-1.0.0.tgz#9bdf1d4af57d66d47cb353a6335a3281098e1501"
+
+ms@0.7.1:
+ version "0.7.1"
+ resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.1.tgz#9cd13c03adbff25b65effde7ce864ee952017098"
+
+ms@0.7.2:
+ version "0.7.2"
+ resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.2.tgz#ae25cf2512b3885a1d95d7f037868d8431124765"
+
+ms@2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
+
+mustache@^2.2.1:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/mustache/-/mustache-2.3.0.tgz#4028f7778b17708a489930a6e52ac3bca0da41d0"
+
+mute-stream@0.0.5:
+ version "0.0.5"
+ resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.5.tgz#8fbfabb0a98a253d3184331f9e8deb7372fac6c0"
+
+mute-stream@0.0.6:
+ version "0.0.6"
+ resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.6.tgz#48962b19e169fd1dfc240b3f1e7317627bbc47db"
+
+mute-stream@0.0.7:
+ version "0.0.7"
+ resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab"
+
+nan@^2.3.0, nan@^2.3.2:
+ version "2.6.2"
+ resolved "https://registry.yarnpkg.com/nan/-/nan-2.6.2.tgz#e4ff34e6c95fdfb5aecc08de6596f43605a7db45"
+
+native-promise-only@^0.8.1:
+ version "0.8.1"
+ resolved "https://registry.yarnpkg.com/native-promise-only/-/native-promise-only-0.8.1.tgz#20a318c30cb45f71fe7adfbf7b21c99c1472ef11"
+
+natural-compare@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
+
+negotiator@0.6.1:
+ version "0.6.1"
+ resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9"
+
+nise@^1.0.1:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/nise/-/nise-1.2.0.tgz#079d6cadbbcb12ba30e38f1c999f36ad4d6baa53"
+ dependencies:
+ formatio "^1.2.0"
+ just-extend "^1.1.26"
+ lolex "^1.6.0"
+ path-to-regexp "^1.7.0"
+ text-encoding "^0.6.4"
+
+node-fetch@^1.3.3:
+ version "1.7.1"
+ resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.1.tgz#899cb3d0a3c92f952c47f1b876f4c8aeabd400d5"
+ dependencies:
+ encoding "^0.1.11"
+ is-stream "^1.0.1"
+
+node-fetch@^2.0.0-alpha.9:
+ version "2.0.0-alpha.9"
+ resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.0.0-alpha.9.tgz#990c0634f510f76449a0d6f6eaec96b22f273628"
+
+node-gyp@^3.3.1:
+ version "3.6.2"
+ resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-3.6.2.tgz#9bfbe54562286284838e750eac05295853fa1c60"
+ dependencies:
+ fstream "^1.0.0"
+ glob "^7.0.3"
+ graceful-fs "^4.1.2"
+ minimatch "^3.0.2"
+ mkdirp "^0.5.0"
+ nopt "2 || 3"
+ npmlog "0 || 1 || 2 || 3 || 4"
+ osenv "0"
+ request "2"
+ rimraf "2"
+ semver "~5.3.0"
+ tar "^2.0.0"
+ which "1"
+
+node-int64@^0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b"
+
+node-modules-path@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/node-modules-path/-/node-modules-path-1.0.1.tgz#40096b08ce7ad0ea14680863af449c7c75a5d1c8"
+
+node-notifier@^5.0.1:
+ version "5.1.2"
+ resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-5.1.2.tgz#2fa9e12605fa10009d44549d6fcd8a63dde0e4ff"
+ dependencies:
+ growly "^1.3.0"
+ semver "^5.3.0"
+ shellwords "^0.1.0"
+ which "^1.2.12"
+
+node-pre-gyp@^0.6.36:
+ version "0.6.36"
+ resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.36.tgz#db604112cb74e0d477554e9b505b17abddfab786"
+ dependencies:
+ mkdirp "^0.5.1"
+ nopt "^4.0.1"
+ npmlog "^4.0.2"
+ rc "^1.1.7"
+ request "^2.81.0"
+ rimraf "^2.6.1"
+ semver "^5.3.0"
+ tar "^2.2.1"
+ tar-pack "^3.4.0"
+
+node-rest-client@^1.5.1:
+ version "1.8.0"
+ resolved "https://registry.yarnpkg.com/node-rest-client/-/node-rest-client-1.8.0.tgz#8d3c566b817e27394cb7273783a41caefe3e5955"
+ dependencies:
+ debug "~2.2.0"
+ xml2js ">=0.2.4"
+
+node-sass@^4.1.0:
+ version "4.5.3"
+ resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.5.3.tgz#d09c9d1179641239d1b97ffc6231fdcec53e1568"
+ dependencies:
+ async-foreach "^0.1.3"
+ chalk "^1.1.1"
+ cross-spawn "^3.0.0"
+ gaze "^1.0.0"
+ get-stdin "^4.0.1"
+ glob "^7.0.3"
+ in-publish "^2.0.0"
+ lodash.assign "^4.2.0"
+ lodash.clonedeep "^4.3.2"
+ lodash.mergewith "^4.6.0"
+ meow "^3.7.0"
+ mkdirp "^0.5.1"
+ nan "^2.3.2"
+ node-gyp "^3.3.1"
+ npmlog "^4.0.0"
+ request "^2.79.0"
+ sass-graph "^2.1.1"
+ stdout-stream "^1.4.0"
+
+"nopt@2 || 3", nopt@^3.0.6, nopt@~3.0.6:
+ version "3.0.6"
+ resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9"
+ dependencies:
+ abbrev "1"
+
+nopt@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d"
+ dependencies:
+ abbrev "1"
+ osenv "^0.1.4"
+
+normalize-package-data@^2.3.2, normalize-package-data@^2.3.4:
+ version "2.4.0"
+ resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.4.0.tgz#12f95a307d58352075a04907b84ac8be98ac012f"
+ dependencies:
+ hosted-git-info "^2.1.4"
+ is-builtin-module "^1.0.0"
+ semver "2 || 3 || 4 || 5"
+ validate-npm-package-license "^3.0.1"
+
+normalize-path@^2.0.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9"
+ dependencies:
+ remove-trailing-separator "^1.0.1"
+
+normalize.css@4.1.1:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/normalize.css/-/normalize.css-4.1.1.tgz#4f0b1d5a235383252b04d8566b866cc5fcad9f0c"
+
+npm-git-info@^1.0.0, npm-git-info@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/npm-git-info/-/npm-git-info-1.0.3.tgz#a933c42ec321e80d3646e0d6e844afe94630e1d5"
+
+npm-package-arg@^4.1.1:
+ version "4.2.1"
+ resolved "https://registry.yarnpkg.com/npm-package-arg/-/npm-package-arg-4.2.1.tgz#593303fdea85f7c422775f17f9eb7670f680e3ec"
+ dependencies:
+ hosted-git-info "^2.1.5"
+ semver "^5.1.0"
+
+npm-run-path@^2.0.0:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f"
+ dependencies:
+ path-key "^2.0.0"
+
+"npmlog@0 || 1 || 2 || 3 || 4", npmlog@^4.0.0, npmlog@^4.0.2:
+ version "4.1.2"
+ resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b"
+ dependencies:
+ are-we-there-yet "~1.1.2"
+ console-control-strings "~1.1.0"
+ gauge "~2.7.3"
+ set-blocking "~2.0.0"
+
+nth-check@~1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.1.tgz#9929acdf628fc2c41098deab82ac580cf149aae4"
+ dependencies:
+ boolbase "~1.0.0"
+
+number-is-nan@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d"
+
+oauth-sign@~0.8.1:
+ version "0.8.2"
+ resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43"
+
+object-assign@4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.0.tgz#7a3b3d0e98063d43f4c03f2e8ae6cd51a86883a0"
+
+object-assign@4.1.1, object-assign@^4.0.1, object-assign@^4.1.0:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
+
+object-assign@^2.0.0:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-2.1.1.tgz#43c36e5d569ff8e4816c4efa8be02d26967c18aa"
+
+object-component@0.0.3:
+ version "0.0.3"
+ resolved "https://registry.yarnpkg.com/object-component/-/object-component-0.0.3.tgz#f0c69aa50efc95b866c186f400a33769cb2f1291"
+
+object.omit@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/object.omit/-/object.omit-2.0.1.tgz#1a9c744829f39dbb858c76ca3579ae2a54ebd1fa"
+ dependencies:
+ for-own "^0.1.4"
+ is-extendable "^0.1.1"
+
+on-finished@~2.3.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947"
+ dependencies:
+ ee-first "1.1.1"
+
+on-headers@~1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.1.tgz#928f5d0f470d49342651ea6794b0857c100693f7"
+
+once@^1.3.0, once@^1.3.3:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
+ dependencies:
+ wrappy "1"
+
+onetime@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/onetime/-/onetime-1.1.0.tgz#a1f7838f8314c516f05ecefcbc4ccfe04b4ed789"
+
+onetime@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/onetime/-/onetime-2.0.1.tgz#067428230fd67443b2794b22bba528b6867962d4"
+ dependencies:
+ mimic-fn "^1.0.0"
+
+optimist@^0.6.1:
+ version "0.6.1"
+ resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686"
+ dependencies:
+ minimist "~0.0.1"
+ wordwrap "~0.0.2"
+
+optionator@^0.8.2:
+ version "0.8.2"
+ resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.2.tgz#364c5e409d3f4d6301d6c0b4c05bba50180aeb64"
+ dependencies:
+ deep-is "~0.1.3"
+ fast-levenshtein "~2.0.4"
+ levn "~0.3.0"
+ prelude-ls "~1.1.2"
+ type-check "~0.3.2"
+ wordwrap "~1.0.0"
+
+options@>=0.0.5:
+ version "0.0.6"
+ resolved "https://registry.yarnpkg.com/options/-/options-0.0.6.tgz#ec22d312806bb53e731773e7cdaefcf1c643128f"
+
+ora@^0.2.0:
+ version "0.2.3"
+ resolved "https://registry.yarnpkg.com/ora/-/ora-0.2.3.tgz#37527d220adcd53c39b73571d754156d5db657a4"
+ dependencies:
+ chalk "^1.1.1"
+ cli-cursor "^1.0.2"
+ cli-spinners "^0.1.2"
+ object-assign "^4.0.1"
+
+os-homedir@^1.0.0, os-homedir@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3"
+
+os-locale@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-1.4.0.tgz#20f9f17ae29ed345e8bde583b13d2009803c14d9"
+ dependencies:
+ lcid "^1.0.0"
+
+os-shim@^0.1.2:
+ version "0.1.3"
+ resolved "https://registry.yarnpkg.com/os-shim/-/os-shim-0.1.3.tgz#6b62c3791cf7909ea35ed46e17658bb417cb3917"
+
+os-tmpdir@^1.0.0, os-tmpdir@^1.0.1, os-tmpdir@~1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
+
+osenv@0, osenv@^0.1.3, osenv@^0.1.4:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.4.tgz#42fe6d5953df06c8064be6f176c3d05aaaa34644"
+ dependencies:
+ os-homedir "^1.0.0"
+ os-tmpdir "^1.0.0"
+
+output-file-sync@^1.1.0:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/output-file-sync/-/output-file-sync-1.1.2.tgz#d0a33eefe61a205facb90092e826598d5245ce76"
+ dependencies:
+ graceful-fs "^4.1.4"
+ mkdirp "^0.5.1"
+ object-assign "^4.1.0"
+
+p-finally@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae"
+
+p-limit@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.1.0.tgz#b07ff2d9a5d88bec806035895a2bab66a27988bc"
+
+p-locate@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43"
+ dependencies:
+ p-limit "^1.1.0"
+
+parse-bmfont-ascii@^1.0.3:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/parse-bmfont-ascii/-/parse-bmfont-ascii-1.0.6.tgz#11ac3c3ff58f7c2020ab22769079108d4dfa0285"
+
+parse-bmfont-binary@^1.0.5:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/parse-bmfont-binary/-/parse-bmfont-binary-1.0.6.tgz#d038b476d3e9dd9db1e11a0b0e53a22792b69006"
+
+parse-bmfont-xml@^1.1.0:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/parse-bmfont-xml/-/parse-bmfont-xml-1.1.3.tgz#d6b66a371afd39c5007d9f0eeb262a4f2cce7b7c"
+ dependencies:
+ xml-parse-from-string "^1.0.0"
+ xml2js "^0.4.5"
+
+parse-glob@^3.0.4:
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/parse-glob/-/parse-glob-3.0.4.tgz#b2c376cfb11f35513badd173ef0bb6e3a388391c"
+ dependencies:
+ glob-base "^0.3.0"
+ is-dotfile "^1.0.0"
+ is-extglob "^1.0.0"
+ is-glob "^2.0.0"
+
+parse-headers@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/parse-headers/-/parse-headers-2.0.1.tgz#6ae83a7aa25a9d9b700acc28698cd1f1ed7e9536"
+ dependencies:
+ for-each "^0.3.2"
+ trim "0.0.1"
+
+parse-json@^2.2.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9"
+ dependencies:
+ error-ex "^1.2.0"
+
+parse-passwd@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6"
+
+parse-png@^1.0.0, parse-png@^1.1.1:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/parse-png/-/parse-png-1.1.2.tgz#f5c2ad7c7993490986020a284c19aee459711ff2"
+ dependencies:
+ pngjs "^3.2.0"
+
+parsejson@0.0.3:
+ version "0.0.3"
+ resolved "https://registry.yarnpkg.com/parsejson/-/parsejson-0.0.3.tgz#ab7e3759f209ece99437973f7d0f1f64ae0e64ab"
+ dependencies:
+ better-assert "~1.0.0"
+
+parseqs@0.0.5:
+ version "0.0.5"
+ resolved "https://registry.yarnpkg.com/parseqs/-/parseqs-0.0.5.tgz#d5208a3738e46766e291ba2ea173684921a8b89d"
+ dependencies:
+ better-assert "~1.0.0"
+
+parseuri@0.0.5:
+ version "0.0.5"
+ resolved "https://registry.yarnpkg.com/parseuri/-/parseuri-0.0.5.tgz#80204a50d4dbb779bfdc6ebe2778d90e4bce320a"
+ dependencies:
+ better-assert "~1.0.0"
+
+parseurl@~1.3.1:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.1.tgz#c8ab8c9223ba34888aa64a297b28853bec18da56"
+
+path-exists@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-1.0.0.tgz#d5a8998eb71ef37a74c34eb0d9eba6e878eea081"
+
+path-exists@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b"
+ dependencies:
+ pinkie-promise "^2.0.0"
+
+path-exists@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515"
+
+path-is-absolute@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
+
+path-is-inside@^1.0.1, path-is-inside@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53"
+
+path-key@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40"
+
+path-parse@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.5.tgz#3c1adf871ea9cd6c9431b6ea2bd74a0ff055c4c1"
+
+path-posix@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/path-posix/-/path-posix-1.0.0.tgz#06b26113f56beab042545a23bfa88003ccac260f"
+
+path-to-regexp@0.1.7:
+ version "0.1.7"
+ resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
+
+path-to-regexp@^1.7.0:
+ version "1.7.0"
+ resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.7.0.tgz#59fde0f435badacba103a84e9d3bc64e96b9937d"
+ dependencies:
+ isarray "0.0.1"
+
+path-type@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441"
+ dependencies:
+ graceful-fs "^4.1.2"
+ pify "^2.0.0"
+ pinkie-promise "^2.0.0"
+
+pend@~1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50"
+
+performance-now@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5"
+
+phantomjs-prebuilt@^2.1.10:
+ version "2.1.14"
+ resolved "https://registry.yarnpkg.com/phantomjs-prebuilt/-/phantomjs-prebuilt-2.1.14.tgz#d53d311fcfb7d1d08ddb24014558f1188c516da0"
+ dependencies:
+ es6-promise "~4.0.3"
+ extract-zip "~1.5.0"
+ fs-extra "~1.0.0"
+ hasha "~2.2.0"
+ kew "~0.7.0"
+ progress "~1.1.8"
+ request "~2.79.0"
+ request-progress "~2.0.1"
+ which "~1.2.10"
+
+pify@^2.0.0, pify@^2.3.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
+
+pinkie-promise@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa"
+ dependencies:
+ pinkie "^2.0.0"
+
+pinkie@^2.0.0:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870"
+
+pixelmatch@^4.0.0:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/pixelmatch/-/pixelmatch-4.0.2.tgz#8f47dcec5011b477b67db03c243bc1f3085e8854"
+ dependencies:
+ pngjs "^3.0.0"
+
+pluralize@^1.2.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-1.2.1.tgz#d1a21483fd22bb41e58a12fa3421823140897c45"
+
+pluralize@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-4.0.0.tgz#59b708c1c0190a2f692f1c7618c446b052fd1762"
+
+pn@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/pn/-/pn-1.0.0.tgz#1cf5a30b0d806cd18f88fc41a6b5d4ad615b3ba9"
+
+pngjs@^3.0.0, pngjs@^3.2.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-3.2.0.tgz#fc9fcea1a8a375da54a51148019d5abd41dbabde"
+
+portfinder@^1.0.7:
+ version "1.0.13"
+ resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.13.tgz#bb32ecd87c27104ae6ee44b5a3ccbf0ebb1aede9"
+ dependencies:
+ async "^1.5.2"
+ debug "^2.2.0"
+ mkdirp "0.5.x"
+
+prelude-ls@~1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
+
+preserve@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b"
+
+pretender@^1.1.0:
+ version "1.5.1"
+ resolved "https://registry.yarnpkg.com/pretender/-/pretender-1.5.1.tgz#bd9098c03d39c3bc7dcb84a28ee27e096e2e32b8"
+ dependencies:
+ fake-xml-http-request "^1.6.0"
+ route-recognizer "^0.3.3"
+
+pretender@^1.6.1:
+ version "1.6.1"
+ resolved "https://registry.yarnpkg.com/pretender/-/pretender-1.6.1.tgz#77d1e42ac8c6b298f5cd43534a87645df035db8c"
+ dependencies:
+ fake-xml-http-request "^1.6.0"
+ route-recognizer "^0.3.3"
+
+prettier-eslint-cli@^4.2.1:
+ version "4.2.1"
+ resolved "https://registry.yarnpkg.com/prettier-eslint-cli/-/prettier-eslint-cli-4.2.1.tgz#7a36dd4c8e2243f077f30c266ea6c90c616a09e8"
+ dependencies:
+ arrify "^1.0.1"
+ babel-runtime "^6.23.0"
+ boolify "^1.0.0"
+ camelcase-keys "^4.1.0"
+ chalk "^1.1.3"
+ common-tags "^1.4.0"
+ eslint "^3.19.0"
+ find-up "^2.1.0"
+ get-stdin "^5.0.1"
+ glob "^7.1.1"
+ ignore "^3.2.7"
+ indent-string "^3.1.0"
+ lodash.memoize "^4.1.2"
+ loglevel-colored-level-prefix "^1.0.0"
+ messageformat "^1.0.2"
+ prettier-eslint "^6.0.0"
+ rxjs "^5.3.0"
+ yargs "^7.1.0"
+
+prettier-eslint@^6.0.0:
+ version "6.4.2"
+ resolved "https://registry.yarnpkg.com/prettier-eslint/-/prettier-eslint-6.4.2.tgz#9bafd9549e0827396c75848e8dbeb65525b9096e"
+ dependencies:
+ common-tags "^1.4.0"
+ dlv "^1.1.0"
+ eslint "^3.19.0"
+ indent-string "^3.1.0"
+ lodash.merge "^4.6.0"
+ loglevel-colored-level-prefix "^1.0.0"
+ prettier "^1.5.0"
+ pretty-format "^20.0.3"
+ require-relative "^0.8.7"
+
+prettier@^1.5.0, prettier@^1.5.3:
+ version "1.5.3"
+ resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.5.3.tgz#59dadc683345ec6b88f88b94ed4ae7e1da394bfe"
+
+pretty-format@^20.0.3:
+ version "20.0.3"
+ resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-20.0.3.tgz#020e350a560a1fe1a98dc3beb6ccffb386de8b14"
+ dependencies:
+ ansi-regex "^2.1.1"
+ ansi-styles "^3.0.0"
+
+printf@^0.2.3:
+ version "0.2.5"
+ resolved "https://registry.yarnpkg.com/printf/-/printf-0.2.5.tgz#c438ca2ca33e3927671db4ab69c0e52f936a4f0f"
+
+private@^0.1.6, private@~0.1.5:
+ version "0.1.7"
+ resolved "https://registry.yarnpkg.com/private/-/private-0.1.7.tgz#68ce5e8a1ef0a23bb570cc28537b5332aba63ef1"
+
+process-nextick-args@~1.0.6:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3"
+
+process-relative-require@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/process-relative-require/-/process-relative-require-1.0.0.tgz#1590dfcf5b8f2983ba53e398446b68240b4cc68a"
+ dependencies:
+ node-modules-path "^1.0.0"
+
+process@~0.5.1:
+ version "0.5.2"
+ resolved "https://registry.yarnpkg.com/process/-/process-0.5.2.tgz#1638d8a8e34c2f440a91db95ab9aeb677fc185cf"
+
+progress@^1.1.8, progress@~1.1.8:
+ version "1.1.8"
+ resolved "https://registry.yarnpkg.com/progress/-/progress-1.1.8.tgz#e260c78f6161cdd9b0e56cc3e0a85de17c7a57be"
+
+progress@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.0.tgz#8a1be366bf8fc23db2bd23f10c6fe920b4389d1f"
+
+promise-map-series@^0.2.1:
+ version "0.2.3"
+ resolved "https://registry.yarnpkg.com/promise-map-series/-/promise-map-series-0.2.3.tgz#c2d377afc93253f6bd03dbb77755eb88ab20a847"
+ dependencies:
+ rsvp "^3.0.14"
+
+promised-io@*:
+ version "0.3.5"
+ resolved "https://registry.yarnpkg.com/promised-io/-/promised-io-0.3.5.tgz#4ad217bb3658bcaae9946b17a8668ecd851e1356"
+
+proxy-addr@~1.1.4:
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-1.1.4.tgz#27e545f6960a44a627d9b44467e35c1b6b4ce2f3"
+ dependencies:
+ forwarded "~0.1.0"
+ ipaddr.js "1.3.0"
+
+pseudomap@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3"
+
+punycode@^1.4.1:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
+
+q@^1.1.2:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/q/-/q-1.5.0.tgz#dd01bac9d06d30e6f219aecb8253ee9ebdc308f1"
+
+qs@6.4.0, qs@^6.4.0, qs@~6.4.0:
+ version "6.4.0"
+ resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233"
+
+qs@~6.3.0:
+ version "6.3.2"
+ resolved "https://registry.yarnpkg.com/qs/-/qs-6.3.2.tgz#e75bd5f6e268122a2a0e0bda630b2550c166502c"
+
+quick-lru@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-1.1.0.tgz#4360b17c61136ad38078397ff11416e186dcfbb8"
+
+quick-temp@^0.1.0, quick-temp@^0.1.2, quick-temp@^0.1.3, quick-temp@^0.1.5, quick-temp@^0.1.8:
+ version "0.1.8"
+ resolved "https://registry.yarnpkg.com/quick-temp/-/quick-temp-0.1.8.tgz#bab02a242ab8fb0dd758a3c9776b32f9a5d94408"
+ dependencies:
+ mktemp "~0.4.0"
+ rimraf "^2.5.4"
+ underscore.string "~3.3.4"
+
+qunit-notifications@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/qunit-notifications/-/qunit-notifications-0.1.1.tgz#3001afc6a6a77dfbd962ccbcddde12dec5286c09"
+
+qunitjs@^2.0.1:
+ version "2.4.0"
+ resolved "https://registry.yarnpkg.com/qunitjs/-/qunitjs-2.4.0.tgz#58f3a81e846687f2e7f637c5bedc9c267f887261"
+ dependencies:
+ chokidar "1.6.1"
+ commander "2.9.0"
+ exists-stat "1.0.0"
+ findup-sync "0.4.3"
+ js-reporters "1.2.0"
+ resolve "1.3.2"
+ walk-sync "0.3.1"
+
+randomatic@^1.1.3:
+ version "1.1.7"
+ resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-1.1.7.tgz#c7abe9cc8b87c0baa876b19fde83fd464797e38c"
+ dependencies:
+ is-number "^3.0.0"
+ kind-of "^4.0.0"
+
+range-parser@~1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e"
+
+raw-body@~1.1.0:
+ version "1.1.7"
+ resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-1.1.7.tgz#1d027c2bfa116acc6623bca8f00016572a87d425"
+ dependencies:
+ bytes "1"
+ string_decoder "0.10"
+
+rc@^1.1.7:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.1.tgz#2e03e8e42ee450b8cb3dce65be1bf8974e1dfd95"
+ dependencies:
+ deep-extend "~0.4.0"
+ ini "~1.3.0"
+ minimist "^1.2.0"
+ strip-json-comments "~2.0.1"
+
+read-chunk@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/read-chunk/-/read-chunk-1.0.1.tgz#5f68cab307e663f19993527d9b589cace4661194"
+
+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"
+ dependencies:
+ find-up "^1.0.0"
+ read-pkg "^1.0.0"
+
+read-pkg@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28"
+ dependencies:
+ load-json-file "^1.0.0"
+ normalize-package-data "^2.3.2"
+ path-type "^1.0.0"
+
+readable-stream@1.1:
+ version "1.1.13"
+ resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.13.tgz#f6eef764f514c89e2b9e23146a75ba106756d23e"
+ dependencies:
+ core-util-is "~1.0.0"
+ inherits "~2.0.1"
+ isarray "0.0.1"
+ string_decoder "~0.10.x"
+
+readable-stream@^2, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.1.5, readable-stream@^2.2.2:
+ version "2.3.3"
+ resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.3.tgz#368f2512d79f9d46fdfc71349ae7878bbc1eb95c"
+ dependencies:
+ core-util-is "~1.0.0"
+ inherits "~2.0.3"
+ isarray "~1.0.0"
+ process-nextick-args "~1.0.6"
+ safe-buffer "~5.1.1"
+ string_decoder "~1.0.3"
+ util-deprecate "~1.0.1"
+
+readable-stream@~1.0.2:
+ version "1.0.34"
+ resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c"
+ dependencies:
+ core-util-is "~1.0.0"
+ inherits "~2.0.1"
+ isarray "0.0.1"
+ string_decoder "~0.10.x"
+
+readable-stream@~2.0.0:
+ version "2.0.6"
+ resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.0.6.tgz#8f90341e68a53ccc928788dacfcd11b36eb9b78e"
+ dependencies:
+ core-util-is "~1.0.0"
+ inherits "~2.0.1"
+ isarray "~1.0.0"
+ process-nextick-args "~1.0.6"
+ string_decoder "~0.10.x"
+ util-deprecate "~1.0.1"
+
+readdirp@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.1.0.tgz#4ed0ad060df3073300c48440373f72d1cc642d78"
+ dependencies:
+ graceful-fs "^4.1.2"
+ minimatch "^3.0.2"
+ readable-stream "^2.0.2"
+ set-immediate-shim "^1.0.1"
+
+readline2@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/readline2/-/readline2-1.0.1.tgz#41059608ffc154757b715d9989d199ffbf372e35"
+ dependencies:
+ code-point-at "^1.0.0"
+ is-fullwidth-code-point "^1.0.0"
+ mute-stream "0.0.5"
+
+recast@0.10.33, recast@^0.10.10:
+ version "0.10.33"
+ resolved "https://registry.yarnpkg.com/recast/-/recast-0.10.33.tgz#942808f7aa016f1fa7142c461d7e5704aaa8d697"
+ dependencies:
+ ast-types "0.8.12"
+ esprima-fb "~15001.1001.0-dev-harmony-fb"
+ private "~0.1.5"
+ source-map "~0.5.0"
+
+recast@^0.11.17, recast@^0.11.3:
+ version "0.11.23"
+ resolved "https://registry.yarnpkg.com/recast/-/recast-0.11.23.tgz#451fd3004ab1e4df9b4e4b66376b2a21912462d3"
+ dependencies:
+ ast-types "0.9.6"
+ esprima "~3.1.0"
+ private "~0.1.5"
+ source-map "~0.5.0"
+
+rechoir@^0.6.2:
+ version "0.6.2"
+ resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384"
+ dependencies:
+ resolve "^1.1.6"
+
+redent@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde"
+ dependencies:
+ indent-string "^2.1.0"
+ strip-indent "^1.0.1"
+
+redeyed@~1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/redeyed/-/redeyed-1.0.1.tgz#e96c193b40c0816b00aec842698e61185e55498a"
+ dependencies:
+ esprima "~3.0.0"
+
+regenerate@^1.2.1:
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.3.2.tgz#d1941c67bad437e1be76433add5b385f95b19260"
+
+regenerator-runtime@^0.10.0:
+ version "0.10.5"
+ resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz#336c3efc1220adcedda2c9fab67b5a7955a33658"
+
+regenerator-runtime@^0.9.5:
+ version "0.9.6"
+ resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.9.6.tgz#d33eb95d0d2001a4be39659707c51b0cb71ce029"
+
+regenerator-transform@0.9.11:
+ version "0.9.11"
+ resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.9.11.tgz#3a7d067520cb7b7176769eb5ff868691befe1283"
+ dependencies:
+ babel-runtime "^6.18.0"
+ babel-types "^6.19.0"
+ private "^0.1.6"
+
+regenerator@0.8.40:
+ version "0.8.40"
+ resolved "https://registry.yarnpkg.com/regenerator/-/regenerator-0.8.40.tgz#a0e457c58ebdbae575c9f8cd75127e93756435d8"
+ dependencies:
+ commoner "~0.10.3"
+ defs "~1.1.0"
+ esprima-fb "~15001.1001.0-dev-harmony-fb"
+ private "~0.1.5"
+ recast "0.10.33"
+ through "~2.3.8"
+
+regex-cache@^0.4.2:
+ version "0.4.3"
+ resolved "https://registry.yarnpkg.com/regex-cache/-/regex-cache-0.4.3.tgz#9b1a6c35d4d0dfcef5711ae651e8e9d3d7114145"
+ dependencies:
+ is-equal-shallow "^0.1.3"
+ is-primitive "^2.0.0"
+
+regexpu-core@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-2.0.0.tgz#49d038837b8dcf8bfa5b9a42139938e6ea2ae240"
+ dependencies:
+ regenerate "^1.2.1"
+ regjsgen "^0.2.0"
+ regjsparser "^0.1.4"
+
+regexpu@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/regexpu/-/regexpu-1.3.0.tgz#e534dc991a9e5846050c98de6d7dd4a55c9ea16d"
+ dependencies:
+ esprima "^2.6.0"
+ recast "^0.10.10"
+ regenerate "^1.2.1"
+ regjsgen "^0.2.0"
+ regjsparser "^0.1.4"
+
+regjsgen@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.2.0.tgz#6c016adeac554f75823fe37ac05b92d5a4edb1f7"
+
+regjsparser@^0.1.4:
+ version "0.1.5"
+ resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.1.5.tgz#7ee8f84dc6fa792d3fd0ae228d24bd949ead205c"
+ dependencies:
+ jsesc "~0.5.0"
+
+remove-trailing-separator@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.0.2.tgz#69b062d978727ad14dc6b56ba4ab772fd8d70511"
+
+repeat-element@^1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.2.tgz#ef089a178d1483baae4d93eb98b4f9e4e11d990a"
+
+repeat-string@^1.5.2:
+ version "1.6.1"
+ resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637"
+
+repeating@^1.1.0, repeating@^1.1.2:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/repeating/-/repeating-1.1.3.tgz#3d4114218877537494f97f77f9785fab810fa4ac"
+ dependencies:
+ is-finite "^1.0.0"
+
+repeating@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda"
+ dependencies:
+ is-finite "^1.0.0"
+
+replace-ext@0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-0.0.1.tgz#29bbd92078a739f0bcce2b4ee41e837953522924"
+
+request-progress@~2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/request-progress/-/request-progress-2.0.1.tgz#5d36bb57961c673aa5b788dbc8141fdf23b44e08"
+ dependencies:
+ throttleit "^1.0.0"
+
+request@2, request@^2.65.0, request@^2.79.0, request@^2.81.0:
+ version "2.81.0"
+ resolved "https://registry.yarnpkg.com/request/-/request-2.81.0.tgz#c6928946a0e06c5f8d6f8a9333469ffda46298a0"
+ dependencies:
+ aws-sign2 "~0.6.0"
+ aws4 "^1.2.1"
+ caseless "~0.12.0"
+ combined-stream "~1.0.5"
+ extend "~3.0.0"
+ forever-agent "~0.6.1"
+ form-data "~2.1.1"
+ har-validator "~4.2.1"
+ hawk "~3.1.3"
+ http-signature "~1.1.0"
+ is-typedarray "~1.0.0"
+ isstream "~0.1.2"
+ json-stringify-safe "~5.0.1"
+ mime-types "~2.1.7"
+ oauth-sign "~0.8.1"
+ performance-now "^0.2.0"
+ qs "~6.4.0"
+ safe-buffer "^5.0.1"
+ stringstream "~0.0.4"
+ tough-cookie "~2.3.0"
+ tunnel-agent "^0.6.0"
+ uuid "^3.0.0"
+
+request@~2.79.0:
+ version "2.79.0"
+ resolved "https://registry.yarnpkg.com/request/-/request-2.79.0.tgz#4dfe5bf6be8b8cdc37fcf93e04b65577722710de"
+ dependencies:
+ aws-sign2 "~0.6.0"
+ aws4 "^1.2.1"
+ caseless "~0.11.0"
+ combined-stream "~1.0.5"
+ extend "~3.0.0"
+ forever-agent "~0.6.1"
+ form-data "~2.1.1"
+ har-validator "~2.0.6"
+ hawk "~3.1.3"
+ http-signature "~1.1.0"
+ is-typedarray "~1.0.0"
+ isstream "~0.1.2"
+ json-stringify-safe "~5.0.1"
+ mime-types "~2.1.7"
+ oauth-sign "~0.8.1"
+ qs "~6.3.0"
+ stringstream "~0.0.4"
+ tough-cookie "~2.3.0"
+ tunnel-agent "~0.4.1"
+ uuid "^3.0.0"
+
+require-directory@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
+
+require-main-filename@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1"
+
+require-relative@^0.8.7:
+ version "0.8.7"
+ resolved "https://registry.yarnpkg.com/require-relative/-/require-relative-0.8.7.tgz#7999539fc9e047a37928fa196f8e1563dabd36de"
+
+require-uncached@^1.0.2, require-uncached@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/require-uncached/-/require-uncached-1.0.3.tgz#4e0d56d6c9662fd31e43011c4b95aa49955421d3"
+ dependencies:
+ caller-path "^0.1.0"
+ resolve-from "^1.0.0"
+
+requires-port@1.x.x:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
+
+reserved-words@^0.1.1:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/reserved-words/-/reserved-words-0.1.2.tgz#00a0940f98cd501aeaaac316411d9adc52b31ab1"
+
+resize-img@^1.1.0:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/resize-img/-/resize-img-1.1.2.tgz#fad650faf3ef2c53ea63112bc272d95e9d92550e"
+ dependencies:
+ bmp-js "0.0.1"
+ file-type "^3.8.0"
+ get-stream "^2.0.0"
+ jimp "^0.2.21"
+ jpeg-js "^0.1.1"
+ parse-png "^1.1.1"
+
+resolve-dir@^0.1.0:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/resolve-dir/-/resolve-dir-0.1.1.tgz#b219259a5602fac5c5c496ad894a6e8cc430261e"
+ dependencies:
+ expand-tilde "^1.2.2"
+ global-modules "^0.2.3"
+
+resolve-from@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-1.0.1.tgz#26cbfe935d1aeeeabb29bc3fe5aeb01e93d44226"
+
+resolve@1.3.2:
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.3.2.tgz#1f0442c9e0cbb8136e87b9305f932f46c7f28235"
+ dependencies:
+ path-parse "^1.0.5"
+
+resolve@^1.1.2, resolve@^1.1.6, resolve@^1.1.7, resolve@^1.3.0, resolve@^1.3.3:
+ version "1.3.3"
+ resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.3.3.tgz#655907c3469a8680dc2de3a275a8fdd69691f0e5"
+ dependencies:
+ path-parse "^1.0.5"
+
+restore-cursor@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-1.0.1.tgz#34661f46886327fed2991479152252df92daa541"
+ dependencies:
+ exit-hook "^1.0.0"
+ onetime "^1.0.0"
+
+restore-cursor@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf"
+ dependencies:
+ onetime "^2.0.0"
+ signal-exit "^3.0.2"
+
+right-align@^0.1.1:
+ version "0.1.3"
+ resolved "https://registry.yarnpkg.com/right-align/-/right-align-0.1.3.tgz#61339b722fe6a3515689210d24e14c96148613ef"
+ dependencies:
+ align-text "^0.1.1"
+
+rimraf@2, rimraf@^2.2.8, rimraf@^2.3.2, rimraf@^2.3.4, rimraf@^2.4.3, rimraf@^2.4.4, rimraf@^2.5.1, rimraf@^2.5.3, rimraf@^2.5.4, rimraf@^2.6.1:
+ version "2.6.1"
+ resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.1.tgz#c2338ec643df7a1b7fe5c54fa86f57428a55f33d"
+ dependencies:
+ glob "^7.0.5"
+
+rimraf@~2.2.6:
+ version "2.2.8"
+ resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.2.8.tgz#e439be2aaee327321952730f99a8929e4fc50582"
+
+route-recognizer@^0.2.3:
+ version "0.2.10"
+ resolved "https://registry.yarnpkg.com/route-recognizer/-/route-recognizer-0.2.10.tgz#024b2283c2e68d13a7c7f5173a5924645e8902df"
+
+route-recognizer@^0.3.3:
+ version "0.3.3"
+ resolved "https://registry.yarnpkg.com/route-recognizer/-/route-recognizer-0.3.3.tgz#1d365e27fa6995e091675f7dc940a8c00353bd29"
+
+rsvp@^3.0.14, rsvp@^3.0.16, rsvp@^3.0.17, rsvp@^3.0.18, rsvp@^3.0.21, rsvp@^3.0.6, rsvp@^3.1.0, rsvp@^3.2.1, rsvp@^3.3.3, rsvp@^3.5.0:
+ version "3.6.1"
+ resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-3.6.1.tgz#34f4a7ac2859f7bacc8f49789c5604f1e26ae702"
+
+rsvp@^4.7.0:
+ version "4.7.0"
+ resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.7.0.tgz#dc1b0b1a536f7dec9d2be45e0a12ad4197c9fd96"
+
+rsvp@~3.0.6:
+ version "3.0.21"
+ resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-3.0.21.tgz#49c588fe18ef293bcd0ab9f4e6756e6ac433359f"
+
+rsvp@~3.2.1:
+ version "3.2.1"
+ resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-3.2.1.tgz#07cb4a5df25add9e826ebc67dcc9fd89db27d84a"
+
+run-async@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/run-async/-/run-async-0.1.0.tgz#c8ad4a5e110661e402a7d21b530e009f25f8e389"
+ dependencies:
+ once "^1.3.0"
+
+run-async@^2.2.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0"
+ dependencies:
+ is-promise "^2.1.0"
+
+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"
+ dependencies:
+ rx-lite "*"
+
+rx-lite@*, rx-lite@^4.0.8:
+ version "4.0.8"
+ resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-4.0.8.tgz#0b1e11af8bc44836f04a6407e92da42467b79444"
+
+rx-lite@^3.1.2:
+ version "3.1.2"
+ resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-3.1.2.tgz#19ce502ca572665f3b647b10939f97fd1615f102"
+
+rx@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/rx/-/rx-4.1.0.tgz#a5f13ff79ef3b740fe30aa803fb09f98805d4782"
+
+rxjs@^5.3.0:
+ version "5.4.3"
+ resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-5.4.3.tgz#0758cddee6033d68e0fd53676f0f3596ce3d483f"
+ dependencies:
+ symbol-observable "^1.0.1"
+
+safe-buffer@5.1.1, safe-buffer@^5.0.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
+ version "5.1.1"
+ resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853"
+
+safe-json-parse@~1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/safe-json-parse/-/safe-json-parse-1.0.1.tgz#3e76723e38dfdda13c9b1d29a1e07ffee4b30b57"
+
+samsam@1.x, samsam@^1.1.3:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/samsam/-/samsam-1.3.0.tgz#8d1d9350e25622da30de3e44ba692b5221ab7c50"
+
+sane@^1.1.1, sane@^1.6.0:
+ version "1.7.0"
+ resolved "https://registry.yarnpkg.com/sane/-/sane-1.7.0.tgz#b3579bccb45c94cf20355cc81124990dfd346e30"
+ dependencies:
+ anymatch "^1.3.0"
+ exec-sh "^0.2.0"
+ fb-watchman "^2.0.0"
+ minimatch "^3.0.2"
+ minimist "^1.1.1"
+ walker "~1.0.5"
+ watch "~0.10.0"
+
+sass-graph@^2.1.1:
+ version "2.2.4"
+ resolved "https://registry.yarnpkg.com/sass-graph/-/sass-graph-2.2.4.tgz#13fbd63cd1caf0908b9fd93476ad43a51d1e0b49"
+ dependencies:
+ glob "^7.0.0"
+ lodash "^4.0.0"
+ scss-tokenizer "^0.2.3"
+ yargs "^7.0.0"
+
+sax@>=0.6.0:
+ version "1.2.4"
+ resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
+
+scss-tokenizer@^0.2.3:
+ version "0.2.3"
+ resolved "https://registry.yarnpkg.com/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz#8eb06db9a9723333824d3f5530641149847ce5d1"
+ dependencies:
+ js-base64 "^2.1.8"
+ source-map "^0.4.2"
+
+select@^1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/select/-/select-1.1.2.tgz#0e7350acdec80b1108528786ec1d4418d11b396d"
+
+"semver@2 || 3 || 4 || 5", semver@^5.1.0, semver@^5.1.1, semver@^5.3.0, semver@~5.3.0:
+ version "5.3.0"
+ resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f"
+
+semver@^4.1.0:
+ version "4.3.6"
+ resolved "https://registry.yarnpkg.com/semver/-/semver-4.3.6.tgz#300bc6e0e86374f7ba61068b5b1ecd57fc6532da"
+
+semver@^5.4.1:
+ version "5.5.0"
+ resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab"
+
+send@0.15.3:
+ version "0.15.3"
+ resolved "https://registry.yarnpkg.com/send/-/send-0.15.3.tgz#5013f9f99023df50d1bd9892c19e3defd1d53309"
+ dependencies:
+ debug "2.6.7"
+ depd "~1.1.0"
+ destroy "~1.0.4"
+ encodeurl "~1.0.1"
+ escape-html "~1.0.3"
+ etag "~1.8.0"
+ fresh "0.5.0"
+ http-errors "~1.6.1"
+ mime "1.3.4"
+ ms "2.0.0"
+ on-finished "~2.3.0"
+ range-parser "~1.2.0"
+ statuses "~1.3.1"
+
+serve-static@1.12.3:
+ version "1.12.3"
+ resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.12.3.tgz#9f4ba19e2f3030c547f8af99107838ec38d5b1e2"
+ dependencies:
+ encodeurl "~1.0.1"
+ escape-html "~1.0.3"
+ parseurl "~1.3.1"
+ send "0.15.3"
+
+set-blocking@^2.0.0, set-blocking@~2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
+
+set-immediate-shim@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz#4b2b1b27eb808a9f8dcc481a58e5e56f599f3f61"
+
+setprototypeof@1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.3.tgz#66567e37043eeb4f04d91bd658c0cbefb55b8e04"
+
+shebang-command@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
+ dependencies:
+ shebang-regex "^1.0.0"
+
+shebang-regex@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3"
+
+shelljs@^0.7.5:
+ version "0.7.8"
+ resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.7.8.tgz#decbcf874b0d1e5fb72e14b164a9683048e9acb3"
+ dependencies:
+ glob "^7.0.0"
+ interpret "^1.0.0"
+ rechoir "^0.6.2"
+
+shellwords@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.0.tgz#66afd47b6a12932d9071cbfd98a52e785cd0ba14"
+
+signal-exit@^3.0.0, signal-exit@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d"
+
+silent-error@^1.0.0, silent-error@^1.0.1, silent-error@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/silent-error/-/silent-error-1.1.0.tgz#2209706f1c850a9f1d10d0d840918b46f26e1bc9"
+ dependencies:
+ debug "^2.2.0"
+
+simple-dom@^0.3.0:
+ version "0.3.2"
+ resolved "https://registry.yarnpkg.com/simple-dom/-/simple-dom-0.3.2.tgz#0663d10f1556f1500551d518f56e3aba0871371d"
+
+simple-fmt@~0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/simple-fmt/-/simple-fmt-0.1.0.tgz#191bf566a59e6530482cb25ab53b4a8dc85c3a6b"
+
+simple-html-tokenizer@^0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/simple-html-tokenizer/-/simple-html-tokenizer-0.3.0.tgz#9b8b5559d80e331a544dd13dd59382e5d0d94411"
+
+simple-html-tokenizer@^0.4.1:
+ version "0.4.1"
+ resolved "https://registry.yarnpkg.com/simple-html-tokenizer/-/simple-html-tokenizer-0.4.1.tgz#028988bb7fe8b2e6645676d82052587d440b02d3"
+
+simple-is@~0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/simple-is/-/simple-is-0.2.0.tgz#2abb75aade39deb5cc815ce10e6191164850baf0"
+
+sinon@^3.2.1:
+ version "3.3.0"
+ resolved "https://registry.yarnpkg.com/sinon/-/sinon-3.3.0.tgz#9132111b4bbe13c749c2848210864250165069b1"
+ dependencies:
+ build "^0.1.4"
+ diff "^3.1.0"
+ formatio "1.2.0"
+ lodash.get "^4.4.2"
+ lolex "^2.1.2"
+ native-promise-only "^0.8.1"
+ nise "^1.0.1"
+ path-to-regexp "^1.7.0"
+ samsam "^1.1.3"
+ text-encoding "0.6.4"
+ type-detect "^4.0.0"
+
+slash@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55"
+
+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"
+ dependencies:
+ hoek "2.x.x"
+
+socket.io-adapter@0.5.0:
+ version "0.5.0"
+ resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-0.5.0.tgz#cb6d4bb8bec81e1078b99677f9ced0046066bb8b"
+ dependencies:
+ debug "2.3.3"
+ socket.io-parser "2.3.1"
+
+socket.io-client@1.6.0:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-1.6.0.tgz#5b668f4f771304dfeed179064708386fa6717853"
+ dependencies:
+ backo2 "1.0.2"
+ component-bind "1.0.0"
+ component-emitter "1.2.1"
+ debug "2.3.3"
+ engine.io-client "1.8.0"
+ has-binary "0.1.7"
+ indexof "0.0.1"
+ object-component "0.0.3"
+ parseuri "0.0.5"
+ socket.io-parser "2.3.1"
+ to-array "0.1.4"
+
+socket.io-parser@2.3.1:
+ version "2.3.1"
+ resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-2.3.1.tgz#dd532025103ce429697326befd64005fcfe5b4a0"
+ dependencies:
+ component-emitter "1.1.2"
+ debug "2.2.0"
+ isarray "0.0.1"
+ json3 "3.3.2"
+
+socket.io@1.6.0:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-1.6.0.tgz#3e40d932637e6bd923981b25caf7c53e83b6e2e1"
+ dependencies:
+ debug "2.3.3"
+ engine.io "1.8.0"
+ has-binary "0.1.7"
+ object-assign "4.1.0"
+ socket.io-adapter "0.5.0"
+ socket.io-client "1.6.0"
+ socket.io-parser "2.3.1"
+
+sort-object-keys@^1.1.1:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/sort-object-keys/-/sort-object-keys-1.1.2.tgz#d3a6c48dc2ac97e6bc94367696e03f6d09d37952"
+
+sort-package-json@^1.4.0:
+ version "1.7.0"
+ resolved "https://registry.yarnpkg.com/sort-package-json/-/sort-package-json-1.7.0.tgz#13b362ff6400c5b4eaa9ba220f9ea7c3d6644b5f"
+ dependencies:
+ sort-object-keys "^1.1.1"
+
+source-map-support@^0.2.10:
+ version "0.2.10"
+ resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.2.10.tgz#ea5a3900a1c1cb25096a0ae8cc5c2b4b10ded3dc"
+ dependencies:
+ source-map "0.1.32"
+
+source-map-support@^0.4.2:
+ version "0.4.15"
+ resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.15.tgz#03202df65c06d2bd8c7ec2362a193056fef8d3b1"
+ dependencies:
+ source-map "^0.5.6"
+
+source-map-url@^0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.3.0.tgz#7ecaf13b57bcd09da8a40c5d269db33799d4aaf9"
+
+source-map@0.1.32:
+ version "0.1.32"
+ resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.1.32.tgz#c8b6c167797ba4740a8ea33252162ff08591b266"
+ dependencies:
+ amdefine ">=0.0.4"
+
+source-map@0.4.x, source-map@^0.4.2, source-map@^0.4.4:
+ version "0.4.4"
+ resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.4.4.tgz#eba4f5da9c0dc999de68032d8b4f76173652036b"
+ dependencies:
+ amdefine ">=0.0.4"
+
+source-map@^0.5.0, source-map@^0.5.6, source-map@~0.5.0, source-map@~0.5.1:
+ version "0.5.6"
+ resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412"
+
+spawn-args@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/spawn-args/-/spawn-args-0.2.0.tgz#fb7d0bd1d70fd4316bd9e3dec389e65f9d6361bb"
+
+spawn-sync@^1.0.15:
+ version "1.0.15"
+ resolved "https://registry.yarnpkg.com/spawn-sync/-/spawn-sync-1.0.15.tgz#b00799557eb7fb0c8376c29d44e8a1ea67e57476"
+ dependencies:
+ concat-stream "^1.4.7"
+ os-shim "^0.1.2"
+
+spdx-correct@~1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-1.0.2.tgz#4b3073d933ff51f3912f03ac5519498a4150db40"
+ dependencies:
+ spdx-license-ids "^1.0.2"
+
+spdx-expression-parse@~1.0.0:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-1.0.4.tgz#9bdf2f20e1f40ed447fbe273266191fced51626c"
+
+spdx-license-ids@^1.0.2:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz#c9df7a3424594ade6bd11900d596696dc06bac57"
+
+sprintf-js@^1.0.3, sprintf-js@~1.0.2:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
+
+sri-toolbox@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/sri-toolbox/-/sri-toolbox-0.2.0.tgz#a7fea5c3fde55e675cf1c8c06f3ebb5c2935835e"
+
+sshpk@^1.7.0:
+ version "1.13.1"
+ resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.13.1.tgz#512df6da6287144316dc4c18fe1cf1d940739be3"
+ dependencies:
+ asn1 "~0.2.3"
+ assert-plus "^1.0.0"
+ dashdash "^1.12.0"
+ getpass "^0.1.1"
+ optionalDependencies:
+ bcrypt-pbkdf "^1.0.0"
+ ecc-jsbn "~0.1.1"
+ jsbn "~0.1.0"
+ tweetnacl "~0.14.0"
+
+stable@~0.1.3:
+ version "0.1.6"
+ resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.6.tgz#910f5d2aed7b520c6e777499c1f32e139fdecb10"
+
+stack-trace@0.0.x:
+ version "0.0.10"
+ resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0"
+
+"statuses@>= 1.3.1 < 2", statuses@~1.3.1:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e"
+
+stdout-stream@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/stdout-stream/-/stdout-stream-1.4.0.tgz#a2c7c8587e54d9427ea9edb3ac3f2cd522df378b"
+ dependencies:
+ readable-stream "^2.0.1"
+
+stream-to-buffer@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/stream-to-buffer/-/stream-to-buffer-0.1.0.tgz#26799d903ab2025c9bd550ac47171b00f8dd80a9"
+ dependencies:
+ stream-to "~0.2.0"
+
+stream-to@~0.2.0:
+ version "0.2.2"
+ resolved "https://registry.yarnpkg.com/stream-to/-/stream-to-0.2.2.tgz#84306098d85fdb990b9fa300b1b3ccf55e8ef01d"
+
+string-template@~0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/string-template/-/string-template-0.2.1.tgz#42932e598a352d01fc22ec3367d9d84eec6c9add"
+
+string-width@^1.0.1, string-width@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3"
+ dependencies:
+ code-point-at "^1.0.0"
+ is-fullwidth-code-point "^1.0.0"
+ strip-ansi "^3.0.0"
+
+string-width@^2.0.0, string-width@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.0.tgz#030664561fc146c9423ec7d978fe2457437fe6d0"
+ dependencies:
+ is-fullwidth-code-point "^2.0.0"
+ strip-ansi "^4.0.0"
+
+string_decoder@0.10, string_decoder@~0.10.x:
+ version "0.10.31"
+ resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94"
+
+string_decoder@~1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.3.tgz#0fc67d7c141825de94282dd536bec6b9bce860ab"
+ dependencies:
+ safe-buffer "~5.1.0"
+
+stringmap@~0.2.2:
+ version "0.2.2"
+ resolved "https://registry.yarnpkg.com/stringmap/-/stringmap-0.2.2.tgz#556c137b258f942b8776f5b2ef582aa069d7d1b1"
+
+stringset@~0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/stringset/-/stringset-0.2.1.tgz#ef259c4e349344377fcd1c913dd2e848c9c042b5"
+
+stringstream@~0.0.4:
+ version "0.0.5"
+ resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878"
+
+strip-ansi@^0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-0.3.0.tgz#25f48ea22ca79187f3174a4db8759347bb126220"
+ dependencies:
+ ansi-regex "^0.2.1"
+
+strip-ansi@^3.0.0, strip-ansi@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf"
+ dependencies:
+ ansi-regex "^2.0.0"
+
+strip-ansi@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f"
+ dependencies:
+ ansi-regex "^3.0.0"
+
+strip-bom@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e"
+ dependencies:
+ is-utf8 "^0.2.0"
+
+strip-bom@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
+
+strip-eof@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf"
+
+strip-indent@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-1.0.1.tgz#0c7962a6adefa7bbd4ac366460a638552ae1a0a2"
+ dependencies:
+ get-stdin "^4.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"
+
+styled_string@0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/styled_string/-/styled_string-0.0.1.tgz#d22782bd81295459bc4f1df18c4bad8e94dd124a"
+
+sum-up@^1.0.1:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/sum-up/-/sum-up-1.0.3.tgz#1c661f667057f63bcb7875aa1438bc162525156e"
+ dependencies:
+ chalk "^1.0.0"
+
+supports-color@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-0.2.0.tgz#d92de2694eb3f67323973d7ae3d8b55b4c22190a"
+
+supports-color@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7"
+
+supports-color@^4.0.0:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.2.0.tgz#ad986dc7eb2315d009b4d77c8169c2231a684037"
+ dependencies:
+ has-flag "^2.0.0"
+
+svg2png@~3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/svg2png/-/svg2png-3.0.1.tgz#a2644d68b0231ac00af431aa163714ff17106447"
+ dependencies:
+ phantomjs-prebuilt "^2.1.10"
+ pn "^1.0.0"
+ yargs "^3.31.0"
+
+symbol-observable@^1.0.1:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.0.4.tgz#29bf615d4aa7121bdd898b22d4b3f9bc4e2aa03d"
+
+symlink-or-copy@^1.0.0, symlink-or-copy@^1.0.1, symlink-or-copy@^1.1.8:
+ version "1.1.8"
+ resolved "https://registry.yarnpkg.com/symlink-or-copy/-/symlink-or-copy-1.1.8.tgz#cabe61e0010c1c023c173b25ee5108b37f4b4aa3"
+
+table@^3.7.8:
+ version "3.8.3"
+ resolved "https://registry.yarnpkg.com/table/-/table-3.8.3.tgz#2bbc542f0fda9861a755d3947fefd8b3f513855f"
+ dependencies:
+ ajv "^4.7.0"
+ ajv-keywords "^1.0.0"
+ chalk "^1.1.1"
+ lodash "^4.0.0"
+ slice-ansi "0.0.4"
+ string-width "^2.0.0"
+
+table@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/table/-/table-4.0.1.tgz#a8116c133fac2c61f4a420ab6cdf5c4d61f0e435"
+ dependencies:
+ ajv "^4.7.0"
+ ajv-keywords "^1.0.0"
+ chalk "^1.1.1"
+ lodash "^4.0.0"
+ slice-ansi "0.0.4"
+ string-width "^2.0.0"
+
+tap-parser@^5.1.0:
+ version "5.4.0"
+ resolved "https://registry.yarnpkg.com/tap-parser/-/tap-parser-5.4.0.tgz#6907e89725d7b7fa6ae41ee2c464c3db43188aec"
+ dependencies:
+ events-to-array "^1.0.1"
+ js-yaml "^3.2.7"
+ optionalDependencies:
+ readable-stream "^2"
+
+tar-pack@^3.4.0:
+ version "3.4.0"
+ resolved "https://registry.yarnpkg.com/tar-pack/-/tar-pack-3.4.0.tgz#23be2d7f671a8339376cbdb0b8fe3fdebf317984"
+ dependencies:
+ debug "^2.2.0"
+ fstream "^1.0.10"
+ fstream-ignore "^1.0.5"
+ once "^1.3.3"
+ readable-stream "^2.1.4"
+ rimraf "^2.5.1"
+ tar "^2.2.1"
+ uid-number "^0.0.6"
+
+tar@^2.0.0, tar@^2.2.1:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.1.tgz#8e4d2a256c0e2185c6b18ad694aec968b83cb1d1"
+ dependencies:
+ block-stream "*"
+ fstream "^1.0.2"
+ inherits "2"
+
+temp@0.8.3:
+ version "0.8.3"
+ resolved "https://registry.yarnpkg.com/temp/-/temp-0.8.3.tgz#e0c6bc4d26b903124410e4fed81103014dfc1f59"
+ dependencies:
+ os-tmpdir "^1.0.0"
+ rimraf "~2.2.6"
+
+testem@^1.15.0:
+ version "1.18.0"
+ resolved "https://registry.yarnpkg.com/testem/-/testem-1.18.0.tgz#4a9f798509d260dca928823aaae5dbc6a9c977ee"
+ dependencies:
+ backbone "^1.1.2"
+ bluebird "^3.4.6"
+ charm "^1.0.0"
+ commander "^2.6.0"
+ consolidate "^0.14.0"
+ cross-spawn "^5.1.0"
+ express "^4.10.7"
+ fireworm "^0.7.0"
+ glob "^7.0.4"
+ http-proxy "^1.13.1"
+ js-yaml "^3.2.5"
+ lodash.assignin "^4.1.0"
+ lodash.clonedeep "^4.4.1"
+ lodash.find "^4.5.1"
+ lodash.uniqby "^4.7.0"
+ mkdirp "^0.5.1"
+ mustache "^2.2.1"
+ node-notifier "^5.0.1"
+ npmlog "^4.0.0"
+ printf "^0.2.3"
+ rimraf "^2.4.4"
+ socket.io "1.6.0"
+ spawn-args "^0.2.0"
+ styled_string "0.0.1"
+ tap-parser "^5.1.0"
+ xmldom "^0.1.19"
+
+text-encoding@0.6.4, text-encoding@^0.6.4:
+ version "0.6.4"
+ resolved "https://registry.yarnpkg.com/text-encoding/-/text-encoding-0.6.4.tgz#e399a982257a276dae428bb92845cb71bdc26d19"
+
+text-table@~0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
+
+"textextensions@1 || 2":
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/textextensions/-/textextensions-2.1.0.tgz#1be0dc2a0dc244d44be8a09af6a85afb93c4dbc3"
+
+throttleit@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-1.0.0.tgz#9e785836daf46743145a5984b6268d828528ac6c"
+
+through2@^2.0.0:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.3.tgz#0004569b37c7c74ba39c43f3ced78d1ad94140be"
+ dependencies:
+ readable-stream "^2.1.5"
+ xtend "~4.0.1"
+
+through@^2.3.6, through@^2.3.8, through@~2.3.8:
+ version "2.3.8"
+ resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
+
+timespan@2.x:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/timespan/-/timespan-2.3.0.tgz#4902ce040bd13d845c8f59b27e9d59bad6f39929"
+
+tiny-emitter@^2.0.0:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-2.0.2.tgz#82d27468aca5ade8e5fd1e6d22b57dd43ebdfb7c"
+
+tiny-lr@^1.0.3:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/tiny-lr/-/tiny-lr-1.0.5.tgz#21f40bf84ebd1f853056680375eef1670c334112"
+ dependencies:
+ body "^5.1.0"
+ debug "~2.6.7"
+ faye-websocket "~0.10.0"
+ livereload-js "^2.2.2"
+ object-assign "^4.1.0"
+ qs "^6.4.0"
+
+tinycolor2@^1.1.2:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.4.1.tgz#f4fad333447bc0b07d4dc8e9209d8f39a8ac77e8"
+
+tmp@0.0.28:
+ version "0.0.28"
+ resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.28.tgz#172735b7f614ea7af39664fa84cf0de4e515d120"
+ dependencies:
+ os-tmpdir "~1.0.1"
+
+tmp@^0.0.29:
+ version "0.0.29"
+ resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.29.tgz#f25125ff0dd9da3ccb0c2dd371ee1288bb9128c0"
+ dependencies:
+ os-tmpdir "~1.0.1"
+
+tmp@^0.0.31:
+ version "0.0.31"
+ resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.31.tgz#8f38ab9438e17315e5dbd8b3657e8bfb277ae4a7"
+ dependencies:
+ os-tmpdir "~1.0.1"
+
+tmpl@1.0.x:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1"
+
+to-array@0.1.4:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/to-array/-/to-array-0.1.4.tgz#17e6c11f73dd4f3d74cda7a4ff3238e9ad9bf890"
+
+to-fast-properties@^1.0.0, to-fast-properties@^1.0.1:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47"
+
+to-ico@^1.1.2:
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/to-ico/-/to-ico-1.1.4.tgz#b4c7b4afd2aa9fe65356c38c4bcb62e077de1ca7"
+ dependencies:
+ arrify "^1.0.1"
+ buffer-alloc "^1.1.0"
+ image-size "^0.5.0"
+ parse-png "^1.0.0"
+ resize-img "^1.1.0"
+
+tough-cookie@~2.3.0:
+ version "2.3.2"
+ resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.2.tgz#f081f76e4c85720e6c37a5faced737150d84072a"
+ dependencies:
+ punycode "^1.4.1"
+
+tree-sync@^1.2.1, tree-sync@^1.2.2:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/tree-sync/-/tree-sync-1.2.2.tgz#2cf76b8589f59ffedb58db5a3ac7cb013d0158b7"
+ dependencies:
+ debug "^2.2.0"
+ fs-tree-diff "^0.5.6"
+ mkdirp "^0.5.1"
+ quick-temp "^0.1.5"
+ walk-sync "^0.2.7"
+
+trim-newlines@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613"
+
+trim-right@^1.0.0, trim-right@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003"
+
+trim@0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/trim/-/trim-0.0.1.tgz#5858547f6b290757ee95cccc666fb50084c460dd"
+
+try-resolve@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/try-resolve/-/try-resolve-1.0.1.tgz#cfde6fabd72d63e5797cfaab873abbe8e700e912"
+
+tryit@^1.0.1:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/tryit/-/tryit-1.0.3.tgz#393be730a9446fd1ead6da59a014308f36c289cb"
+
+tryor@~0.1.2:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/tryor/-/tryor-0.1.2.tgz#8145e4ca7caff40acde3ccf946e8b8bb75b4172b"
+
+tunnel-agent@^0.6.0:
+ version "0.6.0"
+ resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd"
+ dependencies:
+ safe-buffer "^5.0.1"
+
+tunnel-agent@~0.4.1:
+ version "0.4.3"
+ resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.4.3.tgz#6373db76909fe570e08d73583365ed828a74eeeb"
+
+tweetnacl@^0.14.3, tweetnacl@~0.14.0:
+ version "0.14.5"
+ resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
+
+type-check@~0.3.2:
+ version "0.3.2"
+ resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72"
+ dependencies:
+ prelude-ls "~1.1.2"
+
+type-detect@^4.0.0:
+ version "4.0.7"
+ resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.7.tgz#862bd2cf6058ad92799ff5a5b8cf7b6cec726198"
+
+type-is@~1.6.15:
+ version "1.6.15"
+ resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.15.tgz#cab10fb4909e441c82842eafe1ad646c81804410"
+ dependencies:
+ media-typer "0.3.0"
+ mime-types "~2.1.15"
+
+typedarray@^0.0.6, typedarray@~0.0.5:
+ version "0.0.6"
+ resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
+
+uc.micro@^1.0.1, uc.micro@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.3.tgz#7ed50d5e0f9a9fb0a573379259f2a77458d50192"
+
+uglify-js@1.x:
+ version "1.3.5"
+ resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-1.3.5.tgz#4b5bfff9186effbaa888e4c9e94bd9fc4c94929d"
+
+uglify-js@^2.6, uglify-js@^2.7.0:
+ version "2.8.29"
+ resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.29.tgz#29c5733148057bb4e1f75df35b7a9cb72e6a59dd"
+ dependencies:
+ source-map "~0.5.1"
+ yargs "~3.10.0"
+ optionalDependencies:
+ uglify-to-browserify "~1.0.0"
+
+uglify-to-browserify@~1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7"
+
+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.0.x:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.0.2.tgz#ace116ab557cd197386a4e88f4685378c8b2e4fa"
+
+underscore.string@~3.3.4:
+ version "3.3.4"
+ resolved "https://registry.yarnpkg.com/underscore.string/-/underscore.string-3.3.4.tgz#2c2a3f9f83e64762fdc45e6ceac65142864213db"
+ dependencies:
+ sprintf-js "^1.0.3"
+ util-deprecate "^1.0.2"
+
+underscore@>=1.8.3, underscore@^1.8.3:
+ version "1.8.3"
+ resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.8.3.tgz#4f3fb53b106e6097fcf9cb4109f2a5e9bdfa5022"
+
+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"
+
+universalify@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.0.tgz#9eb1c4651debcc670cc94f1a75762332bb967778"
+
+unpipe@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
+
+untildify@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/untildify/-/untildify-2.1.0.tgz#17eb2807987f76952e9c0485fc311d06a826a2e0"
+ dependencies:
+ os-homedir "^1.0.0"
+
+url-regex@^3.0.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/url-regex/-/url-regex-3.2.0.tgz#dbad1e0c9e29e105dd0b1f09f6862f7fdb482724"
+ dependencies:
+ ip-regex "^1.0.1"
+
+user-home@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/user-home/-/user-home-1.1.1.tgz#2b5be23a32b63a7c9deb8d0f28d485724a3df190"
+
+user-home@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/user-home/-/user-home-2.0.0.tgz#9c70bfd8169bc1dcbf48604e0f04b8b49cde9e9f"
+ dependencies:
+ os-homedir "^1.0.0"
+
+username-sync@1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/username-sync/-/username-sync-1.0.1.tgz#1cde87eefcf94b8822984d938ba2b797426dae1f"
+
+util-deprecate@^1.0.2, util-deprecate@~1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
+
+utils-merge@1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.0.tgz#0294fb922bb9375153541c4f7096231f287c8af8"
+
+uuid@^3.0.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04"
+
+validate-npm-package-license@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz#2804babe712ad3379459acfbe24746ab2c303fbc"
+ dependencies:
+ spdx-correct "~1.0.0"
+ spdx-expression-parse "~1.0.0"
+
+validate-npm-package-name@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/validate-npm-package-name/-/validate-npm-package-name-3.0.0.tgz#5fa912d81eb7d0c74afc140de7317f0ca7df437e"
+ dependencies:
+ builtins "^1.0.3"
+
+vary@~1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.1.tgz#67535ebb694c1d52257457984665323f587e8d37"
+
+verror@1.3.6:
+ version "1.3.6"
+ resolved "https://registry.yarnpkg.com/verror/-/verror-1.3.6.tgz#cff5df12946d297d2baaefaa2689e25be01c005c"
+ dependencies:
+ extsprintf "1.0.2"
+
+vinyl@^1.1.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-1.2.0.tgz#5c88036cf565e5df05558bfc911f8656df218884"
+ dependencies:
+ clone "^1.0.0"
+ clone-stats "^0.0.1"
+ replace-ext "0.0.1"
+
+walk-sync@0.3.1:
+ version "0.3.1"
+ resolved "https://registry.yarnpkg.com/walk-sync/-/walk-sync-0.3.1.tgz#558a16aeac8c0db59c028b73c66f397684ece465"
+ dependencies:
+ ensure-posix-path "^1.0.0"
+ matcher-collection "^1.0.0"
+
+walk-sync@^0.1.3:
+ version "0.1.3"
+ resolved "https://registry.yarnpkg.com/walk-sync/-/walk-sync-0.1.3.tgz#8a07261a00bda6cfb1be25e9f100fad57546f583"
+
+walk-sync@^0.2.5, walk-sync@^0.2.7:
+ version "0.2.7"
+ resolved "https://registry.yarnpkg.com/walk-sync/-/walk-sync-0.2.7.tgz#b49be4ee6867657aeb736978b56a29d10fa39969"
+ dependencies:
+ ensure-posix-path "^1.0.0"
+ matcher-collection "^1.0.0"
+
+walk-sync@^0.3.0, walk-sync@^0.3.1:
+ version "0.3.2"
+ resolved "https://registry.yarnpkg.com/walk-sync/-/walk-sync-0.3.2.tgz#4827280afc42d0e035367c4a4e31eeac0d136f75"
+ dependencies:
+ ensure-posix-path "^1.0.0"
+ matcher-collection "^1.0.0"
+
+walker@1.x, walker@~1.0.5:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.7.tgz#2f7f9b8fd10d677262b18a884e28d19618e028fb"
+ dependencies:
+ makeerror "1.0.x"
+
+watch@~0.10.0:
+ version "0.10.0"
+ resolved "https://registry.yarnpkg.com/watch/-/watch-0.10.0.tgz#77798b2da0f9910d595f1ace5b0c2258521f21dc"
+
+websocket-driver@>=0.5.1:
+ version "0.6.5"
+ resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.6.5.tgz#5cb2556ceb85f4373c6d8238aa691c8454e13a36"
+ dependencies:
+ websocket-extensions ">=0.1.1"
+
+websocket-extensions@>=0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.1.tgz#76899499c184b6ef754377c2dbb0cd6cb55d29e7"
+
+whatwg-fetch@^2.0.3:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.3.tgz#9c84ec2dcf68187ff00bc64e1274b442176e1c84"
+
+which-module@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/which-module/-/which-module-1.0.0.tgz#bba63ca861948994ff307736089e3b96026c2a4f"
+
+which@1, which@^1.2.12, which@^1.2.9, which@~1.2.10:
+ version "1.2.14"
+ resolved "https://registry.yarnpkg.com/which/-/which-1.2.14.tgz#9a87c4378f03e827cecaf1acdf56c736c01c14e5"
+ dependencies:
+ isexe "^2.0.0"
+
+wide-align@^1.1.0:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.2.tgz#571e0f1b0604636ebc0dfc21b0339bbe31341710"
+ dependencies:
+ string-width "^1.0.2"
+
+window-size@0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.0.tgz#5438cd2ea93b202efa3a19fe8887aee7c94f9c9d"
+
+window-size@^0.1.2, window-size@^0.1.4:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.4.tgz#f8e1aa1ee5a53ec5bf151ffa09742a6ad7697876"
+
+winston@*:
+ version "2.4.0"
+ resolved "https://registry.yarnpkg.com/winston/-/winston-2.4.0.tgz#808050b93d52661ed9fb6c26b3f0c826708b0aee"
+ dependencies:
+ async "~1.0.0"
+ colors "1.0.x"
+ cycle "1.0.x"
+ eyes "0.1.x"
+ isstream "0.1.x"
+ stack-trace "0.0.x"
+
+wordwrap@0.0.2:
+ version "0.0.2"
+ resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f"
+
+wordwrap@~0.0.2:
+ version "0.0.3"
+ resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107"
+
+wordwrap@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"
+
+workerpool@^2.2.1:
+ version "2.2.2"
+ resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-2.2.2.tgz#1cf53bacafd98ca5d808ff54cc72f3fecb5e1d56"
+ dependencies:
+ object-assign "4.1.1"
+
+wrap-ansi@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85"
+ dependencies:
+ string-width "^1.0.1"
+ strip-ansi "^3.0.1"
+
+wrappy@1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
+
+wrench@1.3.x:
+ version "1.3.9"
+ resolved "https://registry.yarnpkg.com/wrench/-/wrench-1.3.9.tgz#6f13ec35145317eb292ca5f6531391b244111411"
+
+write-file-atomic@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-2.1.0.tgz#1769f4b551eedce419f0505deae2e26763542d37"
+ dependencies:
+ graceful-fs "^4.1.11"
+ imurmurhash "^0.1.4"
+ slide "^1.1.5"
+
+write@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/write/-/write-0.2.1.tgz#5fc03828e264cea3fe91455476f7a3c566cb0757"
+ dependencies:
+ mkdirp "^0.5.1"
+
+ws@1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/ws/-/ws-1.1.1.tgz#082ddb6c641e85d4bb451f03d52f06eabdb1f018"
+ dependencies:
+ options ">=0.0.5"
+ ultron "1.0.x"
+
+wtf-8@1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/wtf-8/-/wtf-8-1.0.0.tgz#392d8ba2d0f1c34d1ee2d630f15d0efb68e1048a"
+
+xdg-basedir@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-3.0.0.tgz#496b2cc109eca8dbacfe2dc72b603c17c5870ad4"
+
+xhr@^2.0.1:
+ version "2.4.0"
+ resolved "https://registry.yarnpkg.com/xhr/-/xhr-2.4.0.tgz#e16e66a45f869861eeefab416d5eff722dc40993"
+ dependencies:
+ global "~4.3.0"
+ is-function "^1.0.1"
+ parse-headers "^2.0.0"
+ xtend "^4.0.0"
+
+xml-parse-from-string@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/xml-parse-from-string/-/xml-parse-from-string-1.0.1.tgz#a9029e929d3dbcded169f3c6e28238d95a5d5a28"
+
+xml2js@>=0.2.4, xml2js@^0.4.5:
+ version "0.4.17"
+ resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.17.tgz#17be93eaae3f3b779359c795b419705a8817e868"
+ dependencies:
+ sax ">=0.6.0"
+ xmlbuilder "^4.1.0"
+
+xmlbuilder@^4.1.0:
+ version "4.2.1"
+ resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-4.2.1.tgz#aa58a3041a066f90eaa16c2f5389ff19f3f461a5"
+ dependencies:
+ lodash "^4.0.0"
+
+xmldom@^0.1.19:
+ version "0.1.27"
+ resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.1.27.tgz#d501f97b3bdb403af8ef9ecc20573187aadac0e9"
+
+xmlhttprequest-ssl@1.5.3:
+ version "1.5.3"
+ resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.3.tgz#185a888c04eca46c3e4070d99f7b49de3528992d"
+
+xtend@^4.0.0, xtend@~4.0.0, xtend@~4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af"
+
+y18n@^3.2.0, y18n@^3.2.1:
+ version "3.2.1"
+ resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41"
+
+yallist@^2.1.2:
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"
+
+yam@0.0.22:
+ version "0.0.22"
+ resolved "https://registry.yarnpkg.com/yam/-/yam-0.0.22.tgz#38a76cb79a19284d9206ed49031e359a1340bd06"
+ dependencies:
+ fs-extra "^0.30.0"
+ lodash.merge "^4.4.0"
+
+yargs-parser@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-5.0.0.tgz#275ecf0d7ffe05c77e64e7c86e4cd94bf0e1228a"
+ dependencies:
+ camelcase "^3.0.0"
+
+yargs@^3.31.0:
+ version "3.32.0"
+ resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.32.0.tgz#03088e9ebf9e756b69751611d2a5ef591482c995"
+ dependencies:
+ camelcase "^2.0.1"
+ cliui "^3.0.3"
+ decamelize "^1.1.1"
+ os-locale "^1.4.0"
+ string-width "^1.0.1"
+ window-size "^0.1.4"
+ y18n "^3.2.0"
+
+yargs@^7.0.0, yargs@^7.1.0:
+ version "7.1.0"
+ resolved "https://registry.yarnpkg.com/yargs/-/yargs-7.1.0.tgz#6ba318eb16961727f5d284f8ea003e8d6154d0c8"
+ dependencies:
+ camelcase "^3.0.0"
+ cliui "^3.2.0"
+ decamelize "^1.1.1"
+ get-caller-file "^1.0.1"
+ os-locale "^1.4.0"
+ read-pkg-up "^1.0.1"
+ require-directory "^2.1.1"
+ require-main-filename "^1.0.1"
+ set-blocking "^2.0.0"
+ string-width "^1.0.2"
+ which-module "^1.0.0"
+ y18n "^3.2.1"
+ yargs-parser "^5.0.0"
+
+yargs@~3.10.0:
+ version "3.10.0"
+ resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.10.0.tgz#f7ee7bd857dd7c1d2d38c0e74efbd681d1431fd1"
+ dependencies:
+ camelcase "^1.0.2"
+ cliui "^2.1.0"
+ decamelize "^1.0.0"
+ window-size "0.1.0"
+
+yargs@~3.27.0:
+ version "3.27.0"
+ resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.27.0.tgz#21205469316e939131d59f2da0c6d7f98221ea40"
+ dependencies:
+ camelcase "^1.2.1"
+ cliui "^2.1.0"
+ decamelize "^1.0.0"
+ os-locale "^1.4.0"
+ window-size "^0.1.2"
+ y18n "^3.2.0"
+
+yauzl@2.4.1:
+ version "2.4.1"
+ resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.4.1.tgz#9528f442dab1b2284e58b4379bb194e22e0c4005"
+ dependencies:
+ fd-slicer "~1.0.1"
+
+yeast@0.1.2:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/yeast/-/yeast-0.1.2.tgz#008e06d8094320c372dbc2f8ed76a0ca6c8ac419"