grafana/public/app/plugins/panel/graph/Legend/Legend.tsx

341 lines
9.2 KiB
TypeScript

import { sortBy as _sortBy } from 'lodash';
import React, { PureComponent } from 'react';
import { TimeSeries } from 'app/core/core';
import { CustomScrollbar, Icon } from '@grafana/ui';
import { LegendStat, LegendItem, LEGEND_STATS } from './LegendSeriesItem';
interface LegendProps {
seriesList: TimeSeries[];
optionalClass?: string;
}
interface LegendEventHandlers {
onToggleSeries?: (hiddenSeries: any) => void;
onToggleSort?: (sortBy: any, sortDesc: any) => void;
onToggleAxis?: (series: TimeSeries) => void;
onColorChange?: (series: TimeSeries, color: string) => void;
}
interface LegendComponentEventHandlers {
onToggleSeries?: (series: TimeSeries, event: any) => void;
onToggleSort?: (sortBy: LegendStat | undefined, sortDesc: any) => 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?: LegendStat;
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: GraphLegendProps) {
super(props);
this.state = {
hiddenSeries: this.props.hiddenSeries,
};
}
sortLegend() {
let seriesList: TimeSeries[] = [...this.props.seriesList] || [];
const sortBy = this.props.sort;
if (sortBy && this.props[sortBy] && this.props.alignAsTable) {
seriesList = _sortBy(seriesList, (series) => {
let sort = series.stats[sortBy];
if (sort === null) {
sort = -Infinity;
}
return sort;
}) as TimeSeries[];
if (this.props.sortDesc) {
seriesList = seriesList.reverse();
}
}
return seriesList;
}
onToggleSeries = (series: TimeSeries, event: any) => {
if (!this.props.onToggleSeries) {
return;
}
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: TimeSeries) {
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: LegendStat) => {
if (!this.props.onToggleSort) {
return;
}
let sortDesc = this.props.sortDesc;
let sortBy = this.props.sort;
if (stat !== sortBy) {
sortDesc = undefined;
}
// if already sort ascending, disable sorting
if (sortDesc === false) {
sortBy = undefined;
sortDesc = undefined;
} 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: any = { values, min, max, avg, current, total };
if (!seriesList) {
return null;
}
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 &&
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: LegendStat;
onClick?: (statName: LegendStat) => void;
}
class LegendTableHeaderItem extends PureComponent<LegendTableHeaderProps & LegendSortProps> {
onClick = () => {
if (this.props.onClick) {
this.props.onClick(this.props.statName);
}
};
render() {
const { statName, sort, sortDesc } = this.props;
return (
<th className="pointer" onClick={this.onClick}>
{statName}
{sort === statName && <Icon name={sortDesc ? 'angle-down' : 'angle-up'} />}
</th>
);
}
}
export class Legend extends PureComponent<GraphLegendProps> {
render() {
return (
<CustomScrollbar hideHorizontalTrack>
<GraphLegend {...this.props} />
</CustomScrollbar>
);
}
}
export default Legend;