Show stacked histogram in log viewer
parent
2ce5c5a4ec
commit
a1b6b507e5
|
@ -37,6 +37,7 @@
|
|||
"@types/chai": "^4.1.2",
|
||||
"@types/chroma-js": "^1.3.4",
|
||||
"@types/codemirror": "^0.0.56",
|
||||
"@types/d3-scale": "^2.0.1",
|
||||
"@types/dygraphs": "^1.1.6",
|
||||
"@types/enzyme": "^3.1.9",
|
||||
"@types/jest": "^22.1.4",
|
||||
|
@ -127,6 +128,7 @@
|
|||
"chroma-js": "^1.3.6",
|
||||
"classnames": "^2.2.3",
|
||||
"codemirror": "^5.36.0",
|
||||
"d3-scale": "^2.1.0",
|
||||
"dygraphs": "2.1.0",
|
||||
"enzyme-adapter-react-16": "^1.1.1",
|
||||
"eslint-plugin-babel": "^4.1.2",
|
||||
|
|
|
@ -1,15 +1,21 @@
|
|||
import moment from 'moment'
|
||||
import _ from 'lodash'
|
||||
import {Source, Namespace, TimeRange, QueryConfig} from 'src/types'
|
||||
import {
|
||||
Source,
|
||||
Namespace,
|
||||
TimeRange,
|
||||
QueryConfig,
|
||||
RemoteDataState,
|
||||
} from 'src/types'
|
||||
import {getSource} from 'src/shared/apis'
|
||||
import {getDatabasesWithRetentionPolicies} from 'src/shared/apis/databases'
|
||||
import {
|
||||
buildHistogramQueryConfig,
|
||||
buildTableQueryConfig,
|
||||
buildLogQuery,
|
||||
parseHistogramQueryResponse,
|
||||
} from 'src/logs/utils'
|
||||
import {getDeep} from 'src/utils/wrappers'
|
||||
import buildQuery from 'src/utils/influxql'
|
||||
import {executeQueryAsync} from 'src/logs/api'
|
||||
import {LogsState, Filter, TableData} from 'src/types/logs'
|
||||
|
||||
|
@ -41,6 +47,7 @@ export enum ActionTypes {
|
|||
SetNamespace = 'LOGS_SET_NAMESPACE',
|
||||
SetHistogramQueryConfig = 'LOGS_SET_HISTOGRAM_QUERY_CONFIG',
|
||||
SetHistogramData = 'LOGS_SET_HISTOGRAM_DATA',
|
||||
SetHistogramDataStatus = 'LOGS_SET_HISTOGRAM_DATA_STATUS',
|
||||
SetTableQueryConfig = 'LOGS_SET_TABLE_QUERY_CONFIG',
|
||||
SetTableData = 'LOGS_SET_TABLE_DATA',
|
||||
ChangeZoom = 'LOGS_CHANGE_ZOOM',
|
||||
|
@ -132,6 +139,11 @@ interface SetHistogramData {
|
|||
}
|
||||
}
|
||||
|
||||
interface SetHistogramDataStatus {
|
||||
type: ActionTypes.SetHistogramDataStatus
|
||||
payload: RemoteDataState
|
||||
}
|
||||
|
||||
interface SetTableQueryConfig {
|
||||
type: ActionTypes.SetTableQueryConfig
|
||||
payload: {
|
||||
|
@ -156,7 +168,6 @@ interface SetSearchTerm {
|
|||
interface ChangeZoomAction {
|
||||
type: ActionTypes.ChangeZoom
|
||||
payload: {
|
||||
data: object[]
|
||||
timeRange: TimeRange
|
||||
}
|
||||
}
|
||||
|
@ -168,6 +179,7 @@ export type Action =
|
|||
| SetNamespaceAction
|
||||
| SetHistogramQueryConfig
|
||||
| SetHistogramData
|
||||
| SetHistogramDataStatus
|
||||
| ChangeZoomAction
|
||||
| SetTableData
|
||||
| SetTableQueryConfig
|
||||
|
@ -220,9 +232,16 @@ export const removeFilter = (id: string): RemoveFilterAction => ({
|
|||
payload: {id},
|
||||
})
|
||||
|
||||
const setHistogramData = (response): SetHistogramData => ({
|
||||
const setHistogramData = (data): SetHistogramData => ({
|
||||
type: ActionTypes.SetHistogramData,
|
||||
payload: {data: [{response}]},
|
||||
payload: {data},
|
||||
})
|
||||
|
||||
const setHistogramDataStatus = (
|
||||
status: RemoteDataState
|
||||
): SetHistogramDataStatus => ({
|
||||
type: ActionTypes.SetHistogramDataStatus,
|
||||
payload: status,
|
||||
})
|
||||
|
||||
export const executeHistogramQueryAsync = () => async (
|
||||
|
@ -240,9 +259,18 @@ export const executeHistogramQueryAsync = () => async (
|
|||
|
||||
if (_.every([queryConfig, timeRange, namespace, proxyLink])) {
|
||||
const query = buildLogQuery(timeRange, queryConfig, filters, searchTerm)
|
||||
const response = await executeQueryAsync(proxyLink, namespace, query)
|
||||
|
||||
dispatch(setHistogramData(response))
|
||||
try {
|
||||
dispatch(setHistogramDataStatus(RemoteDataState.Loading))
|
||||
|
||||
const response = await executeQueryAsync(proxyLink, namespace, query)
|
||||
const data = parseHistogramQueryResponse(response)
|
||||
|
||||
dispatch(setHistogramData(data))
|
||||
dispatch(setHistogramDataStatus(RemoteDataState.Done))
|
||||
} catch {
|
||||
dispatch(setHistogramDataStatus(RemoteDataState.Error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -465,23 +493,10 @@ export const changeZoomAsync = (timeRange: TimeRange) => async (
|
|||
getState: GetState
|
||||
): Promise<void> => {
|
||||
const state = getState()
|
||||
|
||||
const namespace = getNamespace(state)
|
||||
const proxyLink = getProxyLink(state)
|
||||
|
||||
if (namespace && proxyLink) {
|
||||
const queryConfig = buildHistogramQueryConfig(namespace, timeRange)
|
||||
const query = buildQuery(timeRange, queryConfig)
|
||||
const response = await executeQueryAsync(proxyLink, namespace, query)
|
||||
|
||||
dispatch({
|
||||
type: ActionTypes.ChangeZoom,
|
||||
payload: {
|
||||
data: [{response}],
|
||||
timeRange,
|
||||
},
|
||||
})
|
||||
|
||||
await dispatch(setTimeRangeAsync(timeRange))
|
||||
await dispatch(executeTableQueryAsync())
|
||||
}
|
||||
|
|
|
@ -1,33 +0,0 @@
|
|||
import React, {PureComponent} from 'react'
|
||||
import LineGraph from 'src/shared/components/LineGraph'
|
||||
import {DEFAULT_LINE_COLORS} from 'src/shared/constants/graphColorPalettes'
|
||||
|
||||
import {TimeRange} from 'src/types'
|
||||
|
||||
interface Props {
|
||||
onZoom: (timeRange: TimeRange) => void
|
||||
timeRange: TimeRange
|
||||
data: object[]
|
||||
}
|
||||
|
||||
class LogViewerChart extends PureComponent<Props> {
|
||||
public render() {
|
||||
const {timeRange, data, onZoom} = this.props
|
||||
return (
|
||||
<LineGraph
|
||||
onZoom={onZoom}
|
||||
queries={[]}
|
||||
data={data}
|
||||
displayOptions={{animatedZooms: false}}
|
||||
setResolution={this.setResolution}
|
||||
isBarGraph={true}
|
||||
timeRange={timeRange}
|
||||
colors={DEFAULT_LINE_COLORS}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
private setResolution = () => {}
|
||||
}
|
||||
|
||||
export default LogViewerChart
|
|
@ -1,6 +1,9 @@
|
|||
import React, {PureComponent} from 'react'
|
||||
import uuid from 'uuid'
|
||||
import _ from 'lodash'
|
||||
import {connect} from 'react-redux'
|
||||
import {AutoSizer} from 'react-virtualized'
|
||||
|
||||
import {
|
||||
getSourceAndPopulateNamespacesAsync,
|
||||
setTimeRangeAsync,
|
||||
|
@ -15,15 +18,16 @@ import {
|
|||
} from 'src/logs/actions'
|
||||
import {getSourcesAsync} from 'src/shared/actions/sources'
|
||||
import LogViewerHeader from 'src/logs/components/LogViewerHeader'
|
||||
import Graph from 'src/logs/components/LogsGraph'
|
||||
import HistogramChart from 'src/shared/components/HistogramChart'
|
||||
import LogsGraphContainer from 'src/logs/components/LogsGraphContainer'
|
||||
import SearchBar from 'src/logs/components/LogsSearchBar'
|
||||
import FilterBar from 'src/logs/components/LogsFilterBar'
|
||||
import LogViewerChart from 'src/logs/components/LogViewerChart'
|
||||
import LogsTable from 'src/logs/components/LogsTable'
|
||||
import {getDeep} from 'src/utils/wrappers'
|
||||
|
||||
import {Source, Namespace, TimeRange} from 'src/types'
|
||||
import {Source, Namespace, TimeRange, RemoteDataState} from 'src/types'
|
||||
import {Filter} from 'src/types/logs'
|
||||
import {HistogramData, TimePeriod} from 'src/types/histogram'
|
||||
|
||||
interface Props {
|
||||
sources: Source[]
|
||||
|
@ -42,7 +46,8 @@ interface Props {
|
|||
removeFilter: (id: string) => void
|
||||
changeFilter: (id: string, operator: string, value: string) => void
|
||||
timeRange: TimeRange
|
||||
histogramData: object[]
|
||||
histogramData: HistogramData
|
||||
histogramDataStatus: RemoteDataState
|
||||
tableData: {
|
||||
columns: string[]
|
||||
values: string[]
|
||||
|
@ -97,7 +102,7 @@ class LogsPage extends PureComponent<Props, State> {
|
|||
<div className="page">
|
||||
{this.header}
|
||||
<div className="page-contents logs-viewer">
|
||||
<Graph>{this.chart}</Graph>
|
||||
<LogsGraphContainer>{this.chart}</LogsGraphContainer>
|
||||
<SearchBar
|
||||
searchString={searchTerm}
|
||||
onSearch={this.handleSubmitSearch}
|
||||
|
@ -170,24 +175,24 @@ class LogsPage extends PureComponent<Props, State> {
|
|||
private get histogramTotal(): number {
|
||||
const {histogramData} = this.props
|
||||
|
||||
const values = getDeep<Array<[number, number]>>(
|
||||
histogramData,
|
||||
'0.response.results.0.series.0.values',
|
||||
[]
|
||||
)
|
||||
|
||||
return values.reduce((acc, v) => acc + v[1], 0)
|
||||
return _.sumBy(histogramData, 'value')
|
||||
}
|
||||
|
||||
private get chart(): JSX.Element {
|
||||
const {histogramData, timeRange} = this.props
|
||||
const {histogramData, histogramDataStatus} = this.props
|
||||
|
||||
return (
|
||||
<LogViewerChart
|
||||
timeRange={timeRange}
|
||||
data={histogramData}
|
||||
onZoom={this.handleChartZoom}
|
||||
/>
|
||||
<AutoSizer>
|
||||
{({width, height}) => (
|
||||
<HistogramChart
|
||||
data={histogramData}
|
||||
dataStatus={histogramDataStatus}
|
||||
width={width}
|
||||
height={height}
|
||||
onZoom={this.handleChartZoom}
|
||||
/>
|
||||
)}
|
||||
</AutoSizer>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -262,11 +267,15 @@ class LogsPage extends PureComponent<Props, State> {
|
|||
this.props.setNamespaceAsync(namespace)
|
||||
}
|
||||
|
||||
private handleChartZoom = (timeRange: TimeRange) => {
|
||||
if (timeRange.lower) {
|
||||
this.props.changeZoomAsync(timeRange)
|
||||
this.setState({liveUpdating: true})
|
||||
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() {
|
||||
|
@ -283,6 +292,7 @@ const mapStateToProps = ({
|
|||
timeRange,
|
||||
currentNamespace,
|
||||
histogramData,
|
||||
histogramDataStatus,
|
||||
tableData,
|
||||
searchTerm,
|
||||
filters,
|
||||
|
@ -295,6 +305,7 @@ const mapStateToProps = ({
|
|||
timeRange,
|
||||
currentNamespace,
|
||||
histogramData,
|
||||
histogramDataStatus,
|
||||
tableData,
|
||||
searchTerm,
|
||||
filters,
|
||||
|
|
|
@ -9,6 +9,8 @@ import {
|
|||
IncrementQueryCountAction,
|
||||
ConcatMoreLogsAction,
|
||||
} from 'src/logs/actions'
|
||||
|
||||
import {RemoteDataState} from 'src/types'
|
||||
import {LogsState} from 'src/types/logs'
|
||||
|
||||
const defaultState: LogsState = {
|
||||
|
@ -20,6 +22,7 @@ const defaultState: LogsState = {
|
|||
tableQueryConfig: null,
|
||||
tableData: {columns: [], values: []},
|
||||
histogramData: [],
|
||||
histogramDataStatus: RemoteDataState.NotStarted,
|
||||
searchTerm: '',
|
||||
filters: [],
|
||||
queryCount: 0,
|
||||
|
@ -108,13 +111,14 @@ export default (state: LogsState = defaultState, action: Action) => {
|
|||
return {...state, histogramQueryConfig: action.payload.queryConfig}
|
||||
case ActionTypes.SetHistogramData:
|
||||
return {...state, histogramData: action.payload.data}
|
||||
case ActionTypes.SetHistogramDataStatus:
|
||||
return {...state, histogramDataStatus: action.payload}
|
||||
case ActionTypes.SetTableQueryConfig:
|
||||
return {...state, tableQueryConfig: action.payload.queryConfig}
|
||||
case ActionTypes.SetTableData:
|
||||
return {...state, tableData: action.payload.data}
|
||||
case ActionTypes.ChangeZoom:
|
||||
const {timeRange, data} = action.payload
|
||||
return {...state, timeRange, histogramData: data}
|
||||
return {...state, timeRange: action.payload.timeRange}
|
||||
case ActionTypes.SetSearchTerm:
|
||||
const {searchTerm} = action.payload
|
||||
return {...state, searchTerm}
|
||||
|
|
|
@ -4,6 +4,7 @@ import uuid from 'uuid'
|
|||
import {Filter} from 'src/types/logs'
|
||||
import {TimeRange, Namespace, QueryConfig} from 'src/types'
|
||||
import {NULL_STRING} from 'src/shared/constants/queryFillOptions'
|
||||
import {getDeep} from 'src/utils/wrappers'
|
||||
import {
|
||||
quoteIfTimestamp,
|
||||
buildSelect,
|
||||
|
@ -12,6 +13,8 @@ import {
|
|||
buildFill,
|
||||
} from 'src/utils/influxql'
|
||||
|
||||
import {HistogramData} from 'src/types/histogram'
|
||||
|
||||
const BIN_COUNT = 30
|
||||
|
||||
const histogramFields = [
|
||||
|
@ -156,7 +159,7 @@ const computeSeconds = (range: TimeRange) => {
|
|||
const createGroupBy = (range: TimeRange) => {
|
||||
const seconds = computeSeconds(range)
|
||||
const time = `${Math.max(Math.floor(seconds / BIN_COUNT), 1)}s`
|
||||
const tags = []
|
||||
const tags = ['severity']
|
||||
|
||||
return {time, tags}
|
||||
}
|
||||
|
@ -197,3 +200,39 @@ export const buildTableQueryConfig = (
|
|||
fill: null,
|
||||
}
|
||||
}
|
||||
|
||||
export const parseHistogramQueryResponse = (
|
||||
response: object
|
||||
): HistogramData => {
|
||||
const series = getDeep<any[]>(response, 'results.0.series', [])
|
||||
const data = series.reduce((acc, current) => {
|
||||
const group = getDeep<string>(current, 'tags.severity', '')
|
||||
|
||||
if (!current.columns || !current.values) {
|
||||
return acc
|
||||
}
|
||||
|
||||
const timeColIndex = current.columns.findIndex(v => v === 'time')
|
||||
const countColIndex = current.columns.findIndex(v => v === 'count')
|
||||
|
||||
if (timeColIndex < 0 || countColIndex < 0) {
|
||||
return acc
|
||||
}
|
||||
|
||||
const vs = current.values.map(v => {
|
||||
const time = v[timeColIndex]
|
||||
const value = v[countColIndex]
|
||||
|
||||
return {
|
||||
key: `${group} ${value} ${time}`,
|
||||
time,
|
||||
value,
|
||||
group,
|
||||
}
|
||||
})
|
||||
|
||||
return [...acc, ...vs]
|
||||
}, [])
|
||||
|
||||
return data
|
||||
}
|
||||
|
|
|
@ -0,0 +1,245 @@
|
|||
import React, {PureComponent, MouseEvent} from 'react'
|
||||
import _ from 'lodash'
|
||||
import {scaleLinear, scaleTime, ScaleLinear, ScaleTime} from 'd3-scale'
|
||||
|
||||
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 {getDeep} from 'src/utils/wrappers'
|
||||
|
||||
import {RemoteDataState} from 'src/types'
|
||||
import {
|
||||
TimePeriod,
|
||||
HistogramData,
|
||||
HistogramDatum,
|
||||
Margins,
|
||||
TooltipAnchor,
|
||||
} from 'src/types/histogram'
|
||||
|
||||
const PADDING_TOP = 0.2
|
||||
const TOOLTIP_HORIZONTAL_MARGIN = 5
|
||||
const TOOLTIP_REFLECT_DIST = 100
|
||||
|
||||
// Rather than use these magical constants, we could also render a digit and
|
||||
// capture its measured width with as state before rendering anything else.
|
||||
// Doing so would be robust but overkill.
|
||||
const DIGIT_WIDTH = 7
|
||||
const PERIOD_DIGIT_WIDTH = 4
|
||||
|
||||
interface Props {
|
||||
data: HistogramData
|
||||
dataStatus: RemoteDataState
|
||||
width: number
|
||||
height: number
|
||||
onZoom: (TimePeriod) => void
|
||||
}
|
||||
|
||||
interface State {
|
||||
hoverX: number
|
||||
hoverY: number
|
||||
hoverDatum?: HistogramDatum
|
||||
hoverAnchor: TooltipAnchor
|
||||
}
|
||||
|
||||
class HistogramChart extends PureComponent<Props, State> {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.state = {hoverX: -1, hoverY: -1, hoverAnchor: 'left'}
|
||||
}
|
||||
|
||||
public render() {
|
||||
const {width, height, data} = this.props
|
||||
const {margins} = this
|
||||
|
||||
if (width === 0 || height === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (!data.length) {
|
||||
return (
|
||||
<HistogramChartSkeleton
|
||||
width={width}
|
||||
height={height}
|
||||
margins={margins}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const {hoverDatum, hoverX, hoverY, hoverAnchor} = this.state
|
||||
const {
|
||||
xScale,
|
||||
yScale,
|
||||
adjustedWidth,
|
||||
adjustedHeight,
|
||||
bodyTransform,
|
||||
loadingClass,
|
||||
} = this
|
||||
|
||||
return (
|
||||
<>
|
||||
<svg
|
||||
width={width}
|
||||
height={height}
|
||||
className={`histogram-chart ${loadingClass}`}
|
||||
onMouseOver={this.handleMouseMove}
|
||||
onMouseOut={this.handleMouseOut}
|
||||
>
|
||||
<defs>
|
||||
<clipPath id="histogram-chart--bars-clip">
|
||||
<rect x="0" y="0" width={adjustedWidth} height={adjustedHeight} />
|
||||
</clipPath>
|
||||
</defs>
|
||||
<g className="histogram-chart--axes">
|
||||
<HistogramChartAxes
|
||||
width={width}
|
||||
height={height}
|
||||
margins={margins}
|
||||
xScale={xScale}
|
||||
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"
|
||||
clipPath="url(#histogram-chart--bars-clip)"
|
||||
>
|
||||
<HistogramChartBars
|
||||
width={adjustedWidth}
|
||||
height={adjustedHeight}
|
||||
data={data}
|
||||
xScale={xScale}
|
||||
yScale={yScale}
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
<HistogramChartTooltip
|
||||
datum={hoverDatum}
|
||||
x={hoverX}
|
||||
y={hoverY}
|
||||
anchor={hoverAnchor}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
private get xScale(): ScaleTime<number, number> {
|
||||
const {adjustedWidth} = this
|
||||
const {data} = this.props
|
||||
|
||||
const [t0, t1] = extentBy(data, d => d.time)
|
||||
|
||||
return scaleTime()
|
||||
.domain([new Date(t0.time), new Date(t1.time)])
|
||||
.range([0, adjustedWidth])
|
||||
}
|
||||
|
||||
private get yScale(): ScaleLinear<number, number> {
|
||||
const {adjustedHeight, maxAggregateCount} = this
|
||||
|
||||
return scaleLinear()
|
||||
.domain([0, maxAggregateCount + PADDING_TOP * maxAggregateCount])
|
||||
.range([adjustedHeight, 0])
|
||||
}
|
||||
|
||||
private get adjustedWidth(): number {
|
||||
const {margins} = this
|
||||
|
||||
return this.props.width - margins.left - margins.right
|
||||
}
|
||||
|
||||
private get adjustedHeight(): number {
|
||||
const {margins} = this
|
||||
|
||||
return this.props.height - margins.top - margins.bottom
|
||||
}
|
||||
|
||||
private get bodyTransform(): string {
|
||||
const {margins} = this
|
||||
|
||||
return `translate(${margins.left}, ${margins.top})`
|
||||
}
|
||||
|
||||
private get margins(): Margins {
|
||||
const {maxAggregateCount} = this
|
||||
|
||||
const domainTop = maxAggregateCount + PADDING_TOP * maxAggregateCount
|
||||
const left = domainTop.toString().length * DIGIT_WIDTH + PERIOD_DIGIT_WIDTH
|
||||
|
||||
return {top: 5, right: 0, bottom: 20, left}
|
||||
}
|
||||
|
||||
private get maxAggregateCount(): number {
|
||||
const {data} = this.props
|
||||
|
||||
if (!data.length) {
|
||||
return 0
|
||||
}
|
||||
|
||||
const groups = _.groupBy(data, 'time')
|
||||
const counts = Object.values(groups).map(group =>
|
||||
group.reduce((sum, current) => sum + current.value, 0)
|
||||
)
|
||||
|
||||
return Math.max(...counts)
|
||||
}
|
||||
|
||||
private get loadingClass(): string {
|
||||
const {dataStatus} = this.props
|
||||
|
||||
return dataStatus === RemoteDataState.Loading ? 'loading' : ''
|
||||
}
|
||||
|
||||
private handleBrush = (t: TimePeriod): void => {
|
||||
this.props.onZoom(t)
|
||||
this.setState({hoverDatum: null})
|
||||
}
|
||||
|
||||
private handleMouseMove = (e: MouseEvent<SVGElement>): void => {
|
||||
const key = getDeep<string>(e, 'target.dataset.key', '')
|
||||
|
||||
if (!key) {
|
||||
return
|
||||
}
|
||||
|
||||
const {data} = this.props
|
||||
const hoverDatum = data.find(d => d.key === key)
|
||||
|
||||
if (!hoverDatum) {
|
||||
return
|
||||
}
|
||||
|
||||
const bar = e.target as SVGRectElement
|
||||
const barRect = bar.getBoundingClientRect()
|
||||
const barRectHeight = barRect.bottom - barRect.top
|
||||
const hoverY = barRect.top + barRectHeight / 2
|
||||
|
||||
let hoverX = barRect.right + TOOLTIP_HORIZONTAL_MARGIN
|
||||
let hoverAnchor: TooltipAnchor = 'left'
|
||||
|
||||
if (hoverX >= window.innerWidth - TOOLTIP_REFLECT_DIST) {
|
||||
hoverX = window.innerWidth - barRect.left + TOOLTIP_HORIZONTAL_MARGIN
|
||||
hoverAnchor = 'right'
|
||||
}
|
||||
|
||||
this.setState({hoverDatum, hoverX, hoverY, hoverAnchor})
|
||||
}
|
||||
|
||||
private handleMouseOut = (): void => {
|
||||
this.setState({hoverDatum: null})
|
||||
}
|
||||
}
|
||||
|
||||
export default HistogramChart
|
|
@ -0,0 +1,99 @@
|
|||
import React, {PureComponent} from 'react'
|
||||
import uuid from 'uuid'
|
||||
import {ScaleLinear, ScaleTime} from 'd3-scale'
|
||||
|
||||
import {Margins} from 'src/types/histogram'
|
||||
|
||||
const Y_TICK_COUNT = 5
|
||||
const Y_TICK_PADDING_RIGHT = 5
|
||||
const X_TICK_COUNT = 10
|
||||
const X_TICK_PADDING_TOP = 8
|
||||
|
||||
interface Props {
|
||||
width: number
|
||||
height: number
|
||||
margins: Margins
|
||||
xScale: ScaleTime<number, number>
|
||||
yScale: ScaleLinear<number, number>
|
||||
}
|
||||
|
||||
class HistogramChartAxes extends PureComponent<Props> {
|
||||
public render() {
|
||||
const {xTickData, yTickData} = this
|
||||
|
||||
return (
|
||||
<>
|
||||
{this.renderYTicks(yTickData)}
|
||||
{this.renderYLabels(yTickData)}
|
||||
{this.renderXLabels(xTickData)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
private renderYTicks(yTickData) {
|
||||
return yTickData.map(({x1, x2, y}) => (
|
||||
<line className="y-tick" key={uuid.v4()} x1={x1} x2={x2} y1={y} y2={y} />
|
||||
))
|
||||
}
|
||||
|
||||
private renderYLabels(yTickData) {
|
||||
return yTickData.map(({x1, y, label}) => (
|
||||
<text
|
||||
className="y-label"
|
||||
key={uuid.v4()}
|
||||
x={x1 - Y_TICK_PADDING_RIGHT}
|
||||
y={y}
|
||||
>
|
||||
{label}
|
||||
</text>
|
||||
))
|
||||
}
|
||||
|
||||
private renderXLabels(xTickData) {
|
||||
return xTickData.map(({x, y, label}) => (
|
||||
<text className="x-label" key={uuid.v4()} y={y} x={x}>
|
||||
{label}
|
||||
</text>
|
||||
))
|
||||
}
|
||||
|
||||
private get xTickData() {
|
||||
const {margins, xScale, width, height} = this.props
|
||||
|
||||
const y = height - margins.bottom + X_TICK_PADDING_TOP
|
||||
const formatTime = xScale.tickFormat()
|
||||
|
||||
return xScale
|
||||
.ticks(X_TICK_COUNT)
|
||||
.filter(val => {
|
||||
const x = xScale(val)
|
||||
|
||||
// Don't render labels that will be cut off
|
||||
return x > margins.left && x < width - margins.right
|
||||
})
|
||||
.map(val => {
|
||||
const x = xScale(val)
|
||||
|
||||
return {
|
||||
x,
|
||||
y,
|
||||
label: formatTime(val),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private get yTickData() {
|
||||
const {width, margins, yScale} = this.props
|
||||
|
||||
return yScale.ticks(Y_TICK_COUNT).map(val => {
|
||||
return {
|
||||
label: val,
|
||||
x1: margins.left,
|
||||
x2: margins.left + width,
|
||||
y: margins.top + yScale(val),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default HistogramChartAxes
|
|
@ -0,0 +1,131 @@
|
|||
import React, {PureComponent} from 'react'
|
||||
import uuid from 'uuid'
|
||||
import _ from 'lodash'
|
||||
import {ScaleLinear, ScaleTime} from 'd3-scale'
|
||||
|
||||
import {HistogramData, HistogramDatum} from 'src/types/histogram'
|
||||
|
||||
const BAR_BORDER_RADIUS = 4
|
||||
const BAR_PADDING_SIDES = 4
|
||||
|
||||
interface Props {
|
||||
width: number
|
||||
height: number
|
||||
data: HistogramData
|
||||
xScale: ScaleTime<number, number>
|
||||
yScale: ScaleLinear<number, number>
|
||||
}
|
||||
|
||||
class HistogramChartBars extends PureComponent<Props> {
|
||||
public render() {
|
||||
return this.renderData.map(group => {
|
||||
const {key, clip, bars} = group
|
||||
|
||||
return (
|
||||
<g key={key} className="histogram-chart-bars--bars">
|
||||
<defs>
|
||||
<clipPath id={`histogram-chart-bars--clip-${key}`}>
|
||||
<rect
|
||||
x={clip.x}
|
||||
y={clip.y}
|
||||
width={clip.width}
|
||||
height={clip.height}
|
||||
rx={BAR_BORDER_RADIUS}
|
||||
ry={BAR_BORDER_RADIUS}
|
||||
/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
{bars.map(d => (
|
||||
<rect
|
||||
key={d.key}
|
||||
className="histogram-chart-bars--bar"
|
||||
x={d.x}
|
||||
y={d.y}
|
||||
width={d.width}
|
||||
height={d.height}
|
||||
clipPath={`url(#histogram-chart-bars--clip-${key})`}
|
||||
data-group={d.group}
|
||||
data-key={d.key}
|
||||
/>
|
||||
))}
|
||||
</g>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
private get renderData() {
|
||||
const {data, xScale, yScale} = this.props
|
||||
const {barWidth, sortFn} = this
|
||||
|
||||
const visibleData = data.filter(d => d.value !== 0)
|
||||
const groups = Object.values(_.groupBy(visibleData, 'time'))
|
||||
|
||||
for (const group of groups) {
|
||||
group.sort(sortFn)
|
||||
}
|
||||
|
||||
return groups.map(group => {
|
||||
const x = xScale(group[0].time) - barWidth / 2
|
||||
const groupTotal = _.sumBy(group, 'value')
|
||||
|
||||
const renderData = {
|
||||
key: uuid.v4(),
|
||||
clip: {
|
||||
x,
|
||||
y: yScale(groupTotal),
|
||||
width: barWidth,
|
||||
height: yScale(0) - yScale(groupTotal) + BAR_BORDER_RADIUS,
|
||||
},
|
||||
bars: [],
|
||||
}
|
||||
|
||||
let offset = 0
|
||||
|
||||
group.forEach((d: HistogramDatum) => {
|
||||
const height = yScale(0) - yScale(d.value)
|
||||
|
||||
renderData.bars.push({
|
||||
key: d.key,
|
||||
group: d.group,
|
||||
x,
|
||||
y: yScale(d.value) - offset,
|
||||
width: barWidth,
|
||||
height,
|
||||
})
|
||||
|
||||
offset += height
|
||||
})
|
||||
|
||||
return renderData
|
||||
})
|
||||
}
|
||||
|
||||
private get sortFn() {
|
||||
const {data} = this.props
|
||||
|
||||
const counts = {}
|
||||
|
||||
for (const d of data) {
|
||||
if (counts[d.group]) {
|
||||
counts[d.group] += d.value
|
||||
} else {
|
||||
counts[d.group] = d.value
|
||||
}
|
||||
}
|
||||
|
||||
return (a, b) => counts[b.group] - counts[a.group]
|
||||
}
|
||||
|
||||
private get barWidth() {
|
||||
const {data, xScale, width} = this.props
|
||||
|
||||
const dataInView = data.filter(
|
||||
d => xScale(d.time) >= 0 && xScale(d.time) <= width
|
||||
)
|
||||
const barCount = Object.values(_.groupBy(dataInView, 'time')).length
|
||||
|
||||
return Math.round(width / barCount - BAR_PADDING_SIDES)
|
||||
}
|
||||
}
|
||||
|
||||
export default HistogramChartBars
|
|
@ -0,0 +1,37 @@
|
|||
import React, {SFC} from 'react'
|
||||
import _ from 'lodash'
|
||||
|
||||
import {Margins} from 'src/types/histogram'
|
||||
|
||||
const NUM_TICKS = 5
|
||||
|
||||
interface Props {
|
||||
width: number
|
||||
height: number
|
||||
margins: Margins
|
||||
}
|
||||
|
||||
const HistogramChartSkeleton: SFC<Props> = props => {
|
||||
const {margins, width, height} = props
|
||||
|
||||
const spacing = (height - margins.top - margins.bottom) / NUM_TICKS
|
||||
const y1 = height - margins.bottom
|
||||
const tickYs = _.range(0, NUM_TICKS).map(i => y1 - i * spacing)
|
||||
|
||||
return (
|
||||
<svg className="histogram-chart-skeleton" width={width} height={height}>
|
||||
{tickYs.map((y, i) => (
|
||||
<line
|
||||
key={i}
|
||||
className="y-tick"
|
||||
x1={margins.left}
|
||||
x2={width - margins.right}
|
||||
y1={y}
|
||||
y2={y}
|
||||
/>
|
||||
))}
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export default HistogramChartSkeleton
|
|
@ -0,0 +1,42 @@
|
|||
import React, {SFC, CSSProperties} from 'react'
|
||||
|
||||
import {HistogramDatum, TooltipAnchor} from 'src/types/histogram'
|
||||
|
||||
interface Props {
|
||||
datum: HistogramDatum
|
||||
x: number
|
||||
y: number
|
||||
anchor?: TooltipAnchor
|
||||
}
|
||||
|
||||
const HistogramChartTooltip: SFC<Props> = props => {
|
||||
const {datum, x, y, anchor = 'left'} = props
|
||||
|
||||
if (!datum) {
|
||||
return null
|
||||
}
|
||||
|
||||
const style: CSSProperties = {
|
||||
position: 'fixed',
|
||||
top: y,
|
||||
}
|
||||
|
||||
if (anchor === 'left') {
|
||||
style.left = x
|
||||
} else {
|
||||
style.right = x
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="histogram-chart-tooltip"
|
||||
style={style}
|
||||
data-group={datum.group}
|
||||
>
|
||||
<div className="histogram-chart-tooltip--value">{datum.value}</div>
|
||||
<div className="histogram-chart-tooltip--group">{datum.group}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default HistogramChartTooltip
|
|
@ -0,0 +1,163 @@
|
|||
import React, {
|
||||
PureComponent,
|
||||
RefObject,
|
||||
MouseEvent as ReactMouseEvent,
|
||||
} from 'react'
|
||||
import {ScaleTime} from 'd3-scale'
|
||||
|
||||
import {TimePeriod} from 'src/types/histogram'
|
||||
|
||||
interface Props {
|
||||
width: number
|
||||
height: number
|
||||
xScale: ScaleTime<number, number>
|
||||
onBrush?: (t: TimePeriod) => void
|
||||
onDoubleClick?: () => void
|
||||
}
|
||||
|
||||
interface State {
|
||||
dragging: boolean
|
||||
dragStartPos: number
|
||||
dragPos: number
|
||||
}
|
||||
|
||||
class XBrush extends PureComponent<Props, State> {
|
||||
private draggableArea: RefObject<SVGRectElement>
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
dragging: false,
|
||||
dragStartPos: 0,
|
||||
dragPos: 0,
|
||||
}
|
||||
|
||||
this.draggableArea = React.createRef<SVGRectElement>()
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
// These are usually cleaned up on handleDragEnd; this ensures they will
|
||||
// also be cleaned up if the component is destroyed mid-brush
|
||||
document.removeEventListener('movemove', this.handleDrag)
|
||||
document.removeEventListener('mouseup', this.handleDragEnd)
|
||||
}
|
||||
|
||||
public render() {
|
||||
const {width, height} = this.props
|
||||
|
||||
return (
|
||||
<>
|
||||
{this.renderSelection()}
|
||||
<rect
|
||||
ref={this.draggableArea}
|
||||
className="x-brush--area"
|
||||
width={width}
|
||||
height={height}
|
||||
onMouseDown={this.handleDragStart}
|
||||
onDoubleClick={this.handleDoubleClick}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
private renderSelection(): JSX.Element {
|
||||
const {height} = this.props
|
||||
const {dragging, dragStartPos, dragPos} = this.state
|
||||
|
||||
if (!dragging) {
|
||||
return null
|
||||
}
|
||||
|
||||
const x = Math.min(dragStartPos, dragPos)
|
||||
const width = Math.abs(dragStartPos - dragPos)
|
||||
|
||||
return (
|
||||
<rect
|
||||
className="x-brush--selection"
|
||||
y={0}
|
||||
height={height}
|
||||
x={x}
|
||||
width={width}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
private handleDragStart = (e: ReactMouseEvent<SVGRectElement>): void => {
|
||||
// A user can mousedown (start a brush) then move outside of the current
|
||||
// element while still holding the mouse down, therfore we must listen to
|
||||
// mouse events everywhere, not just within this component.
|
||||
document.addEventListener('mousemove', this.handleDrag)
|
||||
document.addEventListener('mouseup', this.handleDragEnd)
|
||||
|
||||
const x = this.getX(e)
|
||||
|
||||
this.setState({
|
||||
dragging: true,
|
||||
dragStartPos: x,
|
||||
dragPos: x,
|
||||
})
|
||||
}
|
||||
|
||||
private handleDrag = (e: MouseEvent): void => {
|
||||
const {dragging} = this.state
|
||||
|
||||
if (!dragging) {
|
||||
return
|
||||
}
|
||||
|
||||
this.setState({dragPos: this.getX(e)})
|
||||
}
|
||||
|
||||
private handleDragEnd = (): void => {
|
||||
document.removeEventListener('movemove', this.handleDrag)
|
||||
document.removeEventListener('mouseup', this.handleDragEnd)
|
||||
|
||||
const {xScale, onBrush} = this.props
|
||||
const {dragging, dragPos, dragStartPos} = this.state
|
||||
|
||||
if (!dragging) {
|
||||
return
|
||||
}
|
||||
|
||||
this.setState({dragging: false})
|
||||
|
||||
if (!onBrush || Math.round(dragPos) === Math.round(dragStartPos)) {
|
||||
return
|
||||
}
|
||||
|
||||
const startX = Math.min(dragStartPos, dragPos)
|
||||
const endX = Math.max(dragStartPos, dragPos)
|
||||
const start = xScale.invert(startX).getTime()
|
||||
const end = xScale.invert(endX).getTime()
|
||||
|
||||
onBrush({start, end})
|
||||
}
|
||||
|
||||
private handleDoubleClick = (): void => {
|
||||
const {onDoubleClick} = this.props
|
||||
|
||||
if (onDoubleClick) {
|
||||
onDoubleClick()
|
||||
}
|
||||
}
|
||||
|
||||
private getX = (e: MouseEvent | ReactMouseEvent<SVGRectElement>): number => {
|
||||
const {width} = this.props
|
||||
|
||||
const {left} = this.draggableArea.current.getBoundingClientRect()
|
||||
const x = e.pageX - left
|
||||
|
||||
if (x < 0) {
|
||||
return 0
|
||||
}
|
||||
|
||||
if (x > width) {
|
||||
return width
|
||||
}
|
||||
|
||||
return x
|
||||
}
|
||||
}
|
||||
|
||||
export default XBrush
|
|
@ -73,6 +73,7 @@
|
|||
@import 'components/threshold-controls';
|
||||
@import 'components/kapacitor-logs-table';
|
||||
@import 'components/dropdown-placeholder';
|
||||
@import 'components/histogram-chart';
|
||||
|
||||
// Pages
|
||||
@import 'pages/config-endpoints';
|
||||
|
|
|
@ -0,0 +1,99 @@
|
|||
@keyframes blur-in {
|
||||
from {
|
||||
filter: blur(0);
|
||||
}
|
||||
|
||||
to {
|
||||
filter: blur(2px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes blur-out {
|
||||
from {
|
||||
filter: blur(2px);
|
||||
}
|
||||
|
||||
to {
|
||||
filter: blur(0);
|
||||
}
|
||||
}
|
||||
|
||||
.histogram-chart {
|
||||
user-select: none;
|
||||
|
||||
&:not(.loading) {
|
||||
animation-duration: 0.1s;
|
||||
animation-name: blur-out;
|
||||
}
|
||||
|
||||
&.loading {
|
||||
animation-duration: 0.3s;
|
||||
animation-name: blur-in;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
}
|
||||
|
||||
.histogram-chart-bars--bar {
|
||||
shape-rendering: crispEdges;
|
||||
fill: $c-amethyst;
|
||||
opacity: 1;
|
||||
pointer: cursor;
|
||||
shape-rendering: crispEdges;
|
||||
}
|
||||
|
||||
.histogram-chart--axes, .histogram-chart-skeleton {
|
||||
.x-label, .y-label {
|
||||
fill: $g13-mist;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.x-label {
|
||||
text-anchor: middle;
|
||||
alignment-baseline: hanging;
|
||||
}
|
||||
|
||||
.y-label {
|
||||
text-anchor: end;
|
||||
alignment-baseline: middle;
|
||||
}
|
||||
|
||||
.y-tick {
|
||||
stroke-width: 1;
|
||||
stroke: $g5-pepper;
|
||||
shape-rendering: crispEdges;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.histogram-chart-skeleton, .histogram-chart:not(.loading) .x-brush--area {
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
.histogram-chart .x-brush--area {
|
||||
visibility: hidden;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.histogram-chart .x-brush--selection {
|
||||
fill: gray;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.histogram-chart-tooltip {
|
||||
padding: 8px;
|
||||
background-color: $g0-obsidian;
|
||||
border-radius: 3px;
|
||||
@extend %drop-shadow;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: $g13-mist;
|
||||
display: flex;
|
||||
align-items: space-between;
|
||||
transform: translate(0, -50%);
|
||||
pointer-events: none;
|
||||
|
||||
.histogram-chart-tooltip--value {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
|
@ -3,12 +3,29 @@
|
|||
----------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
$logs-viewer-graph-height: 180px;
|
||||
$logs-viewer-graph-height: 220px;
|
||||
$logs-viewer-search-height: 46px;
|
||||
$logs-viewer-filter-height: 42px;
|
||||
$logs-viewer-results-text-indent: 33px;
|
||||
$logs-viewer-gutter: 60px;
|
||||
|
||||
$severity-emerg: $c-ruby;
|
||||
$severity-alert: $c-fire;
|
||||
$severity-crit: $c-curacao;
|
||||
$severity-err: $c-tiger;
|
||||
$severity-warning: $c-pineapple;
|
||||
$severity-notice: $c-rainforest;
|
||||
$severity-info: $c-star;
|
||||
$severity-debug: $g9-mountain;
|
||||
$severity-emerg-intense: $c-fire;
|
||||
$severity-alert-intense: $c-curacao;
|
||||
$severity-crit-intense: $c-tiger;
|
||||
$severity-err-intense: $c-pineapple;
|
||||
$severity-warning-intense: $c-thunder;
|
||||
$severity-notice-intense: $c-honeydew;
|
||||
$severity-info-intense: $c-comet;
|
||||
$severity-debug-intense: $g10-wolf;
|
||||
|
||||
.logs-viewer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
@ -232,28 +249,28 @@ $logs-viewer-gutter: 60px;
|
|||
margin-left: 2px;
|
||||
|
||||
&.emerg-severity {
|
||||
@include gradient-diag-up($c-ruby, $c-fire);
|
||||
@include gradient-diag-up($severity-emerg, $severity-emerg-intense);
|
||||
}
|
||||
&.alert-severity {
|
||||
@include gradient-diag-up($c-fire, $c-curacao);
|
||||
@include gradient-diag-up($severity-alert, $severity-alert-intense);
|
||||
}
|
||||
&.crit-severity {
|
||||
@include gradient-diag-up($c-curacao, $c-tiger);
|
||||
@include gradient-diag-up($severity-crit, $severity-crit-intense);
|
||||
}
|
||||
&.err-severity {
|
||||
@include gradient-diag-up($c-tiger, $c-pineapple);
|
||||
@include gradient-diag-up($severity-err, $severity-err-intense);
|
||||
}
|
||||
&.warning-severity {
|
||||
@include gradient-diag-up($c-pineapple, $c-thunder);
|
||||
@include gradient-diag-up($severity-warning, $severity-warning-intense);
|
||||
}
|
||||
&.notice-severity {
|
||||
@include gradient-diag-up($c-rainforest, $c-honeydew);
|
||||
@include gradient-diag-up($severity-notice, $severity-notice-intense);
|
||||
}
|
||||
&.info-severity {
|
||||
@include gradient-diag-up($c-star, $c-comet);
|
||||
@include gradient-diag-up($severity-info, $severity-info-intense);
|
||||
}
|
||||
&.debug-severity {
|
||||
@include gradient-diag-up($g9-mountain, $g10-wolf);
|
||||
@include gradient-diag-up($severity-debug, $severity-debug-intense);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -310,3 +327,79 @@ $logs-viewer-gutter: 60px;
|
|||
background-color: $c-laser;
|
||||
}
|
||||
}
|
||||
|
||||
.logs-viewer .histogram-chart-bars--bar, .logs-viewer .histogram-chart-tooltip {
|
||||
&[data-group="emerg"] {
|
||||
fill: $severity-emerg;
|
||||
color: $severity-emerg;
|
||||
}
|
||||
|
||||
&[data-group="alert"] {
|
||||
fill: $severity-alert;
|
||||
color: $severity-alert;
|
||||
}
|
||||
|
||||
&[data-group="crit"] {
|
||||
fill: $severity-crit;
|
||||
color: $severity-crit;
|
||||
}
|
||||
|
||||
&[data-group="err"] {
|
||||
fill: $severity-err;
|
||||
color: $severity-err;
|
||||
}
|
||||
|
||||
&[data-group="warning"] {
|
||||
fill: $severity-warning;
|
||||
color: $severity-warning;
|
||||
}
|
||||
|
||||
&[data-group="notice"] {
|
||||
fill: $severity-notice;
|
||||
color: $severity-notice;
|
||||
}
|
||||
|
||||
&[data-group="info"] {
|
||||
fill: $severity-info;
|
||||
color: $severity-info;
|
||||
}
|
||||
|
||||
&[data-group="debug"] {
|
||||
fill: $severity-debug;
|
||||
color: $severity-debug;
|
||||
}
|
||||
}
|
||||
|
||||
.logs-viewer .histogram-chart-bars--bar:hover {
|
||||
&[data-group="emerg"] {
|
||||
fill: $severity-emerg-intense;
|
||||
}
|
||||
|
||||
&[data-group="alert"] {
|
||||
fill: $severity-alert-intense;
|
||||
}
|
||||
|
||||
&[data-group="crit"] {
|
||||
fill: $severity-crit-intense;
|
||||
}
|
||||
|
||||
&[data-group="err"] {
|
||||
fill: $severity-err-intense;
|
||||
}
|
||||
|
||||
&[data-group="warning"] {
|
||||
fill: $severity-warning-intense;
|
||||
}
|
||||
|
||||
&[data-group="notice"] {
|
||||
fill: $severity-notice-intense;
|
||||
}
|
||||
|
||||
&[data-group="info"] {
|
||||
fill: $severity-info-intense;
|
||||
}
|
||||
|
||||
&[data-group="debug"] {
|
||||
fill: $severity-debug-intense;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
type UnixTime = number
|
||||
|
||||
export interface HistogramDatum {
|
||||
key: string
|
||||
time: UnixTime
|
||||
value: number
|
||||
group: string
|
||||
}
|
||||
|
||||
export interface TimePeriod {
|
||||
start: UnixTime
|
||||
end: UnixTime
|
||||
}
|
||||
|
||||
export type HistogramData = HistogramDatum[]
|
||||
|
||||
export type TooltipAnchor = 'left' | 'right'
|
||||
|
||||
export interface Margins {
|
||||
top: number
|
||||
right: number
|
||||
bottom: number
|
||||
left: number
|
||||
}
|
|
@ -1,4 +1,10 @@
|
|||
import {QueryConfig, TimeRange, Namespace, Source} from 'src/types'
|
||||
import {
|
||||
QueryConfig,
|
||||
TimeRange,
|
||||
Namespace,
|
||||
Source,
|
||||
RemoteDataState,
|
||||
} from 'src/types'
|
||||
|
||||
export interface Filter {
|
||||
id: string
|
||||
|
@ -19,6 +25,7 @@ export interface LogsState {
|
|||
timeRange: TimeRange
|
||||
histogramQueryConfig: QueryConfig | null
|
||||
histogramData: object[]
|
||||
histogramDataStatus: RemoteDataState
|
||||
tableQueryConfig: QueryConfig | null
|
||||
tableData: TableData
|
||||
searchTerm: string | null
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
export default function extentBy<T>(
|
||||
collection: T[],
|
||||
keyFn: (v: any) => number
|
||||
): T[] {
|
||||
let min = Infinity
|
||||
let max = -Infinity
|
||||
let minItem
|
||||
let maxItem
|
||||
|
||||
for (const item of collection) {
|
||||
const val = keyFn(item)
|
||||
|
||||
if (val <= min) {
|
||||
min = val
|
||||
minItem = item
|
||||
}
|
||||
|
||||
if (val >= max) {
|
||||
max = val
|
||||
maxItem = item
|
||||
}
|
||||
}
|
||||
|
||||
return [minItem, maxItem]
|
||||
}
|
53
ui/yarn.lock
53
ui/yarn.lock
|
@ -32,6 +32,16 @@
|
|||
version "0.0.56"
|
||||
resolved "https://registry.yarnpkg.com/@types/codemirror/-/codemirror-0.0.56.tgz#1fcf68df0d0a49791d843dadda7d94891ac88669"
|
||||
|
||||
"@types/d3-scale@^2.0.1":
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/d3-scale/-/d3-scale-2.0.1.tgz#f94cd991c50422b2e68d8f43be3f9fffdb1ae7be"
|
||||
dependencies:
|
||||
"@types/d3-time" "*"
|
||||
|
||||
"@types/d3-time@*":
|
||||
version "1.0.8"
|
||||
resolved "https://registry.yarnpkg.com/@types/d3-time/-/d3-time-1.0.8.tgz#6c083127b330b3c2fc65cd0f3a6e9cbd9607b28c"
|
||||
|
||||
"@types/dygraphs@^1.1.6":
|
||||
version "1.1.6"
|
||||
resolved "https://registry.yarnpkg.com/@types/dygraphs/-/dygraphs-1.1.6.tgz#20ff1a01e353e813ff97898c0fee5defc66626be"
|
||||
|
@ -2606,6 +2616,49 @@ cyclist@~0.2.2:
|
|||
version "0.2.2"
|
||||
resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-0.2.2.tgz#1b33792e11e914a2fd6d6ed6447464444e5fa640"
|
||||
|
||||
d3-array@^1.2.0:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-1.2.1.tgz#d1ca33de2f6ac31efadb8e050a021d7e2396d5dc"
|
||||
|
||||
d3-collection@1:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/d3-collection/-/d3-collection-1.0.4.tgz#342dfd12837c90974f33f1cc0a785aea570dcdc2"
|
||||
|
||||
d3-color@1:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-1.2.0.tgz#d1ea19db5859c86854586276ec892cf93148459a"
|
||||
|
||||
d3-format@1:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-1.3.0.tgz#a3ac44269a2011cdb87c7b5693040c18cddfff11"
|
||||
|
||||
d3-interpolate@1:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-1.2.0.tgz#40d81bd8e959ff021c5ea7545bc79b8d22331c41"
|
||||
dependencies:
|
||||
d3-color "1"
|
||||
|
||||
d3-scale@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-2.1.0.tgz#8d3fd3e2a7c9080782a523c08507c5248289eef8"
|
||||
dependencies:
|
||||
d3-array "^1.2.0"
|
||||
d3-collection "1"
|
||||
d3-format "1"
|
||||
d3-interpolate "1"
|
||||
d3-time "1"
|
||||
d3-time-format "2"
|
||||
|
||||
d3-time-format@2:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-2.1.1.tgz#85b7cdfbc9ffca187f14d3c456ffda268081bb31"
|
||||
dependencies:
|
||||
d3-time "1"
|
||||
|
||||
d3-time@1:
|
||||
version "1.0.8"
|
||||
resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-1.0.8.tgz#dbd2d6007bf416fe67a76d17947b784bffea1e84"
|
||||
|
||||
d@1:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/d/-/d-1.0.0.tgz#754bb5bfe55451da69a58b94d45f4c5b0462d58f"
|
||||
|
|
Loading…
Reference in New Issue