Merge remote-tracking branch 'origin/master' into fill-query-widget
commit
1371c6afad
32
CHANGELOG.md
32
CHANGELOG.md
|
@ -2,18 +2,48 @@
|
|||
### Bug Fixes
|
||||
1. [#1886](https://github.com/influxdata/chronograf/pull/1886): Fix limit of 100 alert rules on alert rules page
|
||||
1. [#1930](https://github.com/influxdata/chronograf/pull/1930): Fix graphs when y-values are constant
|
||||
|
||||
1. [#1951](https://github.com/influxdata/chronograf/pull/1951): Fix crosshair not being removed when user leaves graph
|
||||
1. [#1943](https://github.com/influxdata/chronograf/pull/1943): Fix inability to add kapacitor from source page on fresh install
|
||||
1. [#1947](https://github.com/influxdata/chronograf/pull/1947): Fix DataExplorer crash if field property not present on queryConfig
|
||||
1. [#1957](https://github.com/influxdata/chronograf/pull/1957): Fix stacked graphs not being fully displayed
|
||||
|
||||
### Features
|
||||
1. [#1928](https://github.com/influxdata/chronograf/pull/1928): Add prefix, suffix, scale, and other y-axis formatting
|
||||
1. [#1886](https://github.com/influxdata/chronograf/pull/1886): Fix limit of 100 alert rules on alert rules page
|
||||
1. [#1934](https://github.com/influxdata/chronograf/pull/1943): Zoom syncronization and enhancement
|
||||
|
||||
### UI Improvements
|
||||
1. [#1933](https://github.com/influxdata/chronograf/pull/1933): Use line-stacked graph type for memory information - thank you, @Joxit!
|
||||
1. [#1940](https://github.com/influxdata/chronograf/pull/1940): Improve cell sizes in Admin Database tables
|
||||
1. [#1942](https://github.com/influxdata/chronograf/pull/1942): Polish appearance of optional alert parameters in Kapacitor rule builder
|
||||
1. [#1944](https://github.com/influxdata/chronograf/pull/1944): Add active state for Status page navbar icon
|
||||
1. [#1944](https://github.com/influxdata/chronograf/pull/1944): Improve UX of navigation to a sub-nav item in the navbar
|
||||
|
||||
## v1.3.7.0 [2017-08-23]
|
||||
## v1.3.7.0 [unreleased]
|
||||
### Features
|
||||
1. [#1928](https://github.com/influxdata/chronograf/pull/1928): Add prefix, suffix, scale, and other y-axis formatting
|
||||
|
||||
### UI Improvements
|
||||
|
||||
## v1.3.7.0
|
||||
### Bug Fixes
|
||||
1. [#1795](https://github.com/influxdata/chronograf/pull/1795): Fix uptime status on Windows hosts running Telegraf
|
||||
1. [#1715](https://github.com/influxdata/chronograf/pull/1715): Chronograf now renders on IE11.
|
||||
1. [#1870](https://github.com/influxdata/chronograf/pull/1870): Fix console error for placing prop on div
|
||||
1. [#1864](https://github.com/influxdata/chronograf/pull/1864): Fix Write Data form upload button and add `onDragExit` handler
|
||||
1. [#1891](https://github.com/influxdata/chronograf/pull/1891): Fix Kapacitor config for PagerDuty via the UI
|
||||
1. [#1872](https://github.com/influxdata/chronograf/pull/1872): Prevent stats in the legend from wrapping line
|
||||
|
||||
### Features
|
||||
1. [#1863](https://github.com/influxdata/chronograf/pull/1863): Improve 'new-sources' server flag example by adding 'type' key
|
||||
|
||||
### UI Improvements
|
||||
1. [#1862](https://github.com/influxdata/chronograf/pull/1862): Show "Add Graph" button on cells with no queries
|
||||
|
||||
## v1.3.6.1 [2017-08-14]
|
||||
**Upgrade Note** This release (1.3.6.1) fixes a possibly data corruption issue with dashboard cells' graph types. If you upgraded to 1.3.6.0 and visited any dashboard, once you have then upgraded to this release (1.3.6.1) you will need to manually reset the graph type for every cell via the cell's caret --> Edit --> Display Options. If you upgraded directly to 1.3.6.1, you should not experience this issue.
|
||||
|
||||
### Bug Fixes
|
||||
1. [#1795](https://github.com/influxdata/chronograf/pull/1795): Fix uptime status on Windows hosts running Telegraf
|
||||
1. [#1715](https://github.com/influxdata/chronograf/pull/1715): Chronograf now renders on IE11.
|
||||
|
|
|
@ -260,7 +260,7 @@ func (g *GroupByVar) parseRelative(fragment string) (time.Duration, error) {
|
|||
// example, the fragement "time > '1985-10-25T00:01:21-0800 and time <
|
||||
// '1985-10-25T00:01:22-0800'" would yield a duration of 1m'
|
||||
func (g *GroupByVar) parseAbsolute(fragment string) (time.Duration, error) {
|
||||
timePtn := `time\s[>|<]\s'([0-9\-TZ\:]+)'` // Playground: http://gobular.com/x/41a45095-c384-46ea-b73c-54ef91ab93af
|
||||
timePtn := `time\s[>|<]\s'([0-9\-T\:\.Z]+)'` // Playground: http://gobular.com/x/208f66bd-1889-4269-ab47-1efdfeeb63f0
|
||||
re, err := regexp.Compile(timePtn)
|
||||
if err != nil {
|
||||
// this is a developer error and should complain loudly
|
||||
|
|
|
@ -29,6 +29,13 @@ func Test_GroupByVar(t *testing.T) {
|
|||
1000,
|
||||
10 * time.Second,
|
||||
},
|
||||
{
|
||||
"absolute time with nano",
|
||||
"SELECT mean(usage_idle) FROM cpu WHERE time > '2017-07-24T15:33:42.994Z' and time < '2017-08-24T15:33:42.994Z' GROUP BY :interval:",
|
||||
744 * time.Hour,
|
||||
1000,
|
||||
10 * time.Second,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range gbvTests {
|
||||
|
|
|
@ -146,6 +146,20 @@ describe('Chronograf.Reducers.DataExplorer.queryConfigs', () => {
|
|||
expect(newState[queryId].fields[1].funcs.length).to.equal(1)
|
||||
expect(newState[queryId].fields[1].funcs[0]).to.equal('func1')
|
||||
})
|
||||
|
||||
it('adds the field property to query config if not found', () => {
|
||||
delete state[queryId].fields
|
||||
expect(state[queryId].fields).to.equal(undefined)
|
||||
|
||||
const field = 'fk1'
|
||||
const newState = reducer(
|
||||
state,
|
||||
toggleField(queryId, {field: 'fk1', funcs: []})
|
||||
)
|
||||
|
||||
expect(newState[queryId].fields.length).to.equal(1)
|
||||
expect(newState[queryId].fields[0].field).to.equal(field)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
@ -28,15 +28,11 @@ class AlertsTable extends Component {
|
|||
filterAlerts = (searchTerm, newAlerts) => {
|
||||
const alerts = newAlerts || this.props.alerts
|
||||
const filterText = searchTerm.toLowerCase()
|
||||
const filteredAlerts = alerts.filter(h => {
|
||||
if (h.host === null || h.name === null || h.level === null) {
|
||||
return false
|
||||
}
|
||||
|
||||
const filteredAlerts = alerts.filter(({name, host, level}) => {
|
||||
return (
|
||||
h.name.toLowerCase().includes(filterText) ||
|
||||
h.host.toLowerCase().includes(filterText) ||
|
||||
h.level.toLowerCase().includes(filterText)
|
||||
(name && name.toLowerCase().includes(filterText)) ||
|
||||
(host && host.toLowerCase().includes(filterText)) ||
|
||||
(level && level.toLowerCase().includes(filterText))
|
||||
)
|
||||
})
|
||||
this.setState({searchTerm, filteredAlerts})
|
||||
|
|
|
@ -7,6 +7,7 @@ import FancyScrollbar from 'shared/components/FancyScrollbar'
|
|||
|
||||
const Dashboard = ({
|
||||
source,
|
||||
onZoom,
|
||||
dashboard,
|
||||
onAddCell,
|
||||
onEditCell,
|
||||
|
@ -71,6 +72,7 @@ const Dashboard = ({
|
|||
onDeleteCell={onDeleteCell}
|
||||
onSummonOverlayTechnologies={onSummonOverlayTechnologies}
|
||||
synchronizer={synchronizer}
|
||||
onZoom={onZoom}
|
||||
/>
|
||||
: <div className="dashboard__empty">
|
||||
<p>This Dashboard has no Cells</p>
|
||||
|
@ -127,6 +129,7 @@ Dashboard.propTypes = {
|
|||
onSelectTemplate: func.isRequired,
|
||||
showTemplateControlBar: bool,
|
||||
onCancelEditCell: func,
|
||||
onZoom: func,
|
||||
}
|
||||
|
||||
export default Dashboard
|
||||
|
|
|
@ -11,7 +11,8 @@ const DashboardHeader = ({
|
|||
buttonText,
|
||||
dashboard,
|
||||
headerText,
|
||||
timeRange,
|
||||
timeRange: {upper, lower},
|
||||
zoomedTimeRange: {zoomedLower, zoomedUpper},
|
||||
autoRefresh,
|
||||
isHidden,
|
||||
handleChooseTimeRange,
|
||||
|
@ -81,7 +82,10 @@ const DashboardHeader = ({
|
|||
/>
|
||||
<TimeRangeDropdown
|
||||
onChooseTimeRange={handleChooseTimeRange}
|
||||
selected={timeRange}
|
||||
selected={{
|
||||
upper: zoomedUpper || upper,
|
||||
lower: zoomedLower || lower,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="btn btn-default btn-sm btn-square"
|
||||
|
@ -95,6 +99,13 @@ const DashboardHeader = ({
|
|||
|
||||
const {array, bool, func, number, shape, string} = PropTypes
|
||||
|
||||
DashboardHeader.defaultProps = {
|
||||
zoomedTimeRange: {
|
||||
zoomedLower: null,
|
||||
zoomedUpper: null,
|
||||
},
|
||||
}
|
||||
|
||||
DashboardHeader.propTypes = {
|
||||
sourceID: string,
|
||||
children: array,
|
||||
|
@ -115,6 +126,7 @@ DashboardHeader.propTypes = {
|
|||
onEditDashboard: func,
|
||||
onToggleTempVarControls: func,
|
||||
showTemplateControlBar: bool,
|
||||
zoomedTimeRange: shape({}),
|
||||
}
|
||||
|
||||
export default DashboardHeader
|
||||
|
|
|
@ -31,6 +31,7 @@ class DashboardPage extends Component {
|
|||
isEditMode: false,
|
||||
selectedCell: null,
|
||||
isTemplating: false,
|
||||
zoomedTimeRange: {zoomedLower: null, zoomedUpper: null},
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -196,12 +197,19 @@ class DashboardPage extends Component {
|
|||
)
|
||||
}
|
||||
|
||||
handleZoomedTimeRange = (zoomedLower, zoomedUpper) => {
|
||||
this.setState({zoomedTimeRange: {zoomedLower, zoomedUpper}})
|
||||
}
|
||||
|
||||
getActiveDashboard() {
|
||||
const {params: {dashboardID}, dashboards} = this.props
|
||||
return dashboards.find(d => d.id === +dashboardID)
|
||||
}
|
||||
|
||||
render() {
|
||||
const {zoomedTimeRange} = this.state
|
||||
const {zoomedLower, zoomedUpper} = zoomedTimeRange
|
||||
|
||||
const {
|
||||
source,
|
||||
timeRange,
|
||||
|
@ -217,8 +225,11 @@ class DashboardPage extends Component {
|
|||
params: {sourceID, dashboardID},
|
||||
} = this.props
|
||||
|
||||
const lowerType = lower && lower.includes('Z') ? 'timeStamp' : 'constant'
|
||||
const upperType = upper && upper.includes('Z') ? 'timeStamp' : 'constant'
|
||||
const low = zoomedLower ? zoomedLower : lower
|
||||
const up = zoomedUpper ? zoomedUpper : upper
|
||||
|
||||
const lowerType = low && low.includes(':') ? 'timeStamp' : 'constant'
|
||||
const upperType = up && up.includes(':') ? 'timeStamp' : 'constant'
|
||||
|
||||
const dashboardTime = {
|
||||
id: 'dashtime',
|
||||
|
@ -226,7 +237,7 @@ class DashboardPage extends Component {
|
|||
type: lowerType,
|
||||
values: [
|
||||
{
|
||||
value: lower,
|
||||
value: low,
|
||||
type: lowerType,
|
||||
selected: true,
|
||||
},
|
||||
|
@ -239,7 +250,7 @@ class DashboardPage extends Component {
|
|||
type: upperType,
|
||||
values: [
|
||||
{
|
||||
value: upper || 'now()',
|
||||
value: up || 'now()',
|
||||
type: upperType,
|
||||
selected: true,
|
||||
},
|
||||
|
@ -310,6 +321,7 @@ class DashboardPage extends Component {
|
|||
sourceID={sourceID}
|
||||
dashboard={dashboard}
|
||||
timeRange={timeRange}
|
||||
zoomedTimeRange={zoomedTimeRange}
|
||||
autoRefresh={autoRefresh}
|
||||
isHidden={inPresentationMode}
|
||||
onAddCell={this.handleAddCell}
|
||||
|
@ -337,6 +349,7 @@ class DashboardPage extends Component {
|
|||
dashboard={dashboard}
|
||||
timeRange={timeRange}
|
||||
autoRefresh={autoRefresh}
|
||||
onZoom={this.handleZoomedTimeRange}
|
||||
onAddCell={this.handleAddCell}
|
||||
synchronizer={this.synchronizer}
|
||||
inPresentationMode={inPresentationMode}
|
||||
|
|
|
@ -288,22 +288,17 @@ Dygraph.Plugins.Crosshair = (function() {
|
|||
|
||||
var width = e.dygraph.width_
|
||||
var height = e.dygraph.height_
|
||||
var xLabelPixels = 20
|
||||
|
||||
this.canvas_.width = width
|
||||
this.canvas_.height = height
|
||||
this.canvas_.height = height - xLabelPixels
|
||||
this.canvas_.style.width = width + 'px' // for IE
|
||||
this.canvas_.style.height = height + 'px' // for IE
|
||||
this.canvas_.style.height = height - xLabelPixels + 'px' // for IE
|
||||
|
||||
var ctx = this.canvas_.getContext('2d')
|
||||
ctx.clearRect(0, 0, width, height)
|
||||
|
||||
const gradient = ctx.createLinearGradient(0, 0, 0, height)
|
||||
gradient.addColorStop(0, 'rgba(255, 255, 255, 0.0)')
|
||||
gradient.addColorStop(0.11, 'rgba(255, 255, 255, 1.0)')
|
||||
gradient.addColorStop(0.89, 'rgba(255, 255, 255, 1.0)')
|
||||
gradient.addColorStop(1, 'rgba(255, 255, 255, 0.0)')
|
||||
|
||||
ctx.strokeStyle = gradient
|
||||
ctx.lineWidth = 1.5
|
||||
ctx.strokeStyle = '#C6CAD3'
|
||||
ctx.lineWidth = 1
|
||||
|
||||
// If graphs have different time ranges, it's possible to select a point on
|
||||
// one graph that doesn't exist in another, resulting in an exception.
|
||||
|
|
|
@ -68,11 +68,18 @@ class KapacitorPage extends Component {
|
|||
|
||||
handleSubmit(e) {
|
||||
e.preventDefault()
|
||||
const {addFlashMessage, source, params, router} = this.props
|
||||
const {
|
||||
addFlashMessage,
|
||||
source,
|
||||
source: {kapacitors = []},
|
||||
params,
|
||||
router,
|
||||
} = this.props
|
||||
const {kapacitor} = this.state
|
||||
|
||||
const kapNames = source.kapacitors.map(k => k.name)
|
||||
if (kapNames.includes(kapacitor.name)) {
|
||||
const isNameTaken = kapacitors.some(k => k.name === kapacitor.name)
|
||||
|
||||
if (isNameTaken) {
|
||||
addFlashMessage({
|
||||
type: 'error',
|
||||
text: `There is already a Kapacitor configuration named "${kapacitor.name}"`,
|
||||
|
|
|
@ -3,25 +3,24 @@ import React, {Component, PropTypes} from 'react'
|
|||
import shallowCompare from 'react-addons-shallow-compare'
|
||||
|
||||
import _ from 'lodash'
|
||||
import moment from 'moment'
|
||||
|
||||
import Dygraphs from 'src/external/dygraph'
|
||||
import getRange from 'shared/parsing/getRangeForDygraph'
|
||||
|
||||
import {DISPLAY_OPTIONS} from 'src/dashboards/constants'
|
||||
import {LINE_COLORS, multiColumnBarPlotter} from 'src/shared/graphs/helpers'
|
||||
import getRange, {getStackedRange} from 'shared/parsing/getRangeForDygraph'
|
||||
import DygraphLegend from 'src/shared/components/DygraphLegend'
|
||||
import {DISPLAY_OPTIONS} from 'src/dashboards/constants'
|
||||
import {buildDefaultYLabel} from 'shared/presenters'
|
||||
import {numberValueFormatter} from 'src/utils/formatting'
|
||||
|
||||
const {LINEAR, LOG, BASE_10} = DISPLAY_OPTIONS
|
||||
const labelWidth = 60
|
||||
const avgCharPixels = 7
|
||||
|
||||
const hasherino = (str, len) =>
|
||||
str
|
||||
.split('')
|
||||
.map(char => char.charCodeAt(0))
|
||||
.reduce((hash, code) => hash + code, 0) % len
|
||||
import {
|
||||
OPTIONS,
|
||||
LINE_COLORS,
|
||||
LABEL_WIDTH,
|
||||
CHAR_PIXELS,
|
||||
barPlotter,
|
||||
hasherino,
|
||||
highlightSeriesOpts,
|
||||
} from 'src/shared/graphs/helpers'
|
||||
const {LINEAR, LOG, BASE_10, BASE_2} = DISPLAY_OPTIONS
|
||||
|
||||
export default class Dygraph extends Component {
|
||||
constructor(props) {
|
||||
|
@ -42,157 +41,61 @@ export default class Dygraph extends Component {
|
|||
}
|
||||
|
||||
componentDidMount() {
|
||||
const timeSeries = this.getTimeSeries()
|
||||
// dygraphSeries is a legend label and its corresponding y-axis e.g. {legendLabel1: 'y', legendLabel2: 'y2'};
|
||||
const {
|
||||
axes: {y, y2},
|
||||
dygraphSeries,
|
||||
ruleValues,
|
||||
overrideLineColors,
|
||||
isGraphFilled,
|
||||
isGraphFilled: fillGraph,
|
||||
isBarGraph,
|
||||
options,
|
||||
} = this.props
|
||||
|
||||
const timeSeries = this.getTimeSeries()
|
||||
const graphRef = this.graphRef
|
||||
const legendRef = this.legendRef
|
||||
const finalLineColors = [...(overrideLineColors || LINE_COLORS)]
|
||||
|
||||
const hashColorDygraphSeries = {}
|
||||
const {length} = finalLineColors
|
||||
|
||||
for (const seriesName in dygraphSeries) {
|
||||
const series = dygraphSeries[seriesName]
|
||||
const hashIndex = hasherino(seriesName, length)
|
||||
const color = finalLineColors[hashIndex]
|
||||
hashColorDygraphSeries[seriesName] = {...series, color}
|
||||
}
|
||||
|
||||
const axisLabelWidth =
|
||||
labelWidth +
|
||||
y.prefix.length * avgCharPixels +
|
||||
y.suffix.length * avgCharPixels
|
||||
|
||||
const defaultOptions = {
|
||||
plugins: isBarGraph
|
||||
? []
|
||||
: [
|
||||
new Dygraphs.Plugins.Crosshair({
|
||||
direction: 'vertical',
|
||||
}),
|
||||
],
|
||||
let defaultOptions = {
|
||||
fillGraph,
|
||||
logscale: y.scale === LOG,
|
||||
labelsSeparateLines: false,
|
||||
labelsKMB: true,
|
||||
rightGap: 0,
|
||||
highlightSeriesBackgroundAlpha: 1.0,
|
||||
highlightSeriesBackgroundColor: 'rgb(41, 41, 51)',
|
||||
fillGraph: isGraphFilled,
|
||||
axisLineWidth: 2,
|
||||
gridLineWidth: 1,
|
||||
highlightCircleSize: isBarGraph ? 0 : 3,
|
||||
animatedZooms: true,
|
||||
hideOverlayOnMouseOut: false,
|
||||
colors: finalLineColors,
|
||||
series: hashColorDygraphSeries,
|
||||
colors: this.getLineColors(),
|
||||
series: this.hashColorDygraphSeries(),
|
||||
legendFormatter: this.legendFormatter,
|
||||
highlightCallback: this.highlightCallback,
|
||||
unhighlightCallback: this.unhighlightCallback,
|
||||
plugins: [new Dygraphs.Plugins.Crosshair({direction: 'vertical'})],
|
||||
axes: {
|
||||
y: {
|
||||
valueRange: getRange(timeSeries, y.bounds, ruleValues),
|
||||
valueRange: options.stackedGraph
|
||||
? getStackedRange(y.bounds)
|
||||
: getRange(timeSeries, y.bounds, ruleValues),
|
||||
axisLabelFormatter: (yval, __, opts) =>
|
||||
numberValueFormatter(yval, opts, y.prefix, y.suffix),
|
||||
axisLabelWidth,
|
||||
labelsKMB: y.base === '10',
|
||||
labelsKMG2: y.base === '2',
|
||||
axisLabelWidth: this.getLabelWidth(),
|
||||
labelsKMB: y.base === BASE_10,
|
||||
labelsKMG2: y.base === BASE_2,
|
||||
},
|
||||
y2: {
|
||||
valueRange: getRange(timeSeries, y2.bounds),
|
||||
},
|
||||
},
|
||||
highlightSeriesOpts: {
|
||||
strokeWidth: 2,
|
||||
highlightCircleSize: isBarGraph ? 0 : 5,
|
||||
},
|
||||
legendFormatter: legend => {
|
||||
if (!legend.x) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const {state: {legend: prevLegend}} = this
|
||||
const highlighted = legend.series.find(s => s.isHighlighted)
|
||||
const prevHighlighted = prevLegend.series.find(s => s.isHighlighted)
|
||||
|
||||
const yVal = highlighted && highlighted.y
|
||||
const prevY = prevHighlighted && prevHighlighted.y
|
||||
|
||||
if (legend.x === prevLegend.x && yVal === prevY) {
|
||||
return ''
|
||||
}
|
||||
|
||||
this.setState({legend})
|
||||
return ''
|
||||
},
|
||||
highlightCallback: e => {
|
||||
// Move the Legend on hover
|
||||
const graphRect = graphRef.getBoundingClientRect()
|
||||
const legendRect = legendRef.getBoundingClientRect()
|
||||
|
||||
const graphWidth = graphRect.width + 32 // Factoring in padding from parent
|
||||
const graphHeight = graphRect.height
|
||||
const graphBottom = graphRect.bottom
|
||||
const legendWidth = legendRect.width
|
||||
const legendHeight = legendRect.height
|
||||
const screenHeight = window.innerHeight
|
||||
const legendMaxLeft = graphWidth - legendWidth / 2
|
||||
const trueGraphX = e.pageX - graphRect.left
|
||||
|
||||
let legendLeft = trueGraphX
|
||||
|
||||
// Enforcing max & min legend offsets
|
||||
if (trueGraphX < legendWidth / 2) {
|
||||
legendLeft = legendWidth / 2
|
||||
} else if (trueGraphX > legendMaxLeft) {
|
||||
legendLeft = legendMaxLeft
|
||||
}
|
||||
|
||||
// Disallow screen overflow of legend
|
||||
const isLegendBottomClipped = graphBottom + legendHeight > screenHeight
|
||||
|
||||
const legendTop = isLegendBottomClipped
|
||||
? graphHeight + 8 - legendHeight
|
||||
: graphHeight + 8
|
||||
|
||||
legendRef.style.left = `${legendLeft}px`
|
||||
legendRef.style.top = `${legendTop}px`
|
||||
|
||||
this.setState({isHidden: false})
|
||||
},
|
||||
unhighlightCallback: e => {
|
||||
const {top, bottom, left, right} = legendRef.getBoundingClientRect()
|
||||
|
||||
const mouseY = e.clientY
|
||||
const mouseX = e.clientX
|
||||
|
||||
const mouseInLegendY = mouseY <= bottom && mouseY >= top
|
||||
const mouseInLegendX = mouseX <= right && mouseX >= left
|
||||
const isMouseHoveringLegend = mouseInLegendY && mouseInLegendX
|
||||
|
||||
if (!isMouseHoveringLegend) {
|
||||
this.setState({isHidden: true})
|
||||
|
||||
if (!this.visibility().find(bool => bool === true)) {
|
||||
this.setState({filterText: ''})
|
||||
}
|
||||
}
|
||||
},
|
||||
highlightSeriesOpts,
|
||||
zoomCallback: (lower, upper) => this.handleZoom(lower, upper),
|
||||
}
|
||||
|
||||
if (isBarGraph) {
|
||||
defaultOptions.plotter = multiColumnBarPlotter
|
||||
defaultOptions = {
|
||||
...defaultOptions,
|
||||
plotter: barPlotter,
|
||||
plugins: [],
|
||||
highlightSeriesOpts: {
|
||||
...highlightSeriesOpts,
|
||||
highlightCircleSize: 0,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
this.dygraph = new Dygraphs(graphRef, timeSeries, {
|
||||
...defaultOptions,
|
||||
...options,
|
||||
...OPTIONS,
|
||||
})
|
||||
|
||||
const {w} = this.dygraph.getArea()
|
||||
|
@ -227,15 +130,7 @@ export default class Dygraph extends Component {
|
|||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
const {
|
||||
labels,
|
||||
axes: {y, y2},
|
||||
options,
|
||||
dygraphSeries,
|
||||
ruleValues,
|
||||
isBarGraph,
|
||||
overrideLineColors,
|
||||
} = this.props
|
||||
const {labels, axes: {y, y2}, options, ruleValues, isBarGraph} = this.props
|
||||
|
||||
const dygraph = this.dygraph
|
||||
if (!dygraph) {
|
||||
|
@ -245,48 +140,31 @@ export default class Dygraph extends Component {
|
|||
}
|
||||
|
||||
const timeSeries = this.getTimeSeries()
|
||||
const ylabel = this.getLabel('y')
|
||||
const finalLineColors = [...(overrideLineColors || LINE_COLORS)]
|
||||
|
||||
const hashColorDygraphSeries = {}
|
||||
const {length} = finalLineColors
|
||||
|
||||
for (const seriesName in dygraphSeries) {
|
||||
const series = dygraphSeries[seriesName]
|
||||
const hashIndex = hasherino(seriesName, length)
|
||||
const color = finalLineColors[hashIndex]
|
||||
hashColorDygraphSeries[seriesName] = {...series, color}
|
||||
}
|
||||
|
||||
const axisLabelWidth =
|
||||
labelWidth +
|
||||
y.prefix.length * avgCharPixels +
|
||||
y.suffix.length * avgCharPixels
|
||||
|
||||
const updateOptions = {
|
||||
...options,
|
||||
labels,
|
||||
file: timeSeries,
|
||||
ylabel,
|
||||
logscale: y.scale === LOG,
|
||||
ylabel: this.getLabel('y'),
|
||||
axes: {
|
||||
y: {
|
||||
valueRange: getRange(timeSeries, y.bounds, ruleValues),
|
||||
valueRange: options.stackedGraph
|
||||
? getStackedRange(y.bounds)
|
||||
: getRange(timeSeries, y.bounds, ruleValues),
|
||||
axisLabelFormatter: (yval, __, opts) =>
|
||||
numberValueFormatter(yval, opts, y.prefix, y.suffix),
|
||||
axisLabelWidth,
|
||||
labelsKMB: y.base === '10',
|
||||
labelsKMG2: y.base === '2',
|
||||
axisLabelWidth: this.getLabelWidth(),
|
||||
labelsKMB: y.base === BASE_10,
|
||||
labelsKMG2: y.base === BASE_2,
|
||||
},
|
||||
y2: {
|
||||
valueRange: getRange(timeSeries, y2.bounds),
|
||||
},
|
||||
},
|
||||
stepPlot: options.stepPlot,
|
||||
stackedGraph: options.stackedGraph,
|
||||
underlayCallback: options.underlayCallback,
|
||||
colors: finalLineColors,
|
||||
series: hashColorDygraphSeries,
|
||||
plotter: isBarGraph ? multiColumnBarPlotter : null,
|
||||
colors: this.getLineColors(),
|
||||
series: this.hashColorDygraphSeries(),
|
||||
plotter: isBarGraph ? barPlotter : null,
|
||||
visibility: this.visibility(),
|
||||
}
|
||||
|
||||
|
@ -298,6 +176,31 @@ export default class Dygraph extends Component {
|
|||
this.props.setResolution(w)
|
||||
}
|
||||
|
||||
handleZoom = (lower, upper) => {
|
||||
const {onZoom} = this.props
|
||||
|
||||
if (this.dygraph.isZoomed() === false) {
|
||||
return onZoom(null, null)
|
||||
}
|
||||
|
||||
onZoom(this.formatTimeRange(lower), this.formatTimeRange(upper))
|
||||
}
|
||||
|
||||
hashColorDygraphSeries = () => {
|
||||
const {dygraphSeries} = this.props
|
||||
const colors = this.getLineColors()
|
||||
const hashColorDygraphSeries = {}
|
||||
|
||||
for (const seriesName in dygraphSeries) {
|
||||
const series = dygraphSeries[seriesName]
|
||||
const hashIndex = hasherino(seriesName, colors.length)
|
||||
const color = colors[hashIndex]
|
||||
hashColorDygraphSeries[seriesName] = {...series, color}
|
||||
}
|
||||
|
||||
return hashColorDygraphSeries
|
||||
}
|
||||
|
||||
sync = () => {
|
||||
if (!this.state.isSynced) {
|
||||
this.props.synchronizer(this.dygraph)
|
||||
|
@ -342,6 +245,19 @@ export default class Dygraph extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
getLineColors = () => {
|
||||
return [...(this.props.overrideLineColors || LINE_COLORS)]
|
||||
}
|
||||
|
||||
getLabelWidth = () => {
|
||||
const {axes: {y}} = this.props
|
||||
return (
|
||||
LABEL_WIDTH +
|
||||
y.prefix.length * CHAR_PIXELS +
|
||||
y.suffix.length * CHAR_PIXELS
|
||||
)
|
||||
}
|
||||
|
||||
visibility = () => {
|
||||
const timeSeries = this.getTimeSeries()
|
||||
const {filterText, legend} = this.state
|
||||
|
@ -384,6 +300,103 @@ export default class Dygraph extends Component {
|
|||
this.dygraph.predraw_()
|
||||
}
|
||||
|
||||
formatTimeRange = timeRange => {
|
||||
if (!timeRange) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return moment(timeRange).utc().format()
|
||||
}
|
||||
|
||||
deselectCrosshair = () => {
|
||||
const plugins = this.dygraph.plugins_
|
||||
const crosshair = plugins.find(
|
||||
({plugin}) => plugin.toString() === 'Crosshair Plugin'
|
||||
)
|
||||
|
||||
if (!crosshair || this.props.isBarGraph) {
|
||||
return
|
||||
}
|
||||
|
||||
crosshair.plugin.deselect()
|
||||
}
|
||||
|
||||
unhighlightCallback = e => {
|
||||
const {top, bottom, left, right} = this.legendRef.getBoundingClientRect()
|
||||
|
||||
const mouseY = e.clientY
|
||||
const mouseX = e.clientX
|
||||
|
||||
const mouseBuffer = 5
|
||||
const mouseInLegendY = mouseY <= bottom && mouseY >= top - mouseBuffer
|
||||
const mouseInLegendX = mouseX <= right && mouseX >= left
|
||||
const isMouseHoveringLegend = mouseInLegendY && mouseInLegendX
|
||||
|
||||
if (!isMouseHoveringLegend) {
|
||||
this.setState({isHidden: true})
|
||||
|
||||
if (!this.visibility().find(bool => bool === true)) {
|
||||
this.setState({filterText: ''})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
highlightCallback = e => {
|
||||
// Move the Legend on hover
|
||||
const graphRect = this.graphRef.getBoundingClientRect()
|
||||
const legendRect = this.legendRef.getBoundingClientRect()
|
||||
|
||||
const graphWidth = graphRect.width + 32 // Factoring in padding from parent
|
||||
const graphHeight = graphRect.height
|
||||
const graphBottom = graphRect.bottom
|
||||
const legendWidth = legendRect.width
|
||||
const legendHeight = legendRect.height
|
||||
const screenHeight = window.innerHeight
|
||||
const legendMaxLeft = graphWidth - legendWidth / 2
|
||||
const trueGraphX = e.pageX - graphRect.left
|
||||
|
||||
let legendLeft = trueGraphX
|
||||
|
||||
// Enforcing max & min legend offsets
|
||||
if (trueGraphX < legendWidth / 2) {
|
||||
legendLeft = legendWidth / 2
|
||||
} else if (trueGraphX > legendMaxLeft) {
|
||||
legendLeft = legendMaxLeft
|
||||
}
|
||||
|
||||
// Disallow screen overflow of legend
|
||||
const isLegendBottomClipped = graphBottom + legendHeight > screenHeight
|
||||
|
||||
const legendTop = isLegendBottomClipped
|
||||
? graphHeight + 8 - legendHeight
|
||||
: graphHeight + 8
|
||||
|
||||
this.legendRef.style.left = `${legendLeft}px`
|
||||
this.legendRef.style.top = `${legendTop}px`
|
||||
|
||||
this.setState({isHidden: false})
|
||||
}
|
||||
|
||||
legendFormatter = legend => {
|
||||
if (!legend.x) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const {state: {legend: prevLegend}} = this
|
||||
const highlighted = legend.series.find(s => s.isHighlighted)
|
||||
const prevHighlighted = prevLegend.series.find(s => s.isHighlighted)
|
||||
|
||||
const yVal = highlighted && highlighted.y
|
||||
const prevY = prevHighlighted && prevHighlighted.y
|
||||
|
||||
if (legend.x === prevLegend.x && yVal === prevY) {
|
||||
return ''
|
||||
}
|
||||
|
||||
this.setState({legend})
|
||||
return ''
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
legend,
|
||||
|
@ -396,7 +409,7 @@ export default class Dygraph extends Component {
|
|||
} = this.state
|
||||
|
||||
return (
|
||||
<div className="dygraph-child">
|
||||
<div className="dygraph-child" onMouseLeave={this.deselectCrosshair}>
|
||||
<DygraphLegend
|
||||
{...legend}
|
||||
sortType={sortType}
|
||||
|
@ -446,6 +459,7 @@ Dygraph.defaultProps = {
|
|||
isGraphFilled: true,
|
||||
overrideLineColors: null,
|
||||
dygraphRef: () => {},
|
||||
onZoom: () => {},
|
||||
}
|
||||
|
||||
Dygraph.propTypes = {
|
||||
|
@ -477,4 +491,5 @@ Dygraph.propTypes = {
|
|||
synchronizer: func,
|
||||
setResolution: func,
|
||||
dygraphRef: func,
|
||||
onZoom: func,
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
import React, {PropTypes, Component} from 'react'
|
||||
|
||||
import FieldListItem from 'src/data_explorer/components/FieldListItem'
|
||||
import GroupByTimeDropdown from 'src/data_explorer/components/GroupByTimeDropdown'
|
||||
|
@ -8,42 +8,13 @@ import FancyScrollbar from 'shared/components/FancyScrollbar'
|
|||
import {showFieldKeys} from 'shared/apis/metaQuery'
|
||||
import showFieldKeysParser from 'shared/parsing/showFieldKeys'
|
||||
|
||||
const {bool, func, shape, string} = PropTypes
|
||||
|
||||
const FieldList = React.createClass({
|
||||
propTypes: {
|
||||
query: shape({
|
||||
database: string,
|
||||
retentionPolicy: string,
|
||||
measurement: string,
|
||||
}).isRequired,
|
||||
onToggleField: func.isRequired,
|
||||
onGroupByTime: func.isRequired,
|
||||
onFill: func.isRequired,
|
||||
applyFuncsToField: func.isRequired,
|
||||
isKapacitorRule: bool,
|
||||
isInDataExplorer: bool,
|
||||
},
|
||||
|
||||
getDefaultProps() {
|
||||
return {
|
||||
isKapacitorRule: false,
|
||||
}
|
||||
},
|
||||
|
||||
contextTypes: {
|
||||
source: shape({
|
||||
links: shape({
|
||||
proxy: string.isRequired,
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
},
|
||||
|
||||
getInitialState() {
|
||||
return {
|
||||
class FieldList extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
fields: [],
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const {database, measurement} = this.props.query
|
||||
|
@ -52,7 +23,7 @@ const FieldList = React.createClass({
|
|||
}
|
||||
|
||||
this._getFields()
|
||||
},
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {database, measurement, retentionPolicy} = this.props.query
|
||||
|
@ -74,20 +45,47 @@ const FieldList = React.createClass({
|
|||
}
|
||||
|
||||
this._getFields()
|
||||
},
|
||||
}
|
||||
|
||||
handleFill(value) {
|
||||
this.props.onFill(value)
|
||||
},
|
||||
|
||||
handleGroupByTime(groupBy) {
|
||||
handleGroupByTime = groupBy => {
|
||||
this.props.onGroupByTime(groupBy.menuOption)
|
||||
},
|
||||
}
|
||||
|
||||
handleFill = fill => {
|
||||
this.props.onFill(fill)
|
||||
}
|
||||
|
||||
_getFields = () => {
|
||||
const {database, measurement, retentionPolicy} = this.props.query
|
||||
const {source} = this.context
|
||||
const proxySource = source.links.proxy
|
||||
|
||||
showFieldKeys(
|
||||
proxySource,
|
||||
database,
|
||||
measurement,
|
||||
retentionPolicy
|
||||
).then(resp => {
|
||||
const {errors, fieldSets} = showFieldKeysParser(resp.data)
|
||||
if (errors.length) {
|
||||
console.error('Error parsing fields keys: ', errors)
|
||||
}
|
||||
|
||||
this.setState({
|
||||
fields: fieldSets[measurement].map(f => ({field: f, funcs: []})),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
const {query, isKapacitorRule, isInDataExplorer} = this.props
|
||||
const hasAggregates = query.fields.some(f => f.funcs && f.funcs.length)
|
||||
const hasGroupByTime = query.groupBy.time
|
||||
const {
|
||||
query: {fields = [], groupBy, fill},
|
||||
isKapacitorRule,
|
||||
isInDataExplorer,
|
||||
} = this.props
|
||||
|
||||
const hasAggregates = fields.some(f => f.funcs && f.funcs.length)
|
||||
const hasGroupByTime = groupBy.time
|
||||
|
||||
return (
|
||||
<div className="query-builder--column">
|
||||
|
@ -97,27 +95,24 @@ const FieldList = React.createClass({
|
|||
? <div style={{display: 'flex', alignItems: 'center'}}>
|
||||
<GroupByTimeDropdown
|
||||
isOpen={!hasGroupByTime}
|
||||
selected={query.groupBy.time}
|
||||
selected={groupBy.time}
|
||||
onChooseGroupByTime={this.handleGroupByTime}
|
||||
isInRuleBuilder={isKapacitorRule}
|
||||
isInDataExplorer={isInDataExplorer}
|
||||
/>
|
||||
{isKapacitorRule
|
||||
? null
|
||||
: <FillQuery
|
||||
value={query.fill}
|
||||
onSelection={this.handleFill}
|
||||
/>}
|
||||
: <FillQuery value={fill} onSelection={this.handleFill} />}
|
||||
</div>
|
||||
: null}
|
||||
</div>
|
||||
{this.renderList()}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
renderList() {
|
||||
const {database, measurement} = this.props.query
|
||||
const {database, measurement, fields = []} = this.props.query
|
||||
if (!database || !measurement) {
|
||||
return (
|
||||
<div className="query-builder--list-empty">
|
||||
|
@ -132,9 +127,7 @@ const FieldList = React.createClass({
|
|||
<div className="query-builder--list">
|
||||
<FancyScrollbar>
|
||||
{this.state.fields.map(fieldFunc => {
|
||||
const selectedField = this.props.query.fields.find(
|
||||
f => f.field === fieldFunc.field
|
||||
)
|
||||
const selectedField = fields.find(f => f.field === fieldFunc.field)
|
||||
return (
|
||||
<FieldListItem
|
||||
key={fieldFunc.field}
|
||||
|
@ -149,31 +142,35 @@ const FieldList = React.createClass({
|
|||
</FancyScrollbar>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
_getFields() {
|
||||
const {database, measurement, retentionPolicy} = this.props.query
|
||||
const {source} = this.context
|
||||
const proxySource = source.links.proxy
|
||||
const {bool, func, shape, string} = PropTypes
|
||||
|
||||
showFieldKeys(
|
||||
proxySource,
|
||||
database,
|
||||
measurement,
|
||||
retentionPolicy
|
||||
).then(resp => {
|
||||
const {errors, fieldSets} = showFieldKeysParser(resp.data)
|
||||
if (errors.length) {
|
||||
// TODO: do something
|
||||
}
|
||||
FieldList.defaultProps = {
|
||||
isKapacitorRule: false,
|
||||
}
|
||||
|
||||
this.setState({
|
||||
fields: fieldSets[measurement].map(f => {
|
||||
return {field: f, funcs: []}
|
||||
}),
|
||||
})
|
||||
})
|
||||
},
|
||||
})
|
||||
FieldList.contextTypes = {
|
||||
source: shape({
|
||||
links: shape({
|
||||
proxy: string.isRequired,
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
}
|
||||
|
||||
FieldList.propTypes = {
|
||||
query: shape({
|
||||
database: string,
|
||||
retentionPolicy: string,
|
||||
measurement: string,
|
||||
}).isRequired,
|
||||
onToggleField: func.isRequired,
|
||||
onGroupByTime: func.isRequired,
|
||||
onFill: func.isRequired,
|
||||
applyFuncsToField: func.isRequired,
|
||||
isKapacitorRule: bool,
|
||||
isInDataExplorer: bool,
|
||||
}
|
||||
|
||||
export default FieldList
|
||||
|
|
|
@ -148,6 +148,7 @@ class LayoutRenderer extends Component {
|
|||
templates,
|
||||
synchronizer,
|
||||
isEditable,
|
||||
onZoom,
|
||||
} = this.props
|
||||
|
||||
return cells.map(cell => {
|
||||
|
@ -176,6 +177,7 @@ class LayoutRenderer extends Component {
|
|||
queries={this.standardizeQueries(cell, source)}
|
||||
cellHeight={h}
|
||||
axes={axes}
|
||||
onZoom={onZoom}
|
||||
/>}
|
||||
</NameableGraph>
|
||||
</div>
|
||||
|
@ -303,6 +305,7 @@ LayoutRenderer.propTypes = {
|
|||
isStatusPage: bool,
|
||||
isEditable: bool,
|
||||
onCancelEditCell: func,
|
||||
onZoom: func,
|
||||
}
|
||||
|
||||
export default LayoutRenderer
|
||||
|
|
|
@ -46,6 +46,7 @@ export default React.createClass({
|
|||
synchronizer: func,
|
||||
setResolution: func,
|
||||
cellHeight: number,
|
||||
onZoom: func,
|
||||
},
|
||||
|
||||
getDefaultProps() {
|
||||
|
@ -101,6 +102,7 @@ export default React.createClass({
|
|||
synchronizer,
|
||||
timeRange,
|
||||
cellHeight,
|
||||
onZoom,
|
||||
} = this.props
|
||||
const {labels, timeSeries, dygraphSeries} = this._timeSeries
|
||||
|
||||
|
@ -176,6 +178,7 @@ export default React.createClass({
|
|||
synchronizer={synchronizer}
|
||||
timeRange={timeRange}
|
||||
setResolution={this.props.setResolution}
|
||||
onZoom={onZoom}
|
||||
/>
|
||||
{showSingleStat
|
||||
? <div className="single-stat single-stat-line">
|
||||
|
|
|
@ -10,6 +10,7 @@ const RefreshingSingleStat = AutoRefresh(SingleStat)
|
|||
const RefreshingGraph = ({
|
||||
axes,
|
||||
type,
|
||||
onZoom,
|
||||
queries,
|
||||
templates,
|
||||
timeRange,
|
||||
|
@ -46,6 +47,7 @@ const RefreshingGraph = ({
|
|||
synchronizer={synchronizer}
|
||||
editQueryStatus={editQueryStatus}
|
||||
axes={axes}
|
||||
onZoom={onZoom}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -64,6 +66,7 @@ RefreshingGraph.propTypes = {
|
|||
axes: shape(),
|
||||
queries: arrayOf(shape()).isRequired,
|
||||
editQueryStatus: func,
|
||||
onZoom: func,
|
||||
}
|
||||
|
||||
export default RefreshingGraph
|
||||
|
|
|
@ -9,27 +9,21 @@ import CustomTimeRangeOverlay from 'shared/components/CustomTimeRangeOverlay'
|
|||
import timeRanges from 'hson!shared/data/timeRanges.hson'
|
||||
import {DROPDOWN_MENU_MAX_HEIGHT} from 'shared/constants/index'
|
||||
|
||||
const emptyTime = {lower: '', upper: ''}
|
||||
|
||||
class TimeRangeDropdown extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
const {lower, upper} = props.selected
|
||||
|
||||
super(props)
|
||||
const isTimeValid = moment(upper).isValid() && moment(lower).isValid()
|
||||
const customTimeRange = isTimeValid ? {lower, upper} : emptyTime
|
||||
|
||||
this.state = {
|
||||
autobind: false,
|
||||
isOpen: false,
|
||||
isCustomTimeRangeOpen: false,
|
||||
customTimeRange:
|
||||
moment(props.selected.upper).isValid() &&
|
||||
moment(props.selected.lower).isValid()
|
||||
? {
|
||||
lower,
|
||||
upper,
|
||||
}
|
||||
: {
|
||||
lower: '',
|
||||
upper: '',
|
||||
},
|
||||
customTimeRange,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -27,7 +27,7 @@ export const darkenColor = colorStr => {
|
|||
}
|
||||
|
||||
// Bar Graph code below is adapted from http://dygraphs.com/tests/plotters.html
|
||||
export const multiColumnBarPlotter = e => {
|
||||
export const barPlotter = e => {
|
||||
// We need to handle all the series simultaneously.
|
||||
if (e.seriesIndex !== 0) {
|
||||
return
|
||||
|
@ -99,3 +99,28 @@ export const multiColumnBarPlotter = e => {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const OPTIONS = {
|
||||
rightGap: 0,
|
||||
axisLineWidth: 2,
|
||||
gridLineWidth: 1,
|
||||
animatedZooms: true,
|
||||
labelsSeparateLines: false,
|
||||
hideOverlayOnMouseOut: false,
|
||||
highlightSeriesBackgroundAlpha: 1.0,
|
||||
highlightSeriesBackgroundColor: 'rgb(41, 41, 51)',
|
||||
}
|
||||
|
||||
export const highlightSeriesOpts = {
|
||||
strokeWidth: 2,
|
||||
highlightCircleSize: 5,
|
||||
}
|
||||
|
||||
export const hasherino = (str, len) =>
|
||||
str
|
||||
.split('')
|
||||
.map(char => char.charCodeAt(0))
|
||||
.reduce((hash, code) => hash + code, 0) % len
|
||||
|
||||
export const LABEL_WIDTH = 60
|
||||
export const CHAR_PIXELS = 7
|
||||
|
|
|
@ -79,4 +79,10 @@ const getRange = (
|
|||
return [min, max]
|
||||
}
|
||||
|
||||
const coerceToNum = str => (str ? +str : null)
|
||||
export const getStackedRange = (bounds = [null, null]) => [
|
||||
coerceToNum(bounds[0]),
|
||||
coerceToNum(bounds[1]),
|
||||
]
|
||||
|
||||
export default getRange
|
||||
|
|
|
@ -86,6 +86,7 @@ const NavBlock = React.createClass({
|
|||
{this.renderSquare()}
|
||||
<div className="sidebar-menu">
|
||||
{children}
|
||||
<div className="sidebar-menu--triangle" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -69,10 +69,14 @@ const SideNav = React.createClass({
|
|||
const dataExplorerLink = `${sourcePrefix}/chronograf/data-explorer`
|
||||
const isUsingAuth = !!logoutLink
|
||||
|
||||
const isDefaultPage = location.split('/').includes(DEFAULT_HOME_PAGE)
|
||||
|
||||
return isHidden
|
||||
? null
|
||||
: <NavBar location={location}>
|
||||
<div className="sidebar--item">
|
||||
<div
|
||||
className={isDefaultPage ? 'sidebar--item active' : 'sidebar--item'}
|
||||
>
|
||||
<Link
|
||||
to={`${sourcePrefix}/${DEFAULT_HOME_PAGE}`}
|
||||
className="sidebar--square sidebar--logo"
|
||||
|
|
|
@ -93,6 +93,7 @@ $sidebar-menu--gutter: 18px;
|
|||
*/
|
||||
.sidebar--item:hover {
|
||||
cursor: pointer;
|
||||
z-index: 5;
|
||||
|
||||
.sidebar--square {background-color: $sidebar--item-bg-hover;}
|
||||
.sidebar--icon {color: $sidebar--icon-hover;}
|
||||
|
@ -115,6 +116,16 @@ $sidebar-menu--gutter: 18px;
|
|||
background-color: $sidebar--logo-bg-hover;
|
||||
.sidebar--icon {color: $sidebar--logo-color-hover;}
|
||||
}
|
||||
.sidebar--item.active .sidebar--square.sidebar--logo {
|
||||
background-color: $sidebar--logo-bg-hover;
|
||||
.sidebar--icon {
|
||||
color: $sidebar--logo-color-hover;
|
||||
text-shadow:
|
||||
0 0 9px $c-hydrogen,
|
||||
0 0 15px $c-neutrino,
|
||||
0 0 20px $c-yeti;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Sidebar Sub Menus
|
||||
|
@ -177,3 +188,13 @@ $sidebar-menu--gutter: 18px;
|
|||
font-weight: 400;
|
||||
padding: 0px $sidebar-menu--gutter;
|
||||
}
|
||||
// Invisible triangle for easier mouse movement when navigating to sub items
|
||||
.sidebar-menu--item + .sidebar-menu--triangle {
|
||||
position: absolute;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
z-index: -1;
|
||||
top: $sidebar--width;
|
||||
left: 0px;
|
||||
transform: translate(-50%,-50%) rotate(45deg);
|
||||
}
|
||||
|
|
|
@ -27,15 +27,23 @@ export function chooseMeasurement(query, measurement, isKapacitorRule = false) {
|
|||
}
|
||||
|
||||
export const toggleField = (query, {field, funcs}, isKapacitorRule = false) => {
|
||||
const isSelected = query.fields.find(f => f.field === field)
|
||||
const {fields, groupBy} = query
|
||||
|
||||
if (!fields) {
|
||||
return {
|
||||
...query,
|
||||
fields: [{field, funcs: ['mean']}],
|
||||
}
|
||||
}
|
||||
|
||||
const isSelected = fields.find(f => f.field === field)
|
||||
if (isSelected) {
|
||||
const nextFields = query.fields.filter(f => f.field !== field)
|
||||
const nextFields = fields.filter(f => f.field !== field)
|
||||
if (!nextFields.length) {
|
||||
const nextGroupBy = {...query.groupBy, time: null}
|
||||
return {
|
||||
...query,
|
||||
fields: nextFields,
|
||||
groupBy: nextGroupBy,
|
||||
groupBy: {...groupBy, time: null},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue