Merge pull request #3897 from influxdata/logs-histogram-window
Improve Logs Histogram Time Controlspull/10616/head
commit
2742c93bdb
|
@ -2,7 +2,7 @@ import moment from 'moment'
|
|||
import _ from 'lodash'
|
||||
import {Dispatch} from 'redux'
|
||||
|
||||
import {Source, Namespace, TimeRange, QueryConfig} from 'src/types'
|
||||
import {Source, Namespace, QueryConfig} from 'src/types'
|
||||
import {getSource} from 'src/shared/apis'
|
||||
import {getDatabasesWithRetentionPolicies} from 'src/shared/apis/databases'
|
||||
import {
|
||||
|
@ -20,7 +20,17 @@ import {
|
|||
getLogConfig as getLogConfigAJAX,
|
||||
updateLogConfig as updateLogConfigAJAX,
|
||||
} from 'src/logs/api'
|
||||
import {LogsState, Filter, TableData, LogConfig} from 'src/types/logs'
|
||||
|
||||
import {
|
||||
LogsState,
|
||||
Filter,
|
||||
TableData,
|
||||
LogConfig,
|
||||
TimeRange,
|
||||
TimeBounds,
|
||||
TimeWindow,
|
||||
TimeMarker,
|
||||
} from 'src/types/logs'
|
||||
|
||||
export const INITIAL_LIMIT = 100
|
||||
|
||||
|
@ -31,7 +41,7 @@ const defaultTableData: TableData = {
|
|||
'timestamp',
|
||||
'facility',
|
||||
'procid',
|
||||
'application',
|
||||
'appname',
|
||||
'host',
|
||||
'message',
|
||||
],
|
||||
|
@ -47,13 +57,14 @@ type GetState = () => State
|
|||
export enum ActionTypes {
|
||||
SetSource = 'LOGS_SET_SOURCE',
|
||||
SetNamespaces = 'LOGS_SET_NAMESPACES',
|
||||
SetTimeRange = 'LOGS_SET_TIMERANGE',
|
||||
SetTimeBounds = 'LOGS_SET_TIMEBOUNDS',
|
||||
SetTimeWindow = 'LOGS_SET_TIMEWINDOW',
|
||||
SetTimeMarker = 'LOGS_SET_TIMEMARKER',
|
||||
SetNamespace = 'LOGS_SET_NAMESPACE',
|
||||
SetHistogramQueryConfig = 'LOGS_SET_HISTOGRAM_QUERY_CONFIG',
|
||||
SetHistogramData = 'LOGS_SET_HISTOGRAM_DATA',
|
||||
SetTableQueryConfig = 'LOGS_SET_TABLE_QUERY_CONFIG',
|
||||
SetTableData = 'LOGS_SET_TABLE_DATA',
|
||||
ChangeZoom = 'LOGS_CHANGE_ZOOM',
|
||||
SetSearchTerm = 'LOGS_SET_SEARCH_TERM',
|
||||
AddFilter = 'LOGS_ADD_FILTER',
|
||||
RemoveFilter = 'LOGS_REMOVE_FILTER',
|
||||
|
@ -167,10 +178,24 @@ interface SetNamespaceAction {
|
|||
}
|
||||
}
|
||||
|
||||
interface SetTimeRangeAction {
|
||||
type: ActionTypes.SetTimeRange
|
||||
interface SetTimeBoundsAction {
|
||||
type: ActionTypes.SetTimeBounds
|
||||
payload: {
|
||||
timeRange: TimeRange
|
||||
timeBounds: TimeBounds
|
||||
}
|
||||
}
|
||||
|
||||
interface SetTimeWindowAction {
|
||||
type: ActionTypes.SetTimeWindow
|
||||
payload: {
|
||||
timeWindow: TimeWindow
|
||||
}
|
||||
}
|
||||
|
||||
interface SetTimeMarkerAction {
|
||||
type: ActionTypes.SetTimeMarker
|
||||
payload: {
|
||||
timeMarker: TimeMarker
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -209,13 +234,6 @@ interface SetSearchTerm {
|
|||
}
|
||||
}
|
||||
|
||||
interface ChangeZoomAction {
|
||||
type: ActionTypes.ChangeZoom
|
||||
payload: {
|
||||
timeRange: TimeRange
|
||||
}
|
||||
}
|
||||
|
||||
export interface SetConfigsAction {
|
||||
type: ActionTypes.SetConfig
|
||||
payload: {
|
||||
|
@ -226,11 +244,12 @@ export interface SetConfigsAction {
|
|||
export type Action =
|
||||
| SetSourceAction
|
||||
| SetNamespacesAction
|
||||
| SetTimeRangeAction
|
||||
| SetTimeBoundsAction
|
||||
| SetTimeWindowAction
|
||||
| SetTimeMarkerAction
|
||||
| SetNamespaceAction
|
||||
| SetHistogramQueryConfig
|
||||
| SetHistogramData
|
||||
| ChangeZoomAction
|
||||
| SetTableData
|
||||
| SetTableQueryConfig
|
||||
| SetSearchTerm
|
||||
|
@ -313,6 +332,21 @@ export const setTableBackwardData = (
|
|||
payload: {data},
|
||||
})
|
||||
|
||||
export const setTimeWindow = (timeWindow: TimeWindow): SetTimeWindowAction => ({
|
||||
type: ActionTypes.SetTimeWindow,
|
||||
payload: {timeWindow},
|
||||
})
|
||||
|
||||
export const setTimeMarker = (timeMarker: TimeMarker): SetTimeMarkerAction => ({
|
||||
type: ActionTypes.SetTimeMarker,
|
||||
payload: {timeMarker},
|
||||
})
|
||||
|
||||
export const setTimeBounds = (timeBounds: TimeBounds): SetTimeBoundsAction => ({
|
||||
type: ActionTypes.SetTimeBounds,
|
||||
payload: {timeBounds},
|
||||
})
|
||||
|
||||
export const executeTableForwardQueryAsync = () => async (
|
||||
dispatch,
|
||||
getState: GetState
|
||||
|
@ -501,7 +535,13 @@ export const setHistogramQueryConfigAsync = () => async (
|
|||
const timeRange = getDeep<TimeRange | null>(state, 'logs.timeRange', null)
|
||||
|
||||
if (timeRange && namespace) {
|
||||
const queryConfig = buildHistogramQueryConfig(namespace, timeRange)
|
||||
const queryTimeRange = {
|
||||
upper: timeRange.upper,
|
||||
lower: timeRange.lower,
|
||||
seconds: timeRange.seconds,
|
||||
}
|
||||
|
||||
const queryConfig = buildHistogramQueryConfig(namespace, queryTimeRange)
|
||||
|
||||
dispatch({
|
||||
type: ActionTypes.SetHistogramQueryConfig,
|
||||
|
@ -645,17 +685,7 @@ export const setNamespaces = (
|
|||
},
|
||||
})
|
||||
|
||||
export const setTimeRange = timeRange => ({
|
||||
type: ActionTypes.SetTimeRange,
|
||||
payload: {
|
||||
timeRange,
|
||||
},
|
||||
})
|
||||
|
||||
export const setTimeRangeAsync = (timeRange: TimeRange) => async (
|
||||
dispatch
|
||||
): Promise<void> => {
|
||||
dispatch(setTimeRange(timeRange))
|
||||
export const setTimeRangeAsync = () => async (dispatch): Promise<void> => {
|
||||
dispatch(setHistogramQueryConfigAsync())
|
||||
dispatch(setTableQueryConfigAsync())
|
||||
}
|
||||
|
@ -693,19 +723,6 @@ export const getSourceAndPopulateNamespacesAsync = (sourceID: string) => async (
|
|||
}
|
||||
}
|
||||
|
||||
export const changeZoomAsync = (timeRange: TimeRange) => async (
|
||||
dispatch,
|
||||
getState: GetState
|
||||
): Promise<void> => {
|
||||
const state = getState()
|
||||
const namespace = getNamespace(state)
|
||||
const proxyLink = getProxyLink(state)
|
||||
|
||||
if (namespace && proxyLink) {
|
||||
await dispatch(setTimeRangeAsync(timeRange))
|
||||
}
|
||||
}
|
||||
|
||||
export const getLogConfigAsync = (url: string) => async (
|
||||
dispatch: Dispatch<SetConfigsAction>
|
||||
): Promise<void> => {
|
||||
|
|
|
@ -6,9 +6,10 @@ import classnames from 'classnames'
|
|||
import Dropdown from 'src/shared/components/Dropdown'
|
||||
import PageHeader from 'src/reusable_ui/components/page_layout/PageHeader'
|
||||
import PageHeaderTitle from 'src/reusable_ui/components/page_layout/PageHeaderTitle'
|
||||
import TimeRangeDropdown from 'src/logs/components/TimeRangeDropdown'
|
||||
import TimeMarkerDropdown from 'src/logs/components/TimeMarkerDropdown'
|
||||
import TimeWindowDropdown from 'src/logs/components/TimeWindowDropdown'
|
||||
import Authorized, {EDITOR_ROLE} from 'src/auth/Authorized'
|
||||
import {TimeRange} from 'src/types'
|
||||
import {TimeRange, TimeWindow, TimeMarker} from 'src/types/logs'
|
||||
|
||||
interface SourceItem {
|
||||
id: string
|
||||
|
@ -20,13 +21,14 @@ interface Props {
|
|||
availableSources: Source[]
|
||||
currentSource: Source | null
|
||||
currentNamespaces: Namespace[]
|
||||
timeRange: TimeRange
|
||||
liveUpdating: boolean
|
||||
onChooseSource: (sourceID: string) => void
|
||||
onChooseNamespace: (namespace: Namespace) => void
|
||||
onChooseTimerange: (timeRange: TimeRange) => void
|
||||
onChangeLiveUpdatingStatus: () => void
|
||||
onShowOptionsOverlay: () => void
|
||||
timeRange: TimeRange
|
||||
onSetTimeMarker: (timeMarker: TimeMarker) => void
|
||||
onSetTimeWindow: (timeWindow: TimeWindow) => void
|
||||
}
|
||||
|
||||
class LogViewerHeader extends PureComponent<Props> {
|
||||
|
@ -50,7 +52,16 @@ class LogViewerHeader extends PureComponent<Props> {
|
|||
}
|
||||
|
||||
private get optionsComponents(): JSX.Element {
|
||||
const {timeRange, onShowOptionsOverlay} = this.props
|
||||
const {onShowOptionsOverlay, onSetTimeWindow, onSetTimeMarker} = this.props
|
||||
|
||||
// Todo: Replace w/ getDeep
|
||||
const timeRange = _.get(this.props, 'timeRange', {
|
||||
upper: null,
|
||||
lower: 'now() - 1m',
|
||||
seconds: 60,
|
||||
windowOption: '1m',
|
||||
timeOption: 'now',
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -67,9 +78,13 @@ class LogViewerHeader extends PureComponent<Props> {
|
|||
selected={this.selectedNamespace}
|
||||
onChoose={this.handleChooseNamespace}
|
||||
/>
|
||||
<TimeRangeDropdown
|
||||
onChooseTimeRange={this.handleChooseTimeRange}
|
||||
selected={timeRange}
|
||||
<TimeMarkerDropdown
|
||||
onSetTimeMarker={onSetTimeMarker}
|
||||
selectedTimeMarker={timeRange.timeOption}
|
||||
/>
|
||||
<TimeWindowDropdown
|
||||
selectedTimeWindow={timeRange}
|
||||
onSetTimeWindow={onSetTimeWindow}
|
||||
/>
|
||||
<Authorized requiredRole={EDITOR_ROLE}>
|
||||
<button
|
||||
|
@ -104,10 +119,6 @@ class LogViewerHeader extends PureComponent<Props> {
|
|||
)
|
||||
}
|
||||
|
||||
private handleChooseTimeRange = (timerange: TimeRange) => {
|
||||
this.props.onChooseTimerange(timerange)
|
||||
}
|
||||
|
||||
private handleChooseSource = (item: SourceItem) => {
|
||||
this.props.onChooseSource(item.id)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
import React, {Component} from 'react'
|
||||
import Dropdown from 'src/shared/components/Dropdown'
|
||||
import {TimeMarker} from 'src/types/logs'
|
||||
|
||||
interface Props {
|
||||
onSetTimeMarker: (TimeMarker: TimeMarker) => void
|
||||
selectedTimeMarker: string
|
||||
}
|
||||
|
||||
class TimeMarkerDropdown extends Component<Props> {
|
||||
public render() {
|
||||
const {selectedTimeMarker} = this.props
|
||||
const items = [{text: 'now'}, {text: '2018-07-10T22:22:21.769Z'}]
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
onChoose={this.handleChoose}
|
||||
buttonSize="btn-sm"
|
||||
buttonColor="btn-default"
|
||||
selected={selectedTimeMarker}
|
||||
items={items}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
private handleChoose = dropdownItem => {
|
||||
const {onSetTimeMarker} = this.props
|
||||
|
||||
onSetTimeMarker({timeOption: dropdownItem.text})
|
||||
}
|
||||
}
|
||||
|
||||
export default TimeMarkerDropdown
|
|
@ -1,173 +0,0 @@
|
|||
import React, {Component} from 'react'
|
||||
import classnames from 'classnames'
|
||||
import moment from 'moment'
|
||||
import _ from 'lodash'
|
||||
|
||||
import FancyScrollbar from 'src/shared/components/FancyScrollbar'
|
||||
import timeRanges from 'src/logs/data/timeRanges'
|
||||
import {DROPDOWN_MENU_MAX_HEIGHT} from 'src/shared/constants/index'
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
import {ClickOutside} from 'src/shared/components/ClickOutside'
|
||||
import CustomTimeRange from 'src/shared/components/CustomTimeRange'
|
||||
|
||||
import {TimeRange} from 'src/types'
|
||||
|
||||
const dateFormat = 'YYYY-MM-DD HH:mm'
|
||||
const emptyTime = {lower: '', upper: ''}
|
||||
const format = t => moment(t.replace(/\'/g, '')).format(dateFormat)
|
||||
|
||||
interface Props {
|
||||
selected: {
|
||||
lower: string
|
||||
upper?: string
|
||||
}
|
||||
|
||||
onChooseTimeRange: (timeRange: TimeRange) => void
|
||||
}
|
||||
|
||||
interface State {
|
||||
autobind: boolean
|
||||
isOpen: boolean
|
||||
isCustomTimeRangeOpen: boolean
|
||||
customTimeRange: TimeRange
|
||||
}
|
||||
|
||||
@ErrorHandling
|
||||
class TimeRangeDropdown extends Component<Props, State> {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
const {lower, upper} = props.selected
|
||||
|
||||
const isTimeValid = moment(upper).isValid() && moment(lower).isValid()
|
||||
const customTimeRange = isTimeValid ? {lower, upper} : emptyTime
|
||||
|
||||
this.state = {
|
||||
autobind: false,
|
||||
isOpen: false,
|
||||
isCustomTimeRangeOpen: false,
|
||||
customTimeRange,
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
const {selected} = this.props
|
||||
const {customTimeRange, isCustomTimeRangeOpen} = this.state
|
||||
|
||||
return (
|
||||
<ClickOutside onClickOutside={this.handleClickOutside}>
|
||||
<div className="time-range-dropdown">
|
||||
<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.findTimeRangeInputValue(selected)}
|
||||
</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={
|
||||
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}>
|
||||
<a href="#" onClick={this.handleSelection(item)}>
|
||||
{item.menuOption}
|
||||
</a>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</FancyScrollbar>
|
||||
</ul>
|
||||
</div>
|
||||
{isCustomTimeRangeOpen ? (
|
||||
<ClickOutside onClickOutside={this.handleCloseCustomTimeRange}>
|
||||
<div className="custom-time--overlay">
|
||||
<CustomTimeRange
|
||||
onApplyTimeRange={this.handleApplyCustomTimeRange}
|
||||
timeRange={customTimeRange}
|
||||
onClose={this.handleCloseCustomTimeRange}
|
||||
isVisible={isCustomTimeRangeOpen}
|
||||
timeInterval={300}
|
||||
page="default"
|
||||
/>
|
||||
</div>
|
||||
</ClickOutside>
|
||||
) : null}
|
||||
</div>
|
||||
</ClickOutside>
|
||||
)
|
||||
}
|
||||
|
||||
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', {
|
||||
'dropdown-290': absoluteTimeRange,
|
||||
'dropdown-120': !absoluteTimeRange,
|
||||
open: isOpen,
|
||||
})
|
||||
}
|
||||
|
||||
private findTimeRangeInputValue = ({upper, lower}: TimeRange) => {
|
||||
if (upper && lower) {
|
||||
if (upper === 'now()') {
|
||||
return `${format(lower)} - Now`
|
||||
}
|
||||
|
||||
return `${format(lower)} - ${format(upper)}`
|
||||
}
|
||||
|
||||
const selected = timeRanges.find(range => range.lower === lower)
|
||||
return selected ? selected.inputValue : 'Custom'
|
||||
}
|
||||
|
||||
private handleClickOutside = () => {
|
||||
this.setState({isOpen: false})
|
||||
}
|
||||
|
||||
private handleSelection = timeRange => () => {
|
||||
this.props.onChooseTimeRange(timeRange)
|
||||
this.setState({customTimeRange: emptyTime, isOpen: false})
|
||||
}
|
||||
|
||||
private toggleMenu = () => {
|
||||
this.setState({isOpen: !this.state.isOpen})
|
||||
}
|
||||
|
||||
private showCustomTimeRange = () => {
|
||||
this.setState({isCustomTimeRangeOpen: true})
|
||||
}
|
||||
|
||||
private handleApplyCustomTimeRange = customTimeRange => {
|
||||
this.props.onChooseTimeRange({...customTimeRange})
|
||||
this.setState({customTimeRange, isOpen: false})
|
||||
}
|
||||
|
||||
private handleCloseCustomTimeRange = () => {
|
||||
this.setState({isCustomTimeRangeOpen: false})
|
||||
}
|
||||
}
|
||||
export default TimeRangeDropdown
|
|
@ -0,0 +1,45 @@
|
|||
import React, {Component} from 'react'
|
||||
import Dropdown from 'src/shared/components/Dropdown'
|
||||
import {TIME_RANGE_VALUES} from 'src/logs/constants'
|
||||
import {TimeWindow, TimeRange} from 'src/types/logs'
|
||||
|
||||
interface Props {
|
||||
onSetTimeWindow: (timeWindow: TimeWindow) => void
|
||||
selectedTimeWindow: TimeRange
|
||||
}
|
||||
|
||||
class TimeWindowDropdown extends Component<Props> {
|
||||
public render() {
|
||||
return (
|
||||
<Dropdown
|
||||
className="dropdown-90"
|
||||
selected={this.selected}
|
||||
onChoose={this.handleChoose}
|
||||
buttonSize="btn-sm"
|
||||
buttonColor="btn-default"
|
||||
items={TIME_RANGE_VALUES}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
private get selected(): string {
|
||||
const {
|
||||
selectedTimeWindow: {timeOption, windowOption},
|
||||
} = this.props
|
||||
|
||||
if (timeOption === 'now') {
|
||||
return `- ${windowOption}`
|
||||
}
|
||||
|
||||
return `+/- ${windowOption}`
|
||||
}
|
||||
|
||||
private handleChoose = (dropdownItem): void => {
|
||||
const {onSetTimeWindow} = this.props
|
||||
const {text, seconds} = dropdownItem
|
||||
|
||||
onSetTimeWindow({seconds, windowOption: text})
|
||||
}
|
||||
}
|
||||
|
||||
export default TimeWindowDropdown
|
|
@ -171,3 +171,12 @@ export enum EncodingVisibilityOptions {
|
|||
visible = 'visible',
|
||||
hidden = 'hidden',
|
||||
}
|
||||
|
||||
export const TIME_RANGE_VALUES = [
|
||||
{text: '1m', seconds: 60},
|
||||
{text: '5m', seconds: 300},
|
||||
{text: '10m', seconds: 600},
|
||||
{text: '15m', seconds: 900},
|
||||
]
|
||||
|
||||
export const SECONDS_TO_MS = 1000
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import React, {Component} from 'react'
|
||||
import uuid from 'uuid'
|
||||
import _ from 'lodash'
|
||||
import moment from 'moment'
|
||||
import {connect} from 'react-redux'
|
||||
import {AutoSizer} from 'react-virtualized'
|
||||
|
||||
|
@ -9,9 +10,11 @@ import {
|
|||
setTableRelativeTimeAsync,
|
||||
getSourceAndPopulateNamespacesAsync,
|
||||
setTimeRangeAsync,
|
||||
setTimeBounds,
|
||||
setTimeWindow,
|
||||
setTimeMarker,
|
||||
setNamespaceAsync,
|
||||
executeQueriesAsync,
|
||||
changeZoomAsync,
|
||||
setSearchTermAsync,
|
||||
addFilter,
|
||||
removeFilter,
|
||||
|
@ -33,10 +36,10 @@ 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 {SeverityFormatOptions} from 'src/logs/constants'
|
||||
import {Source, Namespace, TimeRange} from 'src/types'
|
||||
import {SeverityFormatOptions, SECONDS_TO_MS} from 'src/logs/constants'
|
||||
import {Source, Namespace} from 'src/types'
|
||||
|
||||
import {HistogramData, TimePeriod, HistogramColor} from 'src/types/histogram'
|
||||
import {HistogramData, HistogramColor} from 'src/types/histogram'
|
||||
import {
|
||||
Filter,
|
||||
SeverityLevelColor,
|
||||
|
@ -44,6 +47,10 @@ import {
|
|||
LogsTableColumn,
|
||||
LogConfig,
|
||||
TableData,
|
||||
TimeRange,
|
||||
TimeWindow,
|
||||
TimeMarker,
|
||||
TimeBounds,
|
||||
} from 'src/types/logs'
|
||||
import {applyChangesToTableData} from 'src/logs/utils/table'
|
||||
|
||||
|
@ -55,8 +62,10 @@ interface Props {
|
|||
getSource: (sourceID: string) => void
|
||||
getSources: () => void
|
||||
setTimeRangeAsync: (timeRange: TimeRange) => void
|
||||
setTimeBounds: (timeBounds: TimeBounds) => void
|
||||
setTimeWindow: (timeWindow: TimeWindow) => void
|
||||
setTimeMarker: (timeMarker: TimeMarker) => void
|
||||
setNamespaceAsync: (namespace: Namespace) => void
|
||||
changeZoomAsync: (timeRange: TimeRange) => void
|
||||
executeQueriesAsync: () => void
|
||||
setSearchTermAsync: (searchTerm: string) => void
|
||||
setTableRelativeTime: (time: number) => void
|
||||
|
@ -316,8 +325,8 @@ class LogsPage extends Component<Props, State> {
|
|||
width={width}
|
||||
height={height}
|
||||
colorScale={colorForSeverity}
|
||||
onZoom={this.handleChartZoom}
|
||||
colors={histogramColors}
|
||||
onBarClick={this.handleBarClick}
|
||||
/>
|
||||
)}
|
||||
</AutoSizer>
|
||||
|
@ -337,12 +346,13 @@ class LogsPage extends Component<Props, State> {
|
|||
|
||||
return (
|
||||
<LogViewerHeader
|
||||
timeRange={timeRange}
|
||||
onSetTimeMarker={this.handleSetTimeMarker}
|
||||
onSetTimeWindow={this.handleSetTimeWindow}
|
||||
liveUpdating={liveUpdating && !this.isSpecificTimeRange}
|
||||
availableSources={sources}
|
||||
timeRange={timeRange}
|
||||
onChooseSource={this.handleChooseSource}
|
||||
onChooseNamespace={this.handleChooseNamespace}
|
||||
onChooseTimerange={this.handleChooseTimerange}
|
||||
currentSource={currentSource}
|
||||
currentNamespaces={currentNamespaces}
|
||||
currentNamespace={currentNamespace}
|
||||
|
@ -387,11 +397,45 @@ class LogsPage extends Component<Props, State> {
|
|||
this.props.executeQueriesAsync()
|
||||
}
|
||||
|
||||
private handleChooseTimerange = (timeRange: TimeRange) => {
|
||||
this.props.setTimeRangeAsync(timeRange)
|
||||
private handleBarClick = (time: string): void => {
|
||||
const timeOption = moment(time).toISOString()
|
||||
|
||||
this.handleSetTimeMarker({timeOption})
|
||||
}
|
||||
|
||||
private handleSetTimeBounds = async () => {
|
||||
const {seconds, windowOption, timeOption} = this.props.timeRange
|
||||
let lower = `now() - ${windowOption}`
|
||||
let upper = null
|
||||
|
||||
if (timeOption !== 'now') {
|
||||
const numberTimeOption = new Date(timeOption).valueOf()
|
||||
const milliseconds = seconds * SECONDS_TO_MS
|
||||
lower = moment(numberTimeOption - milliseconds).toISOString()
|
||||
upper = moment(numberTimeOption + milliseconds).toISOString()
|
||||
}
|
||||
|
||||
const timeBounds: TimeBounds = {
|
||||
lower,
|
||||
upper,
|
||||
}
|
||||
|
||||
await this.props.setTimeBounds(timeBounds)
|
||||
|
||||
this.props.setTimeRangeAsync(this.props.timeRange)
|
||||
this.fetchNewDataset()
|
||||
}
|
||||
|
||||
private handleSetTimeWindow = async (timeWindow: TimeWindow) => {
|
||||
await this.props.setTimeWindow(timeWindow)
|
||||
this.handleSetTimeBounds()
|
||||
}
|
||||
|
||||
private handleSetTimeMarker = async (timeMarker: TimeMarker) => {
|
||||
await this.props.setTimeMarker(timeMarker)
|
||||
this.handleSetTimeBounds()
|
||||
}
|
||||
|
||||
private handleChooseSource = (sourceID: string) => {
|
||||
this.props.getSource(sourceID)
|
||||
}
|
||||
|
@ -400,17 +444,6 @@ class LogsPage extends Component<Props, State> {
|
|||
this.props.setNamespaceAsync(namespace)
|
||||
}
|
||||
|
||||
private handleChartZoom = (t: TimePeriod) => {
|
||||
const {start, end} = t
|
||||
const timeRange = {
|
||||
lower: new Date(start).toISOString(),
|
||||
upper: new Date(end).toISOString(),
|
||||
}
|
||||
|
||||
this.props.changeZoomAsync(timeRange)
|
||||
this.setState({liveUpdating: true})
|
||||
}
|
||||
|
||||
private fetchNewDataset() {
|
||||
this.setState({liveUpdating: true})
|
||||
this.props.executeQueriesAsync()
|
||||
|
@ -521,9 +554,11 @@ const mapDispatchToProps = {
|
|||
getSource: getSourceAndPopulateNamespacesAsync,
|
||||
getSources: getSourcesAsync,
|
||||
setTimeRangeAsync,
|
||||
setTimeBounds,
|
||||
setTimeWindow,
|
||||
setTimeMarker,
|
||||
setNamespaceAsync,
|
||||
executeQueriesAsync,
|
||||
changeZoomAsync,
|
||||
setSearchTermAsync,
|
||||
addFilter,
|
||||
removeFilter,
|
||||
|
|
|
@ -30,10 +30,16 @@ const defaultTableData: TableData = {
|
|||
values: [],
|
||||
}
|
||||
|
||||
const defaultState: LogsState = {
|
||||
export const defaultState: LogsState = {
|
||||
currentSource: null,
|
||||
currentNamespaces: [],
|
||||
timeRange: {lower: 'now() - 1m', upper: null},
|
||||
timeRange: {
|
||||
upper: null,
|
||||
lower: 'now() - 1m',
|
||||
seconds: 60,
|
||||
windowOption: '1m',
|
||||
timeOption: 'now',
|
||||
},
|
||||
currentNamespace: null,
|
||||
histogramQueryConfig: null,
|
||||
tableQueryConfig: null,
|
||||
|
@ -176,8 +182,15 @@ export default (state: LogsState = defaultState, action: Action) => {
|
|||
return {...state, currentSource: action.payload.source}
|
||||
case ActionTypes.SetNamespaces:
|
||||
return {...state, currentNamespaces: action.payload.namespaces}
|
||||
case ActionTypes.SetTimeRange:
|
||||
return {...state, timeRange: action.payload.timeRange}
|
||||
case ActionTypes.SetTimeBounds:
|
||||
const {upper, lower} = action.payload.timeBounds
|
||||
return {...state, timeRange: {...state.timeRange, upper, lower}}
|
||||
case ActionTypes.SetTimeWindow:
|
||||
const {windowOption, seconds} = action.payload.timeWindow
|
||||
return {...state, timeRange: {...state.timeRange, windowOption, seconds}}
|
||||
case ActionTypes.SetTimeMarker:
|
||||
const {timeOption} = action.payload.timeMarker
|
||||
return {...state, timeRange: {...state.timeRange, timeOption}}
|
||||
case ActionTypes.SetNamespace:
|
||||
return {...state, currentNamespace: action.payload.namespace}
|
||||
case ActionTypes.SetHistogramQueryConfig:
|
||||
|
@ -206,8 +219,6 @@ export default (state: LogsState = defaultState, action: Action) => {
|
|||
backward: action.payload.data,
|
||||
},
|
||||
}
|
||||
case ActionTypes.ChangeZoom:
|
||||
return {...state, timeRange: action.payload.timeRange}
|
||||
case ActionTypes.SetSearchTerm:
|
||||
const {searchTerm} = action.payload
|
||||
return {...state, searchTerm}
|
||||
|
|
|
@ -6,12 +6,10 @@ import HistogramChartAxes from 'src/shared/components/HistogramChartAxes'
|
|||
import HistogramChartBars from 'src/shared/components/HistogramChartBars'
|
||||
import HistogramChartTooltip from 'src/shared/components/HistogramChartTooltip'
|
||||
import HistogramChartSkeleton from 'src/shared/components/HistogramChartSkeleton'
|
||||
import XBrush from 'src/shared/components/XBrush'
|
||||
|
||||
import extentBy from 'src/utils/extentBy'
|
||||
|
||||
import {
|
||||
TimePeriod,
|
||||
HistogramData,
|
||||
Margins,
|
||||
HoverData,
|
||||
|
@ -33,7 +31,7 @@ interface Props {
|
|||
height: number
|
||||
colors: HistogramColor[]
|
||||
colorScale: ColorScale
|
||||
onZoom: (TimePeriod) => void
|
||||
onBarClick?: (time: string) => void
|
||||
}
|
||||
|
||||
interface State {
|
||||
|
@ -48,7 +46,7 @@ class HistogramChart extends PureComponent<Props, State> {
|
|||
}
|
||||
|
||||
public render() {
|
||||
const {width, height, data, colorScale, colors} = this.props
|
||||
const {width, height, data, colorScale, colors, onBarClick} = this.props
|
||||
const {margins} = this
|
||||
|
||||
if (width === 0 || height === 0) {
|
||||
|
@ -85,14 +83,6 @@ class HistogramChart extends PureComponent<Props, State> {
|
|||
yScale={yScale}
|
||||
/>
|
||||
</g>
|
||||
<g className="histogram-chart--brush" transform={bodyTransform}>
|
||||
<XBrush
|
||||
xScale={xScale}
|
||||
width={adjustedWidth}
|
||||
height={adjustedHeight}
|
||||
onBrush={this.handleBrush}
|
||||
/>
|
||||
</g>
|
||||
<g
|
||||
transform={bodyTransform}
|
||||
className="histogram-chart--bars"
|
||||
|
@ -108,6 +98,7 @@ class HistogramChart extends PureComponent<Props, State> {
|
|||
hoverData={hoverData}
|
||||
onHover={this.handleHover}
|
||||
colors={colors}
|
||||
onBarClick={onBarClick}
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
|
@ -183,11 +174,6 @@ class HistogramChart extends PureComponent<Props, State> {
|
|||
return Math.max(...counts)
|
||||
}
|
||||
|
||||
private handleBrush = (t: TimePeriod): void => {
|
||||
this.props.onZoom(t)
|
||||
this.setState({hoverData: null})
|
||||
}
|
||||
|
||||
private handleHover = (hoverData: HoverData): void => {
|
||||
this.setState({hoverData})
|
||||
}
|
||||
|
|
|
@ -45,26 +45,6 @@ const getSortFn = (data: HistogramData): SortFn => {
|
|||
return (a, b) => counts[b.group] - counts[a.group]
|
||||
}
|
||||
|
||||
interface BarGroup {
|
||||
key: string
|
||||
clip: {
|
||||
x: number
|
||||
y: number
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
bars: Array<{
|
||||
key: string
|
||||
group: string
|
||||
x: number
|
||||
y: number
|
||||
width: number
|
||||
height: number
|
||||
fill: string
|
||||
}>
|
||||
data: HistogramData
|
||||
}
|
||||
|
||||
const getBarGroups = ({
|
||||
data,
|
||||
width,
|
||||
|
@ -133,6 +113,25 @@ const getBarGroups = ({
|
|||
})
|
||||
}
|
||||
|
||||
interface BarGroup {
|
||||
key: string
|
||||
clip: {
|
||||
x: number
|
||||
y: number
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
bars: Array<{
|
||||
key: string
|
||||
group: string
|
||||
x: number
|
||||
y: number
|
||||
width: number
|
||||
height: number
|
||||
fill: string
|
||||
}>
|
||||
data: HistogramData
|
||||
}
|
||||
interface Props {
|
||||
width: number
|
||||
height: number
|
||||
|
@ -143,6 +142,7 @@ interface Props {
|
|||
hoverData?: HoverData
|
||||
colors: HistogramColor[]
|
||||
onHover: (h: HoverData) => void
|
||||
onBarClick?: (time: string) => void
|
||||
}
|
||||
|
||||
interface State {
|
||||
|
@ -173,6 +173,7 @@ class HistogramChartBars extends PureComponent<Props, State> {
|
|||
data-key={key}
|
||||
onMouseOver={this.handleMouseOver}
|
||||
onMouseOut={this.handleMouseOut}
|
||||
onClick={this.handleBarClick(group.data)}
|
||||
>
|
||||
<defs>
|
||||
<clipPath id={`histogram-chart-bars--clip-${key}`}>
|
||||
|
@ -205,6 +206,15 @@ class HistogramChartBars extends PureComponent<Props, State> {
|
|||
})
|
||||
}
|
||||
|
||||
private handleBarClick = data => (): void => {
|
||||
const {onBarClick} = this.props
|
||||
|
||||
if (onBarClick) {
|
||||
const time = data[0].time
|
||||
onBarClick(time)
|
||||
}
|
||||
}
|
||||
|
||||
private handleMouseOver = (e: MouseEvent<SVGGElement>): void => {
|
||||
const groupKey = getDeep<string>(e, 'currentTarget.dataset.key', '')
|
||||
|
||||
|
|
|
@ -7,11 +7,6 @@ export interface HistogramDatum {
|
|||
group: string
|
||||
}
|
||||
|
||||
export interface TimePeriod {
|
||||
start: UnixTime
|
||||
end: UnixTime
|
||||
}
|
||||
|
||||
export type HistogramData = HistogramDatum[]
|
||||
|
||||
export type TooltipAnchor = 'left' | 'right'
|
||||
|
@ -23,6 +18,11 @@ export interface Margins {
|
|||
left: number
|
||||
}
|
||||
|
||||
export interface TimePeriod {
|
||||
start: UnixTime
|
||||
end: UnixTime
|
||||
}
|
||||
|
||||
export interface HoverData {
|
||||
data: HistogramData
|
||||
x: number
|
||||
|
|
|
@ -3,9 +3,10 @@ import {
|
|||
SeverityColorOptions,
|
||||
SeverityLevelOptions,
|
||||
} from 'src/logs/constants'
|
||||
import {QueryConfig, TimeRange, Namespace, Source} from 'src/types'
|
||||
import {QueryConfig, Namespace, Source} from 'src/types'
|
||||
import {FieldOption} from 'src/types/dashboards'
|
||||
import {TimeSeriesValue} from 'src/types/series'
|
||||
import {TimeRange} from 'src/types/logs'
|
||||
|
||||
export interface Filter {
|
||||
id: string
|
||||
|
@ -78,3 +79,47 @@ export interface ServerEncoding {
|
|||
value: string
|
||||
name?: string
|
||||
}
|
||||
|
||||
export interface TimeRange {
|
||||
upper?: string
|
||||
lower: string
|
||||
seconds?: number
|
||||
windowOption: string
|
||||
timeOption: string
|
||||
}
|
||||
|
||||
export interface TimeBounds {
|
||||
upper: string | null
|
||||
lower: string
|
||||
}
|
||||
|
||||
export interface TimeWindow {
|
||||
seconds: number
|
||||
windowOption: string
|
||||
}
|
||||
|
||||
export interface TimeMarker {
|
||||
timeOption: string
|
||||
}
|
||||
|
||||
export interface TimeRange {
|
||||
upper?: string
|
||||
lower: string
|
||||
seconds?: number
|
||||
windowOption: string
|
||||
timeOption: string
|
||||
}
|
||||
|
||||
export interface TimeBounds {
|
||||
upper: string | null
|
||||
lower: string
|
||||
}
|
||||
|
||||
export interface TimeWindow {
|
||||
seconds: number
|
||||
windowOption: string
|
||||
}
|
||||
|
||||
export interface TimeMarker {
|
||||
timeOption: string
|
||||
}
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
import reducer, {defaultState} from 'src/logs/reducers'
|
||||
import {setTimeWindow, setTimeMarker, setTimeBounds} from 'src/logs/actions'
|
||||
|
||||
describe('Logs.Reducers', () => {
|
||||
it('can set a time window', () => {
|
||||
const actionPayload = {
|
||||
windowOption: '10m',
|
||||
seconds: 600,
|
||||
}
|
||||
|
||||
const expected = {
|
||||
timeOption: 'now',
|
||||
windowOption: '10m',
|
||||
upper: null,
|
||||
lower: 'now() - 1m',
|
||||
seconds: 600,
|
||||
}
|
||||
|
||||
const actual = reducer(defaultState, setTimeWindow(actionPayload))
|
||||
expect(actual.timeRange).toEqual(expected)
|
||||
})
|
||||
|
||||
it('can set a time marker', () => {
|
||||
const actionPayload = {
|
||||
timeOption: '2018-07-10T22:22:21.769Z',
|
||||
}
|
||||
|
||||
const expected = {
|
||||
timeOption: '2018-07-10T22:22:21.769Z',
|
||||
windowOption: '1m',
|
||||
upper: null,
|
||||
lower: 'now() - 1m',
|
||||
seconds: 60,
|
||||
}
|
||||
|
||||
const actual = reducer(defaultState, setTimeMarker(actionPayload))
|
||||
expect(actual.timeRange).toEqual(expected)
|
||||
})
|
||||
|
||||
it('can set the time bounds', () => {
|
||||
const payload = {
|
||||
upper: '2018-07-10T22:20:21.769Z',
|
||||
lower: '2018-07-10T22:22:21.769Z',
|
||||
}
|
||||
|
||||
const expected = {
|
||||
timeOption: 'now',
|
||||
windowOption: '1m',
|
||||
upper: '2018-07-10T22:20:21.769Z',
|
||||
lower: '2018-07-10T22:22:21.769Z',
|
||||
seconds: 60,
|
||||
}
|
||||
|
||||
const actual = reducer(defaultState, setTimeBounds(payload))
|
||||
expect(actual.timeRange).toEqual(expected)
|
||||
})
|
||||
})
|
|
@ -309,25 +309,6 @@ exports[`HistogramChart displays the visualization with bars if nonempty data is
|
|||
</text>
|
||||
</HistogramChartAxes>
|
||||
</g>
|
||||
<g
|
||||
className="histogram-chart--brush"
|
||||
transform="translate(25, 5)"
|
||||
>
|
||||
<XBrush
|
||||
height={375}
|
||||
onBrush={[Function]}
|
||||
width={575}
|
||||
xScale={[Function]}
|
||||
>
|
||||
<rect
|
||||
className="x-brush--area"
|
||||
height={375}
|
||||
onDoubleClick={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
width={575}
|
||||
/>
|
||||
</XBrush>
|
||||
</g>
|
||||
<g
|
||||
className="histogram-chart--bars"
|
||||
clipPath="url(#histogram-chart--bars-clip)"
|
||||
|
@ -368,6 +349,7 @@ exports[`HistogramChart displays the visualization with bars if nonempty data is
|
|||
className="histogram-chart-bars--bars"
|
||||
data-key="1-1-193.5"
|
||||
key="1-1-193.5"
|
||||
onClick={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
>
|
||||
|
@ -402,6 +384,7 @@ exports[`HistogramChart displays the visualization with bars if nonempty data is
|
|||
className="histogram-chart-bars--bars"
|
||||
data-key="2-2-481"
|
||||
key="2-2-481"
|
||||
onClick={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
>
|
||||
|
|
Loading…
Reference in New Issue