mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
be961c5466
commit
64bc85963b
@ -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,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
@ -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 {
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
})
|
})
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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"]}]'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -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 {
|
||||||
|
@ -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,
|
||||||
|
@ -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);
|
||||||
|
@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
@ -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}
|
||||||
|
@ -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 } } });
|
||||||
|
@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -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.
|
||||||
|
@ -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> = {
|
||||||
|
@ -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));
|
||||||
|
@ -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,
|
||||||
|
@ -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];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -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 (
|
||||||
|
@ -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');
|
||||||
|
|
||||||
|
@ -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 (
|
||||||
|
@ -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={() => {}}
|
||||||
|
@ -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}
|
||||||
|
@ -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> = {};
|
||||||
|
|
||||||
|
@ -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({
|
||||||
|
@ -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: {
|
||||||
|
@ -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);
|
||||||
|
@ -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)
|
||||||
|
@ -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);
|
||||||
|
@ -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);
|
||||||
|
@ -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;
|
||||||
|
@ -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);
|
||||||
|
@ -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);
|
||||||
|
@ -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',
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
@ -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
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -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: [] }));
|
||||||
|
@ -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');
|
|
||||||
}
|
|
||||||
|
@ -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;
|
||||||
|
@ -323,6 +323,7 @@ export function lokiStreamsToDataframes(
|
|||||||
limit,
|
limit,
|
||||||
stats,
|
stats,
|
||||||
custom,
|
custom,
|
||||||
|
preferredVisualisationType: 'logs',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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',
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
@ -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 {
|
||||||
|
Loading…
Reference in New Issue
Block a user