diff --git a/changelog/24103.txt b/changelog/24103.txt new file mode 100644 index 0000000000..f86bfd9969 --- /dev/null +++ b/changelog/24103.txt @@ -0,0 +1,3 @@ +```release-note:improvement +ui: Sort list view of entities and aliases alphabetically using the item name +``` diff --git a/ui/app/routes/vault/cluster/access/identity/aliases/index.js b/ui/app/routes/vault/cluster/access/identity/aliases/index.js index a20d144806..de17b51ba6 100644 --- a/ui/app/routes/vault/cluster/access/identity/aliases/index.js +++ b/ui/app/routes/vault/cluster/access/identity/aliases/index.js @@ -13,6 +13,7 @@ export default Route.extend(ListRoute, { responsePath: 'data.keys', page: params.page, pageFilter: params.pageFilter, + sortBy: 'name', }) .catch((err) => { if (err.httpStatus === 404) { diff --git a/ui/app/routes/vault/cluster/access/identity/index.js b/ui/app/routes/vault/cluster/access/identity/index.js index 5414932046..eb05e16d75 100644 --- a/ui/app/routes/vault/cluster/access/identity/index.js +++ b/ui/app/routes/vault/cluster/access/identity/index.js @@ -13,6 +13,7 @@ export default Route.extend(ListRoute, { responsePath: 'data.keys', page: params.page, pageFilter: params.pageFilter, + sortBy: 'name', }) .catch((err) => { if (err.httpStatus === 404) { diff --git a/ui/app/serializers/identity/_base.js b/ui/app/serializers/identity/_base.js index 89efa631a4..2d2d729c93 100644 --- a/ui/app/serializers/identity/_base.js +++ b/ui/app/serializers/identity/_base.js @@ -4,6 +4,10 @@ import ApplicationSerializer from '../application'; export default ApplicationSerializer.extend({ normalizeItems(payload) { if (payload.data.keys && Array.isArray(payload.data.keys)) { + if (typeof payload.data.keys[0] !== 'string') { + // If keys is not an array of strings, it was already normalized into objects in extractLazyPaginatedData + return payload.data.keys; + } return payload.data.keys.map((key) => { const model = payload.data.key_info[key]; model.id = key; diff --git a/ui/app/serializers/identity/entity-alias.js b/ui/app/serializers/identity/entity-alias.js index c7246dba8a..064b400252 100644 --- a/ui/app/serializers/identity/entity-alias.js +++ b/ui/app/serializers/identity/entity-alias.js @@ -1,2 +1,13 @@ import IdentitySerializer from './_base'; -export default IdentitySerializer.extend(); +export default IdentitySerializer.extend({ + extractLazyPaginatedData(payload) { + return payload.data.keys.map((key) => { + const model = payload.data.key_info[key]; + model.id = key; + if (payload.backend) { + model.backend = payload.backend; + } + return model; + }); + }, +}); diff --git a/ui/app/serializers/identity/entity.js b/ui/app/serializers/identity/entity.js index 7dcea71eee..3ded08006f 100644 --- a/ui/app/serializers/identity/entity.js +++ b/ui/app/serializers/identity/entity.js @@ -7,4 +7,14 @@ export default IdentitySerializer.extend(EmbeddedRecordsMixin, { attrs: { aliases: { embedded: 'always' }, }, + extractLazyPaginatedData(payload) { + return payload.data.keys.map((key) => { + const model = payload.data.key_info[key]; + model.id = key; + if (payload.backend) { + model.backend = payload.backend; + } + return model; + }); + }, }); diff --git a/ui/app/services/store.js b/ui/app/services/store.js index fb73fe32f7..b57ba178cc 100644 --- a/ui/app/services/store.js +++ b/ui/app/services/store.js @@ -7,6 +7,7 @@ import { assert } from '@ember/debug'; import { set, get, computed } from '@ember/object'; import clamp from 'vault/utils/clamp'; import config from 'vault/config/environment'; +import sortObjects from 'vault/utils/sort-objects'; const { DEFAULT_PAGE_SIZE } = config.APP; @@ -176,11 +177,12 @@ export default Store.extend({ // store data cache as { response, dataset} // also populated `lazyCaches` attribute storeDataset(modelName, query, response, array) { - const dataSet = { + const dataset = query.sortBy ? sortObjects(array, query.sortBy) : array; + const value = { response, - dataset: array, + dataset, }; - this.setLazyCacheForModel(modelName, query, dataSet); + this.setLazyCacheForModel(modelName, query, value); }, clearDataset(modelName) { diff --git a/ui/app/utils/sort-objects.js b/ui/app/utils/sort-objects.js new file mode 100644 index 0000000000..7677cd2605 --- /dev/null +++ b/ui/app/utils/sort-objects.js @@ -0,0 +1,14 @@ +export default function sortObjects(array, key) { + if (Array.isArray(array) && array?.every((e) => e[key] && typeof e[key] === 'string')) { + return array.sort((a, b) => { + // ignore upper vs lowercase + const valueA = a[key].toUpperCase(); + const valueB = b[key].toUpperCase(); + if (valueA < valueB) return -1; + if (valueA > valueB) return 1; + return 0; + }); + } + // if not sortable, return original array + return array; +} diff --git a/ui/tests/unit/utils/sort-objects-test.js b/ui/tests/unit/utils/sort-objects-test.js new file mode 100644 index 0000000000..a7326c7759 --- /dev/null +++ b/ui/tests/unit/utils/sort-objects-test.js @@ -0,0 +1,86 @@ +import sortObjects from 'vault/utils/sort-objects'; +import { module, test } from 'qunit'; + +module('Unit | Utility | sort-objects', function () { + test('it sorts array of objects', function (assert) { + const originalArray = [ + { foo: 'grape', bar: 'third' }, + { foo: 'banana', bar: 'second' }, + { foo: 'lemon', bar: 'fourth' }, + { foo: 'apple', bar: 'first' }, + ]; + const expectedArray = [ + { bar: 'first', foo: 'apple' }, + { bar: 'second', foo: 'banana' }, + { bar: 'third', foo: 'grape' }, + { bar: 'fourth', foo: 'lemon' }, + ]; + + assert.propEqual(sortObjects(originalArray, 'foo'), expectedArray, 'it sorts array of objects'); + + const originalWithNumbers = [ + { foo: 'Z', bar: 'fourth' }, + { foo: '1', bar: 'first' }, + { foo: '2', bar: 'second' }, + { foo: 'A', bar: 'third' }, + ]; + const expectedWithNumbers = [ + { bar: 'first', foo: '1' }, + { bar: 'second', foo: '2' }, + { bar: 'third', foo: 'A' }, + { bar: 'fourth', foo: 'Z' }, + ]; + assert.propEqual( + sortObjects(originalWithNumbers, 'foo'), + expectedWithNumbers, + 'it sorts strings with numbers and letters' + ); + }); + + test('it disregards capitalization', function (assert) { + // sort() arranges capitalized values before lowercase, the helper removes case by making all strings toUppercase() + const originalArray = [ + { foo: 'something-a', bar: 'third' }, + { foo: 'D-something', bar: 'second' }, + { foo: 'SOMETHING-b', bar: 'fourth' }, + { foo: 'a-something', bar: 'first' }, + ]; + const expectedArray = [ + { bar: 'first', foo: 'a-something' }, + { bar: 'second', foo: 'D-something' }, + { bar: 'third', foo: 'something-a' }, + { bar: 'fourth', foo: 'SOMETHING-b' }, + ]; + + assert.propEqual( + sortObjects(originalArray, 'foo'), + expectedArray, + 'it sorts array of objects regardless of capitalization' + ); + }); + + test('it fails gracefully', function (assert) { + const originalArray = [ + { foo: 'b', bar: 'two' }, + { foo: 'a', bar: 'one' }, + ]; + assert.propEqual( + sortObjects(originalArray, 'someKey'), + originalArray, + 'it returns original array if key does not exist' + ); + assert.deepEqual(sortObjects('not an array'), 'not an array', 'it returns original arg if not an array'); + + const notStrings = [ + { foo: '1', bar: 'third' }, + { foo: 'Z', bar: 'second' }, + { foo: 1, bar: 'fourth' }, + { foo: 2, bar: 'first' }, + ]; + assert.propEqual( + sortObjects(notStrings, 'foo'), + notStrings, + 'it returns original array if values are not all strings' + ); + }); +});