diff --git a/packages/grafana-data/src/types/datasource.ts b/packages/grafana-data/src/types/datasource.ts index d1fea53c2a4..3c074821a57 100644 --- a/packages/grafana-data/src/types/datasource.ts +++ b/packages/grafana-data/src/types/datasource.ts @@ -240,6 +240,15 @@ abstract class DataSourceApi< */ abstract testDatasource(): Promise; + /** + * Override to skip executing a query + * + * @returns false if the query should be skipped + * + * @virtual + */ + filterQuery?(query: TQuery): boolean; + /** * Get hints for query improvements */ diff --git a/public/app/features/alerting/unified/RuleViewer.test.tsx b/public/app/features/alerting/unified/RuleViewer.test.tsx index 71eacc65f22..69f13c65ff9 100644 --- a/public/app/features/alerting/unified/RuleViewer.test.tsx +++ b/public/app/features/alerting/unified/RuleViewer.test.tsx @@ -19,6 +19,10 @@ jest.mock('@grafana/runtime', () => ({ getDataSourceSrv: () => { return { getInstanceSettings: () => ({ name: 'prometheus' }), + get: () => + Promise.resolve({ + filterQuery: () => true, + }), }; }, })); diff --git a/public/app/features/alerting/unified/components/rule-editor/QueryRows.tsx b/public/app/features/alerting/unified/components/rule-editor/QueryRows.tsx index d6377747a12..cda10ba55b9 100644 --- a/public/app/features/alerting/unified/components/rule-editor/QueryRows.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/QueryRows.tsx @@ -136,6 +136,7 @@ export class QueryRows extends PureComponent { return { ...item, refId: query.refId, + queryType: item.model.queryType ?? '', model: { ...item.model, ...query, diff --git a/public/app/features/alerting/unified/components/rule-editor/VizWrapper.tsx b/public/app/features/alerting/unified/components/rule-editor/VizWrapper.tsx index c4a59c9a194..a033016d428 100644 --- a/public/app/features/alerting/unified/components/rule-editor/VizWrapper.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/VizWrapper.tsx @@ -24,7 +24,8 @@ export const VizWrapper: FC = ({ data, currentPanel, changePanel, onThres }); const vizHeight = useVizHeight(data, currentPanel, options.frameIndex); const styles = useStyles2(getStyles(vizHeight)); - const [fieldConfig, setFieldConfig] = useState(defaultFieldConfig(thresholds)); + + const [fieldConfig, setFieldConfig] = useState(defaultFieldConfig(thresholds, data)); useEffect(() => { setFieldConfig((fieldConfig) => ({ @@ -32,6 +33,7 @@ export const VizWrapper: FC = ({ data, currentPanel, changePanel, onThres defaults: { ...fieldConfig.defaults, thresholds: thresholds, + unit: defaultUnit(data), custom: { ...fieldConfig.defaults.custom, thresholdsStyle: { @@ -40,7 +42,7 @@ export const VizWrapper: FC = ({ data, currentPanel, changePanel, onThres }, }, })); - }, [thresholds, setFieldConfig]); + }, [thresholds, setFieldConfig, data]); const context: PanelContext = useMemo( () => ({ @@ -98,13 +100,19 @@ const getStyles = (visHeight: number) => (theme: GrafanaTheme2) => ({ `, }); -function defaultFieldConfig(thresholds: ThresholdsConfig): FieldConfigSource { +function defaultUnit(data: PanelData): string | undefined { + return data.series[0]?.fields.find((field) => field.type === 'number')?.config.unit; +} + +function defaultFieldConfig(thresholds: ThresholdsConfig, data: PanelData): FieldConfigSource { if (!thresholds) { return { defaults: {}, overrides: [] }; } + return { defaults: { thresholds: thresholds, + unit: defaultUnit(data), custom: { thresholdsStyle: { mode: 'line', diff --git a/public/app/features/alerting/unified/state/AlertingQueryRunner.test.ts b/public/app/features/alerting/unified/state/AlertingQueryRunner.test.ts index 68ea5be3962..84a0a79edb3 100644 --- a/public/app/features/alerting/unified/state/AlertingQueryRunner.test.ts +++ b/public/app/features/alerting/unified/state/AlertingQueryRunner.test.ts @@ -2,13 +2,14 @@ import { ArrayVector, DataFrame, DataFrameJSON, + DataSourceApi, Field, FieldType, getDefaultRelativeTimeRange, LoadingState, rangeUtil, } from '@grafana/data'; -import { FetchResponse } from '@grafana/runtime'; +import { DataSourceSrv, FetchResponse } from '@grafana/runtime'; import { BackendSrv } from 'app/core/services/backend_srv'; import { AlertQuery } from 'app/types/unified-alerting-dto'; import { Observable, of, throwError } from 'rxjs'; @@ -28,7 +29,8 @@ describe('AlertingQueryRunner', () => { const runner = new AlertingQueryRunner( mockBackendSrv({ fetch: () => of(response), - }) + }), + mockDataSourceSrv() ); const data = runner.get(); @@ -82,7 +84,8 @@ describe('AlertingQueryRunner', () => { const runner = new AlertingQueryRunner( mockBackendSrv({ fetch: () => of(response), - }) + }), + mockDataSourceSrv() ); const data = runner.get(); @@ -110,7 +113,8 @@ describe('AlertingQueryRunner', () => { const runner = new AlertingQueryRunner( mockBackendSrv({ fetch: () => of(response).pipe(delay(210)), - }) + }), + mockDataSourceSrv() ); const data = runner.get(); @@ -162,7 +166,8 @@ describe('AlertingQueryRunner', () => { const runner = new AlertingQueryRunner( mockBackendSrv({ fetch: () => throwError(error), - }) + }), + mockDataSourceSrv() ); const data = runner.get(); @@ -178,6 +183,28 @@ describe('AlertingQueryRunner', () => { expect(data.B.error).toEqual(error); }); }); + + it('should not execute if a query fails filterQuery check', async () => { + const runner = new AlertingQueryRunner( + mockBackendSrv({ + fetch: () => throwError(new Error("shouldn't happen")), + }), + mockDataSourceSrv({ filterQuery: () => false }) + ); + + 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.Done); + expect(data.A.series).toHaveLength(0); + + expect(data.B.state).toEqual(LoadingState.Done); + expect(data.B.series).toHaveLength(0); + }); + }); }); type MockBackendSrvConfig = { @@ -191,6 +218,12 @@ const mockBackendSrv = ({ fetch }: MockBackendSrvConfig): BackendSrv => { } as unknown) as BackendSrv; }; +const mockDataSourceSrv = (dsApi?: Partial) => { + return ({ + get: () => Promise.resolve(dsApi ?? {}), + } as unknown) as DataSourceSrv; +}; + const expectDataFrameWithValues = ({ time, values }: { time: number[]; values: number[] }): DataFrame => { return { fields: [ diff --git a/public/app/features/alerting/unified/state/AlertingQueryRunner.ts b/public/app/features/alerting/unified/state/AlertingQueryRunner.ts index 8d34bea192c..4822898c950 100644 --- a/public/app/features/alerting/unified/state/AlertingQueryRunner.ts +++ b/public/app/features/alerting/unified/state/AlertingQueryRunner.ts @@ -11,7 +11,7 @@ import { TimeRange, withLoadingIndicator, } from '@grafana/data'; -import { FetchResponse, toDataQueryError } from '@grafana/runtime'; +import { FetchResponse, getDataSourceSrv, toDataQueryError } from '@grafana/runtime'; import { BackendSrv, getBackendSrv } from 'app/core/services/backend_srv'; import { preProcessPanelData } from 'app/features/query/state/runRequest'; import { AlertQuery } from 'app/types/unified-alerting-dto'; @@ -32,7 +32,7 @@ export class AlertingQueryRunner { private subscription?: Unsubscribable; private lastResult: Record; - constructor(private backendSrv = getBackendSrv()) { + constructor(private backendSrv = getBackendSrv(), private dataSourceSrv = getDataSourceSrv()) { this.subject = new ReplaySubject(1); this.lastResult = {}; } @@ -41,12 +41,24 @@ export class AlertingQueryRunner { return this.subject.asObservable(); } - run(queries: AlertQuery[]) { + async run(queries: AlertQuery[]) { if (queries.length === 0) { const empty = initialState(queries, LoadingState.Done); return this.subject.next(empty); } + // do not execute if one more of the queries are not runnable, + // for example not completely configured + for (const query of queries) { + if (!isExpressionQuery(query.model)) { + const ds = await this.dataSourceSrv.get(query.datasourceUid); + if (ds.filterQuery && !ds.filterQuery(query.model)) { + 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) => { diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/datasource.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/datasource.ts index 4a5d103c6ef..c903011d3d1 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/datasource.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/datasource.ts @@ -73,6 +73,14 @@ export default class Datasource extends DataSourceApi): Observable { const byType = new Map>();