Merge pull request #3885 from influxdata/features/infinite-scroll

Initial log viewer infinite scroll behaviour
pull/3893/head
Brandon Farmer 2018-07-11 22:06:09 -07:00 committed by GitHub
commit ee9d7ab1fb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 1191 additions and 331 deletions

View File

@ -9,6 +9,8 @@ import {
buildHistogramQueryConfig,
buildTableQueryConfig,
buildLogQuery,
buildForwardLogQuery,
buildBackwardLogQuery,
parseHistogramQueryResponse,
} from 'src/logs/utils'
import {
@ -21,8 +23,11 @@ import {
// getLogConfig as getLogConfigAJAX,
// updateLogConfig as updateLogConfigAJAX,
} from 'src/logs/api'
import {serverLogData} from 'src/logs/data/serverLogData'
import {LogsState, Filter, TableData, LogConfig} from 'src/types/logs'
export const INITIAL_LIMIT = 100
const defaultTableData: TableData = {
columns: [
'time',
@ -60,7 +65,45 @@ export enum ActionTypes {
IncrementQueryCount = 'LOGS_INCREMENT_QUERY_COUNT',
DecrementQueryCount = 'LOGS_DECREMENT_QUERY_COUNT',
ConcatMoreLogs = 'LOGS_CONCAT_MORE_LOGS',
PrependMoreLogs = 'LOGS_PREPEND_MORE_LOGS',
SetConfig = 'SET_CONFIG',
SetTableRelativeTime = 'SET_TABLE_RELATIVE_TIME',
SetTableCustomTime = 'SET_TABLE_CUSTOM_TIME',
SetTableForwardData = 'SET_TABLE_FORWARD_DATA',
SetTableBackwardData = 'SET_TABLE_BACKWARD_DATA',
ClearRowsAdded = 'CLEAR_ROWS_ADDED',
}
export interface ClearRowsAddedAction {
type: ActionTypes.ClearRowsAdded
}
export interface SetTableForwardDataAction {
type: ActionTypes.SetTableForwardData
payload: {
data: TableData
}
}
export interface SetTableBackwardDataAction {
type: ActionTypes.SetTableBackwardData
payload: {
data: TableData
}
}
export interface SetTableRelativeTimeAction {
type: ActionTypes.SetTableRelativeTime
payload: {
time: number
}
}
export interface SetTableCustomTimeAction {
type: ActionTypes.SetTableCustomTime
payload: {
time: string
}
}
export interface ConcatMoreLogsAction {
@ -70,6 +113,13 @@ export interface ConcatMoreLogsAction {
}
}
export interface PrependMoreLogsAction {
type: ActionTypes.PrependMoreLogs
payload: {
series: TableData
}
}
export interface IncrementQueryCountAction {
type: ActionTypes.IncrementQueryCount
}
@ -194,7 +244,13 @@ export type Action =
| DecrementQueryCountAction
| IncrementQueryCountAction
| ConcatMoreLogsAction
| PrependMoreLogsAction
| SetConfigsAction
| SetTableCustomTimeAction
| SetTableRelativeTimeAction
| SetTableForwardDataAction
| SetTableBackwardDataAction
| ClearRowsAddedAction
const getTimeRange = (state: State): TimeRange | null =>
getDeep<TimeRange | null>(state, 'logs.timeRange', null)
@ -217,6 +273,135 @@ const getSearchTerm = (state: State): string | null =>
const getFilters = (state: State): Filter[] =>
getDeep<Filter[]>(state, 'logs.filters', [])
const getTableSelectedTime = (state: State): string => {
const custom = getDeep<string>(state, 'logs.tableTime.custom', '')
if (!_.isEmpty(custom)) {
return custom
}
const relative = getDeep<number>(state, 'logs.tableTime.relative', 0)
return moment()
.subtract(relative, 'seconds')
.toISOString()
}
export const clearRowsAdded = () => ({
type: ActionTypes.ClearRowsAdded,
})
export const setTableCustomTime = (time: string): SetTableCustomTimeAction => ({
type: ActionTypes.SetTableCustomTime,
payload: {time},
})
export const setTableRelativeTime = (
time: number
): SetTableRelativeTimeAction => ({
type: ActionTypes.SetTableRelativeTime,
payload: {time},
})
export const setTableForwardData = (
data: TableData
): SetTableForwardDataAction => ({
type: ActionTypes.SetTableForwardData,
payload: {data},
})
export const setTableBackwardData = (
data: TableData
): SetTableBackwardDataAction => ({
type: ActionTypes.SetTableBackwardData,
payload: {data},
})
export const executeTableForwardQueryAsync = () => async (
dispatch,
getState: GetState
) => {
const state = getState()
const time = getTableSelectedTime(state)
const queryConfig = getTableQueryConfig(state)
const namespace = getNamespace(state)
const proxyLink = getProxyLink(state)
const searchTerm = getSearchTerm(state)
const filters = getFilters(state)
if (!_.every([queryConfig, time, namespace, proxyLink])) {
return
}
try {
dispatch(incrementQueryCount())
const query = buildForwardLogQuery(time, queryConfig, filters, searchTerm)
const response = await executeQueryAsync(
proxyLink,
namespace,
`${query} ORDER BY time ASC LIMIT ${INITIAL_LIMIT}`
)
const series = getDeep(response, 'results.0.series.0', defaultTableData)
const result = {
columns: series.columns,
values: _.reverse(series.values),
}
dispatch(setTableForwardData(result))
} finally {
dispatch(decrementQueryCount())
}
}
export const executeTableBackwardQueryAsync = () => async (
dispatch,
getState: GetState
) => {
const state = getState()
const time = getTableSelectedTime(state)
const queryConfig = getTableQueryConfig(state)
const namespace = getNamespace(state)
const proxyLink = getProxyLink(state)
const searchTerm = getSearchTerm(state)
const filters = getFilters(state)
if (!_.every([queryConfig, time, namespace, proxyLink])) {
return
}
try {
dispatch(incrementQueryCount())
const query = buildBackwardLogQuery(time, queryConfig, filters, searchTerm)
const response = await executeQueryAsync(
proxyLink,
namespace,
`${query} ORDER BY time DESC LIMIT ${INITIAL_LIMIT}`
)
const series = getDeep(response, 'results.0.series.0', defaultTableData)
dispatch(setTableBackwardData(series))
} finally {
dispatch(decrementQueryCount())
}
}
export const setTableCustomTimeAsync = (time: string) => async dispatch => {
await dispatch(setTableCustomTime(time))
await dispatch(executeTableQueryAsync())
}
export const setTableRelativeTimeAsync = (time: number) => async dispatch => {
await dispatch(setTableRelativeTime(time))
await dispatch(executeTableQueryAsync())
}
export const changeFilter = (id: string, operator: string, value: string) => ({
type: ActionTypes.ChangeFilter,
payload: {id, operator, value},
@ -272,44 +457,12 @@ export const executeHistogramQueryAsync = () => async (
}
}
const setTableData = (series: TableData): SetTableData => ({
type: ActionTypes.SetTableData,
payload: {data: {columns: series.columns, values: series.values}},
})
export const executeTableQueryAsync = () => async (
dispatch,
getState: GetState
): Promise<void> => {
const state = getState()
const queryConfig = getTableQueryConfig(state)
const timeRange = getTimeRange(state)
const namespace = getNamespace(state)
const proxyLink = getProxyLink(state)
const searchTerm = getSearchTerm(state)
const filters = getFilters(state)
if (!_.every([queryConfig, timeRange, namespace, proxyLink])) {
return
}
try {
dispatch(incrementQueryCount())
const query = buildLogQuery(timeRange, queryConfig, filters, searchTerm)
const response = await executeQueryAsync(
proxyLink,
namespace,
`${query} ORDER BY time DESC LIMIT 1000`
)
const series = getDeep(response, 'results.0.series.0', defaultTableData)
dispatch(setTableData(series))
} finally {
dispatch(decrementQueryCount())
}
export const executeTableQueryAsync = () => async (dispatch): Promise<void> => {
await Promise.all([
dispatch(executeTableForwardQueryAsync()),
dispatch(executeTableBackwardQueryAsync()),
dispatch(clearRowsAdded()),
])
}
export const decrementQueryCount = () => ({
@ -388,14 +541,13 @@ export const setTableQueryConfigAsync = () => async (
}
}
export const fetchMoreAsync = (
queryTimeEnd: string,
lastTime: number
) => async (dispatch, getState): Promise<void> => {
export const fetchMoreAsync = (queryTimeEnd: string) => async (
dispatch,
getState
): Promise<void> => {
const state = getState()
const tableQueryConfig = getTableQueryConfig(state)
const time = moment(lastTime).toISOString()
const timeRange = {lower: queryTimeEnd, upper: time}
const timeRange = {lower: queryTimeEnd}
const newQueryConfig = {
...tableQueryConfig,
range: timeRange,
@ -407,11 +559,17 @@ export const fetchMoreAsync = (
const params = [namespace, proxyLink, tableQueryConfig]
if (_.every(params)) {
const query = buildLogQuery(timeRange, newQueryConfig, filters, searchTerm)
const query = buildBackwardLogQuery(
queryTimeEnd,
newQueryConfig,
filters,
searchTerm
)
const response = await executeQueryAsync(
proxyLink,
namespace,
`${query} ORDER BY time DESC LIMIT 1000`
`${query} ORDER BY time DESC LIMIT ${INITIAL_LIMIT}`
)
const series = getDeep(response, 'results.0.series.0', defaultTableData)
@ -419,11 +577,57 @@ export const fetchMoreAsync = (
}
}
export const fetchNewerAsync = (queryTimeStart: string) => async (
dispatch,
getState
): Promise<void> => {
const state = getState()
const tableQueryConfig = getTableQueryConfig(state)
const timeRange = {lower: queryTimeStart}
const newQueryConfig = {
...tableQueryConfig,
range: timeRange,
}
const namespace = getNamespace(state)
const proxyLink = getProxyLink(state)
const searchTerm = getSearchTerm(state)
const filters = getFilters(state)
const params = [namespace, proxyLink, tableQueryConfig]
if (_.every(params)) {
const query = buildForwardLogQuery(
queryTimeStart,
newQueryConfig,
filters,
searchTerm
)
const response = await executeQueryAsync(
proxyLink,
namespace,
`${query} ORDER BY time ASC LIMIT ${INITIAL_LIMIT}`
)
const series = getDeep(response, 'results.0.series.0', defaultTableData)
await dispatch(
PrependMoreLogs({
columns: series.columns,
values: _.reverse(series.values),
})
)
}
}
export const ConcatMoreLogs = (series: TableData): ConcatMoreLogsAction => ({
type: ActionTypes.ConcatMoreLogs,
payload: {series},
})
export const PrependMoreLogs = (series: TableData): PrependMoreLogsAction => ({
type: ActionTypes.PrependMoreLogs,
payload: {series},
})
export const setNamespaceAsync = (namespace: Namespace) => async (
dispatch
): Promise<void> => {
@ -445,15 +649,17 @@ export const setNamespaces = (
},
})
export const setTimeRangeAsync = (timeRange: TimeRange) => async (
dispatch
): Promise<void> => {
dispatch({
export const setTimeRange = timeRange => ({
type: ActionTypes.SetTimeRange,
payload: {
timeRange,
},
})
export const setTimeRangeAsync = (timeRange: TimeRange) => async (
dispatch
): Promise<void> => {
dispatch(setTimeRange(timeRange))
dispatch(setHistogramQueryConfigAsync())
dispatch(setTableQueryConfigAsync())
}
@ -504,147 +710,6 @@ export const changeZoomAsync = (timeRange: TimeRange) => async (
}
}
const serverLogData = {
columns: [
{
name: 'severity',
position: 1,
encodings: [
{
type: 'visibility',
value: 'visible',
},
{
type: 'label',
value: 'icon',
},
{
type: 'label',
value: 'text',
},
{
type: 'color',
value: 'emerg',
name: 'ruby',
},
{
type: 'color',
value: 'alert',
name: 'fire',
},
{
type: 'color',
value: 'crit',
name: 'curacao',
},
{
type: 'color',
value: 'err',
name: 'tiger',
},
{
type: 'color',
value: 'warning',
name: 'pineapple',
},
{
type: 'color',
value: 'notice',
name: 'rainforest',
},
{
type: 'color',
value: 'info',
name: 'star',
},
{
type: 'color',
value: 'debug',
name: 'wolf',
},
],
},
{
name: 'timestamp',
position: 2,
encodings: [
{
type: 'visibility',
value: 'visible',
},
],
},
{
name: 'message',
position: 3,
encodings: [
{
type: 'visibility',
value: 'visible',
},
],
},
{
name: 'facility',
position: 4,
encodings: [
{
type: 'visibility',
value: 'visible',
},
],
},
{
name: 'time',
position: 0,
encodings: [
{
type: 'visibility',
value: 'hidden',
},
],
},
{
name: 'procid',
position: 5,
encodings: [
{
type: 'visibility',
value: 'visible',
},
{
type: 'displayName',
value: 'Proc ID',
},
],
},
{
name: 'host',
position: 7,
encodings: [
{
type: 'visibility',
value: 'visible',
},
],
},
{
name: 'appname',
position: 6,
encodings: [
{
type: 'visibility',
value: 'visible',
},
{
type: 'displayName',
value: 'Application',
},
],
},
],
}
export const getLogConfigAsync = (url: string) => async (
dispatch: Dispatch<SetConfigsAction>
): Promise<void> => {

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,
@ -21,7 +24,6 @@ import {
getColumnsFromData,
} from 'src/logs/utils/table'
import timeRanges from 'src/logs/data/timeRanges'
import {
SeverityFormatOptions,
SeverityColorOptions,
@ -35,21 +37,28 @@ import {
SeverityFormat,
SeverityLevelColor,
} from 'src/types/logs'
import {INITIAL_LIMIT} from 'src/logs/actions'
const ROW_HEIGHT = 26
const CHAR_WIDTH = 9
interface Props {
data: TableData
isScrolledToTop: boolean
onScrollVertical: () => void
onScrolledToTop: () => void
onTagSelection: (selection: {tag: string; key: string}) => void
fetchMore: (queryTimeEnd: string, time: number) => Promise<void>
fetchMore: (time: string) => Promise<void>
fetchNewer: (time: string) => void
hasScrolled: boolean
count: number
timeRange: TimeRange
queryCount: number
tableColumns: LogsTableColumn[]
severityFormat: SeverityFormat
severityLevelColors: SeverityLevelColor[]
scrollToRow?: number
tableInfiniteData: {
forward: TableData
backward: TableData
}
}
interface State {
@ -59,20 +68,50 @@ interface State {
currentMessageWidth: number
isMessageVisible: boolean
lastQueryTime: number
firstQueryTime: number
visibleColumnsCount: number
}
const calculateScrollTop = (currentMessageWidth, data, scrollToRow) => {
const rowCharLimit = calculateRowCharWidth(currentMessageWidth)
return _.reduce(
_.range(0, scrollToRow),
(acc, index) => {
return acc + calculateMessageHeight(index, data, rowCharLimit)
},
0
)
}
class LogsTable extends Component<Props, State> {
public static getDerivedStateFromProps(props, state): State {
const {isScrolledToTop} = props
const {
isScrolledToTop,
scrollToRow,
data,
tableColumns,
severityFormat,
} = props
const currentMessageWidth = getMessageWidth(
data,
tableColumns,
severityFormat
)
let lastQueryTime = _.get(state, 'lastQueryTime', null)
let firstQueryTime = _.get(state, 'firstQueryTime', null)
let scrollTop = _.get(state, 'scrollTop', 0)
if (isScrolledToTop) {
lastQueryTime = null
firstQueryTime = null
scrollTop = 0
}
if (scrollToRow) {
scrollTop = calculateScrollTop(currentMessageWidth, data, scrollToRow)
}
const scrollLeft = _.get(state, 'scrollLeft', 0)
let isMessageVisible: boolean = false
@ -87,16 +126,14 @@ class LogsTable extends Component<Props, State> {
...state,
isQuerying: false,
lastQueryTime,
firstQueryTime,
scrollTop,
scrollLeft,
currentRow: -1,
currentMessageWidth: getMessageWidth(
props.data,
props.tableColumns,
props.severityFormat
),
currentMessageWidth,
isMessageVisible,
visibleColumnsCount,
scrollToRow,
}
}
@ -123,9 +160,12 @@ class LogsTable extends Component<Props, State> {
currentRow: -1,
currentMessageWidth: 0,
lastQueryTime: null,
firstQueryTime: null,
isMessageVisible,
visibleColumnsCount,
}
this.loadMoreAboveRows = _.throttle(this.loadMoreAboveRows, 5000)
}
public componentDidUpdate() {
@ -187,8 +227,8 @@ class LogsTable extends Component<Props, State> {
</AutoSizer>
<InfiniteLoader
isRowLoaded={this.isRowLoaded}
loadMoreRows={this.loadMoreRows}
rowCount={this.props.count}
loadMoreRows={this.loadMoreBelowRows}
rowCount={this.rowCount() + INITIAL_LIMIT}
>
{({registerChild, onRowsRendered}) => (
<AutoSizer>
@ -204,21 +244,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 +265,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 {scrollToRow} = this.props
const {scrollLeft, scrollTop} = this.state
const result: any = {
width,
height,
rowHeight: this.calculateRowHeight,
rowCount: this.rowCount(),
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 (scrollToRow) {
result.scrollToRow = scrollToRow
}
return result
}
private handleGridScroll = ({scrollLeft}) => {
this.handleScroll({scrollLeft})
}
@ -255,6 +321,10 @@ class LogsTable extends Component<Props, State> {
this.setState({scrollTop})
if (scrollTop < 200 && scrollTop < previousTop) {
this.loadMoreAboveRows()
}
if (scrollTop === 0) {
this.props.onScrolledToTop()
} else if (scrollTop !== previousTop) {
@ -275,35 +345,59 @@ class LogsTable extends Component<Props, State> {
}
}
private loadMoreRows = async () => {
const data = getValuesFromData(this.props.data)
const {timeRange} = this.props
private loadMoreAboveRows = async () => {
// Prevent multiple queries at the same time
const {queryCount} = this.props
if (queryCount > 0) {
return
}
const data = getValuesFromData(this.props.tableInfiniteData.forward)
const firstTime = getDeep(data, '0.0', new Date().getTime() / 1000)
const {firstQueryTime} = this.state
if (firstQueryTime && firstQueryTime > firstTime) {
return
}
this.setState({firstQueryTime: firstTime})
await this.props.fetchNewer(moment(firstTime).toISOString())
}
private loadMoreBelowRows = async () => {
// Prevent multiple queries at the same time
const {queryCount} = this.props
if (queryCount > 0) {
return
}
const data = getValuesFromData(this.props.tableInfiniteData.backward)
const lastTime = getDeep(
data,
`${data.length - 1}.0`,
new Date().getTime() / 1000
)
const upper = getDeep<string>(timeRange, 'upper', null)
const lower = getDeep<string>(timeRange, 'lower', null)
if (this.state.lastQueryTime && this.state.lastQueryTime <= lastTime) {
// Guard against fetching on scrolling back up then down
const {lastQueryTime} = this.state
if (lastQueryTime && lastQueryTime <= lastTime) {
return
}
const firstQueryTime = getDeep<number>(data, '0.0', null)
let queryTimeEnd = lower
if (!upper) {
const foundTimeRange = timeRanges.find(range => range.lower === lower)
queryTimeEnd = moment(firstQueryTime)
.subtract(foundTimeRange.seconds, 'seconds')
.toISOString()
}
this.setState({lastQueryTime: lastTime})
await this.props.fetchMore(queryTimeEnd, lastTime)
await this.props.fetchMore(moment(lastTime).toISOString())
}
private rowCount = (): number => {
const data = this.props.tableInfiniteData
return (
getDeep<number>(data, 'forward.values.length', 0) +
getDeep<number>(data, 'backward.values.length', 0)
)
}
private isRowLoaded = ({index}) => {
return !!getValuesFromData(this.props.data)[index]
return index < this.rowCount() - 1
}
private handleWindowResize = () => {
@ -346,8 +440,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 +449,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

@ -0,0 +1,158 @@
import React, {Component, MouseEvent} from 'react'
import classnames from 'classnames'
import moment from 'moment'
import FancyScrollbar from 'src/shared/components/FancyScrollbar'
import timePoints from 'src/logs/data/timePoints'
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 CustomSingularTime from 'src/shared/components/CustomSingularTime'
interface Props {
customTime?: string
relativeTime?: number
onChooseCustomTime: (time: string) => void
onChooseRelativeTime: (time: number) => void
}
interface State {
isOpen: boolean
isTimeSelectorOpen: boolean
}
const dateFormat = 'YYYY-MM-DD HH:mm'
const format = (t: string) => moment(t.replace(/\'/g, '')).format(dateFormat)
@ErrorHandling
class TimeRangeDropdown extends Component<Props, State> {
constructor(props) {
super(props)
this.state = {
isOpen: false,
isTimeSelectorOpen: false,
}
}
public render() {
const {isTimeSelectorOpen} = this.state
return (
<ClickOutside onClickOutside={this.handleClickOutside}>
<div className="time-range-dropdown" style={{display: 'inline'}}>
<div className={this.dropdownClassName}>
<div
className="btn btn-sm btn-default dropdown-toggle"
onClick={this.toggleMenu}
>
<span className="icon clock" />
<span className="dropdown-selected">{this.timeInputValue}</span>
<span className="caret" />
</div>
<ul className="dropdown-menu">
<FancyScrollbar
autoHide={false}
autoHeight={true}
maxHeight={DROPDOWN_MENU_MAX_HEIGHT}
>
<div>
<li className="dropdown-header">Absolute Time</li>
<li
className={
isTimeSelectorOpen
? 'active dropdown-item custom-timerange'
: 'dropdown-item custom-timerange'
}
>
<a href="#" onClick={this.handleOpenCustomTime}>
Date Picker
</a>
</li>
</div>
<li className="dropdown-header">Relative Time</li>
{timePoints.map(point => {
return (
<li className="dropdown-item" key={`pot-${point.value}`}>
<a
href="#"
onClick={this.handleSelection}
data-value={point.value}
>
{point.text}
</a>
</li>
)
})}
</FancyScrollbar>
</ul>
</div>
{isTimeSelectorOpen ? (
<ClickOutside onClickOutside={this.handleCloseCustomTime}>
<div className="custom-time--overlay">
<CustomSingularTime
onSelected={this.handleCustomSelection}
time={this.props.customTime}
/>
</div>
</ClickOutside>
) : null}
</div>
</ClickOutside>
)
}
private get dropdownClassName(): string {
const {isOpen} = this.state
const absoluteTimeRange = !!this.props.customTime
return classnames('dropdown', {
'dropdown-290': absoluteTimeRange,
'dropdown-120': !absoluteTimeRange,
open: isOpen,
})
}
private handleCustomSelection = (time: string) => {
this.handleCloseCustomTime()
this.props.onChooseCustomTime(time)
this.setState({isOpen: false})
}
private handleSelection = (e: MouseEvent<HTMLAnchorElement>) => {
e.preventDefault()
const {dataset} = e.target as HTMLAnchorElement
this.props.onChooseRelativeTime(+dataset.value)
this.setState({isOpen: false})
}
private get timeInputValue(): string {
if (!this.props.customTime) {
const point = timePoints.find(p => p.value === this.props.relativeTime)
if (point) {
return point.text
}
return 'None'
}
return format(this.props.customTime)
}
private handleClickOutside = () => {
this.setState({isOpen: false})
}
private toggleMenu = () => {
this.setState({isOpen: !this.state.isOpen})
}
private handleCloseCustomTime = () => {
this.setState({isTimeSelectorOpen: false})
}
private handleOpenCustomTime = () => {
this.setState({isTimeSelectorOpen: true})
}
}
export default TimeRangeDropdown

View File

@ -23,8 +23,6 @@ interface Props {
}
onChooseTimeRange: (timeRange: TimeRange) => void
preventCustomTimeRange?: boolean
page?: string
}
interface State {
@ -36,10 +34,6 @@ interface State {
@ErrorHandling
class TimeRangeDropdown extends Component<Props, State> {
public static defaultProps = {
page: 'default',
}
constructor(props) {
super(props)
const {lower, upper} = props.selected
@ -56,7 +50,7 @@ class TimeRangeDropdown extends Component<Props, State> {
}
public render() {
const {selected, preventCustomTimeRange, page} = this.props
const {selected} = this.props
const {customTimeRange, isCustomTimeRangeOpen} = this.state
return (
@ -79,7 +73,6 @@ class TimeRangeDropdown extends Component<Props, State> {
autoHeight={true}
maxHeight={DROPDOWN_MENU_MAX_HEIGHT}
>
{preventCustomTimeRange ? null : (
<div>
<li className="dropdown-header">Absolute Time</li>
<li
@ -94,10 +87,7 @@ class TimeRangeDropdown extends Component<Props, State> {
</a>
</li>
</div>
)}
<li className="dropdown-header">
{preventCustomTimeRange ? '' : 'Relative '}Time
</li>
<li className="dropdown-header">Relative Time</li>
{timeRanges.map(item => {
return (
<li className="dropdown-item" key={item.menuOption}>
@ -118,8 +108,8 @@ class TimeRangeDropdown extends Component<Props, State> {
timeRange={customTimeRange}
onClose={this.handleCloseCustomTimeRange}
isVisible={isCustomTimeRangeOpen}
timeInterval={60}
page={page}
timeInterval={300}
page="default"
/>
</div>
</ClickOutside>
@ -131,9 +121,7 @@ class TimeRangeDropdown extends Component<Props, State> {
private get dropdownClassName(): string {
const {isOpen} = this.state
const {lower, upper} = _.get(this.props, 'selected', {upper: '', lower: ''})
const absoluteTimeRange = !_.isEmpty(lower) && !_.isEmpty(upper)
return classnames('dropdown', {

View File

@ -1,10 +1,12 @@
import React, {PureComponent} from 'react'
import React, {Component} from 'react'
import uuid from 'uuid'
import _ from 'lodash'
import {connect} from 'react-redux'
import {AutoSizer} from 'react-virtualized'
import {
setTableCustomTimeAsync,
setTableRelativeTimeAsync,
getSourceAndPopulateNamespacesAsync,
setTimeRangeAsync,
setNamespaceAsync,
@ -15,6 +17,7 @@ import {
removeFilter,
changeFilter,
fetchMoreAsync,
fetchNewerAsync,
getLogConfigAsync,
updateLogConfigAsync,
} from 'src/logs/actions'
@ -26,14 +29,10 @@ import OptionsOverlay from 'src/logs/components/OptionsOverlay'
import SearchBar from 'src/logs/components/LogsSearchBar'
import FilterBar from 'src/logs/components/LogsFilterBar'
import LogsTable from 'src/logs/components/LogsTable'
import PointInTimeDropDown from 'src/logs/components/PointInTimeDropDown'
import {getDeep} from 'src/utils/wrappers'
import {colorForSeverity} from 'src/logs/utils/colors'
import OverlayTechnology from 'src/reusable_ui/components/overlays/OverlayTechnology'
import {
orderTableColumns,
filterTableColumns,
} from 'src/dashboards/utils/tableGraph'
import {SeverityFormatOptions} from 'src/logs/constants'
import {Source, Namespace, TimeRange} from 'src/types'
@ -46,6 +45,7 @@ import {
LogConfig,
TableData,
} from 'src/types/logs'
import {applyChangesToTableData} from 'src/logs/utils/table'
interface Props {
sources: Source[]
@ -59,12 +59,16 @@ interface Props {
changeZoomAsync: (timeRange: TimeRange) => void
executeQueriesAsync: () => void
setSearchTermAsync: (searchTerm: string) => void
fetchMoreAsync: (queryTimeEnd: string, lastTime: number) => Promise<void>
setTableRelativeTime: (time: number) => void
setTableCustomTime: (time: string) => void
fetchMoreAsync: (queryTimeEnd: string) => Promise<void>
fetchNewerAsync: (queryTimeEnd: string) => Promise<void>
addFilter: (filter: Filter) => void
removeFilter: (id: string) => void
changeFilter: (id: string, operator: string, value: string) => void
getConfig: (url: string) => Promise<void>
updateConfig: (url: string, config: LogConfig) => Promise<void>
newRowsAdded: number
timeRange: TimeRange
histogramData: HistogramData
tableData: TableData
@ -73,6 +77,14 @@ interface Props {
queryCount: number
logConfig: LogConfig
logConfigLink: string
tableInfiniteData: {
forward: TableData
backward: TableData
}
tableTime: {
custom: string
relative: number
}
}
interface State {
@ -80,9 +92,10 @@ interface State {
liveUpdating: boolean
isOverlayVisible: boolean
histogramColors: HistogramColor[]
hasScrolled: boolean
}
class LogsPage extends PureComponent<Props, State> {
class LogsPage extends Component<Props, State> {
public static getDerivedStateFromProps(props: Props) {
const severityLevelColors: SeverityLevelColor[] = _.get(
props.logConfig,
@ -97,6 +110,7 @@ class LogsPage extends PureComponent<Props, State> {
}
private interval: NodeJS.Timer
private loadingNewer: boolean = false
constructor(props: Props) {
super(props)
@ -106,6 +120,7 @@ class LogsPage extends PureComponent<Props, State> {
liveUpdating: false,
isOverlayVisible: false,
histogramColors: [],
hasScrolled: false,
}
}
@ -131,8 +146,7 @@ class LogsPage extends PureComponent<Props, State> {
}
public render() {
const {liveUpdating} = this.state
const {searchTerm, filters, queryCount, timeRange} = this.props
const {searchTerm, filters, queryCount, timeRange, tableTime} = this.props
return (
<>
@ -140,6 +154,17 @@ class LogsPage extends PureComponent<Props, State> {
{this.header}
<div className="page-contents logs-viewer">
<LogsGraphContainer>{this.chart}</LogsGraphContainer>
<div style={{height: '50px', position: 'relative'}}>
<div style={{position: 'absolute', right: '10px', top: '10px'}}>
<span style={{marginRight: '10px'}}>Go to </span>
<PointInTimeDropDown
customTime={tableTime.custom}
relativeTime={tableTime.relative}
onChooseCustomTime={this.handleChooseCustomTime}
onChooseRelativeTime={this.handleChooseRelativeTime}
/>
</div>
</div>
<SearchBar
searchString={searchTerm}
onSearch={this.handleSubmitSearch}
@ -153,16 +178,21 @@ class LogsPage extends PureComponent<Props, State> {
/>
<LogsTable
count={this.histogramTotal}
queryCount={queryCount}
data={this.tableData}
onScrollVertical={this.handleVerticalScroll}
onScrolledToTop={this.handleScrollToTop}
isScrolledToTop={liveUpdating}
isScrolledToTop={false}
onTagSelection={this.handleTagSelection}
fetchMore={this.props.fetchMoreAsync}
fetchNewer={this.fetchNewer}
timeRange={timeRange}
scrollToRow={this.tableScrollToRow}
tableColumns={this.tableColumns}
severityFormat={this.severityFormat}
severityLevelColors={this.severityLevelColors}
hasScrolled={this.state.hasScrolled}
tableInfiniteData={this.props.tableInfiniteData}
/>
</div>
</div>
@ -171,19 +201,50 @@ class LogsPage extends PureComponent<Props, State> {
)
}
private fetchNewer = (time: string) => {
this.loadingNewer = true
this.props.fetchNewerAsync(time)
}
private get tableScrollToRow() {
if (this.loadingNewer && this.props.newRowsAdded) {
this.loadingNewer = false
return this.props.newRowsAdded || 0
}
if (this.state.hasScrolled) {
return
}
return Math.max(
_.get(this.props, 'tableInfiniteData.forward.values.length', 0) - 3,
0
)
}
private handleChooseCustomTime = (time: string) => {
this.props.setTableCustomTime(time)
}
private handleChooseRelativeTime = (time: number) => {
this.props.setTableRelativeTime(time)
}
private get tableData(): TableData {
const {tableData} = this.props
const tableColumns = this.tableColumns
const columns = _.get(tableData, 'columns', [])
const values = _.get(tableData, 'values', [])
const data = [columns, ...values]
const forwardData = applyChangesToTableData(
this.props.tableInfiniteData.forward,
this.tableColumns
)
const filteredData = filterTableColumns(data, tableColumns)
const orderedData = orderTableColumns(filteredData, tableColumns)
const updatedColumns: string[] = _.get(orderedData, '0', [])
const updatedValues = _.slice(orderedData, 1)
const backwardData = applyChangesToTableData(
this.props.tableInfiniteData.backward,
this.tableColumns
)
return {columns: updatedColumns, values: updatedValues}
return {
columns: forwardData.columns,
values: [...forwardData.values, ...backwardData.values],
}
}
private get logConfigLink(): string {
@ -219,8 +280,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 +360,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 +412,8 @@ class LogsPage extends PureComponent<Props, State> {
}
private fetchNewDataset() {
this.props.executeQueriesAsync()
this.setState({liveUpdating: true})
this.props.executeQueriesAsync()
}
private handleToggleOverlay = (): void => {
@ -420,6 +481,7 @@ const mapStateToProps = ({
config: {logViewer},
},
logs: {
newRowsAdded,
currentSource,
currentNamespaces,
timeRange,
@ -430,6 +492,8 @@ const mapStateToProps = ({
filters,
queryCount,
logConfig,
tableTime,
tableInfiniteData,
},
}) => ({
sources,
@ -443,7 +507,10 @@ const mapStateToProps = ({
filters,
queryCount,
logConfig,
tableTime,
logConfigLink: logViewer,
tableInfiniteData,
newRowsAdded,
})
const mapDispatchToProps = {
@ -458,6 +525,9 @@ const mapDispatchToProps = {
removeFilter,
changeFilter,
fetchMoreAsync,
fetchNewerAsync,
setTableCustomTime: setTableCustomTimeAsync,
setTableRelativeTime: setTableRelativeTimeAsync,
getConfig: getLogConfigAsync,
updateConfig: updateLogConfigAsync,
}

View File

@ -0,0 +1,140 @@
export const serverLogData = {
columns: [
{
name: 'severity',
position: 1,
encodings: [
{
type: 'visibility',
value: 'visible',
},
{
type: 'label',
value: 'icon',
},
{
type: 'label',
value: 'text',
},
{
type: 'color',
value: 'emerg',
name: 'ruby',
},
{
type: 'color',
value: 'alert',
name: 'fire',
},
{
type: 'color',
value: 'crit',
name: 'curacao',
},
{
type: 'color',
value: 'err',
name: 'tiger',
},
{
type: 'color',
value: 'warning',
name: 'pineapple',
},
{
type: 'color',
value: 'notice',
name: 'rainforest',
},
{
type: 'color',
value: 'info',
name: 'star',
},
{
type: 'color',
value: 'debug',
name: 'wolf',
},
],
},
{
name: 'timestamp',
position: 2,
encodings: [
{
type: 'visibility',
value: 'visible',
},
],
},
{
name: 'message',
position: 3,
encodings: [
{
type: 'visibility',
value: 'visible',
},
],
},
{
name: 'facility',
position: 4,
encodings: [
{
type: 'visibility',
value: 'visible',
},
],
},
{
name: 'time',
position: 0,
encodings: [
{
type: 'visibility',
value: 'hidden',
},
],
},
{
name: 'procid',
position: 5,
encodings: [
{
type: 'visibility',
value: 'visible',
},
{
type: 'displayName',
value: 'Proc ID',
},
],
},
{
name: 'host',
position: 7,
encodings: [
{
type: 'visibility',
value: 'visible',
},
],
},
{
name: 'appname',
position: 6,
encodings: [
{
type: 'visibility',
value: 'visible',
},
{
type: 'displayName',
value: 'Application',
},
],
},
],
}

View File

@ -0,0 +1,26 @@
export default [
{
text: '1 minute ago',
value: 60,
},
{
text: '5 minute ago',
value: 300,
},
{
text: '10 minute ago',
value: 600,
},
{
text: '30 minute ago',
value: 1800,
},
{
text: '1 hour ago',
value: 3600,
},
{
text: '3 hour ago',
value: 10800,
},
]

View File

@ -9,11 +9,26 @@ import {
DecrementQueryCountAction,
IncrementQueryCountAction,
ConcatMoreLogsAction,
PrependMoreLogsAction,
SetConfigsAction,
} from 'src/logs/actions'
import {SeverityFormatOptions} from 'src/logs/constants'
import {LogsState} from 'src/types/logs'
import {LogsState, TableData} from 'src/types/logs'
const defaultTableData: TableData = {
columns: [
'time',
'severity',
'timestamp',
'facility',
'procid',
'appname',
'host',
'message',
],
values: [],
}
const defaultState: LogsState = {
currentSource: null,
@ -32,6 +47,12 @@ const defaultState: LogsState = {
severityFormat: SeverityFormatOptions.dotText,
severityLevelColors: [],
},
tableTime: {},
tableInfiniteData: {
forward: defaultTableData,
backward: defaultTableData,
},
newRowsAdded: 0,
}
const removeFilter = (
@ -92,14 +113,46 @@ const concatMoreLogs = (
const {
series: {values},
} = action.payload
const {tableData} = state
const vals = [...tableData.values, ...values]
const {tableInfiniteData} = state
const {backward} = tableInfiniteData
const vals = [...backward.values, ...values]
return {
...state,
tableData: {
columns: tableData.columns,
tableInfiniteData: {
...tableInfiniteData,
backward: {
columns: backward.columns,
values: vals,
},
},
}
}
const prependMoreLogs = (
state: LogsState,
action: PrependMoreLogsAction
): LogsState => {
const {
series: {values},
} = action.payload
const {tableInfiniteData} = state
const {forward} = tableInfiniteData
const vals = [...values, ...forward.values]
const uniqueValues = _.uniqBy(vals, '0')
const newRowsAdded = uniqueValues.length - forward.values.length
return {
...state,
newRowsAdded,
tableInfiniteData: {
...tableInfiniteData,
forward: {
columns: forward.columns,
values: uniqueValues,
},
},
}
}
@ -135,11 +188,33 @@ export default (state: LogsState = defaultState, action: Action) => {
return {...state, tableQueryConfig: action.payload.queryConfig}
case ActionTypes.SetTableData:
return {...state, tableData: action.payload.data}
case ActionTypes.ClearRowsAdded:
return {...state, newRowsAdded: null}
case ActionTypes.SetTableForwardData:
return {
...state,
tableInfiniteData: {
...state.tableInfiniteData,
forward: action.payload.data,
},
}
case ActionTypes.SetTableBackwardData:
return {
...state,
tableInfiniteData: {
...state.tableInfiniteData,
backward: action.payload.data,
},
}
case ActionTypes.ChangeZoom:
return {...state, timeRange: action.payload.timeRange}
case ActionTypes.SetSearchTerm:
const {searchTerm} = action.payload
return {...state, searchTerm}
case ActionTypes.SetTableCustomTime:
return {...state, tableTime: {custom: action.payload.time}}
case ActionTypes.SetTableRelativeTime:
return {...state, tableTime: {relative: action.payload.time}}
case ActionTypes.AddFilter:
return addFilter(state, action)
case ActionTypes.RemoveFilter:
@ -152,6 +227,8 @@ export default (state: LogsState = defaultState, action: Action) => {
return decrementQueryCount(state, action)
case ActionTypes.ConcatMoreLogs:
return concatMoreLogs(state, action)
case ActionTypes.PrependMoreLogs:
return prependMoreLogs(state, action)
case ActionTypes.SetConfig:
return setConfigs(state, action)
default:

View File

@ -115,6 +115,99 @@ export const filtersClause = (filters: Filter[]): string => {
).join(' AND ')
}
export function buildInfiniteWhereClause({
lower,
upper,
tags,
areTagsAccepted,
}: QueryConfig): string {
const timeClauses = []
if (lower) {
timeClauses.push(`time >= '${lower}'`)
}
if (upper) {
timeClauses.push(`time < '${upper}'`)
}
const tagClauses = _.keys(tags).map(k => {
const operator = areTagsAccepted ? '=' : '!='
if (tags[k].length > 1) {
const joinedOnOr = tags[k]
.map(v => `"${k}"${operator}'${v}'`)
.join(' OR ')
return `(${joinedOnOr})`
}
return `"${k}"${operator}'${tags[k]}'`
})
const subClauses = timeClauses.concat(tagClauses)
if (!subClauses.length) {
return ''
}
return ` WHERE ${subClauses.join(' AND ')}`
}
export function buildGeneralLogQuery(
condition: string,
config: QueryConfig,
filters: Filter[],
searchTerm: string | null = null
) {
const {groupBy, fill = NULL_STRING} = config
const select = buildSelect(config, '')
const dimensions = buildGroupBy(groupBy)
const fillClause = groupBy.time ? buildFill(fill) : ''
if (!_.isEmpty(searchTerm)) {
condition = `${condition} AND message =~ ${new RegExp(searchTerm)}`
}
if (!_.isEmpty(filters)) {
condition = `${condition} AND ${filtersClause(filters)}`
}
return `${select}${condition}${dimensions}${fillClause}`
}
export function buildBackwardLogQuery(
upper: string,
config: QueryConfig,
filters: Filter[],
searchTerm: string | null = null
) {
const {tags, areTagsAccepted} = config
const condition = buildInfiniteWhereClause({
upper,
tags,
areTagsAccepted,
})
return buildGeneralLogQuery(condition, config, filters, searchTerm)
}
export function buildForwardLogQuery(
lower: string,
config: QueryConfig,
filters: Filter[],
searchTerm: string | null = null
) {
const {tags, areTagsAccepted} = config
const condition = buildInfiniteWhereClause({
lower,
tags,
areTagsAccepted,
})
return buildGeneralLogQuery(condition, config, filters, searchTerm)
}
export function buildLogQuery(
timeRange: TimeRange,
config: QueryConfig,
@ -144,6 +237,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

@ -3,7 +3,12 @@ import moment from 'moment'
import {getDeep} from 'src/utils/wrappers'
import {TableData, LogsTableColumn, SeverityFormat} from 'src/types/logs'
import {SeverityFormatOptions} from 'src/logs/constants'
import {
orderTableColumns,
filterTableColumns,
} from 'src/dashboards/utils/tableGraph'
export const ROW_HEIGHT = 26
const CHAR_WIDTH = 9
export const getValuesFromData = (data: TableData): string[][] =>
@ -69,6 +74,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[],
@ -97,3 +123,22 @@ export const getMessageWidth = (
return calculatedWidth - CHAR_WIDTH
}
export const applyChangesToTableData = (
tableData: TableData,
tableColumns: LogsTableColumn[]
): TableData => {
const columns = _.get(tableData, 'columns', [])
const values = _.get(tableData, 'values', [])
const data = [columns, ...values]
const filteredData = filterTableColumns(data, tableColumns)
const orderedData = orderTableColumns(filteredData, tableColumns)
const updatedColumns: string[] = _.get(orderedData, '0', [])
const updatedValues = _.slice(orderedData, 1)
return {
columns: updatedColumns,
values: updatedValues,
}
}

View File

@ -0,0 +1,97 @@
import React, {Component} from 'react'
import rome from 'rome'
import {ErrorHandling} from 'src/shared/decorators/errors'
import {formatTimeRange} from 'src/shared/utils/time'
interface Props {
onSelected: (time: string) => 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
style={{marginTop: '10px'}}
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 time = date.toISOString()
this.props.onSelected(time)
}
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)
}

View File

@ -32,6 +32,15 @@ export interface LogsState {
filters: Filter[]
queryCount: number
logConfig: LogConfig
tableInfiniteData: {
forward: TableData
backward: TableData
}
tableTime: {
custom?: string
relative?: string
}
newRowsAdded: number
}
export interface LogConfig {