Merge pull request #3588 from westlywright/feature.global.role.create.edit

Global Roles
This commit is contained in:
Westly Wright 2019-11-19 10:03:34 -07:00 committed by GitHub
commit ed6f83375d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 263 additions and 137 deletions

View File

@ -9,15 +9,23 @@ const SPECIAL = [BASE, ADMIN, USER];
export default Resource.extend({ export default Resource.extend({
access: service(),
intl: service(), intl: service(),
router: service(), router: service(),
canRemove: false,
// I think its safe to hack around this - wjw // I think its safe to hack around this - wjw
_displayState: 'active', _displayState: 'active',
// because of this the state shows as "Unknown" with bright yellow background // because of this the state shows as "Unknown" with bright yellow background
stateColor: 'text-success', stateColor: 'text-success',
canClone: computed('access.me', 'id', function() {
return this.access.allows('globalrole', 'create', 'global');
}),
canRemove: computed('id', 'builtin', function() {
return !this.builtin;
}),
isHidden: computed('id', function() { isHidden: computed('id', function() {
return SPECIAL.includes(get(this, 'id')); return SPECIAL.includes(get(this, 'id'));
}), }),
@ -74,5 +82,14 @@ export default Resource.extend({
edit() { edit() {
this.get('router').transitionTo('global-admin.security.roles.edit', this.get('id'), { queryParams: { type: 'global' } }); this.get('router').transitionTo('global-admin.security.roles.edit', this.get('id'), { queryParams: { type: 'global' } });
}, },
clone() {
this.router.transitionTo('global-admin.security.roles.new', {
queryParams: {
context: 'global',
id: this.id
}
});
}
} }
}); });

View File

