Allow selection of single time from log viewer
parent
b5a07a5854
commit
0e0a263993
|
@ -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())
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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[],
|
||||
|
|
|
@ -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
|
|
@ -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 = () => {
|
||||
|
|
|
@ -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)
|
||||
}
|
Loading…
Reference in New Issue