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 { 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]; return { ...state, 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, }; } } 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, };