diff --git a/x-pack/plugins/infra/docs/state_machines/README.md b/x-pack/plugins/infra/docs/state_machines/README.md index b7eac58a54668..b282211313c29 100644 --- a/x-pack/plugins/infra/docs/state_machines/README.md +++ b/x-pack/plugins/infra/docs/state_machines/README.md @@ -6,3 +6,4 @@ implementation patterns: - [Patterns for designing XState state machines](./xstate_machine_patterns.md) - [Patterns for using XState with React](./xstate_react_patterns.md) +- [Patterns for working with URLs and URL precedence](./xstate_url_patterns_and_precedence.md) diff --git a/x-pack/plugins/infra/docs/state_machines/xstate_url_patterns_and_precedence.md b/x-pack/plugins/infra/docs/state_machines/xstate_url_patterns_and_precedence.md new file mode 100644 index 0000000000000..cac619a75fa91 --- /dev/null +++ b/x-pack/plugins/infra/docs/state_machines/xstate_url_patterns_and_precedence.md @@ -0,0 +1,114 @@ +# URL patterns and URL precedence + +## Summary + +When working with state it's common to synchronise a portion to the URL. + +### Patterns + +Within our state machines we begin in an `uninitialized` state, from here we move in to states that represent initialisation of intitial values. This may differ between machines depending on which Kibana services (if any) are relied on. It could also be possible to have a machine that merely has defaults and does not rely on services and URL state. + +For example here is an example of our `uninitialized` state immediately transitioning to `initializingFromTimeFilterService`. + +```ts +uninitialized: { + always: { + target: 'initializingFromTimeFilterService', + }, +}, +``` + +Our `initializingFromTimeFilterService` target might look something like this: + +```ts + initializingFromTimeFilterService: { + on: { + INITIALIZED_FROM_TIME_FILTER_SERVICE: { + target: 'initializingFromUrl', + actions: ['updateTimeContextFromTimeFilterService'], + }, + }, + invoke: { + src: 'initializeFromTimeFilterService', + }, +}, +``` + +This invokes an (xstate) service to interact with the (Kibana) service and read values. We then receive an `INITIALIZED_FROM_TIME_FILTER_SERVICE` event, store what we need in context, and move to the next level of initialisation (URL). + +As the target becomes `initializingFromUrl` we see much the same thing: + +```ts +initializingFromUrl: { + on: { + INITIALIZED_FROM_URL: { + target: 'initialized', + actions: ['storeQuery', 'storeFilters', 'updateTimeContextFromUrl'], + }, + }, + invoke: { + src: 'initializeFromUrl', + }, +}, +``` + +Eventually we receive an `INITIALIZED_FROM_URL` event, values are stored in context, and we then move to the `initialized` state. + +The code that interacts with the URL is in a file called `url_state_storage_service.ts` under the directory of the machine. + +This is powerful because we could have as many layers as we need here, and we will only move to the `initialized` state at the end of the chain. Since the UI won't attempt to render content until we're in an `initialized` state we are safe from subtle race conditions where we might attempt to read a value too early. + +## Precedence + +In the Logs solution the order of precedence is as follows: + +- Defaults +- Kibana services (time filter, query, filter manager etc) +- URL + +That is to say the URL has most precedence and will overwrite defaults and service values. + +### Log Stream + +Within the Log Stream we have the following state held in the URL (and managed by xstate): + +- Log filter + - Time range + - From + - To + - Refresh interval + - Pause + - Value + - Query + - Language + - Query + - Filters + +- Log position + - Position + - Time + - Tiebreaker + +#### Warning! + +Due to legacy reasons the `logFilter` key should be initialised before the `logPosition` key. Otherwise the `logPosition` key might be overwritten before the `logFilter` code has had a chance to read from the key. + +#### Backwards compatibility + +The Log Stream does have some legacy URL state that needs to be translated for backwards compatibility. Here is an example of the previous legacy formats: + +- Log filter + - Language + - Query + +- Log filter (this version is older than language / query) + - Kind + - Expression + +- Log position + - Start (now log filter > time range > from) + - End (now log filter > time range > to) + - StreamLive (now log filter > refresh interval > pause) + - Position + - Time (used to determine log filter > time range > from / to if start and end aren't set within legacy log position) + - Tiebreaker \ No newline at end of file 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 e4e6ad6c54deb..677428ac7a3f7 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 @@ -5,7 +5,4 @@ * 2.0. */ -export * from './log_position_state'; -export * from './replace_log_position_in_query_string'; export * from './use_log_position'; -export type { LogPositionUrlState } from './use_log_position_url_state_sync'; 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 deleted file mode 100644 index b87dca28fc048..0000000000000 --- a/x-pack/plugins/infra/public/containers/logs/log_position/log_position_state.test.ts +++ /dev/null @@ -1,287 +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 moment from 'moment'; -import { createInitialLogPositionState, updateStateFromUrlState } from './log_position_state'; - -describe('function createInitialLogPositionState', () => { - it('initializes state without url and timefilter', () => { - const initialState = createInitialLogPositionState({ - initialStateFromUrl: null, - 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-1d", - "to": "now", - }, - "lastChangedCompletely": 1640995200000, - }, - "timestamps": Object { - "endTimestamp": 1640995200000, - "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, - }, - } - `); - }); -}); - -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, - }, - latestPosition: { - 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, - }, - latestPosition: { - 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, - latestPosition: 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-2d', - end: 'now-1d', - }, - updateDate - )(initialState); - - expect(newState).toEqual({ - ...initialState, - timeRange: { - expression: { - from: 'now-2d', - to: 'now-1d', - }, - lastChangedCompletely: updateDate.valueOf(), - }, - timestamps: { - startTimestamp: moment(updateDate).subtract(2, 'day').valueOf(), - endTimestamp: moment(updateDate).subtract(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 deleted file mode 100644 index cd5f11346c56a..0000000000000 --- a/x-pack/plugins/infra/public/containers/logs/log_position/log_position_state.ts +++ /dev/null @@ -1,308 +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 { RefreshInterval } from '@kbn/data-plugin/public'; -import { TimeRange } from '@kbn/es-query'; -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 { 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 { - timeRange: { - expression: TimeRange; - lastChangedCompletely: number; - }; - timestamps: { - startTimestamp: number; - endTimestamp: number; - lastChangedTimestamp: number; - }; - refreshInterval: RefreshInterval; - latestPosition: TimeKey | null; - targetPosition: TimeKey | null; - visiblePositions: VisiblePositions; -} - -export interface InitialLogPositionArguments { - initialStateFromUrl: LogPositionUrlState | null; - initialStateFromTimefilter: TimefilterState | null; - 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, - now, -}: InitialLogPositionArguments): LogPositionState => { - const nowTimestamp = now?.valueOf() ?? Date.now(); - - return pipe( - { - timeRange: { - expression: initialTimeRangeExpression, - lastChangedCompletely: nowTimestamp, - }, - timestamps: { - startTimestamp: datemathToEpochMillis(initialTimeRangeExpression.from, 'down', now) ?? 0, - endTimestamp: datemathToEpochMillis(initialTimeRangeExpression.to, 'up', now) ?? 0, - lastChangedTimestamp: nowTimestamp, - }, - refreshInterval: initialRefreshInterval, - targetPosition: null, - latestPosition: null, - visiblePositions: initialVisiblePositions, - }, - initialStateFromUrl != null - ? initializeStateFromUrlState(initialStateFromUrl, now) - : initialStateFromTimefilter != null - ? updateStateFromTimefilterState(initialStateFromTimefilter, now) - : identity - ); -}; - -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 })(state), - stopLiveStreaming: (state: LogPositionState) => () => - updateRefreshInterval({ pause: true })(state), - jumpToTargetPosition: (state: LogPositionState) => (targetPosition: TimeKey | null) => - updateTargetPosition(targetPosition)(state), - jumpToTargetPositionTime: (state: LogPositionState) => (time: number) => - updateTargetPosition({ time })(state), - reportVisiblePositions: (state: LogPositionState) => (visiblePositions: VisiblePositions) => - updateVisiblePositions(visiblePositions)(state), - }); - -/** - * Common updaters - */ - -const updateVisiblePositions = (visiblePositions: VisiblePositions) => - produce((draftState) => { - draftState.visiblePositions = visiblePositions; - - updateLatestPositionDraft(draftState); - }); - -const updateTargetPosition = (targetPosition: Partial | null) => - produce((draftState) => { - if (targetPosition?.time != null) { - draftState.targetPosition = { - time: targetPosition.time, - tiebreaker: targetPosition.tiebreaker ?? 0, - }; - } 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; - const newTo = timeRange?.to; - const nowTimestamp = now?.valueOf() ?? Date.now(); - - // Update expression and timestamps - if (newFrom != null) { - draftState.timeRange.expression.from = newFrom; - const newStartTimestamp = datemathToEpochMillis(newFrom, 'down', now); - if (newStartTimestamp != null) { - draftState.timestamps.startTimestamp = newStartTimestamp; - draftState.timestamps.lastChangedTimestamp = nowTimestamp; - } - } - if (newTo != null) { - draftState.timeRange.expression.to = newTo; - const newEndTimestamp = datemathToEpochMillis(newTo, 'up', now); - if (newEndTimestamp != null) { - draftState.timestamps.endTimestamp = newEndTimestamp; - draftState.timestamps.lastChangedTimestamp = nowTimestamp; - } - } - if (newFrom != null && newTo != null) { - draftState.timeRange.lastChangedCompletely = nowTimestamp; - } - - // 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; - - updateLatestPositionDraft(draftState); - } - }); - -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; - - updateLatestPositionDraft(draftState); - } - }), - (currentState) => { - if (!currentState.refreshInterval.pause) { - 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.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 => - pipe( - state, - updateTargetPosition(urlState?.position ?? null), - updateTimeRange( - { - from: urlState?.start, - to: urlState?.end, - }, - now - ), - 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, now?: Date) => - (state: LogPositionState): LogPositionState => - pipe( - state, - updateTimeRange( - { - from: timefilterState?.timeRange?.from, - to: timefilterState?.timeRange?.to, - }, - now - ), - updateRefreshInterval({ - pause: timefilterState?.refreshInterval?.pause, - 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/log_position_timefilter_state.ts b/x-pack/plugins/infra/public/containers/logs/log_position/log_position_timefilter_state.ts deleted file mode 100644 index 35c2a2f367c4e..0000000000000 --- a/x-pack/plugins/infra/public/containers/logs/log_position/log_position_timefilter_state.ts +++ /dev/null @@ -1,55 +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 { 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/replace_log_position_in_query_string.ts b/x-pack/plugins/infra/public/containers/logs/log_position/replace_log_position_in_query_string.ts deleted file mode 100644 index e447c2c1436d2..0000000000000 --- a/x-pack/plugins/infra/public/containers/logs/log_position/replace_log_position_in_query_string.ts +++ /dev/null @@ -1,24 +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 { 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, - }); 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 61e543b6b96ea..2471acf6e9283 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 @@ -6,24 +6,14 @@ */ 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 { 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'; +import { useMemo } from 'react'; +import { VisiblePositions } from '../../../observability_logs/log_stream_position_state/src/types'; import { - createLogPositionStateContainer, - getTimefilterState, - getUrlState, - LogPositionState, - updateStateFromTimefilterState, - updateStateFromUrlState, -} from './log_position_state'; -import { useLogPositionTimefilterStateSync } from './log_position_timefilter_state'; -import { LogPositionUrlState, useLogPositionUrlStateSync } from './use_log_position_url_state_sync'; + LogStreamPageActorRef, + LogStreamPageCallbacks, +} from '../../../observability_logs/log_stream_page/state'; +import { MatchedStateFromActor } from '../../../observability_logs/xstate_helpers'; +import { TimeKey } from '../../../../common/time'; type TimeKeyOrNull = TimeKey | null; @@ -36,14 +26,6 @@ interface DateRange { lastCompleteDateRangeExpressionUpdate: number; } -interface VisiblePositions { - startKey: TimeKeyOrNull; - middleKey: TimeKeyOrNull; - endKey: TimeKeyOrNull; - pagesAfterEnd: number; - pagesBeforeStart: number; -} - export type LogPositionStateParams = DateRange & { targetPosition: TimeKeyOrNull; isStreaming: boolean; @@ -68,69 +50,44 @@ type UpdateDateRangeFn = ( newDateRange: Partial> ) => void; -const DESIRED_BUFFER_PAGES = 2; -const RELATIVE_END_UPDATE_DELAY = 1000; - -export const useLogPositionState: () => LogPositionStateParams & LogPositionCallbacks = () => { - const { initialStateFromUrl, startSyncingWithUrl } = useLogPositionUrlStateSync(); - const { initialStateFromTimefilter, startSyncingWithTimefilter } = - useLogPositionTimefilterStateSync(); - - const [logPositionStateContainer] = useState(() => - withReduxDevTools( - createLogPositionStateContainer({ - initialStateFromUrl, - initialStateFromTimefilter, - }), - { - name: 'logPosition', - } - ) - ); - - 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, visiblePositions } = latestLogPositionState; - - const isStreaming = useMemo( - () => !latestLogPositionState.refreshInterval.pause, - [latestLogPositionState] - ); - - const updateDateRange = useCallback( - (newDateRange: Partial>) => - logPositionStateContainer.transitions.updateTimeRange({ - from: newDateRange.startDateExpression, - to: newDateRange.endDateExpression, - }), - [logPositionStateContainer] - ); +export const useLogPositionState = ({ + logStreamPageState, + logStreamPageCallbacks, +}: { + logStreamPageState: InitializedLogStreamPageState; + logStreamPageCallbacks: LogStreamPageCallbacks; +}): LogPositionStateParams & LogPositionCallbacks => { + const dateRange = useMemo(() => getLegacyDateRange(logStreamPageState), [logStreamPageState]); + + const { refreshInterval, targetPosition, visiblePositions, latestPosition } = + logStreamPageState.context; + + const actions = useMemo(() => { + const { + updateTimeRange, + jumpToTargetPosition, + jumpToTargetPositionTime, + reportVisiblePositions, + startLiveStreaming, + stopLiveStreaming, + } = logStreamPageCallbacks; + + return { + jumpToTargetPosition, + jumpToTargetPositionTime, + reportVisiblePositions, + startLiveStreaming, + stopLiveStreaming, + updateDateRange: ( + newDateRange: Partial> + ) => { + updateTimeRange({ + from: newDateRange.startDateExpression, + to: newDateRange.endDateExpression, + }); + }, + }; + }, [logStreamPageCallbacks]); const visibleTimeInterval = useMemo( () => @@ -140,62 +97,41 @@ export const useLogPositionState: () => LogPositionStateParams & LogPositionCall [visiblePositions.startKey, visiblePositions.endKey] ); - // `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 (throttledPagesAfterEnd <= DESIRED_BUFFER_PAGES) { - logPositionStateContainer.transitions.updateTimeRange({ to: 'now' }); - } - }, [dateRange.endDateExpression, throttledPagesAfterEnd, 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, + isStreaming: !refreshInterval.pause, ...dateRange, // visible positions state firstVisiblePosition: visiblePositions.startKey, pagesBeforeStart: visiblePositions.pagesBeforeStart, pagesAfterEnd: visiblePositions.pagesAfterEnd, - visibleMidpoint: latestLogPositionState.latestPosition, - visibleMidpointTime: latestLogPositionState.latestPosition?.time ?? null, + visibleMidpoint: latestPosition, + visibleMidpointTime: latestPosition?.time ?? null, visibleTimeInterval, // actions - jumpToTargetPosition: logPositionStateContainer.transitions.jumpToTargetPosition, - jumpToTargetPositionTime: logPositionStateContainer.transitions.jumpToTargetPositionTime, - reportVisiblePositions: logPositionStateContainer.transitions.reportVisiblePositions, - startLiveStreaming: logPositionStateContainer.transitions.startLiveStreaming, - stopLiveStreaming: logPositionStateContainer.transitions.stopLiveStreaming, - updateDateRange, + ...actions, }; }; export const [LogPositionStateProvider, useLogPositionStateContext] = createContainer(useLogPositionState); -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, -}); +const getLegacyDateRange = (logStreamPageState: InitializedLogStreamPageState): DateRange => { + return { + startDateExpression: logStreamPageState.context.timeRange.from, + endDateExpression: logStreamPageState.context.timeRange.to, + startTimestamp: logStreamPageState.context.timestamps.startTimestamp, + endTimestamp: logStreamPageState.context.timestamps.endTimestamp, + lastCompleteDateRangeExpressionUpdate: + logStreamPageState.context.timeRange.lastChangedCompletely, + timestampsLastUpdate: logStreamPageState.context.timestamps.lastChangedTimestamp, + }; +}; + +type InitializedLogStreamPageState = MatchedStateFromActor< + LogStreamPageActorRef, + { hasLogViewIndices: 'initialized' } +>; 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 deleted file mode 100644 index b9e6a8a5b3eb6..0000000000000 --- a/x-pack/plugins/infra/public/containers/logs/log_position/use_log_position_url_state_sync.ts +++ /dev/null @@ -1,78 +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 { minimalTimeKeyRT } from '../../../../common/time'; -import { datemathStringRT } from '../../../utils/datemath'; -import { useKbnUrlStateStorageFromRouterContext } from '../../../utils/kbn_url_state_context'; - -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; - -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, - set: (key: string, state: State) => - urlStateStorage.set(key, state, { replace: true }), - }, - }); - - start(); - - return stop; - }, - [initialStateFromUrl, urlStateStorage] - ); - - return { - initialStateFromUrl, - startSyncingWithUrl, - }; -}; diff --git a/x-pack/plugins/infra/public/observability_logs/log_stream_page/state/src/initial_parameters_service.ts b/x-pack/plugins/infra/public/observability_logs/log_stream_page/state/src/initial_parameters_service.ts index ec1460e41b86f..431d2df5d99a1 100644 --- a/x-pack/plugins/infra/public/observability_logs/log_stream_page/state/src/initial_parameters_service.ts +++ b/x-pack/plugins/infra/public/observability_logs/log_stream_page/state/src/initial_parameters_service.ts @@ -5,16 +5,22 @@ * 2.0. */ +import { RefreshInterval } from '@kbn/data-plugin/public'; import { InvokeCreator, Receiver } from 'xstate'; -import { ParsedQuery } from '../../../log_stream_query_state'; +import { TimeKey } from '../../../../../common/time'; +import { VisiblePositions } from '../../../log_stream_position_state'; +import { ExtendedTimeRange, ParsedQuery, Timestamps } from '../../../log_stream_query_state'; import { LogStreamPageContext, LogStreamPageEvent } from './types'; -export const waitForInitialParameters = +export const waitForInitialQueryParameters = (): InvokeCreator => (_context, _event) => (send, onEvent: Receiver) => { // constituents of the set of initial parameters let latestValidQuery: ParsedQuery | undefined; + let latestTimeRange: ExtendedTimeRange | undefined; + let latestRefreshInterval: RefreshInterval | undefined; + let latestTimestamps: Timestamps | undefined; onEvent((event) => { switch (event.type) { @@ -23,13 +29,60 @@ export const waitForInitialParameters = case 'INVALID_QUERY_CHANGED': latestValidQuery = event.parsedQuery; break; + case 'TIME_CHANGED': + latestTimeRange = event.timeRange; + latestRefreshInterval = event.refreshInterval; + latestTimestamps = event.timestamps; + break; } // if all constituents of the parameters have been delivered - if (latestValidQuery != null) { + if ( + latestValidQuery !== undefined && + latestTimeRange !== undefined && + latestRefreshInterval !== undefined && + latestTimestamps !== undefined + ) { send({ - type: 'RECEIVED_INITIAL_PARAMETERS', + type: 'RECEIVED_INITIAL_QUERY_PARAMETERS', validatedQuery: latestValidQuery, + timeRange: latestTimeRange, + refreshInterval: latestRefreshInterval, + timestamps: latestTimestamps, + }); + } + }); + }; + +export const waitForInitialPositionParameters = + (): InvokeCreator => + (_context, _event) => + (send, onEvent: Receiver) => { + // constituents of the set of initial parameters + let latestTargetPosition: TimeKey | null; + let latestLatestPosition: TimeKey | null; + let latestVisiblePositions: VisiblePositions; + + onEvent((event) => { + switch (event.type) { + case 'POSITIONS_CHANGED': + latestTargetPosition = event.targetPosition; + latestLatestPosition = event.latestPosition; + latestVisiblePositions = event.visiblePositions; + break; + } + + // if all constituents of the parameters have been delivered + if ( + latestTargetPosition !== undefined && + latestLatestPosition !== undefined && + latestVisiblePositions !== undefined + ) { + send({ + type: 'RECEIVED_INITIAL_POSITION_PARAMETERS', + targetPosition: latestTargetPosition, + latestPosition: latestLatestPosition, + visiblePositions: latestVisiblePositions, }); } }); diff --git a/x-pack/plugins/infra/public/observability_logs/log_stream_page/state/src/provider.tsx b/x-pack/plugins/infra/public/observability_logs/log_stream_page/state/src/provider.tsx index b5d31e0ea8187..0da26759b6a04 100644 --- a/x-pack/plugins/infra/public/observability_logs/log_stream_page/state/src/provider.tsx +++ b/x-pack/plugins/infra/public/observability_logs/log_stream_page/state/src/provider.tsx @@ -7,6 +7,7 @@ import { useInterpret } from '@xstate/react'; import createContainer from 'constate'; +import useMount from 'react-use/lib/useMount'; import { isDevMode } from '../../../../utils/dev_mode'; import { createLogStreamPageStateMachine, @@ -21,9 +22,17 @@ export const useLogStreamPageState = ({ filterManagerService, urlStateStorage, useDevTools = isDevMode(), + timeFilterService, }: { useDevTools?: boolean; } & LogStreamPageStateMachineDependencies) => { + useMount(() => { + // eslint-disable-next-line no-console + console.log( + "A warning in console stating: 'The result of getSnapshot should be cached to avoid an infinite loop' is expected. This will be fixed once we can upgrade versions." + ); + }); + const logStreamPageStateService = useInterpret( () => createLogStreamPageStateMachine({ @@ -33,6 +42,7 @@ export const useLogStreamPageState = ({ toastsService, filterManagerService, urlStateStorage, + timeFilterService, }), { devTools: useDevTools } ); diff --git a/x-pack/plugins/infra/public/observability_logs/log_stream_page/state/src/state_machine.ts b/x-pack/plugins/infra/public/observability_logs/log_stream_page/state/src/state_machine.ts index 3e343e15bf01f..dba97dc5f8efb 100644 --- a/x-pack/plugins/infra/public/observability_logs/log_stream_page/state/src/state_machine.ts +++ b/x-pack/plugins/infra/public/observability_logs/log_stream_page/state/src/state_machine.ts @@ -5,25 +5,36 @@ * 2.0. */ +import { RefreshInterval } from '@kbn/data-plugin/public'; +import { TimeRange } from '@kbn/es-query'; import { actions, ActorRefFrom, createMachine, EmittedFrom } from 'xstate'; +import { datemathToEpochMillis } from '../../../../utils/datemath'; +import { createLogStreamPositionStateMachine } from '../../../log_stream_position_state/src/state_machine'; import { createLogStreamQueryStateMachine, + DEFAULT_REFRESH_INTERVAL, + DEFAULT_TIMERANGE, LogStreamQueryStateMachineDependencies, } from '../../../log_stream_query_state'; import type { LogViewNotificationChannel } from '../../../log_view_state'; import { OmitDeprecatedState } from '../../../xstate_helpers'; -import { waitForInitialParameters } from './initial_parameters_service'; +import { + waitForInitialQueryParameters, + waitForInitialPositionParameters, +} from './initial_parameters_service'; import type { LogStreamPageContext, LogStreamPageContextWithLogView, LogStreamPageContextWithLogViewError, + LogStreamPageContextWithPositions, LogStreamPageContextWithQuery, + LogStreamPageContextWithTime, LogStreamPageEvent, LogStreamPageTypestate, } from './types'; export const createPureLogStreamPageStateMachine = (initialContext: LogStreamPageContext = {}) => - /** @xstate-layout N4IgpgJg5mDOIC5QBsD2UDKAXATmAhgLYAK+M2+WYAdAK4B2Alk1o-sowF6QDEAMgHkAggBEAkgDkA4gH1BsgGpiAogHUZGACpCASpuUiA2gAYAuolAAHVLEatU9CyAAeiALQAmAJzUPHgGwArIEALAAcHmHhXsEhADQgAJ6IAQDM1IHGYQDs2ZnZxl5e-l6pAL5lCWiYuAQkZGAUVHRMLGwc3BD8wuLScgKKKuoAYkJifAYm5kgg1rb2jjOuCJ4hAIzUqZklHgX+xqlrkQnJCKlB1P4loYH+qV7hHoEVVejYeESk5FiUNAzMdnaXF4glEklk8hkSjUGgAqgBheHKAyTMxOOaAhxOZbZNbZajGXLBVIkrJrYInRBHYzUEIeVIhVJhNZZLws7IhF4garvOpfRo-Zr-NrsYFdUG9CEDKFDOGI5EiSZraZWGyYxagHEhEIEwkeI7Zc7GO6UhCZHzZML7MKBDwhQp0zmVblvWqfBpNGhofAQZhQPjoBSMMAAd26YL6kOhIzGEyMaJmGIW2JS4VpJTCWXp5K82Q8pvN1Et1tt9oedq5PLd9W+v2o3t99H9geDYYl4P6gxhGARSJR8ZVszVyaWiEZPnt-m8Ry8xjta38pqN1FK+uy+w8xhCgS8lddHxrArrDb9AagQdD4clnZl3d7CqVg6TjCxo4QISumy2MWNMWiAVNO58WMFkImMOc50CMI9xqA9+U9etUB9U8W1DYZ8EYZAQR6Dso1lLRdH0Ad0WHF8NRcdxvF8AJYgiKIwhiUIC1SDwMgNcC8g5O1nmdKs4I9QUaAAC3wWAzwvEMxHoX0AGM4BaAFWFFToeB0ZQkTEBQDBkSQxE0MQhD4GRiF0IQAFllH0HQMCmEj5jIlMEBnfxqAiYojjWVJCjnJcwnSIIrSuNZZ2CGJyl4-c+QEusRLE1DJOkxg5NgBSRQ6XgFEMsQRBkABFWFlB0ABNGR4QACSEaRUSfUjX01Kl-DyWk1giW0p3tElTTxfFwhCPYQIXXE1idV5YKi2tmli8TWyk2T5OFQFlN4SRMr4bK8oK4rSoqqriMTWryOWBdGtckCQMCEknn8MIl21FcWLxVJDUzfwWv8GDeXdCbhNE6bQ1mpL5MUoEVNW9b8sKkrysqqRqrs9VHMGwJmtagI7QOVICznFd1yebdMgutY1gqZ16FQCA4CcPjxqPKh4ZHeqVkiHwtl-XZjQOTzTTcNk2NtQ4-A5q0PureDBNSxb0ogemHLfOkurpahyXArIbTZIItxF-jvsQ5Cmz+kMZbqiiEF2DJQiuK0bVyH94iSRAmTCTZ3ICPMtj8HjRs+w8EJPfX4vQzDICNw7EGR5k7S8NJGrZNXAJ3IsnvtImt28aCIrGr7aZ+uLzxmxLkpDxHNxpC7dn2K404pe331YrwokNQ1GIF7ItZphCpvigHkolpSpaLt8jjTMv12NKd6+r04nlYgbDjxO0-KnNus4736u4LoG0rFAfGc8qIi0Cck1cCQ1K4LJ4iznS1zjzG0WOXn3xcIRhYFsf28-+jf4H2+zjaOw4XKbijt4YIWRbjZHjhaJ6T1cSwIxiTMoQA */ + /** @xstate-layout N4IgpgJg5mDOIC5QBsD2UDKAXATmAhgLYAK+M2+WYAdAK4B2Alk1o-sowF6QDEAMgHkAggBEAkgDkA4gH1BsgGpiAogHUZGACpCASpuUiA2gAYAuolAAHVLEatU9CyAAeiALQAmAJzUPHgGwArIEALAAcHmHhXsEhADQgAJ6IAQDM1IHGYQDs2ZnZxl5e-l6pAL5lCWiYuAQkZGAUVHRMLGwc3BD8wuLScgKKKuoAYkJifAYm5kgg1rb2jjOuCJ4hAIzUqZklHgX+xqlrkQnJCKlB1P4loYH+qV7hHoEVVejYeESk5FiUNAzMdnaXF4glEklk8hkSjUGgAqgBheHKAyTMxOOaAhxOZbZNbZajGXLBVIkrJrYInRBHYzUEIeVIhVJhNZZLws7IhF4garvOpfRo-Zr-NrsYFdUG9CEDKFDOGI5EiSZraZWGyYxagHEhEIEwkeI7Zc7GO6UhCZHzZML7MKBDwhQp0zmVblvWqfBpNGhofAQZhQPjoBSMMAAd26YL6kOhIzGEyMaJmGIW2JS4VpJTCWXp5K82Q8pvN1Et1tt9oedq5PLd9W+v2o3t99H9geDYYl4P6gxhGARSJR8ZVszVyaWiEZPnt-m8Ry8xjta38pqN1FK+uy+w8xhCgS8lddHxrArrDb9AagQdD4clnZl3d7CqVg6TjCxo4QISumy2MWNMWiAVNO58WMFkImMOc50CMI9xqA9+U9etUB9U8W1DYZ8EYZAQR6Dso1lLRdH0Ad0WHF8NRcdxvF8AJYgiKIwhiUIC1SDwMgNcC8g5O1nmdKs4I9QUaAAC3wWAzwvEMxHoX0AGM4GoAFWFFTg-QARVoMAcESHgdGUJExAUAwZEkMRNDEIQ+BkVTYWUHQAE0ZGIXQhAAWWUfQdAwKYSPmMinFOKcNgKXYwnOPMbRYhJlhCYoYN5d1a2aESxNQyTpMYOTYAUkUOjUjStJ4BQLLEEQrJs+yZHhAAJIRpFRJ9SNfTUx2KAtDSLOdLTCyJAhYuLq3gwTqGS8TWyk2T5MUoEVKbdTNO0yQir4EqytshzqtqqR6p89UU3fVqkkQS1WNKXEoP8cLeo8fr+MS4TRNG0Nxoyyacq4PL5p4My3Mqmq6uIxNGvI6KDtOW1Ag6kLuoi67eP3PkBLrEbUuezLssBZS-WIIHYB0vTlAMoyTLMizHIEDBTLEAQJEc5y3I8ryE1VXymoo-bF0OhAGI2EDbVCi6er6uHYIRu7hoelH0rRqbMabbGWfoXHiHJynqYwX7Nu2wGFb2mKOdOIofEKBksgFmGbtFo8kol88xql16MY6XglpW6y1o1-7vO13a3wXPJaTWCJbSne0SQLOcV3XJ5t0yXq1jWC2Eqt+6Uttp77aymWna6b7lA9raAeZn3moQBdCV8b99hiQ0rnzTntx1XMpxuWPDgT4X4sPBDkbTtKJszt7Oh4ZWKbMtX861ouRxLsv8XpHcq8CGup0A+OCVCVJskY4w49h14RaT7ubYk1GHaU7OeAAKVhFziBkTQBHv3Qts0MnR6piQvanvzff2OfK8KEvc4K967Gkjs3GOO826Jy7kNHuJ8M7o3PmKPGys9AygpgAIQmG-VWEhGYNR1r-cu89iiAOXnXU4WwwjgOjsEKB8cYGDSRsfO2-ckHTV4LCYgIghD6HvmIH6OhNZfyHEQmef8K4L3IcAyhR01g+DWCxTIkDd5MMRtbVOCD2FZxQdw3h-DdLDF0hgKqxkJAeSWqI58rNlizykWQ6usilwsloS3Bh7d96d2YZox6fcXoD0digpyW0ZDKAkKVTBsJhjDFsjIXSQhqqTzEcXNm9jSGLwoaaAIJ0o7uLju3Z09BUAQDgE4PiltPQ7WnmzTwDFNjbC8LsY0BwlGmjcCxAk9IOSZgXDuUotx1Fi2FEEzo1Sf4lzpKaNYdJqDknAlkG0bIghbiGcnRCyEmx+PGbYlI+JYhXCtDaXIP54icyZDQ+4-gjgCy2H4HiXiBoaK9EhRszZe7oUwpAHZwNEAQ2ZHaJpdwLpsiWYBHcRZN72njlubw0EO5PLFvAthASfl7TcOEaZEcQhR3WDaGKXVPEugPrAlhWiUXS0Hh9LSaK3zGEAvqSGXUzZXTWUfcl6cdFUrljjWlJd6WcwiKxTcUMWVC0ebddZyLOUBI4cpb53sal2KZDqPI5IllANrgWJ4TL+aXXFcS7xzzqCEEYLAWwWzJb9z5Wkw4-hfCFD8NvG0F1wUWk3pvXEXqDjlAqGUIAA */ createMachine( { context: initialContext, @@ -83,31 +94,65 @@ export const createPureLogStreamPageStateMachine = (initialContext: LogStreamPag }, }, hasLogViewIndices: { - initial: 'uninitialized', + initial: 'initializingQuery', states: { - uninitialized: { + initializingQuery: { + meta: { + _DX_warning_: + "The Query machine must be invoked and complete initialisation before the Position machine is invoked. This is due to legacy URL dependencies on the 'logPosition' key, we need to read the key before it is reset by the Position machine.", + }, + invoke: { - src: 'waitForInitialParameters', - id: 'waitForInitialParameters', + src: 'waitForInitialQueryParameters', + id: 'waitForInitialQueryParameters', }, on: { - RECEIVED_INITIAL_PARAMETERS: { - target: 'initialized', - actions: 'storeQuery', + RECEIVED_INITIAL_QUERY_PARAMETERS: { + target: 'initializingPositions', + actions: ['storeQuery', 'storeTime', 'forwardToLogPosition'], }, VALID_QUERY_CHANGED: { - target: 'uninitialized', + target: 'initializingQuery', internal: true, - actions: 'forwardToInitialParameters', + actions: 'forwardToInitialQueryParameters', }, INVALID_QUERY_CHANGED: { - target: 'uninitialized', + target: 'initializingQuery', + internal: true, + actions: 'forwardToInitialQueryParameters', + }, + TIME_CHANGED: { + target: 'initializingQuery', + internal: true, + actions: 'forwardToInitialQueryParameters', + }, + }, + }, + initializingPositions: { + meta: { + _DX_warning_: + "The Position machine must be invoked after the Query machine has been invoked and completed initialisation. This is due to the Query machine having some legacy URL dependencies on the 'logPosition' key, we don't want the Position machine to reset the URL parameters before the Query machine has had a chance to read them.", + }, + invoke: [ + { + src: 'waitForInitialPositionParameters', + id: 'waitForInitialPositionParameters', + }, + ], + on: { + RECEIVED_INITIAL_POSITION_PARAMETERS: { + target: 'initialized', + actions: ['storePositions'], + }, + + POSITIONS_CHANGED: { + target: 'initializingPositions', internal: true, - actions: 'forwardToInitialParameters', + actions: 'forwardToInitialPositionParameters', }, }, }, @@ -118,21 +163,65 @@ export const createPureLogStreamPageStateMachine = (initialContext: LogStreamPag internal: true, actions: 'storeQuery', }, + TIME_CHANGED: { + target: 'initialized', + internal: true, + actions: ['storeTime', 'forwardToLogPosition'], + }, + POSITIONS_CHANGED: { + target: 'initialized', + internal: true, + actions: ['storePositions'], + }, + JUMP_TO_TARGET_POSITION: { + target: 'initialized', + internal: true, + actions: ['forwardToLogPosition'], + }, + REPORT_VISIBLE_POSITIONS: { + target: 'initialized', + internal: true, + actions: ['forwardToLogPosition'], + }, + UPDATE_TIME_RANGE: { + target: 'initialized', + internal: true, + actions: ['forwardToLogStreamQuery'], + }, + UPDATE_REFRESH_INTERVAL: { + target: 'initialized', + internal: true, + actions: ['forwardToLogStreamQuery'], + }, + PAGE_END_BUFFER_REACHED: { + target: 'initialized', + internal: true, + actions: ['forwardToLogStreamQuery'], + }, }, }, }, - invoke: { - src: 'logStreamQuery', - id: 'logStreamQuery', - }, + invoke: [ + { + src: 'logStreamQuery', + id: 'logStreamQuery', + }, + { + src: 'logStreamPosition', + id: 'logStreamPosition', + }, + ], }, missingLogViewIndices: {}, }, }, { actions: { - forwardToInitialParameters: actions.forwardTo('waitForInitialParameters'), + forwardToInitialQueryParameters: actions.forwardTo('waitForInitialQueryParameters'), + forwardToInitialPositionParameters: actions.forwardTo('waitForInitialPositionParameters'), + forwardToLogPosition: actions.forwardTo('logStreamPosition'), + forwardToLogStreamQuery: actions.forwardTo('logStreamQuery'), storeLogViewError: actions.assign((_context, event) => event.type === 'LOADING_LOG_VIEW_FAILED' ? ({ logViewError: event.error } as LogStreamPageContextWithLogViewError) @@ -147,7 +236,7 @@ export const createPureLogStreamPageStateMachine = (initialContext: LogStreamPag : {} ), storeQuery: actions.assign((_context, event) => - event.type === 'RECEIVED_INITIAL_PARAMETERS' + event.type === 'RECEIVED_INITIAL_QUERY_PARAMETERS' ? ({ parsedQuery: event.validatedQuery, } as LogStreamPageContextWithQuery) @@ -157,6 +246,26 @@ export const createPureLogStreamPageStateMachine = (initialContext: LogStreamPag } as LogStreamPageContextWithQuery) : {} ), + storeTime: actions.assign((_context, event) => { + return 'timeRange' in event && 'refreshInterval' in event && 'timestamps' in event + ? ({ + timeRange: event.timeRange, + refreshInterval: event.refreshInterval, + timestamps: event.timestamps, + } as LogStreamPageContextWithTime) + : {}; + }), + storePositions: actions.assign((_context, event) => { + return 'targetPosition' in event && + 'visiblePositions' in event && + 'latestPosition' in event + ? ({ + targetPosition: event.targetPosition, + visiblePositions: event.visiblePositions, + latestPosition: event.latestPosition, + } as LogStreamPageContextWithPositions) + : {}; + }), }, guards: { hasLogViewIndices: (_context, event) => @@ -169,6 +278,7 @@ export const createPureLogStreamPageStateMachine = (initialContext: LogStreamPag export type LogStreamPageStateMachine = ReturnType; export type LogStreamPageActorRef = OmitDeprecatedState>; export type LogStreamPageState = EmittedFrom; +export type LogStreamPageSend = LogStreamPageActorRef['send']; export type LogStreamPageStateMachineDependencies = { logViewStateNotifications: LogViewNotificationChannel; @@ -181,6 +291,7 @@ export const createLogStreamPageStateMachine = ({ toastsService, filterManagerService, urlStateStorage, + timeFilterService, }: LogStreamPageStateMachineDependencies) => createPureLogStreamPageStateMachine().withConfig({ services: { @@ -190,9 +301,23 @@ export const createLogStreamPageStateMachine = ({ throw new Error('Failed to spawn log stream query service: no LogView in context'); } + const nowTimestamp = Date.now(); + const initialTimeRangeExpression: TimeRange = DEFAULT_TIMERANGE; + const initialRefreshInterval: RefreshInterval = DEFAULT_REFRESH_INTERVAL; + return createLogStreamQueryStateMachine( { dataViews: [context.resolvedLogView.dataViewReference], + timeRange: { + ...initialTimeRangeExpression, + lastChangedCompletely: nowTimestamp, + }, + timestamps: { + startTimestamp: datemathToEpochMillis(initialTimeRangeExpression.from, 'down') ?? 0, + endTimestamp: datemathToEpochMillis(initialTimeRangeExpression.to, 'up') ?? 0, + lastChangedTimestamp: nowTimestamp, + }, + refreshInterval: initialRefreshInterval, }, { kibanaQuerySettings, @@ -200,9 +325,30 @@ export const createLogStreamPageStateMachine = ({ toastsService, filterManagerService, urlStateStorage, + timeFilterService, + } + ); + }, + logStreamPosition: (context) => { + return createLogStreamPositionStateMachine( + { + targetPosition: null, + latestPosition: null, + visiblePositions: { + endKey: null, + middleKey: null, + startKey: null, + pagesBeforeStart: Infinity, + pagesAfterEnd: Infinity, + }, + }, + { + urlStateStorage, + toastsService, } ); }, - waitForInitialParameters: waitForInitialParameters(), + waitForInitialQueryParameters: waitForInitialQueryParameters(), + waitForInitialPositionParameters: waitForInitialPositionParameters(), }, }); diff --git a/x-pack/plugins/infra/public/observability_logs/log_stream_page/state/src/types.ts b/x-pack/plugins/infra/public/observability_logs/log_stream_page/state/src/types.ts index 7a970a3e06e70..eb42dccdf2486 100644 --- a/x-pack/plugins/infra/public/observability_logs/log_stream_page/state/src/types.ts +++ b/x-pack/plugins/infra/public/observability_logs/log_stream_page/state/src/types.ts @@ -5,8 +5,22 @@ * 2.0. */ +import { TimeRange } from '@kbn/es-query'; +import { TimeKey } from '../../../../../common/time'; import type { LogViewStatus } from '../../../../../common/log_views'; -import { ParsedQuery } from '../../../log_stream_query_state'; +import { + JumpToTargetPositionEvent, + LogStreamPositionContext, + ReportVisiblePositionsEvent, + VisiblePositions, +} from '../../../log_stream_position_state'; +import { LogStreamPositionNotificationEvent } from '../../../log_stream_position_state/src/notifications'; +import { + LogStreamQueryContextWithTime, + ParsedQuery, + UpdateRefreshIntervalEvent, + UpdateTimeRangeEvent, +} from '../../../log_stream_query_state'; import { LogStreamQueryNotificationEvent } from '../../../log_stream_query_state/src/notifications'; import type { LogViewContextWithError, @@ -14,13 +28,31 @@ import type { LogViewNotificationEvent, } from '../../../log_view_state'; +export interface ReceivedInitialQueryParametersEvent { + type: 'RECEIVED_INITIAL_QUERY_PARAMETERS'; + validatedQuery: ParsedQuery; + timeRange: LogStreamPageContextWithTime['timeRange']; + refreshInterval: LogStreamPageContextWithTime['refreshInterval']; + timestamps: LogStreamPageContextWithTime['timestamps']; +} + +export interface ReceivedInitialPositionParametersEvent { + type: 'RECEIVED_INITIAL_POSITION_PARAMETERS'; + targetPosition: LogStreamPageContextWithPositions['targetPosition']; + latestPosition: LogStreamPageContextWithPositions['latestPosition']; + visiblePositions: LogStreamPageContextWithPositions['visiblePositions']; +} + export type LogStreamPageEvent = | LogViewNotificationEvent | LogStreamQueryNotificationEvent - | { - type: 'RECEIVED_INITIAL_PARAMETERS'; - validatedQuery: ParsedQuery; - }; + | LogStreamPositionNotificationEvent + | ReceivedInitialQueryParametersEvent + | ReceivedInitialPositionParametersEvent + | JumpToTargetPositionEvent + | ReportVisiblePositionsEvent + | UpdateTimeRangeEvent + | UpdateRefreshIntervalEvent; export interface LogStreamPageContextWithLogView { logViewStatus: LogViewStatus; @@ -35,6 +67,9 @@ export interface LogStreamPageContextWithQuery { parsedQuery: ParsedQuery; } +export type LogStreamPageContextWithTime = LogStreamQueryContextWithTime; +export type LogStreamPageContextWithPositions = LogStreamPositionContext; + export type LogStreamPageTypestate = | { value: 'uninitialized'; @@ -58,7 +93,10 @@ export type LogStreamPageTypestate = } | { value: { hasLogViewIndices: 'initialized' }; - context: LogStreamPageContextWithLogView & LogStreamPageContextWithQuery; + context: LogStreamPageContextWithLogView & + LogStreamPageContextWithQuery & + LogStreamPageContextWithTime & + LogStreamPageContextWithPositions; } | { value: 'missingLogViewIndices'; @@ -67,3 +105,12 @@ export type LogStreamPageTypestate = export type LogStreamPageStateValue = LogStreamPageTypestate['value']; export type LogStreamPageContext = LogStreamPageTypestate['context']; + +export interface LogStreamPageCallbacks { + updateTimeRange: (timeRange: Partial) => void; + jumpToTargetPosition: (targetPosition: TimeKey | null) => void; + jumpToTargetPositionTime: (time: number) => void; + reportVisiblePositions: (visiblePositions: VisiblePositions) => void; + startLiveStreaming: () => void; + stopLiveStreaming: () => void; +} diff --git a/x-pack/plugins/infra/public/observability_logs/log_stream_position_state/index.ts b/x-pack/plugins/infra/public/observability_logs/log_stream_position_state/index.ts new file mode 100644 index 0000000000000..ddc7e818615b6 --- /dev/null +++ b/x-pack/plugins/infra/public/observability_logs/log_stream_position_state/index.ts @@ -0,0 +1,9 @@ +/* + * 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. + */ + +export * from './src/types'; +export * from './src/defaults'; diff --git a/x-pack/plugins/infra/public/observability_logs/log_stream_position_state/src/defaults.ts b/x-pack/plugins/infra/public/observability_logs/log_stream_position_state/src/defaults.ts new file mode 100644 index 0000000000000..15556d8f2b4d7 --- /dev/null +++ b/x-pack/plugins/infra/public/observability_logs/log_stream_position_state/src/defaults.ts @@ -0,0 +1,9 @@ +/* + * 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. + */ + +export const DESIRED_BUFFER_PAGES = 2; +export const RELATIVE_END_UPDATE_DELAY = 1000; diff --git a/x-pack/plugins/infra/public/observability_logs/log_stream_position_state/src/notifications.ts b/x-pack/plugins/infra/public/observability_logs/log_stream_position_state/src/notifications.ts new file mode 100644 index 0000000000000..59eda6dd5da5c --- /dev/null +++ b/x-pack/plugins/infra/public/observability_logs/log_stream_position_state/src/notifications.ts @@ -0,0 +1,44 @@ +/* + * 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 { + LogStreamPositionContext, + LogStreamPositionContextWithLatestPosition, + LogStreamPositionContextWithTargetPosition, + LogStreamPositionContextWithVisiblePositions, +} from './types'; + +export type PositionsChangedEvent = { + type: 'POSITIONS_CHANGED'; +} & LogStreamPositionContextWithTargetPosition & + LogStreamPositionContextWithLatestPosition & + LogStreamPositionContextWithVisiblePositions; + +export interface PageEndBufferReachedEvent { + type: 'PAGE_END_BUFFER_REACHED'; +} + +export type LogStreamPositionNotificationEvent = PositionsChangedEvent | PageEndBufferReachedEvent; + +export const LogStreamPositionNotificationEventSelectors = { + positionsChanged: (context: LogStreamPositionContext) => { + return 'targetPosition' in context && + 'latestPosition' in context && + 'visiblePositions' in context + ? ({ + type: 'POSITIONS_CHANGED', + targetPosition: context.targetPosition, + latestPosition: context.latestPosition, + visiblePositions: context.visiblePositions, + } as LogStreamPositionNotificationEvent) + : undefined; + }, + pageEndBufferReached: (context: LogStreamPositionContext) => + ({ + type: 'PAGE_END_BUFFER_REACHED', + } as LogStreamPositionNotificationEvent), +}; diff --git a/x-pack/plugins/infra/public/observability_logs/log_stream_position_state/src/state_machine.ts b/x-pack/plugins/infra/public/observability_logs/log_stream_position_state/src/state_machine.ts new file mode 100644 index 0000000000000..d4053d14ab304 --- /dev/null +++ b/x-pack/plugins/infra/public/observability_logs/log_stream_position_state/src/state_machine.ts @@ -0,0 +1,226 @@ +/* + * 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 { IKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public'; +import { actions, ActorRefFrom, createMachine, EmittedFrom, SpecialTargets } from 'xstate'; +import { isSameTimeKey } from '../../../../common/time'; +import { OmitDeprecatedState, sendIfDefined } from '../../xstate_helpers'; +import { DESIRED_BUFFER_PAGES, RELATIVE_END_UPDATE_DELAY } from './defaults'; +import { LogStreamPositionNotificationEventSelectors } from './notifications'; +import type { + LogStreamPositionContext, + LogStreamPositionContextWithLatestPosition, + LogStreamPositionContextWithTargetPosition, + LogStreamPositionContextWithVisiblePositions, + LogStreamPositionEvent, + LogStreamPositionTypestate, +} from './types'; +import { initializeFromUrl, updateContextInUrl } from './url_state_storage_service'; + +export const createPureLogStreamPositionStateMachine = (initialContext: LogStreamPositionContext) => + /** @xstate-layout N4IgpgJg5mDOIC5QBsD2UDKAXATmAhgLYAK+M2+WYAdAK4B2Alk1o-sowF6QDEAMgHkAggBEAkgDkA4gH1BsgGpiAogHUZGACpCASpuUiA2gAYAuolAAHVLEatU9CyAAeiALQAmAJzUPHgGwArIEALAAcHmHhXsEhADQgAJ6IAQDM1IHGYQDs2ZnZxl5e-l6pAL5lCWiYuAQkZGAUVHRMLGwc3BD8wuLScgKKKuoAYkJifAYm5kgg1rb2jjOuCJ4hAIzUqZklHgX+xqlrkQnJCKlB1P4loYH+qV7hHoEVVejYeESk5FiUNAzMdnaXF4glEklk8hkSjUGgAqgBheHKAyTMxOOaAhxOZbZNbZajGXLBVIkrJrYInRBHYzUEIeVIhVJhNZZLws7IhF4garvOpfRo-Zr-NrsYFdUG9CEDKFDOGI5EiSZraZWGyYxagHEhEIEwkeI7Zc7GO6UhCZHzZML7MKBDwhQp0zmVblvWqfBpNGhofAQZhQPjoBSMMAAd26YL6kOhIzGEyMaJmGIW2JS4VpJTCWXp5K82Q8pvN1Et1tt9oedq5PLd9W+v2o3t99H9geDYYl4P6gxhGARSJR8ZVszVyaWiEZPnt-m8Ry8xjta38pqN1FK+uy+w8xhCgS8lddHxrArrDb9AagQdD4clnZl3d7CqVg6TjCxo4QISumy2MWNMWiAVNO58WMFkImMOc50CMI9xqA9+U9etUB9U8W1DYZ8EYZAQR6Dso1lLRdH0Ad0WHF8NRcdxvF8AJYgiKIwhiUIC1SDwMgNcC8g5O1nmdKs4I9QUaAAC3wWAzwvEMxHoX0AGM4BaAFWFFToeB0ZQkTEBQDBkSQxE0MQhD4GRiF0IQAFllH0HQMCmEj5jIlMEBnfxqAiYojjWVJCjnJcwnSIIrSuNZZ2CGJyl4-c+QEusRLE1DJOkxg5NgBSRQ6XgFEMsQRBkABFWFlB0ABNGR4QACSEaRUSfUjX01Kl-DyWk1giW0p3tElTTxfFwhCPYQIXXE1idV5YKi2tmli8TWyk2T5OFQFlN4SRMr4bK8oK4rSoqqriMTWryOWBdGtckCQMCEknn8MIl21FcWLxVJDUzfwWv8GDeXdCbhNE6bQ1mpL5MUoEVNW9b8sKkrysqqRqrs9VHMGwJmtagI7QOVICznFd1yebdMgutY1gqZ16FQCA4CcPjxqPKh4ZHeqVkiHwtl-XZjQOTzTTcNk2NtQ4-A5q0PureDBNSxb0ogemHLfOkurpahyXArIbTZIItxF-jvsQ5Cmz+kMZbqiiEF2DJQiuK0bVyH94iSRAmTCTZ3ICPMtj8HjRs+w8EJPfX4vQzDICNw7EGR5k7S8NJGrZNXAJ3IsnvtImt28aCIrGr7aZ+uLzxmxLkpDxHNxpC7dn2K404pe331YrwokNQ1GIF7ItZphCpvigHkolpSpaLt8jjTMv12NKd6+r04nlYgbDjxO0-KnNus4736u4LoG0rFAfGc8qIi0Cck1cCQ1K4LJ4iznS1zjzG0WOXn3xcIRhYFsf28-+jf4H2+zjaOw4XKbijt4YIWRbjZHjhaJ6T1cSwIxiTMoQA */ + createMachine( + { + context: initialContext, + predictableActionArguments: true, + id: 'logStreamPositionState', + initial: 'uninitialized', + states: { + uninitialized: { + meta: { + _DX_warning_: + "The Position machine cannot initializeFromUrl until after the Query machine has initialized, this is due to a dual dependency on the 'logPosition' URL parameter for legacy reasons.", + }, + on: { + RECEIVED_INITIAL_QUERY_PARAMETERS: { + target: 'initializingFromUrl', + }, + }, + }, + initializingFromUrl: { + on: { + INITIALIZED_FROM_URL: [ + { + target: 'initialized', + actions: ['storeTargetPosition', 'storeLatestPosition'], + }, + ], + }, + invoke: { + src: 'initializeFromUrl', + }, + }, + initialized: { + type: 'parallel', + states: { + positions: { + initial: 'initialized', + states: { + initialized: { + entry: ['updateContextInUrl', 'notifyPositionsChanged'], + on: { + JUMP_TO_TARGET_POSITION: { + target: 'initialized', + actions: ['updateTargetPosition'], + }, + REPORT_VISIBLE_POSITIONS: { + target: 'initialized', + actions: ['updateVisiblePositions'], + }, + TIME_CHANGED: { + target: 'initialized', + actions: ['updatePositionsFromTimeChange'], + }, + }, + }, + }, + }, + throttlingPageEndNotifications: { + initial: 'idle', + states: { + idle: { + on: { + REPORT_VISIBLE_POSITIONS: { + target: 'throttling', + }, + }, + }, + throttling: { + after: { + [RELATIVE_END_UPDATE_DELAY]: [ + { + target: 'notifying', + cond: 'hasReachedPageEndBuffer', + }, + { + target: 'idle', + }, + ], + }, + on: { + REPORT_VISIBLE_POSITIONS: { + target: 'throttling', + }, + }, + }, + notifying: { + entry: ['notifyPageEndBufferReached'], + always: 'idle', + }, + }, + }, + }, + }, + }, + }, + { + actions: { + notifyPositionsChanged: actions.pure(() => undefined), + notifyPageEndBufferReached: actions.pure(() => undefined), + storeTargetPosition: actions.assign((_context, event) => + 'targetPosition' in event + ? ({ + targetPosition: event.targetPosition, + } as LogStreamPositionContextWithTargetPosition) + : {} + ), + storeLatestPosition: actions.assign((_context, event) => + 'latestPosition' in event + ? ({ + latestPosition: event.latestPosition, + } as LogStreamPositionContextWithLatestPosition) + : {} + ), + updateTargetPosition: actions.assign((_context, event) => { + if (!('targetPosition' in event)) return {}; + + const nextTargetPosition = event.targetPosition?.time + ? { + time: event.targetPosition.time, + tiebreaker: event.targetPosition.tiebreaker ?? 0, + } + : null; + + const nextLatestPosition = !isSameTimeKey(_context.targetPosition, nextTargetPosition) + ? nextTargetPosition + : _context.latestPosition; + + return { + targetPosition: nextTargetPosition, + latestPosition: nextLatestPosition, + } as LogStreamPositionContextWithLatestPosition & + LogStreamPositionContextWithTargetPosition; + }), + updatePositionsFromTimeChange: actions.assign((_context, event) => { + if (!('timeRange' in event)) return {}; + + // Reset the target position if it doesn't fall within the new range. + const targetPositionShouldReset = + _context.targetPosition && + (event.timestamps.startTimestamp > _context.targetPosition.time || + event.timestamps.endTimestamp < _context.targetPosition.time); + + return { + targetPosition: targetPositionShouldReset ? null : _context.targetPosition, + latestPosition: targetPositionShouldReset ? null : _context.latestPosition, + } as LogStreamPositionContextWithLatestPosition & + LogStreamPositionContextWithTargetPosition; + }), + updateVisiblePositions: actions.assign((_context, event) => + 'visiblePositions' in event + ? ({ + visiblePositions: event.visiblePositions, + latestPosition: !isSameTimeKey( + _context.visiblePositions.middleKey, + event.visiblePositions.middleKey + ) + ? event.visiblePositions.middleKey + : _context.visiblePositions.middleKey, + } as LogStreamPositionContextWithVisiblePositions) + : {} + ), + }, + guards: { + // User is close to the bottom of the page. + hasReachedPageEndBuffer: (context, event) => + context.visiblePositions.pagesAfterEnd < DESIRED_BUFFER_PAGES, + }, + } + ); + +export type LogStreamPositionStateMachine = ReturnType< + typeof createPureLogStreamPositionStateMachine +>; +export type LogStreamPositionActorRef = OmitDeprecatedState< + ActorRefFrom +>; +export type LogStreamPositionState = EmittedFrom; + +export interface LogStreamPositionStateMachineDependencies { + urlStateStorage: IKbnUrlStateStorage; + toastsService: IToasts; +} + +export const createLogStreamPositionStateMachine = ( + initialContext: LogStreamPositionContext, + { urlStateStorage, toastsService }: LogStreamPositionStateMachineDependencies +) => + createPureLogStreamPositionStateMachine(initialContext).withConfig({ + actions: { + updateContextInUrl: updateContextInUrl({ toastsService, urlStateStorage }), + notifyPositionsChanged: sendIfDefined(SpecialTargets.Parent)( + LogStreamPositionNotificationEventSelectors.positionsChanged + ), + notifyPageEndBufferReached: sendIfDefined(SpecialTargets.Parent)( + LogStreamPositionNotificationEventSelectors.pageEndBufferReached + ), + }, + services: { + initializeFromUrl: initializeFromUrl({ toastsService, urlStateStorage }), + }, + }); diff --git a/x-pack/plugins/infra/public/observability_logs/log_stream_position_state/src/types.ts b/x-pack/plugins/infra/public/observability_logs/log_stream_position_state/src/types.ts new file mode 100644 index 0000000000000..980ca00b7c8e9 --- /dev/null +++ b/x-pack/plugins/infra/public/observability_logs/log_stream_position_state/src/types.ts @@ -0,0 +1,65 @@ +/* + * 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 { TimeKey } from '../../../../common/time'; +import { ReceivedInitialQueryParametersEvent } from '../../log_stream_page/state'; +import { TimeChangedEvent } from '../../log_stream_query_state/src/notifications'; + +export interface VisiblePositions { + startKey: TimeKey | null; + middleKey: TimeKey | null; + endKey: TimeKey | null; + pagesAfterEnd: number; + pagesBeforeStart: number; +} + +export interface LogStreamPositionContextWithTargetPosition { + targetPosition: TimeKey | null; +} + +export interface LogStreamPositionContextWithLatestPosition { + latestPosition: TimeKey | null; +} +export interface LogStreamPositionContextWithVisiblePositions { + visiblePositions: VisiblePositions; +} +export type LogStreamPositionState = LogStreamPositionContextWithTargetPosition & + LogStreamPositionContextWithLatestPosition & + LogStreamPositionContextWithVisiblePositions; + +export type LogStreamPositionTypestate = + | { + value: 'uninitialized'; + context: LogStreamPositionState; + } + | { + value: 'initialized'; + context: LogStreamPositionState; + }; +export type LogStreamPositionContext = LogStreamPositionTypestate['context']; +export type LogStreamPositionStateValue = LogStreamPositionTypestate['value']; + +export interface JumpToTargetPositionEvent { + type: 'JUMP_TO_TARGET_POSITION'; + targetPosition: Partial | null; +} + +export interface ReportVisiblePositionsEvent { + type: 'REPORT_VISIBLE_POSITIONS'; + visiblePositions: VisiblePositions; +} + +export type LogStreamPositionEvent = + | { + type: 'INITIALIZED_FROM_URL'; + latestPosition: TimeKey | null; + targetPosition: TimeKey | null; + } + | ReceivedInitialQueryParametersEvent + | JumpToTargetPositionEvent + | ReportVisiblePositionsEvent + | TimeChangedEvent; diff --git a/x-pack/plugins/infra/public/observability_logs/log_stream_position_state/src/url_state_storage_service.ts b/x-pack/plugins/infra/public/observability_logs/log_stream_position_state/src/url_state_storage_service.ts new file mode 100644 index 0000000000000..219a2f0105e07 --- /dev/null +++ b/x-pack/plugins/infra/public/observability_logs/log_stream_position_state/src/url_state_storage_service.ts @@ -0,0 +1,111 @@ +/* + * 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 { IKbnUrlStateStorage, withNotifyOnErrors } from '@kbn/kibana-utils-plugin/public'; +import * as Either from 'fp-ts/lib/Either'; +import { pipe } from 'fp-ts/lib/function'; +import * as rt from 'io-ts'; +import { InvokeCreator } from 'xstate'; +import { minimalTimeKeyRT, pickTimeKey } from '../../../../common/time'; +import { createPlainError, formatErrors } from '../../../../common/runtime_types'; +import { replaceStateKeyInQueryString } from '../../../utils/url_state'; +import type { LogStreamPositionContext, LogStreamPositionEvent } from './types'; +interface LogStreamPositionUrlStateDependencies { + positionStateKey?: string; + toastsService: IToasts; + urlStateStorage: IKbnUrlStateStorage; +} + +export const defaultPositionStateKey = 'logPosition'; + +export const updateContextInUrl = + ({ + urlStateStorage, + positionStateKey = defaultPositionStateKey, + }: LogStreamPositionUrlStateDependencies) => + (context: LogStreamPositionContext, _event: LogStreamPositionEvent) => { + if (!('latestPosition' in context)) { + throw new Error('Missing keys from context needed to sync to the URL'); + } + + urlStateStorage.set( + positionStateKey, + positionStateInUrlRT.encode({ + position: context.latestPosition ? pickTimeKey(context.latestPosition) : null, + }) + ); + }; + +export const initializeFromUrl = + ({ + positionStateKey = defaultPositionStateKey, + urlStateStorage, + toastsService, + }: LogStreamPositionUrlStateDependencies): InvokeCreator< + LogStreamPositionContext, + LogStreamPositionEvent + > => + (_context, _event) => + (send) => { + const positionQueryValueFromUrl = urlStateStorage.get(positionStateKey) ?? {}; + + const initialUrlValues = pipe( + decodePositionQueryValueFromUrl(positionQueryValueFromUrl), + Either.map(({ position }) => ({ + targetPosition: position?.time + ? { + time: position.time, + tiebreaker: position.tiebreaker ?? 0, + } + : null, + })), + Either.map(({ targetPosition }) => ({ + targetPosition, + latestPosition: targetPosition, + })) + ); + + if (Either.isLeft(initialUrlValues)) { + withNotifyOnErrors(toastsService).onGetError( + createPlainError(formatErrors(initialUrlValues.left)) + ); + + send({ + type: 'INITIALIZED_FROM_URL', + targetPosition: null, + latestPosition: null, + }); + } else { + send({ + type: 'INITIALIZED_FROM_URL', + targetPosition: initialUrlValues.right.targetPosition ?? null, + latestPosition: initialUrlValues.right.latestPosition ?? null, + }); + } + }; + +export const positionStateInUrlRT = rt.partial({ + position: rt.union([rt.partial(minimalTimeKeyRT.props), rt.null]), +}); + +export type PositionStateInUrl = rt.TypeOf; + +const decodePositionQueryValueFromUrl = (queryValueFromUrl: unknown) => { + return positionStateInUrlRT.decode(queryValueFromUrl); +}; + +// Used by linkTo components +export const replaceLogPositionInQueryString = (time: number) => + Number.isNaN(time) + ? (value: string) => value + : replaceStateKeyInQueryString(defaultPositionStateKey, { + position: { + time, + tiebreaker: 0, + }, + }); diff --git a/x-pack/plugins/infra/public/observability_logs/log_stream_query_state/src/defaults.ts b/x-pack/plugins/infra/public/observability_logs/log_stream_query_state/src/defaults.ts new file mode 100644 index 0000000000000..243b01f4b9570 --- /dev/null +++ b/x-pack/plugins/infra/public/observability_logs/log_stream_query_state/src/defaults.ts @@ -0,0 +1,22 @@ +/* + * 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. + */ + +export const DEFAULT_QUERY = { + language: 'kuery', + query: '', +}; + +export const DEFAULT_FILTERS = []; + +export const DEFAULT_TIMERANGE = { + from: 'now-1d', + to: 'now', +}; + +export const DEFAULT_REFRESH_TIME_RANGE = DEFAULT_TIMERANGE; + +export const DEFAULT_REFRESH_INTERVAL = { pause: true, value: 5000 }; diff --git a/x-pack/plugins/infra/public/observability_logs/log_stream_query_state/src/index.ts b/x-pack/plugins/infra/public/observability_logs/log_stream_query_state/src/index.ts index 5d89520ed2ac5..b9f6065f99092 100644 --- a/x-pack/plugins/infra/public/observability_logs/log_stream_query_state/src/index.ts +++ b/x-pack/plugins/infra/public/observability_logs/log_stream_query_state/src/index.ts @@ -9,3 +9,5 @@ export * from './errors'; export * from './state_machine'; export * from './types'; export * from './url_state_storage_service'; +export * from './time_filter_state_service'; +export * from './defaults'; diff --git a/x-pack/plugins/infra/public/observability_logs/log_stream_query_state/src/notifications.ts b/x-pack/plugins/infra/public/observability_logs/log_stream_query_state/src/notifications.ts index 3599144d9fd4b..51fba835c22ad 100644 --- a/x-pack/plugins/infra/public/observability_logs/log_stream_query_state/src/notifications.ts +++ b/x-pack/plugins/infra/public/observability_logs/log_stream_query_state/src/notifications.ts @@ -5,7 +5,15 @@ * 2.0. */ -import { LogStreamQueryContext, ParsedQuery } from './types'; +import { RefreshInterval } from '@kbn/data-plugin/public'; +import { ExtendedTimeRange, LogStreamQueryContext, ParsedQuery, Timestamps } from './types'; + +export interface TimeChangedEvent { + type: 'TIME_CHANGED'; + timeRange: ExtendedTimeRange; + refreshInterval: RefreshInterval; + timestamps: Timestamps; +} export type LogStreamQueryNotificationEvent = | { @@ -16,7 +24,8 @@ export type LogStreamQueryNotificationEvent = type: 'INVALID_QUERY_CHANGED'; parsedQuery: ParsedQuery; error: Error; - }; + } + | TimeChangedEvent; export const logStreamQueryNotificationEventSelectors = { validQueryChanged: (context: LogStreamQueryContext) => @@ -34,4 +43,14 @@ export const logStreamQueryNotificationEventSelectors = { error: context.validationError, } as LogStreamQueryNotificationEvent) : undefined, + timeChanged: (context: LogStreamQueryContext) => { + return 'timeRange' in context && 'refreshInterval' in context && 'timestamps' in context + ? ({ + type: 'TIME_CHANGED', + timeRange: context.timeRange, + refreshInterval: context.refreshInterval, + timestamps: context.timestamps, + } as LogStreamQueryNotificationEvent) + : undefined; + }, }; diff --git a/x-pack/plugins/infra/public/observability_logs/log_stream_query_state/src/state_machine.ts b/x-pack/plugins/infra/public/observability_logs/log_stream_query_state/src/state_machine.ts index 951551534d742..bbae54bf8803e 100644 --- a/x-pack/plugins/infra/public/observability_logs/log_stream_query_state/src/state_machine.ts +++ b/x-pack/plugins/infra/public/observability_logs/log_stream_query_state/src/state_machine.ts @@ -6,10 +6,14 @@ */ import { IToasts } from '@kbn/core-notifications-browser'; -import type { FilterManager, QueryStringContract } from '@kbn/data-plugin/public'; +import type { + FilterManager, + QueryStringContract, + TimefilterContract, +} from '@kbn/data-plugin/public'; import { EsQueryConfig } from '@kbn/es-query'; import { IKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public'; -import { actions, ActorRefFrom, createMachine, SpecialTargets } from 'xstate'; +import { actions, ActorRefFrom, createMachine, SpecialTargets, send } from 'xstate'; import { OmitDeprecatedState, sendIfDefined } from '../../xstate_helpers'; import { logStreamQueryNotificationEventSelectors } from './notifications'; import { @@ -24,6 +28,9 @@ import type { LogStreamQueryContextWithFilters, LogStreamQueryContextWithParsedQuery, LogStreamQueryContextWithQuery, + LogStreamQueryContextWithRefreshInterval, + LogStreamQueryContextWithTime, + LogStreamQueryContextWithTimeRange, LogStreamQueryContextWithValidationError, LogStreamQueryEvent, LogStreamQueryTypestate, @@ -31,15 +38,24 @@ import type { import { initializeFromUrl, safeDefaultParsedQuery, - updateFiltersInUrl, - updateQueryInUrl, + updateContextInUrl, } from './url_state_storage_service'; +import { + initializeFromTimeFilterService, + subscribeToTimeFilterServiceChanges, + updateTimeContextFromTimeFilterService, + updateTimeContextFromTimeRangeUpdate, + updateTimeContextFromRefreshIntervalUpdate, + updateTimeInTimeFilterService, + updateTimeContextFromUrl, +} from './time_filter_state_service'; import { showValidationErrorToast, validateQuery } from './validate_query_service'; +import { DEFAULT_REFRESH_INTERVAL, DEFAULT_REFRESH_TIME_RANGE } from './defaults'; export const createPureLogStreamQueryStateMachine = ( - initialContext: LogStreamQueryContextWithDataViews + initialContext: LogStreamQueryContextWithDataViews & LogStreamQueryContextWithTime ) => - /** @xstate-layout N4IgpgJg5mDOIC5QEUCuYBOBPAdKgdgJZEAuhAhgDaEBekAxANoAMAuoqAA4D2shZ3fBxAAPRAFoATAHZJOAMwBWJQBYAjNIBszAJwAOTZIA0ILBMnyca+Wp2bFenfL1b5zTQF8PJtJlzF+CmoaYigAMQxuAFsAVQxKegBJADlEgBVEgEEAGUSALQBRABEAfTCAJQB5AFkSmPLslnYkEB4+ASEWsQRxeRU5PXdNA001RX1tRRMzBBkdHHVmFWZpHTVJC2ZFFS8fdGwcAAtyWF9semQYgvKATTKq2oBlAszygGEACRKAIVeSz8yyQA4sUmsI2oFBMJupIVNIcLoVHpBoppLJFMZTIh7DgdMsnNINHZlip5LsQGdcMdTvssPQwolsmlro97jUSs9Xp8fn8AcDQWxwbxIZ1QN1tHICWoXJJNDoMfJ5NNsYpcfj5ITVpoSZ5vBTaUcTpT6EVMmlMiUAGqJAoAdVZfJBRTBLQhHWhiDscllKwsKnUOmYajUyoQqysmmkKh00mYbijjkU5MphppfhwGDAADcqIQIOQyPgoPRLTlEqaMpVkmVMoyBc0uML3V1PZHcfK1P6UXY0aG3JZ5Pp1WpRgZJEm9SnqSnMznqPnC8XS7kK4kqxyYm83gVivWhe1CFCWwhB8wFjJRvJJINZZHpH24woh7obKPDBO9um53mC6ES2XV3XR5N23XdnUFV0m0PUVRAkNZcVkUY8UUQxmDjPRQ39NQcCjNENUkWwVEUJZpGTA1vwXP9l3LM012rMJa2yPdIIPI8xQkaRLB0CZiO2aQ9EkJxQxQ1UBKUZg9HWUl1FI8l8G4CA4GESl9xFD0elPHBBk0YYdLGCYtlDKRtARcYtivFw0S0Mj0wIAIyFzOgIFU5t2J6QS9BwDYVmlHRYR0rZNCMjRsJHST-RE8dOJ2ScDXsoJaFCCJojiSgXOg9Ten6LzI1GawrKvWFQ07SxZE41Y1jsawP31dNp1pdK2NghBbHmRFkS2NFx0xGZxA0ORFDMyMPNWcYbIOeqv1zZyWLU48NWwyqtTcXQ9FJTDlgQ-pkW1HbJPGqkjTi-AKMamDuj8lQEWlBwVAlWUrw2s8Y22gwkQMfbYrqo701nabfyLM71NjbCBOWCTfLhdxQ1hM9ZScPQNW4xQR2sA6cAogGoCB49eiULTL1RPQMUDQcpixBB-WeqNx2lIlEdkrwgA */ + /** @xstate-layout N4IgpgJg5mDOIC5QEUCuYBOBPAdKgdgJZEAuhAhgDaEBekAxANoAMAuoqAA4D2shZ3fBxAAPRAEYAHAGYck8QCYArAoAsC8QHZNygJySANCCwTpszc2njpANnH2FN1UtUBfV0bSZcxfhWo0xFAAYhjcALYAqhiU9ACSAHJxACpxAIIAMnEAWgCiACIA+sEASgDyALKFkSUZLOxIIDx8AkKNYghKzJJyCvJOurqqA4bGiPI4XczTdjbM2kq64u6e6Ng4vmRUtEGhEdGxiSnpWXlFpZXVtYziDVy8foLCHSpGJgjizj02SjYa3X0dH8ViAvOtNv4dvgQmFwslCOEwMFCJQSJgAMqYABuhAAxmB4klUpkcgViuUqqkKrlinEMslciVCujGQA1OIAYVy9WEzUebVAHWkkhsOGmmnkqnUumkzBsmmkb0Quk0ot0coUZnscukSkkILBPlIkLoEBwAEc1lh6MhIoyAJrky4stIlDkACUKACFXYUPWkEgBxAo8xp81rPcbiJQ4GzSPoKOXMX7RxVjD7MVTiOTyxOSVT5zRKCUGq0bY3bU0Wq30YJ0hkldFOqout2en1M-1BkNsXkPCPtKMxuMJpMppRp95LHrDZi6CfSbQ2STF0vect+SuQaveej5NLJNKFdm5ADqTa7wfyofuLUIT0HCHkw-jkkTc3Hk-GCgUYvm8kGAtCzXcEKwCbdLXXLFtggcgyGhehWRJfdUjKBJmUiDkuQKHs7iaft7wFUQJCzVQcGkdU7GjOdxF0JUECGZhyMUJRFGYBMVX1DxQTLCEtzNSD1mg6hYPgqBEOQg84jQ4o0jpXC+zvB9BRIz5yMo+wuiWOj03sOYcC0Wi+gA7RkxAo1N3AgSywwMBhMIUSggkrIUOk9DgjkjIFLDAjlOIj5SPUuVNJonT3nESxRVVX4FU0RQbH0aRzI3LYrJ3dZbPsxyEKQlypJk9FMOw-JvNvflIwCtSKOC6jtPo-ocEo5QEo0H9fmSvi0rIRF6CpGkLkpOJqVpelGWZNlORpS9SvwpSiI6Z9Y1fd9kzsCd6KkVRNFjX5-l0BQLCLLjVnXTraG3bqCUiAAFFCaT6woSgDYMb1m8rH0Wkc3zHNavw+eUmOkVRF2XFVhjmGwOrA86zUu+gbrux7clKXJ0U9RIG1y17w0IirPuWn7Uw21QZTFKx4wsH81CUJQocsmGcEulKTQYbHfPm0ws0mTVweBiV51UDadF0SYZDMYHJE0KUtrp1KGaZs7TSYW5FPelSPiB7MVCBmwnEXSQBaFyQem6LUpYogsdHcbj8G4CA4GEQ1VYHdWAFobHo12Y0GH3fb9pLuMNPAiGh01ndxx91A2+McAsKw7F1I7AVlk1dlhA5w78joDp6A383lRctC0wXdLnHBEysTU40UA2ZcD3jQ7TiJ4URZFUQxbE8TATOOYQeV6MGQHFkXRd51owYU-4nuKrikWvpWz96JJpimrsRwJQLaNJ7SwT3jKl3-NVbb58J9b02LGcNXJn8Cx+beGd3nAsrgoJp8fexJfL+Q2q0AtbBLqcLhJgUQiuIP4zgwHHR4qdUOEEyxZTfurMButYzzllBROYUo-j1WcDgKUjhjaEMsNYe+VZH7EAQT5OaFVkGigShOSwuhMHDAUBtOUMZZQaGBrqXUIo3D1xgfTMhNk7IwRftCRB-kP7bT6IoZQv8ZBOBwWROYMoZQ-klpITMpCLoIm7lQtWh95RLVHB+X60cY4TmsOqQuqot4CNAkI3RiJmZTwMQfDoigtCNWilote0wlBCyBuXEm+hpQWF1jo2GeicCwBIC-XEkjPGsTIrRCcE5dYS1okbUUZgzAHWTEDSwUTGYxLibZcg4RX7uIjkglJBk0EZL1gBIWB05B5OjFLYs+hNDW1cEAA */ createMachine( { context: initialContext, @@ -50,93 +66,161 @@ export const createPureLogStreamQueryStateMachine = ( states: { uninitialized: { always: { - target: 'initializingFromUrl', + target: 'initializingFromTimeFilterService', }, }, initializingFromUrl: { on: { INITIALIZED_FROM_URL: { - target: 'validating', - actions: ['storeQuery', 'storeFilters'], + target: 'initialized', + actions: ['storeQuery', 'storeFilters', 'updateTimeContextFromUrl'], }, }, - invoke: { src: 'initializeFromUrl', }, }, - - hasQuery: { - entry: ['updateQueryInUrl', 'updateQueryInSearchBar', 'updateFiltersInSearchBar'], - invoke: [ - { - src: 'subscribeToQuerySearchBarChanges', - }, - { - src: 'subscribeToFilterSearchBarChanges', + initializingFromTimeFilterService: { + on: { + INITIALIZED_FROM_TIME_FILTER_SERVICE: { + target: 'initializingFromUrl', + actions: ['updateTimeContextFromTimeFilterService'], }, - ], - initial: 'revalidating', + }, + invoke: { + src: 'initializeFromTimeFilterService', + }, + }, + initialized: { + type: 'parallel', states: { - valid: { - entry: 'notifyValidQueryChanged', - }, - invalid: { - entry: 'notifyInvalidQueryChanged', - }, - revalidating: { - invoke: { - src: 'validateQuery', - }, - on: { - VALIDATION_FAILED: { - target: 'invalid', - actions: ['storeValidationError', 'showValidationErrorToast'], + query: { + entry: ['updateContextInUrl', 'updateQueryInSearchBar', 'updateFiltersInSearchBar'], + invoke: [ + { + src: 'subscribeToQuerySearchBarChanges', }, - VALIDATION_SUCCEEDED: { - target: 'valid', - actions: ['clearValidationError', 'storeParsedQuery'], + { + src: 'subscribeToFilterSearchBarChanges', + }, + ], + initial: 'validating', + states: { + validating: { + invoke: { + src: 'validateQuery', + }, + on: { + VALIDATION_SUCCEEDED: { + target: 'valid', + actions: 'storeParsedQuery', + }, + + VALIDATION_FAILED: { + target: 'invalid', + actions: [ + 'storeValidationError', + 'storeDefaultParsedQuery', + 'showValidationErrorToast', + ], + }, + }, + }, + valid: { + entry: 'notifyValidQueryChanged', + }, + invalid: { + entry: 'notifyInvalidQueryChanged', + }, + revalidating: { + invoke: { + src: 'validateQuery', + }, + on: { + VALIDATION_FAILED: { + target: 'invalid', + actions: ['storeValidationError', 'showValidationErrorToast'], + }, + VALIDATION_SUCCEEDED: { + target: 'valid', + actions: ['clearValidationError', 'storeParsedQuery'], + }, + }, }, }, - }, - }, - on: { - QUERY_FROM_SEARCH_BAR_CHANGED: { - target: '.revalidating', - actions: ['storeQuery', 'updateQueryInUrl'], - }, + on: { + QUERY_FROM_SEARCH_BAR_CHANGED: { + target: '.revalidating', + actions: ['storeQuery', 'updateContextInUrl'], + }, - FILTERS_FROM_SEARCH_BAR_CHANGED: { - target: '.revalidating', - actions: ['storeFilters', 'updateFiltersInUrl'], - }, + FILTERS_FROM_SEARCH_BAR_CHANGED: { + target: '.revalidating', + actions: ['storeFilters', 'updateContextInUrl'], + }, - DATA_VIEWS_CHANGED: { - target: '.revalidating', - actions: 'storeDataViews', + DATA_VIEWS_CHANGED: { + target: '.revalidating', + actions: 'storeDataViews', + }, + }, }, - }, - }, - - validating: { - invoke: { - src: 'validateQuery', - }, + time: { + initial: 'initialized', + entry: ['notifyTimeChanged', 'updateTimeInTimeFilterService'], + invoke: [ + { + src: 'subscribeToTimeFilterServiceChanges', + }, + ], + states: { + initialized: { + always: [{ target: 'streaming', cond: 'isStreaming' }, { target: 'static' }], + }, + static: { + on: { + PAGE_END_BUFFER_REACHED: { + actions: ['expandPageEnd'], + }, + }, + }, + streaming: { + after: { + refresh: { target: 'streaming', actions: ['refreshTime'] }, + }, + }, + }, + on: { + TIME_FROM_TIME_FILTER_SERVICE_CHANGED: { + target: '.initialized', + actions: [ + 'updateTimeContextFromTimeFilterService', + 'notifyTimeChanged', + 'updateContextInUrl', + ], + }, - on: { - VALIDATION_SUCCEEDED: { - target: 'hasQuery.valid', - actions: 'storeParsedQuery', - }, + UPDATE_TIME_RANGE: { + target: '.initialized', + actions: [ + 'updateTimeContextFromTimeRangeUpdate', + 'notifyTimeChanged', + 'updateTimeInTimeFilterService', + 'updateContextInUrl', + ], + }, - VALIDATION_FAILED: { - target: 'hasQuery.invalid', - actions: [ - 'storeValidationError', - 'storeDefaultParsedQuery', - 'showValidationErrorToast', - ], + UPDATE_REFRESH_INTERVAL: { + target: '.initialized', + actions: [ + 'updateTimeContextFromRefreshIntervalUpdate', + 'notifyTimeChanged', + 'updateTimeInTimeFilterService', + 'updateContextInUrl', + ], + }, + }, }, }, }, @@ -146,12 +230,25 @@ export const createPureLogStreamQueryStateMachine = ( actions: { notifyInvalidQueryChanged: actions.pure(() => undefined), notifyValidQueryChanged: actions.pure(() => undefined), + notifyTimeChanged: actions.pure(() => undefined), storeQuery: actions.assign((_context, event) => { return 'query' in event ? ({ query: event.query } as LogStreamQueryContextWithQuery) : {}; }), storeFilters: actions.assign((_context, event) => 'filters' in event ? ({ filters: event.filters } as LogStreamQueryContextWithFilters) : {} ), + storeTimeRange: actions.assign((_context, event) => + 'timeRange' in event + ? ({ timeRange: event.timeRange } as LogStreamQueryContextWithTimeRange) + : {} + ), + storeRefreshInterval: actions.assign((_context, event) => + 'refreshInterval' in event + ? ({ + refreshInterval: event.refreshInterval, + } as LogStreamQueryContextWithRefreshInterval) + : {} + ), storeDataViews: actions.assign((_context, event) => 'dataViews' in event ? ({ dataViews: event.dataViews } as LogStreamQueryContextWithDataViews) @@ -180,6 +277,22 @@ export const createPureLogStreamQueryStateMachine = ( 'validationError' >) ), + updateTimeContextFromTimeFilterService, + updateTimeContextFromTimeRangeUpdate, + updateTimeContextFromRefreshIntervalUpdate, + refreshTime: send({ type: 'UPDATE_TIME_RANGE', timeRange: DEFAULT_REFRESH_TIME_RANGE }), + expandPageEnd: send({ type: 'UPDATE_TIME_RANGE', timeRange: { to: 'now' } }), + updateTimeContextFromUrl, + }, + guards: { + isStreaming: (context, event) => + 'refreshInterval' in context ? !context.refreshInterval.pause : false, + }, + delays: { + refresh: (context, event) => + 'refreshInterval' in context + ? context.refreshInterval.value + : DEFAULT_REFRESH_INTERVAL.value, }, } ); @@ -190,20 +303,24 @@ export interface LogStreamQueryStateMachineDependencies { filterManagerService: FilterManager; urlStateStorage: IKbnUrlStateStorage; toastsService: IToasts; + timeFilterService: TimefilterContract; } export const createLogStreamQueryStateMachine = ( - initialContext: LogStreamQueryContextWithDataViews, + initialContext: LogStreamQueryContextWithDataViews & LogStreamQueryContextWithTime, { kibanaQuerySettings, queryStringService, toastsService, filterManagerService, urlStateStorage, + timeFilterService, }: LogStreamQueryStateMachineDependencies ) => createPureLogStreamQueryStateMachine(initialContext).withConfig({ actions: { + updateContextInUrl: updateContextInUrl({ toastsService, urlStateStorage }), + // Query notifyInvalidQueryChanged: sendIfDefined(SpecialTargets.Parent)( logStreamQueryNotificationEventSelectors.invalidQueryChanged ), @@ -211,13 +328,17 @@ export const createLogStreamQueryStateMachine = ( logStreamQueryNotificationEventSelectors.validQueryChanged ), showValidationErrorToast: showValidationErrorToast({ toastsService }), - updateQueryInUrl: updateQueryInUrl({ toastsService, urlStateStorage }), updateQueryInSearchBar: updateQueryInSearchBar({ queryStringService }), - updateFiltersInUrl: updateFiltersInUrl({ toastsService, urlStateStorage }), updateFiltersInSearchBar: updateFiltersInSearchBar({ filterManagerService }), + // Time + updateTimeInTimeFilterService: updateTimeInTimeFilterService({ timeFilterService }), + notifyTimeChanged: sendIfDefined(SpecialTargets.Parent)( + logStreamQueryNotificationEventSelectors.timeChanged + ), }, services: { initializeFromUrl: initializeFromUrl({ toastsService, urlStateStorage }), + initializeFromTimeFilterService: initializeFromTimeFilterService({ timeFilterService }), validateQuery: validateQuery({ kibanaQuerySettings }), subscribeToQuerySearchBarChanges: subscribeToQuerySearchBarChanges({ queryStringService, @@ -225,6 +346,9 @@ export const createLogStreamQueryStateMachine = ( subscribeToFilterSearchBarChanges: subscribeToFilterSearchBarChanges({ filterManagerService, }), + subscribeToTimeFilterServiceChanges: subscribeToTimeFilterServiceChanges({ + timeFilterService, + }), }, }); diff --git a/x-pack/plugins/infra/public/observability_logs/log_stream_query_state/src/time_filter_state_service.ts b/x-pack/plugins/infra/public/observability_logs/log_stream_query_state/src/time_filter_state_service.ts new file mode 100644 index 0000000000000..aaa610ea1ef12 --- /dev/null +++ b/x-pack/plugins/infra/public/observability_logs/log_stream_query_state/src/time_filter_state_service.ts @@ -0,0 +1,188 @@ +/* + * 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 { map, merge } from 'rxjs'; +import { actions, InvokeCreator } from 'xstate'; +import { datemathToEpochMillis } from '../../../utils/datemath'; +import { DEFAULT_REFRESH_TIME_RANGE } from './defaults'; +import { LogStreamQueryContext, LogStreamQueryEvent } from './types'; + +export interface TimefilterState { + timeRange: TimeRange; + refreshInterval: RefreshInterval; +} + +export const initializeFromTimeFilterService = + ({ + timeFilterService, + }: { + timeFilterService: TimefilterContract; + }): InvokeCreator => + (_context, _event) => + (send) => { + const timeRange = timeFilterService.getTime(); + const refreshInterval = timeFilterService.getRefreshInterval(); + + send({ + type: 'INITIALIZED_FROM_TIME_FILTER_SERVICE', + timeRange, + refreshInterval, + }); + }; + +export const updateTimeInTimeFilterService = + ({ timeFilterService }: { timeFilterService: TimefilterContract }) => + (context: LogStreamQueryContext, event: LogStreamQueryEvent) => { + if ('timeRange' in context) { + timeFilterService.setTime(context.timeRange); + } + + if ('refreshInterval' in context) { + timeFilterService.setRefreshInterval(context.refreshInterval); + } + }; + +export const subscribeToTimeFilterServiceChanges = + ({ + timeFilterService, + }: { + timeFilterService: TimefilterContract; + }): InvokeCreator => + (context) => + merge(timeFilterService.getTimeUpdate$(), timeFilterService.getRefreshIntervalUpdate$()).pipe( + map(() => getTimefilterState(timeFilterService)), + map((timeState): LogStreamQueryEvent => { + return { + type: 'TIME_FROM_TIME_FILTER_SERVICE_CHANGED', + ...timeState, + }; + }) + ); + +const getTimefilterState = (timeFilterService: TimefilterContract): TimefilterState => ({ + timeRange: timeFilterService.getTime(), + refreshInterval: timeFilterService.getRefreshInterval(), +}); + +export const updateTimeContextFromTimeFilterService = actions.assign( + (context: LogStreamQueryContext, event: LogStreamQueryEvent) => { + if ( + event.type === 'TIME_FROM_TIME_FILTER_SERVICE_CHANGED' || + event.type === 'INITIALIZED_FROM_TIME_FILTER_SERVICE' + ) { + return { + ...getTimeFromEvent(context, event), + refreshInterval: event.refreshInterval, + }; + } else { + return {}; + } + } +); + +export const updateTimeContextFromUrl = actions.assign( + (context: LogStreamQueryContext, event: LogStreamQueryEvent) => { + if (event.type === 'INITIALIZED_FROM_URL') { + return { + ...('timeRange' in event && event.timeRange ? { ...getTimeFromEvent(context, event) } : {}), + ...('refreshInterval' in event && event.refreshInterval + ? { refreshInterval: event.refreshInterval } + : {}), + }; + } else { + return {}; + } + } +); + +export const updateTimeContextFromTimeRangeUpdate = actions.assign( + (context: LogStreamQueryContext, event: LogStreamQueryEvent) => { + if ('timeRange' in event && event.type === 'UPDATE_TIME_RANGE') { + return getTimeFromEvent(context, event); + } else { + return {}; + } + } +); + +export const updateTimeContextFromRefreshIntervalUpdate = actions.assign( + (context: LogStreamQueryContext, event: LogStreamQueryEvent) => { + if ( + 'refreshInterval' in event && + 'refreshInterval' in context && + event.type === 'UPDATE_REFRESH_INTERVAL' + ) { + const pause = event.refreshInterval.pause ?? context.refreshInterval.pause; + const value = event.refreshInterval.value ?? context.refreshInterval.value; + + const nowTimestamp = Date.now(); + + const draftContext = { + refreshInterval: { + pause, + value, + }, + ...(!pause + ? { + timeRange: { + ...DEFAULT_REFRESH_TIME_RANGE, + lastChangedCompletely: nowTimestamp, + }, + } + : {}), + ...(!pause + ? { + timestamps: { + startTimestamp: datemathToEpochMillis(DEFAULT_REFRESH_TIME_RANGE.from, 'down') ?? 0, + endTimestamp: datemathToEpochMillis(DEFAULT_REFRESH_TIME_RANGE.to, 'down') ?? 0, + lastChangedTimestamp: nowTimestamp, + }, + } + : {}), + }; + + return draftContext; + } else { + return {}; + } + } +); + +const getTimeFromEvent = (context: LogStreamQueryContext, event: LogStreamQueryEvent) => { + if (!('timeRange' in event) || !('timeRange' in context) || !('timestamps' in context)) { + throw new Error('Missing keys to get time from event'); + } + + const nowTimestamp = Date.now(); + const from = event.timeRange?.from ?? context.timeRange.from; + const to = event.timeRange?.to ?? context.timeRange.to; + + const fromTimestamp = event.timeRange?.from + ? datemathToEpochMillis(from, 'down') + : context.timestamps.startTimestamp; + const toTimestamp = event.timeRange?.to + ? datemathToEpochMillis(to, 'down') + : context.timestamps.endTimestamp; + + return { + timeRange: { + from, + to, + lastChangedCompletely: + event.timeRange?.from && event.timeRange?.to + ? nowTimestamp + : context.timeRange.lastChangedCompletely, + }, + timestamps: { + startTimestamp: fromTimestamp ?? 0, + endTimestamp: toTimestamp ?? 0, + lastChangedTimestamp: nowTimestamp, + }, + }; +}; diff --git a/x-pack/plugins/infra/public/observability_logs/log_stream_query_state/src/types.ts b/x-pack/plugins/infra/public/observability_logs/log_stream_query_state/src/types.ts index 46706254f446f..b56ce2148a17e 100644 --- a/x-pack/plugins/infra/public/observability_logs/log_stream_query_state/src/types.ts +++ b/x-pack/plugins/infra/public/observability_logs/log_stream_query_state/src/types.ts @@ -5,7 +5,9 @@ * 2.0. */ -import { AggregateQuery, BoolQuery, DataViewBase, Query, Filter } from '@kbn/es-query'; +import { RefreshInterval } from '@kbn/data-plugin/public'; +import { AggregateQuery, BoolQuery, DataViewBase, Query, Filter, TimeRange } from '@kbn/es-query'; +import { PageEndBufferReachedEvent } from '../../log_stream_position_state/src/notifications'; export type AnyQuery = Query | AggregateQuery; @@ -36,38 +38,78 @@ export interface LogStreamQueryContextWithValidationError { validationError: Error; } +export type ExtendedTimeRange = TimeRange & { lastChangedCompletely: number }; +export interface LogStreamQueryContextWithTimeRange { + timeRange: ExtendedTimeRange; +} + +export interface LogStreamQueryContextWithRefreshInterval { + refreshInterval: RefreshInterval; +} + +export interface Timestamps { + startTimestamp: number; + endTimestamp: number; + lastChangedTimestamp: number; +} + +export interface LogStreamQueryContextWithTimestamps { + timestamps: Timestamps; +} + +export type LogStreamQueryContextWithTime = LogStreamQueryContextWithTimeRange & + LogStreamQueryContextWithRefreshInterval & + LogStreamQueryContextWithTimestamps; + export type LogStreamQueryTypestate = | { value: 'uninitialized'; - context: LogStreamQueryContextWithDataViews; + context: LogStreamQueryContextWithDataViews & LogStreamQueryContextWithTime; } | { - value: 'hasQuery' | { hasQuery: 'validating' }; + value: 'query' | { query: 'validating' }; context: LogStreamQueryContextWithDataViews & LogStreamQueryContextWithParsedQuery & LogStreamQueryContextWithQuery & - LogStreamQueryContextWithFilters; + LogStreamQueryContextWithFilters & + LogStreamQueryContextWithTime; } | { - value: { hasQuery: 'valid' }; + value: { query: 'valid' }; context: LogStreamQueryContextWithDataViews & LogStreamQueryContextWithParsedQuery & LogStreamQueryContextWithQuery & - LogStreamQueryContextWithFilters; + LogStreamQueryContextWithFilters & + LogStreamQueryContextWithTime; } | { - value: { hasQuery: 'invalid' }; + value: { query: 'invalid' }; context: LogStreamQueryContextWithDataViews & LogStreamQueryContextWithParsedQuery & LogStreamQueryContextWithQuery & LogStreamQueryContextWithFilters & + LogStreamQueryContextWithTime & LogStreamQueryContextWithValidationError; + } + | { + value: 'time' | { time: 'initialized' } | { time: 'streaming' } | { time: 'static' }; + context: LogStreamQueryContextWithDataViews & LogStreamQueryContextWithTime; }; export type LogStreamQueryContext = LogStreamQueryTypestate['context']; export type LogStreamQueryStateValue = LogStreamQueryTypestate['value']; +export interface UpdateTimeRangeEvent { + type: 'UPDATE_TIME_RANGE'; + timeRange: Partial; +} + +export interface UpdateRefreshIntervalEvent { + type: 'UPDATE_REFRESH_INTERVAL'; + refreshInterval: Partial; +} + export type LogStreamQueryEvent = | { type: 'QUERY_FROM_SEARCH_BAR_CHANGED'; @@ -93,4 +135,19 @@ export type LogStreamQueryEvent = type: 'INITIALIZED_FROM_URL'; query: AnyQuery; filters: Filter[]; - }; + timeRange: TimeRange | null; + refreshInterval: RefreshInterval | null; + } + | { + type: 'INITIALIZED_FROM_TIME_FILTER_SERVICE'; + timeRange: TimeRange; + refreshInterval: RefreshInterval; + } + | { + type: 'TIME_FROM_TIME_FILTER_SERVICE_CHANGED'; + timeRange: TimeRange; + refreshInterval: RefreshInterval; + } + | UpdateTimeRangeEvent + | UpdateRefreshIntervalEvent + | PageEndBufferReachedEvent; diff --git a/x-pack/plugins/infra/public/observability_logs/log_stream_query_state/src/url_state_storage_service.ts b/x-pack/plugins/infra/public/observability_logs/log_stream_query_state/src/url_state_storage_service.ts index fabd26cc03d22..3a8d08635dca7 100644 --- a/x-pack/plugins/infra/public/observability_logs/log_stream_query_state/src/url_state_storage_service.ts +++ b/x-pack/plugins/infra/public/observability_logs/log_stream_query_state/src/url_state_storage_service.ts @@ -10,28 +10,53 @@ import { Query } from '@kbn/es-query'; import { IKbnUrlStateStorage, withNotifyOnErrors } from '@kbn/kibana-utils-plugin/public'; import * as Array from 'fp-ts/lib/Array'; import * as Either from 'fp-ts/lib/Either'; -import { pipe } from 'fp-ts/lib/function'; +import { identity, pipe } from 'fp-ts/lib/function'; import * as rt from 'io-ts'; import { InvokeCreator } from 'xstate'; +import { DurationInputObject } from 'moment'; +import moment from 'moment'; +import { minimalTimeKeyRT } from '../../../../common/time'; +import { datemathStringRT } from '../../../utils/datemath'; import { createPlainError, formatErrors } from '../../../../common/runtime_types'; import { replaceStateKeyInQueryString } from '../../../utils/url_state'; import type { LogStreamQueryContext, LogStreamQueryEvent, ParsedQuery } from './types'; +import { + DEFAULT_FILTERS, + DEFAULT_QUERY, + DEFAULT_REFRESH_INTERVAL, + DEFAULT_TIMERANGE, +} from './defaults'; interface LogStreamQueryUrlStateDependencies { filterStateKey?: string; + positionStateKey?: string; savedQueryIdKey?: string; toastsService: IToasts; urlStateStorage: IKbnUrlStateStorage; } const defaultFilterStateKey = 'logFilter'; -const defaultFilterStateValue: Required = { - query: { - language: 'kuery', - query: '', - }, - filters: [], +const defaultPositionStateKey = 'logPosition'; // NOTE: Provides backwards compatibility for start / end / streamLive previously stored under the logPosition key. + +type RequiredDefaults = Required>; +type OptionalDefaults = Pick; +type FullDefaults = Required; + +const requiredDefaultFilterStateValue: RequiredDefaults = { + query: DEFAULT_QUERY, + filters: DEFAULT_FILTERS, +}; + +const optionalDefaultFilterStateValue = { + timeRange: DEFAULT_TIMERANGE, + refreshInterval: DEFAULT_REFRESH_INTERVAL, }; + +const defaultFilterStateValue: FullDefaults = { + ...requiredDefaultFilterStateValue, + ...optionalDefaultFilterStateValue, +}; + export const safeDefaultParsedQuery: ParsedQuery = { bool: { must: [], @@ -41,33 +66,19 @@ export const safeDefaultParsedQuery: ParsedQuery = { }, }; -export const updateQueryInUrl = - ({ - urlStateStorage, - filterStateKey = defaultFilterStateKey, - }: LogStreamQueryUrlStateDependencies) => - (context: LogStreamQueryContext, _event: LogStreamQueryEvent) => { - if (!('query' in context)) { - throw new Error(); - } - - urlStateStorage.set( - filterStateKey, - filterStateInUrlRT.encode({ - query: context.query, - filters: context.filters, - }) - ); - }; - -export const updateFiltersInUrl = +export const updateContextInUrl = ({ urlStateStorage, filterStateKey = defaultFilterStateKey, }: LogStreamQueryUrlStateDependencies) => (context: LogStreamQueryContext, _event: LogStreamQueryEvent) => { - if (!('filters' in context)) { - throw new Error(); + if ( + !('query' in context) || + !('filters' in context) || + !('timeRange' in context) || + !('refreshInterval' in context) + ) { + throw new Error('Missing keys from context needed to sync to the URL'); } urlStateStorage.set( @@ -75,6 +86,8 @@ export const updateFiltersInUrl = filterStateInUrlRT.encode({ query: context.query, filters: context.filters, + timeRange: context.timeRange, + refreshInterval: context.refreshInterval, }) ); }; @@ -82,6 +95,7 @@ export const updateFiltersInUrl = export const initializeFromUrl = ({ filterStateKey = defaultFilterStateKey, + positionStateKey = defaultPositionStateKey, toastsService, urlStateStorage, }: LogStreamQueryUrlStateDependencies): InvokeCreator< @@ -90,23 +104,92 @@ export const initializeFromUrl = > => (_context, _event) => (send) => { - const queryValueFromUrl = urlStateStorage.get(filterStateKey) ?? defaultFilterStateValue; + const filterQueryValueFromUrl = + urlStateStorage.get(filterStateKey) ?? requiredDefaultFilterStateValue; + const filterQueryE = decodeFilterQueryValueFromUrl(filterQueryValueFromUrl); - const queryE = decodeQueryValueFromUrl(queryValueFromUrl); + // NOTE: Access logPosition for backwards compatibility with values previously stored under that key. + const positionQueryValueFromUrl = urlStateStorage.get(positionStateKey) ?? {}; + const positionQueryE = decodePositionQueryValueFromUrl(positionQueryValueFromUrl); - if (Either.isLeft(queryE)) { - withNotifyOnErrors(toastsService).onGetError(createPlainError(formatErrors(queryE.left))); + if (Either.isLeft(filterQueryE) || Either.isLeft(positionQueryE)) { + withNotifyOnErrors(toastsService).onGetError( + createPlainError( + formatErrors([ + ...(Either.isLeft(filterQueryE) ? filterQueryE.left : []), + ...(Either.isLeft(positionQueryE) ? positionQueryE.left : []), + ]) + ) + ); send({ type: 'INITIALIZED_FROM_URL', query: defaultFilterStateValue.query, filters: defaultFilterStateValue.filters, + timeRange: null, + refreshInterval: null, }); } else { send({ type: 'INITIALIZED_FROM_URL', - query: queryE.right.query ?? defaultFilterStateValue.query, - filters: queryE.right.filters ?? defaultFilterStateValue.filters, + query: filterQueryE.right.query ?? defaultFilterStateValue.query, + filters: filterQueryE.right.filters ?? defaultFilterStateValue.filters, + timeRange: pipe( + // Via the logFilter key + pipe( + filterQueryE.right.timeRange, + Either.fromNullable(null), + Either.chain(({ from, to }) => + from && to ? Either.right({ from, to }) : Either.left(null) + ) + ), + // Via the legacy logPosition key, and start / end timeRange parameters + Either.alt(() => + pipe( + positionQueryE.right, + Either.fromNullable(null), + Either.chain(({ start, end }) => + start && end ? Either.right({ from: start, to: end }) : Either.left(null) + ) + ) + ), + // Via the legacy logPosition key, and deriving from / to from position.time + Either.alt(() => + pipe( + positionQueryE.right, + Either.fromNullable(null), + Either.chain(({ position }) => + position && position.time + ? Either.right({ + from: getTimeRangeStartFromTime(position.time), + to: getTimeRangeEndFromTime(position.time), + }) + : Either.left(null) + ) + ) + ), + Either.fold(identity, identity) + ), + refreshInterval: pipe( + // Via the logFilter key + pipe(filterQueryE.right.refreshInterval, Either.fromNullable(null)), + // Via the legacy logPosition key, and the boolean streamLive parameter + Either.alt(() => + pipe( + positionQueryE.right, + Either.fromNullable(null), + Either.chain(({ streamLive }) => + typeof streamLive === 'boolean' + ? Either.right({ + pause: !streamLive, + value: defaultFilterStateValue.refreshInterval.value, // NOTE: Was not previously synced to the URL, so falls straight to the default. + }) + : Either.left(null) + ) + ) + ), + Either.fold(identity, identity) + ), }); } }; @@ -148,9 +231,17 @@ const filterStateInUrlRT = rt.partial({ }), ]), filters: rt.array(filter), + timeRange: rt.strict({ + from: rt.string, + to: rt.string, + }), + refreshInterval: rt.strict({ + pause: rt.boolean, + value: rt.number, + }), }); -type FilterStateInUrl = rt.TypeOf; +export type FilterStateInUrl = rt.TypeOf; const legacyFilterStateInUrlRT = rt.union([ rt.strict({ @@ -170,7 +261,14 @@ const legacyLegacyFilterStateWithExpressionInUrlRT = rt.type({ expression: rt.string, }); -const decodeQueryValueFromUrl = (queryValueFromUrl: unknown) => +export const legacyPositionStateInUrlRT = rt.partial({ + streamLive: rt.boolean, + start: datemathStringRT, + end: datemathStringRT, + position: rt.union([rt.partial(minimalTimeKeyRT.props), rt.null]), +}); + +const decodeFilterQueryValueFromUrl = (queryValueFromUrl: unknown) => Either.getAltValidation(Array.getMonoid()).alt( pipe( pipe( @@ -187,5 +285,29 @@ const decodeQueryValueFromUrl = (queryValueFromUrl: unknown) => () => filterStateInUrlRT.decode(queryValueFromUrl) ); -export const replaceLogFilterInQueryString = (query: Query) => - replaceStateKeyInQueryString(defaultFilterStateKey, query); +const decodePositionQueryValueFromUrl = (queryValueFromUrl: unknown) => { + return legacyPositionStateInUrlRT.decode(queryValueFromUrl); +}; + +const ONE_HOUR = 3600000; +export const replaceLogFilterInQueryString = (query: Query, time?: number) => + replaceStateKeyInQueryString(defaultFilterStateKey, { + query, + ...(time && !Number.isNaN(time) + ? { + timeRange: { + from: new Date(time - ONE_HOUR).toISOString(), + to: new Date(time + ONE_HOUR).toISOString(), + }, + } + : {}), + refreshInterval: DEFAULT_REFRESH_INTERVAL, + }); + +const defaultTimeRangeFromPositionOffset: DurationInputObject = { hours: 1 }; + +const getTimeRangeStartFromTime = (time: number): string => + moment(time).subtract(defaultTimeRangeFromPositionOffset).toISOString(); + +const getTimeRangeEndFromTime = (time: number): string => + moment(time).add(defaultTimeRangeFromPositionOffset).toISOString(); diff --git a/x-pack/plugins/infra/public/pages/link_to/link_to_logs.test.tsx b/x-pack/plugins/infra/public/pages/link_to/link_to_logs.test.tsx index 0345b25688586..c6af835dad803 100644 --- a/x-pack/plugins/infra/public/pages/link_to/link_to_logs.test.tsx +++ b/x-pack/plugins/infra/public/pages/link_to/link_to_logs.test.tsx @@ -73,10 +73,10 @@ describe('LinkToLogsPage component', () => { const searchParams = new URLSearchParams(history.location.search); expect(searchParams.get('sourceId')).toEqual('default'); expect(searchParams.get('logFilter')).toMatchInlineSnapshot( - `"(language:kuery,query:'FILTER_FIELD:FILTER_VALUE')"` + `"(query:(language:kuery,query:'FILTER_FIELD:FILTER_VALUE'),refreshInterval:(pause:!t,value:5000),timeRange:(from:'2019-02-20T12:58:09.404Z',to:'2019-02-20T14:58:09.404Z'))"` ); expect(searchParams.get('logPosition')).toMatchInlineSnapshot( - `"(end:'2019-02-20T14:58:09.404Z',position:(tiebreaker:0,time:1550671089404),start:'2019-02-20T12:58:09.404Z',streamLive:!f)"` + `"(position:(tiebreaker:0,time:1550671089404))"` ); }); @@ -93,7 +93,9 @@ describe('LinkToLogsPage component', () => { const searchParams = new URLSearchParams(history.location.search); expect(searchParams.get('sourceId')).toEqual('OTHER_SOURCE'); - expect(searchParams.get('logFilter')).toMatchInlineSnapshot(`"(language:kuery,query:'')"`); + expect(searchParams.get('logFilter')).toMatchInlineSnapshot( + `"(query:(language:kuery,query:''),refreshInterval:(pause:!t,value:5000))"` + ); expect(searchParams.get('logPosition')).toEqual(null); }); }); @@ -113,10 +115,10 @@ describe('LinkToLogsPage component', () => { const searchParams = new URLSearchParams(history.location.search); expect(searchParams.get('sourceId')).toEqual('default'); expect(searchParams.get('logFilter')).toMatchInlineSnapshot( - `"(language:kuery,query:'FILTER_FIELD:FILTER_VALUE')"` + `"(query:(language:kuery,query:'FILTER_FIELD:FILTER_VALUE'),refreshInterval:(pause:!t,value:5000),timeRange:(from:'2019-02-20T12:58:09.404Z',to:'2019-02-20T14:58:09.404Z'))"` ); expect(searchParams.get('logPosition')).toMatchInlineSnapshot( - `"(end:'2019-02-20T14:58:09.404Z',position:(tiebreaker:0,time:1550671089404),start:'2019-02-20T12:58:09.404Z',streamLive:!f)"` + `"(position:(tiebreaker:0,time:1550671089404))"` ); }); @@ -133,7 +135,9 @@ describe('LinkToLogsPage component', () => { const searchParams = new URLSearchParams(history.location.search); expect(searchParams.get('sourceId')).toEqual('OTHER_SOURCE'); - expect(searchParams.get('logFilter')).toMatchInlineSnapshot(`"(language:kuery,query:'')"`); + expect(searchParams.get('logFilter')).toMatchInlineSnapshot( + `"(query:(language:kuery,query:''),refreshInterval:(pause:!t,value:5000))"` + ); expect(searchParams.get('logPosition')).toEqual(null); }); }); @@ -153,7 +157,7 @@ describe('LinkToLogsPage component', () => { const searchParams = new URLSearchParams(history.location.search); expect(searchParams.get('sourceId')).toEqual('default'); expect(searchParams.get('logFilter')).toMatchInlineSnapshot( - `"(language:kuery,query:'host.name: HOST_NAME')"` + `"(query:(language:kuery,query:'host.name: HOST_NAME'),refreshInterval:(pause:!t,value:5000))"` ); expect(searchParams.get('logPosition')).toEqual(null); }); @@ -174,10 +178,10 @@ describe('LinkToLogsPage component', () => { const searchParams = new URLSearchParams(history.location.search); expect(searchParams.get('sourceId')).toEqual('default'); expect(searchParams.get('logFilter')).toMatchInlineSnapshot( - `"(language:kuery,query:'(host.name: HOST_NAME) and (FILTER_FIELD:FILTER_VALUE)')"` + `"(query:(language:kuery,query:'(host.name: HOST_NAME) and (FILTER_FIELD:FILTER_VALUE)'),refreshInterval:(pause:!t,value:5000),timeRange:(from:'2019-02-20T12:58:09.404Z',to:'2019-02-20T14:58:09.404Z'))"` ); expect(searchParams.get('logPosition')).toMatchInlineSnapshot( - `"(end:'2019-02-20T14:58:09.404Z',position:(tiebreaker:0,time:1550671089404),start:'2019-02-20T12:58:09.404Z',streamLive:!f)"` + `"(position:(tiebreaker:0,time:1550671089404))"` ); }); @@ -195,7 +199,7 @@ describe('LinkToLogsPage component', () => { const searchParams = new URLSearchParams(history.location.search); expect(searchParams.get('sourceId')).toEqual('OTHER_SOURCE'); expect(searchParams.get('logFilter')).toMatchInlineSnapshot( - `"(language:kuery,query:'host.name: HOST_NAME')"` + `"(query:(language:kuery,query:'host.name: HOST_NAME'),refreshInterval:(pause:!t,value:5000))"` ); expect(searchParams.get('logPosition')).toEqual(null); }); @@ -231,7 +235,7 @@ describe('LinkToLogsPage component', () => { const searchParams = new URLSearchParams(history.location.search); expect(searchParams.get('sourceId')).toEqual('default'); expect(searchParams.get('logFilter')).toMatchInlineSnapshot( - `"(language:kuery,query:'container.id: CONTAINER_ID')"` + `"(query:(language:kuery,query:'container.id: CONTAINER_ID'),refreshInterval:(pause:!t,value:5000))"` ); expect(searchParams.get('logPosition')).toEqual(null); }); @@ -252,10 +256,10 @@ describe('LinkToLogsPage component', () => { const searchParams = new URLSearchParams(history.location.search); expect(searchParams.get('sourceId')).toEqual('default'); expect(searchParams.get('logFilter')).toMatchInlineSnapshot( - `"(language:kuery,query:'(container.id: CONTAINER_ID) and (FILTER_FIELD:FILTER_VALUE)')"` + `"(query:(language:kuery,query:'(container.id: CONTAINER_ID) and (FILTER_FIELD:FILTER_VALUE)'),refreshInterval:(pause:!t,value:5000),timeRange:(from:'2019-02-20T12:58:09.404Z',to:'2019-02-20T14:58:09.404Z'))"` ); expect(searchParams.get('logPosition')).toMatchInlineSnapshot( - `"(end:'2019-02-20T14:58:09.404Z',position:(tiebreaker:0,time:1550671089404),start:'2019-02-20T12:58:09.404Z',streamLive:!f)"` + `"(position:(tiebreaker:0,time:1550671089404))"` ); }); @@ -289,7 +293,7 @@ describe('LinkToLogsPage component', () => { const searchParams = new URLSearchParams(history.location.search); expect(searchParams.get('sourceId')).toEqual('default'); expect(searchParams.get('logFilter')).toMatchInlineSnapshot( - `"(language:kuery,query:'kubernetes.pod.uid: POD_UID')"` + `"(query:(language:kuery,query:'kubernetes.pod.uid: POD_UID'),refreshInterval:(pause:!t,value:5000))"` ); expect(searchParams.get('logPosition')).toEqual(null); }); @@ -308,10 +312,10 @@ describe('LinkToLogsPage component', () => { const searchParams = new URLSearchParams(history.location.search); expect(searchParams.get('sourceId')).toEqual('default'); expect(searchParams.get('logFilter')).toMatchInlineSnapshot( - `"(language:kuery,query:'(kubernetes.pod.uid: POD_UID) and (FILTER_FIELD:FILTER_VALUE)')"` + `"(query:(language:kuery,query:'(kubernetes.pod.uid: POD_UID) and (FILTER_FIELD:FILTER_VALUE)'),refreshInterval:(pause:!t,value:5000),timeRange:(from:'2019-02-20T12:58:09.404Z',to:'2019-02-20T14:58:09.404Z'))"` ); expect(searchParams.get('logPosition')).toMatchInlineSnapshot( - `"(end:'2019-02-20T14:58:09.404Z',position:(tiebreaker:0,time:1550671089404),start:'2019-02-20T12:58:09.404Z',streamLive:!f)"` + `"(position:(tiebreaker:0,time:1550671089404))"` ); }); diff --git a/x-pack/plugins/infra/public/pages/link_to/redirect_to_logs.test.tsx b/x-pack/plugins/infra/public/pages/link_to/redirect_to_logs.test.tsx index 39f276b982d76..58ba0df004f26 100644 --- a/x-pack/plugins/infra/public/pages/link_to/redirect_to_logs.test.tsx +++ b/x-pack/plugins/infra/public/pages/link_to/redirect_to_logs.test.tsx @@ -20,7 +20,7 @@ describe('RedirectToLogs component', () => { expect(component).toMatchInlineSnapshot(` `); }); @@ -34,7 +34,7 @@ describe('RedirectToLogs component', () => { expect(component).toMatchInlineSnapshot(` `); }); @@ -46,7 +46,7 @@ describe('RedirectToLogs component', () => { expect(component).toMatchInlineSnapshot(` `); }); diff --git a/x-pack/plugins/infra/public/pages/link_to/redirect_to_logs.tsx b/x-pack/plugins/infra/public/pages/link_to/redirect_to_logs.tsx index f5a4ed2f9462c..32bc4c909284d 100644 --- a/x-pack/plugins/infra/public/pages/link_to/redirect_to_logs.tsx +++ b/x-pack/plugins/infra/public/pages/link_to/redirect_to_logs.tsx @@ -5,11 +5,11 @@ * 2.0. */ -import { flowRight } from 'lodash'; import React from 'react'; import { match as RouteMatch, Redirect, RouteComponentProps } from 'react-router-dom'; -import { replaceLogPositionInQueryString } from '../../containers/logs/log_position'; +import { flowRight } from 'lodash'; import { replaceSourceIdInQueryString } from '../../containers/source_id'; +import { replaceLogPositionInQueryString } from '../../observability_logs/log_stream_position_state/src/url_state_storage_service'; import { replaceLogFilterInQueryString } from '../../observability_logs/log_stream_query_state'; import { getFilterFromLocation, getTimeFromLocation } from './query_params'; @@ -24,9 +24,10 @@ interface RedirectToLogsProps extends RedirectToLogsType { export const RedirectToLogs = ({ location, match }: RedirectToLogsProps) => { const sourceId = match.params.sourceId || 'default'; const filter = getFilterFromLocation(location); + const time = getTimeFromLocation(location); const searchString = flowRight( - replaceLogFilterInQueryString({ language: 'kuery', query: filter }), - replaceLogPositionInQueryString(getTimeFromLocation(location)), + replaceLogFilterInQueryString({ language: 'kuery', query: filter }, time), + replaceLogPositionInQueryString(time), replaceSourceIdInQueryString(sourceId) )(''); diff --git a/x-pack/plugins/infra/public/pages/link_to/redirect_to_node_logs.tsx b/x-pack/plugins/infra/public/pages/link_to/redirect_to_node_logs.tsx index eb439e6358869..4f5fb79acc76f 100644 --- a/x-pack/plugins/infra/public/pages/link_to/redirect_to_node_logs.tsx +++ b/x-pack/plugins/infra/public/pages/link_to/redirect_to_node_logs.tsx @@ -7,19 +7,19 @@ import { i18n } from '@kbn/i18n'; import { LinkDescriptor } from '@kbn/observability-plugin/public'; -import { flowRight } from 'lodash'; import React from 'react'; import { Redirect, RouteComponentProps } from 'react-router-dom'; import useMount from 'react-use/lib/useMount'; +import { flowRight } from 'lodash'; import { findInventoryFields } from '../../../common/inventory_models'; import { InventoryItemType } from '../../../common/inventory_models/types'; import { LoadingPage } from '../../components/loading_page'; -import { replaceLogPositionInQueryString } from '../../containers/logs/log_position'; import { replaceSourceIdInQueryString } from '../../containers/source_id'; import { useKibanaContextForPlugin } from '../../hooks/use_kibana'; import { useLogView } from '../../hooks/use_log_view'; import { replaceLogFilterInQueryString } from '../../observability_logs/log_stream_query_state'; import { getFilterFromLocation, getTimeFromLocation } from './query_params'; +import { replaceLogPositionInQueryString } from '../../observability_logs/log_stream_position_state/src/url_state_storage_service'; type RedirectToNodeLogsType = RouteComponentProps<{ nodeId: string; @@ -60,10 +60,11 @@ export const RedirectToNodeLogs = ({ const nodeFilter = `${findInventoryFields(nodeType).id}: ${nodeId}`; const userFilter = getFilterFromLocation(location); const filter = userFilter ? `(${nodeFilter}) and (${userFilter})` : nodeFilter; + const time = getTimeFromLocation(location); const searchString = flowRight( - replaceLogFilterInQueryString({ language: 'kuery', query: filter }), - replaceLogPositionInQueryString(getTimeFromLocation(location)), + replaceLogFilterInQueryString({ language: 'kuery', query: filter }, time), + replaceLogPositionInQueryString(time), replaceSourceIdInQueryString(sourceId) )(''); diff --git a/x-pack/plugins/infra/public/pages/logs/stream/page.tsx b/x-pack/plugins/infra/public/pages/logs/stream/page.tsx index 562c0ef1b9c37..326bc3906e5c5 100644 --- a/x-pack/plugins/infra/public/pages/logs/stream/page.tsx +++ b/x-pack/plugins/infra/public/pages/logs/stream/page.tsx @@ -31,7 +31,11 @@ export const StreamPage = () => { const { services: { data: { - query: { queryString: queryStringService, filterManager: filterManagerService }, + query: { + queryString: queryStringService, + filterManager: filterManagerService, + timefilter: { timefilter: timeFilterService }, + }, }, notifications: { toasts: toastsService }, }, @@ -49,6 +53,7 @@ export const StreamPage = () => { toastsService={toastsService} filterManagerService={filterManagerService} urlStateStorage={urlStateStorage} + timeFilterService={timeFilterService} > diff --git a/x-pack/plugins/infra/public/pages/logs/stream/page_content.tsx b/x-pack/plugins/infra/public/pages/logs/stream/page_content.tsx index 485a099a7a6e1..6fb2475d3d1e8 100644 --- a/x-pack/plugins/infra/public/pages/logs/stream/page_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/stream/page_content.tsx @@ -5,10 +5,14 @@ * 2.0. */ +import { TimeRange } from '@kbn/es-query'; import { useActor } from '@xstate/react'; -import React from 'react'; +import React, { useMemo } from 'react'; +import { VisiblePositions } from '../../../observability_logs/log_stream_position_state'; +import { TimeKey } from '../../../../common/time'; import { SourceLoadingPage } from '../../../components/source_loading_page'; import { + LogStreamPageCallbacks, LogStreamPageState, useLogStreamPageStateContext, } from '../../../observability_logs/log_stream_page/state'; @@ -20,16 +24,54 @@ import { LogStreamPageContentProviders } from './page_providers'; export const ConnectedStreamPageContent: React.FC = () => { const logStreamPageStateService = useLogStreamPageStateContext(); + const [logStreamPageState, logStreamPageSend] = useActor(logStreamPageStateService); - const [logStreamPageState] = useActor(logStreamPageStateService); + const pageStateCallbacks = useMemo(() => { + return { + updateTimeRange: (timeRange: Partial) => { + logStreamPageSend({ + type: 'UPDATE_TIME_RANGE', + timeRange, + }); + }, + jumpToTargetPosition: (targetPosition: TimeKey | null) => { + logStreamPageSend({ type: 'JUMP_TO_TARGET_POSITION', targetPosition }); + }, + jumpToTargetPositionTime: (time: number) => { + logStreamPageSend({ type: 'JUMP_TO_TARGET_POSITION', targetPosition: { time } }); + }, + reportVisiblePositions: (visiblePositions: VisiblePositions) => { + logStreamPageSend({ + type: 'REPORT_VISIBLE_POSITIONS', + visiblePositions, + }); + }, + startLiveStreaming: () => { + logStreamPageSend({ type: 'UPDATE_REFRESH_INTERVAL', refreshInterval: { pause: false } }); + }, + stopLiveStreaming: () => { + logStreamPageSend({ type: 'UPDATE_REFRESH_INTERVAL', refreshInterval: { pause: true } }); + }, + }; + }, [logStreamPageSend]); - return ; + return ( + + ); }; -export const StreamPageContentForState: React.FC<{ logStreamPageState: LogStreamPageState }> = ({ - logStreamPageState, -}) => { - if (logStreamPageState.matches('uninitialized') || logStreamPageState.matches('loadingLogView')) { +export const StreamPageContentForState: React.FC<{ + logStreamPageState: LogStreamPageState; + logStreamPageCallbacks: LogStreamPageCallbacks; +}> = ({ logStreamPageState, logStreamPageCallbacks }) => { + if ( + logStreamPageState.matches('uninitialized') || + logStreamPageState.matches({ hasLogViewIndices: 'uninitialized' }) || + logStreamPageState.matches('loadingLogView') + ) { return ; } else if (logStreamPageState.matches('loadingLogViewFailed')) { return ; @@ -37,8 +79,14 @@ export const StreamPageContentForState: React.FC<{ logStreamPageState: LogStream return ; } else if (logStreamPageState.matches({ hasLogViewIndices: 'initialized' })) { return ( - - + + ); } else { diff --git a/x-pack/plugins/infra/public/pages/logs/stream/page_logs_content.tsx b/x-pack/plugins/infra/public/pages/logs/stream/page_logs_content.tsx index bdfc4d1cb2cc8..43f692cf9af8c 100644 --- a/x-pack/plugins/infra/public/pages/logs/stream/page_logs_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/stream/page_logs_content.tsx @@ -31,7 +31,10 @@ import { useViewLogInProviderContext } from '../../../containers/logs/view_log_i import { WithLogTextviewUrlState } from '../../../containers/logs/with_log_textview'; import { useKibanaContextForPlugin } from '../../../hooks/use_kibana'; import { useLogViewContext } from '../../../hooks/use_log_view'; -import { LogStreamPageActorRef } from '../../../observability_logs/log_stream_page/state'; +import { + LogStreamPageActorRef, + LogStreamPageCallbacks, +} from '../../../observability_logs/log_stream_page/state'; import { type ParsedQuery } from '../../../observability_logs/log_stream_query_state'; import { MatchedStateFromActor } from '../../../observability_logs/xstate_helpers'; import { datemathToEpochMillis, isValidDatemath } from '../../../utils/datemath'; @@ -43,7 +46,8 @@ const PAGE_THRESHOLD = 2; export const StreamPageLogsContent = React.memo<{ filterQuery: ParsedQuery; -}>(({ filterQuery }) => { + logStreamPageCallbacks: LogStreamPageCallbacks; +}>(({ filterQuery, logStreamPageCallbacks }) => { const { data: { query: { queryString }, @@ -291,12 +295,18 @@ type InitializedLogStreamPageState = MatchedStateFromActor< export const StreamPageLogsContentForState = React.memo<{ logStreamPageState: InitializedLogStreamPageState; -}>(({ logStreamPageState }) => { + logStreamPageCallbacks: LogStreamPageCallbacks; +}>(({ logStreamPageState, logStreamPageCallbacks }) => { const { context: { parsedQuery }, } = logStreamPageState; - return ; + return ( + + ); }); const LogPageMinimapColumn = euiStyled.div` 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 4ec53a9cd11ef..3260411aa4ed0 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 @@ -7,7 +7,10 @@ import stringify from 'json-stable-stringify'; import React, { useMemo } from 'react'; -import { LogStreamPageActorRef } from '../../../observability_logs/log_stream_page/state'; +import { + LogStreamPageActorRef, + LogStreamPageCallbacks, +} from '../../../observability_logs/log_stream_page/state'; import { LogEntryFlyoutProvider } from '../../../containers/logs/log_flyout'; import { LogHighlightsStateProvider } from '../../../containers/logs/log_highlights/log_highlights'; import { @@ -90,11 +93,15 @@ const LogHighlightsState: React.FC<{ export const LogStreamPageContentProviders: React.FC<{ logStreamPageState: InitializedLogStreamPageState; -}> = ({ children, logStreamPageState }) => { + logStreamPageCallbacks: LogStreamPageCallbacks; +}> = ({ children, logStreamPageState, logStreamPageCallbacks }) => { return ( - + diff --git a/x-pack/test/functional/apps/infra/link_to.ts b/x-pack/test/functional/apps/infra/link_to.ts index 05eccc8e57ebc..890f85369877c 100644 --- a/x-pack/test/functional/apps/infra/link_to.ts +++ b/x-pack/test/functional/apps/infra/link_to.ts @@ -46,10 +46,10 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { expect(parsedUrl.pathname).to.be('/app/logs/stream'); expect(parsedUrl.searchParams.get('logFilter')).to.be( - `(language:kuery,query:'trace.id:${traceId}')` + `(query:(language:kuery,query:\'trace.id:${traceId}'),refreshInterval:(pause:!t,value:5000),timeRange:(from:'${startDate}',to:'${endDate}'))` ); expect(parsedUrl.searchParams.get('logPosition')).to.be( - `(end:'${endDate}',position:(tiebreaker:0,time:${timestamp}),start:'${startDate}',streamLive:!f)` + `(position:(tiebreaker:0,time:${timestamp}))` ); expect(parsedUrl.searchParams.get('sourceId')).to.be('default'); expect(documentTitle).to.contain('Stream - Logs - Observability - Elastic'); diff --git a/x-pack/test/functional/apps/infra/log_stream.ts b/x-pack/test/functional/apps/infra/log_stream.ts index a0836e4ef57e9..1918c39527d6b 100644 --- a/x-pack/test/functional/apps/infra/log_stream.ts +++ b/x-pack/test/functional/apps/infra/log_stream.ts @@ -50,8 +50,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const parsedUrl = new URL(currentUrl); expect(parsedUrl.pathname).to.be('/app/logs/stream'); - expect(parsedUrl.searchParams.get('logFilter')).to.be( - `(filters:!(),query:(language:kuery,query:\'service.id:"${SERVICE_ID}"\'))` + expect(parsedUrl.searchParams.get('logFilter')).to.contain( + `(filters:!(),query:(language:kuery,query:\'service.id:"${SERVICE_ID}"\')` ); }); }); @@ -77,8 +77,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const parsedUrl = new URL(currentUrl); expect(parsedUrl.pathname).to.be('/app/logs/stream'); - expect(parsedUrl.searchParams.get('logFilter')).to.be( - `(filters:!(),query:(language:kuery,query:\'service.id:"${SERVICE_ID}"\'))` + expect(parsedUrl.searchParams.get('logFilter')).to.contain( + `(filters:!(),query:(language:kuery,query:\'service.id:"${SERVICE_ID}"\')` ); }); }); 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 24b7a538d77f6..8166af7848275 100644 --- a/x-pack/test/functional/apps/infra/logs_source_configuration.ts +++ b/x-pack/test/functional/apps/infra/logs_source_configuration.ts @@ -33,11 +33,13 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); describe('Allows indices configuration', () => { - const logPosition = { - start: DATES.metricsAndLogs.stream.startWithData, - end: DATES.metricsAndLogs.stream.endWithData, + const logFilter = { + timeRange: { + from: DATES.metricsAndLogs.stream.startWithData, + to: DATES.metricsAndLogs.stream.endWithData, + }, }; - const formattedLocalStart = new Date(logPosition.start).toLocaleDateString('en-US', { + const formattedLocalStart = new Date(logFilter.timeRange.from).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric', @@ -106,7 +108,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); it('renders the default log columns with their headers', async () => { - await logsUi.logStreamPage.navigateTo({ logPosition }); + await logsUi.logStreamPage.navigateTo({ logFilter }); await retry.try(async () => { const columnHeaderLabels = await logsUi.logStreamPage.getColumnHeaderLabels(); @@ -126,7 +128,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); it('records telemetry for logs', async () => { - await logsUi.logStreamPage.navigateTo({ logPosition }); + await logsUi.logStreamPage.navigateTo({ logFilter }); await logsUi.logStreamPage.getStreamEntries(); @@ -161,7 +163,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); it('renders the changed log columns with their headers', async () => { - await logsUi.logStreamPage.navigateTo({ logPosition }); + await logsUi.logStreamPage.navigateTo({ logFilter }); await retry.try(async () => { const columnHeaderLabels = await logsUi.logStreamPage.getColumnHeaderLabels(); 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 040495cd754c3..3be86646a0533 100644 --- a/x-pack/test/functional/page_objects/infra_logs_page.ts +++ b/x-pack/test/functional/page_objects/infra_logs_page.ts @@ -6,14 +6,16 @@ */ 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 } from '@kbn/rison'; +import { PositionStateInUrl } from '@kbn/infra-plugin/public/observability_logs/log_stream_position_state/src/url_state_storage_service'; +import { FilterStateInUrl } from '@kbn/infra-plugin/public/observability_logs/log_stream_query_state/src/url_state_storage_service'; import { FtrProviderContext } from '../ftr_provider_context'; export interface TabsParams { stream: { - logPosition?: Partial; + logPosition?: Partial; + logFilter?: Partial; flyoutOptions?: Partial; }; settings: never;