Filter Auth methods by name or type (#20747)

* glimmerize controller

* search selects added and working

* add test and cleanup disable

* small fix on name filtering

* add changelog

* Add comment about individualized names

* Update methods.js

remove spaces
This commit is contained in:
Angel Garbarino 2023-05-26 10:07:36 -06:00 committed by GitHub
parent 325c0dd1ac
commit 4180f56d73
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 158 additions and 26 deletions

3
changelog/20747.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:improvement
ui: Add filtering by auth type and auth name to the Authentication Method list view.
```

View File

@ -4,22 +4,74 @@
*/ */
import Controller from '@ember/controller'; import Controller from '@ember/controller';
import { task } from 'ember-concurrency'; import { dropTask } from 'ember-concurrency';
import { inject as service } from '@ember/service'; import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking';
export default Controller.extend({ export default class VaultClusterAccessMethodsController extends Controller {
flashMessages: service(), @service flashMessages;
queryParams: { @tracked authMethodOptions = [];
page: 'page', @tracked selectedAuthType = null;
pageFilter: 'pageFilter', @tracked selectedAuthName = null;
},
page: 1, queryParams = ['page, pageFilter'];
pageFilter: null,
filter: null,
disableMethod: task(function* (method) { page = 1;
pageFilter = null;
filter = null;
get authMethodList() {
// return an options list to filter by engine type, ex: 'kv'
if (this.selectedAuthType) {
// check first if the user has also filtered by name.
// names are individualized across type so you can't have the same name for an aws auth method and userpass.
// this means it's fine to filter by first type and then name or just name.
if (this.selectedAuthName) {
return this.model.filter((method) => this.selectedAuthName === method.id);
}
// otherwise filter by auth type
return this.model.filter((method) => this.selectedAuthType === method.type);
}
// return an options list to filter by auth name, ex: 'my-userpass'
if (this.selectedAuthName) {
return this.model.filter((method) => this.selectedAuthName === method.id);
}
// no filters, return full sorted list.
return this.model;
}
get authMethodArrayByType() {
const arrayOfAllAuthTypes = this.authMethodList.map((modelObject) => modelObject.type);
// filter out repeated auth types (e.g. [userpass, userpass] => [userpass])
const arrayOfUniqueAuthTypes = [...new Set(arrayOfAllAuthTypes)];
return arrayOfUniqueAuthTypes.map((authType) => ({
name: authType,
id: authType,
}));
}
get authMethodArrayByName() {
return this.authMethodList.map((modelObject) => ({
name: modelObject.id,
id: modelObject.id,
}));
}
@action
filterAuthType([type]) {
this.selectedAuthType = type;
}
@action
filterAuthName([name]) {
this.selectedAuthName = name;
}
@dropTask
*disableMethod(method) {
const { type, path } = method; const { type, path } = method;
try { try {
yield method.destroyRecord(); yield method.destroyRecord();
@ -29,5 +81,5 @@ export default Controller.extend({
`There was an error disabling Auth Method at ${path}: ${err.errors.join(' ')}.` `There was an error disabling Auth Method at ${path}: ${err.errors.join(' ')}.`
); );
} }
}).drop(), }
}); }

View File

@ -6,19 +6,19 @@
import Route from '@ember/routing/route'; import Route from '@ember/routing/route';
import { inject as service } from '@ember/service'; import { inject as service } from '@ember/service';
export default Route.extend({ export default class VaultClusterAccessMethodsRoute extends Route {
store: service(), @service store;
queryParams: { queryParams = {
page: { page: {
refreshModel: true, refreshModel: true,
}, },
pageFilter: { pageFilter: {
refreshModel: true, refreshModel: true,
}, },
}, };
model() { model() {
return this.store.findAll('auth-method'); return this.store.findAll('auth-method');
}, }
}); }

View File

@ -7,6 +7,33 @@
</PageHeader> </PageHeader>
<Toolbar> <Toolbar>
<ToolbarFilters>
<SearchSelect
@id="filter-by-auth-type"
@options={{this.authMethodArrayByType}}
@selectLimit="1"
@disallowNewItems={{true}}
@fallbackComponent="input-search"
@onChange={{this.filterAuthType}}
@placeholder={{"Filter by auth type"}}
@displayInherit={{true}}
@inputValue={{if this.selectedAuthType (array this.selectedAuthType)}}
@disabled={{if this.selectedAuthName true false}}
class="is-marginless"
/>
<SearchSelect
@id="filter-by-auth-name"
@options={{this.authMethodArrayByName}}
@selectLimit="1"
@disallowNewItems={{true}}
@fallbackComponent="input-search"
@onChange={{this.filterAuthName}}
@placeholder={{"Filter by auth name"}}
@displayInherit={{true}}
@inputValue={{if this.selectedAuthName (array this.selectedAuthName)}}
class="is-marginless has-left-padding-s"
/>
</ToolbarFilters>
<ToolbarActions> <ToolbarActions>
<ToolbarLink @route="vault.cluster.settings.auth.enable" @type="add" data-test-auth-enable> <ToolbarLink @route="vault.cluster.settings.auth.enable" @type="add" data-test-auth-enable>
Enable new method Enable new method
@ -14,7 +41,7 @@
</ToolbarActions> </ToolbarActions>
</Toolbar> </Toolbar>
{{#each (sort-by "path" this.model) as |method|}} {{#each (sort-by "path" this.authMethodList) as |method|}}
<LinkedBlock <LinkedBlock
@params={{array "vault.cluster.access.method" method.id}} @params={{array "vault.cluster.access.method" method.id}}
class="list-item-row" class="list-item-row"

View File

@ -4,22 +4,71 @@
*/ */
import { currentRouteName } from '@ember/test-helpers'; import { currentRouteName } from '@ember/test-helpers';
import { clickTrigger } from 'ember-power-select/test-support/helpers';
import { module, test } from 'qunit'; import { module, test } from 'qunit';
import { setupApplicationTest } from 'ember-qunit'; import { setupApplicationTest } from 'ember-qunit';
import { create } from 'ember-cli-page-object';
import page from 'vault/tests/pages/access/methods'; import page from 'vault/tests/pages/access/methods';
import authEnable from 'vault/tests/pages/settings/auth/enable';
import authPage from 'vault/tests/pages/auth'; import authPage from 'vault/tests/pages/auth';
import ss from 'vault/tests/pages/components/search-select';
import consoleClass from 'vault/tests/pages/components/console/ui-panel';
module('Acceptance | /access/', function (hooks) { import { v4 as uuidv4 } from 'uuid';
const consoleComponent = create(consoleClass);
const searchSelect = create(ss);
module('Acceptance | auth-methods list view', function (hooks) {
setupApplicationTest(hooks); setupApplicationTest(hooks);
hooks.beforeEach(function () { hooks.beforeEach(function () {
this.uid = uuidv4();
return authPage.login(); return authPage.login();
}); });
test('it navigates', async function (assert) { test('it navigates to auth method', async function (assert) {
await page.visit(); await page.visit();
assert.strictEqual(currentRouteName(), 'vault.cluster.access.methods', 'navigates to the correct route'); assert.strictEqual(currentRouteName(), 'vault.cluster.access.methods', 'navigates to the correct route');
assert.ok(page.methodsLink.isActive, 'the first link is active'); assert.ok(page.methodsLink.isActive, 'the first link is active');
assert.strictEqual(page.methodsLink.text, 'Authentication methods'); assert.strictEqual(page.methodsLink.text, 'Authentication methods');
}); });
test('it filters by name and auth type', async function (assert) {
assert.expect(4);
const authPath1 = `userpass-1-${this.uid}`;
const authPath2 = `userpass-2-${this.uid}`;
const type = 'userpass';
await authEnable.visit();
await authEnable.enable(type, authPath1);
await authEnable.visit();
await authEnable.enable(type, authPath2);
await page.visit();
// filter by auth type
await clickTrigger('#filter-by-auth-type');
await searchSelect.options.objectAt(0).click();
const rows = document.querySelectorAll('[data-test-auth-backend-link]');
const rowsUserpass = Array.from(rows).filter((row) => row.innerText.includes('userpass'));
assert.strictEqual(rows.length, rowsUserpass.length, 'all rows returned are userpass');
// filter by name
await clickTrigger('#filter-by-auth-name');
const firstItemToSelect = searchSelect.options.objectAt(0).text;
await searchSelect.options.objectAt(0).click();
const singleRow = document.querySelectorAll('[data-test-auth-backend-link]');
assert.strictEqual(singleRow.length, 1, 'returns only one row');
assert.dom(singleRow[0]).includesText(firstItemToSelect, 'shows the filtered by auth name');
// clear filter by engine name
await searchSelect.deleteButtons.objectAt(1).click();
const rowsAgain = document.querySelectorAll('[data-test-auth-backend-link]');
assert.ok(rowsAgain.length > 1, 'filter has been removed');
// cleanup
await consoleComponent.runCommands([`delete sys/auth/${authPath1}`]);
await consoleComponent.runCommands([`delete sys/auth/${authPath2}`]);
});
}); });

View File

@ -7,7 +7,7 @@ import { currentRouteName, settled } from '@ember/test-helpers';
import { clickTrigger } from 'ember-power-select/test-support/helpers'; import { clickTrigger } from 'ember-power-select/test-support/helpers';
import { create } from 'ember-cli-page-object'; import { create } from 'ember-cli-page-object';
import { module, test } from 'qunit'; import { module, test } from 'qunit';
import { runCommands } from 'vault/tests/helpers/pki/pki-run-commands'; import consoleClass from 'vault/tests/pages/components/console/ui-panel';
import { setupApplicationTest } from 'ember-qunit'; import { setupApplicationTest } from 'ember-qunit';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
@ -16,6 +16,7 @@ import backendsPage from 'vault/tests/pages/secrets/backends';
import authPage from 'vault/tests/pages/auth'; import authPage from 'vault/tests/pages/auth';
import ss from 'vault/tests/pages/components/search-select'; import ss from 'vault/tests/pages/components/search-select';
const consoleComponent = create(consoleClass);
const searchSelect = create(ss); const searchSelect = create(ss);
module('Acceptance | secret-engine list view', function (hooks) { module('Acceptance | secret-engine list view', function (hooks) {
@ -76,7 +77,7 @@ module('Acceptance | secret-engine list view', function (hooks) {
assert.dom(rowSupported[0]).hasClass('linked-block', `linked-block class is added to supported engines.`); assert.dom(rowSupported[0]).hasClass('linked-block', `linked-block class is added to supported engines.`);
// cleanup // cleanup
await runCommands([`delete sys/mounts/${enginePath}`]); await consoleComponent.runCommands([`delete sys/mounts/${enginePath}`]);
}); });
test('it filters by name and engine type', async function (assert) { test('it filters by name and engine type', async function (assert) {
@ -109,7 +110,7 @@ module('Acceptance | secret-engine list view', function (hooks) {
assert.ok(rowsAgain.length > 1, 'filter has been removed'); assert.ok(rowsAgain.length > 1, 'filter has been removed');
// cleanup // cleanup
await runCommands([`delete sys/mounts/${enginePath1}`]); await consoleComponent.runCommands([`delete sys/mounts/${enginePath1}`]);
await runCommands([`delete sys/mounts/${enginePath2}`]); await consoleComponent.runCommands([`delete sys/mounts/${enginePath2}`]);
}); });
}); });