[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:
parent
926f9a912d
commit
066f64d07c
|
@ -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;
|
||||
`;
|
|
@ -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};
|
||||
`;
|
|
@ -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';
|
||||
};
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
|
@ -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`,
|
||||
},
|
||||
];
|
|
@ -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)};
|
||||
`;
|
|
@ -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;
|
||||
`;
|
|
@ -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;
|
||||
`;
|
|
@ -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';
|
||||
};
|
|
@ -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)}
|
||||
`;
|
|
@ -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;
|
||||
`;
|
|
@ -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 : '')};
|
||||
`;
|
|
@ -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;
|
||||
|
|
|
@ -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)}
|
||||
`;
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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 ? (
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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');
|
||||
},
|
||||
|
|
|
@ -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() {
|
||||
|
|
Loading…
Reference in a new issue