graph legend: refactor

This commit is contained in:
Alexander Zobnin
2018-10-18 12:30:34 +03:00
parent 46ec15a11e
commit fe0c5c73dd
6 changed files with 321 additions and 209 deletions
-2
View File
@@ -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']);
}
@@ -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<SeriesColorPickerProps, any> {
render() {
return (
<div className="graph-legend-popover">
{this.props.yaxis && <AxisSelector yaxis={this.props.yaxis} onToggleAxis={this.props.onToggleAxis} />}
<ColorPickerPopover color={this.props.color} onColorSelect={this.props.onColorChange} />
</div>
);
}
}
interface AxisSelectorProps {
yaxis: number;
onToggleAxis: () => void;
}
export class SeriesColorPicker extends React.Component<Props, any> {
interface AxisSelectorState {
yaxis: number;
}
export class AxisSelector extends React.PureComponent<AxisSelectorProps, AxisSelectorState> {
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 (
<div className="p-b-1">
@@ -39,15 +62,6 @@ export class SeriesColorPicker extends React.Component<Props, any> {
</div>
);
}
render() {
return (
<div className="graph-legend-popover">
{this.props.series.yaxis && this.renderAxisSelection()}
<ColorPickerPopover color={this.props.series.color} onColorSelect={this.onColorChange} />
</div>
);
}
}
react2AngularDirective('seriesColorPicker', SeriesColorPicker, ['series', 'onColorChange', 'onToggleAxis']);
@@ -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<WithSeriesColorPickerProps, any> {
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 = (
<SeriesColorPicker color={color} yaxis={yaxis} onColorChange={onColorChange} onToggleAxis={onToggleAxis} />
);
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 (
<div className={optionalClass} ref={e => (this.pickerElem = e)} onClick={this.openColorPicker}>
<WrappedComponent {...wrappedComponentProps} />
</div>
);
}
};
}
@@ -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<GraphLegendProps> = {
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<GraphLegendProps, GraphLegendState> {
static defaultProps = defaultGraphLegendProps;
export class GraphLegend extends React.PureComponent<GraphLegendProps> {
static defaultProps: Partial<GraphLegendProps> = {
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<GraphLegendProps, GraphLege
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),
...seriesValuesProps,
...sortProps,
};
@@ -124,7 +122,7 @@ class LegendSeriesList extends React.PureComponent<GraphLegendProps> {
const { seriesList, hiddenSeries, values, min, max, avg, current, total } = this.props;
const seriesValuesProps = { values, min, max, avg, current, total };
return seriesList.map((series, i) => (
<LegendSeriesItem
<LegendItem
key={series.id}
series={series}
index={i}
@@ -132,137 +130,12 @@ class LegendSeriesList extends React.PureComponent<GraphLegendProps> {
{...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<LegendSeriesItemProps> {
render() {
const { series, index, hiddenSeries } = this.props;
const seriesOptionClasses = getOptionSeriesCSSClasses(series, hiddenSeries);
const valueItems = this.props.values ? renderLegendValues(this.props, series) : [];
return (
<div className={`graph-legend-series ${seriesOptionClasses}`} data-series-index={index}>
<LegendSeriesLabel
label={series.aliasEscaped}
color={series.color}
onLabelClick={e => this.props.onLabelClick(e)}
onColorChange={e => this.props.onColorChange(e)}
/>
{valueItems}
</div>
);
}
}
interface LegendSeriesLabelProps {
label: string;
color: string;
onLabelClick?: (event) => void;
onColorChange?: (color: string) => void;
}
class LegendSeriesLabel extends React.PureComponent<LegendSeriesLabelProps> {
pickerElem: any;
colorPickerDrop: any;
openColorPicker() {
if (this.colorPickerDrop) {
this.destroyDrop();
}
const dropContent = <ColorPickerPopover color={this.props.color} onColorSelect={this.props.onColorChange} />;
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 [
<div
key="icon"
className="graph-legend-icon"
ref={e => (this.pickerElem = e)}
onClick={() => this.openColorPicker()}
>
<i className="fa fa-minus pointer" style={{ color: color }} />
</div>,
<a className="graph-legend-alias pointer" title={label} key="label" onClick={e => this.props.onLabelClick(e)}>
{label}
</a>,
];
}
}
interface LegendValueProps {
value: string;
valueName: string;
asTable?: boolean;
}
function LegendValue(props: LegendValueProps) {
const value = props.value;
const valueName = props.valueName;
if (props.asTable) {
return <td className={`graph-legend-value ${valueName}`}>{value}</td>;
}
return <div className={`graph-legend-value ${valueName}`}>{value}</div>;
}
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(
<LegendValue key={valueName} valueName={valueName} value={valueFormatted} asTable={asTable} />
);
}
}
return legendValueItems;
}
class LegendTable extends React.PureComponent<Partial<GraphLegendProps>> {
onToggleSort(stat) {
let sortDesc = this.props.sortDesc;
@@ -305,14 +178,16 @@ class LegendTable extends React.PureComponent<Partial<GraphLegendProps>> {
)}
</tr>
{seriesList.map((series, i) => (
<LegendSeriesItemAsTable
<LegendItem
key={series.id}
asTable={true}
series={series}
index={i}
hiddenSeries={this.props.hiddenSeries}
{...seriesValuesProps}
onLabelClick={e => this.props.onToggleSeries(series, e)}
onColorChange={color => this.props.onColorChange(series, color)}
onToggleAxis={() => this.props.onToggleAxis(series)}
{...seriesValuesProps}
/>
))}
</tbody>
@@ -329,46 +204,14 @@ interface LegendTableHeaderProps {
function LegendTableHeaderItem(props: LegendTableHeaderProps & LegendSortProps) {
const { statName, sort, sortDesc } = props;
return (
<th className="pointer" data-stat={statName} onClick={e => props.onClick(e)}>
<th className="pointer" onClick={e => props.onClick(e)}>
{statName}
{sort === statName && <span className={sortDesc ? 'fa fa-caret-down' : 'fa fa-caret-up'} />}
</th>
);
}
class LegendSeriesItemAsTable extends React.PureComponent<LegendSeriesItemProps> {
render() {
const { series, index, hiddenSeries } = this.props;
const seriesOptionClasses = getOptionSeriesCSSClasses(series, hiddenSeries);
const valueItems = this.props.values ? renderLegendValues(this.props, series, true) : [];
return (
<tr className={`graph-legend-series ${seriesOptionClasses}`} data-series-index={index}>
<td>
<LegendSeriesLabel
label={series.aliasEscaped}
color={series.color}
onLabelClick={e => this.props.onLabelClick(e)}
onColorChange={e => this.props.onColorChange(e)}
/>
</td>
{valueItems}
</tr>
);
}
}
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<GraphLegendProps, GraphLegendState> {
export class Legend extends React.Component<GraphLegendProps> {
render() {
return (
<CustomScrollbar>
@@ -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<LegendItemProps> {
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 = (
<LegendSeriesLabel
label={aliasEscaped}
color={color}
yaxis={yaxis}
onLabelClick={this.props.onLabelClick}
onColorChange={this.props.onColorChange}
onToggleAxis={this.props.onToggleAxis}
/>
);
if (asTable) {
return (
<tr className={`graph-legend-series ${seriesOptionClasses}`}>
<td>{seriesLabel}</td>
{valueItems}
</tr>
);
} else {
return (
<div className={`graph-legend-series ${seriesOptionClasses}`}>
{seriesLabel}
{valueItems}
</div>
);
}
}
}
interface LegendSeriesLabelProps {
label: string;
color: string;
yaxis?: number;
onLabelClick?: (event) => void;
}
class LegendSeriesLabel extends React.PureComponent<LegendSeriesLabelProps & LegendSeriesIconProps> {
static defaultProps = {
yaxis: undefined,
onLabelClick: () => {},
};
render() {
const { label, color, yaxis } = this.props;
const { onColorChange, onToggleAxis } = this.props;
return [
<LegendSeriesIcon
key="icon"
color={color}
yaxis={yaxis}
onColorChange={onColorChange}
onToggleAxis={onToggleAxis}
/>,
<a className="graph-legend-alias pointer" title={label} key="label" onClick={e => this.props.onLabelClick(e)}>
{label}
</a>,
];
}
}
interface LegendSeriesIconProps {
color: string;
yaxis?: number;
onColorChange?: (color: string) => void;
onToggleAxis?: () => void;
}
function SeriesIcon(props) {
return <i className="fa fa-minus pointer" style={{ color: props.color }} />;
}
class LegendSeriesIcon extends React.PureComponent<LegendSeriesIconProps> {
static defaultProps = {
yaxis: undefined,
onColorChange: () => {},
onToggleAxis: () => {},
};
render() {
const { color, yaxis } = this.props;
const IconWithColorPicker = withColorPicker(SeriesIcon);
return (
<IconWithColorPicker
optionalClass="graph-legend-icon"
color={color}
yaxis={yaxis}
onColorChange={this.props.onColorChange}
onToggleAxis={this.props.onToggleAxis}
/>
);
}
}
interface LegendValueProps {
value: string;
valueName: string;
asTable?: boolean;
}
function LegendValue(props: LegendValueProps) {
const value = props.value;
const valueName = props.valueName;
if (props.asTable) {
return <td className={`graph-legend-value ${valueName}`}>{value}</td>;
}
return <div className={`graph-legend-value ${valueName}`}>{value}</div>;
}
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(
<LegendValue key={valueName} valueName={valueName} value={valueFormatted} asTable={asTable} />
);
}
}
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(' ');
}
+2 -1
View File
@@ -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');