Merge branch 'master' into jo-upgrade-copy

This commit is contained in:
Joshua Ogle 2018-05-25 15:51:57 -06:00 committed by GitHub
commit 0a598dd37b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
101 changed files with 2725 additions and 471 deletions

View file

@ -16,6 +16,10 @@ FEATURES:
* Userpass auth CIDR restrictions: When using the `userpass` auth method you
can now limit authentication to specific CIDRs; these will also be encoded
in resultant tokens to limit their use.
* Vault Browser CLI: The UI now supports usage of read/write/list/delete
commands in a CLI that can be accessed from the nav bar. Complex inputs such
as JSON files are not currently supported. This surfaces features otherwise
unsupported in Vault's UI.
IMPROVEMENTS:

View file

@ -0,0 +1,8 @@
import ApplicationAdapter from './application';
export default ApplicationAdapter.extend({
namespace: 'v1',
pathForType(modelName) {
return modelName;
},
});

View file

@ -0,0 +1,36 @@
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() {},
onShiftCommand() {},
value: null,
isFullscreen: null,
didRender() {
this.element.scrollIntoView();
},
actions: {
handleKeyUp(event) {
const keyCode = event.keyCode;
switch (keyCode) {
case keys.ENTER:
this.get('onExecuteCommand')(event.target.value);
break;
case keys.UP:
case keys.DOWN:
this.get('onShiftCommand')(keyCode);
break;
default:
this.get('onValueUpdate')(event.target.value);
}
},
fullscreen() {
this.get('onFullscreen')();
}
},
});

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,9 @@
import Ember from 'ember';
const { computed } = Ember;
export default Ember.Component.extend({
content: null,
list: computed('content', function() {
return this.get('content').keys;
}),
});

View file

@ -0,0 +1,28 @@
import Ember from 'ember';
import columnify from 'columnify';
const { computed } = Ember;
export function stringifyObjectValues(data) {
Object.keys(data).forEach(item => {
let val = data[item];
if (typeof val !== 'string') {
val = JSON.stringify(val);
}
data[item] = val;
});
}
export default Ember.Component.extend({
content: null,
columns: computed('content', function() {
let data = this.get('content');
stringifyObjectValues(data);
return columnify(data, {
preserveNewLines: true,
headingTransform: function(heading) {
return Ember.String.capitalize(heading);
},
});
}),
});

View file

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

View file

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

View file

@ -0,0 +1,6 @@
import Ember from 'ember';
export default Ember.Component.extend({
'data-test-component': 'console/output-log',
log: null,
});

View file

@ -0,0 +1,84 @@
import Ember from 'ember';
import {
parseCommand,
extractDataAndFlags,
logFromResponse,
logFromError,
logErrorFromInput,
executeUICommand,
} from 'vault/lib/console-helpers';
const { inject, computed } = Ember;
export default Ember.Component.extend({
classNames: 'console-ui-panel-scroller',
classNameBindings: ['isFullscreen:fullscreen'],
isFullscreen: false,
console: inject.service(),
inputValue: null,
log: computed.alias('console.log'),
logAndOutput(command, logContent) {
this.set('inputValue', '');
this.get('console').logAndOutput(command, logContent);
},
executeCommand(command, shouldThrow = false) {
let service = this.get('console');
let serviceArgs;
if(executeUICommand(command, (args) => this.logAndOutput(args), (args) => service.clearLog(args), () => this.toggleProperty('isFullscreen'))){
return;
}
// parse to verify it's valid
try {
serviceArgs = parseCommand(command, shouldThrow);
} catch (e) {
this.logAndOutput(command, { type: 'help' });
return;
}
// we have a invalid command but don't want to throw
if (serviceArgs === false) {
return;
}
let [method, flagArray, path, dataArray] = serviceArgs;
if (dataArray || flagArray) {
var { data, flags } = extractDataAndFlags(dataArray, flagArray);
}
let inputError = logErrorFromInput(path, method, flags, dataArray);
if (inputError) {
this.logAndOutput(command, inputError);
return;
}
let serviceFn = service[method];
serviceFn.call(service, path, data, flags.wrapTTL)
.then(resp => {
this.logAndOutput(command, logFromResponse(resp, path, method, flags));
})
.catch(error => {
this.logAndOutput(command, logFromError(error, path, method));
});
},
shiftCommandIndex(keyCode) {
this.get('console').shiftCommandIndex(keyCode, (val) => {
this.set('inputValue', val);
});
},
actions: {
toggleFullscreen() {
this.toggleProperty('isFullscreen');
},
executeCommand(val) {
this.executeCommand(val, true);
},
shiftCommandIndex(direction) {
this.shiftCommandIndex(direction);
},
},
});

View file

