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 ```ts
import React from 'react'; import React from 'react';
import { ExploreQueryFieldProps } from '@grafana/data'; import { QueryEditorProps } from '@grafana/data';
import { QueryField } from '@grafana/ui'; import { QueryField } from '@grafana/ui';
import { DataSource } from './DataSource'; import { DataSource } from './DataSource';
import { MyQuery, MyDataSourceOptions } from './types'; import { MyQuery, MyDataSourceOptions } from './types';
export type Props = ExploreQueryFieldProps<DataSource, MyQuery, MyDataSourceOptions>; export type Props = QueryEditorProps<DataSource, MyQuery, MyDataSourceOptions>;
export default (props: Props) => { export default (props: Props) => {
return <h2>My query editor</h2>; 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().should('be.visible');
e2e.components.TraceViewer.spanBar()
.its('length')
.then((oldLength) => {
e2e.pages.Explore.General.scrollBar().scrollTo('center'); e2e.pages.Explore.General.scrollBar().scrollTo('center');
// After scrolling we should load more spans // 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; return this;
} }
setExploreQueryField(ExploreQueryField: ComponentType<ExploreQueryFieldProps<DSType, TQuery, TOptions>>) { setExploreQueryField(ExploreQueryField: ComponentType<QueryEditorProps<DSType, TQuery, TOptions>>) {
this.components.ExploreQueryField = ExploreQueryField; this.components.ExploreQueryField = ExploreQueryField;
return this; return this;
} }
setExploreMetricsQueryField(ExploreQueryField: ComponentType<ExploreQueryFieldProps<DSType, TQuery, TOptions>>) { setExploreMetricsQueryField(ExploreQueryField: ComponentType<QueryEditorProps<DSType, TQuery, TOptions>>) {
this.components.ExploreMetricsQueryField = ExploreQueryField; this.components.ExploreMetricsQueryField = ExploreQueryField;
return this; return this;
} }
setExploreLogsQueryField(ExploreQueryField: ComponentType<ExploreQueryFieldProps<DSType, TQuery, TOptions>>) { setExploreLogsQueryField(ExploreQueryField: ComponentType<QueryEditorProps<DSType, TQuery, TOptions>>) {
this.components.ExploreLogsQueryField = ExploreQueryField; this.components.ExploreLogsQueryField = ExploreQueryField;
return this; return this;
} }
@ -147,9 +147,9 @@ export interface DataSourcePluginComponents<
AnnotationsQueryCtrl?: any; AnnotationsQueryCtrl?: any;
VariableQueryEditor?: any; VariableQueryEditor?: any;
QueryEditor?: ComponentType<QueryEditorProps<DSType, TQuery, TOptions>>; QueryEditor?: ComponentType<QueryEditorProps<DSType, TQuery, TOptions>>;
ExploreQueryField?: ComponentType<ExploreQueryFieldProps<DSType, TQuery, TOptions>>; ExploreQueryField?: ComponentType<QueryEditorProps<DSType, TQuery, TOptions>>;
ExploreMetricsQueryField?: ComponentType<ExploreQueryFieldProps<DSType, TQuery, TOptions>>; ExploreMetricsQueryField?: ComponentType<QueryEditorProps<DSType, TQuery, TOptions>>;
ExploreLogsQueryField?: ComponentType<ExploreQueryFieldProps<DSType, TQuery, TOptions>>; ExploreLogsQueryField?: ComponentType<QueryEditorProps<DSType, TQuery, TOptions>>;
QueryEditorHelp?: ComponentType<QueryEditorHelpProps<TQuery>>; QueryEditorHelp?: ComponentType<QueryEditorHelpProps<TQuery>>;
ConfigEditor?: ComponentType<DataSourcePluginOptionsEditorProps<TOptions, TSecureOptions>>; ConfigEditor?: ComponentType<DataSourcePluginOptionsEditorProps<TOptions, TSecureOptions>>;
MetadataInspector?: ComponentType<MetadataInspectorProps<DSType, TQuery, TOptions>>; MetadataInspector?: ComponentType<MetadataInspectorProps<DSType, TQuery, TOptions>>;
@ -295,7 +295,8 @@ abstract class DataSourceApi<
modifyQuery?(query: TQuery, action: QueryFixAction): TQuery; modifyQuery?(query: TQuery, action: QueryFixAction): TQuery;
/** /**
* Used in explore * @deprecated since version 8.2.0
* Not used anymore.
*/ */
getHighlighterExpression?(query: TQuery): string[]; getHighlighterExpression?(query: TQuery): string[];
@ -373,7 +374,7 @@ export interface QueryEditorProps<
data?: PanelData; data?: PanelData;
range?: TimeRange; range?: TimeRange;
exploreId?: any; exploreId?: any;
history?: HistoryItem[]; history?: Array<HistoryItem<TQuery>>;
queries?: DataQuery[]; queries?: DataQuery[];
app?: CoreApp; app?: CoreApp;
} }
@ -385,15 +386,14 @@ export enum ExploreMode {
Tracing = 'Tracing', Tracing = 'Tracing',
} }
export interface ExploreQueryFieldProps< /**
* @deprecated use QueryEditorProps instead
*/
export type ExploreQueryFieldProps<
DSType extends DataSourceApi<TQuery, TOptions>, DSType extends DataSourceApi<TQuery, TOptions>,
TQuery extends DataQuery = DataQuery, TQuery extends DataQuery = DataQuery,
TOptions extends DataSourceJsonData = DataSourceJsonData TOptions extends DataSourceJsonData = DataSourceJsonData
> extends QueryEditorProps<DSType, TQuery, TOptions> { > = QueryEditorProps<DSType, TQuery, TOptions>;
history: any[];
onBlur?: () => void;
exploreId?: any;
}
export interface QueryEditorHelpProps<TQuery extends DataQuery = DataQuery> { export interface QueryEditorHelpProps<TQuery extends DataQuery = DataQuery> {
datasource: DataSourceApi<TQuery>; datasource: DataSourceApi<TQuery>;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -165,11 +165,10 @@ export function buildQueryTransaction(
scanning, scanning,
id: generateKey(), // reusing for unique ID id: generateKey(), // reusing for unique ID
done: false, 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[]) => const isSegment = (segment: { [key: string]: string }, ...props: string[]) =>
props.some((prop) => segment.hasOwnProperty(prop)); 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. * A target is non-empty when it has keys (with non-empty values) other than refId, key and context.
*/ */
const validKeys = ['refId', 'key', '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 ( return (
queries && queries &&
queries.some((query: any) => { 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 * 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>>, history: Array<HistoryItem<T>>,
datasourceId: string, datasourceId: string,
queries: T[] 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 { AbsoluteTimeRange, DataQuery, LoadingState, RawTimeRange, DataFrame, GrafanaTheme2 } from '@grafana/data';
import LogsContainer from './LogsContainer'; import LogsContainer from './LogsContainer';
import QueryRows from './QueryRows'; import { QueryRows } from './QueryRows';
import TableContainer from './TableContainer'; import TableContainer from './TableContainer';
import RichHistoryContainer from './RichHistory/RichHistoryContainer'; import RichHistoryContainer from './RichHistory/RichHistoryContainer';
import ExploreQueryInspector from './ExploreQueryInspector'; import ExploreQueryInspector from './ExploreQueryInspector';
@ -268,7 +268,6 @@ export class Explore extends React.PureComponent<Props, ExploreState> {
datasourceInstance, datasourceInstance,
datasourceMissing, datasourceMissing,
exploreId, exploreId,
queryKeys,
graphResult, graphResult,
queryResponse, queryResponse,
isLive, isLive,
@ -292,7 +291,7 @@ export class Explore extends React.PureComponent<Props, ExploreState> {
{datasourceInstance && ( {datasourceInstance && (
<div className="explore-container"> <div className="explore-container">
<div className={cx('panel-container', styles.queryContainer)}> <div className={cx('panel-container', styles.queryContainer)}>
<QueryRows exploreId={exploreId} queryKeys={queryKeys} /> <QueryRows exploreId={exploreId} />
<SecondaryActions <SecondaryActions
addQueryRowButtonDisabled={isLive} addQueryRowButtonDisabled={isLive}
// We cannot show multiple traces at the same time right now so we do not show add query button. // 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[]; logsQueries?: DataQuery[];
visibleRange?: AbsoluteTimeRange; visibleRange?: AbsoluteTimeRange;
theme: GrafanaTheme2; theme: GrafanaTheme2;
highlighterExpressions?: string[];
loading: boolean; loading: boolean;
loadingState: LoadingState; loadingState: LoadingState;
absoluteRange: AbsoluteTimeRange; absoluteRange: AbsoluteTimeRange;
@ -254,7 +253,6 @@ export class UnthemedLogs extends PureComponent<Props, State> {
logsMeta, logsMeta,
logsSeries, logsSeries,
visibleRange, visibleRange,
highlighterExpressions,
loading = false, loading = false,
loadingState, loadingState,
onClickFilterLabel, onClickFilterLabel,
@ -368,7 +366,6 @@ export class UnthemedLogs extends PureComponent<Props, State> {
deduplicatedRows={dedupedRows} deduplicatedRows={dedupedRows}
dedupStrategy={dedupStrategy} dedupStrategy={dedupStrategy}
getRowContext={this.props.getRowContext} getRowContext={this.props.getRowContext}
highlighterExpressions={highlighterExpressions}
onClickFilterLabel={onClickFilterLabel} onClickFilterLabel={onClickFilterLabel}
onClickFilterOutLabel={onClickFilterOutLabel} onClickFilterOutLabel={onClickFilterOutLabel}
showContextToggle={showContextToggle} showContextToggle={showContextToggle}

View File

@ -62,7 +62,6 @@ export class LogsContainer extends PureComponent<LogsContainerProps> {
const { const {
loading, loading,
loadingState, loadingState,
logsHighlighterExpressions,
logRows, logRows,
logsMeta, logsMeta,
logsSeries, logsSeries,
@ -123,7 +122,6 @@ export class LogsContainer extends PureComponent<LogsContainerProps> {
logsSeries={logsSeries} logsSeries={logsSeries}
logsQueries={logsQueries} logsQueries={logsQueries}
width={width} width={width}
highlighterExpressions={logsHighlighterExpressions}
loading={loading} loading={loading}
loadingState={loadingState} loadingState={loadingState}
onChangeTime={this.onChangeTime} onChangeTime={this.onChangeTime}
@ -153,22 +151,11 @@ function mapStateToProps(state: StoreState, { exploreId }: { exploreId: string }
const explore = state.explore; const explore = state.explore;
// @ts-ignore // @ts-ignore
const item: ExploreItemState = explore[exploreId]; const item: ExploreItemState = explore[exploreId];
const { const { logsResult, loading, scanning, datasourceInstance, isLive, isPaused, range, absoluteRange } = item;
logsHighlighterExpressions,
logsResult,
loading,
scanning,
datasourceInstance,
isLive,
isPaused,
range,
absoluteRange,
} = item;
const timeZone = getTimeZone(state.user); const timeZone = getTimeZone(state.user);
return { return {
loading, loading,
logsHighlighterExpressions,
logRows: logsResult?.rows, logRows: logsResult?.rows,
logsMeta: logsResult?.meta, logsMeta: logsResult?.meta,
logsSeries: logsResult?.series, 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, { useCallback, useMemo } from 'react';
import React, { PureComponent } from 'react';
// Components
import QueryRow from './QueryRow';
// Types
import { ExploreId } from 'app/types/explore'; 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 { interface Props {
className?: string;
exploreId: ExploreId; exploreId: ExploreId;
queryKeys: string[];
} }
export default class QueryRows extends PureComponent<QueryRowsProps> { const makeSelectors = (exploreId: ExploreId) => {
render() { const exploreItemSelector = getExploreItemSelector(exploreId);
const { className = '', exploreId, queryKeys } = this.props; return {
return ( getQueries: createSelector(exploreItemSelector, (s) => s!.queries),
<div className={className}> getQueryResponse: createSelector(exploreItemSelector, (s) => s!.queryResponse),
{queryKeys.map((key, index) => { getHistory: createSelector(exploreItemSelector, (s) => s!.history),
return <QueryRow key={key} exploreId={exploreId} index={index} />; getEventBridge: createSelector(exploreItemSelector, (s) => s!.eventBridge),
})} getDatasourceInstanceSettings: createSelector(
</div> 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'; } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors'; import { selectors } from '@grafana/e2e-selectors';
import { setTimeSrv } from '../dashboard/services/TimeSrv';
import { from, Observable } from 'rxjs'; import { from, Observable } from 'rxjs';
import { LokiDatasource } from '../../plugins/datasource/loki/datasource'; import { LokiDatasource } from '../../plugins/datasource/loki/datasource';
import { LokiQuery } from '../../plugins/datasource/loki/types'; import { LokiQuery } from '../../plugins/datasource/loki/types';
@ -63,13 +62,13 @@ describe('Wrapper', () => {
// At this point url should be initialised to some defaults // At this point url should be initialised to some defaults
expect(locationService.getSearchObject()).toEqual({ expect(locationService.getSearchObject()).toEqual({
orgId: '1', orgId: '1',
left: JSON.stringify(['now-1h', 'now', 'loki', {}]), left: JSON.stringify(['now-1h', 'now', 'loki', { refId: 'A' }]),
}); });
expect(datasources.loki.query).not.toBeCalled(); expect(datasources.loki.query).not.toBeCalled();
}); });
it('runs query when url contains query and renders results', async () => { 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 }); const { datasources, store } = setup({ query });
(datasources.loki.query as Mock).mockReturnValueOnce(makeLogsQueryResponse()); (datasources.loki.query as Mock).mockReturnValueOnce(makeLogsQueryResponse());
@ -91,7 +90,7 @@ describe('Wrapper', () => {
expect(store.getState().explore.richHistory[0]).toMatchObject({ expect(store.getState().explore.richHistory[0]).toMatchObject({
datasourceId: '1', datasourceId: '1',
datasourceName: 'loki', datasourceName: 'loki',
queries: [{ expr: '{ label="value"}' }], queries: [{ expr: '{ label="value"}', refId: 'A' }],
}); });
// We called the data source query method once // We called the data source query method once
@ -141,7 +140,7 @@ describe('Wrapper', () => {
}); });
it('handles changing the datasource manually', async () => { 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 }); const { datasources } = setup({ query });
(datasources.loki.query as Mock).mockReturnValueOnce(makeLogsQueryResponse()); (datasources.loki.query as Mock).mockReturnValueOnce(makeLogsQueryResponse());
// Wait for rendering the editor // Wait for rendering the editor
@ -152,7 +151,7 @@ describe('Wrapper', () => {
expect(datasources.elastic.query).not.toBeCalled(); expect(datasources.elastic.query).not.toBeCalled();
expect(locationService.getSearchObject()).toEqual({ expect(locationService.getSearchObject()).toEqual({
orgId: '1', 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 () => { it('inits with two panes if specified in url', async () => {
const query = { const query = {
left: JSON.stringify(['now-1h', 'now', 'loki', { expr: '{ label="value"}' }]), left: JSON.stringify(['now-1h', 'now', 'loki', { expr: '{ label="value"}', refId: 'A' }]),
right: JSON.stringify(['now-1h', 'now', 'elastic', { expr: 'error' }]), right: JSON.stringify(['now-1h', 'now', 'elastic', { expr: 'error', refId: 'A' }]),
}; };
const { datasources } = setup({ query }); const { datasources } = setup({ query });
@ -211,8 +210,8 @@ describe('Wrapper', () => {
it('can close a pane from a split', async () => { it('can close a pane from a split', async () => {
const query = { const query = {
left: JSON.stringify(['now-1h', 'now', 'loki', {}]), left: JSON.stringify(['now-1h', 'now', 'loki', { refId: 'A' }]),
right: JSON.stringify(['now-1h', 'now', 'elastic', {}]), right: JSON.stringify(['now-1h', 'now', 'elastic', { refId: 'A' }]),
}; };
setup({ query }); setup({ query });
const closeButtons = await screen.findAllByTitle(/Close split pane/i); const closeButtons = await screen.findAllByTitle(/Close split pane/i);
@ -325,12 +324,6 @@ function setup(options?: SetupOptions): { datasources: { [name: string]: DataSou
}, },
} as any); } as any);
setTimeSrv({
init() {},
getValidIntervals(intervals: string[]): string[] {
return intervals;
},
} as any);
setEchoSrv(new Echo()); setEchoSrv(new Echo());
const store = configureStore(); const store = configureStore();

View File

@ -16,7 +16,6 @@ exports[`Explore should render component 1`] = `
> >
<QueryRows <QueryRows
exploreId="left" exploreId="left"
queryKeys={Array []}
/> />
<SecondaryActions <SecondaryActions
addQueryRowButtonDisabled={false} 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, graphResult: null,
logsResult: null, logsResult: null,
tableResult: null, tableResult: null,
latency: 0,
loading: false, loading: false,
queryResponse: { queryResponse: {
// When creating an empty query response we also create a timeRange object with the current time. // 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, graphResult: null,
tableResult: null, tableResult: null,
logsResult: null, logsResult: null,
latency: 0,
queryResponse: createEmptyQueryResponse(), queryResponse: createEmptyQueryResponse(),
loading: false, loading: false,
queryKeys: [], queryKeys: [],
history, history,
datasourceMissing: false, datasourceMissing: false,
logsHighlighterExpressions: undefined,
}; };
} }

