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