diff --git a/docs/sources/developers/plugins/add-support-for-explore-queries.md b/docs/sources/developers/plugins/add-support-for-explore-queries.md index 525a833c876..0a842a84960 100644 --- a/docs/sources/developers/plugins/add-support-for-explore-queries.md +++ b/docs/sources/developers/plugins/add-support-for-explore-queries.md @@ -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; + export type Props = QueryEditorProps; export default (props: Props) => { return

My query editor

; diff --git a/e2e/suite1/specs/trace-view-scrolling.spec.ts b/e2e/suite1/specs/trace-view-scrolling.spec.ts index 2fca9a7e7c2..c55444c2bfe 100644 --- a/e2e/suite1/specs/trace-view-scrolling.spec.ts +++ b/e2e/suite1/specs/trace-view-scrolling.spec.ts @@ -29,9 +29,13 @@ describe('Trace view', () => { e2e.components.TraceViewer.spanBar().should('be.visible'); - e2e.pages.Explore.General.scrollBar().scrollTo('center'); + 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); + // After scrolling we should load more spans + e2e.components.TraceViewer.spanBar().its('length').should('be.gt', oldLength); + }); }); }); diff --git a/packages/grafana-data/src/types/datasource.ts b/packages/grafana-data/src/types/datasource.ts index 9f3cef1700e..d1fea53c2a4 100644 --- a/packages/grafana-data/src/types/datasource.ts +++ b/packages/grafana-data/src/types/datasource.ts @@ -62,17 +62,17 @@ export class DataSourcePlugin< return this; } - setExploreQueryField(ExploreQueryField: ComponentType>) { + setExploreQueryField(ExploreQueryField: ComponentType>) { this.components.ExploreQueryField = ExploreQueryField; return this; } - setExploreMetricsQueryField(ExploreQueryField: ComponentType>) { + setExploreMetricsQueryField(ExploreQueryField: ComponentType>) { this.components.ExploreMetricsQueryField = ExploreQueryField; return this; } - setExploreLogsQueryField(ExploreQueryField: ComponentType>) { + setExploreLogsQueryField(ExploreQueryField: ComponentType>) { this.components.ExploreLogsQueryField = ExploreQueryField; return this; } @@ -147,9 +147,9 @@ export interface DataSourcePluginComponents< AnnotationsQueryCtrl?: any; VariableQueryEditor?: any; QueryEditor?: ComponentType>; - ExploreQueryField?: ComponentType>; - ExploreMetricsQueryField?: ComponentType>; - ExploreLogsQueryField?: ComponentType>; + ExploreQueryField?: ComponentType>; + ExploreMetricsQueryField?: ComponentType>; + ExploreLogsQueryField?: ComponentType>; QueryEditorHelp?: ComponentType>; ConfigEditor?: ComponentType>; MetadataInspector?: ComponentType>; @@ -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>; 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 extends DataQuery = DataQuery, TOptions extends DataSourceJsonData = DataSourceJsonData -> extends QueryEditorProps { - history: any[]; - onBlur?: () => void; - exploreId?: any; -} +> = QueryEditorProps; export interface QueryEditorHelpProps { datasource: DataSourceApi; diff --git a/packages/grafana-data/src/types/explore.ts b/packages/grafana-data/src/types/explore.ts index d6bd1e899d8..335c65e60e2 100644 --- a/packages/grafana-data/src/types/explore.ts +++ b/packages/grafana-data/src/types/explore.ts @@ -1,10 +1,12 @@ import { DataQuery } from './query'; import { RawTimeRange, TimeRange } from './time'; +type AnyQuery = DataQuery & Record; + /** @internal */ -export interface ExploreUrlState { +export interface ExploreUrlState { 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; diff --git a/packages/grafana-ui/src/components/Logs/LogRow.tsx b/packages/grafana-ui/src/components/Logs/LogRow.tsx index 6b4e72d7c33..e68d932369d 100644 --- a/packages/grafana-ui/src/components/Logs/LogRow.tsx +++ b/packages/grafana-ui/src/components/Logs/LogRow.tsx @@ -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 { onClickFilterOutLabel, onClickShowDetectedField, onClickHideDetectedField, - highlighterExpressions, enableLogDetails, row, showDuplicates, @@ -192,7 +190,6 @@ class UnThemedLogRow extends PureComponent { /> ) : ( boolean; - highlighterExpressions?: string[]; getRows: () => LogRowModel[]; onToggleContext: () => void; updateLimit?: () => void; @@ -100,7 +98,6 @@ class UnThemedLogRowMessage extends PureComponent { render() { const { - highlighterExpressions, row, theme, errors, @@ -118,11 +115,7 @@ class UnThemedLogRowMessage extends PureComponent { 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 { /> )} - {renderLogMessage(hasAnsi, restructuredEntry, highlights, highlightClassName)} + {renderLogMessage(hasAnsi, restructuredEntry, row.searchWords, highlightClassName)} {showContextToggle?.(row) && ( { { { logRows={rows} deduplicatedRows={dedupedRows} dedupStrategy={LogsDedupStrategy.none} - highlighterExpressions={[]} showLabels={false} showTime={false} wrapLogMessage={true} @@ -89,7 +86,6 @@ describe('LogRows', () => { { { { prettifyLogMessage, logRows, deduplicatedRows, - highlighterExpressions, timeZone, onClickFilterLabel, onClickFilterOutLabel, @@ -129,7 +127,6 @@ class UnThemedLogRows extends PureComponent { key={row.uid} getRows={getRows} getRowContext={getRowContext} - highlighterExpressions={highlighterExpressions} row={row} showContextToggle={showContextToggle} showDuplicates={showDuplicates} diff --git a/public/app/core/utils/explore.test.ts b/public/app/core/utils/explore.test.ts index e0146dc2d97..d14385c62f2 100644 --- a/public/app/core/utils/explore.test.ts +++ b/public/app/core/utils/explore.test.ts @@ -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: { diff --git a/public/app/core/utils/explore.ts b/public/app/core/utils/explore.ts index e792ef1cef8..c418bb7ad1a 100644 --- a/public/app/core/utils/explore.ts +++ b/public/app/core/utils/explore.ts @@ -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(queries: TQuery[]): boolean { +export function hasNonEmptyQuery(queries: TQuery[]): boolean { return ( queries && queries.some((query: any) => { @@ -302,7 +301,7 @@ export function hasNonEmptyQuery(queries: TQuery /** * Update the query history. Side-effect: store history in local storage */ -export function updateHistory( +export function updateHistory( history: Array>, datasourceId: string, queries: T[] diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index 9abc1155506..380f3be4f92 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -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 { datasourceInstance, datasourceMissing, exploreId, - queryKeys, graphResult, queryResponse, isLive, @@ -292,7 +291,7 @@ export class Explore extends React.PureComponent { {datasourceInstance && (
- + { logsMeta, logsSeries, visibleRange, - highlighterExpressions, loading = false, loadingState, onClickFilterLabel, @@ -368,7 +366,6 @@ export class UnthemedLogs extends PureComponent { deduplicatedRows={dedupedRows} dedupStrategy={dedupStrategy} getRowContext={this.props.getRowContext} - highlighterExpressions={highlighterExpressions} onClickFilterLabel={onClickFilterLabel} onClickFilterOutLabel={onClickFilterOutLabel} showContextToggle={showContextToggle} diff --git a/public/app/features/explore/LogsContainer.tsx b/public/app/features/explore/LogsContainer.tsx index 4c4f0864f71..2a9b35afd31 100644 --- a/public/app/features/explore/LogsContainer.tsx +++ b/public/app/features/explore/LogsContainer.tsx @@ -62,7 +62,6 @@ export class LogsContainer extends PureComponent { const { loading, loadingState, - logsHighlighterExpressions, logRows, logsMeta, logsSeries, @@ -123,7 +122,6 @@ export class LogsContainer extends PureComponent { 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, diff --git a/public/app/features/explore/QueryEditor.tsx b/public/app/features/explore/QueryEditor.tsx deleted file mode 100644 index 46c1a870c51..00000000000 --- a/public/app/features/explore/QueryEditor.tsx +++ /dev/null @@ -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 { - element: any; - component?: AngularComponent; - angularScope: any; - - async componentDidMount() { - if (!this.element) { - return; - } - - const { datasource, initialQuery, exploreEvents, range } = this.props; - - const loader = getAngularLoader(); - const template = ' '; - 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
(this.element = element)} style={{ width: '100%' }} />; - } -} diff --git a/public/app/features/explore/QueryRow.test.tsx b/public/app/features/explore/QueryRow.test.tsx deleted file mode 100644 index b75e482dd59..00000000000 --- a/public/app/features/explore/QueryRow.test.tsx +++ /dev/null @@ -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 = { - 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(); - return wrapper; -}; - -const QueryEditor = () =>
; - -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); - }); - }); -}); diff --git a/public/app/features/explore/QueryRow.tsx b/public/app/features/explore/QueryRow.tsx deleted file mode 100644 index 2d0ecee06d1..00000000000 --- a/public/app/features/explore/QueryRow.tsx +++ /dev/null @@ -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; - -interface QueryRowState { - textEditModeEnabled: boolean; -} - -// Empty function to override blur execution on query field -const noopOnBlur = () => {}; - -export class QueryRow extends PureComponent { - 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 = ( - - ); - } else { - QueryEditor = ( - - ); - } - - const DatasourceCheatsheet = datasourceInstance.components?.QueryEditorHelp; - return ( - <> - {QueryEditor} - {DatasourceCheatsheet && ( - - this.onChange(query)} datasource={datasourceInstance!} /> - - )} - - ); - }; - - 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 ( - <> -
-
{this.renderQueryEditor(datasourceInstance)}
- -
- {queryErrors.length > 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); diff --git a/public/app/features/explore/QueryRowActions.test.tsx b/public/app/features/explore/QueryRowActions.test.tsx deleted file mode 100644 index 59321c40a90..00000000000 --- a/public/app/features/explore/QueryRowActions.test.tsx +++ /dev/null @@ -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(); - 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); - }); -}); diff --git a/public/app/features/explore/QueryRowActions.tsx b/public/app/features/explore/QueryRowActions.tsx deleted file mode 100644 index 26f57970407..00000000000 --- a/public/app/features/explore/QueryRowActions.tsx +++ /dev/null @@ -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 ( -
- {canToggleEditorModes && ( -
- -
- )} -
- -
-
- -
-
- -
-
- ); -} diff --git a/public/app/features/explore/QueryRows.test.tsx b/public/app/features/explore/QueryRows.test.tsx new file mode 100644 index 00000000000..7242c2ef907 --- /dev/null +++ b/public/app/features/explore/QueryRows.test.tsx @@ -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 = { + 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( + + + + ); + + // 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(); + }); +}); diff --git a/public/app/features/explore/QueryRows.tsx b/public/app/features/explore/QueryRows.tsx index 1957263119a..3951641493b 100644 --- a/public/app/features/explore/QueryRows.tsx +++ b/public/app/features/explore/QueryRows.tsx @@ -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 { - render() { - const { className = '', exploreId, queryKeys } = this.props; - return ( -
- {queryKeys.map((key, index) => { - return ; - })} -
- ); - } -} +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 ( + + ); +}; diff --git a/public/app/features/explore/QueryStatus.test.tsx b/public/app/features/explore/QueryStatus.test.tsx deleted file mode 100644 index ecfc962e0f0..00000000000 --- a/public/app/features/explore/QueryStatus.test.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from 'react'; -import { shallow } from 'enzyme'; - -import { LoadingState, TimeRange, PanelData } from '@grafana/data'; - -import QueryStatus from './QueryStatus'; - -describe('', () => { - it('should render with a latency', () => { - const res: PanelData = { series: [], state: LoadingState.Done, timeRange: {} as TimeRange }; - const wrapper = shallow(); - 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(); - expect(wrapper.getElement()).toBe(null); - }); -}); diff --git a/public/app/features/explore/QueryStatus.tsx b/public/app/features/explore/QueryStatus.tsx deleted file mode 100644 index 4ff6e845d1a..00000000000 --- a/public/app/features/explore/QueryStatus.tsx +++ /dev/null @@ -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 { - render() { - const { queryResponse, latency } = this.props; - const className = - queryResponse.state === LoadingState.Done || LoadingState.Error - ? 'query-transaction' - : 'query-transaction query-transaction--loading'; - return ( -
- {/*
{transaction.resultType}:
*/} -
- {queryResponse.state === LoadingState.Done || LoadingState.Error ? formatLatency(latency) : } -
-
- ); - } -} - -interface QueryStatusProps { - queryResponse: PanelData; - latency: number; -} - -export default class QueryStatus extends PureComponent { - render() { - const { queryResponse, latency } = this.props; - - if (queryResponse.state === LoadingState.NotStarted) { - return null; - } - - return ( -
- -
- ); - } -} diff --git a/public/app/features/explore/Wrapper.test.tsx b/public/app/features/explore/Wrapper.test.tsx index c6044d28069..0ffbda113ee 100644 --- a/public/app/features/explore/Wrapper.test.tsx +++ b/public/app/features/explore/Wrapper.test.tsx @@ -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(); diff --git a/public/app/features/explore/__snapshots__/Explore.test.tsx.snap b/public/app/features/explore/__snapshots__/Explore.test.tsx.snap index 1cb9668a69e..193e62eaaa0 100644 --- a/public/app/features/explore/__snapshots__/Explore.test.tsx.snap +++ b/public/app/features/explore/__snapshots__/Explore.test.tsx.snap @@ -16,7 +16,6 @@ exports[`Explore should render component 1`] = ` > -
- -
-
- -
-
- -
-
- -
-
-`; diff --git a/public/app/features/explore/state/datasource.test.ts b/public/app/features/explore/state/datasource.test.ts index 504a85bef13..36c940859db 100644 --- a/public/app/features/explore/state/datasource.test.ts +++ b/public/app/features/explore/state/datasource.test.ts @@ -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. diff --git a/public/app/features/explore/state/datasource.ts b/public/app/features/explore/state/datasource.ts index 342e61fe47d..54755dd5208 100644 --- a/public/app/features/explore/state/datasource.ts +++ b/public/app/features/explore/state/datasource.ts @@ -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, }; } diff --git a/public/app/features/explore/state/explorePane.test.ts b/public/app/features/explore/state/explorePane.test.ts index 84f916bc000..b65d2bc8020 100644 --- a/public/app/features/explore/state/explorePane.test.ts +++ b/public/app/features/explore/state/explorePane.test.ts @@ -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 }) ) ); diff --git a/public/app/features/explore/state/explorePane.ts b/public/app/features/explore/state/explorePane.ts index 03c67cb605b..6512272b4df 100644 --- a/public/app/features/explore/state/explorePane.ts +++ b/public/app/features/explore/state/explorePane.ts @@ -44,17 +44,6 @@ export interface ChangeSizePayload { } export const changeSizeAction = createAction('explore/changeSize'); -/** - * Highlight expressions in the log results - */ -export interface HighlightLogsExpressionPayload { - exploreId: ExploreId; - expressions: string[]; -} -export const highlightLogsExpressionAction = createAction( - '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: [], }; } diff --git a/public/app/features/explore/state/query.test.ts b/public/app/features/explore/state/query.test.ts index 54d8f52e907..a80de95bc84 100644 --- a/public/app/features/explore/state/query.test.ts +++ b/public/app/features/explore/state/query.test.ts @@ -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() - .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() - .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', () => { diff --git a/public/app/features/explore/state/query.ts b/public/app/features/explore/state/query.ts index c5039d0ed05..d3deb6394c0 100644 --- a/public/app/features/explore/state/query.ts +++ b/public/app/features/explore/state/query.ts @@ -50,26 +50,15 @@ export interface AddQueryRowPayload { } export const addQueryRowAction = createAction('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('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('explore/changeQuery'); +export const changeQueriesAction = createAction('explore/changeQueries'); /** * Clear all queries and results. @@ -193,31 +182,6 @@ export function addQueryRow(exploreId: ExploreId, index: number): ThunkResult { - 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, diff --git a/public/app/features/explore/state/selectors.ts b/public/app/features/explore/state/selectors.ts index ec790dd8121..0fee6ab2be6 100644 --- a/public/app/features/explore/state/selectors.ts +++ b/public/app/features/explore/state/selectors.ts @@ -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]; diff --git a/public/app/features/explore/state/utils.ts b/public/app/features/explore/state/utils.ts index 8fb96f4a303..f6215106ec2 100644 --- a/public/app/features/explore/state/utils.ts +++ b/public/app/features/explore/state/utils.ts @@ -42,7 +42,6 @@ export const makeExplorePaneState = (): ExploreItemState => ({ scanning: false, loading: false, queryKeys: [], - latency: 0, isLive: false, isPaused: false, queryResponse: createEmptyQueryResponse(), diff --git a/public/app/features/query/components/QueryEditorRow.tsx b/public/app/features/query/components/QueryEditorRow.tsx index c99f8080239..3d34bdc2b3f 100644 --- a/public/app/features/query/components/QueryEditorRow.tsx +++ b/public/app/features/query/components/QueryEditorRow.tsx @@ -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 { onRunQuery: () => void; visualization?: ReactNode; hideDisableQuery?: boolean; + app?: CoreApp; + history?: Array>; + eventBus?: EventBusExtended; } interface State { @@ -108,7 +113,7 @@ export class QueryEditorRow extends PureComponent () => console.log('legacy render function called, it does nothing'), - events: new EventBusSrv(), + events: this.props.eventBus || new EventBusSrv(), range: getTimeSrv().timeRange(), }; } @@ -193,29 +198,52 @@ export class QueryEditorRow extends PureComponent) { + 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
(this.element = element)} />; } - if (datasource?.components?.QueryEditor) { - const QueryEditor = datasource.components.QueryEditor; + if (datasource) { + let QueryEditor = this.getReactQueryEditor(datasource); - return ( - - ); + if (QueryEditor) { + return ( + + ); + } } return
Data source plugin does not export any Query Editor component
; diff --git a/public/app/features/query/components/QueryEditorRows.tsx b/public/app/features/query/components/QueryEditorRows.tsx index 2ca3ad21f47..96c343f15f8 100644 --- a/public/app/features/query/components/QueryEditorRows.tsx +++ b/public/app/features/query/components/QueryEditorRows.tsx @@ -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>; + eventBus?: EventBusExtended; } export class QueryEditorRows extends PureComponent { @@ -89,7 +101,7 @@ export class QueryEditorRows extends PureComponent { }; render() { - const { dsSettings, data, queries } = this.props; + const { dsSettings, data, queries, app, history, eventBus } = this.props; return ( @@ -117,6 +129,9 @@ export class QueryEditorRows extends PureComponent { onAddQuery={this.props.onAddQuery} onRunQuery={this.props.onRunQueries} queries={queries} + app={app} + history={history} + eventBus={eventBus} /> ); })} diff --git a/public/app/plugins/datasource/cloud-monitoring/components/QueryEditor.tsx b/public/app/plugins/datasource/cloud-monitoring/components/QueryEditor.tsx index 19f3004a914..7513fcd4396 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/QueryEditor.tsx +++ b/public/app/plugins/datasource/cloud-monitoring/components/QueryEditor.tsx @@ -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; +export type Props = QueryEditorProps; export class QueryEditor extends PureComponent { async UNSAFE_componentWillMount() { diff --git a/public/app/plugins/datasource/cloudwatch/components/LogsQueryField.tsx b/public/app/plugins/datasource/cloudwatch/components/LogsQueryField.tsx index 7badbcd7bc3..1e0c423ded0 100644 --- a/public/app/plugins/datasource/cloudwatch/components/LogsQueryField.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/LogsQueryField.tsx @@ -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 { + extends QueryEditorProps { absoluteRange: AbsoluteTimeRange; onLabelsRefresh?: () => void; ExtraFieldElement?: ReactNode; diff --git a/public/app/plugins/datasource/cloudwatch/components/MetricsQueryEditor.tsx b/public/app/plugins/datasource/cloudwatch/components/MetricsQueryEditor.tsx index 25b738bde0b..a7761b5c673 100644 --- a/public/app/plugins/datasource/cloudwatch/components/MetricsQueryEditor.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/MetricsQueryEditor.tsx @@ -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; +export type Props = QueryEditorProps; interface State { showMeta: boolean; diff --git a/public/app/plugins/datasource/cloudwatch/components/PanelQueryEditor.tsx b/public/app/plugins/datasource/cloudwatch/components/PanelQueryEditor.tsx index 38fc79c9596..8bcb6def01c 100644 --- a/public/app/plugins/datasource/cloudwatch/components/PanelQueryEditor.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/PanelQueryEditor.tsx @@ -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; +export type Props = QueryEditorProps; const apiModes = { Metrics: { label: 'CloudWatch Metrics', value: 'Metrics' }, diff --git a/public/app/plugins/datasource/loki/components/LokiExploreQueryEditor.tsx b/public/app/plugins/datasource/loki/components/LokiExploreQueryEditor.tsx index 640aac68b0a..509b1d1e3c7 100644 --- a/public/app/plugins/datasource/loki/components/LokiExploreQueryEditor.tsx +++ b/public/app/plugins/datasource/loki/components/LokiExploreQueryEditor.tsx @@ -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; +type Props = QueryEditorProps; export function LokiExploreQueryEditor(props: Props) { const { query, data, datasource, history, onChange, onRunQuery, range } = props; diff --git a/public/app/plugins/datasource/loki/components/LokiQueryField.tsx b/public/app/plugins/datasource/loki/components/LokiQueryField.tsx index 02c24a42121..3682204a99e 100644 --- a/public/app/plugins/datasource/loki/components/LokiQueryField.tsx +++ b/public/app/plugins/datasource/loki/components/LokiQueryField.tsx @@ -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 { - history: LokiHistoryItem[]; +export interface LokiQueryFieldProps extends QueryEditorProps { ExtraFieldElement?: ReactNode; placeholder?: string; 'data-testid'?: string; diff --git a/public/app/plugins/datasource/loki/datasource.ts b/public/app/plugins/datasource/loki/datasource.ts index 7dd20585c40..7580a8abf71 100644 --- a/public/app/plugins/datasource/loki/datasource.ts +++ b/public/app/plugins/datasource/loki/datasource.ts @@ -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 { 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)!; diff --git a/public/app/plugins/datasource/prometheus/components/PromExploreQueryEditor.tsx b/public/app/plugins/datasource/prometheus/components/PromExploreQueryEditor.tsx index 3287560173c..6f9e0d901ec 100644 --- a/public/app/plugins/datasource/prometheus/components/PromExploreQueryEditor.tsx +++ b/public/app/plugins/datasource/prometheus/components/PromExploreQueryEditor.tsx @@ -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; +export type Props = QueryEditorProps; export const PromExploreQueryEditor: FC = (props: Props) => { const { range, query, data, datasource, history, onChange, onRunQuery } = props; diff --git a/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx b/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx index 2407b80317b..3e0c540d0b5 100644 --- a/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx +++ b/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx @@ -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 { - history: Array>; +interface PromQueryFieldProps extends QueryEditorProps { ExtraFieldElement?: ReactNode; placeholder?: string; 'data-testid'?: string; @@ -273,6 +265,7 @@ class PromQueryField extends React.PureComponent=|!=|<=|>|<|=|~|,)/; +interface AutocompleteContext { + history?: Array>; +} 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> } = { history: [] } + context: AutocompleteContext = {} ): Promise => { const emptyResult: TypeaheadOutput = { suggestions: [] }; @@ -194,13 +197,13 @@ export default class PromQlLanguageProvider extends LanguageProvider { return emptyResult; }; - getBeginningCompletionItems = (context: { history: Array> }): TypeaheadOutput => { + getBeginningCompletionItems = (context: AutocompleteContext): TypeaheadOutput => { return { suggestions: [...this.getEmptyCompletionItems(context).suggestions, ...this.getTermCompletionItems().suggestions], }; }; - getEmptyCompletionItems = (context: { history: Array> }): TypeaheadOutput => { + getEmptyCompletionItems = (context: AutocompleteContext): TypeaheadOutput => { const { history } = context; const suggestions: CompletionItemGroup[] = []; diff --git a/public/app/plugins/datasource/tempo/QueryField.tsx b/public/app/plugins/datasource/tempo/QueryField.tsx index 19bf8db4595..aa7dfa3d132 100644 --- a/public/app/plugins/datasource/tempo/QueryField.tsx +++ b/public/app/plugins/datasource/tempo/QueryField.tsx @@ -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, Themeable2 {} +interface Props extends QueryEditorProps, Themeable2 {} const DEFAULT_QUERY_TYPE: TempoQueryType = 'traceId'; diff --git a/public/app/plugins/datasource/zipkin/QueryField.tsx b/public/app/plugins/datasource/zipkin/QueryField.tsx index 125ada3b4bb..00352a987ad 100644 --- a/public/app/plugins/datasource/zipkin/QueryField.tsx +++ b/public/app/plugins/datasource/zipkin/QueryField.tsx @@ -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; +type Props = QueryEditorProps; export const ZipkinQueryField = ({ query, onChange, onRunQuery, datasource }: Props) => { const serviceOptions = useServices(datasource); diff --git a/public/app/types/explore.ts b/public/app/types/explore.ts index a4b6989b8f7..853245966b3 100644 --- a/public/app/types/explore.ts +++ b/public/app/types/explore.ts @@ -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