feat: 🎸 add principals list tab

This commit is contained in:
Randall Morey committed 2020-08-12 13:41:23 -04:00
1 parent 4ce8f0aa99
commit 971e483d01
16 files changed
+201 -4

No files matched your search

@@ -0,0 +1,20 @@
import Fragment from 'ember-data-model-fragments/fragment';
import { attr } from '@ember-data/model';
export default class FragmentPrincipal extends Fragment {
// =attributes
@attr('string', {
description: 'ID of the related scope'
}) scope_id;
@attr('string', {
description: 'ID of the related principal (user or group)'
}) principal_id;
@attr('string', {
description: 'Type of the related principal (user or group)'
}) type;
}
+9
View File
@@ -5,6 +5,15 @@ export default class RoleModel extends GeneratedRoleModel {
// =attributes
/**
* Principals are users and groups assigned to a role. They are represented
* as references to User and Group instances. Since Ember Data relationships
* are wanting, we do not model these as a polymorphic relationship as might
* see obvious. Instead, the application layer is expected to load referenced
* users and groups as needed.
*/
@fragmentArray('fragment-principal', {readOnly: true}) principals;
/**
* Grants is read-only _most_ under normal circumstances. But grants can
* be persisted via a dedicated call to `saveGrants()`.
@@ -0,0 +1,18 @@
import JSONSerializer from '@ember-data/serializer/json';
export default class FragmentPrincipalSerializer extends JSONSerializer {
// =methods
/**
* Renames `id` to `principal_id` because fragments may not have IDs.
* Otherwise normalizes as normal.
* @param {ModelClass} typeClass
* @param {object} resourceHash
* @return {object}
*/
normalize(typeClass, { id: principal_id, ...obj }) {
return super.normalize(typeClass, { principal_id, ...obj });
}
}
@@ -0,0 +1 @@
export { default } from 'api/models/fragment-principal';
@@ -0,0 +1 @@
export { default } from 'api/serializers/fragment-principal';
@@ -0,0 +1,13 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
module('Unit | Model | fragment principal', function(hooks) {
setupTest(hooks);
// Replace this with your real tests.
test('it exists', function(assert) {
let store = this.owner.lookup('service:store');
let model = store.createRecord('fragment-principal', {});
assert.ok(model);
});
});
@@ -0,0 +1,30 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
import FragmentPrincipal from 'api/models/fragment-principal';
module('Unit | Serializer | fragment principal', function(hooks) {
setupTest(hooks);
test('it normalizes `id` into `principal_id`', function(assert) {
assert.expect(1);
const store = this.owner.lookup('service:store');
const serializer = store.serializerFor('fragment-principal');
const normalized = serializer.normalize(FragmentPrincipal, {
scope_id: '1',
principal_id: '2',
type: 'user'
});
assert.deepEqual(normalized, {
data: {
attributes: {
scope_id: '1',
principal_id: '2',
type: 'user'
},
id: null,
relationships: {},
type: 'fragment-principal'
}
});
});
});
+1
View File
@@ -23,6 +23,7 @@ Router.map(function () {
this.route('roles', function () {
this.route('role', { path: ':role_id' }, function () {
this.route('grants');
this.route('principals');
});
this.route('new');
});
@@ -0,0 +1,37 @@
import Route from '@ember/routing/route';
import { all } from 'rsvp';
export default class ScopesScopeRolesRolePrincipalsRoute extends Route {
// =methods
/**
* Returns users and groups associated with this role.
* @param {object} params
* @return {Promise{RoleModel}}
*/
model() {
const adapterOptions = { scopeID: this.modelFor('scopes.scope').id };
const role = this.modelFor('scopes.scope.roles.role');
const users = role.principals
.filterBy('type', 'user')
.map(({ principal_id }) =>
this.store.findRecord('user', principal_id, { adapterOptions })
.then(model => ({
type: 'user',
model
}))
);
const groups = role.principals
.filterBy('type', 'group')
.map(({ principal_id }) =>
this.store.findRecord('group', principal_id, { adapterOptions })
.then(model => ({
type: 'group',
model
}))
);
return all(users.concat(groups));
}
}
@@ -15,6 +15,9 @@
<nav.link @route='scopes.scope.roles.role.index'>
{{t 'titles.settings'}}
</nav.link>
<nav.link @route='scopes.scope.roles.role.principals'>
{{t 'titles.principals'}}
</nav.link>
<nav.link @route='scopes.scope.roles.role.grants'>
{{t 'titles.grants'}}
</nav.link>
@@ -0,0 +1,22 @@
{{title (t 'titles.principals')}}
<Rose::Table as |table|>
<table.header as |header|>
<header.row as |row|>
<row.cell>{{t 'form.label.name'}}</row.cell>
<row.cell>{{t 'form.label.type'}}</row.cell>
</header.row>
</table.header>
<table.body as |body|>
{{#each @model as |principal|}}
<body.row as |row|>
<row.cell>
{{principal.model.displayName}}
</row.cell>
<row.cell>
{{t (concat 'resources.' principal.type)}}
</row.cell>
</body.row>
{{/each}}
</table.body>
</Rose::Table>
+26
View File
@@ -1,4 +1,5 @@
import factory from '../generated/factories/role';
import { trait } from 'ember-cli-mirage';
export default factory.extend({
@@ -8,4 +9,29 @@ export default factory.extend({
'id=*;type=host-catalog;actions=create,read'
],
/**
* Create roles with associated "principals": related users and groups.
*/
withPrincipals: trait({
afterCreate(role, server) {
const users = server.createList('user', 2);
const groups = server.createList('group', 2);
const userPrincipalFragments = users.map(user => ({
scope_id: role.scope.id,
id: user.id,
type: 'user'
}));
const groupPrincipalFragments = groups.map(group => ({
scope_id: role.scope.id,
id: group.id,
type: 'group'
}));
const principals = [
...userPrincipalFragments,
...groupPrincipalFragments
];
role.update({ principals });
}
})
});
+3 -4
View File
@@ -18,11 +18,10 @@ export default function(server) {
// User
server.createList('user', 5, { scope });
// Role
server.createList('role', 5, { scope });
// Groups
// Group
server.createList('group', 5, { scope });
// Role
server.createList('role', 5, { scope }, 'withPrincipals');
// Other resources
+4
View File
@@ -218,6 +218,10 @@ module('Acceptance | roles', function (hooks) {
);
});
test('principals may be viewed in principals tab', async function (assert) {
assert.expect(0);
});
test('grants can be updated and saved or canceled', async function (assert) {
assert.expect(0);
});
@@ -0,0 +1,11 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
module('Unit | Route | scopes/scope/roles/role/principals', function(hooks) {
setupTest(hooks);
test('it exists', function(assert) {
let route = this.owner.lookup('route:scopes/scope/roles/role/principals');
assert.ok(route);
});
});
+2
View File
@@ -47,6 +47,7 @@ titles:
groups-welcome: Welcome to Groups
grants: Permissions
new-grant: New Grant
principals: Principals
descriptions:
host-catalogs: Overview of collections of hosts and host sets.
empty-set: There are no items to display yet. You may be able to add items or try back later.
@@ -69,6 +70,7 @@ form:
identification: Username
password: Password
grant: Grant
principal: Principal
errors:
titles:
unauthenticated-error: You are not signed in