View File

@ -119,7 +119,7 @@ describe('refreshExplore', () => {
await dispatch( await dispatch(
refreshExplore( refreshExplore(
ExploreId.left, ExploreId.left,
serializeStateToUrlParam({ datasource: 'someDs', queries: [{ expr: 'count()' }], range: testRange }) serializeStateToUrlParam({ datasource: 'someDs', queries: [{ expr: 'count()', refId: 'A' }], range: testRange })
) )
); );
// same // same
@ -138,7 +138,7 @@ describe('refreshExplore', () => {
await dispatch( await dispatch(
refreshExplore( refreshExplore(
ExploreId.left, 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'); 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. * 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. * 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 }; 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)) { if (initializeExploreAction.match(action)) {
const { containerWidth, eventBridge, queries, range, originPanelId, datasourceInstance, history } = action.payload; const { containerWidth, eventBridge, queries, range, originPanelId, datasourceInstance, history } = action.payload;
@ -237,7 +215,6 @@ export const paneReducer = (state: ExploreItemState = makeExplorePaneState(), ac
history, history,
datasourceMissing: !datasourceInstance, datasourceMissing: !datasourceInstance,
queryResponse: createEmptyQueryResponse(), queryResponse: createEmptyQueryResponse(),
logsHighlighterExpressions: undefined,
cache: [], cache: [],
}; };
} }

View File

@ -6,7 +6,6 @@ import {
clearCache, clearCache,
importQueries, importQueries,
queryReducer, queryReducer,
removeQueryRowAction,
runQueries, runQueries,
scanStartAction, scanStartAction,
scanStopAction, scanStopAction,
@ -34,7 +33,6 @@ import { configureStore } from '../../../store/configureStore';
import { setTimeSrv } from '../../dashboard/services/TimeSrv'; import { setTimeSrv } from '../../dashboard/services/TimeSrv';
import Mock = jest.Mock; import Mock = jest.Mock;
const QUERY_KEY_REGEX = /Q-(?:[a-z0-9]+-){5}(?:[0-9]+)/;
const t = toUtc(); const t = toUtc();
const testRange = { const testRange = {
from: t, from: t,
@ -213,55 +211,6 @@ describe('reducer', () => {
queryKeys: ['mockKey-0'], queryKeys: ['mockKey-0'],
} as unknown) as ExploreItemState); } 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', () => { describe('caching', () => {

View File

@ -50,26 +50,15 @@ export interface AddQueryRowPayload {
} }
export const addQueryRowAction = createAction<AddQueryRowPayload>('explore/addQueryRow'); 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. * 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. * 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; exploreId: ExploreId;
query: DataQuery; queries: DataQuery[];
index: number;
override: boolean;
} }
export const changeQueryAction = createAction<ChangeQueryPayload>('explore/changeQuery'); export const changeQueriesAction = createAction<ChangeQueriesPayload>('explore/changeQueries');
/** /**
* Clear all queries and results. * 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. * Clear all queries and results.
*/ */
@ -352,7 +316,6 @@ export const runQueries = (
// If we don't have results saved in cache, run new queries // If we don't have results saved in cache, run new queries
} else { } else {
if (!hasNonEmptyQuery(queries)) { if (!hasNonEmptyQuery(queries)) {
dispatch(clearQueriesAction({ exploreId }));
dispatch(stateSave({ replace: options?.replaceUrl })); // Remember to save to state and update location dispatch(stateSave({ replace: options?.replaceUrl })); // Remember to save to state and update location
return; return;
} }
@ -515,23 +478,16 @@ export const queryReducer = (state: ExploreItemState, action: AnyAction): Explor
return { return {
...state, ...state,
queries: nextQueries, queries: nextQueries,
logsHighlighterExpressions: undefined,
queryKeys: getQueryKeys(nextQueries, state.datasourceInstance), queryKeys: getQueryKeys(nextQueries, state.datasourceInstance),
}; };
} }
if (changeQueryAction.match(action)) { if (changeQueriesAction.match(action)) {
const { queries } = state; const { queries } = action.payload;
const { query, index } = action.payload;
// Override path: queries are completely reset
const nextQuery: DataQuery = generateNewKeyAndAddRefIdIfMissing(query, queries, index);
const nextQueries = [...queries];
nextQueries[index] = nextQuery;
return { return {
...state, ...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)) { if (setQueriesAction.match(action)) {
const { queries } = action.payload; const { queries } = action.payload;
return { return {
@ -752,8 +681,6 @@ export const processQueryResponse = (
return { ...state }; return { ...state };
} }
const latency = request.endTime ? request.endTime - request.startTime : 0;
// Send legacy data to Angular editors // Send legacy data to Angular editors
if (state.datasourceInstance?.components?.QueryCtrl) { if (state.datasourceInstance?.components?.QueryCtrl) {
const legacy = series.map((v) => toLegacyResponseData(v)); const legacy = series.map((v) => toLegacyResponseData(v));
@ -762,7 +689,6 @@ export const processQueryResponse = (
return { return {
...state, ...state,
latency,
queryResponse: response, queryResponse: response,
graphResult, graphResult,
tableResult, tableResult,

View File

@ -1,3 +1,5 @@
import { ExploreId, StoreState } from 'app/types'; import { ExploreId, StoreState } from 'app/types';
export const isSplit = (state: StoreState) => Boolean(state.explore[ExploreId.left] && state.explore[ExploreId.right]); 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, scanning: false,
loading: false, loading: false,
queryKeys: [], queryKeys: [],
latency: 0,
isLive: false, isLive: false,
isPaused: false, isPaused: false,
queryResponse: createEmptyQueryResponse(), queryResponse: createEmptyQueryResponse(),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
import React, { memo, FC, useEffect } from 'react'; import React, { memo, FC, useEffect } from 'react';
// Types // Types
import { ExploreQueryFieldProps } from '@grafana/data'; import { QueryEditorProps } from '@grafana/data';
import { PrometheusDatasource } from '../datasource'; import { PrometheusDatasource } from '../datasource';
import { PromQuery, PromOptions } from '../types'; import { PromQuery, PromOptions } from '../types';
@ -9,7 +9,7 @@ import { PromQuery, PromOptions } from '../types';
import PromQueryField from './PromQueryField'; import PromQueryField from './PromQueryField';
import { PromExploreExtraField } from './PromExploreExtraField'; 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) => { export const PromExploreQueryEditor: FC<Props> = (props: Props) => {
const { range, query, data, datasource, history, onChange, onRunQuery } = 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 { PromQuery, PromOptions } from '../types';
import { roundMsToMin } from '../language_utils'; import { roundMsToMin } from '../language_utils';
import { CancelablePromise, makePromiseCancelable } from 'app/core/utils/CancelablePromise'; import { CancelablePromise, makePromiseCancelable } from 'app/core/utils/CancelablePromise';
import { import { QueryEditorProps, QueryHint, isDataFrame, toLegacyResponseData, TimeRange } from '@grafana/data';
ExploreQueryFieldProps,
QueryHint,
isDataFrame,
toLegacyResponseData,
HistoryItem,
TimeRange,
} from '@grafana/data';
import { PrometheusDatasource } from '../datasource'; import { PrometheusDatasource } from '../datasource';
import { PrometheusMetricsBrowser } from './PrometheusMetricsBrowser'; import { PrometheusMetricsBrowser } from './PrometheusMetricsBrowser';
import { MonacoQueryFieldLazy } from './monaco-query-field/MonacoQueryFieldLazy'; import { MonacoQueryFieldLazy } from './monaco-query-field/MonacoQueryFieldLazy';
@ -76,8 +69,7 @@ export function willApplySuggestion(suggestion: string, { typeaheadContext, type
return suggestion; return suggestion;
} }
interface PromQueryFieldProps extends ExploreQueryFieldProps<PrometheusDatasource, PromQuery, PromOptions> { interface PromQueryFieldProps extends QueryEditorProps<PrometheusDatasource, PromQuery, PromOptions> {
history: Array<HistoryItem<PromQuery>>;
ExtraFieldElement?: ReactNode; ExtraFieldElement?: ReactNode;
placeholder?: string; placeholder?: string;
'data-testid'?: string; 'data-testid'?: string;
@ -273,6 +265,7 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
query, query,
ExtraFieldElement, ExtraFieldElement,
placeholder = 'Enter a PromQL query (run with Shift+Enter)', placeholder = 'Enter a PromQL query (run with Shift+Enter)',
history = [],
} = this.props; } = this.props;
const { labelBrowserVisible, syntaxLoaded, hint } = this.state; const { labelBrowserVisible, syntaxLoaded, hint } = this.state;
@ -302,7 +295,7 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
{isMonacoEditorEnabled ? ( {isMonacoEditorEnabled ? (
<MonacoQueryFieldLazy <MonacoQueryFieldLazy
languageProvider={languageProvider} languageProvider={languageProvider}
history={this.props.history} history={history}
onChange={this.onChangeQuery} onChange={this.onChangeQuery}
onRunQuery={this.props.onRunQuery} onRunQuery={this.props.onRunQuery}
initialValue={query.expr ?? ''} 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|==|>=|!=|<=|>|<|=|~|,)/; const PREFIX_DELIMITER_REGEX = /(="|!="|=~"|!~"|\{|\[|\(|\+|-|\/|\*|%|\^|\band\b|\bor\b|\bunless\b|==|>=|!=|<=|>|<|=|~|,)/;
interface AutocompleteContext {
history?: Array<HistoryItem<PromQuery>>;
}
export default class PromQlLanguageProvider extends LanguageProvider { export default class PromQlLanguageProvider extends LanguageProvider {
histogramMetrics: string[]; histogramMetrics: string[];
timeRange?: { start: number; end: number }; timeRange?: { start: number; end: number };
@ -140,7 +143,7 @@ export default class PromQlLanguageProvider extends LanguageProvider {
provideCompletionItems = async ( provideCompletionItems = async (
{ prefix, text, value, labelKey, wrapperClasses }: TypeaheadInput, { prefix, text, value, labelKey, wrapperClasses }: TypeaheadInput,
context: { history: Array<HistoryItem<PromQuery>> } = { history: [] } context: AutocompleteContext = {}
): Promise<TypeaheadOutput> => { ): Promise<TypeaheadOutput> => {
const emptyResult: TypeaheadOutput = { suggestions: [] }; const emptyResult: TypeaheadOutput = { suggestions: [] };
@ -194,13 +197,13 @@ export default class PromQlLanguageProvider extends LanguageProvider {
return emptyResult; return emptyResult;
}; };
getBeginningCompletionItems = (context: { history: Array<HistoryItem<PromQuery>> }): TypeaheadOutput => { getBeginningCompletionItems = (context: AutocompleteContext): TypeaheadOutput => {
return { return {
suggestions: [...this.getEmptyCompletionItems(context).suggestions, ...this.getTermCompletionItems().suggestions], suggestions: [...this.getEmptyCompletionItems(context).suggestions, ...this.getTermCompletionItems().suggestions],
}; };
}; };
getEmptyCompletionItems = (context: { history: Array<HistoryItem<PromQuery>> }): TypeaheadOutput => { getEmptyCompletionItems = (context: AutocompleteContext): TypeaheadOutput => {
const { history } = context; const { history } = context;
const suggestions: CompletionItemGroup[] = []; const suggestions: CompletionItemGroup[] = [];

View File

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

View File

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

View File

@ -81,11 +81,6 @@ export interface ExploreItemState {
* Used to distinguish URL state injection versus split view state injection. * Used to distinguish URL state injection versus split view state injection.
*/ */
initialized: boolean; 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. * Log query result to be displayed in the logs result viewer.
*/ */
@ -122,8 +117,6 @@ export interface ExploreItemState {
*/ */
refreshInterval?: string; refreshInterval?: string;
latency: number;
/** /**
* If true, the view is in live tailing mode. * If true, the view is in live tailing mode.
*/ */
@ -176,7 +169,6 @@ export interface QueryTransaction {
done: boolean; done: boolean;
error?: string | JSX.Element; error?: string | JSX.Element;
hints?: QueryHint[]; hints?: QueryHint[];
latency: number;
request: DataQueryRequest; request: DataQueryRequest;
queries: DataQuery[]; queries: DataQuery[];
result?: any; // Table model / Timeseries[] / Logs result?: any; // Table model / Timeseries[] / Logs