Merge pull request #14821 from grafana/davkal/explore-redux

Explore: Redux migration
This commit is contained in:
David 2019-01-16 11:13:30 +01:00 committed by GitHub
commit 25e6d075ab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 2441 additions and 1190 deletions

View File

@ -6,26 +6,13 @@ import {
clearHistory,
hasNonEmptyQuery,
} from './explore';
import { ExploreState } from 'app/types/explore';
import { ExploreUrlState } from 'app/types/explore';
import store from 'app/core/store';
const DEFAULT_EXPLORE_STATE: ExploreState = {
const DEFAULT_EXPLORE_STATE: ExploreUrlState = {
datasource: null,
datasourceError: null,
datasourceLoading: null,
datasourceMissing: false,
exploreDatasources: [],
graphInterval: 1000,
history: [],
initialQueries: [],
queryTransactions: [],
queries: [],
range: DEFAULT_RANGE,
showingGraph: true,
showingLogs: true,
showingTable: true,
supportsGraph: null,
supportsLogs: null,
supportsTable: null,
};
describe('state functions', () => {
@ -68,21 +55,19 @@ describe('state functions', () => {
it('returns url parameter value for a state object', () => {
const state = {
...DEFAULT_EXPLORE_STATE,
initialDatasource: 'foo',
datasource: 'foo',
queries: [
{
expr: 'metric{test="a/b"}',
},
{
expr: 'super{foo="x/z"}',
},
],
range: {
from: 'now-5h',
to: 'now',
},
initialQueries: [
{
refId: '1',
expr: 'metric{test="a/b"}',
},
{
refId: '2',
expr: 'super{foo="x/z"}',
},
],
};
expect(serializeStateToUrlParam(state)).toBe(
'{"datasource":"foo","queries":[{"expr":"metric{test=\\"a/b\\"}"},' +
@ -93,21 +78,19 @@ describe('state functions', () => {
it('returns url parameter value for a state object', () => {
const state = {
...DEFAULT_EXPLORE_STATE,
initialDatasource: 'foo',
datasource: 'foo',
queries: [
{
expr: 'metric{test="a/b"}',
},
{
expr: 'super{foo="x/z"}',
},
],
range: {
from: 'now-5h',
to: 'now',
},
initialQueries: [
{
refId: '1',
expr: 'metric{test="a/b"}',
},
{
refId: '2',
expr: 'super{foo="x/z"}',
},
],
};
expect(serializeStateToUrlParam(state, true)).toBe(
'["now-5h","now","foo",{"expr":"metric{test=\\"a/b\\"}"},{"expr":"super{foo=\\"x/z\\"}"}]'
@ -119,35 +102,24 @@ describe('state functions', () => {
it('can parse the serialized state into the original state', () => {
const state = {
...DEFAULT_EXPLORE_STATE,
initialDatasource: 'foo',
datasource: 'foo',
queries: [
{
expr: 'metric{test="a/b"}',
},
{
expr: 'super{foo="x/z"}',
},
],
range: {
from: 'now - 5h',
to: 'now',
},
initialQueries: [
{
refId: '1',
expr: 'metric{test="a/b"}',
},
{
refId: '2',
expr: 'super{foo="x/z"}',
},
],
};
const serialized = serializeStateToUrlParam(state);
const parsed = parseUrlState(serialized);
// Account for datasource vs datasourceName
const { datasource, queries, ...rest } = parsed;
const resultState = {
...rest,
datasource: DEFAULT_EXPLORE_STATE.datasource,
initialDatasource: datasource,
initialQueries: queries,
};
expect(state).toMatchObject(resultState);
expect(state).toMatchObject(parsed);
});
});
});

View File

@ -1,6 +1,7 @@
import _ from 'lodash';
import { colors } from '@grafana/ui';
import { colors, RawTimeRange, IntervalValues } from '@grafana/ui';
import * as dateMath from 'app/core/utils/datemath';
import { renderUrl } from 'app/core/utils/url';
import kbn from 'app/core/utils/kbn';
import store from 'app/core/store';
@ -8,9 +9,15 @@ import { parse as parseDate } from 'app/core/utils/datemath';
import TimeSeries from 'app/core/time_series2';
import TableModel, { mergeTablesIntoModel } from 'app/core/table_model';
import { ExploreState, ExploreUrlState, HistoryItem, QueryTransaction } from 'app/types/explore';
import { DataQuery, DataSourceApi } from 'app/types/series';
import { RawTimeRange, IntervalValues } from '@grafana/ui';
import {
ExploreUrlState,
HistoryItem,
QueryTransaction,
ResultType,
QueryIntervals,
QueryOptions,
} from 'app/types/explore';
import { DataQuery } from 'app/types/series';
export const DEFAULT_RANGE = {
from: 'now-6h',
@ -19,6 +26,8 @@ export const DEFAULT_RANGE = {
const MAX_HISTORY_ITEMS = 100;
export const LAST_USED_DATASOURCE_KEY = 'grafana.explore.datasource';
/**
* Returns an Explore-URL that contains a panel's queries and the dashboard time range.
*
@ -77,7 +86,63 @@ export async function getExploreUrl(
return url;
}
const clearQueryKeys: ((query: DataQuery) => object) = ({ key, refId, ...rest }) => rest;
export function buildQueryTransaction(
query: DataQuery,
rowIndex: number,
resultType: ResultType,
queryOptions: QueryOptions,
range: RawTimeRange,
queryIntervals: QueryIntervals,
scanning: boolean
): QueryTransaction {
const { interval, intervalMs } = queryIntervals;
const configuredQueries = [
{
...query,
...queryOptions,
},
];
// Clone range for query request
// const queryRange: RawTimeRange = { ...range };
// const { from, to, raw } = this.timeSrv.timeRange();
// Most datasource is using `panelId + query.refId` for cancellation logic.
// Using `format` here because it relates to the view panel that the request is for.
// However, some datasources don't use `panelId + query.refId`, but only `panelId`.
// Therefore panel id has to be unique.
const panelId = `${queryOptions.format}-${query.key}`;
const options = {
interval,
intervalMs,
panelId,
targets: configuredQueries, // Datasources rely on DataQueries being passed under the targets key.
range: {
from: dateMath.parse(range.from, false),
to: dateMath.parse(range.to, true),
raw: range,
},
rangeRaw: range,
scopedVars: {
__interval: { text: interval, value: interval },
__interval_ms: { text: intervalMs, value: intervalMs },
},
};
return {
options,
query,
resultType,
rowIndex,
scanning,
id: generateKey(), // reusing for unique ID
done: false,
latency: 0,
};
}
export const clearQueryKeys: ((query: DataQuery) => object) = ({ key, refId, ...rest }) => rest;
export function parseUrlState(initial: string | undefined): ExploreUrlState {
if (initial) {
@ -103,12 +168,7 @@ export function parseUrlState(initial: string | undefined): ExploreUrlState {
return { datasource: null, queries: [], range: DEFAULT_RANGE };
}
export function serializeStateToUrlParam(state: ExploreState, compact?: boolean): string {
const urlState: ExploreUrlState = {
datasource: state.initialDatasource,
queries: state.initialQueries.map(clearQueryKeys),
range: state.range,
};
export function serializeStateToUrlParam(urlState: ExploreUrlState, compact?: boolean): string {
if (compact) {
return JSON.stringify([urlState.range.from, urlState.range.to, urlState.datasource, ...urlState.queries]);
}
@ -123,7 +183,7 @@ export function generateRefId(index = 0): string {
return `${index + 1}`;
}
export function generateQueryKeys(index = 0): { refId: string; key: string } {
export function generateEmptyQuery(index = 0): { refId: string; key: string } {
return { refId: generateRefId(index), key: generateKey(index) };
}
@ -132,20 +192,23 @@ export function generateQueryKeys(index = 0): { refId: string; key: string } {
*/
export function ensureQueries(queries?: DataQuery[]): DataQuery[] {
if (queries && typeof queries === 'object' && queries.length > 0) {
return queries.map((query, i) => ({ ...query, ...generateQueryKeys(i) }));
return queries.map((query, i) => ({ ...query, ...generateEmptyQuery(i) }));
}
return [{ ...generateQueryKeys() }];
return [{ ...generateEmptyQuery() }];
}
/**
* 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
)
);
}
@ -180,8 +243,8 @@ export function calculateResultsFromQueryTransactions(
};
}
export function getIntervals(range: RawTimeRange, datasource: DataSourceApi, resolution: number): IntervalValues {
if (!datasource || !resolution) {
export function getIntervals(range: RawTimeRange, lowLimit: string, resolution: number): IntervalValues {
if (!resolution) {
return { interval: '1s', intervalMs: 1000 };
}
@ -190,7 +253,7 @@ export function getIntervals(range: RawTimeRange, datasource: DataSourceApi, res
to: parseDate(range.to, true),
};
return kbn.calculateInterval(absoluteRange, resolution, datasource.interval);
return kbn.calculateInterval(absoluteRange, resolution, lowLimit);
}
export function makeTimeSeriesList(dataList) {

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,61 @@
import React, { PureComponent } from 'react';
import { hot } from 'react-hot-loader';
import { connect } from 'react-redux';
import { RawTimeRange, TimeRange } from '@grafana/ui';
import { ExploreId, ExploreItemState } from 'app/types/explore';
import { StoreState } from 'app/types';
import { toggleGraph } from './state/actions';
import Graph from './Graph';
import Panel from './Panel';
interface GraphContainerProps {
onChangeTime: (range: TimeRange) => void;
exploreId: ExploreId;
graphResult?: any[];
loading: boolean;
range: RawTimeRange;
showingGraph: boolean;
showingTable: boolean;
split: boolean;
toggleGraph: typeof toggleGraph;
}
export class GraphContainer extends PureComponent<GraphContainerProps> {
onClickGraphButton = () => {
this.props.toggleGraph(this.props.exploreId);
};
render() {
const { exploreId, graphResult, loading, onChangeTime, showingGraph, showingTable, range, split } = this.props;
const graphHeight = showingGraph && showingTable ? '200px' : '400px';
return (
<Panel label="Graph" isOpen={showingGraph} loading={loading} onToggle={this.onClickGraphButton}>
<Graph
data={graphResult}
height={graphHeight}
id={`explore-graph-${exploreId}`}
onChangeTime={onChangeTime}
range={range}
split={split}
/>
</Panel>
);
}
}
function mapStateToProps(state: StoreState, { exploreId }) {
const explore = state.explore;
const { split } = explore;
const item: ExploreItemState = explore[exploreId];
const { graphResult, queryTransactions, range, showingGraph, showingTable } = item;
const loading = queryTransactions.some(qt => qt.resultType === 'Graph' && !qt.done);
return { graphResult, loading, range, showingGraph, showingTable, split };
}
const mapDispatchToProps = {
toggleGraph,
};
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(GraphContainer));

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

@ -0,0 +1,91 @@
import React, { PureComponent } from 'react';
import { hot } from 'react-hot-loader';
import { connect } from 'react-redux';
import { RawTimeRange, TimeRange } from '@grafana/ui';
import { ExploreId, ExploreItemState } from 'app/types/explore';
import { LogsModel } from 'app/core/logs_model';
import { StoreState } from 'app/types';
import { toggleLogs } from './state/actions';
import Logs from './Logs';
import Panel from './Panel';
interface LogsContainerProps {
exploreId: ExploreId;
loading: boolean;
logsHighlighterExpressions?: string[];
logsResult?: LogsModel;
onChangeTime: (range: TimeRange) => void;
onClickLabel: (key: string, value: string) => void;
onStartScanning: () => void;
onStopScanning: () => void;
range: RawTimeRange;
scanning?: boolean;
scanRange?: RawTimeRange;
showingLogs: boolean;
toggleLogs: typeof toggleLogs;
}
export class LogsContainer extends PureComponent<LogsContainerProps> {
onClickLogsButton = () => {
this.props.toggleLogs(this.props.exploreId);
};
render() {
const {
exploreId,
loading,
logsHighlighterExpressions,
logsResult,
onChangeTime,
onClickLabel,
onStartScanning,
onStopScanning,
range,
showingLogs,
scanning,
scanRange,
} = this.props;
return (
<Panel label="Logs" loading={loading} isOpen={showingLogs} onToggle={this.onClickLogsButton}>
<Logs
data={logsResult}
exploreId={exploreId}
key={logsResult.id}
highlighterExpressions={logsHighlighterExpressions}
loading={loading}
onChangeTime={onChangeTime}
onClickLabel={onClickLabel}
onStartScanning={onStartScanning}
onStopScanning={onStopScanning}
range={range}
scanning={scanning}
scanRange={scanRange}
/>
</Panel>
);
}
}
function mapStateToProps(state: StoreState, { exploreId }) {
const explore = state.explore;
const item: ExploreItemState = explore[exploreId];
const { logsHighlighterExpressions, logsResult, queryTransactions, scanning, scanRange, showingLogs, range } = item;
const loading = queryTransactions.some(qt => qt.resultType === 'Logs' && !qt.done);
return {
loading,
logsHighlighterExpressions,
logsResult,
scanning,
scanRange,
showingLogs,
range,
};
}
const mapDispatchToProps = {
toggleLogs,
};
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(LogsContainer));

View File

@ -48,7 +48,7 @@ export default class QueryEditor extends PureComponent<QueryEditorProps, any> {
getNextQueryLetter: x => '',
},
hideEditorRowActions: true,
...getIntervals(range, datasource, null), // Possible to get resolution?
...getIntervals(range, (datasource || {}).interval, null), // Possible to get resolution?
},
};

View File

@ -0,0 +1,163 @@
import React, { PureComponent } from 'react';
import { hot } from 'react-hot-loader';
import { connect } from 'react-redux';
import { RawTimeRange } from '@grafana/ui';
import _ from 'lodash';
import { QueryTransaction, HistoryItem, QueryHint, ExploreItemState, ExploreId } from 'app/types/explore';
import { Emitter } from 'app/core/utils/emitter';
import { DataQuery, StoreState } from 'app/types';
// import DefaultQueryField from './QueryField';
import QueryEditor from './QueryEditor';
import QueryTransactionStatus from './QueryTransactionStatus';
import {
addQueryRow,
changeQuery,
highlightLogsExpression,
modifyQueries,
removeQueryRow,
runQueries,
} from './state/actions';
function getFirstHintFromTransactions(transactions: QueryTransaction[]): QueryHint {
const transaction = transactions.find(qt => qt.hints && qt.hints.length > 0);
if (transaction) {
return transaction.hints[0];
}
return undefined;
}
interface QueryRowProps {
addQueryRow: typeof addQueryRow;
changeQuery: typeof changeQuery;
className?: string;
exploreId: ExploreId;
datasourceInstance: any;
highlightLogsExpression: typeof highlightLogsExpression;
history: HistoryItem[];
index: number;
initialQuery: DataQuery;
modifyQueries: typeof modifyQueries;
queryTransactions: QueryTransaction[];
exploreEvents: Emitter;
range: RawTimeRange;
removeQueryRow: typeof removeQueryRow;
runQueries: typeof runQueries;
}
export class QueryRow extends PureComponent<QueryRowProps> {
onExecuteQuery = () => {
const { exploreId } = this.props;
this.props.runQueries(exploreId);
};
onChangeQuery = (query: DataQuery, override?: boolean) => {
const { datasourceInstance, exploreId, index } = this.props;
this.props.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);
}
};
onClickAddButton = () => {
const { exploreId, index } = this.props;
this.props.addQueryRow(exploreId, index);
};
onClickClearButton = () => {
this.onChangeQuery(null, true);
};
onClickHintFix = action => {
const { datasourceInstance, exploreId, index } = this.props;
if (datasourceInstance && datasourceInstance.modifyQuery) {
const modifier = (queries: DataQuery, action: any) => datasourceInstance.modifyQuery(queries, action);
this.props.modifyQueries(exploreId, action, index, modifier);
}
};
onClickRemoveButton = () => {
const { exploreId, index } = this.props;
this.props.removeQueryRow(exploreId, index);
};
updateLogsHighlights = _.debounce((value: DataQuery) => {
const { datasourceInstance } = this.props;
if (datasourceInstance.getHighlighterExpression) {
const expressions = [datasourceInstance.getHighlighterExpression(value)];
this.props.highlightLogsExpression(this.props.exploreId, expressions);
}
}, 500);
render() {
const { datasourceInstance, history, index, initialQuery, queryTransactions, exploreEvents, range } = this.props;
const transactions = queryTransactions.filter(t => t.rowIndex === index);
const transactionWithError = transactions.find(t => t.error !== undefined);
const hint = getFirstHintFromTransactions(transactions);
const queryError = transactionWithError ? transactionWithError.error : null;
const QueryField = datasourceInstance.pluginExports.ExploreQueryField;
return (
<div className="query-row">
<div className="query-row-status">
<QueryTransactionStatus transactions={transactions} />
</div>
<div className="query-row-field">
{QueryField ? (
<QueryField
datasource={datasourceInstance}
error={queryError}
hint={hint}
initialQuery={initialQuery}
history={history}
onClickHintFix={this.onClickHintFix}
onPressEnter={this.onExecuteQuery}
onQueryChange={this.onChangeQuery}
/>
) : (
<QueryEditor
datasource={datasourceInstance}
error={queryError}
onQueryChange={this.onChangeQuery}
onExecuteQuery={this.onExecuteQuery}
initialQuery={initialQuery}
exploreEvents={exploreEvents}
range={range}
/>
)}
</div>
<div className="query-row-tools">
<button className="btn navbar-button navbar-button--tight" onClick={this.onClickClearButton}>
<i className="fa fa-times" />
</button>
<button className="btn navbar-button navbar-button--tight" onClick={this.onClickAddButton}>
<i className="fa fa-plus" />
</button>
<button className="btn navbar-button navbar-button--tight" onClick={this.onClickRemoveButton}>
<i className="fa fa-minus" />
</button>
</div>
</div>
);
}
}
function mapStateToProps(state: StoreState, { exploreId, index }) {
const explore = state.explore;
const item: ExploreItemState = explore[exploreId];
const { datasourceInstance, history, initialQueries, queryTransactions, range } = item;
const initialQuery = initialQueries[index];
return { datasourceInstance, history, initialQuery, queryTransactions, range };
}
const mapDispatchToProps = {
addQueryRow,
changeQuery,
highlightLogsExpression,
modifyQueries,
removeQueryRow,
runQueries,
};
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(QueryRow));

View File

@ -1,159 +1,25 @@
import React, { PureComponent } from 'react';
import { QueryTransaction, HistoryItem, QueryHint } from 'app/types/explore';
import { Emitter } from 'app/core/utils/emitter';
import { DataQuery } from 'app/types';
import { ExploreId } from 'app/types/explore';
// import DefaultQueryField from './QueryField';
import QueryEditor from './QueryEditor';
import QueryTransactionStatus from './QueryTransactionStatus';
import { DataSource, DataQuery } from 'app/types';
import { RawTimeRange } from '@grafana/ui';
import QueryRow from './QueryRow';
function getFirstHintFromTransactions(transactions: QueryTransaction[]): QueryHint {
const transaction = transactions.find(qt => qt.hints && qt.hints.length > 0);
if (transaction) {
return transaction.hints[0];
}
return undefined;
}
interface QueryRowEventHandlers {
onAddQueryRow: (index: number) => void;
onChangeQuery: (value: DataQuery, index: number, override?: boolean) => void;
onClickHintFix: (action: object, index?: number) => void;
onExecuteQuery: () => void;
onRemoveQueryRow: (index: number) => void;
}
interface QueryRowCommonProps {
interface QueryRowsProps {
className?: string;
datasource: DataSource;
history: HistoryItem[];
transactions: QueryTransaction[];
exploreEvents: Emitter;
range: RawTimeRange;
exploreId: ExploreId;
initialQueries: DataQuery[];
}
type QueryRowProps = QueryRowCommonProps &
QueryRowEventHandlers & {
index: number;
initialQuery: DataQuery;
};
class QueryRow extends PureComponent<QueryRowProps> {
onExecuteQuery = () => {
const { onExecuteQuery } = this.props;
onExecuteQuery();
};
onChangeQuery = (value: DataQuery, override?: boolean) => {
const { index, onChangeQuery } = this.props;
if (onChangeQuery) {
onChangeQuery(value, index, override);
}
};
onClickAddButton = () => {
const { index, onAddQueryRow } = this.props;
if (onAddQueryRow) {
onAddQueryRow(index);
}
};
onClickClearButton = () => {
this.onChangeQuery(null, true);
};
onClickHintFix = action => {
const { index, onClickHintFix } = this.props;
if (onClickHintFix) {
onClickHintFix(action, index);
}
};
onClickRemoveButton = () => {
const { index, onRemoveQueryRow } = this.props;
if (onRemoveQueryRow) {
onRemoveQueryRow(index);
}
};
onPressEnter = () => {
const { onExecuteQuery } = this.props;
if (onExecuteQuery) {
onExecuteQuery();
}
};
render() {
const { datasource, history, initialQuery, transactions, exploreEvents, range } = this.props;
const transactionWithError = transactions.find(t => t.error !== undefined);
const hint = getFirstHintFromTransactions(transactions);
const queryError = transactionWithError ? transactionWithError.error : null;
const QueryField = datasource.pluginExports.ExploreQueryField;
return (
<div className="query-row">
<div className="query-row-status">
<QueryTransactionStatus transactions={transactions} />
</div>
<div className="query-row-field">
{QueryField ? (
<QueryField
datasource={datasource}
error={queryError}
hint={hint}
initialQuery={initialQuery}
history={history}
onClickHintFix={this.onClickHintFix}
onPressEnter={this.onPressEnter}
onQueryChange={this.onChangeQuery}
/>
) : (
<QueryEditor
datasource={datasource}
error={queryError}
onQueryChange={this.onChangeQuery}
onExecuteQuery={this.onExecuteQuery}
initialQuery={initialQuery}
exploreEvents={exploreEvents}
range={range}
/>
)}
</div>
<div className="query-row-tools">
<button className="btn navbar-button navbar-button--tight" onClick={this.onClickClearButton}>
<i className="fa fa-times" />
</button>
<button className="btn navbar-button navbar-button--tight" onClick={this.onClickAddButton}>
<i className="fa fa-plus" />
</button>
<button className="btn navbar-button navbar-button--tight" onClick={this.onClickRemoveButton}>
<i className="fa fa-minus" />
</button>
</div>
</div>
);
}
}
type QueryRowsProps = QueryRowCommonProps &
QueryRowEventHandlers & {
initialQueries: DataQuery[];
};
export default class QueryRows extends PureComponent<QueryRowsProps> {
render() {
const { className = '', initialQueries, transactions, ...handlers } = this.props;
const { className = '', exploreEvents, exploreId, initialQueries } = this.props;
return (
<div className={className}>
{initialQueries.map((query, index) => (
<QueryRow
key={query.key}
index={index}
initialQuery={query}
transactions={transactions.filter(t => t.rowIndex === index)}
{...handlers}
/>
// TODO instead of relying on initialQueries, move to react key list in redux
<QueryRow key={query.key} exploreEvents={exploreEvents} exploreId={exploreId} index={index} />
))}
</div>
);

View File

@ -0,0 +1,49 @@
import React, { PureComponent } from 'react';
import { hot } from 'react-hot-loader';
import { connect } from 'react-redux';
import { ExploreId, ExploreItemState } from 'app/types/explore';
import { StoreState } from 'app/types';
import { toggleGraph } from './state/actions';
import Table from './Table';
import Panel from './Panel';
import TableModel from 'app/core/table_model';
interface TableContainerProps {
exploreId: ExploreId;
loading: boolean;
onClickCell: (key: string, value: string) => void;
showingTable: boolean;
tableResult?: TableModel;
toggleGraph: typeof toggleGraph;
}
export class TableContainer extends PureComponent<TableContainerProps> {
onClickTableButton = () => {
this.props.toggleGraph(this.props.exploreId);
};
render() {
const { loading, onClickCell, showingTable, tableResult } = this.props;
return (
<Panel label="Table" loading={loading} isOpen={showingTable} onToggle={this.onClickTableButton}>
<Table data={tableResult} loading={loading} onClickCell={onClickCell} />
</Panel>
);
}
}
function mapStateToProps(state: StoreState, { exploreId }) {
const explore = state.explore;
const item: ExploreItemState = explore[exploreId];
const { queryTransactions, showingTable, tableResult } = item;
const loading = queryTransactions.some(qt => qt.resultType === 'Table' && !qt.done);
return { loading, showingTable, tableResult };
}
const mapDispatchToProps = {
toggleGraph,
};
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(TableContainer));

View File

@ -3,91 +3,56 @@ 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 { StoreState } from 'app/types';
import { ExploreState } from 'app/types/explore';
import { ExploreId, ExploreUrlState } from 'app/types/explore';
import { parseUrlState } from 'app/core/utils/explore';
import { initializeExploreSplit } from './state/actions';
import ErrorBoundary from './ErrorBoundary';
import Explore from './Explore';
interface WrapperProps {
backendSrv?: any;
datasourceSrv?: any;
initializeExploreSplit: typeof initializeExploreSplit;
split: boolean;
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 Component<WrapperProps, WrapperState> {
urlStates: { [key: string]: string };
export class Wrapper extends Component<WrapperProps> {
initialSplit: boolean;
urlStates: { [key: string]: ExploreUrlState };
constructor(props: WrapperProps) {
super(props);
this.urlStates = props.urlStates;
this.state = {
split: Boolean(props.urlStates[STATE_KEY_RIGHT]),
splitState: undefined,
};
this.urlStates = {};
const { left, right } = props.urlStates;
if (props.urlStates.left) {
this.urlStates.leftState = parseUrlState(left);
}
if (props.urlStates.right) {
this.urlStates.rightState = parseUrlState(right);
this.initialSplit = true;
}
}
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,
});
componentDidMount() {
if (this.initialSplit) {
this.props.initializeExploreSplit();
}
};
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;
// 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 { split } = this.props;
const { leftState, rightState } = this.urlStates;
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} urlState={leftState} />
</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} urlState={rightState} />
</ErrorBoundary>
)}
</div>
@ -95,11 +60,14 @@ export class Wrapper extends Component<WrapperProps, WrapperState> {
}
}
const mapStateToProps = (state: StoreState) => ({
urlStates: state.location.query,
});
const mapStateToProps = (state: StoreState) => {
const urlStates = state.location.query;
const { split } = state.explore;
return { split, urlStates };
};
const mapDispatchToProps = {
initializeExploreSplit,
updateLocation,
};

View File

@ -0,0 +1,302 @@
import { RawTimeRange, TimeRange } from '@grafana/ui';
import { Emitter } from 'app/core/core';
import {
ExploreId,
ExploreItemState,
HistoryItem,
RangeScanner,
ResultType,
QueryTransaction,
} from 'app/types/explore';
import { DataSourceSelectItem } from 'app/types/datasources';
import { DataQuery } from 'app/types';
export enum ActionTypes {
AddQueryRow = 'explore/ADD_QUERY_ROW',
ChangeDatasource = 'explore/CHANGE_DATASOURCE',
ChangeQuery = 'explore/CHANGE_QUERY',
ChangeSize = 'explore/CHANGE_SIZE',
ChangeTime = 'explore/CHANGE_TIME',
ClearQueries = 'explore/CLEAR_QUERIES',
HighlightLogsExpression = 'explore/HIGHLIGHT_LOGS_EXPRESSION',
InitializeExplore = 'explore/INITIALIZE_EXPLORE',
InitializeExploreSplit = 'explore/INITIALIZE_EXPLORE_SPLIT',
LoadDatasourceFailure = 'explore/LOAD_DATASOURCE_FAILURE',
LoadDatasourceMissing = 'explore/LOAD_DATASOURCE_MISSING',
LoadDatasourcePending = 'explore/LOAD_DATASOURCE_PENDING',
LoadDatasourceSuccess = 'explore/LOAD_DATASOURCE_SUCCESS',
ModifyQueries = 'explore/MODIFY_QUERIES',
QueryTransactionFailure = 'explore/QUERY_TRANSACTION_FAILURE',
QueryTransactionStart = 'explore/QUERY_TRANSACTION_START',
QueryTransactionSuccess = 'explore/QUERY_TRANSACTION_SUCCESS',
RemoveQueryRow = 'explore/REMOVE_QUERY_ROW',
RunQueries = 'explore/RUN_QUERIES',
RunQueriesEmpty = 'explore/RUN_QUERIES_EMPTY',
ScanRange = 'explore/SCAN_RANGE',
ScanStart = 'explore/SCAN_START',
ScanStop = 'explore/SCAN_STOP',
SetQueries = 'explore/SET_QUERIES',
SplitClose = 'explore/SPLIT_CLOSE',
SplitOpen = 'explore/SPLIT_OPEN',
StateSave = 'explore/STATE_SAVE',
ToggleGraph = 'explore/TOGGLE_GRAPH',
ToggleLogs = 'explore/TOGGLE_LOGS',
ToggleTable = 'explore/TOGGLE_TABLE',
}
export interface AddQueryRowAction {
type: ActionTypes.AddQueryRow;
payload: {
exploreId: ExploreId;
index: number;
query: DataQuery;
};
}
export interface ChangeQueryAction {
type: ActionTypes.ChangeQuery;
payload: {
exploreId: ExploreId;
query: DataQuery;
index: number;
override: boolean;
};
}
export interface ChangeSizeAction {
type: ActionTypes.ChangeSize;
payload: {
exploreId: ExploreId;
width: number;
height: number;
};
}
export interface ChangeTimeAction {
type: ActionTypes.ChangeTime;
payload: {
exploreId: ExploreId;
range: TimeRange;
};
}
export interface ClearQueriesAction {
type: ActionTypes.ClearQueries;
payload: {
exploreId: ExploreId;
};
}
export interface HighlightLogsExpressionAction {
type: ActionTypes.HighlightLogsExpression;
payload: {
exploreId: ExploreId;
expressions: string[];
};
}
export interface InitializeExploreAction {
type: ActionTypes.InitializeExplore;
payload: {
exploreId: ExploreId;
containerWidth: number;
datasource: string;
eventBridge: Emitter;
exploreDatasources: DataSourceSelectItem[];
queries: DataQuery[];
range: RawTimeRange;
};
}
export interface InitializeExploreSplitAction {
type: ActionTypes.InitializeExploreSplit;
}
export interface LoadDatasourceFailureAction {
type: ActionTypes.LoadDatasourceFailure;
payload: {
exploreId: ExploreId;
error: string;
};
}
export interface LoadDatasourcePendingAction {
type: ActionTypes.LoadDatasourcePending;
payload: {
exploreId: ExploreId;
datasourceId: number;
};
}
export interface LoadDatasourceMissingAction {
type: ActionTypes.LoadDatasourceMissing;
payload: {
exploreId: ExploreId;
};
}
export interface LoadDatasourceSuccessAction {
type: ActionTypes.LoadDatasourceSuccess;
payload: {
exploreId: ExploreId;
StartPage?: any;
datasourceInstance: any;
history: HistoryItem[];
initialDatasource: string;
initialQueries: DataQuery[];
logsHighlighterExpressions?: any[];
showingStartPage: boolean;
supportsGraph: boolean;
supportsLogs: boolean;
supportsTable: boolean;
};
}
export interface ModifyQueriesAction {
type: ActionTypes.ModifyQueries;
payload: {
exploreId: ExploreId;
modification: any;
index: number;
modifier: (queries: DataQuery[], modification: any) => DataQuery[];
};
}
export interface QueryTransactionFailureAction {
type: ActionTypes.QueryTransactionFailure;
payload: {
exploreId: ExploreId;
queryTransactions: QueryTransaction[];
};
}
export interface QueryTransactionStartAction {
type: ActionTypes.QueryTransactionStart;
payload: {
exploreId: ExploreId;
resultType: ResultType;
rowIndex: number;
transaction: QueryTransaction;
};
}
export interface QueryTransactionSuccessAction {
type: ActionTypes.QueryTransactionSuccess;
payload: {
exploreId: ExploreId;
history: HistoryItem[];
queryTransactions: QueryTransaction[];
};
}
export interface RemoveQueryRowAction {
type: ActionTypes.RemoveQueryRow;
payload: {
exploreId: ExploreId;
index: number;
};
}
export interface RunQueriesEmptyAction {
type: ActionTypes.RunQueriesEmpty;
payload: {
exploreId: ExploreId;
};
}
export interface ScanStartAction {
type: ActionTypes.ScanStart;
payload: {
exploreId: ExploreId;
scanner: RangeScanner;
};
}
export interface ScanRangeAction {
type: ActionTypes.ScanRange;
payload: {
exploreId: ExploreId;
range: RawTimeRange;
};
}
export interface ScanStopAction {
type: ActionTypes.ScanStop;
payload: {
exploreId: ExploreId;
};
}
export interface SetQueriesAction {
type: ActionTypes.SetQueries;
payload: {
exploreId: ExploreId;
queries: DataQuery[];
};
}
export interface SplitCloseAction {
type: ActionTypes.SplitClose;
}
export interface SplitOpenAction {
type: ActionTypes.SplitOpen;
payload: {
itemState: ExploreItemState;
};
}
export interface StateSaveAction {
type: ActionTypes.StateSave;
}
export interface ToggleTableAction {
type: ActionTypes.ToggleTable;
payload: {
exploreId: ExploreId;
};
}
export interface ToggleGraphAction {
type: ActionTypes.ToggleGraph;
payload: {
exploreId: ExploreId;
};
}
export interface ToggleLogsAction {
type: ActionTypes.ToggleLogs;
payload: {
exploreId: ExploreId;
};
}
export type Action =
| AddQueryRowAction
| ChangeQueryAction
| ChangeSizeAction
| ChangeTimeAction
| ClearQueriesAction
| HighlightLogsExpressionAction
| InitializeExploreAction
| InitializeExploreSplitAction
| LoadDatasourceFailureAction
| LoadDatasourceMissingAction
| LoadDatasourcePendingAction
| LoadDatasourceSuccessAction
| ModifyQueriesAction
| QueryTransactionFailureAction
| QueryTransactionStartAction
| QueryTransactionSuccessAction
| RemoveQueryRowAction
| RunQueriesEmptyAction
| ScanRangeAction
| ScanStartAction
| ScanStopAction
| SetQueriesAction
| SplitCloseAction
| SplitOpenAction
| ToggleGraphAction
| ToggleLogsAction
| ToggleTableAction;

View File

@ -0,0 +1,757 @@
import _ from 'lodash';
import { ThunkAction } from 'redux-thunk';
import { RawTimeRange, TimeRange } from '@grafana/ui';
import {
LAST_USED_DATASOURCE_KEY,
clearQueryKeys,
ensureQueries,
generateEmptyQuery,
hasNonEmptyQuery,
makeTimeSeriesList,
updateHistory,
buildQueryTransaction,
serializeStateToUrlParam,
} from 'app/core/utils/explore';
import { updateLocation } from 'app/core/actions';
import store from 'app/core/store';
import { DataSourceSelectItem } from 'app/types/datasources';
import { DataQuery, StoreState } from 'app/types';
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
import {
ExploreId,
ExploreUrlState,
RangeScanner,
ResultType,
QueryOptions,
QueryTransaction,
QueryHint,
QueryHintGetter,
} from 'app/types/explore';
import { Emitter } from 'app/core/core';
import {
Action as ThunkableAction,
ActionTypes,
AddQueryRowAction,
ChangeSizeAction,
HighlightLogsExpressionAction,
LoadDatasourceFailureAction,
LoadDatasourceMissingAction,
LoadDatasourcePendingAction,
LoadDatasourceSuccessAction,
QueryTransactionStartAction,
ScanStopAction,
} from './actionTypes';
type ThunkResult<R> = ThunkAction<R, StoreState, undefined, ThunkableAction>;
/**
* Adds a query row after the row with the given index.
*/
export function addQueryRow(exploreId: ExploreId, index: number): AddQueryRowAction {
const query = generateEmptyQuery(index + 1);
return { type: ActionTypes.AddQueryRow, payload: { exploreId, index, query } };
}
/**
* Loads a new datasource identified by the given name.
*/
export function changeDatasource(exploreId: ExploreId, datasource: string): ThunkResult<void> {
return async dispatch => {
const instance = await getDatasourceSrv().get(datasource);
dispatch(loadDatasource(exploreId, instance));
};
}
/**
* Query change handler for the query row with the given index.
* If `override` is reset the query modifications and run the queries. Use this to set queries via a link.
*/
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, payload: { exploreId, query, index, override } });
if (override) {
dispatch(runQueries(exploreId));
}
};
}
/**
* Keep track of the Explore container size, in particular the width.
* The width will be used to calculate graph intervals (number of datapoints).
*/
export function changeSize(
exploreId: ExploreId,
{ height, width }: { height: number; width: number }
): ChangeSizeAction {
return { type: ActionTypes.ChangeSize, payload: { exploreId, height, width } };
}
/**
* Change the time range of Explore. Usually called from the Timepicker or a graph interaction.
*/
export function changeTime(exploreId: ExploreId, range: TimeRange): ThunkResult<void> {
return dispatch => {
dispatch({ type: ActionTypes.ChangeTime, payload: { exploreId, range } });
dispatch(runQueries(exploreId));
};
}
/**
* Clear all queries and results.
*/
export function clearQueries(exploreId: ExploreId): ThunkResult<void> {
return dispatch => {
dispatch(scanStop(exploreId));
dispatch({ type: ActionTypes.ClearQueries, payload: { exploreId } });
dispatch(stateSave());
};
}
/**
* Highlight expressions in the log results
*/
export function highlightLogsExpression(exploreId: ExploreId, expressions: string[]): HighlightLogsExpressionAction {
return { type: ActionTypes.HighlightLogsExpression, payload: { exploreId, expressions } };
}
/**
* Initialize Explore state with state from the URL and the React component.
* Call this only on components for with the Explore state has not been initialized.
*/
export function initializeExplore(
exploreId: ExploreId,
datasource: string,
queries: DataQuery[],
range: RawTimeRange,
containerWidth: number,
eventBridge: Emitter
): ThunkResult<void> {
return async dispatch => {
const exploreDatasources: DataSourceSelectItem[] = getDatasourceSrv()
.getExternal()
.map(ds => ({
value: ds.name,
name: ds.name,
meta: ds.meta,
}));
dispatch({
type: ActionTypes.InitializeExplore,
payload: {
exploreId,
containerWidth,
datasource,
eventBridge,
exploreDatasources,
queries,
range,
},
});
if (exploreDatasources.length > 1) {
let instance;
if (datasource) {
instance = await getDatasourceSrv().get(datasource);
} else {
instance = await getDatasourceSrv().get();
}
dispatch(loadDatasource(exploreId, instance));
} else {
dispatch(loadDatasourceMissing(exploreId));
}
};
}
/**
* Initialize the wrapper split state
*/
export function initializeExploreSplit() {
return async dispatch => {
dispatch({ type: ActionTypes.InitializeExploreSplit });
};
}
/**
* Display an error that happened during the selection of a datasource
*/
export const loadDatasourceFailure = (exploreId: ExploreId, error: string): LoadDatasourceFailureAction => ({
type: ActionTypes.LoadDatasourceFailure,
payload: {
exploreId,
error,
},
});
/**
* Display an error when no datasources have been configured
*/
export const loadDatasourceMissing = (exploreId: ExploreId): LoadDatasourceMissingAction => ({
type: ActionTypes.LoadDatasourceMissing,
payload: { exploreId },
});
/**
* Start the async process of loading a datasource to display a loading indicator
*/
export const loadDatasourcePending = (exploreId: ExploreId, datasourceId: number): LoadDatasourcePendingAction => ({
type: ActionTypes.LoadDatasourcePending,
payload: {
exploreId,
datasourceId,
},
});
/**
* Datasource loading was successfully completed. The instance is stored in the state as well in case we need to
* run datasource-specific code. Existing queries are imported to the new datasource if an importer exists,
* e.g., Prometheus -> Loki queries.
*/
export const loadDatasourceSuccess = (
exploreId: ExploreId,
instance: any,
queries: DataQuery[]
): LoadDatasourceSuccessAction => {
// Capabilities
const supportsGraph = instance.meta.metrics;
const supportsLogs = instance.meta.logs;
const supportsTable = instance.meta.tables;
// Custom components
const StartPage = instance.pluginExports.ExploreStartPage;
const historyKey = `grafana.explore.history.${instance.meta.id}`;
const history = store.getObject(historyKey, []);
// Save last-used datasource
store.set(LAST_USED_DATASOURCE_KEY, instance.name);
return {
type: ActionTypes.LoadDatasourceSuccess,
payload: {
exploreId,
StartPage,
datasourceInstance: instance,
history,
initialDatasource: instance.name,
initialQueries: queries,
showingStartPage: Boolean(StartPage),
supportsGraph,
supportsLogs,
supportsTable,
},
};
};
/**
* Main action to asynchronously load a datasource. Dispatches lots of smaller actions for feedback.
*/
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(exploreId, datasourceId));
let datasourceError = null;
try {
const testResult = await instance.testDatasource();
datasourceError = testResult.status === 'success' ? null : testResult.message;
} catch (error) {
datasourceError = (error && error.statusText) || 'Network error';
}
if (datasourceError) {
dispatch(loadDatasourceFailure(exploreId, datasourceError));
return;
}
if (datasourceId !== getState().explore[exploreId].requestedDatasourceId) {
// User already changed datasource again, discard results
return;
}
if (instance.init) {
instance.init();
}
// Check if queries can be imported from previously selected datasource
const queries = getState().explore[exploreId].modifiedQueries;
let importedQueries = queries;
const origin = getState().explore[exploreId].datasourceInstance;
if (origin) {
if (origin.meta.id === instance.meta.id) {
// Keep same queries if same type of datasource
importedQueries = [...queries];
} else if (instance.importQueries) {
// Datasource-specific importers
importedQueries = await instance.importQueries(queries, origin.meta);
} else {
// Default is blank queries
importedQueries = ensureQueries();
}
}
if (datasourceId !== getState().explore[exploreId].requestedDatasourceId) {
// User already changed datasource again, discard results
return;
}
// Reset edit state with new queries
const nextQueries = importedQueries.map((q, i) => ({
...importedQueries[i],
...generateEmptyQuery(i),
}));
dispatch(loadDatasourceSuccess(exploreId, instance, nextQueries));
dispatch(runQueries(exploreId));
};
}
/**
* Action to modify a query given a datasource-specific modifier action.
* @param exploreId Explore area
* @param modification Action object with a type, e.g., ADD_FILTER
* @param index Optional query row index. If omitted, the modification is applied to all query rows.
* @param modifier Function that executes the modification, typically `datasourceInstance.modifyQueries`.
*/
export function modifyQueries(
exploreId: ExploreId,
modification: any,
index: number,
modifier: any
): ThunkResult<void> {
return dispatch => {
dispatch({ type: ActionTypes.ModifyQueries, payload: { exploreId, modification, index, modifier } });
if (!modification.preventSubmit) {
dispatch(runQueries(exploreId));
}
};
}
/**
* Mark a query transaction as failed with an error extracted from the query response.
* The transaction will be marked as `done`.
*/
export function queryTransactionFailure(
exploreId: ExploreId,
transactionId: string,
response: any,
datasourceId: string
): ThunkResult<void> {
return (dispatch, getState) => {
const { datasourceInstance, queryTransactions } = getState().explore[exploreId];
if (datasourceInstance.meta.id !== datasourceId || response.cancelled) {
// Navigated away, queries did not matter
return;
}
// Transaction might have been discarded
if (!queryTransactions.find(qt => qt.id === transactionId)) {
return;
}
console.error(response);
let error: string;
let errorDetails: string;
if (response.data) {
if (typeof response.data === 'string') {
error = response.data;
} else if (response.data.error) {
error = response.data.error;
if (response.data.response) {
errorDetails = response.data.response;
}
} else {
throw new Error('Could not handle error response');
}
} else if (response.message) {
error = response.message;
} else if (typeof response === 'string') {
error = response;
} else {
error = 'Unknown error during query transaction. Please check JS console logs.';
}
// Mark transactions as complete
const nextQueryTransactions = queryTransactions.map(qt => {
if (qt.id === transactionId) {
return {
...qt,
error,
errorDetails,
done: true,
};
}
return qt;
});
dispatch({
type: ActionTypes.QueryTransactionFailure,
payload: { exploreId, queryTransactions: nextQueryTransactions },
});
};
}
/**
* Start a query transaction for the given result type.
* @param exploreId Explore area
* @param transaction Query options and `done` status.
* @param resultType Associate the transaction with a result viewer, e.g., Graph
* @param rowIndex Index is used to associate latency for this transaction with a query row
*/
export function queryTransactionStart(
exploreId: ExploreId,
transaction: QueryTransaction,
resultType: ResultType,
rowIndex: number
): QueryTransactionStartAction {
return { type: ActionTypes.QueryTransactionStart, payload: { exploreId, resultType, rowIndex, transaction } };
}
/**
* Complete a query transaction, mark the transaction as `done` and store query state in URL.
* If the transaction was started by a scanner, it keeps on scanning for more results.
* Side-effect: the query is stored in localStorage.
* @param exploreId Explore area
* @param transactionId ID
* @param result Response from `datasourceInstance.query()`
* @param latency Duration between request and response
* @param queries Queries from all query rows
* @param datasourceId Origin datasource instance, used to discard results if current datasource is different
*/
export function queryTransactionSuccess(
exploreId: ExploreId,
transactionId: string,
result: any,
latency: number,
queries: DataQuery[],
datasourceId: string
): ThunkResult<void> {
return (dispatch, getState) => {
const { datasourceInstance, history, queryTransactions, scanner, scanning } = getState().explore[exploreId];
// If datasource already changed, results do not matter
if (datasourceInstance.meta.id !== datasourceId) {
return;
}
// Transaction might have been discarded
const transaction = queryTransactions.find(qt => qt.id === transactionId);
if (!transaction) {
return;
}
// Get query hints
let hints: QueryHint[];
if (datasourceInstance.getQueryHints as QueryHintGetter) {
hints = datasourceInstance.getQueryHints(transaction.query, result);
}
// Mark transactions as complete and attach result
const nextQueryTransactions = queryTransactions.map(qt => {
if (qt.id === transactionId) {
return {
...qt,
hints,
latency,
result,
done: true,
};
}
return qt;
});
// Side-effect: Saving history in localstorage
const nextHistory = updateHistory(history, datasourceId, queries);
dispatch({
type: ActionTypes.QueryTransactionSuccess,
payload: {
exploreId,
history: nextHistory,
queryTransactions: nextQueryTransactions,
},
});
// Keep scanning for results if this was the last scanning transaction
if (scanning) {
if (_.size(result) === 0) {
const other = nextQueryTransactions.find(qt => qt.scanning && !qt.done);
if (!other) {
const range = scanner();
dispatch({ type: ActionTypes.ScanRange, payload: { exploreId, range } });
}
} else {
// We can stop scanning if we have a result
dispatch(scanStop(exploreId));
}
}
};
}
/**
* Remove query row of the given index, as well as associated query results.
*/
export function removeQueryRow(exploreId: ExploreId, index: number): ThunkResult<void> {
return dispatch => {
dispatch({ type: ActionTypes.RemoveQueryRow, payload: { exploreId, index } });
dispatch(runQueries(exploreId));
};
}
/**
* Main action to run queries and dispatches sub-actions based on which result viewers are active
*/
export function runQueries(exploreId: ExploreId) {
return (dispatch, getState) => {
const {
datasourceInstance,
modifiedQueries,
showingLogs,
showingGraph,
showingTable,
supportsGraph,
supportsLogs,
supportsTable,
} = getState().explore[exploreId];
if (!hasNonEmptyQuery(modifiedQueries)) {
dispatch({ type: ActionTypes.RunQueriesEmpty, payload: { exploreId } });
return;
}
// Some datasource's query builders allow per-query interval limits,
// but we're using the datasource interval limit for now
const interval = datasourceInstance.interval;
// Keep table queries first since they need to return quickly
if (showingTable && supportsTable) {
dispatch(
runQueriesForType(
exploreId,
'Table',
{
interval,
format: 'table',
instant: true,
valueWithRefId: true,
},
data => data[0]
)
);
}
if (showingGraph && supportsGraph) {
dispatch(
runQueriesForType(
exploreId,
'Graph',
{
interval,
format: 'time_series',
instant: false,
},
makeTimeSeriesList
)
);
}
if (showingLogs && supportsLogs) {
dispatch(runQueriesForType(exploreId, 'Logs', { interval, format: 'logs' }));
}
dispatch(stateSave());
};
}
/**
* Helper action to build a query transaction object and handing the query to the datasource.
* @param exploreId Explore area
* @param resultType Result viewer that will be associated with this query result
* @param queryOptions Query options as required by the datasource's `query()` function.
* @param resultGetter Optional result extractor, e.g., if the result is a list and you only need the first element.
*/
function runQueriesForType(
exploreId: ExploreId,
resultType: ResultType,
queryOptions: QueryOptions,
resultGetter?: any
) {
return async (dispatch, getState) => {
const {
datasourceInstance,
eventBridge,
modifiedQueries: queries,
queryIntervals,
range,
scanning,
} = getState().explore[exploreId];
const datasourceId = datasourceInstance.meta.id;
// Run all queries concurrently
queries.forEach(async (query, rowIndex) => {
const transaction = buildQueryTransaction(
query,
rowIndex,
resultType,
queryOptions,
range,
queryIntervals,
scanning
);
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(exploreId, transaction.id, results, latency, queries, datasourceId));
} catch (response) {
eventBridge.emit('data-error', response);
dispatch(queryTransactionFailure(exploreId, transaction.id, response, datasourceId));
}
});
};
}
/**
* Start a scan for more results using the given scanner.
* @param exploreId Explore area
* @param scanner Function that a) returns a new time range and b) triggers a query run for the new range
*/
export function scanStart(exploreId: ExploreId, scanner: RangeScanner): ThunkResult<void> {
return dispatch => {
// Register the scanner
dispatch({ type: ActionTypes.ScanStart, payload: { exploreId, scanner } });
// Scanning must trigger query run, and return the new range
const range = scanner();
// Set the new range to be displayed
dispatch({ type: ActionTypes.ScanRange, payload: { exploreId, range } });
};
}
/**
* Stop any scanning for more results.
*/
export function scanStop(exploreId: ExploreId): ScanStopAction {
return { type: ActionTypes.ScanStop, payload: { exploreId } };
}
/**
* Reset queries to the given queries. Any modifications will be discarded.
* Use this action for clicks on query examples. Triggers a query run.
*/
export function setQueries(exploreId: ExploreId, rawQueries: DataQuery[]): ThunkResult<void> {
return dispatch => {
// Inject react keys into query objects
const queries = rawQueries.map(q => ({ ...q, ...generateEmptyQuery() }));
dispatch({
type: ActionTypes.SetQueries,
payload: {
exploreId,
queries,
},
});
dispatch(runQueries(exploreId));
};
}
/**
* Close the split view and save URL state.
*/
export function splitClose(): ThunkResult<void> {
return dispatch => {
dispatch({ type: ActionTypes.SplitClose });
dispatch(stateSave());
};
}
/**
* Open the split view and copy the left state to be the right state.
* The right state is automatically initialized.
* The copy keeps all query modifications but wipes the query results.
*/
export function splitOpen(): ThunkResult<void> {
return (dispatch, getState) => {
// Clone left state to become the right state
const leftState = getState().explore.left;
const itemState = {
...leftState,
queryTransactions: [],
initialQueries: leftState.modifiedQueries.slice(),
};
dispatch({ type: ActionTypes.SplitOpen, payload: { itemState } });
dispatch(stateSave());
};
}
/**
* Saves Explore state to URL using the `left` and `right` parameters.
* If split view is not active, `right` will not be set.
*/
export function stateSave() {
return (dispatch, getState) => {
const { left, right, split } = getState().explore;
const urlStates: { [index: string]: string } = {};
const leftUrlState: ExploreUrlState = {
datasource: left.datasourceInstance.name,
queries: left.modifiedQueries.map(clearQueryKeys),
range: left.range,
};
urlStates.left = serializeStateToUrlParam(leftUrlState, true);
if (split) {
const rightUrlState: ExploreUrlState = {
datasource: right.datasourceInstance.name,
queries: right.modifiedQueries.map(clearQueryKeys),
range: right.range,
};
urlStates.right = serializeStateToUrlParam(rightUrlState, true);
}
dispatch(updateLocation({ query: urlStates }));
};
}
/**
* Expand/collapse the graph result viewer. When collapsed, graph queries won't be run.
*/
export function toggleGraph(exploreId: ExploreId): ThunkResult<void> {
return (dispatch, getState) => {
dispatch({ type: ActionTypes.ToggleGraph, payload: { exploreId } });
if (getState().explore[exploreId].showingGraph) {
dispatch(runQueries(exploreId));
}
};
}
/**
* Expand/collapse the logs result viewer. When collapsed, log queries won't be run.
*/
export function toggleLogs(exploreId: ExploreId): ThunkResult<void> {
return (dispatch, getState) => {
dispatch({ type: ActionTypes.ToggleLogs, payload: { exploreId } });
if (getState().explore[exploreId].showingLogs) {
dispatch(runQueries(exploreId));
}
};
}
/**
* Expand/collapse the table result viewer. When collapsed, table queries won't be run.
*/
export function toggleTable(exploreId: ExploreId): ThunkResult<void> {
return (dispatch, getState) => {
dispatch({ type: ActionTypes.ToggleTable, payload: { exploreId } });
if (getState().explore[exploreId].showingTable) {
dispatch(runQueries(exploreId));
}
};
}

View File

@ -0,0 +1,462 @@
import {
calculateResultsFromQueryTransactions,
generateEmptyQuery,
getIntervals,
ensureQueries,
} from 'app/core/utils/explore';
import { ExploreItemState, ExploreState, QueryTransaction } from 'app/types/explore';
import { DataQuery } from 'app/types/series';
import { Action, ActionTypes } from './actionTypes';
export const DEFAULT_RANGE = {
from: 'now-6h',
to: 'now',
};
// Millies step for helper bar charts
const DEFAULT_GRAPH_INTERVAL = 15 * 1000;
/**
* Returns a fresh Explore area state
*/
const makeExploreItemState = (): ExploreItemState => ({
StartPage: undefined,
containerWidth: 0,
datasourceInstance: null,
datasourceError: null,
datasourceLoading: null,
datasourceMissing: false,
exploreDatasources: [],
history: [],
initialQueries: [],
initialized: false,
modifiedQueries: [],
queryTransactions: [],
queryIntervals: { interval: '15s', intervalMs: DEFAULT_GRAPH_INTERVAL },
range: DEFAULT_RANGE,
scanning: false,
scanRange: null,
showingGraph: true,
showingLogs: true,
showingTable: true,
supportsGraph: null,
supportsLogs: null,
supportsTable: null,
});
/**
* Global Explore state that handles multiple Explore areas and the split state
*/
const initialExploreState: ExploreState = {
split: null,
left: makeExploreItemState(),
right: makeExploreItemState(),
};
/**
* Reducer for an Explore area, to be used by the global Explore reducer.
*/
const itemReducer = (state, action: Action): ExploreItemState => {
switch (action.type) {
case ActionTypes.AddQueryRow: {
const { initialQueries, modifiedQueries, queryTransactions } = state;
const { index, query } = action.payload;
// Add new query row after given index, keep modifications of existing rows
const nextModifiedQueries = [
...modifiedQueries.slice(0, index + 1),
{ ...query },
...initialQueries.slice(index + 1),
];
// Add to initialQueries, which will cause a new row to be rendered
const nextQueries = [...initialQueries.slice(0, index + 1), { ...query }, ...initialQueries.slice(index + 1)];
// Ongoing transactions need to update their row indices
const nextQueryTransactions = queryTransactions.map(qt => {
if (qt.rowIndex > index) {
return {
...qt,
rowIndex: qt.rowIndex + 1,
};
}
return qt;
});
return {
...state,
initialQueries: nextQueries,
logsHighlighterExpressions: undefined,
modifiedQueries: nextModifiedQueries,
queryTransactions: nextQueryTransactions,
};
}
case ActionTypes.ChangeQuery: {
const { initialQueries, queryTransactions } = state;
let { modifiedQueries } = state;
const { query, index, override } = action.payload;
// Fast path: only change modifiedQueries to not trigger an update
modifiedQueries[index] = query;
if (!override) {
return {
...state,
modifiedQueries,
};
}
// Override path: queries are completely reset
const nextQuery: DataQuery = {
...query,
...generateEmptyQuery(index),
};
const nextQueries = [...initialQueries];
nextQueries[index] = nextQuery;
modifiedQueries = [...nextQueries];
// Discard ongoing transaction related to row query
const nextQueryTransactions = queryTransactions.filter(qt => qt.rowIndex !== index);
return {
...state,
initialQueries: nextQueries,
modifiedQueries: nextQueries.slice(),
queryTransactions: nextQueryTransactions,
};
}
case ActionTypes.ChangeSize: {
const { range, datasourceInstance } = state;
let interval = '1s';
if (datasourceInstance && datasourceInstance.interval) {
interval = datasourceInstance.interval;
}
const containerWidth = action.payload.width;
const queryIntervals = getIntervals(range, interval, containerWidth);
return { ...state, containerWidth, queryIntervals };
}
case ActionTypes.ChangeTime: {
return {
...state,
range: action.payload.range,
};
}
case ActionTypes.ClearQueries: {
const queries = ensureQueries();
return {
...state,
initialQueries: queries.slice(),
modifiedQueries: queries.slice(),
queryTransactions: [],
showingStartPage: Boolean(state.StartPage),
};
}
case ActionTypes.HighlightLogsExpression: {
const { expressions } = action.payload;
return { ...state, logsHighlighterExpressions: expressions };
}
case ActionTypes.InitializeExplore: {
const { containerWidth, datasource, eventBridge, exploreDatasources, queries, range } = action.payload;
return {
...state,
containerWidth,
eventBridge,
exploreDatasources,
range,
initialDatasource: datasource,
initialQueries: queries,
initialized: true,
modifiedQueries: queries.slice(),
};
}
case ActionTypes.LoadDatasourceFailure: {
return { ...state, datasourceError: action.payload.error, datasourceLoading: false };
}
case ActionTypes.LoadDatasourceMissing: {
return { ...state, datasourceMissing: true, datasourceLoading: false };
}
case ActionTypes.LoadDatasourcePending: {
return { ...state, datasourceLoading: true, requestedDatasourceId: action.payload.datasourceId };
}
case ActionTypes.LoadDatasourceSuccess: {
const { containerWidth, range } = state;
const {
StartPage,
datasourceInstance,
history,
initialDatasource,
initialQueries,
showingStartPage,
supportsGraph,
supportsLogs,
supportsTable,
} = action.payload;
const queryIntervals = getIntervals(range, datasourceInstance.interval, containerWidth);
return {
...state,
queryIntervals,
StartPage,
datasourceInstance,
history,
initialDatasource,
initialQueries,
showingStartPage,
supportsGraph,
supportsLogs,
supportsTable,
datasourceLoading: false,
datasourceMissing: false,
logsHighlighterExpressions: undefined,
modifiedQueries: initialQueries.slice(),
queryTransactions: [],
};
}
case ActionTypes.ModifyQueries: {
const { initialQueries, modifiedQueries, queryTransactions } = state;
const { modification, index, modifier } = action.payload as any;
let nextQueries: DataQuery[];
let nextQueryTransactions;
if (index === undefined) {
// Modify all queries
nextQueries = initialQueries.map((query, i) => ({
...modifier(modifiedQueries[i], modification),
...generateEmptyQuery(i),
}));
// Discard all ongoing transactions
nextQueryTransactions = [];
} else {
// Modify query only at index
nextQueries = initialQueries.map((query, i) => {
// Synchronize all queries with local query cache to ensure consistency
// TODO still needed?
return i === index
? {
...modifier(modifiedQueries[i], modification),
...generateEmptyQuery(i),
}
: query;
});
nextQueryTransactions = queryTransactions
// Consume the hint corresponding to the action
.map(qt => {
if (qt.hints != null && qt.rowIndex === index) {
qt.hints = qt.hints.filter(hint => hint.fix.action !== modification);
}
return qt;
})
// Preserve previous row query transaction to keep results visible if next query is incomplete
.filter(qt => modification.preventSubmit || qt.rowIndex !== index);
}
return {
...state,
initialQueries: nextQueries,
modifiedQueries: nextQueries.slice(),
queryTransactions: nextQueryTransactions,
};
}
case ActionTypes.QueryTransactionFailure: {
const { queryTransactions } = action.payload;
return {
...state,
queryTransactions,
showingStartPage: false,
};
}
case ActionTypes.QueryTransactionStart: {
const { datasourceInstance, queryIntervals, queryTransactions } = state;
const { resultType, rowIndex, transaction } = action.payload;
// Discarding existing transactions of same type
const remainingTransactions = queryTransactions.filter(
qt => !(qt.resultType === resultType && qt.rowIndex === rowIndex)
);
// Append new transaction
const nextQueryTransactions: QueryTransaction[] = [...remainingTransactions, transaction];
const results = calculateResultsFromQueryTransactions(
nextQueryTransactions,
datasourceInstance,
queryIntervals.intervalMs
);
return {
...state,
...results,
queryTransactions: nextQueryTransactions,
showingStartPage: false,
};
}
case ActionTypes.QueryTransactionSuccess: {
const { datasourceInstance, queryIntervals } = state;
const { history, queryTransactions } = action.payload;
const results = calculateResultsFromQueryTransactions(
queryTransactions,
datasourceInstance,
queryIntervals.intervalMs
);
return {
...state,
...results,
history,
queryTransactions,
showingStartPage: false,
};
}
case ActionTypes.RemoveQueryRow: {
const { datasourceInstance, initialQueries, queryIntervals, queryTransactions } = state;
let { modifiedQueries } = state;
const { index } = action.payload;
modifiedQueries = [...modifiedQueries.slice(0, index), ...modifiedQueries.slice(index + 1)];
if (initialQueries.length <= 1) {
return state;
}
const nextQueries = [...initialQueries.slice(0, index), ...initialQueries.slice(index + 1)];
// Discard transactions related to row query
const nextQueryTransactions = queryTransactions.filter(qt => qt.rowIndex !== index);
const results = calculateResultsFromQueryTransactions(
nextQueryTransactions,
datasourceInstance,
queryIntervals.intervalMs
);
return {
...state,
...results,
initialQueries: nextQueries,
logsHighlighterExpressions: undefined,
modifiedQueries: nextQueries.slice(),
queryTransactions: nextQueryTransactions,
};
}
case ActionTypes.RunQueriesEmpty: {
return { ...state, queryTransactions: [] };
}
case ActionTypes.ScanRange: {
return { ...state, scanRange: action.payload.range };
}
case ActionTypes.ScanStart: {
return { ...state, scanning: true };
}
case ActionTypes.ScanStop: {
const { queryTransactions } = state;
const nextQueryTransactions = queryTransactions.filter(qt => qt.scanning && !qt.done);
return { ...state, queryTransactions: nextQueryTransactions, scanning: false, scanRange: undefined };
}
case ActionTypes.SetQueries: {
const { queries } = action.payload;
return { ...state, initialQueries: queries.slice(), modifiedQueries: queries.slice() };
}
case ActionTypes.ToggleGraph: {
const showingGraph = !state.showingGraph;
let nextQueryTransactions = state.queryTransactions;
if (!showingGraph) {
// Discard transactions related to Graph query
nextQueryTransactions = state.queryTransactions.filter(qt => qt.resultType !== 'Graph');
}
return { ...state, queryTransactions: nextQueryTransactions, showingGraph };
}
case ActionTypes.ToggleLogs: {
const showingLogs = !state.showingLogs;
let nextQueryTransactions = state.queryTransactions;
if (!showingLogs) {
// Discard transactions related to Logs query
nextQueryTransactions = state.queryTransactions.filter(qt => qt.resultType !== 'Logs');
}
return { ...state, queryTransactions: nextQueryTransactions, showingLogs };
}
case ActionTypes.ToggleTable: {
const showingTable = !state.showingTable;
if (showingTable) {
return { ...state, showingTable, queryTransactions: state.queryTransactions };
}
// Toggle off needs discarding of table queries and results
const nextQueryTransactions = state.queryTransactions.filter(qt => qt.resultType !== 'Table');
const results = calculateResultsFromQueryTransactions(
nextQueryTransactions,
state.datasourceInstance,
state.queryIntervals.intervalMs
);
return { ...state, ...results, queryTransactions: nextQueryTransactions, showingTable };
}
}
return state;
};
/**
* Global Explore reducer that handles multiple Explore areas (left and right).
* Actions that have an `exploreId` get routed to the ExploreItemReducer.
*/
export const exploreReducer = (state = initialExploreState, action: Action): ExploreState => {
switch (action.type) {
case ActionTypes.SplitClose: {
return {
...state,
split: false,
};
}
case ActionTypes.SplitOpen: {
return {
...state,
split: true,
right: action.payload.itemState,
};
}
case ActionTypes.InitializeExploreSplit: {
return {
...state,
split: true,
};
}
}
if (action.payload) {
const { exploreId } = action.payload as any;
if (exploreId !== undefined) {
const exploreItemState = state[exploreId];
return {
...state,
[exploreId]: itemReducer(exploreItemState, action),
};
}
}
return state;
};
export default {
explore: exploreReducer,
};

View File

@ -7,6 +7,7 @@ import teamsReducers from 'app/features/teams/state/reducers';
import apiKeysReducers from 'app/features/api-keys/state/reducers';
import foldersReducers from 'app/features/folders/state/reducers';
import dashboardReducers from 'app/features/dashboard/state/reducers';
import exploreReducers from 'app/features/explore/state/reducers';
import pluginReducers from 'app/features/plugins/state/reducers';
import dataSourcesReducers from 'app/features/datasources/state/reducers';
import usersReducers from 'app/features/users/state/reducers';
@ -20,6 +21,7 @@ const rootReducers = {
...apiKeysReducers,
...foldersReducers,
...dashboardReducers,
...exploreReducers,
...pluginReducers,
...dataSourcesReducers,
...usersReducers,

View File

@ -1,10 +1,12 @@
import { Value } from 'slate';
import { RawTimeRange, TimeRange } from '@grafana/ui';
import { Emitter } from 'app/core/core';
import { LogsModel } from 'app/core/logs_model';
import TableModel from 'app/core/table_model';
import { DataSourceSelectItem } from 'app/types/datasources';
import { DataQuery } from './series';
import { RawTimeRange } from '@grafana/ui';
import TableModel from 'app/core/table_model';
import { LogsModel } from 'app/core/logs_model';
import { DataSourceSelectItem } from 'app/types/datasources';
export interface CompletionItem {
/**
@ -76,6 +78,174 @@ export interface CompletionItemGroup {
skipSort?: boolean;
}
export enum ExploreId {
left = 'left',
right = 'right',
}
/**
* Global Explore state
*/
export interface ExploreState {
/**
* True if split view is active.
*/
split: boolean;
/**
* Explore state of the left split (left is default in non-split view).
*/
left: ExploreItemState;
/**
* Explore state of the right area in split view.
*/
right: ExploreItemState;
}
export interface ExploreItemState {
/**
* React component to be shown when no queries have been run yet, e.g., for a query language cheat sheet.
*/
StartPage?: any;
/**
* Width used for calculating the graph interval (can't have more datapoints than pixels)
*/
containerWidth: number;
/**
* Datasource instance that has been selected. Datasource-specific logic can be run on this object.
*/
datasourceInstance: any;
/**
* Error to be shown when datasource loading or testing failed.
*/
datasourceError: string;
/**
* True if the datasource is loading. `null` if the loading has not started yet.
*/
datasourceLoading: boolean | null;
/**
* True if there is no datasource to be selected.
*/
datasourceMissing: boolean;
/**
* Emitter to send events to the rest of Grafana.
*/
eventBridge?: Emitter;
/**
* List of datasources to be shown in the datasource selector.
*/
exploreDatasources: DataSourceSelectItem[];
/**
* List of timeseries to be shown in the Explore graph result viewer.
*/
graphResult?: any[];
/**
* History of recent queries. Datasource-specific and initialized via localStorage.
*/
history: HistoryItem[];
/**
* Initial datasource for this Explore, e.g., set via URL.
*/
initialDatasource?: string;
/**
* Initial queries for this Explore, e.g., set via URL. Each query will be
* converted to a query row. Query edits should be tracked in `modifiedQueries` though.
*/
initialQueries: DataQuery[];
/**
* True if this Explore area has been initialized.
* Used to distinguish URL state injection versus split view state injection.
*/
initialized: boolean;
/**
* Log line substrings to be highlighted as you type in a query field.
* Currently supports only the first query row.
*/
logsHighlighterExpressions?: string[];
/**
* Log query result to be displayed in the logs result viewer.
*/
logsResult?: LogsModel;
/**
* Copy of `initialQueries` that tracks user edits.
* Don't connect this property to a react component as it is updated on every query change.
* Used when running queries. Needs to be reset to `initialQueries` when those are reset as well.
*/
modifiedQueries: DataQuery[];
/**
* Query intervals for graph queries to determine how many datapoints to return.
* Needs to be updated when `datasourceInstance` or `containerWidth` is changed.
*/
queryIntervals: QueryIntervals;
/**
* List of query transaction to track query duration and query result.
* Graph/Logs/Table results are calculated on the fly from the transaction,
* based on the transaction's result types. Transaction also holds the row index
* so that results can be dropped and re-computed without running queries again
* when query rows are removed.
*/
queryTransactions: QueryTransaction[];
/**
* Tracks datasource when selected in the datasource selector.
* Allows the selection to be discarded if something went wrong during the asynchronous
* loading of the datasource.
*/
requestedDatasourceId?: number;
/**
* Time range for this Explore. Managed by the time picker and used by all query runs.
*/
range: TimeRange | RawTimeRange;
/**
* Scanner function that calculates a new range, triggers a query run, and returns the new range.
*/
scanner?: RangeScanner;
/**
* True if scanning for more results is active.
*/
scanning?: boolean;
/**
* Current scanning range to be shown to the user while scanning is active.
*/
scanRange?: RawTimeRange;
/**
* True if graph result viewer is expanded. Query runs will contain graph queries.
*/
showingGraph: boolean;
/**
* True if logs result viewer is expanded. Query runs will contain logs queries.
*/
showingLogs: boolean;
/**
* True StartPage needs to be shown. Typically set to `false` once queries have been run.
*/
showingStartPage?: boolean;
/**
* True if table result viewer is expanded. Query runs will contain table queries.
*/
showingTable: boolean;
/**
* True if `datasourceInstance` supports graph queries.
*/
supportsGraph: boolean | null;
/**
* True if `datasourceInstance` supports logs queries.
*/
supportsLogs: boolean | null;
/**
* True if `datasourceInstance` supports table queries.
*/
supportsTable: boolean | null;
/**
* Table model that combines all query table results into a single table.
*/
tableResult?: TableModel;
}
export interface ExploreUrlState {
datasource: string;
queries: any[]; // Should be a DataQuery, but we're going to strip refIds, so typing makes less sense
range: RawTimeRange;
}
export interface HistoryItem {
ts: number;
query: DataQuery;
@ -128,6 +298,19 @@ export interface QueryHintGetter {
(query: DataQuery, results: any[], ...rest: any): QueryHint[];
}
export interface QueryIntervals {
interval: string;
intervalMs: number;
}
export interface QueryOptions {
interval: string;
format: string;
hinting?: boolean;
instant?: boolean;
valueWithRefId?: boolean;
}
export interface QueryTransaction {
id: string;
done: boolean;
@ -142,6 +325,8 @@ export interface QueryTransaction {
scanning?: boolean;
}
export type RangeScanner = () => RawTimeRange;
export interface TextMatch {
text: string;
start: number;
@ -149,38 +334,4 @@ export interface TextMatch {
end: number;
}
export interface ExploreState {
StartPage?: any;
datasource: any;
datasourceError: any;
datasourceLoading: boolean | null;
datasourceMissing: boolean;
exploreDatasources: DataSourceSelectItem[];
graphInterval: number; // in ms
graphResult?: any[];
history: HistoryItem[];
initialDatasource?: string;
initialQueries: DataQuery[];
logsHighlighterExpressions?: string[];
logsResult?: LogsModel;
queryTransactions: QueryTransaction[];
range: RawTimeRange;
scanning?: boolean;
scanRange?: RawTimeRange;
showingGraph: boolean;
showingLogs: boolean;
showingStartPage?: boolean;
showingTable: boolean;
supportsGraph: boolean | null;
supportsLogs: boolean | null;
supportsTable: boolean | null;
tableResult?: TableModel;
}
export interface ExploreUrlState {
datasource: string;
queries: any[]; // Should be a DataQuery, but we're going to strip refIds, so typing makes less sense
range: RawTimeRange;
}
export type ResultType = 'Graph' | 'Logs' | 'Table';

View File

@ -19,6 +19,7 @@ import {
} from './appNotifications';
import { DashboardSearchHit } from './search';
import { ValidationEvents, ValidationRule } from './form';
import { ExploreState } from './explore';
export {
Team,
TeamsState,
@ -81,6 +82,7 @@ export interface StoreState {
folder: FolderState;
dashboard: DashboardState;
dataSources: DataSourcesState;
explore: ExploreState;
users: UsersState;
organization: OrganizationState;
appNotifications: AppNotificationsState;

View File

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