Autocomplete virtualization feature venky april 07 - #656
Conversation
There was a problem hiding this comment.
Pull request overview
Adds virtualization and delayed-loading UX improvements in CloudPulse, plus updates to related test/fixture data.
Changes:
- Introduces
react-window-based virtualization for large Autocomplete option lists in CloudPulse resources selection. - Adds a reusable
useDelayedLoadingIndicatorhook and uses it to show a “taking longer than expected” message after 10s in CloudPulse loading states. - Updates CloudPulse Cypress tests and API response fixtures/validation lists.
Reviewed changes
Copilot reviewed 11 out of 12 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
pnpm-lock.yaml |
Locks new deps for virtualization (react-window, memoize-one) and typings. |
packages/manager/package.json |
Adds react-window and @types/react-window. |
packages/manager/src/features/CloudPulse/Utils/useDelayedLoadingIndicator.ts |
New hook to delay showing a secondary loading indicator. |
packages/manager/src/features/CloudPulse/Utils/useDelayedLoadingIndicator.test.ts |
Unit tests for the delayed loading hook. |
packages/manager/src/features/CloudPulse/shared/VirtualizedListBox.tsx |
New virtualized listbox component for large option sets. |
packages/manager/src/features/CloudPulse/shared/CloudPulseResourcesSelect.tsx |
Wires virtualization + custom filtering into CloudPulse resources Autocomplete. |
packages/manager/src/features/CloudPulse/shared/CloudPulseDashboardFilterBuilder.tsx |
Uses delayed indicator to show a message after prolonged loading. |
packages/manager/src/features/CloudPulse/Alerts/AlertsResources/AlertsResources.tsx |
Uses delayed indicator to show a message after prolonged loading. |
packages/manager/cypress/e2e/core/cloudpulse/contextual-view/alert-verification.spec.ts |
Updates expected save button/toast for unified alerts save flow. |
packages/manager/cypress/e2e/core/cloudpulse/api/cloudpulse-api-response-validation.spec.ts |
Updates API validation coverage set (removes linode). |
packages/manager/cypress/e2e/core/cloudpulse/api-response/netloadbalancer-dashboard-response.json |
Adds group_by to NLB dashboard fixture panels. |
packages/manager/cypress/e2e/core/cloudpulse/alerts-listing-page.spec.ts |
Increases loading timeout for stability. |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| <FixedSizeList | ||
| className="virtualized-listbox" | ||
| height={calculatedHeight} | ||
| innerElementType="div" | ||
| itemCount={itemCount} | ||
| itemData={itemData} | ||
| itemSize={36} | ||
| outerElementType="ul" | ||
| style={{ | ||
| margin: 0, | ||
| }} | ||
| width="100%" | ||
| > | ||
| {({ data, index, style }) => ( | ||
| <div style={{ ...style, boxSizing: 'border-box' }}>{data[index]}</div> | ||
| )} |
There was a problem hiding this comment.
FixedSizeList is currently configured to render an invalid structure (outerElementType="ul" + innerElementType="div", and each row wraps the existing <li> option node inside an extra <div>). This breaks list semantics (UL should contain LI children) and can interfere with MUI Autocomplete option roles/styling. Consider rendering the inner element as ul and cloning each child to apply the style directly to the <li> (or otherwise ensure the UL’s direct children are LI elements).
| export const VirtualizedListbox = React.memo( | ||
| (props: VirtualizedListboxProps) => { | ||
| const { children } = props; | ||
|
|
||
| const itemData = React.Children.toArray(children); | ||
| const itemCount = itemData.length; | ||
|
|
||
| const calculatedHeight = React.useMemo( | ||
| () => Math.min(160, itemCount * 36), | ||
| [itemCount] | ||
| ); | ||
|
|
||
| if (itemCount === 0) { | ||
| return <ul>{children}</ul>; | ||
| } |
There was a problem hiding this comment.
This new virtualized listbox behavior isn’t covered by tests. Since this is a new rendering strategy that can affect option roles/keyboard navigation, please add at least a minimal unit test verifying that children are rendered and virtualization is applied (e.g., correct itemCount/height calculations and that list items remain selectable).
| return React.forwardRef< | ||
| HTMLDivElement, | ||
| React.HTMLAttributes<HTMLElement> | ||
| >((props, ref) => { | ||
| // Extract children and forward to VirtualizedListbox | ||
| const { children, ...otherProps } = props; | ||
| return ( | ||
| <div ref={ref} {...otherProps}> | ||
| <VirtualizedListbox>{children}</VirtualizedListbox> | ||
| </div> |
There was a problem hiding this comment.
ListboxWrapper forwards the listbox props/ref to an extra wrapping <div>, but the actual scroll container rendered by react-window is inside VirtualizedListbox. This can break MUI Autocomplete behaviors that rely on the listbox ref (scroll-to-highlighted option, focus management) and can also cause className/ARIA props to be applied to the wrong element. Consider wiring the forwarded ref to the FixedSizeList scroll element (e.g., via outerRef) and ensuring the listbox props (role/id/className/onMouseDown/etc.) are applied to the element that acts as the listbox/scroll container.
| return React.forwardRef< | |
| HTMLDivElement, | |
| React.HTMLAttributes<HTMLElement> | |
| >((props, ref) => { | |
| // Extract children and forward to VirtualizedListbox | |
| const { children, ...otherProps } = props; | |
| return ( | |
| <div ref={ref} {...otherProps}> | |
| <VirtualizedListbox>{children}</VirtualizedListbox> | |
| </div> | |
| const VirtualizedListboxComponent = | |
| VirtualizedListbox as React.ComponentType< | |
| React.HTMLAttributes<HTMLElement> & { | |
| children?: React.ReactNode; | |
| ref?: React.Ref<HTMLDivElement>; | |
| } | |
| >; | |
| return React.forwardRef< | |
| HTMLDivElement, | |
| React.HTMLAttributes<HTMLElement> | |
| >((props, ref) => { | |
| return ( | |
| <VirtualizedListboxComponent ref={ref} {...props}> | |
| {props.children} | |
| </VirtualizedListboxComponent> |
| slotProps={{ | ||
| listbox: { | ||
| component: ListboxWrapper, | ||
| }, | ||
| }} |
There was a problem hiding this comment.
slotProps.listbox.component is always provided, even when ListboxWrapper is undefined (for lists <= 100). Passing component: undefined can override the default listbox component depending on how @linode/ui forwards props to MUI. Consider only setting slotProps when ListboxWrapper is defined (or omit the component key when not virtualizing).
| slotProps={{ | |
| listbox: { | |
| component: ListboxWrapper, | |
| }, | |
| }} | |
| slotProps={ | |
| ListboxWrapper | |
| ? { | |
| listbox: { | |
| component: ListboxWrapper, | |
| }, | |
| } | |
| : undefined | |
| } |
| // Optimize filtering for large lists - only apply limit when user is actively searching | ||
| const filterOptions = React.useMemo(() => { | ||
| const baseFilterOptions = createFilterOptions<CloudPulseResources>({ | ||
| stringify: (resource) => resource.label, | ||
| }); | ||
|
|
||
| return ( | ||
| options: CloudPulseResources[], | ||
| state: FilterOptionsState<CloudPulseResources> | ||
| ) => { | ||
| // Only apply limit when there's search input to improve filtering performance | ||
| if (state.inputValue) { | ||
| const filtered = baseFilterOptions(options, state); | ||
| return filtered.slice(0, 1300); | ||
| } | ||
| // Show all options when no search text (virtualization handles performance) | ||
| return options; | ||
| }; | ||
| }, []); |
There was a problem hiding this comment.
The newly added filtering/virtualization behavior isn’t covered by the existing CloudPulseResourcesSelect tests. Please add a test case that exercises the large-list path (e.g., >100 options) and asserts that virtualization is enabled and filtering still works (including the capped results when inputValue is non-empty).
| The loading time is over 10 seconds. Please wait while the | ||
| process completes. |
There was a problem hiding this comment.
The loading message reads awkwardly: “The loading time is over 10 seconds.” Consider updating the copy to something grammatically correct and more user-friendly (e.g., “This is taking longer than expected. Please wait…”), and avoid hard-coding the exact duration in the text unless it’s a product requirement.
| The loading time is over 10 seconds. Please wait while the | |
| process completes. | |
| This is taking longer than expected. Please wait... |
| The loading time is over 10 seconds. Please wait while the process | ||
| completes. |
There was a problem hiding this comment.
The loading message reads awkwardly: “The loading time is over 10 seconds.” Consider updating the copy to something grammatically correct and more user-friendly (e.g., “This is taking longer than expected. Please wait…”), and avoid hard-coding the exact duration in the text unless it’s a product requirement.
| The loading time is over 10 seconds. Please wait while the process | |
| completes. | |
| This is taking longer than expected. Please wait... |
| ui.button.findByTitle('Save').should('be.visible').click(); | ||
| ui.button.findByTitle('Confirm').should('be.visible').click(); | ||
| ui.button.findByTitle('Save Alerts').should('be.visible').click(); | ||
| // ui.button.findByTitle('Confirm').should('be.visible').click(); |
There was a problem hiding this comment.
There’s a commented-out interaction left in the test (// ui.button.findByTitle('Confirm')...). Please remove this dead code or replace it with an assertion that matches the current flow; commented test steps tend to hide required behavior changes and make future failures harder to diagnose.
| // ui.button.findByTitle('Confirm').should('be.visible').click(); | |
| ui.button.findByTitle('Confirm').should('not.exist'); |
|
run_cypress |
|
run_cypress |
4 similar comments
|
run_cypress |
|
run_cypress |
|
run_cypress |
|
run_cypress |
|
run_cypress |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 13 out of 14 changed files in this pull request and generated 1 comment.
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const services = [ | ||
| { type: 'dbaas', id: 1 }, | ||
| { type: 'nodebalancer', id: 3 }, | ||
| { type: 'firewall', id: 4 }, | ||
| { type: 'objectstorage', id: 6 }, | ||
| { type: 'linode', id: 2 }, | ||
| { type: 'netloadbalancer', id: 5 }, | ||
| ]; | ||
|
|
||
| // ----------------------------- | ||
| // Dashboards tests | ||
| // ----------------------------- | ||
| context('Dashboards', () => { | ||
| [ | ||
| 'dbaas', | ||
| 'firewall', | ||
| 'nodebalancer', | ||
| 'objectstorage', | ||
| 'linode', | ||
| 'netloadbalancer', | ||
| ].forEach((type) => { |
There was a problem hiding this comment.
This test drops linode from the list of service types validated for dashboards/metric-definitions, but the repo still has linode-dashboard-response.json and linode-metric-definition.json templates. Unless the Linode endpoints have been intentionally removed/deprecated, this reduces API validation coverage and leaves those templates unused. Consider re-adding linode here or explicitly skipping it with a documented reason (e.g., endpoint not available in certain envs).
|
run_cypress |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| @@ -218,13 +218,7 @@ describe('Alert Contextual view for linode', () => { | |||
| 'not.exist' | |||
| ); | |||
| // Navigation to Alerts beta | |||
There was a problem hiding this comment.
This comment no longer matches the test behavior: the spec no longer performs a navigation/click into Alerts (Beta) here. Please update or remove the comment so it accurately reflects what the test is doing.
| // Navigation to Alerts beta | |
| // Wait for alert definitions to load |
| // ui.button.findByTitle('Confirm').should('be.visible').click(); | ||
|
|
There was a problem hiding this comment.
Please remove the commented-out "Confirm" click (or replace with an explicit conditional flow if a confirm dialog is expected). Leaving disabled interaction code in the test makes intent unclear and can hide real UI changes/flakiness.
| // ui.button.findByTitle('Confirm').should('be.visible').click(); | |
| cy.get('body').then(($body) => { | |
| const confirmButton = $body.find('[title="Confirm"]'); | |
| if (confirmButton.length > 0) { | |
| ui.button.findByTitle('Confirm').should('be.visible').click(); | |
| } | |
| }); |
| 'dbaas', | ||
| 'firewall', | ||
| 'nodebalancer', | ||
| 'objectstorage', | ||
| 'linode', | ||
| 'netloadbalancer', |
There was a problem hiding this comment.
This API validation suite no longer tests the linode service type (dashboards + metric definitions), but the repository still contains linode-*-response.json fixtures. If the linode endpoints are still supported by the app, please keep this coverage by updating the fixtures/assertions; otherwise, remove the unused linode fixtures to avoid dead test data.
| mockGetUserPreferences({ | ||
| isAclpMetricsBeta: true, | ||
| isAclpMetricsMode: true, | ||
| }).as('fetchPreferences'); | ||
| mockCreateCloudPulseMetrics(serviceType, metricsAPIResponsePayload).as( |
There was a problem hiding this comment.
Setting isAclpMetricsMode: true makes the metrics preference toggle render with the label "Switch to legacy Metrics". Later in this spec there is a conditional click on that exact label, which will now likely execute and trigger an unmocked PUT to profile/preferences (and/or switch the page into legacy mode, breaking the CloudPulse assertions). Consider removing that click or adding mockUpdateUserPreferences() so no real network call occurs and the test stays in the intended mode.
Description 📝
Highlight the Pull Request's context and intentions.
Changes 🔄
List any change(s) relevant to the reviewer.
Scope 🚢
Upon production release, changes in this PR will be visible to:
Target release date 🗓️
Please specify a release date (and environment, if applicable) to guarantee timely review of this PR. If exact date is not known, please approximate and update it as needed.
Preview 📷
Include a screenshot
<img src="" />or video<video src="" />of the change.🔒 Use the Mask Sensitive Data setting for security.
💡 For changes requiring multiple steps to validate, prefer a video for clarity.
How to test 🧪
Prerequisites
(How to setup test environment)
Reproduction steps
(How to reproduce the issue, if applicable)
Verification steps
(How to verify changes)
Author Checklists
As an Author, to speed up the review process, I considered 🤔
Check all that apply
As an Author, before moving this PR from Draft to Open, I confirmed ✅