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..2325d7bd0 100644 --- a/src/scenes/components/TimepointExplorer/TimepointExplorer.context.tsx +++ b/src/scenes/components/TimepointExplorer/TimepointExplorer.context.tsx @@ -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; @@ -97,6 +98,7 @@ interface TimepointExplorerContextType { timepointWidth: number; viewerState: ViewerState; viewMode: ViewMode; + shouldScrollToViewer: boolean; vizDisplay: VizDisplay; vizOptions: Record; yAxisMax: number; @@ -226,6 +228,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 +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); @@ -358,6 +365,7 @@ export const TimepointExplorerProvider = ({ children, check }: TimepointExplorer handleViewModeChange, handleVizDisplayChange, handleVizOptionChange, + handleSetScrollToViewer, hoveredState, isCheckCreationWithinTimeRange, isError, @@ -377,6 +385,7 @@ export const TimepointExplorerProvider = ({ children, check }: TimepointExplorer timepointWidth, viewerState, viewMode, + shouldScrollToViewer, vizDisplay, vizOptions, yAxisMax, @@ -398,6 +407,7 @@ export const TimepointExplorerProvider = ({ children, check }: TimepointExplorer handleViewModeChange, handleVizDisplayChange, handleVizOptionChange, + handleSetScrollToViewer, hoveredState, isCheckCreationWithinTimeRange, isError, @@ -417,6 +427,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..74ce527ca 100644 --- a/src/scenes/components/TimepointExplorer/TimepointExplorer.hooks.ts +++ b/src/scenes/components/TimepointExplorer/TimepointExplorer.hooks.ts @@ -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; 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/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 ( diff --git a/src/scenes/components/TimepointExplorer/TimepointViewer.tsx b/src/scenes/components/TimepointExplorer/TimepointViewer.tsx index bcaf65a27..f952e3360 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 { isInitialised, viewerState, shouldScrollToViewer, handleSetScrollToViewer } = useTimepointExplorerContext(); const [logsView, setLogsView] = useState(LOGS_VIEW_OPTIONS[0].value); 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.} + {isInitialised ? 'Click on a data point above to view detailed logs.' : 'Loading...'} )}
@@ -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..5ece2afff 100644 --- a/src/scenes/components/TimepointExplorer/__tests__/TimepointExplorer.mocks.ts +++ b/src/scenes/components/TimepointExplorer/__tests__/TimepointExplorer.mocks.ts @@ -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 diff --git a/src/scenes/components/TimepointExplorer/__tests__/TimepointExplorer.test.tsx b/src/scenes/components/TimepointExplorer/__tests__/TimepointExplorer.test.tsx index 5f0301195..26ed6401e 100644 --- a/src/scenes/components/TimepointExplorer/__tests__/TimepointExplorer.test.tsx +++ b/src/scenes/components/TimepointExplorer/__tests__/TimepointExplorer.test.tsx @@ -5,26 +5,132 @@ 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 { FeatureName, HTTPCheck } 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 = 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', { + result: async (req) => { + const url = new URL(req.url); + const refId = url.searchParams.get('refId'); + + 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]), + }; + } + + 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]), + }; + } + + 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: {} } }; + }, + }) + ); +} + + function renderTimepointExplorer() { - return ; + return ; } +const mockScrollIntoView = jest.fn(); + describe('TimepointExplorer', () => { + beforeEach(() => { + Element.prototype.scrollIntoView = mockScrollIntoView; + 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)); 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 call scrollIntoView when a timepoint with data is clicked`, async () => { + mockFeatureToggles({ [FeatureName.TimepointExplorer]: true }); + const { user } = render(renderTimepointExplorer()); + + expect(mockScrollIntoView).not.toHaveBeenCalled(); + + 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/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', } 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, + }, + }, + }; +} +