Commit 64bc8596 by Andrej Ocenas Committed by GitHub

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>
parent be961c54
......@@ -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;
}
......
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,
......
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,
};
}
......
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 {
......@@ -192,11 +184,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.
*/
......
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,
})
);
......@@ -167,16 +160,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();
}
getGraphResult(): GraphSeriesXY[] | null {
if (this.state.mode !== ExploreMode.Metrics) {
return null;
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);
}
}
}
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) {
getGraphResult(): GraphSeriesXY[] | null {
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 (onlyTables.length === 0) {
return null;
}
if (frameARefId > frameBRefId) {
return 1;
}
if (frameARefId < frameBRefId) {
return -1;
}
return 0;
});
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');
......
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)
......
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;
......
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',
},
}),
],
});
......
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}
/>
}
/>
);
......
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');
}
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',
},
};
});
......
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 {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment