mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-11-01 11:08:10 +00:00
UI console (#4631)
* adding columnify and ember-cli-cjs-transform * add yargs-parser * remove vendored yargs-parser tokenizer and use cjs transform to import it from actual yargs-parser * add clear command that clears the log, but maintains history * make codemirror have no gutter and be auto-height when rendered in the console output log * add fullscreen command and hook up fullscreen toggle button * hook up copy button
This commit is contained in:
8
ui/app/adapters/console.js
Normal file
8
ui/app/adapters/console.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import ApplicationAdapter from './application';
|
||||
|
||||
export default ApplicationAdapter.extend({
|
||||
namespace: 'v1',
|
||||
pathForType(modelName) {
|
||||
return modelName;
|
||||
},
|
||||
});
|
||||
36
ui/app/components/console/command-input.js
Normal file
36
ui/app/components/console/command-input.js
Normal 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')();
|
||||
}
|
||||
},
|
||||
});
|
||||
3
ui/app/components/console/log-command.js
Normal file
3
ui/app/components/console/log-command.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import Ember from 'ember';
|
||||
|
||||
export default Ember.Component.extend({});
|
||||
3
ui/app/components/console/log-error.js
Normal file
3
ui/app/components/console/log-error.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import Ember from 'ember';
|
||||
|
||||
export default Ember.Component.extend({});
|
||||
3
ui/app/components/console/log-help.js
Normal file
3
ui/app/components/console/log-help.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import Ember from 'ember';
|
||||
|
||||
export default Ember.Component.extend({});
|
||||
3
ui/app/components/console/log-json.js
Normal file
3
ui/app/components/console/log-json.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import Ember from 'ember';
|
||||
|
||||
export default Ember.Component.extend({});
|
||||
9
ui/app/components/console/log-list.js
Normal file
9
ui/app/components/console/log-list.js
Normal 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;
|
||||
}),
|
||||
});
|
||||
28
ui/app/components/console/log-object.js
Normal file
28
ui/app/components/console/log-object.js
Normal 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);
|
||||
},
|
||||
});
|
||||
}),
|
||||
});
|
||||
3
ui/app/components/console/log-success.js
Normal file
3
ui/app/components/console/log-success.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import Ember from 'ember';
|
||||
|
||||
export default Ember.Component.extend({});
|
||||
3
ui/app/components/console/log-text.js
Normal file
3
ui/app/components/console/log-text.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import Ember from 'ember';
|
||||
|
||||
export default Ember.Component.extend({});
|
||||
6
ui/app/components/console/output-log.js
Normal file
6
ui/app/components/console/output-log.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import Ember from 'ember';
|
||||
|
||||
export default Ember.Component.extend({
|
||||
'data-test-component': 'console/output-log',
|
||||
log: null,
|
||||
});
|
||||
84
ui/app/components/console/ui-panel.js
Normal file
84
ui/app/components/console/ui-panel.js
Normal 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);
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -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) {
|
||||
|
||||
@@ -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');
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
7
ui/app/helpers/multi-line-join.js
Normal file
7
ui/app/helpers/multi-line-join.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import Ember from 'ember';
|
||||
|
||||
export function multiLineJoin([arr]) {
|
||||
return arr.join('\n');
|
||||
}
|
||||
|
||||
export default Ember.Helper.helper(multiLineJoin);
|
||||
183
ui/app/lib/console-helpers.js
Normal file
183
ui/app/lib/console-helpers.js
Normal 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' };
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -67,7 +67,7 @@ export default Ember.Route.extend({
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
})
|
||||
}),
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
105
ui/app/services/console.js
Normal file
105
ui/app/services/console.js
Normal 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,
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -171,3 +171,7 @@ $gutter-grey: #2a2f36;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cm-s-auto-height.CodeMirror {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
149
ui/app/styles/components/console-ui-panel.scss
Normal file
149
ui/app/styles/components/console-ui-panel.scss
Normal file
@@ -0,0 +1,149 @@
|
||||
.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 {
|
||||
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;
|
||||
}
|
||||
10
ui/app/styles/components/env-banner.scss
Normal file
10
ui/app/styles/components/env-banner.scss
Normal file
@@ -0,0 +1,10 @@
|
||||
.env-banner {
|
||||
&,
|
||||
&:not(:last-child):not(:last-child) {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.level-item {
|
||||
padding: $size-10 $size-8;
|
||||
}
|
||||
}
|
||||
@@ -59,7 +59,7 @@
|
||||
|
||||
.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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@keyframes vault-loading-animation {
|
||||
@keyframes vault-loading-animation {
|
||||
0%,
|
||||
70%,
|
||||
100% {
|
||||
@@ -8,13 +8,13 @@
|
||||
35% {
|
||||
transform: scale3D(0, 0, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#vault-loading {
|
||||
#vault-loading {
|
||||
polygon {
|
||||
animation: vault-loading-animation 1.3s infinite ease-in-out;
|
||||
transform-origin: 50% 50%;
|
||||
fill: #DCE2E9;
|
||||
fill: #dce2e9;
|
||||
}
|
||||
|
||||
.vault-loading-order-1 {
|
||||
@@ -32,16 +32,16 @@
|
||||
.vault-loading-order-4 {
|
||||
animation-delay: .4s;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#vault-loading-animated {
|
||||
#vault-loading-animated {
|
||||
@media all and (-ms-high-contrast: none), (-ms-high-contrast: active) {
|
||||
// For IE11
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#vault-loading-static {
|
||||
#vault-loading-static {
|
||||
display: none;
|
||||
font-size: 9px;
|
||||
|
||||
@@ -49,4 +49,4 @@
|
||||
// For IE11
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -41,5 +41,8 @@ input::-webkit-inner-spin-button {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
text-decoration: underline;
|
||||
-moz-user-select: text;
|
||||
-webkit-user-select: text; /* Chrome all / Safari all */
|
||||
-moz-user-select: text; /* Firefox all */
|
||||
-ms-user-select: text; /* IE 10+ */
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
@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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
16
ui/app/templates/components/console/command-input.hbs
Normal file
16
ui/app/templates/components/console/command-input.hbs
Normal 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}}
|
||||
1
ui/app/templates/components/console/log-command.hbs
Normal file
1
ui/app/templates/components/console/log-command.hbs
Normal file
@@ -0,0 +1 @@
|
||||
<pre class="console-ui-command">{{i-con glyph="chevron-right" size=12}}{{content}}</pre>
|
||||
4
ui/app/templates/components/console/log-error.hbs
Normal file
4
ui/app/templates/components/console/log-error.hbs
Normal 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>
|
||||
16
ui/app/templates/components/console/log-help.hbs
Normal file
16
ui/app/templates/components/console/log-help.hbs
Normal 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 <command> [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>
|
||||
10
ui/app/templates/components/console/log-json.hbs
Normal file
10
ui/app/templates/components/console/log-json.hbs
Normal file
@@ -0,0 +1,10 @@
|
||||
{{json-editor
|
||||
value=(stringify content)
|
||||
options=(hash
|
||||
readOnly=true
|
||||
lineNumbers=false
|
||||
autoHeight=true
|
||||
gutters=false
|
||||
theme='hashi auto-height'
|
||||
)
|
||||
}}
|
||||
21
ui/app/templates/components/console/log-list.hbs
Normal file
21
ui/app/templates/components/console/log-list.hbs
Normal 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>
|
||||
18
ui/app/templates/components/console/log-object.hbs
Normal file
18
ui/app/templates/components/console/log-object.hbs
Normal 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>
|
||||
4
ui/app/templates/components/console/log-success.hbs
Normal file
4
ui/app/templates/components/console/log-success.hbs
Normal 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>
|
||||
1
ui/app/templates/components/console/log-text.hbs
Normal file
1
ui/app/templates/components/console/log-text.hbs
Normal file
@@ -0,0 +1 @@
|
||||
<pre>{{content}}</pre>
|
||||
5
ui/app/templates/components/console/output-log.hbs
Normal file
5
ui/app/templates/components/console/output-log.hbs
Normal file
@@ -0,0 +1,5 @@
|
||||
{{#each log as |message|}}
|
||||
{{#unless message.hidden}}
|
||||
{{component (concat 'console/log-' message.type) content=message.content}}
|
||||
{{/unless}}
|
||||
{{/each}}
|
||||
16
ui/app/templates/components/console/ui-panel.hbs
Normal file
16
ui/app/templates/components/console/ui-panel.hbs
Normal 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>
|
||||
21
ui/app/templates/svg/icons/console-active.hbs
Normal file
21
ui/app/templates/svg/icons/console-active.hbs
Normal 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>
|
||||
17
ui/app/templates/svg/icons/console.hbs
Normal file
17
ui/app/templates/svg/icons/console.hbs
Normal 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>
|
||||
1
ui/app/templates/svg/icons/copy.hbs
Normal file
1
ui/app/templates/svg/icons/copy.hbs
Normal 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"/>
|
||||
1
ui/app/templates/svg/icons/fullscreen-close.hbs
Normal file
1
ui/app/templates/svg/icons/fullscreen-close.hbs
Normal 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"/>
|
||||
1
ui/app/templates/svg/icons/fullscreen-open.hbs
Normal file
1
ui/app/templates/svg/icons/fullscreen-open.hbs
Normal 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"/>
|
||||
@@ -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.
|
||||
//
|
||||
|
||||
@@ -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.*"
|
||||
|
||||
15
ui/tests/integration/components/console/log-command-test.js
Normal file
15
ui/tests/integration/components/console/log-command-test.js
Normal 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);
|
||||
});
|
||||
13
ui/tests/integration/components/console/log-error-test.js
Normal file
13
ui/tests/integration/components/console/log-error-test.js
Normal 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);
|
||||
});
|
||||
24
ui/tests/integration/components/console/log-json-test.js
Normal file
24
ui/tests/integration/components/console/log-json-test.js
Normal 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);
|
||||
});
|
||||
19
ui/tests/integration/components/console/log-list-test.js
Normal file
19
ui/tests/integration/components/console/log-list-test.js
Normal 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}`);
|
||||
});
|
||||
27
ui/tests/integration/components/console/log-object-test.js
Normal file
27
ui/tests/integration/components/console/log-object-test.js
Normal 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}`);
|
||||
});
|
||||
17
ui/tests/integration/components/console/log-text-test.js
Normal file
17
ui/tests/integration/components/console/log-text-test.js
Normal 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);
|
||||
});
|
||||
118
ui/tests/integration/components/console/ui-panel-test.js
Normal file
118
ui/tests/integration/components/console/ui-panel-test.js
Normal 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'
|
||||
);
|
||||
});
|
||||
});
|
||||
18
ui/tests/pages/components/console/ui-panel.js
Normal file
18
ui/tests/pages/components/console/ui-panel.js
Normal 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'),
|
||||
};
|
||||
13
ui/tests/unit/adapters/console-test.js
Normal file
13
ui/tests/unit/adapters/console-test.js
Normal 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');
|
||||
});
|
||||
328
ui/tests/unit/lib/console-helpers-test.js
Normal file
328
ui/tests/unit/lib/console-helpers-test.js
Normal 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=R4nm063hgMVo4BTT5xOs5nHLeLXA6lar7ZJ3Nt0i \
|
||||
region=us-east-1`,
|
||||
expected: [
|
||||
'write',
|
||||
[],
|
||||
'aws/config/root',
|
||||
[
|
||||
'access_key=AKIAJWVN5Z4FOFT7NLNA',
|
||||
'secret_key=R4nm063hgMVo4BTT5xOs5nHLeLXA6lar7ZJ3Nt0i',
|
||||
'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=R4nm063hgMVo4BTT5xOs5nHLeLXA6lar7ZJ3Nt0i',
|
||||
'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');
|
||||
});
|
||||
});
|
||||
94
ui/tests/unit/services/console-test.js
Normal file
94
ui/tests/unit/services/console-test.js
Normal 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`);
|
||||
});
|
||||
});
|
||||
824
ui/yarn.lock
824
ui/yarn.lock
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user