mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
410f5e3e3a
commit
29d4f6a217
@ -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;
|
||||
|
@ -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
|
||||
|
69
packages/grafana-data/src/utils/tests/mockDataSource.ts
Normal file
69
packages/grafana-data/src/utils/tests/mockDataSource.ts
Normal 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);
|
||||
}
|
||||
}
|
@ -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.
|
||||
|
@ -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;
|
||||
/**
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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"`
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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 && (
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
@ -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) {
|
||||
|
@ -102,7 +102,7 @@ function renderScenario(overrides: Partial<Props>) {
|
||||
},
|
||||
],
|
||||
dataSource: {} as DataSourceInstanceSettings,
|
||||
disabled: false,
|
||||
hidden: false,
|
||||
onChange: jest.fn(),
|
||||
onClick: jest.fn(),
|
||||
collapsedText: '',
|
||||
|
@ -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>}
|
||||
|
77
public/app/features/query/state/__mocks__/mockDataSource.ts
Normal file
77
public/app/features/query/state/__mocks__/mockDataSource.ts
Normal 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();
|
||||
};
|
@ -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) => {
|
||||
|
@ -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));
|
||||
|
@ -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[];
|
||||
|
@ -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: {
|
||||
|
@ -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: {
|
||||
|
@ -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"
|
||||
|
@ -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ŧ"
|
||||
|
Loading…
Reference in New Issue
Block a user