diff --git a/ui/app/adapters/console.js b/ui/app/adapters/console.js new file mode 100644 index 0000000000..473f2ce424 --- /dev/null +++ b/ui/app/adapters/console.js @@ -0,0 +1,8 @@ +import ApplicationAdapter from './application'; + +export default ApplicationAdapter.extend({ + namespace: 'v1', + pathForType(modelName) { + return modelName; + }, +}); diff --git a/ui/app/components/console/command-input.js b/ui/app/components/console/command-input.js new file mode 100644 index 0000000000..5ee6eeffe1 --- /dev/null +++ b/ui/app/components/console/command-input.js @@ -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')(); + } + }, +}); diff --git a/ui/app/components/console/log-command.js b/ui/app/components/console/log-command.js new file mode 100644 index 0000000000..6e705e6764 --- /dev/null +++ b/ui/app/components/console/log-command.js @@ -0,0 +1,3 @@ +import Ember from 'ember'; + +export default Ember.Component.extend({}); diff --git a/ui/app/components/console/log-error.js b/ui/app/components/console/log-error.js new file mode 100644 index 0000000000..6e705e6764 --- /dev/null +++ b/ui/app/components/console/log-error.js @@ -0,0 +1,3 @@ +import Ember from 'ember'; + +export default Ember.Component.extend({}); diff --git a/ui/app/components/console/log-help.js b/ui/app/components/console/log-help.js new file mode 100644 index 0000000000..6e705e6764 --- /dev/null +++ b/ui/app/components/console/log-help.js @@ -0,0 +1,3 @@ +import Ember from 'ember'; + +export default Ember.Component.extend({}); diff --git a/ui/app/components/console/log-json.js b/ui/app/components/console/log-json.js new file mode 100644 index 0000000000..6e705e6764 --- /dev/null +++ b/ui/app/components/console/log-json.js @@ -0,0 +1,3 @@ +import Ember from 'ember'; + +export default Ember.Component.extend({}); diff --git a/ui/app/components/console/log-list.js b/ui/app/components/console/log-list.js new file mode 100644 index 0000000000..fcca15f272 --- /dev/null +++ b/ui/app/components/console/log-list.js @@ -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; + }), +}); diff --git a/ui/app/components/console/log-object.js b/ui/app/components/console/log-object.js new file mode 100644 index 0000000000..275f1edb72 --- /dev/null +++ b/ui/app/components/console/log-object.js @@ -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); + }, + }); + }), +}); diff --git a/ui/app/components/console/log-success.js b/ui/app/components/console/log-success.js new file mode 100644 index 0000000000..6e705e6764 --- /dev/null +++ b/ui/app/components/console/log-success.js @@ -0,0 +1,3 @@ +import Ember from 'ember'; + +export default Ember.Component.extend({}); diff --git a/ui/app/components/console/log-text.js b/ui/app/components/console/log-text.js new file mode 100644 index 0000000000..6e705e6764 --- /dev/null +++ b/ui/app/components/console/log-text.js @@ -0,0 +1,3 @@ +import Ember from 'ember'; + +export default Ember.Component.extend({}); diff --git a/ui/app/components/console/output-log.js b/ui/app/components/console/output-log.js new file mode 100644 index 0000000000..a4c209e243 --- /dev/null +++ b/ui/app/components/console/output-log.js @@ -0,0 +1,6 @@ +import Ember from 'ember'; + +export default Ember.Component.extend({ + 'data-test-component': 'console/output-log', + log: null, +}); diff --git a/ui/app/components/console/ui-panel.js b/ui/app/components/console/ui-panel.js new file mode 100644 index 0000000000..fe35294d3b --- /dev/null +++ b/ui/app/components/console/ui-panel.js @@ -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); + }, + }, +}); diff --git a/ui/app/components/json-editor.js b/ui/app/components/json-editor.js index 7b1a1c6692..8377e75e31 100644 --- a/ui/app/components/json-editor.js +++ b/ui/app/components/json-editor.js @@ -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) { diff --git a/ui/app/controllers/application.js b/ui/app/controllers/application.js index 5f6f6c9a9e..353649234f 100644 --- a/ui/app/controllers/application.js +++ b/ui/app/controllers/application.js @@ -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'); + }, + }, }); diff --git a/ui/app/helpers/multi-line-join.js b/ui/app/helpers/multi-line-join.js new file mode 100644 index 0000000000..cd22380f11 --- /dev/null +++ b/ui/app/helpers/multi-line-join.js @@ -0,0 +1,7 @@ +import Ember from 'ember'; + +export function multiLineJoin([arr]) { + return arr.join('\n'); +} + +export default Ember.Helper.helper(multiLineJoin); diff --git a/ui/app/lib/console-helpers.js b/ui/app/lib/console-helpers.js new file mode 100644 index 0000000000..0d8b12128d --- /dev/null +++ b/ui/app/lib/console-helpers.js @@ -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' }; + } +} diff --git a/ui/app/routes/vault/cluster/logout.js b/ui/app/routes/vault/cluster/logout.js index 0ddc7c110f..824d931164 100644 --- a/ui/app/routes/vault/cluster/logout.js +++ b/ui/app/routes/vault/cluster/logout.js @@ -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(); diff --git a/ui/app/routes/vault/cluster/secrets/backend/list.js b/ui/app/routes/vault/cluster/secrets/backend/list.js index dfbde5b705..1731c1525a 100644 --- a/ui/app/routes/vault/cluster/secrets/backend/list.js +++ b/ui/app/routes/vault/cluster/secrets/backend/list.js @@ -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; + } + }), }); }, diff --git a/ui/app/services/console.js b/ui/app/services/console.js new file mode 100644 index 0000000000..e35555f3bf --- /dev/null +++ b/ui/app/services/console.js @@ -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, + }); + }, +}); diff --git a/ui/app/styles/components/codemirror.scss b/ui/app/styles/components/codemirror.scss index 37f773a040..68cb9971c6 100644 --- a/ui/app/styles/components/codemirror.scss +++ b/ui/app/styles/components/codemirror.scss @@ -171,3 +171,7 @@ $gutter-grey: #2a2f36; } } } + +.cm-s-auto-height.CodeMirror { + height: auto; +} diff --git a/ui/app/styles/components/console-ui-panel.scss b/ui/app/styles/components/console-ui-panel.scss new file mode 100644 index 0000000000..9649bb8bb1 --- /dev/null +++ b/ui/app/styles/components/console-ui-panel.scss @@ -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; +} diff --git a/ui/app/styles/components/env-banner.scss b/ui/app/styles/components/env-banner.scss new file mode 100644 index 0000000000..d2851dcf69 --- /dev/null +++ b/ui/app/styles/components/env-banner.scss @@ -0,0 +1,10 @@ +.env-banner { + &, + &:not(:last-child):not(:last-child) { + margin: 0; + } + + .level-item { + padding: $size-10 $size-8; + } +} diff --git a/ui/app/styles/components/status-menu.scss b/ui/app/styles/components/status-menu.scss index 1c6937dae0..1fe965b998 100644 --- a/ui/app/styles/components/status-menu.scss +++ b/ui/app/styles/components/status-menu.scss @@ -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 { diff --git a/ui/app/styles/components/tool-tip.scss b/ui/app/styles/components/tool-tip.scss index a971e6e9b2..5d8c02b3cf 100644 --- a/ui/app/styles/components/tool-tip.scss +++ b/ui/app/styles/components/tool-tip.scss @@ -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; diff --git a/ui/app/styles/components/upgrade-overlay.scss b/ui/app/styles/components/upgrade-overlay.scss index 5ec71408eb..4ad81dc4ca 100644 --- a/ui/app/styles/components/upgrade-overlay.scss +++ b/ui/app/styles/components/upgrade-overlay.scss @@ -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; } diff --git a/ui/app/styles/components/vault-loading.scss b/ui/app/styles/components/vault-loading.scss index 1634dfa7ee..610375007d 100644 --- a/ui/app/styles/components/vault-loading.scss +++ b/ui/app/styles/components/vault-loading.scss @@ -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; - } - } \ No newline at end of file +#vault-loading-static { + display: none; + font-size: 9px; + + @media all and (-ms-high-contrast: none), (-ms-high-contrast: active) { + // For IE11 + display: block; + } +} diff --git a/ui/app/styles/core.scss b/ui/app/styles/core.scss index 3c8acc6437..bad506107d 100644 --- a/ui/app/styles/core.scss +++ b/ui/app/styles/core.scss @@ -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"; diff --git a/ui/app/styles/core/buttons.scss b/ui/app/styles/core/buttons.scss index 5efa15a61e..9472a28733 100644 --- a/ui/app/styles/core/buttons.scss +++ b/ui/app/styles/core/buttons.scss @@ -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 { diff --git a/ui/app/styles/core/generic.scss b/ui/app/styles/core/generic.scss index 358b6dd323..664c2a56b9 100644 --- a/ui/app/styles/core/generic.scss +++ b/ui/app/styles/core/generic.scss @@ -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; } diff --git a/ui/app/styles/utils/_bulma_variables.scss b/ui/app/styles/utils/_bulma_variables.scss index 7d68128498..6a97abeda0 100644 --- a/ui/app/styles/utils/_bulma_variables.scss +++ b/ui/app/styles/utils/_bulma_variables.scss @@ -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; diff --git a/ui/app/styles/utils/animations.scss b/ui/app/styles/utils/animations.scss index 8c7c3b2d7b..0020823b45 100644 --- a/ui/app/styles/utils/animations.scss +++ b/ui/app/styles/utils/animations.scss @@ -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); } diff --git a/ui/app/styles/utils/mixins.scss b/ui/app/styles/utils/mixins.scss index 97f3cc0126..9832a8e86c 100644 --- a/ui/app/styles/utils/mixins.scss +++ b/ui/app/styles/utils/mixins.scss @@ -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; diff --git a/ui/app/templates/application.hbs b/ui/app/templates/application.hbs index c3d67ecfe2..e4480ce1be 100644 --- a/ui/app/templates/application.hbs +++ b/ui/app/templates/application.hbs @@ -1,6 +1,6 @@