mirror of https://github.com/grafana/grafana.git
				
				
				
			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:
		
							parent
							
								
									e251863085
								
							
						
					
					
						commit
						f79173c99d
					
				|  | @ -21,12 +21,12 @@ The query editor for Explore is similar to the query editor for the data source | |||
|    ```ts | ||||
|    import React from 'react'; | ||||
| 
 | ||||
|    import { ExploreQueryFieldProps } from '@grafana/data'; | ||||
|    import { QueryEditorProps } from '@grafana/data'; | ||||
|    import { QueryField } from '@grafana/ui'; | ||||
|    import { DataSource } from './DataSource'; | ||||
|    import { MyQuery, MyDataSourceOptions } from './types'; | ||||
| 
 | ||||
|    export type Props = ExploreQueryFieldProps<DataSource, MyQuery, MyDataSourceOptions>; | ||||
|    export type Props = QueryEditorProps<DataSource, MyQuery, MyDataSourceOptions>; | ||||
| 
 | ||||
|    export default (props: Props) => { | ||||
|      return <h2>My query editor</h2>; | ||||
|  |  | |||
|  | @ -29,9 +29,13 @@ describe('Trace view', () => { | |||
| 
 | ||||
|     e2e.components.TraceViewer.spanBar().should('be.visible'); | ||||
| 
 | ||||
|     e2e.components.TraceViewer.spanBar() | ||||
|       .its('length') | ||||
|       .then((oldLength) => { | ||||
|         e2e.pages.Explore.General.scrollBar().scrollTo('center'); | ||||
| 
 | ||||
|         // After scrolling we should load more spans
 | ||||
|     e2e.components.TraceViewer.spanBar().should('have.length', 140); | ||||
|         e2e.components.TraceViewer.spanBar().its('length').should('be.gt', oldLength); | ||||
|       }); | ||||
|   }); | ||||
| }); | ||||
|  |  | |||
|  | @ -62,17 +62,17 @@ export class DataSourcePlugin< | |||
|     return this; | ||||
|   } | ||||
| 
 | ||||
|   setExploreQueryField(ExploreQueryField: ComponentType<ExploreQueryFieldProps<DSType, TQuery, TOptions>>) { | ||||
|   setExploreQueryField(ExploreQueryField: ComponentType<QueryEditorProps<DSType, TQuery, TOptions>>) { | ||||
|     this.components.ExploreQueryField = ExploreQueryField; | ||||
|     return this; | ||||
|   } | ||||
| 
 | ||||
|   setExploreMetricsQueryField(ExploreQueryField: ComponentType<ExploreQueryFieldProps<DSType, TQuery, TOptions>>) { | ||||
|   setExploreMetricsQueryField(ExploreQueryField: ComponentType<QueryEditorProps<DSType, TQuery, TOptions>>) { | ||||
|     this.components.ExploreMetricsQueryField = ExploreQueryField; | ||||
|     return this; | ||||
|   } | ||||
| 
 | ||||
|   setExploreLogsQueryField(ExploreQueryField: ComponentType<ExploreQueryFieldProps<DSType, TQuery, TOptions>>) { | ||||
|   setExploreLogsQueryField(ExploreQueryField: ComponentType<QueryEditorProps<DSType, TQuery, TOptions>>) { | ||||
|     this.components.ExploreLogsQueryField = ExploreQueryField; | ||||
|     return this; | ||||
|   } | ||||
|  | @ -147,9 +147,9 @@ export interface DataSourcePluginComponents< | |||
|   AnnotationsQueryCtrl?: any; | ||||
|   VariableQueryEditor?: any; | ||||
|   QueryEditor?: ComponentType<QueryEditorProps<DSType, TQuery, TOptions>>; | ||||
|   ExploreQueryField?: ComponentType<ExploreQueryFieldProps<DSType, TQuery, TOptions>>; | ||||
|   ExploreMetricsQueryField?: ComponentType<ExploreQueryFieldProps<DSType, TQuery, TOptions>>; | ||||
|   ExploreLogsQueryField?: ComponentType<ExploreQueryFieldProps<DSType, TQuery, TOptions>>; | ||||
|   ExploreQueryField?: ComponentType<QueryEditorProps<DSType, TQuery, TOptions>>; | ||||
|   ExploreMetricsQueryField?: ComponentType<QueryEditorProps<DSType, TQuery, TOptions>>; | ||||
|   ExploreLogsQueryField?: ComponentType<QueryEditorProps<DSType, TQuery, TOptions>>; | ||||
|   QueryEditorHelp?: ComponentType<QueryEditorHelpProps<TQuery>>; | ||||
|   ConfigEditor?: ComponentType<DataSourcePluginOptionsEditorProps<TOptions, TSecureOptions>>; | ||||
|   MetadataInspector?: ComponentType<MetadataInspectorProps<DSType, TQuery, TOptions>>; | ||||
|  | @ -295,7 +295,8 @@ abstract class DataSourceApi< | |||
|   modifyQuery?(query: TQuery, action: QueryFixAction): TQuery; | ||||
| 
 | ||||
|   /** | ||||
|    * Used in explore | ||||
|    * @deprecated since version 8.2.0 | ||||
|    * Not used anymore. | ||||
|    */ | ||||
|   getHighlighterExpression?(query: TQuery): string[]; | ||||
| 
 | ||||
|  | @ -373,7 +374,7 @@ export interface QueryEditorProps< | |||
|   data?: PanelData; | ||||
|   range?: TimeRange; | ||||
|   exploreId?: any; | ||||
|   history?: HistoryItem[]; | ||||
|   history?: Array<HistoryItem<TQuery>>; | ||||
|   queries?: DataQuery[]; | ||||
|   app?: CoreApp; | ||||
| } | ||||
|  | @ -385,15 +386,14 @@ export enum ExploreMode { | |||
|   Tracing = 'Tracing', | ||||
| } | ||||
| 
 | ||||
| export interface ExploreQueryFieldProps< | ||||
| /** | ||||
|  * @deprecated use QueryEditorProps instead | ||||
|  */ | ||||
| export type ExploreQueryFieldProps< | ||||
|   DSType extends DataSourceApi<TQuery, TOptions>, | ||||
|   TQuery extends DataQuery = DataQuery, | ||||
|   TOptions extends DataSourceJsonData = DataSourceJsonData | ||||
| > extends QueryEditorProps<DSType, TQuery, TOptions> { | ||||
|   history: any[]; | ||||
|   onBlur?: () => void; | ||||
|   exploreId?: any; | ||||
| } | ||||
| > = QueryEditorProps<DSType, TQuery, TOptions>; | ||||
| 
 | ||||
| export interface QueryEditorHelpProps<TQuery extends DataQuery = DataQuery> { | ||||
|   datasource: DataSourceApi<TQuery>; | ||||
|  |  | |||
|  | @ -1,10 +1,12 @@ | |||
| import { DataQuery } from './query'; | ||||
| import { RawTimeRange, TimeRange } from './time'; | ||||
| 
 | ||||
| type AnyQuery = DataQuery & Record<string, any>; | ||||
| 
 | ||||
| /** @internal */ | ||||
| export interface ExploreUrlState { | ||||
| export interface ExploreUrlState<T extends DataQuery = AnyQuery> { | ||||
|   datasource: string; | ||||
|   queries: any[]; // Should be a DataQuery, but we're going to strip refIds, so typing makes less sense
 | ||||
|   queries: T[]; | ||||
|   range: RawTimeRange; | ||||
|   originPanelId?: number; | ||||
|   context?: string; | ||||
|  |  | |||
|  | @ -33,7 +33,6 @@ import { LogRowMessage } from './LogRowMessage'; | |||
| import { LogLabels } from './LogLabels'; | ||||
| 
 | ||||
| interface Props extends Themeable2 { | ||||
|   highlighterExpressions?: string[]; | ||||
|   row: LogRowModel; | ||||
|   showDuplicates: boolean; | ||||
|   showLabels: boolean; | ||||
|  | @ -130,7 +129,6 @@ class UnThemedLogRow extends PureComponent<Props, State> { | |||
|       onClickFilterOutLabel, | ||||
|       onClickShowDetectedField, | ||||
|       onClickHideDetectedField, | ||||
|       highlighterExpressions, | ||||
|       enableLogDetails, | ||||
|       row, | ||||
|       showDuplicates, | ||||
|  | @ -192,7 +190,6 @@ class UnThemedLogRow extends PureComponent<Props, State> { | |||
|             /> | ||||
|           ) : ( | ||||
|             <LogRowMessage | ||||
|               highlighterExpressions={highlighterExpressions} | ||||
|               row={processedRow} | ||||
|               getRows={getRows} | ||||
|               errors={errors} | ||||
|  |  | |||
|  | @ -1,5 +1,4 @@ | |||
| import React, { PureComponent } from 'react'; | ||||
| import { isEqual } from 'lodash'; | ||||
| import tinycolor from 'tinycolor2'; | ||||
| import { css, cx } from '@emotion/css'; | ||||
| import { LogRowModel, findHighlightChunksInText, GrafanaTheme2 } from '@grafana/data'; | ||||
|  | @ -27,7 +26,6 @@ interface Props extends Themeable2 { | |||
|   errors?: LogRowContextQueryErrors; | ||||
|   context?: LogRowContextRows; | ||||
|   showContextToggle?: (row?: LogRowModel) => boolean; | ||||
|   highlighterExpressions?: string[]; | ||||
|   getRows: () => LogRowModel[]; | ||||
|   onToggleContext: () => void; | ||||
|   updateLimit?: () => void; | ||||
|  | @ -100,7 +98,6 @@ class UnThemedLogRowMessage extends PureComponent<Props> { | |||
| 
 | ||||
|   render() { | ||||
|     const { | ||||
|       highlighterExpressions, | ||||
|       row, | ||||
|       theme, | ||||
|       errors, | ||||
|  | @ -118,11 +115,7 @@ class UnThemedLogRowMessage extends PureComponent<Props> { | |||
|     const { hasAnsi, raw } = row; | ||||
|     const restructuredEntry = restructureLog(raw, prettifyLogMessage); | ||||
| 
 | ||||
|     const previewHighlights = highlighterExpressions?.length && !isEqual(highlighterExpressions, row.searchWords); | ||||
|     const highlights = previewHighlights ? highlighterExpressions : row.searchWords; | ||||
|     const highlightClassName = previewHighlights | ||||
|       ? cx([style.logsRowMatchHighLight, style.logsRowMatchHighLightPreview]) | ||||
|       : cx([style.logsRowMatchHighLight]); | ||||
|     const highlightClassName = cx([style.logsRowMatchHighLight]); | ||||
|     const styles = getStyles(theme); | ||||
| 
 | ||||
|     return ( | ||||
|  | @ -146,7 +139,7 @@ class UnThemedLogRowMessage extends PureComponent<Props> { | |||
|             /> | ||||
|           )} | ||||
|           <span className={cx(styles.positionRelative, { [styles.rowWithContext]: contextIsOpen })}> | ||||
|             {renderLogMessage(hasAnsi, restructuredEntry, highlights, highlightClassName)} | ||||
|             {renderLogMessage(hasAnsi, restructuredEntry, row.searchWords, highlightClassName)} | ||||
|           </span> | ||||
|           {showContextToggle?.(row) && ( | ||||
|             <span | ||||
|  |  | |||
|  | @ -12,7 +12,6 @@ describe('LogRows', () => { | |||
|       <LogRows | ||||
|         logRows={rows} | ||||
|         dedupStrategy={LogsDedupStrategy.none} | ||||
|         highlighterExpressions={[]} | ||||
|         showLabels={false} | ||||
|         showTime={false} | ||||
|         wrapLogMessage={true} | ||||
|  | @ -35,7 +34,6 @@ describe('LogRows', () => { | |||
|       <LogRows | ||||
|         logRows={rows} | ||||
|         dedupStrategy={LogsDedupStrategy.none} | ||||
|         highlighterExpressions={[]} | ||||
|         showLabels={false} | ||||
|         showTime={false} | ||||
|         wrapLogMessage={true} | ||||
|  | @ -67,7 +65,6 @@ describe('LogRows', () => { | |||
|         logRows={rows} | ||||
|         deduplicatedRows={dedupedRows} | ||||
|         dedupStrategy={LogsDedupStrategy.none} | ||||
|         highlighterExpressions={[]} | ||||
|         showLabels={false} | ||||
|         showTime={false} | ||||
|         wrapLogMessage={true} | ||||
|  | @ -89,7 +86,6 @@ describe('LogRows', () => { | |||
|       <LogRows | ||||
|         logRows={rows} | ||||
|         dedupStrategy={LogsDedupStrategy.none} | ||||
|         highlighterExpressions={[]} | ||||
|         showLabels={false} | ||||
|         showTime={false} | ||||
|         wrapLogMessage={true} | ||||
|  | @ -112,7 +108,6 @@ describe('LogRows', () => { | |||
|       <LogRows | ||||
|         logRows={rows} | ||||
|         dedupStrategy={LogsDedupStrategy.none} | ||||
|         highlighterExpressions={[]} | ||||
|         showLabels={false} | ||||
|         showTime={false} | ||||
|         wrapLogMessage={true} | ||||
|  | @ -137,7 +132,6 @@ describe('LogRows', () => { | |||
|       <LogRows | ||||
|         logRows={rows} | ||||
|         dedupStrategy={LogsDedupStrategy.none} | ||||
|         highlighterExpressions={[]} | ||||
|         showLabels={false} | ||||
|         showTime={false} | ||||
|         wrapLogMessage={true} | ||||
|  |  | |||
|  | @ -16,7 +16,6 @@ export interface Props extends Themeable2 { | |||
|   logRows?: LogRowModel[]; | ||||
|   deduplicatedRows?: LogRowModel[]; | ||||
|   dedupStrategy: LogsDedupStrategy; | ||||
|   highlighterExpressions?: string[]; | ||||
|   showLabels: boolean; | ||||
|   showTime: boolean; | ||||
|   wrapLogMessage: boolean; | ||||
|  | @ -88,7 +87,6 @@ class UnThemedLogRows extends PureComponent<Props, State> { | |||
|       prettifyLogMessage, | ||||
|       logRows, | ||||
|       deduplicatedRows, | ||||
|       highlighterExpressions, | ||||
|       timeZone, | ||||
|       onClickFilterLabel, | ||||
|       onClickFilterOutLabel, | ||||
|  | @ -129,7 +127,6 @@ class UnThemedLogRows extends PureComponent<Props, State> { | |||
|                 key={row.uid} | ||||
|                 getRows={getRows} | ||||
|                 getRowContext={getRowContext} | ||||
|                 highlighterExpressions={highlighterExpressions} | ||||
|                 row={row} | ||||
|                 showContextToggle={showContextToggle} | ||||
|                 showDuplicates={showDuplicates} | ||||
|  |  | |||
|  | @ -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: { | ||||
|  |  | |||
|  | @ -165,11 +165,10 @@ export function buildQueryTransaction( | |||
|     scanning, | ||||
|     id: generateKey(), // reusing for unique ID
 | ||||
|     done: false, | ||||
|     latency: 0, | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
| export const clearQueryKeys: (query: DataQuery) => object = ({ key, refId, ...rest }) => rest; | ||||
| export const clearQueryKeys: (query: DataQuery) => DataQuery = ({ key, ...rest }) => rest; | ||||
| 
 | ||||
| const isSegment = (segment: { [key: string]: string }, ...props: string[]) => | ||||
|   props.some((prop) => segment.hasOwnProperty(prop)); | ||||
|  | @ -286,7 +285,7 @@ export function ensureQueries(queries?: DataQuery[]): DataQuery[] { | |||
|  * A target is non-empty when it has keys (with non-empty values) other than refId, key and context. | ||||
|  */ | ||||
| const validKeys = ['refId', 'key', 'context']; | ||||
| export function hasNonEmptyQuery<TQuery extends DataQuery = any>(queries: TQuery[]): boolean { | ||||
| export function hasNonEmptyQuery<TQuery extends DataQuery>(queries: TQuery[]): boolean { | ||||
|   return ( | ||||
|     queries && | ||||
|     queries.some((query: any) => { | ||||
|  | @ -302,7 +301,7 @@ export function hasNonEmptyQuery<TQuery extends DataQuery = any>(queries: TQuery | |||
| /** | ||||
|  * Update the query history. Side-effect: store history in local storage | ||||
|  */ | ||||
| export function updateHistory<T extends DataQuery = any>( | ||||
| export function updateHistory<T extends DataQuery>( | ||||
|   history: Array<HistoryItem<T>>, | ||||
|   datasourceId: string, | ||||
|   queries: T[] | ||||
|  |  | |||
|  | @ -9,7 +9,7 @@ import { ErrorBoundaryAlert, CustomScrollbar, Collapse, withTheme2, Themeable2 } | |||
| import { AbsoluteTimeRange, DataQuery, LoadingState, RawTimeRange, DataFrame, GrafanaTheme2 } from '@grafana/data'; | ||||
| 
 | ||||
| import LogsContainer from './LogsContainer'; | ||||
| import QueryRows from './QueryRows'; | ||||
| import { QueryRows } from './QueryRows'; | ||||
| import TableContainer from './TableContainer'; | ||||
| import RichHistoryContainer from './RichHistory/RichHistoryContainer'; | ||||
| import ExploreQueryInspector from './ExploreQueryInspector'; | ||||
|  | @ -268,7 +268,6 @@ export class Explore extends React.PureComponent<Props, ExploreState> { | |||
|       datasourceInstance, | ||||
|       datasourceMissing, | ||||
|       exploreId, | ||||
|       queryKeys, | ||||
|       graphResult, | ||||
|       queryResponse, | ||||
|       isLive, | ||||
|  | @ -292,7 +291,7 @@ export class Explore extends React.PureComponent<Props, ExploreState> { | |||
|         {datasourceInstance && ( | ||||
|           <div className="explore-container"> | ||||
|             <div className={cx('panel-container', styles.queryContainer)}> | ||||
|               <QueryRows exploreId={exploreId} queryKeys={queryKeys} /> | ||||
|               <QueryRows exploreId={exploreId} /> | ||||
|               <SecondaryActions | ||||
|                 addQueryRowButtonDisabled={isLive} | ||||
|                 // We cannot show multiple traces at the same time right now so we do not show add query button.
 | ||||
|  |  | |||
|  | @ -53,7 +53,6 @@ interface Props extends Themeable2 { | |||
|   logsQueries?: DataQuery[]; | ||||
|   visibleRange?: AbsoluteTimeRange; | ||||
|   theme: GrafanaTheme2; | ||||
|   highlighterExpressions?: string[]; | ||||
|   loading: boolean; | ||||
|   loadingState: LoadingState; | ||||
|   absoluteRange: AbsoluteTimeRange; | ||||
|  | @ -254,7 +253,6 @@ export class UnthemedLogs extends PureComponent<Props, State> { | |||
|       logsMeta, | ||||
|       logsSeries, | ||||
|       visibleRange, | ||||
|       highlighterExpressions, | ||||
|       loading = false, | ||||
|       loadingState, | ||||
|       onClickFilterLabel, | ||||
|  | @ -368,7 +366,6 @@ export class UnthemedLogs extends PureComponent<Props, State> { | |||
|               deduplicatedRows={dedupedRows} | ||||
|               dedupStrategy={dedupStrategy} | ||||
|               getRowContext={this.props.getRowContext} | ||||
|               highlighterExpressions={highlighterExpressions} | ||||
|               onClickFilterLabel={onClickFilterLabel} | ||||
|               onClickFilterOutLabel={onClickFilterOutLabel} | ||||
|               showContextToggle={showContextToggle} | ||||
|  |  | |||
|  | @ -62,7 +62,6 @@ export class LogsContainer extends PureComponent<LogsContainerProps> { | |||
|     const { | ||||
|       loading, | ||||
|       loadingState, | ||||
|       logsHighlighterExpressions, | ||||
|       logRows, | ||||
|       logsMeta, | ||||
|       logsSeries, | ||||
|  | @ -123,7 +122,6 @@ export class LogsContainer extends PureComponent<LogsContainerProps> { | |||
|               logsSeries={logsSeries} | ||||
|               logsQueries={logsQueries} | ||||
|               width={width} | ||||
|               highlighterExpressions={logsHighlighterExpressions} | ||||
|               loading={loading} | ||||
|               loadingState={loadingState} | ||||
|               onChangeTime={this.onChangeTime} | ||||
|  | @ -153,22 +151,11 @@ function mapStateToProps(state: StoreState, { exploreId }: { exploreId: string } | |||
|   const explore = state.explore; | ||||
|   // @ts-ignore
 | ||||
|   const item: ExploreItemState = explore[exploreId]; | ||||
|   const { | ||||
|     logsHighlighterExpressions, | ||||
|     logsResult, | ||||
|     loading, | ||||
|     scanning, | ||||
|     datasourceInstance, | ||||
|     isLive, | ||||
|     isPaused, | ||||
|     range, | ||||
|     absoluteRange, | ||||
|   } = item; | ||||
|   const { logsResult, loading, scanning, datasourceInstance, isLive, isPaused, range, absoluteRange } = item; | ||||
|   const timeZone = getTimeZone(state.user); | ||||
| 
 | ||||
|   return { | ||||
|     loading, | ||||
|     logsHighlighterExpressions, | ||||
|     logRows: logsResult?.rows, | ||||
|     logsMeta: logsResult?.meta, | ||||
|     logsSeries: logsResult?.series, | ||||
|  |  | |||
|  | @ -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%' }} />; | ||||
|   } | ||||
| } | ||||
|  | @ -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); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
|  | @ -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); | ||||
|  | @ -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); | ||||
|   }); | ||||
| }); | ||||
|  | @ -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> | ||||
|   ); | ||||
| } | ||||
|  | @ -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(); | ||||
|   }); | ||||
| }); | ||||
|  | @ -1,27 +1,79 @@ | |||
| // Libraries
 | ||||
| import React, { PureComponent } from 'react'; | ||||
| 
 | ||||
| // Components
 | ||||
| import QueryRow from './QueryRow'; | ||||
| 
 | ||||
| // Types
 | ||||
| import React, { useCallback, useMemo } from 'react'; | ||||
| import { ExploreId } from 'app/types/explore'; | ||||
| import { useDispatch, useSelector } from 'react-redux'; | ||||
| import { getDatasourceSrv } from '../plugins/datasource_srv'; | ||||
| import { runQueries, changeQueriesAction } from './state/query'; | ||||
| import { CoreApp, DataQuery } from '@grafana/data'; | ||||
| import { getNextRefIdChar } from 'app/core/utils/query'; | ||||
| import { QueryEditorRows } from '../query/components/QueryEditorRows'; | ||||
| import { createSelector } from '@reduxjs/toolkit'; | ||||
| import { getExploreItemSelector } from './state/selectors'; | ||||
| 
 | ||||
| interface QueryRowsProps { | ||||
|   className?: string; | ||||
| interface Props { | ||||
|   exploreId: ExploreId; | ||||
|   queryKeys: string[]; | ||||
| } | ||||
| 
 | ||||
| export default class QueryRows extends PureComponent<QueryRowsProps> { | ||||
|   render() { | ||||
|     const { className = '', exploreId, queryKeys } = this.props; | ||||
|     return ( | ||||
|       <div className={className}> | ||||
|         {queryKeys.map((key, index) => { | ||||
|           return <QueryRow key={key} exploreId={exploreId} index={index} />; | ||||
|         })} | ||||
|       </div> | ||||
| const makeSelectors = (exploreId: ExploreId) => { | ||||
|   const exploreItemSelector = getExploreItemSelector(exploreId); | ||||
|   return { | ||||
|     getQueries: createSelector(exploreItemSelector, (s) => s!.queries), | ||||
|     getQueryResponse: createSelector(exploreItemSelector, (s) => s!.queryResponse), | ||||
|     getHistory: createSelector(exploreItemSelector, (s) => s!.history), | ||||
|     getEventBridge: createSelector(exploreItemSelector, (s) => s!.eventBridge), | ||||
|     getDatasourceInstanceSettings: createSelector( | ||||
|       exploreItemSelector, | ||||
|       (s) => getDatasourceSrv().getInstanceSettings(s!.datasourceInstance?.name)! | ||||
|     ), | ||||
|   }; | ||||
| }; | ||||
| 
 | ||||
| export const QueryRows = ({ exploreId }: Props) => { | ||||
|   const dispatch = useDispatch(); | ||||
|   const { getQueries, getDatasourceInstanceSettings, getQueryResponse, getHistory, getEventBridge } = useMemo( | ||||
|     () => makeSelectors(exploreId), | ||||
|     [exploreId] | ||||
|   ); | ||||
| 
 | ||||
|   const queries = useSelector(getQueries); | ||||
|   const dsSettings = useSelector(getDatasourceInstanceSettings); | ||||
|   const queryResponse = useSelector(getQueryResponse); | ||||
|   const history = useSelector(getHistory); | ||||
|   const eventBridge = useSelector(getEventBridge); | ||||
| 
 | ||||
|   const onRunQueries = useCallback(() => { | ||||
|     dispatch(runQueries(exploreId)); | ||||
|   }, [dispatch, exploreId]); | ||||
| 
 | ||||
|   const onChange = useCallback( | ||||
|     (newQueries: DataQuery[]) => { | ||||
|       dispatch(changeQueriesAction({ queries: newQueries, exploreId })); | ||||
| 
 | ||||
|       // if we are removing a query we want to run the remaining ones
 | ||||
|       if (newQueries.length < queries.length) { | ||||
|         onRunQueries(); | ||||
|       } | ||||
| } | ||||
|     }, | ||||
|     [dispatch, exploreId, onRunQueries, queries] | ||||
|   ); | ||||
| 
 | ||||
|   const onAddQuery = useCallback( | ||||
|     (query: DataQuery) => { | ||||
|       onChange([...queries, { ...query, refId: getNextRefIdChar(queries) }]); | ||||
|     }, | ||||
|     [onChange, queries] | ||||
|   ); | ||||
| 
 | ||||
|   return ( | ||||
|     <QueryEditorRows | ||||
|       dsSettings={dsSettings} | ||||
|       queries={queries} | ||||
|       onQueriesChange={onChange} | ||||
|       onAddQuery={onAddQuery} | ||||
|       onRunQueries={onRunQueries} | ||||
|       data={queryResponse} | ||||
|       app={CoreApp.Explore} | ||||
|       history={history} | ||||
|       eventBus={eventBridge} | ||||
|     /> | ||||
|   ); | ||||
| }; | ||||
|  |  | |||
|  | @ -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); | ||||
|   }); | ||||
| }); | ||||
|  | @ -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> | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | @ -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(); | ||||
|  |  | |||
|  | @ -16,7 +16,6 @@ exports[`Explore should render component 1`] = ` | |||
|     > | ||||
|       <QueryRows | ||||
|         exploreId="left" | ||||
|         queryKeys={Array []} | ||||
|       /> | ||||
|       <SecondaryActions | ||||
|         addQueryRowButtonDisabled={false} | ||||
|  |  | |||
|  | @ -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> | ||||
| `; | ||||
|  | @ -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.
 | ||||
|  |  | |||
|  | @ -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, | ||||
|     }; | ||||
|   } | ||||
| 
 | ||||
|  |  | |||
|  | @ -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 }) | ||||
|       ) | ||||
|     ); | ||||
| 
 | ||||
|  |  | |||
|  | @ -44,17 +44,6 @@ export interface ChangeSizePayload { | |||
| } | ||||
| export const changeSizeAction = createAction<ChangeSizePayload>('explore/changeSize'); | ||||
| 
 | ||||
| /** | ||||
|  * Highlight expressions in the log results | ||||
|  */ | ||||
| export interface HighlightLogsExpressionPayload { | ||||
|   exploreId: ExploreId; | ||||
|   expressions: string[]; | ||||
| } | ||||
| export const highlightLogsExpressionAction = createAction<HighlightLogsExpressionPayload>( | ||||
|   'explore/highlightLogsExpression' | ||||
| ); | ||||
| 
 | ||||
| /** | ||||
|  * Initialize Explore state with state from the URL and the React component. | ||||
|  * Call this only on components for with the Explore state has not been initialized. | ||||
|  | @ -210,17 +199,6 @@ export const paneReducer = (state: ExploreItemState = makeExplorePaneState(), ac | |||
|     return { ...state, containerWidth }; | ||||
|   } | ||||
| 
 | ||||
|   if (highlightLogsExpressionAction.match(action)) { | ||||
|     const { expressions: newExpressions } = action.payload; | ||||
|     const { logsHighlighterExpressions: currentExpressions } = state; | ||||
| 
 | ||||
|     return { | ||||
|       ...state, | ||||
|       // Prevents re-renders. As logsHighlighterExpressions [] comes from datasource, we cannot control if it returns new array or not.
 | ||||
|       logsHighlighterExpressions: isEqual(newExpressions, currentExpressions) ? currentExpressions : newExpressions, | ||||
|     }; | ||||
|   } | ||||
| 
 | ||||
|   if (initializeExploreAction.match(action)) { | ||||
|     const { containerWidth, eventBridge, queries, range, originPanelId, datasourceInstance, history } = action.payload; | ||||
| 
 | ||||
|  | @ -237,7 +215,6 @@ export const paneReducer = (state: ExploreItemState = makeExplorePaneState(), ac | |||
|       history, | ||||
|       datasourceMissing: !datasourceInstance, | ||||
|       queryResponse: createEmptyQueryResponse(), | ||||
|       logsHighlighterExpressions: undefined, | ||||
|       cache: [], | ||||
|     }; | ||||
|   } | ||||
|  |  | |||
|  | @ -6,7 +6,6 @@ import { | |||
|   clearCache, | ||||
|   importQueries, | ||||
|   queryReducer, | ||||
|   removeQueryRowAction, | ||||
|   runQueries, | ||||
|   scanStartAction, | ||||
|   scanStopAction, | ||||
|  | @ -34,7 +33,6 @@ import { configureStore } from '../../../store/configureStore'; | |||
| import { setTimeSrv } from '../../dashboard/services/TimeSrv'; | ||||
| import Mock = jest.Mock; | ||||
| 
 | ||||
| const QUERY_KEY_REGEX = /Q-(?:[a-z0-9]+-){5}(?:[0-9]+)/; | ||||
| const t = toUtc(); | ||||
| const testRange = { | ||||
|   from: t, | ||||
|  | @ -213,55 +211,6 @@ describe('reducer', () => { | |||
|           queryKeys: ['mockKey-0'], | ||||
|         } as unknown) as ExploreItemState); | ||||
|     }); | ||||
|     it('removes a query row', () => { | ||||
|       reducerTester<ExploreItemState>() | ||||
|         .givenReducer(queryReducer, ({ | ||||
|           queries: [ | ||||
|             { refId: 'A', key: 'mockKey' }, | ||||
|             { refId: 'B', key: 'mockKey' }, | ||||
|           ], | ||||
|           queryKeys: ['mockKey-0', 'mockKey-1'], | ||||
|         } as unknown) as ExploreItemState) | ||||
|         .whenActionIsDispatched( | ||||
|           removeQueryRowAction({ | ||||
|             exploreId: ExploreId.left, | ||||
|             index: 0, | ||||
|           }) | ||||
|         ) | ||||
|         .thenStatePredicateShouldEqual((resultingState: ExploreItemState) => { | ||||
|           expect(resultingState.queries.length).toBe(1); | ||||
|           expect(resultingState.queries[0].refId).toBe('A'); | ||||
|           expect(resultingState.queries[0].key).toMatch(QUERY_KEY_REGEX); | ||||
|           expect(resultingState.queryKeys[0]).toMatch(QUERY_KEY_REGEX); | ||||
|           return true; | ||||
|         }); | ||||
|     }); | ||||
|     it('reassigns query refId after removing a query to keep queries in order', () => { | ||||
|       reducerTester<ExploreItemState>() | ||||
|         .givenReducer(queryReducer, ({ | ||||
|           queries: [{ refId: 'A' }, { refId: 'B' }, { refId: 'C' }], | ||||
|           queryKeys: ['undefined-0', 'undefined-1', 'undefined-2'], | ||||
|         } as unknown) as ExploreItemState) | ||||
|         .whenActionIsDispatched( | ||||
|           removeQueryRowAction({ | ||||
|             exploreId: ExploreId.left, | ||||
|             index: 0, | ||||
|           }) | ||||
|         ) | ||||
|         .thenStatePredicateShouldEqual((resultingState: ExploreItemState) => { | ||||
|           expect(resultingState.queries.length).toBe(2); | ||||
|           const queriesRefIds = resultingState.queries.map((query) => query.refId); | ||||
|           const queriesKeys = resultingState.queries.map((query) => query.key); | ||||
|           expect(queriesRefIds).toEqual(['A', 'B']); | ||||
|           queriesKeys.forEach((queryKey) => { | ||||
|             expect(queryKey).toMatch(QUERY_KEY_REGEX); | ||||
|           }); | ||||
|           resultingState.queryKeys.forEach((queryKey) => { | ||||
|             expect(queryKey).toMatch(QUERY_KEY_REGEX); | ||||
|           }); | ||||
|           return true; | ||||
|         }); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   describe('caching', () => { | ||||
|  |  | |||
|  | @ -50,26 +50,15 @@ export interface AddQueryRowPayload { | |||
| } | ||||
| export const addQueryRowAction = createAction<AddQueryRowPayload>('explore/addQueryRow'); | ||||
| 
 | ||||
| /** | ||||
|  * Remove query row of the given index, as well as associated query results. | ||||
|  */ | ||||
| export interface RemoveQueryRowPayload { | ||||
|   exploreId: ExploreId; | ||||
|   index: number; | ||||
| } | ||||
| export const removeQueryRowAction = createAction<RemoveQueryRowPayload>('explore/removeQueryRow'); | ||||
| 
 | ||||
| /** | ||||
|  * Query change handler for the query row with the given index. | ||||
|  * If `override` is reset the query modifications and run the queries. Use this to set queries via a link. | ||||
|  */ | ||||
| export interface ChangeQueryPayload { | ||||
| export interface ChangeQueriesPayload { | ||||
|   exploreId: ExploreId; | ||||
|   query: DataQuery; | ||||
|   index: number; | ||||
|   override: boolean; | ||||
|   queries: DataQuery[]; | ||||
| } | ||||
| export const changeQueryAction = createAction<ChangeQueryPayload>('explore/changeQuery'); | ||||
| export const changeQueriesAction = createAction<ChangeQueriesPayload>('explore/changeQueries'); | ||||
| 
 | ||||
| /** | ||||
|  * Clear all queries and results. | ||||
|  | @ -193,31 +182,6 @@ export function addQueryRow(exploreId: ExploreId, index: number): ThunkResult<vo | |||
|   }; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Query change handler for the query row with the given index. | ||||
|  * If `override` is reset the query modifications and run the queries. Use this to set queries via a link. | ||||
|  */ | ||||
| export function changeQuery( | ||||
|   exploreId: ExploreId, | ||||
|   query: DataQuery, | ||||
|   index: number, | ||||
|   override = false | ||||
| ): ThunkResult<void> { | ||||
|   return (dispatch, getState) => { | ||||
|     // Null query means reset
 | ||||
|     if (query === null) { | ||||
|       const queries = getState().explore[exploreId]!.queries; | ||||
|       const { refId, key } = queries[index]; | ||||
|       query = generateNewKeyAndAddRefIdIfMissing({ refId, key }, queries, index); | ||||
|     } | ||||
| 
 | ||||
|     dispatch(changeQueryAction({ exploreId, query, index, override })); | ||||
|     if (override) { | ||||
|       dispatch(runQueries(exploreId)); | ||||
|     } | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Clear all queries and results. | ||||
|  */ | ||||
|  | @ -352,7 +316,6 @@ export const runQueries = ( | |||
|       // If we don't have results saved in cache, run new queries
 | ||||
|     } else { | ||||
|       if (!hasNonEmptyQuery(queries)) { | ||||
|         dispatch(clearQueriesAction({ exploreId })); | ||||
|         dispatch(stateSave({ replace: options?.replaceUrl })); // Remember to save to state and update location
 | ||||
|         return; | ||||
|       } | ||||
|  | @ -515,23 +478,16 @@ export const queryReducer = (state: ExploreItemState, action: AnyAction): Explor | |||
|     return { | ||||
|       ...state, | ||||
|       queries: nextQueries, | ||||
|       logsHighlighterExpressions: undefined, | ||||
|       queryKeys: getQueryKeys(nextQueries, state.datasourceInstance), | ||||
|     }; | ||||
|   } | ||||
| 
 | ||||
|   if (changeQueryAction.match(action)) { | ||||
|     const { queries } = state; | ||||
|     const { query, index } = action.payload; | ||||
| 
 | ||||
|     // Override path: queries are completely reset
 | ||||
|     const nextQuery: DataQuery = generateNewKeyAndAddRefIdIfMissing(query, queries, index); | ||||
|     const nextQueries = [...queries]; | ||||
|     nextQueries[index] = nextQuery; | ||||
|   if (changeQueriesAction.match(action)) { | ||||
|     const { queries } = action.payload; | ||||
| 
 | ||||
|     return { | ||||
|       ...state, | ||||
|       queries: nextQueries, | ||||
|       queries, | ||||
|     }; | ||||
|   } | ||||
| 
 | ||||
|  | @ -587,33 +543,6 @@ export const queryReducer = (state: ExploreItemState, action: AnyAction): Explor | |||
|     }; | ||||
|   } | ||||
| 
 | ||||
|   if (removeQueryRowAction.match(action)) { | ||||
|     const { queries } = state; | ||||
|     const { index } = action.payload; | ||||
| 
 | ||||
|     if (queries.length <= 1) { | ||||
|       return state; | ||||
|     } | ||||
| 
 | ||||
|     // removes a query under a given index and reassigns query keys and refIds to keep everything in order
 | ||||
|     const queriesAfterRemoval: DataQuery[] = [...queries.slice(0, index), ...queries.slice(index + 1)].map((query) => { | ||||
|       return { ...query, refId: '' }; | ||||
|     }); | ||||
| 
 | ||||
|     const nextQueries: DataQuery[] = []; | ||||
| 
 | ||||
|     queriesAfterRemoval.forEach((query, i) => { | ||||
|       nextQueries.push(generateNewKeyAndAddRefIdIfMissing(query, nextQueries, i)); | ||||
|     }); | ||||
| 
 | ||||
|     return { | ||||
|       ...state, | ||||
|       queries: nextQueries, | ||||
|       logsHighlighterExpressions: undefined, | ||||
|       queryKeys: getQueryKeys(nextQueries, state.datasourceInstance), | ||||
|     }; | ||||
|   } | ||||
| 
 | ||||
|   if (setQueriesAction.match(action)) { | ||||
|     const { queries } = action.payload; | ||||
|     return { | ||||
|  | @ -752,8 +681,6 @@ export const processQueryResponse = ( | |||
|     return { ...state }; | ||||
|   } | ||||
| 
 | ||||
|   const latency = request.endTime ? request.endTime - request.startTime : 0; | ||||
| 
 | ||||
|   // Send legacy data to Angular editors
 | ||||
|   if (state.datasourceInstance?.components?.QueryCtrl) { | ||||
|     const legacy = series.map((v) => toLegacyResponseData(v)); | ||||
|  | @ -762,7 +689,6 @@ export const processQueryResponse = ( | |||
| 
 | ||||
|   return { | ||||
|     ...state, | ||||
|     latency, | ||||
|     queryResponse: response, | ||||
|     graphResult, | ||||
|     tableResult, | ||||
|  |  | |||
|  | @ -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]; | ||||
|  |  | |||
|  | @ -42,7 +42,6 @@ export const makeExplorePaneState = (): ExploreItemState => ({ | |||
|   scanning: false, | ||||
|   loading: false, | ||||
|   queryKeys: [], | ||||
|   latency: 0, | ||||
|   isLive: false, | ||||
|   isPaused: false, | ||||
|   queryResponse: createEmptyQueryResponse(), | ||||
|  |  | |||
|  | @ -8,11 +8,13 @@ import { AngularComponent, getAngularLoader } from '@grafana/runtime'; | |||
| import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv'; | ||||
| import { ErrorBoundaryAlert, HorizontalGroup } from '@grafana/ui'; | ||||
| import { | ||||
|   CoreApp, | ||||
|   DataQuery, | ||||
|   DataSourceApi, | ||||
|   DataSourceInstanceSettings, | ||||
|   EventBusExtended, | ||||
|   EventBusSrv, | ||||
|   HistoryItem, | ||||
|   LoadingState, | ||||
|   PanelData, | ||||
|   PanelEvents, | ||||
|  | @ -45,6 +47,9 @@ interface Props<TQuery extends DataQuery> { | |||
|   onRunQuery: () => void; | ||||
|   visualization?: ReactNode; | ||||
|   hideDisableQuery?: boolean; | ||||
|   app?: CoreApp; | ||||
|   history?: Array<HistoryItem<TQuery>>; | ||||
|   eventBus?: EventBusExtended; | ||||
| } | ||||
| 
 | ||||
| interface State<TQuery extends DataQuery> { | ||||
|  | @ -108,7 +113,7 @@ export class QueryEditorRow<TQuery extends DataQuery> extends PureComponent<Prop | |||
|         this.props.onRunQuery(); | ||||
|       }, | ||||
|       render: () => () => console.log('legacy render function called, it does nothing'), | ||||
|       events: new EventBusSrv(), | ||||
|       events: this.props.eventBus || new EventBusSrv(), | ||||
|       range: getTimeSrv().timeRange(), | ||||
|     }; | ||||
|   } | ||||
|  | @ -193,17 +198,37 @@ export class QueryEditorRow<TQuery extends DataQuery> extends PureComponent<Prop | |||
|     this.renderAngularQueryEditor(); | ||||
|   }; | ||||
| 
 | ||||
|   getReactQueryEditor(ds: DataSourceApi<TQuery>) { | ||||
|     if (!ds) { | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     switch (this.props.app) { | ||||
|       case CoreApp.Explore: | ||||
|         return ( | ||||
|           ds.components?.ExploreMetricsQueryField || | ||||
|           ds.components?.ExploreLogsQueryField || | ||||
|           ds.components?.ExploreQueryField || | ||||
|           ds.components?.QueryEditor | ||||
|         ); | ||||
|       case CoreApp.Dashboard: | ||||
|       default: | ||||
|         return ds.components?.QueryEditor; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   renderPluginEditor = () => { | ||||
|     const { query, onChange, queries, onRunQuery } = this.props; | ||||
|     const { query, onChange, queries, onRunQuery, app = CoreApp.Dashboard, history } = this.props; | ||||
|     const { datasource, data } = this.state; | ||||
| 
 | ||||
|     if (datasource?.components?.QueryCtrl) { | ||||
|       return <div ref={(element) => (this.element = element)} />; | ||||
|     } | ||||
| 
 | ||||
|     if (datasource?.components?.QueryEditor) { | ||||
|       const QueryEditor = datasource.components.QueryEditor; | ||||
|     if (datasource) { | ||||
|       let QueryEditor = this.getReactQueryEditor(datasource); | ||||
| 
 | ||||
|       if (QueryEditor) { | ||||
|         return ( | ||||
|           <QueryEditor | ||||
|             key={datasource?.name} | ||||
|  | @ -214,9 +239,12 @@ export class QueryEditorRow<TQuery extends DataQuery> extends PureComponent<Prop | |||
|             data={data} | ||||
|             range={getTimeSrv().timeRange()} | ||||
|             queries={queries} | ||||
|             app={app} | ||||
|             history={history} | ||||
|           /> | ||||
|         ); | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     return <div>Data source plugin does not export any Query Editor component</div>; | ||||
|   }; | ||||
|  |  | |||
|  | @ -2,7 +2,14 @@ | |||
| import React, { PureComponent } from 'react'; | ||||
| 
 | ||||
| // Types
 | ||||
| import { DataQuery, DataSourceInstanceSettings, PanelData } from '@grafana/data'; | ||||
| import { | ||||
|   CoreApp, | ||||
|   DataQuery, | ||||
|   DataSourceInstanceSettings, | ||||
|   EventBusExtended, | ||||
|   HistoryItem, | ||||
|   PanelData, | ||||
| } from '@grafana/data'; | ||||
| import { QueryEditorRow } from './QueryEditorRow'; | ||||
| import { DragDropContext, Droppable, DropResult } from 'react-beautiful-dnd'; | ||||
| import { getDataSourceSrv } from '@grafana/runtime'; | ||||
|  | @ -19,6 +26,11 @@ interface Props { | |||
| 
 | ||||
|   // Query Response Data
 | ||||
|   data: PanelData; | ||||
| 
 | ||||
|   // Misc
 | ||||
|   app?: CoreApp; | ||||
|   history?: Array<HistoryItem<DataQuery>>; | ||||
|   eventBus?: EventBusExtended; | ||||
| } | ||||
| 
 | ||||
| export class QueryEditorRows extends PureComponent<Props> { | ||||
|  | @ -89,7 +101,7 @@ export class QueryEditorRows extends PureComponent<Props> { | |||
|   }; | ||||
| 
 | ||||
|   render() { | ||||
|     const { dsSettings, data, queries } = this.props; | ||||
|     const { dsSettings, data, queries, app, history, eventBus } = this.props; | ||||
| 
 | ||||
|     return ( | ||||
|       <DragDropContext onDragEnd={this.onDragEnd}> | ||||
|  | @ -117,6 +129,9 @@ export class QueryEditorRows extends PureComponent<Props> { | |||
|                       onAddQuery={this.props.onAddQuery} | ||||
|                       onRunQuery={this.props.onRunQueries} | ||||
|                       queries={queries} | ||||
|                       app={app} | ||||
|                       history={history} | ||||
|                       eventBus={eventBus} | ||||
|                     /> | ||||
|                   ); | ||||
|                 })} | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| import React, { PureComponent } from 'react'; | ||||
| import { css } from '@emotion/css'; | ||||
| import { ExploreQueryFieldProps } from '@grafana/data'; | ||||
| import { QueryEditorProps } from '@grafana/data'; | ||||
| import { Button, Select } from '@grafana/ui'; | ||||
| import { MetricQueryEditor, SLOQueryEditor, QueryEditorRow } from './'; | ||||
| import { CloudMonitoringQuery, MetricQuery, QueryType, SLOQuery, EditorMode } from '../types'; | ||||
|  | @ -10,7 +10,7 @@ import { defaultQuery as defaultSLOQuery } from './SLO/SLOQueryEditor'; | |||
| import { toOption } from '../functions'; | ||||
| import CloudMonitoringDatasource from '../datasource'; | ||||
| 
 | ||||
| export type Props = ExploreQueryFieldProps<CloudMonitoringDatasource, CloudMonitoringQuery>; | ||||
| export type Props = QueryEditorProps<CloudMonitoringDatasource, CloudMonitoringQuery>; | ||||
| 
 | ||||
| export class QueryEditor extends PureComponent<Props> { | ||||
|   async UNSAFE_componentWillMount() { | ||||
|  |  | |||
|  | @ -19,7 +19,7 @@ import { Editor, Node, Plugin } from 'slate'; | |||
| import syntax from '../syntax'; | ||||
| 
 | ||||
| // Types
 | ||||
| import { AbsoluteTimeRange, ExploreQueryFieldProps, SelectableValue } from '@grafana/data'; | ||||
| import { AbsoluteTimeRange, QueryEditorProps, SelectableValue } from '@grafana/data'; | ||||
| import { CloudWatchJsonData, CloudWatchLogsQuery, CloudWatchQuery } from '../types'; | ||||
| import { CloudWatchDatasource } from '../datasource'; | ||||
| import { LanguageMap, languages as prismLanguages } from 'prismjs'; | ||||
|  | @ -33,7 +33,7 @@ import { InputActionMeta } from '@grafana/ui/src/components/Select/types'; | |||
| import { getStatsGroups } from '../utils/query/getStatsGroups'; | ||||
| 
 | ||||
| export interface CloudWatchLogsQueryFieldProps | ||||
|   extends ExploreQueryFieldProps<CloudWatchDatasource, CloudWatchQuery, CloudWatchJsonData> { | ||||
|   extends QueryEditorProps<CloudWatchDatasource, CloudWatchQuery, CloudWatchJsonData> { | ||||
|   absoluteRange: AbsoluteTimeRange; | ||||
|   onLabelsRefresh?: () => void; | ||||
|   ExtraFieldElement?: ReactNode; | ||||
|  |  | |||
|  | @ -1,13 +1,13 @@ | |||
| import React, { PureComponent, ChangeEvent } from 'react'; | ||||
| 
 | ||||
| import { ExploreQueryFieldProps, PanelData } from '@grafana/data'; | ||||
| import { QueryEditorProps, PanelData } from '@grafana/data'; | ||||
| import { LegacyForms, ValidationEvents, EventsWithValidation, Icon } from '@grafana/ui'; | ||||
| const { Input, Switch } = LegacyForms; | ||||
| import { CloudWatchQuery, CloudWatchMetricsQuery, CloudWatchJsonData, ExecutedQueryPreview } from '../types'; | ||||
| import { CloudWatchDatasource } from '../datasource'; | ||||
| import { QueryField, Alias, MetricsQueryFieldsEditor } from './'; | ||||
| 
 | ||||
| export type Props = ExploreQueryFieldProps<CloudWatchDatasource, CloudWatchQuery, CloudWatchJsonData>; | ||||
| export type Props = QueryEditorProps<CloudWatchDatasource, CloudWatchQuery, CloudWatchJsonData>; | ||||
| 
 | ||||
| interface State { | ||||
|   showMeta: boolean; | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| import React, { PureComponent } from 'react'; | ||||
| import { pick } from 'lodash'; | ||||
| import { ExploreQueryFieldProps, ExploreMode } from '@grafana/data'; | ||||
| import { QueryEditorProps, ExploreMode } from '@grafana/data'; | ||||
| import { Segment } from '@grafana/ui'; | ||||
| import { CloudWatchJsonData, CloudWatchQuery } from '../types'; | ||||
| import { CloudWatchDatasource } from '../datasource'; | ||||
|  | @ -8,7 +8,7 @@ import { QueryInlineField } from './'; | |||
| import { MetricsQueryEditor } from './MetricsQueryEditor'; | ||||
| import LogsQueryEditor from './LogsQueryEditor'; | ||||
| 
 | ||||
| export type Props = ExploreQueryFieldProps<CloudWatchDatasource, CloudWatchQuery, CloudWatchJsonData>; | ||||
| export type Props = QueryEditorProps<CloudWatchDatasource, CloudWatchQuery, CloudWatchJsonData>; | ||||
| 
 | ||||
| const apiModes = { | ||||
|   Metrics: { label: 'CloudWatch Metrics', value: 'Metrics' }, | ||||
|  |  | |||
|  | @ -2,13 +2,13 @@ | |||
| import React, { memo } from 'react'; | ||||
| 
 | ||||
| // Types
 | ||||
| import { ExploreQueryFieldProps } from '@grafana/data'; | ||||
| import { QueryEditorProps } from '@grafana/data'; | ||||
| import { LokiDatasource } from '../datasource'; | ||||
| import { LokiQuery, LokiOptions } from '../types'; | ||||
| import { LokiQueryField } from './LokiQueryField'; | ||||
| import { LokiOptionFields } from './LokiOptionFields'; | ||||
| 
 | ||||
| type Props = ExploreQueryFieldProps<LokiDatasource, LokiQuery, LokiOptions>; | ||||
| type Props = QueryEditorProps<LokiDatasource, LokiQuery, LokiOptions>; | ||||
| 
 | ||||
| export function LokiExploreQueryEditor(props: Props) { | ||||
|   const { query, data, datasource, history, onChange, onRunQuery, range } = props; | ||||
|  |  | |||
|  | @ -11,10 +11,10 @@ import { | |||
| } from '@grafana/ui'; | ||||
| import { Plugin, Node } from 'slate'; | ||||
| import { LokiLabelBrowser } from './LokiLabelBrowser'; | ||||
| import { ExploreQueryFieldProps } from '@grafana/data'; | ||||
| import { QueryEditorProps } from '@grafana/data'; | ||||
| import { LokiQuery, LokiOptions } from '../types'; | ||||
| import { LanguageMap, languages as prismLanguages } from 'prismjs'; | ||||
| import LokiLanguageProvider, { LokiHistoryItem } from '../language_provider'; | ||||
| import LokiLanguageProvider from '../language_provider'; | ||||
| import { shouldRefreshLabels } from '../language_utils'; | ||||
| import LokiDatasource from '../datasource'; | ||||
| 
 | ||||
|  | @ -55,8 +55,7 @@ function willApplySuggestion(suggestion: string, { typeaheadContext, typeaheadTe | |||
|   return suggestion; | ||||
| } | ||||
| 
 | ||||
| export interface LokiQueryFieldProps extends ExploreQueryFieldProps<LokiDatasource, LokiQuery, LokiOptions> { | ||||
|   history: LokiHistoryItem[]; | ||||
| export interface LokiQueryFieldProps extends QueryEditorProps<LokiDatasource, LokiQuery, LokiOptions> { | ||||
|   ExtraFieldElement?: ReactNode; | ||||
|   placeholder?: string; | ||||
|   'data-testid'?: string; | ||||
|  |  | |||
|  | @ -36,7 +36,7 @@ import { | |||
|   lokiStreamsToDataFrames, | ||||
|   processRangeQueryResponse, | ||||
| } from './result_transformer'; | ||||
| import { addParsedLabelToQuery, getHighlighterExpressionsFromQuery, queryHasPipeParser } from './query_utils'; | ||||
| import { addParsedLabelToQuery, queryHasPipeParser } from './query_utils'; | ||||
| 
 | ||||
| import { | ||||
|   LokiOptions, | ||||
|  | @ -429,10 +429,6 @@ export class LokiDatasource extends DataSourceApi<LokiQuery, LokiOptions> { | |||
|     return { ...query, expr: expression }; | ||||
|   } | ||||
| 
 | ||||
|   getHighlighterExpression(query: LokiQuery): string[] { | ||||
|     return getHighlighterExpressionsFromQuery(query.expr); | ||||
|   } | ||||
| 
 | ||||
|   getTime(date: string | DateTime, roundUp: boolean) { | ||||
|     if (typeof date === 'string') { | ||||
|       date = dateMath.parse(date, roundUp)!; | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| import React, { memo, FC, useEffect } from 'react'; | ||||
| 
 | ||||
| // Types
 | ||||
| import { ExploreQueryFieldProps } from '@grafana/data'; | ||||
| import { QueryEditorProps } from '@grafana/data'; | ||||
| 
 | ||||
| import { PrometheusDatasource } from '../datasource'; | ||||
| import { PromQuery, PromOptions } from '../types'; | ||||
|  | @ -9,7 +9,7 @@ import { PromQuery, PromOptions } from '../types'; | |||
| import PromQueryField from './PromQueryField'; | ||||
| import { PromExploreExtraField } from './PromExploreExtraField'; | ||||
| 
 | ||||
| export type Props = ExploreQueryFieldProps<PrometheusDatasource, PromQuery, PromOptions>; | ||||
| export type Props = QueryEditorProps<PrometheusDatasource, PromQuery, PromOptions>; | ||||
| 
 | ||||
| export const PromExploreQueryEditor: FC<Props> = (props: Props) => { | ||||
|   const { range, query, data, datasource, history, onChange, onRunQuery } = props; | ||||
|  |  | |||
|  | @ -19,14 +19,7 @@ import { LanguageMap, languages as prismLanguages } from 'prismjs'; | |||
| import { PromQuery, PromOptions } from '../types'; | ||||
| import { roundMsToMin } from '../language_utils'; | ||||
| import { CancelablePromise, makePromiseCancelable } from 'app/core/utils/CancelablePromise'; | ||||
| import { | ||||
|   ExploreQueryFieldProps, | ||||
|   QueryHint, | ||||
|   isDataFrame, | ||||
|   toLegacyResponseData, | ||||
|   HistoryItem, | ||||
|   TimeRange, | ||||
| } from '@grafana/data'; | ||||
| import { QueryEditorProps, QueryHint, isDataFrame, toLegacyResponseData, TimeRange } from '@grafana/data'; | ||||
| import { PrometheusDatasource } from '../datasource'; | ||||
| import { PrometheusMetricsBrowser } from './PrometheusMetricsBrowser'; | ||||
| import { MonacoQueryFieldLazy } from './monaco-query-field/MonacoQueryFieldLazy'; | ||||
|  | @ -76,8 +69,7 @@ export function willApplySuggestion(suggestion: string, { typeaheadContext, type | |||
|   return suggestion; | ||||
| } | ||||
| 
 | ||||
| interface PromQueryFieldProps extends ExploreQueryFieldProps<PrometheusDatasource, PromQuery, PromOptions> { | ||||
|   history: Array<HistoryItem<PromQuery>>; | ||||
| interface PromQueryFieldProps extends QueryEditorProps<PrometheusDatasource, PromQuery, PromOptions> { | ||||
|   ExtraFieldElement?: ReactNode; | ||||
|   placeholder?: string; | ||||
|   'data-testid'?: string; | ||||
|  | @ -273,6 +265,7 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF | |||
|       query, | ||||
|       ExtraFieldElement, | ||||
|       placeholder = 'Enter a PromQL query (run with Shift+Enter)', | ||||
|       history = [], | ||||
|     } = this.props; | ||||
| 
 | ||||
|     const { labelBrowserVisible, syntaxLoaded, hint } = this.state; | ||||
|  | @ -302,7 +295,7 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF | |||
|             {isMonacoEditorEnabled ? ( | ||||
|               <MonacoQueryFieldLazy | ||||
|                 languageProvider={languageProvider} | ||||
|                 history={this.props.history} | ||||
|                 history={history} | ||||
|                 onChange={this.onChangeQuery} | ||||
|                 onRunQuery={this.props.onRunQuery} | ||||
|                 initialValue={query.expr ?? ''} | ||||
|  |  | |||
|  | @ -62,6 +62,9 @@ function addMetricsMetadata(metric: string, metadata?: PromMetricsMetadata): Com | |||
| 
 | ||||
| const PREFIX_DELIMITER_REGEX = /(="|!="|=~"|!~"|\{|\[|\(|\+|-|\/|\*|%|\^|\band\b|\bor\b|\bunless\b|==|>=|!=|<=|>|<|=|~|,)/; | ||||
| 
 | ||||
| interface AutocompleteContext { | ||||
|   history?: Array<HistoryItem<PromQuery>>; | ||||
| } | ||||
| export default class PromQlLanguageProvider extends LanguageProvider { | ||||
|   histogramMetrics: string[]; | ||||
|   timeRange?: { start: number; end: number }; | ||||
|  | @ -140,7 +143,7 @@ export default class PromQlLanguageProvider extends LanguageProvider { | |||
| 
 | ||||
|   provideCompletionItems = async ( | ||||
|     { prefix, text, value, labelKey, wrapperClasses }: TypeaheadInput, | ||||
|     context: { history: Array<HistoryItem<PromQuery>> } = { history: [] } | ||||
|     context: AutocompleteContext = {} | ||||
|   ): Promise<TypeaheadOutput> => { | ||||
|     const emptyResult: TypeaheadOutput = { suggestions: [] }; | ||||
| 
 | ||||
|  | @ -194,13 +197,13 @@ export default class PromQlLanguageProvider extends LanguageProvider { | |||
|     return emptyResult; | ||||
|   }; | ||||
| 
 | ||||
|   getBeginningCompletionItems = (context: { history: Array<HistoryItem<PromQuery>> }): TypeaheadOutput => { | ||||
|   getBeginningCompletionItems = (context: AutocompleteContext): TypeaheadOutput => { | ||||
|     return { | ||||
|       suggestions: [...this.getEmptyCompletionItems(context).suggestions, ...this.getTermCompletionItems().suggestions], | ||||
|     }; | ||||
|   }; | ||||
| 
 | ||||
|   getEmptyCompletionItems = (context: { history: Array<HistoryItem<PromQuery>> }): TypeaheadOutput => { | ||||
|   getEmptyCompletionItems = (context: AutocompleteContext): TypeaheadOutput => { | ||||
|     const { history } = context; | ||||
|     const suggestions: CompletionItemGroup[] = []; | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| import { css } from '@emotion/css'; | ||||
| import { DataSourceApi, ExploreQueryFieldProps, SelectableValue } from '@grafana/data'; | ||||
| import { DataSourceApi, QueryEditorProps, SelectableValue } from '@grafana/data'; | ||||
| import { config, getDataSourceSrv } from '@grafana/runtime'; | ||||
| import { | ||||
|   FileDropzone, | ||||
|  | @ -21,7 +21,7 @@ import { PrometheusDatasource } from '../prometheus/datasource'; | |||
| import useAsync from 'react-use/lib/useAsync'; | ||||
| import NativeSearch from './NativeSearch'; | ||||
| 
 | ||||
| interface Props extends ExploreQueryFieldProps<TempoDatasource, TempoQuery>, Themeable2 {} | ||||
| interface Props extends QueryEditorProps<TempoDatasource, TempoQuery>, Themeable2 {} | ||||
| 
 | ||||
| const DEFAULT_QUERY_TYPE: TempoQueryType = 'traceId'; | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| import { css } from '@emotion/css'; | ||||
| import { ExploreQueryFieldProps } from '@grafana/data'; | ||||
| import { QueryEditorProps } from '@grafana/data'; | ||||
| import { | ||||
|   ButtonCascader, | ||||
|   CascaderOption, | ||||
|  | @ -21,7 +21,7 @@ import { apiPrefix } from './constants'; | |||
| import { ZipkinDatasource } from './datasource'; | ||||
| import { ZipkinQuery, ZipkinQueryType, ZipkinSpan } from './types'; | ||||
| 
 | ||||
| type Props = ExploreQueryFieldProps<ZipkinDatasource, ZipkinQuery>; | ||||
| type Props = QueryEditorProps<ZipkinDatasource, ZipkinQuery>; | ||||
| 
 | ||||
| export const ZipkinQueryField = ({ query, onChange, onRunQuery, datasource }: Props) => { | ||||
|   const serviceOptions = useServices(datasource); | ||||
|  |  | |||
|  | @ -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
 | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue