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) {