diff --git a/ui/app/styles/components/confirm.scss b/ui/app/styles/components/confirm.scss index f85f7f8767..9e80017ae9 100644 --- a/ui/app/styles/components/confirm.scss +++ b/ui/app/styles/components/confirm.scss @@ -1,3 +1,75 @@ +.confirm-wrapper { + position: relative; + overflow: hidden; + border-radius: 2px; + box-shadow: $box-shadow, $box-shadow-middle; +} + +.confirm { + transition: transform $speed; + padding-top: 2px; +} + +.show-confirm { + transform: translateX(-100%); + transition: transform $speed; +} + +.confirm.show-confirm { + visibility: hidden; +} + +.confirm-overlay { + position: absolute; + background-color: white; + top: 0; + left: 100%; + width: 100%; +} + +.confirm, +.confirm-overlay { + button.link, + a { + background-color: $white; + color: $menu-item-color; + + &:hover { + background-color: $menu-item-hover-background-color; + color: $menu-item-hover-color; + } + + &.is-active { + background-color: $menu-item-active-background-color; + color: $menu-item-active-color; + } + + &.is-destroy { + color: $red; + + &:hover { + background-color: $red; + color: $white; + } + } + + &.disabled { + opacity: 0.5; + + &:hover { + background: transparent; + cursor: default; + } + } + } +} + +.confirm-action span .button { + display: block; + margin: 0.25rem auto; + width: 95%; +} + .confirm-action > span { @include from($tablet) { align-items: center; @@ -22,7 +94,7 @@ } } -.popup-menu-content .confirm-action-message { +.confirm-action-message { margin: 0; .message { @@ -56,6 +128,7 @@ flex: 1; text-align: center; width: auto; + padding: $spacing-xs; } } } diff --git a/ui/app/styles/components/namespace-picker.scss b/ui/app/styles/components/namespace-picker.scss index 8e6b72308d..c972a6fcac 100644 --- a/ui/app/styles/components/namespace-picker.scss +++ b/ui/app/styles/components/namespace-picker.scss @@ -64,6 +64,10 @@ border-radius: $radius; box-shadow: $box-shadow, $box-shadow-high; + &.ember-basic-dropdown-content { + background: $white; + } + @include from($mobile) { width: $drawer-width; } diff --git a/ui/app/styles/components/popup-menu.scss b/ui/app/styles/components/popup-menu.scss index 8f624e2483..eef0d28372 100644 --- a/ui/app/styles/components/popup-menu.scss +++ b/ui/app/styles/components/popup-menu.scss @@ -135,6 +135,8 @@ } .ember-basic-dropdown-content { + background-color: transparent; + &--left.popup-menu { margin: 0px 0 0 -8px; } diff --git a/ui/app/templates/components/auth-info.hbs b/ui/app/templates/components/auth-info.hbs index b06e753839..3999e80cdb 100644 --- a/ui/app/templates/components/auth-info.hbs +++ b/ui/app/templates/components/auth-info.hbs @@ -1,3 +1,4 @@ +
- +
diff --git a/ui/app/templates/vault/cluster/access/identity/index.hbs b/ui/app/templates/vault/cluster/access/identity/index.hbs index 37bbc37f0c..ef7833d1f1 100644 --- a/ui/app/templates/vault/cluster/access/identity/index.hbs +++ b/ui/app/templates/vault/cluster/access/identity/index.hbs @@ -31,65 +31,63 @@
{{#popup-menu name="identity-item" onOpen=(action "reloadRecord" item)}} - + {{/popup-menu}}
diff --git a/ui/app/templates/vault/cluster/access/namespaces/index.hbs b/ui/app/templates/vault/cluster/access/namespaces/index.hbs index 990c36761b..17b4b29d74 100644 --- a/ui/app/templates/vault/cluster/access/namespaces/index.hbs +++ b/ui/app/templates/vault/cluster/access/namespaces/index.hbs @@ -34,7 +34,7 @@ {{list.item.id}} - + {{#with (concat currentNamespace (if currentNamespace "/") list.item.id) as |targetNamespace|}} {{#if (contains targetNamespace accessibleNamespaces)}}
  • @@ -45,11 +45,11 @@ {{/if}} {{/with}}
  • - - Delete - + }} />
  • diff --git a/ui/app/templates/vault/cluster/policies/index.hbs b/ui/app/templates/vault/cluster/policies/index.hbs index 15fa46a342..da395c4471 100644 --- a/ui/app/templates/vault/cluster/policies/index.hbs +++ b/ui/app/templates/vault/cluster/policies/index.hbs @@ -88,51 +88,50 @@ {{/link-to}}
    - {{#popup-menu name="policy-nav"}} - + +
    {{/linked-block}} diff --git a/ui/app/templates/vault/cluster/secrets/backends.hbs b/ui/app/templates/vault/cluster/secrets/backends.hbs index 40ed9ffddf..ad3299b205 100644 --- a/ui/app/templates/vault/cluster/secrets/backends.hbs +++ b/ui/app/templates/vault/cluster/secrets/backends.hbs @@ -69,6 +69,7 @@
    {{#popup-menu name="engine-menu"}} + + + + {{/popup-menu}}
    @@ -131,29 +134,30 @@
    - {{#popup-menu name="engine-menu"}} - - {{/popup-menu}} + + + + +
    diff --git a/ui/blueprints/story/files/__path__/stories/__name__.stories.js b/ui/blueprints/story/files/__path__/stories/__name__.stories.js index 3d434c8418..f3cea75dc3 100644 --- a/ui/blueprints/story/files/__path__/stories/__name__.stories.js +++ b/ui/blueprints/story/files/__path__/stories/__name__.stories.js @@ -1,4 +1,4 @@ -/* eslint-disable import/extensions */ + import hbs from 'htmlbars-inline-precompile'; import { storiesOf } from '@storybook/ember'; <%= importMD %> diff --git a/ui/lib/core/addon/components/confirm-action.js b/ui/lib/core/addon/components/confirm-action.js index a58f30d5c0..980fa3ad0c 100644 --- a/ui/lib/core/addon/components/confirm-action.js +++ b/ui/lib/core/addon/components/confirm-action.js @@ -3,7 +3,7 @@ import layout from '../templates/components/confirm-action'; /** * @module ConfirmAction - * `ConfirmAction` is a button followed by a confirmation message and button used to prevent users from performing actions they do not intend to. + * `ConfirmAction` is a button followed by a pop up confirmation message and button used to prevent users from performing actions they do not intend to. * * @example * ```js diff --git a/ui/lib/core/addon/components/confirm.js b/ui/lib/core/addon/components/confirm.js new file mode 100644 index 0000000000..8949f53438 --- /dev/null +++ b/ui/lib/core/addon/components/confirm.js @@ -0,0 +1,70 @@ +import Component from '@ember/component'; +import { computed } from '@ember/object'; +import { htmlSafe } from '@ember/template'; +import layout from '../templates/components/confirm'; +import { next } from '@ember/runloop'; + +/** + * @module Confirm + * `Confirm` components prevent users from performing actions they do not intend to by showing a confirmation message as an overlay. This is a contextual component that should always be rendered with a `Message` which triggers the message. + * + * @example + * ```js + *
    + * + * + * + *
    + * ``` + */ + +export default Component.extend({ + layout, + openTrigger: null, + height: 0, + focusTrigger: null, + style: computed('height', function() { + return htmlSafe(`height: ${this.height}px`); + }), + wormholeReference: null, + wormholeId: computed(function() { + return `confirm-${this.elementId}`; + }), + didInsertElement() { + this.set('wormholeReference', this.element.querySelector(`#${this.wormholeId}`)); + }, + didRender() { + this.updateHeight(); + }, + updateHeight: function() { + let height; + height = this.openTrigger + ? this.element.querySelector('.confirm-overlay').clientHeight + : this.element.querySelector('.confirm').clientHeight; + this.set('height', height); + }, + actions: { + onTrigger: function(itemId, e) { + this.set('openTrigger', itemId); + + // store a reference to the trigger so we can focus the element + // after clicking cancel + this.set('focusTrigger', e.target); + this.updateHeight(); + }, + onCancel: function() { + this.set('openTrigger', ''); + this.updateHeight(); + + next(() => { + this.focusTrigger.focus(); + this.set('focusTrigger', null); + }); + }, + }, +}); diff --git a/ui/lib/core/addon/components/confirm/message.js b/ui/lib/core/addon/components/confirm/message.js new file mode 100644 index 0000000000..197e738d3e --- /dev/null +++ b/ui/lib/core/addon/components/confirm/message.js @@ -0,0 +1,54 @@ +import Component from '@ember/component'; +import { computed } from '@ember/object'; +import layout from '../../templates/components/confirm/message'; + +/** + * @module Message + * `Message` components trigger and display a confirmation message. They should only be used within a `Confirm` component. + * + * @example + * ```js + *
    + * + * + * + *
    + * ``` + * + * @property id=null {ID} - A unique identifier used to bind a trigger to a confirmation message. + * @property onConfirm=null {Func} - The action to take when the user clicks the confirm button. + * @property [triggerText='Delete'] {String} - The text on the trigger button. + * @property [title='Delete this?'] {String} - The header text to display in the confirmation message. + * @property [message='You will not be able to recover it later.'] {String} - The message to display above the confirm and cancel buttons. + * @property [confirmButtonText='Delete'] {String} - The text to display on the confirm button. + * @property [cancelButtonText='Cancel'] {String} - The text to display on the cancel button. + */ + +export default Component.extend({ + layout, + tagName: '', + renderedTrigger: null, + id: null, + onCancel() {}, + onConfirm() {}, + resetTrigger() {}, + title: 'Delete this?', + message: 'You will not be able to recover it later.', + triggerText: 'Delete', + confirmButtonText: 'Delete', + cancelButtonText: 'Cancel', + showConfirm: computed('renderedTrigger', function() { + return this.renderedTrigger === this.id; + }), + actions: { + onConfirm() { + this.onConfirm(); + this.resetTrigger(); + }, + }, +}); diff --git a/ui/lib/core/addon/templates/components/confirm.hbs b/ui/lib/core/addon/templates/components/confirm.hbs new file mode 100644 index 0000000000..7fa7ce3e14 --- /dev/null +++ b/ui/lib/core/addon/templates/components/confirm.hbs @@ -0,0 +1,17 @@ +{{! template-lint-disable no-inline-styles}} + +
    +
    + {{yield (hash + Message=(component "confirm/message" + renderedTrigger=(readonly this.openTrigger) + wormholeReference=this.wormholeReference + onCancel=(action 'onCancel') + onTrigger=(action 'onTrigger') + resetTrigger=(action (mut this.openTrigger) "") + )) + }} +
    +
    +
    +
    diff --git a/ui/lib/core/addon/templates/components/confirm/message.hbs b/ui/lib/core/addon/templates/components/confirm/message.hbs new file mode 100644 index 0000000000..b5d9f5e423 --- /dev/null +++ b/ui/lib/core/addon/templates/components/confirm/message.hbs @@ -0,0 +1,40 @@ +{{#if showConfirm}} + {{#maybe-in-element wormholeReference false}} +
    +
    +
    + + {{title}} +
    +

    + {{message}} +

    +
    +
    + + +
    +
    + {{/maybe-in-element}} +{{/if}} + + diff --git a/ui/lib/core/addon/templates/components/list-item/popup-menu.hbs b/ui/lib/core/addon/templates/components/list-item/popup-menu.hbs index 9823c5e91c..ede6cd46c3 100644 --- a/ui/lib/core/addon/templates/components/list-item/popup-menu.hbs +++ b/ui/lib/core/addon/templates/components/list-item/popup-menu.hbs @@ -1,10 +1,12 @@ {{#if hasMenu}} - + + + {{else}} {{yield item}} diff --git a/ui/lib/core/app/components/confirm.js b/ui/lib/core/app/components/confirm.js new file mode 100644 index 0000000000..58f22e40fe --- /dev/null +++ b/ui/lib/core/app/components/confirm.js @@ -0,0 +1 @@ +export { default } from 'core/components/confirm'; diff --git a/ui/lib/core/app/components/confirm/message.js b/ui/lib/core/app/components/confirm/message.js new file mode 100644 index 0000000000..5d6b1e2b5d --- /dev/null +++ b/ui/lib/core/app/components/confirm/message.js @@ -0,0 +1 @@ +export { default } from 'core/components/confirm/message'; diff --git a/ui/lib/core/stories/confirm-action.md b/ui/lib/core/stories/confirm-action.md index 5067ef3100..d6c61ffe04 100644 --- a/ui/lib/core/stories/confirm-action.md +++ b/ui/lib/core/stories/confirm-action.md @@ -1,7 +1,7 @@ ## ConfirmAction -`ConfirmAction` is a button followed by a confirmation message and button used to prevent users from performing actions they do not intend to. +`ConfirmAction` is a button followed by a pop up confirmation message and button used to prevent users from performing actions they do not intend to. **Properties** diff --git a/ui/lib/core/stories/confirm.md b/ui/lib/core/stories/confirm.md new file mode 100644 index 0000000000..0d64791c8a --- /dev/null +++ b/ui/lib/core/stories/confirm.md @@ -0,0 +1,28 @@ + + +## Confirm +`Confirm` components prevent users from performing actions they do not intend to by showing a confirmation message as an overlay. This is a contextual component that should always be rendered with a `Message` which triggers the message. + +See the `Message` component for a description of properties. + +**Example** + +```js +
    + + + +
    +``` + +**See** + +- [Uses of Confirm](https://github.com/hashicorp/vault/search?l=Handlebars&q=Confirm+OR+confirm) +- [Confirm Source Code](https://github.com/hashicorp/vault/blob/master/ui/lib/core/addon/components/confirm.js) + +--- diff --git a/ui/lib/core/stories/confirm.stories.js b/ui/lib/core/stories/confirm.stories.js new file mode 100644 index 0000000000..e548b86e3f --- /dev/null +++ b/ui/lib/core/stories/confirm.stories.js @@ -0,0 +1,47 @@ +import hbs from 'htmlbars-inline-precompile'; +import { storiesOf } from '@storybook/ember'; +import notes from './confirm.md'; +import { withKnobs, text } from '@storybook/addon-knobs'; + +storiesOf('Confirm/Confirm', module) + .addParameters({ options: { showPanel: true } }) + .addDecorator( + withKnobs({ + escapeHTML: false, + }) + ) + .add( + `Confirm`, + () => ({ + template: hbs` +
    Confirm
    + + `, + context: { + id: 'foo', + onConfirm: () => { + alert('Confirmed!'); + }, + title: text('title', 'Delete this?'), + message: text('message', 'You will not be able to recover it later.'), + confirmButtonText: text('confirmButtonText', 'Delete'), + cancelButtonText: text('cancelButtonText', 'Cancel'), + triggerText: text('triggerText', 'Delete'), + }, + }), + { notes } + ); diff --git a/ui/lib/core/stories/message.md b/ui/lib/core/stories/message.md new file mode 100644 index 0000000000..b27c3b2920 --- /dev/null +++ b/ui/lib/core/stories/message.md @@ -0,0 +1,39 @@ + + +## Message +`Message` components trigger and display a confirmation message. They should only be used within a `Confirm` component. + +**Properties** + +| Param | Type | Default | Description | +| --- | --- | --- | --- | +| id | ID | | A unique identifier used to bind a trigger to a confirmation message. | +| onConfirm | Func | | The action to take when the user clicks the confirm button. | +| [triggerText] | String |'Delete' | The text on the trigger button. | +| [title] | String | 'Delete this?' | The header text to display in the confirmation message. | +| [message] | String | 'You will not be able to recover it later.' | The message to display above the confirm and cancel buttons. | +| [confirmButtonText] | String | 'Delete' | The text to display on the confirm button. | +| [cancelButtonText] | String | 'Cancel' | The text to display on the cancel button. | + + +**Example** + +```js +
    + + + +
    +``` + +**See** + +- [Uses of Confirm](https://github.com/hashicorp/vault/search?l=Handlebars&q=Confirm+OR+confirm) +- [Confirm Source Code](https://github.com/hashicorp/vault/blob/master/ui/lib/core/addon/components/confirm.js) + +--- diff --git a/ui/lib/core/stories/message.stories.js b/ui/lib/core/stories/message.stories.js new file mode 100644 index 0000000000..9b587aae0b --- /dev/null +++ b/ui/lib/core/stories/message.stories.js @@ -0,0 +1,19 @@ +import hbs from 'htmlbars-inline-precompile'; +import { storiesOf } from '@storybook/ember'; +import notes from './message.md'; + +storiesOf('Confirm/Message/', module) + .addParameters({ options: { showPanel: true } }) + .add( + `Message`, + () => ({ + template: hbs` +
    Message
    +

    + Message components should never render on their own. See the Confirm component for an example of what a Message looks like. +

    + `, + context: {}, + }), + { notes } + ); diff --git a/ui/lib/kmip/addon/templates/credentials/index.hbs b/ui/lib/kmip/addon/templates/credentials/index.hbs index b17e1689a5..83cc564cec 100644 --- a/ui/lib/kmip/addon/templates/credentials/index.hbs +++ b/ui/lib/kmip/addon/templates/credentials/index.hbs @@ -54,7 +54,7 @@ {{list.item.id}} - +
  • {{#link-to "credentials.show" this.scope this.role list.item.id class="is-block"}} View credentials @@ -62,9 +62,13 @@
  • {{#if list.item.deletePath.canDelete}} - - Revoke credentials - + /> {{/if}}
    diff --git a/ui/lib/kmip/addon/templates/scope/roles.hbs b/ui/lib/kmip/addon/templates/scope/roles.hbs index 1ed34486f8..7c96a95145 100644 --- a/ui/lib/kmip/addon/templates/scope/roles.hbs +++ b/ui/lib/kmip/addon/templates/scope/roles.hbs @@ -64,7 +64,7 @@ {{list.item.id}} - +
  • {{#link-to "credentials" this.scope list.item.id class="is-block"}} View credentials @@ -84,9 +84,11 @@ {{/if}} {{#if list.item.updatePath.canDelete}} - - Delete role - + /> {{/if}} diff --git a/ui/lib/kmip/addon/templates/scopes/index.hbs b/ui/lib/kmip/addon/templates/scopes/index.hbs index d48281ef2d..d3ad6afff7 100644 --- a/ui/lib/kmip/addon/templates/scopes/index.hbs +++ b/ui/lib/kmip/addon/templates/scopes/index.hbs @@ -54,7 +54,7 @@ {{list.item.id}} - +
  • {{#link-to "scope" list.item.id class="is-block"}} View scope @@ -62,9 +62,13 @@
  • {{#if list.item.updatePath.canDelete}} - - Delete scope - + data-test-scope-delete="true" /> {{/if}}
    diff --git a/ui/stories/alert-popup.stories.js b/ui/stories/alert-popup.stories.js index ba5410e99b..5116382dca 100644 --- a/ui/stories/alert-popup.stories.js +++ b/ui/stories/alert-popup.stories.js @@ -1,4 +1,4 @@ -/* eslint-disable import/extensions */ + import hbs from 'htmlbars-inline-precompile'; import { storiesOf } from '@storybook/ember'; import notes from './alert-popup.md'; diff --git a/ui/stories/auth-config-form/config.stories.js b/ui/stories/auth-config-form/config.stories.js index 24b161f907..d986b3ed71 100644 --- a/ui/stories/auth-config-form/config.stories.js +++ b/ui/stories/auth-config-form/config.stories.js @@ -1,4 +1,4 @@ -/* eslint-disable import/extensions */ + import hbs from 'htmlbars-inline-precompile'; import { storiesOf } from '@storybook/ember'; import { withKnobs, select } from '@storybook/addon-knobs'; diff --git a/ui/stories/auth-config-form/options.stories.js b/ui/stories/auth-config-form/options.stories.js index 6d2d202361..6525cca5f8 100644 --- a/ui/stories/auth-config-form/options.stories.js +++ b/ui/stories/auth-config-form/options.stories.js @@ -1,4 +1,4 @@ -/* eslint-disable import/extensions */ + import hbs from 'htmlbars-inline-precompile'; import { storiesOf } from '@storybook/ember'; import { withKnobs, select } from '@storybook/addon-knobs'; diff --git a/ui/stories/auth-form.stories.js b/ui/stories/auth-form.stories.js index 49738b7be1..0368921860 100644 --- a/ui/stories/auth-form.stories.js +++ b/ui/stories/auth-form.stories.js @@ -1,4 +1,4 @@ -/* eslint-disable import/extensions */ + import hbs from 'htmlbars-inline-precompile'; import { storiesOf } from '@storybook/ember'; import notes from './auth-form.md'; diff --git a/ui/stories/http-requests-bar-chart.stories.js b/ui/stories/http-requests-bar-chart.stories.js index fba42ba134..8078146d7d 100644 --- a/ui/stories/http-requests-bar-chart.stories.js +++ b/ui/stories/http-requests-bar-chart.stories.js @@ -1,4 +1,4 @@ -/* eslint-disable import/extensions */ + import hbs from 'htmlbars-inline-precompile'; import { storiesOf } from '@storybook/ember'; import { withKnobs, object } from '@storybook/addon-knobs'; diff --git a/ui/stories/http-requests-container.stories.js b/ui/stories/http-requests-container.stories.js index f7d5c06fca..5aec14b956 100644 --- a/ui/stories/http-requests-container.stories.js +++ b/ui/stories/http-requests-container.stories.js @@ -1,4 +1,4 @@ -/* eslint-disable import/extensions */ + import hbs from 'htmlbars-inline-precompile'; import { storiesOf } from '@storybook/ember'; import { withKnobs, object } from '@storybook/addon-knobs'; diff --git a/ui/stories/http-requests-table.stories.js b/ui/stories/http-requests-table.stories.js index 396018938e..bbc349117a 100644 --- a/ui/stories/http-requests-table.stories.js +++ b/ui/stories/http-requests-table.stories.js @@ -1,4 +1,4 @@ -/* eslint-disable import/extensions */ + import hbs from 'htmlbars-inline-precompile'; import { storiesOf } from '@storybook/ember'; import { withKnobs, object } from '@storybook/addon-knobs'; diff --git a/ui/tests/integration/components/confirm-test.js b/ui/tests/integration/components/confirm-test.js new file mode 100644 index 0000000000..05df00a976 --- /dev/null +++ b/ui/tests/integration/components/confirm-test.js @@ -0,0 +1,102 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'ember-qunit'; +import { render, click } from '@ember/test-helpers'; +import hbs from 'htmlbars-inline-precompile'; +import sinon from 'sinon'; + +module('Integration | Component | Confirm', function(hooks) { + setupRenderingTest(hooks); + + hooks.beforeEach(function() { + this.set('id', 'foo'); + this.set('title', 'Are you sure?'); + this.set('message', 'You will not be able to recover this item later.'); + this.set('triggerText', 'Click me!'); + this.set('onConfirm', sinon.spy()); + }); + + test('it renders', async function(assert) { + await render(hbs` + + + + `); + + assert.dom('.confirm-wrapper').exists(); + assert.dom('.confirm').containsText(this.triggerText); + }); + + test('does not show the confirmation message until it is triggered', async function(assert) { + await render(hbs` + + + + `); + assert.dom('.confirm-overlay').doesNotContainText(this.message); + + await click('[data-test-confirm-action-trigger]'); + + assert.dom('.confirm-overlay').containsText(this.title); + assert.dom('.confirm-overlay').containsText(this.message); + }); + + test('it calls onConfirm when the confirm button is clicked', async function(assert) { + await render(hbs` + + + + `); + await click('[data-test-confirm-action-trigger]'); + await click('[data-test-confirm-button=true]'); + + assert.ok(this.onConfirm.calledOnce); + }); + + test('it shows only the active triggers message', async function(assert) { + await render(hbs` + + + + + `); + + await click(`[data-test-confirm-action-trigger=${this.id}]`); + assert.dom('.confirm-overlay').containsText(this.title); + assert.dom('.confirm-overlay').containsText(this.message); + + await click('[data-test-confirm-cancel-button]'); + + await click("[data-test-confirm-action-trigger='bar']"); + assert.dom('.confirm-overlay').containsText('Wow'); + assert.dom('.confirm-overlay').containsText('Bazinga!'); + }); +});