mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Logs UI] Refactor Time and Position log stream state (#149052)
## Summary
Closes https://github.com/elastic/kibana/issues/145137
Initially this issue was just going to include moving the time context
into the query state machine. However, this actually made working with
the dependant log position code a lot harder. As such the log position
code has also been moved to it's own state machine.
## 🕵️♀️ Reviewer hints and notable changes
- There are some comments left inline (*previous logic* notes might be
useful)
- There is now a new machine for dealing with Log Position state (target
position, latest position, visible positions).
- Time based context (time range, timestamps, refresh interval) is now
moved to the query machine (this will also make dealing with saved
queries easier).
- The page state machine is the only machine that the UI interacts with
(either reading context or sending events). The page state machine
co-ordinates forwarding necessary events to other internal machines.
- Ensure relevant notifications reach their targets, e.g. when time is
changed, positions should also update.
- [There is some documentation regarding URL state and
precedence](f9ca0f795c
).
- `updateContextInUrl` now always sets the full URL-relevant context in
the URL when called (since the `urlStateStorage.set()` call replaces the
whole key).
- Within the Log Stream Query state machine the `initialized` state node
is now modelled as a parallel state node, this is so `query` and `time`
can act independently (time needs to handle things like the refresh
interval, for example).
## 🕹 Testing
(Just some ideas)
- Can the time range be changed?
- Can the refresh interval be changed?
- Is state synchronised to the URL and to relevant Kibana services (time
filter service etc)?
- When streaming is enabled, are requests dispatched correctly on an
interval?
- Do positions update correctly whilst interacting with the stream?
(scrolling etc)
- Does the backwards compatibility of initialising from the URL work?
## 🎨 State machine diagrams
### Log stream page

### Log stream query

### Log stream position

## ⚠️ Warnings
- [There is a known bug with
streaming](https://github.com/elastic/kibana/issues/136159#issuecomment-1408902137).
- [There is a known issue with a console
error](https://github.com/elastic/kibana/pull/149052#issuecomment-1420579941)
---------
Co-authored-by: Felix Stürmer <weltenwort@users.noreply.github.com>
This commit is contained in:
parent
eaa08841f6
commit
8583231046
38 changed files with 1738 additions and 1108 deletions
|
@ -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)
|
||||
|
|
|
@ -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
|
|
@ -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';
|
||||
|
|
|
@ -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(),
|
||||
});
|
|
@ -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<TimeRange>) =>
|
||||
updateTimeRange(timeRange)(state),
|
||||
updateRefreshInterval:
|
||||
(state: LogPositionState) => (refreshInterval: Partial<RefreshInterval>) =>
|
||||
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<LogPositionState>((draftState) => {
|
||||
draftState.visiblePositions = visiblePositions;
|
||||
|
||||
updateLatestPositionDraft(draftState);
|
||||
});
|
||||
|
||||
const updateTargetPosition = (targetPosition: Partial<MinimalTimeKey> | null) =>
|
||||
produce<LogPositionState>((draftState) => {
|
||||
if (targetPosition?.time != null) {
|
||||
draftState.targetPosition = {
|
||||
time: targetPosition.time,
|
||||
tiebreaker: targetPosition.tiebreaker ?? 0,
|
||||
};
|
||||
} else {
|
||||
draftState.targetPosition = null;
|
||||
}
|
||||
|
||||
updateLatestPositionDraft(draftState);
|
||||
});
|
||||
|
||||
const updateLatestPositionDraft = (draftState: Draft<LogPositionState>) => {
|
||||
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<TimeRange>, now?: Date) =>
|
||||
produce<LogPositionState>((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<RefreshInterval>) => (state: LogPositionState) =>
|
||||
pipe(
|
||||
state,
|
||||
produce<LogPositionState>((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<MinimalTimeKey> | null | undefined
|
||||
): string | undefined =>
|
||||
position?.time != null
|
||||
? moment(position.time).subtract(defaultTimeRangeFromPositionOffset).toISOString()
|
||||
: undefined;
|
||||
|
||||
const getTimeRangeEndFromPosition = (
|
||||
position: Partial<MinimalTimeKey> | null | undefined
|
||||
): string | undefined =>
|
||||
position?.time != null
|
||||
? moment(position.time).add(defaultTimeRangeFromPositionOffset).toISOString()
|
||||
: undefined;
|
|
@ -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<TimefilterState>) => {
|
||||
timefilterStateStorage.set(timefilterStateStorageKey, stateContainer.get());
|
||||
|
||||
const { start, stop } = syncState({
|
||||
storageKey: timefilterStateStorageKey,
|
||||
stateContainer,
|
||||
stateStorage: timefilterStateStorage,
|
||||
});
|
||||
|
||||
start();
|
||||
|
||||
return stop;
|
||||
},
|
||||
[timefilterStateStorage]
|
||||
);
|
||||
|
||||
return {
|
||||
initialStateFromTimefilter,
|
||||
startSyncingWithTimefilter,
|
||||
};
|
||||
};
|
|
@ -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<LogPositionUrlState>(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,
|
||||
});
|
|
@ -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<Pick<DateRange, 'startDateExpression' | 'endDateExpression'>>
|
||||
) => void;
|
||||
|
||||
const DESIRED_BUFFER_PAGES = 2;
|
||||
const RELATIVE_END_UPDATE_DELAY = 1000;
|
||||
export const useLogPositionState = ({
|
||||
logStreamPageState,
|
||||
logStreamPageCallbacks,
|
||||
}: {
|
||||
logStreamPageState: InitializedLogStreamPageState;
|
||||
logStreamPageCallbacks: LogStreamPageCallbacks;
|
||||
}): LogPositionStateParams & LogPositionCallbacks => {
|
||||
const dateRange = useMemo(() => getLegacyDateRange(logStreamPageState), [logStreamPageState]);
|
||||
|
||||
export const useLogPositionState: () => LogPositionStateParams & LogPositionCallbacks = () => {
|
||||
const { initialStateFromUrl, startSyncingWithUrl } = useLogPositionUrlStateSync();
|
||||
const { initialStateFromTimefilter, startSyncingWithTimefilter } =
|
||||
useLogPositionTimefilterStateSync();
|
||||
const { refreshInterval, targetPosition, visiblePositions, latestPosition } =
|
||||
logStreamPageState.context;
|
||||
|
||||
const [logPositionStateContainer] = useState(() =>
|
||||
withReduxDevTools(
|
||||
createLogPositionStateContainer({
|
||||
initialStateFromUrl,
|
||||
initialStateFromTimefilter,
|
||||
}),
|
||||
{
|
||||
name: 'logPosition',
|
||||
}
|
||||
)
|
||||
);
|
||||
const actions = useMemo(() => {
|
||||
const {
|
||||
updateTimeRange,
|
||||
jumpToTargetPosition,
|
||||
jumpToTargetPositionTime,
|
||||
reportVisiblePositions,
|
||||
startLiveStreaming,
|
||||
stopLiveStreaming,
|
||||
} = logStreamPageCallbacks;
|
||||
|
||||
useEffect(() => {
|
||||
return startSyncingWithUrl(
|
||||
wrapStateContainer<LogPositionState, LogPositionUrlState>({
|
||||
wrapGet: getUrlState,
|
||||
wrapSet: updateStateFromUrlState,
|
||||
})(logPositionStateContainer)
|
||||
);
|
||||
}, [logPositionStateContainer, startSyncingWithUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
return startSyncingWithTimefilter(
|
||||
wrapStateContainer<LogPositionState, TimefilterState>({
|
||||
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<UpdateDateRangeFn>(
|
||||
(newDateRange: Partial<Pick<DateRange, 'startDateExpression' | 'endDateExpression'>>) =>
|
||||
logPositionStateContainer.transitions.updateTimeRange({
|
||||
from: newDateRange.startDateExpression,
|
||||
to: newDateRange.endDateExpression,
|
||||
}),
|
||||
[logPositionStateContainer]
|
||||
);
|
||||
return {
|
||||
jumpToTargetPosition,
|
||||
jumpToTargetPositionTime,
|
||||
reportVisiblePositions,
|
||||
startLiveStreaming,
|
||||
stopLiveStreaming,
|
||||
updateDateRange: (
|
||||
newDateRange: Partial<Pick<DateRange, 'startDateExpression' | 'endDateExpression'>>
|
||||
) => {
|
||||
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' }
|
||||
>;
|
||||
|
|
|
@ -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<typeof logPositionUrlStateRT>;
|
||||
|
||||
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<LogPositionUrlState>) => {
|
||||
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: <State>(key: string, state: State) =>
|
||||
urlStateStorage.set(key, state, { replace: true }),
|
||||
},
|
||||
});
|
||||
|
||||
start();
|
||||
|
||||
return stop;
|
||||
},
|
||||
[initialStateFromUrl, urlStateStorage]
|
||||
);
|
||||
|
||||
return {
|
||||
initialStateFromUrl,
|
||||
startSyncingWithUrl,
|
||||
};
|
||||
};
|
|
@ -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<LogStreamPageContext, LogStreamPageEvent> =>
|
||||
(_context, _event) =>
|
||||
(send, onEvent: Receiver<LogStreamPageEvent>) => {
|
||||
// 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<LogStreamPageContext, LogStreamPageEvent> =>
|
||||
(_context, _event) =>
|
||||
(send, onEvent: Receiver<LogStreamPageEvent>) => {
|
||||
// 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,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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 }
|
||||
);
|
||||
|
|
|
@ -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<LogStreamPageContext, LogStreamPageEvent, LogStreamPageTypestate>(
|
||||
{
|
||||
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: 'forwardToInitialParameters',
|
||||
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: '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<typeof createPureLogStreamPageStateMachine>;
|
||||
export type LogStreamPageActorRef = OmitDeprecatedState<ActorRefFrom<LogStreamPageStateMachine>>;
|
||||
export type LogStreamPageState = EmittedFrom<LogStreamPageActorRef>;
|
||||
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,
|
||||
}
|
||||
);
|
||||
},
|
||||
waitForInitialParameters: waitForInitialParameters(),
|
||||
logStreamPosition: (context) => {
|
||||
return createLogStreamPositionStateMachine(
|
||||
{
|
||||
targetPosition: null,
|
||||
latestPosition: null,
|
||||
visiblePositions: {
|
||||
endKey: null,
|
||||
middleKey: null,
|
||||
startKey: null,
|
||||
pagesBeforeStart: Infinity,
|
||||
pagesAfterEnd: Infinity,
|
||||
},
|
||||
},
|
||||
{
|
||||
urlStateStorage,
|
||||
toastsService,
|
||||
}
|
||||
);
|
||||
},
|
||||
waitForInitialQueryParameters: waitForInitialQueryParameters(),
|
||||
waitForInitialPositionParameters: waitForInitialPositionParameters(),
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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<TimeRange>) => void;
|
||||
jumpToTargetPosition: (targetPosition: TimeKey | null) => void;
|
||||
jumpToTargetPositionTime: (time: number) => void;
|
||||
reportVisiblePositions: (visiblePositions: VisiblePositions) => void;
|
||||
startLiveStreaming: () => void;
|
||||
stopLiveStreaming: () => void;
|
||||
}
|
||||
|
|
|
@ -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';
|
|
@ -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;
|
|
@ -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),
|
||||
};
|
|
@ -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<LogStreamPositionContext, LogStreamPositionEvent, LogStreamPositionTypestate>(
|
||||
{
|
||||
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<LogStreamPositionStateMachine>
|
||||
>;
|
||||
export type LogStreamPositionState = EmittedFrom<LogStreamPositionActorRef>;
|
||||
|
||||
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 }),
|
||||
},
|
||||
});
|
|
@ -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<TimeKey> | 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;
|
|
@ -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<typeof positionStateInUrlRT>;
|
||||
|
||||
const decodePositionQueryValueFromUrl = (queryValueFromUrl: unknown) => {
|
||||
return positionStateInUrlRT.decode(queryValueFromUrl);
|
||||
};
|
||||
|
||||
// Used by linkTo components
|
||||
export const replaceLogPositionInQueryString = (time: number) =>
|
||||
Number.isNaN(time)
|
||||
? (value: string) => value
|
||||
: replaceStateKeyInQueryString<PositionStateInUrl>(defaultPositionStateKey, {
|
||||
position: {
|
||||
time,
|
||||
tiebreaker: 0,
|
||||
},
|
||||
});
|
|
@ -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 };
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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<LogStreamQueryContext, LogStreamQueryEvent, LogStreamQueryTypestate>(
|
||||
{
|
||||
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',
|
||||
},
|
||||
],
|
||||
initial: 'revalidating',
|
||||
states: {
|
||||
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'],
|
||||
},
|
||||
},
|
||||
initializingFromTimeFilterService: {
|
||||
on: {
|
||||
INITIALIZED_FROM_TIME_FILTER_SERVICE: {
|
||||
target: 'initializingFromUrl',
|
||||
actions: ['updateTimeContextFromTimeFilterService'],
|
||||
},
|
||||
},
|
||||
on: {
|
||||
QUERY_FROM_SEARCH_BAR_CHANGED: {
|
||||
target: '.revalidating',
|
||||
actions: ['storeQuery', 'updateQueryInUrl'],
|
||||
},
|
||||
|
||||
FILTERS_FROM_SEARCH_BAR_CHANGED: {
|
||||
target: '.revalidating',
|
||||
actions: ['storeFilters', 'updateFiltersInUrl'],
|
||||
},
|
||||
|
||||
DATA_VIEWS_CHANGED: {
|
||||
target: '.revalidating',
|
||||
actions: 'storeDataViews',
|
||||
},
|
||||
invoke: {
|
||||
src: 'initializeFromTimeFilterService',
|
||||
},
|
||||
},
|
||||
|
||||
validating: {
|
||||
invoke: {
|
||||
src: 'validateQuery',
|
||||
},
|
||||
|
||||
on: {
|
||||
VALIDATION_SUCCEEDED: {
|
||||
target: 'hasQuery.valid',
|
||||
actions: 'storeParsedQuery',
|
||||
},
|
||||
|
||||
VALIDATION_FAILED: {
|
||||
target: 'hasQuery.invalid',
|
||||
actions: [
|
||||
'storeValidationError',
|
||||
'storeDefaultParsedQuery',
|
||||
'showValidationErrorToast',
|
||||
initialized: {
|
||||
type: 'parallel',
|
||||
states: {
|
||||
query: {
|
||||
entry: ['updateContextInUrl', 'updateQueryInSearchBar', 'updateFiltersInSearchBar'],
|
||||
invoke: [
|
||||
{
|
||||
src: 'subscribeToQuerySearchBarChanges',
|
||||
},
|
||||
{
|
||||
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', 'updateContextInUrl'],
|
||||
},
|
||||
|
||||
FILTERS_FROM_SEARCH_BAR_CHANGED: {
|
||||
target: '.revalidating',
|
||||
actions: ['storeFilters', 'updateContextInUrl'],
|
||||
},
|
||||
|
||||
DATA_VIEWS_CHANGED: {
|
||||
target: '.revalidating',
|
||||
actions: 'storeDataViews',
|
||||
},
|
||||
},
|
||||
},
|
||||
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',
|
||||
],
|
||||
},
|
||||
|
||||
UPDATE_TIME_RANGE: {
|
||||
target: '.initialized',
|
||||
actions: [
|
||||
'updateTimeContextFromTimeRangeUpdate',
|
||||
'notifyTimeChanged',
|
||||
'updateTimeInTimeFilterService',
|
||||
'updateContextInUrl',
|
||||
],
|
||||
},
|
||||
|
||||
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,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -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<LogStreamQueryContext, LogStreamQueryEvent> =>
|
||||
(_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<LogStreamQueryContext, LogStreamQueryEvent> =>
|
||||
(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,
|
||||
},
|
||||
};
|
||||
};
|
|
@ -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' };
|
||||
context: LogStreamQueryContextWithDataViews &
|
||||
LogStreamQueryContextWithParsedQuery &
|
||||
LogStreamQueryContextWithQuery &
|
||||
LogStreamQueryContextWithFilters;
|
||||
}
|
||||
| {
|
||||
value: { hasQuery: 'valid' };
|
||||
context: LogStreamQueryContextWithDataViews &
|
||||
LogStreamQueryContextWithParsedQuery &
|
||||
LogStreamQueryContextWithQuery &
|
||||
LogStreamQueryContextWithFilters;
|
||||
}
|
||||
| {
|
||||
value: { hasQuery: 'invalid' };
|
||||
value: 'query' | { query: 'validating' };
|
||||
context: LogStreamQueryContextWithDataViews &
|
||||
LogStreamQueryContextWithParsedQuery &
|
||||
LogStreamQueryContextWithQuery &
|
||||
LogStreamQueryContextWithFilters &
|
||||
LogStreamQueryContextWithTime;
|
||||
}
|
||||
| {
|
||||
value: { query: 'valid' };
|
||||
context: LogStreamQueryContextWithDataViews &
|
||||
LogStreamQueryContextWithParsedQuery &
|
||||
LogStreamQueryContextWithQuery &
|
||||
LogStreamQueryContextWithFilters &
|
||||
LogStreamQueryContextWithTime;
|
||||
}
|
||||
| {
|
||||
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<TimeRange>;
|
||||
}
|
||||
|
||||
export interface UpdateRefreshIntervalEvent {
|
||||
type: 'UPDATE_REFRESH_INTERVAL';
|
||||
refreshInterval: Partial<RefreshInterval>;
|
||||
}
|
||||
|
||||
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;
|
||||
|
|
|
@ -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<FilterStateInUrl> = {
|
||||
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<Omit<FilterStateInUrl, 'timeRange' | 'refreshInterval'>>;
|
||||
type OptionalDefaults = Pick<FilterStateInUrl, 'timeRange' | 'refreshInterval'>;
|
||||
type FullDefaults = Required<RequiredDefaults & OptionalDefaults>;
|
||||
|
||||
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 =
|
||||
export const updateContextInUrl =
|
||||
({
|
||||
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 =
|
||||
({
|
||||
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<typeof filterStateInUrlRT>;
|
||||
export type FilterStateInUrl = rt.TypeOf<typeof filterStateInUrlRT>;
|
||||
|
||||
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<rt.ValidationError>()).alt<FilterStateInUrl>(
|
||||
pipe(
|
||||
pipe(
|
||||
|
@ -187,5 +285,29 @@ const decodeQueryValueFromUrl = (queryValueFromUrl: unknown) =>
|
|||
() => filterStateInUrlRT.decode(queryValueFromUrl)
|
||||
);
|
||||
|
||||
export const replaceLogFilterInQueryString = (query: Query) =>
|
||||
replaceStateKeyInQueryString<Query>(defaultFilterStateKey, query);
|
||||
const decodePositionQueryValueFromUrl = (queryValueFromUrl: unknown) => {
|
||||
return legacyPositionStateInUrlRT.decode(queryValueFromUrl);
|
||||
};
|
||||
|
||||
const ONE_HOUR = 3600000;
|
||||
export const replaceLogFilterInQueryString = (query: Query, time?: number) =>
|
||||
replaceStateKeyInQueryString<FilterStateInUrl>(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();
|
||||
|
|
|
@ -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))"`
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ describe('RedirectToLogs component', () => {
|
|||
|
||||
expect(component).toMatchInlineSnapshot(`
|
||||
<Redirect
|
||||
to="/stream?sourceId=default&logPosition=(end:'2019-02-20T14:58:09.404Z',position:(tiebreaker:0,time:1550671089404),start:'2019-02-20T12:58:09.404Z',streamLive:!f)&logFilter=(language:kuery,query:'')"
|
||||
to="/stream?sourceId=default&logPosition=(position:(tiebreaker:0,time:1550671089404))&logFilter=(query:(language:kuery,query:''),refreshInterval:(pause:!t,value:5000),timeRange:(from:'2019-02-20T12:58:09.404Z',to:'2019-02-20T14:58:09.404Z'))"
|
||||
/>
|
||||
`);
|
||||
});
|
||||
|
@ -34,7 +34,7 @@ describe('RedirectToLogs component', () => {
|
|||
|
||||
expect(component).toMatchInlineSnapshot(`
|
||||
<Redirect
|
||||
to="/stream?sourceId=default&logPosition=(end:'2019-02-20T14:58:09.404Z',position:(tiebreaker:0,time:1550671089404),start:'2019-02-20T12:58:09.404Z',streamLive:!f)&logFilter=(language:kuery,query:'FILTER_FIELD:FILTER_VALUE')"
|
||||
to="/stream?sourceId=default&logPosition=(position:(tiebreaker:0,time:1550671089404))&logFilter=(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'))"
|
||||
/>
|
||||
`);
|
||||
});
|
||||
|
@ -46,7 +46,7 @@ describe('RedirectToLogs component', () => {
|
|||
|
||||
expect(component).toMatchInlineSnapshot(`
|
||||
<Redirect
|
||||
to="/stream?sourceId=SOME-OTHER-SOURCE&logFilter=(language:kuery,query:'')"
|
||||
to="/stream?sourceId=SOME-OTHER-SOURCE&logFilter=(query:(language:kuery,query:''),refreshInterval:(pause:!t,value:5000))"
|
||||
/>
|
||||
`);
|
||||
});
|
||||
|
|
|
@ -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)
|
||||
)('');
|
||||
|
||||
|
|
|
@ -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)
|
||||
)('');
|
||||
|
||||
|
|
|
@ -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}
|
||||
>
|
||||
<ConnectedStreamPageContentMemo />
|
||||
</LogStreamPageStateProvider>
|
||||
|
|
|
@ -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<TimeRange>) => {
|
||||
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 <StreamPageContentForState logStreamPageState={logStreamPageState} />;
|
||||
return (
|
||||
<StreamPageContentForState
|
||||
logStreamPageState={logStreamPageState}
|
||||
logStreamPageCallbacks={pageStateCallbacks}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
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 <SourceLoadingPage />;
|
||||
} else if (logStreamPageState.matches('loadingLogViewFailed')) {
|
||||
return <ConnectedLogViewErrorPage />;
|
||||
|
@ -37,8 +79,14 @@ export const StreamPageContentForState: React.FC<{ logStreamPageState: LogStream
|
|||
return <StreamPageMissingIndicesContent />;
|
||||
} else if (logStreamPageState.matches({ hasLogViewIndices: 'initialized' })) {
|
||||
return (
|
||||
<LogStreamPageContentProviders logStreamPageState={logStreamPageState}>
|
||||
<StreamPageLogsContentForState logStreamPageState={logStreamPageState} />
|
||||
<LogStreamPageContentProviders
|
||||
logStreamPageState={logStreamPageState}
|
||||
logStreamPageCallbacks={logStreamPageCallbacks}
|
||||
>
|
||||
<StreamPageLogsContentForState
|
||||
logStreamPageState={logStreamPageState}
|
||||
logStreamPageCallbacks={logStreamPageCallbacks}
|
||||
/>
|
||||
</LogStreamPageContentProviders>
|
||||
);
|
||||
} else {
|
||||
|
|
|
@ -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 <StreamPageLogsContent filterQuery={parsedQuery} />;
|
||||
return (
|
||||
<StreamPageLogsContent
|
||||
filterQuery={parsedQuery}
|
||||
logStreamPageCallbacks={logStreamPageCallbacks}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
const LogPageMinimapColumn = euiStyled.div`
|
||||
|
|
|
@ -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 (
|
||||
<LogViewConfigurationProvider>
|
||||
<LogEntryFlyoutProvider>
|
||||
<LogPositionStateProvider>
|
||||
<LogPositionStateProvider
|
||||
logStreamPageState={logStreamPageState}
|
||||
logStreamPageCallbacks={logStreamPageCallbacks}
|
||||
>
|
||||
<ViewLogInContext>
|
||||
<LogEntriesStateProvider logStreamPageState={logStreamPageState}>
|
||||
<LogHighlightsState logStreamPageState={logStreamPageState}>
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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}"\')`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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<LogPositionUrlState>;
|
||||
logPosition?: Partial<PositionStateInUrl>;
|
||||
logFilter?: Partial<FilterStateInUrl>;
|
||||
flyoutOptions?: Partial<FlyoutOptionsUrlState>;
|
||||
};
|
||||
settings: never;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue