diff --git a/src/ImageRunModal.jsx b/src/ImageRunModal.jsx index 9e334b73..411b12fb 100644 --- a/src/ImageRunModal.jsx +++ b/src/ImageRunModal.jsx @@ -9,11 +9,12 @@ import { Modal } from "@patternfly/react-core/dist/esm/components/Modal"; import { NumberInput } from "@patternfly/react-core/dist/esm/components/NumberInput"; import { Popover } from "@patternfly/react-core/dist/esm/components/Popover"; import { Radio } from "@patternfly/react-core/dist/esm/components/Radio"; +import { Spinner } from "@patternfly/react-core/dist/esm/components/Spinner/index.js"; import { Tab, TabTitleText, Tabs } from "@patternfly/react-core/dist/esm/components/Tabs"; import { Text, TextContent, TextList, TextListItem, TextVariants } from "@patternfly/react-core/dist/esm/components/Text"; import { TextInput } from "@patternfly/react-core/dist/esm/components/TextInput"; import { ToggleGroup, ToggleGroupItem } from "@patternfly/react-core/dist/esm/components/ToggleGroup"; -import { Select, SelectGroup, SelectOption, SelectVariant } from "@patternfly/react-core/dist/esm/deprecated/components/Select"; +import { Bullseye } from "@patternfly/react-core/dist/esm/layouts/Bullseye/index.js"; import { Flex, FlexItem } from "@patternfly/react-core/dist/esm/layouts/Flex"; import { Grid, GridItem } from "@patternfly/react-core/dist/esm/layouts/Grid"; import { OutlinedQuestionCircleIcon } from '@patternfly/react-icons'; @@ -23,6 +24,7 @@ import { debounce } from 'throttle-debounce'; import cockpit from 'cockpit'; import { DynamicListForm } from 'cockpit-components-dynamic-list.jsx'; +import { TypeaheadSelect } from 'cockpit-components-typeahead-select'; import { onDownloadContainer, onDownloadContainerFinished } from './Containers.jsx'; import { EnvVar, validateEnvVar } from './Env.jsx'; @@ -583,44 +585,33 @@ export class ImageRunModal extends React.Component { const input = this.buildFilterRegex(searchText, false); - const results = imageRegistries - .map((reg, index) => { - const filtered = (reg in images ? images[reg] : []) - .filter(image => { - if (image.isSystem && !isSystem) { - return false; - } - if ('isSystem' in image && !image.isSystem && isSystem) { - return false; - } - return image.Name.search(input) !== -1; - }) - .map((image, index) => { - return ( - - ); - }); - - if (filtered.length === 0) { - return []; - } else { - return ( - - {filtered} - - ); - } - }) - .filter(group => group.length !== 0); // filter out empty groups + const results = []; + imageRegistries.forEach(reg => { + let need_header = this.state.searchByRegistry == 'all'; + (reg in images ? images[reg] : []) + .filter(image => { + if (image.isSystem && !isSystem) { + return false; + } + if ('isSystem' in image && !image.isSystem && isSystem) { + return false; + } + return image.Name.search(input) !== -1; + }) + .forEach(image => { + if (need_header) { + results.push({ key: results.length, decorator: "header", content: reg }); + need_header = false; + } - // Remove when there is a filter selected. - if (this.state.searchByRegistry !== 'all' && imageRegistries.length === 1 && results.length === 1) { - return results[0].props.children; - } + results.push({ + key: results.length, + value: image, + content: image.toString(), + description: image.Description + }); + }); + }); return results; }; @@ -804,6 +795,12 @@ export class ImageRunModal extends React.Component { ); + const spinnerOptions = ( + this.state.searchInProgress + ? [{ value: "_searching", content: , isDisabled: true }] + : [] + ); + /* ignore Enter key, it otherwise opens the first popover help; this clears * the search input and is still irritating from other elements like check boxes */ const defaultBody = ( @@ -901,30 +898,22 @@ export class ImageRunModal extends React.Component { } > - + // We do our own filtering when producing imageListOptions + filterFunction={(filterValue, options) => options} + selectOptions={imageListOptions.concat(spinnerOptions)} + footer={footer} /> {(image || localImage) && diff --git a/test/check-application b/test/check-application index 2e867b31..7e5c22ad 100755 --- a/test/check-application +++ b/test/check-application @@ -504,28 +504,29 @@ class TestApplication(testlib.MachineCase): # Check showing of entrypoint b.click("#containers-containers-create-container-btn") - b.click("#create-image-image-select-typeahead") - b.click(f'button.pf-v5-c-select__menu-item:contains("{IMG_REGISTRY}")') + b.click("#create-image-image input") + b.click(f'button.pf-v5-c-menu__item:contains("{IMG_REGISTRY}")') b.wait_val("#run-image-dialog-command", '/etc/docker/registry/config.yml') b.wait_text("#run-image-dialog-entrypoint", '/entrypoint.sh') # Deleting image will cleanup both command and entrypoint - b.click("button.pf-v5-c-select__toggle-clear") + b.click("button[aria-label='Clear input value']") b.wait_val("#run-image-dialog-command", '') b.wait_not_present("#run-image-dialog-entrypoint") # Edited command will not be cleared - b.click("#create-image-image-select-typeahead") - b.click(f'button.pf-v5-c-select__menu-item:contains("{IMG_REGISTRY}")') + b.click("#create-image-image input") + time.sleep(1) # wait for TypeaheadSelect focus shenanigans + b.click(f'button.pf-v5-c-menu__item:contains("{IMG_REGISTRY}")') b.wait_val("#run-image-dialog-command", '/etc/docker/registry/config.yml') b.set_input_text("#run-image-dialog-command", '/etc/docker/registry/config.yaml') - b.click("button.pf-v5-c-select__toggle-clear") + b.click("button[aria-label='Clear input value']") b.wait_not_present("#run-image-dialog-entrypoint") b.wait_val("#run-image-dialog-command", '/etc/docker/registry/config.yaml') # Setting a new image will still keep the old command and not prefill it - b.click("#create-image-image-select-typeahead") - b.click(f'button.pf-v5-c-select__menu-item:contains({IMG_ALPINE})') + b.click("#create-image-image input") + b.click(f'button.pf-v5-c-menu__item:contains({IMG_ALPINE})') b.wait_visible("#run-image-dialog-pull-latest-image") b.wait_val("#run-image-dialog-command", '/etc/docker/registry/config.yaml') @@ -553,8 +554,8 @@ class TestApplication(testlib.MachineCase): # Test the isSystem boolean for searching # https://github.com/cockpit-project/cockpit-podman/pull/891 b.click("#containers-containers-create-container-btn") - b.set_input_text("#create-image-image-select-typeahead", "registry") - b.wait_visible('button.pf-v5-c-select__menu-item:contains("registry")') + b.set_input_text("#create-image-image input", "registry") + b.wait_visible('button.pf-v5-c-menu__item:contains("registry")') self.confirm_modal("Cancel") def testBasicUser(self): @@ -875,9 +876,9 @@ class TestApplication(testlib.MachineCase): # Intermediate images are not shown in create container dialog b.click("#containers-containers-create-container-btn") b.wait_visible('div.pf-v5-c-modal-box header:contains("Create container")') - b.click("#create-image-image-select-typeahead") - b.wait_visible(f".pf-v5-c-select__menu-item:contains('{IMG_REGISTRY}')") - b.wait_not_present(".pf-v5-c-select__menu-item:contains('none')") + b.click("#create-image-image input") + b.wait_visible(f".pf-v5-c-menu__item:contains('{IMG_REGISTRY}')") + b.wait_not_present(".pf-v5-c-menu__item:contains('none')") b.click(".pf-v5-c-modal-box .btn-cancel") b.wait_not_present(".pf-v5-c-modal-box") @@ -1750,40 +1751,40 @@ class TestApplication(testlib.MachineCase): # Test special characters. # Initially both the registry image and Alpine are shown - b.click("#create-image-image") - b.wait_visible(f'button.pf-v5-c-select__menu-item:contains("{IMG_ALPINE_LATEST}")') - b.wait_visible(f'button.pf-v5-c-select__menu-item:contains("{IMG_REGISTRY_LATEST}")') + b.click("#create-image-image input") + b.wait_visible(f'button.pf-v5-c-menu__item:contains("{IMG_ALPINE_LATEST}")') + b.wait_visible(f'button.pf-v5-c-menu__item:contains("{IMG_REGISTRY_LATEST}")') # Filter it down to just Alpine - b.set_input_text("#create-image-image-select-typeahead", "|alpi*ne?\\") - b.wait_not_present(f'button.pf-v5-c-select__menu-item:contains("{IMG_REGISTRY_LATEST}")') - b.wait_visible(f'button.pf-v5-c-select__menu-item:contains("{IMG_ALPINE_LATEST}")') + b.set_input_text("#create-image-image input", "|alpi*ne?\\") + b.wait_not_present(f'button.pf-v5-c-menu__item:contains("{IMG_REGISTRY_LATEST}")') + b.wait_visible(f'button.pf-v5-c-menu__item:contains("{IMG_ALPINE_LATEST}")') # No local results found - b.set_input_text("#create-image-image-select-typeahead", "notfound") + b.set_input_text("#create-image-image input", "notfound") b.click('button.pf-v5-c-toggle-group__button:contains("Local")') - b.wait_text("button.pf-v5-c-select__menu-item.pf-m-disabled", "No images found") + b.wait_text("button.pf-v5-c-menu__item[disabled]", "No images found") # Local results found - b.set_input_text("#create-image-image-select-typeahead", "registry") - b.wait_text("button.pf-v5-c-select__menu-item", IMG_REGISTRY_LATEST) + b.set_input_text("#create-image-image input", "registry") + b.wait_text("button.pf-v5-c-menu__item:not([disabled])", IMG_REGISTRY_LATEST) if auth: b.assert_pixels(".pf-v5-c-modal-box", "image-select", skip_layouts=["rtl"]) # Local registry b.click('button.pf-v5-c-toggle-group__button:contains("localhost:5000")') - b.set_input_text("#create-image-image-select-typeahead", "my-busybox", blur=False) - b.wait_text("button.pf-v5-c-select__menu-item:not(.pf-m-disabled)", "localhost:5000/my-busybox") + b.set_input_text("#create-image-image input", "my-busybox", blur=False) + b.wait_text("button.pf-v5-c-menu__item:not([disabled])", "localhost:5000/my-busybox") # pressing Enter does not disturb the dialog or open popup help - b.wait_val("#create-image-image-select-typeahead", "my-busybox") + b.wait_val("#create-image-image input", "my-busybox") b.key("Enter") time.sleep(0.3) self.assertFalse(b.is_present(".pf-v5-c-popover")) - b.wait_val("#create-image-image-select-typeahead", "my-busybox") + b.wait_val("#create-image-image input", "my-busybox") # Select image - b.click('button.pf-v5-c-select__menu-item:contains("localhost:5000/my-busybox")') + b.click('button.pf-v5-c-menu__item:contains("localhost:5000/my-busybox")') # Remote image, no pull latest image option b.wait_not_present("#run-image-dialog-pull-latest-image") @@ -1807,14 +1808,14 @@ class TestApplication(testlib.MachineCase): b.set_input_text("#run-image-dialog-name", container_name) # Local registry - b.set_input_text("#create-image-image-select-typeahead", "no-container") - b.wait_text("button.pf-v5-c-select__menu-item.pf-m-disabled", "No images found") + b.set_input_text("#create-image-image input", "no-container") + b.wait_text("button.pf-v5-c-menu__item[disabled]", "No images found") # Search with full url - b.set_input_text("#create-image-image-select-typeahead", "localhost:5000/my-busybox") + b.set_input_text("#create-image-image input", "localhost:5000/my-busybox") b.click('button.pf-v5-c-toggle-group__button:contains("Local")') # Select image - b.click('button.pf-v5-c-select__menu-item:contains("localhost:5000/my-busybox")') + b.click('button.pf-v5-c-menu__item:contains("localhost:5000/my-busybox")') # Pull the latest image b.set_checked("#run-image-dialog-pull-latest-image", True) @@ -1839,20 +1840,20 @@ class TestApplication(testlib.MachineCase): b.set_input_text("#run-image-dialog-name", container_name) # Open image dropdown, it should list both Alpine and Busybox from the local repository - b.click("#create-image-image") - b.wait_visible(f'button.pf-v5-c-select__menu-item:contains("{IMG_ALPINE_LATEST}")') - b.wait_visible(f'button.pf-v5-c-select__menu-item:contains("{IMG_BUSYBOX_LATEST}")') + b.click("#create-image-image input") + b.wait_visible(f'button.pf-v5-c-menu__item:contains("{IMG_ALPINE_LATEST}")') + b.wait_visible(f'button.pf-v5-c-menu__item:contains("{IMG_BUSYBOX_LATEST}")') # Filter it down to just Busybox - b.set_input_text("#create-image-image-select-typeahead", IMG_BUSYBOX_LATEST) - b.wait_visible(f'button.pf-v5-c-select__menu-item:contains("{IMG_BUSYBOX_LATEST}")') - b.wait_not_present(f'button.pf-v5-c-select__menu-item:contains("{IMG_ALPINE_LATEST}")') + b.set_input_text("#create-image-image input", IMG_BUSYBOX_LATEST) + b.wait_visible(f'button.pf-v5-c-menu__item:contains("{IMG_BUSYBOX_LATEST}")') + b.wait_not_present(f'button.pf-v5-c-menu__item:contains("{IMG_ALPINE_LATEST}")') # Switch to another view; different images have different registries, so don't assume their name, just # that at least one more exists b.click('.image-search-footer .pf-v5-c-toggle-group__item:nth-of-type(3) button') - b.wait_in_text(".pf-v5-c-select__menu-list", "No images found") + b.wait_in_text(".pf-v5-c-menu", "No images found") # Back to local and let's select Busybox and create the container b.click('button.pf-v5-c-toggle-group__button:contains("Local")') - b.click(f'button.pf-v5-c-select__menu-item:contains("{IMG_BUSYBOX_LATEST}")') + b.click(f'button.pf-v5-c-menu__item:contains("{IMG_BUSYBOX_LATEST}")') b.click('.pf-v5-c-modal-box__footer #create-image-create-run-btn') b.wait(lambda: self.getContainerAttr(container_name, "State", sel) in 'Running') @@ -1896,7 +1897,8 @@ class TestApplication(testlib.MachineCase): b.wait_visible('div.pf-v5-c-modal-box header:contains("Create container")') # Inspect and fill modal dialog - b.wait_val("#create-image-image-select-typeahead", IMG_BUSYBOX_LATEST) + b.wait_val("#create-image-image input", IMG_BUSYBOX_LATEST) + time.sleep(1) # wait for TypeaheadSelect focus shenanigans # Check that there is autogenerated name and then overwrite it b.wait_not_val("#run-image-dialog-name", "") @@ -2189,7 +2191,7 @@ class TestApplication(testlib.MachineCase): b.click(f'#containers-images tbody tr:contains("{IMG_BUSYBOX}") .ct-container-create') b.wait_visible('div.pf-v5-c-modal-box header:contains("Create container")') - b.wait_val("#create-image-image-select-typeahead", IMG_BUSYBOX_LATEST) + b.wait_val("#create-image-image input", IMG_BUSYBOX_LATEST) b.set_input_text("#run-image-dialog-name", "busybox-without-publish") # Set up command line @@ -2272,7 +2274,7 @@ class TestApplication(testlib.MachineCase): b.click(f'#containers-images tbody tr:contains("{IMG_BUSYBOX}") .ct-container-create') b.wait_visible('div.pf-v5-c-modal-box header:contains("Create container")') - b.wait_val("#create-image-image-select-typeahead", IMG_BUSYBOX_LATEST) + b.wait_val("#create-image-image input", IMG_BUSYBOX_LATEST) b.set_input_text("#run-image-dialog-name", container_name) b.set_input_text("#run-image-dialog-command", "sh -c sleep infinity") @@ -2813,20 +2815,20 @@ class TestApplication(testlib.MachineCase): b.set_input_text("#run-image-dialog-name", container_name) # Open image dropdown, it should list both Alpine and Busybox from the local repository - b.click("#create-image-image") - b.wait_visible(f'button.pf-v5-c-select__menu-item:contains("{IMG_ALPINE_LATEST}")') - b.wait_visible(f'button.pf-v5-c-select__menu-item:contains("{IMG_BUSYBOX_LATEST}")') + b.click("#create-image-image input") + b.wait_visible(f'button.pf-v5-c-menu__item:contains("{IMG_ALPINE_LATEST}")') + b.wait_visible(f'button.pf-v5-c-menu__item:contains("{IMG_BUSYBOX_LATEST}")') # Filter it down to just Busybox - b.set_input_text("#create-image-image-select-typeahead", IMG_BUSYBOX_LATEST) - b.wait_visible(f'button.pf-v5-c-select__menu-item:contains("{IMG_BUSYBOX_LATEST}")') - b.wait_not_present(f'button.pf-v5-c-select__menu-item:contains("{IMG_ALPINE_LATEST}")') + b.set_input_text("#create-image-image input", IMG_BUSYBOX_LATEST) + b.wait_visible(f'button.pf-v5-c-menu__item:contains("{IMG_BUSYBOX_LATEST}")') + b.wait_not_present(f'button.pf-v5-c-menu__item:contains("{IMG_ALPINE_LATEST}")') # Switch to another view; different images have different registries, so don't assume their name, just # that at least one more exists b.click('.image-search-footer .pf-v5-c-toggle-group__item:nth-of-type(3) button') - b.wait_in_text(".pf-v5-c-select__menu-list", "No images found") + b.wait_in_text(".pf-v5-c-menu", "No images found") # Back to local and let's select Busybox and create the container b.click('button.pf-v5-c-toggle-group__button:contains("Local")') - b.click(f'button.pf-v5-c-select__menu-item:contains("{IMG_BUSYBOX_LATEST}")') + b.click(f'button.pf-v5-c-menu__item:contains("{IMG_BUSYBOX_LATEST}")') b.click('.pf-v5-c-modal-box__footer #create-image-create-run-btn') b.wait_not_present("#run-image-dialog-name") @@ -3131,15 +3133,15 @@ class TestApplication(testlib.MachineCase): b.click("#containers-containers button.pf-v5-c-button.pf-m-primary") b.set_input_text("#run-image-dialog-name", "new-busybox") # Searching for container by prefix fails - b.set_input_text("#create-image-image-select-typeahead", "localhost:80/my-busy") - b.wait_text("button.pf-v5-c-select__menu-item.pf-m-disabled", "No images found") + b.set_input_text("#create-image-image input", "localhost:80/my-busy") + b.wait_text("button.pf-v5-c-menu__item[disabled]", "No images found") b.wait_in_text(".pf-v5-c-alert.pf-m-danger", "couldn't search registry") # Giving full image name finds valid manifest therefore image is pullable - b.set_input_text("#create-image-image-select-typeahead", container_name) - b.wait_text("button.pf-v5-c-select__menu-item:not(.pf-m-disabled)", container_name) + b.set_input_text("#create-image-image input", container_name) + b.wait_text("button.pf-v5-c-menu__item:not([disabled])", container_name) # Select image and create a container - b.click(f"button.pf-v5-c-select__menu-item:contains({container_name})") + b.click(f"button.pf-v5-c-menu__item:contains({container_name})") b.click("#create-image-create-btn") # Wait for download to finish @@ -3150,25 +3152,25 @@ class TestApplication(testlib.MachineCase): b.click("#containers-containers button.pf-v5-c-button.pf-m-primary") # "couldn't search registry" error is hidden when some local image is found - b.set_input_text("#create-image-image-select-typeahead", "") - b.set_input_text("#create-image-image-select-typeahead", "localhost:80/my-busy") - b.wait_text("button.pf-v5-c-select__menu-item:not(.pf-m-disabled)", f"{container_name}:latest") + b.set_input_text("#create-image-image input", "") + b.set_input_text("#create-image-image input", "localhost:80/my-busy") + b.wait_text("button.pf-v5-c-menu__item:not([disabled])", f"{container_name}:latest") b.wait_not_present(".pf-v5-c-alert.pf-m-danger") b.click('button.pf-v5-c-toggle-group__button:contains("Local")') # Error is shown again when no image was found locally - b.set_input_text("#create-image-image-select-typeahead", "localhost:80/their-busy") - b.wait_text("button.pf-v5-c-select__menu-item.pf-m-disabled", "No images found") + b.set_input_text("#create-image-image input", "localhost:80/their-busy") + b.wait_text("button.pf-v5-c-menu__item[disabled]", "No images found") b.wait_in_text(".pf-v5-c-alert.pf-m-danger", "couldn't search registry") # Searching for full container name with tag finds the image b.set_input_text("#run-image-dialog-name", "tagged-busybox") # Search should still work with spaces around image name - b.set_input_text("#create-image-image-select-typeahead", " localhost:80/my-busybox:nosearch-tag ") + b.set_input_text("#create-image-image input", " localhost:80/my-busybox:nosearch-tag ") b.click('button.pf-v5-c-toggle-group__button:contains("All")') - b.wait_text("button.pf-v5-c-select__menu-item:not(.pf-m-disabled)", container_name + ":nosearch-tag") - b.wait_js_cond("document.getElementsByClassName('pf-v5-c-select__menu-item').length === 1") - b.click(f"button.pf-v5-c-select__menu-item:contains({container_name + ":nosearch-tag"})") + b.wait_text("button.pf-v5-c-menu__item:not([disabled])", container_name + ":nosearch-tag") + b.wait_js_cond("document.querySelectorAll('.pf-v5-c-menu__item:not([disabled])').length === 1") + b.click(f"button.pf-v5-c-menu__item:contains({container_name + ":nosearch-tag"})") b.click("#create-image-create-btn") # Wait for download to finish @@ -3178,9 +3180,9 @@ class TestApplication(testlib.MachineCase): # Check that manifest search with tag also works on searchable repository b.click("#containers-containers button.pf-v5-c-button.pf-m-primary") - b.set_input_text("#create-image-image-select-typeahead", "localhost:5000/my-busybox:search-tag") - b.wait_text("button.pf-v5-c-select__menu-item:not(.pf-m-disabled)", "localhost:5000/my-busybox:search-tag") - b.wait_js_cond("document.getElementsByClassName('pf-v5-c-select__menu-item').length === 1") + b.set_input_text("#create-image-image input", "localhost:5000/my-busybox:search-tag") + b.wait_text("button.pf-v5-c-menu__item:not([disabled])", "localhost:5000/my-busybox:search-tag") + b.wait_js_cond("document.querySelectorAll('.pf-v5-c-menu__item:not([disabled])').length === 1") if __name__ == '__main__': diff --git a/test/reference b/test/reference index 18c2db44..960d3140 160000 --- a/test/reference +++ b/test/reference @@ -1 +1 @@ -Subproject commit 18c2db442c0d3d13f0752981456351e2701aeb86 +Subproject commit 960d3140db01ff2226edb71af25a9df9fa829493