WIP: First draft intentions

1. Listing, filtering by action and searching by source name and
destination name
2. Edit/Create page, edits ping the API double fine, need to work through
creates and deletes
3. Currently uses a `Precedence` intention keyname that doesn't yet
exist in the real API
This commit is contained in:
John Cowen 2018-05-22 16:03:45 +01:00 committed by Jack Pearkes
parent c3e92a236f
commit b38e5df630
34 changed files with 768 additions and 2 deletions

View File

@ -0,0 +1,64 @@
import Adapter, { DATACENTER_KEY as API_DATACENTER_KEY } from './application';
import { FOREIGN_KEY as DATACENTER_KEY } from 'consul-ui/models/dc';
import { PRIMARY_KEY, SLUG_KEY } from 'consul-ui/models/intention';
import { OK as HTTP_OK } from 'consul-ui/utils/http/status';
import makeAttrable from 'consul-ui/utils/makeAttrable';
export default Adapter.extend({
urlForQuery: function(query, modelName) {
return this.appendURL('connect/intentions', [], this.cleanQuery(query));
},
urlForQueryRecord: function(query, modelName) {
return this.appendURL('connect/intentions', [query.id], this.cleanQuery(query));
},
urlForCreateRecord: function(modelName, snapshot) {
return this.appendURL('connect/intentions', [], {
[API_DATACENTER_KEY]: snapshot.attr(DATACENTER_KEY),
});
},
urlForUpdateRecord: function(id, modelName, snapshot) {
return this.appendURL('connect/intentions', [snapshot.attr(SLUG_KEY)], {
[API_DATACENTER_KEY]: snapshot.attr(DATACENTER_KEY),
});
},
urlForDeleteRecord: function(id, modelName, snapshot) {
return this.appendURL('connect/intentions', [snapshot.attr(SLUG_KEY)], {
[API_DATACENTER_KEY]: snapshot.attr(DATACENTER_KEY),
});
},
isUpdateRecord: function(url) {
return (
url.pathname ===
this.parseURL(
this.urlForUpdateRecord(null, 'intention', makeAttrable({ [DATACENTER_KEY]: '' }))
).pathname
);
},
handleResponse: function(status, headers, payload, requestData) {
let response = payload;
if (status === HTTP_OK) {
const url = this.parseURL(requestData.url);
switch (true) {
case this.isQueryRecord(url):
case this.isUpdateRecord(url):
// case this.isCreateRecord(url):
response = {
...response,
...{
[PRIMARY_KEY]: this.uidForURL(url),
},
};
break;
default:
response = response.map((item, i, arr) => {
return {
...item,
...{
[PRIMARY_KEY]: this.uidForURL(url, item[SLUG_KEY]),
},
};
});
}
}
return this._super(status, headers, response, requestData);
},
});

View File

@ -0,0 +1,8 @@
import Component from '@ember/component';
export default Component.extend({
tagName: 'form',
classNames: ['filter-bar'],
'data-test-intention-filter': true,
onchange: function() {},
});

View File

@ -0,0 +1,2 @@
import Controller from './edit';
export default Controller.extend();

View File

@ -0,0 +1,30 @@
import Controller from '@ember/controller';
import { set } from '@ember/object';
// import Changeset from 'ember-changeset';
// import validations from 'consul-ui/validations/acl';
// import lookupValidator from 'ember-changeset-validations';
export default Controller.extend({
setProperties: function(model) {
this.changeset = model.item; //new Changeset(model.item, lookupValidator(validations), validations);
this._super({
...model,
...{
item: this.changeset,
},
});
},
actions: {
change: function(e) {
const target = e.target;
switch (target.name) {
case 'SourceType':
set(this.changeset, target.name, target.value);
break;
case 'Action':
set(this.changeset, target.name, target.value);
break;
}
},
},
});

View File

