diff --git a/changelog/27178.txt b/changelog/27178.txt new file mode 100644 index 0000000000..c84c67f34e --- /dev/null +++ b/changelog/27178.txt @@ -0,0 +1,3 @@ +```release-note:change +ui/kubernetes: Update the roles filter-input to use explicit search. +``` diff --git a/ui/lib/core/addon/components/filter-input-explicit.hbs b/ui/lib/core/addon/components/filter-input-explicit.hbs new file mode 100644 index 0000000000..2cf1f2ed29 --- /dev/null +++ b/ui/lib/core/addon/components/filter-input-explicit.hbs @@ -0,0 +1,19 @@ +{{! + Copyright (c) HashiCorp, Inc. + SPDX-License-Identifier: BUSL-1.1 +~}} + +
+ + + + +
\ No newline at end of file diff --git a/ui/lib/core/app/components/filter-input-explicit.js b/ui/lib/core/app/components/filter-input-explicit.js new file mode 100644 index 0000000000..1327ecdafc --- /dev/null +++ b/ui/lib/core/app/components/filter-input-explicit.js @@ -0,0 +1,6 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +export { default } from 'core/components/filter-input-explicit'; diff --git a/ui/lib/kubernetes/addon/components/page/roles.hbs b/ui/lib/kubernetes/addon/components/page/roles.hbs index e1ade3fa61..810b45ae5f 100644 --- a/ui/lib/kubernetes/addon/components/page/roles.hbs +++ b/ui/lib/kubernetes/addon/components/page/roles.hbs @@ -6,8 +6,11 @@ {{#unless @promptConfig}} diff --git a/ui/lib/kubernetes/addon/components/page/roles.js b/ui/lib/kubernetes/addon/components/page/roles.js index e3ef2eca90..6de4188d12 100644 --- a/ui/lib/kubernetes/addon/components/page/roles.js +++ b/ui/lib/kubernetes/addon/components/page/roles.js @@ -9,24 +9,58 @@ import { action } from '@ember/object'; import { getOwner } from '@ember/application'; import errorMessage from 'vault/utils/error-message'; import { tracked } from '@glimmer/tracking'; +import keys from 'core/utils/key-codes'; /** * @module Roles - * RolesPage component is a child component to show list of roles + * RolesPage component is a child component to show list of roles. + * It also handles the filtering actions of roles. * * @param {array} roles - array of roles * @param {boolean} promptConfig - whether or not to display config cta - * @param {array} pageFilter - array of filtered roles + * @param {string} filterValue - value of queryParam pageFilter * @param {array} breadcrumbs - breadcrumbs as an array of objects that contain label and route */ export default class RolesPageComponent extends Component { @service flashMessages; + @service router; + @tracked query; @tracked roleToDelete = null; + constructor() { + super(...arguments); + this.query = this.args.filterValue; + } + get mountPoint() { return getOwner(this).mountPoint; } + navigate(pageFilter) { + const route = `${this.mountPoint}.roles.index`; + const args = [route, { queryParams: { pageFilter: pageFilter || null } }]; + this.router.transitionTo(...args); + } + + @action + handleKeyDown(event) { + if (event.keyCode === keys.ESC) { + // On escape, transition to roles index route. + this.navigate(); + } + // ignore all other key events + } + + @action handleInput(evt) { + this.query = evt.target.value; + } + + @action + handleSearch(evt) { + evt.preventDefault(); + this.navigate(this.query); + } + @action async onDelete(model) { try { diff --git a/ui/lib/kubernetes/addon/components/tab-page-header.hbs b/ui/lib/kubernetes/addon/components/tab-page-header.hbs index 6b6fc74a31..cc765170ab 100644 --- a/ui/lib/kubernetes/addon/components/tab-page-header.hbs +++ b/ui/lib/kubernetes/addon/components/tab-page-header.hbs @@ -28,10 +28,12 @@ {{#if @filterRoles}} - {{/if}} diff --git a/ui/tests/acceptance/secrets/backend/kubernetes/roles-test.js b/ui/tests/acceptance/secrets/backend/kubernetes/roles-test.js index 4a8dcdbdf6..23af404a13 100644 --- a/ui/tests/acceptance/secrets/backend/kubernetes/roles-test.js +++ b/ui/tests/acceptance/secrets/backend/kubernetes/roles-test.js @@ -10,6 +10,7 @@ import kubernetesScenario from 'vault/mirage/scenarios/kubernetes'; import kubernetesHandlers from 'vault/mirage/handlers/kubernetes'; import authPage from 'vault/tests/pages/auth'; import { fillIn, visit, currentURL, click, currentRouteName } from '@ember/test-helpers'; +import { GENERAL } from 'vault/tests/helpers/general-selectors'; module('Acceptance | kubernetes | roles', function (hooks) { setupApplicationTest(hooks); @@ -30,7 +31,8 @@ module('Acceptance | kubernetes | roles', function (hooks) { test('it should filter roles', async function (assert) { await this.visitRoles(); assert.dom('[data-test-list-item-link]').exists({ count: 3 }, 'Roles list renders'); - await fillIn('[data-test-component="navigate-input"]', '1'); + await fillIn(GENERAL.filterInputExplicit, '1'); + await click(GENERAL.filterInputExplicitSearch); assert.dom('[data-test-list-item-link]').exists({ count: 1 }, 'Filtered roles list renders'); assert.ok(currentURL().includes('pageFilter=1'), 'pageFilter query param value is set'); }); diff --git a/ui/tests/helpers/general-selectors.ts b/ui/tests/helpers/general-selectors.ts index 49260cb17b..674a5bb51a 100644 --- a/ui/tests/helpers/general-selectors.ts +++ b/ui/tests/helpers/general-selectors.ts @@ -20,6 +20,8 @@ export const GENERAL = { filter: (name: string) => `[data-test-filter="${name}"]`, filterInput: '[data-test-filter-input]', + filterInputExplicit: '[data-test-filter-input-explicit]', + filterInputExplicitSearch: '[data-test-filter-input-explicit-search]', confirmModalInput: '[data-test-confirmation-modal-input]', confirmButton: '[data-test-confirm-button]', confirmTrigger: '[data-test-confirm-action-trigger]', diff --git a/ui/tests/integration/components/filter-input-explicit-test.js b/ui/tests/integration/components/filter-input-explicit-test.js new file mode 100644 index 0000000000..3dff54736c --- /dev/null +++ b/ui/tests/integration/components/filter-input-explicit-test.js @@ -0,0 +1,61 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'ember-qunit'; +import { render, typeIn, click } from '@ember/test-helpers'; +import hbs from 'htmlbars-inline-precompile'; +import { GENERAL } from 'vault/tests/helpers/general-selectors'; +import sinon from 'sinon'; + +const handler = (e) => { + // required because filter-input-explicit passes handleSearch on form submit + if (e && e.preventDefault) e.preventDefault(); + return; +}; + +module('Integration | Component | filter-input-explicit', function (hooks) { + setupRenderingTest(hooks); + + hooks.beforeEach(function () { + this.handleSearch = sinon.spy(handler); + this.handleInput = sinon.spy(); + this.handleKeyDown = sinon.spy(); + this.query = ''; + this.placeholder = 'Filter roles'; + + this.renderComponent = () => { + return render( + hbs`` + ); + }; + }); + + test('it renders', async function (assert) { + this.query = 'foo'; + await this.renderComponent(); + + assert + .dom(GENERAL.filterInputExplicit) + .hasAttribute('placeholder', 'Filter roles', 'Placeholder passed to input element'); + assert.dom(GENERAL.filterInputExplicit).hasValue('foo', 'Value passed to input element'); + }); + + test('it should call handleSearch on submit', async function (assert) { + await this.renderComponent(); + await typeIn(GENERAL.filterInputExplicit, 'bar'); + await click(GENERAL.filterInputExplicitSearch); + assert.ok(this.handleSearch.calledOnce, 'handleSearch was called once'); + }); + + test('it should send keydown event on keydown', async function (assert) { + await this.renderComponent(); + await typeIn(GENERAL.filterInputExplicit, 'a'); + await typeIn(GENERAL.filterInputExplicit, 'b'); + + assert.ok(this.handleKeyDown.calledTwice, 'handle keydown was called twice'); + assert.ok(this.handleSearch.notCalled, 'handleSearch was not called on a keydown event'); + }); +}); diff --git a/ui/tests/integration/components/kubernetes/page/roles-test.js b/ui/tests/integration/components/kubernetes/page/roles-test.js index 70562e2adf..bf557e2d39 100644 --- a/ui/tests/integration/components/kubernetes/page/roles-test.js +++ b/ui/tests/integration/components/kubernetes/page/roles-test.js @@ -10,6 +10,7 @@ import { setupMirage } from 'ember-cli-mirage/test-support'; import { render, click } from '@ember/test-helpers'; import hbs from 'htmlbars-inline-precompile'; import { allowAllCapabilitiesStub } from 'vault/tests/helpers/stubs'; +import { GENERAL } from 'vault/tests/helpers/general-selectors'; module('Integration | Component | kubernetes | Page::Roles', function (hooks) { setupRenderingTest(hooks); @@ -58,7 +59,7 @@ module('Integration | Component | kubernetes | Page::Roles', function (hooks) { .dom('[data-test-toolbar-roles-action]') .doesNotExist('Create role', 'Toolbar action does not render when not configured'); assert - .dom('[data-test-nav-input]') + .dom(GENERAL.filterInputExplicit) .doesNotExist('Roles filter input does not render when not configured'); assert.dom('[data-test-config-cta]').exists('Config cta renders'); }); @@ -70,7 +71,7 @@ module('Integration | Component | kubernetes | Page::Roles', function (hooks) { assert .dom('[data-test-toolbar-roles-action] svg') .hasClass('flight-icon-plus', 'Toolbar action has correct icon'); - assert.dom('[data-test-nav-input]').exists('Roles filter input renders'); + assert.dom(GENERAL.filterInputExplicit).exists('Roles filter input renders'); assert.dom('[data-test-empty-state-title]').hasText('No roles yet', 'Title renders'); assert .dom('[data-test-empty-state-message]') diff --git a/ui/tests/integration/components/kubernetes/tab-page-header-test.js b/ui/tests/integration/components/kubernetes/tab-page-header-test.js index e4e24ab6cf..e203d1e753 100644 --- a/ui/tests/integration/components/kubernetes/tab-page-header-test.js +++ b/ui/tests/integration/components/kubernetes/tab-page-header-test.js @@ -9,6 +9,8 @@ import { setupEngine } from 'ember-engines/test-support'; import { setupMirage } from 'ember-cli-mirage/test-support'; import { render } from '@ember/test-helpers'; import hbs from 'htmlbars-inline-precompile'; +import { GENERAL } from 'vault/tests/helpers/general-selectors'; +import sinon from 'sinon'; module('Integration | Component | kubernetes | TabPageHeader', function (hooks) { setupRenderingTest(hooks); @@ -28,12 +30,18 @@ module('Integration | Component | kubernetes | TabPageHeader', function (hooks) this.model = this.store.peekRecord('secret-engine', 'kubernetes-test'); this.mount = this.model.path.slice(0, -1); this.breadcrumbs = [{ label: 'Secrets', route: 'secrets', linkExternal: true }, { label: this.mount }]; + this.handleSearch = sinon.spy(); + this.handleInput = sinon.spy(); + this.handleKeyDown = sinon.spy(); }); test('it should render breadcrumbs', async function (assert) { - await render(hbs``, { - owner: this.engine, - }); + await render( + hbs``, + { + owner: this.engine, + } + ); assert.dom('[data-test-breadcrumbs] li:nth-child(1) a').hasText('Secrets', 'Secrets breadcrumb renders'); assert @@ -42,9 +50,12 @@ module('Integration | Component | kubernetes | TabPageHeader', function (hooks) }); test('it should render title', async function (assert) { - await render(hbs``, { - owner: this.engine, - }); + await render( + hbs``, + { + owner: this.engine, + } + ); assert .dom('[data-test-header-title] svg') .hasClass('flight-icon-kubernetes-color', 'Correct icon renders in title'); @@ -52,9 +63,12 @@ module('Integration | Component | kubernetes | TabPageHeader', function (hooks) }); test('it should render tabs', async function (assert) { - await render(hbs``, { - owner: this.engine, - }); + await render( + hbs``, + { + owner: this.engine, + } + ); assert.dom('[data-test-tab="overview"]').hasText('Overview', 'Overview tab renders'); assert.dom('[data-test-tab="roles"]').hasText('Roles', 'Roles tab renders'); assert.dom('[data-test-tab="config"]').hasText('Configuration', 'Configuration tab renders'); @@ -62,16 +76,16 @@ module('Integration | Component | kubernetes | TabPageHeader', function (hooks) test('it should render filter for roles', async function (assert) { await render( - hbs``, + hbs``, { owner: this.engine } ); - assert.dom('[data-test-nav-input] input').hasValue('test', 'Filter renders with provided value'); + assert.dom(GENERAL.filterInputExplicit).hasValue('test', 'Filter renders with provided value'); }); test('it should yield block for toolbar actions', async function (assert) { await render( hbs` - + It yields! `,