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: