Icu 13897 create remove tag modal (#2377)
* feat: 🎸 add action dropdown in tags table ✅ Closes: https://hashicorp.atlassian.net/browse/ICU-13897 * feat: 🎸 add removeApiTags method to worker ✅ Closes: https://hashicorp.atlassian.net/browse/ICU-13897 * feat: 🎸 add tag removal workflow ✅ Closes: https://hashicorp.atlassian.net/browse/ICU-13897 * test: 💍 add tests for removing a worker tag ✅ Closes: https://hashicorp.atlassian.net/browse/ICU-13897 * test: 💍 add tests for removing a tag ✅ Closes: https://hashicorp.atlassian.net/browse/ICU-13897 * refactor: 💡 fix spelling issue ✅ Closes: https://hashicorp.atlassian.net/browse/ICU-13897 * refactor: 💡 use mirage worker serializer to remove empty tag ✅ Closes: https://hashicorp.atlassian.net/browse/ICU-13897 * refactor: 💡 update modal id to class ✅ Closes: https://hashicorp.atlassian.net/browse/ICU-13897 * refactor: 💡 add autofocus to remove tag modal ✅ Closes: https://hashicorp.atlassian.net/browse/ICU-13897 * feat: 🎸 update tags controller attributes ✅ Closes: https://hashicorp.atlassian.net/browse/ICU-13897 * refactor: 💡 create normalizer to add empty object for api_tags ✅ Closes: https://hashicorp.atlassian.net/browse/ICU-13897 * refactor: 💡 update getter signature ✅ Closes: https://hashicorp.atlassian.net/browse/ICU-13897 * refactor: 💡 remove unneeded parens ✅ Closes: https://hashicorp.atlassian.net/browse/ICU-13897 * refactor: 💡 remove type from Hds::Button ✅ Closes: https://hashicorp.atlassian.net/browse/ICU-13897 * refactor: 💡 update confirmation modal class name ✅ Closes: https://hashicorp.atlassian.net/browse/ICU-13897 * feat: 🎸 add emptyObjectIfMissing model attribute ✅ Closes: https://hashicorp.atlassian.net/browse/ICU-13897 * refactor: 💡 PR feedback ✅ Closes: https://hashicorp.atlassian.net/browse/ICU-13897 * refactor: 💡 remove default to null in toggleModal ✅ Closes: https://hashicorp.atlassian.net/browse/ICU-13897
This commit is contained in:
1 parent
723101b779
commit
f9af4f0092
16 files changed
+443
-12
No files matched your search
@@ -18,7 +18,17 @@ export default class WorkerAbility extends ModelAbility {
|
||||
return this.hasAuthorizedCollectionAction('create:worker-led');
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {boolean}
|
||||
*/
|
||||
get canSetWorkerTags() {
|
||||
return this.hasAuthorizedAction('set-worker-tags');
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {boolean}
|
||||
*/
|
||||
get canRemoveTags() {
|
||||
return this.hasAuthorizedAction('remove-worker-tags');
|
||||
}
|
||||
}
|
||||
@@ -86,6 +86,7 @@ export default class WorkerModel extends GeneratedWorkerModel {
|
||||
'The deduplicated union of the tags reported by the worker ' +
|
||||
'from its configuration and any tags added through other means.\nOutput only.',
|
||||
readOnly: true,
|
||||
emptyObjectIfMissing: true,
|
||||
})
|
||||
canonical_tags;
|
||||
|
||||
@@ -93,11 +94,13 @@ export default class WorkerModel extends GeneratedWorkerModel {
|
||||
description:
|
||||
"The tags set in the worker's configuration file.\nOutput only.",
|
||||
readOnly: true,
|
||||
emptyObjectIfMissing: true,
|
||||
})
|
||||
config_tags;
|
||||
|
||||
@attr({
|
||||
description: 'The api tags set for the worker.\nOutput only.',
|
||||
emptyObjectIfMissing: true,
|
||||
})
|
||||
api_tags;
|
||||
|
||||
@@ -132,4 +135,15 @@ export default class WorkerModel extends GeneratedWorkerModel {
|
||||
adapterOptions: { method: 'set-worker-tags', apiTags },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to remove api tags on the worker.
|
||||
* @param {object} apiTags
|
||||
* @return {Promise}
|
||||
*/
|
||||
removeApiTags(apiTags) {
|
||||
return this.save({
|
||||
adapterOptions: { method: 'remove-worker-tags', apiTags },
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -179,6 +179,8 @@ export default class ApplicationSerializer extends RESTSerializer {
|
||||
payload = structuredClone(payload);
|
||||
// Check for and normalize missing arrays
|
||||
payload = this.normalizeMissingArrays(store, primaryModelClass, payload);
|
||||
// Check for and normalize missing objects
|
||||
payload = this.normalizeMissingObjects(store, primaryModelClass, payload);
|
||||
// Setup a new payload data structure.
|
||||
const transformedPayload = {};
|
||||
// Find the Ember-data-expected root key name.
|
||||
@@ -199,7 +201,7 @@ export default class ApplicationSerializer extends RESTSerializer {
|
||||
|
||||
/**
|
||||
* Resets missing array fields to an empty array if they are annotated by
|
||||
* `normalizeToEmptyArray: true` in the associated model attribute
|
||||
* `emptyArrayIfMissing: true` in the associated model attribute
|
||||
* declaration. Our API excludes arrays when they are empty from
|
||||
* singular responses.
|
||||
*
|
||||
@@ -222,6 +224,30 @@ export default class ApplicationSerializer extends RESTSerializer {
|
||||
return payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets missing object fields to an empty object if they are annotated by
|
||||
* `emptyObjectIfMissing: true` in the associated model attribute
|
||||
* declaration.
|
||||
*
|
||||
* @param {Store} store
|
||||
* @param {Model} primaryModelClass
|
||||
* @param {object} payload
|
||||
* @return {object}
|
||||
*/
|
||||
normalizeMissingObjects(store, primaryModelClass, payload) {
|
||||
const attrDefs = store
|
||||
.getSchemaDefinitionService()
|
||||
.attributesDefinitionFor({ type: primaryModelClass.modelName });
|
||||
if (attrDefs) {
|
||||
Object.keys(attrDefs).forEach((key) => {
|
||||
if (!payload[key] && attrDefs[key]?.options?.emptyObjectIfMissing) {
|
||||
payload[key] = {};
|
||||
}
|
||||
});
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hydrates the incoming scope JSON with additional boolean fields and
|
||||
* delegates nested attributes and secrets normalization.
|
||||
|
||||
@@ -689,13 +689,23 @@ function routes() {
|
||||
const id = idMethod.split(':')[0];
|
||||
const method = idMethod.split(':')[1];
|
||||
const worker = workers.find(id);
|
||||
let updatedAttrs = {};
|
||||
let updatedAttrs = {
|
||||
version: attrs.version,
|
||||
api_tags: worker.api_tags,
|
||||
};
|
||||
|
||||
if (method === 'remove-worker-tags') {
|
||||
const key = Object.keys(attrs.apiTags)[0];
|
||||
const value = attrs.apiTags[key][0];
|
||||
updatedAttrs.api_tags[key] = updatedAttrs.api_tags[key].filter(
|
||||
(tag) => {
|
||||
return tag !== value;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (method === 'set-worker-tags') {
|
||||
updatedAttrs = {
|
||||
version: attrs.version,
|
||||
apiTags: attrs.apiTags,
|
||||
};
|
||||
updatedAttrs.api_tags = attrs.apiTags;
|
||||
}
|
||||
|
||||
return worker.update(updatedAttrs);
|
||||
|
||||
@@ -17,6 +17,7 @@ export default factory.extend({
|
||||
'update',
|
||||
'delete',
|
||||
'set-worker-tags',
|
||||
'remove-worker-tags',
|
||||
],
|
||||
config_tags: (i) => {
|
||||
if (i % 3 === 0) {
|
||||
|
||||
@@ -7,4 +7,21 @@ import ApplicationSerializer from './application';
|
||||
|
||||
export default ApplicationSerializer.extend({
|
||||
modelName: 'worker',
|
||||
|
||||
_hashForModel(model) {
|
||||
const json = ApplicationSerializer.prototype._hashForModel.apply(
|
||||
this,
|
||||
arguments,
|
||||
);
|
||||
if (json.api_tags) {
|
||||
const keys = Object.keys(model.api_tags);
|
||||
keys.forEach((key) => {
|
||||
if (json.api_tags[key]?.length === 0) {
|
||||
delete json.api_tags[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return json;
|
||||
},
|
||||
});
|
||||
@@ -71,6 +71,38 @@ module('Unit | Model | worker', function (hooks) {
|
||||
const model = store.peekRecord('worker', workerId);
|
||||
await model.setApiTags(tags);
|
||||
});
|
||||
|
||||
test('it has a `removeApiTags` method that targets a specific POST API', async function (assert) {
|
||||
assert.expect(1);
|
||||
const workerId = 'w_123';
|
||||
const tags = {
|
||||
tag1: ['value1'],
|
||||
};
|
||||
this.server.post(
|
||||
`/workers/${workerId}:remove-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,
|
||||
api_tags: tags,
|
||||
},
|
||||
},
|
||||
});
|
||||
const model = store.peekRecord('worker', workerId);
|
||||
|
||||
await model.removeApiTags(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');
|
||||
|
||||
@@ -71,4 +71,45 @@ module('Unit | Serializer | worker', function (hooks) {
|
||||
api_tags: { key: ['value'] },
|
||||
});
|
||||
});
|
||||
|
||||
test('it normalizes missing `api_tags`, `config_tags`, and `canonical_tags`', function (assert) {
|
||||
const store = this.owner.lookup('service:store');
|
||||
const serializer = store.serializerFor('worker');
|
||||
const worker = store.createRecord('worker').constructor;
|
||||
const payload = {
|
||||
id: 'w_123',
|
||||
name: 'hardest worker',
|
||||
scope: { id: 'global' },
|
||||
version: 1,
|
||||
address: 'localhost',
|
||||
};
|
||||
|
||||
const normalized = serializer.normalizeSingleResponse(
|
||||
store,
|
||||
worker,
|
||||
payload,
|
||||
);
|
||||
|
||||
assert.deepEqual(normalized, {
|
||||
data: {
|
||||
attributes: {
|
||||
address: 'localhost',
|
||||
api_tags: {},
|
||||
authorized_actions: [],
|
||||
canonical_tags: {},
|
||||
config_tags: {},
|
||||
name: 'hardest worker',
|
||||
scope: {
|
||||
id: 'global',
|
||||
scope_id: 'global',
|
||||
},
|
||||
version: 1,
|
||||
},
|
||||
id: 'w_123',
|
||||
relationships: {},
|
||||
type: 'worker',
|
||||
},
|
||||
included: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user