Repository navigation
console: Fix second loading spinner on the cluster list - #39875
Merged
leedqin merged 1 commit intoOct 19, 2026
Merged
Conversation
jdonelson
approved these changes
Oct 16, 2026
|
|
||
| /** `useAvailableClusterSizes`, with `data` undefined while loading instead of suspending. */ | ||
| export function useAvailableClusterSizesWithoutSuspense() { | ||
| return useQuery(availableClusterSizesQuery()); |
Contributor
There was a problem hiding this comment.
The 2 flavors of this hook feel unnecessary. They're both wrappers around 1-line calls to another hook. It seems like the calling locations could just call useQuery(availableClusterSizesQuery()); or useSuspenseQuery(availableClusterSizesQuery()); and we'd save ourselves 1 layer of complexity. Unless there's something I'm not seeing, of course.
Contributor
Author
There was a problem hiding this comment.
Agreed, removed both in 64f5bb6: the call sites now use useQuery(availableClusterSizesQuery()) or useSuspenseQuery(availableClusterSizesQuery()) directly.
| <> | ||
| <MenuItem | ||
| // The modal needs the sizes. A click before they load would otherwise | ||
| // open it later, unprompted. |
Contributor
There was a problem hiding this comment.
I don't know if this comment is necessary.
On a cold load the cluster list showed its spinner, rendered the rows once the clusters SUBSCRIBE snapshot arrived, and then replaced them with a second spinner. Each row's closed overflow menu mounts AlterClusterMenuItem, whose cluster sizes query was a useSuspenseQuery, so the first render of the rows suspended to the page's Suspense fallback until the sizes returned. The menu item now reads the sizes with useQuery on a shared availableClusterSizesQuery and stays disabled until they load. The new cluster form and the new replica modal still read them with useSuspenseQuery. Closes: CNS-192
leedqin
force-pushed
the
console-cluster-list-double-spinner
branch
from
October 19, 2026 17:33
466baa3 to
64f5bb6
Compare
leedqin
added a commit
that referenced
this pull request
Nov 2, 2026
…page (#39992) ### Motivation Closes: [CNS-204](https://linear.app/materializeinc/issue/CNS-204) Seventh PR in the cluster details redesign stack, on top of #39889. The redesigned cluster page kept the classic page's Replicas tab as a stopgap, and its only way to edit or drop the cluster was the breadcrumb's overflow menu. This moves a cluster's configuration into a drawer opened from the page header, in the same format as the Connect drawer, and drops the Replicas tab. ### Description **Opening it.** A gear button next to the cluster's title, after its size and replica pills, opens **Configure ‹cluster›**. - The open section is the `configure` search parameter (`overview`, `replicas` or `auto-scaling`), so it survives a reload and can be linked. - `useSyncObjectToSearchParams`, which lists such as the Objects tab's Sources and Sinks use to put their sort in the URL, now keeps search parameters it didn't write, so `configure` survives on every tab. It still removes a key it wrote once its object drops it, such as a cleared filter. - The classic Replicas tab's URL, `…/replicas`, redirects to `?configure=replicas`. - A rename from Edit keeps the parameter, so the drawer stays open on the renamed cluster. - On the redesigned page, the breadcrumb no longer shows its Alter/Drop overflow menu, so the drawer is the one place to edit or drop the cluster. The classic page is unchanged. - The gear and the drawer each look the cluster up in their own component, so another cluster's status updates don't re-render the page. **Layout.** It's a 640px side drawer, like the Connect drawer, with a row of `ConnectMethodCard`s that switch the panel below: - **Overview**, subtitled with the size, or "Unmanaged". - Shows name, size, management, replication factor, owner and creation time. - While a resize is in progress, an info alert names its target size, and its target replication factor when that changes too. `mz_clusters`, and so the details, keep the current values until it finishes. - **Edit** opens the existing Alter cluster modal for managed user clusters. Like the cluster list's Alter cluster item since #39875, it reads the sizes without suspending. It stays disabled until the sizes load. The Alter cluster modal now starts its Replicas field at the cluster's replication factor, which the app-wide clusters subscribe now carries. It used to start at the replica count, which also counts a hydration burst's or a graceful resize's extra replicas, so saving any edit during one kept them. It also sends the size and replication factor only when they change, and leaves out the SET when neither does. While a resize runs, `mz_clusters` reports the realized size and factor, and setting both back to them cancels the resize, so saving a rename used to cancel it. Both fix the classic page's Alter cluster item too. - Owners of a user cluster also get a **Drop cluster** danger zone, using the existing drop confirmation. - **Replicas**, subtitled "N online" or "N of M online". - A UniversalTable of each replica's name, size, status and creation time. An offline replica's pill carries its reason. - Unmanaged clusters get **New replica** and a per-replica drop for owners, through the existing modals. Managed clusters explain that they run one replica per unit of replication factor, counted from the factor so a running burst replica isn't included, since the server rejects dropping one of their replicas. Only user clusters are pointed at Edit. - **Auto-scaling**, subtitled "Enabled" or "Off", shown only from v26.34.0, when `mz_internal.mz_cluster_auto_scaling_strategies` first shipped. - Shows the burst size for hydration and the linger duration. - A note while a burst replica is running, including after its policy was removed and the strategy reads as null. - Copyable `ALTER CLUSTER … SET (AUTO SCALING STRATEGY = …)` and `RESET` statements to change or remove the policy, or the statement to turn one on. The linger is written as whole seconds where it is one, and in milliseconds otherwise, so copying the statement keeps a 0s or 1.5s linger. System clusters get none, since they can't be altered. - Unmanaged clusters are told autoscaling needs a managed cluster. **Data.** The cluster list already carries name, size, management, the replication factor (added to its subscribe here), owner id and replica statuses, and `useOwners` already has every role's name. Everything else comes from one `executeSqlV2` request when the drawer opens: - creation times, from `mz_object_lifetimes`; - the autoscaling strategy, by cluster id; - an in-progress reconfiguration's target size and replication factor, from `mz_cluster_reconfigurations`, which shipped with the autoscaling relation in v26.34.0. The request reads at strict serializable, the session default. A serializable read can come back from before a write that just committed, and since the drawer doesn't poll, a replica added or a policy altered moments earlier could stay missing until the drawer reopened. Cluster and replica ids share the `u<N>` form but come from separate counters, so a cluster's id can also be a replica's. `buildObjectCreationTimesQuery` now takes the object types to read and returns each row's `object_type`, and the drawer matches creation times on type and id. When the replica set changes, for example a resize or a burst replica coming and going, the request's key changes. The drawer keeps showing the previous answer for the same cluster until the new one arrives, rather than blanking the overview. `mz_cluster_auto_scaling_strategies` and `mz_cluster_reconfigurations` are added to `types/materialize.d.ts` by hand, matching the generated shape, with the file's TODO for hand-added relations. The owners, the replica limit and, where Edit can show, the sizes are read once by the drawer and passed to its sections, so switching sections makes no requests. `buildObjectCreationTimesQuery` requires its object types, and the earlier caller passes its two explicitly. **Also:** a `SettingsIcon` in the icon set, and an `ownerName` lookup on `useOwners`. ### Verification - `clusterConfigurationModel.test.ts`: - the strategy JSON's hydration size, linger and in-flight burst, no policy, and a burst that outlasts a removed policy; - a replica is online only when every process is; - the generated statements quote the cluster and write the linger in seconds, or in milliseconds when it isn't a whole number of seconds, including 0; - a size with a quote is escaped; - the resize note names the target size, and the replication factor when it changes too, and says nothing when neither changes. - `ClusterConfigurationDrawer.test.tsx`, with creation-time fixtures where each id is also an object of the other type: - the overview shows the owner, the replication factor, the cluster's own creation time, Edit and Drop; - an in-progress resize's target size shows in an alert on the overview; - Edit is disabled while the sizes load, then opens the Alter cluster modal, filled from the replication factor even while a burst replica runs; - switching sections back and forth reads the owners, the replica limit and the sizes once; - the Replicas text counts the replication factor rather than a burst replica, falls back when the factor can't be read, and doesn't point a system cluster at Edit; - an unmanaged cluster's owner can drop a replica from its row; - a zero linger shows and copies as 0s; - a non-owner gets no Drop, and a system cluster gets no Edit, Drop or statements; - the Replicas card switches to the replica table with each replica's own creation time; - an unmanaged cluster's owner gets New replica, and its auto-scaling section says it needs a managed cluster; - a configured policy, an unset one, and a running burst after the policy is removed; - a failed read shows an error and keeps what the cluster list has; - an environment before v26.34.0 has no auto-scaling card and opens a link to it on the overview. - `ClusterDetail.test.tsx`: - the redesigned page has no Replicas tab and has the gear; - the gear opens the drawer on the overview; - the classic Replicas tab's URL opens it on the replicas; - the gear opens it over the Sources list, whose real sort hook rewrites the query string. - `useSyncObjectToSearchParams.test.ts`: a search parameter the hook didn't write survives, and one it wrote goes once its object drops it. - `queries.test.ts`: `useOwners` names an owner by its role id. - `clusterConfiguration.test.sql.ts`, against a real Materialize: creation times told apart for a cluster and a replica that share an id, and autoscaling policies, with and without a linger, parsed as the drawer parses them, and no resize target once a resize settles or for a cluster never resized. - `clusterList.test.sql.ts` reads a managed cluster's replication factor and none for an unmanaged one, and the clusters subscribe tests expect the new column. - `AlterClusterModal.test.tsx`: the Replicas field starts at the replication factor, not a running burst's replica count; a rename sends only the RENAME; a replication factor change sends no SIZE. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- devin-review-badge-begin --> --- <a href="https://app.devin.ai/review/materializeinc/materialize/pull/39992" target="_blank"><picture><source media="(prefers-color-scheme: dark)" srcset="https://static.devin.ai/assets/gh-devin-review-dark.svg?v=4"><img src="https://static.devin.ai/assets/gh-devin-review-light.svg?v=4" alt="Devin Review"></picture></a> <!-- devin-review-badge-end -->
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
Closes: CNS-192
On a cold load the cluster list shows its loading spinner and renders the rows once the clusters SUBSCRIBE snapshot arrives. It then swaps them for a second spinner before the table finally shows. Both the classic table and the per-replica preview table are affected.
Each row's overflow menu renders its items while the menu is closed. For clusters the user owns that are managed and not system clusters, that includes
AlterClusterMenuItem, which read the cluster sizes throughuseAvailableClusterSizes, auseSuspenseQuery. On a cold cache the first render of the rows suspends, and React hides them behind theClustersListPageSuspense fallback until the sizes return.Before #38014 the list itself loaded through a suspending query, so both waits sat behind one fallback. Since the list moved to the subscribe-fed atom with its own loading state, they show up as two separate spinners.
Description
useAvailableClusterSizesWithoutSuspensereads the same query, with the same key and fetcher, without suspending.AlterClusterMenuItemuses it.useAvailableClusterSizeskeeps suspending forNewClusterFormandNewReplicaModal. SwitchingNewClusterFormwould break its default size: the size<select>is uncontrolled with adefaultValue, so options that arrive after mount leave the first size selected instead of the default.Not in this PR:
NewReplicaModalis mounted while closed on the cluster Replicas tab and still suspends on the sizes. That serializes the sizes request after the replicas query.OverflowMenumounts every item's hooks while the menu is closed. Making it lazy would remove the per-row fetches entirely, but it changes every overflow menu in the console.Verification
New tests in
ClustersList.test.tsxhold the sizes request open and check that the per-replica rows and the classic table's rows still render, and that "Alter cluster" is disabled until the sizes load and enabled after. The loading tests fail against the previousAlterClusterMenuItem.I also reproduced the bug with a temporary harness, not committed, that renders the real route tree and subscribe stack against a mocked SUBSCRIBE WebSocket. Before: spinner, snapshot, second spinner with the table hidden, table. After: spinner, snapshot, table. Not yet checked in a browser against production.
🤖 Generated with Claude Code