diff --git a/addons/api/addon/adapters/group.js b/addons/api/addon/adapters/group.js new file mode 100644 index 000000000..b077634f4 --- /dev/null +++ b/addons/api/addon/adapters/group.js @@ -0,0 +1,14 @@ +import ApplicationAdapter from './application'; +import ScopeAdapterMixin from '../mixins/scope-adapter'; + +export default class GroupAdapter extends ApplicationAdapter.extend( + ScopeAdapterMixin +) { + // =attributes + + /** + * Group is scoped to org only, for now. + * @type {boolean} + */ + includeProject = false; +} diff --git a/addons/api/app/adapters/group.js b/addons/api/app/adapters/group.js new file mode 100644 index 000000000..bb77ff26c --- /dev/null +++ b/addons/api/app/adapters/group.js @@ -0,0 +1 @@ +export { default } from 'api/adapters/group'; diff --git a/addons/api/tests/unit/adapters/group-test.js b/addons/api/tests/unit/adapters/group-test.js new file mode 100644 index 000000000..2c1c53d3c --- /dev/null +++ b/addons/api/tests/unit/adapters/group-test.js @@ -0,0 +1,21 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; +import config from 'ember-get-config'; + +module('Unit | Adapter | group', function(hooks) { + setupTest(hooks); + + test('it exists', function(assert) { + let adapter = this.owner.lookup('adapter:group'); + assert.ok(adapter); + }); + + 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:group'); + assert.equal(adapter.urlPrefix(), `${config.api.namespace}/orgs/1`); + }); +}); diff --git a/ui/core/app/components/form/group/index.hbs b/ui/core/app/components/form/group/index.hbs new file mode 100644 index 000000000..e8d100b60 --- /dev/null +++ b/ui/core/app/components/form/group/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/components/form/host-catalog/index.hbs b/ui/core/app/components/form/host-catalog/index.hbs index 9db695baa..e8d100b60 100644 --- a/ui/core/app/components/form/host-catalog/index.hbs +++ b/ui/core/app/components/form/host-catalog/index.hbs @@ -2,7 +2,7 @@ @onSubmit={{@submit}} @cancel={{@cancel}} @disabled={{@model.isSaving}} - @error={{true}} + @error={{@model.errors.base}} as |form| > diff --git a/ui/core/app/components/form/project/index.hbs b/ui/core/app/components/form/project/index.hbs index 9db695baa..e8d100b60 100644 --- a/ui/core/app/components/form/project/index.hbs +++ b/ui/core/app/components/form/project/index.hbs @@ -2,7 +2,7 @@ @onSubmit={{@submit}} @cancel={{@cancel}} @disabled={{@model.isSaving}} - @error={{true}} + @error={{@model.errors.base}} as |form| > diff --git a/ui/core/app/components/form/role/index.hbs b/ui/core/app/components/form/role/index.hbs index 9db695baa..e8d100b60 100644 --- a/ui/core/app/components/form/role/index.hbs +++ b/ui/core/app/components/form/role/index.hbs @@ -2,7 +2,7 @@ @onSubmit={{@submit}} @cancel={{@cancel}} @disabled={{@model.isSaving}} - @error={{true}} + @error={{@model.errors.base}} as |form| > diff --git a/ui/core/app/controllers/orgs/org/groups.js b/ui/core/app/controllers/orgs/org/groups.js new file mode 100644 index 000000000..b6c1716e9 --- /dev/null +++ b/ui/core/app/controllers/orgs/org/groups.js @@ -0,0 +1,17 @@ +import Controller from '@ember/controller'; +import { inject as service } from '@ember/service'; + +export default class OrgsOrgGroupsController extends Controller { + // =services + + @service intl; + + // =attributes + + /** + * @type {string} + */ + get breadCrumb() { + return this.intl.t('resources.groups'); + } +} diff --git a/ui/core/app/controllers/orgs/org/groups/group.js b/ui/core/app/controllers/orgs/org/groups/group.js new file mode 100644 index 000000000..972fb39ed --- /dev/null +++ b/ui/core/app/controllers/orgs/org/groups/group.js @@ -0,0 +1,12 @@ +import Controller from '@ember/controller'; +import { alias } from '@ember/object/computed'; + +export default class OrgsOrgGroupsGroupController extends Controller { + // =attributes + + /** + * @type {string} + */ + @alias('model.displayName') breadCrumb; + +} diff --git a/ui/core/app/controllers/orgs/org/groups/new.js b/ui/core/app/controllers/orgs/org/groups/new.js new file mode 100644 index 000000000..9ca97fbe5 --- /dev/null +++ b/ui/core/app/controllers/orgs/org/groups/new.js @@ -0,0 +1,18 @@ +import Controller from '@ember/controller'; +import { inject as service } from '@ember/service'; + +export default class OrgsOrgGroupsNewController extends Controller { + // =services + + @service intl; + + // =attributes + + /** + * @type {string} + */ + get breadCrumb() { + return this.intl.t('titles.new'); + } + +} diff --git a/ui/core/app/router.js b/ui/core/app/router.js index 6be92febe..61566937f 100644 --- a/ui/core/app/router.js +++ b/ui/core/app/router.js @@ -28,6 +28,11 @@ Router.map(function () { this.route('role', { path: ':role_id' }, function() {}); this.route('new'); }); + + this.route('groups', function() { + this.route('group', { path: ':group_id' }, function() {}); + this.route('new'); + }); }); }); }); diff --git a/ui/core/app/routes/orgs/org/groups.js b/ui/core/app/routes/orgs/org/groups.js new file mode 100644 index 000000000..80bb0af1a --- /dev/null +++ b/ui/core/app/routes/orgs/org/groups.js @@ -0,0 +1,13 @@ +import Route from '@ember/routing/route'; + +export default class OrgsOrgGroupsRoute extends Route { + // =methods + + /** + * Returns all groups from store. + * @return {Promise[GroupModel]} + */ + model() { + return this.store.findAll('group'); + } +} diff --git a/ui/core/app/routes/orgs/org/groups/group.js b/ui/core/app/routes/orgs/org/groups/group.js new file mode 100644 index 000000000..98c6b65c9 --- /dev/null +++ b/ui/core/app/routes/orgs/org/groups/group.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 OrgsOrgGroupsGroupRoute extends Route { + // =services + + @service intl; + @service notify; + + // =methods + + /** + * Return a group with id + * @param {object} params + * @return {Promise{GroupModel}} + */ + model({ group_id: id }) { + return this.store.findRecord('group', id); + } + + // =actions + + /** + * Rollback changes on group. + * @param {GroupModel} group + */ + @action + cancel(group) { + group.rollbackAttributes(); + } + + /** + * Handle saving a group. + * @param {GroupModel} group + * @param {Event} e + */ + @action + async save(group) { + try { + await group.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 group and redirect to groups index. + * @param {GroupModel} group + */ + @action + async delete(group) { + try { + await group.destroyRecord(); + this.transitionTo('orgs.org.groups'); + this.notify.success(this.intl.t('notify.group.delete-success')); + } catch (error) { + // TODO: replace with translated strings + this.notify.error(error.message, { closeAfter: null }); + } + } +} diff --git a/ui/core/app/routes/orgs/org/groups/group/index.js b/ui/core/app/routes/orgs/org/groups/group/index.js new file mode 100644 index 000000000..521f73a9c --- /dev/null +++ b/ui/core/app/routes/orgs/org/groups/group/index.js @@ -0,0 +1,4 @@ +import Route from '@ember/routing/route'; + +export default class OrgsOrgGroupsGroupIndexRoute extends Route { +} diff --git a/ui/core/app/routes/orgs/org/groups/index.js b/ui/core/app/routes/orgs/org/groups/index.js new file mode 100644 index 000000000..9c566dfa0 --- /dev/null +++ b/ui/core/app/routes/orgs/org/groups/index.js @@ -0,0 +1,3 @@ +import Route from '@ember/routing/route'; + +export default class OrgsOrgGroupsIndexRoute extends Route {} diff --git a/ui/core/app/routes/orgs/org/groups/new.js b/ui/core/app/routes/orgs/org/groups/new.js new file mode 100644 index 000000000..6f353d368 --- /dev/null +++ b/ui/core/app/routes/orgs/org/groups/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 OrgsOrgGroupsNewRoute extends Route { + // =services + + @service intl; + @service notify; + + // =methods + + /** + * Returns a new unsaved group. + * @return {Promise{GroupModel}} + */ + model() { + return this.store.createRecord('group'); + } + + // =actions + + /** + * Rollback changes on group by destroying unsaved instance + * and redirect to group index. + * @param {GroupModel} group + */ + @action + cancel(group) { + group.rollbackAttributes(); + this.transitionTo('orgs.org.groups'); + } + + /** + * Handle saving a group. + * @param {GroupModel} group + * @param {Event} e + */ + @action + async save(group) { + try { + await group.save(); + this.transitionTo('orgs.org.groups.group', group); + this.notify.success(this.intl.t('notify.group.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/index.js b/ui/core/app/routes/orgs/org/roles/index.js index 89b868927..92a715a56 100644 --- a/ui/core/app/routes/orgs/org/roles/index.js +++ b/ui/core/app/routes/orgs/org/roles/index.js @@ -1,3 +1,3 @@ import Route from '@ember/routing/route'; -export default class OrgsOrgRolesIndexRoute extends Route { } +export default class OrgsOrgRolesIndexRoute extends Route {} diff --git a/ui/core/app/templates/orgs/org.hbs b/ui/core/app/templates/orgs/org.hbs index 17926a1aa..91937621c 100644 --- a/ui/core/app/templates/orgs/org.hbs +++ b/ui/core/app/templates/orgs/org.hbs @@ -11,6 +11,10 @@ {{outlet "sidebar"}} + + {{t "resources.groups"}} + + {{t "resources.roles"}} diff --git a/ui/core/app/templates/orgs/org/groups.hbs b/ui/core/app/templates/orgs/org/groups.hbs new file mode 100644 index 000000000..c53da7f4b --- /dev/null +++ b/ui/core/app/templates/orgs/org/groups.hbs @@ -0,0 +1,3 @@ +{{title (t "resources.groups")}} + +{{outlet}} diff --git a/ui/core/app/templates/orgs/org/groups/group.hbs b/ui/core/app/templates/orgs/org/groups/group.hbs new file mode 100644 index 000000000..2b05f838d --- /dev/null +++ b/ui/core/app/templates/orgs/org/groups/group.hbs @@ -0,0 +1,3 @@ +{{title @model.name}} + +{{outlet}} diff --git a/ui/core/app/templates/orgs/org/groups/group/index.hbs b/ui/core/app/templates/orgs/org/groups/group/index.hbs new file mode 100644 index 000000000..576796dae --- /dev/null +++ b/ui/core/app/templates/orgs/org/groups/group/index.hbs @@ -0,0 +1,27 @@ + + + + + + + +

{{t "resources.group"}}

+
+ + + + + + {{t 'actions.delete'}} + + + +
diff --git a/ui/core/app/templates/orgs/org/groups/index.hbs b/ui/core/app/templates/orgs/org/groups/index.hbs new file mode 100644 index 000000000..cf02056b2 --- /dev/null +++ b/ui/core/app/templates/orgs/org/groups/index.hbs @@ -0,0 +1,63 @@ + + + + + + + +

{{t "resources.groups"}}

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

{{t "titles.new"}}

+
+ + + + +
diff --git a/ui/core/mirage/config.js b/ui/core/mirage/config.js index ca2462ef9..781a607d5 100644 --- a/ui/core/mirage/config.js +++ b/ui/core/mirage/config.js @@ -54,6 +54,13 @@ export default function() { this.patch('/orgs/:org_id/roles/:id'); this.del('/orgs/:org_id/roles/:id'); + // group + this.get('/orgs/:org_id/groups'); + this.post('/orgs/:org_id/groups'); + this.get('/orgs/:org_id/groups/:id'); + this.patch('/orgs/:org_id/groups/:id'); + this.del('/orgs/:org_id/groups/:id'); + // Other resources // host-catalog diff --git a/ui/core/mirage/factories/group.js b/ui/core/mirage/factories/group.js new file mode 100644 index 000000000..e5f474099 --- /dev/null +++ b/ui/core/mirage/factories/group.js @@ -0,0 +1,3 @@ +import factory from '../generated/factories/group'; + +export default factory.extend(); diff --git a/ui/core/mirage/generated/factories/group.js b/ui/core/mirage/generated/factories/group.js new file mode 100644 index 000000000..7c78ee7ec --- /dev/null +++ b/ui/core/mirage/generated/factories/group.js @@ -0,0 +1,16 @@ +import { Factory } from 'ember-cli-mirage'; +import { random, date } from 'faker'; + +/** + * GeneratedGroupModelFactory + * Group contains all fields related to a Group 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 0f542378f..d4e4dc2b7 100644 --- a/ui/core/mirage/scenarios/default.js +++ b/ui/core/mirage/scenarios/default.js @@ -17,6 +17,8 @@ export default function(server) { // Role server.createList('role', 5); + // Groups + server.createList('group', 5); // Other resources diff --git a/ui/core/mirage/serializers/group.js b/ui/core/mirage/serializers/group.js new file mode 100644 index 000000000..13ec42167 --- /dev/null +++ b/ui/core/mirage/serializers/group.js @@ -0,0 +1,5 @@ +import ApplicationSerializer from './application'; + +export default ApplicationSerializer.extend({ + modelName: 'group' +}); diff --git a/ui/core/tests/acceptance/bread-crumbs-test.js b/ui/core/tests/acceptance/bread-crumbs-test.js index 4d2b65f9b..7c33595e9 100644 --- a/ui/core/tests/acceptance/bread-crumbs-test.js +++ b/ui/core/tests/acceptance/bread-crumbs-test.js @@ -9,17 +9,15 @@ module('Acceptance | breadcrumbs', function (hooks) { let org; let project; - let user; let hostCatalog; - let projectsURL; - let newProjectURL; - let projectURL; - let hostCatalogsURL; - let newHostCatalogURL; - let hostCatalogURL; - let usersURL; - let newUserURL; - let userURL; + let user; + let role; + let group; + let projectsURL, projectURL, newProjectURL; + let hostCatalogsURL, hostCatalogURL, newHostCatalogURL; + let usersURL, userURL, newUserURL; + let rolesURL, roleURL, newRoleURL; + let groupsURL, groupURL, newGroupURL; let crumbPath; @@ -29,14 +27,25 @@ module('Acceptance | breadcrumbs', function (hooks) { user = this.server.create('user'); hostCatalog = this.server.create('host-catalog'); + role = this.server.create('role'); + group = this.server.create('group'); + projectsURL = `/orgs/${org.id}/projects`; newProjectURL = `${projectsURL}/new`; projectURL = `${projectsURL}/${project.id}`; + hostCatalogsURL = `${projectURL}/host-catalogs`; newHostCatalogURL = `${hostCatalogsURL}/new`; hostCatalogURL = `${hostCatalogsURL}/${hostCatalog.id}`; - // Users + rolesURL = `/orgs/${org.id}/roles`; + newRoleURL = `${rolesURL}/new`; + roleURL = `${rolesURL}/${role.id}`; + + groupsURL = `/orgs/${org.id}/groups`; + newGroupURL = `${groupsURL}/new`; + groupURL = `${groupsURL}/${group.id}`; + usersURL = `/orgs/${org.id}/users`; newUserURL = `${usersURL}/${user.id}`; userURL = `${usersURL}/new`; @@ -45,7 +54,7 @@ module('Acceptance | breadcrumbs', function (hooks) { test('can navigate via breadcrumbs to projects from project', async function (assert) { assert.expect(3); await visit(projectURL); - assert.equal(currentURL(), projectURL, 'We begin on th expected page.'); + assert.equal(currentURL(), projectURL, 'We begin on the expected page.'); crumbPath = new URL(find('.rose-nav-breadcrumbs-link:last-child').href).pathname; assert.equal(crumbPath, projectURL, 'Last crumb has expected URL.'); await click(find('.rose-nav-breadcrumbs-link:first-child')); @@ -55,7 +64,7 @@ module('Acceptance | breadcrumbs', function (hooks) { test('can navigate via breadcrumbs to projects from new project', async function (assert) { assert.expect(3); await visit(newProjectURL); - assert.equal(currentURL(), newProjectURL, 'We begin on th expected page.'); + assert.equal(currentURL(), newProjectURL, 'We begin on the expected page.'); crumbPath = new URL(find('.rose-nav-breadcrumbs-link:last-child').href).pathname; assert.equal(crumbPath, newProjectURL, 'Last crumb has expected URL.'); await click(find('.rose-nav-breadcrumbs-link:first-child')); @@ -65,7 +74,7 @@ module('Acceptance | breadcrumbs', function (hooks) { test('can navigate via breadcrumbs to project from host catalogs', async function (assert) { assert.expect(3); await visit(hostCatalogsURL); - assert.equal(currentURL(), hostCatalogsURL, 'We begin on th expected page.'); + assert.equal(currentURL(), hostCatalogsURL, 'We begin on the expected page.'); crumbPath = new URL(find('.rose-nav-breadcrumbs-link:last-child').href).pathname; assert.equal(crumbPath, hostCatalogsURL, 'Last crumb has expected URL.'); await click(findAll('.rose-nav-breadcrumbs-link')[1]); @@ -75,7 +84,7 @@ module('Acceptance | breadcrumbs', function (hooks) { test('can navigate via breadcrumbs to host catalogs from host catalog', async function (assert) { assert.expect(3); await visit(hostCatalogURL); - assert.equal(currentURL(), hostCatalogURL, 'We begin on th expected page.'); + assert.equal(currentURL(), hostCatalogURL, 'We begin on the expected page.'); crumbPath = new URL(find('.rose-nav-breadcrumbs-link:last-child').href).pathname; assert.equal(crumbPath, hostCatalogURL, 'Last crumb has expected URL.'); await click(findAll('.rose-nav-breadcrumbs-link')[2]); @@ -85,13 +94,53 @@ module('Acceptance | breadcrumbs', function (hooks) { test('can navigate via breadcrumbs to host catalogs from new host catalog', async function (assert) { assert.expect(3); await visit(newHostCatalogURL); - assert.equal(currentURL(), newHostCatalogURL, 'We begin on th expected page.'); + assert.equal(currentURL(), newHostCatalogURL, 'We begin on the expected page.'); crumbPath = new URL(find('.rose-nav-breadcrumbs-link:last-child').href).pathname; assert.equal(crumbPath, newHostCatalogURL, 'Last crumb has expected URL.'); await click(findAll('.rose-nav-breadcrumbs-link')[2]); assert.equal(currentURL(), hostCatalogsURL, 'After navigating via breadcrumbs, we are one level up.'); }); + test('can navigate via breadcrumbs to roles from role', async function (assert) { + assert.expect(3); + await visit(roleURL); + assert.equal(currentURL(), roleURL, 'We begin on the expected page.'); + crumbPath = new URL(find('.rose-nav-breadcrumbs-link:last-child').href).pathname; + assert.equal(crumbPath, roleURL, 'Last crumb has expected URL.'); + await click(find('.rose-nav-breadcrumbs-link:first-child')); + assert.equal(currentURL(), rolesURL, 'After navigating via breadcrumbs, we are one level up.'); + }); + + test('can navigate via breadcrumbs to roles from new role', async function (assert) { + assert.expect(3); + await visit(newRoleURL); + assert.equal(currentURL(), newRoleURL, 'We begin on the expected page.'); + crumbPath = new URL(find('.rose-nav-breadcrumbs-link:last-child').href).pathname; + assert.equal(crumbPath, newRoleURL, 'Last crumb has expected URL.'); + await click(find('.rose-nav-breadcrumbs-link:first-child')); + assert.equal(currentURL(), rolesURL, 'After navigating via breadcrumbs, we are one level up.'); + }); + + test('can navigate via breadcrumbs to groups from group', async function (assert) { + assert.expect(3); + await visit(groupURL); + assert.equal(currentURL(), groupURL, 'We begin on the expected page.'); + crumbPath = new URL(find('.rose-nav-breadcrumbs-link:last-child').href).pathname; + assert.equal(crumbPath, groupURL, 'Last crumb has expected URL.'); + await click(find('.rose-nav-breadcrumbs-link:first-child')); + assert.equal(currentURL(), groupsURL, 'After navigating via breadcrumbs, we are one level up.'); + }); + + test('can navigate via breadcrumbs to groups from new group', async function (assert) { + assert.expect(3); + await visit(newGroupURL); + assert.equal(currentURL(), newGroupURL, 'We begin on the expected page.'); + crumbPath = new URL(find('.rose-nav-breadcrumbs-link:last-child').href).pathname; + assert.equal(crumbPath, newGroupURL, 'Last crumb has expected URL.'); + await click(find('.rose-nav-breadcrumbs-link:first-child')); + assert.equal(currentURL(), groupsURL, 'After navigating via breadcrumbs, we are one level up.'); + }); + test('can navigate via breadcrumbs to users from user', async function (assert) { assert.expect(3); await visit(userURL); diff --git a/ui/core/tests/acceptance/groups-test.js b/ui/core/tests/acceptance/groups-test.js new file mode 100644 index 000000000..7f3f2ec69 --- /dev/null +++ b/ui/core/tests/acceptance/groups-test.js @@ -0,0 +1,192 @@ +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 | groups', function(hooks) { + setupApplicationTest(hooks); + setupMirage(hooks); + + hooks.beforeEach(function () { + this.server.create('org'); + }); + + test('visiting groups', async function (assert) { + assert.expect(1); + await visit('/orgs/1/groups'); + await a11yAudit(); + assert.equal(currentURL(), '/orgs/1/groups'); + }); + + test('can create new group', async function (assert) { + assert.expect(4); + assert.equal(this.server.db.groups.length, 0); + await visit('/orgs/1/groups/new'); + await fillIn('[name="name"]', 'group name'); + await click('[type="submit"]'); + assert.equal(currentURL(), '/orgs/1/groups/1'); + assert.equal(this.server.db.groups.length, 1); + assert.equal(this.server.db.groups[0].name, 'group name'); + }); + + test('can cancel new group creation', async function (assert) { + assert.expect(3); + assert.equal(this.server.db.groups.length, 0); + await visit('/orgs/1/groups/new'); + await fillIn('[name="name"]', 'group name'); + await click('.rose-form-actions [type="button"]'); + assert.equal(currentURL(), '/orgs/1/groups'); + assert.equal(this.server.db.groups.length, 0); + }); + + test('saving a new group with invalid fields displays error messages', async function (assert) { + assert.expect(2); + this.server.post('/orgs/:org_id/groups', () => { + 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/groups/new'); + await fillIn('[name="name"]', 'group 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 group', async function (assert) { + assert.expect(2); + this.server.createList('group', 1, {name: 'Admin group'}); + await visit('/orgs/1/groups/1'); + await fillIn('[name="name"]', 'Updated admin group'); + await click('.rose-form-actions [type="submit"]'); + assert.equal(currentURL(), '/orgs/1/groups/1'); + assert.equal(this.server.db.groups[0].name, 'Updated admin group'); + }); + + test('can cancel changes to an existing group', async function (assert) { + assert.expect(1); + this.server.createList('group', 1, {name: 'Admin group'}); + await visit('/orgs/1/groups/1'); + await fillIn('[name="name"]', 'Updated admin group'); + await click('.rose-form-actions [type="button"]'); + assert.equal(find('[name="name"]').value, 'Admin group'); + }); + + test('can delete a group', async function(assert) { + assert.expect(2); + this.server.createList('group', 1); + assert.equal(this.server.db.groups.length, 1); + await visit('/orgs/1/groups/1'); + await click('.rose-button-warning'); + assert.equal(this.server.db.groups.length, 0); + }); + + test('saving an existing group with invalid fields displays error messages', async function (assert) { + assert.expect(2); + this.server.createList('group', 1); + this.server.patch('/orgs/:org_id/groups/: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/groups/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('group', 1); + this.server.patch('/orgs/:org_id/groups/:id', () => { + return new Response( + 490, + {}, + { + status: 490, + code: 'error', + message: 'Oops.', + } + ); + }); + await visit('/orgs/1/groups/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('group', 1); + this.server.del('/orgs/:org_id/groups/:id', () => { + return new Response( + 490, + {}, + { + status: 490, + code: 'error', + message: 'Oops.', + } + ); + }); + await visit('/orgs/1/groups/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/groups-test.js b/ui/core/tests/unit/controllers/orgs/org/groups-test.js new file mode 100644 index 000000000..0ca58eed0 --- /dev/null +++ b/ui/core/tests/unit/controllers/orgs/org/groups-test.js @@ -0,0 +1,12 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module('Unit | Controller | orgs/org/groups', function(hooks) { + setupTest(hooks); + + // Replace this with your real tests. + test('it exists', function(assert) { + let controller = this.owner.lookup('controller:orgs/org/groups'); + assert.ok(controller); + }); +}); diff --git a/ui/core/tests/unit/controllers/orgs/org/groups/group-test.js b/ui/core/tests/unit/controllers/orgs/org/groups/group-test.js new file mode 100644 index 000000000..49cf5b3a6 --- /dev/null +++ b/ui/core/tests/unit/controllers/orgs/org/groups/group-test.js @@ -0,0 +1,12 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module('Unit | Controller | orgs/org/groups/group', function(hooks) { + setupTest(hooks); + + // Replace this with your real tests. + test('it exists', function(assert) { + let controller = this.owner.lookup('controller:orgs/org/groups/group'); + assert.ok(controller); + }); +}); diff --git a/ui/core/tests/unit/controllers/orgs/org/groups/new-test.js b/ui/core/tests/unit/controllers/orgs/org/groups/new-test.js new file mode 100644 index 000000000..ef02542c9 --- /dev/null +++ b/ui/core/tests/unit/controllers/orgs/org/groups/new-test.js @@ -0,0 +1,12 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module('Unit | Controller | orgs/org/groups/new', function(hooks) { + setupTest(hooks); + + // Replace this with your real tests. + test('it exists', function(assert) { + let controller = this.owner.lookup('controller:orgs/org/groups/new'); + assert.ok(controller); + }); +}); diff --git a/ui/core/tests/unit/routes/orgs/org/groups-test.js b/ui/core/tests/unit/routes/orgs/org/groups-test.js new file mode 100644 index 000000000..a56f69e76 --- /dev/null +++ b/ui/core/tests/unit/routes/orgs/org/groups-test.js @@ -0,0 +1,11 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module('Unit | Route | orgs/org/groups', function(hooks) { + setupTest(hooks); + + test('it exists', function(assert) { + let route = this.owner.lookup('route:orgs/org/groups'); + assert.ok(route); + }); +}); diff --git a/ui/core/tests/unit/routes/orgs/org/groups/group-test.js b/ui/core/tests/unit/routes/orgs/org/groups/group-test.js new file mode 100644 index 000000000..cec8245a8 --- /dev/null +++ b/ui/core/tests/unit/routes/orgs/org/groups/group-test.js @@ -0,0 +1,11 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module('Unit | Route | orgs/org/groups/group', function(hooks) { + setupTest(hooks); + + test('it exists', function(assert) { + let route = this.owner.lookup('route:orgs/org/groups/group'); + assert.ok(route); + }); +}); diff --git a/ui/core/tests/unit/routes/orgs/org/groups/group/index-test.js b/ui/core/tests/unit/routes/orgs/org/groups/group/index-test.js new file mode 100644 index 000000000..2496b24bf --- /dev/null +++ b/ui/core/tests/unit/routes/orgs/org/groups/group/index-test.js @@ -0,0 +1,11 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module('Unit | Route | orgs/org/groups/group/index', function(hooks) { + setupTest(hooks); + + test('it exists', function(assert) { + let route = this.owner.lookup('route:orgs/org/groups/group/index'); + assert.ok(route); + }); +}); diff --git a/ui/core/tests/unit/routes/orgs/org/groups/index-test.js b/ui/core/tests/unit/routes/orgs/org/groups/index-test.js new file mode 100644 index 000000000..c2f9135eb --- /dev/null +++ b/ui/core/tests/unit/routes/orgs/org/groups/index-test.js @@ -0,0 +1,11 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module('Unit | Route | orgs/org/groups/index', function(hooks) { + setupTest(hooks); + + test('it exists', function(assert) { + let route = this.owner.lookup('route:orgs/org/groups/index'); + assert.ok(route); + }); +}); diff --git a/ui/core/tests/unit/routes/orgs/org/groups/new-test.js b/ui/core/tests/unit/routes/orgs/org/groups/new-test.js new file mode 100644 index 000000000..09f10c831 --- /dev/null +++ b/ui/core/tests/unit/routes/orgs/org/groups/new-test.js @@ -0,0 +1,11 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module('Unit | Route | orgs/org/groups/new', function(hooks) { + setupTest(hooks); + + test('it exists', function(assert) { + let route = this.owner.lookup('route:orgs/org/groups/new'); + assert.ok(route); + }); +}); diff --git a/ui/core/translations/en-us.yaml b/ui/core/translations/en-us.yaml index 636a127fb..bda14642a 100644 --- a/ui/core/translations/en-us.yaml +++ b/ui/core/translations/en-us.yaml @@ -9,6 +9,8 @@ resources: users: Users role: Role roles: Roles + group: Group + groups: Groups projects: Projects project: Project host-catalog: Host Catalog @@ -24,6 +26,7 @@ titles: host-catalogs-welcome: Welcome to Host Catalogs users-welcome: Welcome to Users roles-welcome: Welcome to Roles + groups-welcome: Welcome to Groups 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. @@ -31,6 +34,7 @@ descriptions: host-catalogs-welcome: Host catalogs allow you to configure to collect and create hosts and host sets. To start, create a host catalog. users-welcome: Users are authorized to access Watchtower. roles-welcome: Roles are collections of capabilities that can be granted to user and/or groups. + groups-welcome: Groups are collections of users. states: info: Info success: Success @@ -62,6 +66,9 @@ notify: role: create-success: Role created succesfully. delete-success: Deleted role successfully. + group: + create-success: Group created succesfully. + delete-success: Deleted group successfully. dev: default-org: Default Org menu-item: Menu item