From 2b59aeee4b7a22728829facfd8a87a70371a3476 Mon Sep 17 00:00:00 2001 From: Susmitha Girumala Date: Mon, 22 Jun 2020 21:39:38 -0400 Subject: [PATCH 1/6] =?UTF-8?q?chore:=20=F0=9F=A4=96=20Add=20role=20CRUD?= =?UTF-8?q?=20forms=20to=20admin=20ui?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- addons/api/addon/adapters/role.js | 14 ++ addons/api/app/adapters/role.js | 1 + addons/api/tests/unit/adapters/role-test.js | 16 ++ ui/core/app/components/form/role/index.hbs | 49 +++++ ui/core/app/controllers/orgs/org/roles.js | 18 ++ ui/core/app/controllers/orgs/org/roles/new.js | 18 ++ .../app/controllers/orgs/org/roles/role.js | 18 ++ ui/core/app/router.js | 4 + ui/core/app/routes/orgs/org/roles.js | 32 +++ ui/core/app/routes/orgs/org/roles/index.js | 3 + ui/core/app/routes/orgs/org/roles/new.js | 50 +++++ ui/core/app/routes/orgs/org/roles/role.js | 64 ++++++ ui/core/app/templates/orgs/org/roles.hbs | 3 + .../app/templates/orgs/org/roles/_sidebar.hbs | 18 ++ .../app/templates/orgs/org/roles/index.hbs | 63 ++++++ ui/core/app/templates/orgs/org/roles/new.hbs | 17 ++ ui/core/app/templates/orgs/org/roles/role.hbs | 3 + .../templates/orgs/org/roles/role/index.hbs | 27 +++ ui/core/mirage/config.js | 7 + ui/core/mirage/factories/role.js | 3 + ui/core/mirage/generated/factories/role.js | 16 ++ ui/core/mirage/scenarios/default.js | 4 + ui/core/mirage/serializers/role.js | 5 + ui/core/tests/acceptance/roles-test.js | 191 ++++++++++++++++++ .../unit/controllers/orgs/org/roles-test.js | 11 + .../controllers/orgs/org/roles/new-test.js | 11 + .../controllers/orgs/org/roles/role-test.js | 11 + .../tests/unit/routes/orgs/org/roles-test.js | 11 + .../unit/routes/orgs/org/roles/index-test.js | 11 + .../unit/routes/orgs/org/roles/new-test.js | 11 + .../unit/routes/orgs/org/roles/role-test.js | 11 + ui/core/translations/en-us.yaml | 11 + 32 files changed, 732 insertions(+) create mode 100644 addons/api/addon/adapters/role.js create mode 100644 addons/api/app/adapters/role.js create mode 100644 addons/api/tests/unit/adapters/role-test.js create mode 100644 ui/core/app/components/form/role/index.hbs create mode 100644 ui/core/app/controllers/orgs/org/roles.js create mode 100644 ui/core/app/controllers/orgs/org/roles/new.js create mode 100644 ui/core/app/controllers/orgs/org/roles/role.js create mode 100644 ui/core/app/routes/orgs/org/roles.js create mode 100644 ui/core/app/routes/orgs/org/roles/index.js create mode 100644 ui/core/app/routes/orgs/org/roles/new.js create mode 100644 ui/core/app/routes/orgs/org/roles/role.js create mode 100644 ui/core/app/templates/orgs/org/roles.hbs create mode 100644 ui/core/app/templates/orgs/org/roles/_sidebar.hbs create mode 100644 ui/core/app/templates/orgs/org/roles/index.hbs create mode 100644 ui/core/app/templates/orgs/org/roles/new.hbs create mode 100644 ui/core/app/templates/orgs/org/roles/role.hbs create mode 100644 ui/core/app/templates/orgs/org/roles/role/index.hbs create mode 100644 ui/core/mirage/factories/role.js create mode 100644 ui/core/mirage/generated/factories/role.js create mode 100644 ui/core/mirage/serializers/role.js create mode 100644 ui/core/tests/acceptance/roles-test.js create mode 100644 ui/core/tests/unit/controllers/orgs/org/roles-test.js create mode 100644 ui/core/tests/unit/controllers/orgs/org/roles/new-test.js create mode 100644 ui/core/tests/unit/controllers/orgs/org/roles/role-test.js create mode 100644 ui/core/tests/unit/routes/orgs/org/roles-test.js create mode 100644 ui/core/tests/unit/routes/orgs/org/roles/index-test.js create mode 100644 ui/core/tests/unit/routes/orgs/org/roles/new-test.js create mode 100644 ui/core/tests/unit/routes/orgs/org/roles/role-test.js diff --git a/addons/api/addon/adapters/role.js b/addons/api/addon/adapters/role.js new file mode 100644 index 000000000..c9b0c13b0 --- /dev/null +++ b/addons/api/addon/adapters/role.js @@ -0,0 +1,14 @@ +import ApplicationAdapter from './application'; +import ScopeAdapterMixin from '../mixins/scope-adapter'; + +export default class RoleAdapter extends ApplicationAdapter.extend( + ScopeAdapterMixin +) { + // =attributes + + /** + * Role is scoped to org only, for now. + * @type {boolean} + */ + includeProject = false; +} diff --git a/addons/api/app/adapters/role.js b/addons/api/app/adapters/role.js new file mode 100644 index 000000000..f12325923 --- /dev/null +++ b/addons/api/app/adapters/role.js @@ -0,0 +1 @@ +export { default } from 'api/adapters/role'; diff --git a/addons/api/tests/unit/adapters/role-test.js b/addons/api/tests/unit/adapters/role-test.js new file mode 100644 index 000000000..adb7b1abd --- /dev/null +++ b/addons/api/tests/unit/adapters/role-test.js @@ -0,0 +1,16 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; +import config from 'ember-get-config'; + +module('Unit | Adapter | role', function(hooks) { + setupTest(hooks); + + test('it generates URL prefixes without project scope', function (assert) { + assert.expect(1); + const service = this.owner.lookup('service:scope'); + service.org = { id: 1 }; + service.project = { id: 2 }; + const adapter = this.owner.lookup('adapter:role'); + assert.equal(adapter.urlPrefix(), `${config.api.namespace}/orgs/1`); + }); +}); diff --git a/ui/core/app/components/form/role/index.hbs b/ui/core/app/components/form/role/index.hbs new file mode 100644 index 000000000..9db695baa --- /dev/null +++ b/ui/core/app/components/form/role/index.hbs @@ -0,0 +1,49 @@ + + + + {{#if @model.errors.name}} + + {{#each @model.errors.name as |error|}} + {{error.message}} + {{/each}} + + {{/if}} + + + + {{#if @model.errors.description}} + + {{#each @model.errors.description as |error|}} + {{error.message}} + {{/each}} + + {{/if}} + + + + + diff --git a/ui/core/app/controllers/orgs/org/roles.js b/ui/core/app/controllers/orgs/org/roles.js new file mode 100644 index 000000000..7f62b0e1a --- /dev/null +++ b/ui/core/app/controllers/orgs/org/roles.js @@ -0,0 +1,18 @@ +import Controller from '@ember/controller'; +import { inject as service } from '@ember/service'; + +export default class OrgsOrgRolesController extends Controller { + // =services + + @service intl; + + // =attributes + + /** + * @type {string} + */ + get breadCrumb() { + return this.intl.t('resources.roles'); + } + +} diff --git a/ui/core/app/controllers/orgs/org/roles/new.js b/ui/core/app/controllers/orgs/org/roles/new.js new file mode 100644 index 000000000..38a098eaa --- /dev/null +++ b/ui/core/app/controllers/orgs/org/roles/new.js @@ -0,0 +1,18 @@ +import Controller from '@ember/controller'; +import { inject as service } from '@ember/service'; + +export default class OrgsOrgRolesNewController extends Controller { + // =services + + @service intl; + + // =attributes + + /** + * @type {string} + */ + get breadCrumb() { + return this.intl.t('titles.new'); + } + +} diff --git a/ui/core/app/controllers/orgs/org/roles/role.js b/ui/core/app/controllers/orgs/org/roles/role.js new file mode 100644 index 000000000..7a5564ac0 --- /dev/null +++ b/ui/core/app/controllers/orgs/org/roles/role.js @@ -0,0 +1,18 @@ +import Controller from '@ember/controller'; +import { inject as service } from '@ember/service'; + +export default class OrgsOrgRolesRoleController extends Controller { + // =services + + @service intl; + + // =attributes + + /** + * @type {string} + */ + get breadCrumb() { + return this.intl.t('resources.role'); + } + +} diff --git a/ui/core/app/router.js b/ui/core/app/router.js index 76af92941..b9360c541 100644 --- a/ui/core/app/router.js +++ b/ui/core/app/router.js @@ -18,6 +18,10 @@ Router.map(function () { }); this.route('new'); }); + this.route('roles', function() { + this.route('role', { path: ':role_id' }, function() {}); + this.route('new'); + }); }); }); }); diff --git a/ui/core/app/routes/orgs/org/roles.js b/ui/core/app/routes/orgs/org/roles.js new file mode 100644 index 000000000..9c804bf0b --- /dev/null +++ b/ui/core/app/routes/orgs/org/roles.js @@ -0,0 +1,32 @@ +import Route from '@ember/routing/route'; + +export default class OrgsOrgRolesRoute extends Route { + // =methods + + /** + * Returns all roles from store. + * @return {Promise[RoleModel]} + */ + model() { + return this.store.findAll('role').catch(() => { + // FIX ME: API response of "{}" generates following error: + // Error while processing route: orgs.org.roles.index + // Error: Assertion Failed: You made a 'findAll' request for 'role' records, but the adapter's response did not have any data + }); + } + + /** + * Renders the roles-specific sidebar template. + * @override + * @param {object} controller + * @param {object} model + */ + renderTemplate(controller, model) { + this.render('orgs/org/roles/_sidebar', { + into: 'application', + outlet: 'sidebar', + model: model + }); + super.renderTemplate(...arguments); + } +} diff --git a/ui/core/app/routes/orgs/org/roles/index.js b/ui/core/app/routes/orgs/org/roles/index.js new file mode 100644 index 000000000..89b868927 --- /dev/null +++ b/ui/core/app/routes/orgs/org/roles/index.js @@ -0,0 +1,3 @@ +import Route from '@ember/routing/route'; + +export default class OrgsOrgRolesIndexRoute extends Route { } diff --git a/ui/core/app/routes/orgs/org/roles/new.js b/ui/core/app/routes/orgs/org/roles/new.js new file mode 100644 index 000000000..cad253804 --- /dev/null +++ b/ui/core/app/routes/orgs/org/roles/new.js @@ -0,0 +1,50 @@ +import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; +import { action } from '@ember/object'; + +export default class OrgsOrgRolesNewRoute extends Route { + // =services + + @service intl; + @service notify; + + // =methods + + /** + * Returns a new unsaved role. + * @return {Promise{RoleModel}} + */ + model() { + return this.store.createRecord('role'); + } + + // =actions + + /** + * Rollback changes on role by destroying unsaved instance + * and redirect to role index. + * @param {RoleModel} role + */ + @action + cancel(role) { + role.rollbackAttributes(); + this.transitionTo('orgs.org.roles'); + } + + /** + * Handle saving a role. + * @param {RoleModel} role + * @param {Event} e + */ + @action + async save(role) { + try { + await role.save(); + this.transitionTo('orgs.org.roles.role', role); + this.notify.success(this.intl.t('notify.role.create-success')); + } catch (error) { + // TODO: replace with translated strings + this.notify.error(error.message, { closeAfter: null }); + } + } +} diff --git a/ui/core/app/routes/orgs/org/roles/role.js b/ui/core/app/routes/orgs/org/roles/role.js new file mode 100644 index 000000000..1820f2ba1 --- /dev/null +++ b/ui/core/app/routes/orgs/org/roles/role.js @@ -0,0 +1,64 @@ +import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; +import { action } from '@ember/object'; + +export default class OrgsOrgRolesRoleRoute extends Route { + // =services + + @service intl; + @service notify; + + // =methods + + /** + * Return a role with id + * @param {object} params + * @return {Promise{RoleModel}} + */ + model({ role_id: id }) { + return this.store.findRecord('role', id); + } + + // =actions + + /** + * Rollback changes on role. + * @param {RoleModel} role + */ + @action + cancel(role) { + role.rollbackAttributes(); + } + + /** + * Handle saving a role. + * @param {RoleModel} role + * @param {Event} e + */ + @action + async save(role) { + try { + await role.save(); + this.notify.success(this.intl.t('notify.save-success')); + } catch (error) { + // TODO: replace with translated strings + this.notify.error(error.message, { closeAfter: null }); + } + } + + /** + * Delete role and redirect to roles index. + * @param {RoleModel} role + */ + @action + async delete(role) { + try { + await role.destroyRecord(); + this.transitionTo('orgs.org.roles'); + this.notify.success(this.intl.t('notify.role.delete-success')); + } catch (error) { + // TODO: replace with translated strings + this.notify.error(error.message, { closeAfter: null }); + } + } +} diff --git a/ui/core/app/templates/orgs/org/roles.hbs b/ui/core/app/templates/orgs/org/roles.hbs new file mode 100644 index 000000000..6d511b7f7 --- /dev/null +++ b/ui/core/app/templates/orgs/org/roles.hbs @@ -0,0 +1,3 @@ +{{title (t "resources.roles")}} + +{{outlet}} diff --git a/ui/core/app/templates/orgs/org/roles/_sidebar.hbs b/ui/core/app/templates/orgs/org/roles/_sidebar.hbs new file mode 100644 index 000000000..584ba2ccc --- /dev/null +++ b/ui/core/app/templates/orgs/org/roles/_sidebar.hbs @@ -0,0 +1,18 @@ + + + + {{t "resources.projects"}} + + + + + + + {{t "resources.roles"}} + + + diff --git a/ui/core/app/templates/orgs/org/roles/index.hbs b/ui/core/app/templates/orgs/org/roles/index.hbs new file mode 100644 index 000000000..498aecac5 --- /dev/null +++ b/ui/core/app/templates/orgs/org/roles/index.hbs @@ -0,0 +1,63 @@ + + + + + + + +

{{t "resources.roles"}}

+
+ + + {{#if @model}} + {{t 'titles.new'}} + {{/if}} + + + + + {{#if @model}} + + + + + {{t 'form.label.name'}} + {{t 'form.label.description'}} + + + + {{#each @model as |role|}} + + + + {{role.name}} + + + {{role.description}} + + {{/each}} + + + + {{else}} + + + + + {{t 'descriptions.roles-welcome'}} + + + + {{t 'titles.new'}} + + + + + {{/if}} + + + +
diff --git a/ui/core/app/templates/orgs/org/roles/new.hbs b/ui/core/app/templates/orgs/org/roles/new.hbs new file mode 100644 index 000000000..911310f26 --- /dev/null +++ b/ui/core/app/templates/orgs/org/roles/new.hbs @@ -0,0 +1,17 @@ + + + + + + +

{{t "titles.new"}}

+
+ + + + +
diff --git a/ui/core/app/templates/orgs/org/roles/role.hbs b/ui/core/app/templates/orgs/org/roles/role.hbs new file mode 100644 index 000000000..2b05f838d --- /dev/null +++ b/ui/core/app/templates/orgs/org/roles/role.hbs @@ -0,0 +1,3 @@ +{{title @model.name}} + +{{outlet}} diff --git a/ui/core/app/templates/orgs/org/roles/role/index.hbs b/ui/core/app/templates/orgs/org/roles/role/index.hbs new file mode 100644 index 000000000..1b35c1929 --- /dev/null +++ b/ui/core/app/templates/orgs/org/roles/role/index.hbs @@ -0,0 +1,27 @@ + + + + + + + +

{{t "resources.role"}}

+
+ + + + + + {{t 'actions.delete'}} + + + +
diff --git a/ui/core/mirage/config.js b/ui/core/mirage/config.js index 247fc6f36..8a3e0f49c 100644 --- a/ui/core/mirage/config.js +++ b/ui/core/mirage/config.js @@ -40,6 +40,13 @@ export default function() { this.patch('/orgs/:org_id/auth-methods/:id'); this.del('/orgs/:org_id/auth-methods/:id'); + // role + this.get('/orgs/:org_id/roles'); + this.post('/orgs/:org_id/roles'); + this.get('/orgs/:org_id/roles/:id'); + this.patch('/orgs/:org_id/roles/:id'); + this.del('/orgs/:org_id/roles/:id'); + // Other resources // host-catalog diff --git a/ui/core/mirage/factories/role.js b/ui/core/mirage/factories/role.js new file mode 100644 index 000000000..abcb8de67 --- /dev/null +++ b/ui/core/mirage/factories/role.js @@ -0,0 +1,3 @@ +import factory from '../generated/factories/role'; + +export default factory.extend(); diff --git a/ui/core/mirage/generated/factories/role.js b/ui/core/mirage/generated/factories/role.js new file mode 100644 index 000000000..6265c7116 --- /dev/null +++ b/ui/core/mirage/generated/factories/role.js @@ -0,0 +1,16 @@ +import { Factory } from 'ember-cli-mirage'; +import { random, date } from 'faker'; + +/** + * GeneratedRoleModelFactory + * Role contains all field related to a Role resource + */ +export default Factory.extend({ + + name: () => random.words(), + description: () => random.words(), + created_time: () => date.recent(), + updated_time: () => date.recent(), + disabled: () => random.boolean(), + +}); diff --git a/ui/core/mirage/scenarios/default.js b/ui/core/mirage/scenarios/default.js index b8accbf8b..ef06c6ee4 100644 --- a/ui/core/mirage/scenarios/default.js +++ b/ui/core/mirage/scenarios/default.js @@ -12,6 +12,10 @@ export default function(server) { // same items for request at all scopes. server.createList('auth-method', 3); + // IAM + // Roles + server.createList('role', 5); + // Other resources server.createList('host-catalog', 6); diff --git a/ui/core/mirage/serializers/role.js b/ui/core/mirage/serializers/role.js new file mode 100644 index 000000000..c0db8a130 --- /dev/null +++ b/ui/core/mirage/serializers/role.js @@ -0,0 +1,5 @@ +import ApplicationSerializer from './application'; + +export default ApplicationSerializer.extend({ + modelName: 'role' +}); diff --git a/ui/core/tests/acceptance/roles-test.js b/ui/core/tests/acceptance/roles-test.js new file mode 100644 index 000000000..976ba47b7 --- /dev/null +++ b/ui/core/tests/acceptance/roles-test.js @@ -0,0 +1,191 @@ +import { module, test } from 'qunit'; +import { visit, currentURL, click, fillIn, find } from '@ember/test-helpers'; +import { setupApplicationTest } from 'ember-qunit'; +import setupMirage from 'ember-cli-mirage/test-support/setup-mirage'; +import a11yAudit from 'ember-a11y-testing/test-support/audit'; +import { Response } from 'miragejs'; + +module('Acceptance | roles', function (hooks) { + setupApplicationTest(hooks); + setupMirage(hooks); + + hooks.beforeEach(function () { + this.server.create('org'); + }); + + test('visiting roles', async function (assert) { + assert.expect(1); + await visit('/orgs/1/roles'); + await a11yAudit(); + assert.equal(currentURL(), '/orgs/1/roles'); + }); + + test('can create new role', async function (assert) { + assert.expect(4); + assert.equal(this.server.db.roles.length, 0); + await visit('/orgs/1/roles/new'); + await fillIn('[name="name"]', 'role name'); + await click('[type="submit"]'); + assert.equal(currentURL(), '/orgs/1/roles/1'); + assert.equal(this.server.db.roles.length, 1); + assert.equal(this.server.db.roles[0].name, 'role name'); + }); + + test('can cancel new role creation', async function (assert) { + assert.expect(3); + assert.equal(this.server.db.roles.length, 0); + await visit('/orgs/1/roles/new'); + await fillIn('[name="name"]', 'role name'); + await click('.rose-form-actions [type="button"]'); + assert.equal(currentURL(), '/orgs/1/roles'); + assert.equal(this.server.db.roles.length, 0); + }); + + test('saving a new role with invalid fields displays error messages', async function (assert) { + assert.expect(2); + this.server.post('/orgs/:org_id/roles', () => { + return new Response( + 400, + {}, + { + status: 400, + code: 'invalid_argument', + message: 'The request was invalid.', + details: { + fields: [ + { + name: 'name', + message: 'Name is required.', + }, + ], + }, + } + ); + }); + await visit('/orgs/1/roles/new'); + await fillIn('[name="name"]', 'role name'); + await click('[type="submit"]'); + assert.ok( + find('[role="alert"]').textContent.trim(), + 'The request was invalid.', + 'Displays primary error message.' + ); + assert.ok( + find('.rose-form-error-message').textContent.trim(), + 'Name is required.', + 'Displays field-level errors.' + ); + }); + + test('can save changes to an existing role', async function (assert) { + assert.expect(2); + this.server.createList('role', 1, {name: 'Admin role'}); + await visit('/orgs/1/roles/1'); + await fillIn('[name="name"]', 'Updated admin role'); + await click('.rose-form-actions [type="submit"]'); + assert.equal(currentURL(), '/orgs/1/roles/1'); + assert.equal(this.server.db.roles[0].name, 'Updated admin role'); + }); + + test('can cancel changes to an existing role', async function (assert) { + assert.expect(1); + this.server.createList('role', 1, {name: 'Admin role'}); + await visit('/orgs/1/roles/1'); + await fillIn('[name="name"]', 'Updated admin role'); + await click('.rose-form-actions [type="button"]'); + assert.equal(find('[name="name"]').value, 'Admin role'); + }); + + test('can delete a role', async function(assert) { + assert.expect(2); + this.server.createList('role', 1); + assert.equal(this.server.db.roles.length, 1); + await visit('/orgs/1/roles/1'); + await click('.rose-button-warning'); + assert.equal(this.server.db.roles.length, 0); + }); + + test('saving an existing role with invalid fields displays error messages', async function (assert) { + assert.expect(2); + this.server.createList('role', 1); + this.server.patch('/orgs/:org_id/roles/:id', () => { + return new Response( + 400, + {}, + { + status: 400, + code: 'invalid_argument', + message: 'The request was invalid.', + details: { + fields: [ + { + name: 'name', + message: 'Name is required.', + }, + ], + }, + } + ); + }); + await visit('/orgs/1/roles/1'); + await fillIn('[name="name"]', 'random string'); + await click('[type="submit"]'); + assert.ok( + find('[role="alert"]').textContent.trim(), + 'The request was invalid.', + 'Displays primary error message.' + ); + assert.ok( + find('.rose-form-error-message').textContent.trim(), + 'Name is required.', + 'Displays field-level errors.' + ); + }); + + test('errors are displayed when save project fails', async function (assert) { + assert.expect(1); + this.server.createList('role', 1); + this.server.patch('/orgs/:org_id/roles/:id', () => { + return new Response( + 490, + {}, + { + status: 490, + code: 'error', + message: 'Oops.', + } + ); + }); + await visit('/orgs/1/roles/1'); + await fillIn('[name="name"]', 'Role name'); + await click('[type="submit"]'); + assert.ok( + find('[role="alert"]').textContent.trim(), + 'Oops.', + 'Displays primary error message.' + ); + }); + + test('errors are displayed when delete project fails', async function (assert) { + assert.expect(1); + this.server.createList('role', 1); + this.server.del('/orgs/:org_id/roles/:id', () => { + return new Response( + 490, + {}, + { + status: 490, + code: 'error', + message: 'Oops.', + } + ); + }); + await visit('/orgs/1/roles/1'); + await click('.rose-button-warning'); + assert.ok( + find('[role="alert"]').textContent.trim(), + 'Oops.', + 'Displays primary error message.' + ); + }); +}); diff --git a/ui/core/tests/unit/controllers/orgs/org/roles-test.js b/ui/core/tests/unit/controllers/orgs/org/roles-test.js new file mode 100644 index 000000000..816459cd3 --- /dev/null +++ b/ui/core/tests/unit/controllers/orgs/org/roles-test.js @@ -0,0 +1,11 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module('Unit | Controller | orgs/org/roles', function(hooks) { + setupTest(hooks); + + test('it exists', function(assert) { + let controller = this.owner.lookup('controller:orgs/org/roles'); + assert.ok(controller); + }); +}); diff --git a/ui/core/tests/unit/controllers/orgs/org/roles/new-test.js b/ui/core/tests/unit/controllers/orgs/org/roles/new-test.js new file mode 100644 index 000000000..8d948ae06 --- /dev/null +++ b/ui/core/tests/unit/controllers/orgs/org/roles/new-test.js @@ -0,0 +1,11 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module('Unit | Controller | orgs/org/roles/new', function(hooks) { + setupTest(hooks); + + test('it exists', function(assert) { + let controller = this.owner.lookup('controller:orgs/org/roles/new'); + assert.ok(controller); + }); +}); diff --git a/ui/core/tests/unit/controllers/orgs/org/roles/role-test.js b/ui/core/tests/unit/controllers/orgs/org/roles/role-test.js new file mode 100644 index 000000000..cd56c941f --- /dev/null +++ b/ui/core/tests/unit/controllers/orgs/org/roles/role-test.js @@ -0,0 +1,11 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module('Unit | Controller | orgs/org/roles/role', function(hooks) { + setupTest(hooks); + + test('it exists', function(assert) { + let controller = this.owner.lookup('controller:orgs/org/roles/role'); + assert.ok(controller); + }); +}); diff --git a/ui/core/tests/unit/routes/orgs/org/roles-test.js b/ui/core/tests/unit/routes/orgs/org/roles-test.js new file mode 100644 index 000000000..38bc3acb2 --- /dev/null +++ b/ui/core/tests/unit/routes/orgs/org/roles-test.js @@ -0,0 +1,11 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module('Unit | Route | orgs/org/roles', function(hooks) { + setupTest(hooks); + + test('it exists', function(assert) { + let route = this.owner.lookup('route:orgs/org/roles'); + assert.ok(route); + }); +}); diff --git a/ui/core/tests/unit/routes/orgs/org/roles/index-test.js b/ui/core/tests/unit/routes/orgs/org/roles/index-test.js new file mode 100644 index 000000000..12f247adb --- /dev/null +++ b/ui/core/tests/unit/routes/orgs/org/roles/index-test.js @@ -0,0 +1,11 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module('Unit | Route | orgs/org/roles/index', function(hooks) { + setupTest(hooks); + + test('it exists', function(assert) { + let route = this.owner.lookup('route:orgs/org/roles/index'); + assert.ok(route); + }); +}); diff --git a/ui/core/tests/unit/routes/orgs/org/roles/new-test.js b/ui/core/tests/unit/routes/orgs/org/roles/new-test.js new file mode 100644 index 000000000..d613eb34a --- /dev/null +++ b/ui/core/tests/unit/routes/orgs/org/roles/new-test.js @@ -0,0 +1,11 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module('Unit | Route | orgs/org/roles/new', function(hooks) { + setupTest(hooks); + + test('it exists', function(assert) { + let route = this.owner.lookup('route:orgs/org/roles/new'); + assert.ok(route); + }); +}); diff --git a/ui/core/tests/unit/routes/orgs/org/roles/role-test.js b/ui/core/tests/unit/routes/orgs/org/roles/role-test.js new file mode 100644 index 000000000..149fd657e --- /dev/null +++ b/ui/core/tests/unit/routes/orgs/org/roles/role-test.js @@ -0,0 +1,11 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module('Unit | Route | orgs/org/roles/role', function(hooks) { + setupTest(hooks); + + test('it exists', function(assert) { + let route = this.owner.lookup('route:orgs/org/roles/role'); + assert.ok(route); + }); +}); diff --git a/ui/core/translations/en-us.yaml b/ui/core/translations/en-us.yaml index 29ba0c47f..d8151b25b 100644 --- a/ui/core/translations/en-us.yaml +++ b/ui/core/translations/en-us.yaml @@ -5,22 +5,28 @@ actions: save: Save help: Need help? resources: + role: Role + roles: Roles projects: Projects project: Project host-catalog: Host Catalog host-catalogs: Host Catalogs titles: + iam: IAM new: New + general: General overview: Overview settings: Settings empty-set: No Items Yet projects-welcome: Welcome to Projects host-catalogs-welcome: Welcome to Host Catalogs + roles-welcome: Welcome to Roles 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. projects-welcome: Projects allow you to create and manage host catalogs. To start, create a project. host-catalogs-welcome: Host catalogs allow you to configure to collect and create hosts and host sets. To start, create a host catalog. + roles-welcome: Roles are collections of capabilities that can be granted to user and/or groups. states: info: Info success: Success @@ -44,6 +50,11 @@ errors: notfound-error: We could not find the requested resource. You can ask your administrator or try again later. server-error: We ran into a problem and could not continue. You can ask your administrator or try again later. unknown-error: Please contact your administrator or try again later. +notify: + save-success: Save succeeded. + role: + create-success: Role created succesfully. + delete-success: Deleted role successfully. dev: default-org: Default Org menu-item: Menu item From c5c73f798b98cbede6674edd4a996fabf12f9697 Mon Sep 17 00:00:00 2001 From: Randall Morey Date: Wed, 24 Jun 2020 10:40:00 -0400 Subject: [PATCH 2/6] =?UTF-8?q?chore:=20=F0=9F=A4=96=20extract=20sidebar/b?= =?UTF-8?q?ody=20layout=20into=20separate=20component?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/rose/layout/global.stories.mdx | 5 +- .../addon/components/rose/layout/sidebar.js | 7 + .../rose/layout/sidebar.stories.mdx | 33 +++ .../rose/components/layout/_global.scss | 39 +-- .../styles/rose/components/layout/_index.scss | 10 + .../rose/components/layout/_sidebar.scss | 27 ++ .../components/rose/layout/global.hbs | 7 +- .../components/rose/layout/sidebar.hbs | 6 + .../app/components/rose/layout/sidebar.js | 1 + .../tests/dummy/app/templates/playground.hbs | 278 +----------------- .../components/rose/layout/global-test.js | 18 +- .../components/rose/layout/sidebar-test.js | 21 ++ ui/core/app/templates/application.hbs | 13 +- 13 files changed, 141 insertions(+), 324 deletions(-) create mode 100644 addons/rose/addon/components/rose/layout/sidebar.js create mode 100644 addons/rose/addon/components/rose/layout/sidebar.stories.mdx create mode 100644 addons/rose/addon/styles/rose/components/layout/_sidebar.scss create mode 100644 addons/rose/addon/templates/components/rose/layout/sidebar.hbs create mode 100644 addons/rose/app/components/rose/layout/sidebar.js create mode 100644 addons/rose/tests/integration/components/rose/layout/sidebar-test.js diff --git a/addons/rose/addon/components/rose/layout/global.stories.mdx b/addons/rose/addon/components/rose/layout/global.stories.mdx index a56e928b9..9b8e34ea4 100644 --- a/addons/rose/addon/components/rose/layout/global.stories.mdx +++ b/addons/rose/addon/components/rose/layout/global.stories.mdx @@ -7,12 +7,12 @@ import { text, boolean, select } from '@storybook/addon-knobs'; # Layout|Global -A top-level layout/positioning component that allows three distinct sections to be configured: header, navigation, and body. +A top-level layout/positioning component that allows two distinct sections to +be configured: header and body. ```htmlbars - ``` @@ -22,7 +22,6 @@ A top-level layout/positioning component that allows three distinct sections to template: hbs` Header - Sidebar Body `, diff --git a/addons/rose/addon/components/rose/layout/sidebar.js b/addons/rose/addon/components/rose/layout/sidebar.js new file mode 100644 index 000000000..b58f2003a --- /dev/null +++ b/addons/rose/addon/components/rose/layout/sidebar.js @@ -0,0 +1,7 @@ +import Component from '@ember/component'; +import layout from '../../../templates/components/rose/layout/sidebar'; + +export default Component.extend({ + layout, + tagName: '', +}); diff --git a/addons/rose/addon/components/rose/layout/sidebar.stories.mdx b/addons/rose/addon/components/rose/layout/sidebar.stories.mdx new file mode 100644 index 000000000..28dc3fd85 --- /dev/null +++ b/addons/rose/addon/components/rose/layout/sidebar.stories.mdx @@ -0,0 +1,33 @@ +import { Meta, Story, Preview } from '@storybook/addon-docs/blocks'; +import { hbs } from 'ember-cli-htmlbars'; +import { action } from '@storybook/addon-actions'; +import { text, boolean, select } from '@storybook/addon-knobs'; + + + +# Layout|Sidebar + +A top-level layout/positioning component that allows two distinct sections to +be configured: header and body. + +```htmlbars + + + sidebar content + + + body content + + +``` + + + {{ + template: hbs` + + Body + Sidebar + + `, + }} + diff --git a/addons/rose/addon/styles/rose/components/layout/_global.scss b/addons/rose/addon/styles/rose/components/layout/_global.scss index 894f83410..d9b9b771a 100644 --- a/addons/rose/addon/styles/rose/components/layout/_global.scss +++ b/addons/rose/addon/styles/rose/components/layout/_global.scss @@ -1,39 +1,10 @@ -@use '../../variables/sizing'; -@use '../../variables/media'; - .rose-layout-global { - display: grid; - grid-template-areas: - 'header header header header' - 'navigation body body body'; - // Define 16rem static grid column for navigation - grid-template-columns: (sizing.rems(m) * 16) auto 1fr; - grid-template-rows: auto 1fr; - height: 100vh; + min-height: 100vh; + display: flex; + flex-direction: column; .rose-layout-global-body { - grid-area: body; - padding: sizing.rems(l); - } - - .rose-layout-global-header { - grid-area: header; - } - - .rose-layout-global-sidebar { - grid-area: navigation; - border-right: sizing.rems(xxxxs) solid var(--ui-border-subtler-1); - } -} - -@media (max-width: media.width(small)) { - .rose-layout-global { - grid-template-columns: 1fr; - grid-template-rows: 0.1fr auto 1fr; - grid-template-areas: - 'header' - 'navigation' - 'body'; - padding-bottom: 0; + flex: 1; + display: flex; } } diff --git a/addons/rose/addon/styles/rose/components/layout/_index.scss b/addons/rose/addon/styles/rose/components/layout/_index.scss index 1ed6d8338..5afb56808 100644 --- a/addons/rose/addon/styles/rose/components/layout/_index.scss +++ b/addons/rose/addon/styles/rose/components/layout/_index.scss @@ -1,3 +1,13 @@ +@use '../../variables/sizing'; + @import 'global'; +@import 'sidebar'; @import 'page'; @import 'centered'; + +// TODO: this is prescriptive and can be removed if it becomes a problem, +// but used here reflects the fact that the "main" element, of which there +// can only be one per page, generally has padding in Structure layouts. +main { + padding: sizing.rems(l); +} diff --git a/addons/rose/addon/styles/rose/components/layout/_sidebar.scss b/addons/rose/addon/styles/rose/components/layout/_sidebar.scss new file mode 100644 index 000000000..e5cc54668 --- /dev/null +++ b/addons/rose/addon/styles/rose/components/layout/_sidebar.scss @@ -0,0 +1,27 @@ +@use '../../variables/sizing'; +@use '../../variables/media'; + +.rose-layout-sidebar { + --sidebar-width: #{sizing.rems(m) * 16}; + display: flex; + flex-grow: 1; + + .rose-layout-sidebar-body { + flex: 1; + } + + .rose-layout-sidebar-sidebar { + width: var(--sidebar-width); + border-right: sizing.rems(xxxxs) solid var(--ui-border-subtler-1); + } +} + +@media (max-width: media.width(small)) { + .rose-layout-sidebar { + flex-direction: column; + + .rose-layout-sidebar-sidebar { + width: auto; + } + } +} diff --git a/addons/rose/addon/templates/components/rose/layout/global.hbs b/addons/rose/addon/templates/components/rose/layout/global.hbs index 5f516ec80..bfa2c8e75 100644 --- a/addons/rose/addon/templates/components/rose/layout/global.hbs +++ b/addons/rose/addon/templates/components/rose/layout/global.hbs @@ -1,7 +1,6 @@ -
+
{{yield (hash header=(component "rose/anonymous" tagName="header" class="rose-layout-global-header") - sidebar=(component "rose/anonymous" tagName="aside" class="rose-layout-global-sidebar") - body=(component "rose/anonymous" tagName="main" class="rose-layout-global-body") + body=(component "rose/anonymous" class="rose-layout-global-body") )}} -
+ diff --git a/addons/rose/addon/templates/components/rose/layout/sidebar.hbs b/addons/rose/addon/templates/components/rose/layout/sidebar.hbs new file mode 100644 index 000000000..6bfb342e4 --- /dev/null +++ b/addons/rose/addon/templates/components/rose/layout/sidebar.hbs @@ -0,0 +1,6 @@ +
+ {{yield (hash + body=(component "rose/anonymous" class="rose-layout-sidebar-body") + sidebar=(component "rose/anonymous" tagName="aside" class="rose-layout-sidebar-sidebar") + )}} +
diff --git a/addons/rose/app/components/rose/layout/sidebar.js b/addons/rose/app/components/rose/layout/sidebar.js new file mode 100644 index 000000000..f078ea1c6 --- /dev/null +++ b/addons/rose/app/components/rose/layout/sidebar.js @@ -0,0 +1 @@ +export { default } from 'rose/components/rose/layout/sidebar'; \ No newline at end of file diff --git a/addons/rose/tests/dummy/app/templates/playground.hbs b/addons/rose/tests/dummy/app/templates/playground.hbs index 8f7d026b4..cf74a1e48 100644 --- a/addons/rose/tests/dummy/app/templates/playground.hbs +++ b/addons/rose/tests/dummy/app/templates/playground.hbs @@ -30,274 +30,24 @@ - - - Item Name - Item Name - Item Name - Item Name - - - - - Breadcrumbs - Header - Navigation - Actions - Body - - Dummy app index template + + + + + Item Name + Item Name + Item Name + Item Name + + - - Menu item - Menu item - Button menu item - + + Body content + - - An error occurred. - + -

Icon

- - - - -

Button

- Primary -
- Primary -
- - Secondary -
- Secondary -
- -

Form

- - -

Inputs

- - - - - -

Textareas

- - - - - -

Selects

- - - Choose an option - Value 1 - Value 2 - Value 3 - - - - - Choose an option - - - - -

Radio Group

- - - - - - -

Checkboxes

- - - - - - - -
- - - - - - - - - Address - Host Set - Name - Description - - - - - 1.1.1.1 - host-set-1, host-set-2 - optional name - description - - - - - Footer - - - - - - Level 1 - Level 2 - Level 3 - Level 4 - Level 5 - - - - - You must be granted permissions to view this data. - Ask your administrator if you think you should have access. - - - - Go back - - Need help? - - - - - Sorry, we couldn't find the page you were looking for. You can go back or to one of the links below. - - - - Go back - - Need help? - - - - - We ran into a problem and could not continue. You can ask your administrator or try again later. - - - - Try Again - - Need help? - - - - - Your logs will appear here when we start receiving them. If your server is currently outputting logs, check your configuration. - - - - - - Namespaces allow you to create distinct areas with their own separate policies, mounts, tokens, and identity entities. - - Add a namespace - - - - - - Namespaces allow you to create distinct areas with their own separate policies, mounts, tokens, and identity entities. - - Add a namespace - -
diff --git a/addons/rose/tests/integration/components/rose/layout/global-test.js b/addons/rose/tests/integration/components/rose/layout/global-test.js index 0b262991e..2e6b91039 100644 --- a/addons/rose/tests/integration/components/rose/layout/global-test.js +++ b/addons/rose/tests/integration/components/rose/layout/global-test.js @@ -8,7 +8,7 @@ module('Integration | Component | rose/layout/global', function (hooks) { test('it renders', async function (assert) { await render(hbs``); - assert.ok(find('div')); + assert.ok(find('section')); assert.ok(find('.rose-layout-global')); }); @@ -17,13 +17,6 @@ module('Integration | Component | rose/layout/global', function (hooks) { assert.ok(find('#global')); }); - test('it renders with content', async function (assert) { - await render(hbs` -