mirror of
https://github.com/grafana/grafana.git
synced 2025-08-03 02:21:48 +08:00
WIP Explore redux migration
This commit is contained in:
@ -1,6 +1,7 @@
|
|||||||
import _ from 'lodash';
|
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 { renderUrl } from 'app/core/utils/url';
|
||||||
import kbn from 'app/core/utils/kbn';
|
import kbn from 'app/core/utils/kbn';
|
||||||
import store from 'app/core/store';
|
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 TimeSeries from 'app/core/time_series2';
|
||||||
import TableModel, { mergeTablesIntoModel } from 'app/core/table_model';
|
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 { DataQuery, DataSourceApi } from 'app/types/series';
|
||||||
import { RawTimeRange, IntervalValues } from '@grafana/ui';
|
|
||||||
|
|
||||||
export const DEFAULT_RANGE = {
|
export const DEFAULT_RANGE = {
|
||||||
from: 'now-6h',
|
from: 'now-6h',
|
||||||
@ -19,6 +26,8 @@ export const DEFAULT_RANGE = {
|
|||||||
|
|
||||||
const MAX_HISTORY_ITEMS = 100;
|
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.
|
* 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;
|
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;
|
const clearQueryKeys: ((query: DataQuery) => object) = ({ key, refId, ...rest }) => rest;
|
||||||
|
|
||||||
export function parseUrlState(initial: string | undefined): ExploreUrlState {
|
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 };
|
return { datasource: null, queries: [], range: DEFAULT_RANGE };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function serializeStateToUrlParam(state: ExploreState, compact?: boolean): string {
|
export function serializeStateToUrlParam(urlState: ExploreUrlState, compact?: boolean): string {
|
||||||
const urlState: ExploreUrlState = {
|
// const urlState: ExploreUrlState = {
|
||||||
datasource: state.initialDatasource,
|
// datasource: state.initialDatasource,
|
||||||
queries: state.initialQueries.map(clearQueryKeys),
|
// queries: state.initialQueries.map(clearQueryKeys),
|
||||||
range: state.range,
|
// range: state.range,
|
||||||
};
|
// };
|
||||||
if (compact) {
|
if (compact) {
|
||||||
return JSON.stringify([urlState.range.from, urlState.range.to, urlState.datasource, ...urlState.queries]);
|
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}`;
|
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) };
|
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[] {
|
export function ensureQueries(queries?: DataQuery[]): DataQuery[] {
|
||||||
if (queries && typeof queries === 'object' && queries.length > 0) {
|
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
694
public/app/features/explore/state/actions.ts
Normal file
694
public/app/features/explore/state/actions.ts
Normal 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 };
|
||||||
|
}
|
412
public/app/features/explore/state/reducers.ts
Normal file
412
public/app/features/explore/state/reducers.ts
Normal 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,
|
||||||
|
};
|
@ -7,6 +7,7 @@ import teamsReducers from 'app/features/teams/state/reducers';
|
|||||||
import apiKeysReducers from 'app/features/api-keys/state/reducers';
|
import apiKeysReducers from 'app/features/api-keys/state/reducers';
|
||||||
import foldersReducers from 'app/features/folders/state/reducers';
|
import foldersReducers from 'app/features/folders/state/reducers';
|
||||||
import dashboardReducers from 'app/features/dashboard/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 pluginReducers from 'app/features/plugins/state/reducers';
|
||||||
import dataSourcesReducers from 'app/features/datasources/state/reducers';
|
import dataSourcesReducers from 'app/features/datasources/state/reducers';
|
||||||
import usersReducers from 'app/features/users/state/reducers';
|
import usersReducers from 'app/features/users/state/reducers';
|
||||||
@ -20,6 +21,7 @@ const rootReducers = {
|
|||||||
...apiKeysReducers,
|
...apiKeysReducers,
|
||||||
...foldersReducers,
|
...foldersReducers,
|
||||||
...dashboardReducers,
|
...dashboardReducers,
|
||||||
|
...exploreReducers,
|
||||||
...pluginReducers,
|
...pluginReducers,
|
||||||
...dataSourcesReducers,
|
...dataSourcesReducers,
|
||||||
...usersReducers,
|
...usersReducers,
|
||||||
|
@ -4,7 +4,6 @@ import { DataQuery } from './series';
|
|||||||
import { RawTimeRange } from '@grafana/ui';
|
import { RawTimeRange } from '@grafana/ui';
|
||||||
import TableModel from 'app/core/table_model';
|
import TableModel from 'app/core/table_model';
|
||||||
import { LogsModel } from 'app/core/logs_model';
|
import { LogsModel } from 'app/core/logs_model';
|
||||||
import { DataSourceSelectItem } from 'app/types/datasources';
|
|
||||||
|
|
||||||
export interface CompletionItem {
|
export interface CompletionItem {
|
||||||
/**
|
/**
|
||||||
@ -128,6 +127,19 @@ export interface QueryHintGetter {
|
|||||||
(query: DataQuery, results: any[], ...rest: any): QueryHint[];
|
(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 {
|
export interface QueryTransaction {
|
||||||
id: string;
|
id: string;
|
||||||
done: boolean;
|
done: boolean;
|
||||||
@ -142,6 +154,8 @@ export interface QueryTransaction {
|
|||||||
scanning?: boolean;
|
scanning?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type RangeScanner = () => RawTimeRange;
|
||||||
|
|
||||||
export interface TextMatch {
|
export interface TextMatch {
|
||||||
text: string;
|
text: string;
|
||||||
start: number;
|
start: number;
|
||||||
@ -153,18 +167,11 @@ export interface ExploreState {
|
|||||||
StartPage?: any;
|
StartPage?: any;
|
||||||
datasource: any;
|
datasource: any;
|
||||||
datasourceError: any;
|
datasourceError: any;
|
||||||
datasourceLoading: boolean | null;
|
|
||||||
datasourceMissing: boolean;
|
|
||||||
exploreDatasources: DataSourceSelectItem[];
|
|
||||||
graphInterval: number; // in ms
|
|
||||||
graphResult?: any[];
|
graphResult?: any[];
|
||||||
history: HistoryItem[];
|
history: HistoryItem[];
|
||||||
initialDatasource?: string;
|
|
||||||
initialQueries: DataQuery[];
|
|
||||||
logsHighlighterExpressions?: string[];
|
logsHighlighterExpressions?: string[];
|
||||||
logsResult?: LogsModel;
|
logsResult?: LogsModel;
|
||||||
queryTransactions: QueryTransaction[];
|
queryTransactions: QueryTransaction[];
|
||||||
range: RawTimeRange;
|
|
||||||
scanning?: boolean;
|
scanning?: boolean;
|
||||||
scanRange?: RawTimeRange;
|
scanRange?: RawTimeRange;
|
||||||
showingGraph: boolean;
|
showingGraph: boolean;
|
||||||
|
@ -19,6 +19,7 @@ import {
|
|||||||
} from './appNotifications';
|
} from './appNotifications';
|
||||||
import { DashboardSearchHit } from './search';
|
import { DashboardSearchHit } from './search';
|
||||||
import { ValidationEvents, ValidationRule } from './form';
|
import { ValidationEvents, ValidationRule } from './form';
|
||||||
|
import { ExploreState } from 'app/features/explore/state/reducers';
|
||||||
export {
|
export {
|
||||||
Team,
|
Team,
|
||||||
TeamsState,
|
TeamsState,
|
||||||
@ -81,6 +82,7 @@ export interface StoreState {
|
|||||||
folder: FolderState;
|
folder: FolderState;
|
||||||
dashboard: DashboardState;
|
dashboard: DashboardState;
|
||||||
dataSources: DataSourcesState;
|
dataSources: DataSourcesState;
|
||||||
|
explore: ExploreState;
|
||||||
users: UsersState;
|
users: UsersState;
|
||||||
organization: OrganizationState;
|
organization: OrganizationState;
|
||||||
appNotifications: AppNotificationsState;
|
appNotifications: AppNotificationsState;
|
||||||
|
Reference in New Issue
Block a user