From e2aa683e997420e53f06e211dc115289d2a04937 Mon Sep 17 00:00:00 2001 From: Chelsea Shaw <82459713+hashishaw@users.noreply.github.com> Date: Wed, 15 Nov 2023 14:58:12 -0600 Subject: [PATCH] UI: helper sort-objects to alphabetize list items (#24103) (#24145) * move list to component * use helper instead * add changelog * clarify changelog copy * delete components now that helper is in use * move helper to util, remove template helper invokation * add optional sorting to lazyPaginatedQuery based on sortBy query attribute * Add serialization to entity-alias and entity so that they can be sorted by name on list view * Same logic as base normalizeItems for extractLazyPaginatedData so that metadata shows on list * Add headers --------- --- changelog/24103.txt | 3 + .../cluster/access/identity/aliases/index.js | 1 + .../vault/cluster/access/identity/index.js | 1 + ui/app/serializers/identity/_base.js | 4 + ui/app/serializers/identity/entity-alias.js | 13 ++- ui/app/serializers/identity/entity.js | 10 +++ ui/app/services/store.js | 8 +- ui/app/utils/sort-objects.js | 14 +++ ui/tests/unit/utils/sort-objects-test.js | 86 +++++++++++++++++++ 9 files changed, 136 insertions(+), 4 deletions(-) create mode 100644 changelog/24103.txt create mode 100644 ui/app/utils/sort-objects.js create mode 100644 ui/tests/unit/utils/sort-objects-test.js diff --git a/changelog/24103.txt b/changelog/24103.txt new file mode 100644 index 000000000000..f86bfd996949 --- /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 3455935e5b7e..fc38ed7a162f 100644 --- a/ui/app/routes/vault/cluster/access/identity/aliases/index.js +++ b/ui/app/routes/vault/cluster/access/identity/aliases/index.js @@ -18,6 +18,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 f709021cd2aa..503e20f92e73 100644 --- a/ui/app/routes/vault/cluster/access/identity/index.js +++ b/ui/app/routes/vault/cluster/access/identity/index.js @@ -18,6 +18,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 4d9fd80c4fbd..8c9854fe0873 100644 --- a/ui/app/serializers/identity/_base.js +++ b/ui/app/serializers/identity/_base.js @@ -9,6 +9,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 a0087fddd0da..bc0ebff51913 100644 --- a/ui/app/serializers/identity/entity-alias.js +++ b/ui/app/serializers/identity/entity-alias.js @@ -4,4 +4,15 @@ */ 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 20694a9fdda4..5d79369b0631 100644 --- a/ui/app/serializers/identity/entity.js +++ b/ui/app/serializers/identity/entity.js @@ -12,4 +12,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 2d627a205bd9..30cabb1e6491 100644 --- a/ui/app/services/store.js +++ b/ui/app/services/store.js @@ -12,6 +12,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; @@ -185,11 +186,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 000000000000..7677cd260577 --- /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 000000000000..a7326c775948 --- /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' + ); + }); +});