[Logs UI] Implement log stream page state as a state machine (#145234)

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Kerry Gallagher <kerry.gallagher@elastic.co>
closes https://github.com/elastic/kibana/issues/145131
This commit is contained in:
Felix Stürmer 2022-12-13 17:50:36 +01:00 committed by GitHub
parent f7706252fd
commit eb75937130
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
46 changed files with 1440 additions and 227 deletions

View file

@ -466,6 +466,7 @@
"@turf/length": "^6.0.2",
"@types/adm-zip": "^0.5.0",
"@types/byte-size": "^8.1.0",
"@xstate/react": "^3.0.1",
"JSONStream": "1.3.5",
"abort-controller": "^3.0.0",
"adm-zip": "^0.5.9",
@ -687,6 +688,7 @@
"vinyl": "^2.2.0",
"whatwg-fetch": "^3.0.0",
"xml2js": "^0.4.22",
"xstate": "^4.34.0",
"xterm": "^5.0.0",
"yauzl": "^2.10.0",
"yazl": "^2.5.1"

View file

@ -258,6 +258,16 @@
"labels": ["Team: AWP: Visualization", "release_note:skip", "backport:skip"],
"enabled": true,
"prCreation": "immediate"
},
{
"groupName": "XState",
"matchPackageNames": ["xstate"],
"matchPackagePrefixes": ["@xstate/"],
"reviewers": ["team:infra-monitoring-ui"],
"matchBaseBranches": ["main"],
"labels": ["Team:Infra Monitoring UI", "release_note:skip"],
"enabled": true,
"prCreation": "immediate"
}
]
}

View file

