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);
});
// Disable row with refId A
e2e.components.QueryEditorRow.actionButton('Disable query').eq(1).should('be.visible').click();
// Hide response for row with refId A
e2e.components.QueryEditorRow.actionButton('Hide response').eq(1).should('be.visible').click();
expectInspectorResultAndClose((keys) => {
const length = keys.length;
expect(keys[length - 1].innerText).equals('B:');
});
// Enable row with refId B
e2e.components.QueryEditorRow.actionButton('Disable query').eq(1).should('be.visible').click();
// Show response for row with refId A
e2e.components.QueryEditorRow.actionButton('Hide response').eq(1).should('be.visible').click();
expectInspectorResultAndClose((keys) => {
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
*
* 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<
TQuery extends DataQuery = DataQuery,
@ -263,11 +260,12 @@ abstract class DataSourceApi<
abstract testDatasource(): Promise<TestDataSourceResponse>;
/**
* This function is not called automatically unless running within the DataSourceWithBackend
*
* @deprecated
* Optionally, you can implement this method to prevent certain queries from being executed.
* Return false to prevent the query from being executed.
*/
filterQuery?(query: TQuery): boolean;
filterQuery?(query: TQuery): boolean {
return true;
}
/**
* 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;
let targets = request.targets;
if (this.filterQuery) {
targets = targets.filter((q) => this.filterQuery!(q));
}
let hasExpr = false;
const pluginIDs = new Set<string>();
const dsUIDs = new Set<string>();
@ -275,16 +271,6 @@ class DataSourceWithBackend<
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
* be used to modify the query structure before sending to the backend.

View File

@ -43,9 +43,7 @@ export interface DataQuery {
*/
datasource?: unknown;
/**
* true if query is disabled (ie should not be returned to the dashboard)
* 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)
* If hide is set to true, Grafana will filter out the response(s) associated with this query before returning it to the panel.
*/
hide?: boolean;
/**

View File

@ -23,9 +23,7 @@ DataQuery: {
// By default, the UI will assign A->Z; however setting meaningful names may be useful.
refId: string
// true if query is disabled (ie should not be returned to the dashboard)
// 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)
// If hide is set to true, Grafana will filter out the response(s) associated with this query before returning it to the panel.
hide?: bool
// Specify the query flavor

View File

@ -8,9 +8,9 @@ package server
import (
"github.com/google/wire"
sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
"github.com/grafana/grafana/pkg/api"
"github.com/grafana/grafana/pkg/api/avatar"
"github.com/grafana/grafana/pkg/api/routing"

View File

@ -234,9 +234,7 @@ type AzureMonitorQuery struct {
Datasource *any `json:"datasource,omitempty"`
GrafanaTemplateVariableFn *any `json:"grafanaTemplateVariableFn,omitempty"`
// Hide true if query is disabled (ie should not be returned to the dashboard)
// 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)
// If hide is set to true, Grafana will filter out the response(s) associated with this query before returning it to the panel.
Hide *bool `json:"hide,omitempty"`
Namespace *string `json:"namespace,omitempty"`
@ -331,9 +329,7 @@ type DataQuery struct {
// TODO this shouldn't be unknown but DataSourceRef | null
Datasource *any `json:"datasource,omitempty"`
// Hide true if query is disabled (ie should not be returned to the dashboard)
// 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)
// If hide is set to true, Grafana will filter out the response(s) associated with this query before returning it to the panel.
Hide *bool `json:"hide,omitempty"`
// Specify the query flavor

View File

@ -98,9 +98,7 @@ type CloudMonitoringQuery struct {
// TODO this shouldn't be unknown but DataSourceRef | null
Datasource *any `json:"datasource,omitempty"`
// Hide true if query is disabled (ie should not be returned to the dashboard)
// 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)
// If hide is set to true, Grafana will filter out the response(s) associated with this query before returning it to the panel.
Hide *bool `json:"hide,omitempty"`
// Time interval in milliseconds.
@ -138,9 +136,7 @@ type DataQuery struct {
// TODO this shouldn't be unknown but DataSourceRef | null
Datasource *any `json:"datasource,omitempty"`
// Hide true if query is disabled (ie should not be returned to the dashboard)
// 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)
// If hide is set to true, Grafana will filter out the response(s) associated with this query before returning it to the panel.
Hide *bool `json:"hide,omitempty"`
// 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.
Dimensions *Dimensions `json:"dimensions,omitempty"`
// Hide true if query is disabled (ie should not be returned to the dashboard)
// 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)
// If hide is set to true, Grafana will filter out the response(s) associated with this query before returning it to the panel.
Hide *bool `json:"hide,omitempty"`
// Only show metrics that exactly match all defined dimension names.
@ -188,9 +186,7 @@ type CloudWatchLogsQuery struct {
// The CloudWatch Logs Insights query to execute
Expression *string `json:"expression,omitempty"`
// Hide true if query is disabled (ie should not be returned to the dashboard)
// 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)
// If hide is set to true, Grafana will filter out the response(s) associated with this query before returning it to the panel.
Hide *bool `json:"hide,omitempty"`
Id *string `json:"id,omitempty"`
@ -238,9 +234,7 @@ type CloudWatchMetricsQuery struct {
// Math expression query
Expression *string `json:"expression,omitempty"`
// Hide true if query is disabled (ie should not be returned to the dashboard)
// 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)
// If hide is set to true, Grafana will filter out the response(s) associated with this query before returning it to the panel.
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.
@ -300,9 +294,7 @@ type DataQuery struct {
// TODO this shouldn't be unknown but DataSourceRef | null
Datasource *any `json:"datasource,omitempty"`
// Hide true if query is disabled (ie should not be returned to the dashboard)
// 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)
// If hide is set to true, Grafana will filter out the response(s) associated with this query before returning it to the panel.
Hide *bool `json:"hide,omitempty"`
// Specify the query flavor

View File

@ -172,9 +172,7 @@ type DataQuery struct {
// TODO this shouldn't be unknown but DataSourceRef | null
Datasource *any `json:"datasource,omitempty"`
// Hide true if query is disabled (ie should not be returned to the dashboard)
// 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)
// If hide is set to true, Grafana will filter out the response(s) associated with this query before returning it to the panel.
Hide *bool `json:"hide,omitempty"`
// Specify the query flavor

View File

@ -26,9 +26,7 @@ type DataQuery struct {
// TODO this shouldn't be unknown but DataSourceRef | null
Datasource *any `json:"datasource,omitempty"`
// Hide true if query is disabled (ie should not be returned to the dashboard)
// 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)
// If hide is set to true, Grafana will filter out the response(s) associated with this query before returning it to the panel.
Hide *bool `json:"hide,omitempty"`
// Specify the query flavor
@ -52,9 +50,7 @@ type GrafanaPyroscopeDataQuery struct {
// Allows to group the results.
GroupBy []string `json:"groupBy,omitempty"`
// Hide true if query is disabled (ie should not be returned to the dashboard)
// 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)
// If hide is set to true, Grafana will filter out the response(s) associated with this query before returning it to the panel.
Hide *bool `json:"hide,omitempty"`
// Specifies the query label selectors.

View File

@ -84,9 +84,7 @@ type DataQuery struct {
// TODO this shouldn't be unknown but DataSourceRef | null
Datasource *any `json:"datasource,omitempty"`
// Hide true if query is disabled (ie should not be returned to the dashboard)
// 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)
// If hide is set to true, Grafana will filter out the response(s) associated with this query before returning it to the panel.
Hide *bool `json:"hide,omitempty"`
// Specify the query flavor
@ -171,9 +169,7 @@ type TestDataDataQuery struct {
ErrorType *TestDataDataQueryErrorType `json:"errorType,omitempty"`
FlamegraphDiff *bool `json:"flamegraphDiff,omitempty"`
// Hide true if query is disabled (ie should not be returned to the dashboard)
// 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)
// If hide is set to true, Grafana will filter out the response(s) associated with this query before returning it to the panel.
Hide *bool `json:"hide,omitempty"`
Labels *string `json:"labels,omitempty"`
LevelColumn *bool `json:"levelColumn,omitempty"`

View File

@ -46,9 +46,7 @@ type DataQuery struct {
// TODO this shouldn't be unknown but DataSourceRef | null
Datasource *any `json:"datasource,omitempty"`
// Hide true if query is disabled (ie should not be returned to the dashboard)
// 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)
// If hide is set to true, Grafana will filter out the response(s) associated with this query before returning it to the panel.
Hide *bool `json:"hide,omitempty"`
// Specify the query flavor
@ -73,9 +71,7 @@ type LokiDataQuery struct {
// The LogQL query.
Expr *string `json:"expr,omitempty"`
// Hide true if query is disabled (ie should not be returned to the dashboard)
// 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)
// If hide is set to true, Grafana will filter out the response(s) associated with this query before returning it to the panel.
Hide *bool `json:"hide,omitempty"`
// @deprecated, now use queryType.

View File

@ -26,9 +26,7 @@ type DataQuery struct {
// TODO this shouldn't be unknown but DataSourceRef | null
Datasource *any `json:"datasource,omitempty"`
// Hide true if query is disabled (ie should not be returned to the dashboard)
// 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)
// If hide is set to true, Grafana will filter out the response(s) associated with this query before returning it to the panel.
Hide *bool `json:"hide,omitempty"`
// Specify the query flavor
@ -49,9 +47,7 @@ type ParcaDataQuery struct {
// TODO this shouldn't be unknown but DataSourceRef | null
Datasource *any `json:"datasource,omitempty"`
// Hide true if query is disabled (ie should not be returned to the dashboard)
// 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)
// If hide is set to true, Grafana will filter out the response(s) associated with this query before returning it to the panel.
Hide *bool `json:"hide,omitempty"`
// Specifies the query label selectors.

View File

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

View File

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

View File

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

View File

@ -9,7 +9,7 @@ import { DataSourcePicker } from 'app/features/datasources/components/picker/Dat
export interface Props<TQuery extends DataQuery = DataQuery> {
query: TQuery;
queries: TQuery[];
disabled?: boolean;
hidden?: boolean;
dataSource: DataSourceInstanceSettings;
renderExtras?: () => ReactNode;
onChangeDataSource?: (settings: DataSourceInstanceSettings) => void;
@ -20,7 +20,7 @@ export interface Props<TQuery extends DataQuery = DataQuery> {
}
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 [isEditing, setIsEditing] = useState<boolean>(false);
@ -117,7 +117,7 @@ export const QueryEditorRowHeader = <TQuery extends DataQuery>(props: Props<TQue
)}
{renderDataSource(props, styles)}
{renderExtras && <div className={styles.itemWrapper}>{renderExtras()}</div>}
{disabled && <em className={styles.contextInfo}>Disabled</em>}
{hidden && <em className={styles.contextInfo}>Hidden</em>}
</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 {
CoreApp,
DataFrame,
DataQueryRequest,
DataQueryResponse,
@ -11,12 +12,14 @@ import {
PanelData,
} from '@grafana/data';
import { setEchoSrv } from '@grafana/runtime';
import { DataQuery } from '@grafana/schema';
import { deepFreeze } from '../../../../test/core/redux/reducerTester';
import { Echo } from '../../../core/services/echo/Echo';
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');
@ -371,6 +374,193 @@ describe('runRequest', () => {
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) => {

View File

@ -150,6 +150,12 @@ export function runRequest(
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();
state = processResponsePacket(packet, state);
@ -195,6 +201,15 @@ export function callQueryMethod(
: 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 (config.publicDashboardAccessToken) {
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 CloudMonitoringDataSource from '../datasource';
import { CloudMonitoringQuery } from '../types/query';
import { CloudMonitoringOptions, CustomVariableModel } from '../types/types';
let getTempVars = () => [] as CustomVariableModel[];
@ -36,48 +35,6 @@ function getTestcontext({ response = {}, throws = false, templateSrv = getTempla
}
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', () => {
beforeEach(() => {
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', () => {
const response = {
results: {

View File

@ -3,9 +3,7 @@ import { of } from 'rxjs';
import {
dataFrameToJSON,
getDefaultTimeRange,
DataQueryRequest,
DataSourceInstanceSettings,
dateTime,
FieldType,
createDataFrame,
} from '@grafana/data';
@ -47,34 +45,6 @@ describe('MySQLDatasource', () => {
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', () => {
const response = {
results: {

View File

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

View File

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