Icu 13895 create create new tags route and view (#2368)

* feat: 🎸 create canSetWorkerTags ability for workers

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-13895

* feat: 🎸 add create-tags route

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-13895

* feat: 🎸 update worker serializer for adding api_tags

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-13895

* feat: 🎸 add setApiTags method on worker model

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-13895

* style: 💄 update list-wrapper styles after hds upgrade

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-13895

* feat: 🎸 add create-tags component for workers

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-13895

* feat: 🎸 add creat-tags route and view

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-13895

* test: 💍 add acceptance tests for worker create-tags workflow

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-13895

* test: 💍 add test for worker tag creation

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-13895

* test: 💍 fix test setup for workers

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-13895

* test: 💍 add license headers and update setupTest import

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-13895

* test: 💍 fix normal serialize test for worker

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-13895

* refactor: 💡 create worker PKI type const

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-13895

* refactor: 💡 add comments to methods and actions

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-13895

* feat: 🎸 create Tag class to use in multiple places

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-13895

* refactor: 💡 move apiTags to route controller

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-13895

* refactor: 💡 move routeWillChange logic to route

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-13895

* refactor: 💡 routeWillChange logic updates

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-13895

* refactor: 💡 remove unused services

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-13895

* test: 💍 add missing controller tests

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-13895

* refactor: 💡 small update to API tags description

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-13895

* refactor: 💡 add tracked to Tag class

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-13895

* refactor: 💡 use willTransition vs routeWillChange event

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-13895

* refactor: 💡 use controllerFor in route

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-13895
This commit is contained in:
Cameron Perera authored and cameronperera committed 2024-07-16 15:48:23 -05:00
1 parent 87f84e0c24
commit 4e57c863ab
27 files changed
+741 -32

No files matched your search

+4
View File
@@ -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');
}
}
+14 -2
View File
@@ -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 },
});
}
}
+22 -1
View File
@@ -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,
};
}
}
+21 -1
View File
@@ -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(
+1 -3
View File
@@ -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 {
@@ -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(),
@@ -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));
});
});
+34 -1
View File
@@ -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', {
@@ -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'] },
});
});
});
@@ -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}
@@ -0,0 +1,36 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: BUSL-1.1
}}
<Rose::Form
@onSubmit={{this.save}}
@cancel={{@cancel}}
@disabled={{@model.isSaving}}
@showEditToggle={{false}}
as |form|
>
<Form::Field::ListWrapper @layout='vertical' @disabled={{form.disabled}}>
<:field as |F|>
<F.KeyValue
@name='api_tags'
@options={{@apiTags}}
@disabled={{form.disabled}}
@addOption={{this.addApiTag}}
@removeOptionByIndex={{this.removeApiTagByIndex}}
>
<:key as |K|>
<K.text />
</:key>
<:value as |V|>
<V.text />
</:value>
</F.KeyValue>
</:field>
</Form::Field::ListWrapper>
<form.actions
@submitText={{t 'actions.save'}}
@cancelText={{t 'actions.cancel'}}
/>
</Rose::Form>
@@ -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);
}
}
@@ -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
@@ -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;
}
}
@@ -5,6 +5,12 @@
<Hds::Dropdown data-test-manage-worker-dropdown as |dd|>
<dd.ToggleButton @text={{t 'actions.manage'}} @color='secondary' />
{{#if (can 'setWorkerTags worker' @model)}}
<dd.Interactive
@text={{t 'resources.worker.actions.create_tags'}}
@route='scopes.scope.workers.worker.create-tags'
/>
{{/if}}
{{#if (can 'delete worker' @model)}}
<dd.Interactive
@icon='trash'
@@ -0,0 +1,47 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import Controller from '@ember/controller';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import { loading } from 'ember-loading';
import { notifySuccess, notifyError } from 'core/decorators/notify';
import { TrackedArray } from 'tracked-built-ins';
export default class ScopesScopeWorkersWorkerCreateTagsController extends Controller {
// =services
@service router;
// =attributes
apiTags = new TrackedArray([]);
// =actions
/**
* Add api tags for a worker.
* @param {object} worker
*/
@action
@loading
@notifyError(({ message }) => 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');
}
}
+1
View File
@@ -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 () {
@@ -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
}
}
}
}
+6
View File
@@ -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 {
@@ -0,0 +1,34 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: BUSL-1.1
}}
{{page-title (t 'resources.worker.actions.create_tags')}}
<Breadcrumbs::Item
@text={{t 'resources.worker.actions.create_tags'}}
@route='scopes.scope.workers.worker.create-tags'
/>
<Rose::Layout::Page as |page|>
<page.breadcrumbs>
<Breadcrumbs::Container />
</page.breadcrumbs>
<page.header>
<h2>
{{t 'resources.worker.tags.create.title'}}
{{! TODO: Add doclink }}
</h2>
<p>{{t 'resources.worker.tags.create.description'}}</p>
</page.header>
<page.body>
<Form::Worker::CreateTags
@model={{@model}}
@apiTags={{this.apiTags}}
@submit={{this.save}}
@cancel={{this.cancel}}
/>
</page.body>
</Rose::Layout::Page>
@@ -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);
});
});
@@ -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`;
@@ -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`<Form::Worker::CreateTags @model={{this.model}} @apiTags={{this.apiTags}} @submit={{this.save}} @cancel={{this.cancel}} />`,
);
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`<Form::Worker::CreateTags @model={{this.model}} @apiTags={{this.apiTags}} @submit={{this.save}} @cancel={{this.cancel}} />`,
);
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`<Form::Worker::CreateTags @model={{this.model}} @apiTags={{this.apiTags}} @submit={{this.save}} @cancel={{this.cancel}} />`,
);
await click(CANCEL_BUTTON_SELECTOR);
assert.true(this.called);
});
},
);
@@ -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');
});
},
);
@@ -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');
});
});
@@ -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);
});
},
);
@@ -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);
});
},
);