mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Explore: use @grafana/ui legend (#17027)
This commit is contained in:
parent
13f137a17d
commit
34f9b3ff2b
@ -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(
|
||||
|
@ -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>
|
||||
|
@ -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 }} />;
|
||||
};
|
||||
|
@ -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';
|
||||
|
@ -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);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -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
Load Diff
Loading…
Reference in New Issue
Block a user