mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Log UI] Flyout for Log Events (#28885)
* Adding flyout to log viewer * Adding filtering * Fixing typescript errors * Adding a test for graphql; fixing test data for 7.0.0 * Adding terminate_after:1 to logItem request * fixing test data * Switching back to old data * Fixing data for tests * Adding i18n translations * changing label from add to set * Make flyout call more robust; fixing typings * Adding loading screen to flyout * Fixing linting errors * Update x-pack/plugins/infra/public/components/logging/log_flyout.tsx Co-Authored-By: simianhacker <chris@chriscowan.us> * Fixing visible mis-spelling * Fixing types * Change withLogFlyout to be conditional; Add icon instead of onClick for flyout * Adding dark mode support * Adding user-select:none to icon div * Removing remnants of a failed experiment * Adding aria-label to view details button * Fixing padding on date element * Removing unused variable that somehow got past the linters * Fixing empty_kibana * Fixing data for infra * Fixing merge weirdness
This commit is contained in:
parent
caa31170d3
commit
7e9315a528
38 changed files with 1171 additions and 57 deletions
114
x-pack/plugins/infra/public/components/logging/log_flyout.tsx
Normal file
114
x-pack/plugins/infra/public/components/logging/log_flyout.tsx
Normal file
|
@ -0,0 +1,114 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiBasicTable,
|
||||
EuiButtonIcon,
|
||||
EuiFlyout,
|
||||
EuiFlyoutBody,
|
||||
EuiFlyoutHeader,
|
||||
EuiTitle,
|
||||
EuiToolTip,
|
||||
} from '@elastic/eui';
|
||||
import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { InfraLogItem, InfraLogItemField } from '../../graphql/types';
|
||||
import { InfraLoadingPanel } from '../loading';
|
||||
interface Props {
|
||||
flyoutItem: InfraLogItem | null;
|
||||
hideFlyout: () => void;
|
||||
setFilter: (filter: string) => void;
|
||||
intl: InjectedIntl;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
export const LogFlyout = injectI18n(
|
||||
({ flyoutItem, loading, hideFlyout, setFilter, intl }: Props) => {
|
||||
const handleFilter = (field: InfraLogItemField) => () => {
|
||||
const filter = `${field.field}:"${field.value}"`;
|
||||
setFilter(filter);
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
field: 'field',
|
||||
name: intl.formatMessage({
|
||||
defaultMessage: 'Field',
|
||||
id: 'xpack.infra.logFlyout.fieldColumnLabel',
|
||||
}),
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
field: 'value',
|
||||
name: intl.formatMessage({
|
||||
defaultMessage: 'Value',
|
||||
id: 'xpack.infra.logFlyout.valueColumnLabel',
|
||||
}),
|
||||
sortable: true,
|
||||
render: (name: string, item: InfraLogItemField) => (
|
||||
<span>
|
||||
<EuiToolTip
|
||||
content={intl.formatMessage({
|
||||
id: 'xpack.infra.logFlyout.setFilterTooltip',
|
||||
defaultMessage: 'Set Filter',
|
||||
})}
|
||||
>
|
||||
<EuiButtonIcon
|
||||
color="text"
|
||||
iconType="filter"
|
||||
aria-label={intl.formatMessage({
|
||||
id: 'xpack.infra.logFlyout.filterAriaLabel',
|
||||
defaultMessage: 'Filter',
|
||||
})}
|
||||
onClick={handleFilter(item)}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
{item.value}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
];
|
||||
return (
|
||||
<EuiFlyout onClose={() => hideFlyout()} size="m">
|
||||
<EuiFlyoutHeader hasBorder>
|
||||
<EuiTitle size="s">
|
||||
<h3 id="flyoutTitle">
|
||||
<FormattedMessage
|
||||
defaultMessage="Log event document details"
|
||||
id="xpack.infra.logFlyout.flyoutTitle"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>
|
||||
{loading || flyoutItem === null ? (
|
||||
<InfraFlyoutLoadingPanel>
|
||||
<InfraLoadingPanel
|
||||
height="100%"
|
||||
width="100%"
|
||||
text={intl.formatMessage({
|
||||
id: 'xpack.infra.logFlyout.loadingMessage',
|
||||
defaultMessage: 'Loading Event',
|
||||
})}
|
||||
/>
|
||||
</InfraFlyoutLoadingPanel>
|
||||
) : (
|
||||
<EuiBasicTable columns={columns} items={flyoutItem.fields} />
|
||||
)}
|
||||
</EuiFlyoutBody>
|
||||
</EuiFlyout>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export const InfraFlyoutLoadingPanel = styled.div`
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
`;
|
|
@ -64,6 +64,7 @@ const LogTextStreamItemDateFieldWrapper = LogTextStreamItemField.extend.attrs<{
|
|||
border-right: solid 2px ${props => props.theme.eui.euiColorLightShade};
|
||||
color: ${props => props.theme.eui.euiColorDarkShade};
|
||||
white-space: pre;
|
||||
padding: 0 ${props => props.theme.eui.paddingSizes.l};
|
||||
|
||||
${props => (props.hasHighlights ? highlightedFieldStyle : '')};
|
||||
${props => (props.isHovered ? hoveredFieldStyle : '')};
|
||||
|
|
|
@ -19,5 +19,5 @@ export const LogTextStreamItemField = styled.div.attrs<{
|
|||
[switchProp.default]: props.theme.eui.euiFontSize,
|
||||
})};
|
||||
line-height: ${props => props.theme.eui.euiLineHeight};
|
||||
padding: 2px ${props => props.theme.eui.euiSize};
|
||||
padding: 2px ${props => props.theme.eui.euiSize} 2px 0;
|
||||
`;
|
||||
|
|
|
@ -105,7 +105,6 @@ const LogTextStreamItemMessageFieldWrapper = LogTextStreamItemField.extend.attrs
|
|||
|
||||
const HighlightSpan = styled.span`
|
||||
display: inline-block;
|
||||
padding: 0 ${props => props.theme.eui.euiSizeXs};
|
||||
background-color: ${props => props.theme.eui.euiColorSecondary};
|
||||
color: ${props => props.theme.eui.euiColorGhost};
|
||||
font-weight: ${props => props.theme.eui.euiFontWeightMedium};
|
||||
|
|
|
@ -14,10 +14,11 @@ interface StreamItemProps {
|
|||
item: StreamItem;
|
||||
scale: TextScale;
|
||||
wrap: boolean;
|
||||
openFlyoutWithItem: (id: string) => void;
|
||||
}
|
||||
|
||||
export const LogTextStreamItemView = React.forwardRef<Element, StreamItemProps>(
|
||||
({ item, scale, wrap }, ref) => {
|
||||
({ item, scale, wrap, openFlyoutWithItem }, ref) => {
|
||||
switch (item.kind) {
|
||||
case 'logEntry':
|
||||
return (
|
||||
|
@ -27,6 +28,7 @@ export const LogTextStreamItemView = React.forwardRef<Element, StreamItemProps>(
|
|||
searchResult={item.searchResult}
|
||||
scale={scale}
|
||||
wrap={wrap}
|
||||
openFlyoutWithItem={openFlyoutWithItem}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -4,9 +4,12 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { darken, transparentize } from 'polished';
|
||||
import * as React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { EuiButtonIcon, EuiToolTip } from '@elastic/eui';
|
||||
import { InjectedIntl, injectI18n } from '@kbn/i18n/react';
|
||||
import { LogEntry } from '../../../../common/log_entry';
|
||||
import { SearchResult } from '../../../../common/log_search_result';
|
||||
import { TextScale } from '../../../../common/log_text_scale';
|
||||
|
@ -20,65 +23,110 @@ interface LogTextStreamLogEntryItemViewProps {
|
|||
searchResult?: SearchResult;
|
||||
scale: TextScale;
|
||||
wrap: boolean;
|
||||
openFlyoutWithItem: (id: string) => void;
|
||||
intl: InjectedIntl;
|
||||
}
|
||||
|
||||
interface LogTextStreamLogEntryItemViewState {
|
||||
isHovered: boolean;
|
||||
}
|
||||
|
||||
export class LogTextStreamLogEntryItemView extends React.PureComponent<
|
||||
LogTextStreamLogEntryItemViewProps,
|
||||
LogTextStreamLogEntryItemViewState
|
||||
> {
|
||||
public readonly state = {
|
||||
isHovered: false,
|
||||
};
|
||||
|
||||
public handleMouseEnter: React.MouseEventHandler<HTMLDivElement> = () => {
|
||||
this.setState({
|
||||
isHovered: true,
|
||||
});
|
||||
};
|
||||
|
||||
public handleMouseLeave: React.MouseEventHandler<HTMLDivElement> = () => {
|
||||
this.setState({
|
||||
export const LogTextStreamLogEntryItemView = injectI18n(
|
||||
class extends React.PureComponent<
|
||||
LogTextStreamLogEntryItemViewProps,
|
||||
LogTextStreamLogEntryItemViewState
|
||||
> {
|
||||
public readonly state = {
|
||||
isHovered: false,
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
public render() {
|
||||
const { boundingBoxRef, logEntry, scale, searchResult, wrap } = this.props;
|
||||
const { isHovered } = this.state;
|
||||
public handleMouseEnter: React.MouseEventHandler<HTMLDivElement> = () => {
|
||||
this.setState({
|
||||
isHovered: true,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<LogTextStreamLogEntryItemDiv
|
||||
innerRef={
|
||||
/* Workaround for missing RefObject support in styled-components */
|
||||
boundingBoxRef as any
|
||||
}
|
||||
onMouseEnter={this.handleMouseEnter}
|
||||
onMouseLeave={this.handleMouseLeave}
|
||||
>
|
||||
<LogTextStreamItemDateField
|
||||
hasHighlights={!!searchResult}
|
||||
isHovered={isHovered}
|
||||
scale={scale}
|
||||
public handleMouseLeave: React.MouseEventHandler<HTMLDivElement> = () => {
|
||||
this.setState({
|
||||
isHovered: false,
|
||||
});
|
||||
};
|
||||
|
||||
public handleClick: React.MouseEventHandler<HTMLButtonElement> = () => {
|
||||
this.props.openFlyoutWithItem(this.props.logEntry.gid);
|
||||
};
|
||||
|
||||
public render() {
|
||||
const { intl, boundingBoxRef, logEntry, scale, searchResult, wrap } = this.props;
|
||||
const { isHovered } = this.state;
|
||||
const viewDetailsLabel = intl.formatMessage({
|
||||
id: 'xpack.infra.logEntryItemView.viewDetailsToolTip',
|
||||
defaultMessage: 'View Details',
|
||||
});
|
||||
|
||||
return (
|
||||
<LogTextStreamLogEntryItemDiv
|
||||
innerRef={
|
||||
/* Workaround for missing RefObject support in styled-components */
|
||||
boundingBoxRef as any
|
||||
}
|
||||
onMouseEnter={this.handleMouseEnter}
|
||||
onMouseLeave={this.handleMouseLeave}
|
||||
>
|
||||
{formatTime(logEntry.fields.time)}
|
||||
</LogTextStreamItemDateField>
|
||||
<LogTextStreamItemMessageField
|
||||
highlights={searchResult ? searchResult.matches.message || [] : []}
|
||||
isHovered={isHovered}
|
||||
isWrapped={wrap}
|
||||
scale={scale}
|
||||
>
|
||||
{logEntry.fields.message}
|
||||
</LogTextStreamItemMessageField>
|
||||
</LogTextStreamLogEntryItemDiv>
|
||||
);
|
||||
<LogTextStreamItemDateField
|
||||
hasHighlights={!!searchResult}
|
||||
isHovered={isHovered}
|
||||
scale={scale}
|
||||
>
|
||||
{formatTime(logEntry.fields.time)}
|
||||
</LogTextStreamItemDateField>
|
||||
<LogTextStreamIconDiv isHovered={isHovered}>
|
||||
{isHovered ? (
|
||||
<EuiToolTip content={viewDetailsLabel}>
|
||||
<EuiButtonIcon
|
||||
onClick={this.handleClick}
|
||||
iconType="expand"
|
||||
aria-label={viewDetailsLabel}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
) : (
|
||||
<EmptyIcon />
|
||||
)}
|
||||
</LogTextStreamIconDiv>
|
||||
<LogTextStreamItemMessageField
|
||||
highlights={searchResult ? searchResult.matches.message || [] : []}
|
||||
isHovered={isHovered}
|
||||
isWrapped={wrap}
|
||||
scale={scale}
|
||||
>
|
||||
{logEntry.fields.message}
|
||||
</LogTextStreamItemMessageField>
|
||||
</LogTextStreamLogEntryItemDiv>
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
interface IconProps {
|
||||
isHovered: boolean;
|
||||
}
|
||||
|
||||
const EmptyIcon = styled.div`
|
||||
width: 24px;
|
||||
`;
|
||||
|
||||
const LogTextStreamIconDiv = styled<IconProps, 'div'>('div')`
|
||||
flex-grow: 0;
|
||||
background-color: ${props =>
|
||||
props.isHovered
|
||||
? props.theme.darkMode
|
||||
? transparentize(0.9, darken(0.05, props.theme.eui.euiColorHighlight))
|
||||
: darken(0.05, props.theme.eui.euiColorHighlight)
|
||||
: 'transparent'};
|
||||
text-align: center;
|
||||
user-select: none;
|
||||
`;
|
||||
|
||||
const LogTextStreamLogEntryItemDiv = styled.div`
|
||||
font-family: ${props => props.theme.eui.euiCodeFontFamily};
|
||||
font-size: ${props => props.theme.eui.euiFontSize};
|
||||
|
|
|
@ -42,6 +42,8 @@ interface ScrollableLogTextStreamViewProps {
|
|||
}
|
||||
) => any;
|
||||
loadNewerItems: () => void;
|
||||
setFlyoutItem: (id: string) => void;
|
||||
showFlyout: () => void;
|
||||
}
|
||||
|
||||
interface ScrollableLogTextStreamViewState {
|
||||
|
@ -141,7 +143,13 @@ export class ScrollableLogTextStreamView extends React.PureComponent<
|
|||
key={getStreamItemId(item)}
|
||||
>
|
||||
{measureRef => (
|
||||
<LogTextStreamItemView ref={measureRef} item={item} scale={scale} wrap={wrap} />
|
||||
<LogTextStreamItemView
|
||||
openFlyoutWithItem={this.handleOpenFlyout}
|
||||
ref={measureRef}
|
||||
item={item}
|
||||
scale={scale}
|
||||
wrap={wrap}
|
||||
/>
|
||||
)}
|
||||
</MeasurableItemView>
|
||||
))}
|
||||
|
@ -160,6 +168,11 @@ export class ScrollableLogTextStreamView extends React.PureComponent<
|
|||
}
|
||||
}
|
||||
|
||||
private handleOpenFlyout = (id: string) => {
|
||||
this.props.setFlyoutItem(id);
|
||||
this.props.showFlyout();
|
||||
};
|
||||
|
||||
private handleReload = () => {
|
||||
const { jumpToTarget, target } = this.props;
|
||||
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* 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 gql from 'graphql-tag';
|
||||
|
||||
export const flyoutItemQuery = gql`
|
||||
query FlyoutItemQuery($sourceId: ID!, $itemId: ID!) {
|
||||
source(id: $sourceId) {
|
||||
id
|
||||
logItem(id: $itemId) {
|
||||
id
|
||||
index
|
||||
fields {
|
||||
field
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* 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 { WithOptions } from '../with_options';
|
||||
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;
|
||||
}
|
||||
|
||||
export const WithLogFlyout = ({ children }: WithFlyoutProps) => {
|
||||
return (
|
||||
<WithOptions>
|
||||
{({ sourceId }) => (
|
||||
<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>
|
||||
)}
|
||||
</WithOptions>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,105 @@
|
|||
/*
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
};
|
|
@ -40,11 +40,11 @@ function findOrCreateGroupWithNodes(
|
|||
}
|
||||
}
|
||||
}
|
||||
const lastPath = last(path);
|
||||
const existingGroup = groups.find(g => g.id === id);
|
||||
if (isWaffleMapGroupWithNodes(existingGroup)) {
|
||||
return existingGroup;
|
||||
}
|
||||
const lastPath = last(path);
|
||||
return {
|
||||
id,
|
||||
name:
|
||||
|
@ -65,11 +65,11 @@ function findOrCreateGroupWithGroups(
|
|||
path: InfraNodePath[]
|
||||
): InfraWaffleMapGroupOfGroups {
|
||||
const id = path.length === 0 ? '__all__' : createId(path);
|
||||
const lastPath = last(path);
|
||||
const existingGroup = groups.find(g => g.id === id);
|
||||
if (isWaffleMapGroupWithGroups(existingGroup)) {
|
||||
return existingGroup;
|
||||
}
|
||||
const lastPath = last(path);
|
||||
return {
|
||||
id,
|
||||
name:
|
||||
|
|
|
@ -299,6 +299,29 @@
|
|||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "logItem",
|
||||
"description": "",
|
||||
"args": [
|
||||
{
|
||||
"name": "id",
|
||||
"description": "",
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": { "kind": "SCALAR", "name": "ID", "ofType": null }
|
||||
},
|
||||
"defaultValue": null
|
||||
}
|
||||
],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": { "kind": "OBJECT", "name": "InfraLogItem", "ofType": null }
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "map",
|
||||
"description": "A hierarchy of hosts, pods, containers, services or arbitrary groups",
|
||||
|
@ -1310,6 +1333,96 @@
|
|||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "InfraLogItem",
|
||||
"description": "",
|
||||
"fields": [
|
||||
{
|
||||
"name": "id",
|
||||
"description": "The ID of the document",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": { "kind": "SCALAR", "name": "ID", "ofType": null }
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "index",
|
||||
"description": "The index where the document was found",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": { "kind": "SCALAR", "name": "String", "ofType": null }
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "fields",
|
||||
"description": "An array of flattened fields and values",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "LIST",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": { "kind": "OBJECT", "name": "InfraLogItemField", "ofType": null }
|
||||
}
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"inputFields": null,
|
||||
"interfaces": [],
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "InfraLogItemField",
|
||||
"description": "",
|
||||
"fields": [
|
||||
{
|
||||
"name": "field",
|
||||
"description": "The flattened field name",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": { "kind": "SCALAR", "name": "String", "ofType": null }
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "value",
|
||||
"description": "The value for the Field as a string",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": { "kind": "SCALAR", "name": "String", "ofType": null }
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"inputFields": null,
|
||||
"interfaces": [],
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "INPUT_OBJECT",
|
||||
"name": "InfraTimerangeInput",
|
||||
|
|
|
@ -34,6 +34,8 @@ export interface InfraSource {
|
|||
logEntriesBetween: InfraLogEntryInterval;
|
||||
/** A consecutive span of summary buckets within an interval */
|
||||
logSummaryBetween: InfraLogSummaryInterval;
|
||||
|
||||
logItem: InfraLogItem;
|
||||
/** A hierarchy of hosts, pods, containers, services or arbitrary groups */
|
||||
map?: InfraResponse | null;
|
||||
|
||||
|
@ -177,6 +179,22 @@ export interface InfraLogSummaryBucket {
|
|||
entriesCount: number;
|
||||
}
|
||||
|
||||
export interface InfraLogItem {
|
||||
/** The ID of the document */
|
||||
id: string;
|
||||
/** The index where the document was found */
|
||||
index: string;
|
||||
/** An array of flattened fields and values */
|
||||
fields: InfraLogItemField[];
|
||||
}
|
||||
|
||||
export interface InfraLogItemField {
|
||||
/** The flattened field name */
|
||||
field: string;
|
||||
/** The value for the Field as a string */
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface InfraResponse {
|
||||
nodes: InfraNode[];
|
||||
}
|
||||
|
@ -395,6 +413,9 @@ export interface LogSummaryBetweenInfraSourceArgs {
|
|||
/** The query to filter the log entries by */
|
||||
filterQuery?: string | null;
|
||||
}
|
||||
export interface LogItemInfraSourceArgs {
|
||||
id: string;
|
||||
}
|
||||
export interface MapInfraSourceArgs {
|
||||
timerange: InfraTimerangeInput;
|
||||
|
||||
|
@ -522,6 +543,45 @@ export type InfraLogMessageSegment = InfraLogMessageFieldSegment | InfraLogMessa
|
|||
// Documents
|
||||
// ====================================================
|
||||
|
||||
export namespace FlyoutItemQuery {
|
||||
export type Variables = {
|
||||
sourceId: string;
|
||||
itemId: string;
|
||||
};
|
||||
|
||||
export type Query = {
|
||||
__typename?: 'Query';
|
||||
|
||||
source: Source;
|
||||
};
|
||||
|
||||
export type Source = {
|
||||
__typename?: 'InfraSource';
|
||||
|
||||
id: string;
|
||||
|
||||
logItem: LogItem;
|
||||
};
|
||||
|
||||
export type LogItem = {
|
||||
__typename?: 'InfraLogItem';
|
||||
|
||||
id: string;
|
||||
|
||||
index: string;
|
||||
|
||||
fields: Fields[];
|
||||
};
|
||||
|
||||
export type Fields = {
|
||||
__typename?: 'InfraLogItemField';
|
||||
|
||||
field: string;
|
||||
|
||||
value: string;
|
||||
};
|
||||
}
|
||||
|
||||
export namespace MetadataQuery {
|
||||
export type Variables = {
|
||||
sourceId: string;
|
||||
|
|
|
@ -12,10 +12,14 @@ import { LogsToolbar } from './toolbar';
|
|||
|
||||
import { EmptyPage } from '../../components/empty_page';
|
||||
import { Header } from '../../components/header';
|
||||
import { LogFlyout } from '../../components/logging/log_flyout';
|
||||
import { ColumnarPage } from '../../components/page';
|
||||
|
||||
import { InfraHeaderFeedbackLink } from '../../components/header_feedback_link';
|
||||
import { WithLogFilterUrlState } from '../../containers/logs/with_log_filter';
|
||||
import { WithLogFilter, WithLogFilterUrlState } from '../../containers/logs/with_log_filter';
|
||||
import { WithLogFlyout } from '../../containers/logs/with_log_flyout';
|
||||
import { WithFlyoutOptions } from '../../containers/logs/with_log_flyout_options';
|
||||
import { WithFlyoutOptionsUrlState } from '../../containers/logs/with_log_flyout_options';
|
||||
import { WithLogMinimapUrlState } from '../../containers/logs/with_log_minimap';
|
||||
import { WithLogPositionUrlState } from '../../containers/logs/with_log_position';
|
||||
import { WithLogTextviewUrlState } from '../../containers/logs/with_log_textview';
|
||||
|
@ -61,8 +65,32 @@ export const LogsPage = injectI18n(
|
|||
<WithLogPositionUrlState />
|
||||
<WithLogMinimapUrlState />
|
||||
<WithLogTextviewUrlState />
|
||||
<WithFlyoutOptionsUrlState />
|
||||
<LogsToolbar />
|
||||
<LogsPageContent />
|
||||
<WithLogFilter indexPattern={derivedIndexPattern}>
|
||||
{({ applyFilterQueryFromKueryExpression }) => (
|
||||
<React.Fragment>
|
||||
<WithFlyoutOptions>
|
||||
{({ showFlyout, setFlyoutItem }) => (
|
||||
<LogsPageContent
|
||||
showFlyout={showFlyout}
|
||||
setFlyoutItem={setFlyoutItem}
|
||||
/>
|
||||
)}
|
||||
</WithFlyoutOptions>
|
||||
<WithLogFlyout>
|
||||
{({ flyoutItem, hideFlyout, loading }) => (
|
||||
<LogFlyout
|
||||
setFilter={applyFilterQueryFromKueryExpression}
|
||||
flyoutItem={flyoutItem}
|
||||
hideFlyout={hideFlyout}
|
||||
loading={loading}
|
||||
/>
|
||||
)}
|
||||
</WithLogFlyout>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</WithLogFilter>
|
||||
</>
|
||||
) : isLoading ? (
|
||||
<SourceLoadingPage />
|
||||
|
|
|
@ -17,7 +17,12 @@ import { WithLogTextview } from '../../containers/logs/with_log_textview';
|
|||
import { WithStreamItems } from '../../containers/logs/with_stream_items';
|
||||
import { WithSummary } from '../../containers/logs/with_summary';
|
||||
|
||||
export const LogsPageContent: React.SFC = () => (
|
||||
interface Props {
|
||||
setFlyoutItem: (id: string) => void;
|
||||
showFlyout: () => void;
|
||||
}
|
||||
|
||||
export const LogsPageContent: React.SFC<Props> = ({ showFlyout, setFlyoutItem }) => (
|
||||
<PageContent>
|
||||
<AutoSizer content>
|
||||
{({ measureRef, content: { width = 0, height = 0 } }) => (
|
||||
|
@ -57,6 +62,8 @@ export const LogsPageContent: React.SFC = () => (
|
|||
target={targetPosition}
|
||||
width={width}
|
||||
wrap={wrap}
|
||||
setFlyoutItem={setFlyoutItem}
|
||||
showFlyout={showFlyout}
|
||||
/>
|
||||
)}
|
||||
</WithStreamItems>
|
||||
|
|
|
@ -13,5 +13,6 @@ export {
|
|||
waffleFilterActions,
|
||||
waffleTimeActions,
|
||||
waffleOptionsActions,
|
||||
flyoutOptionsActions,
|
||||
} from './local';
|
||||
export { logEntriesActions, logSummaryActions } from './remote';
|
||||
|
|
|
@ -12,3 +12,4 @@ export { metricTimeActions } from './metric_time';
|
|||
export { waffleFilterActions } from './waffle_filter';
|
||||
export { waffleTimeActions } from './waffle_time';
|
||||
export { waffleOptionsActions } from './waffle_options';
|
||||
export { flyoutOptionsActions } from './log_flyout';
|
||||
|
|
|
@ -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;
|
||||
* 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');
|
11
x-pack/plugins/infra/public/store/local/log_flyout/index.ts
Normal file
11
x-pack/plugins/infra/public/store/local/log_flyout/index.ts
Normal 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;
|
||||
* 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';
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* 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,
|
||||
});
|
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* 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;
|
|
@ -7,6 +7,7 @@
|
|||
import { combineReducers } from 'redux';
|
||||
|
||||
import { initialLogFilterState, logFilterReducer, LogFilterState } from './log_filter';
|
||||
import { flyoutOptionsReducer, FlyoutOptionsState, initialFlyoutOptionsState } from './log_flyout';
|
||||
import { initialLogMinimapState, logMinimapReducer, LogMinimapState } from './log_minimap';
|
||||
import { initialLogPositionState, logPositionReducer, LogPositionState } from './log_position';
|
||||
import { initialLogTextviewState, logTextviewReducer, LogTextviewState } from './log_textview';
|
||||
|
@ -28,6 +29,7 @@ export interface LocalState {
|
|||
waffleFilter: WaffleFilterState;
|
||||
waffleTime: WaffleTimeState;
|
||||
waffleMetrics: WaffleOptionsState;
|
||||
logFlyout: FlyoutOptionsState;
|
||||
}
|
||||
|
||||
export const initialLocalState: LocalState = {
|
||||
|
@ -39,6 +41,7 @@ export const initialLocalState: LocalState = {
|
|||
waffleFilter: initialWaffleFilterState,
|
||||
waffleTime: initialWaffleTimeState,
|
||||
waffleMetrics: initialWaffleOptionsState,
|
||||
logFlyout: initialFlyoutOptionsState,
|
||||
};
|
||||
|
||||
export const localReducer = combineReducers<LocalState>({
|
||||
|
@ -50,4 +53,5 @@ export const localReducer = combineReducers<LocalState>({
|
|||
waffleFilter: waffleFilterReducer,
|
||||
waffleTime: waffleTimeReducer,
|
||||
waffleMetrics: waffleOptionsReducer,
|
||||
logFlyout: flyoutOptionsReducer,
|
||||
});
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
import { globalizeSelectors } from '../../utils/typed_redux';
|
||||
import { logFilterSelectors as innerLogFilterSelectors } from './log_filter';
|
||||
import { flyoutOptionsSelectors as innerFlyoutOptionsSelectors } from './log_flyout';
|
||||
import { logMinimapSelectors as innerLogMinimapSelectors } from './log_minimap';
|
||||
import { logPositionSelectors as innerLogPositionSelectors } from './log_position';
|
||||
import { logTextviewSelectors as innerLogTextviewSelectors } from './log_textview';
|
||||
|
@ -54,3 +55,8 @@ export const waffleOptionsSelectors = globalizeSelectors(
|
|||
(state: LocalState) => state.waffleMetrics,
|
||||
innerWaffleOptionsSelectors
|
||||
);
|
||||
|
||||
export const flyoutOptionsSelectors = globalizeSelectors(
|
||||
(state: LocalState) => state.logFlyout,
|
||||
innerFlyoutOptionsSelectors
|
||||
);
|
||||
|
|
|
@ -9,6 +9,7 @@ import { createSelector } from 'reselect';
|
|||
import { getLogEntryAtTime } from '../utils/log_entry';
|
||||
import { globalizeSelectors } from '../utils/typed_redux';
|
||||
import {
|
||||
flyoutOptionsSelectors as localFlyoutOptionsSelectors,
|
||||
logFilterSelectors as localLogFilterSelectors,
|
||||
logMinimapSelectors as localLogMinimapSelectors,
|
||||
logPositionSelectors as localLogPositionSelectors,
|
||||
|
@ -38,6 +39,7 @@ export const metricTimeSelectors = globalizeSelectors(selectLocal, localMetricTi
|
|||
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
|
||||
|
|
|
@ -31,6 +31,11 @@ export type InfraSourceLogSummaryBetweenResolver = ChildResolverOf<
|
|||
QuerySourceResolver
|
||||
>;
|
||||
|
||||
export type InfraSourceLogItem = ChildResolverOf<
|
||||
InfraResolverOf<InfraSourceResolvers.LogItemResolver>,
|
||||
QuerySourceResolver
|
||||
>;
|
||||
|
||||
export const createLogEntriesResolvers = (libs: {
|
||||
logEntries: InfraLogEntriesDomain;
|
||||
}): {
|
||||
|
@ -38,6 +43,7 @@ export const createLogEntriesResolvers = (libs: {
|
|||
logEntriesAround: InfraSourceLogEntriesAroundResolver;
|
||||
logEntriesBetween: InfraSourceLogEntriesBetweenResolver;
|
||||
logSummaryBetween: InfraSourceLogSummaryBetweenResolver;
|
||||
logItem: InfraSourceLogItem;
|
||||
};
|
||||
InfraLogMessageSegment: {
|
||||
__resolveType(
|
||||
|
@ -115,6 +121,9 @@ export const createLogEntriesResolvers = (libs: {
|
|||
buckets,
|
||||
};
|
||||
},
|
||||
async logItem(source, args, { req }) {
|
||||
return await libs.logEntries.getLogItem(req, args.id, source.configuration);
|
||||
},
|
||||
},
|
||||
InfraLogMessageSegment: {
|
||||
__resolveType: (messageSegment: InfraLogMessageSegment) => {
|
||||
|
|
|
@ -78,6 +78,22 @@ export const logEntriesSchema = gql`
|
|||
buckets: [InfraLogSummaryBucket!]!
|
||||
}
|
||||
|
||||
type InfraLogItemField {
|
||||
"The flattened field name"
|
||||
field: String!
|
||||
"The value for the Field as a string"
|
||||
value: String!
|
||||
}
|
||||
|
||||
type InfraLogItem {
|
||||
"The ID of the document"
|
||||
id: ID!
|
||||
"The index where the document was found"
|
||||
index: String!
|
||||
"An array of flattened fields and values"
|
||||
fields: [InfraLogItemField!]!
|
||||
}
|
||||
|
||||
extend type InfraSource {
|
||||
"A consecutive span of log entries surrounding a point in time"
|
||||
logEntriesAround(
|
||||
|
@ -114,5 +130,6 @@ export const logEntriesSchema = gql`
|
|||
"The query to filter the log entries by"
|
||||
filterQuery: String
|
||||
): InfraLogSummaryInterval!
|
||||
logItem(id: ID!): InfraLogItem!
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -62,6 +62,8 @@ export interface InfraSource {
|
|||
logEntriesBetween: InfraLogEntryInterval;
|
||||
/** A consecutive span of summary buckets within an interval */
|
||||
logSummaryBetween: InfraLogSummaryInterval;
|
||||
|
||||
logItem: InfraLogItem;
|
||||
/** A hierarchy of hosts, pods, containers, services or arbitrary groups */
|
||||
map?: InfraResponse | null;
|
||||
|
||||
|
@ -205,6 +207,22 @@ export interface InfraLogSummaryBucket {
|
|||
entriesCount: number;
|
||||
}
|
||||
|
||||
export interface InfraLogItem {
|
||||
/** The ID of the document */
|
||||
id: string;
|
||||
/** The index where the document was found */
|
||||
index: string;
|
||||
/** An array of flattened fields and values */
|
||||
fields: InfraLogItemField[];
|
||||
}
|
||||
|
||||
export interface InfraLogItemField {
|
||||
/** The flattened field name */
|
||||
field: string;
|
||||
/** The value for the Field as a string */
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface InfraResponse {
|
||||
nodes: InfraNode[];
|
||||
}
|
||||
|
@ -423,6 +441,9 @@ export interface LogSummaryBetweenInfraSourceArgs {
|
|||
/** The query to filter the log entries by */
|
||||
filterQuery?: string | null;
|
||||
}
|
||||
export interface LogItemInfraSourceArgs {
|
||||
id: string;
|
||||
}
|
||||
export interface MapInfraSourceArgs {
|
||||
timerange: InfraTimerangeInput;
|
||||
|
||||
|
@ -596,6 +617,8 @@ export namespace InfraSourceResolvers {
|
|||
logEntriesBetween?: LogEntriesBetweenResolver<InfraLogEntryInterval, TypeParent, Context>;
|
||||
/** A consecutive span of summary buckets within an interval */
|
||||
logSummaryBetween?: LogSummaryBetweenResolver<InfraLogSummaryInterval, TypeParent, Context>;
|
||||
|
||||
logItem?: LogItemResolver<InfraLogItem, TypeParent, Context>;
|
||||
/** A hierarchy of hosts, pods, containers, services or arbitrary groups */
|
||||
map?: MapResolver<InfraResponse | null, TypeParent, Context>;
|
||||
|
||||
|
@ -688,6 +711,15 @@ export namespace InfraSourceResolvers {
|
|||
filterQuery?: string | null;
|
||||
}
|
||||
|
||||
export type LogItemResolver<
|
||||
R = InfraLogItem,
|
||||
Parent = InfraSource,
|
||||
Context = InfraContext
|
||||
> = Resolver<R, Parent, Context, LogItemArgs>;
|
||||
export interface LogItemArgs {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export type MapResolver<
|
||||
R = InfraResponse | null,
|
||||
Parent = InfraSource,
|
||||
|
@ -1144,6 +1176,53 @@ export namespace InfraLogSummaryBucketResolvers {
|
|||
> = Resolver<R, Parent, Context>;
|
||||
}
|
||||
|
||||
export namespace InfraLogItemResolvers {
|
||||
export interface Resolvers<Context = InfraContext, TypeParent = InfraLogItem> {
|
||||
/** The ID of the document */
|
||||
id?: IdResolver<string, TypeParent, Context>;
|
||||
/** The index where the document was found */
|
||||
index?: IndexResolver<string, TypeParent, Context>;
|
||||
/** An array of flattened fields and values */
|
||||
fields?: FieldsResolver<InfraLogItemField[], TypeParent, Context>;
|
||||
}
|
||||
|
||||
export type IdResolver<R = string, Parent = InfraLogItem, Context = InfraContext> = Resolver<
|
||||
R,
|
||||
Parent,
|
||||
Context
|
||||
>;
|
||||
export type IndexResolver<R = string, Parent = InfraLogItem, Context = InfraContext> = Resolver<
|
||||
R,
|
||||
Parent,
|
||||
Context
|
||||
>;
|
||||
export type FieldsResolver<
|
||||
R = InfraLogItemField[],
|
||||
Parent = InfraLogItem,
|
||||
Context = InfraContext
|
||||
> = Resolver<R, Parent, Context>;
|
||||
}
|
||||
|
||||
export namespace InfraLogItemFieldResolvers {
|
||||
export interface Resolvers<Context = InfraContext, TypeParent = InfraLogItemField> {
|
||||
/** The flattened field name */
|
||||
field?: FieldResolver<string, TypeParent, Context>;
|
||||
/** The value for the Field as a string */
|
||||
value?: ValueResolver<string, TypeParent, Context>;
|
||||
}
|
||||
|
||||
export type FieldResolver<
|
||||
R = string,
|
||||
Parent = InfraLogItemField,
|
||||
Context = InfraContext
|
||||
> = Resolver<R, Parent, Context>;
|
||||
export type ValueResolver<
|
||||
R = string,
|
||||
Parent = InfraLogItemField,
|
||||
Context = InfraContext
|
||||
> = Resolver<R, Parent, Context>;
|
||||
}
|
||||
|
||||
export namespace InfraResponseResolvers {
|
||||
export interface Resolvers<Context = InfraContext, TypeParent = InfraResponse> {
|
||||
nodes?: NodesResolver<InfraNode[], TypeParent, Context>;
|
||||
|
|
|
@ -5,10 +5,12 @@
|
|||
*/
|
||||
|
||||
import { timeMilliseconds } from 'd3-time';
|
||||
import first from 'lodash/fp/first';
|
||||
import get from 'lodash/fp/get';
|
||||
import has from 'lodash/fp/has';
|
||||
import zip from 'lodash/fp/zip';
|
||||
|
||||
import { JsonObject } from 'x-pack/plugins/infra/common/typed_json';
|
||||
import { compareTimeKeys, isTimeKey, TimeKey } from '../../../../common/time';
|
||||
import {
|
||||
LogEntriesAdapter,
|
||||
|
@ -28,6 +30,12 @@ const DAY_MILLIS = 24 * 60 * 60 * 1000;
|
|||
const LOOKUP_OFFSETS = [0, 1, 7, 30, 365, 10000, Infinity].map(days => days * DAY_MILLIS);
|
||||
const TIMESTAMP_FORMAT = 'epoch_millis';
|
||||
|
||||
interface LogItemHit {
|
||||
_index: string;
|
||||
_id: string;
|
||||
_source: JsonObject;
|
||||
}
|
||||
|
||||
export class InfraKibanaLogEntriesAdapter implements LogEntriesAdapter {
|
||||
constructor(private readonly framework: InfraBackendFrameworkAdapter) {}
|
||||
|
||||
|
@ -152,6 +160,35 @@ export class InfraKibanaLogEntriesAdapter implements LogEntriesAdapter {
|
|||
: [];
|
||||
}
|
||||
|
||||
public async getLogItem(
|
||||
request: InfraFrameworkRequest,
|
||||
id: string,
|
||||
sourceConfiguration: InfraSourceConfiguration
|
||||
) {
|
||||
const search = (searchOptions: object) =>
|
||||
this.framework.callWithRequest<LogItemHit, {}>(request, 'search', searchOptions);
|
||||
|
||||
const params = {
|
||||
index: sourceConfiguration.logAlias,
|
||||
terminate_after: 1,
|
||||
body: {
|
||||
size: 1,
|
||||
query: {
|
||||
ids: {
|
||||
values: [id],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const response = await search(params);
|
||||
const document = first(response.hits.hits);
|
||||
if (!document) {
|
||||
throw new Error('Document not found');
|
||||
}
|
||||
return document;
|
||||
}
|
||||
|
||||
private async getLogEntryDocumentsBetween(
|
||||
request: InfraFrameworkRequest,
|
||||
sourceConfiguration: InfraSourceConfiguration,
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { convertDocumentSourceToLogItemFields } from './convert_document_source_to_log_item_fields';
|
||||
|
||||
describe('convertDocumentSourceToLogItemFields', () => {
|
||||
test('should convert document', () => {
|
||||
const doc = {
|
||||
agent: {
|
||||
hostname: 'demo-stack-client-01',
|
||||
id: '7adef8b6-2ab7-45cd-a0d5-b3baad735f1b',
|
||||
type: 'filebeat',
|
||||
ephemeral_id: 'a0c8164b-3564-4e32-b0bf-f4db5a7ae566',
|
||||
version: '7.0.0',
|
||||
},
|
||||
tags: ['prod', 'web'],
|
||||
metadata: [{ key: 'env', value: 'prod' }, { key: 'stack', value: 'web' }],
|
||||
host: {
|
||||
hostname: 'packer-virtualbox-iso-1546820004',
|
||||
name: 'demo-stack-client-01',
|
||||
},
|
||||
};
|
||||
|
||||
const fields = convertDocumentSourceToLogItemFields(doc);
|
||||
expect(fields).toEqual([
|
||||
{
|
||||
field: 'agent.hostname',
|
||||
value: 'demo-stack-client-01',
|
||||
},
|
||||
{
|
||||
field: 'agent.id',
|
||||
value: '7adef8b6-2ab7-45cd-a0d5-b3baad735f1b',
|
||||
},
|
||||
{
|
||||
field: 'agent.type',
|
||||
value: 'filebeat',
|
||||
},
|
||||
{
|
||||
field: 'agent.ephemeral_id',
|
||||
value: 'a0c8164b-3564-4e32-b0bf-f4db5a7ae566',
|
||||
},
|
||||
{
|
||||
field: 'agent.version',
|
||||
value: '7.0.0',
|
||||
},
|
||||
{
|
||||
field: 'tags',
|
||||
value: '["prod","web"]',
|
||||
},
|
||||
{
|
||||
field: 'metadata',
|
||||
value: '[{"key":"env","value":"prod"},{"key":"stack","value":"web"}]',
|
||||
},
|
||||
{
|
||||
field: 'host.hostname',
|
||||
value: 'packer-virtualbox-iso-1546820004',
|
||||
},
|
||||
{
|
||||
field: 'host.name',
|
||||
value: 'demo-stack-client-01',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { isArray, isPlainObject } from 'lodash';
|
||||
import { JsonObject } from 'x-pack/plugins/infra/common/typed_json';
|
||||
import { InfraLogItemField } from '../../../graphql/types';
|
||||
|
||||
const isJsonObject = (subject: any): subject is JsonObject => {
|
||||
return isPlainObject(subject);
|
||||
};
|
||||
|
||||
const serializeValue = (value: any): string => {
|
||||
if (isArray(value) || isPlainObject(value)) {
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
return `${value}`;
|
||||
};
|
||||
|
||||
export const convertDocumentSourceToLogItemFields = (
|
||||
source: JsonObject,
|
||||
path: string[] = [],
|
||||
fields: InfraLogItemField[] = []
|
||||
): InfraLogItemField[] => {
|
||||
return Object.keys(source).reduce((acc, key) => {
|
||||
const value = source[key];
|
||||
const nextPath = [...path, key];
|
||||
if (isJsonObject(value)) {
|
||||
return convertDocumentSourceToLogItemFields(value, nextPath, acc);
|
||||
}
|
||||
const field = { field: nextPath.join('.'), value: serializeValue(value) };
|
||||
return [...acc, field];
|
||||
}, fields);
|
||||
};
|
|
@ -4,8 +4,10 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { sortBy } from 'lodash';
|
||||
import { TimeKey } from '../../../../common/time';
|
||||
import { JsonObject } from '../../../../common/typed_json';
|
||||
import { InfraLogItem } from '../../../graphql/types';
|
||||
import {
|
||||
InfraLogEntry,
|
||||
InfraLogMessageSegment,
|
||||
|
@ -14,6 +16,7 @@ import {
|
|||
import { InfraDateRangeAggregationBucket, InfraFrameworkRequest } from '../../adapters/framework';
|
||||
import { InfraSourceConfiguration, InfraSources } from '../../sources';
|
||||
import { builtinRules } from './builtin_rules';
|
||||
import { convertDocumentSourceToLogItemFields } from './convert_document_source_to_log_item_fields';
|
||||
import { compileFormattingRules } from './message';
|
||||
|
||||
export class InfraLogEntriesDomain {
|
||||
|
@ -121,6 +124,32 @@ export class InfraLogEntriesDomain {
|
|||
const buckets = dateRangeBuckets.map(convertDateRangeBucketToSummaryBucket);
|
||||
return buckets;
|
||||
}
|
||||
|
||||
public async getLogItem(
|
||||
request: InfraFrameworkRequest,
|
||||
id: string,
|
||||
sourceConfiguration: InfraSourceConfiguration
|
||||
): Promise<InfraLogItem> {
|
||||
const document = await this.adapter.getLogItem(request, id, sourceConfiguration);
|
||||
const defaultFields = [
|
||||
{ field: '_index', value: document._index },
|
||||
{ field: '_id', value: document._id },
|
||||
];
|
||||
return {
|
||||
id: document._id,
|
||||
index: document._index,
|
||||
fields: sortBy(
|
||||
[...defaultFields, ...convertDocumentSourceToLogItemFields(document._source)],
|
||||
'field'
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
interface LogItemHit {
|
||||
_index: string;
|
||||
_id: string;
|
||||
_source: JsonObject;
|
||||
}
|
||||
|
||||
export interface LogEntriesAdapter {
|
||||
|
@ -153,6 +182,12 @@ export interface LogEntriesAdapter {
|
|||
bucketSize: number,
|
||||
filterQuery?: LogEntryQuery
|
||||
): Promise<InfraDateRangeAggregationBucket[]>;
|
||||
|
||||
getLogItem(
|
||||
request: InfraFrameworkRequest,
|
||||
id: string,
|
||||
source: InfraSourceConfiguration
|
||||
): Promise<LogItemHit>;
|
||||
}
|
||||
|
||||
export type LogEntryQuery = JsonObject;
|
||||
|
|
|
@ -14,5 +14,6 @@ export default function ({ loadTestFile }) {
|
|||
loadTestFile(require.resolve('./metrics'));
|
||||
loadTestFile(require.resolve('./sources'));
|
||||
loadTestFile(require.resolve('./waffle'));
|
||||
loadTestFile(require.resolve('./log_item'));
|
||||
});
|
||||
}
|
||||
|
|
175
x-pack/test/api_integration/apis/infra/log_item.ts
Normal file
175
x-pack/test/api_integration/apis/infra/log_item.ts
Normal file
|
@ -0,0 +1,175 @@
|
|||
/*
|
||||
* 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 expect from 'expect.js';
|
||||
import { flyoutItemQuery } from '../../../../plugins/infra/public/containers/logs/flyout_item.gql_query';
|
||||
import { FlyoutItemQuery } from '../../../../plugins/infra/public/graphql/types';
|
||||
import { KbnTestProvider } from './types';
|
||||
|
||||
const logItemTests: KbnTestProvider = ({ getService }) => {
|
||||
const esArchiver = getService('esArchiver');
|
||||
const client = getService('infraOpsGraphQLClient');
|
||||
describe('Log Item GraphQL Endpoint', () => {
|
||||
before(() => esArchiver.load('infra/metrics_and_logs'));
|
||||
after(() => esArchiver.unload('infra/metrics_and_logs'));
|
||||
|
||||
it('should basically work', () => {
|
||||
return client
|
||||
.query<FlyoutItemQuery.Query>({
|
||||
query: flyoutItemQuery,
|
||||
variables: {
|
||||
sourceId: 'default',
|
||||
itemId: 'yT2Mg2YBh-opCxJv8Vqj',
|
||||
},
|
||||
})
|
||||
.then(resp => {
|
||||
expect(resp.data.source).to.have.property('logItem');
|
||||
const { logItem } = resp.data.source;
|
||||
if (!logItem) {
|
||||
throw new Error('Log item should not be falsey');
|
||||
}
|
||||
expect(logItem).to.have.property('id', 'yT2Mg2YBh-opCxJv8Vqj');
|
||||
expect(logItem).to.have.property('index', 'filebeat-7.0.0-alpha1-2018.10.17');
|
||||
expect(logItem).to.have.property('fields');
|
||||
expect(logItem.fields).to.eql([
|
||||
{
|
||||
field: '@timestamp',
|
||||
value: '2018-10-17T19:42:22.000Z',
|
||||
__typename: 'InfraLogItemField',
|
||||
},
|
||||
{
|
||||
field: '_id',
|
||||
value: 'yT2Mg2YBh-opCxJv8Vqj',
|
||||
__typename: 'InfraLogItemField',
|
||||
},
|
||||
{
|
||||
field: '_index',
|
||||
value: 'filebeat-7.0.0-alpha1-2018.10.17',
|
||||
__typename: 'InfraLogItemField',
|
||||
},
|
||||
{
|
||||
field: 'apache2.access.body_sent.bytes',
|
||||
value: '1336',
|
||||
__typename: 'InfraLogItemField',
|
||||
},
|
||||
{
|
||||
field: 'apache2.access.http_version',
|
||||
value: '1.1',
|
||||
__typename: 'InfraLogItemField',
|
||||
},
|
||||
{
|
||||
field: 'apache2.access.method',
|
||||
value: 'GET',
|
||||
__typename: 'InfraLogItemField',
|
||||
},
|
||||
{
|
||||
field: 'apache2.access.referrer',
|
||||
value: '-',
|
||||
__typename: 'InfraLogItemField',
|
||||
},
|
||||
{
|
||||
field: 'apache2.access.remote_ip',
|
||||
value: '10.128.0.11',
|
||||
__typename: 'InfraLogItemField',
|
||||
},
|
||||
{
|
||||
field: 'apache2.access.response_code',
|
||||
value: '200',
|
||||
__typename: 'InfraLogItemField',
|
||||
},
|
||||
{
|
||||
field: 'apache2.access.url',
|
||||
value: '/a-fresh-start-will-put-you-on-your-way',
|
||||
__typename: 'InfraLogItemField',
|
||||
},
|
||||
{
|
||||
field: 'apache2.access.user_agent.device',
|
||||
value: 'Other',
|
||||
__typename: 'InfraLogItemField',
|
||||
},
|
||||
{
|
||||
field: 'apache2.access.user_agent.name',
|
||||
value: 'Other',
|
||||
__typename: 'InfraLogItemField',
|
||||
},
|
||||
{
|
||||
field: 'apache2.access.user_agent.os',
|
||||
value: 'Other',
|
||||
__typename: 'InfraLogItemField',
|
||||
},
|
||||
{
|
||||
field: 'apache2.access.user_agent.os_name',
|
||||
value: 'Other',
|
||||
__typename: 'InfraLogItemField',
|
||||
},
|
||||
{
|
||||
field: 'apache2.access.user_name',
|
||||
value: '-',
|
||||
__typename: 'InfraLogItemField',
|
||||
},
|
||||
{
|
||||
field: 'beat.hostname',
|
||||
value: 'demo-stack-apache-01',
|
||||
__typename: 'InfraLogItemField',
|
||||
},
|
||||
{
|
||||
field: 'beat.name',
|
||||
value: 'demo-stack-apache-01',
|
||||
__typename: 'InfraLogItemField',
|
||||
},
|
||||
{
|
||||
field: 'beat.version',
|
||||
value: '7.0.0-alpha1',
|
||||
__typename: 'InfraLogItemField',
|
||||
},
|
||||
{
|
||||
field: 'fileset.module',
|
||||
value: 'apache2',
|
||||
__typename: 'InfraLogItemField',
|
||||
},
|
||||
{
|
||||
field: 'fileset.name',
|
||||
value: 'access',
|
||||
__typename: 'InfraLogItemField',
|
||||
},
|
||||
{
|
||||
field: 'host.name',
|
||||
value: 'demo-stack-apache-01',
|
||||
__typename: 'InfraLogItemField',
|
||||
},
|
||||
{
|
||||
field: 'input.type',
|
||||
value: 'log',
|
||||
__typename: 'InfraLogItemField',
|
||||
},
|
||||
{
|
||||
field: 'offset',
|
||||
value: '5497614',
|
||||
__typename: 'InfraLogItemField',
|
||||
},
|
||||
{
|
||||
field: 'prospector.type',
|
||||
value: 'log',
|
||||
__typename: 'InfraLogItemField',
|
||||
},
|
||||
{
|
||||
field: 'read_timestamp',
|
||||
value: '2018-10-17T19:42:23.160Z',
|
||||
__typename: 'InfraLogItemField',
|
||||
},
|
||||
{
|
||||
field: 'source',
|
||||
value: '/var/log/apache2/access.log',
|
||||
__typename: 'InfraLogItemField',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// tslint:disable-next-line no-default-export
|
||||
export default logItemTests;
|
Binary file not shown.
Binary file not shown.
|
@ -1708,4 +1708,4 @@
|
|||
},
|
||||
"aliases": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Binary file not shown.
|
@ -10745,4 +10745,4 @@
|
|||
},
|
||||
"aliases": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue