import { css, cx } from '@emotion/css'; import React, { useState, ChangeEvent, FocusEvent, useCallback } from 'react'; import { rangeUtil, PanelData, DataSourceApi, GrafanaTheme2 } from '@grafana/data'; import { Input, InlineSwitch, useStyles2, InlineLabel } from '@grafana/ui'; import { QueryOperationRow } from 'app/core/components/QueryOperationRow/QueryOperationRow'; import { QueryGroupOptions } from 'app/types'; interface Props { options: QueryGroupOptions; dataSource: DataSourceApi; data: PanelData; onChange: (options: QueryGroupOptions) => void; } export const QueryGroupOptionsEditor = React.memo(({ options, dataSource, data, onChange }: Props) => { 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); const styles = useStyles2(getStyles); const onRelativeTimeChange = useCallback((event: ChangeEvent) => { setTimeRangeFrom(event.target.value); }, []); const onTimeShiftChange = useCallback((event: ChangeEvent) => { setTimeRangeShift(event.target.value); }, []); const onOverrideTime = useCallback( (event: FocusEvent) => { const newValue = emptyToNull(event.target.value); const isValid = timeRangeValidation(newValue); if (isValid && options.timeRange?.from !== newValue) { onChange({ ...options, timeRange: { ...(options.timeRange ?? {}), from: newValue, }, }); } setRelativeTimeIsValid(isValid); }, [onChange, options] ); const onTimeShift = useCallback( (event: FocusEvent) => { const newValue = emptyToNull(event.target.value); const isValid = timeRangeValidation(newValue); if (isValid && options.timeRange?.shift !== newValue) { onChange({ ...options, timeRange: { ...(options.timeRange ?? {}), shift: newValue, }, }); } setTimeShiftIsValid(isValid); }, [onChange, options] ); const onToggleTimeOverride = useCallback(() => { const newTimeRangeHide = !timeRangeHide; setTimeRangeHide(newTimeRangeHide); onChange({ ...options, timeRange: { ...(options.timeRange ?? {}), hide: newTimeRangeHide, }, }); }, [onChange, options, timeRangeHide]); const onCacheTimeoutBlur = useCallback( (event: ChangeEvent) => { onChange({ ...options, cacheTimeout: emptyToNull(event.target.value), }); }, [onChange, options] ); const onQueryCachingTTLBlur = useCallback( (event: ChangeEvent) => { let ttl: number | null = parseInt(event.target.value, 10); if (isNaN(ttl) || ttl === 0) { ttl = null; } onChange({ ...options, queryCachingTTL: ttl, }); }, [onChange, options] ); const onMaxDataPointsBlur = useCallback( (event: ChangeEvent) => { let maxDataPoints: number | null = parseInt(event.currentTarget.value, 10); if (isNaN(maxDataPoints) || maxDataPoints === 0) { maxDataPoints = null; } if (maxDataPoints !== options.maxDataPoints) { onChange({ ...options, maxDataPoints, }); } }, [onChange, options] ); const onMinIntervalBlur = useCallback( (event: ChangeEvent) => { const minInterval = emptyToNull(event.target.value); if (minInterval !== options.minInterval) { onChange({ ...options, minInterval, }); } }, [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.`; if (!dataSource.meta.queryOptions?.cacheTimeout) { return null; } return ( <> Cache timeout ); }; const renderQueryCachingTTLOption = () => { const tooltip = `Cache time-to-live: How long results from this queries in this panel will be cached, in milliseconds. Defaults to the TTL in the caching configuration for this datasource.`; if (!dataSource.cachingConfig?.enabled) { return null; } return ( <> Cache TTL ); }; const renderMaxDataPointsOption = () => { const realMd = data.request?.maxDataPoints; const value = options.maxDataPoints ?? ''; const isAuto = value === ''; return ( <> The maximum data points per series. Used directly by some data sources and used in calculation of auto interval. With streaming data this value is used for the rolling buffer. } > Max data points {isAuto && ( <> = Width of panel )} ); }; const renderIntervalOption = () => { const realInterval = data.request?.interval; const minIntervalOnDs = dataSource.interval ?? 'No limit'; return ( <> A lower limit for the interval. Recommended to be set to write frequency, for example 1m if your data is written every minute. Default value can be set in data source settings for most data sources. } htmlFor="min-interval-input" > Min interval The evaluated interval that is sent to data source and is used in $__interval and{' '} $__interval_ms. This value is not exactly equal to Time range / max data points, it will approximate a series of magic number. } > Interval {realInterval} = Time range / max data points ); }; 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()} 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 now-5m and 5m, or variables like{' '} $_relativeTime. } > 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 now-1h and 1h, or variables like $_timeShift. } > Time shift {(timeRangeShift || timeRangeFrom) && ( <> Hide time info )}
); }); QueryGroupOptionsEditor.displayName = 'QueryGroupOptionsEditor'; function timeRangeValidation(value: string | null) { return !value || rangeUtil.isValidTimeSpan(value); } function emptyToNull(value: string) { return value === '' ? null : value; } function getStyles(theme: GrafanaTheme2) { return { grid: css({ display: 'grid', gridTemplateColumns: `auto minmax(5em, 1fr) auto 1fr`, gap: theme.spacing(0.5), gridAutoRows: theme.spacing(4), whiteSpace: 'nowrap', }), firstColumn: css({ gridColumn: 1, }), collapsedText: css({ marginLeft: theme.spacing(2), fontSize: theme.typography.size.sm, color: theme.colors.text.secondary, }), noSquish: css({ display: 'flex', alignItems: 'center', padding: theme.spacing(0, 1), fontWeight: theme.typography.fontWeightMedium, fontSize: theme.typography.size.sm, backgroundColor: theme.colors.background.secondary, borderRadius: theme.shape.radius.default, }), left: css({ justifySelf: 'left', }), operator: css({ color: theme.v1.palette.orange, }), }; }