[Logs UI] Add column headers (#36467)

This PR adds column headers to the Logs UI. The header row also contains a quick link to the column configuration flyout. The detail flyout link, which was previously coupled to the `message` field, is now an independent column at the last position.

closes elastic/kibana#35780
closes elastic/kibana#36105
This commit is contained in:
Felix Stürmer 2019-05-20 18:58:24 -04:00 committed by GitHub
parent 926f9a912d
commit 066f64d07c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 917 additions and 591 deletions

View file

@ -0,0 +1,125 @@
/*
* 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 { EuiButtonIcon } from '@elastic/eui';
import { injectI18n } from '@kbn/i18n/react';
import React, { useMemo } from 'react';
import euiStyled from '../../../../../../common/eui_styled_components';
import {
LogColumnConfiguration,
isTimestampLogColumnConfiguration,
isFieldLogColumnConfiguration,
isMessageLogColumnConfiguration,
} from '../../../utils/source_configuration';
import { LogEntryColumnWidth, LogEntryColumn, LogEntryColumnContent } from './log_entry_column';
import { ASSUMED_SCROLLBAR_WIDTH } from './vertical_scroll_panel';
export const LogColumnHeaders = injectI18n<{
columnConfigurations: LogColumnConfiguration[];
columnWidths: LogEntryColumnWidth[];
showColumnConfiguration: () => void;
}>(({ columnConfigurations, columnWidths, intl, showColumnConfiguration }) => {
const iconColumnWidth = useMemo(() => columnWidths[columnWidths.length - 1], [columnWidths]);
const showColumnConfigurationLabel = intl.formatMessage({
id: 'xpack.infra.logColumnHeaders.configureColumnsLabel',
defaultMessage: 'Configure columns',
});
return (
<LogColumnHeadersWrapper>
{columnConfigurations.map((columnConfiguration, columnIndex) => {
const columnWidth = columnWidths[columnIndex];
if (isTimestampLogColumnConfiguration(columnConfiguration)) {
return (
<LogColumnHeader
columnWidth={columnWidth}
data-test-subj="logColumnHeader timestampLogColumnHeader"
key={columnConfiguration.timestampColumn.id}
>
Timestamp
</LogColumnHeader>
);
} else if (isMessageLogColumnConfiguration(columnConfiguration)) {
return (
<LogColumnHeader
columnWidth={columnWidth}
data-test-subj="logColumnHeader messageLogColumnHeader"
key={columnConfiguration.messageColumn.id}
>
Message
</LogColumnHeader>
);
} else if (isFieldLogColumnConfiguration(columnConfiguration)) {
return (
<LogColumnHeader
columnWidth={columnWidth}
data-test-subj="logColumnHeader fieldLogColumnHeader"
key={columnConfiguration.fieldColumn.id}
>
{columnConfiguration.fieldColumn.field}
</LogColumnHeader>
);
}
})}
<LogColumnHeader
columnWidth={iconColumnWidth}
data-test-subj="logColumnHeader iconLogColumnHeader"
key="iconColumnHeader"
>
<EuiButtonIcon
aria-label={showColumnConfigurationLabel}
color="text"
iconType="gear"
onClick={showColumnConfiguration}
title={showColumnConfigurationLabel}
/>
</LogColumnHeader>
</LogColumnHeadersWrapper>
);
});
const LogColumnHeader: React.FunctionComponent<{
columnWidth: LogEntryColumnWidth;
'data-test-subj'?: string;
}> = ({ children, columnWidth, 'data-test-subj': dataTestSubj }) => (
<LogColumnHeaderWrapper data-test-subj={dataTestSubj} {...columnWidth}>
<LogColumnHeaderContent>{children}</LogColumnHeaderContent>
</LogColumnHeaderWrapper>
);
const LogColumnHeadersWrapper = euiStyled.div.attrs({
role: 'row',
})`
align-items: stretch;
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: flex-start;
overflow: hidden;
padding-right: ${ASSUMED_SCROLLBAR_WIDTH}px;
`;
const LogColumnHeaderWrapper = LogEntryColumn.extend.attrs({
role: 'columnheader',
})`
align-items: center;
border-bottom: ${props => props.theme.eui.euiBorderThick};
display: flex;
flex-direction: row;
height: 32px;
overflow: hidden;
`;
const LogColumnHeaderContent = LogEntryColumnContent.extend`
color: ${props => props.theme.eui.euiTitleColor};
font-size: ${props => props.theme.eui.euiFontSizeS};
font-weight: ${props => props.theme.eui.euiFontWeightSemiBold};
line-height: ${props => props.theme.eui.euiLineHeight};
text-overflow: clip;
white-space: pre;
`;

View file

@ -1,22 +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 euiStyled from '../../../../../../common/eui_styled_components';
import { switchProp } from '../../../utils/styles';
export const LogTextStreamItemField = euiStyled.div.attrs<{
scale?: 'small' | 'medium' | 'large';
}>({})`
font-size: ${props =>
switchProp('scale', {
large: props.theme.eui.euiFontSizeM,
medium: props.theme.eui.euiFontSizeS,
small: props.theme.eui.euiFontSizeXS,
[switchProp.default]: props.theme.eui.euiFontSize,
})};
line-height: ${props => props.theme.eui.euiLineHeight};
padding: 2px ${props => props.theme.eui.paddingSizes.m};
`;

View file

@ -1,132 +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 { darken, transparentize } from 'polished';
import React, { useMemo } from 'react';
// import euiStyled, { css } from '../../../../../../common/eui_styled_components';
import { css } from '../../../../../../common/eui_styled_components';
import { TextScale } from '../../../../common/log_text_scale';
import { tintOrShade } from '../../../utils/styles';
import { LogTextStreamItemField } from './item_field';
import {
isConstantSegment,
isFieldSegment,
LogEntryMessageSegment,
} from '../../../utils/log_entry';
interface LogTextStreamItemMessageFieldProps {
dataTestSubj?: string;
segments: LogEntryMessageSegment[];
isHovered: boolean;
isWrapped: boolean;
scale: TextScale;
isHighlighted: boolean;
}
export const LogTextStreamItemMessageField: React.FunctionComponent<
LogTextStreamItemMessageFieldProps
> = ({ dataTestSubj, isHighlighted, isHovered, isWrapped, scale, segments }) => {
const message = useMemo(() => segments.map(formatMessageSegment).join(''), [segments]);
return (
<LogTextStreamItemMessageFieldWrapper
data-test-subj={dataTestSubj}
hasHighlights={false}
isHighlighted={isHighlighted}
isHovered={isHovered}
isWrapped={isWrapped}
scale={scale}
>
{message}
</LogTextStreamItemMessageFieldWrapper>
);
};
// const renderHighlightFragments = (text: string, highlights: string[]): React.ReactNode[] => {
// const renderedHighlights = highlights.reduce(
// ({ lastFragmentEnd, renderedFragments }, highlight) => {
// const fragmentStart = text.indexOf(highlight, lastFragmentEnd);
// return {
// lastFragmentEnd: fragmentStart + highlight.length,
// renderedFragments: [
// ...renderedFragments,
// text.slice(lastFragmentEnd, fragmentStart),
// <HighlightSpan key={fragmentStart}>{highlight}</HighlightSpan>,
// ],
// };
// },
// {
// lastFragmentEnd: 0,
// renderedFragments: [],
// } as {
// lastFragmentEnd: number;
// renderedFragments: React.ReactNode[];
// }
// );
//
// return [...renderedHighlights.renderedFragments, text.slice(renderedHighlights.lastFragmentEnd)];
// };
const highlightedFieldStyle = css`
background-color: ${props =>
tintOrShade(
props.theme.eui.euiTextColor as any, // workaround for incorrect upstream `tintOrShade` types
props.theme.eui.euiColorSecondary as any,
0.15
)};
`;
const hoveredFieldStyle = css`
background-color: ${props =>
props.theme.darkMode
? transparentize(0.9, darken(0.05, props.theme.eui.euiColorHighlight))
: darken(0.05, props.theme.eui.euiColorHighlight)};
`;
const wrappedFieldStyle = css`
overflow: visible;
white-space: pre-wrap;
word-break: break-all;
`;
const unwrappedFieldStyle = css`
overflow: hidden;
white-space: pre;
`;
const LogTextStreamItemMessageFieldWrapper = LogTextStreamItemField.extend.attrs<{
hasHighlights: boolean;
isHovered: boolean;
isHighlighted: boolean;
isWrapped?: boolean;
}>({})`
flex: 5 0 0%
text-overflow: ellipsis;
background-color: ${props => props.theme.eui.euiColorEmptyShade};
padding-left: 0;
${props => (props.hasHighlights ? highlightedFieldStyle : '')};
${props => (props.isHovered || props.isHighlighted ? hoveredFieldStyle : '')};
${props => (props.isWrapped ? wrappedFieldStyle : unwrappedFieldStyle)};
`;
// const HighlightSpan = euiStyled.span`
// display: inline-block;
// background-color: ${props => props.theme.eui.euiColorSecondary};
// color: ${props => props.theme.eui.euiColorGhost};
// font-weight: ${props => props.theme.eui.euiFontWeightMedium};
// `;
const formatMessageSegment = (messageSegment: LogEntryMessageSegment): string => {
if (isFieldSegment(messageSegment)) {
return messageSegment.value;
} else if (isConstantSegment(messageSegment)) {
return messageSegment.constant;
}
return 'failed to format message';
};

View file

@ -1,37 +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 React from 'react';
import { TextScale } from '../../../../common/log_text_scale';
import { StreamItem } from './item';
import { LogTextStreamLogEntryItemView } from './log_entry_item_view';
interface StreamItemProps {
item: StreamItem;
scale: TextScale;
wrap: boolean;
openFlyoutWithItem: (id: string) => void;
isHighlighted: boolean;
}
export const LogTextStreamItemView = React.forwardRef<Element, StreamItemProps>(
({ item, scale, wrap, openFlyoutWithItem, isHighlighted }, ref) => {
switch (item.kind) {
case 'logEntry':
return (
<LogTextStreamLogEntryItemView
boundingBoxRef={ref}
logEntry={item.logEntry}
scale={scale}
wrap={wrap}
openFlyoutWithItem={openFlyoutWithItem}
isHighlighted={isHighlighted}
/>
);
}
}
);

View file

@ -0,0 +1,81 @@
/*
* 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 euiStyled from '../../../../../../common/eui_styled_components';
import {
LogColumnConfiguration,
isMessageLogColumnConfiguration,
isTimestampLogColumnConfiguration,
} from '../../../utils/source_configuration';
const DATE_COLUMN_SLACK_FACTOR = 1.1;
const FIELD_COLUMN_MIN_WIDTH_CHARACTERS = 10;
const DETAIL_FLYOUT_ICON_MIN_WIDTH = 32;
const COLUMN_PADDING = 8;
interface LogEntryColumnProps {
baseWidth: string;
growWeight: number;
shrinkWeight: number;
}
export const LogEntryColumn = euiStyled.div.attrs<LogEntryColumnProps>({
role: 'cell',
})`
align-items: stretch;
display: flex;
flex-basis: ${props => props.baseWidth || '0%'};
flex-direction: row;
flex-grow: ${props => props.growWeight || 0};
flex-shrink: ${props => props.shrinkWeight || 0};
overflow: hidden;
`;
export const LogEntryColumnContent = euiStyled.div`
flex: 1 0 0%;
padding: 2px ${COLUMN_PADDING}px;
`;
export type LogEntryColumnWidth = Pick<
LogEntryColumnProps,
'baseWidth' | 'growWeight' | 'shrinkWeight'
>;
export const getColumnWidths = (
columns: LogColumnConfiguration[],
characterWidth: number,
formattedDateWidth: number
): LogEntryColumnWidth[] => [
...columns.map(column => {
if (isTimestampLogColumnConfiguration(column)) {
return {
growWeight: 0,
shrinkWeight: 0,
baseWidth: `${Math.ceil(characterWidth * formattedDateWidth * DATE_COLUMN_SLACK_FACTOR) +
2 * COLUMN_PADDING}px`,
};
} else if (isMessageLogColumnConfiguration(column)) {
return {
growWeight: 5,
shrinkWeight: 0,
baseWidth: '0%',
};
} else {
return {
growWeight: 1,
shrinkWeight: 0,
baseWidth: `${Math.ceil(characterWidth * FIELD_COLUMN_MIN_WIDTH_CHARACTERS) +
2 * COLUMN_PADDING}px`,
};
}
}),
// the detail flyout icon column
{
growWeight: 0,
shrinkWeight: 0,
baseWidth: `${DETAIL_FLYOUT_ICON_MIN_WIDTH + 2 * COLUMN_PADDING}px`,
},
];

View file

@ -8,50 +8,56 @@ import { darken, transparentize } from 'polished';
import React, { useMemo } from 'react';
import { css } from '../../../../../../common/eui_styled_components';
import { TextScale } from '../../../../common/log_text_scale';
import { LogTextStreamItemField } from './item_field';
import { LogEntryColumnContent } from './log_entry_column';
interface LogTextStreamItemFieldFieldProps {
dataTestSubj?: string;
interface LogEntryFieldColumnProps {
encodedValue: string;
isHighlighted: boolean;
isHovered: boolean;
scale: TextScale;
isWrapped: boolean;
}
export const LogTextStreamItemFieldField: React.FunctionComponent<
LogTextStreamItemFieldFieldProps
> = ({ dataTestSubj, encodedValue, isHighlighted, isHovered, scale }) => {
export const LogEntryFieldColumn: React.FunctionComponent<LogEntryFieldColumnProps> = ({
encodedValue,
isHighlighted,
isHovered,
isWrapped,
}) => {
const value = useMemo(() => JSON.parse(encodedValue), [encodedValue]);
return (
<LogTextStreamItemFieldFieldWrapper
data-test-subj={dataTestSubj}
isHighlighted={isHighlighted}
isHovered={isHovered}
scale={scale}
>
<FieldColumnContent isHighlighted={isHighlighted} isHovered={isHovered} isWrapped={isWrapped}>
{value}
</LogTextStreamItemFieldFieldWrapper>
</FieldColumnContent>
);
};
const hoveredFieldStyle = css`
const hoveredContentStyle = css`
background-color: ${props =>
props.theme.darkMode
? transparentize(0.9, darken(0.05, props.theme.eui.euiColorHighlight))
: darken(0.05, props.theme.eui.euiColorHighlight)};
`;
const LogTextStreamItemFieldFieldWrapper = LogTextStreamItemField.extend.attrs<{
const wrappedContentStyle = css`
overflow: visible;
white-space: pre-wrap;
word-break: break-all;
`;
const unwrappedContentStyle = css`
overflow: hidden;
white-space: pre;
`;
const FieldColumnContent = LogEntryColumnContent.extend.attrs<{
isHighlighted: boolean;
isHovered: boolean;
isWrapped?: boolean;
}>({})`
flex: 1 0 0%;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
background-color: ${props => props.theme.eui.euiColorEmptyShade};
text-overflow: ellipsis;
${props => (props.isHovered || props.isHighlighted ? hoveredFieldStyle : '')};
${props => (props.isHovered || props.isHighlighted ? hoveredContentStyle : '')};
${props => (props.isWrapped ? wrappedContentStyle : unwrappedContentStyle)};
`;

View file

@ -0,0 +1,68 @@
/*
* 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 { EuiButtonIcon } from '@elastic/eui';
import { injectI18n } from '@kbn/i18n/react';
import React from 'react';
import { LogEntryColumnContent } from './log_entry_column';
import { hoveredContentStyle } from './text_styles';
import euiStyled from '../../../../../../common/eui_styled_components';
interface LogEntryIconColumnProps {
isHighlighted: boolean;
isHovered: boolean;
}
export const LogEntryIconColumn: React.FunctionComponent<LogEntryIconColumnProps> = ({
children,
isHighlighted,
isHovered,
}) => {
return (
<IconColumnContent isHighlighted={isHighlighted} isHovered={isHovered}>
{children}
</IconColumnContent>
);
};
export const LogEntryDetailsIconColumn = injectI18n<
LogEntryIconColumnProps & {
openFlyout: () => void;
}
>(({ intl, isHighlighted, isHovered, openFlyout }) => {
const label = intl.formatMessage({
id: 'xpack.infra.logEntryItemView.viewDetailsToolTip',
defaultMessage: 'View Details',
});
return (
<LogEntryIconColumn isHighlighted={isHighlighted} isHovered={isHovered}>
{isHovered ? (
<AbsoluteIconButtonWrapper>
<EuiButtonIcon onClick={openFlyout} iconType="expand" title={label} aria-label={label} />
</AbsoluteIconButtonWrapper>
) : null}
</LogEntryIconColumn>
);
});
const IconColumnContent = LogEntryColumnContent.extend.attrs<{
isHighlighted: boolean;
isHovered: boolean;
}>({})`
background-color: ${props => props.theme.eui.euiColorEmptyShade};
overflow: hidden;
user-select: none;
${props => (props.isHovered || props.isHighlighted ? hoveredContentStyle : '')};
`;
// this prevents the button from influencing the line height
const AbsoluteIconButtonWrapper = euiStyled.div`
overflow: hidden;
position: absolute;
`;

View file

@ -1,163 +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 { darken, transparentize } from 'polished';
import React, { useState, useCallback, Fragment } from 'react';
import { EuiButtonIcon, EuiToolTip } from '@elastic/eui';
import { injectI18n, InjectedIntl } from '@kbn/i18n/react';
import euiStyled from '../../../../../../common/eui_styled_components';
import {
LogEntry,
isFieldColumn,
isMessageColumn,
isTimestampColumn,
} from '../../../utils/log_entry';
import { TextScale } from '../../../../common/log_text_scale';
import { LogTextStreamItemDateField } from './item_date_field';
import { LogTextStreamItemFieldField } from './item_field_field';
import { LogTextStreamItemMessageField } from './item_message_field';
interface LogTextStreamLogEntryItemViewProps {
boundingBoxRef?: React.Ref<Element>;
isHighlighted: boolean;
intl: InjectedIntl;
logEntry: LogEntry;
openFlyoutWithItem: (id: string) => void;
scale: TextScale;
wrap: boolean;
}
export const LogTextStreamLogEntryItemView = injectI18n(
({
boundingBoxRef,
isHighlighted,
intl,
logEntry,
openFlyoutWithItem,
scale,
wrap,
}: LogTextStreamLogEntryItemViewProps) => {
const [isHovered, setIsHovered] = useState(false);
const setItemIsHovered = useCallback(() => {
setIsHovered(true);
}, []);
const setItemIsNotHovered = useCallback(() => {
setIsHovered(false);
}, []);
const openFlyout = useCallback(() => openFlyoutWithItem(logEntry.gid), [
openFlyoutWithItem,
logEntry.gid,
]);
return (
<LogTextStreamLogEntryItemDiv
data-test-subj="streamEntry logTextStreamEntry"
innerRef={
/* Workaround for missing RefObject support in styled-components */
boundingBoxRef as any
}
onMouseEnter={setItemIsHovered}
onMouseLeave={setItemIsNotHovered}
>
{logEntry.columns.map((column, columnIndex) => {
if (isTimestampColumn(column)) {
return (
<LogTextStreamItemDateField
dataTestSubj="logColumn timestampLogColumn"
hasHighlights={false}
isHighlighted={isHighlighted}
isHovered={isHovered}
key={`${columnIndex}`}
scale={scale}
time={column.timestamp}
/>
);
} else if (isMessageColumn(column)) {
const viewDetailsLabel = intl.formatMessage({
id: 'xpack.infra.logEntryItemView.viewDetailsToolTip',
defaultMessage: 'View Details',
});
return (
<Fragment key={`${columnIndex}`}>
<LogTextStreamIconDiv isHighlighted={isHighlighted} isHovered={isHovered}>
{isHovered ? (
<EuiToolTip content={viewDetailsLabel}>
<EuiButtonIcon
onClick={openFlyout}
iconType="expand"
aria-label={viewDetailsLabel}
/>
</EuiToolTip>
) : (
<EmptyIcon />
)}
</LogTextStreamIconDiv>
<LogTextStreamItemMessageField
dataTestSubj="logColumn messageLogColumn"
isHighlighted={isHighlighted}
isHovered={isHovered}
isWrapped={wrap}
scale={scale}
segments={column.message}
/>
</Fragment>
);
} else if (isFieldColumn(column)) {
return (
<LogTextStreamItemFieldField
dataTestSubj={`logColumn fieldLogColumn fieldLogColumn:${column.field}`}
encodedValue={column.value}
isHighlighted={isHighlighted}
isHovered={isHovered}
key={`${columnIndex}`}
scale={scale}
/>
);
}
})}
</LogTextStreamLogEntryItemDiv>
);
}
);
interface IconProps {
isHovered: boolean;
isHighlighted: boolean;
}
const EmptyIcon = euiStyled.div`
width: 24px;
`;
const LogTextStreamIconDiv = euiStyled<IconProps, 'div'>('div')`
flex-grow: 0;
background-color: ${props =>
props.isHovered || props.isHighlighted
? 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;
font-size: 0.9em;
`;
const LogTextStreamLogEntryItemDiv = euiStyled.div`
font-family: ${props => props.theme.eui.euiCodeFontFamily};
font-size: ${props => props.theme.eui.euiFontSize};
line-height: ${props => props.theme.eui.euiLineHeight};
color: ${props => props.theme.eui.euiTextColor};
overflow: hidden;
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: flex-start;
align-items: stretch;
`;

