[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:
Felix Stürmer 2023-01-03 19:28:50 +01:00 committed by GitHub
parent 6f3e0a3b95
commit 46d689220b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
39 changed files with 1264 additions and 432 deletions

View file

@ -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);

View 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)

View file

@ -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.

View file

@ -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} />;
});
```

View file

@ -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);

View file

@ -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,
});

View file

@ -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 });

View file

@ -5,11 +5,4 @@
* 2.0.
*/
export {
createLogStreamPageStateMachine,
LogStreamPageStateProvider,
useLogStreamPageState,
useLogStreamPageStateContext,
type LogStreamPageContext,
type LogStreamPageEvent,
} from './src';
export * from './src';

View file

@ -6,5 +6,6 @@
*/
export * from './provider';
export * from './selectors';
export * from './state_machine';
export * from './types';

View file

@ -0,0 +1,36 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { 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,
});
}
});
};

View file

@ -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 }
);

View file

@ -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;

View file

@ -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(),
},
});

View file

@ -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;

View file

@ -5,5 +5,4 @@
* 2.0.
*/
export * from './log_filter_state';
export * from './with_log_filter_url_state';
export * from './src';

View file

@ -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';

View file

@ -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,
};

View file

@ -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);
}
};

View file

@ -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>>;

View file

@ -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[];
};

View file

@ -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);

View file

@ -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',
}
);

View file

@ -5,5 +5,7 @@
* 2.0.
*/
export * from './invalid_state_callout';
export * from './notification_channel';
export * from './send_actions';
export * from './types';

View file

@ -0,0 +1,33 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { 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' }
);

View file

@ -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>;

View file

@ -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<{}>;

View file

@ -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<{

View file

@ -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;
}

View file

@ -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',
});

View file

@ -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);

View file

@ -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} />;
}
};

View file

@ -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%;

View file

@ -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} />
));

View file

@ -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' }
>;

View file

@ -59,7 +59,7 @@ export const LogsToolbar = () => {
indexPatterns={dataViews}
showQueryInput={true}
showQueryMenu={false}
showFilterBar={false}
showFilterBar={true}
showDatePicker={true}
displayStyle="inPage"
/>

View file

@ -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.",

View file

@ -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": "ライブストリーミングログのテーブルを追加します。",

View file

@ -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": "添加实时流式传输日志的表。",