From 721e05431d9e398f3d0bb5980e767d3dba44a3dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josh=20Goldberg=20=E2=9C=A8?= Date: Tue, 3 Mar 2026 13:41:54 -0500 Subject: [PATCH 01/38] feat(logs): moved LogsInfiniteTable inline with expand/contract button --- .../events/ourlogs/ourlogsDrawer.tsx | 9 +- .../events/ourlogs/ourlogsSection.spec.tsx | 1 + .../app/views/explore/logs/content.spec.tsx | 3 + .../app/views/explore/logs/logsTab.spec.tsx | 3 + static/app/views/explore/logs/logsTab.tsx | 7 +- static/app/views/explore/logs/styles.tsx | 22 ++++- .../logs/tables/logsInfiniteTable.spec.tsx | 1 + .../explore/logs/tables/logsInfiniteTable.tsx | 88 ++++++++++--------- .../explore/logs/tables/useExpando.spec.tsx | 46 ++++++++++ .../views/explore/logs/tables/useExpando.tsx | 56 ++++++++++++ .../performance/newTraceDetails/index.tsx | 7 +- .../details/span/components/logDetails.tsx | 6 +- .../newTraceDetails/traceOurlogs.spec.tsx | 9 +- .../newTraceDetails/traceOurlogs.tsx | 17 ++-- .../views/replays/detail/ourlogs/index.tsx | 6 +- tests/js/fixtures/virtualization.ts | 18 ++++ 16 files changed, 217 insertions(+), 82 deletions(-) create mode 100644 static/app/views/explore/logs/tables/useExpando.spec.tsx create mode 100644 static/app/views/explore/logs/tables/useExpando.tsx create mode 100644 tests/js/fixtures/virtualization.ts diff --git a/static/app/components/events/ourlogs/ourlogsDrawer.tsx b/static/app/components/events/ourlogs/ourlogsDrawer.tsx index e266c5a386d2bc..eaa8cd6d15cdd4 100644 --- a/static/app/components/events/ourlogs/ourlogsDrawer.tsx +++ b/static/app/components/events/ourlogs/ourlogsDrawer.tsx @@ -1,4 +1,4 @@ -import {useMemo, useRef} from 'react'; +import {useMemo} from 'react'; import moment from 'moment-timezone'; import {ProjectAvatar} from '@sentry/scraps/avatar'; @@ -83,7 +83,6 @@ export function OurlogsDrawer({ const searchQueryBuilderProps = useTraceItemSearchQueryBuilderProps( tracesItemSearchQueryBuilderProps ); - const containerRef = useRef(null); const additionalData = useMemo( () => ({ @@ -156,12 +155,12 @@ export function OurlogsDrawer({ )} - - + + diff --git a/static/app/components/events/ourlogs/ourlogsSection.spec.tsx b/static/app/components/events/ourlogs/ourlogsSection.spec.tsx index e7d855ad86b00f..4e6da88799f269 100644 --- a/static/app/components/events/ourlogs/ourlogsSection.spec.tsx +++ b/static/app/components/events/ourlogs/ourlogsSection.spec.tsx @@ -43,6 +43,7 @@ jest.mock('@tanstack/react-virtual', () => { {key: '3', index: 2, start: 100, end: 150, lane: 0}, ]), getTotalSize: jest.fn().mockReturnValue(150), + measure: jest.fn(), scrollToIndex: jest.fn(), options: { scrollMargin: 0, diff --git a/static/app/views/explore/logs/content.spec.tsx b/static/app/views/explore/logs/content.spec.tsx index 8bc517f6624ca3..8727c7a7bbb3a6 100644 --- a/static/app/views/explore/logs/content.spec.tsx +++ b/static/app/views/explore/logs/content.spec.tsx @@ -2,6 +2,7 @@ import {createLogFixtures, initializeLogsTest} from 'sentry-fixture/log'; import {ProjectKeysFixture} from 'sentry-fixture/projectKeys'; import {TeamFixture} from 'sentry-fixture/team'; import {TimeSeriesFixture} from 'sentry-fixture/timeSeries'; +import {mockGetBoundingClientRect} from 'sentry-fixture/virtualization'; import { render, @@ -21,6 +22,8 @@ import type {OurLogsResponseItem} from 'sentry/views/explore/logs/types'; import LogsPage from './content'; +beforeEach(mockGetBoundingClientRect); + describe('LogsPage', () => { let organization: Organization; let project: Project; diff --git a/static/app/views/explore/logs/logsTab.spec.tsx b/static/app/views/explore/logs/logsTab.spec.tsx index a65043b70f2bd9..3e945e60dd2ce5 100644 --- a/static/app/views/explore/logs/logsTab.spec.tsx +++ b/static/app/views/explore/logs/logsTab.spec.tsx @@ -1,5 +1,6 @@ import {initializeLogsTest} from 'sentry-fixture/log'; import {TimeSeriesFixture} from 'sentry-fixture/timeSeries'; +import {mockGetBoundingClientRect} from 'sentry-fixture/virtualization'; import {render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary'; @@ -28,6 +29,8 @@ const datePageFilterProps: DatePageFilterProps = { }), }; +beforeEach(mockGetBoundingClientRect); + describe('LogsTabContent', () => { const {organization, project, setupPageFilters} = initializeLogsTest(); diff --git a/static/app/views/explore/logs/logsTab.tsx b/static/app/views/explore/logs/logsTab.tsx index ea4b8eff160aad..41cc3966d3ec30 100644 --- a/static/app/views/explore/logs/logsTab.tsx +++ b/static/app/views/explore/logs/logsTab.tsx @@ -95,6 +95,8 @@ import {useRawCounts} from 'sentry/views/explore/useRawCounts'; // eslint-disable-next-line boundaries/element-types import QuotaExceededAlert from 'getsentry/components/performance/quotaExceededAlert'; +import {useExpando} from './tables/useExpando'; + type LogsTabProps = { datePageFilterProps: DatePageFilterProps; }; @@ -422,6 +424,7 @@ export function LogsTabContent({datePageFilterProps}: LogsTabProps) { }, [pageFilters.selection.datetime]); const {infiniteLogsQueryResult} = useLogsPageData(); + const expando = useExpando(); return ( @@ -507,15 +510,17 @@ export function LogsTabContent({datePageFilterProps}: LogsTabProps) { } /> + {expando.button} )} {tableTab === 'logs' ? ( ) : ( diff --git a/static/app/views/explore/logs/styles.tsx b/static/app/views/explore/logs/styles.tsx index 9086df330465d8..a543443c9a3d70 100644 --- a/static/app/views/explore/logs/styles.tsx +++ b/static/app/views/explore/logs/styles.tsx @@ -127,8 +127,11 @@ export const LogTableBodyCell = styled(TableBodyCell)` export const LogTableBody = styled(TableBody)<{ disableBodyPadding?: boolean; + expanded?: boolean; showHeader?: boolean; }>` + --collapsedMaxHeight: max(20vh, 15rem); + ${p => p.showHeader ? '' @@ -138,6 +141,15 @@ export const LogTableBody = styled(TableBody)<{ padding-top: ${p.theme.space.md}; padding-bottom: ${p.theme.space.md}; `} + overflow-y: auto; + max-height: ${p => + p.expanded + ? `calc(95vh - ${GRID_BODY_ROW_HEIGHT * 1.5}px)` + : 'var(--collapsedMaxHeight)'}; + + @media (height >= 60rem) { + --collapsedMaxHeight: max(40vh, 20rem); + } `; export const LogDetailTableBodyCell = styled(TableBodyCell)` @@ -291,6 +303,7 @@ export const LogsItemContainer = styled('div')` flex: 1 1 auto; margin-top: ${p => p.theme.space.md}; margin-bottom: ${p => p.theme.space.md}; + position: relative; `; export const LogsTableActionsContainer = styled(LogsItemContainer)` @@ -417,12 +430,13 @@ export const FloatingBackToTopContainer = styled('div')<{ tableLeft?: number; tableWidth?: number; }>` - position: ${p => (p.inReplay ? 'absolute' : 'fixed')}; + --floatingWidth: ${p => (p.tableWidth ? `${p.tableWidth}px` : '100%')}; + position: absolute; z-index: 1; opacity: ${p => (p.inReplay ? 1 : 0.9)}; - ${p => (p.inReplay ? 'top: 90px;' : 'top: 20px;')} - ${p => (p.inReplay ? '' : p.tableLeft ? `left: ${p.tableLeft}px;` : 'left: 0;')} - width: ${p => (p.tableWidth ? `${p.tableWidth}px` : '100%')}; + top: ${p => (p.inReplay ? '90px;' : '65px;')}; + left: calc(50% - var(--floatingWidth) / 2); + width: var(--floatingWidth); display: flex; justify-content: center; diff --git a/static/app/views/explore/logs/tables/logsInfiniteTable.spec.tsx b/static/app/views/explore/logs/tables/logsInfiniteTable.spec.tsx index 8f90c846d45a9a..6782ffce7ff825 100644 --- a/static/app/views/explore/logs/tables/logsInfiniteTable.spec.tsx +++ b/static/app/views/explore/logs/tables/logsInfiniteTable.spec.tsx @@ -66,6 +66,7 @@ jest.mock('@tanstack/react-virtual', () => { {key: '3', index: 2, start: 100, end: 150, lane: 0}, ]), getTotalSize: jest.fn().mockReturnValue(150), + measure: jest.fn(), options: { scrollMargin: 0, }, diff --git a/static/app/views/explore/logs/tables/logsInfiniteTable.tsx b/static/app/views/explore/logs/tables/logsInfiniteTable.tsx index 2741399e6abf02..641e58ac6138c4 100644 --- a/static/app/views/explore/logs/tables/logsInfiniteTable.tsx +++ b/static/app/views/explore/logs/tables/logsInfiniteTable.tsx @@ -1,9 +1,18 @@ -import type {CSSProperties, RefObject} from 'react'; -import {Fragment, useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import { + Fragment, + useCallback, + useEffect, + useLayoutEffect, + useMemo, + useRef, + useState, + type CSSProperties, + type RefObject, +} from 'react'; import styled from '@emotion/styled'; import * as Sentry from '@sentry/react'; import type {Virtualizer} from '@tanstack/react-virtual'; -import {useVirtualizer, useWindowVirtualizer} from '@tanstack/react-virtual'; +import {useVirtualizer} from '@tanstack/react-virtual'; import {Button} from '@sentry/scraps/button'; import {Flex, Stack} from '@sentry/scraps/layout'; @@ -89,28 +98,27 @@ type LogsTableProps = { showVerticalScrollbar?: boolean; }; emptyRenderer?: () => React.ReactNode; + expanded?: boolean; localOnlyItemFilters?: { filterText: string; filteredItems: OurLogsResponseItem[]; }; numberAttributes?: TagCollection; - scrollContainer?: React.RefObject; stringAttributes?: TagCollection; }; const {info, fmt} = Sentry.logger; const LOGS_GRID_SCROLL_PIXEL_REVERSE_THRESHOLD = LOGS_GRID_BODY_ROW_HEIGHT * 2; // If you are less than this number of pixels from the top of the table while scrolling backward, fetch the previous page. -const LOGS_OVERSCAN_AMOUNT = 50; // How many items to render beyond the visible area. export function LogsInfiniteTable({ embedded = false, + expanded, localOnlyItemFilters, emptyRenderer, numberAttributes, stringAttributes, booleanAttributes, - scrollContainer, embeddedStyling, embeddedOptions, additionalData, @@ -255,23 +263,18 @@ export function LogsInfiniteTable({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [searchString, localOnlyItemFilters?.filterText]); - const windowVirtualizer = useWindowVirtualizer({ + const virtualizer = useVirtualizer({ count: data?.length ?? 0, estimateSize, - overscan: LOGS_OVERSCAN_AMOUNT, + overscan: expanded ? 25 : 10, + getScrollElement: () => tableBodyRef?.current, getItemKey: (index: number) => data?.[index]?.[OurLogKnownFieldKey.ID] ?? index, - scrollMargin: tableBodyRef.current?.offsetTop ?? 0, }); - const containerVirtualizer = useVirtualizer({ - count: data?.length ?? 0, - estimateSize, - overscan: LOGS_OVERSCAN_AMOUNT, - getScrollElement: () => scrollContainer?.current ?? null, - getItemKey: (index: number) => data?.[index]?.[OurLogKnownFieldKey.ID] ?? index, - }); + useLayoutEffect(() => { + virtualizer.measure(); + }, [expanded, virtualizer]); - const virtualizer = scrollContainer?.current ? containerVirtualizer : windowVirtualizer; const virtualItems = virtualizer.getVirtualItems(); const firstItem = virtualItems[0]?.start; @@ -292,13 +295,13 @@ export function LogsInfiniteTable({ useEffect(() => { if ( pseudoRowIndex !== -1 && - scrollContainer?.current && + tableBodyRef?.current && !additionalData?.scrollToDisabled ) { setTimeout(() => { const scrollToIndex = pseudoRowIndex === -2 ? baseDataLength.current : pseudoRowIndex; - containerVirtualizer.scrollToIndex(scrollToIndex, { + virtualizer.scrollToIndex(scrollToIndex, { behavior: 'smooth', align: 'center', }); @@ -306,8 +309,8 @@ export function LogsInfiniteTable({ } }, [ pseudoRowIndex, - containerVirtualizer, - scrollContainer, + virtualizer, + tableBodyRef, baseDataLength, additionalData?.scrollToDisabled, ]); @@ -336,9 +339,7 @@ export function LogsInfiniteTable({ ] : [0, 0]; - const {scrollDirection, scrollOffset, isScrolling} = scrollContainer - ? containerVirtualizer - : virtualizer; + const {scrollDirection, scrollOffset, isScrolling} = virtualizer; useEffect(() => { if (isFunctionScrolling && !isScrolling && scrollOffset === 0) { @@ -472,6 +473,7 @@ export function LogsInfiniteTable({ showHeader={!embedded} ref={tableBodyRef} disableBodyPadding={embeddedStyling?.disableBodyPadding} + expanded={expanded} > {paddingTop > 0 && ( @@ -538,24 +540,26 @@ export function LogsInfiniteTable({ )} - - {!embeddedOptions?.replay && ( - + {expanded && ( + + {!embeddedOptions?.replay && ( + + )} diff --git a/static/app/views/explore/logs/tables/useExpando.spec.tsx b/static/app/views/explore/logs/tables/useExpando.spec.tsx new file mode 100644 index 00000000000000..929535531d7211 --- /dev/null +++ b/static/app/views/explore/logs/tables/useExpando.spec.tsx @@ -0,0 +1,46 @@ +import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary'; + +import {useExpando, type Expando} from './useExpando'; + +let hookResult: Expando; + +function ExpandoWrapper() { + hookResult = useExpando(); + return
{hookResult.button}
; +} + +const mockScrollIntoView = jest.fn(); + +describe('useExpando', () => { + beforeEach(() => { + Element.prototype.scrollIntoView = mockScrollIntoView; + }); + + it('sets expanded to false when the button is not yet clicked', () => { + render(); + + expect(screen.getByText('Expand')).toBeInTheDocument(); + expect(hookResult.expanded).toBe(false); + }); + + it('sets expanded to true and scrolls into view when the button is clicked', async () => { + render(); + + await userEvent.click(screen.getByText('Expand')); + + expect(screen.getByText('Collapse')).toBeInTheDocument(); + expect(hookResult.expanded).toBe(true); + expect(mockScrollIntoView.mock.calls).toEqual([[{block: 'start'}]]); + }); + + it('sets expanded to false when the button is clicked a second time', async () => { + render(); + + await userEvent.click(screen.getByText('Expand')); + await userEvent.click(screen.getByText('Collapse')); + + expect(screen.getByText('Expand')).toBeInTheDocument(); + expect(hookResult.expanded).toBe(false); + expect(mockScrollIntoView.mock.calls).toEqual([[{block: 'start'}]]); + }); +}); diff --git a/static/app/views/explore/logs/tables/useExpando.tsx b/static/app/views/explore/logs/tables/useExpando.tsx new file mode 100644 index 00000000000000..5e9dbb8a650715 --- /dev/null +++ b/static/app/views/explore/logs/tables/useExpando.tsx @@ -0,0 +1,56 @@ +import {useCallback, useLayoutEffect, useRef, useState} from 'react'; +import styled from '@emotion/styled'; + +import {Button} from '@sentry/scraps/button'; + +import {IconContract, IconExpand} from 'sentry/icons'; +import {t} from 'sentry/locale'; +import {TableActionButton} from 'sentry/views/explore/components/tableActionButton'; + +export interface Expando { + button: React.ReactNode; + expanded: boolean; +} + +export function useExpando(): Expando { + const [expanded, setExpanded] = useState(false); + const ref = useRef(null); + + const [Icon, text] = expanded + ? [IconContract, t('Collapse')] + : [IconExpand, t('Expand')]; + + const toggleExpanded = useCallback(() => { + setExpanded(previousExpanded => !previousExpanded); + }, []); + + useLayoutEffect(() => { + if (expanded) { + ref.current?.scrollIntoView({ + block: 'start', + }); + } + }, [expanded]); + + const buttonProps = { + 'aria-label': text, + icon: , + onClick: toggleExpanded, + ref, + size: 'sm', + } as const; + + return { + button: ( + } + desktop={{text}} + /> + ), + expanded, + }; +} + +const ExpandoButton = styled(Button)` + scroll-margin-top: ${p => p.theme.space.md}; +`; diff --git a/static/app/views/performance/newTraceDetails/index.tsx b/static/app/views/performance/newTraceDetails/index.tsx index adedd207d7155e..3775bee5ecbe9b 100644 --- a/static/app/views/performance/newTraceDetails/index.tsx +++ b/static/app/views/performance/newTraceDetails/index.tsx @@ -137,7 +137,6 @@ function TraceViewImpl({traceSlug}: {traceSlug: string}) { logs: logsData, traceId: traceSlug, }); - const traceInnerLayoutRef = useRef(null); const {tabOptions, currentTab, onTabChange} = useTraceLayoutTabs({ tree, @@ -162,7 +161,7 @@ function TraceViewImpl({traceSlug}: {traceSlug: string}) { logs={logsData} metrics={metricsData} /> - + ) : null} - {currentTab === TraceLayoutTabKeys.LOGS ? ( - - ) : null} + {currentTab === TraceLayoutTabKeys.LOGS ? : null} {currentTab === TraceLayoutTabKeys.METRICS ? ( diff --git a/static/app/views/performance/newTraceDetails/traceDrawer/details/span/components/logDetails.tsx b/static/app/views/performance/newTraceDetails/traceDrawer/details/span/components/logDetails.tsx index 8f491e49ff54da..4d7a77f55a7c90 100644 --- a/static/app/views/performance/newTraceDetails/traceDrawer/details/span/components/logDetails.tsx +++ b/static/app/views/performance/newTraceDetails/traceDrawer/details/span/components/logDetails.tsx @@ -1,5 +1,3 @@ -import {useRef} from 'react'; - import {t} from 'sentry/locale'; import {useLogsPageDataQueryResult} from 'sentry/views/explore/contexts/logs/logsPageData'; import {LogsInfiniteTable} from 'sentry/views/explore/logs/tables/logsInfiniteTable'; @@ -9,18 +7,16 @@ import {FoldSection} from 'sentry/views/issueDetails/streamline/foldSection'; export function LogDetails() { const logsQueryResult = useLogsPageDataQueryResult(); - const scrollContainer = useRef(null); if (!logsQueryResult?.data?.length) { return null; } return ( - + ); } diff --git a/static/app/views/performance/newTraceDetails/traceOurlogs.spec.tsx b/static/app/views/performance/newTraceDetails/traceOurlogs.spec.tsx index c9faae3d9139cf..75d323d9244d4b 100644 --- a/static/app/views/performance/newTraceDetails/traceOurlogs.spec.tsx +++ b/static/app/views/performance/newTraceDetails/traceOurlogs.spec.tsx @@ -1,5 +1,5 @@ -import {useRef} from 'react'; import {OrganizationFixture} from 'sentry-fixture/organization'; +import {mockGetBoundingClientRect} from 'sentry-fixture/virtualization'; import {render, screen} from 'sentry-test/reactTestingLibrary'; @@ -12,14 +12,15 @@ import { const TRACE_SLUG = '00000000000000000000000000000000'; function Component({traceSlug}: {traceSlug: string}) { - const ref = useRef(null); return ( - + ); } +beforeEach(mockGetBoundingClientRect); + describe('TraceViewLogsSection', () => { it('renders empty logs', async () => { const organization = OrganizationFixture({features: ['ourlogs-enabled']}); @@ -63,7 +64,7 @@ describe('TraceViewLogsSection', () => { expect(screen.getByTestId('loading-indicator')).toBeInTheDocument(); - expect(await screen.findByText(/i am a log/)).toBeInTheDocument(); + await screen.findByText(/i am a log/); expect(mockRequest).toHaveBeenCalledTimes(1); }); }); diff --git a/static/app/views/performance/newTraceDetails/traceOurlogs.tsx b/static/app/views/performance/newTraceDetails/traceOurlogs.tsx index eb7e46aae42ba8..0197c6d8ba40bf 100644 --- a/static/app/views/performance/newTraceDetails/traceOurlogs.tsx +++ b/static/app/views/performance/newTraceDetails/traceOurlogs.tsx @@ -41,23 +41,15 @@ export function TraceViewLogsDataProvider({ ); } -export function TraceViewLogsSection({ - scrollContainer, -}: { - scrollContainer: React.RefObject; -}) { +export function TraceViewLogsSection() { return ( - + ); } -function LogsSectionContent({ - scrollContainer, -}: { - scrollContainer: React.RefObject; -}) { +function LogsSectionContent() { const organization = useOrganization(); const {selection} = usePageFilters(); const traceIds = useLogsFrozenTraceIds(); @@ -85,7 +77,7 @@ function LogsSectionContent({ {t('Open in Logs')} - +
); @@ -97,5 +89,6 @@ const TableContainer = styled('div')` const StyledPanel = styled(Panel)` padding: ${p => p.theme.space.xl}; + padding-bottom: 0; margin: 0; `; diff --git a/static/app/views/replays/detail/ourlogs/index.tsx b/static/app/views/replays/detail/ourlogs/index.tsx index 04949f05f6e2dd..891f252edf4773 100644 --- a/static/app/views/replays/detail/ourlogs/index.tsx +++ b/static/app/views/replays/detail/ourlogs/index.tsx @@ -1,4 +1,4 @@ -import {useCallback, useMemo, useRef} from 'react'; +import {useCallback, useMemo} from 'react'; import styled from '@emotion/styled'; import {Placeholder} from 'sentry/components/placeholder'; @@ -73,7 +73,6 @@ function OurLogsContent({replayId, startTimestampMs}: OurLogsContentProps) { const {attributes: stringAttributes} = useLogItemAttributes({}, 'string'); const {attributes: numberAttributes} = useLogItemAttributes({}, 'number'); const {attributes: booleanAttributes} = useLogItemAttributes({}, 'boolean'); - const scrollContainerRef = useRef(null); const {currentTime, setCurrentTime} = useReplayContext(); const [currentHoverTime] = useCurrentHoverTime(); @@ -121,7 +120,7 @@ function OurLogsContent({replayId, startTimestampMs}: OurLogsContentProps) { return ( - + {isPending ? ( ) : ( @@ -129,7 +128,6 @@ function OurLogsContent({replayId, startTimestampMs}: OurLogsContentProps) { stringAttributes={stringAttributes} numberAttributes={numberAttributes} booleanAttributes={booleanAttributes} - scrollContainer={scrollContainerRef} allowPagination embedded embeddedOptions={embeddedOptions} diff --git a/tests/js/fixtures/virtualization.ts b/tests/js/fixtures/virtualization.ts new file mode 100644 index 00000000000000..3c092e1fe7da58 --- /dev/null +++ b/tests/js/fixtures/virtualization.ts @@ -0,0 +1,18 @@ +/** + * Tanstack Virtual renders zero items in the default zero-sized JSDom environment. + * This forces all elements to have a non-zero size to render at least a few rows. + * https://github.com/TanStack/virtual/issues/641 + */ +export function mockGetBoundingClientRect() { + Element.prototype.getBoundingClientRect = jest.fn(() => ({ + width: 500, + height: 500, + top: 0, + left: 0, + bottom: 500, + right: 500, + x: 0, + y: 0, + toJSON: () => {}, + })); +} From d85c3cb4c2aeb24bc75c911860f9502f99b70654 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josh=20Goldberg=20=E2=9C=A8?= Date: Mon, 16 Mar 2026 12:56:03 -0400 Subject: [PATCH 02/38] fix: min-height and always-expanded --- static/app/views/explore/logs/styles.tsx | 4 ++++ static/app/views/replays/detail/ourlogs/index.tsx | 1 + 2 files changed, 5 insertions(+) diff --git a/static/app/views/explore/logs/styles.tsx b/static/app/views/explore/logs/styles.tsx index a543443c9a3d70..6ee397af193e1c 100644 --- a/static/app/views/explore/logs/styles.tsx +++ b/static/app/views/explore/logs/styles.tsx @@ -142,6 +142,10 @@ export const LogTableBody = styled(TableBody)<{ padding-bottom: ${p.theme.space.md}; `} overflow-y: auto; + + /* If a parent renderer bails out, the element might default to 0px: which causes Tanstack Virtual to stay at 0. */ + min-height: 1px; + max-height: ${p => p.expanded ? `calc(95vh - ${GRID_BODY_ROW_HEIGHT * 1.5}px)` diff --git a/static/app/views/replays/detail/ourlogs/index.tsx b/static/app/views/replays/detail/ourlogs/index.tsx index 891f252edf4773..73dcf9c53fc81f 100644 --- a/static/app/views/replays/detail/ourlogs/index.tsx +++ b/static/app/views/replays/detail/ourlogs/index.tsx @@ -131,6 +131,7 @@ function OurLogsContent({replayId, startTimestampMs}: OurLogsContentProps) { allowPagination embedded embeddedOptions={embeddedOptions} + expanded localOnlyItemFilters={{ filteredItems: filteredLogItems, filterText: filterProps.searchTerm, From 1493effca85dce49a8912ec594c99aa8bbb0732b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josh=20Goldberg=20=E2=9C=A8?= Date: Mon, 16 Mar 2026 13:36:17 -0400 Subject: [PATCH 03/38] WIP: table height is going to kill me --- static/app/views/explore/logs/styles.tsx | 3 ++- static/app/views/explore/logs/tables/logsInfiniteTable.tsx | 6 ++++-- static/app/views/replays/detail/ourlogs/index.tsx | 5 ++--- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/static/app/views/explore/logs/styles.tsx b/static/app/views/explore/logs/styles.tsx index 6ee397af193e1c..875b1be020193b 100644 --- a/static/app/views/explore/logs/styles.tsx +++ b/static/app/views/explore/logs/styles.tsx @@ -146,6 +146,7 @@ export const LogTableBody = styled(TableBody)<{ /* If a parent renderer bails out, the element might default to 0px: which causes Tanstack Virtual to stay at 0. */ min-height: 1px; + height: 100%; max-height: ${p => p.expanded ? `calc(95vh - ${GRID_BODY_ROW_HEIGHT * 1.5}px)` @@ -438,7 +439,7 @@ export const FloatingBackToTopContainer = styled('div')<{ position: absolute; z-index: 1; opacity: ${p => (p.inReplay ? 1 : 0.9)}; - top: ${p => (p.inReplay ? '90px;' : '65px;')}; + top: ${p => (p.inReplay ? p.theme.space.md : '65px;')}; left: calc(50% - var(--floatingWidth) / 2); width: var(--floatingWidth); display: flex; diff --git a/static/app/views/explore/logs/tables/logsInfiniteTable.tsx b/static/app/views/explore/logs/tables/logsInfiniteTable.tsx index 641e58ac6138c4..b10124c88865f5 100644 --- a/static/app/views/explore/logs/tables/logsInfiniteTable.tsx +++ b/static/app/views/explore/logs/tables/logsInfiniteTable.tsx @@ -454,8 +454,10 @@ export function LogsInfiniteTable({ ` `; const TableScrollContainer = styled('div')` - overflow-y: auto; - overflow-x: hidden; + overflow-y: hidden; + position: relative; height: 100%; - min-height: 0; border: 1px solid ${p => p.theme.tokens.border.primary}; border-radius: ${p => p.theme.radius.md}; `; From 68f0e067932374846757494aa7ce16dc2e55c35d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josh=20Goldberg=20=E2=9C=A8?= Date: Mon, 16 Mar 2026 16:36:28 -0400 Subject: [PATCH 04/38] fix: assorted height fixes for replays --- .../app/components/tables/gridEditable/styles.tsx | 6 +----- static/app/views/explore/components/table.tsx | 5 +++-- static/app/views/explore/logs/styles.tsx | 14 +++++++------- .../explore/logs/tables/logsInfiniteTable.tsx | 13 ++++++------- .../newTraceDetails/traceOurlogs.spec.tsx | 2 +- 5 files changed, 18 insertions(+), 22 deletions(-) diff --git a/static/app/components/tables/gridEditable/styles.tsx b/static/app/components/tables/gridEditable/styles.tsx index 4d799b950a49d7..f56b19337cbf6d 100644 --- a/static/app/components/tables/gridEditable/styles.tsx +++ b/static/app/components/tables/gridEditable/styles.tsx @@ -54,7 +54,7 @@ export const Body = styled( showVerticalScrollbar?: boolean; }) => ( - {children} + {children} ) )` @@ -336,7 +336,3 @@ export const GridResizer = styled('div')<{dataRows: number}>` opacity: 0.4; } `; - -const StyledPanelBody = styled(PanelBody)` - height: 100%; -`; diff --git a/static/app/views/explore/components/table.tsx b/static/app/views/explore/components/table.tsx index 8c71850944f409..665d2a77e6b197 100644 --- a/static/app/views/explore/components/table.tsx +++ b/static/app/views/explore/components/table.tsx @@ -1,5 +1,5 @@ import type React from 'react'; -import {useCallback, useEffect, useMemo, useRef} from 'react'; +import {useCallback, useEffect, useMemo, useRef, type CSSProperties} from 'react'; import styled from '@emotion/styled'; import {COL_WIDTH_MINIMUM} from 'sentry/components/tables/gridEditable'; @@ -18,6 +18,7 @@ import {defined} from 'sentry/utils'; import {Actions} from 'sentry/views/discover/table/cellAction'; interface TableProps extends React.ComponentProps { + height?: CSSProperties['height']; ref?: React.Ref; showVerticalScrollbar?: boolean; // Size of the loading element in order to match the height of the row. @@ -27,7 +28,7 @@ interface TableProps extends React.ComponentProps { export function Table({ref, children, style, ...props}: TableProps) { return ( <_TableWrapper {...props}> - <_Table ref={ref} style={style}> + <_Table ref={ref} height={props.height} style={style}> {children} diff --git a/static/app/views/explore/logs/styles.tsx b/static/app/views/explore/logs/styles.tsx index 875b1be020193b..dc6c4f89405042 100644 --- a/static/app/views/explore/logs/styles.tsx +++ b/static/app/views/explore/logs/styles.tsx @@ -12,6 +12,7 @@ import {GRID_BODY_ROW_HEIGHT} from 'sentry/components/tables/gridEditable/styles import {NumberContainer} from 'sentry/utils/discover/styles'; import {unreachable} from 'sentry/utils/unreachable'; import { + Table, TableBody, TableBodyCell, TableHeadCell, @@ -125,13 +126,16 @@ export const LogTableBodyCell = styled(TableBodyCell)` } `; +export const LogTable = styled(Table)` + height: 100%; + margin-bottom: 0; +`; + export const LogTableBody = styled(TableBody)<{ disableBodyPadding?: boolean; expanded?: boolean; showHeader?: boolean; }>` - --collapsedMaxHeight: max(20vh, 15rem); - ${p => p.showHeader ? '' @@ -150,11 +154,7 @@ export const LogTableBody = styled(TableBody)<{ max-height: ${p => p.expanded ? `calc(95vh - ${GRID_BODY_ROW_HEIGHT * 1.5}px)` - : 'var(--collapsedMaxHeight)'}; - - @media (height >= 60rem) { - --collapsedMaxHeight: max(40vh, 20rem); - } + : 'max(calc(60vh - 20rem), 10rem)'}; `; export const LogDetailTableBodyCell = styled(TableBodyCell)` diff --git a/static/app/views/explore/logs/tables/logsInfiniteTable.tsx b/static/app/views/explore/logs/tables/logsInfiniteTable.tsx index b10124c88865f5..d10116971e57cd 100644 --- a/static/app/views/explore/logs/tables/logsInfiniteTable.tsx +++ b/static/app/views/explore/logs/tables/logsInfiniteTable.tsx @@ -31,7 +31,6 @@ import type {Event} from 'sentry/types/event'; import type {TagCollection} from 'sentry/types/group'; import {defined} from 'sentry/utils'; import { - Table, TableBodyCell, TableHead, TableHeadCell, @@ -53,6 +52,7 @@ import { FloatingBottomContainer, HoveringRowLoadingRendererContainer, LOGS_GRID_BODY_ROW_HEIGHT, + LogTable, LogTableBody, LogTableRow, } from 'sentry/views/explore/logs/styles'; @@ -452,12 +452,11 @@ export function LogsInfiniteTable({ return ( -
)} -
+ {expanded && ( { expect(screen.getByTestId('loading-indicator')).toBeInTheDocument(); - await screen.findByText(/i am a log/); + expect(await screen.findByText(/i am a log/)).toBeInTheDocument(); expect(mockRequest).toHaveBeenCalledTimes(1); }); }); From ed56c7458d0e43d416539a1b2e3bd414e6b31ad7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josh=20Goldberg=20=E2=9C=A8?= Date: Tue, 17 Mar 2026 09:33:02 -0400 Subject: [PATCH 05/38] test: reduce to not testing expanded itself --- .../explore/logs/tables/useExpando.spec.tsx | 17 ++++++----------- .../views/explore/logs/tables/useExpando.tsx | 2 +- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/static/app/views/explore/logs/tables/useExpando.spec.tsx b/static/app/views/explore/logs/tables/useExpando.spec.tsx index 929535531d7211..c98f5a37eabc5c 100644 --- a/static/app/views/explore/logs/tables/useExpando.spec.tsx +++ b/static/app/views/explore/logs/tables/useExpando.spec.tsx @@ -1,12 +1,10 @@ import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary'; -import {useExpando, type Expando} from './useExpando'; - -let hookResult: Expando; +import {useExpando} from './useExpando'; function ExpandoWrapper() { - hookResult = useExpando(); - return
{hookResult.button}
; + const {button} = useExpando(); + return
{button}
; } const mockScrollIntoView = jest.fn(); @@ -16,31 +14,28 @@ describe('useExpando', () => { Element.prototype.scrollIntoView = mockScrollIntoView; }); - it('sets expanded to false when the button is not yet clicked', () => { + it('is contracted when the button is not yet clicked', () => { render(); expect(screen.getByText('Expand')).toBeInTheDocument(); - expect(hookResult.expanded).toBe(false); }); - it('sets expanded to true and scrolls into view when the button is clicked', async () => { + it('expands and scrolls into view when the button is clicked', async () => { render(); await userEvent.click(screen.getByText('Expand')); expect(screen.getByText('Collapse')).toBeInTheDocument(); - expect(hookResult.expanded).toBe(true); expect(mockScrollIntoView.mock.calls).toEqual([[{block: 'start'}]]); }); - it('sets expanded to false when the button is clicked a second time', async () => { + it('contracts when the button is clicked a second time', async () => { render(); await userEvent.click(screen.getByText('Expand')); await userEvent.click(screen.getByText('Collapse')); expect(screen.getByText('Expand')).toBeInTheDocument(); - expect(hookResult.expanded).toBe(false); expect(mockScrollIntoView.mock.calls).toEqual([[{block: 'start'}]]); }); }); diff --git a/static/app/views/explore/logs/tables/useExpando.tsx b/static/app/views/explore/logs/tables/useExpando.tsx index 5e9dbb8a650715..a5853c3d14f0d6 100644 --- a/static/app/views/explore/logs/tables/useExpando.tsx +++ b/static/app/views/explore/logs/tables/useExpando.tsx @@ -7,7 +7,7 @@ import {IconContract, IconExpand} from 'sentry/icons'; import {t} from 'sentry/locale'; import {TableActionButton} from 'sentry/views/explore/components/tableActionButton'; -export interface Expando { +interface Expando { button: React.ReactNode; expanded: boolean; } From 298fec4bc254ab664fd1c269e421d7588de0a0e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josh=20Goldberg=20=E2=9C=A8?= Date: Tue, 17 Mar 2026 10:13:45 -0400 Subject: [PATCH 06/38] semicolon, and fixtures --- {tests/js => static/app/utils}/fixtures/virtualization.ts | 0 static/app/views/explore/logs/styles.tsx | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename {tests/js => static/app/utils}/fixtures/virtualization.ts (100%) diff --git a/tests/js/fixtures/virtualization.ts b/static/app/utils/fixtures/virtualization.ts similarity index 100% rename from tests/js/fixtures/virtualization.ts rename to static/app/utils/fixtures/virtualization.ts diff --git a/static/app/views/explore/logs/styles.tsx b/static/app/views/explore/logs/styles.tsx index dc6c4f89405042..5052e20f338d69 100644 --- a/static/app/views/explore/logs/styles.tsx +++ b/static/app/views/explore/logs/styles.tsx @@ -439,7 +439,7 @@ export const FloatingBackToTopContainer = styled('div')<{ position: absolute; z-index: 1; opacity: ${p => (p.inReplay ? 1 : 0.9)}; - top: ${p => (p.inReplay ? p.theme.space.md : '65px;')}; + top: ${p => (p.inReplay ? p.theme.space.md : '65px')}; left: calc(50% - var(--floatingWidth) / 2); width: var(--floatingWidth); display: flex; From 345f652ac836af1a4a0c520bdbbbe8ede1514026 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josh=20Goldberg=20=E2=9C=A8?= Date: Tue, 17 Mar 2026 10:51:15 -0400 Subject: [PATCH 07/38] fix: height prop spreading; desktop vs mobile refs --- static/app/views/explore/components/table.tsx | 4 ++-- .../explore/logs/tables/useExpando.spec.tsx | 10 ++++++++-- .../views/explore/logs/tables/useExpando.tsx | 20 ++++++++++++------- 3 files changed, 23 insertions(+), 11 deletions(-) diff --git a/static/app/views/explore/components/table.tsx b/static/app/views/explore/components/table.tsx index 665d2a77e6b197..57cfd1484e71f9 100644 --- a/static/app/views/explore/components/table.tsx +++ b/static/app/views/explore/components/table.tsx @@ -25,10 +25,10 @@ interface TableProps extends React.ComponentProps { size?: number; } -export function Table({ref, children, style, ...props}: TableProps) { +export function Table({ref, children, height, style, ...props}: TableProps) { return ( <_TableWrapper {...props}> - <_Table ref={ref} height={props.height} style={style}> + <_Table ref={ref} height={height} style={style}> {children} diff --git a/static/app/views/explore/logs/tables/useExpando.spec.tsx b/static/app/views/explore/logs/tables/useExpando.spec.tsx index c98f5a37eabc5c..db054a709cf165 100644 --- a/static/app/views/explore/logs/tables/useExpando.spec.tsx +++ b/static/app/views/explore/logs/tables/useExpando.spec.tsx @@ -26,7 +26,10 @@ describe('useExpando', () => { await userEvent.click(screen.getByText('Expand')); expect(screen.getByText('Collapse')).toBeInTheDocument(); - expect(mockScrollIntoView.mock.calls).toEqual([[{block: 'start'}]]); + expect(mockScrollIntoView.mock.calls).toEqual([ + [{block: 'start'}], + [{block: 'start'}], + ]); }); it('contracts when the button is clicked a second time', async () => { @@ -36,6 +39,9 @@ describe('useExpando', () => { await userEvent.click(screen.getByText('Collapse')); expect(screen.getByText('Expand')).toBeInTheDocument(); - expect(mockScrollIntoView.mock.calls).toEqual([[{block: 'start'}]]); + expect(mockScrollIntoView.mock.calls).toEqual([ + [{block: 'start'}], + [{block: 'start'}], + ]); }); }); diff --git a/static/app/views/explore/logs/tables/useExpando.tsx b/static/app/views/explore/logs/tables/useExpando.tsx index a5853c3d14f0d6..8254962a228148 100644 --- a/static/app/views/explore/logs/tables/useExpando.tsx +++ b/static/app/views/explore/logs/tables/useExpando.tsx @@ -14,7 +14,8 @@ interface Expando { export function useExpando(): Expando { const [expanded, setExpanded] = useState(false); - const ref = useRef(null); + const refDesktop = useRef(null); + const refMobile = useRef(null); const [Icon, text] = expanded ? [IconContract, t('Collapse')] @@ -26,9 +27,11 @@ export function useExpando(): Expando { useLayoutEffect(() => { if (expanded) { - ref.current?.scrollIntoView({ - block: 'start', - }); + for (const ref of [refDesktop, refMobile]) { + ref.current?.scrollIntoView({ + block: 'start', + }); + } } }, [expanded]); @@ -36,15 +39,18 @@ export function useExpando(): Expando { 'aria-label': text, icon: , onClick: toggleExpanded, - ref, size: 'sm', } as const; return { button: ( } - desktop={{text}} + desktop={ + + {text} + + } + mobile={} /> ), expanded, From 717e02db651e710a176e6280e134aaa87ca69a74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josh=20Goldberg=20=E2=9C=A8?= Date: Tue, 17 Mar 2026 13:37:11 -0400 Subject: [PATCH 08/38] ref: update virtualization file path --- static/app/views/explore/logs/content.spec.tsx | 2 +- static/app/views/explore/logs/logsTab.spec.tsx | 2 +- .../app/views/performance/newTraceDetails/traceOurlogs.spec.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/static/app/views/explore/logs/content.spec.tsx b/static/app/views/explore/logs/content.spec.tsx index 8727c7a7bbb3a6..04a9ec085bed72 100644 --- a/static/app/views/explore/logs/content.spec.tsx +++ b/static/app/views/explore/logs/content.spec.tsx @@ -2,7 +2,6 @@ import {createLogFixtures, initializeLogsTest} from 'sentry-fixture/log'; import {ProjectKeysFixture} from 'sentry-fixture/projectKeys'; import {TeamFixture} from 'sentry-fixture/team'; import {TimeSeriesFixture} from 'sentry-fixture/timeSeries'; -import {mockGetBoundingClientRect} from 'sentry-fixture/virtualization'; import { render, @@ -17,6 +16,7 @@ import {ProjectsStore} from 'sentry/stores/projectsStore'; import {TeamStore} from 'sentry/stores/teamStore'; import type {Organization} from 'sentry/types/organization'; import type {Project} from 'sentry/types/project'; +import {mockGetBoundingClientRect} from 'sentry/utils/fixtures/virtualization'; import {LOGS_AUTO_REFRESH_KEY} from 'sentry/views/explore/contexts/logs/logsAutoRefreshContext'; import type {OurLogsResponseItem} from 'sentry/views/explore/logs/types'; diff --git a/static/app/views/explore/logs/logsTab.spec.tsx b/static/app/views/explore/logs/logsTab.spec.tsx index 3e945e60dd2ce5..54c6c313ec54e2 100644 --- a/static/app/views/explore/logs/logsTab.spec.tsx +++ b/static/app/views/explore/logs/logsTab.spec.tsx @@ -1,11 +1,11 @@ import {initializeLogsTest} from 'sentry-fixture/log'; import {TimeSeriesFixture} from 'sentry-fixture/timeSeries'; -import {mockGetBoundingClientRect} from 'sentry-fixture/virtualization'; import {render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary'; import type {DatePageFilterProps} from 'sentry/components/pageFilters/date/datePageFilter'; import {LogsAnalyticsPageSource} from 'sentry/utils/analytics/logsAnalyticsEvent'; +import {mockGetBoundingClientRect} from 'sentry/utils/fixtures/virtualization'; import {LOGS_AUTO_REFRESH_KEY} from 'sentry/views/explore/contexts/logs/logsAutoRefreshContext'; import {LogsPageDataProvider} from 'sentry/views/explore/contexts/logs/logsPageData'; import { diff --git a/static/app/views/performance/newTraceDetails/traceOurlogs.spec.tsx b/static/app/views/performance/newTraceDetails/traceOurlogs.spec.tsx index 96eae8ddace08b..5ea45a120794fc 100644 --- a/static/app/views/performance/newTraceDetails/traceOurlogs.spec.tsx +++ b/static/app/views/performance/newTraceDetails/traceOurlogs.spec.tsx @@ -1,8 +1,8 @@ import {OrganizationFixture} from 'sentry-fixture/organization'; -import {mockGetBoundingClientRect} from 'sentry-fixture/virtualization'; import {render, screen} from 'sentry-test/reactTestingLibrary'; +import {mockGetBoundingClientRect} from 'sentry/utils/fixtures/virtualization'; import {OurLogKnownFieldKey} from 'sentry/views/explore/logs/types'; import { TraceViewLogsDataProvider, From 0c09ee7be54d36899bebeae0041f658d0abf93bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josh=20Goldberg=20=E2=9C=A8?= Date: Tue, 17 Mar 2026 14:17:35 -0400 Subject: [PATCH 09/38] ref: remove unused tableLeft --- static/app/views/explore/logs/styles.tsx | 1 - static/app/views/explore/logs/tables/logsInfiniteTable.tsx | 1 - 2 files changed, 2 deletions(-) diff --git a/static/app/views/explore/logs/styles.tsx b/static/app/views/explore/logs/styles.tsx index 5052e20f338d69..4dd782fec3eeba 100644 --- a/static/app/views/explore/logs/styles.tsx +++ b/static/app/views/explore/logs/styles.tsx @@ -432,7 +432,6 @@ export const LogsSidebarCollapseButton = styled(Button)<{sidebarOpen: boolean}>` export const FloatingBackToTopContainer = styled('div')<{ inReplay?: boolean; - tableLeft?: number; tableWidth?: number; }>` --floatingWidth: ${p => (p.tableWidth ? `${p.tableWidth}px` : '100%')}; diff --git a/static/app/views/explore/logs/tables/logsInfiniteTable.tsx b/static/app/views/explore/logs/tables/logsInfiniteTable.tsx index d10116971e57cd..d77563705bcf98 100644 --- a/static/app/views/explore/logs/tables/logsInfiniteTable.tsx +++ b/static/app/views/explore/logs/tables/logsInfiniteTable.tsx @@ -544,7 +544,6 @@ export function LogsInfiniteTable({ {expanded && ( {!embeddedOptions?.replay && ( From 738df5eb2ebe413264de2b94cc09b377de868214 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josh=20Goldberg=20=E2=9C=A8?= Date: Wed, 18 Mar 2026 13:32:02 -0400 Subject: [PATCH 10/38] feat: remove excess scrollbars with a responsive height (WIP) --- .../app/views/explore/components/styles.tsx | 8 +++--- static/app/views/explore/logs/content.tsx | 24 +++++++++++++++--- static/app/views/explore/logs/logsTab.tsx | 4 +-- static/app/views/explore/logs/styles.tsx | 25 +++++++++++++++---- .../views/explore/logs/tables/useExpando.tsx | 22 +++------------- 5 files changed, 51 insertions(+), 32 deletions(-) diff --git a/static/app/views/explore/components/styles.tsx b/static/app/views/explore/components/styles.tsx index f9a3eb5a3b2fea..ee24f47eb58076 100644 --- a/static/app/views/explore/components/styles.tsx +++ b/static/app/views/explore/components/styles.tsx @@ -1,7 +1,7 @@ import {css} from '@emotion/react'; import styled from '@emotion/styled'; -import {Container, type ContainerProps} from '@sentry/scraps/layout'; +import {Flex, type FlexProps} from '@sentry/scraps/layout'; import * as Layout from 'sentry/components/layouts/thirds'; import {SchemaHintsSection} from 'sentry/views/explore/components/schemaHints/schemaHintsList'; @@ -30,14 +30,16 @@ export const ExploreControlSection = styled('aside')<{expanded: boolean}>` } `; -export function ExploreContentSection(props: ContainerProps) { +export function ExploreContentSection(props: FlexProps<'div'>) { const hasPageFrame = useHasPageFrameFeature(); return ( - ); diff --git a/static/app/views/explore/logs/content.tsx b/static/app/views/explore/logs/content.tsx index dff06c1ebaefc7..34409f8a8dae25 100644 --- a/static/app/views/explore/logs/content.tsx +++ b/static/app/views/explore/logs/content.tsx @@ -1,5 +1,7 @@ +import {css, Global} from '@emotion/react'; + import {LinkButton} from '@sentry/scraps/button'; -import {Grid, Stack} from '@sentry/scraps/layout'; +import {Grid} from '@sentry/scraps/layout'; import {FeedbackButton} from 'sentry/components/feedbackButton/feedbackButton'; import * as Layout from 'sentry/components/layouts/thirds'; @@ -61,7 +63,23 @@ export default function LogsContent() { analyticsPageSource={LogsAnalyticsPageSource.EXPLORE_LOGS} source="location" > - + div > div#main > [data-viewport-constrained]) { + height: 100vh; + height: 100dvh; + overflow: hidden; + } + div:has(> div#main > [data-viewport-constrained]) { + min-height: 0; + } + `} + /> + {defined(onboardingProject) ? ( @@ -74,7 +92,7 @@ export default function LogsContent() { )} - + diff --git a/static/app/views/explore/logs/logsTab.tsx b/static/app/views/explore/logs/logsTab.tsx index 41cc3966d3ec30..03fb999ee678ae 100644 --- a/static/app/views/explore/logs/logsTab.tsx +++ b/static/app/views/explore/logs/logsTab.tsx @@ -437,7 +437,7 @@ export function LogsTabContent({datePageFilterProps}: LogsTabProps) { {sidebarOpen ? : null} - + - + diff --git a/static/app/views/explore/logs/styles.tsx b/static/app/views/explore/logs/styles.tsx index 4dd782fec3eeba..f780434648b4ad 100644 --- a/static/app/views/explore/logs/styles.tsx +++ b/static/app/views/explore/logs/styles.tsx @@ -127,8 +127,17 @@ export const LogTableBodyCell = styled(TableBodyCell)` `; export const LogTable = styled(Table)` - height: 100%; + flex: 1; + min-height: 0; + display: flex; + flex-direction: column; margin-bottom: 0; + overflow-x: hidden; + + > div { + flex: 1; + min-height: 0; + } `; export const LogTableBody = styled(TableBody)<{ @@ -145,16 +154,13 @@ export const LogTableBody = styled(TableBody)<{ padding-top: ${p.theme.space.md}; padding-bottom: ${p.theme.space.md}; `} + overflow-x: hidden; overflow-y: auto; /* If a parent renderer bails out, the element might default to 0px: which causes Tanstack Virtual to stay at 0. */ min-height: 1px; height: 100%; - max-height: ${p => - p.expanded - ? `calc(95vh - ${GRID_BODY_ROW_HEIGHT * 1.5}px)` - : 'max(calc(60vh - 20rem), 10rem)'}; `; export const LogDetailTableBodyCell = styled(TableBodyCell)` @@ -306,18 +312,27 @@ export function TableActionsContainer(props: FlexProps<'div'>) { export const LogsItemContainer = styled('div')` flex: 1 1 auto; + min-height: 0; + overflow: hidden; margin-top: ${p => p.theme.space.md}; margin-bottom: ${p => p.theme.space.md}; position: relative; + display: flex; + flex-direction: column; `; export const LogsTableActionsContainer = styled(LogsItemContainer)` + flex: 0 0 auto; + overflow: visible; margin-bottom: 0; display: flex; + flex-direction: row; justify-content: space-between; `; export const LogsGraphContainer = styled(LogsItemContainer)` + flex: 0 0 auto; + overflow: visible; display: flex; flex-direction: column; gap: ${p => p.theme.space.md}; diff --git a/static/app/views/explore/logs/tables/useExpando.tsx b/static/app/views/explore/logs/tables/useExpando.tsx index 8254962a228148..eb7b3ec9125d5d 100644 --- a/static/app/views/explore/logs/tables/useExpando.tsx +++ b/static/app/views/explore/logs/tables/useExpando.tsx @@ -1,4 +1,4 @@ -import {useCallback, useLayoutEffect, useRef, useState} from 'react'; +import {useCallback, useState} from 'react'; import styled from '@emotion/styled'; import {Button} from '@sentry/scraps/button'; @@ -14,8 +14,6 @@ interface Expando { export function useExpando(): Expando { const [expanded, setExpanded] = useState(false); - const refDesktop = useRef(null); - const refMobile = useRef(null); const [Icon, text] = expanded ? [IconContract, t('Collapse')] @@ -25,16 +23,6 @@ export function useExpando(): Expando { setExpanded(previousExpanded => !previousExpanded); }, []); - useLayoutEffect(() => { - if (expanded) { - for (const ref of [refDesktop, refMobile]) { - ref.current?.scrollIntoView({ - block: 'start', - }); - } - } - }, [expanded]); - const buttonProps = { 'aria-label': text, icon: , @@ -45,12 +33,8 @@ export function useExpando(): Expando { return { button: ( - {text} - - } - mobile={} + desktop={{text}} + mobile={} /> ), expanded, From bf33571864bd45caf9043fa1e10d3c3dd19fa970 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josh=20Goldberg=20=E2=9C=A8?= Date: Wed, 18 Mar 2026 15:28:23 -0400 Subject: [PATCH 11/38] fix(logs): Disable scroll anchoring on virtual scroll container Browser scroll anchoring fights with TanStack Virtual's own scroll correction when the scroll container uses a flex/grid-based height instead of an explicit max-height. This causes a feedback loop where a single large scroll makes the table keep scrolling indefinitely. Adding overflow-anchor: none disables the browser mechanism, letting TanStack Virtual manage scroll position corrections exclusively. Co-Authored-By: Claude Sonnet 4 Made-with: Cursor --- static/app/views/explore/logs/styles.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/static/app/views/explore/logs/styles.tsx b/static/app/views/explore/logs/styles.tsx index f780434648b4ad..365b51b8bc191d 100644 --- a/static/app/views/explore/logs/styles.tsx +++ b/static/app/views/explore/logs/styles.tsx @@ -156,6 +156,7 @@ export const LogTableBody = styled(TableBody)<{ `} overflow-x: hidden; overflow-y: auto; + overflow-anchor: none; /* If a parent renderer bails out, the element might default to 0px: which causes Tanstack Virtual to stay at 0. */ min-height: 1px; From 205761a4e87c2d0d0e058c4d2da4d1f66eef76a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josh=20Goldberg=20=E2=9C=A8?= Date: Tue, 24 Mar 2026 16:44:21 -0400 Subject: [PATCH 12/38] wip: css styling cleanup attempt --- .../components/viewportConstrainedPage.tsx | 12 ++++ static/app/views/explore/logs/content.tsx | 23 +------- static/app/views/explore/logs/logsTab.tsx | 59 +++++++++++-------- static/app/views/explore/logs/styles.tsx | 55 +++++++++-------- 4 files changed, 78 insertions(+), 71 deletions(-) create mode 100644 static/app/views/explore/components/viewportConstrainedPage.tsx diff --git a/static/app/views/explore/components/viewportConstrainedPage.tsx b/static/app/views/explore/components/viewportConstrainedPage.tsx new file mode 100644 index 00000000000000..b11c248ce91a69 --- /dev/null +++ b/static/app/views/explore/components/viewportConstrainedPage.tsx @@ -0,0 +1,12 @@ +import type {FlexProps} from '@sentry/scraps/layout'; + +import * as Layout from 'sentry/components/layouts/thirds'; + +/** + * A page layout that constrains itself to the viewport height to prevent + * window-level scrolling. Content within must manage its own overflow + * (e.g. via scrollable table bodies). + */ +export function ViewportConstrainedPage(props: FlexProps<'main'>) { + return ; +} diff --git a/static/app/views/explore/logs/content.tsx b/static/app/views/explore/logs/content.tsx index 34409f8a8dae25..b81d1e55dba572 100644 --- a/static/app/views/explore/logs/content.tsx +++ b/static/app/views/explore/logs/content.tsx @@ -1,5 +1,3 @@ -import {css, Global} from '@emotion/react'; - import {LinkButton} from '@sentry/scraps/button'; import {Grid} from '@sentry/scraps/layout'; @@ -21,6 +19,7 @@ import {useMaxPickableDays} from 'sentry/utils/useMaxPickableDays'; import {useOrganization} from 'sentry/utils/useOrganization'; import {useProjects} from 'sentry/utils/useProjects'; import {ExploreBreadcrumb} from 'sentry/views/explore/components/breadcrumb'; +import {ViewportConstrainedPage} from 'sentry/views/explore/components/viewportConstrainedPage'; import {LogsPageDataProvider} from 'sentry/views/explore/contexts/logs/logsPageData'; import {useGetSavedQuery} from 'sentry/views/explore/hooks/useGetSavedQueries'; import {LogsTabOnboarding} from 'sentry/views/explore/logs/logsOnboarding'; @@ -63,23 +62,7 @@ export default function LogsContent() { analyticsPageSource={LogsAnalyticsPageSource.EXPLORE_LOGS} source="location" > - div > div#main > [data-viewport-constrained]) { - height: 100vh; - height: 100dvh; - overflow: hidden; - } - div:has(> div#main > [data-viewport-constrained]) { - min-height: 0; - } - `} - /> - + {defined(onboardingProject) ? ( @@ -92,7 +75,7 @@ export default function LogsContent() { )} - + diff --git a/static/app/views/explore/logs/logsTab.tsx b/static/app/views/explore/logs/logsTab.tsx index 03fb999ee678ae..ca46cbdaee7c20 100644 --- a/static/app/views/explore/logs/logsTab.tsx +++ b/static/app/views/explore/logs/logsTab.tsx @@ -436,37 +436,44 @@ export function LogsTabContent({datePageFilterProps}: LogsTabProps) { {sidebarOpen ? : null} - - - - } - onClick={() => setSidebarOpen(!sidebarOpen)} - > - {sidebarOpen ? null : t('Advanced')} - - - + + {!expando.expanded && ( + + + } + onClick={() => setSidebarOpen(!sidebarOpen)} + > + {sidebarOpen ? null : t('Advanced')} + + + + )} - - - + {!expando.expanded && ( + + + + )} diff --git a/static/app/views/explore/logs/styles.tsx b/static/app/views/explore/logs/styles.tsx index 365b51b8bc191d..9db0155dda4e53 100644 --- a/static/app/views/explore/logs/styles.tsx +++ b/static/app/views/explore/logs/styles.tsx @@ -134,6 +134,8 @@ export const LogTable = styled(Table)` margin-bottom: 0; overflow-x: hidden; + /* PanelBody inside Table's Panel wrapper needs flex sizing to let + LogTableBody fill the available height and scroll internally. */ > div { flex: 1; min-height: 0; @@ -311,33 +313,36 @@ export function TableActionsContainer(props: FlexProps<'div'>) { return ; } -export const LogsItemContainer = styled('div')` - flex: 1 1 auto; - min-height: 0; - overflow: hidden; - margin-top: ${p => p.theme.space.md}; - margin-bottom: ${p => p.theme.space.md}; - position: relative; - display: flex; - flex-direction: column; -`; +export function LogsItemContainer(props: FlexProps<'div'>) { + return ( + + ); +} -export const LogsTableActionsContainer = styled(LogsItemContainer)` - flex: 0 0 auto; - overflow: visible; - margin-bottom: 0; - display: flex; - flex-direction: row; - justify-content: space-between; -`; +export function LogsTableActionsContainer(props: FlexProps<'div'>) { + return ( + + ); +} -export const LogsGraphContainer = styled(LogsItemContainer)` - flex: 0 0 auto; - overflow: visible; - display: flex; - flex-direction: column; - gap: ${p => p.theme.space.md}; -`; +export function LogsGraphContainer(props: FlexProps<'div'>) { + return ( + + ); +} export const AutoRefreshLabel = styled('label')` display: flex; From fbc108e3ce0d111171c2da6c9f15442356b07460 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josh=20Goldberg=20=E2=9C=A8?= Date: Wed, 25 Mar 2026 12:24:43 -0400 Subject: [PATCH 13/38] fix(logs): Viewport-constrained layout for logs page Replace hacky Global styles with :has() selectors and data attributes with clean CSS containment approach. Use contain: size on Layout.Page to prevent content from inflating the app shell beyond viewport height. Keep ExploreBodyContent in row direction at all breakpoints so ExploreContentSection always gets its height via cross-axis stretch, ensuring reliable height propagation to the virtualized scroll container on both desktop and mobile. Hide ExploreControlSection on mobile where it was non-functional (toggle button already hidden) and just wasting vertical space. Co-Authored-By: Claude Sonnet 4 Made-with: Cursor --- static/app/views/explore/logs/logsTab.tsx | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/static/app/views/explore/logs/logsTab.tsx b/static/app/views/explore/logs/logsTab.tsx index ca46cbdaee7c20..99fbccdabe03f2 100644 --- a/static/app/views/explore/logs/logsTab.tsx +++ b/static/app/views/explore/logs/logsTab.tsx @@ -1,4 +1,5 @@ import {Fragment, memo, useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import styled from '@emotion/styled'; import {Button} from '@sentry/scraps/button'; import {TabList, Tabs} from '@sentry/scraps/tabs'; @@ -432,10 +433,10 @@ export function LogsTabContent({datePageFilterProps}: LogsTabProps) { datePageFilterProps={datePageFilterProps} searchBarWidthOffset={columnEditorButtonRef.current?.clientWidth} /> - - + + {sidebarOpen ? : null} - + {!expando.expanded && ( @@ -534,7 +535,17 @@ export function LogsTabContent({datePageFilterProps}: LogsTabProps) { )} - +
); } + +const ViewportConstrainedBody = styled(ExploreBodyContent)` + flex-direction: row; +`; + +const LogsControlSection = styled(ExploreControlSection)` + @media (max-width: ${p => p.theme.breakpoints.md}) { + display: none; + } +`; From d41f48f61eeb3da2ea8606af2af132cd606fba05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josh=20Goldberg=20=E2=9C=A8?= Date: Thu, 26 Mar 2026 11:53:40 -0400 Subject: [PATCH 14/38] ref(logs): Remove > div bandaid with opt-in contentsBody prop Add a `display` prop to PanelBody and a `contentsBody` prop to gridEditable Body so callers can opt into `display: contents` on the intermediate PanelBody div. This makes the PanelBody transparent to flex layout, letting flex sizing flow directly from Panel to its grandchildren. LogTable uses this to eliminate the fragile `> div` selector that was reaching into PanelBody's implementation details. Co-Authored-By: Claude Sonnet 4 Made-with: Cursor --- static/app/components/panels/panelBody.tsx | 2 ++ .../app/components/tables/gridEditable/styles.tsx | 4 +++- static/app/views/explore/logs/styles.tsx | 13 +++++-------- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/static/app/components/panels/panelBody.tsx b/static/app/components/panels/panelBody.tsx index cf9e3814367987..df16b1ea523688 100644 --- a/static/app/components/panels/panelBody.tsx +++ b/static/app/components/panels/panelBody.tsx @@ -3,10 +3,12 @@ import styled from '@emotion/styled'; import {textStyles} from 'sentry/styles/text'; type BaseProps = { + display?: 'contents'; withPadding?: boolean; }; export const PanelBody = styled('div')` + ${p => p.display && `display: ${p.display};`} padding: ${p => (p.withPadding ? p.theme.space.xl : undefined)}; ${textStyles}; `; diff --git a/static/app/components/tables/gridEditable/styles.tsx b/static/app/components/tables/gridEditable/styles.tsx index f56b19337cbf6d..aa4c4b78f06607 100644 --- a/static/app/components/tables/gridEditable/styles.tsx +++ b/static/app/components/tables/gridEditable/styles.tsx @@ -47,14 +47,16 @@ export const HeaderButtonContainer = styled('div')` export const Body = styled( ({ children, + contentsBody, showVerticalScrollbar: _, ...props }: React.ComponentProps & { children?: React.ReactNode; + contentsBody?: boolean; showVerticalScrollbar?: boolean; }) => ( - {children} + {children} ) )` diff --git a/static/app/views/explore/logs/styles.tsx b/static/app/views/explore/logs/styles.tsx index 9db0155dda4e53..5d4c57a6636328 100644 --- a/static/app/views/explore/logs/styles.tsx +++ b/static/app/views/explore/logs/styles.tsx @@ -126,20 +126,17 @@ export const LogTableBodyCell = styled(TableBodyCell)` } `; -export const LogTable = styled(Table)` +function ContentsTable(props: React.ComponentProps) { + return ; +} + +export const LogTable = styled(ContentsTable)` flex: 1; min-height: 0; display: flex; flex-direction: column; margin-bottom: 0; overflow-x: hidden; - - /* PanelBody inside Table's Panel wrapper needs flex sizing to let - LogTableBody fill the available height and scroll internally. */ - > div { - flex: 1; - min-height: 0; - } `; export const LogTableBody = styled(TableBody)<{ From 15878c23469ccfd7020daeb9eb11741cfb17d389 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josh=20Goldberg=20=E2=9C=A8?= Date: Tue, 31 Mar 2026 10:43:15 -0400 Subject: [PATCH 15/38] fix: yet again fix scrolling --- static/app/components/tables/gridEditable/styles.tsx | 2 ++ static/app/views/explore/logs/logsTab.tsx | 1 + 2 files changed, 3 insertions(+) diff --git a/static/app/components/tables/gridEditable/styles.tsx b/static/app/components/tables/gridEditable/styles.tsx index aa4c4b78f06607..f10d2529ce0e89 100644 --- a/static/app/components/tables/gridEditable/styles.tsx +++ b/static/app/components/tables/gridEditable/styles.tsx @@ -100,6 +100,8 @@ export const Grid = styled('table')<{ ? css` height: 100%; max-height: ${typeof p.height === 'number' ? p.height + 'px' : p.height}; + flex: 1; + min-height: 0; &:has(> thead + tbody) { grid-template-rows: auto 1fr; diff --git a/static/app/views/explore/logs/logsTab.tsx b/static/app/views/explore/logs/logsTab.tsx index 99fbccdabe03f2..92292e28e1e61a 100644 --- a/static/app/views/explore/logs/logsTab.tsx +++ b/static/app/views/explore/logs/logsTab.tsx @@ -542,6 +542,7 @@ export function LogsTabContent({datePageFilterProps}: LogsTabProps) { const ViewportConstrainedBody = styled(ExploreBodyContent)` flex-direction: row; + min-height: 0; `; const LogsControlSection = styled(ExploreControlSection)` From a2de821e497c493f5eb050d2d18fe3e7df9115dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josh=20Goldberg=20=E2=9C=A8?= Date: Tue, 31 Mar 2026 11:32:24 -0400 Subject: [PATCH 16/38] fix(logs): Use CSS size containment for viewport-constrained page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace maxHeight: 100vh with contain: size. The maxHeight approach didn't account for sibling elements (TopBar, Footer) in the flex chain, causing a scrollbar equal to the footer's height. Size containment prevents the page's intrinsic size from bubbling up through the flex algorithm, so it fills exactly the remaining space after siblings — naturally accounting for the footer. Co-Authored-By: Claude Sonnet 4 Made-with: Cursor --- .../components/viewportConstrainedPage.tsx | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/static/app/views/explore/components/viewportConstrainedPage.tsx b/static/app/views/explore/components/viewportConstrainedPage.tsx index b11c248ce91a69..b6e83c655b57a1 100644 --- a/static/app/views/explore/components/viewportConstrainedPage.tsx +++ b/static/app/views/explore/components/viewportConstrainedPage.tsx @@ -1,12 +1,21 @@ +import styled from '@emotion/styled'; + import type {FlexProps} from '@sentry/scraps/layout'; import * as Layout from 'sentry/components/layouts/thirds'; /** * A page layout that constrains itself to the viewport height to prevent - * window-level scrolling. Content within must manage its own overflow - * (e.g. via scrollable table bodies). + * window-level scrolling. Uses CSS size containment so that the page's + * intrinsic size doesn't bubble up through the flex chain — the flex + * algorithm sizes it to exactly the remaining space after siblings + * (TopBar, Footer, etc.), and content within must manage its own + * overflow (e.g. via scrollable table bodies). */ export function ViewportConstrainedPage(props: FlexProps<'main'>) { - return ; + return ; } + +const ConstrainedPage = styled(Layout.Page)` + contain: size; +`; From 5dd23fa5acaf2da1279a7b1da4d0e922fb7fee7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josh=20Goldberg=20=E2=9C=A8?= Date: Tue, 31 Mar 2026 11:46:39 -0400 Subject: [PATCH 17/38] feat(logs): Gate expando/collapso behind feature flag Gate the table expand/collapse button and viewport-constrained layout behind the `ourlogs-table-expando` feature flag. When the flag is off, the table behaves as on master with infinite height and no button. Also support a `?logsTableExpand=true` query param to force-enable the feature without the flag for local testing. Co-Authored-By: Claude Sonnet 4 Made-with: Cursor --- src/sentry/features/temporary.py | 2 ++ .../components/viewportConstrainedPage.tsx | 13 ++++++++++++- static/app/views/explore/logs/content.tsx | 7 ++++++- static/app/views/explore/logs/logsTab.tsx | 15 +++++++++++---- 4 files changed, 31 insertions(+), 6 deletions(-) diff --git a/src/sentry/features/temporary.py b/src/sentry/features/temporary.py index 0bf28bcf3fc8de..b49646a6ab3af4 100644 --- a/src/sentry/features/temporary.py +++ b/src/sentry/features/temporary.py @@ -456,6 +456,8 @@ def register_temporary_features(manager: FeatureManager) -> None: manager.add("organizations:ourlogs-stats", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) # Enable overlaying charts in logs manager.add("organizations:ourlogs-overlay-charts-ui", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) + # Enable the expand/collapse table height toggle in the logs UI + manager.add("organizations:ourlogs-table-expando", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) # Enable alerting on trace metrics manager.add("organizations:tracemetrics-alerts", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) # Enable attributes dropdown side panel in trace metrics diff --git a/static/app/views/explore/components/viewportConstrainedPage.tsx b/static/app/views/explore/components/viewportConstrainedPage.tsx index b6e83c655b57a1..b828a9d1f1c8c0 100644 --- a/static/app/views/explore/components/viewportConstrainedPage.tsx +++ b/static/app/views/explore/components/viewportConstrainedPage.tsx @@ -4,6 +4,10 @@ import type {FlexProps} from '@sentry/scraps/layout'; import * as Layout from 'sentry/components/layouts/thirds'; +interface ViewportConstrainedPageProps extends FlexProps<'main'> { + constrained?: boolean; +} + /** * A page layout that constrains itself to the viewport height to prevent * window-level scrolling. Uses CSS size containment so that the page's @@ -12,7 +16,14 @@ import * as Layout from 'sentry/components/layouts/thirds'; * (TopBar, Footer, etc.), and content within must manage its own * overflow (e.g. via scrollable table bodies). */ -export function ViewportConstrainedPage(props: FlexProps<'main'>) { +export function ViewportConstrainedPage({ + constrained = true, + ...props +}: ViewportConstrainedPageProps) { + if (!constrained) { + return ; + } + return ; } diff --git a/static/app/views/explore/logs/content.tsx b/static/app/views/explore/logs/content.tsx index b81d1e55dba572..17f475bc524540 100644 --- a/static/app/views/explore/logs/content.tsx +++ b/static/app/views/explore/logs/content.tsx @@ -15,6 +15,7 @@ import {defined} from 'sentry/utils'; import {trackAnalytics} from 'sentry/utils/analytics'; import {LogsAnalyticsPageSource} from 'sentry/utils/analytics/logsAnalyticsEvent'; import {useDatePageFilterProps} from 'sentry/utils/useDatePageFilterProps'; +import {useLocation} from 'sentry/utils/useLocation'; import {useMaxPickableDays} from 'sentry/utils/useMaxPickableDays'; import {useOrganization} from 'sentry/utils/useOrganization'; import {useProjects} from 'sentry/utils/useProjects'; @@ -34,6 +35,10 @@ import {useOnboardingProject} from 'sentry/views/insights/common/queries/useOnbo export default function LogsContent() { const organization = useOrganization(); + const location = useLocation(); + const hasExpando = + organization.features.includes('ourlogs-table-expando') || + location.query.logsTableExpand === 'true'; const maxPickableDays = useMaxPickableDays({ dataCategories: [DataCategory.LOG_BYTE], }); @@ -62,7 +67,7 @@ export default function LogsContent() { analyticsPageSource={LogsAnalyticsPageSource.EXPLORE_LOGS} source="location" > - + {defined(onboardingProject) ? ( diff --git a/static/app/views/explore/logs/logsTab.tsx b/static/app/views/explore/logs/logsTab.tsx index 92292e28e1e61a..b9ea66ebf1ec66 100644 --- a/static/app/views/explore/logs/logsTab.tsx +++ b/static/app/views/explore/logs/logsTab.tsx @@ -26,6 +26,7 @@ import type {AggregationKey} from 'sentry/utils/fields'; import {HOUR} from 'sentry/utils/formatters'; import {useQueryClient, type InfiniteData} from 'sentry/utils/queryClient'; import {useChartInterval} from 'sentry/utils/useChartInterval'; +import {useLocation} from 'sentry/utils/useLocation'; import {useOrganization} from 'sentry/utils/useOrganization'; import {OverChartButtonGroup} from 'sentry/views/explore/components/overChartButtonGroup'; import {SchemaHintsList} from 'sentry/views/explore/components/schemaHints/schemaHintsList'; @@ -425,7 +426,13 @@ export function LogsTabContent({datePageFilterProps}: LogsTabProps) { }, [pageFilters.selection.datetime]); const {infiniteLogsQueryResult} = useLogsPageData(); + const organization = useOrganization(); + const location = useLocation(); + const hasExpando = + organization.features.includes('ourlogs-table-expando') || + location.query.logsTableExpand === 'true'; const expando = useExpando(); + const isExpanded = hasExpando && expando.expanded; return ( @@ -438,7 +445,7 @@ export function LogsTabContent({datePageFilterProps}: LogsTabProps) { {sidebarOpen ? : null} - {!expando.expanded && ( + {!isExpanded && ( - {!expando.expanded && ( + {!isExpanded && ( } /> - {expando.button} + {hasExpando && expando.button} )} {tableTab === 'logs' ? ( Date: Tue, 31 Mar 2026 11:54:39 -0400 Subject: [PATCH 18/38] fix(logs): Rename query param to logsTableExpando Co-Authored-By: Claude Sonnet 4 Made-with: Cursor --- static/app/views/explore/logs/content.tsx | 2 +- static/app/views/explore/logs/logsTab.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/static/app/views/explore/logs/content.tsx b/static/app/views/explore/logs/content.tsx index 17f475bc524540..e38bcb0d31633d 100644 --- a/static/app/views/explore/logs/content.tsx +++ b/static/app/views/explore/logs/content.tsx @@ -38,7 +38,7 @@ export default function LogsContent() { const location = useLocation(); const hasExpando = organization.features.includes('ourlogs-table-expando') || - location.query.logsTableExpand === 'true'; + location.query.logsTableExpando === 'true'; const maxPickableDays = useMaxPickableDays({ dataCategories: [DataCategory.LOG_BYTE], }); diff --git a/static/app/views/explore/logs/logsTab.tsx b/static/app/views/explore/logs/logsTab.tsx index b9ea66ebf1ec66..7ed69c80abb877 100644 --- a/static/app/views/explore/logs/logsTab.tsx +++ b/static/app/views/explore/logs/logsTab.tsx @@ -430,7 +430,7 @@ export function LogsTabContent({datePageFilterProps}: LogsTabProps) { const location = useLocation(); const hasExpando = organization.features.includes('ourlogs-table-expando') || - location.query.logsTableExpand === 'true'; + location.query.logsTableExpando === 'true'; const expando = useExpando(); const isExpanded = hasExpando && expando.expanded; From 5499fe19b1aaa2e5cf5730d066cd20450db7fd48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josh=20Goldberg=20=E2=9C=A8?= Date: Tue, 31 Mar 2026 12:12:23 -0400 Subject: [PATCH 19/38] fix(logs): Remove stale scrollIntoView assertions from useExpando tests The scrollIntoView behavior was removed from useExpando but the test assertions were not updated, causing CI failures. Co-Authored-By: Claude Sonnet 4 Made-with: Cursor --- .../explore/logs/tables/useExpando.spec.tsx | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/static/app/views/explore/logs/tables/useExpando.spec.tsx b/static/app/views/explore/logs/tables/useExpando.spec.tsx index db054a709cf165..f533eb15aab6c0 100644 --- a/static/app/views/explore/logs/tables/useExpando.spec.tsx +++ b/static/app/views/explore/logs/tables/useExpando.spec.tsx @@ -7,29 +7,19 @@ function ExpandoWrapper() { return
{button}
; } -const mockScrollIntoView = jest.fn(); - describe('useExpando', () => { - beforeEach(() => { - Element.prototype.scrollIntoView = mockScrollIntoView; - }); - it('is contracted when the button is not yet clicked', () => { render(); expect(screen.getByText('Expand')).toBeInTheDocument(); }); - it('expands and scrolls into view when the button is clicked', async () => { + it('expands when the button is clicked', async () => { render(); await userEvent.click(screen.getByText('Expand')); expect(screen.getByText('Collapse')).toBeInTheDocument(); - expect(mockScrollIntoView.mock.calls).toEqual([ - [{block: 'start'}], - [{block: 'start'}], - ]); }); it('contracts when the button is clicked a second time', async () => { @@ -39,9 +29,5 @@ describe('useExpando', () => { await userEvent.click(screen.getByText('Collapse')); expect(screen.getByText('Expand')).toBeInTheDocument(); - expect(mockScrollIntoView.mock.calls).toEqual([ - [{block: 'start'}], - [{block: 'start'}], - ]); }); }); From 444f042fdb51e092619cd1b108fd1abe1083fb8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josh=20Goldberg=20=E2=9C=A8?= Date: Tue, 31 Mar 2026 12:28:00 -0400 Subject: [PATCH 20/38] fix(logs): Restore window virtualizer when feature flag is off When the ourlogs-table-expando flag is off, the page has no viewport constraint so the table body grows to fit all content with no scroll events. This broke infinite scroll loading since the container virtualizer never detected scrolling. Restore the dual-virtualizer pattern from master: use useWindowVirtualizer when expanded is undefined (flag off) and the container useVirtualizer when expanded is defined (flag on). Also only apply overflow-y: auto and height: 100% to LogTableBody when using the contained virtualizer. Co-Authored-By: Claude Sonnet 4 Made-with: Cursor --- static/app/views/explore/logs/styles.tsx | 9 +++++++-- .../explore/logs/tables/logsInfiniteTable.tsx | 18 +++++++++++++++--- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/static/app/views/explore/logs/styles.tsx b/static/app/views/explore/logs/styles.tsx index 5d4c57a6636328..04896bd55a0ea9 100644 --- a/static/app/views/explore/logs/styles.tsx +++ b/static/app/views/explore/logs/styles.tsx @@ -154,13 +154,18 @@ export const LogTableBody = styled(TableBody)<{ padding-bottom: ${p.theme.space.md}; `} overflow-x: hidden; - overflow-y: auto; overflow-anchor: none; /* If a parent renderer bails out, the element might default to 0px: which causes Tanstack Virtual to stay at 0. */ min-height: 1px; - height: 100%; + ${p => + p.expanded === undefined + ? '' + : ` + overflow-y: auto; + height: 100%; + `} `; export const LogDetailTableBodyCell = styled(TableBodyCell)` diff --git a/static/app/views/explore/logs/tables/logsInfiniteTable.tsx b/static/app/views/explore/logs/tables/logsInfiniteTable.tsx index d77563705bcf98..6cb189f006bfe8 100644 --- a/static/app/views/explore/logs/tables/logsInfiniteTable.tsx +++ b/static/app/views/explore/logs/tables/logsInfiniteTable.tsx @@ -12,7 +12,7 @@ import { import styled from '@emotion/styled'; import * as Sentry from '@sentry/react'; import type {Virtualizer} from '@tanstack/react-virtual'; -import {useVirtualizer} from '@tanstack/react-virtual'; +import {useVirtualizer, useWindowVirtualizer} from '@tanstack/react-virtual'; import {Button} from '@sentry/scraps/button'; import {Flex, Stack} from '@sentry/scraps/layout'; @@ -263,14 +263,26 @@ export function LogsInfiniteTable({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [searchString, localOnlyItemFilters?.filterText]); - const virtualizer = useVirtualizer({ - count: data?.length ?? 0, + const useContainedVirtualizer = expanded !== undefined; + + const windowVirtualizer = useWindowVirtualizer({ + count: useContainedVirtualizer ? 0 : (data?.length ?? 0), + estimateSize, + overscan: 50, + getItemKey: (index: number) => data?.[index]?.[OurLogKnownFieldKey.ID] ?? index, + scrollMargin: tableBodyRef.current?.offsetTop ?? 0, + }); + + const containerVirtualizer = useVirtualizer({ + count: useContainedVirtualizer ? (data?.length ?? 0) : 0, estimateSize, overscan: expanded ? 25 : 10, getScrollElement: () => tableBodyRef?.current, getItemKey: (index: number) => data?.[index]?.[OurLogKnownFieldKey.ID] ?? index, }); + const virtualizer = useContainedVirtualizer ? containerVirtualizer : windowVirtualizer; + useLayoutEffect(() => { virtualizer.measure(); }, [expanded, virtualizer]); From 101be467be48364a1cab3bd93b8f41d14a5f9bea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josh=20Goldberg=20=E2=9C=A8?= Date: Tue, 31 Mar 2026 12:32:05 -0400 Subject: [PATCH 21/38] fix(logs): Pass undefined instead of false when expando flag is off isExpanded was false (not undefined) when the flag was off, so expanded !== undefined was true and the container virtualizer was still being used instead of the window virtualizer. Co-Authored-By: Claude Sonnet 4 Made-with: Cursor --- static/app/views/explore/logs/logsTab.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/app/views/explore/logs/logsTab.tsx b/static/app/views/explore/logs/logsTab.tsx index 7ed69c80abb877..a0b0cdf345f6c9 100644 --- a/static/app/views/explore/logs/logsTab.tsx +++ b/static/app/views/explore/logs/logsTab.tsx @@ -432,7 +432,7 @@ export function LogsTabContent({datePageFilterProps}: LogsTabProps) { organization.features.includes('ourlogs-table-expando') || location.query.logsTableExpando === 'true'; const expando = useExpando(); - const isExpanded = hasExpando && expando.expanded; + const isExpanded = hasExpando ? expando.expanded : undefined; return ( From 56a3d9f42a8800836a06c277be627deb3bbee5d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josh=20Goldberg=20=E2=9C=A8?= Date: Tue, 31 Mar 2026 12:48:02 -0400 Subject: [PATCH 22/38] fix: add missing measure to mock --- static/app/views/explore/logs/tables/logsInfiniteTable.spec.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/static/app/views/explore/logs/tables/logsInfiniteTable.spec.tsx b/static/app/views/explore/logs/tables/logsInfiniteTable.spec.tsx index 6782ffce7ff825..7ad8cfb3b32e47 100644 --- a/static/app/views/explore/logs/tables/logsInfiniteTable.spec.tsx +++ b/static/app/views/explore/logs/tables/logsInfiniteTable.spec.tsx @@ -52,6 +52,7 @@ jest.mock('@tanstack/react-virtual', () => { {key: '3', index: 2, start: 100, end: 150, lane: 0}, ]), getTotalSize: jest.fn().mockReturnValue(150), + measure: jest.fn(), options: { scrollMargin: 0, }, From ac730d6280c0f3f9feb8936423e3d63bf2307482 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josh=20Goldberg=20=E2=9C=A8?= Date: Tue, 31 Mar 2026 13:27:16 -0400 Subject: [PATCH 23/38] ref: consolidate detection logic --- .../components/overChartButtonGroup.tsx | 1 - static/app/views/explore/logs/content.tsx | 7 ++----- static/app/views/explore/logs/logsTab.tsx | 15 ++++---------- .../views/explore/logs/tables/useExpando.tsx | 20 ++++++++++++++----- 4 files changed, 21 insertions(+), 22 deletions(-) diff --git a/static/app/views/explore/components/overChartButtonGroup.tsx b/static/app/views/explore/components/overChartButtonGroup.tsx index 03c72e396e27cc..1102841c173fb5 100644 --- a/static/app/views/explore/components/overChartButtonGroup.tsx +++ b/static/app/views/explore/components/overChartButtonGroup.tsx @@ -4,7 +4,6 @@ export function OverChartButtonGroup(props: FlexProps<'div'>) { return ( diff --git a/static/app/views/explore/logs/content.tsx b/static/app/views/explore/logs/content.tsx index e38bcb0d31633d..b537601c21bc35 100644 --- a/static/app/views/explore/logs/content.tsx +++ b/static/app/views/explore/logs/content.tsx @@ -15,7 +15,6 @@ import {defined} from 'sentry/utils'; import {trackAnalytics} from 'sentry/utils/analytics'; import {LogsAnalyticsPageSource} from 'sentry/utils/analytics/logsAnalyticsEvent'; import {useDatePageFilterProps} from 'sentry/utils/useDatePageFilterProps'; -import {useLocation} from 'sentry/utils/useLocation'; import {useMaxPickableDays} from 'sentry/utils/useMaxPickableDays'; import {useOrganization} from 'sentry/utils/useOrganization'; import {useProjects} from 'sentry/utils/useProjects'; @@ -26,6 +25,7 @@ import {useGetSavedQuery} from 'sentry/views/explore/hooks/useGetSavedQueries'; import {LogsTabOnboarding} from 'sentry/views/explore/logs/logsOnboarding'; import {LogsQueryParamsProvider} from 'sentry/views/explore/logs/logsQueryParamsProvider'; import {LogsTabContent} from 'sentry/views/explore/logs/logsTab'; +import {useExpando} from 'sentry/views/explore/logs/tables/useExpando'; import { useQueryParamsId, useQueryParamsTitle, @@ -35,10 +35,7 @@ import {useOnboardingProject} from 'sentry/views/insights/common/queries/useOnbo export default function LogsContent() { const organization = useOrganization(); - const location = useLocation(); - const hasExpando = - organization.features.includes('ourlogs-table-expando') || - location.query.logsTableExpando === 'true'; + const {enabled: hasExpando} = useExpando(); const maxPickableDays = useMaxPickableDays({ dataCategories: [DataCategory.LOG_BYTE], }); diff --git a/static/app/views/explore/logs/logsTab.tsx b/static/app/views/explore/logs/logsTab.tsx index a0b0cdf345f6c9..627386da7a035f 100644 --- a/static/app/views/explore/logs/logsTab.tsx +++ b/static/app/views/explore/logs/logsTab.tsx @@ -26,7 +26,6 @@ import type {AggregationKey} from 'sentry/utils/fields'; import {HOUR} from 'sentry/utils/formatters'; import {useQueryClient, type InfiniteData} from 'sentry/utils/queryClient'; import {useChartInterval} from 'sentry/utils/useChartInterval'; -import {useLocation} from 'sentry/utils/useLocation'; import {useOrganization} from 'sentry/utils/useOrganization'; import {OverChartButtonGroup} from 'sentry/views/explore/components/overChartButtonGroup'; import {SchemaHintsList} from 'sentry/views/explore/components/schemaHints/schemaHintsList'; @@ -426,13 +425,7 @@ export function LogsTabContent({datePageFilterProps}: LogsTabProps) { }, [pageFilters.selection.datetime]); const {infiniteLogsQueryResult} = useLogsPageData(); - const organization = useOrganization(); - const location = useLocation(); - const hasExpando = - organization.features.includes('ourlogs-table-expando') || - location.query.logsTableExpando === 'true'; const expando = useExpando(); - const isExpanded = hasExpando ? expando.expanded : undefined; return ( @@ -445,7 +438,7 @@ export function LogsTabContent({datePageFilterProps}: LogsTabProps) { {sidebarOpen ? : null} - {!isExpanded && ( + {!expando.expanded && ( - {!isExpanded && ( + {!expando.expanded && ( } /> - {hasExpando && expando.button} + {expando.enabled && expando.button} )} {tableTab === 'logs' ? ( { - setExpanded(previousExpanded => !previousExpanded); + setExpandedState(previousExpanded => !previousExpanded); }, []); const buttonProps = { @@ -37,7 +46,8 @@ export function useExpando(): Expando { mobile={} /> ), - expanded, + enabled, + expanded: enabled ? expandedState : undefined, }; } From ddb5c57e8c7a04c6b396c85aeafee7c8e48a3b8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josh=20Goldberg=20=E2=9C=A8?= Date: Wed, 1 Apr 2026 09:06:18 -0400 Subject: [PATCH 24/38] Remove footer on smaller viewport heights --- .../views/explore/components/viewportConstrainedPage.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/static/app/views/explore/components/viewportConstrainedPage.tsx b/static/app/views/explore/components/viewportConstrainedPage.tsx index b828a9d1f1c8c0..a4a56c8e89924e 100644 --- a/static/app/views/explore/components/viewportConstrainedPage.tsx +++ b/static/app/views/explore/components/viewportConstrainedPage.tsx @@ -15,6 +15,9 @@ interface ViewportConstrainedPageProps extends FlexProps<'main'> { * algorithm sizes it to exactly the remaining space after siblings * (TopBar, Footer, etc.), and content within must manage its own * overflow (e.g. via scrollable table bodies). + * + * When constrained, the footer is also hidden at smaller viewport heights. + * Similar to mobile, this is to leave more height space for essential UI. */ export function ViewportConstrainedPage({ constrained = true, @@ -29,4 +32,10 @@ export function ViewportConstrainedPage({ const ConstrainedPage = styled(Layout.Page)` contain: size; + + @media (max-height: 900px) { + ~ footer { + display: none; + } + } `; From 5d4cc4083c375a91ffae520b1f4da1fe6ac4e584 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josh=20Goldberg=20=E2=9C=A8?= Date: Wed, 1 Apr 2026 09:24:29 -0400 Subject: [PATCH 25/38] 175px height for tables, and splitNumber: 2 --- .../widgets/timeSeriesWidget/timeSeriesWidgetYAxis.tsx | 1 + static/app/views/explore/logs/logsGraph.tsx | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/static/app/views/dashboards/widgets/timeSeriesWidget/timeSeriesWidgetYAxis.tsx b/static/app/views/dashboards/widgets/timeSeriesWidget/timeSeriesWidgetYAxis.tsx index f2604e5f6167c3..4ee490f9ccbcce 100644 --- a/static/app/views/dashboards/widgets/timeSeriesWidget/timeSeriesWidgetYAxis.tsx +++ b/static/app/views/dashboards/widgets/timeSeriesWidget/timeSeriesWidgetYAxis.tsx @@ -16,6 +16,7 @@ export function TimeSeriesWidgetYAxis( { type: 'value', animation: false, + splitNumber: 2, axisPointer: { type: 'line' as const, snap: false, diff --git a/static/app/views/explore/logs/logsGraph.tsx b/static/app/views/explore/logs/logsGraph.tsx index f45cdc78d1b4d7..d3879bde3d068f 100644 --- a/static/app/views/explore/logs/logsGraph.tsx +++ b/static/app/views/explore/logs/logsGraph.tsx @@ -235,7 +235,7 @@ function Graph({ /> ) } - height={visualize.visible ? 200 : 50} + height={visualize.visible ? 175 : 50} revealActions="always" /> ); From 019091078bc1d0232cf5cc4dde14946edcbfe15a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josh=20Goldberg=20=E2=9C=A8?= Date: Wed, 1 Apr 2026 09:27:36 -0400 Subject: [PATCH 26/38] fix: remove flex 1 1 auto from LogsItemContainer --- static/app/views/explore/logs/styles.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/static/app/views/explore/logs/styles.tsx b/static/app/views/explore/logs/styles.tsx index 04896bd55a0ea9..bc41f90a043b41 100644 --- a/static/app/views/explore/logs/styles.tsx +++ b/static/app/views/explore/logs/styles.tsx @@ -319,7 +319,6 @@ export function LogsItemContainer(props: FlexProps<'div'>) { return ( Date: Wed, 1 Apr 2026 10:31:15 -0400 Subject: [PATCH 27/38] Roll out useIsShortViewport --- static/app/utils/useIsShortViewport.tsx | 7 +++++++ .../timeSeriesWidget/timeSeriesWidgetVisualization.tsx | 6 ++++++ .../views/explore/components/viewportConstrainedPage.tsx | 3 ++- static/app/views/explore/logs/logsGraph.tsx | 4 +++- 4 files changed, 18 insertions(+), 2 deletions(-) create mode 100644 static/app/utils/useIsShortViewport.tsx diff --git a/static/app/utils/useIsShortViewport.tsx b/static/app/utils/useIsShortViewport.tsx new file mode 100644 index 00000000000000..0c2ee58ee94174 --- /dev/null +++ b/static/app/utils/useIsShortViewport.tsx @@ -0,0 +1,7 @@ +import {useMedia} from 'sentry/utils/useMedia'; + +export const SHORT_VIEWPORT_HEIGHT = 900; + +export function useIsShortViewport(): boolean { + return useMedia(`(max-height: ${SHORT_VIEWPORT_HEIGHT}px)`); +} diff --git a/static/app/views/dashboards/widgets/timeSeriesWidget/timeSeriesWidgetVisualization.tsx b/static/app/views/dashboards/widgets/timeSeriesWidget/timeSeriesWidgetVisualization.tsx index 7e3c2f4b8500f4..574f90ac27394f 100644 --- a/static/app/views/dashboards/widgets/timeSeriesWidget/timeSeriesWidgetVisualization.tsx +++ b/static/app/views/dashboards/widgets/timeSeriesWidget/timeSeriesWidgetVisualization.tsx @@ -35,6 +35,7 @@ import {defined, escape} from 'sentry/utils'; import {uniq} from 'sentry/utils/array/uniq'; import type {AggregationOutputType} from 'sentry/utils/discover/fields'; import {RangeMap, type Range} from 'sentry/utils/number/rangeMap'; +import {useIsShortViewport} from 'sentry/utils/useIsShortViewport'; import {useLocation} from 'sentry/utils/useLocation'; import {useNavigate} from 'sentry/utils/useNavigate'; import {useWidgetSyncContext} from 'sentry/views/dashboards/contexts/widgetSyncContext'; @@ -134,6 +135,7 @@ export function TimeSeriesWidgetVisualization(props: TimeSeriesWidgetVisualizati // have the same difference in `timestamp`s) even though this is rare, since // the backend zerofills the data + const isShortViewport = useIsShortViewport(); const chartRef = useRef(null); const unregisterRef = useRef<(() => void) | null>(null); const {register: registerWithWidgetSyncContext, groupName} = useWidgetSyncContext(); @@ -245,8 +247,11 @@ export function TimeSeriesWidgetVisualization(props: TimeSeriesWidgetVisualizati const axisRangeProp = getAxisRange(props.axisRange) ?? 'auto'; + const yAxisSplitNumber = isShortViewport ? 2 : undefined; + const leftYAxis = TimeSeriesWidgetYAxis( { + splitNumber: yAxisSplitNumber, axisLabel: { formatter: (value: number) => formatYAxisValue(value, leftYAxisType, unitForType[leftYAxisType] ?? undefined), @@ -260,6 +265,7 @@ export function TimeSeriesWidgetVisualization(props: TimeSeriesWidgetVisualizati const rightYAxis = rightYAxisType ? TimeSeriesWidgetYAxis( { + splitNumber: yAxisSplitNumber, axisLabel: { formatter: (value: number) => formatYAxisValue( diff --git a/static/app/views/explore/components/viewportConstrainedPage.tsx b/static/app/views/explore/components/viewportConstrainedPage.tsx index a4a56c8e89924e..f7dca6fd29e891 100644 --- a/static/app/views/explore/components/viewportConstrainedPage.tsx +++ b/static/app/views/explore/components/viewportConstrainedPage.tsx @@ -3,6 +3,7 @@ import styled from '@emotion/styled'; import type {FlexProps} from '@sentry/scraps/layout'; import * as Layout from 'sentry/components/layouts/thirds'; +import {SHORT_VIEWPORT_HEIGHT} from 'sentry/utils/useIsShortViewport'; interface ViewportConstrainedPageProps extends FlexProps<'main'> { constrained?: boolean; @@ -33,7 +34,7 @@ export function ViewportConstrainedPage({ const ConstrainedPage = styled(Layout.Page)` contain: size; - @media (max-height: 900px) { + @media (max-height: ${SHORT_VIEWPORT_HEIGHT}px) { ~ footer { display: none; } diff --git a/static/app/views/explore/logs/logsGraph.tsx b/static/app/views/explore/logs/logsGraph.tsx index d3879bde3d068f..b9c29223a2df07 100644 --- a/static/app/views/explore/logs/logsGraph.tsx +++ b/static/app/views/explore/logs/logsGraph.tsx @@ -15,6 +15,7 @@ import {trackAnalytics} from 'sentry/utils/analytics'; import {EventView} from 'sentry/utils/discover/eventView'; import {DiscoverDatasets} from 'sentry/utils/discover/types'; import {useChartInterval} from 'sentry/utils/useChartInterval'; +import {useIsShortViewport} from 'sentry/utils/useIsShortViewport'; import {useLocation} from 'sentry/utils/useLocation'; import {useOrganization} from 'sentry/utils/useOrganization'; import {useProjects} from 'sentry/utils/useProjects'; @@ -118,6 +119,7 @@ function Graph({ timeseriesResult, visualize, }: GraphProps) { + const isShortViewport = useIsShortViewport(); const {isEmpty: tableIsEmpty, isPending: tableIsPending} = useLogsPageDataQueryResult(); const aggregate = visualize.yAxis; @@ -235,7 +237,7 @@ function Graph({ /> ) } - height={visualize.visible ? 175 : 50} + height={visualize.visible ? (isShortViewport ? 175 : 200) : 50} revealActions="always" /> ); From 9bbeb10b5e23684cdc47bca3e139ae1191a77883 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josh=20Goldberg=20=E2=9C=A8?= Date: Wed, 1 Apr 2026 12:46:14 -0400 Subject: [PATCH 28/38] properly pipe through a non-undefined 5 --- .../widgets/timeSeriesWidget/timeSeriesWidgetVisualization.tsx | 2 +- .../widgets/timeSeriesWidget/timeSeriesWidgetYAxis.tsx | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/static/app/views/dashboards/widgets/timeSeriesWidget/timeSeriesWidgetVisualization.tsx b/static/app/views/dashboards/widgets/timeSeriesWidget/timeSeriesWidgetVisualization.tsx index 574f90ac27394f..419d1085f4e957 100644 --- a/static/app/views/dashboards/widgets/timeSeriesWidget/timeSeriesWidgetVisualization.tsx +++ b/static/app/views/dashboards/widgets/timeSeriesWidget/timeSeriesWidgetVisualization.tsx @@ -247,7 +247,7 @@ export function TimeSeriesWidgetVisualization(props: TimeSeriesWidgetVisualizati const axisRangeProp = getAxisRange(props.axisRange) ?? 'auto'; - const yAxisSplitNumber = isShortViewport ? 2 : undefined; + const yAxisSplitNumber = isShortViewport ? 2 : 5; const leftYAxis = TimeSeriesWidgetYAxis( { diff --git a/static/app/views/dashboards/widgets/timeSeriesWidget/timeSeriesWidgetYAxis.tsx b/static/app/views/dashboards/widgets/timeSeriesWidget/timeSeriesWidgetYAxis.tsx index 4ee490f9ccbcce..f2604e5f6167c3 100644 --- a/static/app/views/dashboards/widgets/timeSeriesWidget/timeSeriesWidgetYAxis.tsx +++ b/static/app/views/dashboards/widgets/timeSeriesWidget/timeSeriesWidgetYAxis.tsx @@ -16,7 +16,6 @@ export function TimeSeriesWidgetYAxis( { type: 'value', animation: false, - splitNumber: 2, axisPointer: { type: 'line' as const, snap: false, From 37afa315ecff71dd38ebdb424693fb7a72f7736e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josh=20Goldberg=20=E2=9C=A8?= Date: Wed, 1 Apr 2026 15:12:13 -0400 Subject: [PATCH 29/38] isContainedVirtualizer --- .../app/views/explore/logs/tables/logsInfiniteTable.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/static/app/views/explore/logs/tables/logsInfiniteTable.tsx b/static/app/views/explore/logs/tables/logsInfiniteTable.tsx index 6cb189f006bfe8..f3c35cbd498d6a 100644 --- a/static/app/views/explore/logs/tables/logsInfiniteTable.tsx +++ b/static/app/views/explore/logs/tables/logsInfiniteTable.tsx @@ -263,10 +263,10 @@ export function LogsInfiniteTable({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [searchString, localOnlyItemFilters?.filterText]); - const useContainedVirtualizer = expanded !== undefined; + const isContainedVirtualizer = expanded !== undefined; const windowVirtualizer = useWindowVirtualizer({ - count: useContainedVirtualizer ? 0 : (data?.length ?? 0), + count: isContainedVirtualizer ? 0 : (data?.length ?? 0), estimateSize, overscan: 50, getItemKey: (index: number) => data?.[index]?.[OurLogKnownFieldKey.ID] ?? index, @@ -274,14 +274,14 @@ export function LogsInfiniteTable({ }); const containerVirtualizer = useVirtualizer({ - count: useContainedVirtualizer ? (data?.length ?? 0) : 0, + count: isContainedVirtualizer ? (data?.length ?? 0) : 0, estimateSize, overscan: expanded ? 25 : 10, getScrollElement: () => tableBodyRef?.current, getItemKey: (index: number) => data?.[index]?.[OurLogKnownFieldKey.ID] ?? index, }); - const virtualizer = useContainedVirtualizer ? containerVirtualizer : windowVirtualizer; + const virtualizer = isContainedVirtualizer ? containerVirtualizer : windowVirtualizer; useLayoutEffect(() => { virtualizer.measure(); From 2dccb428af9036b918623a4325d137df7496a4d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josh=20Goldberg=20=E2=9C=A8?= Date: Thu, 2 Apr 2026 10:35:43 -0400 Subject: [PATCH 30/38] Simplify expanded, and apply in traceOurlogs --- .../app/views/explore/logs/tables/logsInfiniteTable.tsx | 8 +++----- .../views/performance/newTraceDetails/traceOurlogs.tsx | 2 +- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/static/app/views/explore/logs/tables/logsInfiniteTable.tsx b/static/app/views/explore/logs/tables/logsInfiniteTable.tsx index f3c35cbd498d6a..c7b71b97433966 100644 --- a/static/app/views/explore/logs/tables/logsInfiniteTable.tsx +++ b/static/app/views/explore/logs/tables/logsInfiniteTable.tsx @@ -263,10 +263,8 @@ export function LogsInfiniteTable({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [searchString, localOnlyItemFilters?.filterText]); - const isContainedVirtualizer = expanded !== undefined; - const windowVirtualizer = useWindowVirtualizer({ - count: isContainedVirtualizer ? 0 : (data?.length ?? 0), + count: expanded ? 0 : (data?.length ?? 0), estimateSize, overscan: 50, getItemKey: (index: number) => data?.[index]?.[OurLogKnownFieldKey.ID] ?? index, @@ -274,14 +272,14 @@ export function LogsInfiniteTable({ }); const containerVirtualizer = useVirtualizer({ - count: isContainedVirtualizer ? (data?.length ?? 0) : 0, + count: expanded ? (data?.length ?? 0) : 0, estimateSize, overscan: expanded ? 25 : 10, getScrollElement: () => tableBodyRef?.current, getItemKey: (index: number) => data?.[index]?.[OurLogKnownFieldKey.ID] ?? index, }); - const virtualizer = isContainedVirtualizer ? containerVirtualizer : windowVirtualizer; + const virtualizer = expanded ? containerVirtualizer : windowVirtualizer; useLayoutEffect(() => { virtualizer.measure(); diff --git a/static/app/views/performance/newTraceDetails/traceOurlogs.tsx b/static/app/views/performance/newTraceDetails/traceOurlogs.tsx index 0197c6d8ba40bf..d86991d1e1c199 100644 --- a/static/app/views/performance/newTraceDetails/traceOurlogs.tsx +++ b/static/app/views/performance/newTraceDetails/traceOurlogs.tsx @@ -77,7 +77,7 @@ function LogsSectionContent() { {t('Open in Logs')} - + ); From 63039c2c63a787905a1ffa8876da7a1ab2051470 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josh=20Goldberg=20=E2=9C=A8?= Date: Thu, 2 Apr 2026 10:49:13 -0400 Subject: [PATCH 31/38] No, expanded is actually a false|true|undefined tri-state --- .../app/views/explore/logs/tables/logsInfiniteTable.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/static/app/views/explore/logs/tables/logsInfiniteTable.tsx b/static/app/views/explore/logs/tables/logsInfiniteTable.tsx index c7b71b97433966..f3c35cbd498d6a 100644 --- a/static/app/views/explore/logs/tables/logsInfiniteTable.tsx +++ b/static/app/views/explore/logs/tables/logsInfiniteTable.tsx @@ -263,8 +263,10 @@ export function LogsInfiniteTable({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [searchString, localOnlyItemFilters?.filterText]); + const isContainedVirtualizer = expanded !== undefined; + const windowVirtualizer = useWindowVirtualizer({ - count: expanded ? 0 : (data?.length ?? 0), + count: isContainedVirtualizer ? 0 : (data?.length ?? 0), estimateSize, overscan: 50, getItemKey: (index: number) => data?.[index]?.[OurLogKnownFieldKey.ID] ?? index, @@ -272,14 +274,14 @@ export function LogsInfiniteTable({ }); const containerVirtualizer = useVirtualizer({ - count: expanded ? (data?.length ?? 0) : 0, + count: isContainedVirtualizer ? (data?.length ?? 0) : 0, estimateSize, overscan: expanded ? 25 : 10, getScrollElement: () => tableBodyRef?.current, getItemKey: (index: number) => data?.[index]?.[OurLogKnownFieldKey.ID] ?? index, }); - const virtualizer = expanded ? containerVirtualizer : windowVirtualizer; + const virtualizer = isContainedVirtualizer ? containerVirtualizer : windowVirtualizer; useLayoutEffect(() => { virtualizer.measure(); From edd1fa3a4caa4e086e54e8a6b1f6a37b855af831 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josh=20Goldberg=20=E2=9C=A8?= Date: Thu, 2 Apr 2026 10:59:15 -0400 Subject: [PATCH 32/38] fix: FloatingBackToTopContainer in non-expando sections --- static/app/views/explore/logs/styles.tsx | 3 +- .../explore/logs/tables/logsInfiniteTable.tsx | 37 +++++++++---------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/static/app/views/explore/logs/styles.tsx b/static/app/views/explore/logs/styles.tsx index bc41f90a043b41..f981724be5c0cd 100644 --- a/static/app/views/explore/logs/styles.tsx +++ b/static/app/views/explore/logs/styles.tsx @@ -454,10 +454,11 @@ export const LogsSidebarCollapseButton = styled(Button)<{sidebarOpen: boolean}>` export const FloatingBackToTopContainer = styled('div')<{ inReplay?: boolean; + position?: 'absolute' | 'fixed'; tableWidth?: number; }>` --floatingWidth: ${p => (p.tableWidth ? `${p.tableWidth}px` : '100%')}; - position: absolute; + position: ${p => p.position}; z-index: 1; opacity: ${p => (p.inReplay ? 1 : 0.9)}; top: ${p => (p.inReplay ? p.theme.space.md : '65px')}; diff --git a/static/app/views/explore/logs/tables/logsInfiniteTable.tsx b/static/app/views/explore/logs/tables/logsInfiniteTable.tsx index f3c35cbd498d6a..fca7ce0b2e130b 100644 --- a/static/app/views/explore/logs/tables/logsInfiniteTable.tsx +++ b/static/app/views/explore/logs/tables/logsInfiniteTable.tsx @@ -553,25 +553,24 @@ export function LogsInfiniteTable({ )} - {expanded && ( - - {!embeddedOptions?.replay && ( - - )} + + {!embeddedOptions?.replay && ( + From 0abde9ac0b421910131dc23577862f5ab07e30ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josh=20Goldberg=20=E2=9C=A8?= Date: Thu, 2 Apr 2026 11:12:30 -0400 Subject: [PATCH 33/38] fix: replay embedding overflow and scroll issues --- static/app/views/replays/detail/ourlogs/index.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/static/app/views/replays/detail/ourlogs/index.tsx b/static/app/views/replays/detail/ourlogs/index.tsx index d81f22400a408e..523488db1e03a0 100644 --- a/static/app/views/replays/detail/ourlogs/index.tsx +++ b/static/app/views/replays/detail/ourlogs/index.tsx @@ -165,7 +165,8 @@ const BorderedSection = styled(FluidHeight)<{isStatus?: boolean}>` const TableScrollContainer = styled('div')` overflow-y: hidden; position: relative; - height: 100%; + display: flex; + flex-direction: column; border: 1px solid ${p => p.theme.tokens.border.primary}; border-radius: ${p => p.theme.radius.md}; `; From a4a83d09c52e4b1e594fb06e1dae8f5524636f70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josh=20Goldberg=20=E2=9C=A8?= Date: Thu, 2 Apr 2026 11:17:54 -0400 Subject: [PATCH 34/38] Rename to useTableExpando; extract useLogsTableExpandoFeatureFlag --- static/app/views/explore/logs/content.tsx | 4 ++-- static/app/views/explore/logs/logsTab.tsx | 4 ++-- ...useExpando.spec.tsx => useTableExpando.spec.tsx} | 6 +++--- .../tables/{useExpando.tsx => useTableExpando.tsx} | 13 +++++++++---- 4 files changed, 16 insertions(+), 11 deletions(-) rename static/app/views/explore/logs/tables/{useExpando.spec.tsx => useTableExpando.spec.tsx} (86%) rename static/app/views/explore/logs/tables/{useExpando.tsx => useTableExpando.tsx} (85%) diff --git a/static/app/views/explore/logs/content.tsx b/static/app/views/explore/logs/content.tsx index b537601c21bc35..aebfbcaa6e7cee 100644 --- a/static/app/views/explore/logs/content.tsx +++ b/static/app/views/explore/logs/content.tsx @@ -25,7 +25,7 @@ import {useGetSavedQuery} from 'sentry/views/explore/hooks/useGetSavedQueries'; import {LogsTabOnboarding} from 'sentry/views/explore/logs/logsOnboarding'; import {LogsQueryParamsProvider} from 'sentry/views/explore/logs/logsQueryParamsProvider'; import {LogsTabContent} from 'sentry/views/explore/logs/logsTab'; -import {useExpando} from 'sentry/views/explore/logs/tables/useExpando'; +import {useLogsTableExpandoFeatureFlag} from 'sentry/views/explore/logs/tables/useTableExpando'; import { useQueryParamsId, useQueryParamsTitle, @@ -35,7 +35,7 @@ import {useOnboardingProject} from 'sentry/views/insights/common/queries/useOnbo export default function LogsContent() { const organization = useOrganization(); - const {enabled: hasExpando} = useExpando(); + const hasExpando = useLogsTableExpandoFeatureFlag(); const maxPickableDays = useMaxPickableDays({ dataCategories: [DataCategory.LOG_BYTE], }); diff --git a/static/app/views/explore/logs/logsTab.tsx b/static/app/views/explore/logs/logsTab.tsx index f025e354a65768..51dd7e4c5d6911 100644 --- a/static/app/views/explore/logs/logsTab.tsx +++ b/static/app/views/explore/logs/logsTab.tsx @@ -96,7 +96,7 @@ import {useRawCounts} from 'sentry/views/explore/useRawCounts'; // eslint-disable-next-line boundaries/dependencies import QuotaExceededAlert from 'getsentry/components/performance/quotaExceededAlert'; -import {useExpando} from './tables/useExpando'; +import {useTableExpando} from './tables/useTableExpando'; type LogsTabProps = { datePageFilterProps: DatePageFilterProps; @@ -425,7 +425,7 @@ export function LogsTabContent({datePageFilterProps}: LogsTabProps) { }, [pageFilters.selection.datetime]); const {infiniteLogsQueryResult} = useLogsPageData(); - const expando = useExpando(); + const expando = useTableExpando(); return ( diff --git a/static/app/views/explore/logs/tables/useExpando.spec.tsx b/static/app/views/explore/logs/tables/useTableExpando.spec.tsx similarity index 86% rename from static/app/views/explore/logs/tables/useExpando.spec.tsx rename to static/app/views/explore/logs/tables/useTableExpando.spec.tsx index f533eb15aab6c0..50e420b3ee56a7 100644 --- a/static/app/views/explore/logs/tables/useExpando.spec.tsx +++ b/static/app/views/explore/logs/tables/useTableExpando.spec.tsx @@ -1,13 +1,13 @@ import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary'; -import {useExpando} from './useExpando'; +import {useTableExpando} from './useTableExpando'; function ExpandoWrapper() { - const {button} = useExpando(); + const {button} = useTableExpando(); return
{button}
; } -describe('useExpando', () => { +describe('useTableExpando', () => { it('is contracted when the button is not yet clicked', () => { render(); diff --git a/static/app/views/explore/logs/tables/useExpando.tsx b/static/app/views/explore/logs/tables/useTableExpando.tsx similarity index 85% rename from static/app/views/explore/logs/tables/useExpando.tsx rename to static/app/views/explore/logs/tables/useTableExpando.tsx index 85cd2dba67beba..28d3c9914f9437 100644 --- a/static/app/views/explore/logs/tables/useExpando.tsx +++ b/static/app/views/explore/logs/tables/useTableExpando.tsx @@ -9,19 +9,24 @@ import {useLocation} from 'sentry/utils/useLocation'; import {useOrganization} from 'sentry/utils/useOrganization'; import {TableActionButton} from 'sentry/views/explore/components/tableActionButton'; -interface Expando { +interface TableExpando { button: React.ReactNode; enabled: boolean; expanded: boolean | undefined; } -export function useExpando(): Expando { +export function useLogsTableExpandoFeatureFlag() { const organization = useOrganization(); const location = useLocation(); - const enabled = + + return ( organization.features.includes('ourlogs-table-expando') || - location.query.logsTableExpando === 'true'; + location.query.logsTableExpando === 'true' + ); +} +export function useTableExpando(): TableExpando { + const enabled = useLogsTableExpandoFeatureFlag(); const [expandedState, setExpandedState] = useState(false); const [Icon, text] = expandedState From 71c5702a3ff2d0b6209783cdab20d698cbb9cb13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josh=20Goldberg=20=E2=9C=A8?= Date: Thu, 2 Apr 2026 11:20:46 -0400 Subject: [PATCH 35/38] fix: bump up those overscan numbers --- static/app/views/explore/logs/tables/logsInfiniteTable.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/app/views/explore/logs/tables/logsInfiniteTable.tsx b/static/app/views/explore/logs/tables/logsInfiniteTable.tsx index fca7ce0b2e130b..3633d2961d9da8 100644 --- a/static/app/views/explore/logs/tables/logsInfiniteTable.tsx +++ b/static/app/views/explore/logs/tables/logsInfiniteTable.tsx @@ -276,7 +276,7 @@ export function LogsInfiniteTable({ const containerVirtualizer = useVirtualizer({ count: isContainedVirtualizer ? (data?.length ?? 0) : 0, estimateSize, - overscan: expanded ? 25 : 10, + overscan: expanded ? 50 : 25, getScrollElement: () => tableBodyRef?.current, getItemKey: (index: number) => data?.[index]?.[OurLogKnownFieldKey.ID] ?? index, }); From ffdb3d5387c27ac77930d2d4c3b67c5577e465a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josh=20Goldberg=20=E2=9C=A8?= Date: Thu, 2 Apr 2026 14:00:24 -0400 Subject: [PATCH 36/38] Add flex-based height constraints and min in traceOurlogs --- .../views/performance/newTraceDetails/traceOurlogs.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/static/app/views/performance/newTraceDetails/traceOurlogs.tsx b/static/app/views/performance/newTraceDetails/traceOurlogs.tsx index d86991d1e1c199..6f0a5c9c6c78db 100644 --- a/static/app/views/performance/newTraceDetails/traceOurlogs.tsx +++ b/static/app/views/performance/newTraceDetails/traceOurlogs.tsx @@ -85,10 +85,18 @@ function LogsSectionContent() { const TableContainer = styled('div')` margin-top: ${p => p.theme.space.xl}; + display: flex; + flex-direction: column; + flex: 1; + min-height: 0; `; const StyledPanel = styled(Panel)` padding: ${p => p.theme.space.xl}; padding-bottom: 0; margin: 0; + display: flex; + flex-direction: column; + flex: 1; + min-height: 0; `; From 034843bf00db9cac3470f7ad618bdbf593c8140c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josh=20Goldberg=20=E2=9C=A8?= Date: Thu, 2 Apr 2026 14:28:06 -0400 Subject: [PATCH 37/38] Good news: no more manual left needed on FloatingBackToTopContainer --- static/app/views/explore/logs/styles.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/static/app/views/explore/logs/styles.tsx b/static/app/views/explore/logs/styles.tsx index f981724be5c0cd..d6b05ecb5fe43c 100644 --- a/static/app/views/explore/logs/styles.tsx +++ b/static/app/views/explore/logs/styles.tsx @@ -462,7 +462,6 @@ export const FloatingBackToTopContainer = styled('div')<{ z-index: 1; opacity: ${p => (p.inReplay ? 1 : 0.9)}; top: ${p => (p.inReplay ? p.theme.space.md : '65px')}; - left: calc(50% - var(--floatingWidth) / 2); width: var(--floatingWidth); display: flex; justify-content: center; From 9bd2a9ad8b356958068512b6cd31987d9c7c314d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josh=20Goldberg=20=E2=9C=A8?= Date: Tue, 7 Apr 2026 11:33:37 -0400 Subject: [PATCH 38/38] feat: also hide footer when expanded --- .../components/viewportConstrainedPage.tsx | 23 +++++++++++++++---- static/app/views/explore/logs/content.tsx | 14 +++++++---- .../app/views/explore/logs/logsTab.spec.tsx | 23 +++++++++++++++---- static/app/views/explore/logs/logsTab.tsx | 14 +++++------ .../explore/logs/tables/useTableExpando.tsx | 14 ++++------- 5 files changed, 58 insertions(+), 30 deletions(-) diff --git a/static/app/views/explore/components/viewportConstrainedPage.tsx b/static/app/views/explore/components/viewportConstrainedPage.tsx index f7dca6fd29e891..10fe34b6c3cbbd 100644 --- a/static/app/views/explore/components/viewportConstrainedPage.tsx +++ b/static/app/views/explore/components/viewportConstrainedPage.tsx @@ -7,6 +7,7 @@ import {SHORT_VIEWPORT_HEIGHT} from 'sentry/utils/useIsShortViewport'; interface ViewportConstrainedPageProps extends FlexProps<'main'> { constrained?: boolean; + hideFooter?: boolean; } /** @@ -17,18 +18,26 @@ interface ViewportConstrainedPageProps extends FlexProps<'main'> { * (TopBar, Footer, etc.), and content within must manage its own * overflow (e.g. via scrollable table bodies). * - * When constrained, the footer is also hidden at smaller viewport heights. - * Similar to mobile, this is to leave more height space for essential UI. + * When constrained, the global footer sibling is hidden on smaller + * viewport heights and when `hideFooter` is set. */ export function ViewportConstrainedPage({ constrained = true, - ...props + hideFooter, + ...rest }: ViewportConstrainedPageProps) { if (!constrained) { - return ; + return ; } - return ; + return ( + + ); } const ConstrainedPage = styled(Layout.Page)` @@ -39,4 +48,8 @@ const ConstrainedPage = styled(Layout.Page)` display: none; } } + + &[data-hide-footer] ~ footer { + display: none; + } `; diff --git a/static/app/views/explore/logs/content.tsx b/static/app/views/explore/logs/content.tsx index f4958a434a4106..4ebebdfa5eff23 100644 --- a/static/app/views/explore/logs/content.tsx +++ b/static/app/views/explore/logs/content.tsx @@ -26,7 +26,7 @@ import {useGetSavedQuery} from 'sentry/views/explore/hooks/useGetSavedQueries'; import {LogsTabOnboarding} from 'sentry/views/explore/logs/logsOnboarding'; import {LogsQueryParamsProvider} from 'sentry/views/explore/logs/logsQueryParamsProvider'; import {LogsTabContent} from 'sentry/views/explore/logs/logsTab'; -import {useLogsTableExpandoFeatureFlag} from 'sentry/views/explore/logs/tables/useTableExpando'; +import {useTableExpando} from 'sentry/views/explore/logs/tables/useTableExpando'; import { useQueryParamsId, useQueryParamsTitle, @@ -36,7 +36,7 @@ import {useOnboardingProject} from 'sentry/views/insights/common/queries/useOnbo export default function LogsContent() { const organization = useOrganization(); - const hasExpando = useLogsTableExpandoFeatureFlag(); + const tableExpando = useTableExpando(); const maxPickableDays = useMaxPickableDays({ dataCategories: [DataCategory.LOG_BYTE], }); @@ -66,7 +66,10 @@ export default function LogsContent() { analyticsPageSource={LogsAnalyticsPageSource.EXPLORE_LOGS} source="location" > - + {defined(onboardingProject) ? ( @@ -76,7 +79,10 @@ export default function LogsContent() { datePageFilterProps={datePageFilterProps} /> ) : ( - + )} diff --git a/static/app/views/explore/logs/logsTab.spec.tsx b/static/app/views/explore/logs/logsTab.spec.tsx index 54c6c313ec54e2..0a86d104317db7 100644 --- a/static/app/views/explore/logs/logsTab.spec.tsx +++ b/static/app/views/explore/logs/logsTab.spec.tsx @@ -16,8 +16,23 @@ import {LOGS_SORT_BYS_KEY} from 'sentry/views/explore/contexts/logs/sortBys'; import {AlwaysPresentLogFields} from 'sentry/views/explore/logs/constants'; import {LogsQueryParamsProvider} from 'sentry/views/explore/logs/logsQueryParamsProvider'; import {LogsTabContent} from 'sentry/views/explore/logs/logsTab'; +import {useTableExpando} from 'sentry/views/explore/logs/tables/useTableExpando'; import {OurLogKnownFieldKey} from 'sentry/views/explore/logs/types'; +function LogsTabContentHarness({ + datePageFilterProps, +}: { + datePageFilterProps: DatePageFilterProps; +}) { + const tableExpando = useTableExpando(); + return ( + + ); +} + const datePageFilterProps: DatePageFilterProps = { defaultPeriod: '7d' as const, maxPickableDays: 7, @@ -178,7 +193,7 @@ describe('LogsTabContent', () => { it('should call APIs as expected', async () => { render( - + , {initialRouterConfig, organization} ); @@ -230,7 +245,7 @@ describe('LogsTabContent', () => { it('should switch between modes', async () => { render( - + , {initialRouterConfig, organization} ); @@ -274,7 +289,7 @@ describe('LogsTabContent', () => { it('should pass caseInsensitive to the query', async () => { render( - + , {initialRouterConfig, organization} ); @@ -331,7 +346,7 @@ describe('LogsTabContent', () => { autorefreshEnabledRouterConfig.location.query[LOGS_AUTO_REFRESH_KEY] = 'enabled'; render( - + , { initialRouterConfig: autorefreshEnabledRouterConfig, diff --git a/static/app/views/explore/logs/logsTab.tsx b/static/app/views/explore/logs/logsTab.tsx index 51dd7e4c5d6911..a2c10489ad57c0 100644 --- a/static/app/views/explore/logs/logsTab.tsx +++ b/static/app/views/explore/logs/logsTab.tsx @@ -96,10 +96,11 @@ import {useRawCounts} from 'sentry/views/explore/useRawCounts'; // eslint-disable-next-line boundaries/dependencies import QuotaExceededAlert from 'getsentry/components/performance/quotaExceededAlert'; -import {useTableExpando} from './tables/useTableExpando'; +import type {TableExpando} from './tables/useTableExpando'; type LogsTabProps = { datePageFilterProps: DatePageFilterProps; + tableExpando: TableExpando; }; interface LogsSearchBarProps { @@ -240,7 +241,7 @@ const LogsSearchSection = memo(function LogsSearchSection({ ); }); -export function LogsTabContent({datePageFilterProps}: LogsTabProps) { +export function LogsTabContent({datePageFilterProps, tableExpando}: LogsTabProps) { const pageFilters = usePageFilters(); const fields = useQueryParamsFields(); const mode = useQueryParamsMode(); @@ -425,7 +426,6 @@ export function LogsTabContent({datePageFilterProps}: LogsTabProps) { }, [pageFilters.selection.datetime]); const {infiniteLogsQueryResult} = useLogsPageData(); - const expando = useTableExpando(); return ( @@ -438,7 +438,7 @@ export function LogsTabContent({datePageFilterProps}: LogsTabProps) { {sidebarOpen ? : null} - {!expando.expanded && ( + {!tableExpando.expanded && ( - {!expando.expanded && ( + {!tableExpando.expanded && ( } /> - {expando.enabled && expando.button} + {tableExpando.enabled && tableExpando.button} )} {tableTab === 'logs' ? (