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({
|
||||
title: 'testDS',
|
||||
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,
|
||||
})
|
||||
);
|
||||
|
@ -15,7 +15,7 @@ export enum LoadingState {
|
||||
Error = 'Error',
|
||||
}
|
||||
|
||||
export type PreferredVisualisationType = 'graph' | 'table';
|
||||
export type PreferredVisualisationType = 'graph' | 'table' | 'logs' | 'trace';
|
||||
|
||||
export interface QueryResultMeta {
|
||||
/** DatasSource Specific Values */
|
||||
@ -47,6 +47,7 @@ export interface QueryResultMeta {
|
||||
searchWords?: string[]; // 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
|
||||
instant?: boolean;
|
||||
}
|
||||
|
||||
export interface QueryResultMetaStat extends FieldConfig {
|
||||
|
@ -300,7 +300,6 @@ export interface QueryEditorProps<
|
||||
* Contains query response filtered by refId of QueryResultBase and possible query error
|
||||
*/
|
||||
data?: PanelData;
|
||||
exploreMode?: ExploreMode;
|
||||
exploreId?: any;
|
||||
history?: HistoryItem[];
|
||||
}
|
||||
@ -324,13 +323,11 @@ export interface ExploreQueryFieldProps<
|
||||
history: any[];
|
||||
onBlur?: () => void;
|
||||
absoluteRange?: AbsoluteTimeRange;
|
||||
exploreMode?: ExploreMode;
|
||||
exploreId?: any;
|
||||
}
|
||||
|
||||
export interface ExploreStartPageProps {
|
||||
datasource: DataSourceApi;
|
||||
exploreMode: ExploreMode;
|
||||
onClickExample: (query: DataQuery) => void;
|
||||
exploreId?: any;
|
||||
}
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { ExploreMode } from './datasource';
|
||||
import { RawTimeRange } from './time';
|
||||
import { LogsDedupStrategy } from './logs';
|
||||
|
||||
@ -6,7 +5,6 @@ import { LogsDedupStrategy } from './logs';
|
||||
export interface ExploreUrlState {
|
||||
datasource: string;
|
||||
queries: any[]; // Should be a DataQuery, but we're going to strip refIds, so typing makes less sense
|
||||
mode: ExploreMode;
|
||||
range: RawTimeRange;
|
||||
ui: ExploreUIState;
|
||||
originPanelId?: number;
|
||||
|
@ -31,7 +31,7 @@ describe('mapInternalLinkToExplore', () => {
|
||||
expect.objectContaining({
|
||||
title: 'testDS',
|
||||
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,
|
||||
})
|
||||
);
|
||||
|
@ -2,7 +2,6 @@ import {
|
||||
DataLink,
|
||||
DataQuery,
|
||||
DataSourceInstanceSettings,
|
||||
ExploreMode,
|
||||
Field,
|
||||
InterpolateFunction,
|
||||
LinkModel,
|
||||
@ -82,7 +81,6 @@ function generateInternalHref<T extends DataQuery = any>(datasourceName: string,
|
||||
queries: [query],
|
||||
// This should get overwritten if datasource does not support that mode and we do not know what mode is
|
||||
// preferred anyway.
|
||||
mode: ExploreMode.Metrics,
|
||||
ui: {
|
||||
showingGraph: true,
|
||||
showingTable: true,
|
||||
|
@ -139,7 +139,6 @@ export function serializeStateToUrlParam(urlState: ExploreUrlState, compact?: bo
|
||||
urlState.range.to,
|
||||
urlState.datasource,
|
||||
...urlState.queries,
|
||||
{ mode: urlState.mode },
|
||||
{
|
||||
ui: [
|
||||
!!urlState.ui.showingGraph,
|
||||
|
@ -45,6 +45,14 @@ func (e *CloudWatchExecutor) executeLogActions(ctx context.Context, queryContext
|
||||
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})}
|
||||
return nil
|
||||
})
|
||||
|
@ -32,7 +32,6 @@ import {
|
||||
import { getThemeColor } from 'app/core/utils/colors';
|
||||
|
||||
import { sortInAscendingOrder, deduplicateLogRowsById } from 'app/core/utils/explore';
|
||||
import { getGraphSeriesModel } from 'app/plugins/panel/graph2/getGraphSeriesModel';
|
||||
import { decimalSIPrefix } from '@grafana/data/src/valueFormats/symbolFormatters';
|
||||
|
||||
export const LogLevelColor = {
|
||||
@ -143,19 +142,23 @@ export function makeSeriesForLogs(sortedRows: LogRowModel[], bucketSize: number,
|
||||
const fieldCache = new FieldCache(data);
|
||||
|
||||
const timeField = fieldCache.getFirstFieldOfType(FieldType.time);
|
||||
timeField.display = getDisplayProcessor({
|
||||
field: timeField,
|
||||
timeZone,
|
||||
});
|
||||
if (timeField) {
|
||||
timeField.display = getDisplayProcessor({
|
||||
field: timeField,
|
||||
timeZone,
|
||||
});
|
||||
}
|
||||
|
||||
const valueField = fieldCache.getFirstFieldOfType(FieldType.number);
|
||||
valueField.config = {
|
||||
...valueField.config,
|
||||
color: series.color,
|
||||
};
|
||||
valueField.name = series.alias;
|
||||
const fieldDisplayProcessor = getDisplayProcessor({ field: valueField, timeZone });
|
||||
valueField.display = (value: any) => ({ ...fieldDisplayProcessor(value), color: series.color });
|
||||
if (valueField) {
|
||||
valueField.config = {
|
||||
...valueField.config,
|
||||
color: series.color,
|
||||
};
|
||||
valueField.name = series.alias;
|
||||
const fieldDisplayProcessor = getDisplayProcessor({ field: valueField, timeZone });
|
||||
valueField.display = (value: any) => ({ ...fieldDisplayProcessor(value), color: series.color });
|
||||
}
|
||||
|
||||
const points = getFlotPairs({
|
||||
xField: timeField,
|
||||
@ -201,35 +204,21 @@ export function dataFrameToLogsModel(
|
||||
timeZone: TimeZone,
|
||||
absoluteRange?: AbsoluteTimeRange
|
||||
): LogsModel {
|
||||
const { logSeries, metricSeries } = separateLogsAndMetrics(dataFrame);
|
||||
const { logSeries } = separateLogsAndMetrics(dataFrame);
|
||||
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 (metricSeries.length === 0) {
|
||||
// Create histogram metrics from logs using the interval as bucket size for the line count
|
||||
if (intervalMs && logsModel.rows.length > 0) {
|
||||
const sortedRows = logsModel.rows.sort(sortInAscendingOrder);
|
||||
const { visibleRange, bucketSize } = getSeriesProperties(sortedRows, intervalMs, absoluteRange);
|
||||
logsModel.visibleRange = visibleRange;
|
||||
logsModel.series = makeSeriesForLogs(sortedRows, bucketSize, timeZone);
|
||||
} else {
|
||||
logsModel.series = [];
|
||||
}
|
||||
// Create histogram metrics from logs using the interval as bucket size for the line count
|
||||
if (intervalMs && logsModel.rows.length > 0) {
|
||||
const sortedRows = logsModel.rows.sort(sortInAscendingOrder);
|
||||
const { visibleRange, bucketSize } = getSeriesProperties(sortedRows, intervalMs, absoluteRange);
|
||||
logsModel.visibleRange = visibleRange;
|
||||
logsModel.series = makeSeriesForLogs(sortedRows, bucketSize, timeZone);
|
||||
} 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',
|
||||
}
|
||||
);
|
||||
logsModel.series = [];
|
||||
}
|
||||
|
||||
return logsModel;
|
||||
}
|
||||
|
||||
@ -431,8 +420,8 @@ export function logSeriesToLogsModel(logSeries: DataFrame[]): LogsModel | undefi
|
||||
// Stats are per query, keeping track by refId
|
||||
const { refId } = series;
|
||||
if (refId && !queriesVisited[refId]) {
|
||||
if (totalBytesKey && series.meta.stats) {
|
||||
const byteStat = series.meta.stats.find(stat => stat.displayName === totalBytesKey);
|
||||
if (totalBytesKey && series.meta?.stats) {
|
||||
const byteStat = series.meta?.stats.find(stat => stat.displayName === totalBytesKey);
|
||||
if (byteStat) {
|
||||
totalBytes += byteStat.value;
|
||||
}
|
||||
|
@ -18,7 +18,6 @@ import store from 'app/core/store';
|
||||
import {
|
||||
DataQueryError,
|
||||
dateTime,
|
||||
ExploreMode,
|
||||
LogLevel,
|
||||
LogRowModel,
|
||||
LogsDedupStrategy,
|
||||
@ -33,7 +32,6 @@ const DEFAULT_EXPLORE_STATE: ExploreUrlState = {
|
||||
datasource: '',
|
||||
queries: [],
|
||||
range: DEFAULT_RANGE,
|
||||
mode: ExploreMode.Metrics,
|
||||
ui: {
|
||||
showingGraph: true,
|
||||
showingTable: true,
|
||||
@ -101,7 +99,6 @@ describe('state functions', () => {
|
||||
expect(serializeStateToUrlParam(state)).toBe(
|
||||
'{"datasource":"foo","queries":[{"expr":"metric{test=\\"a/b\\"}"},' +
|
||||
'{"expr":"super{foo=\\"x/z\\"}"}],"range":{"from":"now-5h","to":"now"},' +
|
||||
'"mode":"Metrics",' +
|
||||
'"ui":{"showingGraph":true,"showingTable":true,"showingLogs":true,"dedupStrategy":"none"}}'
|
||||
);
|
||||
});
|
||||
@ -124,7 +121,7 @@ describe('state functions', () => {
|
||||
},
|
||||
};
|
||||
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,
|
||||
dateMath,
|
||||
DefaultTimeZone,
|
||||
ExploreMode,
|
||||
HistoryItem,
|
||||
IntervalValues,
|
||||
LogRowModel,
|
||||
@ -249,9 +248,6 @@ export function parseUrlState(initial: string | undefined): ExploreUrlState {
|
||||
const metricProperties = ['expr', 'expression', 'target', 'datasource', 'query'];
|
||||
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 ui = uiState
|
||||
? {
|
||||
@ -263,7 +259,7 @@ export function parseUrlState(initial: string | undefined): ExploreUrlState {
|
||||
: DEFAULT_UI_STATE;
|
||||
|
||||
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 {
|
||||
|
@ -2,15 +2,7 @@
|
||||
import _ from 'lodash';
|
||||
|
||||
// Services & Utils
|
||||
import {
|
||||
DataQuery,
|
||||
DataSourceApi,
|
||||
ExploreMode,
|
||||
dateTimeFormat,
|
||||
AppEvents,
|
||||
urlUtil,
|
||||
ExploreUrlState,
|
||||
} from '@grafana/data';
|
||||
import { DataQuery, DataSourceApi, dateTimeFormat, AppEvents, urlUtil, ExploreUrlState } from '@grafana/data';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import store from 'app/core/store';
|
||||
import { SortOrder } from './explore';
|
||||
@ -187,15 +179,6 @@ export const createUrlFromRichHistory = (query: RichHistoryQuery) => {
|
||||
range: { from: 'now-1h', to: 'now' },
|
||||
datasource: query.datasourceName,
|
||||
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: {
|
||||
showingGraph: true,
|
||||
showingLogs: true,
|
||||
|
@ -1,23 +1,12 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
DataSourceApi,
|
||||
LoadingState,
|
||||
ExploreMode,
|
||||
toUtc,
|
||||
DataQueryError,
|
||||
DataQueryRequest,
|
||||
CoreApp,
|
||||
MutableDataFrame,
|
||||
} from '@grafana/data';
|
||||
import { DataSourceApi, LoadingState, toUtc, DataQueryError, DataQueryRequest, CoreApp } from '@grafana/data';
|
||||
import { getFirstNonQueryRowSpecificError } from 'app/core/utils/explore';
|
||||
import { ExploreId } from 'app/types/explore';
|
||||
import { shallow } from 'enzyme';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
import { Explore, ExploreProps } from './Explore';
|
||||
import { scanStopAction } from './state/actionTypes';
|
||||
import { toggleGraph } from './state/actions';
|
||||
import { SecondaryActions } from './SecondaryActions';
|
||||
import { TraceView } from './TraceView/TraceView';
|
||||
import { getTheme } from '@grafana/ui';
|
||||
|
||||
const dummyProps: ExploreProps = {
|
||||
@ -64,7 +53,6 @@ const dummyProps: ExploreProps = {
|
||||
to: 'now',
|
||||
},
|
||||
},
|
||||
mode: ExploreMode.Metrics,
|
||||
initialUI: {
|
||||
showingTable: false,
|
||||
showingGraph: false,
|
||||
@ -119,6 +107,10 @@ const dummyProps: ExploreProps = {
|
||||
originPanelId: 1,
|
||||
addQueryRow: jest.fn(),
|
||||
theme: getTheme(),
|
||||
showMetrics: true,
|
||||
showLogs: true,
|
||||
showTable: true,
|
||||
showTrace: true,
|
||||
};
|
||||
|
||||
const setupErrors = (hasRefId?: boolean) => {
|
||||
@ -144,34 +136,6 @@ describe('Explore', () => {
|
||||
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 () => {
|
||||
const queryErrors = setupErrors(true);
|
||||
const queryError = getFirstNonQueryRowSpecificError(queryErrors);
|
||||
|
@ -11,7 +11,6 @@ import {
|
||||
AbsoluteTimeRange,
|
||||
DataQuery,
|
||||
DataSourceApi,
|
||||
ExploreMode,
|
||||
GrafanaTheme,
|
||||
GraphSeriesXY,
|
||||
LoadingState,
|
||||
@ -21,6 +20,7 @@ import {
|
||||
TimeZone,
|
||||
ExploreUIState,
|
||||
ExploreUrlState,
|
||||
LogsModel,
|
||||
} from '@grafana/data';
|
||||
|
||||
import store from 'app/core/store';
|
||||
@ -58,6 +58,7 @@ import { getTimeZone } from '../profile/state/selectors';
|
||||
import { ErrorContainer } from './ErrorContainer';
|
||||
import { scanStopAction } from './state/actionTypes';
|
||||
import { ExploreGraphPanel } from './ExploreGraphPanel';
|
||||
//TODO:unification
|
||||
import { TraceView } from './TraceView/TraceView';
|
||||
import { SecondaryActions } from './SecondaryActions';
|
||||
import { FILTER_FOR_OPERATOR, FILTER_OUT_OPERATOR, FilterItem } from '@grafana/ui/src/components/Table/types';
|
||||
@ -104,12 +105,12 @@ export interface ExploreProps {
|
||||
initialDatasource: string;
|
||||
initialQueries: DataQuery[];
|
||||
initialRange: TimeRange;
|
||||
mode: ExploreMode;
|
||||
initialUI: ExploreUIState;
|
||||
isLive: boolean;
|
||||
syncedTimes: boolean;
|
||||
updateTimeRange: typeof updateTimeRange;
|
||||
graphResult?: GraphSeriesXY[] | null;
|
||||
logsResult?: LogsModel;
|
||||
loading?: boolean;
|
||||
absoluteRange: AbsoluteTimeRange;
|
||||
showingGraph?: boolean;
|
||||
@ -121,6 +122,10 @@ export interface ExploreProps {
|
||||
originPanelId: number;
|
||||
addQueryRow: typeof addQueryRow;
|
||||
theme: GrafanaTheme;
|
||||
showMetrics: boolean;
|
||||
showTable: boolean;
|
||||
showLogs: boolean;
|
||||
showTrace: boolean;
|
||||
}
|
||||
|
||||
interface ExploreState {
|
||||
@ -170,7 +175,6 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
||||
initialDatasource,
|
||||
initialQueries,
|
||||
initialRange,
|
||||
mode,
|
||||
initialUI,
|
||||
originPanelId,
|
||||
} = this.props;
|
||||
@ -183,7 +187,6 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
||||
initialDatasource,
|
||||
initialQueries,
|
||||
initialRange,
|
||||
mode,
|
||||
width,
|
||||
this.exploreEvents,
|
||||
initialUI,
|
||||
@ -301,7 +304,6 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
||||
exploreId,
|
||||
split,
|
||||
queryKeys,
|
||||
mode,
|
||||
graphResult,
|
||||
loading,
|
||||
absoluteRange,
|
||||
@ -312,6 +314,10 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
||||
syncedTimes,
|
||||
isLive,
|
||||
theme,
|
||||
showMetrics,
|
||||
showTable,
|
||||
showLogs,
|
||||
showTrace,
|
||||
} = this.props;
|
||||
const { showRichHistory } = this.state;
|
||||
const exploreClass = split ? 'explore explore-split' : 'explore';
|
||||
@ -334,7 +340,8 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
||||
<SecondaryActions
|
||||
addQueryRowButtonDisabled={isLive}
|
||||
// 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}
|
||||
onClickAddQueryRowButton={this.onClickAddQueryRowButton}
|
||||
onClickRichHistoryButton={this.toggleShowRichHistory}
|
||||
@ -355,14 +362,13 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
||||
<StartPage
|
||||
onClickExample={this.onClickExample}
|
||||
datasource={datasourceInstance}
|
||||
exploreMode={mode}
|
||||
exploreId={exploreId}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{!showStartPage && (
|
||||
<>
|
||||
{mode === ExploreMode.Metrics && (
|
||||
{showMetrics && (
|
||||
<ExploreGraphPanel
|
||||
series={graphResult}
|
||||
width={width}
|
||||
@ -379,7 +385,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
||||
showLines={true}
|
||||
/>
|
||||
)}
|
||||
{mode === ExploreMode.Metrics && (
|
||||
{showTable && (
|
||||
<TableContainer
|
||||
width={width}
|
||||
exploreId={exploreId}
|
||||
@ -388,7 +394,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{mode === ExploreMode.Logs && (
|
||||
{showLogs && (
|
||||
<LogsContainer
|
||||
width={width}
|
||||
exploreId={exploreId}
|
||||
@ -399,7 +405,8 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
||||
onStopScanning={this.onStopScanning}
|
||||
/>
|
||||
)}
|
||||
{mode === ExploreMode.Tracing &&
|
||||
{/* TODO:unification */}
|
||||
{showTrace &&
|
||||
// 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
|
||||
queryResponse.series[0] && (
|
||||
@ -442,9 +449,12 @@ function mapStateToProps(state: StoreState, { exploreId }: ExploreProps): Partia
|
||||
urlState,
|
||||
update,
|
||||
isLive,
|
||||
supportedModes,
|
||||
mode,
|
||||
graphResult,
|
||||
logsResult,
|
||||
showLogs,
|
||||
showMetrics,
|
||||
showTable,
|
||||
showTrace,
|
||||
loading,
|
||||
showingGraph,
|
||||
showingTable,
|
||||
@ -452,31 +462,13 @@ function mapStateToProps(state: StoreState, { exploreId }: ExploreProps): Partia
|
||||
queryResponse,
|
||||
} = item;
|
||||
|
||||
const { datasource, queries, range: urlRange, mode: urlMode, ui, originPanelId } = (urlState ||
|
||||
{}) as ExploreUrlState;
|
||||
const { datasource, queries, range: urlRange, ui, originPanelId } = (urlState || {}) as ExploreUrlState;
|
||||
const initialDatasource = datasource || store.get(lastUsedDatasourceKeyForOrgId(state.user.orgId));
|
||||
const initialQueries: DataQuery[] = ensureQueriesMemoized(queries);
|
||||
const initialRange = urlRange
|
||||
? getTimeRangeFromUrlMemoized(urlRange, timeZone)
|
||||
: 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;
|
||||
|
||||
return {
|
||||
@ -489,10 +481,10 @@ function mapStateToProps(state: StoreState, { exploreId }: ExploreProps): Partia
|
||||
initialDatasource,
|
||||
initialQueries,
|
||||
initialRange,
|
||||
mode: newMode,
|
||||
initialUI,
|
||||
isLive,
|
||||
graphResult,
|
||||
graphResult: graphResult ?? undefined,
|
||||
logsResult: logsResult ?? undefined,
|
||||
loading,
|
||||
showingGraph,
|
||||
showingTable,
|
||||
@ -501,6 +493,10 @@ function mapStateToProps(state: StoreState, { exploreId }: ExploreProps): Partia
|
||||
originPanelId,
|
||||
syncedTimes,
|
||||
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 { ExploreId, ExploreItemState } from 'app/types/explore';
|
||||
import { Icon, IconButton, LegacyForms, SetInterval, ToggleButton, ToggleButtonGroup, Tooltip } from '@grafana/ui';
|
||||
import { DataQuery, ExploreMode, RawTimeRange, TimeRange, TimeZone } from '@grafana/data';
|
||||
import { Icon, IconButton, LegacyForms, SetInterval, Tooltip } from '@grafana/ui';
|
||||
import { DataQuery, RawTimeRange, TimeRange, TimeZone } from '@grafana/data';
|
||||
import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker';
|
||||
import { StoreState } from 'app/types/store';
|
||||
import {
|
||||
cancelQueries,
|
||||
changeDatasource,
|
||||
changeMode,
|
||||
changeRefreshInterval,
|
||||
clearQueries,
|
||||
runQueries,
|
||||
@ -60,8 +59,6 @@ interface StateProps {
|
||||
splitted: boolean;
|
||||
syncedTimes: boolean;
|
||||
refreshInterval?: string;
|
||||
supportedModes: ExploreMode[];
|
||||
selectedMode: ExploreMode;
|
||||
hasLiveOption: boolean;
|
||||
isLive: boolean;
|
||||
isPaused: boolean;
|
||||
@ -81,7 +78,6 @@ interface DispatchProps {
|
||||
split: typeof splitOpen;
|
||||
syncTimes: typeof syncTimes;
|
||||
changeRefreshInterval: typeof changeRefreshInterval;
|
||||
changeMode: typeof changeMode;
|
||||
updateLocation: typeof updateLocation;
|
||||
setDashboardQueriesToUpdateOnLoad: typeof setDashboardQueriesToUpdateOnLoad;
|
||||
onChangeTimeZone: typeof updateTimeZoneForSession;
|
||||
@ -111,11 +107,6 @@ export class UnConnectedExploreToolbar extends PureComponent<Props> {
|
||||
changeRefreshInterval(exploreId, item);
|
||||
};
|
||||
|
||||
onModeChange = (mode: ExploreMode) => {
|
||||
const { changeMode, exploreId } = this.props;
|
||||
changeMode(exploreId, mode);
|
||||
};
|
||||
|
||||
onChangeTimeSync = () => {
|
||||
const { syncTimes, exploreId } = this.props;
|
||||
syncTimes(exploreId);
|
||||
@ -174,8 +165,6 @@ export class UnConnectedExploreToolbar extends PureComponent<Props> {
|
||||
refreshInterval,
|
||||
onChangeTime,
|
||||
split,
|
||||
supportedModes,
|
||||
selectedMode,
|
||||
hasLiveOption,
|
||||
isLive,
|
||||
isPaused,
|
||||
@ -195,8 +184,6 @@ export class UnConnectedExploreToolbar extends PureComponent<Props> {
|
||||
const showSmallDataSourcePicker = (splitted ? containerWidth < 700 : containerWidth < 800) || false;
|
||||
const showSmallTimePicker = splitted || containerWidth < 1210;
|
||||
|
||||
const showModeToggle = supportedModes.length > 1;
|
||||
|
||||
return (
|
||||
<div className={splitted ? 'explore-toolbar splitted' : 'explore-toolbar'}>
|
||||
<div className="explore-toolbar-item">
|
||||
@ -239,26 +226,6 @@ export class UnConnectedExploreToolbar extends PureComponent<Props> {
|
||||
hideTextValue={showSmallDataSourcePicker}
|
||||
/>
|
||||
</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>
|
||||
) : null}
|
||||
|
||||
@ -369,8 +336,6 @@ const mapStateToProps = (state: StoreState, { exploreId }: OwnProps): StateProps
|
||||
range,
|
||||
refreshInterval,
|
||||
loading,
|
||||
supportedModes,
|
||||
mode,
|
||||
isLive,
|
||||
isPaused,
|
||||
originPanelId,
|
||||
@ -379,7 +344,7 @@ const mapStateToProps = (state: StoreState, { exploreId }: OwnProps): StateProps
|
||||
containerWidth,
|
||||
} = exploreItem;
|
||||
|
||||
const hasLiveOption = !!(datasourceInstance?.meta?.streaming && mode === ExploreMode.Logs);
|
||||
const hasLiveOption = !!datasourceInstance?.meta?.streaming;
|
||||
|
||||
return {
|
||||
datasourceMissing,
|
||||
@ -389,15 +354,13 @@ const mapStateToProps = (state: StoreState, { exploreId }: OwnProps): StateProps
|
||||
timeZone: getTimeZone(state.user),
|
||||
splitted,
|
||||
refreshInterval,
|
||||
supportedModes,
|
||||
selectedMode: supportedModes.includes(mode) ? mode : supportedModes[0],
|
||||
hasLiveOption,
|
||||
isLive,
|
||||
isPaused,
|
||||
originPanelId,
|
||||
queries,
|
||||
syncedTimes,
|
||||
datasourceLoading,
|
||||
datasourceLoading: datasourceLoading ?? undefined,
|
||||
containerWidth,
|
||||
};
|
||||
};
|
||||
@ -412,7 +375,6 @@ const mapDispatchToProps: DispatchProps = {
|
||||
closeSplit: splitClose,
|
||||
split: splitOpen,
|
||||
syncTimes,
|
||||
changeMode: changeMode,
|
||||
setDashboardQueriesToUpdateOnLoad,
|
||||
onChangeTimeZone: updateTimeZoneForSession,
|
||||
};
|
||||
|
@ -62,7 +62,15 @@ interface LogsContainerProps {
|
||||
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) => {
|
||||
const { exploreId, updateTimeRange } = this.props;
|
||||
updateTimeRange({ exploreId, absoluteRange });
|
||||
@ -94,6 +102,12 @@ export class LogsContainer extends PureComponent<LogsContainerProps> {
|
||||
return getFieldLinksForExplore(field, rowIndex, this.props.splitOpen, this.props.range);
|
||||
};
|
||||
|
||||
onToggleCollapse = () => {
|
||||
this.setState(state => ({
|
||||
logsContainerOpen: !state.logsContainerOpen,
|
||||
}));
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
loading,
|
||||
@ -116,6 +130,8 @@ export class LogsContainer extends PureComponent<LogsContainerProps> {
|
||||
exploreId,
|
||||
} = this.props;
|
||||
|
||||
const { logsContainerOpen } = this.state;
|
||||
|
||||
return (
|
||||
<>
|
||||
<LogsCrossFadeTransition visible={isLive}>
|
||||
@ -135,7 +151,13 @@ export class LogsContainer extends PureComponent<LogsContainerProps> {
|
||||
</Collapse>
|
||||
</LogsCrossFadeTransition>
|
||||
<LogsCrossFadeTransition visible={!isLive}>
|
||||
<Collapse label="Logs" loading={loading} isOpen>
|
||||
<Collapse
|
||||
label="Logs"
|
||||
loading={loading}
|
||||
isOpen={logsContainerOpen}
|
||||
onToggle={this.onToggleCollapse}
|
||||
collapsible
|
||||
>
|
||||
<Logs
|
||||
dedupStrategy={this.props.dedupStrategy || LogsDedupStrategy.none}
|
||||
logRows={logRows}
|
||||
|
@ -3,7 +3,7 @@ import { QueryRow, QueryRowProps } from './QueryRow';
|
||||
import { shallow } from 'enzyme';
|
||||
import { ExploreId } from 'app/types/explore';
|
||||
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 props: QueryRowProps = {
|
||||
@ -23,7 +23,6 @@ const setup = (propOverrides?: object) => {
|
||||
removeQueryRowAction: jest.fn() as any,
|
||||
runQueries: jest.fn(),
|
||||
queryResponse: {} as PanelData,
|
||||
mode: ExploreMode.Metrics,
|
||||
latency: 1,
|
||||
};
|
||||
|
||||
@ -33,34 +32,9 @@ const setup = (propOverrides?: object) => {
|
||||
return wrapper;
|
||||
};
|
||||
|
||||
const ExploreMetricsQueryField = () => <div />;
|
||||
const ExploreLogsQueryField = () => <div />;
|
||||
const ExploreQueryField = () => <div />;
|
||||
const QueryEditor = () => <div />;
|
||||
|
||||
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 ', () => {
|
||||
it('it should render QueryEditor if datasource has it', () => {
|
||||
const wrapper = setup({ datasourceInstance: { components: { QueryEditor } } });
|
||||
|
@ -20,7 +20,6 @@ import {
|
||||
TimeRange,
|
||||
AbsoluteTimeRange,
|
||||
LoadingState,
|
||||
ExploreMode,
|
||||
} from '@grafana/data';
|
||||
|
||||
import { ExploreItemState, ExploreId } from 'app/types/explore';
|
||||
@ -48,7 +47,6 @@ export interface QueryRowProps extends PropsFromParent {
|
||||
removeQueryRowAction: typeof removeQueryRowAction;
|
||||
runQueries: typeof runQueries;
|
||||
queryResponse: PanelData;
|
||||
mode: ExploreMode;
|
||||
latency: number;
|
||||
}
|
||||
|
||||
@ -102,12 +100,13 @@ export class QueryRow extends PureComponent<QueryRowProps, QueryRowState> {
|
||||
};
|
||||
|
||||
setReactQueryEditor = () => {
|
||||
const { mode, datasourceInstance } = this.props;
|
||||
const { datasourceInstance } = this.props;
|
||||
let QueryEditor;
|
||||
|
||||
if (mode === ExploreMode.Metrics && datasourceInstance.components?.ExploreMetricsQueryField) {
|
||||
// TODO:unification
|
||||
if (datasourceInstance.components?.ExploreMetricsQueryField) {
|
||||
QueryEditor = datasourceInstance.components.ExploreMetricsQueryField;
|
||||
} else if (mode === ExploreMode.Logs && datasourceInstance.components?.ExploreLogsQueryField) {
|
||||
} else if (datasourceInstance.components?.ExploreLogsQueryField) {
|
||||
QueryEditor = datasourceInstance.components.ExploreLogsQueryField;
|
||||
} else if (datasourceInstance.components?.ExploreQueryField) {
|
||||
QueryEditor = datasourceInstance.components.ExploreQueryField;
|
||||
@ -126,7 +125,6 @@ export class QueryRow extends PureComponent<QueryRowProps, QueryRowState> {
|
||||
range,
|
||||
absoluteRange,
|
||||
queryResponse,
|
||||
mode,
|
||||
exploreId,
|
||||
} = this.props;
|
||||
|
||||
@ -145,7 +143,6 @@ export class QueryRow extends PureComponent<QueryRowProps, QueryRowState> {
|
||||
onChange={this.onChange}
|
||||
data={queryResponse}
|
||||
absoluteRange={absoluteRange}
|
||||
exploreMode={mode}
|
||||
exploreId={exploreId}
|
||||
/>
|
||||
);
|
||||
@ -174,10 +171,9 @@ export class QueryRow extends PureComponent<QueryRowProps, QueryRowState> {
|
||||
}, 500);
|
||||
|
||||
render() {
|
||||
const { datasourceInstance, query, queryResponse, mode, latency } = this.props;
|
||||
const { datasourceInstance, query, queryResponse, latency } = this.props;
|
||||
|
||||
const canToggleEditorModes =
|
||||
mode === ExploreMode.Metrics && has(datasourceInstance, 'components.QueryCtrl.prototype.toggleEditorMode');
|
||||
const canToggleEditorModes = has(datasourceInstance, 'components.QueryCtrl.prototype.toggleEditorMode');
|
||||
const isNotStarted = queryResponse.state === LoadingState.NotStarted;
|
||||
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) {
|
||||
const explore = state.explore;
|
||||
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];
|
||||
|
||||
return {
|
||||
@ -214,7 +210,6 @@ function mapStateToProps(state: StoreState, { exploreId, index }: QueryRowProps)
|
||||
range,
|
||||
absoluteRange,
|
||||
queryResponse,
|
||||
mode,
|
||||
latency,
|
||||
};
|
||||
}
|
||||
|
@ -13,7 +13,6 @@ import {
|
||||
PanelData,
|
||||
QueryFixAction,
|
||||
TimeRange,
|
||||
ExploreMode,
|
||||
ExploreUIState,
|
||||
} from '@grafana/data';
|
||||
import { ExploreId, ExploreItemState } from 'app/types/explore';
|
||||
@ -24,11 +23,6 @@ export interface AddQueryRowPayload {
|
||||
query: DataQuery;
|
||||
}
|
||||
|
||||
export interface ChangeModePayload {
|
||||
exploreId: ExploreId;
|
||||
mode: ExploreMode;
|
||||
}
|
||||
|
||||
export interface ChangeQueryPayload {
|
||||
exploreId: ExploreId;
|
||||
query: DataQuery;
|
||||
@ -62,7 +56,6 @@ export interface InitializeExplorePayload {
|
||||
eventBridge: Emitter;
|
||||
queries: DataQuery[];
|
||||
range: TimeRange;
|
||||
mode: ExploreMode;
|
||||
ui: ExploreUIState;
|
||||
originPanelId?: number | null;
|
||||
}
|
||||
@ -149,7 +142,6 @@ export interface UpdateDatasourceInstancePayload {
|
||||
exploreId: ExploreId;
|
||||
datasourceInstance: DataSourceApi;
|
||||
version?: string;
|
||||
mode?: ExploreMode;
|
||||
}
|
||||
|
||||
export interface ToggleLogLevelPayload {
|
||||
@ -191,11 +183,6 @@ export interface ResetExplorePayload {
|
||||
*/
|
||||
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.
|
||||
* 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 { 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,
|
||||
changeDatasource,
|
||||
changeMode,
|
||||
loadDatasource,
|
||||
navigateToExplore,
|
||||
refreshExplore,
|
||||
} from './actions';
|
||||
import { cancelQueries, loadDatasource, navigateToExplore, refreshExplore } from './actions';
|
||||
import { ExploreId, ExploreUpdateState } from 'app/types';
|
||||
import { thunkTester } from 'test/core/thunk/thunkTester';
|
||||
import {
|
||||
cancelQueriesAction,
|
||||
changeModeAction,
|
||||
initializeExploreAction,
|
||||
InitializeExplorePayload,
|
||||
loadDatasourcePendingAction,
|
||||
loadDatasourceReadyAction,
|
||||
scanStopAction,
|
||||
setQueriesAction,
|
||||
updateDatasourceInstanceAction,
|
||||
updateUIStateAction,
|
||||
} from './actionTypes';
|
||||
import { Emitter } from 'app/core/core';
|
||||
@ -80,7 +70,6 @@ const setup = (updateOverides?: Partial<ExploreUpdateState>) => {
|
||||
datasource: 'some-datasource',
|
||||
queries: [],
|
||||
range: range.raw,
|
||||
mode: ExploreMode.Metrics,
|
||||
ui,
|
||||
};
|
||||
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('when loadDatasource thunk is dispatched', () => {
|
||||
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 url = 'http://www.someurl.com';
|
||||
const panel: Partial<PanelModel> = {
|
||||
|
@ -16,7 +16,6 @@ import {
|
||||
QueryFixAction,
|
||||
RawTimeRange,
|
||||
TimeRange,
|
||||
ExploreMode,
|
||||
ExploreUrlState,
|
||||
ExploreUIState,
|
||||
} from '@grafana/data';
|
||||
@ -53,7 +52,6 @@ import { ExploreItemState, ThunkResult } from 'app/types';
|
||||
import { ExploreId, QueryOptions } from 'app/types/explore';
|
||||
import {
|
||||
addQueryRowAction,
|
||||
changeModeAction,
|
||||
changeQueryAction,
|
||||
changeRangeAction,
|
||||
changeRefreshIntervalAction,
|
||||
@ -141,16 +139,11 @@ export function changeDatasource(
|
||||
const orgId = getState().user.orgId;
|
||||
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(
|
||||
updateDatasourceInstanceAction({
|
||||
exploreId,
|
||||
datasourceInstance: newDataSourceInstance,
|
||||
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.
|
||||
* 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,
|
||||
queries: DataQuery[],
|
||||
range: TimeRange,
|
||||
mode: ExploreMode,
|
||||
containerWidth: number,
|
||||
eventBridge: Emitter,
|
||||
ui: ExploreUIState,
|
||||
@ -306,7 +288,6 @@ export function initializeExplore(
|
||||
eventBridge,
|
||||
queries,
|
||||
range,
|
||||
mode,
|
||||
ui,
|
||||
originPanelId,
|
||||
})
|
||||
@ -444,7 +425,6 @@ export const runQueries = (exploreId: ExploreId): ThunkResult<void> => {
|
||||
queryResponse,
|
||||
querySubscription,
|
||||
history,
|
||||
mode,
|
||||
showingGraph,
|
||||
showingTable,
|
||||
} = exploreItemState;
|
||||
@ -461,11 +441,11 @@ export const runQueries = (exploreId: ExploreId): ThunkResult<void> => {
|
||||
|
||||
// Some datasource's query builders allow per-query interval limits,
|
||||
// but we're using the datasource interval limit for now
|
||||
const minInterval = datasourceInstance.interval;
|
||||
const minInterval = datasourceInstance?.interval;
|
||||
|
||||
stopQueryState(querySubscription);
|
||||
|
||||
const datasourceId = datasourceInstance.meta.id;
|
||||
const datasourceId = datasourceInstance?.meta.id;
|
||||
|
||||
const queryOptions: QueryOptions = {
|
||||
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.
|
||||
// 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
|
||||
maxDataPoints: mode === ExploreMode.Logs && datasourceId === 'loki' ? undefined : containerWidth,
|
||||
// TODO:unification
|
||||
// maxDataPoints: mode === ExploreMode.Logs && datasourceId === 'loki' ? undefined : containerWidth,
|
||||
maxDataPoints: containerWidth,
|
||||
liveStreaming: live,
|
||||
showingGraph,
|
||||
showingTable,
|
||||
mode,
|
||||
};
|
||||
|
||||
const datasourceName = exploreItemState.requestedDatasourceName;
|
||||
@ -591,7 +572,6 @@ export const stateSave = (): ThunkResult<void> => {
|
||||
datasource: left.datasourceInstance!.name,
|
||||
queries: left.queries.map(clearQueryKeys),
|
||||
range: toRawTimeRange(left.range),
|
||||
mode: left.mode,
|
||||
ui: {
|
||||
showingGraph: left.showingGraph,
|
||||
showingLogs: true,
|
||||
@ -605,7 +585,6 @@ export const stateSave = (): ThunkResult<void> => {
|
||||
datasource: right.datasourceInstance!.name,
|
||||
queries: right.queries.map(clearQueryKeys),
|
||||
range: toRawTimeRange(right.range),
|
||||
mode: right.mode,
|
||||
ui: {
|
||||
showingGraph: right.showingGraph,
|
||||
showingLogs: true,
|
||||
@ -837,7 +816,7 @@ export function refreshExplore(exploreId: ExploreId): ThunkResult<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
const { datasource, queries, range: urlRange, mode, ui, originPanelId } = urlState;
|
||||
const { datasource, queries, range: urlRange, ui, originPanelId } = urlState;
|
||||
const refreshQueries: DataQuery[] = [];
|
||||
|
||||
for (let index = 0; index < queries.length; index++) {
|
||||
@ -852,17 +831,7 @@ export function refreshExplore(exploreId: ExploreId): ThunkResult<void> {
|
||||
if (update.datasource) {
|
||||
const initialQueries = ensureQueries(queries);
|
||||
dispatch(
|
||||
initializeExplore(
|
||||
exploreId,
|
||||
datasource,
|
||||
initialQueries,
|
||||
range,
|
||||
mode,
|
||||
containerWidth,
|
||||
eventBridge,
|
||||
ui,
|
||||
originPanelId
|
||||
)
|
||||
initializeExplore(exploreId, datasource, initialQueries, range, containerWidth, eventBridge, ui, originPanelId)
|
||||
);
|
||||
return;
|
||||
}
|
||||
@ -881,11 +850,6 @@ export function refreshExplore(exploreId: ExploreId): ThunkResult<void> {
|
||||
dispatch(setQueriesAction({ exploreId, queries: refreshQueries }));
|
||||
}
|
||||
|
||||
// need to refresh mode
|
||||
if (update.mode) {
|
||||
dispatch(changeModeAction({ exploreId, mode }));
|
||||
}
|
||||
|
||||
// always run queries when refresh is needed
|
||||
if (update.queries || update.ui || update.range) {
|
||||
dispatch(runQueries(exploreId));
|
||||
|
@ -22,7 +22,6 @@ import {
|
||||
import { ExploreId, ExploreItemState, ExploreState } from 'app/types/explore';
|
||||
import { reducerTester } from 'test/core/redux/reducerTester';
|
||||
import {
|
||||
changeModeAction,
|
||||
changeRangeAction,
|
||||
changeRefreshIntervalAction,
|
||||
scanStartAction,
|
||||
@ -75,21 +74,6 @@ describe('Explore item reducer', () => {
|
||||
});
|
||||
|
||||
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('and datasourceInstance supports graph, logs, table and has a startpage', () => {
|
||||
it('then it should set correct state', () => {
|
||||
@ -118,7 +102,6 @@ describe('Explore item reducer', () => {
|
||||
logsResult: null,
|
||||
tableResult: null,
|
||||
supportedModes: [ExploreMode.Metrics, ExploreMode.Logs],
|
||||
mode: ExploreMode.Metrics,
|
||||
latency: 0,
|
||||
loading: false,
|
||||
queryResponse: createEmptyQueryResponse(),
|
||||
@ -185,7 +168,7 @@ describe('Explore item reducer', () => {
|
||||
.whenActionIsDispatched(toggleGraphAction({ exploreId: ExploreId.left }))
|
||||
.thenStateShouldEqual(({ showingGraph: true, graphResult: [] } as unknown) as ExploreItemState)
|
||||
.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 }))
|
||||
.thenStateShouldEqual(({ showingTable: true, tableResult: table } as unknown) as ExploreItemState)
|
||||
.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: '',
|
||||
to: '',
|
||||
},
|
||||
mode: ExploreMode.Metrics,
|
||||
ui: {
|
||||
dedupStrategy: LogsDedupStrategy.none,
|
||||
showingGraph: false,
|
||||
|
@ -30,7 +30,6 @@ import { ExploreId, ExploreItemState, ExploreState, ExploreUpdateState } from 'a
|
||||
import {
|
||||
addQueryRowAction,
|
||||
changeLoadingStateAction,
|
||||
changeModeAction,
|
||||
changeQueryAction,
|
||||
changeRangeAction,
|
||||
changeRefreshIntervalAction,
|
||||
@ -114,7 +113,6 @@ export const makeExploreItemState = (): ExploreItemState => ({
|
||||
update: makeInitialUpdateState(),
|
||||
latency: 0,
|
||||
supportedModes: [],
|
||||
mode: (null as unknown) as ExploreMode,
|
||||
isLive: false,
|
||||
isPaused: false,
|
||||
urlReplaced: false,
|
||||
@ -189,18 +187,6 @@ export const itemReducer = (state: ExploreItemState = makeExploreItemState(), ac
|
||||
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)) {
|
||||
const { refreshInterval } = action.payload;
|
||||
const live = RefreshPicker.isLive(refreshInterval);
|
||||
@ -255,13 +241,12 @@ export const itemReducer = (state: ExploreItemState = makeExploreItemState(), ac
|
||||
}
|
||||
|
||||
if (initializeExploreAction.match(action)) {
|
||||
const { containerWidth, eventBridge, queries, range, mode, ui, originPanelId } = action.payload;
|
||||
const { containerWidth, eventBridge, queries, range, ui, originPanelId } = action.payload;
|
||||
return {
|
||||
...state,
|
||||
containerWidth,
|
||||
eventBridge,
|
||||
range,
|
||||
mode,
|
||||
queries,
|
||||
initialized: true,
|
||||
queryKeys: getQueryKeys(queries, state.datasourceInstance),
|
||||
@ -272,7 +257,7 @@ export const itemReducer = (state: ExploreItemState = makeExploreItemState(), ac
|
||||
}
|
||||
|
||||
if (updateDatasourceInstanceAction.match(action)) {
|
||||
const { datasourceInstance, version, mode } = action.payload;
|
||||
const { datasourceInstance, version } = action.payload;
|
||||
|
||||
// Custom components
|
||||
stopQueryState(state.querySubscription);
|
||||
@ -294,7 +279,7 @@ export const itemReducer = (state: ExploreItemState = makeExploreItemState(), ac
|
||||
}
|
||||
|
||||
const updatedDatasourceInstance = Object.assign(datasourceInstance, { meta: newMetadata });
|
||||
const [supportedModes, newMode] = getModesForDatasource(updatedDatasourceInstance, state.mode);
|
||||
const supportedModes = getModesForDatasource(updatedDatasourceInstance);
|
||||
|
||||
return {
|
||||
...state,
|
||||
@ -307,7 +292,6 @@ export const itemReducer = (state: ExploreItemState = makeExploreItemState(), ac
|
||||
loading: false,
|
||||
queryKeys: [],
|
||||
supportedModes,
|
||||
mode: mode ?? newMode,
|
||||
originPanelId: state.urlState && state.urlState.originPanelId,
|
||||
};
|
||||
}
|
||||
@ -430,7 +414,7 @@ export const itemReducer = (state: ExploreItemState = makeExploreItemState(), ac
|
||||
return { ...state, showingGraph };
|
||||
}
|
||||
|
||||
return { ...state, showingGraph, graphResult: null };
|
||||
return { ...state, showingGraph };
|
||||
}
|
||||
|
||||
if (toggleTableAction.match(action)) {
|
||||
@ -439,7 +423,7 @@ export const itemReducer = (state: ExploreItemState = makeExploreItemState(), ac
|
||||
return { ...state, showingTable };
|
||||
}
|
||||
|
||||
return { ...state, showingTable, tableResult: null };
|
||||
return { ...state, showingTable };
|
||||
}
|
||||
|
||||
if (queriesImportedAction.match(action)) {
|
||||
@ -570,6 +554,10 @@ export const processQueryResponse = (
|
||||
logsResult,
|
||||
loading: loadingState === LoadingState.Loading || loadingState === LoadingState.Streaming,
|
||||
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 queries = _.isEqual(urlState ? urlState.queries : [], state.urlState.queries) === 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;
|
||||
|
||||
return {
|
||||
@ -612,18 +599,16 @@ export const updateChildRefreshState = (
|
||||
datasource,
|
||||
queries,
|
||||
range,
|
||||
mode,
|
||||
ui,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const getModesForDatasource = (dataSource: DataSourceApi, currentMode: ExploreMode): [ExploreMode[], ExploreMode] => {
|
||||
const getModesForDatasource = (dataSource: DataSourceApi): ExploreMode[] => {
|
||||
const supportsGraph = dataSource.meta.metrics;
|
||||
const supportsLogs = dataSource.meta.logs;
|
||||
const supportsTracing = dataSource.meta.tracing;
|
||||
|
||||
let mode = currentMode || ExploreMode.Metrics;
|
||||
const supportedModes: ExploreMode[] = [];
|
||||
|
||||
if (supportsGraph) {
|
||||
@ -638,17 +623,7 @@ const getModesForDatasource = (dataSource: DataSourceApi, currentMode: ExploreMo
|
||||
supportedModes.push(ExploreMode.Tracing);
|
||||
}
|
||||
|
||||
if (supportedModes.length === 1) {
|
||||
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];
|
||||
return supportedModes;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -6,7 +6,7 @@ jest.mock('@grafana/data/src/datetime/formatter', () => ({
|
||||
import { ResultProcessor } from './ResultProcessor';
|
||||
import { ExploreItemState } from 'app/types/explore';
|
||||
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 timeSeries = toDataFrame({
|
||||
@ -34,9 +34,20 @@ const testContext = (options: any = {}) => {
|
||||
|
||||
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 = {
|
||||
mode: ExploreMode.Metrics,
|
||||
dataFrames: [timeSeries, table, emptyTable],
|
||||
dataFrames: [timeSeries, table, emptyTable, logs],
|
||||
graphResult: [] as TimeSeries[],
|
||||
tableResult: new TableModel(),
|
||||
logsResult: { hasUniqueLabels: false, rows: [] as LogRowModel[] },
|
||||
@ -45,7 +56,6 @@ const testContext = (options: any = {}) => {
|
||||
const combinedOptions = { ...defaultOptions, ...options };
|
||||
|
||||
const state = ({
|
||||
mode: combinedOptions.mode,
|
||||
graphResult: combinedOptions.graphResult,
|
||||
tableResult: combinedOptions.tableResult,
|
||||
logsResult: combinedOptions.logsResult,
|
||||
@ -191,10 +201,9 @@ describe('ResultProcessor', () => {
|
||||
|
||||
describe('when calling getLogsResult', () => {
|
||||
it('then it should return correct logs result', () => {
|
||||
const { resultProcessor, dataFrames } = testContext({ mode: ExploreMode.Logs });
|
||||
const timeField = dataFrames[0].fields[0];
|
||||
const valueField = dataFrames[0].fields[1];
|
||||
const logsDataFrame = dataFrames[1];
|
||||
const { resultProcessor, dataFrames } = testContext({});
|
||||
const logsDataFrame = dataFrames[3];
|
||||
|
||||
const theResult = resultProcessor.getLogsResult();
|
||||
|
||||
expect(theResult).toEqual({
|
||||
@ -258,24 +267,37 @@ describe('ResultProcessor', () => {
|
||||
],
|
||||
series: [
|
||||
{
|
||||
label: 'A-series',
|
||||
color: '#7EB26D',
|
||||
data: [
|
||||
[100, 4],
|
||||
[200, 5],
|
||||
[300, 6],
|
||||
],
|
||||
info: [],
|
||||
label: 'unknown',
|
||||
color: '#8e8e8e',
|
||||
data: [[0, 3]],
|
||||
isVisible: true,
|
||||
yAxis: {
|
||||
index: 1,
|
||||
min: 0,
|
||||
tickDecimals: 0,
|
||||
},
|
||||
seriesIndex: 0,
|
||||
timeField,
|
||||
valueField,
|
||||
timeStep: 100,
|
||||
timeField: {
|
||||
name: 'Time',
|
||||
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,
|
||||
TimeZone,
|
||||
getDisplayProcessor,
|
||||
ExploreMode,
|
||||
PreferredVisualisationType,
|
||||
standardTransformers,
|
||||
} from '@grafana/data';
|
||||
@ -16,27 +15,51 @@ import { getGraphSeriesModel } from 'app/plugins/panel/graph2/getGraphSeriesMode
|
||||
import { config } from 'app/core/config';
|
||||
|
||||
export class ResultProcessor {
|
||||
graphFrames: DataFrame[] = [];
|
||||
tableFrames: DataFrame[] = [];
|
||||
logsFrames: DataFrame[] = [];
|
||||
traceFrames: DataFrame[] = [];
|
||||
|
||||
constructor(
|
||||
private state: ExploreItemState,
|
||||
private dataFrames: DataFrame[],
|
||||
private intervalMs: number,
|
||||
private timeZone: TimeZone
|
||||
) {}
|
||||
) {
|
||||
this.classifyFrames();
|
||||
}
|
||||
|
||||
private classifyFrames() {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getGraphResult(): GraphSeriesXY[] | null {
|
||||
if (this.state.mode !== ExploreMode.Metrics) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const onlyTimeSeries = this.dataFrames.filter(frame => isTimeSeries(frame, this.state.datasourceInstance?.meta.id));
|
||||
const timeSeriesToShowInGraph = onlyTimeSeries.filter(frame => shouldShowInVisualisationType(frame, 'graph'));
|
||||
|
||||
if (timeSeriesToShowInGraph.length === 0) {
|
||||
if (this.graphFrames.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return getGraphSeriesModel(
|
||||
timeSeriesToShowInGraph,
|
||||
this.graphFrames,
|
||||
this.timeZone,
|
||||
{},
|
||||
{ showBars: false, showLines: true, showPoints: false },
|
||||
@ -45,30 +68,24 @@ export class ResultProcessor {
|
||||
}
|
||||
|
||||
getTableResult(): DataFrame | null {
|
||||
if (this.state.mode !== ExploreMode.Metrics) {
|
||||
if (this.tableFrames.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const onlyTables = this.dataFrames
|
||||
.filter((frame: DataFrame) => shouldShowInVisualisationType(frame, 'table'))
|
||||
.sort((frameA: DataFrame, frameB: DataFrame) => {
|
||||
const frameARefId = frameA.refId!;
|
||||
const frameBRefId = frameB.refId!;
|
||||
this.tableFrames.sort((frameA: DataFrame, frameB: DataFrame) => {
|
||||
const frameARefId = frameA.refId!;
|
||||
const frameBRefId = frameB.refId!;
|
||||
|
||||
if (frameARefId > frameBRefId) {
|
||||
return 1;
|
||||
}
|
||||
if (frameARefId < frameBRefId) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
if (frameARefId > frameBRefId) {
|
||||
return 1;
|
||||
}
|
||||
if (frameARefId < frameBRefId) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
if (onlyTables.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const hasOnlyTimeseries = onlyTables.every(df => isTimeSeries(df));
|
||||
const hasOnlyTimeseries = this.tableFrames.every(df => isTimeSeries(df));
|
||||
|
||||
// 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
|
||||
@ -77,7 +94,7 @@ export class ResultProcessor {
|
||||
? standardTransformers.seriesToColumnsTransformer.transformer({})
|
||||
: standardTransformers.mergeTransformer.transformer({});
|
||||
|
||||
const data = transformer(onlyTables)[0];
|
||||
const data = transformer(this.tableFrames)[0];
|
||||
|
||||
// set display processor
|
||||
for (const field of data.fields) {
|
||||
@ -92,11 +109,11 @@ export class ResultProcessor {
|
||||
}
|
||||
|
||||
getLogsResult(): LogsModel | null {
|
||||
if (this.state.mode !== ExploreMode.Logs) {
|
||||
if (this.logsFrames.length === 0) {
|
||||
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 sortedNewResults = sortLogsResult(newResults, sortOrder);
|
||||
const rows = sortedNewResults.rows;
|
||||
@ -128,6 +145,10 @@ function shouldShowInVisualisationType(frame: DataFrame, visualisation: Preferre
|
||||
return true;
|
||||
}
|
||||
|
||||
function shouldShowInVisualisationTypeStrict(frame: DataFrame, visualisation: PreferredVisualisationType) {
|
||||
return frame.meta?.preferredVisualisationType === visualisation;
|
||||
}
|
||||
|
||||
// TEMP: Temporary hack. Remove when logs/metrics unification is done
|
||||
function isTimeSeriesCloudWatch(frame: DataFrame): boolean {
|
||||
return (
|
||||
|
@ -49,7 +49,7 @@ describe('getFieldLinksForExplore', () => {
|
||||
const links = getFieldLinksForExplore(field, 0, splitfn, range);
|
||||
|
||||
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');
|
||||
|
||||
|
@ -1,13 +1,11 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { stripIndent, stripIndents } from 'common-tags';
|
||||
import { ExploreStartPageProps, ExploreMode } from '@grafana/data';
|
||||
import { ExploreStartPageProps } from '@grafana/data';
|
||||
import Prism from 'prismjs';
|
||||
import tokenizer from '../syntax';
|
||||
import { flattenTokens } from '@grafana/ui/src/slate-plugins/slate-prism';
|
||||
import { css, cx } from 'emotion';
|
||||
import { CloudWatchLogsQuery } from '../types';
|
||||
import { changeModeAction } from 'app/features/explore/state/actionTypes';
|
||||
import { dispatch } from 'app/store/store';
|
||||
|
||||
interface QueryExample {
|
||||
category: string;
|
||||
@ -217,19 +215,8 @@ const exampleCategory = css`
|
||||
`;
|
||||
|
||||
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) {
|
||||
if (query.expression?.includes('stats')) {
|
||||
this.switchToMetrics(query);
|
||||
} else {
|
||||
this.props.onClickExample(query);
|
||||
}
|
||||
this.props.onClickExample(query);
|
||||
}
|
||||
|
||||
renderExpression(expr: string, keyPrefix: string) {
|
||||
|
@ -20,7 +20,7 @@ const labelClass = css`
|
||||
`;
|
||||
|
||||
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;
|
||||
if (data?.request?.range?.from) {
|
||||
@ -44,7 +44,6 @@ export const CloudWatchLogsQueryEditor = memo(function CloudWatchLogsQueryEditor
|
||||
return (
|
||||
<CloudWatchLogsQueryField
|
||||
exploreId={exploreId}
|
||||
exploreMode={exploreMode}
|
||||
datasource={datasource}
|
||||
query={query}
|
||||
onBlur={() => {}}
|
||||
|
@ -14,23 +14,20 @@ import {
|
||||
Select,
|
||||
MultiSelect,
|
||||
} from '@grafana/ui';
|
||||
import Plain from 'slate-plain-serializer';
|
||||
|
||||
// Utils & Services
|
||||
// dom also includes Element polyfills
|
||||
import { Plugin, Node, Editor, Value } from 'slate';
|
||||
import { Plugin, Node, Editor } from 'slate';
|
||||
import syntax from '../syntax';
|
||||
|
||||
// Types
|
||||
import { ExploreQueryFieldProps, AbsoluteTimeRange, SelectableValue, ExploreMode, AppEvents } from '@grafana/data';
|
||||
import { ExploreQueryFieldProps, AbsoluteTimeRange, SelectableValue, AppEvents } from '@grafana/data';
|
||||
import { CloudWatchQuery, CloudWatchLogsQuery } from '../types';
|
||||
import { CloudWatchDatasource } from '../datasource';
|
||||
import Prism, { Grammar } from 'prismjs';
|
||||
import { CloudWatchLanguageProvider } from '../language_provider';
|
||||
import { css } from 'emotion';
|
||||
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 { InputActionMeta } from '@grafana/ui/src/components/Select/types';
|
||||
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) => {
|
||||
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() {
|
||||
const { ExtraFieldElement, data, query, syntaxLoaded, datasource, allowCustomValue } = this.props;
|
||||
const {
|
||||
@ -411,7 +375,6 @@ export class CloudWatchLogsQueryField extends React.PureComponent<CloudWatchLogs
|
||||
portalOrigin="cloudwatch"
|
||||
syntaxLoaded={syntaxLoaded}
|
||||
disabled={loadingLogGroups || selectedLogGroups.length === 0}
|
||||
onRichValueChange={this.checkForStatsQuery}
|
||||
/>
|
||||
</div>
|
||||
{ExtraFieldElement}
|
||||
|
@ -79,7 +79,7 @@ export class CloudWatchDatasource extends DataSourceApi<CloudWatchQuery, CloudWa
|
||||
datasourceName: string;
|
||||
debouncedAlert: (datasourceName: string, region: 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;
|
||||
|
||||
/** @ngInject */
|
||||
@ -228,7 +228,11 @@ export class CloudWatchDatasource extends DataSourceApi<CloudWatchQuery, CloudWa
|
||||
): Observable<DataQueryResponse> {
|
||||
this.logQueries = {};
|
||||
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> = {};
|
||||
|
||||
|
@ -188,7 +188,12 @@ describe('CloudWatchDatasource', () => {
|
||||
const expectedData = [
|
||||
{
|
||||
...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({
|
||||
|
@ -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;
|
||||
expect(links.length).toBe(1);
|
||||
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[0]).toEqual({
|
||||
expect(df.fields[0].config.links?.length).toBe(1);
|
||||
expect(df.fields[0].config.links?.[0]).toEqual({
|
||||
title: '',
|
||||
url: 'someUrl',
|
||||
});
|
||||
expect(df.fields[1].config.links.length).toBe(1);
|
||||
expect(df.fields[1].config.links[0]).toEqual({
|
||||
expect(df.fields[1].config.links?.length).toBe(1);
|
||||
expect(df.fields[1].config.links?.[0]).toEqual({
|
||||
title: '',
|
||||
url: '',
|
||||
internal: {
|
||||
|
@ -365,7 +365,7 @@ export class ElasticDatasource extends DataSourceApi<ElasticsearchQuery, Elastic
|
||||
let queryObj;
|
||||
if (target.isLogsQuery || queryDef.hasMetricOfType(target, 'logs')) {
|
||||
target.bucketAggs = [queryDef.defaultBucketAgg()];
|
||||
target.metrics = [queryDef.defaultMetricAgg()];
|
||||
target.metrics = [];
|
||||
// Setting this for metrics queries that are typed as logs
|
||||
target.isLogsQuery = true;
|
||||
queryObj = this.queryBuilder.getLogsQuery(target, adhocFilters, queryString);
|
||||
|
@ -2,7 +2,14 @@ import _ from 'lodash';
|
||||
import flatten from 'app/core/utils/flatten';
|
||||
import * as queryDef from './query_def';
|
||||
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';
|
||||
|
||||
export class ElasticResponse {
|
||||
@ -430,7 +437,7 @@ export class ElasticResponse {
|
||||
|
||||
const { propNames, docs } = flattenHits(response.hits.hits);
|
||||
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
|
||||
for (const doc of docs) {
|
||||
@ -443,6 +450,7 @@ export class ElasticResponse {
|
||||
series.add(doc);
|
||||
}
|
||||
|
||||
series = addPreferredVisualisationType(series, 'logs');
|
||||
dataFrame.push(series);
|
||||
}
|
||||
|
||||
@ -578,7 +586,7 @@ const createEmptyDataFrame = (
|
||||
return series;
|
||||
};
|
||||
|
||||
const addPreferredVisualisationType = (series: any, type: string) => {
|
||||
const addPreferredVisualisationType = (series: any, type: PreferredVisualisationType) => {
|
||||
let s = series;
|
||||
s.meta
|
||||
? (s.meta.preferredVisualisationType = type)
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { DataSourcePlugin } from '@grafana/data';
|
||||
import { ElasticDatasource } from './datasource';
|
||||
import { ElasticQueryCtrl } from './query_ctrl';
|
||||
import ElasticsearchQueryField from './components/ElasticsearchQueryField';
|
||||
import { ConfigEditor } from './configuration/ConfigEditor';
|
||||
|
||||
class ElasticAnnotationsQueryCtrl {
|
||||
@ -11,5 +10,4 @@ class ElasticAnnotationsQueryCtrl {
|
||||
export const plugin = new DataSourcePlugin(ElasticDatasource)
|
||||
.setQueryCtrl(ElasticQueryCtrl)
|
||||
.setConfigEditor(ConfigEditor)
|
||||
.setExploreLogsQueryField(ElasticsearchQueryField)
|
||||
.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.
|
||||
* 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];
|
||||
const size = (metric.settings && metric.settings.size !== 0 && metric.settings.size) || 500;
|
||||
return this.documentQuery(query, size);
|
||||
|
@ -149,6 +149,8 @@ export default class InfluxDatasource extends DataSourceWithBackend<InfluxQuery,
|
||||
});
|
||||
|
||||
switch (target.resultFormat) {
|
||||
case 'logs':
|
||||
meta.preferredVisualisationType = 'logs';
|
||||
case 'table': {
|
||||
seriesList.push(influxSeries.getTable());
|
||||
break;
|
||||
|
@ -1,6 +1,5 @@
|
||||
import InfluxDatasource from './datasource';
|
||||
import { InfluxQueryCtrl } from './query_ctrl';
|
||||
import { InfluxLogsQueryField } from './components/InfluxLogsQueryField';
|
||||
import InfluxStartPage from './components/InfluxStartPage';
|
||||
import { DataSourcePlugin } from '@grafana/data';
|
||||
import ConfigEditor from './components/ConfigEditor';
|
||||
@ -16,5 +15,4 @@ export const plugin = new DataSourcePlugin(InfluxDatasource)
|
||||
.setConfigEditor(ConfigEditor)
|
||||
.setQueryCtrl(InfluxQueryCtrl)
|
||||
.setAnnotationQueryCtrl(InfluxAnnotationsQueryCtrl)
|
||||
.setExploreLogsQueryField(InfluxLogsQueryField)
|
||||
.setExploreStartPage(InfluxStartPage);
|
||||
|
@ -38,6 +38,7 @@ export class InfluxQueryCtrl extends QueryCtrl {
|
||||
this.resultFormats = [
|
||||
{ text: 'Time series', value: 'time_series' },
|
||||
{ text: 'Table', value: 'table' },
|
||||
{ text: 'Logs', value: 'logs' },
|
||||
];
|
||||
|
||||
this.policySegment = uiSegmentSrv.newSegment(this.target.policy);
|
||||
|
@ -48,6 +48,9 @@ export class JaegerDatasource extends DataSourceApi<JaegerQuery> {
|
||||
values: response?.data?.data || [],
|
||||
},
|
||||
],
|
||||
meta: {
|
||||
preferredVisualisationType: 'trace',
|
||||
},
|
||||
}),
|
||||
],
|
||||
};
|
||||
@ -64,6 +67,9 @@ export class JaegerDatasource extends DataSourceApi<JaegerQuery> {
|
||||
values: [],
|
||||
},
|
||||
],
|
||||
meta: {
|
||||
preferredVisualisationType: 'trace',
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { shuffle } from 'lodash';
|
||||
import { ExploreStartPageProps, DataQuery, ExploreMode } from '@grafana/data';
|
||||
import { ExploreStartPageProps, DataQuery } from '@grafana/data';
|
||||
import LokiLanguageProvider from '../language_provider';
|
||||
|
||||
const DEFAULT_EXAMPLES = ['{job="default/prometheus"}'];
|
||||
@ -46,7 +46,7 @@ export default class LokiCheatSheet extends PureComponent<ExploreStartPageProps,
|
||||
|
||||
checkUserLabels = async () => {
|
||||
// Set example from user labels
|
||||
const provider: LokiLanguageProvider = this.props.datasource.languageProvider;
|
||||
const provider: LokiLanguageProvider = this.props.datasource?.languageProvider;
|
||||
if (provider.started) {
|
||||
const labels = provider.getLabelKeys() || [];
|
||||
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;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<h2>Loki Cheat Sheet</h2>
|
||||
<div className="cheat-sheet-item">
|
||||
<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.
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
renderMetricsCheatSheet() {
|
||||
return (
|
||||
<div>
|
||||
<h2>LogQL Cheat Sheet</h2>
|
||||
{LOGQL_EXAMPLES.map(item => (
|
||||
<div className="cheat-sheet-item" key={item.expression}>
|
||||
<div className="cheat-sheet-item__title">{item.title}</div>
|
||||
@ -132,10 +124,4 @@ export default class LokiCheatSheet extends PureComponent<ExploreStartPageProps,
|
||||
</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);
|
||||
});
|
||||
});
|
||||
|
||||
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';
|
||||
|
||||
// Types
|
||||
import { AbsoluteTimeRange, ExploreQueryFieldProps, ExploreMode } from '@grafana/data';
|
||||
import { AbsoluteTimeRange, ExploreQueryFieldProps } from '@grafana/data';
|
||||
import { LokiDatasource } from '../datasource';
|
||||
import { LokiQuery, LokiOptions } from '../types';
|
||||
import { LokiQueryField } from './LokiQueryField';
|
||||
@ -12,7 +12,7 @@ import LokiExploreExtraField from './LokiExploreExtraField';
|
||||
type Props = ExploreQueryFieldProps<LokiDatasource, LokiQuery, LokiOptions>;
|
||||
|
||||
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;
|
||||
if (data && data.request) {
|
||||
@ -72,16 +72,14 @@ export function LokiExploreQueryEditor(props: Props) {
|
||||
data={data}
|
||||
absoluteRange={absolute}
|
||||
ExtraFieldElement={
|
||||
exploreMode === ExploreMode.Logs ? (
|
||||
<LokiExploreExtraField
|
||||
label={'Line limit'}
|
||||
onChangeFunc={onMaxLinesChange}
|
||||
onKeyDownFunc={onReturnKeyDown}
|
||||
value={query?.maxLines?.toString() || ''}
|
||||
type={'number'}
|
||||
min={0}
|
||||
/>
|
||||
) : null
|
||||
<LokiExploreExtraField
|
||||
label={'Line limit'}
|
||||
onChangeFunc={onMaxLinesChange}
|
||||
onKeyDownFunc={onReturnKeyDown}
|
||||
value={query?.maxLines?.toString() || ''}
|
||||
type={'number'}
|
||||
min={0}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
@ -1,15 +1,7 @@
|
||||
import LokiDatasource, { RangeQueryOptions } from './datasource';
|
||||
import { LokiQuery, LokiResponse, LokiResultType } from './types';
|
||||
import { getQueryOptions } from 'test/helpers/getQueryOptions';
|
||||
import {
|
||||
AnnotationQueryRequest,
|
||||
DataFrame,
|
||||
DataSourceApi,
|
||||
dateTime,
|
||||
ExploreMode,
|
||||
FieldCache,
|
||||
TimeRange,
|
||||
} from '@grafana/data';
|
||||
import { AnnotationQueryRequest, DataFrame, DataSourceApi, dateTime, FieldCache, TimeRange } from '@grafana/data';
|
||||
import { TemplateSrv } from 'app/features/templating/template_srv';
|
||||
import { makeMockLokiDatasource } from './mocks';
|
||||
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__
|
||||
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
...((jest.requireActual('@grafana/runtime') as unknown) as object),
|
||||
...jest.requireActual('@grafana/runtime'),
|
||||
getBackendSrv: () => backendSrv,
|
||||
}));
|
||||
|
||||
@ -110,24 +102,9 @@ describe('LokiDatasource', () => {
|
||||
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 () => {
|
||||
const options = getQueryOptions<LokiQuery>({
|
||||
targets: [{ expr: '{job="grafana"}', refId: 'B' }],
|
||||
exploreMode: ExploreMode.Logs,
|
||||
});
|
||||
|
||||
ds.runInstantQuery = jest.fn(() => of({ data: [] }));
|
||||
|
@ -19,7 +19,6 @@ import {
|
||||
LoadingState,
|
||||
AnnotationEvent,
|
||||
DataFrameView,
|
||||
TimeSeries,
|
||||
PluginMeta,
|
||||
DataSourceApi,
|
||||
DataSourceInstanceSettings,
|
||||
@ -27,7 +26,6 @@ import {
|
||||
DataQueryRequest,
|
||||
DataQueryResponse,
|
||||
AnnotationQueryRequest,
|
||||
ExploreMode,
|
||||
ScopedVars,
|
||||
} from '@grafana/data';
|
||||
|
||||
@ -92,30 +90,7 @@ export class LokiDatasource extends DataSourceApi<LokiQuery, LokiOptions> {
|
||||
expr: this.templateSrv.replace(target.expr, options.scopedVars, this.interpolateQueryExpr),
|
||||
}));
|
||||
|
||||
if (options.exploreMode === ExploreMode.Metrics) {
|
||||
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;
|
||||
}
|
||||
})
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
filteredTargets.forEach(target => subQueries.push(this.runRangeQuery(target, options, filteredTargets.length)));
|
||||
|
||||
// No valid targets, return the empty result to save a round trip.
|
||||
if (isEmpty(subQueries)) {
|
||||
@ -149,7 +124,10 @@ export class LokiDatasource extends DataSourceApi<LokiQuery, LokiOptions> {
|
||||
filter((response: any) => (response.cancelled ? false : true)),
|
||||
map((response: { data: LokiResponse }) => {
|
||||
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 {
|
||||
@ -582,7 +560,3 @@ export function lokiSpecialRegexEscape(value: any) {
|
||||
}
|
||||
|
||||
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 { Observable } from 'rxjs';
|
||||
import { Observable, throwError } from 'rxjs';
|
||||
import { webSocket } from 'rxjs/webSocket';
|
||||
import { LokiTailResponse } from './types';
|
||||
import { finalize, map } from 'rxjs/operators';
|
||||
import { finalize, map, catchError } from 'rxjs/operators';
|
||||
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: 'labels', type: FieldType.other }); // The labels for each line
|
||||
data.addField({ name: 'id', type: FieldType.string });
|
||||
data.meta = { ...data.meta, preferredVisualisationType: 'logs' };
|
||||
|
||||
stream = webSocket(target.url).pipe(
|
||||
finalize(() => {
|
||||
delete this.streams[target.url];
|
||||
}),
|
||||
|
||||
map((response: LokiTailResponse) => {
|
||||
appendResponseToBufferedData(response, data);
|
||||
return [data];
|
||||
}),
|
||||
catchError(err => {
|
||||
return throwError(`error: ${err.reason}`);
|
||||
}),
|
||||
finalize(() => {
|
||||
delete this.streams[target.url];
|
||||
})
|
||||
);
|
||||
this.streams[target.url] = stream;
|
||||
|
@ -323,6 +323,7 @@ export function lokiStreamsToDataframes(
|
||||
limit,
|
||||
stats,
|
||||
custom,
|
||||
preferredVisualisationType: 'logs',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
@ -1,3 +1,5 @@
|
||||
import set from 'lodash/set';
|
||||
|
||||
import {
|
||||
ArrayDataFrame,
|
||||
arrowTableToDataFrame,
|
||||
@ -85,6 +87,11 @@ export class TestDataDataSource extends DataSourceApi<TestDataQuery> {
|
||||
const table = t as TableData;
|
||||
table.refId = query.refId;
|
||||
table.name = query.alias;
|
||||
|
||||
if (query.scenarioId === 'logs') {
|
||||
set(table, 'meta.preferredVisualisationType', 'logs');
|
||||
}
|
||||
|
||||
data.push(table);
|
||||
}
|
||||
|
||||
|
@ -128,8 +128,9 @@ export function runLogsStream(
|
||||
});
|
||||
data.refId = 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: 'time', type: FieldType.time });
|
||||
data.meta = { preferredVisualisationType: 'logs' };
|
||||
|
||||
const { speed } = query;
|
||||
|
||||
|
@ -74,6 +74,9 @@ function responseToDataQueryResponse(response: { data: ZipkinSpan[] }): DataQuer
|
||||
values: response?.data ? [transformResponse(response?.data)] : [],
|
||||
},
|
||||
],
|
||||
meta: {
|
||||
preferredVisualisationType: 'trace',
|
||||
},
|
||||
}),
|
||||
],
|
||||
};
|
||||
@ -89,6 +92,9 @@ const emptyDataQueryResponse = {
|
||||
values: [],
|
||||
},
|
||||
],
|
||||
meta: {
|
||||
preferredVisualisationType: 'trace',
|
||||
},
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
@ -166,7 +166,6 @@ export interface ExploreItemState {
|
||||
|
||||
latency: number;
|
||||
supportedModes: ExploreMode[];
|
||||
mode: ExploreMode;
|
||||
|
||||
/**
|
||||
* If true, the view is in live tailing mode.
|
||||
@ -188,6 +187,11 @@ export interface ExploreItemState {
|
||||
* query of that panel.
|
||||
*/
|
||||
originPanelId?: number | null;
|
||||
|
||||
showLogs?: boolean;
|
||||
showMetrics?: boolean;
|
||||
showTable?: boolean;
|
||||
showTrace?: boolean;
|
||||
}
|
||||
|
||||
export interface ExploreUpdateState {
|
||||
|
Loading…
Reference in New Issue
Block a user