Browse Source

Explore: use @grafana/ui legend (#17027)

Dominik Prokop 6 years ago
parent
commit
34f9b3ff2b

+ 5 - 3
packages/grafana-ui/src/components/Graph/GraphLegend.tsx

@@ -14,10 +14,10 @@ interface GraphLegendProps extends LegendProps {
   displayMode: LegendDisplayMode;
   sortBy?: string;
   sortDesc?: boolean;
-  onSeriesColorChange: SeriesColorChangeHandler;
+  onSeriesColorChange?: SeriesColorChangeHandler;
   onSeriesAxisToggle?: SeriesAxisToggleHandler;
-  onToggleSort: (sortBy: string) => void;
-  onLabelClick: (item: LegendItem, event: React.MouseEvent<HTMLElement>) => void;
+  onToggleSort?: (sortBy: string) => void;
+  onLabelClick?: (item: LegendItem, event: React.MouseEvent<HTMLElement>) => void;
 }
 
 export const GraphLegend: React.FunctionComponent<GraphLegendProps> = ({
@@ -116,3 +116,5 @@ export const GraphLegend: React.FunctionComponent<GraphLegendProps> = ({
     />
   );
 };
+
+GraphLegend.displayName = 'GraphLegend';

+ 28 - 7
packages/grafana-ui/src/components/Graph/GraphLegendItem.tsx

@@ -10,9 +10,9 @@ export interface GraphLegendItemProps {
   key?: React.Key;
   item: LegendItem;
   className?: string;
-  onLabelClick: (item: LegendItem, event: React.MouseEvent<HTMLDivElement>) => void;
-  onSeriesColorChange: SeriesColorChangeHandler;
-  onToggleAxis: () => void;
+  onLabelClick?: (item: LegendItem, event: React.MouseEvent<HTMLDivElement>) => void;
+  onSeriesColorChange?: SeriesColorChangeHandler;
+  onToggleAxis?: () => void;
 }
 
 export const GraphLegendListItem: React.FunctionComponent<GraphLegendItemProps> = ({
@@ -21,19 +21,31 @@ export const GraphLegendListItem: React.FunctionComponent<GraphLegendItemProps>
   onToggleAxis,
   onLabelClick,
 }) => {
+  const theme = useContext(ThemeContext);
+
   return (
     <>
       <LegendSeriesIcon
+        disabled={!onSeriesColorChange}
         color={item.color}
-        onColorChange={color => onSeriesColorChange(item.label, color)}
+        onColorChange={color => {
+          if (onSeriesColorChange) {
+            onSeriesColorChange(item.label, color);
+          }
+        }}
         onToggleAxis={onToggleAxis}
         yAxis={item.yAxis}
       />
       <div
-        onClick={event => onLabelClick(item, event)}
+        onClick={event => {
+          if (onLabelClick) {
+            onLabelClick(item, event);
+          }
+        }}
         className={css`
           cursor: pointer;
           white-space: nowrap;
+          color: ${!item.isVisible && theme.colors.linkDisabled};
         `}
       >
         {item.label}
@@ -74,13 +86,22 @@ export const GraphLegendTableRow: React.FunctionComponent<GraphLegendItemProps>
           `}
         >
           <LegendSeriesIcon
+            disabled={!!onSeriesColorChange}
             color={item.color}
-            onColorChange={color => onSeriesColorChange(item.label, color)}
+            onColorChange={color => {
+              if (onSeriesColorChange) {
+                onSeriesColorChange(item.label, color);
+              }
+            }}
             onToggleAxis={onToggleAxis}
             yAxis={item.yAxis}
           />
           <div
-            onClick={event => onLabelClick(item, event)}
+            onClick={event => {
+              if (onLabelClick) {
+                onLabelClick(item, event);
+              }
+            }}
             className={css`
               cursor: pointer;
               white-space: nowrap;

+ 1 - 1
packages/grafana-ui/src/components/Legend/LegendList.tsx

@@ -28,7 +28,7 @@ export const LegendList: React.FunctionComponent<LegendComponentProps> = ({
     );
   };
 
-  const getItemKey = (item: LegendItem) => item.label;
+  const getItemKey = (item: LegendItem) => `${item.label}`;
 
   const styles = {
     wrapper: cx(

+ 29 - 3
packages/grafana-ui/src/components/Legend/LegendSeriesIcon.tsx

@@ -1,8 +1,10 @@
 import React from 'react';
+import { css, cx } from 'emotion';
 import { SeriesColorPicker } from '../ColorPicker/ColorPicker';
-import { SeriesIcon } from './SeriesIcon';
+import { SeriesIcon, SeriesIconProps } from './SeriesIcon';
 
 interface LegendSeriesIconProps {
+  disabled: boolean;
   color: string;
   yAxis: number;
   onColorChange: (color: string) => void;
@@ -10,12 +12,36 @@ interface LegendSeriesIconProps {
 }
 
 export const LegendSeriesIcon: React.FunctionComponent<LegendSeriesIconProps> = ({
+  disabled,
   yAxis,
   color,
   onColorChange,
   onToggleAxis,
 }) => {
-  return (
+  let iconProps: SeriesIconProps = {
+    color,
+  };
+
+  if (!disabled) {
+    iconProps = {
+      ...iconProps,
+      className: 'pointer',
+    };
+  }
+
+  return disabled ? (
+    <span
+      className={cx(
+        'graph-legend-icon',
+        disabled &&
+          css`
+            cursor: default;
+          `
+      )}
+    >
+      <SeriesIcon {...iconProps} />
+    </span>
+  ) : (
     <SeriesColorPicker
       yaxis={yAxis}
       color={color}
@@ -25,7 +51,7 @@ export const LegendSeriesIcon: React.FunctionComponent<LegendSeriesIconProps> =
     >
       {({ ref, showColorPicker, hideColorPicker }) => (
         <span ref={ref} onClick={showColorPicker} onMouseLeave={hideColorPicker} className="graph-legend-icon">
-          <SeriesIcon color={color} />
+          <SeriesIcon {...iconProps} />
         </span>
       )}
     </SeriesColorPicker>

+ 7 - 2
packages/grafana-ui/src/components/Legend/SeriesIcon.tsx

@@ -1,5 +1,10 @@
 import React from 'react';
+import { cx } from 'emotion';
 
-export const SeriesIcon: React.FunctionComponent<{ color: string }> = ({ color }) => {
-  return <i className="fa fa-minus pointer" style={{ color }} />;
+export interface SeriesIconProps {
+  color: string;
+  className?: string;
+}
+export const SeriesIcon: React.FunctionComponent<SeriesIconProps> = ({ color, className }) => {
+  return <i className={cx('fa', 'fa-minus', className)} style={{ color }} />;
 };

+ 11 - 1
packages/grafana-ui/src/components/index.ts

@@ -45,10 +45,20 @@ export { TableInputCSV } from './Table/TableInputCSV';
 export { BigValue } from './BigValue/BigValue';
 export { Gauge } from './Gauge/Gauge';
 export { Graph } from './Graph/Graph';
+export { GraphLegend } from './Graph/GraphLegend';
 export { GraphWithLegend } from './Graph/GraphWithLegend';
 export { BarGauge } from './BarGauge/BarGauge';
 export { VizRepeater } from './VizRepeater/VizRepeater';
-export { LegendOptions, LegendBasicOptions, LegendRenderOptions, LegendList, LegendTable } from './Legend/Legend';
+export {
+  LegendOptions,
+  LegendBasicOptions,
+  LegendRenderOptions,
+  LegendList,
+  LegendTable,
+  LegendItem,
+  LegendPlacement,
+  LegendDisplayMode,
+} from './Legend/Legend';
 // Panel editors
 export { ThresholdsEditor } from './ThresholdsEditor/ThresholdsEditor';
 export { ClickOutsideWrapper } from './ClickOutsideWrapper/ClickOutsideWrapper';

+ 64 - 44
public/app/features/explore/Graph.tsx

@@ -1,17 +1,15 @@
 import $ from 'jquery';
 import React, { PureComponent } from 'react';
+import difference from 'lodash/difference';
 
 import 'vendor/flot/jquery.flot';
 import 'vendor/flot/jquery.flot.time';
 import 'vendor/flot/jquery.flot.selection';
 import 'vendor/flot/jquery.flot.stack';
 
-import { TimeZone, AbsoluteTimeRange } from '@grafana/ui';
+import { TimeZone, AbsoluteTimeRange, GraphLegend, LegendItem, LegendDisplayMode } from '@grafana/ui';
 import TimeSeries from 'app/core/time_series2';
 
-import Legend from './Legend';
-import { equal, intersect } from './utils/set';
-
 const MAX_NUMBER_OF_TIME_SERIES = 20;
 
 // Copied from graph.ts
@@ -89,7 +87,7 @@ interface GraphState {
    * Type parameter refers to the `alias` property of a `TimeSeries`.
    * Consequently, all series sharing the same alias will share visibility state.
    */
-  hiddenSeries: Set<string>;
+  hiddenSeries: string[];
   showAllTimeSeries: boolean;
 }
 
@@ -98,11 +96,11 @@ export class Graph extends PureComponent<GraphProps, GraphState> {
   dynamicOptions = null;
 
   state = {
-    hiddenSeries: new Set(),
+    hiddenSeries: [],
     showAllTimeSeries: false,
   };
 
-  getGraphData() {
+  getGraphData(): TimeSeries[] {
     const { data } = this.props;
 
     return this.state.showAllTimeSeries ? data : data.slice(0, MAX_NUMBER_OF_TIME_SERIES);
@@ -121,7 +119,7 @@ export class Graph extends PureComponent<GraphProps, GraphState> {
       prevProps.split !== this.props.split ||
       prevProps.height !== this.props.height ||
       prevProps.width !== this.props.width ||
-      !equal(prevState.hiddenSeries, this.state.hiddenSeries)
+      prevState.hiddenSeries !== this.state.hiddenSeries
     ) {
       this.draw();
     }
@@ -168,38 +166,6 @@ export class Graph extends PureComponent<GraphProps, GraphState> {
     );
   };
 
-  onToggleSeries = (series: TimeSeries, exclusive: boolean) => {
-    this.setState((state, props) => {
-      const { data, onToggleSeries } = props;
-      const { hiddenSeries } = state;
-
-      // Deduplicate series as visibility tracks the alias property
-      const oneSeriesVisible = hiddenSeries.size === new Set(data.map(d => d.alias)).size - 1;
-
-      let nextHiddenSeries = new Set();
-      if (exclusive) {
-        if (hiddenSeries.has(series.alias) || !oneSeriesVisible) {
-          nextHiddenSeries = new Set(data.filter(d => d.alias !== series.alias).map(d => d.alias));
-        }
-      } else {
-        // Prune hidden series no longer part of those available from the most recent query
-        const availableSeries = new Set(data.map(d => d.alias));
-        nextHiddenSeries = intersect(new Set(hiddenSeries), availableSeries);
-        if (nextHiddenSeries.has(series.alias)) {
-          nextHiddenSeries.delete(series.alias);
-        } else {
-          nextHiddenSeries.add(series.alias);
-        }
-      }
-      if (onToggleSeries) {
-        onToggleSeries(series.alias, nextHiddenSeries);
-      }
-      return {
-        hiddenSeries: nextHiddenSeries,
-      };
-    }, this.draw);
-  };
-
   draw() {
     const { userOptions = {} } = this.props;
     const { hiddenSeries } = this.state;
@@ -210,7 +176,7 @@ export class Graph extends PureComponent<GraphProps, GraphState> {
 
     if (data && data.length > 0) {
       series = data
-        .filter((ts: TimeSeries) => !hiddenSeries.has(ts.alias))
+        .filter((ts: TimeSeries) => hiddenSeries.indexOf(ts.alias) === -1)
         .map((ts: TimeSeries) => ({
           color: ts.color,
           label: ts.label,
@@ -229,11 +195,57 @@ export class Graph extends PureComponent<GraphProps, GraphState> {
     $.plot($el, series, options);
   }
 
-  render() {
-    const { height = 100, id = 'graph' } = this.props;
+  getLegendItems = (): LegendItem[] => {
     const { hiddenSeries } = this.state;
     const data = this.getGraphData();
 
+    return data.map(series => {
+      return {
+        label: series.alias,
+        color: series.color,
+        isVisible: hiddenSeries.indexOf(series.alias) === -1,
+        yAxis: 1,
+      };
+    });
+  };
+
+  onSeriesToggle(label: string, event: React.MouseEvent<HTMLElement>) {
+    // This implementation is more or less a copy of GraphPanel's logic.
+    // TODO: we need to use Graph's panel controller or split it into smaller
+    // controllers to remove code duplication. Right now we cant easily use that, since Explore
+    // is not using SeriesData for graph yet
+
+    const exclusive = event.ctrlKey || event.metaKey || event.shiftKey;
+
+    this.setState((state, props) => {
+      const { data } = props;
+      let nextHiddenSeries = [];
+      if (exclusive) {
+        // Toggling series with key makes the series itself to toggle
+        if (state.hiddenSeries.indexOf(label) > -1) {
+          nextHiddenSeries = state.hiddenSeries.filter(series => series !== label);
+        } else {
+          nextHiddenSeries = state.hiddenSeries.concat([label]);
+        }
+      } else {
+        // Toggling series with out key toggles all the series but the clicked one
+        const allSeriesLabels = data.map(series => series.label);
+
+        if (state.hiddenSeries.length + 1 === allSeriesLabels.length) {
+          nextHiddenSeries = [];
+        } else {
+          nextHiddenSeries = difference(allSeriesLabels, [label]);
+        }
+      }
+
+      return {
+        hiddenSeries: nextHiddenSeries,
+      };
+    });
+  }
+
+  render() {
+    const { height = 100, id = 'graph' } = this.props;
     return (
       <>
         {this.props.data && this.props.data.length > MAX_NUMBER_OF_TIME_SERIES && !this.state.showAllTimeSeries && (
@@ -246,7 +258,15 @@ export class Graph extends PureComponent<GraphProps, GraphState> {
           </div>
         )}
         <div id={id} className="explore-graph" style={{ height }} />
-        <Legend data={data} hiddenSeries={hiddenSeries} onToggleSeries={this.onToggleSeries} />
+
+        <GraphLegend
+          items={this.getLegendItems()}
+          displayMode={LegendDisplayMode.List}
+          placement="under"
+          onLabelClick={(item, event) => {
+            this.onSeriesToggle(item.label, event);
+          }}
+        />
       </>
     );
   }

+ 0 - 66
public/app/features/explore/Legend.tsx

@@ -1,66 +0,0 @@
-import React, { MouseEvent, PureComponent } from 'react';
-import classNames from 'classnames';
-import { TimeSeries } from 'app/core/core';
-
-interface LegendProps {
-  data: TimeSeries[];
-  hiddenSeries: Set<string>;
-  onToggleSeries?: (series: TimeSeries, exclusive: boolean) => void;
-}
-
-interface LegendItemProps {
-  hidden: boolean;
-  onClickLabel?: (series: TimeSeries, event: MouseEvent) => void;
-  series: TimeSeries;
-}
-
-class LegendItem extends PureComponent<LegendItemProps> {
-  onClickLabel = e => this.props.onClickLabel(this.props.series, e);
-
-  render() {
-    const { hidden, series } = this.props;
-    const seriesClasses = classNames({
-      'graph-legend-series-hidden': hidden,
-    });
-    return (
-      <div className={`graph-legend-series ${seriesClasses}`}>
-        <div className="graph-legend-icon">
-          <i className="fa fa-minus pointer" style={{ color: series.color }} />
-        </div>
-        <a className="graph-legend-alias pointer" title={series.alias} onClick={this.onClickLabel}>
-          {series.alias}
-        </a>
-      </div>
-    );
-  }
-}
-
-export default class Legend extends PureComponent<LegendProps> {
-  static defaultProps = {
-    onToggleSeries: () => {},
-  };
-
-  onClickLabel = (series: TimeSeries, event: MouseEvent) => {
-    const { onToggleSeries } = this.props;
-    const exclusive = event.ctrlKey || event.metaKey || event.shiftKey;
-    onToggleSeries(series, !exclusive);
-  };
-
-  render() {
-    const { data, hiddenSeries } = this.props;
-    const items = data || [];
-    return (
-      <div className="graph-legend ps">
-        {items.map((series, i) => (
-          <LegendItem
-            hidden={hiddenSeries.has(series.alias)}
-            // Workaround to resolve conflicts since series visibility tracks the alias property
-            key={`${series.id}-${i}`}
-            onClickLabel={this.onClickLabel}
-            series={series}
-          />
-        ))}
-      </div>
-    );
-  }
-}

File diff suppressed because it is too large
+ 240 - 901
public/app/features/explore/__snapshots__/Graph.test.tsx.snap


Some files were not shown because too many files changed in this diff