diff --git a/src/components/create-vm-dialog/createVmDialog.jsx b/src/components/create-vm-dialog/createVmDialog.jsx index 08e56cf9f..053a72061 100644 --- a/src/components/create-vm-dialog/createVmDialog.jsx +++ b/src/components/create-vm-dialog/createVmDialog.jsx @@ -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 { @@ -76,6 +77,7 @@ import { correctSpecialCases, filterReleaseEolDates, getOSStringRepresentation, + getOSDescription, needsRHToken, isDownloadableOs, loadOfflineToken, @@ -405,10 +407,8 @@ const SourceRow = ({ connectionName, source, sourceType, networks, nodeDevices, class OSRow extends React.Component { constructor(props) { super(props); - const IGNORE_VENDORS = ['ALTLinux', 'Mandriva', 'GNOME Project']; const osInfoListExt = this.props.osInfoList .map(os => correctSpecialCases(os)) - .filter(os => filterReleaseEolDates(os) && !IGNORE_VENDORS.find(vendor => vendor == os.vendor)) .sort((a, b) => { if (a.vendor == b.vendor) { // Sort OS with numbered version by version @@ -426,22 +426,33 @@ class OSRow extends React.Component { return getOSStringRepresentation(a).toLowerCase() > getOSStringRepresentation(b).toLowerCase() ? 1 : -1; }); + const IGNORE_VENDORS = ['ALTLinux', 'Mandriva', 'GNOME Project']; + const newOsEntries = []; + const oldOsEntries = []; + for (const os of osInfoListExt) { + if (filterReleaseEolDates(os) && !IGNORE_VENDORS.find(vendor => vendor == os.vendor)) + newOsEntries.push(os); + else + 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(), - osEntries: osInfoListExt, - }; - 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, }; } @@ -454,30 +465,21 @@ class OSRow extends React.Component { data-loading={!!isLoading} id="os-select-group" label={_("Operating system")}> - { - 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"> - {this.state.osEntries.map(os => ())} - + { + const os = this.state.osInfoListExt.find(os => os.shortId === value); + if (os) + onValueChanged('os', os); + }} + onClearSelection={() => { + onValueChanged('os', null); + }} + /> ); diff --git a/src/components/create-vm-dialog/createVmDialogUtils.js b/src/components/create-vm-dialog/createVmDialogUtils.js index 64be4c745..a5bceeb69 100644 --- a/src/components/create-vm-dialog/createVmDialogUtils.js +++ b/src/components/create-vm-dialog/createVmDialogUtils.js @@ -17,6 +17,10 @@ * along with Cockpit; If not, see . */ +import cockpit from 'cockpit'; +import React from 'react'; +import { ExclamationTriangleIcon, OutlinedClockIcon } from "@patternfly/react-icons"; + import { getTodayYearShifted, } from "../../helpers.js"; @@ -24,6 +28,8 @@ import { import * as python from "python.js"; import autoDetectOSScript from './autoDetectOS.py'; +const _ = cockpit.gettext; + const ACCEPT_RELEASE_DATES_AFTER = getTodayYearShifted(-3); const ACCEPT_EOL_DATES_AFTER = getTodayYearShifted(-1); const RHSM_TOKEN = "rhsm-offline-token"; @@ -76,6 +82,14 @@ export function filterReleaseEolDates(os) { ); } +export function getOSDescription(os) { + if (os.eolDate && compareDates(ACCEPT_EOL_DATES_AFTER, os.eolDate) < 0) + return {cockpit.format(_("Vendor support ended $0"), os.eolDate)}; + if (!os.eolDate && os.releaseDate && compareDates(ACCEPT_RELEASE_DATES_AFTER, os.releaseDate) < 0) + return {cockpit.format(_("Released $0"), os.releaseDate)}; + return null; +} + export function compareDates(a, b, emptyFirst = false) { if (!a) { if (!b) { diff --git a/src/machines.scss b/src/machines.scss index a4f6b4c7c..0d7ce67b7 100644 --- a/src/machines.scss +++ b/src/machines.scss @@ -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; +} diff --git a/test/check-machines-create b/test/check-machines-create index 67d9b0d64..78da3c72f 100755 --- a/test/check-machines-create +++ b/test/check-machines-create @@ -91,16 +91,16 @@ class TestMachinesCreate(machineslib.VirtualMachinesCase): pixel_test_tag="auto")) # check if older os are filtered - runner.checkFilteredOsTest(TestMachinesCreate.VmDialog(self, os_name=config.REDHAT_RHEL_4_7_FILTERED_OS, - pixel_test_tag="filter")) - - runner.checkFilteredOsTest(TestMachinesCreate.VmDialog(self, os_name=config.MANDRIVA_2011_FILTERED_OS)) + runner.checkFilteredOsTest(TestMachinesCreate.VmDialog(self, os_name=config.OLD_FILTERED_OS, + os_is_old=True)) - runner.checkFilteredOsTest(TestMachinesCreate.VmDialog(self, os_name=config.MAGEIA_3_FILTERED_OS)) + runner.checkFilteredOsTest(TestMachinesCreate.VmDialog(self, os_name=config.UNSUPPORTED_FILTERED_OS, + os_is_unsupported=True, + pixel_test_tag="filter-unsupported")) - # check that newer oses are present and searchable with substring match - runner.checkFilteredOsTest(TestMachinesCreate.VmDialog(self, os_name=config.WINDOWS_SERVER_10, - os_search_name=config.WINDOWS_SERVER_10_SHORT)) + runner.checkFilteredOsTest(TestMachinesCreate.VmDialog(self, os_name=config.NOT_FOUND_FILTERED_OS, + os_is_not_found=True, + pixel_test_tag="filter")) # check OS versions are sorted in alphabetical order runner.checkSortedOsTest(TestMachinesCreate.VmDialog(self), [config.FEDORA_29, config.FEDORA_28]) @@ -935,7 +935,9 @@ 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 - REDHAT_RHEL_4_7_FILTERED_OS = 'Red Hat Enterprise Linux 4.9' + 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' FEDORA_28 = 'Fedora 28' FEDORA_28_SHORTID = 'fedora28' @@ -950,13 +952,6 @@ vnc_password= "{vnc_passwd}" CENTOS_7 = 'CentOS 7' - MANDRIVA_2011_FILTERED_OS = 'Mandriva Linux 2011' - - MAGEIA_3_FILTERED_OS = 'Mageia 3' - - WINDOWS_SERVER_10 = 'Microsoft Windows 10' - WINDOWS_SERVER_10_SHORT = 'win' - class VmDialog: vmId = 0 @@ -969,6 +964,9 @@ vnc_password= "{vnc_passwd}" os_name="Fedora 28", os_search_name=None, os_short_id="fedora28", + os_is_unsupported=False, + os_is_old=False, + os_is_not_found=False, expected_os_name=None, is_unattended=None, profile=None, @@ -1017,6 +1015,9 @@ vnc_password= "{vnc_passwd}" self.os_name = os_name self.os_search_name = os_search_name self.os_short_id = os_short_id + self.os_is_unsupported = os_is_unsupported + self.os_is_old = os_is_old + self.os_is_not_found = os_is_not_found self.expected_os_name = expected_os_name self.is_unattended = is_unattended self.profile = profile @@ -1138,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") @@ -1176,23 +1175,25 @@ vnc_password= "{vnc_passwd}" return self - def checkOsFiltered(self, present=False): + def checkOsFiltered(self): b = self.browser b.focus("#os-select-group input") - # os_search_name is meant to be used to test substring much + # os_search_name is meant to be used to test substring match b.input_text(self.os_search_name or self.os_name) - if not present: - try: - with b.wait_timeout(5): - b.wait_in_text("#os-select li button", "No results found") - return self - except AssertionError: - # os found which is not ok - self.fail(f"{self.os_name} was not filtered") + if not self.os_is_not_found: + # There should be exactly one entry + 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:not(:disabled)", "Vendor support ended") + elif self.os_is_old: + b.wait_in_text("#os-select li button:not(:disabled)", "Released ") else: - b.wait_visible(f"#os-select li button:contains({self.os_search_name})") + b.wait_in_text("#os-select li button", "No results found") + + return self def checkRhelIsDownloadable(self): b = self.browser @@ -1204,7 +1205,7 @@ vnc_password= "{vnc_passwd}" b.wait_visible(f"#os-select li button:contains('{TestMachinesCreate.TestCreateConfig.RHEL_8_1}')") b.wait_not_present(f"#os-select li button:contains('{TestMachinesCreate.TestCreateConfig.RHEL_7_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 @@ -1212,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 @@ -1361,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) @@ -1992,7 +1993,7 @@ vnc_password= "{vnc_passwd}" dialog.open() \ b.select_from_dropdown("#source-type", dialog.sourceType) - 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('{dialog.os_name}') button") b.wait_attr("#os-select-group input", "value", dialog.os_name) if not dialog.offline_token_autofilled: