Commit 200784ea by David Kaltschmidt

Explore: Store UI state in URL

Storing queries, split state, and time range in URL.

- harmonize query serialization when generating Explore URLs in
  dashboards (use of `renderUrl`)
- move URL parse/serialization to Wrapper
- keep UI states under two keys, one for left and one for right Explore
- add option to angular router to not reload page on search change
- add lots of types
- fix time service function that gets triggered by URL change
parent abefadb3
import { Action } from 'app/core/actions/location';
import { LocationState, UrlQueryMap } from 'app/types';
import { toUrlParams } from 'app/core/utils/url';
import { LocationState } from 'app/types';
import { renderUrl } from 'app/core/utils/url';
export const initialState: LocationState = {
url: '',
......@@ -9,13 +9,6 @@ export const initialState: LocationState = {
routeParams: {},
};
function renderUrl(path: string, query: UrlQueryMap | undefined): string {
if (query && Object.keys(query).length > 0) {
path += '?' + toUrlParams(query);
}
return path;
}
export const locationReducer = (state = initialState, action: Action): LocationState => {
switch (action.type) {
case 'UPDATE_LOCATION': {
......
......@@ -4,7 +4,7 @@ import _ from 'lodash';
import config from 'app/core/config';
import coreModule from 'app/core/core_module';
import appEvents from 'app/core/app_events';
import { encodePathComponent } from 'app/core/utils/location_util';
import { renderUrl } from 'app/core/utils/url';
import Mousetrap from 'mousetrap';
import 'mousetrap-global-bind';
......@@ -200,8 +200,8 @@ export class KeybindingSrv {
...datasource.getExploreState(panel),
range,
};
const exploreState = encodePathComponent(JSON.stringify(state));
this.$location.url(`/explore?state=${exploreState}`);
const exploreState = JSON.stringify(state);
this.$location.url(renderUrl('/explore', { state: exploreState }));
}
}
});
......
......@@ -2,6 +2,15 @@
* @preserve jquery-param (c) 2015 KNOWLEDGECODE | MIT
*/
import { UrlQueryMap } from 'app/types';
export function renderUrl(path: string, query: UrlQueryMap | undefined): string {
if (query && Object.keys(query).length > 0) {
path += '?' + toUrlParams(query);
}
return path;
}
export function toUrlParams(a) {
const s = [];
const rbracket = /\[\]$/;
......
......@@ -113,7 +113,7 @@ export class TimeSrv {
}
private timeHasChangedSinceLoad() {
return this.timeAtLoad.from !== this.time.from || this.timeAtLoad.to !== this.time.to;
return this.timeAtLoad && (this.timeAtLoad.from !== this.time.from || this.timeAtLoad.to !== this.time.to);
}
setAutoRefresh(interval) {
......
......@@ -2,11 +2,11 @@ import React from 'react';
import { hot } from 'react-hot-loader';
import Select from 'react-select';
import { Query, Range, ExploreUrlState } from 'app/types/explore';
import kbn from 'app/core/utils/kbn';
import colors from 'app/core/utils/colors';
import store from 'app/core/store';
import TimeSeries from 'app/core/time_series2';
import { decodePathComponent } from 'app/core/utils/location_util';
import { parse as parseDate } from 'app/core/utils/datemath';
import ElapsedTime from './ElapsedTime';
......@@ -47,37 +47,32 @@ function makeTimeSeriesList(dataList, options) {
});
}
function parseUrlState(initial: string | undefined) {
if (initial) {
try {
const parsed = JSON.parse(decodePathComponent(initial));
return {
datasource: parsed.datasource,
queries: parsed.queries.map(q => q.query),
range: parsed.range,
};
} catch (e) {
console.error(e);
}
}
return { datasource: null, queries: [], range: DEFAULT_RANGE };
interface ExploreProps {
datasourceSrv: any;
onChangeSplit: (split: boolean, state?: ExploreState) => void;
onSaveState: (key: string, state: ExploreState) => void;
position: string;
split: boolean;
splitState?: ExploreState;
stateKey: string;
urlState: ExploreUrlState;
}
interface ExploreState {
export interface ExploreState {
datasource: any;
datasourceError: any;
datasourceLoading: boolean | null;
datasourceMissing: boolean;
datasourceName?: string;
graphResult: any;
history: any[];
initialDatasource?: string;
latency: number;
loading: any;
logsResult: any;
queries: any[];
queries: Query[];
queryErrors: any[];
queryHints: any[];
range: any;
range: Range;
requestOptions: any;
showingGraph: boolean;
showingLogs: boolean;
......@@ -88,20 +83,21 @@ interface ExploreState {
tableResult: any;
}
export class Explore extends React.Component<any, ExploreState> {
export class Explore extends React.Component<ExploreProps, ExploreState> {
el: any;
constructor(props) {
super(props);
const initialState: ExploreState = props.initialState;
const { datasource, queries, range } = parseUrlState(props.routeParams.state);
// Split state overrides everything
const splitState: ExploreState = props.splitState;
const { datasource, queries, range } = props.urlState;
this.state = {
datasource: null,
datasourceError: null,
datasourceLoading: null,
datasourceMissing: false,
datasourceName: datasource,
graphResult: null,
initialDatasource: datasource,
history: [],
latency: 0,
loading: false,
......@@ -118,13 +114,13 @@ export class Explore extends React.Component<any, ExploreState> {
supportsLogs: null,
supportsTable: null,
tableResult: null,
...initialState,
...splitState,
};
}
async componentDidMount() {
const { datasourceSrv } = this.props;
const { initialDatasource } = this.state;
const { datasourceName } = this.state;
if (!datasourceSrv) {
throw new Error('No datasource service passed as props.');
}
......@@ -133,15 +129,15 @@ export class Explore extends React.Component<any, ExploreState> {
this.setState({ datasourceLoading: true });
// Priority: datasource in url, default datasource, first explore datasource
let datasource;
if (initialDatasource) {
datasource = await datasourceSrv.get(initialDatasource);
if (datasourceName) {
datasource = await datasourceSrv.get(datasourceName);
} else {
datasource = await datasourceSrv.get();
}
if (!datasource.meta.explore) {
datasource = await datasourceSrv.get(datasources[0].name);
}
this.setDatasource(datasource);
await this.setDatasource(datasource);
} else {
this.setState({ datasourceMissing: true });
}
......@@ -188,9 +184,14 @@ export class Explore extends React.Component<any, ExploreState> {
supportsLogs,
supportsTable,
datasourceLoading: false,
datasourceName: datasource.name,
queries: nextQueries,
},
() => datasourceError === null && this.onSubmit()
() => {
if (datasourceError === null) {
this.onSubmit();
}
}
);
}
......@@ -220,7 +221,8 @@ export class Explore extends React.Component<any, ExploreState> {
queryHints: [],
tableResult: null,
});
const datasource = await this.props.datasourceSrv.get(option.value);
const datasourceName = option.value;
const datasource = await this.props.datasourceSrv.get(datasourceName);
this.setDatasource(datasource);
};
......@@ -259,21 +261,25 @@ export class Explore extends React.Component<any, ExploreState> {
};
onClickClear = () => {
this.setState({
graphResult: null,
logsResult: null,
latency: 0,
queries: ensureQueries(),
queryErrors: [],
queryHints: [],
tableResult: null,
});
this.setState(
{
graphResult: null,
logsResult: null,
latency: 0,
queries: ensureQueries(),
queryErrors: [],
queryHints: [],
tableResult: null,
},
this.saveState
);
};
onClickCloseSplit = () => {
const { onChangeSplit } = this.props;
if (onChangeSplit) {
onChangeSplit(false);
this.saveState();
}
};
......@@ -291,6 +297,7 @@ export class Explore extends React.Component<any, ExploreState> {
state.queries = state.queries.map(({ edited, ...rest }) => rest);
if (onChangeSplit) {
onChangeSplit(true, state);
this.saveState();
}
};
......@@ -349,6 +356,7 @@ export class Explore extends React.Component<any, ExploreState> {
if (showingLogs && supportsLogs) {
this.runLogsQuery();
}
this.saveState();
};
onQuerySuccess(datasourceId: string, queries: any[]): void {
......@@ -471,6 +479,11 @@ export class Explore extends React.Component<any, ExploreState> {
return datasource.metadataRequest(url);
};
saveState = () => {
const { stateKey, onSaveState } = this.props;
onSaveState(stateKey, this.state);
};
render() {
const { datasourceSrv, position, split } = this.props;
const {
......
import React, { PureComponent } from 'react';
import { hot } from 'react-hot-loader';
import { connect } from 'react-redux';
import Explore from './Explore';
import { updateLocation } from 'app/core/actions';
import { StoreState } from 'app/types';
import { ExploreUrlState } from 'app/types/explore';
export default class Wrapper extends PureComponent<any, any> {
state = {
initialState: null,
split: false,
import Explore, { ExploreState } from './Explore';
import { DEFAULT_RANGE } from './TimePicker';
function parseUrlState(initial: string | undefined): ExploreUrlState {
if (initial) {
try {
return JSON.parse(decodeURI(initial));
} catch (e) {
console.error(e);
}
}
return { datasource: null, queries: [], range: DEFAULT_RANGE };
}
function serializeStateToUrlParam(state: ExploreState): string {
const urlState: ExploreUrlState = {
datasource: state.datasourceName,
queries: state.queries.map(q => ({ query: q.query })),
range: state.range,
};
return JSON.stringify(urlState);
}
interface WrapperProps {
backendSrv?: any;
datasourceSrv?: any;
updateLocation: typeof updateLocation;
urlStates: { [key: string]: string };
}
interface WrapperState {
split: boolean;
splitState: ExploreState;
}
const STATE_KEY_LEFT = 'state';
const STATE_KEY_RIGHT = 'stateRight';
export class Wrapper extends PureComponent<WrapperProps, WrapperState> {
urlStates: { [key: string]: string };
constructor(props: WrapperProps) {
super(props);
this.urlStates = props.urlStates;
this.state = {
split: Boolean(props.urlStates[STATE_KEY_RIGHT]),
splitState: undefined,
};
}
onChangeSplit = (split: boolean, splitState: ExploreState) => {
this.setState({ split, splitState });
};
handleChangeSplit = (split, initialState) => {
this.setState({ split, initialState });
onSaveState = (key: string, state: ExploreState) => {
const urlState = serializeStateToUrlParam(state);
this.urlStates[key] = urlState;
this.props.updateLocation({
query: this.urlStates,
});
};
render() {
const { datasourceSrv } = this.props;
// State overrides for props from first Explore
const { initialState, split } = this.state;
const { split, splitState } = this.state;
const urlStateLeft = parseUrlState(this.urlStates[STATE_KEY_LEFT]);
const urlStateRight = parseUrlState(this.urlStates[STATE_KEY_RIGHT]);
return (
<div className="explore-wrapper">
<Explore {...this.props} position="left" onChangeSplit={this.handleChangeSplit} split={split} />
{split ? (
<Explore
datasourceSrv={datasourceSrv}
onChangeSplit={this.onChangeSplit}
onSaveState={this.onSaveState}
position="left"
split={split}
stateKey={STATE_KEY_LEFT}
urlState={urlStateLeft}
/>
{split && (
<Explore
{...this.props}
initialState={initialState}
onChangeSplit={this.handleChangeSplit}
datasourceSrv={datasourceSrv}
onChangeSplit={this.onChangeSplit}
onSaveState={this.onSaveState}
position="right"
split={split}
splitState={splitState}
stateKey={STATE_KEY_RIGHT}
urlState={urlStateRight}
/>
) : null}
)}
</div>
);
}
}
const mapStateToProps = (state: StoreState) => ({
urlStates: state.location.query,
});
const mapDispatchToProps = {
updateLocation,
};
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(Wrapper));
......@@ -3,8 +3,8 @@ export function generateQueryKey(index = 0) {
}
export function ensureQueries(queries?) {
if (queries && typeof queries === 'object' && queries.length > 0 && typeof queries[0] === 'string') {
return queries.map((query, i) => ({ key: generateQueryKey(i), query }));
if (queries && typeof queries === 'object' && queries.length > 0 && typeof queries[0].query === 'string') {
return queries.map(({ query }, i) => ({ key: generateQueryKey(i), query }));
}
return [{ key: generateQueryKey(), query: '' }];
}
......
......@@ -6,7 +6,7 @@ import kbn from 'app/core/utils/kbn';
import { PanelCtrl } from 'app/features/panel/panel_ctrl';
import * as rangeUtil from 'app/core/utils/rangeutil';
import * as dateMath from 'app/core/utils/datemath';
import { encodePathComponent } from 'app/core/utils/location_util';
import { renderUrl } from 'app/core/utils/url';
import { metricsTabDirective } from './metrics_tab';
......@@ -331,8 +331,8 @@ class MetricsPanelCtrl extends PanelCtrl {
...this.datasource.getExploreState(this.panel),
range,
};
const exploreState = encodePathComponent(JSON.stringify(state));
this.$location.url(`/explore?state=${exploreState}`);
const exploreState = JSON.stringify(state);
this.$location.url(renderUrl('/explore', { state: exploreState }));
}
addQuery(target) {
......
......@@ -115,6 +115,7 @@ export function setupAngularRoutes($routeProvider, $locationProvider) {
})
.when('/explore', {
template: '<react-container />',
reloadOnSearch: false,
resolve: {
roles: () => ['Editor', 'Admin'],
component: () => import(/* webpackChunkName: "explore" */ 'app/features/explore/Wrapper'),
......
export interface Range {
from: string;
to: string;
}
export interface Query {
query: string;
edited?: boolean;
key?: string;
}
export interface ExploreUrlState {
datasource: string;
queries: Query[];
range: Range;
}
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