Merge pull request #4219 from hashicorp/feature/ui-v2-2

UI integration branch merge
This commit is contained in:
John Cowen 2018-06-13 19:07:46 +01:00 committed by GitHub
commit 853a40b256
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
91 changed files with 2165 additions and 75 deletions

2
ui-v2/.gitignore vendored
View File

@ -8,3 +8,5 @@
/yarn-error.log
/testem.log
/public/consul-api-double

View File

@ -13,3 +13,18 @@ format:
yarn run format:js
.PHONY: server dist lint format
.DEFAULT_GOAL=all
.PHONY: deps test all build start
all: deps
deps: node_modules yarn.lock package.json
node_modules:
yarn
build:
yarn run build
start:
yarn run start
test:
yarn run test
test-view:
yarn run test:view

View File

@ -1,25 +1,67 @@
import Component from 'ember-collection/components/ember-collection';
import needsRevalidate from 'ember-collection/utils/needs-revalidate';
import identity from 'ember-collection/utils/identity';
import Grid from 'ember-collection/layouts/grid';
import SlotsMixin from 'ember-block-slots';
import style from 'ember-computed-style';
import qsaFactory from 'consul-ui/utils/qsa-factory';
import { computed, get, set } from '@ember/object';
/**
* Heavily extended `ember-collection` component
* This adds support for z-index calculations to enable
* Popup menus to pop over either rows above or below
* the popup.
* Additionally adds calculations for figuring out what the height
* of the tabular component should be depending on the other elements
* in the page.
* Currently everything is here together for clarity, but to be split up
* in the future
*/
const $$ = document.querySelectorAll.bind(document);
// ember doesn't like you using `$` hence `$$`
const $$ = qsaFactory();
// basic pseudo CustomEvent interface
// TODO: use actual custom events once I've reminded
// myself re: support/polyfills
const createSizeEvent = function(detail) {
return {
detail: { width: window.innerWidth, height: window.innerHeight },
};
};
// need to copy Cell in wholesale as there is no way to import it
// there is no change made to `Cell` here, its only here as its
// private in `ember-collection`
// TODO: separate both Cell and ZIndexedGrid out
class Cell {
constructor(key, item, index, style) {
this.key = key;
this.hidden = false;
this.item = item;
this.index = index;
this.style = style;
}
}
// this is an amount of rows in the table NOT items
// unlikely to have 10000 DOM rows ever :)
const maxZIndex = 10000;
// Adds z-index styling to the default Grid
class ZIndexedGrid extends Grid {
formatItemStyle(index, w, h) {
let style = super.formatItemStyle(...arguments);
style += 'z-index: ' + (10000 - index);
formatItemStyle(index, w, h, checked) {
let style = super.formatItemStyle(index, w, h);
// count backwards from maxZIndex
let zIndex = maxZIndex - index;
// apart from the row that contains an opened dropdown menu
// this one should be highest z-index, so use max plus 1
if (checked == index) {
zIndex = maxZIndex + 1;
}
style += 'z-index: ' + zIndex;
return style;
}
}
// TODO instead of degrading gracefully
// basic DOM closest utility to cope with no support
// TODO: instead of degrading gracefully
// add a while polyfill for closest
const closest = function(sel, el) {
try {
@ -28,6 +70,32 @@ const closest = function(sel, el) {
return;
}
};
const sibling = function(el, name) {
let sibling = el;
while ((sibling = sibling.nextSibling)) {
if (sibling.nodeType === 1) {
if (sibling.nodeName.toLowerCase() === name) {
return sibling;
}
}
}
};
/**
* The tabular-collection can contain 'actions' the UI for which
* uses dropdown 'action groups', so a group of different actions.
* State makes use of native HTML state using radiogroups
* to ensure that only a single dropdown can be open at one time.
* Therefore we listen to change events to do anything extra when
* a dropdown is opened (the change function is bound to the instance of
* the `tabular-component` on init, hoisted here for visibility)
*
* The extra functionality we have here is to detect whether the opened
* dropdown menu would be cut off or not if it 'dropped down'.
* If it would be cut off we use CSS to 'drop it up' instead.
* We also set this row to have the max z-index here, and mark this
* row as the 'checked row' for when a scroll/grid re-calculation is
* performed
*/
const change = function(e) {
if (e instanceof MouseEvent) {
return;
@ -37,6 +105,23 @@ const change = function(e) {
const value = e.currentTarget.value;
if (value != get(this, 'checked')) {
set(this, 'checked', value);
// 'actions_close' would mean that all menus have been closed
// therefore we don't need to calculate
if (e.currentTarget.getAttribute('id') !== 'actions_close') {
const $tr = closest('tr', e.currentTarget);
const $group = sibling(e.currentTarget, 'ul');
const $footer = [...$$('footer[role="contentinfo"]')][0];
const groupRect = $group.getBoundingClientRect();
const footerRect = $footer.getBoundingClientRect();
const groupBottom = groupRect.top + $group.clientHeight;
const footerTop = footerRect.top;
if (groupBottom > footerTop) {
$group.classList.add('above');
} else {
$group.classList.remove('above');
}
$tr.style.zIndex = maxZIndex + 1;
}
} else {
set(this, 'checked', null);
}
@ -62,7 +147,7 @@ export default Component.extend(SlotsMixin, {
this._super(...arguments);
this.change = change.bind(this);
this.confirming = [];
// TODO: This should auto calculate properly from the CSS
// TODO: The row height should auto calculate properly from the CSS
this['cell-layout'] = new ZIndexedGrid(get(this, 'width'), 50);
this.handler = () => {
this.resize(createSizeEvent());
@ -79,23 +164,34 @@ export default Component.extend(SlotsMixin, {
},
didInsertElement: function() {
this._super(...arguments);
// TODO: Consider moving all DOM lookups here
// this seems to be the earliest place I can get them
window.addEventListener('resize', this.handler);
this.handler();
this.didAppear();
},
willDestroyElement: function() {
window.removeEventListener('resize', this.handler);
},
didAppear: function() {
this.handler();
},
resize: function(e) {
const $footer = [...$$('#wrapper > footer')][0];
const $thead = [...$$('main > div')][0];
if ($thead) {
// TODO: This should auto calculate properly from the CSS
this.set('height', Math.max(0, new Number(e.detail.height - ($footer.clientHeight + 218))));
this['cell-layout'] = new ZIndexedGrid($thead.clientWidth, 50);
const $tbody = [...$$('tbody', this.element)][0];
const $appContent = [...$$('main > div')][0];
if ($appContent) {
const rect = $tbody.getBoundingClientRect();
const $footer = [...$$('footer[role="contentinfo"]')][0];
const space = rect.top + $footer.clientHeight;
const height = new Number(e.detail.height - space);
this.set('height', Math.max(0, height));
// TODO: The row height should auto calculate properly from the CSS
this['cell-layout'] = new ZIndexedGrid($appContent.clientWidth, 50);
this.updateItems();
this.updateScrollPosition();
}
},
// `ember-collection` bug workaround
// https://github.com/emberjs/ember-collection/issues/138
_needsRevalidate: function() {
if (this.isDestroyed || this.isDestroying) {
return;
@ -106,8 +202,115 @@ export default Component.extend(SlotsMixin, {
needsRevalidate(this);
}
},
// need to overwrite this completely so I can pass through the checked index
// unfortunately the nicest way I could think to do this is to copy this in wholesale
// to add an extra argument for `formatItemStyle` in 3 places
// tradeoff between changing as little code as possible in the original code
updateCells: function() {
if (!this._items) {
return;
}
const numItems = get(this._items, 'length');
if (this._cellLayout.length !== numItems) {
this._cellLayout.length = numItems;
}
var priorMap = this._cellMap;
var cellMap = Object.create(null);
var index = this._cellLayout.indexAt(
this._scrollLeft,
this._scrollTop,
this._clientWidth,
this._clientHeight
);
var count = this._cellLayout.count(
this._scrollLeft,
this._scrollTop,
this._clientWidth,
this._clientHeight
);
var items = this._items;
var bufferBefore = Math.min(index, this._buffer);
index -= bufferBefore;
count += bufferBefore;
count = Math.min(count + this._buffer, get(items, 'length') - index);
var i, style, itemIndex, itemKey, cell;
var newItems = [];
for (i = 0; i < count; i++) {
itemIndex = index + i;
itemKey = identity(items.objectAt(itemIndex));
if (priorMap) {
cell = priorMap[itemKey];
}
if (cell) {
// additional `checked` argument
style = this._cellLayout.formatItemStyle(
itemIndex,
this._clientWidth,
this._clientHeight,
this.checked
);
set(cell, 'style', style);
set(cell, 'hidden', false);
set(cell, 'key', itemKey);
cellMap[itemKey] = cell;
} else {
newItems.push(itemIndex);
}
}
for (i = 0; i < this._cells.length; i++) {
cell = this._cells[i];
if (!cellMap[cell.key]) {
if (newItems.length) {
itemIndex = newItems.pop();
let item = items.objectAt(itemIndex);
itemKey = identity(item);
// additional `checked` argument
style = this._cellLayout.formatItemStyle(
itemIndex,
this._clientWidth,
this._clientHeight,
this.checked
);
set(cell, 'style', style);
set(cell, 'key', itemKey);
set(cell, 'index', itemIndex);
set(cell, 'item', item);
set(cell, 'hidden', false);
cellMap[itemKey] = cell;
} else {
set(cell, 'hidden', true);
set(cell, 'style', 'height: 0; display: none;');
}
}
}
for (i = 0; i < newItems.length; i++) {
itemIndex = newItems[i];
let item = items.objectAt(itemIndex);
itemKey = identity(item);
// additional `checked` argument
style = this._cellLayout.formatItemStyle(
itemIndex,
this._clientWidth,
this._clientHeight,
this.checked
);
cell = new Cell(itemKey, item, itemIndex, style);
cellMap[itemKey] = cell;
this._cells.pushObject(cell);
}
this._cellMap = cellMap;
},
actions: {
click: function(e) {
// click on row functionality
// so if you click the actual row but not a link
// find the first link and fire that instead
const name = e.target.nodeName.toLowerCase();
switch (name) {
case 'input':

View File

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

View File

@ -0,0 +1,18 @@
import Controller from '@ember/controller';
import { get } from '@ember/object';
import WithFiltering from 'consul-ui/mixins/with-filtering';
import rightTrim from 'consul-ui/utils/right-trim';
export default Controller.extend(WithFiltering, {
queryParams: {
s: {
as: 'filter',
replace: true,
},
},
filter: function(item, { s = '' }) {
const key = rightTrim(get(item, 'Key'), '/')
.split('/')
.pop();
return key.toLowerCase().indexOf(s.toLowerCase()) !== -1;
},
});

View File

@ -1,7 +1,11 @@
import Controller from '@ember/controller';
import { get, set } from '@ember/object';
import { getOwner } from '@ember/application';
import WithFiltering from 'consul-ui/mixins/with-filtering';
import qsaFactory from 'consul-ui/utils/qsa-factory';
import getComponentFactory from 'consul-ui/utils/get-component-factory';
const $$ = qsaFactory();
export default Controller.extend(WithFiltering, {
queryParams: {
s: {
@ -14,13 +18,30 @@ export default Controller.extend(WithFiltering, {
set(this, 'selectedTab', 'health-checks');
},
filter: function(item, { s = '' }) {
const term = s.toLowerCase();
return (
get(item, 'Service')
.toLowerCase()
.indexOf(s.toLowerCase()) !== -1
.indexOf(term) !== -1 ||
get(item, 'Port')
.toString()
.toLowerCase()
.indexOf(term) !== -1
);
},
actions: {
change: function(e) {
set(this, 'selectedTab', e.target.value);
const getComponent = getComponentFactory(getOwner(this));
// Ensure tabular-collections sizing is recalculated
// now it is visible in the DOM
[...$$('.tab-section input[type="radio"]:checked + div table')].forEach(function(item) {
const component = getComponent(item);
if (component && typeof component.didAppear === 'function') {
getComponent(item).didAppear();
}
});
},
sortChecksByImportance: function(a, b) {
const statusA = get(a, 'Status');
const statusB = get(b, 'Status');

View File

@ -1,7 +1,6 @@
import { helper } from '@ember/component/helper';
import leftTrim from 'consul-ui/utils/left-trim';
export function leftTrim([str = '', search = ''], hash) {
return str.indexOf(search) === 0 ? str.substr(search.length) : str;
}
export default helper(leftTrim);
export default helper(function([str = '', search = ''], hash) {
return leftTrim(str, search);
});

View File

@ -1,8 +1,7 @@
import { helper } from '@ember/component/helper';
export function rightTrim([str = '', search = ''], hash) {
const pos = str.length - search.length;
return str.indexOf(search) === pos ? str.substr(0, pos) : str;
}
import rightTrim from 'consul-ui/utils/right-trim';
export default helper(rightTrim);
export default helper(function([str = '', search = ''], hash) {
return rightTrim(str, search);
});

View File

@ -3,9 +3,13 @@ import Mixin from '@ember/object/mixin';
import { next } from '@ember/runloop';
import { get } from '@ember/object';
const isOutside = function(element, e) {
const isRemoved = !e.target || !document.contains(e.target);
const isInside = element === e.target || element.contains(e.target);
return !isRemoved && !isInside;
if (element) {
const isRemoved = !e.target || !document.contains(e.target);
const isInside = element === e.target || element.contains(e.target);
return !isRemoved && !isInside;
} else {
return false;
}
};
const handler = function(e) {
const el = get(this, 'element');

View File

@ -7,7 +7,12 @@ export const SLUG_KEY = 'ID';
export default Model.extend({
[PRIMARY_KEY]: attr('string'),
[SLUG_KEY]: attr('string'),
Name: attr('string'),
Name: attr('string', {
// TODO: Why didn't I have to do this for KV's?
// this is to ensure that Name is '' and not null when creating
// maybe its due to the fact that `Key` is the primaryKey in Kv's
defaultValue: '',
}),
Type: attr('string'),
Rules: attr('string'),
CreateIndex: attr('number'),

View File

@ -5,6 +5,12 @@ import { get } from '@ember/object';
import WithKvActions from 'consul-ui/mixins/kv/with-actions';
export default Route.extend(WithKvActions, {
queryParams: {
s: {
as: 'filter',
replace: true,
},
},
repo: service('kv'),
model: function(params) {
const key = params.key || '/';

View File

@ -70,19 +70,30 @@
%action-group ul {
position: absolute;
right: -10px;
top: 35px;
padding: 1px;
}
%action-group ul::before {
position: absolute;
right: 18px;
top: -6px;
content: '';
display: block;
width: 10px;
height: 10px;
}
%action-group ul:not(.above) {
top: 35px;
}
%action-group ul:not(.above)::before {
top: -6px;
transform: rotate(45deg);
}
%action-group ul.above {
bottom: 35px;
}
%action-group ul.above::before {
bottom: -6px;
transform: rotate(225deg);
}
%action-group li {
position: relative;
z-index: 1;

View File

@ -44,6 +44,7 @@
margin-left: 12px;
}
%filter-bar fieldset {
min-width: 210px;
width: auto;
}
}

View File

@ -14,8 +14,10 @@
display: block;
max-width: 100%;
min-width: 100%;
padding: 0.625em;
min-height: 70px;
padding: 0.625em 15px;
resize: vertical;
line-height: 1.5;
}
%form-element [type='text'],
%form-element [type='password'] {
@ -37,14 +39,16 @@
box-shadow: none;
border-radius: $radius-small;
}
.has-error > input {
.has-error > input,
.has-error > textarea {
border: 1px solid;
}
%form-element > span {
color: $text-gray;
}
%form-element [type='text'],
%form-element [type='password'] {
%form-element [type='password'],
%form-element textarea {
color: $user-text-gray;
}
%form-element [type='text'],

View File

@ -63,6 +63,7 @@ h2,
body,
pre code,
input,
textarea,
td {
font-size: $size-6;
}

View File

@ -51,7 +51,7 @@
<main>
{{yield}}
</main>
<footer data-test-footer>
<footer role="contentinfo" data-test-footer>
<a data-test-footer-copyright href="{{env 'CONSUL_COPYRIGHT_URL'}}/" rel="noopener noreferrer" target="_blank">&copy; {{env 'CONSUL_COPYRIGHT_YEAR'}} HashiCorp</a>
<p data-test-footer-version>Consul {{env 'CONSUL_VERSION'}}</p>
<a data-test-footer-docs href="{{env 'CONSUL_DOCUMENTATION_URL'}}/index.html" rel="help noopener noreferrer" target="_blank">Documentation</a>

View File

@ -10,7 +10,7 @@
{{#ember-native-scrollable tagName='tbody' content-size=_contentSize scroll-left=_scrollLeft scroll-top=_scrollTop scrollChange=(action "scrollChange") clientSizeChange=(action "clientSizeChange")}}
<tr></tr>
{{~#each _cells as |cell|~}}
<tr style={{{cell.style}}} onclick={{action 'click'}}>
<tr data-test-tabular-row style={{{cell.style}}} onclick={{action 'click'}}>
{{#yield-slot 'row'}}{{yield cell.item cell.index}}{{/yield-slot}}
{{#if hasActions }}
<td class="actions">

View File

@ -26,12 +26,13 @@
</fieldset>
<div>
{{#if create }}
<button type="submit" {{ action "create" item}} disabled={{if item.isInvalid 'disabled'}}>Save</button>
{{! we only need to check for an empty name here as ember munges autofocus, once we have autofocus back revisit this}}
<button type="submit" {{ action "create" item}} disabled={{if (or item.isPristine item.isInvalid (eq item.Name '')) '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')) }}
{{# if (and (not create) (not-eq item.ID 'anonymous')) }}
{{#confirmation-dialog message='Are you sure you want to delete this ACL token?'}}
{{#block-slot 'action' as |confirm|}}
<button type="button" class="type-delete" {{action confirm 'delete' item parent}}>Delete</button>

View File

@ -11,21 +11,23 @@
<div>
<label class="type-toggle">
<input type="checkbox" name="json" checked={{if json 'checked' }} onchange={{action 'change'}} />
<span>JSON</span>
<span>Code</span>
</label>
<label class="type-text{{if item.error.Value ' has-error'}}">
<span>Value</span>
{{#if json}}
{{ code-editor value=(atob item.Value) onkeyup=(action 'change') }}
{{else}}
<input autofocus={{not create}} type="text" name="value" value={{atob item.Value}} onkeyup={{action 'change'}} />
<textarea autofocus={{not create}} name="value" onkeyup={{action 'change'}}>{{atob item.Value}}</textarea>
{{/if}}
</label>
</div>
{{/if}}
</fieldset>
{{!TODO This has a <div> around it in acls, remove or add for consistency }}
{{#if create }}
<button type="submit" {{ action "create" item parent}} disabled={{if item.isInvalid 'disabled'}}>Save</button>
{{! we only need to check for an empty keyname here as ember munges autofocus, once we have autofocus back revisit this}}
<button type="submit" {{ action "create" item parent}} disabled={{if (or item.isPristine item.isInvalid (eq (left-trim item.Key parent.Key) '')) 'disabled'}}>Save</button>
{{ else }}
<button type="submit" {{ action "update" item parent}} disabled={{if item.isInvalid 'disabled'}}>Save</button>
<button type="reset" {{ action "cancel" item parent}}>Cancel changes</button>

View File

@ -18,6 +18,13 @@
{{/if}}
</h1>
{{/block-slot}}
{{#block-slot 'toolbar'}}
{{#if (gt items.length 0) }}
<form class="filter-bar">
{{freetext-filter onchange=(action 'filter') value=filter.s placeholder="Search by name"}}
</form>
{{/if}}
{{/block-slot}}
{{#block-slot 'actions'}}
{{#if (not-eq parent.Key '/') }}
<a href="{{href-to 'dc.kv.create' parent.Key}}" class="type-create">Create</a>
@ -26,9 +33,9 @@
{{/if}}
{{/block-slot}}
{{#block-slot 'content'}}
{{#if (gt items.length 0)}}
{{#if (gt filtered.length 0)}}
{{#tabular-collection
items=(sort-by 'isFolder:desc' items) as |item index|
items=(sort-by 'isFolder:desc' 'Key:asc' filtered) as |item index|
}}
{{#block-slot 'header'}}
<th>Name</th>

View File

@ -1,6 +1,6 @@
<ul>
<ul data-test-node-healthchecks>
{{#each (sort-by (action 'sortChecksByImportance') item.Checks) as |check| }}
{{healthcheck-status tagName='li' name=check.Name class=check.Status status=check.Status notes=check.Notes output=check.Output}}
{{healthcheck-status data-test-node-healthcheck=check.Name tagName='li' name=check.Name class=check.Status status=check.Status notes=check.Notes output=check.Output}}
{{/each}}
</ul>

View File

@ -1,10 +1,11 @@
{{#if (gt items.length 0) }}
<form class="filter-bar">
{{freetext-filter onchange=(action 'filter') value=filter.s placeholder="Search by name"}}
{{freetext-filter onchange=(action 'filter') value=filter.s placeholder="Search by name/port"}}
</form>
{{/if}}
{{#if (gt filtered.length 0)}}
{{#tabular-collection
data-test-services
items=filtered as |item index|
}}
{{#block-slot 'header'}}
@ -13,10 +14,10 @@
<th>Tags</th>
{{/block-slot}}
{{#block-slot 'row'}}
<td>
<td data-test-service-name="{{item.Service}}">
<a href={{href-to 'dc.services.show' item.Service }}>{{item.Service}}</a>
</td>
<td>
<td data-test-service-port="{{item.Port}}" class="port">
{{item.Port}}
</td>
<td>

View File

@ -45,7 +45,7 @@
)
) as |panel|
}}
{{#tab-section id=panel.id selected=(eq (if selectedTab selectedTab 'health-checks') panel.id) onchange=(action (mut selectedTab) value="target.value")}}
{{#tab-section id=panel.id selected=(eq (if selectedTab selectedTab 'health-checks') panel.id) onchange=(action "change")}}
{{partial panel.partial}}
{{/tab-section}}
{{/each}}

View File

@ -0,0 +1,9 @@
export default function(owner, key = '-view-registry:main') {
const components = owner.lookup(key);
return function(el) {
const id = el.getAttribute('id');
if (id) {
return components[id];
}
};
}

View File

@ -0,0 +1,3 @@
export default function leftTrim(str = '', search = '') {
return str.indexOf(search) === 0 ? str.substr(search.length) : str;
}

View File

@ -0,0 +1,5 @@
export default function(doc = document) {
return function(sel, context = doc) {
return context.querySelectorAll(sel);
};
}

View File

@ -0,0 +1,4 @@
export default function rightTrim(str = '', search = '') {
const pos = str.length - search.length;
return str.lastIndexOf(search) === pos ? str.substr(0, pos) : str;
}

View File

@ -2,5 +2,4 @@ import { validatePresence, validateLength } from 'ember-changeset-validations/va
export default {
Name: [validatePresence(true), validateLength({ min: 1 })],
Type: validatePresence(true),
ID: validateLength({ min: 1 }),
};

View File

@ -1,7 +1,7 @@
'use strict';
const EmberApp = require('ember-cli/lib/broccoli/ember-app');
const stew = require('broccoli-stew');
module.exports = function(defaults) {
let app = new EmberApp(defaults, {
'ember-cli-babel': {
@ -43,6 +43,9 @@ module.exports = function(defaults) {
// modules that you would like to import into your application
// please specify an object with the list of modules as keys
// along with the exports of each module as its value.
return app.toTree();
let tree = app.toTree();
if (app.env === 'production') {
tree = stew.rm(tree, 'consul-api-double');
}
return tree;
};

View File

@ -15,7 +15,9 @@
"format:js": "prettier --write \"{app,config,lib,server,tests}/**/*.js\" ./*.js ./.*.js",
"start": "ember serve",
"test": "ember test",
"precommit": "lint-staged"
"test:view": "ember test --server",
"precommit": "lint-staged",
"postinstall": "rsync -aq ./node_modules/@hashicorp/consul-api-double/ ./public/consul-api-double/"
},
"lint-staged": {
"{app,config,lib,server,tests}/**/*.js": [
@ -27,10 +29,14 @@
"git add"
]
},
"dependencies": {},
"devDependencies": {
"@hashicorp/consul-api-double": "^1.0.0",
"@hashicorp/ember-cli-api-double": "^1.0.2",
"babel-plugin-transform-object-rest-spread": "^6.26.0",
"base64-js": "^1.3.0",
"broccoli-asset-rev": "^2.4.5",
"broccoli-stew": "^1.5.0",
"ember-ajax": "^3.0.0",
"ember-block-slots": "^1.1.11",
"ember-browserify": "^1.2.2",
@ -40,6 +46,7 @@
"ember-cli-app-version": "^3.0.0",
"ember-cli-autoprefixer": "^0.8.1",
"ember-cli-babel": "^6.6.0",
"ember-cli-cjs-transform": "^1.2.0",
"ember-cli-clipboard": "^0.9.0",
"ember-cli-code-coverage": "^1.0.0-beta.4",
"ember-cli-dependency-checker": "^2.0.0",
@ -55,6 +62,7 @@
"ember-cli-shims": "^1.2.0",
"ember-cli-sri": "^2.1.0",
"ember-cli-uglify": "^2.0.0",
"ember-cli-yadda": "^0.4.0",
"ember-collection": "^1.0.0-alpha.7",
"ember-composable-helpers": "^2.1.0",
"ember-computed-style": "^0.2.0",

View File

@ -0,0 +1,35 @@
@setupApplicationTest
Feature: Acl Filter
Scenario: Filtering [Model]
Given 1 datacenter model with the value "dc-1"
And 2 [Model] models
When I visit the [Page] page for yaml
---
dc: dc-1
---
Then the url should be [Url]
Then I see 2 [Model] models
And I see allIsSelected on the filter
When I click management on the filter
Then I see managementIsSelected on the filter
And I see 1 [Model] model
When I click client on the filter
Then I see clientIsSelected on the filter
And I see 1 [Model] model
When I click all on the filter
Then I see allIsSelected on the filter
Then I type with yaml
---
s: Anonymous Token
---
And I see 1 [Model] model with the name "Anonymous Token"
Where:
-------------------------------------------------
| Model | Page | Url |
| acl | acls | /dc-1/acls |
-------------------------------------------------

View File

@ -0,0 +1,88 @@
@setupApplicationTest
Feature: components / catalog-filter
Scenario: Filtering [Model]
Given 1 datacenter model with the value "dc-1"
And 3 service models from yaml
---
- ChecksPassing: 1
ChecksWarning: 0
ChecksCritical: 0
- ChecksPassing: 0
ChecksWarning: 1
ChecksCritical: 0
- ChecksPassing: 0
ChecksWarning: 0
ChecksCritical: 1
---
And 3 node models from yaml
---
- Checks:
- Status: passing
- Checks:
- Status: warning
- Checks:
- Status: critical
---
When I visit the [Page] page for yaml
---
dc: dc-1
---
Then the url should be [Url]
Then I see 3 [Model] models
And I see allIsSelected on the filter
When I click passing on the filter
And I see passingIsSelected on the filter
And I see 1 [Model] model
When I click warning on the filter
And I see warningIsSelected on the filter
And I see 1 [Model] model
When I click critical on the filter
And I see criticalIsSelected on the filter
And I see 1 [Model] model
When I click all on the filter
And I see allIsSelected on the filter
Then I type with yaml
---
s: [Model]-0
---
And I see 1 [Model] model with the name "[Model]-0"
Where:
-------------------------------------------------
| Model | Page | Url |
| service | services | /dc-1/services |
| node | nodes | /dc-1/nodes |
-------------------------------------------------
Scenario: Filtering [Model] in [Page]
Given 1 datacenter model with the value "dc1"
And 2 node models from yaml
---
- ID: node-0
---
When I visit the node page for yaml
---
dc: dc1
node: node-0
---
# And I see 3 healthcheck model with the name "Disk Util"
# And then pause for 5000
When I click services on the tabs
And I see servicesIsSelected on the tabs
Then I type with yaml
---
s: 65535
---
And I see 1 [Model] model
And I see 1 [Model] model with the port "65535"
Where:
-------------------------------------------------
| Model | Page | Url |
| service | node | /dc-1/nodes/node-0 |
-------------------------------------------------

View File

@ -0,0 +1,26 @@
@setupApplicationTest
Feature: components / kv-filter
Scenario: Filtering using the freetext filter
Given 1 datacenter model with the value "dc-1"
And 2 [Model] models from yaml
---
- hi
- there
---
When I visit the [Page] page for yaml
---
dc: dc-1
---
Then the url should be [Url]
Then I type with yaml
---
s: [Text]
---
And I see 1 [Model] model with the name "[Text]"
Where:
----------------------------------------------------------------
| Model | Page | Url | Text | Property |
| kv | kvs | /dc-1/kv | hi | name |
| kv | kvs | /dc-1/kv | there | name |
----------------------------------------------------------------

View File

@ -0,0 +1,17 @@
@setupApplicationTest
Feature: dc / acls / delete: ACL Delete
Scenario: Delete ACL
Given 1 datacenter model with the value "datacenter"
And 1 acl model from yaml
---
Name: something
ID: key
---
When I visit the acls page for yaml
---
dc: datacenter
---
And I click actions on the acls
And I click delete on the acls
And I click confirmDelete on the acls
Then a PUT request is made to "/v1/acl/destroy/key?dc=datacenter"

View File

@ -0,0 +1,39 @@
@setupApplicationTest
Feature: dc / acls / update: ACL Update
Scenario: Update to [Name], [Type], [Rules]
Given 1 datacenter model with the value "datacenter"
And 1 acl model from yaml
---
ID: key
---
When I visit the acl page for yaml
---
dc: datacenter
acl: key
---
Then the url should be /datacenter/acls/key
Then I type with yaml
---
name: [Name]
---
And I click "[value=[Type]]"
And I submit
Then a PUT request is made to "/v1/acl/update?dc=datacenter" with the body from yaml
---
Name: [Name]
Type: [Type]
---
Where:
----------------------------------------------------------
| Name | Type | Rules |
| key-name | client | node "0" {policy = "read"} |
| key name | management | node "0" {policy = "write"} |
| 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

View File

@ -0,0 +1,8 @@
@setupApplicationTest
Feature: Datacenters
@ignore
Scenario: Arriving at the service page
Given 10 datacenter models
When I visit the index page
And I click "[data-test-datacenter-selected]"
Then I see 10 datacenter models

View File

@ -0,0 +1,16 @@
@setupApplicationTest
Feature: dc / kvs / delete: KV Delete
Scenario: Delete ACL
Given 1 datacenter model with the value "datacenter"
And 1 kv model from yaml
---
- key-name
---
When I visit the kvs page for yaml
---
dc: datacenter
---
And I click actions on the kvs
And I click delete on the kvs
And I click confirmDelete on the kvs
Then a DELETE request is made to "/v1/kv/key-name?dc=datacenter"

View File

@ -0,0 +1,56 @@
@setupApplicationTest
Feature: dc / kvs / list-order
In order to be able to find key values easier
As a user
I want to see the Key/Values listed alphabetically
Scenario: I have 19 folders
Given 1 datacenter model with the value "datacenter"
And 19 kv models from yaml
---
- __secretzzz/
- a-thing-service/
- a-thing-y-again-service/
- a-thing-y-againzz-service/
- a-z-search-service/
- blood-pressure-service/
- callToAction-items/
- configuration/
- content-service/
- currentRepository-jobs/
- currentRepository-service/
- first-service/
- logs-service/
- rabmq-svc/
- rabmqUtilities/
- schedule-service/
- vanApp-service/
- vanCat-service/
- vanTaxi-service/
---
When I visit the kvs page for yaml
---
dc: datacenter
---
Then I see name on the kvs like yaml
---
- __secretzzz/
- a-thing-service/
- a-thing-y-again-service/
- a-thing-y-againzz-service/
- a-z-search-service/
- blood-pressure-service/
- callToAction-items/
- configuration/
- content-service/
- currentRepository-jobs/
- currentRepository-service/
- first-service/
- logs-service/
- rabmq-svc/
- rabmqUtilities/
- schedule-service/
- vanApp-service/
- vanCat-service/
- vanTaxi-service/
---

View File

@ -0,0 +1,36 @@
@setupApplicationTest
Feature: dc / kvs / update: KV Update
Scenario: Update to [Name] change value to [Value]
Given 1 datacenter model with the value "datacenter"
And 1 kv model from yaml
---
Key: [Name]
---
When I visit the kv page for yaml
---
dc: datacenter
kv: [Name]
---
Then the url should be /datacenter/kv/[Name]/edit
Then I type with yaml
---
value: [Value]
---
And I submit
Then a PUT request is made to "/v1/kv/[Name]?dc=datacenter" with the body "[Value]"
Where:
--------------------------------------------
| Name | Value |
| key | value |
| key-name | a value |
| folder/key-name | a value |
--------------------------------------------
@ignore
Scenario: The feedback dialog says success or failure
Then ok
@ignore
Scenario: KV's with spaces are saved correctly
Then ok
@ignore
Scenario: KV's with returns are saved correctly
Then ok

View File

@ -0,0 +1,20 @@
@setupApplicationTest
Feature: List Models
Scenario: Listing [Model]
Given 1 datacenter model with the value "dc-1"
And 3 [Model] models
When I visit the [Page] page for yaml
---
dc: dc-1
---
Then the url should be [Url]
Then I see 3 [Model] models
Where:
-------------------------------------------------
| Model | Page | Url |
| service | services | /dc-1/services |
| node | nodes | /dc-1/nodes |
| kv | kvs | /dc-1/kv |
| acl | acls | /dc-1/acls |
-------------------------------------------------

View File

@ -0,0 +1,11 @@
@setupApplicationTest
Feature: Nodes
Scenario:
Given 1 datacenter model with the value "dc-1"
And 3 node models
When I visit the nodes page for yaml
---
dc: dc-1
---
Then the url should be /dc-1/nodes
Then I see 3 node models

View File

@ -0,0 +1,16 @@
@setupApplicationTest
Feature: Search services within nodes by name and port
Scenario: Given 1 node
Given 1 datacenter model with the value "dc1"
And 1 node models from yaml
---
- ID: node-0
---
When I visit the node page for yaml
---
dc: dc1
node: node-0
---
When I click services on the tabs
And I see servicesIsSelected on the tabs

View File

@ -0,0 +1,46 @@
@setupApplicationTest
Feature: Show node
Scenario: Given 2 nodes all the tabs are visible and clickable
Given 1 datacenter model with the value "dc1"
And 2 node models from yaml
---
- ID: node-0
- ID: node-1
---
When I visit the node page for yaml
---
dc: dc1
node: node-0
---
And I see healthChecksIsSelected on the tabs
When I click services on the tabs
And I see servicesIsSelected on the tabs
When I click roundTripTime on the tabs
And I see roundTripTimeIsSelected on the tabs
When I click lockSessions on the tabs
And I see lockSessionsIsSelected on the tabs
@ignore
Scenario: Given 1 node all the tabs are visible and clickable and the RTT one isn't there
Given 1 datacenter model with the value "dc1"
And 1 node models from yaml
---
- ID: node-0
---
When I visit the node page for yaml
---
dc: dc1
node: node-0
---
And I see healthChecksIsSelected on the tabs
When I click services on the tabs
And I see servicesIsSelected on the tabs
And I don't see roundTripTime on the tabs
When I click lockSessions on the tabs
And I see lockSessionsIsSelected on the tabs

View File

@ -0,0 +1,11 @@
@setupApplicationTest
Feature: Services
Scenario:
Given 1 datacenter model with the value "dc-1"
And 3 service models
When I visit the services page for yaml
---
dc: dc-1
---
Then the url should be /dc-1/services
Then I see 3 service models

View File

@ -0,0 +1,6 @@
@setupApplicationTest
Feature: index forwarding
Scenario: Arriving at the index page when there is only one datacenter
Given 1 datacenter model with the value "datacenter"
When I visit the index page
Then the url should be /datacenter/services

View File

@ -0,0 +1,43 @@
@setupApplicationTest
Feature: Page Navigation
Background:
Given 1 datacenter model with the value "dc-1"
Scenario: Visiting the index page
When I visit the index page for yaml
---
dc: dc-1
---
Then the url should be /dc-1/services
Scenario: Clicking [Link] in the navigation takes me to [Url]
When I visit the services page for yaml
---
dc: dc-1
---
When I click [Link] on the navigation
Then the url should be [Url]
Where:
--------------------------------------
| Link | Url |
| nodes | /dc-1/nodes |
| kvs | /dc-1/kv |
| acls | /dc-1/acls |
| settings | /settings |
--------------------------------------
Scenario: Clicking a [Item] in the [Model] listing
When I visit the [Model] page for yaml
---
dc: dc-1
---
When I click [Item] on the [Model]
Then the url should be [Url]
Where:
--------------------------------------------------------
| Item | Model | Url |
| service | services | /dc-1/services/service-0 |
| node | nodes | /dc-1/nodes/node-0 |
| kv | kvs | /dc-1/kv/necessitatibus-0/edit |
| acl | acls | /dc-1/acls/anonymous |
--------------------------------------------------------
@ignore
Scenario: Clicking a kv in the kvs listing, without depending on the salt ^
Then ok

View File

@ -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);
});
}

View File

@ -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);
});
}

View File

@ -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);
});
}

View File

@ -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);
});
}

View File

@ -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);
});
}

View File

@ -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);
});
}

View File

@ -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);
});
}

View File

@ -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);
});
}

View File

@ -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);
});
}

View File

@ -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);
});
}

View File

@ -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);
});
}

View File

@ -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);
});
}

View File

@ -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);
});
}

View File

@ -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);
});
}

View File

@ -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);
});
}

View File

@ -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);
});
}

View File

@ -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);
});
}

View File

@ -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);
});
}

View File

@ -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);
});
}

