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:
parent
0ab4c138c2
commit
b004a24cdf
|
@ -17,6 +17,7 @@
|
|||
/npm-debug.log*
|
||||
/testem.log
|
||||
/yarn-error.log
|
||||
/.storybook/preview-head.html
|
||||
|
||||
# ember-try
|
||||
/.node_modules.ember-try/
|
||||
|
|
|
@ -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>
|
|
@ -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);
|
|
@ -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'),
|
||||
//
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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';
|
||||
|
|
|
@ -20,5 +20,4 @@
|
|||
</h1>
|
||||
</p.levelLeft>
|
||||
</PageHeader>
|
||||
|
||||
{{partial (concat 'partials/transit-form-' mode)}}
|
||||
|
|
|
@ -9,7 +9,6 @@
|
|||
data-test-transit-key-rotate="true"
|
||||
>
|
||||
Rotate encryption key
|
||||
<Chevron @isButton={{true}} />
|
||||
</ConfirmAction>
|
||||
{{/if}}
|
||||
{{else}}
|
||||
|
|
|
@ -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)}}
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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}}
|
||||
|
|
|
@ -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`
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue