Commit 0742dbc9 by Torkel Ödegaard Committed by GitHub

MaxDataPoints: Now used in interval calculation for all data sources (#23915)

* MaxDataPoints: Now enabled for all

* Updates to code and test

* Moved the panel query inspector

* PaneQueryRunner: Simplify logic and only take in maxDataPoints not width
parent 32492dd6
......@@ -109,7 +109,7 @@ const getQueryOperationRowStyles = stylesFactory((theme: GrafanaTheme) => {
`,
content: css`
margin-top: ${theme.spacing.inlineFormMargin};
margin-left: ${theme.spacing.xl};
margin-left: ${theme.spacing.lg};
`,
};
});
......
......@@ -200,8 +200,7 @@ export class PanelChrome extends PureComponent<Props, State> {
timezone: this.props.dashboard.getTimezone(),
timeRange: timeData.timeRange,
timeInfo: timeData.timeInfo,
widthPixels: width,
maxDataPoints: panel.maxDataPoints,
maxDataPoints: panel.maxDataPoints || width,
minInterval: panel.interval,
scopedVars: panel.scopedVars,
cacheTimeout: panel.cacheTimeout,
......
import React, { FC, ChangeEvent } from 'react';
import { InlineFormLabel, LegacyForms } from '@grafana/ui';
const { Input } = LegacyForms;
interface Props {
label: string;
placeholder?: string;
name: string;
value: string;
onBlur: (event: ChangeEvent<HTMLInputElement>) => void;
onChange: (event: ChangeEvent<HTMLInputElement>) => void;
tooltipInfo?: any;
}
export const DataSourceOption: FC<Props> = ({ label, placeholder, name, value, onBlur, onChange, tooltipInfo }) => {
return (
<div className="gf-form gf-form--flex-end">
<InlineFormLabel width={9} tooltip={tooltipInfo}>
{label}
</InlineFormLabel>
<Input
type="text"
className="gf-form-input width-6"
placeholder={placeholder}
name={name}
spellCheck={false}
onBlur={onBlur}
onChange={onChange}
value={value}
/>
</div>
);
};
......@@ -172,7 +172,15 @@ export class QueriesTab extends PureComponent<Props, State> {
<DataSourcePicker datasources={this.datasources} onChange={this.onChangeDataSource} current={currentDS} />
</div>
<div className={styles.dataSourceRowItem}>
<Button variant="secondary" icon="info-circle" title="Open data source help" onClick={this.onOpenHelp} />
<Button
variant="secondary"
icon="question-circle"
title="Open data source help"
onClick={this.onOpenHelp}
/>
</div>
<div className={styles.dataSourceRowItemOptions}>
<QueryOptions panel={panel} datasource={currentDS} data={data} />
</div>
<div className={styles.dataSourceRowItem}>
<Button
......@@ -183,9 +191,6 @@ export class QueriesTab extends PureComponent<Props, State> {
Query inspector
</Button>
</div>
<div className={styles.dataSourceRowItemOptions}>
<QueryOptions panel={panel} datasource={currentDS} data={data} />
</div>
</div>
</div>
);
......@@ -327,6 +332,7 @@ const getStyles = stylesFactory(() => {
`,
dataSourceRowItemOptions: css`
flex-grow: 1;
margin-right: ${theme.spacing.inlineFormMargin};
`,
queriesWrapper: css`
padding-bottom: 16px;
......
......@@ -13,8 +13,7 @@ import {
InlineFormLabel,
stylesFactory,
} from '@grafana/ui';
import { DataSourceOption } from './DataSourceOption';
const { Input, Switch } = LegacyForms;
const { Switch, Input } = LegacyForms;
// Types
import { PanelModel } from '../state';
......@@ -57,46 +56,6 @@ interface State {
}
export class QueryOptions extends PureComponent<Props, State> {
allOptions: any = {
cacheTimeout: {
label: 'Cache timeout',
placeholder: '60',
name: 'cacheTimeout',
tooltipInfo: (
<>
If your time series store has a query cache this option can override the default cache timeout. Specify a
numeric value in seconds.
</>
),
},
maxDataPoints: {
label: 'Max data points',
placeholder: 'auto',
name: 'maxDataPoints',
tooltipInfo: (
<>
The maximum data points the query should return. For graphs this is automatically set to one data point per
pixel. For some data sources this can also be capped in the datasource settings page. With streaming data,
this value is used for the rolling buffer.
</>
),
},
minInterval: {
label: 'Min time interval',
placeholder: '0',
name: 'minInterval',
panelKey: 'interval',
tooltipInfo: (
<>
A lower limit for the auto group by time interval. Recommended to be set to write frequency, for example{' '}
<code>1m</code> if your data is written every minute. Access auto interval via variable{' '}
<code>$__interval</code> for time range string and <code>$__interval_ms</code> for numeric variable that can
be used in math expressions.
</>
),
},
};
constructor(props: Props) {
super(props);
......@@ -127,6 +86,7 @@ export class QueryOptions extends PureComponent<Props, State> {
const { value } = event.target;
const { panel } = this.props;
const emptyToNullValue = emptyToNull(value);
if (status === LegacyInputStatus.Valid && panel.timeFrom !== emptyToNullValue) {
panel.timeFrom = emptyToNullValue;
panel.refresh();
......@@ -137,6 +97,7 @@ export class QueryOptions extends PureComponent<Props, State> {
const { value } = event.target;
const { panel } = this.props;
const emptyToNullValue = emptyToNull(value);
if (status === LegacyInputStatus.Valid && panel.timeShift !== emptyToNullValue) {
panel.timeShift = emptyToNullValue;
panel.refresh();
......@@ -163,35 +124,132 @@ export class QueryOptions extends PureComponent<Props, State> {
this.setState({ ...this.state, [panelKey]: event.target.value });
};
/**
* Show options for any value that is set, or values that the
* current datasource says it will use
*/
renderOptions = () => {
renderCacheTimeoutOption() {
const { datasource } = this.props;
const queryOptions: any = datasource.meta.queryOptions || {};
return Object.keys(this.allOptions).map(key => {
const options = this.allOptions[key];
const panelKey = options.panelKey || key;
// @ts-ignore
const value = this.state[panelKey];
if (queryOptions[key]) {
return (
<DataSourceOption
key={key}
{...options}
onChange={this.onDataSourceOptionChange(panelKey)}
onBlur={this.onDataSourceOptionBlur(panelKey)}
value={value}
const { cacheTimeout } = this.state;
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?.maxDataPoints) {
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"
name={name}
spellCheck={false}
onBlur={this.onDataSourceOptionBlur('maxDataPoints')}
onChange={this.onDataSourceOptionChange('maxDataPoints')}
value={cacheTimeout}
/>
);
}
return null; // nothing to render
});
};
</div>
</div>
);
}
renderMaxDataPointsOption() {
const { data } = this.props;
const { maxDataPoints } = this.state;
const realMd = data.request?.maxDataPoints;
const isAuto = maxDataPoints === '';
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="text"
className="width-6"
placeholder={`${realMd}`}
name={name}
spellCheck={false}
onBlur={this.onDataSourceOptionBlur('maxDataPoints')}
onChange={this.onDataSourceOptionChange('maxDataPoints')}
value={maxDataPoints}
/>
{isAuto && (
<>
<div className="gf-form-label query-segment-operator">=</div>
<div className="gf-form-label">Width of panel</div>
</>
)}
</div>
</div>
);
}
renderIntervalOption() {
const { data } = this.props;
const { interval } = this.state;
const realInterval = data.request?.interval;
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={`${realInterval}`}
name={name}
spellCheck={false}
onBlur={this.onDataSourceOptionBlur('interval')}
onChange={this.onDataSourceOptionChange('interval')}
value={interval}
/>
</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">Max data points / time range</div>
</div>
</div>
</>
);
}
onOpenOptions = () => {
this.setState({ isOpen: true });
......@@ -215,8 +273,8 @@ export class QueryOptions extends PureComponent<Props, State> {
}
let intervalDesc = interval;
if (intervalDesc === '' && data.request) {
intervalDesc = `auto = ${data.request.interval}`;
if (data.request) {
intervalDesc = `${data.request.interval}`;
}
return (
......@@ -240,7 +298,9 @@ export class QueryOptions extends PureComponent<Props, State> {
onOpen={this.onOpenOptions}
onClose={this.onCloseOptions}
>
{this.renderOptions()}
{this.renderMaxDataPointsOption()}
{this.renderIntervalOption()}
{this.renderCacheTimeoutOption()}
<div className="gf-form">
<InlineFormLabel width={9}>Relative time</InlineFormLabel>
......
......@@ -31,7 +31,6 @@ interface ScenarioContext {
// Options used in setup
maxDataPoints?: number | null;
widthPixels: number;
dsInterval?: string;
minInterval?: string;
scopedVars: ScopedVars;
......@@ -53,7 +52,7 @@ function describeQueryRunnerScenario(description: string, scenarioFn: ScenarioFn
getTransformations: () => undefined,
};
const ctx: ScenarioContext = {
widthPixels: 200,
maxDataPoints: 200,
scopedVars: {
server: { text: 'Server1', value: 'server-1' },
},
......@@ -93,12 +92,11 @@ function describeQueryRunnerScenario(description: string, scenarioFn: ScenarioFn
datasource,
scopedVars: ctx.scopedVars,
minInterval: ctx.minInterval,
widthPixels: ctx.widthPixels,
maxDataPoints: ctx.maxDataPoints,
timeRange: {
from: grafanaData.dateTime().subtract(1, 'days'),
to: grafanaData.dateTime(),
raw: { from: '1h', to: 'now' },
raw: { from: '1d', to: 'now' },
},
panelId: 1,
queries: [{ refId: 'A', test: 1 }],
......@@ -137,10 +135,9 @@ describe('PanelQueryRunner', () => {
});
});
describeQueryRunnerScenario('with no maxDataPoints or minInterval', ctx => {
describeQueryRunnerScenario('with maxDataPoints', ctx => {
ctx.setup(() => {
ctx.maxDataPoints = null;
ctx.widthPixels = 200;
ctx.maxDataPoints = 200;
});
it('should return data', async () => {
......@@ -163,7 +160,7 @@ describe('PanelQueryRunner', () => {
describeQueryRunnerScenario('with no panel min interval but datasource min interval', ctx => {
ctx.setup(() => {
ctx.widthPixels = 20000;
ctx.maxDataPoints = 20000;
ctx.dsInterval = '15s';
});
......@@ -174,7 +171,7 @@ describe('PanelQueryRunner', () => {
describeQueryRunnerScenario('with panel min interval and data source min interval', ctx => {
ctx.setup(() => {
ctx.widthPixels = 20000;
ctx.maxDataPoints = 20000;
ctx.dsInterval = '15s';
ctx.minInterval = '30s';
});
......@@ -192,6 +189,10 @@ describe('PanelQueryRunner', () => {
it('should pass maxDataPoints if specified', async () => {
expect(ctx.queryCalledWith?.maxDataPoints).toBe(10);
});
it('should use instead of width to calculate interval', async () => {
expect(ctx.queryCalledWith?.interval).toBe('2h');
});
});
describeQueryRunnerScenario(
......
......@@ -38,8 +38,7 @@ export interface QueryRunnerOptions<
timezone?: string;
timeRange: TimeRange;
timeInfo?: string; // String description of time range for display
widthPixels: number;
maxDataPoints: number | undefined | null;
maxDataPoints: number;
minInterval: string | undefined | null;
scopedVars?: ScopedVars;
cacheTimeout?: string;
......@@ -115,7 +114,6 @@ export class PanelQueryRunner {
timeRange,
timeInfo,
cacheTimeout,
widthPixels,
maxDataPoints,
scopedVars,
minInterval,
......@@ -139,7 +137,7 @@ export class PanelQueryRunner {
interval: '',
intervalMs: 0,
targets: cloneDeep(queries),
maxDataPoints: maxDataPoints || widthPixels,
maxDataPoints: maxDataPoints,
scopedVars: scopedVars || {},
cacheTimeout,
startTime: Date.now(),
......@@ -160,7 +158,7 @@ export class PanelQueryRunner {
});
const lowerIntervalLimit = minInterval ? templateSrv.replace(minInterval, request.scopedVars) : ds.interval;
const norm = kbn.calculateInterval(timeRange, widthPixels, lowerIntervalLimit);
const norm = kbn.calculateInterval(timeRange, maxDataPoints, lowerIntervalLimit);
// make shallow copy of scoped vars,
// and add built in variables interval and interval_ms
......
......@@ -188,8 +188,7 @@ class MetricsPanelCtrl extends PanelCtrl {
timezone: this.dashboard.getTimezone(),
timeInfo: this.timeInfo,
timeRange: this.range,
widthPixels: this.width,
maxDataPoints: panel.maxDataPoints,
maxDataPoints: panel.maxDataPoints || this.width,
minInterval: panel.interval,
scopedVars: panel.scopedVars,
cacheTimeout: panel.cacheTimeout,
......
......@@ -25,12 +25,12 @@ export const PromSettings = (props: Props) => {
<FormField
label="Scrape interval"
labelWidth={13}
placeholder="15s"
inputEl={
<Input
className="width-6"
value={value.jsonData.timeInterval}
spellCheck={false}
placeholder="15s"
onChange={onChangeHandler('timeInterval', value, onChange)}
validationEvents={promSettingsValidationEvents}
/>
......
......@@ -90,6 +90,10 @@ $input-border: 1px solid $input-border-color;
.select-container {
margin-right: $space-xs;
}
.gf-form-spacing {
margin-right: $space-xs;
}
}
.gf-form-button-row {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment