ICU-16782 sort admin aliases (#2843)
* feat: 🎸 add sorting to aliases/index route ✅ Closes: https://hashicorp.atlassian.net/browse/ICU-16782 * refactor: 💡 use hds table components directly for aliases table ✅ Closes: https://hashicorp.atlassian.net/browse/ICU-16782 * feat: 🎸 wire up sorting with alias table, incl. query params ✅ Closes: https://hashicorp.atlassian.net/browse/ICU-16782 * test: 💍 add tests for sorting aliases ✅ Closes: https://hashicorp.atlassian.net/browse/ICU-16782 * fix: 🐛 use {{true}} instead of 'true' for @isVisuallyHidden ✅ Closes: https://hashicorp.atlassian.net/browse/ICU-16782
This commit is contained in:
1 parent
5a302b0bcc
commit
204bd10e04
4 files changed
+243
-102
No files matched your search
@@ -20,11 +20,19 @@ export default class ScopesScopeAliasesIndexController extends Controller {
|
||||
|
||||
// =attributes
|
||||
|
||||
queryParams = ['search', 'page', 'pageSize'];
|
||||
queryParams = [
|
||||
'search',
|
||||
'page',
|
||||
'pageSize',
|
||||
'sortAttribute',
|
||||
'sortDirection',
|
||||
];
|
||||
|
||||
@tracked search;
|
||||
@tracked page = 1;
|
||||
@tracked pageSize = 10;
|
||||
@tracked sortAttribute;
|
||||
@tracked sortDirection;
|
||||
|
||||
/**
|
||||
* If can list (at least): return default welcome message.
|
||||
@@ -124,4 +132,11 @@ export default class ScopesScopeAliasesIndexController extends Controller {
|
||||
this.search = value;
|
||||
this.page = 1;
|
||||
}
|
||||
|
||||
@action
|
||||
sortBy(attribute, direction) {
|
||||
this.sortAttribute = attribute;
|
||||
this.sortDirection = direction;
|
||||
this.page = 1;
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,14 @@ export default class ScopesScopeAliasesIndexRoute extends Route {
|
||||
pageSize: {
|
||||
refreshModel: true,
|
||||
},
|
||||
sortAttribute: {
|
||||
refreshModel: true,
|
||||
replace: true,
|
||||
},
|
||||
sortDirection: {
|
||||
refreshModel: true,
|
||||
replace: true,
|
||||
},
|
||||
};
|
||||
|
||||
// =methods
|
||||
@@ -42,7 +50,14 @@ export default class ScopesScopeAliasesIndexRoute extends Route {
|
||||
}
|
||||
|
||||
retrieveData = restartableTask(
|
||||
async ({ search, page, pageSize, useDebounce }) => {
|
||||
async ({
|
||||
search,
|
||||
page,
|
||||
pageSize,
|
||||
useDebounce,
|
||||
sortAttribute,
|
||||
sortDirection,
|
||||
}) => {
|
||||
if (useDebounce) {
|
||||
await timeout(250);
|
||||
}
|
||||
@@ -59,12 +74,18 @@ export default class ScopesScopeAliasesIndexRoute extends Route {
|
||||
collection: 'aliases',
|
||||
})
|
||||
) {
|
||||
const sort = {
|
||||
attribute: sortAttribute,
|
||||
direction: sortDirection,
|
||||
};
|
||||
|
||||
aliases = await this.store.query('alias', {
|
||||
scope_id,
|
||||
query: { search },
|
||||
query: { search, sort },
|
||||
page,
|
||||
pageSize,
|
||||
});
|
||||
|
||||
totalItems = aliases.meta?.totalItems;
|
||||
// since we don't receive target info from aliases list API,
|
||||
// we query the store to fetch target information based on the destination id
|
||||
|
||||
@@ -44,113 +44,136 @@
|
||||
|
||||
{{#if @model.aliases}}
|
||||
<Hds::Table
|
||||
@model={{@model.aliases}}
|
||||
@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=(t 'titles.actions') isVisuallyHidden=true)
|
||||
}}
|
||||
@valign='middle'
|
||||
@sortBy={{this.sortAttribute}}
|
||||
@sortOrder={{this.sortDirection}}
|
||||
@onSort={{this.sortBy}}
|
||||
>
|
||||
<:body as |B|>
|
||||
<B.Tr data-test-alias-row={{B.data.alias.id}}>
|
||||
<B.Td>
|
||||
{{#if (can 'read model' B.data.alias)}}
|
||||
<LinkTo
|
||||
@route='scopes.scope.aliases.alias'
|
||||
@model={{B.data.alias.id}}
|
||||
>
|
||||
{{B.data.alias.value}}
|
||||
</LinkTo>
|
||||
{{else}}
|
||||
<Hds::Text::Body @tag='p'>
|
||||
{{B.data.alias.value}}
|
||||
</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><Hds::Icon @name='target' @isInline={{true}} /></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
|
||||
canUpdate=(can 'update model' B.data.alias)
|
||||
canDelete=(can 'delete model' B.data.alias)
|
||||
)
|
||||
as |perms|
|
||||
}}
|
||||
{{#if (or perms.canUpdate perms.canDelete)}}
|
||||
<Hds::Dropdown as |dd|>
|
||||
<:head as |H|>
|
||||
<H.Tr>
|
||||
<H.ThSort
|
||||
@onClickSort={{fn H.setSortBy 'value'}}
|
||||
@sortOrder={{if (eq 'value' H.sortBy) H.sortOrder}}
|
||||
>
|
||||
{{t 'resources.alias.title'}}
|
||||
</H.ThSort>
|
||||
<H.Th>
|
||||
{{t 'resources.alias.titles.points_to'}}
|
||||
</H.Th>
|
||||
<H.Th>
|
||||
{{t 'resources.scope.title'}}
|
||||
</H.Th>
|
||||
<H.Th>
|
||||
{{t 'resources.alias.titles.resource_id'}}
|
||||
</H.Th>
|
||||
<H.Th @isVisuallyHidden={{true}}>
|
||||
{{t 'titles.actions'}}
|
||||
</H.Th>
|
||||
</H.Tr>
|
||||
</:head>
|
||||
|
||||
<dd.ToggleIcon
|
||||
@icon='more-horizontal'
|
||||
@text={{t 'actions.manage'}}
|
||||
@hasChevron={{false}}
|
||||
@size='small'
|
||||
/>
|
||||
{{#if perms.canUpdate}}
|
||||
<dd.Interactive
|
||||
@route='scopes.scope.aliases.alias'
|
||||
@model={{B.data.alias.id}}
|
||||
>
|
||||
{{t 'actions.edit'}}
|
||||
</dd.Interactive>
|
||||
{{#if B.data.alias.destination_id}}
|
||||
<:body as |B|>
|
||||
{{#each @model.aliases as |data|}}
|
||||
<B.Tr data-test-alias-row={{data.alias.id}}>
|
||||
<B.Td>
|
||||
{{#if (can 'read model' data.alias)}}
|
||||
<LinkTo
|
||||
@route='scopes.scope.aliases.alias'
|
||||
@model={{data.alias.id}}
|
||||
>
|
||||
{{data.alias.value}}
|
||||
</LinkTo>
|
||||
{{else}}
|
||||
<Hds::Text::Body @tag='p'>
|
||||
{{data.alias.value}}
|
||||
</Hds::Text::Body>
|
||||
{{/if}}
|
||||
{{#if data.alias.description}}
|
||||
<Hds::Text::Body @tag='p'>
|
||||
{{data.alias.description}}
|
||||
</Hds::Text::Body>
|
||||
{{/if}}
|
||||
</B.Td>
|
||||
<B.Td>
|
||||
{{#if data.target.displayName}}
|
||||
<div class='aliases-list-cell-item'>
|
||||
<span><Hds::Icon
|
||||
@name='target'
|
||||
@isInline={{true}}
|
||||
/></span>
|
||||
<span> {{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 data.alias.destination_id}}
|
||||
<Hds::Copy::Snippet
|
||||
@textToCopy={{data.alias.destination_id}}
|
||||
@color='secondary'
|
||||
/>
|
||||
{{/if}}
|
||||
</B.Td>
|
||||
<B.Td>
|
||||
{{#let
|
||||
(hash
|
||||
canUpdate=(can 'update model' data.alias)
|
||||
canDelete=(can 'delete model' data.alias)
|
||||
)
|
||||
as |perms|
|
||||
}}
|
||||
{{#if (or perms.canUpdate perms.canDelete)}}
|
||||
<Hds::Dropdown as |dd|>
|
||||
|
||||
<dd.ToggleIcon
|
||||
@icon='more-horizontal'
|
||||
@text={{t 'actions.manage'}}
|
||||
@hasChevron={{false}}
|
||||
@size='small'
|
||||
/>
|
||||
{{#if perms.canUpdate}}
|
||||
<dd.Interactive
|
||||
@color='action'
|
||||
{{on 'click' (fn this.clearAlias B.data.alias)}}
|
||||
@route='scopes.scope.aliases.alias'
|
||||
@model={{data.alias.id}}
|
||||
>
|
||||
{{t 'actions.clear'}}
|
||||
{{t 'actions.edit'}}
|
||||
</dd.Interactive>
|
||||
{{#if data.alias.destination_id}}
|
||||
<dd.Interactive
|
||||
@color='action'
|
||||
{{on 'click' (fn this.clearAlias data.alias)}}
|
||||
>
|
||||
{{t 'actions.clear'}}
|
||||
</dd.Interactive>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
{{#if perms.canDelete}}
|
||||
<dd.Interactive
|
||||
@color='critical'
|
||||
{{on 'click' (fn this.deleteAlias data.alias)}}
|
||||
>
|
||||
{{t 'actions.delete'}}
|
||||
</dd.Interactive>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</Hds::Dropdown>
|
||||
|
||||
{{#if perms.canDelete}}
|
||||
<dd.Interactive
|
||||
@color='critical'
|
||||
{{on 'click' (fn this.deleteAlias B.data.alias)}}
|
||||
>
|
||||
{{t 'actions.delete'}}
|
||||
</dd.Interactive>
|
||||
{{/if}}
|
||||
</Hds::Dropdown>
|
||||
|
||||
{{/if}}
|
||||
{{/let}}
|
||||
</B.Td>
|
||||
</B.Tr>
|
||||
{{/if}}
|
||||
{{/let}}
|
||||
</B.Td>
|
||||
</B.Tr>
|
||||
{{/each}}
|
||||
</:body>
|
||||
</Hds::Table>
|
||||
<Rose::Pagination
|
||||
|
||||
@@ -10,6 +10,7 @@ import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
|
||||
import { setupIndexedDb } from 'api/test-support/helpers/indexed-db';
|
||||
import { authenticateSession } from 'ember-simple-auth/test-support';
|
||||
import * as commonSelectors from 'admin/tests/helpers/selectors';
|
||||
import { faker } from '@faker-js/faker';
|
||||
|
||||
module('Acceptance | aliases | list', function (hooks) {
|
||||
setupApplicationTest(hooks);
|
||||
@@ -234,4 +235,85 @@ module('Acceptance | aliases | list', function (hooks) {
|
||||
.dom(NO_RESULTS_MSG_SELECTOR)
|
||||
.includesText(intl.t('titles.no-results-found'));
|
||||
});
|
||||
|
||||
test('aliases are sorted by created_time descending by default', async function (assert) {
|
||||
this.server.schema.aliases.all().destroy();
|
||||
|
||||
const years = ['2026', '2025', '2024', '2023'];
|
||||
faker.helpers.shuffle(years).map((year) => {
|
||||
return this.server.create('alias', {
|
||||
id: `alt_${year}`,
|
||||
value: `Alias ${year}`,
|
||||
created_time: `${year}-01-01T00:00:00Z`,
|
||||
scope: instances.scopes.global,
|
||||
destination_id: instances.target.id,
|
||||
});
|
||||
});
|
||||
|
||||
await visit(urls.aliases);
|
||||
|
||||
assert.dom(commonSelectors.TABLE_ROWS).exists({ count: years.length });
|
||||
assert.dom(commonSelectors.TABLE_ROW(1)).containsText('Alias 2026');
|
||||
assert.dom(commonSelectors.TABLE_ROW(2)).containsText('Alias 2025');
|
||||
assert.dom(commonSelectors.TABLE_ROW(3)).containsText('Alias 2024');
|
||||
assert.dom(commonSelectors.TABLE_ROW(4)).containsText('Alias 2023');
|
||||
});
|
||||
|
||||
test.each(
|
||||
'sorting',
|
||||
{
|
||||
'on value': {
|
||||
attribute: {
|
||||
key: 'value',
|
||||
values: ['alpha', 'beta', 'delta', 'gamma', 'epsilon'],
|
||||
},
|
||||
expectedAscendingSort: ['alpha', 'beta', 'delta', 'epsilon', 'gamma'],
|
||||
column: 1,
|
||||
},
|
||||
},
|
||||
async function (assert, input) {
|
||||
this.server.schema.aliases.all().destroy();
|
||||
faker.helpers.shuffle(input.attribute.values).forEach((value) => {
|
||||
this.server.create('alias', {
|
||||
[input.attribute.key]: value,
|
||||
scope: instances.scopes.global,
|
||||
destination_id: instances.target.id,
|
||||
});
|
||||
});
|
||||
|
||||
await visit(urls.aliases);
|
||||
|
||||
// click the sort button again to sort in ascending order
|
||||
await click(commonSelectors.TABLE_SORT_BTN(input.column));
|
||||
assert.true(currentURL().includes('sortDirection=asc'));
|
||||
assert.true(
|
||||
currentURL().includes(`sortAttribute=${input.attribute.key}`),
|
||||
);
|
||||
assert
|
||||
.dom(commonSelectors.TABLE_SORT_BTN_ARROW_UP(input.column))
|
||||
.isVisible();
|
||||
assert
|
||||
.dom(commonSelectors.TABLE_ROWS)
|
||||
.isVisible({ count: input.attribute.values.length });
|
||||
|
||||
input.expectedAscendingSort.forEach((expected, index) => {
|
||||
// nth-child index starts at 1
|
||||
assert.dom(commonSelectors.TABLE_ROW(index + 1)).containsText(expected);
|
||||
});
|
||||
|
||||
// click the sort button again to sort in descending order
|
||||
await click(commonSelectors.TABLE_SORT_BTN(input.column));
|
||||
assert.true(currentURL().includes('sortDirection=desc'));
|
||||
assert.true(
|
||||
currentURL().includes(`sortAttribute=${input.attribute.key}`),
|
||||
);
|
||||
assert
|
||||
.dom(commonSelectors.TABLE_SORT_BTN_ARROW_DOWN(input.column))
|
||||
.isVisible();
|
||||
input.expectedAscendingSort.toReversed().forEach((expected, index) => {
|
||||
// nth-child index starts at 1
|
||||
assert.dom(commonSelectors.TABLE_ROW(index + 1)).containsText(expected);
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
Reference in new issue
Block a user