Allow selection of single time from log viewer

pull/10616/head
Brandon Farmer 2018-07-06 10:12:24 -07:00
parent b5a07a5854
commit 0e0a263993
9 changed files with 247 additions and 77 deletions

View File

@ -62,7 +62,6 @@ export enum ActionTypes {
ConcatMoreLogs = 'LOGS_CONCAT_MORE_LOGS', ConcatMoreLogs = 'LOGS_CONCAT_MORE_LOGS',
SetConfig = 'SET_CONFIG', SetConfig = 'SET_CONFIG',
} }
export interface ConcatMoreLogsAction { export interface ConcatMoreLogsAction {
type: ActionTypes.ConcatMoreLogs type: ActionTypes.ConcatMoreLogs
payload: { payload: {
@ -445,15 +444,17 @@ export const setNamespaces = (
}, },
}) })
export const setTimeRange = timeRange => ({
type: ActionTypes.SetTimeRange,
payload: {
timeRange,
},
})
export const setTimeRangeAsync = (timeRange: TimeRange) => async ( export const setTimeRangeAsync = (timeRange: TimeRange) => async (
dispatch dispatch
): Promise<void> => { ): Promise<void> => {
dispatch({ dispatch(setTimeRange(timeRange))
type: ActionTypes.SetTimeRange,
payload: {
timeRange,
},
})
dispatch(setHistogramQueryConfigAsync()) dispatch(setHistogramQueryConfigAsync())
dispatch(setTableQueryConfigAsync()) dispatch(setTableQueryConfigAsync())
} }

View File

