Merge pull request #3885 from influxdata/features/infinite-scroll
Initial log viewer infinite scroll behaviourpull/3893/head
commit
ee9d7ab1fb
|
@ -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 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())
|
||||
}
|
||||
|
@ -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> => {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
|
@ -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,25 +73,21 @@ class TimeRangeDropdown extends Component<Props, State> {
|
|||
autoHeight={true}
|
||||
maxHeight={DROPDOWN_MENU_MAX_HEIGHT}
|
||||
>
|
||||
{preventCustomTimeRange ? null : (
|
||||
<div>
|
||||
<li className="dropdown-header">Absolute Time</li>
|
||||
<li
|
||||
className={
|
||||
isCustomTimeRangeOpen
|
||||
? 'active dropdown-item custom-timerange'
|
||||
: 'dropdown-item custom-timerange'
|
||||
}
|
||||
>
|
||||
<a href="#" onClick={this.showCustomTimeRange}>
|
||||
Date Picker
|
||||
</a>
|
||||
</li>
|
||||
</div>
|
||||
)}
|
||||
<li className="dropdown-header">
|
||||
{preventCustomTimeRange ? '' : 'Relative '}Time
|
||||
</li>
|
||||
<div>
|
||||
<li className="dropdown-header">Absolute Time</li>
|
||||
<li
|
||||
className={
|
||||
isCustomTimeRangeOpen
|
||||
? 'active dropdown-item custom-timerange'
|
||||
: 'dropdown-item custom-timerange'
|
||||
}
|
||||
>
|
||||
<a href="#" onClick={this.showCustomTimeRange}>
|
||||
Date Picker
|
||||
</a>
|
||||
</li>
|
||||
</div>
|
||||
<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', {
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
|
@ -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,
|
||||
},
|
||||
]
|
|
@ -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,13 +113,45 @@ 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,
|
||||
values: vals,
|
||||
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:
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
|
@ -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)
|
||||
}
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue