From ca437ca6f0bebe4f3ac3bc0169cd1ce994577ed6 Mon Sep 17 00:00:00 2001 From: John Lacuna Date: Fri, 30 Jan 2026 16:10:51 -0500 Subject: [PATCH 1/7] feat: improve Timepoint Explorer selection experience --- .../TimepointExplorer.constants.ts | 5 + .../TimepointExplorer.context.tsx | 23 +++-- .../TimepointExplorer.hooks.ts | 44 --------- .../TimepointExplorer/TimepointListEntry.tsx | 4 +- .../TimepointListEntryBar.tsx | 28 ++++-- .../TimepointListEntryReachability.tsx | 17 +++- .../TimepointExplorer/TimepointViewer.tsx | 30 ++++-- .../__tests__/TimepointExplorer.mocks.ts | 15 ++- .../__tests__/TimepointExplorer.test.tsx | 93 +++++++++++++++++++ src/test/dataTestIds.ts | 2 + 10 files changed, 181 insertions(+), 80 deletions(-) diff --git a/src/scenes/components/TimepointExplorer/TimepointExplorer.constants.ts b/src/scenes/components/TimepointExplorer/TimepointExplorer.constants.ts index 23b7d611b..a8650e1e3 100644 --- a/src/scenes/components/TimepointExplorer/TimepointExplorer.constants.ts +++ b/src/scenes/components/TimepointExplorer/TimepointExplorer.constants.ts @@ -39,3 +39,8 @@ export const ANNOTATION_COLOR_NO_DATA = `orange`; export const ANNOTATION_COLOR_CHECK_CREATED = `yellow`; export const ANNOTATION_COLOR_CHECK_UPDATED = `blue`; export const ANNOTATION_COLOR_ALERTS_FIRING = `red`; + +// Selection styling constants +export const NON_SELECTED_BAR_OPACITY = 0.7; +export const SELECTED_BAR_BORDER_WIDTH = 3; +export const BAR_BORDER_WIDTH = 2; diff --git a/src/scenes/components/TimepointExplorer/TimepointExplorer.context.tsx b/src/scenes/components/TimepointExplorer/TimepointExplorer.context.tsx index baf40d8c1..ea8f04218 100644 --- a/src/scenes/components/TimepointExplorer/TimepointExplorer.context.tsx +++ b/src/scenes/components/TimepointExplorer/TimepointExplorer.context.tsx @@ -27,7 +27,6 @@ import { useBuiltCheckConfigs, useCurrentAdjustedTime, useExecutionDurationLogs, - useIsInitialised, useIsListResultPending, usePersistedMaxProbeDuration, useSceneAnnotationEvents, @@ -73,6 +72,7 @@ interface TimepointExplorerContextType { handleMiniMapPageChange: (page: number) => void; handleMiniMapSectionChange: (sectionIndex: number) => void; handleRefetch: () => void; + handleSetScrollToViewer: (shouldScroll: boolean) => void; handleTimepointWidthChange: (timepointWidth: number, currentSectionRange: MiniMapSection) => void; handleViewerStateChange: (state: ViewerState) => void; handleViewModeChange: (viewMode: ViewMode) => void; @@ -82,7 +82,6 @@ interface TimepointExplorerContextType { isCheckCreationWithinTimeRange: boolean; isError: boolean; isFetching: boolean; - isInitialised: boolean; isLoading: boolean; isLogsRetentionPeriodWithinTimerange: boolean; listLogsMap: Record; @@ -97,6 +96,7 @@ interface TimepointExplorerContextType { timepointWidth: number; viewerState: ViewerState; viewMode: ViewMode; + shouldScrollToViewer: boolean; vizDisplay: VizDisplay; vizOptions: Record; yAxisMax: number; @@ -226,6 +226,7 @@ export const TimepointExplorerProvider = ({ children, check }: TimepointExplorer const isError = isCheckConfigsError || isExecutionDurationLogsError || isMaxProbeDurationError; const [viewerState, setViewerState] = useState([]); + const [shouldScrollToViewer, setShouldScrollToViewer] = useState(false); const handleMiniMapSectionChange = useCallback((sectionIndex: number) => { setMiniMapCurrentSectionIndex(sectionIndex); @@ -245,6 +246,10 @@ export const TimepointExplorerProvider = ({ children, check }: TimepointExplorer setViewerState(state); }, []); + const handleSetScrollToViewer = useCallback((shouldScroll: boolean) => { + setShouldScrollToViewer(shouldScroll); + }, []); + const handleTimepointDisplayCountChange = useCallback( (count: number, currentSectionRange: MiniMapSection) => { const newMiniMapPages = getMiniMapPages(timepoints.length, count, MAX_MINIMAP_SECTIONS); @@ -321,14 +326,6 @@ export const TimepointExplorerProvider = ({ children, check }: TimepointExplorer listLogsMap, }); - const isInitialised = useIsInitialised({ - check, - isLoading, - handleViewerStateChange, - timepoints, - currentAdjustedTime, - }); - const renderingStrategy = getRenderingStrategy({ isLogsRetentionPeriodWithinTimerange, timepoints, @@ -358,11 +355,11 @@ export const TimepointExplorerProvider = ({ children, check }: TimepointExplorer handleViewModeChange, handleVizDisplayChange, handleVizOptionChange, + handleSetScrollToViewer, hoveredState, isCheckCreationWithinTimeRange, isError, isFetching, - isInitialised, isLoading, isLogsRetentionPeriodWithinTimerange, listLogsMap, @@ -377,6 +374,7 @@ export const TimepointExplorerProvider = ({ children, check }: TimepointExplorer timepointWidth, viewerState, viewMode, + shouldScrollToViewer, vizDisplay, vizOptions, yAxisMax, @@ -398,11 +396,11 @@ export const TimepointExplorerProvider = ({ children, check }: TimepointExplorer handleViewModeChange, handleVizDisplayChange, handleVizOptionChange, + handleSetScrollToViewer, hoveredState, isCheckCreationWithinTimeRange, isError, isFetching, - isInitialised, isLoading, isLogsRetentionPeriodWithinTimerange, listLogsMap, @@ -417,6 +415,7 @@ export const TimepointExplorerProvider = ({ children, check }: TimepointExplorer timepointWidth, viewerState, viewMode, + shouldScrollToViewer, vizDisplay, vizOptions, yAxisMax, diff --git a/src/scenes/components/TimepointExplorer/TimepointExplorer.hooks.ts b/src/scenes/components/TimepointExplorer/TimepointExplorer.hooks.ts index a63a031e5..28661fe66 100644 --- a/src/scenes/components/TimepointExplorer/TimepointExplorer.hooks.ts +++ b/src/scenes/components/TimepointExplorer/TimepointExplorer.hooks.ts @@ -37,7 +37,6 @@ import { TimepointStatus, TimepointVizOptions, UnixTimestamp, - ViewerState, } from 'scenes/components/TimepointExplorer/TimepointExplorer.types'; import { buildConfigTimeRanges, @@ -45,7 +44,6 @@ import { buildTimepoints, extractFrequenciesAndConfigs, getCouldBePending, - getIsInTheFuture, getPendingProbeNames, getTimeAdjustedTimepoint, getVisibleTimepoints, @@ -475,48 +473,6 @@ export function useCurrentAdjustedTime(check: Check) { return currentAdjustedTime; } -interface UseIsInitialisedProps { - check: Check; - currentAdjustedTime: UnixTimestamp; - handleViewerStateChange: (viewerState: ViewerState) => void; - isLoading: boolean; - timepoints: StatelessTimepoint[]; -} - -export function useIsInitialised({ - check, - isLoading, - handleViewerStateChange, - timepoints, - currentAdjustedTime, -}: UseIsInitialisedProps) { - const probeVar = useSceneVarProbes(check); - const persistedIsLoading = usePersisted(isLoading, (isLoading) => !isLoading); - - const handleOnInitialised = useCallback(() => { - const notInTheFuture = timepoints.filter((t) => !getIsInTheFuture(t, currentAdjustedTime)); - // todo: add logic to pick a sensible entry depending on how close a timepoint is to the creation date - // e.g. if you create a time point with two seconds until the end of the timepoint, the user will essentially - // see a missing result as the first entry - // might make sense to bump the creation time artificially so it aligns with the timepoint? - const lastNotInTheFuture = notInTheFuture[notInTheFuture.length - 1] || timepoints[0]; - const firstProbe = probeVar[0]; - - handleViewerStateChange([lastNotInTheFuture, firstProbe, 0]); - }, [currentAdjustedTime, probeVar, timepoints, handleViewerStateChange]); - - useEffect(() => { - // we have to wait until we have the checkConfigs and subsequent timepoints built - // before knowing what timepoints are available and what to select - if (!persistedIsLoading) { - handleOnInitialised(); - } - // eslint-disable-next-line react-hooks/exhaustive-deps -- we only want to run this once - }, [persistedIsLoading]); - - return !persistedIsLoading; -} - export function useSelectedProbeNames(statefulTimepoint: StatefulTimepoint) { const { check, checkConfigs } = useTimepointExplorerContext(); const latestConfigDate = checkConfigs[checkConfigs.length - 1].from; diff --git a/src/scenes/components/TimepointExplorer/TimepointListEntry.tsx b/src/scenes/components/TimepointExplorer/TimepointListEntry.tsx index 57c51033f..966d7ae8e 100644 --- a/src/scenes/components/TimepointExplorer/TimepointListEntry.tsx +++ b/src/scenes/components/TimepointExplorer/TimepointListEntry.tsx @@ -34,7 +34,7 @@ export const TimepointListEntry = ({ timepoint }: TimepointListEntryProps) => { }; const Entry = (props: TimepointListEntryProps) => { - const { check, currentAdjustedTime, isInitialised, isLoading, viewMode } = useTimepointExplorerContext(); + const { check, currentAdjustedTime, isLoading, viewMode } = useTimepointExplorerContext(); const statefulTimepoint = useStatefulTimepoint(props.timepoint); const isInTheFuture = getIsInTheFuture(props.timepoint, currentAdjustedTime); const selectedProbeNames = useSceneVarProbes(check); @@ -46,7 +46,7 @@ const Entry = (props: TimepointListEntryProps) => { return
; } - if (isEntryLoading || !isInitialised) { + if (isEntryLoading) { return ; } diff --git a/src/scenes/components/TimepointExplorer/TimepointListEntryBar.tsx b/src/scenes/components/TimepointExplorer/TimepointListEntryBar.tsx index 01be99531..56d921a89 100644 --- a/src/scenes/components/TimepointExplorer/TimepointListEntryBar.tsx +++ b/src/scenes/components/TimepointExplorer/TimepointListEntryBar.tsx @@ -3,9 +3,15 @@ import { GrafanaTheme2 } from '@grafana/data'; import { Icon, styleMixins, Tooltip, useStyles2 } from '@grafana/ui'; import { css, cx } from '@emotion/css'; import { TimepointDetailsClick, trackTimepointDetailsClicked } from 'features/tracking/timepointExplorerEvents'; +import { DataTestIds } from 'test/dataTestIds'; import { PlainButton } from 'components/PlainButton'; -import { TIMEPOINT_GAP_PX } from 'scenes/components/TimepointExplorer/TimepointExplorer.constants'; +import { + BAR_BORDER_WIDTH, + NON_SELECTED_BAR_OPACITY, + SELECTED_BAR_BORDER_WIDTH, + TIMEPOINT_GAP_PX, +} from 'scenes/components/TimepointExplorer/TimepointExplorer.constants'; import { useTimepointExplorerContext } from 'scenes/components/TimepointExplorer/TimepointExplorer.context'; import { useSelectedProbeNames, @@ -32,14 +38,14 @@ export const TimepointListEntryBar = ({ timepoint, }: TimepointListEntryPendingProps) => { const statefulTimepoint = useStatefulTimepoint(timepoint); - const { handleViewerStateChange, yAxisMax, viewerState, timepointWidth, vizDisplay } = useTimepointExplorerContext(); + const { handleViewerStateChange, handleSetScrollToViewer, yAxisMax, viewerState, timepointWidth, vizDisplay } = useTimepointExplorerContext(); const selectedProbeNames = useSelectedProbeNames(statefulTimepoint); const height = getEntryHeight(statefulTimepoint.maxProbeDuration, yAxisMax); - const styles = useStyles2(getStyles, timepointWidth, height); const probeNameToView = selectedProbeNames.sort((a, b) => a.localeCompare(b))[0]; const [viewerTimepoint] = viewerState; const isSelected = viewerTimepoint?.adjustedTime === timepoint.adjustedTime; + const styles = useStyles2(getStyles, timepointWidth, height, isSelected, !!viewerTimepoint); const ref = useRef(null); const handleViewerStateClick = useCallback(() => { @@ -47,8 +53,9 @@ export const TimepointListEntryBar = ({ component: analyticsEventName, status, }); + handleSetScrollToViewer(true); handleViewerStateChange([timepoint, probeNameToView, 0]); - }, [analyticsEventName, status, timepoint, probeNameToView, handleViewerStateChange]); + }, [analyticsEventName, status, timepoint, probeNameToView, handleViewerStateChange, handleSetScrollToViewer]); if (!vizDisplay.includes(status)) { return
; @@ -62,7 +69,7 @@ export const TimepointListEntryBar = ({
)} } ref={ref} interactive placement="top"> - + { +const getStyles = ( + theme: GrafanaTheme2, + timepointWidth: number, + height: number, + isSelected: boolean, + hasSelection: boolean, +) => { return { container: css` height: ${height}%; display: flex; flex-direction: column; align-items: center; + opacity: ${hasSelection && !isSelected ? NON_SELECTED_BAR_OPACITY : 1}; `, button: css` width: calc(${timepointWidth}px + ${TIMEPOINT_GAP_PX}px); @@ -131,7 +145,7 @@ const getStyles = (theme: GrafanaTheme2, timepointWidth: number, height: number) } `, selected: css` - border-width: 2px; + border-width: ${isSelected ? SELECTED_BAR_BORDER_WIDTH : BAR_BORDER_WIDTH}px; z-index: 1; `, selectedIcon: css` diff --git a/src/scenes/components/TimepointExplorer/TimepointListEntryReachability.tsx b/src/scenes/components/TimepointExplorer/TimepointListEntryReachability.tsx index ba1f0914e..48931211e 100644 --- a/src/scenes/components/TimepointExplorer/TimepointListEntryReachability.tsx +++ b/src/scenes/components/TimepointExplorer/TimepointListEntryReachability.tsx @@ -7,6 +7,9 @@ import { trackTimepointDetailsClicked } from 'features/tracking/timepointExplore import { LokiFieldNames } from 'features/parseLokiLogs/parseLokiLogs.types'; import { PlainButton } from 'components/PlainButton'; import { + BAR_BORDER_WIDTH, + NON_SELECTED_BAR_OPACITY, + SELECTED_BAR_BORDER_WIDTH, TIMEPOINT_GAP_PX, TIMEPOINT_THEME_HEIGHT_PX, } from 'scenes/components/TimepointExplorer/TimepointExplorer.constants'; @@ -29,6 +32,7 @@ const ICON_MAP: Record = { export const TimepointListEntryReachability = ({ timepoint }: TimepointListEntryProps) => { const { handleHoverStateChange, + handleSetScrollToViewer, handleViewerStateChange, hoveredState, timepointWidth, @@ -37,9 +41,12 @@ export const TimepointListEntryReachability = ({ timepoint }: TimepointListEntry yAxisMax, } = useTimepointExplorerContext(); const statefulTimepoint = useStatefulTimepoint(timepoint); - const styles = useStyles2(getStyles, timepointWidth); const entryHeight = getEntryHeight(statefulTimepoint.maxProbeDuration, yAxisMax); const [hoveredTimepoint, hoveredProbeName, hoveredExecutionIndex] = hoveredState || []; + const [viewerTimepoint] = viewerState; + const hasSelection = !!viewerTimepoint; + const isTimepointSelected = viewerTimepoint?.adjustedTime === timepoint.adjustedTime; + const styles = useStyles2(getStyles, timepointWidth, hasSelection, isTimepointSelected); // add the timepoint size to the height so the entries are rendered in the middle of the Y Axis line const height = `calc(${entryHeight}% + ${timepointWidth}px)`; @@ -59,9 +66,10 @@ export const TimepointListEntryReachability = ({ timepoint }: TimepointListEntry component: 'reachability-entry', status: statefulTimepoint.status, }); + handleSetScrollToViewer(true); handleViewerStateChange([statefulTimepoint, probeName, index]); }, - [statefulTimepoint, handleViewerStateChange] + [statefulTimepoint, handleViewerStateChange, handleSetScrollToViewer] ); if (!executionsToRender.length) { @@ -121,7 +129,7 @@ export const TimepointListEntryReachability = ({ timepoint }: TimepointListEntry ); }; -const getStyles = (theme: GrafanaTheme2, timepointWidth: number) => { +const getStyles = (theme: GrafanaTheme2, timepointWidth: number, hasSelection: boolean, isTimepointSelected: boolean) => { return { timepoint: css` display: flex; @@ -140,6 +148,7 @@ const getStyles = (theme: GrafanaTheme2, timepointWidth: number) => { position: relative; left: 50%; transform: translateX(-50%); + opacity: ${hasSelection && !isTimepointSelected ? NON_SELECTED_BAR_OPACITY : 1}; `, executionContainer: css` position: absolute; @@ -170,7 +179,7 @@ const getStyles = (theme: GrafanaTheme2, timepointWidth: number) => { } `, viewed: css` - border-width: 2px; + border-width: ${isTimepointSelected ? SELECTED_BAR_BORDER_WIDTH : BAR_BORDER_WIDTH}px; z-index: 1; `, hovered: css` diff --git a/src/scenes/components/TimepointExplorer/TimepointViewer.tsx b/src/scenes/components/TimepointExplorer/TimepointViewer.tsx index bcaf65a27..b3e9e1b96 100644 --- a/src/scenes/components/TimepointExplorer/TimepointViewer.tsx +++ b/src/scenes/components/TimepointExplorer/TimepointViewer.tsx @@ -1,9 +1,10 @@ -import React, { useCallback, useRef, useState } from 'react'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; import { dateTimeFormat, GrafanaTheme2 } from '@grafana/data'; import { Box, LoadingBar, Stack, Text, useStyles2 } from '@grafana/ui'; import { css } from '@emotion/css'; import { trackTimepointViewerLogsViewToggled } from 'features/tracking/timepointExplorerEvents'; import { useResizeObserver } from 'usehooks-ts'; +import { DataTestIds } from 'test/dataTestIds'; import { formatDuration } from 'utils'; import { QueryErrorBoundary } from 'components/QueryErrorBoundary'; @@ -19,10 +20,21 @@ import { TimepointViewerActions } from 'scenes/components/TimepointExplorer/Time import { TimepointViewerExecutions } from 'scenes/components/TimepointExplorer/TimepointViewerExecutions'; export const TimepointViewer = () => { - const { isInitialised, viewerState } = useTimepointExplorerContext(); + const { viewerState, shouldScrollToViewer, handleSetScrollToViewer } = useTimepointExplorerContext(); const [logsView, setLogsView] = useState(LOGS_VIEW_OPTIONS[0].value); - const [viewerTimepoint, viewerProbeName] = viewerState; + const [viewerTimepoint, viewerProbeName] = viewerState || []; const styles = useStyles2(getStyles); + const containerRef = useRef(null); + + useEffect(() => { + if (shouldScrollToViewer && viewerTimepoint && containerRef.current) { + containerRef.current.scrollIntoView({ + behavior: 'smooth', + block: 'start', + }); + handleSetScrollToViewer(false); + } + }, [shouldScrollToViewer, viewerTimepoint, handleSetScrollToViewer]); const handleChangeLogsView = useCallback((view: LogsView) => { trackTimepointViewerLogsViewToggled({ @@ -32,7 +44,7 @@ export const TimepointViewer = () => { }, []); return ( -
+
{viewerTimepoint ? (
@@ -48,8 +60,7 @@ export const TimepointViewer = () => {
) : ( - {isInitialised ? 'No timepoint selected' : 'Loading...'} - {isInitialised && Select a timepoint to view logs.} + Click on a data point above to view detailed logs. )}
@@ -81,9 +92,9 @@ const TimepointViewerContent = ({ logsView, probeNameToView, timepoint }: Timepo const pendingProbeNames = couldResultBePending ? getPendingProbes({ - entryProbeNames: data.filter((d) => d.executions.length).map((d) => d.probeName), - selectedProbeNames: probeVar, - }) + entryProbeNames: data.filter((d) => d.executions.length).map((d) => d.probeName), + selectedProbeNames: probeVar, + }) : []; const enableRefetch = !!pendingProbeNames.length; @@ -154,6 +165,7 @@ const getStyles = (theme: GrafanaTheme2) => ({ display: flex; flex-direction: column; gap: ${theme.spacing(2)}; + scroll-margin-top: 25vh; `, loadingBarContainer: css` position: absolute; diff --git a/src/scenes/components/TimepointExplorer/__tests__/TimepointExplorer.mocks.ts b/src/scenes/components/TimepointExplorer/__tests__/TimepointExplorer.mocks.ts index 185e0de98..71e5cd3e3 100644 --- a/src/scenes/components/TimepointExplorer/__tests__/TimepointExplorer.mocks.ts +++ b/src/scenes/components/TimepointExplorer/__tests__/TimepointExplorer.mocks.ts @@ -1,9 +1,20 @@ jest.mock('@grafana/scenes-react', () => { const actual = jest.requireActual('@grafana/scenes-react'); + const fromDate = new Date('2024-01-01T00:00:00Z'); + const toDate = new Date('2024-01-02T00:00:00Z'); + let mockTimeRange = { - from: new Date('2024-01-01T00:00:00Z'), - to: new Date('2024-01-02T00:00:00Z'), + from: { + ...fromDate, + toDate: () => fromDate, + valueOf: () => fromDate.valueOf(), + }, + to: { + ...toDate, + toDate: () => toDate, + valueOf: () => toDate.valueOf(), + }, raw: { from: 'now-1d', to: 'now' }, }; diff --git a/src/scenes/components/TimepointExplorer/__tests__/TimepointExplorer.test.tsx b/src/scenes/components/TimepointExplorer/__tests__/TimepointExplorer.test.tsx index 5f0301195..01d540b38 100644 --- a/src/scenes/components/TimepointExplorer/__tests__/TimepointExplorer.test.tsx +++ b/src/scenes/components/TimepointExplorer/__tests__/TimepointExplorer.test.tsx @@ -10,21 +10,114 @@ import { TimepointExplorer } from 'scenes/components/TimepointExplorer/Timepoint import { DataTestIds } from 'test/dataTestIds'; import { mockFeatureToggles } from 'test/utils'; import { FeatureName } from 'types'; +import { StatelessTimepoint } from 'scenes/components/TimepointExplorer/TimepointExplorer.types'; + +const baseTime = new Date('2024-01-01T12:00:00Z').getTime(); +const mockTimepoints: StatelessTimepoint[] = [ + { + adjustedTime: baseTime - 300000, // 5 minutes before base time + timepointDuration: 60000, + index: 0, + config: { + frequency: 60000, + from: baseTime - 400000, + to: baseTime - 300000, + type: undefined, + }, + }, + { + adjustedTime: baseTime - 240000, // 4 minutes before base time + timepointDuration: 60000, + index: 1, + config: { + frequency: 60000, + from: baseTime - 300000, + to: baseTime - 240000, + type: undefined, + }, + }, + { + adjustedTime: baseTime - 180000, // 3 minutes before base time + timepointDuration: 60000, + index: 2, + config: { + frequency: 60000, + from: baseTime - 240000, + to: baseTime - 180000, + type: undefined, + }, + }, +]; + +jest.mock('scenes/components/TimepointExplorer/TimepointExplorer.hooks', () => ({ + ...jest.requireActual('scenes/components/TimepointExplorer/TimepointExplorer.hooks'), + useTimepoints: jest.fn(() => mockTimepoints), +})); + +jest.mock('scenes/components/TimepointExplorer/TimepointViewer.hooks', () => ({ + useTimepointLogs: jest.fn(() => ({ + data: [], + isFetching: false, + isLoading: false, + refetch: jest.fn(), + })), +})); + +const mockScrollIntoView = jest.fn(); +Element.prototype.scrollIntoView = mockScrollIntoView; function renderTimepointExplorer() { return ; } describe('TimepointExplorer', () => { + beforeEach(() => { + mockScrollIntoView.mockClear(); + }); + it(`should not render if the feature flag is off`, async () => { render(renderTimepointExplorer()); await waitFor(() => screen.queryByTestId(DataTestIds.TimepointList)); expect(screen.queryByTestId(DataTestIds.TimepointList)).not.toBeInTheDocument(); + expect(screen.queryByTestId(DataTestIds.TimepointViewer)).not.toBeInTheDocument(); }); it('should render if the feature flag is on', async () => { mockFeatureToggles({ [FeatureName.TimepointExplorer]: true }); render(renderTimepointExplorer()); await waitFor(() => screen.findByTestId(DataTestIds.TimepointList)); + await waitFor(() => screen.findByTestId(DataTestIds.TimepointViewer)); + }); + + it(`should not show empty state message after a timepoint is selected`, async () => { + mockFeatureToggles({ [FeatureName.TimepointExplorer]: true }); + const { user } = render(renderTimepointExplorer()); + + await waitFor(() => screen.queryByTestId(DataTestIds.TimepointList)); + await waitFor(() => screen.findByTestId(DataTestIds.TimepointViewer)); + + expect(screen.getByText('Click on a data point above to view detailed logs.')).toBeInTheDocument(); + + const timepointButton = await waitFor(() => screen.findByTestId(`${DataTestIds.TimepointListEntryBar}-${mockTimepoints[2].index}`)); + await user.click(timepointButton); + + expect(screen.queryByText('Click on a data point above to view detailed logs.')).not.toBeInTheDocument(); + }); + + it(`should call scrollIntoView when a timepoint with data is clicked`, async () => { + mockFeatureToggles({ [FeatureName.TimepointExplorer]: true }); + const { user } = render(renderTimepointExplorer()); + + expect(mockScrollIntoView).not.toHaveBeenCalled(); + + const timepointButton = await waitFor(() => screen.findByTestId(`${DataTestIds.TimepointListEntryBar}-${mockTimepoints[2].index}`)); + await user.click(timepointButton); + + expect(mockScrollIntoView).toHaveBeenCalledWith({ + behavior: 'smooth', + block: 'start', + }); + }); + }); diff --git a/src/test/dataTestIds.ts b/src/test/dataTestIds.ts index 02b347566..87a3822b6 100644 --- a/src/test/dataTestIds.ts +++ b/src/test/dataTestIds.ts @@ -157,4 +157,6 @@ export enum DataTestIds { ThresholdSave = 'threshold-save', ThresholdUpperLimit = 'threshold-upper-limit', TimepointList = 'timepoint-list', + TimepointListEntryBar = 'timepoint-list-entry-bar', + TimepointViewer = 'timepoint-viewer', } From 56f169cde28bc9debfe9edd0bd72f4c6f94addf7 Mon Sep 17 00:00:00 2001 From: John Lacuna Date: Mon, 2 Feb 2026 09:40:31 -0500 Subject: [PATCH 2/7] chore: clean up tests --- .../__tests__/TimepointExplorer.mocks.ts | 17 ++++------------- .../__tests__/TimepointExplorer.test.tsx | 4 ---- 2 files changed, 4 insertions(+), 17 deletions(-) diff --git a/src/scenes/components/TimepointExplorer/__tests__/TimepointExplorer.mocks.ts b/src/scenes/components/TimepointExplorer/__tests__/TimepointExplorer.mocks.ts index 71e5cd3e3..4f135996b 100644 --- a/src/scenes/components/TimepointExplorer/__tests__/TimepointExplorer.mocks.ts +++ b/src/scenes/components/TimepointExplorer/__tests__/TimepointExplorer.mocks.ts @@ -1,20 +1,11 @@ +import { dateTime } from '@grafana/data'; + jest.mock('@grafana/scenes-react', () => { const actual = jest.requireActual('@grafana/scenes-react'); - const fromDate = new Date('2024-01-01T00:00:00Z'); - const toDate = new Date('2024-01-02T00:00:00Z'); - let mockTimeRange = { - from: { - ...fromDate, - toDate: () => fromDate, - valueOf: () => fromDate.valueOf(), - }, - to: { - ...toDate, - toDate: () => toDate, - valueOf: () => toDate.valueOf(), - }, + from: dateTime('2024-01-01T00:00:00Z'), + to: dateTime('2024-01-02T00:00:00Z'), raw: { from: 'now-1d', to: 'now' }, }; diff --git a/src/scenes/components/TimepointExplorer/__tests__/TimepointExplorer.test.tsx b/src/scenes/components/TimepointExplorer/__tests__/TimepointExplorer.test.tsx index 01d540b38..a3f9b12bd 100644 --- a/src/scenes/components/TimepointExplorer/__tests__/TimepointExplorer.test.tsx +++ b/src/scenes/components/TimepointExplorer/__tests__/TimepointExplorer.test.tsx @@ -71,10 +71,6 @@ function renderTimepointExplorer() { } describe('TimepointExplorer', () => { - beforeEach(() => { - mockScrollIntoView.mockClear(); - }); - it(`should not render if the feature flag is off`, async () => { render(renderTimepointExplorer()); await waitFor(() => screen.queryByTestId(DataTestIds.TimepointList)); From 0dfa6bfd2f5156eeca246e65eb8d468da3961002 Mon Sep 17 00:00:00 2001 From: John Lacuna Date: Mon, 2 Feb 2026 16:42:56 -0500 Subject: [PATCH 3/7] chore: use MSW over mocking hooks --- .../__tests__/TimepointExplorer.mocks.ts | 7 +- .../__tests__/TimepointExplorer.test.tsx | 132 ++++++++++------- .../fixtures/httpCheck/promUniqueConfigs.ts | 140 ++++++++++++++++++ 3 files changed, 225 insertions(+), 54 deletions(-) diff --git a/src/scenes/components/TimepointExplorer/__tests__/TimepointExplorer.mocks.ts b/src/scenes/components/TimepointExplorer/__tests__/TimepointExplorer.mocks.ts index 4f135996b..5ece2afff 100644 --- a/src/scenes/components/TimepointExplorer/__tests__/TimepointExplorer.mocks.ts +++ b/src/scenes/components/TimepointExplorer/__tests__/TimepointExplorer.mocks.ts @@ -3,10 +3,11 @@ import { dateTime } from '@grafana/data'; jest.mock('@grafana/scenes-react', () => { const actual = jest.requireActual('@grafana/scenes-react'); + const now = Date.now(); let mockTimeRange = { - from: dateTime('2024-01-01T00:00:00Z'), - to: dateTime('2024-01-02T00:00:00Z'), - raw: { from: 'now-1d', to: 'now' }, + from: dateTime(now - 15 * 60 * 1000), + to: dateTime(now), + raw: { from: 'now-15m', to: 'now' }, }; // Provide a default 'probe' variable so useSceneVar/useSceneVarProbes don't touch SceneContext diff --git a/src/scenes/components/TimepointExplorer/__tests__/TimepointExplorer.test.tsx b/src/scenes/components/TimepointExplorer/__tests__/TimepointExplorer.test.tsx index a3f9b12bd..d216d78ff 100644 --- a/src/scenes/components/TimepointExplorer/__tests__/TimepointExplorer.test.tsx +++ b/src/scenes/components/TimepointExplorer/__tests__/TimepointExplorer.test.tsx @@ -5,54 +5,76 @@ import React from 'react'; import { screen, waitFor } from '@testing-library/react'; import { BASIC_HTTP_CHECK } from 'test/fixtures/checks'; import { render } from 'test/render'; +import { apiRoute } from 'test/handlers'; +import { server } from 'test/server'; +import { checksLogs1 } from 'test/fixtures/httpCheck/checkLogs'; +import { + createUniqueConfigFrame, + createUniqueConfigsResponse, + createMaxProbeDurationFrame, + createMaxProbeDurationResponse, +} from 'test/fixtures/httpCheck/promUniqueConfigs'; import { TimepointExplorer } from 'scenes/components/TimepointExplorer/TimepointExplorer'; import { DataTestIds } from 'test/dataTestIds'; import { mockFeatureToggles } from 'test/utils'; import { FeatureName } from 'types'; -import { StatelessTimepoint } from 'scenes/components/TimepointExplorer/TimepointExplorer.types'; - -const baseTime = new Date('2024-01-01T12:00:00Z').getTime(); -const mockTimepoints: StatelessTimepoint[] = [ - { - adjustedTime: baseTime - 300000, // 5 minutes before base time - timepointDuration: 60000, - index: 0, - config: { - frequency: 60000, - from: baseTime - 400000, - to: baseTime - 300000, - type: undefined, - }, - }, - { - adjustedTime: baseTime - 240000, // 4 minutes before base time - timepointDuration: 60000, - index: 1, - config: { - frequency: 60000, - from: baseTime - 300000, - to: baseTime - 240000, - type: undefined, - }, - }, - { - adjustedTime: baseTime - 180000, // 3 minutes before base time - timepointDuration: 60000, - index: 2, - config: { - frequency: 60000, - from: baseTime - 240000, - to: baseTime - 180000, - type: undefined, - }, - }, -]; - -jest.mock('scenes/components/TimepointExplorer/TimepointExplorer.hooks', () => ({ - ...jest.requireActual('scenes/components/TimepointExplorer/TimepointExplorer.hooks'), - useTimepoints: jest.fn(() => mockTimepoints), -})); +import { + REF_ID_EXECUTION_LIST_LOGS, + REF_ID_MAX_PROBE_DURATION, + REF_ID_UNIQUE_CHECK_CONFIGS, +} from 'scenes/components/TimepointExplorer/TimepointExplorer.constants'; + +const baseTime = new Date('2024-01-01T12:00:00Z').getTime(); // ✓ This already works + +// MSW handler that returns data to generate exactly 3 timepoints +function setupMSWHandlers() { + server.use( + apiRoute('getHttpDashboard', { + result: async (req) => { + const url = new URL(req.url); + const refId = url.searchParams.get('refId'); + + // Check configs - return a single config that spans our desired timepoint range + if (refId === REF_ID_UNIQUE_CHECK_CONFIGS) { + const frame = createUniqueConfigFrame({ + configVersion: String((baseTime - 360000) * 1_000_000), + frequency: '60000', + timestamps: [baseTime - 360000], + values: [1], + }); + + return { + json: createUniqueConfigsResponse([frame]), + }; + } + + // Max probe duration + if (refId === REF_ID_MAX_PROBE_DURATION) { + const frame = createMaxProbeDurationFrame({ + refId: REF_ID_MAX_PROBE_DURATION, + job: BASIC_HTTP_CHECK.job, + instance: BASIC_HTTP_CHECK.target, + probe: 'atlanta', + timestamps: [baseTime], + values: [2.5], + }); + + return { + json: createMaxProbeDurationResponse(REF_ID_MAX_PROBE_DURATION, [frame]), + }; + } + + // Execution logs + if (refId?.startsWith(REF_ID_EXECUTION_LIST_LOGS)) { + return { json: checksLogs1(refId) }; + } + + return { json: { results: {} } }; + }, + }) + ); +} jest.mock('scenes/components/TimepointExplorer/TimepointViewer.hooks', () => ({ useTimepointLogs: jest.fn(() => ({ @@ -71,6 +93,15 @@ function renderTimepointExplorer() { } describe('TimepointExplorer', () => { + beforeEach(() => { + jest.spyOn(Date, 'now').mockReturnValue(baseTime); + setupMSWHandlers(); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + it(`should not render if the feature flag is off`, async () => { render(renderTimepointExplorer()); await waitFor(() => screen.queryByTestId(DataTestIds.TimepointList)); @@ -89,16 +120,15 @@ describe('TimepointExplorer', () => { mockFeatureToggles({ [FeatureName.TimepointExplorer]: true }); const { user } = render(renderTimepointExplorer()); - await waitFor(() => screen.queryByTestId(DataTestIds.TimepointList)); - await waitFor(() => screen.findByTestId(DataTestIds.TimepointViewer)); + await screen.findByTestId(DataTestIds.TimepointList); + await screen.findByTestId(DataTestIds.TimepointViewer); expect(screen.getByText('Click on a data point above to view detailed logs.')).toBeInTheDocument(); - const timepointButton = await waitFor(() => screen.findByTestId(`${DataTestIds.TimepointListEntryBar}-${mockTimepoints[2].index}`)); - await user.click(timepointButton); + const timepointButtons = await screen.findAllByTestId(new RegExp(`${DataTestIds.TimepointListEntryBar}-`)); + await user.click(timepointButtons[0]); expect(screen.queryByText('Click on a data point above to view detailed logs.')).not.toBeInTheDocument(); - }); it(`should call scrollIntoView when a timepoint with data is clicked`, async () => { @@ -107,13 +137,13 @@ describe('TimepointExplorer', () => { expect(mockScrollIntoView).not.toHaveBeenCalled(); - const timepointButton = await waitFor(() => screen.findByTestId(`${DataTestIds.TimepointListEntryBar}-${mockTimepoints[2].index}`)); - await user.click(timepointButton); + const timepointButtons = await screen.findAllByTestId(new RegExp(`${DataTestIds.TimepointListEntryBar}-`)); + + await user.click(timepointButtons[0]); expect(mockScrollIntoView).toHaveBeenCalledWith({ behavior: 'smooth', block: 'start', }); }); - }); diff --git a/src/test/fixtures/httpCheck/promUniqueConfigs.ts b/src/test/fixtures/httpCheck/promUniqueConfigs.ts index b8d543c61..7853f4b30 100644 --- a/src/test/fixtures/httpCheck/promUniqueConfigs.ts +++ b/src/test/fixtures/httpCheck/promUniqueConfigs.ts @@ -121,3 +121,143 @@ export const promUniqueConfigs = { }, }, }; + +interface CreateUniqueConfigFrameOptions { + configVersion: string; + frequency: string; + timestamps: number[]; + values?: number[]; + interval?: number; + includeCustomMeta?: boolean; + executedQueryString?: string; +} + +export function createUniqueConfigFrame({ + configVersion, + frequency, + timestamps, + values, + interval, + includeCustomMeta = false, + executedQueryString, +}: CreateUniqueConfigFrameOptions) { + const baseFrame = promUniqueConfigs.results.uniqueCheckConfigs.frames[0]; + const baseTimeField = baseFrame.schema.fields[0]; + const baseValueField = baseFrame.schema.fields[1]; + + return { + ...baseFrame, + schema: { + ...baseFrame.schema, + meta: { + ...baseFrame.schema.meta, + ...(includeCustomMeta && baseFrame.schema.meta.custom && { custom: baseFrame.schema.meta.custom }), + ...(executedQueryString && { executedQueryString }), + }, + fields: [ + { + ...baseTimeField, + ...(interval && { + config: { + ...baseTimeField.config, + interval, + }, + }), + }, + { + ...baseValueField, + labels: { + config_version: configVersion, + frequency, + }, + config: { + displayNameFromDS: `{config_version="${configVersion}", frequency="${frequency}"}`, + }, + }, + ], + }, + data: { + values: [timestamps, values || timestamps.map(() => 1)], + }, + }; +} + +export function createUniqueConfigsResponse(frames: Array>) { + return { + results: { + uniqueCheckConfigs: { + ...promUniqueConfigs.results.uniqueCheckConfigs, + frames, + }, + }, + }; +} + +interface CreateMaxProbeDurationFrameOptions { + refId: string; + job: string; + instance: string; + probe: string; + timestamps: number[]; + values: number[]; + interval?: number; +} + +export function createMaxProbeDurationFrame({ + refId, + job, + instance, + probe, + timestamps, + values, + interval = 30000, +}: CreateMaxProbeDurationFrameOptions) { + return { + schema: { + refId, + meta: { + type: 'timeseries-multi' as const, + typeVersion: [0, 1] as [number, number], + }, + fields: [ + { + name: 'Time', + type: 'time' as const, + typeInfo: { + frame: 'time.Time', + }, + config: { + interval, + }, + }, + { + name: 'Value', + type: 'number' as const, + typeInfo: { + frame: 'float64', + }, + labels: { + job, + instance, + probe, + }, + }, + ], + }, + data: { + values: [timestamps, values], + }, + }; +} + +export function createMaxProbeDurationResponse(refId: string, frames: Array>) { + return { + results: { + [refId]: { + status: 200, + frames, + }, + }, + }; +} + From 57f7c411bd95a1d7931fa9a903c9ba7f3d665ff9 Mon Sep 17 00:00:00 2001 From: John Lacuna Date: Tue, 3 Feb 2026 10:51:31 -0500 Subject: [PATCH 4/7] fix: failing test --- .../TimepointExplorer.context.tsx | 12 +++++ .../TimepointExplorer.hooks.ts | 45 +++++++++++++++++++ .../TimepointExplorer/TimepointListEntry.tsx | 4 +- .../TimepointExplorer/TimepointViewer.tsx | 6 +-- .../__tests__/TimepointExplorer.test.tsx | 39 ++++------------ 5 files changed, 71 insertions(+), 35 deletions(-) diff --git a/src/scenes/components/TimepointExplorer/TimepointExplorer.context.tsx b/src/scenes/components/TimepointExplorer/TimepointExplorer.context.tsx index ea8f04218..2325d7bd0 100644 --- a/src/scenes/components/TimepointExplorer/TimepointExplorer.context.tsx +++ b/src/scenes/components/TimepointExplorer/TimepointExplorer.context.tsx @@ -27,6 +27,7 @@ import { useBuiltCheckConfigs, useCurrentAdjustedTime, useExecutionDurationLogs, + useIsInitialised, useIsListResultPending, usePersistedMaxProbeDuration, useSceneAnnotationEvents, @@ -82,6 +83,7 @@ interface TimepointExplorerContextType { isCheckCreationWithinTimeRange: boolean; isError: boolean; isFetching: boolean; + isInitialised: boolean; isLoading: boolean; isLogsRetentionPeriodWithinTimerange: boolean; listLogsMap: Record; @@ -326,6 +328,14 @@ export const TimepointExplorerProvider = ({ children, check }: TimepointExplorer listLogsMap, }); + const isInitialised = useIsInitialised({ + check, + isLoading, + handleViewerStateChange, + timepoints, + currentAdjustedTime, + }); + const renderingStrategy = getRenderingStrategy({ isLogsRetentionPeriodWithinTimerange, timepoints, @@ -360,6 +370,7 @@ export const TimepointExplorerProvider = ({ children, check }: TimepointExplorer isCheckCreationWithinTimeRange, isError, isFetching, + isInitialised, isLoading, isLogsRetentionPeriodWithinTimerange, listLogsMap, @@ -401,6 +412,7 @@ export const TimepointExplorerProvider = ({ children, check }: TimepointExplorer isCheckCreationWithinTimeRange, isError, isFetching, + isInitialised, isLoading, isLogsRetentionPeriodWithinTimerange, listLogsMap, diff --git a/src/scenes/components/TimepointExplorer/TimepointExplorer.hooks.ts b/src/scenes/components/TimepointExplorer/TimepointExplorer.hooks.ts index 28661fe66..74ce527ca 100644 --- a/src/scenes/components/TimepointExplorer/TimepointExplorer.hooks.ts +++ b/src/scenes/components/TimepointExplorer/TimepointExplorer.hooks.ts @@ -37,6 +37,7 @@ import { TimepointStatus, TimepointVizOptions, UnixTimestamp, + ViewerState, } from 'scenes/components/TimepointExplorer/TimepointExplorer.types'; import { buildConfigTimeRanges, @@ -44,6 +45,7 @@ import { buildTimepoints, extractFrequenciesAndConfigs, getCouldBePending, + getIsInTheFuture, getPendingProbeNames, getTimeAdjustedTimepoint, getVisibleTimepoints, @@ -473,6 +475,49 @@ export function useCurrentAdjustedTime(check: Check) { return currentAdjustedTime; } +interface UseIsInitialisedProps { + check: Check; + currentAdjustedTime: UnixTimestamp; + handleViewerStateChange: (viewerState: ViewerState) => void; + isLoading: boolean; + timepoints: StatelessTimepoint[]; +} + +export function useIsInitialised({ + check, + isLoading, + handleViewerStateChange, + timepoints, + currentAdjustedTime, +}: UseIsInitialisedProps) { + const probeVar = useSceneVarProbes(check); + const persistedIsLoading = usePersisted(isLoading, (isLoading) => !isLoading); + + const handleOnInitialised = useCallback(() => { + const notInTheFuture = timepoints.filter((t) => !getIsInTheFuture(t, currentAdjustedTime)); + // todo: add logic to pick a sensible entry depending on how close a timepoint is to the creation date + // e.g. if you create a time point with two seconds until the end of the timepoint, the user will essentially + // see a missing result as the first entry + // might make sense to bump the creation time artificially so it aligns with the timepoint? + const lastNotInTheFuture = notInTheFuture[notInTheFuture.length - 1] || timepoints[0]; + const firstProbe = probeVar[0]; + + handleViewerStateChange([lastNotInTheFuture, firstProbe, 0]); + }, [currentAdjustedTime, probeVar, timepoints, handleViewerStateChange]); + + useEffect(() => { + // we have to wait until we have the checkConfigs and subsequent timepoints built + // before knowing what timepoints are available and what to select + if (!persistedIsLoading) { + handleOnInitialised(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps -- we only want to run this once + }, [persistedIsLoading]); + + return !persistedIsLoading; +} + + export function useSelectedProbeNames(statefulTimepoint: StatefulTimepoint) { const { check, checkConfigs } = useTimepointExplorerContext(); const latestConfigDate = checkConfigs[checkConfigs.length - 1].from; diff --git a/src/scenes/components/TimepointExplorer/TimepointListEntry.tsx b/src/scenes/components/TimepointExplorer/TimepointListEntry.tsx index 966d7ae8e..57c51033f 100644 --- a/src/scenes/components/TimepointExplorer/TimepointListEntry.tsx +++ b/src/scenes/components/TimepointExplorer/TimepointListEntry.tsx @@ -34,7 +34,7 @@ export const TimepointListEntry = ({ timepoint }: TimepointListEntryProps) => { }; const Entry = (props: TimepointListEntryProps) => { - const { check, currentAdjustedTime, isLoading, viewMode } = useTimepointExplorerContext(); + const { check, currentAdjustedTime, isInitialised, isLoading, viewMode } = useTimepointExplorerContext(); const statefulTimepoint = useStatefulTimepoint(props.timepoint); const isInTheFuture = getIsInTheFuture(props.timepoint, currentAdjustedTime); const selectedProbeNames = useSceneVarProbes(check); @@ -46,7 +46,7 @@ const Entry = (props: TimepointListEntryProps) => { return
; } - if (isEntryLoading) { + if (isEntryLoading || !isInitialised) { return ; } diff --git a/src/scenes/components/TimepointExplorer/TimepointViewer.tsx b/src/scenes/components/TimepointExplorer/TimepointViewer.tsx index b3e9e1b96..f952e3360 100644 --- a/src/scenes/components/TimepointExplorer/TimepointViewer.tsx +++ b/src/scenes/components/TimepointExplorer/TimepointViewer.tsx @@ -20,9 +20,9 @@ import { TimepointViewerActions } from 'scenes/components/TimepointExplorer/Time import { TimepointViewerExecutions } from 'scenes/components/TimepointExplorer/TimepointViewerExecutions'; export const TimepointViewer = () => { - const { viewerState, shouldScrollToViewer, handleSetScrollToViewer } = useTimepointExplorerContext(); + const { isInitialised, viewerState, shouldScrollToViewer, handleSetScrollToViewer } = useTimepointExplorerContext(); const [logsView, setLogsView] = useState(LOGS_VIEW_OPTIONS[0].value); - const [viewerTimepoint, viewerProbeName] = viewerState || []; + const [viewerTimepoint, viewerProbeName] = viewerState; const styles = useStyles2(getStyles); const containerRef = useRef(null); @@ -60,7 +60,7 @@ export const TimepointViewer = () => {
) : ( - Click on a data point above to view detailed logs. + {isInitialised ? 'Click on a data point above to view detailed logs.' : 'Loading...'} )}
diff --git a/src/scenes/components/TimepointExplorer/__tests__/TimepointExplorer.test.tsx b/src/scenes/components/TimepointExplorer/__tests__/TimepointExplorer.test.tsx index d216d78ff..5b65748c1 100644 --- a/src/scenes/components/TimepointExplorer/__tests__/TimepointExplorer.test.tsx +++ b/src/scenes/components/TimepointExplorer/__tests__/TimepointExplorer.test.tsx @@ -21,13 +21,13 @@ import { mockFeatureToggles } from 'test/utils'; import { FeatureName } from 'types'; import { REF_ID_EXECUTION_LIST_LOGS, + REF_ID_EXECUTION_VIEWER_LOGS, REF_ID_MAX_PROBE_DURATION, REF_ID_UNIQUE_CHECK_CONFIGS, } from 'scenes/components/TimepointExplorer/TimepointExplorer.constants'; -const baseTime = new Date('2024-01-01T12:00:00Z').getTime(); // ✓ This already works +const baseTime = new Date().getTime(); -// MSW handler that returns data to generate exactly 3 timepoints function setupMSWHandlers() { server.use( apiRoute('getHttpDashboard', { @@ -35,7 +35,6 @@ function setupMSWHandlers() { const url = new URL(req.url); const refId = url.searchParams.get('refId'); - // Check configs - return a single config that spans our desired timepoint range if (refId === REF_ID_UNIQUE_CHECK_CONFIGS) { const frame = createUniqueConfigFrame({ configVersion: String((baseTime - 360000) * 1_000_000), @@ -49,7 +48,6 @@ function setupMSWHandlers() { }; } - // Max probe duration if (refId === REF_ID_MAX_PROBE_DURATION) { const frame = createMaxProbeDurationFrame({ refId: REF_ID_MAX_PROBE_DURATION, @@ -65,35 +63,31 @@ function setupMSWHandlers() { }; } - // Execution logs if (refId?.startsWith(REF_ID_EXECUTION_LIST_LOGS)) { return { json: checksLogs1(refId) }; } + if (refId?.startsWith(REF_ID_EXECUTION_VIEWER_LOGS)) { + return { json: checksLogs1(refId) }; + } + return { json: { results: {} } }; }, }) ); } -jest.mock('scenes/components/TimepointExplorer/TimepointViewer.hooks', () => ({ - useTimepointLogs: jest.fn(() => ({ - data: [], - isFetching: false, - isLoading: false, - refetch: jest.fn(), - })), -})); -const mockScrollIntoView = jest.fn(); -Element.prototype.scrollIntoView = mockScrollIntoView; function renderTimepointExplorer() { return ; } +const mockScrollIntoView = jest.fn(); + describe('TimepointExplorer', () => { beforeEach(() => { + Element.prototype.scrollIntoView = mockScrollIntoView; jest.spyOn(Date, 'now').mockReturnValue(baseTime); setupMSWHandlers(); }); @@ -116,21 +110,6 @@ describe('TimepointExplorer', () => { await waitFor(() => screen.findByTestId(DataTestIds.TimepointViewer)); }); - it(`should not show empty state message after a timepoint is selected`, async () => { - mockFeatureToggles({ [FeatureName.TimepointExplorer]: true }); - const { user } = render(renderTimepointExplorer()); - - await screen.findByTestId(DataTestIds.TimepointList); - await screen.findByTestId(DataTestIds.TimepointViewer); - - expect(screen.getByText('Click on a data point above to view detailed logs.')).toBeInTheDocument(); - - const timepointButtons = await screen.findAllByTestId(new RegExp(`${DataTestIds.TimepointListEntryBar}-`)); - await user.click(timepointButtons[0]); - - expect(screen.queryByText('Click on a data point above to view detailed logs.')).not.toBeInTheDocument(); - }); - it(`should call scrollIntoView when a timepoint with data is clicked`, async () => { mockFeatureToggles({ [FeatureName.TimepointExplorer]: true }); const { user } = render(renderTimepointExplorer()); From 65cccc0188b424987578f385d66260b1a5a777cf Mon Sep 17 00:00:00 2001 From: John Lacuna Date: Tue, 3 Feb 2026 17:05:09 -0500 Subject: [PATCH 5/7] fix: add scroll flag to probe click --- .../TimepointExplorer/TimepointListEntryTooltip.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/scenes/components/TimepointExplorer/TimepointListEntryTooltip.tsx b/src/scenes/components/TimepointExplorer/TimepointListEntryTooltip.tsx index 89189feb5..7d9c7eebf 100644 --- a/src/scenes/components/TimepointExplorer/TimepointListEntryTooltip.tsx +++ b/src/scenes/components/TimepointExplorer/TimepointListEntryTooltip.tsx @@ -24,7 +24,7 @@ interface TimepointListEntryTooltipProps { export const TimepointListEntryTooltip = ({ timepoint }: TimepointListEntryTooltipProps) => { const styles = useStyles2(getStyles); - const { currentAdjustedTime, handleHoverStateChange, handleViewerStateChange, hoveredState, viewerState } = + const { currentAdjustedTime, handleHoverStateChange, handleSetScrollToViewer, handleViewerStateChange, hoveredState, viewerState } = useTimepointExplorerContext(); const statefulTimepoint = useStatefulTimepoint(timepoint); @@ -48,9 +48,10 @@ export const TimepointListEntryTooltip = ({ timepoint }: TimepointListEntryToolt component: 'tooltip', status: statefulTimepoint.status, }); + handleSetScrollToViewer(true); handleViewerStateChange([statefulTimepoint, probeName, index]); }, - [statefulTimepoint, handleViewerStateChange] + [statefulTimepoint, handleViewerStateChange, handleSetScrollToViewer] ); return ( From f99c58c470ab62b29ffd9bb64569909aa572ab39 Mon Sep 17 00:00:00 2001 From: John Lacuna Date: Wed, 4 Feb 2026 09:19:09 -0500 Subject: [PATCH 6/7] fix: failing test --- .../TimepointExplorer/__tests__/TimepointExplorer.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/scenes/components/TimepointExplorer/__tests__/TimepointExplorer.test.tsx b/src/scenes/components/TimepointExplorer/__tests__/TimepointExplorer.test.tsx index 5b65748c1..9d4ba1e03 100644 --- a/src/scenes/components/TimepointExplorer/__tests__/TimepointExplorer.test.tsx +++ b/src/scenes/components/TimepointExplorer/__tests__/TimepointExplorer.test.tsx @@ -26,7 +26,7 @@ import { REF_ID_UNIQUE_CHECK_CONFIGS, } from 'scenes/components/TimepointExplorer/TimepointExplorer.constants'; -const baseTime = new Date().getTime(); +const baseTime = Date.now(); function setupMSWHandlers() { server.use( From 75eb710c0a77701f454659a3fd3e11503605ad37 Mon Sep 17 00:00:00 2001 From: John Lacuna Date: Thu, 5 Feb 2026 11:14:52 -0500 Subject: [PATCH 7/7] fix: test flakiness --- .../__tests__/TimepointExplorer.test.tsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/scenes/components/TimepointExplorer/__tests__/TimepointExplorer.test.tsx b/src/scenes/components/TimepointExplorer/__tests__/TimepointExplorer.test.tsx index 9d4ba1e03..26ed6401e 100644 --- a/src/scenes/components/TimepointExplorer/__tests__/TimepointExplorer.test.tsx +++ b/src/scenes/components/TimepointExplorer/__tests__/TimepointExplorer.test.tsx @@ -18,7 +18,7 @@ import { import { TimepointExplorer } from 'scenes/components/TimepointExplorer/TimepointExplorer'; import { DataTestIds } from 'test/dataTestIds'; import { mockFeatureToggles } from 'test/utils'; -import { FeatureName } from 'types'; +import { FeatureName, HTTPCheck } from 'types'; import { REF_ID_EXECUTION_LIST_LOGS, REF_ID_EXECUTION_VIEWER_LOGS, @@ -28,6 +28,14 @@ import { const baseTime = Date.now(); +const TIME_MODIFIED_HTTP_CHECK: HTTPCheck = { + ...BASIC_HTTP_CHECK, + frequency: 60000, + timeout: 10000, + created: Math.floor((baseTime - 60 * 60 * 1000) / 1000), + modified: Math.floor((baseTime - 10 * 60 * 1000) / 1000), +}; + function setupMSWHandlers() { server.use( apiRoute('getHttpDashboard', { @@ -80,7 +88,7 @@ function setupMSWHandlers() { function renderTimepointExplorer() { - return ; + return ; } const mockScrollIntoView = jest.fn();