The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/public/app/features/explore/state/reducers.ts

463 lines
14 KiB

import {
calculateResultsFromQueryTransactions,
generateEmptyQuery,
getIntervals,
ensureQueries,
} from 'app/core/utils/explore';
import { ExploreItemState, ExploreState, QueryTransaction } from 'app/types/explore';
import { DataQuery } from '@grafana/ui/src/types';
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
*/
export 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
*/
export const initialExploreState: ExploreState = {
split: null,
left: makeExploreItemState(),
right: makeExploreItemState(),
};
/**
* Reducer for an Explore area, to be used by the global Explore reducer.
*/
export 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, requestedDatasourceName: action.payload.datasourceName };
}
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,
datasourceError: null,
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, scanner: action.payload.scanner };
}
case ActionTypes.ScanStop: {
const { queryTransactions } = state;
const nextQueryTransactions = queryTransactions.filter(qt => qt.scanning && !qt.done);
return {
...state,
queryTransactions: nextQueryTransactions,
scanning: false,
scanRange: undefined,
scanner: 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 };
}
case ActionTypes.ResetExplore: {
return initialExploreState;
}
}
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,
};