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) => { ...@@ -109,7 +109,7 @@ const getQueryOperationRowStyles = stylesFactory((theme: GrafanaTheme) => {
`, `,
content: css` content: css`
margin-top: ${theme.spacing.inlineFormMargin}; 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> { ...@@ -200,8 +200,7 @@ export class PanelChrome extends PureComponent<Props, State> {
timezone: this.props.dashboard.getTimezone(), timezone: this.props.dashboard.getTimezone(),
timeRange: timeData.timeRange, timeRange: timeData.timeRange,
timeInfo: timeData.timeInfo, timeInfo: timeData.timeInfo,
widthPixels: width, maxDataPoints: panel.maxDataPoints || width,
maxDataPoints: panel.maxDataPoints,
minInterval: panel.interval, minInterval: panel.interval,
scopedVars: panel.scopedVars, scopedVars: panel.scopedVars,
cacheTimeout: panel.cacheTimeout, 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> { ...@@ -172,7 +172,15 @@ export class QueriesTab extends PureComponent<Props, State> {
<DataSourcePicker datasources={this.datasources} onChange={this.onChangeDataSource} current={currentDS} /> <DataSourcePicker datasources={this.datasources} onChange={this.onChangeDataSource} current={currentDS} />
</div> </div>
<div className={styles.dataSourceRowItem}> <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>
<div className={styles.dataSourceRowItem}> <div className={styles.dataSourceRowItem}>
<Button <Button
...@@ -183,9 +191,6 @@ export class QueriesTab extends PureComponent<Props, State> { ...@@ -183,9 +191,6 @@ export class QueriesTab extends PureComponent<Props, State> {
Query inspector Query inspector
</Button> </Button>
</div> </div>
<div className={styles.dataSourceRowItemOptions}>
<QueryOptions panel={panel} datasource={currentDS} data={data} />
</div>
</div> </div>
</div> </div>
); );
...@@ -327,6 +332,7 @@ const getStyles = stylesFactory(() => { ...@@ -327,6 +332,7 @@ const getStyles = stylesFactory(() => {
`, `,
dataSourceRowItemOptions: css` dataSourceRowItemOptions: css`
flex-grow: 1; flex-grow: 1;
margin-right: ${theme.spacing.inlineFormMargin};
`, `,
queriesWrapper: css` queriesWrapper: css`
padding-bottom: 16px; padding-bottom: 16px;
......
...@@ -13,8 +13,7 @@ import { ...@@ -13,8 +13,7 @@ import {
InlineFormLabel, InlineFormLabel,
stylesFactory, stylesFactory,
} from '@grafana/ui'; } from '@grafana/ui';
import { DataSourceOption } from './DataSourceOption'; const { Switch, Input } = LegacyForms;
const { Input, Switch } = LegacyForms;
// Types // Types
import { PanelModel } from '../state'; import { PanelModel } from '../state';
...@@ -57,46 +56,6 @@ interface State { ...@@ -57,46 +56,6 @@ interface State {
} }
export class QueryOptions extends PureComponent<Props, 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) { constructor(props: Props) {
super(props); super(props);
...@@ -127,6 +86,7 @@ export class QueryOptions extends PureComponent<Props, State> { ...@@ -127,6 +86,7 @@ export class QueryOptions extends PureComponent<Props, State> {
const { value } = event.target; const { value } = event.target;
const { panel } = this.props; const { panel } = this.props;
const emptyToNullValue = emptyToNull(value); const emptyToNullValue = emptyToNull(value);
if (status === LegacyInputStatus.Valid && panel.timeFrom !== emptyToNullValue) { if (status === LegacyInputStatus.Valid && panel.timeFrom !== emptyToNullValue) {
panel.timeFrom = emptyToNullValue; panel.timeFrom = emptyToNullValue;
panel.refresh(); panel.refresh();
...@@ -137,6 +97,7 @@ export class QueryOptions extends PureComponent<Props, State> { ...@@ -137,6 +97,7 @@ export class QueryOptions extends PureComponent<Props, State> {
const { value } = event.target; const { value } = event.target;
const { panel } = this.props; const { panel } = this.props;
const emptyToNullValue = emptyToNull(value); const emptyToNullValue = emptyToNull(value);
if (status === LegacyInputStatus.Valid && panel.timeShift !== emptyToNullValue) { if (status === LegacyInputStatus.Valid && panel.timeShift !== emptyToNullValue) {
panel.timeShift = emptyToNullValue; panel.timeShift = emptyToNullValue;
panel.refresh(); panel.refresh();
...@@ -163,35 +124,132 @@ export class QueryOptions extends PureComponent<Props, State> { ...@@ -163,35 +124,132 @@ export class QueryOptions extends PureComponent<Props, State> {
this.setState({ ...this.state, [panelKey]: event.target.value }); this.setState({ ...this.state, [panelKey]: event.target.value });
}; };
/** renderCacheTimeoutOption() {
* Show options for any value that is set, or values that the
* current datasource says it will use
*/
renderOptions = () => {
const { datasource } = this.props; const { datasource } = this.props;
const queryOptions: any = datasource.meta.queryOptions || {}; 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.`;
return Object.keys(this.allOptions).map(key => { if (!datasource.meta.queryOptions?.maxDataPoints) {
const options = this.allOptions[key]; return null;
const panelKey = options.panelKey || key; }
// @ts-ignore return (
const value = this.state[panelKey]; <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}
/>
</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;
if (queryOptions[key]) {
return ( return (
<DataSourceOption <>
key={key} <div className="gf-form-inline">
{...options} <div className="gf-form">
onChange={this.onDataSourceOptionChange(panelKey)} <InlineFormLabel
onBlur={this.onDataSourceOptionBlur(panelKey)} width={9}
value={value} 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>
</>
); );
} }
return null; // nothing to render
});
};
onOpenOptions = () => { onOpenOptions = () => {
this.setState({ isOpen: true }); this.setState({ isOpen: true });
...@@ -215,8 +273,8 @@ export class QueryOptions extends PureComponent<Props, State> { ...@@ -215,8 +273,8 @@ export class QueryOptions extends PureComponent<Props, State> {
} }
let intervalDesc = interval; let intervalDesc = interval;
if (intervalDesc === '' && data.request) { if (data.request) {
intervalDesc = `auto = ${data.request.interval}`; intervalDesc = `${data.request.interval}`;
} }
return ( return (
...@@ -240,7 +298,9 @@ export class QueryOptions extends PureComponent<Props, State> { ...@@ -240,7 +298,9 @@ export class QueryOptions extends PureComponent<Props, State> {
onOpen={this.onOpenOptions} onOpen={this.onOpenOptions}
onClose={this.onCloseOptions} onClose={this.onCloseOptions}
> >
{this.renderOptions()} {this.renderMaxDataPointsOption()}
{this.renderIntervalOption()}
{this.renderCacheTimeoutOption()}
<div className="gf-form"> <div className="gf-form">
<InlineFormLabel width={9}>Relative time</InlineFormLabel> <InlineFormLabel width={9}>Relative time</InlineFormLabel>
......
...@@ -31,7 +31,6 @@ interface ScenarioContext { ...@@ -31,7 +31,6 @@ interface ScenarioContext {
// Options used in setup // Options used in setup
maxDataPoints?: number | null; maxDataPoints?: number | null;
widthPixels: number;
dsInterval?: string; dsInterval?: string;
minInterval?: string; minInterval?: string;
scopedVars: ScopedVars; scopedVars: ScopedVars;
...@@ -53,7 +52,7 @@ function describeQueryRunnerScenario(description: string, scenarioFn: ScenarioFn ...@@ -53,7 +52,7 @@ function describeQueryRunnerScenario(description: string, scenarioFn: ScenarioFn
getTransformations: () => undefined, getTransformations: () => undefined,
}; };
const ctx: ScenarioContext = { const ctx: ScenarioContext = {
widthPixels: 200, maxDataPoints: 200,
scopedVars: { scopedVars: {
server: { text: 'Server1', value: 'server-1' }, server: { text: 'Server1', value: 'server-1' },
}, },
...@@ -93,12 +92,11 @@ function describeQueryRunnerScenario(description: string, scenarioFn: ScenarioFn ...@@ -93,12 +92,11 @@ function describeQueryRunnerScenario(description: string, scenarioFn: ScenarioFn
datasource, datasource,
scopedVars: ctx.scopedVars, scopedVars: ctx.scopedVars,
minInterval: ctx.minInterval, minInterval: ctx.minInterval,
widthPixels: ctx.widthPixels,
maxDataPoints: ctx.maxDataPoints, maxDataPoints: ctx.maxDataPoints,
timeRange: { timeRange: {
from: grafanaData.dateTime().subtract(1, 'days'), from: grafanaData.dateTime().subtract(1, 'days'),
to: grafanaData.dateTime(), to: grafanaData.dateTime(),
raw: { from: '1h', to: 'now' }, raw: { from: '1d', to: 'now' },
}, },
panelId: 1, panelId: 1,
queries: [{ refId: 'A', test: 1 }], queries: [{ refId: 'A', test: 1 }],
...@@ -137,10 +135,9 @@ describe('PanelQueryRunner', () => { ...@@ -137,10 +135,9 @@ describe('PanelQueryRunner', () => {
}); });
}); });
describeQueryRunnerScenario('with no maxDataPoints or minInterval', ctx => { describeQueryRunnerScenario('with maxDataPoints', ctx => {
ctx.setup(() => { ctx.setup(() => {
ctx.maxDataPoints = null; ctx.maxDataPoints = 200;
ctx.widthPixels = 200;
}); });
it('should return data', async () => { it('should return data', async () => {
...@@ -163,7 +160,7 @@ describe('PanelQueryRunner', () => { ...@@ -163,7 +160,7 @@ describe('PanelQueryRunner', () => {
describeQueryRunnerScenario('with no panel min interval but datasource min interval', ctx => { describeQueryRunnerScenario('with no panel min interval but datasource min interval', ctx => {
ctx.setup(() => { ctx.setup(() => {
ctx.widthPixels = 20000; ctx.maxDataPoints = 20000;
ctx.dsInterval = '15s'; ctx.dsInterval = '15s';
}); });
...@@ -174,7 +171,7 @@ describe('PanelQueryRunner', () => { ...@@ -174,7 +171,7 @@ describe('PanelQueryRunner', () => {
describeQueryRunnerScenario('with panel min interval and data source min interval', ctx => { describeQueryRunnerScenario('with panel min interval and data source min interval', ctx => {
ctx.setup(() => { ctx.setup(() => {
ctx.widthPixels = 20000; ctx.maxDataPoints = 20000;
ctx.dsInterval = '15s'; ctx.dsInterval = '15s';
ctx.minInterval = '30s'; ctx.minInterval = '30s';
}); });
...@@ -192,6 +189,10 @@ describe('PanelQueryRunner', () => { ...@@ -192,6 +189,10 @@ describe('PanelQueryRunner', () => {
it('should pass maxDataPoints if specified', async () => { it('should pass maxDataPoints if specified', async () => {
expect(ctx.queryCalledWith?.maxDataPoints).toBe(10); expect(ctx.queryCalledWith?.maxDataPoints).toBe(10);
}); });
it('should use instead of width to calculate interval', async () => {
expect(ctx.queryCalledWith?.interval).toBe('2h');
});
}); });
describeQueryRunnerScenario( describeQueryRunnerScenario(
......
...@@ -38,8 +38,7 @@ export interface QueryRunnerOptions< ...@@ -38,8 +38,7 @@ export interface QueryRunnerOptions<
timezone?: string; timezone?: string;
timeRange: TimeRange; timeRange: TimeRange;
timeInfo?: string; // String description of time range for display timeInfo?: string; // String description of time range for display
widthPixels: number; maxDataPoints: number;
maxDataPoints: number | undefined | null;
minInterval: string | undefined | null; minInterval: string | undefined | null;
scopedVars?: ScopedVars; scopedVars?: ScopedVars;
cacheTimeout?: string; cacheTimeout?: string;
...@@ -115,7 +114,6 @@ export class PanelQueryRunner { ...@@ -115,7 +114,6 @@ export class PanelQueryRunner {
timeRange, timeRange,
timeInfo, timeInfo,
cacheTimeout, cacheTimeout,
widthPixels,
maxDataPoints, maxDataPoints,
scopedVars, scopedVars,
minInterval, minInterval,
...@@ -139,7 +137,7 @@ export class PanelQueryRunner { ...@@ -139,7 +137,7 @@ export class PanelQueryRunner {
interval: '', interval: '',
intervalMs: 0, intervalMs: 0,
targets: cloneDeep(queries), targets: cloneDeep(queries),
maxDataPoints: maxDataPoints || widthPixels, maxDataPoints: maxDataPoints,
scopedVars: scopedVars || {}, scopedVars: scopedVars || {},
cacheTimeout, cacheTimeout,
startTime: Date.now(), startTime: Date.now(),
...@@ -160,7 +158,7 @@ export class PanelQueryRunner { ...@@ -160,7 +158,7 @@ export class PanelQueryRunner {
}); });
const lowerIntervalLimit = minInterval ? templateSrv.replace(minInterval, request.scopedVars) : ds.interval; 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, // make shallow copy of scoped vars,
// and add built in variables interval and interval_ms // and add built in variables interval and interval_ms
......
...@@ -188,8 +188,7 @@ class MetricsPanelCtrl extends PanelCtrl { ...@@ -188,8 +188,7 @@ class MetricsPanelCtrl extends PanelCtrl {
timezone: this.dashboard.getTimezone(), timezone: this.dashboard.getTimezone(),
timeInfo: this.timeInfo, timeInfo: this.timeInfo,
timeRange: this.range, timeRange: this.range,
widthPixels: this.width, maxDataPoints: panel.maxDataPoints || this.width,
maxDataPoints: panel.maxDataPoints,
minInterval: panel.interval, minInterval: panel.interval,
scopedVars: panel.scopedVars, scopedVars: panel.scopedVars,
cacheTimeout: panel.cacheTimeout, cacheTimeout: panel.cacheTimeout,
......
...@@ -25,12 +25,12 @@ export const PromSettings = (props: Props) => { ...@@ -25,12 +25,12 @@ export const PromSettings = (props: Props) => {
<FormField <FormField
label="Scrape interval" label="Scrape interval"
labelWidth={13} labelWidth={13}
placeholder="15s"
inputEl={ inputEl={
<Input <Input
className="width-6" className="width-6"
value={value.jsonData.timeInterval} value={value.jsonData.timeInterval}
spellCheck={false} spellCheck={false}
placeholder="15s"
onChange={onChangeHandler('timeInterval', value, onChange)} onChange={onChangeHandler('timeInterval', value, onChange)}
validationEvents={promSettingsValidationEvents} validationEvents={promSettingsValidationEvents}
/> />
......
...@@ -90,6 +90,10 @@ $input-border: 1px solid $input-border-color; ...@@ -90,6 +90,10 @@ $input-border: 1px solid $input-border-color;
.select-container { .select-container {
margin-right: $space-xs; margin-right: $space-xs;
} }
.gf-form-spacing {
margin-right: $space-xs;
}
} }
.gf-form-button-row { .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