@ -0,0 +1,223 @@
## Summary
Within the Infra plugin (specifically Logs) we use [Xstate](https://xstate.js.org/) for managing state. Xstate brings finite state machines and statecharts to JavaScript and TypeScript. The [Xstate docs](https://xstate.js.org/docs/) themselves are good, but this documentation serves to highlight patterns and certain choices we've made with regards to solution usage.
## Optional actions / exposing events
Xstate has methods and means for parent <-> child communication, and when we want to communicate from child to parent the most convenient method is to use [`sendParent()`](https://xstate.js.org/docs/guides/communication.html#sending-events). In cases where the parent <-> child relationship is "baked in" this is by far the easiest and most direct method to use. However, there are occasions where you might have a more generic machine that you want to compose within another machine so that it (the parent) can respond to certain events. In this case blindly responding to the events that result from `sendParent()` would require knowing about the internals of that machine, even though the relationship is more generic in nature (and the child machine may well be used elsewhere). In this case it is nice to have a more explicit contract, so that we can say "hello actor, what events do you emit?" and then we can selectively respond to those.
The pattern we have used to deal with this involves assigning the actions in an optional manner, with them being a no-op by default.
### Example
In Logs we have a scenario where there is a more generic `LogView` state machine, and a more specific `LogStream` state machine. The stream machine needs to respond to the log view machine (but it's entirely possible the log view machine *could* be composed elsewhere).
In the pure implementation of the `LogView` machine the following actions are defined as no-ops:
```ts
actions: {
notifyLoadingStarted: actions.pure(() => undefined),
notifyLoadingSucceeded: actions.pure(() => undefined),
notifyLoadingFailed: actions.pure(() => undefined)
}
```
We can now override these actions when that machine is created elsewhere. For example, let's say we were spawning a `LogView` machine with optional actions:
```ts
spawnLogViewMachine: assign({
logViewMachineRef: () =>
spawn(
createLogViewStateMachine().withConfig({
actions: {
notifyLoadingStarted: sendIfDefined(SpecialTargets.Parent)(
logViewListenerEventSelectors.loadingLogViewStarted
),
notifyLoadingSucceeded: sendIfDefined(SpecialTargets.Parent)(
logViewListenerEventSelectors.loadingLogViewSucceeded
),
notifyLoadingFailed: sendIfDefined(SpecialTargets.Parent)(
logViewListenerEventSelectors.loadingLogViewFailed
),
},
}),
'logViewMachine'
),
}),
},
```
Here the `LogView` machine would instead send an event to the parent that spawned the machine (rather than the no-op).
When the `loading` state is entered within the `LogView` machine the `notifyLoadingStarted` action is executed.
```ts
loading: {
entry: 'notifyLoadingStarted'
},
```
`logViewNotificationEventSelectors.loadingLogViewStarted` (and company) define the event based on the shape of what's in `context`, for example:
```ts
loadingLogViewStarted: (context: LogViewContext) =>
'logViewId' in context
? ({
type: 'loadingLogViewStarted',
logViewId: context.logViewId,
} as LogViewNotificationEvent)
: undefined,
```
The consumer can now choose to respond to this event in some way.
## Event notifications from outside of a machine
Xstate has several mechanisms for parent <-> child communication, a parent can (for example) [`invoke`](https://xstate.js.org/docs/guides/communication.html#the-invoke-property) a child actor, or [`spawn`](https://xstate.js.org/docs/guides/actors.html#spawning-actors) an actor and assign it to `context`. However, we might need to communicate with an actor that was instantiated outside of the machine, here the parent -> child relationship is less obvious, but we still want to enforce a pattern that makes this obvious and "contractual".
In our real `LogView` -> `Stream` example the `LogView` machine is actually instantiated in a very different part of the React hierarchy to the stream machine, but we still want to respond to these events. The problem is the stream machine will no longer be directly spawning or invoking the `LogView` machine, so there is no strict parent <-> child relationship.
We have opted to use a notification channel approach to this.
When the machine is created it is passed a notification channel:
```ts
createLogStreamPageStateMachine({
logViewStateNotifications,
}),
```
Within our UI this channel is created within a hook that manages `LogView`s (but it could be created anywhere):
`const [logViewStateNotifications] = useState(() => createLogViewNotificationChannel());`
Now when the stream machine is created a `logViewNotifications` service can be defined, and that service is the result of calling `createService()` on the channel, this returns an Observable.
```ts
createPureLogStreamPageStateMachine().withConfig({
services: {
logViewNotifications: () => logViewStateNotifications.createService(),
},
});
```
The service itself is `invoked`:
```ts
invoke: {
src: 'logViewNotifications',
},
```
When the Observable emits these events will be responded to via the machine.
## createPure() vs create()
We have developed a pattern whereby each machine has a pure and non-pure version. The pure version defines the core parts of the machine (states, actions, transitions etc), this is useful for things like tests. It contains the things that are *always* required. Then there is the non-pure version (this can be thought of as the UI-centric version) this version will inject the real services, actions etc.
Pure example:
```ts
export const createPureLogStreamPageStateMachine = (initialContext: LogStreamPageContext = {}) =>
createMachine<LogStreamPageContext, LogStreamPageEvent, LogStreamPageTypestate>(
{
context: initialContext,
predictableActionArguments: true,
invoke: {
src: 'logViewNotifications',
},
id: 'logStreamPageState',
initial: 'uninitialized',
states: {
uninitialized: {
on: {
loadingLogViewStarted: {
target: 'loadingLogView',
},
loadingLogViewFailed: {
target: 'loadingLogViewFailed',
actions: 'storeLogViewError',
},
},
},
// More states
},
{
actions: {
storeLogViewError: assign((_context, event) =>
event.type === 'loadingLogViewFailed'
? ({ logViewError: event.error } as LogStreamPageContextWithLogViewError)
: {}
),
},
guards: {
hasLogViewIndices: (_context, event) =>
event.type === 'loadingLogViewSucceeded' &&
['empty', 'available'].includes(event.status.index),
},
}
);
```
Non-pure example:
```ts
export const createLogStreamPageStateMachine = ({
logViewStateNotifications,
}: {
logViewStateNotifications: LogViewNotificationChannel;
}) =>
createPureLogStreamPageStateMachine().withConfig({
services: {
logViewNotifications: () => logViewStateNotifications.createService(),
},
});
```
Here we call `withConfig()` which returns a new instance with our overrides, in this case we inject the correct services.
## Pairing with React
There is a [`@xstate/react` library](https://xstate.js.org/docs/recipes/react.html#usage-with-react) that provides some helpful hooks and utilities for combining React and Xstate.
We have opted to use a provider approach for providing state to the React hierarchy, e.g.:
```ts
export const useLogStreamPageState = ({
logViewStateNotifications,
}: {
logViewStateNotifications: LogViewNotificationChannel;
}) => {
const logStreamPageStateService = useInterpret(
() =>
createLogStreamPageStateMachine({
logViewStateNotifications,
})
);
return logStreamPageStateService;
};
export const [LogStreamPageStateProvider, useLogStreamPageStateContext] =
createContainer(useLogStreamPageState);
```
[`useInterpret`](https://xstate.js.org/docs/packages/xstate-react/#useinterpret-machine-options-observer) returns a **static** reference:
> returns a static reference (to just the interpreted machine) which will not rerender when its state changes
When dealing with state it is best to use [selectors](https://xstate.js.org/docs/packages/xstate-react/#useselector-actor-selector-compare-getsnapshot), the `useSelector` hook can significantly increase performance over `useMachine`:
> This hook will only cause a rerender if the selected value changes, as determined by the optional compare function.
## TypeScript usage
Our usage of Xstate is fully typed. We have opted for a [Typestate](https://xstate.js.org/docs/guides/typescript.html#typestates) approach, which allows us to narrow down the shape of `context` based on the state `value`. [Typegen](https://xstate.js.org/docs/guides/typescript.html#typegen) may be a possible solution in the future, but at the time of writing this causes some friction with the way we work.
## DX Tools
We recommend using the [Xstate VSCode extension](https://marketplace.visualstudio.com/items?itemName=statelyai.stately-vscode), this includes various features, but arguably the most useful is being able to visually work with the machine. Even if you don't work with VSCode day to day it may be worth installing to utilise this extension for Xstate work.
When [devTools](https://xstate.js.org/docs/guides/interpretation.html#options) are enabled you can also make use of the [Redux DevTools extension](https://github.com/reduxjs/redux-devtools) to view information about your running state machines.
You can also use [Stately.ai](https://stately.ai/) directly in the browser.

View file

@ -92,7 +92,7 @@ export const ExpressionEditor: React.FC<
const isInternal = props.metadata?.isInternal ?? false;
const [logViewId] = useSourceId();
const {
services: { http, logViews },
services: { logViews },
} = useKibanaContextForPlugin(); // injected during alert registration
return (
@ -102,7 +102,7 @@ export const ExpressionEditor: React.FC<
<Editor {...props} />
</SourceStatusWrapper>
) : (
<LogViewProvider logViewId={logViewId} logViews={logViews.client} fetch={http.fetch}>
<LogViewProvider logViewId={logViewId} logViews={logViews.client}>
<SourceStatusWrapper {...props}>
<Editor {...props} />
</SourceStatusWrapper>

View file

@ -137,7 +137,6 @@ Read more at https://github.com/elastic/kibana/blob/main/src/plugins/kibana_reac
} = useLogView({
logViewId: logView.logViewId,
logViews,
fetch: http.fetch,
});
const parsedQuery = useMemo<BuiltEsQuery | undefined>(() => {

View file

@ -5,8 +5,13 @@
* 2.0.
*/
import { interpret } from 'xstate';
import { createLogViewMock } from '../../common/log_views/log_view.mock';
import { createResolvedLogViewMockFromAttributes } from '../../common/log_views/resolved_log_view.mock';
import {
createLogViewNotificationChannel,
createPureLogViewStateMachine,
} from '../observability_logs/log_view_state/src';
import { useLogView } from './use_log_view';
type UseLogView = typeof useLogView;
@ -29,11 +34,14 @@ export const createUninitializedUseLogViewMock =
isUninitialized: true,
latestLoadLogViewFailures: [],
load: jest.fn(),
retry: jest.fn(),
logView: undefined,
logViewId,
logViewStatus: undefined,
resolvedLogView: undefined,
update: jest.fn(),
logViewStateService: interpret(createPureLogViewStateMachine({ logViewId })),
logViewStateNotifications: createLogViewNotificationChannel(),
});
export const createLoadingUseLogViewMock =

View file

@ -5,118 +5,133 @@
* 2.0.
*/
import { useInterpret, useSelector } from '@xstate/react';
import createContainer from 'constate';
import { useCallback, useEffect, useState } from 'react';
import type { HttpHandler } from '@kbn/core/public';
import { LogView, LogViewAttributes, LogViewStatus, ResolvedLogView } from '../../common/log_views';
import { waitFor } from 'xstate/lib/waitFor';
import { LogViewAttributes } from '../../common/log_views';
import {
createLogViewNotificationChannel,
createLogViewStateMachine,
} from '../observability_logs/log_view_state';
import type { ILogViewsClient } from '../services/log_views';
import { isRejectedPromiseState, useTrackedPromise } from '../utils/use_tracked_promise';
import { isDevMode } from '../utils/dev_mode';
export const useLogView = ({
logViewId,
logViews,
fetch,
useDevTools = isDevMode(),
}: {
logViewId: string;
logViews: ILogViewsClient;
fetch: HttpHandler;
useDevTools?: boolean;
}) => {
const [logView, setLogView] = useState<LogView | undefined>(undefined);
const [logViewStateNotifications] = useState(() => createLogViewNotificationChannel());
const [resolvedLogView, setResolvedLogView] = useState<ResolvedLogView | undefined>(undefined);
const [logViewStatus, setLogViewStatus] = useState<LogViewStatus | undefined>(undefined);
const [loadLogViewRequest, loadLogView] = useTrackedPromise(
const logViewStateService = useInterpret(
() =>
createLogViewStateMachine({
initialContext: {
logViewId,
},
logViews,
notificationChannel: logViewStateNotifications,
}),
{
cancelPreviousOn: 'resolution',
createPromise: logViews.getLogView.bind(logViews),
onResolve: setLogView,
},
[logViews]
devTools: useDevTools,
}
);
const [resolveLogViewRequest, resolveLogView] = useTrackedPromise(
{
cancelPreviousOn: 'resolution',
createPromise: logViews.resolveLogView.bind(logViews),
onResolve: setResolvedLogView,
},
[logViews]
useEffect(() => {
logViewStateService.send({
type: 'LOG_VIEW_ID_CHANGED',
logViewId,
});
}, [logViewId, logViewStateService]);
const logView = useSelector(logViewStateService, (state) =>
state.matches('resolving') || state.matches('checkingStatus') || state.matches('resolved')
? state.context.logView
: undefined
);
const [updateLogViewRequest, updateLogView] = useTrackedPromise(
{
cancelPreviousOn: 'resolution',
createPromise: logViews.putLogView.bind(logViews),
onResolve: setLogView,
},
[logViews]
const resolvedLogView = useSelector(logViewStateService, (state) =>
state.matches('checkingStatus') || state.matches('resolved')
? state.context.resolvedLogView
: undefined
);
const [loadLogViewStatusRequest, loadLogViewStatus] = useTrackedPromise(
{
cancelPreviousOn: 'resolution',
createPromise: logViews.getResolvedLogViewStatus.bind(logViews),
onResolve: setLogViewStatus,
},
[logViews]
const logViewStatus = useSelector(logViewStateService, (state) =>
state.matches('resolved') ? state.context.status : undefined
);
const isLoadingLogView = loadLogViewRequest.state === 'pending';
const isResolvingLogView = resolveLogViewRequest.state === 'pending';
const isLoadingLogViewStatus = loadLogViewStatusRequest.state === 'pending';
const isUpdatingLogView = updateLogViewRequest.state === 'pending';
const isLoadingLogView = useSelector(logViewStateService, (state) => state.matches('loading'));
const isResolvingLogView = useSelector(logViewStateService, (state) =>
state.matches('resolving')
);
const isLoadingLogViewStatus = useSelector(logViewStateService, (state) =>
state.matches('checkingStatus')
);
const isUpdatingLogView = useSelector(logViewStateService, (state) => state.matches('updating'));
const isLoading =
isLoadingLogView || isResolvingLogView || isLoadingLogViewStatus || isUpdatingLogView;
const isUninitialized = loadLogViewRequest.state === 'uninitialized';
const isUninitialized = useSelector(logViewStateService, (state) =>
state.matches('uninitialized')
);
const hasFailedLoadingLogView = loadLogViewRequest.state === 'rejected';
const hasFailedResolvingLogView = resolveLogViewRequest.state === 'rejected';
const hasFailedLoadingLogViewStatus = loadLogViewStatusRequest.state === 'rejected';
const hasFailedLoadingLogView = useSelector(logViewStateService, (state) =>
state.matches('loadingFailed')
);
const hasFailedResolvingLogView = useSelector(logViewStateService, (state) =>
state.matches('resolutionFailed')
);
const hasFailedLoadingLogViewStatus = useSelector(logViewStateService, (state) =>
state.matches('checkingStatusFailed')
);
const latestLoadLogViewFailures = [
loadLogViewRequest,
resolveLogViewRequest,
loadLogViewStatusRequest,
]
.filter(isRejectedPromiseState)
.map(({ value }) => (value instanceof Error ? value : new Error(`${value}`)));
const latestLoadLogViewFailures = useSelector(logViewStateService, (state) =>
state.matches('loadingFailed') ||
state.matches('resolutionFailed') ||
state.matches('checkingStatusFailed')
? [state.context.error]
: []
);
const hasFailedLoading = latestLoadLogViewFailures.length > 0;
const load = useCallback(async () => {
const loadedLogView = await loadLogView(logViewId);
const resolvedLoadedLogView = await resolveLogView(loadedLogView.id, loadedLogView.attributes);
const resolvedLogViewStatus = await loadLogViewStatus(resolvedLoadedLogView);
return [loadedLogView, resolvedLoadedLogView, resolvedLogViewStatus];
}, [logViewId, loadLogView, loadLogViewStatus, resolveLogView]);
const retry = useCallback(
() =>
logViewStateService.send({
type: 'RETRY',
}),
[logViewStateService]
);
const update = useCallback(
async (logViewAttributes: Partial<LogViewAttributes>) => {
const updatedLogView = await updateLogView(logViewId, logViewAttributes);
const resolvedUpdatedLogView = await resolveLogView(
updatedLogView.id,
updatedLogView.attributes
);
const resolvedLogViewStatus = await loadLogViewStatus(resolvedUpdatedLogView);
logViewStateService.send({
type: 'UPDATE',
attributes: logViewAttributes,
});
return [updatedLogView, resolvedUpdatedLogView, resolvedLogViewStatus];
const doneState = await waitFor(
logViewStateService,
(state) => state.matches('updatingFailed') || state.matches('resolved')
);
if (doneState.matches('updatingFailed')) {
throw doneState.context.error;
}
},
[logViewId, loadLogViewStatus, resolveLogView, updateLogView]
[logViewStateService]
);
useEffect(() => {
load();
}, [load]);
return {
logViewId,
isUninitialized,
derivedDataView: resolvedLogView?.dataViewReference,
// underlying state machine
logViewStateService,
logViewStateNotifications,
// Failure states
hasFailedLoading,
@ -126,18 +141,22 @@ export const useLogView = ({
latestLoadLogViewFailures,
// Loading states
isUninitialized,
isLoading,
isLoadingLogView,
isLoadingLogViewStatus,
isResolvingLogView,
// data
logViewId,
logView,
resolvedLogView,
logViewStatus,
derivedDataView: resolvedLogView?.dataViewReference,
// actions
load,
load: retry,
retry,
update,
};
};

View file

@ -0,0 +1,3 @@
# @kbn/observability-logs-log-stream-page-state
The state machine of the Observability Logs UI stream page.

View file

@ -0,0 +1,15 @@
/*
* 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 {
createLogStreamPageStateMachine,
LogStreamPageStateProvider,
useLogStreamPageState,
useLogStreamPageStateContext,
type LogStreamPageContext,
type LogStreamPageEvent,
} from './src';

View file

@ -0,0 +1,10 @@
/*
* 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 './provider';
export * from './state_machine';
export * from './types';

View file

@ -0,0 +1,33 @@
/*
* 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 { useInterpret } from '@xstate/react';
import createContainer from 'constate';
import { isDevMode } from '../../../../utils/dev_mode';
import { type LogViewNotificationChannel } from '../../../log_view_state';
import { createLogStreamPageStateMachine } from './state_machine';
export const useLogStreamPageState = ({
logViewStateNotifications,
useDevTools = isDevMode(),
}: {
logViewStateNotifications: LogViewNotificationChannel;
useDevTools?: boolean;
}) => {
const logStreamPageStateService = useInterpret(
() =>
createLogStreamPageStateMachine({
logViewStateNotifications,
}),
{ devTools: useDevTools }
);
return logStreamPageStateService;
};
export const [LogStreamPageStateProvider, useLogStreamPageStateContext] =
createContainer(useLogStreamPageState);

View file

@ -0,0 +1,127 @@
/*
* 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 { assign, createMachine } from 'xstate';
import type { LogViewNotificationChannel } from '../../../log_view_state';
import type {
LogStreamPageContext,
LogStreamPageContextWithLogView,
LogStreamPageContextWithLogViewError,
LogStreamPageEvent,
LogStreamPageTypestate,
} from './types';
export const createPureLogStreamPageStateMachine = (initialContext: LogStreamPageContext = {}) =>
/** @xstate-layout N4IgpgJg5mDOIC5QBsD2UDKAXATmAhgLYAK+M2+WYAdAK4B2Alk1o-sowF6QDEAMgHkAggBEAkgDkA4gH1BsgGpiAogHUZGACpCASpuUiA2gAYAuolAAHVLEatU9CyAAeiALQAmAJzUPHgGwArIEALAAcHmHhXsEhADQgAJ6IAQDM1IHGYQDs2ZnZxl5e-l6pAL5lCWiYuAQkZGAUVHRMLGwc3BD8wuLScgKKKuoAYkJifAYm5kgg1rb2jjOuCJ4hAIzUqZklHgX+xqlrkQnJCKlB1P4loYH+qV7hHoEVVejYeESk5FiUNAzMdnaXF4glEklk8hkSjUGgAqgBheHKAyTMxOOaAhxOZbZNbZajGXLBVIkrJrYInRBHYzUEIeVIhVJhNZZLws7IhF4garvOpfRo-Zr-NrsYFdUG9CEDKFDOGI5EiSZraZWGyYxagHEhEIEwkeI7Zc7GO6UhCZHzZML7MKBDwhQp0zmVblvWqfBpNGhofAQZhQPjoBSMMAAd26YL6kOhIzGEyMaJmGIW2JS4VpJTCWXp5K82Q8pvN1Et1tt9oedq5PLd9W+v2o3t99H9geDYYl4P6gxhGARSJR8ZVszVyaWiEZPnt-m8Ry8xjta38pqN1FK+uy+w8xhCgS8lddHxrArrDb9AagQdD4clnZl3d7CqVg6TjCxo4QISumy2MWNMWiAVNO58WMFkImMOc50CMI9xqA9+U9etUB9U8W1DYZ8EYZAQR6Dso1lLRdH0Ad0WHF8NRcdxvF8AJYgiKIwhiUIC1SDwMgNcC8g5O1nmdKs4I9QUaAAC3wWAzwvEMxHoX0AGM4BaAFWFFToeB0ZQkTEBQDBkIQ+D4GRiF0IQAFllH0HQMCmEj5jIlMEDWFj0mNfw1i8SJcVCVJTXtfEGJc7d6RCLjDQqZ16FQCA4CcPi+QE35rPVOzPAYzZtjcvYDgc003DWHVCSCIJbkNWcvCtGDeXdWshVaQFlMgBKR01Md8ySKk6WoclwKyG02SCLdyureDBMQ5Cm3E1sGtst9dgyUIritG1ch-eJWrOMIwk2Yojn8PMtj8HjXlg2Kqq9JDG2bc9W3QzD6sTUjXyas1qGZO03LuHa2R6wCdyLVIORAlk6VKgb+JO6gRLE1DJOkxg5PgO6bIeij7IcnVHlufwQlnFzMaXTdNpcqC8jZQ5chB46j2aCHxtDKTZPk4Vao6W7VUR8jljWNYIlpCIMax40FxW04fOeraAoZYLyl4-cKYQ6mobp2H5MUoFOkmpGOZ3fEdvWQ0STc21vMJUX-NtCW6Wg6WjsqymaEIRhYFsMaFZhuH1fZqlDn8XxCj8RisiK76LT+v7cTDg4pYqIA */
createMachine<LogStreamPageContext, LogStreamPageEvent, LogStreamPageTypestate>(
{
context: initialContext,
predictableActionArguments: true,
invoke: {
src: 'logViewNotifications',
},
id: 'logStreamPageState',
initial: 'uninitialized',
states: {
uninitialized: {
on: {
LOADING_LOG_VIEW_STARTED: {
target: 'loadingLogView',
},
LOADING_LOG_VIEW_FAILED: {
target: 'loadingLogViewFailed',
actions: 'storeLogViewError',
},
LOADING_LOG_VIEW_SUCCEEDED: [
{
target: 'hasLogViewIndices',
cond: 'hasLogViewIndices',
actions: 'storeResolvedLogView',
},
{
target: 'missingLogViewIndices',
actions: 'storeResolvedLogView',
},
],
},
},
loadingLogView: {
on: {
LOADING_LOG_VIEW_FAILED: {
target: 'loadingLogViewFailed',
actions: 'storeLogViewError',
},
LOADING_LOG_VIEW_SUCCEEDED: [
{
target: 'hasLogViewIndices',
cond: 'hasLogViewIndices',
actions: 'storeResolvedLogView',
},
{
target: 'missingLogViewIndices',
actions: 'storeResolvedLogView',
},
],
},
},
loadingLogViewFailed: {
on: {
LOADING_LOG_VIEW_STARTED: {
target: 'loadingLogView',
},
},
},
hasLogViewIndices: {
initial: 'uninitialized',
states: {
uninitialized: {
on: {
RECEIVED_ALL_PARAMETERS: {
target: 'initialized',
},
},
},
initialized: {},
},
},
missingLogViewIndices: {},
},
},
{
actions: {
storeLogViewError: assign((_context, event) =>
event.type === 'LOADING_LOG_VIEW_FAILED'
? ({ logViewError: event.error } as LogStreamPageContextWithLogViewError)
: {}
),
storeResolvedLogView: assign((_context, event) =>
event.type === 'LOADING_LOG_VIEW_SUCCEEDED'
? ({
logViewStatus: event.status,
resolvedLogView: event.resolvedLogView,
} as LogStreamPageContextWithLogView)
: {}
),
},
guards: {
hasLogViewIndices: (_context, event) =>
event.type === 'LOADING_LOG_VIEW_SUCCEEDED' &&
['empty', 'available'].includes(event.status.index),
},
}
);
export const createLogStreamPageStateMachine = ({
logViewStateNotifications,
}: {
logViewStateNotifications: LogViewNotificationChannel;
}) =>
createPureLogStreamPageStateMachine().withConfig({
services: {
logViewNotifications: () => logViewStateNotifications.createService(),
},
});

View file

@ -0,0 +1,53 @@
/*
* 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 type { LogViewStatus } from '../../../../../common/log_views';
import type {
LogViewContextWithError,
LogViewContextWithResolvedLogView,
LogViewNotificationEvent,
} from '../../../log_view_state';
export type LogStreamPageEvent =
| LogViewNotificationEvent
| {
type: 'RECEIVED_ALL_PARAMETERS';
};
export interface LogStreamPageContextWithLogView {
logViewStatus: LogViewStatus;
resolvedLogView: LogViewContextWithResolvedLogView['resolvedLogView'];
}
export interface LogStreamPageContextWithLogViewError {
logViewError: LogViewContextWithError['error'];
}
export type LogStreamPageTypestate =
| {
value: 'uninitialized';
context: {};
}
| {
value: 'loadingLogView';
context: {};
}
| {
value: 'loadingLogViewFailed';
context: LogStreamPageContextWithLogViewError;
}
| {
value: 'hasLogViewIndices';
context: LogStreamPageContextWithLogView;
}
| {
value: 'missingLogViewIndices';
context: LogStreamPageContextWithLogView;
};
export type LogStreamPageStateValue = LogStreamPageTypestate['value'];
export type LogStreamPageContext = LogStreamPageTypestate['context'];

View file

@ -0,0 +1,3 @@
# @kbn/observability-logs-log-view-state
A state machine to manage log views

View file

@ -0,0 +1,8 @@
/*
* 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';

View file

@ -0,0 +1,10 @@
/*
* 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 { createLogViewNotificationChannel, type LogViewNotificationEvent } from './notifications';
export * from './state_machine';
export * from './types';

View file

@ -0,0 +1,53 @@
/*
* 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 { LogViewStatus, ResolvedLogView } from '../../../../common/log_views';
import { createNotificationChannel } from '../../xstate_helpers';
import { LogViewContext, LogViewEvent } from './types';
export type LogViewNotificationEvent =
| {
type: 'LOADING_LOG_VIEW_STARTED';
logViewId: string;
}
| {
type: 'LOADING_LOG_VIEW_SUCCEEDED';
resolvedLogView: ResolvedLogView;
status: LogViewStatus;
}
| {
type: 'LOADING_LOG_VIEW_FAILED';
error: Error;
};
export const createLogViewNotificationChannel = () =>
createNotificationChannel<LogViewContext, LogViewEvent, LogViewNotificationEvent>();
export const logViewNotificationEventSelectors = {
loadingLogViewStarted: (context: LogViewContext) =>
'logViewId' in context
? ({
type: 'LOADING_LOG_VIEW_STARTED',
logViewId: context.logViewId,
} as LogViewNotificationEvent)
: undefined,
loadingLogViewSucceeded: (context: LogViewContext) =>
'resolvedLogView' in context && 'status' in context
? ({
type: 'LOADING_LOG_VIEW_SUCCEEDED',
resolvedLogView: context.resolvedLogView,
status: context.status,
} as LogViewNotificationEvent)
: undefined,
loadingLogViewFailed: (context: LogViewContext) =>
'error' in context
? ({
type: 'LOADING_LOG_VIEW_FAILED',
error: context.error,
} as LogViewNotificationEvent)
: undefined,
};

View file

@ -0,0 +1,314 @@
/*
* 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 { catchError, from, map, of, throwError } from 'rxjs';
import { createMachine, actions, assign } from 'xstate';
import { ILogViewsClient } from '../../../services/log_views';
import { NotificationChannel } from '../../xstate_helpers';
import { LogViewNotificationEvent, logViewNotificationEventSelectors } from './notifications';
import {
LogViewContext,
LogViewContextWithError,
LogViewContextWithId,
LogViewContextWithLogView,
LogViewContextWithResolvedLogView,
LogViewContextWithStatus,
LogViewEvent,
LogViewTypestate,
} from './types';
export const createPureLogViewStateMachine = (initialContext: LogViewContextWithId) =>
/** @xstate-layout N4IgpgJg5mDOIC5QBkD2UBqBLMB3AxMgPIDiA+hgJICiA6mZQCJkDCAEgIIByJ1jA2gAYAuolAAHVLCwAXLKgB2YkAA9EAFkEBmAHQAmAGwB2dQYAcAVkGmAjGYMAaEAE9EAWi3qjO9WcGCLIzMbLWMLAE4AX0inNEwcAgBVAAVGDgAVaiFRJBBJaTlFZTUEG20DfUEDLUtwg3CbdXUtJ1cEDz11HSMLTzMagwtjAOjY9Gw8HQBXBSxZuQBDABssAC9IfGzlfNl5JVySz3CdGwMwrRsbCK0a1sQyiqNDCMEjcK09QT8LUZA4idwOiWqAWEDmUEIRA4jEoPDIAGVEiwWNQ+HwtrkdoV9qASkZPDoLnpwmZniTgmY7ghSTorP5fFo6lorHobL9-gkgSCwQoIcRobDyAAxDiUZDokTbKS7IoHRBWMzdT4vCxDPQ3PRUvTqnThcL+OoWVmdU7qdnjTkAJzgqCWADdwfgAErUeFEZCJdKUIhcMgisUSnISaXY4qIcm0y6XJ5mdR6Iw2IxanV6g2DY3qRPm+KTa2wW0O3nO13uz3e32I5GoxiBqUFPZh0ra7z9S6CBo9G4tFyIGl06z9JlWOzZgE6ADGAAswOOANbg+EyBYyKawfDsagsADSgoR6QyiXhftF4oEksxIYbctKFhCOksxjsF3CQSTPYQ2t0qfb6ZsJqMo6clOM7zryi7Lqu65sJuO5wvC+7pIeCJIiiaJnkGeSXrKuL3K+3RnJ8eqGPGgRUm8PjEvq5jBKE6oATEfwWrmNr2hsLr8swxDkFQdAYsG9bYao9x6BYXSkjcBrqp8RiOO+Hg6NUAzhEM6gkvUNRmgxHKTMCoLgkKCxYEsbHUOkToAJp8ZhAk4kJCCMl0MlPKqoS+O2b5tJ0jynGc+KCHowR1HogHMfmSxTNiBlGSZZmWee-EyrZJT6jYCkfARVxaDJsZaqY3Q+cYWj+YFBghYCwFzguS4rrAUXGRAxaxVZWJXjhpSZt4ng2O84Qie2njdp5eUJmchXFd1BjBVpTGAlM4gQMujopGkXpwv6p7NVhSXCV43SqfU2qqYYWVUm42rHAOcb1L12gsmV0zzYtRbLRku6VqhNboXWiWNi+3j6spfSDB84TqKdZjHAY-kRE05hfMSbLTTms2PXIvJ1SZHFkFxFA0LQm02Y2xiKsyJJNPqNxQ5Scl-hYOgBEVt6fsYqn0QxCioBAcDKNpuDfaG14hIq9T2FcjSWEEgync0XQkv4k1ZQYjQRD8SNjjMcy7MsayQPzrV2XYFQi0rt6+IE9gWFSZR09qZzNN1fRDFEaucrpPJQHrgklF4ej0yJInKRDpIeYg5hKoMZhvGUbxFfRYzIzoeYFuCnvbQgcs+Gb+oib4x1UsE4e9PYZzWLe90VaBUDgTVqeNmLF1GlU9S+FDRpkcLKkhO8Vwkqr8djknrEQLX16spcCl0poRoGE0NztxP1QvmLARPM7-eu9y+mGfVI9tV4RuXOqgSJsSIlUrRJyr8fdT+FUfeMQng8RXsGPDxehPXkvlTVAmQy9Le59JqX2JPiF8gxMyR3LtOSqYFqqrlfrvA2jcfAWH6IYGePtwjnwiCcYqHxbCqk0Jpdekw5oLTRh7d+P1P5nAUp8Dq6hRJeFVKdTovtSR2CaD+TMTQzD3TIU9KACCqECzauLOmYtiZZRqAOVhxJ7ysljCEGSTQ4xs0iEAA */
createMachine<LogViewContext, LogViewEvent, LogViewTypestate>(
{
context: initialContext,
preserveActionOrder: true,
predictableActionArguments: true,
id: 'LogView',
initial: 'uninitialized',
states: {
uninitialized: {
always: {
target: 'loading',
},
},
loading: {
entry: 'notifyLoadingStarted',
invoke: {
src: 'loadLogView',
},
on: {
LOADING_SUCCEEDED: {
target: 'resolving',
actions: 'storeLogView',
},
LOADING_FAILED: {
target: 'loadingFailed',
actions: 'storeError',
},
},
},
resolving: {
invoke: {
src: 'resolveLogView',
},
on: {
RESOLUTION_FAILED: {
target: 'resolutionFailed',
actions: 'storeError',
},
RESOLUTION_SUCCEEDED: {
target: 'checkingStatus',
actions: 'storeResolvedLogView',
},
},
},
checkingStatus: {
invoke: {
src: 'loadLogViewStatus',
},
on: {
CHECKING_STATUS_FAILED: {
target: 'checkingStatusFailed',
actions: 'storeError',
},
CHECKING_STATUS_SUCCEEDED: {
target: 'resolved',
actions: 'storeStatus',
},
},
},
resolved: {
entry: 'notifyLoadingSucceeded',
on: {
RELOAD_LOG_VIEW: {
target: 'loading',
},
},
},
loadingFailed: {
entry: 'notifyLoadingFailed',
on: {
RETRY: {
target: 'loading',
},
},
},
resolutionFailed: {
entry: 'notifyLoadingFailed',
on: {
RETRY: {
target: 'resolving',
},
},
},
checkingStatusFailed: {
entry: 'notifyLoadingFailed',
on: {
RETRY: {
target: 'checkingStatus',
},
},
},
updating: {
entry: 'notifyLoadingStarted',
invoke: {
src: 'updateLogView',
},
on: {
UPDATING_FAILED: {
target: 'updatingFailed',
actions: 'storeError',
},
UPDATING_SUCCEEDED: {
target: 'resolving',
actions: 'storeLogView',
},
},
},
updatingFailed: {
entry: 'notifyLoadingFailed',
on: {
RELOAD_LOG_VIEW: {
target: 'loading',
},
},
},
},
on: {
LOG_VIEW_ID_CHANGED: {
target: '.loading',
actions: 'storeLogViewId',
cond: 'isLogViewIdDifferent',
},
UPDATE: {
target: '.updating',
},
},
},
{
actions: {
notifyLoadingStarted: actions.pure(() => undefined),
notifyLoadingSucceeded: actions.pure(() => undefined),
notifyLoadingFailed: actions.pure(() => undefined),
storeLogViewId: assign((context, event) =>
'logViewId' in event
? ({
logViewId: event.logViewId,
} as LogViewContextWithId)
: {}
),
storeLogView: assign((context, event) =>
'logView' in event
? ({
logView: event.logView,
} as LogViewContextWithLogView)
: {}
),
storeResolvedLogView: assign((context, event) =>
'resolvedLogView' in event
? ({
resolvedLogView: event.resolvedLogView,
} as LogViewContextWithResolvedLogView)
: {}
),
storeStatus: assign((context, event) =>
'status' in event
? ({
status: event.status,
} as LogViewContextWithStatus)
: {}
),
storeError: assign((context, event) =>
'error' in event
? ({
error: event.error,
} as LogViewContextWithError)
: {}
),
},
guards: {
isLogViewIdDifferent: (context, event) =>
'logViewId' in event ? event.logViewId !== context.logViewId : false,
},
}
);
export interface LogViewStateMachineDependencies {
initialContext: LogViewContextWithId;
logViews: ILogViewsClient;
notificationChannel?: NotificationChannel<LogViewContext, LogViewEvent, LogViewNotificationEvent>;
}
export const createLogViewStateMachine = ({
initialContext,
logViews,
notificationChannel,
}: LogViewStateMachineDependencies) =>
createPureLogViewStateMachine(initialContext).withConfig({
actions:
notificationChannel != null
? {
notifyLoadingStarted: notificationChannel.notify(
logViewNotificationEventSelectors.loadingLogViewStarted
),
notifyLoadingSucceeded: notificationChannel.notify(
logViewNotificationEventSelectors.loadingLogViewSucceeded
),
notifyLoadingFailed: notificationChannel.notify(
logViewNotificationEventSelectors.loadingLogViewFailed
),
}
: {},
services: {
loadLogView: (context) =>
from(
'logViewId' in context
? logViews.getLogView(context.logViewId)
: throwError(() => new Error('Failed to load log view: No id found in context.'))
).pipe(
map(
(logView): LogViewEvent => ({
type: 'LOADING_SUCCEEDED',
logView,
})
),
catchError((error) =>
of<LogViewEvent>({
type: 'LOADING_FAILED',
error,
})
)
),
updateLogView: (context, event) =>
from(
'logViewId' in context && event.type === 'UPDATE'
? logViews.putLogView(context.logViewId, event.attributes)
: throwError(
() =>
new Error(
'Failed to update log view: Not invoked by update event with matching id.'
)
)
).pipe(
map(
(logView): LogViewEvent => ({
type: 'UPDATING_SUCCEEDED',
logView,
})
),
catchError((error) =>
of<LogViewEvent>({
type: 'UPDATING_FAILED',
error,
})
)
),
resolveLogView: (context) =>
from(
'logView' in context
? logViews.resolveLogView(context.logView.id, context.logView.attributes)
: throwError(
() => new Error('Failed to resolve log view: No log view found in context.')
)
).pipe(
map(
(resolvedLogView): LogViewEvent => ({
type: 'RESOLUTION_SUCCEEDED',
resolvedLogView,
})
),
catchError((error) =>
of<LogViewEvent>({
type: 'RESOLUTION_FAILED',
error,
})
)
),
loadLogViewStatus: (context) =>
from(
'resolvedLogView' in context
? logViews.getResolvedLogViewStatus(context.resolvedLogView)
: throwError(
() => new Error('Failed to resolve log view: No log view found in context.')
)
).pipe(
map(
(status): LogViewEvent => ({
type: 'CHECKING_STATUS_SUCCEEDED',
status,
})
),
catchError((error) =>
of<LogViewEvent>({
type: 'CHECKING_STATUS_FAILED',
error,
})
)
),
},
});

View file

@ -0,0 +1,140 @@
/*
* 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 type { ActorRef } from 'xstate';
import type {
LogView,
LogViewAttributes,
LogViewStatus,
ResolvedLogView,
} from '../../../../common/log_views';
import { type NotificationChannel } from '../../xstate_helpers';
import { type LogViewNotificationEvent } from './notifications';
export interface LogViewContextWithId {
logViewId: string;
}
export interface LogViewContextWithLogView {
logView: LogView;
}
export interface LogViewContextWithResolvedLogView {
resolvedLogView: ResolvedLogView;
}
export interface LogViewContextWithStatus {
status: LogViewStatus;
}
export interface LogViewContextWithError {
error: Error;
}
export type LogViewTypestate =
| {
value: 'uninitialized';
context: LogViewContextWithId;
}
| {
value: 'loading';
context: LogViewContextWithId;
}
| {
value: 'resolving';
context: LogViewContextWithId & LogViewContextWithLogView;
}
| {
value: 'checkingStatus';
context: LogViewContextWithId & LogViewContextWithLogView & LogViewContextWithResolvedLogView;
}
| {
value: 'resolved';
context: LogViewContextWithId &
LogViewContextWithLogView &
LogViewContextWithResolvedLogView &
LogViewContextWithStatus;
}
| {
value: 'updating';
context: LogViewContextWithId;
}
| {
value: 'loadingFailed';
context: LogViewContextWithId & LogViewContextWithError;
}
| {
value: 'updatingFailed';
context: LogViewContextWithId & LogViewContextWithError;
}
| {
value: 'resolutionFailed';
context: LogViewContextWithId & LogViewContextWithLogView & LogViewContextWithError;
}
| {
value: 'checkingStatusFailed';
context: LogViewContextWithId & LogViewContextWithLogView & LogViewContextWithError;
};
export type LogViewContext = LogViewTypestate['context'];
export type LogViewStateValue = LogViewTypestate['value'];
export type LogViewEvent =
| {
type: 'LOG_VIEW_ID_CHANGED';
logViewId: string;
}
| {
type: 'LOADING_SUCCEEDED';
logView: LogView;
}
| {
type: 'LOADING_FAILED';
error: Error;
}
| {
type: 'RESOLUTION_SUCCEEDED';
resolvedLogView: ResolvedLogView;
}
| {
type: 'UPDATE';
attributes: Partial<LogViewAttributes>;
}
| {
type: 'UPDATING_SUCCEEDED';
logView: LogView;
}
| {
type: 'UPDATING_FAILED';
error: Error;
}
| {
type: 'RESOLUTION_FAILED';
error: Error;
}
| {
type: 'CHECKING_STATUS_SUCCEEDED';
status: LogViewStatus;
}
| {
type: 'CHECKING_STATUS_FAILED';
error: Error;
}
| {
type: 'RETRY';
}
| {
type: 'RELOAD_LOG_VIEW';
};
export type LogViewActorRef = ActorRef<LogViewEvent, LogViewContext>;
export type LogViewNotificationChannel = NotificationChannel<
LogViewContext,
LogViewEvent,
LogViewNotificationEvent
>;

View file

@ -0,0 +1,3 @@
# @kbn/observability-logs-xstate-helpers
Helpers to design well-typed state machines with XState

View file

@ -0,0 +1,8 @@
/*
* 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';

View file

@ -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 './notification_channel';
export * from './send_actions';

View file

@ -0,0 +1,41 @@
/*
* 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 { ReplaySubject } from 'rxjs';
import { ActionFunction, EventObject, Expr, Subscribable } from 'xstate';
export interface NotificationChannel<TContext, TEvent extends EventObject, TSentEvent> {
createService: () => Subscribable<TSentEvent>;
notify: (
eventExpr: Expr<TContext, TEvent, TSentEvent | undefined>
) => ActionFunction<TContext, TEvent>;
}
export const createNotificationChannel = <
TContext,
TEvent extends EventObject,
TSentEvent
>(): NotificationChannel<TContext, TEvent, TSentEvent> => {
const eventsSubject = new ReplaySubject<TSentEvent>(1);
const createService = () => eventsSubject.asObservable();
const notify =
(eventExpr: Expr<TContext, TEvent, TSentEvent | undefined>) =>
(context: TContext, event: TEvent) => {
const eventToSend = eventExpr(context, event);
if (eventToSend != null) {
eventsSubject.next(eventToSend);
}
};
return {
createService,
notify,
};
};

View file

@ -0,0 +1,67 @@
/*
* 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 { actions, ActorRef, EventObject } from 'xstate';
import { sendIfDefined } from './send_actions';
describe('function sendIfDefined', () => {
it('sends the events to the specified target', () => {
const actor = createMockActor();
const createEvent = (context: {}) => ({
type: 'testEvent',
});
const action = sendIfDefined(actor)(createEvent).get({}, { type: 'triggeringEvent' });
expect(action).toEqual([
actions.send('testEvent', {
to: actor,
}),
]);
});
it('sends the events created by the event expression', () => {
const actor = createMockActor();
const createEvent = (context: {}) => ({
type: 'testEvent',
payload: 'payload',
});
const action = sendIfDefined(actor)(createEvent).get({}, { type: 'triggeringEvent' });
expect(action).toEqual([
actions.send(
{
type: 'testEvent',
payload: 'payload',
},
{
to: actor,
}
),
]);
});
it("doesn't send anything when the event expression returns undefined", () => {
const actor = createMockActor();
const createEvent = (context: {}) => undefined;
const action = sendIfDefined(actor)(createEvent).get({}, { type: 'triggeringEvent' });
expect(action).toEqual(undefined);
});
});
const createMockActor = <T extends EventObject>(): ActorRef<T> => ({
getSnapshot: jest.fn(),
id: 'mockActor',
send: jest.fn(),
subscribe: jest.fn(),
[Symbol.observable]() {
return this;
},
});

View file

@ -0,0 +1,36 @@
/*
* 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 {
actions,
ActorRef,
AnyEventObject,
EventObject,
Expr,
PureAction,
SendActionOptions,
} from 'xstate';
export const sendIfDefined =
<TSentEvent extends EventObject = AnyEventObject>(target: string | ActorRef<TSentEvent>) =>
<TContext, TEvent extends EventObject>(
eventExpr: Expr<TContext, TEvent, TSentEvent | undefined>,
options?: SendActionOptions<TContext, TEvent>
): PureAction<TContext, TEvent> => {
return actions.pure((context, event) => {
const targetEvent = eventExpr(context, event);
return targetEvent != null
? [
actions.send(targetEvent, {
...options,
to: target,
}),
]
: undefined;
});
};

View file

@ -35,7 +35,6 @@ export const RedirectToNodeLogs = ({
}: RedirectToNodeLogsType) => {
const { services } = useKibanaContextForPlugin();
const { isLoading, load } = useLogView({
fetch: services.http.fetch,
logViewId: sourceId,
logViews: services.logViews.client,
});

View file

@ -23,7 +23,7 @@ import { SubscriptionSplashPage } from '../../../components/subscription_splash_
import { useLogAnalysisCapabilitiesContext } from '../../../containers/logs/log_analysis';
import { useLogEntryCategoriesModuleContext } from '../../../containers/logs/log_analysis/modules/log_entry_categories';
import { useLogViewContext } from '../../../hooks/use_log_view';
import { LogsPageTemplate } from '../page_template';
import { LogsPageTemplate } from '../shared/page_template';
import { LogEntryCategoriesResultsContent } from './page_results_content';
import { LogEntryCategoriesSetupContent } from './page_setup_content';

View file

@ -7,22 +7,15 @@
import React from 'react';
import { LogAnalysisSetupFlyoutStateProvider } from '../../../components/logging/log_analysis_setup/setup_flyout';
import { LogSourceErrorPage } from '../../../components/logging/log_source_error_page';
import { SourceLoadingPage } from '../../../components/source_loading_page';
import { LogEntryCategoriesModuleProvider } from '../../../containers/logs/log_analysis/modules/log_entry_categories';
import { useActiveKibanaSpace } from '../../../hooks/use_kibana_space';
import { useLogViewContext } from '../../../hooks/use_log_view';
import { ConnectedLogViewErrorPage } from '../shared/page_log_view_error';
export const LogEntryCategoriesPageProviders: React.FunctionComponent = ({ children }) => {
const {
hasFailedLoading,
isLoading,
isUninitialized,
latestLoadLogViewFailures,
load,
resolvedLogView,
logViewId,
} = useLogViewContext();
const { hasFailedLoading, isLoading, isUninitialized, resolvedLogView, logViewId } =
useLogViewContext();
const { space } = useActiveKibanaSpace();
// This is a rather crude way of guarding the dependent providers against
@ -31,7 +24,7 @@ export const LogEntryCategoriesPageProviders: React.FunctionComponent = ({ child
if (space == null) {
return null;
} else if (hasFailedLoading) {
return <LogSourceErrorPage errors={latestLoadLogViewFailures} onRetry={load} />;
return <ConnectedLogViewErrorPage />;
} else if (isLoading || isUninitialized) {
return <SourceLoadingPage />;
} else if (resolvedLogView != null) {

View file

@ -25,7 +25,7 @@ import { useLogEntryCategoriesModuleContext } from '../../../containers/logs/log
import { ViewLogInContextProvider } from '../../../containers/logs/view_log_in_context';
import { useKibanaContextForPlugin } from '../../../hooks/use_kibana';
import { useLogViewContext } from '../../../hooks/use_log_view';
import { LogsPageTemplate } from '../page_template';
import { LogsPageTemplate } from '../shared/page_template';
import { PageViewLogInContext } from '../stream/page_view_log_in_context';
import { TopCategoriesSection } from './sections/top_categories';
import { useLogEntryCategoriesResults } from './use_log_entry_categories_results';

View file

@ -25,7 +25,7 @@ import { useLogAnalysisCapabilitiesContext } from '../../../containers/logs/log_
import { useLogEntryCategoriesModuleContext } from '../../../containers/logs/log_analysis/modules/log_entry_categories';
import { useLogEntryRateModuleContext } from '../../../containers/logs/log_analysis/modules/log_entry_rate';
import { useLogViewContext } from '../../../hooks/use_log_view';
import { LogsPageTemplate } from '../page_template';
import { LogsPageTemplate } from '../shared/page_template';
import { LogEntryRateResultsContent } from './page_results_content';
import { LogEntryRateSetupContent } from './page_setup_content';

View file

@ -7,24 +7,17 @@
import React from 'react';
import { LogAnalysisSetupFlyoutStateProvider } from '../../../components/logging/log_analysis_setup/setup_flyout';
import { LogSourceErrorPage } from '../../../components/logging/log_source_error_page';
import { SourceLoadingPage } from '../../../components/source_loading_page';
import { LogEntryCategoriesModuleProvider } from '../../../containers/logs/log_analysis/modules/log_entry_categories';
import { LogEntryRateModuleProvider } from '../../../containers/logs/log_analysis/modules/log_entry_rate';
import { LogEntryFlyoutProvider } from '../../../containers/logs/log_flyout';
import { useActiveKibanaSpace } from '../../../hooks/use_kibana_space';
import { useLogViewContext } from '../../../hooks/use_log_view';
import { ConnectedLogViewErrorPage } from '../shared/page_log_view_error';
export const LogEntryRatePageProviders: React.FunctionComponent = ({ children }) => {
const {
hasFailedLoading,
isLoading,
isUninitialized,
latestLoadLogViewFailures,
load,
logViewId,
resolvedLogView,
} = useLogViewContext();
const { hasFailedLoading, isLoading, isUninitialized, logViewId, resolvedLogView } =
useLogViewContext();
const { space } = useActiveKibanaSpace();
// This is a rather crude way of guarding the dependent providers against
@ -35,7 +28,7 @@ export const LogEntryRatePageProviders: React.FunctionComponent = ({ children })
} else if (isLoading || isUninitialized) {
return <SourceLoadingPage />;
} else if (hasFailedLoading) {
return <LogSourceErrorPage errors={latestLoadLogViewFailures} onRetry={load} />;
return <ConnectedLogViewErrorPage />;
} else if (resolvedLogView != null) {
return (
<LogEntryFlyoutProvider>

View file

@ -29,7 +29,7 @@ import { useLogEntryCategoriesModuleContext } from '../../../containers/logs/log
import { useLogEntryRateModuleContext } from '../../../containers/logs/log_analysis/modules/log_entry_rate';
import { useLogEntryFlyoutContext } from '../../../containers/logs/log_flyout';
import { useLogViewContext } from '../../../hooks/use_log_view';
import { LogsPageTemplate } from '../page_template';
import { LogsPageTemplate } from '../shared/page_template';
import { AnomaliesResults } from './sections/anomalies';
import { useDatasetFiltering } from './use_dataset_filtering';
import { useLogEntryAnomaliesResults } from './use_log_entry_anomalies_results';

View file

@ -14,12 +14,9 @@ import { LogViewProvider } from '../../hooks/use_log_view';
export const LogsPageProviders: React.FunctionComponent = ({ children }) => {
const [sourceId] = useSourceId();
const { services } = useKibanaContextForPlugin();
return (
<LogViewProvider
fetch={services.http.fetch}
logViewId={sourceId}
logViews={services.logViews.client}
>
<LogViewProvider logViewId={sourceId} logViews={services.logViews.client}>
<LogAnalysisCapabilitiesProvider>{children}</LogAnalysisCapabilitiesProvider>
</LogViewProvider>
);

View file

@ -22,7 +22,7 @@ import { SourceLoadingPage } from '../../../components/source_loading_page';
import { useLogsBreadcrumbs } from '../../../hooks/use_logs_breadcrumbs';
import { useLogViewContext } from '../../../hooks/use_log_view';
import { settingsTitle } from '../../../translations';
import { LogsPageTemplate } from '../page_template';
import { LogsPageTemplate } from '../shared/page_template';
import { IndicesConfigurationPanel } from './indices_configuration_panel';
import { LogColumnsConfigurationPanel } from './log_columns_configuration_panel';
import { NameConfigurationPanel } from './name_configuration_panel';

View file

@ -7,17 +7,19 @@
import { EuiButton, EuiButtonEmpty, EuiCallOut, EuiEmptyPrompt, EuiSpacer } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import React from 'react';
import React, { useCallback } from 'react';
import { SavedObjectNotFound } from '@kbn/kibana-utils-plugin/common';
import { useLinkProps } from '@kbn/observability-plugin/public';
import { useSelector } from '@xstate/react';
import { useLogViewContext } from '../../../hooks/use_log_view';
import {
FetchLogViewStatusError,
FetchLogViewError,
ResolveLogViewError,
} from '../../../common/log_views';
import { LogsPageTemplate } from '../../pages/logs/page_template';
} from '../../../../common/log_views';
import { LogsPageTemplate } from './page_template';
export const LogSourceErrorPage: React.FC<{
export const LogViewErrorPage: React.FC<{
errors: Error[];
onRetry: () => void;
}> = ({ errors, onRetry }) => {
@ -71,6 +73,28 @@ export const LogSourceErrorPage: React.FC<{
);
};
export const LogSourceErrorPage = LogViewErrorPage;
export const ConnectedLogViewErrorPage: React.FC = () => {
const { logViewStateService } = useLogViewContext();
const errors = useSelector(logViewStateService, (state) => {
return state.matches('loadingFailed') ||
state.matches('resolutionFailed') ||
state.matches('checkingStatusFailed')
? [state.context.error]
: [];
});
const retry = useCallback(() => {
logViewStateService.send({
type: 'RETRY',
});
}, [logViewStateService]);
return <LogSourceErrorPage errors={errors} onRetry={retry} />;
};
const LogSourceErrorMessage: React.FC<{ error: Error }> = ({ error }) => {
if (error instanceof ResolveLogViewError) {
return (

View file

@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n';
import type { LazyObservabilityPageTemplateProps } from '@kbn/observability-plugin/public';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { NoDataConfig } from '@kbn/shared-ux-page-kibana-template';
import { useKibanaContextForPlugin } from '../../hooks/use_kibana';
import { useKibanaContextForPlugin } from '../../../hooks/use_kibana';
interface LogsPageTemplateProps extends LazyObservabilityPageTemplateProps {
hasData?: boolean;

View file

@ -8,9 +8,10 @@
import { EuiErrorBoundary } from '@elastic/eui';
import React from 'react';
import { useTrackPageview } from '@kbn/observability-plugin/public';
import { useLogViewContext } from '../../../hooks/use_log_view';
import { useLogsBreadcrumbs } from '../../../hooks/use_logs_breadcrumbs';
import { StreamPageContent } from './page_content';
import { LogsPageProviders } from './page_providers';
import { ConnectedStreamPageContent } from './page_content';
import { LogStreamPageProviders } from './page_providers';
import { streamTitle } from '../../../translations';
export const StreamPage = () => {
@ -22,11 +23,14 @@ export const StreamPage = () => {
text: streamTitle,
},
]);
const { logViewStateNotifications } = useLogViewContext();
return (
<EuiErrorBoundary>
<LogsPageProviders>
<StreamPageContent />
</LogsPageProviders>
<LogStreamPageProviders logViewStateNotifications={logViewStateNotifications}>
<ConnectedStreamPageContent />
</LogStreamPageProviders>
</EuiErrorBoundary>
);
};

View file

@ -5,40 +5,88 @@
* 2.0.
*/
import { i18n } from '@kbn/i18n';
import React from 'react';
import { APP_WRAPPER_CLASS } from '@kbn/core/public';
import { LogSourceErrorPage } from '../../../components/logging/log_source_error_page';
import { i18n } from '@kbn/i18n';
import { useSelector } from '@xstate/react';
import React from 'react';
import { SourceLoadingPage } from '../../../components/source_loading_page';
import { useLogViewContext } from '../../../hooks/use_log_view';
import { LogsPageTemplate } from '../page_template';
import { LogsPageLogsContent } from './page_logs_content';
import { useLogStreamPageStateContext } from '../../../observability_logs/log_stream_page/state/src/provider';
import { fullHeightContentStyles } from '../../../page_template.styles';
import { ConnectedLogViewErrorPage } from '../shared/page_log_view_error';
import { LogsPageTemplate } from '../shared/page_template';
import { LogsPageLogsContent } from './page_logs_content';
import { LogStreamPageContentProviders } from './page_providers';
const streamTitle = i18n.translate('xpack.infra.logs.streamPageTitle', {
defaultMessage: 'Stream',
});
export const StreamPageContent: React.FunctionComponent = () => {
const {
hasFailedLoading,
isLoading,
isUninitialized,
latestLoadLogViewFailures,
load,
logViewStatus,
} = useLogViewContext();
interface InjectedProps {
isLoading: boolean;
hasFailedLoading: boolean;
hasIndices: boolean;
missingIndices: boolean;
}
if (isLoading || isUninitialized) {
export const ConnectedStreamPageContent: React.FC = () => {
const logStreamPageStateService = useLogStreamPageStateContext();
const isLoading = useSelector(logStreamPageStateService, (state) => {
return state.matches('uninitialized') || state.matches('loadingLogView');
});
const hasFailedLoading = useSelector(logStreamPageStateService, (state) =>
state.matches('loadingLogViewFailed')
);
const hasIndices = useSelector(logStreamPageStateService, (state) =>
state.matches('hasLogViewIndices')
);
const missingIndices = useSelector(logStreamPageStateService, (state) =>
state.matches('missingLogViewIndices')
);
return (
<StreamPageContent
isLoading={isLoading}
hasFailedLoading={hasFailedLoading}
hasIndices={hasIndices}
missingIndices={missingIndices}
/>
);
};
export const StreamPageContent: React.FC<InjectedProps> = (props: InjectedProps) => {
const { isLoading, hasFailedLoading, hasIndices, missingIndices } = props;
if (isLoading) {
return <SourceLoadingPage />;
} else if (hasFailedLoading) {
return <LogSourceErrorPage errors={latestLoadLogViewFailures} onRetry={load} />;
} else {
return <ConnectedLogViewErrorPage />;
} else if (missingIndices) {
return (
<div className={APP_WRAPPER_CLASS}>
<LogsPageTemplate
hasData={logViewStatus?.index !== 'missing'}
isDataLoading={isLoading}
hasData={false}
isDataLoading={false}
pageHeader={{
pageTitle: streamTitle,
}}
pageSectionProps={{
contentProps: {
css: fullHeightContentStyles,
},
}}
/>
</div>
);
} else if (hasIndices) {
return (
<div className={APP_WRAPPER_CLASS}>
<LogsPageTemplate
hasData={true}
isDataLoading={false}
pageHeader={{
pageTitle: streamTitle,
}}
@ -48,9 +96,13 @@ export const StreamPageContent: React.FunctionComponent = () => {
},
}}
>
<LogsPageLogsContent />
<LogStreamPageContentProviders>
<LogsPageLogsContent />
</LogStreamPageContentProviders>
</LogsPageTemplate>
</div>
);
} else {
return null;
}
};

View file

@ -1,65 +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 { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { useLinkProps } from '@kbn/observability-plugin/public';
import { NoIndices } from '../../../components/empty_states/no_indices';
import { ViewSourceConfigurationButton } from '../../../components/source_configuration/view_source_configuration_button';
export const LogsPageNoIndicesContent = () => {
const {
services: { application },
} = useKibana<{}>();
const canConfigureSource = application?.capabilities?.logs?.configureSource ? true : false;
const tutorialLinkProps = useLinkProps({
app: 'integrations',
hash: '/browse',
});
return (
<NoIndices
data-test-subj="noLogsIndicesPrompt"
title={i18n.translate('xpack.infra.logsPage.noLoggingIndicesTitle', {
defaultMessage: "Looks like you don't have any logging indices.",
})}
message={i18n.translate('xpack.infra.logsPage.noLoggingIndicesDescription', {
defaultMessage: "Let's add some!",
})}
actions={
<EuiFlexGroup>
<EuiFlexItem>
<EuiButton
{...tutorialLinkProps}
color="primary"
fill
data-test-subj="logsViewSetupInstructionsButton"
>
{i18n.translate('xpack.infra.logsPage.noLoggingIndicesInstructionsActionLabel', {
defaultMessage: 'View setup instructions',
})}
</EuiButton>
</EuiFlexItem>
{canConfigureSource ? (
<EuiFlexItem>
<ViewSourceConfigurationButton app="logs" data-test-subj="configureSourceButton">
{i18n.translate('xpack.infra.configureSourceActionLabel', {
defaultMessage: 'Change source configuration',
})}
</ViewSourceConfigurationButton>
</EuiFlexItem>
) : null}
</EuiFlexGroup>
}
/>
);
};

View file

@ -21,6 +21,8 @@ import { LogStreamProvider, useLogStreamContext } from '../../../containers/logs
import { LogViewConfigurationProvider } from '../../../containers/logs/log_view_configuration';
import { ViewLogInContextProvider } from '../../../containers/logs/view_log_in_context';
import { useLogViewContext } from '../../../hooks/use_log_view';
import { LogStreamPageStateProvider } from '../../../observability_logs/log_stream_page/state';
import { type LogViewNotificationChannel } from '../../../observability_logs/log_view_state';
const LogFilterState: React.FC = ({ children }) => {
const { derivedDataView } = useLogViewContext();
@ -92,14 +94,17 @@ const LogHighlightsState: React.FC = ({ children }) => {
return <LogHighlightsStateProvider {...highlightsProps}>{children}</LogHighlightsStateProvider>;
};
export const LogsPageProviders: React.FunctionComponent = ({ children }) => {
const { logViewStatus } = useLogViewContext();
// The providers assume the source is loaded, so short-circuit them otherwise
if (logViewStatus?.index === 'missing') {
return <>{children}</>;
}
export const LogStreamPageProviders: React.FunctionComponent<{
logViewStateNotifications: LogViewNotificationChannel;
}> = ({ children, logViewStateNotifications }) => {
return (
<LogStreamPageStateProvider logViewStateNotifications={logViewStateNotifications}>
{children}
</LogStreamPageStateProvider>
);
};
export const LogStreamPageContentProviders: React.FunctionComponent = ({ children }) => {
return (
<LogViewConfigurationProvider>
<LogEntryFlyoutProvider>

View file

@ -0,0 +1,12 @@
/*
* 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 getReduxDevtools = () => (window as any).__REDUX_DEVTOOLS_EXTENSION__;
export const hasReduxDevtools = () => getReduxDevtools() != null;
export const isDevMode = () => process.env.NODE_ENV !== 'production';

View file

@ -7,13 +7,14 @@
import { ReduxLikeStateContainer } from '@kbn/kibana-utils-plugin/public';
import { EnhancerOptions } from 'redux-devtools-extension';
import { getReduxDevtools, hasReduxDevtools, isDevMode } from './dev_mode';
export const withReduxDevTools = <StateContainer extends ReduxLikeStateContainer<any>>(
stateContainer: StateContainer,
config?: EnhancerOptions
): StateContainer => {
if (process.env.NODE_ENV !== 'production' && (window as any).__REDUX_DEVTOOLS_EXTENSION__) {
const devToolsExtension = (window as any).__REDUX_DEVTOOLS_EXTENSION__;
if (isDevMode() && hasReduxDevtools()) {
const devToolsExtension = getReduxDevtools();
const devToolsInstance = devToolsExtension.connect({
...config,

View file

@ -16880,9 +16880,6 @@
"xpack.infra.logSourceErrorPage.navigateToSettingsButtonLabel": "Modifier la configuration",
"xpack.infra.logSourceErrorPage.resolveLogSourceConfigurationErrorTitle": "Impossible de résoudre la configuration de la source de logs",
"xpack.infra.logSourceErrorPage.tryAgainButtonLabel": "Réessayer",
"xpack.infra.logsPage.noLoggingIndicesDescription": "Ajoutons-en !",
"xpack.infra.logsPage.noLoggingIndicesInstructionsActionLabel": "Voir les instructions de configuration",
"xpack.infra.logsPage.noLoggingIndicesTitle": "Il semble que vous n'avez aucun index de logging.",
"xpack.infra.logsPage.toolbar.kqlSearchFieldPlaceholder": "Recherche d'entrées de log… (par ex. host.name:host-1)",
"xpack.infra.logsPage.toolbar.logFilterErrorToastTitle": "Erreur de filtrage du log",
"xpack.infra.logsPage.toolbar.logFilterUnsupportedLanguageError": "SQL n'est pas pris en charge",

View file

@ -16866,9 +16866,6 @@
"xpack.infra.logSourceErrorPage.navigateToSettingsButtonLabel": "構成を変更",
"xpack.infra.logSourceErrorPage.resolveLogSourceConfigurationErrorTitle": "ログソース構成を解決できませんでした",
"xpack.infra.logSourceErrorPage.tryAgainButtonLabel": "再試行",
"xpack.infra.logsPage.noLoggingIndicesDescription": "追加しましょう!",
"xpack.infra.logsPage.noLoggingIndicesInstructionsActionLabel": "セットアップの手順を表示",
"xpack.infra.logsPage.noLoggingIndicesTitle": "ログインデックスがないようです。",
"xpack.infra.logsPage.toolbar.kqlSearchFieldPlaceholder": "ログエントリーを検索中…host.name:host-1",
"xpack.infra.logsPage.toolbar.logFilterErrorToastTitle": "ログフィルターエラー",
"xpack.infra.logsPage.toolbar.logFilterUnsupportedLanguageError": "SQLはサポートされていません",

View file

@ -16885,9 +16885,6 @@
"xpack.infra.logSourceErrorPage.navigateToSettingsButtonLabel": "更改配置",
"xpack.infra.logSourceErrorPage.resolveLogSourceConfigurationErrorTitle": "无法解决日志源配置",
"xpack.infra.logSourceErrorPage.tryAgainButtonLabel": "重试",
"xpack.infra.logsPage.noLoggingIndicesDescription": "让我们添加一些!",
"xpack.infra.logsPage.noLoggingIndicesInstructionsActionLabel": "查看设置说明",
"xpack.infra.logsPage.noLoggingIndicesTitle": "似乎您没有任何日志索引。",
"xpack.infra.logsPage.toolbar.kqlSearchFieldPlaceholder": "搜索日志条目……(例如 host.name:host-1",
"xpack.infra.logsPage.toolbar.logFilterErrorToastTitle": "日志筛选错误",
"xpack.infra.logsPage.toolbar.logFilterUnsupportedLanguageError": "不支持 SQL",

View file

@ -8397,6 +8397,14 @@
resolved "https://registry.yarnpkg.com/@xobotyi/scrollbar-width/-/scrollbar-width-1.9.5.tgz#80224a6919272f405b87913ca13b92929bdf3c4d"
integrity sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ==
"@xstate/react@^3.0.1":
version "3.0.1"
resolved "https://registry.yarnpkg.com/@xstate/react/-/react-3.0.1.tgz#937eeb5d5d61734ab756ca40146f84a6fe977095"
integrity sha512-/tq/gg92P9ke8J+yDNDBv5/PAxBvXJf2cYyGDByzgtl5wKaxKxzDT82Gj3eWlCJXkrBg4J5/V47//gRJuVH2fA==
dependencies:
use-isomorphic-layout-effect "^1.0.0"
use-sync-external-store "^1.0.0"
"@xtuc/ieee754@^1.2.0":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790"
@ -26883,7 +26891,7 @@ use-composed-ref@^1.3.0:
resolved "https://registry.yarnpkg.com/use-composed-ref/-/use-composed-ref-1.3.0.tgz#3d8104db34b7b264030a9d916c5e94fbe280dbda"
integrity sha512-GLMG0Jc/jiKov/3Ulid1wbv3r54K9HlMW29IWcDFPEqFkSO2nS0MuefWgMJpeHQ9YJeXDL3ZUF+P3jdXlZX/cQ==
use-isomorphic-layout-effect@^1.1.1:
use-isomorphic-layout-effect@^1.0.0, use-isomorphic-layout-effect@^1.1.1:
version "1.1.2"
resolved "https://registry.yarnpkg.com/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz#497cefb13d863d687b08477d9e5a164ad8c1a6fb"
integrity sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==
@ -26915,7 +26923,7 @@ use-sidecar@^1.1.2:
detect-node-es "^1.1.0"
tslib "^2.0.0"
use-sync-external-store@^1.2.0:
use-sync-external-store@^1.0.0, use-sync-external-store@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a"
integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==
@ -28246,6 +28254,11 @@ xpath@0.0.32:
resolved "https://registry.yarnpkg.com/xpath/-/xpath-0.0.32.tgz#1b73d3351af736e17ec078d6da4b8175405c48af"
integrity sha512-rxMJhSIoiO8vXcWvSifKqhvV96GjiD5wYb8/QHdoRyQvraTpp4IEv944nhGausZZ3u7dhQXteZuZbaqfpB7uYw==
xstate@^4.34.0:
version "4.34.0"
resolved "https://registry.yarnpkg.com/xstate/-/xstate-4.34.0.tgz#401901c478f0b2a7f07576c020b6e6f750b5bd10"
integrity sha512-MFnYz7cJrWuXSZ8IPkcCyLB1a2T3C71kzMeShXKmNaEjBR/JQebKZPHTtxHKZpymESaWO31rA3IQ30TC6LW+sw==
"xtend@>=4.0.0 <4.1.0-0", xtend@^4.0.0, xtend@^4.0.1, xtend@^4.0.2, xtend@~4.0.0, xtend@~4.0.1:
version "4.0.2"
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"