diff --git a/ui/core/app/components/copyable/index.hbs b/ui/core/app/components/copyable/index.hbs
index 497e5f2f7..618f3315b 100644
--- a/ui/core/app/components/copyable/index.hbs
+++ b/ui/core/app/components/copyable/index.hbs
@@ -1,20 +1,21 @@
-{{#if this.isClipboardLibrarySupported}}
-
- {{#if this.copied}}
-
- {{yield}}
-
- {{else}}
-
- {{yield}}
-
- {{/if}}
+
+ {{#if this.copied}}
+
+ {{yield}}
+
+ {{else}}
+
+ {{yield}}
+
+ {{/if}}
+
+ {{#if this.isClipboardLibrarySupported}}
{{this.actionText}}
-
-{{else}}
- {{yield}}
-{{/if}}
+ >
+ {{this.actionText}}
+
+ {{/if}}
+
diff --git a/ui/core/app/components/copyable/index.js b/ui/core/app/components/copyable/index.js
index 0801d56b5..17652e911 100644
--- a/ui/core/app/components/copyable/index.js
+++ b/ui/core/app/components/copyable/index.js
@@ -3,7 +3,6 @@ import ClipboardJS from 'clipboard';
import { action } from '@ember/object';
import { generateComponentID } from 'rose/utilities/component-auto-id';
import { task, timeout } from 'ember-concurrency';
-import { bind } from '@ember/runloop';
import { tracked } from '@glimmer/tracking';
export default class CopyableComponent extends Component {
@@ -20,6 +19,8 @@ export default class CopyableComponent extends Component {
@tracked copied = false;
+ clipboard = null;
+
/**
* A Ember Concurrency-based task that updates copy icon to a success state
* and back after 1 second. This icon flash indicates success on
@@ -30,13 +31,12 @@ export default class CopyableComponent extends Component {
* is an attribute.
* @type {Task}
*/
+ /* istanbul ignore next */
@task(function * () {
this.copied = true;
yield timeout(1000);
this.copied = false;
- }).drop() successIconTimer;
-
- // =methods
+ }).drop() confirmCopyTimer;
/**
* Checks for ClipboardJS support.
@@ -46,14 +46,6 @@ export default class CopyableComponent extends Component {
return ClipboardJS.isSupported();
}
- /**
- * When copy to clipboard is successful, begin timer to show success icon.
- */
- onCopySuccess(clipboardEvent) {
- clipboardEvent.clearSelection();
- this.successIconTimer.perform();
- }
-
// =actions
/**
@@ -61,12 +53,16 @@ export default class CopyableComponent extends Component {
*/
@action
register() {
- if (!this.clipboard) {
- this.clipboard = new ClipboardJS(`#${this.copyableButtonId}`, {
- text: () => this.args.text,
- });
- this.clipboard.on('success', bind(this, this.onCopySuccess));
- }
+ this.destroy();
+ /* istanbul ignore next */
+ this.clipboard = new ClipboardJS(`#${this.copyableButtonId}`, {
+ text: () => this.args.text,
+ });
+ /* istanbul ignore next */
+ this.clipboard.on('success', (clipboardEvent) => {
+ clipboardEvent.clearSelection();
+ this.confirmCopyTimer.perform();
+ });
}
/**
@@ -75,7 +71,8 @@ export default class CopyableComponent extends Component {
@action
destroy() {
this.clipboard?.destroy();
- this.successIconTimer.cancelAll();
+ this.clipboard = null;
+ this.confirmCopyTimer.cancelAll();
}
}
diff --git a/ui/core/app/components/form/group/add-members/index.hbs b/ui/core/app/components/form/group/add-members/index.hbs
index 3f93cdc7a..53cfd450f 100644
--- a/ui/core/app/components/form/group/add-members/index.hbs
+++ b/ui/core/app/components/form/group/add-members/index.hbs
@@ -39,10 +39,14 @@
{{#unless this.availableUsers}}
-
-
- {{t "descriptions.no-available-users"}}
-
-
+
+
+ {{t "resources.group.messages.no-members.description"}}
+
+
+
+ {{t "actions.back"}}
+
+
{{/unless}}
diff --git a/ui/core/app/components/form/host-set/hosts/index.hbs b/ui/core/app/components/form/host-set/hosts/index.hbs
deleted file mode 100644
index 6fb9e5158..000000000
--- a/ui/core/app/components/form/host-set/hosts/index.hbs
+++ /dev/null
@@ -1,50 +0,0 @@
-
-
-
-
- {{!--
- {{#if @model.errors.host_ids}}
-
- {{#each @model.errors.host_ids as |error|}}
- {{error.message}}
- {{/each}}
-
- {{/if}}
- --}}
-
-
-
-
- {{t 'form.id.label'}}
- {{t 'form.name.label'}}
- {{t 'form.address.label'}}
-
-
-
- {{#each this.options as |option|}}
-
-
-
-
- {{option.host.name}}
- {{option.host.attributes.address}}
-
- {{/each}}
-
-
-
-
diff --git a/ui/core/app/components/form/host-set/hosts/index.js b/ui/core/app/components/form/host-set/hosts/index.js
deleted file mode 100644
index 5b6484bc9..000000000
--- a/ui/core/app/components/form/host-set/hosts/index.js
+++ /dev/null
@@ -1,21 +0,0 @@
-import Component from '@glimmer/component';
-import { computed } from '@ember/object';
-
-export default class FormHostSetHostsComponent extends Component {
- // =attributes
-
- /**
- * An array of wrapped `HostModel` instances where the wrapper includes
- * a `selected` boolean field indicating if the host is represented
- * in `model.host_ids`.
- * @type {[{host: HostModel, selected: boolean}]}
- */
- @computed('args.{hosts.[],model.host_ids.[]}')
- get options() {
- const { hosts, model: hostSet } = this.args;
- return hosts.map((host) => ({
- host,
- selected: Boolean(hostSet.host_ids.findBy('value', host.id)),
- }));
- }
-}
diff --git a/ui/core/app/controllers/scopes/scope/projects/project/sessions.js b/ui/core/app/controllers/scopes/scope/projects/project/sessions.js
new file mode 100644
index 000000000..4c49105c6
--- /dev/null
+++ b/ui/core/app/controllers/scopes/scope/projects/project/sessions.js
@@ -0,0 +1,19 @@
+import Controller from '@ember/controller';
+import { inject as service } from '@ember/service';
+
+export default class ScopesScopeProjectsProjectSessionsController extends Controller {
+
+ // =services
+
+ @service intl;
+
+ // =attributes
+
+ /**
+ * @type {string}
+ */
+ get breadCrumb() {
+ return this.intl.t('resources.session.title_plural');
+ }
+
+}
diff --git a/ui/core/app/routes/scopes/scope/projects/project/host-catalogs/host-catalog/host-sets/host-set/add-hosts.js b/ui/core/app/routes/scopes/scope/projects/project/host-catalogs/host-catalog/host-sets/host-set/add-hosts.js
index 75fd25649..d892490ab 100644
--- a/ui/core/app/routes/scopes/scope/projects/project/host-catalogs/host-catalog/host-sets/host-set/add-hosts.js
+++ b/ui/core/app/routes/scopes/scope/projects/project/host-catalogs/host-catalog/host-sets/host-set/add-hosts.js
@@ -64,25 +64,6 @@ export default class ScopesScopeProjectsProjectHostCatalogsHostCatalogHostSetsHo
// =actions
- /**
- * Toggles the presence of the passed host ID in the passed host set's
- * `host_ids` field.
- * @param {HostSetModel} hostSet
- * @param {HostModel} host
- */
- @action
- toggleHost(hostSet, host) {
- const isHostSelected = Boolean(hostSet.host_ids.findBy('value', host.id));
- const fragment = hostSet.host_ids.findBy('value', host.id);
- if (isHostSelected) {
- // if host is already selected, filter it out
- hostSet.host_ids.removeObject(fragment);
- } else {
- // if host is not selected, add it
- hostSet.host_ids.addObject({ value: host.id });
- }
- }
-
/**
* Saves host IDs on the host set.
* @param {HostSetModel} hostSet
diff --git a/ui/core/app/styles/app.scss b/ui/core/app/styles/app.scss
index 90663507b..8a45265fa 100644
--- a/ui/core/app/styles/app.scss
+++ b/ui/core/app/styles/app.scss
@@ -94,6 +94,8 @@
}
.copyable {
+ white-space: nowrap;
+
.copyable-button {
margin: 0;
padding: 0 sizing.rems(xxs);
diff --git a/ui/core/app/templates/scopes/scope/auth-methods/auth-method/accounts/index.hbs b/ui/core/app/templates/scopes/scope/auth-methods/auth-method/accounts/index.hbs
index 291055676..864a55b8b 100644
--- a/ui/core/app/templates/scopes/scope/auth-methods/auth-method/accounts/index.hbs
+++ b/ui/core/app/templates/scopes/scope/auth-methods/auth-method/accounts/index.hbs
@@ -44,6 +44,10 @@
{{t "resources.account.messages.welcome.description"}}
+
+
+ {{t 'resources.account.actions.create'}}
+
diff --git a/ui/core/app/templates/scopes/scope/groups/group/members.hbs b/ui/core/app/templates/scopes/scope/groups/group/members.hbs
index a478d6b83..58a6383c6 100644
--- a/ui/core/app/templates/scopes/scope/groups/group/members.hbs
+++ b/ui/core/app/templates/scopes/scope/groups/group/members.hbs
@@ -53,6 +53,10 @@
{{t "resources.group.messages.members.description"}}
+
+
+ {{t 'resources.group.actions.add-members'}}
+
diff --git a/ui/core/app/templates/scopes/scope/projects/project/targets/target/host-sets.hbs b/ui/core/app/templates/scopes/scope/projects/project/targets/target/host-sets.hbs
index f6e32b99a..5e49da20d 100644
--- a/ui/core/app/templates/scopes/scope/projects/project/targets/target/host-sets.hbs
+++ b/ui/core/app/templates/scopes/scope/projects/project/targets/target/host-sets.hbs
@@ -58,6 +58,10 @@
{{t "resources.target.host-set.messages.welcome.description"}}
+
+
+ {{t 'resources.target.actions.add-host-sets'}}
+
diff --git a/ui/core/app/templates/scopes/scope/users/user/accounts.hbs b/ui/core/app/templates/scopes/scope/users/user/accounts.hbs
index 8e0715fa5..c8ea1415d 100644
--- a/ui/core/app/templates/scopes/scope/users/user/accounts.hbs
+++ b/ui/core/app/templates/scopes/scope/users/user/accounts.hbs
@@ -5,6 +5,7 @@
+ {{t 'form.name.label'}}
{{t 'form.username.label'}}
{{t 'form.id.label'}}
{{t "titles.actions"}}
@@ -14,8 +15,12 @@
{{#each @model.accounts as |account|}}
- {{account.username}}
+ {{account.displayName}}
+ {{account.description}}
+
+ {{account.username}}
+
{{t "resources.user.messages.accounts.description"}}
+
+
+ {{t 'resources.user.actions.add-accounts'}}
+
diff --git a/ui/core/tests/acceptance/host-sets/hosts-test.js b/ui/core/tests/acceptance/host-sets/hosts-test.js
new file mode 100644
index 000000000..2bd0d6997
--- /dev/null
+++ b/ui/core/tests/acceptance/host-sets/hosts-test.js
@@ -0,0 +1,162 @@
+import { module, test } from 'qunit';
+import { visit, currentURL, click, find, findAll } 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';
+import {
+ authenticateSession,
+ // These are left here intentionally for future reference.
+ //currentSession,
+ //invalidateSession,
+} from 'ember-simple-auth/test-support';
+
+module('Acceptance | host-sets | hosts', function (hooks) {
+ setupApplicationTest(hooks);
+ setupMirage(hooks);
+
+ const instances = {
+ scopes: {
+ global: null,
+ org: null,
+ project: null,
+ },
+ hostCatalog: null,
+ hostSet: null,
+ };
+ const urls = {
+ orgScope: null,
+ projects: null,
+ project: null,
+ hostCatalogs: null,
+ hostCatalog: null,
+ hostSets: null,
+ hostSet: null,
+ hostSetHosts: null
+ };
+ let hostSetHostsCount;
+
+ hooks.beforeEach(function () {
+ // Setup Mirage mock resources for this test
+ authenticateSession();
+ instances.scopes.global = this.server.create('scope', { id: 'global' });
+ instances.scopes.org = this.server.create('scope', {
+ type: 'org',
+ scope: { id: 'global', type: 'global' },
+ });
+ instances.scopes.project = this.server.create('scope', {
+ type: 'project',
+ scope: { id: instances.scopes.org.id, type: 'org' },
+ });
+ instances.hostCatalog = this.server.create('host-catalog', {
+ scope: instances.scopes.project,
+ }, 'withChildren');
+ instances.hostSet = this.server.schema.hostSets.all().models[0];
+ // Generate route URLs for resources
+ urls.orgScope = `/scopes/${instances.scopes.org.id}`;
+ urls.projects = `${urls.orgScope}/projects`;
+ urls.project = `${urls.projects}/${instances.hostCatalog.scope.id}`;
+ urls.hostCatalogs = `${urls.project}/host-catalogs`;
+ urls.hostCatalog = `${urls.hostCatalogs}/${instances.hostCatalog.id}`;
+ urls.hostSets = `${urls.hostCatalog}/host-sets`;
+ urls.hostSet = `${urls.hostSets}/${instances.hostSet.id}`;
+ urls.hostSetHosts = `${urls.hostSet}/hosts`;
+ urls.addHosts = `${urls.hostSet}/add-hosts`;
+ // Counts
+ hostSetHostsCount = instances.hostSet.hosts.length;
+ });
+
+ test('visiting host set hosts', async function (assert) {
+ assert.expect(2);
+ await visit(urls.hostSetHosts);
+ await a11yAudit();
+ assert.equal(currentURL(), urls.hostSetHosts);
+ assert.equal(findAll('tbody tr').length, hostSetHostsCount);
+ });
+
+ test('can remove a host', async function (assert) {
+ assert.expect(2);
+ await visit(urls.hostSetHosts);
+ assert.equal(findAll('tbody tr').length, hostSetHostsCount);
+ await click('tbody tr .rose-dropdown-button-danger');
+ assert.equal(findAll('tbody tr').length, hostSetHostsCount - 1);
+ });
+
+ test('shows error message on host remove error', async function (assert) {
+ assert.expect(2);
+ this.server.post('/host-sets/:idMethod', () => {
+ return new Response(
+ 400,
+ {},
+ {
+ status: 400,
+ code: 'invalid_argument',
+ message: 'The request was invalid.',
+ details: {},
+ }
+ );
+ });
+ await visit(urls.hostSetHosts);
+ assert.equal(findAll('tbody tr').length, hostSetHostsCount);
+ await click('tbody tr .rose-dropdown-button-danger');
+ assert.ok(find('[role="alert"]'));
+ });
+
+ test('visiting add hosts', async function(assert) {
+ assert.expect(1);
+ await visit(urls.addHosts);
+ await a11yAudit();
+ assert.equal(currentURL(), urls.addHosts);
+ });
+
+ test('select and save hosts to add', async function (assert) {
+ assert.expect(3);
+ instances.hostSet.update({ hostIds: [] });
+ await visit(urls.hostSetHosts);
+ assert.equal(findAll('tbody tr').length, 0);
+ await click('.rose-layout-page-actions a')
+ assert.equal(currentURL(), urls.addHosts);
+ // Click three times to select, unselect, then reselect (for coverage)
+ await click('tbody label');
+ await click('tbody label');
+ await click('tbody label');
+ await click('form [type="submit"]');
+ await visit(urls.hostSetHosts);
+ assert.equal(findAll('tbody tr').length, 1);
+ });
+
+ test('select and cancel hosts to add', async function (assert) {
+ assert.expect(4);
+ await visit(urls.hostSetHosts);
+ assert.equal(findAll('tbody tr').length, hostSetHostsCount);
+ await click('tbody tr .rose-dropdown-button-danger');
+ assert.equal(findAll('tbody tr').length, hostSetHostsCount - 1);
+ await click('.rose-layout-page-actions a')
+ assert.equal(currentURL(), urls.addHosts);
+ await click('tbody label');
+ await click('form [type="button"]');
+ await visit(urls.hostSetHosts);
+ assert.equal(findAll('tbody tr').length, hostSetHostsCount - 1);
+ });
+
+ test('shows error message on host add error', async function (assert) {
+ assert.expect(1);
+ this.server.post('/host-sets/:idMethod', () => {
+ return new Response(
+ 400,
+ {},
+ {
+ status: 400,
+ code: 'invalid_argument',
+ message: 'The request was invalid.',
+ details: {},
+ }
+ );
+ });
+ instances.hostSet.update({ hostIds: [] });
+ await visit(urls.addHosts);
+ await click('tbody label');
+ await click('form [type="submit"]');
+ assert.ok(find('[role="alert"]'));
+ });
+});
diff --git a/ui/core/tests/integration/components/copyable-test.js b/ui/core/tests/integration/components/copyable-test.js
index 8d6a29cf9..0123c353c 100644
--- a/ui/core/tests/integration/components/copyable-test.js
+++ b/ui/core/tests/integration/components/copyable-test.js
@@ -7,7 +7,7 @@ module('Integration | Component | copyable', function(hooks) {
setupRenderingTest(hooks);
test('it renders', async function(assert) {
- await render(hbs`text`);
+ await render(hbs`text`);
assert.ok(find('.copyable'));
assert.equal(find('.copyable-content').textContent.trim(), 'text');
assert.equal(find('.copyable-button').textContent.trim(), 'Copy me');
diff --git a/ui/core/tests/integration/components/form/host-set/hosts-test.js b/ui/core/tests/integration/components/form/host-set/hosts-test.js
deleted file mode 100644
index e5d2bfe87..000000000
--- a/ui/core/tests/integration/components/form/host-set/hosts-test.js
+++ /dev/null
@@ -1,25 +0,0 @@
-import { module, test } from 'qunit';
-import { setupRenderingTest } from 'ember-qunit';
-import { render, find } from '@ember/test-helpers';
-import { hbs } from 'ember-cli-htmlbars';
-
-module('Integration | Component | form/host-set/hosts', function (hooks) {
- setupRenderingTest(hooks);
-
- test('it renders', async function (assert) {
- this.submit = () => {};
- this.cancel = () => {};
- this.model = { host_ids: [] };
- this.hosts = [];
-
- await render(hbs`
- `);
-
- assert.ok(find('form'));
- });
-});
diff --git a/ui/core/tests/unit/controllers/scopes/scope/projects/project/sessions-test.js b/ui/core/tests/unit/controllers/scopes/scope/projects/project/sessions-test.js
new file mode 100644
index 000000000..8361f91f2
--- /dev/null
+++ b/ui/core/tests/unit/controllers/scopes/scope/projects/project/sessions-test.js
@@ -0,0 +1,12 @@
+import { module, test } from 'qunit';
+import { setupTest } from 'ember-qunit';
+
+module('Unit | Controller | scopes/scope/projects/project/sessions', function(hooks) {
+ setupTest(hooks);
+
+ // Replace this with your real tests.
+ test('it exists', function(assert) {
+ let controller = this.owner.lookup('controller:scopes/scope/projects/project/sessions');
+ assert.ok(controller);
+ });
+});
diff --git a/ui/core/translations/resources/en-us.yaml b/ui/core/translations/resources/en-us.yaml
index 81ade9e0f..fccc4b485 100644
--- a/ui/core/translations/resources/en-us.yaml
+++ b/ui/core/translations/resources/en-us.yaml
@@ -42,6 +42,9 @@ group:
add-members:
title: Add Members
description: Select users to add users to this group.
+ no-members:
+ title: No Members Available
+ description: There are no members available to add to this group.
actions:
add-members: Add Members
host-catalog: