mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Logs UI] Refactor log stream query state (#146884)
Co-authored-by: Kerry Gallagher <kerry.gallagher@elastic.co> Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> closes https://github.com/elastic/kibana/issues/145133 closes https://github.com/elastic/kibana/issues/142764
This commit is contained in:
parent
6f3e0a3b95
commit
46d689220b
39 changed files with 1264 additions and 432 deletions
|
@ -36,7 +36,7 @@ const formatError = (error: ValidationError) =>
|
|||
error.value
|
||||
)} does not match expected type ${getErrorType(error)}`;
|
||||
|
||||
const formatErrors = (errors: ValidationError[]) =>
|
||||
export const formatErrors = (errors: ValidationError[]) =>
|
||||
`Failed to validate: \n${errors.map((error) => ` ${formatError(error)}`).join('\n')}`;
|
||||
|
||||
export const createPlainError = (message: string) => new Error(message);
|
||||
|
|
8
x-pack/plugins/infra/docs/state_machines/README.md
Normal file
8
x-pack/plugins/infra/docs/state_machines/README.md
Normal file
|
@ -0,0 +1,8 @@
|
|||
# `infra` plugin XState state machines patterns
|
||||
|
||||
The Logs UI part of the `infra` plugin uses state machines/state charts based on
|
||||
the XState library. Please see the following guides for design and
|
||||
implementation patterns:
|
||||
|
||||
- [Patterns for designing XState state machines](./xstate_machine_patterns.md)
|
||||
- [Patterns for using XState with React](./xstate_react_patterns.md)
|
|
@ -1,6 +1,8 @@
|
|||
# Patterns for designing XState state machines
|
||||
|
||||
## 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.
|
||||
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 creating and composing state machines in our solution plugin. See [Patterns for using XState with React](./xstate_react_patterns.md) for more patterns specific to UI state machines and their consumption in React component hierarchies.
|
||||
|
||||
## Optional actions / exposing events
|
||||
|
||||
|
@ -176,40 +178,6 @@ export const createLogStreamPageStateMachine = ({
|
|||
|
||||
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.
|
||||
|
@ -220,4 +188,4 @@ We recommend using the [Xstate VSCode extension](https://marketplace.visualstudi
|
|||
|
||||
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.
|
||||
You can also use [Stately.ai](https://stately.ai/) directly in the browser.
|
|
@ -0,0 +1,137 @@
|
|||
# Patterns for using XState with React
|
||||
|
||||
## Modelling UI state
|
||||
|
||||
The core idea is to have a top-level state machines that corresponds to and "owns" the state of each respective page or section in the UI. Its states are designed to match the decision tree that is performed in the React component hierarchy about which layout is applied and which components should be rendered.
|
||||
|
||||
> **Example:** The page should render an empty state when no log indices can be found, but render the log stream including the search bar when log indices are available.
|
||||
|
||||
That means that branches to render one component or the other should ideally be performed based on the result of the respective `pageState.matches()` call (more specific examples later in the section about connecting components).
|
||||
|
||||
In addition to just representing the UI decision tree, the purpose of the page state machine is also to invoke service state machines, that perform side-effects like data loading or state synchronization with other parts of Kibana.
|
||||
|
||||
> **Example:** The `LogView` service state machine resolves the log view and notifies about its availability via special notification events.
|
||||
|
||||
> **Example:** The `LogStreamQuery` service state machine interacts with Kibana's `querystring`, `filterManager` and URL state facilities and notifies the page state machine about changes.
|
||||
|
||||
## Interpreting and providing UI state machines
|
||||
|
||||
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.:
|
||||
|
||||
```typescript
|
||||
export const useLogStreamPageState = ({
|
||||
kibanaQuerySettings,
|
||||
logViewStateNotifications,
|
||||
queryStringService,
|
||||
toastsService,
|
||||
filterManagerService,
|
||||
urlStateStorage,
|
||||
useDevTools = isDevMode(),
|
||||
}: {
|
||||
useDevTools?: boolean;
|
||||
} & LogStreamPageStateMachineDependencies) => {
|
||||
const logStreamPageStateService = useInterpret(
|
||||
() =>
|
||||
createLogStreamPageStateMachine({
|
||||
kibanaQuerySettings,
|
||||
logViewStateNotifications,
|
||||
queryStringService,
|
||||
toastsService,
|
||||
filterManagerService,
|
||||
urlStateStorage,
|
||||
}),
|
||||
{ devTools: useDevTools }
|
||||
);
|
||||
|
||||
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.
|
||||
|
||||
## Consuming UI state machines
|
||||
|
||||
So we want to translate the UI layout decision tree that the page state machine represents into a React hierarchy. At the same time we want to keep the individual components on the page independent of the specific page structure so they can be re-used in different contexts.
|
||||
|
||||
> **Example:** The component that renders the log stream is used in the log stream page of the Logs UI, but also in a dashboard embeddable and a shared React component. These "parent components" each have similar, but not identical state structures.
|
||||
|
||||
The redux community has solved that problem using the "connect" pattern, which we should be able to apply with a few small modifications as well. At the core the idea is to leave the presentational components decoupled from a specific page-wide state structure and just have them consume the relevant data and callbacks as props.
|
||||
|
||||
> **Example:** The component that renders the log stream receives log entries and scroll position reporting callbacks as props instead of accessing page-specific state from the context.
|
||||
|
||||
In order to take advantage of the fact that the page layout decisions are already encoded in the page state machine itself, we can use a three-layer component structure:
|
||||
|
||||
- **Presentational components** receive detailed domain-specific data and callbacks as props
|
||||
- **"For state" components** receive a page state machine state (and if needed a "send"/"dispatch" callback) as props
|
||||
- **"Connected" components** access the page state machine from context
|
||||
|
||||
This layering preserves several desirable properties:
|
||||
|
||||
- Presentational components can be re-used on different pages and tested in unit-tests and storybooks by just passing the respective props.
|
||||
- "For state" components are specific to the page state and can match states to branch into different page layouts. They can be tested in unit-tests and storybooks by just passing the corresponding page state data structures.
|
||||
- "Connected" components know how to gain access to the page state machine from the context and subscribe to their changes using `useActor`. They each work with a specific page state machine provider. They're harder to test, but very slim and of low complexity.
|
||||
|
||||
Assuming the UI state machine is provided as shown in the earlier section, the consumption could look like this:
|
||||
|
||||
**A connected component:**
|
||||
```typescript
|
||||
export const ConnectedStreamPageContent: React.FC = () => {
|
||||
const logStreamPageStateService = useLogStreamPageStateContext();
|
||||
|
||||
const [logStreamPageState] = useActor(logStreamPageStateService);
|
||||
|
||||
return <StreamPageContentForState logStreamPageState={logStreamPageState} />;
|
||||
};
|
||||
```
|
||||
|
||||
**A "for state" layout component:**
|
||||
```typescript
|
||||
export const StreamPageContentForState: React.FC<{ logStreamPageState: LogStreamPageState }> = ({
|
||||
logStreamPageState, // <-- this could be any state of the page state machine
|
||||
}) => {
|
||||
if (logStreamPageState.matches('uninitialized') || logStreamPageState.matches('loadingLogView')) {
|
||||
return <SourceLoadingPage />;
|
||||
} else if (logStreamPageState.matches('loadingLogViewFailed')) {
|
||||
return <ConnectedLogViewErrorPage />;
|
||||
} else if (logStreamPageState.matches('missingLogViewIndices')) {
|
||||
return <StreamPageMissingIndicesContent />;
|
||||
} else if (logStreamPageState.matches({ hasLogViewIndices: 'initialized' })) {
|
||||
return ( // <-- here the matching has narrowed the state to `hasLogViewIndices.initialized`
|
||||
<LogStreamPageContentProviders logStreamPageState={logStreamPageState}>
|
||||
<StreamPageLogsContentForState logStreamPageState={logStreamPageState} />
|
||||
</LogStreamPageContentProviders>
|
||||
);
|
||||
} else {
|
||||
return <InvalidStateCallout state={logStreamPageState} />;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
**A "for state" content component**
|
||||
```typescript
|
||||
type InitializedLogStreamPageState = MatchedStateFromActor<
|
||||
LogStreamPageActorRef,
|
||||
{ hasLogViewIndices: 'initialized' }
|
||||
>;
|
||||
|
||||
export const StreamPageLogsContentForState = React.memo<{
|
||||
logStreamPageState: InitializedLogStreamPageState; // <-- this expects to be rendered in a specific state and the compiler will enforce that
|
||||
}>(({ logStreamPageState }) => {
|
||||
const {
|
||||
context: { parsedQuery },
|
||||
} = logStreamPageState;
|
||||
|
||||
return <StreamPageLogsContent filterQuery={parsedQuery} />;
|
||||
});
|
||||
```
|
|
@ -1,149 +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 { useMemo, useEffect, useCallback, useState } from 'react';
|
||||
import { merge, of } from 'rxjs';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { buildEsQuery, DataViewBase, Query, AggregateQuery, isOfQueryType } from '@kbn/es-query';
|
||||
import createContainer from 'constate';
|
||||
import { useKibanaQuerySettings } from '../../../utils/use_kibana_query_settings';
|
||||
import { BuiltEsQuery } from '../log_stream';
|
||||
import { useKibanaContextForPlugin } from '../../../hooks/use_kibana';
|
||||
import { useSubscription } from '../../../utils/use_observable';
|
||||
import { UnsupportedLanguageError, QueryParsingError } from './errors';
|
||||
|
||||
interface ILogFilterState {
|
||||
filterQuery: {
|
||||
parsedQuery: BuiltEsQuery;
|
||||
serializedQuery: string;
|
||||
originalQuery: Query;
|
||||
} | null;
|
||||
queryStringQuery: Query | AggregateQuery | null;
|
||||
validationError: Error | null;
|
||||
}
|
||||
|
||||
export const DEFAULT_QUERY = {
|
||||
language: 'kuery',
|
||||
query: '',
|
||||
};
|
||||
|
||||
const INITIAL_LOG_FILTER_STATE = {
|
||||
filterQuery: null,
|
||||
queryStringQuery: null,
|
||||
validationError: null,
|
||||
};
|
||||
|
||||
// Error toasts
|
||||
export const errorToastTitle = i18n.translate(
|
||||
'xpack.infra.logsPage.toolbar.logFilterErrorToastTitle',
|
||||
{
|
||||
defaultMessage: 'Log filter error',
|
||||
}
|
||||
);
|
||||
|
||||
const unsupportedLanguageError = i18n.translate(
|
||||
'xpack.infra.logsPage.toolbar.logFilterUnsupportedLanguageError',
|
||||
{
|
||||
defaultMessage: 'SQL is not supported',
|
||||
}
|
||||
);
|
||||
|
||||
export const useLogFilterState = ({ dataView }: { dataView?: DataViewBase }) => {
|
||||
const {
|
||||
notifications: { toasts },
|
||||
data: {
|
||||
query: { queryString },
|
||||
},
|
||||
} = useKibanaContextForPlugin().services;
|
||||
|
||||
const kibanaQuerySettings = useKibanaQuerySettings();
|
||||
|
||||
const [logFilterState, setLogFilterState] = useState<ILogFilterState>(INITIAL_LOG_FILTER_STATE);
|
||||
|
||||
useEffect(() => {
|
||||
const handleValidationError = (error: Error) => {
|
||||
if (error instanceof UnsupportedLanguageError) {
|
||||
toasts.addError(error, { title: errorToastTitle });
|
||||
queryString.setQuery(DEFAULT_QUERY);
|
||||
} else if (error instanceof QueryParsingError) {
|
||||
toasts.addError(error, { title: errorToastTitle });
|
||||
}
|
||||
};
|
||||
|
||||
if (logFilterState.validationError) {
|
||||
handleValidationError(logFilterState.validationError);
|
||||
}
|
||||
}, [logFilterState.validationError, queryString, toasts]);
|
||||
|
||||
const parseQuery = useCallback(
|
||||
(filterQuery: Query) => {
|
||||
return buildEsQuery(dataView, filterQuery, [], kibanaQuerySettings);
|
||||
},
|
||||
[dataView, kibanaQuerySettings]
|
||||
);
|
||||
|
||||
const getNewLogFilterState = useCallback(
|
||||
(newQuery: Query | AggregateQuery) =>
|
||||
(previousLogFilterState: ILogFilterState): ILogFilterState => {
|
||||
try {
|
||||
if (!isOfQueryType(newQuery)) {
|
||||
throw new UnsupportedLanguageError(unsupportedLanguageError);
|
||||
}
|
||||
try {
|
||||
const parsedQuery = parseQuery(newQuery);
|
||||
return {
|
||||
filterQuery: {
|
||||
parsedQuery,
|
||||
serializedQuery: JSON.stringify(parsedQuery),
|
||||
originalQuery: newQuery,
|
||||
},
|
||||
queryStringQuery: newQuery,
|
||||
validationError: null,
|
||||
};
|
||||
} catch (error) {
|
||||
throw new QueryParsingError(error);
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
...previousLogFilterState,
|
||||
queryStringQuery: newQuery,
|
||||
validationError: error,
|
||||
};
|
||||
}
|
||||
},
|
||||
[parseQuery]
|
||||
);
|
||||
|
||||
useSubscription(
|
||||
useMemo(() => {
|
||||
return merge(of(undefined), queryString.getUpdates$()); // NOTE: getUpdates$ uses skip(1) so we do this to ensure an initial emit of a value.
|
||||
}, [queryString]),
|
||||
useMemo(() => {
|
||||
return {
|
||||
next: () => {
|
||||
setLogFilterState(getNewLogFilterState(queryString.getQuery()));
|
||||
},
|
||||
};
|
||||
}, [getNewLogFilterState, queryString])
|
||||
);
|
||||
|
||||
// NOTE: If the dataView changes the query will need to be reparsed and the filter regenerated.
|
||||
useEffect(() => {
|
||||
if (dataView) {
|
||||
setLogFilterState(getNewLogFilterState(queryString.getQuery()));
|
||||
}
|
||||
}, [dataView, getNewLogFilterState, queryString]);
|
||||
|
||||
return {
|
||||
queryStringQuery: logFilterState.queryStringQuery, // NOTE: Query String Manager query.
|
||||
filterQuery: logFilterState.filterQuery, // NOTE: Valid and syntactically correct query applied to requests etc.
|
||||
validationError: logFilterState.validationError,
|
||||
};
|
||||
};
|
||||
|
||||
export const [LogFilterStateProvider, useLogFilterStateContext] =
|
||||
createContainer(useLogFilterState);
|
|
@ -1,70 +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 * as rt from 'io-ts';
|
||||
import React from 'react';
|
||||
import { Query } from '@kbn/es-query';
|
||||
import { replaceStateKeyInQueryString, UrlStateContainer } from '../../../utils/url_state';
|
||||
import { useLogFilterStateContext, DEFAULT_QUERY } from './log_filter_state';
|
||||
import { useKibanaContextForPlugin } from '../../../hooks/use_kibana';
|
||||
|
||||
export const WithLogFilterUrlState: React.FC = () => {
|
||||
const {
|
||||
data: {
|
||||
query: { queryString },
|
||||
},
|
||||
} = useKibanaContextForPlugin().services;
|
||||
|
||||
const { queryStringQuery } = useLogFilterStateContext();
|
||||
|
||||
return (
|
||||
<UrlStateContainer
|
||||
urlState={queryStringQuery}
|
||||
urlStateKey="logFilter"
|
||||
mapToUrlState={mapToFilterQuery}
|
||||
onChange={(urlState) => {
|
||||
if (urlState) {
|
||||
queryString.setQuery(urlState);
|
||||
}
|
||||
}}
|
||||
onInitialize={(urlState) => {
|
||||
if (urlState) {
|
||||
queryString.setQuery(urlState);
|
||||
} else {
|
||||
queryString.setQuery(DEFAULT_QUERY);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const mapToFilterQuery = (value: any): Query | undefined => {
|
||||
if (legacyFilterQueryUrlStateRT.is(value)) {
|
||||
// migrate old url state
|
||||
return {
|
||||
language: value.kind,
|
||||
query: value.expression,
|
||||
};
|
||||
} else if (filterQueryUrlStateRT.is(value)) {
|
||||
return value;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
export const replaceLogFilterInQueryString = (query: Query) =>
|
||||
replaceStateKeyInQueryString<Query>('logFilter', query);
|
||||
|
||||
const filterQueryUrlStateRT = rt.type({
|
||||
language: rt.string,
|
||||
query: rt.string,
|
||||
});
|
||||
|
||||
const legacyFilterQueryUrlStateRT = rt.type({
|
||||
kind: rt.literal('kuery'),
|
||||
expression: rt.string,
|
||||
});
|
|
@ -5,10 +5,12 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useSelector } from '@xstate/react';
|
||||
import stringify from 'json-stable-stringify';
|
||||
import useThrottle from 'react-use/lib/useThrottle';
|
||||
import { useLogViewContext } from '../../../hooks/use_log_view';
|
||||
import { useLogStreamPageStateContext } from '../../../observability_logs/log_stream_page/state';
|
||||
import { RendererFunction } from '../../../utils/typed_react';
|
||||
import { useLogFilterStateContext } from '../log_filter';
|
||||
import { useLogPositionStateContext } from '../log_position';
|
||||
import { LogSummaryBuckets, useLogSummary } from './log_summary';
|
||||
|
||||
|
@ -24,7 +26,11 @@ export const WithSummary = ({
|
|||
}>;
|
||||
}) => {
|
||||
const { logViewId } = useLogViewContext();
|
||||
const { filterQuery } = useLogFilterStateContext();
|
||||
const serializedParsedQuery = useSelector(useLogStreamPageStateContext(), (logStreamPageState) =>
|
||||
logStreamPageState.matches({ hasLogViewIndices: 'initialized' })
|
||||
? stringify(logStreamPageState.context.parsedQuery)
|
||||
: null
|
||||
);
|
||||
const { startTimestamp, endTimestamp } = useLogPositionStateContext();
|
||||
|
||||
// Keep it reasonably updated for the `now` case, but don't reload all the time when the user scrolls
|
||||
|
@ -35,7 +41,7 @@ export const WithSummary = ({
|
|||
logViewId,
|
||||
throttledStartTimestamp,
|
||||
throttledEndTimestamp,
|
||||
filterQuery?.serializedQuery ?? null
|
||||
serializedParsedQuery
|
||||
);
|
||||
|
||||
return children({ buckets, start, end });
|
||||
|
|
|
@ -5,11 +5,4 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export {
|
||||
createLogStreamPageStateMachine,
|
||||
LogStreamPageStateProvider,
|
||||
useLogStreamPageState,
|
||||
useLogStreamPageStateContext,
|
||||
type LogStreamPageContext,
|
||||
type LogStreamPageEvent,
|
||||
} from './src';
|
||||
export * from './src';
|
||||
|
|
|
@ -6,5 +6,6 @@
|
|||
*/
|
||||
|
||||
export * from './provider';
|
||||
export * from './selectors';
|
||||
export * from './state_machine';
|
||||
export * from './types';
|
||||
|
|
|
@ -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 { InvokeCreator, Receiver } from 'xstate';
|
||||
import { ParsedQuery } from '../../../log_stream_query_state';
|
||||
import { LogStreamPageContext, LogStreamPageEvent } from './types';
|
||||
|
||||
export const waitForInitialParameters =
|
||||
(): InvokeCreator<LogStreamPageContext, LogStreamPageEvent> =>
|
||||
(_context, _event) =>
|
||||
(send, onEvent: Receiver<LogStreamPageEvent>) => {
|
||||
// constituents of the set of initial parameters
|
||||
let latestValidQuery: ParsedQuery | undefined;
|
||||
|
||||
onEvent((event) => {
|
||||
switch (event.type) {
|
||||
// event types that deliver the parameters
|
||||
case 'VALID_QUERY_CHANGED':
|
||||
case 'INVALID_QUERY_CHANGED':
|
||||
latestValidQuery = event.parsedQuery;
|
||||
break;
|
||||
}
|
||||
|
||||
// if all constituents of the parameters have been delivered
|
||||
if (latestValidQuery != null) {
|
||||
send({
|
||||
type: 'RECEIVED_INITIAL_PARAMETERS',
|
||||
validatedQuery: latestValidQuery,
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
|
@ -8,20 +8,31 @@
|
|||
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';
|
||||
import {
|
||||
createLogStreamPageStateMachine,
|
||||
type LogStreamPageStateMachineDependencies,
|
||||
} from './state_machine';
|
||||
|
||||
export const useLogStreamPageState = ({
|
||||
kibanaQuerySettings,
|
||||
logViewStateNotifications,
|
||||
queryStringService,
|
||||
toastsService,
|
||||
filterManagerService,
|
||||
urlStateStorage,
|
||||
useDevTools = isDevMode(),
|
||||
}: {
|
||||
logViewStateNotifications: LogViewNotificationChannel;
|
||||
useDevTools?: boolean;
|
||||
}) => {
|
||||
} & LogStreamPageStateMachineDependencies) => {
|
||||
const logStreamPageStateService = useInterpret(
|
||||
() =>
|
||||
createLogStreamPageStateMachine({
|
||||
kibanaQuerySettings,
|
||||
logViewStateNotifications,
|
||||
queryStringService,
|
||||
toastsService,
|
||||
filterManagerService,
|
||||
urlStateStorage,
|
||||
}),
|
||||
{ devTools: useDevTools }
|
||||
);
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* 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 { LogStreamQueryActorRef } from '../../../log_stream_query_state';
|
||||
import { MatchedStateFromActor } from '../../../xstate_helpers';
|
||||
import { LogStreamPageActorRef } from './state_machine';
|
||||
|
||||
type LogStreamPageStateWithLogViewIndices =
|
||||
| MatchedStateFromActor<LogStreamPageActorRef, 'hasLogViewIndices'>
|
||||
| MatchedStateFromActor<LogStreamPageActorRef, { hasLogViewIndices: 'initialized' }>
|
||||
| MatchedStateFromActor<LogStreamPageActorRef, { hasLogViewIndices: 'uninitialized' }>;
|
||||
|
||||
export const selectLogStreamQueryChildService = (
|
||||
state: LogStreamPageStateWithLogViewIndices
|
||||
): LogStreamQueryActorRef => state.children.logStreamQuery;
|
|
@ -5,18 +5,25 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { assign, createMachine } from 'xstate';
|
||||
import { actions, ActorRefFrom, createMachine, EmittedFrom } from 'xstate';
|
||||
import {
|
||||
createLogStreamQueryStateMachine,
|
||||
LogStreamQueryStateMachineDependencies,
|
||||
} from '../../../log_stream_query_state';
|
||||
import type { LogViewNotificationChannel } from '../../../log_view_state';
|
||||
import { OmitDeprecatedState } from '../../../xstate_helpers';
|
||||
import { waitForInitialParameters } from './initial_parameters_service';
|
||||
import type {
|
||||
LogStreamPageContext,
|
||||
LogStreamPageContextWithLogView,
|
||||
LogStreamPageContextWithLogViewError,
|
||||
LogStreamPageContextWithQuery,
|
||||
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 */
|
||||
/** @xstate-layout N4IgpgJg5mDOIC5QBsD2UDKAXATmAhgLYAK+M2+WYAdAK4B2Alk1o-sowF6QDEAMgHkAggBEAkgDkA4gH1BsgGpiAogHUZGACpCASpuUiA2gAYAuolAAHVLEatU9CyAAeiALQAmAJzUPHgGwArIEALAAcHmHhXsEhADQgAJ6IAQDM1IHGYQDs2ZnZxl5e-l6pAL5lCWiYuAQkZGAUVHRMLGwc3BD8wuLScgKKKuoAYkJifAYm5kgg1rb2jjOuCJ4hAIzUqZklHgX+xqlrkQnJCKlB1P4loYH+qV7hHoEVVejYeESk5FiUNAzMdnaXF4glEklk8hkSjUGgAqgBheHKAyTMxOOaAhxOZbZNbZajGXLBVIkrJrYInRBHYzUEIeVIhVJhNZZLws7IhF4garvOpfRo-Zr-NrsYFdUG9CEDKFDOGI5EiSZraZWGyYxagHEhEIEwkeI7Zc7GO6UhCZHzZML7MKBDwhQp0zmVblvWqfBpNGhofAQZhQPjoBSMMAAd26YL6kOhIzGEyMaJmGIW2JS4VpJTCWXp5K82Q8pvN1Et1tt9oedq5PLd9W+v2o3t99H9geDYYl4P6gxhGARSJR8ZVszVyaWiEZPnt-m8Ry8xjta38pqN1FK+uy+w8xhCgS8lddHxrArrDb9AagQdD4clnZl3d7CqVg6TjCxo4QISumy2MWNMWiAVNO58WMFkImMOc50CMI9xqA9+U9etUB9U8W1DYZ8EYZAQR6Dso1lLRdH0Ad0WHF8NRcdxvF8AJYgiKIwhiUIC1SDwMgNcC8g5O1nmdKs4I9QUaAAC3wWAzwvEMxHoX0AGM4BaAFWFFToeB0ZQkTEBQDBkSQxE0MQhD4GRiF0IQAFllH0HQMCmEj5jIlMEBnfxqAiYojjWVJCjnJcwnSIIrSuNZZ2CGJyl4-c+QEusRLE1DJOkxg5NgBSRQ6XgFEMsQRBkABFWFlB0ABNGR4QACSEaRUSfUjX01Kl-DyWk1giW0p3tElTTxfFwhCPYQIXXE1idV5YKi2tmli8TWyk2T5OFQFlN4SRMr4bK8oK4rSoqqriMTWryOWBdGtckCQMCEknn8MIl21FcWLxVJDUzfwWv8GDeXdCbhNE6bQ1mpL5MUoEVNW9b8sKkrysqqRqrs9VHMGwJmtagI7QOVICznFd1yebdMgutY1gqZ16FQCA4CcPjxqPKh4ZHeqVkiHwtl-XZjQOTzTTcNk2NtQ4-A5q0PureDBNSxb0ogemHLfOkurpahyXArIbTZIItxF-jvsQ5Cmz+kMZbqiiEF2DJQiuK0bVyH94iSRAmTCTZ3ICPMtj8HjRs+w8EJPfX4vQzDICNw7EGR5k7S8NJGrZNXAJ3IsnvtImt28aCIrGr7aZ+uLzxmxLkpDxHNxpC7dn2K404pe331YrwokNQ1GIF7ItZphCpvigHkolpSpaLt8jjTMv12NKd6+r04nlYgbDjxO0-KnNus4736u4LoG0rFAfGc8qIi0Cck1cCQ1K4LJ4iznS1zjzG0WOXn3xcIRhYFsf28-+jf4H2+zjaOw4XKbijt4YIWRbjZHjhaJ6T1cSwIxiTMoQA */
|
||||
createMachine<LogStreamPageContext, LogStreamPageEvent, LogStreamPageTypestate>(
|
||||
{
|
||||
context: initialContext,
|
||||
|
@ -77,15 +84,47 @@ export const createPureLogStreamPageStateMachine = (initialContext: LogStreamPag
|
|||
},
|
||||
hasLogViewIndices: {
|
||||
initial: 'uninitialized',
|
||||
|
||||
states: {
|
||||
uninitialized: {
|
||||
invoke: {
|
||||
src: 'waitForInitialParameters',
|
||||
id: 'waitForInitialParameters',
|
||||
},
|
||||
|
||||
on: {
|
||||
RECEIVED_ALL_PARAMETERS: {
|
||||
RECEIVED_INITIAL_PARAMETERS: {
|
||||
target: 'initialized',
|
||||
actions: 'storeQuery',
|
||||
},
|
||||
|
||||
VALID_QUERY_CHANGED: {
|
||||
target: 'uninitialized',
|
||||
internal: true,
|
||||
actions: 'forwardToInitialParameters',
|
||||
},
|
||||
|
||||
INVALID_QUERY_CHANGED: {
|
||||
target: 'uninitialized',
|
||||
internal: true,
|
||||
actions: 'forwardToInitialParameters',
|
||||
},
|
||||
},
|
||||
},
|
||||
initialized: {},
|
||||
initialized: {
|
||||
on: {
|
||||
VALID_QUERY_CHANGED: {
|
||||
target: 'initialized',
|
||||
internal: true,
|
||||
actions: 'storeQuery',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
invoke: {
|
||||
src: 'logStreamQuery',
|
||||
id: 'logStreamQuery',
|
||||
},
|
||||
},
|
||||
missingLogViewIndices: {},
|
||||
|
@ -93,12 +132,13 @@ export const createPureLogStreamPageStateMachine = (initialContext: LogStreamPag
|
|||
},
|
||||
{
|
||||
actions: {
|
||||
storeLogViewError: assign((_context, event) =>
|
||||
forwardToInitialParameters: actions.forwardTo('waitForInitialParameters'),
|
||||
storeLogViewError: actions.assign((_context, event) =>
|
||||
event.type === 'LOADING_LOG_VIEW_FAILED'
|
||||
? ({ logViewError: event.error } as LogStreamPageContextWithLogViewError)
|
||||
: {}
|
||||
),
|
||||
storeResolvedLogView: assign((_context, event) =>
|
||||
storeResolvedLogView: actions.assign((_context, event) =>
|
||||
event.type === 'LOADING_LOG_VIEW_SUCCEEDED'
|
||||
? ({
|
||||
logViewStatus: event.status,
|
||||
|
@ -106,6 +146,17 @@ export const createPureLogStreamPageStateMachine = (initialContext: LogStreamPag
|
|||
} as LogStreamPageContextWithLogView)
|
||||
: {}
|
||||
),
|
||||
storeQuery: actions.assign((_context, event) =>
|
||||
event.type === 'RECEIVED_INITIAL_PARAMETERS'
|
||||
? ({
|
||||
parsedQuery: event.validatedQuery,
|
||||
} as LogStreamPageContextWithQuery)
|
||||
: event.type === 'VALID_QUERY_CHANGED'
|
||||
? ({
|
||||
parsedQuery: event.parsedQuery,
|
||||
} as LogStreamPageContextWithQuery)
|
||||
: {}
|
||||
),
|
||||
},
|
||||
guards: {
|
||||
hasLogViewIndices: (_context, event) =>
|
||||
|
@ -115,13 +166,43 @@ export const createPureLogStreamPageStateMachine = (initialContext: LogStreamPag
|
|||
}
|
||||
);
|
||||
|
||||
export const createLogStreamPageStateMachine = ({
|
||||
logViewStateNotifications,
|
||||
}: {
|
||||
export type LogStreamPageStateMachine = ReturnType<typeof createPureLogStreamPageStateMachine>;
|
||||
export type LogStreamPageActorRef = OmitDeprecatedState<ActorRefFrom<LogStreamPageStateMachine>>;
|
||||
export type LogStreamPageState = EmittedFrom<LogStreamPageActorRef>;
|
||||
|
||||
export type LogStreamPageStateMachineDependencies = {
|
||||
logViewStateNotifications: LogViewNotificationChannel;
|
||||
}) =>
|
||||
} & LogStreamQueryStateMachineDependencies;
|
||||
|
||||
export const createLogStreamPageStateMachine = ({
|
||||
kibanaQuerySettings,
|
||||
logViewStateNotifications,
|
||||
queryStringService,
|
||||
toastsService,
|
||||
filterManagerService,
|
||||
urlStateStorage,
|
||||
}: LogStreamPageStateMachineDependencies) =>
|
||||
createPureLogStreamPageStateMachine().withConfig({
|
||||
services: {
|
||||
logViewNotifications: () => logViewStateNotifications.createService(),
|
||||
logStreamQuery: (context) => {
|
||||
if (!('resolvedLogView' in context)) {
|
||||
throw new Error('Failed to spawn log stream query service: no LogView in context');
|
||||
}
|
||||
|
||||
return createLogStreamQueryStateMachine(
|
||||
{
|
||||
dataViews: [context.resolvedLogView.dataViewReference],
|
||||
},
|
||||
{
|
||||
kibanaQuerySettings,
|
||||
queryStringService,
|
||||
toastsService,
|
||||
filterManagerService,
|
||||
urlStateStorage,
|
||||
}
|
||||
);
|
||||
},
|
||||
waitForInitialParameters: waitForInitialParameters(),
|
||||
},
|
||||
});
|
||||
|
|
|
@ -6,6 +6,8 @@
|
|||
*/
|
||||
|
||||
import type { LogViewStatus } from '../../../../../common/log_views';
|
||||
import { ParsedQuery } from '../../../log_stream_query_state';
|
||||
import { LogStreamQueryNotificationEvent } from '../../../log_stream_query_state/src/notifications';
|
||||
import type {
|
||||
LogViewContextWithError,
|
||||
LogViewContextWithResolvedLogView,
|
||||
|
@ -14,8 +16,10 @@ import type {
|
|||
|
||||
export type LogStreamPageEvent =
|
||||
| LogViewNotificationEvent
|
||||
| LogStreamQueryNotificationEvent
|
||||
| {
|
||||
type: 'RECEIVED_ALL_PARAMETERS';
|
||||
type: 'RECEIVED_INITIAL_PARAMETERS';
|
||||
validatedQuery: ParsedQuery;
|
||||
};
|
||||
|
||||
export interface LogStreamPageContextWithLogView {
|
||||
|
@ -27,6 +31,10 @@ export interface LogStreamPageContextWithLogViewError {
|
|||
logViewError: LogViewContextWithError['error'];
|
||||
}
|
||||
|
||||
export interface LogStreamPageContextWithQuery {
|
||||
parsedQuery: ParsedQuery;
|
||||
}
|
||||
|
||||
export type LogStreamPageTypestate =
|
||||
| {
|
||||
value: 'uninitialized';
|
||||
|
@ -44,6 +52,14 @@ export type LogStreamPageTypestate =
|
|||
value: 'hasLogViewIndices';
|
||||
context: LogStreamPageContextWithLogView;
|
||||
}
|
||||
| {
|
||||
value: { hasLogViewIndices: 'uninitialized' };
|
||||
context: LogStreamPageContextWithLogView;
|
||||
}
|
||||
| {
|
||||
value: { hasLogViewIndices: 'initialized' };
|
||||
context: LogStreamPageContextWithLogView & LogStreamPageContextWithQuery;
|
||||
}
|
||||
| {
|
||||
value: 'missingLogViewIndices';
|
||||
context: LogStreamPageContextWithLogView;
|
||||
|
|
|
@ -5,5 +5,4 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export * from './log_filter_state';
|
||||
export * from './with_log_filter_url_state';
|
||||
export * from './src';
|
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* 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 './errors';
|
||||
export * from './state_machine';
|
||||
export * from './types';
|
||||
export * from './url_state_storage_service';
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* 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 { LogStreamQueryContext, ParsedQuery } from './types';
|
||||
|
||||
export type LogStreamQueryNotificationEvent =
|
||||
| {
|
||||
type: 'VALID_QUERY_CHANGED';
|
||||
parsedQuery: ParsedQuery;
|
||||
}
|
||||
| {
|
||||
type: 'INVALID_QUERY_CHANGED';
|
||||
parsedQuery: ParsedQuery;
|
||||
error: Error;
|
||||
};
|
||||
|
||||
export const logStreamQueryNotificationEventSelectors = {
|
||||
validQueryChanged: (context: LogStreamQueryContext) =>
|
||||
'parsedQuery' in context
|
||||
? ({
|
||||
type: 'VALID_QUERY_CHANGED',
|
||||
parsedQuery: context.parsedQuery,
|
||||
} as LogStreamQueryNotificationEvent)
|
||||
: undefined,
|
||||
invalidQueryChanged: (context: LogStreamQueryContext) =>
|
||||
'validationError' in context
|
||||
? ({
|
||||
type: 'INVALID_QUERY_CHANGED',
|
||||
parsedQuery: context.parsedQuery,
|
||||
error: context.validationError,
|
||||
} as LogStreamQueryNotificationEvent)
|
||||
: undefined,
|
||||
};
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* 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 { FilterManager, QueryStringContract } from '@kbn/data-plugin/public';
|
||||
import { map } from 'rxjs/operators';
|
||||
import type { InvokeCreator } from 'xstate';
|
||||
import type { LogStreamQueryContext, LogStreamQueryEvent } from './types';
|
||||
|
||||
export const subscribeToQuerySearchBarChanges =
|
||||
({
|
||||
queryStringService,
|
||||
}: {
|
||||
queryStringService: QueryStringContract;
|
||||
}): InvokeCreator<LogStreamQueryContext, LogStreamQueryEvent> =>
|
||||
(context) =>
|
||||
queryStringService.getUpdates$().pipe(
|
||||
map(() => queryStringService.getQuery()),
|
||||
map((query): LogStreamQueryEvent => {
|
||||
return {
|
||||
type: 'QUERY_FROM_SEARCH_BAR_CHANGED',
|
||||
query,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
export const updateQueryInSearchBar =
|
||||
({ queryStringService }: { queryStringService: QueryStringContract }) =>
|
||||
(context: LogStreamQueryContext, event: LogStreamQueryEvent) => {
|
||||
if ('query' in context) {
|
||||
queryStringService.setQuery(context.query);
|
||||
}
|
||||
};
|
||||
|
||||
export const subscribeToFilterSearchBarChanges =
|
||||
({
|
||||
filterManagerService,
|
||||
}: {
|
||||
filterManagerService: FilterManager;
|
||||
}): InvokeCreator<LogStreamQueryContext, LogStreamQueryEvent> =>
|
||||
(context) =>
|
||||
filterManagerService.getUpdates$().pipe(
|
||||
map(() => filterManagerService.getFilters()),
|
||||
map((filters): LogStreamQueryEvent => {
|
||||
return {
|
||||
type: 'FILTERS_FROM_SEARCH_BAR_CHANGED',
|
||||
filters,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
export const updateFiltersInSearchBar =
|
||||
({ filterManagerService }: { filterManagerService: FilterManager }) =>
|
||||
(context: LogStreamQueryContext, event: LogStreamQueryEvent) => {
|
||||
if ('filters' in context) {
|
||||
filterManagerService.setFilters(context.filters);
|
||||
}
|
||||
};
|
|
@ -0,0 +1,232 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { IToasts } from '@kbn/core-notifications-browser';
|
||||
import type { FilterManager, QueryStringContract } from '@kbn/data-plugin/public';
|
||||
import { EsQueryConfig } from '@kbn/es-query';
|
||||
import { IKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public';
|
||||
import { actions, ActorRefFrom, createMachine, SpecialTargets } from 'xstate';
|
||||
import { OmitDeprecatedState, sendIfDefined } from '../../xstate_helpers';
|
||||
import { logStreamQueryNotificationEventSelectors } from './notifications';
|
||||
import {
|
||||
subscribeToFilterSearchBarChanges,
|
||||
subscribeToQuerySearchBarChanges,
|
||||
updateFiltersInSearchBar,
|
||||
updateQueryInSearchBar,
|
||||
} from './search_bar_state_service';
|
||||
import type {
|
||||
LogStreamQueryContext,
|
||||
LogStreamQueryContextWithDataViews,
|
||||
LogStreamQueryContextWithFilters,
|
||||
LogStreamQueryContextWithParsedQuery,
|
||||
LogStreamQueryContextWithQuery,
|
||||
LogStreamQueryContextWithValidationError,
|
||||
LogStreamQueryEvent,
|
||||
LogStreamQueryTypestate,
|
||||
} from './types';
|
||||
import {
|
||||
initializeFromUrl,
|
||||
safeDefaultParsedQuery,
|
||||
updateFiltersInUrl,
|
||||
updateQueryInUrl,
|
||||
} from './url_state_storage_service';
|
||||
import { showValidationErrorToast, validateQuery } from './validate_query_service';
|
||||
|
||||
export const createPureLogStreamQueryStateMachine = (
|
||||
initialContext: LogStreamQueryContextWithDataViews
|
||||
) =>
|
||||
/** @xstate-layout N4IgpgJg5mDOIC5QEUCuYBOBPAdKgdgJZEAuhAhgDaEBekAxANoAMAuoqAA4D2shZ3fBxAAPRAFoATAHZJOAMwBWJQBYAjNIBszAJwAOTZIA0ILBMnyca+Wp2bFenfL1b5zTQF8PJtJlzF+CmoaYigAMQxuAFsAVQxKegBJADlEgBVEgEEAGUSALQBRABEAfTCAJQB5AFkSmPLslnYkEB4+ASEWsQRxeRU5PXdNA001RX1tRRMzBBkdHHVmFWZpHTVJC2ZFFS8fdGwcAAtyWF9semQYgvKATTKq2oBlAszygGEACRKAIVeSz8yyQA4sUmsI2oFBMJupIVNIcLoVHpBoppLJFMZTIh7DgdMsnNINHZlip5LsQGdcMdTvssPQwolsmlro97jUSs9Xp8fn8AcDQWxwbxIZ1QN1tHICWoXJJNDoMfJ5NNsYpcfj5ITVpoSZ5vBTaUcTpT6EVMmlMiUAGqJAoAdVZfJBRTBLQhHWhiDscllKwsKnUOmYajUyoQqysmmkKh00mYbijjkU5MphppfhwGDAADcqIQIOQyPgoPRLTlEqaMpVkmVMoyBc0uML3V1PZHcfK1P6UXY0aG3JZ5Pp1WpRgZJEm9SnqSnMznqPnC8XS7kK4kqxyYm83gVivWhe1CFCWwhB8wFjJRvJJINZZHpH24woh7obKPDBO9um53mC6ES2XV3XR5N23XdnUFV0m0PUVRAkNZcVkUY8UUQxmDjPRQ39NQcCjNENUkWwVEUJZpGTA1vwXP9l3LM012rMJa2yPdIIPI8xQkaRLB0CZiO2aQ9EkJxQxQ1UBKUZg9HWUl1FI8l8G4CA4GESl9xFD0elPHBBk0YYdLGCYtlDKRtARcYtivFw0S0Mj0wIAIyFzOgIFU5t2J6QS9BwDYVmlHRYR0rZNCMjRsJHST-RE8dOJ2ScDXsoJaFCCJojiSgXOg9Ten6LzI1GawrKvWFQ07SxZE41Y1jsawP31dNp1pdK2NghBbHmRFkS2NFx0xGZxA0ORFDMyMPNWcYbIOeqv1zZyWLU48NWwyqtTcXQ9FJTDlgQ-pkW1HbJPGqkjTi-AKMamDuj8lQEWlBwVAlWUrw2s8Y22gwkQMfbYrqo701nabfyLM71NjbCBOWCTfLhdxQ1hM9ZScPQNW4xQR2sA6cAogGoCB49eiULTL1RPQMUDQcpixBB-WeqNx2lIlEdkrwgA */
|
||||
createMachine<LogStreamQueryContext, LogStreamQueryEvent, LogStreamQueryTypestate>(
|
||||
{
|
||||
context: initialContext,
|
||||
preserveActionOrder: true,
|
||||
predictableActionArguments: true,
|
||||
id: 'Query',
|
||||
initial: 'uninitialized',
|
||||
states: {
|
||||
uninitialized: {
|
||||
always: {
|
||||
target: 'initializingFromUrl',
|
||||
},
|
||||
},
|
||||
|
||||
initializingFromUrl: {
|
||||
on: {
|
||||
INITIALIZED_FROM_URL: {
|
||||
target: 'validating',
|
||||
actions: ['storeQuery', 'storeFilters'],
|
||||
},
|
||||
},
|
||||
|
||||
invoke: {
|
||||
src: 'initializeFromUrl',
|
||||
},
|
||||
},
|
||||
|
||||
hasQuery: {
|
||||
entry: ['updateQueryInUrl', 'updateQueryInSearchBar', 'updateFiltersInSearchBar'],
|
||||
invoke: [
|
||||
{
|
||||
src: 'subscribeToQuerySearchBarChanges',
|
||||
},
|
||||
{
|
||||
src: 'subscribeToFilterSearchBarChanges',
|
||||
},
|
||||
],
|
||||
initial: 'revalidating',
|
||||
states: {
|
||||
valid: {
|
||||
entry: 'notifyValidQueryChanged',
|
||||
},
|
||||
invalid: {
|
||||
entry: 'notifyInvalidQueryChanged',
|
||||
},
|
||||
revalidating: {
|
||||
invoke: {
|
||||
src: 'validateQuery',
|
||||
},
|
||||
on: {
|
||||
VALIDATION_FAILED: {
|
||||
target: 'invalid',
|
||||
actions: ['storeValidationError', 'showValidationErrorToast'],
|
||||
},
|
||||
VALIDATION_SUCCEEDED: {
|
||||
target: 'valid',
|
||||
actions: ['clearValidationError', 'storeParsedQuery'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
on: {
|
||||
QUERY_FROM_SEARCH_BAR_CHANGED: {
|
||||
target: '.revalidating',
|
||||
actions: ['storeQuery', 'updateQueryInUrl'],
|
||||
},
|
||||
|
||||
FILTERS_FROM_SEARCH_BAR_CHANGED: {
|
||||
target: '.revalidating',
|
||||
actions: ['storeFilters', 'updateFiltersInUrl'],
|
||||
},
|
||||
|
||||
DATA_VIEWS_CHANGED: {
|
||||
target: '.revalidating',
|
||||
actions: 'storeDataViews',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
validating: {
|
||||
invoke: {
|
||||
src: 'validateQuery',
|
||||
},
|
||||
|
||||
on: {
|
||||
VALIDATION_SUCCEEDED: {
|
||||
target: 'hasQuery.valid',
|
||||
actions: 'storeParsedQuery',
|
||||
},
|
||||
|
||||
VALIDATION_FAILED: {
|
||||
target: 'hasQuery.invalid',
|
||||
actions: [
|
||||
'storeValidationError',
|
||||
'storeDefaultParsedQuery',
|
||||
'showValidationErrorToast',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
actions: {
|
||||
notifyInvalidQueryChanged: actions.pure(() => undefined),
|
||||
notifyValidQueryChanged: actions.pure(() => undefined),
|
||||
storeQuery: actions.assign((_context, event) => {
|
||||
return 'query' in event ? ({ query: event.query } as LogStreamQueryContextWithQuery) : {};
|
||||
}),
|
||||
storeFilters: actions.assign((_context, event) =>
|
||||
'filters' in event ? ({ filters: event.filters } as LogStreamQueryContextWithFilters) : {}
|
||||
),
|
||||
storeDataViews: actions.assign((_context, event) =>
|
||||
'dataViews' in event
|
||||
? ({ dataViews: event.dataViews } as LogStreamQueryContextWithDataViews)
|
||||
: {}
|
||||
),
|
||||
storeValidationError: actions.assign((_context, event) =>
|
||||
'error' in event
|
||||
? ({
|
||||
validationError: event.error,
|
||||
} as LogStreamQueryContextWithQuery & LogStreamQueryContextWithValidationError)
|
||||
: {}
|
||||
),
|
||||
storeDefaultParsedQuery: actions.assign(
|
||||
(_context, _event) =>
|
||||
({ parsedQuery: safeDefaultParsedQuery } as LogStreamQueryContextWithParsedQuery)
|
||||
),
|
||||
storeParsedQuery: actions.assign((_context, event) =>
|
||||
'parsedQuery' in event
|
||||
? ({ parsedQuery: event.parsedQuery } as LogStreamQueryContextWithParsedQuery)
|
||||
: {}
|
||||
),
|
||||
clearValidationError: actions.assign(
|
||||
(_context, _event) =>
|
||||
({ validationError: undefined } as Omit<
|
||||
LogStreamQueryContextWithValidationError,
|
||||
'validationError'
|
||||
>)
|
||||
),
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export interface LogStreamQueryStateMachineDependencies {
|
||||
kibanaQuerySettings: EsQueryConfig;
|
||||
queryStringService: QueryStringContract;
|
||||
filterManagerService: FilterManager;
|
||||
urlStateStorage: IKbnUrlStateStorage;
|
||||
toastsService: IToasts;
|
||||
}
|
||||
|
||||
export const createLogStreamQueryStateMachine = (
|
||||
initialContext: LogStreamQueryContextWithDataViews,
|
||||
{
|
||||
kibanaQuerySettings,
|
||||
queryStringService,
|
||||
toastsService,
|
||||
filterManagerService,
|
||||
urlStateStorage,
|
||||
}: LogStreamQueryStateMachineDependencies
|
||||
) =>
|
||||
createPureLogStreamQueryStateMachine(initialContext).withConfig({
|
||||
actions: {
|
||||
notifyInvalidQueryChanged: sendIfDefined(SpecialTargets.Parent)(
|
||||
logStreamQueryNotificationEventSelectors.invalidQueryChanged
|
||||
),
|
||||
notifyValidQueryChanged: sendIfDefined(SpecialTargets.Parent)(
|
||||
logStreamQueryNotificationEventSelectors.validQueryChanged
|
||||
),
|
||||
showValidationErrorToast: showValidationErrorToast({ toastsService }),
|
||||
updateQueryInUrl: updateQueryInUrl({ toastsService, urlStateStorage }),
|
||||
updateQueryInSearchBar: updateQueryInSearchBar({ queryStringService }),
|
||||
updateFiltersInUrl: updateFiltersInUrl({ toastsService, urlStateStorage }),
|
||||
updateFiltersInSearchBar: updateFiltersInSearchBar({ filterManagerService }),
|
||||
},
|
||||
services: {
|
||||
initializeFromUrl: initializeFromUrl({ toastsService, urlStateStorage }),
|
||||
validateQuery: validateQuery({ kibanaQuerySettings }),
|
||||
subscribeToQuerySearchBarChanges: subscribeToQuerySearchBarChanges({
|
||||
queryStringService,
|
||||
}),
|
||||
subscribeToFilterSearchBarChanges: subscribeToFilterSearchBarChanges({
|
||||
filterManagerService,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
export type LogStreamQueryStateMachine = ReturnType<typeof createLogStreamQueryStateMachine>;
|
||||
export type LogStreamQueryActorRef = OmitDeprecatedState<ActorRefFrom<LogStreamQueryStateMachine>>;
|
|
@ -0,0 +1,96 @@
|
|||
/*
|
||||
* 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 { AggregateQuery, BoolQuery, DataViewBase, Query, Filter } from '@kbn/es-query';
|
||||
|
||||
export type AnyQuery = Query | AggregateQuery;
|
||||
|
||||
export interface ParsedQuery {
|
||||
bool: BoolQuery;
|
||||
}
|
||||
|
||||
export interface LogStreamQueryContextWithDataViews {
|
||||
dataViews: DataViewBase[];
|
||||
}
|
||||
|
||||
export interface LogStreamQueryContextWithSavedQueryId {
|
||||
savedQueryId: string;
|
||||
}
|
||||
export interface LogStreamQueryContextWithQuery {
|
||||
query: AnyQuery;
|
||||
}
|
||||
|
||||
export interface LogStreamQueryContextWithParsedQuery {
|
||||
parsedQuery: ParsedQuery;
|
||||
}
|
||||
|
||||
export interface LogStreamQueryContextWithFilters {
|
||||
filters: Filter[];
|
||||
}
|
||||
|
||||
export interface LogStreamQueryContextWithValidationError {
|
||||
validationError: Error;
|
||||
}
|
||||
|
||||
export type LogStreamQueryTypestate =
|
||||
| {
|
||||
value: 'uninitialized';
|
||||
context: LogStreamQueryContextWithDataViews;
|
||||
}
|
||||
| {
|
||||
value: 'hasQuery' | { hasQuery: 'validating' };
|
||||
context: LogStreamQueryContextWithDataViews &
|
||||
LogStreamQueryContextWithParsedQuery &
|
||||
LogStreamQueryContextWithQuery &
|
||||
LogStreamQueryContextWithFilters;
|
||||
}
|
||||
| {
|
||||
value: { hasQuery: 'valid' };
|
||||
context: LogStreamQueryContextWithDataViews &
|
||||
LogStreamQueryContextWithParsedQuery &
|
||||
LogStreamQueryContextWithQuery &
|
||||
LogStreamQueryContextWithFilters;
|
||||
}
|
||||
| {
|
||||
value: { hasQuery: 'invalid' };
|
||||
context: LogStreamQueryContextWithDataViews &
|
||||
LogStreamQueryContextWithParsedQuery &
|
||||
LogStreamQueryContextWithQuery &
|
||||
LogStreamQueryContextWithFilters &
|
||||
LogStreamQueryContextWithValidationError;
|
||||
};
|
||||
|
||||
export type LogStreamQueryContext = LogStreamQueryTypestate['context'];
|
||||
|
||||
export type LogStreamQueryStateValue = LogStreamQueryTypestate['value'];
|
||||
|
||||
export type LogStreamQueryEvent =
|
||||
| {
|
||||
type: 'QUERY_FROM_SEARCH_BAR_CHANGED';
|
||||
query: AnyQuery;
|
||||
}
|
||||
| {
|
||||
type: 'FILTERS_FROM_SEARCH_BAR_CHANGED';
|
||||
filters: Filter[];
|
||||
}
|
||||
| {
|
||||
type: 'DATA_VIEWS_CHANGED';
|
||||
dataViews: DataViewBase[];
|
||||
}
|
||||
| {
|
||||
type: 'VALIDATION_SUCCEEDED';
|
||||
parsedQuery: ParsedQuery;
|
||||
}
|
||||
| {
|
||||
type: 'VALIDATION_FAILED';
|
||||
error: Error;
|
||||
}
|
||||
| {
|
||||
type: 'INITIALIZED_FROM_URL';
|
||||
query: AnyQuery;
|
||||
filters: Filter[];
|
||||
};
|
|
@ -0,0 +1,178 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { IToasts } from '@kbn/core-notifications-browser';
|
||||
import { Query } from '@kbn/es-query';
|
||||
import { IKbnUrlStateStorage, withNotifyOnErrors } from '@kbn/kibana-utils-plugin/public';
|
||||
import * as Array from 'fp-ts/lib/Array';
|
||||
import * as Either from 'fp-ts/lib/Either';
|
||||
import { pipe } from 'fp-ts/lib/function';
|
||||
import * as rt from 'io-ts';
|
||||
import { InvokeCreator } from 'xstate';
|
||||
import { createPlainError, formatErrors } from '../../../../common/runtime_types';
|
||||
import { replaceStateKeyInQueryString } from '../../../utils/url_state';
|
||||
import type { LogStreamQueryContext, LogStreamQueryEvent, ParsedQuery } from './types';
|
||||
|
||||
interface LogStreamQueryUrlStateDependencies {
|
||||
filterStateKey?: string;
|
||||
savedQueryIdKey?: string;
|
||||
toastsService: IToasts;
|
||||
urlStateStorage: IKbnUrlStateStorage;
|
||||
}
|
||||
|
||||
const defaultFilterStateKey = 'logFilter';
|
||||
const defaultFilterStateValue: Required<FilterStateInUrl> = {
|
||||
query: {
|
||||
language: 'kuery',
|
||||
query: '',
|
||||
},
|
||||
filters: [],
|
||||
};
|
||||
export const safeDefaultParsedQuery: ParsedQuery = {
|
||||
bool: {
|
||||
must: [],
|
||||
must_not: [],
|
||||
should: [],
|
||||
filter: [{ match_none: {} }],
|
||||
},
|
||||
};
|
||||
|
||||
export const updateQueryInUrl =
|
||||
({
|
||||
urlStateStorage,
|
||||
filterStateKey = defaultFilterStateKey,
|
||||
}: LogStreamQueryUrlStateDependencies) =>
|
||||
(context: LogStreamQueryContext, _event: LogStreamQueryEvent) => {
|
||||
if (!('query' in context)) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
urlStateStorage.set(
|
||||
filterStateKey,
|
||||
filterStateInUrlRT.encode({
|
||||
query: context.query,
|
||||
filters: context.filters,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
export const updateFiltersInUrl =
|
||||
({
|
||||
urlStateStorage,
|
||||
filterStateKey = defaultFilterStateKey,
|
||||
}: LogStreamQueryUrlStateDependencies) =>
|
||||
(context: LogStreamQueryContext, _event: LogStreamQueryEvent) => {
|
||||
if (!('filters' in context)) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
urlStateStorage.set(
|
||||
filterStateKey,
|
||||
filterStateInUrlRT.encode({
|
||||
query: context.query,
|
||||
filters: context.filters,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
export const initializeFromUrl =
|
||||
({
|
||||
filterStateKey = defaultFilterStateKey,
|
||||
toastsService,
|
||||
urlStateStorage,
|
||||
}: LogStreamQueryUrlStateDependencies): InvokeCreator<
|
||||
LogStreamQueryContext,
|
||||
LogStreamQueryEvent
|
||||
> =>
|
||||
(_context, _event) =>
|
||||
(send) => {
|
||||
const queryValueFromUrl = urlStateStorage.get(filterStateKey) ?? defaultFilterStateValue;
|
||||
|
||||
const queryE = decodeQueryValueFromUrl(queryValueFromUrl);
|
||||
|
||||
if (Either.isLeft(queryE)) {
|
||||
withNotifyOnErrors(toastsService).onGetError(createPlainError(formatErrors(queryE.left)));
|
||||
|
||||
send({
|
||||
type: 'INITIALIZED_FROM_URL',
|
||||
query: defaultFilterStateValue.query,
|
||||
filters: defaultFilterStateValue.filters,
|
||||
});
|
||||
} else {
|
||||
send({
|
||||
type: 'INITIALIZED_FROM_URL',
|
||||
query: queryE.right.query ?? defaultFilterStateValue.query,
|
||||
filters: queryE.right.filters ?? defaultFilterStateValue.filters,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const filterMeta = rt.partial({
|
||||
alias: rt.union([rt.string, rt.null]),
|
||||
disabled: rt.boolean,
|
||||
negate: rt.boolean,
|
||||
controlledBy: rt.string,
|
||||
group: rt.string,
|
||||
index: rt.string,
|
||||
isMultiIndex: rt.boolean,
|
||||
type: rt.string,
|
||||
key: rt.string,
|
||||
params: rt.any,
|
||||
value: rt.any,
|
||||
});
|
||||
|
||||
const filter = rt.intersection([
|
||||
rt.type({
|
||||
meta: filterMeta,
|
||||
}),
|
||||
rt.partial({
|
||||
query: rt.UnknownRecord,
|
||||
}),
|
||||
]);
|
||||
|
||||
const filterStateInUrlRT = rt.partial({
|
||||
query: rt.union([
|
||||
rt.strict({
|
||||
language: rt.string,
|
||||
query: rt.union([rt.string, rt.record(rt.string, rt.unknown)]),
|
||||
}),
|
||||
rt.strict({
|
||||
sql: rt.string,
|
||||
}),
|
||||
rt.strict({
|
||||
esql: rt.string,
|
||||
}),
|
||||
]),
|
||||
filters: rt.array(filter),
|
||||
});
|
||||
|
||||
type FilterStateInUrl = rt.TypeOf<typeof filterStateInUrlRT>;
|
||||
|
||||
const legacyFilterStateInUrlRT = rt.union([
|
||||
rt.strict({
|
||||
language: rt.string,
|
||||
query: rt.union([rt.string, rt.record(rt.string, rt.unknown)]),
|
||||
}),
|
||||
rt.strict({
|
||||
sql: rt.string,
|
||||
}),
|
||||
rt.strict({
|
||||
esql: rt.string,
|
||||
}),
|
||||
]);
|
||||
|
||||
const decodeQueryValueFromUrl = (queryValueFromUrl: unknown) =>
|
||||
Either.getAltValidation(Array.getMonoid<rt.ValidationError>()).alt<FilterStateInUrl>(
|
||||
pipe(
|
||||
legacyFilterStateInUrlRT.decode(queryValueFromUrl),
|
||||
Either.map((legacyQuery) => ({ query: legacyQuery }))
|
||||
),
|
||||
() => filterStateInUrlRT.decode(queryValueFromUrl)
|
||||
);
|
||||
|
||||
export const replaceLogFilterInQueryString = (query: Query) =>
|
||||
replaceStateKeyInQueryString<Query>(defaultFilterStateKey, query);
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { IToasts } from '@kbn/core-notifications-browser';
|
||||
import { buildEsQuery, EsQueryConfig, isOfQueryType } from '@kbn/es-query';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { InvokeCreator } from 'xstate';
|
||||
import { QueryParsingError, UnsupportedLanguageError } from './errors';
|
||||
import type { LogStreamQueryContext, LogStreamQueryEvent } from './types';
|
||||
|
||||
export const validateQuery =
|
||||
({
|
||||
kibanaQuerySettings,
|
||||
}: {
|
||||
kibanaQuerySettings: EsQueryConfig;
|
||||
}): InvokeCreator<LogStreamQueryContext, LogStreamQueryEvent> =>
|
||||
(context) =>
|
||||
(send) => {
|
||||
if (!('query' in context)) {
|
||||
throw new Error('Failed to validate query: no query in context');
|
||||
}
|
||||
|
||||
const { dataViews, query, filters } = context;
|
||||
|
||||
if (!isOfQueryType(query)) {
|
||||
send({
|
||||
type: 'VALIDATION_FAILED',
|
||||
error: new UnsupportedLanguageError('Failed to validate query: unsupported language'),
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsedQuery = buildEsQuery(dataViews, query, filters, kibanaQuerySettings);
|
||||
|
||||
send({
|
||||
type: 'VALIDATION_SUCCEEDED',
|
||||
parsedQuery,
|
||||
});
|
||||
} catch (error) {
|
||||
send({
|
||||
type: 'VALIDATION_FAILED',
|
||||
error: new QueryParsingError(`${error}`),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const showValidationErrorToast =
|
||||
({ toastsService }: { toastsService: IToasts }) =>
|
||||
(_context: LogStreamQueryContext, event: LogStreamQueryEvent) => {
|
||||
if (event.type !== 'VALIDATION_FAILED') {
|
||||
return;
|
||||
}
|
||||
|
||||
toastsService.addError(event.error, {
|
||||
title: validationErrorToastTitle,
|
||||
});
|
||||
};
|
||||
|
||||
const validationErrorToastTitle = i18n.translate(
|
||||
'xpack.infra.logsPage.toolbar.logFilterErrorToastTitle',
|
||||
{
|
||||
defaultMessage: 'Log filter error',
|
||||
}
|
||||
);
|
|
@ -5,5 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export * from './invalid_state_callout';
|
||||
export * from './notification_channel';
|
||||
export * from './send_actions';
|
||||
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 { EuiCallOut } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import stringify from 'json-stable-stringify';
|
||||
import React from 'react';
|
||||
import type { State } from 'xstate';
|
||||
|
||||
export const InvalidStateCallout: React.FC<{ state: State<any, any, any, any, any> }> = ({
|
||||
state,
|
||||
}) => (
|
||||
<EuiCallOut title={invalidStateCalloutTitle} color="danger" iconType="alert">
|
||||
<FormattedMessage
|
||||
id="xpack.infra.logs.common.invalidStateMessage"
|
||||
defaultMessage="Unable to handle state {stateValue}."
|
||||
values={{
|
||||
stateValue: stringify(state.value),
|
||||
}}
|
||||
tagName="pre"
|
||||
/>
|
||||
</EuiCallOut>
|
||||
);
|
||||
|
||||
const invalidStateCalloutTitle = i18n.translate(
|
||||
'xpack.infra.logs.common.invalidStateCalloutTitle',
|
||||
{ defaultMessage: 'Invalid state encountered' }
|
||||
);
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* 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, ActorRefWithDeprecatedState, EmittedFrom, State, StateValue } from 'xstate';
|
||||
|
||||
export type OmitDeprecatedState<T extends ActorRefWithDeprecatedState<any, any, any, any>> = Omit<
|
||||
T,
|
||||
'state'
|
||||
>;
|
||||
|
||||
export type MatchedState<
|
||||
TState extends State<any, any, any, any, any>,
|
||||
TStateValue extends StateValue
|
||||
> = TState extends State<
|
||||
any,
|
||||
infer TEvent,
|
||||
infer TStateSchema,
|
||||
infer TTypestate,
|
||||
infer TResolvedTypesMeta
|
||||
>
|
||||
? State<
|
||||
(TTypestate extends any
|
||||
? { value: TStateValue; context: any } extends TTypestate
|
||||
? TTypestate
|
||||
: never
|
||||
: never)['context'],
|
||||
TEvent,
|
||||
TStateSchema,
|
||||
TTypestate,
|
||||
TResolvedTypesMeta
|
||||
> & {
|
||||
value: TStateValue;
|
||||
}
|
||||
: never;
|
||||
|
||||
export type MatchedStateFromActor<
|
||||
TActorRef extends ActorRef<any, any>,
|
||||
TStateValue extends StateValue
|
||||
> = MatchedState<EmittedFrom<TActorRef>, TStateValue>;
|
|
@ -8,10 +8,9 @@
|
|||
import { flowRight } from 'lodash';
|
||||
import React from 'react';
|
||||
import { match as RouteMatch, Redirect, RouteComponentProps } from 'react-router-dom';
|
||||
|
||||
import { replaceLogFilterInQueryString } from '../../containers/logs/log_filter';
|
||||
import { replaceLogPositionInQueryString } from '../../containers/logs/log_position';
|
||||
import { replaceSourceIdInQueryString } from '../../containers/source_id';
|
||||
import { replaceLogFilterInQueryString } from '../../observability_logs/log_stream_query_state';
|
||||
import { getFilterFromLocation, getTimeFromLocation } from './query_params';
|
||||
|
||||
type RedirectToLogsType = RouteComponentProps<{}>;
|
||||
|
|
|
@ -6,19 +6,19 @@
|
|||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { LinkDescriptor } from '@kbn/observability-plugin/public';
|
||||
import { flowRight } from 'lodash';
|
||||
import React from 'react';
|
||||
import { Redirect, RouteComponentProps } from 'react-router-dom';
|
||||
import useMount from 'react-use/lib/useMount';
|
||||
import { LinkDescriptor } from '@kbn/observability-plugin/public';
|
||||
import { findInventoryFields } from '../../../common/inventory_models';
|
||||
import { InventoryItemType } from '../../../common/inventory_models/types';
|
||||
import { LoadingPage } from '../../components/loading_page';
|
||||
import { replaceLogFilterInQueryString } from '../../containers/logs/log_filter';
|
||||
import { replaceLogPositionInQueryString } from '../../containers/logs/log_position';
|
||||
import { replaceSourceIdInQueryString } from '../../containers/source_id';
|
||||
import { useKibanaContextForPlugin } from '../../hooks/use_kibana';
|
||||
import { useLogView } from '../../hooks/use_log_view';
|
||||
import { replaceLogFilterInQueryString } from '../../observability_logs/log_stream_query_state';
|
||||
import { getFilterFromLocation, getTimeFromLocation } from './query_params';
|
||||
|
||||
type RedirectToNodeLogsType = RouteComponentProps<{
|
||||
|
|
|
@ -12,7 +12,7 @@ import { useKibana } from '@kbn/kibana-react-plugin/public';
|
|||
import { NoDataConfig } from '@kbn/shared-ux-page-kibana-template';
|
||||
import { useKibanaContextForPlugin } from '../../../hooks/use_kibana';
|
||||
|
||||
interface LogsPageTemplateProps extends LazyObservabilityPageTemplateProps {
|
||||
export interface LogsPageTemplateProps extends LazyObservabilityPageTemplateProps {
|
||||
hasData?: boolean;
|
||||
isDataLoading?: boolean;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* 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 { APP_WRAPPER_CLASS } from '@kbn/core/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { fullHeightContentStyles } from '../../../../page_template.styles';
|
||||
import { LogsPageTemplate, LogsPageTemplateProps } from '../../shared/page_template';
|
||||
|
||||
export const LogStreamPageTemplate: React.FC<LogsPageTemplateProps> = React.memo((props) => (
|
||||
<div className={APP_WRAPPER_CLASS}>
|
||||
<LogsPageTemplate
|
||||
pageHeader={{
|
||||
pageTitle: streamTitle,
|
||||
}}
|
||||
pageSectionProps={{
|
||||
contentProps: {
|
||||
css: fullHeightContentStyles,
|
||||
},
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
));
|
||||
|
||||
const streamTitle = i18n.translate('xpack.infra.logs.streamPageTitle', {
|
||||
defaultMessage: 'Stream',
|
||||
});
|
|
@ -6,13 +6,16 @@
|
|||
*/
|
||||
|
||||
import { EuiErrorBoundary } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { useTrackPageview } from '@kbn/observability-plugin/public';
|
||||
import { useLogViewContext } from '../../../hooks/use_log_view';
|
||||
import React from 'react';
|
||||
import { useKibanaContextForPlugin } from '../../../hooks/use_kibana';
|
||||
import { useLogsBreadcrumbs } from '../../../hooks/use_logs_breadcrumbs';
|
||||
import { ConnectedStreamPageContent } from './page_content';
|
||||
import { LogStreamPageProviders } from './page_providers';
|
||||
import { useLogViewContext } from '../../../hooks/use_log_view';
|
||||
import { LogStreamPageStateProvider } from '../../../observability_logs/log_stream_page/state';
|
||||
import { streamTitle } from '../../../translations';
|
||||
import { useKbnUrlStateStorageFromRouterContext } from '../../../utils/kbn_url_state_context';
|
||||
import { useKibanaQuerySettings } from '../../../utils/use_kibana_query_settings';
|
||||
import { ConnectedStreamPageContent } from './page_content';
|
||||
|
||||
export const StreamPage = () => {
|
||||
useTrackPageview({ app: 'infra_logs', path: 'stream' });
|
||||
|
@ -25,12 +28,32 @@ export const StreamPage = () => {
|
|||
]);
|
||||
|
||||
const { logViewStateNotifications } = useLogViewContext();
|
||||
const {
|
||||
services: {
|
||||
data: {
|
||||
query: { queryString: queryStringService, filterManager: filterManagerService },
|
||||
},
|
||||
notifications: { toasts: toastsService },
|
||||
},
|
||||
} = useKibanaContextForPlugin();
|
||||
|
||||
const kibanaQuerySettings = useKibanaQuerySettings();
|
||||
const urlStateStorage = useKbnUrlStateStorageFromRouterContext();
|
||||
|
||||
return (
|
||||
<EuiErrorBoundary>
|
||||
<LogStreamPageProviders logViewStateNotifications={logViewStateNotifications}>
|
||||
<ConnectedStreamPageContent />
|
||||
</LogStreamPageProviders>
|
||||
<LogStreamPageStateProvider
|
||||
kibanaQuerySettings={kibanaQuerySettings}
|
||||
logViewStateNotifications={logViewStateNotifications}
|
||||
queryStringService={queryStringService}
|
||||
toastsService={toastsService}
|
||||
filterManagerService={filterManagerService}
|
||||
urlStateStorage={urlStateStorage}
|
||||
>
|
||||
<ConnectedStreamPageContentMemo />
|
||||
</LogStreamPageStateProvider>
|
||||
</EuiErrorBoundary>
|
||||
);
|
||||
};
|
||||
|
||||
const ConnectedStreamPageContentMemo = React.memo(ConnectedStreamPageContent);
|
||||
|
|
|
@ -5,104 +5,43 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { APP_WRAPPER_CLASS } from '@kbn/core/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useSelector } from '@xstate/react';
|
||||
import { useActor } from '@xstate/react';
|
||||
import React from 'react';
|
||||
import { SourceLoadingPage } from '../../../components/source_loading_page';
|
||||
import { useLogStreamPageStateContext } from '../../../observability_logs/log_stream_page/state/src/provider';
|
||||
import { fullHeightContentStyles } from '../../../page_template.styles';
|
||||
import {
|
||||
LogStreamPageState,
|
||||
useLogStreamPageStateContext,
|
||||
} from '../../../observability_logs/log_stream_page/state';
|
||||
import { InvalidStateCallout } from '../../../observability_logs/xstate_helpers';
|
||||
import { ConnectedLogViewErrorPage } from '../shared/page_log_view_error';
|
||||
import { LogsPageTemplate } from '../shared/page_template';
|
||||
import { LogsPageLogsContent } from './page_logs_content';
|
||||
import { StreamPageLogsContentForState } from './page_logs_content';
|
||||
import { StreamPageMissingIndicesContent } from './page_missing_indices_content';
|
||||
import { LogStreamPageContentProviders } from './page_providers';
|
||||
|
||||
const streamTitle = i18n.translate('xpack.infra.logs.streamPageTitle', {
|
||||
defaultMessage: 'Stream',
|
||||
});
|
||||
|
||||
interface InjectedProps {
|
||||
isLoading: boolean;
|
||||
hasFailedLoading: boolean;
|
||||
hasIndices: boolean;
|
||||
missingIndices: boolean;
|
||||
}
|
||||
|
||||
export const ConnectedStreamPageContent: React.FC = () => {
|
||||
const logStreamPageStateService = useLogStreamPageStateContext();
|
||||
|
||||
const isLoading = useSelector(logStreamPageStateService, (state) => {
|
||||
return state.matches('uninitialized') || state.matches('loadingLogView');
|
||||
});
|
||||
const [logStreamPageState] = useActor(logStreamPageStateService);
|
||||
|
||||
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}
|
||||
/>
|
||||
);
|
||||
return <StreamPageContentForState logStreamPageState={logStreamPageState} />;
|
||||
};
|
||||
|
||||
export const StreamPageContent: React.FC<InjectedProps> = (props: InjectedProps) => {
|
||||
const { isLoading, hasFailedLoading, hasIndices, missingIndices } = props;
|
||||
|
||||
if (isLoading) {
|
||||
export const StreamPageContentForState: React.FC<{ logStreamPageState: LogStreamPageState }> = ({
|
||||
logStreamPageState,
|
||||
}) => {
|
||||
if (logStreamPageState.matches('uninitialized') || logStreamPageState.matches('loadingLogView')) {
|
||||
return <SourceLoadingPage />;
|
||||
} else if (hasFailedLoading) {
|
||||
} else if (logStreamPageState.matches('loadingLogViewFailed')) {
|
||||
return <ConnectedLogViewErrorPage />;
|
||||
} else if (missingIndices) {
|
||||
} else if (logStreamPageState.matches('missingLogViewIndices')) {
|
||||
return <StreamPageMissingIndicesContent />;
|
||||
} else if (logStreamPageState.matches({ hasLogViewIndices: 'initialized' })) {
|
||||
return (
|
||||
<div className={APP_WRAPPER_CLASS}>
|
||||
<LogsPageTemplate
|
||||
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,
|
||||
}}
|
||||
pageSectionProps={{
|
||||
contentProps: {
|
||||
css: fullHeightContentStyles,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<LogStreamPageContentProviders>
|
||||
<LogsPageLogsContent />
|
||||
</LogStreamPageContentProviders>
|
||||
</LogsPageTemplate>
|
||||
</div>
|
||||
<LogStreamPageContentProviders logStreamPageState={logStreamPageState}>
|
||||
<StreamPageLogsContentForState logStreamPageState={logStreamPageState} />
|
||||
</LogStreamPageContentProviders>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
return <InvalidStateCallout state={logStreamPageState} />;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -7,9 +7,9 @@
|
|||
|
||||
import { EuiSpacer } from '@elastic/eui';
|
||||
import type { Query } from '@kbn/es-query';
|
||||
import { euiStyled } from '@kbn/kibana-react-plugin/common';
|
||||
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||
import usePrevious from 'react-use/lib/usePrevious';
|
||||
import { euiStyled } from '@kbn/kibana-react-plugin/common';
|
||||
import { LogEntry } from '../../../../common/log_entry';
|
||||
import { TimeKey } from '../../../../common/time';
|
||||
import { AutoSizer } from '../../../components/auto_sizer';
|
||||
|
@ -18,7 +18,6 @@ import { LogMinimap } from '../../../components/logging/log_minimap';
|
|||
import { ScrollableLogTextStreamView } from '../../../components/logging/log_text_stream';
|
||||
import { LogEntryStreamItem } from '../../../components/logging/log_text_stream/item';
|
||||
import { PageContent } from '../../../components/page';
|
||||
import { useLogFilterStateContext } from '../../../containers/logs/log_filter';
|
||||
import {
|
||||
useLogEntryFlyoutContext,
|
||||
WithFlyoutOptionsUrlState,
|
||||
|
@ -30,15 +29,21 @@ import { WithSummary } from '../../../containers/logs/log_summary';
|
|||
import { useLogViewConfigurationContext } from '../../../containers/logs/log_view_configuration';
|
||||
import { useViewLogInProviderContext } from '../../../containers/logs/view_log_in_context';
|
||||
import { WithLogTextviewUrlState } from '../../../containers/logs/with_log_textview';
|
||||
import { useKibanaContextForPlugin } from '../../../hooks/use_kibana';
|
||||
import { useLogViewContext } from '../../../hooks/use_log_view';
|
||||
import { LogStreamPageActorRef } from '../../../observability_logs/log_stream_page/state';
|
||||
import { type ParsedQuery } from '../../../observability_logs/log_stream_query_state';
|
||||
import { MatchedStateFromActor } from '../../../observability_logs/xstate_helpers';
|
||||
import { datemathToEpochMillis, isValidDatemath } from '../../../utils/datemath';
|
||||
import { LogStreamPageTemplate } from './components/stream_page_template';
|
||||
import { LogsToolbar } from './page_toolbar';
|
||||
import { PageViewLogInContext } from './page_view_log_in_context';
|
||||
import { useKibanaContextForPlugin } from '../../../hooks/use_kibana';
|
||||
|
||||
const PAGE_THRESHOLD = 2;
|
||||
|
||||
export const LogsPageLogsContent: React.FunctionComponent = () => {
|
||||
export const StreamPageLogsContent = React.memo<{
|
||||
filterQuery: ParsedQuery;
|
||||
}>(({ filterQuery }) => {
|
||||
const {
|
||||
data: {
|
||||
query: { queryString },
|
||||
|
@ -71,7 +76,6 @@ export const LogsPageLogsContent: React.FunctionComponent = () => {
|
|||
updateDateRange,
|
||||
lastCompleteDateRangeExpressionUpdate,
|
||||
} = useLogPositionStateContext();
|
||||
const { filterQuery } = useLogFilterStateContext();
|
||||
|
||||
const {
|
||||
isReloading,
|
||||
|
@ -210,7 +214,7 @@ export const LogsPageLogsContent: React.FunctionComponent = () => {
|
|||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<LogStreamPageTemplate hasData={true} isDataLoading={false}>
|
||||
<WithLogTextviewUrlState />
|
||||
<WithFlyoutOptionsUrlState />
|
||||
<LogsToolbar />
|
||||
|
@ -276,9 +280,24 @@ export const LogsPageLogsContent: React.FunctionComponent = () => {
|
|||
}}
|
||||
</AutoSizer>
|
||||
</PageContent>
|
||||
</>
|
||||
</LogStreamPageTemplate>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
type InitializedLogStreamPageState = MatchedStateFromActor<
|
||||
LogStreamPageActorRef,
|
||||
{ hasLogViewIndices: 'initialized' }
|
||||
>;
|
||||
|
||||
export const StreamPageLogsContentForState = React.memo<{
|
||||
logStreamPageState: InitializedLogStreamPageState;
|
||||
}>(({ logStreamPageState }) => {
|
||||
const {
|
||||
context: { parsedQuery },
|
||||
} = logStreamPageState;
|
||||
|
||||
return <StreamPageLogsContent filterQuery={parsedQuery} />;
|
||||
});
|
||||
|
||||
const LogPageMinimapColumn = euiStyled.div`
|
||||
flex: 1 0 0%;
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { LogStreamPageTemplate } from './components/stream_page_template';
|
||||
|
||||
export const StreamPageMissingIndicesContent = React.memo(() => (
|
||||
<LogStreamPageTemplate hasData={false} isDataLoading={false} />
|
||||
));
|
|
@ -5,12 +5,9 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
LogFilterStateProvider,
|
||||
useLogFilterStateContext,
|
||||
WithLogFilterUrlState,
|
||||
} from '../../../containers/logs/log_filter';
|
||||
import stringify from 'json-stable-stringify';
|
||||
import React, { useMemo } from 'react';
|
||||
import { LogStreamPageActorRef } from '../../../observability_logs/log_stream_page/state';
|
||||
import { LogEntryFlyoutProvider } from '../../../containers/logs/log_flyout';
|
||||
import { LogHighlightsStateProvider } from '../../../containers/logs/log_highlights/log_highlights';
|
||||
import {
|
||||
|
@ -21,19 +18,7 @@ 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();
|
||||
|
||||
return (
|
||||
<LogFilterStateProvider dataView={derivedDataView}>
|
||||
<WithLogFilterUrlState />
|
||||
{children}
|
||||
</LogFilterStateProvider>
|
||||
);
|
||||
};
|
||||
import { MatchedStateFromActor } from '../../../observability_logs/xstate_helpers';
|
||||
|
||||
const ViewLogInContext: React.FC = ({ children }) => {
|
||||
const { startTimestamp, endTimestamp } = useLogPositionStateContext();
|
||||
|
@ -54,10 +39,14 @@ const ViewLogInContext: React.FC = ({ children }) => {
|
|||
);
|
||||
};
|
||||
|
||||
const LogEntriesStateProvider: React.FC = ({ children }) => {
|
||||
const LogEntriesStateProvider: React.FC<{
|
||||
logStreamPageState: InitializedLogStreamPageState;
|
||||
}> = ({ children, logStreamPageState }) => {
|
||||
const { logViewId } = useLogViewContext();
|
||||
const { startTimestamp, endTimestamp, targetPosition } = useLogPositionStateContext();
|
||||
const { filterQuery } = useLogFilterStateContext();
|
||||
const {
|
||||
context: { parsedQuery },
|
||||
} = logStreamPageState;
|
||||
|
||||
// Don't render anything if the date range is incorrect.
|
||||
if (!startTimestamp || !endTimestamp) {
|
||||
|
@ -69,7 +58,7 @@ const LogEntriesStateProvider: React.FC = ({ children }) => {
|
|||
sourceId={logViewId}
|
||||
startTimestamp={startTimestamp}
|
||||
endTimestamp={endTimestamp}
|
||||
query={filterQuery?.parsedQuery}
|
||||
query={parsedQuery}
|
||||
center={targetPosition ?? undefined}
|
||||
>
|
||||
{children}
|
||||
|
@ -77,10 +66,15 @@ const LogEntriesStateProvider: React.FC = ({ children }) => {
|
|||
);
|
||||
};
|
||||
|
||||
const LogHighlightsState: React.FC = ({ children }) => {
|
||||
const LogHighlightsState: React.FC<{
|
||||
logStreamPageState: InitializedLogStreamPageState;
|
||||
}> = ({ children, logStreamPageState }) => {
|
||||
const { logViewId, logView } = useLogViewContext();
|
||||
const { topCursor, bottomCursor, entries } = useLogStreamContext();
|
||||
const { filterQuery } = useLogFilterStateContext();
|
||||
const serializedParsedQuery = useMemo(
|
||||
() => stringify(logStreamPageState.context.parsedQuery),
|
||||
[logStreamPageState.context.parsedQuery]
|
||||
);
|
||||
|
||||
const highlightsProps = {
|
||||
sourceId: logViewId,
|
||||
|
@ -89,35 +83,32 @@ const LogHighlightsState: React.FC = ({ children }) => {
|
|||
entriesEnd: bottomCursor,
|
||||
centerCursor: entries.length > 0 ? entries[Math.floor(entries.length / 2)].cursor : null,
|
||||
size: entries.length,
|
||||
filterQuery: filterQuery?.serializedQuery ?? null,
|
||||
filterQuery: serializedParsedQuery,
|
||||
};
|
||||
return <LogHighlightsStateProvider {...highlightsProps}>{children}</LogHighlightsStateProvider>;
|
||||
};
|
||||
|
||||
export const LogStreamPageProviders: React.FunctionComponent<{
|
||||
logViewStateNotifications: LogViewNotificationChannel;
|
||||
}> = ({ children, logViewStateNotifications }) => {
|
||||
return (
|
||||
<LogStreamPageStateProvider logViewStateNotifications={logViewStateNotifications}>
|
||||
{children}
|
||||
</LogStreamPageStateProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export const LogStreamPageContentProviders: React.FunctionComponent = ({ children }) => {
|
||||
export const LogStreamPageContentProviders: React.FC<{
|
||||
logStreamPageState: InitializedLogStreamPageState;
|
||||
}> = ({ children, logStreamPageState }) => {
|
||||
return (
|
||||
<LogViewConfigurationProvider>
|
||||
<LogEntryFlyoutProvider>
|
||||
<LogPositionStateProvider>
|
||||
<ViewLogInContext>
|
||||
<LogFilterState>
|
||||
<LogEntriesStateProvider>
|
||||
<LogHighlightsState>{children}</LogHighlightsState>
|
||||
</LogEntriesStateProvider>
|
||||
</LogFilterState>
|
||||
<LogEntriesStateProvider logStreamPageState={logStreamPageState}>
|
||||
<LogHighlightsState logStreamPageState={logStreamPageState}>
|
||||
{children}
|
||||
</LogHighlightsState>
|
||||
</LogEntriesStateProvider>
|
||||
</ViewLogInContext>
|
||||
</LogPositionStateProvider>
|
||||
</LogEntryFlyoutProvider>
|
||||
</LogViewConfigurationProvider>
|
||||
);
|
||||
};
|
||||
|
||||
type InitializedLogStreamPageState = MatchedStateFromActor<
|
||||
LogStreamPageActorRef,
|
||||
{ hasLogViewIndices: 'initialized' }
|
||||
>;
|
||||
|
|
|
@ -59,7 +59,7 @@ export const LogsToolbar = () => {
|
|||
indexPatterns={dataViews}
|
||||
showQueryInput={true}
|
||||
showQueryMenu={false}
|
||||
showFilterBar={false}
|
||||
showFilterBar={true}
|
||||
showDatePicker={true}
|
||||
displayStyle="inPage"
|
||||
/>
|
||||
|
|
|
@ -16804,7 +16804,6 @@
|
|||
"xpack.infra.logSourceErrorPage.tryAgainButtonLabel": "Réessayer",
|
||||
"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",
|
||||
"xpack.infra.logStream.kqlErrorTitle": "Expression KQL non valide",
|
||||
"xpack.infra.logStream.unknownErrorTitle": "Une erreur s'est produite",
|
||||
"xpack.infra.logStreamEmbeddable.description": "Ajoutez un tableau de logs de diffusion en direct.",
|
||||
|
|
|
@ -16790,7 +16790,6 @@
|
|||
"xpack.infra.logSourceErrorPage.tryAgainButtonLabel": "再試行",
|
||||
"xpack.infra.logsPage.toolbar.kqlSearchFieldPlaceholder": "ログエントリーを検索中…(例:host.name:host-1)",
|
||||
"xpack.infra.logsPage.toolbar.logFilterErrorToastTitle": "ログフィルターエラー",
|
||||
"xpack.infra.logsPage.toolbar.logFilterUnsupportedLanguageError": "SQLはサポートされていません",
|
||||
"xpack.infra.logStream.kqlErrorTitle": "無効なKQL式",
|
||||
"xpack.infra.logStream.unknownErrorTitle": "エラーが発生しました",
|
||||
"xpack.infra.logStreamEmbeddable.description": "ライブストリーミングログのテーブルを追加します。",
|
||||
|
|
|
@ -16809,7 +16809,6 @@
|
|||
"xpack.infra.logSourceErrorPage.tryAgainButtonLabel": "重试",
|
||||
"xpack.infra.logsPage.toolbar.kqlSearchFieldPlaceholder": "搜索日志条目……(例如 host.name:host-1)",
|
||||
"xpack.infra.logsPage.toolbar.logFilterErrorToastTitle": "日志筛选错误",
|
||||
"xpack.infra.logsPage.toolbar.logFilterUnsupportedLanguageError": "不支持 SQL",
|
||||
"xpack.infra.logStream.kqlErrorTitle": "KQL 表达式无效",
|
||||
"xpack.infra.logStream.unknownErrorTitle": "发生错误",
|
||||
"xpack.infra.logStreamEmbeddable.description": "添加实时流式传输日志的表。",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue