diff --git a/addons/api/addon/models/target.js b/addons/api/addon/models/target.js index 0dad6ba7a..99e1f3d16 100644 --- a/addons/api/addon/models/target.js +++ b/addons/api/addon/models/target.js @@ -30,16 +30,42 @@ export default class TargetModel extends GeneratedTargetModel { // =methods /** - * Saves the `host_set_ids` array on the via the `set-hosts` method. + * Adds host sets via the `add-host-sets` method. * See serializer and adapter for more information. + * @param {[string]} hostSetIDs * @param {object} options * @param {object} options.adapterOptions * @return {Promise} */ - saveHostSets(options={ adapterOptions: {} }) { + addHostSets(hostSetIDs, options={ adapterOptions: {} }) { const defaultAdapterOptions = { - method: 'set-host-sets', - serializeHostSets: true + method: 'add-host-sets', + hostSetIDs + }; + // There is no "deep merge" in ES. + // All of this nonsense is here to ensure we get + // a decent merge of `adapterOptions`. + return this.save({ + ...options, + adapterOptions: { + ...defaultAdapterOptions, + ...options.adapterOptions + } + }); + } + + /** + * Delete host sets via the `delete-host-sets` method. + * See serializer and adapter for more information. + * @param {[string]} hostSetIDs + * @param {object} options + * @param {object} options.adapterOptions + * @return {Promise} + */ + deleteHostSets(hostSetIDs, options={ adapterOptions: {} }) { + const defaultAdapterOptions = { + method: 'delete-host-sets', + hostSetIDs }; // There is no "deep merge" in ES. // All of this nonsense is here to ensure we get diff --git a/addons/api/addon/serializers/target.js b/addons/api/addon/serializers/target.js index fefaf0fcb..eae3ad5b1 100644 --- a/addons/api/addon/serializers/target.js +++ b/addons/api/addon/serializers/target.js @@ -5,30 +5,32 @@ export default class TargetSerializer extends ApplicationSerializer { // =methods /** - * If `adapterOptions.serializeHostSets` is true, the serialization should - * include **only host sets** and the version. Normally, host sets are not - * serialized. + * If `adapterOptions.hostSets` is set to an array of host set models, + * the resulting target serialization should include **only host sets** + * and the version. Normally, host sets are not serialized. * @override * @param {Snapshot} snapshot * @return {object} */ serialize(snapshot) { - const serializeHostSets = snapshot?.adapterOptions?.serializeHostSets; let serialized = super.serialize(...arguments); - if (serializeHostSets) serialized = this.serializeWithHostSets(snapshot); + const hostSetIDs = snapshot?.adapterOptions?.hostSetIDs; + if (hostSetIDs) serialized = + this.serializeWithHostSets(snapshot, hostSetIDs); return serialized; } /** - * Returns a payload containing only the host_set_ids array. + * Returns a payload containing only the host_set_ids array using IDs + * passed into the function (rather than existing host sets on the model). * @param {Snapshot} snapshot + * @param {[string]} hostSetIDs * @return {object} */ - serializeWithHostSets(snapshot) { + serializeWithHostSets(snapshot, hostSetIDs) { return { version: snapshot.attr('version'), - host_set_ids: - snapshot.attr('host_sets').map(hostSet => hostSet.attr('host_set_id')) + host_set_ids: hostSetIDs }; } diff --git a/addons/api/tests/unit/models/target-test.js b/addons/api/tests/unit/models/target-test.js index 04baf1bab..7d3aab836 100644 --- a/addons/api/tests/unit/models/target-test.js +++ b/addons/api/tests/unit/models/target-test.js @@ -57,9 +57,42 @@ module('Unit | Model | target', function (hooks) { assert.ok(target.hostSets[0].hostCatalog, 'Host catalog is resolved'); }); - test('it has a `saveHostSets` method that targets a specific POST API endpoint and serialization', async function (assert) { + test('it has an `addHostSets` method that targets a specific POST API endpoint and serialization', async function (assert) { assert.expect(1); - this.server.post('/v1/scopes/o_1/targets/123abc:set-host-sets', (schema, request) => { + this.server.post('/v1/scopes/o_1/targets/123abc:add-host-sets', (schema, request) => { + const body = JSON.parse(request.requestBody); + assert.deepEqual(body, { + host_set_ids: ['123_abc', 'foobar'], + version: 1 + }); + }); + const store = this.owner.lookup('service:store'); + store.push({ + data: { + id: '123abc', + type: 'target', + attributes: { + name: 'Target', + description: 'Description', + host_sets: [ + { host_set_id: '1', host_catalog_id: '2' }, + { host_set_id: '3', host_catalog_id: '4' } + ], + version: 1, + scope: { + scope_id: 'o_1', + type: 'scope' + } + } + } + }); + const model = store.peekRecord('target', '123abc'); + await model.addHostSets(['123_abc', 'foobar']); + }); + + test('it has a `deleteHostSets` method that targets a specific POST API endpoint and serialization', async function (assert) { + assert.expect(1); + this.server.post('/v1/scopes/o_1/targets/123abc:delete-host-sets', (schema, request) => { const body = JSON.parse(request.requestBody); assert.deepEqual(body, { host_set_ids: ['1', '3'], @@ -87,6 +120,6 @@ module('Unit | Model | target', function (hooks) { } }); const model = store.peekRecord('target', '123abc'); - await model.saveHostSets(); + await model.deleteHostSets(['1', '3']); }); }); diff --git a/addons/api/tests/unit/serializers/target-test.js b/addons/api/tests/unit/serializers/target-test.js index a3ca9bd69..e37eb1de2 100644 --- a/addons/api/tests/unit/serializers/target-test.js +++ b/addons/api/tests/unit/serializers/target-test.js @@ -29,7 +29,7 @@ module('Unit | Serializer | target', function(hooks) { }); - test('it serializes only host sets when `adapterOptions.serializeHostSets` is true', function (assert) { + test('it serializes only host sets and version when an `adapterOptions.hostSetIDs` array is passed', function (assert) { assert.expect(1); const store = this.owner.lookup('service:store'); const serializer = store.serializerFor('target'); @@ -44,11 +44,11 @@ module('Unit | Serializer | target', function(hooks) { }); const snapshot = record._createSnapshot(); snapshot.adapterOptions = { - serializeHostSets: true + hostSetIDs: ['4', '5'] }; const serializedRecord = serializer.serialize(snapshot); assert.deepEqual(serializedRecord, { - host_set_ids: ['1', '3'], + host_set_ids: ['4', '5'], version: 1 }); }); diff --git a/ui/core/app/controllers/scopes/scope/projects/project/targets/target/host-sets.js b/ui/core/app/controllers/scopes/scope/projects/project/targets/target/host-sets.js new file mode 100644 index 000000000..a8fb5fd35 --- /dev/null +++ b/ui/core/app/controllers/scopes/scope/projects/project/targets/target/host-sets.js @@ -0,0 +1,19 @@ +import Controller from '@ember/controller'; +import { inject as service } from '@ember/service'; + +export default class ScopesScopeProjectsProjectTargetsTargetHostSetsController extends Controller { + + // =services + + @service intl; + + // =attributes + + /** + * Host sets breadcrumb + * @type {string} + */ + get breadCrumb() { + return this.intl.t('resources.host-sets'); + } +} diff --git a/ui/core/app/router.js b/ui/core/app/router.js index 13e46eb06..e5577e83a 100644 --- a/ui/core/app/router.js +++ b/ui/core/app/router.js @@ -36,7 +36,11 @@ Router.map(function () { }); }); this.route('targets', function() { - this.route('target', { path: ':target_id' }, function() {}); + this.route('target', { path: ':target_id' }, function() { + this.route('host-sets', function() { + this.route('host-set', { path: ':host_set_id'}, function() {}); + }); + }); this.route('new'); }); }); diff --git a/ui/core/app/routes/scopes/scope/projects/project/targets/target/host-sets.js b/ui/core/app/routes/scopes/scope/projects/project/targets/target/host-sets.js new file mode 100644 index 000000000..34418acea --- /dev/null +++ b/ui/core/app/routes/scopes/scope/projects/project/targets/target/host-sets.js @@ -0,0 +1,43 @@ +import Route from '@ember/routing/route'; +import { all, hash } from 'rsvp'; + +export default class ScopesScopeProjectsProjectTargetsTargetHostSetsRoute extends Route { + // =methods + + /** + * Loads all host-sets and associated host-catalogs under the current target + * and it's parent scope. + * @return {Promise{[HostSetModel, HostCatalogModel]}} + */ + beforeModel() { + const { scopeID, host_sets } = + this.modelFor('scopes.scope.projects.project.targets.target'); + const promises = host_sets + .map(({ host_set_id, host_catalog_id: hostCatalogID }) => + hash({ + // TODO: multiple host sets may belong to the same catalog, + // resulting in the catalog being loaded multiple times. + // An improvement would be to find the unique set of catalogs first. + hostCatalog: this.store.findRecord( + 'host-catalog', + hostCatalogID, + { adapterOptions: { scopeID } } + ), + hostSet: this.store.findRecord( + 'host-set', + host_set_id, + { adapterOptions: { scopeID, hostCatalogID } } + ) + }) + ); + return all(promises); + } + + /** + * Returns the previously loaded target instance. + * @return {TargetModel} + */ + model() { + return this.modelFor('scopes.scope.projects.project.targets.target'); + } +} diff --git a/ui/core/app/routes/scopes/scope/projects/project/targets/target/host-sets/host-set.js b/ui/core/app/routes/scopes/scope/projects/project/targets/target/host-sets/host-set.js new file mode 100644 index 000000000..2b45938a3 --- /dev/null +++ b/ui/core/app/routes/scopes/scope/projects/project/targets/target/host-sets/host-set.js @@ -0,0 +1,4 @@ +import Route from '@ember/routing/route'; + +export default class ScopesScopeProjectsProjectTargetsTargetHostSetsHostSetRoute extends Route { +} diff --git a/ui/core/app/routes/scopes/scope/projects/project/targets/target/host-sets/index.js b/ui/core/app/routes/scopes/scope/projects/project/targets/target/host-sets/index.js new file mode 100644 index 000000000..6e14d67c1 --- /dev/null +++ b/ui/core/app/routes/scopes/scope/projects/project/targets/target/host-sets/index.js @@ -0,0 +1,4 @@ +import Route from '@ember/routing/route'; + +export default class ScopesScopeProjectsProjectTargetsTargetHostSetsIndexRoute extends Route { +} diff --git a/ui/core/app/templates/scopes/scope/projects/project/targets/target.hbs b/ui/core/app/templates/scopes/scope/projects/project/targets/target.hbs index e2147cab0..50b97cd47 100644 --- a/ui/core/app/templates/scopes/scope/projects/project/targets/target.hbs +++ b/ui/core/app/templates/scopes/scope/projects/project/targets/target.hbs @@ -1 +1,38 @@ -{{outlet}} \ No newline at end of file + + + + + + + + {{t "resources.target"}} + + + + + + {{t 'titles.details'}} + + + {{t 'titles.host-sets'}} + + + + + + + + {{t 'actions.delete-target'}} + + + + + + {{outlet}} + + + diff --git a/ui/core/app/templates/scopes/scope/projects/project/targets/target/host-sets.hbs b/ui/core/app/templates/scopes/scope/projects/project/targets/target/host-sets.hbs new file mode 100644 index 000000000..3effb2b0d --- /dev/null +++ b/ui/core/app/templates/scopes/scope/projects/project/targets/target/host-sets.hbs @@ -0,0 +1,41 @@ +{{title (t "resources.host-sets")}} + +{{#if @model.hostSets}} + + + + + {{t "form.label.name"}} + {{t "form.label.type"}} + {{t "form.label.id"}} + + + + {{#each @model.hostSets as |hostSet|}} + + + {{hostSet.model.displayName}} + {{hostSet.model.description}} + + + {{hostSet.model.type}} + + + {{hostSet.model.id}} + + + {{/each}} + + + +{{else}} + + + + + {{t "descriptions.target-host-sets-welcome"}} + + + + +{{/if}} diff --git a/ui/core/app/templates/scopes/scope/projects/project/targets/target/host-sets/host-set.hbs b/ui/core/app/templates/scopes/scope/projects/project/targets/target/host-sets/host-set.hbs new file mode 100644 index 000000000..e2147cab0 --- /dev/null +++ b/ui/core/app/templates/scopes/scope/projects/project/targets/target/host-sets/host-set.hbs @@ -0,0 +1 @@ +{{outlet}} \ No newline at end of file diff --git a/ui/core/app/templates/scopes/scope/projects/project/targets/target/index.hbs b/ui/core/app/templates/scopes/scope/projects/project/targets/target/index.hbs index da4860986..5c39ce009 100644 --- a/ui/core/app/templates/scopes/scope/projects/project/targets/target/index.hbs +++ b/ui/core/app/templates/scopes/scope/projects/project/targets/target/index.hbs @@ -1,39 +1,5 @@ - - - - - - - - {{t "resources.target"}} - - - - - - {{t 'titles.details'}} - - - - - - - - {{t 'actions.delete-target'}} - - - - - - - - - + \ No newline at end of file diff --git a/ui/core/mirage/config.js b/ui/core/mirage/config.js index 56ebd68d0..f4bebd6cd 100644 --- a/ui/core/mirage/config.js +++ b/ui/core/mirage/config.js @@ -130,6 +130,31 @@ export default function() { this.get('/scopes/:scope_id/targets/:id'); this.patch('/scopes/:scope_id/targets/:id'); this.del('/scopes/:scope_id/targets/:id'); + this.post('/scopes/:scope_id/targets/:idMethod', function ({ targets }, { params: { idMethod } }) { + const attrs = this.normalizedRequestAttrs(); + const id = idMethod.split(':')[0]; + const method = idMethod.split(':')[1]; + const target = targets.find(id); + const updatedAttrs = { + version: attrs.version, + hostSetIds: target.hostSetIds + }; + // If adding host sets, push them into the array + if (method === 'add-host-sets') { + attrs.hostSetIds.forEach(id => { + if (!updatedAttrs.hostSetIds.includes(id)) { + updatedAttrs.hostSetIds.push(id); + } + }); + } + // If deleting host sets, filter them out of the array + if (method === 'delete-host-sets') { + updatedAttrs.hostSetIds = updatedAttrs.hostSetIds.filter(id => { + return !attrs.hostSetIds.includes(id); + }); + } + return target.update(updatedAttrs); + }); // Uncomment the following line and the Response import above // Then change the response code to simulate error responses. diff --git a/ui/core/mirage/factories/target.js b/ui/core/mirage/factories/target.js index b664ff88a..7c74806a6 100644 --- a/ui/core/mirage/factories/target.js +++ b/ui/core/mirage/factories/target.js @@ -1,3 +1,22 @@ import factory from '../generated/factories/target'; +import { trait } from 'ember-cli-mirage'; -export default factory.extend(); +const randomBoolean = (chance = 0.5) => Math.random() < chance; +const hostSetChance = 0.3; + +export default factory.extend({ + + /** + * Randomly selects existing host sets to assign to target. + */ + withRandomHostSets: trait({ + afterCreate(target, server) { + let randomlySelectedHostSets; + randomlySelectedHostSets = server.schema.hostSets + .where(item => item.scope.id === target.scope.id).models + .filter(() => randomBoolean(hostSetChance)); + target.update({hostSets: randomlySelectedHostSets}); + } + }) + +}); diff --git a/ui/core/mirage/generated/factories/target.js b/ui/core/mirage/generated/factories/target.js index 134cc7c02..2fca0871d 100644 --- a/ui/core/mirage/generated/factories/target.js +++ b/ui/core/mirage/generated/factories/target.js @@ -6,7 +6,7 @@ import { random, date } from 'faker'; */ export default Factory.extend({ - protocol: 'SSH', + protocol: 'tcp', name: () => random.words(), description: () => random.words(), created_time: () => date.recent(), diff --git a/ui/core/mirage/models/host-set.js b/ui/core/mirage/models/host-set.js index 2f78f5510..3aa8fbf7e 100644 --- a/ui/core/mirage/models/host-set.js +++ b/ui/core/mirage/models/host-set.js @@ -2,6 +2,7 @@ import { Model, belongsTo, hasMany } from 'ember-cli-mirage'; export default Model.extend({ + target: belongsTo(), hostCatalog: belongsTo(), hosts: hasMany() diff --git a/ui/core/mirage/models/target.js b/ui/core/mirage/models/target.js new file mode 100644 index 000000000..d33295663 --- /dev/null +++ b/ui/core/mirage/models/target.js @@ -0,0 +1,7 @@ +import { Model, hasMany } from 'ember-cli-mirage'; + +export default Model.extend({ + + hostSets: hasMany() + +}); diff --git a/ui/core/mirage/scenarios/default.js b/ui/core/mirage/scenarios/default.js index 439b9fd01..2b212f999 100644 --- a/ui/core/mirage/scenarios/default.js +++ b/ui/core/mirage/scenarios/default.js @@ -33,7 +33,7 @@ export default function(server) { server.db.scopes.where({type: 'project'}).forEach(({ id, type }) => { const scope = { id, type }; server.createList('host-catalog', 2, { scope }, 'withChildren'); - server.createList('target', 2, { scope }); + server.createList('target', 2, { scope }, 'withRandomHostSets'); }); - + } diff --git a/ui/core/mirage/serializers/target.js b/ui/core/mirage/serializers/target.js index 3d4bc5f3e..c9c6935a0 100644 --- a/ui/core/mirage/serializers/target.js +++ b/ui/core/mirage/serializers/target.js @@ -1,5 +1,24 @@ import ApplicationSerializer from './application'; +import { underscore } from '@ember/string'; export default ApplicationSerializer.extend({ - modelName: 'target' + modelName: 'target', + + keyForRelationshipIds(relationshipName) { + return `${this._container.inflector.singularize( + underscore(relationshipName) + )}_ids`; + }, + + _hashForModel(model) { + const json = ApplicationSerializer.prototype._hashForModel.apply(this, arguments); + const { hostSets } = this.schema; + json.host_sets = model.hostSetIds.map(host_set_id => { + const hostSet = hostSets.find(host_set_id); + const host_catalog_id = hostSet?.hostCatalog?.id; + return { host_set_id, host_catalog_id }; + }); + return json; + } + }); diff --git a/ui/core/tests/acceptance/targets-test.js b/ui/core/tests/acceptance/targets-test.js index 8ff26fa7f..01f73d443 100644 --- a/ui/core/tests/acceptance/targets-test.js +++ b/ui/core/tests/acceptance/targets-test.js @@ -58,4 +58,8 @@ module('Acceptance | targets', function (hooks) { test('saving an existing target with invalid fields displays error messages', async function (assert) { assert.expect(0); }); + + test('viewing target host sets', async function (assert) { + assert.expect(0); + }); }); diff --git a/ui/core/tests/unit/controllers/scopes/scope/projects/project/targets/target/host-sets-test.js b/ui/core/tests/unit/controllers/scopes/scope/projects/project/targets/target/host-sets-test.js new file mode 100644 index 000000000..796ce6793 --- /dev/null +++ b/ui/core/tests/unit/controllers/scopes/scope/projects/project/targets/target/host-sets-test.js @@ -0,0 +1,12 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module('Unit | Controller | scopes/scope/projects/project/targets/target/host-sets', function(hooks) { + setupTest(hooks); + + // Replace this with your real tests. + test('it exists', function(assert) { + let controller = this.owner.lookup('controller:scopes/scope/projects/project/targets/target/host-sets'); + assert.ok(controller); + }); +}); diff --git a/ui/core/tests/unit/routes/scopes/scope/projects/project/targets/target/host-sets-test.js b/ui/core/tests/unit/routes/scopes/scope/projects/project/targets/target/host-sets-test.js new file mode 100644 index 000000000..41d4cf0e3 --- /dev/null +++ b/ui/core/tests/unit/routes/scopes/scope/projects/project/targets/target/host-sets-test.js @@ -0,0 +1,11 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module('Unit | Route | scopes/scope/projects/project/targets/target/host-sets', function(hooks) { + setupTest(hooks); + + test('it exists', function(assert) { + let route = this.owner.lookup('route:scopes/scope/projects/project/targets/target/host-sets'); + assert.ok(route); + }); +}); diff --git a/ui/core/tests/unit/routes/scopes/scope/projects/project/targets/target/host-sets/host-set-test.js b/ui/core/tests/unit/routes/scopes/scope/projects/project/targets/target/host-sets/host-set-test.js new file mode 100644 index 000000000..fb7e2480e --- /dev/null +++ b/ui/core/tests/unit/routes/scopes/scope/projects/project/targets/target/host-sets/host-set-test.js @@ -0,0 +1,11 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module('Unit | Route | scopes/scope/projects/project/targets/target/host-sets/host-set', function(hooks) { + setupTest(hooks); + + test('it exists', function(assert) { + let route = this.owner.lookup('route:scopes/scope/projects/project/targets/target/host-sets/host-set'); + assert.ok(route); + }); +}); diff --git a/ui/core/translations/en-us.yaml b/ui/core/translations/en-us.yaml index 69b41f0cc..8e2ffd179 100644 --- a/ui/core/translations/en-us.yaml +++ b/ui/core/translations/en-us.yaml @@ -36,7 +36,7 @@ host-set: static: Static target: protocols: - SSH: SSH + tcp: TCP host: types: static: Static @@ -79,6 +79,7 @@ titles: groups-welcome: Welcome to Groups auth-methods-welcome: Welcome to Auth Methods targets-welcome: Create your first Target + target-host-sets-welcome: No Host Sets Associated grants: Permissions new-grant: New Grant principals: Principals @@ -99,6 +100,7 @@ descriptions: groups-welcome: Groups are collections of users. auth-methods-welcome: Auth methods are configurations to authenticate users. targets-welcome: Targets provide Users the means to connect to and interact networked services. A Target contains a collection of Hosts and a collection of Credentials. + target-host-sets-welcome: Add Host Sets to this target to provide access. no-host-set-hosts: Before associating hosts with this host set, create hosts within the host catalog. states: info: Info
{{hostSet.model.description}}
{{hostSet.model.id}}