1 parent
ac1d98bbec
commit
db75b2e292
11 files changed
+388
-56
No files matched your search
@@ -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 });
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -67,6 +67,26 @@
|
||||
</F.Control>
|
||||
</Hds::Form::Fieldset>
|
||||
|
||||
<Hds::Form::TextInput::Field
|
||||
name='value'
|
||||
@isRequired={{true}}
|
||||
@value={{@model.value}}
|
||||
@isInvalid={{@model.errors.value}}
|
||||
disabled={{form.disabled}}
|
||||
{{on 'input' (set-from-event @model 'value')}}
|
||||
as |F|
|
||||
>
|
||||
<F.Label>{{t 'resources.alias.form.value.label'}}</F.Label>
|
||||
<F.HelperText>{{t 'resources.alias.form.value.help'}}</F.HelperText>
|
||||
{{#if @model.errors.value}}
|
||||
<F.Error as |E|>
|
||||
{{#each @model.errors.value as |error|}}
|
||||
<E.Message>{{error.message}}</E.Message>
|
||||
{{/each}}
|
||||
</F.Error>
|
||||
{{/if}}
|
||||
</Hds::Form::TextInput::Field>
|
||||
|
||||
<Hds::Form::TextInput::Field
|
||||
name='destination_id'
|
||||
@isOptional={{true}}
|
||||
@@ -122,26 +142,6 @@
|
||||
<F.Label>{{t 'resources.scope.title'}}</F.Label>
|
||||
</InfoField>
|
||||
|
||||
<Hds::Form::TextInput::Field
|
||||
name='value'
|
||||
@isRequired={{true}}
|
||||
@value={{@model.value}}
|
||||
@isInvalid={{@model.errors.value}}
|
||||
disabled={{form.disabled}}
|
||||
{{on 'input' (set-from-event @model 'value')}}
|
||||
as |F|
|
||||
>
|
||||
<F.Label>{{t 'resources.alias.form.value.label'}}</F.Label>
|
||||
<F.HelperText>{{t 'resources.alias.form.value.help'}}</F.HelperText>
|
||||
{{#if @model.errors.value}}
|
||||
<F.Error as |E|>
|
||||
{{#each @model.errors.value as |error|}}
|
||||
<E.Message>{{error.message}}</E.Message>
|
||||
{{/each}}
|
||||
</F.Error>
|
||||
{{/if}}
|
||||
</Hds::Form::TextInput::Field>
|
||||
|
||||
{{#if (can 'save model' @model)}}
|
||||
<form.actions
|
||||
@enableEditText={{t 'actions.edit-form'}}
|
||||
|
||||
@@ -48,7 +48,7 @@ export default class ScopesScopeAliasesIndexController extends Controller {
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an alias in current scope and redirect to index
|
||||
* Delete an alias in current scope
|
||||
* @param {AliasModel} alias
|
||||
*/
|
||||
@action
|
||||
@@ -58,7 +58,6 @@ export default class ScopesScopeAliasesIndexController extends Controller {
|
||||
@notifySuccess('notifications.delete-success')
|
||||
async delete(alias) {
|
||||
await alias.destroyRecord();
|
||||
await this.router.replaceWith('scopes.scope.aliases');
|
||||
await this.router.refresh();
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { all, hash } from 'rsvp';
|
||||
|
||||
export default class ScopesScopeAliasesRoute extends Route {
|
||||
// =services
|
||||
@@ -36,7 +37,20 @@ export default class ScopesScopeAliasesRoute extends Route {
|
||||
collection: 'aliases',
|
||||
})
|
||||
) {
|
||||
return this.store.query('alias', { scope_id });
|
||||
const aliases = await this.store.query('alias', { scope_id });
|
||||
// since we don't receive target info from aliases list API,
|
||||
// we query the store to fetch target information based on the destination id
|
||||
const aliasesWithResourceNames = await all(
|
||||
aliases.map((alias) =>
|
||||
hash({
|
||||
alias,
|
||||
target: alias.destination_id
|
||||
? this.store.findRecord('target', alias.destination_id)
|
||||
: null,
|
||||
}),
|
||||
),
|
||||
);
|
||||
return aliasesWithResourceNames;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -7,5 +7,28 @@
|
||||
<page.breadcrumbs>
|
||||
<Breadcrumbs::Container />
|
||||
</page.breadcrumbs>
|
||||
<page.header>
|
||||
<h2>
|
||||
{{t 'resources.alias.title'}}
|
||||
{{! TODO: DOC LINK }}
|
||||
</h2>
|
||||
<p>{{t 'resources.alias.description'}}</p>
|
||||
<Hds::Copy::Snippet @color='secondary' @textToCopy={{@model.id}} />
|
||||
</page.header>
|
||||
|
||||
<page.navigation>
|
||||
<Rose::Nav::Tabs as |nav|>
|
||||
<nav.link @route='scopes.scope.aliases.alias.index'>
|
||||
{{t 'titles.details'}}
|
||||
</nav.link>
|
||||
</Rose::Nav::Tabs>
|
||||
</page.navigation>
|
||||
|
||||
<page.body>
|
||||
<Form::Alias
|
||||
@model={{@model}}
|
||||
@submit={{fn this.aliases.save @model}}
|
||||
@cancel={{fn this.aliases.cancel @model}}
|
||||
/>
|
||||
</page.body>
|
||||
</Rose::Layout::Page>
|
||||
@@ -25,35 +25,138 @@
|
||||
{{/if}}
|
||||
</page.actions>
|
||||
<page.body>
|
||||
<Rose::Layout::Centered>
|
||||
<Rose::Message
|
||||
@title={{t 'resources.alias.messages.none.title'}}
|
||||
as |message|
|
||||
{{#if @model}}
|
||||
<Hds::Table
|
||||
@model={{@model}}
|
||||
@columns={{array
|
||||
(hash label=(t 'resources.alias.title'))
|
||||
(hash label=(t 'resources.alias.titles.points_to'))
|
||||
(hash label=(t 'resources.scope.title'))
|
||||
(hash label=(t 'resources.alias.titles.resource_id'))
|
||||
(hash label='')
|
||||
}}
|
||||
@valign='middle'
|
||||
>
|
||||
<message.description>
|
||||
{{#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|>
|
||||
<B.Tr>
|
||||
<B.Td>
|
||||
{{#if (can 'read model' B.data.alias)}}
|
||||
<LinkTo
|
||||
@route='scopes.scope.aliases.alias'
|
||||
@model={{B.data.alias.id}}
|
||||
>
|
||||
{{B.data.alias.displayName}}
|
||||
</LinkTo>
|
||||
{{else}}
|
||||
<Hds::Text::Body @tag='p'>
|
||||
{{B.data.alias.displayName}}
|
||||
</Hds::Text::Body>
|
||||
{{/if}}
|
||||
{{#if B.data.alias.description}}
|
||||
<Hds::Text::Body @tag='p'>
|
||||
{{B.data.alias.description}}
|
||||
</Hds::Text::Body>
|
||||
{{/if}}
|
||||
</B.Td>
|
||||
<B.Td>
|
||||
{{#if B.data.target.displayName}}
|
||||
<div class='aliases-list-cell-item'>
|
||||
<span><FlightIcon @name='target' /></span>
|
||||
<span> {{B.data.target.displayName}} </span>
|
||||
</div>
|
||||
{{else}}
|
||||
<Hds::Text::Body
|
||||
@tag='p'
|
||||
@size='100'
|
||||
@weight='medium'
|
||||
@color='faint'
|
||||
>
|
||||
{{t 'titles.not-set'}}
|
||||
</Hds::Text::Body>
|
||||
{{/if}}
|
||||
</B.Td>
|
||||
<B.Td>
|
||||
<Hds::Badge @text={{t 'titles.global'}} @icon='globe' />
|
||||
</B.Td>
|
||||
<B.Td>
|
||||
{{#if B.data.alias.destination_id}}
|
||||
<Hds::Copy::Snippet
|
||||
@textToCopy={{B.data.alias.destination_id}}
|
||||
@color='secondary'
|
||||
/>
|
||||
{{/if}}
|
||||
</B.Td>
|
||||
<B.Td>
|
||||
{{#let
|
||||
(hash
|
||||
canRead=(can 'read model' B.data.alias)
|
||||
canDelete=(can 'delete model' B.data.alias)
|
||||
)
|
||||
as |perms|
|
||||
}}
|
||||
{{#if (or perms.canRead perms.canDelete)}}
|
||||
<Hds::Dropdown as |dd|>
|
||||
<dd.ToggleIcon
|
||||
@icon='more-horizontal'
|
||||
@text={{t 'actions.manage'}}
|
||||
@hasChevron={{false}}
|
||||
@size='small'
|
||||
/>
|
||||
{{#if perms.canRead}}
|
||||
<dd.Interactive
|
||||
@route='scopes.scope.aliases.alias'
|
||||
@model={{B.data.alias.id}}
|
||||
@text={{t 'actions.edit'}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{#if (and perms.canRead perms.canDelete)}}
|
||||
<dd.Separator />
|
||||
{{/if}}
|
||||
{{#if perms.canDelete}}
|
||||
<dd.Interactive
|
||||
@text={{t 'actions.delete'}}
|
||||
@color='critical'
|
||||
{{on 'click' (fn this.delete B.data.alias)}}
|
||||
/>
|
||||
{{/if}}
|
||||
</Hds::Dropdown>
|
||||
{{/if}}
|
||||
{{/let}}
|
||||
</B.Td>
|
||||
</B.Tr>
|
||||
</:body>
|
||||
</Hds::Table>
|
||||
{{else}}
|
||||
<Rose::Layout::Centered>
|
||||
<Rose::Message
|
||||
@title={{t 'resources.alias.messages.none.title'}}
|
||||
as |message|
|
||||
>
|
||||
<message.description>
|
||||
{{#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}}
|
||||
</message.description>
|
||||
{{#if (can 'create model' this.scope collection='aliases')}}
|
||||
<Hds::Link::Standalone
|
||||
@icon='plus-circle'
|
||||
@iconPosition='leading'
|
||||
@route='scopes.scope.aliases.new'
|
||||
@text={{t 'resources.alias.messages.none.link'}}
|
||||
/>
|
||||
{{/if}}
|
||||
</message.description>
|
||||
{{#if (can 'create model' this.scope collection='aliases')}}
|
||||
<Hds::Link::Standalone
|
||||
@icon='plus-circle'
|
||||
@iconPosition='leading'
|
||||
@route='scopes.scope.aliases.new'
|
||||
@text={{t 'resources.alias.messages.none.link'}}
|
||||
/>
|
||||
{{/if}}
|
||||
</Rose::Message>
|
||||
</Rose::Layout::Centered>
|
||||
</Rose::Message>
|
||||
</Rose::Layout::Centered>
|
||||
{{/if}}
|
||||
</page.body>
|
||||
</Rose::Layout::Page>
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user