Explore: Reuse Dashboard's QueryRows component (#38942)

* WIP

* Functional without custom wrapper component, needs highlight

* Remove latency from explore

* Sync eventbus

* Some cleanup & removal of unused code

* Avoid clearing queries when running all empty queries

* Run remaining queries when removing one

* Update snapshots

* fix failing tests

* type cleanup

* Refactor QueryRows

* update snapshot

* Remove highlighter expressions

* minor fixes in queryrows

* remove unwanted change

* fix failing e2e test

* Persist refId in explore url state

* make traces test slightly more robust

* add test for query duplication
This commit is contained in:
Giordano Ricci 2021-09-15 16:26:23 +01:00 committed by GitHub
parent e251863085
commit f79173c99d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
48 changed files with 310 additions and 920 deletions

View File

@ -21,12 +21,12 @@ The query editor for Explore is similar to the query editor for the data source
```ts
import React from 'react';
import { ExploreQueryFieldProps } from '@grafana/data';
import { QueryEditorProps } from '@grafana/data';
import { QueryField } from '@grafana/ui';
import { DataSource } from './DataSource';
import { MyQuery, MyDataSourceOptions } from './types';
export type Props = ExploreQueryFieldProps<DataSource, MyQuery, MyDataSourceOptions>;
export type Props = QueryEditorProps<DataSource, MyQuery, MyDataSourceOptions>;
export default (props: Props) => {
return <h2>My query editor</h2>;

View File

@ -29,9 +29,13 @@ describe('Trace view', () => {
e2e.components.TraceViewer.spanBar().should('be.visible');
e2e.components.TraceViewer.spanBar()
.its('length')
.then((oldLength) => {
e2e.pages.Explore.General.scrollBar().scrollTo('center');
// After scrolling we should load more spans
e2e.components.TraceViewer.spanBar().should('have.length', 140);
e2e.components.TraceViewer.spanBar().its('length').should('be.gt', oldLength);
});
});
});

View File

@ -62,17 +62,17 @@ export class DataSourcePlugin<
return this;
}
setExploreQueryField(ExploreQueryField: ComponentType<ExploreQueryFieldProps<DSType, TQuery, TOptions>>) {
setExploreQueryField(ExploreQueryField: ComponentType<QueryEditorProps<DSType, TQuery, TOptions>>) {
this.components.ExploreQueryField = ExploreQueryField;
return this;
}
setExploreMetricsQueryField(ExploreQueryField: ComponentType<ExploreQueryFieldProps<DSType, TQuery, TOptions>>) {
setExploreMetricsQueryField(ExploreQueryField: ComponentType<QueryEditorProps<DSType, TQuery, TOptions>>) {
this.components.ExploreMetricsQueryField = ExploreQueryField;
return this;
}
setExploreLogsQueryField(ExploreQueryField: ComponentType<ExploreQueryFieldProps<DSType, TQuery, TOptions>>) {
setExploreLogsQueryField(ExploreQueryField: ComponentType<QueryEditorProps<DSType, TQuery, TOptions>>) {
this.components.ExploreLogsQueryField = ExploreQueryField;
return this;
}
@ -147,9 +147,9 @@ export interface DataSourcePluginComponents<
AnnotationsQueryCtrl?: any;
VariableQueryEditor?: any;
QueryEditor?: ComponentType<QueryEditorProps<DSType, TQuery, TOptions>>;
ExploreQueryField?: ComponentType<ExploreQueryFieldProps<DSType, TQuery, TOptions>>;
ExploreMetricsQueryField?: ComponentType<ExploreQueryFieldProps<DSType, TQuery, TOptions>>;
ExploreLogsQueryField?: ComponentType<ExploreQueryFieldProps<DSType, TQuery, TOptions>>;
ExploreQueryField?: ComponentType<QueryEditorProps<DSType, TQuery, TOptions>>;
ExploreMetricsQueryField?: ComponentType<QueryEditorProps<DSType, TQuery, TOptions>>;
ExploreLogsQueryField?: ComponentType<QueryEditorProps<DSType, TQuery, TOptions>>;
QueryEditorHelp?: ComponentType<QueryEditorHelpProps<TQuery>>;
ConfigEditor?: ComponentType<DataSourcePluginOptionsEditorProps<TOptions, TSecureOptions>>;
MetadataInspector?: ComponentType<MetadataInspectorProps<DSType, TQuery, TOptions>>;
@ -295,7 +295,8 @@ abstract class DataSourceApi<
modifyQuery?(query: TQuery, action: QueryFixAction): TQuery;
/**
* Used in explore
* @deprecated since version 8.2.0
* Not used anymore.
*/
getHighlighterExpression?(query: TQuery): string[];
@ -373,7 +374,7 @@ export interface QueryEditorProps<
data?: PanelData;
range?: TimeRange;
exploreId?: any;
history?: HistoryItem[];
history?: Array<HistoryItem<TQuery>>;
queries?: DataQuery[];
app?: CoreApp;
}
@ -385,15 +386,14 @@ export enum ExploreMode {
Tracing = 'Tracing',
}
export interface ExploreQueryFieldProps<
/**
* @deprecated use QueryEditorProps instead
*/
export type ExploreQueryFieldProps<
DSType extends DataSourceApi<TQuery, TOptions>,
TQuery extends DataQuery = DataQuery,
TOptions extends DataSourceJsonData = DataSourceJsonData
> extends QueryEditorProps<DSType, TQuery, TOptions> {
history: any[];
onBlur?: () => void;
exploreId?: any;
}
> = QueryEditorProps<DSType, TQuery, TOptions>;
export interface QueryEditorHelpProps<TQuery extends DataQuery = DataQuery> {
datasource: DataSourceApi<TQuery>;

View File

@ -1,10 +1,12 @@
import { DataQuery } from './query';
import { RawTimeRange, TimeRange } from './time';
type AnyQuery = DataQuery & Record<string, any>;
/** @internal */
export interface ExploreUrlState {
export interface ExploreUrlState<T extends DataQuery = AnyQuery> {
datasource: string;
queries: any[]; // Should be a DataQuery, but we're going to strip refIds, so typing makes less sense
queries: T[];
range: RawTimeRange;
originPanelId?: number;
context?: string;

View File

@ -33,7 +33,6 @@ import { LogRowMessage } from './LogRowMessage';
import { LogLabels } from './LogLabels';
interface Props extends Themeable2 {
highlighterExpressions?: string[];
row: LogRowModel;
showDuplicates: boolean;
showLabels: boolean;
@ -130,7 +129,6 @@ class UnThemedLogRow extends PureComponent<Props, State> {
onClickFilterOutLabel,
onClickShowDetectedField,
onClickHideDetectedField,
highlighterExpressions,
enableLogDetails,
row,
showDuplicates,
@ -192,7 +190,6 @@ class UnThemedLogRow extends PureComponent<Props, State> {
/>
) : (
<LogRowMessage
highlighterExpressions={highlighterExpressions}
row={processedRow}
getRows={getRows}
errors={errors}

View File

@ -1,5 +1,4 @@
import React, { PureComponent } from 'react';
import { isEqual } from 'lodash';
import tinycolor from 'tinycolor2';
import { css, cx } from '@emotion/css';
import { LogRowModel, findHighlightChunksInText, GrafanaTheme2 } from '@grafana/data';
@ -27,7 +26,6 @@ interface Props extends Themeable2 {
errors?: LogRowContextQueryErrors;
context?: LogRowContextRows;
showContextToggle?: (row?: LogRowModel) => boolean;
highlighterExpressions?: string[];
getRows: () => LogRowModel[];
onToggleContext: () => void;
updateLimit?: () => void;
@ -100,7 +98,6 @@ class UnThemedLogRowMessage extends PureComponent<Props> {
render() {
const {
highlighterExpressions,
row,
theme,
errors,
@ -118,11 +115,7 @@ class UnThemedLogRowMessage extends PureComponent<Props> {
const { hasAnsi, raw } = row;
const restructuredEntry = restructureLog(raw, prettifyLogMessage);
const previewHighlights = highlighterExpressions?.length && !isEqual(highlighterExpressions, row.searchWords);
const highlights = previewHighlights ? highlighterExpressions : row.searchWords;
const highlightClassName = previewHighlights
? cx([style.logsRowMatchHighLight, style.logsRowMatchHighLightPreview])
: cx([style.logsRowMatchHighLight]);
const highlightClassName = cx([style.logsRowMatchHighLight]);
const styles = getStyles(theme);
return (
@ -146,7 +139,7 @@ class UnThemedLogRowMessage extends PureComponent<Props> {
/>
)}
<span className={cx(styles.positionRelative, { [styles.rowWithContext]: contextIsOpen })}>
{renderLogMessage(hasAnsi, restructuredEntry, highlights, highlightClassName)}
{renderLogMessage(hasAnsi, restructuredEntry, row.searchWords, highlightClassName)}
</span>
{showContextToggle?.(row) && (
<span

View File

@ -12,7 +12,6 @@ describe('LogRows', () => {
<LogRows
logRows={rows}
dedupStrategy={LogsDedupStrategy.none}
highlighterExpressions={[]}
showLabels={false}
showTime={false}
wrapLogMessage={true}
@ -35,7 +34,6 @@ describe('LogRows', () => {
<LogRows
logRows={rows}
dedupStrategy={LogsDedupStrategy.none}
highlighterExpressions={[]}
showLabels={false}
showTime={false}
wrapLogMessage={true}
@ -67,7 +65,6 @@ describe('LogRows', () => {
logRows={rows}
deduplicatedRows={dedupedRows}
dedupStrategy={LogsDedupStrategy.none}
highlighterExpressions={[]}
showLabels={false}
showTime={false}
wrapLogMessage={true}
@ -89,7 +86,6 @@ describe('LogRows', () => {
<LogRows
logRows={rows}
dedupStrategy={LogsDedupStrategy.none}
highlighterExpressions={[]}
showLabels={false}
showTime={false}
wrapLogMessage={true}
@ -112,7 +108,6 @@ describe('LogRows', () => {
<LogRows
logRows={rows}
dedupStrategy={LogsDedupStrategy.none}
highlighterExpressions={[]}
showLabels={false}
showTime={false}
wrapLogMessage={true}
@ -137,7 +132,6 @@ describe('LogRows', () => {
<LogRows
logRows={rows}
dedupStrategy={LogsDedupStrategy.none}
highlighterExpressions={[]}
showLabels={false}
showTime={false}
wrapLogMessage={true}

View File

@ -16,7 +16,6 @@ export interface Props extends Themeable2 {
logRows?: LogRowModel[];
deduplicatedRows?: LogRowModel[];
dedupStrategy: LogsDedupStrategy;
highlighterExpressions?: string[];
showLabels: boolean;
showTime: boolean;
wrapLogMessage: boolean;
@ -88,7 +87,6 @@ class UnThemedLogRows extends PureComponent<Props, State> {
prettifyLogMessage,
logRows,
deduplicatedRows,
highlighterExpressions,
timeZone,
onClickFilterLabel,
onClickFilterOutLabel,
@ -129,7 +127,6 @@ class UnThemedLogRows extends PureComponent<Props, State> {
key={row.uid}
getRows={getRows}
getRowContext={getRowContext}
highlighterExpressions={highlighterExpressions}
row={row}
showContextToggle={showContextToggle}
showDuplicates={showDuplicates}

View File

@ -95,9 +95,11 @@ describe('state functions', () => {
queries: [
{
expr: 'metric{test="a/b"}',
refId: 'A',
},
{
expr: 'super{foo="x/z"}',
refId: 'B',
},
],
range: {
@ -107,8 +109,8 @@ describe('state functions', () => {
};
expect(serializeStateToUrlParam(state)).toBe(
'{"datasource":"foo","queries":[{"expr":"metric{test=\\"a/b\\"}"},' +
'{"expr":"super{foo=\\"x/z\\"}"}],"range":{"from":"now-5h","to":"now"}}'
'{"datasource":"foo","queries":[{"expr":"metric{test=\\"a/b\\"}","refId":"A"},' +
'{"expr":"super{foo=\\"x/z\\"}","refId":"B"}],"range":{"from":"now-5h","to":"now"}}'
);
});
@ -119,9 +121,11 @@ describe('state functions', () => {
queries: [
{
expr: 'metric{test="a/b"}',
refId: 'A',
},
{
expr: 'super{foo="x/z"}',
refId: 'B',
},
],
range: {
@ -130,7 +134,7 @@ describe('state functions', () => {
},
};
expect(serializeStateToUrlParam(state, true)).toBe(
'["now-5h","now","foo",{"expr":"metric{test=\\"a/b\\"}"},{"expr":"super{foo=\\"x/z\\"}"}]'
'["now-5h","now","foo",{"expr":"metric{test=\\"a/b\\"}","refId":"A"},{"expr":"super{foo=\\"x/z\\"}","refId":"B"}]'
);
});
});
@ -143,9 +147,11 @@ describe('state functions', () => {
queries: [
{
expr: 'metric{test="a/b"}',
refId: 'A',
},
{
expr: 'super{foo="x/z"}',
refId: 'B',
},
],
range: {
@ -165,9 +171,11 @@ describe('state functions', () => {
queries: [
{
expr: 'metric{test="a/b"}',
refId: 'A',
},
{
expr: 'super{foo="x/z"}',
refId: 'B',
},
],
range: {

View File

@ -165,11 +165,10 @@ export function buildQueryTransaction(
scanning,
id: generateKey(), // reusing for unique ID
done: false,
latency: 0,
};
}
export const clearQueryKeys: (query: DataQuery) => object = ({ key, refId, ...rest }) => rest;
export const clearQueryKeys: (query: DataQuery) => DataQuery = ({ key, ...rest }) => rest;
const isSegment = (segment: { [key: string]: string }, ...props: string[]) =>
props.some((prop) => segment.hasOwnProperty(prop));
@ -286,7 +285,7 @@ export function ensureQueries(queries?: DataQuery[]): DataQuery[] {
* A target is non-empty when it has keys (with non-empty values) other than refId, key and context.
*/
const validKeys = ['refId', 'key', 'context'];
export function hasNonEmptyQuery<TQuery extends DataQuery = any>(queries: TQuery[]): boolean {
export function hasNonEmptyQuery<TQuery extends DataQuery>(queries: TQuery[]): boolean {
return (
queries &&
queries.some((query: any) => {
@ -302,7 +301,7 @@ export function hasNonEmptyQuery<TQuery extends DataQuery = any>(queries: TQuery
/**
* Update the query history. Side-effect: store history in local storage
*/
export function updateHistory<T extends DataQuery = any>(
export function updateHistory<T extends DataQuery>(
history: Array<HistoryItem<T>>,
datasourceId: string,
queries: T[]

View File

@ -9,7 +9,7 @@ import { ErrorBoundaryAlert, CustomScrollbar, Collapse, withTheme2, Themeable2 }
import { AbsoluteTimeRange, DataQuery, LoadingState, RawTimeRange, DataFrame, GrafanaTheme2 } from '@grafana/data';
import LogsContainer from './LogsContainer';
import QueryRows from './QueryRows';
import { QueryRows } from './QueryRows';
import TableContainer from './TableContainer';
import RichHistoryContainer from './RichHistory/RichHistoryContainer';
import ExploreQueryInspector from './ExploreQueryInspector';
@ -268,7 +268,6 @@ export class Explore extends React.PureComponent<Props, ExploreState> {
datasourceInstance,
datasourceMissing,
exploreId,
queryKeys,
graphResult,
queryResponse,
isLive,
@ -292,7 +291,7 @@ export class Explore extends React.PureComponent<Props, ExploreState> {
{datasourceInstance && (
<div className="explore-container">
<div className={cx('panel-container', styles.queryContainer)}>
<QueryRows exploreId={exploreId} queryKeys={queryKeys} />
<QueryRows exploreId={exploreId} />
<SecondaryActions
addQueryRowButtonDisabled={isLive}
// We cannot show multiple traces at the same time right now so we do not show add query button.

View File

@ -53,7 +53,6 @@ interface Props extends Themeable2 {
logsQueries?: DataQuery[];
visibleRange?: AbsoluteTimeRange;
theme: GrafanaTheme2;
highlighterExpressions?: string[];
loading: boolean;
loadingState: LoadingState;
absoluteRange: AbsoluteTimeRange;
@ -254,7 +253,6 @@ export class UnthemedLogs extends PureComponent<Props, State> {
logsMeta,
logsSeries,
visibleRange,
highlighterExpressions,
loading = false,
loadingState,
onClickFilterLabel,
@ -368,7 +366,6 @@ export class UnthemedLogs extends PureComponent<Props, State> {
deduplicatedRows={dedupedRows}
dedupStrategy={dedupStrategy}
getRowContext={this.props.getRowContext}
highlighterExpressions={highlighterExpressions}
onClickFilterLabel={onClickFilterLabel}
onClickFilterOutLabel={onClickFilterOutLabel}
showContextToggle={showContextToggle}

View File

@ -62,7 +62,6 @@ export class LogsContainer extends PureComponent<LogsContainerProps> {
const {
loading,
loadingState,
logsHighlighterExpressions,
logRows,
logsMeta,
logsSeries,
@ -123,7 +122,6 @@ export class LogsContainer extends PureComponent<LogsContainerProps> {
logsSeries={logsSeries}
logsQueries={logsQueries}
width={width}
highlighterExpressions={logsHighlighterExpressions}
loading={loading}
loadingState={loadingState}
onChangeTime={this.onChangeTime}
@ -153,22 +151,11 @@ function mapStateToProps(state: StoreState, { exploreId }: { exploreId: string }
const explore = state.explore;
// @ts-ignore
const item: ExploreItemState = explore[exploreId];
const {
logsHighlighterExpressions,
logsResult,
loading,
scanning,
datasourceInstance,
isLive,
isPaused,
range,
absoluteRange,
} = item;
const { logsResult, loading, scanning, datasourceInstance, isLive, isPaused, range, absoluteRange } = item;
const timeZone = getTimeZone(state.user);
return {
loading,
logsHighlighterExpressions,
logRows: logsResult?.rows,
logsMeta: logsResult?.meta,
logsSeries: logsResult?.series,

View File

@ -1,104 +0,0 @@
// Libraries
import React, { PureComponent } from 'react';
// Services
import { getAngularLoader, AngularComponent } from '@grafana/runtime';
// Types
import { DataQuery, TimeRange, EventBusExtended } from '@grafana/data';
import 'app/features/plugins/plugin_loader';
interface QueryEditorProps {
error?: any;
datasource: any;
onExecuteQuery?: () => void;
onQueryChange?: (value: DataQuery) => void;
initialQuery: DataQuery;
exploreEvents: EventBusExtended;
range: TimeRange;
textEditModeEnabled?: boolean;
}
export default class QueryEditor extends PureComponent<QueryEditorProps, any> {
element: any;
component?: AngularComponent;
angularScope: any;
async componentDidMount() {
if (!this.element) {
return;
}
const { datasource, initialQuery, exploreEvents, range } = this.props;
const loader = getAngularLoader();
const template = '<plugin-component type="query-ctrl"> </plugin-component>';
const target = { datasource: datasource.name, ...initialQuery };
const scopeProps = {
ctrl: {
datasource,
target,
range,
refresh: () => {
setTimeout(() => {
// the "hide" attribute of the quries can be changed from the "outside",
// it will be applied to "this.props.initialQuery.hide", but not to "target.hide".
// so we have to apply it.
if (target.hide !== this.props.initialQuery.hide) {
target.hide = this.props.initialQuery.hide;
}
this.props.onQueryChange?.(target);
this.props.onExecuteQuery?.();
}, 1);
},
onQueryChange: () => {
setTimeout(() => {
this.props.onQueryChange?.(target);
}, 1);
},
events: exploreEvents,
panel: { datasource, targets: [target] },
dashboard: {},
},
};
this.component = loader.load(this.element, scopeProps, template);
this.angularScope = scopeProps.ctrl;
setTimeout(() => {
this.props.onQueryChange?.(target);
this.props.onExecuteQuery?.();
}, 1);
}
componentDidUpdate(prevProps: QueryEditorProps) {
const hasToggledEditorMode = prevProps.textEditModeEnabled !== this.props.textEditModeEnabled;
const hasNewError = prevProps.error !== this.props.error;
if (this.component) {
if (hasToggledEditorMode && this.angularScope && this.angularScope.toggleEditorMode) {
this.angularScope.toggleEditorMode();
}
if (this.angularScope) {
this.angularScope.range = this.props.range;
}
if (hasNewError || hasToggledEditorMode) {
// Some query controllers listen to data error events and need a digest
// for some reason this needs to be done in next tick
setTimeout(this.component.digest);
}
}
}
componentWillUnmount() {
if (this.component) {
this.component.destroy();
}
}
render() {
return <div className="gf-form-query" ref={(element) => (this.element = element)} style={{ width: '100%' }} />;
}
}

View File

@ -1,47 +0,0 @@
import React, { ComponentProps } from 'react';
import { QueryRow } from './QueryRow';
import { shallow } from 'enzyme';
import { ExploreId } from 'app/types/explore';
import { DataSourceApi, TimeRange, AbsoluteTimeRange, PanelData, EventBusExtended } from '@grafana/data';
const setup = (propOverrides?: object) => {
const props: ComponentProps<typeof QueryRow> = {
exploreId: ExploreId.left,
index: 1,
exploreEvents: {} as EventBusExtended,
changeQuery: jest.fn(),
datasourceInstance: {} as DataSourceApi,
highlightLogsExpressionAction: jest.fn() as any,
history: [],
query: {
refId: 'A',
},
modifyQueries: jest.fn(),
range: {} as TimeRange,
absoluteRange: {} as AbsoluteTimeRange,
removeQueryRowAction: jest.fn() as any,
runQueries: jest.fn(),
queryResponse: {} as PanelData,
latency: 1,
};
Object.assign(props, propOverrides);
const wrapper = shallow(<QueryRow {...props} />);
return wrapper;
};
const QueryEditor = () => <div />;
describe('QueryRow', () => {
describe('if datasource does not have Explore query fields ', () => {
it('it should render QueryEditor if datasource has it', () => {
const wrapper = setup({ datasourceInstance: { components: { QueryEditor } } });
expect(wrapper.find(QueryEditor)).toHaveLength(1);
});
it('it should not render QueryEditor if datasource does not have it', () => {
const wrapper = setup({ datasourceInstance: { components: {} } });
expect(wrapper.find(QueryEditor)).toHaveLength(0);
});
});
});

View File

@ -1,203 +0,0 @@
// Libraries
import React, { PureComponent } from 'react';
import { debounce, has } from 'lodash';
import { connect, ConnectedProps } from 'react-redux';
import AngularQueryEditor from './QueryEditor';
import { QueryRowActions } from './QueryRowActions';
import { StoreState } from 'app/types';
import { DataQuery, LoadingState, DataSourceApi } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { ExploreItemState, ExploreId } from 'app/types/explore';
import { highlightLogsExpressionAction } from './state/explorePane';
import { ErrorContainer } from './ErrorContainer';
import { changeQuery, modifyQueries, removeQueryRowAction, runQueries } from './state/query';
import { HelpToggle } from '../query/components/HelpToggle';
interface OwnProps {
exploreId: ExploreId;
index: number;
}
type QueryRowProps = OwnProps & ConnectedProps<typeof connector>;
interface QueryRowState {
textEditModeEnabled: boolean;
}
// Empty function to override blur execution on query field
const noopOnBlur = () => {};
export class QueryRow extends PureComponent<QueryRowProps, QueryRowState> {
state: QueryRowState = {
textEditModeEnabled: false,
};
onRunQuery = () => {
const { exploreId } = this.props;
this.props.runQueries(exploreId);
};
onChange = (query: DataQuery, override?: boolean) => {
const { datasourceInstance, exploreId, index } = this.props;
this.props.changeQuery(exploreId, query, index, override);
if (query && !override && datasourceInstance?.getHighlighterExpression && index === 0) {
// Live preview of log search matches. Only use on first row for now
this.updateLogsHighlights(query);
}
};
onClickToggleDisabled = () => {
const { exploreId, index, query } = this.props;
const newQuery = {
...query,
hide: !query.hide,
};
this.props.changeQuery(exploreId, newQuery, index, true);
};
onClickRemoveButton = () => {
const { exploreId, index } = this.props;
this.props.removeQueryRowAction({ exploreId, index });
this.props.runQueries(exploreId);
};
onClickToggleEditorMode = () => {
this.setState({ textEditModeEnabled: !this.state.textEditModeEnabled });
};
setReactQueryEditor = (datasourceInstance: DataSourceApi) => {
let QueryEditor;
// TODO:unification
if (datasourceInstance.components?.ExploreMetricsQueryField) {
QueryEditor = datasourceInstance.components.ExploreMetricsQueryField;
} else if (datasourceInstance.components?.ExploreLogsQueryField) {
QueryEditor = datasourceInstance.components.ExploreLogsQueryField;
} else if (datasourceInstance.components?.ExploreQueryField) {
QueryEditor = datasourceInstance.components.ExploreQueryField;
} else {
QueryEditor = datasourceInstance.components?.QueryEditor;
}
return QueryEditor;
};
renderQueryEditor = (datasourceInstance: DataSourceApi) => {
const { history, query, exploreEvents, range, queryResponse, exploreId } = this.props;
const queryErrors = queryResponse.error && queryResponse.error.refId === query.refId ? [queryResponse.error] : [];
const ReactQueryEditor = this.setReactQueryEditor(datasourceInstance);
let QueryEditor: JSX.Element;
if (ReactQueryEditor) {
QueryEditor = (
<ReactQueryEditor
datasource={datasourceInstance}
query={query}
history={history}
onRunQuery={this.onRunQuery}
onBlur={noopOnBlur}
onChange={this.onChange}
data={queryResponse}
range={range}
exploreId={exploreId}
/>
);
} else {
QueryEditor = (
<AngularQueryEditor
error={queryErrors}
datasource={datasourceInstance}
onQueryChange={this.onChange}
onExecuteQuery={this.onRunQuery}
initialQuery={query}
exploreEvents={exploreEvents}
range={range}
textEditModeEnabled={this.state.textEditModeEnabled}
/>
);
}
const DatasourceCheatsheet = datasourceInstance.components?.QueryEditorHelp;
return (
<>
{QueryEditor}
{DatasourceCheatsheet && (
<HelpToggle>
<DatasourceCheatsheet onClickExample={(query) => this.onChange(query)} datasource={datasourceInstance!} />
</HelpToggle>
)}
</>
);
};
updateLogsHighlights = debounce((value: DataQuery) => {
const { datasourceInstance } = this.props;
if (datasourceInstance?.getHighlighterExpression) {
const { exploreId } = this.props;
const expressions = datasourceInstance.getHighlighterExpression(value);
this.props.highlightLogsExpressionAction({ exploreId, expressions });
}
}, 500);
render() {
const { datasourceInstance, query, queryResponse, latency } = this.props;
if (!datasourceInstance) {
return <>Loading data source</>;
}
const canToggleEditorModes = has(datasourceInstance, 'components.QueryCtrl.prototype.toggleEditorMode');
const isNotStarted = queryResponse.state === LoadingState.NotStarted;
// We show error without refId in ResponseErrorContainer so this condition needs to match se we don't loose errors.
const queryErrors = queryResponse.error && queryResponse.error.refId === query.refId ? [queryResponse.error] : [];
return (
<>
<div className="query-row" aria-label={selectors.components.QueryEditorRows.rows}>
<div className="query-row-field flex-shrink-1">{this.renderQueryEditor(datasourceInstance)}</div>
<QueryRowActions
canToggleEditorModes={canToggleEditorModes}
isDisabled={query.hide}
isNotStarted={isNotStarted}
latency={latency}
onClickToggleEditorMode={this.onClickToggleEditorMode}
onClickToggleDisabled={this.onClickToggleDisabled}
onClickRemoveButton={this.onClickRemoveButton}
/>
</div>
{queryErrors.length > 0 && <ErrorContainer queryError={queryErrors[0]} />}
</>
);
}
}
function mapStateToProps(state: StoreState, { exploreId, index }: OwnProps) {
const explore = state.explore;
const item: ExploreItemState = explore[exploreId]!;
const { datasourceInstance, history, queries, range, absoluteRange, queryResponse, latency, eventBridge } = item;
const query = queries[index];
return {
datasourceInstance,
history,
query,
range,
absoluteRange,
queryResponse,
latency,
exploreEvents: eventBridge,
};
}
const mapDispatchToProps = {
changeQuery,
highlightLogsExpressionAction,
modifyQueries,
removeQueryRowAction,
runQueries,
};
const connector = connect(mapStateToProps, mapDispatchToProps);
export default connector(QueryRow);

View File

@ -1,39 +0,0 @@
import React from 'react';
import { QueryRowActions, Props } from './QueryRowActions';
import { shallow } from 'enzyme';
const setup = (propOverrides?: object) => {
const props: Props = {
isDisabled: false,
isNotStarted: true,
canToggleEditorModes: true,
onClickToggleEditorMode: () => {},
onClickToggleDisabled: () => {},
onClickRemoveButton: () => {},
latency: 0,
};
Object.assign(props, propOverrides);
const wrapper = shallow(<QueryRowActions {...props} />);
return wrapper;
};
describe('QueryRowActions', () => {
it('should render component', () => {
const wrapper = setup();
expect(wrapper).toMatchSnapshot();
});
it('should render component without editor mode', () => {
const wrapper = setup({ canToggleEditorModes: false });
expect(wrapper.find({ 'aria-label': 'Edit mode button' })).toHaveLength(0);
});
it('should change icon to eye-slash when query row result is hidden', () => {
const wrapper = setup({ isDisabled: true });
expect(wrapper.find({ title: 'Enable query' })).toHaveLength(1);
});
it('should change icon to eye when query row result is not hidden', () => {
const wrapper = setup({ isDisabled: false });
expect(wrapper.find({ title: 'Disable query' })).toHaveLength(1);
});
});

View File

@ -1,64 +0,0 @@
import React from 'react';
import { Icon } from '@grafana/ui';
function formatLatency(value: number) {
return `${(value / 1000).toFixed(1)}s`;
}
export type Props = {
canToggleEditorModes: boolean;
isDisabled?: boolean;
isNotStarted: boolean;
latency: number;
onClickToggleEditorMode: () => void;
onClickToggleDisabled: () => void;
onClickRemoveButton: () => void;
};
export function QueryRowActions(props: Props) {
const {
canToggleEditorModes,
onClickToggleEditorMode,
onClickToggleDisabled,
onClickRemoveButton,
isDisabled,
isNotStarted,
latency,
} = props;
return (
<div className="gf-form-inline flex-shrink-0">
{canToggleEditorModes && (
<div className="gf-form">
<button
aria-label="Edit mode button"
className="gf-form-label gf-form-label--btn"
onClick={onClickToggleEditorMode}
>
<Icon name="pen" />
</button>
</div>
)}
<div className="gf-form">
<button disabled className="gf-form-label" title="Query row latency">
{formatLatency(latency)}
</button>
</div>
<div className="gf-form">
<button
disabled={isNotStarted}
className="gf-form-label gf-form-label--btn"
onClick={onClickToggleDisabled}
title={isDisabled ? 'Enable query' : 'Disable query'}
>
<Icon name={isDisabled ? 'eye-slash' : 'eye'} />
</button>
</div>
<div className="gf-form">
<button className="gf-form-label gf-form-label--btn" onClick={onClickRemoveButton} title="Remove query">
<Icon name="minus" />
</button>
</div>
</div>
);
}

View File

@ -0,0 +1,80 @@
import React from 'react';
import { fireEvent, render, screen } from '@testing-library/react';
import { configureStore } from 'app/store/configureStore';
import { Provider } from 'react-redux';
import { QueryRows } from './QueryRows';
import { ExploreId, ExploreState } from 'app/types';
import { makeExplorePaneState } from './state/utils';
import { setDataSourceSrv } from '@grafana/runtime';
import { UserState } from '../profile/state/reducers';
import { DataQuery } from '../../../../packages/grafana-data/src';
function setup(queries: DataQuery[]) {
const defaultDs = {
name: 'newDs',
meta: { id: 'newDs' },
};
const datasources: Record<string, any> = {
newDs: defaultDs,
someDs: {
name: 'someDs',
meta: { id: 'someDs' },
components: {
QueryEditor: () => 'someDs query editor',
},
},
};
setDataSourceSrv({
getList() {
return Object.values(datasources).map((d) => ({ name: d.name }));
},
getInstanceSettings(name: string) {
return datasources[name] || defaultDs;
},
get(name?: string) {
return Promise.resolve(name ? datasources[name] || defaultDs : defaultDs);
},
} as any);
const leftState = makeExplorePaneState();
const initialState: ExploreState = {
left: {
...leftState,
datasourceInstance: datasources.someDs,
queries,
},
syncedTimes: false,
right: undefined,
richHistory: [],
};
const store = configureStore({ explore: initialState, user: { orgId: 1 } as UserState });
return {
store,
datasources,
};
}
describe('Explore QueryRows', () => {
it('Should duplicate a query and generate a valid refId', async () => {
const { store } = setup([{ refId: 'A' }]);
render(
<Provider store={store}>
<QueryRows exploreId={ExploreId.left} />
</Provider>
);
// waiting for the d&d component to fully render.
await screen.findAllByText('someDs query editor');
let duplicateButton = screen.getByTitle('Duplicate query');
fireEvent.click(duplicateButton);
// We should have another row with refId B
expect(await screen.findByLabelText('Query editor row title B')).toBeInTheDocument();
});
});

View File

@ -1,27 +1,79 @@
// Libraries
import React, { PureComponent } from 'react';
// Components
import QueryRow from './QueryRow';
// Types
import React, { useCallback, useMemo } from 'react';
import { ExploreId } from 'app/types/explore';
import { useDispatch, useSelector } from 'react-redux';
import { getDatasourceSrv } from '../plugins/datasource_srv';
import { runQueries, changeQueriesAction } from './state/query';
import { CoreApp, DataQuery } from '@grafana/data';
import { getNextRefIdChar } from 'app/core/utils/query';
import { QueryEditorRows } from '../query/components/QueryEditorRows';
import { createSelector } from '@reduxjs/toolkit';
import { getExploreItemSelector } from './state/selectors';
interface QueryRowsProps {
className?: string;
interface Props {
exploreId: ExploreId;
queryKeys: string[];
}
export default class QueryRows extends PureComponent<QueryRowsProps> {
render() {
const { className = '', exploreId, queryKeys } = this.props;
return (
<div className={className}>
{queryKeys.map((key, index) => {
return <QueryRow key={key} exploreId={exploreId} index={index} />;
})}
</div>
const makeSelectors = (exploreId: ExploreId) => {
const exploreItemSelector = getExploreItemSelector(exploreId);
return {
getQueries: createSelector(exploreItemSelector, (s) => s!.queries),
getQueryResponse: createSelector(exploreItemSelector, (s) => s!.queryResponse),
getHistory: createSelector(exploreItemSelector, (s) => s!.history),
getEventBridge: createSelector(exploreItemSelector, (s) => s!.eventBridge),
getDatasourceInstanceSettings: createSelector(
exploreItemSelector,
(s) => getDatasourceSrv().getInstanceSettings(s!.datasourceInstance?.name)!
),
};
};
export const QueryRows = ({ exploreId }: Props) => {
const dispatch = useDispatch();
const { getQueries, getDatasourceInstanceSettings, getQueryResponse, getHistory, getEventBridge } = useMemo(
() => makeSelectors(exploreId),
[exploreId]
);
const queries = useSelector(getQueries);
const dsSettings = useSelector(getDatasourceInstanceSettings);
const queryResponse = useSelector(getQueryResponse);
const history = useSelector(getHistory);
const eventBridge = useSelector(getEventBridge);
const onRunQueries = useCallback(() => {
dispatch(runQueries(exploreId));
}, [dispatch, exploreId]);
const onChange = useCallback(
(newQueries: DataQuery[]) => {
dispatch(changeQueriesAction({ queries: newQueries, exploreId }));
// if we are removing a query we want to run the remaining ones
if (newQueries.length < queries.length) {
onRunQueries();
}
}
},
[dispatch, exploreId, onRunQueries, queries]
);
const onAddQuery = useCallback(
(query: DataQuery) => {
onChange([...queries, { ...query, refId: getNextRefIdChar(queries) }]);
},
[onChange, queries]
);
return (
<QueryEditorRows
dsSettings={dsSettings}
queries={queries}
onQueriesChange={onChange}
onAddQuery={onAddQuery}
onRunQueries={onRunQueries}
data={queryResponse}
app={CoreApp.Explore}
history={history}
eventBus={eventBridge}
/>
);
};

View File

@ -1,19 +0,0 @@
import React from 'react';
import { shallow } from 'enzyme';
import { LoadingState, TimeRange, PanelData } from '@grafana/data';
import QueryStatus from './QueryStatus';
describe('<QueryStatus />', () => {
it('should render with a latency', () => {
const res: PanelData = { series: [], state: LoadingState.Done, timeRange: {} as TimeRange };
const wrapper = shallow(<QueryStatus latency={0} queryResponse={res} />);
expect(wrapper.find('div').exists()).toBeTruthy();
});
it('should not render when query has not started', () => {
const res: PanelData = { series: [], state: LoadingState.NotStarted, timeRange: {} as TimeRange };
const wrapper = shallow(<QueryStatus latency={0} queryResponse={res} />);
expect(wrapper.getElement()).toBe(null);
});
});

View File

@ -1,52 +0,0 @@
import React, { PureComponent } from 'react';
import { ElapsedTime } from './ElapsedTime';
import { PanelData, LoadingState } from '@grafana/data';
function formatLatency(value: number) {
return `${(value / 1000).toFixed(1)}s`;
}
interface QueryStatusItemProps {
queryResponse: PanelData;
latency: number;
}
class QueryStatusItem extends PureComponent<QueryStatusItemProps> {
render() {
const { queryResponse, latency } = this.props;
const className =
queryResponse.state === LoadingState.Done || LoadingState.Error
? 'query-transaction'
: 'query-transaction query-transaction--loading';
return (
<div className={className}>
{/* <div className="query-transaction__type">{transaction.resultType}:</div> */}
<div className="query-transaction__duration">
{queryResponse.state === LoadingState.Done || LoadingState.Error ? formatLatency(latency) : <ElapsedTime />}
</div>
</div>
);
}
}
interface QueryStatusProps {
queryResponse: PanelData;
latency: number;
}
export default class QueryStatus extends PureComponent<QueryStatusProps> {
render() {
const { queryResponse, latency } = this.props;
if (queryResponse.state === LoadingState.NotStarted) {
return null;
}
return (
<div className="query-transactions">
<QueryStatusItem queryResponse={queryResponse} latency={latency} />
</div>
);
}
}

View File

@ -15,7 +15,6 @@ import {
} from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { setTimeSrv } from '../dashboard/services/TimeSrv';
import { from, Observable } from 'rxjs';
import { LokiDatasource } from '../../plugins/datasource/loki/datasource';
import { LokiQuery } from '../../plugins/datasource/loki/types';
@ -63,13 +62,13 @@ describe('Wrapper', () => {
// At this point url should be initialised to some defaults
expect(locationService.getSearchObject()).toEqual({
orgId: '1',
left: JSON.stringify(['now-1h', 'now', 'loki', {}]),
left: JSON.stringify(['now-1h', 'now', 'loki', { refId: 'A' }]),
});
expect(datasources.loki.query).not.toBeCalled();
});
it('runs query when url contains query and renders results', async () => {
const query = { left: JSON.stringify(['now-1h', 'now', 'loki', { expr: '{ label="value"}' }]) };
const query = { left: JSON.stringify(['now-1h', 'now', 'loki', { expr: '{ label="value"}', refId: 'A' }]) };
const { datasources, store } = setup({ query });
(datasources.loki.query as Mock).mockReturnValueOnce(makeLogsQueryResponse());
@ -91,7 +90,7 @@ describe('Wrapper', () => {
expect(store.getState().explore.richHistory[0]).toMatchObject({
datasourceId: '1',
datasourceName: 'loki',
queries: [{ expr: '{ label="value"}' }],
queries: [{ expr: '{ label="value"}', refId: 'A' }],
});
// We called the data source query method once
@ -141,7 +140,7 @@ describe('Wrapper', () => {
});
it('handles changing the datasource manually', async () => {
const query = { left: JSON.stringify(['now-1h', 'now', 'loki', { expr: '{ label="value"}' }]) };
const query = { left: JSON.stringify(['now-1h', 'now', 'loki', { expr: '{ label="value"}', refId: 'A' }]) };
const { datasources } = setup({ query });
(datasources.loki.query as Mock).mockReturnValueOnce(makeLogsQueryResponse());
// Wait for rendering the editor
@ -152,7 +151,7 @@ describe('Wrapper', () => {
expect(datasources.elastic.query).not.toBeCalled();
expect(locationService.getSearchObject()).toEqual({
orgId: '1',
left: JSON.stringify(['now-1h', 'now', 'elastic', {}]),
left: JSON.stringify(['now-1h', 'now', 'elastic', { refId: 'A' }]),
});
});
@ -169,8 +168,8 @@ describe('Wrapper', () => {
it('inits with two panes if specified in url', async () => {
const query = {
left: JSON.stringify(['now-1h', 'now', 'loki', { expr: '{ label="value"}' }]),
right: JSON.stringify(['now-1h', 'now', 'elastic', { expr: 'error' }]),
left: JSON.stringify(['now-1h', 'now', 'loki', { expr: '{ label="value"}', refId: 'A' }]),
right: JSON.stringify(['now-1h', 'now', 'elastic', { expr: 'error', refId: 'A' }]),
};
const { datasources } = setup({ query });
@ -211,8 +210,8 @@ describe('Wrapper', () => {
it('can close a pane from a split', async () => {
const query = {
left: JSON.stringify(['now-1h', 'now', 'loki', {}]),
right: JSON.stringify(['now-1h', 'now', 'elastic', {}]),
left: JSON.stringify(['now-1h', 'now', 'loki', { refId: 'A' }]),
right: JSON.stringify(['now-1h', 'now', 'elastic', { refId: 'A' }]),
};
setup({ query });
const closeButtons = await screen.findAllByTitle(/Close split pane/i);
@ -325,12 +324,6 @@ function setup(options?: SetupOptions): { datasources: { [name: string]: DataSou
},
} as any);
setTimeSrv({
init() {},
getValidIntervals(intervals: string[]): string[] {
return intervals;
},
} as any);
setEchoSrv(new Echo());
const store = configureStore();

View File

@ -16,7 +16,6 @@ exports[`Explore should render component 1`] = `
>
<QueryRows
exploreId="left"
queryKeys={Array []}
/>
<SecondaryActions
addQueryRowButtonDisabled={false}

