Commit 34f9b3ff by Dominik Prokop Committed by GitHub

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

parent 13f137a1
......@@ -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';
......@@ -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;
......
......@@ -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(
......
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>
......
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 }} />;
};
......@@ -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';
......
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);
}}
/>
</>
);
}
......
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>
);
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment