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