View file

@ -0,0 +1,72 @@
/*
* 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, { memo, useMemo } from 'react';
import { css } from '../../../../../../common/eui_styled_components';
import {
isConstantSegment,
isFieldSegment,
LogEntryMessageSegment,
} from '../../../utils/log_entry';
import { LogEntryColumnContent } from './log_entry_column';
import { hoveredContentStyle } from './text_styles';
interface LogEntryMessageColumnProps {
segments: LogEntryMessageSegment[];
isHovered: boolean;
isWrapped: boolean;
isHighlighted: boolean;
}
export const LogEntryMessageColumn = memo<LogEntryMessageColumnProps>(
({ isHighlighted, isHovered, isWrapped, segments }) => {
const message = useMemo(() => segments.map(formatMessageSegment).join(''), [segments]);
return (
<MessageColumnContent
isHighlighted={isHighlighted}
isHovered={isHovered}
isWrapped={isWrapped}
>
{message}
</MessageColumnContent>
);
}
);
const wrappedContentStyle = css`
overflow: visible;
white-space: pre-wrap;
word-break: break-all;
`;
const unwrappedContentStyle = css`
overflow: hidden;
white-space: pre;
`;
const MessageColumnContent = LogEntryColumnContent.extend.attrs<{
isHovered: boolean;
isHighlighted: boolean;
isWrapped?: boolean;
}>({})`
background-color: ${props => props.theme.eui.euiColorEmptyShade};
text-overflow: ellipsis;
${props => (props.isHovered || props.isHighlighted ? hoveredContentStyle : '')};
${props => (props.isWrapped ? wrappedContentStyle : unwrappedContentStyle)};
`;
const formatMessageSegment = (messageSegment: LogEntryMessageSegment): string => {
if (isFieldSegment(messageSegment)) {
return messageSegment.value;
} else if (isConstantSegment(messageSegment)) {
return messageSegment.constant;
}
return 'failed to format message';
};

View file

@ -0,0 +1,158 @@
/*
* 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 { darken, transparentize } from 'polished';
import React, { useState, useCallback, useMemo } from 'react';
import euiStyled from '../../../../../../common/eui_styled_components';
import {
LogEntry,
isFieldColumn,
isMessageColumn,
isTimestampColumn,
} from '../../../utils/log_entry';
import {
LogColumnConfiguration,
isTimestampLogColumnConfiguration,
isMessageLogColumnConfiguration,
isFieldLogColumnConfiguration,
} from '../../../utils/source_configuration';
import { TextScale } from '../../../../common/log_text_scale';
import { LogEntryColumn, LogEntryColumnWidth } from './log_entry_column';
import { LogEntryFieldColumn } from './log_entry_field_column';
import { LogEntryDetailsIconColumn } from './log_entry_icon_column';
import { LogEntryMessageColumn } from './log_entry_message_column';
import { LogEntryTimestampColumn } from './log_entry_timestamp_column';
import { monospaceTextStyle } from './text_styles';
interface LogEntryRowProps {
boundingBoxRef?: React.Ref<Element>;
columnConfigurations: LogColumnConfiguration[];
columnWidths: LogEntryColumnWidth[];
isHighlighted: boolean;
logEntry: LogEntry;
openFlyoutWithItem: (id: string) => void;
scale: TextScale;
wrap: boolean;
}
export const LogEntryRow = ({
boundingBoxRef,
columnConfigurations,
columnWidths,
isHighlighted,
logEntry,
openFlyoutWithItem,
scale,
wrap,
}: LogEntryRowProps) => {
const [isHovered, setIsHovered] = useState(false);
const setItemIsHovered = useCallback(() => {
setIsHovered(true);
}, []);
const setItemIsNotHovered = useCallback(() => {
setIsHovered(false);
}, []);
const openFlyout = useCallback(() => openFlyoutWithItem(logEntry.gid), [
openFlyoutWithItem,
logEntry.gid,
]);
const iconColumnWidth = useMemo(() => columnWidths[columnWidths.length - 1], [columnWidths]);
return (
<LogEntryRowWrapper
data-test-subj="streamEntry logTextStreamEntry"
innerRef={
/* Workaround for missing RefObject support in styled-components */
boundingBoxRef as any
}
onMouseEnter={setItemIsHovered}
onMouseLeave={setItemIsNotHovered}
scale={scale}
>
{logEntry.columns.map((column, columnIndex) => {
const columnConfiguration = columnConfigurations[columnIndex];
const columnWidth = columnWidths[columnIndex];
if (isTimestampColumn(column) && isTimestampLogColumnConfiguration(columnConfiguration)) {
return (
<LogEntryColumn
data-test-subj="logColumn timestampLogColumn"
key={columnConfiguration.timestampColumn.id}
{...columnWidth}
>
<LogEntryTimestampColumn
isHighlighted={isHighlighted}
isHovered={isHovered}
time={column.timestamp}
/>
</LogEntryColumn>
);
} else if (
isMessageColumn(column) &&
isMessageLogColumnConfiguration(columnConfiguration)
) {
return (
<LogEntryColumn
data-test-subj="logColumn messageLogColumn"
key={columnConfiguration.messageColumn.id}
{...columnWidth}
>
<LogEntryMessageColumn
isHighlighted={isHighlighted}
isHovered={isHovered}
isWrapped={wrap}
segments={column.message}
/>
</LogEntryColumn>
);
} else if (isFieldColumn(column) && isFieldLogColumnConfiguration(columnConfiguration)) {
return (
<LogEntryColumn
data-test-subj={`logColumn fieldLogColumn fieldLogColumn:${column.field}`}
key={columnConfiguration.fieldColumn.id}
{...columnWidth}
>
<LogEntryFieldColumn
isHighlighted={isHighlighted}
isHovered={isHovered}
isWrapped={wrap}
encodedValue={column.value}
/>
</LogEntryColumn>
);
}
})}
<LogEntryColumn key="logColumn iconLogColumn iconLogColumn:details" {...iconColumnWidth}>
<LogEntryDetailsIconColumn
isHighlighted={isHighlighted}
isHovered={isHovered}
openFlyout={openFlyout}
/>
</LogEntryColumn>
</LogEntryRowWrapper>
);
};
const LogEntryRowWrapper = euiStyled.div.attrs<{
scale: TextScale;
}>({
role: 'row',
})`
align-items: stretch;
color: ${props => props.theme.eui.euiTextColor};
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: flex-start;
overflow: hidden;
${props => monospaceTextStyle(props.scale)}
`;