@ -0,0 +1,45 @@
import Controller from '@ember/controller';
import { computed, get } from '@ember/object';
import WithFiltering from 'consul-ui/mixins/with-filtering';
import ucfirst from 'consul-ui/utils/ucfirst';
import numeral from 'numeral';
// TODO: DRY out in acls at least
const createCounter = function(prop) {
return function(items, val) {
return val === '' ? get(items, 'length') : items.filterBy(prop, val).length;
};
};
const countAction = createCounter('Action');
export default Controller.extend(WithFiltering, {
queryParams: {
action: {
as: 'action',
},
s: {
as: 'filter',
replace: true,
},
},
actionFilters: computed('items', function() {
const items = get(this, 'items');
return ['', 'allow', 'deny'].map(function(item) {
return {
label: `${item === '' ? 'All' : ucfirst(item)} (${numeral(
countAction(items, item)
).format()})`,
value: item,
};
});
}),
filter: function(item, { s = '', action = '' }) {
return (
(get(item, 'SourceName')
.toLowerCase()
.indexOf(s.toLowerCase()) !== -1 ||
get(item, 'DestinationName')
.toLowerCase()
.indexOf(s.toLowerCase()) !== -1) &&
(action === '' || get(item, 'Action') === action)
);
},
});

View File

@ -0,0 +1,59 @@
import Mixin from '@ember/object/mixin';
import { get } from '@ember/object';
import WithFeedback from 'consul-ui/mixins/with-feedback';
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.`,
`There was an error adding your intention.`
);
},
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');
},
},
});

View File

@ -0,0 +1,25 @@
import Model from 'ember-data/model';
import attr from 'ember-data/attr';
export const PRIMARY_KEY = 'uid';
export const SLUG_KEY = 'ID';
export default Model.extend({
[PRIMARY_KEY]: attr('string'),
[SLUG_KEY]: attr('string'),
Description: attr('string'),
SourceNS: attr('string'),
SourceName: attr('string'),
DestinationName: attr('string'),
Precedence: attr('number'),
SourceType: attr('string'),
Action: attr('string'),
DefaultAddr: attr('string'),
DefaultPort: attr('number'),
Meta: attr(),
Datacenter: attr('string'),
CreatedAt: attr('date'),
UpdatedAt: attr('date'),
CreateIndex: attr('number'),
ModifyIndex: attr('number'),
});

View File

@ -19,6 +19,11 @@ Router.map(function() {
// Show an individual node
this.route('show', { path: '/:name' });
});
// Intentions represent a consul intention
this.route('intentions', { path: '/intentions' }, function() {
this.route('edit', { path: '/:id' });
this.route('create', { path: '/create' });
});
// Key/Value
this.route('kv', { path: '/kv' }, function() {
this.route('folder', { path: '/*key' });

View File

@ -0,0 +1,33 @@
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
import { hash } from 'rsvp';
import { get, set } from '@ember/object';
import WithIntentionActions from 'consul-ui/mixins/intention/with-actions';
export default Route.extend(WithIntentionActions, {
templateName: 'dc/intentions/edit',
repo: service('intentions'),
beforeModel: function() {
get(this, 'repo').invalidate();
},
model: function(params) {
this.item = get(this, 'repo').create();
set(this.item, 'Datacenter', this.modelFor('dc').dc.Name);
return hash({
create: true,
isLoading: false,
item: this.item,
types: ['consul', 'externaluri'],
intents: ['allow', 'deny'],
});
},
setupController: function(controller, model) {
this._super(...arguments);
controller.setProperties(model);
},
deactivate: function() {
if (get(this.item, 'isNew')) {
this.item.destroyRecord();
}
},
});

View File

@ -0,0 +1,22 @@
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
import { hash } from 'rsvp';
import { get } from '@ember/object';
import WithAclActions from 'consul-ui/mixins/intention/with-actions';
export default Route.extend(WithAclActions, {
repo: service('intentions'),
model: function(params) {
return hash({
isLoading: false,
item: get(this, 'repo').findBySlug(params.id, this.modelFor('dc').dc.Name),
types: ['consul', 'externaluri'],
intents: ['allow', 'deny'],
});
},
setupController: function(controller, model) {
this._super(...arguments);
controller.setProperties(model);
},
});

View File

@ -0,0 +1,23 @@
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
import { hash } from 'rsvp';
import { get } from '@ember/object';
export default Route.extend({
repo: service('intentions'),
queryParams: {
s: {
as: 'filter',
replace: true,
},
},
model: function(params) {
return hash({
items: get(this, 'repo').findAllByDatacenter(this.modelFor('dc').dc.Name),
});
},
setupController: function(controller, model) {
this._super(...arguments);
controller.setProperties(model);
},
});

View File

@ -0,0 +1,6 @@
import Serializer from './application';
import { PRIMARY_KEY } from 'consul-ui/models/intention';
export default Serializer.extend({
primaryKey: PRIMARY_KEY,
});

View File

@ -0,0 +1,48 @@
import Service, { inject as service } from '@ember/service';
import { get, set } from '@ember/object';
import { typeOf } from '@ember/utils';
import { PRIMARY_KEY } from 'consul-ui/models/intention';
export default Service.extend({
store: service('store'),
findAllByDatacenter: function(dc) {
return get(this, 'store')
.query('intention', { dc: dc })
.then(function(items) {
return items.forEach(function(item, i, arr) {
set(item, 'Datacenter', dc);
});
});
},
findBySlug: function(slug, dc) {
return get(this, 'store')
.queryRecord('intention', {
id: slug,
dc: dc,
})
.then(function(item) {
set(item, 'Datacenter', dc);
return item;
});
},
create: function() {
return get(this, 'store').createRecord('intention');
},
persist: function(item) {
return item.save();
},
remove: function(obj) {
let item = obj;
if (typeof obj.destroyRecord === 'undefined') {
item = obj.get('data');
}
if (typeOf(item) === 'object') {
item = get(this, 'store').peekRecord('intention', item[PRIMARY_KEY]);
}
return item.destroyRecord().then(item => {
return get(this, 'store').unloadRecord(item);
});
},
invalidate: function() {
return get(this, 'store').unloadAll('intention');
},
});

View File

@ -45,6 +45,7 @@
@import 'components/notice';
@import 'routes/dc/service/index';
@import 'routes/dc/intention/index';
@import 'routes/dc/kv/index';
main a {

View File

@ -25,12 +25,13 @@
pointer-events: none;
}
%with-folder {
position: relative;
text-indent: 30px;
}
%with-hashicorp,
%with-folder,
%with-chevron,
%with-clipboard {
%with-clipboard,
%with-right-arrow {
position: relative;
}
%with-chevron {
@ -142,6 +143,26 @@
@extend %pseudo-icon;
background-image: url('data:image/svg+xml;charset=UTF-8,<svg width="14" height="14" xmlns="http://www.w3.org/2000/svg"><path d="M13.645 10.092c.24.409.365.88.365 1.37 0 1.392-1.027 2.527-2.294 2.538H2.322c-.824 0-1.592-.487-2.004-1.27a2.761 2.761 0 0 1 0-2.538l4.686-8.904C5.416.505 6.184.018 7.008.018c.824 0 1.592.487 2.004 1.27l4.633 8.804zm-5.989 1.264V9.607H6.344v1.749h1.312zm0-3.048v-4.37H6.344v4.37h1.312z" fill="%23949daa"/></svg>');
}
%with-right-arrow-green {
@extend %pseudo-icon;
background-image: url('data:image/svg+xml;charset=UTF-8,<svg width="16" height="14" xmlns="http://www.w3.org/2000/svg"><path d="M9.263.5L8.084 1.637l4.716 4.55H0v1.625h12.8l-4.716 4.55 1.18 1.138L16 7z" fill="%232EB039"/></svg>');
width: 16px;
height: 14px;
background-color: transparent;
}
%with-deny-icon {
@extend %pseudo-icon;
background-image: url('data:image/svg+xml;charset=UTF-8,<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><path fill="#282C2E" d="M8.79 4l-.737.71L11 7.556H3V8.57h8l-2.947 2.844.736.711L13 8.062z"/><rect stroke="#C73445" stroke-width="1.5" x=".75" y=".75" width="14.5" height="14.5" rx="7.25"/><path d="M3.5 3.5l9 9" stroke="#C73445" stroke-width="1.5" stroke-linecap="square"/></g></svg>');
width: 16px;
height: 16px;
background-color: transparent;
}
%with-deny::before {
@extend %with-deny-icon;
}
%with-allow::before {
@extend %with-right-arrow-green;
}
%with-passing::before {
@extend %with-tick;
border-radius: 100%;

View File

@ -19,6 +19,9 @@ table tr > * {
html.template-service.template-list main table tr {
@extend %services-row;
}
html.template-intention.template-list main table tr {
@extend %intentions-row;
}
html.template-kv.template-list main table tr {
@extend %kvs-row;
}
@ -65,6 +68,12 @@ html.template-node.template-show main table.sessions tr {
tr > * dl {
float: left;
}
%intentions-row > * {
width: calc(25% - 60px);
}
%intentions-row > *:last-child {
width: 60px;
}
%kvs-row > *:first-child {
width: calc(100% - 60px);
}

View File

@ -0,0 +1,8 @@
td.intent-allow strong {
@extend %with-allow;
visibility: hidden;
}
td.intent-deny strong {
@extend %with-deny;
visibility: hidden;
}

View File

@ -27,6 +27,9 @@
<li data-test-main-nav-nodes class={{if (is-href 'dc.nodes' dc.Name) 'is-active'}}>
<a href={{href-to 'dc.nodes' dc.Name}}>Nodes</a>
</li>
<li data-test-main-nav-intentions class={{if (is-href 'dc.intentions' dc.Name) 'is-active'}}>
<a href={{href-to 'dc.intentions' dc.Name}}>Intentions</a>
</li>
<li data-test-main-nav-kvs class={{if (is-href 'dc.kv' dc.Name) 'is-active'}}>
<a href={{href-to 'dc.kv' dc.Name}}>Key/Value</a>
</li>

View File

@ -0,0 +1,4 @@
{{!<form>}}
{{freetext-filter onchange=(action onchange) value=search placeholder="Search by Source or Destination"}}
{{radio-group name="action" value=action items=filters onchange=(action onchange)}}
{{!</form>}}

View File

@ -0,0 +1,58 @@
<form>
<fieldset>
<label class="type-text{{if item.error.SourceName ' has-error'}}">
<span>Source Service</span>
{{input value=item.SourceName name='source' autofocus='autofocus'}}
<em>Choose a Consul Service, write in a future Consul Service, or write any Service URL</em>
</label>
<div role="radiogroup" class={{if item.error.Type ' has-error'}}>
{{#each types as |type|}}
<label>
<span>{{type}}</span>
<input type="radio" name="SourceType" value="{{type}}" checked={{if (eq item.SourceType type) 'checked'}} onchange={{ action 'change' }}/>
</label>
{{/each}}
</div>
<label class="type-text{{if item.error.DestinationName ' has-error'}}">
<span>Destination Service</span>
{{input value=item.DestinationName name='name'}}
<em>Choose a Consul Service, write in a future Consul Service, or write any Service URL</em>
</label>
<div role="radiogroup" class={{if item.error.Action ' has-error'}}>
{{#each itents as |intent|}}
<label>
<span>{{intent}}</span>
<input type="radio" name="Action" value="{{intent}}" checked={{if (eq item.Action intent) 'checked'}} onchange={{ action 'change' }}/>
</label>
{{/each}}
</div>
<label class="type-text{{if item.error.Description ' has-error'}}">
<span>Description</span>
{{input value=item.Description name='description' placeholder="Description"}}
<em>Choose a Consul Service, write in a future Consul Service, or write any Service URL</em>
</label>
</fieldset>
<div>
{{#if create }}
<button type="submit" {{ action "create" item}} disabled={{if item.isInvalid 'disabled'}}>Save</button>
{{ else }}
<button type="submit" {{ action "update" item}} disabled={{if item.isInvalid 'disabled'}}>Save</button>
{{/if}}
<button type="reset" {{ action "cancel" item}}>Cancel</button>
{{# if (and item.ID (not-eq item.ID 'anonymous')) }}
{{#confirmation-dialog message='Are you sure you want to delete this Intention?'}}
{{#block-slot 'action' as |confirm|}}
<button type="button" class="type-delete" {{action confirm 'delete' item parent}}>Delete</button>
{{/block-slot}}
{{#block-slot 'dialog' as |execute cancel message|}}
<p>
{{message}}
</p>
<button type="button" class="type-delete" {{action execute}}>Confirm Delete</button>
<button type="button" class="type-cancel" {{action cancel}}>Cancel</button>
{{/block-slot}}
{{/confirmation-dialog}}
{{/if}}
</div>
</form>

View File

@ -0,0 +1,40 @@
{{#app-view class="acl edit" loading=isLoading}}
{{#block-slot 'breadcrumbs'}}
<ol>
<li><a href={{href-to 'dc.intentions'}}>All Intentions</a></li>
</ol>
{{/block-slot}}
{{#block-slot 'header'}}
<h1>
{{#if item.ID }}
Edit Intention
{{else}}
New Intention
{{/if}}
</h1>
{{/block-slot}}
{{#block-slot 'actions'}}
{{#if (not create) }}
{{#feedback-dialog type='inline'}}
{{#block-slot 'action' as |success error|}}
{{#copy-button success=(action success) error=(action error) clipboardText=item.ID title='copy UUID to clipboard'}}
Copy UUID
{{/copy-button}}
{{/block-slot}}
{{#block-slot 'success'}}
<p>
Copied UUID!
</p>
{{/block-slot}}
{{#block-slot 'error'}}
<p>
Sorry, something went wrong!
</p>
{{/block-slot}}
{{/feedback-dialog}}
{{/if}}
{{/block-slot}}
{{#block-slot 'content'}}
{{ partial 'dc/intentions/form'}}
{{/block-slot}}
{{/app-view}}

View File

@ -0,0 +1,72 @@
{{#app-view class="intention list"}}
{{#block-slot 'header'}}
<h1>
Intentions
</h1>
{{/block-slot}}
{{#block-slot 'actions'}}
<a href="{{href-to 'dc.intentions.create'}}" class="type-create">Create</a>
{{/block-slot}}
{{#block-slot 'toolbar'}}
{{#if (gt items.length 0) }}
{{intention-filter filters=actionFilters search=filters.s type=filters.action onchange=(action 'filter')}}
{{/if}}
{{/block-slot}}
{{#block-slot 'content'}}
{{#if (gt filtered.length 0) }}
{{#tabular-collection
route='dc.intentions.edit'
key='SourceName'
items=filtered as |item index|
}}
{{#block-slot 'header'}}
<th>Source</th>
<th>&nbsp;</th>
<th>Destination</th>
<th>Precedence</th>
{{/block-slot}}
{{#block-slot 'row'}}
<td data-test-intention="{{item.ID}}">
<a href={{href-to 'dc.intentions.edit' item.ID}}>{{item.SourceName}}</a>
</td>
<td class="intent-{{item.Action}}">
<strong>{{item.Action}}</strong>
</td>
<td>
{{item.DestinationName}}
</td>
<td>
{{item.Precedence}}
</td>
{{/block-slot}}
{{#block-slot 'actions' as |index change checked|}}
{{#confirmation-dialog confirming=false index=index message='Are you sure you want to delete this intention?'}}
{{#block-slot 'action' as |confirm|}}
{{#action-group index=index onchange=(action change) checked=(if (eq checked index) 'checked')}}
<ul>
<li>
<a href={{href-to 'dc.intentions.edit' item.ID}}>Edit</a>
</li>
<li>
<a onclick={{action confirm 'delete' item}}>Delete</a>
</li>
</ul>
{{/action-group}}
{{/block-slot}}
{{#block-slot 'dialog' as |execute cancel message|}}
<p>
{{message}}
</p>
<button type="button" class="type-delete" {{action execute}}>Confirm Delete</button>
<button type="button" class="type-cancel" {{action cancel}}>Cancel</button>
{{/block-slot}}
{{/confirmation-dialog}}
{{/block-slot}}
{{/tabular-collection}}
{{else}}
<p>
There are no intentions.
</p>
{{/if}}
{{/block-slot}}
{{/app-view}}

View File

@ -0,0 +1,29 @@
import { moduleForComponent, test } from 'ember-qunit';
import hbs from 'htmlbars-inline-precompile';
moduleForComponent('intention-filter', 'Integration | Component | intention filter', {
integration: true,
});
test('it renders', function(assert) {
// Set any properties with this.set('myProperty', 'value');
// Handle any actions with this.on('myAction', function(val) { ... });
this.render(hbs`{{intention-filter}}`);
assert.equal(
this.$()
.text()
.trim(),
'Search'
);
// // Template block usage:
// this.render(hbs`
// {{#intention-filter}}
// template block text
// {{/intention-filter}}
// `);
// assert.equal(this.$().text().trim(), 'template block text');
});

