Skip to content

Commit

Permalink
WIP - Use the PF TypeaheadSelect template
Browse files Browse the repository at this point in the history
and hack some headers into it (instead of using groups).
  • Loading branch information
mvollmer committed Oct 25, 2024
1 parent 364d2c3 commit 8b9c8c0
Show file tree
Hide file tree
Showing 6 changed files with 138 additions and 65 deletions.
2 changes: 1 addition & 1 deletion node_modules
Submodule node_modules updated 39777 files
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
"@patternfly/react-icons": "5.4.0",
"@patternfly/react-styles": "5.4.0",
"@patternfly/react-table": "5.4.1",
"@patternfly/react-templates": "^1.1.8",
"@patternfly/react-tokens": "5.4.0",
"@xterm/addon-canvas": "0.7.0",
"@xterm/xterm": "5.5.0",
Expand Down
89 changes: 89 additions & 0 deletions src/components/common/typeaheadSelectWithHeaders.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
* This file is part of Cockpit.
*
* Copyright (C) 2024 Red Hat, Inc.
*
* Cockpit is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation; either version 2.1 of the License, or
* (at your option) any later version.
*
* Cockpit is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
*/

/* This is the Patternfly 5 TypeaheadSelect, with support for header
* entries. Headers are a more straightforward alternative to grouping
* with SelectGroup.
*
* Properties of TypeaheadSelectWithHeaders are the same as for
* TypeaheadSelect. The get headers, the "selectOptions" properties
* can contain entries like this:
*
* { header: "Header title" }
*
* These header entries are turned into regular, disabled entries that
* are styled to look like group headers.
*
* For laziness reasons, TypeaheadSelectWithHeaders does not allow to
* override the filterFunction, but you can specify a matchFunction
* that works on individual options.
*/

import React, { useMemo } from 'react';
import { TypeaheadSelect } from '@patternfly/react-templates/dist/esm/components/Select';

function defaultMatchFunction(filterValue, option) {
return option.content.toLowerCase().includes(filterValue.toLowerCase());
}

function transformOption(option, idx) {
if (option.header) {
return {
value: " header:" + idx,
content: "",
description: option.header,
isDisabled: true,
};
} else
return option;
}

function isHeaderOption(option) {
return option.value.startsWith(" header:");
}

export const TypeaheadSelectWithHeaders = ({
selectOptions,
filterFunction, // must be undefined
matchFunction,
...props
}) => {
const transformedSelectOptions = useMemo(() => selectOptions.map(transformOption),
[selectOptions]);

if (filterFunction)
console.warn("TypeaheadSelectWithHeaders does not support filterFunction, use matchFunction");

const headerFilterFunction = (filterValue, options) => {
// Filter by search term
const filtered = options.filter(o => (isHeaderOption(o) ||
(matchFunction || defaultMatchFunction)(filterValue, o)));
// Remove headers that have nothing following them.
const filtered2 = filtered.filter((o, i) => {
return !(isHeaderOption(o) && (i >= filtered.length - 1 || isHeaderOption(filtered[i + 1])));
});
return filtered2;
};

return (
<TypeaheadSelect selectOptions={transformedSelectOptions}
filterFunction={headerFilterFunction}
{...props} />
);
};
84 changes: 31 additions & 53 deletions src/components/create-vm-dialog/createVmDialog.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import { useInit } from "hooks.js";
import cockpit from 'cockpit';
import store from "../../store.js";
import { MachinesConnectionSelector } from '../common/machinesConnectionSelector.jsx';
import { TypeaheadSelectWithHeaders } from '../common/typeaheadSelectWithHeaders.jsx';
import { FormHelper } from "cockpit-components-form-helper.jsx";
import { FileAutoComplete } from "cockpit-components-file-autocomplete.jsx";
import {
Expand Down Expand Up @@ -435,23 +436,23 @@ class OSRow extends React.Component {
oldOsEntries.push(os);
}

const make_option = os => ({
value: os.shortId,
content: getOSStringRepresentation(os),
description: getOSDescription(os),
});

const selectOptions = [
{ header: _("Recommended operating systems") },
...newOsEntries.map(make_option),
{ header: _("Unsupported and older operating systems") },
...oldOsEntries.map(make_option),
];

this.state = {
typeAheadKey: Math.random(),
newOsEntries,
oldOsEntries,
};
this.createValue = os => {
return ({
toString: function() { return this.displayName },
compareTo: function(value) {
if (typeof value == "string")
return this.shortId.toLowerCase().includes(value.toLowerCase()) || this.displayName.toLowerCase().includes(value.toLowerCase());
else
return this.shortId == value.shortId;
},
...os,
displayName: getOSStringRepresentation(os),
});
selectOptions,
osInfoListExt,
};
}

