Commit dfb9419f by Torkel Ödegaard

Merge branch 'graph-legend-to-react'

parents baea76c4 36cd7381
import React from 'react'; import React from 'react';
import { ColorPickerPopover } from './ColorPickerPopover'; import ReactDOM from 'react-dom';
import { react2AngularDirective } from 'app/core/utils/react2angular'; import Drop from 'tether-drop';
import { SeriesColorPickerPopover } from './SeriesColorPickerPopover';
export interface Props { export interface SeriesColorPickerProps {
series: any; color: string;
onColorChange: (color: string) => void; yaxis?: number;
onToggleAxis: () => void; optionalClass?: string;
onColorChange: (newColor: string) => void;
onToggleAxis?: () => void;
} }
export class SeriesColorPicker extends React.Component<Props, any> { export class SeriesColorPicker extends React.Component<SeriesColorPickerProps> {
pickerElem: any;
colorPickerDrop: any;
static defaultProps = {
optionalClass: '',
yaxis: undefined,
onToggleAxis: () => {},
};
constructor(props) { constructor(props) {
super(props); super(props);
this.onColorChange = this.onColorChange.bind(this);
this.onToggleAxis = this.onToggleAxis.bind(this);
}
onColorChange(color) {
this.props.onColorChange(color);
} }
onToggleAxis() { componentWillUnmount() {
this.props.onToggleAxis(); this.destroyDrop();
} }
renderAxisSelection() { onClickToOpen = () => {
const leftButtonClass = this.props.series.yaxis === 1 ? 'btn-success' : 'btn-inverse'; if (this.colorPickerDrop) {
const rightButtonClass = this.props.series.yaxis === 2 ? 'btn-success' : 'btn-inverse'; this.destroyDrop();
}
return ( const { color, yaxis, onColorChange, onToggleAxis } = this.props;
<div className="p-b-1"> const dropContent = (
<label className="small p-r-1">Y Axis:</label> <SeriesColorPickerPopover color={color} yaxis={yaxis} onColorChange={onColorChange} onToggleAxis={onToggleAxis} />
<button onClick={this.onToggleAxis} className={'btn btn-small ' + leftButtonClass}>
Left
</button>
<button onClick={this.onToggleAxis} className={'btn btn-small ' + rightButtonClass}>
Right
</button>
</div>
); );
const dropContentElem = document.createElement('div');
ReactDOM.render(dropContent, dropContentElem);
const drop = new Drop({
target: this.pickerElem,
content: dropContentElem,
position: 'top center',
classes: 'drop-popover',
openOn: 'hover',
hoverCloseDelay: 200,
remove: true,
tetherOptions: {
constraints: [{ to: 'scrollParent', attachment: 'none both' }],
},
});
drop.on('close', this.closeColorPicker.bind(this));
this.colorPickerDrop = drop;
this.colorPickerDrop.open();
};
closeColorPicker() {
setTimeout(() => {
this.destroyDrop();
}, 100);
}
destroyDrop() {
if (this.colorPickerDrop && this.colorPickerDrop.tether) {
this.colorPickerDrop.destroy();
this.colorPickerDrop = null;
}
} }
render() { render() {
const { optionalClass, children } = this.props;
return ( return (
<div className="graph-legend-popover"> <div className={optionalClass} ref={e => (this.pickerElem = e)} onClick={this.onClickToOpen}>
{this.props.series.yaxis && this.renderAxisSelection()} {children}
<ColorPickerPopover color={this.props.series.color} onColorSelect={this.onColorChange} />
</div> </div>
); );
} }
} }
react2AngularDirective('seriesColorPicker', SeriesColorPicker, ['series', 'onColorChange', 'onToggleAxis']);
import React from 'react';
import { ColorPickerPopover } from './ColorPickerPopover';
import { react2AngularDirective } from 'app/core/utils/react2angular';
export interface SeriesColorPickerPopoverProps {
color: string;
yaxis?: number;
onColorChange: (color: string) => void;
onToggleAxis?: () => void;
}
export class SeriesColorPickerPopover extends React.PureComponent<SeriesColorPickerPopoverProps, any> {
render() {
return (
<div className="graph-legend-popover">
{this.props.yaxis && <AxisSelector yaxis={this.props.yaxis} onToggleAxis={this.props.onToggleAxis} />}
<ColorPickerPopover color={this.props.color} onColorSelect={this.props.onColorChange} />
</div>
);
}
}
interface AxisSelectorProps {
yaxis: number;
onToggleAxis: () => void;
}
interface AxisSelectorState {
yaxis: number;
}
export class AxisSelector extends React.PureComponent<AxisSelectorProps, AxisSelectorState> {
constructor(props) {
super(props);
this.state = {
yaxis: this.props.yaxis,
};
this.onToggleAxis = this.onToggleAxis.bind(this);
}
onToggleAxis() {
this.setState({
yaxis: this.state.yaxis === 2 ? 1 : 2,
});
this.props.onToggleAxis();
}
render() {
const leftButtonClass = this.state.yaxis === 1 ? 'btn-success' : 'btn-inverse';
const rightButtonClass = this.state.yaxis === 2 ? 'btn-success' : 'btn-inverse';
return (
<div className="p-b-1">
<label className="small p-r-1">Y Axis:</label>
<button onClick={this.onToggleAxis} className={'btn btn-small ' + leftButtonClass}>
Left
</button>
<button onClick={this.onToggleAxis} className={'btn btn-small ' + rightButtonClass}>
Right
</button>
</div>
);
}
}
react2AngularDirective('seriesColorPickerPopover', SeriesColorPickerPopover, [
'series',
'onColorChange',
'onToggleAxis',
]);
...@@ -14,7 +14,7 @@ import './components/jsontree/jsontree'; ...@@ -14,7 +14,7 @@ import './components/jsontree/jsontree';
import './components/code_editor/code_editor'; import './components/code_editor/code_editor';
import './utils/outline'; import './utils/outline';
import './components/colorpicker/ColorPicker'; import './components/colorpicker/ColorPicker';
import './components/colorpicker/SeriesColorPicker'; import './components/colorpicker/SeriesColorPickerPopover';
import './components/colorpicker/spectrum_picker'; import './components/colorpicker/spectrum_picker';
import './services/search_srv'; import './services/search_srv';
import './services/ng_react'; import './services/ng_react';
......
...@@ -133,6 +133,7 @@ export class PanelModel { ...@@ -133,6 +133,7 @@ export class PanelModel {
} }
destroy() { destroy() {
this.events.emit('panel-teardown');
this.events.removeAllListeners(); this.events.removeAllListeners();
} }
} }
...@@ -48,11 +48,6 @@ export class PanelCtrl { ...@@ -48,11 +48,6 @@ export class PanelCtrl {
} }
$scope.$on('component-did-mount', () => this.panelDidMount()); $scope.$on('component-did-mount', () => this.panelDidMount());
$scope.$on('$destroy', () => {
this.events.emit('panel-teardown');
this.events.removeAllListeners();
});
} }
panelDidMount() { panelDidMount() {
......
import _ from 'lodash';
import React, { PureComponent } from 'react';
import { TimeSeries } from 'app/core/core';
import CustomScrollbar from 'app/core/components/CustomScrollbar/CustomScrollbar';
import { LegendItem, LEGEND_STATS } from './LegendSeriesItem';
interface LegendProps {
seriesList: TimeSeries[];
optionalClass?: string;
}
interface LegendEventHandlers {
onToggleSeries?: (hiddenSeries) => void;
onToggleSort?: (sortBy, sortDesc) => void;
onToggleAxis?: (series: TimeSeries) => void;
onColorChange?: (series: TimeSeries, color: string) => void;
}
interface LegendComponentEventHandlers {
onToggleSeries?: (series, event) => void;
onToggleSort?: (sortBy, sortDesc) => 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?: 'min' | 'max' | 'avg' | 'current' | 'total';
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) {
super(props);
this.state = {
hiddenSeries: this.props.hiddenSeries,
};
}
sortLegend() {
let seriesList = [...this.props.seriesList] || [];
if (this.props.sort) {
seriesList = _.sortBy(seriesList, series => {
let sort = series.stats[this.props.sort];
if (sort === null) {
sort = -Infinity;
}
return sort;
});
if (this.props.sortDesc) {
seriesList = seriesList.reverse();
}
}
return seriesList;
}
onToggleSeries = (series, event) => {
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) {
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 => {
let sortDesc = this.props.sortDesc;
let sortBy = this.props.sort;
if (stat !== sortBy) {
sortDesc = null;
}
// if already sort ascending, disable sorting
if (sortDesc === false) {
sortBy = null;
sortDesc = null;
} 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 = { values, min, max, avg, current, total };
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.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: string;
onClick?: (statName: string) => void;
}
class LegendTableHeaderItem extends PureComponent<LegendTableHeaderProps & LegendSortProps> {
onClick = () => this.props.onClick(this.props.statName);
render() {
const { statName, sort, sortDesc } = this.props;
return (
<th className="pointer" onClick={this.onClick}>
{statName}
{sort === statName && <span className={sortDesc ? 'fa fa-caret-down' : 'fa fa-caret-up'} />}
</th>
);
}
}
export class Legend extends PureComponent<GraphLegendProps> {
render() {
return (
<CustomScrollbar>
<GraphLegend {...this.props} />
</CustomScrollbar>
);
}
}
export default Legend;
import React, { PureComponent } from 'react';
import classNames from 'classnames';
import { TimeSeries } from 'app/core/core';
import { SeriesColorPicker } from 'app/core/components/colorpicker/SeriesColorPicker';
export const LEGEND_STATS = ['min', 'max', 'avg', 'current', 'total'];
export interface LegendLabelProps {
series: TimeSeries;
asTable?: boolean;
hidden?: boolean;
onLabelClick?: (series, event) => void;
onColorChange?: (series, color: string) => void;
onToggleAxis?: (series) => void;
}
export interface LegendValuesProps {
values?: boolean;
min?: boolean;
max?: boolean;
avg?: boolean;
current?: boolean;
total?: boolean;
}
type LegendItemProps = LegendLabelProps & LegendValuesProps;
interface LegendItemState {
yaxis: number;
}
export class LegendItem extends PureComponent<LegendItemProps, LegendItemState> {
static defaultProps = {
asTable: false,
hidden: false,
onLabelClick: () => {},
onColorChange: () => {},
onToggleAxis: () => {},
};
constructor(props) {
super(props);
this.state = {
yaxis: this.props.series.yaxis,
};
}
onLabelClick = e => this.props.onLabelClick(this.props.series, e);
onToggleAxis = () => {
const yaxis = this.state.yaxis === 2 ? 1 : 2;
const info = { alias: this.props.series.alias, yaxis: yaxis };
this.setState({ yaxis: yaxis });
this.props.onToggleAxis(info);
};
onColorChange = color => {
this.props.onColorChange(this.props.series, color);
// Because of PureComponent nature it makes only shallow props comparison and changing of series.color doesn't run
// component re-render. In this case we can't rely on color, selected by user, because it may be overwritten
// by series overrides. So we need to use forceUpdate() to make sure we have proper series color.
this.forceUpdate();
};
renderLegendValues() {
const { series, asTable } = this.props;
const legendValueItems = [];
for (const valueName of LEGEND_STATS) {
if (this.props[valueName]) {
const valueFormatted = series.formatValue(series.stats[valueName]);
legendValueItems.push(
<LegendValue key={valueName} valueName={valueName} value={valueFormatted} asTable={asTable} />
);
}
}
return legendValueItems;
}
render() {
const { series, values, asTable, hidden } = this.props;
const seriesOptionClasses = classNames({
'graph-legend-series-hidden': hidden,
'graph-legend-series--right-y': series.yaxis === 2,
});
const valueItems = values ? this.renderLegendValues() : [];
const seriesLabel = (
<LegendSeriesLabel
label={series.alias}
color={series.color}
yaxis={this.state.yaxis}
onLabelClick={this.onLabelClick}
onColorChange={this.onColorChange}
onToggleAxis={this.onToggleAxis}
/>
);
if (asTable) {
return (
<tr className={`graph-legend-series ${seriesOptionClasses}`}>
<td>{seriesLabel}</td>
{valueItems}
</tr>
);
} else {
return (
<div className={`graph-legend-series ${seriesOptionClasses}`}>
{seriesLabel}
{valueItems}
</div>
);
}
}
}
interface LegendSeriesLabelProps {
label: string;
color: string;
yaxis?: number;
onLabelClick?: (event) => void;
}
class LegendSeriesLabel extends PureComponent<LegendSeriesLabelProps & LegendSeriesIconProps> {
static defaultProps = {
yaxis: undefined,
onLabelClick: () => {},
};
render() {
const { label, color, yaxis } = this.props;
const { onColorChange, onToggleAxis } = this.props;
return [
<LegendSeriesIcon
key="icon"
color={color}
yaxis={yaxis}
onColorChange={onColorChange}
onToggleAxis={onToggleAxis}
/>,
<a className="graph-legend-alias pointer" title={label} key="label" onClick={e => this.props.onLabelClick(e)}>
{label}
</a>,
];
}
}
interface LegendSeriesIconProps {
color: string;
yaxis?: number;
onColorChange?: (color: string) => void;
onToggleAxis?: () => void;
}
interface LegendSeriesIconState {
color: string;
}
function SeriesIcon(props) {
return <i className="fa fa-minus pointer" style={{ color: props.color }} />;
}
class LegendSeriesIcon extends PureComponent<LegendSeriesIconProps, LegendSeriesIconState> {
static defaultProps = {
yaxis: undefined,
onColorChange: () => {},
onToggleAxis: () => {},
};
render() {
return (
<SeriesColorPicker
optionalClass="graph-legend-icon"
yaxis={this.props.yaxis}
color={this.props.color}
onColorChange={this.props.onColorChange}
onToggleAxis={this.props.onToggleAxis}
>
<SeriesIcon color={this.props.color} />
</SeriesColorPicker>
);
}
}
interface LegendValueProps {
value: string;
valueName: string;
asTable?: boolean;
}
function LegendValue(props: LegendValueProps) {
const value = props.value;
const valueName = props.valueName;
if (props.asTable) {
return <td className={`graph-legend-value ${valueName}`}>{value}</td>;
}
return <div className={`graph-legend-value ${valueName}`}>{value}</div>;
}
...@@ -20,6 +20,9 @@ import { EventManager } from 'app/features/annotations/all'; ...@@ -20,6 +20,9 @@ import { EventManager } from 'app/features/annotations/all';
import { convertToHistogramData } from './histogram'; import { convertToHistogramData } from './histogram';
import { alignYLevel } from './align_yaxes'; import { alignYLevel } from './align_yaxes';
import config from 'app/core/config'; import config from 'app/core/config';
import React from 'react';
import ReactDOM from 'react-dom';
import { Legend, GraphLegendProps } from './Legend/Legend';
import { GraphCtrl } from './module'; import { GraphCtrl } from './module';
...@@ -35,6 +38,7 @@ class GraphElement { ...@@ -35,6 +38,7 @@ class GraphElement {
panelWidth: number; panelWidth: number;
eventManager: EventManager; eventManager: EventManager;
thresholdManager: ThresholdManager; thresholdManager: ThresholdManager;
legendElem: HTMLElement;
constructor(private scope, private elem, private timeSrv) { constructor(private scope, private elem, private timeSrv) {
this.ctrl = scope.ctrl; this.ctrl = scope.ctrl;
...@@ -50,7 +54,7 @@ class GraphElement { ...@@ -50,7 +54,7 @@ class GraphElement {
}); });
// panel events // panel events
this.ctrl.events.on('panel-teardown', this.onPanelteardown.bind(this)); this.ctrl.events.on('panel-teardown', this.onPanelTeardown.bind(this));
/** /**
* Split graph rendering into two parts. * Split graph rendering into two parts.
...@@ -63,13 +67,14 @@ class GraphElement { ...@@ -63,13 +67,14 @@ class GraphElement {
// global events // global events
appEvents.on('graph-hover', this.onGraphHover.bind(this), scope); appEvents.on('graph-hover', this.onGraphHover.bind(this), scope);
appEvents.on('graph-hover-clear', this.onGraphHoverClear.bind(this), scope); appEvents.on('graph-hover-clear', this.onGraphHoverClear.bind(this), scope);
this.elem.bind('plotselected', this.onPlotSelected.bind(this)); this.elem.bind('plotselected', this.onPlotSelected.bind(this));
this.elem.bind('plotclick', this.onPlotClick.bind(this)); this.elem.bind('plotclick', this.onPlotClick.bind(this));
scope.$on('$destroy', this.onScopeDestroy.bind(this));
// get graph legend element
if (this.elem && this.elem.parent) {
this.legendElem = this.elem.parent().find('.graph-legend')[0];
}
} }
onRender(renderData) { onRender(renderData) {
...@@ -82,7 +87,26 @@ class GraphElement { ...@@ -82,7 +87,26 @@ class GraphElement {
const graphHeight = this.elem.height(); const graphHeight = this.elem.height();
updateLegendValues(this.data, this.panel, graphHeight); updateLegendValues(this.data, this.panel, graphHeight);
this.ctrl.events.emit('render-legend'); const { values, min, max, avg, current, total } = this.panel.legend;
const { alignAsTable, rightSide, sideWidth, sort, sortDesc, hideEmpty, hideZero } = this.panel.legend;
const legendOptions = { alignAsTable, rightSide, sideWidth, sort, sortDesc, hideEmpty, hideZero };
const valueOptions = { values, min, max, avg, current, total };
const legendProps: GraphLegendProps = {
seriesList: this.data,
hiddenSeries: this.ctrl.hiddenSeries,
...legendOptions,
...valueOptions,
onToggleSeries: this.ctrl.onToggleSeries,
onToggleSort: this.ctrl.onToggleSort,
onColorChange: this.ctrl.onColorChange,
onToggleAxis: this.ctrl.onToggleAxis,
};
const legendReactElem = React.createElement(Legend, legendProps);
ReactDOM.render(legendReactElem, this.legendElem, () => this.onLegendRenderingComplete());
}
onLegendRenderingComplete() {
this.render_panel();
} }
onGraphHover(evt) { onGraphHover(evt) {
...@@ -99,17 +123,20 @@ class GraphElement { ...@@ -99,17 +123,20 @@ class GraphElement {
this.tooltip.show(evt.pos); this.tooltip.show(evt.pos);
} }
onPanelteardown() { onPanelTeardown() {
this.thresholdManager = null; this.thresholdManager = null;
if (this.plot) { if (this.plot) {
this.plot.destroy(); this.plot.destroy();
this.plot = null; this.plot = null;
} }
}
onLegendRenderingComplete() { this.tooltip.destroy();
this.render_panel(); this.elem.off();
this.elem.remove();
console.log('react unmount');
ReactDOM.unmountComponentAtNode(this.legendElem);
} }
onGraphHoverClear(event, info) { onGraphHoverClear(event, info) {
...@@ -157,12 +184,6 @@ class GraphElement { ...@@ -157,12 +184,6 @@ class GraphElement {
} }
} }
onScopeDestroy() {
this.tooltip.destroy();
this.elem.off();
this.elem.remove();
}
shouldAbortRender() { shouldAbortRender() {
if (!this.data) { if (!this.data) {
return true; return true;
......
import _ from 'lodash';
import $ from 'jquery';
import baron from 'baron';
import coreModule from 'app/core/core_module';
/** @ngInject */
function graphLegendDirective(popoverSrv, $timeout) {
return {
link: (scope, elem) => {
let firstRender = true;
const ctrl = scope.ctrl;
const panel = ctrl.panel;
let data;
let seriesList;
let i;
let legendScrollbar;
const legendRightDefaultWidth = 10;
const legendElem = elem.parent();
scope.$on('$destroy', () => {
destroyScrollbar();
});
ctrl.events.on('render-legend', () => {
data = ctrl.seriesList;
if (data) {
render();
}
ctrl.events.emit('legend-rendering-complete');
});
function getSeriesIndexForElement(el) {
return el.parents('[data-series-index]').data('series-index');
}
function openColorSelector(e) {
// if we clicked inside poup container ignore click
if ($(e.target).parents('.popover').length) {
return;
}
const el = $(e.currentTarget).find('.fa-minus');
const index = getSeriesIndexForElement(el);
const series = seriesList[index];
$timeout(() => {
popoverSrv.show({
element: el[0],
position: 'bottom left',
targetAttachment: 'top left',
template:
'<series-color-picker series="series" onToggleAxis="toggleAxis" onColorChange="colorSelected">' +
'</series-color-picker>',
openOn: 'hover',
model: {
series: series,
toggleAxis: () => {
ctrl.toggleAxis(series);
},
colorSelected: color => {
ctrl.changeSeriesColor(series, color);
},
},
});
});
}
function toggleSeries(e) {
const el = $(e.currentTarget);
const index = getSeriesIndexForElement(el);
const seriesInfo = seriesList[index];
const scrollPosition = legendScrollbar.scroller.scrollTop;
ctrl.toggleSeries(seriesInfo, e);
legendScrollbar.scroller.scrollTop = scrollPosition;
}
function sortLegend(e) {
const el = $(e.currentTarget);
const stat = el.data('stat');
if (stat !== panel.legend.sort) {
panel.legend.sortDesc = null;
}
// if already sort ascending, disable sorting
if (panel.legend.sortDesc === false) {
panel.legend.sort = null;
panel.legend.sortDesc = null;
ctrl.render();
return;
}
panel.legend.sortDesc = !panel.legend.sortDesc;
panel.legend.sort = stat;
ctrl.render();
}
function getTableHeaderHtml(statName) {
if (!panel.legend[statName]) {
return '';
}
let html = '<th class="pointer" data-stat="' + statName + '">' + statName;
if (panel.legend.sort === statName) {
const cssClass = panel.legend.sortDesc ? 'fa fa-caret-down' : 'fa fa-caret-up';
html += ' <span class="' + cssClass + '"></span>';
}
return html + '</th>';
}
function render() {
const legendWidth = legendElem.width();
if (!ctrl.panel.legend.show) {
elem.empty();
firstRender = true;
return;
}
if (firstRender) {
elem.on('click', '.graph-legend-icon', openColorSelector);
elem.on('click', '.graph-legend-alias', toggleSeries);
elem.on('click', 'th', sortLegend);
firstRender = false;
}
seriesList = data;
elem.empty();
// 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 = panel.legend.rightSide && panel.legend.sideWidth ? panel.legend.sideWidth + 'px' : '';
const ieWidth: any = panel.legend.rightSide && panel.legend.sideWidth ? panel.legend.sideWidth - 1 + 'px' : '';
legendElem.css('min-width', width);
legendElem.css('width', ieWidth);
elem.toggleClass('graph-legend-table', panel.legend.alignAsTable === true);
let tableHeaderElem;
if (panel.legend.alignAsTable) {
let header = '<tr>';
header += '<th colspan="2" style="text-align:left"></th>';
if (panel.legend.values) {
header += getTableHeaderHtml('min');
header += getTableHeaderHtml('max');
header += getTableHeaderHtml('avg');
header += getTableHeaderHtml('current');
header += getTableHeaderHtml('total');
}
header += '</tr>';
tableHeaderElem = $(header);
}
if (panel.legend.sort) {
seriesList = _.sortBy(seriesList, series => {
let sort = series.stats[panel.legend.sort];
if (sort === null) {
sort = -Infinity;
}
return sort;
});
if (panel.legend.sortDesc) {
seriesList = seriesList.reverse();
}
}
// render first time for getting proper legend height
if (!panel.legend.rightSide || (panel.legend.rightSide && legendWidth !== legendRightDefaultWidth)) {
renderLegendElement(tableHeaderElem);
elem.empty();
}
renderLegendElement(tableHeaderElem);
}
function renderSeriesLegendElements() {
const seriesElements = [];
for (i = 0; i < seriesList.length; i++) {
const series = seriesList[i];
if (series.hideFromLegend(panel.legend)) {
continue;
}
let html = '<div class="graph-legend-series';
if (series.yaxis === 2) {
html += ' graph-legend-series--right-y';
}
if (ctrl.hiddenSeries[series.alias]) {
html += ' graph-legend-series-hidden';
}
html += '" data-series-index="' + i + '">';
html += '<div class="graph-legend-icon">';
html += '<i class="fa fa-minus pointer" style="color:' + series.color + '"></i>';
html += '</div>';
html +=
'<a class="graph-legend-alias pointer" title="' + series.aliasEscaped + '">' + series.aliasEscaped + '</a>';
if (panel.legend.values) {
const avg = series.formatValue(series.stats.avg);
const current = series.formatValue(series.stats.current);
const min = series.formatValue(series.stats.min);
const max = series.formatValue(series.stats.max);
const total = series.formatValue(series.stats.total);
if (panel.legend.min) {
html += '<div class="graph-legend-value min">' + min + '</div>';
}
if (panel.legend.max) {
html += '<div class="graph-legend-value max">' + max + '</div>';
}
if (panel.legend.avg) {
html += '<div class="graph-legend-value avg">' + avg + '</div>';
}
if (panel.legend.current) {
html += '<div class="graph-legend-value current">' + current + '</div>';
}
if (panel.legend.total) {
html += '<div class="graph-legend-value total">' + total + '</div>';
}
}
html += '</div>';
seriesElements.push($(html));
}
return seriesElements;
}
function renderLegendElement(tableHeaderElem) {
const legendWidth = elem.width();
const seriesElements = renderSeriesLegendElements();
if (panel.legend.alignAsTable) {
const tbodyElem = $('<tbody></tbody>');
tbodyElem.append(tableHeaderElem);
tbodyElem.append(seriesElements);
elem.append(tbodyElem);
tbodyElem.wrap('<div class="graph-legend-scroll"></div>');
} else {
elem.append('<div class="graph-legend-scroll"></div>');
elem.find('.graph-legend-scroll').append(seriesElements);
}
if (!panel.legend.rightSide || (panel.legend.rightSide && legendWidth !== legendRightDefaultWidth)) {
addScrollbar();
} else {
destroyScrollbar();
}
}
function addScrollbar() {
const scrollRootClass = 'baron baron__root';
const scrollerClass = 'baron__scroller';
const scrollBarHTML = `
<div class="baron__track">
<div class="baron__bar"></div>
</div>
`;
const scrollRoot = elem;
const scroller = elem.find('.graph-legend-scroll');
// clear existing scroll bar track to prevent duplication
scrollRoot.find('.baron__track').remove();
scrollRoot.addClass(scrollRootClass);
$(scrollBarHTML).appendTo(scrollRoot);
scroller.addClass(scrollerClass);
const scrollbarParams = {
root: scrollRoot[0],
scroller: scroller[0],
bar: '.baron__bar',
track: '.baron__track',
barOnCls: '_scrollbar',
scrollingCls: '_scrolling',
};
if (!legendScrollbar) {
legendScrollbar = baron(scrollbarParams);
} else {
destroyScrollbar();
legendScrollbar = baron(scrollbarParams);
}
// #11830 - compensates for Firefox scrollbar calculation error in the baron framework
scroller[0].style.marginRight = '-' + (scroller[0].offsetWidth - scroller[0].clientWidth) + 'px';
legendScrollbar.scroll();
}
function destroyScrollbar() {
if (legendScrollbar) {
legendScrollbar.dispose();
legendScrollbar = undefined;
}
}
},
};
}
coreModule.directive('graphLegend', graphLegendDirective);
import './graph'; import './graph';
import './legend';
import './series_overrides_ctrl'; import './series_overrides_ctrl';
import './thresholds_form'; import './thresholds_form';
...@@ -244,67 +243,32 @@ class GraphCtrl extends MetricsPanelCtrl { ...@@ -244,67 +243,32 @@ class GraphCtrl extends MetricsPanelCtrl {
} }
} }
changeSeriesColor(series, color) { onColorChange = (series, color) => {
series.setColor(color); series.setColor(color);
this.panel.aliasColors[series.alias] = series.color; this.panel.aliasColors[series.alias] = series.color;
this.render(); this.render();
} };
toggleSeries(serie, event) { onToggleSeries = hiddenSeries => {
if (event.ctrlKey || event.metaKey || event.shiftKey) { this.hiddenSeries = hiddenSeries;
if (this.hiddenSeries[serie.alias]) {
delete this.hiddenSeries[serie.alias];
} else {
this.hiddenSeries[serie.alias] = true;
}
} else {
this.toggleSeriesExclusiveMode(serie);
}
this.render(); this.render();
} };
toggleSeriesExclusiveMode(serie) {
const hidden = this.hiddenSeries;
if (hidden[serie.alias]) {
delete hidden[serie.alias];
}
// check if every other series is hidden
const alreadyExclusive = _.every(this.seriesList, value => {
if (value.alias === serie.alias) {
return true;
}
return hidden[value.alias];
});
if (alreadyExclusive) {
// remove all hidden series
_.each(this.seriesList, value => {
delete this.hiddenSeries[value.alias];
});
} else {
// hide all but this serie
_.each(this.seriesList, value => {
if (value.alias === serie.alias) {
return;
}
this.hiddenSeries[value.alias] = true; onToggleSort = (sortBy, sortDesc) => {
}); this.panel.legend.sort = sortBy;
} this.panel.legend.sortDesc = sortDesc;
} this.render();
};
toggleAxis(info) { onToggleAxis = info => {
let override = _.find(this.panel.seriesOverrides, { alias: info.alias }); let override = _.find(this.panel.seriesOverrides, { alias: info.alias });
if (!override) { if (!override) {
override = { alias: info.alias }; override = { alias: info.alias };
this.panel.seriesOverrides.push(override); this.panel.seriesOverrides.push(override);
} }
info.yaxis = override.yaxis = info.yaxis === 2 ? 1 : 2; override.yaxis = info.yaxis;
this.render(); this.render();
} };
addSeriesOverride(override) { addSeriesOverride(override) {
this.panel.seriesOverrides.push(override || {}); this.panel.seriesOverrides.push(override || {});
......
...@@ -53,7 +53,7 @@ export function SeriesOverridesCtrl($scope, $element, popoverSrv) { ...@@ -53,7 +53,7 @@ export function SeriesOverridesCtrl($scope, $element, popoverSrv) {
element: $element.find('.dropdown')[0], element: $element.find('.dropdown')[0],
position: 'top center', position: 'top center',
openOn: 'click', openOn: 'click',
template: '<series-color-picker series="series" onColorChange="colorSelected" />', template: '<series-color-picker-popover series="series" onColorChange="colorSelected" />',
model: { model: {
autoClose: true, autoClose: true,
colorSelected: $scope.colorSelected, colorSelected: $scope.colorSelected,
......
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
.graph-legend-series { .graph-legend-series {
display: block; display: block;
padding-left: 0px; padding-left: 4px;
} }
.graph-legend-table .graph-legend-series { .graph-legend-table .graph-legend-series {
...@@ -52,9 +52,6 @@ ...@@ -52,9 +52,6 @@
padding-top: 6px; padding-top: 6px;
position: relative; position: relative;
// fix for Firefox (white stripe on the right of scrollbar)
width: calc(100% - 1px);
.popover-content { .popover-content {
padding: 0; padding: 0;
} }
...@@ -62,15 +59,6 @@ ...@@ -62,15 +59,6 @@
.graph-legend-content { .graph-legend-content {
position: relative; position: relative;
// fix for Firefox (white stripe on the right of scrollbar)
width: calc(100% - 1px);
}
.graph-legend-scroll {
position: relative;
overflow: auto !important;
padding: 1px;
} }
.graph-legend-icon { .graph-legend-icon {
...@@ -82,8 +70,8 @@ ...@@ -82,8 +70,8 @@
.graph-legend-icon, .graph-legend-icon,
.graph-legend-alias, .graph-legend-alias,
.graph-legend-value { .graph-legend-value {
display: inline;
cursor: pointer; cursor: pointer;
float: left;
white-space: nowrap; white-space: nowrap;
font-size: 85%; font-size: 85%;
text-align: left; text-align: left;
...@@ -120,6 +108,11 @@ ...@@ -120,6 +108,11 @@
} }
} }
// Don't move series to the right if legend is on the right as well
.graph-panel--legend-right .graph-legend-series--right-y {
float: left;
}
.graph-legend-value { .graph-legend-value {
padding-left: 6px; padding-left: 6px;
} }
...@@ -128,7 +121,8 @@ ...@@ -128,7 +121,8 @@
.body--phantomjs { .body--phantomjs {
.graph-panel--legend-right { .graph-panel--legend-right {
.graph-legend { .graph-legend {
display: inline-block; display: block;
max-width: min-content;
} }
.graph-panel__chart { .graph-panel__chart {
...@@ -138,24 +132,14 @@ ...@@ -138,24 +132,14 @@
.graph-legend-table { .graph-legend-table {
display: table; display: table;
width: auto; width: auto;
.graph-legend-scroll {
display: table;
}
} }
} }
} }
.graph-legend-table { .graph-legend-table {
tbody { padding-bottom: 1px;
display: block; padding-right: 5px;
position: relative; padding-left: 5px;
overflow-y: auto;
overflow-x: hidden;
padding-bottom: 1px;
padding-right: 5px;
padding-left: 5px;
}
.graph-legend-series { .graph-legend-series {
display: table-row; display: table-row;
......
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