mirror of
https://github.com/grafana/grafana.git
synced 2025-02-13 00:55:47 -06:00
* WIP: intial commit * Switch: Adds tooltip * Refactor: Adds props to LogsPanelEditor * Refactor: Moves LogRowContextProvider to grafana/ui * Refactor: Moves LogRowContext and Alert to grafana/ui * Refactor: Moves LogLabelStats to grafana/ui * Refactor: Moves LogLabels and LogLabel to grafana/ui * Refactor: Moves LogMessageAnsi and ansicolor to grafana/ui * Refactor: Moves calculateFieldStats, LogsParsers and getParser to grafana/data * Refactor: Moves findHighlightChunksInText to grafana/data * Refactor: Moves LogRow to grafana/ui * Refactor: Moving ExploreGraphPanel to grafana/ui * Refactor: Copies Logs to grafana/ui * Refactor: Moves ToggleButtonGroup to grafana/ui * Refactor: Adds Logs to LogsPanel * Refactor: Moves styles to emotion * Feature: Adds LogsRows * Refactor: Introduces render limit * Styles: Moves styles to emotion * Styles: Moves styles to emotion * Styles: Moves styles to emotion * Styles: Moves styles to emotion * Refactor: Adds sorting to LogsPanelEditor * Tests: Adds tests for sorting * Refactor: Changes according to PR comments * Refactor: Changes according to PR comments * Refactor: Moves Logs and ExploreGraphPanel out of grafana/ui * Fix: Shows the Show context label again
684 lines
18 KiB
TypeScript
684 lines
18 KiB
TypeScript
import _ from 'lodash';
|
|
import {
|
|
getIntervals,
|
|
ensureQueries,
|
|
getQueryKeys,
|
|
parseUrlState,
|
|
DEFAULT_UI_STATE,
|
|
generateNewKeyAndAddRefIdIfMissing,
|
|
sortLogsResult,
|
|
refreshIntervalToSortOrder,
|
|
} from 'app/core/utils/explore';
|
|
import { ExploreItemState, ExploreState, ExploreId, ExploreUpdateState, ExploreMode } from 'app/types/explore';
|
|
import { LoadingState } from '@grafana/data';
|
|
import { DataQuery } from '@grafana/ui';
|
|
import {
|
|
HigherOrderAction,
|
|
ActionTypes,
|
|
testDataSourcePendingAction,
|
|
testDataSourceSuccessAction,
|
|
testDataSourceFailureAction,
|
|
splitCloseAction,
|
|
SplitCloseActionPayload,
|
|
loadExploreDatasources,
|
|
historyUpdatedAction,
|
|
changeModeAction,
|
|
queryFailureAction,
|
|
setUrlReplacedAction,
|
|
querySuccessAction,
|
|
scanStopAction,
|
|
resetQueryErrorAction,
|
|
queryStartAction,
|
|
runQueriesAction,
|
|
changeRangeAction,
|
|
} from './actionTypes';
|
|
import { reducerFactory } from 'app/core/redux';
|
|
import {
|
|
addQueryRowAction,
|
|
changeQueryAction,
|
|
changeSizeAction,
|
|
changeRefreshIntervalAction,
|
|
clearQueriesAction,
|
|
highlightLogsExpressionAction,
|
|
initializeExploreAction,
|
|
updateDatasourceInstanceAction,
|
|
loadDatasourceMissingAction,
|
|
loadDatasourcePendingAction,
|
|
loadDatasourceReadyAction,
|
|
modifyQueriesAction,
|
|
removeQueryRowAction,
|
|
scanStartAction,
|
|
setQueriesAction,
|
|
toggleTableAction,
|
|
queriesImportedAction,
|
|
updateUIStateAction,
|
|
toggleLogLevelAction,
|
|
} from './actionTypes';
|
|
import { updateLocation } from 'app/core/actions/location';
|
|
import { LocationUpdate } from '@grafana/runtime';
|
|
import TableModel from 'app/core/table_model';
|
|
import { isLive } from '@grafana/ui/src/components/RefreshPicker/RefreshPicker';
|
|
|
|
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,
|
|
mode: 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,
|
|
queryIntervals: { interval: '15s', intervalMs: DEFAULT_GRAPH_INTERVAL },
|
|
range: {
|
|
from: null,
|
|
to: null,
|
|
raw: DEFAULT_RANGE,
|
|
},
|
|
absoluteRange: {
|
|
from: null,
|
|
to: null,
|
|
},
|
|
scanning: false,
|
|
scanRange: null,
|
|
showingGraph: true,
|
|
showingTable: true,
|
|
loadingState: LoadingState.NotStarted,
|
|
queryKeys: [],
|
|
urlState: null,
|
|
update: makeInitialUpdateState(),
|
|
queryErrors: [],
|
|
latency: 0,
|
|
supportedModes: [],
|
|
mode: null,
|
|
isLive: false,
|
|
urlReplaced: false,
|
|
});
|
|
|
|
/**
|
|
* Global Explore state that handles multiple Explore areas and the split state
|
|
*/
|
|
export const initialExploreItemState = makeExploreItemState();
|
|
export const initialExploreState: ExploreState = {
|
|
split: null,
|
|
left: initialExploreItemState,
|
|
right: initialExploreItemState,
|
|
};
|
|
|
|
/**
|
|
* 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 } = 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)];
|
|
|
|
return {
|
|
...state,
|
|
queries: nextQueries,
|
|
logsHighlighterExpressions: undefined,
|
|
queryKeys: getQueryKeys(nextQueries, state.datasourceInstance),
|
|
};
|
|
},
|
|
})
|
|
.addMapper({
|
|
filter: changeQueryAction,
|
|
mapper: (state, action): ExploreItemState => {
|
|
const { queries } = state;
|
|
const { query, index } = action.payload;
|
|
|
|
// Override path: queries are completely reset
|
|
const nextQuery: DataQuery = generateNewKeyAndAddRefIdIfMissing(query, queries, index);
|
|
const nextQueries = [...queries];
|
|
nextQueries[index] = nextQuery;
|
|
|
|
return {
|
|
...state,
|
|
queries: nextQueries,
|
|
queryKeys: getQueryKeys(nextQueries, state.datasourceInstance),
|
|
};
|
|
},
|
|
})
|
|
.addMapper({
|
|
filter: changeSizeAction,
|
|
mapper: (state, action): ExploreItemState => {
|
|
const containerWidth = action.payload.width;
|
|
return { ...state, containerWidth };
|
|
},
|
|
})
|
|
.addMapper({
|
|
filter: changeModeAction,
|
|
mapper: (state, action): ExploreItemState => {
|
|
const mode = action.payload.mode;
|
|
return { ...state, mode };
|
|
},
|
|
})
|
|
.addMapper({
|
|
filter: changeRefreshIntervalAction,
|
|
mapper: (state, action): ExploreItemState => {
|
|
const { refreshInterval } = action.payload;
|
|
const live = isLive(refreshInterval);
|
|
const sortOrder = refreshIntervalToSortOrder(refreshInterval);
|
|
const logsResult = sortLogsResult(state.logsResult, sortOrder);
|
|
|
|
return {
|
|
...state,
|
|
refreshInterval,
|
|
loadingState: live ? LoadingState.Streaming : LoadingState.NotStarted,
|
|
isLive: live,
|
|
logsResult,
|
|
};
|
|
},
|
|
})
|
|
.addMapper({
|
|
filter: clearQueriesAction,
|
|
mapper: (state): ExploreItemState => {
|
|
const queries = ensureQueries();
|
|
return {
|
|
...state,
|
|
queries: queries.slice(),
|
|
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, queries, range, mode, ui } = action.payload;
|
|
return {
|
|
...state,
|
|
containerWidth,
|
|
eventBridge,
|
|
range,
|
|
mode,
|
|
queries,
|
|
initialized: true,
|
|
queryKeys: getQueryKeys(queries, state.datasourceInstance),
|
|
...ui,
|
|
update: makeInitialUpdateState(),
|
|
};
|
|
},
|
|
})
|
|
.addMapper({
|
|
filter: updateDatasourceInstanceAction,
|
|
mapper: (state, action): ExploreItemState => {
|
|
const { datasourceInstance } = action.payload;
|
|
// Capabilities
|
|
const supportsGraph = datasourceInstance.meta.metrics;
|
|
const supportsLogs = datasourceInstance.meta.logs;
|
|
|
|
let mode = state.mode || ExploreMode.Metrics;
|
|
const supportedModes: ExploreMode[] = [];
|
|
|
|
if (supportsGraph) {
|
|
supportedModes.push(ExploreMode.Metrics);
|
|
}
|
|
|
|
if (supportsLogs) {
|
|
supportedModes.push(ExploreMode.Logs);
|
|
}
|
|
|
|
if (supportedModes.length === 1) {
|
|
mode = supportedModes[0];
|
|
}
|
|
|
|
// Custom components
|
|
const StartPage = datasourceInstance.components.ExploreStartPage;
|
|
|
|
return {
|
|
...state,
|
|
datasourceInstance,
|
|
queryErrors: [],
|
|
latency: 0,
|
|
loadingState: LoadingState.NotStarted,
|
|
StartPage,
|
|
showingStartPage: Boolean(StartPage),
|
|
queryKeys: [],
|
|
supportedModes,
|
|
mode,
|
|
};
|
|
},
|
|
})
|
|
.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: loadDatasourceReadyAction,
|
|
mapper: (state, action): ExploreItemState => {
|
|
const { history } = action.payload;
|
|
return {
|
|
...state,
|
|
history,
|
|
datasourceLoading: false,
|
|
datasourceMissing: false,
|
|
logsHighlighterExpressions: undefined,
|
|
update: makeInitialUpdateState(),
|
|
};
|
|
},
|
|
})
|
|
.addMapper({
|
|
filter: modifyQueriesAction,
|
|
mapper: (state, action): ExploreItemState => {
|
|
const { queries } = state;
|
|
const { modification, index, modifier } = action.payload;
|
|
let nextQueries: DataQuery[];
|
|
if (index === undefined) {
|
|
// Modify all queries
|
|
nextQueries = queries.map((query, i) => {
|
|
const nextQuery = modifier({ ...query }, modification);
|
|
return generateNewKeyAndAddRefIdIfMissing(nextQuery, queries, i);
|
|
});
|
|
} else {
|
|
// Modify query only at index
|
|
nextQueries = queries.map((query, i) => {
|
|
if (i === index) {
|
|
const nextQuery = modifier({ ...query }, modification);
|
|
return generateNewKeyAndAddRefIdIfMissing(nextQuery, queries, i);
|
|
}
|
|
|
|
return query;
|
|
});
|
|
}
|
|
return {
|
|
...state,
|
|
queries: nextQueries,
|
|
queryKeys: getQueryKeys(nextQueries, state.datasourceInstance),
|
|
};
|
|
},
|
|
})
|
|
.addMapper({
|
|
filter: queryFailureAction,
|
|
mapper: (state, action): ExploreItemState => {
|
|
const { response } = action.payload;
|
|
const queryErrors = state.queryErrors.concat(response);
|
|
|
|
return {
|
|
...state,
|
|
graphResult: null,
|
|
tableResult: null,
|
|
logsResult: null,
|
|
latency: 0,
|
|
queryErrors,
|
|
loadingState: LoadingState.Error,
|
|
update: makeInitialUpdateState(),
|
|
};
|
|
},
|
|
})
|
|
.addMapper({
|
|
filter: queryStartAction,
|
|
mapper: (state): ExploreItemState => {
|
|
return {
|
|
...state,
|
|
queryErrors: [],
|
|
latency: 0,
|
|
loadingState: LoadingState.Loading,
|
|
update: makeInitialUpdateState(),
|
|
};
|
|
},
|
|
})
|
|
.addMapper({
|
|
filter: querySuccessAction,
|
|
mapper: (state, action): ExploreItemState => {
|
|
const { latency, loadingState, graphResult, tableResult, logsResult } = action.payload;
|
|
|
|
return {
|
|
...state,
|
|
loadingState,
|
|
graphResult,
|
|
tableResult,
|
|
logsResult,
|
|
latency,
|
|
showingStartPage: false,
|
|
update: makeInitialUpdateState(),
|
|
};
|
|
},
|
|
})
|
|
.addMapper({
|
|
filter: removeQueryRowAction,
|
|
mapper: (state, action): ExploreItemState => {
|
|
const { queries, 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)];
|
|
|
|
return {
|
|
...state,
|
|
queries: nextQueries,
|
|
logsHighlighterExpressions: undefined,
|
|
queryKeys: nextQueryKeys,
|
|
};
|
|
},
|
|
})
|
|
.addMapper({
|
|
filter: scanStartAction,
|
|
mapper: (state, action): ExploreItemState => {
|
|
return { ...state, scanning: true };
|
|
},
|
|
})
|
|
.addMapper({
|
|
filter: scanStopAction,
|
|
mapper: (state): ExploreItemState => {
|
|
return {
|
|
...state,
|
|
scanning: false,
|
|
scanRange: 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: toggleTableAction,
|
|
mapper: (state): ExploreItemState => {
|
|
const showingTable = !state.showingTable;
|
|
if (showingTable) {
|
|
return { ...state };
|
|
}
|
|
|
|
return { ...state, tableResult: new TableModel() };
|
|
},
|
|
})
|
|
.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),
|
|
};
|
|
},
|
|
})
|
|
.addMapper({
|
|
filter: testDataSourcePendingAction,
|
|
mapper: (state): ExploreItemState => {
|
|
return {
|
|
...state,
|
|
datasourceError: null,
|
|
};
|
|
},
|
|
})
|
|
.addMapper({
|
|
filter: testDataSourceSuccessAction,
|
|
mapper: (state): ExploreItemState => {
|
|
return {
|
|
...state,
|
|
datasourceError: null,
|
|
};
|
|
},
|
|
})
|
|
.addMapper({
|
|
filter: testDataSourceFailureAction,
|
|
mapper: (state, action): ExploreItemState => {
|
|
return {
|
|
...state,
|
|
datasourceError: action.payload.error,
|
|
graphResult: undefined,
|
|
tableResult: undefined,
|
|
logsResult: undefined,
|
|
update: makeInitialUpdateState(),
|
|
};
|
|
},
|
|
})
|
|
.addMapper({
|
|
filter: loadExploreDatasources,
|
|
mapper: (state, action): ExploreItemState => {
|
|
return {
|
|
...state,
|
|
exploreDatasources: action.payload.exploreDatasources,
|
|
};
|
|
},
|
|
})
|
|
.addMapper({
|
|
filter: runQueriesAction,
|
|
mapper: (state): ExploreItemState => {
|
|
const { range } = state;
|
|
const { datasourceInstance, containerWidth } = state;
|
|
let interval = '1s';
|
|
if (datasourceInstance && datasourceInstance.interval) {
|
|
interval = datasourceInstance.interval;
|
|
}
|
|
const queryIntervals = getIntervals(range, interval, containerWidth);
|
|
return {
|
|
...state,
|
|
range,
|
|
queryIntervals,
|
|
showingStartPage: false,
|
|
};
|
|
},
|
|
})
|
|
.addMapper({
|
|
filter: historyUpdatedAction,
|
|
mapper: (state, action): ExploreItemState => {
|
|
return {
|
|
...state,
|
|
history: action.payload.history,
|
|
};
|
|
},
|
|
})
|
|
.addMapper({
|
|
filter: resetQueryErrorAction,
|
|
mapper: (state, action): ExploreItemState => {
|
|
const { refIds } = action.payload;
|
|
const queryErrors = state.queryErrors.reduce((allErrors, error) => {
|
|
if (error.refId && refIds.indexOf(error.refId) !== -1) {
|
|
return allErrors;
|
|
}
|
|
|
|
return allErrors.concat(error);
|
|
}, []);
|
|
|
|
return {
|
|
...state,
|
|
queryErrors,
|
|
};
|
|
},
|
|
})
|
|
.addMapper({
|
|
filter: setUrlReplacedAction,
|
|
mapper: (state): ExploreItemState => {
|
|
return {
|
|
...state,
|
|
urlReplaced: true,
|
|
};
|
|
},
|
|
})
|
|
.addMapper({
|
|
filter: changeRangeAction,
|
|
mapper: (state, action): ExploreItemState => {
|
|
const { range, absoluteRange } = action.payload;
|
|
return {
|
|
...state,
|
|
range,
|
|
absoluteRange,
|
|
};
|
|
},
|
|
})
|
|
.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, mode: 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 mode = _.isEqual(urlState ? urlState.mode : ExploreMode.Metrics, state.urlState.mode) === false;
|
|
const ui = _.isEqual(urlState ? urlState.ui : DEFAULT_UI_STATE, state.urlState.ui) === false;
|
|
|
|
return {
|
|
...state,
|
|
urlState,
|
|
update: {
|
|
...state.update,
|
|
datasource,
|
|
queries,
|
|
range,
|
|
mode,
|
|
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) {
|
|
// @ts-ignore
|
|
const exploreItemState = state[exploreId];
|
|
return { ...state, [exploreId]: itemReducer(exploreItemState, action) };
|
|
}
|
|
}
|
|
|
|
return state;
|
|
};
|
|
|
|
export default {
|
|
explore: exploreReducer,
|
|
};
|