diff --git a/public/app/core/components/colorpicker/ColorPalette.tsx b/public/app/core/components/colorpicker/ColorPalette.tsx new file mode 100644 index 00000000000..47e0e244e0a --- /dev/null +++ b/public/app/core/components/colorpicker/ColorPalette.tsx @@ -0,0 +1,45 @@ +import React from 'react'; +import coreModule from 'app/core/core_module'; +import { sortedColors } from 'app/core/utils/colors'; + +export interface IProps { + color: string; + onColorSelect: (c: string) => void; +} + +export class GfColorPalette extends React.Component { + paletteColors: string[]; + + constructor(props) { + super(props); + this.paletteColors = sortedColors; + this.onColorSelect = this.onColorSelect.bind(this); + } + + onColorSelect(color) { + return () => { + this.props.onColorSelect(color); + }; + } + + render() { + const colorPaletteItems = this.paletteColors.map((paletteColor) => { + const cssClass = paletteColor.toLowerCase() === this.props.color.toLowerCase() ? 'fa-circle-o' : 'fa-circle'; + return ( +   + + ); + }); + return ( +
+

{colorPaletteItems}

+
+ ); + } +} + +coreModule.directive('gfColorPalette', function (reactDirective) { + return reactDirective(GfColorPalette, ['color', 'onColorSelect']); +}); diff --git a/public/app/core/components/colorpicker/ColorPicker.tsx b/public/app/core/components/colorpicker/ColorPicker.tsx new file mode 100644 index 00000000000..8ef51ce0be7 --- /dev/null +++ b/public/app/core/components/colorpicker/ColorPicker.tsx @@ -0,0 +1,81 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import $ from 'jquery'; +import Drop from 'tether-drop'; +import coreModule from 'app/core/core_module'; +import { ColorPickerPopover } from './ColorPickerPopover'; + +export interface IProps { + color: string; + onChange: (c: string) => void; +} + +export class ColorPicker extends React.Component { + pickerElem: any; + colorPickerDrop: any; + + constructor(props) { + super(props); + this.openColorPicker = this.openColorPicker.bind(this); + this.closeColorPicker = this.closeColorPicker.bind(this); + this.setPickerElem = this.setPickerElem.bind(this); + this.onColorSelect = this.onColorSelect.bind(this); + } + + setPickerElem(elem) { + this.pickerElem = $(elem); + } + + openColorPicker() { + const dropContent = ( + + ); + + let dropContentElem = document.createElement('div'); + ReactDOM.render(dropContent, dropContentElem); + + let drop = new Drop({ + target: this.pickerElem[0], + content: dropContentElem, + position: 'top center', + classes: 'drop-popover drop-popover--form', + openOn: 'hover', + hoverCloseDelay: 200, + tetherOptions: { + constraints: [{ to: 'scrollParent', attachment: "none both" }] + } + }); + + drop.on('close', this.closeColorPicker); + + this.colorPickerDrop = drop; + this.colorPickerDrop.open(); + } + + closeColorPicker() { + setTimeout(() => { + if (this.colorPickerDrop && this.colorPickerDrop.tether) { + this.colorPickerDrop.destroy(); + } + }, 100); + } + + onColorSelect(color) { + this.props.onChange(color); + } + + render() { + return ( +
+
+
+
+
+
+ ); + } +} + +coreModule.directive('colorPicker', function (reactDirective) { + return reactDirective(ColorPicker, ['color', 'onChange']); +}); diff --git a/public/app/core/components/colorpicker/ColorPickerPopover.tsx b/public/app/core/components/colorpicker/ColorPickerPopover.tsx new file mode 100644 index 00000000000..eaec3273507 --- /dev/null +++ b/public/app/core/components/colorpicker/ColorPickerPopover.tsx @@ -0,0 +1,127 @@ +import React from 'react'; +import $ from 'jquery'; +import coreModule from 'app/core/core_module'; +import { GfColorPalette } from './ColorPalette'; +import { GfSpectrumPicker } from './SpectrumPicker'; + +// Spectrum picker uses TinyColor and loads it as a global variable, so we can use it here also +declare var tinycolor; + +export interface IProps { + color: string; + onColorSelect: (c: string) => void; +} + +export class ColorPickerPopover extends React.Component { + pickerNavElem: any; + + constructor(props) { + super(props); + this.state = { + tab: 'palette', + color: this.props.color, + colorString: this.props.color + }; + + this.onColorStringChange = this.onColorStringChange.bind(this); + this.onColorStringBlur = this.onColorStringBlur.bind(this); + this.sampleColorSelected = this.sampleColorSelected.bind(this); + this.spectrumColorSelected = this.spectrumColorSelected.bind(this); + this.setPickerNavElem = this.setPickerNavElem.bind(this); + } + + setPickerNavElem(elem) { + this.pickerNavElem = $(elem); + } + + setColor(color) { + let newColor = tinycolor(color); + if (newColor.isValid()) { + this.setState({ + color: newColor.toString(), + colorString: newColor.toString() + }); + this.props.onColorSelect(color); + } + } + + sampleColorSelected(color) { + this.setColor(color); + } + + spectrumColorSelected(color) { + let rgbColor = color.toRgbString(); + this.setColor(rgbColor); + } + + onColorStringChange(e) { + let colorString = e.target.value; + this.setState({ + colorString: colorString + }); + + let newColor = tinycolor(colorString); + if (newColor.isValid()) { + // Update only color state + this.setState({ + color: newColor.toString(), + }); + this.props.onColorSelect(newColor); + } + } + + onColorStringBlur(e) { + let colorString = e.target.value; + this.setColor(colorString); + } + + componentDidMount() { + this.pickerNavElem.find('li:first').addClass('active'); + this.pickerNavElem.on('show', (e) => { + // use href attr (#name => name) + let tab = e.target.hash.slice(1); + this.setState({ + tab: tab + }); + }); + } + + render() { + const paletteTab = ( +
+ +
+ ); + const spectrumTab = ( +
+ +
+ ); + const currentTab = this.state.tab === 'palette' ? paletteTab : spectrumTab; + + return ( +
+ +
+ {currentTab} +
+
+ + +
+
+ ); + } +} + +coreModule.directive('gfColorPickerPopover', function (reactDirective) { + return reactDirective(ColorPickerPopover, ['color', 'onColorSelect']); +}); diff --git a/public/app/core/components/colorpicker/SeriesColorPicker.tsx b/public/app/core/components/colorpicker/SeriesColorPicker.tsx new file mode 100644 index 00000000000..6ea8b1710b3 --- /dev/null +++ b/public/app/core/components/colorpicker/SeriesColorPicker.tsx @@ -0,0 +1,49 @@ +import React from 'react'; +import coreModule from 'app/core/core_module'; +import { ColorPickerPopover } from './ColorPickerPopover'; + +export interface IProps { + series: any; + onColorChange: (color: string) => void; + onToggleAxis: () => void; +} + +export class SeriesColorPicker extends React.Component { + constructor(props) { + super(props); + this.onColorChange = this.onColorChange.bind(this); + this.onToggleAxis = this.onToggleAxis.bind(this); + } + + onColorChange(color) { + this.props.onColorChange(color); + } + + onToggleAxis() { + this.props.onToggleAxis(); + } + + render() { + const leftButtonClass = this.props.series.yaxis === 1 ? 'btn-success' : 'btn-inverse'; + const rightButtonClass = this.props.series.yaxis === 2 ? 'btn-success' : 'btn-inverse'; + + return ( +
+
+ + + +
+ +
+ ); + } +} + +coreModule.directive('seriesColorPicker', function (reactDirective) { + return reactDirective(SeriesColorPicker, ['series', 'onColorChange', 'onToggleAxis']); +}); diff --git a/public/app/core/components/colorpicker/SpectrumPicker.tsx b/public/app/core/components/colorpicker/SpectrumPicker.tsx new file mode 100644 index 00000000000..2ea0fc65ddf --- /dev/null +++ b/public/app/core/components/colorpicker/SpectrumPicker.tsx @@ -0,0 +1,76 @@ +import React from 'react'; +import coreModule from 'app/core/core_module'; +import _ from 'lodash'; +import $ from 'jquery'; +import 'vendor/spectrum'; + +export interface IProps { + color: string; + options: object; + onColorSelect: (c: string) => void; +} + +export class GfSpectrumPicker extends React.Component { + elem: any; + isMoving: boolean; + + constructor(props) { + super(props); + this.onSpectrumMove = this.onSpectrumMove.bind(this); + this.setComponentElem = this.setComponentElem.bind(this); + } + + setComponentElem(elem) { + this.elem = $(elem); + } + + onSpectrumMove(color) { + this.isMoving = true; + this.props.onColorSelect(color); + } + + componentDidMount() { + let spectrumOptions = _.assignIn({ + flat: true, + showAlpha: true, + showButtons: false, + color: this.props.color, + appendTo: this.elem, + move: this.onSpectrumMove, + }, this.props.options); + + this.elem.spectrum(spectrumOptions); + this.elem.spectrum('show'); + this.elem.spectrum('set', this.props.color); + } + + componentWillUpdate(nextProps) { + // If user move pointer over spectrum field this produce 'move' event and component + // may update props.color. We don't want to update spectrum color in this case, so we can use + // isMoving flag for tracking moving state. Flag should be cleared in componentDidUpdate() which + // is called after updating occurs (when user finished moving). + if (!this.isMoving) { + this.elem.spectrum('set', nextProps.color); + } + } + + componentDidUpdate() { + if (this.isMoving) { + this.isMoving = false; + } + } + + componentWillUnmount() { + this.elem.spectrum('destroy'); + } + + render() { + return ( +
+ ); + } +} + +coreModule.directive('gfSpectrumPicker', function (reactDirective) { + return reactDirective(GfSpectrumPicker, ['color', 'options', 'onColorSelect']); +}); diff --git a/public/app/core/core.ts b/public/app/core/core.ts index 675b9eba2fb..9ac3fd8c2a2 100644 --- a/public/app/core/core.ts +++ b/public/app/core/core.ts @@ -16,6 +16,8 @@ import './partials'; import './components/jsontree/jsontree'; import './components/code_editor/code_editor'; import './utils/outline'; +import './components/colorpicker/ColorPicker'; +import './components/colorpicker/SeriesColorPicker'; import {grafanaAppDirective} from './components/grafana_app'; import {sideMenuDirective} from './components/sidemenu/sidemenu'; diff --git a/public/app/core/utils/colors.ts b/public/app/core/utils/colors.ts index bd774ea02ea..b9dd164d635 100644 --- a/public/app/core/utils/colors.ts +++ b/public/app/core/utils/colors.ts @@ -1,6 +1,12 @@ +import _ from 'lodash'; +// Spectrum picker uses TinyColor and loads it as a global variable, so we can use it here also +declare var tinycolor; -export default [ +export const PALETTE_ROWS = 4; +export const PALETTE_COLUMNS = 14; + +let colors = [ "#7EB26D","#EAB839","#6ED0E0","#EF843C","#E24D42","#1F78C1","#BA43A9","#705DA0", "#508642","#CCA300","#447EBC","#C15C17","#890F02","#0A437C","#6D1F62","#584477", "#B7DBAB","#F4D598","#70DBED","#F9BA8F","#F29191","#82B5D8","#E5A8E2","#AEA2E0", @@ -10,3 +16,26 @@ export default [ "#E0F9D7","#FCEACA","#CFFAFF","#F9E2D2","#FCE2DE","#BADFF4","#F9D9F9","#DEDAF7" ]; +export function sortColorsByHue(hexColors) { + let hslColors = _.map(hexColors, hexToHsl); + + let sortedHSLColors = _.sortBy(hslColors, ['h']); + sortedHSLColors = _.chunk(sortedHSLColors, PALETTE_ROWS); + sortedHSLColors = _.map(sortedHSLColors, chunk => { + return _.sortBy(chunk, 'l'); + }); + sortedHSLColors = _.flattenDeep(_.zip(...sortedHSLColors)); + + return _.map(sortedHSLColors, hslToHex); +} + +export function hexToHsl(color) { + return tinycolor(color).toHsl(); +} + +export function hslToHex(color) { + return tinycolor(color).toHexString(); +} + +export let sortedColors = sortColorsByHue(colors); +export default colors; diff --git a/public/app/plugins/panel/graph/legend.js b/public/app/plugins/panel/graph/legend.js index beee24543d0..b8c2970b8cd 100644 --- a/public/app/plugins/panel/graph/legend.js +++ b/public/app/plugins/panel/graph/legend.js @@ -45,7 +45,8 @@ function (angular, _, $) { popoverSrv.show({ element: el[0], position: 'bottom center', - template: '', + template: '' + + '', openOn: 'hover', model: { series: series, diff --git a/public/app/plugins/panel/singlestat/editor.html b/public/app/plugins/panel/singlestat/editor.html index 6ec8705dbe3..1981b4357f8 100644 --- a/public/app/plugins/panel/singlestat/editor.html +++ b/public/app/plugins/panel/singlestat/editor.html @@ -69,13 +69,13 @@