Datasource: Change query filtering (#84656)

* call filterQuery from queryrunner

* test query hide filtering

* fix more broken tests

* lint errrors

* remove redundant filterQuery call

* skip filter in variable queries

* fix broken cypress test

* change tooltip text

* fix translations

* fix comments

* do not execute query is targets are empty

* add more tests

* remove unsued import

* update translations

* revert id change

* change header text

* update comment for hide prop

* rename hide query prop

* change tooltip and introduce different toggle state text

* update tests

* update comment and regenerate types

* run extract again

* fix broken e2e test

* track event

* fix build issues

* revert changes in wire file
This commit is contained in:
Erik Sundell 2024-03-21 13:39:39 +01:00 committed by GitHub
parent 410f5e3e3a
commit 29d4f6a217
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 476 additions and 214 deletions

View File

@ -62,16 +62,16 @@ describe('Panel edit tests - queries', () => {
expect(resultIds.has('B:')).equals(true); expect(resultIds.has('B:')).equals(true);
}); });
// Disable row with refId A // Hide response for row with refId A
e2e.components.QueryEditorRow.actionButton('Disable query').eq(1).should('be.visible').click(); e2e.components.QueryEditorRow.actionButton('Hide response').eq(1).should('be.visible').click();
expectInspectorResultAndClose((keys) => { expectInspectorResultAndClose((keys) => {
const length = keys.length; const length = keys.length;
expect(keys[length - 1].innerText).equals('B:'); expect(keys[length - 1].innerText).equals('B:');
}); });
// Enable row with refId B // Show response for row with refId A
e2e.components.QueryEditorRow.actionButton('Disable query').eq(1).should('be.visible').click(); e2e.components.QueryEditorRow.actionButton('Hide response').eq(1).should('be.visible').click();
expectInspectorResultAndClose((keys) => { expectInspectorResultAndClose((keys) => {
const length = keys.length; const length = keys.length;

View File

@ -190,9 +190,6 @@ type VariableSupport<TQuery extends DataQuery, TOptions extends DataSourceJsonDa
/** /**
* The main data source abstraction interface, represents an instance of a data source * The main data source abstraction interface, represents an instance of a data source
*
* Although this is a class, datasource implementations do not *yet* need to extend it.
* As such, we can not yet add functions with default implementations.
*/ */
abstract class DataSourceApi< abstract class DataSourceApi<
TQuery extends DataQuery = DataQuery, TQuery extends DataQuery = DataQuery,
@ -263,11 +260,12 @@ abstract class DataSourceApi<
abstract testDatasource(): Promise<TestDataSourceResponse>; abstract testDatasource(): Promise<TestDataSourceResponse>;
/** /**
* This function is not called automatically unless running within the DataSourceWithBackend * Optionally, you can implement this method to prevent certain queries from being executed.
* * Return false to prevent the query from being executed.
* @deprecated
*/ */
filterQuery?(query: TQuery): boolean; filterQuery?(query: TQuery): boolean {
return true;
}
/** /**
* Get hints for query improvements * Get hints for query improvements

View File

@ -0,0 +1,69 @@
import { Observable } from 'rxjs';
import {
DataQuery,
DataQueryRequest,
DataQueryResponse,
DataSourceApi,
DataSourceInstanceSettings,
DataSourceJsonData,
DataSourcePluginMeta,
PluginMetaInfo,
PluginType,
TestDataSourceResponse,
} from '../../types';
export interface TestQuery extends DataQuery {
query: string;
}
export interface TestJsonData extends DataSourceJsonData {
url?: string;
}
const info: PluginMetaInfo = {
author: {
name: '',
},
description: '',
links: [],
logos: {
large: '',
small: '',
},
screenshots: [],
updated: '',
version: '',
};
export const meta: DataSourcePluginMeta<DataSourceJsonData> = {
id: '',
name: '',
type: PluginType.datasource,
info,
module: '',
baseUrl: '',
};
export const TestDataSettings: DataSourceInstanceSettings<TestJsonData> = {
jsonData: { url: 'http://localhost:3000' },
id: 0,
uid: '',
type: '',
name: 'Test Datasource',
meta,
readOnly: false,
access: 'direct',
};
export class TestDataSource extends DataSourceApi<TestQuery, DataSourceJsonData> {
query(request: DataQueryRequest<TestQuery>): Promise<DataQueryResponse> | Observable<DataQueryResponse> {
throw new Error('Method not implemented.');
}
testDatasource(): Promise<TestDataSourceResponse> {
throw new Error('Method not implemented.');
}
constructor(instanceSettings: DataSourceInstanceSettings<TestJsonData> = TestDataSettings) {
super(instanceSettings);
}
}

View File

@ -134,10 +134,6 @@ class DataSourceWithBackend<
const { intervalMs, maxDataPoints, queryCachingTTL, range, requestId, hideFromInspector = false } = request; const { intervalMs, maxDataPoints, queryCachingTTL, range, requestId, hideFromInspector = false } = request;
let targets = request.targets; let targets = request.targets;
if (this.filterQuery) {
targets = targets.filter((q) => this.filterQuery!(q));
}
let hasExpr = false; let hasExpr = false;
const pluginIDs = new Set<string>(); const pluginIDs = new Set<string>();
const dsUIDs = new Set<string>(); const dsUIDs = new Set<string>();
@ -275,16 +271,6 @@ class DataSourceWithBackend<
return queries.map((q) => this.applyTemplateVariables(q, scopedVars, filters)); return queries.map((q) => this.applyTemplateVariables(q, scopedVars, filters));
} }
/**
* Override to skip executing a query. Note this function may not be called
* if the query method is overwritten.
*
* @returns false if the query should be skipped
*
* @virtual
*/
filterQuery?(query: TQuery): boolean;
/** /**
* Override to apply template variables and adhoc filters. The result is usually also `TQuery`, but sometimes this can * Override to apply template variables and adhoc filters. The result is usually also `TQuery`, but sometimes this can
* be used to modify the query structure before sending to the backend. * be used to modify the query structure before sending to the backend.

View File

@ -43,9 +43,7 @@ export interface DataQuery {
*/ */
datasource?: unknown; datasource?: unknown;
/** /**
* true if query is disabled (ie should not be returned to the dashboard) * If hide is set to true, Grafana will filter out the response(s) associated with this query before returning it to the panel.
* Note this does not always imply that the query should not be executed since
* the results from a hidden query may be used as the input to other queries (SSE etc)
*/ */
hide?: boolean; hide?: boolean;
/** /**

View File

@ -23,9 +23,7 @@ DataQuery: {
// By default, the UI will assign A->Z; however setting meaningful names may be useful. // By default, the UI will assign A->Z; however setting meaningful names may be useful.
refId: string refId: string
// true if query is disabled (ie should not be returned to the dashboard) // If hide is set to true, Grafana will filter out the response(s) associated with this query before returning it to the panel.
// Note this does not always imply that the query should not be executed since
// the results from a hidden query may be used as the input to other queries (SSE etc)
hide?: bool hide?: bool
// Specify the query flavor // Specify the query flavor

View File

@ -234,9 +234,7 @@ type AzureMonitorQuery struct {
Datasource *any `json:"datasource,omitempty"` Datasource *any `json:"datasource,omitempty"`
GrafanaTemplateVariableFn *any `json:"grafanaTemplateVariableFn,omitempty"` GrafanaTemplateVariableFn *any `json:"grafanaTemplateVariableFn,omitempty"`
// Hide true if query is disabled (ie should not be returned to the dashboard) // If hide is set to true, Grafana will filter out the response(s) associated with this query before returning it to the panel.
// Note this does not always imply that the query should not be executed since
// the results from a hidden query may be used as the input to other queries (SSE etc)
Hide *bool `json:"hide,omitempty"` Hide *bool `json:"hide,omitempty"`
Namespace *string `json:"namespace,omitempty"` Namespace *string `json:"namespace,omitempty"`
@ -331,9 +329,7 @@ type DataQuery struct {
// TODO this shouldn't be unknown but DataSourceRef | null // TODO this shouldn't be unknown but DataSourceRef | null
Datasource *any `json:"datasource,omitempty"` Datasource *any `json:"datasource,omitempty"`
// Hide true if query is disabled (ie should not be returned to the dashboard) // If hide is set to true, Grafana will filter out the response(s) associated with this query before returning it to the panel.
// Note this does not always imply that the query should not be executed since
// the results from a hidden query may be used as the input to other queries (SSE etc)
Hide *bool `json:"hide,omitempty"` Hide *bool `json:"hide,omitempty"`
// Specify the query flavor // Specify the query flavor

View File

@ -98,9 +98,7 @@ type CloudMonitoringQuery struct {
// TODO this shouldn't be unknown but DataSourceRef | null // TODO this shouldn't be unknown but DataSourceRef | null
Datasource *any `json:"datasource,omitempty"` Datasource *any `json:"datasource,omitempty"`
// Hide true if query is disabled (ie should not be returned to the dashboard) // If hide is set to true, Grafana will filter out the response(s) associated with this query before returning it to the panel.
// Note this does not always imply that the query should not be executed since
// the results from a hidden query may be used as the input to other queries (SSE etc)
Hide *bool `json:"hide,omitempty"` Hide *bool `json:"hide,omitempty"`
// Time interval in milliseconds. // Time interval in milliseconds.
@ -138,9 +136,7 @@ type DataQuery struct {
// TODO this shouldn't be unknown but DataSourceRef | null // TODO this shouldn't be unknown but DataSourceRef | null
Datasource *any `json:"datasource,omitempty"` Datasource *any `json:"datasource,omitempty"`
// Hide true if query is disabled (ie should not be returned to the dashboard) // If hide is set to true, Grafana will filter out the response(s) associated with this query before returning it to the panel.
// Note this does not always imply that the query should not be executed since
// the results from a hidden query may be used as the input to other queries (SSE etc)
Hide *bool `json:"hide,omitempty"` Hide *bool `json:"hide,omitempty"`
// Specify the query flavor // Specify the query flavor

View File

@ -134,9 +134,7 @@ type CloudWatchAnnotationQuery struct {
// A name/value pair that is part of the identity of a metric. For example, you can get statistics for a specific EC2 instance by specifying the InstanceId dimension when you search for metrics. // A name/value pair that is part of the identity of a metric. For example, you can get statistics for a specific EC2 instance by specifying the InstanceId dimension when you search for metrics.
Dimensions *Dimensions `json:"dimensions,omitempty"` Dimensions *Dimensions `json:"dimensions,omitempty"`
// Hide true if query is disabled (ie should not be returned to the dashboard) // If hide is set to true, Grafana will filter out the response(s) associated with this query before returning it to the panel.
// Note this does not always imply that the query should not be executed since
// the results from a hidden query may be used as the input to other queries (SSE etc)
Hide *bool `json:"hide,omitempty"` Hide *bool `json:"hide,omitempty"`
// Only show metrics that exactly match all defined dimension names. // Only show metrics that exactly match all defined dimension names.
@ -188,9 +186,7 @@ type CloudWatchLogsQuery struct {
// The CloudWatch Logs Insights query to execute // The CloudWatch Logs Insights query to execute
Expression *string `json:"expression,omitempty"` Expression *string `json:"expression,omitempty"`
// Hide true if query is disabled (ie should not be returned to the dashboard) // If hide is set to true, Grafana will filter out the response(s) associated with this query before returning it to the panel.
// Note this does not always imply that the query should not be executed since
// the results from a hidden query may be used as the input to other queries (SSE etc)
Hide *bool `json:"hide,omitempty"` Hide *bool `json:"hide,omitempty"`
Id *string `json:"id,omitempty"` Id *string `json:"id,omitempty"`
@ -238,9 +234,7 @@ type CloudWatchMetricsQuery struct {
// Math expression query // Math expression query
Expression *string `json:"expression,omitempty"` Expression *string `json:"expression,omitempty"`
// Hide true if query is disabled (ie should not be returned to the dashboard) // If hide is set to true, Grafana will filter out the response(s) associated with this query before returning it to the panel.
// Note this does not always imply that the query should not be executed since
// the results from a hidden query may be used as the input to other queries (SSE etc)
Hide *bool `json:"hide,omitempty"` Hide *bool `json:"hide,omitempty"`
// ID can be used to reference other queries in math expressions. The ID can include numbers, letters, and underscore, and must start with a lowercase letter. // ID can be used to reference other queries in math expressions. The ID can include numbers, letters, and underscore, and must start with a lowercase letter.
@ -300,9 +294,7 @@ type DataQuery struct {
// TODO this shouldn't be unknown but DataSourceRef | null // TODO this shouldn't be unknown but DataSourceRef | null
Datasource *any `json:"datasource,omitempty"` Datasource *any `json:"datasource,omitempty"`
// Hide true if query is disabled (ie should not be returned to the dashboard) // If hide is set to true, Grafana will filter out the response(s) associated with this query before returning it to the panel.
// Note this does not always imply that the query should not be executed since
// the results from a hidden query may be used as the input to other queries (SSE etc)
Hide *bool `json:"hide,omitempty"` Hide *bool `json:"hide,omitempty"`
// Specify the query flavor // Specify the query flavor

View File

@ -172,9 +172,7 @@ type DataQuery struct {
// TODO this shouldn't be unknown but DataSourceRef | null // TODO this shouldn't be unknown but DataSourceRef | null
Datasource *any `json:"datasource,omitempty"` Datasource *any `json:"datasource,omitempty"`
// Hide true if query is disabled (ie should not be returned to the dashboard) // If hide is set to true, Grafana will filter out the response(s) associated with this query before returning it to the panel.
// Note this does not always imply that the query should not be executed since
// the results from a hidden query may be used as the input to other queries (SSE etc)
Hide *bool `json:"hide,omitempty"` Hide *bool `json:"hide,omitempty"`
// Specify the query flavor // Specify the query flavor

View File

@ -26,9 +26,7 @@ type DataQuery struct {
// TODO this shouldn't be unknown but DataSourceRef | null // TODO this shouldn't be unknown but DataSourceRef | null
Datasource *any `json:"datasource,omitempty"` Datasource *any `json:"datasource,omitempty"`
// Hide true if query is disabled (ie should not be returned to the dashboard) // If hide is set to true, Grafana will filter out the response(s) associated with this query before returning it to the panel.
// Note this does not always imply that the query should not be executed since
// the results from a hidden query may be used as the input to other queries (SSE etc)
Hide *bool `json:"hide,omitempty"` Hide *bool `json:"hide,omitempty"`
// Specify the query flavor // Specify the query flavor
@ -52,9 +50,7 @@ type GrafanaPyroscopeDataQuery struct {
// Allows to group the results. // Allows to group the results.
GroupBy []string `json:"groupBy,omitempty"` GroupBy []string `json:"groupBy,omitempty"`
// Hide true if query is disabled (ie should not be returned to the dashboard) // If hide is set to true, Grafana will filter out the response(s) associated with this query before returning it to the panel.
// Note this does not always imply that the query should not be executed since
// the results from a hidden query may be used as the input to other queries (SSE etc)
Hide *bool `json:"hide,omitempty"` Hide *bool `json:"hide,omitempty"`
// Specifies the query label selectors. // Specifies the query label selectors.

View File

@ -84,9 +84,7 @@ type DataQuery struct {
// TODO this shouldn't be unknown but DataSourceRef | null // TODO this shouldn't be unknown but DataSourceRef | null
Datasource *any `json:"datasource,omitempty"` Datasource *any `json:"datasource,omitempty"`
// Hide true if query is disabled (ie should not be returned to the dashboard) // If hide is set to true, Grafana will filter out the response(s) associated with this query before returning it to the panel.
// Note this does not always imply that the query should not be executed since
// the results from a hidden query may be used as the input to other queries (SSE etc)
Hide *bool `json:"hide,omitempty"` Hide *bool `json:"hide,omitempty"`
// Specify the query flavor // Specify the query flavor
@ -171,9 +169,7 @@ type TestDataDataQuery struct {
ErrorType *TestDataDataQueryErrorType `json:"errorType,omitempty"` ErrorType *TestDataDataQueryErrorType `json:"errorType,omitempty"`
FlamegraphDiff *bool `json:"flamegraphDiff,omitempty"` FlamegraphDiff *bool `json:"flamegraphDiff,omitempty"`
// Hide true if query is disabled (ie should not be returned to the dashboard) // If hide is set to true, Grafana will filter out the response(s) associated with this query before returning it to the panel.
// Note this does not always imply that the query should not be executed since
// the results from a hidden query may be used as the input to other queries (SSE etc)
Hide *bool `json:"hide,omitempty"` Hide *bool `json:"hide,omitempty"`
Labels *string `json:"labels,omitempty"` Labels *string `json:"labels,omitempty"`
LevelColumn *bool `json:"levelColumn,omitempty"` LevelColumn *bool `json:"levelColumn,omitempty"`

View File

@ -46,9 +46,7 @@ type DataQuery struct {
// TODO this shouldn't be unknown but DataSourceRef | null // TODO this shouldn't be unknown but DataSourceRef | null
Datasource *any `json:"datasource,omitempty"` Datasource *any `json:"datasource,omitempty"`
// Hide true if query is disabled (ie should not be returned to the dashboard) // If hide is set to true, Grafana will filter out the response(s) associated with this query before returning it to the panel.
// Note this does not always imply that the query should not be executed since
// the results from a hidden query may be used as the input to other queries (SSE etc)
Hide *bool `json:"hide,omitempty"` Hide *bool `json:"hide,omitempty"`
// Specify the query flavor // Specify the query flavor
@ -73,9 +71,7 @@ type LokiDataQuery struct {
// The LogQL query. // The LogQL query.
Expr *string `json:"expr,omitempty"` Expr *string `json:"expr,omitempty"`
// Hide true if query is disabled (ie should not be returned to the dashboard) // If hide is set to true, Grafana will filter out the response(s) associated with this query before returning it to the panel.
// Note this does not always imply that the query should not be executed since
// the results from a hidden query may be used as the input to other queries (SSE etc)
Hide *bool `json:"hide,omitempty"` Hide *bool `json:"hide,omitempty"`
// @deprecated, now use queryType. // @deprecated, now use queryType.

View File

@ -26,9 +26,7 @@ type DataQuery struct {
// TODO this shouldn't be unknown but DataSourceRef | null // TODO this shouldn't be unknown but DataSourceRef | null
Datasource *any `json:"datasource,omitempty"` Datasource *any `json:"datasource,omitempty"`
// Hide true if query is disabled (ie should not be returned to the dashboard) // If hide is set to true, Grafana will filter out the response(s) associated with this query before returning it to the panel.
// Note this does not always imply that the query should not be executed since
// the results from a hidden query may be used as the input to other queries (SSE etc)
Hide *bool `json:"hide,omitempty"` Hide *bool `json:"hide,omitempty"`
// Specify the query flavor // Specify the query flavor
@ -49,9 +47,7 @@ type ParcaDataQuery struct {
// TODO this shouldn't be unknown but DataSourceRef | null // TODO this shouldn't be unknown but DataSourceRef | null
Datasource *any `json:"datasource,omitempty"` Datasource *any `json:"datasource,omitempty"`
// Hide true if query is disabled (ie should not be returned to the dashboard) // If hide is set to true, Grafana will filter out the response(s) associated with this query before returning it to the panel.
// Note this does not always imply that the query should not be executed since
// the results from a hidden query may be used as the input to other queries (SSE etc)
Hide *bool `json:"hide,omitempty"` Hide *bool `json:"hide,omitempty"`
// Specifies the query label selectors. // Specifies the query label selectors.

View File

@ -52,9 +52,7 @@ type DataQuery struct {
// TODO this shouldn't be unknown but DataSourceRef | null // TODO this shouldn't be unknown but DataSourceRef | null
Datasource *any `json:"datasource,omitempty"` Datasource *any `json:"datasource,omitempty"`
// Hide true if query is disabled (ie should not be returned to the dashboard) // If hide is set to true, Grafana will filter out the response(s) associated with this query before returning it to the panel.
// Note this does not always imply that the query should not be executed since
// the results from a hidden query may be used as the input to other queries (SSE etc)
Hide *bool `json:"hide,omitempty"` Hide *bool `json:"hide,omitempty"`
// Specify the query flavor // Specify the query flavor
@ -88,9 +86,7 @@ type TempoQuery struct {
// Filters that are used to query the metrics summary // Filters that are used to query the metrics summary
GroupBy []TraceqlFilter `json:"groupBy,omitempty"` GroupBy []TraceqlFilter `json:"groupBy,omitempty"`
// Hide true if query is disabled (ie should not be returned to the dashboard) // If hide is set to true, Grafana will filter out the response(s) associated with this query before returning it to the panel.
// Note this does not always imply that the query should not be executed since
// the results from a hidden query may be used as the input to other queries (SSE etc)
Hide *bool `json:"hide,omitempty"` Hide *bool `json:"hide,omitempty"`
// Defines the maximum number of traces that are returned from Tempo // Defines the maximum number of traces that are returned from Tempo

View File

@ -161,7 +161,7 @@ export const QueryWrapper = ({
queries={editorQueries} queries={editorQueries}
renderHeaderExtras={() => <HeaderExtras query={query} index={index} error={error} />} renderHeaderExtras={() => <HeaderExtras query={query} index={index} error={error} />}
app={CoreApp.UnifiedAlerting} app={CoreApp.UnifiedAlerting}
hideDisableQuery={true} hideHideQueryButton={true}
/> />
</div> </div>
{showVizualisation && ( {showVizualisation && (

View File

@ -0,0 +1,59 @@
import { DataSourceApi, dateTime, DataQuery } from '@grafana/data';
import { PanelModel } from '../dashboard/state';
import { createDashboardModelFixture } from '../dashboard/state/__fixtures__/dashboardFixtures';
import { TestQuery, getMockDataSource } from '../query/state/__mocks__/mockDataSource';
import { executeAnnotationQuery } from './executeAnnotationQuery';
import { AnnotationQueryOptions } from './types';
describe('executeAnnotationQuery', () => {
let filterQuerySpy: jest.SpyInstance;
let querySpy: jest.SpyInstance;
let ds: DataSourceApi;
const setup = ({ query, filterQuery }: { query: TestQuery; filterQuery?: typeof ds.filterQuery }) => {
const options: AnnotationQueryOptions = {
range: { from: dateTime(), to: dateTime(), raw: { from: '1h', to: 'now' } },
dashboard: createDashboardModelFixture({
panels: [{ id: 1, type: 'graph' }],
}),
panel: {} as PanelModel,
};
const ds = getMockDataSource();
if (filterQuery) {
ds.filterQuery = filterQuery;
filterQuerySpy = jest.spyOn(ds, 'filterQuery');
}
querySpy = jest.spyOn(ds, 'query');
executeAnnotationQuery(options, ds, {
name: '',
enable: false,
iconColor: '',
target: query,
});
};
beforeEach(() => {
jest.clearAllMocks();
});
it('Should not call query method in case query is filtered out', async () => {
setup({
query: { q: 'SUM(foo)', refId: 'A' },
filterQuery: (query: TestQuery) => query.q !== 'SUM(foo)',
});
expect(filterQuerySpy).toHaveBeenCalledTimes(1);
expect(querySpy).not.toHaveBeenCalled();
});
it('Should call backend in case query is not filtered out', async () => {
setup({
filterQuery: (_: DataQuery) => true,
query: { q: 'SUM(foo)', refId: 'A' },
});
expect(filterQuerySpy).toHaveBeenCalledTimes(1);
expect(querySpy).toHaveBeenCalledTimes(1);
});
});

View File

@ -22,7 +22,7 @@ import {
toLegacyResponseData, toLegacyResponseData,
} from '@grafana/data'; } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors'; import { selectors } from '@grafana/e2e-selectors';
import { AngularComponent, getAngularLoader, getDataSourceSrv } from '@grafana/runtime'; import { AngularComponent, getAngularLoader, getDataSourceSrv, reportInteraction } from '@grafana/runtime';
import { Badge, ErrorBoundaryAlert } from '@grafana/ui'; import { Badge, ErrorBoundaryAlert } from '@grafana/ui';
import { OperationRowHelp } from 'app/core/components/QueryOperationRow/OperationRowHelp'; import { OperationRowHelp } from 'app/core/components/QueryOperationRow/OperationRowHelp';
import { import {
@ -57,7 +57,7 @@ export interface Props<TQuery extends DataQuery> {
onChange: (query: TQuery) => void; onChange: (query: TQuery) => void;
onRunQuery: () => void; onRunQuery: () => void;
visualization?: ReactNode; visualization?: ReactNode;
hideDisableQuery?: boolean; hideHideQueryButton?: boolean;
app?: CoreApp; app?: CoreApp;
history?: Array<HistoryItem<TQuery>>; history?: Array<HistoryItem<TQuery>>;
eventBus?: EventBusExtended; eventBus?: EventBusExtended;
@ -341,7 +341,7 @@ export class QueryEditorRow<TQuery extends DataQuery> extends PureComponent<Prop
} }
}; };
onDisableQuery = () => { onHideQuery = () => {
const { query, onChange, onRunQuery, onQueryToggled } = this.props; const { query, onChange, onRunQuery, onQueryToggled } = this.props;
onChange({ ...query, hide: !query.hide }); onChange({ ...query, hide: !query.hide });
onRunQuery(); onRunQuery();
@ -349,6 +349,10 @@ export class QueryEditorRow<TQuery extends DataQuery> extends PureComponent<Prop
if (onQueryToggled) { if (onQueryToggled) {
onQueryToggled(query.hide); onQueryToggled(query.hide);
} }
reportInteraction('query_editor_row_hide_query_clicked', {
hide: !query.hide,
});
}; };
onToggleHelp = () => { onToggleHelp = () => {
@ -440,9 +444,9 @@ export class QueryEditorRow<TQuery extends DataQuery> extends PureComponent<Prop
}; };
renderActions = (props: QueryOperationRowRenderProps) => { renderActions = (props: QueryOperationRowRenderProps) => {
const { query, hideDisableQuery = false } = this.props; const { query, hideHideQueryButton: hideHideQueryButton = false } = this.props;
const { hasTextEditMode, datasource, showingHelp } = this.state; const { hasTextEditMode, datasource, showingHelp } = this.state;
const isDisabled = !!query.hide; const isHidden = !!query.hide;
const hasEditorHelp = datasource?.components?.QueryEditorHelp; const hasEditorHelp = datasource?.components?.QueryEditorHelp;
@ -471,12 +475,17 @@ export class QueryEditorRow<TQuery extends DataQuery> extends PureComponent<Prop
icon="copy" icon="copy"
onClick={this.onCopyQuery} onClick={this.onCopyQuery}
/> />
{!hideDisableQuery ? ( {!hideHideQueryButton ? (
<QueryOperationToggleAction <QueryOperationToggleAction
title={t('query-operation.header.disable-query', 'Disable query')} dataTestId={selectors.components.QueryEditorRow.actionButton('Hide response')}
icon={isDisabled ? 'eye-slash' : 'eye'} title={
active={isDisabled} query.hide
onClick={this.onDisableQuery} ? t('query-operation.header.show-response', 'Show response')
: t('query-operation.header.hide-response', 'Hide response')
}
icon={isHidden ? 'eye-slash' : 'eye'}
active={isHidden}
onClick={this.onHideQuery}
/> />
) : null} ) : null}
<QueryOperationAction <QueryOperationAction
@ -497,7 +506,7 @@ export class QueryEditorRow<TQuery extends DataQuery> extends PureComponent<Prop
queries={queries} queries={queries}
onChangeDataSource={onChangeDataSource} onChangeDataSource={onChangeDataSource}
dataSource={dataSource} dataSource={dataSource}
disabled={query.hide} hidden={query.hide}
onClick={(e) => this.onToggleEditMode(e, props)} onClick={(e) => this.onToggleEditMode(e, props)}
onChange={onChange} onChange={onChange}
collapsedText={!props.isOpen ? this.renderCollapsedText() : null} collapsedText={!props.isOpen ? this.renderCollapsedText() : null}
@ -510,12 +519,12 @@ export class QueryEditorRow<TQuery extends DataQuery> extends PureComponent<Prop
render() { render() {
const { query, index, visualization, collapsable } = this.props; const { query, index, visualization, collapsable } = this.props;
const { datasource, showingHelp, data } = this.state; const { datasource, showingHelp, data } = this.state;
const isDisabled = query.hide; const isHidden = query.hide;
const error = const error =
data?.error && data.error.refId === query.refId ? data.error : data?.errors?.find((e) => e.refId === query.refId); data?.error && data.error.refId === query.refId ? data.error : data?.errors?.find((e) => e.refId === query.refId);
const rowClasses = classNames('query-editor-row', { const rowClasses = classNames('query-editor-row', {
'query-editor-row--disabled': isDisabled, 'query-editor-row--disabled': isHidden,
'gf-form-disabled': isDisabled, 'gf-form-disabled': isHidden,
}); });
if (!datasource) { if (!datasource) {

View File

@ -102,7 +102,7 @@ function renderScenario(overrides: Partial<Props>) {
}, },
], ],
dataSource: {} as DataSourceInstanceSettings, dataSource: {} as DataSourceInstanceSettings,
disabled: false, hidden: false,
onChange: jest.fn(), onChange: jest.fn(),
onClick: jest.fn(), onClick: jest.fn(),
collapsedText: '', collapsedText: '',

View File

@ -9,7 +9,7 @@ import { DataSourcePicker } from 'app/features/datasources/components/picker/Dat
export interface Props<TQuery extends DataQuery = DataQuery> { export interface Props<TQuery extends DataQuery = DataQuery> {
query: TQuery; query: TQuery;
queries: TQuery[]; queries: TQuery[];
disabled?: boolean; hidden?: boolean;
dataSource: DataSourceInstanceSettings; dataSource: DataSourceInstanceSettings;
renderExtras?: () => ReactNode; renderExtras?: () => ReactNode;
onChangeDataSource?: (settings: DataSourceInstanceSettings) => void; onChangeDataSource?: (settings: DataSourceInstanceSettings) => void;
@ -20,7 +20,7 @@ export interface Props<TQuery extends DataQuery = DataQuery> {
} }
export const QueryEditorRowHeader = <TQuery extends DataQuery>(props: Props<TQuery>) => { export const QueryEditorRowHeader = <TQuery extends DataQuery>(props: Props<TQuery>) => {
const { query, queries, onChange, collapsedText, renderExtras, disabled } = props; const { query, queries, onChange, collapsedText, renderExtras, hidden } = props;
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
const [isEditing, setIsEditing] = useState<boolean>(false); const [isEditing, setIsEditing] = useState<boolean>(false);
@ -117,7 +117,7 @@ export const QueryEditorRowHeader = <TQuery extends DataQuery>(props: Props<TQue
)} )}
{renderDataSource(props, styles)} {renderDataSource(props, styles)}
{renderExtras && <div className={styles.itemWrapper}>{renderExtras()}</div>} {renderExtras && <div className={styles.itemWrapper}>{renderExtras()}</div>}
{disabled && <em className={styles.contextInfo}>Disabled</em>} {hidden && <em className={styles.contextInfo}>Hidden</em>}
</div> </div>
{collapsedText && <div className={styles.collapsedText}>{collapsedText}</div>} {collapsedText && <div className={styles.collapsedText}>{collapsedText}</div>}

View File

@ -0,0 +1,77 @@
import { Observable } from 'rxjs';
import {
DataQuery,
DataSourceJsonData,
PluginMetaInfo,
DataSourcePluginMeta,
PluginType,
DataSourceInstanceSettings,
DataSourceApi,
DataQueryRequest,
DataQueryResponse,
TestDataSourceResponse,
} from '@grafana/data';
export interface TestQuery extends DataQuery {
q?: string;
}
export interface TestJsonData extends DataSourceJsonData {
url?: string;
}
const info: PluginMetaInfo = {
author: {
name: '',
},
description: '',
links: [],
logos: {
large: '',
small: '',
},
screenshots: [],
updated: '',
version: '',
};
export const meta: DataSourcePluginMeta<DataSourceJsonData> = {
id: '',
name: '',
type: PluginType.datasource,
info,
module: '',
baseUrl: '',
};
export const TestDataSettings: DataSourceInstanceSettings<TestJsonData> = {
jsonData: { url: 'http://localhost:3000' },
id: 0,
uid: '',
type: '',
name: 'Test Datasource',
meta,
readOnly: false,
access: 'direct',
};
export class TestDataSource extends DataSourceApi<TestQuery, DataSourceJsonData, {}> {
constructor(instanceSettings: DataSourceInstanceSettings<TestJsonData> = TestDataSettings) {
super(instanceSettings);
}
query(request: DataQueryRequest<TestQuery>): Promise<DataQueryResponse> | Observable<DataQueryResponse> {
return Promise.resolve({
data: [],
});
}
testDatasource(): Promise<TestDataSourceResponse> {
throw new Error('Method not implemented.');
}
}
export const getMockDataSource = () => {
return new TestDataSource();
};

View File

@ -1,6 +1,7 @@
import { Observable, Subscriber, Subscription } from 'rxjs'; import { Observable, Subscriber, Subscription } from 'rxjs';
import { import {
CoreApp,
DataFrame, DataFrame,
DataQueryRequest, DataQueryRequest,
DataQueryResponse, DataQueryResponse,
@ -11,12 +12,14 @@ import {
PanelData, PanelData,
} from '@grafana/data'; } from '@grafana/data';
import { setEchoSrv } from '@grafana/runtime'; import { setEchoSrv } from '@grafana/runtime';
import { DataQuery } from '@grafana/schema';
import { deepFreeze } from '../../../../test/core/redux/reducerTester'; import { deepFreeze } from '../../../../test/core/redux/reducerTester';
import { Echo } from '../../../core/services/echo/Echo'; import { Echo } from '../../../core/services/echo/Echo';
import { createDashboardModelFixture } from '../../dashboard/state/__fixtures__/dashboardFixtures'; import { createDashboardModelFixture } from '../../dashboard/state/__fixtures__/dashboardFixtures';
import { runRequest } from './runRequest'; import { getMockDataSource, TestQuery } from './__mocks__/mockDataSource';
import { callQueryMethod, runRequest } from './runRequest';
jest.mock('app/core/services/backend_srv'); jest.mock('app/core/services/backend_srv');
@ -371,6 +374,193 @@ describe('runRequest', () => {
expect(ctx.results[1].series.length).toBe(1); expect(ctx.results[1].series.length).toBe(1);
}); });
}); });
runRequestScenario('When some queries are hidden', (ctx) => {
ctx.setup(() => {
ctx.request.targets = [{ refId: 'A', hide: true }, { refId: 'B' }];
ctx.start();
ctx.emitPacket({
data: [
{ name: 'DataA-1', refId: 'A' },
{ name: 'DataA-2', refId: 'A' },
{ name: 'DataB-1', refId: 'B' },
{ name: 'DataB-2', refId: 'B' },
],
key: 'A',
});
});
it('should filter out responses that are associated with the hidden queries', () => {
expect(ctx.results[0].series.length).toBe(2);
expect(ctx.results[0].series[0].name).toBe('DataB-1');
expect(ctx.results[0].series[1].name).toBe('DataB-2');
});
});
});
describe('callQueryMethod', () => {
let request: DataQueryRequest<TestQuery>;
let filterQuerySpy: jest.SpyInstance;
let querySpy: jest.SpyInstance;
let defaultQuerySpy: jest.SpyInstance;
let ds: DataSourceApi;
const setup = ({
targets,
filterQuery,
getDefaultQuery,
queryFunction,
}: {
targets: TestQuery[];
getDefaultQuery?: (app: CoreApp) => Partial<TestQuery>;
filterQuery?: typeof ds.filterQuery;
queryFunction?: typeof ds.query;
}) => {
request = {
range: {
from: dateTime(),
to: dateTime(),
raw: { from: '1h', to: 'now' },
},
targets,
requestId: '',
interval: '',
intervalMs: 0,
scopedVars: {},
timezone: '',
app: '',
startTime: 0,
};
const ds = getMockDataSource();
if (filterQuery) {
ds.filterQuery = filterQuery;
filterQuerySpy = jest.spyOn(ds, 'filterQuery');
}
if (getDefaultQuery) {
ds.getDefaultQuery = getDefaultQuery;
defaultQuerySpy = jest.spyOn(ds, 'getDefaultQuery');
}
querySpy = jest.spyOn(ds, 'query');
callQueryMethod(ds, request, queryFunction);
};
beforeEach(() => {
jest.clearAllMocks();
});
it('Should call filterQuery and exclude them from the request', async () => {
setup({
targets: [
{
refId: 'A',
q: 'SUM(foo)',
},
{
refId: 'B',
q: 'SUM(foo2)',
},
{
refId: 'C',
q: 'SUM(foo3)',
},
],
filterQuery: (query: DataQuery) => query.refId !== 'A',
});
expect(filterQuerySpy).toHaveBeenCalledTimes(3);
expect(querySpy).toHaveBeenCalledWith(
expect.objectContaining({
targets: [
{ q: 'SUM(foo2)', refId: 'B' },
{ q: 'SUM(foo3)', refId: 'C' },
],
})
);
});
it('Should not call query function in case targets are empty', async () => {
setup({
targets: [
{
refId: 'A',
q: 'SUM(foo)',
},
{
refId: 'B',
q: 'SUM(foo2)',
},
{
refId: 'C',
q: 'SUM(foo3)',
},
],
filterQuery: (_: DataQuery) => false,
});
expect(filterQuerySpy).toHaveBeenCalledTimes(3);
expect(querySpy).not.toHaveBeenCalled();
});
it('Should not call filterQuery in case a custom query method is provided', async () => {
const queryFunctionMock = jest.fn().mockResolvedValue({ data: [] });
setup({
targets: [
{
refId: 'A',
q: 'SUM(foo)',
},
{
refId: 'B',
q: 'SUM(foo2)',
},
{
refId: 'C',
q: 'SUM(foo3)',
},
],
queryFunction: queryFunctionMock,
filterQuery: (query: DataQuery) => query.refId !== 'A',
});
expect(filterQuerySpy).not.toHaveBeenCalled();
expect(queryFunctionMock).toHaveBeenCalledWith(
expect.objectContaining({
targets: [
{ q: 'SUM(foo)', refId: 'A' },
{ q: 'SUM(foo2)', refId: 'B' },
{ q: 'SUM(foo3)', refId: 'C' },
],
})
);
});
it('Should get ds default query when query is empty', async () => {
setup({
targets: [
{
refId: 'A',
},
{
refId: 'B',
},
{
refId: 'C',
q: 'SUM(foo3)',
},
],
getDefaultQuery: (_: CoreApp) => ({
q: 'SUM(foo2)',
}),
});
expect(defaultQuerySpy).toHaveBeenCalledTimes(2);
expect(querySpy).toHaveBeenCalledWith(
expect.objectContaining({
targets: [
{ q: 'SUM(foo2)', refId: 'A' },
{ q: 'SUM(foo2)', refId: 'B' },
{ q: 'SUM(foo3)', refId: 'C' },
],
})
);
});
}); });
const expectThatRangeHasNotMutated = (ctx: ScenarioCtx) => { const expectThatRangeHasNotMutated = (ctx: ScenarioCtx) => {

View File

@ -150,6 +150,12 @@ export function runRequest(
throw new Error(`Expected response data to be array, got ${typeof packet.data}.`); throw new Error(`Expected response data to be array, got ${typeof packet.data}.`);
} }
// filter out responses for hidden queries
const hiddenQueries = request.targets.filter((q) => q.hide);
for (const query of hiddenQueries) {
packet.data = packet.data.filter((d) => d.refId !== query.refId);
}
request.endTime = Date.now(); request.endTime = Date.now();
state = processResponsePacket(packet, state); state = processResponsePacket(packet, state);
@ -195,6 +201,15 @@ export function callQueryMethod(
: t : t
); );
// do not filter queries in case a custom query function is provided (for example in variable queries)
if (!queryFunction) {
request.targets = request.targets.filter((t) => datasource.filterQuery?.(t) ?? true);
}
if (request.targets.length === 0) {
return of<DataQueryResponse>({ data: [] });
}
// If its a public datasource, just return the result. Expressions will be handled on the backend. // If its a public datasource, just return the result. Expressions will be handled on the backend.
if (config.publicDashboardAccessToken) { if (config.publicDashboardAccessToken) {
return from(datasource.query(request)); return from(datasource.query(request));

View File

@ -1,8 +1,7 @@
import { DataQueryRequest, DataSourceInstanceSettings, toUtc } from '@grafana/data'; import { DataSourceInstanceSettings } from '@grafana/data';
import { getTemplateSrv, TemplateSrv } from '@grafana/runtime'; // will use the version in __mocks__ import { getTemplateSrv, TemplateSrv } from '@grafana/runtime'; // will use the version in __mocks__
import CloudMonitoringDataSource from '../datasource'; import CloudMonitoringDataSource from '../datasource';
import { CloudMonitoringQuery } from '../types/query';
import { CloudMonitoringOptions, CustomVariableModel } from '../types/types'; import { CloudMonitoringOptions, CustomVariableModel } from '../types/types';
let getTempVars = () => [] as CustomVariableModel[]; let getTempVars = () => [] as CustomVariableModel[];
@ -36,48 +35,6 @@ function getTestcontext({ response = {}, throws = false, templateSrv = getTempla
} }
describe('CloudMonitoringDataSource', () => { describe('CloudMonitoringDataSource', () => {
describe('When performing query', () => {
describe('and no time series data is returned', () => {
it('should return a list of datapoints', async () => {
const options = {
range: {
from: toUtc('2017-08-22T20:00:00Z'),
to: toUtc('2017-08-22T23:59:00Z'),
},
rangeRaw: {
from: 'now-4h',
to: 'now',
},
targets: [
{
refId: 'A',
},
],
} as DataQueryRequest<CloudMonitoringQuery>;
const response = {
results: {
A: {
refId: 'A',
meta: {
rawQuery: 'arawquerystring',
},
series: null,
tables: null,
},
},
};
const { ds } = getTestcontext({ response });
await expect(ds.query(options)).toEmitValuesWith((received) => {
const results = received[0];
expect(results.data.length).toBe(0);
});
});
});
});
describe('when interpolating a template variable for the filter', () => { describe('when interpolating a template variable for the filter', () => {
beforeEach(() => { beforeEach(() => {
getTempVars = () => [] as CustomVariableModel[]; getTempVars = () => [] as CustomVariableModel[];

View File

@ -286,34 +286,6 @@ describe('PostgreSQLDatasource', () => {
}); });
}); });
describe('When performing a query with hidden target', () => {
it('should return empty result and backendSrv.fetch should not be called', async () => {
const options = {
range: {
from: dateTime(1432288354),
to: dateTime(1432288401),
},
targets: [
{
format: 'table',
rawQuery: true,
rawSql: 'select time, metric, value from grafana_metric',
refId: 'A',
datasource: 'gdev-ds',
hide: true,
},
],
} as unknown as DataQueryRequest<SQLQuery>;
const { ds } = setupTestContext({});
await expect(ds.query(options)).toEmitValuesWith((received) => {
expect(received[0]).toEqual({ data: [] });
expect(fetchMock).not.toHaveBeenCalled();
});
});
});
describe('When runSql returns an empty dataframe', () => { describe('When runSql returns an empty dataframe', () => {
const response = { const response = {
results: { results: {

View File

@ -3,9 +3,7 @@ import { of } from 'rxjs';
import { import {
dataFrameToJSON, dataFrameToJSON,
getDefaultTimeRange, getDefaultTimeRange,
DataQueryRequest,
DataSourceInstanceSettings, DataSourceInstanceSettings,
dateTime,
FieldType, FieldType,
createDataFrame, createDataFrame,
} from '@grafana/data'; } from '@grafana/data';
@ -47,34 +45,6 @@ describe('MySQLDatasource', () => {
replace: (text: string) => text, replace: (text: string) => text,
}; };
describe('When performing a query with hidden target', () => {
it('should return empty result and backendSrv.fetch should not be called', async () => {
const options = {
range: {
from: dateTime(1432288354),
to: dateTime(1432288401),
},
targets: [
{
format: 'table',
rawQuery: true,
rawSql: 'select time, metric, value from grafana_metric',
refId: 'A',
datasource: 'gdev-ds',
hide: true,
},
],
} as unknown as DataQueryRequest<SQLQuery>;
const { ds, fetchMock } = setupTestContext({});
await expect(ds.query(options)).toEmitValuesWith((received) => {
expect(received[0]).toEqual({ data: [] });
expect(fetchMock).not.toHaveBeenCalled();
});
});
});
describe('When runSql returns an empty dataframe', () => { describe('When runSql returns an empty dataframe', () => {
const response = { const response = {
results: { results: {

View File

@ -1343,11 +1343,12 @@
"header": { "header": {
"collapse-row": "Collapse query row", "collapse-row": "Collapse query row",
"datasource-help": "Show data source help", "datasource-help": "Show data source help",
"disable-query": "Disable query",
"drag-and-drop": "Drag and drop to reorder", "drag-and-drop": "Drag and drop to reorder",
"duplicate-query": "Duplicate query", "duplicate-query": "Duplicate query",
"expand-row": "Expand query row", "expand-row": "Expand query row",
"hide-response": "Hide response",
"remove-query": "Remove query", "remove-query": "Remove query",
"show-response": "Show response",
"toggle-edit-mode": "Toggle text edit mode" "toggle-edit-mode": "Toggle text edit mode"
}, },
"query-editor-not-exported": "Data source plugin does not export any Query Editor component" "query-editor-not-exported": "Data source plugin does not export any Query Editor component"

View File

@ -1343,11 +1343,12 @@
"header": { "header": {
"collapse-row": "Cőľľäpşę qūęřy řőŵ", "collapse-row": "Cőľľäpşę qūęřy řőŵ",
"datasource-help": "Ŝĥőŵ đäŧä şőūřčę ĥęľp", "datasource-help": "Ŝĥőŵ đäŧä şőūřčę ĥęľp",
"disable-query": "Đįşäþľę qūęřy",
"drag-and-drop": "Đřäģ äʼnđ đřőp ŧő řęőřđęř", "drag-and-drop": "Đřäģ äʼnđ đřőp ŧő řęőřđęř",
"duplicate-query": "Đūpľįčäŧę qūęřy", "duplicate-query": "Đūpľįčäŧę qūęřy",
"expand-row": "Ēχpäʼnđ qūęřy řőŵ", "expand-row": "Ēχpäʼnđ qūęřy řőŵ",
"hide-response": "Ħįđę řęşpőʼnşę",
"remove-query": "Ŗęmővę qūęřy", "remove-query": "Ŗęmővę qūęřy",
"show-response": "Ŝĥőŵ řęşpőʼnşę",
"toggle-edit-mode": "Ŧőģģľę ŧęχŧ ęđįŧ mőđę" "toggle-edit-mode": "Ŧőģģľę ŧęχŧ ęđįŧ mőđę"
}, },
"query-editor-not-exported": "Đäŧä şőūřčę pľūģįʼn đőęş ʼnőŧ ęχpőřŧ äʼny Qūęřy Ēđįŧőř čőmpőʼnęʼnŧ" "query-editor-not-exported": "Đäŧä şőūřčę pľūģįʼn đőęş ʼnőŧ ęχpőřŧ äʼny Qūęřy Ēđįŧőř čőmpőʼnęʼnŧ"