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/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/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/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')); - }); -});