Explore: Unification of logs/metrics/traces user interface (#25890)

Removes "Metrics"/"Logs" mode switcher from Explore, allowing for both
metrics and logs queries at the same time.

Co-authored-by: kay delaney <kay@grafana.com>
This commit is contained in:
Andrej Ocenas 2020-07-09 16:14:55 +02:00 committed by GitHub
parent be961c5466
commit 64bc85963b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
52 changed files with 327 additions and 737 deletions

View File

@ -545,7 +545,7 @@ describe('getLinksSupplier', () => {
expect.objectContaining({ expect.objectContaining({
title: 'testDS', title: 'testDS',
href: href:
'/explore?left={"datasource":"testDS","queries":["12345"],"mode":"Metrics","ui":{"showingGraph":true,"showingTable":true,"showingLogs":true}}', '/explore?left={"datasource":"testDS","queries":["12345"],"ui":{"showingGraph":true,"showingTable":true,"showingLogs":true}}',
onClick: undefined, onClick: undefined,
}) })
); );

View File

@ -15,7 +15,7 @@ export enum LoadingState {
Error = 'Error', Error = 'Error',
} }
export type PreferredVisualisationType = 'graph' | 'table'; export type PreferredVisualisationType = 'graph' | 'table' | 'logs' | 'trace';
export interface QueryResultMeta { export interface QueryResultMeta {
/** DatasSource Specific Values */ /** DatasSource Specific Values */
@ -47,6 +47,7 @@ export interface QueryResultMeta {
searchWords?: string[]; // used by log models and loki searchWords?: string[]; // used by log models and loki
limit?: number; // used by log models and loki limit?: number; // used by log models and loki
json?: boolean; // used to keep track of old json doc values json?: boolean; // used to keep track of old json doc values
instant?: boolean;
} }
export interface QueryResultMetaStat extends FieldConfig { export interface QueryResultMetaStat extends FieldConfig {

View File

@ -300,7 +300,6 @@ export interface QueryEditorProps<
* Contains query response filtered by refId of QueryResultBase and possible query error * Contains query response filtered by refId of QueryResultBase and possible query error
*/ */
data?: PanelData; data?: PanelData;
exploreMode?: ExploreMode;
exploreId?: any; exploreId?: any;
history?: HistoryItem[]; history?: HistoryItem[];
} }
@ -324,13 +323,11 @@ export interface ExploreQueryFieldProps<
history: any[]; history: any[];
onBlur?: () => void; onBlur?: () => void;
absoluteRange?: AbsoluteTimeRange; absoluteRange?: AbsoluteTimeRange;
exploreMode?: ExploreMode;
exploreId?: any; exploreId?: any;
} }
export interface ExploreStartPageProps { export interface ExploreStartPageProps {
datasource: DataSourceApi; datasource: DataSourceApi;
exploreMode: ExploreMode;
onClickExample: (query: DataQuery) => void; onClickExample: (query: DataQuery) => void;
exploreId?: any; exploreId?: any;
} }

View File

