mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-11-02 19:47:54 +00:00
UI bug fix: Kubernetes Role filter replace with explicit input filter (#27178)
* initial changes for new component template only handle actions in parent * add changelog * fix current kubernetes test * component test * remove concurrency task * make div and not form due to testing things * address pr feedback * Update ui/tests/integration/components/filter-input-explicit-test.js Co-authored-by: Noelle Daley <noelledaley@users.noreply.github.com> * Update filter-input-explicit-test.js * fix tests * make it a form and fix test: --------- Co-authored-by: Noelle Daley <noelledaley@users.noreply.github.com>
This commit is contained in:
3
changelog/27178.txt
Normal file
3
changelog/27178.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
```release-note:change
|
||||||
|
ui/kubernetes: Update the roles filter-input to use explicit search.
|
||||||
|
```
|
||||||
19
ui/lib/core/addon/components/filter-input-explicit.hbs
Normal file
19
ui/lib/core/addon/components/filter-input-explicit.hbs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{{!
|
||||||
|
Copyright (c) HashiCorp, Inc.
|
||||||
|
SPDX-License-Identifier: BUSL-1.1
|
||||||
|
~}}
|
||||||
|
|
||||||
|
<form {{on "submit" @handleSearch}}>
|
||||||
|
<Hds::SegmentedGroup as |S|>
|
||||||
|
<S.TextInput
|
||||||
|
@value={{@query}}
|
||||||
|
placeholder={{@placeholder}}
|
||||||
|
aria-label="Search by path"
|
||||||
|
size="32"
|
||||||
|
{{on "input" @handleInput}}
|
||||||
|
{{on "keydown" @handleKeyDown}}
|
||||||
|
data-test-filter-input-explicit
|
||||||
|
/>
|
||||||
|
<S.Button @color="secondary" @text="Search" @icon="search" type="submit" data-test-filter-input-explicit-search />
|
||||||
|
</Hds::SegmentedGroup>
|
||||||
|
</form>
|
||||||
6
ui/lib/core/app/components/filter-input-explicit.js
Normal file
6
ui/lib/core/app/components/filter-input-explicit.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) HashiCorp, Inc.
|
||||||
|
* SPDX-License-Identifier: BUSL-1.1
|
||||||
|
*/
|
||||||
|
|
||||||
|
export { default } from 'core/components/filter-input-explicit';
|
||||||
@@ -6,8 +6,11 @@
|
|||||||
<TabPageHeader
|
<TabPageHeader
|
||||||
@model={{@backend}}
|
@model={{@backend}}
|
||||||
@filterRoles={{not @promptConfig}}
|
@filterRoles={{not @promptConfig}}
|
||||||
@rolesFilterValue={{@filterValue}}
|
@query={{this.query}}
|
||||||
@breadcrumbs={{@breadcrumbs}}
|
@breadcrumbs={{@breadcrumbs}}
|
||||||
|
@handleSearch={{this.handleSearch}}
|
||||||
|
@handleInput={{this.handleInput}}
|
||||||
|
@handleKeyDown={{this.handleKeyDown}}
|
||||||
>
|
>
|
||||||
{{#unless @promptConfig}}
|
{{#unless @promptConfig}}
|
||||||
<ToolbarLink @route="roles.create" @type="add" data-test-toolbar-roles-action>
|
<ToolbarLink @route="roles.create" @type="add" data-test-toolbar-roles-action>
|
||||||
|
|||||||
@@ -9,24 +9,58 @@ import { action } from '@ember/object';
|
|||||||
import { getOwner } from '@ember/application';
|
import { getOwner } from '@ember/application';
|
||||||
import errorMessage from 'vault/utils/error-message';
|
import errorMessage from 'vault/utils/error-message';
|
||||||
import { tracked } from '@glimmer/tracking';
|
import { tracked } from '@glimmer/tracking';
|
||||||
|
import keys from 'core/utils/key-codes';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @module Roles
|
* @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 {array} roles - array of roles
|
||||||
* @param {boolean} promptConfig - whether or not to display config cta
|
* @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
|
* @param {array} breadcrumbs - breadcrumbs as an array of objects that contain label and route
|
||||||
*/
|
*/
|
||||||
export default class RolesPageComponent extends Component {
|
export default class RolesPageComponent extends Component {
|
||||||
@service flashMessages;
|
@service flashMessages;
|
||||||
|
@service router;
|
||||||
|
@tracked query;
|
||||||
@tracked roleToDelete = null;
|
@tracked roleToDelete = null;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super(...arguments);
|
||||||
|
this.query = this.args.filterValue;
|
||||||
|
}
|
||||||
|
|
||||||
get mountPoint() {
|
get mountPoint() {
|
||||||
return getOwner(this).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
|
@action
|
||||||
async onDelete(model) {
|
async onDelete(model) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -28,10 +28,12 @@
|
|||||||
<Toolbar aria-label="items for managing kubernetes items">
|
<Toolbar aria-label="items for managing kubernetes items">
|
||||||
{{#if @filterRoles}}
|
{{#if @filterRoles}}
|
||||||
<ToolbarFilters>
|
<ToolbarFilters>
|
||||||
<NavigateInput
|
<FilterInputExplicit
|
||||||
@filter={{@rolesFilterValue}}
|
@query={{@query}}
|
||||||
@placeholder="Filter roles"
|
@placeholder="Filter roles"
|
||||||
@urls={{hash list="vault.cluster.secrets.backend.kubernetes.roles"}}
|
@handleSearch={{@handleSearch}}
|
||||||
|
@handleInput={{@handleInput}}
|
||||||
|
@handleKeyDown={{@handleKeyDown}}
|
||||||
/>
|
/>
|
||||||
</ToolbarFilters>
|
</ToolbarFilters>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import kubernetesScenario from 'vault/mirage/scenarios/kubernetes';
|
|||||||
import kubernetesHandlers from 'vault/mirage/handlers/kubernetes';
|
import kubernetesHandlers from 'vault/mirage/handlers/kubernetes';
|
||||||
import authPage from 'vault/tests/pages/auth';
|
import authPage from 'vault/tests/pages/auth';
|
||||||
import { fillIn, visit, currentURL, click, currentRouteName } from '@ember/test-helpers';
|
import { fillIn, visit, currentURL, click, currentRouteName } from '@ember/test-helpers';
|
||||||
|
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||||
|
|
||||||
module('Acceptance | kubernetes | roles', function (hooks) {
|
module('Acceptance | kubernetes | roles', function (hooks) {
|
||||||
setupApplicationTest(hooks);
|
setupApplicationTest(hooks);
|
||||||
@@ -30,7 +31,8 @@ module('Acceptance | kubernetes | roles', function (hooks) {
|
|||||||
test('it should filter roles', async function (assert) {
|
test('it should filter roles', async function (assert) {
|
||||||
await this.visitRoles();
|
await this.visitRoles();
|
||||||
assert.dom('[data-test-list-item-link]').exists({ count: 3 }, 'Roles list renders');
|
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.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');
|
assert.ok(currentURL().includes('pageFilter=1'), 'pageFilter query param value is set');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ export const GENERAL = {
|
|||||||
|
|
||||||
filter: (name: string) => `[data-test-filter="${name}"]`,
|
filter: (name: string) => `[data-test-filter="${name}"]`,
|
||||||
filterInput: '[data-test-filter-input]',
|
filterInput: '[data-test-filter-input]',
|
||||||
|
filterInputExplicit: '[data-test-filter-input-explicit]',
|
||||||
|
filterInputExplicitSearch: '[data-test-filter-input-explicit-search]',
|
||||||
confirmModalInput: '[data-test-confirmation-modal-input]',
|
confirmModalInput: '[data-test-confirmation-modal-input]',
|
||||||
confirmButton: '[data-test-confirm-button]',
|
confirmButton: '[data-test-confirm-button]',
|
||||||
confirmTrigger: '[data-test-confirm-action-trigger]',
|
confirmTrigger: '[data-test-confirm-action-trigger]',
|
||||||
|
|||||||
@@ -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`<FilterInputExplicit aria-label="test-component" @placeholder={{this.placeholder}} @query={{this.query}} @handleSearch={{this.handleSearch}} @handleInput={{this.handleInput}} @handleKeyDown={{this.handleKeyDown}} />`
|
||||||
|
);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
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');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -10,6 +10,7 @@ import { setupMirage } from 'ember-cli-mirage/test-support';
|
|||||||
import { render, click } from '@ember/test-helpers';
|
import { render, click } from '@ember/test-helpers';
|
||||||
import hbs from 'htmlbars-inline-precompile';
|
import hbs from 'htmlbars-inline-precompile';
|
||||||
import { allowAllCapabilitiesStub } from 'vault/tests/helpers/stubs';
|
import { allowAllCapabilitiesStub } from 'vault/tests/helpers/stubs';
|
||||||
|
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||||
|
|
||||||
module('Integration | Component | kubernetes | Page::Roles', function (hooks) {
|
module('Integration | Component | kubernetes | Page::Roles', function (hooks) {
|
||||||
setupRenderingTest(hooks);
|
setupRenderingTest(hooks);
|
||||||
@@ -58,7 +59,7 @@ module('Integration | Component | kubernetes | Page::Roles', function (hooks) {
|
|||||||
.dom('[data-test-toolbar-roles-action]')
|
.dom('[data-test-toolbar-roles-action]')
|
||||||
.doesNotExist('Create role', 'Toolbar action does not render when not configured');
|
.doesNotExist('Create role', 'Toolbar action does not render when not configured');
|
||||||
assert
|
assert
|
||||||
.dom('[data-test-nav-input]')
|
.dom(GENERAL.filterInputExplicit)
|
||||||
.doesNotExist('Roles filter input does not render when not configured');
|
.doesNotExist('Roles filter input does not render when not configured');
|
||||||
assert.dom('[data-test-config-cta]').exists('Config cta renders');
|
assert.dom('[data-test-config-cta]').exists('Config cta renders');
|
||||||
});
|
});
|
||||||
@@ -70,7 +71,7 @@ module('Integration | Component | kubernetes | Page::Roles', function (hooks) {
|
|||||||
assert
|
assert
|
||||||
.dom('[data-test-toolbar-roles-action] svg')
|
.dom('[data-test-toolbar-roles-action] svg')
|
||||||
.hasClass('flight-icon-plus', 'Toolbar action has correct icon');
|
.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-title]').hasText('No roles yet', 'Title renders');
|
||||||
assert
|
assert
|
||||||
.dom('[data-test-empty-state-message]')
|
.dom('[data-test-empty-state-message]')
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import { setupEngine } from 'ember-engines/test-support';
|
|||||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||||
import { render } from '@ember/test-helpers';
|
import { render } from '@ember/test-helpers';
|
||||||
import hbs from 'htmlbars-inline-precompile';
|
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) {
|
module('Integration | Component | kubernetes | TabPageHeader', function (hooks) {
|
||||||
setupRenderingTest(hooks);
|
setupRenderingTest(hooks);
|
||||||
@@ -28,12 +30,18 @@ module('Integration | Component | kubernetes | TabPageHeader', function (hooks)
|
|||||||
this.model = this.store.peekRecord('secret-engine', 'kubernetes-test');
|
this.model = this.store.peekRecord('secret-engine', 'kubernetes-test');
|
||||||
this.mount = this.model.path.slice(0, -1);
|
this.mount = this.model.path.slice(0, -1);
|
||||||
this.breadcrumbs = [{ label: 'Secrets', route: 'secrets', linkExternal: true }, { label: this.mount }];
|
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) {
|
test('it should render breadcrumbs', async function (assert) {
|
||||||
await render(hbs`<TabPageHeader @model={{this.model}} @breadcrumbs={{this.breadcrumbs}} />`, {
|
await render(
|
||||||
|
hbs`<TabPageHeader @model={{this.model}} @breadcrumbs={{this.breadcrumbs}} @handleSearch={{this.handleSearch}} @handleInput={{this.handleInput}} @handleKeyDown={{this.handleKeyDown}} />`,
|
||||||
|
{
|
||||||
owner: this.engine,
|
owner: this.engine,
|
||||||
});
|
}
|
||||||
|
);
|
||||||
assert.dom('[data-test-breadcrumbs] li:nth-child(1) a').hasText('Secrets', 'Secrets breadcrumb renders');
|
assert.dom('[data-test-breadcrumbs] li:nth-child(1) a').hasText('Secrets', 'Secrets breadcrumb renders');
|
||||||
|
|
||||||
assert
|
assert
|
||||||
@@ -42,9 +50,12 @@ module('Integration | Component | kubernetes | TabPageHeader', function (hooks)
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('it should render title', async function (assert) {
|
test('it should render title', async function (assert) {
|
||||||
await render(hbs`<TabPageHeader @model={{this.model}} @breadcrumbs={{this.breadcrumbs}} />`, {
|
await render(
|
||||||
|
hbs`<TabPageHeader @model={{this.model}} @breadcrumbs={{this.breadcrumbs}} @handleSearch={{this.handleSearch}} @handleInput={{this.handleInput}} @handleKeyDown={{this.handleKeyDown}} />`,
|
||||||
|
{
|
||||||
owner: this.engine,
|
owner: this.engine,
|
||||||
});
|
}
|
||||||
|
);
|
||||||
assert
|
assert
|
||||||
.dom('[data-test-header-title] svg')
|
.dom('[data-test-header-title] svg')
|
||||||
.hasClass('flight-icon-kubernetes-color', 'Correct icon renders in title');
|
.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) {
|
test('it should render tabs', async function (assert) {
|
||||||
await render(hbs`<TabPageHeader @model={{this.model}} @breadcrumbs={{this.breadcrumbs}} />`, {
|
await render(
|
||||||
|
hbs`<TabPageHeader @model={{this.model}} @breadcrumbs={{this.breadcrumbs}} @handleSearch={{this.handleSearch}} @handleInput={{this.handleInput}} @handleKeyDown={{this.handleKeyDown}}/>`,
|
||||||
|
{
|
||||||
owner: this.engine,
|
owner: this.engine,
|
||||||
});
|
}
|
||||||
|
);
|
||||||
assert.dom('[data-test-tab="overview"]').hasText('Overview', 'Overview tab renders');
|
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="roles"]').hasText('Roles', 'Roles tab renders');
|
||||||
assert.dom('[data-test-tab="config"]').hasText('Configuration', 'Configuration 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) {
|
test('it should render filter for roles', async function (assert) {
|
||||||
await render(
|
await render(
|
||||||
hbs`<TabPageHeader @model={{this.model}} @filterRoles={{true}} @rolesFilterValue="test" @breadcrumbs={{this.breadcrumbs}} />`,
|
hbs`<TabPageHeader @model={{this.model}} @filterRoles={{true}} @query="test" @breadcrumbs={{this.breadcrumbs}} @handleSearch={{this.handleSearch}} @handleInput={{this.handleInput}} @handleKeyDown={{this.handleKeyDown}} />`,
|
||||||
{ owner: this.engine }
|
{ 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) {
|
test('it should yield block for toolbar actions', async function (assert) {
|
||||||
await render(
|
await render(
|
||||||
hbs`
|
hbs`
|
||||||
<TabPageHeader @model={{this.model}} @breadcrumbs={{this.breadcrumbs}}>
|
<TabPageHeader @model={{this.model}} @breadcrumbs={{this.breadcrumbs}} @handleSearch={{this.handleSearch}} @handleInput={{this.handleInput}} @handleKeyDown={{this.handleKeyDown}}>
|
||||||
<span data-test-yield>It yields!</span>
|
<span data-test-yield>It yields!</span>
|
||||||
</TabPageHeader>
|
</TabPageHeader>
|
||||||
`,
|
`,
|
||||||
|
|||||||
Reference in New Issue
Block a user