@ -10,6 +10,9 @@ import {getDeep} from 'src/utils/wrappers'
import {colorForSeverity} from 'src/logs/utils/colors' import {colorForSeverity} from 'src/logs/utils/colors'
import { import {
ROW_HEIGHT,
calculateRowCharWidth,
calculateMessageHeight,
getColumnFromData, getColumnFromData,
getValueFromData, getValueFromData,
getValuesFromData, getValuesFromData,
@ -36,8 +39,6 @@ import {
SeverityLevelColor, SeverityLevelColor,
} from 'src/types/logs' } from 'src/types/logs'
const ROW_HEIGHT = 26
const CHAR_WIDTH = 9
interface Props { interface Props {
data: TableData data: TableData
isScrolledToTop: boolean isScrolledToTop: boolean
@ -50,6 +51,8 @@ interface Props {
tableColumns: LogsTableColumn[] tableColumns: LogsTableColumn[]
severityFormat: SeverityFormat severityFormat: SeverityFormat
severityLevelColors: SeverityLevelColor[] severityLevelColors: SeverityLevelColor[]
scrollToRow?: number
hasScrolled: boolean
} }
interface State { interface State {
@ -64,13 +67,35 @@ interface State {
class LogsTable extends Component<Props, State> { class LogsTable extends Component<Props, State> {
public static getDerivedStateFromProps(props, state): State { 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 lastQueryTime = _.get(state, 'lastQueryTime', null)
let scrollTop = _.get(state, 'scrollTop', 0) let scrollTop = _.get(state, 'scrollTop', 0)
if (isScrolledToTop) { if (isScrolledToTop) {
lastQueryTime = null lastQueryTime = null
scrollTop = 0 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) const scrollLeft = _.get(state, 'scrollLeft', 0)
@ -90,11 +115,7 @@ class LogsTable extends Component<Props, State> {
scrollTop, scrollTop,
scrollLeft, scrollLeft,
currentRow: -1, currentRow: -1,
currentMessageWidth: getMessageWidth( currentMessageWidth,
props.data,
props.tableColumns,
props.severityFormat
),
isMessageVisible, isMessageVisible,
visibleColumnsCount, visibleColumnsCount,
} }
@ -204,21 +225,13 @@ class LogsTable extends Component<Props, State> {
autoHide={false} autoHide={false}
> >
<Grid <Grid
height={height} {...this.gridProperties(
rowHeight={this.calculateRowHeight} width,
rowCount={getValuesFromData(this.props.data).length} height,
width={width} onRowsRendered,
scrollLeft={this.state.scrollLeft} columnCount,
scrollTop={this.state.scrollTop} registerChild
cellRenderer={this.cellRenderer} )}
onSectionRendered={this.handleRowRender(onRowsRendered)}
onScroll={this.handleGridScroll}
columnCount={columnCount}
columnWidth={this.getColumnWidth}
ref={(ref: Grid) => {
registerChild(ref)
this.grid = ref
}}
style={{ style={{
height: this.calculateTotalHeight(), height: this.calculateTotalHeight(),
overflowY: 'hidden', overflowY: 'hidden',
@ -233,6 +246,40 @@ class LogsTable extends Component<Props, State> {
) )
} }
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}) => { private handleGridScroll = ({scrollLeft}) => {
this.handleScroll({scrollLeft}) this.handleScroll({scrollLeft})
} }
@ -346,8 +393,7 @@ class LogsTable extends Component<Props, State> {
} }
private get rowCharLimit(): number { private get rowCharLimit(): number {
const {currentMessageWidth} = this.state return calculateRowCharWidth(this.state.currentMessageWidth)
return Math.floor(currentMessageWidth / CHAR_WIDTH)
} }
private calculateTotalHeight = (): number => { private calculateTotalHeight = (): number => {
@ -356,29 +402,17 @@ class LogsTable extends Component<Props, State> {
return _.reduce( return _.reduce(
data, data,
(acc, __, index) => { (acc, __, index) => {
return acc + this.calculateMessageHeight(index) return (
acc +
calculateMessageHeight(index, this.props.data, this.rowCharLimit)
)
}, },
0 0
) )
} }
private calculateMessageHeight = (index: number): number => { private calculateRowHeight = ({index}: {index: number}): number =>
const columns = getColumnsFromData(this.props.data) calculateMessageHeight(index, this.props.data, this.rowCharLimit)
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 headerRenderer = ({key, style, columnIndex}) => { private headerRenderer = ({key, style, columnIndex}) => {
const column = getColumnFromData(this.props.data, columnIndex) const column = getColumnFromData(this.props.data, columnIndex)

View File

@ -8,7 +8,7 @@ import timeRanges from 'src/logs/data/timeRanges'
import {DROPDOWN_MENU_MAX_HEIGHT} from 'src/shared/constants/index' import {DROPDOWN_MENU_MAX_HEIGHT} from 'src/shared/constants/index'
import {ErrorHandling} from 'src/shared/decorators/errors' import {ErrorHandling} from 'src/shared/decorators/errors'
import {ClickOutside} from 'src/shared/components/ClickOutside' 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' import {TimeRange} from 'src/types'
@ -56,7 +56,7 @@ class TimeRangeDropdown extends Component<Props, State> {
} }
public render() { public render() {
const {selected, preventCustomTimeRange, page} = this.props const {selected, preventCustomTimeRange} = this.props
const {customTimeRange, isCustomTimeRangeOpen} = this.state const {customTimeRange, isCustomTimeRangeOpen} = this.state
return ( return (
@ -113,13 +113,11 @@ class TimeRangeDropdown extends Component<Props, State> {
{isCustomTimeRangeOpen ? ( {isCustomTimeRangeOpen ? (
<ClickOutside onClickOutside={this.handleCloseCustomTimeRange}> <ClickOutside onClickOutside={this.handleCloseCustomTimeRange}>
<div className="custom-time--overlay"> <div className="custom-time--overlay">
<CustomTimeRange <CustomSingularTime
onApplyTimeRange={this.handleApplyCustomTimeRange} time={customTimeRange.lower}
timeRange={customTimeRange} onSelected={this.handleApplyCustomTimeRange}
onClose={this.handleCloseCustomTimeRange} onClose={this.handleCloseCustomTimeRange}
isVisible={isCustomTimeRangeOpen} timeInterval={300}
timeInterval={60}
page={page}
/> />
</div> </div>
</ClickOutside> </ClickOutside>

View File

@ -80,6 +80,7 @@ interface State {
liveUpdating: boolean liveUpdating: boolean
isOverlayVisible: boolean isOverlayVisible: boolean
histogramColors: HistogramColor[] histogramColors: HistogramColor[]
hasScrolled: boolean
} }
class LogsPage extends PureComponent<Props, State> { class LogsPage extends PureComponent<Props, State> {
@ -106,6 +107,7 @@ class LogsPage extends PureComponent<Props, State> {
liveUpdating: false, liveUpdating: false,
isOverlayVisible: false, isOverlayVisible: false,
histogramColors: [], histogramColors: [],
hasScrolled: false,
} }
} }
@ -163,6 +165,7 @@ class LogsPage extends PureComponent<Props, State> {
tableColumns={this.tableColumns} tableColumns={this.tableColumns}
severityFormat={this.severityFormat} severityFormat={this.severityFormat}
severityLevelColors={this.severityLevelColors} severityLevelColors={this.severityLevelColors}
hasScrolled={this.state.hasScrolled}
/> />
</div> </div>
</div> </div>
@ -183,7 +186,10 @@ class LogsPage extends PureComponent<Props, State> {
const updatedColumns: string[] = _.get(orderedData, '0', []) const updatedColumns: string[] = _.get(orderedData, '0', [])
const updatedValues = _.slice(orderedData, 1) const updatedValues = _.slice(orderedData, 1)
return {columns: updatedColumns, values: updatedValues} return {
columns: updatedColumns,
values: updatedValues,
}
} }
private get logConfigLink(): string { private get logConfigLink(): string {
@ -219,8 +225,8 @@ class LogsPage extends PureComponent<Props, State> {
private handleVerticalScroll = () => { private handleVerticalScroll = () => {
if (this.state.liveUpdating) { if (this.state.liveUpdating) {
clearInterval(this.interval) clearInterval(this.interval)
this.setState({liveUpdating: false})
} }
this.setState({liveUpdating: false, hasScrolled: true})
} }
private handleTagSelection = (selection: {tag: string; key: string}) => { private handleTagSelection = (selection: {tag: string; key: string}) => {
@ -299,8 +305,8 @@ class LogsPage extends PureComponent<Props, State> {
const {liveUpdating} = this.state const {liveUpdating} = this.state
if (liveUpdating) { if (liveUpdating) {
clearInterval(this.interval)
this.setState({liveUpdating: false}) this.setState({liveUpdating: false})
clearInterval(this.interval)
} else { } else {
this.startUpdating() this.startUpdating()
} }
@ -351,8 +357,8 @@ class LogsPage extends PureComponent<Props, State> {
} }
private fetchNewDataset() { private fetchNewDataset() {
this.props.executeQueriesAsync()
this.setState({liveUpdating: true}) this.setState({liveUpdating: true})
this.props.executeQueriesAsync()
} }
private handleToggleOverlay = (): void => { private handleToggleOverlay = (): void => {

View File

@ -144,6 +144,8 @@ const computeSeconds = (range: TimeRange) => {
if (seconds) { if (seconds) {
return seconds return seconds
} else if (upper && upper.match(/now/) && lower) {
return moment().unix() - moment(lower).unix()
} else if (upper && lower) { } else if (upper && lower) {
return moment(upper).unix() - moment(lower).unix() return moment(upper).unix() - moment(lower).unix()
} else { } else {

View File

@ -4,6 +4,7 @@ import {getDeep} from 'src/utils/wrappers'
import {TableData, LogsTableColumn, SeverityFormat} from 'src/types/logs' import {TableData, LogsTableColumn, SeverityFormat} from 'src/types/logs'
import {SeverityFormatOptions} from 'src/logs/constants' import {SeverityFormatOptions} from 'src/logs/constants'
export const ROW_HEIGHT = 26
const CHAR_WIDTH = 9 const CHAR_WIDTH = 9
export const getValuesFromData = (data: TableData): string[][] => 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 = ( export const getMessageWidth = (
data: TableData, data: TableData,
tableColumns: LogsTableColumn[], tableColumns: LogsTableColumn[],

View File

@ -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<Props, State> {
private calendar?: any
private containerRef: React.RefObject<HTMLDivElement> = React.createRef<
HTMLDivElement
>()
private inputRef: React.RefObject<HTMLInputElement> = 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 (
<div className="custom-time-container">
<div className="custom-time--wrap">
<div className="custom-time--dates">
<div
className="custom-time--lower-container"
ref={this.containerRef}
>
<input
ref={this.inputRef}
className="custom-time--lower form-control input-sm"
onKeyUp={this.handleRefreshCalendar}
/>
</div>
</div>
<div
className="custom-time--apply btn btn-sm btn-primary"
onClick={this.handleClick}
>
Apply
</div>
</div>
</div>
)
}
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

View File

@ -3,6 +3,7 @@ import PropTypes from 'prop-types'
import rome from 'rome' import rome from 'rome'
import moment from 'moment' import moment from 'moment'
import {formatTimeRange} from 'shared/utils/time'
import shortcuts from 'shared/data/timeRangeShortcuts' import shortcuts from 'shared/data/timeRangeShortcuts'
import {ErrorHandling} from 'src/shared/decorators/errors' import {ErrorHandling} from 'src/shared/decorators/errors'
const dateFormat = 'YYYY-MM-DD HH:mm' const dateFormat = 'YYYY-MM-DD HH:mm'
@ -90,21 +91,7 @@ class CustomTimeRange extends Component {
* before passing the string to be parsed. * before passing the string to be parsed.
*/ */
_formatTimeRange = timeRange => { _formatTimeRange = timeRange => {
if (!timeRange) { return formatTimeRange(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)
} }
handleClick = () => { handleClick = () => {

View File

@ -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)
}