View File

@ -0,0 +1,2 @@
import steps from 'consul-ui/tests/steps';
export default steps;

View File

@ -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);
});
}

View File

@ -0,0 +1,24 @@
@setupApplicationTest
Feature: submit blank
In order to prevent form's being saved without values
As a user
I shouldn't be able to submit a blank form
Scenario: Visiting a blank form for [Model]
Given 1 datacenter model with the value "datacenter"
When I visit the [Model] page for yaml
---
dc: datacenter
---
Then the url should be /datacenter/[Slug]/create
And I submit
Then the url should be /datacenter/[Slug]/create
Where:
------------------
| Model | Slug |
| kv | kv |
| acl | acls |
------------------
@ignore
Scenario: The button is disabled
Then ok

View File

@ -0,0 +1,4 @@
import getAPI from '@hashicorp/ember-cli-api-double';
import setCookies from 'consul-ui/tests/helpers/set-cookies';
import typeToURL from 'consul-ui/tests/helpers/type-to-url';
export default getAPI('/consul-api-double', setCookies, typeToURL);

View File

@ -0,0 +1,25 @@
export default function(type, count, obj) {
var key = '';
switch (type) {
case 'dc':
key = 'CONSUL_DATACENTER_COUNT';
break;
case 'service':
key = 'CONSUL_SERVICE_COUNT';
break;
case 'node':
key = 'CONSUL_NODE_COUNT';
break;
case 'kv':
key = 'CONSUL_KV_COUNT';
break;
case 'acl':
key = 'CONSUL_ACL_COUNT';
obj['CONSUL_ENABLE_ACLS'] = 1;
break;
}
if (key) {
obj[key] = count;
}
return obj;
}

