fix(studio): drop unused layout work from the replica sheet close bug

The overview gap was sheet close restoring focus, not chart h-full or a custom scroller.
This commit is contained in:
Danny White committed 2026-08-21 11:33:03 +10:00
1 parent c9bd30a42b
commit ce830fae16
7 files changed
+138 -286

No files matched your search

@@ -93,7 +93,7 @@ const PERCENTAGE_Y_AXIS_PROPS = {
const getUsageCardClassName = (status: UsageMetricStatus) =>
cn(
'flex flex-col transition-colors',
'h-full flex flex-col transition-colors',
status === 'warning' && 'border-warning-400 bg-warning-200/30',
status === 'negative' && 'border-destructive-400 bg-destructive-200/30'
)
@@ -160,14 +160,14 @@ export const ComputeAndDiskUsageCharts = ({ className }: { className?: string })
return (
<div
className={cn(
'grid grid-cols-1 gap-4 @[680px]:grid-cols-2 @[680px]:items-stretch [overflow-anchor:none]',
'grid grid-cols-1 gap-4 @[680px]:grid-cols-2 @[680px]:items-stretch',
className
)}
>
<div id="cpu" className="scroll-mt-24 min-h-0">
<div id="cpu" className="scroll-mt-24 h-full min-h-0">
<span id="ram" className="block h-0 scroll-mt-24" aria-hidden />
<span id="disk_io" className="block h-0 scroll-mt-24" aria-hidden />
<Chart isLoading={isLoading} isErrored={isError}>
<Chart isLoading={isLoading} isErrored={isError} className="h-full">
<ChartCard className={getUsageCardClassName(computeUsageStatus)}>
<ChartHeader align="start" className="min-h-[5.5rem]">
<ChartMetric
@@ -245,8 +245,8 @@ export const ComputeAndDiskUsageCharts = ({ className }: { className?: string })
</Chart>
</div>
<div id="disk" className="scroll-mt-24 min-h-0">
<Chart isLoading={isLoading} isErrored={isError}>
<div id="disk" className="scroll-mt-24 h-full min-h-0">
<Chart isLoading={isLoading} isErrored={isError} className="h-full">
<ChartCard className={getUsageCardClassName(diskUsageStatus)}>
<ChartHeader align="start" className="min-h-[5.5rem]">
<ChartMetric
@@ -68,16 +68,7 @@ export function ComputeSection({
<DocsButton href={`${DOCS_URL}/guides/platform/compute-and-disk`} />
</PageSectionAside>
</PageSectionMeta>
<<<<<<< HEAD
<PageSectionContent ref={settingsRef} className="scroll-mt-24">
=======
<PageSectionContent
ref={settingsRef}
id="compute"
tabIndex={-1}
className="scroll-mt-24 outline-hidden"
>
>>>>>>> 33f4960eb1 (fix(studio): stop the add-replica sheet restoring focus into the page)
<PageSectionContent ref={settingsRef} id="compute" className="scroll-mt-24">
<ComputeSizeField form={form} disabled={disabled} />
</PageSectionContent>
</PageSection>
@@ -28,7 +28,6 @@ import { AdvancedSection, ComputeSection, DiskSection } from './DiskManagementFo
import { DiskMangementRestartRequiredSection } from './DiskManagementRestartRequiredSection'
import { DiskManagementReviewAndSubmitDialog } from './DiskManagementReviewAndSubmitDialog/DiskManagementReviewAndSubmitDialog'
import { useDiskManagementReviewChanges } from './DiskManagementReviewAndSubmitDialog/DiskManagementReviewAndSubmitDialog.hooks'
import { scrollElementIntoContainer } from './scrollElementIntoContainer'
import { BillingChangeBadge } from './ui/BillingChangeBadge'
import {
DISK_LIMITS,
@@ -41,7 +40,6 @@ import {
recommendComputeParser,
subscribeRecommendCompute,
} from '@/components/interfaces/Settings/Infrastructure/ReadReplicas/recommendCompute'
import { useMainScrollContainer } from '@/components/layouts/MainScrollContainerContext'
import { PADDING_CLASSES } from '@/components/layouts/Scaffold'
import { UpgradeToPro } from '@/components/ui/UpgradeToPro'
import {
@@ -92,14 +90,6 @@ export function DiskManagementForm({
const storageSettingsRef = useRef<HTMLDivElement>(null)
const computeSettingsRef = useRef<HTMLDivElement>(null)
const diskSizeSettingsRef = useRef<HTMLDivElement>(null)
const mainScrollContainer = useMainScrollContainer()
const mainScrollContainerRef = useRef(mainScrollContainer)
mainScrollContainerRef.current = mainScrollContainer
const scrollToComputeSection = () => {
scrollElementIntoContainer(computeSettingsRef.current, mainScrollContainerRef.current)
computeSettingsRef.current?.focus({ preventScroll: true })
}
const isSpendCapEnabled = org?.plan.id !== 'free' && !org?.usage_billing_enabled
@@ -402,7 +392,7 @@ export function DiskManagementForm({
setRecommendCompute(null)
const timeoutId = setTimeout(() => {
scrollToComputeSection()
computeSettingsRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' })
}, RECOMMEND_COMPUTE_SCROLL_DELAY_MS)
return () => clearTimeout(timeoutId)
@@ -417,7 +407,7 @@ export function DiskManagementForm({
})
void form.trigger(['provisionedIOPS', 'throughput'])
window.setTimeout(() => {
scrollToComputeSection()
computeSettingsRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' })
}, RECOMMEND_COMPUTE_SCROLL_DELAY_MS)
})
}, [form])
@@ -428,7 +418,7 @@ export function DiskManagementForm({
if (window.location.hash !== '#compute') return
const timeoutId = setTimeout(() => {
scrollToComputeSection()
computeSettingsRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' })
}, 100)
return () => clearTimeout(timeoutId)
@@ -458,7 +448,7 @@ export function DiskManagementForm({
if (!scrollTarget) return
const timeoutId = setTimeout(() => {
scrollElementIntoContainer(scrollTarget.current, mainScrollContainerRef.current)
scrollTarget.current?.scrollIntoView({ behavior: 'smooth', block: 'start' })
}, 100)
return () => clearTimeout(timeoutId)
@@ -466,151 +456,145 @@ export function DiskManagementForm({
return (
<Form {...form}>
{/*
ProjectLayout's main is a viewport-tall flex column. If this form is a
direct flex item, production builds resolve descendant h-full / flex-1
against that main (next dev often does not). Keep a block wrapper so
height stays content-sized.
*/}
<div className="w-full">
<form id="disk-compute-form" onSubmit={form.handleSubmit(onSubmit)}>
<PageContainer size="default" className="pb-16">
<div className="pt-12">
<form id="disk-compute-form" onSubmit={form.handleSubmit(onSubmit)}>
<PageContainer size="default" className="pb-16">
<PageSection>
<PageSectionContent>
{overviewExtra}
<ComputeAndDiskUsageCharts className={cn(overviewExtra && 'mt-6', chartsClassName)} />
</PageSectionContent>
</PageSection>
{(isProjectResizing ||
isProjectRequestingDiskChanges ||
(isEntitlementsLoaded && !isPlanUpgradeRequired && noPermissions)) && (
<div className="relative flex flex-col gap-10">
<DiskMangementRestartRequiredSection
visible={isProjectResizing}
title="Your project will now automatically restart."
description="Your project will be unavailable for up to 2 mins."
/>
<NoticeBar
type="default"
visible={isProjectRequestingDiskChanges}
title="Disk configuration changes have been requested"
description="The requested changes will be applied to your disk shortly"
/>
<NoticeBar
type="default"
visible={isEntitlementsLoaded && !isPlanUpgradeRequired && noPermissions}
title="You do not have permission to update disk configuration"
description="Please contact your organization administrator to update your disk configuration"
/>
</div>
)}
{(isProjectResizing ||
isProjectRequestingDiskChanges ||
(isEntitlementsLoaded && !isPlanUpgradeRequired && noPermissions)) && (
<div className="relative flex flex-col gap-10">
<DiskMangementRestartRequiredSection
visible={isProjectResizing}
title="Your project will now automatically restart."
description="Your project will be unavailable for up to 2 mins."
/>
<NoticeBar
type="default"
visible={isProjectRequestingDiskChanges}
title="Disk configuration changes have been requested"
description="The requested changes will be applied to your disk shortly"
/>
<NoticeBar
type="default"
visible={isEntitlementsLoaded && !isPlanUpgradeRequired && noPermissions}
title="You do not have permission to update disk configuration"
description="Please contact your organization administrator to update your disk configuration"
/>
</div>
{beforeScaling}
<PageSection>
<PageSectionMeta>
<PageSectionSummary>
<PageSectionTitle>Scaling</PageSectionTitle>
</PageSectionSummary>
</PageSectionMeta>
{isEntitlementsLoaded && isPlanUpgradeRequired && (
<UpgradeToPro
featureProposition="configure compute and disk"
primaryText="Only available on Pro Plan and above"
secondaryText="Upgrade to the Pro Plan to configure compute and disk settings."
/>
)}
<PageSectionContent>
<ComputeSection
form={form}
settingsRef={computeSettingsRef}
showBillingBadge={showComputeBillingBadge}
beforePrice={Number(computeSizePrice.oldPrice)}
afterPrice={Number(computeSizePrice.newPrice)}
disabled={disableComputeInputs}
/>
{beforeScaling}
<DiskSection
form={form}
settingsRef={diskSizeSettingsRef}
showBillingBadge={showDiskBillingBadge}
beforePrice={Number(diskSizePrice.oldPrice)}
afterPrice={Number(diskSizePrice.newPrice)}
isAws={isAws}
isAwsK8s={isAwsK8s}
isBranch={isBranch}
isNoticeVisible={isDiskNoticeVisible}
isReadOnlyMode={!!isReadOnlyMode}
usedPercentage={usedPercentage}
isWithinCooldownWindow={isWithinCooldownWindow}
currentDiskSizeGb={defaultValues.totalSize}
disableDiskSizeInput={disableDiskSizeInput}
/>
<PageSection>
<PageSectionMeta>
<PageSectionSummary>
<PageSectionTitle>Scaling</PageSectionTitle>
</PageSectionSummary>
</PageSectionMeta>
{isEntitlementsLoaded && isPlanUpgradeRequired && (
<UpgradeToPro
featureProposition="configure compute and disk"
primaryText="Only available on Pro Plan and above"
secondaryText="Upgrade to the Pro Plan to configure compute and disk settings."
{isAws && (
<AdvancedSection
form={form}
autoscaleSettingsRef={autoscaleSettingsRef}
storageSettingsRef={storageSettingsRef}
showBillingBadge={showAdvancedBillingBadge}
beforePrice={advancedBeforePrice}
afterPrice={advancedAfterPrice}
disableIopsThroughputConfig={disableIopsThroughputConfig}
canUpdateDiskConfiguration={canUpdateDiskConfiguration}
isDiskTooSmallForCustomIops={isDiskTooSmallForCustomIops}
disableDiskInputs={disableDiskInputs}
disableDiskSizeInput={disableDiskSizeInput}
suggestedDiskSizeForCustomIops={suggestedDiskSizeForCustomIops}
/>
)}
<PageSectionContent>
<ComputeSection
form={form}
settingsRef={computeSettingsRef}
showBillingBadge={showComputeBillingBadge}
beforePrice={Number(computeSizePrice.oldPrice)}
afterPrice={Number(computeSizePrice.newPrice)}
disabled={disableComputeInputs}
/>
</PageSectionContent>
</PageSection>
</PageContainer>
<DiskSection
form={form}
settingsRef={diskSizeSettingsRef}
showBillingBadge={showDiskBillingBadge}
beforePrice={Number(diskSizePrice.oldPrice)}
afterPrice={Number(diskSizePrice.newPrice)}
isAws={isAws}
isAwsK8s={isAwsK8s}
isBranch={isBranch}
isNoticeVisible={isDiskNoticeVisible}
isReadOnlyMode={!!isReadOnlyMode}
usedPercentage={usedPercentage}
isWithinCooldownWindow={isWithinCooldownWindow}
currentDiskSizeGb={defaultValues.totalSize}
disableDiskSizeInput={disableDiskSizeInput}
/>
{isAws && (
<AdvancedSection
form={form}
autoscaleSettingsRef={autoscaleSettingsRef}
storageSettingsRef={storageSettingsRef}
showBillingBadge={showAdvancedBillingBadge}
beforePrice={advancedBeforePrice}
afterPrice={advancedAfterPrice}
disableIopsThroughputConfig={disableIopsThroughputConfig}
canUpdateDiskConfiguration={canUpdateDiskConfiguration}
isDiskTooSmallForCustomIops={isDiskTooSmallForCustomIops}
disableDiskInputs={disableDiskInputs}
disableDiskSizeInput={disableDiskSizeInput}
suggestedDiskSizeForCustomIops={suggestedDiskSizeForCustomIops}
/>
<AnimatePresence>
{isDirty ? (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 20 }}
transition={{ duration: 0.1, delay: 0.2 }}
className="z-10 w-full left-0 right-0 sticky bottom-0 bg-surface-100 border-t h-16 items-center flex"
>
<div
className={cn(
'mx-auto w-full max-w-[1200px]',
PADDING_CLASSES,
'flex items-center justify-end gap-3'
)}
</PageSectionContent>
</PageSection>
</PageContainer>
<AnimatePresence>
{isDirty ? (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 20 }}
transition={{ duration: 0.1, delay: 0.2 }}
className="z-10 w-full left-0 right-0 sticky bottom-0 bg-surface-100 border-t h-16 items-center flex"
>
<div
className={cn(
'mx-auto w-full max-w-[1200px]',
PADDING_CLASSES,
'flex items-center justify-end gap-3'
)}
<BillingChangeBadge
show={isDirty}
beforePrice={totalBeforePrice}
afterPrice={totalAfterPrice}
/>
<Button
variant="default"
onClick={() => form.reset()}
disabled={!isDirty}
size="medium"
>
<BillingChangeBadge
show={isDirty}
beforePrice={totalBeforePrice}
afterPrice={totalAfterPrice}
/>
<Button
variant="default"
onClick={() => form.reset()}
disabled={!isDirty}
size="medium"
>
Cancel
</Button>
<DiskManagementReviewAndSubmitDialog
loading={isUpdatingConfig}
disabled={noPermissions}
form={form}
numReplicas={readReplicas.length}
isDialogOpen={isDialogOpen}
onSubmit={onSubmit}
setIsDialogOpen={setIsDialogOpen}
message={message}
/>
</div>
</motion.div>
) : null}
</AnimatePresence>
</form>
</div>
Cancel
</Button>
<DiskManagementReviewAndSubmitDialog
loading={isUpdatingConfig}
disabled={noPermissions}
form={form}
numReplicas={readReplicas.length}
isDialogOpen={isDialogOpen}
onSubmit={onSubmit}
setIsDialogOpen={setIsDialogOpen}
message={message}
/>
</div>
</motion.div>
) : null}
</AnimatePresence>
</form>
</Form>
)
}
@@ -1,49 +0,0 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import { scrollElementIntoContainer } from './scrollElementIntoContainer'
const mockRect = (top: number): DOMRect =>
({
top,
bottom: top + 40,
left: 0,
right: 0,
width: 0,
height: 40,
x: 0,
y: top,
toJSON: () => ({}),
}) as DOMRect
describe('scrollElementIntoContainer', () => {
afterEach(() => {
document.body.replaceChildren()
})
it('scrolls the overflow container by the element offset minus the top margin', () => {
const container = document.createElement('div')
const element = document.createElement('div')
container.append(element)
document.body.append(container)
Object.defineProperty(container, 'scrollTop', { configurable: true, value: 50 })
vi.spyOn(container, 'getBoundingClientRect').mockReturnValue(mockRect(100))
vi.spyOn(element, 'getBoundingClientRect').mockReturnValue(mockRect(500))
const scrollTo = vi.fn()
container.scrollTo = scrollTo
scrollElementIntoContainer(element, container, { offset: 96, behavior: 'auto' })
expect(scrollTo).toHaveBeenCalledWith({ top: 354, behavior: 'auto' })
})
it('falls back to scrollIntoView when there is no overflow container', () => {
const element = document.createElement('div')
const scrollIntoView = vi.fn()
element.scrollIntoView = scrollIntoView
scrollElementIntoContainer(element, null, { behavior: 'auto' })
expect(scrollIntoView).toHaveBeenCalledWith({ behavior: 'auto', block: 'start' })
})
})
@@ -1,30 +0,0 @@
const DEFAULT_OFFSET_PX = 96 // matches scroll-mt-24
/**
* Scroll `element` to the top of `container` without Element.scrollIntoView.
* scrollIntoView walks every overflow ancestor (Studio has two nested mains)
* and, mid layout shift, can inflate a flex item instead of moving scrollTop.
*/
export function scrollElementIntoContainer(
element: HTMLElement | null | undefined,
container: HTMLElement | null | undefined,
{
behavior = 'smooth',
offset = DEFAULT_OFFSET_PX,
}: { behavior?: ScrollBehavior; offset?: number } = {}
) {
if (!element) return
if (!container) {
element.scrollIntoView({ behavior, block: 'start' })
return
}
const top =
element.getBoundingClientRect().top -
container.getBoundingClientRect().top +
container.scrollTop -
offset
container.scrollTo({ top: Math.max(0, top), behavior })
}
@@ -8,7 +8,7 @@ export const InfrastructureTopology = () => {
if (!infrastructureReadReplicas) return null
return (
<div className="w-full h-[400px] shrink-0 border border-muted rounded-md overflow-hidden relative [overflow-anchor:none]">
<div className="w-full h-[400px] border border-muted rounded-md overflow-hidden flex flex-col relative">
<InstanceConfiguration />
</div>
)
@@ -1,44 +0,0 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import {
requestRecommendCompute,
subscribeRecommendCompute,
} from '@/components/interfaces/Settings/Infrastructure/ReadReplicas/recommendCompute'
describe('recommendCompute bridge', () => {
afterEach(() => {
// Clear any leftover subscriber or queued recommendation between tests.
subscribeRecommendCompute(() => {})()
})
it('delivers the recommended size to the active subscriber', () => {
const listener = vi.fn()
subscribeRecommendCompute(listener)
requestRecommendCompute('ci_small')
expect(listener).toHaveBeenCalledWith('ci_small')
})
it('queues a recommendation until a subscriber mounts', () => {
const listener = vi.fn()
requestRecommendCompute('ci_xlarge')
expect(listener).not.toHaveBeenCalled()
subscribeRecommendCompute(listener)
expect(listener).toHaveBeenCalledWith('ci_xlarge')
})
it('replaces the previous subscriber', () => {
const first = vi.fn()
const second = vi.fn()
subscribeRecommendCompute(first)
subscribeRecommendCompute(second)
requestRecommendCompute('ci_xlarge')
expect(first).not.toHaveBeenCalled()
expect(second).toHaveBeenCalledWith('ci_xlarge')
})
})