The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/public/app/features/query/components/QueryGroupOptions.tsx

421 lines
12 KiB

import { css } from '@emotion/css';
import React, { PureComponent, ChangeEvent, FocusEvent } from 'react';
import { rangeUtil, PanelData, DataSourceApi } from '@grafana/data';
import { Switch, Input, InlineField, InlineFormLabel, stylesFactory } from '@grafana/ui';
import { QueryOperationRow } from 'app/core/components/QueryOperationRow/QueryOperationRow';
import { config } from 'app/core/config';
import { QueryGroupOptions } from 'app/types';
interface Props {
options: QueryGroupOptions;
dataSource: DataSourceApi;
data: PanelData;
onChange: (options: QueryGroupOptions) => void;
}
interface State {
timeRangeFrom: string;
timeRangeShift: string;
timeRangeHide: boolean;
isOpen: boolean;
relativeTimeIsValid: boolean;
timeShiftIsValid: boolean;
}
export class QueryGroupOptionsEditor extends PureComponent<Props, State> {
constructor(props: Props) {
super(props);
const { options } = props;
this.state = {
timeRangeFrom: options.timeRange?.from || '',
timeRangeShift: options.timeRange?.shift || '',
timeRangeHide: options.timeRange?.hide ?? false,
isOpen: false,
relativeTimeIsValid: true,
timeShiftIsValid: true,
};
}
onRelativeTimeChange = (event: ChangeEvent<HTMLInputElement>) => {
this.setState({
timeRangeFrom: event.target.value,
});
};
onTimeShiftChange = (event: ChangeEvent<HTMLInputElement>) => {
this.setState({
timeRangeShift: event.target.value,
});
};
onOverrideTime = (event: FocusEvent<HTMLInputElement>) => {
const { options, onChange } = this.props;
const newValue = emptyToNull(event.target.value);
const isValid = timeRangeValidation(newValue);
if (isValid && options.timeRange?.from !== newValue) {
onChange({
...options,
timeRange: {
...(options.timeRange ?? {}),
from: newValue,
},
});
}
this.setState({ relativeTimeIsValid: isValid });
};
onTimeShift = (event: FocusEvent<HTMLInputElement>) => {
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<HTMLInputElement>) => {
const { options, onChange } = this.props;
onChange({
...options,
cacheTimeout: emptyToNull(event.target.value),
});
};
onQueryCachingTTLBlur = (event: ChangeEvent<HTMLInputElement>) => {
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<HTMLInputElement>) => {
const { options, onChange } = this.props;
let maxDataPoints: number | null = parseInt(event.target.value as string, 10);
if (isNaN(maxDataPoints) || maxDataPoints === 0) {
maxDataPoints = null;
}
if (maxDataPoints !== options.maxDataPoints) {
onChange({
...options,
maxDataPoints,
});
}
};
onMinIntervalBlur = (event: ChangeEvent<HTMLInputElement>) => {
const { options, onChange } = this.props;
const minInterval = emptyToNull(event.target.value);
if (minInterval !== options.minInterval) {
onChange({
...options,
minInterval,
});
}
};
renderCacheTimeoutOption() {
const { dataSource, options } = this.props;
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 (
<div className="gf-form-inline">
<div className="gf-form">
<InlineFormLabel width={9} tooltip={tooltip}>
Cache timeout
</InlineFormLabel>
<Input
type="text"
className="width-6"
placeholder="60"
spellCheck={false}
onBlur={this.onCacheTimeoutBlur}
defaultValue={options.cacheTimeout ?? ''}
/>
</div>
</div>
);
}
renderQueryCachingTTLOption() {
const { dataSource, options } = this.props;
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 (
<div className="gf-form-inline">
<div className="gf-form">
<InlineFormLabel width={9} tooltip={tooltip}>
Cache TTL
</InlineFormLabel>
<Input
type="number"
className="width-6"
placeholder={`${dataSource.cachingConfig.TTLMs}`}
spellCheck={false}
onBlur={this.onQueryCachingTTLBlur}
defaultValue={options.queryCachingTTL ?? undefined}
/>
</div>
</div>
);
}
renderMaxDataPointsOption() {
const { data, options } = this.props;
const realMd = data.request?.maxDataPoints;
const value = options.maxDataPoints ?? '';
const isAuto = value === '';
return (
<div className="gf-form-inline">
<div className="gf-form">
<InlineFormLabel
width={9}
tooltip={
<>
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
</InlineFormLabel>
<Input
type="number"
className="width-6"
placeholder={`${realMd}`}
spellCheck={false}
onBlur={this.onMaxDataPointsBlur}
defaultValue={value}
/>
{isAuto && (
<>
<div className="gf-form-label query-segment-operator">=</div>
<div className="gf-form-label">Width of panel</div>
</>
)}
</div>
</div>
);
}
renderIntervalOption() {
const { data, dataSource, options } = this.props;
const realInterval = data.request?.interval;
const minIntervalOnDs = dataSource.interval ?? 'No limit';
return (
<>
<div className="gf-form-inline">
<div className="gf-form">
<InlineFormLabel
width={9}
tooltip={
<>
A lower limit for the interval. Recommended to be set to write frequency, for example <code>1m</code>{' '}
if your data is written every minute. Default value can be set in data source settings for most data
sources.
</>
}
>
Min interval
</InlineFormLabel>
<Input
type="text"
className="width-6"
placeholder={`${minIntervalOnDs}`}
spellCheck={false}
onBlur={this.onMinIntervalBlur}
defaultValue={options.minInterval ?? ''}
/>
</div>
</div>
<div className="gf-form-inline">
<div className="gf-form">
<InlineFormLabel
width={9}
tooltip={
<>
The evaluated interval that is sent to data source and is used in <code>$__interval</code> and{' '}
<code>$__interval_ms</code>
</>
}
>
Interval
</InlineFormLabel>
<InlineFormLabel width={6}>{realInterval}</InlineFormLabel>
<div className="gf-form-label query-segment-operator">=</div>
<div className="gf-form-label">Time range / max data points</div>
</div>
</div>
</>
);
}
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;
if (isOpen) {
return undefined;
}
let mdDesc = options.maxDataPoints ?? '';
if (mdDesc === '' && data.request) {
mdDesc = `auto = ${data.request.maxDataPoints}`;
}
let intervalDesc = options.minInterval;
if (data.request) {
intervalDesc = `${data.request.interval}`;
}
return (
<>
{<div className={styles.collapsedText}>MD = {mdDesc}</div>}
{<div className={styles.collapsedText}>Interval = {intervalDesc}</div>}
</>
);
}
render() {
const { timeRangeHide: hideTimeOverride, relativeTimeIsValid, timeShiftIsValid } = this.state;
const { timeRangeFrom: relativeTime, timeRangeShift: timeShift, isOpen } = this.state;
const styles = getStyles();
return (
<QueryOperationRow
id="Query options"
index={0}
title="Query options"
headerElement={this.renderCollapsedText(styles)}
isOpen={isOpen}
onOpen={this.onOpenOptions}
onClose={this.onCloseOptions}
>
{this.renderMaxDataPointsOption()}
{this.renderIntervalOption()}
{this.renderCacheTimeoutOption()}
{this.renderQueryCachingTTLOption()}
<div className="gf-form">
<InlineFormLabel width={9}>Relative time</InlineFormLabel>
<Input
type="text"
className="width-6"
placeholder="1h"
onChange={this.onRelativeTimeChange}
onBlur={this.onOverrideTime}
invalid={!relativeTimeIsValid}
value={relativeTime}
/>
</div>
<div className="gf-form">
<span className="gf-form-label width-9">Time shift</span>
<Input
type="text"
className="width-6"
placeholder="1h"
onChange={this.onTimeShiftChange}
onBlur={this.onTimeShift}
invalid={!timeShiftIsValid}
value={timeShift}
/>
</div>
{(timeShift || relativeTime) && (
<div className="gf-form-inline">
<InlineField label="Hide time info" labelWidth={18}>
<Switch value={hideTimeOverride} onChange={this.onToggleTimeOverride} />
</InlineField>
</div>
)}
</QueryOperationRow>
);
}
}
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};
`,
};
});
type StylesType = ReturnType<typeof getStyles>;