diff --git a/public/app/core/components/colorpicker/SeriesColorPicker.tsx b/public/app/core/components/colorpicker/SeriesColorPicker.tsx index b514899e2e2..d6feaa31965 100644 --- a/public/app/core/components/colorpicker/SeriesColorPicker.tsx +++ b/public/app/core/components/colorpicker/SeriesColorPicker.tsx @@ -1,53 +1,84 @@ import React from 'react'; -import { ColorPickerPopover } from './ColorPickerPopover'; -import { react2AngularDirective } from 'app/core/utils/react2angular'; +import ReactDOM from 'react-dom'; +import Drop from 'tether-drop'; +import { SeriesColorPickerPopover } from './SeriesColorPickerPopover'; -export interface Props { - series: any; - onColorChange: (color: string) => void; - onToggleAxis: () => void; +export interface SeriesColorPickerProps { + color: string; + yaxis?: number; + optionalClass?: string; + onColorChange: (newColor: string) => void; + onToggleAxis?: () => void; } -export class SeriesColorPicker extends React.Component { +export class SeriesColorPicker extends React.Component { + pickerElem: any; + colorPickerDrop: any; + + static defaultProps = { + optionalClass: '', + yaxis: undefined, + onToggleAxis: () => {}, + }; + constructor(props) { super(props); - this.onColorChange = this.onColorChange.bind(this); - this.onToggleAxis = this.onToggleAxis.bind(this); } - onColorChange(color) { - this.props.onColorChange(color); + componentWillUnmount() { + this.destroyDrop(); } - onToggleAxis() { - this.props.onToggleAxis(); - } + onClickToOpen = () => { + if (this.colorPickerDrop) { + this.destroyDrop(); + } - renderAxisSelection() { - const leftButtonClass = this.props.series.yaxis === 1 ? 'btn-success' : 'btn-inverse'; - const rightButtonClass = this.props.series.yaxis === 2 ? 'btn-success' : 'btn-inverse'; - - return ( -
- - - -
+ const { color, yaxis, onColorChange, onToggleAxis } = this.props; + const dropContent = ( + ); + 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() { + const { optionalClass, children } = this.props; return ( -
- {this.props.series.yaxis && this.renderAxisSelection()} - +
(this.pickerElem = e)} onClick={this.onClickToOpen}> + {children}
); } } - -react2AngularDirective('seriesColorPicker', SeriesColorPicker, ['series', 'onColorChange', 'onToggleAxis']); diff --git a/public/app/core/components/colorpicker/SeriesColorPickerPopover.tsx b/public/app/core/components/colorpicker/SeriesColorPickerPopover.tsx new file mode 100644 index 00000000000..085d554300d --- /dev/null +++ b/public/app/core/components/colorpicker/SeriesColorPickerPopover.tsx @@ -0,0 +1,70 @@ +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 { + render() { + return ( +
+ {this.props.yaxis && } + +
+ ); + } +} + +interface AxisSelectorProps { + yaxis: number; + onToggleAxis: () => void; +} + +interface AxisSelectorState { + yaxis: number; +} + +export class AxisSelector extends React.PureComponent { + 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 ( +
+ + + +
+ ); + } +} + +react2AngularDirective('seriesColorPickerPopover', SeriesColorPickerPopover, [ + 'series', + 'onColorChange', + 'onToggleAxis', +]); diff --git a/public/app/core/core.ts b/public/app/core/core.ts index 18a625d3307..257a2077c97 100644 --- a/public/app/core/core.ts +++ b/public/app/core/core.ts @@ -14,7 +14,7 @@ import './components/jsontree/jsontree'; import './components/code_editor/code_editor'; import './utils/outline'; import './components/colorpicker/ColorPicker'; -import './components/colorpicker/SeriesColorPicker'; +import './components/colorpicker/SeriesColorPickerPopover'; import './components/colorpicker/spectrum_picker'; import './services/search_srv'; import './services/ng_react'; diff --git a/public/app/features/dashboard/panel_model.ts b/public/app/features/dashboard/panel_model.ts index ebf8a6bb224..d82368d8dd7 100644 --- a/public/app/features/dashboard/panel_model.ts +++ b/public/app/features/dashboard/panel_model.ts @@ -133,6 +133,7 @@ export class PanelModel { } destroy() { + this.events.emit('panel-teardown'); this.events.removeAllListeners(); } } diff --git a/public/app/features/panel/panel_ctrl.ts b/public/app/features/panel/panel_ctrl.ts index 5e216f6b34d..08605132e82 100644 --- a/public/app/features/panel/panel_ctrl.ts +++ b/public/app/features/panel/panel_ctrl.ts @@ -48,11 +48,6 @@ export class PanelCtrl { } $scope.$on('component-did-mount', () => this.panelDidMount()); - - $scope.$on('$destroy', () => { - this.events.emit('panel-teardown'); - this.events.removeAllListeners(); - }); } panelDidMount() { diff --git a/public/app/plugins/panel/graph/Legend/Legend.tsx b/public/app/plugins/panel/graph/Legend/Legend.tsx new file mode 100644 index 00000000000..7af61fde4e9 --- /dev/null +++ b/public/app/plugins/panel/graph/Legend/Legend.tsx @@ -0,0 +1,321 @@ +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 { + static defaultProps: Partial = { + 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 ( +
+ {this.props.alignAsTable ? : } +
+ ); + } +} + +class LegendSeriesList extends PureComponent { + 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) => ( +