View File

@ -0,0 +1,23 @@
export default function(type) {
let url = null;
switch (type) {
case 'dc':
url = '/v1/catalog/datacenters';
break;
case 'service':
url = '/v1/internal/ui/services';
break;
case 'node':
url = '/v1/internal/ui/nodes';
// url = '/v1/health/service/_';
break;
case 'kv':
url = '/v1/kv/';
break;
case 'acl':
url = '/v1/acl/list';
// url = '/v1/acl/info/_';
break;
}
return url;
}

View File

@ -0,0 +1,85 @@
import ENV from '../../config/environment';
import { skip } from 'qunit';
import { setupApplicationTest, setupRenderingTest, setupTest } from 'ember-qunit';
import api from 'consul-ui/tests/helpers/api';
// this logic could be anything, but in this case...
// if @ignore, then return skip (for backwards compatibility)
// if have annotations in config, then only run those that have a matching annotation
function checkAnnotations(annotations) {
// if ignore is set then we want to skip for backwards compatibility
if (annotations.ignore) {
return ignoreIt;
}
// if have annotations set in config, the only run those that have a matching annotation
if (ENV.annotations && ENV.annotations.length >= 0) {
for (let annotation in annotations) {
if (ENV.annotations.indexOf(annotation) >= 0) {
// have match, so test it
return 'testIt'; // return something other than a function
}
}
// no match, so don't run it
return logIt;
}
}
// call back functions
function ignoreIt(testElement) {
skip(`${testElement.title}`, function(/*assert*/) {});
}
function logIt(testElement) {
console.info(`Not running or skipping: "${testElement.title}"`); // eslint-disable-line no-console
}
// exported functions
function runFeature(annotations) {
return checkAnnotations(annotations);
}
function runScenario(featureAnnotations, scenarioAnnotations) {
return checkAnnotations(scenarioAnnotations);
}
// setup tests
// you can override these function to add additional setup setups, or handle new setup related annotations
function setupFeature(featureAnnotations) {
return setupYaddaTest(featureAnnotations);
}
function setupScenario(featureAnnotations, scenarioAnnotations) {
let setupFn = setupYaddaTest(scenarioAnnotations);
if (
setupFn &&
(featureAnnotations.setupapplicationtest ||
featureAnnotations.setuprenderingtest ||
featureAnnotations.setuptest)
) {
throw new Error(
'You must not assign any @setupapplicationtest, @setuprenderingtest or @setuptest annotations to a scenario as well as its feature!'
);
}
return function(model) {
model.afterEach(function() {
api.server.reset();
});
};
// return setupFn;
}
function setupYaddaTest(annotations) {
if (annotations.setupapplicationtest) {
return setupApplicationTest;
}
if (annotations.setuprenderingtest) {
return setupRenderingTest;
}
if (annotations.setuptest) {
return setupTest;
}
}
export { runFeature, runScenario, setupFeature, setupScenario };