View File

@ -0,0 +1,12 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
module('Unit | Adapter | intention', function(hooks) {
setupTest(hooks);
// Replace this with your real tests.
test('it exists', function(assert) {
let adapter = this.owner.lookup('adapter:intention');
assert.ok(adapter);
});
});

View File

@ -0,0 +1,12 @@
import { moduleFor, test } from 'ember-qunit';
moduleFor('controller:dc/intentions/create', 'Unit | Controller | dc/intentions/create', {
// Specify the other units that are required for this test.
// needs: ['controller:foo']
});
// Replace this with your real tests.
test('it exists', function(assert) {
let controller = this.subject();
assert.ok(controller);
});

View File

@ -0,0 +1,12 @@
import { moduleFor, test } from 'ember-qunit';
moduleFor('controller:dc/intentions/edit', 'Unit | Controller | dc/intentions/edit', {
// Specify the other units that are required for this test.
// needs: ['controller:foo']
});
// Replace this with your real tests.
test('it exists', function(assert) {
let controller = this.subject();
assert.ok(controller);
});

View File

@ -0,0 +1,12 @@
import { moduleFor, test } from 'ember-qunit';
moduleFor('controller:dc/intentions/index', 'Unit | Controller | dc/intentions/index', {
// Specify the other units that are required for this test.
// needs: ['controller:foo']
});
// Replace this with your real tests.
test('it exists', function(assert) {
let controller = this.subject();
assert.ok(controller);
});

