Merge remote-tracking branch 'origin/master' into fill-query-widget

pull/1885/head
Jared Scheib 2017-09-05 10:52:41 -04:00
commit 1371c6afad
22 changed files with 460 additions and 303 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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}"`,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -86,6 +86,7 @@ const NavBlock = React.createClass({
{this.renderSquare()}
<div className="sidebar-menu">
{children}
<div className="sidebar-menu--triangle" />
</div>
</div>
)

View File

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

View File

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

View File

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