From 0e0a26399388775c6bc9a19d48d64b8fc096ccfb Mon Sep 17 00:00:00 2001 From: Brandon Farmer Date: Fri, 6 Jul 2018 10:12:24 -0700 Subject: [PATCH] Allow selection of single time from log viewer --- ui/src/logs/actions/index.ts | 15 ++- ui/src/logs/components/LogsTable.tsx | 120 +++++++++++------- ui/src/logs/components/TimeRangeDropdown.tsx | 14 +- ui/src/logs/containers/LogsPage.tsx | 14 +- ui/src/logs/utils/index.ts | 2 + ui/src/logs/utils/table.ts | 22 ++++ .../shared/components/CustomSingularTime.tsx | 98 ++++++++++++++ ui/src/shared/components/CustomTimeRange.js | 17 +-- ui/src/shared/utils/time.ts | 22 ++++ 9 files changed, 247 insertions(+), 77 deletions(-) create mode 100644 ui/src/shared/components/CustomSingularTime.tsx create mode 100644 ui/src/shared/utils/time.ts diff --git a/ui/src/logs/actions/index.ts b/ui/src/logs/actions/index.ts index 3ef168c56d..60bffbf38e 100644 --- a/ui/src/logs/actions/index.ts +++ b/ui/src/logs/actions/index.ts @@ -62,7 +62,6 @@ export enum ActionTypes { ConcatMoreLogs = 'LOGS_CONCAT_MORE_LOGS', SetConfig = 'SET_CONFIG', } - export interface ConcatMoreLogsAction { type: ActionTypes.ConcatMoreLogs payload: { @@ -445,15 +444,17 @@ export const setNamespaces = ( }, }) +export const setTimeRange = timeRange => ({ + type: ActionTypes.SetTimeRange, + payload: { + timeRange, + }, +}) + export const setTimeRangeAsync = (timeRange: TimeRange) => async ( dispatch ): Promise => { - dispatch({ - type: ActionTypes.SetTimeRange, - payload: { - timeRange, - }, - }) + dispatch(setTimeRange(timeRange)) dispatch(setHistogramQueryConfigAsync()) dispatch(setTableQueryConfigAsync()) } diff --git a/ui/src/logs/components/LogsTable.tsx b/ui/src/logs/components/LogsTable.tsx index 6dd37f4a2a..db878a03f8 100644 --- a/ui/src/logs/components/LogsTable.tsx +++ b/ui/src/logs/components/LogsTable.tsx @@ -10,6 +10,9 @@ import {getDeep} from 'src/utils/wrappers' import {colorForSeverity} from 'src/logs/utils/colors' import { + ROW_HEIGHT, + calculateRowCharWidth, + calculateMessageHeight, getColumnFromData, getValueFromData, getValuesFromData, @@ -36,8 +39,6 @@ import { SeverityLevelColor, } from 'src/types/logs' -const ROW_HEIGHT = 26 -const CHAR_WIDTH = 9 interface Props { data: TableData isScrolledToTop: boolean @@ -50,6 +51,8 @@ interface Props { tableColumns: LogsTableColumn[] severityFormat: SeverityFormat severityLevelColors: SeverityLevelColor[] + scrollToRow?: number + hasScrolled: boolean } interface State { @@ -64,13 +67,35 @@ interface State { class LogsTable extends Component { public static getDerivedStateFromProps(props, state): State { - const {isScrolledToTop} = props + const { + isScrolledToTop, + scrollToRow, + data, + tableColumns, + severityFormat, + hasScrolled, + } = props + const currentMessageWidth = getMessageWidth( + data, + tableColumns, + severityFormat + ) let lastQueryTime = _.get(state, 'lastQueryTime', null) let scrollTop = _.get(state, 'scrollTop', 0) if (isScrolledToTop) { lastQueryTime = null scrollTop = 0 + } else if (scrollToRow && !hasScrolled) { + const rowCharLimit = calculateRowCharWidth(currentMessageWidth) + + scrollTop = _.reduce( + _.range(0, scrollToRow), + (acc, index) => { + return acc + calculateMessageHeight(index, data, rowCharLimit) + }, + 0 + ) } const scrollLeft = _.get(state, 'scrollLeft', 0) @@ -90,11 +115,7 @@ class LogsTable extends Component { scrollTop, scrollLeft, currentRow: -1, - currentMessageWidth: getMessageWidth( - props.data, - props.tableColumns, - props.severityFormat - ), + currentMessageWidth, isMessageVisible, visibleColumnsCount, } @@ -204,21 +225,13 @@ class LogsTable extends Component { autoHide={false} > { - registerChild(ref) - this.grid = ref - }} + {...this.gridProperties( + width, + height, + onRowsRendered, + columnCount, + registerChild + )} style={{ height: this.calculateTotalHeight(), overflowY: 'hidden', @@ -233,6 +246,40 @@ class LogsTable extends Component { ) } + private gridProperties = ( + width: number, + height: number, + onRowsRendered: (params: {startIndex: number; stopIndex: number}) => void, + columnCount: number, + registerChild: (g: Grid) => void + ) => { + const {hasScrolled, scrollToRow} = this.props + const {scrollLeft, scrollTop} = this.state + const result: {scrollToRow?: number} & any = { + width, + height, + rowHeight: this.calculateRowHeight, + rowCount: getValuesFromData(this.props.data).length, + scrollLeft, + scrollTop, + cellRenderer: this.cellRenderer, + onSectionRendered: this.handleRowRender(onRowsRendered), + onScroll: this.handleGridScroll, + columnCount, + columnWidth: this.getColumnWidth, + ref: (ref: Grid) => { + registerChild(ref) + this.grid = ref + }, + } + + if (!hasScrolled && scrollToRow) { + result.scrollToRow = scrollToRow + } + + return result + } + private handleGridScroll = ({scrollLeft}) => { this.handleScroll({scrollLeft}) } @@ -346,8 +393,7 @@ class LogsTable extends Component { } private get rowCharLimit(): number { - const {currentMessageWidth} = this.state - return Math.floor(currentMessageWidth / CHAR_WIDTH) + return calculateRowCharWidth(this.state.currentMessageWidth) } private calculateTotalHeight = (): number => { @@ -356,29 +402,17 @@ class LogsTable extends Component { return _.reduce( data, (acc, __, index) => { - return acc + this.calculateMessageHeight(index) + return ( + acc + + calculateMessageHeight(index, this.props.data, this.rowCharLimit) + ) }, 0 ) } - private calculateMessageHeight = (index: number): number => { - const columns = getColumnsFromData(this.props.data) - const columnIndex = columns.indexOf('message') - const value = getValueFromData(this.props.data, index, columnIndex) - - if (_.isEmpty(value)) { - return ROW_HEIGHT - } - - const lines = Math.ceil(value.length / (this.rowCharLimit * 0.95)) - - return Math.max(lines, 1) * ROW_HEIGHT + 4 - } - - private calculateRowHeight = ({index}: {index: number}): number => { - return this.calculateMessageHeight(index) - } + private calculateRowHeight = ({index}: {index: number}): number => + calculateMessageHeight(index, this.props.data, this.rowCharLimit) private headerRenderer = ({key, style, columnIndex}) => { const column = getColumnFromData(this.props.data, columnIndex) diff --git a/ui/src/logs/components/TimeRangeDropdown.tsx b/ui/src/logs/components/TimeRangeDropdown.tsx index ef48dd5cc8..d34ae2a3df 100644 --- a/ui/src/logs/components/TimeRangeDropdown.tsx +++ b/ui/src/logs/components/TimeRangeDropdown.tsx @@ -8,7 +8,7 @@ import timeRanges from 'src/logs/data/timeRanges' import {DROPDOWN_MENU_MAX_HEIGHT} from 'src/shared/constants/index' import {ErrorHandling} from 'src/shared/decorators/errors' import {ClickOutside} from 'src/shared/components/ClickOutside' -import CustomTimeRange from 'src/shared/components/CustomTimeRange' +import CustomSingularTime from 'src/shared/components/CustomSingularTime' import {TimeRange} from 'src/types' @@ -56,7 +56,7 @@ class TimeRangeDropdown extends Component { } public render() { - const {selected, preventCustomTimeRange, page} = this.props + const {selected, preventCustomTimeRange} = this.props const {customTimeRange, isCustomTimeRangeOpen} = this.state return ( @@ -113,13 +113,11 @@ class TimeRangeDropdown extends Component { {isCustomTimeRangeOpen ? (
-
diff --git a/ui/src/logs/containers/LogsPage.tsx b/ui/src/logs/containers/LogsPage.tsx index 9a89bb8282..26c2127f90 100644 --- a/ui/src/logs/containers/LogsPage.tsx +++ b/ui/src/logs/containers/LogsPage.tsx @@ -80,6 +80,7 @@ interface State { liveUpdating: boolean isOverlayVisible: boolean histogramColors: HistogramColor[] + hasScrolled: boolean } class LogsPage extends PureComponent { @@ -106,6 +107,7 @@ class LogsPage extends PureComponent { liveUpdating: false, isOverlayVisible: false, histogramColors: [], + hasScrolled: false, } } @@ -163,6 +165,7 @@ class LogsPage extends PureComponent { tableColumns={this.tableColumns} severityFormat={this.severityFormat} severityLevelColors={this.severityLevelColors} + hasScrolled={this.state.hasScrolled} /> @@ -183,7 +186,10 @@ class LogsPage extends PureComponent { const updatedColumns: string[] = _.get(orderedData, '0', []) const updatedValues = _.slice(orderedData, 1) - return {columns: updatedColumns, values: updatedValues} + return { + columns: updatedColumns, + values: updatedValues, + } } private get logConfigLink(): string { @@ -219,8 +225,8 @@ class LogsPage extends PureComponent { private handleVerticalScroll = () => { if (this.state.liveUpdating) { clearInterval(this.interval) - this.setState({liveUpdating: false}) } + this.setState({liveUpdating: false, hasScrolled: true}) } private handleTagSelection = (selection: {tag: string; key: string}) => { @@ -299,8 +305,8 @@ class LogsPage extends PureComponent { const {liveUpdating} = this.state if (liveUpdating) { - clearInterval(this.interval) this.setState({liveUpdating: false}) + clearInterval(this.interval) } else { this.startUpdating() } @@ -351,8 +357,8 @@ class LogsPage extends PureComponent { } private fetchNewDataset() { - this.props.executeQueriesAsync() this.setState({liveUpdating: true}) + this.props.executeQueriesAsync() } private handleToggleOverlay = (): void => { diff --git a/ui/src/logs/utils/index.ts b/ui/src/logs/utils/index.ts index e115469526..989f089c3c 100644 --- a/ui/src/logs/utils/index.ts +++ b/ui/src/logs/utils/index.ts @@ -144,6 +144,8 @@ const computeSeconds = (range: TimeRange) => { if (seconds) { return seconds + } else if (upper && upper.match(/now/) && lower) { + return moment().unix() - moment(lower).unix() } else if (upper && lower) { return moment(upper).unix() - moment(lower).unix() } else { diff --git a/ui/src/logs/utils/table.ts b/ui/src/logs/utils/table.ts index fe846140e5..786d4a9d68 100644 --- a/ui/src/logs/utils/table.ts +++ b/ui/src/logs/utils/table.ts @@ -4,6 +4,7 @@ import {getDeep} from 'src/utils/wrappers' import {TableData, LogsTableColumn, SeverityFormat} from 'src/types/logs' import {SeverityFormatOptions} from 'src/logs/constants' +export const ROW_HEIGHT = 26 const CHAR_WIDTH = 9 export const getValuesFromData = (data: TableData): string[][] => @@ -69,6 +70,27 @@ export const getColumnWidth = (column: string): number => { ) } +export const calculateRowCharWidth = (currentMessageWidth: number): number => + Math.floor(currentMessageWidth / CHAR_WIDTH) + +export const calculateMessageHeight = ( + index: number, + data: TableData, + rowCharLimit: number +): number => { + const columns = getColumnsFromData(data) + const columnIndex = columns.indexOf('message') + const value = getValueFromData(data, index, columnIndex) + + if (_.isEmpty(value)) { + return ROW_HEIGHT + } + + const lines = Math.ceil(value.length / (rowCharLimit * 0.95)) + + return Math.max(lines, 1) * ROW_HEIGHT + 4 +} + export const getMessageWidth = ( data: TableData, tableColumns: LogsTableColumn[], diff --git a/ui/src/shared/components/CustomSingularTime.tsx b/ui/src/shared/components/CustomSingularTime.tsx new file mode 100644 index 0000000000..c6757216a9 --- /dev/null +++ b/ui/src/shared/components/CustomSingularTime.tsx @@ -0,0 +1,98 @@ +import React, {Component} from 'react' +import rome from 'rome' +import {ErrorHandling} from 'src/shared/decorators/errors' +import {formatTimeRange} from 'src/shared/utils/time' + +import {TimeRange} from 'src/types' + +interface Props { + onSelected: (timeRange: TimeRange) => void + time: string + timeInterval?: number + onClose?: () => void +} + +interface State { + time: string +} + +@ErrorHandling +class CustomSingularTime extends Component { + private calendar?: any + private containerRef: React.RefObject = React.createRef< + HTMLDivElement + >() + private inputRef: React.RefObject = React.createRef< + HTMLInputElement + >() + + constructor(props: Props) { + super(props) + + this.state = { + time: props.time, + } + } + + public componentDidMount() { + const {time, timeInterval} = this.props + + this.calendar = rome(this.inputRef.current, { + appendTo: this.containerRef.current, + initialValue: formatTimeRange(time), + autoClose: false, + autoHideOnBlur: false, + autoHideOnClick: false, + timeInterval, + }) + + this.calendar.show() + } + + public render() { + return ( +
+
+
+
+ +
+
+
+ Apply +
+
+
+ ) + } + + private handleRefreshCalendar = () => { + if (this.calendar) { + this.calendar.refresh() + } + } + + private handleClick = () => { + const date = this.calendar.getDate() + if (date) { + const lower = date.toISOString() + this.props.onSelected({lower, upper: 'now()'}) + } + + if (this.props.onClose) { + this.props.onClose() + } + } +} + +export default CustomSingularTime diff --git a/ui/src/shared/components/CustomTimeRange.js b/ui/src/shared/components/CustomTimeRange.js index 056605c0c4..a3976c0101 100644 --- a/ui/src/shared/components/CustomTimeRange.js +++ b/ui/src/shared/components/CustomTimeRange.js @@ -3,6 +3,7 @@ import PropTypes from 'prop-types' import rome from 'rome' import moment from 'moment' +import {formatTimeRange} from 'shared/utils/time' import shortcuts from 'shared/data/timeRangeShortcuts' import {ErrorHandling} from 'src/shared/decorators/errors' const dateFormat = 'YYYY-MM-DD HH:mm' @@ -90,21 +91,7 @@ class CustomTimeRange extends Component { * before passing the string to be parsed. */ _formatTimeRange = timeRange => { - if (!timeRange) { - return '' - } - - if (timeRange === 'now()') { - return moment(new Date()).format(dateFormat) - } - - // If the given time range is relative, create a fixed timestamp based on its value - if (timeRange.match(/^now/)) { - const [, duration, unitOfTime] = timeRange.match(/(\d+)(\w+)/) - moment().subtract(duration, unitOfTime) - } - - return moment(timeRange.replace(/\'/g, '')).format(dateFormat) + return formatTimeRange(timeRange) } handleClick = () => { diff --git a/ui/src/shared/utils/time.ts b/ui/src/shared/utils/time.ts new file mode 100644 index 0000000000..3cdde0cfcf --- /dev/null +++ b/ui/src/shared/utils/time.ts @@ -0,0 +1,22 @@ +import moment from 'moment' + +const dateFormat = 'YYYY-MM-DD HH:mm' + +export const formatTimeRange = (timeRange: string | null): string => { + if (!timeRange) { + return '' + } + + if (timeRange === 'now()') { + return moment(new Date()).format(dateFormat) + } + + if (timeRange.match(/^now/)) { + const [, duration, unitOfTime] = timeRange.match(/(\d+)(\w+)/) + const d = duration as moment.unitOfTime.DurationConstructor + + moment().subtract(d, unitOfTime) + } + + return moment(timeRange.replace(/\'/g, '')).format(dateFormat) +}