Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,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;
Expand All @@ -97,6 +98,7 @@ interface TimepointExplorerContextType {
timepointWidth: number;
viewerState: ViewerState;
viewMode: ViewMode;
shouldScrollToViewer: boolean;
vizDisplay: VizDisplay;
vizOptions: Record<TimepointStatus, string>;
yAxisMax: number;
Expand Down Expand Up @@ -226,6 +228,7 @@ export const TimepointExplorerProvider = ({ children, check }: TimepointExplorer
const isError = isCheckConfigsError || isExecutionDurationLogsError || isMaxProbeDurationError;

const [viewerState, setViewerState] = useState<ViewerState>([]);
const [shouldScrollToViewer, setShouldScrollToViewer] = useState(false);

const handleMiniMapSectionChange = useCallback((sectionIndex: number) => {
setMiniMapCurrentSectionIndex(sectionIndex);
Expand All @@ -245,6 +248,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);
Expand Down Expand Up @@ -358,6 +365,7 @@ export const TimepointExplorerProvider = ({ children, check }: TimepointExplorer
handleViewModeChange,
handleVizDisplayChange,
handleVizOptionChange,
handleSetScrollToViewer,
hoveredState,
isCheckCreationWithinTimeRange,
isError,
Expand All @@ -377,6 +385,7 @@ export const TimepointExplorerProvider = ({ children, check }: TimepointExplorer
timepointWidth,
viewerState,
viewMode,
shouldScrollToViewer,
vizDisplay,
vizOptions,
yAxisMax,
Expand All @@ -398,6 +407,7 @@ export const TimepointExplorerProvider = ({ children, check }: TimepointExplorer
handleViewModeChange,
handleVizDisplayChange,
handleVizOptionChange,
handleSetScrollToViewer,
hoveredState,
isCheckCreationWithinTimeRange,
isError,
Expand All @@ -417,6 +427,7 @@ export const TimepointExplorerProvider = ({ children, check }: TimepointExplorer
timepointWidth,
viewerState,
viewMode,
shouldScrollToViewer,
vizDisplay,
vizOptions,
yAxisMax,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -517,6 +517,7 @@ export function useIsInitialised({
return !persistedIsLoading;
}


export function useSelectedProbeNames(statefulTimepoint: StatefulTimepoint) {
const { check, checkConfigs } = useTimepointExplorerContext();
const latestConfigDate = checkConfigs[checkConfigs.length - 1].from;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -32,23 +38,24 @@ 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<HTMLButtonElement>(null);

const handleViewerStateClick = useCallback(() => {
trackTimepointDetailsClicked({
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 <div />;
Expand All @@ -62,7 +69,7 @@ export const TimepointListEntryBar = ({
</div>
)}
<Tooltip content={<TimepointListEntryTooltip timepoint={timepoint} />} ref={ref} interactive placement="top">
<PlainButton className={styles.button} ref={ref} onClick={handleViewerStateClick} showFocusStyles={false}>
<PlainButton className={styles.button} ref={ref} onClick={handleViewerStateClick} showFocusStyles={false} data-testid={`${DataTestIds.TimepointListEntryBar}-${timepoint.index}`}>
<TimepointVizItem
className={cx(styles.bar, GLOBAL_CLASS, {
[styles.selected]: isSelected,
Expand All @@ -77,13 +84,20 @@ export const TimepointListEntryBar = ({
);
};

const getStyles = (theme: GrafanaTheme2, timepointWidth: number, height: number) => {
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);
Expand Down Expand Up @@ -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`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -29,6 +32,7 @@ const ICON_MAP: Record<number, IconName> = {
export const TimepointListEntryReachability = ({ timepoint }: TimepointListEntryProps) => {
const {
handleHoverStateChange,
handleSetScrollToViewer,
handleViewerStateChange,
hoveredState,
timepointWidth,
Expand All @@ -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)`;
Expand All @@ -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) {
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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 (
Expand Down
28 changes: 20 additions & 8 deletions src/scenes/components/TimepointExplorer/TimepointViewer.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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 { isInitialised, viewerState, shouldScrollToViewer, handleSetScrollToViewer } = useTimepointExplorerContext();
const [logsView, setLogsView] = useState<LogsView>(LOGS_VIEW_OPTIONS[0].value);
const [viewerTimepoint, viewerProbeName] = viewerState;
const styles = useStyles2(getStyles);
const containerRef = useRef<HTMLDivElement>(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({
Expand All @@ -32,7 +44,7 @@ export const TimepointViewer = () => {
}, []);

return (
<div className={styles.container}>
<div ref={containerRef} className={styles.container} data-testid={DataTestIds.TimepointViewer}>
{viewerTimepoint ? (
<div>
<Box padding={2} gap={1} direction="column" position="relative">
Expand All @@ -48,8 +60,7 @@ export const TimepointViewer = () => {
</div>
) : (
<Stack justifyContent={'center'} alignItems={'center'} height={30} direction={'column'}>
<Text variant="h2">{isInitialised ? 'No timepoint selected' : 'Loading...'}</Text>
{isInitialised && <Text>Select a timepoint to view logs.</Text>}
<Text>{isInitialised ? 'Click on a data point above to view detailed logs.' : 'Loading...'}</Text>
</Stack>
)}
</div>
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -154,6 +165,7 @@ const getStyles = (theme: GrafanaTheme2) => ({
display: flex;
flex-direction: column;
gap: ${theme.spacing(2)};
scroll-margin-top: 25vh;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This scrolls to 25% of the view height above the logs, so that the user will be able to see a little bit of the graph

`,
loadingBarContainer: css`
position: absolute;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { dateTime } from '@grafana/data';

jest.mock('@grafana/scenes-react', () => {
const actual = jest.requireActual('@grafana/scenes-react');

const now = Date.now();
let mockTimeRange = {
from: new Date('2024-01-01T00:00:00Z'),
to: new Date('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
Expand Down
Loading