diff --git a/addons/api/mirage/scenarios/default.js b/addons/api/mirage/scenarios/default.js index 712dc8db4..042a20851 100644 --- a/addons/api/mirage/scenarios/default.js +++ b/addons/api/mirage/scenarios/default.js @@ -108,9 +108,6 @@ export default function (server) { server.createList('policy', 3, { scope: globalScope }); server.createList('policy', 3, { scope: orgScope }); - // Aliases - server.createList('alias', 3, { scope: globalScope }); - // Other resources server.schema.scopes.where({ type: 'project' }).models.forEach((scope) => { server.createList('host-catalog', 8, { scope }, 'withChildren'); @@ -127,6 +124,10 @@ export default function (server) { server.createList('role', 3, { scope }); }); + // Aliases + const destination_id = server.schema.targets.all().models[0].id; + server.createList('alias', 3, { scope: globalScope, destination_id }); + // Workers server.createList('worker', 3, { scope: globalScope }); diff --git a/addons/core/translations/en-us.yaml b/addons/core/translations/en-us.yaml index 1e5e250dd..561e3130c 100644 --- a/addons/core/translations/en-us.yaml +++ b/addons/core/translations/en-us.yaml @@ -41,6 +41,7 @@ titles: no-results-found: No results found filters-applied: 'Filters Applied:' unit: Unit + not-set: Not set year: '{numberOfYears, plural, =1 {# year} other {# years}}' days: '{days, plural, =1 {# day} other {# days}}' descriptions: diff --git a/addons/core/translations/resources/en-us.yaml b/addons/core/translations/resources/en-us.yaml index 6d79c3562..85d29cef2 100644 --- a/addons/core/translations/resources/en-us.yaml +++ b/addons/core/translations/resources/en-us.yaml @@ -1005,6 +1005,8 @@ alias: description: An Alias is a DNS-like string associated with a destination resource. titles: new: New Alias + resource_id: Resource ID + points_to: Points to messages: none: title: Welcome to Aliases @@ -1018,7 +1020,7 @@ alias: help: Servers, API Endpoints, and network devices destination_id: label: Target ID - help: Any resource in this scope or any orgs and projects inside it + help: Specify any target across your deployment to associate this alias with value: label: Alias Value help: The alias you would like to use for this resource diff --git a/ui/admin/app/components/form/alias/index.hbs b/ui/admin/app/components/form/alias/index.hbs index 678226eae..d24c9178e 100644 --- a/ui/admin/app/components/form/alias/index.hbs +++ b/ui/admin/app/components/form/alias/index.hbs @@ -67,6 +67,26 @@ + + {{t 'resources.alias.form.value.label'}} + {{t 'resources.alias.form.value.help'}} + {{#if @model.errors.value}} + + {{#each @model.errors.value as |error|}} + {{error.message}} + {{/each}} + + {{/if}} + + {{t 'resources.scope.title'}} - - {{t 'resources.alias.form.value.label'}} - {{t 'resources.alias.form.value.help'}} - {{#if @model.errors.value}} - - {{#each @model.errors.value as |error|}} - {{error.message}} - {{/each}} - - {{/if}} - - {{#if (can 'save model' @model)}} + hash({ + alias, + target: alias.destination_id + ? this.store.findRecord('target', alias.destination_id) + : null, + }), + ), + ); + return aliasesWithResourceNames; } } } diff --git a/ui/admin/app/routes/scopes/scope/aliases/alias/index.js b/ui/admin/app/routes/scopes/scope/aliases/alias/index.js new file mode 100644 index 000000000..fa8715e86 --- /dev/null +++ b/ui/admin/app/routes/scopes/scope/aliases/alias/index.js @@ -0,0 +1,10 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +import Route from '@ember/routing/route'; + +export default class ScopesScopeAliasesAliasIndexRoute extends Route { + // =services +} diff --git a/ui/admin/app/styles/app.scss b/ui/admin/app/styles/app.scss index 9ce4f39ae..8e4f354f3 100644 --- a/ui/admin/app/styles/app.scss +++ b/ui/admin/app/styles/app.scss @@ -856,7 +856,8 @@ } // Session recording - List -.session-recording-list-cell-item { +.session-recording-list-cell-item, +.aliases-list-cell-item { display: flex; gap: 0.5rem; diff --git a/ui/admin/app/templates/scopes/scope/aliases/alias/index.hbs b/ui/admin/app/templates/scopes/scope/aliases/alias/index.hbs index 22425d729..80d3315b3 100644 --- a/ui/admin/app/templates/scopes/scope/aliases/alias/index.hbs +++ b/ui/admin/app/templates/scopes/scope/aliases/alias/index.hbs @@ -7,5 +7,28 @@ + + + {{t 'resources.alias.title'}} + {{! TODO: DOC LINK }} + + {{t 'resources.alias.description'}} + + + + + + {{t 'titles.details'}} + + + + + + + \ No newline at end of file diff --git a/ui/admin/app/templates/scopes/scope/aliases/index.hbs b/ui/admin/app/templates/scopes/scope/aliases/index.hbs index 3ecaea57c..33bc46a40 100644 --- a/ui/admin/app/templates/scopes/scope/aliases/index.hbs +++ b/ui/admin/app/templates/scopes/scope/aliases/index.hbs @@ -25,35 +25,138 @@ {{/if}} - - - - {{#if (can 'list model' this.scope collection='aliases')}} - {{t 'resources.alias.messages.none.description'}} - {{else if (can 'create model' this.scope collection='aliases')}} - {{t - 'descriptions.create-but-not-list' - resource=(t 'resources.alias.title_plural') - }} - {{else}} - {{t - 'descriptions.neither-list-nor-create' - resource=(t 'resources.alias.title_plural') - }} + <:body as |B|> + + + {{#if (can 'read model' B.data.alias)}} + + {{B.data.alias.displayName}} + + {{else}} + + {{B.data.alias.displayName}} + + {{/if}} + {{#if B.data.alias.description}} + + {{B.data.alias.description}} + + {{/if}} + + + {{#if B.data.target.displayName}} + + + {{B.data.target.displayName}} + + {{else}} + + {{t 'titles.not-set'}} + + {{/if}} + + + + + + {{#if B.data.alias.destination_id}} + + {{/if}} + + + {{#let + (hash + canRead=(can 'read model' B.data.alias) + canDelete=(can 'delete model' B.data.alias) + ) + as |perms| + }} + {{#if (or perms.canRead perms.canDelete)}} + + + {{#if perms.canRead}} + + {{/if}} + {{#if (and perms.canRead perms.canDelete)}} + + {{/if}} + {{#if perms.canDelete}} + + {{/if}} + + {{/if}} + {{/let}} + + + + + {{else}} + + + + {{#if (can 'list model' this.scope collection='aliases')}} + {{t 'resources.alias.messages.none.description'}} + {{else if (can 'create model' this.scope collection='aliases')}} + {{t + 'descriptions.create-but-not-list' + resource=(t 'resources.alias.title_plural') + }} + {{else}} + {{t + 'descriptions.neither-list-nor-create' + resource=(t 'resources.alias.title_plural') + }} + {{/if}} + + {{#if (can 'create model' this.scope collection='aliases')}} + {{/if}} - - {{#if (can 'create model' this.scope collection='aliases')}} - - {{/if}} - - + + + {{/if}} \ No newline at end of file diff --git a/ui/admin/tests/acceptance/aliases/list-test.js b/ui/admin/tests/acceptance/aliases/list-test.js new file mode 100644 index 000000000..6c55e98ac --- /dev/null +++ b/ui/admin/tests/acceptance/aliases/list-test.js @@ -0,0 +1,178 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +import { module, test } from 'qunit'; +import { visit, click, currentURL } from '@ember/test-helpers'; +import { setupApplicationTest } from 'ember-qunit'; +import setupMirage from 'ember-cli-mirage/test-support/setup-mirage'; +import { authenticateSession } from 'ember-simple-auth/test-support'; + +module('Acceptance | aliases | list', function (hooks) { + setupApplicationTest(hooks); + setupMirage(hooks); + + let intl; + + const ALIAS_TITLE = 'Aliases'; + const MESSAGE_DESCRIPTION_SELECTOR = '.rose-message-description'; + const MESSAGE_LINK_SELECTOR = '.rose-message-body .hds-link-standalone'; + const DROPDOWN_BUTTON_SELECTOR = '.hds-dropdown-toggle-icon'; + const DROPDOWN_ITEM_SELECTOR = '.hds-dropdown-list-item a'; + + const instances = { + scopes: { + global: null, + org: null, + }, + alias: null, + }; + + const urls = { + globalScope: null, + aliases: null, + alias: 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' }, + }); + urls.globalScope = `/scopes/global`; + urls.aliases = `${urls.globalScope}/aliases`; + + intl = this.owner.lookup('service:intl'); + + authenticateSession({}); + }); + + test('users can navigate to aliases with proper authorization', async function (assert) { + await visit(urls.globalScope); + + assert.ok( + instances.scopes.global.authorized_collection_actions['aliases'].includes( + 'list', + ), + ); + assert.ok( + instances.scopes.global.authorized_collection_actions['aliases'].includes( + 'create', + ), + ); + + assert.dom(`[href="${urls.aliases}"]`).isVisible(); + + await click(`[href="${urls.aliases}"]`); + + assert + .dom(MESSAGE_DESCRIPTION_SELECTOR) + .hasText(intl.t('resources.alias.messages.none.description')); + assert.dom(MESSAGE_LINK_SELECTOR).exists(); + }); + + test('user cannot navigate to index without either list or create actions', async function (assert) { + instances.scopes.global.authorized_collection_actions['aliases'] = []; + + await visit(urls.globalScope); + + assert.false( + instances.scopes.global.authorized_collection_actions['aliases'].includes( + 'list', + ), + ); + assert.false( + instances.scopes.global.authorized_collection_actions['aliases'].includes( + 'create', + ), + ); + assert + .dom('[title="General"] a:nth-of-type(1)') + .doesNotIncludeText(ALIAS_TITLE); + + await visit(urls.aliases); + + assert.dom(MESSAGE_DESCRIPTION_SELECTOR).hasText( + intl.t('descriptions.neither-list-nor-create', { + resource: ALIAS_TITLE, + }), + ); + assert.dom(MESSAGE_LINK_SELECTOR).doesNotExist(); + }); + + test('user can navigate to index with only create action', async function (assert) { + instances.scopes.global.authorized_collection_actions['aliases'] = + instances.scopes.global.authorized_collection_actions['aliases'].filter( + (item) => item !== 'list', + ); + + await visit(urls.globalScope); + + assert.false( + instances.scopes.global.authorized_collection_actions['aliases'].includes( + 'list', + ), + ); + assert.true( + instances.scopes.global.authorized_collection_actions['aliases'].includes( + 'create', + ), + ); + assert.dom(`[href="${urls.aliases}"]`).exists(); + + await click(`[href="${urls.aliases}"]`); + + assert.dom(MESSAGE_DESCRIPTION_SELECTOR).hasText( + intl.t('descriptions.create-but-not-list', { + resource: ALIAS_TITLE, + }), + ); + assert.dom(MESSAGE_LINK_SELECTOR).exists(); + }); + + test('user can navigate to index with only list action', async function (assert) { + instances.scopes.global.authorized_collection_actions['aliases'] = + instances.scopes.global.authorized_collection_actions['aliases'].filter( + (item) => item !== 'create', + ); + + await visit(urls.globalScope); + + assert.true( + instances.scopes.global.authorized_collection_actions['aliases'].includes( + 'list', + ), + ); + assert.false( + instances.scopes.global.authorized_collection_actions['aliases'].includes( + 'create', + ), + ); + assert.dom(`[href="${urls.aliases}"]`).exists(); + + await click(`[href="${urls.aliases}"]`); + + assert + .dom(MESSAGE_DESCRIPTION_SELECTOR) + .hasText(intl.t('resources.alias.messages.none.description')); + assert.dom(MESSAGE_LINK_SELECTOR).doesNotExist(); + }); + + test('edit action in table directs user to appropriate page', async function (assert) { + await visit(urls.globalScope); + instances.alias = this.server.create('alias', { + scope: instances.scopes.global, + }); + urls.alias = `${urls.aliases}/${instances.alias.id}`; + + await click(`[href="${urls.aliases}"]`); + await click(DROPDOWN_BUTTON_SELECTOR); + + assert.dom(DROPDOWN_ITEM_SELECTOR).exists(); + assert.dom(DROPDOWN_ITEM_SELECTOR).hasText('Edit'); + await click(DROPDOWN_ITEM_SELECTOR); + assert.strictEqual(currentURL(), urls.alias); + }); +});
{{t 'resources.alias.description'}}