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({
access: service(),
intl: service(),
router: service(),
canRemove: false,
// I think its safe to hack around this - wjw
_displayState: 'active',
// because of this the state shows as "Unknown" with bright yellow background
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() {
return SPECIAL.includes(get(this, 'id'));
}),
@ -74,5 +82,14 @@ export default Resource.extend({
edit() {
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;
border-bottom: 1px solid $accent-border;
background: $dropdown-bg;
text-align: left;
}
> div {
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 Component from '@ember/component';
import NewOrEdit from 'ui/mixins/new-or-edit';
import C from 'ui/utils/constants';
import layout from './template';
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 }`);
@ -23,22 +24,22 @@ const BASIC_CONTEXT = [
},
];
export default Component.extend(NewOrEdit, {
intl: service(),
router: service(),
export default Component.extend(ViewNewEdit, {
intl: service(),
router: service(),
layout,
model: null,
model: null,
ruleArray: null,
roleArray: null,
readOnly: null,
roleType: null,
contexts: BASIC_CONTEXT,
ruleArray: null,
roleArray: null,
readOnly: null,
roleType: null,
contexts: BASIC_CONTEXT,
mode: 'new',
ruleVerbs,
primaryResource: alias('model.role'),
isGlobal: equal('roleType', 'global'),
readOnlyBuiltInOrGlobal: or('readOnly', 'builtIn', 'isGlobal'),
primaryResource: alias('model.role'),
readOnlyOrBuiltIn: or('readOnly', 'builtIn', 'isView'),
init() {
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);
}),
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) {
let out = '';

View File

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

View File

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

View File

@ -1,5 +1,5 @@
{{#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 verb.value}}
<i class="icon icon-check"/>
@ -7,28 +7,52 @@
<span class="text-muted">&ndash;</span>
{{/if}}
{{else}}
{{input type="checkbox" checked=verb.value disabled=readOnly}}
<Input
@type="checkbox"
@checked={{verb.value}}
@disabled={{readOnly}}
/>
{{/if}}
</td>
{{/each}}
<td>
<td class="pt-5 pb-5">
{{#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}}
{{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}}
</td>
<td>&nbsp;</td>
<td>
{{input type="text"
value=apiGroup
classNames="form-control"
disabled=readOnly
}}
<td class="pt-5 pb-5">&nbsp;</td>
<td class="pt-5 pb-5">
<InputOrDisplay
@editable={{editing}}
@value={{apiGroup}}
>
<Input
@type="text"
@value={{apiGroup}}
@classNames="form-control"
@disabled={{readOnly}}
/>
</InputOrDisplay>
</td>
<td>&nbsp;</td>
<td class="pt-5 pb-5">&nbsp;</td>
{{#unless readOnly}}
<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>
{{/unless}}

View File

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

View File

@ -1,5 +1,5 @@
{{new-edit-role
model=model
editing=true
roleType=type
}}
<NewEditRole
@model={{model}}
@mode="edit"
@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'));
}),
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;
const { context, showOnlyDefaults } = this;
let headers = [...HEADERS];

View File

@ -10,7 +10,7 @@ export default Route.extend({
model(/* params */) {
return hash({
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>
</li>
</ul>
{{#unless (eq context "global")}}
<div class="right-buttons">
{{#link-to
"security.roles.new"
(query-params context=context)
classNames="btn btn-sm bg-primary right-divider-btn"
disabled=(rbac-prevents resource="roletemplate" scope="global" permission="create")
}}
{{t "rolesPage.addRole" context=readableMode}}
{{/link-to}}
<span id="header-search"/>
</div>
{{/unless}}
<div class="right-buttons">
{{#link-to
"security.roles.new"
(query-params context=context)
classNames="btn btn-sm bg-primary right-divider-btn"
disabled=(rbac-prevents resource="globalrole" scope="global" permission="create")
}}
{{t "rolesPage.addRole" context=readableMode}}
{{/link-to}}
<span id="header-search"/>
</div>
</section>
<section class="instances">
{{#sortable-table

View File

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

View File

@ -6,37 +6,53 @@ import { hash } from 'rsvp';
export default Route.extend({
globalStore: service(),
roleTemplateService: service('roleTemplate'),
model( params ) {
const store = get(this, 'globalStore');
return hash({
policies: store.find('podSecurityPolicyTemplate'),
roles: get(this, 'roleTemplateService').fetchFilteredRoleTemplates(null, null),
policies: store.find('podSecurityPolicyTemplate'),
roles: get(this, 'roleTemplateService').fetchFilteredRoleTemplates(null, null),
globalRoles: store.find('globalRole'),
}).then( (res) => {
const id = get(params, 'id');
const { id, context = 'project' } = params;
let role;
if ( id ) {
role = res.roles.findBy('id', id);
if (context === 'global') {
role = res.globalRoles.findBy('id', id);
} else {
role = res.roles.findBy('id', id);
}
if ( !role ) {
this.replaceWith('security.roles.index');
}
role = role.cloneForNew() ;
set(role, 'context', context);
delete role['builtin'];
delete role['annotations'];
delete role['labels'];
delete role['links'];
} else {
role = store.createRecord({
type: 'roleTemplate',
context: get(params, 'context') || 'project',
name: '',
rules: [],
hidden: false,
locked: false,
});
if (context === 'global') {
role = store.createRecord({
type: 'globalRole',
context
});
} else {
role = store.createRecord({
type: 'roleTemplate',
name: '',
rules: [],
hidden: false,
locked: false,
context
});
}
}
set(res, 'role', role);
@ -50,6 +66,10 @@ export default Route.extend({
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
model=model
editing=false
roleType=type
}}
<NewEditRole
@model={{model}}
@mode="new"
@roleType={{type}}
/>