chore: 🤖 aliases list view (#2197)

* chore: 🤖 aliases list view
This commit is contained in:
Dharini Jeeva authored and GitHub committed 2024-03-25 16:54:15 -07:00
1 parent ac1d98bbec
commit db75b2e292
11 files changed
+388 -56

No files matched your search

+4 -3
View File
@@ -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 });
+1
View File
@@ -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
+20 -20
View File
@@ -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();
}
}
+15 -1
View File
@@ -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
}
+2 -1
View File
@@ -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);
});
});