From fe0c5c73ddcaad1657559f64a6940e0b46bfd86e Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Wed, 17 Oct 2018 15:07:31 +0300 Subject: [PATCH] graph legend: refactor --- public/app/core/angular_wrappers.ts | 2 - .../colorpicker/SeriesColorPicker.tsx | 54 +++-- .../colorpicker/withColorPicker.tsx | 83 +++++++ .../panel/graph/{ => Legend}/Legend.tsx | 215 +++--------------- .../panel/graph/Legend/LegendSeriesItem.tsx | 173 ++++++++++++++ public/app/plugins/panel/graph/graph.ts | 3 +- 6 files changed, 321 insertions(+), 209 deletions(-) create mode 100644 public/app/core/components/colorpicker/withColorPicker.tsx rename public/app/plugins/panel/graph/{ => Legend}/Legend.tsx (52%) create mode 100644 public/app/plugins/panel/graph/Legend/LegendSeriesItem.tsx diff --git a/public/app/core/angular_wrappers.ts b/public/app/core/angular_wrappers.ts index 7e72f53204e..6974d40aac8 100644 --- a/public/app/core/angular_wrappers.ts +++ b/public/app/core/angular_wrappers.ts @@ -5,7 +5,6 @@ import EmptyListCTA from './components/EmptyListCTA/EmptyListCTA'; import { SearchResult } from './components/search/SearchResult'; import { TagFilter } from './components/TagFilter/TagFilter'; import { SideMenu } from './components/sidemenu/SideMenu'; -import { GraphLegend } from 'app/plugins/panel/graph/Legend'; export function registerAngularDirectives() { react2AngularDirective('passwordStrength', PasswordStrength, ['password']); @@ -18,5 +17,4 @@ export function registerAngularDirectives() { ['onSelect', { watchDepth: 'reference' }], ['tagOptions', { watchDepth: 'reference' }], ]); - react2AngularDirective('graphLegendReact', GraphLegend, ['seriesList', 'className']); } diff --git a/public/app/core/components/colorpicker/SeriesColorPicker.tsx b/public/app/core/components/colorpicker/SeriesColorPicker.tsx index b514899e2e2..9abd3574ae1 100644 --- a/public/app/core/components/colorpicker/SeriesColorPicker.tsx +++ b/public/app/core/components/colorpicker/SeriesColorPicker.tsx @@ -2,30 +2,53 @@ import React from 'react'; import { ColorPickerPopover } from './ColorPickerPopover'; import { react2AngularDirective } from 'app/core/utils/react2angular'; -export interface Props { - series: any; +export interface SeriesColorPickerProps { + // series: any; + color: string; + yaxis?: number; onColorChange: (color: string) => void; + onToggleAxis?: () => void; +} + +export class SeriesColorPicker extends React.PureComponent { + render() { + return ( +
+ {this.props.yaxis && } + +
+ ); + } +} + +interface AxisSelectorProps { + yaxis: number; onToggleAxis: () => void; } -export class SeriesColorPicker extends React.Component { +interface AxisSelectorState { + yaxis: number; +} + +export class AxisSelector extends React.PureComponent { constructor(props) { super(props); - this.onColorChange = this.onColorChange.bind(this); + this.state = { + yaxis: this.props.yaxis, + }; this.onToggleAxis = this.onToggleAxis.bind(this); } - onColorChange(color) { - this.props.onColorChange(color); - } - onToggleAxis() { + this.setState({ + yaxis: this.state.yaxis === 2 ? 1 : 2, + }); this.props.onToggleAxis(); } - renderAxisSelection() { - const leftButtonClass = this.props.series.yaxis === 1 ? 'btn-success' : 'btn-inverse'; - const rightButtonClass = this.props.series.yaxis === 2 ? 'btn-success' : 'btn-inverse'; + render() { + const leftButtonClass = this.state.yaxis === 1 ? 'btn-success' : 'btn-inverse'; + const rightButtonClass = this.state.yaxis === 2 ? 'btn-success' : 'btn-inverse'; return (
@@ -39,15 +62,6 @@ export class SeriesColorPicker extends React.Component {
); } - - render() { - return ( -
- {this.props.series.yaxis && this.renderAxisSelection()} - -
- ); - } } react2AngularDirective('seriesColorPicker', SeriesColorPicker, ['series', 'onColorChange', 'onToggleAxis']); diff --git a/public/app/core/components/colorpicker/withColorPicker.tsx b/public/app/core/components/colorpicker/withColorPicker.tsx new file mode 100644 index 00000000000..d0567fe4e18 --- /dev/null +++ b/public/app/core/components/colorpicker/withColorPicker.tsx @@ -0,0 +1,83 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import Drop from 'tether-drop'; +import { SeriesColorPicker } from './SeriesColorPicker'; + +export interface WithSeriesColorPickerProps { + color: string; + yaxis?: number; + optionalClass?: string; + onColorChange: (newColor: string) => void; + onToggleAxis?: () => void; +} + +export default function withSeriesColorPicker(WrappedComponent) { + return class extends React.Component { + pickerElem: any; + colorPickerDrop: any; + + static defaultProps = { + optionalClass: '', + yaxis: undefined, + onToggleAxis: () => {}, + }; + + constructor(props) { + super(props); + this.openColorPicker = this.openColorPicker.bind(this); + } + + openColorPicker() { + if (this.colorPickerDrop) { + this.destroyDrop(); + } + + const { color, yaxis, onColorChange, onToggleAxis } = this.props; + const dropContent = ( + + ); + const dropContentElem = document.createElement('div'); + ReactDOM.render(dropContent, dropContentElem); + + const drop = new Drop({ + target: this.pickerElem, + content: dropContentElem, + position: 'top center', + classes: 'drop-popover', + openOn: 'hover', + hoverCloseDelay: 200, + remove: true, + tetherOptions: { + constraints: [{ to: 'scrollParent', attachment: 'none both' }], + }, + }); + + drop.on('close', this.closeColorPicker.bind(this)); + + this.colorPickerDrop = drop; + this.colorPickerDrop.open(); + } + + closeColorPicker() { + setTimeout(() => { + this.destroyDrop(); + }, 100); + } + + destroyDrop() { + if (this.colorPickerDrop && this.colorPickerDrop.tether) { + this.colorPickerDrop.destroy(); + this.colorPickerDrop = null; + } + } + + render() { + const { optionalClass, onColorChange, ...wrappedComponentProps } = this.props; + return ( +
(this.pickerElem = e)} onClick={this.openColorPicker}> + +
+ ); + } + }; +} diff --git a/public/app/plugins/panel/graph/Legend.tsx b/public/app/plugins/panel/graph/Legend/Legend.tsx similarity index 52% rename from public/app/plugins/panel/graph/Legend.tsx rename to public/app/plugins/panel/graph/Legend/Legend.tsx index 5e8ea441623..f6daf778848 100644 --- a/public/app/plugins/panel/graph/Legend.tsx +++ b/public/app/plugins/panel/graph/Legend/Legend.tsx @@ -1,18 +1,15 @@ import _ from 'lodash'; import React from 'react'; -import ReactDOM from 'react-dom'; import { TimeSeries } from 'app/core/core'; import CustomScrollbar from 'app/core/components/CustomScrollbar/CustomScrollbar'; -import Drop from 'tether-drop'; -import { ColorPickerPopover } from 'app/core/components/colorpicker/ColorPickerPopover'; - -const LEGEND_STATS = ['min', 'max', 'avg', 'current', 'total']; +import { LegendItem, LEGEND_STATS } from './LegendSeriesItem'; interface LegendProps { seriesList: TimeSeries[]; optionalClass?: string; onToggleSeries?: (series: TimeSeries, event: Event) => void; onToggleSort?: (sortBy, sortDesc) => void; + onToggleAxis?: (series: TimeSeries) => void; onColorChange?: (series: TimeSeries, color: string) => void; } @@ -41,24 +38,24 @@ interface LegendSortProps { export type GraphLegendProps = LegendProps & LegendDisplayProps & LegendValuesProps & LegendSortProps; -const defaultGraphLegendProps: Partial = { - values: false, - min: false, - max: false, - avg: false, - current: false, - total: false, - alignAsTable: false, - rightSide: false, - sort: undefined, - sortDesc: false, - optionalClass: '', -}; - -export interface GraphLegendState {} - -export class GraphLegend extends React.PureComponent { - static defaultProps = defaultGraphLegendProps; +export class GraphLegend extends React.PureComponent { + static defaultProps: Partial = { + values: false, + min: false, + max: false, + avg: false, + current: false, + total: false, + alignAsTable: false, + rightSide: false, + sort: undefined, + sortDesc: false, + optionalClass: '', + onToggleSeries: () => {}, + onToggleSort: () => {}, + onToggleAxis: () => {}, + onColorChange: () => {}, + }; sortLegend() { let seriesList = this.props.seriesList || []; @@ -107,6 +104,7 @@ export class GraphLegend extends React.PureComponent this.onToggleSeries(s, e), onToggleSort: (sortBy, sortDesc) => this.props.onToggleSort(sortBy, sortDesc), onColorChange: (series, color) => this.props.onColorChange(series, color), + onToggleAxis: series => this.props.onToggleAxis(series), ...seriesValuesProps, ...sortProps, }; @@ -124,7 +122,7 @@ class LegendSeriesList extends React.PureComponent { const { seriesList, hiddenSeries, values, min, max, avg, current, total } = this.props; const seriesValuesProps = { values, min, max, avg, current, total }; return seriesList.map((series, i) => ( - { {...seriesValuesProps} onLabelClick={e => this.props.onToggleSeries(series, e)} onColorChange={color => this.props.onColorChange(series, color)} + onToggleAxis={() => this.props.onToggleAxis(series)} /> )); } } -interface LegendSeriesProps { - series: TimeSeries; - index: number; - onLabelClick?: (event) => void; - onColorChange?: (color: string) => void; -} - -type LegendSeriesItemProps = LegendSeriesProps & LegendDisplayProps & LegendValuesProps; - -class LegendSeriesItem extends React.PureComponent { - render() { - const { series, index, hiddenSeries } = this.props; - const seriesOptionClasses = getOptionSeriesCSSClasses(series, hiddenSeries); - const valueItems = this.props.values ? renderLegendValues(this.props, series) : []; - return ( -
- this.props.onLabelClick(e)} - onColorChange={e => this.props.onColorChange(e)} - /> - {valueItems} -
- ); - } -} - -interface LegendSeriesLabelProps { - label: string; - color: string; - onLabelClick?: (event) => void; - onColorChange?: (color: string) => void; -} - -class LegendSeriesLabel extends React.PureComponent { - pickerElem: any; - colorPickerDrop: any; - - openColorPicker() { - if (this.colorPickerDrop) { - this.destroyDrop(); - } - - const dropContent = ; - const dropContentElem = document.createElement('div'); - ReactDOM.render(dropContent, dropContentElem); - - const drop = new Drop({ - target: this.pickerElem, - content: dropContentElem, - position: 'top center', - classes: 'drop-popover', - openOn: 'hover', - hoverCloseDelay: 200, - remove: true, - tetherOptions: { - constraints: [{ to: 'scrollParent', attachment: 'none both' }], - }, - }); - - drop.on('close', this.closeColorPicker.bind(this)); - - this.colorPickerDrop = drop; - this.colorPickerDrop.open(); - } - - closeColorPicker() { - setTimeout(() => { - this.destroyDrop(); - }, 100); - } - - destroyDrop() { - if (this.colorPickerDrop && this.colorPickerDrop.tether) { - this.colorPickerDrop.destroy(); - this.colorPickerDrop = null; - } - } - - render() { - const { label, color } = this.props; - return [ -
(this.pickerElem = e)} - onClick={() => this.openColorPicker()} - > - -
, - this.props.onLabelClick(e)}> - {label} - , - ]; - } -} - -interface LegendValueProps { - value: string; - valueName: string; - asTable?: boolean; -} - -function LegendValue(props: LegendValueProps) { - const value = props.value; - const valueName = props.valueName; - if (props.asTable) { - return {value}; - } - return
{value}
; -} - -function renderLegendValues(props: LegendSeriesItemProps, series, asTable = false) { - const legendValueItems = []; - for (const valueName of LEGEND_STATS) { - if (props[valueName]) { - const valueFormatted = series.formatValue(series.stats[valueName]); - legendValueItems.push( - - ); - } - } - return legendValueItems; -} - class LegendTable extends React.PureComponent> { onToggleSort(stat) { let sortDesc = this.props.sortDesc; @@ -305,14 +178,16 @@ class LegendTable extends React.PureComponent> { )} {seriesList.map((series, i) => ( - this.props.onToggleSeries(series, e)} onColorChange={color => this.props.onColorChange(series, color)} + onToggleAxis={() => this.props.onToggleAxis(series)} + {...seriesValuesProps} /> ))} @@ -329,46 +204,14 @@ interface LegendTableHeaderProps { function LegendTableHeaderItem(props: LegendTableHeaderProps & LegendSortProps) { const { statName, sort, sortDesc } = props; return ( - props.onClick(e)}> + props.onClick(e)}> {statName} {sort === statName && } ); } -class LegendSeriesItemAsTable extends React.PureComponent { - render() { - const { series, index, hiddenSeries } = this.props; - const seriesOptionClasses = getOptionSeriesCSSClasses(series, hiddenSeries); - const valueItems = this.props.values ? renderLegendValues(this.props, series, true) : []; - return ( - - - this.props.onLabelClick(e)} - onColorChange={e => this.props.onColorChange(e)} - /> - - {valueItems} - - ); - } -} - -function getOptionSeriesCSSClasses(series, hiddenSeries) { - const classes = []; - if (series.yaxis === 2) { - classes.push('graph-legend-series--right-y'); - } - if (hiddenSeries[series.alias] && hiddenSeries[series.alias] === true) { - classes.push('graph-legend-series-hidden'); - } - return classes.join(' '); -} - -export class Legend extends React.Component { +export class Legend extends React.Component { render() { return ( diff --git a/public/app/plugins/panel/graph/Legend/LegendSeriesItem.tsx b/public/app/plugins/panel/graph/Legend/LegendSeriesItem.tsx new file mode 100644 index 00000000000..ce0ed048336 --- /dev/null +++ b/public/app/plugins/panel/graph/Legend/LegendSeriesItem.tsx @@ -0,0 +1,173 @@ +import React from 'react'; +import { TimeSeries } from 'app/core/core'; +import withColorPicker from 'app/core/components/colorpicker/withColorPicker'; + +export const LEGEND_STATS = ['min', 'max', 'avg', 'current', 'total']; + +export interface LegendLabelProps { + index: number; + series: TimeSeries; + asTable?: boolean; + hiddenSeries?: any; + onLabelClick?: (event) => void; + onColorChange?: (color: string) => void; + onToggleAxis?: () => void; +} + +export interface LegendValuesProps { + values?: boolean; + min?: boolean; + max?: boolean; + avg?: boolean; + current?: boolean; + total?: boolean; +} + +type LegendItemProps = LegendLabelProps & LegendValuesProps; + +export class LegendItem extends React.PureComponent { + static defaultProps = { + asTable: false, + hiddenSeries: undefined, + onLabelClick: () => {}, + onColorChange: () => {}, + onToggleAxis: () => {}, + }; + + render() { + const { series, hiddenSeries, asTable } = this.props; + const { aliasEscaped, color, yaxis } = this.props.series; + const seriesOptionClasses = getOptionSeriesCSSClasses(series, hiddenSeries); + const valueItems = this.props.values ? renderLegendValues(this.props, series, asTable) : []; + const seriesLabel = ( + + ); + + if (asTable) { + return ( + + {seriesLabel} + {valueItems} + + ); + } else { + return ( +
+ {seriesLabel} + {valueItems} +
+ ); + } + } +} + +interface LegendSeriesLabelProps { + label: string; + color: string; + yaxis?: number; + onLabelClick?: (event) => void; +} + +class LegendSeriesLabel extends React.PureComponent { + static defaultProps = { + yaxis: undefined, + onLabelClick: () => {}, + }; + + render() { + const { label, color, yaxis } = this.props; + const { onColorChange, onToggleAxis } = this.props; + return [ + , + this.props.onLabelClick(e)}> + {label} + , + ]; + } +} + +interface LegendSeriesIconProps { + color: string; + yaxis?: number; + onColorChange?: (color: string) => void; + onToggleAxis?: () => void; +} + +function SeriesIcon(props) { + return ; +} + +class LegendSeriesIcon extends React.PureComponent { + static defaultProps = { + yaxis: undefined, + onColorChange: () => {}, + onToggleAxis: () => {}, + }; + + render() { + const { color, yaxis } = this.props; + const IconWithColorPicker = withColorPicker(SeriesIcon); + + return ( + + ); + } +} + +interface LegendValueProps { + value: string; + valueName: string; + asTable?: boolean; +} + +function LegendValue(props: LegendValueProps) { + const value = props.value; + const valueName = props.valueName; + if (props.asTable) { + return {value}; + } + return
{value}
; +} + +function renderLegendValues(props: LegendItemProps, series, asTable = false) { + const legendValueItems = []; + for (const valueName of LEGEND_STATS) { + if (props[valueName]) { + const valueFormatted = series.formatValue(series.stats[valueName]); + legendValueItems.push( + + ); + } + } + return legendValueItems; +} + +function getOptionSeriesCSSClasses(series, hiddenSeries) { + const classes = []; + if (series.yaxis === 2) { + classes.push('graph-legend-series--right-y'); + } + if (hiddenSeries[series.alias] && hiddenSeries[series.alias] === true) { + classes.push('graph-legend-series-hidden'); + } + return classes.join(' '); +} diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts index 640e189859c..cc9e9660d3e 100755 --- a/public/app/plugins/panel/graph/graph.ts +++ b/public/app/plugins/panel/graph/graph.ts @@ -22,7 +22,7 @@ import { alignYLevel } from './align_yaxes'; import config from 'app/core/config'; import React from 'react'; import ReactDOM from 'react-dom'; -import { Legend, GraphLegendProps } from './Legend'; +import { Legend, GraphLegendProps } from './Legend/Legend'; import { GraphCtrl } from './module'; @@ -98,6 +98,7 @@ class GraphElement { onToggleSeries: this.ctrl.toggleSeries.bind(this.ctrl), onToggleSort: this.ctrl.toggleSort.bind(this.ctrl), onColorChange: this.ctrl.changeSeriesColor.bind(this.ctrl), + onToggleAxis: this.ctrl.toggleAxis.bind(this.ctrl), }; const legendReactElem = React.createElement(Legend, legendProps); const legendElem = this.elem.parent().find('.graph-legend');