mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[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:
parent
f7706252fd
commit
eb75937130
46 changed files with 1440 additions and 227 deletions
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
223
x-pack/plugins/infra/docs/state_machines/xstate_usage.md
Normal file
223
x-pack/plugins/infra/docs/state_machines/xstate_usage.md
Normal 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.
|
|
@ -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>
|
||||
|
|
|
@ -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>(() => {
|
||||
|
|
|
@ -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 =
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
# @kbn/observability-logs-log-stream-page-state
|
||||
|
||||
The state machine of the Observability Logs UI stream page.
|
|
@ -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';
|
|
@ -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';
|
|
@ -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);
|
|
@ -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(),
|
||||
},
|
||||
});
|
|
@ -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'];
|
|
@ -0,0 +1,3 @@
|
|||
# @kbn/observability-logs-log-view-state
|
||||
|
||||
A state machine to manage log views
|
|
@ -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';
|
|
@ -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';
|
|
@ -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,
|
||||
};
|
|
@ -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,
|
||||
})
|
||||
)
|
||||
),
|
||||
},
|
||||
});
|
|
@ -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
|
||||
>;
|
|
@ -0,0 +1,3 @@
|
|||
# @kbn/observability-logs-xstate-helpers
|
||||
|
||||
Helpers to design well-typed state machines with XState
|
|
@ -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';
|
|
@ -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';
|
|
@ -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,
|
||||
};
|
||||
};
|
|
@ -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;
|
||||
},
|
||||
});
|
|
@ -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;
|
||||
});
|
||||
};
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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 (
|
|
@ -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;
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
|
|
12
x-pack/plugins/infra/public/utils/dev_mode.ts
Normal file
12
x-pack/plugins/infra/public/utils/dev_mode.ts
Normal 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';
|
|
@ -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,
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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はサポートされていません",
|
||||
|
|
|
@ -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",
|
||||
|
|
17
yarn.lock
17
yarn.lock
|
@ -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"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue