Просмотр исходного кода

Merge branch 'graph-legend-to-react'

Torkel Ödegaard 7 лет назад
Родитель
Сommit
dfb9419f86

+ 64 - 33
public/app/core/components/colorpicker/SeriesColorPicker.tsx

@@ -1,53 +1,84 @@
 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 Props {
-  series: any;
-  onColorChange: (color: string) => void;
-  onToggleAxis: () => void;
+export interface SeriesColorPickerProps {
+  color: string;
+  yaxis?: number;
+  optionalClass?: string;
+  onColorChange: (newColor: string) => void;
+  onToggleAxis?: () => void;
 }
 
-export class SeriesColorPicker extends React.Component<Props, any> {
+export class SeriesColorPicker extends React.Component<SeriesColorPickerProps> {
+  pickerElem: any;
+  colorPickerDrop: any;
+
+  static defaultProps = {
+    optionalClass: '',
+    yaxis: undefined,
+    onToggleAxis: () => {},
+  };
+
   constructor(props) {
     super(props);
-    this.onColorChange = this.onColorChange.bind(this);
-    this.onToggleAxis = this.onToggleAxis.bind(this);
-  }
-
-  onColorChange(color) {
-    this.props.onColorChange(color);
   }
 
-  onToggleAxis() {
-    this.props.onToggleAxis();
+  componentWillUnmount() {
+    this.destroyDrop();
   }
 
-  renderAxisSelection() {
-    const leftButtonClass = this.props.series.yaxis === 1 ? 'btn-success' : 'btn-inverse';
-    const rightButtonClass = this.props.series.yaxis === 2 ? 'btn-success' : 'btn-inverse';
+  onClickToOpen = () => {
+    if (this.colorPickerDrop) {
+      this.destroyDrop();
+    }
 
-    return (
-      <div className="p-b-1">
-        <label className="small p-r-1">Y Axis:</label>
-        <button onClick={this.onToggleAxis} className={'btn btn-small ' + leftButtonClass}>
-          Left
-        </button>
-        <button onClick={this.onToggleAxis} className={'btn btn-small ' + rightButtonClass}>
-          Right
-        </button>
-      </div>
+    const { color, yaxis, onColorChange, onToggleAxis } = this.props;
+    const dropContent = (
+      <SeriesColorPickerPopover 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, children } = this.props;
     return (
-      <div className="graph-legend-popover">
-        {this.props.series.yaxis && this.renderAxisSelection()}
-        <ColorPickerPopover color={this.props.series.color} onColorSelect={this.onColorChange} />
+      <div className={optionalClass} ref={e => (this.pickerElem = e)} onClick={this.onClickToOpen}>
+        {children}
       </div>
     );
   }
 }
-
-react2AngularDirective('seriesColorPicker', SeriesColorPicker, ['series', 'onColorChange', 'onToggleAxis']);

+ 70 - 0
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<SeriesColorPickerPopoverProps, 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;
+}
+
+interface AxisSelectorState {
+  yaxis: number;
+}
+
+export class AxisSelector extends React.PureComponent<AxisSelectorProps, AxisSelectorState> {
+  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 (
+      <div className="p-b-1">
+        <label className="small p-r-1">Y Axis:</label>
+        <button onClick={this.onToggleAxis} className={'btn btn-small ' + leftButtonClass}>
+          Left
+        </button>
+        <button onClick={this.onToggleAxis} className={'btn btn-small ' + rightButtonClass}>
+          Right
+        </button>
+      </div>
+    );
+  }
+}
+
+react2AngularDirective('seriesColorPickerPopover', SeriesColorPickerPopover, [
+  'series',
+  'onColorChange',
+  'onToggleAxis',
+]);

+ 1 - 1
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';

+ 1 - 0
public/app/features/dashboard/panel_model.ts

@@ -133,6 +133,7 @@ export class PanelModel {
   }
 
   destroy() {
+    this.events.emit('panel-teardown');
     this.events.removeAllListeners();
   }
 }

+ 0 - 5
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() {

+ 321 - 0
public/app/plugins/panel/graph/Legend/Legend.tsx

@@ -0,0 +1,321 @@
+import _ from 'lodash';
+import React, { PureComponent } from 'react';
+import { TimeSeries } from 'app/core/core';
+import CustomScrollbar from 'app/core/components/CustomScrollbar/CustomScrollbar';
+import { LegendItem, LEGEND_STATS } from './LegendSeriesItem';
+
+interface LegendProps {
+  seriesList: TimeSeries[];
+  optionalClass?: string;
+}
+
+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;
+}
+
+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;
+}
+
+interface LegendSortProps {
+  sort?: 'min' | 'max' | 'avg' | 'current' | 'total';
+  sortDesc?: boolean;
+}
+
+export type GraphLegendProps = LegendProps &
+  LegendDisplayProps &
+  LegendValuesProps &
+  LegendSortProps &
+  LegendEventHandlers;
+export type LegendComponentProps = LegendProps &
+  LegendDisplayProps &
+  LegendValuesProps &
+  LegendSortProps &
+  LegendComponentEventHandlers;
+
+interface LegendState {
+  hiddenSeries: { [seriesAlias: string]: boolean };
+}
+
+export class GraphLegend extends PureComponent<GraphLegendProps, LegendState> {
+  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: () => {},
+  };
+
+  constructor(props) {
+    super(props);
+    this.state = {
+      hiddenSeries: this.props.hiddenSeries,
+    };
+  }
+
+  sortLegend() {
+    let seriesList = [...this.props.seriesList] || [];
+    if (this.props.sort) {
+      seriesList = _.sortBy(seriesList, series => {
+        let sort = series.stats[this.props.sort];
+        if (sort === null) {
+          sort = -Infinity;
+        }
+        return sort;
+      });
+      if (this.props.sortDesc) {
+        seriesList = seriesList.reverse();
+      }
+    }
+    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 = this.props.seriesList.every(value => {
+      if (value.alias === series.alias) {
+        return true;
+      }
+
+      return hiddenSeries[value.alias];
+    });
+
+    if (alreadyExclusive) {
+      // remove all hidden series
+      this.props.seriesList.forEach(value => {
+        delete hiddenSeries[value.alias];
+      });
+    } else {
+      // hide all but this serie
+      this.props.seriesList.forEach(value => {
+        if (value.alias === series.alias) {
+          return;
+        }
+
+        hiddenSeries[value.alias] = true;
+      });
+    }
+
+    return hiddenSeries;
+  }
+
+  render() {
+    const {
+      optionalClass,
+      rightSide,
+      sideWidth,
+      sort,
+      sortDesc,
+      hideEmpty,
+      hideZero,
+      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 = this.sortLegend().filter(series => !series.hideFromLegend(seriesHideProps));
+    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
+    const width: any = rightSide && sideWidth ? sideWidth : undefined;
+    const ieWidth: any = rightSide && sideWidth ? sideWidth - 1 : undefined;
+    const legendStyle: React.CSSProperties = {
+      minWidth: width,
+      width: ieWidth,
+    };
+
+    const legendProps: LegendComponentProps = {
+      seriesList: seriesList,
+      hiddenSeries: hiddenSeries,
+      onToggleSeries: this.onToggleSeries,
+      onToggleAxis: this.props.onToggleAxis,
+      onToggleSort: this.props.onToggleSort,
+      onColorChange: this.props.onColorChange,
+      ...seriesValuesProps,
+      ...sortProps,
+    };
+
+    return (
+      <div className={`graph-legend-content ${legendClass}`} style={legendStyle}>
+        {this.props.alignAsTable ? <LegendTable {...legendProps} /> : <LegendSeriesList {...legendProps} />}
+      </div>
+    );
+  }
+}
+
+class LegendSeriesList extends PureComponent<LegendComponentProps> {
+  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) => (
+      <LegendItem
+        // This trick required because TimeSeries.id is not unique (it's just TimeSeries.alias).
+        // In future would be good to make id unique across the series list.
+        key={`${series.id}-${i}`}
+        series={series}
+        hidden={hiddenSeries[series.alias]}
+        {...seriesValuesProps}
+        onLabelClick={this.props.onToggleSeries}
+        onColorChange={this.props.onColorChange}
+        onToggleAxis={this.props.onToggleAxis}
+      />
+    ));
+  }
+}
+
+class LegendTable extends PureComponent<Partial<LegendComponentProps>> {
+  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, hiddenSeries } = this.props;
+    const seriesValuesProps = { values, min, max, avg, current, total };
+    return (
+      <table>
+        <colgroup>
+          <col style={{ width: '100%' }} />
+        </colgroup>
+        <thead>
+          <tr>
+            <th style={{ textAlign: 'left' }} />
+            {LEGEND_STATS.map(
+              statName =>
+                seriesValuesProps[statName] && (
+                  <LegendTableHeaderItem
+                    key={statName}
+                    statName={statName}
+                    sort={sort}
+                    sortDesc={sortDesc}
+                    onClick={this.onToggleSort}
+                  />
+                )
+            )}
+          </tr>
+        </thead>
+        <tbody>
+          {seriesList.map((series, i) => (
+            <LegendItem
+              key={`${series.id}-${i}`}
+              asTable={true}
+              series={series}
+              hidden={hiddenSeries[series.alias]}
+              onLabelClick={this.props.onToggleSeries}
+              onColorChange={this.props.onColorChange}
+              onToggleAxis={this.props.onToggleAxis}
+              {...seriesValuesProps}
+            />
+          ))}
+        </tbody>
+      </table>
+    );
+  }
+}
+
+interface LegendTableHeaderProps {
+  statName: string;
+  onClick?: (statName: string) => void;
+}
+
+class LegendTableHeaderItem extends PureComponent<LegendTableHeaderProps & LegendSortProps> {
+  onClick = () => this.props.onClick(this.props.statName);
+
+  render() {
+    const { statName, sort, sortDesc } = this.props;
+    return (
+      <th className="pointer" onClick={this.onClick}>
+        {statName}
+        {sort === statName && <span className={sortDesc ? 'fa fa-caret-down' : 'fa fa-caret-up'} />}
+      </th>
+    );
+  }
+}
+
+export class Legend extends PureComponent<GraphLegendProps> {
+  render() {
+    return (
+      <CustomScrollbar>
+        <GraphLegend {...this.props} />
+      </CustomScrollbar>
+    );
+  }
+}
+
+export default Legend;

+ 196 - 0
public/app/plugins/panel/graph/Legend/LegendSeriesItem.tsx

@@ -0,0 +1,196 @@
+import React, { PureComponent } from 'react';
+import classNames from 'classnames';
+import { TimeSeries } from 'app/core/core';
+import { SeriesColorPicker } from 'app/core/components/colorpicker/SeriesColorPicker';
+
+export const LEGEND_STATS = ['min', 'max', 'avg', 'current', 'total'];
+
+export interface LegendLabelProps {
+  series: TimeSeries;
+  asTable?: boolean;
+  hidden?: boolean;
+  onLabelClick?: (series, event) => void;
+  onColorChange?: (series, color: string) => void;
+  onToggleAxis?: (series) => void;
+}
+
+export interface LegendValuesProps {
+  values?: boolean;
+  min?: boolean;
+  max?: boolean;
+  avg?: boolean;
+  current?: boolean;
+  total?: boolean;
+}
+
+type LegendItemProps = LegendLabelProps & LegendValuesProps;
+
+interface LegendItemState {
+  yaxis: number;
+}
+
+export class LegendItem extends PureComponent<LegendItemProps, LegendItemState> {
+  static defaultProps = {
+    asTable: false,
+    hidden: false,
+    onLabelClick: () => {},
+    onColorChange: () => {},
+    onToggleAxis: () => {},
+  };
+
+  constructor(props) {
+    super(props);
+    this.state = {
+      yaxis: this.props.series.yaxis,
+    };
+  }
+
+  onLabelClick = e => this.props.onLabelClick(this.props.series, e);
+
+  onToggleAxis = () => {
+    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);
+    // 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();
+  };
+
+  renderLegendValues() {
+    const { series, asTable } = this.props;
+    const legendValueItems = [];
+    for (const valueName of LEGEND_STATS) {
+      if (this.props[valueName]) {
+        const valueFormatted = series.formatValue(series.stats[valueName]);
+        legendValueItems.push(
+          <LegendValue key={valueName} valueName={valueName} value={valueFormatted} asTable={asTable} />
+        );
+      }
+    }
+    return legendValueItems;
+  }
+
+  render() {
+    const { series, values, asTable, hidden } = this.props;
+    const seriesOptionClasses = classNames({
+      'graph-legend-series-hidden': hidden,
+      'graph-legend-series--right-y': series.yaxis === 2,
+    });
+    const valueItems = values ? this.renderLegendValues() : [];
+    const seriesLabel = (
+      <LegendSeriesLabel
+        label={series.alias}
+        color={series.color}
+        yaxis={this.state.yaxis}
+        onLabelClick={this.onLabelClick}
+        onColorChange={this.onColorChange}
+        onToggleAxis={this.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 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;
+}
+
+interface LegendSeriesIconState {
+  color: string;
+}
+
+function SeriesIcon(props) {
+  return <i className="fa fa-minus pointer" style={{ color: props.color }} />;
+}
+
+class LegendSeriesIcon extends PureComponent<LegendSeriesIconProps, LegendSeriesIconState> {
+  static defaultProps = {
+    yaxis: undefined,
+    onColorChange: () => {},
+    onToggleAxis: () => {},
+  };
+
+  render() {
+    return (
+      <SeriesColorPicker
+        optionalClass="graph-legend-icon"
+        yaxis={this.props.yaxis}
+        color={this.props.color}
+        onColorChange={this.props.onColorChange}
+        onToggleAxis={this.props.onToggleAxis}
+      >
+        <SeriesIcon color={this.props.color} />
+      </SeriesColorPicker>
+    );
+  }
+}
+
+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>;
+}

+ 37 - 16
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 { Legend, GraphLegendProps } from './Legend/Legend';
 
 import { GraphCtrl } from './module';
 
@@ -35,6 +38,7 @@ class GraphElement {
   panelWidth: number;
   eventManager: EventManager;
   thresholdManager: ThresholdManager;
+  legendElem: HTMLElement;
 
   constructor(private scope, private elem, private timeSrv) {
     this.ctrl = scope.ctrl;
@@ -50,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.
@@ -63,13 +67,14 @@ 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));
+
+    // get graph legend element
+    if (this.elem && this.elem.parent) {
+      this.legendElem = this.elem.parent().find('.graph-legend')[0];
+    }
   }
 
   onRender(renderData) {
@@ -82,7 +87,26 @@ 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 };
+    const valueOptions = { values, min, max, avg, current, total };
+    const legendProps: GraphLegendProps = {
+      seriesList: this.data,
+      hiddenSeries: this.ctrl.hiddenSeries,
+      ...legendOptions,
+      ...valueOptions,
+      onToggleSeries: this.ctrl.onToggleSeries,
+      onToggleSort: this.ctrl.onToggleSort,
+      onColorChange: this.ctrl.onColorChange,
+      onToggleAxis: this.ctrl.onToggleAxis,
+    };
+    const legendReactElem = React.createElement(Legend, legendProps);
+    ReactDOM.render(legendReactElem, this.legendElem, () => this.onLegendRenderingComplete());
+  }
+
+  onLegendRenderingComplete() {
+    this.render_panel();
   }
 
   onGraphHover(evt) {
@@ -99,17 +123,20 @@ class GraphElement {
     this.tooltip.show(evt.pos);
   }
 
-  onPanelteardown() {
+  onPanelTeardown() {
     this.thresholdManager = null;
 
     if (this.plot) {
       this.plot.destroy();
       this.plot = null;
     }
-  }
 
-  onLegendRenderingComplete() {
-    this.render_panel();
+    this.tooltip.destroy();
+    this.elem.off();
+    this.elem.remove();
+
+    console.log('react unmount');
+    ReactDOM.unmountComponentAtNode(this.legendElem);
   }
 
   onGraphHoverClear(event, info) {
@@ -157,12 +184,6 @@ class GraphElement {
     }
   }
 
-  onScopeDestroy() {
-    this.tooltip.destroy();
-    this.elem.off();
-    this.elem.remove();
-  }
-
   shouldAbortRender() {
     if (!this.data) {
       return true;

+ 0 - 306
public/app/plugins/panel/graph/legend.ts

@@ -1,306 +0,0 @@
-import _ from 'lodash';
-import $ from 'jquery';
-import baron from 'baron';
-import coreModule from 'app/core/core_module';
-
-/** @ngInject */
-function graphLegendDirective(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:
-              '<series-color-picker series="series" onToggleAxis="toggleAxis" onColorChange="colorSelected">' +
-              '</series-color-picker>',
-            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 = '<th class="pointer" data-stat="' + statName + '">' + statName;
-
-        if (panel.legend.sort === statName) {
-          const cssClass = panel.legend.sortDesc ? 'fa fa-caret-down' : 'fa fa-caret-up';
-          html += ' <span class="' + cssClass + '"></span>';
-        }
-
-        return html + '</th>';
-      }
-
-      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 = '<tr>';
-          header += '<th colspan="2" style="text-align:left"></th>';
-          if (panel.legend.values) {
-            header += getTableHeaderHtml('min');
-            header += getTableHeaderHtml('max');
-            header += getTableHeaderHtml('avg');
-            header += getTableHeaderHtml('current');
-            header += getTableHeaderHtml('total');
-          }
-          header += '</tr>';
-          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 = '<div class="graph-legend-series';
-
-          if (series.yaxis === 2) {
-            html += ' graph-legend-series--right-y';
-          }
-          if (ctrl.hiddenSeries[series.alias]) {
-            html += ' graph-legend-series-hidden';
-          }
-          html += '" data-series-index="' + i + '">';
-          html += '<div class="graph-legend-icon">';
-          html += '<i class="fa fa-minus pointer" style="color:' + series.color + '"></i>';
-          html += '</div>';
-
-          html +=
-            '<a class="graph-legend-alias pointer" title="' + series.aliasEscaped + '">' + series.aliasEscaped + '</a>';
-
-          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 += '<div class="graph-legend-value min">' + min + '</div>';
-            }
-            if (panel.legend.max) {
-              html += '<div class="graph-legend-value max">' + max + '</div>';
-            }
-            if (panel.legend.avg) {
-              html += '<div class="graph-legend-value avg">' + avg + '</div>';
-            }
-            if (panel.legend.current) {
-              html += '<div class="graph-legend-value current">' + current + '</div>';
-            }
-            if (panel.legend.total) {
-              html += '<div class="graph-legend-value total">' + total + '</div>';
-            }
-          }
-
-          html += '</div>';
-          seriesElements.push($(html));
-        }
-        return seriesElements;
-      }
-
-      function renderLegendElement(tableHeaderElem) {
-        const legendWidth = elem.width();
-
-        const seriesElements = renderSeriesLegendElements();
-
-        if (panel.legend.alignAsTable) {
-          const tbodyElem = $('<tbody></tbody>');
-          tbodyElem.append(tableHeaderElem);
-          tbodyElem.append(seriesElements);
-          elem.append(tbodyElem);
-          tbodyElem.wrap('<div class="graph-legend-scroll"></div>');
-        } else {
-          elem.append('<div class="graph-legend-scroll"></div>');
-          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 = `
-          <div class="baron__track">
-            <div class="baron__bar"></div>
-          </div>
-        `;
-
-        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;
-        }
-      }
-    },
-  };
-}
-
-coreModule.directive('graphLegend', graphLegendDirective);

+ 13 - 49
public/app/plugins/panel/graph/module.ts

@@ -1,5 +1,4 @@
 import './graph';
-import './legend';
 import './series_overrides_ctrl';
 import './thresholds_form';
 
@@ -244,67 +243,32 @@ class GraphCtrl extends MetricsPanelCtrl {
     }
   }
 
-  changeSeriesColor(series, color) {
+  onColorChange = (series, color) => {
     series.setColor(color);
     this.panel.aliasColors[series.alias] = series.color;
     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);
-    }
+  onToggleSeries = 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;
-      });
-    }
-  }
+  onToggleSort = (sortBy, sortDesc) => {
+    this.panel.legend.sort = sortBy;
+    this.panel.legend.sortDesc = sortDesc;
+    this.render();
+  };
 
-  toggleAxis(info) {
+  onToggleAxis = 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();
-  }
+  };
 
   addSeriesOverride(override) {
     this.panel.seriesOverrides.push(override || {});

+ 1 - 1
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: '<series-color-picker series="series" onColorChange="colorSelected" />',
+      template: '<series-color-picker-popover series="series" onColorChange="colorSelected" />',
       model: {
         autoClose: true,
         colorSelected: $scope.colorSelected,

+ 12 - 28
public/sass/components/_panel_graph.scss

@@ -14,7 +14,7 @@
 
       .graph-legend-series {
         display: block;
-        padding-left: 0px;
+        padding-left: 4px;
       }
 
       .graph-legend-table .graph-legend-series {
@@ -52,9 +52,6 @@
   padding-top: 6px;
   position: relative;
 
-  // fix for Firefox (white stripe on the right of scrollbar)
-  width: calc(100% - 1px);
-
   .popover-content {
     padding: 0;
   }
@@ -62,15 +59,6 @@
 
 .graph-legend-content {
   position: relative;
-
-  // fix for Firefox (white stripe on the right of scrollbar)
-  width: calc(100% - 1px);
-}
-
-.graph-legend-scroll {
-  position: relative;
-  overflow: auto !important;
-  padding: 1px;
 }
 
 .graph-legend-icon {
@@ -82,8 +70,8 @@
 .graph-legend-icon,
 .graph-legend-alias,
 .graph-legend-value {
+  display: inline;
   cursor: pointer;
-  float: left;
   white-space: nowrap;
   font-size: 85%;
   text-align: left;
@@ -120,6 +108,11 @@
   }
 }
 
+// Don't move series to the right if legend is on the right as well
+.graph-panel--legend-right .graph-legend-series--right-y {
+  float: left;
+}
+
 .graph-legend-value {
   padding-left: 6px;
 }
@@ -128,7 +121,8 @@
 .body--phantomjs {
   .graph-panel--legend-right {
     .graph-legend {
-      display: inline-block;
+      display: block;
+      max-width: min-content;
     }
 
     .graph-panel__chart {
@@ -138,24 +132,14 @@
     .graph-legend-table {
       display: table;
       width: auto;
-
-      .graph-legend-scroll {
-        display: table;
-      }
     }
   }
 }
 
 .graph-legend-table {
-  tbody {
-    display: block;
-    position: relative;
-    overflow-y: auto;
-    overflow-x: hidden;
-    padding-bottom: 1px;
-    padding-right: 5px;
-    padding-left: 5px;
-  }
+  padding-bottom: 1px;
+  padding-right: 5px;
+  padding-left: 5px;
 
   .graph-legend-series {
     display: table-row;