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',
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<void> => {
dispatch({
type: ActionTypes.SetTimeRange,
payload: {
timeRange,
},
})
dispatch(setTimeRange(timeRange))
dispatch(setHistogramQueryConfigAsync())
dispatch(setTableQueryConfigAsync())
}

View File

@ -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<Props, 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 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<Props, State> {
scrollTop,
scrollLeft,
currentRow: -1,
currentMessageWidth: getMessageWidth(
props.data,
props.tableColumns,
props.severityFormat
),
currentMessageWidth,
isMessageVisible,
visibleColumnsCount,
}
@ -204,21 +225,13 @@ class LogsTable extends Component<Props, State> {
autoHide={false}
>
<Grid
height={height}
rowHeight={this.calculateRowHeight}
rowCount={getValuesFromData(this.props.data).length}
width={width}
scrollLeft={this.state.scrollLeft}
scrollTop={this.state.scrollTop}
cellRenderer={this.cellRenderer}
onSectionRendered={this.handleRowRender(onRowsRendered)}
onScroll={this.handleGridScroll}
columnCount={columnCount}
columnWidth={this.getColumnWidth}
ref={(ref: Grid) => {
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<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}) => {
this.handleScroll({scrollLeft})
}
@ -346,8 +393,7 @@ class LogsTable extends Component<Props, State> {
}
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<Props, State> {
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)

View File

@ -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<Props, State> {
}
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<Props, State> {
{isCustomTimeRangeOpen ? (
<ClickOutside onClickOutside={this.handleCloseCustomTimeRange}>
<div className="custom-time--overlay">
<CustomTimeRange
onApplyTimeRange={this.handleApplyCustomTimeRange}
timeRange={customTimeRange}
<CustomSingularTime
time={customTimeRange.lower}
onSelected={this.handleApplyCustomTimeRange}
onClose={this.handleCloseCustomTimeRange}
isVisible={isCustomTimeRangeOpen}
timeInterval={60}
page={page}
timeInterval={300}
/>
</div>
</ClickOutside>

View File

@ -80,6 +80,7 @@ interface State {
liveUpdating: boolean
isOverlayVisible: boolean
histogramColors: HistogramColor[]
hasScrolled: boolean
}
class LogsPage extends PureComponent<Props, State> {
@ -106,6 +107,7 @@ class LogsPage extends PureComponent<Props, State> {
liveUpdating: false,
isOverlayVisible: false,
histogramColors: [],
hasScrolled: false,
}
}
@ -163,6 +165,7 @@ class LogsPage extends PureComponent<Props, State> {
tableColumns={this.tableColumns}
severityFormat={this.severityFormat}
severityLevelColors={this.severityLevelColors}
hasScrolled={this.state.hasScrolled}
/>
</div>
</div>
@ -183,7 +186,10 @@ class LogsPage extends PureComponent<Props, State> {
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<Props, State> {
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<Props, State> {
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<Props, State> {
}
private fetchNewDataset() {
this.props.executeQueriesAsync()
this.setState({liveUpdating: true})
this.props.executeQueriesAsync()
}
private handleToggleOverlay = (): void => {

View File

@ -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 {

View File

@ -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[],

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 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 = () => {

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