Merge pull request #2829 from influxdata/feature/persistent-legend-with-master
Persistent Legend to Annotationspull/10616/head
commit
37fdec9a97
|
@ -1,6 +1,7 @@
|
|||
## 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
|
||||
1. [#2829] (https://github.com/influxdata/chronograf/pull/2829): Add an optional persistent legend which can toggle series visibility to dashboard cells
|
||||
### 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
|
||||
|
|
|
@ -10,11 +10,11 @@ import FancyScrollbar from 'shared/components/FancyScrollbar'
|
|||
import {DISPLAY_OPTIONS, TOOLTIP_CONTENT} from 'src/dashboards/constants'
|
||||
import {GRAPH_TYPES} from 'src/dashboards/graphics/graph'
|
||||
|
||||
import {updateAxes} from 'src/dashboards/actions/cellEditorOverlay'
|
||||
|
||||
const {LINEAR, LOG, BASE_2, BASE_10} = DISPLAY_OPTIONS
|
||||
const getInputMin = scale => (scale === LOG ? '0' : null)
|
||||
|
||||
import {updateAxes} from 'src/dashboards/actions/cellEditorOverlay'
|
||||
|
||||
class AxesOptions extends Component {
|
||||
handleSetPrefixSuffix = e => {
|
||||
const {handleUpdateAxes, axes} = this.props
|
||||
|
@ -73,6 +73,8 @@ class AxesOptions extends Component {
|
|||
const {
|
||||
axes: {y: {bounds, label, prefix, suffix, base, scale, defaultYLabel}},
|
||||
type,
|
||||
staticLegend,
|
||||
onToggleStaticLegend,
|
||||
} = this.props
|
||||
|
||||
const [min, max] = bounds
|
||||
|
@ -162,6 +164,18 @@ class AxesOptions extends Component {
|
|||
onClickTab={this.handleSetScale(LOG)}
|
||||
/>
|
||||
</Tabber>
|
||||
<Tabber labelText="Static Legend">
|
||||
<Tab
|
||||
text="Show"
|
||||
isActive={staticLegend}
|
||||
onClickTab={onToggleStaticLegend(true)}
|
||||
/>
|
||||
<Tab
|
||||
text="Hide"
|
||||
isActive={!staticLegend}
|
||||
onClickTab={onToggleStaticLegend(false)}
|
||||
/>
|
||||
</Tabber>
|
||||
</form>
|
||||
</div>
|
||||
</FancyScrollbar>
|
||||
|
@ -169,7 +183,7 @@ class AxesOptions extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
const {arrayOf, func, shape, string} = PropTypes
|
||||
const {arrayOf, bool, func, shape, string} = PropTypes
|
||||
|
||||
AxesOptions.defaultProps = {
|
||||
axes: {
|
||||
|
@ -193,6 +207,8 @@ AxesOptions.propTypes = {
|
|||
defaultYLabel: string,
|
||||
}),
|
||||
}).isRequired,
|
||||
onToggleStaticLegend: func.isRequired,
|
||||
staticLegend: bool,
|
||||
handleUpdateAxes: func.isRequired,
|
||||
}
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@ import * as queryModifiers from 'src/utils/queryTransitions'
|
|||
import defaultQueryConfig from 'src/utils/defaultQueryConfig'
|
||||
import {buildQuery} from 'utils/influxql'
|
||||
import {getQueryConfig} from 'shared/apis'
|
||||
import {IS_STATIC_LEGEND} from 'src/shared/constants'
|
||||
|
||||
import {
|
||||
removeUnselectedTemplateValues,
|
||||
|
@ -28,7 +29,7 @@ class CellEditorOverlay extends Component {
|
|||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
const {cell: {queries}, sources} = props
|
||||
const {cell: {queries, legend}, sources} = props
|
||||
|
||||
let source = _.get(queries, ['0', 'source'], null)
|
||||
source = sources.find(s => s.links.self === source) || props.source
|
||||
|
@ -45,6 +46,7 @@ class CellEditorOverlay extends Component {
|
|||
queriesWorkingDraft,
|
||||
activeQueryIndex: 0,
|
||||
isDisplayOptionsTabActive: false,
|
||||
staticLegend: IS_STATIC_LEGEND(legend),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -102,8 +104,7 @@ class CellEditorOverlay extends Component {
|
|||
}
|
||||
|
||||
handleSaveCell = () => {
|
||||
const {queriesWorkingDraft} = this.state
|
||||
|
||||
const {queriesWorkingDraft, staticLegend} = this.state
|
||||
const {cell, singleStatColors, gaugeColors} = this.props
|
||||
|
||||
const queries = queriesWorkingDraft.map(q => {
|
||||
|
@ -131,6 +132,12 @@ class CellEditorOverlay extends Component {
|
|||
...cell,
|
||||
queries,
|
||||
colors,
|
||||
legend: staticLegend
|
||||
? {
|
||||
type: 'static',
|
||||
orientation: 'bottom',
|
||||
}
|
||||
: {},
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -142,6 +149,10 @@ class CellEditorOverlay extends Component {
|
|||
this.setState({activeQueryIndex})
|
||||
}
|
||||
|
||||
handleToggleStaticLegend = staticLegend => () => {
|
||||
this.setState({staticLegend})
|
||||
}
|
||||
|
||||
handleSetQuerySource = source => {
|
||||
const queriesWorkingDraft = this.state.queriesWorkingDraft.map(q => ({
|
||||
..._.cloneDeep(q),
|
||||
|
@ -251,6 +262,7 @@ class CellEditorOverlay extends Component {
|
|||
activeQueryIndex,
|
||||
isDisplayOptionsTabActive,
|
||||
queriesWorkingDraft,
|
||||
staticLegend,
|
||||
} = this.state
|
||||
|
||||
const queryActions = {
|
||||
|
@ -282,6 +294,7 @@ class CellEditorOverlay extends Component {
|
|||
autoRefresh={autoRefresh}
|
||||
queryConfigs={queriesWorkingDraft}
|
||||
editQueryStatus={editQueryStatus}
|
||||
staticLegend={staticLegend}
|
||||
/>
|
||||
<CEOBottom>
|
||||
<OverlayControls
|
||||
|
@ -296,7 +309,11 @@ class CellEditorOverlay extends Component {
|
|||
onClickDisplayOptions={this.handleClickDisplayOptionsTab}
|
||||
/>
|
||||
{isDisplayOptionsTabActive
|
||||
? <DisplayOptions queryConfigs={queriesWorkingDraft} />
|
||||
? <DisplayOptions
|
||||
queryConfigs={queriesWorkingDraft}
|
||||
onToggleStaticLegend={this.handleToggleStaticLegend}
|
||||
staticLegend={staticLegend}
|
||||
/>
|
||||
: <QueryMaker
|
||||
source={this.getSource()}
|
||||
templates={templates}
|
||||
|
|
|
@ -35,15 +35,19 @@ class DisplayOptions extends Component {
|
|||
}
|
||||
|
||||
renderOptions = () => {
|
||||
const {cell: {type}} = this.props
|
||||
|
||||
const {cell: {type}, staticLegend, onToggleStaticLegend} = this.props
|
||||
switch (type) {
|
||||
case 'gauge':
|
||||
return <GaugeOptions />
|
||||
case 'single-stat':
|
||||
return <SingleStatOptions />
|
||||
default:
|
||||
return <AxesOptions />
|
||||
return (
|
||||
<AxesOptions
|
||||
onToggleStaticLegend={onToggleStaticLegend}
|
||||
staticLegend={staticLegend}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -56,7 +60,8 @@ class DisplayOptions extends Component {
|
|||
)
|
||||
}
|
||||
}
|
||||
const {arrayOf, shape, string} = PropTypes
|
||||
|
||||
const {arrayOf, bool, func, shape, string} = PropTypes
|
||||
|
||||
DisplayOptions.propTypes = {
|
||||
cell: shape({
|
||||
|
@ -70,6 +75,8 @@ DisplayOptions.propTypes = {
|
|||
}),
|
||||
}).isRequired,
|
||||
queryConfigs: arrayOf(shape()).isRequired,
|
||||
onToggleStaticLegend: func.isRequired,
|
||||
staticLegend: bool,
|
||||
}
|
||||
|
||||
const mapStateToProps = ({cellEditorOverlay: {cell, cell: {axes}}}) => ({
|
||||
|
|
|
@ -2,7 +2,7 @@ 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">
|
||||
<div className="form-group col-md-6">
|
||||
<label>
|
||||
{labelText}
|
||||
{tipID
|
||||
|
|
|
@ -18,6 +18,7 @@ const DashVisualization = (
|
|||
queryConfigs,
|
||||
editQueryStatus,
|
||||
resizerTopHeight,
|
||||
staticLegend,
|
||||
singleStatColors,
|
||||
},
|
||||
{source: {links: {proxy}}}
|
||||
|
@ -37,13 +38,14 @@ const DashVisualization = (
|
|||
autoRefresh={autoRefresh}
|
||||
editQueryStatus={editQueryStatus}
|
||||
resizerTopHeight={resizerTopHeight}
|
||||
staticLegend={staticLegend}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const {arrayOf, func, number, shape, string} = PropTypes
|
||||
const {arrayOf, bool, func, number, shape, string} = PropTypes
|
||||
|
||||
DashVisualization.propTypes = {
|
||||
type: string.isRequired,
|
||||
|
@ -79,6 +81,7 @@ DashVisualization.propTypes = {
|
|||
value: number.isRequired,
|
||||
}).isRequired
|
||||
),
|
||||
staticLegend: bool,
|
||||
}
|
||||
|
||||
DashVisualization.contextTypes = {
|
||||
|
|
|
@ -5,7 +5,13 @@ import AnnotationSpan from 'shared/components/AnnotationSpan'
|
|||
|
||||
import * as schema from 'shared/schemas'
|
||||
|
||||
const Annotation = ({dygraph, annotation, mode, lastUpdated}) =>
|
||||
const Annotation = ({
|
||||
dygraph,
|
||||
annotation,
|
||||
mode,
|
||||
lastUpdated,
|
||||
staticLegendHeight,
|
||||
}) =>
|
||||
<div>
|
||||
{annotation.startTime === annotation.endTime
|
||||
? <AnnotationPoint
|
||||
|
@ -19,6 +25,7 @@ const Annotation = ({dygraph, annotation, mode, lastUpdated}) =>
|
|||
annotation={annotation}
|
||||
mode={mode}
|
||||
dygraph={dygraph}
|
||||
staticLegendHeight={staticLegendHeight}
|
||||
/>}
|
||||
</div>
|
||||
|
||||
|
@ -29,6 +36,7 @@ Annotation.propTypes = {
|
|||
lastUpdated: number,
|
||||
annotation: schema.annotation.isRequired,
|
||||
dygraph: shape({}).isRequired,
|
||||
staticLegendHeight: number,
|
||||
}
|
||||
|
||||
export default Annotation
|
||||
|
|
|
@ -186,7 +186,7 @@ class AnnotationSpan extends React.Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
const {annotation, dygraph} = this.props
|
||||
const {annotation, dygraph, staticLegendHeight} = this.props
|
||||
const {isDragging} = this.state
|
||||
|
||||
return (
|
||||
|
@ -195,6 +195,7 @@ class AnnotationSpan extends React.Component {
|
|||
annotation={annotation}
|
||||
dygraph={dygraph}
|
||||
active={!!isDragging}
|
||||
staticLegendHeight={staticLegendHeight}
|
||||
/>
|
||||
{this.renderLeftMarker(annotation.startTime, dygraph)}
|
||||
{this.renderRightMarker(annotation.endTime, dygraph)}
|
||||
|
@ -207,6 +208,7 @@ AnnotationSpan.propTypes = {
|
|||
annotation: schema.annotation.isRequired,
|
||||
mode: PropTypes.string.isRequired,
|
||||
dygraph: PropTypes.shape({}).isRequired,
|
||||
staticLegendHeight: PropTypes.number.isRequired,
|
||||
updateAnnotationAsync: PropTypes.func.isRequired,
|
||||
updateAnnotation: PropTypes.func.isRequired,
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ import React, {PropTypes} from 'react'
|
|||
import {DYGRAPH_CONTAINER_MARGIN} from 'shared/constants'
|
||||
import * as schema from 'shared/schemas'
|
||||
|
||||
const windowDimensions = (anno, dygraph) => {
|
||||
const windowDimensions = (anno, dygraph, staticLegendHeight) => {
|
||||
// TODO: export and test this function
|
||||
const [startX, endX] = dygraph.xAxisRange()
|
||||
const startTime = Math.max(+anno.startTime, startX)
|
||||
|
@ -16,23 +16,29 @@ const windowDimensions = (anno, dygraph) => {
|
|||
const windowLeftXCoord =
|
||||
Math.min(windowStartXCoord, windowEndXCoord) + DYGRAPH_CONTAINER_MARGIN
|
||||
|
||||
const height = staticLegendHeight
|
||||
? `calc(100% - ${staticLegendHeight + 36}px)`
|
||||
: 'calc(100% - 36px)'
|
||||
|
||||
return {
|
||||
left: `${windowLeftXCoord}px`,
|
||||
width: `${windowWidth}px`,
|
||||
height,
|
||||
}
|
||||
}
|
||||
|
||||
const AnnotationWindow = ({annotation, dygraph, active}) =>
|
||||
const AnnotationWindow = ({annotation, dygraph, active, staticLegendHeight}) =>
|
||||
<div
|
||||
className={`annotation-window${active ? ' active' : ''}`}
|
||||
style={windowDimensions(annotation, dygraph)}
|
||||
style={windowDimensions(annotation, dygraph, staticLegendHeight)}
|
||||
/>
|
||||
|
||||
const {bool, shape} = PropTypes
|
||||
const {bool, number, shape} = PropTypes
|
||||
|
||||
AnnotationWindow.propTypes = {
|
||||
annotation: schema.annotation.isRequired,
|
||||
dygraph: shape({}).isRequired,
|
||||
staticLegendHeight: number,
|
||||
active: bool,
|
||||
}
|
||||
|
||||
|
|
|
@ -41,6 +41,7 @@ class Annotations extends Component {
|
|||
handleAddingAnnotationSuccess,
|
||||
handleMouseEnterTempAnnotation,
|
||||
handleMouseLeaveTempAnnotation,
|
||||
staticLegendHeight,
|
||||
} = this.props
|
||||
|
||||
const annotations = visibleAnnotations(
|
||||
|
@ -64,6 +65,7 @@ class Annotations extends Component {
|
|||
isTempHovering={isTempHovering}
|
||||
onMouseEnterTempAnnotation={handleMouseEnterTempAnnotation}
|
||||
onMouseLeaveTempAnnotation={handleMouseLeaveTempAnnotation}
|
||||
staticLegendHeight={staticLegendHeight}
|
||||
/>}
|
||||
{annotations.map(a =>
|
||||
<Annotation
|
||||
|
@ -71,6 +73,7 @@ class Annotations extends Component {
|
|||
mode={mode}
|
||||
annotation={a}
|
||||
dygraph={dygraph}
|
||||
staticLegendHeight={staticLegendHeight}
|
||||
lastUpdated={lastUpdated}
|
||||
/>
|
||||
)}
|
||||
|
@ -79,7 +82,7 @@ class Annotations extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
const {arrayOf, bool, func, shape, string} = PropTypes
|
||||
const {arrayOf, bool, func, number, shape, string} = PropTypes
|
||||
|
||||
Annotations.propTypes = {
|
||||
annotations: arrayOf(schema.annotation),
|
||||
|
@ -92,6 +95,7 @@ Annotations.propTypes = {
|
|||
handleAddingAnnotationSuccess: func.isRequired,
|
||||
handleMouseEnterTempAnnotation: func.isRequired,
|
||||
handleMouseLeaveTempAnnotation: func.isRequired,
|
||||
staticLegendHeight: number,
|
||||
}
|
||||
|
||||
const mapStateToProps = ({
|
||||
|
|
|
@ -7,6 +7,7 @@ import NanoDate from 'nano-date'
|
|||
|
||||
import Dygraphs from 'src/external/dygraph'
|
||||
import DygraphLegend from 'src/shared/components/DygraphLegend'
|
||||
import StaticLegend from 'src/shared/components/StaticLegend'
|
||||
import Annotations from 'src/shared/components/Annotations'
|
||||
|
||||
import getRange, {getStackedRange} from 'shared/parsing/getRangeForDygraph'
|
||||
|
@ -24,7 +25,6 @@ import {
|
|||
highlightSeriesOpts,
|
||||
} from 'src/shared/graphs/helpers'
|
||||
const {LINEAR, LOG, BASE_10, BASE_2} = DISPLAY_OPTIONS
|
||||
import {ADDING, EDITING} from 'src/shared/annotations/helpers'
|
||||
|
||||
class Dygraph extends Component {
|
||||
constructor(props) {
|
||||
|
@ -32,6 +32,7 @@ class Dygraph extends Component {
|
|||
this.state = {
|
||||
isSynced: false,
|
||||
isHidden: true,
|
||||
staticLegendHeight: null,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -294,11 +295,24 @@ class Dygraph extends Component {
|
|||
|
||||
handleAnnotationsRef = ref => (this.annotationsRef = ref)
|
||||
|
||||
render() {
|
||||
const {isHidden} = this.state
|
||||
const {mode} = this.props
|
||||
handleReceiveStaticLegendHeight = staticLegendHeight => {
|
||||
this.setState({staticLegendHeight})
|
||||
}
|
||||
|
||||
const hideLegend = mode === EDITING || mode === ADDING ? true : isHidden
|
||||
render() {
|
||||
const {isHidden, staticLegendHeight} = this.state
|
||||
const {staticLegend} = this.props
|
||||
|
||||
let dygraphStyle = {...this.props.containerStyle, zIndex: '2'}
|
||||
if (staticLegend) {
|
||||
const cellVerticalPadding = 16
|
||||
|
||||
dygraphStyle = {
|
||||
...this.props.containerStyle,
|
||||
zIndex: '2',
|
||||
height: `calc(100% - ${staticLegendHeight + cellVerticalPadding}px)`,
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="dygraph-child" onMouseLeave={this.deselectCrosshair}>
|
||||
|
@ -306,10 +320,11 @@ class Dygraph extends Component {
|
|||
<Annotations
|
||||
dygraph={this.dygraph}
|
||||
annotationsRef={this.handleAnnotationsRef}
|
||||
staticLegendHeight={staticLegendHeight}
|
||||
/>}
|
||||
{this.dygraph &&
|
||||
<DygraphLegend
|
||||
isHidden={hideLegend}
|
||||
isHidden={isHidden}
|
||||
dygraph={this.dygraph}
|
||||
onHide={this.handleHideLegend}
|
||||
onShow={this.handleShowLegend}
|
||||
|
@ -320,8 +335,15 @@ class Dygraph extends Component {
|
|||
this.props.dygraphRef(r)
|
||||
}}
|
||||
className="dygraph-child-container"
|
||||
style={{...this.props.containerStyle, zIndex: '2'}}
|
||||
style={dygraphStyle}
|
||||
/>
|
||||
{staticLegend &&
|
||||
<StaticLegend
|
||||
dygraph={this.dygraph}
|
||||
handleReceiveStaticLegendHeight={
|
||||
this.handleReceiveStaticLegendHeight
|
||||
}
|
||||
/>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -349,6 +371,9 @@ Dygraph.defaultProps = {
|
|||
overrideLineColors: null,
|
||||
dygraphRef: () => {},
|
||||
onZoom: () => {},
|
||||
staticLegend: {
|
||||
type: null,
|
||||
},
|
||||
}
|
||||
|
||||
Dygraph.propTypes = {
|
||||
|
@ -367,6 +392,7 @@ Dygraph.propTypes = {
|
|||
containerStyle: shape({}),
|
||||
isGraphFilled: bool,
|
||||
isBarGraph: bool,
|
||||
staticLegend: bool,
|
||||
overrideLineColors: array,
|
||||
dygraphSeries: shape({}).isRequired,
|
||||
ruleValues: shape({
|
||||
|
|
|
@ -3,12 +3,7 @@ import _ from 'lodash'
|
|||
import classnames from 'classnames'
|
||||
import uuid from 'node-uuid'
|
||||
|
||||
import {makeLegendStyles} from 'shared/graphs/helpers'
|
||||
|
||||
const removeMeasurement = (label = '') => {
|
||||
const [measurement] = label.match(/^(.*)[.]/g) || ['']
|
||||
return label.replace(measurement, '')
|
||||
}
|
||||
import {makeLegendStyles, removeMeasurement} from 'shared/graphs/helpers'
|
||||
|
||||
class DygraphLegend extends Component {
|
||||
state = {
|
||||
|
|
|
@ -4,7 +4,7 @@ import ReactTooltip from 'react-tooltip'
|
|||
const GraphTips = React.createClass({
|
||||
render() {
|
||||
const graphTipsText =
|
||||
'<h1>Graph Tips:</h1><p><code>Click + Drag</code> Zoom in (X or Y)<br/><code>Shift + Click</code> Pan Graph Window<br/><code>Double Click</code> Reset Graph Window</p>'
|
||||
'<h1>Graph Tips:</h1><p><code>Click + Drag</code> Zoom in (X or Y)<br/><code>Shift + Click</code> Pan Graph Window<br/><code>Double Click</code> Reset Graph Window</p><h1>Static Legend Tips:</h1><p><code>Click</code>Focus on single Series<br/><code>Shift + Click</code> Show/Hide single Series</p>'
|
||||
return (
|
||||
<div
|
||||
className="graph-tips"
|
||||
|
|
|
@ -3,6 +3,7 @@ import WidgetCell from 'shared/components/WidgetCell'
|
|||
import LayoutCell from 'shared/components/LayoutCell'
|
||||
import RefreshingGraph from 'shared/components/RefreshingGraph'
|
||||
import {buildQueriesForLayouts} from 'utils/buildQueriesForLayouts'
|
||||
import {IS_STATIC_LEGEND} from 'src/shared/constants'
|
||||
|
||||
import _ from 'lodash'
|
||||
|
||||
|
@ -39,7 +40,7 @@ const Layout = (
|
|||
{
|
||||
host,
|
||||
cell,
|
||||
cell: {h, axes, type, colors},
|
||||
cell: {h, axes, type, colors, legend},
|
||||
source,
|
||||
sources,
|
||||
onZoom,
|
||||
|
@ -76,6 +77,7 @@ const Layout = (
|
|||
inView={cell.inView}
|
||||
axes={axes}
|
||||
type={type}
|
||||
staticLegend={IS_STATIC_LEGEND(legend)}
|
||||
cellHeight={h}
|
||||
onZoom={onZoom}
|
||||
sources={sources}
|
||||
|
|
|
@ -54,6 +54,7 @@ class LineGraph extends Component {
|
|||
isGraphFilled,
|
||||
showSingleStat,
|
||||
displayOptions,
|
||||
staticLegend,
|
||||
underlayCallback,
|
||||
overrideLineColors,
|
||||
isFetchingInitially,
|
||||
|
@ -120,6 +121,7 @@ class LineGraph extends Component {
|
|||
setResolution={setResolution}
|
||||
overrideLineColors={lineColors}
|
||||
containerStyle={containerStyle}
|
||||
staticLegend={staticLegend}
|
||||
isGraphFilled={showSingleStat ? false : isGraphFilled}
|
||||
/>
|
||||
{showSingleStat
|
||||
|
@ -155,6 +157,7 @@ LineGraph.defaultProps = {
|
|||
underlayCallback: () => {},
|
||||
isGraphFilled: true,
|
||||
overrideLineColors: null,
|
||||
staticLegend: false,
|
||||
}
|
||||
|
||||
LineGraph.propTypes = {
|
||||
|
@ -174,6 +177,7 @@ LineGraph.propTypes = {
|
|||
underlayCallback: func,
|
||||
isGraphFilled: bool,
|
||||
isBarGraph: bool,
|
||||
staticLegend: bool,
|
||||
overrideLineColors: array,
|
||||
showSingleStat: bool,
|
||||
displayOptions: shape({
|
||||
|
|
|
@ -117,6 +117,7 @@ class NewAnnotation extends Component {
|
|||
isTempHovering,
|
||||
tempAnnotation,
|
||||
tempAnnotation: {startTime, endTime},
|
||||
staticLegendHeight,
|
||||
} = this.props
|
||||
const {isMouseOver} = this.state
|
||||
|
||||
|
@ -141,6 +142,7 @@ class NewAnnotation extends Component {
|
|||
annotation={tempAnnotation}
|
||||
dygraph={dygraph}
|
||||
active={true}
|
||||
staticLegendHeight={staticLegendHeight}
|
||||
/>}
|
||||
<div
|
||||
className={classnames('new-annotation', {
|
||||
|
@ -178,7 +180,7 @@ class NewAnnotation extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
const {bool, func, shape, string} = PropTypes
|
||||
const {bool, func, number, shape, string} = PropTypes
|
||||
|
||||
NewAnnotation.contextTypes = {
|
||||
source: shape({
|
||||
|
@ -198,6 +200,7 @@ NewAnnotation.propTypes = {
|
|||
onUpdateAnnotation: func.isRequired,
|
||||
onMouseEnterTempAnnotation: func.isRequired,
|
||||
onMouseLeaveTempAnnotation: func.isRequired,
|
||||
staticLegendHeight: number,
|
||||
}
|
||||
|
||||
const mdtp = {
|
||||
|
|
|
@ -24,6 +24,7 @@ const RefreshingGraph = ({
|
|||
cellHeight,
|
||||
autoRefresh,
|
||||
resizerTopHeight,
|
||||
staticLegend,
|
||||
manualRefresh, // when changed, re-mounts the component
|
||||
synchronizer,
|
||||
resizeCoords,
|
||||
|
@ -95,6 +96,7 @@ const RefreshingGraph = ({
|
|||
isBarGraph={type === 'bar'}
|
||||
synchronizer={synchronizer}
|
||||
resizeCoords={resizeCoords}
|
||||
staticLegend={staticLegend}
|
||||
displayOptions={displayOptions}
|
||||
editQueryStatus={editQueryStatus}
|
||||
grabDataForDownload={grabDataForDownload}
|
||||
|
@ -119,6 +121,7 @@ RefreshingGraph.propTypes = {
|
|||
axes: shape(),
|
||||
queries: arrayOf(shape()).isRequired,
|
||||
editQueryStatus: func,
|
||||
staticLegend: bool,
|
||||
onZoom: func,
|
||||
resizeCoords: shape(),
|
||||
grabDataForDownload: func,
|
||||
|
@ -137,6 +140,7 @@ RefreshingGraph.propTypes = {
|
|||
|
||||
RefreshingGraph.defaultProps = {
|
||||
manualRefresh: 0,
|
||||
staticLegend: false,
|
||||
inView: true,
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,113 @@
|
|||
import React, {PropTypes, Component} from 'react'
|
||||
import _ from 'lodash'
|
||||
import uuid from 'node-uuid'
|
||||
import {removeMeasurement} from 'shared/graphs/helpers'
|
||||
|
||||
const staticLegendItemClassname = (visibilities, i, hoverEnabled) => {
|
||||
if (visibilities.length) {
|
||||
return `${hoverEnabled
|
||||
? 'static-legend--item'
|
||||
: 'static-legend--single'}${visibilities[i] ? '' : ' disabled'}`
|
||||
}
|
||||
|
||||
// all series are visible to match expected initial state
|
||||
return 'static-legend--item'
|
||||
}
|
||||
|
||||
class StaticLegend extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
}
|
||||
|
||||
state = {
|
||||
visibilities: [],
|
||||
clickStatus: false,
|
||||
}
|
||||
|
||||
componentDidMount = () => {
|
||||
const {height} = this.staticLegendRef.getBoundingClientRect()
|
||||
this.props.handleReceiveStaticLegendHeight(height)
|
||||
}
|
||||
|
||||
componentDidUpdate = () => {
|
||||
const {height} = this.staticLegendRef.getBoundingClientRect()
|
||||
this.props.handleReceiveStaticLegendHeight(height)
|
||||
}
|
||||
|
||||
componentWillUnmount = () => {
|
||||
this.props.handleReceiveStaticLegendHeight(null)
|
||||
}
|
||||
|
||||
handleClick = i => e => {
|
||||
const visibilities = this.props.dygraph.visibility()
|
||||
const clickStatus = this.state.clickStatus
|
||||
|
||||
if (e.shiftKey || e.metaKey) {
|
||||
visibilities[i] = !visibilities[i]
|
||||
this.props.dygraph.setVisibility(visibilities)
|
||||
this.setState({visibilities})
|
||||
return
|
||||
}
|
||||
|
||||
const prevClickStatus = clickStatus && visibilities[i]
|
||||
|
||||
const newVisibilities = prevClickStatus
|
||||
? _.map(visibilities, () => true)
|
||||
: _.map(visibilities, () => false)
|
||||
|
||||
newVisibilities[i] = true
|
||||
|
||||
this.props.dygraph.setVisibility(newVisibilities)
|
||||
this.setState({
|
||||
visibilities: newVisibilities,
|
||||
clickStatus: !prevClickStatus,
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
const {dygraph} = this.props
|
||||
const {visibilities} = this.state
|
||||
|
||||
const labels = dygraph ? _.drop(dygraph.getLabels()) : []
|
||||
const colors = dygraph
|
||||
? _.map(labels, l => dygraph.attributes_.series_[l].options.color)
|
||||
: []
|
||||
|
||||
const hoverEnabled = labels.length > 1
|
||||
|
||||
return (
|
||||
<div
|
||||
className="static-legend"
|
||||
ref={s => {
|
||||
this.staticLegendRef = s
|
||||
}}
|
||||
>
|
||||
{_.map(labels, (v, i) =>
|
||||
<div
|
||||
className={staticLegendItemClassname(visibilities, i, hoverEnabled)}
|
||||
key={uuid.v4()}
|
||||
onMouseDown={this.handleClick(i)}
|
||||
>
|
||||
<div
|
||||
className="static-legend--dot"
|
||||
style={{backgroundColor: colors[i]}}
|
||||
/>
|
||||
<span style={{color: colors[i]}}>
|
||||
{removeMeasurement(v)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const {shape, func} = PropTypes
|
||||
|
||||
StaticLegend.propTypes = {
|
||||
sharedLegend: shape({}),
|
||||
dygraph: shape({}),
|
||||
handleReceiveStaticLegendHeight: func.isRequired,
|
||||
}
|
||||
|
||||
export default StaticLegend
|
|
@ -1,3 +1,5 @@
|
|||
import _ from 'lodash'
|
||||
|
||||
export const PERMISSIONS = {
|
||||
ViewAdmin: {
|
||||
description: 'Can view or edit admin screens',
|
||||
|
@ -426,6 +428,9 @@ export const DEFAULT_SOURCE = {
|
|||
metaUrl: '',
|
||||
}
|
||||
|
||||
export const IS_STATIC_LEGEND = legend =>
|
||||
_.get(legend, 'type', false) === 'static'
|
||||
|
||||
export const linksLink = '/chronograf/v1'
|
||||
|
||||
export const cellSupportsAnnotations = cellType => {
|
||||
|
|
|
@ -173,6 +173,12 @@ export const makeLegendStyles = (graph, legend, pageX) => {
|
|||
}
|
||||
}
|
||||
|
||||
// globally matches anything that ends in a '.'
|
||||
export const removeMeasurement = (label = '') => {
|
||||
const [measurement] = label.match(/^(.*)[.]/g) || ['']
|
||||
return label.replace(measurement, '')
|
||||
}
|
||||
|
||||
export const OPTIONS = {
|
||||
rightGap: 0,
|
||||
axisLineWidth: 2,
|
||||
|
|
|
@ -6,6 +6,7 @@ export const fixtureStatusPageCells = [
|
|||
y: 0,
|
||||
w: 12,
|
||||
h: 4,
|
||||
legend: {},
|
||||
name: 'Alert Events per Day – Last 30 Days',
|
||||
queries: [
|
||||
{
|
||||
|
@ -54,6 +55,7 @@ export const fixtureStatusPageCells = [
|
|||
y: 5,
|
||||
w: 6.5,
|
||||
h: 6,
|
||||
legend: {},
|
||||
queries: [
|
||||
{
|
||||
query: '',
|
||||
|
@ -80,6 +82,7 @@ export const fixtureStatusPageCells = [
|
|||
y: 5,
|
||||
w: 3,
|
||||
h: 6,
|
||||
legend: {},
|
||||
queries: [
|
||||
{
|
||||
query: '',
|
||||
|
@ -106,6 +109,7 @@ export const fixtureStatusPageCells = [
|
|||
y: 5,
|
||||
w: 2.5,
|
||||
h: 6,
|
||||
legend: {},
|
||||
queries: [
|
||||
{
|
||||
query: '',
|
||||
|
|
|
@ -52,6 +52,7 @@
|
|||
@import 'components/page-header-dropdown';
|
||||
@import 'components/page-header-editable';
|
||||
@import 'components/page-spinner';
|
||||
@import 'components/static-legend';
|
||||
@import 'components/query-maker';
|
||||
@import 'components/react-tooltips';
|
||||
@import 'components/redacted-input';
|
||||
|
|
|
@ -207,7 +207,6 @@ $timestamp-font-weight: 600;
|
|||
position: absolute;
|
||||
top: 8px;
|
||||
background: linear-gradient(to bottom, $window15 0%, $window0 100%);
|
||||
height: calc(100% - 36px);
|
||||
border-top: 2px dotted $window35;
|
||||
z-index: 1;
|
||||
|
||||
|
|
|
@ -43,13 +43,13 @@ $tooltip-code-color: $c-potassium;
|
|||
h1 {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
margin: 0 0 8px 0;
|
||||
margin: 8px 0;
|
||||
line-height: 1.125em;
|
||||
letter-spacing: 0;
|
||||
font-family: $default-font;
|
||||
|
||||
&:only-child {
|
||||
margin: 0;
|
||||
&:first-of-type {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
static Legend
|
||||
------------------------------------------------------------------------------
|
||||
Seen in a dashboard cell, below the graph
|
||||
NOTE: Styles for the parent are stored in Javascript, in staticLegend.js
|
||||
*/
|
||||
|
||||
.static-legend {
|
||||
position: absolute;
|
||||
width: calc(100% - 32px);
|
||||
bottom: 8px;
|
||||
left: 16px;
|
||||
display: flex;
|
||||
padding-top: 8px;
|
||||
align-items: flex-end;
|
||||
flex-wrap: wrap;
|
||||
max-height: 50%;
|
||||
overflow: auto;
|
||||
@include custom-scrollbar($g3-castle,$g6-smoke);
|
||||
}
|
||||
.static-legend--dot {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin-right: 4px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background-color: $g20-white;
|
||||
}
|
||||
.static-legend--item,
|
||||
.static-legend--single {
|
||||
height: 22px;
|
||||
line-height: 22px;
|
||||
white-space: nowrap;
|
||||
background-color: $g4-onyx;
|
||||
border-radius: 3px;
|
||||
color: $g20-white;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
padding: 0 7px;
|
||||
margin: 1px;
|
||||
}
|
||||
.static-legend--item {
|
||||
transition: background-color 0.25s ease, color 0.25s ease;
|
||||
|
||||
span,
|
||||
.static-legend--dot {
|
||||
opacity: 0.8;
|
||||
transition: opacity 0.25s ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
background-color: $g6-smoke;
|
||||
|
||||
span,
|
||||
.static-legend--dot {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
&.disabled {
|
||||
background-color: $g1-raven;
|
||||
font-style: italic;
|
||||
|
||||
span,
|
||||
.static-legend--dot {
|
||||
opacity: 0.35;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: $g2-kevlar;
|
||||
|
||||
span,
|
||||
.static-legend--dot {
|
||||
opacity: 0.65;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue