From 34f9b3ff2b4a14121a4f2429d3c6d70e0e2026ac Mon Sep 17 00:00:00 2001 From: Dominik Prokop Date: Tue, 14 May 2019 08:46:35 +0200 Subject: [PATCH] Explore: use @grafana/ui legend (#17027) --- .../src/components/Graph/GraphLegend.tsx | 8 +- .../src/components/Graph/GraphLegendItem.tsx | 35 +- .../src/components/Legend/LegendList.tsx | 2 +- .../components/Legend/LegendSeriesIcon.tsx | 32 +- .../src/components/Legend/SeriesIcon.tsx | 9 +- packages/grafana-ui/src/components/index.ts | 12 +- public/app/features/explore/Graph.tsx | 108 +- public/app/features/explore/Legend.tsx | 66 -- .../explore/__snapshots__/Graph.test.tsx.snap | 1002 +++-------------- 9 files changed, 316 insertions(+), 958 deletions(-) delete mode 100644 public/app/features/explore/Legend.tsx diff --git a/packages/grafana-ui/src/components/Graph/GraphLegend.tsx b/packages/grafana-ui/src/components/Graph/GraphLegend.tsx index 451acf4bac8..5b7a0251e05 100644 --- a/packages/grafana-ui/src/components/Graph/GraphLegend.tsx +++ b/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) => void; + onToggleSort?: (sortBy: string) => void; + onLabelClick?: (item: LegendItem, event: React.MouseEvent) => void; } export const GraphLegend: React.FunctionComponent = ({ @@ -116,3 +116,5 @@ export const GraphLegend: React.FunctionComponent = ({ /> ); }; + +GraphLegend.displayName = 'GraphLegend'; diff --git a/packages/grafana-ui/src/components/Graph/GraphLegendItem.tsx b/packages/grafana-ui/src/components/Graph/GraphLegendItem.tsx index e116287d4d2..37371fe0664 100644 --- a/packages/grafana-ui/src/components/Graph/GraphLegendItem.tsx +++ b/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) => void; - onSeriesColorChange: SeriesColorChangeHandler; - onToggleAxis: () => void; + onLabelClick?: (item: LegendItem, event: React.MouseEvent) => void; + onSeriesColorChange?: SeriesColorChangeHandler; + onToggleAxis?: () => void; } export const GraphLegendListItem: React.FunctionComponent = ({ @@ -21,19 +21,31 @@ export const GraphLegendListItem: React.FunctionComponent onToggleAxis, onLabelClick, }) => { + const theme = useContext(ThemeContext); + return ( <> onSeriesColorChange(item.label, color)} + onColorChange={color => { + if (onSeriesColorChange) { + onSeriesColorChange(item.label, color); + } + }} onToggleAxis={onToggleAxis} yAxis={item.yAxis} />
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 `} > onSeriesColorChange(item.label, color)} + onColorChange={color => { + if (onSeriesColorChange) { + onSeriesColorChange(item.label, color); + } + }} onToggleAxis={onToggleAxis} yAxis={item.yAxis} />
onLabelClick(item, event)} + onClick={event => { + if (onLabelClick) { + onLabelClick(item, event); + } + }} className={css` cursor: pointer; white-space: nowrap; diff --git a/packages/grafana-ui/src/components/Legend/LegendList.tsx b/packages/grafana-ui/src/components/Legend/LegendList.tsx index d103aa3ed80..f220b10a587 100644 --- a/packages/grafana-ui/src/components/Legend/LegendList.tsx +++ b/packages/grafana-ui/src/components/Legend/LegendList.tsx @@ -28,7 +28,7 @@ export const LegendList: React.FunctionComponent = ({ ); }; - const getItemKey = (item: LegendItem) => item.label; + const getItemKey = (item: LegendItem) => `${item.label}`; const styles = { wrapper: cx( diff --git a/packages/grafana-ui/src/components/Legend/LegendSeriesIcon.tsx b/packages/grafana-ui/src/components/Legend/LegendSeriesIcon.tsx index 1913c2e500d..787b818c003 100644 --- a/packages/grafana-ui/src/components/Legend/LegendSeriesIcon.tsx +++ b/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 = ({ + disabled, yAxis, color, onColorChange, onToggleAxis, }) => { - return ( + let iconProps: SeriesIconProps = { + color, + }; + + if (!disabled) { + iconProps = { + ...iconProps, + className: 'pointer', + }; + } + + return disabled ? ( + + + + ) : ( = > {({ ref, showColorPicker, hideColorPicker }) => ( - + )} diff --git a/packages/grafana-ui/src/components/Legend/SeriesIcon.tsx b/packages/grafana-ui/src/components/Legend/SeriesIcon.tsx index 091d79f7fd0..70709a0fcd5 100644 --- a/packages/grafana-ui/src/components/Legend/SeriesIcon.tsx +++ b/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 ; +export interface SeriesIconProps { + color: string; + className?: string; +} +export const SeriesIcon: React.FunctionComponent = ({ color, className }) => { + return ; }; diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts index 5a4f58626c6..860dd5a97ab 100644 --- a/packages/grafana-ui/src/components/index.ts +++ b/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'; diff --git a/public/app/features/explore/Graph.tsx b/public/app/features/explore/Graph.tsx index f9c48fc92c7..b5cdca318af 100644 --- a/public/app/features/explore/Graph.tsx +++ b/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; + hiddenSeries: string[]; showAllTimeSeries: boolean; } @@ -98,11 +96,11 @@ export class Graph extends PureComponent { 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 { 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 { ); }; - 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 { 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 { $.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) { + // 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 {
)}
- + + { + this.onSeriesToggle(item.label, event); + }} + /> ); } diff --git a/public/app/features/explore/Legend.tsx b/public/app/features/explore/Legend.tsx deleted file mode 100644 index 3b67aa74d91..00000000000 --- a/public/app/features/explore/Legend.tsx +++ /dev/null @@ -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; - onToggleSeries?: (series: TimeSeries, exclusive: boolean) => void; -} - -interface LegendItemProps { - hidden: boolean; - onClickLabel?: (series: TimeSeries, event: MouseEvent) => void; - series: TimeSeries; -} - -class LegendItem extends PureComponent { - onClickLabel = e => this.props.onClickLabel(this.props.series, e); - - render() { - const { hidden, series } = this.props; - const seriesClasses = classNames({ - 'graph-legend-series-hidden': hidden, - }); - return ( - - ); - } -} - -export default class Legend extends PureComponent { - 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 ( -
- {items.map((series, i) => ( -
- ); - } -} diff --git a/public/app/features/explore/__snapshots__/Graph.test.tsx.snap b/public/app/features/explore/__snapshots__/Graph.test.tsx.snap index c38fb26a252..d43f9485605 100644 --- a/public/app/features/explore/__snapshots__/Graph.test.tsx.snap +++ b/public/app/features/explore/__snapshots__/Graph.test.tsx.snap @@ -11,450 +11,128 @@ exports[`Render should render component 1`] = ` } } /> - `; @@ -484,473 +162,134 @@ exports[`Render should render component with disclaimer 1`] = ` } } /> - `; @@ -966,10 +305,11 @@ exports[`Render should show query return no time series 1`] = ` } } /> - `;