Skip to content

Commit

Permalink
UI: ACL Roles cont. plus Service Identities (#5661)
Browse files Browse the repository at this point in the history
Adds support for ACL Roles and Service Identities CRUD, along with necessary changes to Tokens, and the CSS improvements required.

Also includes refinements/improvements for easier testing of deeply nested components.
  • Loading branch information
johncowen authored May 1, 2019
1 parent 8a822fb commit a2ee02f
Show file tree
Hide file tree
Showing 117 changed files with 2,238 additions and 824 deletions.
20 changes: 5 additions & 15 deletions ui-v2/app/adapters/role.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ 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 minimizeModel from 'consul-ui/utils/minimizeModel';

export default Adapter.extend({
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));
},
Expand Down Expand Up @@ -52,15 +53,6 @@ export default Adapter.extend({
}
return this._super(status, headers, response, requestData);
},
handleSingleResponse: function(url, response, primary, slug) {
// Sometimes we get `Policies: null`, make null equal an empty array
['Policies'].forEach(function(prop) {
if (typeof response[prop] === 'undefined' || response[prop] === null) {
response[prop] = [];
}
});
return this._super(url, response, primary, slug);
},
methodForRequest: function(params) {
switch (params.requestType) {
case REQUEST_CREATE:
Expand All @@ -69,13 +61,11 @@ export default Adapter.extend({
return this._super(...arguments);
},
dataForRequest: function(params) {
let data = this._super(...arguments);
const data = this._super(...arguments);
switch (params.requestType) {
case REQUEST_UPDATE:
case REQUEST_CREATE:
data.role.Policies = minimizeModel(data.role.Policies);
data = data.role;
break;
return data.role;
}
return data;
},
Expand Down
14 changes: 4 additions & 10 deletions ui-v2/app/adapters/token.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,16 @@ import { PRIMARY_KEY, SLUG_KEY } from 'consul-ui/models/token';
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 minimizeModel from 'consul-ui/utils/minimizeModel';

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);
Expand Down Expand Up @@ -109,12 +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
['Policies', 'Roles'].forEach(function(prop) {
if (typeof response[prop] === 'undefined' || response[prop] === null) {
response[prop] = [];
}
});
// Convert an old style update response to a new style
if (typeof response['ID'] !== 'undefined') {
const item = get(this, 'store')
Expand Down Expand Up @@ -172,8 +168,6 @@ export default Adapter.extend({
}
// falls through
case REQUEST_CREATE:
data.token.Policies = minimizeModel(data.token.Policies);
data.token.Roles = minimizeModel(data.token.Roles);
data = data.token;
break;
case REQUEST_SELF:
Expand Down
41 changes: 27 additions & 14 deletions ui-v2/app/components/child-selector.js
Original file line number Diff line number Diff line change
@@ -1,31 +1,37 @@
import Component from '@ember/component';
import SlotsMixin from 'block-slots';
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';
import { alias } from '@ember/object/computed';

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, 'name'));
this.form = get(this, 'formContainer').form(get(this, 'name'));
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('items.[]', 'allOptions.[]', function() {
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, 'items') || [];
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')));
Expand Down Expand Up @@ -54,21 +60,29 @@ export default Component.extend(SlotsMixin, WithListeners, {
}
},
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
this.listen(repo.persist(item), 'message', e => {
const item = e.data;
set(item, 'CreateTime', new Date().getTime());
items.pushObject(item);
this.onchange({ target: this });
// It looks like success is the only potentially unsafe
// operation here
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();
Expand All @@ -84,7 +98,6 @@ export default Component.extend(SlotsMixin, WithListeners, {
change: function(e, value, item) {
const event = get(this, 'dom').normalizeEvent(...arguments);
const items = value;
// const item = event.target.value;
switch (event.target.name) {
case 'items[]':
set(item, 'CreateTime', new Date().getTime());
Expand Down
46 changes: 40 additions & 6 deletions ui-v2/app/components/code-editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,31 +11,57 @@ const DEFAULTS = {
};
export default Component.extend({
settings: service('settings'),
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')
Expand All @@ -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);
},
},
});
18 changes: 12 additions & 6 deletions ui-v2/app/components/form-component.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
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';

export default Component.extend(WithListeners, {
// 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() {},
Expand All @@ -17,13 +19,17 @@ export default Component.extend(WithListeners, {

dom: service('dom'),
container: service('form'),
init: function() {
this._super(...arguments);
},

actions: {
change: function(e, value, item) {
const event = get(this, 'dom').normalizeEvent(e, value);
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);
Expand Down
20 changes: 14 additions & 6 deletions ui-v2/app/components/policy-form.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,33 @@ 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,
type: 'policy',
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() {
change: function(e) {
try {
this._super(...arguments);
} catch (err) {
const scoped = get(this, 'isScoped');
const name = err.target.name;
const value = err.target.value;
switch (name) {
case 'policy[isScoped]':
if (scoped) {
Expand All @@ -32,9 +43,6 @@ export default FormComponent.extend({
}
set(this, 'isScoped', !scoped);
break;
case 'policy[type]':
set(this, 'type', value);
break;
default:
this.onerror(err);
}
Expand Down
Loading

0 comments on commit a2ee02f

Please sign in to comment.