mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: Run queries (#33423)
* alertingqueryrunner first edition * added so we always set name and uid when changing datasource. * wip. * wip * added support for canceling requests. * util for getting time ranges for expression queries * remove logs, store data in state * added structure for marble testing. * change so the expression buttons doesnt submit form. * fixed run button. * replaced mocks with implementation that will set default query + expression. * fixed so we set a datasource name for the default expression rule. * improving expression guard. * Update public/app/features/alerting/components/AlertingQueryEditor.tsx Co-authored-by: Hugo Häggmark <hugo.haggmark@grafana.com> * fixed some nits. * some refactoring after feedback. * use grafanthemev2 * rename grafanatheme * fixing so we convert to correct relative time range. * added some more tests. * fixing so duplicating query works. * added some more tests without marbles. * small refactoring to share code between runRequest and alerting query runner. Co-authored-by: Marcus Andersson <marcus.andersson@grafana.com> Co-authored-by: Hugo Häggmark <hugo.haggmark@grafana.com>
This commit is contained in:
parent
9510c4f112
commit
0006765a40
@ -1,4 +1,6 @@
|
||||
import { TimeRange } from '../types/time';
|
||||
import { dateTime, rangeUtil } from './index';
|
||||
import { timeRangeToRelative } from './rangeutil';
|
||||
|
||||
describe('Range Utils', () => {
|
||||
describe('relative time', () => {
|
||||
@ -58,4 +60,40 @@ describe('Range Utils', () => {
|
||||
expect(timeRange.to.valueOf()).toEqual(dateTime('2021-04-20T15:55:00Z').valueOf());
|
||||
});
|
||||
});
|
||||
|
||||
describe('timeRangeToRelative', () => {
|
||||
it('should convert now-15m to relaitve time range', () => {
|
||||
const now = dateTime('2021-04-20T15:55:00Z');
|
||||
const timeRange: TimeRange = {
|
||||
from: dateTime(now).subtract(15, 'minutes'),
|
||||
to: now,
|
||||
raw: {
|
||||
from: 'now-15m',
|
||||
to: 'now',
|
||||
},
|
||||
};
|
||||
|
||||
const relativeTimeRange = timeRangeToRelative(timeRange, now);
|
||||
|
||||
expect(relativeTimeRange.from).toEqual(900);
|
||||
expect(relativeTimeRange.to).toEqual(0);
|
||||
});
|
||||
|
||||
it('should convert now-2w, now-1w to relative range', () => {
|
||||
const now = dateTime('2021-04-20T15:55:00Z');
|
||||
const timeRange: TimeRange = {
|
||||
from: dateTime(now).subtract(2, 'weeks'),
|
||||
to: dateTime(now).subtract(1, 'week'),
|
||||
raw: {
|
||||
from: 'now-2w',
|
||||
to: 'now-1w',
|
||||
},
|
||||
};
|
||||
|
||||
const relativeTimeRange = timeRangeToRelative(timeRange, now);
|
||||
|
||||
expect(relativeTimeRange.from).toEqual(1209600);
|
||||
expect(relativeTimeRange.to).toEqual(604800);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -439,10 +439,9 @@ export function roundInterval(interval: number) {
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export function timeRangeToRelative(timeRange: TimeRange): RelativeTimeRange {
|
||||
const now = dateTime().unix();
|
||||
const from = (now - timeRange.from.unix()) / 1000;
|
||||
const to = (now - timeRange.to.unix()) / 1000;
|
||||
export function timeRangeToRelative(timeRange: TimeRange, now: DateTime = dateTime()): RelativeTimeRange {
|
||||
const from = now.unix() - timeRange.from.unix();
|
||||
const to = now.unix() - timeRange.to.unix();
|
||||
|
||||
return {
|
||||
from,
|
||||
|
@ -60,3 +60,15 @@ export function getDefaultTimeRange(): TimeRange {
|
||||
raw: { from: 'now-6h', to: 'now' },
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the default realtive time range.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export function getDefaultRelativeTimeRange(): RelativeTimeRange {
|
||||
return {
|
||||
from: 21600,
|
||||
to: 0,
|
||||
};
|
||||
}
|
||||
|
@ -1,15 +1,25 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { css } from '@emotion/css';
|
||||
import { DataSourceApi, GrafanaTheme } from '@grafana/data';
|
||||
import {
|
||||
DataQuery,
|
||||
getDefaultRelativeTimeRange,
|
||||
GrafanaTheme2,
|
||||
LoadingState,
|
||||
PanelData,
|
||||
RelativeTimeRange,
|
||||
} from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { Button, HorizontalGroup, Icon, stylesFactory, Tooltip } from '@grafana/ui';
|
||||
import { config, getDataSourceSrv } from '@grafana/runtime';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { AlertingQueryRows } from './AlertingQueryRows';
|
||||
import { dataSource as expressionDatasource, ExpressionDatasourceUID } from '../../expressions/ExpressionDatasource';
|
||||
import { getNextRefIdChar } from 'app/core/utils/query';
|
||||
import { defaultCondition } from '../../expressions/utils/expressionTypes';
|
||||
import { ExpressionQueryType } from '../../expressions/types';
|
||||
import { GrafanaQuery } from 'app/types/unified-alerting-dto';
|
||||
import { AlertingQueryRunner } from '../state/AlertingQueryRunner';
|
||||
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||
import { isExpressionQuery } from 'app/features/expressions/guards';
|
||||
|
||||
interface Props {
|
||||
value?: GrafanaQuery[];
|
||||
@ -17,33 +27,44 @@ interface Props {
|
||||
}
|
||||
|
||||
interface State {
|
||||
defaultDataSource?: DataSourceApi;
|
||||
panelDataByRefId: Record<string, PanelData>;
|
||||
}
|
||||
export class AlertingQueryEditor extends PureComponent<Props, State> {
|
||||
private runner: AlertingQueryRunner;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
this.state = { panelDataByRefId: {} };
|
||||
this.runner = new AlertingQueryRunner();
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
try {
|
||||
const defaultDataSource = await getDataSourceSrv().get();
|
||||
this.setState({ defaultDataSource });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
componentDidMount() {
|
||||
this.runner.get().subscribe((data) => {
|
||||
this.setState({ panelDataByRefId: data });
|
||||
});
|
||||
}
|
||||
|
||||
onRunQueries = () => {};
|
||||
componentWillUnmount() {
|
||||
this.runner.destroy();
|
||||
}
|
||||
|
||||
onRunQueries = () => {
|
||||
const { value = [] } = this.props;
|
||||
this.runner.run(value);
|
||||
};
|
||||
|
||||
onCancelQueries = () => {
|
||||
this.runner.cancel();
|
||||
};
|
||||
|
||||
onDuplicateQuery = (query: GrafanaQuery) => {
|
||||
const { onChange, value = [] } = this.props;
|
||||
onChange([...value, query]);
|
||||
onChange(addQuery(value, query));
|
||||
};
|
||||
|
||||
onNewAlertingQuery = () => {
|
||||
const { onChange, value = [] } = this.props;
|
||||
const { defaultDataSource } = this.state;
|
||||
const defaultDataSource = getDatasourceSrv().getInstanceSettings('default');
|
||||
|
||||
if (!defaultDataSource) {
|
||||
return;
|
||||
@ -104,9 +125,38 @@ export class AlertingQueryEditor extends PureComponent<Props, State> {
|
||||
);
|
||||
}
|
||||
|
||||
isRunning() {
|
||||
const data = Object.values(this.state.panelDataByRefId).find((d) => Boolean(d));
|
||||
return data?.state === LoadingState.Loading;
|
||||
}
|
||||
|
||||
renderRunQueryButton() {
|
||||
const isRunning = this.isRunning();
|
||||
const styles = getStyles(config.theme2);
|
||||
|
||||
if (isRunning) {
|
||||
return (
|
||||
<div className={styles.runWrapper}>
|
||||
<Button icon="fa fa-spinner" type="button" variant="destructive" onClick={this.onCancelQueries}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.runWrapper}>
|
||||
<Button icon="sync" type="button" onClick={this.onRunQueries}>
|
||||
Run queries
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { value = [] } = this.props;
|
||||
const styles = getStyles(config.theme);
|
||||
const styles = getStyles(config.theme2);
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<AlertingQueryRows
|
||||
@ -116,6 +166,7 @@ export class AlertingQueryEditor extends PureComponent<Props, State> {
|
||||
onRunQueries={this.onRunQueries}
|
||||
/>
|
||||
{this.renderAddQueryRow(styles)}
|
||||
{this.renderRunQueryButton()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -134,33 +185,36 @@ const addQuery = (
|
||||
model: {
|
||||
...queryToAdd.model,
|
||||
hide: false,
|
||||
refId: refId,
|
||||
},
|
||||
relativeTimeRange: {
|
||||
from: 21600,
|
||||
to: 0,
|
||||
refId,
|
||||
},
|
||||
relativeTimeRange: defaultTimeRange(queryToAdd.model),
|
||||
};
|
||||
|
||||
return [...queries, query];
|
||||
};
|
||||
|
||||
const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
const defaultTimeRange = (model: DataQuery): RelativeTimeRange | undefined => {
|
||||
if (isExpressionQuery(model)) {
|
||||
return;
|
||||
}
|
||||
return getDefaultRelativeTimeRange();
|
||||
};
|
||||
|
||||
const getStyles = stylesFactory((theme: GrafanaTheme2) => {
|
||||
return {
|
||||
container: css`
|
||||
background-color: ${theme.colors.panelBg};
|
||||
background-color: ${theme.colors.background.primary};
|
||||
height: 100%;
|
||||
`,
|
||||
refreshWrapper: css`
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
runWrapper: css`
|
||||
margin-top: ${theme.spacing(1)};
|
||||
`,
|
||||
editorWrapper: css`
|
||||
border: 1px solid ${theme.colors.panelBorder};
|
||||
border-radius: ${theme.border.radius.md};
|
||||
border: 1px solid ${theme.colors.border.medium};
|
||||
border-radius: ${theme.shape.borderRadius()};
|
||||
`,
|
||||
expressionButton: css`
|
||||
margin-right: ${theme.spacing.sm};
|
||||
margin-right: ${theme.spacing(0.5)};
|
||||
`,
|
||||
};
|
||||
});
|
||||
|
@ -76,6 +76,7 @@ export class AlertingQueryRows extends PureComponent<Props, State> {
|
||||
|
||||
onChangeQuery(query: DataQuery, index: number) {
|
||||
const { queries, onQueriesChange } = this.props;
|
||||
|
||||
onQueriesChange(
|
||||
queries.map((item, itemIndex) => {
|
||||
if (itemIndex !== index) {
|
||||
@ -112,6 +113,13 @@ export class AlertingQueryRows extends PureComponent<Props, State> {
|
||||
onQueriesChange(update);
|
||||
};
|
||||
|
||||
onDuplicateQuery = (query: DataQuery, source: GrafanaQuery): void => {
|
||||
this.props.onDuplicateQuery({
|
||||
...source,
|
||||
model: query,
|
||||
});
|
||||
};
|
||||
|
||||
getDataSourceSettings = (query: GrafanaQuery): DataSourceInstanceSettings | undefined => {
|
||||
return getDataSourceSrv().getInstanceSettings(query.datasourceUid);
|
||||
};
|
||||
@ -148,7 +156,7 @@ export class AlertingQueryRows extends PureComponent<Props, State> {
|
||||
query={query.model}
|
||||
onChange={(query) => this.onChangeQuery(query, index)}
|
||||
timeRange={
|
||||
!isExpressionQuery(query.model)
|
||||
!isExpressionQuery(query.model) && query.relativeTimeRange
|
||||
? rangeUtil.relativeToTimeRange(query.relativeTimeRange)
|
||||
: undefined
|
||||
}
|
||||
@ -158,7 +166,7 @@ export class AlertingQueryRows extends PureComponent<Props, State> {
|
||||
: undefined
|
||||
}
|
||||
onRemoveQuery={this.onRemoveQuery}
|
||||
onAddQuery={this.props.onDuplicateQuery}
|
||||
onAddQuery={(duplicate) => this.onDuplicateQuery(duplicate, query)}
|
||||
onRunQuery={this.props.onRunQueries}
|
||||
queries={queries}
|
||||
/>
|
||||
|
243
public/app/features/alerting/state/AlertingQueryRunner.test.ts
Normal file
243
public/app/features/alerting/state/AlertingQueryRunner.test.ts
Normal file
@ -0,0 +1,243 @@
|
||||
import {
|
||||
ArrayVector,
|
||||
DataFrame,
|
||||
DataFrameJSON,
|
||||
Field,
|
||||
FieldType,
|
||||
getDefaultRelativeTimeRange,
|
||||
LoadingState,
|
||||
rangeUtil,
|
||||
} from '@grafana/data';
|
||||
import { FetchResponse } from '@grafana/runtime';
|
||||
import { BackendSrv } from 'app/core/services/backend_srv';
|
||||
import { GrafanaQuery } from 'app/types/unified-alerting-dto';
|
||||
import { Observable, of, throwError } from 'rxjs';
|
||||
import { delay, take } from 'rxjs/operators';
|
||||
import { createFetchResponse } from 'test/helpers/createFetchResponse';
|
||||
import { AlertingQueryResponse, AlertingQueryRunner } from './AlertingQueryRunner';
|
||||
|
||||
describe('AlertingQueryRunner', () => {
|
||||
it('should successfully map response and return panel data by refId', async () => {
|
||||
const response = createFetchResponse<AlertingQueryResponse>({
|
||||
results: {
|
||||
A: { frames: [createDataFrameJSON([1, 2, 3])] },
|
||||
B: { frames: [createDataFrameJSON([5, 6])] },
|
||||
},
|
||||
});
|
||||
|
||||
const runner = new AlertingQueryRunner(
|
||||
mockBackendSrv({
|
||||
fetch: () => of(response),
|
||||
})
|
||||
);
|
||||
|
||||
const data = runner.get();
|
||||
runner.run([createQuery('A'), createQuery('B')]);
|
||||
|
||||
await expect(data.pipe(take(1))).toEmitValuesWith((values) => {
|
||||
const [data] = values;
|
||||
expect(data).toEqual({
|
||||
A: {
|
||||
annotations: [],
|
||||
state: LoadingState.Done,
|
||||
series: [
|
||||
expectDataFrameWithValues({
|
||||
time: [1620051612238, 1620051622238, 1620051632238],
|
||||
values: [1, 2, 3],
|
||||
}),
|
||||
],
|
||||
structureRev: 1,
|
||||
timeRange: expect.anything(),
|
||||
timings: {
|
||||
dataProcessingTime: expect.any(Number),
|
||||
},
|
||||
},
|
||||
B: {
|
||||
annotations: [],
|
||||
state: LoadingState.Done,
|
||||
series: [
|
||||
expectDataFrameWithValues({
|
||||
time: [1620051612238, 1620051622238],
|
||||
values: [5, 6],
|
||||
}),
|
||||
],
|
||||
structureRev: 1,
|
||||
timeRange: expect.anything(),
|
||||
timings: {
|
||||
dataProcessingTime: expect.any(Number),
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should successfully map response with sliding relative time range', async () => {
|
||||
const response = createFetchResponse<AlertingQueryResponse>({
|
||||
results: {
|
||||
A: { frames: [createDataFrameJSON([1, 2, 3])] },
|
||||
B: { frames: [createDataFrameJSON([5, 6])] },
|
||||
},
|
||||
});
|
||||
|
||||
const runner = new AlertingQueryRunner(
|
||||
mockBackendSrv({
|
||||
fetch: () => of(response),
|
||||
})
|
||||
);
|
||||
|
||||
const data = runner.get();
|
||||
runner.run([createQuery('A'), createQuery('B')]);
|
||||
|
||||
await expect(data.pipe(take(1))).toEmitValuesWith((values) => {
|
||||
const [data] = values;
|
||||
const relativeA = rangeUtil.timeRangeToRelative(data.A.timeRange);
|
||||
const relativeB = rangeUtil.timeRangeToRelative(data.B.timeRange);
|
||||
const expected = getDefaultRelativeTimeRange();
|
||||
|
||||
expect(relativeA).toEqual(expected);
|
||||
expect(relativeB).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
it('should emit loading state if response is slower then 200ms', async () => {
|
||||
const response = createFetchResponse<AlertingQueryResponse>({
|
||||
results: {
|
||||
A: { frames: [createDataFrameJSON([1, 2, 3])] },
|
||||
B: { frames: [createDataFrameJSON([5, 6])] },
|
||||
},
|
||||
});
|
||||
|
||||
const runner = new AlertingQueryRunner(
|
||||
mockBackendSrv({
|
||||
fetch: () => of(response).pipe(delay(210)),
|
||||
})
|
||||
);
|
||||
|
||||
const data = runner.get();
|
||||
runner.run([createQuery('A'), createQuery('B')]);
|
||||
|
||||
await expect(data.pipe(take(2))).toEmitValuesWith((values) => {
|
||||
const [loading, data] = values;
|
||||
|
||||
expect(loading.A.state).toEqual(LoadingState.Loading);
|
||||
expect(loading.B.state).toEqual(LoadingState.Loading);
|
||||
|
||||
expect(data).toEqual({
|
||||
A: {
|
||||
annotations: [],
|
||||
state: LoadingState.Done,
|
||||
series: [
|
||||
expectDataFrameWithValues({
|
||||
time: [1620051612238, 1620051622238, 1620051632238],
|
||||
values: [1, 2, 3],
|
||||
}),
|
||||
],
|
||||
structureRev: 2,
|
||||
timeRange: expect.anything(),
|
||||
timings: {
|
||||
dataProcessingTime: expect.any(Number),
|
||||
},
|
||||
},
|
||||
B: {
|
||||
annotations: [],
|
||||
state: LoadingState.Done,
|
||||
series: [
|
||||
expectDataFrameWithValues({
|
||||
time: [1620051612238, 1620051622238],
|
||||
values: [5, 6],
|
||||
}),
|
||||
],
|
||||
structureRev: 2,
|
||||
timeRange: expect.anything(),
|
||||
timings: {
|
||||
dataProcessingTime: expect.any(Number),
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should emit error state if fetch request fails', async () => {
|
||||
const error = new Error('could not query data');
|
||||
const runner = new AlertingQueryRunner(
|
||||
mockBackendSrv({
|
||||
fetch: () => throwError(error),
|
||||
})
|
||||
);
|
||||
|
||||
const data = runner.get();
|
||||
runner.run([createQuery('A'), createQuery('B')]);
|
||||
|
||||
await expect(data.pipe(take(1))).toEmitValuesWith((values) => {
|
||||
const [data] = values;
|
||||
|
||||
expect(data.A.state).toEqual(LoadingState.Error);
|
||||
expect(data.A.error).toEqual(error);
|
||||
|
||||
expect(data.B.state).toEqual(LoadingState.Error);
|
||||
expect(data.B.error).toEqual(error);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
type MockBackendSrvConfig = {
|
||||
fetch: () => Observable<FetchResponse<AlertingQueryResponse>>;
|
||||
};
|
||||
|
||||
const mockBackendSrv = ({ fetch }: MockBackendSrvConfig): BackendSrv => {
|
||||
return ({
|
||||
fetch,
|
||||
resolveCancelerIfExists: jest.fn(),
|
||||
} as unknown) as BackendSrv;
|
||||
};
|
||||
|
||||
const expectDataFrameWithValues = ({ time, values }: { time: number[]; values: number[] }): DataFrame => {
|
||||
return {
|
||||
fields: [
|
||||
{
|
||||
config: {},
|
||||
entities: {},
|
||||
name: 'time',
|
||||
state: null,
|
||||
type: FieldType.time,
|
||||
values: new ArrayVector(time),
|
||||
} as Field,
|
||||
{
|
||||
config: {},
|
||||
entities: {},
|
||||
name: 'value',
|
||||
state: null,
|
||||
type: FieldType.number,
|
||||
values: new ArrayVector(values),
|
||||
} as Field,
|
||||
],
|
||||
length: values.length,
|
||||
};
|
||||
};
|
||||
|
||||
const createDataFrameJSON = (values: number[]): DataFrameJSON => {
|
||||
const startTime = 1620051602238;
|
||||
const timeValues = values.map((_, index) => startTime + (index + 1) * 10000);
|
||||
|
||||
return {
|
||||
schema: {
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time },
|
||||
{ name: 'value', type: FieldType.number },
|
||||
],
|
||||
},
|
||||
data: {
|
||||
values: [timeValues, values],
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const createQuery = (refId: string): GrafanaQuery => {
|
||||
return {
|
||||
refId,
|
||||
queryType: '',
|
||||
datasourceUid: '',
|
||||
model: { refId },
|
||||
relativeTimeRange: getDefaultRelativeTimeRange(),
|
||||
};
|
||||
};
|
186
public/app/features/alerting/state/AlertingQueryRunner.ts
Normal file
186
public/app/features/alerting/state/AlertingQueryRunner.ts
Normal file
@ -0,0 +1,186 @@
|
||||
import { merge, Observable, of, OperatorFunction, ReplaySubject, timer, Unsubscribable } from 'rxjs';
|
||||
import { catchError, map, mapTo, share, takeUntil } from 'rxjs/operators';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import {
|
||||
dataFrameFromJSON,
|
||||
DataFrameJSON,
|
||||
getDefaultTimeRange,
|
||||
LoadingState,
|
||||
PanelData,
|
||||
rangeUtil,
|
||||
TimeRange,
|
||||
} from '@grafana/data';
|
||||
import { FetchResponse, toDataQueryError } from '@grafana/runtime';
|
||||
import { BackendSrv, getBackendSrv } from 'app/core/services/backend_srv';
|
||||
import { preProcessPanelData } from 'app/features/query/state/runRequest';
|
||||
import { GrafanaQuery } from 'app/types/unified-alerting-dto';
|
||||
import { getTimeRangeForExpression } from '../unified/utils/timeRange';
|
||||
import { isExpressionQuery } from 'app/features/expressions/guards';
|
||||
import { setStructureRevision } from 'app/features/query/state/processing/revision';
|
||||
import { cancelNetworkRequestsOnUnsubscribe } from 'app/features/query/state/processing/canceler';
|
||||
|
||||
export interface AlertingQueryResult {
|
||||
frames: DataFrameJSON[];
|
||||
}
|
||||
|
||||
export interface AlertingQueryResponse {
|
||||
results: Record<string, AlertingQueryResult>;
|
||||
}
|
||||
export class AlertingQueryRunner {
|
||||
private subject: ReplaySubject<Record<string, PanelData>>;
|
||||
private subscription?: Unsubscribable;
|
||||
private lastResult: Record<string, PanelData>;
|
||||
|
||||
constructor(private backendSrv = getBackendSrv()) {
|
||||
this.subject = new ReplaySubject(1);
|
||||
this.lastResult = {};
|
||||
}
|
||||
|
||||
get(): Observable<Record<string, PanelData>> {
|
||||
return this.subject.asObservable();
|
||||
}
|
||||
|
||||
run(queries: GrafanaQuery[]) {
|
||||
if (queries.length === 0) {
|
||||
const empty = initialState(queries, LoadingState.Done);
|
||||
return this.subject.next(empty);
|
||||
}
|
||||
|
||||
this.subscription = runRequest(this.backendSrv, queries).subscribe({
|
||||
next: (dataPerQuery) => {
|
||||
const nextResult = applyChange(dataPerQuery, (refId, data) => {
|
||||
const previous = this.lastResult[refId];
|
||||
const preProcessed = preProcessPanelData(data, previous);
|
||||
return setStructureRevision(preProcessed, previous);
|
||||
});
|
||||
|
||||
this.lastResult = nextResult;
|
||||
this.subject.next(this.lastResult);
|
||||
},
|
||||
error: (error: Error) => {
|
||||
this.lastResult = mapErrorToPanelData(this.lastResult, error);
|
||||
this.subject.next(this.lastResult);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
cancel() {
|
||||
if (!this.subscription) {
|
||||
return;
|
||||
}
|
||||
this.subscription.unsubscribe();
|
||||
|
||||
let requestIsRunning = false;
|
||||
|
||||
const nextResult = applyChange(this.lastResult, (refId, data) => {
|
||||
if (data.state === LoadingState.Loading) {
|
||||
requestIsRunning = true;
|
||||
}
|
||||
|
||||
return {
|
||||
...data,
|
||||
state: LoadingState.Done,
|
||||
};
|
||||
});
|
||||
|
||||
if (requestIsRunning) {
|
||||
this.subject.next(nextResult);
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (this.subject) {
|
||||
this.subject.complete();
|
||||
}
|
||||
this.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
const runRequest = (backendSrv: BackendSrv, queries: GrafanaQuery[]): Observable<Record<string, PanelData>> => {
|
||||
const initial = initialState(queries, LoadingState.Loading);
|
||||
const request = {
|
||||
data: { data: queries },
|
||||
url: '/api/v1/eval',
|
||||
method: 'POST',
|
||||
requestId: uuidv4(),
|
||||
};
|
||||
|
||||
const runningRequest = backendSrv.fetch<AlertingQueryResponse>(request).pipe(
|
||||
mapToPanelData(initial),
|
||||
catchError((error) => of(mapErrorToPanelData(initial, error))),
|
||||
cancelNetworkRequestsOnUnsubscribe(backendSrv, request.requestId),
|
||||
share()
|
||||
);
|
||||
|
||||
return merge(timer(200).pipe(mapTo(initial), takeUntil(runningRequest)), runningRequest);
|
||||
};
|
||||
|
||||
const initialState = (queries: GrafanaQuery[], state: LoadingState): Record<string, PanelData> => {
|
||||
return queries.reduce((dataByQuery: Record<string, PanelData>, query) => {
|
||||
dataByQuery[query.refId] = {
|
||||
state,
|
||||
series: [],
|
||||
timeRange: getTimeRange(query, queries),
|
||||
};
|
||||
|
||||
return dataByQuery;
|
||||
}, {});
|
||||
};
|
||||
|
||||
const getTimeRange = (query: GrafanaQuery, queries: GrafanaQuery[]): TimeRange => {
|
||||
if (isExpressionQuery(query.model)) {
|
||||
const relative = getTimeRangeForExpression(query.model, queries);
|
||||
return rangeUtil.relativeToTimeRange(relative);
|
||||
}
|
||||
|
||||
if (!query.relativeTimeRange) {
|
||||
console.warn(`Query with refId: ${query.refId} did not have any relative time range, using default.`);
|
||||
return getDefaultTimeRange();
|
||||
}
|
||||
|
||||
return rangeUtil.relativeToTimeRange(query.relativeTimeRange);
|
||||
};
|
||||
|
||||
const mapToPanelData = (
|
||||
dataByQuery: Record<string, PanelData>
|
||||
): OperatorFunction<FetchResponse<AlertingQueryResponse>, Record<string, PanelData>> => {
|
||||
return map((response) => {
|
||||
const { data } = response;
|
||||
const results: Record<string, PanelData> = {};
|
||||
|
||||
for (const [refId, result] of Object.entries(data.results)) {
|
||||
results[refId] = {
|
||||
timeRange: dataByQuery[refId].timeRange,
|
||||
state: LoadingState.Done,
|
||||
series: result.frames.map(dataFrameFromJSON),
|
||||
};
|
||||
}
|
||||
|
||||
return results;
|
||||
});
|
||||
};
|
||||
|
||||
const mapErrorToPanelData = (lastResult: Record<string, PanelData>, error: Error): Record<string, PanelData> => {
|
||||
const queryError = toDataQueryError(error);
|
||||
|
||||
return applyChange(lastResult, (refId, data) => {
|
||||
return {
|
||||
...data,
|
||||
state: LoadingState.Error,
|
||||
error: queryError,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const applyChange = (
|
||||
initial: Record<string, PanelData>,
|
||||
change: (refId: string, data: PanelData) => PanelData
|
||||
): Record<string, PanelData> => {
|
||||
const nextResult: Record<string, PanelData> = {};
|
||||
|
||||
for (const [refId, data] of Object.entries(initial)) {
|
||||
nextResult[refId] = change(refId, data);
|
||||
}
|
||||
|
||||
return nextResult;
|
||||
};
|
@ -16,7 +16,7 @@ import { saveRuleFormAction } from '../../state/actions';
|
||||
import { RuleWithLocation } from 'app/types/unified-alerting';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useCleanup } from 'app/core/hooks/useCleanup';
|
||||
import { rulerRuleToFormValues, defaultFormValues } from '../../utils/rule-form';
|
||||
import { rulerRuleToFormValues, defaultFormValues, getDefaultQueries } from '../../utils/rule-form';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
type Props = {
|
||||
@ -31,7 +31,10 @@ export const AlertRuleForm: FC<Props> = ({ existing }) => {
|
||||
if (existing) {
|
||||
return rulerRuleToFormValues(existing);
|
||||
}
|
||||
return defaultFormValues;
|
||||
return {
|
||||
...defaultFormValues,
|
||||
queries: getDefaultQueries(),
|
||||
};
|
||||
}, [existing]);
|
||||
|
||||
const formAPI = useForm<RuleFormValues>({
|
||||
|
@ -1,57 +0,0 @@
|
||||
export const SAMPLE_QUERIES = [
|
||||
{
|
||||
refId: 'A',
|
||||
queryType: '',
|
||||
relativeTimeRange: {
|
||||
from: 30,
|
||||
to: 0,
|
||||
},
|
||||
datasourceUid: '000000004',
|
||||
model: {
|
||||
intervalMs: 1000,
|
||||
maxDataPoints: 100,
|
||||
pulseWave: {
|
||||
offCount: 6,
|
||||
offValue: 1,
|
||||
onCount: 6,
|
||||
onValue: 10,
|
||||
timeStep: 5,
|
||||
},
|
||||
refId: 'A',
|
||||
scenarioId: 'predictable_pulse',
|
||||
stringInput: '',
|
||||
},
|
||||
},
|
||||
{
|
||||
refId: 'B',
|
||||
queryType: '',
|
||||
relativeTimeRange: {
|
||||
from: 0,
|
||||
to: 0,
|
||||
},
|
||||
datasourceUid: '-100',
|
||||
model: {
|
||||
conditions: [
|
||||
{
|
||||
evaluator: {
|
||||
params: [3],
|
||||
type: 'gt',
|
||||
},
|
||||
operator: {
|
||||
type: 'and',
|
||||
},
|
||||
query: {
|
||||
params: ['A'],
|
||||
},
|
||||
reducer: {
|
||||
type: 'last',
|
||||
},
|
||||
},
|
||||
],
|
||||
intervalMs: 1000,
|
||||
maxDataPoints: 100,
|
||||
refId: 'B',
|
||||
type: 'classic_conditions',
|
||||
},
|
||||
},
|
||||
];
|
@ -1,12 +1,17 @@
|
||||
import { getDefaultTimeRange, rangeUtil } from '@grafana/data';
|
||||
import { getDataSourceSrv } from '@grafana/runtime';
|
||||
import { ExpressionDatasourceID, ExpressionDatasourceUID } from 'app/features/expressions/ExpressionDatasource';
|
||||
import { ExpressionQuery, ExpressionQueryType } from 'app/features/expressions/types';
|
||||
import { RuleWithLocation } from 'app/types/unified-alerting';
|
||||
import {
|
||||
Annotations,
|
||||
GrafanaAlertState,
|
||||
GrafanaQuery,
|
||||
Labels,
|
||||
PostableRuleGrafanaRuleDTO,
|
||||
RulerAlertingRuleDTO,
|
||||
} from 'app/types/unified-alerting-dto';
|
||||
import { SAMPLE_QUERIES } from '../mocks/grafana-queries';
|
||||
import { EvalFunction } from '../../state/alertDef';
|
||||
import { RuleFormType, RuleFormValues } from '../types/rule-form';
|
||||
import { isGrafanaRulesSource } from './datasource';
|
||||
import { arrayToRecord, recordToArray } from './misc';
|
||||
@ -21,7 +26,7 @@ export const defaultFormValues: RuleFormValues = Object.freeze({
|
||||
|
||||
// threshold
|
||||
folder: null,
|
||||
queries: SAMPLE_QUERIES, // @TODO remove the sample eventually
|
||||
queries: [],
|
||||
condition: '',
|
||||
noDataState: GrafanaAlertState.NoData,
|
||||
execErrState: GrafanaAlertState.Alerting,
|
||||
@ -115,3 +120,63 @@ export function rulerRuleToFormValues(ruleWithLocation: RuleWithLocation): RuleF
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const getDefaultQueries = (): GrafanaQuery[] => {
|
||||
const dataSource = getDataSourceSrv().getInstanceSettings('default');
|
||||
|
||||
if (!dataSource) {
|
||||
return [getDefaultExpression('A')];
|
||||
}
|
||||
|
||||
const timeRange = getDefaultTimeRange();
|
||||
const relativeTimeRange = rangeUtil.timeRangeToRelative(timeRange);
|
||||
|
||||
return [
|
||||
{
|
||||
refId: 'A',
|
||||
datasourceUid: dataSource.uid,
|
||||
queryType: '',
|
||||
relativeTimeRange,
|
||||
model: {
|
||||
refId: 'A',
|
||||
hide: false,
|
||||
},
|
||||
},
|
||||
getDefaultExpression('B'),
|
||||
];
|
||||
};
|
||||
|
||||
const getDefaultExpression = (refId: string): GrafanaQuery => {
|
||||
const model: ExpressionQuery = {
|
||||
refId,
|
||||
hide: false,
|
||||
type: ExpressionQueryType.classic,
|
||||
datasource: ExpressionDatasourceID,
|
||||
conditions: [
|
||||
{
|
||||
type: 'query',
|
||||
evaluator: {
|
||||
params: [3],
|
||||
type: EvalFunction.IsAbove,
|
||||
},
|
||||
operator: {
|
||||
type: 'and',
|
||||
},
|
||||
query: {
|
||||
params: ['A'],
|
||||
},
|
||||
reducer: {
|
||||
params: [],
|
||||
type: 'last',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return {
|
||||
refId,
|
||||
datasourceUid: ExpressionDatasourceUID,
|
||||
queryType: '',
|
||||
model,
|
||||
};
|
||||
};
|
||||
|
204
public/app/features/alerting/unified/utils/timeRange.test.ts
Normal file
204
public/app/features/alerting/unified/utils/timeRange.test.ts
Normal file
@ -0,0 +1,204 @@
|
||||
import { ReducerID } from '@grafana/data';
|
||||
import { getTimeRangeForExpression } from './timeRange';
|
||||
import { defaultCondition } from 'app/features/expressions/utils/expressionTypes';
|
||||
import { ExpressionQuery, ExpressionQueryType } from 'app/features/expressions/types';
|
||||
import { GrafanaQuery } from 'app/types/unified-alerting-dto';
|
||||
|
||||
describe('timeRange', () => {
|
||||
describe('getTimeRangeForExpression', () => {
|
||||
describe('classic condition', () => {
|
||||
it('should return referenced query timeRange for classic condition', () => {
|
||||
const expressionQuery: GrafanaQuery = {
|
||||
refId: 'B',
|
||||
queryType: 'expression',
|
||||
datasourceUid: '-100',
|
||||
model: {
|
||||
queryType: 'query',
|
||||
datasource: '__expr__',
|
||||
refId: 'B',
|
||||
conditions: [{ ...defaultCondition, query: { params: ['A'] } }],
|
||||
type: ExpressionQueryType.classic,
|
||||
} as ExpressionQuery,
|
||||
};
|
||||
const query: GrafanaQuery = {
|
||||
refId: 'A',
|
||||
relativeTimeRange: { from: 300, to: 0 },
|
||||
queryType: 'query',
|
||||
datasourceUid: 'dsuid',
|
||||
model: { refId: 'A' },
|
||||
};
|
||||
const queries: GrafanaQuery[] = [query, expressionQuery];
|
||||
|
||||
expect(getTimeRangeForExpression(expressionQuery.model as ExpressionQuery, queries)).toEqual({
|
||||
from: 300,
|
||||
to: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return the min and max time range', () => {
|
||||
const expressionQuery: GrafanaQuery = {
|
||||
refId: 'C',
|
||||
queryType: 'expression',
|
||||
datasourceUid: '-100',
|
||||
model: {
|
||||
queryType: 'query',
|
||||
datasource: '__expr__',
|
||||
refId: 'C',
|
||||
conditions: [
|
||||
{ ...defaultCondition, query: { params: ['A'] } },
|
||||
{ ...defaultCondition, query: { params: ['B'] } },
|
||||
],
|
||||
type: ExpressionQueryType.classic,
|
||||
} as ExpressionQuery,
|
||||
};
|
||||
const queryA: GrafanaQuery = {
|
||||
refId: 'A',
|
||||
relativeTimeRange: { from: 300, to: 0 },
|
||||
datasourceUid: 'dsuid',
|
||||
model: { refId: 'A' },
|
||||
queryType: 'query',
|
||||
};
|
||||
const queryB: GrafanaQuery = {
|
||||
refId: 'B',
|
||||
relativeTimeRange: { from: 600, to: 300 },
|
||||
datasourceUid: 'dsuid',
|
||||
model: { refId: 'B' },
|
||||
queryType: 'query',
|
||||
};
|
||||
const queries: GrafanaQuery[] = [queryA, queryB, expressionQuery];
|
||||
|
||||
expect(getTimeRangeForExpression(expressionQuery.model as ExpressionQuery, queries)).toEqual({
|
||||
from: 600,
|
||||
to: 0,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('math', () => {
|
||||
it('should get timerange for referenced query', () => {
|
||||
const expressionQuery: GrafanaQuery = {
|
||||
refId: 'B',
|
||||
queryType: 'expression',
|
||||
datasourceUid: '-100',
|
||||
model: {
|
||||
queryType: 'query',
|
||||
datasource: '__expr__',
|
||||
refId: 'B',
|
||||
expression: '$A > 10',
|
||||
type: ExpressionQueryType.math,
|
||||
} as ExpressionQuery,
|
||||
};
|
||||
|
||||
const query: GrafanaQuery = {
|
||||
refId: 'A',
|
||||
datasourceUid: 'dsuid',
|
||||
relativeTimeRange: { from: 300, to: 0 },
|
||||
model: { refId: 'A' },
|
||||
queryType: 'query',
|
||||
};
|
||||
|
||||
expect(getTimeRangeForExpression(expressionQuery.model as ExpressionQuery, [expressionQuery, query]));
|
||||
});
|
||||
|
||||
it('should get time ranges for multiple referenced queries', () => {
|
||||
const expressionQuery: GrafanaQuery = {
|
||||
refId: 'C',
|
||||
queryType: 'expression',
|
||||
datasourceUid: '-100',
|
||||
model: {
|
||||
queryType: 'query',
|
||||
datasource: '__expr__',
|
||||
refId: 'C',
|
||||
expression: '$A > 10 && $queryB > 20',
|
||||
type: ExpressionQueryType.math,
|
||||
} as ExpressionQuery,
|
||||
};
|
||||
|
||||
const queryA: GrafanaQuery = {
|
||||
refId: 'A',
|
||||
relativeTimeRange: { from: 300, to: 0 },
|
||||
datasourceUid: 'dsuid',
|
||||
model: { refId: 'A' },
|
||||
queryType: 'query',
|
||||
};
|
||||
|
||||
const queryB: GrafanaQuery = {
|
||||
refId: 'queryB',
|
||||
relativeTimeRange: { from: 600, to: 300 },
|
||||
datasourceUid: 'dsuid',
|
||||
model: { refId: 'queryB' },
|
||||
queryType: 'query',
|
||||
};
|
||||
|
||||
expect(
|
||||
getTimeRangeForExpression(expressionQuery.model as ExpressionQuery, [expressionQuery, queryA, queryB])
|
||||
).toEqual({ from: 600, to: 0 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('resample', () => {
|
||||
it('should get referenced timerange for resample expression', () => {
|
||||
const expressionQuery: GrafanaQuery = {
|
||||
refId: 'B',
|
||||
queryType: 'expression',
|
||||
datasourceUid: '-100',
|
||||
model: {
|
||||
queryType: 'query',
|
||||
datasource: '__expr__',
|
||||
refId: 'B',
|
||||
expression: 'A',
|
||||
type: ExpressionQueryType.resample,
|
||||
window: '10s',
|
||||
} as ExpressionQuery,
|
||||
};
|
||||
|
||||
const queryA: GrafanaQuery = {
|
||||
refId: 'A',
|
||||
relativeTimeRange: { from: 300, to: 0 },
|
||||
datasourceUid: 'dsuid',
|
||||
model: { refId: 'A' },
|
||||
queryType: 'query',
|
||||
};
|
||||
|
||||
const queries = [queryA, expressionQuery];
|
||||
|
||||
expect(getTimeRangeForExpression(expressionQuery.model as ExpressionQuery, queries)).toEqual({
|
||||
from: 300,
|
||||
to: 0,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('reduce', () => {
|
||||
it('should get referenced timerange for reduce expression', () => {
|
||||
const expressionQuery: GrafanaQuery = {
|
||||
refId: 'B',
|
||||
queryType: 'expression',
|
||||
datasourceUid: '-100',
|
||||
model: {
|
||||
queryType: 'query',
|
||||
datasource: '__expr__',
|
||||
refId: 'B',
|
||||
expression: 'A',
|
||||
type: ExpressionQueryType.reduce,
|
||||
reducer: ReducerID.max,
|
||||
} as ExpressionQuery,
|
||||
};
|
||||
|
||||
const queryA: GrafanaQuery = {
|
||||
refId: 'A',
|
||||
relativeTimeRange: { from: 300, to: 0 },
|
||||
datasourceUid: 'dsuid',
|
||||
model: { refId: 'A' },
|
||||
queryType: 'query',
|
||||
};
|
||||
|
||||
const queries = [queryA, expressionQuery];
|
||||
|
||||
expect(getTimeRangeForExpression(expressionQuery.model as ExpressionQuery, queries)).toEqual({
|
||||
from: 300,
|
||||
to: 0,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
76
public/app/features/alerting/unified/utils/timeRange.ts
Normal file
76
public/app/features/alerting/unified/utils/timeRange.ts
Normal file
@ -0,0 +1,76 @@
|
||||
import { RelativeTimeRange } from '@grafana/data';
|
||||
import { GrafanaQuery } from 'app/types/unified-alerting-dto';
|
||||
import { ExpressionQuery, ExpressionQueryType } from '../../../expressions/types';
|
||||
|
||||
const FALL_BACK_TIME_RANGE = { from: 21600, to: 0 };
|
||||
|
||||
export const getTimeRangeForExpression = (query: ExpressionQuery, queries: GrafanaQuery[]): RelativeTimeRange => {
|
||||
const referencedRefIds: string[] | undefined = getReferencedIds(query, queries);
|
||||
|
||||
if (!referencedRefIds) {
|
||||
return FALL_BACK_TIME_RANGE;
|
||||
}
|
||||
|
||||
const { from, to } = getTimeRanges(referencedRefIds, queries);
|
||||
|
||||
if (!from.length && !to.length) {
|
||||
return FALL_BACK_TIME_RANGE;
|
||||
}
|
||||
|
||||
return {
|
||||
from: Math.max(...from),
|
||||
to: Math.min(...to),
|
||||
};
|
||||
};
|
||||
|
||||
const getReferencedIds = (model: ExpressionQuery, queries: GrafanaQuery[]): string[] | undefined => {
|
||||
switch (model.type) {
|
||||
case ExpressionQueryType.classic:
|
||||
return getReferencedIdsForClassicCondition(model);
|
||||
case ExpressionQueryType.math:
|
||||
return getReferencedIdsForMath(model, queries);
|
||||
case ExpressionQueryType.resample:
|
||||
case ExpressionQueryType.reduce:
|
||||
return getReferencedIdsForReduce(model);
|
||||
}
|
||||
};
|
||||
|
||||
const getReferencedIdsForClassicCondition = (model: ExpressionQuery) => {
|
||||
return model.conditions?.map((condition) => {
|
||||
return condition.query.params[0];
|
||||
});
|
||||
};
|
||||
|
||||
const getTimeRanges = (referencedRefIds: string[], queries: GrafanaQuery[]) => {
|
||||
let from: number[] = [];
|
||||
let to = [FALL_BACK_TIME_RANGE.to];
|
||||
for (const referencedRefIdsKey of referencedRefIds) {
|
||||
const query = queries.find((query) => query.refId === referencedRefIdsKey);
|
||||
|
||||
if (!query || !query.relativeTimeRange) {
|
||||
continue;
|
||||
}
|
||||
from.push(query.relativeTimeRange.from);
|
||||
to.push(query.relativeTimeRange.to);
|
||||
}
|
||||
|
||||
return {
|
||||
from,
|
||||
to,
|
||||
};
|
||||
};
|
||||
|
||||
const getReferencedIdsForMath = (model: ExpressionQuery, queries: GrafanaQuery[]) => {
|
||||
return (
|
||||
queries
|
||||
// filter queries of type query and filter expression on if it includes any refIds
|
||||
.filter((q) => q.queryType === 'query' && model.expression?.includes(q.refId))
|
||||
.map((q) => {
|
||||
return q.refId;
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const getReferencedIdsForReduce = (model: ExpressionQuery) => {
|
||||
return model.expression ? [model.expression] : undefined;
|
||||
};
|
@ -76,7 +76,7 @@ export const ClassicConditions: FC<Props> = ({ onChange, query, refIds }) => {
|
||||
</div>
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
<Button variant="secondary" onClick={onAddCondition}>
|
||||
<Button variant="secondary" type="button" onClick={onAddCondition}>
|
||||
<Icon name="plus-circle" />
|
||||
</Button>
|
||||
</div>
|
||||
|
@ -122,7 +122,7 @@ export const Condition: FC<Props> = ({ condition, index, onChange, onRemoveCondi
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<Button variant="secondary" onClick={() => onRemoveCondition(index)}>
|
||||
<Button variant="secondary" type="button" onClick={() => onRemoveCondition(index)}>
|
||||
<Icon name="trash-alt" />
|
||||
</Button>
|
||||
</InlineFieldRow>
|
||||
|
@ -1,7 +1,20 @@
|
||||
import { DataQuery } from '@grafana/data';
|
||||
import { ExpressionDatasourceID } from './ExpressionDatasource';
|
||||
import { ExpressionQuery } from './types';
|
||||
import { ExpressionQuery, ExpressionQueryType } from './types';
|
||||
|
||||
export const isExpressionQuery = (dataQuery?: DataQuery): dataQuery is ExpressionQuery => {
|
||||
return dataQuery?.datasource === ExpressionDatasourceID;
|
||||
if (!dataQuery) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (dataQuery.datasource === ExpressionDatasourceID) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const expression = dataQuery as ExpressionQuery;
|
||||
|
||||
if (typeof expression.type !== 'string') {
|
||||
return false;
|
||||
}
|
||||
return Object.values(ExpressionQueryType).includes(expression.type);
|
||||
};
|
||||
|
@ -8,8 +8,6 @@ import {
|
||||
QueryRunnerOptions,
|
||||
QueryRunner as QueryRunnerSrv,
|
||||
LoadingState,
|
||||
compareArrayValues,
|
||||
compareDataFrameStructures,
|
||||
} from '@grafana/data';
|
||||
import { getTemplateSrv } from '@grafana/runtime';
|
||||
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||
@ -17,6 +15,7 @@ import { cloneDeep } from 'lodash';
|
||||
import { from, Observable, ReplaySubject, Unsubscribable } from 'rxjs';
|
||||
import { first } from 'rxjs/operators';
|
||||
import { getNextRequestId } from './PanelQueryRunner';
|
||||
import { setStructureRevision } from './processing/revision';
|
||||
import { preProcessPanelData, runRequest } from './runRequest';
|
||||
|
||||
export class QueryRunner implements QueryRunnerSrv {
|
||||
@ -102,23 +101,7 @@ export class QueryRunner implements QueryRunnerSrv {
|
||||
this.subscription = runRequest(ds, request).subscribe({
|
||||
next: (data) => {
|
||||
const results = preProcessPanelData(data, this.lastResult);
|
||||
|
||||
// Indicate if the structure has changed since the last query
|
||||
let structureRev = 1;
|
||||
if (this.lastResult?.structureRev && this.lastResult.series) {
|
||||
structureRev = this.lastResult.structureRev;
|
||||
const sameStructure = compareArrayValues(
|
||||
results.series,
|
||||
this.lastResult.series,
|
||||
compareDataFrameStructures
|
||||
);
|
||||
if (!sameStructure) {
|
||||
structureRev++;
|
||||
}
|
||||
}
|
||||
results.structureRev = structureRev;
|
||||
this.lastResult = results;
|
||||
|
||||
this.lastResult = setStructureRevision(results, this.lastResult);
|
||||
// Store preprocessed query results for applying overrides later on in the pipeline
|
||||
this.subject.next(this.lastResult);
|
||||
},
|
||||
|
14
public/app/features/query/state/processing/canceler.ts
Normal file
14
public/app/features/query/state/processing/canceler.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { BackendSrv } from 'app/core/services/backend_srv';
|
||||
import { MonoTypeOperatorFunction } from 'rxjs';
|
||||
import { finalize } from 'rxjs/operators';
|
||||
|
||||
export function cancelNetworkRequestsOnUnsubscribe<T>(
|
||||
backendSrv: BackendSrv,
|
||||
requestId: string | undefined
|
||||
): MonoTypeOperatorFunction<T> {
|
||||
return finalize(() => {
|
||||
if (requestId) {
|
||||
backendSrv.resolveCancelerIfExists(requestId);
|
||||
}
|
||||
});
|
||||
}
|
16
public/app/features/query/state/processing/revision.ts
Normal file
16
public/app/features/query/state/processing/revision.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { compareArrayValues, compareDataFrameStructures, PanelData } from '@grafana/data';
|
||||
|
||||
export const setStructureRevision = (result: PanelData, lastResult: PanelData | undefined) => {
|
||||
let structureRev = 1;
|
||||
|
||||
if (lastResult?.structureRev && lastResult.series) {
|
||||
structureRev = lastResult.structureRev;
|
||||
const sameStructure = compareArrayValues(result.series, lastResult.series, compareDataFrameStructures);
|
||||
if (!sameStructure) {
|
||||
structureRev++;
|
||||
}
|
||||
}
|
||||
|
||||
result.structureRev = structureRev;
|
||||
return result;
|
||||
};
|
@ -1,7 +1,7 @@
|
||||
// Libraries
|
||||
import { from, merge, Observable, of, timer } from 'rxjs';
|
||||
import { isString, map as isArray } from 'lodash';
|
||||
import { catchError, finalize, map, mapTo, share, takeUntil, tap } from 'rxjs/operators';
|
||||
import { catchError, map, mapTo, share, takeUntil, tap } from 'rxjs/operators';
|
||||
// Utils & Services
|
||||
import { backendSrv } from 'app/core/services/backend_srv';
|
||||
// Types
|
||||
@ -28,6 +28,7 @@ import {
|
||||
ExpressionDatasourceUID,
|
||||
} from 'app/features/expressions/ExpressionDatasource';
|
||||
import { ExpressionQuery } from 'app/features/expressions/types';
|
||||
import { cancelNetworkRequestsOnUnsubscribe } from './processing/canceler';
|
||||
|
||||
type MapOfResponsePackets = { [str: string]: DataQueryResponse };
|
||||
|
||||
@ -155,7 +156,7 @@ export function runRequest(
|
||||
tap(emitDataRequestEvent(datasource)),
|
||||
// finalize is triggered when subscriber unsubscribes
|
||||
// This makes sure any still running network requests are cancelled
|
||||
finalize(cancelNetworkRequestsOnUnsubscribe(request)),
|
||||
cancelNetworkRequestsOnUnsubscribe(backendSrv, request.requestId),
|
||||
// this makes it possible to share this observable in takeUntil
|
||||
share()
|
||||
);
|
||||
@ -166,12 +167,6 @@ export function runRequest(
|
||||
return merge(timer(200).pipe(mapTo(state.panelData), takeUntil(dataObservable)), dataObservable);
|
||||
}
|
||||
|
||||
function cancelNetworkRequestsOnUnsubscribe(req: DataQueryRequest) {
|
||||
return () => {
|
||||
backendSrv.resolveCancelerIfExists(req.requestId);
|
||||
};
|
||||
}
|
||||
|
||||
export function callQueryMethod(
|
||||
datasource: DataSourceApi,
|
||||
request: DataQueryRequest,
|
||||
|
@ -95,7 +95,7 @@ export enum GrafanaAlertState {
|
||||
export interface GrafanaQuery {
|
||||
refId: string;
|
||||
queryType: string;
|
||||
relativeTimeRange: RelativeTimeRange;
|
||||
relativeTimeRange?: RelativeTimeRange;
|
||||
datasourceUid: string;
|
||||
model: DataQuery;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user