@ -18,6 +18,10 @@ export default IvyCodemirrorComponent.extend({
'data-test-component': 'json-editor',
updateCodeMirrorOptions() {
const options = assign({}, JSON_EDITOR_DEFAULTS, this.get('options'));
if (options.autoHeight) {
options.viewportMargin = Infinity;
delete options.autoHeight;
}
if (options) {
Object.keys(options).forEach(function(option) {

View file

@ -1,18 +1,21 @@
import Ember from 'ember';
import config from '../config/environment';
const { computed, inject } = Ember;
export default Ember.Controller.extend({
env: config.environment,
auth: Ember.inject.service(),
vaultVersion: Ember.inject.service('version'),
activeCluster: Ember.computed('auth.activeCluster', function() {
auth: inject.service(),
vaultVersion: inject.service('version'),
console: inject.service(),
consoleOpen: computed.alias('console.isOpen'),
activeCluster: computed('auth.activeCluster', function() {
return this.store.peekRecord('cluster', this.get('auth.activeCluster'));
}),
activeClusterName: Ember.computed('auth.activeCluster', function() {
activeClusterName: computed('auth.activeCluster', function() {
const activeCluster = this.store.peekRecord('cluster', this.get('auth.activeCluster'));
return activeCluster ? activeCluster.get('name') : null;
}),
showNav: Ember.computed(
showNav: computed(
'activeClusterName',
'auth.currentToken',
'activeCluster.dr.isSecondary',
@ -30,4 +33,9 @@ export default Ember.Controller.extend({
}
}
),
actions: {
toggleConsole() {
this.toggleProperty('consoleOpen');
},
},
});

View file

@ -0,0 +1,7 @@
import Ember from 'ember';
export function multiLineJoin([arr]) {
return arr.join('\n');
}
export default Ember.Helper.helper(multiLineJoin);

View file

@ -0,0 +1,183 @@
import keys from 'vault/lib/keycodes';
import argTokenizer from 'yargs-parser-tokenizer';
const supportedCommands = ['read', 'write', 'list', 'delete'];
const uiCommands = ['clearall', 'clear', 'fullscreen'];
export function extractDataAndFlags(data, flags) {
return data.concat(flags).reduce((accumulator, val) => {
// will be "key=value" or "-flag=value" or "foo=bar=baz"
// split on the first =
let [item, value] = val.split(/=(.+)/);
if (item.startsWith('-')) {
let flagName = item.replace(/^-/, '');
if (flagName === 'wrap-ttl') {
flagName = 'wrapTTL';
}
accumulator.flags[flagName] = value || true;
return accumulator;
}
// if it exists in data already, then we have multiple
// foo=bar in the list and need to make it an array
if (accumulator.data[item]) {
accumulator.data[item] = [].concat(accumulator.data[item], value);
return accumulator;
}
accumulator.data[item] = value;
return accumulator;
}, { data: {}, flags: {} });
}
export function executeUICommand(command, logAndOutput, clearLog, toggleFullscreen){
const isUICommand = uiCommands.includes(command);
if(isUICommand){
logAndOutput(command);
}
switch(command){
case 'clearall':
clearLog(true);
break;
case 'clear':
clearLog();
break;
case 'fullscreen':
toggleFullscreen();
break;
}
return isUICommand;
}
export function parseCommand(command, shouldThrow) {
let args = argTokenizer(command);
if (args[0] === 'vault') {
args.shift();
}
let [method, ...rest] = args;
let path;
let flags = [];
let data = [];
rest.forEach(arg => {
if (arg.startsWith('-')) {
flags.push(arg);
} else {
if (path) {
data.push(arg);
} else {
path = arg;
}
}
});
if (!supportedCommands.includes(method)) {
if (shouldThrow) {
throw new Error('invalid command');
}
return false;
}
return [method, flags, path, data];
}
export function logFromResponse(response, path, method, flags) {
if (!response) {
let message =
method === 'write'
? `Success! Data written to: ${path}`
: `Success! Data deleted (if it existed) at: ${path}`;
return { type: 'success', content: message };
}
let { format, field } = flags;
let secret = response.auth || response.data || response.wrap_info;
if (field) {
let fieldValue = secret[field];
let response;
if (fieldValue) {
if (format && format === 'json') {
return { type: 'json', content: fieldValue };
}
switch (typeof fieldValue) {
case 'string':
response = { type: 'text', content: fieldValue };
break;
default:
response = { type: 'object', content: fieldValue };
break;
}
} else {
response = { type: 'error', content: `Field "${field}" not present in secret` };
}
return response;
}
if (format && format === 'json') {
// just print whole response
return { type: 'json', content: response };
}
if (method === 'list') {
return { type: 'list', content: secret };
}
return { type: 'object', content: secret };
}
export function logFromError(error, vaultPath, method) {
let content;
let { httpStatus, path } = error;
let verbClause = {
read: 'reading from',
write: 'writing to',
list: 'listing',
delete: 'deleting at',
}[method];
content = `Error ${verbClause}: ${vaultPath}.\nURL: ${path}\nCode: ${httpStatus}`;
if (typeof error.errors[0] === 'string') {
content = `${content}\nErrors:\n ${error.errors.join('\n ')}`;
}
return { type: 'error', content };
}
export function shiftCommandIndex(keyCode, history, index) {
let newInputValue;
let commandHistoryLength = history.length;
if (!commandHistoryLength) { return []; }
if (keyCode === keys.UP) {
index -= 1;
if (index < 0) {
index = commandHistoryLength - 1;
}
} else {
index += 1;
if (index === commandHistoryLength) {
newInputValue = '';
}
if (index > commandHistoryLength) {
index -= 1;
}
}
if (newInputValue !== '') {
newInputValue = history.objectAt(index).content;
}
return [index, newInputValue];
}
export function logErrorFromInput(path, method, flags, dataArray) {
if (path === undefined) {
return { type: 'error', content: 'A path is required to make a request.' };
}
if (method === 'write' && !flags.force && dataArray.length === 0) {
return { type: 'error', content: 'Must supply data or use -force' };
}
}

View file

@ -72,7 +72,7 @@ export default DS.Model.extend({
}),
stateGlyph(state) {
const glyph = 'checkmark';
const glyph = 'checkmark-circled-outline';
const glyphs = {
'stream-wals': 'android-sync',

View file

@ -1,14 +1,18 @@
import Ember from 'ember';
import ModelBoundaryRoute from 'vault/mixins/model-boundary-route';
const { inject } = Ember;
export default Ember.Route.extend(ModelBoundaryRoute, {
auth: Ember.inject.service(),
flashMessages: Ember.inject.service(),
auth: inject.service(),
flashMessages: inject.service(),
console: inject.service(),
modelTypes: ['secret', 'secret-engine'],
beforeModel() {
this.get('auth').deleteCurrentToken();
this.get('console').set('isOpen', false);
this.get('console').clearLog(true);
this.clearModelCache();
this.replaceWith('vault.cluster');
this.get('flashMessages').clearMessages();

View file

@ -49,25 +49,25 @@ export default Ember.Route.extend({
return Ember.RSVP.hash({
secret,
secrets: this.store
.lazyPaginatedQuery(this.getModelType(backend, params.tab), {
id: secret,
backend,
responsePath: 'data.keys',
page: params.page,
pageFilter: params.pageFilter,
size: 100,
})
.then(model => {
this.set('has404', false);
return model;
})
.catch(err => {
if (backendModel && err.httpStatus === 404 && secret === '') {
return [];
} else {
throw err;
}
})
.lazyPaginatedQuery(this.getModelType(backend, params.tab), {
id: secret,
backend,
responsePath: 'data.keys',
page: params.page,
pageFilter: params.pageFilter,
size: 100,
})
.then(model => {
this.set('has404', false);
return model;
})
.catch(err => {
if (backendModel && err.httpStatus === 404 && secret === '') {
return [];
} else {
throw err;
}
}),
});
},

105
ui/app/services/console.js Normal file
View file

@ -0,0 +1,105 @@
// Low level service that allows users to input paths to make requests to vault
// this service provides the UI synecdote to the cli commands read, write, delete, and list
import Ember from 'ember';
import {
shiftCommandIndex,
} from 'vault/lib/console-helpers';
const { Service, getOwner, computed } = Ember;
export function sanitizePath(path) {
//remove whitespace + remove trailing and leading slashes
return path.trim().replace(/^\/+|\/+$/g, '');
}
export function ensureTrailingSlash(path) {
return path.replace(/(\w+[^/]$)/g, '$1/');
}
const VERBS = {
read: 'GET',
list: 'GET',
write: 'POST',
delete: 'DELETE',
};
export default Service.extend({
isOpen: false,
adapter() {
return getOwner(this).lookup('adapter:console');
},
commandHistory: computed('log.[]', function() {
return this.get('log').filterBy('type', 'command');
}),
log: computed(function() {
return [];
}),
commandIndex: null,
shiftCommandIndex(keyCode, setCommandFn = () => {}) {
let [newIndex, newCommand] = shiftCommandIndex(
keyCode,
this.get('commandHistory'),
this.get('commandIndex')
);
if (newCommand !== undefined && newIndex !== undefined) {
this.set('commandIndex', newIndex);
setCommandFn(newCommand);
}
},
clearLog(clearAll=false) {
let log = this.get('log');
let history;
if (!clearAll) {
history = this.get('commandHistory').slice();
history.setEach('hidden', true);
}
log.clear();
if (history) {
log.addObjects(history);
}
},
logAndOutput(command, logContent) {
let log = this.get('log');
log.pushObject({ type: 'command', content: command });
this.set('commandIndex', null);
if (logContent) {
log.pushObject(logContent);
}
},
ajax(operation, path, options = {}) {
let verb = VERBS[operation];
let adapter = this.adapter();
let url = adapter.buildURL(path);
let { data, wrapTTL } = options;
return adapter.ajax(url, verb, {
data,
wrapTTL,
});
},
read(path, data, wrapTTL) {
return this.ajax('read', sanitizePath(path), { wrapTTL });
},
write(path, data, wrapTTL) {
return this.ajax('write', sanitizePath(path), { data, wrapTTL });
},
delete(path) {
return this.ajax('delete', sanitizePath(path));
},
list(path, data, wrapTTL) {
let listPath = ensureTrailingSlash(sanitizePath(path));
return this.ajax('list', listPath, {
data: {
list: true,
},
wrapTTL,
});
},
});

View file

@ -171,3 +171,7 @@ $gutter-grey: #2a2f36;
}
}
}
.cm-s-auto-height.CodeMirror {
height: auto;
}

View file

@ -0,0 +1,151 @@
.console-ui-panel-scroller {
background: linear-gradient(to right, #191A1C, #1B212D);
height: 0;
left: 0;
min-height: 400px;
overflow: auto;
position: fixed;
right: 0;
transform: translate3d(0, -400px, 0);
transition: min-height $speed ease-out, transform $speed ease-in;
will-change: transform, min-height;
z-index: 199;
}
.console-ui-panel {
display: flex;
flex-direction: column;
justify-content: flex-end;
padding: $size-8 $size-8 $size-4;
min-height: 100%;
color: $white;
font-size: $body-size;
font-weight: $font-weight-semibold;
transition: justify-content $speed ease-in;
pre, p {
background: none;
color: inherit;
font-size: $body-size;
&:not(.console-ui-command):not(.CodeMirror-line) {
padding-left: $console-spacing;
}
}
.cm-s-hashi.CodeMirror {
background-color: rgba($black, 0.5) !important;
font-weight: $font-weight-normal;
margin-left: $console-spacing;
padding: $size-8 $size-4;
}
.button,
{
background: transparent;
border: none;
color: $grey-dark;
min-width: 0;
padding: 0 $size-8;
&.active,
&:hover {
background: $blue;
color: $white;
}
}
}
.console-ui-input {
align-items: center;
display: flex;
input {
background-color: rgba($black, 0.5);
border: 0;
caret-color: $white;
color: $white;
flex: 1;
font-family: $family-monospace;
font-size: $body-size;
font-weight: $font-weight-bold;
margin-left: -$size-10;
outline: none;
padding: $size-10;
transition: background-color $speed;
}
}
.console-ui-command {
line-height: 2;
}
.console-ui-output {
transition: background-color $speed;
padding-right: $size-2;
position: relative;
.console-ui-output-actions {
opacity: 0;
position: absolute;
right: 0;
top: 0;
transition: opacity $speed;
will-change: opacity;
}
&:hover {
background: rgba($black, 0.25);
.console-ui-output-actions {
opacity: 1;
}
}
}
.console-ui-alert {
margin-left: calc(#{$console-spacing} - 0.33rem);
position: relative;
.icon {
position: absolute;
left: 0;
top: 0;
}
}
.panel-open .console-ui-panel-scroller {
box-shadow: $box-shadow-highest;
transform: translate3d(0, 0, 0);
}
.panel-open .console-ui-panel-scroller.fullscreen {
bottom: 0;
top: 0;
min-height: 100%;
}
.panel-open {
.navbar, .navbar-sections{
transition: transform $speed ease-in;
}
}
.panel-open.panel-fullscreen {
.navbar, .navbar-sections{
transform: translate3d(0, -100px, 0);
}
}
.page-container > header {
background: linear-gradient(to right, #191A1C, #1B212D);
}
header .navbar,
header .navbar-sections {
z-index: 200;
transform: translate3d(0, 0, 0);
will-change: transform;
}

View file

@ -0,0 +1,10 @@
.env-banner {
&,
&:not(:last-child):not(:last-child) {
margin: 0;
}
.level-item {
padding: $size-10 $size-8;
}
}

View file

@ -15,7 +15,7 @@
z-index: 1;
.notification {
box-shadow: 0 0 25px rgba($black, 0.2);
box-shadow: $box-shadow-high;
margin: 20px;
@include until($desktop) {
margin: 1rem 0;

View file

@ -1,13 +1,13 @@
.linked-block {
cursor: pointer;
transition: box-shadow $speed;
will-change: box-shadow;
&:hover,
&:focus {
position: relative;
box-shadow: $box-link-hover-shadow;
}
&:focus,
&:active {
position: relative;
box-shadow: $box-link-active-shadow;
box-shadow: $box-link-hover-shadow, $box-shadow-middle;
}
}

View file

@ -0,0 +1,3 @@
.login-form {
box-shadow: $box-shadow, $box-shadow-high;
}

View file

@ -1,16 +1,17 @@
.popup-menu-content {
border-radius: 2px;
margin: -2px 0 0 0;
& > .box {
border-radius: 2px;
box-shadow: $box-shadow, $box-shadow-middle;
padding: 0;
position: relative;
width: 175px;
@include css-top-arrow(8px, $white, 1px, $blue, 155px);
}
&.is-wide > .box {
width: 200px;
@include css-top-arrow(8px, $white, 1px, $blue, 178px);
width: 300px;
}
.confirm-action span .button {
@ -18,58 +19,98 @@
margin: .25rem auto;
width: 95%;
}
}
.popup-menu-trigger {
min-width: auto;
}
.popup-menu-trigger.is-active {
&,
&:active,
&:focus {
box-shadow: 0 0 0 1px $blue;
.menu {
padding: $size-11 0;
button.link,
a {
background: transparent;
box-shadow: none;
border: none;
color: $menu-item-color;
display: block;
height: auto;
font-size: $size-7;
font-weight: $font-weight-semibold;
padding: $size-9 $size-8;
text-align: left;
text-decoration: none;
width: 100%;
&:hover {
background-color: $menu-item-hover-background-color;
color: $menu-item-hover-color;
}
&.is-active {
background-color: $menu-item-active-background-color;
color: $menu-item-active-color;
}
&.is-destroy {
color: $red;
&:hover {
background-color: $red;
color: $white;
}
}
}
}
}
.ember-basic-dropdown-content--left.popup-menu {
margin: 0px 0 0 -8px;
}
.popup-menu-content .menu {
button.link,
a {
border-radius: $menu-item-radius;
color: $menu-item-color;
font-size: $size-7;
.menu-label {
color: $grey-dark;
font-size: $size-8;
font-weight: $font-weight-semibold;
display: block;
padding: $size-9 $size-6;
box-shadow: none;
border: none;
background: transparent;
height: auto;
width: 100%;
text-align: left;
text-decoration: none;
letter-spacing: 0;
margin: 0;
padding: $size-10 $size-8 $size-11;
text-transform: none;
}
&:hover {
background-color: $menu-item-hover-background-color;
color: $menu-item-hover-color;
.menu-content {
padding: $size-10 $size-8;
}
hr {
background-color: $grey-light;
margin: $size-11 0;
}
}
.popup-menu-trigger {
height: 2rem;
min-width: 0;
padding: 0 $size-10;
}
.status-menu-content {
margin-top: 8px;
}
.ember-basic-dropdown-content {
&--left.popup-menu {
margin: 0px 0 0 -8px;
}
&--below {
&.ember-basic-dropdown--transitioning-in {
animation: drop-fade-above .15s;
}
&.is-active {
background-color: $menu-item-active-background-color;
color: $menu-item-active-color;
&.ember-basic-dropdown--transitioning-out {
animation: drop-fade-above .15s reverse;
}
}
&--above {
&.ember-basic-dropdown--transitioning-in {
animation: drop-fade-below .15s;
}
&.ember-basic-dropdown--transitioning-out {
animation: drop-fade-below .15s reverse;
}
}
}
.popup-menu-content .menu-list {
margin: 0.1rem;
}
.popup-menu-content .menu-label {
background: $grey-lighter;
font-size: $size-8;
letter-spacing: 0;
margin: 0;
padding: $size-10 calc(#{$size-6} + .1rem);
text-transform: none;
}

View file

@ -55,7 +55,8 @@
}
.menu-label {
color: $grey-light;
color: $grey;
font-weight: $font-weight-semibold;
font-size: $size-small;
line-height: 1;
margin-bottom: $size-8;
@ -72,14 +73,8 @@
transition: 250ms border-width;
&.is-active {
color: $blue;
background-color: $dark-white;
border-right: 4px solid $blue;
}
&:hover {
background-color: $dark-white;
}
}
}
}

View file

@ -1,65 +1,6 @@
.status-menu-content {
max-width: 360px;
min-width: 280px;
border-radius: 3px;
margin: 0 -17px 0 0;
will-change: transform, opacity;
.card {
position: relative;
border-radius: 2px;
border: $base-border;
box-shadow: 0 0 4px rgba($black, 0.21);
color: $black;
@include css-top-arrow(8px, $grey-lighter, 1px, $grey, 100%, -25px);
.card-content {
padding: $size-6 $size-3;
}
}
.card-header-title,
.menu-label {
background: $grey-lighter;
color: $grey-dark;
font-size: $size-7;
font-weight: normal;
letter-spacing: 0;
margin: 0;
padding: 8px $size-3;
text-transform: uppercase;
}
.replication-card {
@include css-top-arrow(8px, $grey-lighter, 1px, $grey, 100%, -98px);
.box-label {
box-shadow: none;
}
a.is-active,
a:hover {
box-shadow: 0 0 0 1px $blue;
}
}
&.ember-basic-dropdown-content--below.ember-basic-dropdown--transitioning-in {
animation: drop-fade-above .15s;
}
&.ember-basic-dropdown-content--below.ember-basic-dropdown--transitioning-out {
animation: drop-fade-above .15s reverse;
}
&.ember-basic-dropdown-content--above.ember-basic-dropdown--transitioning-in {
animation: drop-fade-below .15s;
}
&.ember-basic-dropdown-content--above.ember-basic-dropdown--transitioning-out {
animation: drop-fade-below .15s reverse;
}
}
.status-menu-content-replication {
margin-top: 5px;
}
.is-status-chevron {
line-height: 0;
padding: 0.25em 0 0.25em 0.25em;
padding: 0.3em 0 0 $size-11;
}
.status-menu-user-trigger {
@ -73,39 +14,3 @@
min-width: 0;
}
}
.status-menu-content .menu {
button.link,
a {
font-size: $size-6;
font-weight: 500;
padding: $size-9 $size-3;
border-radius: $menu-item-radius;
color: $menu-item-color;
display: block;
padding: $size-9 $size-3;
box-shadow: none;
border: none;
background: transparent;
height: auto;
width: 100%;
text-align: left;
.icon {
color: $menu-item-hover-background-color;
}
&:hover {
background-color: $menu-item-hover-background-color;
color: $menu-item-hover-color;
.icon {
color: $menu-item-hover-color;
}
}
&.is-active {
background-color: $menu-item-active-background-color;
color: $menu-item-active-color;
}
}
}

View file

@ -1,29 +1,35 @@
.sub-nav {
&.tabs {
background: $grey-lighter;
padding: 0 1.25rem;
box-shadow: inset 0 -1px 0 $grey-light;
ul {
border-color: transparent;
}
li {
&:focus {
box-shadow: none;
}
&.is-active a {
border-color: $blue;
color: $blue;
}
}
a {
color: $grey-dark;
font-weight: $font-weight-semibold;
text-decoration: none;
padding: 1.5rem 1rem;
padding: $size-6 $size-8 $size-8;
border-bottom: 2px solid transparent;
transition: border-color $speed;
&:hover,
&:active {
border-color: $grey-light;
}
}
a:hover,
a:active {
border-color: $grey-light;
}
li:focus {
box-shadow: none;
}
li.is-active a {
border-color: $blue;
color: $blue;
}
.ember-basic-dropdown-trigger {
outline: none;
}

View file

@ -5,7 +5,7 @@
.box {
position: relative;
color: $white;
width: 200px;
max-width: 200px;
background: $grey;
padding: 0.5rem;
line-height: 1.4;
@ -28,6 +28,16 @@
.ember-basic-dropdown-content--left.tool-tip {
margin: 8px 0 0 -11px;
}
.ember-basic-dropdown-content--below.ember-basic-dropdown-content--right.tool-tip {
@include css-top-arrow(8px, $grey, 1px, $grey-dark, calc(100% - 20px));
}
.ember-basic-dropdown-content--above.ember-basic-dropdown-content--right.tool-tip {
@include css-bottom-arrow(8px, $grey, 1px, $grey-dark, calc(100% - 20px));
}
.ember-basic-dropdown-content--above.tool-tip {
margin-top: -2px;
}
.tool-tip-trigger {
border: none;
border-radius: 20px;

View file

@ -10,7 +10,8 @@
}
.modal-background {
background-image: url("/ui/vault-hex.svg"), linear-gradient(90deg, #191A1C, #1B212D);
background-image: url("/ui/vault-hex.svg"),
linear-gradient(90deg, #191a1c, #1b212d);
opacity: 0.97;
}

View file

@ -1,52 +1,52 @@
@keyframes vault-loading-animation {
0%,
70%,
100% {
transform: scale3D(1, 1, 1);
}
35% {
transform: scale3D(0, 0, 1);
}
@keyframes vault-loading-animation {
0%,
70%,
100% {
transform: scale3D(1, 1, 1);
}
#vault-loading {
polygon {
animation: vault-loading-animation 1.3s infinite ease-in-out;
transform-origin: 50% 50%;
fill: #DCE2E9;
}
.vault-loading-order-1 {
animation-delay: .1s;
}
.vault-loading-order-2 {
animation-delay: .2s;
}
.vault-loading-order-3 {
animation-delay: .3s;
}
.vault-loading-order-4 {
animation-delay: .4s;
}
35% {
transform: scale3D(0, 0, 1);
}
}
#vault-loading {
polygon {
animation: vault-loading-animation 1.3s infinite ease-in-out;
transform-origin: 50% 50%;
fill: #dce2e9;
}
#vault-loading-animated {
@media all and (-ms-high-contrast: none), (-ms-high-contrast: active) {
// For IE11
display: none;
}
.vault-loading-order-1 {
animation-delay: .1s;
}
#vault-loading-static {
.vault-loading-order-2 {
animation-delay: .2s;
}
.vault-loading-order-3 {
animation-delay: .3s;
}
.vault-loading-order-4 {
animation-delay: .4s;
}
}
#vault-loading-animated {
@media all and (-ms-high-contrast: none), (-ms-high-contrast: active) {
// For IE11
display: none;
font-size: 9px;
}
}
@media all and (-ms-high-contrast: none), (-ms-high-contrast: active) {
// For IE11
display: block;
}
}
#vault-loading-static {
display: none;
font-size: 9px;
@media all and (-ms-high-contrast: none), (-ms-high-contrast: active) {
// For IE11
display: block;
}
}

View file

@ -46,6 +46,8 @@
@import "./components/box-label";
@import "./components/codemirror";
@import "./components/confirm";
@import "./components/console-ui-panel";
@import "./components/env-banner";
@import "./components/form-section";
@import "./components/global-flash";
@import "./components/init-illustration";
@ -54,6 +56,7 @@
@import "./components/linked-block";
@import "./components/list-pagination";
@import "./components/loader";
@import "./components/login-form";
@import "./components/message-in-page";
@import "./components/page-header";
@import "./components/popup-menu";

View file

@ -1,8 +1,8 @@
$button-box-shadow-standard: 0 3px 1px 0 rgba($black, 0.12);
.button {
box-shadow: $button-box-shadow-standard;
border: 1px solid $grey-light;
box-shadow: $box-shadow-low;
color: $grey-dark;
display: inline-block;
font-size: $size-small;
@ -12,7 +12,8 @@ $button-box-shadow-standard: 0 3px 1px 0 rgba($black, 0.12);
min-width: 6rem;
padding: $size-10 $size-8;
text-decoration: none;
transition: background-color $speed, border-color $speed, box-shadow $speed, color $speed;
transition: background-color $speed, border-color $speed, box-shadow $speed,
color $speed;
vertical-align: middle;
&.is-icon {
@ -57,13 +58,14 @@ $button-box-shadow-standard: 0 3px 1px 0 rgba($black, 0.12);
&.is-hovered {
background-color: darken($color, 5%);
border-color: darken($color, 5%);
box-shadow: $box-shadow-middle;
}
&:active,
&.is-active {
background-color: darken($color, 10%);
border-color: darken($color, 10%);
box-shadow: none;
box-shadow: $box-shadow-middle;
}
&:focus,

View file

@ -33,13 +33,16 @@ input::-webkit-inner-spin-button {
.link {
background: transparent;
border: 0;
color: $blue;
cursor: pointer;
display: inline;
font: inherit;
line-height: normal;
margin: 0;
padding: 0;
text-decoration: underline;
-moz-user-select: text;
color: $blue;
cursor: pointer;
display: inline;
font: inherit;
line-height: normal;
margin: 0;
padding: 0;
text-decoration: underline;
-webkit-user-select: text; /* Chrome all / Safari all */
-moz-user-select: text; /* Firefox all */
-ms-user-select: text; /* IE 10+ */
user-select: text;
}

View file

@ -1,11 +1,11 @@
.column .menu-list a {
.column .menu-list a {
border-radius: 0;
border-right: 0 solid transparent;
font-weight: $font-weight-semibold;
&:hover,
&.is-active {
color: $menu-item-hover-background-color;
background-color: $menu-item-active-color;
color: $menu-item-active-color;
background-color: $menu-item-active-background-color;
}
}

View file

@ -1,17 +1,21 @@
.tag:not(body) {
background-color: lighten($grey-light, 17%);
border-radius: 2px;
color: $grey-dark;
height: auto;
padding: 0 0.75em;
padding: 0 $size-10;
margin-right: 0.5rem;
font-weight: normal;
code {
color: $grey;
color: $grey-dark;
}
}
.tag.is-outlined {
border: 1px solid currentColor;
}
.tag.is-inverted {
border-color: $grey;
background: none;
@ -19,6 +23,7 @@
color: $grey-dark;
}
}
.tag.is-bold {
font-weight: bold;
}

View file

@ -37,7 +37,9 @@ $border: $grey-light;
$hr-margin: 1rem 0;
//typography
$family-sans: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
$family-sans: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
"Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;
$family-primary: $family-sans;
$body-size: 14px;
$size-3: (24/14) + 0rem;
@ -46,6 +48,7 @@ $size-8: (12/14) + 0rem;
$size-9: 0.75rem;
$size-10: 0.5rem;
$size-11: 0.25rem;
$console-spacing: 1.5rem;
$size-small: $size-8;
$font-weight-normal: 400;
$font-weight-semibold: 600;
@ -61,6 +64,10 @@ $radius: 2px;
//box
$box-radius: 0;
$box-shadow: 0 0 0 1px rgba($black, 0.1);
$box-shadow-low: 0 5px 1px -2px rgba($black, 0.12), 0 3px 2px -1px rgba($black, 0);
$box-shadow-middle: 0 8px 4px -4px rgba($black, 0.10), 0 6px 8px -2px rgba($black, 0.05);
$box-shadow-high: 0 12px 5px -7px rgba($black, 0.08), 0 11px 10px -3px rgba($black, 0.10);
$box-shadow-highest: 0 16px 6px -10px rgba($black, 0.06), 0 16px 16px -4px rgba($black, 0.20);
$link: $blue;
$text: $black;
@ -78,6 +85,12 @@ $progress-bar-background-color: lighten($grey-light, 15%);
$base-border: 1px solid $grey-light;
//menu
$menu-item-hover-color: $text;
$menu-item-hover-background-color: $grey-lighter;
$menu-item-active-color: $link;
$menu-item-active-background-color: transparent;
// animations
$speed: 150ms;
$speed-slow: $speed * 2;

View file

@ -14,7 +14,7 @@
}
@include keyframes(drop-fade-below) {
0% {
0% {
opacity: 0;
transform: translateY(-1rem);
}
@ -25,7 +25,7 @@
}
@include keyframes(drop-fade-above) {
0% {
0% {
opacity: 0;
transform: translateY(1rem);
}

View file

@ -1,11 +1,15 @@
@mixin css-top-arrow($size, $color, $border-width, $border-color, $left: 50%, $left-offset: 0px) {
@mixin css-arrow($vertical-direction, $size, $color, $border-width, $border-color, $left: 50%, $left-offset: 0px) {
& {
border: 1px solid $border-color;
}
&:after,
&:before {
bottom: 100%;
@if ($vertical-direction == 'top') {
bottom: 100%;
} @else {
top: 100%;
}
border: solid transparent;
content: " ";
height: 0;
@ -28,6 +32,12 @@
left: calc(#{$left} + #{$left-offset});
margin-left: -($size + round(1.41421356 * $border-width));
}
&:before,
&:after {
@if ($vertical-direction == 'bottom') {
transform: rotate(180deg);
}
}
@at-root .ember-basic-dropdown-content--left#{&} {
&:after,
@ -38,6 +48,13 @@
}
}
@mixin css-top-arrow($size, $color, $border-width, $border-color, $left: 50%, $left-offset: 0px) {
@include css-arrow('top', $size, $color, $border-width, $border-color, $left, $left-offset);
}
@mixin css-bottom-arrow($size, $color, $border-width, $border-color, $left: 50%, $left-offset: 0px) {
@include css-arrow('bottom', $size, $color, $border-width, $border-color, $left, $left-offset);
}
@mixin vault-block {
&:not(:last-child) {
margin-bottom: (5/14) + 0rem;

View file

@ -1,6 +1,6 @@
<div class="page-container">
{{#if showNav}}
<header data-test-header-with-nav>
<header data-test-header-with-nav class="{{if consoleOpen 'panel-open'}} {{if consoleFullscreen ' panel-fullscreen'}}">
<nav class="navbar has-dark-grey-gradient is-grouped-split">
<div class="navbar-brand">
{{#home-link class="navbar-item has-text-white has-current-color-fill"}}
@ -8,6 +8,17 @@
{{/home-link}}
</div>
<div class="navbar-end is-divider-list is-flex">
<div class="navbar-item">
<button type="button" class="button is-transparent" {{action 'toggleConsole'}}>
{{#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>
@ -55,6 +66,7 @@
</a>
</li>
</ul>
{{console/ui-panel isFullscreen=consoleFullscreen}}
</header>
{{/if}}
<div class="global-flash">
@ -129,7 +141,7 @@
</div>
</footer>
{{#if (eq env "development") }}
<div class="level development">
<div class="env-banner level development">
<div class="level-item notification has-background-dark has-text-white">
{{i-con glyph="wand" class="type-icon"}}Local Development
</div>

View file

@ -1,56 +1,57 @@
<div class="card has-arrow">
<header class="card-header is-grey">
<p class="card-header-title is-uppercase has-background-grey-lighter has-text-grey-dark is-size-7">
<div class="popup-menu-content">
<div class="box">
<div class="menu-label">
{{auth.authData.displayName}}
</p>
</header>
<nav class="menu">
<ul class="menu-list">
{{#if (is-before (now interval=1000) auth.tokenExpirationDate)}}
{{#if auth.authData.renewable}}
<li class="action">
<button type="button" {{action "renewToken"}} class="link button {{if isRenewing 'is-loading'}}">
Renew token
</button>
</li>
<li class="action">
{{#confirm-action
onConfirmAction=(action "revokeToken")
confirmMessage=(concat "Are you sure you want to revoke the token for " (get auth 'authData.displayName') "?")
confirmButtonText="Revoke"
confirmButtonClasses="button is-primary"
buttonClasses="button link"
showConfirm=shouldRevoke
class=(if shouldRevoke "message is-block is-warning is-outline")
containerClasses="message-body is-block"
messageClasses="is-block"
}}
Revoke token
{{/confirm-action}}
</li>
{{else}}
<li class="action text-right">
{{#confirm-action
onConfirmAction=(action "revokeToken")
confirmMessage=(concat "Are you sure you want to revoke the token for " (get auth 'authData.displayName') "?")
confirmButtonText="Revoke"
confirmButtonClasses="button is-primary"
buttonClasses="button link"
showConfirm=shouldRevoke
class=(if shouldRevoke "message is-block is-warning is-outline")
containerClasses="message-body is-block"
messageClasses="is-block"
}}
Revoke token
{{/confirm-action}}
</li>
</div>
<nav class="menu">
<ul class="menu-list">
{{#if (is-before (now interval=1000) auth.tokenExpirationDate)}}
{{#if auth.authData.renewable}}
<li class="action">
<button type="button" {{action "renewToken"}} class="link button {{if isRenewing 'is-loading'}}">
Renew token
</button>
</li>
<li class="action">
{{#confirm-action
onConfirmAction=(action "revokeToken")
confirmMessage=(concat "Are you sure you want to revoke the token for " (get auth 'authData.displayName') "?")
confirmButtonText="Revoke"
confirmButtonClasses="button is-primary"
buttonClasses="button link"
showConfirm=shouldRevoke
class=(if shouldRevoke "message is-block is-warning is-outline")
containerClasses="message-body is-block"
messageClasses="is-block"
}}
Revoke token
{{/confirm-action}}
</li>
{{else}}
<li class="action text-right">
{{#confirm-action
onConfirmAction=(action "revokeToken")
confirmMessage=(concat "Are you sure you want to revoke the token for " (get auth 'authData.displayName') "?")
confirmButtonText="Revoke"
confirmButtonClasses="button is-primary"
buttonClasses="button link"
showConfirm=shouldRevoke
class=(if shouldRevoke "message is-block is-warning is-outline")
containerClasses="message-body is-block"
messageClasses="is-block"
}}
Revoke token
{{/confirm-action}}
</li>
{{/if}}
{{/if}}
{{/if}}
<li class="action">
<a href="{{href-to "vault.cluster.logout" activeClusterName }}" id="logout">
Sign out
</a>
</li>
</ul>
</nav>
<li class="action">
<a href="{{href-to "vault.cluster.logout" activeClusterName }}" id="logout">
Sign out
</a>
</li>
</ul>
</nav>
</div>
</div>

View file

@ -0,0 +1,16 @@
{{i-con glyph="chevron-right" size=12}}
<input onkeyup={{action 'handleKeyUp'}} value={{value}} />
{{#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}}

View file

@ -0,0 +1 @@
<pre class="console-ui-command">{{i-con glyph="chevron-right" size=12}}{{content}}</pre>

View file

@ -0,0 +1,4 @@
<div class="console-ui-alert has-text-danger">
{{i-con glyph="close-circled" aria-hidden="true" size=12}}
<pre>{{content}}</pre>
</div>

View file

@ -0,0 +1,16 @@
<div class="console-ui-alert has-text-grey">
{{i-con glyph="information-circled" aria-hidden="true" size=12}}
<pre>Usage: vault &lt;command&gt; [args]
Commands:
read Read data and retrieves secrets
write Write data, configuration, and secrets
delete Delete secrets and configuration
list List data or secrets
Web CLI Commands:
fullscreen Toggle fullscreen display
clear Clear output from the log
clearall Clear output and command history
</pre>
</div>

View file

@ -0,0 +1,10 @@
{{json-editor
value=(stringify content)
options=(hash
readOnly=true
lineNumbers=false
autoHeight=true
gutters=false
theme='hashi auto-height'
)
}}

View file

@ -0,0 +1,21 @@
<div class="console-ui-output">
<pre>Keys
{{#each list as |item|}}
{{item}}
{{/each}}
</pre>
<div class="console-ui-output-actions">
{{#tool-tip renderInPlace=true as |d|}}
{{#d.trigger data-test-tool-tip-trigger=true}}
{{#copy-button clipboardText=(multi-line-join list) class="button is-compact"}}
{{i-con glyph="copy" aria-hidden="true" size=16}}
{{/copy-button}}
{{/d.trigger}}
{{#d.content class="tool-tip"}}
<div class="box">
Copy
</div>
{{/d.content}}
{{/tool-tip}}
</div>
</div>

View file

@ -0,0 +1,18 @@
<div class="console-ui-output">
<pre>{{columns}}</pre>
<div class="console-ui-output-actions">
{{#tool-tip renderInPlace=true as |d|}}
{{#d.trigger data-test-tool-tip-trigger=true}}
{{#copy-button clipboardText=columns class="button is-compact"}}
{{i-con glyph="copy" aria-hidden="true" size=16}}
{{/copy-button}}
{{/d.trigger}}
{{#d.content class="tool-tip"}}
<div class="box">
Copy
</div>
{{/d.content}}
{{/tool-tip}}
</div>
</div>

View file

@ -0,0 +1,4 @@
<div class="console-ui-alert has-text-success">
{{i-con glyph="checkmark-circled" aria-hidden="true" size=12}}
<pre>{{content}}</pre>
</div>

View file

@ -0,0 +1 @@
<pre>{{content}}</pre>

View file

@ -0,0 +1,5 @@
{{#each log as |message|}}
{{#unless message.hidden}}
{{component (concat 'console/log-' message.type) content=message.content}}
{{/unless}}
{{/each}}

View file

@ -0,0 +1,16 @@
<div class="console-ui-panel">
<div class="content">
<p class="has-text-grey is-font-mono">
The Vault Browser CLI provides an easy way to execute the most common CLI commands, such as write, read, delete, and list.
</p>
</div>
{{console/output-log log=log}}
{{console/command-input
isFullscreen=isFullscreen
value=inputValue
onValueUpdate=(action (mut inputValue))
onFullscreen=(action 'toggleFullscreen')
onExecuteCommand=(action 'executeCommand')
onShiftCommand=(action 'shiftCommandIndex')
}}
</div>

View file

@ -12,7 +12,7 @@
<div>
{{model.mountPath}}
<div>
<span class="tag"><code>{{model.mountType}}</code></span>
<span class="tag">{{model.mountType}}</span>
<code class="has-text-grey is-size-8">{{model.mountAccessor}}</code>
</div>
</div>

View file

@ -14,7 +14,7 @@
size=14
class="has-text-grey-light"
}}{{item.name}}</a>
<span class="tag"><code>{{item.mountType}}</code></span>
<span class="tag">{{item.mountType}}</span>
<code class="has-text-grey is-size-8">{{item.mountAccessor}}</code>
</div>
<div class="column has-text-right">

View file

@ -26,7 +26,7 @@
{{#confirm-action
data-test-item-delete=true
confirmButtonClasses="button is-primary"
buttonClasses="link"
buttonClasses="link is-destroy"
onConfirmAction=(action "performTransaction" item)
confirmMessage=(concat "Are you sure you want to delete " item.id "?")
showConfirm=(get this (concat "shouldDelete-" item.id))

View file

@ -5,7 +5,7 @@
{{#confirm-action
confirmButtonClasses="button is-primary"
confirmButtonText="Remove"
buttonClasses="link"
buttonClasses="link is-destroy"
onConfirmAction=(action "performTransaction" model groupArray memberId)
confirmMessage=(concat "Are you sure you want to remove " memberId "?")
showConfirm=(get this (concat "shouldDelete-" memberId))

View file

@ -5,7 +5,7 @@
{{#confirm-action
confirmButtonClasses="button is-primary"
confirmButtonText="Remove"
buttonClasses="link"
buttonClasses="link is-destroy"
onConfirmAction=(action "performTransaction" model key)
confirmMessage=(concat "Are you sure you want to remove " key "?")
showConfirm=(get this (concat "shouldDelete-" key))

View file

@ -15,7 +15,7 @@
{{#confirm-action
confirmButtonClasses="button is-primary"
confirmButtonText="Remove"
buttonClasses="link"
buttonClasses="link is-destroy"
onConfirmAction=(action "performTransaction" model policyName)
confirmMessage=(concat "Are you sure you want to remove " policyName "?")
showConfirm=(get this (concat "shouldDelete-" policyName))

View file

@ -10,7 +10,7 @@
<li class="action">
{{#confirm-action
confirmButtonClasses="button is-primary"
buttonClasses="link"
buttonClasses="link is-destroy"
onConfirmAction=(action "delete" item)
confirmMessage=(concat "Are you sure you want to revoke " item.id "?")
showConfirm=(get this (concat "shouldDelete-" item.id))

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-ghost " (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

@ -1,7 +1,7 @@
<div class="is-flex-v-centered-tablet is-flex-1 is-fullwidth">
<div class="columns is-centered is-gapless is-fullwidth">
<div class="column is-4-desktop is-7-tablet">
<div class="box is-paddingless has-glow is-relative">
<div class="login-form box is-paddingless is-relative">
<div class="hero has-dark-grey-gradient">
<div class="hero-body">
<div class="has-current-color-fill has-text-white">

View file

@ -1,4 +1,4 @@
{{#basic-dropdown-hover horizontalPosition="auto-right" verticalPosition="below" as |d|}}
{{#basic-dropdown-hover horizontalPosition="auto-left" verticalPosition="below" as |d|}}
{{#d.trigger tagName=(if (eq type "replication") "span" "button") class=(if (eq type "replication") "" "button is-transparent")}}
{{#if (eq type "user")}}
<div class="status-menu-user-trigger">

View file

@ -10,13 +10,13 @@
{{capitalize modeForUrl}}
</span>
{{#if secondaryId}}
<span class="tag is-light">
<span class="tag">
<code>
{{secondaryId}}
</code>
</span>
{{/if}}
<span class="tag is-light">
<span class="tag">
<code>
{{clusterIdDisplay}}
</code>
@ -29,7 +29,9 @@
<div class="level-right">
{{#if replicationEnabled}}
{{#if (get cluster (concat mode 'StateGlyph'))}}
{{i-con size=14 glyph=(get cluster (concat mode 'StateGlyph'))}}
<span class="has-text-success">
{{i-con size=16 glyph=(get cluster (concat mode 'StateGlyph'))}}
</span>
{{else if syncProgress}}
<progress value="{{syncProgressPercent}}" max="100" class="progress is-small is-narrow is-info">
{{syncProgress.progress}} of {{syncProgress.total}} keys

View file

@ -18,13 +18,13 @@
{{capitalize modeForUrl}}
</span>
{{#if secondaryId}}
<span class="tag is-light">
<span class="tag">
<code>
{{secondaryId}}
</code>
</span>
{{/if}}
<span class="tag is-light">
<span class="tag">
<code>
{{clusterIdDisplay}}
</code>

View file

@ -57,7 +57,7 @@
<li class="action">
{{#confirm-action
confirmButtonClasses="button is-primary"
buttonClasses="link"
buttonClasses="link is-destroy"
onConfirmAction=(action "delete" item)
confirmMessage=(concat "Are you sure you want to delete " item.id "?")
showConfirm=(get this (concat "shouldDelete-" item.id))

View file

@ -57,7 +57,7 @@
<li class="action">
{{#confirm-action
confirmButtonClasses="button is-primary"
buttonClasses="link"
buttonClasses="link is-destroy"
onConfirmAction=(action "delete" item)
confirmMessage=(concat "Are you sure you want to delete " item.id "?")
showConfirm=(get this (concat "shouldDelete-" item.id))

View file

@ -60,7 +60,7 @@
<li class="action">
{{#confirm-action
confirmButtonClasses="button is-primary"
buttonClasses="link"
buttonClasses="link is-destroy"
onConfirmAction=(action "delete" item)
confirmMessage=(concat "Are you sure you want to delete " item.id "?")
showConfirm=(get this (concat "shouldDelete-" item.id))

View file

@ -1,12 +1,14 @@
<div class="card has-arrow">
<header class="card-header is-grey">
<p class="card-header-title is-uppercase has-background-grey-lighter has-text-grey-dark is-size-7">
In case of emergency
</p>
</header>
<div class="card-content">
<div class="content">
<p class="is-size-7">If you suspect your data has been compromised, you can <a href="{{href-to 'vault.cluster.settings.seal' cluster.name}}" onclick={{action d.actions.close}} class="link">seal your vault</a> to prevent access to your secrets.</p>
<div class="popup-menu-content is-wide">
<div class="box">
<div class="menu-label">
In case of emergency
</div>
<div class="menu-content">
<p>
If you suspect your data has been compromised, you can
<a href="{{href-to 'vault.cluster.settings.seal' cluster.name}}" onclick={{action d.actions.close}} class="link">seal your vault</a>
to prevent access to your secrets.
</p>
</div>
</div>
</div>

View file

@ -1,28 +1,31 @@
<div class="card replication-card">
<nav class="menu">
<p class="menu-label">Disaster Recovery</p>
<ul>
<li>
{{replication-mode-summary
mode="dr"
display='menu'
cluster=cluster
tagName='a'
click=(action d.actions.close)
}}
</li>
</ul>
<p class="menu-label">Performance</p>
<ul>
<li>
{{replication-mode-summary
mode="performance"
display='menu'
cluster=cluster
tagName='a'
click=(action d.actions.close)
}}
</li>
</ul>
</nav>
</div>
<div class="popup-menu-content is-wide is-top-nav-popup">
<div class="box">
<nav class="menu">
<p class="menu-label">Disaster Recovery</p>
<ul>
<li>
{{replication-mode-summary
mode="dr"
display='menu'
cluster=cluster
tagName='a'
click=(action d.actions.close)
}}
</li>
</ul>
<hr />
<p class="menu-label">Performance</p>
<ul>
<li>
{{replication-mode-summary
mode="performance"
display='menu'
cluster=cluster
tagName='a'
click=(action d.actions.close)
}}
</li>
</ul>
</nav>
</div>
</div>

View file

@ -0,0 +1 @@
<path d="M256,512 C114.615104,512 0,397.384896 0,256 C0,114.615104 114.615104,0 256,0 C397.384896,0 512,114.615104 512,256 C512,397.384896 397.384896,512 256,512 Z M256,480 C379.711784,480 480,379.711784 480,256 C480,132.288216 379.711784,32 256,32 C132.288216,32 32,132.288216 32,256 C32,379.711784 132.288216,480 256,480 Z M385.548422,144 L416,174.545548 L217.753913,376.759626 L96,260.314527 L127.007564,227.142329 L217.753913,312.691266 L385.548422,144 Z"/>

View file

@ -0,0 +1,21 @@
<g transform="translate(458 28)">
<path
d="M2.8,39h-409.6c-17,0-30.7,13.9-30.7,30.9V400c0,17.1,13.8,30.9,30.7,30.9H2.8c17,0,30.7-13.9,30.7-30.9 V70C33.5,52.9,19.8,39,2.8,39z"
fill="#0068FF"
/>
<path
d="M2.8,18.4h-409.6c-28.3,0-51.2,23.1-51.2,51.6V400c0,28.5,22.9,51.6,51.2,51.6H2.8 c28.3,0,51.2-23.1,51.2-51.6V70C54,41.5,31.1,18.4,2.8,18.4z M33.5,400c0,17.1-13.8,30.9-30.7,30.9h-409.6 c-17,0-30.7-13.9-30.7-30.9V70c0-17.1,13.8-30.9,30.7-30.9H2.8c17,0,30.7,13.9,30.7,30.9V400z"
fill="#8AB1FF"
/>
<polygon
points="-241.9,235.9 -241.7,235.7 -262.8,214.6 -263,214.8 -319.1,158.7 -340.2,179.8 -284.1,235.9 -340.2,292 -319.1,313.1 -263,257 -262.8,257.2 -241.7,236.1"
fill="#fff"
/>
<rect
x="-241.7"
y="283.5"
width="157.5"
height="29.5"
fill="#fff"
/>
</g>

View file

@ -0,0 +1,17 @@
<g transform="translate(458 28)">
<path
d="M2.8,18.4h-409.6c-28.3,0-51.2,23.1-51.2,51.6V400c0,28.5,22.9,51.6,51.2,51.6H2.8 c28.3,0,51.2-23.1,51.2-51.6V70C54,41.5,31.1,18.4,2.8,18.4z M33.5,400c0,17.1-13.8,30.9-30.7,30.9h-409.6 c-17,0-30.7-13.9-30.7-30.9V70c0-17.1,13.8-30.9,30.7-30.9H2.8c17,0,30.7,13.9,30.7,30.9V400z"
fill="#B3B9C0"
/>
<polygon
points="-241.9,235.9 -241.7,235.7 -262.8,214.6 -263,214.8 -319.1,158.7 -340.2,179.8 -284.1,235.9 -340.2,292 -319.1,313.1 -263,257 -262.8,257.2 -241.7,236.1"
fill="#fff"
/>
<rect
x="-241.7"
y="283.5"
width="157.5"
height="29.5"
fill="#fff"
/>
</g>

View file

@ -0,0 +1 @@
<path d="M272.769063,297.207977 L173.82716,297.207977 L173.82716,465.646091 C173.82716,464.098951 174.365679,464.592593 176.987654,464.592593 L455.111111,464.592593 C457.733087,464.592593 458.271605,464.098951 458.271605,465.646091 L458.271605,141.168724 C458.271605,142.715864 457.733087,142.222222 455.111111,142.222222 L176.987654,142.222222 C174.365679,142.222222 173.82716,142.715864 173.82716,141.168724 L173.82716,260.740741 L280.463115,260.740741 L230.986572,211.264198 L256.515702,185.735068 L345.867656,275.087023 L345.827346,275.127333 L345.867656,275.167643 L256.515702,364.519598 L230.986572,338.990468 L272.769063,297.207977 Z M126.419753,260.740741 L126.419753,141.168724 C126.419753,115.568167 149.059774,94.8148148 176.987654,94.8148148 L300.246914,94.8148148 L300.246914,47.4074074 L47.4074074,47.4074074 L47.4074074,335.012346 L126.419753,335.012346 L126.419753,297.207977 L79.0123457,297.207977 L79.0123457,260.740741 L126.419753,260.740741 Z M126.419753,382.419753 L46.3539095,382.419753 C20.7533522,382.419753 0,363.395847 0,339.928669 L0,42.4910837 C0,19.0239062 20.7533522,0 46.3539095,0 L301.300412,0 C326.900969,0 347.654321,19.0239062 347.654321,42.4910837 L347.654321,94.8148148 L455.111111,94.8148148 C483.038992,94.8148148 505.679012,115.568167 505.679012,141.168724 L505.679012,465.646091 C505.679012,491.246648 483.038992,512 455.111111,512 L176.987654,512 C149.059774,512 126.419753,491.246648 126.419753,465.646091 L126.419753,382.419753 Z"/>

View file

@ -0,0 +1 @@
<path d="M186.527813,356.143158 L47.8085404,494.86243 L21.6999823,468.753872 L165.927777,324.526077 L53.6999823,324.526077 L53.6999823,287.97114 L223.02503,287.97114 L223.02503,288.02886 L223.08275,288.02886 L223.08275,461.722531 L186.527813,461.722531 L186.527813,356.143158 Z M332.526077,154.910732 L471.245349,16.1914596 L497.353908,42.3000177 L353.126113,186.527813 L465.308859,186.527813 L465.308859,223.08275 L296.02886,223.08275 L296.02886,223.02503 L295.97114,223.02503 L295.97114,53.6999823 L332.526077,53.6999823 L332.526077,154.910732 Z"/>

View file

@ -0,0 +1 @@
<path d="M36.5260773,442.910732 L175.245349,304.19146 L201.353908,330.300018 L57.1261127,474.527813 L159.08275,474.527813 L159.08275,511.08275 L0.0288599258,511.08275 L0.0288599258,511.02503 L-0.0288599258,511.02503 L-0.0288599258,351.97114 L36.5260773,351.97114 L36.5260773,442.910732 Z M474.527813,68.143158 L335.80854,206.86243 L309.699982,180.753872 L453.927777,36.5260773 L351.97114,36.5260773 L351.97114,-0.0288599258 L511.02503,-0.0288599258 L511.02503,0.0288599258 L511.08275,0.0288599258 L511.08275,159.08275 L474.527813,159.08275 L474.527813,68.143158 Z"/>

View file

@ -3,7 +3,7 @@
<g id="vault-loading-y1">
<polygon class="vault-loading-order-4" points="0,0 5,10 10,0" style="transform-origin: 5px 5px" />
<polygon class="vault-loading-order-2" points="5,10 10,0 15,10" style="transform-origin: 10px 5px" />
<polygon class="vault-loading-order-3" points="10,0 15,10 20,0" xstyle="transform-origin: 15px 5px" />
<polygon class="vault-loading-order-3" points="10,0 15,10 20,0" style="transform-origin: 15px 5px" />
<polygon class="vault-loading-order-1" points="15,10 20,0 25,10" style="transform-origin: 20px 5px" />
<polygon class="vault-loading-order-3" points="20,0 25,10 30,0" style="transform-origin: 25px 5px" />
<polygon class="vault-loading-order-2" points="25,10 30,0 35,10" style="transform-origin: 30px 5px" />
@ -29,4 +29,4 @@
<text id="vault-loading-static" x="20" y="20" text-anchor="middle">
Loading...
</text>
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View file

@ -26,13 +26,11 @@
{{i-con glyph="folder" size=14 class="has-text-grey-light"}} <span data-test-path data-test-id={{method.id}}>{{method.path}}</span>
<br />
<span class="tag">
<code>
{{#if (eq method.type 'plugin')}}
{{method.type}}: {{method.config.plugin_name}}
{{else}}
{{method.type}}
{{/if}}
</code>
{{#if (eq method.type 'plugin')}}
{{method.type}}: {{method.config.plugin_name}}
{{else}}
{{method.type}}
{{/if}}
</span>
<code class="has-text-grey is-size-8">
{{method.accessor}}
@ -61,7 +59,7 @@
<li class="action">
{{#confirm-action
confirmButtonClasses="button is-primary"
buttonClasses="link"
buttonClasses="link is-destroy"
onConfirmAction=(perform disableMethod method)
confirmMessage=(concat "Are you sure you want to disable auth via " method.id "?")
showConfirm=(get this (concat "shouldDelete-" method.id))

View file

@ -5,10 +5,8 @@
<h1 class="title is-3">
{{uppercase policyType}} Policies
{{#unless (eq policyType "acl")}}
<span class="tag is-inverted is-outlined" aria-label="Enforcement level: {{model.enforcementLevel}}">
<code>
Sentinel
</code>
<span class="tag" aria-label="Enforcement level: {{model.enforcementLevel}}">
Sentinel
</span>
{{/unless}}
</h1>
@ -120,7 +118,7 @@
<li class="action">
{{#confirm-action
confirmButtonClasses="button is-primary"
buttonClasses="link"
buttonClasses="link is-destroy"
onConfirmAction=(action "deletePolicy" item)
confirmMessage=(concat "Are you sure you want to delete " item.id "?")
showConfirm=(get this (concat "shouldDelete-" item.id))

View file

@ -10,10 +10,8 @@
<h1 class="title is-3">
{{model.id}}
{{#if model.enforcementLevel}}
<span class="tag is-medium is-inverted is-outlined" aria-label="Enforcement level: {{model.enforcementLevel}}">
<code>
{{model.enforcementLevel}}
</code>
<span class="tag is-medium" aria-label="Enforcement level: {{model.enforcementLevel}}">
{{model.enforcementLevel}}
</span>
{{/if}}
</h1>

View file

@ -12,7 +12,7 @@
<h1 class="title is-3" data-test-policy-name="true">
{{model.id}}
{{#if model.enforcementLevel}}
<span class="tag is-inverted is-outlined is-font-mono has-text-grey-dark" aria-label="Enforcement level: {{model.enforcementLevel}}">
<span class="tag" aria-label="Enforcement level: {{model.enforcementLevel}}">
{{model.enforcementLevel}}
</span>
{{/if}}
@ -81,4 +81,3 @@
}}
</div>
</div>

View file

@ -18,7 +18,7 @@
confirmMessage=(concat "Are you sure you want to revoke " secondary "?")
confirmButtonText="Revoke"
cancelButtonText="Cancel"
buttonClasses="button link"
buttonClasses="button link is-destroy"
confirmButtonClasses="is-danger is-outlined button"
showConfirm=(get this (concat secondary '-isRevoking'))
containerClasses="is-block"

View file

@ -30,13 +30,11 @@
>{{i-con glyph="folder" size=14 class="has-text-grey-light"}}{{backend.path}}</a>
<br />
<span class="tag">
<code>
{{#if (eq backend.type 'plugin')}}
{{backend.type}}: {{backend.config.plugin_name}}
{{else}}
{{backend.type}}
{{/if}}
</code>
{{#if (eq backend.type 'plugin')}}
{{backend.type}}: {{backend.config.plugin_name}}
{{else}}
{{backend.type}}
{{/if}}
</span>
<code class="has-text-grey is-size-8">
{{#if (eq backend.options.version 2)}}

View file

@ -55,6 +55,18 @@ module.exports = function(defaults) {
app.import('node_modules/text-encoder-lite/index.js');
app.import('node_modules/Duration.js/duration.js');
app.import('node_modules/columnify/columnify.js', {
using: [
{ transformation: 'cjs', as: 'columnify' }
]
});
app.import('node_modules/yargs-parser/lib/tokenize-arg-string.js', {
using: [
{ transformation: 'cjs', as: 'yargs-parser-tokenizer' }
]
});
// Use `app.import` to add additional libraries to the generated
// output files.
//

View file

@ -14,7 +14,7 @@
"start2": "ember server --proxy=http://localhost:8202 --port=4202",
"test": "node scripts/start-vault.js & ember test",
"test-oss": "yarn run test -f='!enterprise'",
"fmt-js": "prettier-eslint --single-quote --trailing-comma es5 --print-width=110 --write {app,tests,config,lib,mirage}/**/*.js",
"fmt-js": "prettier-eslint --single-quote --no-use-tabs --trailing-comma es5 --print-width=110 --write '{app,tests,config,lib,mirage}/**/*.js'",
"fmt-styles": "prettier --write app/styles/**/*.*",
"fmt": "yarn run fmt-js && yarn run fmt-styles",
"precommit": "lint-staged"
@ -33,19 +33,24 @@
}
},
"devDependencies": {
"Duration.js": "icholy/Duration.js#golang_compatible",
"autosize": "3.0.17",
"babel-plugin-transform-object-rest-spread": "^6.23.0",
"base64-js": "1.2.1",
"broccoli-asset-rev": "^2.4.5",
"broccoli-sri-hash": "meirish/broccoli-sri-hash#rooturl",
"bulma": "^0.5.2",
"bulma-switch": "^0.0.1",
"codemirror": "5.15.2",
"cool-checkboxes-for-bulma.io": "^1.1.0",
"ember-ajax": "^3.0.0",
"ember-api-actions": "^0.1.8",
"ember-basic-dropdown": "^0.33.5",
"ember-basic-dropdown-hover": "^0.2.0",
"ember-cli": "~2.15.0",
"ember-cli": "~2.16.0",
"ember-cli-autoprefixer": "^0.8.1",
"ember-cli-babel": "^6.3.0",
"ember-cli-cjs-transform": "^1.2.0",
"ember-cli-clipboard": "^0.8.0",
"ember-cli-content-security-policy": "^1.0.0",
"ember-cli-dependency-checker": "^1.3.0",
@ -83,18 +88,16 @@
"ember-test-selectors": "^0.3.6",
"ember-truth-helpers": "1.2.0",
"ivy-codemirror": "2.1.0",
"jsonlint": "1.6.0",
"loader.js": "^4.2.3",
"normalize.css": "4.1.1",
"prettier": "^1.5.3",
"prettier-eslint-cli": "^4.2.1",
"qunit-dom": "^0.6.2",
"string.prototype.startswith": "mathiasbynens/String.prototype.startsWith",
"text-encoder-lite": "1.0.0",
"base64-js": "1.2.1",
"autosize": "3.0.17",
"jsonlint": "1.6.0",
"codemirror": "5.15.2",
"Duration.js": "icholy/Duration.js#golang_compatible",
"string.prototype.startswith": "mathiasbynens/String.prototype.startsWith"
"columnify": "^1.5.4",
"yargs-parser": "^10.0.0"
},
"engines": {
"node": "^4.5 || 6.* || >= 7.*"

View file

@ -0,0 +1,15 @@
import { moduleForComponent, test } from 'ember-qunit';
import hbs from 'htmlbars-inline-precompile';
moduleForComponent('console/log-command', 'Integration | Component | console/log command', {
integration: true,
});
test('it renders', function(assert) {
const commandText = 'list this/path';
this.set('content', commandText);
this.render(hbs`{{console/log-command content=content}}`);
assert.dom('pre').includesText(commandText);
});

View file

@ -0,0 +1,13 @@
import { moduleForComponent, test } from 'ember-qunit';
import hbs from 'htmlbars-inline-precompile';
moduleForComponent('console/log-error', 'Integration | Component | console/log error', {
integration: true,
});
test('it renders', function(assert) {
const errorText = 'Error deleting at: sys/foo.\nURL: v1/sys/foo\nCode: 404';
this.set('content', errorText);
this.render(hbs`{{console/log-error content=content}}`);
assert.dom('pre').includesText(errorText);
});

View file

@ -0,0 +1,24 @@
import { moduleForComponent, test } from 'ember-qunit';
import hbs from 'htmlbars-inline-precompile';
moduleForComponent('console/log-json', 'Integration | Component | console/log json', {
integration: true,
beforeEach() {
this.inject.service('code-mirror', { as: 'codeMirror' });
},
});
test('it renders', function(assert) {
// Set any properties with this.set('myProperty', 'value');
// Handle any actions with this.on('myAction', function(val) { ... });
const objectContent = { one: 'two', three: 'four', seven: { five: 'six' }, eight: [5, 6] };
const expectedText = JSON.stringify(objectContent, null, 2);
this.set('content', objectContent);
this.render(hbs`{{console/log-json content=content}}`);
const instance = this.codeMirror.instanceFor(this.$('[data-test-component=json-editor]').attr('id'));
assert.equal(instance.getValue(), expectedText);
});

View file

@ -0,0 +1,19 @@
import { moduleForComponent, test } from 'ember-qunit';
import hbs from 'htmlbars-inline-precompile';
moduleForComponent('console/log-list', 'Integration | Component | console/log list', {
integration: true,
});
test('it renders', function(assert) {
// Set any properties with this.set('myProperty', 'value');
// Handle any actions with this.on('myAction', function(val) { ... });
const listContent = { keys: ['one', 'two'] };
const expectedText = 'Keys\none\ntwo';
this.set('content', listContent);
this.render(hbs`{{console/log-list content=content}}`);
assert.dom('pre').includesText(`${expectedText}`);
});

View file

@ -0,0 +1,27 @@
import { moduleForComponent, test } from 'ember-qunit';
import hbs from 'htmlbars-inline-precompile';
import columnify from 'columnify';
import { capitalize } from 'vault/helpers/capitalize';
import { stringifyObjectValues } from 'vault/components/console/log-object';
moduleForComponent('console/log-object', 'Integration | Component | console/log object', {
integration: true,
});
test('it renders', function(assert) {
const objectContent = { one: 'two', three: 'four', seven: { five: 'six' }, eight: [5, 6] };
const data = { one: 'two', three: 'four', seven: { five: 'six' }, eight: [5, 6] };
stringifyObjectValues(data);
const expectedText = columnify(data, {
preserveNewLines: true,
headingTransform: function(heading) {
return capitalize([heading]);
},
});
this.set('content', objectContent);
this.render(hbs`{{console/log-object content=content}}`);
assert.dom('pre').includesText(`${expectedText}`);
});

View file

@ -0,0 +1,17 @@
import { moduleForComponent, test } from 'ember-qunit';
import hbs from 'htmlbars-inline-precompile';
moduleForComponent('console/log-text', 'Integration | Component | console/log text', {
integration: true,
});
test('it renders', function(assert) {
// Set any properties with this.set('myProperty', 'value');
// Handle any actions with this.on('myAction', function(val) { ... });
const text = 'Success! You did a thing!';
this.set('content', text);
this.render(hbs`{{console/log-text content=content}}`);
assert.dom('pre').includesText(text);
});

View file

@ -0,0 +1,118 @@
import { moduleForComponent, test } from 'ember-qunit';
import { create } from 'ember-cli-page-object';
import wait from 'ember-test-helpers/wait';
import uiPanel from 'vault/tests/pages/components/console/ui-panel';
import hbs from 'htmlbars-inline-precompile';
const component = create(uiPanel);
moduleForComponent('console/ui-panel', 'Integration | Component | console/ui panel', {
integration: true,
beforeEach() {
component.setContext(this);
},
afterEach() {
component.removeContext();
},
});
test('it renders', function(assert) {
this.render(hbs`{{console/ui-panel}}`);
assert.ok(component.hasInput);
});
test('it clears console input on enter', function(assert) {
this.render(hbs`{{console/ui-panel}}`);
component.consoleInput('list this/thing/here').enter();
return wait().then(() => {
assert.equal(component.consoleInputValue, '', 'empties input field on enter');
});
});
test('it clears the log when using clear command', function(assert) {
this.render(hbs`{{console/ui-panel}}`);
component.consoleInput('list this/thing/here').enter();
component.consoleInput('list this/other/thing').enter();
component.consoleInput('read another/thing').enter();
wait().then(() => {
assert.notEqual(component.logOutput, '', 'there is output in the log');
component.consoleInput('clear').enter();
});
wait().then(() => component.up());
return wait().then(() => {
assert.equal(component.logOutput, '', 'clears the output log');
assert.equal(
component.consoleInputValue,
'clear',
'populates console input with previous command on up after enter'
);
});
});
test('it adds command to history on enter', function(assert) {
this.render(hbs`{{console/ui-panel}}`);
component.consoleInput('list this/thing/here').enter();
wait().then(() => component.up());
wait().then(() => {
assert.equal(
component.consoleInputValue,
'list this/thing/here',
'populates console input with previous command on up after enter'
);
});
wait().then(() => component.down());
return wait().then(() => {
assert.equal(component.consoleInputValue, '', 'populates console input with next command on down');
});
});
test('it cycles through history with more than one command', function(assert) {
this.render(hbs`{{console/ui-panel}}`);
component.consoleInput('list this/thing/here').enter();
wait().then(() => component.consoleInput('read that/thing/there').enter());
wait().then(() => component.consoleInput('qwerty').enter());
wait().then(() => component.up());
wait().then(() => {
assert.equal(
component.consoleInputValue,
'qwerty',
'populates console input with previous command on up after enter'
);
});
wait().then(() => component.up());
wait().then(() => {
assert.equal(
component.consoleInputValue,
'read that/thing/there',
'populates console input with previous command on up'
);
});
wait().then(() => component.up());
wait().then(() => {
assert.equal(
component.consoleInputValue,
'list this/thing/here',
'populates console input with previous command on up'
);
});
wait().then(() => component.up());
wait().then(() => {
assert.equal(
component.consoleInputValue,
'qwerty',
'populates console input with initial command if cycled through all previous commands'
);
});
wait().then(() => component.down());
return wait().then(() => {
assert.equal(
component.consoleInputValue,
'',
'clears console input if down pressed after history is on most recent command'
);
});
});

View file

@ -0,0 +1,18 @@
import { text, triggerable, fillable, value, isPresent } from 'ember-cli-page-object';
import keys from 'vault/lib/keycodes';
export default {
consoleInput: fillable('[data-test-component="console/command-input"] input'),
consoleInputValue: value('[data-test-component="console/command-input"] input'),
logOutput: text('[data-test-component="console/output-log"]'),
up: triggerable('keyup', '[data-test-component="console/command-input"] input', {
eventProperties: { keyCode: keys.UP },
}),
down: triggerable('keyup', '[data-test-component="console/command-input"] input', {
eventProperties: { keyCode: keys.DOWN },
}),
enter: triggerable('keyup', '[data-test-component="console/command-input"] input', {
eventProperties: { keyCode: keys.ENTER },
}),
hasInput: isPresent('[data-test-component="console/command-input"] input'),
};

View file

@ -0,0 +1,13 @@
import { moduleFor, test } from 'ember-qunit';
moduleFor('adapter:console', 'Unit | Adapter | console', {
needs: ['service:auth', 'service:flash-messages', 'service:version'],
});
test('it builds the correct URL', function(assert) {
let adapter = this.subject();
let sysPath = 'sys/health';
let awsPath = 'aws/roles/my-other-role';
assert.equal(adapter.buildURL(sysPath), '/v1/sys/health');
assert.equal(adapter.buildURL(awsPath), '/v1/aws/roles/my-other-role');
});

View file

@ -0,0 +1,328 @@
import { module, test } from 'qunit';
import {
parseCommand,
extractDataAndFlags,
logFromResponse,
logFromError,
logErrorFromInput,
} from 'vault/lib/console-helpers';
module('lib/console-helpers', 'Unit | Lib | console helpers');
const testCommands = [
{
name: 'write with data',
command: `vault write aws/config/root \
access_key=AKIAJWVN5Z4FOFT7NLNA \
secret_key=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 \
region=us-east-1`,
expected: [
'write',
[],
'aws/config/root',
[
'access_key=AKIAJWVN5Z4FOFT7NLNA',
'secret_key=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',
'region=us-east-1',
],
],
},
{
name: 'read with field',
command: `vault read -field=access_key aws/creds/my-role`,
expected: ['read', ['-field=access_key'], 'aws/creds/my-role', []],
},
];
testCommands.forEach(function(testCase) {
test(`#parseCommand: ${testCase.name}`, function(assert) {
let result = parseCommand(testCase.command);
assert.deepEqual(result, testCase.expected);
});
});
test('#parseCommand: invalid commands', function(assert) {
let command = 'vault kv get foo';
let result = parseCommand(command);
assert.equal(result, false, 'parseCommand returns false by default');
assert.throws(
() => {
parseCommand(command, true);
},
/invalid command/,
'throws on invalid command when `shouldThrow` is true'
);
});
const testExtractCases = [
{
name: 'data fields',
input: [
[
'access_key=AKIAJWVN5Z4FOFT7NLNA',
'secret_key=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',
'region=us-east-1',
],
[],
],
expected: {
data: {
access_key: 'AKIAJWVN5Z4FOFT7NLNA',
secret_key: 'R4nm063hgMVo4BTT5xOs5nHLeLXA6lar7ZJ3Nt0i',
region: 'us-east-1',
},
flags: {},
},
},
{
name: 'repeated data and a flag',
input: [['allowed_domains=example.com', 'allowed_domains=foo.example.com'], ['-wrap-ttl=2h']],
expected: {
data: {
allowed_domains: ['example.com', 'foo.example.com'],
},
flags: {
wrapTTL: '2h',
},
},
},
{
name: 'data with more than one equals sign',
input: [['foo=bar=baz', 'foo=baz=bop', 'some=value=val'], []],
expected: {
data: {
foo: ['bar=baz', 'baz=bop'],
some: 'value=val',
},
flags: {},
},
},
];
testExtractCases.forEach(function(testCase) {
test(`#extractDataAndFlags: ${testCase.name}`, function(assert) {
let { data, flags } = extractDataAndFlags(...testCase.input);
assert.deepEqual(data, testCase.expected.data, 'has expected data');
assert.deepEqual(flags, testCase.expected.flags, 'has expected flags');
});
});
let testResponseCases = [
{
name: 'write response, no content',
args: [null, 'foo/bar', 'write', {}],
expectedData: {
type: 'success',
content: 'Success! Data written to: foo/bar',
},
},
{
name: 'delete response, no content',
args: [null, 'foo/bar', 'delete', {}],
expectedData: {
type: 'success',
content: 'Success! Data deleted (if it existed) at: foo/bar',
},
},
{
name: 'write, with content',
args: [{ data: { one: 'two' } }, 'foo/bar', 'write', {}],
expectedData: {
type: 'object',
content: { one: 'two' },
},
},
{
name: 'with wrap-ttl flag',
args: [{ wrap_info: { one: 'two' } }, 'foo/bar', 'read', { wrapTTL: '1h' }],
expectedData: {
type: 'object',
content: { one: 'two' },
},
},
{
name: 'with -format=json flag and wrap-ttl flag',
args: [{ foo: 'bar', wrap_info: { one: 'two' } }, 'foo/bar', 'read', { format: 'json', wrapTTL: '1h' }],
expectedData: {
type: 'json',
content: { foo: 'bar', wrap_info: { one: 'two' } },
},
},
{
name: 'with -format=json and -field flags',
args: [{ foo: 'bar', data: { one: 'two' } }, 'foo/bar', 'read', { format: 'json', field: 'one' }],
expectedData: {
type: 'json',
content: 'two',
},
},
{
name: 'with -format=json and -field, and -wrap-ttl flags',
args: [
{ foo: 'bar', wrap_info: { one: 'two' } },
'foo/bar',
'read',
{ format: 'json', wrapTTL: '1h', field: 'one' },
],
expectedData: {
type: 'json',
content: 'two',
},
},
{
name: 'with string field flag and wrap-ttl flag',
args: [{ foo: 'bar', wrap_info: { one: 'two' } }, 'foo/bar', 'read', { field: 'one', wrapTTL: '1h' }],
expectedData: {
type: 'text',
content: 'two',
},
},
{
name: 'with object field flag and wrap-ttl flag',
args: [
{ foo: 'bar', wrap_info: { one: { two: 'three' } } },
'foo/bar',
'read',
{ field: 'one', wrapTTL: '1h' },
],
expectedData: {
type: 'object',
content: { two: 'three' },
},
},
{
name: 'with response data and string field flag',
args: [{ foo: 'bar', data: { one: 'two' } }, 'foo/bar', 'read', { field: 'one', wrapTTL: '1h' }],
expectedData: {
type: 'text',
content: 'two',
},
},
{
name: 'with response data and object field flag ',
args: [
{ foo: 'bar', data: { one: { two: 'three' } } },
'foo/bar',
'read',
{ field: 'one', wrapTTL: '1h' },
],
expectedData: {
type: 'object',
content: { two: 'three' },
},
},
{
name: 'response with data',
args: [{ foo: 'bar', data: { one: 'two' } }, 'foo/bar', 'read', {}],
expectedData: {
type: 'object',
content: { one: 'two' },
},
},
{
name: 'with response data, field flag, and field missing',
args: [{ foo: 'bar', data: { one: 'two' } }, 'foo/bar', 'read', { field: 'foo' }],
expectedData: {
type: 'error',
content: 'Field "foo" not present in secret',
},
},
{
name: 'with response data and auth block',
args: [{ data: { one: 'two' }, auth: { three: 'four' } }, 'auth/token/create', 'write', {}],
expectedData: {
type: 'object',
content: { three: 'four' },
},
},
{
name: 'with -field and -format with an object field',
args: [{ data: { one: { three: 'two' } } }, 'sys/mounts', 'read', { field: 'one', format: 'json' }],
expectedData: {
type: 'json',
content: { three: 'two' },
},
},
{
name: 'with -field and -format with a string field',
args: [{ data: { one: 'two' } }, 'sys/mounts', 'read', { field: 'one', format: 'json' }],
expectedData: {
type: 'json',
content: 'two',
},
},
];
testResponseCases.forEach(function(testCase) {
test(`#logFromResponse: ${testCase.name}`, function(assert) {
let data = logFromResponse(...testCase.args);
assert.deepEqual(data, testCase.expectedData);
});
});
let testErrorCases = [
{
name: 'AdapterError write',
args: [{ httpStatus: 404, path: 'v1/sys/foo', errors: [{}] }, 'sys/foo', 'write'],
expectedContent: 'Error writing to: sys/foo.\nURL: v1/sys/foo\nCode: 404',
},
{
name: 'AdapterError read',
args: [{ httpStatus: 404, path: 'v1/sys/foo', errors: [{}] }, 'sys/foo', 'read'],
expectedContent: 'Error reading from: sys/foo.\nURL: v1/sys/foo\nCode: 404',
},
{
name: 'AdapterError list',
args: [{ httpStatus: 404, path: 'v1/sys/foo', errors: [{}] }, 'sys/foo', 'list'],
expectedContent: 'Error listing: sys/foo.\nURL: v1/sys/foo\nCode: 404',
},
{
name: 'AdapterError delete',
args: [{ httpStatus: 404, path: 'v1/sys/foo', errors: [{}] }, 'sys/foo', 'delete'],
expectedContent: 'Error deleting at: sys/foo.\nURL: v1/sys/foo\nCode: 404',
},
{
name: 'VaultError single error',
args: [{ httpStatus: 404, path: 'v1/sys/foo', errors: ['no client token'] }, 'sys/foo', 'delete'],
expectedContent: 'Error deleting at: sys/foo.\nURL: v1/sys/foo\nCode: 404\nErrors:\n no client token',
},
{
name: 'VaultErrors multiple errors',
args: [
{ httpStatus: 404, path: 'v1/sys/foo', errors: ['no client token', 'this is an error'] },
'sys/foo',
'delete',
],
expectedContent:
'Error deleting at: sys/foo.\nURL: v1/sys/foo\nCode: 404\nErrors:\n no client token\n this is an error',
},
];
testErrorCases.forEach(function(testCase) {
test(`#logFromError: ${testCase.name}`, function(assert) {
let data = logFromError(...testCase.args);
assert.deepEqual(data, { type: 'error', content: testCase.expectedContent }, 'returns the expected data');
});
});
const testCommandCases = [
{
name: 'errors when command does not include a path',
args: [],
expectedContent: 'A path is required to make a request.',
},
{
name: 'errors when write command does not include data and does not have force tag',
args: ['foo/bar', 'write', {}, []],
expectedContent: 'Must supply data or use -force',
},
];
testCommandCases.forEach(function(testCase) {
test(`#logErrorFromInput: ${testCase.name}`, function(assert) {
let data = logErrorFromInput(...testCase.args);
assert.deepEqual(data, { type: 'error', content: testCase.expectedContent }, 'returns the pcorrect data');
});
});

View file

@ -0,0 +1,94 @@
import { moduleFor, test } from 'ember-qunit';
import { sanitizePath, ensureTrailingSlash } from 'vault/services/console';
import sinon from 'sinon';
moduleFor('service:console', 'Unit | Service | console', {
needs: ['service:auth'],
beforeEach() {},
afterEach() {},
});
test('#sanitizePath', function(assert) {
assert.equal(sanitizePath(' /foo/bar/baz/ '), 'foo/bar/baz', 'removes spaces and slashs on either side');
assert.equal(sanitizePath('//foo/bar/baz/'), 'foo/bar/baz', 'removes more than one slash');
});
test('#ensureTrailingSlash', function(assert) {
assert.equal(ensureTrailingSlash('foo/bar'), 'foo/bar/', 'adds trailing slash');
assert.equal(ensureTrailingSlash('baz/'), 'baz/', 'keeps trailing slash if there is one');
});
let testCases = [
{
method: 'read',
args: ['/sys/health', {}],
expectedURL: 'sys/health',
expectedVerb: 'GET',
expectedOptions: { data: undefined, wrapTTL: undefined },
},
{
method: 'read',
args: ['/secrets/foo/bar', {}, '30m'],
expectedURL: 'secrets/foo/bar',
expectedVerb: 'GET',
expectedOptions: { data: undefined, wrapTTL: '30m' },
},
{
method: 'write',
args: ['aws/roles/my-other-role', { arn: 'arn=arn:aws:iam::aws:policy/AmazonEC2ReadOnlyAccess' }],
expectedURL: 'aws/roles/my-other-role',
expectedVerb: 'POST',
expectedOptions: {
data: { arn: 'arn=arn:aws:iam::aws:policy/AmazonEC2ReadOnlyAccess' },
wrapTTL: undefined,
},
},
{
method: 'list',
args: ['secret/mounts', {}],
expectedURL: 'secret/mounts/',
expectedVerb: 'GET',
expectedOptions: { data: { list: true }, wrapTTL: undefined },
},
{
method: 'list',
args: ['secret/mounts', {}, '1h'],
expectedURL: 'secret/mounts/',
expectedVerb: 'GET',
expectedOptions: { data: { list: true }, wrapTTL: '1h' },
},
{
method: 'delete',
args: ['secret/secrets/kv'],
expectedURL: 'secret/secrets/kv',
expectedVerb: 'DELETE',
expectedOptions: { data: undefined, wrapTTL: undefined },
},
];
test('it reads, writes, lists, deletes', function(assert) {
let ajax = sinon.stub();
let uiConsole = this.subject({
adapter() {
return {
buildURL(url) {
return url;
},
ajax,
};
},
});
testCases.forEach(testCase => {
uiConsole[testCase.method](...testCase.args);
let [url, verb, options] = ajax.lastCall.args;
assert.equal(url, testCase.expectedURL, `${testCase.method}: uses trimmed passed url`);
assert.equal(verb, testCase.expectedVerb, `${testCase.method}: uses the correct verb`);
assert.deepEqual(options, testCase.expectedOptions, `${testCase.method}: uses the correct options`);
});
});

File diff suppressed because it is too large Load diff

View file

@ -935,7 +935,7 @@ func TestTokenStore_RevokeSelf(t *testing.T) {
t.Fatalf("err: %v\nresp: %#v", err, resp)
}
time.Sleep(200 * time.Millisecond)
time.Sleep(1000 * time.Millisecond)
lookup := []string{ent1.ID, ent2.ID, ent3.ID, ent4.ID}
for _, id := range lookup {

Some files were not shown because too many files have changed in this diff Show more