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);
+ });
+ },
+);