UI - make engine list more consistent with the auth method list (#4598)

* remove expanding behavior from engines list and add a configuration route

* use page header component, secret tab component for the template on the secret engine configuration route

* move abstraction to secret-list-header and remove secret-tabs

* add attrs to secret engine model and adjust mount controller code to support that

* fix top level nav so that we can use the back button properly

* fix tests
This commit is contained in:
Matthew Irish
2018-05-23 11:25:52 -05:00
committed by GitHub
parent 0545944fc5
commit c722bc0e39
27 changed files with 284 additions and 136 deletions

View File

@@ -0,0 +1,5 @@
import Ember from 'ember';
export default Ember.Component.extend({
tagName: '',
});

View File

@@ -0,0 +1,5 @@
import Ember from 'ember';
export default Ember.Component.extend({
tagName: '',
});

View File

@@ -0,0 +1,5 @@
import Ember from 'ember';
export default Ember.Component.extend({
tagName: '',
});

View File

@@ -0,0 +1,6 @@
import Ember from 'ember';
export default Ember.Component.extend({
tagName: '',
hasLevel: true,
});

View File

@@ -0,0 +1,14 @@
import Ember from 'ember';
export default Ember.Component.extend({
tagName: '',
// api
isCertTab: false,
isConfigure: false,
baseKey: null,
backendCrumb: null,
model: null,
});

View File

@@ -30,7 +30,6 @@ export default Ember.Controller.extend({
description: null,
default_lease_ttl: null,
max_lease_ttl: null,
force_no_cache: null,
showConfig: false,
local: false,
sealWrap: false,
@@ -50,7 +49,6 @@ export default Ember.Controller.extend({
description: null,
default_lease_ttl: null,
max_lease_ttl: null,
force_no_cache: null,
local: false,
showConfig: false,
sealWrap: false,
@@ -82,7 +80,6 @@ export default Ember.Controller.extend({
selectedType: type,
description,
default_lease_ttl,
force_no_cache,
local,
max_lease_ttl,
sealWrap,
@@ -92,7 +89,6 @@ export default Ember.Controller.extend({
'selectedType',
'description',
'default_lease_ttl',
'force_no_cache',
'local',
'max_lease_ttl',
'sealWrap',
@@ -112,9 +108,8 @@ export default Ember.Controller.extend({
if (this.get('showConfig')) {
attrs.config = {
default_lease_ttl,
max_lease_ttl,
force_no_cache,
defaultLeaseTtl: default_lease_ttl,
maxLeaseTtl: max_lease_ttl,
};
}

View File

@@ -2,5 +2,7 @@ import attr from 'ember-data/attr';
import Fragment from 'ember-data-model-fragments/fragment';
export default Fragment.extend({
version: attr('number'),
version: attr('number', {
label: 'Version',
}),
});

View File

@@ -3,6 +3,8 @@ import DS from 'ember-data';
import { queryRecord } from 'ember-computed-query';
import { fragment } from 'ember-data-model-fragments/attributes';
import { expandAttributeMeta } from 'vault/utils/field-to-attrs';
const { attr } = DS;
const { computed } = Ember;
@@ -16,11 +18,26 @@ export default DS.Model.extend({
name: attr('string'),
type: attr('string'),
description: attr('string'),
config: attr('object'),
options: fragment('mount-options'),
config: fragment('mount-config', { defaultValue: {} }),
options: fragment('mount-options', { defaultValue: {} }),
local: attr('boolean'),
sealWrap: attr('boolean'),
formFields: [
'type',
'path',
'description',
'accessor',
'local',
'sealWrap',
'config.{defaultLeaseTtl,maxLeaseTtl}',
'options.{version}',
],
attrs: computed('formFields', function() {
return expandAttributeMeta(this, this.get('formFields'));
}),
shouldIncludeInList: computed('type', function() {
return !LIST_EXCLUDED_BACKENDS.includes(this.get('type'));
}),

View File

@@ -68,6 +68,7 @@ Router.map(function() {
this.route('backends', { path: '/' });
this.route('backend', { path: '/:backend' }, function() {
this.route('index', { path: '/' });
this.route('configuration');
// because globs / params can't be empty,
// we have to special-case ids of '' with thier own routes
this.route('list-root', { path: '/list/' });

View File

@@ -0,0 +1,7 @@
import Ember from 'ember';
export default Ember.Route.extend({
model() {
return this.modelFor('vault.cluster.secrets.backend');
},
});

View File

@@ -20,8 +20,7 @@
}
}
.popup-menu-trigger {
width: 3rem;
height: 2rem;
min-width: auto;
}
.popup-menu-trigger.is-active {
&,

View File

@@ -146,7 +146,6 @@ $button-box-shadow-standard: 0 3px 1px 0 rgba($black, 0.12);
}
}
&.is-more-icon,
&.tool-tip-trigger {
color: $black;
min-width: auto;

View File

@@ -40,7 +40,7 @@
{{/if}}
</li>
<li class="{{if (is-active-route (array 'vault.cluster.policies' 'vault.cluster.policy')) 'is-active'}}">
<a href="{{href-to "vault.cluster.policies" activeClusterName current-when='vault.cluster.policies vault.cluster.policy'}}">
<a href="{{href-to "vault.cluster.policies" "acl" current-when='vault.cluster.policies vault.cluster.policy'}}">
Policies
</a>
</li>

View File

@@ -1,4 +1,5 @@
<ul>
{{yield}}
{{#each secretPath as |path index|}}
<li class="{{if (is-active-route path.path path.model isExact=true) 'is-active'}}">
<span class="sep">&#x0002f;</span>

View File

@@ -0,0 +1 @@
{{yield}}

View File

@@ -0,0 +1 @@
{{yield}}

View File

@@ -0,0 +1,15 @@
<header class="page-header">
{{#if hasLevel}}
{{yield (hash top=(component 'page-header-top'))}}
<div class="level">
<div class="level-left">
{{yield (hash levelLeft=(component 'page-header-level-left'))}}
</div>
<div class="level-right field is-grouped">
{{yield (hash levelRight=(component 'page-header-level-right'))}}
</div>
</div>
{{else}}
{{yield (hash top=(component 'page-header-top'))}}
{{/if}}
</header>

View File

@@ -1,5 +1,5 @@
{{#basic-dropdown class="popup-menu" horizontalPosition="auto-right" verticalPosition="below" onOpen=onOpen as |d|}}
{{#d.trigger tagName="button" class=(concat "popup-menu-trigger button is-transparent " (if d.isOpen "is-active")) data-test-popup-menu-trigger=true}}
{{#d.trigger tagName="button" class=(concat "popup-menu-trigger button is-ghost " (if d.isOpen "is-active")) data-test-popup-menu-trigger=true}}
{{i-con
glyph="more"
class="has-text-black auto-width"

View File

@@ -0,0 +1,108 @@
{{#with (options-for-backend model.type) as |options|}}
{{#page-header as |p|}}
{{#p.top}}
{{#key-value-header
baseKey=baseKey
path="vault.cluster.secrets.backend.list"
root=backendCrumb
}}
<li>
<span class="sep">&#x0002f;</span>
<a href={{href-to "vault.cluster.secrets"}}>
secrets
</a>
</li>
{{/key-value-header}}
{{/p.top}}
{{#p.levelLeft}}
<h1 class="title is-3">
{{model.id}}
<span class="tag is-outlined is-inverted has-text-grey-dark is-font-mono">
{{or options.displayName (capitalize model.type)}}
</span>
{{#if (eq model.options.version 2)}}
<span class="has-text-grey-dark has-text-weight-normal is-size-6">
Version 2
</span>
{{/if}}
</h1>
{{/p.levelLeft}}
{{#p.levelRight}}
{{#unless (or isCertTab isConfigure)}}
<div class="control">
{{#secret-link
mode="create"
secret=(or baseKey.id '')
queryParams=(query-params initialKey='')
class="button has-icon-right is-ghost is-compact"
data-test-secret-create=true
}}
{{options.create}}
{{i-con glyph="chevron-right" size=11}}
{{/secret-link}}
</div>
{{/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
}}
class="button has-icon-right is-ghost is-compact"
data-test-secret-backend-configure=true
>
Configure {{i-con glyph="chevron-right" size=11}}
</a>
</div>
{{/if}}
{{/p.levelRight}}
{{/page-header}}
{{#if options.tabs}}
<div class="box is-bottomless is-marginless is-fullwidth is-paddingless">
<nav class="tabs sub-nav">
<ul>
{{#each options.tabs as |oTab|}}
{{#if oTab.tab}}
{{#link-to 'vault.cluster.secrets.backend.list-root' (query-params tab=oTab.tab) tagName="li" activeClass="is-active" data-test-tab=oTab.label}}
{{#link-to 'vault.cluster.secrets.backend.list-root' (query-params tab=oTab.tab)}}
{{oTab.label}}
{{/link-to}}
{{/link-to}}
{{else}}
{{#link-to 'vault.cluster.secrets.backend.list-root' (query-params tab='') tagName="li" activeClass="is-active" data-test-tab=oTab.label}}
{{#link-to 'vault.cluster.secrets.backend.list-root' (query-params tab='')}}
{{oTab.label}}
{{/link-to}}
{{/link-to}}
{{/if}}
{{/each}}
{{#link-to 'vault.cluster.secrets.backend.configuration' tagName="li" activeClass="is-active"}}
{{#link-to 'vault.cluster.secrets.backend.configuration' }}
Configuration
{{/link-to}}
{{/link-to}}
</ul>
</nav>
</div>
{{else}}
{{!-- if there are no tabs in the options, we'll hardcode them here --}}
<div class="box is-bottomless is-marginless is-fullwidth is-paddingless">
<nav class="tabs sub-nav">
<ul>
{{#if (contains model.type (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)}}
{{/link-to}}
{{/link-to}}
{{/if}}
{{#link-to 'vault.cluster.secrets.backend.configuration' tagName="li" activeClass="is-active"}}
{{#link-to 'vault.cluster.secrets.backend.configuration' }}
Configuration
{{/link-to}}
{{/link-to}}
</ul>
</nav>
</div>
{{/if}}
{{/with}}

View File

@@ -0,0 +1,19 @@
{{secret-list-header model=model isConfigure=true backendCrumb=(hash label=model.id text=model.id path="vault.cluster.secrets.backend.list-root" model=model.id)}}
<div class="box is-fullwidth is-sideless is-paddingless is-marginless">
{{#each model.attrs as |attr|}}
{{#if (eq attr.type "object")}}
{{info-table-row
alwaysRender=true
label=(or attr.options.label (to-label attr.name))
value=(stringify (get model attr.name))
}}
{{else}}
{{info-table-row
alwaysRender=(not-eq attr.name 'options.version')
label=(or attr.options.label (to-label attr.name))
value=(get model attr.name)
}}
{{/if}}
{{/each}}
</div>

View File

@@ -1,79 +1,6 @@
{{secret-list-header isCertTab=(eq tab "certs") model=backendModel baseKey=baseKey backendCrumb=backendCrumb}}
{{#with (options-for-backend backendType tab) as |options|}}
<header class="page-header">
{{key-value-header
baseKey=baseKey
path="vault.cluster.secrets.backend.list"
root=backendCrumb
}}
<div class="level">
<div class="level-left">
<h1 class="title is-3">
{{backend}}
<span class="tag is-outlined is-inverted has-text-grey-dark is-font-mono">
{{or options.displayName (capitalize backendType)}}
</span>
{{#if (eq backendModel.options.version 2)}}
<span class="has-text-grey-dark has-text-weight-normal is-size-6">
Version 2
</span>
{{/if}}
</h1>
</div>
<div class="level-right field is-grouped">
{{#if (not-eq tab "certs")}}
<div class="control">
{{#secret-link
mode="create"
secret=(or baseKey.id '')
queryParams=(query-params initialKey='')
class="button has-icon-right is-ghost is-compact"
data-test-secret-create=true
}}
{{options.create}}
{{i-con glyph="chevron-right" size=11}}
{{/secret-link}}
</div>
{{/if}}
{{#if (or (eq backendType "aws") (eq backendType "ssh") (eq backendType "pki"))}}
<div class="control">
<a href={{href-to
"vault.cluster.settings.configure-secret-backend"
backend
}}
class="button has-icon-right is-ghost is-compact"
data-test-secret-backend-configure=true
>
Configure
{{i-con glyph="chevron-right" size=11}}
</a>
</div>
{{/if}}
</div>
</div>
</header>
{{#if options.tabs}}
<div class="box is-bottomless is-marginless is-fullwidth is-paddingless">
<nav class="tabs sub-nav">
<ul>
{{#each options.tabs as |oTab|}}
{{#if oTab.tab}}
{{#link-to 'vault.cluster.secrets.backend.list-root' (query-params tab=oTab.tab) tagName="li" activeClass="is-active" data-test-tab=oTab.label}}
{{#link-to 'vault.cluster.secrets.backend.list-root' (query-params tab=oTab.tab)}}
{{oTab.label}}
{{/link-to}}
{{/link-to}}
{{else}}
{{#link-to 'vault.cluster.secrets.backend.list-root' (query-params tab='') tagName="li" activeClass="is-active" data-test-tab=oTab.label}}
{{#link-to 'vault.cluster.secrets.backend.list-root' (query-params tab='')}}
{{oTab.label}}
{{/link-to}}
{{/link-to}}
{{/if}}
{{/each}}
</ul>
</nav>
</div>
{{/if}}
<div class="box is-sideless has-background-grey-lighter has-short-padding is-marginless">
<div class="level">
<div class="level-left">
@@ -90,17 +17,15 @@
mode=(if (eq tab 'certs') 'secrets-cert' 'secrets')
}}
{{#if filterFocused}}
&nbsp;
&nbsp;
{{#if filterMatchesKey}}
{{#unless filterIsFolder}}
<p class="help has-text-grey is-size-8">
<p class="input-hint">
<kbd>Enter</kbd> to view {{filter}}
</p>
{{/unless}}
{{/if}}
{{#if firstPartialMatch}}
<p class="help has-text-grey is-size-8">
<p class="input-hint">
<kbd>Tab</kbd> to autocomplete
</p>
{{/if}}

View File

@@ -18,11 +18,8 @@
{{#linked-block
"vault.cluster.secrets.backend.list-root"
backend.id
class=(concat
'box is-sideless is-marginless has-pointer '
(if (get this (concat backend.accessor '-open')) 'has-background-white-bis')
)
data-test-secret-backend-link=backend.id
class="box is-sideless is-marginless has-pointer"
data-test-secret-backend-row=backend.id
}}
<div class="level is-mobile">
<div class="level-left">
@@ -53,25 +50,35 @@
</div>
<div class="level-right is-flex is-paddingless is-marginless">
<div class="level-item">
<button class="button is-more-icon is-ghost" data-test-secret-backend-detail=true type="button" {{action (toggle (concat backend.accessor '-open') this)}}>
{{i-con glyph="more" size=16 aria-label=(concat backend.path ' details')}}
</button>
</div>
</div>
</div>
{{#if (get this (concat backend.accessor '-open'))}}
{{partial "partials/backend-details"}}
{{#popup-menu name="engine-menu"}}
<nav class="menu">
<ul class="menu-list">
<li class="action">
<a href="{{href-to 'vault.cluster.secrets.backend.configuration' backend.id}}">
View Configuration
</a>
</li>
{{#if item.updatePath.isPending}}
<li class="action">
<button disabled=true type="button" class="link button is-loading is-transparent"></button>
</li>
{{/if}}
</ul>
</nav>
{{/popup-menu}}
</div>
</div>
</div>
{{/linked-block}}
{{/each}}
{{#each unsupportedBackends as |backend|}}
<div class="box is-sideless is-marginless has-background-transition {{if (get this (concat backend.accessor '-open')) 'has-background-white-bis'}}"
<div class="box is-sideless is-marginless"
data-test-secret-backend-row={{backend.id}}
>
<div class="level is-mobile">
<div class="level-left">
<div>
<div class="has-text-weight-semibold">
<div data-test-secret-path class="has-text-weight-semibold">
{{i-con glyph="folder" size=14 class="has-text-grey-light"}} {{backend.path}}
</div>
<span class="tag">
@@ -90,14 +97,19 @@
</div>
<div class="level-right is-flex is-paddingless is-marginless">
<div class="level-item">
<button class="button is-more-icon is-ghost" data-test-secret-backend-detail=true type="button" {{action (toggle (concat backend.accessor '-open') this)}}>
{{i-con glyph="more" size=16 aria-label=(concat backend.path ' details')}}
</button>
{{#popup-menu name="engine-menu"}}
<nav class="menu">
<ul class="menu-list">
<li class="action">
<a href="{{href-to 'vault.cluster.secrets.backend.configuration' backend.id}}" data-test-engine-config>
View Configuration
</a>
</li>
</ul>
</nav>
{{/popup-menu}}
</div>
</div>
</div>
{{#if (get this (concat backend.accessor '-open'))}}
{{partial "partials/backend-details"}}
{{/if}}
</div>
{{/each}}

View File

@@ -42,11 +42,12 @@ export const expandAttributeMeta = function(modelClass, attributeNames, namePref
let attributeMap = map || new Map();
modelClass.eachAttribute((name, meta) => {
let fieldName = namePrefix ? namePrefix + name : name;
if (meta.isFragment) {
let maybeFragment = Ember.get(modelClass, fieldName);
if (meta.isFragment && maybeFragment) {
// pass the fragment and all fields that start with
// the fragment name down to get extracted from the Fragment
expandAttributeMeta(
Ember.get(modelClass, fieldName),
maybeFragment,
fields.filter(f => f.startsWith(fieldName)),
fieldName + '.',
attributeMap
@@ -60,13 +61,15 @@ export const expandAttributeMeta = function(modelClass, attributeNames, namePref
// so we'll replace each key in `fields` with the expanded meta
fields = fields.map(field => {
let meta = attributeMap.get(field);
const { type, options } = meta;
if (meta) {
var { type, options } = meta;
}
return {
// using field name here because it is the full path,
// name on the attribute meta will be relative to the fragment it's on
name: field,
type,
options,
type: type,
options: options,
};
});
return fields;

View File

@@ -1,5 +1,6 @@
import { test } from 'qunit';
import moduleForAcceptance from 'vault/tests/helpers/module-for-acceptance';
import backendListPage from 'vault/tests/pages/secrets/backends';
moduleForAcceptance('Acceptance | settings', {
beforeEach() {
@@ -36,14 +37,19 @@ test('settings', function(assert) {
find('[data-test-flash-message]').text().trim(),
`Successfully mounted '${type}' at '${path}'!`
);
let row = backendListPage.rows().findByPath(path);
row.menu();
});
andThen(() => {
backendListPage.configLink();
});
//show mount details
click(`[data-test-secret-backend-row="${path}"] [data-test-secret-backend-detail]`);
andThen(() => {
assert.ok(
find('[data-test-secret-backend-details="default-ttl"]').text().match(/100/),
'displays the input ttl'
currentURL(),
'/vault/secrets/${path}/configuration',
'navigates to the config page'
);
});
});

View File

@@ -1,7 +1,7 @@
import { test } from 'qunit';
import moduleForAcceptance from 'vault/tests/helpers/module-for-acceptance';
import page from 'vault/tests/pages/settings/mount-secret-backend';
import listPage from 'vault/tests/pages/secrets/backends';
import configPage from 'vault/tests/pages/secrets/backend/configuration';
moduleForAcceptance('Acceptance | settings/mount-secret-backend', {
beforeEach() {
@@ -30,13 +30,9 @@ test('it sets the ttl corrects when mounting', function(assert) {
.maxTTLUnit('h')
.submit();
listPage.visit();
configPage.visit({backend: path});
andThen(() => {
listPage.links().findByPath(path).toggleDetails();
});
andThen(() => {
const details = listPage.links().findByPath(path);
assert.equal(details.defaultTTL, defaultTTLSeconds, 'shows the proper TTL');
assert.equal(details.maxTTL, maxTTLSeconds, 'shows the proper max TTL');
assert.equal(configPage.defaultTTL, defaultTTLSeconds, 'shows the proper TTL');
assert.equal(configPage.maxTTL, maxTTLSeconds, 'shows the proper max TTL');
});
});

View File

@@ -0,0 +1,7 @@
import { create, visitable, text } from 'ember-cli-page-object';
export default create({
visit: visitable('/vault/secrets/:backend/configuration'),
defaultTTL: text('[data-test-row-value="Default Lease TTL"]'),
maxTTL: text('[data-test-row-value="Max Lease TTL"]'),
});

View File

@@ -1,17 +1,16 @@
import { create, visitable, collection, text, clickable } from 'ember-cli-page-object';
import { create, visitable, collection, clickable, text } from 'ember-cli-page-object';
export default create({
visit: visitable('/vault/secrets'),
links: collection({
itemScope: '[data-test-secret-backend-link]',
rows: collection({
itemScope: '[data-test-secret-backend-row]',
item: {
path: text('[data-test-secret-path]'),
toggleDetails: clickable('[data-test-secret-backend-detail]'),
defaultTTL: text('[data-test-secret-backend-details="default-ttl"]'),
maxTTL: text('[data-test-secret-backend-details="max-ttl"]'),
menu: clickable('[data-test-popup-menu-trigger]'),
},
findByPath(path) {
return this.toArray().findBy('path', path + '/');
},
}),
configLink: clickable('[data-test-engine-config]'),
});