UI namespaces (#5119)
* add namespace sidebar item * depend on ember-inflector directly * list-view and list-item components * fill out components and render empty namespaces page * list namespaces in access * add menu contextual component to list item * popup contextual component * full crud for namespaces * add namespaces service and picker component * split application and vault.cluster templates and controllers, add namespace query param, add namespace-picker to vault.namespace template * remove usage of href-to * remove ember-href-to from deps * add ember-responsive * start styling the picker and link to appropriate namespaces, use ember-responsive to render picker in different places based on the breakpoint * get query param working and save ns to authdata when authenticating, feed through ns in application adapter * move to observer on the controller for setting state on the service * set state in the beforeModel hook and clear the ember data model cache * nav to secrets on change and make error handling more resilient utilizing the method that atlas does to eagerly update URLs * add a list of sys endpoints in a helper * hide header elements if not in the root namespace * debounce namespace input on auth, fix 404 for auth method fetch, move auth method fetch to a task on the auth-form component and refretch on namespace change * fix display of supported engines and exclusion of sys and identity engines * don't fetch replication status if you're in a non-root namespace * hide seal sub-menu if not in the root namespace * don't autocomplete auth form inputs * always send some requests to the root namespace * use methodType and engineType instead of type in case there it is ns_ prefixed * use sys/internal/ui/namespaces to fetch the list in the dropdown * don't use model for namespace picker and always make the request to the token namespace * fix header handling for fetch calls * use namespace-reminder component on creation and edit forms throughout the application * add namespace-reminder to the console * add flat * add deepmerge for creating the tree in the menu * delayed rendering for animation timing * design and code feedback on the first round * white text in the namespace picker * fix namespace picker issues with root keys * separate path-to-tree * add tests for path-to-tree util * hide picker if you're in the root ns and you can't access other namespaces * show error message if you enter invalid characters for namespace path * return a different model if we dont have the namespaces feature and show upgrade page * if a token has a namespace_path, use that as the root user namespace and transition them there on login * use token namespace for user, but use specified namespace to log in * always renew tokens in the token namespace * fix edition-badge test
This commit is contained in:
parent
97ada3fe0b
commit
21af204683
|
@ -1,12 +1,15 @@
|
|||
import Ember from 'ember';
|
||||
import DS from 'ember-data';
|
||||
import fetch from 'fetch';
|
||||
import config from '../config/environment';
|
||||
|
||||
const POLLING_URL_PATTERNS = ['sys/seal-status', 'sys/health', 'sys/replication/status'];
|
||||
const { APP } = config;
|
||||
const { POLLING_URLS, NAMESPACE_ROOT_URLS } = APP;
|
||||
const { inject, assign, set, RSVP } = Ember;
|
||||
|
||||
export default DS.RESTAdapter.extend({
|
||||
auth: inject.service(),
|
||||
namespaceService: inject.service('namespace'),
|
||||
controlGroup: inject.service(),
|
||||
|
||||
flashMessages: inject.service(),
|
||||
|
@ -25,17 +28,26 @@ export default DS.RESTAdapter.extend({
|
|||
return false;
|
||||
},
|
||||
|
||||
_preRequest(url, options) {
|
||||
const token = options.clientToken || this.get('auth.currentToken');
|
||||
addHeaders(url, options) {
|
||||
let token = options.clientToken || this.get('auth.currentToken');
|
||||
let headers = {};
|
||||
if (token && !options.unauthenticated) {
|
||||
options.headers = assign(options.headers || {}, {
|
||||
'X-Vault-Token': token,
|
||||
});
|
||||
headers['X-Vault-Token'] = token;
|
||||
if (options.wrapTTL) {
|
||||
assign(options.headers, { 'X-Vault-Wrap-TTL': options.wrapTTL });
|
||||
headers['X-Vault-Wrap-TTL'] = options.wrapTTL;
|
||||
}
|
||||
}
|
||||
const isPolling = POLLING_URL_PATTERNS.some(str => url.includes(str));
|
||||
let namespace =
|
||||
typeof options.namespace === 'undefined' ? this.get('namespaceService.path') : options.namespace;
|
||||
if (namespace && !NAMESPACE_ROOT_URLS.some(str => url.includes(str))) {
|
||||
headers['X-Vault-Namespace'] = namespace;
|
||||
}
|
||||
options.headers = assign(options.headers || {}, headers);
|
||||
},
|
||||
|
||||
_preRequest(url, options) {
|
||||
this.addHeaders(url, options);
|
||||
const isPolling = POLLING_URLS.some(str => url.includes(str));
|
||||
if (!isPolling) {
|
||||
this.get('auth').setLastFetch(Date.now());
|
||||
}
|
||||
|
@ -87,8 +99,8 @@ export default DS.RESTAdapter.extend({
|
|||
rawRequest(url, type, options = {}) {
|
||||
let opts = this._preRequest(url, options);
|
||||
return fetch(url, {
|
||||
method: type | 'GET',
|
||||
headers: opts.headers | {},
|
||||
method: type || 'GET',
|
||||
headers: opts.headers || {},
|
||||
}).then(response => {
|
||||
if (response.status >= 200 && response.status < 300) {
|
||||
return RSVP.resolve(response);
|
||||
|
|
|
@ -26,7 +26,9 @@ export default ApplicationAdapter.extend({
|
|||
};
|
||||
})
|
||||
.catch(() => {
|
||||
return [];
|
||||
return {
|
||||
data: {},
|
||||
};
|
||||
});
|
||||
}
|
||||
return this.ajax(this.url(), 'GET').catch(e => {
|
||||
|
|
|
@ -20,6 +20,7 @@ const REPLICATION_ENDPOINTS = {
|
|||
const REPLICATION_MODES = ['dr', 'performance'];
|
||||
export default ApplicationAdapter.extend({
|
||||
version: inject.service(),
|
||||
namespaceService: inject.service('namespace'),
|
||||
shouldBackgroundReloadRecord() {
|
||||
return true;
|
||||
},
|
||||
|
@ -28,7 +29,7 @@ export default ApplicationAdapter.extend({
|
|||
health: this.health(),
|
||||
sealStatus: this.sealStatus().catch(e => e),
|
||||
};
|
||||
if (this.get('version.isEnterprise')) {
|
||||
if (this.get('version.isEnterprise') && this.get('namespaceService.inRootNamespace')) {
|
||||
fetches.replicationStatus = this.replicationStatus().catch(e => e);
|
||||
}
|
||||
return Ember.RSVP.hash(fetches).then(({ health, sealStatus, replicationStatus }) => {
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
import ApplicationAdapter from './application';
|
||||
|
||||
export default ApplicationAdapter.extend({
|
||||
pathForType() {
|
||||
return 'namespaces';
|
||||
},
|
||||
urlForFindAll(modelName, snapshot) {
|
||||
if (snapshot.adapterOptions && snapshot.adapterOptions.forUser) {
|
||||
return `/${this.urlPrefix()}/internal/ui/namespaces`;
|
||||
}
|
||||
return `/${this.urlPrefix()}/namespaces?list=true`;
|
||||
},
|
||||
|
||||
urlForCreateRecord(modelName, snapshot) {
|
||||
let id = snapshot.attr('path');
|
||||
return this.buildURL(modelName, id);
|
||||
},
|
||||
|
||||
createRecord(store, type, snapshot) {
|
||||
let id = snapshot.attr('path');
|
||||
return this._super(...arguments).then(() => {
|
||||
return { id };
|
||||
});
|
||||
},
|
||||
|
||||
findAll(store, type, sinceToken, snapshot) {
|
||||
if (snapshot.adapterOptions && typeof snapshot.adapterOptions.namespace !== 'undefined') {
|
||||
return this.ajax(this.urlForFindAll('namespace', snapshot), 'GET', {
|
||||
namespace: snapshot.adapterOptions.namespace,
|
||||
});
|
||||
}
|
||||
return this._super(...arguments);
|
||||
},
|
||||
});
|
|
@ -65,9 +65,9 @@ export default ApplicationAdapter.extend({
|
|||
return Ember.RSVP.hash({
|
||||
backend: backendPath,
|
||||
id: this.id(backendPath),
|
||||
der: this.rawRequest(derURL, { unauthenticated: true }).then(response => response.blob()),
|
||||
pem: this.rawRequest(pemURL, { unauthenticated: true }).then(response => response.text()),
|
||||
ca_chain: this.rawRequest(chainURL, { unauthenticated: true }).then(response => response.text()),
|
||||
der: this.rawRequest(derURL, 'GET', { unauthenticated: true }).then(response => response.blob()),
|
||||
pem: this.rawRequest(pemURL, 'GET', { unauthenticated: true }).then(response => response.text()),
|
||||
ca_chain: this.rawRequest(chainURL, 'GET', { unauthenticated: true }).then(response => response.text()),
|
||||
});
|
||||
},
|
||||
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
//https://github.com/jgthms/bulma/blob/6ad2e3df0589e5d6ff7a9c03ee1c78a546bedeaf/sass/utilities/initial-variables.sass#L48-L59
|
||||
//https://github.com/jgthms/bulma/blob/6ad2e3df0589e5d6ff7a9c03ee1c78a546bedeaf/sass/utilities/mixins.sass#L71-L130
|
||||
export default {
|
||||
mobile: '(max-width: 768px)',
|
||||
tablet: '(min-width: 769px)',
|
||||
desktop: '(min-width: 1088px)',
|
||||
};
|
|
@ -12,7 +12,6 @@ const DEFAULTS = {
|
|||
};
|
||||
|
||||
export default Ember.Component.extend(DEFAULTS, {
|
||||
classNames: ['auth-form'],
|
||||
router: inject.service(),
|
||||
auth: inject.service(),
|
||||
flashMessages: inject.service(),
|
||||
|
@ -24,6 +23,30 @@ export default Ember.Component.extend(DEFAULTS, {
|
|||
methods: null,
|
||||
cluster: null,
|
||||
redirectTo: null,
|
||||
namespace: null,
|
||||
// internal
|
||||
oldNamespace: null,
|
||||
didReceiveAttrs() {
|
||||
this._super(...arguments);
|
||||
let token = this.get('wrappedToken');
|
||||
let newMethod = this.get('selectedAuth');
|
||||
let oldMethod = this.get('oldSelectedAuth');
|
||||
|
||||
let ns = this.get('namespace');
|
||||
let oldNS = this.get('oldNamespace');
|
||||
if (oldNS === null || oldNS !== ns) {
|
||||
this.get('fetchMethods').perform();
|
||||
}
|
||||
this.set('oldNamespace', ns);
|
||||
if (oldMethod && oldMethod !== newMethod) {
|
||||
this.resetDefaults();
|
||||
}
|
||||
this.set('oldSelectedAuth', newMethod);
|
||||
|
||||
if (token) {
|
||||
this.get('unwrapToken').perform(token);
|
||||
}
|
||||
},
|
||||
|
||||
didRender() {
|
||||
this._super(...arguments);
|
||||
|
@ -35,10 +58,12 @@ export default Ember.Component.extend(DEFAULTS, {
|
|||
// this is here because we're changing the `with` attr and there's no way to short-circuit rendering,
|
||||
// so we'll just nav -> get new attrs -> re-render
|
||||
if (!this.get('selectedAuth') || (this.get('selectedAuth') && !this.get('selectedAuthBackend'))) {
|
||||
this.get('router').replaceWith('vault.cluster.auth', this.get('cluster.name'), {
|
||||
this.set('selectedAuth', this.firstMethod());
|
||||
this.get('router').replaceWith({
|
||||
queryParams: {
|
||||
with: this.firstMethod(),
|
||||
wrappedToken: this.get('wrappedToken'),
|
||||
namespace: this.get('namespace'),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -50,40 +75,27 @@ export default Ember.Component.extend(DEFAULTS, {
|
|||
return get(firstMethod, 'path') || get(firstMethod, 'type');
|
||||
},
|
||||
|
||||
didReceiveAttrs() {
|
||||
this._super(...arguments);
|
||||
let token = this.get('wrappedToken');
|
||||
let newMethod = this.get('selectedAuth');
|
||||
let oldMethod = this.get('oldSelectedAuth');
|
||||
|
||||
if (oldMethod && oldMethod !== newMethod) {
|
||||
this.resetDefaults();
|
||||
}
|
||||
this.set('oldSelectedAuth', newMethod);
|
||||
|
||||
if (token) {
|
||||
this.get('unwrapToken').perform(token);
|
||||
}
|
||||
},
|
||||
|
||||
resetDefaults() {
|
||||
this.setProperties(DEFAULTS);
|
||||
},
|
||||
|
||||
selectedAuthIsPath: computed.match('selectedAuth', /\/$/),
|
||||
selectedAuthBackend: Ember.computed(
|
||||
'allSupportedMethods',
|
||||
'methods',
|
||||
'methods.[]',
|
||||
'selectedAuth',
|
||||
'selectedAuthIsPath',
|
||||
function() {
|
||||
let methods = this.get('methods');
|
||||
let selectedAuth = this.get('selectedAuth');
|
||||
let keyIsPath = this.get('selectedAuthIsPath');
|
||||
if (!methods) {
|
||||
return {};
|
||||
}
|
||||
if (keyIsPath) {
|
||||
return methods.findBy('path', selectedAuth);
|
||||
} else {
|
||||
return BACKENDS.findBy('type', selectedAuth);
|
||||
}
|
||||
return BACKENDS.findBy('type', selectedAuth);
|
||||
}
|
||||
),
|
||||
|
||||
|
@ -107,7 +119,7 @@ export default Ember.Component.extend(DEFAULTS, {
|
|||
hasMethodsWithPath: computed('methodsToShow', function() {
|
||||
return this.get('methodsToShow').isAny('path');
|
||||
}),
|
||||
methodsToShow: computed('methods', 'methods.[]', function() {
|
||||
methodsToShow: computed('methods', function() {
|
||||
let methods = this.get('methods') || [];
|
||||
let shownMethods = methods.filter(m =>
|
||||
BACKENDS.find(b => get(b, 'type').toLowerCase() === get(m, 'type').toLowerCase())
|
||||
|
@ -128,6 +140,24 @@ export default Ember.Component.extend(DEFAULTS, {
|
|||
}
|
||||
}),
|
||||
|
||||
fetchMethods: task(function*() {
|
||||
let store = this.get('store');
|
||||
this.set('methods', null);
|
||||
store.unloadAll('auth-method');
|
||||
try {
|
||||
let methods = yield store.findAll('auth-method', {
|
||||
adapterOptions: {
|
||||
unauthenticated: true,
|
||||
},
|
||||
});
|
||||
this.set('methods', methods);
|
||||
} catch (e) {
|
||||
this.set('error', `There was an error fetching auth methods: ${e.errors[0]}`);
|
||||
}
|
||||
}),
|
||||
|
||||
showLoading: computed.or('fetchMethods.isRunning', 'unwrapToken.isRunning'),
|
||||
|
||||
handleError(e) {
|
||||
this.set('loading', false);
|
||||
let errors = e.errors.map(error => {
|
||||
|
@ -149,9 +179,9 @@ export default Ember.Component.extend(DEFAULTS, {
|
|||
let targetRoute = this.get('redirectTo') || 'vault.cluster';
|
||||
let backend = this.get('selectedAuthBackend') || {};
|
||||
let backendMeta = BACKENDS.find(
|
||||
b => get(b, 'type').toLowerCase() === get(backend, 'type').toLowerCase()
|
||||
b => (get(b, 'type') || '').toLowerCase() === (get(backend, 'type') || '').toLowerCase()
|
||||
);
|
||||
let attributes = get(backendMeta, 'formAttributes');
|
||||
let attributes = get(backendMeta || {}, 'formAttributes') || {};
|
||||
|
||||
data = Ember.assign(data, this.getProperties(...attributes));
|
||||
if (this.get('customPath') || get(backend, 'id')) {
|
||||
|
@ -159,9 +189,9 @@ export default Ember.Component.extend(DEFAULTS, {
|
|||
}
|
||||
const clusterId = this.get('cluster.id');
|
||||
this.get('auth').authenticate({ clusterId, backend: get(backend, 'type'), data }).then(
|
||||
({ isRoot }) => {
|
||||
({ isRoot, namespace }) => {
|
||||
this.set('loading', false);
|
||||
const transition = this.get('router').transitionTo(targetRoute);
|
||||
const transition = this.get('router').transitionTo(targetRoute, { queryParams: { namespace } });
|
||||
if (isRoot) {
|
||||
transition.followRedirects().then(() => {
|
||||
this.get('flashMessages').warning(
|
||||
|
|
|
@ -2,8 +2,6 @@ import Ember from 'ember';
|
|||
import keys from 'vault/lib/keycodes';
|
||||
|
||||
export default Ember.Component.extend({
|
||||
'data-test-component': 'console/command-input',
|
||||
classNames: 'console-ui-input',
|
||||
onExecuteCommand() {},
|
||||
onFullscreen() {},
|
||||
onValueUpdate() {},
|
||||
|
|
|
@ -11,12 +11,14 @@ export default Ember.Component.extend({
|
|||
successMessage: 'Saved!',
|
||||
deleteSuccessMessage: 'Deleted!',
|
||||
deleteButtonText: 'Delete',
|
||||
saveButtonText: 'Save',
|
||||
cancelLink: null,
|
||||
|
||||
/*
|
||||
* @param Function
|
||||
* @public
|
||||
*
|
||||
* Optional param to call a function upon successfully saving an entity
|
||||
* Optional param to call a function upon successfully saving a model
|
||||
*/
|
||||
onSave: () => {},
|
||||
|
||||
|
|
|
@ -1,18 +1,8 @@
|
|||
import Ember from 'ember';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
|
||||
const { computed } = Ember;
|
||||
|
||||
export default Ember.Component.extend({
|
||||
layout: hbs`<a href="{{href-to 'vault.cluster' 'vault'}}" class={{class}}>
|
||||
{{#if hasBlock}}
|
||||
{{yield}}
|
||||
{{else}}
|
||||
{{text}}
|
||||
{{/if}}
|
||||
</a>
|
||||
`,
|
||||
const { Component, computed } = Ember;
|
||||
|
||||
export default Component.extend({
|
||||
tagName: '',
|
||||
|
||||
text: computed(function() {
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
import Ember from 'ember';
|
||||
import { task } from 'ember-concurrency';
|
||||
|
||||
const { inject } = Ember;
|
||||
export default Ember.Component.extend({
|
||||
flashMessages: inject.service(),
|
||||
tagName: '',
|
||||
linkParams: null,
|
||||
componentName: null,
|
||||
hasMenu: false,
|
||||
|
||||
callMethod: task(function*(method, model, successMessage, failureMessage) {
|
||||
let flash = this.get('flashMessages');
|
||||
try {
|
||||
yield model[method]();
|
||||
flash.success(successMessage);
|
||||
} catch (e) {
|
||||
let errString = e.errors.join(' ');
|
||||
flash.danger(failureMessage + errString);
|
||||
model.rollbackAttributes();
|
||||
}
|
||||
}),
|
||||
});
|
|
@ -0,0 +1,5 @@
|
|||
import Ember from 'ember';
|
||||
|
||||
export default Ember.Component.extend({
|
||||
tagName: '',
|
||||
});
|
|
@ -0,0 +1,5 @@
|
|||
import Ember from 'ember';
|
||||
|
||||
export default Ember.Component.extend({
|
||||
tagName: '',
|
||||
});
|
|
@ -0,0 +1,14 @@
|
|||
import Ember from 'ember';
|
||||
import { pluralize } from 'ember-inflector';
|
||||
|
||||
const { computed } = Ember;
|
||||
export default Ember.Component.extend({
|
||||
tagName: '',
|
||||
items: null,
|
||||
itemNoun: 'item',
|
||||
|
||||
emptyMessage: computed('itemNoun', function() {
|
||||
let items = pluralize(this.get('itemNoun'));
|
||||
return `There are currently no ${items}`;
|
||||
}),
|
||||
});
|
|
@ -7,7 +7,7 @@ export default Ember.Component.extend({
|
|||
mounts: null,
|
||||
|
||||
// singleton mounts are not eligible for per-mount-filtering
|
||||
singletonMountTypes: ['cubbyhole', 'system', 'token', 'identity'],
|
||||
singletonMountTypes: ['cubbyhole', 'system', 'token', 'identity', 'ns_system', 'ns_identity'],
|
||||
|
||||
actions: {
|
||||
addOrRemovePath(path, e) {
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
import Ember from 'ember';
|
||||
|
||||
const { Component, computed, inject } = Ember;
|
||||
|
||||
export default Component.extend({
|
||||
namespaceService: inject.service('namespace'),
|
||||
currentNamespace: computed.alias('namespaceService.path'),
|
||||
|
||||
tagName: '',
|
||||
//public api
|
||||
targetNamespace: null,
|
||||
showLastSegment: false,
|
||||
|
||||
normalizedNamespace: computed('targetNamespace', function() {
|
||||
let ns = this.get('targetNamespace');
|
||||
return (ns || '').replace(/\.+/g, '/').replace('☃', '.');
|
||||
}),
|
||||
|
||||
namespaceDisplay: computed('normalizedNamespace', 'showLastSegment', function() {
|
||||
let ns = this.get('normalizedNamespace');
|
||||
let showLastSegment = this.get('showLastSegment');
|
||||
let parts = ns.split('/');
|
||||
if (ns === '') {
|
||||
return 'root';
|
||||
}
|
||||
return showLastSegment ? parts[parts.length - 1] : ns;
|
||||
}),
|
||||
|
||||
isCurrentNamespace: computed('targetNamespace', 'currentNamespace', function() {
|
||||
return this.get('currentNamespace') === this.get('targetNamespace');
|
||||
}),
|
||||
});
|
|
@ -0,0 +1,141 @@
|
|||
import Ember from 'ember';
|
||||
import keyUtils from 'vault/lib/key-utils';
|
||||
import pathToTree from 'vault/lib/path-to-tree';
|
||||
import { task, timeout } from 'ember-concurrency';
|
||||
|
||||
const { ancestorKeysForKey } = keyUtils;
|
||||
const { Component, computed, inject } = Ember;
|
||||
const DOT_REPLACEMENT = '☃';
|
||||
const ANIMATION_DURATION = 250;
|
||||
|
||||
export default Component.extend({
|
||||
tagName: '',
|
||||
namespaceService: inject.service('namespace'),
|
||||
auth: inject.service(),
|
||||
namespace: null,
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
this.get('namespaceService.findNamespacesForUser').perform();
|
||||
},
|
||||
|
||||
didReceiveAttrs() {
|
||||
this._super(...arguments);
|
||||
|
||||
let ns = this.get('namespace');
|
||||
let oldNS = this.get('oldNamespace');
|
||||
if (!oldNS || ns !== oldNS) {
|
||||
this.get('setForAnimation').perform();
|
||||
}
|
||||
this.set('oldNamespace', ns);
|
||||
},
|
||||
|
||||
setForAnimation: task(function*() {
|
||||
let leaves = this.get('menuLeaves');
|
||||
let lastLeaves = this.get('lastMenuLeaves');
|
||||
if (!lastLeaves) {
|
||||
this.set('lastMenuLeaves', leaves);
|
||||
yield timeout(0);
|
||||
return;
|
||||
}
|
||||
let isAdding = leaves.length > lastLeaves.length;
|
||||
let changedLeaf = (isAdding ? leaves : lastLeaves).get('lastObject');
|
||||
this.set('isAdding', isAdding);
|
||||
this.set('changedLeaf', changedLeaf);
|
||||
|
||||
// if we're adding we want to render immediately an animate it in
|
||||
// if we're not adding, we need time to move the item out before
|
||||
// a rerender removes it
|
||||
if (isAdding) {
|
||||
this.set('lastMenuLeaves', leaves);
|
||||
yield timeout(0);
|
||||
return;
|
||||
}
|
||||
yield timeout(ANIMATION_DURATION);
|
||||
this.set('lastMenuLeaves', leaves);
|
||||
}).drop(),
|
||||
|
||||
isAnimating: computed.alias('setForAnimation.isRunning'),
|
||||
|
||||
namespacePath: computed.alias('namespaceService.path'),
|
||||
|
||||
// this is an array of namespace paths that the current user
|
||||
// has access to
|
||||
accessibleNamespaces: computed.alias('namespaceService.accessibleNamespaces'),
|
||||
inRootNamespace: computed.alias('namespaceService.inRootNamespace'),
|
||||
|
||||
namespaceTree: computed('accessibleNamespaces', function() {
|
||||
let nsList = this.get('accessibleNamespaces');
|
||||
|
||||
if (!nsList) {
|
||||
return [];
|
||||
}
|
||||
return pathToTree(nsList);
|
||||
}),
|
||||
|
||||
maybeAddRoot(leaves) {
|
||||
let userRoot = this.get('auth.authData.userRootNamespace');
|
||||
if (userRoot === '') {
|
||||
leaves.unshift('');
|
||||
}
|
||||
|
||||
return leaves.uniq();
|
||||
},
|
||||
|
||||
pathToLeaf(path) {
|
||||
// dots are allowed in namespace paths
|
||||
// so we need to preserve them, and replace slashes with dots
|
||||
// in order to use Ember's get function on the namespace tree
|
||||
// to pull out the correct level
|
||||
return (
|
||||
path
|
||||
// trim trailing slash
|
||||
.replace(/\/$/, '')
|
||||
// replace dots with snowman
|
||||
.replace(/\.+/g, DOT_REPLACEMENT)
|
||||
// replace slash with dots
|
||||
.replace(/\/+/g, '.')
|
||||
);
|
||||
},
|
||||
|
||||
// an array that keeps track of what additional panels to render
|
||||
// on the menu stack
|
||||
// if you're in 'foo/bar/baz',
|
||||
// this array will be: ['foo', 'foo.bar', 'foo.bar.baz']
|
||||
// the template then iterates over this, and does Ember.get(namespaceTree, leaf)
|
||||
// to render the nodes of each leaf
|
||||
|
||||
// gets set as 'lastMenuLeaves' in the ember concurrency task above
|
||||
menuLeaves: computed('namespacePath', 'namespaceTree', function() {
|
||||
let ns = this.get('namespacePath');
|
||||
let leaves = ancestorKeysForKey(ns) || [];
|
||||
leaves.push(ns);
|
||||
leaves = this.maybeAddRoot(leaves);
|
||||
|
||||
leaves = leaves.map(this.pathToLeaf);
|
||||
return leaves;
|
||||
}),
|
||||
|
||||
// the nodes at the root of the namespace tree
|
||||
// these will get rendered as the bottom layer
|
||||
rootLeaves: computed('namespaceTree', function() {
|
||||
let tree = this.get('namespaceTree');
|
||||
let leaves = Object.keys(tree);
|
||||
return leaves;
|
||||
}),
|
||||
|
||||
currentLeaf: computed.alias('lastMenuLeaves.lastObject'),
|
||||
canAccessMultipleNamespaces: computed.gt('accessibleNamespaces.length', 1),
|
||||
isUserRootNamespace: computed('auth.authData.userRootNamespace', 'namespacePath', function() {
|
||||
return this.get('auth.authData.userRootNamespace') === this.get('namespacePath');
|
||||
}),
|
||||
|
||||
namespaceDisplay: computed('namespacePath', 'accessibleNamespaces', 'accessibleNamespaces.[]', function() {
|
||||
let namespace = this.get('namespacePath');
|
||||
if (namespace === '') {
|
||||
return '';
|
||||
}
|
||||
let parts = namespace.split('/');
|
||||
return parts[parts.length - 1];
|
||||
}),
|
||||
});
|
|
@ -0,0 +1,18 @@
|
|||
import Ember from 'ember';
|
||||
|
||||
const { Component, inject, computed } = Ember;
|
||||
|
||||
export default Component.extend({
|
||||
namespace: inject.service(),
|
||||
showMessage: computed.not('namespace.inRootNamespace'),
|
||||
//public API
|
||||
noun: null,
|
||||
mode: 'edit',
|
||||
modeVerb: computed(function() {
|
||||
let mode = this.get('mode');
|
||||
if (!mode) {
|
||||
return '';
|
||||
}
|
||||
return mode.endsWith('e') ? `${mode}d` : `${mode}ed`;
|
||||
}),
|
||||
});
|
|
@ -1,6 +1,5 @@
|
|||
import Ember from 'ember';
|
||||
import { hrefTo } from 'vault/helpers/href-to';
|
||||
const { computed, get, getProperties } = Ember;
|
||||
const { computed, get, getProperties, Component, inject } = Ember;
|
||||
|
||||
const replicationAttr = function(attr) {
|
||||
return computed('mode', `cluster.{dr,performance}.${attr}`, function() {
|
||||
|
@ -8,8 +7,10 @@ const replicationAttr = function(attr) {
|
|||
return get(cluster, `${mode}.${attr}`);
|
||||
});
|
||||
};
|
||||
export default Ember.Component.extend({
|
||||
version: Ember.inject.service(),
|
||||
export default Component.extend({
|
||||
version: inject.service(),
|
||||
router: inject.service(),
|
||||
namespace: inject.service(),
|
||||
classNames: ['level', 'box-label'],
|
||||
classNameBindings: ['isMenu:is-mobile'],
|
||||
attributeBindings: ['href', 'target'],
|
||||
|
@ -22,7 +23,12 @@ export default Ember.Component.extend({
|
|||
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 this.get('router').urlFor(
|
||||
'vault.cluster.replication.mode.index',
|
||||
this.get('cluster.name'),
|
||||
mode,
|
||||
{ queryParams: { namespace: this.get('namespace.path') } }
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}),
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
import Ember from 'ember';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
import { hrefTo } from 'vault/helpers/href-to';
|
||||
const { computed } = Ember;
|
||||
const { computed, Component } = Ember;
|
||||
|
||||
export function linkParams({ mode, secret, queryParams }) {
|
||||
let params;
|
||||
|
@ -20,7 +18,8 @@ export function linkParams({ mode, secret, queryParams }) {
|
|||
return params;
|
||||
}
|
||||
|
||||
export default Ember.Component.extend({
|
||||
export default Component.extend({
|
||||
tagName: '',
|
||||
mode: 'list',
|
||||
|
||||
secret: null,
|
||||
|
@ -28,16 +27,7 @@ export default Ember.Component.extend({
|
|||
ariaLabel: null,
|
||||
|
||||
linkParams: computed('mode', 'secret', 'queryParams', function() {
|
||||
return linkParams(this.getProperties('mode', 'secret', 'queryParams'));
|
||||
let data = this.getProperties('mode', 'secret', 'queryParams');
|
||||
return linkParams(data);
|
||||
}),
|
||||
|
||||
attributeBindings: ['href', 'aria-label:ariaLabel'],
|
||||
|
||||
href: computed('linkParams', function() {
|
||||
return hrefTo(this, ...this.get('linkParams'));
|
||||
}),
|
||||
|
||||
layout: hbs`{{yield}}`,
|
||||
|
||||
tagName: 'a',
|
||||
});
|
||||
|
|
|
@ -1,41 +1,16 @@
|
|||
import Ember from 'ember';
|
||||
import config from '../config/environment';
|
||||
|
||||
const { computed, inject } = Ember;
|
||||
export default Ember.Controller.extend({
|
||||
const { Controller, computed, inject } = Ember;
|
||||
export default Controller.extend({
|
||||
env: config.environment,
|
||||
auth: inject.service(),
|
||||
vaultVersion: inject.service('version'),
|
||||
console: inject.service(),
|
||||
consoleOpen: computed.alias('console.isOpen'),
|
||||
store: inject.service(),
|
||||
activeCluster: computed('auth.activeCluster', function() {
|
||||
return this.store.peekRecord('cluster', this.get('auth.activeCluster'));
|
||||
return this.get('store').peekRecord('cluster', this.get('auth.activeCluster'));
|
||||
}),
|
||||
activeClusterName: computed('auth.activeCluster', function() {
|
||||
const activeCluster = this.store.peekRecord('cluster', this.get('auth.activeCluster'));
|
||||
activeClusterName: computed('activeCluster', function() {
|
||||
const activeCluster = this.get('activeCluster');
|
||||
return activeCluster ? activeCluster.get('name') : null;
|
||||
}),
|
||||
showNav: 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;
|
||||
}
|
||||
}
|
||||
),
|
||||
actions: {
|
||||
toggleConsole() {
|
||||
this.toggleProperty('consoleOpen');
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,8 +1,63 @@
|
|||
import Ember from 'ember';
|
||||
|
||||
const { inject, Controller } = Ember;
|
||||
|
||||
const { Controller, computed, observer, inject } = Ember;
|
||||
export default Controller.extend({
|
||||
auth: inject.service(),
|
||||
version: inject.service(),
|
||||
store: inject.service(),
|
||||
media: inject.service(),
|
||||
namespaceService: inject.service('namespace'),
|
||||
|
||||
vaultVersion: inject.service('version'),
|
||||
console: inject.service(),
|
||||
|
||||
queryParams: [
|
||||
{
|
||||
namespaceQueryParam: {
|
||||
scope: 'controller',
|
||||
as: 'namespace',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
namespaceQueryParam: '',
|
||||
|
||||
onQPChange: observer('namespaceQueryParam', function() {
|
||||
this.get('namespaceService').setNamespace(this.get('namespaceQueryParam'));
|
||||
}),
|
||||
|
||||
consoleOpen: computed.alias('console.isOpen'),
|
||||
|
||||
activeCluster: computed('auth.activeCluster', function() {
|
||||
return this.get('store').peekRecord('cluster', this.get('auth.activeCluster'));
|
||||
}),
|
||||
|
||||
activeClusterName: computed('activeCluster', function() {
|
||||
const activeCluster = this.get('activeCluster');
|
||||
return activeCluster ? activeCluster.get('name') : null;
|
||||
}),
|
||||
|
||||
showNav: 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;
|
||||
}
|
||||
}
|
||||
),
|
||||
|
||||
actions: {
|
||||
toggleConsole() {
|
||||
this.toggleProperty('consoleOpen');
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
import Ember from 'ember';
|
||||
|
||||
const { inject, Controller } = Ember;
|
||||
export default Controller.extend({
|
||||
namespaceService: inject.service('namespace'),
|
||||
actions: {
|
||||
onSave({ saveType }) {
|
||||
if (saveType === 'save') {
|
||||
// fetch new namespaces for the namespace picker
|
||||
this.get('namespaceService.findNamespacesForUser').perform();
|
||||
return this.transitionToRoute('vault.cluster.access.namespaces.index');
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
|
@ -1,9 +1,20 @@
|
|||
import Ember from 'ember';
|
||||
import { task, timeout } from 'ember-concurrency';
|
||||
|
||||
export default Ember.Controller.extend({
|
||||
vaultController: Ember.inject.controller('vault'),
|
||||
const { inject, computed, Controller } = Ember;
|
||||
export default Controller.extend({
|
||||
vaultController: inject.controller('vault'),
|
||||
clusterController: inject.controller('vault.cluster'),
|
||||
namespaceService: inject.service('namespace'),
|
||||
namespaceQueryParam: computed.alias('clusterController.namespaceQueryParam'),
|
||||
queryParams: [{ authMethod: 'with' }],
|
||||
wrappedToken: Ember.computed.alias('vaultController.wrappedToken'),
|
||||
wrappedToken: computed.alias('vaultController.wrappedToken'),
|
||||
authMethod: '',
|
||||
redirectTo: null,
|
||||
|
||||
updateNamespace: task(function*(value) {
|
||||
yield timeout(200);
|
||||
this.get('namespaceService').setNamespace(value, true);
|
||||
this.set('namespaceQueryParam', value);
|
||||
}).restartable(),
|
||||
});
|
||||
|
|
|
@ -8,7 +8,7 @@ export default Controller.extend({
|
|||
|
||||
supportedBackends: computed('displayableBackends', 'displayableBackends.[]', function() {
|
||||
return (this.get('displayableBackends') || [])
|
||||
.filter(backend => LINKED_BACKENDS.includes(backend.get('type')))
|
||||
.filter(backend => LINKED_BACKENDS.includes(backend.get('engineType')))
|
||||
.sortBy('id');
|
||||
}),
|
||||
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
import Ember from 'ember';
|
||||
|
||||
const { inject, Controller } = Ember;
|
||||
export default Controller.extend({
|
||||
namespaceService: inject.service('namespace'),
|
||||
});
|
|
@ -11,6 +11,7 @@ const FEATURES = [
|
|||
'GCP CKMS Autounseal',
|
||||
'Seal Wrapping',
|
||||
'Control Groups',
|
||||
'Namespaces',
|
||||
];
|
||||
|
||||
export function hasFeature(featureName, features) {
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
import flat from 'flat';
|
||||
import deepmerge from 'deepmerge';
|
||||
|
||||
const { unflatten } = flat;
|
||||
const DOT_REPLACEMENT = '☃';
|
||||
|
||||
//function that takes a list of path and returns a deeply nested object
|
||||
//representing a tree of all of those paths
|
||||
//
|
||||
//
|
||||
// given ["foo", "bar", "foo1", "foo/bar", "foo/baz", "foo/bar/baz"]
|
||||
//
|
||||
// returns {
|
||||
// bar: null,
|
||||
// foo: {
|
||||
// bar: {
|
||||
// baz: null
|
||||
// },
|
||||
// baz: null,
|
||||
// },
|
||||
// foo1: null,
|
||||
// }
|
||||
export default function(paths) {
|
||||
// first sort the list by length, then alphanumeric
|
||||
let list = paths.slice(0).sort((a, b) => b.length - a.length || b.localeCompare(a));
|
||||
// then reduce to an array
|
||||
// and we remove all of the items that have a string
|
||||
// that starts with the same prefix from the list
|
||||
// so if we have "foo/bar/baz", both "foo" and "foo/bar"
|
||||
// won't be included in the list
|
||||
let tree = list.reduce((accumulator, ns) => {
|
||||
let nsWithPrefix = accumulator.find(path => path.startsWith(ns));
|
||||
// we need to make sure it's a match for the full path part
|
||||
let isFullMatch = nsWithPrefix && nsWithPrefix.charAt(ns.length) === '/';
|
||||
if (!isFullMatch) {
|
||||
accumulator.push(ns);
|
||||
}
|
||||
return accumulator;
|
||||
}, []);
|
||||
|
||||
// after the reduction we're left with an array that contains
|
||||
// strings that represent the longest branches
|
||||
// we'll replace the dots in the paths, then expand the path
|
||||
// to a nested object that we can then query with Ember.get
|
||||
return deepmerge.all(
|
||||
tree.map(p => {
|
||||
p = p.replace(/\.+/g, DOT_REPLACEMENT);
|
||||
return unflatten({ [p]: null }, { delimiter: '/' });
|
||||
})
|
||||
);
|
||||
}
|
|
@ -1,3 +1,16 @@
|
|||
// usage:
|
||||
//
|
||||
// import lazyCapabilities, { apiPath } from 'vault/macros/lazy-capabilities';
|
||||
//
|
||||
// export default DS.Model.extend({
|
||||
// //pass the template string as the first arg, and be sure to use '' around the
|
||||
// //paramerters that get interpolated in the string - that's how the template function
|
||||
// //knows where to put each value
|
||||
// zeroAddressPath: lazyCapabilities(apiPath`${'id'}/config/zeroaddress`, 'id'),
|
||||
//
|
||||
// });
|
||||
//
|
||||
|
||||
import { queryRecord } from 'ember-computed-query';
|
||||
|
||||
export function apiPath(strings, ...keys) {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import Ember from 'ember';
|
||||
|
||||
const { get } = Ember;
|
||||
const { get, inject, Mixin, RSVP } = Ember;
|
||||
const INIT = 'vault.cluster.init';
|
||||
const UNSEAL = 'vault.cluster.unseal';
|
||||
const AUTH = 'vault.cluster.auth';
|
||||
|
@ -9,15 +9,16 @@ 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(),
|
||||
export default Mixin.create({
|
||||
auth: inject.service(),
|
||||
|
||||
transitionToTargetRoute() {
|
||||
const targetRoute = this.targetRouteName();
|
||||
if (targetRoute && targetRoute !== this.routeName) {
|
||||
return this.transitionTo(targetRoute);
|
||||
}
|
||||
return Ember.RSVP.resolve();
|
||||
|
||||
return RSVP.resolve();
|
||||
},
|
||||
|
||||
beforeModel() {
|
||||
|
|
|
@ -5,6 +5,7 @@ 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';
|
||||
import lazyCapabilities, { apiPath } from 'vault/macros/lazy-capabilities';
|
||||
|
||||
const { attr, hasMany } = DS;
|
||||
const { computed } = Ember;
|
||||
|
@ -27,6 +28,11 @@ export default DS.Model.extend({
|
|||
defaultValue: METHODS[0].value,
|
||||
possibleValues: METHODS,
|
||||
}),
|
||||
// namespaces introduced types with a `ns_` prefix for built-in engines
|
||||
// so we need to strip that to normalize the type
|
||||
methodType: computed('type', function() {
|
||||
return this.get('type').replace(/^ns_/, '');
|
||||
}),
|
||||
description: attr('string', {
|
||||
editType: 'textarea',
|
||||
}),
|
||||
|
@ -108,17 +114,8 @@ export default DS.Model.extend({
|
|||
'id',
|
||||
'configPathTmpl'
|
||||
),
|
||||
deletePath: queryRecord(
|
||||
'capabilities',
|
||||
context => {
|
||||
const { id } = context.get('id');
|
||||
return {
|
||||
id: `sys/auth/${id}`,
|
||||
};
|
||||
},
|
||||
'id'
|
||||
),
|
||||
canDisable: computed.alias('deletePath.canDelete'),
|
||||
|
||||
deletePath: lazyCapabilities(apiPath`sys/auth/${'id'}`, 'id'),
|
||||
canDisable: computed.alias('deletePath.canDelete'),
|
||||
canEdit: computed.alias('configPath.canUpdate'),
|
||||
});
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
import Ember from 'ember';
|
||||
import DS from 'ember-data';
|
||||
|
||||
const { attr } = DS;
|
||||
const { computed } = Ember;
|
||||
import { expandAttributeMeta } from 'vault/utils/field-to-attrs';
|
||||
|
||||
export default DS.Model.extend({
|
||||
path: attr('string', {
|
||||
validationAttr: 'pathIsValid',
|
||||
invalidMessage: 'You have entered and invalid path please only include letters, numbers, -, ., and _.',
|
||||
}),
|
||||
pathIsValid: computed('path', function() {
|
||||
return this.get('path') && this.get('path').match(/^[\w\d-.]+$/g);
|
||||
}),
|
||||
description: attr('string', {
|
||||
editType: 'textarea',
|
||||
}),
|
||||
fields: computed(function() {
|
||||
return expandAttributeMeta(this, ['path']);
|
||||
}),
|
||||
});
|
|
@ -23,8 +23,8 @@ export default DS.Model.extend({
|
|||
local: attr('boolean'),
|
||||
sealWrap: attr('boolean'),
|
||||
|
||||
modelTypeForKV: computed('type', 'options.version', function() {
|
||||
let type = this.get('type');
|
||||
modelTypeForKV: computed('engineType', 'options.version', function() {
|
||||
let type = this.get('engineType');
|
||||
let version = this.get('options.version');
|
||||
let modelType = 'secret';
|
||||
if ((type === 'kv' || type === 'generic') && version === 2) {
|
||||
|
@ -48,8 +48,14 @@ export default DS.Model.extend({
|
|||
return expandAttributeMeta(this, this.get('formFields'));
|
||||
}),
|
||||
|
||||
shouldIncludeInList: computed('type', function() {
|
||||
return !LIST_EXCLUDED_BACKENDS.includes(this.get('type'));
|
||||
// namespaces introduced types with a `ns_` prefix for built-in engines
|
||||
// so we need to strip that to normalize the type
|
||||
engineType: computed('type', function() {
|
||||
return (this.get('type') || '').replace(/^ns_/, '');
|
||||
}),
|
||||
|
||||
shouldIncludeInList: computed('engineType', function() {
|
||||
return !LIST_EXCLUDED_BACKENDS.includes(this.get('engineType'));
|
||||
}),
|
||||
|
||||
localDisplay: Ember.computed('local', function() {
|
||||
|
|
|
@ -66,6 +66,10 @@ Router.map(function() {
|
|||
});
|
||||
this.route('control-groups');
|
||||
this.route('control-group-accessor', { path: '/control-groups/:accessor' });
|
||||
this.route('namespaces', function() {
|
||||
this.route('index', { path: '/' });
|
||||
this.route('create');
|
||||
});
|
||||
});
|
||||
this.route('secrets', function() {
|
||||
this.route('backends', { path: '/' });
|
||||
|
|
|
@ -4,19 +4,55 @@ import ControlGroupError from 'vault/lib/control-group-error';
|
|||
const { inject } = Ember;
|
||||
export default Ember.Route.extend({
|
||||
controlGroup: inject.service(),
|
||||
routing: inject.service('router'),
|
||||
namespaceService: inject.service('namespace'),
|
||||
|
||||
actions: {
|
||||
willTransition() {
|
||||
window.scrollTo(0, 0);
|
||||
},
|
||||
error(err, transition) {
|
||||
error(error, transition) {
|
||||
let controlGroup = this.get('controlGroup');
|
||||
if (err instanceof ControlGroupError) {
|
||||
return controlGroup.handleError(err, transition);
|
||||
if (error instanceof ControlGroupError) {
|
||||
return controlGroup.handleError(error, transition);
|
||||
}
|
||||
if (err.path === '/v1/sys/wrapping/unwrap') {
|
||||
if (error.path === '/v1/sys/wrapping/unwrap') {
|
||||
controlGroup.unmarkTokenForUnwrap();
|
||||
}
|
||||
|
||||
let router = this.get('routing');
|
||||
let errorURL = transition.intent.url;
|
||||
let { name, contexts, queryParams } = transition.intent;
|
||||
|
||||
// If the transition is internal to Ember, we need to generate the URL
|
||||
// from the route parameters ourselves
|
||||
if (!errorURL) {
|
||||
try {
|
||||
errorURL = router.urlFor(name, ...(contexts || []), { queryParams });
|
||||
} catch (e) {
|
||||
// If this fails, something weird is happening with URL transitions
|
||||
errorURL = null;
|
||||
}
|
||||
}
|
||||
// because we're using rootURL, we need to trim this from the front to get
|
||||
// the ember-routeable url
|
||||
if (errorURL) {
|
||||
errorURL = errorURL.replace('/ui', '');
|
||||
}
|
||||
|
||||
error.errorURL = errorURL;
|
||||
|
||||
// if we have queryParams, update the namespace so that the observer can fire on the controller
|
||||
if (queryParams) {
|
||||
this.controllerFor('vault.cluster').set('namespaceQueryParam', queryParams.namespace || '');
|
||||
}
|
||||
|
||||
// Assuming we have a URL, push it into browser history and update the
|
||||
// location bar for the user
|
||||
if (errorURL) {
|
||||
router.get('location').setURL(errorURL);
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
},
|
||||
|
|
|
@ -3,14 +3,22 @@ import ClusterRoute from 'vault/mixins/cluster-route';
|
|||
import ModelBoundaryRoute from 'vault/mixins/model-boundary-route';
|
||||
|
||||
const POLL_INTERVAL_MS = 10000;
|
||||
const { inject } = Ember;
|
||||
const { inject, Route, getOwner } = Ember;
|
||||
|
||||
export default Ember.Route.extend(ModelBoundaryRoute, ClusterRoute, {
|
||||
export default Route.extend(ModelBoundaryRoute, ClusterRoute, {
|
||||
namespaceService: inject.service('namespace'),
|
||||
version: inject.service(),
|
||||
store: inject.service(),
|
||||
auth: inject.service(),
|
||||
currentCluster: Ember.inject.service(),
|
||||
currentCluster: inject.service(),
|
||||
modelTypes: ['node', 'secret', 'secret-engine'],
|
||||
globalNamespaceModels: ['node', 'cluster'],
|
||||
|
||||
queryParams: {
|
||||
namespaceQueryParam: {
|
||||
refreshModel: true,
|
||||
},
|
||||
},
|
||||
|
||||
getClusterId(params) {
|
||||
const { cluster_name } = params;
|
||||
|
@ -18,8 +26,24 @@ export default Ember.Route.extend(ModelBoundaryRoute, ClusterRoute, {
|
|||
return cluster ? cluster.get('id') : null;
|
||||
},
|
||||
|
||||
clearNonGlobalModels() {
|
||||
// this method clears all of the ember data cached models except
|
||||
// the model types blacklisted in `globalNamespaceModels`
|
||||
let store = this.store;
|
||||
let modelsToKeep = this.get('globalNamespaceModels');
|
||||
for (let model of getOwner(this).lookup('data-adapter:main').getModelTypes()) {
|
||||
let { name } = model;
|
||||
if (modelsToKeep.includes(name)) {
|
||||
return;
|
||||
}
|
||||
store.unloadAll(name);
|
||||
}
|
||||
},
|
||||
|
||||
beforeModel() {
|
||||
const params = this.paramsFor(this.routeName);
|
||||
this.clearNonGlobalModels();
|
||||
this.get('namespaceService').setNamespace(params.namespaceQueryParam);
|
||||
const id = this.getClusterId(params);
|
||||
if (id) {
|
||||
this.get('auth').setCluster(id);
|
||||
|
@ -61,6 +85,12 @@ export default Ember.Route.extend(ModelBoundaryRoute, ClusterRoute, {
|
|||
this.get('currentCluster').setCluster(model);
|
||||
this._super(...arguments);
|
||||
this.poll();
|
||||
|
||||
// Check that namespaces is enabled and if not,
|
||||
// clear the namespace by transition to this route w/o it
|
||||
if (this.get('namespaceService.path') && !this.get('version.hasNamespaces')) {
|
||||
return this.transitionTo(this.routeName, { queryParams: { namespace: '' } });
|
||||
}
|
||||
return this.transitionToTargetRoute();
|
||||
},
|
||||
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
import Ember from 'ember';
|
||||
import UnloadModel from 'vault/mixins/unload-model-route';
|
||||
|
||||
const { inject } = Ember;
|
||||
|
||||
export default Ember.Route.extend(UnloadModel, {
|
||||
version: inject.service(),
|
||||
beforeModel() {
|
||||
return this.get('version').fetchFeatures().then(() => {
|
||||
return this._super(...arguments);
|
||||
});
|
||||
},
|
||||
model() {
|
||||
return this.get('version.hasNamespaces') ? this.store.createRecord('namespace') : null;
|
||||
},
|
||||
});
|
|
@ -0,0 +1,24 @@
|
|||
import Ember from 'ember';
|
||||
import UnloadModel from 'vault/mixins/unload-model-route';
|
||||
|
||||
const { inject } = Ember;
|
||||
|
||||
export default Ember.Route.extend(UnloadModel, {
|
||||
version: inject.service(),
|
||||
beforeModel() {
|
||||
this.store.unloadAll('namespace');
|
||||
return this.get('version').fetchFeatures().then(() => {
|
||||
return this._super(...arguments);
|
||||
});
|
||||
},
|
||||
model() {
|
||||
return this.get('version.hasNamespaces')
|
||||
? this.store.findAll('namespace').catch(e => {
|
||||
if (e.httpStatus === 404) {
|
||||
return [];
|
||||
}
|
||||
throw e;
|
||||
})
|
||||
: null;
|
||||
},
|
||||
});
|
|
@ -2,28 +2,18 @@ import ClusterRouteBase from './cluster-route-base';
|
|||
import Ember from 'ember';
|
||||
import config from 'vault/config/environment';
|
||||
|
||||
const { RSVP, inject } = Ember;
|
||||
const { inject } = Ember;
|
||||
|
||||
export default ClusterRouteBase.extend({
|
||||
flashMessages: inject.service(),
|
||||
version: inject.service(),
|
||||
beforeModel() {
|
||||
this.store.unloadAll('auth-method');
|
||||
return this._super();
|
||||
return this._super().then(() => {
|
||||
return this.get('version').fetchFeatures();
|
||||
});
|
||||
},
|
||||
model() {
|
||||
let cluster = this._super(...arguments);
|
||||
return this.store
|
||||
.findAll('auth-method', {
|
||||
adapterOptions: {
|
||||
unauthenticated: true,
|
||||
},
|
||||
})
|
||||
.then(result => {
|
||||
return RSVP.hash({
|
||||
cluster,
|
||||
methods: result,
|
||||
});
|
||||
});
|
||||
return this._super(...arguments);
|
||||
},
|
||||
resetController(controller) {
|
||||
controller.set('wrappedToken', '');
|
||||
|
|
|
@ -22,7 +22,7 @@ export default Ember.Route.extend({
|
|||
let { backend } = this.paramsFor('vault.cluster.secrets.backend');
|
||||
let { secret } = this.paramsFor(this.routeName);
|
||||
let backendModel = this.store.peekRecord('secret-engine', backend);
|
||||
let type = backendModel && backendModel.get('type');
|
||||
let type = backendModel && backendModel.get('engineType');
|
||||
if (!type || !SUPPORTED_BACKENDS.includes(type)) {
|
||||
return this.transitionTo('vault.cluster.secrets');
|
||||
}
|
||||
|
@ -33,7 +33,7 @@ export default Ember.Route.extend({
|
|||
|
||||
getModelType(backend, tab) {
|
||||
let backendModel = this.store.peekRecord('secret-engine', backend);
|
||||
let type = backendModel.get('type');
|
||||
let type = backendModel.get('engineType');
|
||||
let types = {
|
||||
transit: 'transit-key',
|
||||
ssh: 'role-ssh',
|
||||
|
@ -115,7 +115,7 @@ export default Ember.Route.extend({
|
|||
backend,
|
||||
backendModel,
|
||||
baseKey: { id: secret },
|
||||
backendType: backendModel.get('type'),
|
||||
backendType: backendModel.get('engineType'),
|
||||
});
|
||||
if (!has404) {
|
||||
const pageFilter = secretParams.pageFilter;
|
||||
|
|
|
@ -6,7 +6,7 @@ export default Ember.Route.extend(UnloadModelRoute, {
|
|||
capabilities(secret) {
|
||||
const { backend } = this.paramsFor('vault.cluster.secrets.backend');
|
||||
let backendModel = this.modelFor('vault.cluster.secrets.backend');
|
||||
let backendType = backendModel.get('type');
|
||||
let backendType = backendModel.get('engineType');
|
||||
let version = backendModel.get('options.version');
|
||||
let path;
|
||||
if (backendType === 'transit') {
|
||||
|
@ -22,7 +22,7 @@ export default Ember.Route.extend(UnloadModelRoute, {
|
|||
},
|
||||
|
||||
backendType() {
|
||||
return this.modelFor('vault.cluster.secrets.backend').get('type');
|
||||
return this.modelFor('vault.cluster.secrets.backend').get('engineType');
|
||||
},
|
||||
|
||||
templateName: 'vault/cluster/secrets/backend/secretEditLayout',
|
||||
|
@ -45,7 +45,7 @@ export default Ember.Route.extend(UnloadModelRoute, {
|
|||
|
||||
modelType(backend, secret) {
|
||||
let backendModel = this.modelFor('vault.cluster.secrets.backend', backend);
|
||||
let type = backendModel.get('type');
|
||||
let type = backendModel.get('engineType');
|
||||
let types = {
|
||||
transit: 'transit-key',
|
||||
ssh: 'role-ssh',
|
||||
|
|
|
@ -10,7 +10,7 @@ export default Ember.Route.extend({
|
|||
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');
|
||||
const modelType = model && model.get('methodType');
|
||||
if (!model || (modelType !== 'token' && !METHODS.findBy('type', modelType))) {
|
||||
const error = new DS.AdapterError();
|
||||
Ember.set(error, 'httpStatus', 404);
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
import ApplicationSerializer from './application';
|
||||
|
||||
export default ApplicationSerializer.extend({
|
||||
normalizeList(payload) {
|
||||
const data = payload.data.keys
|
||||
? payload.data.keys.map(key => ({
|
||||
path: key,
|
||||
// remove the trailing slash from the id
|
||||
id: key.replace(/\/$/, ''),
|
||||
}))
|
||||
: payload.data;
|
||||
|
||||
return data;
|
||||
},
|
||||
|
||||
normalizeResponse(store, primaryModelClass, payload, id, requestType) {
|
||||
const nullResponses = ['deleteRecord', 'createRecord'];
|
||||
let cid = (id || payload.id || '').replace(/\/$/, '');
|
||||
let normalizedPayload = nullResponses.includes(requestType)
|
||||
? { id: cid, path: cid }
|
||||
: this.normalizeList(payload);
|
||||
return this._super(store, primaryModelClass, normalizedPayload, id, requestType);
|
||||
},
|
||||
});
|
|
@ -3,7 +3,7 @@ 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 { get, isArray, computed, getOwner, Service, inject } = Ember;
|
||||
|
||||
const TOKEN_SEPARATOR = '☃';
|
||||
const TOKEN_PREFIX = 'vault-';
|
||||
|
@ -13,7 +13,8 @@ const BACKENDS = supportedAuthBackends();
|
|||
|
||||
export { TOKEN_SEPARATOR, TOKEN_PREFIX, ROOT_PREFIX };
|
||||
|
||||
export default Ember.Service.extend({
|
||||
export default Service.extend({
|
||||
namespace: inject.service(),
|
||||
expirationCalcTS: null,
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
|
@ -69,17 +70,25 @@ export default Ember.Service.extend({
|
|||
'X-Vault-Token': this.get('currentToken'),
|
||||
},
|
||||
};
|
||||
|
||||
let namespace =
|
||||
typeof options.namespace === 'undefined' ? this.get('namespaceService.path') : options.namespace;
|
||||
if (namespace) {
|
||||
defaults.headers['X-Vault-Namespace'] = namespace;
|
||||
}
|
||||
return Ember.$.ajax(Ember.assign(defaults, options));
|
||||
},
|
||||
|
||||
renewCurrentToken() {
|
||||
let namespace = this.get('authData.userRootNamespace');
|
||||
const url = '/v1/auth/token/renew-self';
|
||||
return this.ajax(url, 'POST');
|
||||
return this.ajax(url, 'POST', { namespace });
|
||||
},
|
||||
|
||||
revokeCurrentToken() {
|
||||
let namespace = this.get('authData.userRootNamespace');
|
||||
const url = '/v1/auth/token/revoke-self';
|
||||
return this.ajax(url, 'POST');
|
||||
return this.ajax(url, 'POST', { namespace });
|
||||
},
|
||||
|
||||
calculateExpiration(resp, creationTime) {
|
||||
|
@ -97,8 +106,9 @@ export default Ember.Service.extend({
|
|||
},
|
||||
|
||||
persistAuthData() {
|
||||
const [firstArg, resp] = arguments;
|
||||
let [firstArg, resp] = arguments;
|
||||
let tokens = this.get('tokens');
|
||||
let currentNamespace = this.get('namespace.path') || '';
|
||||
let tokenName;
|
||||
let options;
|
||||
let backend;
|
||||
|
@ -110,7 +120,7 @@ export default Ember.Service.extend({
|
|||
backend = options.backend;
|
||||
}
|
||||
|
||||
const currentBackend = BACKENDS.findBy('type', backend);
|
||||
let currentBackend = BACKENDS.findBy('type', backend);
|
||||
let displayName;
|
||||
if (isArray(currentBackend.displayNamePath)) {
|
||||
displayName = currentBackend.displayNamePath.map(name => get(resp, name)).join('/');
|
||||
|
@ -118,8 +128,26 @@ export default Ember.Service.extend({
|
|||
displayName = get(resp, currentBackend.displayNamePath);
|
||||
}
|
||||
|
||||
const { entity_id, policies, renewable } = resp;
|
||||
let { entity_id, policies, renewable, namespace_path } = resp;
|
||||
// here we prefer namespace_path if its defined,
|
||||
// else we look and see if there's already a namespace saved
|
||||
// and then finally we'll use the current query param if the others
|
||||
// haven't set a value yet
|
||||
// all of the typeof checks are necessary because the root namespace is ''
|
||||
let userRootNamespace = namespace_path && namespace_path.replace(/\/$/, '');
|
||||
// if we're logging in with token and there's no namespace_path, we can assume
|
||||
// that the token belongs to the root namespace
|
||||
if (backend === 'token' && !userRootNamespace) {
|
||||
userRootNamespace = '';
|
||||
}
|
||||
if (typeof userRootNamespace === 'undefined') {
|
||||
userRootNamespace = this.get('authData.userRootNamespace');
|
||||
}
|
||||
if (typeof userRootNamespace === 'undefined') {
|
||||
userRootNamespace = currentNamespace;
|
||||
}
|
||||
let data = {
|
||||
userRootNamespace,
|
||||
displayName,
|
||||
backend: currentBackend,
|
||||
token: resp.client_token || get(resp, currentBackend.tokenPath),
|
||||
|
@ -148,6 +176,7 @@ export default Ember.Service.extend({
|
|||
this.set('allowExpiration', false);
|
||||
this.setTokenData(tokenName, data);
|
||||
return Ember.RSVP.resolve({
|
||||
namespace: currentNamespace || data.userRootNamespace,
|
||||
token: tokenName,
|
||||
isRoot: policies.includes('root'),
|
||||
});
|
||||
|
@ -253,7 +282,7 @@ export default Ember.Service.extend({
|
|||
const adapter = this.clusterAdapter();
|
||||
|
||||
return adapter.authenticate(options).then(resp => {
|
||||
return this.persistAuthData(options, resp.auth || resp.data);
|
||||
return this.persistAuthData(options, resp.auth || resp.data, this.get('namespace.path'));
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -269,6 +298,7 @@ export default Ember.Service.extend({
|
|||
this.set('tokens', tokenNames);
|
||||
},
|
||||
|
||||
// returns the key for the token to use
|
||||
currentTokenName: computed('activeCluster', 'tokens', 'tokens.[]', function() {
|
||||
const regex = new RegExp(this.get('activeCluster'));
|
||||
return this.get('tokens').find(key => regex.test(key));
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
import Ember from 'ember';
|
||||
import { task } from 'ember-concurrency';
|
||||
|
||||
const { Service, computed, inject } = Ember;
|
||||
const ROOT_NAMESPACE = '';
|
||||
export default Service.extend({
|
||||
store: inject.service(),
|
||||
auth: inject.service(),
|
||||
userRootNamespace: computed.alias('auth.authData.userRootNamespace'),
|
||||
//populated by the query param on the cluster route
|
||||
path: null,
|
||||
// list of namespaces available to the current user under the
|
||||
// current namespace
|
||||
accessibleNamespaces: null,
|
||||
|
||||
inRootNamespace: computed.equal('path', ROOT_NAMESPACE),
|
||||
|
||||
setNamespace(path) {
|
||||
this.set('path', path);
|
||||
},
|
||||
|
||||
findNamespacesForUser: task(function*() {
|
||||
// uses the adapter and the raw response here since
|
||||
// models get wiped when switching namespaces and we
|
||||
// want to keep track of these separately
|
||||
let store = this.get('store');
|
||||
let adapter = store.adapterFor('namespace');
|
||||
try {
|
||||
let ns = yield adapter.findAll(store, 'namespace', null, {
|
||||
adapterOptions: {
|
||||
forUser: true,
|
||||
namespace: this.get('userRootNamespace'),
|
||||
},
|
||||
});
|
||||
this.set('accessibleNamespaces', ns.data.keys.map(n => n.replace(/\/$/, '')));
|
||||
} catch (e) {
|
||||
//do nothing here
|
||||
}
|
||||
}).drop(),
|
||||
});
|
|
@ -23,6 +23,7 @@ export default Service.extend({
|
|||
hasDRReplication: hasFeature('DR Replication'),
|
||||
|
||||
hasSentinel: hasFeature('Sentinel'),
|
||||
hasNamespaces: hasFeature('Namespaces'),
|
||||
|
||||
isEnterprise: computed.match('version', /\+.+$/),
|
||||
|
||||
|
|
|
@ -2,4 +2,18 @@
|
|||
@extend .box;
|
||||
@extend .is-bottomless;
|
||||
padding: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.auth-form .vault-loader {
|
||||
position: absolute;
|
||||
top: -1px;
|
||||
left: -1px;
|
||||
right: -1px;
|
||||
bottom: -1px;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,102 @@
|
|||
.namespace-picker {
|
||||
border-right: 1px solid rgba($black, 0.5);
|
||||
margin-right: $size-10;
|
||||
position: relative;
|
||||
padding: 0.5rem;
|
||||
color: $white;
|
||||
fill: $white;
|
||||
}
|
||||
.namespace-picker.no-namespaces {
|
||||
border: none;
|
||||
padding-right: 0;
|
||||
}
|
||||
.namespace-picker-trigger {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.namespace-name {
|
||||
display: inline-block;
|
||||
margin-left: $size-10;
|
||||
font-size: 1rem;
|
||||
}
|
||||
.namespace-picker-content {
|
||||
width: 300px;
|
||||
max-height: 300px;
|
||||
overflow: auto;
|
||||
border-radius: $radius;
|
||||
box-shadow: $box-shadow, $box-shadow-high;
|
||||
}
|
||||
.namespace-picker-content .level-left {
|
||||
max-width: 210px;
|
||||
overflow-wrap: break-word;
|
||||
word-wrap: break-word;
|
||||
-ms-word-break: break-all;
|
||||
word-break: break-all;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.namespace-header-bar {
|
||||
padding: $size-11 $size-10;
|
||||
box-shadow: $box-shadow;
|
||||
font-weight: $font-weight-semibold;
|
||||
min-height: 32px;
|
||||
.namespace-manage-link {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.namespace-header {
|
||||
margin: $size-9 $size-9 0;
|
||||
color: $grey;
|
||||
font-size: $size-8;
|
||||
font-weight: $font-weight-semibold;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.current-namespace {
|
||||
border-bottom: 1px solid rgba($black, 0.1);
|
||||
}
|
||||
|
||||
.namespace-list {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.namespace-link {
|
||||
color: $black;
|
||||
text-decoration: none;
|
||||
font-weight: $font-weight-semibold;
|
||||
padding: $size-10 $size-9;
|
||||
}
|
||||
|
||||
.leaf-panel {
|
||||
transition: transform ease-in-out 250ms;
|
||||
will-change: transform;
|
||||
transform: translateX(0);
|
||||
background: $white;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
.leaf-panel-left {
|
||||
transform: translateX(-300px);
|
||||
}
|
||||
.leaf-panel-adding,
|
||||
.leaf-panel-current {
|
||||
position: relative;
|
||||
& .namespace-link:last-child {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
}
|
||||
.animated-list {
|
||||
.leaf-panel-exiting,
|
||||
.leaf-panel-adding {
|
||||
transform: translateX(300px);
|
||||
z-index: 20;
|
||||
}
|
||||
}
|
||||
.leaf-panel-adding {
|
||||
z-index: 100;
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
.namespace-reminder {
|
||||
color: $grey;
|
||||
margin: 0 0 $size-6 0;
|
||||
}
|
||||
|
||||
.console-reminder p.namespace-reminder {
|
||||
margin-bottom: 0;
|
||||
opacity: 0.7;
|
||||
position: absolute;
|
||||
color: $grey;
|
||||
font-family: $family-monospace;
|
||||
}
|
|
@ -3,13 +3,19 @@ a.splash-page-logo {
|
|||
svg {
|
||||
transform: scale(.5);
|
||||
transform-origin: left;
|
||||
fill: currentColor;
|
||||
fill: $white;
|
||||
}
|
||||
}
|
||||
a.splash-page-logo.is-active {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.splash-page-container {
|
||||
margin: $size-2 0;
|
||||
}
|
||||
.splash-page-header {
|
||||
padding: .75rem 1.5rem;
|
||||
padding: $size-6 $size-5;
|
||||
}
|
||||
.splash-page-sub-header {
|
||||
margin: 0 $size-5 $size-6;
|
||||
}
|
||||
|
|
|
@ -62,6 +62,8 @@
|
|||
@import "./components/login-form";
|
||||
@import "./components/masked-input";
|
||||
@import "./components/message-in-page";
|
||||
@import "./components/namespace-picker";
|
||||
@import "./components/namespace-reminder";
|
||||
@import "./components/page-header";
|
||||
@import "./components/popup-menu";
|
||||
@import "./components/radial-progress";
|
||||
|
|
|
@ -199,6 +199,17 @@ $button-box-shadow-standard: 0 3px 1px 0 rgba($black, 0.12);
|
|||
}
|
||||
}
|
||||
|
||||
.button.icon {
|
||||
box-sizing: border-box;
|
||||
padding: 0 $size-11;
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
&,
|
||||
& .icon {
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.button .icon.auto-width {
|
||||
width: auto;
|
||||
margin: 0 !important;
|
||||
|
|
|
@ -43,6 +43,9 @@ a.navbar-item {
|
|||
left: 3.5em;
|
||||
top: 0;
|
||||
height: 3.25rem;
|
||||
&.with-ns-picker {
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.icon.edition-icon {
|
||||
|
|
|
@ -1,120 +1,5 @@
|
|||
<div class="page-container">
|
||||
{{#if showNav}}
|
||||
<NavHeader data-test-header-with-nav @class="{{if consoleOpen 'panel-open'}} {{if consoleFullscreen ' panel-fullscreen'}}" as |Nav|>
|
||||
<Nav.home>
|
||||
<HomeLink @class="navbar-item has-text-white has-current-color-fill">
|
||||
{{partial 'svg/vault-logo'}}
|
||||
</HomeLink>
|
||||
</Nav.home>
|
||||
<Nav.items>
|
||||
<div class="navbar-item">
|
||||
<button type="button" class="button is-transparent" {{action 'toggleConsole'}} data-test-console-toggle>
|
||||
{{#if consoleOpen}}
|
||||
{{i-con glyph="console-active" size=24}}
|
||||
{{i-con glyph="chevron-up" aria-hidden="true" size=8 class="has-text-white auto-width is-status-chevron"}}
|
||||
{{else}}
|
||||
{{i-con glyph="console" size=24}}
|
||||
{{i-con glyph="chevron-down" aria-hidden="true" size=8 class="has-text-white auto-width is-status-chevron"}}
|
||||
{{/if}}
|
||||
</button>
|
||||
</div>
|
||||
<div class="navbar-item">
|
||||
{{status-menu}}
|
||||
</div>
|
||||
<div class="navbar-item">
|
||||
{{status-menu type="user"}}
|
||||
</div>
|
||||
</Nav.items>
|
||||
<Nav.main>
|
||||
<ul class="navbar-sections tabs tabs-subnav">
|
||||
<li class="{{if (is-active-route 'vault.cluster.secrets') 'is-active'}}">
|
||||
<a href="{{href-to "vault.cluster.secrets" activeClusterName}}">
|
||||
Secrets
|
||||
</a>
|
||||
</li>
|
||||
<li class="{{if (is-active-route 'vault.cluster.access') 'is-active'}}">
|
||||
<a href="{{href-to "vault.cluster.access" activeClusterName}}">
|
||||
Access
|
||||
</a>
|
||||
</li>
|
||||
<li class="{{if (is-active-route 'vault.cluster.replication') 'is-active'}}">
|
||||
{{#if activeCluster.anyReplicationEnabled}}
|
||||
{{status-menu type="replication"}}
|
||||
{{else}}
|
||||
<a href="{{href-to "vault.cluster.replication" activeClusterName}}">
|
||||
Replication
|
||||
{{#if (is-version "OSS")}}
|
||||
<ICon @glyph="edition-enterprise" @size=16 @class="edition-icon" />
|
||||
{{/if}}
|
||||
</a>
|
||||
{{/if}}
|
||||
</li>
|
||||
<li class="{{if (is-active-route (array 'vault.cluster.policies' 'vault.cluster.policy')) 'is-active'}}">
|
||||
<a href="{{href-to "vault.cluster.policies" "acl" current-when='vault.cluster.policies vault.cluster.policy'}}">
|
||||
Policies
|
||||
</a>
|
||||
</li>
|
||||
<li class="{{if (is-active-route 'vault.cluster.tools') 'is-active'}}">
|
||||
<a href="{{href-to "vault.cluster.tools.tool" activeClusterName "wrap" }}">
|
||||
Tools
|
||||
</a>
|
||||
</li>
|
||||
<li class="{{if (is-active-route 'vault.cluster.settings') 'is-active'}}">
|
||||
<a href="{{href-to "vault.cluster.settings" activeClusterName}}">
|
||||
Settings
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
{{console/ui-panel isFullscreen=consoleFullscreen}}
|
||||
</Nav.main>
|
||||
</NavHeader>
|
||||
{{/if}}
|
||||
<div class="global-flash">
|
||||
{{#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}}
|
||||
<h5 class="title is-5 has-text-{{if (eq flash.type 'warning') 'dark-yellow' flash.type}}">
|
||||
{{get (message-types flash.type) "text"}}
|
||||
</h5>
|
||||
<span data-test-flash-message-body=true>
|
||||
{{flash.message}}
|
||||
</span>
|
||||
<button type="button" class="delete" {{action close}}>
|
||||
{{i-con excludeIconClass=true glyph="close" aria-label="Close"}}
|
||||
</button>
|
||||
{{/if}}
|
||||
{{/flash-message}}
|
||||
{{/each}}
|
||||
</div>
|
||||
{{#if showNav}}
|
||||
<section class="section">
|
||||
<div class="container is-widescreen">
|
||||
{{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}}
|
||||
</div>
|
||||
</section>
|
||||
{{else}}
|
||||
{{outlet}}
|
||||
{{/if}}
|
||||
|
||||
{{outlet}}
|
||||
<footer class="footer has-text-grey">
|
||||
<div class="level">
|
||||
<div class="level-item is-size-7 has-text-centered">
|
||||
|
@ -135,7 +20,7 @@
|
|||
</span>
|
||||
{{/if}}
|
||||
<span>
|
||||
<a class="has-text-grey" target="_blank" href="https://www.vaultproject.io/docs/index.html">Documentation</a>
|
||||
<a class="has-text-grey" target="_blank" rel="noreferrer noopener" href="https://www.vaultproject.io/docs/index.html">Documentation</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<form {{action (perform saveModel) on="submit"}}>
|
||||
<div class="box is-sideless is-fullwidth is-marginless">
|
||||
<NamespaceReminder @mode="save" @noun="auth method" />
|
||||
{{message-error model=model}}
|
||||
{{#if model.attrs}}
|
||||
{{#each model.attrs as |attr|}}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<form {{action (perform saveModel) on="submit"}}>
|
||||
<div class="box is-sideless is-fullwidth is-marginless">
|
||||
<NamespaceReminder @mode="save" @noun="auth method" />
|
||||
{{message-error model=model}}
|
||||
{{#each model.tuneAttrs as |attr|}}
|
||||
{{form-field data-test-field attr=attr model=model}}
|
||||
|
|
|
@ -1,94 +1,101 @@
|
|||
<nav class="tabs sub-nav is-marginless">
|
||||
<ul>
|
||||
{{#each methodsToShow as |method|}}
|
||||
{{#with (or method.path method.type) as |methodKey|}}
|
||||
{{#if hasMethodsWithPath}}
|
||||
<li class="{{if (and selectedAuthIsPath (eq (or selectedAuthBackend.path selectedAuthBackend.type) methodKey)) 'is-active' ''}}" data-test-auth-method>
|
||||
<a href="{{href-to 'vault.cluster.auth' cluster.name (query-params with=methodKey)}}" data-test-auth-method-link={{method.type}}>
|
||||
{{or method.id (capitalize method.type)}}
|
||||
</a>
|
||||
</li>
|
||||
{{else}}
|
||||
<li class="{{if (eq (or selectedAuthBackend.path selectedAuthBackend.type) methodKey) 'is-active' ''}}" data-test-auth-method>
|
||||
<a href="{{href-to 'vault.cluster.auth' cluster.name (query-params with=methodKey)}}" data-test-auth-method-link={{method.type}}>
|
||||
{{or method.id method.typeDisplay}}
|
||||
</a>
|
||||
</li>
|
||||
{{/if}}
|
||||
{{/with}}
|
||||
{{/each}}
|
||||
{{#if hasMethodsWithPath}}
|
||||
<li class="{{if (not selectedAuthIsPath) 'is-active' ''}}" data-test-auth-method>
|
||||
<a href="{{href-to 'vault.cluster.auth' cluster.name (query-params with='token')}}" data-test-auth-method-link=other>
|
||||
Other
|
||||
</a>
|
||||
</li>
|
||||
{{/if}}
|
||||
</ul>
|
||||
</nav>
|
||||
<form
|
||||
id="auth-form"
|
||||
{{action (action "doSubmit") on="submit"}}
|
||||
>
|
||||
<div class="box is-marginless is-shadowless">
|
||||
{{#if (and cluster.standby hasCSPError)}}
|
||||
{{message-error errorMessage=cspErrorText data-test-auth-error=true}}
|
||||
{{else}}
|
||||
{{message-error errorMessage=error data-test-auth-error=true}}
|
||||
{{/if}}
|
||||
{{#if (and hasMethodsWithPath (not selectedAuthIsPath))}}
|
||||
<div class="field">
|
||||
<label for="selectedMethod" class="is-label">
|
||||
Method
|
||||
</label>
|
||||
<div class="control is-expanded" >
|
||||
<div class="select is-fullwidth">
|
||||
<select
|
||||
name="selectedMethod"
|
||||
id="selectedMethod"
|
||||
onchange={{action (mut selectedAuth) value="target.value"}}
|
||||
data-test-method-select
|
||||
>
|
||||
{{#each (supported-auth-backends) as |method|}}
|
||||
<option selected={{eq selectedAuthBackend.type method.type}} value={{method.type}}>
|
||||
{{method.typeDisplay}}
|
||||
</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
<div class="auth-form">
|
||||
{{#if showLoading}}
|
||||
<div class="vault-loader">
|
||||
{{partial 'svg/vault-loading'}}
|
||||
</div>
|
||||
{{/if}}
|
||||
<nav class="tabs sub-nav is-marginless">
|
||||
<ul>
|
||||
{{#each methodsToShow as |method|}}
|
||||
{{#with (or method.path method.type) as |methodKey|}}
|
||||
{{#if hasMethodsWithPath}}
|
||||
<li class="{{if (and selectedAuthIsPath (eq (or selectedAuthBackend.path selectedAuthBackend.type) methodKey)) 'is-active' ''}}" data-test-auth-method>
|
||||
{{#link-to 'vault.cluster.auth' cluster.name (query-params with=methodKey) data-test-auth-method-link=method.type}}
|
||||
{{or method.id (capitalize method.type)}}
|
||||
{{/link-to}}
|
||||
</li>
|
||||
{{else}}
|
||||
<li class="{{if (eq (or selectedAuthBackend.path selectedAuthBackend.type) methodKey) 'is-active' ''}}" data-test-auth-method>
|
||||
{{#link-to 'vault.cluster.auth' cluster.name (query-params with=methodKey) data-test-auth-method-link=method.type}}
|
||||
{{or method.id method.typeDisplay}}
|
||||
{{/link-to}}
|
||||
</li>
|
||||
{{/if}}
|
||||
{{/with}}
|
||||
{{/each}}
|
||||
{{#if hasMethodsWithPath}}
|
||||
<li class="{{if (not selectedAuthIsPath) 'is-active' ''}}" data-test-auth-method>
|
||||
{{#link-to 'vault.cluster.auth' cluster.name (query-params with='token') data-test-auth-method-link="other"}}
|
||||
Other
|
||||
{{/link-to}}
|
||||
</li>
|
||||
{{/if}}
|
||||
</ul>
|
||||
</nav>
|
||||
<form
|
||||
id="auth-form"
|
||||
{{action (action "doSubmit") on="submit"}}
|
||||
>
|
||||
<div class="box is-marginless is-shadowless">
|
||||
{{#if (and cluster.standby hasCSPError)}}
|
||||
{{message-error errorMessage=cspErrorText data-test-auth-error=true}}
|
||||
{{else}}
|
||||
{{message-error errorMessage=error data-test-auth-error=true}}
|
||||
{{/if}}
|
||||
{{#if (and hasMethodsWithPath (not selectedAuthIsPath))}}
|
||||
<div class="field">
|
||||
<label for="selectedMethod" class="is-label">
|
||||
Method
|
||||
</label>
|
||||
<div class="control is-expanded" >
|
||||
<div class="select is-fullwidth">
|
||||
<select
|
||||
name="selectedMethod"
|
||||
id="selectedMethod"
|
||||
onchange={{action (mut selectedAuth) value="target.value"}}
|
||||
data-test-method-select
|
||||
>
|
||||
{{#each (supported-auth-backends) as |method|}}
|
||||
<option selected={{eq selectedAuthBackend.type method.type}} value={{method.type}}>
|
||||
{{capitalize method.type}}
|
||||
</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{partial providerPartialName}}
|
||||
{{#unless (or selectedAuthIsPath (eq selectedAuthBackend.type "token"))}}
|
||||
<div class="box has-slim-padding is-shadowless">
|
||||
{{toggle-button toggleTarget=this toggleAttr="useCustomPath"}}
|
||||
<div class="field">
|
||||
{{#if useCustomPath}}
|
||||
<label for="custom-path" class="is-label">
|
||||
Mount path
|
||||
</label>
|
||||
<div class="control">
|
||||
<input
|
||||
type="text"
|
||||
name="custom-path"
|
||||
id="custom-path"
|
||||
class="input"
|
||||
value={{customPath}}
|
||||
oninput={{action (mut customPath) value="target.value"}}
|
||||
/>
|
||||
</div>
|
||||
<p class="help has-text-grey-dark">
|
||||
If this backend was mounted using a non-default path, enter it here.
|
||||
</p>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{partial providerPartialName}}
|
||||
{{#unless (or selectedAuthIsPath (eq selectedAuthBackend.type "token"))}}
|
||||
<div class="box has-slim-padding is-shadowless">
|
||||
{{toggle-button toggleTarget=this toggleAttr="useCustomPath"}}
|
||||
<div class="field">
|
||||
{{#if useCustomPath}}
|
||||
<label for="custom-path" class="is-label">
|
||||
Mount path
|
||||
</label>
|
||||
<div class="control">
|
||||
<input
|
||||
type="text"
|
||||
name="custom-path"
|
||||
id="custom-path"
|
||||
class="input"
|
||||
value={{customPath}}
|
||||
oninput={{action (mut customPath) value="target.value"}}
|
||||
/>
|
||||
</div>
|
||||
<p class="help has-text-grey-dark">
|
||||
If this backend was mounted using a non-default path, enter it here.
|
||||
</p>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/unless}}
|
||||
</div>
|
||||
<div class="box is-marginless is-shadowless">
|
||||
<button data-test-auth-submit=true type="submit" disabled={{loading}} class="button is-primary {{if loading 'is-loading'}}" id="auth-submit">
|
||||
Sign In
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{{/unless}}
|
||||
</div>
|
||||
<div class="box is-marginless is-shadowless">
|
||||
<button data-test-auth-submit=true type="submit" disabled={{loading}} class="button is-primary {{if loading 'is-loading'}}" id="auth-submit">
|
||||
Sign In
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
@ -47,9 +47,9 @@
|
|||
{{/if}}
|
||||
{{/if}}
|
||||
<li class="action">
|
||||
<a href="{{href-to "vault.cluster.logout" activeClusterName }}" id="logout">
|
||||
{{#link-to "vault.cluster.logout" activeClusterName id="logout"}}
|
||||
Sign out
|
||||
</a>
|
||||
{{/link-to}}
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
|
|
@ -30,6 +30,7 @@
|
|||
</div>
|
||||
{{else}}
|
||||
<form {{action "saveCA" on="submit"}} data-test-generate-root-cert="true">
|
||||
<NamespaceReminder @mode="save" @noun="PKI change" />
|
||||
{{#if model.uploadPemBundle}}
|
||||
{{#message-in-page type="warning" data-test-warning=true}}
|
||||
<em>If you have already set a certificate and key, they will be overridden with the successful saving of a new <code>PEM bundle</code>.</em>
|
||||
|
@ -90,8 +91,9 @@
|
|||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
{{message-error model=model}}
|
||||
<h2 data-test-title class="title is-3">Sign intermediate</h2>
|
||||
<NamespaceReminder @mode="save" @noun="PKI change" />
|
||||
{{message-error model=model}}
|
||||
<form {{action "saveCA" on="submit"}} data-test-sign-intermediate-form="true">
|
||||
{{partial "partials/form-field-groups-loop"}}
|
||||
<div class="field is-grouped box is-fullwidth is-bottomless">
|
||||
|
@ -109,8 +111,9 @@
|
|||
</form>
|
||||
{{/if}}
|
||||
{{else if setSignedIntermediate}}
|
||||
{{message-error model=model}}
|
||||
<h2 data-test-title class="title is-3">Set signed intermediate</h2>
|
||||
<NamespaceReminder @mode="save" @noun="PKI change" />
|
||||
{{message-error model=model}}
|
||||
<p class="has-text-grey-dark">
|
||||
Submit a signed CA certificate corresponding to a generated private key.
|
||||
</p>
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
<NamespaceReminder @mode="save" @noun="PKI change" />
|
||||
|
||||
{{#if (eq section "tidy")}}
|
||||
<p class="has-text-grey-dark" data-test-text="true">
|
||||
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.
|
||||
|
|
|
@ -1,20 +1,23 @@
|
|||
{{#if isRunning}}
|
||||
<div class="control console-spinner is-loading"></div>
|
||||
{{else}}
|
||||
{{i-con glyph="chevron-right" size=12 }}
|
||||
{{/if}}
|
||||
<input onkeyup={{action 'handleKeyUp'}} value={{value}} autocomplete="off" spellcheck="false" />
|
||||
{{#tool-tip horizontalPosition="auto-right" verticalPosition=(if isFullscreen "above" "below") as |d|}}
|
||||
{{#d.trigger tagName="button" type="button" class=(concat "button is-compact" (if isFullscreen " active")) click=(action "fullscreen") data-test-tool-tip-trigger=true}}
|
||||
{{i-con glyph=(if isFullscreen "fullscreen-close" "fullscreen-open") aria-hidden="true" size=16}}
|
||||
{{/d.trigger}}
|
||||
{{#d.content class="tool-tip"}}
|
||||
<div class="box">
|
||||
{{#if isFullscreen}}
|
||||
Minimize
|
||||
{{else}}
|
||||
Maximize
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/d.content}}
|
||||
{{/tool-tip}}
|
||||
<div class="console-ui-input" data-test-component="console/command-input">
|
||||
{{#if isRunning}}
|
||||
<div class="control console-spinner is-loading"></div>
|
||||
{{else}}
|
||||
{{i-con glyph="chevron-right" size=12 }}
|
||||
{{/if}}
|
||||
<input onkeyup={{action 'handleKeyUp'}} value={{value}} autocomplete="off" spellcheck="false" />
|
||||
{{#tool-tip horizontalPosition="auto-right" verticalPosition=(if isFullscreen "above" "below") as |d|}}
|
||||
{{#d.trigger tagName="button" type="button" class=(concat "button is-compact" (if isFullscreen " active")) click=(action "fullscreen") data-test-tool-tip-trigger=true}}
|
||||
{{i-con glyph=(if isFullscreen "fullscreen-close" "fullscreen-open") aria-hidden="true" size=16}}
|
||||
{{/d.trigger}}
|
||||
{{#d.content class="tool-tip"}}
|
||||
<div class="box">
|
||||
{{#if isFullscreen}}
|
||||
Minimize
|
||||
{{else}}
|
||||
Maximize
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/d.content}}
|
||||
{{/tool-tip}}
|
||||
</div>
|
||||
<NamespaceReminder @class="console-reminder" @mode="execute" @noun="command" />
|
||||
|
|
|
@ -25,9 +25,9 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="field is-grouped box is-fullwidth is-bottomless">
|
||||
<a href={{href-to 'vault.cluster.access.control-groups'}} class="button" >
|
||||
{{#link-to 'vault.cluster.access.control-groups' class="button"}}
|
||||
<ICon @glyph="chevron-left" @size=10 /> Back
|
||||
</a>
|
||||
{{/link-to}}
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="control-group-success" data-test-unwrap-form>
|
||||
|
|
|
@ -13,9 +13,9 @@
|
|||
<div class="control-group">
|
||||
<div data-test-requestor-text>
|
||||
{{#if model.requestEntity.canRead}}
|
||||
<a href="{{href-to 'vault.cluster.access.identity.show' 'entities' model.requestEntity.id 'details'}}">
|
||||
{{#link-to 'vault.cluster.access.identity.show' 'entities' model.requestEntity.id 'details'}}
|
||||
{{requestorName}}
|
||||
</a>
|
||||
{{/link-to}}
|
||||
{{else}}
|
||||
{{requestorName}}
|
||||
{{/if}}
|
||||
|
@ -47,7 +47,7 @@
|
|||
Already approved by
|
||||
{{#each model.authorizations as |authorization index|}}
|
||||
{{~#if authorization.canRead~}}
|
||||
<a href="{{href-to 'vault.cluster.access.identity.show' 'entities' authorization.id 'details'}}">{{authorization.name}}</a>
|
||||
{{#link-to 'vault.cluster.access.identity.show' 'entities' authorization.id 'details'}}{{authorization.name}}{{/link-to}}
|
||||
{{~else~}}
|
||||
{{authorization.name}}
|
||||
{{~/if~}}{{#if (lt (inc index) model.authorizations.length)}},{{/if}}
|
||||
|
@ -73,9 +73,9 @@
|
|||
<div class="field is-grouped box is-fullwidth is-bottomless">
|
||||
{{#if model.canAuthorize}}
|
||||
{{#if (or model.approved currentUserHasAuthorized)}}
|
||||
<a href={{href-to 'vault.cluster.access.control-groups'}} class="button" data-test-back-link >
|
||||
{{#link-to 'vault.cluster.access.control-groups'class="button" data-test-back-link=true}}
|
||||
<ICon @glyph="chevron-left" @size=10 /> Back
|
||||
</a>
|
||||
{{/link-to}}
|
||||
{{else}}
|
||||
<button
|
||||
type="button"
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<form {{action (perform save model) on="submit"}}>
|
||||
<MessageError @model={{model}} data-test-edit-form-error />
|
||||
<div class="box is-sideless is-fullwidth is-marginless">
|
||||
<NamespaceReminder @mode="save" />
|
||||
{{#each model.fields as |attr|}}
|
||||
{{form-field data-test-field attr=attr model=model}}
|
||||
{{/each}}
|
||||
|
@ -9,9 +10,16 @@
|
|||
<div class="field is-grouped">
|
||||
<div class="control">
|
||||
<button type="submit" data-test-edit-form-submit class="button is-primary {{if save.isRunning 'loading'}}" disabled={{save.isRunning}}>
|
||||
Save
|
||||
{{saveButtonText}}
|
||||
</button>
|
||||
</div>
|
||||
{{#if cancelLinkParams}}
|
||||
<div class="control">
|
||||
{{#link-to params=cancelLinkParams class="button"}}
|
||||
Cancel
|
||||
{{/link-to}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{#if model.canDelete}}
|
||||
<ConfirmAction
|
||||
|
|
|
@ -101,10 +101,18 @@
|
|||
<input
|
||||
data-test-input={{attr.name}}
|
||||
id={{attr.name}}
|
||||
autocomplete="off"
|
||||
value={{or (get model valuePath) attr.options.defaultValue}}
|
||||
oninput={{action (action "setAndBroadcast" valuePath) value="target.value"}}
|
||||
class="input"
|
||||
class="input"
|
||||
/>
|
||||
{{#if attr.options.validationAttr}}
|
||||
{{#if (and (get model valuePath) (not (get model attr.options.validationAttr)))}}
|
||||
<p class="has-text-danger">
|
||||
{{attr.options.invalidMessage}}
|
||||
</p>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</div>
|
||||
{{else if (eq attr.type 'boolean')}}
|
||||
|
|
|
@ -4,21 +4,21 @@
|
|||
<ul>
|
||||
<li>
|
||||
<span class="sep">/</span>
|
||||
<a href={{href-to "vault.cluster.secrets.backend" backend.id}} data-test-link="role-list">
|
||||
{{#link-to "vault.cluster.secrets.backend" backend.id data-test-link="role-list"}}
|
||||
{{backend.id}}
|
||||
</a>
|
||||
{{/link-to}}
|
||||
</li>
|
||||
<li class="is-active">
|
||||
<span class="sep">/</span>
|
||||
<a href={{href-to "vault.cluster.secrets.backend" backend.id}}>
|
||||
{{#link-to "vault.cluster.secrets.backend" backend.id}}
|
||||
creds
|
||||
</a>
|
||||
{{/link-to}}
|
||||
</li>
|
||||
<li>
|
||||
<span class="sep">/</span>
|
||||
<a href={{href-to "vault.cluster.secrets.backend.show" model.role.name}}>
|
||||
{{#link-to "vault.cluster.secrets.backend.show" model.role.name}}
|
||||
{{model.role.name}}
|
||||
</a>
|
||||
{{/link-to}}
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
@ -98,6 +98,7 @@
|
|||
{{else}}
|
||||
<form {{action "create" on="submit"}} data-test-secret-generate-form="true">
|
||||
<div class="box is-sideless is-fullwidth is-marginless">
|
||||
<NamespaceReminder @mode="generate" @noun="credential" />
|
||||
{{message-error model=model}}
|
||||
{{#if model.fieldGroups}}
|
||||
{{partial "partials/form-field-groups-loop"}}
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
{{#link-to 'vault.cluster' 'vault' class=class}}
|
||||
{{#if hasBlock}}
|
||||
{{yield}}
|
||||
{{else}}
|
||||
{{text}}
|
||||
{{/if}}
|
||||
{{/link-to}}
|
|
@ -1,6 +1,7 @@
|
|||
<form {{action (perform save) on="submit"}}>
|
||||
{{message-error model=model}}
|
||||
<div class="box is-sideless is-fullwidth is-marginless">
|
||||
<NamespaceReminder @mode={{mode}} @noun={{lowercase (humanize model.identityType)}} />
|
||||
{{message-error model=model}}
|
||||
{{#if (eq mode "merge")}}
|
||||
{{#message-in-page type="warning"}}
|
||||
Metadata on merged entities is not preserved, you will need to recreate it on the entity you merge to.
|
||||
|
@ -22,13 +23,13 @@
|
|||
{{/if}}
|
||||
</button>
|
||||
{{#if (or (eq mode "merge") (eq mode "create" ))}}
|
||||
<a href={{href-to cancelLink}} class="button" data-test-cancel-link>
|
||||
{{#link-to cancelLink class="button" data-test-cancel-link=true}}
|
||||
Cancel
|
||||
</a>
|
||||
{{/link-to}}
|
||||
{{else}}
|
||||
<a href={{href-to cancelLink model.id "details"}} class="button" data-test-cancel-link>
|
||||
{{#link-to cancelLink model.id "details" class="button" data-test-cancel-link=true}}
|
||||
Cancel
|
||||
</a>
|
||||
{{/link-to}}
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -6,29 +6,29 @@
|
|||
</p.levelLeft>
|
||||
<p.levelRight>
|
||||
{{#if (eq identityType "entity")}}
|
||||
<a href="{{href-to 'vault.cluster.access.identity.merge' (pluralize identityType)}}" class="button has-icon-right is-ghost is-compact" data-test-entity-merge-link=true>
|
||||
{{#link-to "vault.cluster.access.identity.merge" (pluralize identityType) class="button has-icon-right is-ghost is-compact" data-test-entity-merge-link=true}}
|
||||
Merge {{pluralize identityType}}
|
||||
{{i-con glyph="chevron-right" size=11}}
|
||||
</a>
|
||||
{{/link-to}}
|
||||
{{/if}}
|
||||
<a href="{{href-to 'vault.cluster.access.identity.create' (pluralize identityType)}}" class="button has-icon-right is-ghost is-compact" data-test-entity-create-link=true>
|
||||
{{#link-to "vault.cluster.access.identity.create" (pluralize identityType) class="button has-icon-right is-ghost is-compact" data-test-entity-create-link=true}}
|
||||
Create {{identityType}}
|
||||
{{i-con glyph="chevron-right" size=11}}
|
||||
</a>
|
||||
{{/link-to}}
|
||||
</p.levelRight>
|
||||
</PageHeader>
|
||||
<div class="box is-sideless is-fullwidth is-paddingless is-marginless">
|
||||
<nav class="tabs sub-nav">
|
||||
<ul>
|
||||
{{#link-to "vault.cluster.access.identity.index" tagName="li"}}
|
||||
<a href={{href-to "vault.cluster.access.identity.index" (pluralize identityType)}}>
|
||||
{{#link-to "vault.cluster.access.identity.index" (pluralize identityType) tagName="li"}}
|
||||
{{#link-to "vault.cluster.access.identity.index" (pluralize identityType)}}
|
||||
{{capitalize (pluralize identityType)}}
|
||||
</a>
|
||||
{{/link-to}}
|
||||
{{/link-to}}
|
||||
{{#link-to "vault.cluster.access.identity.aliases.index" tagName="li"}}
|
||||
<a href={{href-to "vault.cluster.access.identity.aliases.index" (pluralize identityType)}}>
|
||||
{{#link-to "vault.cluster.access.identity.aliases.index" (pluralize identityType) tagName="li"}}
|
||||
{{#link-to "vault.cluster.access.identity.aliases.index" (pluralize identityType)}}
|
||||
Aliases
|
||||
</a>
|
||||
{{/link-to}}
|
||||
{{/link-to}}
|
||||
</ul>
|
||||
</nav>
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
{{info-table-row label="Name" value=model.name data-test-alias-name=true}}
|
||||
{{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}}
|
||||
<a href={{href-to 'vault.cluster.access.identity.show' (if (eq model.identityType "entity-alias") "entities" "groups") model.canonicalId "details"}}
|
||||
{{#link-to "vault.cluster.access.identity.show" (if (eq model.identityType "entity-alias") "entities" "groups") model.canonicalId "details"
|
||||
class="has-text-black is-font-mono"
|
||||
>
|
||||
}}
|
||||
{{model.canonicalId}}
|
||||
</a>
|
||||
{{/link-to}}
|
||||
{{/info-table-row}}
|
||||
{{info-table-row label="Merged from Entity ID" value=model.mergedFromCanonicalIds}}
|
||||
{{#info-table-row label="Mount" value=model.mountAccessor }}
|
||||
|
|
|
@ -7,13 +7,13 @@
|
|||
}}
|
||||
<div class="columns is-mobile">
|
||||
<div class="column is-10">
|
||||
<a href={{href-to "vault.cluster.access.identity.aliases.show" item.id "details"}}
|
||||
{{#link-to "vault.cluster.access.identity.aliases.show" item.id "details"
|
||||
class="has-text-black has-text-weight-semibold"
|
||||
>{{i-con
|
||||
}}{{i-con
|
||||
glyph='role'
|
||||
size=14
|
||||
class="has-text-grey-light"
|
||||
}}<span class="has-text-weight-semibold">{{item.name}}</span></a>
|
||||
}}<span class="has-text-weight-semibold">{{item.name}}</span>{{/link-to}}
|
||||
<div class="has-text-grey">
|
||||
{{item.id}}
|
||||
</div>
|
||||
|
|
|
@ -1,26 +1,26 @@
|
|||
{{#if model.groupIds}}
|
||||
{{#each model.directGroupIds as |gid|}}
|
||||
<a href={{href-to "vault.cluster.access.identity.show" "groups" gid "details" }}
|
||||
{{#link-to "vault.cluster.access.identity.show" "groups" gid "details"
|
||||
class="list-item-row"
|
||||
>{{i-con
|
||||
}}{{i-con
|
||||
glyph='folder'
|
||||
size=14
|
||||
class="has-text-grey-light"
|
||||
}}{{gid}}</a>
|
||||
}}{{gid}}{{/link-to}}
|
||||
{{/each}}
|
||||
{{#each model.inheritedGroupIds as |gid|}}
|
||||
{{#linked-block
|
||||
"vault.cluster.access.identity.show" "groups" gid "details"
|
||||
class="list-item-row"
|
||||
}}
|
||||
<a href={{href-to "vault.cluster.access.identity.show" "groups" gid "details" }}
|
||||
{{#link-to "vault.cluster.access.identity.show" "groups" gid "details"
|
||||
class="has-text-black"
|
||||
>{{i-con
|
||||
}}{{i-con
|
||||
glyph='folder'
|
||||
size=14
|
||||
class="has-text-grey-light"
|
||||
}}{{gid}}
|
||||
</a>
|
||||
{{/link-to}}
|
||||
<span class="tag has-text-grey is-size-8">inherited</span>
|
||||
{{/linked-block}}
|
||||
{{/each}}
|
||||
|
|
|
@ -9,13 +9,13 @@
|
|||
}}
|
||||
<div class="columns is-mobile">
|
||||
<div class="column is-10">
|
||||
<a href={{href-to "vault.cluster.access.identity.show" "groups" gid "details" }}
|
||||
{{#link-to "vault.cluster.access.identity.show" "groups" gid "details"
|
||||
class="is-block has-text-black has-text-weight-semibold"
|
||||
>{{i-con
|
||||
}}{{i-con
|
||||
glyph='folder'
|
||||
size=14
|
||||
class="has-text-grey-light"
|
||||
}}{{gid}}</a>
|
||||
}}{{gid}}{{/link-to}}
|
||||
</div>
|
||||
<div class="column has-text-right">
|
||||
{{#if model.canEdit}}
|
||||
|
@ -35,13 +35,13 @@
|
|||
}}
|
||||
<div class="columns">
|
||||
<div class="column is-10">
|
||||
<a href={{href-to "vault.cluster.access.identity.show" "entities" gid "details" }}
|
||||
{{#link-to "vault.cluster.access.identity.show" "entities" gid "details"
|
||||
class="is-block has-text-black has-text-weight-semibold"
|
||||
>{{i-con
|
||||
}}{{i-con
|
||||
glyph='role'
|
||||
size=14
|
||||
class="has-text-grey-light"
|
||||
}}{{gid}}</a>
|
||||
}}{{gid}}{{/link-to}}
|
||||
</div>
|
||||
<div class="column has-text-right">
|
||||
{{#if model.canEdit}}
|
||||
|
|
|
@ -9,13 +9,13 @@
|
|||
}}
|
||||
<div class="columns is-mobile">
|
||||
<div class="column is-10">
|
||||
<a href={{href-to "vault.cluster.access.identity.show" "groups" gid "details" }}
|
||||
{{#link-to "vault.cluster.access.identity.show" "groups" gid "details"
|
||||
class="is-block has-text-black has-text-weight-semibold"
|
||||
>{{i-con
|
||||
}}{{i-con
|
||||
glyph='folder'
|
||||
size=14
|
||||
class="has-text-grey-light"
|
||||
}}{{gid}}</a>
|
||||
}}{{gid}}{{/link-to}}
|
||||
</div>
|
||||
<div class="column has-text-right">
|
||||
</div>
|
||||
|
|
|
@ -7,10 +7,10 @@
|
|||
}}
|
||||
<div class="columns is-mobile">
|
||||
<div class="column is-10">
|
||||
<a href={{href-to "vault.cluster.policy.show" "acl" policyName}}
|
||||
{{#link-to "vault.cluster.policy.show" "acl" policyName
|
||||
class="is-block has-text-black has-text-weight-semibold"
|
||||
><span class="is-underline">{{policyName}}</span>
|
||||
</a>
|
||||
}}<span class="is-underline">{{policyName}}</span>
|
||||
{{/link-to}}
|
||||
</div>
|
||||
<div class="column has-text-right">
|
||||
{{#if model.canEdit}}
|
||||
|
|
|
@ -3,9 +3,9 @@
|
|||
<nav class="menu">
|
||||
<ul class="menu-list">
|
||||
<li class="action">
|
||||
<a href={{href-to "vault.cluster.access.identity.aliases.show" (pluralize item.parentType) item.id "details" }}>
|
||||
{{#link-to "vault.cluster.access.identity.aliases.show" (pluralize item.parentType) item.id "details"}}
|
||||
Details
|
||||
</a>
|
||||
{{/link-to}}
|
||||
</li>
|
||||
{{#if item.updatePath.isPending}}
|
||||
<li class="action">
|
||||
|
@ -16,9 +16,9 @@
|
|||
{{else}}
|
||||
{{#if item.canEdit}}
|
||||
<li class="action">
|
||||
<a href={{href-to "vault.cluster.access.identity.aliases.edit" (pluralize item.parentType) item.id}}>
|
||||
{{#link-to "vault.cluster.access.identity.aliases.edit" (pluralize item.parentType) item.id}}
|
||||
Edit
|
||||
</a>
|
||||
{{/link-to}}
|
||||
</li>
|
||||
{{/if}}
|
||||
{{#if item.canDelete}}
|
||||
|
|
|
@ -2,14 +2,14 @@
|
|||
<nav class="menu">
|
||||
<ul class="menu-list">
|
||||
<li class="action">
|
||||
<a href={{href-to "vault.cluster.policy.show" "acl" policyName }}>
|
||||
{{#link-to "vault.cluster.policy.show" "acl" policyName}}
|
||||
View Policy
|
||||
</a>
|
||||
{{/link-to}}
|
||||
</li>
|
||||
<li class="action">
|
||||
<a href={{href-to "vault.cluster.policy.edit" "acl" policyName }}>
|
||||
{{#link-to "vault.cluster.policy.edit" "acl" policyName}}
|
||||
Edit Policy
|
||||
</a>
|
||||
{{/link-to}}
|
||||
</li>
|
||||
<li class="action">
|
||||
{{#confirm-action
|
||||
|
|
|
@ -4,9 +4,9 @@
|
|||
<li class="{{if (is-active-route path.path path.model isExact=true) 'is-active'}}">
|
||||
<span class="sep">/</span>
|
||||
{{#if linkToPaths}}
|
||||
<a href={{href-to params=(array path.path path.model)}} data-test-secret-root-link={{if (eq index 0) true false}}>
|
||||
{{#link-to params=(array path.path path.model) data-test-secret-root-link=(if (eq index 0) true false)}}
|
||||
{{path.text}}
|
||||
</a>
|
||||
{{/link-to}}
|
||||
{{else}}
|
||||
<span>{{path.text}}</span>
|
||||
{{/if}}
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
{{#if componentName}}
|
||||
{{component componentName item=item}}
|
||||
{{else if linkParams}}
|
||||
<LinkedBlock @params={{linkParams}} @class="list-item-row">
|
||||
<div class="level is-mobile">
|
||||
<div class="level-left">
|
||||
<div>
|
||||
{{#link-to params=linkParams class="has-text-weight-semibold"}}
|
||||
{{yield (hash content=(component "list-item/content"))}}
|
||||
{{/link-to}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-right">
|
||||
<div class="level-item">
|
||||
{{#if hasBlock}}
|
||||
{{yield (hash callMethod=callMethod menu=(component "list-item/popup-menu"))}}
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</LinkedBlock>
|
||||
{{else}}
|
||||
<div class="list-item-row">
|
||||
<div class="level is-mobile">
|
||||
<div class="level-left">
|
||||
<div class="has-text-grey has-text-weight-semibold">
|
||||
{{yield (hash content=(component "list-item/content"))}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-right">
|
||||
<div class="level-item">
|
||||
{{yield (hash callMethod=callMethod menu=(component "list-item/popup-menu"))}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
|
@ -0,0 +1 @@
|
|||
{{yield}}
|
|
@ -0,0 +1,7 @@
|
|||
<PopupMenu>
|
||||
<nav class="menu">
|
||||
<ul class="menu-list">
|
||||
{{yield item}}
|
||||
</ul>
|
||||
</nav>
|
||||
</PopupMenu>
|
|
@ -0,0 +1,19 @@
|
|||
{{#if items.length}}
|
||||
<div class="box is-fullwidth is-bottomless is-sideless is-paddingless">
|
||||
{{#each items as |item|}}
|
||||
{{yield (hash deleteItem=deleteItem saveItem=saveItem item=item)}}
|
||||
{{/each}}
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="box is-bottomless has-background-white-bis">
|
||||
<div class="columns is-centered">
|
||||
<div class="column is-half has-text-centered">
|
||||
<div class="box is-shadowless has-background-white-bis">
|
||||
<p class="has-text-grey">
|
||||
{{emptyMessage}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
|
@ -11,6 +11,7 @@
|
|||
</PageHeader>
|
||||
<form {{action (perform mountBackend) on="submit"}}>
|
||||
<div class="box is-sideless is-fullwidth is-marginless">
|
||||
<NamespaceReminder @mode="enable" @noun={{if (eq mountType "auth") "auth method" "secret engine"}} />
|
||||
{{message-error model=mountModel}}
|
||||
{{form-field-groups model=mountModel onChange=(action "onTypeChange") renderGroup="default"}}
|
||||
{{#if mountModel.authConfigs.firstObject}}
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
{{#link-to "vault.cluster.secrets" (query-params namespace=normalizedNamespace)
|
||||
class=(concat "is-block " class)
|
||||
}}
|
||||
{{#if hasBlock}}
|
||||
{{yield}}
|
||||
{{else}}
|
||||
<div class="level is-mobile">
|
||||
<span class="level-left">{{namespaceDisplay}}</span>
|
||||
<button type="button" class="button is-ghost icon level-right">
|
||||
<ICon @glyph="chevron-right" @size="12" @class="has-text-grey" />
|
||||
</button>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/link-to}}
|
|
@ -0,0 +1,70 @@
|
|||
{{#if (and (not accessibleNamespaces.length) inRootNamespace)}}
|
||||
<div class="namespace-picker no-namespaces">
|
||||
{{!-- Just yield the logo if they're in the root namespace and only have access to it --}}
|
||||
{{yield}}
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="namespace-picker">
|
||||
<BasicDropdown @horizontalPosition="auto-left" @verticalPosition="below" as |D|>
|
||||
<D.trigger
|
||||
@tagName="button"
|
||||
@class="button is-transparent namespace-picker-trigger has-current-color"
|
||||
>
|
||||
{{yield}} {{#if namespaceDisplay}}<span class="namespace-name">{{namespaceDisplay}}</span>{{/if}}
|
||||
<ICon
|
||||
@glyph="chevron-down"
|
||||
@size=8
|
||||
@class="has-text-white auto-width is-status-chevron"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</D.trigger>
|
||||
<D.content @class="namespace-picker-content">
|
||||
<div class="namespace-header-bar level is-mobile">
|
||||
<div class="level-left">
|
||||
{{#if (not isUserRootNamespace)}}
|
||||
<NamespaceLink @targetNamespace={{or (object-at (dec 2 menuLeaves.length) lastMenuLeaves) auth.authData.userRootNamespace}} @class="namespace-link button is-ghost icon">
|
||||
<ICon
|
||||
@glyph="chevron-left"
|
||||
@size=12
|
||||
@class="has-text-info"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</NamespaceLink>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="level-right">
|
||||
{{#link-to "vault.cluster.access.namespaces" class="namespace-manage-link"}}
|
||||
Manage
|
||||
{{/link-to}}
|
||||
</div>
|
||||
</div>
|
||||
<header class="current-namespace">
|
||||
<h5 class="namespace-header">Current namespace</h5>
|
||||
<div class="level is-mobile namespace-link">
|
||||
<span class="level-left">{{or namespacePath "root"}}</span>
|
||||
<ICon @glyph="checkmark-circled-outline" @size="16" @class="has-text-success level-right" />
|
||||
</div>
|
||||
</header>
|
||||
<div class="namespace-list {{if isAnimating "animated-list"}}">
|
||||
{{#if (contains '' lastMenuLeaves)}}
|
||||
{{!-- leaf is '' which is the root namespace, and then we need to iterate the root leaves --}}
|
||||
<div class="leaf-panel
|
||||
{{if (eq '' currentLeaf) "leaf-panel-current" "leaf-panel-left"}}
|
||||
">{{~#each rootLeaves as |rootLeaf|}}
|
||||
<NamespaceLink @targetNamespace={{rootLeaf}} @class="namespace-link" @showLastSegment={{true}} />
|
||||
{{/each~}}</div>
|
||||
{{/if}}
|
||||
{{#each lastMenuLeaves as |leaf index|}}
|
||||
<div class="leaf-panel
|
||||
{{if (eq leaf currentLeaf) "leaf-panel-current" "leaf-panel-left"}}
|
||||
{{if (and isAdding (eq leaf changedLeaf)) "leaf-panel-adding"}}
|
||||
{{if (and (not isAdding) (eq leaf changedLeaf)) "leaf-panel-exiting"}}
|
||||
">{{~#each-in (get namespaceTree leaf) as |leafName|}}
|
||||
<NamespaceLink @targetNamespace={{concat leaf "/" leafName}} @class="namespace-link" @showLastSegment={{true}} />
|
||||
{{/each-in~}}</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
</D.content>
|
||||
</BasicDropdown>
|
||||
</div>
|
||||
{{/if}}
|
|
@ -0,0 +1,5 @@
|
|||
{{#if showMessage}}
|
||||
<p class="namespace-reminder">
|
||||
This {{noun}} will be {{modeVerb}} in the <span class="tag">{{namespace.path}}</span>namespace.
|
||||
</p>
|
||||
{{/if}}
|
|
@ -2,9 +2,9 @@
|
|||
<nav class="menu">
|
||||
<ul class="menu-list">
|
||||
<li class="action">
|
||||
<a href={{href-to 'vault.cluster.secrets.backend.show' item.idForNav}} data-test-pki-cert-link="show">
|
||||
{{#link-to "vault.cluster.secrets.backend.show" item.idForNav data-test-pki-cert-link="show"}}
|
||||
Details
|
||||
</a>
|
||||
{{/link-to}}
|
||||
</li>
|
||||
{{#if item.canRevoke}}
|
||||
<li class="action">
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
{{#link-to params=linkParams
|
||||
class=class
|
||||
data-test-secret-create=data-test-secret-create
|
||||
data-test-credentials-link=data-test-credentials-link
|
||||
data-test-backend-credentials=data-test-backend-credentials
|
||||
data-test-edit-link=data-test-edit-link
|
||||
data-test-sign-link=data-test-sign-link
|
||||
data-test-transit-link=data-test-transit-link
|
||||
data-test-transit-key-actions-link=data-test-transit-key-actions-link
|
||||
data-test-transit-action-link=data-test-transit-action-link
|
||||
}}
|
||||
{{yield}}
|
||||
{{/link-to}}
|
|
@ -1,4 +1,4 @@
|
|||
{{#with (options-for-backend model.type) as |options|}}
|
||||
{{#with (options-for-backend model.engineType) as |options|}}
|
||||
<PageHeader as |p|>
|
||||
<p.top>
|
||||
{{#key-value-header
|
||||
|
@ -8,9 +8,9 @@
|
|||
}}
|
||||
<li>
|
||||
<span class="sep">/</span>
|
||||
<a href={{href-to "vault.cluster.secrets"}}>
|
||||
{{#link-to "vault.cluster.secrets"}}
|
||||
secrets
|
||||
</a>
|
||||
{{/link-to}}
|
||||
</li>
|
||||
{{/key-value-header}}
|
||||
</p.top>
|
||||
|
@ -44,15 +44,12 @@
|
|||
{{/unless}}
|
||||
{{#if (or (eq model.type "aws") (eq model.type "ssh") (eq model.type "pki"))}}
|
||||
<div class="control">
|
||||
<a href={{href-to
|
||||
"vault.cluster.settings.configure-secret-backend"
|
||||
model.id
|
||||
}}
|
||||
{{#link-to "vault.cluster.settings.configure-secret-backend" model.id
|
||||
class="button has-icon-right is-ghost is-compact"
|
||||
data-test-secret-backend-configure=true
|
||||
>
|
||||
Configure {{i-con glyph="chevron-right" size=11}}
|
||||
</a>
|
||||
}}
|
||||
Configure {{i-con glyph="chevron-right" size=11}}
|
||||
{{/link-to}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</p.levelRight>
|
||||
|
@ -89,7 +86,7 @@
|
|||
<div class="box is-bottomless is-marginless is-fullwidth is-paddingless">
|
||||
<nav class="tabs sub-nav">
|
||||
<ul>
|
||||
{{#if (contains model.type (supported-secret-backends))}}
|
||||
{{#if (contains model.engineType (supported-secret-backends))}}
|
||||
{{#link-to 'vault.cluster.secrets.backend.list-root' tagName="li" activeClass="is-active" current-when="vault.cluster.secrets.backend.list-root vault.cluster.secrets.backend.list"}}
|
||||
{{#link-to 'vault.cluster.secrets.backend.list-root'}}
|
||||
{{capitalize (pluralize options.item)}}
|
||||
|
|
|
@ -5,9 +5,9 @@
|
|||
<ul>
|
||||
{{#each tabs as |tab|}}
|
||||
{{#link-to params=tab.routeParams tagName="li" data-test-auth-section-tab=true}}
|
||||
<a href={{href-to params=tab.routeParams}}>
|
||||
{{#link-to params=tab.routeParams}}
|
||||
{{tab.label}}
|
||||
</a>
|
||||
{{/link-to}}
|
||||
{{/link-to}}
|
||||
{{/each}}
|
||||
</ul>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<NavHeader as |Nav|>
|
||||
<Nav.home>
|
||||
<HomeLink @class="navbar-item splash-page-logo">
|
||||
<HomeLink @class="navbar-item splash-page-logo has-text-white">
|
||||
{{partial "svg/vault-edition-logo"}}
|
||||
</HomeLink>
|
||||
</Nav.home>
|
||||
|
@ -18,6 +18,9 @@
|
|||
<div class="splash-page-header">
|
||||
{{yield (hash header=(component 'splash-page/splash-header'))}}
|
||||
</div>
|
||||
<div class="splash-page-sub-header">
|
||||
{{yield (hash sub-header=(component 'splash-page/splash-header'))}}
|
||||
</div>
|
||||
<div class="login-form box is-paddingless is-relative">
|
||||
{{yield (hash content=(component 'splash-page/splash-content'))}}
|
||||
</div>
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
{{message-error errors=errors}}
|
||||
<form onsubmit={{action "doSubmit"}}>
|
||||
{{partial (concat "partials/tools/" selectedAction)}}
|
||||
</form>
|
||||
|
|
|
@ -47,6 +47,7 @@
|
|||
</div>
|
||||
{{else}}
|
||||
<div class="box is-sideless is-fullwidth is-marginless">
|
||||
<NamespaceReminder @mode="perform" @noun="datakey creation" />
|
||||
<div class="field">
|
||||
<label for="param" class="is-label">Output format</label>
|
||||
<div class="control is-expanded">
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
</div>
|
||||
{{else}}
|
||||
<div class="box is-sideless is-fullwidth is-marginless">
|
||||
<NamespaceReminder @mode="perform" @noun="encryption" />
|
||||
{{key-version-select
|
||||
key=key
|
||||
onVersionChange=(action (mut key_version))
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
</div>
|
||||
{{else}}
|
||||
<div class="box is-sideless is-fullwidth is-marginless">
|
||||
<NamespaceReminder @mode="perform" @noun="HMAC creation" />
|
||||
{{key-version-select
|
||||
key=key
|
||||
onVersionChange=(action (mut key_version))
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<form {{action 'doSubmit' (hash ciphertext=ciphertext context=context nonce=nonce key_version=key_version) on="submit"}}>
|
||||
<div class="box is-sideless is-fullwidth is-marginless">
|
||||
<NamespaceReminder @mode="perform" @noun="rewrap" />
|
||||
{{key-version-select
|
||||
key=key
|
||||
onVersionChange=(action (mut key_version))
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
</div>
|
||||
{{else}}
|
||||
<div class="box is-sideless is-fullwidth is-marginless">
|
||||
<NamespaceReminder @mode="perform" @noun="signing" />
|
||||
{{key-version-select
|
||||
key=key
|
||||
onVersionChange=(action (mut key_version))
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
id="token"
|
||||
class="input"
|
||||
data-test-token=true
|
||||
autocomplete="off"
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
value=token
|
||||
name="token"
|
||||
class="input"
|
||||
autocomplete="off"
|
||||
data-test-token=true
|
||||
}}
|
||||
</div>
|
||||
|
|
|
@ -42,9 +42,9 @@
|
|||
</div>
|
||||
<div class="level-right">
|
||||
{{#if replicationDisabled}}
|
||||
<a href="{{href-to 'vault.cluster.replication.mode.index' cluster.name mode}}" class="button is-primary">
|
||||
{{#link-to "vault.cluster.replication.mode.index" cluster.name mode class="button is-primary"}}
|
||||
Enable
|
||||
</a>
|
||||
{{/link-to}}
|
||||
{{else if (eq mode 'dr')}}
|
||||
{{cluster.drReplicationStateDisplay}}
|
||||
{{else if (eq mode 'performance')}}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue