mirror of
https://github.com/grafana/grafana.git
synced 2025-02-13 00:55:47 -06:00
* feat(explore): make it possible to close left pane of split view * Use action's type prop instead of enum
617 lines
18 KiB
TypeScript
617 lines
18 KiB
TypeScript
// @ts-ignore
|
|
import _ from 'lodash';
|
|
import {
|
|
calculateResultsFromQueryTransactions,
|
|
generateEmptyQuery,
|
|
getIntervals,
|
|
ensureQueries,
|
|
getQueryKeys,
|
|
parseUrlState,
|
|
DEFAULT_UI_STATE,
|
|
} from 'app/core/utils/explore';
|
|
import { ExploreItemState, ExploreState, QueryTransaction, ExploreId, ExploreUpdateState } from 'app/types/explore';
|
|
import { DataQuery } from '@grafana/ui/src/types';
|
|
|
|
import { HigherOrderAction, ActionTypes, SplitCloseActionPayload, splitCloseAction } from './actionTypes';
|
|
import { reducerFactory } from 'app/core/redux';
|
|
import {
|
|
addQueryRowAction,
|
|
changeQueryAction,
|
|
changeSizeAction,
|
|
changeTimeAction,
|
|
clearQueriesAction,
|
|
highlightLogsExpressionAction,
|
|
initializeExploreAction,
|
|
updateDatasourceInstanceAction,
|
|
loadDatasourceFailureAction,
|
|
loadDatasourceMissingAction,
|
|
loadDatasourcePendingAction,
|
|
loadDatasourceSuccessAction,
|
|
modifyQueriesAction,
|
|
queryTransactionFailureAction,
|
|
queryTransactionStartAction,
|
|
queryTransactionSuccessAction,
|
|
removeQueryRowAction,
|
|
scanRangeAction,
|
|
scanStartAction,
|
|
scanStopAction,
|
|
setQueriesAction,
|
|
toggleGraphAction,
|
|
toggleLogsAction,
|
|
toggleTableAction,
|
|
queriesImportedAction,
|
|
updateUIStateAction,
|
|
toggleLogLevelAction,
|
|
} from './actionTypes';
|
|
import { updateLocation } from 'app/core/actions/location';
|
|
import { LocationUpdate } from 'app/types';
|
|
|
|
export const DEFAULT_RANGE = {
|
|
from: 'now-6h',
|
|
to: 'now',
|
|
};
|
|
|
|
// Millies step for helper bar charts
|
|
const DEFAULT_GRAPH_INTERVAL = 15 * 1000;
|
|
|
|
export const makeInitialUpdateState = (): ExploreUpdateState => ({
|
|
datasource: false,
|
|
queries: false,
|
|
range: false,
|
|
ui: false,
|
|
});
|
|
/**
|
|
* Returns a fresh Explore area state
|
|
*/
|
|
export const makeExploreItemState = (): ExploreItemState => ({
|
|
StartPage: undefined,
|
|
containerWidth: 0,
|
|
datasourceInstance: null,
|
|
requestedDatasourceName: null,
|
|
datasourceError: null,
|
|
datasourceLoading: null,
|
|
datasourceMissing: false,
|
|
exploreDatasources: [],
|
|
history: [],
|
|
queries: [],
|
|
initialized: false,
|
|
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,
|
|
queryKeys: [],
|
|
urlState: null,
|
|
update: makeInitialUpdateState(),
|
|
});
|
|
|
|
/**
|
|
* 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 = reducerFactory<ExploreItemState>({} as ExploreItemState)
|
|
.addMapper({
|
|
filter: addQueryRowAction,
|
|
mapper: (state, action): ExploreItemState => {
|
|
const { queries, queryTransactions } = state;
|
|
const { index, query } = action.payload;
|
|
|
|
// Add to queries, which will cause a new row to be rendered
|
|
const nextQueries = [...queries.slice(0, index + 1), { ...query }, ...queries.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,
|
|
queries: nextQueries,
|
|
logsHighlighterExpressions: undefined,
|
|
queryTransactions: nextQueryTransactions,
|
|
queryKeys: getQueryKeys(nextQueries, state.datasourceInstance),
|
|
};
|
|
},
|
|
})
|
|
.addMapper({
|
|
filter: changeQueryAction,
|
|
mapper: (state, action): ExploreItemState => {
|
|
const { queries, queryTransactions } = state;
|
|
const { query, index } = action.payload;
|
|
|
|
// Override path: queries are completely reset
|
|
const nextQuery: DataQuery = { ...query, ...generateEmptyQuery(state.queries) };
|
|
const nextQueries = [...queries];
|
|
nextQueries[index] = nextQuery;
|
|
|
|
// Discard ongoing transaction related to row query
|
|
const nextQueryTransactions = queryTransactions.filter(qt => qt.rowIndex !== index);
|
|
|
|
return {
|
|
...state,
|
|
queries: nextQueries,
|
|
queryTransactions: nextQueryTransactions,
|
|
queryKeys: getQueryKeys(nextQueries, state.datasourceInstance),
|
|
};
|
|
},
|
|
})
|
|
.addMapper({
|
|
filter: changeSizeAction,
|
|
mapper: (state, action): ExploreItemState => {
|
|
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 };
|
|
},
|
|
})
|
|
.addMapper({
|
|
filter: changeTimeAction,
|
|
mapper: (state, action): ExploreItemState => {
|
|
return { ...state, range: action.payload.range };
|
|
},
|
|
})
|
|
.addMapper({
|
|
filter: clearQueriesAction,
|
|
mapper: (state): ExploreItemState => {
|
|
const queries = ensureQueries();
|
|
return {
|
|
...state,
|
|
queries: queries.slice(),
|
|
queryTransactions: [],
|
|
showingStartPage: Boolean(state.StartPage),
|
|
queryKeys: getQueryKeys(queries, state.datasourceInstance),
|
|
};
|
|
},
|
|
})
|
|
.addMapper({
|
|
filter: highlightLogsExpressionAction,
|
|
mapper: (state, action): ExploreItemState => {
|
|
const { expressions } = action.payload;
|
|
return { ...state, logsHighlighterExpressions: expressions };
|
|
},
|
|
})
|
|
.addMapper({
|
|
filter: initializeExploreAction,
|
|
mapper: (state, action): ExploreItemState => {
|
|
const { containerWidth, eventBridge, exploreDatasources, queries, range, ui } = action.payload;
|
|
return {
|
|
...state,
|
|
containerWidth,
|
|
eventBridge,
|
|
exploreDatasources,
|
|
range,
|
|
queries,
|
|
initialized: true,
|
|
queryKeys: getQueryKeys(queries, state.datasourceInstance),
|
|
...ui,
|
|
update: makeInitialUpdateState(),
|
|
};
|
|
},
|
|
})
|
|
.addMapper({
|
|
filter: updateDatasourceInstanceAction,
|
|
mapper: (state, action): ExploreItemState => {
|
|
const { datasourceInstance } = action.payload;
|
|
return { ...state, datasourceInstance, queryKeys: getQueryKeys(state.queries, datasourceInstance) };
|
|
},
|
|
})
|
|
.addMapper({
|
|
filter: loadDatasourceFailureAction,
|
|
mapper: (state, action): ExploreItemState => {
|
|
return {
|
|
...state,
|
|
datasourceError: action.payload.error,
|
|
datasourceLoading: false,
|
|
update: makeInitialUpdateState(),
|
|
};
|
|
},
|
|
})
|
|
.addMapper({
|
|
filter: loadDatasourceMissingAction,
|
|
mapper: (state): ExploreItemState => {
|
|
return {
|
|
...state,
|
|
datasourceMissing: true,
|
|
datasourceLoading: false,
|
|
update: makeInitialUpdateState(),
|
|
};
|
|
},
|
|
})
|
|
.addMapper({
|
|
filter: loadDatasourcePendingAction,
|
|
mapper: (state, action): ExploreItemState => {
|
|
return { ...state, datasourceLoading: true, requestedDatasourceName: action.payload.requestedDatasourceName };
|
|
},
|
|
})
|
|
.addMapper({
|
|
filter: loadDatasourceSuccessAction,
|
|
mapper: (state, action): ExploreItemState => {
|
|
const { containerWidth, range } = state;
|
|
const {
|
|
StartPage,
|
|
datasourceInstance,
|
|
history,
|
|
showingStartPage,
|
|
supportsGraph,
|
|
supportsLogs,
|
|
supportsTable,
|
|
} = action.payload;
|
|
const queryIntervals = getIntervals(range, datasourceInstance.interval, containerWidth);
|
|
|
|
return {
|
|
...state,
|
|
queryIntervals,
|
|
StartPage,
|
|
datasourceInstance,
|
|
history,
|
|
showingStartPage,
|
|
supportsGraph,
|
|
supportsLogs,
|
|
supportsTable,
|
|
datasourceLoading: false,
|
|
datasourceMissing: false,
|
|
datasourceError: null,
|
|
logsHighlighterExpressions: undefined,
|
|
queryTransactions: [],
|
|
update: makeInitialUpdateState(),
|
|
};
|
|
},
|
|
})
|
|
.addMapper({
|
|
filter: modifyQueriesAction,
|
|
mapper: (state, action): ExploreItemState => {
|
|
const { queries, queryTransactions } = state;
|
|
const { modification, index, modifier } = action.payload;
|
|
let nextQueries: DataQuery[];
|
|
let nextQueryTransactions: QueryTransaction[];
|
|
if (index === undefined) {
|
|
// Modify all queries
|
|
nextQueries = queries.map((query, i) => ({
|
|
...modifier({ ...query }, modification),
|
|
...generateEmptyQuery(state.queries),
|
|
}));
|
|
// Discard all ongoing transactions
|
|
nextQueryTransactions = [];
|
|
} else {
|
|
// Modify query only at index
|
|
nextQueries = queries.map((query, i) => {
|
|
// Synchronize all queries with local query cache to ensure consistency
|
|
// TODO still needed?
|
|
return i === index
|
|
? { ...modifier({ ...query }, modification), ...generateEmptyQuery(state.queries) }
|
|
: 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,
|
|
queries: nextQueries,
|
|
queryKeys: getQueryKeys(nextQueries, state.datasourceInstance),
|
|
queryTransactions: nextQueryTransactions,
|
|
};
|
|
},
|
|
})
|
|
.addMapper({
|
|
filter: queryTransactionFailureAction,
|
|
mapper: (state, action): ExploreItemState => {
|
|
const { queryTransactions } = action.payload;
|
|
return {
|
|
...state,
|
|
queryTransactions,
|
|
showingStartPage: false,
|
|
update: makeInitialUpdateState(),
|
|
};
|
|
},
|
|
})
|
|
.addMapper({
|
|
filter: queryTransactionStartAction,
|
|
mapper: (state, action): ExploreItemState => {
|
|
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,
|
|
update: makeInitialUpdateState(),
|
|
};
|
|
},
|
|
})
|
|
.addMapper({
|
|
filter: queryTransactionSuccessAction,
|
|
mapper: (state, action): ExploreItemState => {
|
|
const { datasourceInstance, queryIntervals } = state;
|
|
const { history, queryTransactions } = action.payload;
|
|
const results = calculateResultsFromQueryTransactions(
|
|
queryTransactions,
|
|
datasourceInstance,
|
|
queryIntervals.intervalMs
|
|
);
|
|
|
|
return {
|
|
...state,
|
|
...results,
|
|
history,
|
|
queryTransactions,
|
|
showingStartPage: false,
|
|
update: makeInitialUpdateState(),
|
|
};
|
|
},
|
|
})
|
|
.addMapper({
|
|
filter: removeQueryRowAction,
|
|
mapper: (state, action): ExploreItemState => {
|
|
const { datasourceInstance, queries, queryIntervals, queryTransactions, queryKeys } = state;
|
|
const { index } = action.payload;
|
|
|
|
if (queries.length <= 1) {
|
|
return state;
|
|
}
|
|
|
|
const nextQueries = [...queries.slice(0, index), ...queries.slice(index + 1)];
|
|
const nextQueryKeys = [...queryKeys.slice(0, index), ...queryKeys.slice(index + 1)];
|
|
|
|
// Discard transactions related to row query
|
|
const nextQueryTransactions = queryTransactions.filter(qt => nextQueries.some(nq => nq.key === qt.query.key));
|
|
const results = calculateResultsFromQueryTransactions(
|
|
nextQueryTransactions,
|
|
datasourceInstance,
|
|
queryIntervals.intervalMs
|
|
);
|
|
|
|
return {
|
|
...state,
|
|
...results,
|
|
queries: nextQueries,
|
|
logsHighlighterExpressions: undefined,
|
|
queryTransactions: nextQueryTransactions,
|
|
queryKeys: nextQueryKeys,
|
|
};
|
|
},
|
|
})
|
|
.addMapper({
|
|
filter: scanRangeAction,
|
|
mapper: (state, action): ExploreItemState => {
|
|
return { ...state, scanRange: action.payload.range };
|
|
},
|
|
})
|
|
.addMapper({
|
|
filter: scanStartAction,
|
|
mapper: (state, action): ExploreItemState => {
|
|
return { ...state, scanning: true, scanner: action.payload.scanner };
|
|
},
|
|
})
|
|
.addMapper({
|
|
filter: scanStopAction,
|
|
mapper: (state): ExploreItemState => {
|
|
const { queryTransactions } = state;
|
|
const nextQueryTransactions = queryTransactions.filter(qt => qt.scanning && !qt.done);
|
|
return {
|
|
...state,
|
|
queryTransactions: nextQueryTransactions,
|
|
scanning: false,
|
|
scanRange: undefined,
|
|
scanner: undefined,
|
|
update: makeInitialUpdateState(),
|
|
};
|
|
},
|
|
})
|
|
.addMapper({
|
|
filter: setQueriesAction,
|
|
mapper: (state, action): ExploreItemState => {
|
|
const { queries } = action.payload;
|
|
return {
|
|
...state,
|
|
queries: queries.slice(),
|
|
queryKeys: getQueryKeys(queries, state.datasourceInstance),
|
|
};
|
|
},
|
|
})
|
|
.addMapper({
|
|
filter: updateUIStateAction,
|
|
mapper: (state, action): ExploreItemState => {
|
|
return { ...state, ...action.payload };
|
|
},
|
|
})
|
|
.addMapper({
|
|
filter: toggleGraphAction,
|
|
mapper: (state): ExploreItemState => {
|
|
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 };
|
|
},
|
|
})
|
|
.addMapper({
|
|
filter: toggleLogsAction,
|
|
mapper: (state): ExploreItemState => {
|
|
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 };
|
|
},
|
|
})
|
|
.addMapper({
|
|
filter: toggleTableAction,
|
|
mapper: (state): ExploreItemState => {
|
|
const showingTable = !state.showingTable;
|
|
if (showingTable) {
|
|
return { ...state, 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 };
|
|
},
|
|
})
|
|
.addMapper({
|
|
filter: queriesImportedAction,
|
|
mapper: (state, action): ExploreItemState => {
|
|
const { queries } = action.payload;
|
|
return {
|
|
...state,
|
|
queries,
|
|
queryKeys: getQueryKeys(queries, state.datasourceInstance),
|
|
};
|
|
},
|
|
})
|
|
.addMapper({
|
|
filter: toggleLogLevelAction,
|
|
mapper: (state, action): ExploreItemState => {
|
|
const { hiddenLogLevels } = action.payload;
|
|
return {
|
|
...state,
|
|
hiddenLogLevels: Array.from(hiddenLogLevels),
|
|
};
|
|
},
|
|
})
|
|
.create();
|
|
|
|
export const updateChildRefreshState = (
|
|
state: Readonly<ExploreItemState>,
|
|
payload: LocationUpdate,
|
|
exploreId: ExploreId
|
|
): ExploreItemState => {
|
|
const path = payload.path || '';
|
|
const queryState = payload.query[exploreId] as string;
|
|
if (!queryState) {
|
|
return state;
|
|
}
|
|
|
|
const urlState = parseUrlState(queryState);
|
|
if (!state.urlState || path !== '/explore') {
|
|
// we only want to refresh when browser back/forward
|
|
return { ...state, urlState, update: { datasource: false, queries: false, range: false, ui: false } };
|
|
}
|
|
|
|
const datasource = _.isEqual(urlState ? urlState.datasource : '', state.urlState.datasource) === false;
|
|
const queries = _.isEqual(urlState ? urlState.queries : [], state.urlState.queries) === false;
|
|
const range = _.isEqual(urlState ? urlState.range : DEFAULT_RANGE, state.urlState.range) === false;
|
|
const ui = _.isEqual(urlState ? urlState.ui : DEFAULT_UI_STATE, state.urlState.ui) === false;
|
|
|
|
return {
|
|
...state,
|
|
urlState,
|
|
update: {
|
|
...state.update,
|
|
datasource,
|
|
queries,
|
|
range,
|
|
ui,
|
|
},
|
|
};
|
|
};
|
|
|
|
/**
|
|
* 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: HigherOrderAction): ExploreState => {
|
|
switch (action.type) {
|
|
case splitCloseAction.type: {
|
|
const { itemId } = action.payload as SplitCloseActionPayload;
|
|
const targetSplit = {
|
|
left: itemId === ExploreId.left ? state.right : state.left,
|
|
right: initialExploreState.right,
|
|
};
|
|
return {
|
|
...state,
|
|
...targetSplit,
|
|
split: false,
|
|
};
|
|
}
|
|
|
|
case ActionTypes.SplitOpen: {
|
|
return { ...state, split: true, right: { ...action.payload.itemState } };
|
|
}
|
|
|
|
case ActionTypes.ResetExplore: {
|
|
return initialExploreState;
|
|
}
|
|
|
|
case updateLocation.type: {
|
|
const { query } = action.payload;
|
|
if (!query || !query[ExploreId.left]) {
|
|
return state;
|
|
}
|
|
|
|
const split = query[ExploreId.right] ? true : false;
|
|
const leftState = state[ExploreId.left];
|
|
const rightState = state[ExploreId.right];
|
|
|
|
return {
|
|
...state,
|
|
split,
|
|
[ExploreId.left]: updateChildRefreshState(leftState, action.payload, ExploreId.left),
|
|
[ExploreId.right]: updateChildRefreshState(rightState, action.payload, ExploreId.right),
|
|
};
|
|
}
|
|
}
|
|
|
|
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,
|
|
};
|