"View Surrounding Logs" functionality (#35727)

* Add "View Surrounding Logs" functionality

Move Logs Flyout state from Redux to Hooks

* Cleanup unused imports etc

* Amend for Prettier

* Stop live streaming when a filter is applied

* Change tooltip wording

* Compose functions for props higher in the component hierarchy

* Don't make a request at all without a flyoutId

* Handle no response

* Use TimeKey on document

* Appease Prettier

* Clean up unused props

* Use shared InfraTimeKey fragment

* Add semicolon
This commit is contained in:
Kerry Gallagher 2019-05-02 22:39:38 +01:00 committed by GitHub
parent 53ad55cf6f
commit dd6c2e2a2e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 349 additions and 287 deletions

View file

@ -186,6 +186,8 @@ export interface InfraLogItem {
id: string;
/** The index where the document was found */
index: string;
/** Time key for the document - derived from the source configuration timestamp and tiebreaker settings */
key: InfraTimeKey;
/** An array of flattened fields and values */
fields: InfraLogItemField[];
}
@ -551,9 +553,19 @@ export namespace FlyoutItemQuery {
index: string;
key: Key;
fields: Fields[];
};
export type Key = {
__typename?: 'InfraTimeKey';
time: number;
tiebreaker: number;
};
export type Fields = {
__typename?: 'InfraLogItemField';

View file

@ -14,24 +14,40 @@ import {
EuiToolTip,
} from '@elastic/eui';
import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react';
import moment from 'moment';
import React from 'react';
import euiStyled from '../../../../../common/eui_styled_components';
import { TimeKey } from '../../../common/time';
import { InfraLogItem, InfraLogItemField } from '../../graphql/types';
import { InfraLoadingPanel } from '../loading';
interface Props {
flyoutItem: InfraLogItem | null;
hideFlyout: () => void;
setFlyoutVisibility: (visible: boolean) => void;
setFilter: (filter: string) => void;
setTarget: (timeKey: TimeKey, flyoutItemId: string) => void;
intl: InjectedIntl;
loading: boolean;
}
export const LogFlyout = injectI18n(
({ flyoutItem, loading, hideFlyout, setFilter, intl }: Props) => {
({ flyoutItem, loading, setFlyoutVisibility, setFilter, setTarget, intl }: Props) => {
const handleFilter = (field: InfraLogItemField) => () => {
const filter = `${field.field}:"${field.value}"`;
setFilter(filter);
if (flyoutItem && flyoutItem.key) {
const timestampMoment = moment(flyoutItem.key.time);
if (timestampMoment.isValid()) {
setTarget(
{
time: timestampMoment.valueOf(),
tiebreaker: flyoutItem.key.tiebreaker,
},
flyoutItem.id
);
}
}
};
const columns = [
@ -55,7 +71,7 @@ export const LogFlyout = injectI18n(
<EuiToolTip
content={intl.formatMessage({
id: 'xpack.infra.logFlyout.setFilterTooltip',
defaultMessage: 'Set Filter',
defaultMessage: 'View event with filter',
})}
>
<EuiButtonIcon
@ -74,7 +90,7 @@ export const LogFlyout = injectI18n(
},
];
return (
<EuiFlyout onClose={() => hideFlyout()} size="m">
<EuiFlyout onClose={() => setFlyoutVisibility(false)} size="m">
<EuiFlyoutHeader hasBorder>
<EuiTitle size="s">
<h3 id="flyoutTitle">

View file

@ -17,6 +17,7 @@ interface LogTextStreamItemDateFieldProps {
hasHighlights: boolean;
isHovered: boolean;
scale: TextScale;
isHighlighted: boolean;
}
export class LogTextStreamItemDateField extends React.PureComponent<
@ -24,12 +25,13 @@ export class LogTextStreamItemDateField extends React.PureComponent<
{}
> {
public render() {
const { children, hasHighlights, isHovered, scale } = this.props;
const { children, hasHighlights, isHovered, isHighlighted, scale } = this.props;
return (
<LogTextStreamItemDateFieldWrapper
hasHighlights={hasHighlights}
isHovered={isHovered}
isHighlighted={isHighlighted}
scale={scale}
>
{children}
@ -59,6 +61,7 @@ const hoveredFieldStyle = css`
const LogTextStreamItemDateFieldWrapper = LogTextStreamItemField.extend.attrs<{
hasHighlights: boolean;
isHovered: boolean;
isHighlighted: boolean;
}>({})`
background-color: ${props => props.theme.eui.euiColorLightestShade};
border-right: solid 2px ${props => props.theme.eui.euiColorLightShade};
@ -67,5 +70,5 @@ const LogTextStreamItemDateFieldWrapper = LogTextStreamItemField.extend.attrs<{
padding: 0 ${props => props.theme.eui.paddingSizes.l};
${props => (props.hasHighlights ? highlightedFieldStyle : '')};
${props => (props.isHovered ? hoveredFieldStyle : '')};
${props => (props.isHovered || props.isHighlighted ? hoveredFieldStyle : '')};
`;

View file

@ -18,6 +18,7 @@ interface LogTextStreamItemMessageFieldProps {
isHovered: boolean;
isWrapped: boolean;
scale: TextScale;
isHighlighted: boolean;
}
export class LogTextStreamItemMessageField extends React.PureComponent<
@ -25,7 +26,7 @@ export class LogTextStreamItemMessageField extends React.PureComponent<
{}
> {
public render() {
const { children, highlights, isHovered, isWrapped, scale } = this.props;
const { children, highlights, isHovered, isHighlighted, isWrapped, scale } = this.props;
const hasHighlights = highlights.length > 0;
const content = hasHighlights ? renderHighlightFragments(children, highlights) : children;
@ -33,6 +34,7 @@ export class LogTextStreamItemMessageField extends React.PureComponent<
<LogTextStreamItemMessageFieldWrapper
hasHighlights={hasHighlights}
isHovered={isHovered}
isHighlighted={isHighlighted}
isWrapped={isWrapped}
scale={scale}
>
@ -97,6 +99,7 @@ const unwrappedFieldStyle = css`
const LogTextStreamItemMessageFieldWrapper = LogTextStreamItemField.extend.attrs<{
hasHighlights: boolean;
isHovered: boolean;
isHighlighted: boolean;
isWrapped?: boolean;
}>({})`
flex-grow: 1;
@ -104,7 +107,7 @@ const LogTextStreamItemMessageFieldWrapper = LogTextStreamItemField.extend.attrs
background-color: ${props => props.theme.eui.euiColorEmptyShade};
${props => (props.hasHighlights ? highlightedFieldStyle : '')};
${props => (props.isHovered ? hoveredFieldStyle : '')};
${props => (props.isHovered || props.isHighlighted ? hoveredFieldStyle : '')};
${props => (props.isWrapped ? wrappedFieldStyle : unwrappedFieldStyle)};
`;

View file

@ -15,10 +15,11 @@ interface StreamItemProps {
scale: TextScale;
wrap: boolean;
openFlyoutWithItem: (id: string) => void;
isHighlighted: boolean;
}
export const LogTextStreamItemView = React.forwardRef<Element, StreamItemProps>(
({ item, scale, wrap, openFlyoutWithItem }, ref) => {
({ item, scale, wrap, openFlyoutWithItem, isHighlighted }, ref) => {
switch (item.kind) {
case 'logEntry':
return (
@ -29,6 +30,7 @@ export const LogTextStreamItemView = React.forwardRef<Element, StreamItemProps>(
scale={scale}
wrap={wrap}
openFlyoutWithItem={openFlyoutWithItem}
isHighlighted={isHighlighted}
/>
);
}

View file

@ -25,6 +25,7 @@ interface LogTextStreamLogEntryItemViewProps {
wrap: boolean;
openFlyoutWithItem: (id: string) => void;
intl: InjectedIntl;
isHighlighted: boolean;
}
interface LogTextStreamLogEntryItemViewState {
@ -57,7 +58,15 @@ export const LogTextStreamLogEntryItemView = injectI18n(
};
public render() {
const { intl, boundingBoxRef, logEntry, scale, searchResult, wrap } = this.props;
const {
intl,
boundingBoxRef,
logEntry,
scale,
searchResult,
wrap,
isHighlighted,
} = this.props;
const { isHovered } = this.state;
const viewDetailsLabel = intl.formatMessage({
id: 'xpack.infra.logEntryItemView.viewDetailsToolTip',
@ -76,11 +85,12 @@ export const LogTextStreamLogEntryItemView = injectI18n(
<LogTextStreamItemDateField
hasHighlights={!!searchResult}
isHovered={isHovered}
isHighlighted={isHighlighted}
scale={scale}
>
<FormattedTime time={logEntry.fields.time} />
</LogTextStreamItemDateField>
<LogTextStreamIconDiv isHovered={isHovered}>
<LogTextStreamIconDiv isHovered={isHovered} isHighlighted={isHighlighted}>
{isHovered ? (
<EuiToolTip content={viewDetailsLabel}>
<EuiButtonIcon
@ -96,6 +106,7 @@ export const LogTextStreamLogEntryItemView = injectI18n(
<LogTextStreamItemMessageField
highlights={searchResult ? searchResult.matches.message || [] : []}
isHovered={isHovered}
isHighlighted={isHighlighted}
isWrapped={wrap}
scale={scale}
>
@ -109,6 +120,7 @@ export const LogTextStreamLogEntryItemView = injectI18n(
interface IconProps {
isHovered: boolean;
isHighlighted: boolean;
}
const EmptyIcon = euiStyled.div`
@ -118,7 +130,7 @@ const EmptyIcon = euiStyled.div`
const LogTextStreamIconDiv = euiStyled<IconProps, 'div'>('div')`
flex-grow: 0;
background-color: ${props =>
props.isHovered
props.isHovered || props.isHighlighted
? props.theme.darkMode
? transparentize(0.9, darken(0.05, props.theme.eui.euiColorHighlight))
: darken(0.05, props.theme.eui.euiColorHighlight)

View file

@ -43,8 +43,9 @@ interface ScrollableLogTextStreamViewProps {
) => any;
loadNewerItems: () => void;
setFlyoutItem: (id: string) => void;
showFlyout: () => void;
setFlyoutVisibility: (visible: boolean) => void;
intl: InjectedIntl;
highlightedItem: string | null;
}
interface ScrollableLogTextStreamViewState {
@ -102,6 +103,7 @@ class ScrollableLogTextStreamViewClass extends React.PureComponent<
isStreaming,
lastLoadedTime,
intl,
highlightedItem,
} = this.props;
const { targetId } = this.state;
const hasItems = items.length > 0;
@ -169,6 +171,9 @@ class ScrollableLogTextStreamViewClass extends React.PureComponent<
item={item}
scale={scale}
wrap={wrap}
isHighlighted={
highlightedItem ? item.logEntry.gid === highlightedItem : false
}
/>
)}
</MeasurableItemView>
@ -190,7 +195,7 @@ class ScrollableLogTextStreamViewClass extends React.PureComponent<
private handleOpenFlyout = (id: string) => {
this.props.setFlyoutItem(id);
this.props.showFlyout();
this.props.setFlyoutVisibility(true);
};
private handleReload = () => {

View file

@ -5,6 +5,7 @@
*/
import gql from 'graphql-tag';
import { sharedFragments } from '../../../common/graphql/shared';
export const flyoutItemQuery = gql`
query FlyoutItemQuery($sourceId: ID!, $itemId: ID!) {
@ -13,6 +14,9 @@ export const flyoutItemQuery = gql`
logItem(id: $itemId) {
id
index
key {
...InfraTimeKeyFields
}
fields {
field
value
@ -20,4 +24,6 @@ export const flyoutItemQuery = gql`
}
}
}
${sharedFragments.InfraTimeKey}
`;

View file

@ -0,0 +1,171 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import createContainer from 'constate-latest';
import { isString } from 'lodash';
import React, { useContext, useEffect, useMemo, useState } from 'react';
import { FlyoutItemQuery, InfraLogItem } from '../../graphql/types';
import { useApolloClient } from '../../utils/apollo_context';
import { UrlStateContainer } from '../../utils/url_state';
import { useTrackedPromise } from '../../utils/use_tracked_promise';
import { flyoutItemQuery } from './flyout_item.gql_query';
export enum FlyoutVisibility {
hidden = 'hidden',
visible = 'visible',
}
interface FlyoutOptionsUrlState {
flyoutId?: string | null;
flyoutVisibility?: string | null;
surroundingLogsId?: string | null;
}
export const useLogFlyout = ({ sourceId }: { sourceId: string }) => {
const [flyoutVisible, setFlyoutVisibility] = useState<boolean>(false);
const [flyoutId, setFlyoutId] = useState<string | null>(null);
const [flyoutItem, setFlyoutItem] = useState<InfraLogItem | null>(null);
const [surroundingLogsId, setSurroundingLogsId] = useState<string | null>(null);
const apolloClient = useApolloClient();
const [loadFlyoutItemRequest, loadFlyoutItem] = useTrackedPromise(
{
cancelPreviousOn: 'creation',
createPromise: async () => {
if (!apolloClient) {
throw new Error('Failed to load flyout item: No apollo client available.');
}
if (!flyoutId) {
return;
}
return await apolloClient.query<FlyoutItemQuery.Query, FlyoutItemQuery.Variables>({
fetchPolicy: 'no-cache',
query: flyoutItemQuery,
variables: {
itemId: flyoutId,
sourceId,
},
});
},
onResolve: response => {
if (response) {
const { data } = response;
setFlyoutItem((data && data.source && data.source.logItem) || null);
}
},
},
[apolloClient, sourceId, flyoutId]
);
const isLoading = useMemo(
() => {
return loadFlyoutItemRequest.state === 'pending';
},
[loadFlyoutItemRequest.state]
);
useEffect(
() => {
if (flyoutId) {
loadFlyoutItem();
}
},
[loadFlyoutItem, flyoutId]
);
return {
flyoutVisible,
setFlyoutVisibility,
flyoutId,
setFlyoutId,
surroundingLogsId,
setSurroundingLogsId,
isLoading,
flyoutItem,
};
};
export const LogFlyout = createContainer(useLogFlyout);
export const WithFlyoutOptionsUrlState = () => {
const {
flyoutVisible,
setFlyoutVisibility,
flyoutId,
setFlyoutId,
surroundingLogsId,
setSurroundingLogsId,
} = useContext(LogFlyout.Context);
return (
<UrlStateContainer
urlState={{
flyoutVisibility: flyoutVisible ? FlyoutVisibility.visible : FlyoutVisibility.hidden,
flyoutId,
surroundingLogsId,
}}
urlStateKey="flyoutOptions"
mapToUrlState={mapToUrlState}
onChange={newUrlState => {
if (newUrlState && newUrlState.flyoutId) {
setFlyoutId(newUrlState.flyoutId);
}
if (newUrlState && newUrlState.surroundingLogsId) {
setSurroundingLogsId(newUrlState.surroundingLogsId);
}
if (newUrlState && newUrlState.flyoutVisibility === FlyoutVisibility.visible) {
setFlyoutVisibility(true);
}
if (newUrlState && newUrlState.flyoutVisibility === FlyoutVisibility.hidden) {
setFlyoutVisibility(false);
}
}}
onInitialize={initialUrlState => {
if (initialUrlState && initialUrlState.flyoutId) {
setFlyoutId(initialUrlState.flyoutId);
}
if (initialUrlState && initialUrlState.surroundingLogsId) {
setSurroundingLogsId(initialUrlState.surroundingLogsId);
}
if (initialUrlState && initialUrlState.flyoutVisibility === FlyoutVisibility.visible) {
setFlyoutVisibility(true);
}
if (initialUrlState && initialUrlState.flyoutVisibility === FlyoutVisibility.hidden) {
setFlyoutVisibility(false);
}
}}
/>
);
};
const mapToUrlState = (value: any): FlyoutOptionsUrlState | undefined =>
value
? {
flyoutId: mapToFlyoutIdState(value.flyoutId),
flyoutVisibility: mapToFlyoutVisibilityState(value.flyoutVisibility),
surroundingLogsId: mapToSurroundingLogsIdState(value.surroundingLogsId),
}
: undefined;
const mapToFlyoutIdState = (subject: any) => {
return subject && isString(subject) ? subject : undefined;
};
const mapToSurroundingLogsIdState = (subject: any) => {
return subject && isString(subject) ? subject : undefined;
};
const mapToFlyoutVisibilityState = (subject: any) => {
if (subject) {
if (subject === 'visible') {
return FlyoutVisibility.visible;
}
if (subject === 'hidden') {
return FlyoutVisibility.hidden;
}
}
};

View file

@ -1,56 +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;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { Query } from 'react-apollo';
import { FlyoutItemQuery, InfraLogItem } from '../../graphql/types';
import { FlyoutVisibility } from '../../store/local/log_flyout';
import { flyoutItemQuery } from './flyout_item.gql_query';
import { WithFlyoutOptions } from './with_log_flyout_options';
interface WithFlyoutArgs {
flyoutItem: InfraLogItem | null;
setFlyoutItem: (id: string) => void;
showFlyout: () => void;
hideFlyout: () => void;
error?: string | undefined;
loading: boolean;
}
interface WithFlyoutProps {
children: (args: WithFlyoutArgs) => React.ReactNode;
sourceId: string;
}
export const WithLogFlyout = ({ children, sourceId }: WithFlyoutProps) => {
return (
<WithFlyoutOptions>
{({ showFlyout, hideFlyout, setFlyoutItem, flyoutId, flyoutVisibility }) =>
flyoutVisibility === FlyoutVisibility.visible ? (
<Query<FlyoutItemQuery.Query, FlyoutItemQuery.Variables>
query={flyoutItemQuery}
fetchPolicy="no-cache"
variables={{
itemId: (flyoutId != null && flyoutId) || '',
sourceId,
}}
>
{({ data, error, loading }) => {
return children({
showFlyout,
hideFlyout,
setFlyoutItem,
flyoutItem: (data && data.source && data.source.logItem) || null,
error: error && error.message,
loading,
});
}}
</Query>
) : null
}
</WithFlyoutOptions>
);
};

View file

@ -1,105 +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;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { isString } from 'lodash';
import { flyoutOptionsActions, flyoutOptionsSelectors, State } from '../../store';
import { FlyoutVisibility } from '../../store/local/log_flyout';
import { asChildFunctionRenderer } from '../../utils/typed_react';
import { bindPlainActionCreators } from '../../utils/typed_redux';
import { UrlStateContainer } from '../../utils/url_state';
const selectOptionsUrlState = createSelector(
flyoutOptionsSelectors.selectFlyoutId,
flyoutOptionsSelectors.selectFlyoutVisibility,
(flyoutId, flyoutVisibility) => ({
flyoutVisibility,
flyoutId,
})
);
export const withFlyoutOptions = connect(
(state: State) => ({
flyoutVisibility: flyoutOptionsSelectors.selectFlyoutVisibility(state),
flyoutId: flyoutOptionsSelectors.selectFlyoutId(state),
urlState: selectOptionsUrlState(state),
}),
bindPlainActionCreators({
setFlyoutItem: flyoutOptionsActions.setFlyoutItem,
showFlyout: flyoutOptionsActions.showFlyout,
hideFlyout: flyoutOptionsActions.hideFlyout,
})
);
export const WithFlyoutOptions = asChildFunctionRenderer(withFlyoutOptions);
/**
* Url State
*/
interface FlyoutOptionsUrlState {
flyoutId?: ReturnType<typeof flyoutOptionsSelectors.selectFlyoutId>;
flyoutVisibility?: ReturnType<typeof flyoutOptionsSelectors.selectFlyoutVisibility>;
}
export const WithFlyoutOptionsUrlState = () => (
<WithFlyoutOptions>
{({ setFlyoutItem, showFlyout, hideFlyout, urlState }) => (
<UrlStateContainer
urlState={urlState}
urlStateKey="flyoutOptions"
mapToUrlState={mapToUrlState}
onChange={newUrlState => {
if (newUrlState && newUrlState.flyoutId) {
setFlyoutItem(newUrlState.flyoutId);
}
if (newUrlState && newUrlState.flyoutVisibility === FlyoutVisibility.visible) {
showFlyout();
}
if (newUrlState && newUrlState.flyoutVisibility === FlyoutVisibility.hidden) {
hideFlyout();
}
}}
onInitialize={initialUrlState => {
if (initialUrlState && initialUrlState.flyoutId) {
setFlyoutItem(initialUrlState.flyoutId);
}
if (initialUrlState && initialUrlState.flyoutVisibility === FlyoutVisibility.visible) {
showFlyout();
}
if (initialUrlState && initialUrlState.flyoutVisibility === FlyoutVisibility.hidden) {
hideFlyout();
}
}}
/>
)}
</WithFlyoutOptions>
);
const mapToUrlState = (value: any): FlyoutOptionsUrlState | undefined =>
value
? {
flyoutId: mapToFlyoutIdState(value.flyoutId),
flyoutVisibility: mapToFlyoutVisibilityState(value.flyoutVisibility),
}
: undefined;
const mapToFlyoutIdState = (subject: any) => {
return subject && isString(subject) ? subject : undefined;
};
const mapToFlyoutVisibilityState = (subject: any) => {
if (subject) {
if (subject === 'visible') {
return FlyoutVisibility.visible;
}
if (subject === 'hidden') {
return FlyoutVisibility.hidden;
}
}
};

View file

@ -1382,6 +1382,18 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "key",
"description": "Time key for the document - derived from the source configuration timestamp and tiebreaker settings",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": { "kind": "OBJECT", "name": "InfraTimeKey", "ofType": null }
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "fields",
"description": "An array of flattened fields and values",

View file

@ -186,6 +186,8 @@ export interface InfraLogItem {
id: string;
/** The index where the document was found */
index: string;
/** Time key for the document - derived from the source configuration timestamp and tiebreaker settings */
key: InfraTimeKey;
/** An array of flattened fields and values */
fields: InfraLogItemField[];
}
@ -551,9 +553,19 @@ export namespace FlyoutItemQuery {
index: string;
key: Key;
fields: Fields[];
};
export type Key = {
__typename?: 'InfraTimeKey';
time: number;
tiebreaker: number;
};
export type Fields = {
__typename?: 'InfraLogItemField';

View file

@ -16,9 +16,10 @@ import { PageContent } from '../../components/page';
import { WithSummary } from '../../containers/logs/log_summary';
import { LogViewConfiguration } from '../../containers/logs/log_view_configuration';
import { WithLogFilter, WithLogFilterUrlState } from '../../containers/logs/with_log_filter';
import { WithLogFlyout } from '../../containers/logs/with_log_flyout';
import { WithFlyoutOptionsUrlState } from '../../containers/logs/with_log_flyout_options';
import { WithFlyoutOptions } from '../../containers/logs/with_log_flyout_options';
import {
LogFlyout as LogFlyoutState,
WithFlyoutOptionsUrlState,
} from '../../containers/logs/log_flyout';
import { WithLogMinimapUrlState } from '../../containers/logs/with_log_minimap';
import { WithLogPositionUrlState } from '../../containers/logs/with_log_position';
import { WithLogPosition } from '../../containers/logs/with_log_position';
@ -29,8 +30,17 @@ import { Source } from '../../containers/source';
import { LogsToolbar } from './page_toolbar';
export const LogsPageLogsContent: React.FunctionComponent = () => {
const { derivedIndexPattern, sourceId } = useContext(Source.Context);
const { derivedIndexPattern } = useContext(Source.Context);
const { intervalSize, textScale, textWrap } = useContext(LogViewConfiguration.Context);
const {
setFlyoutVisibility,
flyoutVisible,
setFlyoutId,
surroundingLogsId,
setSurroundingLogsId,
flyoutItem,
isLoading,
} = useContext(LogFlyoutState.Context);
return (
<>
@ -40,22 +50,29 @@ export const LogsPageLogsContent: React.FunctionComponent = () => {
<WithLogTextviewUrlState />
<WithFlyoutOptionsUrlState />
<LogsToolbar />
<WithLogFilter indexPattern={derivedIndexPattern}>
{({ applyFilterQueryFromKueryExpression }) => (
<WithLogFlyout sourceId={sourceId}>
{({ flyoutItem, hideFlyout, loading }) => (
<LogFlyout
setFilter={applyFilterQueryFromKueryExpression}
flyoutItem={flyoutItem}
hideFlyout={hideFlyout}
loading={loading}
/>
)}
</WithLogFlyout>
<WithLogPosition>
{({ jumpToTargetPosition, stopLiveStreaming }) => (
<WithLogFilter indexPattern={derivedIndexPattern}>
{({ applyFilterQueryFromKueryExpression }) =>
flyoutVisible ? (
<LogFlyout
setFilter={applyFilterQueryFromKueryExpression}
setTarget={(timeKey, flyoutItemId) => {
jumpToTargetPosition(timeKey);
setSurroundingLogsId(flyoutItemId);
stopLiveStreaming();
}}
setFlyoutVisibility={setFlyoutVisibility}
flyoutItem={flyoutItem}
loading={isLoading}
/>
) : null
}
</WithLogFilter>
)}
</WithLogFilter>
<WithFlyoutOptions>
{({ showFlyout, setFlyoutItem }) => (
</WithLogPosition>
<WithLogFilter indexPattern={derivedIndexPattern}>
{({ filterQuery }) => (
<PageContent>
<AutoSizer content>
{({ measureRef, content: { width = 0, height = 0 } }) => (
@ -93,8 +110,9 @@ export const LogsPageLogsContent: React.FunctionComponent = () => {
target={targetPosition}
width={width}
wrap={textWrap}
setFlyoutItem={setFlyoutItem}
showFlyout={showFlyout}
setFlyoutItem={setFlyoutId}
setFlyoutVisibility={setFlyoutVisibility}
highlightedItem={surroundingLogsId ? surroundingLogsId : null}
/>
)}
</WithStreamItems>
@ -130,7 +148,7 @@ export const LogsPageLogsContent: React.FunctionComponent = () => {
</AutoSizer>
</PageContent>
)}
</WithFlyoutOptions>
</WithLogFilter>
</>
);
};

View file

@ -7,13 +7,16 @@
import React from 'react';
import { SourceConfigurationFlyoutState } from '../../components/source_configuration';
import { LogFlyout } from '../../containers/logs/log_flyout';
import { LogViewConfiguration } from '../../containers/logs/log_view_configuration';
import { Source } from '../../containers/source';
export const LogsPageProviders: React.FunctionComponent = ({ children }) => (
<Source.Provider sourceId="default">
<SourceConfigurationFlyoutState.Provider>
<LogViewConfiguration.Provider>{children}</LogViewConfiguration.Provider>
<LogViewConfiguration.Provider>
<LogFlyout.Provider sourceId="default">{children}</LogFlyout.Provider>
</LogViewConfiguration.Provider>
</SourceConfigurationFlyoutState.Provider>
</Source.Provider>
);

View file

@ -16,6 +16,7 @@ import { LogTextScaleControls } from '../../components/logging/log_text_scale_co
import { LogTextWrapControls } from '../../components/logging/log_text_wrap_controls';
import { LogTimeControls } from '../../components/logging/log_time_controls';
import { SourceConfigurationButton } from '../../components/source_configuration';
import { LogFlyout } from '../../containers/logs/log_flyout';
import { LogViewConfiguration } from '../../containers/logs/log_view_configuration';
import { WithLogFilter } from '../../containers/logs/with_log_filter';
import { WithLogPosition } from '../../containers/logs/with_log_position';
@ -35,6 +36,7 @@ export const LogsToolbar = injectI18n(({ intl }) => {
textWrap,
} = useContext(LogViewConfiguration.Context);
const { setSurroundingLogsId } = useContext(LogFlyout.Context);
return (
<Toolbar>
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween" gutterSize="s">
@ -52,8 +54,14 @@ export const LogsToolbar = injectI18n(({ intl }) => {
isLoadingSuggestions={isLoadingSuggestions}
isValid={isFilterQueryDraftValid}
loadSuggestions={loadSuggestions}
onChange={setFilterQueryDraftFromKueryExpression}
onSubmit={applyFilterQueryFromKueryExpression}
onChange={(expression: string) => {
setSurroundingLogsId(null);
setFilterQueryDraftFromKueryExpression(expression);
}}
onSubmit={(expression: string) => {
setSurroundingLogsId(null);
applyFilterQueryFromKueryExpression(expression);
}}
placeholder={intl.formatMessage({
id: 'xpack.infra.logsPage.toolbar.kqlSearchFieldPlaceholder',
defaultMessage: 'Search for log entries… (e.g. host.name:host-1)',
@ -97,7 +105,10 @@ export const LogsToolbar = injectI18n(({ intl }) => {
currentTime={visibleMidpointTime}
isLiveStreaming={isAutoReloading}
jumpToTime={jumpToTargetPositionTime}
startLiveStreaming={startLiveStreaming}
startLiveStreaming={interval => {
startLiveStreaming(interval);
setSurroundingLogsId(null);
}}
stopLiveStreaming={stopLiveStreaming}
/>
)}

View file

@ -10,6 +10,5 @@ export {
waffleFilterActions,
waffleTimeActions,
waffleOptionsActions,
flyoutOptionsActions,
} from './local';
export { logEntriesActions } from './remote';

View file

@ -9,4 +9,3 @@ export { logPositionActions } from './log_position';
export { waffleFilterActions } from './waffle_filter';
export { waffleTimeActions } from './waffle_time';
export { waffleOptionsActions } from './waffle_options';
export { flyoutOptionsActions } from './log_flyout';

View file

@ -1,11 +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;
* you may not use this file except in compliance with the Elastic License.
*/
import actionCreatorFactory from 'typescript-fsa';
const actionCreator = actionCreatorFactory('x-pack/infra/local/log_flyout');
export const setFlyoutItem = actionCreator<string>('SET_FLYOUT_ITEM');
export const showFlyout = actionCreator('SHOW_FLYOUT');
export const hideFlyout = actionCreator('HIDE_FLYOUT');

View file

@ -1,11 +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;
* you may not use this file except in compliance with the Elastic License.
*/
import * as flyoutOptionsActions from './actions';
import * as flyoutOptionsSelectors from './selector';
export { flyoutOptionsActions, flyoutOptionsSelectors };
export * from './reducer';

View file

@ -1,39 +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;
* you may not use this file except in compliance with the Elastic License.
*/
import { combineReducers } from 'redux';
import { reducerWithInitialState } from 'typescript-fsa-reducers';
import { hideFlyout, setFlyoutItem, showFlyout } from './actions';
export enum FlyoutVisibility {
hidden = 'hidden',
visible = 'visible',
}
export interface FlyoutOptionsState {
visibility: FlyoutVisibility;
itemId: string;
}
export const initialFlyoutOptionsState: FlyoutOptionsState = {
visibility: FlyoutVisibility.hidden,
itemId: '',
};
const currentFlyoutReducer = reducerWithInitialState(initialFlyoutOptionsState.itemId).case(
setFlyoutItem,
(current, target) => target
);
const currentFlyoutVisibilityReducer = reducerWithInitialState(initialFlyoutOptionsState.visibility)
.case(hideFlyout, () => FlyoutVisibility.hidden)
.case(showFlyout, () => FlyoutVisibility.visible);
export const flyoutOptionsReducer = combineReducers<FlyoutOptionsState>({
itemId: currentFlyoutReducer,
visibility: currentFlyoutVisibilityReducer,
});

View file

@ -1,10 +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;
* you may not use this file except in compliance with the Elastic License.
*/
import { FlyoutOptionsState } from './reducer';
export const selectFlyoutId = (state: FlyoutOptionsState) => state.itemId;
export const selectFlyoutVisibility = (state: FlyoutOptionsState) => state.visibility;

View file

@ -7,7 +7,6 @@
import { combineReducers } from 'redux';
import { initialLogFilterState, logFilterReducer, LogFilterState } from './log_filter';
import { flyoutOptionsReducer, FlyoutOptionsState, initialFlyoutOptionsState } from './log_flyout';
import { initialLogPositionState, logPositionReducer, LogPositionState } from './log_position';
import { initialWaffleFilterState, waffleFilterReducer, WaffleFilterState } from './waffle_filter';
import {
@ -23,7 +22,6 @@ export interface LocalState {
waffleFilter: WaffleFilterState;
waffleTime: WaffleTimeState;
waffleMetrics: WaffleOptionsState;
logFlyout: FlyoutOptionsState;
}
export const initialLocalState: LocalState = {
@ -32,7 +30,6 @@ export const initialLocalState: LocalState = {
waffleFilter: initialWaffleFilterState,
waffleTime: initialWaffleTimeState,
waffleMetrics: initialWaffleOptionsState,
logFlyout: initialFlyoutOptionsState,
};
export const localReducer = combineReducers<LocalState>({
@ -41,5 +38,4 @@ export const localReducer = combineReducers<LocalState>({
waffleFilter: waffleFilterReducer,
waffleTime: waffleTimeReducer,
waffleMetrics: waffleOptionsReducer,
logFlyout: flyoutOptionsReducer,
});

View file

@ -6,7 +6,6 @@
import { globalizeSelectors } from '../../utils/typed_redux';
import { logFilterSelectors as innerLogFilterSelectors } from './log_filter';
import { flyoutOptionsSelectors as innerFlyoutOptionsSelectors } from './log_flyout';
import { logPositionSelectors as innerLogPositionSelectors } from './log_position';
import { LocalState } from './reducer';
import { waffleFilterSelectors as innerWaffleFilterSelectors } from './waffle_filter';
@ -37,8 +36,3 @@ export const waffleOptionsSelectors = globalizeSelectors(
(state: LocalState) => state.waffleMetrics,
innerWaffleOptionsSelectors
);
export const flyoutOptionsSelectors = globalizeSelectors(
(state: LocalState) => state.logFlyout,
innerFlyoutOptionsSelectors
);

View file

@ -9,7 +9,6 @@ import { createSelector } from 'reselect';
import { getLogEntryAtTime } from '../utils/log_entry';
import { globalizeSelectors } from '../utils/typed_redux';
import {
flyoutOptionsSelectors as localFlyoutOptionsSelectors,
logFilterSelectors as localLogFilterSelectors,
logPositionSelectors as localLogPositionSelectors,
waffleFilterSelectors as localWaffleFilterSelectors,
@ -30,7 +29,6 @@ export const logPositionSelectors = globalizeSelectors(selectLocal, localLogPosi
export const waffleFilterSelectors = globalizeSelectors(selectLocal, localWaffleFilterSelectors);
export const waffleTimeSelectors = globalizeSelectors(selectLocal, localWaffleTimeSelectors);
export const waffleOptionsSelectors = globalizeSelectors(selectLocal, localWaffleOptionsSelectors);
export const flyoutOptionsSelectors = globalizeSelectors(selectLocal, localFlyoutOptionsSelectors);
/**
* remote selectors

View file

@ -90,6 +90,8 @@ export const logEntriesSchema = gql`
id: ID!
"The index where the document was found"
index: String!
"Time key for the document - derived from the source configuration timestamp and tiebreaker settings"
key: InfraTimeKey!
"An array of flattened fields and values"
fields: [InfraLogItemField!]!
}

View file

@ -214,6 +214,8 @@ export interface InfraLogItem {
id: string;
/** The index where the document was found */
index: string;
/** Time key for the document - derived from the source configuration timestamp and tiebreaker settings */
key: InfraTimeKey;
/** An array of flattened fields and values */
fields: InfraLogItemField[];
}
@ -1170,6 +1172,8 @@ export namespace InfraLogItemResolvers {
id?: IdResolver<string, TypeParent, Context>;
/** The index where the document was found */
index?: IndexResolver<string, TypeParent, Context>;
/** Time key for the document - derived from the source configuration timestamp and tiebreaker settings */
key?: KeyResolver<InfraTimeKey, TypeParent, Context>;
/** An array of flattened fields and values */
fields?: FieldsResolver<InfraLogItemField[], TypeParent, Context>;
}
@ -1184,6 +1188,11 @@ export namespace InfraLogItemResolvers {
Parent,
Context
>;
export type KeyResolver<
R = InfraTimeKey,
Parent = InfraLogItem,
Context = InfraContext
> = Resolver<R, Parent, Context>;
export type FieldsResolver<
R = InfraLogItemField[],
Parent = InfraLogItem,

View file

@ -34,6 +34,7 @@ interface LogItemHit {
_index: string;
_id: string;
_source: JsonObject;
sort: [number, number];
}
export class InfraKibanaLogEntriesAdapter implements LogEntriesAdapter {
@ -173,6 +174,10 @@ export class InfraKibanaLogEntriesAdapter implements LogEntriesAdapter {
terminate_after: 1,
body: {
size: 1,
sort: [
{ [sourceConfiguration.fields.timestamp]: 'desc' },
{ [sourceConfiguration.fields.tiebreaker]: 'desc' },
],
query: {
ids: {
values: [id],

View file

@ -135,9 +135,14 @@ export class InfraLogEntriesDomain {
{ field: '_index', value: document._index },
{ field: '_id', value: document._id },
];
return {
id: document._id,
index: document._index,
key: {
time: document.sort[0],
tiebreaker: document.sort[1],
},
fields: sortBy(
[...defaultFields, ...convertDocumentSourceToLogItemFields(document._source)],
'field'
@ -150,6 +155,7 @@ interface LogItemHit {
_index: string;
_id: string;
_source: JsonObject;
sort: [number, number];
}
export interface LogEntriesAdapter {