diff --git a/CHANGELOG.md b/CHANGELOG.md index ce8c79611..2e2df798b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,12 @@ ## v1.3.8.0 [unreleased] ### 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 values are of constant values +1. [#1930](https://github.com/influxdata/chronograf/pull/1930): Fix graphs when y-values are constant + ### 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 ### UI Improvements @@ -19,11 +22,21 @@ 1. [#1872](https://github.com/influxdata/chronograf/pull/1872): Prevent stats in the legend from wrapping line 1. [#1899](https://github.com/influxdata/chronograf/pull/1899): Fix raw query editor in Data Explorer not using selected time 1. [#1922](https://github.com/influxdata/chronograf/pull/1922): Fix Safari display issues in the Cell Editor display options +1. [#1715](https://github.com/influxdata/chronograf/pull/1715): Chronograf now renders on IE11. +1. [#1866](https://github.com/influxdata/chronograf/pull/1866): Fix missing cell type (and consequently single-stat) +1. [#1866](https://github.com/influxdata/chronograf/pull/1866): Fix data corruption issue with dashboard graph types + **Note**: If you upgraded to 1.3.6.0 and visited any dashboard, you will need to manually reset the graph type for every cell via the cell's caret -> Edit -> Display Options. +1. [#1870](https://github.com/influxdata/chronograf/pull/1870): Fix console error for placing prop on div +1. [#1845](https://github.com/influxdata/chronograf/pull/1845): Fix inaccessible scroll bar in Data Explorer table +1. [#1866](https://github.com/influxdata/chronograf/pull/1866): Fix non-persistence of dashboard graph types +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 1. [#1898](https://github.com/influxdata/chronograf/pull/1898): Add an input and validation to custom time range calendar dropdowns 1. [#1904](https://github.com/influxdata/chronograf/pull/1904): Add support for selecting template variables with URL params +1. [#1859](https://github.com/influxdata/chronograf/pull/1859): Add y-axis controls to the API for layouts ### UI Improvements 1. [#1862](https://github.com/influxdata/chronograf/pull/1862): Show "Add Graph" button on cells with no queries @@ -46,11 +59,15 @@ 1. [#1798](https://github.com/influxdata/chronograf/pull/1798): Fix domain not updating in visualizations when changing time range manually 1. [#1799](https://github.com/influxdata/chronograf/pull/1799): Prevent console error spam from Dygraph's synchronize method when a dashboard has only one graph 1. [#1813](https://github.com/influxdata/chronograf/pull/1813): Guarantee UUID for each Alert Table key to prevent dropping items when keys overlap +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 properly on IE11. ### Features 1. [#1744](https://github.com/influxdata/chronograf/pull/1744): Add a few time range shortcuts to the custom time range menu 1. [#1714](https://github.com/influxdata/chronograf/pull/1714): Add ability to edit a dashboard graph's y-axis bounds 1. [#1714](https://github.com/influxdata/chronograf/pull/1714): Add ability to edit a dashboard graph's y-axis label +1. [#1744](https://github.com/influxdata/chronograf/pull/1744): Add a few pre-set time range selections to the custom time range menu-- be sure to add a sensible GROUP BY +1. [#1744](https://github.com/influxdata/chronograf/pull/1744): Add a few time range shortcuts to the custom time range menu ### UI Improvements 1. [#1796](https://github.com/influxdata/chronograf/pull/1796): Add spinner write data modal to indicate data is being written diff --git a/bolt/internal/internal.go b/bolt/internal/internal.go index fefa93e78..ca6df1827 100644 --- a/bolt/internal/internal.go +++ b/bolt/internal/internal.go @@ -203,6 +203,10 @@ func MarshalDashboard(d chronograf.Dashboard) ([]byte, error) { axes[a] = &Axis{ Bounds: r.Bounds, Label: r.Label, + Prefix: r.Prefix, + Suffix: r.Suffix, + Base: r.Base, + Scale: r.Scale, } } @@ -281,14 +285,30 @@ func UnmarshalDashboard(data []byte, d *chronograf.Dashboard) error { axes := make(map[string]chronograf.Axis, len(c.Axes)) for a, r := range c.Axes { + // axis base defaults to 10 + if r.Base == "" { + r.Base = "10" + } + + if r.Scale == "" { + r.Scale = "linear" + } + if r.Bounds != nil { axes[a] = chronograf.Axis{ Bounds: r.Bounds, Label: r.Label, + Prefix: r.Prefix, + Suffix: r.Suffix, + Base: r.Base, + Scale: r.Scale, } + } else { axes[a] = chronograf.Axis{ Bounds: []string{}, + Base: r.Base, + Scale: r.Scale, } } } diff --git a/bolt/internal/internal.pb.go b/bolt/internal/internal.pb.go index 19a6c8d44..4af4f8fd0 100644 --- a/bolt/internal/internal.pb.go +++ b/bolt/internal/internal.pb.go @@ -121,6 +121,10 @@ type Axis struct { LegacyBounds []int64 `protobuf:"varint,1,rep,name=legacyBounds" json:"legacyBounds,omitempty"` Bounds []string `protobuf:"bytes,2,rep,name=bounds" json:"bounds,omitempty"` Label string `protobuf:"bytes,3,opt,name=label,proto3" json:"label,omitempty"` + Prefix string `protobuf:"bytes,4,opt,name=prefix,proto3" json:"prefix,omitempty"` + Suffix string `protobuf:"bytes,5,opt,name=suffix,proto3" json:"suffix,omitempty"` + Base string `protobuf:"bytes,6,opt,name=base,proto3" json:"base,omitempty"` + Scale string `protobuf:"bytes,7,opt,name=scale,proto3" json:"scale,omitempty"` } func (m *Axis) Reset() { *m = Axis{} } diff --git a/bolt/internal/internal.proto b/bolt/internal/internal.proto index b146cfd0a..aa4fe470b 100644 --- a/bolt/internal/internal.proto +++ b/bolt/internal/internal.proto @@ -38,6 +38,10 @@ message Axis { repeated int64 legacyBounds = 1; // legacyBounds are an ordered 2-tuple consisting of lower and upper axis extents, respectively repeated string bounds = 2; // bounds are an arbitrary list of client-defined bounds. string label = 3; // label is a description of this axis + string prefix = 4; // specifies the prefix for axis values + string suffix = 5; // specifies the suffix for axis values + string base = 6; // defines the base for axis values + string scale = 7; // represents the magnitude of the numbers on this axis } message Template { diff --git a/bolt/internal/internal_test.go b/bolt/internal/internal_test.go index 7a53243d2..937a192d4 100644 --- a/bolt/internal/internal_test.go +++ b/bolt/internal/internal_test.go @@ -168,6 +168,10 @@ func Test_MarshalDashboard(t *testing.T) { "y": chronograf.Axis{ Bounds: []string{"0", "3", "1-7", "foo"}, Label: "foo", + Prefix: "M", + Suffix: "m", + Base: "2", + Scale: "roflscale", }, }, Type: "line", @@ -241,6 +245,8 @@ func Test_MarshalDashboard_WithLegacyBounds(t *testing.T) { Axes: map[string]chronograf.Axis{ "y": chronograf.Axis{ Bounds: []string{}, + Base: "10", + Scale: "linear", }, }, Type: "line", @@ -260,7 +266,7 @@ func Test_MarshalDashboard_WithLegacyBounds(t *testing.T) { } } -func Test_MarshalDashboard_WithNoLegacyBounds(t *testing.T) { +func Test_MarshalDashboard_WithEmptyLegacyBounds(t *testing.T) { dashboard := chronograf.Dashboard{ ID: 1, Cells: []chronograf.DashboardCell{ @@ -314,6 +320,8 @@ func Test_MarshalDashboard_WithNoLegacyBounds(t *testing.T) { Axes: map[string]chronograf.Axis{ "y": chronograf.Axis{ Bounds: []string{}, + Base: "10", + Scale: "linear", }, }, Type: "line", diff --git a/chronograf.go b/chronograf.go index 535adfe1e..af329eb8b 100644 --- a/chronograf.go +++ b/chronograf.go @@ -644,6 +644,10 @@ type Axis struct { Bounds []string `json:"bounds"` // bounds are an arbitrary list of client-defined strings that specify the viewport for a cell LegacyBounds [2]int64 `json:"-"` // legacy bounds are for testing a migration from an earlier version of axis Label string `json:"label"` // label is a description of this Axis + Prefix string `json:"prefix"` // Prefix represents a label prefix for formatting axis values + Suffix string `json:"suffix"` // Suffix represents a label suffix for formatting axis values + Base string `json:"base"` // Base represents the radix for formatting axis values + Scale string `json:"scale"` // Scale is the axis formatting scale. Supported: "log", "linear" } // DashboardCell holds visual and query information for a cell diff --git a/server/cells.go b/server/cells.go index 29d78b000..2d01b406c 100644 --- a/server/cells.go +++ b/server/cells.go @@ -76,17 +76,34 @@ func ValidDashboardCellRequest(c *chronograf.DashboardCell) error { // HasCorrectAxes verifies that only permitted axes exist within a DashboardCell func HasCorrectAxes(c *chronograf.DashboardCell) error { - for axis, _ := range c.Axes { - switch axis { - case "x", "y", "y2": - // no-op - default: + for label, axis := range c.Axes { + if !oneOf(label, "x", "y", "y2") { + return chronograf.ErrInvalidAxis + } + + if !oneOf(axis.Scale, "linear", "log", "") { + return chronograf.ErrInvalidAxis + } + + if !oneOf(axis.Base, "10", "2", "") { return chronograf.ErrInvalidAxis } } + return nil } +// oneOf reports whether a provided string is a member of a variadic list of +// valid options +func oneOf(prop string, validOpts ...string) bool { + for _, valid := range validOpts { + if prop == valid { + return true + } + } + return false +} + // CorrectWidthHeight changes the cell to have at least the // minimum width and height func CorrectWidthHeight(c *chronograf.DashboardCell) { diff --git a/server/cells_test.go b/server/cells_test.go index c0ade6f5f..12b109679 100644 --- a/server/cells_test.go +++ b/server/cells_test.go @@ -55,6 +55,78 @@ func Test_Cells_CorrectAxis(t *testing.T) { }, true, }, + { + "linear scale value", + &chronograf.DashboardCell{ + Axes: map[string]chronograf.Axis{ + "x": chronograf.Axis{ + Scale: "linear", + Bounds: []string{"0", "100"}, + }, + }, + }, + false, + }, + { + "log scale value", + &chronograf.DashboardCell{ + Axes: map[string]chronograf.Axis{ + "x": chronograf.Axis{ + Scale: "log", + Bounds: []string{"0", "100"}, + }, + }, + }, + false, + }, + { + "invalid scale value", + &chronograf.DashboardCell{ + Axes: map[string]chronograf.Axis{ + "x": chronograf.Axis{ + Scale: "potatoes", + Bounds: []string{"0", "100"}, + }, + }, + }, + true, + }, + { + "base 10 axis", + &chronograf.DashboardCell{ + Axes: map[string]chronograf.Axis{ + "x": chronograf.Axis{ + Base: "10", + Bounds: []string{"0", "100"}, + }, + }, + }, + false, + }, + { + "base 2 axis", + &chronograf.DashboardCell{ + Axes: map[string]chronograf.Axis{ + "x": chronograf.Axis{ + Base: "2", + Bounds: []string{"0", "100"}, + }, + }, + }, + false, + }, + { + "invalid base", + &chronograf.DashboardCell{ + Axes: map[string]chronograf.Axis{ + "x": chronograf.Axis{ + Base: "all your base are belong to us", + Bounds: []string{"0", "100"}, + }, + }, + }, + true, + }, } for _, test := range axisTests { diff --git a/server/swagger.json b/server/swagger.json index 8141f7142..d0a4f0de1 100644 --- a/server/swagger.json +++ b/server/swagger.json @@ -3720,13 +3720,13 @@ "type": "object", "properties": { "x": { - "$ref": "#/definitions/DashboardRange" + "$ref": "#/definitions/Axis" }, "y": { - "$ref": "#/definitions/DashboardRange" + "$ref": "#/definitions/Axis" }, "y2": { - "$ref": "#/definitions/DashboardRange" + "$ref": "#/definitions/Axis" } } }, @@ -3811,7 +3811,7 @@ } } }, - "DashboardRange": { + "Axis": { "type": "object", "description": "A description of a particular axis for a visualization", "properties": { @@ -3824,6 +3824,26 @@ "type": "integer", "format": "int64" } + }, + "label": { + "description": "label is a description of this Axis", + "type": "string" + }, + "prefix": { + "description": "Prefix represents a label prefix for formatting axis values.", + "type": "string" + }, + "suffix": { + "description": "Suffix represents a label suffix for formatting axis values.", + "type": "string" + }, + "base": { + "description": "Base represents the radix for formatting axis values.", + "type": "string" + }, + "scale": { + "description": "Scale is the axis formatting scale. Supported: \"log\", \"linear\"", + "type": "string" } } }, diff --git a/ui/src/dashboards/components/AxesOptions.js b/ui/src/dashboards/components/AxesOptions.js index aea060b67..bcdd746e1 100644 --- a/ui/src/dashboards/components/AxesOptions.js +++ b/ui/src/dashboards/components/AxesOptions.js @@ -1,19 +1,22 @@ import React, {PropTypes} from 'react' -import _ from 'lodash' import OptIn from 'shared/components/OptIn' +import Input from 'src/dashboards/components/DisplayOptionsInput' +import {Tabber, Tab} from 'src/dashboards/components/Tabber' +import {DISPLAY_OPTIONS, TOOLTIP_CONTENT} from 'src/dashboards/constants' + +const {LINEAR, LOG, BASE_2, BASE_10} = DISPLAY_OPTIONS -// TODO: add logic for for Prefix, Suffix, Scale, and Multiplier const AxesOptions = ({ + axes: {y: {bounds, label, prefix, suffix, base, scale, defaultYLabel}}, + onSetBase, + onSetScale, + onSetLabel, + onSetPrefixSuffix, onSetYAxisBoundMin, onSetYAxisBoundMax, - onSetLabel, - axes, }) => { - const min = _.get(axes, ['y', 'bounds', '0'], '') - const max = _.get(axes, ['y', 'bounds', '1'], '') - const label = _.get(axes, ['y', 'label'], '') - const defaultYLabel = _.get(axes, ['y', 'defaultYLabel'], '') + const [min, max] = bounds return (
K/M/B = Thousand / Million / Billion
K/M/G = Kilo / Mega / Giga
', +} diff --git a/ui/src/data_explorer/components/Table.js b/ui/src/data_explorer/components/Table.js index 978c99d74..88c1ca555 100644 --- a/ui/src/data_explorer/components/Table.js +++ b/ui/src/data_explorer/components/Table.js @@ -103,7 +103,7 @@ class ChronoTable extends Component { const minWidth = 70 const rowHeight = 34 const headerHeight = 30 - const stylePixelOffset = 125 + const stylePixelOffset = 130 const defaultColumnWidth = 200 const styleAdjustedHeight = height - stylePixelOffset const width = diff --git a/ui/src/shared/components/Dygraph.js b/ui/src/shared/components/Dygraph.js index ea0f7b925..627641085 100644 --- a/ui/src/shared/components/Dygraph.js +++ b/ui/src/shared/components/Dygraph.js @@ -7,9 +7,15 @@ import _ from 'lodash' 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 DygraphLegend from 'src/shared/components/DygraphLegend' 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 @@ -35,18 +41,11 @@ export default class Dygraph extends Component { } } - static defaultProps = { - containerStyle: {}, - isGraphFilled: true, - overrideLineColors: null, - dygraphRef: () => {}, - } - componentDidMount() { const timeSeries = this.getTimeSeries() // dygraphSeries is a legend label and its corresponding y-axis e.g. {legendLabel1: 'y', legendLabel2: 'y2'}; const { - axes, + axes: {y, y2}, dygraphSeries, ruleValues, overrideLineColors, @@ -69,8 +68,10 @@ export default class Dygraph extends Component { hashColorDygraphSeries[seriesName] = {...series, color} } - const yAxis = _.get(axes, ['y', 'bounds'], [null, null]) - const y2Axis = _.get(axes, ['y2', 'bounds'], undefined) + const axisLabelWidth = + labelWidth + + y.prefix.length * avgCharPixels + + y.suffix.length * avgCharPixels const defaultOptions = { plugins: isBarGraph @@ -80,6 +81,7 @@ export default class Dygraph extends Component { direction: 'vertical', }), ], + logscale: y.scale === LOG, labelsSeparateLines: false, labelsKMB: true, rightGap: 0, @@ -95,10 +97,15 @@ export default class Dygraph extends Component { series: hashColorDygraphSeries, axes: { y: { - valueRange: getRange(timeSeries, yAxis, ruleValues), + valueRange: getRange(timeSeries, y.bounds, ruleValues), + axisLabelFormatter: (yval, __, opts) => + numberValueFormatter(yval, opts, y.prefix, y.suffix), + axisLabelWidth, + labelsKMB: y.base === '10', + labelsKMG2: y.base === '2', }, y2: { - valueRange: getRange(timeSeries, y2Axis), + valueRange: getRange(timeSeries, y2.bounds), }, }, highlightSeriesOpts: { @@ -114,10 +121,10 @@ export default class Dygraph extends Component { const highlighted = legend.series.find(s => s.isHighlighted) const prevHighlighted = prevLegend.series.find(s => s.isHighlighted) - const y = highlighted && highlighted.y + const yVal = highlighted && highlighted.y const prevY = prevHighlighted && prevHighlighted.y - if (legend.x === prevLegend.x && y === prevY) { + if (legend.x === prevLegend.x && yVal === prevY) { return '' } @@ -222,7 +229,7 @@ export default class Dygraph extends Component { componentDidUpdate() { const { labels, - axes, + axes: {y, y2}, options, dygraphSeries, ruleValues, @@ -237,8 +244,6 @@ export default class Dygraph extends Component { ) } - const y = _.get(axes, ['y', 'bounds'], [null, null]) - const y2 = _.get(axes, ['y2', 'bounds'], undefined) const timeSeries = this.getTimeSeries() const ylabel = this.getLabel('y') const finalLineColors = [...(overrideLineColors || LINE_COLORS)] @@ -253,16 +258,27 @@ export default class Dygraph extends Component { hashColorDygraphSeries[seriesName] = {...series, color} } + const axisLabelWidth = + labelWidth + + y.prefix.length * avgCharPixels + + y.suffix.length * avgCharPixels + const updateOptions = { labels, file: timeSeries, ylabel, + logscale: y.scale === LOG, axes: { y: { - valueRange: getRange(timeSeries, y, ruleValues), + valueRange: getRange(timeSeries, y.bounds, ruleValues), + axisLabelFormatter: (yval, __, opts) => + numberValueFormatter(yval, opts, y.prefix, y.suffix), + axisLabelWidth, + labelsKMB: y.base === '10', + labelsKMG2: y.base === '2', }, y2: { - valueRange: getRange(timeSeries, y2), + valueRange: getRange(timeSeries, y2.bounds), }, }, stepPlot: options.stepPlot, @@ -275,8 +291,10 @@ export default class Dygraph extends Component { } dygraph.updateOptions(updateOptions) - dygraph.resize() + const {w} = this.dygraph.getArea() + this.resize() + this.dygraph.resize() this.props.setResolution(w) } @@ -361,6 +379,11 @@ export default class Dygraph extends Component { handleLegendRef = el => (this.legendRef = el) + resize = () => { + this.dygraph.resizeElements_() + this.dygraph.predraw_() + } + render() { const { legend, @@ -386,16 +409,16 @@ export default class Dygraph extends Component { onSnip={this.handleSnipLabel} onSort={this.handleSortLegend} legendRef={this.handleLegendRef} - onInputChange={this.handleLegendInputChange} onToggleFilter={this.handleToggleFilter} + onInputChange={this.handleLegendInputChange} />