diff --git a/public/app/core/utils/explore.test.ts b/public/app/core/utils/explore.test.ts index 1c00142c3b8..dae6554ade0 100644 --- a/public/app/core/utils/explore.test.ts +++ b/public/app/core/utils/explore.test.ts @@ -8,6 +8,7 @@ import { } from './explore'; import { ExploreUrlState } from 'app/types/explore'; import store from 'app/core/store'; +import { LogsDedupStrategy } from 'app/core/logs_model'; const DEFAULT_EXPLORE_STATE: ExploreUrlState = { datasource: null, @@ -17,6 +18,7 @@ const DEFAULT_EXPLORE_STATE: ExploreUrlState = { showingGraph: true, showingTable: true, showingLogs: true, + dedupStrategy: LogsDedupStrategy.none, } }; @@ -78,7 +80,7 @@ describe('state functions', () => { expect(serializeStateToUrlParam(state)).toBe( '{"datasource":"foo","queries":[{"expr":"metric{test=\\"a/b\\"}"},' + '{"expr":"super{foo=\\"x/z\\"}"}],"range":{"from":"now-5h","to":"now"},' + - '"ui":{"showingGraph":true,"showingTable":true,"showingLogs":true}}' + '"ui":{"showingGraph":true,"showingTable":true,"showingLogs":true,"dedupStrategy":"none"}}' ); }); @@ -100,7 +102,7 @@ describe('state functions', () => { }, }; expect(serializeStateToUrlParam(state, true)).toBe( - '["now-5h","now","foo",{"expr":"metric{test=\\"a/b\\"}"},{"expr":"super{foo=\\"x/z\\"}"},{"ui":[true,true,true]}]' + '["now-5h","now","foo",{"expr":"metric{test=\\"a/b\\"}"},{"expr":"super{foo=\\"x/z\\"}"},{"ui":[true,true,true,"none"]}]' ); }); }); diff --git a/public/app/core/utils/explore.ts b/public/app/core/utils/explore.ts index 107f411353c..1dcd66c6369 100644 --- a/public/app/core/utils/explore.ts +++ b/public/app/core/utils/explore.ts @@ -21,6 +21,7 @@ import { QueryIntervals, QueryOptions, } from 'app/types/explore'; +import { LogsDedupStrategy } from 'app/core/logs_model'; export const DEFAULT_RANGE = { from: 'now-6h', @@ -31,6 +32,7 @@ export const DEFAULT_UI_STATE = { showingTable: true, showingGraph: true, showingLogs: true, + dedupStrategy: LogsDedupStrategy.none, }; const MAX_HISTORY_ITEMS = 100; @@ -183,6 +185,7 @@ export function parseUrlState(initial: string | undefined): ExploreUrlState { showingGraph: segment.ui[0], showingLogs: segment.ui[1], showingTable: segment.ui[2], + dedupStrategy: segment.ui[3], }; } }); @@ -204,7 +207,7 @@ export function serializeStateToUrlParam(urlState: ExploreUrlState, compact?: bo urlState.range.to, urlState.datasource, ...urlState.queries, - { ui: [!!urlState.ui.showingGraph, !!urlState.ui.showingLogs, !!urlState.ui.showingTable] }, + { ui: [!!urlState.ui.showingGraph, !!urlState.ui.showingLogs, !!urlState.ui.showingTable, urlState.ui.dedupStrategy] }, ]); } return JSON.stringify(urlState); diff --git a/public/app/features/explore/GraphContainer.tsx b/public/app/features/explore/GraphContainer.tsx index 3950d89c11f..92aac41367c 100644 --- a/public/app/features/explore/GraphContainer.tsx +++ b/public/app/features/explore/GraphContainer.tsx @@ -25,7 +25,7 @@ interface GraphContainerProps { export class GraphContainer extends PureComponent { onClickGraphButton = () => { - this.props.toggleGraph(this.props.exploreId); + this.props.toggleGraph(this.props.exploreId, this.props.showingGraph); }; onChangeTime = (timeRange: TimeRange) => { diff --git a/public/app/features/explore/Logs.tsx b/public/app/features/explore/Logs.tsx index 1fde869d27e..f41555b9121 100644 --- a/public/app/features/explore/Logs.tsx +++ b/public/app/features/explore/Logs.tsx @@ -58,14 +58,15 @@ interface Props { range?: RawTimeRange; scanning?: boolean; scanRange?: RawTimeRange; + dedupStrategy: LogsDedupStrategy; onChangeTime?: (range: RawTimeRange) => void; onClickLabel?: (label: string, value: string) => void; onStartScanning?: () => void; onStopScanning?: () => void; + onDedupStrategyChange: (dedupStrategy: LogsDedupStrategy) => void; } interface State { - dedup: LogsDedupStrategy; deferLogs: boolean; hiddenLogLevels: Set; renderAll: boolean; @@ -79,7 +80,6 @@ export default class Logs extends PureComponent { renderAllTimer: NodeJS.Timer; state = { - dedup: LogsDedupStrategy.none, deferLogs: true, hiddenLogLevels: new Set(), renderAll: false, @@ -112,12 +112,11 @@ export default class Logs extends PureComponent { } onChangeDedup = (dedup: LogsDedupStrategy) => { - this.setState(prevState => { - if (prevState.dedup === dedup) { - return { dedup: LogsDedupStrategy.none }; - } - return { dedup }; - }); + const { onDedupStrategyChange } = this.props; + if (this.props.dedupStrategy === dedup) { + return onDedupStrategyChange(LogsDedupStrategy.none); + } + return onDedupStrategyChange(dedup); }; onChangeLabels = (event: React.SyntheticEvent) => { @@ -173,17 +172,19 @@ export default class Logs extends PureComponent { return null; } - const { dedup, deferLogs, hiddenLogLevels, renderAll, showLocalTime, showUtc } = this.state; + const { deferLogs, hiddenLogLevels, renderAll, showLocalTime, showUtc, } = this.state; let { showLabels } = this.state; + const { dedupStrategy } = this.props; const hasData = data && data.rows && data.rows.length > 0; - const showDuplicates = dedup !== LogsDedupStrategy.none; + const showDuplicates = dedupStrategy !== LogsDedupStrategy.none; // Filtering const filteredData = filterLogLevels(data, hiddenLogLevels); - const dedupedData = dedupLogRows(filteredData, dedup); + const dedupedData = dedupLogRows(filteredData, dedupStrategy); const dedupCount = dedupedData.rows.reduce((sum, row) => sum + row.duplicates, 0); const meta = [...data.meta]; - if (dedup !== LogsDedupStrategy.none) { + + if (dedupStrategy !== LogsDedupStrategy.none) { meta.push({ label: 'Dedup count', value: dedupCount, @@ -236,7 +237,7 @@ export default class Logs extends PureComponent { key={i} value={dedupType} onChange={this.onChangeDedup} - selected={dedup === dedupType} + selected={dedupStrategy === dedupType} tooltip={LogsDedupDescription[dedupType]} > {dedupType} diff --git a/public/app/features/explore/LogsContainer.tsx b/public/app/features/explore/LogsContainer.tsx index fbb0597d2db..190c1c43b5a 100644 --- a/public/app/features/explore/LogsContainer.tsx +++ b/public/app/features/explore/LogsContainer.tsx @@ -4,10 +4,10 @@ import { connect } from 'react-redux'; import { RawTimeRange, TimeRange } from '@grafana/ui'; import { ExploreId, ExploreItemState } from 'app/types/explore'; -import { LogsModel } from 'app/core/logs_model'; +import { LogsModel, LogsDedupStrategy } from 'app/core/logs_model'; import { StoreState } from 'app/types'; -import { toggleLogs } from './state/actions'; +import { toggleLogs, changeDedupStrategy } from './state/actions'; import Logs from './Logs'; import Panel from './Panel'; @@ -25,12 +25,18 @@ interface LogsContainerProps { scanRange?: RawTimeRange; showingLogs: boolean; toggleLogs: typeof toggleLogs; + changeDedupStrategy: typeof changeDedupStrategy; + dedupStrategy: LogsDedupStrategy; width: number; } export class LogsContainer extends PureComponent { onClickLogsButton = () => { - this.props.toggleLogs(this.props.exploreId); + this.props.toggleLogs(this.props.exploreId, this.props.showingLogs); + }; + + handleDedupStrategyChange = (dedupStrategy: LogsDedupStrategy) => { + this.props.changeDedupStrategy(this.props.exploreId, dedupStrategy); }; render() { @@ -53,6 +59,7 @@ export class LogsContainer extends PureComponent { return ( { onClickLabel={onClickLabel} onStartScanning={onStartScanning} onStopScanning={onStopScanning} + onDedupStrategyChange={this.handleDedupStrategyChange} range={range} scanning={scanning} scanRange={scanRange} @@ -72,11 +80,23 @@ export class LogsContainer extends PureComponent { } } +const selectItemUIState = (itemState: ExploreItemState) => { + const { showingGraph, showingLogs, showingTable, showingStartPage, dedupStrategy } = itemState; + return { + showingGraph, + showingLogs, + showingTable, + showingStartPage, + dedupStrategy, + }; +}; function mapStateToProps(state: StoreState, { exploreId }) { const explore = state.explore; const item: ExploreItemState = explore[exploreId]; - const { logsHighlighterExpressions, logsResult, queryTransactions, scanning, scanRange, showingLogs, range } = item; + const { logsHighlighterExpressions, logsResult, queryTransactions, scanning, scanRange, range } = item; const loading = queryTransactions.some(qt => qt.resultType === 'Logs' && !qt.done); + const {showingLogs, dedupStrategy} = selectItemUIState(item); + return { loading, logsHighlighterExpressions, @@ -85,11 +105,13 @@ function mapStateToProps(state: StoreState, { exploreId }) { scanRange, showingLogs, range, + dedupStrategy, }; } const mapDispatchToProps = { toggleLogs, + changeDedupStrategy, }; export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(LogsContainer)); diff --git a/public/app/features/explore/TableContainer.tsx b/public/app/features/explore/TableContainer.tsx index f386e5ab99b..e41d4a1eecb 100644 --- a/public/app/features/explore/TableContainer.tsx +++ b/public/app/features/explore/TableContainer.tsx @@ -21,7 +21,7 @@ interface TableContainerProps { export class TableContainer extends PureComponent { onClickTableButton = () => { - this.props.toggleTable(this.props.exploreId); + this.props.toggleTable(this.props.exploreId, this.props.showingTable); }; render() { diff --git a/public/app/features/explore/state/actionTypes.ts b/public/app/features/explore/state/actionTypes.ts index 98af5e8076e..d54a8754c3d 100644 --- a/public/app/features/explore/state/actionTypes.ts +++ b/public/app/features/explore/state/actionTypes.ts @@ -192,6 +192,10 @@ export interface ToggleLogsPayload { exploreId: ExploreId; } +export interface UpdateUIStatePayload extends Partial{ + exploreId: ExploreId; +} + export interface UpdateDatasourceInstancePayload { exploreId: ExploreId; datasourceInstance: DataSourceApi; @@ -366,6 +370,11 @@ export const splitCloseAction = noPayloadActionCreatorFactory('explore/SPLIT_CLO export const splitOpenAction = actionCreatorFactory('explore/SPLIT_OPEN').create(); export const stateSaveAction = noPayloadActionCreatorFactory('explore/STATE_SAVE').create(); +/** + * Update state of Explores UI elements (panels visiblity and deduplication strategy) + */ +export const updateUIStateAction = actionCreatorFactory('explore/UPDATE_UI_STATE').create(); + /** * Expand/collapse the table result viewer. When collapsed, table queries won't be run. */ diff --git a/public/app/features/explore/state/actions.ts b/public/app/features/explore/state/actions.ts index f6fa5c05d63..b84a0534836 100644 --- a/public/app/features/explore/state/actions.ts +++ b/public/app/features/explore/state/actions.ts @@ -67,14 +67,26 @@ import { ToggleGraphPayload, ToggleLogsPayload, ToggleTablePayload, + updateUIStateAction, } from './actionTypes'; import { ActionOf, ActionCreator } from 'app/core/redux/actionCreatorFactory'; +import { LogsDedupStrategy } from 'app/core/logs_model'; type ThunkResult = ThunkAction; -// /** -// * Adds a query row after the row with the given index. -// */ +/** + * Updates UI state and save it to the URL + */ +const updateExploreUIState = (exploreId, uiStateFragment: Partial) => { + return dispatch => { + dispatch(updateUIStateAction({ exploreId, ...uiStateFragment })); + dispatch(stateSave()); + }; +}; + +/** + * Adds a query row after the row with the given index. + */ export function addQueryRow(exploreId: ExploreId, index: number): ActionOf { const query = generateEmptyQuery(index + 1); return addQueryRowAction({ exploreId, index, query }); @@ -669,6 +681,7 @@ export function stateSave() { showingGraph: left.showingGraph, showingLogs: left.showingLogs, showingTable: left.showingTable, + dedupStrategy: left.dedupStrategy, }, }; urlStates.left = serializeStateToUrlParam(leftUrlState, true); @@ -677,7 +690,12 @@ export function stateSave() { datasource: right.datasourceInstance.name, queries: right.queries.map(clearQueryKeys), range: right.range, - ui: { showingGraph: right.showingGraph, showingLogs: right.showingLogs, showingTable: right.showingTable }, + ui: { + showingGraph: right.showingGraph, + showingLogs: right.showingLogs, + showingTable: right.showingTable, + dedupStrategy: right.dedupStrategy, + }, }; urlStates.right = serializeStateToUrlParam(rightUrlState, true); @@ -696,24 +714,26 @@ const togglePanelActionCreator = ( | ActionCreator | ActionCreator | ActionCreator -) => (exploreId: ExploreId) => { - return (dispatch, getState) => { - let shouldRunQueries; - dispatch(actionCreator({ exploreId })); - dispatch(stateSave()); +) => (exploreId: ExploreId, isPanelVisible: boolean) => { + return dispatch => { + let uiFragmentStateUpdate: Partial; + const shouldRunQueries = !isPanelVisible; switch (actionCreator.type) { case toggleGraphAction.type: - shouldRunQueries = getState().explore[exploreId].showingGraph; + uiFragmentStateUpdate = { showingGraph: !isPanelVisible }; break; case toggleLogsAction.type: - shouldRunQueries = getState().explore[exploreId].showingLogs; + uiFragmentStateUpdate = { showingLogs: !isPanelVisible }; break; case toggleTableAction.type: - shouldRunQueries = getState().explore[exploreId].showingTable; + uiFragmentStateUpdate = { showingTable: !isPanelVisible }; break; } + dispatch(actionCreator({ exploreId })); + dispatch(updateExploreUIState(exploreId, uiFragmentStateUpdate)); + if (shouldRunQueries) { dispatch(runQueries(exploreId)); } @@ -734,3 +754,12 @@ export const toggleLogs = togglePanelActionCreator(toggleLogsAction); * Expand/collapse the table result viewer. When collapsed, table queries won't be run. */ export const toggleTable = togglePanelActionCreator(toggleTableAction); + +/** + * Change logs deduplication strategy and update URL. + */ +export const changeDedupStrategy = (exploreId, dedupStrategy: LogsDedupStrategy) => { + return dispatch => { + dispatch(updateExploreUIState(exploreId, { dedupStrategy })); + }; +}; diff --git a/public/app/features/explore/state/reducers.ts b/public/app/features/explore/state/reducers.ts index 76fc7d5de32..255591ee6e3 100644 --- a/public/app/features/explore/state/reducers.ts +++ b/public/app/features/explore/state/reducers.ts @@ -37,6 +37,7 @@ import { toggleLogsAction, toggleTableAction, queriesImportedAction, + updateUIStateAction, } from './actionTypes'; export const DEFAULT_RANGE = { @@ -406,6 +407,12 @@ export const itemReducer = reducerFactory({} as ExploreItemSta }; }, }) + .addMapper({ + filter: updateUIStateAction, + mapper: (state, action): ExploreItemState => { + return { ...state, ...action.payload }; + }, + }) .addMapper({ filter: toggleGraphAction, mapper: (state): ExploreItemState => { @@ -415,7 +422,7 @@ export const itemReducer = reducerFactory({} as ExploreItemSta // Discard transactions related to Graph query nextQueryTransactions = state.queryTransactions.filter(qt => qt.resultType !== 'Graph'); } - return { ...state, queryTransactions: nextQueryTransactions, showingGraph }; + return { ...state, queryTransactions: nextQueryTransactions }; }, }) .addMapper({ @@ -427,7 +434,7 @@ export const itemReducer = reducerFactory({} as ExploreItemSta // Discard transactions related to Logs query nextQueryTransactions = state.queryTransactions.filter(qt => qt.resultType !== 'Logs'); } - return { ...state, queryTransactions: nextQueryTransactions, showingLogs }; + return { ...state, queryTransactions: nextQueryTransactions }; }, }) .addMapper({ @@ -435,7 +442,7 @@ export const itemReducer = reducerFactory({} as ExploreItemSta mapper: (state): ExploreItemState => { const showingTable = !state.showingTable; if (showingTable) { - return { ...state, showingTable, queryTransactions: state.queryTransactions }; + return { ...state, queryTransactions: state.queryTransactions }; } // Toggle off needs discarding of table queries and results @@ -446,7 +453,7 @@ export const itemReducer = reducerFactory({} as ExploreItemSta state.queryIntervals.intervalMs ); - return { ...state, ...results, queryTransactions: nextQueryTransactions, showingTable }; + return { ...state, ...results, queryTransactions: nextQueryTransactions }; }, }) .addMapper({ diff --git a/public/app/features/panel/specs/metrics_panel_ctrl.test.ts b/public/app/features/panel/specs/metrics_panel_ctrl.test.ts index d647af616a9..3ee4c5165cb 100644 --- a/public/app/features/panel/specs/metrics_panel_ctrl.test.ts +++ b/public/app/features/panel/specs/metrics_panel_ctrl.test.ts @@ -1,6 +1,9 @@ jest.mock('app/core/core', () => ({})); jest.mock('app/core/config', () => { return { + bootData: { + user: {}, + }, panels: { test: { id: 'test', diff --git a/public/app/types/explore.ts b/public/app/types/explore.ts index 9c8d977c3ad..066ca226157 100644 --- a/public/app/types/explore.ts +++ b/public/app/types/explore.ts @@ -11,7 +11,7 @@ import { } from '@grafana/ui'; import { Emitter } from 'app/core/core'; -import { LogsModel } from 'app/core/logs_model'; +import { LogsModel, LogsDedupStrategy } from 'app/core/logs_model'; import TableModel from 'app/core/table_model'; export interface CompletionItem { @@ -237,12 +237,18 @@ export interface ExploreItemState { * React keys for rendering of QueryRows */ queryKeys: string[]; + + /** + * Current logs deduplication strategy + */ + dedupStrategy?: LogsDedupStrategy; } export interface ExploreUIState { showingTable: boolean; showingGraph: boolean; showingLogs: boolean; + dedupStrategy?: LogsDedupStrategy; } export interface ExploreUrlState {