ui: Use DataSources in ACLs area (#7681)

* ui: Use Datasource for loading related data in ACLs area

* ui: Use more manual cleanup for Controller event-sources

* Update reconcile to use nspace and add SyncTime to role/policy

* Use the correct value for nspace and dc (the one from the item itself)

* Remove the // check, we no longer need it. Add some TODO
This commit is contained in:
John Cowen 2020-04-22 17:30:26 +01:00 committed by John Cowen
parent 8643565b30
commit 7f3b9d04ba
16 changed files with 105 additions and 78 deletions

View File

@ -2,13 +2,20 @@
<YieldSlot @name="create">{{yield}}</YieldSlot>
<label class="type-text">
<span><YieldSlot @name="label">{{yield}}</YieldSlot></span>
{{#if isOpen}}
<DataSource
@src={{concat '/' (or nspace 'default') '/' dc '/' (pluralize type)}}
@onchange={{action (mut allOptions) value="data"}}
/>
{{/if}}
<PowerSelect
@search={{action "search"}}
@options={{options}}
@loadingMessage="Loading..."
@searchMessage="No possible options"
@searchPlaceholder={{placeholder}}
@onOpen={{action "open"}}
@onOpen={{action (mut isOpen) true}}
@onClose={{action (mut isOpen) false}}
@onChange={{action "change" "items[]" items}} as |item|>
<YieldSlot @name="option" @params={{block-params item}}>{{yield}}</YieldSlot>
</PowerSelect>

View File

@ -53,11 +53,6 @@ export default Component.extend(SlotsMixin, WithListeners, {
reset: function() {
this.form.clear({ Datacenter: this.dc, Namespace: this.nspace });
},
open: function() {
if (!get(this, 'allOptions.closed')) {
set(this, 'allOptions', this.repo.findAllByDatacenter(this.dc, this.nspace));
}
},
save: function(item, items, success = function() {}) {
// Specifically this saves an 'new' option/child
// and then adds it to the selectedOptions, not options
@ -68,20 +63,22 @@ export default Component.extend(SlotsMixin, WithListeners, {
// 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,
this.listen(item, {
message: e => {
this.actions.change.apply(this, [
{
target: {
name: 'items[]',
value: items,
},
},
},
items,
e.data
);
success();
items,
e.data,
]);
success();
},
error: e => this.error(e),
});
this.listen(item, 'error', this.error.bind(this));
},
remove: function(item, items) {
const prop = this.repo.getSlugKey();

View File

@ -3,7 +3,6 @@ import { inject as service } from '@ember/service';
import { set } from '@ember/object';
import { schedule } from '@ember/runloop';
import Ember from 'ember';
/**
* Utility function to set, but actually replace if we should replace
* then call a function on the thing to be replaced (usually a clean up function)
@ -57,7 +56,7 @@ export default Component.extend({
if (this.loading === 'lazy') {
this._lazyListeners.add(
this.dom.isInViewport(this.dom.element(`#${this.guid}`), inViewport => {
set(this, 'isIntersecting', inViewport || Ember.testing);
set(this, 'isIntersecting', inViewport);
if (!this.isIntersecting) {
this.actions.close.bind(this)();
} else {

View File

@ -53,6 +53,9 @@
</label>
</div>
{{#if isScoped }}
<DataSource @src="/*/*/datacenters"
@onchange={{action (mut datacenters) value="data"}}
/>
<div class="checkbox-group" role="group">
{{#each datacenters as |dc| }}
<label class="type-checkbox">

View File

@ -1,10 +1,7 @@
import FormComponent from '../form-component/index';
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',
allowServiceIdentity: true,
@ -14,7 +11,6 @@ export default FormComponent.extend({
init: function() {
this._super(...arguments);
set(this, 'isScoped', get(this, 'item.Datacenters.length') > 0);
set(this, 'datacenters', this.datacenterRepo.findAll());
this.templates = [
{
name: 'Policy',

View File

@ -41,30 +41,31 @@
<BlockSlot @name="set">
<TabularDetails
data-test-policies
@onchange={{action 'loadItem'}}
@onchange={{action 'open'}}
@items={{sort-by 'CreateTime:desc' 'Name:asc' items}} as |item index|
>
<BlockSlot @name="header">
<th>Name</th>
<th>Datacenters</th>
</BlockSlot>
<BlockSlot @name="row">
<td class={{policy/typeof item}}>
{{#if item.ID }}
<a href={{href-to 'dc.acls.policies.edit' item.ID}}>{{item.Name}}</a>
<a href={{href-to 'dc.acls.policies.edit' item.ID}}>{{item.Name}}</a>
{{else}}
<a name={{item.Name}}>{{item.Name}}</a>
{{/if}}
</td>
<td>
{{if (not item.isSaving) (join ', ' (policy/datacenters item)) 'Saving...'}}
</td>
</BlockSlot>
<BlockSlot @name="details">
<label class="type-text">
<span>Rules <a href="{{env 'CONSUL_DOCS_URL'}}/guides/acl.html#rule-specification" rel="help noopener noreferrer" target="_blank">(HCL Format)</a></span>
{{#if (eq item.template '')}}
<CodeEditor @syntax="hcl" @readonly={{true}} @value={{item.Rules}} />
<DataSource
@src={{concat '/' item.Namespace '/' item.Datacenter '/policy/' item.ID}}
@onchange={{action (mut loadedItem) value="data"}}
@loading="lazy"
/>
<CodeEditor @syntax="hcl" @readonly={{true}} @value={{or loadedItem.Rules item.Rules}} />
{{else}}
<CodeEditor @syntax="hcl" @readonly={{true}}>
{{~component 'service-identity' name=item.Name~}}

View File

@ -1,7 +1,6 @@
import ChildSelectorComponent from '../child-selector/index';
import { get, set } from '@ember/object';
import { 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_INVALID_POLICY = 'Invalid service policy';
@ -9,7 +8,6 @@ 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',
allowServiceIdentity: true,
@ -27,7 +25,6 @@ export default ChildSelectorComponent.extend({
reset: function(e) {
this._super(...arguments);
set(this, 'isScoped', false);
set(this, 'datacenters', this.datacenterRepo.findAll());
},
refreshCodeEditor: function(e, target) {
const selector = '.code-editor';
@ -63,22 +60,5 @@ export default ChildSelectorComponent.extend({
open: function(e) {
this.refreshCodeEditor(e, e.target.parentElement);
},
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
// TODO: Temporarily add dc here, will soon be serialized onto the policy itself
const slugKey = this.repo.getSlugKey();
const slug = get(value, slugKey);
updateArrayObject(items, this.repo.findBySlug(slug, this.dc, this.nspace), slugKey, slug);
}
},
},
});

View File

@ -35,27 +35,25 @@ export default Mixin.create(WithListeners, {
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];
// TODO: Check that nulling this out instead of deleting is fine
// pretty sure it is as above is just a falsey check
set(this, prop, null);
}
});
}
Object.keys(this).forEach(prop => {
if (this[prop] && typeof this[prop].close === 'function') {
this[prop].willDestroy();
// 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];
// TODO: Check that nulling this out instead of deleting is fine
// pretty sure it is as above is just a falsey check
set(this, prop, null);
}
});
return this._super(...arguments);
},
willDestroy: function() {
this._super(...arguments);
this.reset(true);
this._super(...arguments);
},
});
export const listen = purify(catchable, function(props) {

View File

@ -21,6 +21,8 @@ export default Model.extend({
//
Datacenter: attr('string'),
Namespace: attr('string'),
SyncTime: attr('number'),
meta: attr(),
Datacenters: attr(),
CreateIndex: attr('number'),
ModifyIndex: attr('number'),

View File

@ -27,6 +27,7 @@ export default Model.extend({
//
Datacenter: attr('string'),
Namespace: attr('string'),
SyncTime: attr('number'),
// TODO: Figure out whether we need this or not
Datacenters: attr(),
Hash: attr('string'),

View File

@ -5,9 +5,18 @@ export default Service.extend({
datacenters: service('repository/dc'),
namespaces: service('repository/nspace'),
token: service('repository/token'),
policies: service('repository/policy'),
policy: service('repository/policy'),
roles: service('repository/role'),
type: service('data-source/protocols/http/blocking'),
source: function(src, configuration) {
const [, , /*nspace*/ dc, model, ...rest] = src.split('/');
// TODO: Consider adding/requiring nspace, dc, model, action, ...rest
const [, nspace, dc, model, ...rest] = src.split('/');
// TODO: Consider throwing if we have an empty nspace or dc
// we are going to use '*' for 'all' when we need that
// and an empty value is the same as 'default'
// reasoning for potentially doing it here is, uri's should
// always be complete, they should never have things like '///model'
let find;
const repo = this[model];
if (typeof repo.reconcile === 'function') {
@ -32,6 +41,13 @@ export default Service.extend({
case 'token':
find = configuration => repo.self(rest[1], dc);
break;
case 'roles':
case 'policies':
find = configuration => repo.findAllByDatacenter(dc, nspace, configuration);
break;
case 'policy':
find = configuration => repo.findBySlug(rest[0], dc, nspace, configuration);
break;
}
return this.type.source(find, configuration);
},

View File

@ -42,9 +42,6 @@ export default Service.extend({
}
if (!sources.has(uri)) {
let [providerName, pathname] = uri.split('://');
if (pathname.startsWith('//')) {
pathname = pathname.substr(2);
}
const provider = this[providerName];
let configuration = {};

View File

@ -1,6 +1,8 @@
import Service, { inject as service } from '@ember/service';
import { assert } from '@ember/debug';
import { typeOf } from '@ember/utils';
import { get } from '@ember/object';
export default Service.extend({
getModelName: function() {
assert('RepositoryService.getModelName should be overridden', false);
@ -15,12 +17,21 @@ export default Service.extend({
store: service('store'),
reconcile: function(meta = {}) {
// unload anything older than our current sync date/time
// FIXME: This needs fixing once again to take nspaces into account
if (typeof meta.date !== 'undefined') {
const checkNspace = meta.nspace !== '';
this.store.peekAll(this.getModelName()).forEach(item => {
const date = item.SyncTime;
if (typeof date !== 'undefined' && date != meta.date) {
this.store.unloadRecord(item);
const dc = get(item, 'Datacenter');
if (dc === meta.dc) {
if (checkNspace) {
const nspace = get(item, 'Namespace');
if (nspace !== meta.namespace) {
return;
}
}
const date = get(item, 'SyncTime');
if (typeof date !== 'undefined' && date != meta.date) {
this.store.unloadRecord(item);
}
}
});
}

View File

@ -17,7 +17,9 @@ Feature: dc / acls / policies / as many / remove: Remove
Then the url should be /datacenter/acls/[Model]s/key
And I see 1 policy model on the policies component
And I click expand on the policies.selectedOptions
And a GET request was made to "/v1/acl/policy/00000000-0000-0000-0000-000000000001?dc=datacenter&ns=@namespace"
# Until we have a reliable way of mocking out and controlling IntersectionObserver
# this will need to stay commented out
# And a GET request was made to "/v1/acl/policy/00000000-0000-0000-0000-000000000001?dc=datacenter&ns=@namespace"
And I click delete on the policies.selectedOptions
And I click confirmDelete on the policies.selectedOptions
And I see 0 policy models on the policies component

View File

@ -1,4 +1,5 @@
import { moduleFor, test, skip } from 'ember-qunit';
import { get } from '@ember/object';
import repo from 'consul-ui/tests/helpers/repo';
const NAME = 'policy';
moduleFor(`service:repository/${NAME}`, `Integration | Service | ${NAME}`, {
@ -6,11 +7,15 @@ moduleFor(`service:repository/${NAME}`, `Integration | Service | ${NAME}`, {
integration: true,
});
skip('translate returns the correct data for the translate endpoint');
const now = new Date().getTime();
const dc = 'dc-1';
const id = 'policy-name';
const undefinedNspace = 'default';
[undefinedNspace, 'team-1', undefined].forEach(nspace => {
test(`findByDatacenter returns the correct data for list endpoint when nspace is ${nspace}`, function(assert) {
get(this.subject(), 'store').serializerFor(NAME).timestamp = function() {
return now;
};
return repo(
'Policy',
'findAllByDatacenter',
@ -32,6 +37,7 @@ const undefinedNspace = 'default';
expected(function(payload) {
return payload.map(item =>
Object.assign({}, item, {
SyncTime: now,
Datacenter: dc,
Namespace: item.Namespace || undefinedNspace,
uid: `["${item.Namespace || undefinedNspace}","${dc}","${item.ID}"]`,
@ -64,6 +70,11 @@ const undefinedNspace = 'default';
Datacenter: dc,
Namespace: item.Namespace || undefinedNspace,
uid: `["${item.Namespace || undefinedNspace}","${dc}","${item.ID}"]`,
meta: {
cursor: undefined,
dc: dc,
nspace: item.Namespace || undefinedNspace,
},
});
})
);

View File

@ -1,4 +1,5 @@
import { moduleFor, test } from 'ember-qunit';
import { get } from '@ember/object';
import repo from 'consul-ui/tests/helpers/repo';
import { createPolicies } from 'consul-ui/tests/helpers/normalizers';
@ -7,11 +8,15 @@ moduleFor(`service:repository/${NAME}`, `Integration | Service | ${NAME}`, {
// Specify the other units that are required for this test.
integration: true,
});
const now = new Date().getTime();
const dc = 'dc-1';
const id = 'role-name';
const undefinedNspace = 'default';
[undefinedNspace, 'team-1', undefined].forEach(nspace => {
test(`findByDatacenter returns the correct data for list endpoint when nspace is ${nspace}`, function(assert) {
get(this.subject(), 'store').serializerFor(NAME).timestamp = function() {
return now;
};
return repo(
'Role',
'findAllByDatacenter',
@ -33,6 +38,7 @@ const undefinedNspace = 'default';
expected(function(payload) {
return payload.map(item =>
Object.assign({}, item, {
SyncTime: now,
Datacenter: dc,
Namespace: item.Namespace || undefinedNspace,
uid: `["${item.Namespace || undefinedNspace}","${dc}","${item.ID}"]`,