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'}}