diff --git a/.betterer.results b/.betterer.results index 31d1f23e1c8..3b6f2de23a9 100644 --- a/.betterer.results +++ b/.betterer.results @@ -4770,8 +4770,7 @@ exports[`better eslint`] = { [0, 0, 0, "No untranslated strings. Wrap text with ", "35"], [0, 0, 0, "No untranslated strings. Wrap text with ", "36"], [0, 0, 0, "No untranslated strings. Wrap text with ", "37"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "38"], - [0, 0, 0, "Styles should be written using objects.", "39"] + [0, 0, 0, "No untranslated strings. Wrap text with ", "38"] ], "public/app/features/query/state/DashboardQueryRunner/AnnotationsQueryRunner.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] @@ -7310,24 +7309,6 @@ exports[`no gf-form usage`] = { "public/app/features/query/components/QueryEditorRow.tsx:5381": [ [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"] ], - "public/app/features/query/components/QueryGroupOptions.tsx:5381": [ - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"] - ], "public/app/features/variables/adhoc/picker/AdHocFilter.tsx:5381": [ [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"] ], diff --git a/public/app/features/query/components/QueryGroupOptions.tsx b/public/app/features/query/components/QueryGroupOptions.tsx index 8fc36b47e1e..c949e1767b2 100644 --- a/public/app/features/query/components/QueryGroupOptions.tsx +++ b/public/app/features/query/components/QueryGroupOptions.tsx @@ -1,11 +1,9 @@ -import { css } from '@emotion/css'; -import { PureComponent, ChangeEvent, FocusEvent } from 'react'; -import * as React from 'react'; +import { css, cx } from '@emotion/css'; +import React, { useState, ChangeEvent, FocusEvent, useCallback } from 'react'; -import { rangeUtil, PanelData, DataSourceApi } from '@grafana/data'; -import { Input, InlineFormLabel, stylesFactory, InlineFieldRow, InlineSwitch } from '@grafana/ui'; +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 { config } from 'app/core/config'; import { QueryGroupOptions } from 'app/types'; interface Props { @@ -15,149 +13,142 @@ interface Props { onChange: (options: QueryGroupOptions) => void; } -interface State { - timeRangeFrom: string; - timeRangeShift: string; - timeRangeHide: boolean; - isOpen: boolean; - relativeTimeIsValid: boolean; - timeShiftIsValid: boolean; -} +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); -export class QueryGroupOptionsEditor extends PureComponent { - constructor(props: Props) { - super(props); + const styles = useStyles2(getStyles); - const { options } = props; + const onRelativeTimeChange = useCallback((event: ChangeEvent) => { + setTimeRangeFrom(event.target.value); + }, []); - this.state = { - timeRangeFrom: options.timeRange?.from || '', - timeRangeShift: options.timeRange?.shift || '', - timeRangeHide: options.timeRange?.hide ?? false, - isOpen: false, - relativeTimeIsValid: true, - timeShiftIsValid: true, - }; - } + const onTimeShiftChange = useCallback((event: ChangeEvent) => { + setTimeRangeShift(event.target.value); + }, []); - onRelativeTimeChange = (event: ChangeEvent) => { - this.setState({ - timeRangeFrom: event.target.value, - }); - }; + const onOverrideTime = useCallback( + (event: FocusEvent) => { + const newValue = emptyToNull(event.target.value); + const isValid = timeRangeValidation(newValue); - onTimeShiftChange = (event: ChangeEvent) => { - this.setState({ - timeRangeShift: event.target.value, - }); - }; + if (isValid && options.timeRange?.from !== newValue) { + onChange({ + ...options, + timeRange: { + ...(options.timeRange ?? {}), + from: newValue, + }, + }); + } - onOverrideTime = (event: FocusEvent) => { - const { options, onChange } = this.props; + setRelativeTimeIsValid(isValid); + }, + [onChange, options] + ); - const newValue = emptyToNull(event.target.value); - const isValid = timeRangeValidation(newValue); + const onTimeShift = 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, - }, - }); - } + if (isValid && options.timeRange?.shift !== newValue) { + onChange({ + ...options, + timeRange: { + ...(options.timeRange ?? {}), + shift: newValue, + }, + }); + } - this.setState({ relativeTimeIsValid: isValid }); - }; + setTimeShiftIsValid(isValid); + }, + [onChange, options] + ); - onTimeShift = (event: FocusEvent) => { - const { options, onChange } = this.props; - - const newValue = emptyToNull(event.target.value); - const isValid = timeRangeValidation(newValue); - - if (isValid && options.timeRange?.shift !== newValue) { - onChange({ - ...options, - timeRange: { - ...(options.timeRange ?? {}), - shift: newValue, - }, - }); - } - - this.setState({ timeShiftIsValid: isValid }); - }; - - onToggleTimeOverride = () => { - const { onChange, options } = this.props; - - this.setState({ timeRangeHide: !this.state.timeRangeHide }, () => { - onChange({ - ...options, - timeRange: { - ...(options.timeRange ?? {}), - hide: this.state.timeRangeHide, - }, - }); - }); - }; - - onCacheTimeoutBlur = (event: ChangeEvent) => { - const { options, onChange } = this.props; + const onToggleTimeOverride = useCallback(() => { + const newTimeRangeHide = !timeRangeHide; + setTimeRangeHide(newTimeRangeHide); onChange({ ...options, - cacheTimeout: emptyToNull(event.target.value), + timeRange: { + ...(options.timeRange ?? {}), + hide: newTimeRangeHide, + }, }); - }; + }, [onChange, options, timeRangeHide]); - onQueryCachingTTLBlur = (event: ChangeEvent) => { - const { options, onChange } = this.props; - - let ttl: number | null = parseInt(event.target.value, 10); - - if (isNaN(ttl) || ttl === 0) { - ttl = null; - } - - onChange({ - ...options, - queryCachingTTL: ttl, - }); - }; - - onMaxDataPointsBlur = (event: ChangeEvent) => { - const { options, onChange } = this.props; - - let maxDataPoints: number | null = parseInt(event.currentTarget.value, 10); - - if (isNaN(maxDataPoints) || maxDataPoints === 0) { - maxDataPoints = null; - } - - if (maxDataPoints !== options.maxDataPoints) { + const onCacheTimeoutBlur = useCallback( + (event: ChangeEvent) => { onChange({ ...options, - maxDataPoints, + 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; + } - onMinIntervalBlur = (event: ChangeEvent) => { - const { options, onChange } = this.props; - const minInterval = emptyToNull(event.target.value); - if (minInterval !== options.minInterval) { onChange({ ...options, - minInterval, + queryCachingTTL: ttl, }); - } - }; + }, + [onChange, options] + ); - renderCacheTimeoutOption() { - const { dataSource, options } = this.props; + 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.`; @@ -166,27 +157,23 @@ export class QueryGroupOptionsEditor extends PureComponent { } return ( -
-
- - Cache timeout - - -
-
+ <> + + Cache timeout + + + ); - } - - renderQueryCachingTTLOption() { - const { dataSource, options } = this.props; + }; + 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) { @@ -194,129 +181,101 @@ export class QueryGroupOptionsEditor extends PureComponent { } return ( -
-
- - Cache TTL - - -
-
+ <> + Cache TTL + + ); - } + }; - renderMaxDataPointsOption() { - const { data, options } = this.props; + 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
+ 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 + + )} + ); - } + }; - renderIntervalOption() { - const { data, dataSource, options } = this.props; + 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. - - } - > - 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
-
-
+ + 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 ); - } - - onOpenOptions = () => { - this.setState({ isOpen: true }); }; - onCloseOptions = () => { - this.setState({ isOpen: false }); - }; - - renderCollapsedText(styles: StylesType): React.ReactNode | undefined { - const { data, options } = this.props; - const { isOpen } = this.state; - + const renderCollapsedText = (): React.ReactNode | undefined => { if (isOpen) { return undefined; } @@ -326,120 +285,135 @@ export class QueryGroupOptionsEditor extends PureComponent { mdDesc = `auto = ${data.request.maxDataPoints}`; } - let intervalDesc = options.minInterval; - if (data.request) { - intervalDesc = `${data.request.interval}`; - } + const intervalDesc = data.request?.interval ?? options.minInterval; return ( <> - {
MD = {mdDesc}
} - {
Interval = {intervalDesc}
} + {MD = {mdDesc}} + {Interval = {intervalDesc}} ); - } - - render() { - const { timeRangeHide: hideTimeOverride, relativeTimeIsValid, timeShiftIsValid } = this.state; - const { timeRangeFrom: relativeTime, timeRangeShift: timeShift, isOpen } = this.state; - const styles = getStyles(); - - return ( - - {this.renderMaxDataPointsOption()} - {this.renderIntervalOption()} - {this.renderCacheTimeoutOption()} - {this.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 - - -
- {(timeShift || relativeTime) && ( - - Hide time info - - - )} -
- ); - } -} - -const timeRangeValidation = (value: string | null) => { - if (!value) { - return true; - } - - return rangeUtil.isValidTimeSpan(value); -}; - -const emptyToNull = (value: string) => { - return value === '' ? null : value; -}; - -const getStyles = stylesFactory(() => { - const { theme } = config; - - return { - collapsedText: css` - margin-left: ${theme.spacing.md}; - font-size: ${theme.typography.size.sm}; - color: ${theme.colors.textWeak}; - `, }; + + 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 + + + + )} +
+
+ ); }); -type StylesType = ReturnType; +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, + }), + }; +}