diff --git a/ui/desktop/app/routes/scopes/scope/projects/targets.js b/ui/desktop/app/routes/scopes/scope/projects/targets.js index 2bed7e272..e33c5b833 100644 --- a/ui/desktop/app/routes/scopes/scope/projects/targets.js +++ b/ui/desktop/app/routes/scopes/scope/projects/targets.js @@ -1,14 +1,13 @@ import Route from '@ember/routing/route'; import { inject as service } from '@ember/service'; import { action } from '@ember/object'; -import { task, timeout } from 'ember-concurrency'; -import config from '../../../../config/environment'; import { resourceFilter } from 'core/decorators/resource-filter'; +import runEvery from 'ember-pollster/decorators/route/run-every'; +import config from '../../../../config/environment'; const POLL_TIMEOUT_SECONDS = config.sessionPollingTimeoutSeconds; export default class ScopesScopeProjectsTargetsRoute extends Route { - // =services @service ipc; @@ -23,26 +22,9 @@ export default class ScopesScopeProjectsTargetsRoute extends Route { @resourceFilter({ allowed: (route) => route.modelFor('scopes.scope.projects'), serialize: ({ id }) => id, - findBySerialized: ({ id }, value) => id === value - }) project; - - /** - * A simple Ember Concurrency-based polling task that refreshes the route - * every POLL_TIMEOUT_SECONDS seconds. This is necessary to display changes - * to session `status` that may occur. - * - * NOTE: tasks are sort of attributes and sort of methods, but they are not - * language-level constructs. Thus we annotate this task as if it - * is an attribute. - * @type {Task} - */ - @task(function * () { - while(true) { - yield timeout(POLL_TIMEOUT_SECONDS * 1000); - yield this.refresh(); - } - /* eslint-disable-next-line prettier/prettier */ - }).drop() poller; + findBySerialized: ({ id }, value) => id === value, + }) + project; // =methods @@ -61,33 +43,30 @@ export default class ScopesScopeProjectsTargetsRoute extends Route { const { id: scope_id } = this.modelFor('scopes.scope'); const { user_id } = this.session.data.authenticated; const projects = this.project || []; - await this.resourceFilterStore.queryBy('session', { user_id }, { - recursive: true, - scope_id - }); - return this.resourceFilterStore.queryBy('target', { - scope_id: projects.map(({ id }) => id), - authorized_actions: [ - { contains: 'authorize-session' }, - ] - }, { - recursive: true, - scope_id - }); + await this.resourceFilterStore.queryBy( + 'session', + { user_id }, + { + recursive: true, + scope_id, + } + ); + return this.resourceFilterStore.queryBy( + 'target', + { + scope_id: projects.map(({ id }) => id), + authorized_actions: [{ contains: 'authorize-session' }], + }, + { + recursive: true, + scope_id, + } + ); } - /** - * When this route is activated (entered), begin polling for changes. - */ - activate() { - this.poller.perform(); - } - - /** - * When this route is deactivated (exited), stop polling for changes. - */ - deactivate() { - this.poller.cancelAll(); + @runEvery(POLL_TIMEOUT_SECONDS * 1000) + poller() { + return this.refresh(); } // =actions @@ -112,7 +91,7 @@ export default class ScopesScopeProjectsTargetsRoute extends Route { const options = { target_id: model.id, - token: this.session.data.authenticated.token + token: this.session.data.authenticated.token, }; if (host) options.host_id = host.id; @@ -132,10 +111,11 @@ export default class ScopesScopeProjectsTargetsRoute extends Route { session.proxy_address = address; session.proxy_port = port; if (credentials) { - credentials.forEach(cred => session.addCredential(cred)); + credentials.forEach((cred) => session.addCredential(cred)); } - } catch(e) { - this.confirm.confirm(e.message, { isConnectError: true }) + } catch (e) { + this.confirm + .confirm(e.message, { isConnectError: true }) // Retry .then(() => this.connect(model)) .catch(() => null /* no op */); diff --git a/ui/desktop/tests/acceptance/projects/targets-test.js b/ui/desktop/tests/acceptance/projects/targets-test.js index 4971f209f..2935cef15 100644 --- a/ui/desktop/tests/acceptance/projects/targets-test.js +++ b/ui/desktop/tests/acceptance/projects/targets-test.js @@ -9,7 +9,6 @@ import { //getRootElement //setupOnerror, } from '@ember/test-helpers'; -import { run, later } from '@ember/runloop'; import { setupApplicationTest } from 'ember-qunit'; import setupMirage from 'ember-cli-mirage/test-support/setup-mirage'; import { Response } from 'miragejs'; @@ -174,52 +173,35 @@ module('Acceptance | projects | targets', function (hooks) { test('visiting index', async function (assert) { assert.expect(2); const targetsCount = this.server.schema.targets.all().models.length; - // This later/cancelTimers technique allows us to test a page with - // active polling. Normally an acceptance test waits for all runloop timers - // to stop before returning from an awaited test, but polling means that - // runloop timers exist indefinitely. We thus schedule a cancelation before - // proceeding with our tests. - later(async () => { - run.cancelTimers(); - await a11yAudit(); - assert.equal(currentURL(), urls.targets); - assert.equal(findAll('tbody tr').length, targetsCount); - }, 750); await visit(urls.targets); + await a11yAudit(); + assert.equal(currentURL(), urls.targets); + assert.equal(findAll('tbody tr').length, targetsCount); }); test('visiting a target', async function (assert) { assert.expect(1); - later(async () => { - run.cancelTimers(); - await click('tbody tr th a'); - assert.equal(currentURL(), urls.sessions); - }, 750); await visit(urls.targets); + await click('tbody tr th a'); + assert.equal(currentURL(), urls.sessions); }); test('visiting empty targets', async function (assert) { assert.expect(1); this.server.get('/targets', () => new Response(200)); - later(async () => { - run.cancelTimers(); - assert.equal( - find('.rose-message-title').textContent.trim(), - 'No Targets Available' - ); - }, 750); await visit(urls.targets); + assert.equal( + find('.rose-message-title').textContent.trim(), + 'No Targets Available' + ); }); test('cannot navigate to a target without proper authorization', async function (assert) { assert.expect(1); instances.target.authorized_actions = instances.target.authorized_actions.filter((item) => item !== 'read'); - later(async () => { - run.cancelTimers(); - assert.notOk(find('main tbody .rose-table-header-cell:nth-child(1) a')); - }, 750); await visit(urls.targets); + assert.notOk(find('main tbody .rose-table-header-cell:nth-child(1) a')); }); test('connecting to a target', async function (assert) { @@ -233,26 +215,22 @@ module('Acceptance | projects | targets', function (hooks) { }); const confirmService = this.owner.lookup('service:confirm'); confirmService.enabled = true; - - later(async () => { - run.cancelTimers(); - await click( - 'tbody tr:first-child td:last-child button', - 'Activate connect mode' - ); - assert.ok(find('.dialog-detail'), 'Success dialog'); - assert.equal(findAll('.rose-dialog-footer button').length, 1); - assert.equal( - find('.rose-dialog-footer button').textContent.trim(), - 'Close', - 'Cannot retry' - ); - assert.equal( - find('.rose-dialog-body .copyable-content').textContent.trim(), - 'a_123:p_123' - ); - }, 750); await visit(urls.targets); + await click( + 'tbody tr:first-child td:last-child button', + 'Activate connect mode' + ); + assert.ok(find('.dialog-detail'), 'Success dialog'); + assert.equal(findAll('.rose-dialog-footer button').length, 1); + assert.equal( + find('.rose-dialog-footer button').textContent.trim(), + 'Close', + 'Cannot retry' + ); + assert.equal( + find('.rose-dialog-body .copyable-content').textContent.trim(), + 'a_123:p_123' + ); }); // Skipping because this test doesn't make sense. Users will never even see @@ -269,12 +247,9 @@ module('Acceptance | projects | targets', function (hooks) { assert.notOk( instances.target.authorized_actions.includes('authorize-session') ); - later(async () => { - run.cancelTimers(); - assert.ok(find('tbody tr:first-child')); - assert.notOk(find('tbody tr:first-child td:last-child button')); - }, 750); await visit(urls.targets); + assert.ok(find('tbody tr:first-child')); + assert.notOk(find('tbody tr:first-child td:last-child button')); }); test('handles cli error on connect', async function (assert) { @@ -282,20 +257,16 @@ module('Acceptance | projects | targets', function (hooks) { stubs.ipcService.withArgs('cliExists').returns(true); const confirmService = this.owner.lookup('service:confirm'); confirmService.enabled = true; - - later(async () => { - run.cancelTimers(); - await click( - 'tbody tr:first-child td:last-child button', - 'Activate connect mode' - ); - assert.ok(find('.rose-dialog-error'), 'Error dialog'); - const dialogButtons = findAll('.rose-dialog-footer button'); - assert.equal(dialogButtons.length, 2); - assert.equal(dialogButtons[0].textContent.trim(), 'Retry', 'Can retry'); - assert.equal(dialogButtons[1].textContent.trim(), 'Cancel', 'Can cancel'); - }, 750); await visit(urls.targets); + await click( + 'tbody tr:first-child td:last-child button', + 'Activate connect mode' + ); + assert.ok(find('.rose-dialog-error'), 'Error dialog'); + const dialogButtons = findAll('.rose-dialog-footer button'); + assert.equal(dialogButtons.length, 2); + assert.equal(dialogButtons[0].textContent.trim(), 'Retry', 'Can retry'); + assert.equal(dialogButtons[1].textContent.trim(), 'Cancel', 'Can cancel'); }); test('handles connect error', async function (assert) { @@ -304,20 +275,16 @@ module('Acceptance | projects | targets', function (hooks) { stubs.ipcService.withArgs('connect').rejects(); const confirmService = this.owner.lookup('service:confirm'); confirmService.enabled = true; - - later(async () => { - run.cancelTimers(); - await click( - 'tbody tr:first-child td:last-child button', - 'Activate connect mode' - ); - assert.ok(find('.rose-dialog-error'), 'Error dialog'); - const dialogButtons = findAll('.rose-dialog-footer button'); - assert.equal(dialogButtons.length, 2); - assert.equal(dialogButtons[0].textContent.trim(), 'Retry', 'Can retry'); - assert.equal(dialogButtons[1].textContent.trim(), 'Cancel', 'Can cancel'); - }, 750); await visit(urls.targets); + await click( + 'tbody tr:first-child td:last-child button', + 'Activate connect mode' + ); + assert.ok(find('.rose-dialog-error'), 'Error dialog'); + const dialogButtons = findAll('.rose-dialog-footer button'); + assert.equal(dialogButtons.length, 2); + assert.equal(dialogButtons[0].textContent.trim(), 'Retry', 'Can retry'); + assert.equal(dialogButtons[1].textContent.trim(), 'Cancel', 'Can cancel'); }); test('can retry on error', async function (assert) { @@ -325,18 +292,14 @@ module('Acceptance | projects | targets', function (hooks) { stubs.ipcService.withArgs('cliExists').rejects(); const confirmService = this.owner.lookup('service:confirm'); confirmService.enabled = true; - - later(async () => { - run.cancelTimers(); - await click( - 'tbody tr:first-child td:last-child button', - 'Activate connect mode' - ); - const firstErrorDialog = find('.rose-dialog'); - await click('.rose-dialog footer .rose-button-primary', 'Retry'); - const secondErrorDialog = find('.rose-dialog'); - assert.notEqual(secondErrorDialog.id, firstErrorDialog.id); - }, 750); await visit(urls.targets); + await click( + 'tbody tr:first-child td:last-child button', + 'Activate connect mode' + ); + const firstErrorDialog = find('.rose-dialog'); + await click('.rose-dialog footer .rose-button-primary', 'Retry'); + const secondErrorDialog = find('.rose-dialog'); + assert.notEqual(secondErrorDialog.id, firstErrorDialog.id); }); });