WIP Explore redux migration

This commit is contained in:
David Kaltschmidt
2019-01-10 14:24:31 +01:00
parent b7451bc774
commit 2be2deddb8
7 changed files with 1413 additions and 737 deletions

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 {
ExploreUrlState,
HistoryItem,
QueryTransaction,
ResultType,
QueryIntervals,
QueryOptions,
} from 'app/types/explore';
import { DataQuery, DataSourceApi } from 'app/types/series';
import { RawTimeRange, IntervalValues } from '@grafana/ui';
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,6 +86,62 @@ export async function getExploreUrl(
return url;
}
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,
};
}
const clearQueryKeys: ((query: DataQuery) => object) = ({ key, refId, ...rest }) => rest;
export function parseUrlState(initial: string | undefined): ExploreUrlState {
@ -103,12 +168,12 @@ 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 {
// const urlState: ExploreUrlState = {
// datasource: state.initialDatasource,
// queries: state.initialQueries.map(clearQueryKeys),
// range: state.range,
// };
if (compact) {
return JSON.stringify([urlState.range.from, urlState.range.to, urlState.datasource, ...urlState.queries]);
}
@ -123,7 +188,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,9 +197,9 @@ 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() }];
}
/**

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,694 @@
import _ from 'lodash';
import { ThunkAction } from 'redux-thunk';
import { RawTimeRange, TimeRange } from '@grafana/ui';
import {
LAST_USED_DATASOURCE_KEY,
ensureQueries,
generateEmptyQuery,
hasNonEmptyQuery,
makeTimeSeriesList,
updateHistory,
buildQueryTransaction,
} from 'app/core/utils/explore';
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 {
HistoryItem,
RangeScanner,
ResultType,
QueryOptions,
QueryTransaction,
QueryHint,
QueryHintGetter,
} from 'app/types/explore';
import { Emitter } from 'app/core/core';
import { dispatch } from 'rxjs/internal/observable/pairs';
export enum ActionTypes {
AddQueryRow = 'ADD_QUERY_ROW',
ChangeDatasource = 'CHANGE_DATASOURCE',
ChangeQuery = 'CHANGE_QUERY',
ChangeSize = 'CHANGE_SIZE',
ChangeTime = 'CHANGE_TIME',
ClickClear = 'CLICK_CLEAR',
ClickExample = 'CLICK_EXAMPLE',
ClickGraphButton = 'CLICK_GRAPH_BUTTON',
ClickLogsButton = 'CLICK_LOGS_BUTTON',
ClickTableButton = 'CLICK_TABLE_BUTTON',
HighlightLogsExpression = 'HIGHLIGHT_LOGS_EXPRESSION',
InitializeExplore = 'INITIALIZE_EXPLORE',
LoadDatasourceFailure = 'LOAD_DATASOURCE_FAILURE',
LoadDatasourceMissing = 'LOAD_DATASOURCE_MISSING',
LoadDatasourcePending = 'LOAD_DATASOURCE_PENDING',
LoadDatasourceSuccess = 'LOAD_DATASOURCE_SUCCESS',
ModifyQueries = 'MODIFY_QUERIES',
QueryTransactionFailure = 'QUERY_TRANSACTION_FAILURE',
QueryTransactionStart = 'QUERY_TRANSACTION_START',
QueryTransactionSuccess = 'QUERY_TRANSACTION_SUCCESS',
RemoveQueryRow = 'REMOVE_QUERY_ROW',
RunQueries = 'RUN_QUERIES',
RunQueriesEmpty = 'RUN_QUERIES',
ScanRange = 'SCAN_RANGE',
ScanStart = 'SCAN_START',
ScanStop = 'SCAN_STOP',
}
export interface AddQueryRowAction {
type: ActionTypes.AddQueryRow;
index: number;
query: DataQuery;
}
export interface ChangeQueryAction {
type: ActionTypes.ChangeQuery;
query: DataQuery;
index: number;
override: boolean;
}
export interface ChangeSizeAction {
type: ActionTypes.ChangeSize;
width: number;
height: number;
}
export interface ChangeTimeAction {
type: ActionTypes.ChangeTime;
range: TimeRange;
}
export interface ClickClearAction {
type: ActionTypes.ClickClear;
}
export interface ClickExampleAction {
type: ActionTypes.ClickExample;
query: DataQuery;
}
export interface ClickGraphButtonAction {
type: ActionTypes.ClickGraphButton;
}
export interface ClickLogsButtonAction {
type: ActionTypes.ClickLogsButton;
}
export interface ClickTableButtonAction {
type: ActionTypes.ClickTableButton;
}
export interface InitializeExploreAction {
type: ActionTypes.InitializeExplore;
containerWidth: number;
datasource: string;
eventBridge: Emitter;
exploreDatasources: DataSourceSelectItem[];
queries: DataQuery[];
range: RawTimeRange;
}
export interface HighlightLogsExpressionAction {
type: ActionTypes.HighlightLogsExpression;
expressions: string[];
}
export interface LoadDatasourceFailureAction {
type: ActionTypes.LoadDatasourceFailure;
error: string;
}
export interface LoadDatasourcePendingAction {
type: ActionTypes.LoadDatasourcePending;
datasourceId: number;
}
export interface LoadDatasourceMissingAction {
type: ActionTypes.LoadDatasourceMissing;
}
export interface LoadDatasourceSuccessAction {
type: ActionTypes.LoadDatasourceSuccess;
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;
modification: any;
index: number;
modifier: (queries: DataQuery[], modification: any) => DataQuery[];
}
export interface QueryTransactionFailureAction {
type: ActionTypes.QueryTransactionFailure;
queryTransactions: QueryTransaction[];
}
export interface QueryTransactionStartAction {
type: ActionTypes.QueryTransactionStart;
resultType: ResultType;
rowIndex: number;
transaction: QueryTransaction;
}
export interface QueryTransactionSuccessAction {
type: ActionTypes.QueryTransactionSuccess;
history: HistoryItem[];
queryTransactions: QueryTransaction[];
}
export interface RemoveQueryRowAction {
type: ActionTypes.RemoveQueryRow;
index: number;
}
export interface ScanStartAction {
type: ActionTypes.ScanStart;
scanner: RangeScanner;
}
export interface ScanRangeAction {
type: ActionTypes.ScanRange;
range: RawTimeRange;
}
export interface ScanStopAction {
type: ActionTypes.ScanStop;
}
export type Action =
| AddQueryRowAction
| ChangeQueryAction
| ChangeSizeAction
| ChangeTimeAction
| ClickClearAction
| ClickExampleAction
| ClickGraphButtonAction
| ClickLogsButtonAction
| ClickTableButtonAction
| HighlightLogsExpressionAction
| InitializeExploreAction
| LoadDatasourceFailureAction
| LoadDatasourceMissingAction
| LoadDatasourcePendingAction
| LoadDatasourceSuccessAction
| ModifyQueriesAction
| QueryTransactionFailureAction
| QueryTransactionStartAction
| QueryTransactionSuccessAction
| RemoveQueryRowAction
| ScanRangeAction
| ScanStartAction
| ScanStopAction;
type ThunkResult<R> = ThunkAction<R, StoreState, undefined, Action>;
export function addQueryRow(index: number): AddQueryRowAction {
const query = generateEmptyQuery(index + 1);
return { type: ActionTypes.AddQueryRow, index, query };
}
export function changeDatasource(datasource: string): ThunkResult<void> {
return async dispatch => {
const instance = await getDatasourceSrv().get(datasource);
dispatch(loadDatasource(instance));
};
}
export function changeQuery(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 });
if (override) {
dispatch(runQueries());
}
};
}
export function changeSize({ height, width }: { height: number; width: number }): ChangeSizeAction {
return { type: ActionTypes.ChangeSize, height, width };
}
export function changeTime(range: TimeRange): ThunkResult<void> {
return dispatch => {
dispatch({ type: ActionTypes.ChangeTime, range });
dispatch(runQueries());
};
}
export function clickExample(rawQuery: DataQuery): 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 });
// TODO save state
};
}
export function clickGraphButton(): ThunkResult<void> {
return (dispatch, getState) => {
dispatch({ type: ActionTypes.ClickGraphButton });
if (getState().explore.showingGraph) {
dispatch(runQueries());
}
};
}
export function clickLogsButton(): ThunkResult<void> {
return (dispatch, getState) => {
dispatch({ type: ActionTypes.ClickLogsButton });
if (getState().explore.showingLogs) {
dispatch(runQueries());
}
};
}
export function clickTableButton(): ThunkResult<void> {
return (dispatch, getState) => {
dispatch({ type: ActionTypes.ClickTableButton });
if (getState().explore.showingTable) {
dispatch(runQueries());
}
};
}
export function highlightLogsExpression(expressions: string[]): HighlightLogsExpressionAction {
return { type: ActionTypes.HighlightLogsExpression, expressions };
}
export function initializeExplore(
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,
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(instance));
} else {
dispatch(loadDatasourceMissing);
}
};
}
export const loadDatasourceFailure = (error: string): LoadDatasourceFailureAction => ({
type: ActionTypes.LoadDatasourceFailure,
error,
});
export const loadDatasourceMissing: LoadDatasourceMissingAction = { type: ActionTypes.LoadDatasourceMissing };
export const loadDatasourcePending = (datasourceId: number): LoadDatasourcePendingAction => ({
type: ActionTypes.LoadDatasourcePending,
datasourceId,
});
export const loadDatasourceSuccess = (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,
StartPage,
datasourceInstance: instance,
history,
initialDatasource: instance.name,
initialQueries: queries,
showingStartPage: Boolean(StartPage),
supportsGraph,
supportsLogs,
supportsTable,
};
};
export function loadDatasource(instance: any): ThunkResult<void> {
return async (dispatch, getState) => {
const datasourceId = instance.meta.id;
// Keep ID to track selection
dispatch(loadDatasourcePending(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(datasourceError));
return;
}
if (datasourceId !== getState().explore.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.modifiedQueries;
let importedQueries = queries;
const origin = getState().explore.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.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(instance, nextQueries));
dispatch(runQueries());
};
}
export function modifyQueries(modification: any, index: number, modifier: any): ThunkResult<void> {
return dispatch => {
dispatch({ type: ActionTypes.ModifyQueries, modification, index, modifier });
if (!modification.preventSubmit) {
dispatch(runQueries());
}
};
}
export function queryTransactionFailure(transactionId: string, response: any, datasourceId: string): ThunkResult<void> {
return (dispatch, getState) => {
const { datasourceInstance, queryTransactions } = getState().explore;
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 null;
}
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, queryTransactions: nextQueryTransactions });
};
}
export function queryTransactionStart(
transaction: QueryTransaction,
resultType: ResultType,
rowIndex: number
): QueryTransactionStartAction {
return { type: ActionTypes.QueryTransactionStart, resultType, rowIndex, transaction };
}
export function queryTransactionSuccess(
transactionId: string,
result: any,
latency: number,
queries: DataQuery[],
datasourceId: string
): ThunkResult<void> {
return (dispatch, getState) => {
const { datasourceInstance, history, queryTransactions, scanner, scanning } = getState().explore;
// 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,
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, range });
}
} else {
// We can stop scanning if we have a result
dispatch(scanStop());
}
}
};
}
export function removeQueryRow(index: number): ThunkResult<void> {
return dispatch => {
dispatch({ type: ActionTypes.RemoveQueryRow, index });
dispatch(runQueries());
};
}
export function runQueries() {
return (dispatch, getState) => {
const {
datasourceInstance,
modifiedQueries,
showingLogs,
showingGraph,
showingTable,
supportsGraph,
supportsLogs,
supportsTable,
} = getState().explore;
if (!hasNonEmptyQuery(modifiedQueries)) {
dispatch({ type: ActionTypes.RunQueriesEmpty });
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(
'Table',
{
interval,
format: 'table',
instant: true,
valueWithRefId: true,
},
data => data[0]
)
);
}
if (showingGraph && supportsGraph) {
dispatch(
runQueriesForType(
'Graph',
{
interval,
format: 'time_series',
instant: false,
},
makeTimeSeriesList
)
);
}
if (showingLogs && supportsLogs) {
dispatch(runQueriesForType('Logs', { interval, format: 'logs' }));
}
// TODO save state
};
}
function runQueriesForType(resultType: ResultType, queryOptions: QueryOptions, resultGetter?: any) {
return async (dispatch, getState) => {
const {
datasourceInstance,
eventBridge,
modifiedQueries: queries,
queryIntervals,
range,
scanning,
} = getState().explore;
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(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));
} catch (response) {
eventBridge.emit('data-error', response);
dispatch(queryTransactionFailure(transaction.id, response, datasourceId));
}
});
};
}
export function scanStart(scanner: RangeScanner): ThunkResult<void> {
return dispatch => {
dispatch({ type: ActionTypes.ScanStart, scanner });
const range = scanner();
dispatch({ type: ActionTypes.ScanRange, range });
};
}
export function scanStop(): ScanStopAction {
return { type: ActionTypes.ScanStop };
}

View File

@ -0,0 +1,412 @@
import { RawTimeRange, TimeRange } from '@grafana/ui';
import {
calculateResultsFromQueryTransactions,
generateEmptyQuery,
getIntervals,
ensureQueries,
} from 'app/core/utils/explore';
import { DataSourceSelectItem } from 'app/types/datasources';
import { HistoryItem, QueryTransaction, QueryIntervals, RangeScanner } from 'app/types/explore';
import { DataQuery } from 'app/types/series';
import { Action, ActionTypes } from './actions';
import { Emitter } from 'app/core/core';
import { LogsModel } from 'app/core/logs_model';
import TableModel from 'app/core/table_model';
// TODO move to types
export interface ExploreState {
StartPage?: any;
containerWidth: number;
datasourceInstance: any;
datasourceError: string;
datasourceLoading: boolean | null;
datasourceMissing: boolean;
eventBridge?: Emitter;
exploreDatasources: DataSourceSelectItem[];
graphResult?: any[];
history: HistoryItem[];
initialDatasource?: string;
initialQueries: DataQuery[];
logsHighlighterExpressions?: string[];
logsResult?: LogsModel;
modifiedQueries: DataQuery[];
queryIntervals: QueryIntervals;
queryTransactions: QueryTransaction[];
requestedDatasourceId?: number;
range: TimeRange | RawTimeRange;
scanner?: RangeScanner;
scanning?: boolean;
scanRange?: RawTimeRange;
showingGraph: boolean;
showingLogs: boolean;
showingStartPage?: boolean;
showingTable: boolean;
supportsGraph: boolean | null;
supportsLogs: boolean | null;
supportsTable: boolean | null;
tableResult?: TableModel;
}
export const DEFAULT_RANGE = {
from: 'now-6h',
to: 'now',
};
// Millies step for helper bar charts
const DEFAULT_GRAPH_INTERVAL = 15 * 1000;
const initialExploreState: ExploreState = {
StartPage: undefined,
containerWidth: 0,
datasourceInstance: null,
datasourceError: null,
datasourceLoading: null,
datasourceMissing: false,
exploreDatasources: [],
history: [],
initialQueries: [],
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,
};
export const exploreReducer = (state = initialExploreState, action: Action): ExploreState => {
switch (action.type) {
case ActionTypes.AddQueryRow: {
const { initialQueries, modifiedQueries, queryTransactions } = state;
const { index, query } = action;
modifiedQueries[index + 1] = query;
const nextQueries = [
...initialQueries.slice(0, index + 1),
{ ...modifiedQueries[index + 1] },
...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,
modifiedQueries,
initialQueries: nextQueries,
logsHighlighterExpressions: undefined,
queryTransactions: nextQueryTransactions,
};
}
case ActionTypes.ChangeQuery: {
const { initialQueries, queryTransactions } = state;
let { modifiedQueries } = state;
const { query, index, override } = action;
modifiedQueries[index] = query;
if (override) {
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,
};
}
return {
...state,
modifiedQueries,
};
}
case ActionTypes.ChangeSize: {
const { range, datasourceInstance } = state;
if (!datasourceInstance) {
return state;
}
const containerWidth = action.width;
const queryIntervals = getIntervals(range, datasourceInstance.interval, containerWidth);
return { ...state, containerWidth, queryIntervals };
}
case ActionTypes.ChangeTime: {
return {
...state,
range: action.range,
};
}
case ActionTypes.ClickClear: {
const queries = ensureQueries();
return {
...state,
initialQueries: queries.slice(),
modifiedQueries: queries.slice(),
showingStartPage: Boolean(state.StartPage),
};
}
case ActionTypes.ClickExample: {
const modifiedQueries = [action.query];
return { ...state, initialQueries: modifiedQueries.slice(), modifiedQueries };
}
case ActionTypes.ClickGraphButton: {
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.ClickLogsButton: {
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.ClickTableButton: {
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 };
}
case ActionTypes.InitializeExplore: {
const { containerWidth, eventBridge, exploreDatasources, range } = action;
return {
...state,
containerWidth,
eventBridge,
exploreDatasources,
range,
initialDatasource: action.datasource,
initialQueries: action.queries,
modifiedQueries: action.queries.slice(),
};
}
case ActionTypes.LoadDatasourceFailure: {
return { ...state, datasourceError: action.error, datasourceLoading: false };
}
case ActionTypes.LoadDatasourceMissing: {
return { ...state, datasourceMissing: true, datasourceLoading: false };
}
case ActionTypes.LoadDatasourcePending: {
return { ...state, datasourceLoading: true, requestedDatasourceId: action.datasourceId };
}
case ActionTypes.LoadDatasourceSuccess: {
const { containerWidth, range } = state;
const queryIntervals = getIntervals(range, action.datasourceInstance.interval, containerWidth);
return {
...state,
queryIntervals,
StartPage: action.StartPage,
datasourceInstance: action.datasourceInstance,
datasourceLoading: false,
datasourceMissing: false,
history: action.history,
initialDatasource: action.initialDatasource,
initialQueries: action.initialQueries,
logsHighlighterExpressions: undefined,
modifiedQueries: action.initialQueries.slice(),
showingStartPage: action.showingStartPage,
supportsGraph: action.supportsGraph,
supportsLogs: action.supportsLogs,
supportsTable: action.supportsTable,
};
}
case ActionTypes.ModifyQueries: {
const { initialQueries, modifiedQueries, queryTransactions } = state;
const { action: modification, index, modifier } = action 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.RemoveQueryRow: {
const { datasourceInstance, initialQueries, queryIntervals, queryTransactions } = state;
let { modifiedQueries } = state;
const { index } = action;
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.QueryTransactionFailure: {
const { queryTransactions } = action;
return {
...state,
queryTransactions,
showingStartPage: false,
};
}
case ActionTypes.QueryTransactionStart: {
const { datasourceInstance, queryIntervals, queryTransactions } = state;
const { resultType, rowIndex, transaction } = action;
// 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;
const results = calculateResultsFromQueryTransactions(
queryTransactions,
datasourceInstance,
queryIntervals.intervalMs
);
return {
...state,
...results,
history,
queryTransactions,
showingStartPage: false,
};
}
case ActionTypes.ScanRange: {
return { ...state, scanRange: action.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 };
}
}
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

@ -4,7 +4,6 @@ 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 {
/**
@ -128,6 +127,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 +154,8 @@ export interface QueryTransaction {
scanning?: boolean;
}
export type RangeScanner = () => RawTimeRange;
export interface TextMatch {
text: string;
start: number;
@ -153,18 +167,11 @@ 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;

View File

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