mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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
This commit is contained in:
parent
abefadb333
commit
200784ea4a
@ -1,6 +1,6 @@
|
||||
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 {
|
||||
|
@ -1,33 +1,113 @@
|
||||
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'),
|
||||
|
16
public/app/types/explore.ts
Normal file
16
public/app/types/explore.ts
Normal file
@ -0,0 +1,16 @@
|
||||
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;
|
||||
}
|
Loading…
Reference in New Issue
Block a user