Merge pull request #3897 from influxdata/logs-histogram-window

Improve Logs Histogram Time Controls
pull/10616/head
Alex Paxton 2018-07-12 13:57:42 -07:00 committed by GitHub
commit 2742c93bdb
14 changed files with 386 additions and 317 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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', '')

View File

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

View File

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

View File

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

View File

@ -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]}
>