From 5f3a71bc7a5d99f9e7c1fbc48c1ff44f973eedef Mon Sep 17 00:00:00 2001 From: kay delaney <45561153+kaydelaney@users.noreply.github.com> Date: Thu, 27 Jun 2019 10:11:49 +0100 Subject: [PATCH] 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 --- public/app/core/utils/explore.test.ts | 6 ++-- public/app/core/utils/explore.ts | 19 ++++++++++-- public/app/features/explore/Explore.tsx | 29 +++++++++++++++---- .../app/features/explore/state/actionTypes.ts | 1 + .../features/explore/state/actions.test.ts | 3 +- public/app/features/explore/state/actions.ts | 18 ++++++++---- .../explore/state/epics/stateSaveEpic.test.ts | 8 ++--- .../explore/state/epics/stateSaveEpic.ts | 2 ++ .../features/explore/state/reducers.test.ts | 2 ++ public/app/features/explore/state/reducers.ts | 8 +++-- .../components/ElasticsearchQueryField.tsx | 4 ++- public/app/types/explore.ts | 2 ++ 12 files changed, 78 insertions(+), 24 deletions(-) diff --git a/public/app/core/utils/explore.test.ts b/public/app/core/utils/explore.test.ts index 344de380320..a2e2e1a2eb8 100644 --- a/public/app/core/utils/explore.test.ts +++ b/public/app/core/utils/explore.test.ts @@ -10,7 +10,7 @@ import { getFirstQueryErrorWithoutRefId, getRefIds, } from './explore'; -import { ExploreUrlState } from 'app/types/explore'; +import { ExploreUrlState, ExploreMode } from 'app/types/explore'; import store from 'app/core/store'; import { DataQueryError, LogsDedupStrategy } from '@grafana/ui'; @@ -18,6 +18,7 @@ const DEFAULT_EXPLORE_STATE: ExploreUrlState = { datasource: null, queries: [], range: DEFAULT_RANGE, + mode: ExploreMode.Metrics, ui: { showingGraph: true, showingTable: true, @@ -84,6 +85,7 @@ 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"}}' ); }); @@ -106,7 +108,7 @@ describe('state functions', () => { }, }; 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"]}]' ); }); }); diff --git a/public/app/core/utils/explore.ts b/public/app/core/utils/explore.ts index 135490e6305..6159efe96d7 100644 --- a/public/app/core/utils/explore.ts +++ b/public/app/core/utils/explore.ts @@ -28,7 +28,14 @@ import { DataQueryRequest, DataStreamObserver, } 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'; export const DEFAULT_RANGE = { @@ -155,10 +162,11 @@ export function buildQueryTransaction( 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 }) => metricProperties.some(prop => segment.hasOwnProperty(prop)); const isUISegment = (segment: { [key: string]: string }) => segment.hasOwnProperty('ui'); +const isModeSegment = (segment: { [key: string]: string }) => segment.hasOwnProperty('mode'); enum ParseUrlStateIndex { RangeFrom = 0, @@ -207,6 +215,7 @@ export function parseUrlState(initial: string | undefined): ExploreUrlState { queries: [], range: DEFAULT_RANGE, ui: DEFAULT_UI_STATE, + mode: null, }; if (!parsed) { @@ -229,6 +238,9 @@ export function parseUrlState(initial: string | undefined): ExploreUrlState { const datasource = parsed[ParseUrlStateIndex.Datasource]; const parsedSegments = parsed.slice(ParseUrlStateIndex.SegmentsStart); 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 ui = uiState ? { @@ -239,7 +251,7 @@ export function parseUrlState(initial: string | undefined): ExploreUrlState { } : DEFAULT_UI_STATE; - return { datasource, queries, range, ui }; + return { datasource, queries, range, ui, mode }; } export function serializeStateToUrlParam(urlState: ExploreUrlState, compact?: boolean): string { @@ -249,6 +261,7 @@ export function serializeStateToUrlParam(urlState: ExploreUrlState, compact?: bo urlState.range.to, urlState.datasource, ...urlState.queries, + { mode: urlState.mode }, { ui: [ !!urlState.ui.showingGraph, diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index a8a7bd9ab04..b662d961158 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -83,9 +83,9 @@ interface ExploreProps { initialDatasource: string; initialQueries: DataQuery[]; initialRange: RawTimeRange; + mode: ExploreMode; initialUI: ExploreUIState; queryErrors: DataQueryError[]; - mode: ExploreMode; isLive: boolean; updateTimeRange: typeof updateTimeRange; } @@ -129,7 +129,7 @@ export class Explore extends React.PureComponent { } 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; // 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 { initialDatasource, initialQueries, initialRange, + mode, width, this.exploreEvents, initialUI @@ -316,14 +317,32 @@ function mapStateToProps(state: StoreState, { exploreId }: ExploreProps) { urlState, update, queryErrors, - mode, isLive, + supportedModes, + mode, } = 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 initialQueries: DataQuery[] = ensureQueries(queries); 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; return { @@ -340,9 +359,9 @@ function mapStateToProps(state: StoreState, { exploreId }: ExploreProps) { initialDatasource, initialQueries, initialRange, + mode: newMode, initialUI, queryErrors, - mode, isLive, }; } diff --git a/public/app/features/explore/state/actionTypes.ts b/public/app/features/explore/state/actionTypes.ts index da7d0d4dcf1..89fdc79dc52 100644 --- a/public/app/features/explore/state/actionTypes.ts +++ b/public/app/features/explore/state/actionTypes.ts @@ -98,6 +98,7 @@ export interface InitializeExplorePayload { eventBridge: Emitter; queries: DataQuery[]; range: TimeRange; + mode: ExploreMode; ui: ExploreUIState; } diff --git a/public/app/features/explore/state/actions.test.ts b/public/app/features/explore/state/actions.test.ts index ab5421770fe..a50d9d08010 100644 --- a/public/app/features/explore/state/actions.test.ts +++ b/public/app/features/explore/state/actions.test.ts @@ -1,5 +1,5 @@ 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 { initializeExploreAction, @@ -55,6 +55,7 @@ const setup = (updateOverides?: Partial) => { datasource: 'some-datasource', queries: [], range: range.raw, + mode: ExploreMode.Metrics, ui, }; const updateDefaults = makeInitialUpdateState(); diff --git a/public/app/features/explore/state/actions.ts b/public/app/features/explore/state/actions.ts index b33805d119c..718f0475bf9 100644 --- a/public/app/features/explore/state/actions.ts +++ b/public/app/features/explore/state/actions.ts @@ -105,10 +105,10 @@ export function changeDatasource(exploreId: ExploreId, datasource: string): Thun const currentDataSourceInstance = getState().explore[exploreId].datasourceInstance; const queries = getState().explore[exploreId].queries; - await dispatch(importQueries(exploreId, queries, currentDataSourceInstance, newDataSourceInstance)); - dispatch(updateDatasourceInstanceAction({ exploreId, datasourceInstance: newDataSourceInstance })); + await dispatch(importQueries(exploreId, queries, currentDataSourceInstance, newDataSourceInstance)); + if (getState().explore[exploreId].isLive) { 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 { return dispatch => { - dispatch(clearQueries(exploreId)); + dispatch(clearQueriesAction({ exploreId })); dispatch(changeModeAction({ exploreId, mode })); - dispatch(runQueries(exploreId)); }; } @@ -236,6 +235,7 @@ export function initializeExplore( datasourceName: string, queries: DataQuery[], rawRange: RawTimeRange, + mode: ExploreMode, containerWidth: number, eventBridge: Emitter, ui: ExploreUIState @@ -251,6 +251,7 @@ export function initializeExplore( eventBridge, queries, range, + mode, ui, }) ); @@ -527,7 +528,7 @@ export function refreshExplore(exploreId: ExploreId): ThunkResult { } const { urlState, update, containerWidth, eventBridge } = itemState; - const { datasource, queries, range: urlRange, ui } = urlState; + const { datasource, queries, range: urlRange, mode, ui } = urlState; const refreshQueries: DataQuery[] = []; for (let index = 0; index < queries.length; index++) { const query = queries[index]; @@ -539,7 +540,7 @@ export function refreshExplore(exploreId: ExploreId): ThunkResult { // need to refresh datasource if (update.datasource) { const initialQueries = ensureQueries(queries); - dispatch(initializeExplore(exploreId, datasource, initialQueries, range, containerWidth, eventBridge, ui)); + dispatch(initializeExplore(exploreId, datasource, initialQueries, range, mode, containerWidth, eventBridge, ui)); return; } @@ -557,6 +558,11 @@ export function refreshExplore(exploreId: ExploreId): ThunkResult { 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)); diff --git a/public/app/features/explore/state/epics/stateSaveEpic.test.ts b/public/app/features/explore/state/epics/stateSaveEpic.test.ts index bee12ad92a9..08df3ac4fb4 100644 --- a/public/app/features/explore/state/epics/stateSaveEpic.test.ts +++ b/public/app/features/explore/state/epics/stateSaveEpic.test.ts @@ -15,7 +15,7 @@ describe('stateSaveEpic', () => { .whenActionIsDispatched(stateSaveAction()) .thenResultingActionsEqual( 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, }), setUrlReplacedAction({ exploreId }) @@ -32,8 +32,8 @@ describe('stateSaveEpic', () => { .thenResultingActionsEqual( updateLocation({ query: { - left: '["now-6h","now","test",{"ui":[true,true,true,null]}]', - right: '["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",{"mode":null},{"ui":[true,true,true,null]}]', }, replace: true, }), @@ -51,7 +51,7 @@ describe('stateSaveEpic', () => { .whenActionIsDispatched(stateSaveAction()) .thenResultingActionsEqual( 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, }) ); diff --git a/public/app/features/explore/state/epics/stateSaveEpic.ts b/public/app/features/explore/state/epics/stateSaveEpic.ts index 107f1de547b..210a14369e8 100644 --- a/public/app/features/explore/state/epics/stateSaveEpic.ts +++ b/public/app/features/explore/state/epics/stateSaveEpic.ts @@ -37,6 +37,7 @@ export const stateSaveEpic: Epic, ActionOf, StoreState> = (ac datasource: left.datasourceInstance.name, queries: left.queries.map(clearQueryKeys), range: toRawTimeRange(left.range), + mode: left.mode, ui: { showingGraph: left.showingGraph, showingLogs: true, @@ -50,6 +51,7 @@ export const stateSaveEpic: Epic, ActionOf, StoreState> = (ac datasource: right.datasourceInstance.name, queries: right.queries.map(clearQueryKeys), range: toRawTimeRange(right.range), + mode: right.mode, ui: { showingGraph: right.showingGraph, showingLogs: true, diff --git a/public/app/features/explore/state/reducers.test.ts b/public/app/features/explore/state/reducers.test.ts index 7175089e4d7..40196df7639 100644 --- a/public/app/features/explore/state/reducers.test.ts +++ b/public/app/features/explore/state/reducers.test.ts @@ -104,6 +104,7 @@ describe('Explore item reducer', () => { datasource: true, queries: true, range: true, + mode: true, ui: true, }, }; @@ -213,6 +214,7 @@ export const setup = (urlStateOverrides?: any) => { from: '', to: '', }, + mode: ExploreMode.Metrics, ui: { dedupStrategy: LogsDedupStrategy.none, showingGraph: false, diff --git a/public/app/features/explore/state/reducers.ts b/public/app/features/explore/state/reducers.ts index 37e9c7950d0..b19a394ba8a 100644 --- a/public/app/features/explore/state/reducers.ts +++ b/public/app/features/explore/state/reducers.ts @@ -70,6 +70,7 @@ export const makeInitialUpdateState = (): ExploreUpdateState => ({ datasource: false, queries: false, range: false, + mode: false, ui: false, }); @@ -215,12 +216,13 @@ export const itemReducer = reducerFactory({} as ExploreItemSta .addMapper({ filter: initializeExploreAction, mapper: (state, action): ExploreItemState => { - const { containerWidth, eventBridge, queries, range, ui } = action.payload; + const { containerWidth, eventBridge, queries, range, mode, ui } = action.payload; return { ...state, containerWidth, eventBridge, range, + mode, queries, initialized: true, queryKeys: getQueryKeys(queries, state.datasourceInstance), @@ -599,13 +601,14 @@ export const updateChildRefreshState = ( return { ...state, 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 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 { @@ -616,6 +619,7 @@ export const updateChildRefreshState = ( datasource, queries, range, + mode, ui, }, }; diff --git a/public/app/plugins/datasource/elasticsearch/components/ElasticsearchQueryField.tsx b/public/app/plugins/datasource/elasticsearch/components/ElasticsearchQueryField.tsx index b19d6db2ff2..ab6ef40272c 100644 --- a/public/app/plugins/datasource/elasticsearch/components/ElasticsearchQueryField.tsx +++ b/public/app/plugins/datasource/elasticsearch/components/ElasticsearchQueryField.tsx @@ -36,7 +36,9 @@ class ElasticsearchQueryField extends React.PureComponent { } componentDidMount() { - this.onChangeQuery('', true); + if (!this.props.query.isLogsQuery) { + this.onChangeQuery('', true); + } } componentWillUnmount() {} diff --git a/public/app/types/explore.ts b/public/app/types/explore.ts index e57810b6658..a9cc5d71853 100644 --- a/public/app/types/explore.ts +++ b/public/app/types/explore.ts @@ -261,6 +261,7 @@ export interface ExploreUpdateState { datasource: boolean; queries: boolean; range: boolean; + mode: boolean; ui: boolean; } @@ -274,6 +275,7 @@ export interface ExploreUIState { 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; }