View File

@ -0,0 +1,2 @@
import yadda from 'npm:yadda';
export default yadda;

View File

@ -1,4 +1,4 @@
import { moduleForComponent, skip } from 'ember-qunit';
import { moduleForComponent, test } from 'ember-qunit';
import hbs from 'htmlbars-inline-precompile';
moduleForComponent('left trim', 'helper:left-trim', {
@ -6,7 +6,7 @@ moduleForComponent('left trim', 'helper:left-trim', {
});
// Replace this with your real tests.
skip('it renders', function(assert) {
test('it renders', function(assert) {
this.set('inputValue', '1234');
this.render(hbs`{{left-trim inputValue}}`);

View File

@ -1,4 +1,4 @@
import { moduleForComponent, skip } from 'ember-qunit';
import { moduleForComponent, test } from 'ember-qunit';
import hbs from 'htmlbars-inline-precompile';
moduleForComponent('right-trim', 'helper:right-trim', {
@ -6,7 +6,7 @@ moduleForComponent('right-trim', 'helper:right-trim', {
});
// Replace this with your real tests.
skip('it renders', function(assert) {
test('it renders', function(assert) {
this.set('inputValue', '1234');
this.render(hbs`{{right-trim inputValue}}`);

View File

@ -0,0 +1,84 @@
// import { assign } from '../-private/helpers';
const assign = Object.assign;
import { getExecutionContext } from 'ember-cli-page-object/-private/execution_context';
import $ from '-jquery';
function fillInDynamicSegments(path, params, encoder) {
return path
.split('/')
.map(function(segment) {
let match = segment.match(/^:(.+)$/);
if (match) {
let [, key] = match;
let value = params[key];
if (typeof value === 'undefined') {
throw new Error(`Missing parameter for '${key}'`);
}
// Remove dynamic segment key from params
delete params[key];
return encoder(value);
}
return segment;
})
.join('/');
}
function appendQueryParams(path, queryParams) {
if (Object.keys(queryParams).length) {
path += `?${$.param(queryParams)}`;
}
return path;
}
/**
* Custom implementation of `visitable`
* Currently aims to be compatible and as close as possible to the
* actual `ember-cli-page-object` version
*
* Additions:
* 1. Injectable encoder, for when you don't want your segments to be encoded
* or you have specific encoding needs
* Specifically in my case for KV urls where the `Key`/Slug shouldn't be encoded,
* defaults to the browsers `encodeURIComponent` for compatibility and ease.
* 2. `path` can be an array of (string) paths OR a string for compatibility.
* If a path cannot be generated due to a lack of properties on the
* dynamic segment params, if will keep trying 'path' in the array
* until it finds one that it can construct. This follows the same thinking
* as 'if you don't specify an item, then we are looking to create one'
*/
export function visitable(path, encoder = encodeURIComponent) {
return {
isDescriptor: true,
value(dynamicSegmentsAndQueryParams = {}) {
let executionContext = getExecutionContext(this);
return executionContext.runAsync(context => {
var params;
let fullPath = (function _try(paths) {
const path = paths.shift();
params = assign({}, dynamicSegmentsAndQueryParams);
var fullPath;
try {
fullPath = fillInDynamicSegments(path, params, encoder);
} catch (e) {
if (paths.length > 0) {
fullPath = _try(paths);
} else {
throw e;
}
}
return fullPath;
})(typeof path === 'string' ? [path] : path.slice(0));
fullPath = appendQueryParams(fullPath, params);
return context.visit(fullPath);
});
},
};
}

View File

@ -1,7 +1,10 @@
import { create, visitable, fillable, clickable } from 'ember-cli-page-object';
import { create, clickable, triggerable } from 'ember-cli-page-object';
import { visitable } from 'consul-ui/tests/lib/page-object/visitable';
export default create({
visit: visitable('/:dc/acls/:acl'),
fillIn: fillable('input, textarea, [contenteditable]'),
// custom visitable
visit: visitable(['/:dc/acls/:acl', '/:dc/acls/create']),
// fillIn: fillable('input, textarea, [contenteditable]'),
name: triggerable('keypress', '[name="name"]'),
submit: clickable('[type=submit]'),
});

View File

@ -3,9 +3,12 @@ import { create, visitable, collection, attribute, clickable } from 'ember-cli-p
import filter from 'consul-ui/tests/pages/components/acl-filter';
export default create({
visit: visitable('/:dc/acls'),
acls: collection('[data-test-acl]', {
name: attribute('data-test-acl'),
acls: collection('[data-test-tabular-row]', {
name: attribute('data-test-acl', '[data-test-acl]'),
acl: clickable('a'),
actions: clickable('label'),
delete: clickable('li:last-child a'),
confirmDelete: clickable('button.type-delete'),
}),
filter: filter,
});

View File

@ -1,7 +1,10 @@
import { create, visitable, fillable, clickable } from 'ember-cli-page-object';
import { create, clickable } from 'ember-cli-page-object';
import { visitable } from 'consul-ui/tests/lib/page-object/visitable';
export default create({
visit: visitable('/:dc/kv/:kv'),
fillIn: fillable('input, textarea, [contenteditable]'),
// custom visitable
visit: visitable(['/:dc/kv/:kv/edit', '/:dc/kv/create'], str => str),
// fillIn: fillable('input, textarea, [contenteditable]'),
// name: triggerable('keypress', '[name="additional"]'),
submit: clickable('[type=submit]'),
});

View File

@ -1,6 +1,12 @@
import { create, visitable, collection } from 'ember-cli-page-object';
import { create, visitable, collection, attribute, clickable } from 'ember-cli-page-object';
export default create({
visit: visitable('/:dc/kv'),
kvs: collection('[data-test-kv]'),
kvs: collection('[data-test-tabular-row]', {
name: attribute('data-test-kv', '[data-test-kv]'),
kv: clickable('a'),
actions: clickable('label'),
delete: clickable('li:last-child a'),
confirmDelete: clickable('button.type-delete'),
}),
});

View File

@ -1,7 +1,13 @@
import { create, visitable } from 'ember-cli-page-object';
import { create, visitable, collection, attribute } from 'ember-cli-page-object';
import radiogroup from 'consul-ui/tests/lib/page-object/radiogroup';
export default create({
visit: visitable('/:dc/nodes/:node'),
tabs: radiogroup('tab', ['health-checks', 'services', 'round-trip-time', 'lock-sessions']),
healthchecks: collection('[data-test-node-healthcheck]', {
name: attribute('data-test-node-healthcheck'),
}),
services: collection('#services [data-test-tabular-row]', {
port: attribute('data-test-service-port', '.port'),
}),
});

239
ui-v2/tests/steps.js Normal file
View File

@ -0,0 +1,239 @@
/* eslint no-console: "off" */
import yadda from './helpers/yadda';
import { currentURL, click, triggerKeyEvent } from '@ember/test-helpers';
import getDictionary from '@hashicorp/ember-cli-api-double/dictionary';
import pages from 'consul-ui/tests/pages';
import api from 'consul-ui/tests/helpers/api';
const create = function(number, name, value) {
// don't return a promise here as
// I don't need it to wait
api.server.createList(name, number, value);
};
var currentPage;
export default function(assert) {
return (
yadda.localisation.English.library(
getDictionary(function(model, cb) {
switch (model) {
case 'datacenter':
case 'datacenters':
case 'dcs':
model = 'dc';
break;
case 'services':
model = 'service';
break;
case 'nodes':
model = 'node';
break;
case 'kvs':
model = 'kv';
break;
case 'acls':
model = 'acl';
break;
}
cb(null, model);
}, yadda)
)
// doubles
.given(['$number $model model', '$number $model models'], function(number, model) {
return create(number, model);
})
.given(['$number $model model with the value "$value"'], function(number, model, value) {
return create(number, model, value);
})
.given(
['$number $model model[s]? from yaml\n$yaml', '$number $model model from json\n$json'],
function(number, model, data) {
return create(number, model, data);
}
)
// interactions
.when('I visit the $name page', function(name) {
currentPage = pages[name];
return currentPage.visit();
})
.when('I visit the $name page for the "$id" $model', function(name, id, model) {
currentPage = pages[name];
return currentPage.visit({
[model]: id,
});
})
.when(
['I visit the $name page for yaml\n$yaml', 'I visit the $name page for json\n$json'],
function(name, data) {
currentPage = pages[name];
// TODO: Consider putting an assertion here for testing the current url
// do I absolutely definitely need that all the time?
return pages[name].visit(data);
}
)
.when('I click "$selector"', function(selector) {
return click(selector);
})
.when('I click $prop on the $component', function(prop, component) {
// Collection
var obj;
if (typeof currentPage[component].objectAt === 'function') {
obj = currentPage[component].objectAt(0);
} else {
obj = currentPage[component];
}
const func = obj[prop].bind(obj);
try {
return func();
} catch (e) {
console.error(e);
throw new Error(`The '${prop}' property on the '${component}' page object doesn't exist`);
}
})
.when('I submit', function(selector) {
return currentPage.submit();
})
.then('I fill in "$name" with "$value"', function(name, value) {
return currentPage.fillIn(name, value);
})
.then(['I fill in with yaml\n$yaml', 'I fill in with json\n$json'], function(data) {
return Object.keys(data).reduce(function(prev, item, i, arr) {
return prev.fillIn(item, data[item]);
}, currentPage);
})
.then(['I type with yaml\n$yaml'], function(data) {
const keys = Object.keys(data);
return keys
.reduce(function(prev, item, i, arr) {
return prev.fillIn(item, data[item]);
}, currentPage)
.then(function() {
return Promise.all(
keys.map(function(item) {
return triggerKeyEvent(`[name="${item}"]`, 'keyup', 83);
})
);
});
})
// debugging helpers
.then('print the current url', function(url) {
console.log(currentURL());
return Promise.resolve();
})
.then('log the "$text"', function(text) {
console.log(text);
return Promise.resolve();
})
.then('pause for $milliseconds', function(milliseconds) {
return new Promise(function(resolve) {
setTimeout(resolve, milliseconds);
});
})
// assertions
.then('a $method request is made to "$url" with the body from yaml\n$yaml', function(
method,
url,
data
) {
const request = api.server.history[api.server.history.length - 2];
assert.equal(
request.method,
method,
`Expected the request method to be ${method}, was ${request.method}`
);
assert.equal(request.url, url, `Expected the request url to be ${url}, was ${request.url}`);
const body = JSON.parse(request.requestBody);
Object.keys(data).forEach(function(key, i, arr) {
assert.equal(
body[key],
data[key],
`Expected the payload to contain ${key} to equal ${body[key]}, ${key} was ${data[key]}`
);
});
})
.then('a $method request is made to "$url" with the body "$body"', function(
method,
url,
data
) {
const request = api.server.history[api.server.history.length - 2];
assert.equal(
request.method,
method,
`Expected the request method to be ${method}, was ${request.method}`
);
assert.equal(request.url, url, `Expected the request url to be ${url}, was ${request.url}`);
const body = request.requestBody;
assert.equal(
body,
data,
`Expected the request body to be ${body}, was ${request.requestBody}`
);
})
.then('a $method request is made to "$url"', function(method, url) {
const request = api.server.history[api.server.history.length - 2];
assert.equal(
request.method,
method,
`Expected the request method to be ${method}, was ${request.method}`
);
assert.equal(request.url, url, `Expected the request url to be ${url}, was ${request.url}`);
})
.then('the url should be $url', function(url) {
const current = currentURL();
assert.equal(current, url, `Expected the url to be ${url} was ${current}`);
})
.then(['I see $num $model', 'I see $num $model model', 'I see $num $model models'], function(
num,
model
) {
const len = currentPage[`${model}s`].filter(function(item) {
return item.isVisible;
}).length;
assert.equal(len, num, `Expected ${num} ${model}s, saw ${len}`);
})
.then(['I see $num $model model with the $property "$value"'], function(
num,
model,
property,
value
) {
const len = currentPage[`${model}s`].filter(function(item) {
return item.isVisible && item[property] == value;
}).length;
assert.equal(
len,
num,
`Expected ${num} ${model}s with ${property} set to "${value}", saw ${len}`
);
})
.then('I see $property on the $component like yaml\n$yaml', function(
property,
component,
yaml
) {
const _component = currentPage[component];
const iterator = new Array(_component.length).fill(true);
iterator.forEach(function(item, i, arr) {
const actual = _component.objectAt(i)[property];
const expected = yaml[i];
assert.deepEqual(
actual,
expected,
`Expected to see ${property} on ${component}[${i}] as ${JSON.stringify(
expected
)}, was ${JSON.stringify(actual)}`
);
});
})
.then(['I see $property on the $component'], function(property, component) {
assert.ok(currentPage[component][property], `Expected to see ${property} on ${component}`);
})
.then(['I see $property'], function(property, component) {
assert.ok(currentPage[property], `Expected to see ${property}`);
})
.then('ok', function() {
assert.ok(true);
})
);
}

View File

@ -0,0 +1,12 @@
import { moduleFor, test } from 'ember-qunit';
moduleFor('controller:dc/kv/folder', 'Unit | Controller | dc/kv/folder', {
// 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/kv/index', 'Unit | Controller | dc/kv/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,48 @@
import getComponentFactory from 'consul-ui/utils/get-component-factory';
import { module, test } from 'qunit';
module('Unit | Utility | get component factory');
test("it uses lookup to locate the instance of the component based on the DOM element's id", function(assert) {
const expected = 'name';
let getComponent = getComponentFactory({
lookup: function() {
return { id: expected };
},
});
assert.equal(typeof getComponent, 'function', 'returns a function');
const actual = getComponent({
getAttribute: function(name) {
return 'id';
},
});
assert.equal(actual, expected, 'performs a lookup based on the id');
});
test("it returns null if it can't find it", function(assert) {
const expected = null;
let getComponent = getComponentFactory({
lookup: function() {
return { id: '' };
},
});
const actual = getComponent({
getAttribute: function(name) {
return 'non-existent';
},
});
assert.equal(actual, expected);
});
test('it returns null if there is no id', function(assert) {
const expected = null;
let getComponent = getComponentFactory({
lookup: function() {
return { id: '' };
},
});
const actual = getComponent({
getAttribute: function(name) {
return;
},
});
assert.equal(actual, expected);
});

View File

@ -0,0 +1,19 @@
import hasStatus from 'consul-ui/utils/hasStatus';
import { module, test, skip } from 'qunit';
module('Unit | Utility | has status');
const checks = {
filterBy: function(prop, value) {
return { length: 0 };
},
};
test('it returns true when passing an empty string (therefore "all")', function(assert) {
assert.ok(hasStatus(checks, ''));
});
test('it returns false when passing an actual status', function(assert) {
['passing', 'critical', 'warning'].forEach(function(item) {
assert.ok(!hasStatus(checks, item), `, with ${item}`);
});
});
skip('it works as a factory, passing ember `get` in to create the function');

View File

@ -0,0 +1,44 @@
import { module } from 'ember-qunit';
import test from 'ember-sinon-qunit/test-support/test';
import leftTrim from 'consul-ui/utils/left-trim';
module('Unit | Utility | left trim');
test('it trims characters from the left hand side', function(assert) {
[
{
args: ['/a/folder/here', '/'],
expected: 'a/folder/here',
},
{
args: ['/a/folder/here', ''],
expected: '/a/folder/here',
},
{
args: ['a/folder/here', '/'],
expected: 'a/folder/here',
},
{
args: ['a/folder/here/', '/'],
expected: 'a/folder/here/',
},
{
args: [],
expected: '',
},
{
args: ['/a/folder/here', '/a/folder'],
expected: '/here',
},
{
args: ['/a/folder/here/', '/a/folder/here'],
expected: '/',
},
{
args: ['/a/folder/here/', '/a/folder/here/'],
expected: '',
},
].forEach(function(item) {
const actual = leftTrim(...item.args);
assert.equal(actual, item.expected);
});
});

View File

@ -0,0 +1,34 @@
import qsaFactory from 'consul-ui/utils/qsa-factory';
import { module, test } from 'qunit';
module('Unit | Utility | qsa factory');
test('querySelectorAll is called on `document` when called with document', function(assert) {
assert.expect(2);
const expected = 'html';
const $$ = qsaFactory({
querySelectorAll: function(sel) {
assert.equal(sel, expected);
return true;
},
});
assert.ok($$(expected));
});
test('querySelectorAll is called on `context` when called with context', function(assert) {
assert.expect(2);
const expected = 'html';
const context = {
querySelectorAll: function(sel) {
assert.equal(sel, expected);
return true;
},
};
const $$ = qsaFactory({
// this should never be called
querySelectorAll: function(sel) {
assert.equal(sel, expected);
return false;
},
});
assert.ok($$(expected, context));
});

View File

@ -0,0 +1,44 @@
import { module } from 'ember-qunit';
import test from 'ember-sinon-qunit/test-support/test';
import rightTrim from 'consul-ui/utils/right-trim';
module('Unit | Utility | right trim');
test('it trims characters from the right hand side', function(assert) {
[
{
args: ['/a/folder/here/', '/'],
expected: '/a/folder/here',
},
{
args: ['/a/folder/here', ''],
expected: '/a/folder/here',
},
{
args: ['a/folder/here', '/'],
expected: 'a/folder/here',
},
{
args: ['a/folder/here/', '/'],
expected: 'a/folder/here',
},
{
args: [],
expected: '',
},
{
args: ['/a/folder/here', '/folder/here'],
expected: '/a',
},
{
args: ['/a/folder/here', 'a/folder/here'],
expected: '/',
},
{
args: ['/a/folder/here/', '/a/folder/here/'],
expected: '',
},
].forEach(function(item) {
const actual = rightTrim(...item.args);
assert.equal(actual, item.expected);
});
});

View File

@ -0,0 +1,93 @@
import sumOfUnhealthy from 'consul-ui/utils/sumOfUnhealthy';
import { module, test, skip } from 'qunit';
module('Unit | Utility | sum of unhealthy');
test('it returns the correct single count', function(assert) {
const expected = 1;
[
[
{
Status: 'critical',
},
],
[
{
Status: 'warning',
},
],
].forEach(function(checks) {
const actual = sumOfUnhealthy(checks);
assert.equal(actual, expected);
});
});
test('it returns the correct single count when there are none', function(assert) {
const expected = 0;
[
[
{
Status: 'passing',
},
{
Status: 'passing',
},
{
Status: 'passing',
},
{
Status: 'passing',
},
],
[
{
Status: 'passing',
},
],
].forEach(function(checks) {
const actual = sumOfUnhealthy(checks);
assert.equal(actual, expected);
});
});
test('it returns the correct multiple count', function(assert) {
const expected = 3;
[
[
{
Status: 'critical',
},
{
Status: 'warning',
},
{
Status: 'warning',
},
{
Status: 'passing',
},
],
[
{
Status: 'passing',
},
{
Status: 'critical',
},
{
Status: 'passing',
},
{
Status: 'warning',
},
{
Status: 'warning',
},
{
Status: 'passing',
},
],
].forEach(function(checks) {
const actual = sumOfUnhealthy(checks);
assert.equal(actual, expected);
});
});
skip('it works as a factory, passing ember `get` in to create the function');

View File

@ -17,6 +17,48 @@
ember-cli-babel "^6.10.0"
ember-cli-htmlbars-inline-precompile "^1.0.0"
"@gardenhq/component-factory@^1.4.0":
version "1.4.0"
resolved "https://registry.yarnpkg.com/@gardenhq/component-factory/-/component-factory-1.4.0.tgz#f5da8ddf2050fde9c69f4426d61fe55de043e78d"
dependencies:
"@gardenhq/domino" "^1.0.0"
"@gardenhq/tick-control" "^2.0.0"
classtrophobic-es5 "^0.2.1"
hyperhtml "^0.15.5"
"@gardenhq/domino@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@gardenhq/domino/-/domino-1.0.0.tgz#832c493f3f05697b7df4ccce00c4cf620dc60923"
optionalDependencies:
min-document "^2.19.0"
unfetch "^2.1.2"
xhr2 "^0.1.4"
"@gardenhq/o@^8.0.1":
version "8.0.1"
resolved "https://registry.yarnpkg.com/@gardenhq/o/-/o-8.0.1.tgz#d6772cec7e4295a951165284cf43fbd0a373b779"
dependencies:
"@gardenhq/component-factory" "^1.4.0"
"@gardenhq/tick-control" "^2.0.0"
"@gardenhq/willow" "^6.2.0"
babel-standalone "^6.24.2"
file-saver "^1.3.3"
mousetrap "^1.6.1"
ncp "^2.0.0"
rollup "^0.41.6"
rollup-plugin-memory "^2.0.0"
uglify-es "^3.0.17"
optionalDependencies:
js-yaml "^3.8.4"
"@gardenhq/tick-control@^2.0.0":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@gardenhq/tick-control/-/tick-control-2.0.0.tgz#f84fe38ca7a09b7b2b52f42945c50429ba639897"
"@gardenhq/willow@^6.2.0":
version "6.2.0"
resolved "https://registry.yarnpkg.com/@gardenhq/willow/-/willow-6.2.0.tgz#3e4bc220a89099732746ead3385cc097bfb70186"
"@glimmer/di@^0.2.0":
version "0.2.0"
resolved "https://registry.yarnpkg.com/@glimmer/di/-/di-0.2.0.tgz#73bfd4a6ee4148a80bf092e8a5d29bcac9d4ce7e"
@ -27,12 +69,46 @@
dependencies:
"@glimmer/di" "^0.2.0"
"@hashicorp/api-double@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@hashicorp/api-double/-/api-double-1.1.0.tgz#299d3c560090dfe9c335db64d63c3ef0c5da79c4"
dependencies:
"@gardenhq/o" "^8.0.1"
"@gardenhq/tick-control" "^2.0.0"
array-range "^1.0.1"
cookie-parser "^1.4.3"
express "^4.16.2"
faker "^4.1.0"
js-yaml "^3.10.0"
"@hashicorp/consul-api-double@^1.0.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@hashicorp/consul-api-double/-/consul-api-double-1.1.0.tgz#658f9e89208fa23f251ca66c66aeb7241a13f23f"
"@hashicorp/ember-cli-api-double@^1.0.2":
version "1.0.2"
resolved "https://registry.yarnpkg.com/@hashicorp/ember-cli-api-double/-/ember-cli-api-double-1.0.2.tgz#684d418cc2a981254cc23035ceb452c86f0cd934"
dependencies:
"@hashicorp/api-double" "^1.1.0"
array-range "^1.0.1"
ember-cli-babel "^6.6.0"
js-yaml "^3.11.0"
pretender "^2.0.0"
"@sinonjs/formatio@^2.0.0":
version "2.0.0"
resolved "http://registry.npmjs.org/@sinonjs/formatio/-/formatio-2.0.0.tgz#84db7e9eb5531df18a8c5e0bfb6e449e55e654b2"
dependencies:
samsam "1.3.0"
"@types/estree@0.0.38":
version "0.0.38"
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.38.tgz#c1be40aa933723c608820a99a373a16d215a1ca2"
"@types/node@*":
version "10.0.8"
resolved "https://registry.yarnpkg.com/@types/node/-/node-10.0.8.tgz#37b4d91d4e958e4c2ba0be2b86e7ed4ff19b0858"
JSONStream@^1.0.3:
version "1.3.2"
resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.2.tgz#c102371b6ec3a7cf3b847ca00c20bb0fce4c6dea"
@ -257,6 +333,10 @@ array-map@~0.0.0:
version "0.0.0"
resolved "https://registry.yarnpkg.com/array-map/-/array-map-0.0.0.tgz#88a2bab73d1cf7bcd5c1b118a003f66f665fa662"
array-range@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/array-range/-/array-range-1.0.1.tgz#f56e46591843611c6a56f77ef02eda7c50089bfc"
array-reduce@~0.0.0:
version "0.0.0"
resolved "https://registry.yarnpkg.com/array-reduce/-/array-reduce-0.0.0.tgz#173899d3ffd1c7d9383e4479525dbe278cab5f2b"
@ -1023,6 +1103,10 @@ babel-runtime@^6.18.0, babel-runtime@^6.22.0, babel-runtime@^6.26.0:
core-js "^2.4.0"
regenerator-runtime "^0.11.0"
babel-standalone@^6.24.2:
version "6.26.0"
resolved "https://registry.yarnpkg.com/babel-standalone/-/babel-standalone-6.26.0.tgz#15fb3d35f2c456695815ebf1ed96fe7f015b6886"
babel-template@^6.16.0, babel-template@^6.24.1, babel-template@^6.26.0:
version "6.26.0"
resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.26.0.tgz#de03e2d16396b069f46dd9fff8521fb1a0e35e02"
@ -1391,7 +1475,7 @@ broccoli-config-replace@^1.1.2:
debug "^2.2.0"
fs-extra "^0.24.0"
broccoli-debug@^0.6.1, broccoli-debug@^0.6.2, broccoli-debug@^0.6.3:
broccoli-debug@^0.6.1, broccoli-debug@^0.6.2, broccoli-debug@^0.6.3, broccoli-debug@^0.6.4:
version "0.6.4"
resolved "https://registry.yarnpkg.com/broccoli-debug/-/broccoli-debug-0.6.4.tgz#986eb3d2005e00e3bb91f9d0a10ab137210cd150"
dependencies:
@ -1842,6 +1926,10 @@ builtin-modules@^1.0.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f"
builtin-modules@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-2.0.0.tgz#60b7ef5ae6546bd7deefa74b08b62a43a232648e"
builtin-status-codes@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8"
@ -2058,6 +2146,10 @@ class-utils@^0.3.5:
isobject "^3.0.0"
static-extend "^0.1.1"
classtrophobic-es5@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/classtrophobic-es5/-/classtrophobic-es5-0.2.1.tgz#9bbfa62a9928abf26f385440032fb49da1cda88f"
clean-base-url@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/clean-base-url/-/clean-base-url-1.0.0.tgz#c901cf0a20b972435b0eccd52d056824a4351b7b"
@ -2376,6 +2468,13 @@ convert-source-map@~1.1.0:
version "1.1.3"
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.1.3.tgz#4829c877e9fe49b3161f3bf3673888e204699860"
cookie-parser@^1.4.3:
version "1.4.3"
resolved "https://registry.yarnpkg.com/cookie-parser/-/cookie-parser-1.4.3.tgz#0fe31fa19d000b95f4aadf1f53fdc2b8a203baa5"
dependencies:
cookie "0.3.1"
cookie-signature "1.0.6"
cookie-signature@1.0.6:
version "1.0.6"
resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
@ -2798,6 +2897,10 @@ dom-serializer@0:
domelementtype "~1.1.1"
entities "~1.1.1"
dom-walk@^0.1.0:
version "0.1.1"
resolved "https://registry.yarnpkg.com/dom-walk/-/dom-walk-0.1.1.tgz#672226dc74c8f799ad35307df936aba11acd6018"
domain-browser@~1.1.0:
version "1.1.7"
resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.1.7.tgz#867aa4b093faa05f1de08c06f4d7b21fdf8698bc"
@ -2992,6 +3095,21 @@ ember-cli-broccoli-sane-watcher@^2.0.4:
rsvp "^3.0.18"
sane "^2.4.1"
ember-cli-cjs-transform@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/ember-cli-cjs-transform/-/ember-cli-cjs-transform-1.2.0.tgz#34a0d2667673caec0248f500a954f45668027e8b"
dependencies:
broccoli-debug "^0.6.4"
broccoli-plugin "^1.3.0"
ember-cli-babel "^6.6.0"
fs-extra "^5.0.0"
hash-for-dep "^1.2.3"
pkg-dir "^2.0.0"
rollup "^0.58.1"
rollup-plugin-commonjs "^9.1.0"
rollup-plugin-node-resolve "^3.3.0"
username "^3.0.0"
ember-cli-clipboard@^0.9.0:
version "0.9.0"
resolved "https://registry.yarnpkg.com/ember-cli-clipboard/-/ember-cli-clipboard-0.9.0.tgz#c0cfce1a8a81ba1646e54bff9d41249b8bc507f7"
@ -3270,6 +3388,14 @@ ember-cli-version-checker@^2.0.0, ember-cli-version-checker@^2.1.0:
resolve "^1.3.3"
semver "^5.3.0"
ember-cli-yadda@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/ember-cli-yadda/-/ember-cli-yadda-0.4.0.tgz#0faaa3a4d945b3fc0974d0535e358c2897fdf170"
dependencies:
broccoli-persistent-filter "^1.4.3"
ember-cli-babel "^6.6.0"
yadda "*"
ember-cli@~2.18.2:
version "2.18.2"
resolved "https://registry.yarnpkg.com/ember-cli/-/ember-cli-2.18.2.tgz#bb15313a15139a85248a86d203643f918ba40f57"
@ -3896,6 +4022,10 @@ estraverse@^4.0.0, estraverse@^4.1.0, estraverse@^4.1.1:
version "4.2.0"
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13"
estree-walker@^0.5.1, estree-walker@^0.5.2:
version "0.5.2"
resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-0.5.2.tgz#d3850be7529c9580d815600b53126515e146dd39"
esutils@^2.0.0, esutils@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b"
@ -3956,6 +4086,18 @@ execa@^0.10.0:
signal-exit "^3.0.0"
strip-eof "^1.0.0"
execa@^0.7.0:
version "0.7.0"
resolved "https://registry.yarnpkg.com/execa/-/execa-0.7.0.tgz#944becd34cc41ee32a63a9faf27ad5a65fc59777"
dependencies:
cross-spawn "^5.0.1"
get-stream "^3.0.0"
is-stream "^1.1.0"
npm-run-path "^2.0.0"
p-finally "^1.0.0"
signal-exit "^3.0.0"
strip-eof "^1.0.0"
execa@^0.8.0:
version "0.8.0"
resolved "https://registry.yarnpkg.com/execa/-/execa-0.8.0.tgz#d8d76bbc1b55217ed190fd6dd49d3c774ecfc8da"
@ -4024,7 +4166,7 @@ expand-tilde@^2.0.0, expand-tilde@^2.0.2:
dependencies:
homedir-polyfill "^1.0.1"
express@^4.10.7, express@^4.12.3:
express@^4.10.7, express@^4.12.3, express@^4.16.2:
version "4.16.3"
resolved "https://registry.yarnpkg.com/express/-/express-4.16.3.tgz#6af8a502350db3246ecc4becf6b5a34d22f7ed53"
dependencies:
@ -4123,6 +4265,14 @@ eyes@0.1.x:
version "0.1.8"
resolved "https://registry.yarnpkg.com/eyes/-/eyes-0.1.8.tgz#62cf120234c683785d902348a800ef3e0cc20bc0"
fake-xml-http-request@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/fake-xml-http-request/-/fake-xml-http-request-2.0.0.tgz#41a92f0ca539477700cb1dafd2df251d55dac8ff"
faker@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/faker/-/faker-4.1.0.tgz#1e45bbbecc6774b3c195fad2835109c6d748cc3f"
fast-deep-equal@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz#c053477817c86b51daa853c81e059b733d023614"
@ -4192,6 +4342,10 @@ file-entry-cache@^2.0.0:
flat-cache "^1.2.1"
object-assign "^4.0.1"
file-saver@^1.3.3:
version "1.3.8"
resolved "https://registry.yarnpkg.com/file-saver/-/file-saver-1.3.8.tgz#e68a30c7cb044e2fb362b428469feb291c2e09d8"
filename-regex@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/filename-regex/-/filename-regex-2.0.1.tgz#c1c4b9bee3e09725ddb106b75c1e301fe2f18b26"
@ -4795,7 +4949,7 @@ hash-base@^3.0.0:
inherits "^2.0.1"
safe-buffer "^5.0.1"
hash-for-dep@^1.0.2:
hash-for-dep@^1.0.2, hash-for-dep@^1.2.3:
version "1.2.3"
resolved "https://registry.yarnpkg.com/hash-for-dep/-/hash-for-dep-1.2.3.tgz#5ec69fca32c23523972d52acb5bb65ffc3664cab"
dependencies:
@ -4960,6 +5114,10 @@ husky@^0.14.3:
normalize-path "^1.0.0"
strip-indent "^2.0.0"
hyperhtml@^0.15.5:
version "0.15.10"
resolved "https://registry.yarnpkg.com/hyperhtml/-/hyperhtml-0.15.10.tgz#5e5f42393d4fc30cd803063fb88a5c9d97625e1c"
iconv-lite@0.4.19:
version "0.4.19"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b"
@ -5259,6 +5417,10 @@ is-integer@^1.0.4:
dependencies:
is-finite "^1.0.0"
is-module@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591"
is-my-ip-valid@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-my-ip-valid/-/is-my-ip-valid-1.0.0.tgz#7b351b8e8edd4d3995d4d066680e664d94696824"
@ -5542,7 +5704,7 @@ js-yaml@0.3.x:
version "0.3.7"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-0.3.7.tgz#d739d8ee86461e54b354d6a7d7d1f2ad9a167f62"
js-yaml@^3.2.5, js-yaml@^3.2.7, js-yaml@^3.6.1, js-yaml@^3.7.0, js-yaml@^3.9.0, js-yaml@^3.9.1:
js-yaml@^3.10.0, js-yaml@^3.11.0, js-yaml@^3.2.5, js-yaml@^3.2.7, js-yaml@^3.6.1, js-yaml@^3.7.0, js-yaml@^3.8.4, js-yaml@^3.9.0, js-yaml@^3.9.1:
version "3.11.0"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.11.0.tgz#597c1a8bd57152f26d622ce4117851a51f5ebaef"
dependencies:
@ -6251,6 +6413,12 @@ lru-cache@^4.0.1:
pseudomap "^1.0.2"
yallist "^2.1.2"
magic-string@^0.22.4:
version "0.22.5"
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.22.5.tgz#8e9cf5afddf44385c1da5bc2a6a0dbd10b03657e"
dependencies:
vlq "^0.2.2"
make-dir@^1.0.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.2.0.tgz#6d6a49eead4aae296c53bbf3a1a008bd6c89469b"
@ -6338,6 +6506,12 @@ media-typer@0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
mem@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/mem/-/mem-1.1.0.tgz#5edd52b485ca1d900fe64895505399a0dfa45f76"
dependencies:
mimic-fn "^1.0.0"
memory-streams@^0.1.0:
version "0.1.3"
resolved "https://registry.yarnpkg.com/memory-streams/-/memory-streams-0.1.3.tgz#d9b0017b4b87f1d92f55f2745c9caacb1dc93ceb"
@ -6382,7 +6556,7 @@ methods@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
micromatch@^2.1.5, micromatch@^2.3.7:
micromatch@^2.1.5, micromatch@^2.3.11, micromatch@^2.3.7:
version "2.3.11"
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-2.3.11.tgz#86677c97d1720b363431d04d0d15293bd38c1565"
dependencies:
@ -6443,6 +6617,12 @@ mimic-fn@^1.0.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022"
min-document@^2.19.0:
version "2.19.0"
resolved "https://registry.yarnpkg.com/min-document/-/min-document-2.19.0.tgz#7bd282e3f5842ed295bb748cdd9f1ffa2c824685"
dependencies:
dom-walk "^0.1.0"
minimalistic-assert@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7"
@ -6530,6 +6710,10 @@ morgan@^1.8.1:
on-finished "~2.3.0"
on-headers "~1.0.1"
mousetrap@^1.6.1:
version "1.6.1"
resolved "https://registry.yarnpkg.com/mousetrap/-/mousetrap-1.6.1.tgz#2a085f5c751294c75e7e81f6ec2545b29cbf42d9"
mout@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/mout/-/mout-1.1.0.tgz#0b29d41e6a80fa9e2d4a5be9d602e1d9d02177f6"
@ -6579,6 +6763,10 @@ natural-compare@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
ncp@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ncp/-/ncp-2.0.0.tgz#195a21d6c46e361d2fb1281ba38b91e9df7bdbb3"
negotiator@0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9"
@ -7151,6 +7339,12 @@ pinkie@^2.0.0:
version "2.0.4"
resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870"
pkg-dir@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-2.0.0.tgz#f6d5d1109e19d63edf428e0bd57e12777615334b"
dependencies:
find-up "^2.1.0"
pluralize@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-7.0.0.tgz#298b89df8b93b0221dbf421ad2b1b1ea23fc6777"
@ -7187,6 +7381,13 @@ preserve@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b"
pretender@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/pretender/-/pretender-2.0.0.tgz#5adae189f1d5b25f86113f9225df25bed54f4072"
dependencies:
fake-xml-http-request "^2.0.0"
route-recognizer "^0.3.3"
prettier@^1.10.2:
version "1.12.0"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.12.0.tgz#d26fc5894b9230de97629b39cae225b503724ce8"
@ -7749,12 +7950,52 @@ ripemd160@^2.0.0, ripemd160@^2.0.1:
hash-base "^3.0.0"
inherits "^2.0.1"
rollup@^0.41.4:
rollup-plugin-commonjs@^9.1.0:
version "9.1.3"
resolved "https://registry.yarnpkg.com/rollup-plugin-commonjs/-/rollup-plugin-commonjs-9.1.3.tgz#37bfbf341292ea14f512438a56df8f9ca3ba4d67"
dependencies:
estree-walker "^0.5.1"
magic-string "^0.22.4"
resolve "^1.5.0"
rollup-pluginutils "^2.0.1"
rollup-plugin-memory@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/rollup-plugin-memory/-/rollup-plugin-memory-2.0.0.tgz#0a8ac6b57fa0e714f89a15c3ac82bc93f89c47c5"
rollup-plugin-node-resolve@^3.3.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/rollup-plugin-node-resolve/-/rollup-plugin-node-resolve-3.3.0.tgz#c26d110a36812cbefa7ce117cadcd3439aa1c713"
dependencies:
builtin-modules "^2.0.0"
is-module "^1.0.0"
resolve "^1.1.6"
rollup-pluginutils@^2.0.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/rollup-pluginutils/-/rollup-pluginutils-2.1.1.tgz#50289c94f8d7426647b14dc9fbb93643645a8d09"
dependencies:
estree-walker "^0.5.2"
micromatch "^2.3.11"
tosource "^1.0.0"
rollup@^0.41.4, rollup@^0.41.6:
version "0.41.6"
resolved "https://registry.yarnpkg.com/rollup/-/rollup-0.41.6.tgz#e0d05497877a398c104d816d2733a718a7a94e2a"
dependencies:
source-map-support "^0.4.0"
rollup@^0.58.1:
version "0.58.2"
resolved "https://registry.yarnpkg.com/rollup/-/rollup-0.58.2.tgz#2feddea8c0c022f3e74b35c48e3c21b3433803ce"
dependencies:
"@types/estree" "0.0.38"
"@types/node" "*"
route-recognizer@^0.3.3:
version "0.3.3"
resolved "https://registry.yarnpkg.com/route-recognizer/-/route-recognizer-0.3.3.tgz#1d365e27fa6995e091675f7dc940a8c00353bd29"
rsvp@^3.0.14, rsvp@^3.0.16, rsvp@^3.0.17, rsvp@^3.0.18, rsvp@^3.0.21, rsvp@^3.0.6, rsvp@^3.1.0, rsvp@^3.2.1, rsvp@^3.3.3, rsvp@^3.5.0:
version "3.6.2"
resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-3.6.2.tgz#2e96491599a96cde1b515d5674a8f7a91452926a"
@ -8684,6 +8925,10 @@ to-regex@^3.0.1, to-regex@^3.0.2:
regex-not "^1.0.2"
safe-regex "^1.1.0"
tosource@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/tosource/-/tosource-1.0.0.tgz#42d88dd116618bcf00d6106dd5446f3427902ff1"
tough-cookie@~2.3.0, tough-cookie@~2.3.3:
version "2.3.4"
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.4.tgz#ec60cee38ac675063ffc97a5c18970578ee83655"
@ -8765,7 +9010,7 @@ uc.micro@^1.0.1, uc.micro@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.5.tgz#0c65f15f815aa08b560a61ce8b4db7ffc3f45376"
uglify-es@^3.1.3:
uglify-es@^3.0.17, uglify-es@^3.1.3:
version "3.3.9"
resolved "https://registry.yarnpkg.com/uglify-es/-/uglify-es-3.3.9.tgz#0c1c4f0700bed8dbc124cdb304d2592ca203e677"
dependencies:
@ -8812,6 +9057,10 @@ underscore@>=1.8.3:
version "1.8.3"
resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.8.3.tgz#4f3fb53b106e6097fcf9cb4109f2a5e9bdfa5022"
unfetch@^2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/unfetch/-/unfetch-2.1.2.tgz#684fee4d8acdb135bdb26c0364c642fc326ca95b"
union-value@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.0.tgz#5c71c34cb5bad5dcebe3ea0cd08207ba5aa1aea4"
@ -8891,6 +9140,13 @@ username@^1.0.1:
dependencies:
meow "^3.4.0"
username@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/username/-/username-3.0.0.tgz#b3dba982a72b4ce59d52f159fa1aeba266af5fc8"
dependencies:
execa "^0.7.0"
mem "^1.1.0"
util-deprecate@^1.0.2, util-deprecate@~1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
@ -8941,6 +9197,10 @@ verror@1.10.0:
core-util-is "1.0.2"
extsprintf "^1.2.0"
vlq@^0.2.2:
version "0.2.3"
resolved "https://registry.yarnpkg.com/vlq/-/vlq-0.2.3.tgz#8f3e4328cf63b1540c0d67e1b2778386f8975b26"
vm-browserify@~0.0.1:
version "0.0.4"
resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-0.0.4.tgz#5d7ea45bbef9e4a6ff65f95438e0a87c357d5a73"
@ -9085,6 +9345,10 @@ xdg-basedir@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-3.0.0.tgz#496b2cc109eca8dbacfe2dc72b603c17c5870ad4"
xhr2@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/xhr2/-/xhr2-0.1.4.tgz#7f87658847716db5026323812f818cadab387a5f"
xmldom@^0.1.19:
version "0.1.27"
resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.1.27.tgz#d501f97b3bdb403af8ef9ecc20573187aadac0e9"
@ -9101,6 +9365,10 @@ y18n@^3.2.0, y18n@^3.2.1:
version "3.2.1"
resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41"
yadda@*:
version "1.4.0"
resolved "https://registry.yarnpkg.com/yadda/-/yadda-1.4.0.tgz#75b87196f4a864c4131705131fbbc2df3367b58f"
yallist@^2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"