Allow multiple Explore items for split

This commit is contained in:
David Kaltschmidt 2019-01-11 18:26:56 +01:00
parent 2be2deddb8
commit 68c039b289
8 changed files with 413 additions and 284 deletions

View File

@ -206,11 +206,14 @@ export function ensureQueries(queries?: DataQuery[]): DataQuery[] {
* A target is non-empty when it has keys (with non-empty values) other than refId and key.
*/
export function hasNonEmptyQuery(queries: DataQuery[]): boolean {
return queries.some(
query =>
Object.keys(query)
.map(k => query[k])
.filter(v => v).length > 2
return (
queries &&
queries.some(
query =>
Object.keys(query)
.map(k => query[k])
.filter(v => v).length > 2
)
);
}

View File

@ -2,14 +2,15 @@ import React from 'react';
import { hot } from 'react-hot-loader';
import { connect } from 'react-redux';
import _ from 'lodash';
import { withSize } from 'react-sizeme';
import { AutoSizer } from 'react-virtualized';
import { RawTimeRange, TimeRange } from '@grafana/ui';
import { DataSourceSelectItem } from 'app/types/datasources';
import { ExploreUrlState, HistoryItem, QueryTransaction, RangeScanner } from 'app/types/explore';
import { ExploreUrlState, HistoryItem, QueryTransaction, RangeScanner, ExploreId } from 'app/types/explore';
import { DataQuery } from 'app/types/series';
import { StoreState } from 'app/types';
import store from 'app/core/store';
import { LAST_USED_DATASOURCE_KEY, ensureQueries } from 'app/core/utils/explore';
import { LAST_USED_DATASOURCE_KEY, ensureQueries, DEFAULT_RANGE } from 'app/core/utils/explore';
import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker';
import { Emitter } from 'app/core/utils/emitter';
@ -20,9 +21,11 @@ import {
changeSize,
changeTime,
clickClear,
clickCloseSplit,
clickExample,
clickGraphButton,
clickLogsButton,
clickSplit,
clickTableButton,
highlightLogsExpression,
initializeExplore,
@ -32,7 +35,7 @@ import {
scanStart,
scanStop,
} from './state/actions';
import { ExploreState } from './state/reducers';
import { ExploreItemState } from './state/reducers';
import Panel from './Panel';
import QueryRows from './QueryRows';
@ -50,17 +53,21 @@ interface ExploreProps {
addQueryRow: typeof addQueryRow;
changeDatasource: typeof changeDatasource;
changeQuery: typeof changeQuery;
changeSize: typeof changeSize;
changeTime: typeof changeTime;
clickClear: typeof clickClear;
clickCloseSplit: typeof clickCloseSplit;
clickExample: typeof clickExample;
clickGraphButton: typeof clickGraphButton;
clickLogsButton: typeof clickLogsButton;
clickSplit: typeof clickSplit;
clickTableButton: typeof clickTableButton;
datasourceError: string;
datasourceInstance: any;
datasourceLoading: boolean | null;
datasourceMissing: boolean;
exploreDatasources: DataSourceSelectItem[];
exploreId: ExploreId;
graphResult?: any[];
highlightLogsExpression: typeof highlightLogsExpression;
history: HistoryItem[];
@ -70,9 +77,6 @@ interface ExploreProps {
logsHighlighterExpressions?: string[];
logsResult?: LogsModel;
modifyQueries: typeof modifyQueries;
onChangeSplit: (split: boolean, state?: ExploreState) => void;
onSaveState: (key: string, state: ExploreState) => void;
position: string;
queryTransactions: QueryTransaction[];
removeQueryRow: typeof removeQueryRow;
range: RawTimeRange;
@ -83,8 +87,6 @@ interface ExploreProps {
scanStart: typeof scanStart;
scanStop: typeof scanStop;
split: boolean;
splitState?: ExploreState;
stateKey: string;
showingGraph: boolean;
showingLogs: boolean;
showingStartPage?: boolean;
@ -132,7 +134,7 @@ interface ExploreProps {
* The result viewers determine some of the query options sent to the datasource, e.g.,
* `format`, to indicate eventual transformations by the datasources' result transformers.
*/
export class Explore extends React.PureComponent<ExploreProps, any> {
export class Explore extends React.PureComponent<ExploreProps> {
el: any;
exploreEvents: Emitter;
/**
@ -147,13 +149,23 @@ export class Explore extends React.PureComponent<ExploreProps, any> {
}
async componentDidMount() {
// Load URL state and parse range
const { datasource, queries, range } = this.props.urlState as ExploreUrlState;
const initialDatasource = datasource || store.get(LAST_USED_DATASOURCE_KEY);
const initialQueries: DataQuery[] = ensureQueries(queries);
const initialRange = { from: parseTime(range.from), to: parseTime(range.to) };
const width = this.el ? this.el.offsetWidth : 0;
this.props.initializeExplore(initialDatasource, initialQueries, initialRange, width, this.exploreEvents);
const { exploreId, split, urlState } = this.props;
if (!split) {
// Load URL state and parse range
const { datasource, queries, range = DEFAULT_RANGE } = (urlState || {}) as ExploreUrlState;
const initialDatasource = datasource || store.get(LAST_USED_DATASOURCE_KEY);
const initialQueries: DataQuery[] = ensureQueries(queries);
const initialRange = { from: parseTime(range.from), to: parseTime(range.to) };
const width = this.el ? this.el.offsetWidth : 0;
this.props.initializeExplore(
exploreId,
initialDatasource,
initialQueries,
initialRange,
width,
this.exploreEvents
);
}
}
componentWillUnmount() {
@ -165,17 +177,17 @@ export class Explore extends React.PureComponent<ExploreProps, any> {
};
onAddQueryRow = index => {
this.props.addQueryRow(index);
this.props.addQueryRow(this.props.exploreId, index);
};
onChangeDatasource = async option => {
this.props.changeDatasource(option.value);
this.props.changeDatasource(this.props.exploreId, option.value);
};
onChangeQuery = (query: DataQuery, index: number, override?: boolean) => {
const { changeQuery, datasourceInstance } = this.props;
const { changeQuery, datasourceInstance, exploreId } = this.props;
changeQuery(query, index, override);
changeQuery(exploreId, query, index, override);
if (query && !override && datasourceInstance.getHighlighterExpression && index === 0) {
// Live preview of log search matches. Only use on first row for now
this.updateLogsHighlights(query);
@ -186,43 +198,36 @@ export class Explore extends React.PureComponent<ExploreProps, any> {
if (this.props.scanning && !changedByScanner) {
this.onStopScanning();
}
this.props.changeTime(range);
this.props.changeTime(this.props.exploreId, range);
};
onClickClear = () => {
this.props.clickClear();
this.props.clickClear(this.props.exploreId);
};
onClickCloseSplit = () => {
const { onChangeSplit } = this.props;
if (onChangeSplit) {
onChangeSplit(false);
}
this.props.clickCloseSplit();
};
onClickGraphButton = () => {
this.props.clickGraphButton();
this.props.clickGraphButton(this.props.exploreId);
};
onClickLogsButton = () => {
this.props.clickLogsButton();
this.props.clickLogsButton(this.props.exploreId);
};
// Use this in help pages to set page to a single query
onClickExample = (query: DataQuery) => {
this.props.clickExample(query);
this.props.clickExample(this.props.exploreId, query);
};
onClickSplit = () => {
const { onChangeSplit } = this.props;
if (onChangeSplit) {
// const state = this.cloneState();
// onChangeSplit(true, state);
}
this.props.clickSplit();
};
onClickTableButton = () => {
this.props.clickTableButton();
this.props.clickTableButton(this.props.exploreId);
};
onClickLabel = (key: string, value: string) => {
@ -233,18 +238,22 @@ export class Explore extends React.PureComponent<ExploreProps, any> {
const { datasourceInstance } = this.props;
if (datasourceInstance && datasourceInstance.modifyQuery) {
const modifier = (queries: DataQuery, action: any) => datasourceInstance.modifyQuery(queries, action);
this.props.modifyQueries(action, index, modifier);
this.props.modifyQueries(this.props.exploreId, action, index, modifier);
}
};
onRemoveQueryRow = index => {
this.props.removeQueryRow(index);
this.props.removeQueryRow(this.props.exploreId, index);
};
onResize = (size: { height: number; width: number }) => {
this.props.changeSize(this.props.exploreId, size);
};
onStartScanning = () => {
// Scanner will trigger a query
const scanner = this.scanPreviousRange;
this.props.scanStart(scanner);
this.props.scanStart(this.props.exploreId, scanner);
};
scanPreviousRange = (): RawTimeRange => {
@ -253,30 +262,21 @@ export class Explore extends React.PureComponent<ExploreProps, any> {
};
onStopScanning = () => {
this.props.scanStop();
this.props.scanStop(this.props.exploreId);
};
onSubmit = () => {
this.props.runQueries();
this.props.runQueries(this.props.exploreId);
};
updateLogsHighlights = _.debounce((value: DataQuery) => {
const { datasourceInstance } = this.props;
if (datasourceInstance.getHighlighterExpression) {
const expressions = [datasourceInstance.getHighlighterExpression(value)];
this.props.highlightLogsExpression(expressions);
this.props.highlightLogsExpression(this.props.exploreId, expressions);
}
}, 500);
// cloneState(): ExploreState {
// // Copy state, but copy queries including modifications
// return {
// ...this.state,
// queryTransactions: [],
// initialQueries: [...this.modifiedQueries],
// };
// }
// saveState = () => {
// const { stateKey, onSaveState } = this.props;
// onSaveState(stateKey, this.cloneState());
@ -290,13 +290,13 @@ export class Explore extends React.PureComponent<ExploreProps, any> {
datasourceLoading,
datasourceMissing,
exploreDatasources,
exploreId,
graphResult,
history,
initialQueries,
logsHighlighterExpressions,
logsResult,
queryTransactions,
position,
range,
scanning,
scanRange,
@ -323,7 +323,7 @@ export class Explore extends React.PureComponent<ExploreProps, any> {
return (
<div className={exploreClass} ref={this.getRef}>
<div className="navbar">
{position === 'left' ? (
{exploreId === 'left' ? (
<div>
<a className="navbar-page-btn">
<i className="fa fa-rocket" />
@ -347,7 +347,7 @@ export class Explore extends React.PureComponent<ExploreProps, any> {
</div>
) : null}
<div className="navbar__spacer" />
{position === 'left' && !split ? (
{exploreId === 'left' && !split ? (
<div className="navbar-buttons">
<button className="btn navbar-button" onClick={this.onClickSplit}>
Split
@ -378,83 +378,96 @@ export class Explore extends React.PureComponent<ExploreProps, any> {
</div>
)}
{datasourceInstance && !datasourceError ? (
<div className="explore-container">
<QueryRows
datasource={datasourceInstance}
history={history}
initialQueries={initialQueries}
onAddQueryRow={this.onAddQueryRow}
onChangeQuery={this.onChangeQuery}
onClickHintFix={this.onModifyQueries}
onExecuteQuery={this.onSubmit}
onRemoveQueryRow={this.onRemoveQueryRow}
transactions={queryTransactions}
exploreEvents={this.exploreEvents}
range={range}
/>
<main className="m-t-2">
<ErrorBoundary>
{showingStartPage && <StartPage onClickExample={this.onClickExample} />}
{!showingStartPage && (
<>
{supportsGraph && (
<Panel
label="Graph"
isOpen={showingGraph}
loading={graphLoading}
onToggle={this.onClickGraphButton}
>
<Graph
data={graphResult}
height={graphHeight}
id={`explore-graph-${position}`}
onChangeTime={this.onChangeTime}
range={range}
split={split}
/>
</Panel>
)}
{supportsTable && (
<Panel
label="Table"
loading={tableLoading}
isOpen={showingTable}
onToggle={this.onClickTableButton}
>
<Table data={tableResult} loading={tableLoading} onClickCell={this.onClickLabel} />
</Panel>
)}
{supportsLogs && (
<Panel label="Logs" loading={logsLoading} isOpen={showingLogs} onToggle={this.onClickLogsButton}>
<Logs
data={logsResult}
key={logsResult.id}
highlighterExpressions={logsHighlighterExpressions}
loading={logsLoading}
position={position}
onChangeTime={this.onChangeTime}
onClickLabel={this.onClickLabel}
onStartScanning={this.onStartScanning}
onStopScanning={this.onStopScanning}
range={range}
scanning={scanning}
scanRange={scanRange}
/>
</Panel>
)}
</>
{datasourceInstance &&
!datasourceError && (
<div className="explore-container">
<QueryRows
datasource={datasourceInstance}
history={history}
initialQueries={initialQueries}
onAddQueryRow={this.onAddQueryRow}
onChangeQuery={this.onChangeQuery}
onClickHintFix={this.onModifyQueries}
onExecuteQuery={this.onSubmit}
onRemoveQueryRow={this.onRemoveQueryRow}
transactions={queryTransactions}
exploreEvents={this.exploreEvents}
range={range}
/>
<AutoSizer onResize={this.onResize} disableHeight>
{({ width }) => (
<main className="m-t-2" style={{ width }}>
<ErrorBoundary>
{showingStartPage && <StartPage onClickExample={this.onClickExample} />}
{!showingStartPage && (
<>
{supportsGraph && (
<Panel
label="Graph"
isOpen={showingGraph}
loading={graphLoading}
onToggle={this.onClickGraphButton}
>
<Graph
data={graphResult}
height={graphHeight}
id={`explore-graph-${exploreId}`}
onChangeTime={this.onChangeTime}
range={range}
split={split}
/>
</Panel>
)}
{supportsTable && (
<Panel
label="Table"
loading={tableLoading}
isOpen={showingTable}
onToggle={this.onClickTableButton}
>
<Table data={tableResult} loading={tableLoading} onClickCell={this.onClickLabel} />
</Panel>
)}
{supportsLogs && (
<Panel
label="Logs"
loading={logsLoading}
isOpen={showingLogs}
onToggle={this.onClickLogsButton}
>
<Logs
data={logsResult}
exploreId={exploreId}
key={logsResult.id}
highlighterExpressions={logsHighlighterExpressions}
loading={logsLoading}
onChangeTime={this.onChangeTime}
onClickLabel={this.onClickLabel}
onStartScanning={this.onStartScanning}
onStopScanning={this.onStopScanning}
range={range}
scanning={scanning}
scanRange={scanRange}
/>
</Panel>
)}
</>
)}
</ErrorBoundary>
</main>
)}
</ErrorBoundary>
</main>
</div>
) : null}
</AutoSizer>
</div>
)}
</div>
);
}
}
function mapStateToProps({ explore }) {
function mapStateToProps(state: StoreState, { exploreId }) {
const explore = state.explore;
const { split } = explore;
const item: ExploreItemState = explore[exploreId];
const {
StartPage,
datasourceError,
@ -480,7 +493,7 @@ function mapStateToProps({ explore }) {
supportsLogs,
supportsTable,
tableResult,
} = explore as ExploreState;
} = item;
return {
StartPage,
datasourceError,
@ -502,6 +515,7 @@ function mapStateToProps({ explore }) {
showingLogs,
showingStartPage,
showingTable,
split,
supportsGraph,
supportsLogs,
supportsTable,
@ -513,20 +527,22 @@ const mapDispatchToProps = {
addQueryRow,
changeDatasource,
changeQuery,
changeSize,
changeTime,
clickClear,
clickCloseSplit,
clickExample,
clickGraphButton,
clickLogsButton,
clickSplit,
clickTableButton,
highlightLogsExpression,
initializeExplore,
modifyQueries,
onSize: changeSize, // used by withSize HOC
removeQueryRow,
runQueries,
scanStart,
scanStop,
};
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(withSize()(Explore)));
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(Explore));

View File

@ -241,9 +241,9 @@ function renderMetaItem(value: any, kind: LogsMetaKind) {
interface LogsProps {
data: LogsModel;
exploreId: string;
highlighterExpressions: string[];
loading: boolean;
position: string;
range?: RawTimeRange;
scanning?: boolean;
scanRange?: RawTimeRange;
@ -348,10 +348,10 @@ export default class Logs extends PureComponent<LogsProps, LogsState> {
render() {
const {
data,
exploreId,
highlighterExpressions,
loading = false,
onClickLabel,
position,
range,
scanning,
scanRange,
@ -400,7 +400,7 @@ export default class Logs extends PureComponent<LogsProps, LogsState> {
data={data.series}
height="100px"
range={range}
id={`explore-logs-graph-${position}`}
id={`explore-logs-graph-${exploreId}`}
onChangeTime={this.props.onChangeTime}
onToggleSeries={this.onToggleLogLevel}
userOptions={graphOptions}

View File

@ -3,9 +3,9 @@ import { hot } from 'react-hot-loader';
import { connect } from 'react-redux';
import { updateLocation } from 'app/core/actions';
import { serializeStateToUrlParam, parseUrlState } from 'app/core/utils/explore';
// import { serializeStateToUrlParam, parseUrlState } from 'app/core/utils/explore';
import { StoreState } from 'app/types';
import { ExploreState } from 'app/types/explore';
import { ExploreId } from 'app/types/explore';
import ErrorBoundary from './ErrorBoundary';
import Explore from './Explore';
@ -13,81 +13,41 @@ import Explore from './Explore';
interface WrapperProps {
backendSrv?: any;
datasourceSrv?: any;
updateLocation: typeof updateLocation;
urlStates: { [key: string]: string };
}
interface WrapperState {
split: boolean;
splitState: ExploreState;
updateLocation: typeof updateLocation;
// urlStates: { [key: string]: string };
}
const STATE_KEY_LEFT = 'state';
const STATE_KEY_RIGHT = 'stateRight';
export class Wrapper extends Component<WrapperProps, WrapperState> {
urlStates: { [key: string]: string };
export class Wrapper extends Component<WrapperProps> {
// urlStates: { [key: string]: string };
constructor(props: WrapperProps) {
super(props);
this.urlStates = props.urlStates;
this.state = {
split: Boolean(props.urlStates[STATE_KEY_RIGHT]),
splitState: undefined,
};
// this.urlStates = props.urlStates;
}
onChangeSplit = (split: boolean, splitState: ExploreState) => {
this.setState({ split, splitState });
// When closing split, remove URL state for split part
if (!split) {
delete this.urlStates[STATE_KEY_RIGHT];
this.props.updateLocation({
query: this.urlStates,
});
}
};
onSaveState = (key: string, state: ExploreState) => {
const urlState = serializeStateToUrlParam(state, true);
this.urlStates[key] = urlState;
this.props.updateLocation({
query: this.urlStates,
});
};
// onSaveState = (key: string, state: ExploreState) => {
// const urlState = serializeStateToUrlParam(state, true);
// this.urlStates[key] = urlState;
// this.props.updateLocation({
// query: this.urlStates,
// });
// };
render() {
const { datasourceSrv } = this.props;
const { split } = this.props;
// State overrides for props from first Explore
const { split, splitState } = this.state;
const urlStateLeft = parseUrlState(this.urlStates[STATE_KEY_LEFT]);
const urlStateRight = parseUrlState(this.urlStates[STATE_KEY_RIGHT]);
// const urlStateLeft = parseUrlState(this.urlStates[STATE_KEY_LEFT]);
// const urlStateRight = parseUrlState(this.urlStates[STATE_KEY_RIGHT]);
return (
<div className="explore-wrapper">
<ErrorBoundary>
<Explore
datasourceSrv={datasourceSrv}
onChangeSplit={this.onChangeSplit}
onSaveState={this.onSaveState}
position="left"
split={split}
stateKey={STATE_KEY_LEFT}
urlState={urlStateLeft}
/>
<Explore exploreId={ExploreId.left} />
</ErrorBoundary>
{split && (
<ErrorBoundary>
<Explore
datasourceSrv={datasourceSrv}
onChangeSplit={this.onChangeSplit}
onSaveState={this.onSaveState}
position="right"
split={split}
splitState={splitState}
stateKey={STATE_KEY_RIGHT}
urlState={urlStateRight}
/>
<Explore exploreId={ExploreId.right} />
</ErrorBoundary>
)}
</div>
@ -95,9 +55,11 @@ export class Wrapper extends Component<WrapperProps, WrapperState> {
}
}
const mapStateToProps = (state: StoreState) => ({
urlStates: state.location.query,
});
const mapStateToProps = (state: StoreState) => {
// urlStates: state.location.query,
const { split } = state.explore;
return { split };
};
const mapDispatchToProps = {
updateLocation,

View File

@ -17,6 +17,7 @@ import { DataSourceSelectItem } from 'app/types/datasources';
import { DataQuery, StoreState } from 'app/types';
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
import {
ExploreId,
HistoryItem,
RangeScanner,
ResultType,
@ -26,7 +27,7 @@ import {
QueryHintGetter,
} from 'app/types/explore';
import { Emitter } from 'app/core/core';
import { dispatch } from 'rxjs/internal/observable/pairs';
import { ExploreItemState } from './reducers';
export enum ActionTypes {
AddQueryRow = 'ADD_QUERY_ROW',
@ -35,9 +36,11 @@ export enum ActionTypes {
ChangeSize = 'CHANGE_SIZE',
ChangeTime = 'CHANGE_TIME',
ClickClear = 'CLICK_CLEAR',
ClickCloseSplit = 'CLICK_CLOSE_SPLIT',
ClickExample = 'CLICK_EXAMPLE',
ClickGraphButton = 'CLICK_GRAPH_BUTTON',
ClickLogsButton = 'CLICK_LOGS_BUTTON',
ClickSplit = 'CLICK_SPLIT',
ClickTableButton = 'CLICK_TABLE_BUTTON',
HighlightLogsExpression = 'HIGHLIGHT_LOGS_EXPRESSION',
InitializeExplore = 'INITIALIZE_EXPLORE',
@ -59,12 +62,14 @@ export enum ActionTypes {
export interface AddQueryRowAction {
type: ActionTypes.AddQueryRow;
exploreId: ExploreId;
index: number;
query: DataQuery;
}
export interface ChangeQueryAction {
type: ActionTypes.ChangeQuery;
exploreId: ExploreId;
query: DataQuery;
index: number;
override: boolean;
@ -72,38 +77,55 @@ export interface ChangeQueryAction {
export interface ChangeSizeAction {
type: ActionTypes.ChangeSize;
exploreId: ExploreId;
width: number;
height: number;
}
export interface ChangeTimeAction {
type: ActionTypes.ChangeTime;
exploreId: ExploreId;
range: TimeRange;
}
export interface ClickClearAction {
type: ActionTypes.ClickClear;
exploreId: ExploreId;
}
export interface ClickCloseSplitAction {
type: ActionTypes.ClickCloseSplit;
}
export interface ClickExampleAction {
type: ActionTypes.ClickExample;
exploreId: ExploreId;
query: DataQuery;
}
export interface ClickGraphButtonAction {
type: ActionTypes.ClickGraphButton;
exploreId: ExploreId;
}
export interface ClickLogsButtonAction {
type: ActionTypes.ClickLogsButton;
exploreId: ExploreId;
}
export interface ClickSplitAction {
type: ActionTypes.ClickSplit;
itemState: ExploreItemState;
}
export interface ClickTableButtonAction {
type: ActionTypes.ClickTableButton;
exploreId: ExploreId;
}
export interface InitializeExploreAction {
type: ActionTypes.InitializeExplore;
exploreId: ExploreId;
containerWidth: number;
datasource: string;
eventBridge: Emitter;
@ -114,25 +136,30 @@ export interface InitializeExploreAction {
export interface HighlightLogsExpressionAction {
type: ActionTypes.HighlightLogsExpression;
exploreId: ExploreId;
expressions: string[];
}
export interface LoadDatasourceFailureAction {
type: ActionTypes.LoadDatasourceFailure;
exploreId: ExploreId;
error: string;
}
export interface LoadDatasourcePendingAction {
type: ActionTypes.LoadDatasourcePending;
exploreId: ExploreId;
datasourceId: number;
}
export interface LoadDatasourceMissingAction {
type: ActionTypes.LoadDatasourceMissing;
exploreId: ExploreId;
}
export interface LoadDatasourceSuccessAction {
type: ActionTypes.LoadDatasourceSuccess;
exploreId: ExploreId;
StartPage?: any;
datasourceInstance: any;
history: HistoryItem[];
@ -147,6 +174,7 @@ export interface LoadDatasourceSuccessAction {
export interface ModifyQueriesAction {
type: ActionTypes.ModifyQueries;
exploreId: ExploreId;
modification: any;
index: number;
modifier: (queries: DataQuery[], modification: any) => DataQuery[];
@ -154,11 +182,13 @@ export interface ModifyQueriesAction {
export interface QueryTransactionFailureAction {
type: ActionTypes.QueryTransactionFailure;
exploreId: ExploreId;
queryTransactions: QueryTransaction[];
}
export interface QueryTransactionStartAction {
type: ActionTypes.QueryTransactionStart;
exploreId: ExploreId;
resultType: ResultType;
rowIndex: number;
transaction: QueryTransaction;
@ -166,27 +196,32 @@ export interface QueryTransactionStartAction {
export interface QueryTransactionSuccessAction {
type: ActionTypes.QueryTransactionSuccess;
exploreId: ExploreId;
history: HistoryItem[];
queryTransactions: QueryTransaction[];
}
export interface RemoveQueryRowAction {
type: ActionTypes.RemoveQueryRow;
exploreId: ExploreId;
index: number;
}
export interface ScanStartAction {
type: ActionTypes.ScanStart;
exploreId: ExploreId;
scanner: RangeScanner;
}
export interface ScanRangeAction {
type: ActionTypes.ScanRange;
exploreId: ExploreId;
range: RawTimeRange;
}
export interface ScanStopAction {
type: ActionTypes.ScanStop;
exploreId: ExploreId;
}
export type Action =
@ -195,9 +230,11 @@ export type Action =
| ChangeSizeAction
| ChangeTimeAction
| ClickClearAction
| ClickCloseSplitAction
| ClickExampleAction
| ClickGraphButtonAction
| ClickLogsButtonAction
| ClickSplitAction
| ClickTableButtonAction
| HighlightLogsExpressionAction
| InitializeExploreAction
@ -215,94 +252,126 @@ export type Action =
| ScanStopAction;
type ThunkResult<R> = ThunkAction<R, StoreState, undefined, Action>;
export function addQueryRow(index: number): AddQueryRowAction {
export function addQueryRow(exploreId: ExploreId, index: number): AddQueryRowAction {
const query = generateEmptyQuery(index + 1);
return { type: ActionTypes.AddQueryRow, index, query };
return { type: ActionTypes.AddQueryRow, exploreId, index, query };
}
export function changeDatasource(datasource: string): ThunkResult<void> {
export function changeDatasource(exploreId: ExploreId, datasource: string): ThunkResult<void> {
return async dispatch => {
const instance = await getDatasourceSrv().get(datasource);
dispatch(loadDatasource(instance));
dispatch(loadDatasource(exploreId, instance));
};
}
export function changeQuery(query: DataQuery, index: number, override: boolean): ThunkResult<void> {
export function changeQuery(
exploreId: ExploreId,
query: DataQuery,
index: number,
override: boolean
): ThunkResult<void> {
return dispatch => {
// Null query means reset
if (query === null) {
query = { ...generateEmptyQuery(index) };
}
dispatch({ type: ActionTypes.ChangeQuery, query, index, override });
dispatch({ type: ActionTypes.ChangeQuery, exploreId, query, index, override });
if (override) {
dispatch(runQueries());
dispatch(runQueries(exploreId));
}
};
}
export function changeSize({ height, width }: { height: number; width: number }): ChangeSizeAction {
return { type: ActionTypes.ChangeSize, height, width };
export function changeSize(
exploreId: ExploreId,
{ height, width }: { height: number; width: number }
): ChangeSizeAction {
return { type: ActionTypes.ChangeSize, exploreId, height, width };
}
export function changeTime(range: TimeRange): ThunkResult<void> {
export function changeTime(exploreId: ExploreId, range: TimeRange): ThunkResult<void> {
return dispatch => {
dispatch({ type: ActionTypes.ChangeTime, range });
dispatch(runQueries());
dispatch({ type: ActionTypes.ChangeTime, exploreId, range });
dispatch(runQueries(exploreId));
};
}
export function clickExample(rawQuery: DataQuery): ThunkResult<void> {
export function clickClear(exploreId: ExploreId): ThunkResult<void> {
return dispatch => {
const query = { ...rawQuery, ...generateEmptyQuery() };
dispatch({
type: ActionTypes.ClickExample,
query,
});
dispatch(runQueries());
};
}
export function clickClear(): ThunkResult<void> {
return dispatch => {
dispatch(scanStop());
dispatch({ type: ActionTypes.ClickClear });
dispatch(scanStop(exploreId));
dispatch({ type: ActionTypes.ClickClear, exploreId });
// TODO save state
};
}
export function clickGraphButton(): ThunkResult<void> {
export function clickCloseSplit(): ThunkResult<void> {
return dispatch => {
dispatch({ type: ActionTypes.ClickCloseSplit });
// When closing split, remove URL state for split part
// TODO save state
};
}
export function clickExample(exploreId: ExploreId, rawQuery: DataQuery): ThunkResult<void> {
return dispatch => {
const query = { ...rawQuery, ...generateEmptyQuery() };
dispatch({
type: ActionTypes.ClickExample,
exploreId,
query,
});
dispatch(runQueries(exploreId));
};
}
export function clickGraphButton(exploreId: ExploreId): ThunkResult<void> {
return (dispatch, getState) => {
dispatch({ type: ActionTypes.ClickGraphButton });
if (getState().explore.showingGraph) {
dispatch(runQueries());
dispatch({ type: ActionTypes.ClickGraphButton, exploreId });
if (getState().explore[exploreId].showingGraph) {
dispatch(runQueries(exploreId));
}
};
}
export function clickLogsButton(): ThunkResult<void> {
export function clickLogsButton(exploreId: ExploreId): ThunkResult<void> {
return (dispatch, getState) => {
dispatch({ type: ActionTypes.ClickLogsButton });
if (getState().explore.showingLogs) {
dispatch(runQueries());
dispatch({ type: ActionTypes.ClickLogsButton, exploreId });
if (getState().explore[exploreId].showingLogs) {
dispatch(runQueries(exploreId));
}
};
}
export function clickTableButton(): ThunkResult<void> {
export function clickSplit(): ThunkResult<void> {
return (dispatch, getState) => {
dispatch({ type: ActionTypes.ClickTableButton });
if (getState().explore.showingTable) {
dispatch(runQueries());
// Clone left state to become the right state
const leftState = getState().explore.left;
const itemState = {
...leftState,
queryTransactions: [],
initialQueries: leftState.modifiedQueries.slice(),
};
dispatch({ type: ActionTypes.ClickSplit, itemState });
// TODO save state
};
}
export function clickTableButton(exploreId: ExploreId): ThunkResult<void> {
return (dispatch, getState) => {
dispatch({ type: ActionTypes.ClickTableButton, exploreId });
if (getState().explore[exploreId].showingTable) {
dispatch(runQueries(exploreId));
}
};
}
export function highlightLogsExpression(expressions: string[]): HighlightLogsExpressionAction {
return { type: ActionTypes.HighlightLogsExpression, expressions };
export function highlightLogsExpression(exploreId: ExploreId, expressions: string[]): HighlightLogsExpressionAction {
return { type: ActionTypes.HighlightLogsExpression, exploreId, expressions };
}
export function initializeExplore(
exploreId: ExploreId,
datasource: string,
queries: DataQuery[],
range: RawTimeRange,
@ -320,6 +389,7 @@ export function initializeExplore(
dispatch({
type: ActionTypes.InitializeExplore,
exploreId,
containerWidth,
datasource,
eventBridge,
@ -335,26 +405,35 @@ export function initializeExplore(
} else {
instance = await getDatasourceSrv().get();
}
dispatch(loadDatasource(instance));
dispatch(loadDatasource(exploreId, instance));
} else {
dispatch(loadDatasourceMissing);
dispatch(loadDatasourceMissing(exploreId));
}
};
}
export const loadDatasourceFailure = (error: string): LoadDatasourceFailureAction => ({
export const loadDatasourceFailure = (exploreId: ExploreId, error: string): LoadDatasourceFailureAction => ({
type: ActionTypes.LoadDatasourceFailure,
exploreId,
error,
});
export const loadDatasourceMissing: LoadDatasourceMissingAction = { type: ActionTypes.LoadDatasourceMissing };
export const loadDatasourceMissing = (exploreId: ExploreId): LoadDatasourceMissingAction => ({
type: ActionTypes.LoadDatasourceMissing,
exploreId,
});
export const loadDatasourcePending = (datasourceId: number): LoadDatasourcePendingAction => ({
export const loadDatasourcePending = (exploreId: ExploreId, datasourceId: number): LoadDatasourcePendingAction => ({
type: ActionTypes.LoadDatasourcePending,
exploreId,
datasourceId,
});
export const loadDatasourceSuccess = (instance: any, queries: DataQuery[]): LoadDatasourceSuccessAction => {
export const loadDatasourceSuccess = (
exploreId: ExploreId,
instance: any,
queries: DataQuery[]
): LoadDatasourceSuccessAction => {
// Capabilities
const supportsGraph = instance.meta.metrics;
const supportsLogs = instance.meta.logs;
@ -369,6 +448,7 @@ export const loadDatasourceSuccess = (instance: any, queries: DataQuery[]): Load
return {
type: ActionTypes.LoadDatasourceSuccess,
exploreId,
StartPage,
datasourceInstance: instance,
history,
@ -381,12 +461,12 @@ export const loadDatasourceSuccess = (instance: any, queries: DataQuery[]): Load
};
};
export function loadDatasource(instance: any): ThunkResult<void> {
export function loadDatasource(exploreId: ExploreId, instance: any): ThunkResult<void> {
return async (dispatch, getState) => {
const datasourceId = instance.meta.id;
// Keep ID to track selection
dispatch(loadDatasourcePending(datasourceId));
dispatch(loadDatasourcePending(exploreId, datasourceId));
let datasourceError = null;
try {
@ -396,11 +476,11 @@ export function loadDatasource(instance: any): ThunkResult<void> {
datasourceError = (error && error.statusText) || 'Network error';
}
if (datasourceError) {
dispatch(loadDatasourceFailure(datasourceError));
dispatch(loadDatasourceFailure(exploreId, datasourceError));
return;
}
if (datasourceId !== getState().explore.requestedDatasourceId) {
if (datasourceId !== getState().explore[exploreId].requestedDatasourceId) {
// User already changed datasource again, discard results
return;
}
@ -410,9 +490,9 @@ export function loadDatasource(instance: any): ThunkResult<void> {
}
// Check if queries can be imported from previously selected datasource
const queries = getState().explore.modifiedQueries;
const queries = getState().explore[exploreId].modifiedQueries;
let importedQueries = queries;
const origin = getState().explore.datasourceInstance;
const origin = getState().explore[exploreId].datasourceInstance;
if (origin) {
if (origin.meta.id === instance.meta.id) {
// Keep same queries if same type of datasource
@ -426,7 +506,7 @@ export function loadDatasource(instance: any): ThunkResult<void> {
}
}
if (datasourceId !== getState().explore.requestedDatasourceId) {
if (datasourceId !== getState().explore[exploreId].requestedDatasourceId) {
// User already changed datasource again, discard results
return;
}
@ -437,23 +517,33 @@ export function loadDatasource(instance: any): ThunkResult<void> {
...generateEmptyQuery(i),
}));
dispatch(loadDatasourceSuccess(instance, nextQueries));
dispatch(runQueries());
dispatch(loadDatasourceSuccess(exploreId, instance, nextQueries));
dispatch(runQueries(exploreId));
};
}
export function modifyQueries(modification: any, index: number, modifier: any): ThunkResult<void> {
export function modifyQueries(
exploreId: ExploreId,
modification: any,
index: number,
modifier: any
): ThunkResult<void> {
return dispatch => {
dispatch({ type: ActionTypes.ModifyQueries, modification, index, modifier });
dispatch({ type: ActionTypes.ModifyQueries, exploreId, modification, index, modifier });
if (!modification.preventSubmit) {
dispatch(runQueries());
dispatch(runQueries(exploreId));
}
};
}
export function queryTransactionFailure(transactionId: string, response: any, datasourceId: string): ThunkResult<void> {
export function queryTransactionFailure(
exploreId: ExploreId,
transactionId: string,
response: any,
datasourceId: string
): ThunkResult<void> {
return (dispatch, getState) => {
const { datasourceInstance, queryTransactions } = getState().explore;
const { datasourceInstance, queryTransactions } = getState().explore[exploreId];
if (datasourceInstance.meta.id !== datasourceId || response.cancelled) {
// Navigated away, queries did not matter
return;
@ -500,19 +590,21 @@ export function queryTransactionFailure(transactionId: string, response: any, da
return qt;
});
dispatch({ type: ActionTypes.QueryTransactionFailure, queryTransactions: nextQueryTransactions });
dispatch({ type: ActionTypes.QueryTransactionFailure, exploreId, queryTransactions: nextQueryTransactions });
};
}
export function queryTransactionStart(
exploreId: ExploreId,
transaction: QueryTransaction,
resultType: ResultType,
rowIndex: number
): QueryTransactionStartAction {
return { type: ActionTypes.QueryTransactionStart, resultType, rowIndex, transaction };
return { type: ActionTypes.QueryTransactionStart, exploreId, resultType, rowIndex, transaction };
}
export function queryTransactionSuccess(
exploreId: ExploreId,
transactionId: string,
result: any,
latency: number,
@ -520,7 +612,7 @@ export function queryTransactionSuccess(
datasourceId: string
): ThunkResult<void> {
return (dispatch, getState) => {
const { datasourceInstance, history, queryTransactions, scanner, scanning } = getState().explore;
const { datasourceInstance, history, queryTransactions, scanner, scanning } = getState().explore[exploreId];
// If datasource already changed, results do not matter
if (datasourceInstance.meta.id !== datasourceId) {
@ -558,6 +650,7 @@ export function queryTransactionSuccess(
dispatch({
type: ActionTypes.QueryTransactionSuccess,
exploreId,
history: nextHistory,
queryTransactions: nextQueryTransactions,
});
@ -568,24 +661,24 @@ export function queryTransactionSuccess(
const other = nextQueryTransactions.find(qt => qt.scanning && !qt.done);
if (!other) {
const range = scanner();
dispatch({ type: ActionTypes.ScanRange, range });
dispatch({ type: ActionTypes.ScanRange, exploreId, range });
}
} else {
// We can stop scanning if we have a result
dispatch(scanStop());
dispatch(scanStop(exploreId));
}
}
};
}
export function removeQueryRow(index: number): ThunkResult<void> {
export function removeQueryRow(exploreId: ExploreId, index: number): ThunkResult<void> {
return dispatch => {
dispatch({ type: ActionTypes.RemoveQueryRow, index });
dispatch(runQueries());
dispatch({ type: ActionTypes.RemoveQueryRow, exploreId, index });
dispatch(runQueries(exploreId));
};
}
export function runQueries() {
export function runQueries(exploreId: ExploreId) {
return (dispatch, getState) => {
const {
datasourceInstance,
@ -596,10 +689,10 @@ export function runQueries() {
supportsGraph,
supportsLogs,
supportsTable,
} = getState().explore;
} = getState().explore[exploreId];
if (!hasNonEmptyQuery(modifiedQueries)) {
dispatch({ type: ActionTypes.RunQueriesEmpty });
dispatch({ type: ActionTypes.RunQueriesEmpty, exploreId });
return;
}
@ -611,6 +704,7 @@ export function runQueries() {
if (showingTable && supportsTable) {
dispatch(
runQueriesForType(
exploreId,
'Table',
{
interval,
@ -625,6 +719,7 @@ export function runQueries() {
if (showingGraph && supportsGraph) {
dispatch(
runQueriesForType(
exploreId,
'Graph',
{
interval,
@ -636,13 +731,18 @@ export function runQueries() {
);
}
if (showingLogs && supportsLogs) {
dispatch(runQueriesForType('Logs', { interval, format: 'logs' }));
dispatch(runQueriesForType(exploreId, 'Logs', { interval, format: 'logs' }));
}
// TODO save state
};
}
function runQueriesForType(resultType: ResultType, queryOptions: QueryOptions, resultGetter?: any) {
function runQueriesForType(
exploreId: ExploreId,
resultType: ResultType,
queryOptions: QueryOptions,
resultGetter?: any
) {
return async (dispatch, getState) => {
const {
datasourceInstance,
@ -651,7 +751,7 @@ function runQueriesForType(resultType: ResultType, queryOptions: QueryOptions, r
queryIntervals,
range,
scanning,
} = getState().explore;
} = getState().explore[exploreId];
const datasourceId = datasourceInstance.meta.id;
// Run all queries concurrently
@ -665,30 +765,30 @@ function runQueriesForType(resultType: ResultType, queryOptions: QueryOptions, r
queryIntervals,
scanning
);
dispatch(queryTransactionStart(transaction, resultType, rowIndex));
dispatch(queryTransactionStart(exploreId, transaction, resultType, rowIndex));
try {
const now = Date.now();
const res = await datasourceInstance.query(transaction.options);
eventBridge.emit('data-received', res.data || []);
const latency = Date.now() - now;
const results = resultGetter ? resultGetter(res.data) : res.data;
dispatch(queryTransactionSuccess(transaction.id, results, latency, queries, datasourceId));
dispatch(queryTransactionSuccess(exploreId, transaction.id, results, latency, queries, datasourceId));
} catch (response) {
eventBridge.emit('data-error', response);
dispatch(queryTransactionFailure(transaction.id, response, datasourceId));
dispatch(queryTransactionFailure(exploreId, transaction.id, response, datasourceId));
}
});
};
}
export function scanStart(scanner: RangeScanner): ThunkResult<void> {
export function scanStart(exploreId: ExploreId, scanner: RangeScanner): ThunkResult<void> {
return dispatch => {
dispatch({ type: ActionTypes.ScanStart, scanner });
dispatch({ type: ActionTypes.ScanStart, exploreId, scanner });
const range = scanner();
dispatch({ type: ActionTypes.ScanRange, range });
dispatch({ type: ActionTypes.ScanRange, exploreId, range });
};
}
export function scanStop(): ScanStopAction {
return { type: ActionTypes.ScanStop };
export function scanStop(exploreId: ExploreId): ScanStopAction {
return { type: ActionTypes.ScanStop, exploreId };
}

View File

@ -16,7 +16,14 @@ import { LogsModel } from 'app/core/logs_model';
import TableModel from 'app/core/table_model';
// TODO move to types
export interface ExploreState {
split: boolean;
left: ExploreItemState;
right: ExploreItemState;
}
export interface ExploreItemState {
StartPage?: any;
containerWidth: number;
datasourceInstance: any;
@ -57,7 +64,7 @@ export const DEFAULT_RANGE = {
// Millies step for helper bar charts
const DEFAULT_GRAPH_INTERVAL = 15 * 1000;
const initialExploreState: ExploreState = {
const makeExploreItemState = (): ExploreItemState => ({
StartPage: undefined,
containerWidth: 0,
datasourceInstance: null,
@ -79,9 +86,15 @@ const initialExploreState: ExploreState = {
supportsGraph: null,
supportsLogs: null,
supportsTable: null,
});
const initialExploreState: ExploreState = {
split: false,
left: makeExploreItemState(),
right: makeExploreItemState(),
};
export const exploreReducer = (state = initialExploreState, action: Action): ExploreState => {
const itemReducer = (state, action: Action): ExploreItemState => {
switch (action.type) {
case ActionTypes.AddQueryRow: {
const { initialQueries, modifiedQueries, queryTransactions } = state;
@ -407,6 +420,36 @@ export const exploreReducer = (state = initialExploreState, action: Action): Exp
return state;
};
export const exploreReducer = (state = initialExploreState, action: Action): ExploreState => {
switch (action.type) {
case ActionTypes.ClickCloseSplit: {
return {
...state,
split: false,
};
}
case ActionTypes.ClickSplit: {
return {
...state,
split: true,
right: action.itemState,
};
}
}
const { exploreId } = action as any;
if (exploreId !== undefined) {
const exploreItemState = state[exploreId];
return {
...state,
[exploreId]: itemReducer(exploreItemState, action),
};
}
return state;
};
export default {
explore: exploreReducer,
};

View File

@ -75,6 +75,11 @@ export interface CompletionItemGroup {
skipSort?: boolean;
}
export enum ExploreId {
left = 'left',
right = 'right',
}
export interface HistoryItem {
ts: number;
query: DataQuery;

View File

@ -1,5 +1,5 @@
.explore {
width: 100%;
flex: 1 1 auto;
&-container {
padding: $dashboard-padding;