mirror of https://github.com/status-im/consul.git
* Move notification texts to a slightly different layer (#4572) * Further Simplify/refactor the actions/notification layer (#4573) 1. Move the 'with-feedback' actions to a 'with-blocking-action' mixin which better describes what it does 2. Additional set of unit tests almost over the entire layer to prove things work/add confidence for further changes The multiple 'with-action' mixins used for every 'index/edit' combo are now reduced down to only contain the functionality related to their specific routes, i.e. where to redirect. The actual functionality to block and carry out the action and then notify are 'almost' split out so that their respective classes/objects do one thing and one thing 'well'. Mixins are chosen for the moment as the decoration approach used by mixins feels better than multiple levels of inheritence, but I would like to take this fuether in the future to a 'compositional' based approach. There is still possible further work to be done here, but I'm a lot happier now this is reduced down into separate parts.
This commit is contained in:
parent
d59331f115
commit
40e71f1b91
|
@ -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');
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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');
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
|
@ -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]);
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
|
@ -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');
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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() {},
|
||||
});
|
||||
|
|
|
@ -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(),
|
||||
});
|
||||
}
|
||||
})
|
||||
|
|
|
@ -4,7 +4,8 @@
|
|||
{{#each flashMessages.queue as |flash|}}
|
||||
{{#flash-message flash=flash as |component flash|}}
|
||||
{{! flashes automatically ucfirst the type }}
|
||||
<p class={{if (eq component.flashType 'Success') 'success' 'error'}}><strong>{{if (eq component.flashType 'Success') 'Success!' 'Error!'}}</strong> {{flash.message}}</p>
|
||||
|
||||
<p data-notification class="{{if (eq component.flashType 'Success') 'success' 'error'}} notification-{{lowercase flash.action}}"><strong>{{if (eq component.flashType 'Success') 'Success!' 'Error!'}}</strong> {{#yield-slot 'notification' (block-params (lowercase component.flashType) (lowercase flash.action) )}}{{yield}}{{/yield-slot}}</p>
|
||||
{{/flash-message}}
|
||||
{{/each}}
|
||||
<div>
|
||||
|
|
|
@ -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}}
|
||||
|
|
@ -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'}}
|
||||
<ol>
|
||||
<li><a data-test-back href={{href-to 'dc.acls'}}>All Tokens</a></li>
|
||||
|
|
|
@ -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'}}
|
||||
<h1>
|
||||
ACL Tokens
|
||||
|
|
|
@ -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'}}
|
||||
<ol>
|
||||
<li><a data-test-back href={{href-to 'dc.intentions'}}>All Intentions</a></li>
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
{{#app-view class="intention list"}}
|
||||
{{#block-slot 'notification' as |status type|}}
|
||||
{{partial 'dc/intentions/notifications'}}
|
||||
{{/block-slot}}
|
||||
{{#block-slot 'header'}}
|
||||
<h1>
|
||||
Intentions
|
||||
|
|
|
@ -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}}
|
||||
|
|
@ -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}}
|
||||
|
|
@ -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'}}
|
||||
<ol>
|
||||
<li><a data-test-back href={{href-to 'dc.kv.index'}}>Key / Values</a></li>
|
||||
|
@ -24,7 +27,7 @@
|
|||
{{/if}}
|
||||
{{partial 'dc/kv/form'}}
|
||||
{{#if session}}
|
||||
<div>
|
||||
<div data-test-session>
|
||||
<h2><a href="{{env 'CONSUL_DOCUMENTATION_URL'}}/internals/sessions.html#session-design" rel="help noopener noreferrer" target="_blank">Lock Session</a></h2>
|
||||
<dl>
|
||||
<dt>Name</dt>
|
||||
|
@ -54,7 +57,7 @@
|
|||
</dl>
|
||||
{{#confirmation-dialog message='Are you sure you want to invalidate this session?'}}
|
||||
{{#block-slot 'action' as |confirm|}}
|
||||
<button type="button" class="type-delete" {{ action confirm "invalidateSession" session }}>Invalidate Session</button>
|
||||
<button type="button" data-test-delete class="type-delete" {{ action confirm "invalidateSession" session }}>Invalidate Session</button>
|
||||
{{/block-slot}}
|
||||
{{#block-slot 'dialog' as |execute cancel message|}}
|
||||
<p>
|
||||
|
|
|
@ -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'}}
|
||||
<ol>
|
||||
{{#if (not-eq parent.Key '/') }}
|
||||
|
|
|
@ -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}}
|
||||
|
|
@ -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'}}
|
||||
<ol>
|
||||
<li><a data-test-back href={{href-to 'dc.nodes'}}>All Nodes</a></li>
|
||||
|
|
|
@ -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'}}
|
||||
<h1>
|
||||
Services
|
||||
|
|
|
@ -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'}}
|
||||
<h1>
|
||||
Settings
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 |
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
}
|
|
@ -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'),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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]'),
|
||||
})
|
||||
)
|
||||
);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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);
|
||||
});
|
|
@ -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);
|
||||
});
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue