Explore: Adds URL support for select mode (#17755)

Adds URL support for mode state (Metrics/Logs). It's important 
to be able to share a link to logs when a data source supports 
both metrics and logs.

Closes #17101
This commit is contained in:
kay delaney
2019-06-27 11:11:49 +02:00
committed by Marcus Efraimsson
parent 021f5351fa
commit 5f3a71bc7a
12 changed files with 78 additions and 24 deletions
+4 -2
View File
@@ -10,7 +10,7 @@ import {
getFirstQueryErrorWithoutRefId, getFirstQueryErrorWithoutRefId,
getRefIds, getRefIds,
} from './explore'; } from './explore';
import { ExploreUrlState } from 'app/types/explore'; import { ExploreUrlState, ExploreMode } from 'app/types/explore';
import store from 'app/core/store'; import store from 'app/core/store';
import { DataQueryError, LogsDedupStrategy } from '@grafana/ui'; import { DataQueryError, LogsDedupStrategy } from '@grafana/ui';
@@ -18,6 +18,7 @@ const DEFAULT_EXPLORE_STATE: ExploreUrlState = {
datasource: null, datasource: null,
queries: [], queries: [],
range: DEFAULT_RANGE, range: DEFAULT_RANGE,
mode: ExploreMode.Metrics,
ui: { ui: {
showingGraph: true, showingGraph: true,
showingTable: true, showingTable: true,
@@ -84,6 +85,7 @@ describe('state functions', () => {
expect(serializeStateToUrlParam(state)).toBe( expect(serializeStateToUrlParam(state)).toBe(
'{"datasource":"foo","queries":[{"expr":"metric{test=\\"a/b\\"}"},' + '{"datasource":"foo","queries":[{"expr":"metric{test=\\"a/b\\"}"},' +
'{"expr":"super{foo=\\"x/z\\"}"}],"range":{"from":"now-5h","to":"now"},' + '{"expr":"super{foo=\\"x/z\\"}"}],"range":{"from":"now-5h","to":"now"},' +
'"mode":"Metrics",' +
'"ui":{"showingGraph":true,"showingTable":true,"showingLogs":true,"dedupStrategy":"none"}}' '"ui":{"showingGraph":true,"showingTable":true,"showingLogs":true,"dedupStrategy":"none"}}'
); );
}); });
@@ -106,7 +108,7 @@ describe('state functions', () => {
}, },
}; };
expect(serializeStateToUrlParam(state, true)).toBe( expect(serializeStateToUrlParam(state, true)).toBe(
'["now-5h","now","foo",{"expr":"metric{test=\\"a/b\\"}"},{"expr":"super{foo=\\"x/z\\"}"},{"ui":[true,true,true,"none"]}]' '["now-5h","now","foo",{"expr":"metric{test=\\"a/b\\"}"},{"expr":"super{foo=\\"x/z\\"}"},{"mode":"Metrics"},{"ui":[true,true,true,"none"]}]'
); );
}); });
}); });
+16 -3
View File
@@ -28,7 +28,14 @@ import {
DataQueryRequest, DataQueryRequest,
DataStreamObserver, DataStreamObserver,
} from '@grafana/ui'; } from '@grafana/ui';
import { ExploreUrlState, HistoryItem, QueryTransaction, QueryIntervals, QueryOptions } from 'app/types/explore'; import {
ExploreUrlState,
HistoryItem,
QueryTransaction,
QueryIntervals,
QueryOptions,
ExploreMode,
} from 'app/types/explore';
import { config } from '../config'; import { config } from '../config';
export const DEFAULT_RANGE = { export const DEFAULT_RANGE = {
@@ -155,10 +162,11 @@ export function buildQueryTransaction(
export const clearQueryKeys: (query: DataQuery) => object = ({ key, refId, ...rest }) => rest; export const clearQueryKeys: (query: DataQuery) => object = ({ key, refId, ...rest }) => rest;
const metricProperties = ['expr', 'target', 'datasource']; const metricProperties = ['expr', 'target', 'datasource', 'query'];
const isMetricSegment = (segment: { [key: string]: string }) => const isMetricSegment = (segment: { [key: string]: string }) =>
metricProperties.some(prop => segment.hasOwnProperty(prop)); metricProperties.some(prop => segment.hasOwnProperty(prop));
const isUISegment = (segment: { [key: string]: string }) => segment.hasOwnProperty('ui'); const isUISegment = (segment: { [key: string]: string }) => segment.hasOwnProperty('ui');
const isModeSegment = (segment: { [key: string]: string }) => segment.hasOwnProperty('mode');
enum ParseUrlStateIndex { enum ParseUrlStateIndex {
RangeFrom = 0, RangeFrom = 0,
@@ -207,6 +215,7 @@ export function parseUrlState(initial: string | undefined): ExploreUrlState {
queries: [], queries: [],
range: DEFAULT_RANGE, range: DEFAULT_RANGE,
ui: DEFAULT_UI_STATE, ui: DEFAULT_UI_STATE,
mode: null,
}; };
if (!parsed) { if (!parsed) {
@@ -229,6 +238,9 @@ export function parseUrlState(initial: string | undefined): ExploreUrlState {
const datasource = parsed[ParseUrlStateIndex.Datasource]; const datasource = parsed[ParseUrlStateIndex.Datasource];
const parsedSegments = parsed.slice(ParseUrlStateIndex.SegmentsStart); const parsedSegments = parsed.slice(ParseUrlStateIndex.SegmentsStart);
const queries = parsedSegments.filter(segment => isMetricSegment(segment)); const queries = parsedSegments.filter(segment => isMetricSegment(segment));
const modeObj = parsedSegments.filter(segment => isModeSegment(segment))[0];
const mode = modeObj ? modeObj.mode : ExploreMode.Metrics;
const uiState = parsedSegments.filter(segment => isUISegment(segment))[0]; const uiState = parsedSegments.filter(segment => isUISegment(segment))[0];
const ui = uiState const ui = uiState
? { ? {
@@ -239,7 +251,7 @@ export function parseUrlState(initial: string | undefined): ExploreUrlState {
} }
: DEFAULT_UI_STATE; : DEFAULT_UI_STATE;
return { datasource, queries, range, ui }; return { datasource, queries, range, ui, mode };
} }
export function serializeStateToUrlParam(urlState: ExploreUrlState, compact?: boolean): string { export function serializeStateToUrlParam(urlState: ExploreUrlState, compact?: boolean): string {
@@ -249,6 +261,7 @@ export function serializeStateToUrlParam(urlState: ExploreUrlState, compact?: bo
urlState.range.to, urlState.range.to,
urlState.datasource, urlState.datasource,
...urlState.queries, ...urlState.queries,
{ mode: urlState.mode },
{ {
ui: [ ui: [
!!urlState.ui.showingGraph, !!urlState.ui.showingGraph,
+24 -5
View File
@@ -83,9 +83,9 @@ interface ExploreProps {
initialDatasource: string; initialDatasource: string;
initialQueries: DataQuery[]; initialQueries: DataQuery[];
initialRange: RawTimeRange; initialRange: RawTimeRange;
mode: ExploreMode;
initialUI: ExploreUIState; initialUI: ExploreUIState;
queryErrors: DataQueryError[]; queryErrors: DataQueryError[];
mode: ExploreMode;
isLive: boolean; isLive: boolean;
updateTimeRange: typeof updateTimeRange; updateTimeRange: typeof updateTimeRange;
} }
@@ -129,7 +129,7 @@ export class Explore extends React.PureComponent<ExploreProps> {
} }
componentDidMount() { componentDidMount() {
const { initialized, exploreId, initialDatasource, initialQueries, initialRange, initialUI } = this.props; const { initialized, exploreId, initialDatasource, initialQueries, initialRange, mode, initialUI } = this.props;
const width = this.el ? this.el.offsetWidth : 0; const width = this.el ? this.el.offsetWidth : 0;
// initialize the whole explore first time we mount and if browser history contains a change in datasource // initialize the whole explore first time we mount and if browser history contains a change in datasource
@@ -139,6 +139,7 @@ export class Explore extends React.PureComponent<ExploreProps> {
initialDatasource, initialDatasource,
initialQueries, initialQueries,
initialRange, initialRange,
mode,
width, width,
this.exploreEvents, this.exploreEvents,
initialUI initialUI
@@ -316,14 +317,32 @@ function mapStateToProps(state: StoreState, { exploreId }: ExploreProps) {
urlState, urlState,
update, update,
queryErrors, queryErrors,
mode,
isLive, isLive,
supportedModes,
mode,
} = item; } = item;
const { datasource, queries, range: urlRange, ui } = (urlState || {}) as ExploreUrlState; const { datasource, queries, range: urlRange, mode: urlMode, ui } = (urlState || {}) as ExploreUrlState;
const initialDatasource = datasource || store.get(LAST_USED_DATASOURCE_KEY); const initialDatasource = datasource || store.get(LAST_USED_DATASOURCE_KEY);
const initialQueries: DataQuery[] = ensureQueries(queries); const initialQueries: DataQuery[] = ensureQueries(queries);
const initialRange = urlRange ? getTimeRangeFromUrl(urlRange, timeZone).raw : DEFAULT_RANGE; const initialRange = urlRange ? getTimeRangeFromUrl(urlRange, timeZone).raw : DEFAULT_RANGE;
let newMode: ExploreMode;
if (supportedModes.length) {
const urlModeIsValid = supportedModes.includes(urlMode);
const modeStateIsValid = supportedModes.includes(mode);
if (urlModeIsValid) {
newMode = urlMode;
} else if (modeStateIsValid) {
newMode = mode;
} else {
newMode = supportedModes[0];
}
} else {
newMode = [ExploreMode.Metrics, ExploreMode.Logs].includes(urlMode) ? urlMode : ExploreMode.Metrics;
}
const initialUI = ui || DEFAULT_UI_STATE; const initialUI = ui || DEFAULT_UI_STATE;
return { return {
@@ -340,9 +359,9 @@ function mapStateToProps(state: StoreState, { exploreId }: ExploreProps) {
initialDatasource, initialDatasource,
initialQueries, initialQueries,
initialRange, initialRange,
mode: newMode,
initialUI, initialUI,
queryErrors, queryErrors,
mode,
isLive, isLive,
}; };
} }
@@ -98,6 +98,7 @@ export interface InitializeExplorePayload {
eventBridge: Emitter; eventBridge: Emitter;
queries: DataQuery[]; queries: DataQuery[];
range: TimeRange; range: TimeRange;
mode: ExploreMode;
ui: ExploreUIState; ui: ExploreUIState;
} }
@@ -1,5 +1,5 @@
import { refreshExplore, testDatasource, loadDatasource } from './actions'; import { refreshExplore, testDatasource, loadDatasource } from './actions';
import { ExploreId, ExploreUrlState, ExploreUpdateState } from 'app/types'; import { ExploreId, ExploreUrlState, ExploreUpdateState, ExploreMode } from 'app/types';
import { thunkTester } from 'test/core/thunk/thunkTester'; import { thunkTester } from 'test/core/thunk/thunkTester';
import { import {
initializeExploreAction, initializeExploreAction,
@@ -55,6 +55,7 @@ const setup = (updateOverides?: Partial<ExploreUpdateState>) => {
datasource: 'some-datasource', datasource: 'some-datasource',
queries: [], queries: [],
range: range.raw, range: range.raw,
mode: ExploreMode.Metrics,
ui, ui,
}; };
const updateDefaults = makeInitialUpdateState(); const updateDefaults = makeInitialUpdateState();
+12 -6
View File
@@ -105,10 +105,10 @@ export function changeDatasource(exploreId: ExploreId, datasource: string): Thun
const currentDataSourceInstance = getState().explore[exploreId].datasourceInstance; const currentDataSourceInstance = getState().explore[exploreId].datasourceInstance;
const queries = getState().explore[exploreId].queries; const queries = getState().explore[exploreId].queries;
await dispatch(importQueries(exploreId, queries, currentDataSourceInstance, newDataSourceInstance));
dispatch(updateDatasourceInstanceAction({ exploreId, datasourceInstance: newDataSourceInstance })); dispatch(updateDatasourceInstanceAction({ exploreId, datasourceInstance: newDataSourceInstance }));
await dispatch(importQueries(exploreId, queries, currentDataSourceInstance, newDataSourceInstance));
if (getState().explore[exploreId].isLive) { if (getState().explore[exploreId].isLive) {
dispatch(changeRefreshInterval(exploreId, offOption.value)); dispatch(changeRefreshInterval(exploreId, offOption.value));
} }
@@ -123,9 +123,8 @@ export function changeDatasource(exploreId: ExploreId, datasource: string): Thun
*/ */
export function changeMode(exploreId: ExploreId, mode: ExploreMode): ThunkResult<void> { export function changeMode(exploreId: ExploreId, mode: ExploreMode): ThunkResult<void> {
return dispatch => { return dispatch => {
dispatch(clearQueries(exploreId)); dispatch(clearQueriesAction({ exploreId }));
dispatch(changeModeAction({ exploreId, mode })); dispatch(changeModeAction({ exploreId, mode }));
dispatch(runQueries(exploreId));
}; };
} }
@@ -236,6 +235,7 @@ export function initializeExplore(
datasourceName: string, datasourceName: string,
queries: DataQuery[], queries: DataQuery[],
rawRange: RawTimeRange, rawRange: RawTimeRange,
mode: ExploreMode,
containerWidth: number, containerWidth: number,
eventBridge: Emitter, eventBridge: Emitter,
ui: ExploreUIState ui: ExploreUIState
@@ -251,6 +251,7 @@ export function initializeExplore(
eventBridge, eventBridge,
queries, queries,
range, range,
mode,
ui, ui,
}) })
); );
@@ -527,7 +528,7 @@ export function refreshExplore(exploreId: ExploreId): ThunkResult<void> {
} }
const { urlState, update, containerWidth, eventBridge } = itemState; const { urlState, update, containerWidth, eventBridge } = itemState;
const { datasource, queries, range: urlRange, ui } = urlState; const { datasource, queries, range: urlRange, mode, ui } = urlState;
const refreshQueries: DataQuery[] = []; const refreshQueries: DataQuery[] = [];
for (let index = 0; index < queries.length; index++) { for (let index = 0; index < queries.length; index++) {
const query = queries[index]; const query = queries[index];
@@ -539,7 +540,7 @@ export function refreshExplore(exploreId: ExploreId): ThunkResult<void> {
// need to refresh datasource // need to refresh datasource
if (update.datasource) { if (update.datasource) {
const initialQueries = ensureQueries(queries); const initialQueries = ensureQueries(queries);
dispatch(initializeExplore(exploreId, datasource, initialQueries, range, containerWidth, eventBridge, ui)); dispatch(initializeExplore(exploreId, datasource, initialQueries, range, mode, containerWidth, eventBridge, ui));
return; return;
} }
@@ -557,6 +558,11 @@ export function refreshExplore(exploreId: ExploreId): ThunkResult<void> {
dispatch(setQueriesAction({ exploreId, queries: refreshQueries })); dispatch(setQueriesAction({ exploreId, queries: refreshQueries }));
} }
// need to refresh mode
if (update.mode) {
dispatch(changeModeAction({ exploreId, mode }));
}
// always run queries when refresh is needed // always run queries when refresh is needed
if (update.queries || update.ui || update.range) { if (update.queries || update.ui || update.range) {
dispatch(runQueries(exploreId)); dispatch(runQueries(exploreId));
@@ -15,7 +15,7 @@ describe('stateSaveEpic', () => {
.whenActionIsDispatched(stateSaveAction()) .whenActionIsDispatched(stateSaveAction())
.thenResultingActionsEqual( .thenResultingActionsEqual(
updateLocation({ updateLocation({
query: { left: '["now-6h","now","test",{"ui":[true,true,true,null]}]' }, query: { left: '["now-6h","now","test",{"mode":null},{"ui":[true,true,true,null]}]' },
replace: true, replace: true,
}), }),
setUrlReplacedAction({ exploreId }) setUrlReplacedAction({ exploreId })
@@ -32,8 +32,8 @@ describe('stateSaveEpic', () => {
.thenResultingActionsEqual( .thenResultingActionsEqual(
updateLocation({ updateLocation({
query: { query: {
left: '["now-6h","now","test",{"ui":[true,true,true,null]}]', left: '["now-6h","now","test",{"mode":null},{"ui":[true,true,true,null]}]',
right: '["now-6h","now","test",{"ui":[true,true,true,null]}]', right: '["now-6h","now","test",{"mode":null},{"ui":[true,true,true,null]}]',
}, },
replace: true, replace: true,
}), }),
@@ -51,7 +51,7 @@ describe('stateSaveEpic', () => {
.whenActionIsDispatched(stateSaveAction()) .whenActionIsDispatched(stateSaveAction())
.thenResultingActionsEqual( .thenResultingActionsEqual(
updateLocation({ updateLocation({
query: { left: '["now-6h","now","test",{"ui":[true,true,true,null]}]' }, query: { left: '["now-6h","now","test",{"mode":null},{"ui":[true,true,true,null]}]' },
replace: false, replace: false,
}) })
); );
@@ -37,6 +37,7 @@ export const stateSaveEpic: Epic<ActionOf<any>, ActionOf<any>, StoreState> = (ac
datasource: left.datasourceInstance.name, datasource: left.datasourceInstance.name,
queries: left.queries.map(clearQueryKeys), queries: left.queries.map(clearQueryKeys),
range: toRawTimeRange(left.range), range: toRawTimeRange(left.range),
mode: left.mode,
ui: { ui: {
showingGraph: left.showingGraph, showingGraph: left.showingGraph,
showingLogs: true, showingLogs: true,
@@ -50,6 +51,7 @@ export const stateSaveEpic: Epic<ActionOf<any>, ActionOf<any>, StoreState> = (ac
datasource: right.datasourceInstance.name, datasource: right.datasourceInstance.name,
queries: right.queries.map(clearQueryKeys), queries: right.queries.map(clearQueryKeys),
range: toRawTimeRange(right.range), range: toRawTimeRange(right.range),
mode: right.mode,
ui: { ui: {
showingGraph: right.showingGraph, showingGraph: right.showingGraph,
showingLogs: true, showingLogs: true,
@@ -104,6 +104,7 @@ describe('Explore item reducer', () => {
datasource: true, datasource: true,
queries: true, queries: true,
range: true, range: true,
mode: true,
ui: true, ui: true,
}, },
}; };
@@ -213,6 +214,7 @@ export const setup = (urlStateOverrides?: any) => {
from: '', from: '',
to: '', to: '',
}, },
mode: ExploreMode.Metrics,
ui: { ui: {
dedupStrategy: LogsDedupStrategy.none, dedupStrategy: LogsDedupStrategy.none,
showingGraph: false, showingGraph: false,
@@ -70,6 +70,7 @@ export const makeInitialUpdateState = (): ExploreUpdateState => ({
datasource: false, datasource: false,
queries: false, queries: false,
range: false, range: false,
mode: false,
ui: false, ui: false,
}); });
@@ -215,12 +216,13 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta
.addMapper({ .addMapper({
filter: initializeExploreAction, filter: initializeExploreAction,
mapper: (state, action): ExploreItemState => { mapper: (state, action): ExploreItemState => {
const { containerWidth, eventBridge, queries, range, ui } = action.payload; const { containerWidth, eventBridge, queries, range, mode, ui } = action.payload;
return { return {
...state, ...state,
containerWidth, containerWidth,
eventBridge, eventBridge,
range, range,
mode,
queries, queries,
initialized: true, initialized: true,
queryKeys: getQueryKeys(queries, state.datasourceInstance), queryKeys: getQueryKeys(queries, state.datasourceInstance),
@@ -599,13 +601,14 @@ export const updateChildRefreshState = (
return { return {
...state, ...state,
urlState, urlState,
update: { datasource: false, queries: false, range: false, ui: false }, update: { datasource: false, queries: false, range: false, mode: false, ui: false },
}; };
} }
const datasource = _.isEqual(urlState ? urlState.datasource : '', state.urlState.datasource) === false; const datasource = _.isEqual(urlState ? urlState.datasource : '', state.urlState.datasource) === false;
const queries = _.isEqual(urlState ? urlState.queries : [], state.urlState.queries) === false; const queries = _.isEqual(urlState ? urlState.queries : [], state.urlState.queries) === false;
const range = _.isEqual(urlState ? urlState.range : DEFAULT_RANGE, state.urlState.range) === false; const range = _.isEqual(urlState ? urlState.range : DEFAULT_RANGE, state.urlState.range) === false;
const mode = _.isEqual(urlState ? urlState.mode : ExploreMode.Metrics, state.urlState.mode) === false;
const ui = _.isEqual(urlState ? urlState.ui : DEFAULT_UI_STATE, state.urlState.ui) === false; const ui = _.isEqual(urlState ? urlState.ui : DEFAULT_UI_STATE, state.urlState.ui) === false;
return { return {
@@ -616,6 +619,7 @@ export const updateChildRefreshState = (
datasource, datasource,
queries, queries,
range, range,
mode,
ui, ui,
}, },
}; };
@@ -36,7 +36,9 @@ class ElasticsearchQueryField extends React.PureComponent<Props, State> {
} }
componentDidMount() { componentDidMount() {
this.onChangeQuery('', true); if (!this.props.query.isLogsQuery) {
this.onChangeQuery('', true);
}
} }
componentWillUnmount() {} componentWillUnmount() {}
+2
View File
@@ -261,6 +261,7 @@ export interface ExploreUpdateState {
datasource: boolean; datasource: boolean;
queries: boolean; queries: boolean;
range: boolean; range: boolean;
mode: boolean;
ui: boolean; ui: boolean;
} }
@@ -274,6 +275,7 @@ export interface ExploreUIState {
export interface ExploreUrlState { export interface ExploreUrlState {
datasource: string; datasource: string;
queries: any[]; // Should be a DataQuery, but we're going to strip refIds, so typing makes less sense queries: any[]; // Should be a DataQuery, but we're going to strip refIds, so typing makes less sense
mode: ExploreMode;
range: RawTimeRange; range: RawTimeRange;
ui: ExploreUIState; ui: ExploreUIState;
} }