Merge branch 'master' into bugfix/same-range
commit
73db221798
19
CHANGELOG.md
19
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
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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{} }
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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 (
|
||||
<div className="display-options--cell">
|
||||
|
@ -46,38 +49,48 @@ const AxesOptions = ({
|
|||
type="number"
|
||||
/>
|
||||
</div>
|
||||
{/* <div className="form-group col-sm-6">
|
||||
<label htmlFor="prefix">Labels Prefix</label>
|
||||
<input
|
||||
className="form-control input-sm"
|
||||
type="text"
|
||||
name="prefix"
|
||||
id="prefix"
|
||||
<Input
|
||||
name="prefix"
|
||||
id="prefix"
|
||||
value={prefix}
|
||||
labelText="Y-Value's Prefix"
|
||||
onChange={onSetPrefixSuffix}
|
||||
/>
|
||||
<Input
|
||||
name="suffix"
|
||||
id="suffix"
|
||||
value={suffix}
|
||||
labelText="Y-Value's Suffix"
|
||||
onChange={onSetPrefixSuffix}
|
||||
/>
|
||||
<Tabber
|
||||
labelText="Y-Value's Format"
|
||||
tipID="Y-Values's Format"
|
||||
tipContent={TOOLTIP_CONTENT.FORMAT}
|
||||
>
|
||||
<Tab
|
||||
text="K/M/B"
|
||||
isActive={base === BASE_10}
|
||||
onClickTab={onSetBase(BASE_10)}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group col-sm-6">
|
||||
<label htmlFor="prefix">Labels Suffix</label>
|
||||
<input
|
||||
className="form-control input-sm"
|
||||
type="text"
|
||||
name="suffix"
|
||||
id="suffix"
|
||||
<Tab
|
||||
text="K/M/G"
|
||||
isActive={base === BASE_2}
|
||||
onClickTab={onSetBase(BASE_2)}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group col-sm-6">
|
||||
<label>Labels Format</label>
|
||||
<ul className="nav nav-tablist nav-tablist-sm">
|
||||
<li className="active">K/M/B</li>
|
||||
<li>K/M/G</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="form-group col-sm-6">
|
||||
<label>Scale</label>
|
||||
<ul className="nav nav-tablist nav-tablist-sm">
|
||||
<li className="active">Linear</li>
|
||||
<li>Logarithmic</li>
|
||||
</ul>
|
||||
</div> */}
|
||||
</Tabber>
|
||||
<Tabber labelText="Scale">
|
||||
<Tab
|
||||
text="Linear"
|
||||
isActive={scale === LINEAR}
|
||||
onClickTab={onSetScale(LINEAR)}
|
||||
/>
|
||||
<Tab
|
||||
text="Logarithmic"
|
||||
isActive={scale === LOG}
|
||||
onClickTab={onSetScale(LOG)}
|
||||
/>
|
||||
</Tabber>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
|
@ -85,10 +98,26 @@ const AxesOptions = ({
|
|||
|
||||
const {arrayOf, func, shape, string} = PropTypes
|
||||
|
||||
AxesOptions.defaultProps = {
|
||||
axes: {
|
||||
y: {
|
||||
bounds: ['', ''],
|
||||
prefix: '',
|
||||
suffix: '',
|
||||
base: BASE_10,
|
||||
scale: LINEAR,
|
||||
defaultYLabel: '',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
AxesOptions.propTypes = {
|
||||
onSetPrefixSuffix: func.isRequired,
|
||||
onSetYAxisBoundMin: func.isRequired,
|
||||
onSetYAxisBoundMax: func.isRequired,
|
||||
onSetLabel: func.isRequired,
|
||||
onSetScale: func.isRequired,
|
||||
onSetBase: func.isRequired,
|
||||
axes: shape({
|
||||
y: shape({
|
||||
bounds: arrayOf(string),
|
||||
|
|
|
@ -88,6 +88,22 @@ class CellEditorOverlay extends Component {
|
|||
this.setState({axes: {...axes, y: {...axes.y, label}}})
|
||||
}
|
||||
|
||||
handleSetPrefixSuffix = e => {
|
||||
const {axes} = this.state
|
||||
const {prefix, suffix} = e.target.form
|
||||
|
||||
this.setState({
|
||||
axes: {
|
||||
...axes,
|
||||
y: {
|
||||
...axes.y,
|
||||
prefix: prefix.value,
|
||||
suffix: suffix.value,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
handleAddQuery = () => {
|
||||
const {queriesWorkingDraft} = this.state
|
||||
const newIndex = queriesWorkingDraft.length
|
||||
|
@ -149,6 +165,34 @@ class CellEditorOverlay extends Component {
|
|||
this.setState({activeQueryIndex})
|
||||
}
|
||||
|
||||
handleSetBase = base => () => {
|
||||
const {axes} = this.state
|
||||
|
||||
this.setState({
|
||||
axes: {
|
||||
...axes,
|
||||
y: {
|
||||
...axes.y,
|
||||
base,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
handleSetScale = scale => () => {
|
||||
const {axes} = this.state
|
||||
|
||||
this.setState({
|
||||
axes: {
|
||||
...axes,
|
||||
y: {
|
||||
...axes.y,
|
||||
scale,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
getActiveQuery = () => {
|
||||
const {queriesWorkingDraft, activeQueryIndex} = this.state
|
||||
const activeQuery = queriesWorkingDraft[activeQueryIndex]
|
||||
|
@ -230,13 +274,16 @@ class CellEditorOverlay extends Component {
|
|||
/>
|
||||
{isDisplayOptionsTabActive
|
||||
? <DisplayOptions
|
||||
axes={axes}
|
||||
onSetBase={this.handleSetBase}
|
||||
onSetLabel={this.handleSetLabel}
|
||||
onSetScale={this.handleSetScale}
|
||||
queryConfigs={queriesWorkingDraft}
|
||||
selectedGraphType={cellWorkingType}
|
||||
onSetPrefixSuffix={this.handleSetPrefixSuffix}
|
||||
onSelectGraphType={this.handleSelectGraphType}
|
||||
onSetYAxisBoundMin={this.handleSetYAxisBoundMin}
|
||||
onSetYAxisBoundMax={this.handleSetYAxisBoundMax}
|
||||
onSetLabel={this.handleSetLabel}
|
||||
axes={axes}
|
||||
queryConfigs={queriesWorkingDraft}
|
||||
/>
|
||||
: <QueryMaker
|
||||
source={source}
|
||||
|
|
|
@ -33,9 +33,12 @@ class DisplayOptions extends Component {
|
|||
|
||||
render() {
|
||||
const {
|
||||
onSetBase,
|
||||
onSetScale,
|
||||
onSetLabel,
|
||||
selectedGraphType,
|
||||
onSelectGraphType,
|
||||
onSetLabel,
|
||||
onSetPrefixSuffix,
|
||||
onSetYAxisBoundMin,
|
||||
onSetYAxisBoundMax,
|
||||
} = this.props
|
||||
|
@ -43,16 +46,19 @@ class DisplayOptions extends Component {
|
|||
|
||||
return (
|
||||
<div className="display-options">
|
||||
<AxesOptions
|
||||
axes={axes}
|
||||
onSetBase={onSetBase}
|
||||
onSetLabel={onSetLabel}
|
||||
onSetScale={onSetScale}
|
||||
onSetPrefixSuffix={onSetPrefixSuffix}
|
||||
onSetYAxisBoundMin={onSetYAxisBoundMin}
|
||||
onSetYAxisBoundMax={onSetYAxisBoundMax}
|
||||
/>
|
||||
<GraphTypeSelector
|
||||
selectedGraphType={selectedGraphType}
|
||||
onSelectGraphType={onSelectGraphType}
|
||||
/>
|
||||
<AxesOptions
|
||||
onSetLabel={onSetLabel}
|
||||
onSetYAxisBoundMin={onSetYAxisBoundMin}
|
||||
onSetYAxisBoundMax={onSetYAxisBoundMax}
|
||||
axes={axes}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -62,9 +68,12 @@ const {arrayOf, func, shape, string} = PropTypes
|
|||
DisplayOptions.propTypes = {
|
||||
selectedGraphType: string.isRequired,
|
||||
onSelectGraphType: func.isRequired,
|
||||
onSetPrefixSuffix: func.isRequired,
|
||||
onSetYAxisBoundMin: func.isRequired,
|
||||
onSetYAxisBoundMax: func.isRequired,
|
||||
onSetScale: func.isRequired,
|
||||
onSetLabel: func.isRequired,
|
||||
onSetBase: func.isRequired,
|
||||
axes: shape({}).isRequired,
|
||||
queryConfigs: arrayOf(shape()).isRequired,
|
||||
}
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
|
||||
const DisplayOptionsInput = ({id, name, value, onChange, labelText}) =>
|
||||
<div className="form-group col-sm-6">
|
||||
<label htmlFor={name}>
|
||||
{labelText}
|
||||
</label>
|
||||
<input
|
||||
className="form-control input-sm"
|
||||
type="text"
|
||||
name={name}
|
||||
id={id}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
const {func, string} = PropTypes
|
||||
|
||||
DisplayOptionsInput.defaultProps = {
|
||||
value: '',
|
||||
}
|
||||
|
||||
DisplayOptionsInput.propTypes = {
|
||||
name: string.isRequired,
|
||||
id: string.isRequired,
|
||||
value: string.isRequired,
|
||||
onChange: func.isRequired,
|
||||
labelText: string,
|
||||
}
|
||||
|
||||
export default DisplayOptionsInput
|
|
@ -0,0 +1,35 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
import QuestionMarkTooltip from 'src/shared/components/QuestionMarkTooltip'
|
||||
|
||||
export const Tabber = ({labelText, children, tipID, tipContent}) =>
|
||||
<div className="form-group col-sm-6">
|
||||
<label>
|
||||
{labelText}
|
||||
{tipID
|
||||
? <QuestionMarkTooltip tipID={tipID} tipContent={tipContent} />
|
||||
: null}
|
||||
</label>
|
||||
<ul className="nav nav-tablist nav-tablist-sm">
|
||||
{children}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
export const Tab = ({isActive, onClickTab, text}) =>
|
||||
<li className={isActive ? 'active' : ''} onClick={onClickTab}>
|
||||
{text}
|
||||
</li>
|
||||
|
||||
const {bool, func, node, string} = PropTypes
|
||||
|
||||
Tabber.propTypes = {
|
||||
children: node.isRequired,
|
||||
labelText: string,
|
||||
tipID: string,
|
||||
tipContent: string,
|
||||
}
|
||||
|
||||
Tab.propTypes = {
|
||||
onClickTab: func.isRequired,
|
||||
isActive: bool.isRequired,
|
||||
text: string.isRequired,
|
||||
}
|
|
@ -94,3 +94,15 @@ export const removeUnselectedTemplateValues = templates => {
|
|||
return {...template, values: selectedValues}
|
||||
})
|
||||
}
|
||||
|
||||
export const DISPLAY_OPTIONS = {
|
||||
LINEAR: 'linear',
|
||||
LOG: 'log',
|
||||
BASE_2: '2',
|
||||
BASE_10: '10',
|
||||
}
|
||||
|
||||
export const TOOLTIP_CONTENT = {
|
||||
FORMAT:
|
||||
'<p>K/M/B = Thousand / Million / Billion</p><p>K/M/G = Kilo / Mega / Giga </p>',
|
||||
}
|
||||
|
|
|
@ -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 =
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
<div
|
||||
ref={r => {
|
||||
this.graphRef = r
|
||||
this.props.dygraphRef(r)
|
||||
}}
|
||||
style={this.props.containerStyle}
|
||||
className="dygraph-child-container"
|
||||
style={this.props.containerStyle}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
@ -404,6 +427,27 @@ export default class Dygraph extends Component {
|
|||
|
||||
const {array, arrayOf, bool, func, shape, string} = PropTypes
|
||||
|
||||
Dygraph.defaultProps = {
|
||||
axes: {
|
||||
y: {
|
||||
bounds: [null, null],
|
||||
prefix: '',
|
||||
suffix: '',
|
||||
base: BASE_10,
|
||||
scale: LINEAR,
|
||||
},
|
||||
y2: {
|
||||
bounds: undefined,
|
||||
prefix: '',
|
||||
suffix: '',
|
||||
},
|
||||
},
|
||||
containerStyle: {},
|
||||
isGraphFilled: true,
|
||||
overrideLineColors: null,
|
||||
dygraphRef: () => {},
|
||||
}
|
||||
|
||||
Dygraph.propTypes = {
|
||||
axes: shape({
|
||||
y: shape({
|
||||
|
|
|
@ -159,12 +159,11 @@ export default React.createClass({
|
|||
: overrideLineColors
|
||||
|
||||
return (
|
||||
<div className={`dygraph ${this.yLabelClass()}`} style={{height: '100%'}}>
|
||||
<div className="dygraph graph--hasYLabel" style={{height: '100%'}}>
|
||||
{isRefreshing ? this.renderSpinner() : null}
|
||||
<Dygraph
|
||||
axes={axes}
|
||||
queries={queries}
|
||||
dygraphRef={this.dygraphRefFunc}
|
||||
containerStyle={{width: '100%', height: '100%'}}
|
||||
overrideLineColors={lineColors}
|
||||
isGraphFilled={showSingleStat ? false : isGraphFilled}
|
||||
|
@ -195,26 +194,6 @@ export default React.createClass({
|
|||
)
|
||||
},
|
||||
|
||||
yLabelClass() {
|
||||
const dygraph = this.dygraphRef
|
||||
|
||||
if (!dygraph) {
|
||||
return 'graph--hasYLabel'
|
||||
}
|
||||
|
||||
const label = dygraph.querySelector('.dygraph-ylabel')
|
||||
|
||||
if (!label) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return 'graph--hasYLabel'
|
||||
},
|
||||
|
||||
dygraphRefFunc(dygraphRef) {
|
||||
this.dygraphRef = dygraphRef
|
||||
},
|
||||
|
||||
renderSpinner() {
|
||||
return (
|
||||
<div className="graph-panel__refreshing">
|
||||
|
|
|
@ -11,7 +11,7 @@ const considerEmpty = (userNumber, number) => {
|
|||
const getRange = (
|
||||
timeSeries,
|
||||
userSelectedRange = [null, null],
|
||||
ruleValues = {value: null, rangeValue: null}
|
||||
ruleValues = {value: null, rangeValue: null, operator: ''}
|
||||
) => {
|
||||
const {value, rangeValue, operator} = ruleValues
|
||||
const [userMin, userMax] = userSelectedRange
|
||||
|
|
|
@ -1,3 +1,114 @@
|
|||
const KMB_LABELS = ['K', 'M', 'B', 'T', 'Q']
|
||||
const KMG2_BIG_LABELS = ['k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y']
|
||||
const KMG2_SMALL_LABELS = ['m', 'u', 'n', 'p', 'f', 'a', 'z', 'y']
|
||||
|
||||
const pow = (base, exp) => {
|
||||
if (exp < 0) {
|
||||
return 1.0 / Math.pow(base, -exp)
|
||||
}
|
||||
|
||||
return Math.pow(base, exp)
|
||||
}
|
||||
|
||||
const round_ = (num, places) => {
|
||||
const shift = Math.pow(10, places)
|
||||
return Math.round(num * shift) / shift
|
||||
}
|
||||
|
||||
const floatFormat = (x, optPrecision) => {
|
||||
// Avoid invalid precision values; [1, 21] is the valid range.
|
||||
const p = Math.min(Math.max(1, optPrecision || 2), 21)
|
||||
|
||||
// This is deceptively simple. The actual algorithm comes from:
|
||||
//
|
||||
// Max allowed length = p + 4
|
||||
// where 4 comes from 'e+n' and '.'.
|
||||
//
|
||||
// Length of fixed format = 2 + y + p
|
||||
// where 2 comes from '0.' and y = # of leading zeroes.
|
||||
//
|
||||
// Equating the two and solving for y yields y = 2, or 0.00xxxx which is
|
||||
// 1.0e-3.
|
||||
//
|
||||
// Since the behavior of toPrecision() is identical for larger numbers, we
|
||||
// don't have to worry about the other bound.
|
||||
//
|
||||
// Finally, the argument for toExponential() is the number of trailing digits,
|
||||
// so we take off 1 for the value before the '.'.
|
||||
return Math.abs(x) < 1.0e-3 && x !== 0.0
|
||||
? x.toExponential(p - 1)
|
||||
: x.toPrecision(p)
|
||||
}
|
||||
|
||||
// taken from https://github.com/danvk/dygraphs/blob/aaec6de56dba8ed712fd7b9d949de47b46a76ccd/src/dygraph-utils.js#L1103
|
||||
export const numberValueFormatter = (x, opts, prefix, suffix) => {
|
||||
const sigFigs = opts('sigFigs')
|
||||
|
||||
if (sigFigs !== null) {
|
||||
// User has opted for a fixed number of significant figures.
|
||||
return floatFormat(x, sigFigs)
|
||||
}
|
||||
|
||||
const digits = opts('digitsAfterDecimal')
|
||||
const maxNumberWidth = opts('maxNumberWidth')
|
||||
|
||||
const kmb = opts('labelsKMB')
|
||||
const kmg2 = opts('labelsKMG2')
|
||||
|
||||
let label
|
||||
|
||||
// switch to scientific notation if we underflow or overflow fixed display.
|
||||
if (
|
||||
x !== 0.0 &&
|
||||
(Math.abs(x) >= Math.pow(10, maxNumberWidth) ||
|
||||
Math.abs(x) < Math.pow(10, -digits))
|
||||
) {
|
||||
label = x.toExponential(digits)
|
||||
} else {
|
||||
label = `${round_(x, digits)}`
|
||||
}
|
||||
|
||||
if (kmb || kmg2) {
|
||||
let k
|
||||
let kLabels = []
|
||||
let mLabels = []
|
||||
if (kmb) {
|
||||
k = 1000
|
||||
kLabels = KMB_LABELS
|
||||
}
|
||||
if (kmg2) {
|
||||
if (kmb) {
|
||||
console.error('Setting both labelsKMB and labelsKMG2. Pick one!')
|
||||
}
|
||||
k = 1024
|
||||
kLabels = KMG2_BIG_LABELS
|
||||
mLabels = KMG2_SMALL_LABELS
|
||||
}
|
||||
|
||||
const absx = Math.abs(x)
|
||||
let n = pow(k, kLabels.length)
|
||||
for (let j = kLabels.length - 1; j >= 0; j -= 1, n /= k) {
|
||||
if (absx >= n) {
|
||||
label = round_(x / n, digits) + kLabels[j]
|
||||
break
|
||||
}
|
||||
}
|
||||
if (kmg2) {
|
||||
const xParts = String(x.toExponential()).split('e-')
|
||||
if (xParts.length === 2 && xParts[1] >= 3 && xParts[1] <= 24) {
|
||||
if (xParts[1] % 3 > 0) {
|
||||
label = round_(xParts[0] / pow(10, xParts[1] % 3), digits)
|
||||
} else {
|
||||
label = Number(xParts[0]).toFixed(2)
|
||||
}
|
||||
label += mLabels[Math.floor(xParts[1] / 3) - 1]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return `${prefix}${label}${suffix}`
|
||||
}
|
||||
|
||||
export const formatBytes = bytes => {
|
||||
if (bytes === 0) {
|
||||
return '0 Bytes'
|
||||
|
|
Loading…
Reference in New Issue