diff --git a/x-pack/legacy/plugins/infra/public/components/formatted_time.tsx b/x-pack/legacy/plugins/infra/public/components/formatted_time.tsx
index f6a6545920fc5..78255c55df124 100644
--- a/x-pack/legacy/plugins/infra/public/components/formatted_time.tsx
+++ b/x-pack/legacy/plugins/infra/public/components/formatted_time.tsx
@@ -17,8 +17,25 @@ const getFormattedTime = (
return userFormat ? moment(time).format(userFormat) : moment(time).format(fallbackFormat);
};
-export const useFormattedTime = (time: number, fallbackFormat?: string) => {
- const [dateFormat] = useKibanaUiSetting('dateFormat');
+interface UseFormattedTimeOptions {
+ format?: 'dateTime' | 'time';
+ fallbackFormat?: string;
+}
+
+export const useFormattedTime = (
+ time: number,
+ { format = 'dateTime', fallbackFormat }: UseFormattedTimeOptions = {}
+) => {
+ // `dateFormat:scaled` is an array of `[key, format]` tuples.
+ // The hook might return `undefined`, so use a sane default for the `find` later.
+ const scaledTuples = useKibanaUiSetting('dateFormat:scaled')[0] || [['', undefined]];
+
+ const formatMap = {
+ dateTime: useKibanaUiSetting('dateFormat')[0],
+ time: scaledTuples.find(([key]: [string, string]) => key === '')[1],
+ };
+
+ const dateFormat = formatMap[format];
const formattedTime = useMemo(() => getFormattedTime(time, dateFormat, fallbackFormat), [
getFormattedTime,
time,
diff --git a/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/column_headers.tsx b/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/column_headers.tsx
index 3d78c8d728fc6..56a84d258c907 100644
--- a/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/column_headers.tsx
+++ b/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/column_headers.tsx
@@ -5,6 +5,7 @@
*/
import React from 'react';
+import { transparentize } from 'polished';
import euiStyled from '../../../../../../common/eui_styled_components';
import {
@@ -20,6 +21,8 @@ import {
LogEntryColumnWidths,
} from './log_entry_column';
import { ASSUMED_SCROLLBAR_WIDTH } from './vertical_scroll_panel';
+import { WithLogPosition } from '../../../containers/logs/with_log_position';
+import { localizedDate } from '../../../utils/formatters/datetime';
export const LogColumnHeaders: React.FunctionComponent<{
columnConfigurations: LogColumnConfiguration[];
@@ -30,13 +33,16 @@ export const LogColumnHeaders: React.FunctionComponent<{
{columnConfigurations.map(columnConfiguration => {
if (isTimestampLogColumnConfiguration(columnConfiguration)) {
return (
-
- Timestamp
-
+
+ {({ firstVisiblePosition }) => (
+
+ {firstVisiblePosition ? localizedDate(firstVisiblePosition.time) : 'Timestamp'}
+
+ )}
+
);
} else if (isMessageLogColumnConfiguration(columnConfiguration)) {
return (
@@ -83,13 +89,16 @@ const LogColumnHeadersWrapper = euiStyled.div.attrs({
justify-content: flex-start;
overflow: hidden;
padding-right: ${ASSUMED_SCROLLBAR_WIDTH}px;
+ border-bottom: ${props => props.theme.eui.euiBorderThin};
+ box-shadow: 0 2px 2px -1px ${props => transparentize(0.3, props.theme.eui.euiColorLightShade)};
+ position: relative;
+ z-index: 1;
`;
const LogColumnHeaderWrapper = LogEntryColumn.extend.attrs({
role: 'columnheader',
})`
align-items: center;
- border-bottom: ${props => props.theme.eui.euiBorderThick};
display: flex;
flex-direction: row;
height: 32px;
diff --git a/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/log_date_row.tsx b/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/log_date_row.tsx
new file mode 100644
index 0000000000000..fbc450950b828
--- /dev/null
+++ b/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/log_date_row.tsx
@@ -0,0 +1,32 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import React from 'react';
+import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule, EuiTitle } from '@elastic/eui';
+import { localizedDate } from '../../../utils/formatters/datetime';
+
+interface LogDateRowProps {
+ timestamp: number;
+}
+
+/**
+ * Show a row with the date in the log stream
+ */
+export const LogDateRow: React.FC = ({ timestamp }) => {
+ const formattedDate = localizedDate(timestamp);
+
+ return (
+
+
+
+ {formattedDate}
+
+
+
+
+
+
+ );
+};
diff --git a/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/log_entry_timestamp_column.tsx b/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/log_entry_timestamp_column.tsx
index c996342d0c060..884e5ff0a5bde 100644
--- a/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/log_entry_timestamp_column.tsx
+++ b/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/log_entry_timestamp_column.tsx
@@ -19,7 +19,7 @@ interface LogEntryTimestampColumnProps {
export const LogEntryTimestampColumn = memo(
({ isHighlighted, isHovered, time }) => {
- const formattedTime = useFormattedTime(time);
+ const formattedTime = useFormattedTime(time, { format: 'time' });
return (
@@ -45,11 +45,8 @@ 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;
diff --git a/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/scrollable_log_text_stream_view.tsx b/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/scrollable_log_text_stream_view.tsx
index 3c1fb2ceec7f6..d439308194d18 100644
--- a/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/scrollable_log_text_stream_view.tsx
+++ b/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/scrollable_log_text_stream_view.tsx
@@ -6,7 +6,8 @@
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
-import React, { useMemo } from 'react';
+import React, { Fragment, useMemo } from 'react';
+import moment from 'moment';
import euiStyled from '../../../../../../common/eui_styled_components';
import { TextScale } from '../../../../common/log_text_scale';
@@ -26,6 +27,7 @@ import { MeasurableItemView } from './measurable_item_view';
import { VerticalScrollPanel } from './vertical_scroll_panel';
import { getColumnWidths, LogEntryColumnWidths } from './log_entry_column';
import { useMeasuredCharacterDimensions } from './text_styles';
+import { LogDateRow } from './log_date_row';
interface ScrollableLogTextStreamViewProps {
columnConfigurations: LogColumnConfiguration[];
@@ -188,35 +190,47 @@ export class ScrollableLogTextStreamView extends React.PureComponent<
isStreaming={false}
lastStreamingUpdate={null}
/>
- {items.map(item => (
-
- {itemMeasureRef => (
-
- )}
-
- ))}
+ {items.map((item, idx) => {
+ const currentTimestamp = item.logEntry.key.time;
+ let showDate = false;
+
+ if (idx > 0) {
+ const prevTimestamp = items[idx - 1].logEntry.key.time;
+ showDate = !moment(currentTimestamp).isSame(prevTimestamp, 'day');
+ }
+
+ return (
+
+ {showDate && }
+
+ {itemMeasureRef => (
+
+ )}
+
+
+ );
+ })}
= ({ children, columnConfigurations, scale }) => {
const { CharacterDimensionsProbe, dimensions } = useMeasuredCharacterDimensions(scale);
const referenceTime = useMemo(() => Date.now(), []);
- const formattedCurrentDate = useFormattedTime(referenceTime);
+ const formattedCurrentDate = useFormattedTime(referenceTime, { format: 'time' });
const columnWidths = useMemo(
() => getColumnWidths(columnConfigurations, dimensions.width, formattedCurrentDate.length),
[columnConfigurations, dimensions.width, formattedCurrentDate]
diff --git a/x-pack/legacy/plugins/infra/public/utils/formatters/datetime.ts b/x-pack/legacy/plugins/infra/public/utils/formatters/datetime.ts
new file mode 100644
index 0000000000000..732bc7fe74bf9
--- /dev/null
+++ b/x-pack/legacy/plugins/infra/public/utils/formatters/datetime.ts
@@ -0,0 +1,17 @@
+/*
+ * 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 { i18n } from '@kbn/i18n';
+
+export function localizedDate(dateTime: number | Date, locale: string = i18n.getLocale()) {
+ const formatter = new Intl.DateTimeFormat(locale, {
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric',
+ });
+
+ return formatter.format(dateTime);
+}
diff --git a/x-pack/test/functional/apps/infra/logs_source_configuration.ts b/x-pack/test/functional/apps/infra/logs_source_configuration.ts
index 3447e03a680e1..183acf3a980ee 100644
--- a/x-pack/test/functional/apps/infra/logs_source_configuration.ts
+++ b/x-pack/test/functional/apps/infra/logs_source_configuration.ts
@@ -66,7 +66,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
await pageObjects.common.navigateToActualUrl('infraLogs', 'logs/stream');
const columnHeaderLabels = await infraLogStream.getColumnHeaderLabels();
- expect(columnHeaderLabels).to.eql(['Timestamp', 'event.dataset', 'Message']);
+ expect(columnHeaderLabels).to.eql(['Oct 17, 2018', 'event.dataset', 'Message']);
const logStreamEntries = await infraLogStream.getStreamEntries();
expect(logStreamEntries.length).to.be.greaterThan(0);
@@ -98,7 +98,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
// TODO: make test more robust
// expect(columnHeaderLabels).to.eql(['host.name', 'Timestamp']);
- expect(columnHeaderLabels).to.eql(['Timestamp', 'host.name']);
+ expect(columnHeaderLabels).to.eql(['Oct 17, 2018', 'host.name']);
const logStreamEntries = await infraLogStream.getStreamEntries();