diff --git a/ui-v2/app/mixins/acl/with-actions.js b/ui-v2/app/mixins/acl/with-actions.js index 90e8c4b38..54a09c8ed 100644 --- a/ui-v2/app/mixins/acl/with-actions.js +++ b/ui-v2/app/mixins/acl/with-actions.js @@ -1,92 +1,32 @@ import Mixin from '@ember/object/mixin'; import { get } from '@ember/object'; import { inject as service } from '@ember/service'; -import WithFeedback from 'consul-ui/mixins/with-feedback'; +import WithBlockingActions from 'consul-ui/mixins/with-blocking-actions'; -export default Mixin.create(WithFeedback, { +export default Mixin.create(WithBlockingActions, { settings: service('settings'), actions: { - create: function(item) { - get(this, 'feedback').execute( - () => { - return get(this, 'repo') - .persist(item) - .then(item => { - return this.transitionTo('dc.acls'); - }); - }, - `Your ACL token has been added.`, - `There was an error adding your ACL token.` - ); - }, - update: function(item) { - get(this, 'feedback').execute( - () => { - return get(this, 'repo') - .persist(item) - .then(() => { - return this.transitionTo('dc.acls'); - }); - }, - `Your ACL token was saved.`, - `There was an error saving your ACL token.` - ); - }, - delete: function(item) { - get(this, 'feedback').execute( - () => { - return ( - get(this, 'repo') - // ember-changeset doesn't support `get` - // and `data` returns an object not a model - .remove(item) - .then(() => { - switch (this.routeName) { - case 'dc.acls.index': - return this.refresh(); - default: - return this.transitionTo('dc.acls'); - } - }) - ); - }, - `Your ACL token was deleted.`, - `There was an error deleting your ACL token.` - ); - }, - cancel: function(item) { - this.transitionTo('dc.acls'); - }, use: function(item) { - get(this, 'feedback').execute( - () => { - return get(this, 'settings') - .persist({ token: get(item, 'ID') }) - .then(() => { - this.transitionTo('dc.services'); - }); - }, - `Now using new ACL token`, - `There was an error using that ACL token` - ); + return get(this, 'feedback').execute(() => { + return get(this, 'settings') + .persist({ token: get(item, 'ID') }) + .then(() => { + return this.transitionTo('dc.services'); + }); + }, 'use'); }, clone: function(item) { - get(this, 'feedback').execute( - () => { - return get(this, 'repo') - .clone(item) - .then(item => { - switch (this.routeName) { - case 'dc.acls.index': - return this.refresh(); - default: - return this.transitionTo('dc.acls'); - } - }); - }, - `Your ACL token was cloned.`, - `There was an error cloning your ACL token.` - ); + return get(this, 'feedback').execute(() => { + return get(this, 'repo') + .clone(item) + .then(item => { + // cloning is similar to delete in that + // if you clone from the listing page, stay on the listing page + // whereas if you clone form another token, take me back to the listing page + // so I can see it + return this.afterDelete(...arguments); + }); + }, 'clone'); }, }, }); diff --git a/ui-v2/app/mixins/intention/with-actions.js b/ui-v2/app/mixins/intention/with-actions.js index f56866607..08653a076 100644 --- a/ui-v2/app/mixins/intention/with-actions.js +++ b/ui-v2/app/mixins/intention/with-actions.js @@ -1,70 +1,17 @@ import Mixin from '@ember/object/mixin'; -import { get } from '@ember/object'; -import WithFeedback from 'consul-ui/mixins/with-feedback'; +import WithBlockingActions from 'consul-ui/mixins/with-blocking-actions'; import { INTERNAL_SERVER_ERROR as HTTP_INTERNAL_SERVER_ERROR } from 'consul-ui/utils/http/status'; -export default Mixin.create(WithFeedback, { - actions: { - create: function(item) { - get(this, 'feedback').execute( - () => { - return get(this, 'repo') - .persist(item) - .then(item => { - return this.transitionTo('dc.intentions'); - }); - }, - `Your intention has been added.`, - function(e) { - if (e.errors && e.errors[0]) { - const error = e.errors[0]; - if (parseInt(error.status) === HTTP_INTERNAL_SERVER_ERROR) { - if (error.detail.indexOf('duplicate intention found:') === 0) { - return `An intention already exists for this Source-Destination pair. Please enter a different combination of Services, or search the intentions to edit an existing intention.`; - } - } - } - return `There was an error adding your intention.`; +export default Mixin.create(WithBlockingActions, { + errorCreate: function(type, e) { + if (e.errors && e.errors[0]) { + const error = e.errors[0]; + if (parseInt(error.status) === HTTP_INTERNAL_SERVER_ERROR) { + if (error.detail.indexOf('duplicate intention found:') === 0) { + return 'exists'; } - ); - }, - update: function(item) { - get(this, 'feedback').execute( - () => { - return get(this, 'repo') - .persist(item) - .then(() => { - return this.transitionTo('dc.intentions'); - }); - }, - `Your intention was saved.`, - `There was an error saving your intention.` - ); - }, - delete: function(item) { - get(this, 'feedback').execute( - () => { - return ( - get(this, 'repo') - // ember-changeset doesn't support `get` - // and `data` returns an object not a model - .remove(item) - .then(() => { - switch (this.routeName) { - case 'dc.intentions.index': - return this.refresh(); - default: - return this.transitionTo('dc.intentions'); - } - }) - ); - }, - `Your intention was deleted.`, - `There was an error deleting your intention.` - ); - }, - cancel: function(item) { - this.transitionTo('dc.intentions'); - }, + } + } + return type; }, }); diff --git a/ui-v2/app/mixins/kv/with-actions.js b/ui-v2/app/mixins/kv/with-actions.js index e9abd5786..b281ef2c5 100644 --- a/ui-v2/app/mixins/kv/with-actions.js +++ b/ui-v2/app/mixins/kv/with-actions.js @@ -1,80 +1,35 @@ import Mixin from '@ember/object/mixin'; import { get, set } from '@ember/object'; -import WithFeedback from 'consul-ui/mixins/with-feedback'; +import WithBlockingActions from 'consul-ui/mixins/with-blocking-actions'; -const transitionToList = function(key = '/', transitionTo) { - if (key === '/') { - return transitionTo('dc.kv.index'); - } else { - return transitionTo('dc.kv.folder', key); - } -}; - -export default Mixin.create(WithFeedback, { +export default Mixin.create(WithBlockingActions, { + // afterCreate just calls afterUpdate + afterUpdate: function(item, parent) { + const key = get(parent, 'Key'); + if (key === '/') { + return this.transitionTo('dc.kv.index'); + } else { + return this.transitionTo('dc.kv.folder', key); + } + }, + afterDelete: function(item, parent) { + if (this.routeName === 'dc.kv.folder') { + return this.refresh(); + } + return this._super(...arguments); + }, actions: { - create: function(item, parent) { - get(this, 'feedback').execute( - () => { - return get(this, 'repo') - .persist(item) - .then(item => { - return transitionToList(get(parent, 'Key'), this.transitionTo.bind(this)); - }); - }, - `Your key has been added.`, - `There was an error adding your key.` - ); - }, - update: function(item, parent) { - get(this, 'feedback').execute( - () => { - return get(this, 'repo') - .persist(item) - .then(() => { - return transitionToList(get(parent, 'Key'), this.transitionTo.bind(this)); - }); - }, - `Your key has been saved.`, - `There was an error saving your key.` - ); - }, - delete: function(item, parent) { - get(this, 'feedback').execute( - () => { - return get(this, 'repo') - .remove(item) - .then(() => { - switch (this.routeName) { - case 'dc.kv.folder': - case 'dc.kv.index': - return this.refresh(); - default: - return transitionToList(get(parent, 'Key'), this.transitionTo.bind(this)); - } - }); - }, - `Your key was deleted.`, - `There was an error deleting your key.` - ); - }, - cancel: function(item, parent) { - return transitionToList(get(parent, 'Key'), this.transitionTo.bind(this)); - }, invalidateSession: function(item) { const controller = this.controller; const repo = get(this, 'sessionRepo'); - get(this, 'feedback').execute( - () => { - return repo.remove(item).then(() => { - const item = get(controller, 'item'); - set(item, 'Session', null); - delete item.Session; - set(controller, 'session', null); - }); - }, - `The session was invalidated.`, - `There was an error invalidating the session.` - ); + return get(this, 'feedback').execute(() => { + return repo.remove(item).then(() => { + const item = get(controller, 'item'); + set(item, 'Session', null); + delete item.Session; + set(controller, 'session', null); + }); + }, 'delete'); }, }, }); diff --git a/ui-v2/app/mixins/with-blocking-actions.js b/ui-v2/app/mixins/with-blocking-actions.js new file mode 100644 index 000000000..5cfca5d0e --- /dev/null +++ b/ui-v2/app/mixins/with-blocking-actions.js @@ -0,0 +1,117 @@ +import Mixin from '@ember/object/mixin'; +import { inject as service } from '@ember/service'; +import { get, set } from '@ember/object'; +/** With Blocking Actions + * This mixin contains common write actions (Create Update Delete) for routes. + * It could also be an Route to extend but decoration seems to be more sense right now. + * + * Each 'blocking action' (blocking in terms of showing some sort of blocking loader) is + * wrapped in the functionality to signal that the page should be blocked + * (currently via the 'feedback' service) as well as some sane default hooks for where the page + * should go when the action has finished. + * + * Hooks can and are being overwritten for custom redirects/error handling on a route by route basis. + * + * Notifications are part of the injectable 'feedback' service, meaning different ones + * could be easily swapped in an out as need be in the future. + * + */ +export default Mixin.create({ + _feedback: service('feedback'), + init: function() { + this._super(...arguments); + const feedback = get(this, '_feedback'); + const route = this; + set(this, 'feedback', { + execute: function(cb, type, error) { + return feedback.execute(cb, type, error, route.controller); + }, + }); + }, + afterCreate: function(item) { + // do the same as update + return this.afterUpdate(...arguments); + }, + afterUpdate: function(item) { + // e.g. dc.intentions.index + const parts = this.routeName.split('.'); + // e.g. index or edit + parts.pop(); + // e.g. dc.intentions, essentially go to the listings page + return this.transitionTo(parts.join('.')); + }, + afterDelete: function(item) { + // e.g. dc.intentions.index + const parts = this.routeName.split('.'); + // e.g. index or edit + const page = parts.pop(); + switch (page) { + case 'index': + // essentially if I'm on the index page, stay there + return this.refresh(); + default: + // e.g. dc.intentions essentially do to the listings page + return this.transitionTo(parts.join('.')); + } + }, + errorCreate: function(type, e) { + return type; + }, + errorUpdate: function(type, e) { + return type; + }, + errorDelete: function(type, e) { + return type; + }, + actions: { + cancel: function() { + // do the same as an update, or override + return this.afterUpdate(...arguments); + }, + create: function(item) { + return get(this, 'feedback').execute( + () => { + return get(this, 'repo') + .persist(item) + .then(item => { + return this.afterCreate(...arguments); + }); + }, + 'create', + (type, e) => { + return this.errorCreate(type, e); + } + ); + }, + update: function(item, parent) { + return get(this, 'feedback').execute( + () => { + return get(this, 'repo') + .persist(item) + .then(() => { + return this.afterUpdate(...arguments); + }); + }, + 'update', + (type, e) => { + return this.errorUpdate(type, e); + } + ); + }, + delete: function(item, parent) { + return get(this, 'feedback').execute( + () => { + return get(this, 'repo') + .remove(item) + .then(() => { + return this.afterDelete(...arguments); + }); + }, + 'delete', + (type, e) => { + return this.errorDelete(type, e); + } + ); + }, + }, +}); diff --git a/ui-v2/app/mixins/with-feedback.js b/ui-v2/app/mixins/with-feedback.js deleted file mode 100644 index 4038e70c6..000000000 --- a/ui-v2/app/mixins/with-feedback.js +++ /dev/null @@ -1,17 +0,0 @@ -import Mixin from '@ember/object/mixin'; -import { inject as service } from '@ember/service'; -import { get, set } from '@ember/object'; - -export default Mixin.create({ - _feedback: service('feedback'), - init: function() { - this._super(...arguments); - const feedback = get(this, '_feedback'); - const route = this; - set(this, 'feedback', { - execute: function() { - feedback.execute(...[...arguments, route.controller]); - }, - }); - }, -}); diff --git a/ui-v2/app/routes/dc/nodes/show.js b/ui-v2/app/routes/dc/nodes/show.js index 75da2ab0d..b13402edc 100644 --- a/ui-v2/app/routes/dc/nodes/show.js +++ b/ui-v2/app/routes/dc/nodes/show.js @@ -5,11 +5,11 @@ import { get, set } from '@ember/object'; import distance from 'consul-ui/utils/distance'; import tomographyFactory from 'consul-ui/utils/tomography'; -import WithFeedback from 'consul-ui/mixins/with-feedback'; +import WithBlockingActions from 'consul-ui/mixins/with-blocking-actions'; const tomography = tomographyFactory(distance); -export default Route.extend(WithFeedback, { +export default Route.extend(WithBlockingActions, { repo: service('nodes'), sessionRepo: service('session'), queryParams: { @@ -49,18 +49,14 @@ export default Route.extend(WithFeedback, { const dc = this.modelFor('dc').dc.Name; const controller = this.controller; const repo = get(this, 'sessionRepo'); - get(this, 'feedback').execute( - () => { - const node = get(item, 'Node'); - return repo.remove(item).then(() => { - return repo.findByNode(node, dc).then(function(sessions) { - set(controller, 'sessions', sessions); - }); + return get(this, 'feedback').execute(() => { + const node = get(item, 'Node'); + return repo.remove(item).then(() => { + return repo.findByNode(node, dc).then(function(sessions) { + set(controller, 'sessions', sessions); }); - }, - `The session was invalidated.`, - `There was an error invalidating the session.` - ); + }); + }, 'delete'); }, }, }); diff --git a/ui-v2/app/routes/settings.js b/ui-v2/app/routes/settings.js index cffbeb8aa..837219325 100644 --- a/ui-v2/app/routes/settings.js +++ b/ui-v2/app/routes/settings.js @@ -3,8 +3,8 @@ import { inject as service } from '@ember/service'; import { hash } from 'rsvp'; import { get } from '@ember/object'; -import WithFeedback from 'consul-ui/mixins/with-feedback'; -export default Route.extend(WithFeedback, { +import WithBlockingActions from 'consul-ui/mixins/with-blocking-actions'; +export default Route.extend(WithBlockingActions, { dcRepo: service('dc'), repo: service('settings'), model: function(params) { @@ -24,24 +24,8 @@ export default Route.extend(WithFeedback, { this._super(...arguments); controller.setProperties(model); }, - actions: { - update: function(item) { - get(this, 'feedback').execute( - () => { - return get(this, 'repo').persist(item); - }, - `Your settings were saved.`, - `There was an error saving your settings.` - ); - }, - delete: function(key) { - get(this, 'feedback').execute( - () => { - return get(this, 'repo').remove(key); - }, - `You settings have been reset.`, - `There was an error resetting your settings.` - ); - }, - }, + // overwrite afterUpdate and afterDelete hooks + // to avoid the default 'return to listing page' + afterUpdate: function() {}, + afterDelete: function() {}, }); diff --git a/ui-v2/app/services/feedback.js b/ui-v2/app/services/feedback.js index cff3cd9ce..2be104ef8 100644 --- a/ui-v2/app/services/feedback.js +++ b/ui-v2/app/services/feedback.js @@ -2,36 +2,41 @@ import Service, { inject as service } from '@ember/service'; import { get, set } from '@ember/object'; import callableType from 'consul-ui/utils/callable-type'; +const TYPE_SUCCESS = 'success'; +const TYPE_ERROR = 'error'; +const defaultStatus = function(type, obj) { + return type; +}; export default Service.extend({ notify: service('flashMessages'), logger: service('logger'), - execute: function(handle, success, error, controller) { + execute: function(handle, action, status = defaultStatus, controller) { set(controller, 'isLoading', true); - const displaySuccess = callableType(success); - const displayError = callableType(error); + const getAction = callableType(action); + const getStatus = callableType(status); const notify = get(this, 'notify'); return ( handle() - //TODO: pass this through to display success.. - .then(() => { + //TODO: pass this through to getAction.. + .then(target => { notify.add({ - type: 'success', + type: getStatus(TYPE_SUCCESS), // here.. - message: displaySuccess(), + action: getAction(), }); }) .catch(e => { get(this, 'logger').execute(e); if (e.name === 'TransitionAborted') { notify.add({ - type: 'success', + type: getStatus(TYPE_SUCCESS), // and here - message: displaySuccess(), + action: getAction(), }); } else { notify.add({ - type: 'error', - message: displayError(e), + type: getStatus(TYPE_ERROR, e), + action: getAction(), }); } }) diff --git a/ui-v2/app/templates/components/app-view.hbs b/ui-v2/app/templates/components/app-view.hbs index be624ad5a..37417d333 100644 --- a/ui-v2/app/templates/components/app-view.hbs +++ b/ui-v2/app/templates/components/app-view.hbs @@ -4,7 +4,8 @@ {{#each flashMessages.queue as |flash|}} {{#flash-message flash=flash as |component flash|}} {{! flashes automatically ucfirst the type }} -

{{if (eq component.flashType 'Success') 'Success!' 'Error!'}} {{flash.message}}

+ +

{{if (eq component.flashType 'Success') 'Success!' 'Error!'}} {{#yield-slot 'notification' (block-params (lowercase component.flashType) (lowercase flash.action) )}}{{yield}}{{/yield-slot}}

{{/flash-message}} {{/each}}
diff --git a/ui-v2/app/templates/dc/acls/-notifications.hbs b/ui-v2/app/templates/dc/acls/-notifications.hbs new file mode 100644 index 000000000..1b577c6ef --- /dev/null +++ b/ui-v2/app/templates/dc/acls/-notifications.hbs @@ -0,0 +1,32 @@ +{{#if (eq type 'create')}} + {{#if (eq status 'success') }} + Your ACL token has been added. + {{else}} + There was an error adding your ACL token. + {{/if}} +{{else if (eq type 'update') }} + {{#if (eq status 'success') }} + Your ACL token has been saved. + {{else}} + There was an error saving your ACL token. + {{/if}} +{{ else if (eq type 'delete')}} + {{#if (eq status 'success') }} + Your ACL token was deleted. + {{else}} + There was an error deleting your ACL token. + {{/if}} +{{ else if (eq type 'use')}} + {{#if (eq status 'success') }} + Now using new ACL token. + {{else}} + There was an error using that ACL token. + {{/if}} +{{ else if (eq type 'clone')}} + {{#if (eq status 'success') }} + Your ACL token was cloned. + {{else}} + There was an error cloning your ACL token. + {{/if}} +{{/if}} + diff --git a/ui-v2/app/templates/dc/acls/edit.hbs b/ui-v2/app/templates/dc/acls/edit.hbs index 4a6f70301..dde086202 100644 --- a/ui-v2/app/templates/dc/acls/edit.hbs +++ b/ui-v2/app/templates/dc/acls/edit.hbs @@ -1,4 +1,7 @@ {{#app-view class="acl edit" loading=isLoading}} + {{#block-slot 'notification' as |status type|}} + {{partial 'dc/acls/notifications'}} + {{/block-slot}} {{#block-slot 'breadcrumbs'}}
  1. All Tokens
  2. diff --git a/ui-v2/app/templates/dc/acls/index.hbs b/ui-v2/app/templates/dc/acls/index.hbs index 7587d4f2c..27bedb2ae 100644 --- a/ui-v2/app/templates/dc/acls/index.hbs +++ b/ui-v2/app/templates/dc/acls/index.hbs @@ -1,4 +1,7 @@ {{#app-view class="acl list" loading=isLoading}} + {{#block-slot 'notification' as |status type|}} + {{partial 'dc/acls/notifications'}} + {{/block-slot}} {{#block-slot 'header'}}

    ACL Tokens diff --git a/ui-v2/app/templates/dc/intentions/edit.hbs b/ui-v2/app/templates/dc/intentions/edit.hbs index 3fc2ba45d..8ee321c36 100644 --- a/ui-v2/app/templates/dc/intentions/edit.hbs +++ b/ui-v2/app/templates/dc/intentions/edit.hbs @@ -1,4 +1,7 @@ {{#app-view class="acl edit" loading=isLoading}} + {{#block-slot 'notification' as |status type|}} + {{partial 'dc/intentions/notifications'}} + {{/block-slot}} {{#block-slot 'breadcrumbs'}}
    1. All Intentions
    2. diff --git a/ui-v2/app/templates/dc/intentions/index.hbs b/ui-v2/app/templates/dc/intentions/index.hbs index dfde3056f..2236f7d79 100644 --- a/ui-v2/app/templates/dc/intentions/index.hbs +++ b/ui-v2/app/templates/dc/intentions/index.hbs @@ -1,4 +1,7 @@ {{#app-view class="intention list"}} + {{#block-slot 'notification' as |status type|}} + {{partial 'dc/intentions/notifications'}} + {{/block-slot}} {{#block-slot 'header'}}

      Intentions diff --git a/ui-v2/app/templates/dc/intentions/notifications.hbs b/ui-v2/app/templates/dc/intentions/notifications.hbs new file mode 100644 index 000000000..cdef9d5f2 --- /dev/null +++ b/ui-v2/app/templates/dc/intentions/notifications.hbs @@ -0,0 +1,22 @@ +{{#if (eq type 'create')}} + {{#if (eq status 'success') }} + Your intention has been added. + {{else if (eq status 'exists') }} + An intention already exists for this Source-Destination pair. Please enter a different combination of Services, or search the intentions to edit an existing intention. + {{else}} + There was an error adding your intention. + {{/if}} +{{else if (eq type 'update') }} + {{#if (eq status 'success') }} + Your intention has been saved. + {{else}} + There was an error saving your intention. + {{/if}} +{{ else if (eq type 'delete')}} + {{#if (eq status 'success') }} + Your intention was deleted. + {{else}} + There was an error deleting your intention. + {{/if}} +{{/if}} + diff --git a/ui-v2/app/templates/dc/kv/-notifications.hbs b/ui-v2/app/templates/dc/kv/-notifications.hbs new file mode 100644 index 000000000..7399dc2cd --- /dev/null +++ b/ui-v2/app/templates/dc/kv/-notifications.hbs @@ -0,0 +1,20 @@ +{{#if (eq type 'create')}} + {{#if (eq status 'success') }} + Your key has been added. + {{else}} + There was an error adding your key. + {{/if}} +{{else if (eq type 'update') }} + {{#if (eq status 'success') }} + Your key has been saved. + {{else}} + There was an error saving your key. + {{/if}} +{{ else if (eq type 'delete')}} + {{#if (eq status 'success') }} + Your key was deleted. + {{else}} + There was an error deleting your key. + {{/if}} +{{/if}} + diff --git a/ui-v2/app/templates/dc/kv/edit.hbs b/ui-v2/app/templates/dc/kv/edit.hbs index 22c7b3973..7bf538afb 100644 --- a/ui-v2/app/templates/dc/kv/edit.hbs +++ b/ui-v2/app/templates/dc/kv/edit.hbs @@ -1,4 +1,7 @@ {{#app-view class="kv edit" loading=isLoading}} + {{#block-slot 'notification' as |status type|}} + {{partial 'dc/kv/notifications'}} + {{/block-slot}} {{#block-slot 'breadcrumbs'}}
      1. Key / Values
      2. @@ -24,7 +27,7 @@ {{/if}} {{partial 'dc/kv/form'}} {{#if session}} -
        +

        Lock Session

        Name
        @@ -54,7 +57,7 @@
        {{#confirmation-dialog message='Are you sure you want to invalidate this session?'}} {{#block-slot 'action' as |confirm|}} - + {{/block-slot}} {{#block-slot 'dialog' as |execute cancel message|}}

        diff --git a/ui-v2/app/templates/dc/kv/index.hbs b/ui-v2/app/templates/dc/kv/index.hbs index 0733bef0c..c403241ae 100644 --- a/ui-v2/app/templates/dc/kv/index.hbs +++ b/ui-v2/app/templates/dc/kv/index.hbs @@ -1,4 +1,7 @@ {{#app-view class="kv list" loading=isLoading}} + {{#block-slot 'notification' as |status type|}} + {{partial 'dc/kv/notifications'}} + {{/block-slot}} {{#block-slot 'breadcrumbs'}}

          {{#if (not-eq parent.Key '/') }} diff --git a/ui-v2/app/templates/dc/nodes/-notifications.hbs b/ui-v2/app/templates/dc/nodes/-notifications.hbs new file mode 100644 index 000000000..70f90530b --- /dev/null +++ b/ui-v2/app/templates/dc/nodes/-notifications.hbs @@ -0,0 +1,8 @@ +{{#if (eq type 'delete')}} + {{#if (eq status 'success') }} + The session was invalidated. + {{else}} + There was an error invalidating the session. + {{/if}} +{{/if}} + diff --git a/ui-v2/app/templates/dc/nodes/show.hbs b/ui-v2/app/templates/dc/nodes/show.hbs index ee143d882..9a63296b8 100644 --- a/ui-v2/app/templates/dc/nodes/show.hbs +++ b/ui-v2/app/templates/dc/nodes/show.hbs @@ -1,4 +1,8 @@ {{#app-view class="node show"}} + {{#block-slot 'notification' as |status type|}} + {{!TODO: Move sessions to its own folder within nodes }} + {{partial 'dc/nodes/notifications'}} + {{/block-slot}} {{#block-slot 'breadcrumbs'}}
          1. All Nodes
          2. diff --git a/ui-v2/app/templates/dc/services/index.hbs b/ui-v2/app/templates/dc/services/index.hbs index 5267078b6..64c9a54e3 100644 --- a/ui-v2/app/templates/dc/services/index.hbs +++ b/ui-v2/app/templates/dc/services/index.hbs @@ -1,4 +1,8 @@ {{#app-view class="service list"}} + {{!TODO: Look at the item passed through to figure what partial to show, also move into its own service partial, for the moment keeping here for visibility}} + {{#block-slot 'notification' as |status type|}} + {{partial 'dc/acls/notifications'}} + {{/block-slot}} {{#block-slot 'header'}}

            Services diff --git a/ui-v2/app/templates/settings.hbs b/ui-v2/app/templates/settings.hbs index 8406d57a0..97390b6a6 100644 --- a/ui-v2/app/templates/settings.hbs +++ b/ui-v2/app/templates/settings.hbs @@ -1,5 +1,20 @@ {{#hashicorp-consul id="wrapper" dcs=dcs dc=dc}} {{#app-view class="settings show"}} + {{#block-slot 'notification' as |status type|}} + {{#if (eq type 'update')}} + {{#if (eq status 'success') }} + Your settings were saved. + {{else}} + There was an error saving your settings. + {{/if}} + {{ else if (eq type 'delete')}} + {{#if (eq status 'success') }} + You settings have been reset. + {{else}} + There was an error resetting your settings. + {{/if}} + {{/if}} + {{/block-slot}} {{#block-slot 'header'}}

            Settings diff --git a/ui-v2/ember-cli-build.js b/ui-v2/ember-cli-build.js index f1a9b6d14..c097b895c 100644 --- a/ui-v2/ember-cli-build.js +++ b/ui-v2/ember-cli-build.js @@ -45,6 +45,9 @@ module.exports = function(defaults) { "ie 11" ] }, + 'ember-cli-string-helpers': { + only: ['lowercase'] + } }); // Use `app.import` to add additional libraries to the generated // output files. diff --git a/ui-v2/package.json b/ui-v2/package.json index 68caedaa4..7f822a76d 100644 --- a/ui-v2/package.json +++ b/ui-v2/package.json @@ -59,6 +59,7 @@ "ember-cli-sass": "^7.1.4", "ember-cli-shims": "^1.2.0", "ember-cli-sri": "^2.1.0", + "ember-cli-string-helpers": "^1.9.0", "ember-cli-uglify": "^2.0.0", "ember-cli-yadda": "^0.4.0", "ember-collection": "^1.0.0-alpha.7", diff --git a/ui-v2/tests/acceptance/dc/acls/update.feature b/ui-v2/tests/acceptance/dc/acls/update.feature index f79d637c7..42a77b3e2 100644 --- a/ui-v2/tests/acceptance/dc/acls/update.feature +++ b/ui-v2/tests/acceptance/dc/acls/update.feature @@ -1,6 +1,6 @@ @setupApplicationTest Feature: dc / acls / update: ACL Update - Scenario: Update to [Name], [Type], [Rules] + Background: Given 1 datacenter model with the value "datacenter" And 1 acl model from yaml --- @@ -12,6 +12,7 @@ Feature: dc / acls / update: ACL Update acl: key --- Then the url should be /datacenter/acls/key + Scenario: Update to [Name], [Type], [Rules] Then I fill in with yaml --- name: [Name] @@ -23,6 +24,9 @@ Feature: dc / acls / update: ACL Update Name: [Name] Type: [Type] --- + Then the url should be /datacenter/acls + And "[data-notification]" has the "notification-update" class + And "[data-notification]" has the "success" class Where: ---------------------------------------------------------- | Name | Type | Rules | @@ -31,9 +35,15 @@ Feature: dc / acls / update: ACL Update | key%20name | client | node "0" {policy = "read"} | | utf8? | management | node "0" {policy = "write"} | ---------------------------------------------------------- -@ignore - Scenario: Rules can be edited/updated - Then ok -@ignore - Scenario: The feedback dialog says success or failure - Then ok + Scenario: There was an error saving the key + Given the url "/v1/acl/update" responds with a 500 status + And I submit + Then the url should be /datacenter/acls/key + Then "[data-notification]" has the "notification-update" class + And "[data-notification]" has the "error" class +# @ignore + # Scenario: Rules can be edited/updated + # Then ok +# @ignore + # Scenario: The feedback dialog says success or failure + # Then ok diff --git a/ui-v2/tests/acceptance/dc/acls/use.feature b/ui-v2/tests/acceptance/dc/acls/use.feature index 6ad722487..cfc1e0eb0 100644 --- a/ui-v2/tests/acceptance/dc/acls/use.feature +++ b/ui-v2/tests/acceptance/dc/acls/use.feature @@ -18,6 +18,8 @@ Feature: dc / acls / use: Using an ACL token And I click actions on the acls And I click use on the acls And I click confirmUse on the acls + Then "[data-notification]" has the "notification-use" class + And "[data-notification]" has the "success" class Then I have settings like yaml --- token: token @@ -34,6 +36,8 @@ Feature: dc / acls / use: Using an ACL token --- And I click use And I click confirmUse + Then "[data-notification]" has the "notification-use" class + And "[data-notification]" has the "success" class Then I have settings like yaml --- token: token diff --git a/ui-v2/tests/acceptance/dc/intentions/update.feature b/ui-v2/tests/acceptance/dc/intentions/update.feature new file mode 100644 index 000000000..7d46b636a --- /dev/null +++ b/ui-v2/tests/acceptance/dc/intentions/update.feature @@ -0,0 +1,41 @@ +@setupApplicationTest +Feature: dc / intentions / update: Intention Update + Background: + Given 1 datacenter model with the value "datacenter" + And 1 intention model from yaml + --- + ID: intention-id + --- + When I visit the intention page for yaml + --- + dc: datacenter + intention: intention-id + --- + Then the url should be /datacenter/intentions/intention-id + Scenario: Update to [Description], [Action], [Rules] + Then I fill in with yaml + --- + Description: [Name] + --- + And I click "[value=[Action]]" + And I submit + Then a PUT request is made to "/v1/connect/intentions/intention-id?dc=datacenter" with the body from yaml + --- + Description: [Name] + Action: [Action] + --- + Then the url should be /datacenter/intentions + And "[data-notification]" has the "notification-update" class + And "[data-notification]" has the "success" class + Where: + ------------------------------ + | Description | Action | + | Desc | allow | + ------------------------------ + Scenario: There was an error saving the intention + Given the url "/v1/connect/intentions/intention-id" responds with a 500 status + And I submit + Then the url should be /datacenter/intentions/intention-id + Then "[data-notification]" has the "notification-update" class + And "[data-notification]" has the "error" class + diff --git a/ui-v2/tests/acceptance/dc/kvs/sessions/invalidate.feature b/ui-v2/tests/acceptance/dc/kvs/sessions/invalidate.feature new file mode 100644 index 000000000..01cf8918d --- /dev/null +++ b/ui-v2/tests/acceptance/dc/kvs/sessions/invalidate.feature @@ -0,0 +1,32 @@ +@setupApplicationTest +Feature: dc / kvs / sessions / invalidate: Invalidate Lock Sessions + In order to invalidate a lock session + As a user + I should be able to invalidate a lock session by clicking a button and confirming + Background: + Given 1 datacenter model with the value "datacenter" + And 1 kv model from yaml + --- + Key: key + --- + When I visit the kv page for yaml + --- + dc: datacenter + kv: key + --- + Then the url should be /datacenter/kv/key/edit + + Scenario: Invalidating the lock session + And I click delete on the session + And I click confirmDelete on the session + Then the last PUT request was made to "/v1/session/destroy/ee52203d-989f-4f7a-ab5a-2bef004164ca?dc=datacenter" + Then the url should be /datacenter/kv/key/edit + And "[data-notification]" has the "notification-delete" class + And "[data-notification]" has the "success" class + Scenario: Invalidating a lock session and receiving an error + Given the url "/v1/session/destroy/ee52203d-989f-4f7a-ab5a-2bef004164ca?dc=datacenter" responds with a 500 status + And I click delete on the session + And I click confirmDelete on the session + Then the url should be /datacenter/kv/key/edit + And "[data-notification]" has the "notification-delete" class + And "[data-notification]" has the "error" class diff --git a/ui-v2/tests/acceptance/dc/kvs/update.feature b/ui-v2/tests/acceptance/dc/kvs/update.feature index ba54ebf6c..970d3ef61 100644 --- a/ui-v2/tests/acceptance/dc/kvs/update.feature +++ b/ui-v2/tests/acceptance/dc/kvs/update.feature @@ -19,6 +19,8 @@ Feature: dc / kvs / update: KV Update --- And I submit Then a PUT request is made to "/v1/kv/[Name]?dc=datacenter" with the body "[Value]" + And "[data-notification]" has the "notification-update" class + And "[data-notification]" has the "success" class Where: -------------------------------------------- | Name | Value | @@ -43,6 +45,9 @@ Feature: dc / kvs / update: KV Update --- And I submit Then a PUT request is made to "/v1/kv/key?dc=datacenter" with the body " " + Then the url should be /datacenter/kv + And "[data-notification]" has the "notification-update" class + And "[data-notification]" has the "success" class Scenario: Update to a key change value to '' And 1 kv model from yaml --- @@ -60,6 +65,9 @@ Feature: dc / kvs / update: KV Update --- And I submit Then a PUT request is made to "/v1/kv/key?dc=datacenter" with no body + Then the url should be /datacenter/kv + And "[data-notification]" has the "notification-update" class + And "[data-notification]" has the "success" class Scenario: Update to a key when the value is empty And 1 kv model from yaml --- @@ -74,9 +82,22 @@ Feature: dc / kvs / update: KV Update Then the url should be /datacenter/kv/key/edit And I submit Then a PUT request is made to "/v1/kv/key?dc=datacenter" with no body -@ignore - Scenario: The feedback dialog says success or failure - Then ok + Then the url should be /datacenter/kv + And "[data-notification]" has the "notification-update" class + And "[data-notification]" has the "success" class + Scenario: There was an error saving the key + When I visit the kv page for yaml + --- + dc: datacenter + kv: key + --- + Then the url should be /datacenter/kv/key/edit + + Given the url "/v1/kv/key" responds with a 500 status + And I submit + Then the url should be /datacenter/kv/key/edit + Then "[data-notification]" has the "notification-update" class + And "[data-notification]" has the "error" class @ignore Scenario: KV's with spaces are saved correctly Then ok diff --git a/ui-v2/tests/acceptance/dc/nodes/sessions/invalidate.feature b/ui-v2/tests/acceptance/dc/nodes/sessions/invalidate.feature index 786b9e4fd..ae77c20f0 100644 --- a/ui-v2/tests/acceptance/dc/nodes/sessions/invalidate.feature +++ b/ui-v2/tests/acceptance/dc/nodes/sessions/invalidate.feature @@ -3,7 +3,7 @@ Feature: dc / nodes / sessions / invalidate: Invalidate Lock Sessions In order to invalidate a lock session As a user I should be able to invalidate a lock session by clicking a button and confirming - Scenario: Given 2 lock sessions + Background: Given 1 datacenter model with the value "dc1" And 1 node model from yaml --- @@ -19,8 +19,20 @@ Feature: dc / nodes / sessions / invalidate: Invalidate Lock Sessions dc: dc1 node: node-0 --- + Then the url should be /dc1/nodes/node-0 And I click lockSessions on the tabs Then I see lockSessionsIsSelected on the tabs + Scenario: Invalidating the lock session And I click delete on the sessions And I click confirmDelete on the sessions Then a PUT request is made to "/v1/session/destroy/7bbbd8bb-fff3-4292-b6e3-cfedd788546a?dc=dc1" + Then the url should be /dc1/nodes/node-0 + And "[data-notification]" has the "notification-delete" class + And "[data-notification]" has the "success" class + Scenario: Invalidating a lock session and receiving an error + Given the url "/v1/session/destroy/7bbbd8bb-fff3-4292-b6e3-cfedd788546a?dc=dc1" responds with a 500 status + And I click delete on the sessions + And I click confirmDelete on the sessions + Then the url should be /dc1/nodes/node-0 + And "[data-notification]" has the "notification-delete" class + And "[data-notification]" has the "error" class diff --git a/ui-v2/tests/acceptance/deleting.feature b/ui-v2/tests/acceptance/deleting.feature index 5f7132881..19fabce57 100644 --- a/ui-v2/tests/acceptance/deleting.feature +++ b/ui-v2/tests/acceptance/deleting.feature @@ -1,8 +1,9 @@ @setupApplicationTest -Feature: deleting: Deleting from the listing and the detail page with confirmation - Scenario: Deleting a [Model] from the [Model] listing page +Feature: deleting: Deleting items with confirmations, success and error notifications + Background: Given 1 datacenter model with the value "datacenter" - And 1 [Model] model from json + Scenario: Deleting a [Model] from the [Model] listing page + Given 1 [Model] model from json --- [Data] --- @@ -14,6 +15,26 @@ Feature: deleting: Deleting from the listing and the detail page with confirmati And I click delete on the [Model]s And I click confirmDelete on the [Model]s Then a [Method] request is made to "[URL]" + And "[data-notification]" has the "notification-delete" class + And "[data-notification]" has the "success" class + When I visit the [Model] page for yaml + --- + dc: datacenter + [Slug] + --- + Given the url "[URL]" responds with a 500 status + And I click delete + And I click confirmDelete + And "[data-notification]" has the "notification-delete" class + And "[data-notification]" has the "error" class + Where: + --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + | Model | Method | URL | Data | Slug | + | acl | PUT | /v1/acl/destroy/something?dc=datacenter | {"Name": "something", "ID": "something"} | acl: something | + | kv | DELETE | /v1/kv/key-name?dc=datacenter | ["key-name"] | kv: key-name | + | intention | DELETE | /v1/connect/intentions/ee52203d-989f-4f7a-ab5a-2bef004164ca?dc=datacenter | {"SourceName": "name", "ID": "ee52203d-989f-4f7a-ab5a-2bef004164ca"} | intention: ee52203d-989f-4f7a-ab5a-2bef004164ca | + --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Scenario: Deleting a [Model] from the [Model] detail page When I visit the [Model] page for yaml --- dc: datacenter @@ -22,6 +43,18 @@ Feature: deleting: Deleting from the listing and the detail page with confirmati And I click delete And I click confirmDelete Then a [Method] request is made to "[URL]" + And "[data-notification]" has the "notification-delete" class + And "[data-notification]" has the "success" class + When I visit the [Model] page for yaml + --- + dc: datacenter + [Slug] + --- + Given the url "[URL]" responds with a 500 status + And I click delete + And I click confirmDelete + And "[data-notification]" has the "notification-delete" class + And "[data-notification]" has the "error" class Where: --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | Model | Method | URL | Data | Slug | diff --git a/ui-v2/tests/acceptance/settings/update.feature b/ui-v2/tests/acceptance/settings/update.feature index 300a9123a..46d5b8feb 100644 --- a/ui-v2/tests/acceptance/settings/update.feature +++ b/ui-v2/tests/acceptance/settings/update.feature @@ -16,4 +16,7 @@ Feature: settings / update: Update Settings --- token: '' --- + And the url should be /settings + And "[data-notification]" has the "notification-update" class + And "[data-notification]" has the "success" class diff --git a/ui-v2/tests/acceptance/steps/dc/intentions/update-steps.js b/ui-v2/tests/acceptance/steps/dc/intentions/update-steps.js new file mode 100644 index 000000000..a7eff3228 --- /dev/null +++ b/ui-v2/tests/acceptance/steps/dc/intentions/update-steps.js @@ -0,0 +1,10 @@ +import steps from '../../steps'; + +// step definitions that are shared between features should be moved to the +// tests/acceptance/steps/steps.js file + +export default function(assert) { + return steps(assert).then('I should find a file', function() { + assert.ok(true, this.step); + }); +} diff --git a/ui-v2/tests/acceptance/steps/dc/kvs/sessions/invalidate-steps.js b/ui-v2/tests/acceptance/steps/dc/kvs/sessions/invalidate-steps.js new file mode 100644 index 000000000..9bfbe9ac9 --- /dev/null +++ b/ui-v2/tests/acceptance/steps/dc/kvs/sessions/invalidate-steps.js @@ -0,0 +1,10 @@ +import steps from '../../../steps'; + +// step definitions that are shared between features should be moved to the +// tests/acceptance/steps/steps.js file + +export default function(assert) { + return steps(assert).then('I should find a file', function() { + assert.ok(true, this.step); + }); +} diff --git a/ui-v2/tests/lib/page-object/createDeletable.js b/ui-v2/tests/lib/page-object/createDeletable.js index cc3aff4c5..88a328d96 100644 --- a/ui-v2/tests/lib/page-object/createDeletable.js +++ b/ui-v2/tests/lib/page-object/createDeletable.js @@ -1,10 +1,13 @@ export default function(clickable) { - return function(obj) { + return function(obj, scope = '') { + if (scope !== '') { + scope = scope + ' '; + } return { ...obj, ...{ - delete: clickable('[data-test-delete]'), - confirmDelete: clickable('button.type-delete'), + delete: clickable(scope + '[data-test-delete]'), + confirmDelete: clickable(scope + 'button.type-delete'), }, }; }; diff --git a/ui-v2/tests/pages/dc/kv/edit.js b/ui-v2/tests/pages/dc/kv/edit.js index b644876cc..0e7b9ea1a 100644 --- a/ui-v2/tests/pages/dc/kv/edit.js +++ b/ui-v2/tests/pages/dc/kv/edit.js @@ -3,6 +3,7 @@ export default function(visitable, submitable, deletable, cancelable) { cancelable( deletable({ visit: visitable(['/:dc/kv/:kv/edit', '/:dc/kv/create'], str => str), + session: deletable({}, '[data-test-session]'), }) ) ); diff --git a/ui-v2/tests/steps.js b/ui-v2/tests/steps.js index 287c0900a..f2a3bd671 100644 --- a/ui-v2/tests/steps.js +++ b/ui-v2/tests/steps.js @@ -60,7 +60,7 @@ export default function(assert) { ) // TODO: Abstract this away from HTTP .given(['the url "$url" responds with a $status status'], function(url, status) { - return api.server.respondWithStatus(url, parseInt(status)); + return api.server.respondWithStatus(url.split('?')[0], parseInt(status)); }) // interactions .when('I visit the $name page', function(name) { @@ -390,10 +390,16 @@ export default function(assert) { // TODO: These should be mergeable .then(['"$selector" has the "$class" class'], function(selector, cls) { // because `find` doesn't work, guessing its sandboxed to ember's container - assert.ok(document.querySelector(selector).classList.contains(cls)); + assert.ok( + document.querySelector(selector).classList.contains(cls), + `Expected [class] to contain ${cls} on ${selector}` + ); }) .then(['"$selector" doesn\'t have the "$class" class'], function(selector, cls) { - assert.ok(!document.querySelector(selector).classList.contains(cls)); + assert.ok( + !document.querySelector(selector).classList.contains(cls), + `Expected [class] not to contain ${cls} on ${selector}` + ); }) .then('ok', function() { assert.ok(true); diff --git a/ui-v2/tests/unit/mixins/acl/with-actions-test.js b/ui-v2/tests/unit/mixins/acl/with-actions-test.js index d92d1c86f..ade74b60f 100644 --- a/ui-v2/tests/unit/mixins/acl/with-actions-test.js +++ b/ui-v2/tests/unit/mixins/acl/with-actions-test.js @@ -1,15 +1,25 @@ -import EmberObject from '@ember/object'; -import AclWithActionsMixin from 'consul-ui/mixins/acl/with-actions'; -import { moduleFor, test } from 'ember-qunit'; +import { moduleFor } from 'ember-qunit'; +import test from 'ember-sinon-qunit/test-support/test'; +import { getOwner } from '@ember/application'; +import Route from 'consul-ui/routes/dc/acls/index'; +import Service from '@ember/service'; + +import Mixin from 'consul-ui/mixins/acl/with-actions'; moduleFor('mixin:acl/with-actions', 'Unit | Mixin | acl/with actions', { // Specify the other units that are required for this test. - needs: ['mixin:with-feedback'], + needs: [ + 'mixin:with-blocking-actions', + 'service:feedback', + 'service:flashMessages', + 'service:logger', + 'service:settings', + 'service:acls', + ], subject: function() { - const AclWithActionsObject = EmberObject.extend(AclWithActionsMixin); - this.register('test-container:acl/with-actions-object', AclWithActionsObject); - // TODO: May need to actually get this from the container - return AclWithActionsObject; + const MixedIn = Route.extend(Mixin); + this.register('test-container:acl/with-actions-object', MixedIn); + return getOwner(this).lookup('test-container:acl/with-actions-object'); }, }); @@ -18,3 +28,64 @@ test('it works', function(assert) { const subject = this.subject(); assert.ok(subject); }); +test('use persists the token and calls transitionTo correctly', function(assert) { + assert.expect(4); + this.register( + 'service:feedback', + Service.extend({ + execute: function(cb, name) { + assert.equal(name, 'use'); + return cb(); + }, + }) + ); + const item = { ID: 'id' }; + this.register( + 'service:settings', + Service.extend({ + persist: function(actual) { + assert.equal(actual.token, item.ID); + return Promise.resolve(actual); + }, + }) + ); + const subject = this.subject(); + const expected = 'dc.services'; + const transitionTo = this.stub(subject, 'transitionTo').returnsArg(0); + return subject.actions.use + .bind(subject)(item) + .then(function(actual) { + assert.ok(transitionTo.calledOnce); + assert.equal(actual, expected); + }); +}); +test('clone clones the token and calls afterDelete correctly', function(assert) { + assert.expect(4); + this.register( + 'service:feedback', + Service.extend({ + execute: function(cb, name) { + assert.equal(name, 'clone'); + return cb(); + }, + }) + ); + const expected = { ID: 'id' }; + this.register( + 'service:acls', + Service.extend({ + clone: function(actual) { + assert.deepEqual(actual, expected); + return Promise.resolve(actual); + }, + }) + ); + const subject = this.subject(); + const afterDelete = this.stub(subject, 'afterDelete').returnsArg(0); + return subject.actions.clone + .bind(subject)(expected) + .then(function(actual) { + assert.ok(afterDelete.calledOnce); + assert.equal(actual, expected); + }); +}); diff --git a/ui-v2/tests/unit/mixins/intention/with-actions-test.js b/ui-v2/tests/unit/mixins/intention/with-actions-test.js index 30f86de97..a33881eda 100644 --- a/ui-v2/tests/unit/mixins/intention/with-actions-test.js +++ b/ui-v2/tests/unit/mixins/intention/with-actions-test.js @@ -1,15 +1,21 @@ -import EmberObject from '@ember/object'; -import IntentionWithActionsMixin from 'consul-ui/mixins/intention/with-actions'; -import { moduleFor, test } from 'ember-qunit'; +import { moduleFor } from 'ember-qunit'; +import test from 'ember-sinon-qunit/test-support/test'; +import { getOwner } from '@ember/application'; +import Route from '@ember/routing/route'; +import Mixin from 'consul-ui/mixins/intention/with-actions'; moduleFor('mixin:intention/with-actions', 'Unit | Mixin | intention/with actions', { // Specify the other units that are required for this test. - needs: ['service:feedback'], + needs: [ + 'mixin:with-blocking-actions', + 'service:feedback', + 'service:flashMessages', + 'service:logger', + ], subject: function() { - const IntentionWithActionsObject = EmberObject.extend(IntentionWithActionsMixin); - this.register('test-container:intention/with-actions-object', IntentionWithActionsObject); - // TODO: May need to actually get this from the container - return IntentionWithActionsObject; + const MixedIn = Route.extend(Mixin); + this.register('test-container:intention/with-actions-object', MixedIn); + return getOwner(this).lookup('test-container:intention/with-actions-object'); }, }); @@ -18,3 +24,17 @@ test('it works', function(assert) { const subject = this.subject(); assert.ok(subject); }); +test('errorCreate returns a different status code if a duplicate intention is found', function(assert) { + const subject = this.subject(); + const expected = 'exists'; + const actual = subject.errorCreate('error', { + errors: [{ status: '500', detail: 'duplicate intention found:' }], + }); + assert.equal(actual, expected); +}); +test('errorCreate returns the same code if there is no error', function(assert) { + const subject = this.subject(); + const expected = 'error'; + const actual = subject.errorCreate('error', {}); + assert.equal(actual, expected); +}); diff --git a/ui-v2/tests/unit/mixins/kv/with-actions-test.js b/ui-v2/tests/unit/mixins/kv/with-actions-test.js index eccc3a141..d42d3c0e8 100644 --- a/ui-v2/tests/unit/mixins/kv/with-actions-test.js +++ b/ui-v2/tests/unit/mixins/kv/with-actions-test.js @@ -1,15 +1,21 @@ -import EmberObject from '@ember/object'; -import KvWithActionsMixin from 'consul-ui/mixins/kv/with-actions'; -import { moduleFor, test } from 'ember-qunit'; +import { moduleFor, skip } from 'ember-qunit'; +import test from 'ember-sinon-qunit/test-support/test'; +import { getOwner } from '@ember/application'; +import Route from '@ember/routing/route'; +import Mixin from 'consul-ui/mixins/kv/with-actions'; moduleFor('mixin:kv/with-actions', 'Unit | Mixin | kv/with actions', { // Specify the other units that are required for this test. - needs: ['mixin:with-feedback'], + needs: [ + 'mixin:with-blocking-actions', + 'service:feedback', + 'service:flashMessages', + 'service:logger', + ], subject: function() { - const KvWithActionsObject = EmberObject.extend(KvWithActionsMixin); - this.register('test-container:kv/with-actions-object', KvWithActionsObject); - // TODO: May need to actually get this from the container - return KvWithActionsObject; + const MixedIn = Route.extend(Mixin); + this.register('test-container:kv/with-actions-object', MixedIn); + return getOwner(this).lookup('test-container:kv/with-actions-object'); }, }); @@ -18,3 +24,29 @@ test('it works', function(assert) { const subject = this.subject(); assert.ok(subject); }); +test('afterUpdate calls transitionTo index when the key is a single slash', function(assert) { + const subject = this.subject(); + const expected = 'dc.kv.index'; + const transitionTo = this.stub(subject, 'transitionTo').returnsArg(0); + const actual = subject.afterUpdate({}, { Key: '/' }); + assert.equal(actual, expected); + assert.ok(transitionTo.calledOnce); +}); +test('afterUpdate calls transitionTo folder when the key is not a single slash', function(assert) { + const subject = this.subject(); + const expected = 'dc.kv.folder'; + const transitionTo = this.stub(subject, 'transitionTo').returnsArg(0); + ['', '/key', 'key/name'].forEach(item => { + const actual = subject.afterUpdate({}, { Key: item }); + assert.equal(actual, expected); + }); + assert.ok(transitionTo.calledThrice); +}); +test('afterDelete calls refresh folder when the routeName is `folder`', function(assert) { + const subject = this.subject(); + subject.routeName = 'dc.kv.folder'; + const refresh = this.stub(subject, 'refresh'); + subject.afterDelete({}, {}); + assert.ok(refresh.calledOnce); +}); +skip('action invalidateSession test'); diff --git a/ui-v2/tests/unit/mixins/with-blocking-actions-test.js b/ui-v2/tests/unit/mixins/with-blocking-actions-test.js new file mode 100644 index 000000000..73ee2aa8e --- /dev/null +++ b/ui-v2/tests/unit/mixins/with-blocking-actions-test.js @@ -0,0 +1,74 @@ +import { moduleFor, skip } from 'ember-qunit'; +import test from 'ember-sinon-qunit/test-support/test'; +import { getOwner } from '@ember/application'; +import Route from '@ember/routing/route'; +import Mixin from 'consul-ui/mixins/with-blocking-actions'; + +moduleFor('mixin:with-blocking-actions', 'Unit | Mixin | with blocking actions', { + // Specify the other units that are required for this test. + needs: ['service:feedback', 'service:flashMessages', 'service:logger'], + subject: function() { + const MixedIn = Route.extend(Mixin); + this.register('test-container:with-blocking-actions-object', MixedIn); + return getOwner(this).lookup('test-container:with-blocking-actions-object'); + }, +}); + +// Replace this with your real tests. +test('it works', function(assert) { + const subject = this.subject(); + assert.ok(subject); +}); +skip('init sets up feedback properly'); +test('afterCreate just calls afterUpdate', function(assert) { + const subject = this.subject(); + const expected = [1, 2, 3, 4]; + const afterUpdate = this.stub(subject, 'afterUpdate').returns(expected); + const actual = subject.afterCreate(expected); + assert.deepEqual(actual, expected); + assert.ok(afterUpdate.calledOnce); +}); +test('afterUpdate calls transitionTo without the last part of the current route name', function(assert) { + const subject = this.subject(); + const expected = 'dc.kv'; + subject.routeName = expected + '.edit'; + const transitionTo = this.stub(subject, 'transitionTo').returnsArg(0); + const actual = subject.afterUpdate(); + assert.equal(actual, expected); + assert.ok(transitionTo.calledOnce); +}); +test('afterDelete calls transitionTo without the last part of the current route name if the last part is not `index`', function(assert) { + const subject = this.subject(); + const expected = 'dc.kv'; + subject.routeName = expected + '.edit'; + const transitionTo = this.stub(subject, 'transitionTo').returnsArg(0); + const actual = subject.afterDelete(); + assert.equal(actual, expected); + assert.ok(transitionTo.calledOnce); +}); +test('afterDelete calls refresh if the last part is `index`', function(assert) { + const subject = this.subject(); + subject.routeName = 'dc.kv.index'; + const expected = 'refresh'; + const refresh = this.stub(subject, 'refresh').returns(expected); + const actual = subject.afterDelete(); + assert.equal(actual, expected); + assert.ok(refresh.calledOnce); +}); +test('the error hooks return type', function(assert) { + const subject = this.subject(); + const expected = 'success'; + ['errorCreate', 'errorUpdate', 'errorDelete'].forEach(function(item) { + const actual = subject[item](expected, {}); + assert.equal(actual, expected); + }); +}); +test('action cancel just calls afterUpdate', function(assert) { + const subject = this.subject(); + const expected = [1, 2, 3, 4]; + const afterUpdate = this.stub(subject, 'afterUpdate').returns(expected); + // TODO: unsure as to whether ember testing should actually bind this for you? + const actual = subject.actions.cancel.bind(subject)(expected); + assert.deepEqual(actual, expected); + assert.ok(afterUpdate.calledOnce); +}); diff --git a/ui-v2/tests/unit/mixins/with-feedback-test.js b/ui-v2/tests/unit/mixins/with-feedback-test.js deleted file mode 100644 index 6e79cf62f..000000000 --- a/ui-v2/tests/unit/mixins/with-feedback-test.js +++ /dev/null @@ -1,20 +0,0 @@ -import EmberObject from '@ember/object'; -import WithFeedbackMixin from 'consul-ui/mixins/with-feedback'; -import { moduleFor, test } from 'ember-qunit'; - -moduleFor('mixin:with-feedback', 'Unit | Mixin | with feedback', { - // Specify the other units that are required for this test. - needs: ['service:feedback'], - subject: function() { - const WithFeedbackObject = EmberObject.extend(WithFeedbackMixin); - this.register('test-container:with-feedback-object', WithFeedbackObject); - // TODO: May need to actually get this from the container - return WithFeedbackObject; - }, -}); - -// Replace this with your real tests. -test('it works', function(assert) { - const subject = this.subject(); - assert.ok(subject); -}); diff --git a/ui-v2/yarn.lock b/ui-v2/yarn.lock index 5c91f370f..7b11ec92c 100644 --- a/ui-v2/yarn.lock +++ b/ui-v2/yarn.lock @@ -3360,6 +3360,13 @@ ember-cli-sri@^2.1.0: dependencies: broccoli-sri-hash "^2.1.0" +ember-cli-string-helpers@^1.9.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/ember-cli-string-helpers/-/ember-cli-string-helpers-1.9.0.tgz#2c1605bc5768ff58cecd2fa1bd0d13d81e47f3d3" + dependencies: + broccoli-funnel "^1.0.1" + ember-cli-babel "^6.6.0" + ember-cli-string-utils@^1.0.0, ember-cli-string-utils@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/ember-cli-string-utils/-/ember-cli-string-utils-1.1.0.tgz#39b677fc2805f55173735376fcef278eaa4452a1"