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:
1 parent
87f84e0c24
commit
4e57c863ab
27 files changed
+741
-32
No files matched your search
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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 },
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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));
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
},
|
||||
);
|
||||
Reference in new issue
Block a user