Skip to content

Commit

Permalink
UI: Update kv list filter to not search on type (#22648)
Browse files Browse the repository at this point in the history
  • Loading branch information
hashishaw committed Aug 31, 2023
1 parent 16f8054 commit 8da06f9
Show file tree
Hide file tree
Showing 5 changed files with 133 additions and 263 deletions.
56 changes: 21 additions & 35 deletions ui/lib/kv/addon/components/kv-list-filter.hbs
Original file line number Diff line number Diff line change
@@ -1,35 +1,21 @@
<div class="navigate-filter">
<div class="field" data-test-nav-input>
<p class="control has-icons-left">
<Input
id="secret-filter"
class="filter input"
placeholder="Filter secrets"
@value={{@filterValue}}
@type="text"
data-test-component="kv-list-filter"
{{on "input" this.handleInput}}
{{on "keydown" this.handleKeyDown}}
{{on "focus" this.setFilterIsFocused}}
{{did-insert this.focusInput}}
autocomplete="off"
/>
<Icon @name="search" class="search-icon has-text-grey-light" />
</p>
</div>
</div>
{{#if (and this.filterIsFocused this.isFilterMatch)}}
<p class="help has-text-grey is-size-8 has-left-padding-xs" data-test-help-enter>
<kbd>ENTER</kbd>
to go to see details.
<kbd>ESC</kbd>
to clear input.
</p>
{{else}}
<p class="help has-text-grey is-size-8 has-left-padding-xs" data-test-help-tab>
<kbd>TAB</kbd>
to autocomplete.
<kbd>ESC</kbd>
to clear input.
</p>
{{/if}}
<form {{on "submit" (perform this.handleSearch)}}>
<Hds::SegmentedGroup as |S|>
<S.TextInput
id="secret-id"
@value={{this.query}}
placeholder="Search secret path"
aria-describedby="Search by secret path"
size="32"
{{on "input" this.handleInput}}
{{on "keydown" this.handleKeyDown}}
data-test-kv-list-filter
/>
<S.Button
@color="secondary"
@text="Search"
@icon={{if this.handleSearch.isRunning "loading" "search"}}
type="submit"
data-test-kv-list-filter-submit
/>
</Hds::SegmentedGroup>
</form>
149 changes: 36 additions & 113 deletions ui/lib/kv/addon/components/kv-list-filter.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,33 +3,41 @@
* SPDX-License-Identifier: MPL-2.0
*/

import Ember from 'ember';
import Component from '@glimmer/component';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import keys from 'core/utils/key-codes';
import { keyIsFolder, parentKeyForKey, keyWithoutParentKey } from 'core/utils/key-utils';
import escapeStringRegexp from 'escape-string-regexp';
import { tracked } from '@glimmer/tracking';
import { task, timeout } from 'ember-concurrency';

/**
* @module KvListFilter
* `KvListFilter` filters through the KV metadata LIST response. It allows users to search through the current list, navigate into directories, and use keyboard functions to: autocomplete, view a secret, create a new secret, or clear the input field.
* `KvListFilter` is used for filtering on the KV metadata LIST response.
* It allows users to search for any text, and will transition to the list
* page with the appropriate parameters depending on the query. This component
* expects that the component will be re-constructed after search, since the
* route will reload the model and completely refresh the page.
* *
* <KvListFilter
* @secrets={{this.model.secrets}}
* @mountPoint={{this.model.mountPoint}}
* @filterValue="beep/my-"
* @pageFilter="my-"
* />
* @param {array} secrets - An array of secret models.
* @param {string} mountPoint - Where in the router files we're located. For this component it will always be vault.cluster.secrets.backend.kv
* @param {string} filterValue - A concatenation between the list-directory's dynamic path "path-to-secret" and the queryParam "pageFilter". For example, if we're inside the beep/ directory searching for any secret that starts with "my-" this value will equal "beep/my-".
* @param {string} pageFilter - The queryParam value, does not include pathToSecret ex: my-.
* @param {string} filterValue - Full initial search value. A concatenation between the list-directory's dynamic path "path-to-secret" and the queryParam "pageFilter". For example, if we're inside the beep/ directory searching for any secret that starts with "my-" this value will equal "beep/my-".
*/

export default class KvListFilterComponent extends Component {
@service router;
@tracked filterIsFocused = false;
@tracked query;

constructor() {
super(...arguments);
this.query = this.args.filterValue;
}

navigate(pathToSecret, pageFilter) {
const route = pathToSecret ? `${this.args.mountPoint}.list-directory` : `${this.args.mountPoint}.list`;
Expand All @@ -45,122 +53,37 @@ export default class KvListFilterComponent extends Component {
this.router.transitionTo(...args);
}

/*
- partialMatch returns the secret that most closely matches the pageFilter queryParam.
- Searches pageFilter and not filterValue because if we're inside a directory we only care about the secrets listed there and not the directory.
- If pageFilter is empty this returns the first secret model in the list.
**/
get partialMatch() {
// If pageFilter is empty we replace it with an empty string because you cannot pass 'undefined' to RegEx.
const value = !this.args.pageFilter ? '' : this.args.pageFilter;
const reg = new RegExp('^' + escapeStringRegexp(value));
const match = this.args.secrets.filter((path) => reg.test(path.fullSecretPath))[0];
if (this.isFilterMatch || !match) return null;

return match.fullSecretPath;
}
/*
- isFilterMatch returns true if the filterValue matches a fullSecretPath.
**/
get isFilterMatch() {
return !!this.args.secrets?.findBy('fullSecretPath', this.args.filterValue);
}
/*
-handleInput is triggered after the value of the input has changed. It is not triggered when input looses focus.
**/
@action
handleInput(event) {
const input = event.target.value;
const isDirectory = keyIsFolder(input);
const parentDirectory = parentKeyForKey(input);
const secretWithinDirectory = keyWithoutParentKey(input);

if (isDirectory) {
this.navigate(input);
} else if (parentDirectory) {
this.navigate(parentDirectory, secretWithinDirectory);
} else {
this.navigate(null, input);
}
}
/*
-handleKeyDown handles: tab, enter, backspace and escape. Ignores everything else.
**/
@action
handleKeyDown(event) {
const input = event.target.value;
const parentDirectory = parentKeyForKey(input);

if (event.keyCode === keys.BACKSPACE) {
this.handleBackspace(input, parentDirectory);
}

if (event.keyCode === keys.TAB) {
event.preventDefault();
this.handleTab();
}

if (event.keyCode === keys.ENTER) {
event.preventDefault();
this.handleEnter(input);
}

if (event.keyCode === keys.ESC) {
this.handleEscape(parentDirectory);
// On escape, transition to the nearest parentDirectory.
// If no parentDirectory, then to the list route.
const input = event.target.value;
const parentDirectory = parentKeyForKey(input);
!parentDirectory ? this.navigate() : this.navigate(parentDirectory);
}
// ignore all other key events
return;
}
// key-code specific methods
handleBackspace(input, parentDirectory) {
const isInputDirectory = keyIsFolder(input);
const inputWithoutParentKey = keyWithoutParentKey(input);
const pageFilter = isInputDirectory ? '' : inputWithoutParentKey.slice(0, -1);
this.navigate(parentDirectory, pageFilter);
}
handleTab() {
const isMatchDirectory = keyIsFolder(this.partialMatch);
const matchParentDirectory = parentKeyForKey(this.partialMatch);
const matchWithinDirectory = keyWithoutParentKey(this.partialMatch);

if (isMatchDirectory) {
// ex: beep/boop/
this.navigate(this.partialMatch);
} else if (!isMatchDirectory && matchParentDirectory) {
// ex: beep/boop/my-
this.navigate(matchParentDirectory, matchWithinDirectory);
} else {
// ex: my-
this.navigate(null, this.partialMatch);
}
}
handleEnter(input) {
if (this.isFilterMatch) {
// if secret exists send to details
this.router.transitionTo(`${this.args.mountPoint}.secret.details`, input);
} else {
// if secret does not exists send to create with the path prefilled with input value.
this.router.transitionTo(`${this.args.mountPoint}.create`, {
queryParams: { initialKey: input },
});
}
}
handleEscape(parentDirectory) {
// transition to the nearest parentDirectory. If no parentDirectory, then to the list route.
!parentDirectory ? this.navigate() : this.navigate(parentDirectory);
@action handleInput(evt) {
this.query = evt.target.value;
}

@action
setFilterIsFocused() {
// tracked property used to show or hide the help-text next to the input. Not involved in focus event itself.
this.filterIsFocused = true;
}

@action
focusInput() {
// set focus to the input when there is either a pageFilter queryParam value and/or list-directory's dynamic path-to-secret has a value.
if (this.args.filterValue) {
document.getElementById('secret-filter')?.focus();
@task
*handleSearch(evt) {
evt.preventDefault();
// shows loader to indicate that the search was executed
yield timeout(Ember.testing ? 0 : 250);
const searchTerm = this.query;
const isDirectory = keyIsFolder(searchTerm);
const parentDirectory = parentKeyForKey(searchTerm);
const secretWithinDirectory = keyWithoutParentKey(searchTerm);
if (isDirectory) {
this.navigate(searchTerm);
} else if (parentDirectory) {
this.navigate(parentDirectory, secretWithinDirectory);
} else {
this.navigate(null, searchTerm);
}
}
}
7 changes: 1 addition & 6 deletions ui/lib/kv/addon/components/page/list.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,7 @@
<:toolbarFilters>
{{#unless @noMetadataListPermissions}}
{{#if (or @secrets @filterValue)}}
<KvListFilter
@secrets={{@secrets}}
@mountPoint={{this.mountPoint}}
@filterValue={{@filterValue}}
@pageFilter={{@pageFilter}}
/>
<KvListFilter @secrets={{@secrets}} @mountPoint={{this.mountPoint}} @filterValue={{@filterValue}} />
{{/if}}
{{/unless}}
</:toolbarFilters>
Expand Down
2 changes: 1 addition & 1 deletion ui/tests/helpers/kv/kv-selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export const PAGE = {
list: {
createSecret: '[data-test-toolbar-create-secret]',
item: (secret) => (!secret ? '[data-test-list-item]' : `[data-test-list-item="${secret}"]`),
filter: `[data-test-component="kv-list-filter"]`,
filter: `[data-test-kv-list-filter]`,
overviewCard: '[data-test-overview-card-container="View secret"]',
overviewInput: '[data-test-view-secret] input',
overviewButton: '[data-test-get-secret-detail]',
Expand Down
Loading

0 comments on commit 8da06f9

Please sign in to comment.