View file

@ -1,31 +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 React from 'react';
import euiStyled from '../../../../../../common/eui_styled_components';
import { LogEntry } from '../../../../common/log_entry';
interface LogEntryStreamItemViewProps {
boundingBoxRef?: React.Ref<{}>;
item: LogEntry;
}
export class LogEntryStreamItemView extends React.PureComponent<LogEntryStreamItemViewProps, {}> {
public render() {
const { boundingBoxRef, item } = this.props;
return (
// @ts-ignore: silence error until styled-components supports React.RefObject<T>
<LogEntryDiv innerRef={boundingBoxRef}>{JSON.stringify(item)}</LogEntryDiv>
);
}
}
const LogEntryDiv = euiStyled.div`
border-top: 1px solid red;
border-bottom: 1px solid green;
`;

View file

@ -8,49 +8,28 @@ import { darken, transparentize } from 'polished';
import React, { memo } from 'react';
import { css } from '../../../../../../common/eui_styled_components';
import { TextScale } from '../../../../common/log_text_scale';
import { tintOrShade } from '../../../utils/styles';
import { useFormattedTime } from '../../formatted_time';
import { LogTextStreamItemField } from './item_field';
import { LogEntryColumnContent } from './log_entry_column';
interface LogTextStreamItemDateFieldProps {
dataTestSubj?: string;
hasHighlights: boolean;
interface LogEntryTimestampColumnProps {
isHighlighted: boolean;
isHovered: boolean;
scale: TextScale;
time: number;
}
export const LogTextStreamItemDateField = memo<LogTextStreamItemDateFieldProps>(
({ dataTestSubj, hasHighlights, isHighlighted, isHovered, scale, time }) => {
export const LogEntryTimestampColumn = memo<LogEntryTimestampColumnProps>(
({ isHighlighted, isHovered, time }) => {
const formattedTime = useFormattedTime(time);
return (
<LogTextStreamItemDateFieldWrapper
data-test-subj={dataTestSubj}
hasHighlights={hasHighlights}
isHovered={isHovered}
isHighlighted={isHighlighted}
scale={scale}
>
<TimestampColumnContent isHovered={isHovered} isHighlighted={isHighlighted}>
{formattedTime}
</LogTextStreamItemDateFieldWrapper>
</TimestampColumnContent>
);
}
);
const highlightedFieldStyle = css`
background-color: ${props =>
tintOrShade(
props.theme.eui.euiTextColor as any,
props.theme.eui.euiColorSecondary as any,
0.15
)};
border-color: ${props => props.theme.eui.euiColorSecondary};
`;
const hoveredFieldStyle = css`
const hoveredContentStyle = css`
background-color: ${props =>
props.theme.darkMode
? transparentize(0.9, darken(0.05, props.theme.eui.euiColorHighlight))
@ -62,16 +41,17 @@ const hoveredFieldStyle = css`
color: ${props => props.theme.eui.euiColorFullShade};
`;
const LogTextStreamItemDateFieldWrapper = LogTextStreamItemField.extend.attrs<{
hasHighlights: boolean;
const TimestampColumnContent = LogEntryColumnContent.extend.attrs<{
isHovered: boolean;
isHighlighted: boolean;
}>({})`
background-color: ${props => props.theme.eui.euiColorLightestShade};
border-right: solid 2px ${props => props.theme.eui.euiColorLightShade};
color: ${props => props.theme.eui.euiColorDarkShade};
overflow: hidden;
text-align: right;
text-overflow: clip;
white-space: pre;
${props => (props.hasHighlights ? highlightedFieldStyle : '')};
${props => (props.isHovered || props.isHighlighted ? hoveredFieldStyle : '')};
${props => (props.isHovered || props.isHighlighted ? hoveredContentStyle : '')};
`;

View file

@ -5,22 +5,28 @@
*/
import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react';
import React from 'react';
import React, { useMemo } from 'react';
import euiStyled from '../../../../../../common/eui_styled_components';
import { TextScale } from '../../../../common/log_text_scale';
import { TimeKey } from '../../../../common/time';
import { callWithoutRepeats } from '../../../utils/handlers';
import { LogColumnConfiguration } from '../../../utils/source_configuration';
import { AutoSizer } from '../../auto_sizer';
import { NoData } from '../../empty_states';
import { useFormattedTime } from '../../formatted_time';
import { InfraLoadingPanel } from '../../loading';
import { getStreamItemBeforeTimeKey, getStreamItemId, parseStreamItemId, StreamItem } from './item';
import { LogTextStreamItemView } from './item_view';
import { LogColumnHeaders } from './column_headers';
import { LogTextStreamLoadingItemView } from './loading_item_view';
import { LogEntryRow } from './log_entry_row';
import { MeasurableItemView } from './measurable_item_view';
import { VerticalScrollPanel } from './vertical_scroll_panel';
import { getColumnWidths, LogEntryColumnWidth } from './log_entry_column';
import { useMeasuredCharacterDimensions } from './text_styles';
interface ScrollableLogTextStreamViewProps {
columnConfigurations: LogColumnConfiguration[];
items: StreamItem[];
scale: TextScale;
wrap: boolean;
@ -44,6 +50,7 @@ interface ScrollableLogTextStreamViewProps {
loadNewerItems: () => void;
setFlyoutItem: (id: string) => void;
setFlyoutVisibility: (visible: boolean) => void;
showColumnConfiguration: () => void;
intl: InjectedIntl;
highlightedItem: string | null;
}
@ -91,17 +98,19 @@ class ScrollableLogTextStreamViewClass extends React.PureComponent<
public render() {
const {
items,
scale,
wrap,
isReloading,
isLoadingMore,
hasMoreBeforeStart,
columnConfigurations,
hasMoreAfterEnd,
isStreaming,
lastLoadedTime,
intl,
hasMoreBeforeStart,
highlightedItem,
intl,
isLoadingMore,
isReloading,
isStreaming,
items,
lastLoadedTime,
scale,
showColumnConfiguration,
wrap,
} = this.props;
const { targetId } = this.state;
const hasItems = items.length > 0;
@ -137,60 +146,76 @@ class ScrollableLogTextStreamViewClass extends React.PureComponent<
testString="logsNoDataPrompt"
/>
) : (
<AutoSizer content>
{({ measureRef, content: { width = 0, height = 0 } }) => (
<ScrollPanelSizeProbe innerRef={measureRef}>
<VerticalScrollPanel
height={height}
width={width}
onVisibleChildrenChange={this.handleVisibleChildrenChange}
target={targetId}
hideScrollbar={true}
data-test-subj={'logStream'}
>
{registerChild => (
<>
<LogTextStreamLoadingItemView
alignment="bottom"
isLoading={isLoadingMore}
hasMore={hasMoreBeforeStart}
isStreaming={false}
lastStreamingUpdate={null}
/>
{items.map(item => (
<MeasurableItemView
register={registerChild}
registrationKey={getStreamItemId(item)}
key={getStreamItemId(item)}
>
{itemMeasureRef => (
<LogTextStreamItemView
openFlyoutWithItem={this.handleOpenFlyout}
ref={itemMeasureRef}
item={item}
scale={scale}
wrap={wrap}
isHighlighted={
highlightedItem ? item.logEntry.gid === highlightedItem : false
}
<WithColumnWidths columnConfigurations={columnConfigurations} scale={scale}>
{({ columnWidths, CharacterDimensionsProbe }) => (
<>
<CharacterDimensionsProbe />
<LogColumnHeaders
columnConfigurations={columnConfigurations}
columnWidths={columnWidths}
showColumnConfiguration={showColumnConfiguration}
/>
<AutoSizer content>
{({ measureRef, content: { width = 0, height = 0 } }) => (
<ScrollPanelSizeProbe innerRef={measureRef}>
<VerticalScrollPanel
height={height}
width={width}
onVisibleChildrenChange={this.handleVisibleChildrenChange}
target={targetId}
hideScrollbar={true}
data-test-subj={'logStream'}
>
{registerChild => (
<>
<LogTextStreamLoadingItemView
alignment="bottom"
isLoading={isLoadingMore}
hasMore={hasMoreBeforeStart}
isStreaming={false}
lastStreamingUpdate={null}
/>
)}
</MeasurableItemView>
))}
<LogTextStreamLoadingItemView
alignment="top"
isLoading={isStreaming || isLoadingMore}
hasMore={hasMoreAfterEnd}
isStreaming={isStreaming}
lastStreamingUpdate={isStreaming ? lastLoadedTime : null}
onLoadMore={this.handleLoadNewerItems}
/>
</>
{items.map(item => (
<MeasurableItemView
register={registerChild}
registrationKey={getStreamItemId(item)}
key={getStreamItemId(item)}
>
{itemMeasureRef => (
<LogEntryRow
columnConfigurations={columnConfigurations}
columnWidths={columnWidths}
openFlyoutWithItem={this.handleOpenFlyout}
boundingBoxRef={itemMeasureRef}
logEntry={item.logEntry}
scale={scale}
wrap={wrap}
isHighlighted={
highlightedItem
? item.logEntry.gid === highlightedItem
: false
}
/>
)}
</MeasurableItemView>
))}
<LogTextStreamLoadingItemView
alignment="top"
isLoading={isStreaming || isLoadingMore}
hasMore={hasMoreAfterEnd}
isStreaming={isStreaming}
lastStreamingUpdate={isStreaming ? lastLoadedTime : null}
onLoadMore={this.handleLoadNewerItems}
/>
</>
)}
</VerticalScrollPanel>
</ScrollPanelSizeProbe>
)}
</VerticalScrollPanel>
</ScrollPanelSizeProbe>
</AutoSizer>
</>
)}
</AutoSizer>
</WithColumnWidths>
)}
</ScrollableLogTextStreamViewWrapper>
);
@ -246,6 +271,39 @@ class ScrollableLogTextStreamViewClass extends React.PureComponent<
export const ScrollableLogTextStreamView = injectI18n(ScrollableLogTextStreamViewClass);
/**
* This function-as-child component calculates the column widths based on the
* given configuration. It depends on the `CharacterDimensionsProbe` it returns
* being rendered so it can measure the monospace character size.
*
* If the above component wasn't a class component, this would have been
* written as a hook.
*/
const WithColumnWidths: React.FunctionComponent<{
children: (
params: { columnWidths: LogEntryColumnWidth[]; CharacterDimensionsProbe: React.ComponentType }
) => React.ReactElement<any> | null;
columnConfigurations: LogColumnConfiguration[];
scale: TextScale;
}> = ({ children, columnConfigurations, scale }) => {
const { CharacterDimensionsProbe, dimensions } = useMeasuredCharacterDimensions(scale);
const referenceTime = useMemo(() => Date.now(), []);
const formattedCurrentDate = useFormattedTime(referenceTime);
const columnWidths = useMemo(
() => getColumnWidths(columnConfigurations, dimensions.width, formattedCurrentDate.length),
[columnConfigurations, dimensions.width, formattedCurrentDate]
);
const childParams = useMemo(
() => ({
columnWidths,
CharacterDimensionsProbe,
}),
[columnWidths, CharacterDimensionsProbe]
);
return children(childParams);
};
const ScrollableLogTextStreamViewWrapper = euiStyled.div`
overflow: hidden;
display: flex;

View file

@ -0,0 +1,88 @@
/*
* 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 { darken, transparentize } from 'polished';
import React, { useMemo, useState, useCallback } from 'react';
import euiStyled, { css } from '../../../../../../common/eui_styled_components';
import { TextScale } from '../../../../common/log_text_scale';
export const monospaceTextStyle = (scale: TextScale) => css`
font-family: ${props => props.theme.eui.euiCodeFontFamily};
font-size: ${props => {
switch (scale) {
case 'large':
return props.theme.eui.euiFontSizeM;
case 'medium':
return props.theme.eui.euiFontSizeS;
case 'small':
return props.theme.eui.euiFontSizeXS;
default:
return props.theme.eui.euiFontSize;
}
}}
line-height: ${props => props.theme.eui.euiLineHeight};
`;
export const hoveredContentStyle = css`
background-color: ${props =>
props.theme.darkMode
? transparentize(0.9, darken(0.05, props.theme.eui.euiColorHighlight))
: darken(0.05, props.theme.eui.euiColorHighlight)};
`;
interface CharacterDimensions {
height: number;
width: number;
}
export const useMeasuredCharacterDimensions = (scale: TextScale) => {
const [dimensions, setDimensions] = useState<CharacterDimensions>({
height: 0,
width: 0,
});
const measureElement = useCallback((element: Element | null) => {
if (!element) {
return;
}
const boundingBox = element.getBoundingClientRect();
setDimensions({
height: boundingBox.height,
width: boundingBox.width,
});
}, []);
const CharacterDimensionsProbe = useMemo(
() => () => (
<MonospaceCharacterDimensionsProbe scale={scale} innerRef={measureElement}>
X
</MonospaceCharacterDimensionsProbe>
),
[scale]
);
return {
CharacterDimensionsProbe,
dimensions,
};
};
const MonospaceCharacterDimensionsProbe = euiStyled.div.attrs<{
scale: TextScale;
}>({
'aria-hidden': true,
})`
visibility: hidden;
position: absolute;
height: auto;
width: auto;
padding: 0;
margin: 0;
${props => monospaceTextStyle(props.scale)}
`;

View file

@ -42,7 +42,7 @@ interface MeasurableChild {
}
const SCROLL_THROTTLE_INTERVAL = 250;
const ASSUMED_SCROLLBAR_WIDTH = 20;
export const ASSUMED_SCROLLBAR_WIDTH = 20;
export class VerticalScrollPanel<Child> extends React.PureComponent<
VerticalScrollPanelProps<Child>

View file

@ -17,7 +17,7 @@ interface InvalidNodeErrorProps {
}
export const InvalidNodeError: React.FunctionComponent<InvalidNodeErrorProps> = ({ nodeName }) => {
const { show } = useContext(SourceConfigurationFlyoutState.Context);
const { showIndicesConfiguration } = useContext(SourceConfigurationFlyoutState.Context);
return (
<WithKibanaChrome>
@ -57,7 +57,7 @@ export const InvalidNodeError: React.FunctionComponent<InvalidNodeErrorProps> =
</EuiButton>
</EuiFlexItem>
<EuiFlexItem>
<EuiButton color="primary" onClick={show}>
<EuiButton color="primary" onClick={showIndicesConfiguration}>
<FormattedMessage
id="xpack.infra.configureSourceActionLabel"
defaultMessage="Change source configuration"

View file

@ -29,7 +29,7 @@ export const AddLogColumnButtonAndPopover: React.FunctionComponent<{
() => [
{
optionProps: {
append: <BuiltinBadge />,
append: <SystemColumnBadge />,
'data-test-subj': 'addTimestampLogColumn',
// this key works around EuiSelectable using a lowercased label as
// key, which leads to conflicts with field names
@ -45,7 +45,7 @@ export const AddLogColumnButtonAndPopover: React.FunctionComponent<{
{
optionProps: {
'data-test-subj': 'addMessageLogColumn',
append: <BuiltinBadge />,
append: <SystemColumnBadge />,
// this key works around EuiSelectable using a lowercased label as
// key, which leads to conflicts with field names
key: 'message',
@ -158,11 +158,11 @@ const usePopoverVisibilityState = (initialState: boolean) => {
);
};
const BuiltinBadge: React.FunctionComponent = () => (
const SystemColumnBadge: React.FunctionComponent = () => (
<EuiBadge>
<FormattedMessage
id="xpack.infra.sourceConfiguration.builtInColumnBadgeLabel"
defaultMessage="Built-in"
id="xpack.infra.sourceConfiguration.systemColumnBadgeLabel"
defaultMessage="System"
/>
</EuiBadge>
);

View file

@ -100,7 +100,7 @@ const TimestampLogColumnConfigurationPanel: React.FunctionComponent<
<FormattedMessage
tagName="span"
id="xpack.infra.sourceConfiguration.timestampLogColumnDescription"
defaultMessage="This built-in field shows the log entry's time as determined by the {timestampSetting} field setting."
defaultMessage="This system field shows the log entry's time as determined by the {timestampSetting} field setting."
values={{
timestampSetting: <code>timestamp</code>,
}}
@ -119,7 +119,7 @@ const MessageLogColumnConfigurationPanel: React.FunctionComponent<
<FormattedMessage
tagName="span"
id="xpack.infra.sourceConfiguration.messageLogColumnDescription"
defaultMessage="This built-in field shows the log entry message as derived from the document fields."
defaultMessage="This system field shows the log entry message as derived from the document fields."
/>
}
removeColumn={logColumnConfigurationProps.remove}
@ -158,7 +158,7 @@ const ExplainedLogColumnConfigurationPanel: React.FunctionComponent<{
removeColumn: () => void;
}> = ({ fieldName, helpText, removeColumn }) => (
<EuiPanel
data-test-subj={`logColumnPanel builtInLogColumnPanel builtInLogColumnPanel:${fieldName}`}
data-test-subj={`logColumnPanel systemLogColumnPanel systemLogColumnPanel:${fieldName}`}
>
<EuiFlexGroup>
<EuiFlexItem grow={1}>{fieldName}</EuiFlexItem>

View file

@ -5,14 +5,13 @@
*/
import { EuiButtonEmpty } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import React, { useContext } from 'react';
import { Source } from '../../containers/source';
import { SourceConfigurationFlyoutState } from './source_configuration_flyout_state';
export const SourceConfigurationButton: React.FunctionComponent = () => {
const { toggleIsVisible } = useContext(SourceConfigurationFlyoutState.Context);
const { source } = useContext(Source.Context);
return (
<EuiButtonEmpty
@ -21,8 +20,12 @@ export const SourceConfigurationButton: React.FunctionComponent = () => {
data-test-subj="configureSourceButton"
iconType="gear"
onClick={toggleIsVisible}
size="xs"
>
{source && source.configuration && source.configuration.name}
<FormattedMessage
id="xpack.infra.sourceConfiguration.sourceConfigurationButtonLabel"
defaultMessage="Configuration"
/>
</EuiButtonEmpty>
);
};

View file

@ -27,7 +27,7 @@ import { FieldsConfigurationPanel } from './fields_configuration_panel';
import { IndicesConfigurationPanel } from './indices_configuration_panel';
import { NameConfigurationPanel } from './name_configuration_panel';
import { LogColumnsConfigurationPanel } from './log_columns_configuration_panel';
import { SourceConfigurationFlyoutState } from './source_configuration_flyout_state';
import { isValidTabId, SourceConfigurationFlyoutState } from './source_configuration_flyout_state';
import { useSourceConfigurationFormState } from './source_configuration_form_state';
const noop = () => undefined;
@ -39,7 +39,9 @@ interface SourceConfigurationFlyoutProps {
export const SourceConfigurationFlyout = injectI18n(
({ intl, shouldAllowEdit }: SourceConfigurationFlyoutProps) => {
const { isVisible, hide } = useContext(SourceConfigurationFlyoutState.Context);
const { activeTabId, hide, isVisible, setActiveTab } = useContext(
SourceConfigurationFlyoutState.Context
);
const {
createSourceConfiguration,
@ -89,65 +91,93 @@ export const SourceConfigurationFlyout = injectI18n(
source,
]);
const tabs: EuiTabbedContentTab[] = useMemo(
() =>
isVisible
? [
{
id: 'indicesAndFieldsTab',
name: intl.formatMessage({
id: 'xpack.infra.sourceConfiguration.sourceConfigurationIndicesTabTitle',
defaultMessage: 'Indices and fields',
}),
content: (
<>
<EuiSpacer />
<NameConfigurationPanel
isLoading={isLoading}
nameFieldProps={indicesConfigurationProps.name}
readOnly={!isWriteable}
/>
<EuiSpacer />
<IndicesConfigurationPanel
isLoading={isLoading}
logAliasFieldProps={indicesConfigurationProps.logAlias}
metricAliasFieldProps={indicesConfigurationProps.metricAlias}
readOnly={!isWriteable}
/>
<EuiSpacer />
<FieldsConfigurationPanel
containerFieldProps={indicesConfigurationProps.containerField}
hostFieldProps={indicesConfigurationProps.hostField}
isLoading={isLoading}
podFieldProps={indicesConfigurationProps.podField}
readOnly={!isWriteable}
tiebreakerFieldProps={indicesConfigurationProps.tiebreakerField}
timestampFieldProps={indicesConfigurationProps.timestampField}
/>
</>
),
},
{
id: 'logsTab',
name: intl.formatMessage({
id: 'xpack.infra.sourceConfiguration.sourceConfigurationLogColumnsTabTitle',
defaultMessage: 'Log Columns',
}),
content: (
<>
<EuiSpacer />
<LogColumnsConfigurationPanel
addLogColumn={addLogColumn}
availableFields={availableFields}
isLoading={isLoading}
logColumnConfiguration={logColumnConfigurationProps}
/>
</>
),
},
]
: [],
[
addLogColumn,
availableFields,
indicesConfigurationProps,
intl.formatMessage,
isLoading,
isVisible,
logColumnConfigurationProps,
isWriteable,
]
);
const activeTab = useMemo(() => tabs.filter(tab => tab.id === activeTabId)[0] || tabs[0], [
activeTabId,
tabs,
]);
const activateTab = useCallback(
(tab: EuiTabbedContentTab) => {
const tabId = tab.id;
if (isValidTabId(tabId)) {
setActiveTab(tabId);
}
},
[setActiveTab, isValidTabId]
);
if (!isVisible || !source || !source.configuration) {
return null;
}
const tabs: EuiTabbedContentTab[] = [
{
id: 'indicesAndFieldsTab',
name: intl.formatMessage({
id: 'xpack.infra.sourceConfiguration.sourceConfigurationIndicesTabTitle',
defaultMessage: 'Indices and fields',
}),
content: (
<>
<EuiSpacer />
<NameConfigurationPanel
isLoading={isLoading}
nameFieldProps={indicesConfigurationProps.name}
readOnly={!isWriteable}
/>
<EuiSpacer />
<IndicesConfigurationPanel
isLoading={isLoading}
logAliasFieldProps={indicesConfigurationProps.logAlias}
metricAliasFieldProps={indicesConfigurationProps.metricAlias}
readOnly={!isWriteable}
/>
<EuiSpacer />
<FieldsConfigurationPanel
containerFieldProps={indicesConfigurationProps.containerField}
hostFieldProps={indicesConfigurationProps.hostField}
isLoading={isLoading}
podFieldProps={indicesConfigurationProps.podField}
readOnly={!isWriteable}
tiebreakerFieldProps={indicesConfigurationProps.tiebreakerField}
timestampFieldProps={indicesConfigurationProps.timestampField}
/>
</>
),
},
{
id: 'logsTab',
name: intl.formatMessage({
id: 'xpack.infra.sourceConfiguration.sourceConfigurationLogColumnsTabTitle',
defaultMessage: 'Log Columns',
}),
content: (
<>
<EuiSpacer />
<LogColumnsConfigurationPanel
addLogColumn={addLogColumn}
availableFields={availableFields}
isLoading={isLoading}
logColumnConfiguration={logColumnConfigurationProps}
/>
</>
),
},
];
return (
<EuiFlyout
aria-labelledby="sourceConfigurationTitle"
@ -173,7 +203,7 @@ export const SourceConfigurationFlyout = injectI18n(
</EuiTitle>
</EuiFlyoutHeader>
<EuiFlyoutBody>
<EuiTabbedContent tabs={tabs} />
<EuiTabbedContent onTabClick={activateTab} selectedTab={activeTab} tabs={tabs} />
</EuiFlyoutBody>
<EuiFlyoutFooter>
{errors.length > 0 ? (

View file

@ -7,27 +7,49 @@
import createContainer from 'constate-latest';
import { useCallback, useState } from 'react';
type TabId = 'indicesAndFieldsTab' | 'logsTab';
const validTabIds: TabId[] = ['indicesAndFieldsTab', 'logsTab'];
export const useSourceConfigurationFlyoutState = ({
initialVisibility = false,
initialTab = 'indicesAndFieldsTab',
}: {
initialVisibility?: boolean;
initialTab?: TabId;
} = {}) => {
const [isVisible, setIsVisible] = useState<boolean>(initialVisibility);
const [activeTabId, setActiveTab] = useState(initialTab);
const toggleIsVisible = useCallback(
() => setIsVisible(isCurrentlyVisible => !isCurrentlyVisible),
[setIsVisible]
);
const show = useCallback(() => setIsVisible(true), [setIsVisible]);
const show = useCallback(
(tabId?: TabId) => {
if (tabId != null) {
setActiveTab(tabId);
}
setIsVisible(true);
},
[setIsVisible]
);
const showIndicesConfiguration = useCallback(() => show('indicesAndFieldsTab'), [show]);
const showLogsConfiguration = useCallback(() => show('logsTab'), [show]);
const hide = useCallback(() => setIsVisible(false), [setIsVisible]);
return {
activeTabId,
hide,
isVisible,
setActiveTab,
show,
showIndicesConfiguration,
showLogsConfiguration,
toggleIsVisible,
};
};
export const isValidTabId = (value: any): value is TabId => validTabIds.includes(value);
export const SourceConfigurationFlyoutState = createContainer(useSourceConfigurationFlyoutState);

View file

@ -35,7 +35,7 @@ interface SnapshotPageProps {
export const SnapshotPage = injectUICapabilities(
injectI18n((props: SnapshotPageProps) => {
const { intl, uiCapabilities } = props;
const { show } = useContext(SourceConfigurationFlyoutState.Context);
const { showIndicesConfiguration } = useContext(SourceConfigurationFlyoutState.Context);
const {
derivedIndexPattern,
hasFailedLoadingSource,
@ -119,7 +119,7 @@ export const SnapshotPage = injectUICapabilities(
<EuiButton
data-test-subj="configureSourceButton"
color="primary"
onClick={show}
onClick={showIndicesConfiguration}
>
{intl.formatMessage({
id: 'xpack.infra.configureSourceActionLabel',

View file

@ -28,9 +28,10 @@ import { ReduxSourceIdBridge, WithStreamItems } from '../../containers/logs/with
import { Source } from '../../containers/source';
import { LogsToolbar } from './page_toolbar';
import { SourceConfigurationFlyoutState } from '../../components/source_configuration';
export const LogsPageLogsContent: React.FunctionComponent = () => {
const { derivedIndexPattern, sourceId, version } = useContext(Source.Context);
const { derivedIndexPattern, source, sourceId, version } = useContext(Source.Context);
const { intervalSize, textScale, textWrap } = useContext(LogViewConfiguration.Context);
const {
setFlyoutVisibility,
@ -41,6 +42,7 @@ export const LogsPageLogsContent: React.FunctionComponent = () => {
flyoutItem,
isLoading,
} = useContext(LogFlyoutState.Context);
const { showLogsConfiguration } = useContext(SourceConfigurationFlyoutState.Context);
return (
<>
@ -86,6 +88,7 @@ export const LogsPageLogsContent: React.FunctionComponent = () => {
loadNewerEntries,
}) => (
<ScrollableLogTextStreamView
columnConfigurations={(source && source.configuration.logColumns) || []}
hasMoreAfterEnd={hasMoreAfterEnd}
hasMoreBeforeStart={hasMoreBeforeStart}
isLoadingMore={isLoadingMore}
@ -97,6 +100,7 @@ export const LogsPageLogsContent: React.FunctionComponent = () => {
loadNewerItems={loadNewerEntries}
reportVisibleInterval={reportVisiblePositions}
scale={textScale}
showColumnConfiguration={showLogsConfiguration}
target={targetPosition}
wrap={textWrap}
setFlyoutItem={setFlyoutId}

View file

@ -22,7 +22,7 @@ interface LogsPageNoIndicesContentProps {
export const LogsPageNoIndicesContent = injectUICapabilities(
injectI18n((props: LogsPageNoIndicesContentProps) => {
const { intl, uiCapabilities } = props;
const { show } = useContext(SourceConfigurationFlyoutState.Context);
const { showIndicesConfiguration } = useContext(SourceConfigurationFlyoutState.Context);
return (
<WithKibanaChrome>
@ -57,7 +57,7 @@ export const LogsPageNoIndicesContent = injectUICapabilities(
<EuiButton
data-test-subj="configureSourceButton"
color="primary"
onClick={show}
onClick={showIndicesConfiguration}
>
{intl.formatMessage({
id: 'xpack.infra.configureSourceActionLabel',

View file

@ -35,13 +35,13 @@ export const getLogEntryAtTime = (entries: LogEntry[], time: TimeKey) => {
};
export const isTimestampColumn = (column: LogEntryColumn): column is LogEntryTimestampColumn =>
'timestamp' in column;
column != null && 'timestamp' in column;
export const isMessageColumn = (column: LogEntryColumn): column is LogEntryMessageColumn =>
'message' in column;
column != null && 'message' in column;
export const isFieldColumn = (column: LogEntryColumn): column is LogEntryFieldColumn =>
'field' in column;
column != null && 'field' in column;
export const isConstantSegment = (
segment: LogEntryMessageSegment

View file

@ -15,14 +15,15 @@ export type TimestampLogColumnConfiguration = SourceConfigurationFields.InfraSou
export const isFieldLogColumnConfiguration = (
logColumnConfiguration: LogColumnConfiguration
): logColumnConfiguration is FieldLogColumnConfiguration => 'fieldColumn' in logColumnConfiguration;
): logColumnConfiguration is FieldLogColumnConfiguration =>
logColumnConfiguration != null && 'fieldColumn' in logColumnConfiguration;
export const isMessageLogColumnConfiguration = (
logColumnConfiguration: LogColumnConfiguration
): logColumnConfiguration is MessageLogColumnConfiguration =>
'messageColumn' in logColumnConfiguration;
logColumnConfiguration != null && 'messageColumn' in logColumnConfiguration;
export const isTimestampLogColumnConfiguration = (
logColumnConfiguration: LogColumnConfiguration
): logColumnConfiguration is TimestampLogColumnConfiguration =>
'timestampColumn' in logColumnConfiguration;
logColumnConfiguration != null && 'timestampColumn' in logColumnConfiguration;

View file

@ -72,7 +72,11 @@ export default ({ getPageObjects, getService }: KibanaFunctionalTestDefaultProvi
await pageObjects.infraLogs.getLogStream();
});
it('renders the default log columns', async () => {
it('renders the default log columns with their headers', async () => {
const columnHeaderLabels = await infraLogStream.getColumnHeaderLabels();
expect(columnHeaderLabels).to.eql(['Timestamp', 'event.dataset', 'Message', '']);
const logStreamEntries = await infraLogStream.getStreamEntries();
expect(logStreamEntries.length).to.be.greaterThan(0);
@ -97,7 +101,11 @@ export default ({ getPageObjects, getService }: KibanaFunctionalTestDefaultProvi
await infraSourceConfigurationFlyout.closeFlyout();
});
it('renders the changed log columns', async () => {
it('renders the changed log columns with their headers', async () => {
const columnHeaderLabels = await infraLogStream.getColumnHeaderLabels();
expect(columnHeaderLabels).to.eql(['Timestamp', 'host.name', '']);
const logStreamEntries = await infraLogStream.getStreamEntries();
expect(logStreamEntries.length).to.be.greaterThan(0);

View file

@ -11,6 +11,13 @@ export function InfraLogStreamProvider({ getService }: KibanaFunctionalTestDefau
const testSubjects = getService('testSubjects');
return {
async getColumnHeaderLabels(): Promise<string[]> {
const columnHeaderElements: WebElementWrapper[] = await testSubjects.findAll(
'logColumnHeader'
);
return await Promise.all(columnHeaderElements.map(element => element.getVisibleText()));
},
async getStreamEntries(): Promise<WebElementWrapper[]> {
return await testSubjects.findAll('streamEntry');
},

View file

@ -36,13 +36,13 @@ export function InfraSourceConfigurationFlyoutProvider({
/**
* Indices and fields
*/
async getNameInput() {
async getNameInput(): Promise<WebElementWrapper> {
return await testSubjects.findDescendant('nameInput', await this.getFlyout());
},
async getLogIndicesInput() {
async getLogIndicesInput(): Promise<WebElementWrapper> {
return await testSubjects.findDescendant('logIndicesInput', await this.getFlyout());
},
async getMetricIndicesInput() {
async getMetricIndicesInput(): Promise<WebElementWrapper> {
return await testSubjects.findDescendant('metricIndicesInput', await this.getFlyout());
},
@ -85,7 +85,7 @@ export function InfraSourceConfigurationFlyoutProvider({
/**
* Form and flyout
*/
async getFlyout() {
async getFlyout(): Promise<WebElementWrapper> {
return await testSubjects.find('sourceConfigurationFlyout');
},
async saveConfiguration() {