From 0e10fdb4150fbaf8f845c6273791b62af4defb45 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Fri, 31 Aug 2018 13:24:49 +0300 Subject: [PATCH 01/29] graph: legend as React component --- public/app/core/angular_wrappers.ts | 2 + public/app/plugins/panel/graph/Legend.tsx | 189 ++++++++++++++++++++++ public/app/plugins/panel/graph/graph.ts | 19 ++- 3 files changed, 209 insertions(+), 1 deletion(-) create mode 100644 public/app/plugins/panel/graph/Legend.tsx diff --git a/public/app/core/angular_wrappers.ts b/public/app/core/angular_wrappers.ts index a4439509f8e..b1105268543 100644 --- a/public/app/core/angular_wrappers.ts +++ b/public/app/core/angular_wrappers.ts @@ -6,6 +6,7 @@ import LoginBackground from './components/Login/LoginBackground'; import { SearchResult } from './components/search/SearchResult'; import { TagFilter } from './components/TagFilter/TagFilter'; import DashboardPermissions from './components/Permissions/DashboardPermissions'; +import { GraphLegend } from 'app/plugins/panel/graph/Legend'; export function registerAngularDirectives() { react2AngularDirective('passwordStrength', PasswordStrength, ['password']); @@ -19,4 +20,5 @@ export function registerAngularDirectives() { ['tagOptions', { watchDepth: 'reference' }], ]); react2AngularDirective('dashboardPermissions', DashboardPermissions, ['backendSrv', 'dashboardId', 'folder']); + react2AngularDirective('graphLegendReact', GraphLegend, ['seriesList', 'className']); } diff --git a/public/app/plugins/panel/graph/Legend.tsx b/public/app/plugins/panel/graph/Legend.tsx new file mode 100644 index 00000000000..a4bfbefd541 --- /dev/null +++ b/public/app/plugins/panel/graph/Legend.tsx @@ -0,0 +1,189 @@ +import _ from 'lodash'; +import React from 'react'; + +const LEGEND_STATS = ['min', 'max', 'avg', 'current', 'total']; + +export interface GraphLegendProps { + seriesList: any[]; + hiddenSeries: any; + values?: boolean; + min?: boolean; + max?: boolean; + avg?: boolean; + current?: boolean; + total?: boolean; + alignAsTable?: boolean; + rightSide?: boolean; + sideWidth?: number; + sort?: 'min' | 'max' | 'avg' | 'current' | 'total'; + sortDesc?: boolean; + className?: string; +} + +export interface GraphLegendState {} + +export class GraphLegend extends React.PureComponent { + sortLegend() { + let seriesList = this.props.seriesList || []; + if (this.props.sort) { + seriesList = _.sortBy(seriesList, function(series) { + let sort = series.stats[this.props.sort]; + if (sort === null) { + sort = -Infinity; + } + return sort; + }); + if (this.props.sortDesc) { + seriesList = seriesList.reverse(); + } + } + return seriesList; + } + + render() { + const { className = '', hiddenSeries } = this.props; + const { values, min, max, avg, current, total } = this.props; + const seriesValuesProps = { values, min, max, avg, current, total }; + const seriesList = this.sortLegend(); + return ( +
+
+
+ {this.props.alignAsTable ? ( + + ) : ( + seriesList.map((series, i) => ( + + )) + )} +
+
+
+ ); + } +} + +interface LegendTableProps { + seriesList: any[]; + hiddenSeries: any; + values?: boolean; + min?: boolean; + max?: boolean; + avg?: boolean; + current?: boolean; + total?: boolean; +} + +class LegendTable extends React.PureComponent { + render() { + const seriesList = this.props.seriesList; + const { values, min, max, avg, current, total } = this.props; + const seriesValuesProps = { values, min, max, avg, current, total }; + const headerStyle: React.CSSProperties = { + textAlign: 'left', + }; + + return ( + + + + {LEGEND_STATS.map( + statName => seriesValuesProps[statName] && + )} + + {seriesList.map((series, i) => ( + + ))} + + ); + } +} + +interface LegendTableHeaderProps { + statName: string; + sortDesc?: boolean; +} + +function LegendTableHeader(props: LegendTableHeaderProps) { + return ( + + {props.statName} + + + ); +} + +interface LegendSeriesItemProps { + series: any; + index: number; + hiddenSeries: any; + values?: boolean; + min?: boolean; + max?: boolean; + avg?: boolean; + current?: boolean; + total?: boolean; +} + +class LegendSeriesItem extends React.Component { + constructor(props) { + super(props); + } + + render() { + const { series, index, hiddenSeries } = this.props; + const seriesOptionClasses = getOptionSeriesCSSClasses(series, hiddenSeries); + const valueItems = this.props.values ? renderLegendValues(this.props, series) : []; + return ( +
+
+ +
+ + {series.aliasEscaped} + + {valueItems} +
+ ); + } +} + +function LegendValue(props) { + const value = props.value; + const valueName = props.valueName; + return
{value}
; +} + +function renderLegendValues(props: LegendSeriesItemProps, series) { + 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]) { + 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 7f9fa0e1693..37841313c82 100755 --- a/public/app/plugins/panel/graph/graph.ts +++ b/public/app/plugins/panel/graph/graph.ts @@ -20,6 +20,9 @@ import { EventManager } from 'app/features/annotations/all'; import { convertToHistogramData } from './histogram'; import { alignYLevel } from './align_yaxes'; import config from 'app/core/config'; +import React from 'react'; +import ReactDOM from 'react-dom'; +import { GraphLegend, GraphLegendProps } from './Legend'; import { GraphCtrl } from './module'; @@ -82,7 +85,21 @@ class GraphElement { const graphHeight = this.elem.height(); updateLegendValues(this.data, this.panel, graphHeight); - this.ctrl.events.emit('render-legend'); + // this.ctrl.events.emit('render-legend'); + const { values, min, max, avg, current, total } = this.panel.legend; + const { alignAsTable, rightSide, sideWidth } = this.panel.legend; + const legendOptions = { alignAsTable, rightSide, sideWidth }; + const valueOptions = { values, min, max, avg, current, total }; + const legendProps: GraphLegendProps = { + seriesList: this.data, + hiddenSeries: this.ctrl.hiddenSeries, + ...legendOptions, + ...valueOptions, + }; + const legendReactElem = React.createElement(GraphLegend, legendProps); + const legendElem = this.elem.parent().find('.graph-legend'); + ReactDOM.render(legendReactElem, legendElem[0]); + this.onLegendRenderingComplete(); } onGraphHover(evt) { From 329f39e4d796a255057ee9e768474fcdd1bbea18 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Fri, 31 Aug 2018 16:34:22 +0300 Subject: [PATCH 02/29] graph: make table markup corresponding to standards --- public/app/plugins/panel/graph/Legend.tsx | 186 +++++++++++++--------- 1 file changed, 110 insertions(+), 76 deletions(-) diff --git a/public/app/plugins/panel/graph/Legend.tsx b/public/app/plugins/panel/graph/Legend.tsx index a4bfbefd541..ba11ba988f2 100644 --- a/public/app/plugins/panel/graph/Legend.tsx +++ b/public/app/plugins/panel/graph/Legend.tsx @@ -41,90 +41,39 @@ export class GraphLegend extends React.PureComponent -
-
- {this.props.alignAsTable ? ( - - ) : ( - seriesList.map((series, i) => ( - - )) - )} -
+
+
+ {this.props.alignAsTable ? ( + + ) : ( + seriesList.map((series, i) => ( + + )) + )}
); } } -interface LegendTableProps { - seriesList: any[]; - hiddenSeries: any; - values?: boolean; - min?: boolean; - max?: boolean; - avg?: boolean; - current?: boolean; - total?: boolean; -} - -class LegendTable extends React.PureComponent { - render() { - const seriesList = this.props.seriesList; - const { values, min, max, avg, current, total } = this.props; - const seriesValuesProps = { values, min, max, avg, current, total }; - const headerStyle: React.CSSProperties = { - textAlign: 'left', - }; - - return ( - - - - {LEGEND_STATS.map( - statName => seriesValuesProps[statName] && - )} - - {seriesList.map((series, i) => ( - - ))} - - ); - } -} - -interface LegendTableHeaderProps { - statName: string; - sortDesc?: boolean; -} - -function LegendTableHeader(props: LegendTableHeaderProps) { - return ( - - {props.statName} - - - ); -} - interface LegendSeriesItemProps { series: any; index: number; @@ -163,20 +112,105 @@ class LegendSeriesItem extends React.Component { function LegendValue(props) { const value = props.value; const valueName = props.valueName; + if (props.asTable) { + return {value}; + } return
{value}
; } -function renderLegendValues(props: LegendSeriesItemProps, series) { +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(); + legendValueItems.push( + + ); } } return legendValueItems; } +interface LegendTableProps { + seriesList: any[]; + hiddenSeries: any; + values?: boolean; + min?: boolean; + max?: boolean; + avg?: boolean; + current?: boolean; + total?: boolean; +} + +class LegendTable extends React.PureComponent { + render() { + const seriesList = this.props.seriesList; + const { values, min, max, avg, current, total } = this.props; + const seriesValuesProps = { values, min, max, avg, current, total }; + + return ( + + + + + {seriesList.map((series, i) => ( + + ))} + +
+ {LEGEND_STATS.map( + statName => seriesValuesProps[statName] && + )} +
+ ); + } +} + +class LegendSeriesItemAsTable extends React.Component { + constructor(props) { + super(props); + } + + render() { + const { series, index, hiddenSeries } = this.props; + const seriesOptionClasses = getOptionSeriesCSSClasses(series, hiddenSeries); + const valueItems = this.props.values ? renderLegendValues(this.props, series, true) : []; + return ( + + +
+ +
+ + {series.aliasEscaped} + + + {valueItems} + + ); + } +} + +interface LegendTableHeaderProps { + statName: string; + sortDesc?: boolean; +} + +function LegendTableHeader(props: LegendTableHeaderProps) { + return ( + + {props.statName} + + + ); +} + function getOptionSeriesCSSClasses(series, hiddenSeries) { const classes = []; if (series.yaxis === 2) { From 60146109ab09d101879c9a9b7832b7e1ae14c2ba Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Fri, 31 Aug 2018 17:27:57 +0300 Subject: [PATCH 03/29] graph legend: minor refactor --- public/app/plugins/panel/graph/Legend.tsx | 51 ++++++++++++++--------- 1 file changed, 32 insertions(+), 19 deletions(-) diff --git a/public/app/plugins/panel/graph/Legend.tsx b/public/app/plugins/panel/graph/Legend.tsx index ba11ba988f2..e1748ea3655 100644 --- a/public/app/plugins/panel/graph/Legend.tsx +++ b/public/app/plugins/panel/graph/Legend.tsx @@ -41,19 +41,23 @@ export class GraphLegend extends React.PureComponent +
{this.props.alignAsTable ? ( @@ -97,18 +101,32 @@ class LegendSeriesItem extends React.Component { const valueItems = this.props.values ? renderLegendValues(this.props, series) : []; return (
-
- -
- - {series.aliasEscaped} - + {valueItems}
); } } +interface LegendSeriesLabelProps { + label: string; + color: string; +} + +function LegendSeriesLabel(props: LegendSeriesLabelProps) { + const { label, color } = props; + return ( +
+
+ +
+ + {label} + +
+ ); +} + function LegendValue(props) { const value = props.value; const valueName = props.valueName; @@ -118,7 +136,7 @@ function LegendValue(props) { return
{value}
; } -function renderLegendValues(props: LegendSeriesItemProps, series, asTable = false) { +function renderLegendValues(props: LegendSeriesItemProps, series, asTable = false): React.ReactElement[] { const legendValueItems = []; for (const valueName of LEGEND_STATS) { if (props[valueName]) { @@ -184,12 +202,7 @@ class LegendSeriesItemAsTable extends React.Component { return ( -
- -
- - {series.aliasEscaped} - + {valueItems} From e8a52117a5f55e05579d29c773ca1b2e83cd2d76 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Mon, 3 Sep 2018 16:54:52 +0300 Subject: [PATCH 04/29] graph legend: react component refactor --- public/app/plugins/panel/graph/Legend.tsx | 171 ++++++++++++---------- public/app/plugins/panel/graph/graph.ts | 5 +- 2 files changed, 97 insertions(+), 79 deletions(-) diff --git a/public/app/plugins/panel/graph/Legend.tsx b/public/app/plugins/panel/graph/Legend.tsx index e1748ea3655..becb52d1aeb 100644 --- a/public/app/plugins/panel/graph/Legend.tsx +++ b/public/app/plugins/panel/graph/Legend.tsx @@ -1,32 +1,62 @@ import _ from 'lodash'; import React from 'react'; +import { TimeSeries } from 'app/core/core'; const LEGEND_STATS = ['min', 'max', 'avg', 'current', 'total']; -export interface GraphLegendProps { - seriesList: any[]; +interface LegendProps { + seriesList: TimeSeries[]; + optionalClass?: string; +} + +interface LegendDisplayProps { hiddenSeries: any; + hideEmpty?: boolean; + hideZero?: boolean; + alignAsTable?: boolean; + rightSide?: boolean; + sideWidth?: number; +} + +interface LegendValuesProps { values?: boolean; min?: boolean; max?: boolean; avg?: boolean; current?: boolean; total?: boolean; - alignAsTable?: boolean; - rightSide?: boolean; - sideWidth?: number; +} + +interface LegendSortProps { sort?: 'min' | 'max' | 'avg' | 'current' | 'total'; sortDesc?: boolean; - className?: string; } +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; + sortLegend() { let seriesList = this.props.seriesList || []; if (this.props.sort) { - seriesList = _.sortBy(seriesList, function(series) { + seriesList = _.sortBy(seriesList, series => { let sort = series.stats[this.props.sort]; if (sort === null) { sort = -Infinity; @@ -41,11 +71,12 @@ export class GraphLegend extends React.PureComponent !series.hideFromLegend(seriesHideProps)); + const legendCustomClasses = `${this.props.alignAsTable ? 'graph-legend-table' : ''} ${optionalClass}`; // Set min-width if side style and there is a value, otherwise remove the CSS property // Set width so it works with IE11 @@ -62,15 +93,7 @@ export class GraphLegend extends React.PureComponent ) : ( - seriesList.map((series, i) => ( - - )) + )}
@@ -78,23 +101,24 @@ export class GraphLegend extends React.PureComponent { + render() { + const { seriesList, hiddenSeries, values, min, max, avg, current, total } = this.props; + const seriesValuesProps = { values, min, max, avg, current, total }; + return seriesList.map((series, i) => ( + + )); + } } -class LegendSeriesItem extends React.Component { - constructor(props) { - super(props); - } +interface LegendSeriesProps { + series: TimeSeries; + index: number; +} +type LegendSeriesItemProps = LegendSeriesProps & LegendDisplayProps & LegendValuesProps; + +class LegendSeriesItem extends React.PureComponent { render() { const { series, index, hiddenSeries } = this.props; const seriesOptionClasses = getOptionSeriesCSSClasses(series, hiddenSeries); @@ -113,21 +137,27 @@ interface LegendSeriesLabelProps { color: string; } -function LegendSeriesLabel(props: LegendSeriesLabelProps) { - const { label, color } = props; - return ( -
-
+class LegendSeriesLabel extends React.PureComponent { + render() { + const { label, color } = this.props; + return [ +
-
- +
, + {label} - -
- ); + , + ]; + } } -function LegendValue(props) { +interface LegendValueProps { + value: string; + valueName: string; + asTable?: boolean; +} + +function LegendValue(props: LegendValueProps) { const value = props.value; const valueName = props.valueName; if (props.asTable) { @@ -149,30 +179,21 @@ function renderLegendValues(props: LegendSeriesItemProps, series, asTable = fals return legendValueItems; } -interface LegendTableProps { - seriesList: any[]; - hiddenSeries: any; - values?: boolean; - min?: boolean; - max?: boolean; - avg?: boolean; - current?: boolean; - total?: boolean; -} - -class LegendTable extends React.PureComponent { +class LegendTable extends React.PureComponent> { render() { const seriesList = this.props.seriesList; - const { values, min, max, avg, current, total } = this.props; + const { values, min, max, avg, current, total, sort, sortDesc } = this.props; const seriesValuesProps = { values, min, max, avg, current, total }; - return ( {seriesList.map((series, i) => ( @@ -190,11 +211,21 @@ class LegendTable extends React.PureComponent { } } -class LegendSeriesItemAsTable extends React.Component { - constructor(props) { - super(props); - } +interface LegendTableHeaderProps { + statName: string; +} +function LegendTableHeader(props: LegendTableHeaderProps & LegendSortProps) { + const { statName, sort, sortDesc } = props; + return ( + + ); +} + +class LegendSeriesItemAsTable extends React.PureComponent { render() { const { series, index, hiddenSeries } = this.props; const seriesOptionClasses = getOptionSeriesCSSClasses(series, hiddenSeries); @@ -210,20 +241,6 @@ class LegendSeriesItemAsTable extends React.Component { } } -interface LegendTableHeaderProps { - statName: string; - sortDesc?: boolean; -} - -function LegendTableHeader(props: LegendTableHeaderProps) { - return ( - - ); -} - function getOptionSeriesCSSClasses(series, hiddenSeries) { const classes = []; if (series.yaxis === 2) { diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts index 37841313c82..a1066295048 100755 --- a/public/app/plugins/panel/graph/graph.ts +++ b/public/app/plugins/panel/graph/graph.ts @@ -86,9 +86,10 @@ class GraphElement { updateLegendValues(this.data, this.panel, graphHeight); // this.ctrl.events.emit('render-legend'); + console.log(this.ctrl); const { values, min, max, avg, current, total } = this.panel.legend; - const { alignAsTable, rightSide, sideWidth } = this.panel.legend; - const legendOptions = { alignAsTable, rightSide, sideWidth }; + const { alignAsTable, rightSide, sideWidth, hideEmpty, hideZero } = this.panel.legend; + const legendOptions = { alignAsTable, rightSide, sideWidth, hideEmpty, hideZero }; const valueOptions = { values, min, max, avg, current, total }; const legendProps: GraphLegendProps = { seriesList: this.data, From b891a858ca0934fbec5fd54b64d55d2763bf6f80 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Tue, 4 Sep 2018 12:49:13 +0300 Subject: [PATCH 05/29] graph legend: implement series toggling and sorting --- public/app/plugins/panel/graph/Legend.tsx | 88 +++++++++++++++++++---- public/app/plugins/panel/graph/graph.ts | 8 ++- public/app/plugins/panel/graph/module.ts | 6 ++ 3 files changed, 84 insertions(+), 18 deletions(-) diff --git a/public/app/plugins/panel/graph/Legend.tsx b/public/app/plugins/panel/graph/Legend.tsx index becb52d1aeb..362ca238e80 100644 --- a/public/app/plugins/panel/graph/Legend.tsx +++ b/public/app/plugins/panel/graph/Legend.tsx @@ -7,6 +7,8 @@ const LEGEND_STATS = ['min', 'max', 'avg', 'current', 'total']; interface LegendProps { seriesList: TimeSeries[]; optionalClass?: string; + onToggleSeries?: (series: TimeSeries, event: Event) => void; + onToggleSort?: (sortBy, sortDesc) => void; } interface LegendDisplayProps { @@ -70,11 +72,18 @@ export class GraphLegend extends React.PureComponent !series.hideFromLegend(seriesHideProps)); const legendCustomClasses = `${this.props.alignAsTable ? 'graph-legend-table' : ''} ${optionalClass}`; @@ -87,14 +96,19 @@ export class GraphLegend extends React.PureComponent this.onToggleSeries(s, e), + onToggleSort: (sortBy, sortDesc) => this.props.onToggleSort(sortBy, sortDesc), + ...seriesValuesProps, + ...sortProps, + }; + return (
- {this.props.alignAsTable ? ( - - ) : ( - - )} + {this.props.alignAsTable ? : }
); @@ -106,7 +120,14 @@ 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) => ( - + this.props.onToggleSeries(series, e)} + /> )); } } @@ -114,6 +135,7 @@ class LegendSeriesList extends React.PureComponent { interface LegendSeriesProps { series: TimeSeries; index: number; + onLabelClick?: (event) => void; } type LegendSeriesItemProps = LegendSeriesProps & LegendDisplayProps & LegendValuesProps; @@ -125,7 +147,11 @@ class LegendSeriesItem extends React.PureComponent { const valueItems = this.props.values ? renderLegendValues(this.props, series) : []; return (
- + this.props.onLabelClick(e)} + /> {valueItems}
); @@ -135,16 +161,18 @@ class LegendSeriesItem extends React.PureComponent { interface LegendSeriesLabelProps { label: string; color: string; + onLabelClick?: (event) => void; + onIconClick?: (event) => void; } class LegendSeriesLabel extends React.PureComponent { render() { const { label, color } = this.props; return [ -
+
this.props.onIconClick(e)}>
, - + this.props.onLabelClick(e)}> {label} , ]; @@ -180,6 +208,24 @@ function renderLegendValues(props: LegendSeriesItemProps, series, asTable = fals } class LegendTable extends React.PureComponent> { + onToggleSort(stat) { + let sortDesc = this.props.sortDesc; + let sortBy = this.props.sort; + if (stat !== sortBy) { + sortDesc = null; + } + + // if already sort ascending, disable sorting + if (sortDesc === false) { + sortBy = null; + sortDesc = null; + } else { + sortDesc = !sortDesc; + sortBy = stat; + } + this.props.onToggleSort(sortBy, sortDesc); + } + render() { const seriesList = this.props.seriesList; const { values, min, max, avg, current, total, sort, sortDesc } = this.props; @@ -192,7 +238,13 @@ class LegendTable extends React.PureComponent> { {LEGEND_STATS.map( statName => seriesValuesProps[statName] && ( - + this.onToggleSort(statName)} + /> ) )}
@@ -203,6 +255,7 @@ class LegendTable extends React.PureComponent> { index={i} hiddenSeries={this.props.hiddenSeries} {...seriesValuesProps} + onLabelClick={e => this.props.onToggleSeries(series, e)} /> ))} @@ -213,12 +266,13 @@ class LegendTable extends React.PureComponent> { interface LegendTableHeaderProps { statName: string; + onClick?: (event) => void; } -function LegendTableHeader(props: LegendTableHeaderProps & LegendSortProps) { +function LegendTableHeaderItem(props: LegendTableHeaderProps & LegendSortProps) { const { statName, sort, sortDesc } = props; return ( - @@ -233,7 +287,11 @@ class LegendSeriesItemAsTable extends React.PureComponent return ( {valueItems} @@ -246,7 +304,7 @@ function getOptionSeriesCSSClasses(series, hiddenSeries) { if (series.yaxis === 2) { classes.push('graph-legend-series--right-y'); } - if (hiddenSeries[series.alias]) { + 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 a1066295048..2a6962d78e7 100755 --- a/public/app/plugins/panel/graph/graph.ts +++ b/public/app/plugins/panel/graph/graph.ts @@ -86,16 +86,18 @@ class GraphElement { updateLegendValues(this.data, this.panel, graphHeight); // this.ctrl.events.emit('render-legend'); - console.log(this.ctrl); + // console.log(this.ctrl); const { values, min, max, avg, current, total } = this.panel.legend; - const { alignAsTable, rightSide, sideWidth, hideEmpty, hideZero } = this.panel.legend; - const legendOptions = { alignAsTable, rightSide, sideWidth, hideEmpty, hideZero }; + const { alignAsTable, rightSide, sideWidth, sort, sortDesc, hideEmpty, hideZero } = this.panel.legend; + const legendOptions = { alignAsTable, rightSide, sideWidth, sort, sortDesc, hideEmpty, hideZero }; const valueOptions = { values, min, max, avg, current, total }; const legendProps: GraphLegendProps = { seriesList: this.data, hiddenSeries: this.ctrl.hiddenSeries, ...legendOptions, ...valueOptions, + onToggleSeries: this.ctrl.toggleSeries.bind(this.ctrl), + onToggleSort: this.ctrl.toggleSort.bind(this.ctrl), }; const legendReactElem = React.createElement(GraphLegend, legendProps); const legendElem = this.elem.parent().find('.graph-legend'); diff --git a/public/app/plugins/panel/graph/module.ts b/public/app/plugins/panel/graph/module.ts index 6467f4e816a..a83417f6e2a 100644 --- a/public/app/plugins/panel/graph/module.ts +++ b/public/app/plugins/panel/graph/module.ts @@ -287,6 +287,12 @@ class GraphCtrl extends MetricsPanelCtrl { } } + toggleSort(sortBy, sortDesc) { + this.panel.legend.sort = sortBy; + this.panel.legend.sortDesc = sortDesc; + this.render(); + } + toggleAxis(info) { var override = _.find(this.panel.seriesOverrides, { alias: info.alias }); if (!override) { From b2ba9c516626dbf3b87a3d0b5895b3aa84ffcc5a Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Thu, 6 Sep 2018 14:23:28 +0300 Subject: [PATCH 06/29] wrapper for react-custom-scrollbars component --- package.json | 2 + .../components/ScrollBar/withScrollBar.tsx | 53 +++++++++++++++++++ public/sass/components/_scrollbar.scss | 43 +++++++++++++++ yarn.lock | 42 ++++++++++++++- 4 files changed, 139 insertions(+), 1 deletion(-) create mode 100644 public/app/core/components/ScrollBar/withScrollBar.tsx diff --git a/package.json b/package.json index 9cc47ff71b8..d7f136cb1b2 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "@types/jest": "^21.1.4", "@types/node": "^8.0.31", "@types/react": "^16.0.25", + "@types/react-custom-scrollbars": "^4.0.5", "@types/react-dom": "^16.0.3", "angular-mocks": "1.6.6", "autoprefixer": "^6.4.0", @@ -154,6 +155,7 @@ "prop-types": "^15.6.0", "rc-cascader": "^0.14.0", "react": "^16.2.0", + "react-custom-scrollbars": "^4.2.1", "react-dom": "^16.2.0", "react-grid-layout": "0.16.6", "react-highlight-words": "^0.10.0", diff --git a/public/app/core/components/ScrollBar/withScrollBar.tsx b/public/app/core/components/ScrollBar/withScrollBar.tsx new file mode 100644 index 00000000000..9f8ad942167 --- /dev/null +++ b/public/app/core/components/ScrollBar/withScrollBar.tsx @@ -0,0 +1,53 @@ +import React from 'react'; +import Scrollbars from 'react-custom-scrollbars'; + +interface WithScrollBarProps { + customClassName?: string; + autoHide?: boolean; + autoHideTimeout?: number; + autoHideDuration?: number; + hideTracksWhenNotNeeded?: boolean; +} + +const withScrollBarDefaultProps: Partial = { + customClassName: 'custom-scrollbars', + autoHide: true, + autoHideTimeout: 200, + autoHideDuration: 200, + hideTracksWhenNotNeeded: false, +}; + +/** + * Wraps component into component from `react-custom-scrollbars` + */ +export default function withScrollBar

(WrappedComponent: React.ComponentType

) { + return class extends React.Component

{ + static defaultProps = withScrollBarDefaultProps; + + render() { + // Use type casting here in order to get rest of the props working. See more + // https://github.com/Microsoft/TypeScript/issues/14409 + // https://github.com/Microsoft/TypeScript/pull/13288 + const { autoHide, autoHideTimeout, autoHideDuration, hideTracksWhenNotNeeded, customClassName, ...props } = this + .props as WithScrollBarProps; + const scrollProps = { autoHide, autoHideTimeout, autoHideDuration, hideTracksWhenNotNeeded }; + + return ( +

} + renderTrackVertical={props =>
} + renderThumbHorizontal={props =>
} + renderThumbVertical={props =>
} + renderView={props =>
} + {...scrollProps} + > + + + ); + } + }; +} diff --git a/public/sass/components/_scrollbar.scss b/public/sass/components/_scrollbar.scss index 78173b73f47..adb9e0c54c0 100644 --- a/public/sass/components/_scrollbar.scss +++ b/public/sass/components/_scrollbar.scss @@ -294,3 +294,46 @@ padding-top: 1px; } } + +// Custom styles for 'react-custom-scrollbars' + +.custom-scrollbars { + // Fix for Firefox. For some reason sometimes .view container gets a height of its content, but in order to + // make scroll working it should fit outer container size (scroll appears only when inner container size is + // greater than outer one). + display: flex; + flex-grow: 1; + + .view { + display: flex; + flex-grow: 1; + } + + .track-vertical { + border-radius: 3px; + width: 6px !important; + + right: 2px; + bottom: 2px; + top: 2px; + } + + .track-horizontal { + border-radius: 3px; + height: 6px !important; + + right: 2px; + bottom: 2px; + left: 2px; + } + + .thumb-vertical { + @include gradient-vertical($scrollbarBackground, $scrollbarBackground2); + border-radius: 6px; + } + + .thumb-horizontal { + @include gradient-horizontal($scrollbarBackground, $scrollbarBackground2); + border-radius: 6px; + } +} diff --git a/yarn.lock b/yarn.lock index c15c77cc45f..54f7572d5d2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -473,6 +473,10 @@ add-dom-event-listener@1.x: dependencies: object-assign "4.x" +add-px-to-style@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/add-px-to-style/-/add-px-to-style-1.0.0.tgz#d0c135441fa8014a8137904531096f67f28f263a" + agent-base@4, agent-base@^4.1.0, agent-base@~4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.2.0.tgz#9838b5c3392b962bad031e6a4c5e1024abec45ce" @@ -3406,6 +3410,14 @@ dom-converter@~0.1: dependencies: utila "~0.3" +dom-css@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/dom-css/-/dom-css-2.1.0.tgz#fdbc2d5a015d0a3e1872e11472bbd0e7b9e6a202" + dependencies: + add-px-to-style "1.0.0" + prefix-style "2.0.1" + to-camel-case "1.0.0" + dom-helpers@^3.3.1: version "3.3.1" resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.3.1.tgz#fc1a4e15ffdf60ddde03a480a9c0fece821dd4a6" @@ -9137,6 +9149,10 @@ prebuild-install@^2.3.0: tunnel-agent "^0.6.0" which-pm-runs "^1.0.0" +prefix-style@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/prefix-style/-/prefix-style-2.0.1.tgz#66bba9a870cfda308a5dc20e85e9120932c95a06" + prelude-ls@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" @@ -9388,7 +9404,7 @@ qw@~1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/qw/-/qw-1.0.1.tgz#efbfdc740f9ad054304426acb183412cc8b996d4" -raf@^3.4.0: +raf@^3.1.0, raf@^3.4.0: version "3.4.0" resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.0.tgz#a28876881b4bc2ca9117d4138163ddb80f781575" dependencies: @@ -9496,6 +9512,14 @@ rc@^1.0.1, rc@^1.1.6, rc@^1.1.7: minimist "^1.2.0" strip-json-comments "~2.0.1" +react-custom-scrollbars@^4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/react-custom-scrollbars/-/react-custom-scrollbars-4.2.1.tgz#830fd9502927e97e8a78c2086813899b2a8b66db" + dependencies: + dom-css "^2.0.0" + prop-types "^15.5.10" + raf "^3.1.0" + react-dom@^16.2.0: version "16.4.0" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.4.0.tgz#099f067dd5827ce36a29eaf9a6cdc7cbf6216b1e" @@ -11335,10 +11359,20 @@ to-buffer@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/to-buffer/-/to-buffer-1.1.1.tgz#493bd48f62d7c43fcded313a03dcadb2e1213a80" +to-camel-case@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/to-camel-case/-/to-camel-case-1.0.0.tgz#1a56054b2f9d696298ce66a60897322b6f423e46" + dependencies: + to-space-case "^1.0.0" + to-fast-properties@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47" +to-no-case@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/to-no-case/-/to-no-case-1.0.2.tgz#c722907164ef6b178132c8e69930212d1b4aa16a" + to-object-path@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af" @@ -11361,6 +11395,12 @@ to-regex@^3.0.1, to-regex@^3.0.2: regex-not "^1.0.2" safe-regex "^1.1.0" +to-space-case@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/to-space-case/-/to-space-case-1.0.0.tgz#b052daafb1b2b29dc770cea0163e5ec0ebc9fc17" + dependencies: + to-no-case "^1.0.0" + toposort@^1.0.0: version "1.0.7" resolved "https://registry.yarnpkg.com/toposort/-/toposort-1.0.7.tgz#2e68442d9f64ec720b8cc89e6443ac6caa950029" From 8db2960d0da06e2178c6d52e18cdad13803d6e89 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Thu, 6 Sep 2018 15:06:54 +0300 Subject: [PATCH 07/29] graph legend: use 'react-custom-scrollbars' for legend scroll --- public/app/plugins/panel/graph/Legend.tsx | 12 +++++++----- public/app/plugins/panel/graph/graph.ts | 5 ++--- public/sass/components/_panel_graph.scss | 7 +------ 3 files changed, 10 insertions(+), 14 deletions(-) diff --git a/public/app/plugins/panel/graph/Legend.tsx b/public/app/plugins/panel/graph/Legend.tsx index 362ca238e80..15f8c7c982a 100644 --- a/public/app/plugins/panel/graph/Legend.tsx +++ b/public/app/plugins/panel/graph/Legend.tsx @@ -1,6 +1,7 @@ import _ from 'lodash'; import React from 'react'; import { TimeSeries } from 'app/core/core'; +import withScrollBar from 'app/core/components/ScrollBar/withScrollBar'; const LEGEND_STATS = ['min', 'max', 'avg', 'current', 'total']; @@ -85,7 +86,7 @@ export class GraphLegend extends React.PureComponent !series.hideFromLegend(seriesHideProps)); - const legendCustomClasses = `${this.props.alignAsTable ? 'graph-legend-table' : ''} ${optionalClass}`; + const legendClass = `${this.props.alignAsTable ? 'graph-legend-table' : ''} ${optionalClass}`; // Set min-width if side style and there is a value, otherwise remove the CSS property // Set width so it works with IE11 @@ -106,10 +107,8 @@ export class GraphLegend extends React.PureComponent -
- {this.props.alignAsTable ? : } -
+
+ {this.props.alignAsTable ? : }
); } @@ -309,3 +308,6 @@ function getOptionSeriesCSSClasses(series, hiddenSeries) { } return classes.join(' '); } + +export const Legend = withScrollBar(GraphLegend); +export default Legend; diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts index 2a6962d78e7..8d510242dfa 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 { GraphLegend, GraphLegendProps } from './Legend'; +import { Legend, GraphLegendProps } from './Legend'; import { GraphCtrl } from './module'; @@ -86,7 +86,6 @@ class GraphElement { updateLegendValues(this.data, this.panel, graphHeight); // this.ctrl.events.emit('render-legend'); - // console.log(this.ctrl); const { values, min, max, avg, current, total } = this.panel.legend; const { alignAsTable, rightSide, sideWidth, sort, sortDesc, hideEmpty, hideZero } = this.panel.legend; const legendOptions = { alignAsTable, rightSide, sideWidth, sort, sortDesc, hideEmpty, hideZero }; @@ -99,7 +98,7 @@ class GraphElement { onToggleSeries: this.ctrl.toggleSeries.bind(this.ctrl), onToggleSort: this.ctrl.toggleSort.bind(this.ctrl), }; - const legendReactElem = React.createElement(GraphLegend, legendProps); + const legendReactElem = React.createElement(Legend, legendProps); const legendElem = this.elem.parent().find('.graph-legend'); ReactDOM.render(legendReactElem, legendElem[0]); this.onLegendRenderingComplete(); diff --git a/public/sass/components/_panel_graph.scss b/public/sass/components/_panel_graph.scss index 72f3ca3dbbe..0d7d4ff05ed 100644 --- a/public/sass/components/_panel_graph.scss +++ b/public/sass/components/_panel_graph.scss @@ -57,9 +57,6 @@ padding-top: 6px; position: relative; - // fix for Firefox (white stripe on the right of scrollbar) - width: calc(100% - 1px); - .popover-content { padding: 0; } @@ -67,11 +64,9 @@ .graph-legend-content { position: relative; - - // fix for Firefox (white stripe on the right of scrollbar) - width: calc(100% - 1px); } +// @TODO: delete unused class .graph-legend-scroll { position: relative; overflow: auto !important; From 28cc605e320bf7ea1e0539df220b744e3baf6dda Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Thu, 6 Sep 2018 15:36:22 +0300 Subject: [PATCH 08/29] tests for withScrollBar() wrapper --- .../__snapshots__/withScrollBar.test.tsx.snap | 86 +++++++++++++++++++ .../ScrollBar/withScrollBar.test.tsx | 23 +++++ 2 files changed, 109 insertions(+) create mode 100644 public/app/core/components/ScrollBar/__snapshots__/withScrollBar.test.tsx.snap create mode 100644 public/app/core/components/ScrollBar/withScrollBar.test.tsx diff --git a/public/app/core/components/ScrollBar/__snapshots__/withScrollBar.test.tsx.snap b/public/app/core/components/ScrollBar/__snapshots__/withScrollBar.test.tsx.snap new file mode 100644 index 00000000000..c6b9b5bb37d --- /dev/null +++ b/public/app/core/components/ScrollBar/__snapshots__/withScrollBar.test.tsx.snap @@ -0,0 +1,86 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`withScrollBar renders correctly 1`] = ` +
+
+
+
+
+
+
+
+
+
+
+`; diff --git a/public/app/core/components/ScrollBar/withScrollBar.test.tsx b/public/app/core/components/ScrollBar/withScrollBar.test.tsx new file mode 100644 index 00000000000..89a24a7db8e --- /dev/null +++ b/public/app/core/components/ScrollBar/withScrollBar.test.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import renderer from 'react-test-renderer'; +import withScrollBar from './withScrollBar'; + +class TestComponent extends React.Component { + render() { + return
; + } +} + +describe('withScrollBar', () => { + it('renders correctly', () => { + const TestComponentWithScroll = withScrollBar(TestComponent); + const tree = renderer + .create( + +

Scrollable content

+
+ ) + .toJSON(); + expect(tree).toMatchSnapshot(); + }); +}); From e67b8a3e1ad449a2d94c1578cd508438e0715222 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Thu, 6 Sep 2018 22:52:14 +0300 Subject: [PATCH 09/29] scrollbar refactor: replace HOC by component with children --- .../ScrollBar/GrafanaScrollbar.test.tsx | 16 ++++++ .../components/ScrollBar/GrafanaScrollbar.tsx | 48 +++++++++++++++++ ...sx.snap => GrafanaScrollbar.test.tsx.snap} | 8 +-- .../ScrollBar/withScrollBar.test.tsx | 23 -------- .../components/ScrollBar/withScrollBar.tsx | 53 ------------------- 5 files changed, 68 insertions(+), 80 deletions(-) create mode 100644 public/app/core/components/ScrollBar/GrafanaScrollbar.test.tsx create mode 100644 public/app/core/components/ScrollBar/GrafanaScrollbar.tsx rename public/app/core/components/ScrollBar/__snapshots__/{withScrollBar.test.tsx.snap => GrafanaScrollbar.test.tsx.snap} (94%) delete mode 100644 public/app/core/components/ScrollBar/withScrollBar.test.tsx delete mode 100644 public/app/core/components/ScrollBar/withScrollBar.tsx diff --git a/public/app/core/components/ScrollBar/GrafanaScrollbar.test.tsx b/public/app/core/components/ScrollBar/GrafanaScrollbar.test.tsx new file mode 100644 index 00000000000..7e519acd29d --- /dev/null +++ b/public/app/core/components/ScrollBar/GrafanaScrollbar.test.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import renderer from 'react-test-renderer'; +import GrafanaScrollbar from './GrafanaScrollbar'; + +describe('GrafanaScrollbar', () => { + it('renders correctly', () => { + const tree = renderer + .create( + +

Scrollable content

+
+ ) + .toJSON(); + expect(tree).toMatchSnapshot(); + }); +}); diff --git a/public/app/core/components/ScrollBar/GrafanaScrollbar.tsx b/public/app/core/components/ScrollBar/GrafanaScrollbar.tsx new file mode 100644 index 00000000000..24e5b0d8828 --- /dev/null +++ b/public/app/core/components/ScrollBar/GrafanaScrollbar.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import Scrollbars from 'react-custom-scrollbars'; + +interface GrafanaScrollBarProps { + customClassName?: string; + autoHide?: boolean; + autoHideTimeout?: number; + autoHideDuration?: number; + hideTracksWhenNotNeeded?: boolean; +} + +const grafanaScrollBarDefaultProps: Partial = { + customClassName: 'custom-scrollbars', + autoHide: true, + autoHideTimeout: 200, + autoHideDuration: 200, + hideTracksWhenNotNeeded: false, +}; + +/** + * Wraps component into component from `react-custom-scrollbars` + */ +class GrafanaScrollbar extends React.Component { + static defaultProps = grafanaScrollBarDefaultProps; + + render() { + const { customClassName, children, ...scrollProps } = this.props; + + return ( +
} + renderTrackVertical={props =>
} + renderThumbHorizontal={props =>
} + renderThumbVertical={props =>
} + renderView={props =>
} + {...scrollProps} + > + {children} + + ); + } +} + +export default GrafanaScrollbar; diff --git a/public/app/core/components/ScrollBar/__snapshots__/withScrollBar.test.tsx.snap b/public/app/core/components/ScrollBar/__snapshots__/GrafanaScrollbar.test.tsx.snap similarity index 94% rename from public/app/core/components/ScrollBar/__snapshots__/withScrollBar.test.tsx.snap rename to public/app/core/components/ScrollBar/__snapshots__/GrafanaScrollbar.test.tsx.snap index c6b9b5bb37d..8e4f51e3587 100644 --- a/public/app/core/components/ScrollBar/__snapshots__/withScrollBar.test.tsx.snap +++ b/public/app/core/components/ScrollBar/__snapshots__/GrafanaScrollbar.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`withScrollBar renders correctly 1`] = ` +exports[`GrafanaScrollbar renders correctly 1`] = `
-
+

+ Scrollable content +

; - } -} - -describe('withScrollBar', () => { - it('renders correctly', () => { - const TestComponentWithScroll = withScrollBar(TestComponent); - const tree = renderer - .create( - -

Scrollable content

-
- ) - .toJSON(); - expect(tree).toMatchSnapshot(); - }); -}); diff --git a/public/app/core/components/ScrollBar/withScrollBar.tsx b/public/app/core/components/ScrollBar/withScrollBar.tsx deleted file mode 100644 index 9f8ad942167..00000000000 --- a/public/app/core/components/ScrollBar/withScrollBar.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import React from 'react'; -import Scrollbars from 'react-custom-scrollbars'; - -interface WithScrollBarProps { - customClassName?: string; - autoHide?: boolean; - autoHideTimeout?: number; - autoHideDuration?: number; - hideTracksWhenNotNeeded?: boolean; -} - -const withScrollBarDefaultProps: Partial = { - customClassName: 'custom-scrollbars', - autoHide: true, - autoHideTimeout: 200, - autoHideDuration: 200, - hideTracksWhenNotNeeded: false, -}; - -/** - * Wraps component into component from `react-custom-scrollbars` - */ -export default function withScrollBar

(WrappedComponent: React.ComponentType

) { - return class extends React.Component

{ - static defaultProps = withScrollBarDefaultProps; - - render() { - // Use type casting here in order to get rest of the props working. See more - // https://github.com/Microsoft/TypeScript/issues/14409 - // https://github.com/Microsoft/TypeScript/pull/13288 - const { autoHide, autoHideTimeout, autoHideDuration, hideTracksWhenNotNeeded, customClassName, ...props } = this - .props as WithScrollBarProps; - const scrollProps = { autoHide, autoHideTimeout, autoHideDuration, hideTracksWhenNotNeeded }; - - return ( -

} - renderTrackVertical={props =>
} - renderThumbHorizontal={props =>
} - renderThumbVertical={props =>
} - renderView={props =>
} - {...scrollProps} - > - - - ); - } - }; -} From 729cc94dafd884f01806413ccbde55f1a55a02c3 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Thu, 6 Sep 2018 22:52:56 +0300 Subject: [PATCH 10/29] graph legend: scroll component refactor --- public/app/plugins/panel/graph/Legend.tsx | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/public/app/plugins/panel/graph/Legend.tsx b/public/app/plugins/panel/graph/Legend.tsx index 15f8c7c982a..2a9b60f392f 100644 --- a/public/app/plugins/panel/graph/Legend.tsx +++ b/public/app/plugins/panel/graph/Legend.tsx @@ -1,7 +1,7 @@ import _ from 'lodash'; import React from 'react'; import { TimeSeries } from 'app/core/core'; -import withScrollBar from 'app/core/components/ScrollBar/withScrollBar'; +import GrafanaScrollbar from 'app/core/components/ScrollBar/GrafanaScrollbar'; const LEGEND_STATS = ['min', 'max', 'avg', 'current', 'total']; @@ -309,5 +309,14 @@ function getOptionSeriesCSSClasses(series, hiddenSeries) { return classes.join(' '); } -export const Legend = withScrollBar(GraphLegend); +export class Legend extends React.Component { + render() { + return ( + + + + ); + } +} + export default Legend; From 349b2787cbb0ff664d784cb41ae2849a82141e5c Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Fri, 7 Sep 2018 14:31:56 +0300 Subject: [PATCH 11/29] scrollbar: use enzyme for tests instead of react-test-renderer --- .../ScrollBar/GrafanaScrollbar.test.tsx | 17 +- .../GrafanaScrollbar.test.tsx.snap | 176 ++++++++++-------- 2 files changed, 111 insertions(+), 82 deletions(-) diff --git a/public/app/core/components/ScrollBar/GrafanaScrollbar.test.tsx b/public/app/core/components/ScrollBar/GrafanaScrollbar.test.tsx index 7e519acd29d..d4d3de6aea7 100644 --- a/public/app/core/components/ScrollBar/GrafanaScrollbar.test.tsx +++ b/public/app/core/components/ScrollBar/GrafanaScrollbar.test.tsx @@ -1,16 +1,15 @@ import React from 'react'; -import renderer from 'react-test-renderer'; +import { mount } from 'enzyme'; +import toJson from 'enzyme-to-json'; import GrafanaScrollbar from './GrafanaScrollbar'; describe('GrafanaScrollbar', () => { it('renders correctly', () => { - const tree = renderer - .create( - -

Scrollable content

-
- ) - .toJSON(); - expect(tree).toMatchSnapshot(); + const tree = mount( + +

Scrollable content

+
+ ); + expect(toJson(tree)).toMatchSnapshot(); }); }); diff --git a/public/app/core/components/ScrollBar/__snapshots__/GrafanaScrollbar.test.tsx.snap b/public/app/core/components/ScrollBar/__snapshots__/GrafanaScrollbar.test.tsx.snap index 8e4f51e3587..7d0af38a6dc 100644 --- a/public/app/core/components/ScrollBar/__snapshots__/GrafanaScrollbar.test.tsx.snap +++ b/public/app/core/components/ScrollBar/__snapshots__/GrafanaScrollbar.test.tsx.snap @@ -1,86 +1,116 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`GrafanaScrollbar renders correctly 1`] = ` -
-
-

- Scrollable content -

-
-
-
-
-
-
-
+ > +
+

+ Scrollable content +

+
+
+
+
+
+
+
+
+ + `; From e4a488baf1279d9d41afd6a4bbe84e9cb6c9a1b5 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Fri, 7 Sep 2018 16:12:28 +0300 Subject: [PATCH 12/29] graph legend: use refactored version of scrollbar, #13175 --- .../ScrollBar/GrafanaScrollbar.test.tsx | 15 --- .../components/ScrollBar/GrafanaScrollbar.tsx | 48 -------- .../GrafanaScrollbar.test.tsx.snap | 116 ------------------ public/app/plugins/panel/graph/Legend.tsx | 8 +- 4 files changed, 4 insertions(+), 183 deletions(-) delete mode 100644 public/app/core/components/ScrollBar/GrafanaScrollbar.test.tsx delete mode 100644 public/app/core/components/ScrollBar/GrafanaScrollbar.tsx delete mode 100644 public/app/core/components/ScrollBar/__snapshots__/GrafanaScrollbar.test.tsx.snap diff --git a/public/app/core/components/ScrollBar/GrafanaScrollbar.test.tsx b/public/app/core/components/ScrollBar/GrafanaScrollbar.test.tsx deleted file mode 100644 index d4d3de6aea7..00000000000 --- a/public/app/core/components/ScrollBar/GrafanaScrollbar.test.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import React from 'react'; -import { mount } from 'enzyme'; -import toJson from 'enzyme-to-json'; -import GrafanaScrollbar from './GrafanaScrollbar'; - -describe('GrafanaScrollbar', () => { - it('renders correctly', () => { - const tree = mount( - -

Scrollable content

-
- ); - expect(toJson(tree)).toMatchSnapshot(); - }); -}); diff --git a/public/app/core/components/ScrollBar/GrafanaScrollbar.tsx b/public/app/core/components/ScrollBar/GrafanaScrollbar.tsx deleted file mode 100644 index 24e5b0d8828..00000000000 --- a/public/app/core/components/ScrollBar/GrafanaScrollbar.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import React from 'react'; -import Scrollbars from 'react-custom-scrollbars'; - -interface GrafanaScrollBarProps { - customClassName?: string; - autoHide?: boolean; - autoHideTimeout?: number; - autoHideDuration?: number; - hideTracksWhenNotNeeded?: boolean; -} - -const grafanaScrollBarDefaultProps: Partial = { - customClassName: 'custom-scrollbars', - autoHide: true, - autoHideTimeout: 200, - autoHideDuration: 200, - hideTracksWhenNotNeeded: false, -}; - -/** - * Wraps component into component from `react-custom-scrollbars` - */ -class GrafanaScrollbar extends React.Component { - static defaultProps = grafanaScrollBarDefaultProps; - - render() { - const { customClassName, children, ...scrollProps } = this.props; - - return ( -
} - renderTrackVertical={props =>
} - renderThumbHorizontal={props =>
} - renderThumbVertical={props =>
} - renderView={props =>
} - {...scrollProps} - > - {children} - - ); - } -} - -export default GrafanaScrollbar; diff --git a/public/app/core/components/ScrollBar/__snapshots__/GrafanaScrollbar.test.tsx.snap b/public/app/core/components/ScrollBar/__snapshots__/GrafanaScrollbar.test.tsx.snap deleted file mode 100644 index 7d0af38a6dc..00000000000 --- a/public/app/core/components/ScrollBar/__snapshots__/GrafanaScrollbar.test.tsx.snap +++ /dev/null @@ -1,116 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`GrafanaScrollbar renders correctly 1`] = ` - - -
-
-

- Scrollable content -

-
-
-
-
-
-
-
-
- - -`; diff --git a/public/app/plugins/panel/graph/Legend.tsx b/public/app/plugins/panel/graph/Legend.tsx index 2a9b60f392f..e493d7d5020 100644 --- a/public/app/plugins/panel/graph/Legend.tsx +++ b/public/app/plugins/panel/graph/Legend.tsx @@ -1,7 +1,7 @@ import _ from 'lodash'; import React from 'react'; import { TimeSeries } from 'app/core/core'; -import GrafanaScrollbar from 'app/core/components/ScrollBar/GrafanaScrollbar'; +import CustomScrollbar from 'app/core/components/CustomScrollbar/CustomScrollbar'; const LEGEND_STATS = ['min', 'max', 'avg', 'current', 'total']; @@ -193,7 +193,7 @@ function LegendValue(props: LegendValueProps) { return
{value}
; } -function renderLegendValues(props: LegendSeriesItemProps, series, asTable = false): React.ReactElement[] { +function renderLegendValues(props: LegendSeriesItemProps, series, asTable = false) { const legendValueItems = []; for (const valueName of LEGEND_STATS) { if (props[valueName]) { @@ -312,9 +312,9 @@ function getOptionSeriesCSSClasses(series, hiddenSeries) { export class Legend extends React.Component { render() { return ( - + - + ); } } From 46ec15a11ed6d6819f2fd88efe73622d29cecdc6 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Tue, 16 Oct 2018 16:50:43 +0300 Subject: [PATCH 13/29] graph legend: add color picker (react) --- public/app/plugins/panel/graph/Legend.tsx | 63 ++++++++++++++++++++++- public/app/plugins/panel/graph/graph.ts | 1 + 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/public/app/plugins/panel/graph/Legend.tsx b/public/app/plugins/panel/graph/Legend.tsx index e493d7d5020..5e8ea441623 100644 --- a/public/app/plugins/panel/graph/Legend.tsx +++ b/public/app/plugins/panel/graph/Legend.tsx @@ -1,7 +1,10 @@ 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']; @@ -10,6 +13,7 @@ interface LegendProps { optionalClass?: string; onToggleSeries?: (series: TimeSeries, event: Event) => void; onToggleSort?: (sortBy, sortDesc) => void; + onColorChange?: (series: TimeSeries, color: string) => void; } interface LegendDisplayProps { @@ -102,6 +106,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), ...seriesValuesProps, ...sortProps, }; @@ -126,6 +131,7 @@ class LegendSeriesList extends React.PureComponent { hiddenSeries={hiddenSeries} {...seriesValuesProps} onLabelClick={e => this.props.onToggleSeries(series, e)} + onColorChange={color => this.props.onColorChange(series, color)} /> )); } @@ -135,6 +141,7 @@ interface LegendSeriesProps { series: TimeSeries; index: number; onLabelClick?: (event) => void; + onColorChange?: (color: string) => void; } type LegendSeriesItemProps = LegendSeriesProps & LegendDisplayProps & LegendValuesProps; @@ -150,6 +157,7 @@ class LegendSeriesItem extends React.PureComponent { label={series.aliasEscaped} color={series.color} onLabelClick={e => this.props.onLabelClick(e)} + onColorChange={e => this.props.onColorChange(e)} /> {valueItems}
@@ -161,14 +169,63 @@ interface LegendSeriesLabelProps { label: string; color: string; onLabelClick?: (event) => void; - onIconClick?: (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 [ -
@@ -290,6 +348,7 @@ class LegendSeriesItemAsTable extends React.PureComponent label={series.aliasEscaped} color={series.color} onLabelClick={e => this.props.onLabelClick(e)} + onColorChange={e => this.props.onColorChange(e)} /> {valueItems} diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts index d41898d397d..640e189859c 100755 --- a/public/app/plugins/panel/graph/graph.ts +++ b/public/app/plugins/panel/graph/graph.ts @@ -97,6 +97,7 @@ class GraphElement { ...valueOptions, onToggleSeries: this.ctrl.toggleSeries.bind(this.ctrl), onToggleSort: this.ctrl.toggleSort.bind(this.ctrl), + onColorChange: this.ctrl.changeSeriesColor.bind(this.ctrl), }; const legendReactElem = React.createElement(Legend, legendProps); const legendElem = this.elem.parent().find('.graph-legend'); From fe0c5c73ddcaad1657559f64a6940e0b46bfd86e Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Wed, 17 Oct 2018 15:07:31 +0300 Subject: [PATCH 14/29] 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
; - } - 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 ( - ); } -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 ( - - - {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 ( + + + {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
; + } + 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'); From 5f712ab529e609b0861e2cb612b8764d637ca410 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Thu, 18 Oct 2018 12:31:06 +0300 Subject: [PATCH 15/29] graph legend: remove unused code --- public/app/plugins/panel/graph/graph.ts | 9 +- public/app/plugins/panel/graph/legend.ts | 305 ----------------------- public/app/plugins/panel/graph/module.ts | 1 - 3 files changed, 4 insertions(+), 311 deletions(-) delete mode 100644 public/app/plugins/panel/graph/legend.ts diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts index cc9e9660d3e..63cfbf68f4e 100755 --- a/public/app/plugins/panel/graph/graph.ts +++ b/public/app/plugins/panel/graph/graph.ts @@ -85,7 +85,6 @@ class GraphElement { const graphHeight = this.elem.height(); updateLegendValues(this.data, this.panel, graphHeight); - // this.ctrl.events.emit('render-legend'); const { values, min, max, avg, current, total } = this.panel.legend; const { alignAsTable, rightSide, sideWidth, sort, sortDesc, hideEmpty, hideZero } = this.panel.legend; const legendOptions = { alignAsTable, rightSide, sideWidth, sort, sortDesc, hideEmpty, hideZero }; @@ -106,6 +105,10 @@ class GraphElement { this.onLegendRenderingComplete(); } + onLegendRenderingComplete() { + this.render_panel(); + } + onGraphHover(evt) { // ignore other graph hover events if shared tooltip is disabled if (!this.dashboard.sharedTooltipModeEnabled()) { @@ -129,10 +132,6 @@ class GraphElement { } } - onLegendRenderingComplete() { - this.render_panel(); - } - onGraphHoverClear(event, info) { if (this.plot) { this.tooltip.clear(this.plot); diff --git a/public/app/plugins/panel/graph/legend.ts b/public/app/plugins/panel/graph/legend.ts deleted file mode 100644 index cf317389941..00000000000 --- a/public/app/plugins/panel/graph/legend.ts +++ /dev/null @@ -1,305 +0,0 @@ -import angular from 'angular'; -import _ from 'lodash'; -import $ from 'jquery'; -import baron from 'baron'; - -const module = angular.module('grafana.directives'); - -module.directive('graphLegend', (popoverSrv, $timeout) => { - return { - link: (scope, elem) => { - let firstRender = true; - const ctrl = scope.ctrl; - const panel = ctrl.panel; - let data; - let seriesList; - let i; - let legendScrollbar; - const legendRightDefaultWidth = 10; - const legendElem = elem.parent(); - - scope.$on('$destroy', () => { - destroyScrollbar(); - }); - - ctrl.events.on('render-legend', () => { - data = ctrl.seriesList; - if (data) { - render(); - } - ctrl.events.emit('legend-rendering-complete'); - }); - - function getSeriesIndexForElement(el) { - return el.parents('[data-series-index]').data('series-index'); - } - - function openColorSelector(e) { - // if we clicked inside poup container ignore click - if ($(e.target).parents('.popover').length) { - return; - } - - const el = $(e.currentTarget).find('.fa-minus'); - const index = getSeriesIndexForElement(el); - const series = seriesList[index]; - - $timeout(() => { - popoverSrv.show({ - element: el[0], - position: 'bottom left', - targetAttachment: 'top left', - template: - '' + - '', - openOn: 'hover', - model: { - series: series, - toggleAxis: () => { - ctrl.toggleAxis(series); - }, - colorSelected: color => { - ctrl.changeSeriesColor(series, color); - }, - }, - }); - }); - } - - function toggleSeries(e) { - const el = $(e.currentTarget); - const index = getSeriesIndexForElement(el); - const seriesInfo = seriesList[index]; - const scrollPosition = legendScrollbar.scroller.scrollTop; - ctrl.toggleSeries(seriesInfo, e); - legendScrollbar.scroller.scrollTop = scrollPosition; - } - - function sortLegend(e) { - const el = $(e.currentTarget); - const stat = el.data('stat'); - - if (stat !== panel.legend.sort) { - panel.legend.sortDesc = null; - } - - // if already sort ascending, disable sorting - if (panel.legend.sortDesc === false) { - panel.legend.sort = null; - panel.legend.sortDesc = null; - ctrl.render(); - return; - } - - panel.legend.sortDesc = !panel.legend.sortDesc; - panel.legend.sort = stat; - ctrl.render(); - } - - function getTableHeaderHtml(statName) { - if (!panel.legend[statName]) { - return ''; - } - let html = '
'; - } - - function render() { - const legendWidth = legendElem.width(); - if (!ctrl.panel.legend.show) { - elem.empty(); - firstRender = true; - return; - } - - if (firstRender) { - elem.on('click', '.graph-legend-icon', openColorSelector); - elem.on('click', '.graph-legend-alias', toggleSeries); - elem.on('click', 'th', sortLegend); - firstRender = false; - } - - seriesList = data; - - elem.empty(); - - // Set min-width if side style and there is a value, otherwise remove the CSS property - // Set width so it works with IE11 - const width: any = panel.legend.rightSide && panel.legend.sideWidth ? panel.legend.sideWidth + 'px' : ''; - const ieWidth: any = panel.legend.rightSide && panel.legend.sideWidth ? panel.legend.sideWidth - 1 + 'px' : ''; - legendElem.css('min-width', width); - legendElem.css('width', ieWidth); - - elem.toggleClass('graph-legend-table', panel.legend.alignAsTable === true); - - let tableHeaderElem; - if (panel.legend.alignAsTable) { - let header = ''; - header += ''; - if (panel.legend.values) { - header += getTableHeaderHtml('min'); - header += getTableHeaderHtml('max'); - header += getTableHeaderHtml('avg'); - header += getTableHeaderHtml('current'); - header += getTableHeaderHtml('total'); - } - header += ''; - tableHeaderElem = $(header); - } - - if (panel.legend.sort) { - seriesList = _.sortBy(seriesList, series => { - let sort = series.stats[panel.legend.sort]; - if (sort === null) { - sort = -Infinity; - } - return sort; - }); - if (panel.legend.sortDesc) { - seriesList = seriesList.reverse(); - } - } - - // render first time for getting proper legend height - if (!panel.legend.rightSide || (panel.legend.rightSide && legendWidth !== legendRightDefaultWidth)) { - renderLegendElement(tableHeaderElem); - elem.empty(); - } - - renderLegendElement(tableHeaderElem); - } - - function renderSeriesLegendElements() { - const seriesElements = []; - for (i = 0; i < seriesList.length; i++) { - const series = seriesList[i]; - - if (series.hideFromLegend(panel.legend)) { - continue; - } - - let html = '
'; - html += '
'; - html += ''; - html += '
'; - - html += - '' + series.aliasEscaped + ''; - - if (panel.legend.values) { - const avg = series.formatValue(series.stats.avg); - const current = series.formatValue(series.stats.current); - const min = series.formatValue(series.stats.min); - const max = series.formatValue(series.stats.max); - const total = series.formatValue(series.stats.total); - - if (panel.legend.min) { - html += '
' + min + '
'; - } - if (panel.legend.max) { - html += '
' + max + '
'; - } - if (panel.legend.avg) { - html += '
' + avg + '
'; - } - if (panel.legend.current) { - html += '
' + current + '
'; - } - if (panel.legend.total) { - html += '
' + total + '
'; - } - } - - html += '
'; - seriesElements.push($(html)); - } - return seriesElements; - } - - function renderLegendElement(tableHeaderElem) { - const legendWidth = elem.width(); - - const seriesElements = renderSeriesLegendElements(); - - if (panel.legend.alignAsTable) { - const tbodyElem = $('
'); - tbodyElem.append(tableHeaderElem); - tbodyElem.append(seriesElements); - elem.append(tbodyElem); - tbodyElem.wrap('
'); - } else { - elem.append('
'); - elem.find('.graph-legend-scroll').append(seriesElements); - } - - if (!panel.legend.rightSide || (panel.legend.rightSide && legendWidth !== legendRightDefaultWidth)) { - addScrollbar(); - } else { - destroyScrollbar(); - } - } - - function addScrollbar() { - const scrollRootClass = 'baron baron__root'; - const scrollerClass = 'baron__scroller'; - const scrollBarHTML = ` -
-
-
- `; - - const scrollRoot = elem; - const scroller = elem.find('.graph-legend-scroll'); - - // clear existing scroll bar track to prevent duplication - scrollRoot.find('.baron__track').remove(); - - scrollRoot.addClass(scrollRootClass); - $(scrollBarHTML).appendTo(scrollRoot); - scroller.addClass(scrollerClass); - - const scrollbarParams = { - root: scrollRoot[0], - scroller: scroller[0], - bar: '.baron__bar', - track: '.baron__track', - barOnCls: '_scrollbar', - scrollingCls: '_scrolling', - }; - - if (!legendScrollbar) { - legendScrollbar = baron(scrollbarParams); - } else { - destroyScrollbar(); - legendScrollbar = baron(scrollbarParams); - } - - // #11830 - compensates for Firefox scrollbar calculation error in the baron framework - scroller[0].style.marginRight = '-' + (scroller[0].offsetWidth - scroller[0].clientWidth) + 'px'; - - legendScrollbar.scroll(); - } - - function destroyScrollbar() { - if (legendScrollbar) { - legendScrollbar.dispose(); - legendScrollbar = undefined; - } - } - }, - }; -}); diff --git a/public/app/plugins/panel/graph/module.ts b/public/app/plugins/panel/graph/module.ts index e89cc6ef172..e897ccaaf98 100644 --- a/public/app/plugins/panel/graph/module.ts +++ b/public/app/plugins/panel/graph/module.ts @@ -1,5 +1,4 @@ import './graph'; -import './legend'; import './series_overrides_ctrl'; import './thresholds_form'; From 44ed188c8449ad1b5c6d328a6c0b348243c2b9fd Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Fri, 19 Oct 2018 14:32:37 +0300 Subject: [PATCH 16/29] graph legend: review fixes --- .../app/plugins/panel/graph/Legend/Legend.tsx | 83 +++++++++++-------- .../panel/graph/Legend/LegendSeriesItem.tsx | 35 +++++--- public/app/plugins/panel/graph/graph.ts | 17 ++-- 3 files changed, 79 insertions(+), 56 deletions(-) diff --git a/public/app/plugins/panel/graph/Legend/Legend.tsx b/public/app/plugins/panel/graph/Legend/Legend.tsx index f6daf778848..ee9de5b685e 100644 --- a/public/app/plugins/panel/graph/Legend/Legend.tsx +++ b/public/app/plugins/panel/graph/Legend/Legend.tsx @@ -57,6 +57,21 @@ export class GraphLegend extends React.PureComponent { onColorChange: () => {}, }; + onToggleSeries = (series, event) => { + this.props.onToggleSeries(series, event); + this.forceUpdate(); + }; + + onToggleAxis = series => { + this.props.onToggleAxis(series); + this.forceUpdate(); + }; + + onColorChange = (series, color) => { + this.props.onColorChange(series, color); + this.forceUpdate(); + }; + sortLegend() { let seriesList = this.props.seriesList || []; if (this.props.sort) { @@ -74,12 +89,6 @@ export class GraphLegend extends React.PureComponent { return seriesList; } - onToggleSeries(series: TimeSeries, event: Event) { - // const scrollPosition = legendScrollbar.scroller.scrollTop; - this.props.onToggleSeries(series, event); - // legendScrollbar.scroller.scrollTop = scrollPosition; - } - render() { const { optionalClass, hiddenSeries, rightSide, sideWidth, sort, sortDesc, hideEmpty, hideZero } = this.props; const { values, min, max, avg, current, total } = this.props; @@ -101,10 +110,10 @@ export class GraphLegend extends React.PureComponent { const legendProps: GraphLegendProps = { seriesList: seriesList, hiddenSeries: hiddenSeries, - onToggleSeries: (s, e) => 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), + onToggleSeries: this.onToggleSeries, + onToggleAxis: this.onToggleAxis, + onToggleSort: this.props.onToggleSort, + onColorChange: this.onColorChange, ...seriesValuesProps, ...sortProps, }; @@ -121,23 +130,22 @@ class LegendSeriesList extends React.PureComponent { render() { const { seriesList, hiddenSeries, values, min, max, avg, current, total } = this.props; const seriesValuesProps = { values, min, max, avg, current, total }; - return seriesList.map((series, i) => ( + return seriesList.map(series => (
{LEGEND_STATS.map( - statName => seriesValuesProps[statName] && + statName => + seriesValuesProps[statName] && ( + + ) )}
+ {statName} + {sort === statName && } + - {props.statName} - -
+ props.onClick(e)}> {statName} {sort === statName && }
- + this.props.onLabelClick(e)} + />
{value}
props.onClick(e)}> + props.onClick(e)}> {statName} {sort === statName && }
- this.props.onLabelClick(e)} - onColorChange={e => this.props.onColorChange(e)} - /> -
{seriesLabel}
{value}' + statName; - - if (panel.legend.sort === statName) { - const cssClass = panel.legend.sortDesc ? 'fa fa-caret-down' : 'fa fa-caret-up'; - html += ' '; - } - - return html + '
@@ -172,21 +180,20 @@ class LegendTable extends React.PureComponent> { statName={statName} sort={sort} sortDesc={sortDesc} - onClick={e => this.onToggleSort(statName)} + onClick={this.onToggleSort} /> ) )} - {seriesList.map((series, i) => ( + {seriesList.map(series => ( this.props.onToggleSeries(series, e)} - onColorChange={color => this.props.onColorChange(series, color)} - onToggleAxis={() => this.props.onToggleAxis(series)} + hidden={hiddenSeries[series.alias]} + onLabelClick={this.props.onToggleSeries} + onColorChange={this.props.onColorChange} + onToggleAxis={this.props.onToggleAxis} {...seriesValuesProps} /> ))} @@ -198,20 +205,24 @@ class LegendTable extends React.PureComponent> { interface LegendTableHeaderProps { statName: string; - onClick?: (event) => void; + onClick?: (statName: string) => void; } -function LegendTableHeaderItem(props: LegendTableHeaderProps & LegendSortProps) { - const { statName, sort, sortDesc } = props; - return ( - - ); +class LegendTableHeaderItem extends React.PureComponent { + onClick = () => this.props.onClick(this.props.statName); + + render() { + const { statName, sort, sortDesc } = this.props; + return ( + + ); + } } -export class Legend extends React.Component { +export class Legend extends React.PureComponent { render() { return ( diff --git a/public/app/plugins/panel/graph/Legend/LegendSeriesItem.tsx b/public/app/plugins/panel/graph/Legend/LegendSeriesItem.tsx index ce0ed048336..a61c1484b50 100644 --- a/public/app/plugins/panel/graph/Legend/LegendSeriesItem.tsx +++ b/public/app/plugins/panel/graph/Legend/LegendSeriesItem.tsx @@ -5,13 +5,12 @@ 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; + hidden?: boolean; + onLabelClick?: (series, event) => void; + onColorChange?: (series, color: string) => void; + onToggleAxis?: (series) => void; } export interface LegendValuesProps { @@ -28,25 +27,35 @@ type LegendItemProps = LegendLabelProps & LegendValuesProps; export class LegendItem extends React.PureComponent { static defaultProps = { asTable: false, - hiddenSeries: undefined, + hidden: false, onLabelClick: () => {}, onColorChange: () => {}, onToggleAxis: () => {}, }; + onLabelClick = e => this.props.onLabelClick(this.props.series, e); + onToggleAxis = () => { + this.props.onToggleAxis(this.props.series); + this.forceUpdate(); + }; + onColorChange = color => { + this.props.onColorChange(this.props.series, color); + // this.forceUpdate(); + }; + render() { - const { series, hiddenSeries, asTable } = this.props; + const { series, hidden, asTable } = this.props; const { aliasEscaped, color, yaxis } = this.props.series; - const seriesOptionClasses = getOptionSeriesCSSClasses(series, hiddenSeries); + const seriesOptionClasses = getOptionSeriesCSSClasses(series, hidden); const valueItems = this.props.values ? renderLegendValues(this.props, series, asTable) : []; const seriesLabel = ( ); @@ -161,12 +170,12 @@ function renderLegendValues(props: LegendItemProps, series, asTable = false) { return legendValueItems; } -function getOptionSeriesCSSClasses(series, hiddenSeries) { +function getOptionSeriesCSSClasses(series, hidden) { const classes = []; if (series.yaxis === 2) { classes.push('graph-legend-series--right-y'); } - if (hiddenSeries[series.alias] && hiddenSeries[series.alias] === true) { + if (hidden) { 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 63cfbf68f4e..d13be60c01f 100755 --- a/public/app/plugins/panel/graph/graph.ts +++ b/public/app/plugins/panel/graph/graph.ts @@ -66,13 +66,16 @@ class GraphElement { // global events appEvents.on('graph-hover', this.onGraphHover.bind(this), scope); - appEvents.on('graph-hover-clear', this.onGraphHoverClear.bind(this), scope); - this.elem.bind('plotselected', this.onPlotSelected.bind(this)); - this.elem.bind('plotclick', this.onPlotClick.bind(this)); scope.$on('$destroy', this.onScopeDestroy.bind(this)); + + // Bind legend event handlers once in constructor to avoid unnecessary re-rendering + this.ctrl.toggleSeries = this.ctrl.toggleSeries.bind(this.ctrl); + this.ctrl.toggleSort = this.ctrl.toggleSort.bind(this.ctrl); + this.ctrl.changeSeriesColor = this.ctrl.changeSeriesColor.bind(this.ctrl); + this.ctrl.toggleAxis = this.ctrl.toggleAxis.bind(this.ctrl); } onRender(renderData) { @@ -94,10 +97,10 @@ class GraphElement { hiddenSeries: this.ctrl.hiddenSeries, ...legendOptions, ...valueOptions, - 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), + onToggleSeries: this.ctrl.toggleSeries, + onToggleSort: this.ctrl.toggleSort, + onColorChange: this.ctrl.changeSeriesColor, + onToggleAxis: this.ctrl.toggleAxis, }; const legendReactElem = React.createElement(Legend, legendProps); const legendElem = this.elem.parent().find('.graph-legend'); From daa95c2375b387a1837757874060172ac556bc5c Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Fri, 19 Oct 2018 16:20:00 +0300 Subject: [PATCH 17/29] graph legend: refactor, move behaviour logic into component --- .../app/plugins/panel/graph/Legend/Legend.tsx | 100 +++++++++++++++--- .../panel/graph/Legend/LegendSeriesItem.tsx | 53 +++++++--- public/app/plugins/panel/graph/graph.ts | 4 +- public/app/plugins/panel/graph/module.ts | 49 +-------- 4 files changed, 135 insertions(+), 71 deletions(-) diff --git a/public/app/plugins/panel/graph/Legend/Legend.tsx b/public/app/plugins/panel/graph/Legend/Legend.tsx index ee9de5b685e..ce770163a83 100644 --- a/public/app/plugins/panel/graph/Legend/Legend.tsx +++ b/public/app/plugins/panel/graph/Legend/Legend.tsx @@ -7,7 +7,17 @@ import { LegendItem, LEGEND_STATS } from './LegendSeriesItem'; interface LegendProps { seriesList: TimeSeries[]; optionalClass?: string; - onToggleSeries?: (series: TimeSeries, event: Event) => void; +} + +interface LegendEventHandlers { + onToggleSeries?: (hiddenSeries) => void; + onToggleSort?: (sortBy, sortDesc) => void; + onToggleAxis?: (series: TimeSeries) => void; + onColorChange?: (series: TimeSeries, color: string) => void; +} + +interface LegendComponentEventHandlers { + onToggleSeries?: (series, event) => void; onToggleSort?: (sortBy, sortDesc) => void; onToggleAxis?: (series: TimeSeries) => void; onColorChange?: (series: TimeSeries, color: string) => void; @@ -36,9 +46,22 @@ interface LegendSortProps { sortDesc?: boolean; } -export type GraphLegendProps = LegendProps & LegendDisplayProps & LegendValuesProps & LegendSortProps; +export type GraphLegendProps = LegendProps & + LegendDisplayProps & + LegendValuesProps & + LegendSortProps & + LegendEventHandlers; +export type LegendComponentProps = LegendProps & + LegendDisplayProps & + LegendValuesProps & + LegendSortProps & + LegendComponentEventHandlers; -export class GraphLegend extends React.PureComponent { +interface LegendState { + hiddenSeries: any; +} + +export class GraphLegend extends React.PureComponent { static defaultProps: Partial = { values: false, min: false, @@ -57,10 +80,12 @@ export class GraphLegend extends React.PureComponent { onColorChange: () => {}, }; - onToggleSeries = (series, event) => { - this.props.onToggleSeries(series, event); - this.forceUpdate(); - }; + constructor(props) { + super(props); + this.state = { + hiddenSeries: this.props.hiddenSeries, + }; + } onToggleAxis = series => { this.props.onToggleAxis(series); @@ -73,7 +98,7 @@ export class GraphLegend extends React.PureComponent { }; sortLegend() { - let seriesList = this.props.seriesList || []; + let seriesList = [...this.props.seriesList] || []; if (this.props.sort) { seriesList = _.sortBy(seriesList, series => { let sort = series.stats[this.props.sort]; @@ -89,10 +114,61 @@ export class GraphLegend extends React.PureComponent { return seriesList; } + onToggleSeries = (series, event) => { + let hiddenSeries = { ...this.state.hiddenSeries }; + if (event.ctrlKey || event.metaKey || event.shiftKey) { + if (hiddenSeries[series.alias]) { + delete hiddenSeries[series.alias]; + } else { + hiddenSeries[series.alias] = true; + } + } else { + hiddenSeries = this.toggleSeriesExclusiveMode(series); + } + this.setState({ hiddenSeries: hiddenSeries }); + this.props.onToggleSeries(hiddenSeries); + }; + + toggleSeriesExclusiveMode(series) { + const hiddenSeries = { ...this.state.hiddenSeries }; + + if (hiddenSeries[series.alias]) { + delete hiddenSeries[series.alias]; + } + + // check if every other series is hidden + const alreadyExclusive = _.every(this.props.seriesList, value => { + if (value.alias === series.alias) { + return true; + } + + return hiddenSeries[value.alias]; + }); + + if (alreadyExclusive) { + // remove all hidden series + _.each(this.props.seriesList, value => { + delete hiddenSeries[value.alias]; + }); + } else { + // hide all but this serie + _.each(this.props.seriesList, value => { + if (value.alias === series.alias) { + return; + } + + hiddenSeries[value.alias] = true; + }); + } + + return hiddenSeries; + } + render() { - const { optionalClass, hiddenSeries, rightSide, sideWidth, sort, sortDesc, hideEmpty, hideZero } = this.props; + const { optionalClass, rightSide, sideWidth, sort, sortDesc, hideEmpty, hideZero } = this.props; const { values, min, max, avg, current, total } = this.props; const seriesValuesProps = { values, min, max, avg, current, total }; + const hiddenSeries = this.state.hiddenSeries; const seriesHideProps = { hideEmpty, hideZero }; const sortProps = { sort, sortDesc }; const seriesList = _.filter(this.sortLegend(), series => !series.hideFromLegend(seriesHideProps)); @@ -107,7 +183,7 @@ export class GraphLegend extends React.PureComponent { width: ieWidth, }; - const legendProps: GraphLegendProps = { + const legendProps: LegendComponentProps = { seriesList: seriesList, hiddenSeries: hiddenSeries, onToggleSeries: this.onToggleSeries, @@ -126,7 +202,7 @@ export class GraphLegend extends React.PureComponent { } } -class LegendSeriesList extends React.PureComponent { +class LegendSeriesList extends React.PureComponent { render() { const { seriesList, hiddenSeries, values, min, max, avg, current, total } = this.props; const seriesValuesProps = { values, min, max, avg, current, total }; @@ -144,7 +220,7 @@ class LegendSeriesList extends React.PureComponent { } } -class LegendTable extends React.PureComponent> { +class LegendTable extends React.PureComponent> { onToggleSort = stat => { let sortDesc = this.props.sortDesc; let sortBy = this.props.sort; diff --git a/public/app/plugins/panel/graph/Legend/LegendSeriesItem.tsx b/public/app/plugins/panel/graph/Legend/LegendSeriesItem.tsx index a61c1484b50..979fde6f828 100644 --- a/public/app/plugins/panel/graph/Legend/LegendSeriesItem.tsx +++ b/public/app/plugins/panel/graph/Legend/LegendSeriesItem.tsx @@ -24,7 +24,11 @@ export interface LegendValuesProps { type LegendItemProps = LegendLabelProps & LegendValuesProps; -export class LegendItem extends React.PureComponent { +interface LegendItemState { + yaxis: number; +} + +export class LegendItem extends React.PureComponent { static defaultProps = { asTable: false, hidden: false, @@ -33,26 +37,35 @@ export class LegendItem extends React.PureComponent { onToggleAxis: () => {}, }; + constructor(props) { + super(props); + this.state = { + yaxis: this.props.series.yaxis, + }; + } + onLabelClick = e => this.props.onLabelClick(this.props.series, e); + onToggleAxis = () => { - this.props.onToggleAxis(this.props.series); - this.forceUpdate(); + const yaxis = this.state.yaxis === 2 ? 1 : 2; + const info = { alias: this.props.series.alias, yaxis: yaxis }; + this.setState({ yaxis: yaxis }); + this.props.onToggleAxis(info); }; + onColorChange = color => { this.props.onColorChange(this.props.series, color); - // this.forceUpdate(); }; render() { const { series, hidden, asTable } = this.props; - const { aliasEscaped, color, yaxis } = this.props.series; const seriesOptionClasses = getOptionSeriesCSSClasses(series, hidden); const valueItems = this.props.values ? renderLegendValues(this.props, series, asTable) : []; const seriesLabel = ( void; } +interface LegendSeriesIconState { + color: string; +} + function SeriesIcon(props) { return ; } -class LegendSeriesIcon extends React.PureComponent { +class LegendSeriesIcon extends React.PureComponent { static defaultProps = { yaxis: undefined, onColorChange: () => {}, onToggleAxis: () => {}, }; + constructor(props) { + super(props); + this.state = { + color: this.props.color, + }; + } + + onColorChange = color => { + this.setState({ color: color }); + this.props.onColorChange(color); + }; + render() { - const { color, yaxis } = this.props; + const { yaxis } = this.props; const IconWithColorPicker = withColorPicker(SeriesIcon); return ( ); diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts index d13be60c01f..8c18ec491ce 100755 --- a/public/app/plugins/panel/graph/graph.ts +++ b/public/app/plugins/panel/graph/graph.ts @@ -75,7 +75,7 @@ class GraphElement { this.ctrl.toggleSeries = this.ctrl.toggleSeries.bind(this.ctrl); this.ctrl.toggleSort = this.ctrl.toggleSort.bind(this.ctrl); this.ctrl.changeSeriesColor = this.ctrl.changeSeriesColor.bind(this.ctrl); - this.ctrl.toggleAxis = this.ctrl.toggleAxis.bind(this.ctrl); + this.ctrl.setSeriesAxis = this.ctrl.setSeriesAxis.bind(this.ctrl); } onRender(renderData) { @@ -100,7 +100,7 @@ class GraphElement { onToggleSeries: this.ctrl.toggleSeries, onToggleSort: this.ctrl.toggleSort, onColorChange: this.ctrl.changeSeriesColor, - onToggleAxis: this.ctrl.toggleAxis, + onToggleAxis: this.ctrl.setSeriesAxis, }; const legendReactElem = React.createElement(Legend, legendProps); const legendElem = this.elem.parent().find('.graph-legend'); diff --git a/public/app/plugins/panel/graph/module.ts b/public/app/plugins/panel/graph/module.ts index e897ccaaf98..e83c537809c 100644 --- a/public/app/plugins/panel/graph/module.ts +++ b/public/app/plugins/panel/graph/module.ts @@ -249,65 +249,24 @@ class GraphCtrl extends MetricsPanelCtrl { this.render(); } - toggleSeries(serie, event) { - if (event.ctrlKey || event.metaKey || event.shiftKey) { - if (this.hiddenSeries[serie.alias]) { - delete this.hiddenSeries[serie.alias]; - } else { - this.hiddenSeries[serie.alias] = true; - } - } else { - this.toggleSeriesExclusiveMode(serie); - } + toggleSeries(hiddenSeries) { + this.hiddenSeries = hiddenSeries; this.render(); } - toggleSeriesExclusiveMode(serie) { - const hidden = this.hiddenSeries; - - if (hidden[serie.alias]) { - delete hidden[serie.alias]; - } - - // check if every other series is hidden - const alreadyExclusive = _.every(this.seriesList, value => { - if (value.alias === serie.alias) { - return true; - } - - return hidden[value.alias]; - }); - - if (alreadyExclusive) { - // remove all hidden series - _.each(this.seriesList, value => { - delete this.hiddenSeries[value.alias]; - }); - } else { - // hide all but this serie - _.each(this.seriesList, value => { - if (value.alias === serie.alias) { - return; - } - - this.hiddenSeries[value.alias] = true; - }); - } - } - toggleSort(sortBy, sortDesc) { this.panel.legend.sort = sortBy; this.panel.legend.sortDesc = sortDesc; this.render(); } - toggleAxis(info) { + setSeriesAxis(info) { let override = _.find(this.panel.seriesOverrides, { alias: info.alias }); if (!override) { override = { alias: info.alias }; this.panel.seriesOverrides.push(override); } - info.yaxis = override.yaxis = info.yaxis === 2 ? 1 : 2; + override.yaxis = info.yaxis; this.render(); } From 4b9462993ee3ad47edf96cbdd6a2228720c6aedc Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Mon, 22 Oct 2018 14:17:46 +0300 Subject: [PATCH 18/29] graph legend: refactor, fix another review issues --- .../colorpicker/SeriesColorPicker.tsx | 102 ++++++++++-------- .../colorpicker/SeriesColorPickerPopover.tsx | 70 ++++++++++++ .../colorpicker/withColorPicker.tsx | 83 -------------- public/app/core/core.ts | 2 +- .../app/plugins/panel/graph/Legend/Legend.tsx | 14 +-- .../panel/graph/Legend/LegendSeriesItem.tsx | 17 +-- .../panel/graph/series_overrides_ctrl.ts | 2 +- 7 files changed, 142 insertions(+), 148 deletions(-) create mode 100644 public/app/core/components/colorpicker/SeriesColorPickerPopover.tsx delete mode 100644 public/app/core/components/colorpicker/withColorPicker.tsx diff --git a/public/app/core/components/colorpicker/SeriesColorPicker.tsx b/public/app/core/components/colorpicker/SeriesColorPicker.tsx index 9abd3574ae1..ae744da423d 100644 --- a/public/app/core/components/colorpicker/SeriesColorPicker.tsx +++ b/public/app/core/components/colorpicker/SeriesColorPicker.tsx @@ -1,67 +1,81 @@ import React from 'react'; -import { ColorPickerPopover } from './ColorPickerPopover'; -import { react2AngularDirective } from 'app/core/utils/react2angular'; +import ReactDOM from 'react-dom'; +import Drop from 'tether-drop'; +import { SeriesColorPickerPopover } from './SeriesColorPickerPopover'; export interface SeriesColorPickerProps { - // series: any; color: string; yaxis?: number; - onColorChange: (color: string) => void; + optionalClass?: string; + onColorChange: (newColor: string) => void; onToggleAxis?: () => void; } -export class SeriesColorPicker extends React.PureComponent { - render() { - return ( -
- {this.props.yaxis && } - -
- ); - } -} +export class SeriesColorPicker extends React.Component { + pickerElem: any; + colorPickerDrop: any; -interface AxisSelectorProps { - yaxis: number; - onToggleAxis: () => void; -} + static defaultProps = { + optionalClass: '', + yaxis: undefined, + onToggleAxis: () => {}, + }; -interface AxisSelectorState { - yaxis: number; -} - -export class AxisSelector extends React.PureComponent { constructor(props) { super(props); - this.state = { - yaxis: this.props.yaxis, - }; - this.onToggleAxis = this.onToggleAxis.bind(this); + this.openColorPicker = this.openColorPicker.bind(this); } - onToggleAxis() { - this.setState({ - yaxis: this.state.yaxis === 2 ? 1 : 2, + 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' }], + }, }); - this.props.onToggleAxis(); + + 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 leftButtonClass = this.state.yaxis === 1 ? 'btn-success' : 'btn-inverse'; - const rightButtonClass = this.state.yaxis === 2 ? 'btn-success' : 'btn-inverse'; - + const { optionalClass, children } = this.props; return ( -
- - - +
(this.pickerElem = e)} onClick={this.openColorPicker}> + {children}
); } } - -react2AngularDirective('seriesColorPicker', SeriesColorPicker, ['series', 'onColorChange', 'onToggleAxis']); diff --git a/public/app/core/components/colorpicker/SeriesColorPickerPopover.tsx b/public/app/core/components/colorpicker/SeriesColorPickerPopover.tsx new file mode 100644 index 00000000000..085d554300d --- /dev/null +++ b/public/app/core/components/colorpicker/SeriesColorPickerPopover.tsx @@ -0,0 +1,70 @@ +import React from 'react'; +import { ColorPickerPopover } from './ColorPickerPopover'; +import { react2AngularDirective } from 'app/core/utils/react2angular'; + +export interface SeriesColorPickerPopoverProps { + color: string; + yaxis?: number; + onColorChange: (color: string) => void; + onToggleAxis?: () => void; +} + +export class SeriesColorPickerPopover extends React.PureComponent { + render() { + return ( +
+ {this.props.yaxis && } + +
+ ); + } +} + +interface AxisSelectorProps { + yaxis: number; + onToggleAxis: () => void; +} + +interface AxisSelectorState { + yaxis: number; +} + +export class AxisSelector extends React.PureComponent { + constructor(props) { + super(props); + this.state = { + yaxis: this.props.yaxis, + }; + this.onToggleAxis = this.onToggleAxis.bind(this); + } + + onToggleAxis() { + this.setState({ + yaxis: this.state.yaxis === 2 ? 1 : 2, + }); + this.props.onToggleAxis(); + } + + render() { + const leftButtonClass = this.state.yaxis === 1 ? 'btn-success' : 'btn-inverse'; + const rightButtonClass = this.state.yaxis === 2 ? 'btn-success' : 'btn-inverse'; + + return ( +
+ + + +
+ ); + } +} + +react2AngularDirective('seriesColorPickerPopover', SeriesColorPickerPopover, [ + 'series', + 'onColorChange', + 'onToggleAxis', +]); diff --git a/public/app/core/components/colorpicker/withColorPicker.tsx b/public/app/core/components/colorpicker/withColorPicker.tsx deleted file mode 100644 index d0567fe4e18..00000000000 --- a/public/app/core/components/colorpicker/withColorPicker.tsx +++ /dev/null @@ -1,83 +0,0 @@ -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/core/core.ts b/public/app/core/core.ts index 173d6b80b15..9a398a5ae5a 100644 --- a/public/app/core/core.ts +++ b/public/app/core/core.ts @@ -14,7 +14,7 @@ import './components/jsontree/jsontree'; import './components/code_editor/code_editor'; import './utils/outline'; import './components/colorpicker/ColorPicker'; -import './components/colorpicker/SeriesColorPicker'; +import './components/colorpicker/SeriesColorPickerPopover'; import './components/colorpicker/spectrum_picker'; import './services/search_srv'; import './services/ng_react'; diff --git a/public/app/plugins/panel/graph/Legend/Legend.tsx b/public/app/plugins/panel/graph/Legend/Legend.tsx index ce770163a83..46d0b2a5906 100644 --- a/public/app/plugins/panel/graph/Legend/Legend.tsx +++ b/public/app/plugins/panel/graph/Legend/Legend.tsx @@ -87,16 +87,6 @@ export class GraphLegend extends React.PureComponent { - this.props.onToggleAxis(series); - this.forceUpdate(); - }; - - onColorChange = (series, color) => { - this.props.onColorChange(series, color); - this.forceUpdate(); - }; - sortLegend() { let seriesList = [...this.props.seriesList] || []; if (this.props.sort) { @@ -187,9 +177,9 @@ export class GraphLegend extends React.PureComponent { this.props.onColorChange(this.props.series, color); + // Because of PureComponent nature it makes only shallow props comparison and changing of series.color doesn't run + // component re-render. In this case we can't rely on color, selected by user, because it may be overwritten + // by series overrides. So we need to use forceUpdate() to make sure we have proper series color. + this.forceUpdate(); }; render() { @@ -156,17 +160,16 @@ class LegendSeriesIcon extends React.PureComponent + > + +
); } } diff --git a/public/app/plugins/panel/graph/series_overrides_ctrl.ts b/public/app/plugins/panel/graph/series_overrides_ctrl.ts index deb7bd8ba61..33520cb403b 100644 --- a/public/app/plugins/panel/graph/series_overrides_ctrl.ts +++ b/public/app/plugins/panel/graph/series_overrides_ctrl.ts @@ -53,7 +53,7 @@ export function SeriesOverridesCtrl($scope, $element, popoverSrv) { element: $element.find('.dropdown')[0], position: 'top center', openOn: 'click', - template: '', + template: '', model: { autoClose: true, colorSelected: $scope.colorSelected, From 302158fb2b53f2bc046ce1c0a2c199b7b979c000 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Mon, 22 Oct 2018 15:00:35 +0300 Subject: [PATCH 19/29] graph legend: fix rendering after legend changes --- public/app/plugins/panel/graph/graph.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts index 8c18ec491ce..558354fdeeb 100755 --- a/public/app/plugins/panel/graph/graph.ts +++ b/public/app/plugins/panel/graph/graph.ts @@ -104,8 +104,7 @@ class GraphElement { }; const legendReactElem = React.createElement(Legend, legendProps); const legendElem = this.elem.parent().find('.graph-legend'); - ReactDOM.render(legendReactElem, legendElem[0]); - this.onLegendRenderingComplete(); + ReactDOM.render(legendReactElem, legendElem[0], () => this.onLegendRenderingComplete()); } onLegendRenderingComplete() { From 36354856f9d6c6f317b2396f20dd96886fd0c885 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Mon, 22 Oct 2018 15:59:08 +0300 Subject: [PATCH 20/29] graph legend: minor refactor --- .../colorpicker/SeriesColorPicker.tsx | 4 ++ .../panel/graph/Legend/LegendSeriesItem.tsx | 56 ++++++++++--------- 2 files changed, 33 insertions(+), 27 deletions(-) diff --git a/public/app/core/components/colorpicker/SeriesColorPicker.tsx b/public/app/core/components/colorpicker/SeriesColorPicker.tsx index ae744da423d..4fa2cc3f2b9 100644 --- a/public/app/core/components/colorpicker/SeriesColorPicker.tsx +++ b/public/app/core/components/colorpicker/SeriesColorPicker.tsx @@ -26,6 +26,10 @@ export class SeriesColorPicker extends React.Component { this.openColorPicker = this.openColorPicker.bind(this); } + componentWillUnmount() { + this.destroyDrop(); + } + openColorPicker() { if (this.colorPickerDrop) { this.destroyDrop(); diff --git a/public/app/plugins/panel/graph/Legend/LegendSeriesItem.tsx b/public/app/plugins/panel/graph/Legend/LegendSeriesItem.tsx index 6c50e4f1c58..55a28fc10b0 100644 --- a/public/app/plugins/panel/graph/Legend/LegendSeriesItem.tsx +++ b/public/app/plugins/panel/graph/Legend/LegendSeriesItem.tsx @@ -61,10 +61,36 @@ export class LegendItem extends React.PureComponent
; } - -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, hidden) { - const classes = []; - if (series.yaxis === 2) { - classes.push('graph-legend-series--right-y'); - } - if (hidden) { - classes.push('graph-legend-series-hidden'); - } - return classes.join(' '); -} From 011d7ffa70488ae917eb34a4c0f20c6a9573d304 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Mon, 22 Oct 2018 16:45:16 +0300 Subject: [PATCH 21/29] graph legend: fix quotes displaying React already escapes all strings, so it's no need to pass escaped alias --- public/app/plugins/panel/graph/Legend/LegendSeriesItem.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/plugins/panel/graph/Legend/LegendSeriesItem.tsx b/public/app/plugins/panel/graph/Legend/LegendSeriesItem.tsx index 55a28fc10b0..c235f132251 100644 --- a/public/app/plugins/panel/graph/Legend/LegendSeriesItem.tsx +++ b/public/app/plugins/panel/graph/Legend/LegendSeriesItem.tsx @@ -93,7 +93,7 @@ export class LegendItem extends React.PureComponent Date: Mon, 22 Oct 2018 17:12:18 +0300 Subject: [PATCH 22/29] graph legend: fix legend when series are having the same alias --- public/app/plugins/panel/graph/Legend/Legend.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/public/app/plugins/panel/graph/Legend/Legend.tsx b/public/app/plugins/panel/graph/Legend/Legend.tsx index 46d0b2a5906..e0ea048af92 100644 --- a/public/app/plugins/panel/graph/Legend/Legend.tsx +++ b/public/app/plugins/panel/graph/Legend/Legend.tsx @@ -196,9 +196,9 @@ class LegendSeriesList extends React.PureComponent { render() { const { seriesList, hiddenSeries, values, min, max, avg, current, total } = this.props; const seriesValuesProps = { values, min, max, avg, current, total }; - return seriesList.map(series => ( + return seriesList.map((series, i) => (
- {seriesList.map(series => ( + {seriesList.map((series, i) => (
props.onClick(e)}> - {statName} - {sort === statName && } - + {statName} + {sort === statName && } +
- + + + + + + {seriesList.map((series, i) => ( Date: Thu, 25 Oct 2018 13:21:28 +0300 Subject: [PATCH 27/29] graph legend: fix phantomjs rendering when legend is on the right --- public/sass/components/_panel_graph.scss | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/public/sass/components/_panel_graph.scss b/public/sass/components/_panel_graph.scss index 96bcd404381..642cb3e9bcc 100644 --- a/public/sass/components/_panel_graph.scss +++ b/public/sass/components/_panel_graph.scss @@ -121,7 +121,8 @@ .body--phantomjs { .graph-panel--legend-right { .graph-legend { - display: inline-block; + display: block; + max-width: min-content; } .graph-panel__chart { From 1dad52eaadef2b7df1b0e50cda107e3ef6a08676 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Thu, 25 Oct 2018 13:28:05 +0300 Subject: [PATCH 28/29] graph legend: fix table padding --- public/sass/components/_panel_graph.scss | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/public/sass/components/_panel_graph.scss b/public/sass/components/_panel_graph.scss index 642cb3e9bcc..ac2d0c839ba 100644 --- a/public/sass/components/_panel_graph.scss +++ b/public/sass/components/_panel_graph.scss @@ -137,14 +137,9 @@ } .graph-legend-table { - table { - width: 100%; - } - tbody { - padding-bottom: 1px; - padding-right: 5px; - padding-left: 5px; - } + padding-bottom: 1px; + padding-right: 5px; + padding-left: 5px; .graph-legend-series { display: table-row; From 36cd73819ad61395d57465c510fbc95cc42824c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 31 Oct 2018 03:25:00 -0700 Subject: [PATCH 29/29] fixed memory leaks and minor refactoring --- .../colorpicker/SeriesColorPicker.tsx | 7 ++-- public/app/features/dashboard/panel_model.ts | 1 + public/app/features/panel/panel_ctrl.ts | 5 --- public/app/plugins/panel/graph/graph.ts | 39 +++++++++---------- public/app/plugins/panel/graph/module.ts | 16 ++++---- 5 files changed, 31 insertions(+), 37 deletions(-) diff --git a/public/app/core/components/colorpicker/SeriesColorPicker.tsx b/public/app/core/components/colorpicker/SeriesColorPicker.tsx index 4fa2cc3f2b9..d6feaa31965 100644 --- a/public/app/core/components/colorpicker/SeriesColorPicker.tsx +++ b/public/app/core/components/colorpicker/SeriesColorPicker.tsx @@ -23,14 +23,13 @@ export class SeriesColorPicker extends React.Component { constructor(props) { super(props); - this.openColorPicker = this.openColorPicker.bind(this); } componentWillUnmount() { this.destroyDrop(); } - openColorPicker() { + onClickToOpen = () => { if (this.colorPickerDrop) { this.destroyDrop(); } @@ -59,7 +58,7 @@ export class SeriesColorPicker extends React.Component { this.colorPickerDrop = drop; this.colorPickerDrop.open(); - } + }; closeColorPicker() { setTimeout(() => { @@ -77,7 +76,7 @@ export class SeriesColorPicker extends React.Component { render() { const { optionalClass, children } = this.props; return ( -
(this.pickerElem = e)} onClick={this.openColorPicker}> +
(this.pickerElem = e)} onClick={this.onClickToOpen}> {children}
); diff --git a/public/app/features/dashboard/panel_model.ts b/public/app/features/dashboard/panel_model.ts index ebf8a6bb224..d82368d8dd7 100644 --- a/public/app/features/dashboard/panel_model.ts +++ b/public/app/features/dashboard/panel_model.ts @@ -133,6 +133,7 @@ export class PanelModel { } destroy() { + this.events.emit('panel-teardown'); this.events.removeAllListeners(); } } diff --git a/public/app/features/panel/panel_ctrl.ts b/public/app/features/panel/panel_ctrl.ts index 5e216f6b34d..08605132e82 100644 --- a/public/app/features/panel/panel_ctrl.ts +++ b/public/app/features/panel/panel_ctrl.ts @@ -48,11 +48,6 @@ export class PanelCtrl { } $scope.$on('component-did-mount', () => this.panelDidMount()); - - $scope.$on('$destroy', () => { - this.events.emit('panel-teardown'); - this.events.removeAllListeners(); - }); } panelDidMount() { diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts index 558354fdeeb..8a38f5f2b3c 100755 --- a/public/app/plugins/panel/graph/graph.ts +++ b/public/app/plugins/panel/graph/graph.ts @@ -38,6 +38,7 @@ class GraphElement { panelWidth: number; eventManager: EventManager; thresholdManager: ThresholdManager; + legendElem: HTMLElement; constructor(private scope, private elem, private timeSrv) { this.ctrl = scope.ctrl; @@ -53,7 +54,7 @@ class GraphElement { }); // panel events - this.ctrl.events.on('panel-teardown', this.onPanelteardown.bind(this)); + this.ctrl.events.on('panel-teardown', this.onPanelTeardown.bind(this)); /** * Split graph rendering into two parts. @@ -69,13 +70,11 @@ class GraphElement { appEvents.on('graph-hover-clear', this.onGraphHoverClear.bind(this), scope); this.elem.bind('plotselected', this.onPlotSelected.bind(this)); this.elem.bind('plotclick', this.onPlotClick.bind(this)); - scope.$on('$destroy', this.onScopeDestroy.bind(this)); - // Bind legend event handlers once in constructor to avoid unnecessary re-rendering - this.ctrl.toggleSeries = this.ctrl.toggleSeries.bind(this.ctrl); - this.ctrl.toggleSort = this.ctrl.toggleSort.bind(this.ctrl); - this.ctrl.changeSeriesColor = this.ctrl.changeSeriesColor.bind(this.ctrl); - this.ctrl.setSeriesAxis = this.ctrl.setSeriesAxis.bind(this.ctrl); + // get graph legend element + if (this.elem && this.elem.parent) { + this.legendElem = this.elem.parent().find('.graph-legend')[0]; + } } onRender(renderData) { @@ -97,14 +96,13 @@ class GraphElement { hiddenSeries: this.ctrl.hiddenSeries, ...legendOptions, ...valueOptions, - onToggleSeries: this.ctrl.toggleSeries, - onToggleSort: this.ctrl.toggleSort, - onColorChange: this.ctrl.changeSeriesColor, - onToggleAxis: this.ctrl.setSeriesAxis, + onToggleSeries: this.ctrl.onToggleSeries, + onToggleSort: this.ctrl.onToggleSort, + onColorChange: this.ctrl.onColorChange, + onToggleAxis: this.ctrl.onToggleAxis, }; const legendReactElem = React.createElement(Legend, legendProps); - const legendElem = this.elem.parent().find('.graph-legend'); - ReactDOM.render(legendReactElem, legendElem[0], () => this.onLegendRenderingComplete()); + ReactDOM.render(legendReactElem, this.legendElem, () => this.onLegendRenderingComplete()); } onLegendRenderingComplete() { @@ -125,13 +123,20 @@ class GraphElement { this.tooltip.show(evt.pos); } - onPanelteardown() { + onPanelTeardown() { this.thresholdManager = null; if (this.plot) { this.plot.destroy(); this.plot = null; } + + this.tooltip.destroy(); + this.elem.off(); + this.elem.remove(); + + console.log('react unmount'); + ReactDOM.unmountComponentAtNode(this.legendElem); } onGraphHoverClear(event, info) { @@ -179,12 +184,6 @@ class GraphElement { } } - onScopeDestroy() { - this.tooltip.destroy(); - this.elem.off(); - this.elem.remove(); - } - shouldAbortRender() { if (!this.data) { return true; diff --git a/public/app/plugins/panel/graph/module.ts b/public/app/plugins/panel/graph/module.ts index 9bb0213635a..a6c5190d937 100644 --- a/public/app/plugins/panel/graph/module.ts +++ b/public/app/plugins/panel/graph/module.ts @@ -243,24 +243,24 @@ class GraphCtrl extends MetricsPanelCtrl { } } - changeSeriesColor(series, color) { + onColorChange = (series, color) => { series.setColor(color); this.panel.aliasColors[series.alias] = series.color; this.render(); - } + }; - toggleSeries(hiddenSeries) { + onToggleSeries = hiddenSeries => { this.hiddenSeries = hiddenSeries; this.render(); - } + }; - toggleSort(sortBy, sortDesc) { + onToggleSort = (sortBy, sortDesc) => { this.panel.legend.sort = sortBy; this.panel.legend.sortDesc = sortDesc; this.render(); - } + }; - setSeriesAxis(info) { + onToggleAxis = info => { let override = _.find(this.panel.seriesOverrides, { alias: info.alias }); if (!override) { override = { alias: info.alias }; @@ -268,7 +268,7 @@ class GraphCtrl extends MetricsPanelCtrl { } override.yaxis = info.yaxis; this.render(); - } + }; addSeriesOverride(override) { this.panel.seriesOverrides.push(override || {});
{LEGEND_STATS.map( @@ -266,6 +269,8 @@ class LegendTable extends PureComponent> { ) )}