View File

@ -0,0 +1,20 @@
import EmberObject from '@ember/object';
import IntentionWithActionsMixin from 'consul-ui/mixins/intention/with-actions';
import { moduleFor, test } from 'ember-qunit';
moduleFor('mixin:intention/with-actions', 'Unit | Mixin | intention/with actions', {
// Specify the other units that are required for this test.
needs: ['service:feedback'],
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;
},
});
// Replace this with your real tests.
test('it works', function(assert) {
const subject = this.subject();
assert.ok(subject);
});

View File

@ -0,0 +1,14 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
import { run } from '@ember/runloop';
module('Unit | Model | intention', function(hooks) {
setupTest(hooks);
// Replace this with your real tests.
test('it exists', function(assert) {
let store = this.owner.lookup('service:store');
let model = run(() => store.createRecord('intention', {}));
assert.ok(model);
});
});

View File

@ -0,0 +1,11 @@
import { moduleFor, test } from 'ember-qunit';
moduleFor('route:dc/intentions/create', 'Unit | Route | dc/intentions/create', {
// Specify the other units that are required for this test.
needs: ['service:intentions', 'service:feedback', 'service:flashMessages'],
});
test('it exists', function(assert) {
let route = this.subject();
assert.ok(route);
});

View File

@ -0,0 +1,11 @@
import { moduleFor, test } from 'ember-qunit';
moduleFor('route:dc/intentions/edit', 'Unit | Route | dc/intentions/edit', {
// Specify the other units that are required for this test.
needs: ['service:intentions', 'service:feedback', 'service:flashMessages'],
});
test('it exists', function(assert) {
let route = this.subject();
assert.ok(route);
});