View File

@ -1,59 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`QueryRowActions should render component 1`] = `
<div
className="gf-form-inline flex-shrink-0"
>
<div
className="gf-form"
>
<button
aria-label="Edit mode button"
className="gf-form-label gf-form-label--btn"
onClick={[Function]}
>
<Icon
name="pen"
/>
</button>
</div>
<div
className="gf-form"
>
<button
className="gf-form-label"
disabled={true}
title="Query row latency"
>
0.0s
</button>
</div>
<div
className="gf-form"
>
<button
className="gf-form-label gf-form-label--btn"
disabled={true}
onClick={[Function]}
title="Disable query"
>
<Icon
name="eye"
/>
</button>
</div>
<div
className="gf-form"
>
<button
className="gf-form-label gf-form-label--btn"
onClick={[Function]}
title="Remove query"
>
<Icon
name="minus"
/>
</button>
</div>
</div>
`;

View File

@ -35,7 +35,6 @@ describe('Datasource reducer', () => {
graphResult: null,
logsResult: null,
tableResult: null,
latency: 0,
loading: false,
queryResponse: {
// When creating an empty query response we also create a timeRange object with the current time.

View File

@ -92,13 +92,11 @@ export const datasourceReducer = (state: ExploreItemState, action: AnyAction): E
graphResult: null,
tableResult: null,
logsResult: null,
latency: 0,
queryResponse: createEmptyQueryResponse(),
loading: false,
queryKeys: [],
history,
datasourceMissing: false,
logsHighlighterExpressions: undefined,
};
}

View File

@ -119,7 +119,7 @@ describe('refreshExplore', () => {
await dispatch(
refreshExplore(
ExploreId.left,
serializeStateToUrlParam({ datasource: 'someDs', queries: [{ expr: 'count()' }], range: testRange })
serializeStateToUrlParam({ datasource: 'someDs', queries: [{ expr: 'count()', refId: 'A' }], range: testRange })
)
);
// same
@ -138,7 +138,7 @@ describe('refreshExplore', () => {
await dispatch(
refreshExplore(
ExploreId.left,
serializeStateToUrlParam({ datasource: 'newDs', queries: [{ expr: 'count()' }], range: testRange })
serializeStateToUrlParam({ datasource: 'newDs', queries: [{ expr: 'count()', refId: 'A' }], range: testRange })
)
);

View File

@ -44,17 +44,6 @@ export interface ChangeSizePayload {
}
export const changeSizeAction = createAction<ChangeSizePayload>('explore/changeSize');
/**
* Highlight expressions in the log results
*/
export interface HighlightLogsExpressionPayload {
exploreId: ExploreId;
expressions: string[];
}
export const highlightLogsExpressionAction = createAction<HighlightLogsExpressionPayload>(
'explore/highlightLogsExpression'
);
/**
* Initialize Explore state with state from the URL and the React component.
* Call this only on components for with the Explore state has not been initialized.
@ -210,17 +199,6 @@ export const paneReducer = (state: ExploreItemState = makeExplorePaneState(), ac
return { ...state, containerWidth };
}
if (highlightLogsExpressionAction.match(action)) {
const { expressions: newExpressions } = action.payload;
const { logsHighlighterExpressions: currentExpressions } = state;
return {
...state,
// Prevents re-renders. As logsHighlighterExpressions [] comes from datasource, we cannot control if it returns new array or not.
logsHighlighterExpressions: isEqual(newExpressions, currentExpressions) ? currentExpressions : newExpressions,
};
}
if (initializeExploreAction.match(action)) {
const { containerWidth, eventBridge, queries, range, originPanelId, datasourceInstance, history } = action.payload;
@ -237,7 +215,6 @@ export const paneReducer = (state: ExploreItemState = makeExplorePaneState(), ac
history,
datasourceMissing: !datasourceInstance,
queryResponse: createEmptyQueryResponse(),
logsHighlighterExpressions: undefined,
cache: [],
};
}

View File

@ -6,7 +6,6 @@ import {
clearCache,
importQueries,
queryReducer,
removeQueryRowAction,
runQueries,
scanStartAction,
scanStopAction,
@ -34,7 +33,6 @@ import { configureStore } from '../../../store/configureStore';
import { setTimeSrv } from '../../dashboard/services/TimeSrv';
import Mock = jest.Mock;
const QUERY_KEY_REGEX = /Q-(?:[a-z0-9]+-){5}(?:[0-9]+)/;
const t = toUtc();
const testRange = {
from: t,
@ -213,55 +211,6 @@ describe('reducer', () => {
queryKeys: ['mockKey-0'],
} as unknown) as ExploreItemState);
});
it('removes a query row', () => {
reducerTester<ExploreItemState>()
.givenReducer(queryReducer, ({
queries: [
{ refId: 'A', key: 'mockKey' },
{ refId: 'B', key: 'mockKey' },
],
queryKeys: ['mockKey-0', 'mockKey-1'],
} as unknown) as ExploreItemState)
.whenActionIsDispatched(
removeQueryRowAction({
exploreId: ExploreId.left,
index: 0,
})
)
.thenStatePredicateShouldEqual((resultingState: ExploreItemState) => {
expect(resultingState.queries.length).toBe(1);
expect(resultingState.queries[0].refId).toBe('A');
expect(resultingState.queries[0].key).toMatch(QUERY_KEY_REGEX);
expect(resultingState.queryKeys[0]).toMatch(QUERY_KEY_REGEX);
return true;
});
});
it('reassigns query refId after removing a query to keep queries in order', () => {
reducerTester<ExploreItemState>()
.givenReducer(queryReducer, ({
queries: [{ refId: 'A' }, { refId: 'B' }, { refId: 'C' }],
queryKeys: ['undefined-0', 'undefined-1', 'undefined-2'],
} as unknown) as ExploreItemState)
.whenActionIsDispatched(
removeQueryRowAction({
exploreId: ExploreId.left,
index: 0,
})
)
.thenStatePredicateShouldEqual((resultingState: ExploreItemState) => {
expect(resultingState.queries.length).toBe(2);
const queriesRefIds = resultingState.queries.map((query) => query.refId);
const queriesKeys = resultingState.queries.map((query) => query.key);
expect(queriesRefIds).toEqual(['A', 'B']);
queriesKeys.forEach((queryKey) => {
expect(queryKey).toMatch(QUERY_KEY_REGEX);
});
resultingState.queryKeys.forEach((queryKey) => {
expect(queryKey).toMatch(QUERY_KEY_REGEX);
});
return true;
});
});
});
describe('caching', () => {

View File

@ -50,26 +50,15 @@ export interface AddQueryRowPayload {
}
export const addQueryRowAction = createAction<AddQueryRowPayload>('explore/addQueryRow');
/**
* Remove query row of the given index, as well as associated query results.
*/
export interface RemoveQueryRowPayload {
exploreId: ExploreId;
index: number;
}
export const removeQueryRowAction = createAction<RemoveQueryRowPayload>('explore/removeQueryRow');
/**
* Query change handler for the query row with the given index.
* If `override` is reset the query modifications and run the queries. Use this to set queries via a link.
*/
export interface ChangeQueryPayload {
export interface ChangeQueriesPayload {
exploreId: ExploreId;
query: DataQuery;
index: number;
override: boolean;
queries: DataQuery[];
}
export const changeQueryAction = createAction<ChangeQueryPayload>('explore/changeQuery');
export const changeQueriesAction = createAction<ChangeQueriesPayload>('explore/changeQueries');
/**
* Clear all queries and results.
@ -193,31 +182,6 @@ export function addQueryRow(exploreId: ExploreId, index: number): ThunkResult<vo
};
}
/**
* Query change handler for the query row with the given index.
* If `override` is reset the query modifications and run the queries. Use this to set queries via a link.
*/
export function changeQuery(
exploreId: ExploreId,
query: DataQuery,
index: number,
override = false
): ThunkResult<void> {
return (dispatch, getState) => {
// Null query means reset
if (query === null) {
const queries = getState().explore[exploreId]!.queries;
const { refId, key } = queries[index];
query = generateNewKeyAndAddRefIdIfMissing({ refId, key }, queries, index);
}
dispatch(changeQueryAction({ exploreId, query, index, override }));
if (override) {
dispatch(runQueries(exploreId));
}
};
}
/**
* Clear all queries and results.
*/
@ -352,7 +316,6 @@ export const runQueries = (
// If we don't have results saved in cache, run new queries
} else {
if (!hasNonEmptyQuery(queries)) {
dispatch(clearQueriesAction({ exploreId }));
dispatch(stateSave({ replace: options?.replaceUrl })); // Remember to save to state and update location
return;
}
@ -515,23 +478,16 @@ export const queryReducer = (state: ExploreItemState, action: AnyAction): Explor
return {
...state,
queries: nextQueries,
logsHighlighterExpressions: undefined,
queryKeys: getQueryKeys(nextQueries, state.datasourceInstance),
};
}
if (changeQueryAction.match(action)) {
const { queries } = state;
const { query, index } = action.payload;
// Override path: queries are completely reset
const nextQuery: DataQuery = generateNewKeyAndAddRefIdIfMissing(query, queries, index);
const nextQueries = [...queries];
nextQueries[index] = nextQuery;
if (changeQueriesAction.match(action)) {
const { queries } = action.payload;
return {
...state,
queries: nextQueries,
queries,
};
}
@ -587,33 +543,6 @@ export const queryReducer = (state: ExploreItemState, action: AnyAction): Explor
};
}
if (removeQueryRowAction.match(action)) {
const { queries } = state;
const { index } = action.payload;
if (queries.length <= 1) {
return state;
}
// removes a query under a given index and reassigns query keys and refIds to keep everything in order
const queriesAfterRemoval: DataQuery[] = [...queries.slice(0, index), ...queries.slice(index + 1)].map((query) => {
return { ...query, refId: '' };
});
const nextQueries: DataQuery[] = [];
queriesAfterRemoval.forEach((query, i) => {
nextQueries.push(generateNewKeyAndAddRefIdIfMissing(query, nextQueries, i));
});
return {
...state,
queries: nextQueries,
logsHighlighterExpressions: undefined,
queryKeys: getQueryKeys(nextQueries, state.datasourceInstance),
};
}
if (setQueriesAction.match(action)) {
const { queries } = action.payload;
return {
@ -752,8 +681,6 @@ export const processQueryResponse = (
return { ...state };
}
const latency = request.endTime ? request.endTime - request.startTime : 0;
// Send legacy data to Angular editors
if (state.datasourceInstance?.components?.QueryCtrl) {
const legacy = series.map((v) => toLegacyResponseData(v));
@ -762,7 +689,6 @@ export const processQueryResponse = (
return {
...state,
latency,
queryResponse: response,
graphResult,
tableResult,

View File

@ -1,3 +1,5 @@
import { ExploreId, StoreState } from 'app/types';
export const isSplit = (state: StoreState) => Boolean(state.explore[ExploreId.left] && state.explore[ExploreId.right]);
export const getExploreItemSelector = (exploreId: ExploreId) => (state: StoreState) => state.explore[exploreId];

View File

@ -42,7 +42,6 @@ export const makeExplorePaneState = (): ExploreItemState => ({
scanning: false,
loading: false,
queryKeys: [],
latency: 0,
isLive: false,
isPaused: false,
queryResponse: createEmptyQueryResponse(),

View File

@ -8,11 +8,13 @@ import { AngularComponent, getAngularLoader } from '@grafana/runtime';
import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv';
import { ErrorBoundaryAlert, HorizontalGroup } from '@grafana/ui';
import {
CoreApp,
DataQuery,
DataSourceApi,
DataSourceInstanceSettings,
EventBusExtended,
EventBusSrv,
HistoryItem,
LoadingState,
PanelData,
PanelEvents,
@ -45,6 +47,9 @@ interface Props<TQuery extends DataQuery> {
onRunQuery: () => void;
visualization?: ReactNode;
hideDisableQuery?: boolean;
app?: CoreApp;
history?: Array<HistoryItem<TQuery>>;
eventBus?: EventBusExtended;
}
interface State<TQuery extends DataQuery> {
@ -108,7 +113,7 @@ export class QueryEditorRow<TQuery extends DataQuery> extends PureComponent<Prop
this.props.onRunQuery();
},
render: () => () => console.log('legacy render function called, it does nothing'),
events: new EventBusSrv(),
events: this.props.eventBus || new EventBusSrv(),
range: getTimeSrv().timeRange(),
};
}
@ -193,17 +198,37 @@ export class QueryEditorRow<TQuery extends DataQuery> extends PureComponent<Prop
this.renderAngularQueryEditor();
};
getReactQueryEditor(ds: DataSourceApi<TQuery>) {
if (!ds) {
return;
}
switch (this.props.app) {
case CoreApp.Explore:
return (
ds.components?.ExploreMetricsQueryField ||
ds.components?.ExploreLogsQueryField ||
ds.components?.ExploreQueryField ||
ds.components?.QueryEditor
);
case CoreApp.Dashboard:
default:
return ds.components?.QueryEditor;
}
}
renderPluginEditor = () => {
const { query, onChange, queries, onRunQuery } = this.props;
const { query, onChange, queries, onRunQuery, app = CoreApp.Dashboard, history } = this.props;
const { datasource, data } = this.state;
if (datasource?.components?.QueryCtrl) {
return <div ref={(element) => (this.element = element)} />;
}
if (datasource?.components?.QueryEditor) {
const QueryEditor = datasource.components.QueryEditor;
if (datasource) {
let QueryEditor = this.getReactQueryEditor(datasource);
if (QueryEditor) {
return (
<QueryEditor
key={datasource?.name}
@ -214,9 +239,12 @@ export class QueryEditorRow<TQuery extends DataQuery> extends PureComponent<Prop
data={data}
range={getTimeSrv().timeRange()}
queries={queries}
app={app}
history={history}
/>
);
}
}
return <div>Data source plugin does not export any Query Editor component</div>;
};

View File

@ -2,7 +2,14 @@
import React, { PureComponent } from 'react';
// Types
import { DataQuery, DataSourceInstanceSettings, PanelData } from '@grafana/data';
import {
CoreApp,
DataQuery,
DataSourceInstanceSettings,
EventBusExtended,
HistoryItem,
PanelData,
} from '@grafana/data';
import { QueryEditorRow } from './QueryEditorRow';
import { DragDropContext, Droppable, DropResult } from 'react-beautiful-dnd';
import { getDataSourceSrv } from '@grafana/runtime';
@ -19,6 +26,11 @@ interface Props {
// Query Response Data
data: PanelData;
// Misc
app?: CoreApp;
history?: Array<HistoryItem<DataQuery>>;
eventBus?: EventBusExtended;
}
export class QueryEditorRows extends PureComponent<Props> {
@ -89,7 +101,7 @@ export class QueryEditorRows extends PureComponent<Props> {
};
render() {
const { dsSettings, data, queries } = this.props;
const { dsSettings, data, queries, app, history, eventBus } = this.props;
return (
<DragDropContext onDragEnd={this.onDragEnd}>
@ -117,6 +129,9 @@ export class QueryEditorRows extends PureComponent<Props> {
onAddQuery={this.props.onAddQuery}
onRunQuery={this.props.onRunQueries}
queries={queries}
app={app}
history={history}
eventBus={eventBus}
/>
);
})}

View File

@ -1,6 +1,6 @@
import React, { PureComponent } from 'react';
import { css } from '@emotion/css';
import { ExploreQueryFieldProps } from '@grafana/data';
import { QueryEditorProps } from '@grafana/data';
import { Button, Select } from '@grafana/ui';
import { MetricQueryEditor, SLOQueryEditor, QueryEditorRow } from './';
import { CloudMonitoringQuery, MetricQuery, QueryType, SLOQuery, EditorMode } from '../types';
@ -10,7 +10,7 @@ import { defaultQuery as defaultSLOQuery } from './SLO/SLOQueryEditor';
import { toOption } from '../functions';
import CloudMonitoringDatasource from '../datasource';
export type Props = ExploreQueryFieldProps<CloudMonitoringDatasource, CloudMonitoringQuery>;
export type Props = QueryEditorProps<CloudMonitoringDatasource, CloudMonitoringQuery>;
export class QueryEditor extends PureComponent<Props> {
async UNSAFE_componentWillMount() {

View File

@ -19,7 +19,7 @@ import { Editor, Node, Plugin } from 'slate';
import syntax from '../syntax';
// Types
import { AbsoluteTimeRange, ExploreQueryFieldProps, SelectableValue } from '@grafana/data';
import { AbsoluteTimeRange, QueryEditorProps, SelectableValue } from '@grafana/data';
import { CloudWatchJsonData, CloudWatchLogsQuery, CloudWatchQuery } from '../types';
import { CloudWatchDatasource } from '../datasource';
import { LanguageMap, languages as prismLanguages } from 'prismjs';
@ -33,7 +33,7 @@ import { InputActionMeta } from '@grafana/ui/src/components/Select/types';
import { getStatsGroups } from '../utils/query/getStatsGroups';
export interface CloudWatchLogsQueryFieldProps
extends ExploreQueryFieldProps<CloudWatchDatasource, CloudWatchQuery, CloudWatchJsonData> {
extends QueryEditorProps<CloudWatchDatasource, CloudWatchQuery, CloudWatchJsonData> {
absoluteRange: AbsoluteTimeRange;
onLabelsRefresh?: () => void;
ExtraFieldElement?: ReactNode;

View File

@ -1,13 +1,13 @@
import React, { PureComponent, ChangeEvent } from 'react';
import { ExploreQueryFieldProps, PanelData } from '@grafana/data';
import { QueryEditorProps, PanelData } from '@grafana/data';
import { LegacyForms, ValidationEvents, EventsWithValidation, Icon } from '@grafana/ui';
const { Input, Switch } = LegacyForms;
import { CloudWatchQuery, CloudWatchMetricsQuery, CloudWatchJsonData, ExecutedQueryPreview } from '../types';
import { CloudWatchDatasource } from '../datasource';
import { QueryField, Alias, MetricsQueryFieldsEditor } from './';
export type Props = ExploreQueryFieldProps<CloudWatchDatasource, CloudWatchQuery, CloudWatchJsonData>;
export type Props = QueryEditorProps<CloudWatchDatasource, CloudWatchQuery, CloudWatchJsonData>;
interface State {
showMeta: boolean;

View File

@ -1,6 +1,6 @@
import React, { PureComponent } from 'react';
import { pick } from 'lodash';
import { ExploreQueryFieldProps, ExploreMode } from '@grafana/data';
import { QueryEditorProps, ExploreMode } from '@grafana/data';
import { Segment } from '@grafana/ui';
import { CloudWatchJsonData, CloudWatchQuery } from '../types';
import { CloudWatchDatasource } from '../datasource';
@ -8,7 +8,7 @@ import { QueryInlineField } from './';
import { MetricsQueryEditor } from './MetricsQueryEditor';
import LogsQueryEditor from './LogsQueryEditor';
export type Props = ExploreQueryFieldProps<CloudWatchDatasource, CloudWatchQuery, CloudWatchJsonData>;
export type Props = QueryEditorProps<CloudWatchDatasource, CloudWatchQuery, CloudWatchJsonData>;
const apiModes = {
Metrics: { label: 'CloudWatch Metrics', value: 'Metrics' },

View File

@ -2,13 +2,13 @@
import React, { memo } from 'react';
// Types
import { ExploreQueryFieldProps } from '@grafana/data';
import { QueryEditorProps } from '@grafana/data';
import { LokiDatasource } from '../datasource';
import { LokiQuery, LokiOptions } from '../types';
import { LokiQueryField } from './LokiQueryField';
import { LokiOptionFields } from './LokiOptionFields';
type Props = ExploreQueryFieldProps<LokiDatasource, LokiQuery, LokiOptions>;
type Props = QueryEditorProps<LokiDatasource, LokiQuery, LokiOptions>;
export function LokiExploreQueryEditor(props: Props) {
const { query, data, datasource, history, onChange, onRunQuery, range } = props;

View File

@ -11,10 +11,10 @@ import {
} from '@grafana/ui';
import { Plugin, Node } from 'slate';
import { LokiLabelBrowser } from './LokiLabelBrowser';
import { ExploreQueryFieldProps } from '@grafana/data';
import { QueryEditorProps } from '@grafana/data';
import { LokiQuery, LokiOptions } from '../types';
import { LanguageMap, languages as prismLanguages } from 'prismjs';
import LokiLanguageProvider, { LokiHistoryItem } from '../language_provider';
import LokiLanguageProvider from '../language_provider';
import { shouldRefreshLabels } from '../language_utils';
import LokiDatasource from '../datasource';
@ -55,8 +55,7 @@ function willApplySuggestion(suggestion: string, { typeaheadContext, typeaheadTe
return suggestion;
}
export interface LokiQueryFieldProps extends ExploreQueryFieldProps<LokiDatasource, LokiQuery, LokiOptions> {
history: LokiHistoryItem[];
export interface LokiQueryFieldProps extends QueryEditorProps<LokiDatasource, LokiQuery, LokiOptions> {
ExtraFieldElement?: ReactNode;
placeholder?: string;
'data-testid'?: string;

View File

@ -36,7 +36,7 @@ import {
lokiStreamsToDataFrames,
processRangeQueryResponse,
} from './result_transformer';
import { addParsedLabelToQuery, getHighlighterExpressionsFromQuery, queryHasPipeParser } from './query_utils';
import { addParsedLabelToQuery, queryHasPipeParser } from './query_utils';
import {
LokiOptions,
@ -429,10 +429,6 @@ export class LokiDatasource extends DataSourceApi<LokiQuery, LokiOptions> {
return { ...query, expr: expression };
}
getHighlighterExpression(query: LokiQuery): string[] {
return getHighlighterExpressionsFromQuery(query.expr);
}
getTime(date: string | DateTime, roundUp: boolean) {
if (typeof date === 'string') {
date = dateMath.parse(date, roundUp)!;

View File

@ -1,7 +1,7 @@
import React, { memo, FC, useEffect } from 'react';
// Types
import { ExploreQueryFieldProps } from '@grafana/data';
import { QueryEditorProps } from '@grafana/data';
import { PrometheusDatasource } from '../datasource';
import { PromQuery, PromOptions } from '../types';
@ -9,7 +9,7 @@ import { PromQuery, PromOptions } from '../types';
import PromQueryField from './PromQueryField';
import { PromExploreExtraField } from './PromExploreExtraField';
export type Props = ExploreQueryFieldProps<PrometheusDatasource, PromQuery, PromOptions>;
export type Props = QueryEditorProps<PrometheusDatasource, PromQuery, PromOptions>;
export const PromExploreQueryEditor: FC<Props> = (props: Props) => {
const { range, query, data, datasource, history, onChange, onRunQuery } = props;

View File

@ -19,14 +19,7 @@ import { LanguageMap, languages as prismLanguages } from 'prismjs';
import { PromQuery, PromOptions } from '../types';
import { roundMsToMin } from '../language_utils';
import { CancelablePromise, makePromiseCancelable } from 'app/core/utils/CancelablePromise';
import {
ExploreQueryFieldProps,
QueryHint,
isDataFrame,
toLegacyResponseData,
HistoryItem,
TimeRange,
} from '@grafana/data';
import { QueryEditorProps, QueryHint, isDataFrame, toLegacyResponseData, TimeRange } from '@grafana/data';
import { PrometheusDatasource } from '../datasource';
import { PrometheusMetricsBrowser } from './PrometheusMetricsBrowser';
import { MonacoQueryFieldLazy } from './monaco-query-field/MonacoQueryFieldLazy';
@ -76,8 +69,7 @@ export function willApplySuggestion(suggestion: string, { typeaheadContext, type
return suggestion;
}
interface PromQueryFieldProps extends ExploreQueryFieldProps<PrometheusDatasource, PromQuery, PromOptions> {
history: Array<HistoryItem<PromQuery>>;
interface PromQueryFieldProps extends QueryEditorProps<PrometheusDatasource, PromQuery, PromOptions> {
ExtraFieldElement?: ReactNode;
placeholder?: string;
'data-testid'?: string;
@ -273,6 +265,7 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
query,
ExtraFieldElement,
placeholder = 'Enter a PromQL query (run with Shift+Enter)',
history = [],
} = this.props;
const { labelBrowserVisible, syntaxLoaded, hint } = this.state;
@ -302,7 +295,7 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
{isMonacoEditorEnabled ? (
<MonacoQueryFieldLazy
languageProvider={languageProvider}
history={this.props.history}
history={history}
onChange={this.onChangeQuery}
onRunQuery={this.props.onRunQuery}
initialValue={query.expr ?? ''}

View File

@ -62,6 +62,9 @@ function addMetricsMetadata(metric: string, metadata?: PromMetricsMetadata): Com
const PREFIX_DELIMITER_REGEX = /(="|!="|=~"|!~"|\{|\[|\(|\+|-|\/|\*|%|\^|\band\b|\bor\b|\bunless\b|==|>=|!=|<=|>|<|=|~|,)/;
interface AutocompleteContext {
history?: Array<HistoryItem<PromQuery>>;
}
export default class PromQlLanguageProvider extends LanguageProvider {
histogramMetrics: string[];
timeRange?: { start: number; end: number };
@ -140,7 +143,7 @@ export default class PromQlLanguageProvider extends LanguageProvider {
provideCompletionItems = async (
{ prefix, text, value, labelKey, wrapperClasses }: TypeaheadInput,
context: { history: Array<HistoryItem<PromQuery>> } = { history: [] }
context: AutocompleteContext = {}
): Promise<TypeaheadOutput> => {
const emptyResult: TypeaheadOutput = { suggestions: [] };
@ -194,13 +197,13 @@ export default class PromQlLanguageProvider extends LanguageProvider {
return emptyResult;
};
getBeginningCompletionItems = (context: { history: Array<HistoryItem<PromQuery>> }): TypeaheadOutput => {
getBeginningCompletionItems = (context: AutocompleteContext): TypeaheadOutput => {
return {
suggestions: [...this.getEmptyCompletionItems(context).suggestions, ...this.getTermCompletionItems().suggestions],
};
};
getEmptyCompletionItems = (context: { history: Array<HistoryItem<PromQuery>> }): TypeaheadOutput => {
getEmptyCompletionItems = (context: AutocompleteContext): TypeaheadOutput => {
const { history } = context;
const suggestions: CompletionItemGroup[] = [];

View File

@ -1,5 +1,5 @@
import { css } from '@emotion/css';
import { DataSourceApi, ExploreQueryFieldProps, SelectableValue } from '@grafana/data';
import { DataSourceApi, QueryEditorProps, SelectableValue } from '@grafana/data';
import { config, getDataSourceSrv } from '@grafana/runtime';
import {
FileDropzone,
@ -21,7 +21,7 @@ import { PrometheusDatasource } from '../prometheus/datasource';
import useAsync from 'react-use/lib/useAsync';
import NativeSearch from './NativeSearch';
interface Props extends ExploreQueryFieldProps<TempoDatasource, TempoQuery>, Themeable2 {}
interface Props extends QueryEditorProps<TempoDatasource, TempoQuery>, Themeable2 {}
const DEFAULT_QUERY_TYPE: TempoQueryType = 'traceId';

View File

@ -1,5 +1,5 @@
import { css } from '@emotion/css';
import { ExploreQueryFieldProps } from '@grafana/data';
import { QueryEditorProps } from '@grafana/data';
import {
ButtonCascader,
CascaderOption,
@ -21,7 +21,7 @@ import { apiPrefix } from './constants';
import { ZipkinDatasource } from './datasource';
import { ZipkinQuery, ZipkinQueryType, ZipkinSpan } from './types';
type Props = ExploreQueryFieldProps<ZipkinDatasource, ZipkinQuery>;
type Props = QueryEditorProps<ZipkinDatasource, ZipkinQuery>;
export const ZipkinQueryField = ({ query, onChange, onRunQuery, datasource }: Props) => {
const serviceOptions = useServices(datasource);

View File

@ -81,11 +81,6 @@ export interface ExploreItemState {
* Used to distinguish URL state injection versus split view state injection.
*/
initialized: boolean;
/**
* Log line substrings to be highlighted as you type in a query field.
* Currently supports only the first query row.
*/
logsHighlighterExpressions?: string[];
/**
* Log query result to be displayed in the logs result viewer.
*/
@ -122,8 +117,6 @@ export interface ExploreItemState {
*/
refreshInterval?: string;
latency: number;
/**
* If true, the view is in live tailing mode.
*/
@ -176,7 +169,6 @@ export interface QueryTransaction {
done: boolean;
error?: string | JSX.Element;
hints?: QueryHint[];
latency: number;
request: DataQueryRequest;
queries: DataQuery[];
result?: any; // Table model / Timeseries[] / Logs