From 61b5880d95ee2a179ee075d30bdd5cf0a6fb106c Mon Sep 17 00:00:00 2001 From: Paul Marbach Date: Tue, 2 Dec 2025 10:57:39 -0500 Subject: [PATCH] add options panel --- .../PanelDataPane/PanelDataPane.tsx | 7 +- .../PanelDataPane/QueryDetailView.tsx | 234 ++++++++++++++++-- .../query/components/QueryGroupOptions.tsx | 192 ++++++-------- 3 files changed, 293 insertions(+), 140 deletions(-) diff --git a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataPane.tsx b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataPane.tsx index f3d0f85239b..71fb19e5d40 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataPane.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataPane.tsx @@ -401,6 +401,7 @@ function PanelDataPaneRendered({ model }: SceneComponentProps) { const { containerProps, primaryProps, secondaryProps, splitterProps } = useSplitter({ direction: 'row', initialSize: 0.25, + handleSize: 'xs', }); return ( @@ -423,7 +424,11 @@ function PanelDataPaneRendered({ model }: SceneComponentProps) { onRemoveTransform={handleRemoveTransform} /> -
+
diff --git a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/QueryDetailView.tsx b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/QueryDetailView.tsx index 9ada0445e35..dcc2165e90f 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/QueryDetailView.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/QueryDetailView.tsx @@ -1,16 +1,27 @@ -import { css } from '@emotion/css'; -import { useCallback, useMemo } from 'react'; +import { css, cx } from '@emotion/css'; +import { useCallback, useMemo, useState } from 'react'; import { useAsync } from 'react-use'; -import { CoreApp, DataQuery, DataSourcePluginContextProvider, GrafanaTheme2, TimeRange } from '@grafana/data'; +import { + CoreApp, + DataQuery, + DataSourcePluginContextProvider, + GrafanaTheme2, + TimeRange, + getDataSourceRef, +} from '@grafana/data'; import { Trans } from '@grafana/i18n'; import { getDataSourceSrv } from '@grafana/runtime'; -import { SceneDataQuery, VizPanel } from '@grafana/scenes'; +import { SceneDataQuery, VizPanel, sceneGraph, SceneQueryRunner } from '@grafana/scenes'; import { ErrorBoundaryAlert, useStyles2 } from '@grafana/ui'; import { QueryOperationRow } from 'app/core/components/QueryOperationRow/QueryOperationRow'; import { QueryErrorAlert } from 'app/features/query/components/QueryErrorAlert'; +import { QueryGroupOptionsEditor } from 'app/features/query/components/QueryGroupOptions'; +import { QueryGroupOptions } from 'app/types/query'; +import { PanelTimeRange } from '../../scene/panel-timerange/PanelTimeRange'; import { getQueryRunnerFor } from '../../utils/utils'; +import { getUpdatedHoverHeader } from '../getPanelFrameOptions'; interface QueryDetailViewProps { panel: VizPanel; @@ -20,6 +31,7 @@ interface QueryDetailViewProps { export function QueryDetailView({ panel, query, queryIndex }: QueryDetailViewProps) { const styles = useStyles2(getStyles); + const [showOptions, setShowOptions] = useState(false); const dsSettings = useMemo(() => { try { @@ -75,6 +87,89 @@ export function QueryDetailView({ panel, query, queryIndex }: QueryDetailViewPro } }, [queryRunner]); + // FIXME this should be a memo or some other structure, not a callback + const buildQueryOptions = useCallback((): QueryGroupOptions => { + if (!queryRunner) { + return { + queries: [], + dataSource: dsSettings ? getDataSourceRef(dsSettings) : { type: undefined, uid: undefined }, + }; + } + + const timeRangeObj = sceneGraph.getTimeRange(panel); + + let timeRangeOpts: QueryGroupOptions['timeRange'] = { + from: undefined, + shift: undefined, + hide: undefined, + }; + + if (timeRangeObj instanceof PanelTimeRange) { + timeRangeOpts = { + from: timeRangeObj.state.timeFrom, + shift: timeRangeObj.state.timeShift, + hide: timeRangeObj.state.hideTimeOverride, + }; + } + + return { + cacheTimeout: dsSettings?.meta.queryOptions?.cacheTimeout ? queryRunner.state.cacheTimeout : undefined, + queryCachingTTL: dsSettings?.cachingConfig?.enabled ? queryRunner.state.queryCachingTTL : undefined, + dataSource: { + default: dsSettings?.isDefault, + ...(dsSettings ? getDataSourceRef(dsSettings) : { type: undefined, uid: undefined }), + }, + queries: queryRunner.state.queries, + maxDataPoints: queryRunner.state.maxDataPoints, + minInterval: queryRunner.state.minInterval, + timeRange: timeRangeOpts, + }; + }, [queryRunner, panel, dsSettings]); + + const handleQueryOptionsChange = useCallback( + (options: QueryGroupOptions) => { + if (!queryRunner) { + return; + } + + const dataObjStateUpdate: Partial = {}; + const panelStateUpdate: Partial = {}; + + if (options.maxDataPoints !== queryRunner.state.maxDataPoints) { + dataObjStateUpdate.maxDataPoints = options.maxDataPoints ?? undefined; + } + + if (options.minInterval !== queryRunner.state.minInterval) { + dataObjStateUpdate.minInterval = options.minInterval ?? undefined; + } + + const timeFrom = options.timeRange?.from ?? undefined; + const timeShift = options.timeRange?.shift ?? undefined; + const hideTimeOverride = options.timeRange?.hide; + + if (timeFrom !== undefined || timeShift !== undefined) { + panelStateUpdate.$timeRange = new PanelTimeRange({ timeFrom, timeShift, hideTimeOverride }); + panelStateUpdate.hoverHeader = getUpdatedHoverHeader(panel.state.title, panelStateUpdate.$timeRange); + } else { + panelStateUpdate.$timeRange = undefined; + panelStateUpdate.hoverHeader = getUpdatedHoverHeader(panel.state.title, undefined); + } + + if (options.cacheTimeout !== queryRunner.state.cacheTimeout) { + dataObjStateUpdate.cacheTimeout = options.cacheTimeout; + } + + if (options.queryCachingTTL !== queryRunner.state.queryCachingTTL) { + dataObjStateUpdate.queryCachingTTL = options.queryCachingTTL; + } + + panel.setState(panelStateUpdate); + queryRunner.setState(dataObjStateUpdate); + queryRunner.runQueries(); + }, + [queryRunner, panel] + ); + const renderQueryEditor = () => { if (!datasource || !dsSettings) { return ( @@ -114,21 +209,72 @@ export function QueryDetailView({ panel, query, queryIndex }: QueryDetailViewPro const error = data?.error || data?.errors?.find((e) => e.refId === query.refId); + const queryOptions = buildQueryOptions(); + const panelData = queryRunnerState?.data; + + const renderCollapsedText = (): React.ReactNode | undefined => { + if (!panelData) { + return undefined; + } + + let mdDesc = queryOptions.maxDataPoints ?? ''; + if (mdDesc === '' && panelData.request) { + mdDesc = `auto = ${panelData.request.maxDataPoints}`; + } + + const intervalDesc = panelData.request?.interval ?? queryOptions.minInterval; + + return ( + <> + { + + MD = {{ mdDesc }} + + } + { + + Interval = {{ intervalDesc }} + + } + + ); + }; + return (
- -
- {renderQueryEditor()} - {error && } +
+
+ +
+ {renderQueryEditor()} + {error && } +
+
- + {showOptions && datasource && panelData && ( +
+ +
+ )} +
+
+ {renderCollapsedText()} + +
); } @@ -136,21 +282,71 @@ export function QueryDetailView({ panel, query, queryIndex }: QueryDetailViewPro const getStyles = (theme: GrafanaTheme2) => { return { container: css({ + width: '100%', + height: '100%', + position: 'relative', + }), + contentWrapper: css({ + display: 'grid', + gridTemplateColumns: '1fr', + width: '100%', + minHeight: 'calc(100% - 36px)', // 36px for footer + }), + contentWrapperTwoColumn: css({ + gridTemplateColumns: '1fr 0.5fr', + }), + mainContent: css({ display: 'flex', flexDirection: 'column', - gap: theme.spacing(2), - padding: theme.spacing(2), - width: '100%', + gap: theme.spacing(1), + overflow: 'scroll', + padding: theme.spacing(2, 2, 0, 2), }), queryContent: css({ display: 'flex', flexDirection: 'column', gap: theme.spacing(1), + minHeight: '100%', + }), + footer: css({ + display: 'flex', + justifyContent: 'flex-end', + borderTop: `1px solid ${theme.colors.border.weak}`, + position: 'sticky', + bottom: 0, + zIndex: theme.zIndex.navbarFixed, + padding: theme.spacing(1, 2), + background: theme.colors.background.secondary, + }), + optionsLink: css({ + background: 'none', + border: 'none', + color: theme.colors.text.link, + cursor: 'pointer', + fontSize: theme.typography.bodySmall.fontSize, + padding: 0, + textDecoration: 'none', + '&:hover': { + textDecoration: 'underline', + }, + }), + optionsColumn: css({ + display: 'flex', + flexDirection: 'column', + paddingLeft: theme.spacing(2), + borderLeft: `1px solid ${theme.colors.border.weak}`, + background: theme.colors.background.secondary, + padding: theme.spacing(2), }), noEditor: css({ padding: theme.spacing(2), textAlign: 'center', color: theme.colors.text.secondary, }), + collapsedText: css({ + marginInline: theme.spacing(1), + fontSize: theme.typography.size.sm, + color: theme.colors.text.secondary, + }), }; }; diff --git a/public/app/features/query/components/QueryGroupOptions.tsx b/public/app/features/query/components/QueryGroupOptions.tsx index 2404236e483..b97c10b09d7 100644 --- a/public/app/features/query/components/QueryGroupOptions.tsx +++ b/public/app/features/query/components/QueryGroupOptions.tsx @@ -2,9 +2,8 @@ import { css, cx } from '@emotion/css'; import React, { useState, ChangeEvent, FocusEvent, useCallback } from 'react'; import { rangeUtil, PanelData, DataSourceApi, GrafanaTheme2 } from '@grafana/data'; -import { Trans, t } from '@grafana/i18n'; +import { Trans } from '@grafana/i18n'; import { Input, InlineSwitch, useStyles2, InlineLabel } from '@grafana/ui'; -import { QueryOperationRow } from 'app/core/components/QueryOperationRow/QueryOperationRow'; import { QueryGroupOptions } from 'app/types/query'; interface Props { @@ -18,7 +17,6 @@ export const QueryGroupOptionsEditor = React.memo(({ options, dataSource, data, const [timeRangeFrom, setTimeRangeFrom] = useState(options.timeRange?.from || ''); const [timeRangeShift, setTimeRangeShift] = useState(options.timeRange?.shift || ''); const [timeRangeHide, setTimeRangeHide] = useState(options.timeRange?.hide ?? false); - const [isOpen, setIsOpen] = useState(false); const [relativeTimeIsValid, setRelativeTimeIsValid] = useState(true); const [timeShiftIsValid, setTimeShiftIsValid] = useState(true); @@ -141,14 +139,6 @@ export const QueryGroupOptionsEditor = React.memo(({ options, dataSource, data, [onChange, options] ); - const onOpenOptions = useCallback(() => { - setIsOpen(true); - }, []); - - const onCloseOptions = useCallback(() => { - setIsOpen(false); - }, []); - const renderCacheTimeoutOption = () => { const tooltip = `If your time series store has a query cache this option can override the default cache timeout. Specify a numeric value in seconds.`; @@ -294,117 +284,79 @@ export const QueryGroupOptionsEditor = React.memo(({ options, dataSource, data, ); }; - const renderCollapsedText = (): React.ReactNode | undefined => { - if (isOpen) { - return undefined; - } - - let mdDesc = options.maxDataPoints ?? ''; - if (mdDesc === '' && data.request) { - mdDesc = `auto = ${data.request.maxDataPoints}`; - } - - const intervalDesc = data.request?.interval ?? options.minInterval; - - return ( - <> - { - - MD = {{ mdDesc }} - - } - { - - Interval = {{ intervalDesc }} - - } - - ); - }; - return ( - -
- {renderMaxDataPointsOption()} - {renderIntervalOption()} - {renderCacheTimeoutOption()} - {renderQueryCachingTTLOption()} +
+ {renderMaxDataPointsOption()} + {renderIntervalOption()} + {renderCacheTimeoutOption()} + {renderQueryCachingTTLOption()} - - Overrides the relative time range for individual panels, which causes them to be different than what is - selected in the dashboard time picker in the top-right corner of the dashboard. For example to configure - the Last 5 minutes the Relative time should be {'{{relativeFrom}}'} and{' '} - {'{{relativeTo}}'}, or variables like {'{{variable}}'}. - - } - > - Relative time - - - - Overrides the time range for individual panels by shifting its start and end relative to the time picker. - For example to configure the Last 1h the Time shift should be {'{{relativeFrom}}'} and{' '} - {'{{relativeTo}}'}, or variables like {'{{variable}}'}. - - } - > - Time shift - - - {(timeRangeShift || timeRangeFrom) && ( - <> - - Hide time info - - - - )} -
- + + Overrides the relative time range for individual panels, which causes them to be different than what is + selected in the dashboard time picker in the top-right corner of the dashboard. For example to configure the + Last 5 minutes the Relative time should be {'{{relativeFrom}}'} and{' '} + {'{{relativeTo}}'}, or variables like {'{{variable}}'}. + + } + > + Relative time + + + + Overrides the time range for individual panels by shifting its start and end relative to the time picker. + For example to configure the Last 1h the Time shift should be {'{{relativeFrom}}'} and{' '} + {'{{relativeTo}}'}, or variables like {'{{variable}}'}. + + } + > + Time shift + + + {(timeRangeShift || timeRangeFrom) && ( + <> + + Hide time info + + + + )} +
); });