diff --git a/addons/api/addon/serializers/target.js b/addons/api/addon/serializers/target.js index d667e3ab9..1fec14cdd 100644 --- a/addons/api/addon/serializers/target.js +++ b/addons/api/addon/serializers/target.js @@ -120,4 +120,17 @@ export default class TargetSerializer extends ApplicationSerializer { storage_bucket_id: snapshot?.attr('storage_bucket_id'), }; } + + normalize(typeClass, hash, ...rest) { + const normalizedHash = structuredClone(hash); + const normalized = super.normalize(typeClass, normalizedHash, ...rest); + // Ember data retains the previous entry in the array attr when the updated attr becomes undefined/empty. + // So, we explicitly set the attr to an empty array if the updated attribute is undefined from the API + + if (!normalized.data.attributes.aliases) { + normalized.data.attributes.aliases = []; + } + + return normalized; + } } diff --git a/addons/api/mirage/serializers/target.js b/addons/api/mirage/serializers/target.js index bdc060de3..2cd3fb798 100644 --- a/addons/api/mirage/serializers/target.js +++ b/addons/api/mirage/serializers/target.js @@ -20,7 +20,7 @@ export default ApplicationSerializer.extend({ this, arguments, ); - const { hostSets } = this.schema; + const { hostSets, aliases } = this.schema; if (model.hostSetIds?.length) { json.host_sources = model.hostSetIds.map((host_set_id) => { const hostSet = hostSets.find(host_set_id); @@ -28,6 +28,23 @@ export default ApplicationSerializer.extend({ return { id: host_set_id, host_catalog_id }; }); } + + // After we clear an associated alias, we try to mimic the BE logic + // by manually filter out the alias id from the target model + + const associatedAliases = aliases + .all() + .models.reduce((arr, { destinationId, id }) => { + // We iterate through the aliases list and + // find the cleared alias by looking for camelCase destinationId since there is no other way to find the impacted alias + if (destinationId !== null) arr.push(id); + return arr; + }, []); + + json.aliases = model.aliases?.filter((alias) => + associatedAliases.includes(alias.id), + ); + return json; }, }); diff --git a/addons/api/tests/unit/serializers/application-test.js b/addons/api/tests/unit/serializers/application-test.js index 57bfd08a7..9b11ceac7 100644 --- a/addons/api/tests/unit/serializers/application-test.js +++ b/addons/api/tests/unit/serializers/application-test.js @@ -285,6 +285,7 @@ module('Unit | Serializer | application', function (hooks) { type: 'target', attributes: { authorized_actions: [], + aliases: [], name: 'Target 1', scope: { id: 'o_123', scope_id: 'o_123' }, host_sources: [], diff --git a/addons/api/tests/unit/serializers/target-test.js b/addons/api/tests/unit/serializers/target-test.js index bc6fa8739..08feb1a8e 100644 --- a/addons/api/tests/unit/serializers/target-test.js +++ b/addons/api/tests/unit/serializers/target-test.js @@ -364,6 +364,7 @@ module('Unit | Serializer | target', function (hooks) { id: '1', type: 'target', attributes: { + aliases: [], authorized_actions: [], name: 'Target 1', host_sources: [ @@ -400,6 +401,7 @@ module('Unit | Serializer | target', function (hooks) { id: '1', type: 'target', attributes: { + aliases: [], brokered_credential_source_ids: [], injected_application_credential_source_ids: [], authorized_actions: [], @@ -432,6 +434,7 @@ module('Unit | Serializer | target', function (hooks) { id: '1', type: 'target', attributes: { + aliases: [], authorized_actions: [], name: 'Target 1', scope: { id: 'o_123', scope_id: 'o_123' }, @@ -464,6 +467,7 @@ module('Unit | Serializer | target', function (hooks) { id: '1', type: 'target', attributes: { + aliases: [], authorized_actions: [], name: 'Target 1', host_sources: [], @@ -496,6 +500,7 @@ module('Unit | Serializer | target', function (hooks) { id: '1', type: 'target', attributes: { + aliases: [], authorized_actions: [], name: 'Target 1', host_sources: [], @@ -528,6 +533,7 @@ module('Unit | Serializer | target', function (hooks) { id: '1', type: 'target', attributes: { + aliases: [], authorized_actions: [], name: 'Target 1', host_sources: [], @@ -561,6 +567,7 @@ module('Unit | Serializer | target', function (hooks) { id: '1', type: 'target', attributes: { + aliases: [], authorized_actions: [], name: 'Target 1', host_sources: [], diff --git a/addons/core/translations/actions/en-us.yaml b/addons/core/translations/actions/en-us.yaml index 7603c6e57..b048d45f6 100644 --- a/addons/core/translations/actions/en-us.yaml +++ b/addons/core/translations/actions/en-us.yaml @@ -44,6 +44,7 @@ toggle-secret: Toggle secret visibility minimize: Minimize fullscreen: Fullscreen clear-all-filters: Clear all filters +clear: Clear clear-filters: Clear filters learn-more: Learn more read-more: Read more diff --git a/addons/core/translations/notifications/en-us.yaml b/addons/core/translations/notifications/en-us.yaml index 90febef09..51702a05d 100644 --- a/addons/core/translations/notifications/en-us.yaml +++ b/addons/core/translations/notifications/en-us.yaml @@ -7,3 +7,4 @@ delete-success: Deleted successfully. add-success: Added successfully. remove-success: Removed successfully. canceled-success: Canceled successfully. +clear-success: Cleared successfully diff --git a/ui/admin/app/controllers/scopes/scope/targets/target/index.js b/ui/admin/app/controllers/scopes/scope/targets/target/index.js index e995ce422..f7adda0d6 100644 --- a/ui/admin/app/controllers/scopes/scope/targets/target/index.js +++ b/ui/admin/app/controllers/scopes/scope/targets/target/index.js @@ -37,6 +37,13 @@ export default class ScopesScopeTargetsTargetIndexController extends Controller return this.model.aliases.length > 3; } + /** + * Decides when to show the flyout component + * @returns {boolean} + */ + get showFlyOutComponent() { + return this.showFlyout && this.model.aliases.length > 3; + } /** * Returns only the first three items in the aliases array if there are more than 3 * @returns {array} diff --git a/ui/admin/app/router.js b/ui/admin/app/router.js index 7effc0c4e..0903dc8ec 100644 --- a/ui/admin/app/router.js +++ b/ui/admin/app/router.js @@ -88,6 +88,8 @@ Router.map(function () { this.route('injected-application-credential-sources'); this.route('add-brokered-credential-sources'); this.route('add-injected-application-credential-sources'); + this.route('create-alias'); + this.route('manage-alias', { path: ':alias_id' }, function () {}); this.route('enable-session-recording', function () { this.route('create-storage-bucket'); }); diff --git a/ui/admin/app/routes/scopes/scope/targets.js b/ui/admin/app/routes/scopes/scope/targets.js index 1a6f377d4..e420fd1b7 100644 --- a/ui/admin/app/routes/scopes/scope/targets.js +++ b/ui/admin/app/routes/scopes/scope/targets.js @@ -140,4 +140,33 @@ export default class ScopesScopeTargetsRoute extends Route { this.router.replaceWith('scopes.scope.targets'); this.refresh(); } + + /** + * Delete an alias + * @param {AliasModel} alias + */ + @action + @loading + @notifyError(({ message }) => message, { catch: true }) + @notifySuccess('notifications.delete-success') + async deleteAlias(alias) { + await alias.destroyRecord(); + await this.router.replaceWith('scopes.scope.targets.target'); + this.router.refresh(); + } + + /** + * Remove destaination_id from alias + * @param {AliasModel} alias + */ + @action + @loading + @notifyError(({ message }) => message, { catch: true }) + @notifySuccess('notifications.clear-success') + async clearAlias(alias) { + alias.destination_id = ''; + await alias.save(); + this.router.refresh('scopes.scope.targets.target'); + await this.router.transitionTo('scopes.scope.targets.target'); + } } diff --git a/ui/admin/app/routes/scopes/scope/targets/target/create-alias.js b/ui/admin/app/routes/scopes/scope/targets/target/create-alias.js new file mode 100644 index 000000000..f4543c194 --- /dev/null +++ b/ui/admin/app/routes/scopes/scope/targets/target/create-alias.js @@ -0,0 +1,58 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; +import { action } from '@ember/object'; +import { notifySuccess, notifyError } from 'core/decorators/notify'; +import { loading } from 'ember-loading'; +import { TYPE_ALIAS_TARGET } from 'api/models/alias'; + +export default class ScopesScopeTargetsTargetCreateAliasRoute extends Route { + // =services + + @service store; + @service router; + // =methods + + /** + * Creates a new unsaved alias. + * @return {AliasModel} + */ + model() { + const scopeModel = this.store.peekRecord('scope', 'global'); + const record = this.store.createRecord('alias', { + type: TYPE_ALIAS_TARGET, + }); + record.scopeModel = scopeModel; + return record; + } + + // =actions + + /** + * Handle save + * @param {AliasModel} alias + */ + @action + @loading + @notifyError(({ message }) => message) + @notifySuccess('notifications.save-success') + async save(alias) { + await alias.save(); + await this.router.transitionTo('scopes.scope.targets.target'); + this.router.refresh(); + } + + /** + * Rollback changes on alias. + * @param {AliasModel} alias + */ + @action + cancel(alias) { + alias.rollbackAttributes(); + this.router.transitionTo('scopes.scope.targets.target'); + } +} diff --git a/ui/admin/app/routes/scopes/scope/targets/target/manage-alias.js b/ui/admin/app/routes/scopes/scope/targets/target/manage-alias.js new file mode 100644 index 000000000..04b43ffa7 --- /dev/null +++ b/ui/admin/app/routes/scopes/scope/targets/target/manage-alias.js @@ -0,0 +1,54 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; +import { action } from '@ember/object'; +import { notifySuccess, notifyError } from 'core/decorators/notify'; +import { loading } from 'ember-loading'; + +export default class ScopesScopeTargetsTargetManageAliasRoute extends Route { + // =services + + @service store; + @service router; + // =methods + + /** + * Creates a new unsaved alias. + * @return {AliasModel} + */ + async model({ alias_id }) { + return this.store.findRecord('alias', alias_id, { + reload: true, + }); + } + + // =actions + + /** + * Handle save + * @param {AliasModel} alias + */ + @action + @loading + @notifyError(({ message }) => message) + @notifySuccess('notifications.save-success') + async save(alias) { + await alias.save(); + await this.router.refresh('scopes.scope.targets.target'); + this.router.transitionTo('scopes.scope.targets.target'); + } + + /** + * Rollback changes on alias. + * @param {AliasModel} alias + */ + @action + async cancel(alias) { + alias.rollbackAttributes(); + await this.router.transitionTo('scopes.scope.targets.target'); + } +} diff --git a/ui/admin/app/styles/app.scss b/ui/admin/app/styles/app.scss index 147d69faa..ec12a8a91 100644 --- a/ui/admin/app/styles/app.scss +++ b/ui/admin/app/styles/app.scss @@ -625,6 +625,7 @@ .target-sidebar-aliases { margin-bottom: 3rem; + width: 300px; .description { color: var(--token-form-helper-text-color); @@ -636,8 +637,28 @@ margin-top: 1rem; } + .link-list-item:first-of-type { + width: 90%; + } + .link-list-item { margin-bottom: 0.5rem; + word-break: break-word; + } + + /* stylelint-disable selector-class-pattern */ + .hds-card__container > ul { + display: flex; + margin-bottom: 0.5rem; + } + + .hds-link-standalone__text { + margin: 1rem 0; + } + /* stylelint-enable selector-class-pattern */ + + .hds-dropdown .hds-dropdown-toggle-icon { + color: var(--stark); } } diff --git a/ui/admin/app/templates/scopes/scope/targets/target/create-alias.hbs b/ui/admin/app/templates/scopes/scope/targets/target/create-alias.hbs new file mode 100644 index 000000000..3f3647f69 --- /dev/null +++ b/ui/admin/app/templates/scopes/scope/targets/target/create-alias.hbs @@ -0,0 +1,33 @@ +{{! + Copyright (c) HashiCorp, Inc. + SPDX-License-Identifier: MPL-2.0 +}} + +{{page-title (t 'titles.new')}} + + + + + + + + +

+ {{t 'resources.alias.titles.new'}} + {{! TODO: Add doc link for alias }} + +

+

{{t 'resources.alias.description'}}

+ +
+ + + +
\ No newline at end of file diff --git a/ui/admin/app/templates/scopes/scope/targets/target/index.hbs b/ui/admin/app/templates/scopes/scope/targets/target/index.hbs index d3c8a17aa..cb0260846 100644 --- a/ui/admin/app/templates/scopes/scope/targets/target/index.hbs +++ b/ui/admin/app/templates/scopes/scope/targets/target/index.hbs @@ -38,16 +38,28 @@ {{t 'resources.alias.form.sidebar.help'}}

{{#if @model.aliases}} - {{#each this.aliases as |alias|}} + + + + + + {{/each}} @@ -59,7 +71,7 @@ {{on 'click' this.targets.toggleFlyout}} /> {{/if}} - {{#if this.showFlyout}} + {{#if this.showFlyOutComponent}} + + + + + + {{/each}} @@ -92,7 +117,7 @@ @icon='plus' @text={{t 'resources.target.actions.add-an-alias'}} @color='secondary' - {{! @route='' }} + @route='scopes.scope.targets.target.create-alias' /> {{/if}} diff --git a/ui/admin/app/templates/scopes/scope/targets/target/manage-alias.hbs b/ui/admin/app/templates/scopes/scope/targets/target/manage-alias.hbs new file mode 100644 index 000000000..20887ed0e --- /dev/null +++ b/ui/admin/app/templates/scopes/scope/targets/target/manage-alias.hbs @@ -0,0 +1,55 @@ +{{! + Copyright (c) HashiCorp, Inc. + SPDX-License-Identifier: MPL-2.0 +}} + + + + + + + +

+ {{t 'resources.alias.title'}} + {{! TODO: DOC LINK }} +

+

{{t 'resources.alias.description'}}

+ +
+ + + + + + + {{#if (can 'delete model' @model)}} + + + {{/if}} + + + + + + + +
\ No newline at end of file diff --git a/ui/admin/tests/acceptance/aliases/create-test.js b/ui/admin/tests/acceptance/aliases/create-test.js index 6bf6e80df..b0a089586 100644 --- a/ui/admin/tests/acceptance/aliases/create-test.js +++ b/ui/admin/tests/acceptance/aliases/create-test.js @@ -14,7 +14,6 @@ import { authenticateSession } from 'ember-simple-auth/test-support'; module('Acceptance | aliases | create', function (hooks) { setupApplicationTest(hooks); setupMirage(hooks); - let features; let getAliasCount; diff --git a/ui/admin/tests/acceptance/targets/create-alias-test.js b/ui/admin/tests/acceptance/targets/create-alias-test.js new file mode 100644 index 000000000..89442c477 --- /dev/null +++ b/ui/admin/tests/acceptance/targets/create-alias-test.js @@ -0,0 +1,169 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +import { module, test } from 'qunit'; +import { visit, click, fillIn } from '@ember/test-helpers'; +import { setupApplicationTest } from 'admin/tests/helpers'; +import setupMirage from 'ember-cli-mirage/test-support/setup-mirage'; +import a11yAudit from 'ember-a11y-testing/test-support/audit'; +import { Response } from 'miragejs'; +import { authenticateSession } from 'ember-simple-auth/test-support'; +import { setupIndexedDb } from 'api/test-support/helpers/indexed-db'; + +import { TYPE_TARGET_TCP, TYPE_TARGET_SSH } from 'api/models/target'; + +module('Acceptance | targets | create-alias', function (hooks) { + setupApplicationTest(hooks); + setupMirage(hooks); + setupIndexedDb(hooks); + + let getAliasCount; + + const SAVE_BTN_SELECTOR = '[type="submit"]'; + const CANCEL_BTN_SELECTOR = '.rose-form-actions [type="button"]'; + const NAME_FIELD_SELECTOR = '[name="name"]'; + const ALIAS_FIELD_SELECTOR = '[name="value"]'; + const DEST_FIELD_SELECTOR = '[name="destination_id"]'; + const ALERT_TEXT_SELECTOR = '[role="alert"] div'; + const FIELD_ERROR_TEXT_SELECTOR = '.hds-form-error__message'; + const NAME_FIELD_TEXT = 'random string'; + const ALIAS_VALUE_TEXT = 'www.target1.com'; + const ADD_AN_ALIAS_SELECTOR = '.target-sidebar-aliases .hds-button'; + + const instances = { + scopes: { + global: null, + org: null, + }, + }; + + const urls = { + globalScope: null, + projectScope: null, + targets: null, + target: null, + }; + + hooks.beforeEach(function () { + instances.scopes.global = this.server.create('scope', { id: 'global' }); + instances.scopes.org = this.server.create('scope', { + type: 'org', + scope: { id: 'global', type: 'global' }, + }); + instances.scopes.project = this.server.create('scope', { + type: 'project', + scope: { id: instances.scopes.org.id, type: 'org' }, + }); + + instances.target = this.server.create('target', { + scope: instances.scopes.project, + type: TYPE_TARGET_TCP, + }); + + urls.globalScope = `/scopes/global`; + + urls.projectScope = `/scopes/${instances.scopes.project.id}`; + urls.targets = `${urls.projectScope}/targets`; + urls.target = `${urls.targets}/${instances.target.id}`; + + getAliasCount = () => this.server.schema.aliases.all().models.length; + + authenticateSession({}); + }); + + test('users can create a new alias for a target of TCP type', async function (assert) { + const aliasCount = getAliasCount(); + await visit(urls.targets); + + await click(`[href="${urls.target}"]`); + await click(ADD_AN_ALIAS_SELECTOR); + + await fillIn(NAME_FIELD_SELECTOR, NAME_FIELD_TEXT); + await fillIn(ALIAS_FIELD_SELECTOR, ALIAS_VALUE_TEXT); + + await fillIn(DEST_FIELD_SELECTOR, instances.target.id); + await click(SAVE_BTN_SELECTOR); + const alias = this.server.schema.aliases.findBy({ + name: NAME_FIELD_TEXT, + }); + + assert.strictEqual(alias.name, NAME_FIELD_TEXT); + assert.strictEqual(alias.scopeId, 'global'); + assert.strictEqual(getAliasCount(), aliasCount + 1); + }); + + test('users can create a new alias for a target of SSH type', async function (assert) { + instances.target = this.server.create('target', { + scope: instances.scopes.project, + type: TYPE_TARGET_SSH, + }); + const aliasCount = getAliasCount(); + + await visit(urls.targets); + + await click(`[href="${urls.target}"]`); + + await click(ADD_AN_ALIAS_SELECTOR); + + await fillIn(NAME_FIELD_SELECTOR, NAME_FIELD_TEXT); + await fillIn(ALIAS_FIELD_SELECTOR, ALIAS_VALUE_TEXT); + await fillIn(DEST_FIELD_SELECTOR, instances.target.id); + + await click(SAVE_BTN_SELECTOR); + const alias = this.server.schema.aliases.findBy({ + name: NAME_FIELD_TEXT, + }); + + assert.strictEqual(alias.name, NAME_FIELD_TEXT); + assert.strictEqual(alias.scopeId, 'global'); + assert.strictEqual(getAliasCount(), aliasCount + 1); + }); + + test('user can cancel new alias creation', async function (assert) { + const aliasCount = getAliasCount(); + instances.target = this.server.create('target', { + scope: instances.scopes.project, + type: TYPE_TARGET_SSH, + }); + await visit(urls.targets); + await click(`[href="${urls.target}"]`); + await click(ADD_AN_ALIAS_SELECTOR); + await fillIn(NAME_FIELD_SELECTOR, NAME_FIELD_TEXT); + await click(CANCEL_BTN_SELECTOR); + + assert.strictEqual(getAliasCount(), aliasCount); + }); + + test('saving a new alias with invalid fields displays error messages', async function (assert) { + this.server.post('/aliases', () => { + return new Response( + 400, + {}, + { + status: 400, + code: 'invalid_argument', + message: 'The request was invalid.', + details: { + request_fields: [ + { + name: 'name', + description: 'Name is required.', + }, + ], + }, + }, + ); + }); + await visit(urls.targets); + + await click(`[href="${urls.target}"]`); + await click(ADD_AN_ALIAS_SELECTOR); + await click(SAVE_BTN_SELECTOR); + await a11yAudit(); + + assert.dom(ALERT_TEXT_SELECTOR).hasText('The request was invalid.'); + assert.dom(FIELD_ERROR_TEXT_SELECTOR).hasText('Name is required.'); + }); +}); diff --git a/ui/admin/tests/acceptance/targets/manage-alias-test.js b/ui/admin/tests/acceptance/targets/manage-alias-test.js new file mode 100644 index 000000000..c1cd49db2 --- /dev/null +++ b/ui/admin/tests/acceptance/targets/manage-alias-test.js @@ -0,0 +1,164 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +import { module, test } from 'qunit'; +import { visit, currentURL, click } 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'; +import { TYPE_TARGET_TCP, TYPE_TARGET_SSH } from 'api/models/target'; + +module('Acceptance | targets | manage-alias', function (hooks) { + setupApplicationTest(hooks); + setupMirage(hooks); + let aliasResourceTwo, aliasResourceOne, getAliasCount; + const ALIASES_SIDEBAR = '.target-sidebar-aliases'; + const ALIASES_SIDEBAR_LIST = '.link-list-item'; + const DROPDOWN_BUTTON_SELECTOR = '.hds-dropdown-toggle-icon'; + const DROPDOWN_ITEM_SELECTOR = '.hds-dropdown-list-item a'; + const DROPDOWN_ACTION = '.hds-dropdown-toggle-button__text'; + const CLEAR_BTN_SELECTOR = '.hds-dropdown-list-item--color-action button'; + const DELETE_BTN_SELECTOR = '.hds-dropdown-list-item--color-critical button'; + const LINK_TO_NEW_ALIAS = '.target-sidebar-aliases .hds-button'; + + const instances = { + scopes: { + global: null, + org: null, + project: null, + }, + sshTarget: null, + tcpTarget: null, + }; + const urls = { + globalScope: null, + orgScope: null, + projectScope: null, + targets: null, + sshTarget: null, + tcpTarget: null, + tcpAlias: null, + aliases: null, + }; + + hooks.beforeEach(function () { + // Generate resources + instances.scopes.global = this.server.create('scope', { id: 'global' }); + instances.scopes.org = this.server.create('scope', { + type: 'org', + scope: { id: 'global', type: 'global' }, + }); + instances.scopes.project = this.server.create('scope', { + type: 'project', + scope: { id: instances.scopes.org.id, type: 'org' }, + }); + instances.alias = this.server.createList('alias', 2, { + scope: instances.scopes.global, + }); + + aliasResourceOne = instances.alias[0]; + aliasResourceTwo = instances.alias[1]; + + instances.sshTarget = this.server.create('target', { + type: TYPE_TARGET_SSH, + scope: instances.scopes.project, + aliases: [ + { id: aliasResourceOne.id, value: aliasResourceOne.value }, + { id: aliasResourceTwo.id, value: aliasResourceTwo.value }, + ], + }); + instances.tcpTarget = this.server.create('target', { + type: TYPE_TARGET_TCP, + scope: instances.scopes.project, + aliases: [{ id: aliasResourceOne.id, value: aliasResourceOne.value }], + }); + + // Generate route URLs for resources + urls.globalScope = `/scopes/global`; + urls.orgScope = `/scopes/${instances.scopes.org.id}/scopes`; + urls.projectScope = `/scopes/${instances.scopes.project.id}`; + urls.targets = `${urls.projectScope}/targets`; + urls.tcpTarget = `${urls.targets}/${instances.tcpTarget.id}`; + urls.sshTarget = `${urls.targets}/${instances.sshTarget.id}`; + urls.unknownTarget = `${urls.targets}/foo`; + + urls.tcpAlias = `${urls.tcpTarget}/${aliasResourceOne.id}`; + + getAliasCount = () => this.server.schema.aliases.all().models.length; + + authenticateSession({}); + }); + + test('clicking on manage should take you to manage page', async function (assert) { + await visit(urls.tcpTarget); + assert.dom(ALIASES_SIDEBAR).exists(); + assert.dom(ALIASES_SIDEBAR_LIST).exists(); + + await click(DROPDOWN_BUTTON_SELECTOR); + + assert.dom(DROPDOWN_ITEM_SELECTOR).exists(); + assert.dom(DROPDOWN_ITEM_SELECTOR).hasText('Manage'); + + await click(DROPDOWN_ITEM_SELECTOR); + assert.strictEqual(currentURL(), urls.tcpAlias); + await a11yAudit(); + }); + + test('clicking on `clear alias` from the dropdown should remove the destination ID and alias should disapper from the target sidebar', async function (assert) { + aliasResourceOne.update({ + destination_id: instances.tcpTarget.id, + }); + await visit(urls.tcpTarget); + assert.dom(ALIASES_SIDEBAR).exists(); + assert.dom(ALIASES_SIDEBAR_LIST).exists(); + await click(DROPDOWN_BUTTON_SELECTOR); + + assert.dom(DROPDOWN_ITEM_SELECTOR).exists(); + assert.dom(DROPDOWN_ITEM_SELECTOR).hasText('Manage'); + + await click(DROPDOWN_ITEM_SELECTOR); + assert.strictEqual(currentURL(), urls.tcpAlias); + await click(DROPDOWN_ACTION); + + await click(CLEAR_BTN_SELECTOR); + + const id = aliasResourceOne.id; + const associatedAlias = this.server.schema.aliases.findBy({ id }); + assert.strictEqual(associatedAlias.destinationId, null); + + assert.strictEqual(currentURL(), urls.tcpTarget); + + assert.dom(ALIASES_SIDEBAR_LIST).doesNotExist(); + assert.dom(LINK_TO_NEW_ALIAS).exists; + }); + + test('clicking on `delete alias` from the dropdown should remove the destination ID and alias should disapper from the target sidebar and deleted from aliases list', async function (assert) { + const aliasCount = getAliasCount(); + aliasResourceOne.update({ + destination_id: instances.tcpTarget.id, + }); + await visit(urls.tcpTarget); + assert.dom(ALIASES_SIDEBAR).exists(); + assert.dom(ALIASES_SIDEBAR_LIST).exists(); + await click(DROPDOWN_BUTTON_SELECTOR); + + assert.dom(DROPDOWN_ITEM_SELECTOR).exists(); + assert.dom(DROPDOWN_ITEM_SELECTOR).hasText('Manage'); + + await click(DROPDOWN_ITEM_SELECTOR); + assert.strictEqual(currentURL(), urls.tcpAlias); + await click(DROPDOWN_ACTION); + + await click(DELETE_BTN_SELECTOR); + + assert.strictEqual(getAliasCount(), aliasCount - 1); + + assert.strictEqual(currentURL(), urls.tcpTarget); + + assert.dom(ALIASES_SIDEBAR_LIST).doesNotExist(); + assert.dom(LINK_TO_NEW_ALIAS).exists; + }); +}); diff --git a/ui/admin/tests/acceptance/targets/read-test.js b/ui/admin/tests/acceptance/targets/read-test.js index c3ee2b7c0..b39569657 100644 --- a/ui/admin/tests/acceptance/targets/read-test.js +++ b/ui/admin/tests/acceptance/targets/read-test.js @@ -23,11 +23,15 @@ module('Acceptance | targets | read', function (hooks) { setupIndexedDb(hooks); let featuresService; + let aliasResource; const ALIASES_SIDEBAR = '.target-sidebar-aliases'; - const ALIASES_SIDEBAR_LIST = '.target-sidebar-aliases .link-list-item'; - const LINK_TO_NEW_ALIAS = '.target-sidebar-aliases button'; + const ALIASES_SIDEBAR_LIST = '.link-list-item'; + const LINK_TO_NEW_ALIAS = '.target-sidebar-aliases .hds-button'; const VIEW_MORE_BTN = '[data-test-view-more]'; const FLYOUT_COMPONENT = '[data-test-flyout]'; + const DROPDOWN_BUTTON_SELECTOR = '.hds-dropdown-toggle-icon'; + const DROPDOWN_ITEM_SELECTOR = '.hds-dropdown-list-item a'; + const instances = { scopes: { global: null, @@ -44,6 +48,8 @@ module('Acceptance | targets | read', function (hooks) { targets: null, sshTarget: null, tcpTarget: null, + alias: null, + aliases: null, }; hooks.beforeEach(function () { @@ -67,14 +73,23 @@ module('Acceptance | targets | read', function (hooks) { scope: instances.scopes.project, }); + instances.alias = this.server.createList('alias', 1, { + scope: instances.scopes.global, + }); + + aliasResource = instances.alias[0]; + // Generate route URLs for resources - urls.globalScope = `/scopes/global/scopes`; + urls.globalScope = `/scopes/global`; urls.orgScope = `/scopes/${instances.scopes.org.id}/scopes`; urls.projectScope = `/scopes/${instances.scopes.project.id}`; urls.targets = `${urls.projectScope}/targets`; urls.sshTarget = `${urls.targets}/${instances.sshTarget.id}`; urls.tcpTarget = `${urls.targets}/${instances.tcpTarget.id}`; urls.unknownTarget = `${urls.targets}/foo`; + urls.aliases = `${urls.globalScope}/aliases`; + + urls.alias = `${urls.tcpTarget}/${aliasResource.id}`; authenticateSession({}); }); @@ -198,7 +213,7 @@ module('Acceptance | targets | read', function (hooks) { test('can view aliases on the right sidebar', async function (assert) { instances.tcpTarget.update({ - aliases: [{ id: 'alt_123', value: 'alias 1' }], + aliases: [{ id: aliasResource.id, value: 'alias 1' }], }); await visit(urls.tcpTarget); assert.dom(ALIASES_SIDEBAR).exists(); @@ -219,10 +234,10 @@ module('Acceptance | targets | read', function (hooks) { test('can click `view more aliases` to see the remaining associated aliases if there are more than 3', async function (assert) { instances.tcpTarget.update({ aliases: [ - { id: 'alt_123', value: 'alias 1' }, - { id: 'alt_125', value: 'alias 5' }, - { id: 'alt_122', value: 'alias 2' }, - { id: 'alt_124', value: 'alias 4' }, + { id: aliasResource.id, value: 'alias 1' }, + { id: aliasResource.id, value: 'alias 5' }, + { id: aliasResource.id, value: 'alias 2' }, + { id: aliasResource.id, value: 'alias 4' }, ], }); await visit(urls.tcpTarget); @@ -232,4 +247,19 @@ module('Acceptance | targets | read', function (hooks) { await click(VIEW_MORE_BTN); assert.dom(FLYOUT_COMPONENT).exists(); }); + + test('users can click on associated alias card on the right sidebar', async function (assert) { + instances.tcpTarget.update({ + aliases: [{ id: aliasResource.id, value: aliasResource.value }], + }); + await visit(urls.tcpTarget); + + assert.dom(ALIASES_SIDEBAR_LIST).exists(); + await click(DROPDOWN_BUTTON_SELECTOR); + + assert.dom(DROPDOWN_ITEM_SELECTOR).exists(); + assert.dom(DROPDOWN_ITEM_SELECTOR).hasText('Manage'); + await click(DROPDOWN_ITEM_SELECTOR); + assert.strictEqual(currentURL(), urls.alias); + }); }); diff --git a/ui/admin/tests/unit/routes/scopes/scope/targets/target/create-alias-test.js b/ui/admin/tests/unit/routes/scopes/scope/targets/target/create-alias-test.js new file mode 100644 index 000000000..b1e1f705c --- /dev/null +++ b/ui/admin/tests/unit/routes/scopes/scope/targets/target/create-alias-test.js @@ -0,0 +1,21 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module( + 'Unit | Route | scopes/scope/targets/target/create-alias', + function (hooks) { + setupTest(hooks); + + test('it exists', function (assert) { + let route = this.owner.lookup( + 'route:scopes/scope/targets/target/create-alias', + ); + assert.ok(route); + }); + }, +); diff --git a/ui/admin/tests/unit/routes/scopes/scope/targets/target/manage-alias-test.js b/ui/admin/tests/unit/routes/scopes/scope/targets/target/manage-alias-test.js new file mode 100644 index 000000000..f2b99cb40 --- /dev/null +++ b/ui/admin/tests/unit/routes/scopes/scope/targets/target/manage-alias-test.js @@ -0,0 +1,21 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module( + 'Unit | Route | scopes/scope/targets/target/manage-alias', + function (hooks) { + setupTest(hooks); + + test('it exists', function (assert) { + let route = this.owner.lookup( + 'route:scopes/scope/targets/target/manage-alias', + ); + assert.ok(route); + }); + }, +);