diff --git a/lib/global-admin/addon/accounts/index/controller.js b/lib/global-admin/addon/accounts/index/controller.js index 12d1ab30e..f4dbd5c3c 100644 --- a/lib/global-admin/addon/accounts/index/controller.js +++ b/lib/global-admin/addon/accounts/index/controller.js @@ -1,10 +1,10 @@ -import { computed } from '@ember/object'; +import { get, computed, set } from '@ember/object'; import { inject as service } from '@ember/service'; import Controller from '@ember/controller'; export default Controller.extend({ access: service(), - sortBy: 'name', + sortBy: null, headers: computed('isLocal', function() { let out = [ { @@ -26,6 +26,11 @@ export default Controller.extend({ return out; }), + init() { + this._super(...arguments); + set(this, 'sortBy', get(this, 'isLocal') ? 'username' : 'name'); + }, + isLocal: computed('access.provider', function() { return true; // TODO 2.0 // return this.get('access.provider') === 'localauthconfig'; diff --git a/lib/global-admin/addon/clusters/detail/edit/route.js b/lib/global-admin/addon/clusters/detail/edit/route.js index 6d059a7d1..ed2016eab 100644 --- a/lib/global-admin/addon/clusters/detail/edit/route.js +++ b/lib/global-admin/addon/clusters/detail/edit/route.js @@ -24,14 +24,4 @@ export default Route.extend({ } }); }, - - setupController(controller, model) { - this._super(...arguments); - - controller.set('errors', null); - - let bindings = (get(model,'cluster.clusterRoleTemplateBindings')||[]).slice(); - bindings = bindings.filter(x =>get(x, 'name') !== 'creator'); - set(controller, 'memberArray', bindings); - } }); diff --git a/lib/global-admin/addon/clusters/detail/edit/template.hbs b/lib/global-admin/addon/clusters/detail/edit/template.hbs index 098d1b61e..e26d2659f 100644 --- a/lib/global-admin/addon/clusters/detail/edit/template.hbs +++ b/lib/global-admin/addon/clusters/detail/edit/template.hbs @@ -21,10 +21,9 @@ }} {{form-members creator=model.me - editing=false - memberArray=memberArray + editing=true memberConfig=memberConfig - project=primaryResource + primaryResource=primaryResource roles=model.roles users=model.users type="cluster" diff --git a/lib/global-admin/addon/clusters/new/rke/template.hbs b/lib/global-admin/addon/clusters/new/rke/template.hbs index 92e9d52ec..75e65b2dd 100644 --- a/lib/global-admin/addon/clusters/new/rke/template.hbs +++ b/lib/global-admin/addon/clusters/new/rke/template.hbs @@ -112,9 +112,8 @@ {{form-members creator=model.me editing=false - memberArray=memberArray memberConfig=memberConfig - project=primaryResource + primaryResource=primaryResource roles=model.roles users=model.users type="cluster" diff --git a/lib/shared/addon/components/form-members/component.js b/lib/shared/addon/components/form-members/component.js index 73cf028fe..35bbe5ddf 100644 --- a/lib/shared/addon/components/form-members/component.js +++ b/lib/shared/addon/components/form-members/component.js @@ -1,54 +1,115 @@ import Component from '@ember/component'; import layout from './template'; -import { computed, observer, get, set } from '@ember/object'; -import { on } from '@ember/object/evented'; +import { computed, get, set, setProperties } from '@ember/object'; import { inject as service } from '@ember/service'; import { all as PromiseAll } from 'rsvp'; export default Component.extend({ layout, - globalStore: service(), - access: service(), + globalStore: service(), + access: service(), - editing: false, - memberArray: null, - memberConfig: null, - model: null, - project: null, - roles: null, - type: null, - users: null, - creator: null, - showCreator: true, - - toAdd: null, - toUpdate: null, - toRemove: null, + editing: false, + memberArray: null, + memberConfig: null, + model: null, + primaryResource: null, + roles: null, + type: null, + users: null, + creator: null, + showCreator: true, + toAddCustom: null, + _bindings: null, init() { this._super(...arguments); - set(this, 'toAdd', []); - set(this, 'toUpdate', []); - set(this, 'toRemove', []); + this.buildUpdateList(get(this,'primaryResource')) this.sendAction('initAlert', this.primaryResourceSaved.bind(this)); }, - didReceiveAttrs() { - let ma = get(this, 'memberArray').filter(( m ) => { - return get(m, 'roleTemplateId').indexOf('-owner') < 0 && !get(this, 'toAdd').includes(m); - }); - if (ma && get(ma, 'length') > 0) { - set(this, 'toUpdate', ma); + buildUpdateList(resource) { + let bindingType = `${get(resource, 'type')}RoleTemplateBindings`; + let bindings = get(resource, bindingType).filter(b => b.name !== 'creator'); + let existing = []; + let grouped = {}; + set(this, "_bindings", bindings.slice()); + if (bindings && get(this, 'editing')) { + bindings.forEach((b) => { + if (grouped[get(b, 'subjectName')]) { + grouped[get(b, 'subjectName')].push(b); + } else { + grouped[get(b, 'subjectName')] = [b]; + } + }); + + if (grouped) { + Object.keys(grouped).forEach((g) => { + if (grouped[g].length >1) { + let idsOut = []; + grouped[g].forEach((m) => { + idsOut.push(get(m, 'roleTemplateId')) + }); + let config = get(this, 'memberConfig'); + set(config, 'subjectKind', 'User'); //should be upper but make sure + set(config, 'subjectName', g); + let record = get(this,'globalStore').createRecord(config); + + let out = { + role: record, + toUpdate: false, + toAdd: false, + toDelete: false, + customRolesExisting: idsOut + } + + existing.push(out); + } else { + existing.push({ + role: grouped[g][0], + toUpdate: true, + toAdd: false, + toDelete: false, + id: Math.random() + }) + } + }); + } } + set(this, 'memberArray', existing); }, primaryResourceSaved: function() { // returns a promise of all the adds/removes/updates to the parent - const pr = get(this, 'project'); + const pr = get(this, 'primaryResource'); const resourceId = get(pr, 'id'); - const add = (get(this, 'toAdd')||[]); - const update = get(this, 'toUpdate')||[]; - const remove = get(this, 'toRemove')||[]; + const memberArray = get(this, 'memberArray').slice(); + + const customToAdd = memberArray.filterBy('customRolesToAdd'); + const customToRemove = memberArray.filterBy('customRolesToRemove'); + + const add = get(this, 'memberArray').filter(r => get(r, 'toAdd') && !get(r, 'customRolesToAdd')).map(a => a.role); + const update = get(this, 'memberArray').filterBy('toUpdate', true).map(u => u.role); + const remove = get(this, 'memberArray').filterBy('toDelete', true).map(r => r.role); + + customToRemove.forEach((rm) => { + // custom obj from custom + get(rm, 'customRolesToRemove').forEach((roleTempId) => { + let match = get(this, '_bindings').find((binding) => { + if (get(binding, 'roleTemplateId') === roleTempId) { + return binding; + } + }); + remove.push(match); + }); + }); + customToAdd.forEach((m) => { + get(m, 'customRolesToAdd').forEach((a) => { + let role = get(m, 'role').clone(); + set(role, 'roleTemplateId', a); + add.addObject(role); + }) + }); add.forEach((x) => { x.set(`${get(this, 'type')}Id`, resourceId); @@ -58,6 +119,7 @@ export default Component.extend({ return PromiseAll(add.map(x => x.save())).then(() => { return PromiseAll(update.map(x => x.save())).then(() => { return PromiseAll(remove.map(x => x.delete())).then(() => { + set(this, 'memberArray', []); return pr; }); }); @@ -65,6 +127,11 @@ export default Component.extend({ }, actions: { + setCustomToAdd(member, customIds, customIdsRemove) { + set(member, 'customRolesToAdd', customIds); + set(member, 'customRolesToRemove', customIdsRemove); + }, + cancel() { this.goBack(); }, @@ -74,18 +141,37 @@ export default Component.extend({ set(config, 'subjectKind', kind.capitalize()); //should be upper but make sure // not setting the name correctly let record = get(this,'globalStore').createRecord(config); - get(this,'memberArray').pushObject(record); - get(this, 'toAdd').pushObject(record); + let out = { + role: record, + toAdd: true, + toUpate: false, + toDelete: false, + id: Math.random() + }; + get(this,'memberArray').pushObject(out); }, removeMember(obj) { - get(this,'memberArray').removeObject(obj); - get(this, 'toRemove').pushObject(obj); + let exists = get(this, 'memberArray').findBy('id', get(obj, 'id')); + if (get(obj, 'toAdd') && exists) { + // we just added and should remove it because the record has not been persisted yet + get(this,'memberArray').removeObject(obj); + } else if (exists) { + setProperties(exists, { + toAdd: false, + toUpdate: false, + toDelete: true, + }); + } }, }, filteredUsers: computed('users.@each.{id,state}', function() { - return get(this, 'users').sortBy('displayName'); + let users = get(this, 'users'); + if (get(this, 'editing')) { + users = users.filter(u => !u.hasOwnProperty('me')); + } + return users.sortBy('displayName'); }), }); diff --git a/lib/shared/addon/components/form-members/template.hbs b/lib/shared/addon/components/form-members/template.hbs index 1e3234f2b..4002e6946 100644 --- a/lib/shared/addon/components/form-members/template.hbs +++ b/lib/shared/addon/components/form-members/template.hbs @@ -10,9 +10,25 @@ - {{project-member-row member=member roles=roles users=users owner=creator type=type}} + {{project-member-row + roles=roles + users=users + owner=creator + type=type + }} {{#each memberArray as |member|}} - {{project-member-row member=member roles=roles users=filteredUsers remove="removeMember" }} + {{#unless member.toDelete}} + {{project-member-row + member=member + editing=editing + resource=primaryResource + roles=roles + users=filteredUsers + pageType=primaryResource.type + remove="removeMember" + alertNewCustoms=(action "setCustomToAdd") + }} + {{/unless}} {{/each}} diff --git a/lib/shared/addon/components/form-scoped-roles/template.hbs b/lib/shared/addon/components/form-scoped-roles/template.hbs index 5fd755376..bd233ba87 100644 --- a/lib/shared/addon/components/form-scoped-roles/template.hbs +++ b/lib/shared/addon/components/form-scoped-roles/template.hbs @@ -9,7 +9,7 @@ {{#accordion-list-item title=(t 'formScopedRoles.title' type=cTyped) - detail=(t 'formScopedRoles.description') + detail=(t 'formScopedRoles.description' type=cTyped) expandOnInit=true showExpand=false }} diff --git a/lib/shared/addon/components/modal-add-custom-roles/component.js b/lib/shared/addon/components/modal-add-custom-roles/component.js new file mode 100644 index 000000000..66ee2b453 --- /dev/null +++ b/lib/shared/addon/components/modal-add-custom-roles/component.js @@ -0,0 +1,44 @@ +import Component from '@ember/component'; +import ModalBase from 'shared/mixins/modal-base'; +import layout from './template'; +import { get, set } from '@ember/object'; +import { alias } from 'ember-computed'; + +export default Component.extend(ModalBase, { + layout, + classNames: ['small-modal'], + type: alias('modalOpts.type'), + + init() { + this._super(...arguments); + let custom = get(this, 'modalOpts.roles').filterBy('hidden', false).filter((role) => { + return role.get('id') !== `${get(this, 'type')}-owner` && role.get('id') !== `${get(this, 'type')}-member`; + }).map((role) => { + let binding = null; + if ( get(this, 'modalOpts.current') ) { + binding = get(this, 'modalOpts.current').findBy('roleTemplateId', get(role, 'id')); + } + return { + role, + active: !!binding, + existing: binding, + } + }); + set(this, 'custom', custom) + }, + + actions: { + save() { + get(this, 'modalOpts.done')(get(this, 'custom')); + this.get('modalService').toggleModal(); + }, + + completed() { + this.get('modalService').toggleModal(); + }, + + goBack() { + this.get('modalService').toggleModal(); + }, + }, +}); diff --git a/lib/shared/addon/components/modal-add-custom-roles/template.hbs b/lib/shared/addon/components/modal-add-custom-roles/template.hbs new file mode 100644 index 000000000..f52ceefac --- /dev/null +++ b/lib/shared/addon/components/modal-add-custom-roles/template.hbs @@ -0,0 +1,24 @@ +
+

Custom Roles

+
+
+ {{#accordion-list-item + title=(t 'formScopedRoles.title' type=type) + detail=(t 'formScopedRoles.description' type=type) + expandOnInit=true + showExpand=false + }} +
+ {{#each custom as |row|}} +
+ +
+ {{/each}} +
+ {{/accordion-list-item}} +
+ +{{save-cancel save="save" cancel="goBack" createLabel="generic.save"}} \ No newline at end of file diff --git a/lib/shared/addon/components/new-edit-project/template.hbs b/lib/shared/addon/components/new-edit-project/template.hbs index 58a1c7c98..ba4032d83 100644 --- a/lib/shared/addon/components/new-edit-project/template.hbs +++ b/lib/shared/addon/components/new-edit-project/template.hbs @@ -33,9 +33,8 @@ {{form-members creator=creator editing=editing - memberArray=memberArray memberConfig=memberConfig - project=primaryResource + primaryResource=primaryResource roles=model.roles users=model.users type="project" diff --git a/lib/shared/addon/components/project-member-row/component.js b/lib/shared/addon/components/project-member-row/component.js index e46bf423c..720f6d516 100644 --- a/lib/shared/addon/components/project-member-row/component.js +++ b/lib/shared/addon/components/project-member-row/component.js @@ -1,7 +1,23 @@ import Component from '@ember/component'; import layout from './template'; -import { computed, observer, get, set } from '@ember/object'; +import { computed, get, observer, set, setProperties } from '@ember/object'; import { on } from '@ember/object/evented'; +import { inject as service } from '@ember/service'; + +const BASIC_ROLES = [ + { + label: 'Standard User', + value: 'member', + }, + { + label: 'Admin', + value: 'owner', + }, + { + label: 'Custom', + value: 'custom', + }, +]; export default Component.extend({ layout, @@ -12,23 +28,77 @@ export default Component.extend({ roles: null, owner: null, type: null, + pageType: null, + modalService: service('modal'), + hasCustom: false, + customRoles: null, actions: { + showEdit(member) { + if (get(member, 'role.roleTemplateId') === 'custom'){ + this.openModal(); + } else { + set(member, 'role.roleTemplateId', 'custom'); + } + }, remove: function () { this.sendAction('remove', get(this,'member')); } }, - init: function () { - this._super(...arguments); - set(this, 'choices', get(this,'roles').filterBy('hidden', false).map(role => { - return { - label: role.name, - value: role.id, - }; - })); + doneAdding(customs) { + setProperties(this, { + hasCustom: true, + customRoles: customs + }); + let customIds = []; + let customIdsRemove = []; + customs.forEach((c) => { + if (get(c, 'active') && !get(c, 'existing')) { + customIds.push(get(c, 'role.id')); + } + if (get(c, 'existing') && !get(c, 'active')) { + customIdsRemove.push(get(c, 'role.id')); + } + }); + this.alertNewCustoms(get(this, 'member'), customIds, customIdsRemove); }, + openModal() { + let current = null; + if (get(this, 'member.customRolesExisting')) { + current = get(this, `resource.${get(this, 'pageType')}RoleTemplateBindings`); + } + // append roletemplate ids from the modal to the custom field. split that field in the add promise of form-members? + get(this,'modalService').toggleModal('modal-add-custom-roles', { + model: get(this, 'member'), + roles: get(this, 'roles'), + done: this.doneAdding.bind(this), + current: current, + type: get(this, 'pageType') + }); + }, + + openCustomModal: on('init', observer('member.role.roleTemplateId', function() { + if (get(this, 'member.role.roleTemplateId') === 'custom') { + this.openModal(); + } + })), + + choices: computed('roles.[]', 'pageType', function() { + let pt = get(this, 'pageType'); + if (pt) { + return BASIC_ROLES.map((r) => { + return { + label: r.label, + value: r.value.indexOf('custom') >= 0 ? 'custom' : `${pt}-${r.value}` + }; + + }); + } + return []; + }), + userList: computed('users.[]', function() { return (get(this, 'users')||[]).map(( user ) =>{ return { @@ -38,11 +108,11 @@ export default Component.extend({ }); }), - kind: computed('member.subjectKind', function () { + kind: computed('member.role.subjectKind', function () { if (get(this, 'owner')) { return `projectsPage.new.form.members.${get(this,'owner.type').toLowerCase()}`; // TODO translations } else { - return `projectsPage.new.form.members.${get(this,'member.subjectKind').toLowerCase()}` + return `projectsPage.new.form.members.${get(this,'member.role.subjectKind').toLowerCase()}` } }), diff --git a/lib/shared/addon/components/project-member-row/template.hbs b/lib/shared/addon/components/project-member-row/template.hbs index 2df2bcb14..d9227f6de 100644 --- a/lib/shared/addon/components/project-member-row/template.hbs +++ b/lib/shared/addon/components/project-member-row/template.hbs @@ -4,20 +4,26 @@ {{#if owner}} {{owner.displayName}} + {{else if editing}} + {{member.role.user.displayName}} {{else}} - {{searchable-select content=userList value=member.subjectName disabled=true}} + {{searchable-select content=userList value=member.role.subjectName}} {{/if}} {{#if owner}} {{if (eq type 'project') 'Project Owner' 'Cluster Owner'}} + {{else if (or hasCustom member.customRolesExisting)}} + Multple Roles {{else}} - {{searchable-select content=choices value=member.roleTemplateId}} + {{searchable-select content=choices value=member.role.roleTemplateId}} {{/if}}  
- + {{#unless owner}} + + {{/unless}}
diff --git a/lib/shared/app/components/modal-add-custom-roles/component.js b/lib/shared/app/components/modal-add-custom-roles/component.js new file mode 100644 index 000000000..5eee68cc0 --- /dev/null +++ b/lib/shared/app/components/modal-add-custom-roles/component.js @@ -0,0 +1 @@ +export { default } from 'shared/components/modal-add-custom-roles/component'; diff --git a/translations/en-us.yaml b/translations/en-us.yaml index 6e9dd7eeb..53ba7dcde 100644 --- a/translations/en-us.yaml +++ b/translations/en-us.yaml @@ -2240,7 +2240,7 @@ formGlobalRoles: formScopedRoles: title: '{type} Permissions' - description: TBD. + description: 'Controls what access users have to the {type}.' mode: admin: label: Owner