Merge pull request #5729 from hashicorp/ui-staging

UI: ui-staging merge
This commit is contained in:
John Cowen 2019-05-01 20:48:14 +01:00 committed by GitHub
commit e31c285565
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
508 changed files with 12113 additions and 3186 deletions

1
ui-v2/.eslintignore Normal file
View File

@ -0,0 +1 @@
app/utils/dom/event-target/event-target-shim/event.js

View File

@ -25,6 +25,9 @@ lint: deps
format: deps
yarn run format:js
steps:
yarn run steps:list
node_modules: yarn.lock package.json
yarn install

View File

@ -1,9 +1,12 @@
import Adapter from 'ember-data/adapters/rest';
import { AbortError } from 'ember-data/adapters/errors';
import { inject as service } from '@ember/service';
import { get } from '@ember/object';
import URL from 'url';
import createURL from 'consul-ui/utils/createURL';
import { FOREIGN_KEY as DATACENTER_KEY } from 'consul-ui/models/dc';
import { HEADERS_SYMBOL as HTTP_HEADERS_SYMBOL } from 'consul-ui/utils/http/consul';
export const REQUEST_CREATE = 'createRecord';
export const REQUEST_READ = 'queryRecord';
@ -16,12 +19,68 @@ export const DATACENTER_QUERY_PARAM = 'dc';
export default Adapter.extend({
namespace: 'v1',
repo: service('settings'),
client: service('client/http'),
manageConnection: function(options) {
const client = get(this, 'client');
const complete = options.complete;
const beforeSend = options.beforeSend;
options.beforeSend = function(xhr) {
if (typeof beforeSend === 'function') {
beforeSend(...arguments);
}
options.id = client.request(options, xhr);
};
options.complete = function(xhr, textStatus) {
client.complete(options.id);
if (typeof complete === 'function') {
complete(...arguments);
}
};
return options;
},
_ajaxRequest: function(options) {
return this._super(this.manageConnection(options));
},
queryRecord: function() {
return this._super(...arguments).catch(function(e) {
if (e instanceof AbortError) {
e.errors[0].status = '0';
}
throw e;
});
},
query: function() {
return this._super(...arguments).catch(function(e) {
if (e instanceof AbortError) {
e.errors[0].status = '0';
}
throw e;
});
},
headersForRequest: function(params) {
return {
...this.get('repo').findHeaders(),
...this._super(...arguments),
};
},
handleResponse: function(status, headers, response, requestData) {
// The ember-data RESTAdapter drops the headers after this call,
// and there is no where else to get to these
// save them to response[HTTP_HEADERS_SYMBOL] for the moment
// so we can save them as meta in the serializer...
if (
(typeof response == 'object' && response.constructor == Object) ||
Array.isArray(response)
) {
// lowercase everything incase we get browser inconsistencies
const lower = {};
Object.keys(headers).forEach(function(key) {
lower[key.toLowerCase()] = headers[key];
});
response[HTTP_HEADERS_SYMBOL] = lower;
}
return this._super(status, headers, response, requestData);
},
handleBooleanResponse: function(url, response, primary, slug) {
return {
// consider a check for a boolean, also for future me,
@ -52,6 +111,12 @@ export default Adapter.extend({
delete _query.id;
}
const query = { ..._query };
if (typeof query.separator !== 'undefined') {
delete query.separator;
}
if (typeof query.index !== 'undefined') {
delete query.index;
}
delete _query[DATACENTER_QUERY_PARAM];
return query;
},

View File

@ -136,6 +136,7 @@ export default Adapter.extend({
}
return null;
}
return data;
},
methodForRequest: function(params) {
switch (params.requestType) {

View File

@ -0,0 +1,20 @@
import Adapter from './application';
import { PRIMARY_KEY, SLUG_KEY } from 'consul-ui/models/proxy';
import { OK as HTTP_OK } from 'consul-ui/utils/http/status';
export default Adapter.extend({
urlForQuery: function(query, modelName) {
if (typeof query.id === 'undefined') {
throw new Error('You must specify an id');
}
// https://www.consul.io/api/catalog.html#list-nodes-for-connect-capable-service
return this.appendURL('catalog/connect', [query.id], this.cleanQuery(query));
},
handleResponse: function(status, headers, payload, requestData) {
let response = payload;
if (status === HTTP_OK) {
const url = this.parseURL(requestData.url);
response = this.handleBatchResponse(url, response, PRIMARY_KEY, SLUG_KEY);
}
return this._super(status, headers, response, requestData);
},
});

View File

@ -0,0 +1,72 @@
import Adapter, {
REQUEST_CREATE,
REQUEST_UPDATE,
DATACENTER_QUERY_PARAM as API_DATACENTER_KEY,
} from './application';
import { PRIMARY_KEY, SLUG_KEY } from 'consul-ui/models/role';
import { FOREIGN_KEY as DATACENTER_KEY } from 'consul-ui/models/dc';
import { OK as HTTP_OK } from 'consul-ui/utils/http/status';
import { PUT as HTTP_PUT } from 'consul-ui/utils/http/method';
import WithPolicies from 'consul-ui/mixins/policy/as-many';
export default Adapter.extend(WithPolicies, {
urlForQuery: function(query, modelName) {
return this.appendURL('acl/roles', [], this.cleanQuery(query));
},
urlForQueryRecord: function(query, modelName) {
if (typeof query.id === 'undefined') {
throw new Error('You must specify an id');
}
return this.appendURL('acl/role', [query.id], this.cleanQuery(query));
},
urlForCreateRecord: function(modelName, snapshot) {
return this.appendURL('acl/role', [], {
[API_DATACENTER_KEY]: snapshot.attr(DATACENTER_KEY),
});
},
urlForUpdateRecord: function(id, modelName, snapshot) {
return this.appendURL('acl/role', [snapshot.attr(SLUG_KEY)], {
[API_DATACENTER_KEY]: snapshot.attr(DATACENTER_KEY),
});
},
urlForDeleteRecord: function(id, modelName, snapshot) {
return this.appendURL('acl/role', [snapshot.attr(SLUG_KEY)], {
[API_DATACENTER_KEY]: snapshot.attr(DATACENTER_KEY),
});
},
handleResponse: function(status, headers, payload, requestData) {
let response = payload;
if (status === HTTP_OK) {
const url = this.parseURL(requestData.url);
switch (true) {
case response === true:
response = this.handleBooleanResponse(url, response, PRIMARY_KEY, SLUG_KEY);
break;
case Array.isArray(response):
response = this.handleBatchResponse(url, response, PRIMARY_KEY, SLUG_KEY);
break;
default:
response = this.handleSingleResponse(url, response, PRIMARY_KEY, SLUG_KEY);
}
}
return this._super(status, headers, response, requestData);
},
methodForRequest: function(params) {
switch (params.requestType) {
case REQUEST_CREATE:
return HTTP_PUT;
}
return this._super(...arguments);
},
dataForRequest: function(params) {
const data = this._super(...arguments);
switch (params.requestType) {
case REQUEST_UPDATE:
case REQUEST_CREATE:
return data.role;
}
return data;
},
});

View File

@ -10,12 +10,15 @@ import { FOREIGN_KEY as DATACENTER_KEY } from 'consul-ui/models/dc';
import { OK as HTTP_OK } from 'consul-ui/utils/http/status';
import { PUT as HTTP_PUT } from 'consul-ui/utils/http/method';
import WithPolicies from 'consul-ui/mixins/policy/as-many';
import WithRoles from 'consul-ui/mixins/role/as-many';
import { get } from '@ember/object';
const REQUEST_CLONE = 'cloneRecord';
const REQUEST_SELF = 'querySelf';
export default Adapter.extend({
export default Adapter.extend(WithRoles, WithPolicies, {
store: service('store'),
cleanQuery: function(_query) {
const query = this._super(...arguments);
@ -108,10 +111,6 @@ export default Adapter.extend({
return this._makeRequest(request);
},
handleSingleResponse: function(url, response, primary, slug) {
// Sometimes we get `Policies: null`, make null equal an empty array
if (typeof response.Policies === 'undefined' || response.Policies === null) {
response.Policies = [];
}
// Convert an old style update response to a new style
if (typeof response['ID'] !== 'undefined') {
const item = get(this, 'store')
@ -169,19 +168,6 @@ export default Adapter.extend({
}
// falls through
case REQUEST_CREATE:
if (Array.isArray(data.token.Policies)) {
data.token.Policies = data.token.Policies.filter(function(item) {
// Just incase, don't save any policies that aren't saved
return !get(item, 'isNew');
}).map(function(item) {
return {
ID: get(item, 'ID'),
Name: get(item, 'Name'),
};
});
} else {
delete data.token.Policies;
}
data = data.token;
break;
case REQUEST_SELF:

View File

@ -1,31 +1,33 @@
import Component from '@ember/component';
import SlotsMixin from 'block-slots';
import { get } from '@ember/object';
import { inject as service } from '@ember/service';
import templatize from 'consul-ui/utils/templatize';
const $html = document.documentElement;
export default Component.extend(SlotsMixin, {
loading: false,
authorized: true,
enabled: true,
classNames: ['app-view'],
classNameBindings: ['enabled::disabled', 'authorized::unauthorized'],
dom: service('dom'),
didReceiveAttrs: function() {
// right now only manually added classes are hoisted to <html>
const $root = get(this, 'dom').root();
let cls = get(this, 'class') || '';
if (get(this, 'loading')) {
cls += ' loading';
} else {
$html.classList.remove(...templatize(['loading']));
$root.classList.remove(...templatize(['loading']));
}
if (cls) {
// its possible for 'layout' templates to change after insert
// check for these specific layouts and clear them out
[...$html.classList].forEach(function(item, i) {
[...$root.classList].forEach(function(item, i) {
if (templatize(['edit', 'show', 'list']).indexOf(item) !== -1) {
$html.classList.remove(item);
$root.classList.remove(item);
}
});
$html.classList.add(...templatize(cls.split(' ')));
$root.classList.add(...templatize(cls.split(' ')));
}
},
didInsertElement: function() {
@ -34,7 +36,8 @@ export default Component.extend(SlotsMixin, {
didDestroyElement: function() {
const cls = get(this, 'class') + ' loading';
if (cls) {
$html.classList.remove(...templatize(cls.split(' ')));
const $root = get(this, 'dom').root();
$root.classList.remove(...templatize(cls.split(' ')));
}
},
});

View File

@ -0,0 +1,19 @@
import Component from '@ember/component';
import { get, set } from '@ember/object';
import SlotsMixin from 'block-slots';
import WithListeners from 'consul-ui/mixins/with-listeners';
export default Component.extend(WithListeners, SlotsMixin, {
tagName: '',
didReceiveAttrs: function() {
this._super(...arguments);
this.removeListeners();
const dispatcher = get(this, 'dispatcher');
if (dispatcher) {
this.listen(dispatcher, 'change', e => {
set(this, 'items', e.target.data);
});
set(this, 'items', get(dispatcher, 'data'));
}
},
});

View File

@ -0,0 +1,113 @@
import Component from '@ember/component';
import { get, set, computed } from '@ember/object';
import { alias } from '@ember/object/computed';
import { inject as service } from '@ember/service';
import { Promise } from 'rsvp';
import SlotsMixin from 'block-slots';
import WithListeners from 'consul-ui/mixins/with-listeners';
export default Component.extend(SlotsMixin, WithListeners, {
onchange: function() {},
error: function() {},
type: '',
dom: service('dom'),
container: service('search'),
formContainer: service('form'),
item: alias('form.data'),
selectedOptions: alias('items'),
init: function() {
this._super(...arguments);
this.searchable = get(this, 'container').searchable(get(this, 'type'));
this.form = get(this, 'formContainer').form(get(this, 'type'));
this.form.clear({ Datacenter: get(this, 'dc') });
},
options: computed('selectedOptions.[]', 'allOptions.[]', function() {
// It's not massively important here that we are defaulting `items` and
// losing reference as its just to figure out the diff
let options = get(this, 'allOptions') || [];
const items = get(this, 'selectedOptions') || [];
if (get(items, 'length') > 0) {
// find a proper ember-data diff
options = options.filter(item => !items.findBy('ID', get(item, 'ID')));
this.searchable.add(options);
}
return options;
}),
actions: {
search: function(term) {
// TODO: make sure we can either search before things are loaded
// or wait until we are loaded, guess power select take care of that
return new Promise(resolve => {
const remove = this.listen(this.searchable, 'change', function(e) {
remove();
resolve(e.target.data);
});
this.searchable.search(term);
});
},
reset: function() {
get(this, 'form').clear({ Datacenter: get(this, 'dc') });
},
open: function() {
if (!get(this, 'allOptions.closed')) {
set(this, 'allOptions', get(this, 'repo').findAllByDatacenter(get(this, 'dc')));
}
},
save: function(item, items, success = function() {}) {
// Specifically this saves an 'new' option/child
// and then adds it to the selectedOptions, not options
const repo = get(this, 'repo');
set(item, 'CreateTime', new Date().getTime());
// TODO: temporary async
// this should be `set(this, 'item', repo.persist(item));`
// need to be sure that its saved before adding/closing the modal for now
// and we don't open the modal on prop change yet
item = repo.persist(item);
this.listen(item, 'message', e => {
this.actions.change.bind(this)(
{
target: {
name: 'items[]',
value: items,
},
},
items,
e.data
);
success();
});
this.listen(item, 'error', this.error.bind(this));
},
remove: function(item, items) {
const prop = get(this, 'repo').getSlugKey();
const value = get(item, prop);
const pos = items.findIndex(function(item) {
return get(item, prop) === value;
});
if (pos !== -1) {
return items.removeAt(pos, 1);
}
this.onchange({ target: this });
},
change: function(e, value, item) {
const event = get(this, 'dom').normalizeEvent(...arguments);
const items = value;
switch (event.target.name) {
case 'items[]':
set(item, 'CreateTime', new Date().getTime());
// this always happens synchronously
items.pushObject(item);
// TODO: Fire a proper event
this.onchange({ target: this });
break;
default:
}
},
},
});

View File

@ -11,31 +11,57 @@ const DEFAULTS = {
};
export default Component.extend({
settings: service('settings'),
helper: service('code-mirror'),
dom: service('dom'),
helper: service('code-mirror/linter'),
classNames: ['code-editor'],
readonly: false,
syntax: '',
onchange: function(value) {
get(this, 'settings').persist({
'code-editor': value,
});
this.setMode(value);
},
// TODO: Change this to oninput to be consistent? We'll have to do it throughout the templates
onkeyup: function() {},
oninput: function() {},
init: function() {
this._super(...arguments);
set(this, 'modes', get(this, 'helper').modes());
},
didReceiveAttrs: function() {
this._super(...arguments);
const editor = get(this, 'editor');
if (editor) {
editor.setOption('readOnly', get(this, 'readonly'));
}
},
setMode: function(mode) {
set(this, 'options', {
...DEFAULTS,
mode: mode.mime,
readOnly: get(this, 'readonly'),
});
const editor = get(this, 'editor');
editor.setOption('mode', mode.mime);
get(this, 'helper').lint(editor, mode.mode);
set(this, 'mode', mode);
},
willDestroyElement: function() {
this._super(...arguments);
if (this.observer) {
this.observer.disconnect();
}
},
didInsertElement: function() {
this._super(...arguments);
const $code = get(this, 'dom').element('textarea ~ pre code', get(this, 'element'));
if ($code.firstChild) {
this.observer = new MutationObserver(([e]) => {
this.oninput(set(this, 'value', e.target.wholeText));
});
this.observer.observe($code, {
attributes: false,
subtree: true,
childList: false,
characterData: true,
});
set(this, 'value', $code.firstChild.wholeText);
}
set(this, 'editor', get(this, 'helper').getEditor(this.element));
get(this, 'settings')
.findBySlug('code-editor')
@ -54,4 +80,12 @@ export default Component.extend({
didAppear: function() {
get(this, 'editor').refresh();
},
actions: {
change: function(value) {
get(this, 'settings').persist({
'code-editor': value,
});
this.setMode(value);
},
},
});

View File

@ -11,9 +11,11 @@ export default Component.extend({
this.append = append.bind(this);
},
didInsertElement: function() {
this._super(...arguments);
get(this, 'buffer').on('add', this.append);
},
didDestroyElement: function() {
this._super(...arguments);
get(this, 'buffer').off('add', this.append);
},
});

View File

@ -9,9 +9,11 @@ export default Component.extend({
return 'modal';
},
didInsertElement: function() {
this._super(...arguments);
get(this, 'buffer').add(this.getBufferName(), this.element);
},
didDestroyElement: function() {
this._super(...arguments);
get(this, 'buffer').remove(this.getBufferName());
},
});

View File

@ -1,8 +1,7 @@
import Component from '@ember/component';
import { get, set } from '@ember/object';
import { inject as service } from '@ember/service';
import qsaFactory from 'consul-ui/utils/dom/qsa-factory';
const $$ = qsaFactory();
import { Promise } from 'rsvp';
import SlotsMixin from 'block-slots';
const STATE_READY = 'ready';
@ -10,6 +9,7 @@ const STATE_SUCCESS = 'success';
const STATE_ERROR = 'error';
export default Component.extend(SlotsMixin, {
wait: service('timeout'),
dom: service('dom'),
classNames: ['with-feedback'],
transition: '',
transitionClassName: 'feedback-dialog-out',
@ -23,6 +23,7 @@ export default Component.extend(SlotsMixin, {
applyTransition: function() {
const wait = get(this, 'wait').execute;
const className = get(this, 'transitionClassName');
// TODO: Make 0 default in wait
wait(0)
.then(() => {
set(this, 'transition', className);
@ -30,7 +31,9 @@ export default Component.extend(SlotsMixin, {
})
.then(() => {
return new Promise(resolve => {
$$(`.${className}`, this.element)[0].addEventListener('transitionend', resolve);
get(this, 'dom')
.element(`.${className}`, this.element)
.addEventListener('transitionend', resolve);
});
})
.then(() => {

View File

@ -0,0 +1,42 @@
import Component from '@ember/component';
import SlotsMixin from 'block-slots';
import { inject as service } from '@ember/service';
import { get } from '@ember/object';
import { alias } from '@ember/object/computed';
import WithListeners from 'consul-ui/mixins/with-listeners';
// match anything that isn't a [ or ] into multiple groups
const propRe = /([^[\]])+/g;
export default Component.extend(WithListeners, SlotsMixin, {
onreset: function() {},
onchange: function() {},
onerror: function() {},
onsuccess: function() {},
data: alias('form.data'),
item: alias('form.data'),
// TODO: Could probably alias item
// or just use data/value instead
dom: service('dom'),
container: service('form'),
actions: {
change: function(e, value, item) {
let event = get(this, 'dom').normalizeEvent(e, value);
const matches = [...event.target.name.matchAll(propRe)];
const prop = matches[matches.length - 1][0];
event = get(this, 'dom').normalizeEvent(
`${get(this, 'type')}[${prop}]`,
event.target.value,
event.target
);
const form = get(this, 'form');
try {
form.handleEvent(event);
this.onchange({ target: this });
} catch (err) {
throw err;
}
},
},
});

View File

@ -1,7 +1,15 @@
import Component from '@ember/component';
import { get } from '@ember/object';
export default Component.extend({
tagName: 'fieldset',
classNames: ['freetext-filter'],
onchange: function(){}
onchange: function(e) {
let searchable = get(this, 'searchable');
if (!Array.isArray(searchable)) {
searchable = [searchable];
}
searchable.forEach(function(item) {
item.search(e.target.value);
});
},
});

View File

@ -1,11 +1,13 @@
import Component from '@ember/component';
import { get, set } from '@ember/object';
const $html = document.documentElement;
const $body = document.body;
import { inject as service } from '@ember/service';
export default Component.extend({
dom: service('dom'),
isDropdownVisible: false,
didInsertElement: function() {
$html.classList.remove('template-with-vertical-menu');
get(this, 'dom')
.root()
.classList.remove('template-with-vertical-menu');
},
actions: {
dropdown: function(e) {
@ -14,12 +16,16 @@ export default Component.extend({
}
},
change: function(e) {
const dom = get(this, 'dom');
const win = dom.viewport();
const $root = dom.root();
const $body = dom.element('body');
if (e.target.checked) {
$html.classList.add('template-with-vertical-menu');
$body.style.height = $html.style.height = window.innerHeight + 'px';
$root.classList.add('template-with-vertical-menu');
$body.style.height = $root.style.height = win.innerHeight + 'px';
} else {
$html.classList.remove('template-with-vertical-menu');
$body.style.height = $html.style.height = null;
$root.classList.remove('template-with-vertical-menu');
$body.style.height = $root.style.height = null;
}
},
},

View File

@ -0,0 +1,4 @@
import Component from '@ember/component';
export default Component.extend({
tagName: '',
});

View File

@ -0,0 +1,36 @@
import Component from '@ember/component';
import { get } from '@ember/object';
export default Component.extend({
// TODO: Could potentially do this on attr change
actions: {
sortChecksByImportance: function(a, b) {
const statusA = get(a, 'Status');
const statusB = get(b, 'Status');
switch (statusA) {
case 'passing':
// a = passing
// unless b is also passing then a is less important
return statusB === 'passing' ? 0 : 1;
case 'critical':
// a = critical
// unless b is also critical then a is more important
return statusB === 'critical' ? 0 : -1;
case 'warning':
// a = warning
switch (statusB) {
// b is passing so a is more important
case 'passing':
return -1;
// b is critical so a is less important
case 'critical':
return 1;
// a and b are both warning, therefore equal
default:
return 0;
}
}
return 0;
},
},
});

View File

@ -0,0 +1,5 @@
import Component from '@ember/component';
export default Component.extend({
classNames: ['healthcheck-output'],
});

View File

@ -1,5 +1,12 @@
import Component from '@ember/component';
import { get, computed } from '@ember/object';
export default Component.extend({
classNames: ['healthcheck-status'],
tagName: '',
count: computed('value', function() {
const value = get(this, 'value');
if (Array.isArray(value)) {
return value.length;
}
return value;
}),
});

View File

@ -1,11 +1,12 @@
import { inject as service } from '@ember/service';
import { computed, get, set } from '@ember/object';
import Component from 'ember-collection/components/ember-collection';
import PercentageColumns from 'ember-collection/layouts/percentage-columns';
import style from 'ember-computed-style';
import WithResizing from 'consul-ui/mixins/with-resizing';
import qsaFactory from 'consul-ui/utils/dom/qsa-factory';
const $$ = qsaFactory();
export default Component.extend(WithResizing, {
dom: service('dom'),
tagName: 'div',
attributeBindings: ['style'],
height: 500,
@ -30,11 +31,13 @@ export default Component.extend(WithResizing, {
};
}),
resize: function(e) {
const $self = this.element;
const $appContent = [...$$('main > div')][0];
// TODO: This top part is very similar to resize in tabular-collection
// see if it make sense to DRY out
const dom = get(this, 'dom');
const $appContent = dom.element('main > div');
if ($appContent) {
const rect = $self.getBoundingClientRect();
const $footer = [...$$('footer[role="contentinfo"]')][0];
const rect = this.element.getBoundingClientRect();
const $footer = dom.element('footer[role="contentinfo"]');
const space = rect.top + $footer.clientHeight;
const height = e.detail.height - space;
this.set('height', Math.max(0, height));

View File

@ -1,11 +1,11 @@
import { get, set } from '@ember/object';
import { inject as service } from '@ember/service';
import Component from 'consul-ui/components/dom-buffer';
import DomBufferComponent from 'consul-ui/components/dom-buffer';
import SlotsMixin from 'block-slots';
import WithResizing from 'consul-ui/mixins/with-resizing';
import templatize from 'consul-ui/utils/templatize';
export default Component.extend(SlotsMixin, WithResizing, {
export default DomBufferComponent.extend(SlotsMixin, WithResizing, {
dom: service('dom'),
checked: true,
height: null,
@ -38,9 +38,11 @@ export default Component.extend(SlotsMixin, WithResizing, {
_close: function(e) {
set(this, 'checked', false);
const dialogPanel = get(this, 'dialog');
const overflowing = get(this, 'overflowingClass');
if (dialogPanel.classList.contains(overflowing)) {
dialogPanel.classList.remove(overflowing);
if (dialogPanel) {
const overflowing = get(this, 'overflowingClass');
if (dialogPanel.classList.contains(overflowing)) {
dialogPanel.classList.remove(overflowing);
}
}
// TODO: should we make a didDisappear?
get(this, 'dom')
@ -105,15 +107,17 @@ export default Component.extend(SlotsMixin, WithResizing, {
},
actions: {
change: function(e) {
if (e && e.target && e.target.checked) {
if (get(e, 'target.checked')) {
this._open(e);
} else {
this._close();
this._close(e);
}
},
close: function() {
get(this, 'dom').element('#modal_close').checked = true;
this.onclose();
const $close = get(this, 'dom').element('#modal_close');
$close.checked = true;
const $input = get(this, 'dom').element('input[name="modal"]', this.element);
$input.onchange({ target: $input });
},
},
});

View File

@ -1,8 +1,8 @@
import Component from 'consul-ui/components/dom-buffer-flush';
import DomBufferFlushComponent from 'consul-ui/components/dom-buffer-flush';
import { inject as service } from '@ember/service';
import { get } from '@ember/object';
export default Component.extend({
export default DomBufferFlushComponent.extend({
dom: service('dom'),
actions: {
change: function(e) {
@ -10,8 +10,10 @@ export default Component.extend({
.filter(function(item) {
return item.getAttribute('id') !== 'modal_close';
})
.forEach(function(item) {
item.onchange();
.forEach(function(item, i) {
if (item.getAttribute('data-checked') === 'true') {
item.onchange({ target: item });
}
});
},
},

View File

@ -0,0 +1,44 @@
import Component from '@ember/component';
import { get, set } from '@ember/object';
export default Component.extend({
classNames: ['phrase-editor'],
item: '',
remove: function(index, e) {
this.items.removeAt(index, 1);
this.onchange(e);
},
add: function(e) {
const value = get(this, 'item').trim();
if (value !== '') {
set(this, 'item', '');
const currentItems = get(this, 'items') || [];
const items = new Set(currentItems).add(value);
if (items.size > currentItems.length) {
set(this, 'items', [...items]);
this.onchange(e);
}
}
},
onkeydown: function(e) {
switch (e.keyCode) {
case 8:
if (e.target.value == '' && this.items.length > 0) {
this.remove(this.items.length - 1);
}
break;
}
},
oninput: function(e) {
set(this, 'item', e.target.value);
},
onchange: function(e) {
let searchable = get(this, 'searchable');
if (!Array.isArray(searchable)) {
searchable = [searchable];
}
searchable.forEach(item => {
item.search(get(this, 'items'));
});
},
});

View File

@ -0,0 +1,53 @@
import FormComponent from './form-component';
import { inject as service } from '@ember/service';
import { get, set } from '@ember/object';
export default FormComponent.extend({
repo: service('repository/policy/component'),
datacenterRepo: service('repository/dc/component'),
type: 'policy',
name: 'policy',
classNames: ['policy-form'],
isScoped: false,
init: function() {
this._super(...arguments);
set(this, 'isScoped', get(this, 'item.Datacenters.length') > 0);
set(this, 'datacenters', get(this, 'datacenterRepo').findAll());
this.templates = [
{
name: 'Policy',
template: '',
},
{
name: 'Service Identity',
template: 'service-identity',
},
];
},
actions: {
change: function(e) {
try {
this._super(...arguments);
} catch (err) {
const scoped = get(this, 'isScoped');
const name = err.target.name;
switch (name) {
case 'policy[isScoped]':
if (scoped) {
set(this, 'previousDatacenters', get(this.item, 'Datacenters'));
set(this.item, 'Datacenters', null);
} else {
set(this.item, 'Datacenters', get(this, 'previousDatacenters'));
set(this, 'previousDatacenters', null);
}
set(this, 'isScoped', !scoped);
break;
default:
this.onerror(err);
}
this.onchange({ target: get(this, 'form') });
}
},
},
});

View File

@ -0,0 +1,82 @@
import ChildSelectorComponent from './child-selector';
import { get, set } from '@ember/object';
import { inject as service } from '@ember/service';
import updateArrayObject from 'consul-ui/utils/update-array-object';
const ERROR_PARSE_RULES = 'Failed to parse ACL rules';
const ERROR_NAME_EXISTS = 'Invalid Policy: A Policy with Name';
export default ChildSelectorComponent.extend({
repo: service('repository/policy/component'),
datacenterRepo: service('repository/dc/component'),
name: 'policy',
type: 'policy',
classNames: ['policy-selector'],
init: function() {
this._super(...arguments);
const source = get(this, 'source');
if (source) {
const event = 'save';
this.listen(source, event, e => {
this.actions[event].bind(this)(...e.data);
});
}
},
reset: function(e) {
this._super(...arguments);
set(this, 'isScoped', false);
set(this, 'datacenters', get(this, 'datacenterRepo').findAll());
},
refreshCodeEditor: function(e, target) {
const selector = '.code-editor';
get(this, 'dom')
.component(selector, target)
.didAppear();
},
error: function(e) {
const item = get(this, 'item');
const err = e.error;
if (typeof err.errors !== 'undefined') {
const error = err.errors[0];
let prop;
let message = error.detail;
switch (true) {
case message.indexOf(ERROR_PARSE_RULES) === 0:
prop = 'Rules';
message = error.detail;
break;
case message.indexOf(ERROR_NAME_EXISTS) === 0:
prop = 'Name';
message = message.substr(ERROR_NAME_EXISTS.indexOf(':') + 1);
break;
}
if (prop) {
item.addError(prop, message);
}
} else {
// TODO: Conponents can't throw, use onerror
throw err;
}
},
actions: {
loadItem: function(e, item, items) {
const target = e.target;
// the Details expander toggle, only load on opening
if (target.checked) {
const value = item;
this.refreshCodeEditor(e, target.parentNode);
if (get(item, 'template') === 'service-identity') {
return;
}
// potentially the item could change between load, so we don't check
// anything to see if its already loaded here
const repo = get(this, 'repo');
// TODO: Temporarily add dc here, will soon be serialized onto the policy itself
const dc = get(this, 'dc');
const slugKey = repo.getSlugKey();
const slug = get(value, slugKey);
updateArrayObject(items, repo.findBySlug(slug, dc), slugKey, slug);
}
},
},
});

View File

@ -0,0 +1,6 @@
import FormComponent from './form-component';
export default FormComponent.extend({
type: 'role',
name: 'role',
classNames: ['role-form'],
});

View File

@ -0,0 +1,42 @@
import ChildSelectorComponent from './child-selector';
import { inject as service } from '@ember/service';
import { get, set } from '@ember/object';
import { alias } from '@ember/object/computed';
import { CallableEventSource as EventSource } from 'consul-ui/utils/dom/event-source';
export default ChildSelectorComponent.extend({
repo: service('repository/role/component'),
name: 'role',
type: 'role',
classNames: ['role-selector'],
state: 'role',
init: function() {
this._super(...arguments);
this.policyForm = get(this, 'formContainer').form('policy');
this.source = new EventSource();
},
// You have to alias data
// is you just set it it loses its reference?
policy: alias('policyForm.data'),
actions: {
reset: function(e) {
this._super(...arguments);
get(this, 'policyForm').clear({ Datacenter: get(this, 'dc') });
},
dispatch: function(type, data) {
this.source.dispatchEvent({ type: type, data: data });
},
change: function() {
const event = get(this, 'dom').normalizeEvent(...arguments);
switch (event.target.name) {
case 'role[state]':
set(this, 'state', event.target.value);
break;
default:
this._super(...arguments);
}
},
},
});

View File

@ -0,0 +1,5 @@
import Component from '@ember/component';
export default Component.extend({
tagName: '',
});

View File

@ -3,4 +3,5 @@ import Component from '@ember/component';
export default Component.extend({
name: 'tab',
tagName: 'nav',
classNames: ['tab-nav'],
});

View File

@ -1,16 +1,12 @@
import Component from 'ember-collection/components/ember-collection';
import CollectionComponent 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 'block-slots';
import WithResizing from 'consul-ui/mixins/with-resizing';
import style from 'ember-computed-style';
import qsaFactory from 'consul-ui/utils/dom/qsa-factory';
import sibling from 'consul-ui/utils/dom/sibling';
import closest from 'consul-ui/utils/dom/closest';
import clickFirstAnchorFactory from 'consul-ui/utils/dom/click-first-anchor';
const clickFirstAnchor = clickFirstAnchorFactory(closest);
import { inject as service } from '@ember/service';
import { computed, get, set } from '@ember/object';
/**
* Heavily extended `ember-collection` component
@ -24,8 +20,6 @@ import { computed, get, set } from '@ember/object';
* in the future
*/
// ember doesn't like you using `$` hence `$$`
const $$ = qsaFactory();
// 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`
@ -85,13 +79,17 @@ const change = function(e) {
// '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 dom = get(this, 'dom');
const $tr = dom.closest('tr', e.currentTarget);
const $group = dom.sibling(e.currentTarget, 'ul');
const groupRect = $group.getBoundingClientRect();
const footerRect = $footer.getBoundingClientRect();
const groupBottom = groupRect.top + $group.clientHeight;
const $footer = dom.element('footer[role="contentinfo"]');
const footerRect = $footer.getBoundingClientRect();
const footerTop = footerRect.top;
if (groupBottom > footerTop) {
$group.classList.add('above');
} else {
@ -113,39 +111,50 @@ const change = function(e) {
}
}
};
export default Component.extend(SlotsMixin, WithResizing, {
export default CollectionComponent.extend(SlotsMixin, WithResizing, {
tagName: 'table',
classNames: ['dom-recycling'],
classNameBindings: ['hasActions'],
attributeBindings: ['style'],
width: 1150,
height: 500,
rowHeight: 50,
maxHeight: 500,
style: style('getStyle'),
checked: null,
hasCaption: false,
dom: service('dom'),
init: function() {
this._super(...arguments);
this.change = change.bind(this);
this.confirming = [];
// TODO: The row height should auto calculate properly from the CSS
this['cell-layout'] = new ZIndexedGrid(get(this, 'width'), 50);
this['cell-layout'] = new ZIndexedGrid(get(this, 'width'), get(this, 'rowHeight'));
},
getStyle: computed('height', function() {
getStyle: computed('rowHeight', '_items', 'maxRows', 'maxHeight', function() {
const maxRows = get(this, 'rows');
let height = get(this, 'maxHeight');
if (maxRows) {
let rows = Math.max(3, get(this._items || [], 'length'));
rows = Math.min(maxRows, rows);
height = get(this, 'rowHeight') * rows + 29;
}
return {
height: get(this, 'height'),
height: height,
};
}),
resize: function(e) {
const $tbody = this.element;
const $appContent = [...$$('main > div')][0];
const dom = get(this, 'dom');
const $appContent = dom.element('main > div');
if ($appContent) {
const border = 1;
const rect = $tbody.getBoundingClientRect();
const $footer = [...$$('footer[role="contentinfo"]')][0];
const $footer = dom.element('footer[role="contentinfo"]');
const space = rect.top + $footer.clientHeight + border;
const height = e.detail.height - space;
this.set('height', Math.max(0, height));
this.set('maxHeight', Math.max(0, height));
// TODO: The row height should auto calculate properly from the CSS
this['cell-layout'] = new ZIndexedGrid($appContent.clientWidth, 50);
this['cell-layout'] = new ZIndexedGrid($appContent.clientWidth, get(this, 'rowHeight'));
this.updateItems();
this.updateScrollPosition();
}
@ -273,7 +282,7 @@ export default Component.extend(SlotsMixin, WithResizing, {
},
actions: {
click: function(e) {
return clickFirstAnchor(e);
return get(this, 'dom').clickFirstAnchor(e);
},
},
});

View File

@ -1,17 +1,26 @@
import Component from '@ember/component';
import SlotsMixin from 'block-slots';
import closest from 'consul-ui/utils/dom/closest';
import clickFirstAnchorFactory from 'consul-ui/utils/dom/click-first-anchor';
const clickFirstAnchor = clickFirstAnchorFactory(closest);
import { inject as service } from '@ember/service';
import { get, set } from '@ember/object';
import { subscribe } from 'consul-ui/utils/computed/purify';
let uid = 0;
export default Component.extend(SlotsMixin, {
dom: service('dom'),
onchange: function() {},
init: function() {
this._super(...arguments);
set(this, 'uid', uid++);
},
inputId: subscribe('name', 'uid', function(name = 'name') {
return `tabular-details-${name}-toggle-${uid}_`;
}),
actions: {
click: function(e) {
clickFirstAnchor(e);
get(this, 'dom').clickFirstAnchor(e);
},
change: function(item, e) {
this.onchange(e, item);
change: function(item, items, e) {
this.onchange(e, item, items);
},
},
});

View File

@ -0,0 +1,6 @@
import Component from '@ember/component';
export default Component.extend({
tagName: 'dl',
classNames: ['tag-list'],
});

View File

@ -0,0 +1,73 @@
import Component from '@ember/component';
import { get, set, computed } from '@ember/object';
const createWeak = function(wm = new WeakMap()) {
return {
get: function(ref, prop) {
let map = wm.get(ref);
if (map) {
return map[prop];
}
},
set: function(ref, prop, value) {
let map = wm.get(ref);
if (typeof map === 'undefined') {
map = {};
wm.set(ref, map);
}
map[prop] = value;
return map[prop];
},
};
};
const weak = createWeak();
// Covers alpha-capitalized dot separated API keys such as
// `{{Name}}`, `{{Service.Name}}` etc. but not `{{}}`
const templateRe = /{{([A-Za-z.0-9_-]+)}}/g;
export default Component.extend({
tagName: 'a',
attributeBindings: ['href', 'rel', 'target'],
rel: computed({
get: function(prop) {
return weak.get(this, prop);
},
set: function(prop, value) {
switch (value) {
case 'external':
value = `${value} noopener noreferrer`;
set(this, 'target', '_blank');
break;
}
return weak.set(this, prop, value);
},
}),
vars: computed({
get: function(prop) {
return weak.get(this, prop);
},
set: function(prop, value) {
weak.set(this, prop, value);
set(this, 'href', weak.get(this, 'template'));
},
}),
href: computed({
get: function(prop) {
return weak.get(this, prop);
},
set: function(prop, value) {
weak.set(this, 'template', value);
const vars = weak.get(this, 'vars');
if (typeof vars !== 'undefined' && typeof value !== 'undefined') {
value = value.replace(templateRe, function(match, group) {
try {
return get(vars, group) || '';
} catch (e) {
return '';
}
});
return weak.set(this, prop, value);
}
return '';
},
}),
});

View File

@ -28,25 +28,26 @@ export default Component.extend({
];
}),
distances: computed('tomography', function() {
const tomography = this.get('tomography');
let distances = tomography.distances || [];
const tomography = get(this, 'tomography');
let distances = get(tomography, 'distances') || [];
distances.forEach((d, i) => {
if (d.distance > get(this, 'max')) {
set(this, 'max', d.distance);
}
});
if (tomography.n > 360) {
let n = distances.length;
let n = get(distances, 'length');
if (n > 360) {
// We have more nodes than we want to show, take a random sampling to keep
// the number around 360.
const sampling = 360 / tomography.n;
const sampling = 360 / n;
distances = distances.filter(function(_, i) {
return i == 0 || i == n - 1 || Math.random() < sampling;
});
n = get(distances, 'length');
}
return distances.map((d, i) => {
return {
rotate: i * 360 / distances.length,
rotate: (i * 360) / n,
y2: -insetSize * (d.distance / get(this, 'max')),
node: d.node,
distance: d.distance,

View File

@ -0,0 +1,11 @@
import ComputedProperty from '@ember/object/computed';
import computedFactory from 'consul-ui/utils/computed/factory';
export default class Catchable extends ComputedProperty {
catch(cb) {
return this.meta({
catch: cb,
});
}
}
export const computed = computedFactory(Catchable);

View File

@ -1,30 +1,31 @@
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';
import { inject as service } from '@ember/service';
import { get } from '@ember/object';
export default Controller.extend({
builder: service('form'),
dom: service('dom'),
init: function() {
this._super(...arguments);
this.form = get(this, 'builder').form('acl');
},
setProperties: function(model) {
this.changeset = new Changeset(model.item, lookupValidator(validations), validations);
this._super({
...model,
...{
item: this.changeset,
},
});
// essentially this replaces the data with changesets
this._super(
Object.keys(model).reduce((prev, key, i) => {
switch (key) {
case 'item':
prev[key] = this.form.setData(prev[key]).getData();
break;
}
return prev;
}, model)
);
},
actions: {
change: function(e) {
const target = e.target || { name: 'Rules', value: e };
switch (target.name) {
case 'Type':
set(this.changeset, target.name, target.value);
break;
case 'Rules':
set(this, 'item.Rules', target.value);
break;
}
change: function(e, value, item) {
const event = get(this, 'dom').normalizeEvent(e, value);
get(this, 'form').handleEvent(event);
},
},
});

View File

@ -1,11 +1,12 @@
import Controller from '@ember/controller';
import { computed, get } from '@ember/object';
import WithFiltering from 'consul-ui/mixins/with-filtering';
import WithSearching from 'consul-ui/mixins/with-searching';
import ucfirst from 'consul-ui/utils/ucfirst';
const countType = function(items, type) {
return type === '' ? get(items, 'length') : items.filterBy('Type', type).length;
};
export default Controller.extend(WithFiltering, {
export default Controller.extend(WithSearching, WithFiltering, {
queryParams: {
type: {
as: 'type',
@ -15,6 +16,17 @@ export default Controller.extend(WithFiltering, {
replace: true,
},
},
init: function() {
this.searchParams = {
acl: 's',
};
this._super(...arguments);
},
searchable: computed('filtered', function() {
return get(this, 'searchables.acl')
.add(get(this, 'filtered'))
.search(get(this, this.searchParams.acl));
}),
typeFilters: computed('items', function() {
const items = get(this, 'items');
return ['', 'management', 'client'].map(function(item) {
@ -27,17 +39,8 @@ export default Controller.extend(WithFiltering, {
};
});
}),
filter: function(item, { s = '', type = '' }) {
const sLower = s.toLowerCase();
return (
(get(item, 'Name')
.toLowerCase()
.indexOf(sLower) !== -1 ||
get(item, 'ID')
.toLowerCase()
.indexOf(sLower) !== -1) &&
(type === '' || get(item, 'Type') === type)
);
filter: function(item, { type = '' }) {
return type === '' || get(item, 'Type') === type;
},
actions: {
sendClone: function(item) {

View File

@ -1,10 +1,8 @@
import Controller from '@ember/controller';
import { inject as service } from '@ember/service';
import { get, set } from '@ember/object';
import { get } from '@ember/object';
export default Controller.extend({
builder: service('form'),
dom: service('dom'),
isScoped: false,
init: function() {
this._super(...arguments);
this.form = get(this, 'builder').form('policy');
@ -21,25 +19,5 @@ export default Controller.extend({
return prev;
}, model)
);
set(this, 'isScoped', get(model.item, 'Datacenters.length') > 0);
},
actions: {
change: function(e, value, item) {
const form = get(this, 'form');
const event = get(this, 'dom').normalizeEvent(e, value);
try {
form.handleEvent(event);
} catch (err) {
const target = event.target;
switch (target.name) {
case 'policy[isScoped]':
set(this, 'isScoped', !get(this, 'isScoped'));
set(this.item, 'Datacenters', null);
break;
default:
throw err;
}
}
},
},
});

View File

@ -1,23 +1,23 @@
import Controller from '@ember/controller';
import { get } from '@ember/object';
import WithFiltering from 'consul-ui/mixins/with-filtering';
export default Controller.extend(WithFiltering, {
import { get, computed } from '@ember/object';
import WithSearching from 'consul-ui/mixins/with-searching';
export default Controller.extend(WithSearching, {
queryParams: {
s: {
as: 'filter',
replace: true,
},
},
filter: function(item, { s = '', type = '' }) {
const sLower = s.toLowerCase();
return (
get(item, 'Name')
.toLowerCase()
.indexOf(sLower) !== -1 ||
get(item, 'Description')
.toLowerCase()
.indexOf(sLower) !== -1
);
init: function() {
this.searchParams = {
policy: 's',
};
this._super(...arguments);
},
searchable: computed('items', function() {
return get(this, 'searchables.policy')
.add(get(this, 'items'))
.search(get(this, this.searchParams.policy));
}),
actions: {},
});

View File

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

View File

@ -0,0 +1,23 @@
import Controller from '@ember/controller';
import { inject as service } from '@ember/service';
import { get } from '@ember/object';
export default Controller.extend({
builder: service('form'),
init: function() {
this._super(...arguments);
this.form = get(this, 'builder').form('role');
},
setProperties: function(model) {
// essentially this replaces the data with changesets
this._super(
Object.keys(model).reduce((prev, key, i) => {
switch (key) {
case 'item':
prev[key] = this.form.setData(prev[key]).getData();
break;
}
return prev;
}, model)
);
},
});

View File

@ -0,0 +1,23 @@
import Controller from '@ember/controller';
import { get, computed } from '@ember/object';
import WithSearching from 'consul-ui/mixins/with-searching';
export default Controller.extend(WithSearching, {
queryParams: {
s: {
as: 'filter',
replace: true,
},
},
init: function() {
this.searchParams = {
role: 's',
};
this._super(...arguments);
},
searchable: computed('items', function() {
return get(this, 'searchables.role')
.add(get(this, 'items'))
.search(get(this, this.searchParams.role));
}),
actions: {},
});

View File

@ -1,6 +1,6 @@
import Controller from '@ember/controller';
import { inject as service } from '@ember/service';
import { get, set } from '@ember/object';
import { get } from '@ember/object';
export default Controller.extend({
dom: service('dom'),
builder: service('form'),
@ -17,59 +17,20 @@ export default Controller.extend({
case 'item':
prev[key] = this.form.setData(prev[key]).getData();
break;
case 'policy':
prev[key] = this.form
.form(key)
.setData(prev[key])
.getData();
break;
}
return prev;
}, model)
);
},
actions: {
sendClearPolicy: function(item) {
set(this, 'isScoped', false);
this.send('clearPolicy');
},
sendCreatePolicy: function(item, policies, success) {
this.send('createPolicy', item, policies, success);
},
refreshCodeEditor: function(selector, parent) {
if (parent.target) {
parent = undefined;
}
get(this, 'dom')
.component(selector, parent)
.didAppear();
},
change: function(e, value, item) {
const form = get(this, 'form');
const event = get(this, 'dom').normalizeEvent(e, value);
const form = get(this, 'form');
try {
form.handleEvent(event);
} catch (err) {
const target = event.target;
switch (target.name) {
case 'policy[isScoped]':
set(this, 'isScoped', !get(this, 'isScoped'));
set(this.policy, 'Datacenters', null);
break;
case 'Policy':
set(value, 'CreateTime', new Date().getTime());
get(this, 'item.Policies').pushObject(value);
break;
case 'Details':
// the Details expander toggle
// only load on opening
if (target.checked) {
this.send('refreshCodeEditor', '.code-editor', target.parentNode);
if (!get(value, 'Rules')) {
this.send('loadPolicy', value, get(this, 'item.Policies'));
}
}
break;
default:
throw err;
}

View File

@ -1,30 +1,24 @@
import Controller from '@ember/controller';
import { get } from '@ember/object';
import WithFiltering from 'consul-ui/mixins/with-filtering';
export default Controller.extend(WithFiltering, {
import { computed, get } from '@ember/object';
import WithSearching from 'consul-ui/mixins/with-searching';
export default Controller.extend(WithSearching, {
queryParams: {
s: {
as: 'filter',
replace: true,
},
},
filter: function(item, { s = '', type = '' }) {
const sLower = s.toLowerCase();
return (
get(item, 'AccessorID')
.toLowerCase()
.indexOf(sLower) !== -1 ||
get(item, 'Name')
.toLowerCase()
.indexOf(sLower) !== -1 ||
get(item, 'Description')
.toLowerCase()
.indexOf(sLower) !== -1 ||
(get(item, 'Policies') || []).some(function(item) {
return item.Name.toLowerCase().indexOf(sLower) !== -1;
})
);
init: function() {
this.searchParams = {
token: 's',
};
this._super(...arguments);
},
searchable: computed('items', function() {
return get(this, 'searchables.token')
.add(get(this, 'items'))
.search(get(this, this.searchParams.token));
}),
actions: {
sendClone: function(item) {
this.send('clone', item);

View File

@ -1,13 +1,14 @@
import Controller from '@ember/controller';
import { inject as service } from '@ember/service';
import { get, set } from '@ember/object';
import Changeset from 'ember-changeset';
import lookupValidator from 'ember-changeset-validations';
import validations from 'consul-ui/validations/intention';
export default Controller.extend({
dom: service('dom'),
builder: service('form'),
init: function() {
this._super(...arguments);
this.form = get(this, 'builder').form('intention');
},
setProperties: function(model) {
this.changeset = new Changeset(model.item, lookupValidator(validations), validations);
const sourceName = get(model.item, 'SourceName');
const destinationName = get(model.item, 'DestinationName');
let source = model.items.findBy('Name', sourceName);
@ -23,50 +24,57 @@ export default Controller.extend({
this._super({
...model,
...{
item: this.changeset,
item: this.form.setData(model.item).getData(),
SourceName: source,
DestinationName: destination,
},
});
},
actions: {
createNewLabel: function(term) {
return `Use a future Consul Service called '${term}'`;
createNewLabel: function(template, term) {
return template.replace(/{{term}}/g, term);
},
isUnique: function(term) {
return !get(this, 'items').findBy('Name', term);
},
change: function(e, value, _target) {
// normalize back to standard event
const target = e.target || { ..._target, ...{ name: e, value: value } };
let name, selected;
name = selected = target.value;
// TODO:
// linter needs this here?
change: function(e, value, item) {
const event = get(this, 'dom').normalizeEvent(e, value);
const form = get(this, 'form');
const target = event.target;
let name;
let selected;
let match;
switch (target.name) {
case 'Description':
case 'Action':
set(this.changeset, target.name, target.value);
break;
case 'SourceName':
case 'DestinationName':
name = selected = target.value;
// Names can be selected Service EmberObjects or typed in strings
// if its not a string, use the `Name` from the Service EmberObject
if (typeof name !== 'string') {
name = get(target.value, 'Name');
}
// linter doesn't like const here
// see if the name is already in the list
match = get(this, 'items').filterBy('Name', name);
if (match.length === 0) {
// if its not make a new 'fake' Service that doesn't exist yet
// and add it to the possible services to make an intention between
selected = { Name: name };
// linter doesn't mind const here?
const items = [selected].concat(this.items.toArray());
set(this, 'items', items);
}
set(this.changeset, target.name, name);
// mutate the value with the string name
// which will be handled by the form
target.value = name;
// these are 'non-form' variables so not on `item`
// these variables also exist in the template so we know
// the current selection
// basically the difference between
// `item.DestinationName` and just `DestinationName`
set(this, target.name, selected);
break;
}
this.changeset.validate();
form.handleEvent(event);
},
},
});

View File

@ -1,6 +1,7 @@
import Controller from '@ember/controller';
import { computed, get } from '@ember/object';
import WithFiltering from 'consul-ui/mixins/with-filtering';
import WithSearching from 'consul-ui/mixins/with-searching';
import ucfirst from 'consul-ui/utils/ucfirst';
// TODO: DRY out in acls at least
const createCounter = function(prop) {
@ -9,7 +10,7 @@ const createCounter = function(prop) {
};
};
const countAction = createCounter('Action');
export default Controller.extend(WithFiltering, {
export default Controller.extend(WithSearching, WithFiltering, {
queryParams: {
action: {
as: 'action',
@ -19,6 +20,17 @@ export default Controller.extend(WithFiltering, {
replace: true,
},
},
init: function() {
this.searchParams = {
intention: 's',
};
this._super(...arguments);
},
searchable: computed('filtered', function() {
return get(this, 'searchables.intention')
.add(get(this, 'filtered'))
.search(get(this, this.searchParams.intention));
}),
actionFilters: computed('items', function() {
const items = get(this, 'items');
return ['', 'allow', 'deny'].map(function(item) {
@ -32,16 +44,6 @@ export default Controller.extend(WithFiltering, {
});
}),
filter: function(item, { s = '', action = '' }) {
const source = get(item, 'SourceName').toLowerCase();
const destination = get(item, 'DestinationName').toLowerCase();
const sLower = s.toLowerCase();
const allLabel = 'All Services (*)'.toLowerCase();
return (
(source.indexOf(sLower) !== -1 ||
destination.indexOf(sLower) !== -1 ||
(source === '*' && allLabel.indexOf(sLower) !== -1) ||
(destination === '*' && allLabel.indexOf(sLower) !== -1)) &&
(action === '' || get(item, 'Action') === action)
);
return action === '' || get(item, 'Action') === action;
},
});

View File

@ -2,41 +2,56 @@ import Controller from '@ember/controller';
import { get, set } from '@ember/object';
import { inject as service } from '@ember/service';
import Changeset from 'ember-changeset';
import validations from 'consul-ui/validations/kv';
import lookupValidator from 'ember-changeset-validations';
export default Controller.extend({
json: true,
dom: service('dom'),
builder: service('form'),
encoder: service('btoa'),
json: true,
init: function() {
this._super(...arguments);
this.form = get(this, 'builder').form('kv');
},
setProperties: function(model) {
// TODO: Potentially save whether json has been clicked to the model,
// setting set(this, 'json', true) here will force the form to always default to code=on
// even if the user has selected code=off on another KV
// ideally we would save the value per KV, but I'd like to not do that on the model
// a set(this, 'json', valueFromSomeStorageJustForThisKV) would be added here
this.changeset = new Changeset(model.item, lookupValidator(validations), validations);
this._super({
...model,
...{
item: this.changeset,
},
});
// essentially this replaces the data with changesets
this._super(
Object.keys(model).reduce((prev, key, i) => {
switch (key) {
case 'item':
prev[key] = this.form.setData(prev[key]).getData();
break;
}
return prev;
}, model)
);
},
actions: {
change: function(e) {
const target = e.target || { name: 'value', value: e };
var parent;
switch (target.name) {
case 'additional':
parent = get(this, 'parent.Key');
set(this.changeset, 'Key', `${parent !== '/' ? parent : ''}${target.value}`);
break;
case 'json':
set(this, 'json', !get(this, 'json'));
break;
case 'value':
set(this, 'item.Value', get(this, 'encoder').execute(target.value));
break;
change: function(e, value, item) {
const event = get(this, 'dom').normalizeEvent(e, value);
const form = get(this, 'form');
try {
form.handleEvent(event);
} catch (err) {
const target = event.target;
let parent;
switch (target.name) {
case 'value':
set(this.item, 'Value', get(this, 'encoder').execute(target.value));
break;
case 'additional':
parent = get(this, 'parent.Key');
set(this.item, 'Key', `${parent !== '/' ? parent : ''}${target.value}`);
break;
case 'json':
// TODO: Potentially save whether json has been clicked to the model,
// setting set(this, 'json', true) here will force the form to always default to code=on
// even if the user has selected code=off on another KV
// ideally we would save the value per KV, but I'd like to not do that on the model
// a set(this, 'json', valueFromSomeStorageJustForThisKV) would be added here
set(this, 'json', !get(this, 'json'));
break;
default:
throw err;
}
}
},
},

View File

@ -1,18 +1,22 @@
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, {
import { get, computed } from '@ember/object';
import WithSearching from 'consul-ui/mixins/with-searching';
export default Controller.extend(WithSearching, {
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;
init: function() {
this.searchParams = {
kv: 's',
};
this._super(...arguments);
},
searchable: computed('items', function() {
return get(this, 'searchables.kv')
.add(get(this, 'items'))
.search(get(this, this.searchParams.kv));
}),
});

View File

@ -1,12 +1,27 @@
import Controller from '@ember/controller';
import { computed } from '@ember/object';
import WithEventSource from 'consul-ui/mixins/with-event-source';
import WithHealthFiltering from 'consul-ui/mixins/with-health-filtering';
import WithSearching from 'consul-ui/mixins/with-searching';
import { get } from '@ember/object';
export default Controller.extend(WithHealthFiltering, {
export default Controller.extend(WithEventSource, WithSearching, WithHealthFiltering, {
init: function() {
this.searchParams = {
healthyNode: 's',
unhealthyNode: 's',
};
this._super(...arguments);
this.columns = [25, 25, 25, 25];
},
searchableHealthy: computed('healthy', function() {
return get(this, 'searchables.healthyNode')
.add(get(this, 'healthy'))
.search(get(this, this.searchParams.healthyNode));
}),
searchableUnhealthy: computed('unhealthy', function() {
return get(this, 'searchables.unhealthyNode')
.add(get(this, 'unhealthy'))
.search(get(this, this.searchParams.unhealthyNode));
}),
unhealthy: computed('filtered', function() {
return get(this, 'filtered').filter(function(item) {
return get(item, 'isUnhealthy');
@ -18,10 +33,6 @@ export default Controller.extend(WithHealthFiltering, {
});
}),
filter: function(item, { s = '', status = '' }) {
return (
get(item, 'Node')
.toLowerCase()
.indexOf(s.toLowerCase()) !== -1 && item.hasStatus(status)
);
return item.hasStatus(status);
},
});

View File

@ -1,18 +1,46 @@
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/dom/qsa-factory';
import getComponentFactory from 'consul-ui/utils/get-component-factory';
import { inject as service } from '@ember/service';
import { get, set, computed } from '@ember/object';
import { alias } from '@ember/object/computed';
import WithSearching from 'consul-ui/mixins/with-searching';
import WithEventSource, { listen } from 'consul-ui/mixins/with-event-source';
const $$ = qsaFactory();
export default Controller.extend(WithFiltering, {
export default Controller.extend(WithEventSource, WithSearching, {
dom: service('dom'),
notify: service('flashMessages'),
items: alias('item.Services'),
queryParams: {
s: {
as: 'filter',
replace: true,
},
},
init: function() {
this.searchParams = {
nodeservice: 's',
};
this._super(...arguments);
},
item: listen('item').catch(function(e) {
if (e.target.readyState === 1) {
// OPEN
if (get(e, 'error.errors.firstObject.status') === '404') {
get(this, 'notify').add({
destroyOnClick: false,
sticky: true,
type: 'warning',
action: 'update',
});
get(this, 'tomography').close();
get(this, 'sessions').close();
}
}
}),
searchable: computed('items', function() {
return get(this, 'searchables.nodeservice')
.add(get(this, 'items'))
.search(get(this, this.searchParams.nodeservice));
}),
setProperties: function() {
this._super(...arguments);
// the default selected tab depends on whether you have any healthchecks or not
@ -20,38 +48,20 @@ export default Controller.extend(WithFiltering, {
// This method is called immediately after `Route::setupController`, and done here rather than there
// as this is a variable used purely for view level things, if the view was different we might not
// need this variable
set(this, 'selectedTab', get(this.item, 'Checks.length') > 0 ? 'health-checks' : 'services');
},
filter: function(item, { s = '' }) {
const term = s.toLowerCase();
return (
get(item, 'Service')
.toLowerCase()
.indexOf(term) !== -1 ||
get(item, 'ID')
.toLowerCase()
.indexOf(term) !== -1 ||
(get(item, 'Tags') || []).some(function(item) {
return item.toLowerCase().indexOf(term) !== -1;
}) ||
get(item, 'Port')
.toString()
.toLowerCase()
.indexOf(term) !== -1
);
set(this, 'selectedTab', get(this, 'item.Checks.length') > 0 ? 'health-checks' : 'services');
},
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();
}
});
get(this, 'dom')
.components('.tab-section input[type="radio"]:checked + div table')
.forEach(function(item) {
if (typeof item.didAppear === 'function') {
item.didAppear();
}
});
},
sortChecksByImportance: function(a, b) {
const statusA = get(a, 'Status');

View File

@ -1,7 +1,8 @@
import Controller from '@ember/controller';
import { get, computed } from '@ember/object';
import { htmlSafe } from '@ember/string';
import WithHealthFiltering from 'consul-ui/mixins/with-health-filtering';
import WithEventSource from 'consul-ui/mixins/with-event-source';
import WithSearching from 'consul-ui/mixins/with-searching';
const max = function(arr, prop) {
return arr.reduce(function(prev, item) {
return Math.max(prev, get(item, prop));
@ -24,19 +25,23 @@ const width = function(num) {
const widthDeclaration = function(num) {
return htmlSafe(`width: ${num}px`);
};
export default Controller.extend(WithHealthFiltering, {
filter: function(item, { s = '', status = '' }) {
const term = s.toLowerCase();
return (
(get(item, 'Name')
.toLowerCase()
.indexOf(term) !== -1 ||
(get(item, 'Tags') || []).some(function(item) {
return item.toLowerCase().indexOf(term) !== -1;
})) &&
item.hasStatus(status)
);
export default Controller.extend(WithEventSource, WithSearching, {
queryParams: {
s: {
as: 'filter',
},
},
init: function() {
this.searchParams = {
service: 's',
};
this._super(...arguments);
},
searchable: computed('items.[]', function() {
return get(this, 'searchables.service')
.add(get(this, 'items'))
.search(get(this, 'terms'));
}),
maxWidth: computed('{maxPassing,maxWarning,maxCritical}', function() {
const PADDING = 32 * 3 + 13;
return ['maxPassing', 'maxWarning', 'maxCritical'].reduce((prev, item) => {
@ -47,15 +52,20 @@ export default Controller.extend(WithHealthFiltering, {
return widthDeclaration(get(this, 'maxWidth'));
}),
remainingWidth: computed('maxWidth', function() {
return htmlSafe(`width: calc(50% - ${Math.round(get(this, 'maxWidth') / 2)}px)`);
// maxWidth is the maximum width of the healthchecks column
// there are currently 2 other columns so divide it by 2 and
// take that off 50% (100% / number of fluid columns)
// also we added a Type column which we've currently fixed to 100px
// so again divide that by 2 and take it off each fluid column
return htmlSafe(`width: calc(50% - 50px - ${Math.round(get(this, 'maxWidth') / 2)}px)`);
}),
maxPassing: computed('items', function() {
maxPassing: computed('items.[]', function() {
return max(get(this, 'items'), 'ChecksPassing');
}),
maxWarning: computed('items', function() {
maxWarning: computed('items.[]', function() {
return max(get(this, 'items'), 'ChecksWarning');
}),
maxCritical: computed('items', function() {
maxCritical: computed('items.[]', function() {
return max(get(this, 'items'), 'ChecksCritical');
}),
passingWidth: computed('maxPassing', function() {

View File

@ -0,0 +1,37 @@
import Controller from '@ember/controller';
import { get, set } from '@ember/object';
import { inject as service } from '@ember/service';
import WithEventSource, { listen } from 'consul-ui/mixins/with-event-source';
export default Controller.extend(WithEventSource, {
notify: service('flashMessages'),
setProperties: function() {
this._super(...arguments);
// This method is called immediately after `Route::setupController`, and done here rather than there
// as this is a variable used purely for view level things, if the view was different we might not
// need this variable
set(this, 'selectedTab', 'service-checks');
},
item: listen('item').catch(function(e) {
if (e.target.readyState === 1) {
// OPEN
if (get(e, 'error.errors.firstObject.status') === '404') {
get(this, 'notify').add({
destroyOnClick: false,
sticky: true,
type: 'warning',
action: 'update',
});
const proxy = get(this, 'proxy');
if (proxy) {
proxy.close();
}
}
}
}),
actions: {
change: function(e) {
set(this, 'selectedTab', e.target.value);
},
},
});

View File

@ -1,34 +1,56 @@
import Controller from '@ember/controller';
import { get } from '@ember/object';
import { computed } from '@ember/object';
import sumOfUnhealthy from 'consul-ui/utils/sumOfUnhealthy';
import hasStatus from 'consul-ui/utils/hasStatus';
import WithHealthFiltering from 'consul-ui/mixins/with-health-filtering';
export default Controller.extend(WithHealthFiltering, {
import { get, set, computed } from '@ember/object';
import { alias } from '@ember/object/computed';
import { inject as service } from '@ember/service';
import WithSearching from 'consul-ui/mixins/with-searching';
import WithEventSource, { listen } from 'consul-ui/mixins/with-event-source';
export default Controller.extend(WithEventSource, WithSearching, {
dom: service('dom'),
notify: service('flashMessages'),
items: alias('item.Nodes'),
init: function() {
this.searchParams = {
serviceInstance: 's',
};
this._super(...arguments);
},
unhealthy: computed('filtered', function() {
return get(this, 'filtered').filter(function(item) {
return sumOfUnhealthy(item.Checks) > 0;
});
setProperties: function() {
this._super(...arguments);
// This method is called immediately after `Route::setupController`, and done here rather than there
// as this is a variable used purely for view level things, if the view was different we might not
// need this variable
set(this, 'selectedTab', 'instances');
},
item: listen('item').catch(function(e) {
if (e.target.readyState === 1) {
// OPEN
if (get(e, 'error.errors.firstObject.status') === '404') {
get(this, 'notify').add({
destroyOnClick: false,
sticky: true,
type: 'warning',
action: 'update',
});
}
}
}),
healthy: computed('filtered', function() {
return get(this, 'filtered').filter(function(item) {
return sumOfUnhealthy(item.Checks) === 0;
});
searchable: computed('items', function() {
return get(this, 'searchables.serviceInstance')
.add(get(this, 'items'))
.search(get(this, this.searchParams.serviceInstance));
}),
filter: function(item, { s = '', status = '' }) {
const term = s.toLowerCase();
return (
get(item, 'Node.Node')
.toLowerCase()
.indexOf(term) !== -1 ||
(get(item, 'Service.ID')
.toLowerCase()
.indexOf(term) !== -1 &&
hasStatus(get(item, 'Checks'), status))
);
actions: {
change: function(e) {
set(this, 'selectedTab', e.target.value);
// Ensure tabular-collections sizing is recalculated
// now it is visible in the DOM
get(this, 'dom')
.components('.tab-section input[type="radio"]:checked + div table')
.forEach(function(item) {
if (typeof item.didAppear === 'function') {
item.didAppear();
}
});
},
},
});

View File

@ -0,0 +1,43 @@
import Controller from '@ember/controller';
import { get, set } from '@ember/object';
import { inject as service } from '@ember/service';
export default Controller.extend({
repo: service('settings'),
dom: service('dom'),
actions: {
key: function(e) {
switch (true) {
case e.keyCode === 13:
// disable ENTER
e.preventDefault();
}
},
change: function(e, value, item) {
const event = get(this, 'dom').normalizeEvent(e, value);
// TODO: Switch to using forms like the rest of the app
// setting utils/form/builder for things to be done before we
// can do that. For the moment just do things normally its a simple
// enough form at the moment
const target = event.target;
const blocking = get(this, 'item.client.blocking');
switch (target.name) {
case 'client[blocking]':
if (typeof blocking === 'undefined') {
set(this, 'item.client', {});
}
set(this, 'item.client.blocking', !blocking);
this.send('update', get(this, 'item'));
break;
case 'urls[service]':
if (typeof get(this, 'item.urls') === 'undefined') {
set(this, 'item.urls', {});
}
set(this, 'item.urls.service', target.value);
this.send('update', get(this, 'item'));
break;
}
},
},
});

5
ui-v2/app/env.js Normal file
View File

@ -0,0 +1,5 @@
import config from './config/environment';
export default function(str) {
const user = window.localStorage.getItem(str);
return user !== null ? user : config[str];
}

6
ui-v2/app/forms/acl.js Normal file
View File

@ -0,0 +1,6 @@
import validations from 'consul-ui/validations/acl';
import builderFactory from 'consul-ui/utils/form/builder';
const builder = builderFactory();
export default function(container, name = '', v = validations, form = builder) {
return form(name, {}).setValidators(v);
}

View File

@ -0,0 +1,6 @@
import validations from 'consul-ui/validations/intention';
import builderFactory from 'consul-ui/utils/form/builder';
const builder = builderFactory();
export default function(container, name = '', v = validations, form = builder) {
return form(name, {}).setValidators(v);
}

6
ui-v2/app/forms/kv.js Normal file
View File

@ -0,0 +1,6 @@
import validations from 'consul-ui/validations/kv';
import builderFactory from 'consul-ui/utils/form/builder';
const builder = builderFactory();
export default function(container, name = '', v = validations, form = builder) {
return form(name, {}).setValidators(v);
}

View File

@ -1,7 +1,7 @@
import validations from 'consul-ui/validations/policy';
import builderFactory from 'consul-ui/utils/form/builder';
const builder = builderFactory();
export default function(name = 'policy', v = validations, form = builder) {
export default function(container, name = 'policy', v = validations, form = builder) {
return form(name, {
Datacenters: {
type: 'array',

8
ui-v2/app/forms/role.js Normal file
View File

@ -0,0 +1,8 @@
import validations from 'consul-ui/validations/role';
import builderFactory from 'consul-ui/utils/form/builder';
const builder = builderFactory();
export default function(container, name = 'role', v = validations, form = builder) {
return form(name, {})
.setValidators(v)
.add(container.form('policy'));
}

View File

@ -1,9 +1,9 @@
import builderFactory from 'consul-ui/utils/form/builder';
import validations from 'consul-ui/validations/token';
import policy from 'consul-ui/forms/policy';
import builderFactory from 'consul-ui/utils/form/builder';
const builder = builderFactory();
export default function(name = '', v = validations, form = builder) {
export default function(container, name = '', v = validations, form = builder) {
return form(name, {})
.setValidators(v)
.add(policy());
.add(container.form('policy'))
.add(container.form('role'));
}

View File

@ -1,8 +0,0 @@
import { helper } from '@ember/component/helper';
import { get } from '@ember/object';
const MANAGEMENT_ID = '00000000-0000-0000-0000-000000000001';
export function isManagement(params, hash) {
return get(params[0], 'ID') === MANAGEMENT_ID;
}
export default helper(isManagement);

View File

@ -0,0 +1,18 @@
import { helper } from '@ember/component/helper';
import { get } from '@ember/object';
const MANAGEMENT_ID = '00000000-0000-0000-0000-000000000001';
export function typeOf(params, hash) {
const item = params[0];
switch (true) {
case get(item, 'ID') === MANAGEMENT_ID:
return 'policy-management';
case typeof get(item, 'template') === 'undefined':
return 'role';
case get(item, 'template') !== '':
return 'policy-service-identity';
default:
return 'policy';
}
}
export default helper(typeOf);

View File

@ -0,0 +1,15 @@
const scripts = document.getElementsByTagName('script');
const current = scripts[scripts.length - 1];
export function initialize(application) {
const Client = application.resolveRegistration('service:client/http');
Client.reopen({
isCurrent: function(src) {
return current.src === src;
},
});
}
export default {
initialize,
};

View File

@ -0,0 +1,23 @@
import Route from '@ember/routing/route';
/**
* This initializer is very similar to:
* https://github.com/kellyselden/ember-controller-lifecycle
*
* Why is this included here:
* 1. Make sure lifecycle functions are functions, not just truthy.
* 2. Right now we don't want a setup function (at least until we are definitely decided that we want one)
* This is possibly a very personal opinion so it makes sense to just include this file here.
*/
Route.reopen({
resetController(controller, exiting, transition) {
this._super(...arguments);
if (typeof controller.reset === 'function') {
controller.reset(exiting);
}
},
});
export function initialize() {}
export default {
initialize,
};

View File

@ -1,15 +1,40 @@
import { get, set } from '@ember/object';
import kv from 'consul-ui/forms/kv';
import acl from 'consul-ui/forms/acl';
import token from 'consul-ui/forms/token';
import policy from 'consul-ui/forms/policy';
import role from 'consul-ui/forms/role';
import intention from 'consul-ui/forms/intention';
export function initialize(application) {
// Service-less injection using private properties at a per-project level
const FormBuilder = application.resolveRegistration('service:form');
const forms = {
token: token(),
policy: policy(),
kv: kv,
acl: acl,
token: token,
policy: policy,
role: role,
intention: intention,
};
FormBuilder.reopen({
form: function(name) {
return forms[name];
let form = get(this.forms, name);
if (!form) {
form = set(this.forms, name, forms[name](this));
// only do special things for our new things for the moment
if (name === 'role' || name === 'policy') {
const repo = get(this, name);
form.clear(function(obj) {
return repo.create(obj);
});
form.submit(function(obj) {
return repo.persist(obj);
});
}
}
return form;
},
});
}

View File

@ -1,44 +1,9 @@
import { inject as service } from '@ember/service';
import { get } from '@ember/object';
import lint from 'consul-ui/utils/editor/lint';
const MODES = [
{
name: 'JSON',
mime: 'application/json',
mode: 'javascript',
ext: ['json', 'map'],
alias: ['json5'],
},
{
name: 'HCL',
mime: 'text/x-ruby',
mode: 'ruby',
ext: ['rb'],
alias: ['jruby', 'macruby', 'rake', 'rb', 'rbx'],
},
{ name: 'YAML', mime: 'text/x-yaml', mode: 'yaml', ext: ['yaml', 'yml'], alias: ['yml'] },
];
export function initialize(application) {
const IvyCodeMirrorComponent = application.resolveRegistration('component:ivy-codemirror');
const IvyCodeMirrorService = application.resolveRegistration('service:code-mirror');
// Make sure ivy-codemirror respects/maintains a `name=""` attribute
IvyCodeMirrorComponent.reopen({
attributeBindings: ['name'],
});
// Add some method to the code-mirror service so I don't have to have 2 services
// for dealing with codemirror
IvyCodeMirrorService.reopen({
dom: service('dom'),
modes: function() {
return MODES;
},
lint: function() {
return lint(...arguments);
},
getEditor: function(element) {
return get(this, 'dom').element('textarea + div', element).CodeMirror;
},
});
}
export default {

View File

@ -0,0 +1,15 @@
import { get } from '@ember/object';
export function initialize(application) {
const PowerSelectComponent = application.resolveRegistration('component:power-select');
PowerSelectComponent.reopen({
updateState: function(changes) {
if (!get(this, 'isDestroyed')) {
return this._super(changes);
}
},
});
}
export default {
initialize,
};

View File

@ -0,0 +1,40 @@
import intention from 'consul-ui/search/filters/intention';
import token from 'consul-ui/search/filters/token';
import policy from 'consul-ui/search/filters/policy';
import role from 'consul-ui/search/filters/role';
import kv from 'consul-ui/search/filters/kv';
import acl from 'consul-ui/search/filters/acl';
import node from 'consul-ui/search/filters/node';
// service instance
import nodeService from 'consul-ui/search/filters/node/service';
import serviceNode from 'consul-ui/search/filters/service/node';
import service from 'consul-ui/search/filters/service';
import filterableFactory from 'consul-ui/utils/search/filterable';
const filterable = filterableFactory();
export function initialize(application) {
// Service-less injection using private properties at a per-project level
const Builder = application.resolveRegistration('service:search');
const searchables = {
intention: intention(filterable),
token: token(filterable),
acl: acl(filterable),
policy: policy(filterable),
role: role(filterable),
kv: kv(filterable),
healthyNode: node(filterable),
unhealthyNode: node(filterable),
serviceInstance: serviceNode(filterable),
nodeservice: nodeService(filterable),
service: service(filterable),
};
Builder.reopen({
searchable: function(name) {
return searchables[name];
},
});
}
export default {
initialize,
};

View File

@ -0,0 +1,102 @@
import env from 'consul-ui/env';
export function initialize(container) {
if (env('CONSUL_UI_DISABLE_REALTIME')) {
return;
}
['node', 'coordinate', 'session', 'service', 'proxy']
.map(function(item) {
// create repositories that return a promise resolving to an EventSource
return {
service: `repository/${item}/event-source`,
extend: 'repository/type/event-source',
// Inject our original respository that is used by this class
// within the callable of the EventSource
services: {
content: `repository/${item}`,
},
};
})
.concat(
['dc', 'policy', 'role'].map(function(item) {
// create repositories that return a promise resolving to an EventSource
return {
service: `repository/${item}/component`,
extend: 'repository/type/component',
// Inject our original respository that is used by this class
// within the callable of the EventSource
services: {
content: `repository/${item}`,
},
};
})
)
.concat([
// These are the routes where we overwrite the 'default'
// repo service. Default repos are repos that return a promise resolving to
// an ember-data record or recordset
{
route: 'dc/nodes/index',
services: {
repo: 'repository/node/event-source',
},
},
{
route: 'dc/nodes/show',
services: {
repo: 'repository/node/event-source',
coordinateRepo: 'repository/coordinate/event-source',
sessionRepo: 'repository/session/event-source',
},
},
{
route: 'dc/services/index',
services: {
repo: 'repository/service/event-source',
},
},
{
route: 'dc/services/show',
services: {
repo: 'repository/service/event-source',
},
},
{
route: 'dc/services/instance',
services: {
repo: 'repository/service/event-source',
proxyRepo: 'repository/proxy/event-source',
},
},
{
service: 'form',
services: {
role: 'repository/role/component',
policy: 'repository/policy/component',
},
},
])
.forEach(function(definition) {
if (typeof definition.extend !== 'undefined') {
// Create the class instances that we need
container.register(
`service:${definition.service}`,
container.resolveRegistration(`service:${definition.extend}`).extend({})
);
}
Object.keys(definition.services).forEach(function(name) {
const servicePath = definition.services[name];
// inject its dependencies, this could probably detect the type
// but hardcode this for the moment
if (typeof definition.route !== 'undefined') {
container.inject(`route:${definition.route}`, name, `service:${servicePath}`);
} else {
container.inject(`service:${definition.service}`, name, `service:${servicePath}`);
}
});
});
}
export default {
initialize,
};

View File

@ -1,16 +1,19 @@
import Mixin from '@ember/object/mixin';
import { inject as service } from '@ember/service';
import { next } from '@ember/runloop';
import { get } from '@ember/object';
const isOutside = function(element, e) {
// TODO: Potentially move this to dom service
const isOutside = function(element, e, doc = document) {
if (element) {
const isRemoved = !e.target || !document.contains(e.target);
const isRemoved = !e.target || !doc.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');
if (isOutside(el, e)) {
@ -18,6 +21,7 @@ const handler = function(e) {
}
};
export default Mixin.create({
dom: service('dom'),
init: function() {
this._super(...arguments);
this.handler = handler.bind(this);
@ -26,12 +30,14 @@ export default Mixin.create({
onblur: function() {},
didInsertElement: function() {
this._super(...arguments);
const doc = get(this, 'dom').document();
next(this, () => {
document.addEventListener('click', this.handler);
doc.addEventListener('click', this.handler);
});
},
willDestroyElement: function() {
this._super(...arguments);
document.removeEventListener('click', this.handler);
const doc = get(this, 'dom').document();
doc.removeEventListener('click', this.handler);
},
});

View File

@ -0,0 +1,70 @@
import { REQUEST_CREATE, REQUEST_UPDATE } from 'consul-ui/adapters/application';
import Mixin from '@ember/object/mixin';
import { get } from '@ember/object';
import minimizeModel from 'consul-ui/utils/minimizeModel';
const normalizeServiceIdentities = function(items) {
return (items || []).map(function(item) {
const policy = {
template: 'service-identity',
Name: item.ServiceName,
};
if (typeof item.Datacenters !== 'undefined') {
policy.Datacenters = item.Datacenters;
}
return policy;
});
};
const normalizePolicies = function(items) {
return (items || []).map(function(item) {
return {
template: '',
...item,
};
});
};
const serializeServiceIdentities = function(items) {
return items
.filter(function(item) {
return get(item, 'template') === 'service-identity';
})
.map(function(item) {
const identity = {
ServiceName: get(item, 'Name'),
};
if (get(item, 'Datacenters')) {
identity.Datacenters = get(item, 'Datacenters');
}
return identity;
});
};
const serializePolicies = function(items) {
return items.filter(function(item) {
return get(item, 'template') === '';
});
};
export default Mixin.create({
handleSingleResponse: function(url, response, primary, slug) {
response.Policies = normalizePolicies(response.Policies).concat(
normalizeServiceIdentities(response.ServiceIdentities)
);
return this._super(url, response, primary, slug);
},
dataForRequest: function(params) {
const data = this._super(...arguments);
const name = params.type.modelName;
switch (params.requestType) {
case REQUEST_UPDATE:
// falls through
case REQUEST_CREATE:
// ServiceIdentities serialization must happen first, or a copy taken
data[name].ServiceIdentities = serializeServiceIdentities(data[name].Policies);
data[name].Policies = minimizeModel(serializePolicies(data[name].Policies));
break;
}
return data;
},
});

View File

@ -0,0 +1,28 @@
import { REQUEST_CREATE, REQUEST_UPDATE } from 'consul-ui/adapters/application';
import Mixin from '@ember/object/mixin';
import minimizeModel from 'consul-ui/utils/minimizeModel';
export default Mixin.create({
handleSingleResponse: function(url, response, primary, slug) {
['Roles'].forEach(function(prop) {
if (typeof response[prop] === 'undefined' || response[prop] === null) {
response[prop] = [];
}
});
return this._super(url, response, primary, slug);
},
dataForRequest: function(params) {
const name = params.type.modelName;
const data = this._super(...arguments);
switch (params.requestType) {
case REQUEST_UPDATE:
// falls through
case REQUEST_CREATE:
data[name].Roles = minimizeModel(data[name].Roles);
break;
}
return data;
},
});

View File

@ -0,0 +1,4 @@
import Mixin from '@ember/object/mixin';
import WithBlockingActions from 'consul-ui/mixins/with-blocking-actions';
export default Mixin.create(WithBlockingActions, {});

View File

@ -0,0 +1,45 @@
import Mixin from '@ember/object/mixin';
import { computed as catchable } from 'consul-ui/computed/catchable';
import purify from 'consul-ui/utils/computed/purify';
import WithListeners from 'consul-ui/mixins/with-listeners';
const PREFIX = '_';
export default Mixin.create(WithListeners, {
setProperties: function(model) {
const _model = {};
Object.keys(model).forEach(key => {
// here (see comment below on deleting)
if (typeof this[key] !== 'undefined' && this[key].isDescriptor) {
_model[`${PREFIX}${key}`] = model[key];
const meta = this.constructor.metaForProperty(key) || {};
if (typeof meta.catch === 'function') {
if (typeof _model[`${PREFIX}${key}`].addEventListener === 'function') {
this.listen(_model[`_${key}`], 'error', meta.catch.bind(this));
}
}
} else {
_model[key] = model[key];
}
});
return this._super(_model);
},
reset: function(exiting) {
if (exiting) {
Object.keys(this).forEach(prop => {
if (this[prop] && typeof this[prop].close === 'function') {
this[prop].close();
// ember doesn't delete on 'resetController' by default
// right now we only call reset when we are exiting, therefore a full
// setProperties will be called the next time we enter the Route so this
// is ok for what we need and means that the above conditional works
// as expected (see 'here' comment above)
delete this[prop];
}
});
}
return this._super(...arguments);
},
});
export const listen = purify(catchable, function(props) {
return props.map(item => `${PREFIX}${item}`);
});

View File

@ -15,7 +15,7 @@ const toKeyValue = function(el) {
};
export default Mixin.create({
filters: {},
filtered: computed('items', 'filters', function() {
filtered: computed('items.[]', 'filters', function() {
const filters = get(this, 'filters');
return get(this, 'items').filter(item => {
return this.filter(item, filters);

View File

@ -1,25 +1,6 @@
import Mixin from '@ember/object/mixin';
import WithFiltering from 'consul-ui/mixins/with-filtering';
import { computed, get } from '@ember/object';
import ucfirst from 'consul-ui/utils/ucfirst';
const countStatus = function(items, status) {
if (status === '') {
return get(items, 'length');
}
const key = `Checks${ucfirst(status)}`;
return items.reduce(function(prev, item, i, arr) {
const num = get(item, key);
return (
prev +
(typeof num !== 'undefined'
? num
: get(item, 'Checks').filter(function(item) {
return item.Status === status;
}).length) || 0
);
}, 0);
};
export default Mixin.create(WithFiltering, {
queryParams: {
status: {
@ -29,22 +10,4 @@ export default Mixin.create(WithFiltering, {
as: 'filter',
},
},
healthFilters: computed('items', function() {
const items = get(this, 'items');
const objs = ['', 'passing', 'warning', 'critical'].map(function(item) {
const count = countStatus(items, item);
return {
count: count,
label: `${item === '' ? 'All' : ucfirst(item)} (${count.toLocaleString()})`,
value: item,
};
});
objs[0].label = `All (${objs
.slice(1)
.reduce(function(prev, item, i, arr) {
return prev + item.count;
}, 0)
.toLocaleString()})`;
return objs;
}),
});

View File

@ -0,0 +1,36 @@
import Controller from '@ember/controller';
import Component from '@ember/component';
import Mixin from '@ember/object/mixin';
import { inject as service } from '@ember/service';
import { get } from '@ember/object';
export default Mixin.create({
dom: service('dom'),
init: function() {
this._super(...arguments);
this._listeners = get(this, 'dom').listeners();
let teardown = ['willDestroy'];
if (this instanceof Component) {
teardown = ['willDestroyElement'];
} else if (this instanceof Controller) {
if (typeof this.reset === 'function') {
teardown.push('reset');
}
}
teardown.forEach(method => {
const destroy = this[method];
this[method] = function() {
if (typeof destroy === 'function') {
destroy.apply(this, arguments);
}
this.removeListeners();
};
});
},
listen: function(target, event, handler) {
return this._listeners.add(...arguments);
},
removeListeners: function() {
return this._listeners.remove(...arguments);
},
});

View File

@ -1,11 +1,12 @@
import Mixin from '@ember/object/mixin';
import { inject as service } from '@ember/service';
import { get } from '@ember/object';
import { assert } from '@ember/debug';
export default Mixin.create({
dom: service('dom'),
resize: function(e) {
assert('with-resizing.resize needs to be overridden', false);
},
win: window,
init: function() {
this._super(...arguments);
this.handler = e => {
@ -17,14 +18,18 @@ export default Mixin.create({
},
didInsertElement: function() {
this._super(...arguments);
get(this, 'win').addEventListener('resize', this.handler, false);
get(this, 'dom')
.viewport()
.addEventListener('resize', this.handler, false);
this.didAppear();
},
didAppear: function() {
this.handler({ target: get(this, 'win') });
this.handler({ target: get(this, 'dom').viewport() });
},
willDestroyElement: function() {
get(this, 'win').removeEventListener('resize', this.handler, false);
get(this, 'dom')
.viewport()
.removeEventListener('resize', this.handler, false);
this._super(...arguments);
},
});

View File

@ -0,0 +1,32 @@
import Mixin from '@ember/object/mixin';
import { inject as service } from '@ember/service';
import { get, set } from '@ember/object';
import WithListeners from 'consul-ui/mixins/with-listeners';
/**
* WithSearching mostly depends on a `searchParams` object which must be set
* inside the `init` function. The naming and usage of this is modelled on
* `queryParams` but in contrast cannot _yet_ be 'hung' of the Controller
* object, it MUST be set in the `init` method.
* Reasons: As well as producing a eslint error, it can also be 'shared' amongst
* child Classes of the component. It is not clear _yet_ whether mixing this in
* avoids this and is something to be looked at in future to slightly improve DX
* Please also see:
* https://emberjs.com/api/ember/2.12/classes/Ember.Object/properties?anchor=mergedProperties
*
*/
export default Mixin.create(WithListeners, {
builder: service('search'),
init: function() {
this._super(...arguments);
const params = this.searchParams || {};
this.searchables = {};
Object.keys(params).forEach(type => {
const key = params[type];
this.searchables[type] = get(this, 'builder').searchable(type);
this.listen(this.searchables[type], 'change', e => {
const value = e.target.value;
set(this, key, value === '' ? null : value);
});
});
},
});

View File

@ -21,6 +21,7 @@ export default Model.extend({
Datacenter: attr('string'),
Segment: attr(),
Coord: attr(),
meta: attr(),
hasStatus: function(status) {
return hasStatus(get(this, 'Checks'), status);
},

View File

@ -24,6 +24,10 @@ const model = Model.extend({
Datacenters: attr(),
CreateIndex: attr('number'),
ModifyIndex: attr('number'),
template: attr('string', {
defaultValue: '',
}),
});
export const ATTRS = writable(model, ['Name', 'Description', 'Rules', 'Datacenters']);
export default model;

12
ui-v2/app/models/proxy.js Normal file
View File

@ -0,0 +1,12 @@
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'),
ServiceName: attr('string'),
ServiceID: attr('string'),
ServiceProxy: attr(),
});

34
ui-v2/app/models/role.js Normal file
View File

@ -0,0 +1,34 @@
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'),
Name: attr('string', {
defaultValue: '',
}),
Description: attr('string', {
defaultValue: '',
}),
Policies: attr({
defaultValue: function() {
return [];
},
}),
ServiceIdentities: attr({
defaultValue: function() {
return [];
},
}),
// frontend only for ordering where CreateIndex can't be used
CreateTime: attr('date'),
//
Datacenter: attr('string'),
// TODO: Figure out whether we need this or not
Datacenters: attr(),
Hash: attr('string'),
CreateIndex: attr('number'),
ModifyIndex: attr('number'),
});

View File

@ -30,6 +30,7 @@ export default Model.extend({
Node: attr(),
Service: attr(),
Checks: attr(),
meta: attr(),
passing: computed('ChecksPassing', 'Checks', function() {
let num = 0;
// TODO: use typeof

View File

@ -8,6 +8,7 @@ export const SLUG_KEY = 'AccessorID';
const model = Model.extend({
[PRIMARY_KEY]: attr('string'),
[SLUG_KEY]: attr('string'),
IDPName: attr('string'),
SecretID: attr('string'),
// Legacy
Type: attr('string'),
@ -27,7 +28,18 @@ const model = Model.extend({
return [];
},
}),
Roles: attr({
defaultValue: function() {
return [];
},
}),
ServiceIdentities: attr({
defaultValue: function() {
return [];
},
}),
CreateTime: attr('date'),
Hash: attr('string'),
CreateIndex: attr('number'),
ModifyIndex: attr('number'),
});
@ -39,6 +51,7 @@ export const ATTRS = writable(model, [
'Local',
'Description',
'Policies',
'Roles',
// SecretID isn't writable but we need it to identify an
// update via the old API, see TokenAdapter dataForRequest
'SecretID',

View File

@ -18,6 +18,9 @@ export const routes = {
show: {
_options: { path: '/:name' },
},
instance: {
_options: { path: '/:name/:id' },
},
},
// Nodes represent a consul node
nodes: {
@ -71,6 +74,15 @@ export const routes = {
_options: { path: '/create' },
},
},
roles: {
_options: { path: '/roles' },
edit: {
_options: { path: '/:id' },
},
create: {
_options: { path: '/create' },
},
},
tokens: {
_options: { path: '/tokens' },
edit: {
@ -88,9 +100,9 @@ export const routes = {
_options: { path: '/' },
},
// The settings page is global.
// settings: {
// _options: { path: '/setting' },
// },
settings: {
_options: { path: '/setting' },
},
notfound: {
_options: { path: '/*path' },
},

View File

@ -4,12 +4,14 @@ import { hash } from 'rsvp';
import { get } from '@ember/object';
import { next } from '@ember/runloop';
import { Promise } from 'rsvp';
import WithBlockingActions from 'consul-ui/mixins/with-blocking-actions';
const $html = document.documentElement;
const removeLoading = function() {
return $html.classList.remove('ember-loading');
const removeLoading = function($from) {
return $from.classList.remove('ember-loading');
};
export default Route.extend(WithBlockingActions, {
dom: service('dom'),
init: function() {
this._super(...arguments);
},
@ -17,20 +19,21 @@ export default Route.extend(WithBlockingActions, {
settings: service('settings'),
actions: {
loading: function(transition, originRoute) {
const $root = get(this, 'dom').root();
let dc = null;
if (originRoute.routeName !== 'dc') {
const model = this.modelFor('dc') || { dcs: null, dc: { Name: null } };
dc = get(this, 'repo').getActive(model.dc.Name, model.dcs);
}
hash({
loading: !$html.classList.contains('ember-loading'),
loading: !$root.classList.contains('ember-loading'),
dc: dc,
}).then(model => {
next(() => {
const controller = this.controllerFor('application');
controller.setProperties(model);
transition.promise.finally(function() {
removeLoading();
removeLoading($root);
controller.setProperties({
loading: false,
dc: model.dc,
@ -74,6 +77,7 @@ export default Route.extend(WithBlockingActions, {
if (error.status === '') {
error.message = 'Error';
}
const $root = get(this, 'dom').root();
hash({
error: error,
dc:
@ -85,13 +89,13 @@ export default Route.extend(WithBlockingActions, {
dcs: model && model.dcs ? model.dcs : [],
})
.then(model => {
removeLoading();
removeLoading($root);
next(() => {
this.controllerFor('error').setProperties(model);
});
})
.catch(e => {
removeLoading();
removeLoading($root);
next(() => {
this.controllerFor('error').setProperties({ error: error });
});

View File

@ -19,7 +19,6 @@ export default Route.extend({
});
},
setupController: function(controller, model) {
this._super(...arguments);
controller.setProperties(model);
},
});

View File

@ -22,7 +22,6 @@ export default Route.extend(WithAclActions, {
});
},
setupController: function(controller, model) {
this._super(...arguments);
controller.setProperties(model);
},
deactivate: function() {

View File

@ -16,7 +16,6 @@ export default Route.extend(WithAclActions, {
});
},
setupController: function(controller, model) {
this._super(...arguments);
controller.setProperties(model);
},
});

View File

@ -36,7 +36,6 @@ export default Route.extend(WithAclActions, {
});
},
setupController: function(controller, model) {
this._super(...arguments);
controller.setProperties(model);
},
});

View File

@ -8,7 +8,6 @@ import WithPolicyActions from 'consul-ui/mixins/policy/with-actions';
export default SingleRoute.extend(WithPolicyActions, {
repo: service('repository/policy'),
tokenRepo: service('repository/token'),
datacenterRepo: service('repository/dc'),
model: function(params) {
const dc = this.modelFor('dc').dc.Name;
const tokenRepo = get(this, 'tokenRepo');
@ -16,7 +15,6 @@ export default SingleRoute.extend(WithPolicyActions, {
return hash({
...model,
...{
datacenters: get(this, 'datacenterRepo').findAll(),
items: tokenRepo.findByPolicy(get(model.item, 'ID'), dc).catch(function(e) {
switch (get(e, 'errors.firstObject.status')) {
case '403':
@ -31,7 +29,6 @@ export default SingleRoute.extend(WithPolicyActions, {
});
},
setupController: function(controller, model) {
this._super(...arguments);
controller.setProperties(model);
},
});

View File

@ -23,7 +23,6 @@ export default Route.extend(WithPolicyActions, {
});
},
setupController: function(controller, model) {
this._super(...arguments);
controller.setProperties(model);
},
});

View File

@ -0,0 +1,6 @@
import Route from './edit';
import CreatingRoute from 'consul-ui/mixins/creating-route';
export default Route.extend(CreatingRoute, {
templateName: 'dc/acls/roles/edit',
});

View File

@ -0,0 +1,34 @@
import SingleRoute from 'consul-ui/routing/single';
import { inject as service } from '@ember/service';
import { hash } from 'rsvp';
import { get } from '@ember/object';
import WithRoleActions from 'consul-ui/mixins/role/with-actions';
export default SingleRoute.extend(WithRoleActions, {
repo: service('repository/role'),
tokenRepo: service('repository/token'),
model: function(params) {
const dc = this.modelFor('dc').dc.Name;
const tokenRepo = get(this, 'tokenRepo');
return this._super(...arguments).then(model => {
return hash({
...model,
...{
items: tokenRepo.findByRole(get(model.item, 'ID'), dc).catch(function(e) {
switch (get(e, 'errors.firstObject.status')) {
case '403':
case '401':
// do nothing the SingleRoute will have caught it already
return;
}
throw e;
}),
},
});
});
},
setupController: function(controller, model) {
controller.setProperties(model);
},
});

View File

@ -0,0 +1,28 @@
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
import { hash } from 'rsvp';
import { get } from '@ember/object';
import WithRoleActions from 'consul-ui/mixins/role/with-actions';
export default Route.extend(WithRoleActions, {
repo: service('repository/role'),
queryParams: {
s: {
as: 'filter',
replace: true,
},
},
model: function(params) {
const repo = get(this, 'repo');
return hash({
...repo.status({
items: repo.findAllByDatacenter(this.modelFor('dc').dc.Name),
}),
isLoading: false,
});
},
setupController: function(controller, model) {
controller.setProperties(model);
},
});

View File

@ -1,105 +1,24 @@
import SingleRoute from 'consul-ui/routing/single';
import { inject as service } from '@ember/service';
import { hash } from 'rsvp';
import { set, get } from '@ember/object';
import updateArrayObject from 'consul-ui/utils/update-array-object';
import { get } from '@ember/object';
import WithTokenActions from 'consul-ui/mixins/token/with-actions';
const ERROR_PARSE_RULES = 'Failed to parse ACL rules';
const ERROR_NAME_EXISTS = 'Invalid Policy: A Policy with Name';
export default SingleRoute.extend(WithTokenActions, {
repo: service('repository/token'),
policyRepo: service('repository/policy'),
datacenterRepo: service('repository/dc'),
settings: service('settings'),
model: function(params, transition) {
const dc = this.modelFor('dc').dc.Name;
const policyRepo = get(this, 'policyRepo');
return this._super(...arguments).then(model => {
return hash({
...model,
...{
// TODO: I only need these to create a new policy
datacenters: get(this, 'datacenterRepo').findAll(),
policy: this.getEmptyPolicy(),
token: get(this, 'settings').findBySlug('token'),
items: policyRepo.findAllByDatacenter(dc).catch(function(e) {
switch (get(e, 'errors.firstObject.status')) {
case '403':
case '401':
// do nothing the SingleRoute will have caught it already
return;
}
throw e;
}),
},
});
});
},
setupController: function(controller, model) {
this._super(...arguments);
controller.setProperties(model);
},
getEmptyPolicy: function() {
const dc = this.modelFor('dc').dc.Name;
return get(this, 'policyRepo').create({ Datacenter: dc });
},
actions: {
// TODO: Some of this could potentially be moved to the repo services
loadPolicy: function(item, items) {
const repo = get(this, 'policyRepo');
const dc = this.modelFor('dc').dc.Name;
const slug = get(item, repo.getSlugKey());
repo.findBySlug(slug, dc).then(item => {
updateArrayObject(items, item, repo.getSlugKey());
});
},
remove: function(item, items) {
return items.removeObject(item);
},
clearPolicy: function() {
// TODO: I should be able to reset the ember-data object
// back to it original state?
// possibly Forms could know how to create
const controller = get(this, 'controller');
controller.setProperties({
policy: this.getEmptyPolicy(),
});
},
createPolicy: function(item, policies, success) {
get(this, 'policyRepo')
.persist(item)
.then(item => {
set(item, 'CreateTime', new Date().getTime());
policies.pushObject(item);
return item;
})
.then(function() {
success();
})
.catch(err => {
if (typeof err.errors !== 'undefined') {
const error = err.errors[0];
let prop;
let message = error.detail;
switch (true) {
case message.indexOf(ERROR_PARSE_RULES) === 0:
prop = 'Rules';
message = error.detail;
break;
case message.indexOf(ERROR_NAME_EXISTS) === 0:
prop = 'Name';
message = message.substr(ERROR_NAME_EXISTS.indexOf(':') + 1);
break;
}
if (prop) {
item.addError(prop, message);
}
} else {
throw err;
}
});
},
},
});

Some files were not shown because too many files have changed in this diff Show More