Skip to content

Commit

Permalink
lib: Use TypeaheadSelect instead of deprecated Select for FileAutoCom…
Browse files Browse the repository at this point in the history
…plete
  • Loading branch information
mvollmer committed Oct 29, 2024
1 parent fe91cc2 commit 5e6acf0
Show file tree
Hide file tree
Showing 4 changed files with 60 additions and 85 deletions.
76 changes: 27 additions & 49 deletions pkg/lib/cockpit-components-file-autocomplete.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@

import cockpit from "cockpit";
import React from "react";
import { Select, SelectOption } from "@patternfly/react-core/dist/esm/deprecated/components/Select/index.js";
import PropTypes from "prop-types";
import { debounce } from 'throttle-debounce';
import { TypeaheadSelect } from "cockpit-components-typeahead-select";

const _ = cockpit.gettext;

Expand All @@ -31,26 +31,18 @@ export class FileAutoComplete extends React.Component {
this.state = {
directory: '', // The current directory we list files/dirs from
displayFiles: [],
isOpen: false,
value: this.props.value || null,
};

this.typeaheadInputValue = "";
this.allowFilesUpdate = true;
this.updateFiles = this.updateFiles.bind(this);
this.finishUpdate = this.finishUpdate.bind(this);
this.onToggle = this.onToggle.bind(this);
this.clearSelection = this.clearSelection.bind(this);
this.onCreateOption = this.onCreateOption.bind(this);

this.onPathChange = (value) => {
if (!value) {
this.clearSelection();
return;
}

this.typeaheadInputValue = value;

const cb = (dirPath) => this.updateFiles(dirPath == '' ? '/' : dirPath);

let path = value;
Expand Down Expand Up @@ -89,12 +81,6 @@ export class FileAutoComplete extends React.Component {
this.allowFilesUpdate = false;
}

onCreateOption(newValue) {
this.setState(prevState => ({
displayFiles: [...prevState.displayFiles, { type: "file", path: newValue }]
}));
}

updateFiles(path) {
if (this.state.directory == path)
return;
Expand Down Expand Up @@ -152,50 +138,42 @@ export class FileAutoComplete extends React.Component {
});
}

onToggle(_, isOpen) {
this.setState({ isOpen });
}

clearSelection() {
this.typeaheadInputValue = "";
this.updateFiles("/");
this.setState({
value: null,
isOpen: false
});
this.setState({ value: null });
this.props.onChange('', null);
}

render() {
const placeholder = this.props.placeholder || _("Path to file");

const selectOptions = this.state.displayFiles
.map(option => <SelectOption key={option.path}
className={option.type}
value={option.path} />);
.map(option => ({ value: option.path, content: option.path, className: option.type }));

return (
<Select
variant="typeahead"
id={this.props.id}
isInputValuePersisted
onTypeaheadInputChanged={this.debouncedChange}
placeholderText={placeholder}
noResultsFoundText={this.state.error || _("No such file or directory")}
selections={this.state.value}
onSelect={(_, value) => {
this.setState({ value, isOpen: false });
this.debouncedChange(value);
this.props.onChange(value || '', null);
}}
onToggle={this.onToggle}
onClear={this.clearSelection}
isOpen={this.state.isOpen}
isCreatable={this.props.isOptionCreatable}
createText={_("Create")}
onCreateOption={this.onCreateOption}
menuAppendTo="parent">
{selectOptions}
</Select>
<TypeaheadSelect toggleProps={ { id: this.props.id } }
onInputChange={this.debouncedChange}
placeholder={placeholder}
noOptionsAvailableMessage={this.state.error || _("No such file or directory")}
noOptionsFoundMessage={this.state.error || _("No such file or directory")}
onToggle={isOpen => {
// Try to list again when
// opening. Calling onPathChange here
// usually does nothing, except when
// there was an error earlier.
if (isOpen)
this.onPathChange(this.state.value);
}}
selected={this.state.value}
onSelect={(_, value) => {
this.setState({ value });
this.onPathChange(value);
this.props.onChange(value || '', null);
}}
onClearSelection={this.clearSelection}
isCreatable={this.props.isOptionCreatable}
createOptionMessage={val => cockpit.format(_("Create $0"), val)}
selectOptions={selectOptions} />
);
}
}
Expand Down
10 changes: 6 additions & 4 deletions test/common/testlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -736,10 +736,12 @@ def set_input_text(
self.wait_val(selector, val)

def set_file_autocomplete_val(self, group_identifier: str, location: str) -> None:
self.set_input_text(f"{group_identifier} .pf-v5-c-select__toggle-typeahead input", location)
# click away the selection list, to force a state update
self.click(f"{group_identifier} .pf-v5-c-select__toggle-typeahead")
self.wait_not_present(f"{group_identifier} .pf-v5-c-select__menu")
self.set_input_text(f"{group_identifier} .pf-v5-c-menu-toggle input", location)
# select the file
self.wait_text(f"{group_identifier} ul li:nth-child(1) button", location)
self.click(f"{group_identifier} ul li:nth-child(1) button")
self.wait_not_present(f"{group_identifier} .pf-v5-c-menu")
self.wait_val(f"{group_identifier} .pf-v5-c-menu-toggle input", location)

@contextlib.contextmanager
def wait_timeout(self, timeout: int) -> Iterator[None]:
Expand Down
44 changes: 20 additions & 24 deletions test/verify/check-pages
Original file line number Diff line number Diff line change
Expand Up @@ -604,40 +604,39 @@ OnCalendar=daily
b.focus("#demo-file-ac input[type=text]")
b.input_text(stuff + "/")
# need to wait for the widget's "fast typing" inhibition delay to trigger the completion popup
b.wait_in_text("#file-autocomplete-widget li:nth-of-type(1) button", stuff + "/")
b.wait_in_text("#file-autocomplete-widget li:nth-of-type(2) button", "dir/")
b.wait_in_text("#file-autocomplete-widget li:nth-of-type(3) button", "dir1/")
b.wait_in_text("#file-autocomplete-widget li:nth-of-type(4) button", "file1.txt")
b.click("#file-autocomplete-widget li:nth-of-type(2) button")
b.wait_in_text("#demo-file-ac li:nth-of-type(1) button", stuff + "/")
b.wait_in_text("#demo-file-ac li:nth-of-type(2) button", "dir/")
b.wait_in_text("#demo-file-ac li:nth-of-type(3) button", "dir1/")
b.wait_in_text("#demo-file-ac li:nth-of-type(4) button", "file1.txt")
b.click("#demo-file-ac li:nth-of-type(2) button")

# clear the file completion widget
b.click("#demo-file-ac div:first-of-type div:first-of-type button:nth-of-type(1)")
# test if input matches one entry, but is the prefix of other entry, widget should not descend into directory
b.focus("#demo-file-ac input[type=text]")
b.input_text(stuff + "/dir")
b.wait_in_text("#file-autocomplete-widget li:nth-of-type(1) button", stuff + "/dir")
b.wait_in_text("#file-autocomplete-widget li:nth-of-type(2) button", stuff + "/dir1")
b.wait_in_text("#demo-file-ac li:nth-of-type(1) button", stuff + "/dir")
b.wait_in_text("#demo-file-ac li:nth-of-type(2) button", stuff + "/dir1")

# clear the file completion widget
b.click("#demo-file-ac div:first-of-type div:first-of-type button:nth-of-type(1)")
b.wait_not_present("#file-autocomplete-widget li")
b.focus("#demo-file-ac input[type=text]")
b.input_text(stuff + "/")
b.wait_in_text("#file-autocomplete-widget li:nth-of-type(1) button", stuff + "/")
b.wait_in_text("#file-autocomplete-widget li:nth-of-type(4) button", "file1.txt")
b.click("#file-autocomplete-widget li:nth-of-type(4) button")
b.wait_not_present("#file-autocomplete-widget li")
b.wait_in_text("#demo-file-ac li:nth-of-type(1) button", stuff + "/")
b.wait_in_text("#demo-file-ac li:nth-of-type(4) button", "file1.txt")
b.click("#demo-file-ac li:nth-of-type(4) button")
b.wait_not_present("#demo-file-ac li")

# now update file1, check robustness with dynamic events
m.execute(f"touch {stuff}/file1.txt")
b.focus("#demo-file-ac input[type=text]")
time.sleep(1)
b.key("Backspace", 5)
# input is now $stuff/file
b.wait_in_text("#file-autocomplete-widget li:nth-of-type(1) button", "file1.txt")
b.wait_in_text("#demo-file-ac li:nth-of-type(1) button", "file1.txt")
b.key("Backspace", 4)
# input is now $stuff/, so all listings should be available
b.wait_in_text("#file-autocomplete-widget li:nth-of-type(4) button", "file1.txt")
b.wait_in_text("#demo-file-ac li:nth-of-type(4) button", "file1.txt")

# add new file
m.execute(f"touch {stuff}/other")
Expand All @@ -649,10 +648,10 @@ OnCalendar=daily
b.key("Backspace", 6)
time.sleep(1)
b.input_text("stuff/")
b.wait_in_text("#file-autocomplete-widget li:nth-of-type(5) button", "other")
b.wait_in_text("#demo-file-ac li:nth-of-type(5) button", "other")
# close the selector
b.click("#demo-file-ac input")
b.wait_not_present("#file-autocomplete-widget")
b.wait_not_present("#demo-file-ac li")

# Create test folder with known files
m.execute("mkdir /home/admin/newdir")
Expand All @@ -664,18 +663,15 @@ OnCalendar=daily
b.wait_val("#demo-file-ac-preselected input", "/home/admin/newdir/file1")
# open the selector
b.click("#demo-file-ac-preselected input")
b.wait_visible("#file-autocomplete-widget-preselected")
b.wait_visible("#demo-file-ac-preselected .pf-v5-c-menu")
# close and open again to reload the dir (which just got created)
b.click("#demo-file-ac-preselected input")
b.wait_not_present("#file-autocomplete-widget-preselected")
b.click("#demo-file-ac-preselected .pf-v5-c-menu-toggle__button")
b.wait_not_present("#demo-file-ac-preselected .pf-v5-c-menu")
b.click("#demo-file-ac-preselected input")
# selection has all the files in the directory
paths = ["/home/admin/newdir", "/home/admin/newdir/dir1", "/home/admin/newdir/dir2", "/home/admin/newdir/file1", "/home/admin/newdir/file2"]
for i in range(5):
b.wait_in_text(f"#file-autocomplete-widget-preselected li:nth-of-type({i + 1}) button", paths[i])
# clicking on input again hides selector dropdown again
b.click("#demo-file-ac-preselected input")
b.wait_not_present("#file-autocomplete-widget-preselected")
b.wait_in_text(f"#demo-file-ac-preselected li:nth-of-type({i + 1}) button", paths[i])

@testlib.skipOstree("No PCP available")
@testlib.skipImage("pcp not currently in testing", "debian-testing")
Expand Down Expand Up @@ -956,7 +952,7 @@ OnCalendar=daily

# Upload permission error
b.drop_superuser()
b.set_file_autocomplete_val("#demo-upload", "/root/")
b.set_file_autocomplete_val("#demo-upload", "/usr/share/")
b.upload_files("#demo-upload input[type='file']", [test_upload_file])
b.wait_in_text(".pf-v5-c-alert", "Not permitted to perform this action")

Expand Down
15 changes: 7 additions & 8 deletions test/verify/check-shell-keys
Original file line number Diff line number Diff line change
Expand Up @@ -304,18 +304,17 @@ session optional pam_ssh_add.so
b.wait_visible("#credential-keys")
b.wait_not_present("#ssh-file-add")
b.click("#ssh-file-add-custom")
b.set_file_autocomplete_val("#ssh-file-add-key", "/bad")
b.set_file_autocomplete_val("#ssh-file-add-key", "/etc/")
b.click("#ssh-file-add")
b.wait_text("#credentials-modal .pf-m-error > .pf-v5-c-helper-text__item-text", "Not a valid private key")

b.click("#credentials-modal .pf-v5-c-select__toggle-typeahead")
b.set_input_text("#credentials-modal .pf-v5-c-select__toggle-typeahead input", "/var/test/")
b.wait_in_text("#credentials-modal .pf-v5-c-select__menu-item.pf-m-disabled", "No such file or directory")
b.focus("#credentials-modal .pf-v5-c-select__toggle-typeahead input")
b.set_input_text("#credentials-modal .pf-v5-c-menu-toggle input", "/var/test/")
b.wait_in_text("#credentials-modal .pf-v5-c-menu__list-item.pf-m-aria-disabled", "No such file or directory")
b.focus("#credentials-modal .pf-v5-c-menu-toggle input")
b.key("Backspace", 5)
b.wait_visible("#credentials-modal .pf-v5-c-select__menu-item.directory:contains('/var/lib/')")
b.click("#credentials-modal .pf-v5-c-select__toggle-clear")
b.wait_val("#credentials-modal .pf-v5-c-select__toggle-typeahead input", "")
b.wait_visible("#credentials-modal .pf-v5-c-menu__list-item.directory:contains('/var/lib/')")
b.click("#credentials-modal .pf-v5-c-menu-toggle button[aria-label='Clear input value']")
b.wait_val("#credentials-modal .pf-v5-c-menu-toggle input", "")

b.set_file_autocomplete_val("#ssh-file-add-key", "/tmp/new.rsa")
b.click("#ssh-file-add")
Expand Down

0 comments on commit 5e6acf0

Please sign in to comment.