Merge Conflicts
commit
90a574cc64
|
@ -1,9 +1,15 @@
|
|||
## v1.4.2.0 [unreleased]
|
||||
### Features
|
||||
1. [#2837] (https://github.com/influxdata/chronograf/pull/2837): Prevent execution of queries in cells that are not in view on the dashboard page
|
||||
### UI Improvements
|
||||
1. [#2848](https://github.com/influxdata/chronograf/pull/2848): Add ability to set a prefix and suffix on Single Stat and Gauge cell types
|
||||
1. [#2831](https://github.com/influxdata/chronograf/pull/2831): Rename 'Create Alerts' page to 'Manage Tasks'; Redesign page to improve clarity of purpose
|
||||
|
||||
### Bug Fixes
|
||||
1. [#2821](https://github.com/influxdata/chronograf/pull/2821): Save only selected template variable values into dashboards for non csv template variables
|
||||
1. [#2842](https://github.com/influxdata/chronograf/pull/2842): Use Generic APIKey for Oauth2 group lookup
|
||||
1. [#2850](https://github.com/influxdata/chronograf/pull/2850): Fix bug in which resizing any cell in a dashboard causes a Gauge cell to resize
|
||||
1. [#2851] (https://github.com/influxdata/chronograf/pull/2851): Maintain y axis labels in dashboard cells
|
||||
|
||||
## v1.4.1.3 [2018-02-14]
|
||||
### Bug Fixes
|
||||
|
|
|
@ -15,11 +15,11 @@ import (
|
|||
|
||||
const (
|
||||
// AllAnnotations returns all annotations from the chronograf database
|
||||
AllAnnotations = `SELECT "start_time", "modified_time_ns", "text", "type", "id" FROM "chronograf"."autogen"."annotations" WHERE "deleted"=false AND time >= %dns and "start_time" <= %d ORDER BY time DESC`
|
||||
AllAnnotations = `SELECT "start_time", "modified_time_ns", "text", "type", "id" FROM "annotations" WHERE "deleted"=false AND time >= %dns and "start_time" <= %d ORDER BY time DESC`
|
||||
// GetAnnotationID returns all annotations from the chronograf database where id is %s
|
||||
GetAnnotationID = `SELECT "start_time", "modified_time_ns", "text", "type", "id" FROM "chronograf"."autogen"."annotations" WHERE "id"='%s' AND "deleted"=false ORDER BY time DESC`
|
||||
// DefaultDB is chronograf. Perhaps later we allow this to be changed
|
||||
DefaultDB = "chronograf"
|
||||
GetAnnotationID = `SELECT "start_time", "modified_time_ns", "text", "type", "id" FROM "annotations" WHERE "id"='%s' AND "deleted"=false ORDER BY time DESC`
|
||||
// AnnotationsDB is chronograf. Perhaps later we allow this to be changed
|
||||
AnnotationsDB = "chronograf"
|
||||
// DefaultRP is autogen. Perhaps later we allow this to be changed
|
||||
DefaultRP = "autogen"
|
||||
// DefaultMeasurement is annotations.
|
||||
|
@ -104,7 +104,7 @@ func (a *AnnotationStore) Update(ctx context.Context, anno *chronograf.Annotatio
|
|||
func (a *AnnotationStore) queryAnnotations(ctx context.Context, query string) ([]chronograf.Annotation, error) {
|
||||
res, err := a.client.Query(ctx, chronograf.Query{
|
||||
Command: query,
|
||||
DB: DefaultDB,
|
||||
DB: AnnotationsDB,
|
||||
Epoch: "ns",
|
||||
})
|
||||
if err != nil {
|
||||
|
@ -126,7 +126,7 @@ func (a *AnnotationStore) queryAnnotations(ctx context.Context, query string) ([
|
|||
|
||||
func toPoint(anno *chronograf.Annotation, now time.Time) *chronograf.Point {
|
||||
return &chronograf.Point{
|
||||
Database: DefaultDB,
|
||||
Database: AnnotationsDB,
|
||||
RetentionPolicy: DefaultRP,
|
||||
Measurement: DefaultMeasurement,
|
||||
Time: anno.EndTime.UnixNano(),
|
||||
|
@ -145,7 +145,7 @@ func toPoint(anno *chronograf.Annotation, now time.Time) *chronograf.Point {
|
|||
|
||||
func toDeletedPoint(anno *chronograf.Annotation, now time.Time) *chronograf.Point {
|
||||
return &chronograf.Point{
|
||||
Database: DefaultDB,
|
||||
Database: AnnotationsDB,
|
||||
RetentionPolicy: DefaultRP,
|
||||
Measurement: DefaultMeasurement,
|
||||
Time: anno.EndTime.UnixNano(),
|
||||
|
|
|
@ -29,7 +29,7 @@ func Test_toPoint(t *testing.T) {
|
|||
},
|
||||
now: time.Unix(0, 0),
|
||||
want: &chronograf.Point{
|
||||
Database: DefaultDB,
|
||||
Database: AnnotationsDB,
|
||||
RetentionPolicy: DefaultRP,
|
||||
Measurement: DefaultMeasurement,
|
||||
Time: time.Time{}.UnixNano(),
|
||||
|
@ -56,7 +56,7 @@ func Test_toPoint(t *testing.T) {
|
|||
},
|
||||
now: time.Unix(0, 0),
|
||||
want: &chronograf.Point{
|
||||
Database: DefaultDB,
|
||||
Database: AnnotationsDB,
|
||||
RetentionPolicy: DefaultRP,
|
||||
Measurement: DefaultMeasurement,
|
||||
Time: time.Unix(200, 0).UnixNano(),
|
||||
|
@ -97,7 +97,7 @@ func Test_toDeletedPoint(t *testing.T) {
|
|||
},
|
||||
now: time.Unix(0, 0),
|
||||
want: &chronograf.Point{
|
||||
Database: DefaultDB,
|
||||
Database: AnnotationsDB,
|
||||
RetentionPolicy: DefaultRP,
|
||||
Measurement: DefaultMeasurement,
|
||||
Time: 0,
|
||||
|
|
|
@ -45,7 +45,7 @@ func newCellResponse(dID chronograf.DashboardID, cell chronograf.DashboardCell)
|
|||
for _, lbl := range []string{"x", "y", "y2"} {
|
||||
if _, found := newAxes[lbl]; !found {
|
||||
newAxes[lbl] = chronograf.Axis{
|
||||
Bounds: []string{},
|
||||
Bounds: []string{"", ""},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -354,6 +354,13 @@ func (s *Service) ReplaceDashboardCell(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
for i, a := range cell.Axes {
|
||||
if len(a.Bounds) == 0 {
|
||||
a.Bounds = []string{"", ""}
|
||||
cell.Axes[i] = a
|
||||
}
|
||||
}
|
||||
|
||||
if err := ValidDashboardCellRequest(&cell); err != nil {
|
||||
invalidData(w, err, s.Logger)
|
||||
return
|
||||
|
|
|
@ -192,13 +192,13 @@ func Test_Service_DashboardCells(t *testing.T) {
|
|||
CellColors: []chronograf.CellColor{},
|
||||
Axes: map[string]chronograf.Axis{
|
||||
"x": chronograf.Axis{
|
||||
Bounds: []string{},
|
||||
Bounds: []string{"", ""},
|
||||
},
|
||||
"y": chronograf.Axis{
|
||||
Bounds: []string{},
|
||||
Bounds: []string{"", ""},
|
||||
},
|
||||
"y2": chronograf.Axis{
|
||||
Bounds: []string{},
|
||||
Bounds: []string{"", ""},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -420,13 +420,13 @@ func TestService_ReplaceDashboardCell(t *testing.T) {
|
|||
},
|
||||
Axes: map[string]chronograf.Axis{
|
||||
"x": {
|
||||
Bounds: []string{},
|
||||
Bounds: []string{"", ""},
|
||||
},
|
||||
"y": {
|
||||
Bounds: []string{},
|
||||
Bounds: []string{"", ""},
|
||||
},
|
||||
"y2": {
|
||||
Bounds: []string{},
|
||||
Bounds: []string{"", ""},
|
||||
},
|
||||
},
|
||||
Type: "line",
|
||||
|
@ -491,7 +491,7 @@ func TestService_ReplaceDashboardCell(t *testing.T) {
|
|||
],
|
||||
"axes": {
|
||||
"x": {
|
||||
"bounds": [],
|
||||
"bounds": ["",""],
|
||||
"label": "",
|
||||
"prefix": "",
|
||||
"suffix": "",
|
||||
|
@ -499,7 +499,7 @@ func TestService_ReplaceDashboardCell(t *testing.T) {
|
|||
"scale": ""
|
||||
},
|
||||
"y": {
|
||||
"bounds": [],
|
||||
"bounds": ["",""],
|
||||
"label": "",
|
||||
"prefix": "",
|
||||
"suffix": "",
|
||||
|
@ -507,7 +507,7 @@ func TestService_ReplaceDashboardCell(t *testing.T) {
|
|||
"scale": ""
|
||||
},
|
||||
"y2": {
|
||||
"bounds": [],
|
||||
"bounds": ["",""],
|
||||
"label": "",
|
||||
"prefix": "",
|
||||
"suffix": "",
|
||||
|
@ -532,7 +532,7 @@ func TestService_ReplaceDashboardCell(t *testing.T) {
|
|||
}
|
||||
}
|
||||
`))),
|
||||
want: `{"i":"3c5c4102-fa40-4585-a8f9-917c77e37192","x":0,"y":0,"w":4,"h":4,"name":"Untitled Cell","queries":[{"query":"SELECT mean(\"usage_user\") AS \"mean_usage_user\" FROM \"telegraf\".\"autogen\".\"cpu\" WHERE time \u003e :dashboardTime: AND \"cpu\"=:cpu: GROUP BY :interval: FILL(null)","queryConfig":{"id":"3cd3eaa4-a4b8-44b3-b69e-0c7bf6b91d9e","database":"telegraf","measurement":"cpu","retentionPolicy":"autogen","fields":[{"value":"mean","type":"func","alias":"mean_usage_user","args":[{"value":"usage_user","type":"field","alias":""}]}],"tags":{"cpu":["ChristohersMBP2.lan"]},"groupBy":{"time":"2s","tags":[]},"areTagsAccepted":true,"fill":"null","rawText":"SELECT mean(\"usage_user\") AS \"mean_usage_user\" FROM \"telegraf\".\"autogen\".\"cpu\" WHERE time \u003e :dashboardTime: AND \"cpu\"=:cpu: GROUP BY :interval: FILL(null)","range":{"upper":"","lower":"now() - 15m"},"shifts":[]},"source":""}],"axes":{"x":{"bounds":[],"label":"","prefix":"","suffix":"","base":"","scale":""},"y":{"bounds":[],"label":"","prefix":"","suffix":"","base":"","scale":""},"y2":{"bounds":[],"label":"","prefix":"","suffix":"","base":"","scale":""}},"type":"line","colors":[{"id":"0","type":"min","hex":"#00C9FF","name":"laser","value":"0"},{"id":"1","type":"max","hex":"#9394FF","name":"comet","value":"100"}],"legend":{},"links":{"self":"/chronograf/v1/dashboards/1/cells/3c5c4102-fa40-4585-a8f9-917c77e37192"}}
|
||||
want: `{"i":"3c5c4102-fa40-4585-a8f9-917c77e37192","x":0,"y":0,"w":4,"h":4,"name":"Untitled Cell","queries":[{"query":"SELECT mean(\"usage_user\") AS \"mean_usage_user\" FROM \"telegraf\".\"autogen\".\"cpu\" WHERE time \u003e :dashboardTime: AND \"cpu\"=:cpu: GROUP BY :interval: FILL(null)","queryConfig":{"id":"3cd3eaa4-a4b8-44b3-b69e-0c7bf6b91d9e","database":"telegraf","measurement":"cpu","retentionPolicy":"autogen","fields":[{"value":"mean","type":"func","alias":"mean_usage_user","args":[{"value":"usage_user","type":"field","alias":""}]}],"tags":{"cpu":["ChristohersMBP2.lan"]},"groupBy":{"time":"2s","tags":[]},"areTagsAccepted":true,"fill":"null","rawText":"SELECT mean(\"usage_user\") AS \"mean_usage_user\" FROM \"telegraf\".\"autogen\".\"cpu\" WHERE time \u003e :dashboardTime: AND \"cpu\"=:cpu: GROUP BY :interval: FILL(null)","range":{"upper":"","lower":"now() - 15m"},"shifts":[]},"source":""}],"axes":{"x":{"bounds":["",""],"label":"","prefix":"","suffix":"","base":"","scale":""},"y":{"bounds":["",""],"label":"","prefix":"","suffix":"","base":"","scale":""},"y2":{"bounds":["",""],"label":"","prefix":"","suffix":"","base":"","scale":""}},"type":"line","colors":[{"id":"0","type":"min","hex":"#00C9FF","name":"laser","value":"0"},{"id":"1","type":"max","hex":"#9394FF","name":"comet","value":"100"}],"legend":{},"links":{"self":"/chronograf/v1/dashboards/1/cells/3c5c4102-fa40-4585-a8f9-917c77e37192"}}
|
||||
`,
|
||||
},
|
||||
{
|
||||
|
@ -854,13 +854,13 @@ func Test_newCellResponses(t *testing.T) {
|
|||
Queries: []chronograf.DashboardQuery{},
|
||||
Axes: map[string]chronograf.Axis{
|
||||
"x": chronograf.Axis{
|
||||
Bounds: []string{},
|
||||
Bounds: []string{"", ""},
|
||||
},
|
||||
"y": chronograf.Axis{
|
||||
Bounds: []string{},
|
||||
Bounds: []string{"", ""},
|
||||
},
|
||||
"y2": chronograf.Axis{
|
||||
Bounds: []string{},
|
||||
Bounds: []string{"", ""},
|
||||
},
|
||||
},
|
||||
CellColors: []chronograf.CellColor{},
|
||||
|
|
|
@ -299,7 +299,7 @@ func Test_newDashboardResponse(t *testing.T) {
|
|||
Label: "foo",
|
||||
},
|
||||
"y2": chronograf.Axis{
|
||||
Bounds: []string{},
|
||||
Bounds: []string{"", ""},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -314,13 +314,13 @@ func Test_newDashboardResponse(t *testing.T) {
|
|||
H: 4,
|
||||
Axes: map[string]chronograf.Axis{
|
||||
"x": chronograf.Axis{
|
||||
Bounds: []string{},
|
||||
Bounds: []string{"", ""},
|
||||
},
|
||||
"y": chronograf.Axis{
|
||||
Bounds: []string{},
|
||||
Bounds: []string{"", ""},
|
||||
},
|
||||
"y2": chronograf.Axis{
|
||||
Bounds: []string{},
|
||||
Bounds: []string{"", ""},
|
||||
},
|
||||
},
|
||||
CellColors: []chronograf.CellColor{},
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import {getAnnotations} from 'shared/annotations/helpers'
|
||||
import {visibleAnnotations} from 'shared/annotations/helpers'
|
||||
import Dygraph from 'src/external/dygraph'
|
||||
|
||||
const start = 1515628800000
|
||||
|
@ -38,16 +38,16 @@ const a2 = {
|
|||
const annotations = [a1]
|
||||
|
||||
describe('Shared.Annotations.Helpers', () => {
|
||||
describe('getAnnotations', () => {
|
||||
describe('visibleAnnotations', () => {
|
||||
it('returns an empty array with no graph or annotations are provided', () => {
|
||||
const actual = getAnnotations(undefined, annotations)
|
||||
const actual = visibleAnnotations(undefined, annotations)
|
||||
const expected = []
|
||||
|
||||
expect(actual).to.deep.equal(expected)
|
||||
})
|
||||
|
||||
it('returns an annotation if it is in the time range', () => {
|
||||
const actual = getAnnotations(graph, annotations)
|
||||
const actual = visibleAnnotations(graph, annotations)
|
||||
const expected = annotations
|
||||
|
||||
expect(actual).to.deep.equal(expected)
|
||||
|
@ -62,7 +62,7 @@ describe('Shared.Annotations.Helpers', () => {
|
|||
}
|
||||
|
||||
const newAnnos = [...annotations, outOfBounds]
|
||||
const actual = getAnnotations(graph, newAnnos)
|
||||
const actual = visibleAnnotations(graph, newAnnos)
|
||||
const expected = annotations
|
||||
|
||||
expect(actual).to.deep.equal(expected)
|
||||
|
@ -71,7 +71,7 @@ describe('Shared.Annotations.Helpers', () => {
|
|||
describe('with a duration', () => {
|
||||
it('it adds an annotation', () => {
|
||||
const withDurations = [...annotations, a2]
|
||||
const actual = getAnnotations(graph, withDurations)
|
||||
const actual = visibleAnnotations(graph, withDurations)
|
||||
const expectedAnnotation = {
|
||||
...a2,
|
||||
time: `${Number(a2.time) + Number(a2.duration)}`,
|
||||
|
@ -93,7 +93,7 @@ describe('Shared.Annotations.Helpers', () => {
|
|||
annotationWithOutOfBoundsDuration,
|
||||
]
|
||||
|
||||
const actual = getAnnotations(graph, withDurations)
|
||||
const actual = visibleAnnotations(graph, withDurations)
|
||||
const expected = withDurations
|
||||
|
||||
expect(actual).to.deep.equal(expected)
|
||||
|
|
|
@ -126,6 +126,7 @@ class AxesOptions extends Component {
|
|||
value={prefix}
|
||||
labelText="Y-Value's Prefix"
|
||||
onChange={this.handleSetPrefixSuffix}
|
||||
maxLength="5"
|
||||
/>
|
||||
<Input
|
||||
name="suffix"
|
||||
|
@ -133,6 +134,7 @@ class AxesOptions extends Component {
|
|||
value={suffix}
|
||||
labelText="Y-Value's Suffix"
|
||||
onChange={this.handleSetPrefixSuffix}
|
||||
maxLength="5"
|
||||
/>
|
||||
<Tabber
|
||||
labelText="Y-Value's Format"
|
||||
|
|
|
@ -23,9 +23,14 @@ const Dashboard = ({
|
|||
onSummonOverlayTechnologies,
|
||||
onSelectTemplate,
|
||||
showTemplateControlBar,
|
||||
setScrollTop,
|
||||
inView,
|
||||
}) => {
|
||||
const cells = dashboard.cells.map(cell => {
|
||||
const dashboardCell = {...cell}
|
||||
const dashboardCell = {
|
||||
...cell,
|
||||
inView: inView(cell),
|
||||
}
|
||||
dashboardCell.queries = dashboardCell.queries.map(q => ({
|
||||
...q,
|
||||
database: q.db,
|
||||
|
@ -39,6 +44,7 @@ const Dashboard = ({
|
|||
className={classnames('page-contents', {
|
||||
'presentation-mode': inPresentationMode,
|
||||
})}
|
||||
setScrollTop={setScrollTop}
|
||||
>
|
||||
<div className="dashboard container-fluid full-width">
|
||||
{inPresentationMode
|
||||
|
@ -119,6 +125,8 @@ Dashboard.propTypes = {
|
|||
onSelectTemplate: func.isRequired,
|
||||
showTemplateControlBar: bool,
|
||||
onZoom: func,
|
||||
setScrollTop: func,
|
||||
inView: func,
|
||||
}
|
||||
|
||||
export default Dashboard
|
||||
|
|
|
@ -15,7 +15,10 @@ import {
|
|||
MIN_THRESHOLDS,
|
||||
} from 'src/dashboards/constants/gaugeColors'
|
||||
|
||||
import {updateGaugeColors} from 'src/dashboards/actions/cellEditorOverlay'
|
||||
import {
|
||||
updateGaugeColors,
|
||||
updateAxes,
|
||||
} from 'src/dashboards/actions/cellEditorOverlay'
|
||||
|
||||
class GaugeOptions extends Component {
|
||||
handleAddThreshold = () => {
|
||||
|
@ -118,8 +121,22 @@ class GaugeOptions extends Component {
|
|||
return allowedToUpdate
|
||||
}
|
||||
|
||||
handleUpdatePrefix = e => {
|
||||
const {handleUpdateAxes, axes} = this.props
|
||||
const newAxes = {...axes, y: {...axes.y, prefix: e.target.value}}
|
||||
|
||||
handleUpdateAxes(newAxes)
|
||||
}
|
||||
|
||||
handleUpdateSuffix = e => {
|
||||
const {handleUpdateAxes, axes} = this.props
|
||||
const newAxes = {...axes, y: {...axes.y, suffix: e.target.value}}
|
||||
|
||||
handleUpdateAxes(newAxes)
|
||||
}
|
||||
|
||||
render() {
|
||||
const {gaugeColors} = this.props
|
||||
const {gaugeColors, axes: {y: {prefix, suffix}}} = this.props
|
||||
|
||||
const disableMaxColor = gaugeColors.length > MIN_THRESHOLDS
|
||||
const disableAddThreshold = gaugeColors.length > MAX_THRESHOLDS
|
||||
|
@ -157,6 +174,28 @@ class GaugeOptions extends Component {
|
|||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="single-stat-controls">
|
||||
<div className="form-group col-xs-6">
|
||||
<label>Prefix</label>
|
||||
<input
|
||||
className="form-control input-sm"
|
||||
placeholder="%, MPH, etc."
|
||||
defaultValue={prefix}
|
||||
onChange={this.handleUpdatePrefix}
|
||||
maxLength="5"
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group col-xs-6">
|
||||
<label>Suffix</label>
|
||||
<input
|
||||
className="form-control input-sm"
|
||||
placeholder="%, MPH, etc."
|
||||
defaultValue={suffix}
|
||||
onChange={this.handleUpdateSuffix}
|
||||
maxLength="5"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</FancyScrollbar>
|
||||
)
|
||||
|
@ -176,13 +215,17 @@ GaugeOptions.propTypes = {
|
|||
}).isRequired
|
||||
),
|
||||
handleUpdateGaugeColors: func.isRequired,
|
||||
handleUpdateAxes: func.isRequired,
|
||||
axes: shape({}).isRequired,
|
||||
}
|
||||
|
||||
const mapStateToProps = ({cellEditorOverlay: {gaugeColors}}) => ({
|
||||
const mapStateToProps = ({cellEditorOverlay: {gaugeColors, cell: {axes}}}) => ({
|
||||
gaugeColors,
|
||||
axes,
|
||||
})
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
handleUpdateGaugeColors: bindActionCreators(updateGaugeColors, dispatch),
|
||||
handleUpdateAxes: bindActionCreators(updateAxes, dispatch),
|
||||
})
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(GaugeOptions)
|
||||
|
|
|
@ -109,6 +109,13 @@ class SingleStatOptions extends Component {
|
|||
return !sortedColors.some(color => color.value === targetValue)
|
||||
}
|
||||
|
||||
handleUpdatePrefix = e => {
|
||||
const {handleUpdateAxes, axes} = this.props
|
||||
const newAxes = {...axes, y: {...axes.y, prefix: e.target.value}}
|
||||
|
||||
handleUpdateAxes(newAxes)
|
||||
}
|
||||
|
||||
handleUpdateSuffix = e => {
|
||||
const {handleUpdateAxes, axes} = this.props
|
||||
const newAxes = {...axes, y: {...axes.y, suffix: e.target.value}}
|
||||
|
@ -117,7 +124,11 @@ class SingleStatOptions extends Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
const {singleStatColors, singleStatType, axes: {y: {suffix}}} = this.props
|
||||
const {
|
||||
singleStatColors,
|
||||
singleStatType,
|
||||
axes: {y: {prefix, suffix}},
|
||||
} = this.props
|
||||
|
||||
const disableAddThreshold = singleStatColors.length > MAX_THRESHOLDS
|
||||
|
||||
|
@ -162,6 +173,26 @@ class SingleStatOptions extends Component {
|
|||
)}
|
||||
</div>
|
||||
<div className="single-stat-controls">
|
||||
<div className="form-group col-xs-6">
|
||||
<label>Prefix</label>
|
||||
<input
|
||||
className="form-control input-sm"
|
||||
placeholder="%, MPH, etc."
|
||||
defaultValue={prefix}
|
||||
onChange={this.handleUpdatePrefix}
|
||||
maxLength="5"
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group col-xs-6">
|
||||
<label>Suffix</label>
|
||||
<input
|
||||
className="form-control input-sm"
|
||||
placeholder="%, MPH, etc."
|
||||
defaultValue={suffix}
|
||||
onChange={this.handleUpdateSuffix}
|
||||
maxLength="5"
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group col-xs-6">
|
||||
<label>Coloring</label>
|
||||
<ul className="nav nav-tablist nav-tablist-sm">
|
||||
|
@ -183,16 +214,6 @@ class SingleStatOptions extends Component {
|
|||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="form-group col-xs-6">
|
||||
<label>Suffix</label>
|
||||
<input
|
||||
className="form-control input-sm"
|
||||
placeholder="%, MPH, etc."
|
||||
defaultValue={suffix}
|
||||
onChange={this.handleUpdateSuffix}
|
||||
maxLength="5"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</FancyScrollbar>
|
||||
|
|
|
@ -32,11 +32,13 @@ import {
|
|||
templateControlBarVisibilityToggled as templateControlBarVisibilityToggledAction,
|
||||
} from 'shared/actions/app'
|
||||
import {presentationButtonDispatcher} from 'shared/dispatchers'
|
||||
import {DASHBOARD_LAYOUT_ROW_HEIGHT} from 'shared/constants'
|
||||
|
||||
const FORMAT_INFLUXQL = 'influxql'
|
||||
const defaultTimeRange = {
|
||||
upper: null,
|
||||
lower: 'now() - 15m',
|
||||
seconds: 900,
|
||||
format: FORMAT_INFLUXQL,
|
||||
}
|
||||
|
||||
|
@ -49,6 +51,8 @@ class DashboardPage extends Component {
|
|||
selectedCell: null,
|
||||
isTemplating: false,
|
||||
zoomedTimeRange: {zoomedLower: null, zoomedUpper: null},
|
||||
scrollTop: 0,
|
||||
windowHeight: window.innerHeight,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -68,10 +72,14 @@ class DashboardPage extends Component {
|
|||
router,
|
||||
notify,
|
||||
getAnnotationsAsync,
|
||||
timeRange,
|
||||
} = this.props
|
||||
|
||||
const fifteenMinutes = Date.now() - 15 * 60 * 1000
|
||||
getAnnotationsAsync(source.links.annotations, fifteenMinutes)
|
||||
getAnnotationsAsync(
|
||||
source.links.annotations,
|
||||
Date.now() - timeRange.seconds * 1000
|
||||
)
|
||||
window.addEventListener('resize', this.handleWindowResize, true)
|
||||
|
||||
const dashboards = await getDashboardsAsync()
|
||||
const dashboard = dashboards.find(
|
||||
|
@ -92,6 +100,27 @@ class DashboardPage extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
handleWindowResize = () => {
|
||||
this.setState({windowHeight: window.innerHeight})
|
||||
}
|
||||
|
||||
componentWillUnMount() {
|
||||
window.removeEventListener('resize', this.handleWindowResize, true)
|
||||
}
|
||||
|
||||
inView = cell => {
|
||||
const {scrollTop, windowHeight} = this.state
|
||||
const bufferValue = 600
|
||||
const cellTop = cell.y * DASHBOARD_LAYOUT_ROW_HEIGHT
|
||||
const cellBottom = (cell.y + cell.h) * DASHBOARD_LAYOUT_ROW_HEIGHT
|
||||
const bufferedWindowBottom = windowHeight + scrollTop + bufferValue
|
||||
const bufferedWindowTop = scrollTop - bufferValue
|
||||
const topInView = cellTop < bufferedWindowBottom
|
||||
const bottomInView = cellBottom > bufferedWindowTop
|
||||
|
||||
return topInView && bottomInView
|
||||
}
|
||||
|
||||
handleOpenTemplateManager = () => {
|
||||
this.setState({isTemplating: true})
|
||||
}
|
||||
|
@ -116,13 +145,21 @@ class DashboardPage extends Component {
|
|||
.then(handleHideCellEditorOverlay)
|
||||
}
|
||||
|
||||
handleChooseTimeRange = ({upper, lower}) => {
|
||||
const {dashboard, dashboardActions} = this.props
|
||||
handleChooseTimeRange = timeRange => {
|
||||
const {
|
||||
dashboard,
|
||||
dashboardActions,
|
||||
getAnnotationsAsync,
|
||||
source,
|
||||
} = this.props
|
||||
dashboardActions.setDashTimeV1(dashboard.id, {
|
||||
upper,
|
||||
lower,
|
||||
...timeRange,
|
||||
format: FORMAT_INFLUXQL,
|
||||
})
|
||||
getAnnotationsAsync(
|
||||
source.links.annotations,
|
||||
Date.now() - timeRange.seconds * 1000
|
||||
)
|
||||
}
|
||||
|
||||
handleUpdatePosition = cells => {
|
||||
|
@ -234,6 +271,10 @@ class DashboardPage extends Component {
|
|||
this.setState({zoomedTimeRange: {zoomedLower, zoomedUpper}})
|
||||
}
|
||||
|
||||
setScrollTop = event => {
|
||||
this.setState({scrollTop: event.target.scrollTop})
|
||||
}
|
||||
|
||||
render() {
|
||||
const {zoomedTimeRange} = this.state
|
||||
const {zoomedLower, zoomedUpper} = zoomedTimeRange
|
||||
|
@ -327,6 +368,7 @@ class DashboardPage extends Component {
|
|||
}
|
||||
|
||||
const {isEditMode, isTemplating} = this.state
|
||||
|
||||
const names = dashboards.map(d => ({
|
||||
name: d.name,
|
||||
link: `/sources/${sourceID}/dashboards/${d.id}`,
|
||||
|
@ -389,6 +431,8 @@ class DashboardPage extends Component {
|
|||
? <Dashboard
|
||||
source={source}
|
||||
sources={sources}
|
||||
setScrollTop={this.setScrollTop}
|
||||
inView={this.inView}
|
||||
dashboard={dashboard}
|
||||
timeRange={timeRange}
|
||||
autoRefresh={autoRefresh}
|
||||
|
|
|
@ -6,6 +6,7 @@ import SourceIndicator from 'shared/components/SourceIndicator'
|
|||
import KapacitorRulesTable from 'src/kapacitor/components/KapacitorRulesTable'
|
||||
import TasksTable from 'src/kapacitor/components/TasksTable'
|
||||
import FancyScrollbar from 'shared/components/FancyScrollbar'
|
||||
import QuestionMarkTooltip from 'shared/components/QuestionMarkTooltip'
|
||||
|
||||
const KapacitorRules = ({
|
||||
source,
|
||||
|
@ -41,17 +42,21 @@ const KapacitorRules = ({
|
|||
)
|
||||
}
|
||||
|
||||
const rulez = rules.filter(r => r.query)
|
||||
const tasks = rules.filter(r => !r.query)
|
||||
const builderRules = rules.filter(r => r.query)
|
||||
|
||||
const rHeader = `${rulez.length} Alert Rule${rulez.length === 1 ? '' : 's'}`
|
||||
const tHeader = `${tasks.length} TICKscript${tasks.length === 1 ? '' : 's'}`
|
||||
const builderHeader = `${builderRules.length} Alert Rule${builderRules.length ===
|
||||
1
|
||||
? ''
|
||||
: 's'}`
|
||||
const scriptsHeader = `${rules.length} TICKscript${rules.length === 1
|
||||
? ''
|
||||
: 's'}`
|
||||
|
||||
return (
|
||||
<PageContents source={source}>
|
||||
<div className="panel-heading u-flex u-ai-center u-jc-space-between">
|
||||
<h2 className="panel-title">
|
||||
{rHeader}
|
||||
{builderHeader}
|
||||
</h2>
|
||||
<div className="u-flex u-ai-center u-jc-space-between">
|
||||
<Link
|
||||
|
@ -65,7 +70,7 @@ const KapacitorRules = ({
|
|||
</div>
|
||||
<KapacitorRulesTable
|
||||
source={source}
|
||||
rules={rulez}
|
||||
rules={builderRules}
|
||||
onDelete={onDelete}
|
||||
onChangeRuleStatus={onChangeRuleStatus}
|
||||
/>
|
||||
|
@ -75,12 +80,12 @@ const KapacitorRules = ({
|
|||
<div className="panel panel-minimal">
|
||||
<div className="panel-heading u-flex u-ai-center u-jc-space-between">
|
||||
<h2 className="panel-title">
|
||||
{tHeader}
|
||||
{scriptsHeader}
|
||||
</h2>
|
||||
<div className="u-flex u-ai-center u-jc-space-between">
|
||||
<Link
|
||||
to={`/sources/${source.id}/tickscript/new`}
|
||||
className="btn btn-sm btn-primary"
|
||||
className="btn btn-sm btn-success"
|
||||
style={{marginRight: '4px'}}
|
||||
>
|
||||
<span className="icon plus" /> Write TICKscript
|
||||
|
@ -89,7 +94,7 @@ const KapacitorRules = ({
|
|||
</div>
|
||||
<TasksTable
|
||||
source={source}
|
||||
tasks={tasks}
|
||||
tasks={rules}
|
||||
onDelete={onDelete}
|
||||
onChangeRuleStatus={onChangeRuleStatus}
|
||||
/>
|
||||
|
@ -105,11 +110,13 @@ const PageContents = ({children}) =>
|
|||
<div className="page-header">
|
||||
<div className="page-header__container">
|
||||
<div className="page-header__left">
|
||||
<h1 className="page-header__title">
|
||||
Build Alert Rules or Write TICKscripts
|
||||
</h1>
|
||||
<h1 className="page-header__title">Manage Tasks</h1>
|
||||
</div>
|
||||
<div className="page-header__right">
|
||||
<QuestionMarkTooltip
|
||||
tipID="manage-tasks--tooltip"
|
||||
tipContent="<b>Alert Rules</b> generate a TICKscript for<br/>you using our Builder UI.<br/><br/>Not all TICKscripts can be edited<br/>using the Builder."
|
||||
/>
|
||||
<SourceIndicator />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -2,8 +2,9 @@ import React, {PropTypes} from 'react'
|
|||
import {Link} from 'react-router'
|
||||
import _ from 'lodash'
|
||||
|
||||
import ConfirmButton from 'src/shared/components/ConfirmButton'
|
||||
import {parseAlertNodeList} from 'src/shared/parsing/parseHandlersFromRule'
|
||||
import {KAPACITOR_RULES_TABLE} from 'src/kapacitor/constants/tableSizing'
|
||||
import {TASKS_TABLE} from 'src/kapacitor/constants/tableSizing'
|
||||
const {
|
||||
colName,
|
||||
colTrigger,
|
||||
|
@ -11,19 +12,19 @@ const {
|
|||
colAlerts,
|
||||
colEnabled,
|
||||
colActions,
|
||||
} = KAPACITOR_RULES_TABLE
|
||||
} = TASKS_TABLE
|
||||
|
||||
const KapacitorRulesTable = ({rules, source, onDelete, onChangeRuleStatus}) =>
|
||||
<div className="panel-body">
|
||||
<table className="table v-center">
|
||||
<table className="table v-center table-highlight">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{width: colName}}>Name</th>
|
||||
<th style={{width: colTrigger}}>Rule Trigger</th>
|
||||
<th style={{minWidth: colName}}>Name</th>
|
||||
<th style={{width: colTrigger}}>Rule Type</th>
|
||||
<th style={{width: colMessage}}>Message</th>
|
||||
<th style={{width: colAlerts}}>Alerts</th>
|
||||
<th style={{width: colAlerts}}>Alert Handlers</th>
|
||||
<th style={{width: colEnabled}} className="text-center">
|
||||
Enabled
|
||||
Task Enabled
|
||||
</th>
|
||||
<th style={{width: colActions}} />
|
||||
</tr>
|
||||
|
@ -48,24 +49,21 @@ const handleDelete = (rule, onDelete) => onDelete(rule)
|
|||
|
||||
const RuleRow = ({rule, source, onDelete, onChangeRuleStatus}) =>
|
||||
<tr key={rule.id}>
|
||||
<td style={{width: colName}} className="monotype">
|
||||
<RuleTitle rule={rule} source={source} />
|
||||
<td style={{minWidth: colName}}>
|
||||
<Link to={`/sources/${source.id}/alert-rules/${rule.id}`}>
|
||||
{rule.name}
|
||||
</Link>
|
||||
</td>
|
||||
<td style={{width: colTrigger}} className="monotype">
|
||||
<td style={{width: colTrigger, textTransform: 'capitalize'}}>
|
||||
{rule.trigger}
|
||||
</td>
|
||||
<td className="monotype">
|
||||
<span
|
||||
className="table-cell-nowrap"
|
||||
style={{display: 'inline-block', maxWidth: colMessage}}
|
||||
>
|
||||
{rule.message}
|
||||
</span>
|
||||
<td style={{width: colMessage}}>
|
||||
{rule.message}
|
||||
</td>
|
||||
<td style={{width: colAlerts}} className="monotype">
|
||||
<td style={{width: colAlerts}}>
|
||||
{parseAlertNodeList(rule)}
|
||||
</td>
|
||||
<td style={{width: colEnabled}} className="monotype text-center">
|
||||
<td style={{width: colEnabled}} className="text-center">
|
||||
<div className="dark-checkbox">
|
||||
<input
|
||||
id={`kapacitor-enabled ${rule.id}`}
|
||||
|
@ -77,39 +75,17 @@ const RuleRow = ({rule, source, onDelete, onChangeRuleStatus}) =>
|
|||
<label htmlFor={`kapacitor-enabled ${rule.id}`} />
|
||||
</div>
|
||||
</td>
|
||||
<td style={{width: colActions}} className="text-right table-cell-nowrap">
|
||||
<Link
|
||||
className="btn btn-info btn-xs"
|
||||
to={`/sources/${source.id}/tickscript/${rule.id}`}
|
||||
>
|
||||
Edit TICKscript
|
||||
</Link>
|
||||
<button
|
||||
className="btn btn-danger btn-xs"
|
||||
onClick={handleDelete(rule, onDelete)}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
<td style={{width: colActions}} className="text-right">
|
||||
<ConfirmButton
|
||||
text="Delete"
|
||||
type="btn-danger"
|
||||
size="btn-xs"
|
||||
customClass="table--show-on-row-hover"
|
||||
confirmAction={handleDelete(rule, onDelete)}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
const RuleTitle = ({rule: {id, name, query}, source}) => {
|
||||
// no queryConfig means the rule was manually created outside of Chronograf
|
||||
if (!query) {
|
||||
return (
|
||||
<i>
|
||||
{name}
|
||||
</i>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Link to={`/sources/${source.id}/alert-rules/${id}`}>
|
||||
{name}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
const {arrayOf, func, shape, string} = PropTypes
|
||||
|
||||
KapacitorRulesTable.propTypes = {
|
||||
|
@ -128,17 +104,4 @@ RuleRow.propTypes = {
|
|||
onDelete: func,
|
||||
}
|
||||
|
||||
RuleTitle.propTypes = {
|
||||
rule: shape({
|
||||
name: string.isRequired,
|
||||
query: shape(),
|
||||
links: shape({
|
||||
self: string.isRequired,
|
||||
}),
|
||||
}),
|
||||
source: shape({
|
||||
id: string.isRequired,
|
||||
}).isRequired,
|
||||
}
|
||||
|
||||
export default KapacitorRulesTable
|
||||
|
|
|
@ -2,25 +2,26 @@ import React, {PropTypes} from 'react'
|
|||
import {Link} from 'react-router'
|
||||
import _ from 'lodash'
|
||||
|
||||
import ConfirmButton from 'src/shared/components/ConfirmButton'
|
||||
import {TASKS_TABLE} from 'src/kapacitor/constants/tableSizing'
|
||||
|
||||
const {colID, colType, colEnabled, colActions} = TASKS_TABLE
|
||||
const {colName, colType, colEnabled, colActions} = TASKS_TABLE
|
||||
|
||||
const TasksTable = ({tasks, source, onDelete, onChangeRuleStatus}) =>
|
||||
<div className="panel-body">
|
||||
<table className="table v-center">
|
||||
<table className="table v-center table-highlight">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{width: colID}}>ID</th>
|
||||
<th style={{minWidth: colName}}>Name</th>
|
||||
<th style={{width: colType}}>Type</th>
|
||||
<th style={{width: colEnabled}} className="text-center">
|
||||
Enabled
|
||||
Task Enabled
|
||||
</th>
|
||||
<th style={{width: colActions}} />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{_.sortBy(tasks, t => t.name.toLowerCase()).map(task => {
|
||||
{_.sortBy(tasks, t => t.id.toLowerCase()).map(task => {
|
||||
return (
|
||||
<TaskRow
|
||||
key={task.id}
|
||||
|
@ -39,15 +40,18 @@ const handleDelete = (task, onDelete) => onDelete(task)
|
|||
|
||||
const TaskRow = ({task, source, onDelete, onChangeRuleStatus}) =>
|
||||
<tr key={task.id}>
|
||||
<td style={{width: colID}} className="monotype">
|
||||
<i>
|
||||
{task.id}
|
||||
</i>
|
||||
<td style={{minWidth: colName}}>
|
||||
<Link
|
||||
className="link-success"
|
||||
to={`/sources/${source.id}/tickscript/${task.id}`}
|
||||
>
|
||||
{task.name}
|
||||
</Link>
|
||||
</td>
|
||||
<td style={{width: colType}} className="monotype">
|
||||
<td style={{width: colType, textTransform: 'capitalize'}}>
|
||||
{task.type}
|
||||
</td>
|
||||
<td style={{width: colEnabled}} className="monotype text-center">
|
||||
<td style={{width: colEnabled}} className="text-center">
|
||||
<div className="dark-checkbox">
|
||||
<input
|
||||
id={`kapacitor-enabled ${task.id}`}
|
||||
|
@ -59,19 +63,14 @@ const TaskRow = ({task, source, onDelete, onChangeRuleStatus}) =>
|
|||
<label htmlFor={`kapacitor-enabled ${task.id}`} />
|
||||
</div>
|
||||
</td>
|
||||
<td style={{width: colActions}} className="text-right table-cell-nowrap">
|
||||
<Link
|
||||
className="btn btn-info btn-xs"
|
||||
to={`/sources/${source.id}/tickscript/${task.id}`}
|
||||
>
|
||||
Edit TICKscript
|
||||
</Link>
|
||||
<button
|
||||
className="btn btn-danger btn-xs"
|
||||
onClick={handleDelete(task, onDelete)}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
<td style={{width: colActions}} className="text-right">
|
||||
<ConfirmButton
|
||||
text="Delete"
|
||||
type="btn-danger"
|
||||
size="btn-xs"
|
||||
customClass="table--show-on-row-hover"
|
||||
confirmAction={handleDelete(task, onDelete)}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
|
|
|
@ -1,15 +1,9 @@
|
|||
export const KAPACITOR_RULES_TABLE = {
|
||||
export const TASKS_TABLE = {
|
||||
colName: '200px',
|
||||
colTrigger: '90px',
|
||||
colMessage: '460px',
|
||||
colMessage: '200px',
|
||||
colAlerts: '120px',
|
||||
colEnabled: '64px',
|
||||
colActions: '176px',
|
||||
}
|
||||
|
||||
export const TASKS_TABLE = {
|
||||
colID: '200px',
|
||||
colEnabled: '95px',
|
||||
colActions: '68px',
|
||||
colType: '90px',
|
||||
colEnabled: '64px',
|
||||
colActions: '176px',
|
||||
}
|
||||
|
|
|
@ -65,7 +65,7 @@ export const addAnnotationAsync = (createUrl, annotation) => async dispatch => {
|
|||
|
||||
export const getAnnotationsAsync = (indexUrl, since) => async dispatch => {
|
||||
const annotations = await api.getAnnotations(indexUrl, since)
|
||||
annotations.forEach(a => dispatch(addAnnotation(a)))
|
||||
dispatch(loadAnnotations(annotations))
|
||||
}
|
||||
|
||||
export const deleteAnnotationAsync = annotation => async dispatch => {
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
export const ANNOTATION_MIN_DELTA = 0.5
|
||||
|
||||
export const ADDING = 'adding'
|
||||
export const EDITING = 'editing'
|
||||
|
||||
|
@ -9,7 +11,7 @@ export const TEMP_ANNOTATION = {
|
|||
endTime: '',
|
||||
}
|
||||
|
||||
export const getAnnotations = (graph, annotations = []) => {
|
||||
export const visibleAnnotations = (graph, annotations = []) => {
|
||||
const [xStart, xEnd] = graph.xAxisRange()
|
||||
|
||||
if (xStart === 0 && xEnd === 0) {
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
import {connect} from 'react-redux'
|
||||
|
||||
import {EDITING} from 'shared/annotations/helpers'
|
||||
import {DYGRAPH_CONTAINER_MARGIN} from 'shared/constants'
|
||||
import {ANNOTATION_MIN_DELTA, EDITING} from 'shared/annotations/helpers'
|
||||
import * as schema from 'shared/schemas'
|
||||
import * as actions from 'shared/actions/annotations'
|
||||
import AnnotationTooltip from 'shared/components/AnnotationTooltip'
|
||||
|
@ -55,14 +56,12 @@ class AnnotationPoint extends React.Component {
|
|||
let newTime = dygraph.toDataXCoord(graphX)
|
||||
const oldTime = +startTime
|
||||
|
||||
const minPercentChange = 0.5
|
||||
|
||||
if (
|
||||
Math.abs(
|
||||
dygraph.toPercentXCoord(newTime) - dygraph.toPercentXCoord(oldTime)
|
||||
) *
|
||||
100 <
|
||||
minPercentChange
|
||||
ANNOTATION_MIN_DELTA
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
@ -87,28 +86,25 @@ class AnnotationPoint extends React.Component {
|
|||
|
||||
render() {
|
||||
const {annotation, mode, dygraph} = this.props
|
||||
const {isDragging} = this.state
|
||||
|
||||
const isEditing = mode === EDITING
|
||||
const humanTime = `${new Date(+annotation.startTime)}`
|
||||
const {isDragging} = this.state
|
||||
|
||||
const flagClass = isDragging
|
||||
? 'annotation-point--flag__dragging'
|
||||
: 'annotation-point--flag'
|
||||
|
||||
const markerClass = isDragging ? 'annotation dragging' : 'annotation'
|
||||
|
||||
const clickClass = isEditing
|
||||
? 'annotation--click-area editing'
|
||||
: 'annotation--click-area'
|
||||
|
||||
const left = `${dygraph.toDomXCoord(annotation.startTime) + 16}px`
|
||||
const left = `${dygraph.toDomXCoord(annotation.startTime) +
|
||||
DYGRAPH_CONTAINER_MARGIN}px`
|
||||
|
||||
return (
|
||||
<div
|
||||
className={markerClass}
|
||||
style={{left}}
|
||||
data-time-ms={annotation.startTime}
|
||||
data-time-local={humanTime}
|
||||
>
|
||||
<div className={markerClass} style={{left}}>
|
||||
<div
|
||||
className={clickClass}
|
||||
draggable={true}
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
import {connect} from 'react-redux'
|
||||
|
||||
import {EDITING} from 'shared/annotations/helpers'
|
||||
import {DYGRAPH_CONTAINER_MARGIN} from 'shared/constants'
|
||||
import {ANNOTATION_MIN_DELTA, EDITING} from 'shared/annotations/helpers'
|
||||
import * as schema from 'shared/schemas'
|
||||
import * as actions from 'shared/actions/annotations'
|
||||
import AnnotationTooltip from 'shared/components/AnnotationTooltip'
|
||||
|
@ -65,14 +66,12 @@ class AnnotationSpan extends React.Component {
|
|||
const graphX = pageX - left
|
||||
let newTime = dygraph.toDataXCoord(graphX)
|
||||
|
||||
const minPercentChange = 0.5
|
||||
|
||||
if (
|
||||
Math.abs(
|
||||
dygraph.toPercentXCoord(newTime) - dygraph.toPercentXCoord(oldTime)
|
||||
) *
|
||||
100 <
|
||||
minPercentChange
|
||||
ANNOTATION_MIN_DELTA
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
@ -92,7 +91,6 @@ class AnnotationSpan extends React.Component {
|
|||
|
||||
renderLeftMarker(startTime, dygraph) {
|
||||
const isEditing = this.props.mode === EDITING
|
||||
const humanTime = `${new Date(+startTime)}`
|
||||
const {isDragging, isMouseOver} = this.state
|
||||
const {annotation} = this.props
|
||||
|
||||
|
@ -110,15 +108,10 @@ class AnnotationSpan extends React.Component {
|
|||
}
|
||||
const showTooltip = isDragging === 'left' || isMouseOver === 'left'
|
||||
|
||||
const left = dygraph.toDomXCoord(startTime) + 16
|
||||
const left = dygraph.toDomXCoord(startTime) + DYGRAPH_CONTAINER_MARGIN
|
||||
|
||||
return (
|
||||
<div
|
||||
className={markerClass}
|
||||
style={{left: `${left}px`}}
|
||||
data-time-ms={startTime}
|
||||
data-time-local={humanTime}
|
||||
>
|
||||
<div className={markerClass} style={{left: `${left}px`}}>
|
||||
{showTooltip &&
|
||||
<AnnotationTooltip
|
||||
isEditing={isEditing}
|
||||
|
@ -161,7 +154,7 @@ class AnnotationSpan extends React.Component {
|
|||
}
|
||||
const showTooltip = isDragging === 'right' || isMouseOver === 'right'
|
||||
|
||||
const left = `${dygraph.toDomXCoord(endTime) + 16}px`
|
||||
const left = `${dygraph.toDomXCoord(endTime) + DYGRAPH_CONTAINER_MARGIN}px`
|
||||
|
||||
return (
|
||||
<div
|
||||
|
@ -201,7 +194,7 @@ class AnnotationSpan extends React.Component {
|
|||
<AnnotationWindow
|
||||
annotation={annotation}
|
||||
dygraph={dygraph}
|
||||
active={isDragging}
|
||||
active={!!isDragging}
|
||||
staticLegendHeight={staticLegendHeight}
|
||||
/>
|
||||
{this.renderLeftMarker(annotation.startTime, dygraph)}
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
|
||||
import {DYGRAPH_CONTAINER_MARGIN} from 'shared/constants'
|
||||
import * as schema from 'shared/schemas'
|
||||
|
||||
const containerLeftPadding = 16
|
||||
|
||||
const windowDimensions = (anno, dygraph, staticLegendHeight) => {
|
||||
// TODO: export and test this function
|
||||
const [startX, endX] = dygraph.xAxisRange()
|
||||
|
@ -12,21 +11,18 @@ const windowDimensions = (anno, dygraph, staticLegendHeight) => {
|
|||
|
||||
const windowStartXCoord = dygraph.toDomXCoord(startTime)
|
||||
const windowEndXCoord = dygraph.toDomXCoord(endTime)
|
||||
const windowWidth = Math.abs(windowEndXCoord - windowStartXCoord)
|
||||
|
||||
const windowWidth = windowEndXCoord - windowStartXCoord
|
||||
const isDurationNegative = windowWidth < 0
|
||||
const foo = isDurationNegative ? windowWidth : 0
|
||||
|
||||
const left = `${windowStartXCoord + containerLeftPadding + foo}px`
|
||||
const width = `${Math.abs(windowWidth)}px`
|
||||
const windowLeftXCoord =
|
||||
Math.min(windowStartXCoord, windowEndXCoord) + DYGRAPH_CONTAINER_MARGIN
|
||||
|
||||
const height = staticLegendHeight
|
||||
? `calc(100% - ${staticLegendHeight + 36}px)`
|
||||
: 'calc(100% - 36px)'
|
||||
|
||||
return {
|
||||
left,
|
||||
width,
|
||||
left: `${windowLeftXCoord}px`,
|
||||
width: `${windowWidth}px`,
|
||||
height,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ import {
|
|||
mouseEnterTempAnnotation,
|
||||
mouseLeaveTempAnnotation,
|
||||
} from 'src/shared/actions/annotations'
|
||||
import {getAnnotations} from 'src/shared/annotations/helpers'
|
||||
import {visibleAnnotations} from 'src/shared/annotations/helpers'
|
||||
|
||||
class Annotations extends Component {
|
||||
state = {
|
||||
|
@ -26,6 +26,10 @@ class Annotations extends Component {
|
|||
this.props.annotationsRef(this)
|
||||
}
|
||||
|
||||
heartbeat = () => {
|
||||
this.setState({lastUpdated: Date.now()})
|
||||
}
|
||||
|
||||
render() {
|
||||
const {lastUpdated} = this.state
|
||||
const {
|
||||
|
@ -40,9 +44,10 @@ class Annotations extends Component {
|
|||
staticLegendHeight,
|
||||
} = this.props
|
||||
|
||||
const annotations = getAnnotations(dygraph, this.props.annotations).filter(
|
||||
a => a.id !== TEMP_ANNOTATION.id
|
||||
)
|
||||
const annotations = visibleAnnotations(
|
||||
dygraph,
|
||||
this.props.annotations
|
||||
).filter(a => a.id !== TEMP_ANNOTATION.id)
|
||||
const tempAnnotation = this.props.annotations.find(
|
||||
a => a.id === TEMP_ANNOTATION.id
|
||||
)
|
||||
|
|
|
@ -9,7 +9,7 @@ const AutoRefresh = ComposedComponent => {
|
|||
constructor() {
|
||||
super()
|
||||
this.state = {
|
||||
lastQuerySuccessful: false,
|
||||
lastQuerySuccessful: true,
|
||||
timeSeries: [],
|
||||
resolution: null,
|
||||
}
|
||||
|
@ -27,6 +27,8 @@ const AutoRefresh = ComposedComponent => {
|
|||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const inViewDidUpdate = this.props.inView !== nextProps.inView
|
||||
|
||||
const queriesDidUpdate = this.queryDifference(
|
||||
this.props.queries,
|
||||
nextProps.queries
|
||||
|
@ -37,10 +39,15 @@ const AutoRefresh = ComposedComponent => {
|
|||
nextProps.templates
|
||||
)
|
||||
|
||||
const shouldRefetch = queriesDidUpdate || tempVarsDidUpdate
|
||||
const shouldRefetch =
|
||||
queriesDidUpdate || tempVarsDidUpdate || inViewDidUpdate
|
||||
|
||||
if (shouldRefetch) {
|
||||
this.executeQueries(nextProps.queries, nextProps.templates)
|
||||
this.executeQueries(
|
||||
nextProps.queries,
|
||||
nextProps.templates,
|
||||
nextProps.inView
|
||||
)
|
||||
}
|
||||
|
||||
if (this.props.autoRefresh !== nextProps.autoRefresh || shouldRefetch) {
|
||||
|
@ -48,7 +55,12 @@ const AutoRefresh = ComposedComponent => {
|
|||
|
||||
if (nextProps.autoRefresh) {
|
||||
this.intervalID = setInterval(
|
||||
() => this.executeQueries(nextProps.queries, nextProps.templates),
|
||||
() =>
|
||||
this.executeQueries(
|
||||
nextProps.queries,
|
||||
nextProps.templates,
|
||||
nextProps.inView
|
||||
),
|
||||
nextProps.autoRefresh
|
||||
)
|
||||
}
|
||||
|
@ -64,10 +76,16 @@ const AutoRefresh = ComposedComponent => {
|
|||
)
|
||||
}
|
||||
|
||||
executeQueries = async (queries, templates = []) => {
|
||||
executeQueries = async (
|
||||
queries,
|
||||
templates = [],
|
||||
inView = this.props.inView
|
||||
) => {
|
||||
const {editQueryStatus, grabDataForDownload} = this.props
|
||||
const {resolution} = this.state
|
||||
|
||||
if (!inView) {
|
||||
return
|
||||
}
|
||||
if (!queries.length) {
|
||||
this.setState({timeSeries: []})
|
||||
return
|
||||
|
@ -148,7 +166,15 @@ const AutoRefresh = ComposedComponent => {
|
|||
const {timeSeries} = this.state
|
||||
|
||||
if (this.state.isFetching && this.state.lastQuerySuccessful) {
|
||||
return this.renderFetching(timeSeries)
|
||||
return (
|
||||
<ComposedComponent
|
||||
{...this.props}
|
||||
data={timeSeries}
|
||||
setResolution={this.setResolution}
|
||||
isFetchingInitially={false}
|
||||
isRefreshing={true}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -160,23 +186,6 @@ const AutoRefresh = ComposedComponent => {
|
|||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Graphs can potentially show mulitple kinds of spinners based on whether
|
||||
* a graph is being fetched for the first time, or is being refreshed.
|
||||
*/
|
||||
renderFetching = data => {
|
||||
const isFirstFetch = !Object.keys(this.state.timeSeries).length
|
||||
return (
|
||||
<ComposedComponent
|
||||
{...this.props}
|
||||
data={data}
|
||||
setResolution={this.setResolution}
|
||||
isFetchingInitially={isFirstFetch}
|
||||
isRefreshing={!isFirstFetch}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
_resultsForQuery = data =>
|
||||
data.length
|
||||
? data.every(({response}) =>
|
||||
|
@ -204,6 +213,7 @@ const AutoRefresh = ComposedComponent => {
|
|||
wrapper.propTypes = {
|
||||
children: element,
|
||||
autoRefresh: number.isRequired,
|
||||
inView: bool,
|
||||
templates: arrayOf(
|
||||
shape({
|
||||
type: string.isRequired,
|
||||
|
|
|
@ -155,9 +155,7 @@ class Dygraph extends Component {
|
|||
colors: this.getLineColors(),
|
||||
series: this.hashColorDygraphSeries(),
|
||||
plotter: isBarGraph ? barPlotter : null,
|
||||
drawCallback: () => {
|
||||
this.annotationsRef.setState({lastUpdated: Date.now()})
|
||||
},
|
||||
drawCallback: this.annotationsRef.heartbeat,
|
||||
}
|
||||
|
||||
dygraph.updateOptions(updateOptions)
|
||||
|
|
|
@ -10,6 +10,7 @@ class FancyScrollbar extends Component {
|
|||
static defaultProps = {
|
||||
autoHide: true,
|
||||
autoHeight: false,
|
||||
setScrollTop: () => {},
|
||||
}
|
||||
|
||||
handleMakeDiv = className => props => {
|
||||
|
@ -17,13 +18,21 @@ class FancyScrollbar extends Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
const {autoHide, autoHeight, children, className, maxHeight} = this.props
|
||||
const {
|
||||
autoHide,
|
||||
autoHeight,
|
||||
children,
|
||||
className,
|
||||
maxHeight,
|
||||
setScrollTop,
|
||||
} = this.props
|
||||
|
||||
return (
|
||||
<Scrollbars
|
||||
className={classnames('fancy-scroll--container', {
|
||||
[className]: className,
|
||||
})}
|
||||
onScroll={setScrollTop}
|
||||
autoHide={autoHide}
|
||||
autoHideTimeout={1000}
|
||||
autoHideDuration={250}
|
||||
|
@ -41,7 +50,7 @@ class FancyScrollbar extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
const {bool, node, number, string} = PropTypes
|
||||
const {bool, func, node, number, string} = PropTypes
|
||||
|
||||
FancyScrollbar.propTypes = {
|
||||
children: node.isRequired,
|
||||
|
@ -49,6 +58,7 @@ FancyScrollbar.propTypes = {
|
|||
autoHide: bool,
|
||||
autoHeight: bool,
|
||||
maxHeight: number,
|
||||
setScrollTop: func,
|
||||
}
|
||||
|
||||
export default FancyScrollbar
|
||||
|
|
|
@ -226,6 +226,7 @@ class Gauge extends Component {
|
|||
minValue,
|
||||
maxValue
|
||||
) => {
|
||||
const {prefix, suffix} = this.props
|
||||
const {degree, lineCount, labelColor, labelFontSize} = GAUGE_SPECS
|
||||
|
||||
const incrementValue = (maxValue - minValue) / lineCount
|
||||
|
@ -258,12 +259,14 @@ class Gauge extends Component {
|
|||
if (i > 3) {
|
||||
ctx.textAlign = 'left'
|
||||
}
|
||||
const labelText = `${prefix}${gaugeValues[i]}${suffix}`
|
||||
|
||||
ctx.rotate(startDegree)
|
||||
ctx.rotate(i * arcIncrement)
|
||||
ctx.translate(labelRadius, 0)
|
||||
ctx.rotate(i * -arcIncrement)
|
||||
ctx.rotate(-startDegree)
|
||||
ctx.fillText(gaugeValues[i], 0, 0)
|
||||
ctx.fillText(labelText, 0, 0)
|
||||
ctx.rotate(startDegree)
|
||||
ctx.rotate(i * arcIncrement)
|
||||
ctx.translate(-labelRadius, 0)
|
||||
|
@ -273,7 +276,7 @@ class Gauge extends Component {
|
|||
}
|
||||
|
||||
drawGaugeValue = (ctx, radius, labelValueFontSize) => {
|
||||
const {gaugePosition} = this.props
|
||||
const {gaugePosition, prefix, suffix} = this.props
|
||||
const {valueColor} = GAUGE_SPECS
|
||||
|
||||
ctx.font = `${labelValueFontSize}px Roboto`
|
||||
|
@ -282,7 +285,8 @@ class Gauge extends Component {
|
|||
ctx.textAlign = 'center'
|
||||
|
||||
const textY = radius
|
||||
ctx.fillText(gaugePosition.toString(), 0, textY)
|
||||
const textContent = `${prefix}${gaugePosition.toString()}${suffix}`
|
||||
ctx.fillText(textContent, 0, textY)
|
||||
}
|
||||
|
||||
drawNeedle = (ctx, radius, minValue, maxValue) => {
|
||||
|
@ -335,6 +339,8 @@ Gauge.propTypes = {
|
|||
value: string.isRequired,
|
||||
}).isRequired
|
||||
).isRequired,
|
||||
prefix: string.isRequired,
|
||||
suffix: string.isRequired,
|
||||
}
|
||||
|
||||
export default Gauge
|
||||
|
|
|
@ -12,11 +12,14 @@ class GaugeChart extends PureComponent {
|
|||
render() {
|
||||
const {
|
||||
data,
|
||||
cellID,
|
||||
cellHeight,
|
||||
isFetchingInitially,
|
||||
colors,
|
||||
resizeCoords,
|
||||
resizerTopHeight,
|
||||
prefix,
|
||||
suffix,
|
||||
} = this.props
|
||||
|
||||
// If data for this graph is being fetched for the first time, show a graph-wide spinner.
|
||||
|
@ -35,11 +38,15 @@ class GaugeChart extends PureComponent {
|
|||
// When a new height is passed the Gauge component resizes internally
|
||||
// Passing in a new often ensures the gauge appears sharp
|
||||
|
||||
const thisGaugeIsResizing = resizeCoords ? cellID === resizeCoords.i : false
|
||||
|
||||
const initialCellHeight =
|
||||
cellHeight && (cellHeight * DASHBOARD_LAYOUT_ROW_HEIGHT).toString()
|
||||
|
||||
const resizeCoordsHeight =
|
||||
resizeCoords && (resizeCoords.h * DASHBOARD_LAYOUT_ROW_HEIGHT).toString()
|
||||
resizeCoords &&
|
||||
thisGaugeIsResizing &&
|
||||
(resizeCoords.h * DASHBOARD_LAYOUT_ROW_HEIGHT).toString()
|
||||
|
||||
const height = (resizeCoordsHeight ||
|
||||
initialCellHeight ||
|
||||
|
@ -54,6 +61,8 @@ class GaugeChart extends PureComponent {
|
|||
height={height}
|
||||
colors={colors}
|
||||
gaugePosition={roundedValue}
|
||||
prefix={prefix}
|
||||
suffix={suffix}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
@ -69,6 +78,7 @@ GaugeChart.defaultProps = {
|
|||
GaugeChart.propTypes = {
|
||||
data: arrayOf(shape()).isRequired,
|
||||
isFetchingInitially: bool,
|
||||
cellID: string,
|
||||
cellHeight: number,
|
||||
resizerTopHeight: number,
|
||||
resizeCoords: shape(),
|
||||
|
@ -81,6 +91,8 @@ GaugeChart.propTypes = {
|
|||
value: string.isRequired,
|
||||
}).isRequired
|
||||
),
|
||||
prefix: string.isRequired,
|
||||
suffix: string.isRequired,
|
||||
}
|
||||
|
||||
export default GaugeChart
|
||||
|
|
|
@ -73,11 +73,12 @@ const Layout = (
|
|||
{cell.isWidget
|
||||
? <WidgetCell cell={cell} timeRange={timeRange} source={source} />
|
||||
: <RefreshingGraph
|
||||
colors={colors}
|
||||
inView={cell.inView}
|
||||
axes={axes}
|
||||
type={type}
|
||||
staticLegend={IS_STATIC_LEGEND(legend)}
|
||||
cellHeight={h}
|
||||
colors={colors}
|
||||
onZoom={onZoom}
|
||||
sources={sources}
|
||||
timeRange={timeRange}
|
||||
|
|
|
@ -34,6 +34,11 @@ class LayoutCell extends Component {
|
|||
|
||||
const queries = _.get(cell, ['queries'], [])
|
||||
|
||||
// Passing the cell ID into the child graph so that further along
|
||||
// we can detect if "this cell is the one being resized"
|
||||
const child = children.length ? children[0] : children
|
||||
const layoutCellGraph = React.cloneElement(child, {cellID: cell.i})
|
||||
|
||||
return (
|
||||
<div className="dash-graph">
|
||||
<Authorized requiredRole={EDITOR_ROLE}>
|
||||
|
@ -50,7 +55,7 @@ class LayoutCell extends Component {
|
|||
<LayoutCellHeader cellName={cell.name} isEditable={isEditable} />
|
||||
<div className="dash-graph--container">
|
||||
{queries.length
|
||||
? children
|
||||
? layoutCellGraph
|
||||
: <div className="graph-empty">
|
||||
<Authorized requiredRole={EDITOR_ROLE}>
|
||||
<button
|
||||
|
@ -71,6 +76,7 @@ const {arrayOf, bool, func, node, number, shape, string} = PropTypes
|
|||
|
||||
LayoutCell.propTypes = {
|
||||
cell: shape({
|
||||
i: string.isRequired,
|
||||
name: string.isRequired,
|
||||
isEditing: bool,
|
||||
x: number.isRequired,
|
||||
|
|
|
@ -8,6 +8,7 @@ import MenuTooltipButton from 'src/shared/components/MenuTooltipButton'
|
|||
import CustomTimeIndicator from 'src/shared/components/CustomTimeIndicator'
|
||||
|
||||
import {EDITING} from 'src/shared/annotations/helpers'
|
||||
import {cellSupportsAnnotations} from 'src/shared/constants/index'
|
||||
|
||||
import {
|
||||
addingAnnotation,
|
||||
|
@ -61,10 +62,15 @@ class LayoutCellMenu extends Component {
|
|||
icon="pencil"
|
||||
menuOptions={[
|
||||
{text: 'Configure', action: onEdit(cell)},
|
||||
{text: 'Add Annotation', action: onStartAddingAnnotation},
|
||||
{
|
||||
text: 'Add Annotation',
|
||||
action: onStartAddingAnnotation,
|
||||
disabled: !cellSupportsAnnotations(cell.type),
|
||||
},
|
||||
{
|
||||
text: 'Edit Annotations',
|
||||
action: onStartEditingAnnotation,
|
||||
disabled: !cellSupportsAnnotations(cell.type),
|
||||
},
|
||||
]}
|
||||
informParent={this.handleToggleSubMenu}
|
||||
|
@ -91,7 +97,7 @@ class LayoutCellMenu extends Component {
|
|||
className="btn btn-xs btn-success"
|
||||
onClick={onDismissEditingAnnotation}
|
||||
>
|
||||
Done
|
||||
Done Editing
|
||||
</div>
|
||||
</div>}
|
||||
</div>
|
||||
|
|
|
@ -33,13 +33,11 @@ class LayoutRenderer extends Component {
|
|||
if (!this.props.onPositionChange) {
|
||||
return
|
||||
}
|
||||
|
||||
const newCells = this.props.cells.map(cell => {
|
||||
const l = layout.find(ly => ly.i === cell.i)
|
||||
const newLayout = {x: l.x, y: l.y, h: l.h, w: l.w}
|
||||
return {...cell, ...newLayout}
|
||||
})
|
||||
|
||||
this.props.onPositionChange(newCells)
|
||||
}
|
||||
|
||||
|
|
|
@ -45,8 +45,12 @@ class MenuTooltipButton extends Component {
|
|||
return menuOptions.map((option, i) =>
|
||||
<div
|
||||
key={i}
|
||||
className="dash-graph-context--menu-item"
|
||||
onClick={this.handleMenuItemClick(option.action)}
|
||||
className={`dash-graph-context--menu-item${option.disabled
|
||||
? ' disabled'
|
||||
: ''}`}
|
||||
onClick={
|
||||
option.disabled ? null : this.handleMenuItemClick(option.action)
|
||||
}
|
||||
>
|
||||
{option.text}
|
||||
</div>
|
||||
|
@ -73,7 +77,7 @@ class MenuTooltipButton extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
const {arrayOf, func, shape, string} = PropTypes
|
||||
const {arrayOf, bool, func, shape, string} = PropTypes
|
||||
|
||||
MenuTooltipButton.defaultProps = {
|
||||
theme: 'default',
|
||||
|
@ -86,6 +90,7 @@ MenuTooltipButton.propTypes = {
|
|||
shape({
|
||||
text: string.isRequired,
|
||||
action: func.isRequired,
|
||||
disabled: bool,
|
||||
})
|
||||
).isRequired,
|
||||
informParent: func,
|
||||
|
|
|
@ -14,14 +14,22 @@ class NewAnnotation extends Component {
|
|||
gatherMode: 'startTime',
|
||||
}
|
||||
|
||||
clampWithinGraphTimerange = timestamp => {
|
||||
const [xRangeStart] = this.props.dygraph.xAxisRange()
|
||||
return Math.max(xRangeStart, timestamp)
|
||||
}
|
||||
|
||||
eventToTimestamp = ({pageX: pxBetweenMouseAndPage}) => {
|
||||
const {left: pxBetweenGraphAndPage} = this.wrapper.getBoundingClientRect()
|
||||
const graphXCoordinate = pxBetweenMouseAndPage - pxBetweenGraphAndPage
|
||||
const timestamp = this.props.dygraph.toDataXCoord(graphXCoordinate)
|
||||
const clamped = this.clampWithinGraphTimerange(timestamp)
|
||||
return `${clamped}`
|
||||
}
|
||||
|
||||
handleMouseDown = e => {
|
||||
const {tempAnnotation, dygraph, onUpdateAnnotation} = this.props
|
||||
|
||||
const wrapperRect = this.wrapper.getBoundingClientRect()
|
||||
const trueGraphX = e.pageX - wrapperRect.left
|
||||
const startTime = `${dygraph.toDataXCoord(trueGraphX)}`
|
||||
|
||||
onUpdateAnnotation({...tempAnnotation, startTime})
|
||||
const startTime = this.eventToTimestamp(e)
|
||||
this.props.onUpdateAnnotation({...this.props.tempAnnotation, startTime})
|
||||
this.setState({gatherMode: 'endTime'})
|
||||
}
|
||||
|
||||
|
@ -30,11 +38,8 @@ class NewAnnotation extends Component {
|
|||
return
|
||||
}
|
||||
|
||||
const {dygraph, tempAnnotation, onUpdateAnnotation} = this.props
|
||||
const wrapperRect = this.wrapper.getBoundingClientRect()
|
||||
const trueGraphX = e.pageX - wrapperRect.left
|
||||
|
||||
const newTime = `${dygraph.toDataXCoord(trueGraphX)}`
|
||||
const {tempAnnotation, onUpdateAnnotation} = this.props
|
||||
const newTime = this.eventToTimestamp(e)
|
||||
|
||||
if (this.state.gatherMode === 'startTime') {
|
||||
onUpdateAnnotation({
|
||||
|
@ -49,22 +54,19 @@ class NewAnnotation extends Component {
|
|||
|
||||
handleMouseUp = e => {
|
||||
const {
|
||||
tempAnnotation,
|
||||
onUpdateAnnotation,
|
||||
addAnnotationAsync,
|
||||
onAddingAnnotationSuccess,
|
||||
onMouseLeaveTempAnnotation,
|
||||
} = this.props
|
||||
|
||||
const createUrl = this.context.source.links.annotations
|
||||
|
||||
const {dygraph, tempAnnotation, onUpdateAnnotation} = this.props
|
||||
const wrapperRect = this.wrapper.getBoundingClientRect()
|
||||
const trueGraphX = e.pageX - wrapperRect.left
|
||||
const upTime = `${dygraph.toDataXCoord(trueGraphX)}`
|
||||
|
||||
const upTime = this.eventToTimestamp(e)
|
||||
const downTime = tempAnnotation.startTime
|
||||
const [startTime, endTime] = [downTime, upTime].sort()
|
||||
|
||||
const newAnnotation = {...tempAnnotation, startTime, endTime}
|
||||
|
||||
onUpdateAnnotation(newAnnotation)
|
||||
addAnnotationAsync(createUrl, {...newAnnotation, id: uuid.v4()})
|
||||
|
||||
|
@ -117,6 +119,7 @@ class NewAnnotation extends Component {
|
|||
tempAnnotation: {startTime, endTime},
|
||||
staticLegendHeight,
|
||||
} = this.props
|
||||
const {isMouseOver} = this.state
|
||||
|
||||
const crosshairOne = Math.max(-1000, dygraph.toDomXCoord(startTime))
|
||||
const crosshairTwo = dygraph.toDomXCoord(endTime)
|
||||
|
@ -157,14 +160,18 @@ class NewAnnotation extends Component {
|
|||
className="new-annotation--crosshair"
|
||||
style={{left: crosshairTwo}}
|
||||
>
|
||||
{this.renderTimestamp(tempAnnotation.endTime)}
|
||||
{isMouseOver &&
|
||||
isDragging &&
|
||||
this.renderTimestamp(tempAnnotation.endTime)}
|
||||
<div className={flagTwoClass} />
|
||||
</div>}
|
||||
<div
|
||||
className="new-annotation--crosshair"
|
||||
style={{left: crosshairOne}}
|
||||
>
|
||||
{isDragging || this.renderTimestamp(tempAnnotation.startTime)}
|
||||
{isMouseOver &&
|
||||
!isDragging &&
|
||||
this.renderTimestamp(tempAnnotation.startTime)}
|
||||
<div className={isDragging ? flagOneClass : pointFlagClass} />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -13,9 +13,11 @@ const RefreshingGaugeChart = AutoRefresh(GaugeChart)
|
|||
|
||||
const RefreshingGraph = ({
|
||||
axes,
|
||||
inView,
|
||||
type,
|
||||
colors,
|
||||
onZoom,
|
||||
cellID,
|
||||
queries,
|
||||
templates,
|
||||
timeRange,
|
||||
|
@ -29,6 +31,9 @@ const RefreshingGraph = ({
|
|||
editQueryStatus,
|
||||
grabDataForDownload,
|
||||
}) => {
|
||||
const prefix = (axes && axes.y.prefix) || ''
|
||||
const suffix = (axes && axes.y.suffix) || ''
|
||||
|
||||
if (!queries.length) {
|
||||
return (
|
||||
<div className="graph-empty">
|
||||
|
@ -40,8 +45,6 @@ const RefreshingGraph = ({
|
|||
}
|
||||
|
||||
if (type === 'single-stat') {
|
||||
const suffix = axes.y.suffix || ''
|
||||
|
||||
return (
|
||||
<RefreshingSingleStat
|
||||
colors={colors}
|
||||
|
@ -50,6 +53,7 @@ const RefreshingGraph = ({
|
|||
templates={templates}
|
||||
autoRefresh={autoRefresh}
|
||||
cellHeight={cellHeight}
|
||||
prefix={prefix}
|
||||
suffix={suffix}
|
||||
/>
|
||||
)
|
||||
|
@ -66,6 +70,9 @@ const RefreshingGraph = ({
|
|||
cellHeight={cellHeight}
|
||||
resizerTopHeight={resizerTopHeight}
|
||||
resizeCoords={resizeCoords}
|
||||
cellID={cellID}
|
||||
prefix={prefix}
|
||||
suffix={suffix}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -81,6 +88,7 @@ const RefreshingGraph = ({
|
|||
colors={colors}
|
||||
onZoom={onZoom}
|
||||
queries={queries}
|
||||
inView={inView}
|
||||
key={manualRefresh}
|
||||
templates={templates}
|
||||
timeRange={timeRange}
|
||||
|
@ -126,11 +134,14 @@ RefreshingGraph.propTypes = {
|
|||
value: string.isRequired,
|
||||
}).isRequired
|
||||
),
|
||||
cellID: string,
|
||||
inView: bool,
|
||||
}
|
||||
|
||||
RefreshingGraph.defaultProps = {
|
||||
manualRefresh: 0,
|
||||
staticLegend: false,
|
||||
inView: true,
|
||||
}
|
||||
|
||||
export default RefreshingGraph
|
||||
|
|
|
@ -415,6 +415,8 @@ export const PAGE_CONTAINER_MARGIN = 30 // TODO: get this dynamically to ensure
|
|||
export const LAYOUT_MARGIN = 4
|
||||
export const DASHBOARD_LAYOUT_ROW_HEIGHT = 83.5
|
||||
|
||||
export const DYGRAPH_CONTAINER_MARGIN = 16
|
||||
|
||||
export const DEFAULT_SOURCE = {
|
||||
url: 'http://localhost:8086',
|
||||
name: 'Influx 1',
|
||||
|
@ -430,3 +432,14 @@ export const IS_STATIC_LEGEND = legend =>
|
|||
_.get(legend, 'type', false) === 'static'
|
||||
|
||||
export const linksLink = '/chronograf/v1'
|
||||
|
||||
export const cellSupportsAnnotations = cellType => {
|
||||
const supportedTypes = [
|
||||
'line',
|
||||
'bar',
|
||||
'line-plus-single-stat',
|
||||
'line-stacked',
|
||||
'line-stepplot',
|
||||
]
|
||||
return !!supportedTypes.find(type => type === cellType)
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import _ from 'lodash'
|
||||
|
||||
const emptyFunny = [
|
||||
'Looks like you don’t have any queries. Be a lot cooler if you did.',
|
||||
'Create a query. Go on, I dare ya!',
|
||||
'Looks like you don’t have any queries. Be a lot cooler if you did!',
|
||||
'Create a query. Go on!',
|
||||
'Create a query. Have fun!',
|
||||
]
|
||||
|
||||
|
|
|
@ -115,13 +115,15 @@ const SideNav = React.createClass({
|
|||
<NavBlock
|
||||
matcher="alerts"
|
||||
icon="alert-triangle"
|
||||
link={`${sourcePrefix}/alerts`}
|
||||
link={`${sourcePrefix}/alert-rules`}
|
||||
location={location}
|
||||
>
|
||||
<NavHeader link={`${sourcePrefix}/alerts`} title="Alerting" />
|
||||
<NavListItem link={`${sourcePrefix}/alerts`}>History</NavListItem>
|
||||
<NavHeader link={`${sourcePrefix}/alert-rules`} title="Alerting" />
|
||||
<NavListItem link={`${sourcePrefix}/alert-rules`}>
|
||||
Create
|
||||
Manage Tasks
|
||||
</NavListItem>
|
||||
<NavListItem link={`${sourcePrefix}/alerts`}>
|
||||
Alert History
|
||||
</NavListItem>
|
||||
</NavBlock>
|
||||
|
||||
|
|
|
@ -38,7 +38,9 @@ $timestamp-font-weight: 600;
|
|||
height: 6px;
|
||||
background-color: $annotation-color;
|
||||
border-radius: 50%;
|
||||
transition: transform 0.25s ease;
|
||||
transition:
|
||||
transform 0.25s ease,
|
||||
background-color 0.25s ease;
|
||||
}
|
||||
|
||||
.annotation-point--flag__dragging {
|
||||
|
|
|
@ -311,6 +311,14 @@ $dash-graph-options-arrow: 8px;
|
|||
background-color: $g8-storm;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&.disabled,
|
||||
&.disabled:hover {
|
||||
cursor: default;
|
||||
background-color: transparent;
|
||||
font-style: italic;
|
||||
color: $g11-sidewalk;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -4778,7 +4778,8 @@ p .label {
|
|||
.table .table--show-on-row-hover {
|
||||
visibility: hidden;
|
||||
}
|
||||
.table > tbody > tr:hover .table--show-on-row-hover {
|
||||
.table > tbody > tr:hover .table--show-on-row-hover,
|
||||
.table > tbody > tr .active.table--show-on-row-hover {
|
||||
visibility: visible;
|
||||
}
|
||||
.table-responsive {
|
||||
|
|
Loading…
Reference in New Issue