@ -1,4 +1,3 @@
import { ExploreMode } from './datasource';
import { RawTimeRange } from './time'; import { RawTimeRange } from './time';
import { LogsDedupStrategy } from './logs'; import { LogsDedupStrategy } from './logs';
@ -6,7 +5,6 @@ import { LogsDedupStrategy } from './logs';
export interface ExploreUrlState { export interface ExploreUrlState {
datasource: string; datasource: string;
queries: any[]; // Should be a DataQuery, but we're going to strip refIds, so typing makes less sense queries: any[]; // Should be a DataQuery, but we're going to strip refIds, so typing makes less sense
mode: ExploreMode;
range: RawTimeRange; range: RawTimeRange;
ui: ExploreUIState; ui: ExploreUIState;
originPanelId?: number; originPanelId?: number;

View File

@ -31,7 +31,7 @@ describe('mapInternalLinkToExplore', () => {
expect.objectContaining({ expect.objectContaining({
title: 'testDS', title: 'testDS',
href: href:
'/explore?left={"datasource":"testDS","queries":[{"query":"12344"}],"mode":"Metrics","ui":{"showingGraph":true,"showingTable":true,"showingLogs":true}}', '/explore?left={"datasource":"testDS","queries":[{"query":"12344"}],"ui":{"showingGraph":true,"showingTable":true,"showingLogs":true}}',
onClick: undefined, onClick: undefined,
}) })
); );

View File

@ -2,7 +2,6 @@ import {
DataLink, DataLink,
DataQuery, DataQuery,
DataSourceInstanceSettings, DataSourceInstanceSettings,
ExploreMode,
Field, Field,
InterpolateFunction, InterpolateFunction,
LinkModel, LinkModel,
@ -82,7 +81,6 @@ function generateInternalHref<T extends DataQuery = any>(datasourceName: string,
queries: [query], queries: [query],
// This should get overwritten if datasource does not support that mode and we do not know what mode is // This should get overwritten if datasource does not support that mode and we do not know what mode is
// preferred anyway. // preferred anyway.
mode: ExploreMode.Metrics,
ui: { ui: {
showingGraph: true, showingGraph: true,
showingTable: true, showingTable: true,

View File

@ -139,7 +139,6 @@ export function serializeStateToUrlParam(urlState: ExploreUrlState, compact?: bo
urlState.range.to, urlState.range.to,
urlState.datasource, urlState.datasource,
...urlState.queries, ...urlState.queries,
{ mode: urlState.mode },
{ {
ui: [ ui: [
!!urlState.ui.showingGraph, !!urlState.ui.showingGraph,

View File

@ -45,6 +45,14 @@ func (e *CloudWatchExecutor) executeLogActions(ctx context.Context, queryContext
return nil return nil
} }
if dataframe.Meta != nil {
dataframe.Meta.PreferredVisualization = "logs"
} else {
dataframe.Meta = &data.FrameMeta{
PreferredVisualization: "logs",
}
}
resultChan <- &tsdb.QueryResult{RefId: query.RefId, Dataframes: tsdb.NewDecodedDataFrames(data.Frames{dataframe})} resultChan <- &tsdb.QueryResult{RefId: query.RefId, Dataframes: tsdb.NewDecodedDataFrames(data.Frames{dataframe})}
return nil return nil
}) })

View File

@ -32,7 +32,6 @@ import {
import { getThemeColor } from 'app/core/utils/colors'; import { getThemeColor } from 'app/core/utils/colors';
import { sortInAscendingOrder, deduplicateLogRowsById } from 'app/core/utils/explore'; import { sortInAscendingOrder, deduplicateLogRowsById } from 'app/core/utils/explore';
import { getGraphSeriesModel } from 'app/plugins/panel/graph2/getGraphSeriesModel';
import { decimalSIPrefix } from '@grafana/data/src/valueFormats/symbolFormatters'; import { decimalSIPrefix } from '@grafana/data/src/valueFormats/symbolFormatters';
export const LogLevelColor = { export const LogLevelColor = {
@ -143,12 +142,15 @@ export function makeSeriesForLogs(sortedRows: LogRowModel[], bucketSize: number,
const fieldCache = new FieldCache(data); const fieldCache = new FieldCache(data);
const timeField = fieldCache.getFirstFieldOfType(FieldType.time); const timeField = fieldCache.getFirstFieldOfType(FieldType.time);
if (timeField) {
timeField.display = getDisplayProcessor({ timeField.display = getDisplayProcessor({
field: timeField, field: timeField,
timeZone, timeZone,
}); });
}
const valueField = fieldCache.getFirstFieldOfType(FieldType.number); const valueField = fieldCache.getFirstFieldOfType(FieldType.number);
if (valueField) {
valueField.config = { valueField.config = {
...valueField.config, ...valueField.config,
color: series.color, color: series.color,
@ -156,6 +158,7 @@ export function makeSeriesForLogs(sortedRows: LogRowModel[], bucketSize: number,
valueField.name = series.alias; valueField.name = series.alias;
const fieldDisplayProcessor = getDisplayProcessor({ field: valueField, timeZone }); const fieldDisplayProcessor = getDisplayProcessor({ field: valueField, timeZone });
valueField.display = (value: any) => ({ ...fieldDisplayProcessor(value), color: series.color }); valueField.display = (value: any) => ({ ...fieldDisplayProcessor(value), color: series.color });
}
const points = getFlotPairs({ const points = getFlotPairs({
xField: timeField, xField: timeField,
@ -201,11 +204,12 @@ export function dataFrameToLogsModel(
timeZone: TimeZone, timeZone: TimeZone,
absoluteRange?: AbsoluteTimeRange absoluteRange?: AbsoluteTimeRange
): LogsModel { ): LogsModel {
const { logSeries, metricSeries } = separateLogsAndMetrics(dataFrame); const { logSeries } = separateLogsAndMetrics(dataFrame);
const logsModel = logSeriesToLogsModel(logSeries); const logsModel = logSeriesToLogsModel(logSeries);
// unification: Removed logic for using metrics data in LogsModel as with the unification changes this would result
// in the incorrect data being used. Instead logs series are always derived from logs.
if (logsModel) { if (logsModel) {
if (metricSeries.length === 0) {
// Create histogram metrics from logs using the interval as bucket size for the line count // Create histogram metrics from logs using the interval as bucket size for the line count
if (intervalMs && logsModel.rows.length > 0) { if (intervalMs && logsModel.rows.length > 0) {
const sortedRows = logsModel.rows.sort(sortInAscendingOrder); const sortedRows = logsModel.rows.sort(sortInAscendingOrder);
@ -215,21 +219,6 @@ export function dataFrameToLogsModel(
} else { } else {
logsModel.series = []; logsModel.series = [];
} }
} else {
// We got metrics in the dataFrame so process those
logsModel.series = getGraphSeriesModel(
metricSeries,
timeZone,
{},
{ showBars: true, showLines: false, showPoints: false },
{
asTable: false,
isVisible: true,
placement: 'under',
}
);
}
return logsModel; return logsModel;
} }
@ -431,8 +420,8 @@ export function logSeriesToLogsModel(logSeries: DataFrame[]): LogsModel | undefi
// Stats are per query, keeping track by refId // Stats are per query, keeping track by refId
const { refId } = series; const { refId } = series;
if (refId && !queriesVisited[refId]) { if (refId && !queriesVisited[refId]) {
if (totalBytesKey && series.meta.stats) { if (totalBytesKey && series.meta?.stats) {
const byteStat = series.meta.stats.find(stat => stat.displayName === totalBytesKey); const byteStat = series.meta?.stats.find(stat => stat.displayName === totalBytesKey);
if (byteStat) { if (byteStat) {
totalBytes += byteStat.value; totalBytes += byteStat.value;
} }

View File

@ -18,7 +18,6 @@ import store from 'app/core/store';
import { import {
DataQueryError, DataQueryError,
dateTime, dateTime,
ExploreMode,
LogLevel, LogLevel,
LogRowModel, LogRowModel,
LogsDedupStrategy, LogsDedupStrategy,
@ -33,7 +32,6 @@ const DEFAULT_EXPLORE_STATE: ExploreUrlState = {
datasource: '', datasource: '',
queries: [], queries: [],
range: DEFAULT_RANGE, range: DEFAULT_RANGE,
mode: ExploreMode.Metrics,
ui: { ui: {
showingGraph: true, showingGraph: true,
showingTable: true, showingTable: true,
@ -101,7 +99,6 @@ describe('state functions', () => {
expect(serializeStateToUrlParam(state)).toBe( expect(serializeStateToUrlParam(state)).toBe(
'{"datasource":"foo","queries":[{"expr":"metric{test=\\"a/b\\"}"},' + '{"datasource":"foo","queries":[{"expr":"metric{test=\\"a/b\\"}"},' +
'{"expr":"super{foo=\\"x/z\\"}"}],"range":{"from":"now-5h","to":"now"},' + '{"expr":"super{foo=\\"x/z\\"}"}],"range":{"from":"now-5h","to":"now"},' +
'"mode":"Metrics",' +
'"ui":{"showingGraph":true,"showingTable":true,"showingLogs":true,"dedupStrategy":"none"}}' '"ui":{"showingGraph":true,"showingTable":true,"showingLogs":true,"dedupStrategy":"none"}}'
); );
}); });
@ -124,7 +121,7 @@ describe('state functions', () => {
}, },
}; };
expect(serializeStateToUrlParam(state, true)).toBe( expect(serializeStateToUrlParam(state, true)).toBe(
'["now-5h","now","foo",{"expr":"metric{test=\\"a/b\\"}"},{"expr":"super{foo=\\"x/z\\"}"},{"mode":"Metrics"},{"ui":[true,true,true,"none"]}]' '["now-5h","now","foo",{"expr":"metric{test=\\"a/b\\"}"},{"expr":"super{foo=\\"x/z\\"}"},{"ui":[true,true,true,"none"]}]'
); );
}); });
}); });

View File

@ -10,7 +10,6 @@ import {
DataSourceApi, DataSourceApi,
dateMath, dateMath,
DefaultTimeZone, DefaultTimeZone,
ExploreMode,
HistoryItem, HistoryItem,
IntervalValues, IntervalValues,
LogRowModel, LogRowModel,
@ -249,9 +248,6 @@ export function parseUrlState(initial: string | undefined): ExploreUrlState {
const metricProperties = ['expr', 'expression', 'target', 'datasource', 'query']; const metricProperties = ['expr', 'expression', 'target', 'datasource', 'query'];
const queries = parsedSegments.filter(segment => isSegment(segment, ...metricProperties)); const queries = parsedSegments.filter(segment => isSegment(segment, ...metricProperties));
const modeObj = parsedSegments.filter(segment => isSegment(segment, 'mode'))[0];
const mode = modeObj ? modeObj.mode : ExploreMode.Metrics;
const uiState = parsedSegments.filter(segment => isSegment(segment, 'ui'))[0]; const uiState = parsedSegments.filter(segment => isSegment(segment, 'ui'))[0];
const ui = uiState const ui = uiState
? { ? {
@ -263,7 +259,7 @@ export function parseUrlState(initial: string | undefined): ExploreUrlState {
: DEFAULT_UI_STATE; : DEFAULT_UI_STATE;
const originPanelId = parsedSegments.filter(segment => isSegment(segment, 'originPanelId'))[0]; const originPanelId = parsedSegments.filter(segment => isSegment(segment, 'originPanelId'))[0];
return { datasource, queries, range, ui, mode, originPanelId }; return { datasource, queries, range, ui, originPanelId };
} }
export function generateKey(index = 0): string { export function generateKey(index = 0): string {

View File

@ -2,15 +2,7 @@
import _ from 'lodash'; import _ from 'lodash';
// Services & Utils // Services & Utils
import { import { DataQuery, DataSourceApi, dateTimeFormat, AppEvents, urlUtil, ExploreUrlState } from '@grafana/data';
DataQuery,
DataSourceApi,
ExploreMode,
dateTimeFormat,
AppEvents,
urlUtil,
ExploreUrlState,
} from '@grafana/data';
import appEvents from 'app/core/app_events'; import appEvents from 'app/core/app_events';
import store from 'app/core/store'; import store from 'app/core/store';
import { SortOrder } from './explore'; import { SortOrder } from './explore';
@ -187,15 +179,6 @@ export const createUrlFromRichHistory = (query: RichHistoryQuery) => {
range: { from: 'now-1h', to: 'now' }, range: { from: 'now-1h', to: 'now' },
datasource: query.datasourceName, datasource: query.datasourceName,
queries: query.queries, queries: query.queries,
/* Default mode is metrics. Exceptions are Loki (logs) and Jaeger (tracing) data sources.
* In the future, we can remove this as we are working on metrics & logs logic.
**/
mode:
query.datasourceId === 'loki'
? ExploreMode.Logs
: query.datasourceId === 'jaeger'
? ExploreMode.Tracing
: ExploreMode.Metrics,
ui: { ui: {
showingGraph: true, showingGraph: true,
showingLogs: true, showingLogs: true,

View File

@ -1,23 +1,12 @@
import React from 'react'; import React from 'react';
import { import { DataSourceApi, LoadingState, toUtc, DataQueryError, DataQueryRequest, CoreApp } from '@grafana/data';
DataSourceApi,
LoadingState,
ExploreMode,
toUtc,
DataQueryError,
DataQueryRequest,
CoreApp,
MutableDataFrame,
} from '@grafana/data';
import { getFirstNonQueryRowSpecificError } from 'app/core/utils/explore'; import { getFirstNonQueryRowSpecificError } from 'app/core/utils/explore';
import { ExploreId } from 'app/types/explore'; import { ExploreId } from 'app/types/explore';
import { shallow } from 'enzyme'; import { shallow } from 'enzyme';
import AutoSizer from 'react-virtualized-auto-sizer';
import { Explore, ExploreProps } from './Explore'; import { Explore, ExploreProps } from './Explore';
import { scanStopAction } from './state/actionTypes'; import { scanStopAction } from './state/actionTypes';
import { toggleGraph } from './state/actions'; import { toggleGraph } from './state/actions';
import { SecondaryActions } from './SecondaryActions'; import { SecondaryActions } from './SecondaryActions';
import { TraceView } from './TraceView/TraceView';
import { getTheme } from '@grafana/ui'; import { getTheme } from '@grafana/ui';
const dummyProps: ExploreProps = { const dummyProps: ExploreProps = {
@ -64,7 +53,6 @@ const dummyProps: ExploreProps = {
to: 'now', to: 'now',
}, },
}, },
mode: ExploreMode.Metrics,
initialUI: { initialUI: {
showingTable: false, showingTable: false,
showingGraph: false, showingGraph: false,
@ -119,6 +107,10 @@ const dummyProps: ExploreProps = {
originPanelId: 1, originPanelId: 1,
addQueryRow: jest.fn(), addQueryRow: jest.fn(),
theme: getTheme(), theme: getTheme(),
showMetrics: true,
showLogs: true,
showTable: true,
showTrace: true,
}; };
const setupErrors = (hasRefId?: boolean) => { const setupErrors = (hasRefId?: boolean) => {
@ -144,34 +136,6 @@ describe('Explore', () => {
expect(wrapper.find(SecondaryActions).props().addQueryRowButtonHidden).toBe(false); expect(wrapper.find(SecondaryActions).props().addQueryRowButtonHidden).toBe(false);
}); });
it('does not show add row button if mode is tracing', () => {
const wrapper = shallow(<Explore {...{ ...dummyProps, mode: ExploreMode.Tracing }} />);
expect(wrapper.find(SecondaryActions).props().addQueryRowButtonHidden).toBe(true);
});
it('renders TraceView if tracing mode', () => {
const wrapper = shallow(
<Explore
{...{
...dummyProps,
mode: ExploreMode.Tracing,
queryResponse: {
...dummyProps.queryResponse,
state: LoadingState.Done,
series: [new MutableDataFrame({ fields: [{ name: 'trace', values: [{}] }] })],
},
}}
/>
);
const autoSizer = shallow(
wrapper
.find(AutoSizer)
.props()
.children({ width: 100, height: 100 }) as React.ReactElement
);
expect(autoSizer.find(TraceView).length).toBe(1);
});
it('should filter out a query-row-specific error when looking for non-query-row-specific errors', async () => { it('should filter out a query-row-specific error when looking for non-query-row-specific errors', async () => {
const queryErrors = setupErrors(true); const queryErrors = setupErrors(true);
const queryError = getFirstNonQueryRowSpecificError(queryErrors); const queryError = getFirstNonQueryRowSpecificError(queryErrors);

View File

@ -11,7 +11,6 @@ import {
AbsoluteTimeRange, AbsoluteTimeRange,
DataQuery, DataQuery,
DataSourceApi, DataSourceApi,
ExploreMode,
GrafanaTheme, GrafanaTheme,
GraphSeriesXY, GraphSeriesXY,
LoadingState, LoadingState,
@ -21,6 +20,7 @@ import {
TimeZone, TimeZone,
ExploreUIState, ExploreUIState,
ExploreUrlState, ExploreUrlState,
LogsModel,
} from '@grafana/data'; } from '@grafana/data';
import store from 'app/core/store'; import store from 'app/core/store';
@ -58,6 +58,7 @@ import { getTimeZone } from '../profile/state/selectors';
import { ErrorContainer } from './ErrorContainer'; import { ErrorContainer } from './ErrorContainer';
import { scanStopAction } from './state/actionTypes'; import { scanStopAction } from './state/actionTypes';
import { ExploreGraphPanel } from './ExploreGraphPanel'; import { ExploreGraphPanel } from './ExploreGraphPanel';
//TODO:unification
import { TraceView } from './TraceView/TraceView'; import { TraceView } from './TraceView/TraceView';
import { SecondaryActions } from './SecondaryActions'; import { SecondaryActions } from './SecondaryActions';
import { FILTER_FOR_OPERATOR, FILTER_OUT_OPERATOR, FilterItem } from '@grafana/ui/src/components/Table/types'; import { FILTER_FOR_OPERATOR, FILTER_OUT_OPERATOR, FilterItem } from '@grafana/ui/src/components/Table/types';
@ -104,12 +105,12 @@ export interface ExploreProps {
initialDatasource: string; initialDatasource: string;
initialQueries: DataQuery[]; initialQueries: DataQuery[];
initialRange: TimeRange; initialRange: TimeRange;
mode: ExploreMode;
initialUI: ExploreUIState; initialUI: ExploreUIState;
isLive: boolean; isLive: boolean;
syncedTimes: boolean; syncedTimes: boolean;
updateTimeRange: typeof updateTimeRange; updateTimeRange: typeof updateTimeRange;
graphResult?: GraphSeriesXY[] | null; graphResult?: GraphSeriesXY[] | null;
logsResult?: LogsModel;
loading?: boolean; loading?: boolean;
absoluteRange: AbsoluteTimeRange; absoluteRange: AbsoluteTimeRange;
showingGraph?: boolean; showingGraph?: boolean;
@ -121,6 +122,10 @@ export interface ExploreProps {
originPanelId: number; originPanelId: number;
addQueryRow: typeof addQueryRow; addQueryRow: typeof addQueryRow;
theme: GrafanaTheme; theme: GrafanaTheme;
showMetrics: boolean;
showTable: boolean;
showLogs: boolean;
showTrace: boolean;
} }
interface ExploreState { interface ExploreState {
@ -170,7 +175,6 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
initialDatasource, initialDatasource,
initialQueries, initialQueries,
initialRange, initialRange,
mode,
initialUI, initialUI,
originPanelId, originPanelId,
} = this.props; } = this.props;
@ -183,7 +187,6 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
initialDatasource, initialDatasource,
initialQueries, initialQueries,
initialRange, initialRange,
mode,
width, width,
this.exploreEvents, this.exploreEvents,
initialUI, initialUI,
@ -301,7 +304,6 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
exploreId, exploreId,
split, split,
queryKeys, queryKeys,
mode,
graphResult, graphResult,
loading, loading,
absoluteRange, absoluteRange,
@ -312,6 +314,10 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
syncedTimes, syncedTimes,
isLive, isLive,
theme, theme,
showMetrics,
showTable,
showLogs,
showTrace,
} = this.props; } = this.props;
const { showRichHistory } = this.state; const { showRichHistory } = this.state;
const exploreClass = split ? 'explore explore-split' : 'explore'; const exploreClass = split ? 'explore explore-split' : 'explore';
@ -334,7 +340,8 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
<SecondaryActions <SecondaryActions
addQueryRowButtonDisabled={isLive} addQueryRowButtonDisabled={isLive}
// We cannot show multiple traces at the same time right now so we do not show add query button. // We cannot show multiple traces at the same time right now so we do not show add query button.
addQueryRowButtonHidden={mode === ExploreMode.Tracing} //TODO:unification
addQueryRowButtonHidden={false}
richHistoryButtonActive={showRichHistory} richHistoryButtonActive={showRichHistory}
onClickAddQueryRowButton={this.onClickAddQueryRowButton} onClickAddQueryRowButton={this.onClickAddQueryRowButton}
onClickRichHistoryButton={this.toggleShowRichHistory} onClickRichHistoryButton={this.toggleShowRichHistory}
@ -355,14 +362,13 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
<StartPage <StartPage
onClickExample={this.onClickExample} onClickExample={this.onClickExample}
datasource={datasourceInstance} datasource={datasourceInstance}
exploreMode={mode}
exploreId={exploreId} exploreId={exploreId}
/> />
</div> </div>
)} )}
{!showStartPage && ( {!showStartPage && (
<> <>
{mode === ExploreMode.Metrics && ( {showMetrics && (
<ExploreGraphPanel <ExploreGraphPanel
series={graphResult} series={graphResult}
width={width} width={width}
@ -379,7 +385,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
showLines={true} showLines={true}
/> />
)} )}
{mode === ExploreMode.Metrics && ( {showTable && (
<TableContainer <TableContainer
width={width} width={width}
exploreId={exploreId} exploreId={exploreId}
@ -388,7 +394,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
} }
/> />
)} )}
{mode === ExploreMode.Logs && ( {showLogs && (
<LogsContainer <LogsContainer
width={width} width={width}
exploreId={exploreId} exploreId={exploreId}
@ -399,7 +405,8 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
onStopScanning={this.onStopScanning} onStopScanning={this.onStopScanning}
/> />
)} )}
{mode === ExploreMode.Tracing && {/* TODO:unification */}
{showTrace &&
// We expect only one trace at the moment to be in the dataframe // We expect only one trace at the moment to be in the dataframe
// If there is not data (like 404) we show a separate error so no need to show anything here // If there is not data (like 404) we show a separate error so no need to show anything here
queryResponse.series[0] && ( queryResponse.series[0] && (
@ -442,9 +449,12 @@ function mapStateToProps(state: StoreState, { exploreId }: ExploreProps): Partia
urlState, urlState,
update, update,
isLive, isLive,
supportedModes,
mode,
graphResult, graphResult,
logsResult,
showLogs,
showMetrics,
showTable,
showTrace,
loading, loading,
showingGraph, showingGraph,
showingTable, showingTable,
@ -452,31 +462,13 @@ function mapStateToProps(state: StoreState, { exploreId }: ExploreProps): Partia
queryResponse, queryResponse,
} = item; } = item;
const { datasource, queries, range: urlRange, mode: urlMode, ui, originPanelId } = (urlState || const { datasource, queries, range: urlRange, ui, originPanelId } = (urlState || {}) as ExploreUrlState;
{}) as ExploreUrlState;
const initialDatasource = datasource || store.get(lastUsedDatasourceKeyForOrgId(state.user.orgId)); const initialDatasource = datasource || store.get(lastUsedDatasourceKeyForOrgId(state.user.orgId));
const initialQueries: DataQuery[] = ensureQueriesMemoized(queries); const initialQueries: DataQuery[] = ensureQueriesMemoized(queries);
const initialRange = urlRange const initialRange = urlRange
? getTimeRangeFromUrlMemoized(urlRange, timeZone) ? getTimeRangeFromUrlMemoized(urlRange, timeZone)
: getTimeRange(timeZone, DEFAULT_RANGE); : getTimeRange(timeZone, DEFAULT_RANGE);
let newMode: ExploreMode | undefined;
if (supportedModes.length) {
const urlModeIsValid = supportedModes.includes(urlMode);
const modeStateIsValid = supportedModes.includes(mode);
if (modeStateIsValid) {
newMode = mode;
} else if (urlModeIsValid) {
newMode = urlMode;
} else {
newMode = supportedModes[0];
}
} else {
newMode = [ExploreMode.Metrics, ExploreMode.Logs, ExploreMode.Tracing].includes(urlMode) ? urlMode : undefined;
}
const initialUI = ui || DEFAULT_UI_STATE; const initialUI = ui || DEFAULT_UI_STATE;
return { return {
@ -489,10 +481,10 @@ function mapStateToProps(state: StoreState, { exploreId }: ExploreProps): Partia
initialDatasource, initialDatasource,
initialQueries, initialQueries,
initialRange, initialRange,
mode: newMode,
initialUI, initialUI,
isLive, isLive,
graphResult, graphResult: graphResult ?? undefined,
logsResult: logsResult ?? undefined,
loading, loading,
showingGraph, showingGraph,
showingTable, showingTable,
@ -501,6 +493,10 @@ function mapStateToProps(state: StoreState, { exploreId }: ExploreProps): Partia
originPanelId, originPanelId,
syncedTimes, syncedTimes,
timeZone, timeZone,
showLogs,
showMetrics,
showTable,
showTrace,
}; };
} }

View File

@ -1,69 +0,0 @@
import React from 'react';
import { shallow, ShallowWrapper } from 'enzyme';
import { UnConnectedExploreToolbar } from './ExploreToolbar';
import { ExploreMode } from '@grafana/data';
import { ExploreId } from '../../types';
import { ToggleButtonGroup } from '@grafana/ui';
jest.mock('./state/selectors', () => {
return {
__esModule: true,
getExploreDatasources: () => [] as any,
};
});
describe('ExploreToolbar', () => {
it('displays correct modes', () => {
let wrapper = shallow(createToolbar([ExploreMode.Tracing, ExploreMode.Logs]));
checkModes(wrapper, ['Logs', 'Tracing']);
wrapper = shallow(createToolbar([ExploreMode.Logs]));
checkModes(wrapper, []);
wrapper = shallow(createToolbar([ExploreMode.Logs, ExploreMode.Tracing, ExploreMode.Metrics]));
checkModes(wrapper, ['Metrics', 'Logs', 'Tracing']);
});
});
function checkModes(wrapper: ShallowWrapper, modes: string[]) {
expect(
wrapper
.find(ToggleButtonGroup)
.children()
.map(node => node.children().text())
).toEqual(modes);
}
function createToolbar(supportedModes: ExploreMode[]) {
return (
<UnConnectedExploreToolbar
datasourceMissing={false}
loading={false}
range={{} as any}
timeZone={'UTC'}
splitted={false}
syncedTimes={false}
supportedModes={supportedModes}
selectedMode={ExploreMode.Tracing}
hasLiveOption={false}
isLive={false}
isPaused={false}
queries={[]}
containerWidth={0}
changeDatasource={(() => {}) as any}
clearAll={(() => {}) as any}
cancelQueries={(() => {}) as any}
runQueries={(() => {}) as any}
closeSplit={(() => {}) as any}
split={(() => {}) as any}
syncTimes={(() => {}) as any}
changeRefreshInterval={(() => {}) as any}
changeMode={(() => {}) as any}
updateLocation={(() => {}) as any}
setDashboardQueriesToUpdateOnLoad={(() => {}) as any}
exploreId={ExploreId.left}
onChangeTime={(() => {}) as any}
onChangeTimeZone={(() => {}) as any}
/>
);
}

View File

@ -6,14 +6,13 @@ import classNames from 'classnames';
import { css } from 'emotion'; import { css } from 'emotion';
import { ExploreId, ExploreItemState } from 'app/types/explore'; import { ExploreId, ExploreItemState } from 'app/types/explore';
import { Icon, IconButton, LegacyForms, SetInterval, ToggleButton, ToggleButtonGroup, Tooltip } from '@grafana/ui'; import { Icon, IconButton, LegacyForms, SetInterval, Tooltip } from '@grafana/ui';
import { DataQuery, ExploreMode, RawTimeRange, TimeRange, TimeZone } from '@grafana/data'; import { DataQuery, RawTimeRange, TimeRange, TimeZone } from '@grafana/data';
import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker'; import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker';
import { StoreState } from 'app/types/store'; import { StoreState } from 'app/types/store';
import { import {
cancelQueries, cancelQueries,
changeDatasource, changeDatasource,
changeMode,
changeRefreshInterval, changeRefreshInterval,
clearQueries, clearQueries,
runQueries, runQueries,
@ -60,8 +59,6 @@ interface StateProps {
splitted: boolean; splitted: boolean;
syncedTimes: boolean; syncedTimes: boolean;
refreshInterval?: string; refreshInterval?: string;
supportedModes: ExploreMode[];
selectedMode: ExploreMode;
hasLiveOption: boolean; hasLiveOption: boolean;
isLive: boolean; isLive: boolean;
isPaused: boolean; isPaused: boolean;
@ -81,7 +78,6 @@ interface DispatchProps {
split: typeof splitOpen; split: typeof splitOpen;
syncTimes: typeof syncTimes; syncTimes: typeof syncTimes;
changeRefreshInterval: typeof changeRefreshInterval; changeRefreshInterval: typeof changeRefreshInterval;
changeMode: typeof changeMode;
updateLocation: typeof updateLocation; updateLocation: typeof updateLocation;
setDashboardQueriesToUpdateOnLoad: typeof setDashboardQueriesToUpdateOnLoad; setDashboardQueriesToUpdateOnLoad: typeof setDashboardQueriesToUpdateOnLoad;
onChangeTimeZone: typeof updateTimeZoneForSession; onChangeTimeZone: typeof updateTimeZoneForSession;
@ -111,11 +107,6 @@ export class UnConnectedExploreToolbar extends PureComponent<Props> {
changeRefreshInterval(exploreId, item); changeRefreshInterval(exploreId, item);
}; };
onModeChange = (mode: ExploreMode) => {
const { changeMode, exploreId } = this.props;
changeMode(exploreId, mode);
};
onChangeTimeSync = () => { onChangeTimeSync = () => {
const { syncTimes, exploreId } = this.props; const { syncTimes, exploreId } = this.props;
syncTimes(exploreId); syncTimes(exploreId);
@ -174,8 +165,6 @@ export class UnConnectedExploreToolbar extends PureComponent<Props> {
refreshInterval, refreshInterval,
onChangeTime, onChangeTime,
split, split,
supportedModes,
selectedMode,
hasLiveOption, hasLiveOption,
isLive, isLive,
isPaused, isPaused,
@ -195,8 +184,6 @@ export class UnConnectedExploreToolbar extends PureComponent<Props> {
const showSmallDataSourcePicker = (splitted ? containerWidth < 700 : containerWidth < 800) || false; const showSmallDataSourcePicker = (splitted ? containerWidth < 700 : containerWidth < 800) || false;
const showSmallTimePicker = splitted || containerWidth < 1210; const showSmallTimePicker = splitted || containerWidth < 1210;
const showModeToggle = supportedModes.length > 1;
return ( return (
<div className={splitted ? 'explore-toolbar splitted' : 'explore-toolbar'}> <div className={splitted ? 'explore-toolbar splitted' : 'explore-toolbar'}>
<div className="explore-toolbar-item"> <div className="explore-toolbar-item">
@ -239,26 +226,6 @@ export class UnConnectedExploreToolbar extends PureComponent<Props> {
hideTextValue={showSmallDataSourcePicker} hideTextValue={showSmallDataSourcePicker}
/> />
</div> </div>
{showModeToggle ? (
<div className="query-type-toggle">
<ToggleButtonGroup label="" transparent={true}>
{[ExploreMode.Metrics, ExploreMode.Logs, ExploreMode.Tracing]
.filter(mode => supportedModes.includes(mode))
.map(mode => {
return (
<ToggleButton
key={mode}
value={mode}
onChange={this.onModeChange}
selected={selectedMode === mode}
>
{mode}
</ToggleButton>
);
})}
</ToggleButtonGroup>
</div>
) : null}
</div> </div>
) : null} ) : null}
@ -369,8 +336,6 @@ const mapStateToProps = (state: StoreState, { exploreId }: OwnProps): StateProps
range, range,
refreshInterval, refreshInterval,
loading, loading,
supportedModes,
mode,
isLive, isLive,
isPaused, isPaused,
originPanelId, originPanelId,
@ -379,7 +344,7 @@ const mapStateToProps = (state: StoreState, { exploreId }: OwnProps): StateProps
containerWidth, containerWidth,
} = exploreItem; } = exploreItem;
const hasLiveOption = !!(datasourceInstance?.meta?.streaming && mode === ExploreMode.Logs); const hasLiveOption = !!datasourceInstance?.meta?.streaming;
return { return {
datasourceMissing, datasourceMissing,
@ -389,15 +354,13 @@ const mapStateToProps = (state: StoreState, { exploreId }: OwnProps): StateProps
timeZone: getTimeZone(state.user), timeZone: getTimeZone(state.user),
splitted, splitted,
refreshInterval, refreshInterval,
supportedModes,
selectedMode: supportedModes.includes(mode) ? mode : supportedModes[0],
hasLiveOption, hasLiveOption,
isLive, isLive,
isPaused, isPaused,
originPanelId, originPanelId,
queries, queries,
syncedTimes, syncedTimes,
datasourceLoading, datasourceLoading: datasourceLoading ?? undefined,
containerWidth, containerWidth,
}; };
}; };
@ -412,7 +375,6 @@ const mapDispatchToProps: DispatchProps = {
closeSplit: splitClose, closeSplit: splitClose,
split: splitOpen, split: splitOpen,
syncTimes, syncTimes,
changeMode: changeMode,
setDashboardQueriesToUpdateOnLoad, setDashboardQueriesToUpdateOnLoad,
onChangeTimeZone: updateTimeZoneForSession, onChangeTimeZone: updateTimeZoneForSession,
}; };

View File

@ -62,7 +62,15 @@ interface LogsContainerProps {
splitOpen: typeof splitOpen; splitOpen: typeof splitOpen;
} }
export class LogsContainer extends PureComponent<LogsContainerProps> { interface LogsContainerState {
logsContainerOpen: boolean;
}
export class LogsContainer extends PureComponent<LogsContainerProps, LogsContainerState> {
state: LogsContainerState = {
logsContainerOpen: true,
};
onChangeTime = (absoluteRange: AbsoluteTimeRange) => { onChangeTime = (absoluteRange: AbsoluteTimeRange) => {
const { exploreId, updateTimeRange } = this.props; const { exploreId, updateTimeRange } = this.props;
updateTimeRange({ exploreId, absoluteRange }); updateTimeRange({ exploreId, absoluteRange });
@ -94,6 +102,12 @@ export class LogsContainer extends PureComponent<LogsContainerProps> {
return getFieldLinksForExplore(field, rowIndex, this.props.splitOpen, this.props.range); return getFieldLinksForExplore(field, rowIndex, this.props.splitOpen, this.props.range);
}; };
onToggleCollapse = () => {
this.setState(state => ({
logsContainerOpen: !state.logsContainerOpen,
}));
};
render() { render() {
const { const {
loading, loading,
@ -116,6 +130,8 @@ export class LogsContainer extends PureComponent<LogsContainerProps> {
exploreId, exploreId,
} = this.props; } = this.props;
const { logsContainerOpen } = this.state;
return ( return (
<> <>
<LogsCrossFadeTransition visible={isLive}> <LogsCrossFadeTransition visible={isLive}>
@ -135,7 +151,13 @@ export class LogsContainer extends PureComponent<LogsContainerProps> {
</Collapse> </Collapse>
</LogsCrossFadeTransition> </LogsCrossFadeTransition>
<LogsCrossFadeTransition visible={!isLive}> <LogsCrossFadeTransition visible={!isLive}>
<Collapse label="Logs" loading={loading} isOpen> <Collapse
label="Logs"
loading={loading}
isOpen={logsContainerOpen}
onToggle={this.onToggleCollapse}
collapsible
>
<Logs <Logs
dedupStrategy={this.props.dedupStrategy || LogsDedupStrategy.none} dedupStrategy={this.props.dedupStrategy || LogsDedupStrategy.none}
logRows={logRows} logRows={logRows}

View File

@ -3,7 +3,7 @@ import { QueryRow, QueryRowProps } from './QueryRow';
import { shallow } from 'enzyme'; import { shallow } from 'enzyme';
import { ExploreId } from 'app/types/explore'; import { ExploreId } from 'app/types/explore';
import { Emitter } from 'app/core/utils/emitter'; import { Emitter } from 'app/core/utils/emitter';
import { DataSourceApi, TimeRange, AbsoluteTimeRange, ExploreMode, PanelData } from '@grafana/data'; import { DataSourceApi, TimeRange, AbsoluteTimeRange, PanelData } from '@grafana/data';
const setup = (propOverrides?: object) => { const setup = (propOverrides?: object) => {
const props: QueryRowProps = { const props: QueryRowProps = {
@ -23,7 +23,6 @@ const setup = (propOverrides?: object) => {
removeQueryRowAction: jest.fn() as any, removeQueryRowAction: jest.fn() as any,
runQueries: jest.fn(), runQueries: jest.fn(),
queryResponse: {} as PanelData, queryResponse: {} as PanelData,
mode: ExploreMode.Metrics,
latency: 1, latency: 1,
}; };
@ -33,34 +32,9 @@ const setup = (propOverrides?: object) => {
return wrapper; return wrapper;
}; };
const ExploreMetricsQueryField = () => <div />;
const ExploreLogsQueryField = () => <div />;
const ExploreQueryField = () => <div />;
const QueryEditor = () => <div />; const QueryEditor = () => <div />;
describe('QueryRow', () => { describe('QueryRow', () => {
describe('if datasource has all query field components ', () => {
const allComponents = {
ExploreMetricsQueryField,
ExploreLogsQueryField,
ExploreQueryField,
QueryEditor,
};
it('it should render ExploreMetricsQueryField in metrics mode', () => {
const wrapper = setup({ mode: ExploreMode.Metrics, datasourceInstance: { components: allComponents } });
expect(wrapper.find(ExploreMetricsQueryField)).toHaveLength(1);
});
it('it should render ExploreLogsQueryField in logs mode', () => {
const wrapper = setup({ mode: ExploreMode.Logs, datasourceInstance: { components: allComponents } });
expect(wrapper.find(ExploreLogsQueryField)).toHaveLength(1);
});
it('it should render ExploreQueryField in tracing mode', () => {
const wrapper = setup({ mode: ExploreMode.Tracing, datasourceInstance: { components: allComponents } });
expect(wrapper.find(ExploreQueryField)).toHaveLength(1);
});
});
describe('if datasource does not have Explore query fields ', () => { describe('if datasource does not have Explore query fields ', () => {
it('it should render QueryEditor if datasource has it', () => { it('it should render QueryEditor if datasource has it', () => {
const wrapper = setup({ datasourceInstance: { components: { QueryEditor } } }); const wrapper = setup({ datasourceInstance: { components: { QueryEditor } } });

View File

@ -20,7 +20,6 @@ import {
TimeRange, TimeRange,
AbsoluteTimeRange, AbsoluteTimeRange,
LoadingState, LoadingState,
ExploreMode,
} from '@grafana/data'; } from '@grafana/data';
import { ExploreItemState, ExploreId } from 'app/types/explore'; import { ExploreItemState, ExploreId } from 'app/types/explore';
@ -48,7 +47,6 @@ export interface QueryRowProps extends PropsFromParent {
removeQueryRowAction: typeof removeQueryRowAction; removeQueryRowAction: typeof removeQueryRowAction;
runQueries: typeof runQueries; runQueries: typeof runQueries;
queryResponse: PanelData; queryResponse: PanelData;
mode: ExploreMode;
latency: number; latency: number;
} }
@ -102,12 +100,13 @@ export class QueryRow extends PureComponent<QueryRowProps, QueryRowState> {
}; };
setReactQueryEditor = () => { setReactQueryEditor = () => {
const { mode, datasourceInstance } = this.props; const { datasourceInstance } = this.props;
let QueryEditor; let QueryEditor;
if (mode === ExploreMode.Metrics && datasourceInstance.components?.ExploreMetricsQueryField) { // TODO:unification
if (datasourceInstance.components?.ExploreMetricsQueryField) {
QueryEditor = datasourceInstance.components.ExploreMetricsQueryField; QueryEditor = datasourceInstance.components.ExploreMetricsQueryField;
} else if (mode === ExploreMode.Logs && datasourceInstance.components?.ExploreLogsQueryField) { } else if (datasourceInstance.components?.ExploreLogsQueryField) {
QueryEditor = datasourceInstance.components.ExploreLogsQueryField; QueryEditor = datasourceInstance.components.ExploreLogsQueryField;
} else if (datasourceInstance.components?.ExploreQueryField) { } else if (datasourceInstance.components?.ExploreQueryField) {
QueryEditor = datasourceInstance.components.ExploreQueryField; QueryEditor = datasourceInstance.components.ExploreQueryField;
@ -126,7 +125,6 @@ export class QueryRow extends PureComponent<QueryRowProps, QueryRowState> {
range, range,
absoluteRange, absoluteRange,
queryResponse, queryResponse,
mode,
exploreId, exploreId,
} = this.props; } = this.props;
@ -145,7 +143,6 @@ export class QueryRow extends PureComponent<QueryRowProps, QueryRowState> {
onChange={this.onChange} onChange={this.onChange}
data={queryResponse} data={queryResponse}
absoluteRange={absoluteRange} absoluteRange={absoluteRange}
exploreMode={mode}
exploreId={exploreId} exploreId={exploreId}
/> />
); );
@ -174,10 +171,9 @@ export class QueryRow extends PureComponent<QueryRowProps, QueryRowState> {
}, 500); }, 500);
render() { render() {
const { datasourceInstance, query, queryResponse, mode, latency } = this.props; const { datasourceInstance, query, queryResponse, latency } = this.props;
const canToggleEditorModes = const canToggleEditorModes = has(datasourceInstance, 'components.QueryCtrl.prototype.toggleEditorMode');
mode === ExploreMode.Metrics && has(datasourceInstance, 'components.QueryCtrl.prototype.toggleEditorMode');
const isNotStarted = queryResponse.state === LoadingState.NotStarted; const isNotStarted = queryResponse.state === LoadingState.NotStarted;
const queryErrors = queryResponse.error && queryResponse.error.refId === query.refId ? [queryResponse.error] : []; const queryErrors = queryResponse.error && queryResponse.error.refId === query.refId ? [queryResponse.error] : [];
@ -204,7 +200,7 @@ export class QueryRow extends PureComponent<QueryRowProps, QueryRowState> {
function mapStateToProps(state: StoreState, { exploreId, index }: QueryRowProps) { function mapStateToProps(state: StoreState, { exploreId, index }: QueryRowProps) {
const explore = state.explore; const explore = state.explore;
const item: ExploreItemState = explore[exploreId]; const item: ExploreItemState = explore[exploreId];
const { datasourceInstance, history, queries, range, absoluteRange, mode, queryResponse, latency } = item; const { datasourceInstance, history, queries, range, absoluteRange, queryResponse, latency } = item;
const query = queries[index]; const query = queries[index];
return { return {
@ -214,7 +210,6 @@ function mapStateToProps(state: StoreState, { exploreId, index }: QueryRowProps)
range, range,
absoluteRange, absoluteRange,
queryResponse, queryResponse,
mode,
latency, latency,
}; };
} }

View File

@ -13,7 +13,6 @@ import {
PanelData, PanelData,
QueryFixAction, QueryFixAction,
TimeRange, TimeRange,
ExploreMode,
ExploreUIState, ExploreUIState,
} from '@grafana/data'; } from '@grafana/data';
import { ExploreId, ExploreItemState } from 'app/types/explore'; import { ExploreId, ExploreItemState } from 'app/types/explore';
@ -24,11 +23,6 @@ export interface AddQueryRowPayload {
query: DataQuery; query: DataQuery;
} }
export interface ChangeModePayload {
exploreId: ExploreId;
mode: ExploreMode;
}
export interface ChangeQueryPayload { export interface ChangeQueryPayload {
exploreId: ExploreId; exploreId: ExploreId;
query: DataQuery; query: DataQuery;
@ -62,7 +56,6 @@ export interface InitializeExplorePayload {
eventBridge: Emitter; eventBridge: Emitter;
queries: DataQuery[]; queries: DataQuery[];
range: TimeRange; range: TimeRange;
mode: ExploreMode;
ui: ExploreUIState; ui: ExploreUIState;
originPanelId?: number | null; originPanelId?: number | null;
} }
@ -149,7 +142,6 @@ export interface UpdateDatasourceInstancePayload {
exploreId: ExploreId; exploreId: ExploreId;
datasourceInstance: DataSourceApi; datasourceInstance: DataSourceApi;
version?: string; version?: string;
mode?: ExploreMode;
} }
export interface ToggleLogLevelPayload { export interface ToggleLogLevelPayload {
@ -191,11 +183,6 @@ export interface ResetExplorePayload {
*/ */
export const addQueryRowAction = createAction<AddQueryRowPayload>('explore/addQueryRow'); export const addQueryRowAction = createAction<AddQueryRowPayload>('explore/addQueryRow');
/**
* Change the mode of Explore.
*/
export const changeModeAction = createAction<ChangeModePayload>('explore/changeMode');
/** /**
* Query change handler for the query row with the given index. * Query change handler for the query row with the given index.
* If `override` is reset the query modifications and run the queries. Use this to set queries via a link. * If `override` is reset the query modifications and run the queries. Use this to set queries via a link.

View File

@ -1,27 +1,17 @@
import { PayloadAction } from '@reduxjs/toolkit'; import { PayloadAction } from '@reduxjs/toolkit';
import { DataQuery, DefaultTimeZone, ExploreMode, LogsDedupStrategy, toUtc, ExploreUrlState } from '@grafana/data'; import { DataQuery, DefaultTimeZone, LogsDedupStrategy, toUtc, ExploreUrlState } from '@grafana/data';
import * as Actions from './actions'; import { cancelQueries, loadDatasource, navigateToExplore, refreshExplore } from './actions';
import {
cancelQueries,
changeDatasource,
changeMode,
loadDatasource,
navigateToExplore,
refreshExplore,
} from './actions';
import { ExploreId, ExploreUpdateState } from 'app/types'; import { ExploreId, ExploreUpdateState } from 'app/types';
import { thunkTester } from 'test/core/thunk/thunkTester'; import { thunkTester } from 'test/core/thunk/thunkTester';
import { import {
cancelQueriesAction, cancelQueriesAction,
changeModeAction,
initializeExploreAction, initializeExploreAction,
InitializeExplorePayload, InitializeExplorePayload,
loadDatasourcePendingAction, loadDatasourcePendingAction,
loadDatasourceReadyAction, loadDatasourceReadyAction,
scanStopAction, scanStopAction,
setQueriesAction, setQueriesAction,
updateDatasourceInstanceAction,
updateUIStateAction, updateUIStateAction,
} from './actionTypes'; } from './actionTypes';
import { Emitter } from 'app/core/core'; import { Emitter } from 'app/core/core';
@ -80,7 +70,6 @@ const setup = (updateOverides?: Partial<ExploreUpdateState>) => {
datasource: 'some-datasource', datasource: 'some-datasource',
queries: [], queries: [],
range: range.raw, range: range.raw,
mode: ExploreMode.Metrics,
ui, ui,
}; };
const updateDefaults = makeInitialUpdateState(); const updateDefaults = makeInitialUpdateState();
@ -219,64 +208,6 @@ describe('running queries', () => {
}); });
}); });
describe('changing datasource', () => {
it('should switch to logs mode when changing from prometheus to loki', async () => {
const lokiMock = {
testDatasource: () => Promise.resolve({ status: 'success' }),
name: 'Loki',
init: jest.fn(),
meta: { id: 'some id', name: 'Loki' },
};
getDatasourceSrvMock.mockImplementation(
() =>
({
getExternal: jest.fn().mockReturnValue([]),
get: jest.fn().mockReturnValue(lokiMock),
} as any)
);
const exploreId = ExploreId.left;
const name = 'Prometheus';
const mockPromDatasourceInstance = {
testDatasource: () => Promise.resolve({ status: 'success' }),
name,
init: jest.fn(),
meta: { id: 'some id', name },
};
const initialState = {
explore: {
[exploreId]: {
requestedDatasourceName: 'Loki',
datasourceInstance: mockPromDatasourceInstance,
},
},
user: {
orgId: 1,
},
};
jest.spyOn(Actions, 'importQueries').mockImplementationOnce(() => jest.fn);
jest.spyOn(Actions, 'loadDatasource').mockImplementationOnce(() => jest.fn);
const runQueriesAction = jest.spyOn(Actions, 'runQueries').mockImplementationOnce(() => jest.fn);
const dispatchedActions = await thunkTester(initialState)
.givenThunk(changeDatasource)
.whenThunkIsDispatched(exploreId, name);
expect(dispatchedActions).toEqual([
updateDatasourceInstanceAction({
exploreId,
datasourceInstance: lokiMock as any,
version: undefined,
mode: ExploreMode.Logs,
}),
]);
// Don't run queries just on datasource change
expect(runQueriesAction).toHaveBeenCalledTimes(0);
});
});
describe('loading datasource', () => { describe('loading datasource', () => {
describe('when loadDatasource thunk is dispatched', () => { describe('when loadDatasource thunk is dispatched', () => {
describe('and all goes fine', () => { describe('and all goes fine', () => {
@ -336,28 +267,6 @@ describe('loading datasource', () => {
}); });
}); });
describe('changing mode', () => {
it('should trigger changeModeAction and updateLocation', async () => {
const { exploreId, initialState, range } = setup();
const dispatchedActions = await thunkTester(initialState)
.givenThunk(changeMode)
.whenThunkIsDispatched(exploreId, ExploreMode.Logs);
const rawTimeRange = Actions.toRawTimeRange(range);
const leftQuery = JSON.stringify([
rawTimeRange.from,
rawTimeRange.to,
initialState.explore.left.datasourceInstance.name,
{},
{ ui: [false, true, false, null] },
]);
expect(dispatchedActions).toEqual([
changeModeAction({ exploreId, mode: ExploreMode.Logs }),
updateLocation({ query: { left: leftQuery, orgId: '1' }, replace: false }),
]);
});
});
const getNavigateToExploreContext = async (openInNewWindow?: (url: string) => void) => { const getNavigateToExploreContext = async (openInNewWindow?: (url: string) => void) => {
const url = 'http://www.someurl.com'; const url = 'http://www.someurl.com';
const panel: Partial<PanelModel> = { const panel: Partial<PanelModel> = {

View File

@ -16,7 +16,6 @@ import {
QueryFixAction, QueryFixAction,
RawTimeRange, RawTimeRange,
TimeRange, TimeRange,
ExploreMode,
ExploreUrlState, ExploreUrlState,
ExploreUIState, ExploreUIState,
} from '@grafana/data'; } from '@grafana/data';
@ -53,7 +52,6 @@ import { ExploreItemState, ThunkResult } from 'app/types';
import { ExploreId, QueryOptions } from 'app/types/explore'; import { ExploreId, QueryOptions } from 'app/types/explore';
import { import {
addQueryRowAction, addQueryRowAction,
changeModeAction,
changeQueryAction, changeQueryAction,
changeRangeAction, changeRangeAction,
changeRefreshIntervalAction, changeRefreshIntervalAction,
@ -141,16 +139,11 @@ export function changeDatasource(
const orgId = getState().user.orgId; const orgId = getState().user.orgId;
const datasourceVersion = newDataSourceInstance.getVersion && (await newDataSourceInstance.getVersion()); const datasourceVersion = newDataSourceInstance.getVersion && (await newDataSourceInstance.getVersion());
// HACK: Switch to logs mode if coming from Prometheus to Loki
const prometheusToLoki =
currentDataSourceInstance?.meta?.name === 'Prometheus' && newDataSourceInstance?.meta?.name === 'Loki';
dispatch( dispatch(
updateDatasourceInstanceAction({ updateDatasourceInstanceAction({
exploreId, exploreId,
datasourceInstance: newDataSourceInstance, datasourceInstance: newDataSourceInstance,
version: datasourceVersion, version: datasourceVersion,
mode: prometheusToLoki ? ExploreMode.Logs : undefined,
}) })
); );
@ -166,16 +159,6 @@ export function changeDatasource(
}; };
} }
/**
* Change the display mode in Explore.
*/
export function changeMode(exploreId: ExploreId, mode: ExploreMode): ThunkResult<void> {
return dispatch => {
dispatch(changeModeAction({ exploreId, mode }));
dispatch(stateSave());
};
}
/** /**
* Query change handler for the query row with the given index. * Query change handler for the query row with the given index.
* If `override` is reset the query modifications and run the queries. Use this to set queries via a link. * If `override` is reset the query modifications and run the queries. Use this to set queries via a link.
@ -291,7 +274,6 @@ export function initializeExplore(
datasourceName: string, datasourceName: string,
queries: DataQuery[], queries: DataQuery[],
range: TimeRange, range: TimeRange,
mode: ExploreMode,
containerWidth: number, containerWidth: number,
eventBridge: Emitter, eventBridge: Emitter,
ui: ExploreUIState, ui: ExploreUIState,
@ -306,7 +288,6 @@ export function initializeExplore(
eventBridge, eventBridge,
queries, queries,
range, range,
mode,
ui, ui,
originPanelId, originPanelId,
}) })
@ -444,7 +425,6 @@ export const runQueries = (exploreId: ExploreId): ThunkResult<void> => {
queryResponse, queryResponse,
querySubscription, querySubscription,
history, history,
mode,
showingGraph, showingGraph,
showingTable, showingTable,
} = exploreItemState; } = exploreItemState;
@ -461,11 +441,11 @@ export const runQueries = (exploreId: ExploreId): ThunkResult<void> => {
// Some datasource's query builders allow per-query interval limits, // Some datasource's query builders allow per-query interval limits,
// but we're using the datasource interval limit for now // but we're using the datasource interval limit for now
const minInterval = datasourceInstance.interval; const minInterval = datasourceInstance?.interval;
stopQueryState(querySubscription); stopQueryState(querySubscription);
const datasourceId = datasourceInstance.meta.id; const datasourceId = datasourceInstance?.meta.id;
const queryOptions: QueryOptions = { const queryOptions: QueryOptions = {
minInterval, minInterval,
@ -473,11 +453,12 @@ export const runQueries = (exploreId: ExploreId): ThunkResult<void> => {
// Loki - used for logs streaming for buffer size, with undefined it falls back to datasource config if it supports that. // Loki - used for logs streaming for buffer size, with undefined it falls back to datasource config if it supports that.
// Elastic - limits the number of datapoints for the counts query and for logs it has hardcoded limit. // Elastic - limits the number of datapoints for the counts query and for logs it has hardcoded limit.
// Influx - used to correctly display logs in graph // Influx - used to correctly display logs in graph
maxDataPoints: mode === ExploreMode.Logs && datasourceId === 'loki' ? undefined : containerWidth, // TODO:unification
// maxDataPoints: mode === ExploreMode.Logs && datasourceId === 'loki' ? undefined : containerWidth,
maxDataPoints: containerWidth,
liveStreaming: live, liveStreaming: live,
showingGraph, showingGraph,
showingTable, showingTable,
mode,
}; };
const datasourceName = exploreItemState.requestedDatasourceName; const datasourceName = exploreItemState.requestedDatasourceName;
@ -591,7 +572,6 @@ export const stateSave = (): ThunkResult<void> => {
datasource: left.datasourceInstance!.name, datasource: left.datasourceInstance!.name,
queries: left.queries.map(clearQueryKeys), queries: left.queries.map(clearQueryKeys),
range: toRawTimeRange(left.range), range: toRawTimeRange(left.range),
mode: left.mode,
ui: { ui: {
showingGraph: left.showingGraph, showingGraph: left.showingGraph,
showingLogs: true, showingLogs: true,
@ -605,7 +585,6 @@ export const stateSave = (): ThunkResult<void> => {
datasource: right.datasourceInstance!.name, datasource: right.datasourceInstance!.name,
queries: right.queries.map(clearQueryKeys), queries: right.queries.map(clearQueryKeys),
range: toRawTimeRange(right.range), range: toRawTimeRange(right.range),
mode: right.mode,
ui: { ui: {
showingGraph: right.showingGraph, showingGraph: right.showingGraph,
showingLogs: true, showingLogs: true,
@ -837,7 +816,7 @@ export function refreshExplore(exploreId: ExploreId): ThunkResult<void> {
return; return;
} }
const { datasource, queries, range: urlRange, mode, ui, originPanelId } = urlState; const { datasource, queries, range: urlRange, ui, originPanelId } = urlState;
const refreshQueries: DataQuery[] = []; const refreshQueries: DataQuery[] = [];
for (let index = 0; index < queries.length; index++) { for (let index = 0; index < queries.length; index++) {
@ -852,17 +831,7 @@ export function refreshExplore(exploreId: ExploreId): ThunkResult<void> {
if (update.datasource) { if (update.datasource) {
const initialQueries = ensureQueries(queries); const initialQueries = ensureQueries(queries);
dispatch( dispatch(
initializeExplore( initializeExplore(exploreId, datasource, initialQueries, range, containerWidth, eventBridge, ui, originPanelId)
exploreId,
datasource,
initialQueries,
range,
mode,
containerWidth,
eventBridge,
ui,
originPanelId
)
); );
return; return;
} }
@ -881,11 +850,6 @@ export function refreshExplore(exploreId: ExploreId): ThunkResult<void> {
dispatch(setQueriesAction({ exploreId, queries: refreshQueries })); dispatch(setQueriesAction({ exploreId, queries: refreshQueries }));
} }
// need to refresh mode
if (update.mode) {
dispatch(changeModeAction({ exploreId, mode }));
}
// always run queries when refresh is needed // always run queries when refresh is needed
if (update.queries || update.ui || update.range) { if (update.queries || update.ui || update.range) {
dispatch(runQueries(exploreId)); dispatch(runQueries(exploreId));

View File

@ -22,7 +22,6 @@ import {
import { ExploreId, ExploreItemState, ExploreState } from 'app/types/explore'; import { ExploreId, ExploreItemState, ExploreState } from 'app/types/explore';
import { reducerTester } from 'test/core/redux/reducerTester'; import { reducerTester } from 'test/core/redux/reducerTester';
import { import {
changeModeAction,
changeRangeAction, changeRangeAction,
changeRefreshIntervalAction, changeRefreshIntervalAction,
scanStartAction, scanStartAction,
@ -75,21 +74,6 @@ describe('Explore item reducer', () => {
}); });
describe('changing datasource', () => { describe('changing datasource', () => {
describe('when changeMode is dispatched', () => {
it('then it should set correct state', () => {
reducerTester<ExploreItemState>()
.givenReducer(itemReducer, ({} as unknown) as ExploreItemState)
.whenActionIsDispatched(changeModeAction({ exploreId: ExploreId.left, mode: ExploreMode.Logs }))
.thenStatePredicateShouldEqual((resultingState: ExploreItemState) => {
expect(resultingState.mode).toEqual(ExploreMode.Logs);
expect(resultingState.logsResult).toBeNull();
expect(resultingState.graphResult).toBeNull();
expect(resultingState.tableResult).toBeNull();
return true;
});
});
});
describe('when updateDatasourceInstanceAction is dispatched', () => { describe('when updateDatasourceInstanceAction is dispatched', () => {
describe('and datasourceInstance supports graph, logs, table and has a startpage', () => { describe('and datasourceInstance supports graph, logs, table and has a startpage', () => {
it('then it should set correct state', () => { it('then it should set correct state', () => {
@ -118,7 +102,6 @@ describe('Explore item reducer', () => {
logsResult: null, logsResult: null,
tableResult: null, tableResult: null,
supportedModes: [ExploreMode.Metrics, ExploreMode.Logs], supportedModes: [ExploreMode.Metrics, ExploreMode.Logs],
mode: ExploreMode.Metrics,
latency: 0, latency: 0,
loading: false, loading: false,
queryResponse: createEmptyQueryResponse(), queryResponse: createEmptyQueryResponse(),
@ -185,7 +168,7 @@ describe('Explore item reducer', () => {
.whenActionIsDispatched(toggleGraphAction({ exploreId: ExploreId.left })) .whenActionIsDispatched(toggleGraphAction({ exploreId: ExploreId.left }))
.thenStateShouldEqual(({ showingGraph: true, graphResult: [] } as unknown) as ExploreItemState) .thenStateShouldEqual(({ showingGraph: true, graphResult: [] } as unknown) as ExploreItemState)
.whenActionIsDispatched(toggleGraphAction({ exploreId: ExploreId.left })) .whenActionIsDispatched(toggleGraphAction({ exploreId: ExploreId.left }))
.thenStateShouldEqual(({ showingGraph: false, graphResult: null } as unknown) as ExploreItemState); .thenStateShouldEqual(({ showingGraph: false, graphResult: [] } as unknown) as ExploreItemState);
}); });
}); });
@ -207,7 +190,7 @@ describe('Explore item reducer', () => {
.whenActionIsDispatched(toggleTableAction({ exploreId: ExploreId.left })) .whenActionIsDispatched(toggleTableAction({ exploreId: ExploreId.left }))
.thenStateShouldEqual(({ showingTable: true, tableResult: table } as unknown) as ExploreItemState) .thenStateShouldEqual(({ showingTable: true, tableResult: table } as unknown) as ExploreItemState)
.whenActionIsDispatched(toggleTableAction({ exploreId: ExploreId.left })) .whenActionIsDispatched(toggleTableAction({ exploreId: ExploreId.left }))
.thenStateShouldEqual(({ showingTable: false, tableResult: null } as unknown) as ExploreItemState); .thenStateShouldEqual(({ showingTable: false, tableResult: table } as unknown) as ExploreItemState);
}); });
}); });
}); });
@ -316,7 +299,6 @@ export const setup = (urlStateOverrides?: any) => {
from: '', from: '',
to: '', to: '',
}, },
mode: ExploreMode.Metrics,
ui: { ui: {
dedupStrategy: LogsDedupStrategy.none, dedupStrategy: LogsDedupStrategy.none,
showingGraph: false, showingGraph: false,

View File

@ -30,7 +30,6 @@ import { ExploreId, ExploreItemState, ExploreState, ExploreUpdateState } from 'a
import { import {
addQueryRowAction, addQueryRowAction,
changeLoadingStateAction, changeLoadingStateAction,
changeModeAction,
changeQueryAction, changeQueryAction,
changeRangeAction, changeRangeAction,
changeRefreshIntervalAction, changeRefreshIntervalAction,
@ -114,7 +113,6 @@ export const makeExploreItemState = (): ExploreItemState => ({
update: makeInitialUpdateState(), update: makeInitialUpdateState(),
latency: 0, latency: 0,
supportedModes: [], supportedModes: [],
mode: (null as unknown) as ExploreMode,
isLive: false, isLive: false,
isPaused: false, isPaused: false,
urlReplaced: false, urlReplaced: false,
@ -189,18 +187,6 @@ export const itemReducer = (state: ExploreItemState = makeExploreItemState(), ac
return { ...state, containerWidth }; return { ...state, containerWidth };
} }
if (changeModeAction.match(action)) {
return {
...state,
mode: action.payload.mode,
graphResult: null,
tableResult: null,
logsResult: null,
queryResponse: createEmptyQueryResponse(),
loading: false,
};
}
if (changeRefreshIntervalAction.match(action)) { if (changeRefreshIntervalAction.match(action)) {
const { refreshInterval } = action.payload; const { refreshInterval } = action.payload;
const live = RefreshPicker.isLive(refreshInterval); const live = RefreshPicker.isLive(refreshInterval);
@ -255,13 +241,12 @@ export const itemReducer = (state: ExploreItemState = makeExploreItemState(), ac
} }
if (initializeExploreAction.match(action)) { if (initializeExploreAction.match(action)) {
const { containerWidth, eventBridge, queries, range, mode, ui, originPanelId } = action.payload; const { containerWidth, eventBridge, queries, range, ui, originPanelId } = action.payload;
return { return {
...state, ...state,
containerWidth, containerWidth,
eventBridge, eventBridge,
range, range,
mode,
queries, queries,
initialized: true, initialized: true,
queryKeys: getQueryKeys(queries, state.datasourceInstance), queryKeys: getQueryKeys(queries, state.datasourceInstance),
@ -272,7 +257,7 @@ export const itemReducer = (state: ExploreItemState = makeExploreItemState(), ac
} }
if (updateDatasourceInstanceAction.match(action)) { if (updateDatasourceInstanceAction.match(action)) {
const { datasourceInstance, version, mode } = action.payload; const { datasourceInstance, version } = action.payload;
// Custom components // Custom components
stopQueryState(state.querySubscription); stopQueryState(state.querySubscription);
@ -294,7 +279,7 @@ export const itemReducer = (state: ExploreItemState = makeExploreItemState(), ac
} }
const updatedDatasourceInstance = Object.assign(datasourceInstance, { meta: newMetadata }); const updatedDatasourceInstance = Object.assign(datasourceInstance, { meta: newMetadata });
const [supportedModes, newMode] = getModesForDatasource(updatedDatasourceInstance, state.mode); const supportedModes = getModesForDatasource(updatedDatasourceInstance);
return { return {
...state, ...state,
@ -307,7 +292,6 @@ export const itemReducer = (state: ExploreItemState = makeExploreItemState(), ac
loading: false, loading: false,
queryKeys: [], queryKeys: [],
supportedModes, supportedModes,
mode: mode ?? newMode,
originPanelId: state.urlState && state.urlState.originPanelId, originPanelId: state.urlState && state.urlState.originPanelId,
}; };
} }
@ -430,7 +414,7 @@ export const itemReducer = (state: ExploreItemState = makeExploreItemState(), ac
return { ...state, showingGraph }; return { ...state, showingGraph };
} }
return { ...state, showingGraph, graphResult: null }; return { ...state, showingGraph };
} }
if (toggleTableAction.match(action)) { if (toggleTableAction.match(action)) {
@ -439,7 +423,7 @@ export const itemReducer = (state: ExploreItemState = makeExploreItemState(), ac
return { ...state, showingTable }; return { ...state, showingTable };
} }
return { ...state, showingTable, tableResult: null }; return { ...state, showingTable };
} }
if (queriesImportedAction.match(action)) { if (queriesImportedAction.match(action)) {
@ -570,6 +554,10 @@ export const processQueryResponse = (
logsResult, logsResult,
loading: loadingState === LoadingState.Loading || loadingState === LoadingState.Streaming, loading: loadingState === LoadingState.Loading || loadingState === LoadingState.Streaming,
update: makeInitialUpdateState(), update: makeInitialUpdateState(),
showLogs: !!logsResult,
showMetrics: !!graphResult,
showTable: !!tableResult,
showTrace: !!processor.traceFrames.length,
}; };
}; };
@ -601,7 +589,6 @@ export const updateChildRefreshState = (
const datasource = _.isEqual(urlState ? urlState.datasource : '', state.urlState.datasource) === false; const datasource = _.isEqual(urlState ? urlState.datasource : '', state.urlState.datasource) === false;
const queries = _.isEqual(urlState ? urlState.queries : [], state.urlState.queries) === false; const queries = _.isEqual(urlState ? urlState.queries : [], state.urlState.queries) === false;
const range = _.isEqual(urlState ? urlState.range : DEFAULT_RANGE, state.urlState.range) === false; const range = _.isEqual(urlState ? urlState.range : DEFAULT_RANGE, state.urlState.range) === false;
const mode = _.isEqual(urlState ? urlState.mode : ExploreMode.Metrics, state.urlState.mode) === false;
const ui = _.isEqual(urlState ? urlState.ui : DEFAULT_UI_STATE, state.urlState.ui) === false; const ui = _.isEqual(urlState ? urlState.ui : DEFAULT_UI_STATE, state.urlState.ui) === false;
return { return {
@ -612,18 +599,16 @@ export const updateChildRefreshState = (
datasource, datasource,
queries, queries,
range, range,
mode,
ui, ui,
}, },
}; };
}; };
const getModesForDatasource = (dataSource: DataSourceApi, currentMode: ExploreMode): [ExploreMode[], ExploreMode] => { const getModesForDatasource = (dataSource: DataSourceApi): ExploreMode[] => {
const supportsGraph = dataSource.meta.metrics; const supportsGraph = dataSource.meta.metrics;
const supportsLogs = dataSource.meta.logs; const supportsLogs = dataSource.meta.logs;
const supportsTracing = dataSource.meta.tracing; const supportsTracing = dataSource.meta.tracing;
let mode = currentMode || ExploreMode.Metrics;
const supportedModes: ExploreMode[] = []; const supportedModes: ExploreMode[] = [];
if (supportsGraph) { if (supportsGraph) {
@ -638,17 +623,7 @@ const getModesForDatasource = (dataSource: DataSourceApi, currentMode: ExploreMo
supportedModes.push(ExploreMode.Tracing); supportedModes.push(ExploreMode.Tracing);
} }
if (supportedModes.length === 1) { return supportedModes;
mode = supportedModes[0];
}
// HACK: Used to set Loki's default explore mode to Logs mode.
// A better solution would be to introduce a "default" or "preferred" mode to the datasource config
if (dataSource.meta.name === 'Loki' && (!currentMode || supportedModes.indexOf(currentMode) === -1)) {
mode = ExploreMode.Logs;
}
return [supportedModes, mode];
}; };
/** /**

View File

@ -6,7 +6,7 @@ jest.mock('@grafana/data/src/datetime/formatter', () => ({
import { ResultProcessor } from './ResultProcessor'; import { ResultProcessor } from './ResultProcessor';
import { ExploreItemState } from 'app/types/explore'; import { ExploreItemState } from 'app/types/explore';
import TableModel from 'app/core/table_model'; import TableModel from 'app/core/table_model';
import { ExploreMode, FieldType, LogRowModel, TimeSeries, toDataFrame } from '@grafana/data'; import { FieldType, LogRowModel, TimeSeries, toDataFrame, ArrayVector } from '@grafana/data';
const testContext = (options: any = {}) => { const testContext = (options: any = {}) => {
const timeSeries = toDataFrame({ const timeSeries = toDataFrame({
@ -34,9 +34,20 @@ const testContext = (options: any = {}) => {
const emptyTable = toDataFrame({ name: 'empty-table', refId: 'A', fields: [] }); const emptyTable = toDataFrame({ name: 'empty-table', refId: 'A', fields: [] });
const logs = toDataFrame({
name: 'logs-res',
refId: 'A',
fields: [
{ name: 'value', type: FieldType.number, values: [4, 5, 6] },
{ name: 'time', type: FieldType.time, values: [100, 100, 100] },
{ name: 'tsNs', type: FieldType.time, values: ['100000002', undefined, '100000001'] },
{ name: 'message', type: FieldType.string, values: ['this is a message', 'second message', 'third'] },
],
meta: { preferredVisualisationType: 'logs' },
});
const defaultOptions = { const defaultOptions = {
mode: ExploreMode.Metrics, dataFrames: [timeSeries, table, emptyTable, logs],
dataFrames: [timeSeries, table, emptyTable],
graphResult: [] as TimeSeries[], graphResult: [] as TimeSeries[],
tableResult: new TableModel(), tableResult: new TableModel(),
logsResult: { hasUniqueLabels: false, rows: [] as LogRowModel[] }, logsResult: { hasUniqueLabels: false, rows: [] as LogRowModel[] },
@ -45,7 +56,6 @@ const testContext = (options: any = {}) => {
const combinedOptions = { ...defaultOptions, ...options }; const combinedOptions = { ...defaultOptions, ...options };
const state = ({ const state = ({
mode: combinedOptions.mode,
graphResult: combinedOptions.graphResult, graphResult: combinedOptions.graphResult,
tableResult: combinedOptions.tableResult, tableResult: combinedOptions.tableResult,
logsResult: combinedOptions.logsResult, logsResult: combinedOptions.logsResult,
@ -191,10 +201,9 @@ describe('ResultProcessor', () => {
describe('when calling getLogsResult', () => { describe('when calling getLogsResult', () => {
it('then it should return correct logs result', () => { it('then it should return correct logs result', () => {
const { resultProcessor, dataFrames } = testContext({ mode: ExploreMode.Logs }); const { resultProcessor, dataFrames } = testContext({});
const timeField = dataFrames[0].fields[0]; const logsDataFrame = dataFrames[3];
const valueField = dataFrames[0].fields[1];
const logsDataFrame = dataFrames[1];
const theResult = resultProcessor.getLogsResult(); const theResult = resultProcessor.getLogsResult();
expect(theResult).toEqual({ expect(theResult).toEqual({
@ -258,24 +267,37 @@ describe('ResultProcessor', () => {
], ],
series: [ series: [
{ {
label: 'A-series', label: 'unknown',
color: '#7EB26D', color: '#8e8e8e',
data: [ data: [[0, 3]],
[100, 4],
[200, 5],
[300, 6],
],
info: [],
isVisible: true, isVisible: true,
yAxis: { yAxis: {
index: 1, index: 1,
min: 0,
tickDecimals: 0,
}, },
seriesIndex: 0, seriesIndex: 0,
timeField, timeField: {
valueField, name: 'Time',
timeStep: 100, type: 'time',
config: { unit: 'time:YYYY-MM-DD HH:mm:ss' },
values: new ArrayVector([0]),
index: 0,
display: expect.anything(),
},
valueField: {
name: 'unknown',
type: 'number',
config: { unit: undefined, color: '#8e8e8e' },
values: new ArrayVector([3]),
labels: undefined,
index: 1,
display: expect.anything(),
},
timeStep: 0,
}, },
], ],
visibleRange: undefined,
}); });
}); });
}); });

View File

@ -5,7 +5,6 @@ import {
FieldType, FieldType,
TimeZone, TimeZone,
getDisplayProcessor, getDisplayProcessor,
ExploreMode,
PreferredVisualisationType, PreferredVisualisationType,
standardTransformers, standardTransformers,
} from '@grafana/data'; } from '@grafana/data';
@ -16,27 +15,51 @@ import { getGraphSeriesModel } from 'app/plugins/panel/graph2/getGraphSeriesMode
import { config } from 'app/core/config'; import { config } from 'app/core/config';
export class ResultProcessor { export class ResultProcessor {
graphFrames: DataFrame[] = [];
tableFrames: DataFrame[] = [];
logsFrames: DataFrame[] = [];
traceFrames: DataFrame[] = [];
constructor( constructor(
private state: ExploreItemState, private state: ExploreItemState,
private dataFrames: DataFrame[], private dataFrames: DataFrame[],
private intervalMs: number, private intervalMs: number,
private timeZone: TimeZone private timeZone: TimeZone
) {} ) {
this.classifyFrames();
getGraphResult(): GraphSeriesXY[] | null {
if (this.state.mode !== ExploreMode.Metrics) {
return null;
} }
const onlyTimeSeries = this.dataFrames.filter(frame => isTimeSeries(frame, this.state.datasourceInstance?.meta.id)); private classifyFrames() {
const timeSeriesToShowInGraph = onlyTimeSeries.filter(frame => shouldShowInVisualisationType(frame, 'graph')); for (const frame of this.dataFrames) {
if (shouldShowInVisualisationTypeStrict(frame, 'logs')) {
this.logsFrames.push(frame);
} else if (shouldShowInVisualisationTypeStrict(frame, 'graph')) {
this.graphFrames.push(frame);
} else if (shouldShowInVisualisationTypeStrict(frame, 'trace')) {
this.traceFrames.push(frame);
} else if (shouldShowInVisualisationTypeStrict(frame, 'table')) {
this.tableFrames.push(frame);
} else if (isTimeSeries(frame, this.state.datasourceInstance?.meta.id)) {
if (shouldShowInVisualisationType(frame, 'graph')) {
this.graphFrames.push(frame);
}
if (shouldShowInVisualisationType(frame, 'table')) {
this.tableFrames.push(frame);
}
} else {
// We fallback to table if we do not have any better meta info about the dataframe.
this.tableFrames.push(frame);
}
}
}
if (timeSeriesToShowInGraph.length === 0) { getGraphResult(): GraphSeriesXY[] | null {
if (this.graphFrames.length === 0) {
return null; return null;
} }
return getGraphSeriesModel( return getGraphSeriesModel(
timeSeriesToShowInGraph, this.graphFrames,
this.timeZone, this.timeZone,
{}, {},
{ showBars: false, showLines: true, showPoints: false }, { showBars: false, showLines: true, showPoints: false },
@ -45,13 +68,11 @@ export class ResultProcessor {
} }
getTableResult(): DataFrame | null { getTableResult(): DataFrame | null {
if (this.state.mode !== ExploreMode.Metrics) { if (this.tableFrames.length === 0) {
return null; return null;
} }
const onlyTables = this.dataFrames this.tableFrames.sort((frameA: DataFrame, frameB: DataFrame) => {
.filter((frame: DataFrame) => shouldShowInVisualisationType(frame, 'table'))
.sort((frameA: DataFrame, frameB: DataFrame) => {
const frameARefId = frameA.refId!; const frameARefId = frameA.refId!;
const frameBRefId = frameB.refId!; const frameBRefId = frameB.refId!;
@ -64,11 +85,7 @@ export class ResultProcessor {
return 0; return 0;
}); });
if (onlyTables.length === 0) { const hasOnlyTimeseries = this.tableFrames.every(df => isTimeSeries(df));
return null;
}
const hasOnlyTimeseries = onlyTables.every(df => isTimeSeries(df));
// If we have only timeseries we do join on default time column which makes more sense. If we are showing // If we have only timeseries we do join on default time column which makes more sense. If we are showing
// non timeseries or some mix of data we are not trying to join on anything and just try to merge them in // non timeseries or some mix of data we are not trying to join on anything and just try to merge them in
@ -77,7 +94,7 @@ export class ResultProcessor {
? standardTransformers.seriesToColumnsTransformer.transformer({}) ? standardTransformers.seriesToColumnsTransformer.transformer({})
: standardTransformers.mergeTransformer.transformer({}); : standardTransformers.mergeTransformer.transformer({});
const data = transformer(onlyTables)[0]; const data = transformer(this.tableFrames)[0];
// set display processor // set display processor
for (const field of data.fields) { for (const field of data.fields) {
@ -92,11 +109,11 @@ export class ResultProcessor {
} }
getLogsResult(): LogsModel | null { getLogsResult(): LogsModel | null {
if (this.state.mode !== ExploreMode.Logs) { if (this.logsFrames.length === 0) {
return null; return null;
} }
const newResults = dataFrameToLogsModel(this.dataFrames, this.intervalMs, this.timeZone, this.state.absoluteRange); const newResults = dataFrameToLogsModel(this.logsFrames, this.intervalMs, this.timeZone, this.state.absoluteRange);
const sortOrder = refreshIntervalToSortOrder(this.state.refreshInterval); const sortOrder = refreshIntervalToSortOrder(this.state.refreshInterval);
const sortedNewResults = sortLogsResult(newResults, sortOrder); const sortedNewResults = sortLogsResult(newResults, sortOrder);
const rows = sortedNewResults.rows; const rows = sortedNewResults.rows;
@ -128,6 +145,10 @@ function shouldShowInVisualisationType(frame: DataFrame, visualisation: Preferre
return true; return true;
} }
function shouldShowInVisualisationTypeStrict(frame: DataFrame, visualisation: PreferredVisualisationType) {
return frame.meta?.preferredVisualisationType === visualisation;
}
// TEMP: Temporary hack. Remove when logs/metrics unification is done // TEMP: Temporary hack. Remove when logs/metrics unification is done
function isTimeSeriesCloudWatch(frame: DataFrame): boolean { function isTimeSeriesCloudWatch(frame: DataFrame): boolean {
return ( return (

View File

@ -49,7 +49,7 @@ describe('getFieldLinksForExplore', () => {
const links = getFieldLinksForExplore(field, 0, splitfn, range); const links = getFieldLinksForExplore(field, 0, splitfn, range);
expect(links[0].href).toBe( expect(links[0].href).toBe(
'/explore?left={"range":{"from":"now-1h","to":"now"},"datasource":"test_ds","queries":[{"query":"query_1"}],"mode":"Metrics","ui":{"showingGraph":true,"showingTable":true,"showingLogs":true}}' '/explore?left={"range":{"from":"now-1h","to":"now"},"datasource":"test_ds","queries":[{"query":"query_1"}],"ui":{"showingGraph":true,"showingTable":true,"showingLogs":true}}'
); );
expect(links[0].title).toBe('test_ds'); expect(links[0].title).toBe('test_ds');

View File

@ -1,13 +1,11 @@
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import { stripIndent, stripIndents } from 'common-tags'; import { stripIndent, stripIndents } from 'common-tags';
import { ExploreStartPageProps, ExploreMode } from '@grafana/data'; import { ExploreStartPageProps } from '@grafana/data';
import Prism from 'prismjs'; import Prism from 'prismjs';
import tokenizer from '../syntax'; import tokenizer from '../syntax';
import { flattenTokens } from '@grafana/ui/src/slate-plugins/slate-prism'; import { flattenTokens } from '@grafana/ui/src/slate-plugins/slate-prism';
import { css, cx } from 'emotion'; import { css, cx } from 'emotion';
import { CloudWatchLogsQuery } from '../types'; import { CloudWatchLogsQuery } from '../types';
import { changeModeAction } from 'app/features/explore/state/actionTypes';
import { dispatch } from 'app/store/store';
interface QueryExample { interface QueryExample {
category: string; category: string;
@ -217,20 +215,9 @@ const exampleCategory = css`
`; `;
export default class LogsCheatSheet extends PureComponent<ExploreStartPageProps, { userExamples: string[] }> { export default class LogsCheatSheet extends PureComponent<ExploreStartPageProps, { userExamples: string[] }> {
switchToMetrics = (query: CloudWatchLogsQuery) => {
const { onClickExample, exploreId } = this.props;
dispatch(changeModeAction({ exploreId, mode: ExploreMode.Metrics }));
onClickExample(query);
};
onClickExample(query: CloudWatchLogsQuery) { onClickExample(query: CloudWatchLogsQuery) {
if (query.expression?.includes('stats')) {
this.switchToMetrics(query);
} else {
this.props.onClickExample(query); this.props.onClickExample(query);
} }
}
renderExpression(expr: string, keyPrefix: string) { renderExpression(expr: string, keyPrefix: string) {
return ( return (

View File

@ -20,7 +20,7 @@ const labelClass = css`
`; `;
export const CloudWatchLogsQueryEditor = memo(function CloudWatchLogsQueryEditor(props: Props) { export const CloudWatchLogsQueryEditor = memo(function CloudWatchLogsQueryEditor(props: Props) {
const { query, data, datasource, onRunQuery, onChange, exploreId, exploreMode, allowCustomValue = false } = props; const { query, data, datasource, onRunQuery, onChange, exploreId, allowCustomValue = false } = props;
let absolute: AbsoluteTimeRange; let absolute: AbsoluteTimeRange;
if (data?.request?.range?.from) { if (data?.request?.range?.from) {
@ -44,7 +44,6 @@ export const CloudWatchLogsQueryEditor = memo(function CloudWatchLogsQueryEditor
return ( return (
<CloudWatchLogsQueryField <CloudWatchLogsQueryField
exploreId={exploreId} exploreId={exploreId}
exploreMode={exploreMode}
datasource={datasource} datasource={datasource}
query={query} query={query}
onBlur={() => {}} onBlur={() => {}}

View File

@ -14,23 +14,20 @@ import {
Select, Select,
MultiSelect, MultiSelect,
} from '@grafana/ui'; } from '@grafana/ui';
import Plain from 'slate-plain-serializer';
// Utils & Services // Utils & Services
// dom also includes Element polyfills // dom also includes Element polyfills
import { Plugin, Node, Editor, Value } from 'slate'; import { Plugin, Node, Editor } from 'slate';
import syntax from '../syntax'; import syntax from '../syntax';
// Types // Types
import { ExploreQueryFieldProps, AbsoluteTimeRange, SelectableValue, ExploreMode, AppEvents } from '@grafana/data'; import { ExploreQueryFieldProps, AbsoluteTimeRange, SelectableValue, AppEvents } from '@grafana/data';
import { CloudWatchQuery, CloudWatchLogsQuery } from '../types'; import { CloudWatchQuery, CloudWatchLogsQuery } from '../types';
import { CloudWatchDatasource } from '../datasource'; import { CloudWatchDatasource } from '../datasource';
import Prism, { Grammar } from 'prismjs'; import Prism, { Grammar } from 'prismjs';
import { CloudWatchLanguageProvider } from '../language_provider'; import { CloudWatchLanguageProvider } from '../language_provider';
import { css } from 'emotion'; import { css } from 'emotion';
import { ExploreId } from 'app/types'; import { ExploreId } from 'app/types';
import { dispatch } from 'app/store/store';
import { changeModeAction } from 'app/features/explore/state/actionTypes';
import { appEvents } from 'app/core/core'; import { appEvents } from 'app/core/core';
import { InputActionMeta } from '@grafana/ui/src/components/Select/types'; import { InputActionMeta } from '@grafana/ui/src/components/Select/types';
import { getStatsGroups } from '../utils/query/getStatsGroups'; import { getStatsGroups } from '../utils/query/getStatsGroups';
@ -274,11 +271,6 @@ export class CloudWatchLogsQueryField extends React.PureComponent<CloudWatchLogs
); );
}; };
switchToMetrics = () => {
const { exploreId } = this.props;
dispatch(changeModeAction({ exploreId, mode: ExploreMode.Metrics }));
};
onQueryFieldClick = (_event: Event, _editor: Editor, next: () => any) => { onQueryFieldClick = (_event: Event, _editor: Editor, next: () => any) => {
const { selectedLogGroups, loadingLogGroups } = this.state; const { selectedLogGroups, loadingLogGroups } = this.state;
@ -299,34 +291,6 @@ export class CloudWatchLogsQueryField extends React.PureComponent<CloudWatchLogs
}); });
}; };
/**
* Check if query is stats query in logs mode and shows a hint to switch to metrics mode. Needs to be done
* on update of the rich Value because standard onChange is not called on load for example.
*/
checkForStatsQuery = debounce((value: Value) => {
const { datasource } = this.props;
// TEMP: Remove when logs/metrics unification is complete
if (datasource.languageProvider && this.props.exploreMode === ExploreMode.Logs) {
const cloudwatchLanguageProvider = datasource.languageProvider as CloudWatchLanguageProvider;
const queryUsesStatsCommand = cloudwatchLanguageProvider.isStatsQuery(Plain.serialize(value));
if (queryUsesStatsCommand) {
this.setState({
hint: {
message: 'You are trying to run a stats query in Logs mode. ',
fix: {
label: 'Switch to Metrics mode.',
action: this.switchToMetrics,
},
},
});
} else {
this.setState({
hint: undefined,
});
}
}
}, 250);
render() { render() {
const { ExtraFieldElement, data, query, syntaxLoaded, datasource, allowCustomValue } = this.props; const { ExtraFieldElement, data, query, syntaxLoaded, datasource, allowCustomValue } = this.props;
const { const {
@ -411,7 +375,6 @@ export class CloudWatchLogsQueryField extends React.PureComponent<CloudWatchLogs
portalOrigin="cloudwatch" portalOrigin="cloudwatch"
syntaxLoaded={syntaxLoaded} syntaxLoaded={syntaxLoaded}
disabled={loadingLogGroups || selectedLogGroups.length === 0} disabled={loadingLogGroups || selectedLogGroups.length === 0}
onRichValueChange={this.checkForStatsQuery}
/> />
</div> </div>
{ExtraFieldElement} {ExtraFieldElement}

View File

@ -79,7 +79,7 @@ export class CloudWatchDatasource extends DataSourceApi<CloudWatchQuery, CloudWa
datasourceName: string; datasourceName: string;
debouncedAlert: (datasourceName: string, region: string) => void; debouncedAlert: (datasourceName: string, region: string) => void;
debouncedCustomAlert: (title: string, message: string) => void; debouncedCustomAlert: (title: string, message: string) => void;
logQueries: Record<string, { id: string; region: string }>; logQueries: Record<string, { id: string; region: string; statsQuery: boolean }>;
languageProvider: CloudWatchLanguageProvider; languageProvider: CloudWatchLanguageProvider;
/** @ngInject */ /** @ngInject */
@ -228,7 +228,11 @@ export class CloudWatchDatasource extends DataSourceApi<CloudWatchQuery, CloudWa
): Observable<DataQueryResponse> { ): Observable<DataQueryResponse> {
this.logQueries = {}; this.logQueries = {};
queryParams.forEach(param => { queryParams.forEach(param => {
this.logQueries[param.refId] = { id: param.queryId, region: param.region }; this.logQueries[param.refId] = {
id: param.queryId,
region: param.region,
statsQuery: param.statsGroups?.length > 0 ?? false,
};
}); });
let prevRecordsMatched: Record<string, number> = {}; let prevRecordsMatched: Record<string, number> = {};

View File

@ -188,7 +188,12 @@ describe('CloudWatchDatasource', () => {
const expectedData = [ const expectedData = [
{ {
...fakeFrames[MAX_ATTEMPTS - 1], ...fakeFrames[MAX_ATTEMPTS - 1],
meta: { custom: { ...fakeFrames[MAX_ATTEMPTS - 1].meta!.custom, Status: 'Complete' } }, meta: {
custom: {
...fakeFrames[MAX_ATTEMPTS - 1].meta!.custom,
Status: 'Complete',
},
},
}, },
]; ];
expect(myResponse).toEqual({ expect(myResponse).toEqual({

View File

@ -233,8 +233,8 @@ describe('ElasticDatasource', function(this: any) {
}, },
], ],
}); });
// 1 for logs and 1 for counts.
expect(response.data.length).toBe(2); expect(response.data.length).toBe(1);
const links = response.data[0].fields.find((field: Field) => field.name === 'host').config.links; const links = response.data[0].fields.find((field: Field) => field.name === 'host').config.links;
expect(links.length).toBe(1); expect(links.length).toBe(1);
expect(links[0].url).toBe('http://localhost:3000/${__value.raw}'); expect(links[0].url).toBe('http://localhost:3000/${__value.raw}');
@ -885,13 +885,13 @@ describe('enhanceDataFrame', () => {
}, },
]); ]);
expect(df.fields[0].config.links.length).toBe(1); expect(df.fields[0].config.links?.length).toBe(1);
expect(df.fields[0].config.links[0]).toEqual({ expect(df.fields[0].config.links?.[0]).toEqual({
title: '', title: '',
url: 'someUrl', url: 'someUrl',
}); });
expect(df.fields[1].config.links.length).toBe(1); expect(df.fields[1].config.links?.length).toBe(1);
expect(df.fields[1].config.links[0]).toEqual({ expect(df.fields[1].config.links?.[0]).toEqual({
title: '', title: '',
url: '', url: '',
internal: { internal: {

View File

@ -365,7 +365,7 @@ export class ElasticDatasource extends DataSourceApi<ElasticsearchQuery, Elastic
let queryObj; let queryObj;
if (target.isLogsQuery || queryDef.hasMetricOfType(target, 'logs')) { if (target.isLogsQuery || queryDef.hasMetricOfType(target, 'logs')) {
target.bucketAggs = [queryDef.defaultBucketAgg()]; target.bucketAggs = [queryDef.defaultBucketAgg()];
target.metrics = [queryDef.defaultMetricAgg()]; target.metrics = [];
// Setting this for metrics queries that are typed as logs // Setting this for metrics queries that are typed as logs
target.isLogsQuery = true; target.isLogsQuery = true;
queryObj = this.queryBuilder.getLogsQuery(target, adhocFilters, queryString); queryObj = this.queryBuilder.getLogsQuery(target, adhocFilters, queryString);

View File

@ -2,7 +2,14 @@ import _ from 'lodash';
import flatten from 'app/core/utils/flatten'; import flatten from 'app/core/utils/flatten';
import * as queryDef from './query_def'; import * as queryDef from './query_def';
import TableModel from 'app/core/table_model'; import TableModel from 'app/core/table_model';
import { DataQueryResponse, DataFrame, toDataFrame, FieldType, MutableDataFrame } from '@grafana/data'; import {
DataQueryResponse,
DataFrame,
toDataFrame,
FieldType,
MutableDataFrame,
PreferredVisualisationType,
} from '@grafana/data';
import { ElasticsearchAggregation } from './types'; import { ElasticsearchAggregation } from './types';
export class ElasticResponse { export class ElasticResponse {
@ -430,7 +437,7 @@ export class ElasticResponse {
const { propNames, docs } = flattenHits(response.hits.hits); const { propNames, docs } = flattenHits(response.hits.hits);
if (docs.length > 0) { if (docs.length > 0) {
const series = createEmptyDataFrame(propNames, this.targets[0].timeField, logMessageField, logLevelField); let series = createEmptyDataFrame(propNames, this.targets[0].timeField, logMessageField, logLevelField);
// Add a row for each document // Add a row for each document
for (const doc of docs) { for (const doc of docs) {
@ -443,6 +450,7 @@ export class ElasticResponse {
series.add(doc); series.add(doc);
} }
series = addPreferredVisualisationType(series, 'logs');
dataFrame.push(series); dataFrame.push(series);
} }
@ -578,7 +586,7 @@ const createEmptyDataFrame = (
return series; return series;
}; };
const addPreferredVisualisationType = (series: any, type: string) => { const addPreferredVisualisationType = (series: any, type: PreferredVisualisationType) => {
let s = series; let s = series;
s.meta s.meta
? (s.meta.preferredVisualisationType = type) ? (s.meta.preferredVisualisationType = type)

View File

@ -1,7 +1,6 @@
import { DataSourcePlugin } from '@grafana/data'; import { DataSourcePlugin } from '@grafana/data';
import { ElasticDatasource } from './datasource'; import { ElasticDatasource } from './datasource';
import { ElasticQueryCtrl } from './query_ctrl'; import { ElasticQueryCtrl } from './query_ctrl';
import ElasticsearchQueryField from './components/ElasticsearchQueryField';
import { ConfigEditor } from './configuration/ConfigEditor'; import { ConfigEditor } from './configuration/ConfigEditor';
class ElasticAnnotationsQueryCtrl { class ElasticAnnotationsQueryCtrl {
@ -11,5 +10,4 @@ class ElasticAnnotationsQueryCtrl {
export const plugin = new DataSourcePlugin(ElasticDatasource) export const plugin = new DataSourcePlugin(ElasticDatasource)
.setQueryCtrl(ElasticQueryCtrl) .setQueryCtrl(ElasticQueryCtrl)
.setConfigEditor(ConfigEditor) .setConfigEditor(ConfigEditor)
.setExploreLogsQueryField(ElasticsearchQueryField)
.setAnnotationQueryCtrl(ElasticAnnotationsQueryCtrl); .setAnnotationQueryCtrl(ElasticAnnotationsQueryCtrl);

View File

@ -221,7 +221,7 @@ export class ElasticQueryBuilder {
* Check if metric type is raw_document. If metric doesn't have size (or size is 0), update size to 500. * Check if metric type is raw_document. If metric doesn't have size (or size is 0), update size to 500.
* Otherwise it will not be a valid query and error will be thrown. * Otherwise it will not be a valid query and error will be thrown.
*/ */
if (target.metrics[0].type === 'raw_document') { if (target.metrics?.[0]?.type === 'raw_document') {
metric = target.metrics[0]; metric = target.metrics[0];
const size = (metric.settings && metric.settings.size !== 0 && metric.settings.size) || 500; const size = (metric.settings && metric.settings.size !== 0 && metric.settings.size) || 500;
return this.documentQuery(query, size); return this.documentQuery(query, size);

View File

@ -149,6 +149,8 @@ export default class InfluxDatasource extends DataSourceWithBackend<InfluxQuery,
}); });
switch (target.resultFormat) { switch (target.resultFormat) {
case 'logs':
meta.preferredVisualisationType = 'logs';
case 'table': { case 'table': {
seriesList.push(influxSeries.getTable()); seriesList.push(influxSeries.getTable());
break; break;

View File

@ -1,6 +1,5 @@
import InfluxDatasource from './datasource'; import InfluxDatasource from './datasource';
import { InfluxQueryCtrl } from './query_ctrl'; import { InfluxQueryCtrl } from './query_ctrl';
import { InfluxLogsQueryField } from './components/InfluxLogsQueryField';
import InfluxStartPage from './components/InfluxStartPage'; import InfluxStartPage from './components/InfluxStartPage';
import { DataSourcePlugin } from '@grafana/data'; import { DataSourcePlugin } from '@grafana/data';
import ConfigEditor from './components/ConfigEditor'; import ConfigEditor from './components/ConfigEditor';
@ -16,5 +15,4 @@ export const plugin = new DataSourcePlugin(InfluxDatasource)
.setConfigEditor(ConfigEditor) .setConfigEditor(ConfigEditor)
.setQueryCtrl(InfluxQueryCtrl) .setQueryCtrl(InfluxQueryCtrl)
.setAnnotationQueryCtrl(InfluxAnnotationsQueryCtrl) .setAnnotationQueryCtrl(InfluxAnnotationsQueryCtrl)
.setExploreLogsQueryField(InfluxLogsQueryField)
.setExploreStartPage(InfluxStartPage); .setExploreStartPage(InfluxStartPage);

View File

@ -38,6 +38,7 @@ export class InfluxQueryCtrl extends QueryCtrl {
this.resultFormats = [ this.resultFormats = [
{ text: 'Time series', value: 'time_series' }, { text: 'Time series', value: 'time_series' },
{ text: 'Table', value: 'table' }, { text: 'Table', value: 'table' },
{ text: 'Logs', value: 'logs' },
]; ];
this.policySegment = uiSegmentSrv.newSegment(this.target.policy); this.policySegment = uiSegmentSrv.newSegment(this.target.policy);

View File

@ -48,6 +48,9 @@ export class JaegerDatasource extends DataSourceApi<JaegerQuery> {
values: response?.data?.data || [], values: response?.data?.data || [],
}, },
], ],
meta: {
preferredVisualisationType: 'trace',
},
}), }),
], ],
}; };
@ -64,6 +67,9 @@ export class JaegerDatasource extends DataSourceApi<JaegerQuery> {
values: [], values: [],
}, },
], ],
meta: {
preferredVisualisationType: 'trace',
},
}), }),
], ],
}); });

View File

@ -1,6 +1,6 @@
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import { shuffle } from 'lodash'; import { shuffle } from 'lodash';
import { ExploreStartPageProps, DataQuery, ExploreMode } from '@grafana/data'; import { ExploreStartPageProps, DataQuery } from '@grafana/data';
import LokiLanguageProvider from '../language_provider'; import LokiLanguageProvider from '../language_provider';
const DEFAULT_EXAMPLES = ['{job="default/prometheus"}']; const DEFAULT_EXAMPLES = ['{job="default/prometheus"}'];
@ -46,7 +46,7 @@ export default class LokiCheatSheet extends PureComponent<ExploreStartPageProps,
checkUserLabels = async () => { checkUserLabels = async () => {
// Set example from user labels // Set example from user labels
const provider: LokiLanguageProvider = this.props.datasource.languageProvider; const provider: LokiLanguageProvider = this.props.datasource?.languageProvider;
if (provider.started) { if (provider.started) {
const labels = provider.getLabelKeys() || []; const labels = provider.getLabelKeys() || [];
const preferredLabel = PREFERRED_LABELS.find(l => labels.includes(l)); const preferredLabel = PREFERRED_LABELS.find(l => labels.includes(l));
@ -76,11 +76,11 @@ export default class LokiCheatSheet extends PureComponent<ExploreStartPageProps,
); );
} }
renderLogsCheatSheet() { render() {
const { userExamples } = this.state; const { userExamples } = this.state;
return ( return (
<> <div>
<h2>Loki Cheat Sheet</h2> <h2>Loki Cheat Sheet</h2>
<div className="cheat-sheet-item"> <div className="cheat-sheet-item">
<div className="cheat-sheet-item__title">See your logs</div> <div className="cheat-sheet-item__title">See your logs</div>
@ -114,14 +114,6 @@ export default class LokiCheatSheet extends PureComponent<ExploreStartPageProps,
supports exact and regular expression filters. supports exact and regular expression filters.
</div> </div>
</div> </div>
</>
);
}
renderMetricsCheatSheet() {
return (
<div>
<h2>LogQL Cheat Sheet</h2>
{LOGQL_EXAMPLES.map(item => ( {LOGQL_EXAMPLES.map(item => (
<div className="cheat-sheet-item" key={item.expression}> <div className="cheat-sheet-item" key={item.expression}>
<div className="cheat-sheet-item__title">{item.title}</div> <div className="cheat-sheet-item__title">{item.title}</div>
@ -132,10 +124,4 @@ export default class LokiCheatSheet extends PureComponent<ExploreStartPageProps,
</div> </div>
); );
} }
render() {
const { exploreMode } = this.props;
return exploreMode === ExploreMode.Logs ? this.renderLogsCheatSheet() : this.renderMetricsCheatSheet();
}
} }

View File

@ -87,12 +87,4 @@ describe('LokiExploreQueryEditor', () => {
expect(wrapper.find(LokiExploreExtraField).length).toBe(1); expect(wrapper.find(LokiExploreExtraField).length).toBe(1);
}); });
}); });
it('should render LokiQueryField with no ExtraFieldElement when ExploreMode is not Logs', async () => {
// @ts-ignore strict null error TS2345: Argument of type '() => Promise<void>' is not assignable to parameter of type '() => void | undefined'.
await act(async () => {
const wrapper = setup(mount, { exploreMode: ExploreMode.Metrics });
expect(wrapper.find(LokiExploreExtraField).length).toBe(0);
});
});
}); });

View File

@ -3,7 +3,7 @@ import React, { memo } from 'react';
import _ from 'lodash'; import _ from 'lodash';
// Types // Types
import { AbsoluteTimeRange, ExploreQueryFieldProps, ExploreMode } from '@grafana/data'; import { AbsoluteTimeRange, ExploreQueryFieldProps } from '@grafana/data';
import { LokiDatasource } from '../datasource'; import { LokiDatasource } from '../datasource';
import { LokiQuery, LokiOptions } from '../types'; import { LokiQuery, LokiOptions } from '../types';
import { LokiQueryField } from './LokiQueryField'; import { LokiQueryField } from './LokiQueryField';
@ -12,7 +12,7 @@ import LokiExploreExtraField from './LokiExploreExtraField';
type Props = ExploreQueryFieldProps<LokiDatasource, LokiQuery, LokiOptions>; type Props = ExploreQueryFieldProps<LokiDatasource, LokiQuery, LokiOptions>;
export function LokiExploreQueryEditor(props: Props) { export function LokiExploreQueryEditor(props: Props) {
const { query, data, datasource, exploreMode, history, onChange, onRunQuery } = props; const { query, data, datasource, history, onChange, onRunQuery } = props;
let absolute: AbsoluteTimeRange; let absolute: AbsoluteTimeRange;
if (data && data.request) { if (data && data.request) {
@ -72,7 +72,6 @@ export function LokiExploreQueryEditor(props: Props) {
data={data} data={data}
absoluteRange={absolute} absoluteRange={absolute}
ExtraFieldElement={ ExtraFieldElement={
exploreMode === ExploreMode.Logs ? (
<LokiExploreExtraField <LokiExploreExtraField
label={'Line limit'} label={'Line limit'}
onChangeFunc={onMaxLinesChange} onChangeFunc={onMaxLinesChange}
@ -81,7 +80,6 @@ export function LokiExploreQueryEditor(props: Props) {
type={'number'} type={'number'}
min={0} min={0}
/> />
) : null
} }
/> />
); );

View File

@ -1,15 +1,7 @@
import LokiDatasource, { RangeQueryOptions } from './datasource'; import LokiDatasource, { RangeQueryOptions } from './datasource';
import { LokiQuery, LokiResponse, LokiResultType } from './types'; import { LokiQuery, LokiResponse, LokiResultType } from './types';
import { getQueryOptions } from 'test/helpers/getQueryOptions'; import { getQueryOptions } from 'test/helpers/getQueryOptions';
import { import { AnnotationQueryRequest, DataFrame, DataSourceApi, dateTime, FieldCache, TimeRange } from '@grafana/data';
AnnotationQueryRequest,
DataFrame,
DataSourceApi,
dateTime,
ExploreMode,
FieldCache,
TimeRange,
} from '@grafana/data';
import { TemplateSrv } from 'app/features/templating/template_srv'; import { TemplateSrv } from 'app/features/templating/template_srv';
import { makeMockLokiDatasource } from './mocks'; import { makeMockLokiDatasource } from './mocks';
import { of } from 'rxjs'; import { of } from 'rxjs';
@ -19,7 +11,7 @@ import { CustomVariableModel } from '../../../features/variables/types';
import { initialCustomVariableModelState } from '../../../features/variables/custom/reducer'; // will use the version in __mocks__ import { initialCustomVariableModelState } from '../../../features/variables/custom/reducer'; // will use the version in __mocks__
jest.mock('@grafana/runtime', () => ({ jest.mock('@grafana/runtime', () => ({
...((jest.requireActual('@grafana/runtime') as unknown) as object), ...jest.requireActual('@grafana/runtime'),
getBackendSrv: () => backendSrv, getBackendSrv: () => backendSrv,
})); }));
@ -110,24 +102,9 @@ describe('LokiDatasource', () => {
datasourceRequestMock.mockImplementation(() => Promise.resolve(testResp)); datasourceRequestMock.mockImplementation(() => Promise.resolve(testResp));
}); });
test('should run instant query and range query when in metrics mode', async () => {
const options = getQueryOptions<LokiQuery>({
targets: [{ expr: 'rate({job="grafana"}[5m])', refId: 'A' }],
exploreMode: ExploreMode.Metrics,
});
ds.runInstantQuery = jest.fn(() => of({ data: [] }));
ds.runRangeQuery = jest.fn(() => of({ data: [] }));
await ds.query(options).toPromise();
expect(ds.runInstantQuery).toBeCalled();
expect(ds.runRangeQuery).toBeCalled();
});
test('should just run range query when in logs mode', async () => { test('should just run range query when in logs mode', async () => {
const options = getQueryOptions<LokiQuery>({ const options = getQueryOptions<LokiQuery>({
targets: [{ expr: '{job="grafana"}', refId: 'B' }], targets: [{ expr: '{job="grafana"}', refId: 'B' }],
exploreMode: ExploreMode.Logs,
}); });
ds.runInstantQuery = jest.fn(() => of({ data: [] })); ds.runInstantQuery = jest.fn(() => of({ data: [] }));

View File

@ -19,7 +19,6 @@ import {
LoadingState, LoadingState,
AnnotationEvent, AnnotationEvent,
DataFrameView, DataFrameView,
TimeSeries,
PluginMeta, PluginMeta,
DataSourceApi, DataSourceApi,
DataSourceInstanceSettings, DataSourceInstanceSettings,
@ -27,7 +26,6 @@ import {
DataQueryRequest, DataQueryRequest,
DataQueryResponse, DataQueryResponse,
AnnotationQueryRequest, AnnotationQueryRequest,
ExploreMode,
ScopedVars, ScopedVars,
} from '@grafana/data'; } from '@grafana/data';
@ -92,30 +90,7 @@ export class LokiDatasource extends DataSourceApi<LokiQuery, LokiOptions> {
expr: this.templateSrv.replace(target.expr, options.scopedVars, this.interpolateQueryExpr), expr: this.templateSrv.replace(target.expr, options.scopedVars, this.interpolateQueryExpr),
})); }));
if (options.exploreMode === ExploreMode.Metrics) { filteredTargets.forEach(target => subQueries.push(this.runRangeQuery(target, options, filteredTargets.length)));
filteredTargets.forEach(target =>
subQueries.push(
this.runInstantQuery(target, options, filteredTargets.length),
this.runRangeQuery(target, options, filteredTargets.length)
)
);
} else {
filteredTargets.forEach(target =>
subQueries.push(
this.runRangeQuery(target, options, filteredTargets.length).pipe(
map(dataQueryResponse => {
if (options.exploreMode === ExploreMode.Logs && dataQueryResponse.data.find(d => isTimeSeries(d))) {
throw new Error(
'Logs mode does not support queries that return time series data. Please perform a logs query or switch to Metrics mode.'
);
} else {
return dataQueryResponse;
}
})
)
)
);
}
// No valid targets, return the empty result to save a round trip. // No valid targets, return the empty result to save a round trip.
if (isEmpty(subQueries)) { if (isEmpty(subQueries)) {
@ -149,7 +124,10 @@ export class LokiDatasource extends DataSourceApi<LokiQuery, LokiOptions> {
filter((response: any) => (response.cancelled ? false : true)), filter((response: any) => (response.cancelled ? false : true)),
map((response: { data: LokiResponse }) => { map((response: { data: LokiResponse }) => {
if (response.data.data.resultType === LokiResultType.Stream) { if (response.data.data.resultType === LokiResultType.Stream) {
throw new Error('Metrics mode does not support logs. Use an aggregation or switch to Logs mode.'); return {
data: [],
key: `${target.refId}_instant`,
};
} }
return { return {
@ -582,7 +560,3 @@ export function lokiSpecialRegexEscape(value: any) {
} }
export default LokiDatasource; export default LokiDatasource;
function isTimeSeries(data: any): data is TimeSeries {
return data.hasOwnProperty('datapoints');
}

View File

@ -1,8 +1,8 @@
import { DataFrame, FieldType, parseLabels, KeyValue, CircularDataFrame } from '@grafana/data'; import { DataFrame, FieldType, parseLabels, KeyValue, CircularDataFrame } from '@grafana/data';
import { Observable } from 'rxjs'; import { Observable, throwError } from 'rxjs';
import { webSocket } from 'rxjs/webSocket'; import { webSocket } from 'rxjs/webSocket';
import { LokiTailResponse } from './types'; import { LokiTailResponse } from './types';
import { finalize, map } from 'rxjs/operators'; import { finalize, map, catchError } from 'rxjs/operators';
import { appendResponseToBufferedData } from './result_transformer'; import { appendResponseToBufferedData } from './result_transformer';
/** /**
@ -35,15 +35,18 @@ export class LiveStreams {
data.addField({ name: 'line', type: FieldType.string }).labels = parseLabels(target.query); data.addField({ name: 'line', type: FieldType.string }).labels = parseLabels(target.query);
data.addField({ name: 'labels', type: FieldType.other }); // The labels for each line data.addField({ name: 'labels', type: FieldType.other }); // The labels for each line
data.addField({ name: 'id', type: FieldType.string }); data.addField({ name: 'id', type: FieldType.string });
data.meta = { ...data.meta, preferredVisualisationType: 'logs' };
stream = webSocket(target.url).pipe( stream = webSocket(target.url).pipe(
finalize(() => {
delete this.streams[target.url];
}),
map((response: LokiTailResponse) => { map((response: LokiTailResponse) => {
appendResponseToBufferedData(response, data); appendResponseToBufferedData(response, data);
return [data]; return [data];
}),
catchError(err => {
return throwError(`error: ${err.reason}`);
}),
finalize(() => {
delete this.streams[target.url];
}) })
); );
this.streams[target.url] = stream; this.streams[target.url] = stream;

View File

@ -323,6 +323,7 @@ export function lokiStreamsToDataframes(
limit, limit,
stats, stats,
custom, custom,
preferredVisualisationType: 'logs',
}, },
}; };
}); });

View File

@ -1,3 +1,5 @@
import set from 'lodash/set';
import { import {
ArrayDataFrame, ArrayDataFrame,
arrowTableToDataFrame, arrowTableToDataFrame,
@ -85,6 +87,11 @@ export class TestDataDataSource extends DataSourceApi<TestDataQuery> {
const table = t as TableData; const table = t as TableData;
table.refId = query.refId; table.refId = query.refId;
table.name = query.alias; table.name = query.alias;
if (query.scenarioId === 'logs') {
set(table, 'meta.preferredVisualisationType', 'logs');
}
data.push(table); data.push(table);
} }

View File

@ -128,8 +128,9 @@ export function runLogsStream(
}); });
data.refId = target.refId; data.refId = target.refId;
data.name = target.alias || 'Logs ' + target.refId; data.name = target.alias || 'Logs ' + target.refId;
data.addField({ name: 'time', type: FieldType.time });
data.addField({ name: 'line', type: FieldType.string }); data.addField({ name: 'line', type: FieldType.string });
data.addField({ name: 'time', type: FieldType.time });
data.meta = { preferredVisualisationType: 'logs' };
const { speed } = query; const { speed } = query;

View File

@ -74,6 +74,9 @@ function responseToDataQueryResponse(response: { data: ZipkinSpan[] }): DataQuer
values: response?.data ? [transformResponse(response?.data)] : [], values: response?.data ? [transformResponse(response?.data)] : [],
}, },
], ],
meta: {
preferredVisualisationType: 'trace',
},
}), }),
], ],
}; };
@ -89,6 +92,9 @@ const emptyDataQueryResponse = {
values: [], values: [],
}, },
], ],
meta: {
preferredVisualisationType: 'trace',
},
}), }),
], ],
}; };

View File

@ -166,7 +166,6 @@ export interface ExploreItemState {
latency: number; latency: number;
supportedModes: ExploreMode[]; supportedModes: ExploreMode[];
mode: ExploreMode;
/** /**
* If true, the view is in live tailing mode. * If true, the view is in live tailing mode.
@ -188,6 +187,11 @@ export interface ExploreItemState {
* query of that panel. * query of that panel.
*/ */
originPanelId?: number | null; originPanelId?: number | null;
showLogs?: boolean;
showMetrics?: boolean;
showTable?: boolean;
showTrace?: boolean;
} }
export interface ExploreUpdateState { export interface ExploreUpdateState {