@ -43,6 +43,7 @@
font-weight: bold; font-weight: bold;
border-bottom: 1px solid $accent-border; border-bottom: 1px solid $accent-border;
background: $dropdown-bg; background: $dropdown-bg;
text-align: left;
} }
> div { > div {
padding-left: $indent + $group-indent; padding-left: $indent + $group-indent;

View File

@ -1,10 +1,11 @@
import { alias, equal, or } from '@ember/object/computed'; import { alias, or } from '@ember/object/computed';
import { inject as service } from '@ember/service'; import { inject as service } from '@ember/service';
import Component from '@ember/component'; import Component from '@ember/component';
import NewOrEdit from 'ui/mixins/new-or-edit';
import C from 'ui/utils/constants'; import C from 'ui/utils/constants';
import layout from './template'; import layout from './template';
import { get, computed, set } from '@ember/object'; import { get, computed, set } from '@ember/object';
import { isEmpty } from '@ember/utils';
import ViewNewEdit from 'shared/mixins/view-new-edit';
const ruleVerbs = C.RULE_VERBS.map((verb) => `rolesPage.new.form.allow.${ verb }`); const ruleVerbs = C.RULE_VERBS.map((verb) => `rolesPage.new.form.allow.${ verb }`);
@ -23,7 +24,7 @@ const BASIC_CONTEXT = [
}, },
]; ];
export default Component.extend(NewOrEdit, { export default Component.extend(ViewNewEdit, {
intl: service(), intl: service(),
router: service(), router: service(),
layout, layout,
@ -34,11 +35,11 @@ export default Component.extend(NewOrEdit, {
readOnly: null, readOnly: null,
roleType: null, roleType: null,
contexts: BASIC_CONTEXT, contexts: BASIC_CONTEXT,
mode: 'new',
ruleVerbs, ruleVerbs,
primaryResource: alias('model.role'), primaryResource: alias('model.role'),
isGlobal: equal('roleType', 'global'), readOnlyOrBuiltIn: or('readOnly', 'builtIn', 'isView'),
readOnlyBuiltInOrGlobal: or('readOnly', 'builtIn', 'isGlobal'),
init() { init() {
this._super(...arguments); this._super(...arguments);
@ -119,6 +120,59 @@ export default Component.extend(NewOrEdit, {
return get(this, 'model.roles').filter((role) => get(this, 'model.role.id') !== role.id); return get(this, 'model.roles').filter((role) => get(this, 'model.role.id') !== role.id);
}), }),
ruleResources: computed('model.globalRoles.[]', 'model.roleTemplates.[]', function() {
const {
model: { globalRoles, roles: roleTemplates },
roleType
} = this;
let groupedResourceRules;
switch (roleType) {
case 'global':
if (!isEmpty(globalRoles)) {
groupedResourceRules = this.getRuleResourceList(globalRoles);
}
break;
default:
if (!isEmpty(roleTemplates)) {
groupedResourceRules = this.getRuleResourceList(roleTemplates.filterBy('context', roleType));
}
break;
}
return groupedResourceRules;
}),
getRuleResourceList(roles) {
const groupedResourceRules = [];
roles.forEach((role) => {
if (!isEmpty(role.rules)) {
// currently is ungrouped but can be grouped.
// The problem is that these are just default resources in a particular role,
// they are not unique so they show up duplicated under different groups.
// we need some discussion whether this is okay or not
// const group = role.name;
role.rules.forEach((rule) => {
if (!isEmpty(rule.resources)) {
rule.resources.forEach((resource) => {
if (resource !== '*') {
groupedResourceRules.push({
// group,
label: resource,
value: resource
});
}
});
}
});
}
});
return groupedResourceRules.uniqBy('value').sortBy('label');
},
getDefaultField(type) { getDefaultField(type) {
let out = ''; let out = '';

View File

@ -1,9 +1,9 @@
<section class="header clearfix"> <section class="header clearfix">
<div class="pull-left"> <div class="pull-left">
<h1> <h1>
{{#if readOnlyBuiltInOrGlobal}} {{#if readOnlyOrBuiltIn}}
{{t "rolesPage.title"}}: {{model.role.name}} {{t "rolesPage.title"}}: {{model.role.name}}
{{else if editing}} {{else if isEdit}}
{{t "rolesPage.editRole"}} {{t "rolesPage.editRole"}}
{{else}} {{else}}
{{t "rolesPage.addRole" context=readableRole}} {{t "rolesPage.addRole" context=readableRole}}
@ -12,7 +12,7 @@
</div> </div>
</section> </section>
{{#unless readOnlyBuiltInOrGlobal}} {{#unless readOnlyOrBuiltIn}}
<section class="mb-10"> <section class="mb-10">
{{form-name-description {{form-name-description
model=model.role model=model.role
@ -22,7 +22,7 @@
</section> </section>
{{/unless}} {{/unless}}
{{#if (and model.role.description readOnlyBuiltInOrGlobal)}} {{#if (and model.role.description readOnlyOrBuiltIn)}}
<div class="row mb-30"> <div class="row mb-30">
{{banner-message {{banner-message
color="bg-secondary mb-0 mt-10" color="bg-secondary mb-0 mt-10"
@ -65,7 +65,7 @@
</label> </label>
</div> </div>
{{/if}} {{/if}}
{{#if editing}} {{#if isEdit}}
<p class="help-block"> <p class="help-block">
{{t "rolesPage.new.form.locked.detail"}} {{t "rolesPage.new.form.locked.detail"}}
</p> </p>
@ -105,7 +105,7 @@
</label> </label>
</div> </div>
{{/if}} {{/if}}
{{#if editing}} {{#if isEdit}}
<p class="help-block"> <p class="help-block">
{{t "rolesPage.new.form.locked.detail"}} {{t "rolesPage.new.form.locked.detail"}}
</p> </p>
@ -125,7 +125,7 @@
title=(t "rolesPage.resources.title") title=(t "rolesPage.resources.title")
}} }}
{{#if ruleArray.length}} {{#if ruleArray.length}}
<table class="table fixed no-lines"> <table class="table fixed">
<thead> <thead>
<tr> <tr>
{{#each ruleVerbs as |verb|}} {{#each ruleVerbs as |verb|}}
@ -140,21 +140,23 @@
</thead> </thead>
<tbody> <tbody>
{{#each ruleArray as |rule|}} {{#each ruleArray as |rule|}}
{{role-rule-row <RoleRuleRow
readOnly=readOnlyBuiltInOrGlobal @readOnly={{readOnlyOrBuiltIn}}
rule=rule @rule={{rule}}
remove=(action "removeRule") @rules={{ruleResources}}
}} @editing={{or isEdit isNew}}
@remove={{action "removeRule"}}
/>
{{/each}} {{/each}}
</tbody> </tbody>
</table> </table>
{{else if readOnlyBuiltInOrGlobal}} {{else if readOnlyOrBuiltIn}}
<span class="text-muted"> <span class="text-muted">
{{t "generic.none"}} {{t "generic.none"}}
</span> </span>
{{/if}} {{/if}}
{{#unless readOnlyBuiltInOrGlobal}} {{#unless readOnlyOrBuiltIn}}
<div> <div>
<button class="btn bg-primary icon-btn p-0" {{action "addRule"}}> <button class="btn bg-primary icon-btn p-0" {{action "addRule"}}>
<span class="darken"><i class="icon icon-plus text-small"/></span> <span class="darken"><i class="icon icon-plus text-small"/></span>
@ -164,6 +166,7 @@
{{/unless}} {{/unless}}
{{/accordion-list-item}} {{/accordion-list-item}}
{{#unless (eq roleType "global")}}
{{#accordion-list-item {{#accordion-list-item
title=(t "rolesPage.inherit.title") title=(t "rolesPage.inherit.title")
detail=(t "rolesPage.inherit.detail") detail=(t "rolesPage.inherit.detail")
@ -185,18 +188,18 @@
{{#each roleArray as |role|}} {{#each roleArray as |role|}}
{{other-role-row {{other-role-row
model=role model=role
readOnly=readOnlyBuiltInOrGlobal readOnly=readOnlyOrBuiltIn
otherRoles=otherRoles otherRoles=otherRoles
remove=(action "removeOtherRole") remove=(action "removeOtherRole")
}} }}
{{/each}} {{/each}}
</tbody> </tbody>
</table> </table>
{{else if readOnlyBuiltInOrGlobal}} {{else if readOnlyOrBuiltIn}}
<span class="text-muted">{{t "generic.none"}}</span> <span class="text-muted">{{t "generic.none"}}</span>
{{/if}} {{/if}}
{{#unless readOnlyBuiltInOrGlobal}} {{#unless readOnlyOrBuiltIn}}
<div> <div>
<button class="btn bg-primary icon-btn p-0" {{action "addOtherRole"}}> <button class="btn bg-primary icon-btn p-0" {{action "addOtherRole"}}>
<span class="darken"><i class="icon icon-plus text-small"/></span> <span class="darken"><i class="icon icon-plus text-small"/></span>
@ -205,13 +208,14 @@
</div> </div>
{{/unless}} {{/unless}}
{{/accordion-list-item}} {{/accordion-list-item}}
{{/unless}}
{{/accordion-list}} {{/accordion-list}}
{{top-errors errors=errors}} {{top-errors errors=errors}}
{{#unless readOnly}} {{#unless readOnly}}
{{save-cancel {{save-cancel
editing=editing editing=isEdit
save=(action "save") save=(action "save")
cancel=(action "cancel") cancel=(action "cancel")
}} }}

View File

@ -2,6 +2,7 @@ import Component from '@ember/component';
import { get, set, observer } from '@ember/object' import { get, set, observer } from '@ember/object'
import C from 'ui/utils/constants'; import C from 'ui/utils/constants';
import layout from './template'; import layout from './template';
import { isEmpty } from '@ember/utils';
const verbs = C.RULE_VERBS; const verbs = C.RULE_VERBS;
@ -12,13 +13,15 @@ export default Component.extend({
resource: null, resource: null,
apiGroup: null, apiGroup: null,
readOnly: null, readOnly: null,
editing: true,
tagName: 'TR', tagName: 'TR',
classNames: 'main-row', classNames: 'main-row',
init() { init() {
this._super(...arguments); this._super(...arguments);
const rule = get(this, 'rule'); const { rule } = this;
let { rules } = this;
const currentVerbs = get(rule, 'verbs'); const currentVerbs = get(rule, 'verbs');
set(this, 'verbs', verbs.map((verb) => { set(this, 'verbs', verbs.map((verb) => {
@ -27,7 +30,9 @@ export default Component.extend({
value: currentVerbs.indexOf('*') > -1 || currentVerbs.indexOf(verb) > -1, value: currentVerbs.indexOf('*') > -1 || currentVerbs.indexOf(verb) > -1,
}; };
})); }));
const rules = C.ROLE_RULES.sort();
if (isEmpty(rules)) {
rules = C.ROLE_RULES.sort();
set(this, 'rules', rules.map((rule) => { set(this, 'rules', rules.map((rule) => {
return { return {
@ -35,6 +40,8 @@ export default Component.extend({
value: rule.toLowerCase(), value: rule.toLowerCase(),
}; };
})); }));
}
if ((get(rule, 'resources') || []).get('length') > 0) { if ((get(rule, 'resources') || []).get('length') > 0) {
set(this, 'resource', get(rule, 'resources').join(',')); set(this, 'resource', get(rule, 'resources').join(','));
} }

View File

@ -1,5 +1,5 @@
{{#each verbs as |verb|}} {{#each verbs as |verb|}}
<td class="text-center" data-title="{{verb.key}}" > <td class="pt-5 pb-5 text-center" data-title="{{verb.key}}" >
{{#if readOnly}} {{#if readOnly}}
{{#if verb.value}} {{#if verb.value}}
<i class="icon icon-check"/> <i class="icon icon-check"/>
@ -7,28 +7,52 @@
<span class="text-muted">&ndash;</span> <span class="text-muted">&ndash;</span>
{{/if}} {{/if}}
{{else}} {{else}}
{{input type="checkbox" checked=verb.value disabled=readOnly}} <Input
@type="checkbox"
@checked={{verb.value}}
@disabled={{readOnly}}
/>
{{/if}} {{/if}}
</td> </td>
{{/each}} {{/each}}
<td> <td class="pt-5 pb-5">
{{#if (and readOnly rule.nonResourceURLs)}} {{#if (and readOnly rule.nonResourceURLs)}}
<span class="text-muted">Non-Resource URL: {{join-array rule.nonResourceURLs}}</span> <span class="text-muted">
Non-Resource URL: {{join-array rule.nonResourceURLs}}
</span>
{{else}} {{else}}
{{searchable-select allowCustom=true content=rules value=resource readOnly=readOnly}} <InputOrDisplay
@editable={{editing}}
@value={{resource}}
>
<SearchableSelect
@allowCustom={{true}}
@content={{rules}}
@value={{resource}}
@readOnly={{readOnly}}
/>
</InputOrDisplay>
{{/if}} {{/if}}
</td> </td>
<td>&nbsp;</td> <td class="pt-5 pb-5">&nbsp;</td>
<td> <td class="pt-5 pb-5">
{{input type="text" <InputOrDisplay
value=apiGroup @editable={{editing}}
classNames="form-control" @value={{apiGroup}}
disabled=readOnly >
}} <Input
@type="text"
@value={{apiGroup}}
@classNames="form-control"
@disabled={{readOnly}}
/>
</InputOrDisplay>
</td> </td>
<td>&nbsp;</td> <td class="pt-5 pb-5">&nbsp;</td>
{{#unless readOnly}} {{#unless readOnly}}
<div class="input-group-btn"> <div class="input-group-btn">
<button class="btn bg-primary btn-sm" {{action "remove"}}><i class="icon icon-minus"/></button> <button class="btn bg-primary btn-sm" {{action "remove"}}>
<i class="icon icon-minus"/>
</button>
</div> </div>
{{/unless}} {{/unless}}

View File

@ -1,6 +1,6 @@
{{new-edit-role <NewEditRole
model=model @model={{model}}
readOnly=true @readOnly={{true}}
editing=false @mode="view"
roleType=type @roleType={{type}}
}} />

View File

@ -1,5 +1,5 @@
{{new-edit-role <NewEditRole
model=model @model={{model}}
editing=true @mode="edit"
roleType=type @roleType={{type}}
}} />

View File

@ -65,7 +65,7 @@ export default Controller.extend({
return get(this, 'model.roleTemplates').filter( (role ) => !get(role, 'hidden') && (get(role, 'context') !== 'project') || !role.hasOwnProperty('context')); return get(this, 'model.roleTemplates').filter( (role ) => !get(role, 'hidden') && (get(role, 'context') !== 'project') || !role.hasOwnProperty('context'));
}), }),
filteredContent: computed('context', 'model.roleTemplates.@each.{name,state,transitioning}', 'showOnlyDefaults', function() { filteredContent: computed('context', 'clusterRows.@each.{name,state,transitioning}', 'projectRows.@each.{name,state,transitioning}', 'showOnlyDefaults', 'globalRows.@each.{name,state}', function() {
let content = null; let content = null;
const { context, showOnlyDefaults } = this; const { context, showOnlyDefaults } = this;
let headers = [...HEADERS]; let headers = [...HEADERS];

View File

@ -10,7 +10,7 @@ export default Route.extend({
model(/* params */) { model(/* params */) {
return hash({ return hash({
roleTemplates: get(this, 'roleTemplateService.allVisibleRoleTemplates'), roleTemplates: get(this, 'roleTemplateService.allVisibleRoleTemplates'),
globalRoles: get(this, 'globalStore').find('globalRole'), globalRoles: get(this, 'globalStore').findAll('globalrole'),
}); });
}, },

View File

@ -10,19 +10,17 @@
<a href="#" {{action (action (mut context) ) "project"}} class="{{if (eq context "project") "active" ""}}">{{t "rolesPage.headers.project"}}</a> <a href="#" {{action (action (mut context) ) "project"}} class="{{if (eq context "project") "active" ""}}">{{t "rolesPage.headers.project"}}</a>
</li> </li>
</ul> </ul>
{{#unless (eq context "global")}}
<div class="right-buttons"> <div class="right-buttons">
{{#link-to {{#link-to
"security.roles.new" "security.roles.new"
(query-params context=context) (query-params context=context)
classNames="btn btn-sm bg-primary right-divider-btn" classNames="btn btn-sm bg-primary right-divider-btn"
disabled=(rbac-prevents resource="roletemplate" scope="global" permission="create") disabled=(rbac-prevents resource="globalrole" scope="global" permission="create")
}} }}
{{t "rolesPage.addRole" context=readableMode}} {{t "rolesPage.addRole" context=readableMode}}
{{/link-to}} {{/link-to}}
<span id="header-search"/> <span id="header-search"/>
</div> </div>
{{/unless}}
</section> </section>
<section class="instances"> <section class="instances">
{{#sortable-table {{#sortable-table

View File

@ -1,6 +1,7 @@
import Controller from '@ember/controller'; import Controller from '@ember/controller';
export default Controller.extend({ export default Controller.extend({
queryParams: ['id'], queryParams: ['id', 'context'],
id: null, id: null,
context: null,
}); });

View File

@ -6,38 +6,54 @@ import { hash } from 'rsvp';
export default Route.extend({ export default Route.extend({
globalStore: service(), globalStore: service(),
roleTemplateService: service('roleTemplate'), roleTemplateService: service('roleTemplate'),
model( params ) { model( params ) {
const store = get(this, 'globalStore'); const store = get(this, 'globalStore');
return hash({ return hash({
policies: store.find('podSecurityPolicyTemplate'), policies: store.find('podSecurityPolicyTemplate'),
roles: get(this, 'roleTemplateService').fetchFilteredRoleTemplates(null, null), roles: get(this, 'roleTemplateService').fetchFilteredRoleTemplates(null, null),
globalRoles: store.find('globalRole'),
}).then( (res) => { }).then( (res) => {
const id = get(params, 'id'); const { id, context = 'project' } = params;
let role; let role;
if ( id ) { if ( id ) {
if (context === 'global') {
role = res.globalRoles.findBy('id', id);
} else {
role = res.roles.findBy('id', id); role = res.roles.findBy('id', id);
}
if ( !role ) { if ( !role ) {
this.replaceWith('security.roles.index'); this.replaceWith('security.roles.index');
} }
role = role.cloneForNew() ; role = role.cloneForNew() ;
set(role, 'context', context);
delete role['builtin']; delete role['builtin'];
delete role['annotations']; delete role['annotations'];
delete role['labels']; delete role['labels'];
delete role['links']; delete role['links'];
} else {
if (context === 'global') {
role = store.createRecord({
type: 'globalRole',
context
});
} else { } else {
role = store.createRecord({ role = store.createRecord({
type: 'roleTemplate', type: 'roleTemplate',
context: get(params, 'context') || 'project',
name: '', name: '',
rules: [], rules: [],
hidden: false, hidden: false,
locked: false, locked: false,
context
}); });
} }
}
set(res, 'role', role); set(res, 'role', role);
@ -50,6 +66,10 @@ export default Route.extend({
setProperties(controller, { type: get(model, 'role.context') }); setProperties(controller, { type: get(model, 'role.context') });
}, },
queryParams: { context: { refreshModel: false } },
queryParams: {
context: { refreshModel: true },
id: { refreshModel: true }
},
}); });

View File

@ -1,5 +1,5 @@
{{new-edit-role <NewEditRole
model=model @model={{model}}
editing=false @mode="new"
roleType=type @roleType={{type}}
}} />