View File

@ -0,0 +1,11 @@
import { moduleFor, test } from 'ember-qunit';
moduleFor('route:dc/intentions/index', 'Unit | Route | dc/intentions/index', {
// Specify the other units that are required for this test.
needs: ['service:intentions', 'service:feedback', 'service:flashMessages'],
});
test('it exists', function(assert) {
let route = this.subject();
assert.ok(route);
});

View File

@ -0,0 +1,24 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
import { run } from '@ember/runloop';
module('Unit | Serializer | intention', function(hooks) {
setupTest(hooks);
// Replace this with your real tests.
test('it exists', function(assert) {
let store = this.owner.lookup('service:store');
let serializer = store.serializerFor('intention');
assert.ok(serializer);
});
test('it serializes records', function(assert) {
let store = this.owner.lookup('service:store');
let record = run(() => store.createRecord('intention', {}));
let serializedRecord = record.serialize();
assert.ok(serializedRecord);
});
});

View File

@ -0,0 +1,12 @@
import { moduleFor, test } from 'ember-qunit';
moduleFor('service:intentions', 'Unit | Service | intentions', {
// Specify the other units that are required for this test.
// needs: ['service:foo']
});
// Replace this with your real tests.
test('it exists', function(assert) {
let service = this.subject();
assert.ok(service);
});