From ce830fae1670bfbe659c44dc00b36d9dede059fe Mon Sep 17 00:00:00 2001 From: Danny White <3104761+dnywh@users.noreply.github.com> Date: Mon, 17 Aug 2026 14:37:10 +1000 Subject: [PATCH] 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. --- .../ComputeAndDiskUsageCharts.tsx | 12 +- .../DiskManagementForm.sections.tsx | 11 +- .../DiskManagement/DiskManagementForm.tsx | 276 +++++++++--------- .../scrollElementIntoContainer.test.ts | 49 ---- .../scrollElementIntoContainer.ts | 30 -- .../Infrastructure/InfrastructureTopology.tsx | 2 +- .../ReadReplicas/recommendCompute.test.ts | 44 --- 7 files changed, 138 insertions(+), 286 deletions(-) delete mode 100644 apps/studio/components/interfaces/DiskManagement/scrollElementIntoContainer.test.ts delete mode 100644 apps/studio/components/interfaces/DiskManagement/scrollElementIntoContainer.ts delete mode 100644 apps/studio/components/interfaces/Settings/Infrastructure/ReadReplicas/recommendCompute.test.ts diff --git a/apps/studio/components/interfaces/DiskManagement/ComputeAndDiskUsageCharts.tsx b/apps/studio/components/interfaces/DiskManagement/ComputeAndDiskUsageCharts.tsx index 4e984cc0d71..ec8d8b68dbc 100644 --- a/apps/studio/components/interfaces/DiskManagement/ComputeAndDiskUsageCharts.tsx +++ b/apps/studio/components/interfaces/DiskManagement/ComputeAndDiskUsageCharts.tsx @@ -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 (
-
+
- +
-
- +
+ -<<<<<<< HEAD - -======= - ->>>>>>> 33f4960eb1 (fix(studio): stop the add-replica sheet restoring focus into the page) + diff --git a/apps/studio/components/interfaces/DiskManagement/DiskManagementForm.tsx b/apps/studio/components/interfaces/DiskManagement/DiskManagementForm.tsx index f2ad45031ce..826697fce12 100644 --- a/apps/studio/components/interfaces/DiskManagement/DiskManagementForm.tsx +++ b/apps/studio/components/interfaces/DiskManagement/DiskManagementForm.tsx @@ -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(null) const computeSettingsRef = useRef(null) const diskSizeSettingsRef = useRef(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 (
- {/* - 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. - */} -
- - -
+ + + + {overviewExtra} + + + + {(isProjectResizing || + isProjectRequestingDiskChanges || + (isEntitlementsLoaded && !isPlanUpgradeRequired && noPermissions)) && ( +
+ + +
+ )} - {(isProjectResizing || - isProjectRequestingDiskChanges || - (isEntitlementsLoaded && !isPlanUpgradeRequired && noPermissions)) && ( -
- - - -
+ {beforeScaling} + + + + + Scaling + + + {isEntitlementsLoaded && isPlanUpgradeRequired && ( + )} + + - {beforeScaling} + - - - - Scaling - - - {isEntitlementsLoaded && isPlanUpgradeRequired && ( - )} - - + + +
- - - {isAws && ( - + + {isDirty ? ( + +
- - - - - {isDirty ? ( - -
+ - -
-
- ) : null} -
- -
+ Cancel + + +
+ + ) : null} + + ) } diff --git a/apps/studio/components/interfaces/DiskManagement/scrollElementIntoContainer.test.ts b/apps/studio/components/interfaces/DiskManagement/scrollElementIntoContainer.test.ts deleted file mode 100644 index eae11aafea6..00000000000 --- a/apps/studio/components/interfaces/DiskManagement/scrollElementIntoContainer.test.ts +++ /dev/null @@ -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' }) - }) -}) diff --git a/apps/studio/components/interfaces/DiskManagement/scrollElementIntoContainer.ts b/apps/studio/components/interfaces/DiskManagement/scrollElementIntoContainer.ts deleted file mode 100644 index ae0bfefdbd0..00000000000 --- a/apps/studio/components/interfaces/DiskManagement/scrollElementIntoContainer.ts +++ /dev/null @@ -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 }) -} diff --git a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureTopology.tsx b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureTopology.tsx index 7ec1e969a89..3d03b7cd8c8 100644 --- a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureTopology.tsx +++ b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureTopology.tsx @@ -8,7 +8,7 @@ export const InfrastructureTopology = () => { if (!infrastructureReadReplicas) return null return ( -
+
) diff --git a/apps/studio/components/interfaces/Settings/Infrastructure/ReadReplicas/recommendCompute.test.ts b/apps/studio/components/interfaces/Settings/Infrastructure/ReadReplicas/recommendCompute.test.ts deleted file mode 100644 index 62e417ab7b5..00000000000 --- a/apps/studio/components/interfaces/Settings/Infrastructure/ReadReplicas/recommendCompute.test.ts +++ /dev/null @@ -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') - }) -})