Commit 5f3a71bc by kay delaney Committed by Marcus Efraimsson

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
parent 021f5351
......@@ -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"]}]'
);
});
});
......
......@@ -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,
......
......@@ -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<ExploreProps> {
}
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<ExploreProps> {
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,
};
}
......
......@@ -98,6 +98,7 @@ export interface InitializeExplorePayload {
eventBridge: Emitter;
queries: DataQuery[];
range: TimeRange;
mode: ExploreMode;
ui: ExploreUIState;
}
......
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<ExploreUpdateState>) => {
datasource: 'some-datasource',
queries: [],
range: range.raw,
mode: ExploreMode.Metrics,
ui,
};
const updateDefaults = makeInitialUpdateState();
......
......@@ -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<void> {
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<void> {
}
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<void> {
// 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<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));
......
......@@ -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,
})
);
......
......@@ -37,6 +37,7 @@ export const stateSaveEpic: Epic<ActionOf<any>, ActionOf<any>, 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<any>, ActionOf<any>, StoreState> = (ac
datasource: right.datasourceInstance.name,
queries: right.queries.map(clearQueryKeys),
range: toRawTimeRange(right.range),
mode: right.mode,
ui: {
showingGraph: right.showingGraph,
showingLogs: true,
......
......@@ -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,
......
......@@ -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<ExploreItemState>({} 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,
},
};
......
......@@ -36,7 +36,9 @@ class ElasticsearchQueryField extends React.PureComponent<Props, State> {
}
componentDidMount() {
this.onChangeQuery('', true);
if (!this.props.query.isLogsQuery) {
this.onChangeQuery('', true);
}
}
componentWillUnmount() {}
......
......@@ -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;
}
......
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