Expand All @@ -464,44 +465,21 @@ class OSRow extends React.Component {
data-loading={!!isLoading}
id="os-select-group"
label={_("Operating system")}>
<PFSelect
variant="typeahead"
key={this.state.typeAheadKey}
id='os-select'
isDisabled={isLoading}
selections={os ? this.createValue(os) : null}
typeAheadAriaLabel={_("Choose an operating system")}
placeholderText={_("Choose an operating system")}
onSelect={(event, value) => {
this.setState({
isOpen: false
});
onValueChanged('os', value);
}}
onClear={() => {
this.setState({ isOpen: false });
onValueChanged('os', null);
}}
onToggle={(_event, isOpen) => this.setState({ isOpen })}
isOpen={this.state.isOpen}
menuAppendTo="parent"
isGrouped
>
<SelectGroup label={_("Recommended operating systems")} key="new">
{this.state.newOsEntries
.map(os => (<SelectOption key={os.id}
description={getOSDescription(os)}
value={this.createValue(os)} />))
}
</SelectGroup>
<SelectGroup label={_("Unsupported and older operating systems")} key="old">
{this.state.oldOsEntries
.map(os => (<SelectOption key={os.id}
description={getOSDescription(os)}
value={this.createValue(os)} />))
}
</SelectGroup>
</PFSelect>
<TypeaheadSelectWithHeaders key={this.state.typeAheadKey}
id='os-select'
isDisabled={isLoading}
placeholder={_("Choose an operating system")}
selectOptions={this.state.selectOptions}
selected={os?.shortId}
onSelect={(event, value) => {
const os = this.state.osInfoListExt.find(os => os.shortId === value);
if (os)
onValueChanged('os', os);
}}
onClearSelection={() => {
onValueChanged('os', null);
}}
/>
<FormHelper helperTextInvalid={validationStateOS == "error" && validationFailed.os} />
</FormGroup>
);
Expand Down
7 changes: 7 additions & 0 deletions src/machines.scss
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,10 @@
#storage-pool-delete-modal span.pf-v5-c-check__body {
margin-block-start: 0;
}

#os-select {
/* Don't get too tall */
max-block-size: min(20rem, 50vh);
/* Don't have a horizontal scrollbar */
overflow-y: auto;
}
20 changes: 9 additions & 11 deletions test/check-machines-create
Original file line number Diff line number Diff line change
Expand Up @@ -935,7 +935,7 @@ vnc_password= "{vnc_passwd}"
TREE_URL = 'https://archive.fedoraproject.org/pub/archive/fedora/linux/releases/28/Server/x86_64/os'

# LINUX can be filtered if 3 years old
OLD_FILTERED_OS = 'Red Hat Enterprise Linux 8.3'
OLD_FILTERED_OS = 'Red Hat Enterprise Linux 8.3 (Ootpa)'
UNSUPPORTED_FILTERED_OS = 'Fedora 34'
NOT_FOUND_FILTERED_OS = 'Red Hat Enterprise Linux 4.9'

Expand Down Expand Up @@ -1079,7 +1079,7 @@ vnc_password= "{vnc_passwd}"
# https://bugzilla.redhat.com/show_bug.cgi?id=1987120
b.select_from_dropdown("#source-type", "url")
fake_fedora = "Fedora 28" # 128 MiB minimum storage
suse = "SUSE CaaS Platform Unknown" # 20 GiB minimum storage
suse = "SUSE CaaS Platform Unknown (unknown)" # 20 GiB minimum storage
b.set_input_text("#os-select-group input", fake_fedora)
b.click(f"#os-select li button:contains('{fake_fedora}')")
b.wait_val("#storage-limit", "128")
Expand Down Expand Up @@ -1139,15 +1139,13 @@ vnc_password= "{vnc_passwd}"
b.wait_not_present("#navbar-oops")

# re-input an OS which is "Fedora 28"
# need to click the extend button to show the OS list
b.click("#os-select-group button[aria-label=\"Options menu\"]")
b.set_input_text("#os-select-group input", "Fedora")
b.click("#os-select li button:contains('Fedora 28')")
b.wait_attr_contains("#os-select-group input", "value", "Fedora 28")
b.wait_not_present("#navbar-oops")

# click the 'X' button to clear the OS input and check there is no Ooops
b.click("#os-select-group button[aria-label=\"Clear all\"]")
b.click("#os-select-group button[aria-label=\"Clear input value\"]")
b.wait_attr("#os-select-group input", "value", "")
b.wait_not_present("#navbar-oops")

Expand Down Expand Up @@ -1186,12 +1184,12 @@ vnc_password= "{vnc_passwd}"

if not self.os_is_not_found:
# There should be exactly one entry
b.wait_in_text("#os-select li button", self.os_name)
b.wait_in_text("#os-select li button:not(:disabled)", self.os_name)
# It might have a description
if self.os_is_unsupported:
b.wait_in_text("#os-select li button", "Vendor support ended")
b.wait_in_text("#os-select li button:not(:disabled)", "Vendor support ended")
elif self.os_is_old:
b.wait_in_text("#os-select li button", "Released ")
b.wait_in_text("#os-select li button:not(:disabled)", "Released ")
else:
b.wait_in_text("#os-select li button", "No results found")

Expand All @@ -1215,14 +1213,14 @@ vnc_password= "{vnc_passwd}"
def checkOsSorted(self, sorted_list):
b = self.browser

b.click("#os-select-group .pf-v5-c-select .pf-v5-c-button")
b.click("#os-select-group button.pf-v5-c-menu-toggle__button")

# Find the first OS from the sorted list, and get a text of it's next neighbour
next_os = b.text(f"#os-select-group li:contains({sorted_list[0]}) + li")
# The next neighbour should contain the second OS from the sorted list
self.assertEqual(next_os, sorted_list[1])
# make the select list go away to not obscure other elements
b.click("#os-select-group .pf-v5-c-select__toggle-button")
b.click("#os-select-group button.pf-v5-c-menu-toggle__button")
b.wait_not_present("#os-select li")
return self

Expand Down Expand Up @@ -1364,7 +1362,7 @@ vnc_password= "{vnc_passwd}"

b.wait_not_present("#offline-token")
if self.os_name:
b.click("#os-select-group > div button")
b.click("#os-select-group button.pf-v5-c-menu-toggle__button")
b.click(f"#os-select li:contains('{self.os_name}') button")
b.wait_attr("#os-select-group input", "value", self.os_name)

Expand Down

0 comments on commit 8b9c8c0

Please sign in to comment.