From bc7ed3f0ed409c2d296b64b541424f722cb15795 Mon Sep 17 00:00:00 2001 From: Devon Anderson Date: Mon, 29 Aug 2022 10:24:41 -0400 Subject: [PATCH] ICU 5247 detailed worker view and ICU 5248 edit worker view (#1246) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 🎸 Add list view for workers ✅ Closes: ICU-5251 * feat: 🎸 Add route for worker detail and include links in table * refactor: 💡 Updated id display in table and fixed worker route * test: 💍 Add workers list test * feat: 🎸 Add worker detailed view and edit view Added worker detailed view and editable form view for name and description of worker ✅ Closes: ICU-5247 ICU-5248 * refactor: 💡 fixed conflict merge fixed merge conflicts for scope/workers/worker ✅ Closes: ICU-5247 * Update ui/admin/app/routes/scopes/scope/workers.js Co-authored-by: Dharini Jeeva * Update ui/admin/app/templates/scopes/scope/workers/worker/index.hbs Co-authored-by: Dharini Jeeva * test: 💍 Test for worker/update-test.js Created update-test.js and refactored worker/form.hbs for correct display of last_seen ✅ Closes: ICU-5247 * Update ui/admin/app/routes/scopes/scope/workers.js Co-authored-by: Zhihe Li * Update ui/admin/app/templates/scopes/scope/workers/worker/index.hbs Co-authored-by: Zhihe Li * Update ui/admin/tests/acceptance/workers/read-test.js Co-authored-by: Zhihe Li * refactor: 💡 code refactor and test creation Refactored code for detailed worker view, created controller for detailew worker view to display worker name in breadcrumbs, added test for `delete`, `update`. ✅ Closes: ICU-5247 * refactor: 💡 Refactor test Refactor update-test.js and clean up code. ✅ Closes: ICU-5247 * refactor: 💡 fixed workers/update-test.js added local variable for better test coverage for cancel test in workers/update-test.js ✅ Closes: ICU-5247 * refactor: 💡 fix test Refactored yaml file for `last_seen.description` ✅ Closes: ICU-5247 * Update ui/admin/app/routes/scopes/scope/workers.js Co-authored-by: Zhihe Li * removed `type` from `` component * Update ui/admin/app/controllers/scopes/scope/workers/worker.js Co-authored-by: Zhihe Li * spacing for linter * refactor: 💡 Refactor test Refactored `worker/read-test.js` to to test correct authorization ✅ Closes: ICU-5247 * ci: 🎡 Fixed lint fixed lint ✅ Closes: ICU-5247 * refactor: 💡 Added custom relatvie date to field Added custom relative date to `last_status_time` input for worker view. ✅ Closes: ICU-5247 * refactor: 💡 refactored yaml file changed delete dropdown for worker to `actions.delete` ✅ Closes: ICU-5247 * Removed unused services from workers.js * merge changes from main * fix lint * fix lint * Update index.hbs lint issues * refactor: 💡 fixed lint for hbs fixed linting issues for hbs files ✅ Closes: ICU-5247 ICU-5249 Co-authored-by: Zhihe Li Co-authored-by: Dharini Jeeva --- addons/api/addon/models/worker.js | 2 +- .../api/mirage/generated/factories/worker.js | 5 +- addons/core/translations/form/en-us.yaml | 11 ++ addons/core/translations/resources/en-us.yaml | 3 +- ui/admin/app/components/form/worker/index.hbs | 89 +++++++++++++++ .../scopes/scope/workers/worker.js | 13 +++ ui/admin/app/formats.js | 8 ++ ui/admin/app/routes/scopes/scope/workers.js | 43 ++++++- .../scopes/scope/workers/worker/index.hbs | 51 ++++++++- .../tests/acceptance/workers/delete-test.js | 108 ++++++++++++++++++ .../tests/acceptance/workers/read-test.js | 68 +++++++++++ .../tests/acceptance/workers/update-test.js | 98 ++++++++++++++++ .../scopes/scope/workers/worker-test.js | 13 +++ 13 files changed, 505 insertions(+), 7 deletions(-) create mode 100644 ui/admin/app/components/form/worker/index.hbs create mode 100644 ui/admin/app/controllers/scopes/scope/workers/worker.js create mode 100644 ui/admin/tests/acceptance/workers/delete-test.js create mode 100644 ui/admin/tests/acceptance/workers/read-test.js create mode 100644 ui/admin/tests/acceptance/workers/update-test.js create mode 100644 ui/admin/tests/unit/controllers/scopes/scope/workers/worker-test.js diff --git a/addons/api/addon/models/worker.js b/addons/api/addon/models/worker.js index a210ff597..a6fa2f0c2 100644 --- a/addons/api/addon/models/worker.js +++ b/addons/api/addon/models/worker.js @@ -4,7 +4,7 @@ import { attr } from '@ember-data/model'; export default class WorkerModel extends GeneratedWorkerModel { // =attributes - /** + /** * Names are optional on models in our API. Thus we need to fallback on ID * for display purposes. * @type {string} diff --git a/addons/api/mirage/generated/factories/worker.js b/addons/api/mirage/generated/factories/worker.js index 7ce830569..8e5061871 100644 --- a/addons/api/mirage/generated/factories/worker.js +++ b/addons/api/mirage/generated/factories/worker.js @@ -10,8 +10,9 @@ export default Factory.extend({ description: () => faker.random.words(), created_time: () => faker.date.recent(), updated_time: () => faker.date.recent(), - version: () => faker.datatype.number(), - address: () => `${faker.internet.ipv4()}:${faker.internet.port()}`, + version: () => faker.datatype.number({ max: 4 }), + address: () => + `${faker.datatype.uuid()}.proxy.boundary.hashicorp.cloud:${faker.internet.port()}`, last_status_time: () => faker.date.recent(), active_connection_count: () => faker.datatype.number({ max: 10 }), }); diff --git a/addons/core/translations/form/en-us.yaml b/addons/core/translations/form/en-us.yaml index 6702191b3..bcf17abd0 100644 --- a/addons/core/translations/form/en-us.yaml +++ b/addons/core/translations/form/en-us.yaml @@ -41,6 +41,17 @@ session_max_seconds: session_connection_limit: label: Maximum Connections help: The maximum number of connections allowed per session. For unlimited, specify "-1". +worker_version: + label: Version +worker_authorized_actions: + label: Authorized Actions +worker_last_seen: + label: Last Seen + help: Last time the worker reported status. +worker_address: + help: URL and port for this worker. +worker_type: + help: Type of worker worker_filter: label: Worker Filter help: Optional boolean expression to filter the workers that are allowed to satisfy this request. diff --git a/addons/core/translations/resources/en-us.yaml b/addons/core/translations/resources/en-us.yaml index 80ce5e0fa..84f774a03 100644 --- a/addons/core/translations/resources/en-us.yaml +++ b/addons/core/translations/resources/en-us.yaml @@ -404,7 +404,7 @@ credential: password: label: Password help: The password to use when signing in with this credential store. - private_key: + private_key: label: SSH Private Key help: The private key to use when connecting with this credential store. passphrase: @@ -438,6 +438,7 @@ worker: description: Workers are the server components that perform the session handling. actions: register: Register Worker + delete: Remove Worker messages: welcome: title: Welcome to Workers diff --git a/ui/admin/app/components/form/worker/index.hbs b/ui/admin/app/components/form/worker/index.hbs new file mode 100644 index 000000000..159b39bee --- /dev/null +++ b/ui/admin/app/components/form/worker/index.hbs @@ -0,0 +1,89 @@ + + + {{#unless form.isEditable}} + + {{/unless}} + + + {{#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}} + + + {{#unless form.isEditable}} + + {{#let + (relative-datetime-live @model.last_status_time) + (format-date @model.last_status_time format='yymmddt') + as |relativeDate customFormat| + }} + + {{/let}} + {{/unless}} + + {{#if (can 'save worker' @model)}} + + {{/if}} + \ No newline at end of file diff --git a/ui/admin/app/controllers/scopes/scope/workers/worker.js b/ui/admin/app/controllers/scopes/scope/workers/worker.js new file mode 100644 index 000000000..3b1d0b551 --- /dev/null +++ b/ui/admin/app/controllers/scopes/scope/workers/worker.js @@ -0,0 +1,13 @@ +import Controller from '@ember/controller'; + +export default class ScopesScopeWorkersWorkerController extends Controller { + // =attributes + + /** + * A worker breadcrumb + * @type {string} + */ + get breadCrumb() { + return this.model.displayName; + } +} diff --git a/ui/admin/app/formats.js b/ui/admin/app/formats.js index 36c956ce3..1f808e253 100644 --- a/ui/admin/app/formats.js +++ b/ui/admin/app/formats.js @@ -21,6 +21,14 @@ export default { minute: 'numeric', second: 'numeric', }, + yymmddt: { + year: 'numeric', + month: 'short', + day: '2-digit', + hour: 'numeric', + minute: 'numeric', + timeZoneName: 'short', + }, }, number: { compact: { notation: 'compact' }, diff --git a/ui/admin/app/routes/scopes/scope/workers.js b/ui/admin/app/routes/scopes/scope/workers.js index 4d629acc5..afba82223 100644 --- a/ui/admin/app/routes/scopes/scope/workers.js +++ b/ui/admin/app/routes/scopes/scope/workers.js @@ -1,10 +1,15 @@ import Route from '@ember/routing/route'; import { inject as service } from '@ember/service'; import { action } from '@ember/object'; +import { loading } from 'ember-loading'; +import { confirm } from 'core/decorators/confirm'; +import { notifySuccess, notifyError } from 'core/decorators/notify'; + export default class ScopesScopeWorkersRoute extends Route { // =services - + + @service can @service session; @service router; @@ -24,4 +29,40 @@ export default class ScopesScopeWorkersRoute extends Route { refresh() { return super.refresh(...arguments); } + + /** + * Save a worker in current scope. + * @param {WorkerModel} worker + * @param {Event} e + */ + @action + @loading + @notifyError(({ message }) => message) + @notifySuccess(({ isNew }) => + isNew ? 'notifications.create-success' : 'notifications.save-success' + ) + async save(worker) { + await worker.save(); + if (this.can.can('read model', worker)) { + await this.router.transitionTo('scopes.scope.workers.worker', worker); + } else { + await this.router.transitionTo('scopes.scope.workers'); + } + this.refresh(); + } + + /** + * Delete worker in current scope and redirect to index. + * @param {UserModel} worker + */ + @action + @loading + @confirm('questions.delete-confirm') + @notifyError(({ message }) => message, { catch: true }) + @notifySuccess('notifications.delete-success') + async delete(worker) { + await worker.destroyRecord(); + await this.router.replaceWith('scopes.scope.workers'); + this.refresh(); + } } diff --git a/ui/admin/app/templates/scopes/scope/workers/worker/index.hbs b/ui/admin/app/templates/scopes/scope/workers/worker/index.hbs index b6d9ec078..236970277 100644 --- a/ui/admin/app/templates/scopes/scope/workers/worker/index.hbs +++ b/ui/admin/app/templates/scopes/scope/workers/worker/index.hbs @@ -1,2 +1,49 @@ -{{page-title 'Worker'}} -{{outlet}} \ No newline at end of file + + + + + + + +

+ {{t 'resources.worker.title'}} + +

+

{{t 'resources.worker.description'}}

+ + {{@model.id}} + +
+ + + + {{#if (can 'delete worker' @model)}} + + {{t 'resources.worker.actions.delete'}} + + {{/if}} + + + + + + + + +
\ No newline at end of file diff --git a/ui/admin/tests/acceptance/workers/delete-test.js b/ui/admin/tests/acceptance/workers/delete-test.js new file mode 100644 index 000000000..3d1e93ea3 --- /dev/null +++ b/ui/admin/tests/acceptance/workers/delete-test.js @@ -0,0 +1,108 @@ +import { module, test } from 'qunit'; +import { visit, find, click } from '@ember/test-helpers'; +import { setupApplicationTest } from 'ember-qunit'; +import setupMirage from 'ember-cli-mirage/test-support/setup-mirage'; +import { Response } from 'miragejs'; +import { resolve, reject } from 'rsvp'; +import sinon from 'sinon'; +import { + authenticateSession, + // These are left here intentionally for future reference. + //currentSession, + //invalidateSession, +} from 'ember-simple-auth/test-support'; + +module('Acceptance | workers | delete', function (hooks) { + setupApplicationTest(hooks); + setupMirage(hooks); + + let getWorkerCount; + + const instances = { + scopes: { + global: null, + }, + }; + + const urls = { + globalScope: null, + workers: null, + worker: null, + }; + + hooks.beforeEach(function () { + // Generate resources + instances.scopes.global = this.server.create('scope', { id: 'global' }); + instances.worker = this.server.create('worker', { + scope: instances.scopes.global, + }); + + urls.globalScope = `/scopes/global`; + urls.workers = `${urls.globalScope}/workers`; + urls.worker = `${urls.workers}/${instances.worker.id}`; + + getWorkerCount = () => this.server.schema.workers.all().models.length; + + authenticateSession({}); + }); + + test('can delete a worker', async function (assert) { + assert.expect(1); + const count = getWorkerCount(); + await visit(urls.worker); + await click('.rose-layout-page-actions .rose-dropdown-button-danger'); + assert.strictEqual(getWorkerCount(), count - 1); + }); + + test('can accept delete worker via dialog', async function (assert) { + assert.expect(2); + const confirmService = this.owner.lookup('service:confirm'); + confirmService.enabled = true; + confirmService.confirm = sinon.fake.returns(resolve()); + const count = getWorkerCount(); + await visit(urls.worker); + await click('.rose-layout-page-actions .rose-dropdown-button-danger'); + assert.strictEqual(getWorkerCount(), count - 1); + assert.ok(confirmService.confirm.calledOnce); + }); + + test('can cancel delete worker via dialog', async function (assert) { + assert.expect(2); + const confirmService = this.owner.lookup('service:confirm'); + confirmService.enabled = true; + confirmService.confirm = sinon.fake.returns(reject()); + const count = getWorkerCount(); + await visit(urls.worker); + await click('.rose-layout-page-actions .rose-dropdown-button-danger'); + assert.strictEqual(getWorkerCount(), count); + assert.ok(confirmService.confirm.calledOnce); + }); + + test('cannot delete worker without proper authorization', async function (assert) { + assert.expect(1); + instances.worker.authorized_actions = + instances.worker.authorized_actions.filter((item) => item !== 'delete'); + await visit(urls.worker); + assert + .dom('.rose-layout-page-actions .rose-dropdown-button-danger') + .isNotVisible(); + }); + + test('deleting a worker which errors displays error messages', async function (assert) { + assert.expect(1); + this.server.del('/workers/:id', () => { + return new Response( + 490, + {}, + { + status: 490, + code: 'error', + message: 'Oops.', + } + ); + }); + await visit(urls.worker); + await click('.rose-layout-page-actions .rose-dropdown-button-danger'); + assert.ok(find('[role="alert"]').textContent.trim(), 'Oops.'); + }); +}); diff --git a/ui/admin/tests/acceptance/workers/read-test.js b/ui/admin/tests/acceptance/workers/read-test.js new file mode 100644 index 000000000..1ae25c84c --- /dev/null +++ b/ui/admin/tests/acceptance/workers/read-test.js @@ -0,0 +1,68 @@ +import { module, test } from 'qunit'; +import { visit } 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 { + authenticateSession, + // These are left here intentionally for future reference. + //currentSession, + //invalidateSession, +} from 'ember-simple-auth/test-support'; + +module('Acceptance | workers | read', function (hooks) { + setupApplicationTest(hooks); + setupMirage(hooks); + + const instances = { + scopes: { + global: null, + }, + worker: null, + }; + + const urls = { + globalScope: null, + workers: null, + worker: null, + }; + + hooks.beforeEach(function () { + //Generate the resources + instances.scopes.global = this.server.create('scope', { id: 'global' }); + instances.worker = this.server.create('worker', { + scope: instances.scopes.global, + }); + // Generate route URLs for resources + urls.globalScope = '/scopes/global'; + urls.workers = `${urls.globalScope}/workers`; + urls.worker = `${urls.workers}/${instances.worker.id}`; + authenticateSession({}); + }); + + test('visiting workers', async function (assert) { + assert.expect(1); + await visit(urls.workers); + await a11yAudit(); + console.log(urls.workers); + assert.dom(`[href="${urls.workers}"]`).isVisible(); + }); + + test('cannot navigate to an worker form without proper authorization', async function (assert) { + assert.expect(1); + instances.worker.authorized_actions = + instances.worker.authorized_actions.filter((itm) => itm !== 'read'); + await visit(urls.workers); + await a11yAudit(); + assert + .dom('main tbody .rose-table-header-cell:nth-child(1) a') + .isNotVisible(); + }); + + test('can navigate to an worker form with proper authorization', async function (assert) { + assert.expect(1); + await visit(urls.workers); + await a11yAudit(); + assert.dom('main tbody .rose-table-header-cell:nth-child(1) a').isVisible(); + }); +}); diff --git a/ui/admin/tests/acceptance/workers/update-test.js b/ui/admin/tests/acceptance/workers/update-test.js new file mode 100644 index 000000000..1a744ae1c --- /dev/null +++ b/ui/admin/tests/acceptance/workers/update-test.js @@ -0,0 +1,98 @@ +import { module, test } from 'qunit'; +import { visit, click, find, fillIn } from '@ember/test-helpers'; +import { setupApplicationTest } from 'ember-qunit'; +import setupMirage from 'ember-cli-mirage/test-support/setup-mirage'; +import { Response } from 'miragejs'; +import { + authenticateSession, + // These are left here intentionally for future reference. + //currentSession, + //invalidateSession, +} from 'ember-simple-auth/test-support'; + +module('Acceptance | workers | update', function (hooks) { + setupApplicationTest(hooks); + setupMirage(hooks); + + const instances = { + scopes: { + global: null, + }, + }; + + const urls = { + globalScope: null, + workers: null, + worker: null, + }; + + hooks.beforeEach(function () { + //Generate the resources + instances.scopes.global = this.server.create('scope', { id: 'global' }); + instances.worker = this.server.create('worker', { + scope: instances.scopes.global, + }); + // Generate route URLs for resources + urls.globalScope = '/scopes/global'; + urls.workers = `${urls.globalScope}/workers`; + (urls.worker = `${urls.workers}/${instances.worker.id}`), + authenticateSession({}); + }); + + test('can save changes to an existing worker', async function (assert) { + assert.expect(2); + await visit(urls.worker); + await click('form [type="button"]', 'Click edit mode'); + await fillIn('[name="name"]', 'Updated worker name'); + await click('.rose-form-actions [type="submit"]'); + assert.dom(`[href="${urls.worker}"]`).isVisible(); + await assert.dom('input[name="name"]').hasValue('Updated worker name'); + }); + + test('can cancel changes to an existing worker', async function (assert) { + assert.expect(1); + const name = instances.worker.name; + await visit(urls.worker); + await click('form [type="button"]', 'Click edit mode'); + await fillIn('[name="name"]', `Updated Worker Name`); + await click('.rose-form-actions [type="button"]'); + await assert.dom('input[name="name"]').hasValue(`${name}`); + }); + + test('saving an existing worker with invalid fields displays error messages', async function (assert) { + assert.expect(2); + this.server.patch('/workers/:id', () => { + return new Response( + 400, + {}, + { + status: 400, + code: 'invalid_argument', + message: 'The request was invalid.', + details: { + request_fields: [ + { + name: 'name', + description: 'Name is required', + }, + ], + }, + } + ); + }); + await visit(urls.worker); + await click('.rose-form-actions [type="button"]', 'Click edit mode'); + await fillIn('[name="name"]', 'Worker Name'); + await click('[type="submit"]'); + assert.strictEqual( + find('.rose-notification-body').textContent.trim(), + 'The request was invalid.', + 'Displays primary error message.' + ); + assert.strictEqual( + find('.rose-form-error-message').textContent.trim(), + 'Name is required', + 'Displays field-level errors.' + ); + }); +}); diff --git a/ui/admin/tests/unit/controllers/scopes/scope/workers/worker-test.js b/ui/admin/tests/unit/controllers/scopes/scope/workers/worker-test.js new file mode 100644 index 000000000..afb5877cd --- /dev/null +++ b/ui/admin/tests/unit/controllers/scopes/scope/workers/worker-test.js @@ -0,0 +1,13 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module('Unit | Controller | scopes/scope/users/user', function (hooks) { + setupTest(hooks); + + test('it exists', function (assert) { + let controller = this.owner.lookup( + 'controller:scopes/scope/workers/worker' + ); + assert.ok(controller); + }); +});