From 3f919b75e48281a0cf63e4cfbd2ac8ac4f46e500 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20St=C3=BCrmer?= Date: Tue, 1 Nov 2022 13:06:25 +0000 Subject: [PATCH 01/21] Introduce provider for shared KbnUrlStateStorage --- x-pack/plugins/infra/public/apps/logs_app.tsx | 16 ++++++--- .../public/utils/kbn_url_state_context.ts | 34 +++++++++++++++++++ 2 files changed, 45 insertions(+), 5 deletions(-) create mode 100644 x-pack/plugins/infra/public/utils/kbn_url_state_context.ts diff --git a/x-pack/plugins/infra/public/apps/logs_app.tsx b/x-pack/plugins/infra/public/apps/logs_app.tsx index 0b345150daf8d..9e5722e316b00 100644 --- a/x-pack/plugins/infra/public/apps/logs_app.tsx +++ b/x-pack/plugins/infra/public/apps/logs_app.tsx @@ -19,6 +19,7 @@ import { LogsPage } from '../pages/logs'; import { InfraClientStartDeps, InfraClientStartExports } from '../types'; import { CommonInfraProviders, CoreProviders } from './common_providers'; import { prepareMountElement } from './common_styles'; +import { KbnUrlStateStorageFromRouterProvider } from '../utils/kbn_url_state_context'; export const renderApp = ( core: CoreStart, @@ -69,11 +70,16 @@ const LogsApp: React.FC<{ triggersActionsUI={plugins.triggersActionsUi} > - - - {uiCapabilities?.logs?.show && } - - + + + + {uiCapabilities?.logs?.show && } + + + diff --git a/x-pack/plugins/infra/public/utils/kbn_url_state_context.ts b/x-pack/plugins/infra/public/utils/kbn_url_state_context.ts new file mode 100644 index 0000000000000..7a751e30f4082 --- /dev/null +++ b/x-pack/plugins/infra/public/utils/kbn_url_state_context.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { IToasts } from '@kbn/core-notifications-browser'; +import { createKbnUrlStateStorage, withNotifyOnErrors } from '@kbn/kibana-utils-plugin/public'; +import createContainer from 'constate'; +import { History } from 'history'; +import { useState } from 'react'; + +const useKbnUrlStateStorageFromRouter = ({ + history, + toastsService, +}: { + history: History; + toastsService: IToasts; +}) => { + const [urlStateStorage] = useState(() => + createKbnUrlStateStorage({ + history, + useHash: false, + useHashQuery: false, + ...withNotifyOnErrors(toastsService), + }) + ); + + return urlStateStorage; +}; + +export const [KbnUrlStateStorageFromRouterProvider, useKbnUrlStateStorageFromRouterContext] = + createContainer(useKbnUrlStateStorageFromRouter); From 8d313da1ec17bb7dc2da4db96e4d4199cce5e612 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20St=C3=BCrmer?= Date: Tue, 1 Nov 2022 13:08:00 +0000 Subject: [PATCH 02/21] Introduce a timefilter state storage --- src/plugins/kibana_utils/public/index.ts | 1 + .../public/utils/timefilter_state_storage.ts | 64 +++++++++++++++++++ 2 files changed, 65 insertions(+) create mode 100644 x-pack/plugins/infra/public/utils/timefilter_state_storage.ts diff --git a/src/plugins/kibana_utils/public/index.ts b/src/plugins/kibana_utils/public/index.ts index 4aa6244e1b24e..d8882f74ee3b1 100644 --- a/src/plugins/kibana_utils/public/index.ts +++ b/src/plugins/kibana_utils/public/index.ts @@ -93,6 +93,7 @@ export { replaceUrlHashQuery, } from './state_management/url'; export type { + IStateStorage, IStateSyncConfig, ISyncStateRef, IKbnUrlStateStorage, diff --git a/x-pack/plugins/infra/public/utils/timefilter_state_storage.ts b/x-pack/plugins/infra/public/utils/timefilter_state_storage.ts new file mode 100644 index 0000000000000..d990ec81649c1 --- /dev/null +++ b/x-pack/plugins/infra/public/utils/timefilter_state_storage.ts @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { RefreshInterval, TimefilterContract } from '@kbn/data-plugin/public'; +import { TimeRange } from '@kbn/es-query'; +import { IStateStorage } from '@kbn/kibana-utils-plugin/public'; +import { map, merge, Observable, of } from 'rxjs'; + +export const timefilterStateStorageKey = 'timefilter'; +type TimefilterStateStorageKey = typeof timefilterStateStorageKey; + +interface ITimefilterStateStorage extends IStateStorage { + set(key: TimefilterStateStorageKey, state: TimefilterState): void; + set(key: string, state: State): void; + get(key: TimefilterStateStorageKey): TimefilterState | null; + get(key: string): State | null; + change$(key: TimefilterStateStorageKey): Observable; + change$(key: string): Observable; +} + +export interface TimefilterState { + timeRange?: TimeRange; + refreshInterval?: RefreshInterval; +} + +export const createTimefilterStateStorage = ({ + timefilter, +}: { + timefilter: TimefilterContract; +}): ITimefilterStateStorage => { + return { + set: (key, state) => { + if (key !== timefilterStateStorageKey) { + return; + } + + // TS doesn't narrow the overload arguments correctly + const { timeRange, refreshInterval } = state as TimefilterState; + + if (timeRange != null) { + timefilter.setTime(timeRange); + } + if (refreshInterval != null) { + timefilter.setRefreshInterval(refreshInterval); + } + }, + get: (key) => (key === timefilterStateStorageKey ? getTimefilterState(timefilter) : null), + change$: (key) => + key === timefilterStateStorageKey + ? merge(timefilter.getTimeUpdate$(), timefilter.getRefreshIntervalUpdate$()).pipe( + map(() => getTimefilterState(timefilter)) + ) + : of(null), + }; +}; + +const getTimefilterState = (timefilter: TimefilterContract): TimefilterState => ({ + timeRange: timefilter.getTime(), + refreshInterval: timefilter.getRefreshInterval(), +}); From 12d2ac7fc070b5decbd4a4512a2526f0bacad11d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20St=C3=BCrmer?= Date: Tue, 1 Nov 2022 13:08:54 +0000 Subject: [PATCH 03/21] Express TimeKey as a runtime type --- x-pack/plugins/infra/common/time/time_key.ts | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/infra/common/time/time_key.ts b/x-pack/plugins/infra/common/time/time_key.ts index 42b14625d22a9..1fb0c628198ed 100644 --- a/x-pack/plugins/infra/common/time/time_key.ts +++ b/x-pack/plugins/infra/common/time/time_key.ts @@ -6,14 +6,20 @@ */ import { ascending, bisector } from 'd3-array'; +import * as rt from 'io-ts'; import { pick } from 'lodash'; -export interface TimeKey { - time: number; - tiebreaker: number; - gid?: string; - fromAutoReload?: boolean; -} +export const timeKeyRT = rt.intersection([ + rt.type({ + time: rt.number, + tiebreaker: rt.number, + }), + rt.partial({ + gid: rt.string, + fromAutoReload: rt.boolean, + }), +]); +export type TimeKey = rt.TypeOf; export interface UniqueTimeKey extends TimeKey { gid: string; From 7b7dca04258d3e868a12abe4c0dbc1a7a1492b82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20St=C3=BCrmer?= Date: Tue, 1 Nov 2022 13:10:23 +0000 Subject: [PATCH 04/21] Move out utility function --- .../replace_log_position_in_query_string.ts | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 x-pack/plugins/infra/public/containers/logs/log_position/replace_log_position_in_query_string.ts diff --git a/x-pack/plugins/infra/public/containers/logs/log_position/replace_log_position_in_query_string.ts b/x-pack/plugins/infra/public/containers/logs/log_position/replace_log_position_in_query_string.ts new file mode 100644 index 0000000000000..e447c2c1436d2 --- /dev/null +++ b/x-pack/plugins/infra/public/containers/logs/log_position/replace_log_position_in_query_string.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { replaceStateKeyInQueryString } from '../../../utils/url_state'; +import { LogPositionUrlState, LOG_POSITION_URL_STATE_KEY } from './use_log_position_url_state_sync'; + +const ONE_HOUR = 3600000; + +export const replaceLogPositionInQueryString = (time: number) => + Number.isNaN(time) + ? (value: string) => value + : replaceStateKeyInQueryString(LOG_POSITION_URL_STATE_KEY, { + position: { + time, + tiebreaker: 0, + }, + end: new Date(time + ONE_HOUR).toISOString(), + start: new Date(time - ONE_HOUR).toISOString(), + streamLive: false, + }); From b3a94375fd230e55267b6f80b7d090eb301ea0b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20St=C3=BCrmer?= Date: Tue, 1 Nov 2022 13:13:43 +0000 Subject: [PATCH 05/21] Refactor log position state to use state container --- .../containers/logs/log_position/index.ts | 4 +- .../logs/log_position/log_position_state.ts | 401 ++++++++---------- .../log_position_timefilter_state.ts | 55 +++ .../logs/log_position/use_log_position.ts | 238 +++++++++++ .../use_log_position_url_state_sync.ts | 78 ++++ .../with_log_position_url_state.tsx | 327 ++++++++------ .../pages/logs/stream/page_providers.tsx | 11 +- .../public/pages/logs/stream/page_toolbar.tsx | 2 +- x-pack/plugins/infra/public/utils/datemath.ts | 16 + .../public/utils/wrap_state_container.ts | 23 + .../page_objects/infra_logs_page.ts | 5 +- 11 files changed, 792 insertions(+), 368 deletions(-) create mode 100644 x-pack/plugins/infra/public/containers/logs/log_position/log_position_timefilter_state.ts create mode 100644 x-pack/plugins/infra/public/containers/logs/log_position/use_log_position.ts create mode 100644 x-pack/plugins/infra/public/containers/logs/log_position/use_log_position_url_state_sync.ts create mode 100644 x-pack/plugins/infra/public/utils/wrap_state_container.ts diff --git a/x-pack/plugins/infra/public/containers/logs/log_position/index.ts b/x-pack/plugins/infra/public/containers/logs/log_position/index.ts index 41d284caf9425..75013c13b130a 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_position/index.ts +++ b/x-pack/plugins/infra/public/containers/logs/log_position/index.ts @@ -6,4 +6,6 @@ */ export * from './log_position_state'; -export * from './with_log_position_url_state'; +export * from './replace_log_position_in_query_string'; +export * from './use_log_position'; +export { LogPositionUrlState } from './use_log_position_url_state_sync'; diff --git a/x-pack/plugins/infra/public/containers/logs/log_position/log_position_state.ts b/x-pack/plugins/infra/public/containers/logs/log_position/log_position_state.ts index 521a5bf8562fc..09c7570d87089 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_position/log_position_state.ts +++ b/x-pack/plugins/infra/public/containers/logs/log_position/log_position_state.ts @@ -5,249 +5,192 @@ * 2.0. */ -import { useState, useMemo, useEffect, useCallback } from 'react'; -import createContainer from 'constate'; -import useSetState from 'react-use/lib/useSetState'; -import useInterval from 'react-use/lib/useInterval'; +import { RefreshInterval } from '@kbn/data-plugin/public'; +import { TimeRange } from '@kbn/es-query'; +import { createStateContainer, ReduxLikeStateContainer } from '@kbn/kibana-utils-plugin/public'; +import { pipe } from 'fp-ts/lib/pipeable'; +import produce from 'immer'; +import logger from 'redux-logger'; import { TimeKey } from '../../../../common/time'; -import { datemathToEpochMillis, isValidDatemath } from '../../../utils/datemath'; -import { useKibanaTimefilterTime } from '../../../hooks/use_kibana_timefilter_time'; - -type TimeKeyOrNull = TimeKey | null; - -interface DateRange { - startDateExpression: string; - endDateExpression: string; - startTimestamp: number; - endTimestamp: number; - timestampsLastUpdate: number; - lastCompleteDateRangeExpressionUpdate: number; -} - -interface VisiblePositions { - startKey: TimeKeyOrNull; - middleKey: TimeKeyOrNull; - endKey: TimeKeyOrNull; - pagesAfterEnd: number; - pagesBeforeStart: number; -} - -export interface LogPositionStateParams { - isInitialized: boolean; - targetPosition: TimeKeyOrNull; - isStreaming: boolean; - firstVisiblePosition: TimeKeyOrNull; - pagesBeforeStart: number; - pagesAfterEnd: number; - visibleMidpoint: TimeKeyOrNull; - visibleMidpointTime: number | null; - visibleTimeInterval: { start: number; end: number } | null; - startDateExpression: string; - endDateExpression: string; - startTimestamp: number | null; - endTimestamp: number | null; - timestampsLastUpdate: number; - lastCompleteDateRangeExpressionUpdate: number; +import { datemathToEpochMillis } from '../../../utils/datemath'; +import { TimefilterState } from '../../../utils/timefilter_state_storage'; +import { LogPositionUrlState } from './use_log_position_url_state_sync'; + +export interface LogPositionState { + targetPosition: TimeKey | null; + timeRange: { + expression: TimeRange; + lastChangedCompletely: number; + }; + timestamps: { + startTimestamp: number; + endTimestamp: number; + lastChangedTimestamp: number; + }; + refreshInterval: RefreshInterval; } -export interface LogPositionCallbacks { - initialize: () => void; - jumpToTargetPosition: (pos: TimeKeyOrNull) => void; - jumpToTargetPositionTime: (time: number) => void; - reportVisiblePositions: (visPos: VisiblePositions) => void; - startLiveStreaming: () => void; - stopLiveStreaming: () => void; - updateDateRange: (newDateRage: Partial) => void; +export interface InitialLogPositionArguments { + initialStateFromUrl: LogPositionUrlState | null; + initialStateFromTimefilter: TimefilterState | null; } -const DESIRED_BUFFER_PAGES = 2; - -const useVisibleMidpoint = (middleKey: TimeKeyOrNull, targetPosition: TimeKeyOrNull) => { - // Of the two dependencies `middleKey` and `targetPosition`, return - // whichever one was the most recently updated. This allows the UI controls - // to display a newly-selected `targetPosition` before loading new data; - // otherwise the previous `middleKey` would linger in the UI for the entirety - // of the loading operation, which the user could perceive as unresponsiveness - const [store, update] = useState({ - middleKey, - targetPosition, - currentValue: middleKey || targetPosition, - }); - useEffect(() => { - if (middleKey !== store.middleKey) { - update({ targetPosition, middleKey, currentValue: middleKey }); - } else if (targetPosition !== store.targetPosition) { - update({ targetPosition, middleKey, currentValue: targetPosition }); - } - }, [middleKey, targetPosition]); // eslint-disable-line react-hooks/exhaustive-deps - - return store.currentValue; +export const createInitialLogPositionState = ({ + initialStateFromUrl, + initialStateFromTimefilter, +}: InitialLogPositionArguments): LogPositionState => { + const nowTimestamp = Date.now(); + + return pipe( + { + targetPosition: null, + timeRange: { + expression: { + from: 'now-1d', + to: 'now', + }, + lastChangedCompletely: nowTimestamp, + }, + timestamps: { + startTimestamp: datemathToEpochMillis('now-1d', 'down') ?? 0, + endTimestamp: datemathToEpochMillis('now', 'up') ?? 0, + lastChangedTimestamp: nowTimestamp, + }, + refreshInterval: { + pause: true, + value: 5000, + }, + }, + updateStateFromTimefilterState(initialStateFromTimefilter), + updateStateFromUrlState(initialStateFromUrl) + ); }; -const TIME_DEFAULTS = { from: 'now-1d', to: 'now' }; -const STREAMING_INTERVAL = 5000; - -export const useLogPositionState: () => LogPositionStateParams & LogPositionCallbacks = () => { - const [getTime, setTime] = useKibanaTimefilterTime(TIME_DEFAULTS); - const { from: start, to: end } = getTime(); - - const DEFAULT_DATE_RANGE = { - startDateExpression: start, - endDateExpression: end, - }; - - // Flag to determine if `LogPositionState` has been fully initialized. - // - // When the page loads, there might be initial state in the URL. We want to - // prevent the entries from showing until we have processed that initial - // state. That prevents double fetching. - const [isInitialized, setInitialized] = useState(false); - const initialize = useCallback(() => { - setInitialized(true); - }, [setInitialized]); - - const [targetPosition, jumpToTargetPosition] = useState(null); - const [isStreaming, setIsStreaming] = useState(false); - const [visiblePositions, reportVisiblePositions] = useState({ - endKey: null, - middleKey: null, - startKey: null, - pagesBeforeStart: Infinity, - pagesAfterEnd: Infinity, +export const createLogPositionStateContainer = (initialArguments: InitialLogPositionArguments) => + createStateContainer(createInitialLogPositionState(initialArguments), { + updateTimeRange: (state: LogPositionState) => (timeRange: Partial) => + updateTimeRange(timeRange)(state), + updateRefreshInterval: + (state: LogPositionState) => (refreshInterval: Partial) => + updateRefreshInterval(refreshInterval)(state), + startLiveStreaming: (state: LogPositionState) => () => + updateRefreshInterval({ pause: false, value: 5000 })(state), + stopLiveStreaming: (state: LogPositionState) => () => + updateRefreshInterval({ pause: true })(state), + jumpToTargetPosition: (state: LogPositionState) => (targetPosition: TimeKey | null) => + produce(state, (draftState) => { + draftState.targetPosition = targetPosition; + }), + jumpToTargetPositionTime: (state: LogPositionState) => (time: number) => + produce(state, (draftState) => { + draftState.targetPosition = { tiebreaker: 0, time }; + }), }); - // We group the `startDate` and `endDate` values in the same object to be able - // to set both at the same time, saving a re-render - const [dateRange, setDateRange] = useSetState({ - ...DEFAULT_DATE_RANGE, - startTimestamp: datemathToEpochMillis(DEFAULT_DATE_RANGE.startDateExpression)!, - endTimestamp: datemathToEpochMillis(DEFAULT_DATE_RANGE.endDateExpression, 'up')!, - timestampsLastUpdate: Date.now(), - lastCompleteDateRangeExpressionUpdate: Date.now(), - }); +export const withLogger = >( + stateContainer: StateContainer +): StateContainer => { + stateContainer.addMiddleware(logger as any); + return stateContainer; +}; - useEffect(() => { - if (isInitialized) { - if ( - TIME_DEFAULTS.from !== dateRange.startDateExpression || - TIME_DEFAULTS.to !== dateRange.endDateExpression - ) { - setTime({ from: dateRange.startDateExpression, to: dateRange.endDateExpression }); +export const updateTimeRange = (timeRange: Partial) => + produce((draftState) => { + const newFrom = timeRange?.from; + const newTo = timeRange?.to; + const nowTimestamp = Date.now(); + + // Update expression and timestamps + if (newFrom != null) { + draftState.timeRange.expression.from = newFrom; + const newStartTimestamp = datemathToEpochMillis(newFrom, 'down'); + if (newStartTimestamp != null) { + draftState.timestamps.startTimestamp = newStartTimestamp; + draftState.timestamps.lastChangedTimestamp = nowTimestamp; } } - }, [isInitialized, dateRange.startDateExpression, dateRange.endDateExpression, setTime]); - - const { startKey, middleKey, endKey, pagesBeforeStart, pagesAfterEnd } = visiblePositions; - - const visibleMidpoint = useVisibleMidpoint(middleKey, targetPosition); - - const visibleTimeInterval = useMemo( - () => (startKey && endKey ? { start: startKey.time, end: endKey.time } : null), - [startKey, endKey] - ); - - // Allow setting `startDate` and `endDate` separately, or together - const updateDateRange = useCallback( - (newDateRange: Partial) => { - // Prevent unnecessary re-renders - if (!('startDateExpression' in newDateRange) && !('endDateExpression' in newDateRange)) { - return; + if (newTo != null) { + draftState.timeRange.expression.to = newTo; + const newEndTimestamp = datemathToEpochMillis(newTo, 'up'); + if (newEndTimestamp != null) { + draftState.timestamps.endTimestamp = newEndTimestamp; + draftState.timestamps.lastChangedTimestamp = nowTimestamp; } - - const nextStartDateExpression = - newDateRange.startDateExpression || dateRange.startDateExpression; - const nextEndDateExpression = newDateRange.endDateExpression || dateRange.endDateExpression; - - if (!isValidDatemath(nextStartDateExpression) || !isValidDatemath(nextEndDateExpression)) { - return; - } - - // Dates are valid, so the function cannot return `null` - const nextStartTimestamp = datemathToEpochMillis(nextStartDateExpression)!; - const nextEndTimestamp = datemathToEpochMillis(nextEndDateExpression, 'up')!; - - // Reset the target position if it doesn't fall within the new range. - if ( - targetPosition && - (nextStartTimestamp > targetPosition.time || nextEndTimestamp < targetPosition.time) - ) { - jumpToTargetPosition(null); - } - - setDateRange((prevState) => ({ - ...newDateRange, - startTimestamp: nextStartTimestamp, - endTimestamp: nextEndTimestamp, - timestampsLastUpdate: Date.now(), - // NOTE: Complete refers to the last time an update was requested with both expressions. These require a full refresh (unless streaming). Timerange expansion - // and pagination however do not. - lastCompleteDateRangeExpressionUpdate: - 'startDateExpression' in newDateRange && 'endDateExpression' in newDateRange - ? Date.now() - : prevState.lastCompleteDateRangeExpressionUpdate, - })); - }, - [setDateRange, dateRange, targetPosition] - ); - - // `endTimestamp` update conditions - useEffect(() => { - if (dateRange.endDateExpression !== 'now') { - return; } - - // User is close to the bottom edge of the scroll. - if (visiblePositions.pagesAfterEnd <= DESIRED_BUFFER_PAGES) { - setDateRange({ - endTimestamp: datemathToEpochMillis(dateRange.endDateExpression, 'up')!, - timestampsLastUpdate: Date.now(), - }); + if (newFrom != null && newTo != null) { + draftState.timeRange.lastChangedCompletely = nowTimestamp; } - }, [dateRange.endDateExpression, visiblePositions, setDateRange]); - - const startLiveStreaming = useCallback(() => { - setIsStreaming(true); - jumpToTargetPosition(null); - updateDateRange({ startDateExpression: 'now-1d', endDateExpression: 'now' }); - }, [updateDateRange]); - const stopLiveStreaming = useCallback(() => { - setIsStreaming(false); - }, []); - - useInterval( - () => updateDateRange({ startDateExpression: 'now-1d', endDateExpression: 'now' }), - isStreaming ? STREAMING_INTERVAL : null - ); - - const state = { - isInitialized, - targetPosition, - isStreaming, - firstVisiblePosition: startKey, - pagesBeforeStart, - pagesAfterEnd, - visibleMidpoint, - visibleMidpointTime: visibleMidpoint ? visibleMidpoint.time : null, - visibleTimeInterval, - ...dateRange, - }; - - const callbacks = { - initialize, - jumpToTargetPosition, - jumpToTargetPositionTime: useCallback( - (time: number) => jumpToTargetPosition({ tiebreaker: 0, time }), - [jumpToTargetPosition] - ), - reportVisiblePositions, - startLiveStreaming, - stopLiveStreaming, - updateDateRange, - }; - - return { ...state, ...callbacks }; -}; + // Reset the target position if it doesn't fall within the new range. + if ( + draftState.targetPosition != null && + (draftState.timestamps.startTimestamp > draftState.targetPosition.time || + draftState.timestamps.endTimestamp < draftState.targetPosition.time) + ) { + draftState.targetPosition = null; + } + }); -export const [LogPositionStateProvider, useLogPositionStateContext] = - createContainer(useLogPositionState); +export const updateRefreshInterval = + (refreshInterval: Partial) => (state: LogPositionState) => + pipe( + state, + produce((draftState) => { + if (refreshInterval.pause != null) { + draftState.refreshInterval.pause = refreshInterval.pause; + } + if (refreshInterval.value != null) { + draftState.refreshInterval.value = refreshInterval.value; + } + + if (!draftState.refreshInterval.pause) { + draftState.targetPosition = null; + } + }), + (currentState) => { + if (!currentState.refreshInterval.pause) { + return updateTimeRange({ from: 'now-1d', to: 'now' })(currentState); + } else { + return currentState; + } + } + ); + +export const getUrlState = (state: LogPositionState): LogPositionUrlState => ({ + streamLive: !state.refreshInterval.pause, + start: state.timeRange.expression.from, + end: state.timeRange.expression.to, + position: state.targetPosition, +}); + +export const updateStateFromUrlState = + (urlState: LogPositionUrlState | null) => + (state: LogPositionState): LogPositionState => + pipe( + state, + updateTimeRange({ + from: urlState?.start, + to: urlState?.end, + }), + updateRefreshInterval({ pause: !urlState?.streamLive }) + ); + +export const getTimefilterState = (state: LogPositionState): TimefilterState => ({ + timeRange: state.timeRange.expression, + refreshInterval: state.refreshInterval, +}); + +export const updateStateFromTimefilterState = + (timefilterState: TimefilterState | null) => + (state: LogPositionState): LogPositionState => + pipe( + state, + updateTimeRange({ + from: timefilterState?.timeRange?.from, + to: timefilterState?.timeRange?.to, + }), + updateRefreshInterval({ + pause: timefilterState?.refreshInterval?.pause, + value: timefilterState?.refreshInterval?.value, + }) + ); diff --git a/x-pack/plugins/infra/public/containers/logs/log_position/log_position_timefilter_state.ts b/x-pack/plugins/infra/public/containers/logs/log_position/log_position_timefilter_state.ts new file mode 100644 index 0000000000000..35c2a2f367c4e --- /dev/null +++ b/x-pack/plugins/infra/public/containers/logs/log_position/log_position_timefilter_state.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { INullableBaseStateContainer, syncState } from '@kbn/kibana-utils-plugin/public'; +import { useCallback, useState } from 'react'; +import { useKibanaContextForPlugin } from '../../../hooks/use_kibana'; +import { + createTimefilterStateStorage, + TimefilterState, + timefilterStateStorageKey, +} from '../../../utils/timefilter_state_storage'; + +export const useLogPositionTimefilterStateSync = () => { + const { + services: { + data: { + query: { + timefilter: { timefilter }, + }, + }, + }, + } = useKibanaContextForPlugin(); + + const [timefilterStateStorage] = useState(() => createTimefilterStateStorage({ timefilter })); + + const [initialStateFromTimefilter] = useState(() => + timefilterStateStorage.get(timefilterStateStorageKey) + ); + + const startSyncingWithTimefilter = useCallback( + (stateContainer: INullableBaseStateContainer) => { + timefilterStateStorage.set(timefilterStateStorageKey, stateContainer.get()); + + const { start, stop } = syncState({ + storageKey: timefilterStateStorageKey, + stateContainer, + stateStorage: timefilterStateStorage, + }); + + start(); + + return stop; + }, + [timefilterStateStorage] + ); + + return { + initialStateFromTimefilter, + startSyncingWithTimefilter, + }; +}; diff --git a/x-pack/plugins/infra/public/containers/logs/log_position/use_log_position.ts b/x-pack/plugins/infra/public/containers/logs/log_position/use_log_position.ts new file mode 100644 index 0000000000000..0c9ef6f3c5bf7 --- /dev/null +++ b/x-pack/plugins/infra/public/containers/logs/log_position/use_log_position.ts @@ -0,0 +1,238 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import createContainer from 'constate'; +import { useCallback, useEffect, useMemo, useState } from 'react'; +import useInterval from 'react-use/lib/useInterval'; +import { TimeKey } from '../../../../common/time'; +import { TimefilterState } from '../../../utils/timefilter_state_storage'; +import { useObservableState } from '../../../utils/use_observable'; +import { wrapStateContainer } from '../../../utils/wrap_state_container'; +import { + createLogPositionStateContainer, + getTimefilterState, + getUrlState, + LogPositionState, + updateStateFromTimefilterState, + updateStateFromUrlState, + withLogger, +} from './log_position_state'; +import { useLogPositionTimefilterStateSync } from './log_position_timefilter_state'; +import { LogPositionUrlState, useLogPositionUrlStateSync } from './use_log_position_url_state_sync'; + +type TimeKeyOrNull = TimeKey | null; + +interface DateRange { + startDateExpression: string; + endDateExpression: string; + startTimestamp: number; + endTimestamp: number; + timestampsLastUpdate: number; + lastCompleteDateRangeExpressionUpdate: number; +} + +interface VisiblePositions { + startKey: TimeKeyOrNull; + middleKey: TimeKeyOrNull; + endKey: TimeKeyOrNull; + pagesAfterEnd: number; + pagesBeforeStart: number; +} + +export type LogPositionStateParams = DateRange & { + targetPosition: TimeKeyOrNull; + isStreaming: boolean; + firstVisiblePosition: TimeKeyOrNull; + pagesBeforeStart: number; + pagesAfterEnd: number; + visibleMidpoint: TimeKeyOrNull; + visibleMidpointTime: number | null; + visibleTimeInterval: { start: number; end: number } | null; +}; + +export interface LogPositionCallbacks { + jumpToTargetPosition: (pos: TimeKeyOrNull) => void; + jumpToTargetPositionTime: (time: number) => void; + reportVisiblePositions: (visPos: VisiblePositions) => void; + startLiveStreaming: () => void; + stopLiveStreaming: () => void; + updateDateRange: UpdateDateRangeFn; +} + +type UpdateDateRangeFn = ( + newDateRange: Partial> +) => void; + +const DESIRED_BUFFER_PAGES = 2; + +export const useLogPositionState: () => LogPositionStateParams & LogPositionCallbacks = () => { + const { initialStateFromUrl, startSyncingWithUrl } = useLogPositionUrlStateSync(); + const { initialStateFromTimefilter, startSyncingWithTimefilter } = + useLogPositionTimefilterStateSync(); + + const [logPositionStateContainer] = useState(() => + withLogger( + createLogPositionStateContainer({ + initialStateFromUrl, + initialStateFromTimefilter, + }) + ) + ); + + useEffect(() => { + return startSyncingWithUrl( + wrapStateContainer({ + wrapGet: getUrlState, + wrapSet: updateStateFromUrlState, + })(logPositionStateContainer) + ); + }, [logPositionStateContainer, startSyncingWithUrl]); + + useEffect(() => { + return startSyncingWithTimefilter( + wrapStateContainer({ + wrapGet: getTimefilterState, + wrapSet: updateStateFromTimefilterState, + })(logPositionStateContainer) + ); + }, [logPositionStateContainer, startSyncingWithTimefilter, startSyncingWithUrl]); + + const { latestValue: latestLogPositionState } = useObservableState( + logPositionStateContainer.state$, + () => logPositionStateContainer.get() + ); + + const dateRange = useMemo( + () => getLegacyDateRange(latestLogPositionState), + [latestLogPositionState] + ); + + const targetPosition = useMemo( + () => latestLogPositionState.targetPosition, + [latestLogPositionState] + ); + + const isStreaming = useMemo( + () => !latestLogPositionState.refreshInterval.pause, + [latestLogPositionState] + ); + + const updateDateRange = useCallback( + (newDateRange: Partial>) => + logPositionStateContainer.transitions.updateTimeRange({ + from: newDateRange.startDateExpression, + to: newDateRange.endDateExpression, + }), + [logPositionStateContainer] + ); + + const { reportVisiblePositions, visibleMidpoint, visiblePositions, visibleTimeInterval } = + useVisiblePositions(targetPosition); + + // `endTimestamp` update conditions + useEffect(() => { + if (dateRange.endDateExpression !== 'now') { + return; + } + + // User is close to the bottom edge of the scroll. + if (visiblePositions.pagesAfterEnd <= DESIRED_BUFFER_PAGES) { + logPositionStateContainer.transitions.updateTimeRange({ to: 'now' }); + } + }, [dateRange.endDateExpression, visiblePositions, logPositionStateContainer]); + + useInterval( + () => logPositionStateContainer.transitions.updateTimeRange({ from: 'now-1d', to: 'now' }), + latestLogPositionState.refreshInterval.pause || + latestLogPositionState.refreshInterval.value <= 0 + ? null + : latestLogPositionState.refreshInterval.value + ); + + return { + // position state + targetPosition, + isStreaming, + ...dateRange, + + // visible positions state + firstVisiblePosition: visiblePositions.startKey, + pagesBeforeStart: visiblePositions.pagesBeforeStart, + pagesAfterEnd: visiblePositions.pagesAfterEnd, + visibleMidpoint, + visibleMidpointTime: visibleMidpoint ? visibleMidpoint.time : null, + visibleTimeInterval, + + // actions + jumpToTargetPosition: logPositionStateContainer.transitions.jumpToTargetPosition, + jumpToTargetPositionTime: logPositionStateContainer.transitions.jumpToTargetPositionTime, + reportVisiblePositions, + startLiveStreaming: logPositionStateContainer.transitions.startLiveStreaming, + stopLiveStreaming: logPositionStateContainer.transitions.stopLiveStreaming, + updateDateRange, + }; +}; + +export const [LogPositionStateProvider, useLogPositionStateContext] = + createContainer(useLogPositionState); + +const useVisiblePositions = (targetPosition: TimeKeyOrNull) => { + const [visiblePositions, reportVisiblePositions] = useState({ + endKey: null, + middleKey: null, + startKey: null, + pagesBeforeStart: Infinity, + pagesAfterEnd: Infinity, + }); + + const { startKey, middleKey, endKey } = visiblePositions; + + const visibleMidpoint = useVisibleMidpoint(middleKey, targetPosition); + + const visibleTimeInterval = useMemo( + () => (startKey && endKey ? { start: startKey.time, end: endKey.time } : null), + [startKey, endKey] + ); + + return { + reportVisiblePositions, + visibleMidpoint, + visiblePositions, + visibleTimeInterval, + }; +}; + +const useVisibleMidpoint = (middleKey: TimeKeyOrNull, targetPosition: TimeKeyOrNull) => { + // Of the two dependencies `middleKey` and `targetPosition`, return + // whichever one was the most recently updated. This allows the UI controls + // to display a newly-selected `targetPosition` before loading new data; + // otherwise the previous `middleKey` would linger in the UI for the entirety + // of the loading operation, which the user could perceive as unresponsiveness + const [store, update] = useState({ + middleKey, + targetPosition, + currentValue: middleKey || targetPosition, + }); + useEffect(() => { + if (middleKey !== store.middleKey) { + update({ targetPosition, middleKey, currentValue: middleKey }); + } else if (targetPosition !== store.targetPosition) { + update({ targetPosition, middleKey, currentValue: targetPosition }); + } + }, [middleKey, targetPosition]); // eslint-disable-line react-hooks/exhaustive-deps + + return store.currentValue; +}; + +const getLegacyDateRange = (logPositionState: LogPositionState): DateRange => ({ + endDateExpression: logPositionState.timeRange.expression.to, + endTimestamp: logPositionState.timestamps.endTimestamp, + lastCompleteDateRangeExpressionUpdate: logPositionState.timeRange.lastChangedCompletely, + startDateExpression: logPositionState.timeRange.expression.from, + startTimestamp: logPositionState.timestamps.startTimestamp, + timestampsLastUpdate: logPositionState.timestamps.lastChangedTimestamp, +}); diff --git a/x-pack/plugins/infra/public/containers/logs/log_position/use_log_position_url_state_sync.ts b/x-pack/plugins/infra/public/containers/logs/log_position/use_log_position_url_state_sync.ts new file mode 100644 index 0000000000000..7b01d54639657 --- /dev/null +++ b/x-pack/plugins/infra/public/containers/logs/log_position/use_log_position_url_state_sync.ts @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { INullableBaseStateContainer, syncState } from '@kbn/kibana-utils-plugin/public'; +import { getOrElseW } from 'fp-ts/lib/Either'; +import { pipe } from 'fp-ts/lib/pipeable'; +import * as rt from 'io-ts'; +import { useCallback, useState } from 'react'; +import { map } from 'rxjs/operators'; +import { timeKeyRT } from '../../../../common/time'; +import { datemathStringRT } from '../../../utils/datemath'; +import { useKbnUrlStateStorageFromRouterContext } from '../../../utils/kbn_url_state_context'; + +export const logPositionUrlStateRT = rt.intersection([ + rt.type({ + streamLive: rt.boolean, + }), + rt.partial({ + position: rt.union([timeKeyRT, rt.null]), + start: datemathStringRT, + end: datemathStringRT, + }), +]); + +export type LogPositionUrlState = rt.TypeOf; + +export const LOG_POSITION_URL_STATE_KEY = 'logPosition'; + +export const useLogPositionUrlStateSync = () => { + const urlStateStorage = useKbnUrlStateStorageFromRouterContext(); + + const [initialStateFromUrl] = useState(() => + pipe( + logPositionUrlStateRT.decode(urlStateStorage.get(LOG_POSITION_URL_STATE_KEY)), + getOrElseW(() => null) + ) + ); + + const startSyncingWithUrl = useCallback( + (stateContainer: INullableBaseStateContainer) => { + if (initialStateFromUrl == null) { + urlStateStorage.set(LOG_POSITION_URL_STATE_KEY, stateContainer.get(), { + replace: true, + }); + } + + const { start, stop } = syncState({ + storageKey: LOG_POSITION_URL_STATE_KEY, + stateContainer: { + state$: stateContainer.state$.pipe(map(logPositionUrlStateRT.encode)), + set: (value) => + stateContainer.set( + pipe( + logPositionUrlStateRT.decode(value), + getOrElseW(() => null) + ) + ), + get: () => logPositionUrlStateRT.encode(stateContainer.get()), + }, + stateStorage: urlStateStorage, + }); + + start(); + + return stop; + }, + [initialStateFromUrl, urlStateStorage] + ); + + return { + initialStateFromUrl, + startSyncingWithUrl, + }; +}; diff --git a/x-pack/plugins/infra/public/containers/logs/log_position/with_log_position_url_state.tsx b/x-pack/plugins/infra/public/containers/logs/log_position/with_log_position_url_state.tsx index d4d8075a2598f..00465b58fe642 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_position/with_log_position_url_state.tsx +++ b/x-pack/plugins/infra/public/containers/logs/log_position/with_log_position_url_state.tsx @@ -5,138 +5,217 @@ * 2.0. */ -import React, { useMemo } from 'react'; - -import { pickTimeKey } from '../../../../common/time'; -import { replaceStateKeyInQueryString, UrlStateContainer } from '../../../utils/url_state'; -import { useLogPositionStateContext, LogPositionStateParams } from './log_position_state'; -import { isValidDatemath, datemathToEpochMillis } from '../../../utils/datemath'; - -/** - * Url State - */ -export interface LogPositionUrlState { - position?: LogPositionStateParams['visibleMidpoint']; - streamLive: boolean; - start?: string; - end?: string; -} - +import { INullableBaseStateContainer, syncState } from '@kbn/kibana-utils-plugin/public'; +import { getOrElseW } from 'fp-ts/lib/Either'; +import { pipe } from 'fp-ts/lib/pipeable'; +import * as rt from 'io-ts'; +import { useCallback, useState } from 'react'; +import { map } from 'rxjs/operators'; +import { timeKeyRT } from '../../../../common/time'; +import { datemathStringRT } from '../../../utils/datemath'; +import { useKbnUrlStateStorageFromRouterContext } from '../../../utils/kbn_url_state_context'; +import { replaceStateKeyInQueryString } from '../../../utils/url_state'; + +// import { INullableBaseStateContainer, syncState } from '@kbn/kibana-utils-plugin/public'; +// import { getOrElseW } from 'fp-ts/lib/Either'; +// import { pipe } from 'fp-ts/lib/pipeable'; +// import * as rt from 'io-ts'; +// import React, { useCallback, useMemo, useState } from 'react'; +// import { map } from 'rxjs/operators'; +// import { pickTimeKey, timeKeyRT } from '../../../../common/time'; +// import { datemathStringRT, datemathToEpochMillis, isValidDatemath } from '../../../utils/datemath'; +// import { useKbnUrlStateStorageFromRouterContext } from '../../../utils/kbn_url_state_context'; +// import { replaceStateKeyInQueryString, UrlStateContainer } from '../../../utils/url_state'; +// import { useLogPositionStateContext } from './log_position_state'; + +export const logPositionUrlStateRT = rt.intersection([ + rt.type({ + streamLive: rt.boolean, + }), + rt.partial({ + position: rt.union([timeKeyRT, rt.null]), + start: datemathStringRT, + end: datemathStringRT, + }), +]); + +export type LogPositionUrlState = rt.TypeOf; + +const LOG_POSITION_URL_STATE_KEY = 'logPosition'; const ONE_HOUR = 3600000; -export const WithLogPositionUrlState = () => { - const { - visibleMidpoint, - isStreaming, - jumpToTargetPosition, - startLiveStreaming, - stopLiveStreaming, - startDateExpression, - endDateExpression, - updateDateRange, - initialize, - } = useLogPositionStateContext(); - const urlState = useMemo( - () => ({ - position: visibleMidpoint ? pickTimeKey(visibleMidpoint) : null, - streamLive: isStreaming, - start: startDateExpression, - end: endDateExpression, - }), - [visibleMidpoint, isStreaming, startDateExpression, endDateExpression] - ); - return ( - { - if (!newUrlState) { - return; - } - - if (newUrlState.start || newUrlState.end) { - updateDateRange({ - startDateExpression: newUrlState.start, - endDateExpression: newUrlState.end, - }); - } - - if (newUrlState.position) { - jumpToTargetPosition(newUrlState.position); - } - - if (newUrlState.streamLive) { - startLiveStreaming(); - } else if (typeof newUrlState.streamLive !== 'undefined' && !newUrlState.streamLive) { - stopLiveStreaming(); - } - }} - onInitialize={(initialUrlState: LogPositionUrlState | undefined) => { - if (initialUrlState) { - const initialPosition = initialUrlState.position; - let initialStartDateExpression = initialUrlState.start; - let initialEndDateExpression = initialUrlState.end; - - if (!initialPosition) { - initialStartDateExpression = initialStartDateExpression || 'now-1d'; - initialEndDateExpression = initialEndDateExpression || 'now'; - } else { - const initialStartTimestamp = initialStartDateExpression - ? datemathToEpochMillis(initialStartDateExpression) - : undefined; - const initialEndTimestamp = initialEndDateExpression - ? datemathToEpochMillis(initialEndDateExpression, 'up') - : undefined; - - // Adjust the start-end range if the target position falls outside or if it's not set. - if (!initialStartTimestamp || initialStartTimestamp > initialPosition.time) { - initialStartDateExpression = new Date(initialPosition.time - ONE_HOUR).toISOString(); - } - - if (!initialEndTimestamp || initialEndTimestamp < initialPosition.time) { - initialEndDateExpression = new Date(initialPosition.time + ONE_HOUR).toISOString(); - } - - jumpToTargetPosition(initialPosition); - } - - if (initialStartDateExpression || initialEndDateExpression) { - updateDateRange({ - startDateExpression: initialStartDateExpression, - endDateExpression: initialEndDateExpression, - }); - } - - if (initialUrlState.streamLive) { - startLiveStreaming(); - } - } - - initialize(); - }} - /> +export const useLogPositionUrlStateSync = () => { + const urlStateStorage = useKbnUrlStateStorageFromRouterContext(); + + const [initialStateFromUrl] = useState(() => + pipe( + logPositionUrlStateRT.decode(urlStateStorage.get(LOG_POSITION_URL_STATE_KEY)), + getOrElseW(() => null) + ) ); -}; -const mapToUrlState = (value: any): LogPositionUrlState | undefined => - value - ? { - position: mapToPositionUrlState(value.position), - streamLive: mapToStreamLiveUrlState(value.streamLive), - start: mapToDate(value.start), - end: mapToDate(value.end), + const startSyncingWithUrl = useCallback( + (stateContainer: INullableBaseStateContainer) => { + if (initialStateFromUrl == null) { + urlStateStorage.set(LOG_POSITION_URL_STATE_KEY, stateContainer.get(), { + replace: true, + }); } - : undefined; -const mapToPositionUrlState = (value: any) => - value && typeof value.time === 'number' && typeof value.tiebreaker === 'number' - ? pickTimeKey(value) - : undefined; + const { start, stop } = syncState({ + storageKey: LOG_POSITION_URL_STATE_KEY, + stateContainer: { + state$: stateContainer.state$.pipe(map(logPositionUrlStateRT.encode)), + set: (value) => + stateContainer.set( + pipe( + logPositionUrlStateRT.decode(value), + getOrElseW(() => null) + ) + ), + get: () => logPositionUrlStateRT.encode(stateContainer.get()), + }, + stateStorage: urlStateStorage, + }); + + start(); + + return stop; + }, + [initialStateFromUrl, urlStateStorage] + ); + + return { + initialStateFromUrl, + startSyncingWithUrl, + }; +}; -const mapToStreamLiveUrlState = (value: any) => (typeof value === 'boolean' ? value : false); +// export const WithLogPositionUrlState = () => { +// const { +// visibleMidpoint, +// isStreaming, +// jumpToTargetPosition, +// startLiveStreaming, +// stopLiveStreaming, +// startDateExpression, +// endDateExpression, +// updateDateRange, +// initialize, +// } = useLogPositionStateContext(); +// const urlState = useMemo( +// () => ({ +// position: visibleMidpoint ? pickTimeKey(visibleMidpoint) : null, +// streamLive: isStreaming, +// start: startDateExpression, +// end: endDateExpression, +// }), +// [visibleMidpoint, isStreaming, startDateExpression, endDateExpression] +// ); + +// const handleChange = useCallback( +// (newUrlState: LogPositionUrlState | undefined) => { +// if (!newUrlState) { +// return; +// } + +// if (newUrlState.start || newUrlState.end) { +// updateDateRange({ +// startDateExpression: newUrlState.start, +// endDateExpression: newUrlState.end, +// }); +// } + +// if (newUrlState.position) { +// jumpToTargetPosition(newUrlState.position); +// } + +// if (newUrlState.streamLive) { +// startLiveStreaming(); +// } else if (typeof newUrlState.streamLive !== 'undefined' && !newUrlState.streamLive) { +// stopLiveStreaming(); +// } +// }, +// [jumpToTargetPosition, startLiveStreaming, stopLiveStreaming, updateDateRange] +// ); + +// const handleInitialize = useCallback( +// (initialUrlState: LogPositionUrlState | undefined) => { +// if (initialUrlState) { +// const initialPosition = initialUrlState.position; +// let initialStartDateExpression = initialUrlState.start; +// let initialEndDateExpression = initialUrlState.end; + +// if (!initialPosition) { +// initialStartDateExpression = initialStartDateExpression || 'now-1d'; +// initialEndDateExpression = initialEndDateExpression || 'now'; +// } else { +// const initialStartTimestamp = initialStartDateExpression +// ? datemathToEpochMillis(initialStartDateExpression) +// : undefined; +// const initialEndTimestamp = initialEndDateExpression +// ? datemathToEpochMillis(initialEndDateExpression, 'up') +// : undefined; + +// // Adjust the start-end range if the target position falls outside or if it's not set. +// if (!initialStartTimestamp || initialStartTimestamp > initialPosition.time) { +// initialStartDateExpression = new Date(initialPosition.time - ONE_HOUR).toISOString(); +// } + +// if (!initialEndTimestamp || initialEndTimestamp < initialPosition.time) { +// initialEndDateExpression = new Date(initialPosition.time + ONE_HOUR).toISOString(); +// } + +// jumpToTargetPosition(initialPosition); +// } + +// if (initialStartDateExpression || initialEndDateExpression) { +// updateDateRange({ +// startDateExpression: initialStartDateExpression, +// endDateExpression: initialEndDateExpression, +// }); +// } + +// if (initialUrlState.streamLive) { +// startLiveStreaming(); +// } +// } + +// initialize(); +// }, +// [initialize, jumpToTargetPosition, startLiveStreaming, updateDateRange] +// ); + +// return ( +// +// ); +// }; + +// const mapToUrlState = (value: any): LogPositionUrlState | undefined => +// value +// ? { +// position: mapToPositionUrlState(value.position), +// streamLive: mapToStreamLiveUrlState(value.streamLive), +// start: mapToDate(value.start), +// end: mapToDate(value.end), +// } +// : undefined; + +// const mapToPositionUrlState = (value: any) => +// value && typeof value.time === 'number' && typeof value.tiebreaker === 'number' +// ? pickTimeKey(value) +// : undefined; + +// const mapToStreamLiveUrlState = (value: any) => (typeof value === 'boolean' ? value : false); + +// const mapToDate = (value: any) => (isValidDatemath(value) ? value : undefined); -const mapToDate = (value: any) => (isValidDatemath(value) ? value : undefined); export const replaceLogPositionInQueryString = (time: number) => Number.isNaN(time) ? (value: string) => value diff --git a/x-pack/plugins/infra/public/pages/logs/stream/page_providers.tsx b/x-pack/plugins/infra/public/pages/logs/stream/page_providers.tsx index 026119ff5c74c..9a726152d9f7c 100644 --- a/x-pack/plugins/infra/public/pages/logs/stream/page_providers.tsx +++ b/x-pack/plugins/infra/public/pages/logs/stream/page_providers.tsx @@ -16,7 +16,6 @@ import { LogHighlightsStateProvider } from '../../../containers/logs/log_highlig import { LogPositionStateProvider, useLogPositionStateContext, - WithLogPositionUrlState, } from '../../../containers/logs/log_position'; import { LogStreamProvider, useLogStreamContext } from '../../../containers/logs/log_stream'; import { LogViewConfigurationProvider } from '../../../containers/logs/log_view_configuration'; @@ -55,8 +54,7 @@ const ViewLogInContext: React.FC = ({ children }) => { const LogEntriesStateProvider: React.FC = ({ children }) => { const { logViewId } = useLogViewContext(); - const { startTimestamp, endTimestamp, targetPosition, isInitialized } = - useLogPositionStateContext(); + const { startTimestamp, endTimestamp, targetPosition } = useLogPositionStateContext(); const { filterQuery } = useLogFilterStateContext(); // Don't render anything if the date range is incorrect. @@ -64,12 +62,6 @@ const LogEntriesStateProvider: React.FC = ({ children }) => { return null; } - // Don't initialize the entries until the position has been fully intialized. - // See `` - if (!isInitialized) { - return null; - } - return ( { - diff --git a/x-pack/plugins/infra/public/pages/logs/stream/page_toolbar.tsx b/x-pack/plugins/infra/public/pages/logs/stream/page_toolbar.tsx index cf30518f78ede..039d46f06913b 100644 --- a/x-pack/plugins/infra/public/pages/logs/stream/page_toolbar.tsx +++ b/x-pack/plugins/infra/public/pages/logs/stream/page_toolbar.tsx @@ -65,7 +65,7 @@ export const LogsToolbar = () => { showQueryInput={true} showQueryMenu={false} showFilterBar={false} - showDatePicker={false} + showDatePicker={true} /> diff --git a/x-pack/plugins/infra/public/utils/datemath.ts b/x-pack/plugins/infra/public/utils/datemath.ts index 2ed4f68b7a934..0780d4e906be6 100644 --- a/x-pack/plugins/infra/public/utils/datemath.ts +++ b/x-pack/plugins/infra/public/utils/datemath.ts @@ -6,6 +6,9 @@ */ import dateMath, { Unit } from '@kbn/datemath'; +import { chain } from 'fp-ts/lib/Either'; +import { pipe } from 'fp-ts/lib/pipeable'; +import * as rt from 'io-ts'; const JS_MAX_DATE = 8640000000000000; @@ -14,6 +17,19 @@ export function isValidDatemath(value: string): boolean { return !!(parsedValue && parsedValue.isValid()); } +export const datemathStringRT = new rt.Type( + 'datemath', + rt.string.is, + (value, context) => + pipe( + rt.string.validate(value, context), + chain((stringValue) => + isValidDatemath(stringValue) ? rt.success(stringValue) : rt.failure(stringValue, context) + ) + ), + String +); + export function datemathToEpochMillis(value: string, round: 'down' | 'up' = 'down'): number | null { const parsedValue = dateMath.parse(value, { roundUp: round === 'up' }); if (!parsedValue || !parsedValue.isValid()) { diff --git a/x-pack/plugins/infra/public/utils/wrap_state_container.ts b/x-pack/plugins/infra/public/utils/wrap_state_container.ts new file mode 100644 index 0000000000000..d6ec59ded314b --- /dev/null +++ b/x-pack/plugins/infra/public/utils/wrap_state_container.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { BaseState, BaseStateContainer } from '@kbn/kibana-utils-plugin/public'; +import { map } from 'rxjs/operators'; + +export const wrapStateContainer = + ({ + wrapSet, + wrapGet, + }: { + wrapSet: (state: StateB | null) => (previousState: StateA) => StateA; + wrapGet: (state: StateA) => StateB; + }) => + (stateContainer: BaseStateContainer) => ({ + get: () => wrapGet(stateContainer.get()), + set: (value: StateB | null) => stateContainer.set(wrapSet(value)(stateContainer.get())), + state$: stateContainer.state$.pipe(map(wrapGet)), + }); diff --git a/x-pack/test/functional/page_objects/infra_logs_page.ts b/x-pack/test/functional/page_objects/infra_logs_page.ts index c1d20c2e977ad..0bcbff031005c 100644 --- a/x-pack/test/functional/page_objects/infra_logs_page.ts +++ b/x-pack/test/functional/page_objects/infra_logs_page.ts @@ -5,11 +5,10 @@ * 2.0. */ -// import moment from 'moment'; +import { FlyoutOptionsUrlState } from '@kbn/infra-plugin/public/containers/logs/log_flyout'; +import { LogPositionUrlState } from '@kbn/infra-plugin/public/containers/logs/log_position'; import querystring from 'querystring'; import { encode, RisonValue } from 'rison-node'; -import { LogPositionUrlState } from '@kbn/infra-plugin/public/containers/logs/log_position/with_log_position_url_state'; -import { FlyoutOptionsUrlState } from '@kbn/infra-plugin/public/containers/logs/log_flyout'; import { FtrProviderContext } from '../ftr_provider_context'; export interface TabsParams { From 4215b32fe68931f555fad24b2c86d7892298798d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20St=C3=BCrmer?= Date: Tue, 1 Nov 2022 13:24:26 +0000 Subject: [PATCH 06/21] Remove unused files --- .../with_log_position_url_state.tsx | 230 ------------------ 1 file changed, 230 deletions(-) delete mode 100644 x-pack/plugins/infra/public/containers/logs/log_position/with_log_position_url_state.tsx diff --git a/x-pack/plugins/infra/public/containers/logs/log_position/with_log_position_url_state.tsx b/x-pack/plugins/infra/public/containers/logs/log_position/with_log_position_url_state.tsx deleted file mode 100644 index 00465b58fe642..0000000000000 --- a/x-pack/plugins/infra/public/containers/logs/log_position/with_log_position_url_state.tsx +++ /dev/null @@ -1,230 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { INullableBaseStateContainer, syncState } from '@kbn/kibana-utils-plugin/public'; -import { getOrElseW } from 'fp-ts/lib/Either'; -import { pipe } from 'fp-ts/lib/pipeable'; -import * as rt from 'io-ts'; -import { useCallback, useState } from 'react'; -import { map } from 'rxjs/operators'; -import { timeKeyRT } from '../../../../common/time'; -import { datemathStringRT } from '../../../utils/datemath'; -import { useKbnUrlStateStorageFromRouterContext } from '../../../utils/kbn_url_state_context'; -import { replaceStateKeyInQueryString } from '../../../utils/url_state'; - -// import { INullableBaseStateContainer, syncState } from '@kbn/kibana-utils-plugin/public'; -// import { getOrElseW } from 'fp-ts/lib/Either'; -// import { pipe } from 'fp-ts/lib/pipeable'; -// import * as rt from 'io-ts'; -// import React, { useCallback, useMemo, useState } from 'react'; -// import { map } from 'rxjs/operators'; -// import { pickTimeKey, timeKeyRT } from '../../../../common/time'; -// import { datemathStringRT, datemathToEpochMillis, isValidDatemath } from '../../../utils/datemath'; -// import { useKbnUrlStateStorageFromRouterContext } from '../../../utils/kbn_url_state_context'; -// import { replaceStateKeyInQueryString, UrlStateContainer } from '../../../utils/url_state'; -// import { useLogPositionStateContext } from './log_position_state'; - -export const logPositionUrlStateRT = rt.intersection([ - rt.type({ - streamLive: rt.boolean, - }), - rt.partial({ - position: rt.union([timeKeyRT, rt.null]), - start: datemathStringRT, - end: datemathStringRT, - }), -]); - -export type LogPositionUrlState = rt.TypeOf; - -const LOG_POSITION_URL_STATE_KEY = 'logPosition'; -const ONE_HOUR = 3600000; - -export const useLogPositionUrlStateSync = () => { - const urlStateStorage = useKbnUrlStateStorageFromRouterContext(); - - const [initialStateFromUrl] = useState(() => - pipe( - logPositionUrlStateRT.decode(urlStateStorage.get(LOG_POSITION_URL_STATE_KEY)), - getOrElseW(() => null) - ) - ); - - const startSyncingWithUrl = useCallback( - (stateContainer: INullableBaseStateContainer) => { - if (initialStateFromUrl == null) { - urlStateStorage.set(LOG_POSITION_URL_STATE_KEY, stateContainer.get(), { - replace: true, - }); - } - - const { start, stop } = syncState({ - storageKey: LOG_POSITION_URL_STATE_KEY, - stateContainer: { - state$: stateContainer.state$.pipe(map(logPositionUrlStateRT.encode)), - set: (value) => - stateContainer.set( - pipe( - logPositionUrlStateRT.decode(value), - getOrElseW(() => null) - ) - ), - get: () => logPositionUrlStateRT.encode(stateContainer.get()), - }, - stateStorage: urlStateStorage, - }); - - start(); - - return stop; - }, - [initialStateFromUrl, urlStateStorage] - ); - - return { - initialStateFromUrl, - startSyncingWithUrl, - }; -}; - -// export const WithLogPositionUrlState = () => { -// const { -// visibleMidpoint, -// isStreaming, -// jumpToTargetPosition, -// startLiveStreaming, -// stopLiveStreaming, -// startDateExpression, -// endDateExpression, -// updateDateRange, -// initialize, -// } = useLogPositionStateContext(); -// const urlState = useMemo( -// () => ({ -// position: visibleMidpoint ? pickTimeKey(visibleMidpoint) : null, -// streamLive: isStreaming, -// start: startDateExpression, -// end: endDateExpression, -// }), -// [visibleMidpoint, isStreaming, startDateExpression, endDateExpression] -// ); - -// const handleChange = useCallback( -// (newUrlState: LogPositionUrlState | undefined) => { -// if (!newUrlState) { -// return; -// } - -// if (newUrlState.start || newUrlState.end) { -// updateDateRange({ -// startDateExpression: newUrlState.start, -// endDateExpression: newUrlState.end, -// }); -// } - -// if (newUrlState.position) { -// jumpToTargetPosition(newUrlState.position); -// } - -// if (newUrlState.streamLive) { -// startLiveStreaming(); -// } else if (typeof newUrlState.streamLive !== 'undefined' && !newUrlState.streamLive) { -// stopLiveStreaming(); -// } -// }, -// [jumpToTargetPosition, startLiveStreaming, stopLiveStreaming, updateDateRange] -// ); - -// const handleInitialize = useCallback( -// (initialUrlState: LogPositionUrlState | undefined) => { -// if (initialUrlState) { -// const initialPosition = initialUrlState.position; -// let initialStartDateExpression = initialUrlState.start; -// let initialEndDateExpression = initialUrlState.end; - -// if (!initialPosition) { -// initialStartDateExpression = initialStartDateExpression || 'now-1d'; -// initialEndDateExpression = initialEndDateExpression || 'now'; -// } else { -// const initialStartTimestamp = initialStartDateExpression -// ? datemathToEpochMillis(initialStartDateExpression) -// : undefined; -// const initialEndTimestamp = initialEndDateExpression -// ? datemathToEpochMillis(initialEndDateExpression, 'up') -// : undefined; - -// // Adjust the start-end range if the target position falls outside or if it's not set. -// if (!initialStartTimestamp || initialStartTimestamp > initialPosition.time) { -// initialStartDateExpression = new Date(initialPosition.time - ONE_HOUR).toISOString(); -// } - -// if (!initialEndTimestamp || initialEndTimestamp < initialPosition.time) { -// initialEndDateExpression = new Date(initialPosition.time + ONE_HOUR).toISOString(); -// } - -// jumpToTargetPosition(initialPosition); -// } - -// if (initialStartDateExpression || initialEndDateExpression) { -// updateDateRange({ -// startDateExpression: initialStartDateExpression, -// endDateExpression: initialEndDateExpression, -// }); -// } - -// if (initialUrlState.streamLive) { -// startLiveStreaming(); -// } -// } - -// initialize(); -// }, -// [initialize, jumpToTargetPosition, startLiveStreaming, updateDateRange] -// ); - -// return ( -// -// ); -// }; - -// const mapToUrlState = (value: any): LogPositionUrlState | undefined => -// value -// ? { -// position: mapToPositionUrlState(value.position), -// streamLive: mapToStreamLiveUrlState(value.streamLive), -// start: mapToDate(value.start), -// end: mapToDate(value.end), -// } -// : undefined; - -// const mapToPositionUrlState = (value: any) => -// value && typeof value.time === 'number' && typeof value.tiebreaker === 'number' -// ? pickTimeKey(value) -// : undefined; - -// const mapToStreamLiveUrlState = (value: any) => (typeof value === 'boolean' ? value : false); - -// const mapToDate = (value: any) => (isValidDatemath(value) ? value : undefined); - -export const replaceLogPositionInQueryString = (time: number) => - Number.isNaN(time) - ? (value: string) => value - : replaceStateKeyInQueryString('logPosition', { - position: { - time, - tiebreaker: 0, - }, - end: new Date(time + ONE_HOUR).toISOString(), - start: new Date(time - ONE_HOUR).toISOString(), - streamLive: false, - }); From e2b1a9cfd50730165c28a4880ba78244b8631941 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20St=C3=BCrmer?= Date: Tue, 1 Nov 2022 18:22:35 +0000 Subject: [PATCH 07/21] Remove the custom date picker --- .../stream/components/stream_live_button.tsx | 31 ++++ .../public/pages/logs/stream/page_toolbar.tsx | 137 +++++++++--------- 2 files changed, 99 insertions(+), 69 deletions(-) create mode 100644 x-pack/plugins/infra/public/pages/logs/stream/components/stream_live_button.tsx diff --git a/x-pack/plugins/infra/public/pages/logs/stream/components/stream_live_button.tsx b/x-pack/plugins/infra/public/pages/logs/stream/components/stream_live_button.tsx new file mode 100644 index 0000000000000..d97a929aa3a16 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/logs/stream/components/stream_live_button.tsx @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiButtonEmpty } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; + +export const StreamLiveButton: React.FC<{ + isStreaming: boolean; + onStartStreaming: () => void; + onStopStreaming: () => void; +}> = ({ isStreaming, onStartStreaming, onStopStreaming }) => + isStreaming ? ( + + + + ) : ( + + + + ); diff --git a/x-pack/plugins/infra/public/pages/logs/stream/page_toolbar.tsx b/x-pack/plugins/infra/public/pages/logs/stream/page_toolbar.tsx index 039d46f06913b..232c10c8b3680 100644 --- a/x-pack/plugins/infra/public/pages/logs/stream/page_toolbar.tsx +++ b/x-pack/plugins/infra/public/pages/logs/stream/page_toolbar.tsx @@ -5,20 +5,20 @@ * 2.0. */ -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import React from 'react'; import { euiStyled } from '@kbn/kibana-react-plugin/common'; -import { useKibanaContextForPlugin } from '../../../hooks/use_kibana'; +import React, { useMemo } from 'react'; import { LogCustomizationMenu } from '../../../components/logging/log_customization_menu'; -import { LogDatepicker } from '../../../components/logging/log_datepicker'; import { LogHighlightsMenu } from '../../../components/logging/log_highlights_menu'; import { LogTextScaleControls } from '../../../components/logging/log_text_scale_controls'; import { LogTextWrapControls } from '../../../components/logging/log_text_wrap_controls'; import { useLogHighlightsStateContext } from '../../../containers/logs/log_highlights/log_highlights'; import { useLogPositionStateContext } from '../../../containers/logs/log_position'; import { useLogViewConfigurationContext } from '../../../containers/logs/log_view_configuration'; +import { useKibanaContextForPlugin } from '../../../hooks/use_kibana'; import { useLogViewContext } from '../../../hooks/use_log_view'; +import { StreamLiveButton } from './components/stream_live_button'; export const LogsToolbar = () => { const { derivedDataView } = useLogViewContext(); @@ -39,74 +39,73 @@ export const LogsToolbar = () => { goToPreviousHighlight, goToNextHighlight, } = useLogHighlightsStateContext(); - const { - isStreaming, - startLiveStreaming, - stopLiveStreaming, - startDateExpression, - endDateExpression, - updateDateRange, - } = useLogPositionStateContext(); + const { isStreaming, startLiveStreaming, stopLiveStreaming } = useLogPositionStateContext(); + + const dataViews = useMemo( + () => (derivedDataView != null ? [derivedDataView] : undefined), + [derivedDataView] + ); return ( -
- - - - - - - - - - - - - - highlightTerm.length > 0).length > 0 - } - goToPreviousHighlight={goToPreviousHighlight} - goToNextHighlight={goToNextHighlight} - hasPreviousHighlight={hasPreviousHighlight} - hasNextHighlight={hasNextHighlight} + <> + + +
+ + + + + - - - - - - - -
+ +
+ + highlightTerm.length > 0).length > 0 + } + goToPreviousHighlight={goToPreviousHighlight} + goToNextHighlight={goToNextHighlight} + hasPreviousHighlight={hasPreviousHighlight} + hasNextHighlight={hasNextHighlight} + /> + + + + + +
+
+ ); }; From 280b1b10b035cadeda8cdf1989df2bec46674a9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20St=C3=BCrmer?= Date: Tue, 1 Nov 2022 19:49:14 +0000 Subject: [PATCH 08/21] Throttle update of timestamp for "now" date --- .../containers/logs/log_position/use_log_position.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/infra/public/containers/logs/log_position/use_log_position.ts b/x-pack/plugins/infra/public/containers/logs/log_position/use_log_position.ts index 0c9ef6f3c5bf7..0dcae09bcd47b 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_position/use_log_position.ts +++ b/x-pack/plugins/infra/public/containers/logs/log_position/use_log_position.ts @@ -8,6 +8,7 @@ import createContainer from 'constate'; import { useCallback, useEffect, useMemo, useState } from 'react'; import useInterval from 'react-use/lib/useInterval'; +import useThrottle from 'react-use/lib/useThrottle'; import { TimeKey } from '../../../../common/time'; import { TimefilterState } from '../../../utils/timefilter_state_storage'; import { useObservableState } from '../../../utils/use_observable'; @@ -68,6 +69,7 @@ type UpdateDateRangeFn = ( ) => void; const DESIRED_BUFFER_PAGES = 2; +const RELATIVE_END_UPDATE_DELAY = 1000; export const useLogPositionState: () => LogPositionStateParams & LogPositionCallbacks = () => { const { initialStateFromUrl, startSyncingWithUrl } = useLogPositionUrlStateSync(); @@ -134,16 +136,20 @@ export const useLogPositionState: () => LogPositionStateParams & LogPositionCall useVisiblePositions(targetPosition); // `endTimestamp` update conditions + const throttledPagesAfterEnd = useThrottle( + visiblePositions.pagesAfterEnd, + RELATIVE_END_UPDATE_DELAY + ); useEffect(() => { if (dateRange.endDateExpression !== 'now') { return; } // User is close to the bottom edge of the scroll. - if (visiblePositions.pagesAfterEnd <= DESIRED_BUFFER_PAGES) { + if (throttledPagesAfterEnd <= DESIRED_BUFFER_PAGES) { logPositionStateContainer.transitions.updateTimeRange({ to: 'now' }); } - }, [dateRange.endDateExpression, visiblePositions, logPositionStateContainer]); + }, [dateRange.endDateExpression, throttledPagesAfterEnd, logPositionStateContainer]); useInterval( () => logPositionStateContainer.transitions.updateTimeRange({ from: 'now-1d', to: 'now' }), From e35620f70df498e159f0dc9d997617f63326abad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20St=C3=BCrmer?= Date: Tue, 1 Nov 2022 19:50:55 +0000 Subject: [PATCH 09/21] Remove unused styled component --- .../infra/public/pages/logs/stream/page_toolbar.tsx | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/x-pack/plugins/infra/public/pages/logs/stream/page_toolbar.tsx b/x-pack/plugins/infra/public/pages/logs/stream/page_toolbar.tsx index 232c10c8b3680..36c2349b471fd 100644 --- a/x-pack/plugins/infra/public/pages/logs/stream/page_toolbar.tsx +++ b/x-pack/plugins/infra/public/pages/logs/stream/page_toolbar.tsx @@ -7,7 +7,6 @@ import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { euiStyled } from '@kbn/kibana-react-plugin/common'; import React, { useMemo } from 'react'; import { LogCustomizationMenu } from '../../../components/logging/log_customization_menu'; import { LogHighlightsMenu } from '../../../components/logging/log_highlights_menu'; @@ -108,13 +107,3 @@ export const LogsToolbar = () => { ); }; - -const QueryBarFlexItem = euiStyled(EuiFlexItem)` - @media (min-width: 1200px) { - flex: 0 0 100% !important; - margin-left: 0 !important; - margin-right: 0 !important; - padding-left: 12px; - padding-right: 12px; - } -`; From bf8520d76463c852c21d17628f8db6027a8eb7d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20St=C3=BCrmer?= Date: Wed, 2 Nov 2022 11:19:19 +0000 Subject: [PATCH 10/21] Fix linter warning about type-only export --- .../plugins/infra/public/containers/logs/log_position/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/infra/public/containers/logs/log_position/index.ts b/x-pack/plugins/infra/public/containers/logs/log_position/index.ts index 75013c13b130a..e4e6ad6c54deb 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_position/index.ts +++ b/x-pack/plugins/infra/public/containers/logs/log_position/index.ts @@ -8,4 +8,4 @@ export * from './log_position_state'; export * from './replace_log_position_in_query_string'; export * from './use_log_position'; -export { LogPositionUrlState } from './use_log_position_url_state_sync'; +export type { LogPositionUrlState } from './use_log_position_url_state_sync'; From a731f88f50fe2efa3d795a414c553730b1772a20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20St=C3=BCrmer?= Date: Wed, 2 Nov 2022 19:18:47 +0000 Subject: [PATCH 11/21] Relax url state parsing and add some tests --- x-pack/plugins/infra/common/time/time_key.ts | 11 +- .../log_position/log_position_state.test.ts | 129 ++++++++++++++++++ .../logs/log_position/log_position_state.ts | 63 +++++---- .../use_log_position_url_state_sync.ts | 18 +-- x-pack/plugins/infra/public/utils/datemath.ts | 8 +- 5 files changed, 189 insertions(+), 40 deletions(-) create mode 100644 x-pack/plugins/infra/public/containers/logs/log_position/log_position_state.test.ts diff --git a/x-pack/plugins/infra/common/time/time_key.ts b/x-pack/plugins/infra/common/time/time_key.ts index 1fb0c628198ed..1f0f242fd7496 100644 --- a/x-pack/plugins/infra/common/time/time_key.ts +++ b/x-pack/plugins/infra/common/time/time_key.ts @@ -9,11 +9,14 @@ import { ascending, bisector } from 'd3-array'; import * as rt from 'io-ts'; import { pick } from 'lodash'; +export const minimalTimeKeyRT = rt.type({ + time: rt.number, + tiebreaker: rt.number, +}); +export type MinimalTimeKey = rt.TypeOf; + export const timeKeyRT = rt.intersection([ - rt.type({ - time: rt.number, - tiebreaker: rt.number, - }), + minimalTimeKeyRT, rt.partial({ gid: rt.string, fromAutoReload: rt.boolean, diff --git a/x-pack/plugins/infra/public/containers/logs/log_position/log_position_state.test.ts b/x-pack/plugins/infra/public/containers/logs/log_position/log_position_state.test.ts new file mode 100644 index 0000000000000..d58ef5c559bcc --- /dev/null +++ b/x-pack/plugins/infra/public/containers/logs/log_position/log_position_state.test.ts @@ -0,0 +1,129 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import moment from 'moment'; +import { createInitialLogPositionState, updateStateFromUrlState } from './log_position_state'; + +describe('function createInitialLogPositionState', () => { + it('creates a valid default state without url and timefilter', () => { + const initialState = createInitialLogPositionState({ + initialStateFromUrl: null, + initialStateFromTimefilter: null, + now: getTestMoment().toDate(), + }); + + expect(initialState).toMatchInlineSnapshot(` + Object { + "refreshInterval": Object { + "pause": true, + "value": 5000, + }, + "targetPosition": null, + "timeRange": Object { + "expression": Object { + "from": "now-1d", + "to": "now", + }, + "lastChangedCompletely": 1640995200000, + }, + "timestamps": Object { + "endTimestamp": 1640995200000, + "lastChangedTimestamp": 1640995200000, + "startTimestamp": 1640908800000, + }, + } + `); + }); +}); + +describe('function updateStateFromUrlState', () => { + it('applies a new target position that is within the date range', () => { + const initialState = createInitialTestState(); + const newState = updateStateFromUrlState({ + position: { + time: initialState.timestamps.startTimestamp + 1, + tiebreaker: 2, + }, + })(initialState); + + expect(newState).toEqual({ + ...initialState, + targetPosition: { + time: initialState.timestamps.startTimestamp + 1, + tiebreaker: 2, + }, + }); + }); + + it('applies a new partial target position that is within the date range', () => { + const initialState = createInitialTestState(); + const newState = updateStateFromUrlState({ + position: { + time: initialState.timestamps.startTimestamp + 1, + }, + })(initialState); + + expect(newState).toEqual({ + ...initialState, + targetPosition: { + time: initialState.timestamps.startTimestamp + 1, + tiebreaker: 0, + }, + }); + }); + + it('rejects a target position that is outside the date range', () => { + const initialState = createInitialTestState(); + const newState = updateStateFromUrlState({ + position: { + time: initialState.timestamps.startTimestamp - 1, + }, + })(initialState); + + expect(newState).toEqual({ + ...initialState, + targetPosition: null, + }); + }); + + it('applies a new time range and updates timestamps', () => { + const initialState = createInitialTestState(); + const updateDate = getTestMoment().add(1, 'hour').toDate(); + const newState = updateStateFromUrlState( + { + start: 'now-1d', + end: 'now+1d', + }, + updateDate + )(initialState); + + expect(newState).toEqual({ + ...initialState, + timeRange: { + expression: { + from: 'now-1d', + to: 'now+1d', + }, + lastChangedCompletely: updateDate.valueOf(), + }, + timestamps: { + startTimestamp: moment(updateDate).subtract(1, 'day').valueOf(), + endTimestamp: moment(updateDate).add(1, 'day').valueOf(), + lastChangedTimestamp: updateDate.valueOf(), + }, + }); + }); +}); + +const getTestMoment = () => moment.utc('2022-01-01T00:00:00.000Z'); + +const createInitialTestState = () => + createInitialLogPositionState({ + initialStateFromUrl: null, + initialStateFromTimefilter: null, + now: getTestMoment().toDate(), + }); diff --git a/x-pack/plugins/infra/public/containers/logs/log_position/log_position_state.ts b/x-pack/plugins/infra/public/containers/logs/log_position/log_position_state.ts index 09c7570d87089..a70fe7b28cc42 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_position/log_position_state.ts +++ b/x-pack/plugins/infra/public/containers/logs/log_position/log_position_state.ts @@ -8,10 +8,10 @@ import { RefreshInterval } from '@kbn/data-plugin/public'; import { TimeRange } from '@kbn/es-query'; import { createStateContainer, ReduxLikeStateContainer } from '@kbn/kibana-utils-plugin/public'; -import { pipe } from 'fp-ts/lib/pipeable'; +import { identity, pipe } from 'fp-ts/lib/function'; import produce from 'immer'; import logger from 'redux-logger'; -import { TimeKey } from '../../../../common/time'; +import { MinimalTimeKey, TimeKey } from '../../../../common/time'; import { datemathToEpochMillis } from '../../../utils/datemath'; import { TimefilterState } from '../../../utils/timefilter_state_storage'; import { LogPositionUrlState } from './use_log_position_url_state_sync'; @@ -33,13 +33,15 @@ export interface LogPositionState { export interface InitialLogPositionArguments { initialStateFromUrl: LogPositionUrlState | null; initialStateFromTimefilter: TimefilterState | null; + now?: Date; } export const createInitialLogPositionState = ({ initialStateFromUrl, initialStateFromTimefilter, + now, }: InitialLogPositionArguments): LogPositionState => { - const nowTimestamp = Date.now(); + const nowTimestamp = now?.valueOf() ?? Date.now(); return pipe( { @@ -52,8 +54,8 @@ export const createInitialLogPositionState = ({ lastChangedCompletely: nowTimestamp, }, timestamps: { - startTimestamp: datemathToEpochMillis('now-1d', 'down') ?? 0, - endTimestamp: datemathToEpochMillis('now', 'up') ?? 0, + startTimestamp: datemathToEpochMillis('now-1d', 'down', now) ?? 0, + endTimestamp: datemathToEpochMillis('now', 'up', now) ?? 0, lastChangedTimestamp: nowTimestamp, }, refreshInterval: { @@ -61,8 +63,11 @@ export const createInitialLogPositionState = ({ value: 5000, }, }, - updateStateFromTimefilterState(initialStateFromTimefilter), - updateStateFromUrlState(initialStateFromUrl) + initialStateFromUrl != null + ? updateStateFromUrlState(initialStateFromUrl) + : initialStateFromTimefilter != null + ? updateStateFromTimefilterState(initialStateFromTimefilter) + : identity ); }; @@ -78,13 +83,9 @@ export const createLogPositionStateContainer = (initialArguments: InitialLogPosi stopLiveStreaming: (state: LogPositionState) => () => updateRefreshInterval({ pause: true })(state), jumpToTargetPosition: (state: LogPositionState) => (targetPosition: TimeKey | null) => - produce(state, (draftState) => { - draftState.targetPosition = targetPosition; - }), + updateTargetPosition(targetPosition)(state), jumpToTargetPositionTime: (state: LogPositionState) => (time: number) => - produce(state, (draftState) => { - draftState.targetPosition = { tiebreaker: 0, time }; - }), + updateTargetPosition({ time })(state), }); export const withLogger = >( @@ -94,16 +95,28 @@ export const withLogger = >( return stateContainer; }; -export const updateTimeRange = (timeRange: Partial) => +const updateTargetPosition = (targetPosition: Partial | null) => + produce((draftState) => { + if (targetPosition?.time != null) { + draftState.targetPosition = { + time: targetPosition.time, + tiebreaker: targetPosition.tiebreaker ?? 0, + }; + } else { + draftState.targetPosition = null; + } + }); + +const updateTimeRange = (timeRange: Partial, now?: Date) => produce((draftState) => { const newFrom = timeRange?.from; const newTo = timeRange?.to; - const nowTimestamp = Date.now(); + const nowTimestamp = now?.valueOf() ?? Date.now(); // Update expression and timestamps if (newFrom != null) { draftState.timeRange.expression.from = newFrom; - const newStartTimestamp = datemathToEpochMillis(newFrom, 'down'); + const newStartTimestamp = datemathToEpochMillis(newFrom, 'down', now); if (newStartTimestamp != null) { draftState.timestamps.startTimestamp = newStartTimestamp; draftState.timestamps.lastChangedTimestamp = nowTimestamp; @@ -111,7 +124,7 @@ export const updateTimeRange = (timeRange: Partial) => } if (newTo != null) { draftState.timeRange.expression.to = newTo; - const newEndTimestamp = datemathToEpochMillis(newTo, 'up'); + const newEndTimestamp = datemathToEpochMillis(newTo, 'up', now); if (newEndTimestamp != null) { draftState.timestamps.endTimestamp = newEndTimestamp; draftState.timestamps.lastChangedTimestamp = nowTimestamp; @@ -131,7 +144,7 @@ export const updateTimeRange = (timeRange: Partial) => } }); -export const updateRefreshInterval = +const updateRefreshInterval = (refreshInterval: Partial) => (state: LogPositionState) => pipe( state, @@ -164,14 +177,18 @@ export const getUrlState = (state: LogPositionState): LogPositionUrlState => ({ }); export const updateStateFromUrlState = - (urlState: LogPositionUrlState | null) => + (urlState: LogPositionUrlState | null, now?: Date) => (state: LogPositionState): LogPositionState => pipe( state, - updateTimeRange({ - from: urlState?.start, - to: urlState?.end, - }), + updateTargetPosition(urlState?.position ?? null), + updateTimeRange( + { + from: urlState?.start, + to: urlState?.end, + }, + now + ), updateRefreshInterval({ pause: !urlState?.streamLive }) ); diff --git a/x-pack/plugins/infra/public/containers/logs/log_position/use_log_position_url_state_sync.ts b/x-pack/plugins/infra/public/containers/logs/log_position/use_log_position_url_state_sync.ts index 7b01d54639657..daa0858089a67 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_position/use_log_position_url_state_sync.ts +++ b/x-pack/plugins/infra/public/containers/logs/log_position/use_log_position_url_state_sync.ts @@ -11,20 +11,16 @@ import { pipe } from 'fp-ts/lib/pipeable'; import * as rt from 'io-ts'; import { useCallback, useState } from 'react'; import { map } from 'rxjs/operators'; -import { timeKeyRT } from '../../../../common/time'; +import { minimalTimeKeyRT } from '../../../../common/time'; import { datemathStringRT } from '../../../utils/datemath'; import { useKbnUrlStateStorageFromRouterContext } from '../../../utils/kbn_url_state_context'; -export const logPositionUrlStateRT = rt.intersection([ - rt.type({ - streamLive: rt.boolean, - }), - rt.partial({ - position: rt.union([timeKeyRT, rt.null]), - start: datemathStringRT, - end: datemathStringRT, - }), -]); +export const logPositionUrlStateRT = rt.partial({ + streamLive: rt.boolean, + position: rt.union([rt.partial(minimalTimeKeyRT.props), rt.null]), + start: datemathStringRT, + end: datemathStringRT, +}); export type LogPositionUrlState = rt.TypeOf; diff --git a/x-pack/plugins/infra/public/utils/datemath.ts b/x-pack/plugins/infra/public/utils/datemath.ts index 0780d4e906be6..68c77e3c0e7ed 100644 --- a/x-pack/plugins/infra/public/utils/datemath.ts +++ b/x-pack/plugins/infra/public/utils/datemath.ts @@ -30,8 +30,12 @@ export const datemathStringRT = new rt.Type( String ); -export function datemathToEpochMillis(value: string, round: 'down' | 'up' = 'down'): number | null { - const parsedValue = dateMath.parse(value, { roundUp: round === 'up' }); +export function datemathToEpochMillis( + value: string, + round: 'down' | 'up' = 'down', + forceNow?: Date +): number | null { + const parsedValue = dateMath.parse(value, { roundUp: round === 'up', forceNow }); if (!parsedValue || !parsedValue.isValid()) { return null; } From 94dab453eee879d0de85e4a8c153a630f2e118d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20St=C3=BCrmer?= Date: Thu, 3 Nov 2022 18:37:40 +0000 Subject: [PATCH 12/21] Only log state changes in development --- .../containers/logs/log_position/log_position_state.ts | 7 +++++-- .../containers/logs/log_position/use_log_position.ts | 4 ++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/infra/public/containers/logs/log_position/log_position_state.ts b/x-pack/plugins/infra/public/containers/logs/log_position/log_position_state.ts index a70fe7b28cc42..82b286b43f5e1 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_position/log_position_state.ts +++ b/x-pack/plugins/infra/public/containers/logs/log_position/log_position_state.ts @@ -88,10 +88,13 @@ export const createLogPositionStateContainer = (initialArguments: InitialLogPosi updateTargetPosition({ time })(state), }); -export const withLogger = >( +export const withDevelopmentLogger = >( stateContainer: StateContainer ): StateContainer => { - stateContainer.addMiddleware(logger as any); + if (process.env.NODE_ENV !== 'production') { + stateContainer.addMiddleware(logger as any); + } + return stateContainer; }; diff --git a/x-pack/plugins/infra/public/containers/logs/log_position/use_log_position.ts b/x-pack/plugins/infra/public/containers/logs/log_position/use_log_position.ts index 0dcae09bcd47b..c84bb760faad7 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_position/use_log_position.ts +++ b/x-pack/plugins/infra/public/containers/logs/log_position/use_log_position.ts @@ -20,7 +20,7 @@ import { LogPositionState, updateStateFromTimefilterState, updateStateFromUrlState, - withLogger, + withDevelopmentLogger, } from './log_position_state'; import { useLogPositionTimefilterStateSync } from './log_position_timefilter_state'; import { LogPositionUrlState, useLogPositionUrlStateSync } from './use_log_position_url_state_sync'; @@ -77,7 +77,7 @@ export const useLogPositionState: () => LogPositionStateParams & LogPositionCall useLogPositionTimefilterStateSync(); const [logPositionStateContainer] = useState(() => - withLogger( + withDevelopmentLogger( createLogPositionStateContainer({ initialStateFromUrl, initialStateFromTimefilter, From 7f58b734d7db4e592283232ce5262a8ad0968c5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20St=C3=BCrmer?= Date: Thu, 3 Nov 2022 18:38:39 +0000 Subject: [PATCH 13/21] Move visible positions into state container --- x-pack/plugins/infra/common/time/time_key.ts | 4 + .../logs/log_position/log_position_state.ts | 151 +++++++++++++++--- .../logs/log_position/use_log_position.ts | 68 ++------ 3 files changed, 142 insertions(+), 81 deletions(-) diff --git a/x-pack/plugins/infra/common/time/time_key.ts b/x-pack/plugins/infra/common/time/time_key.ts index 1f0f242fd7496..efc5a8e7b8517 100644 --- a/x-pack/plugins/infra/common/time/time_key.ts +++ b/x-pack/plugins/infra/common/time/time_key.ts @@ -104,3 +104,7 @@ export const getNextTimeKey = (timeKey: TimeKey) => ({ time: timeKey.time, tiebreaker: timeKey.tiebreaker + 1, }); + +export const isSameTimeKey = (firstKey: TimeKey | null, secondKey: TimeKey | null): boolean => + firstKey === secondKey || + (firstKey != null && secondKey != null && compareTimeKeys(firstKey, secondKey) === 0); diff --git a/x-pack/plugins/infra/public/containers/logs/log_position/log_position_state.ts b/x-pack/plugins/infra/public/containers/logs/log_position/log_position_state.ts index 82b286b43f5e1..913f551b014aa 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_position/log_position_state.ts +++ b/x-pack/plugins/infra/public/containers/logs/log_position/log_position_state.ts @@ -9,15 +9,23 @@ import { RefreshInterval } from '@kbn/data-plugin/public'; import { TimeRange } from '@kbn/es-query'; import { createStateContainer, ReduxLikeStateContainer } from '@kbn/kibana-utils-plugin/public'; import { identity, pipe } from 'fp-ts/lib/function'; -import produce from 'immer'; +import produce, { Draft, original } from 'immer'; +import moment, { DurationInputObject } from 'moment'; import logger from 'redux-logger'; -import { MinimalTimeKey, TimeKey } from '../../../../common/time'; +import { isSameTimeKey, MinimalTimeKey, pickTimeKey, TimeKey } from '../../../../common/time'; import { datemathToEpochMillis } from '../../../utils/datemath'; import { TimefilterState } from '../../../utils/timefilter_state_storage'; import { LogPositionUrlState } from './use_log_position_url_state_sync'; +interface VisiblePositions { + startKey: TimeKey | null; + middleKey: TimeKey | null; + endKey: TimeKey | null; + pagesAfterEnd: number; + pagesBeforeStart: number; +} + export interface LogPositionState { - targetPosition: TimeKey | null; timeRange: { expression: TimeRange; lastChangedCompletely: number; @@ -28,6 +36,9 @@ export interface LogPositionState { lastChangedTimestamp: number; }; refreshInterval: RefreshInterval; + latestPosition: TimeKey | null; + targetPosition: TimeKey | null; + visiblePositions: VisiblePositions; } export interface InitialLogPositionArguments { @@ -36,6 +47,28 @@ export interface InitialLogPositionArguments { now?: Date; } +/** + * Initial state + */ + +const initialTimeRangeExpression: TimeRange = { + from: 'now-1d', + to: 'now', +}; + +const initialRefreshInterval: RefreshInterval = { + pause: true, + value: 5000, +}; + +const initialVisiblePositions: VisiblePositions = { + endKey: null, + middleKey: null, + startKey: null, + pagesBeforeStart: Infinity, + pagesAfterEnd: Infinity, +}; + export const createInitialLogPositionState = ({ initialStateFromUrl, initialStateFromTimefilter, @@ -45,28 +78,24 @@ export const createInitialLogPositionState = ({ return pipe( { - targetPosition: null, timeRange: { - expression: { - from: 'now-1d', - to: 'now', - }, + expression: initialTimeRangeExpression, lastChangedCompletely: nowTimestamp, }, timestamps: { - startTimestamp: datemathToEpochMillis('now-1d', 'down', now) ?? 0, - endTimestamp: datemathToEpochMillis('now', 'up', now) ?? 0, + startTimestamp: datemathToEpochMillis(initialTimeRangeExpression.from, 'down', now) ?? 0, + endTimestamp: datemathToEpochMillis(initialTimeRangeExpression.to, 'up', now) ?? 0, lastChangedTimestamp: nowTimestamp, }, - refreshInterval: { - pause: true, - value: 5000, - }, + refreshInterval: initialRefreshInterval, + targetPosition: null, + latestPosition: null, + visiblePositions: initialVisiblePositions, }, initialStateFromUrl != null - ? updateStateFromUrlState(initialStateFromUrl) + ? initializeStateFromUrlState(initialStateFromUrl, now) : initialStateFromTimefilter != null - ? updateStateFromTimefilterState(initialStateFromTimefilter) + ? updateStateFromTimefilterState(initialStateFromTimefilter, now) : identity ); }; @@ -86,6 +115,8 @@ export const createLogPositionStateContainer = (initialArguments: InitialLogPosi updateTargetPosition(targetPosition)(state), jumpToTargetPositionTime: (state: LogPositionState) => (time: number) => updateTargetPosition({ time })(state), + reportVisiblePositions: (state: LogPositionState) => (visiblePositions: VisiblePositions) => + updateVisiblePositions(visiblePositions)(state), }); export const withDevelopmentLogger = >( @@ -98,6 +129,17 @@ export const withDevelopmentLogger = + produce((draftState) => { + draftState.visiblePositions = visiblePositions; + + updateLatestPositionDraft(draftState); + }); + const updateTargetPosition = (targetPosition: Partial | null) => produce((draftState) => { if (targetPosition?.time != null) { @@ -108,8 +150,22 @@ const updateTargetPosition = (targetPosition: Partial | null) => } else { draftState.targetPosition = null; } + + updateLatestPositionDraft(draftState); }); +const updateLatestPositionDraft = (draftState: Draft) => { + const previousState = original(draftState); + const previousVisibleMiddleKey = previousState?.visiblePositions?.middleKey ?? null; + const previousTargetPosition = previousState?.targetPosition ?? null; + + if (!isSameTimeKey(previousVisibleMiddleKey, draftState.visiblePositions.middleKey)) { + draftState.latestPosition = draftState.visiblePositions.middleKey; + } else if (!isSameTimeKey(previousTargetPosition, draftState.targetPosition)) { + draftState.latestPosition = draftState.targetPosition; + } +}; + const updateTimeRange = (timeRange: Partial, now?: Date) => produce((draftState) => { const newFrom = timeRange?.from; @@ -144,6 +200,8 @@ const updateTimeRange = (timeRange: Partial, now?: Date) => draftState.timestamps.endTimestamp < draftState.targetPosition.time) ) { draftState.targetPosition = null; + + updateLatestPositionDraft(draftState); } }); @@ -161,24 +219,46 @@ const updateRefreshInterval = if (!draftState.refreshInterval.pause) { draftState.targetPosition = null; + + updateLatestPositionDraft(draftState); } }), (currentState) => { if (!currentState.refreshInterval.pause) { - return updateTimeRange({ from: 'now-1d', to: 'now' })(currentState); + return updateTimeRange(initialTimeRangeExpression)(currentState); } else { return currentState; } } ); +/** + * URL state helpers + */ + export const getUrlState = (state: LogPositionState): LogPositionUrlState => ({ streamLive: !state.refreshInterval.pause, start: state.timeRange.expression.from, end: state.timeRange.expression.to, - position: state.targetPosition, + position: state.latestPosition ? pickTimeKey(state.latestPosition) : null, }); +export const initializeStateFromUrlState = + (urlState: LogPositionUrlState | null, now?: Date) => + (state: LogPositionState): LogPositionState => + pipe( + state, + updateTargetPosition(urlState?.position ?? null), + updateTimeRange( + { + from: urlState?.start ?? getTimeRangeStartFromPosition(urlState?.position), + to: urlState?.end ?? getTimeRangeEndFromPosition(urlState?.position), + }, + now + ), + updateRefreshInterval({ pause: !urlState?.streamLive }) + ); + export const updateStateFromUrlState = (urlState: LogPositionUrlState | null, now?: Date) => (state: LogPositionState): LogPositionState => @@ -195,22 +275,45 @@ export const updateStateFromUrlState = updateRefreshInterval({ pause: !urlState?.streamLive }) ); +/** + * Timefilter helpers + */ + export const getTimefilterState = (state: LogPositionState): TimefilterState => ({ timeRange: state.timeRange.expression, refreshInterval: state.refreshInterval, }); export const updateStateFromTimefilterState = - (timefilterState: TimefilterState | null) => + (timefilterState: TimefilterState | null, now?: Date) => (state: LogPositionState): LogPositionState => pipe( state, - updateTimeRange({ - from: timefilterState?.timeRange?.from, - to: timefilterState?.timeRange?.to, - }), + updateTimeRange( + { + from: timefilterState?.timeRange?.from, + to: timefilterState?.timeRange?.to, + }, + now + ), updateRefreshInterval({ pause: timefilterState?.refreshInterval?.pause, - value: timefilterState?.refreshInterval?.value, + value: Math.max(timefilterState?.refreshInterval?.value ?? 0, initialRefreshInterval.value), }) ); + +const defaultTimeRangeFromPositionOffset: DurationInputObject = { hours: 1 }; + +const getTimeRangeStartFromPosition = ( + position: Partial | null | undefined +): string | undefined => + position?.time != null + ? moment(position.time).subtract(defaultTimeRangeFromPositionOffset).toISOString() + : undefined; + +const getTimeRangeEndFromPosition = ( + position: Partial | null | undefined +): string | undefined => + position?.time != null + ? moment(position.time).add(defaultTimeRangeFromPositionOffset).toISOString() + : undefined; diff --git a/x-pack/plugins/infra/public/containers/logs/log_position/use_log_position.ts b/x-pack/plugins/infra/public/containers/logs/log_position/use_log_position.ts index c84bb760faad7..070e36177767c 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_position/use_log_position.ts +++ b/x-pack/plugins/infra/public/containers/logs/log_position/use_log_position.ts @@ -113,10 +113,7 @@ export const useLogPositionState: () => LogPositionStateParams & LogPositionCall [latestLogPositionState] ); - const targetPosition = useMemo( - () => latestLogPositionState.targetPosition, - [latestLogPositionState] - ); + const { targetPosition, visiblePositions } = latestLogPositionState; const isStreaming = useMemo( () => !latestLogPositionState.refreshInterval.pause, @@ -132,8 +129,13 @@ export const useLogPositionState: () => LogPositionStateParams & LogPositionCall [logPositionStateContainer] ); - const { reportVisiblePositions, visibleMidpoint, visiblePositions, visibleTimeInterval } = - useVisiblePositions(targetPosition); + const visibleTimeInterval = useMemo( + () => + visiblePositions.startKey && visiblePositions.endKey + ? { start: visiblePositions.startKey.time, end: visiblePositions.endKey.time } + : null, + [visiblePositions.startKey, visiblePositions.endKey] + ); // `endTimestamp` update conditions const throttledPagesAfterEnd = useThrottle( @@ -169,14 +171,14 @@ export const useLogPositionState: () => LogPositionStateParams & LogPositionCall firstVisiblePosition: visiblePositions.startKey, pagesBeforeStart: visiblePositions.pagesBeforeStart, pagesAfterEnd: visiblePositions.pagesAfterEnd, - visibleMidpoint, - visibleMidpointTime: visibleMidpoint ? visibleMidpoint.time : null, + visibleMidpoint: latestLogPositionState.latestPosition, + visibleMidpointTime: latestLogPositionState.latestPosition?.time ?? null, visibleTimeInterval, // actions jumpToTargetPosition: logPositionStateContainer.transitions.jumpToTargetPosition, jumpToTargetPositionTime: logPositionStateContainer.transitions.jumpToTargetPositionTime, - reportVisiblePositions, + reportVisiblePositions: logPositionStateContainer.transitions.reportVisiblePositions, startLiveStreaming: logPositionStateContainer.transitions.startLiveStreaming, stopLiveStreaming: logPositionStateContainer.transitions.stopLiveStreaming, updateDateRange, @@ -186,54 +188,6 @@ export const useLogPositionState: () => LogPositionStateParams & LogPositionCall export const [LogPositionStateProvider, useLogPositionStateContext] = createContainer(useLogPositionState); -const useVisiblePositions = (targetPosition: TimeKeyOrNull) => { - const [visiblePositions, reportVisiblePositions] = useState({ - endKey: null, - middleKey: null, - startKey: null, - pagesBeforeStart: Infinity, - pagesAfterEnd: Infinity, - }); - - const { startKey, middleKey, endKey } = visiblePositions; - - const visibleMidpoint = useVisibleMidpoint(middleKey, targetPosition); - - const visibleTimeInterval = useMemo( - () => (startKey && endKey ? { start: startKey.time, end: endKey.time } : null), - [startKey, endKey] - ); - - return { - reportVisiblePositions, - visibleMidpoint, - visiblePositions, - visibleTimeInterval, - }; -}; - -const useVisibleMidpoint = (middleKey: TimeKeyOrNull, targetPosition: TimeKeyOrNull) => { - // Of the two dependencies `middleKey` and `targetPosition`, return - // whichever one was the most recently updated. This allows the UI controls - // to display a newly-selected `targetPosition` before loading new data; - // otherwise the previous `middleKey` would linger in the UI for the entirety - // of the loading operation, which the user could perceive as unresponsiveness - const [store, update] = useState({ - middleKey, - targetPosition, - currentValue: middleKey || targetPosition, - }); - useEffect(() => { - if (middleKey !== store.middleKey) { - update({ targetPosition, middleKey, currentValue: middleKey }); - } else if (targetPosition !== store.targetPosition) { - update({ targetPosition, middleKey, currentValue: targetPosition }); - } - }, [middleKey, targetPosition]); // eslint-disable-line react-hooks/exhaustive-deps - - return store.currentValue; -}; - const getLegacyDateRange = (logPositionState: LogPositionState): DateRange => ({ endDateExpression: logPositionState.timeRange.expression.to, endTimestamp: logPositionState.timestamps.endTimestamp, From 3f3ff7622fdaaace674163c91648d799708582d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20St=C3=BCrmer?= Date: Thu, 3 Nov 2022 18:39:35 +0000 Subject: [PATCH 14/21] Add some log position state unit tests --- .../log_position/log_position_state.test.ts | 172 +++++++++++++++++- 1 file changed, 165 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/infra/public/containers/logs/log_position/log_position_state.test.ts b/x-pack/plugins/infra/public/containers/logs/log_position/log_position_state.test.ts index d58ef5c559bcc..b87dca28fc048 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_position/log_position_state.test.ts +++ b/x-pack/plugins/infra/public/containers/logs/log_position/log_position_state.test.ts @@ -9,7 +9,7 @@ import moment from 'moment'; import { createInitialLogPositionState, updateStateFromUrlState } from './log_position_state'; describe('function createInitialLogPositionState', () => { - it('creates a valid default state without url and timefilter', () => { + it('initializes state without url and timefilter', () => { const initialState = createInitialLogPositionState({ initialStateFromUrl: null, initialStateFromTimefilter: null, @@ -18,6 +18,7 @@ describe('function createInitialLogPositionState', () => { expect(initialState).toMatchInlineSnapshot(` Object { + "latestPosition": null, "refreshInterval": Object { "pause": true, "value": 5000, @@ -35,6 +36,154 @@ describe('function createInitialLogPositionState', () => { "lastChangedTimestamp": 1640995200000, "startTimestamp": 1640908800000, }, + "visiblePositions": Object { + "endKey": null, + "middleKey": null, + "pagesAfterEnd": Infinity, + "pagesBeforeStart": Infinity, + "startKey": null, + }, + } + `); + }); + + it('initializes state from complete url state', () => { + const initialState = createInitialLogPositionState({ + initialStateFromUrl: { + start: 'now-2d', + end: 'now-1d', + position: { + time: getTestMoment().subtract(36, 'hours').valueOf(), + tiebreaker: 0, + }, + streamLive: false, + }, + initialStateFromTimefilter: null, + now: getTestMoment().toDate(), + }); + + expect(initialState).toMatchInlineSnapshot(` + Object { + "latestPosition": Object { + "tiebreaker": 0, + "time": 1640865600000, + }, + "refreshInterval": Object { + "pause": true, + "value": 5000, + }, + "targetPosition": Object { + "tiebreaker": 0, + "time": 1640865600000, + }, + "timeRange": Object { + "expression": Object { + "from": "now-2d", + "to": "now-1d", + }, + "lastChangedCompletely": 1640995200000, + }, + "timestamps": Object { + "endTimestamp": 1640908800000, + "lastChangedTimestamp": 1640995200000, + "startTimestamp": 1640822400000, + }, + "visiblePositions": Object { + "endKey": null, + "middleKey": null, + "pagesAfterEnd": Infinity, + "pagesBeforeStart": Infinity, + "startKey": null, + }, + } + `); + }); + + it('initializes state from from url state with just a time range', () => { + const initialState = createInitialLogPositionState({ + initialStateFromUrl: { + start: 'now-2d', + end: 'now-1d', + }, + initialStateFromTimefilter: null, + now: getTestMoment().toDate(), + }); + + expect(initialState).toMatchInlineSnapshot(` + Object { + "latestPosition": null, + "refreshInterval": Object { + "pause": true, + "value": 5000, + }, + "targetPosition": null, + "timeRange": Object { + "expression": Object { + "from": "now-2d", + "to": "now-1d", + }, + "lastChangedCompletely": 1640995200000, + }, + "timestamps": Object { + "endTimestamp": 1640908800000, + "lastChangedTimestamp": 1640995200000, + "startTimestamp": 1640822400000, + }, + "visiblePositions": Object { + "endKey": null, + "middleKey": null, + "pagesAfterEnd": Infinity, + "pagesBeforeStart": Infinity, + "startKey": null, + }, + } + `); + }); + + it('initializes state from from url state with just a position', () => { + const initialState = createInitialLogPositionState({ + initialStateFromUrl: { + position: { + time: getTestMoment().subtract(36, 'hours').valueOf(), + }, + }, + initialStateFromTimefilter: null, + now: getTestMoment().toDate(), + }); + + expect(initialState).toMatchInlineSnapshot(` + Object { + "latestPosition": Object { + "tiebreaker": 0, + "time": 1640865600000, + }, + "refreshInterval": Object { + "pause": true, + "value": 5000, + }, + "targetPosition": Object { + "tiebreaker": 0, + "time": 1640865600000, + }, + "timeRange": Object { + "expression": Object { + "from": "2021-12-30T11:00:00.000Z", + "to": "2021-12-30T13:00:00.000Z", + }, + "lastChangedCompletely": 1640995200000, + }, + "timestamps": Object { + "endTimestamp": 1640869200000, + "lastChangedTimestamp": 1640995200000, + "startTimestamp": 1640862000000, + }, + "visiblePositions": Object { + "endKey": null, + "middleKey": null, + "pagesAfterEnd": Infinity, + "pagesBeforeStart": Infinity, + "startKey": null, + }, } `); }); @@ -56,6 +205,10 @@ describe('function updateStateFromUrlState', () => { time: initialState.timestamps.startTimestamp + 1, tiebreaker: 2, }, + latestPosition: { + time: initialState.timestamps.startTimestamp + 1, + tiebreaker: 2, + }, }); }); @@ -73,6 +226,10 @@ describe('function updateStateFromUrlState', () => { time: initialState.timestamps.startTimestamp + 1, tiebreaker: 0, }, + latestPosition: { + time: initialState.timestamps.startTimestamp + 1, + tiebreaker: 0, + }, }); }); @@ -87,6 +244,7 @@ describe('function updateStateFromUrlState', () => { expect(newState).toEqual({ ...initialState, targetPosition: null, + latestPosition: null, }); }); @@ -95,8 +253,8 @@ describe('function updateStateFromUrlState', () => { const updateDate = getTestMoment().add(1, 'hour').toDate(); const newState = updateStateFromUrlState( { - start: 'now-1d', - end: 'now+1d', + start: 'now-2d', + end: 'now-1d', }, updateDate )(initialState); @@ -105,14 +263,14 @@ describe('function updateStateFromUrlState', () => { ...initialState, timeRange: { expression: { - from: 'now-1d', - to: 'now+1d', + from: 'now-2d', + to: 'now-1d', }, lastChangedCompletely: updateDate.valueOf(), }, timestamps: { - startTimestamp: moment(updateDate).subtract(1, 'day').valueOf(), - endTimestamp: moment(updateDate).add(1, 'day').valueOf(), + startTimestamp: moment(updateDate).subtract(2, 'day').valueOf(), + endTimestamp: moment(updateDate).subtract(1, 'day').valueOf(), lastChangedTimestamp: updateDate.valueOf(), }, }); From e3a78df0a3ccbc4a0a1df5e4007cde9222de7711 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20St=C3=BCrmer?= Date: Thu, 3 Nov 2022 18:39:52 +0000 Subject: [PATCH 15/21] Avoid creating a new history entry on scrolling --- .../logs/log_position/use_log_position_url_state_sync.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/infra/public/containers/logs/log_position/use_log_position_url_state_sync.ts b/x-pack/plugins/infra/public/containers/logs/log_position/use_log_position_url_state_sync.ts index daa0858089a67..b9e6a8a5b3eb6 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_position/use_log_position_url_state_sync.ts +++ b/x-pack/plugins/infra/public/containers/logs/log_position/use_log_position_url_state_sync.ts @@ -57,7 +57,11 @@ export const useLogPositionUrlStateSync = () => { ), get: () => logPositionUrlStateRT.encode(stateContainer.get()), }, - stateStorage: urlStateStorage, + stateStorage: { + ...urlStateStorage, + set: (key: string, state: State) => + urlStateStorage.set(key, state, { replace: true }), + }, }); start(); From 00871799959e8cac9299ce9fb0823fed7e390626 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20St=C3=BCrmer?= Date: Fri, 4 Nov 2022 12:43:57 +0000 Subject: [PATCH 16/21] Make page title test more reliable --- .../test/functional/apps/infra/logs_source_configuration.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/x-pack/test/functional/apps/infra/logs_source_configuration.ts b/x-pack/test/functional/apps/infra/logs_source_configuration.ts index 38cc795034a22..24b7a538d77f6 100644 --- a/x-pack/test/functional/apps/infra/logs_source_configuration.ts +++ b/x-pack/test/functional/apps/infra/logs_source_configuration.ts @@ -54,9 +54,11 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await pageObjects.infraLogs.navigateToTab('settings'); await pageObjects.header.waitUntilLoadingHasFinished(); - const documentTitle = await browser.getTitle(); - expect(documentTitle).to.contain('Settings - Logs - Observability - Elastic'); + retry.try(async () => { + const documentTitle = await browser.getTitle(); + expect(documentTitle).to.contain('Settings - Logs - Observability - Elastic'); + }); }); it('can change the log indices to a pattern that matches nothing', async () => { From 5bbfdfe4ef35b896eb36ba3a872e0598c8f207d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20St=C3=BCrmer?= Date: Fri, 4 Nov 2022 15:40:46 +0000 Subject: [PATCH 17/21] Decouple alerts page tests from log stream state --- .../shared/page_template/page_template.tsx | 4 +- .../page_objects/observability_page.ts | 9 ++++ .../pages/alerts/state_synchronization.ts | 45 ++++++++++++------- 3 files changed, 40 insertions(+), 18 deletions(-) diff --git a/x-pack/plugins/observability/public/components/shared/page_template/page_template.tsx b/x-pack/plugins/observability/public/components/shared/page_template/page_template.tsx index aad9aee7af23f..47a6649e098cd 100644 --- a/x-pack/plugins/observability/public/components/shared/page_template/page_template.tsx +++ b/x-pack/plugins/observability/public/components/shared/page_template/page_template.tsx @@ -98,6 +98,7 @@ export function ObservabilityPageTemplate({ strict: !entry.ignoreTrailingSlash, }) != null); const badgeLocalStorageId = `observability.nav_item_badge_visible_${entry.app}${entry.path}`; + const navId = entry.label.toLowerCase().split(' ').join('_'); return { id: `${sectionIndex}.${entryIndex}`, name: entry.isNewFeature ? ( @@ -107,7 +108,8 @@ export function ObservabilityPageTemplate({ ), href, isSelected, - 'data-nav-id': entry.label.toLowerCase().split(' ').join('_'), + 'data-nav-id': navId, + 'data-test-subj': `observability-nav-${entry.app}-${navId}`, onClick: (event) => { if (entry.onClick) { entry.onClick(event); diff --git a/x-pack/test/functional/page_objects/observability_page.ts b/x-pack/test/functional/page_objects/observability_page.ts index 07cceca4be122..0177939ec3d15 100644 --- a/x-pack/test/functional/page_objects/observability_page.ts +++ b/x-pack/test/functional/page_objects/observability_page.ts @@ -13,6 +13,10 @@ export function ObservabilityPageProvider({ getService, getPageObjects }: FtrPro const testSubjects = getService('testSubjects'); return { + async clickSolutionNavigationEntry(appId: string, navId: string) { + await testSubjects.click(`observability-nav-${appId}-${navId}`); + }, + async expectCreateCaseButtonEnabled() { const button = await testSubjects.find('createNewCaseBtn', 20000); const disabledAttr = await button.getAttribute('disabled'); @@ -54,5 +58,10 @@ export function ObservabilityPageProvider({ getService, getPageObjects }: FtrPro const text = await h2.getVisibleText(); expect(text).to.contain('Kibana feature privileges required'); }, + + async getDatePickerRangeText() { + const datePickerButton = await testSubjects.find('superDatePickerShowDatesButton'); + return await datePickerButton.getVisibleText(); + }, }; } diff --git a/x-pack/test/observability_functional/apps/observability/pages/alerts/state_synchronization.ts b/x-pack/test/observability_functional/apps/observability/pages/alerts/state_synchronization.ts index fe9751dc9c738..8f4bcbb237620 100644 --- a/x-pack/test/observability_functional/apps/observability/pages/alerts/state_synchronization.ts +++ b/x-pack/test/observability_functional/apps/observability/pages/alerts/state_synchronization.ts @@ -17,7 +17,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const find = getService('find'); const testSubjects = getService('testSubjects'); const observability = getService('observability'); - const pageObjects = getPageObjects(['common']); + const pageObjects = getPageObjects(['common', 'observability', 'timePicker']); before(async () => { await esArchiver.load('x-pack/test/functional/es_archives/observability/alerts'); @@ -45,9 +45,14 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); it('should not sync URL state to shared time range on page load ', async () => { - await (await find.byLinkText('Stream')).click(); + await pageObjects.observability.clickSolutionNavigationEntry( + 'observability-overview', + 'overview' + ); + + const observabilityPageDateRange = await pageObjects.observability.getDatePickerRangeText(); - await assertLogsStreamPageTimeRange('Last 1 day'); + expect(observabilityPageDateRange).to.be('Last 15 minutes'); }); it('should apply defaults if URL state is missing', async () => { @@ -61,18 +66,29 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); it('should use shared time range if set', async () => { - await (await find.byLinkText('Stream')).click(); + await pageObjects.observability.clickSolutionNavigationEntry( + 'observability-overview', + 'overview' + ); await setTimeRangeToXDaysAgo(10); - await (await find.byLinkText('Alerts')).click(); + await pageObjects.observability.clickSolutionNavigationEntry( + 'observability-overview', + 'alerts' + ); expect(await observability.alerts.common.getTimeRange()).to.be('Last 10 days'); }); it('should set the shared time range', async () => { await setTimeRangeToXDaysAgo(100); - await (await find.byLinkText('Stream')).click(); + await pageObjects.observability.clickSolutionNavigationEntry( + 'observability-overview', + 'overview' + ); + + const observabilityPageDateRange = await pageObjects.observability.getDatePickerRangeText(); - await assertLogsStreamPageTimeRange('Last 100 days'); + expect(observabilityPageDateRange).to.be('Last 100 days'); }); async function assertAlertsPageState(expected: { @@ -90,18 +106,13 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { expect(timeRange).to.be(expected.timeRange); } - async function assertLogsStreamPageTimeRange(expected: string) { - // Only handles relative time ranges - const datePickerButton = await testSubjects.find('superDatePickerShowDatesButton'); - const timerange = await datePickerButton.getVisibleText(); - expect(timerange).to.be(expected); - } - async function setTimeRangeToXDaysAgo(numberOfDays: number) { await (await testSubjects.find('superDatePickerToggleQuickMenuButton')).click(); - const numerOfDaysField = await find.byCssSelector('[aria-label="Time value"]'); - await numerOfDaysField.clearValueWithKeyboard(); - await numerOfDaysField.type(numberOfDays.toString()); + const numberField = await find.byCssSelector('[aria-label="Time value"]'); + await numberField.clearValueWithKeyboard(); + await numberField.type(numberOfDays.toString()); + const unitField = await find.byCssSelector('[aria-label="Time unit"]'); + await unitField.type('Days'); await find.clickByButtonText('Apply'); } }); From 34242ea58a65d785fa169ead60904676178d8c3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20St=C3=BCrmer?= Date: Fri, 4 Nov 2022 16:19:34 +0000 Subject: [PATCH 18/21] Avoid resetting the refresh interval --- .../public/containers/logs/log_position/log_position_state.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/infra/public/containers/logs/log_position/log_position_state.ts b/x-pack/plugins/infra/public/containers/logs/log_position/log_position_state.ts index 913f551b014aa..ef08b044652be 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_position/log_position_state.ts +++ b/x-pack/plugins/infra/public/containers/logs/log_position/log_position_state.ts @@ -108,7 +108,7 @@ export const createLogPositionStateContainer = (initialArguments: InitialLogPosi (state: LogPositionState) => (refreshInterval: Partial) => updateRefreshInterval(refreshInterval)(state), startLiveStreaming: (state: LogPositionState) => () => - updateRefreshInterval({ pause: false, value: 5000 })(state), + updateRefreshInterval({ pause: false })(state), stopLiveStreaming: (state: LogPositionState) => () => updateRefreshInterval({ pause: true })(state), jumpToTargetPosition: (state: LogPositionState) => (targetPosition: TimeKey | null) => From 325103cff669f7b31d5b5079f7001df7c77d6531 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20St=C3=BCrmer?= Date: Fri, 4 Nov 2022 17:40:10 +0000 Subject: [PATCH 19/21] Use redux devtools instead of redux-logger --- .../logs/log_position/log_position_state.ts | 13 +-------- .../logs/log_position/use_log_position.ts | 9 ++++-- .../public/utils/state_container_devtools.ts | 29 +++++++++++++++++++ 3 files changed, 36 insertions(+), 15 deletions(-) create mode 100644 x-pack/plugins/infra/public/utils/state_container_devtools.ts diff --git a/x-pack/plugins/infra/public/containers/logs/log_position/log_position_state.ts b/x-pack/plugins/infra/public/containers/logs/log_position/log_position_state.ts index ef08b044652be..cd5f11346c56a 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_position/log_position_state.ts +++ b/x-pack/plugins/infra/public/containers/logs/log_position/log_position_state.ts @@ -7,11 +7,10 @@ import { RefreshInterval } from '@kbn/data-plugin/public'; import { TimeRange } from '@kbn/es-query'; -import { createStateContainer, ReduxLikeStateContainer } from '@kbn/kibana-utils-plugin/public'; +import { createStateContainer } from '@kbn/kibana-utils-plugin/public'; import { identity, pipe } from 'fp-ts/lib/function'; import produce, { Draft, original } from 'immer'; import moment, { DurationInputObject } from 'moment'; -import logger from 'redux-logger'; import { isSameTimeKey, MinimalTimeKey, pickTimeKey, TimeKey } from '../../../../common/time'; import { datemathToEpochMillis } from '../../../utils/datemath'; import { TimefilterState } from '../../../utils/timefilter_state_storage'; @@ -119,16 +118,6 @@ export const createLogPositionStateContainer = (initialArguments: InitialLogPosi updateVisiblePositions(visiblePositions)(state), }); -export const withDevelopmentLogger = >( - stateContainer: StateContainer -): StateContainer => { - if (process.env.NODE_ENV !== 'production') { - stateContainer.addMiddleware(logger as any); - } - - return stateContainer; -}; - /** * Common updaters */ diff --git a/x-pack/plugins/infra/public/containers/logs/log_position/use_log_position.ts b/x-pack/plugins/infra/public/containers/logs/log_position/use_log_position.ts index 070e36177767c..61e543b6b96ea 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_position/use_log_position.ts +++ b/x-pack/plugins/infra/public/containers/logs/log_position/use_log_position.ts @@ -10,6 +10,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import useInterval from 'react-use/lib/useInterval'; import useThrottle from 'react-use/lib/useThrottle'; import { TimeKey } from '../../../../common/time'; +import { withReduxDevTools } from '../../../utils/state_container_devtools'; import { TimefilterState } from '../../../utils/timefilter_state_storage'; import { useObservableState } from '../../../utils/use_observable'; import { wrapStateContainer } from '../../../utils/wrap_state_container'; @@ -20,7 +21,6 @@ import { LogPositionState, updateStateFromTimefilterState, updateStateFromUrlState, - withDevelopmentLogger, } from './log_position_state'; import { useLogPositionTimefilterStateSync } from './log_position_timefilter_state'; import { LogPositionUrlState, useLogPositionUrlStateSync } from './use_log_position_url_state_sync'; @@ -77,11 +77,14 @@ export const useLogPositionState: () => LogPositionStateParams & LogPositionCall useLogPositionTimefilterStateSync(); const [logPositionStateContainer] = useState(() => - withDevelopmentLogger( + withReduxDevTools( createLogPositionStateContainer({ initialStateFromUrl, initialStateFromTimefilter, - }) + }), + { + name: 'logPosition', + } ) ); diff --git a/x-pack/plugins/infra/public/utils/state_container_devtools.ts b/x-pack/plugins/infra/public/utils/state_container_devtools.ts new file mode 100644 index 0000000000000..b34db3d347450 --- /dev/null +++ b/x-pack/plugins/infra/public/utils/state_container_devtools.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ReduxLikeStateContainer } from '@kbn/kibana-utils-plugin/public'; +import { EnhancerOptions } from 'redux-devtools-extension'; + +export const withReduxDevTools = >( + stateContainer: StateContainer, + config?: EnhancerOptions +): StateContainer => { + if (process.env.NODE_ENV !== 'production' && (window as any).__REDUX_DEVTOOLS_EXTENSION__) { + const devToolsExtension = (window as any).__REDUX_DEVTOOLS_EXTENSION__; + + const devToolsInstance = devToolsExtension.connect(config); + + devToolsInstance.init(stateContainer.getState()); + + stateContainer.addMiddleware(({ getState }) => (next) => (action) => { + devToolsInstance.send(action, getState()); + return next(action); + }); + } + + return stateContainer; +}; From b8ba64f25ac0ea994f6be17d671ff40dbc5435ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20St=C3=BCrmer?= Date: Tue, 8 Nov 2022 20:06:53 +0000 Subject: [PATCH 20/21] Avoid serialization errors with SyntheticEvents --- .../infra/public/utils/state_container_devtools.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/infra/public/utils/state_container_devtools.ts b/x-pack/plugins/infra/public/utils/state_container_devtools.ts index b34db3d347450..7a37f35093152 100644 --- a/x-pack/plugins/infra/public/utils/state_container_devtools.ts +++ b/x-pack/plugins/infra/public/utils/state_container_devtools.ts @@ -15,7 +15,13 @@ export const withReduxDevTools = replaceReactSyntheticEvent(value), + }, + }); devToolsInstance.init(stateContainer.getState()); @@ -27,3 +33,9 @@ export const withReduxDevTools = + typeof value === 'object' && value != null && (value as any).nativeEvent instanceof Event; + +const replaceReactSyntheticEvent = (value: unknown) => + isReactSyntheticEvent(value) ? '[ReactSyntheticEvent]' : value; From e1ebdb9b6a5a7e68edfb86ec2f3dc3f1a17ac8a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20St=C3=BCrmer?= Date: Tue, 8 Nov 2022 20:15:52 +0000 Subject: [PATCH 21/21] Disable redux devtools features requiring dispatch --- .../infra/public/utils/state_container_devtools.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/x-pack/plugins/infra/public/utils/state_container_devtools.ts b/x-pack/plugins/infra/public/utils/state_container_devtools.ts index 7a37f35093152..c68936eca7921 100644 --- a/x-pack/plugins/infra/public/utils/state_container_devtools.ts +++ b/x-pack/plugins/infra/public/utils/state_container_devtools.ts @@ -21,6 +21,16 @@ export const withReduxDevTools = replaceReactSyntheticEvent(value), }, + features: { + lock: false, + persist: false, + import: false, + jump: false, + skip: false, + reorder: false, + dispatch: false, + ...config?.features, + }, }); devToolsInstance.init(stateContainer.getState());