diff --git a/addons/api/addon/abilities/worker.js b/addons/api/addon/abilities/worker.js index 5c7e12c3d..ac5fe9e76 100644 --- a/addons/api/addon/abilities/worker.js +++ b/addons/api/addon/abilities/worker.js @@ -17,4 +17,8 @@ export default class WorkerAbility extends ModelAbility { get canCreateWorkerLed() { return this.hasAuthorizedCollectionAction('create:worker-led'); } + + get canSetWorkerTags() { + return this.hasAuthorizedAction('set-worker-tags'); + } } diff --git a/addons/api/addon/models/worker.js b/addons/api/addon/models/worker.js index 21e4a3628..b33603fa9 100644 --- a/addons/api/addon/models/worker.js +++ b/addons/api/addon/models/worker.js @@ -6,6 +6,8 @@ import GeneratedWorkerModel from '../generated/models/worker'; import { attr } from '@ember-data/model'; +export const TYPE_WORKER_PKI = 'pki'; + export const TAG_TYPE_CONFIG = 'config'; export const TAG_TYPE_API = 'api'; @@ -26,7 +28,7 @@ export default class WorkerModel extends GeneratedWorkerModel { * @type {boolean} */ get isPki() { - return this.type === 'pki'; + return this.type === TYPE_WORKER_PKI; } /** @@ -96,7 +98,6 @@ export default class WorkerModel extends GeneratedWorkerModel { @attr({ description: 'The api tags set for the worker.\nOutput only.', - readOnly: true, }) api_tags; @@ -120,4 +121,15 @@ export default class WorkerModel extends GeneratedWorkerModel { }, }); } + + /** + * Method to set api tags on the worker. + * @param {object} apiTags + * @return {Promise} + */ + setApiTags(apiTags) { + return this.save({ + adapterOptions: { method: 'set-worker-tags', apiTags }, + }); + } } diff --git a/addons/api/addon/serializers/worker.js b/addons/api/addon/serializers/worker.js index 25dac70f4..837127c95 100644 --- a/addons/api/addon/serializers/worker.js +++ b/addons/api/addon/serializers/worker.js @@ -19,11 +19,19 @@ export default class WorkerSerializer extends ApplicationSerializer { let serialized = super.serialize(...arguments); const workerGeneratedAuthToken = snapshot?.adapterOptions?.workerGeneratedAuthToken; - if (workerGeneratedAuthToken) + if (workerGeneratedAuthToken) { serialized = this.serializeWithGeneratedToken( snapshot, workerGeneratedAuthToken, ); + } + const apiTags = snapshot?.adapterOptions?.apiTags; + if (apiTags) { + serialized = this.serializeWithApiTags(snapshot, apiTags); + } else { + // remove api_tags from payload if not set + delete serialized.api_tags; + } return serialized; } @@ -39,4 +47,17 @@ export default class WorkerSerializer extends ApplicationSerializer { worker_generated_auth_token: workerGeneratedAuthToken, }; } + + /** + * Returns a payload with api_tags and version serialized. + * @param {Snapshot} snapshot + * @param {object} apiTags + * @return {object} + */ + serializeWithApiTags(snapshot, apiTags) { + return { + version: snapshot.attr('version'), + api_tags: apiTags, + }; + } } diff --git a/addons/api/mirage/config.js b/addons/api/mirage/config.js index e8087c897..e79d1358e 100644 --- a/addons/api/mirage/config.js +++ b/addons/api/mirage/config.js @@ -16,6 +16,7 @@ import initializeMockIPC from './scenarios/ipc'; import makeBooleanFilter from './helpers/bexpr-filter'; import { faker } from '@faker-js/faker'; import { asciicasts } from './data/asciicasts'; +import { TYPE_WORKER_PKI } from 'api/models/worker'; const isTesting = environmentConfig.environment === 'test'; @@ -676,11 +677,30 @@ function routes() { // This POST only takes in a token so we need to generate a random worker to return const newWorker = this.create('worker', { - type: 'pki', + type: TYPE_WORKER_PKI, scope: globalScope, }); return workers.create(newWorker.attrs); }); + this.post( + '/workers/:idMethod', + function ({ workers }, { params: { idMethod } }) { + const attrs = this.normalizedRequestAttrs(); + const id = idMethod.split(':')[0]; + const method = idMethod.split(':')[1]; + const worker = workers.find(id); + let updatedAttrs = {}; + + if (method === 'set-worker-tags') { + updatedAttrs = { + version: attrs.version, + apiTags: attrs.apiTags, + }; + } + + return worker.update(updatedAttrs); + }, + ); // storage-buckets this.get( diff --git a/addons/api/mirage/factories/worker.js b/addons/api/mirage/factories/worker.js index 3fcdce27b..0c711b20a 100644 --- a/addons/api/mirage/factories/worker.js +++ b/addons/api/mirage/factories/worker.js @@ -8,8 +8,6 @@ import permissions from '../helpers/permissions'; import generateId from '../helpers/id'; import { faker } from '@faker-js/faker'; -const types = ['pki', 'kms']; - export default factory.extend({ id: () => generateId('w_'), authorized_actions: () => @@ -18,8 +16,8 @@ export default factory.extend({ 'read', 'update', 'delete', + 'set-worker-tags', ], - type: (i) => types[i % types.length], config_tags: (i) => { if (i % 3 === 0) { return { diff --git a/addons/api/mirage/generated/factories/worker.js b/addons/api/mirage/generated/factories/worker.js index b8008658b..5ff4e6101 100644 --- a/addons/api/mirage/generated/factories/worker.js +++ b/addons/api/mirage/generated/factories/worker.js @@ -5,6 +5,7 @@ import { Factory } from 'miragejs'; import { faker } from '@faker-js/faker'; +import { TYPE_WORKER_PKI } from 'api/models/worker'; /** * GeneratedWorkerModelFactory @@ -13,6 +14,7 @@ import { faker } from '@faker-js/faker'; export default Factory.extend({ name: () => faker.word.words(), description: () => faker.word.words(), + type: TYPE_WORKER_PKI, created_time: () => faker.date.recent(), updated_time: () => faker.date.recent(), version: () => faker.number.int(), diff --git a/addons/api/tests/unit/abilities/worker-test.js b/addons/api/tests/unit/abilities/worker-test.js new file mode 100644 index 000000000..7a8fe37ed --- /dev/null +++ b/addons/api/tests/unit/abilities/worker-test.js @@ -0,0 +1,56 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module('Unit | Abilities | Worker', function (hooks) { + setupTest(hooks); + + let canService; + let store; + + const instances = { + worker: null, + }; + + hooks.beforeEach(function () { + canService = this.owner.lookup('service:can'); + store = this.owner.lookup('service:store'); + instances.worker = store.createRecord('worker'); + }); + + test('can create worker-led', function (assert) { + const model = { + authorized_collection_actions: { workers: ['create:worker-led'] }, + }; + assert.true( + canService.can('createWorkerLed worker', model, { + collection: 'workers', + }), + ); + }); + + test('cannot create worker-led', function (assert) { + const model = { + authorized_collection_actions: { workers: [] }, + }; + assert.true( + canService.cannot('createWorkerLed worker', model, { + collection: 'workers', + }), + ); + }); + + test('can set worker tags', function (assert) { + instances.worker.authorized_actions = ['set-worker-tags']; + assert.true(canService.can('setWorkerTags worker', instances.worker)); + }); + + test('cannot set worker tags', function (assert) { + instances.worker.authorized_actions = []; + assert.true(canService.cannot('setWorkerTags worker', instances.worker)); + }); +}); diff --git a/addons/api/tests/unit/models/worker-test.js b/addons/api/tests/unit/models/worker-test.js index 83009c6e4..535d18df8 100644 --- a/addons/api/tests/unit/models/worker-test.js +++ b/addons/api/tests/unit/models/worker-test.js @@ -6,6 +6,7 @@ import { module, test } from 'qunit'; import { setupTest } from 'ember-qunit'; import setupMirage from 'ember-cli-mirage/test-support/setup-mirage'; +import { TYPE_WORKER_PKI } from 'api/models/worker'; module('Unit | Model | worker', function (hooks) { setupTest(hooks); @@ -26,7 +27,7 @@ module('Unit | Model | worker', function (hooks) { const store = this.owner.lookup('service:store'); const model = store.createRecord('worker', { - type: 'pki', + type: TYPE_WORKER_PKI, scope: { scope_id: scopeId, type: 'global', @@ -39,6 +40,38 @@ module('Unit | Model | worker', function (hooks) { assert.ok(worker); }); + test('it has `setApiTags` method that targets a specific POST API', async function (assert) { + assert.expect(1); + const workerId = 'w_123'; + const tags = { + tag1: ['value1', 'value2'], + tag2: ['value3'], + }; + this.server.post( + `/workers/${workerId}:set-worker-tags`, + (schema, request) => { + const body = JSON.parse(request.requestBody); + assert.deepEqual(body, { api_tags: tags, version: 1 }); + return { id: workerId }; + }, + ); + + const store = this.owner.lookup('service:store'); + store.push({ + data: { + id: workerId, + type: 'worker', + attributes: { + name: 'fake worker', + version: 1, + }, + }, + }); + + const model = store.peekRecord('worker', workerId); + await model.setApiTags(tags); + }); + test('it has a `configTagList` method that returns an array of key/value pair objects', function (assert) { const store = this.owner.lookup('service:store'); const model = store.createRecord('worker', { diff --git a/addons/api/tests/unit/serializers/worker-test.js b/addons/api/tests/unit/serializers/worker-test.js index bc3c86755..487fdd2d2 100644 --- a/addons/api/tests/unit/serializers/worker-test.js +++ b/addons/api/tests/unit/serializers/worker-test.js @@ -9,7 +9,7 @@ import { setupTest } from 'ember-qunit'; module('Unit | Serializer | worker', function (hooks) { setupTest(hooks); - test('it serializes normally without adapterOptions.workerGeneratedAuthToken', function (assert) { + test('it serializes normally', function (assert) { const store = this.owner.lookup('service:store'); const serializer = store.serializerFor('worker'); const record = store.createRecord('worker', { @@ -50,4 +50,25 @@ module('Unit | Serializer | worker', function (hooks) { worker_generated_auth_token: '123-abc', }); }); + + test('it serializes using `adapterOptions.apiTags`', function (assert) { + const store = this.owner.lookup('service:store'); + const serializer = store.serializerFor('worker'); + const record = store.createRecord('worker', { + name: 'worker', + api_tags: null, + version: 1, + }); + const snapshot = record._createSnapshot(); + snapshot.adapterOptions = { + apiTags: { + key: ['value'], + }, + }; + const serializedRecord = serializer.serialize(snapshot); + assert.deepEqual(serializedRecord, { + version: 1, + api_tags: { key: ['value'] }, + }); + }); }); diff --git a/addons/core/translations/resources/en-us.yaml b/addons/core/translations/resources/en-us.yaml index ffe553ea7..fc0a45ce3 100644 --- a/addons/core/translations/resources/en-us.yaml +++ b/addons/core/translations/resources/en-us.yaml @@ -975,6 +975,7 @@ worker: actions: register: Register Worker delete: Remove Worker + create_tags: Create new tags messages: welcome: title: Welcome to Workers @@ -990,6 +991,10 @@ worker: type: Type tags: title_plural: Tags + create: + title: Create new tags + description: Create new api worker tags. You can only add API tags in the admin UI or CLI. Config tags are added in the worker config file. + success: Tag(s) successfully created description: Tags are used to define the type of worker and to make it distinguishable amongst other workers. flyout: title: Tags in {workerName} diff --git a/ui/admin/app/components/form/worker/create-tags/index.hbs b/ui/admin/app/components/form/worker/create-tags/index.hbs new file mode 100644 index 000000000..d9c75cdf8 --- /dev/null +++ b/ui/admin/app/components/form/worker/create-tags/index.hbs @@ -0,0 +1,36 @@ +{{! + Copyright (c) HashiCorp, Inc. + SPDX-License-Identifier: BUSL-1.1 +}} + + + + <:field as |F|> + + <:key as |K|> + + + <:value as |V|> + + + + + + + + \ No newline at end of file diff --git a/ui/admin/app/components/form/worker/create-tags/index.js b/ui/admin/app/components/form/worker/create-tags/index.js new file mode 100644 index 000000000..da2827782 --- /dev/null +++ b/ui/admin/app/components/form/worker/create-tags/index.js @@ -0,0 +1,62 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +import Component from '@glimmer/component'; +import { inject as service } from '@ember/service'; +import { action } from '@ember/object'; +import Tag from '../tag'; + +export default class FormWorkerCreateTagsIndexComponent extends Component { + // =services + + @service router; + + // =actions + + /** + * Creates a new Tag object and adds it to the `apiTags` array. + * @param {object} option + */ + @action + addApiTag(option) { + this.args.apiTags.push(new Tag(option.key, option.value)); + } + + /** + * Removes a Tag object from the `apiTags` array by index. + * @param {number} index + */ + @action + removeApiTagByIndex(index) { + this.args.apiTags.splice(index, 1); + } + + /** + * Uses `@apiTags` to build and submits it to the parent component. + * If there are no tags, it transitions to the tags route. + * @returns {void} + */ + @action + save() { + if (this.args.apiTags.length === 0) { + this.router.transitionTo('scopes.scope.workers.worker.tags'); + return; + } + + const apiTags = structuredClone(this.args.model.api_tags ?? {}); + this.args.apiTags.forEach((tag) => { + let key = tag.key; + let values = tag.value.split(','); + + if (!apiTags[key]) { + apiTags[key] = values; + } else { + apiTags[key] = [...new Set([...apiTags[key], ...values])]; + } + }); + + this.args.submit(apiTags); + } +} diff --git a/ui/admin/app/components/form/worker/create-worker-led/index.js b/ui/admin/app/components/form/worker/create-worker-led/index.js index 7062b89be..038fb2c96 100644 --- a/ui/admin/app/components/form/worker/create-worker-led/index.js +++ b/ui/admin/app/components/form/worker/create-worker-led/index.js @@ -8,16 +8,7 @@ import { tracked } from '@glimmer/tracking'; import { inject as service } from '@ember/service'; import { action } from '@ember/object'; import { A } from '@ember/array'; - -class Tag { - @tracked key; - @tracked value; - - constructor(key, value) { - this.key = key; - this.value = value; - } -} +import Tag from '../tag'; export default class FormWorkerCreateWorkerLedComponent extends Component { // =services diff --git a/ui/admin/app/components/form/worker/tag.js b/ui/admin/app/components/form/worker/tag.js new file mode 100644 index 000000000..def35c8fa --- /dev/null +++ b/ui/admin/app/components/form/worker/tag.js @@ -0,0 +1,16 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +import { tracked } from '@glimmer/tracking'; + +export default class Tag { + @tracked key; + @tracked value; + + constructor(key, value) { + this.key = key; + this.value = value; + } +} diff --git a/ui/admin/app/components/workers/worker/actions/index.hbs b/ui/admin/app/components/workers/worker/actions/index.hbs index e6882134e..0b8a2abe1 100644 --- a/ui/admin/app/components/workers/worker/actions/index.hbs +++ b/ui/admin/app/components/workers/worker/actions/index.hbs @@ -5,6 +5,12 @@ + {{#if (can 'setWorkerTags worker' @model)}} + + {{/if}} {{#if (can 'delete worker' @model)}} message) + @notifySuccess('resources.worker.tags.create.success') + async save(apiTags) { + const worker = this.model; + await worker.setApiTags(apiTags); + // Clear the apiTags array after saving + this.apiTags = new TrackedArray([]); + this.router.transitionTo('scopes.scope.workers.worker.tags', worker); + } + + /** + * Cancel the creation of api tags for a worker. + */ + @action + cancel() { + this.apiTags = new TrackedArray([]); + this.router.replaceWith('scopes.scope.workers.worker.tags'); + } +} diff --git a/ui/admin/app/router.js b/ui/admin/app/router.js index 8e7970414..3de395831 100644 --- a/ui/admin/app/router.js +++ b/ui/admin/app/router.js @@ -155,6 +155,7 @@ Router.map(function () { this.route('new'); this.route('worker', { path: ':worker_id' }, function () { this.route('tags'); + this.route('create-tags'); }); }); this.route('session-recordings', function () { diff --git a/ui/admin/app/routes/scopes/scope/workers/worker/create-tags.js b/ui/admin/app/routes/scopes/scope/workers/worker/create-tags.js new file mode 100644 index 000000000..c361396d3 --- /dev/null +++ b/ui/admin/app/routes/scopes/scope/workers/worker/create-tags.js @@ -0,0 +1,35 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +import Route from '@ember/routing/route'; +import { action } from '@ember/object'; +import { inject as service } from '@ember/service'; +import { TrackedArray } from 'tracked-built-ins'; + +export default class ScopesScopeWorkersWorkerCreateTagsRoute extends Route { + // =services + + @service confirm; + @service intl; + + @action + async willTransition(transition) { + // eslint-disable-next-line ember/no-controller-access-in-routes + const controller = this.controllerFor(this.routeName); + if (controller.get('apiTags').length) { + transition.abort(); + try { + await this.confirm.confirm(this.intl.t('questions.abandon-confirm'), { + title: 'titles.abandon-confirm', + confirm: 'actions.discard', + }); + controller.set('apiTags', new TrackedArray([])); + transition.retry(); + } catch (e) { + // if user denies, do nothing + } + } + } +} diff --git a/ui/admin/app/styles/app.scss b/ui/admin/app/styles/app.scss index d5fdb924d..dd792a8cd 100644 --- a/ui/admin/app/styles/app.scss +++ b/ui/admin/app/styles/app.scss @@ -699,9 +699,15 @@ .hds-table__th { height: auto; line-height: 1; + border-bottom: unset; } } + // Remove divider between table header cells + .hds-table__thead .hds-table__tr .hds-table__th + .hds-table__th::before { + all: unset; + } + .hds-table__thead, .hds-table__tbody { .hds-table__tr { diff --git a/ui/admin/app/templates/scopes/scope/workers/worker/create-tags.hbs b/ui/admin/app/templates/scopes/scope/workers/worker/create-tags.hbs new file mode 100644 index 000000000..4d8855328 --- /dev/null +++ b/ui/admin/app/templates/scopes/scope/workers/worker/create-tags.hbs @@ -0,0 +1,34 @@ +{{! + Copyright (c) HashiCorp, Inc. + SPDX-License-Identifier: BUSL-1.1 +}} + +{{page-title (t 'resources.worker.actions.create_tags')}} + + + + + + + + + + + {{t 'resources.worker.tags.create.title'}} + {{! TODO: Add doclink }} + + {{t 'resources.worker.tags.create.description'}} + + + + + + \ No newline at end of file diff --git a/ui/admin/tests/acceptance/workers/create-tags-test.js b/ui/admin/tests/acceptance/workers/create-tags-test.js new file mode 100644 index 000000000..96d3740c9 --- /dev/null +++ b/ui/admin/tests/acceptance/workers/create-tags-test.js @@ -0,0 +1,142 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +import { module, test } from 'qunit'; +import { visit, currentURL, click, fillIn } 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 } from 'ember-simple-auth/test-support'; + +module('Acceptance | workers | worker | create-tags', function (hooks) { + setupApplicationTest(hooks); + setupMirage(hooks); + + const SAVE_BUTTON_SELECTOR = '[type="submit"]'; + const CANCEL_BUTTON_SELECTOR = '.rose-form-actions [type="button"]'; + const MANAGE_DROPDOWN_TOGGLE = + '[data-test-manage-worker-dropdown] div button'; + const CREATE_TAGS_BUTTON_SELECTOR = + '[data-test-manage-worker-dropdown] div:first-child a'; + const KEY_INPUT_SELECTOR = '[name="api_tags"] tr td:first-child input'; + const VALUE_INPUT_SELECTOR = '[name="api_tags"] tr td:nth-child(2) input'; + const ADD_INPUT_SELECTOR = '[name="api_tags"] tr td:last-child button'; + const DISCARD_CHANGES_DIALOG = '.rose-dialog'; + const DISCARD_CHANGES_DISCARD_BUTTON = + '.rose-dialog-footer .rose-button-primary'; + const DISCARD_CHANGES_CANCEL_BUTTON = + '.rose-dialog-footer .rose-button-secondary'; + + const instances = { + scopes: { + global: null, + }, + worker: null, + }; + + const urls = { + workers: null, + worker: null, + tags: null, + createTags: null, + }; + + hooks.beforeEach(function () { + instances.scopes.global = this.server.create('scope', { id: 'global' }); + instances.worker = this.server.create('worker', { + scope: instances.scopes.global, + }); + urls.workers = `/scopes/global/workers`; + urls.worker = `${urls.workers}/${instances.worker.id}`; + urls.tags = `${urls.worker}/tags`; + urls.createTags = `${urls.worker}/create-tags`; + + authenticateSession({}); + }); + + test('visiting worker create tags', async function (assert) { + await visit(urls.worker); + + await click(MANAGE_DROPDOWN_TOGGLE); + await click(CREATE_TAGS_BUTTON_SELECTOR); + await a11yAudit(); + + assert.strictEqual(currentURL(), urls.createTags); + }); + + test('cannot visit worker create tags without proper permissions', async function (assert) { + instances.worker.authorized_actions = + instances.worker.authorized_actions.filter( + (item) => item !== 'set-worker-tags', + ); + await visit(urls.worker); + + await click(MANAGE_DROPDOWN_TOGGLE); + + assert.dom(CREATE_TAGS_BUTTON_SELECTOR).doesNotExist(); + }); + + test('user can save worker tags', async function (assert) { + await visit(urls.tags); + + assert.dom('tbody tr').exists({ count: 11 }); + + await click(MANAGE_DROPDOWN_TOGGLE); + await click(CREATE_TAGS_BUTTON_SELECTOR); + await fillIn(KEY_INPUT_SELECTOR, 'key'); + await fillIn(VALUE_INPUT_SELECTOR, 'value'); + await click(ADD_INPUT_SELECTOR); + + await click(SAVE_BUTTON_SELECTOR); + + assert.dom('tbody tr').exists({ count: 12 }); + assert.strictEqual(currentURL(), urls.tags); + }); + + test('user can cancel tag creation', async function (assert) { + await visit(urls.worker); + + await click(MANAGE_DROPDOWN_TOGGLE); + await click(CREATE_TAGS_BUTTON_SELECTOR); + + await click(CANCEL_BUTTON_SELECTOR); + + assert.strictEqual(currentURL(), urls.tags); + }); + + test('user is prompted to confirm exit when there are unsaved changes', async function (assert) { + const confirmService = this.owner.lookup('service:confirm'); + confirmService.enabled = true; + await visit(urls.createTags); + await fillIn(KEY_INPUT_SELECTOR, 'key'); + await fillIn(VALUE_INPUT_SELECTOR, 'value'); + await click(ADD_INPUT_SELECTOR); + + await click(`[href="${urls.workers}"]`); + + assert.dom(DISCARD_CHANGES_DIALOG).isVisible(); + + await click(DISCARD_CHANGES_DISCARD_BUTTON); + + assert.strictEqual(currentURL(), urls.workers); + }); + + test('user can cancel transition when there are unsaved changes', async function (assert) { + const confirmService = this.owner.lookup('service:confirm'); + confirmService.enabled = true; + await visit(urls.createTags); + await fillIn(KEY_INPUT_SELECTOR, 'key'); + await fillIn(VALUE_INPUT_SELECTOR, 'value'); + await click(ADD_INPUT_SELECTOR); + + await click(`[href="${urls.workers}"]`); + + assert.dom(DISCARD_CHANGES_DIALOG).isVisible(); + + await click(DISCARD_CHANGES_CANCEL_BUTTON); + + assert.strictEqual(currentURL(), urls.createTags); + }); +}); diff --git a/ui/admin/tests/acceptance/workers/tags-test.js b/ui/admin/tests/acceptance/workers/tags-test.js index 4a0b2b10d..fc9e2a12b 100644 --- a/ui/admin/tests/acceptance/workers/tags-test.js +++ b/ui/admin/tests/acceptance/workers/tags-test.js @@ -33,8 +33,9 @@ module('Acceptance | workers | worker | tags', function (hooks) { hooks.beforeEach(function () { instances.scopes.global = this.server.create('scope', { id: 'global' }); - const scope = instances.scopes.global; - instances.worker = this.server.create('worker', { scopeId: scope.id }); + instances.worker = this.server.create('worker', { + scope: instances.scopes.global, + }); urls.workers = `/scopes/global/workers`; urls.worker = `${urls.workers}/${instances.worker.id}`; urls.tags = `${urls.worker}/tags`; diff --git a/ui/admin/tests/integration/components/form/worker/create-tags/index-test.js b/ui/admin/tests/integration/components/form/worker/create-tags/index-test.js new file mode 100644 index 000000000..cb8db563d --- /dev/null +++ b/ui/admin/tests/integration/components/form/worker/create-tags/index-test.js @@ -0,0 +1,70 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'admin/tests/helpers'; +import { setupIntl } from 'ember-intl/test-support'; +import { click, render } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; + +module( + 'Integration | Component | form/worker/create-tags/index', + function (hooks) { + setupRenderingTest(hooks); + setupIntl(hooks); + + const SAVE_BUTTON_SELECTOR = '[type="submit"]'; + const CANCEL_BUTTON_SELECTOR = '.rose-form-actions [type="button"]'; + + test('it renders', async function (assert) { + this.model = {}; + this.apiTags = []; + this.save = () => {}; + this.cancel = () => {}; + + await render( + hbs``, + ); + + assert + .dom(this.element) + .includesText('Key Value Actions Add Save Cancel'); + }); + + test('it calls the passed in save action', async function (assert) { + this.model = {}; + this.apiTags = [{ key: 'key', value: 'value' }]; + this.save = () => { + this.set('called', true); + }; + this.cancel = () => {}; + + await render( + hbs``, + ); + + await click(SAVE_BUTTON_SELECTOR); + + assert.true(this.called); + }); + + test('it calls the passed in cancel action', async function (assert) { + this.model = {}; + this.apiTags = []; + this.save = () => {}; + this.cancel = () => { + this.set('called', true); + }; + + await render( + hbs``, + ); + + await click(CANCEL_BUTTON_SELECTOR); + + assert.true(this.called); + }); + }, +); diff --git a/ui/admin/tests/integration/components/workers/worker/actions/index-test.js b/ui/admin/tests/integration/components/workers/worker/actions/index-test.js index cc4d07343..9e2ee87f9 100644 --- a/ui/admin/tests/integration/components/workers/worker/actions/index-test.js +++ b/ui/admin/tests/integration/components/workers/worker/actions/index-test.js @@ -16,7 +16,10 @@ module( setupIntl(hooks); test('it renders', async function (assert) { - this.set('model', { id: 'w_123', authorized_actions: ['delete'] }); + this.set('model', { + id: 'w_123', + authorized_actions: ['delete', 'set-worker-tags'], + }); this.set('delete', () => {}); await render( @@ -25,6 +28,7 @@ module( await click('button'); assert.dom(this.element).includesText('Remove Worker'); + assert.dom(this.element).includesText('Create new tags'); }); }, ); diff --git a/ui/admin/tests/unit/controllers/scopes/scope/workers/new-test.js b/ui/admin/tests/unit/controllers/scopes/scope/workers/new-test.js index 855d4f138..730124b0b 100644 --- a/ui/admin/tests/unit/controllers/scopes/scope/workers/new-test.js +++ b/ui/admin/tests/unit/controllers/scopes/scope/workers/new-test.js @@ -57,14 +57,4 @@ module('Unit | Controller | scopes/scope/workers/new', function (hooks) { assert.notEqual(worker.name, 'test'); }); - - test('createWorkerLed action saves changes on the specified model', async function (assert) { - await visit(urls.workers); - const worker = await store.findRecord('worker', instances.worker.id); - worker.name = 'test'; - - await controller.createWorkerLed(worker); - - assert.strictEqual(worker.name, 'test'); - }); }); diff --git a/ui/admin/tests/unit/controllers/scopes/scope/workers/worker/create-tags-test.js b/ui/admin/tests/unit/controllers/scopes/scope/workers/worker/create-tags-test.js new file mode 100644 index 000000000..e65a32dae --- /dev/null +++ b/ui/admin/tests/unit/controllers/scopes/scope/workers/worker/create-tags-test.js @@ -0,0 +1,75 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; +import { currentURL, visit } from '@ember/test-helpers'; +import setupMirage from 'ember-cli-mirage/test-support/setup-mirage'; +import { authenticateSession } from 'ember-simple-auth/test-support'; + +module( + 'Unit | Controller | scopes/scope/workers/worker/create-tags', + function (hooks) { + setupTest(hooks); + setupMirage(hooks); + + let store; + let controller; + + const instances = { + scopes: { + global: null, + }, + worker: null, + }; + + const urls = { + workers: null, + tags: null, + createTags: null, + }; + + hooks.beforeEach(function () { + authenticateSession({}); + store = this.owner.lookup('service:store'); + controller = this.owner.lookup( + 'controller:scopes/scope/workers/worker/create-tags', + ); + + instances.scopes.global = this.server.create('scope', { id: 'global' }); + instances.worker = this.server.create('worker', { + scope: instances.scopes.global, + }); + + urls.workers = `/scopes/global/workers`; + urls.tags = `${urls.workers}/${instances.worker.id}/tags`; + urls.createTags = `${urls.workers}/${instances.worker.id}/create-tags`; + }); + + test('it exists', function (assert) { + assert.ok(controller); + }); + + test('save action sets api tags for a worker', async function (assert) { + await visit(urls.createTags); + const worker = await store.findRecord('worker', instances.worker.id); + const apiTags = { key: ['value'] }; + + await controller.save(apiTags); + + assert.deepEqual(worker.api_tags, apiTags); + }); + + test('cancel action clears api tags for a worker and transitions to tags route', async function (assert) { + await visit(urls.createTags); + controller.apiTags = [{ key: 'key', value: 'value' }]; + + await controller.cancel(); + + assert.deepEqual(controller.apiTags, []); + assert.strictEqual(currentURL(), urls.tags); + }); + }, +); diff --git a/ui/admin/tests/unit/routes/scopes/scope/workers/worker/create-tags-test.js b/ui/admin/tests/unit/routes/scopes/scope/workers/worker/create-tags-test.js new file mode 100644 index 000000000..28d690d6b --- /dev/null +++ b/ui/admin/tests/unit/routes/scopes/scope/workers/worker/create-tags-test.js @@ -0,0 +1,21 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module( + 'Unit | Route | scopes/scope/workers/worker/create-tags', + function (hooks) { + setupTest(hooks); + + test('it exists', function (assert) { + let route = this.owner.lookup( + 'route:scopes/scope/workers/worker/create-tags', + ); + assert.ok(route); + }); + }, +);
{{t 'resources.worker.tags.create.description'}}