diff --git a/changelog/_9577.txt b/changelog/_9577.txt new file mode 100644 index 0000000000..5b03ca9b57 --- /dev/null +++ b/changelog/_9577.txt @@ -0,0 +1,3 @@ +```release-note:improvement +ui/secrets: Updated filters on secret engines list to sort by path, engine type and version +``` diff --git a/ui/app/components/secret-engine/list.hbs b/ui/app/components/secret-engine/list.hbs index c9e83bfb31..accb408798 100644 --- a/ui/app/components/secret-engine/list.hbs +++ b/ui/app/components/secret-engine/list.hbs @@ -20,35 +20,84 @@ - - - + + + + + + + {{#each this.secretEngineArrayByType as |type|}} + {{type.name}} + {{/each}} + + + + {{#if this.engineTypeFilters.length}} + + + + {{#each this.secretEngineArrayByVersions as |backend|}} + + {{backend.version}} + + {{/each}} + {{else}} + + {{/if}} + + + + + {{#if (and (not this.engineTypeFilters) (not this.engineVersionFilters))}} + No filters applied: + + + + {{else}} + Filters applied: + {{#each this.engineTypeFilters as |type|}} + + {{/each}} + {{#each this.engineVersionFilters as |version|}} + + {{/each}} + - - - + {{/if}} + {{#each this.sortedDisplayableBackends as |backend|}}
{{#if backend.icon}} - + {{/if}} diff --git a/ui/app/components/secret-engine/list.ts b/ui/app/components/secret-engine/list.ts index 47a27f1ecc..ef13aa2425 100644 --- a/ui/app/components/secret-engine/list.ts +++ b/ui/app/components/secret-engine/list.ts @@ -38,10 +38,16 @@ export default class SecretEngineList extends Component { @service declare readonly version: VersionService; @tracked secretEngineOptions: Array | [] = []; - @tracked selectedEngineType = ''; - @tracked selectedEngineName = ''; @tracked engineToDisable: SecretsEngineResource | undefined = undefined; + @tracked engineTypeFilters: Array = []; + @tracked engineVersionFilters: Array = []; + @tracked searchText = ''; + + // search text for dropdown filters + @tracked typeSearchText = ''; + @tracked versionSearchText = ''; + get clusterName() { return this.version.clusterName; } @@ -52,28 +58,86 @@ export default class SecretEngineList extends Component { get sortedDisplayableBackends() { // show supported secret engines first and then organize those by id. - const sortedBackends = this.displayableBackends.sort( - (a, b) => Number(b.isSupportedBackend) - Number(a.isSupportedBackend) || a.id.localeCompare(b.id) - ); + let sortedBackends = this.displayableBackends + .slice() + .sort( + (a, b) => Number(b.isSupportedBackend) - Number(a.isSupportedBackend) || a.id.localeCompare(b.id) + ); - // return an options list to filter by engine type, ex: 'kv' - if (this.selectedEngineType) { - // check first if the user has also filtered by name. - if (this.selectedEngineName) { - return sortedBackends.filter((backend) => this.selectedEngineName === backend.id); - } - // otherwise filter by engine type - return sortedBackends.filter((backend) => this.selectedEngineType === backend.engineType); + // filters by engine type, ex: 'kv' + if (this.engineTypeFilters.length > 0) { + sortedBackends = sortedBackends.filter((backend) => + this.engineTypeFilters.includes(backend.engineType) + ); } - // return an options list to filter by engine name, ex: 'secret' - if (this.selectedEngineName) { - return sortedBackends.filter((backend) => this.selectedEngineName === backend.id); + // filters by engine version, ex: 'v1.21.0...' + if (this.engineVersionFilters.length > 0) { + sortedBackends = sortedBackends.filter((backend) => + this.engineVersionFilters.includes(backend.running_plugin_version) + ); + } + + // if there is search text, filter path name by that + if (this.searchText.trim() !== '') { + sortedBackends = sortedBackends.filter((backend) => + backend.path.toLowerCase().includes(this.searchText.toLowerCase()) + ); } // no filters, return full sorted list. return sortedBackends; } + // Returns filter options for engine type dropdown + get typeFilterOptions() { + // if there is search text, filter types by that + if (this.typeSearchText.trim() !== '') { + return this.displayableBackends.filter((backend) => + backend.engineType.toLowerCase().includes(this.typeSearchText.toLowerCase()) + ); + } + + return this.displayableBackends; + } + + // Returns filter options for version dropdown + get versionFilterOptions() { + // if there is search text, filter versions by that + if (this.versionSearchText.trim() !== '') { + // filtered by sorted backends array since an engine type filter has to be selected first + return this.sortedDisplayableBackends.filter((backend) => + backend.running_plugin_version.toLowerCase().includes(this.versionSearchText.toLowerCase()) + ); + } + return this.sortedDisplayableBackends; + } + + // Returns filtered engines list by type + get secretEngineArrayByType() { + const arrayOfAllEngineTypes = this.typeFilterOptions.map((modelObject) => modelObject.engineType); + // filter out repeated engineTypes (e.g. [kv, kv] => [kv]) + const arrayOfUniqueEngineTypes = [...new Set(arrayOfAllEngineTypes)]; + + return arrayOfUniqueEngineTypes.map((engineType) => ({ + name: engineType, + id: engineType, + icon: engineDisplayData(engineType)?.glyph ?? 'lock', + })); + } + + // Returns filtered engines list by version + get secretEngineArrayByVersions() { + const arrayOfAllEngineVersions = this.versionFilterOptions.map( + (modelObject) => modelObject.running_plugin_version + ); + // filter out repeated engineVersions (e.g. [1.0, 1.0] => [1.0]) + const arrayOfUniqueEngineVersions = [...new Set(arrayOfAllEngineVersions)]; + return arrayOfUniqueEngineVersions.map((version) => ({ + version, + id: version, + })); + } + generateToolTipText = (backend: SecretsEngineResource) => { const displayData = engineDisplayData(backend.type); @@ -96,35 +160,40 @@ export default class SecretEngineList extends Component { } }; - // Filtering & searching - get secretEngineArrayByType() { - const arrayOfAllEngineTypes = this.sortedDisplayableBackends.map((modelObject) => modelObject.engineType); - // filter out repeated engineTypes (e.g. [kv, kv] => [kv]) - const arrayOfUniqueEngineTypes = [...new Set(arrayOfAllEngineTypes)]; - - return arrayOfUniqueEngineTypes.map((engineType) => ({ - name: engineType, - id: engineType, - })); - } - - get secretEngineArrayByName() { - return this.sortedDisplayableBackends.map((modelObject) => ({ - name: modelObject.id, - id: modelObject.id, - })); + @action + setSearchText(type: string, event: Event) { + const target = event.target as HTMLInputElement; + if (type === 'type') { + this.typeSearchText = target.value; + } else if (type === 'version') { + this.versionSearchText = target.value; + } else { + this.searchText = target.value; + } } @action - filterEngineType(type: string[]) { - const [selectedType] = type; - this.selectedEngineType = selectedType || ''; + filterByEngineType(type: string) { + if (this.engineTypeFilters.includes(type)) { + this.engineTypeFilters = this.engineTypeFilters.filter((t) => t !== type); + } else { + this.engineTypeFilters = [...this.engineTypeFilters, type]; + } } @action - filterEngineName(name: string[]) { - const [selectedName] = name; - this.selectedEngineName = selectedName || ''; + filterByEngineVersion(version: string) { + if (this.engineVersionFilters.includes(version)) { + this.engineVersionFilters = this.engineVersionFilters.filter((v) => v !== version); + } else { + this.engineVersionFilters = [...this.engineVersionFilters, version]; + } + } + + @action + clearAllFilters() { + this.engineTypeFilters = []; + this.engineVersionFilters = []; } @dropTask diff --git a/ui/tests/acceptance/secret-engine-list-view-test.js b/ui/tests/acceptance/secret-engine-list-view-test.js index 984626eb30..8a1e5d1f3a 100644 --- a/ui/tests/acceptance/secret-engine-list-view-test.js +++ b/ui/tests/acceptance/secret-engine-list-view-test.js @@ -4,7 +4,6 @@ */ import { click, fillIn, currentRouteName, visit, currentURL } from '@ember/test-helpers'; -import { selectChoose } from 'ember-power-select/test-support'; import { module, test } from 'qunit'; import { setupApplicationTest } from 'ember-qunit'; import { v4 as uuidv4 } from 'uuid'; @@ -162,7 +161,7 @@ module('Acceptance | secret-engine list view', function (hooks) { await runCmd(mountEngineCmd('alicloud', enginePath)); await visit('/vault/secrets'); // to reduce flakiness, searching by engine name first in case there are pagination issues - await selectChoose(GENERAL.searchSelect.trigger('filter-by-engine-name'), enginePath); + await fillIn(GENERAL.inputSearch('secret-engine-path'), enginePath); assert.dom(SES.secretsBackendLink(enginePath)).exists('the alicloud engine is mounted'); await click(GENERAL.menuTrigger); diff --git a/ui/tests/acceptance/secrets/backend/generic/secret-test.js b/ui/tests/acceptance/secrets/backend/generic/secret-test.js index a213f0e8a4..e865735d16 100644 --- a/ui/tests/acceptance/secrets/backend/generic/secret-test.js +++ b/ui/tests/acceptance/secrets/backend/generic/secret-test.js @@ -3,8 +3,7 @@ * SPDX-License-Identifier: BUSL-1.1 */ -import { click, currentRouteName, settled, visit } from '@ember/test-helpers'; -import { selectChoose } from 'ember-power-select/test-support'; +import { click, currentRouteName, fillIn, visit } from '@ember/test-helpers'; import { module, test } from 'qunit'; import { setupApplicationTest } from 'ember-qunit'; import { v4 as uuidv4 } from 'uuid'; @@ -20,6 +19,7 @@ import { createSecret } from 'vault/tests/helpers/secret-engine/secret-engine-he import { create } from 'ember-cli-page-object'; import { deleteEngineCmd, runCmd } from 'vault/tests/helpers/commands'; +import { GENERAL } from 'vault/tests/helpers/general-selectors'; const cli = create(consolePanel); @@ -66,8 +66,7 @@ module('Acceptance | secrets/generic/create', function (hooks) { `write sys/mounts/${path}/tune options=version=2`, ]); await visit('/vault/secrets'); - await selectChoose('[data-test-component="search-select"]#filter-by-engine-name', path); - await settled(); + await fillIn(GENERAL.inputSearch('secret-engine-path'), path); await click(SES.secretsBackendLink(path)); assert.strictEqual( currentRouteName(), diff --git a/ui/tests/acceptance/settings-test.js b/ui/tests/acceptance/settings-test.js index e39de8dd56..512af187e1 100644 --- a/ui/tests/acceptance/settings-test.js +++ b/ui/tests/acceptance/settings-test.js @@ -4,7 +4,6 @@ */ import { currentURL, visit, click, fillIn } from '@ember/test-helpers'; -import { selectChoose } from 'ember-power-select/test-support'; import { module, test } from 'qunit'; import { setupApplicationTest } from 'ember-qunit'; import { v4 as uuidv4 } from 'uuid'; @@ -56,7 +55,7 @@ module('Acceptance | secret engine mount settings', function (hooks) { await visit('/vault/secrets/mounts'); await runCmd(mountEngineCmd(type, path), false); await visit('/vault/secrets'); - await selectChoose(GENERAL.searchSelect.trigger('filter-by-engine-name'), path); + await fillIn(GENERAL.inputSearch('secret-engine-path'), path); await click(GENERAL.menuTrigger); await click(GENERAL.menuItem('view-configuration')); assert.strictEqual( @@ -75,7 +74,7 @@ module('Acceptance | secret engine mount settings', function (hooks) { await visit('/vault/secrets/mounts'); await runCmd(mountEngineCmd(type, path), false); await visit('/vault/secrets'); - await selectChoose(GENERAL.searchSelect.trigger('filter-by-engine-name'), path); + await fillIn(GENERAL.inputSearch('secret-engine-path'), path); await click(GENERAL.menuTrigger); await click(GENERAL.menuItem('view-configuration')); assert.strictEqual( diff --git a/ui/tests/helpers/general-selectors.ts b/ui/tests/helpers/general-selectors.ts index 625f301f1e..0e178e2b25 100644 --- a/ui/tests/helpers/general-selectors.ts +++ b/ui/tests/helpers/general-selectors.ts @@ -171,4 +171,5 @@ export const GENERAL = { /* ────── Misc ────── */ icon: (name: string) => (name ? `[data-test-icon="${name}"]` : '[data-test-icon]'), badge: (name: string) => (name ? `[data-test-badge="${name}"]` : '[data-test-badge]'), + tooltip: (label: string) => `[data-test-tooltip="${label}"]`, }; diff --git a/ui/tests/helpers/secret-engine/secret-engine-helpers.js b/ui/tests/helpers/secret-engine/secret-engine-helpers.js index 9e37a34c88..9fcf13d4df 100644 --- a/ui/tests/helpers/secret-engine/secret-engine-helpers.js +++ b/ui/tests/helpers/secret-engine/secret-engine-helpers.js @@ -19,13 +19,14 @@ export async function createSecret(path, key, value) { return; } -export const createSecretsEngine = (store, type, path) => { +export const createSecretsEngine = (store, type, path, version) => { if (store) { store.pushPayload('secret-engine', { modelName: 'secret-engine', id: path, path: `${path}/`, type: type, + running_plugin_version: version, data: { type: type, }, @@ -36,6 +37,7 @@ export const createSecretsEngine = (store, type, path) => { return new SecretsEngineResource({ path: `${path}/`, type, + running_plugin_version: version, }); }; /* Create configurations methods diff --git a/ui/tests/integration/components/list-test.js b/ui/tests/integration/components/list-test.js index 7d86f57c16..4b0f146e8c 100644 --- a/ui/tests/integration/components/list-test.js +++ b/ui/tests/integration/components/list-test.js @@ -5,14 +5,12 @@ import { module, test } from 'qunit'; import { setupRenderingTest } from 'vault/tests/helpers'; -import { render, click, find, findAll, triggerEvent } from '@ember/test-helpers'; +import { render, click, findAll, triggerEvent, fillIn } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; import { v4 as uuidv4 } from 'uuid'; import sinon from 'sinon'; import { setupMirage } from 'ember-cli-mirage/test-support'; import { overrideResponse } from 'vault/tests/helpers/stubs'; -import { clickTrigger } from 'ember-power-select/test-support/helpers'; -import { selectChoose } from 'ember-power-select/test-support'; import { createSecretsEngine } from 'vault/tests/helpers/secret-engine/secret-engine-helpers'; import { SECRET_ENGINE_SELECTORS as SES } from 'vault/tests/helpers/secret-engine/secret-engine-selectors'; import { GENERAL } from 'vault/tests/helpers/general-selectors'; @@ -37,8 +35,8 @@ module('Integration | Component | secret-engine/list', function (hooks) { this.secretEngineModels = [ createSecretsEngine(undefined, 'cubbyhole', 'cubbyhole-test'), createSecretsEngine(undefined, 'kv', 'kv-test'), - createSecretsEngine(undefined, 'aws', 'aws-1'), - createSecretsEngine(undefined, 'aws', 'aws-2'), + createSecretsEngine(undefined, 'aws', 'aws-1', 'v1.0.0'), + createSecretsEngine(undefined, 'aws', 'aws-2', 'v2.0.0'), createSecretsEngine(undefined, 'nomad', 'nomad-test'), createSecretsEngine(undefined, 'badType', 'external-test'), ]; @@ -66,12 +64,13 @@ module('Integration | Component | secret-engine/list', function (hooks) { test('hovering over the icon of an external unrecognized engine type sets unrecognized tooltip text', async function (assert) { await render(hbs``); + await fillIn(GENERAL.inputSearch('secret-engine-path'), 'external-test'); - await selectChoose(GENERAL.searchSelect.trigger('filter-by-engine-name'), 'external-test'); - await triggerEvent('.hds-tooltip-button', 'mouseenter'); + const engineTooltip = document.querySelector(GENERAL.tooltip('Backend type')); + await triggerEvent(engineTooltip, 'mouseenter'); assert - .dom('.hds-tooltip-container') + .dom(engineTooltip.nextSibling) .hasText( `This engine's type is not recognized by the UI. Please use the CLI to manage this engine.`, 'shows tooltip text for unknown engine' @@ -80,12 +79,13 @@ module('Integration | Component | secret-engine/list', function (hooks) { test('hovering over the icon of an unsupported engine sets unsupported tooltip text', async function (assert) { await render(hbs``); + await fillIn(GENERAL.inputSearch('secret-engine-path'), 'nomad'); - await selectChoose(GENERAL.searchSelect.trigger('filter-by-engine-type'), 'nomad'); - await triggerEvent('.hds-tooltip-button', 'mouseenter'); + const engineTooltip = document.querySelector(GENERAL.tooltip('Backend type')); + await triggerEvent(engineTooltip, 'mouseenter'); assert - .dom('.hds-tooltip-container') + .dom(engineTooltip.nextSibling) .hasText( 'The UI only supports configuration views for these secret engines. The CLI must be used to manage other engine resources.', 'shows tooltip text for unsupported engine' @@ -94,21 +94,22 @@ module('Integration | Component | secret-engine/list', function (hooks) { test('hovering over the icon of a supported engine sets engine name as tooltip', async function (assert) { await render(hbs``); - await selectChoose(GENERAL.searchSelect.trigger('filter-by-engine-name'), 'aws-1'); + await fillIn(GENERAL.inputSearch('secret-engine-path'), 'aws-1'); - await triggerEvent('.hds-tooltip-button', 'mouseenter'); + const engineTooltip = document.querySelector(GENERAL.tooltip('Backend type')); + await triggerEvent(engineTooltip, 'mouseenter'); - assert.dom('.hds-tooltip-container').hasText('AWS', 'shows tooltip text for supported engine with name'); + assert.dom(engineTooltip.nextSibling).hasText('AWS', 'shows tooltip text for supported engine with name'); }); test('hovering over the icon of a kv engine shows engine name and version', async function (assert) { await render(hbs``); + await fillIn(GENERAL.inputSearch('secret-engine-path'), `kv-test`); - await selectChoose(GENERAL.searchSelect.trigger('filter-by-engine-name'), `kv-test`); - - await triggerEvent('.hds-tooltip-button', 'mouseenter'); + const engineTooltip = document.querySelector(GENERAL.tooltip('Backend type')); + await triggerEvent(engineTooltip, 'mouseenter'); assert - .dom('.hds-tooltip-container') + .dom(engineTooltip.nextSibling) .hasText('KV version 1', 'shows tooltip text for kv engine with version'); }); @@ -126,28 +127,42 @@ module('Integration | Component | secret-engine/list', function (hooks) { .hasClass('linked-block', `linked-block class is added to supported aws engines.`); }); - test('it filters by name and engine type', async function (assert) { + test('it filters by engine path and engine type', async function (assert) { await render(hbs``); // filter by type - await clickTrigger('#filter-by-engine-type'); - await click(GENERAL.searchSelect.option()); + await click(GENERAL.toggleInput('filter-by-engine-type')); + await click(GENERAL.checkboxByAttr('aws')); const rows = findAll(SES.secretsBackendLink()); const rowsAws = Array.from(rows).filter((row) => row.innerText.includes('aws')); - assert.strictEqual(rows.length, rowsAws.length, 'all rows returned are aws'); - // filter by name - await clickTrigger('#filter-by-engine-name'); - const firstItemToSelect = find(GENERAL.searchSelect.option()).innerText; - await click(GENERAL.searchSelect.option()); - const singleRow = document.querySelectorAll(SES.secretsBackendLink()); - assert.strictEqual(singleRow.length, 1, 'returns only one row'); - assert.dom(singleRow[0]).includesText(firstItemToSelect, 'shows the filtered by name engine'); - // clear filter by engine name - await click(`#filter-by-engine-name ${GENERAL.searchSelect.removeSelected}`); + // clear filter by type + await click(GENERAL.button('Clear all')); + assert.true(document.querySelectorAll(SES.secretsBackendLink()).length > 1, 'filter has been removed'); + + // filter by path + await fillIn(GENERAL.inputSearch('secret-engine-path'), 'kv'); + const singleRow = document.querySelectorAll(SES.secretsBackendLink()); + assert.dom(singleRow[0]).includesText('kv', 'shows the filtered by path engine'); + + // clear filter by engine path + await fillIn(GENERAL.inputSearch('secret-engine-path'), ''); const rowsAgain = document.querySelectorAll(SES.secretsBackendLink()); - assert.true(rowsAgain.length > 1, 'filter has been removed'); + assert.true(rowsAgain.length > 1, 'search filter text has been removed'); + }); + + test('it filters by engine version', async function (assert) { + await render(hbs``); + // select engine type + await click(GENERAL.toggleInput('filter-by-engine-type')); + await click(GENERAL.checkboxByAttr('aws')); + + // filter by version + await click(GENERAL.toggleInput('filter-by-engine-version')); + await click(GENERAL.checkboxByAttr('v2.0.0')); + const singleRow = document.querySelectorAll(SES.secretsBackendLink()); + assert.dom(singleRow[0]).includesText('aws-2', 'shows the single engine filtered by version'); }); test('it applies overflow styling', async function (assert) {