UI: Redesign transit UX (#8304)

* add placeholder for Key actions tab

* navigate to key items by default

* add placeholder key actions list page

* remove extra whitespace from component blueprint

* add SelectableCard

* move key actions from side nav to top nav

* make tabs active

* remove toolbar from key actions pages

* add divs to link to each key action on key actions page

* move preview-head to gitignore

* use selectable card css

* remove key actions

* use css grid

* update selectable card styling

* update Key Actions page header

* make cards clickable

* refactor supportedActions to include glyph

* make header black on hover

* rename selectable-card transit card and update styling

* add description and glyph for other key types

* use human readable titles for key action names

* update tests; still need to fix failing ones

* use datakey instead of data-key

* fix some failing tests

* fix more tests

* remove extra chevron from rotate button

* remove whitespace

* remove pauseTest

* use rename export to export key in the template instead of the model

* fix last few failing tests

* WIP

* link to key actions page by default

* test for transit action title

* only add query params when viewing a transit secret

* update structure icons

* add missing structure icons

* resolve merge conflicts from rebase

* use filter and map for supported actions

* only add query params for transit secrets
This commit is contained in:
Noelle Daley 2020-02-14 11:20:44 -06:00 committed by GitHub
parent 0ab4c138c2
commit b004a24cdf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 217 additions and 116 deletions

1
ui/.gitignore vendored
View File

@ -17,6 +17,7 @@
/npm-debug.log*
/testem.log
/yarn-error.log
/.storybook/preview-head.html
# ember-try
/.node_modules.ember-try/

View File

@ -1,27 +0,0 @@
<meta name="vault/config/environment" content="%7B%22modulePrefix%22%3A%22vault%22%2C%22environment%22%3A%22development%22%2C%22rootURL%22%3A%22%2Fui%2F%22%2C%22locationType%22%3A%22auto%22%2C%22EmberENV%22%3A%7B%22FEATURES%22%3A%7B%7D%2C%22EXTEND_PROTOTYPES%22%3A%7B%22Date%22%3Afalse%7D%2C%22_JQUERY_INTEGRATION%22%3Afalse%7D%2C%22APP%22%3A%7B%22POLLING_URLS%22%3A%5B%22sys%2Fhealth%22%2C%22sys%2Freplication%2Fstatus%22%2C%22sys%2Fseal-status%22%5D%2C%22NAMESPACE_ROOT_URLS%22%3A%5B%22sys%2Fhealth%22%2C%22sys%2Fseal-status%22%2C%22sys%2Flicense%2Ffeatures%22%5D%2C%22DEFAULT_PAGE_SIZE%22%3A15%2C%22LOG_TRANSITIONS%22%3Atrue%7D%2C%22flashMessageDefaults%22%3A%7B%22timeout%22%3A7000%2C%22sticky%22%3Afalse%7D%2C%22contentSecurityPolicyHeader%22%3A%22Content-Security-Policy%22%2C%22contentSecurityPolicyMeta%22%3Atrue%2C%22contentSecurityPolicy%22%3A%7B%22connect-src%22%3A%5B%22'self'%22%5D%2C%22img-src%22%3A%5B%22'self'%22%2C%22data%3A%22%5D%2C%22form-action%22%3A%5B%22'none'%22%5D%2C%22script-src%22%3A%5B%22'self'%22%5D%2C%22style-src%22%3A%5B%22'unsafe-inline'%22%2C%22'self'%22%5D%2C%22default-src%22%3A%5B%22'none'%22%5D%2C%22font-src%22%3A%5B%22'self'%22%5D%2C%22media-src%22%3A%5B%22'self'%22%5D%7D%2C%22exportApplicationGlobal%22%3Atrue%7D" />
<meta name="kmip/config/environment" content="%7B%22modulePrefix%22%3A%22kmip%22%2C%22environment%22%3A%22development%22%7D" />
<meta name="open-api-explorer/config/environment" content="%7B%22modulePrefix%22%3A%22open-api-explorer%22%2C%22environment%22%3A%22development%22%2C%22APP%22%3A%7B%22NAMESPACE_ROOT_URLS%22%3A%5B%22sys/health%22%2C%22sys/seal-status%22%2C%22sys/license/features%22%5D%7D%7D" />
<meta name="replication/config/environment" content="%7B%22modulePrefix%22%3A%22replication%22%2C%22environment%22%3A%22development%22%7D" />
<meta name="vault/config/asset-manifest"
content="%7B%22bundles%22%3A%7B%22kmip%22%3A%7B%22assets%22%3A%5B%7B%22uri%22%3A%22/ui/engines-dist/kmip/assets/engine-vendor.js%22%2C%22type%22%3A%22js%22%7D%2C%7B%22uri%22%3A%22/ui/engines-dist/kmip/assets/engine.js%22%2C%22type%22%3A%22js%22%7D%5D%7D%2C%22open-api-explorer%22%3A%7B%22assets%22%3A%5B%7B%22uri%22%3A%22/ui/engines-dist/open-api-explorer/assets/engine-vendor.css%22%2C%22type%22%3A%22css%22%7D%2C%7B%22uri%22%3A%22/ui/engines-dist/open-api-explorer/assets/engine-vendor.js%22%2C%22type%22%3A%22js%22%7D%2C%7B%22uri%22%3A%22/ui/engines-dist/open-api-explorer/assets/engine.css%22%2C%22type%22%3A%22css%22%7D%2C%7B%22uri%22%3A%22/ui/engines-dist/open-api-explorer/assets/engine.js%22%2C%22type%22%3A%22js%22%7D%5D%7D%2C%22replication%22%3A%7B%22assets%22%3A%5B%7B%22uri%22%3A%22/ui/engines-dist/replication/assets/engine-vendor.js%22%2C%22type%22%3A%22js%22%7D%2C%7B%22uri%22%3A%22/ui/engines-dist/replication/assets/engine.js%22%2C%22type%22%3A%22js%22%7D%5D%7D%7D%7D" />
<link rel="stylesheet" href="/assets/vendor.css" />
<link rel="stylesheet" href="/assets/vault.css" />
<link rel="icon" href="/favicon.png" />
<script>
(function() {
var srcUrl = null;
var host = location.hostname || 'localhost';
var defaultPort = location.protocol === 'https:' ? 443 : 80;
var port = 4200;
var path = '';
var prefixURL = '';
var src = srcUrl || prefixURL + '/_lr/livereload.js?port=' + port + '&host=' + host + path;
var script = document.createElement('script');
script.type = 'text/javascript';
script.src = location.protocol + '//' + host + ':4200' + src;
document.getElementsByTagName('head')[0].appendChild(script);
}());
</script>
<script>runningTests = true;</script>
<script src="/assets/vendor.js"></script>
<script src="/assets/vault.js"></script>

View File

@ -0,0 +1,10 @@
import { helper } from '@ember/component/helper';
export function secretQueryParams([backendType]) {
if (backendType === 'transit') {
return { tab: 'actions' };
}
return;
}
export default helper(secretQueryParams);

View File

@ -4,7 +4,7 @@
//
// 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
// //parameters 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'),
//

View File

@ -7,14 +7,38 @@ import lazyCapabilities, { apiPath } from 'vault/macros/lazy-capabilities';
const { attr } = DS;
const ACTION_VALUES = {
encrypt: 'supportsEncryption',
decrypt: 'supportsDecryption',
datakey: 'supportsEncryption',
rewrap: 'supportsEncryption',
sign: 'supportsSigning',
hmac: true,
verify: true,
export: 'exportable',
encrypt: {
isSupported: 'supportsEncryption',
description: 'Looks up wrapping properties for the given token',
glyph: 'lock-closed',
},
decrypt: {
isSupported: 'supportsDecryption',
description: 'Decrypts the provided ciphertext using this key',
glyph: 'envelope-unsealed--outline',
},
datakey: {
isSupported: 'supportsEncryption',
description: 'Generates a new key and value encrypted with this key',
glyph: 'key',
},
rewrap: {
isSupported: 'supportsEncryption',
description: 'Rewraps the ciphertext using the latest version of the named key',
glyph: 'refresh-default',
},
sign: {
isSupported: 'supportsSigning',
description: 'Get the cryptographic signature of the given data',
glyph: 'edit',
},
hmac: { isSupported: true, description: 'Generate a data digest using a hash algorithm', glyph: 'remix' },
verify: {
isSupported: true,
description: 'Validate the provided signature for the given data',
glyph: 'check-circle-outline',
},
export: { isSupported: 'exportable', description: 'Get the named key', glyph: 'exit' },
};
export default DS.Model.extend({
@ -56,12 +80,14 @@ export default DS.Model.extend({
},
supportedActions: computed('type', function() {
return Object.keys(ACTION_VALUES).filter(name => {
const isSupported = ACTION_VALUES[name];
if (typeof isSupported === 'boolean') {
return isSupported;
}
return get(this, isSupported);
return Object.keys(ACTION_VALUES)
.filter(name => {
const { isSupported } = ACTION_VALUES[name];
return typeof isSupported === 'boolean' || get(this, isSupported);
})
.map(name => {
const { description, glyph } = ACTION_VALUES[name];
return { name, description, glyph };
});
}),
@ -116,9 +142,7 @@ export default DS.Model.extend({
return types;
}),
backend: attr('string', {
readOnly: true,
}),
backend: attr('string'),
rotatePath: lazyCapabilities(apiPath`${'backend'}/keys/${'id'}/rotate`, 'backend', 'id'),
canRotate: alias('rotatePath.canUpdate'),

View File

@ -0,0 +1,41 @@
.transit-card-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 0.2fr));
grid-template-rows: 1fr;
align-content: start;
grid-gap: 2rem;
margin-top: $spacing-l;
}
.transit-card {
border-radius: $radius;
box-shadow: 0 0 0 1px rgba($grey-dark, 0.3), $box-shadow-middle;
display: grid;
grid-template-columns: 0.45fr 2fr;
padding: $spacing-m;
.transit-icon {
justify-self: center;
}
.transit-action-description {
font-family: $family-sans;
font-size: $size-8;
color: $grey;
}
.title {
color: $grey;
font-size: $size-7;
margin-bottom: $spacing-xxs;
}
&:hover {
box-shadow: 0 0 0 1px $blue-500, $box-shadow-middle;
background: $blue-010;
.title {
color: initial;
}
}
}

View File

@ -88,6 +88,7 @@
@import './components/token-expire-warning';
@import './components/toolbar';
@import './components/tool-tip';
@import './components/transit-card';
@import './components/unseal-warning';
@import './components/ui-wizard';
@import './components/vault-loading';

View File

@ -20,5 +20,4 @@
</h1>
</p.levelLeft>
</PageHeader>
{{partial (concat 'partials/transit-form-' mode)}}

View File

@ -9,7 +9,6 @@
data-test-transit-key-rotate="true"
>
Rotate encryption key
<Chevron @isButton={{true}} />
</ConfirmAction>
{{/if}}
{{else}}

View File

@ -1,3 +1,5 @@
{{#linked-block
(concat
"vault.cluster.secrets.backend."
@ -8,12 +10,14 @@
class="list-item-row"
data-test-secret-link=item.id
encode=true
queryParams=(secret-query-params backendModel.type)
}}
<div class="columns is-mobile">
<div class="column is-10">
<SecretLink
@mode={{if item.isFolder "list" "show" }}
@secret={{item.id}}
@queryParams={{if (eq backendModel.type "transit") (query-params tab="actions") ""}}
@class="has-text-black has-text-weight-semibold"><Icon
@glyph={{if item.isFolder 'folder-outline' 'file-outline' }}
@class="has-text-grey-light"/>{{if (eq item.id ' ') '(self)' (or item.keyWithoutParent item.id)}}

View File

@ -1,12 +1,24 @@
<div class="tabs-container box is-sideless is-fullwidth is-paddingless is-marginless">
<nav class="tabs">
<ul>
<li class="{{if (eq tab '') 'is-active'}}">
<li class="{{if (eq tab 'actions') 'is-active'}}">
{{#secret-link
secret=key.id
mode="show"
replace=true
queryParams=(query-params tab='')
queryParams=(query-params tab='actions')
data-test-transit-key-actions-link=true
}}
Key Actions
{{/secret-link}}
</li>
<li class="{{if (eq tab 'details') 'is-active'}}">
{{#secret-link
secret=key.id
mode="show"
replace=true
queryParams=(query-params tab='details')
data-test-transit-link="details"
}}
Details
@ -28,6 +40,7 @@
</nav>
</div>
{{#unless (eq tab 'actions')}}
<Toolbar>
<ToolbarActions>
{{#if (eq tab 'versions')}}
@ -48,18 +61,39 @@
Edit encryption key
</ToolbarSecretLink>
{{/if}}
<ToolbarSecretLink
@secret={{key.id}}
@mode="actions"
@data-test-transit-key-actions-link=true
>
Key actions
</ToolbarSecretLink>
{{/if}}
</ToolbarActions>
</Toolbar>
{{/unless}}
{{#if (eq tab 'versions')}}
{{#if (eq tab 'actions')}}
<div class="transit-card-container">
{{#each model.supportedActions as |supportedAction|}}
{{#linked-block
"vault.cluster.secrets.backend.actions"
model.id
queryParams=(hash action=supportedAction.name)
class="transit-card"
data-test-transit-card=supportedAction.name
}}
<div class="transit-icon">
<Icon
@glyph={{supportedAction.glyph}}
@size="l"
class="has-text-grey auto-width"
aria-label={{concat backend.path " options"}} />
</div>
<div class="transit-description">
<h2 class="title is-6" data-test-transit-action-title={{supportedAction.name}}>
{{if (eq supportedAction.name 'export') 'Export Key' (humanize supportedAction.name)}}
</h2>
<p class="transit-action-description">{{supportedAction.description}}</p>
</div>
{{/linked-block}}
{{/each}}
</div>
{{else if (eq tab 'versions')}}
{{#if (or
(eq key.type "aes256-gcm96")
(eq key.type "chacha20-poly1305")

View File

@ -1,20 +1,5 @@
<div class="columns">
{{#menu-sidebar title="Transit Actions" class="is-2"}}
{{#each model.supportedActions as |supportedAction|}}
<li>
{{#secret-link
mode="actions"
secret=model.id
class=(if (eq supportedAction selectedAction) "is-active")
queryParams=(query-params action=supportedAction)
data-test-transit-action-link=supportedAction
}}
{{capitalize supportedAction}}
{{/secret-link}}
</li>
{{/each}}
{{/menu-sidebar}}
<div class="column is-10">
<div class="column">
<PageHeader as |p|>
<p.top>
{{key-value-header
@ -27,21 +12,39 @@
</p.top>
<p.levelLeft>
<h1 class="title is-3">
{{model.id}}
{{#secret-link
class="is-inline has-text-info"
secret=model.id
mode="show"
replace=true
queryParams=(query-params tab='actions')
data-test-transit-link="actions"
}}
<Icon @glyph="arrow-left" />
{{/secret-link}}
Key Actions
</h1>
</p.levelLeft>
</PageHeader>
<Toolbar>
<ToolbarActions>
<ToolbarSecretLink
@secret={{model.id}}
@mode="show"
>
Details
</ToolbarSecretLink>
</ToolbarActions>
</Toolbar>
<div class="tabs-container box is-sideless is-fullwidth is-paddingless is-marginless">
<nav class="tabs">
<ul>
{{#each model.supportedActions as |supportedAction|}}
<li class="{{if (eq supportedAction.name selectedAction) 'is-active'}}">
{{#secret-link
mode="actions"
secret=model.id
queryParams=(query-params action=supportedAction.name)
data-test-transit-action-link=supportedAction.name
}}
{{if (eq supportedAction.name 'export') 'Export Key' (humanize supportedAction.name)}}
{{/secret-link}}
</li>
{{/each}}
</ul>
</nav>
</div>
<TransitKeyActions
@selectedAction={{selectedAction}}

View File

@ -264,9 +264,21 @@ module('Acceptance | transit', function(hooks) {
await click('[data-test-transit-key-actions-link]');
await settled();
assert.ok(
currentURL().startsWith(`/vault/secrets/${path}/actions/${name}`),
`${name}: navigates to tranist actions`
currentURL().startsWith(`/vault/secrets/${path}/show/${name}?tab=actions`),
`${name}: navigates to transit actions`
);
const keyAction = key.supportsEncryption ? 'encrypt' : 'sign';
const actionTitle = find(`[data-test-transit-action-title=${keyAction}]`).innerText.toLowerCase();
assert.equal(
actionTitle.includes(keyAction),
true,
`shows a card with title that links to the ${name} transit action`
);
await click(`[data-test-transit-card=${keyAction}]`);
await settled();
assert.ok(
find('[data-test-transit-key-version-select]'),
`${name}: the rotated key allows you to select versions`

View File

@ -19,7 +19,7 @@ module('Unit | Model | transit key', function(hooks) {
})
);
let supportedActions = model.get('supportedActions');
let supportedActions = model.get('supportedActions').map(k => k.name);
assert.deepEqual(['encrypt', 'decrypt', 'datakey', 'rewrap', 'hmac', 'verify'], supportedActions);
});

View File

@ -1082,9 +1082,9 @@
"@glimmer/util" "^0.41.4"
"@hashicorp/structure-icons@^1.3.0":
version "1.3.0"
resolved "https://registry.yarnpkg.com/@hashicorp/structure-icons/-/structure-icons-1.3.0.tgz#1c7c1cb43a1c1aa92b073a7aa7956495ae14c3e0"
integrity sha512-wTKpdaAPphEY2kg5QbQTSUlhqLTpBBR1+1dXp4LYTN0PtMSpetyDDDhcSyvKE8i4h2nwPJBRRfeFlE1snaHd7w==
version "1.8.1"
resolved "https://registry.yarnpkg.com/@hashicorp/structure-icons/-/structure-icons-1.8.1.tgz#d29945df2b41dcb317b141e51a26bd4be796a164"
integrity sha512-XFYdCIshmaR3Igc8eWpOZ2Gr3IR/0TogXZ4PQ9bz1E9cLzF3njBcs3tCpJUOwRwe/wMI5YTlL/sOGvcZ77AB/Q==
"@icons/material@^0.2.4":
version "0.2.4"