diff --git a/addons/api/addon/utils/sort-results.js b/addons/api/addon/utils/sort-results.js
index 2ec441b6f..da876b4d6 100644
--- a/addons/api/addon/utils/sort-results.js
+++ b/addons/api/addon/utils/sort-results.js
@@ -42,7 +42,7 @@ export const sortResults = (results, { querySort, schema }) => {
// Default sort direction is ascending unless we are sorting by `created_time` (default sort attribute)
const defaultSortDirection =
- sortAttribute === SORT_DEFAULT_ATTRIBUTE
+ sortAttribute === SORT_DEFAULT_ATTRIBUTE && !querySort.sortFunction
? SORT_DIRECTION_DESCENDING
: SORT_DIRECTION_ASCENDING;
const sortDirection = querySort.direction || defaultSortDirection;
@@ -55,6 +55,17 @@ export const sortResults = (results, { querySort, schema }) => {
throw new Error('Invalid sort direction');
}
+ // Execute custom sort function if one is provided.
+ if (querySort.sortFunction) {
+ return results.toSorted((a, b) => {
+ const sortResult = querySort.sortFunction(a, b);
+
+ return sortDirection === SORT_DIRECTION_ASCENDING
+ ? sortResult
+ : -1 * sortResult;
+ });
+ }
+
const sortAttributeDataType = schema.attributes.get(sortAttribute)?.type;
const sortFunction =
diff --git a/addons/api/tests/unit/handlers/indexed-db-handler-test.js b/addons/api/tests/unit/handlers/indexed-db-handler-test.js
index c06f65720..d4cdbc0b1 100644
--- a/addons/api/tests/unit/handlers/indexed-db-handler-test.js
+++ b/addons/api/tests/unit/handlers/indexed-db-handler-test.js
@@ -306,6 +306,54 @@ module('Unit | Handler | indexed-db-handler', function (hooks) {
['Alias 001', 'Alias 002', 'Alias 003', 'Alias 004'],
);
});
+
+ test('it sorts by descending, then ascending `name` using custom string sorting', async function (assert) {
+ const alias1 = this.server.create('alias', { name: 'alias1' });
+ const alias2 = this.server.create('alias', { name: 'alias2' });
+ const alias3 = this.server.create('alias', { name: 'alias3' });
+ const alias4 = this.server.create('alias', { name: 'alias4' });
+
+ const sortName = (recordA, recordB) => {
+ const nameMap = {
+ alias3: 'Alpha',
+ alias4: 'Beta',
+ alias1: 'Delta',
+ alias2: 'Epsilon',
+ };
+ return String(nameMap[recordA.attributes.name]).localeCompare(
+ String(nameMap[recordB.attributes.name]),
+ );
+ };
+
+ const shuffledAliases = faker.helpers.shuffle([
+ alias1,
+ alias2,
+ alias3,
+ alias4,
+ ]);
+
+ this.server.get(
+ 'aliases',
+ createPaginatedResponseHandler(shuffledAliases, {
+ pageSize: 1,
+ }),
+ );
+
+ const resultsDesc = await store.query('alias', {
+ query: { sort: { sortFunction: sortName, direction: 'desc' } },
+ });
+ const resultsAsc = await store.query('alias', {
+ query: { sort: { sortFunction: sortName, direction: 'asc' } },
+ });
+ assert.deepEqual(
+ resultsDesc.map(({ name }) => name),
+ ['alias2', 'alias1', 'alias4', 'alias3'],
+ );
+ assert.deepEqual(
+ resultsAsc.map(({ name }) => name),
+ ['alias3', 'alias4', 'alias1', 'alias2'],
+ );
+ });
});
module('option: pushToStore', function (hooks) {
diff --git a/addons/api/tests/unit/utils/sort-results-test.js b/addons/api/tests/unit/utils/sort-results-test.js
index bc3678cc9..b13746b35 100644
--- a/addons/api/tests/unit/utils/sort-results-test.js
+++ b/addons/api/tests/unit/utils/sort-results-test.js
@@ -421,4 +421,68 @@ module('Unit | Utility | sortResults', function (hooks) {
);
});
});
+
+ module('custom function sorting', function () {
+ const result01 = createJsonApiMockRecord('id_01', 'target', {
+ name: 'target1',
+ });
+ const result02 = createJsonApiMockRecord('id_02', 'target', {
+ name: 'target2',
+ });
+ const result03 = createJsonApiMockRecord('id_03', 'target', {
+ name: 'target3',
+ });
+ const result04 = createJsonApiMockRecord('id_04', 'target', {
+ name: 'target4',
+ });
+
+ const sortFunction = (recordA, recordB) => {
+ const nameMap = {
+ target3: 'Alpha',
+ target4: 'Beta',
+ target1: 'Delta',
+ target2: 'Epsilon',
+ };
+ return String(nameMap[recordA.attributes.name]).localeCompare(
+ String(nameMap[recordB.attributes.name]),
+ );
+ };
+ const schema = { attributes: new Map() };
+
+ test.each(
+ 'it sorts by `name` using custom sorting function',
+ {
+ 'ascending (default)': {
+ querySort: { sortFunction },
+ expectedResults: ['target3', 'target4', 'target1', 'target2'],
+ },
+ descending: {
+ querySort: { sortFunction, direction: 'desc' },
+ expectedResults: ['target2', 'target1', 'target4', 'target3'],
+ },
+ ascending: {
+ querySort: { sortFunction, direction: 'asc' },
+ expectedResults: ['target3', 'target4', 'target1', 'target2'],
+ },
+ },
+ function (assert, input) {
+ const shuffledResults = faker.helpers.shuffle([
+ result01,
+ result02,
+ result03,
+ result04,
+ ]);
+ schema.attributes.set('name', { type: 'string' });
+ const sortedResults = sortResults(shuffledResults, {
+ querySort: input.querySort,
+ schema,
+ });
+
+ assert.deepEqual(
+ sortedResults.map(({ attributes: { name } }) => name),
+ input.expectedResults,
+ );
+ },
+ );
+ });
});
diff --git a/ui/admin/app/routes/scopes/scope/credential-stores/index.js b/ui/admin/app/routes/scopes/scope/credential-stores/index.js
index 7af327672..e905ee83a 100644
--- a/ui/admin/app/routes/scopes/scope/credential-stores/index.js
+++ b/ui/admin/app/routes/scopes/scope/credential-stores/index.js
@@ -6,6 +6,7 @@
import Route from '@ember/routing/route';
import { service } from '@ember/service';
import { restartableTask, timeout } from 'ember-concurrency';
+import { sortNameWithIdFallback } from 'admin/utils/sort-name-with-id-fallback';
export default class ScopesScopeCredentialStoresIndexRoute extends Route {
// =services
@@ -77,10 +78,10 @@ export default class ScopesScopeCredentialStoresIndexRoute extends Route {
filters.type.push({ equals: type });
});
- const sort = {
- attribute: sortAttribute,
- direction: sortDirection,
- };
+ const sort =
+ sortAttribute === 'name'
+ ? { sortFunction: sortNameWithIdFallback, direction: sortDirection }
+ : { attribute: sortAttribute, direction: sortDirection };
let credentialStores;
let totalItems = 0;
diff --git a/ui/admin/app/routes/scopes/scope/groups/index.js b/ui/admin/app/routes/scopes/scope/groups/index.js
index ee098eff1..84c9dc83c 100644
--- a/ui/admin/app/routes/scopes/scope/groups/index.js
+++ b/ui/admin/app/routes/scopes/scope/groups/index.js
@@ -6,6 +6,7 @@
import Route from '@ember/routing/route';
import { service } from '@ember/service';
import { restartableTask, timeout } from 'ember-concurrency';
+import { sortNameWithIdFallback } from 'admin/utils/sort-name-with-id-fallback';
export default class ScopesScopeGroupsIndexRoute extends Route {
// =services
@@ -63,15 +64,16 @@ export default class ScopesScopeGroupsIndexRoute extends Route {
const scope = this.modelFor('scopes.scope');
const { id: scope_id } = scope;
+ const filters = { scope_id: [{ equals: scope_id }] };
+
+ const sort =
+ sortAttribute === 'name'
+ ? { sortFunction: sortNameWithIdFallback, direction: sortDirection }
+ : { attribute: sortAttribute, direction: sortDirection };
+
let groups = [];
let totalItems = 0;
let doGroupsExist = false;
- const filters = { scope_id: [{ equals: scope_id }] };
- const sort = {
- attribute: sortAttribute,
- direction: sortDirection,
- };
-
if (this.can.can('list model', scope, { collection: 'groups' })) {
groups = await this.store.query('group', {
scope_id,
diff --git a/ui/admin/app/routes/scopes/scope/host-catalogs/index.js b/ui/admin/app/routes/scopes/scope/host-catalogs/index.js
index ee4bb32f3..dfeccdd74 100644
--- a/ui/admin/app/routes/scopes/scope/host-catalogs/index.js
+++ b/ui/admin/app/routes/scopes/scope/host-catalogs/index.js
@@ -6,6 +6,7 @@
import Route from '@ember/routing/route';
import { service } from '@ember/service';
import { restartableTask, timeout } from 'ember-concurrency';
+import { sortNameWithIdFallback } from 'admin/utils/sort-name-with-id-fallback';
export default class ScopesScopeHostCatalogsIndexRoute extends Route {
// =services
@@ -70,10 +71,10 @@ export default class ScopesScopeHostCatalogsIndexRoute extends Route {
'plugin.name': [],
};
- const sort = {
- attribute: sortAttribute,
- direction: sortDirection,
- };
+ const sort =
+ sortAttribute === 'name'
+ ? { sortFunction: sortNameWithIdFallback, direction: sortDirection }
+ : { attribute: sortAttribute, direction: sortDirection };
let hostCatalogs;
let totalItems = 0;
diff --git a/ui/admin/app/routes/scopes/scope/roles/index.js b/ui/admin/app/routes/scopes/scope/roles/index.js
index 764205d23..3706ff72f 100644
--- a/ui/admin/app/routes/scopes/scope/roles/index.js
+++ b/ui/admin/app/routes/scopes/scope/roles/index.js
@@ -6,6 +6,8 @@
import Route from '@ember/routing/route';
import { service } from '@ember/service';
import { restartableTask, timeout } from 'ember-concurrency';
+import { sortNameWithIdFallback } from 'admin/utils/sort-name-with-id-fallback';
+import { GRANT_SCOPE_THIS } from 'api/models/role';
export default class ScopesScopeRolesIndexRoute extends Route {
// =attributes
@@ -31,6 +33,21 @@ export default class ScopesScopeRolesIndexRoute extends Route {
},
};
+ customSortFunction = {
+ name: sortNameWithIdFallback,
+ grant_scope_ids: (recordA, recordB) => {
+ const a = recordA.attributes?.grant_scope_ids.includes(GRANT_SCOPE_THIS);
+ const b = recordB.attributes?.grant_scope_ids.includes(GRANT_SCOPE_THIS);
+ if (a === b) {
+ return 0;
+ } else if (a) {
+ return 1;
+ } else {
+ return -1;
+ }
+ },
+ };
+
// =services
@service can;
@@ -66,10 +83,11 @@ export default class ScopesScopeRolesIndexRoute extends Route {
const filters = {
scope_id: [{ equals: scope_id }],
};
- const sort = {
- attribute: sortAttribute,
- direction: sortDirection,
- };
+
+ const sortFunction = this.customSortFunction[sortAttribute];
+ const sort = sortFunction
+ ? { sortFunction, direction: sortDirection }
+ : { attribute: sortAttribute, direction: sortDirection };
let roles;
let totalItems = 0;
diff --git a/ui/admin/app/routes/scopes/scope/scopes/index.js b/ui/admin/app/routes/scopes/scope/scopes/index.js
index a843f7f52..9477502a1 100644
--- a/ui/admin/app/routes/scopes/scope/scopes/index.js
+++ b/ui/admin/app/routes/scopes/scope/scopes/index.js
@@ -6,6 +6,7 @@
import Route from '@ember/routing/route';
import { service } from '@ember/service';
import { restartableTask, timeout } from 'ember-concurrency';
+import { sortNameWithIdFallback } from 'admin/utils/sort-name-with-id-fallback';
export default class ScopesScopeScopesIndexRoute extends Route {
// =attributes
@@ -67,10 +68,10 @@ export default class ScopesScopeScopesIndexRoute extends Route {
scope_id: [{ equals: scope_id }],
};
- const sort = {
- attribute: sortAttribute,
- direction: sortDirection,
- };
+ const sort =
+ sortAttribute === 'name'
+ ? { sortFunction: sortNameWithIdFallback, direction: sortDirection }
+ : { attribute: sortAttribute, direction: sortDirection };
const subScopes = await this.store.query('scope', {
scope_id,
diff --git a/ui/admin/app/routes/scopes/scope/session-recordings/index.js b/ui/admin/app/routes/scopes/scope/session-recordings/index.js
index 2b00a6cff..252ea5cc5 100644
--- a/ui/admin/app/routes/scopes/scope/session-recordings/index.js
+++ b/ui/admin/app/routes/scopes/scope/session-recordings/index.js
@@ -7,12 +7,18 @@ import Route from '@ember/routing/route';
import { service } from '@ember/service';
import { action } from '@ember/object';
import { restartableTask, timeout } from 'ember-concurrency';
+import {
+ STATE_SESSION_RECORDING_STARTED,
+ STATE_SESSION_RECORDING_AVAILABLE,
+ STATE_SESSION_RECORDING_UNKNOWN,
+} from 'api/models/session-recording';
export default class ScopesScopeSessionRecordingsIndexRoute extends Route {
// =services
@service store;
@service router;
@service can;
+ @service intl;
// =attributes
@@ -65,6 +71,17 @@ export default class ScopesScopeSessionRecordingsIndexRoute extends Route {
return this.retrieveData.perform({ ...params, useDebounce });
}
+ sortState = (recordA, recordB) => {
+ const stateMap = {
+ [STATE_SESSION_RECORDING_AVAILABLE]: this.intl.t('states.completed'),
+ [STATE_SESSION_RECORDING_STARTED]: this.intl.t('states.recording'),
+ [STATE_SESSION_RECORDING_UNKNOWN]: this.intl.t('states.failed'),
+ };
+ return String(stateMap[recordA.attributes.state]).localeCompare(
+ String(stateMap[recordB.attributes.state]),
+ );
+ };
+
retrieveData = restartableTask(
async ({
search,
@@ -105,15 +122,16 @@ export default class ScopesScopeSessionRecordingsIndexRoute extends Route {
filters['create_time_values.target.id'].push({ equals: target });
});
+ const sort =
+ sortAttribute === 'state'
+ ? { sortFunction: this.sortState, direction: sortDirection }
+ : { attribute: sortAttribute, direction: sortDirection };
+
if (
this.can.can('list scope', scope, {
collection: 'session-recordings',
})
) {
- const sort = {
- attribute: sortAttribute,
- direction: sortDirection,
- };
const queryOptions = {
scope_id,
recursive: true,
diff --git a/ui/admin/app/routes/scopes/scope/targets/index.js b/ui/admin/app/routes/scopes/scope/targets/index.js
index f6e8d2ba9..8d77121d2 100644
--- a/ui/admin/app/routes/scopes/scope/targets/index.js
+++ b/ui/admin/app/routes/scopes/scope/targets/index.js
@@ -5,11 +5,12 @@
import Route from '@ember/routing/route';
import { service } from '@ember/service';
+import { restartableTask, timeout } from 'ember-concurrency';
import {
STATUS_SESSION_ACTIVE,
STATUS_SESSION_PENDING,
} from 'api/models/session';
-import { restartableTask, timeout } from 'ember-concurrency';
+import { TYPE_TARGET_SSH, TYPE_TARGET_TCP } from 'api/models/target';
export default class ScopesScopeTargetsIndexRoute extends Route {
// =services
@@ -17,6 +18,7 @@ export default class ScopesScopeTargetsIndexRoute extends Route {
@service can;
@service store;
@service session;
+ @service intl;
// =attributes
@@ -62,6 +64,16 @@ export default class ScopesScopeTargetsIndexRoute extends Route {
return this.retrieveData.perform({ ...params, useDebounce });
}
+ sortType = (recordA, recordB) => {
+ const typeMap = {
+ [TYPE_TARGET_SSH]: this.intl.t('resources.target.types.ssh'),
+ [TYPE_TARGET_TCP]: this.intl.t('resources.target.types.tcp'),
+ };
+ return String(typeMap[recordA.attributes.type]).localeCompare(
+ String(typeMap[recordB.attributes.type]),
+ );
+ };
+
retrieveData = restartableTask(
async ({
search,
@@ -105,14 +117,15 @@ export default class ScopesScopeTargetsIndexRoute extends Route {
this.addActiveSessionFilters(filters, availableSessions, sessions);
}
+ const sort =
+ sortAttribute === 'type'
+ ? { sortFunction: this.sortType, direction: sortDirection }
+ : { attribute: sortAttribute, direction: sortDirection };
+
let targets;
let totalItems = 0;
let doTargetsExist = false;
if (this.can.can('list model', scope, { collection: 'targets' })) {
- const sort = {
- attribute: sortAttribute,
- direction: sortDirection,
- };
targets = await this.store.query('target', {
scope_id,
query: { search, filters, sort },
diff --git a/ui/admin/app/routes/scopes/scope/users/index.js b/ui/admin/app/routes/scopes/scope/users/index.js
index da4b4e616..34b458dbd 100644
--- a/ui/admin/app/routes/scopes/scope/users/index.js
+++ b/ui/admin/app/routes/scopes/scope/users/index.js
@@ -6,6 +6,7 @@
import Route from '@ember/routing/route';
import { service } from '@ember/service';
import { restartableTask, timeout } from 'ember-concurrency';
+import { sortNameWithIdFallback } from 'admin/utils/sort-name-with-id-fallback';
export default class ScopesScopeUsersIndexRoute extends Route {
// =attributes
@@ -68,10 +69,10 @@ export default class ScopesScopeUsersIndexRoute extends Route {
scope_id: [{ equals: scope_id }],
};
- const sort = {
- attribute: sortAttribute,
- direction: sortDirection,
- };
+ const sort =
+ sortAttribute === 'name'
+ ? { sortFunction: sortNameWithIdFallback, direction: sortDirection }
+ : { attribute: sortAttribute, direction: sortDirection };
let users;
let totalItems = 0;
diff --git a/ui/admin/app/templates/scopes/scope/roles/index.hbs b/ui/admin/app/templates/scopes/scope/roles/index.hbs
index 14f39513a..024038c61 100644
--- a/ui/admin/app/templates/scopes/scope/roles/index.hbs
+++ b/ui/admin/app/templates/scopes/scope/roles/index.hbs
@@ -59,9 +59,10 @@
>
{{t 'form.name.label'}}
-
- {{t 'resources.role.titles.grants-applied'}}
-
+ {{t 'resources.role.titles.grants-applied'}}