Просмотр исходного кода

Unified colorpicker (#9347)

* colorpicker: initial picker with predefined palette and spectrum

* colorpicker: highlight selected color

* colorpicker: add onChange() callback

* colorpicker: replace singlestat picker by new one

* colorpicker: style tweak

* colorpicker: parse color on input blur

* colorpicker: sort palette by hue and lightness

* colorpicker: refactor, move colors sorting to 'app/core/utils/colors'

* tech: colorpicker - fix linter errors

* colorpicker: convert to React components

* colorpicker: fix spectrum import after moving to webpack

* colorpicker: minor refactor

* colorpicker: initial series color picker

* colorpicker: fix tests error
Alexander Zobnin 8 лет назад
Родитель
Сommit
2aae2556a5

+ 45 - 0
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<IProps, any> {
+  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 (
+        <i key={paletteColor} className={"pointer fa " + cssClass}
+          style={{'color': paletteColor}}
+          onClick={this.onColorSelect(paletteColor)}>&nbsp;
+        </i>
+      );
+    });
+    return (
+      <div className="graph-legend-popover">
+        <p className="m-b-0">{colorPaletteItems}</p>
+      </div>
+    );
+  }
+}
+
+coreModule.directive('gfColorPalette', function (reactDirective) {
+  return reactDirective(GfColorPalette, ['color', 'onColorSelect']);
+});

+ 81 - 0
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<IProps, any> {
+  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 = (
+      <ColorPickerPopover color={this.props.color} onColorSelect={this.onColorSelect} />
+    );
+
+    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 (
+      <div className="sp-replacer sp-light" onClick={this.openColorPicker} ref={this.setPickerElem}>
+        <div className="sp-preview">
+          <div className="sp-preview-inner" style={{backgroundColor: this.props.color}}>
+          </div>
+        </div>
+      </div>
+    );
+  }
+}
+
+coreModule.directive('colorPicker', function (reactDirective) {
+  return reactDirective(ColorPicker, ['color', 'onChange']);
+});

+ 127 - 0
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<IProps, any> {
+  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 = (
+      <div id="palette">
+        <GfColorPalette color={this.state.color} onColorSelect={this.sampleColorSelected} />
+      </div>
+    );
+    const spectrumTab = (
+      <div id="spectrum">
+        <GfSpectrumPicker color={this.props.color} onColorSelect={this.spectrumColorSelected} options={{}} />
+      </div>
+    );
+    const currentTab = this.state.tab === 'palette' ? paletteTab : spectrumTab;
+
+    return (
+      <div className="gf-color-picker">
+        <ul className="nav nav-tabs" id="colorpickernav" ref={this.setPickerNavElem}>
+          <li className="gf-tabs-item-colorpicker">
+            <a href="#palette" data-toggle="tab">Colors</a>
+          </li>
+          <li className="gf-tabs-item-colorpicker">
+            <a href="#spectrum" data-toggle="tab">Spectrum</a>
+          </li>
+        </ul>
+        <div className="colorpicker-container">
+          {currentTab}
+        </div>
+        <div className="color-model-container">
+          <input className="gf-form-input" value={this.state.colorString}
+            onChange={this.onColorStringChange} onBlur={this.onColorStringBlur}>
+          </input>
+        </div>
+      </div>
+    );
+  }
+}
+
+coreModule.directive('gfColorPickerPopover', function (reactDirective) {
+  return reactDirective(ColorPickerPopover, ['color', 'onColorSelect']);
+});

+ 49 - 0
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<IProps, any> {
+  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 (
+      <div className="graph-legend-popover">
+        <div className="p-b-1">
+          <label>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>
+        <ColorPickerPopover color={this.props.series.color} onColorSelect={this.onColorChange} />
+      </div>
+    );
+  }
+}
+
+coreModule.directive('seriesColorPicker', function (reactDirective) {
+  return reactDirective(SeriesColorPicker, ['series', 'onColorChange', 'onToggleAxis']);
+});

+ 76 - 0
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<IProps, any> {
+  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 (
+      <div className="spectrum-container" ref={this.setComponentElem}></div>
+    );
+  }
+}
+
+coreModule.directive('gfSpectrumPicker', function (reactDirective) {
+  return reactDirective(GfSpectrumPicker, ['color', 'options', 'onColorSelect']);
+});

+ 2 - 0
public/app/core/core.ts

@@ -16,6 +16,8 @@ import './partials';
 import './components/jsontree/jsontree';
 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/SeriesColorPicker';
 
 
 import {grafanaAppDirective} from './components/grafana_app';
 import {grafanaAppDirective} from './components/grafana_app';
 import {sideMenuDirective} from './components/sidemenu/sidemenu';
 import {sideMenuDirective} from './components/sidemenu/sidemenu';

+ 30 - 1
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",
   "#7EB26D","#EAB839","#6ED0E0","#EF843C","#E24D42","#1F78C1","#BA43A9","#705DA0",
   "#508642","#CCA300","#447EBC","#C15C17","#890F02","#0A437C","#6D1F62","#584477",
   "#508642","#CCA300","#447EBC","#C15C17","#890F02","#0A437C","#6D1F62","#584477",
   "#B7DBAB","#F4D598","#70DBED","#F9BA8F","#F29191","#82B5D8","#E5A8E2","#AEA2E0",
   "#B7DBAB","#F4D598","#70DBED","#F9BA8F","#F29191","#82B5D8","#E5A8E2","#AEA2E0",
@@ -10,3 +16,26 @@ export default [
   "#E0F9D7","#FCEACA","#CFFAFF","#F9E2D2","#FCE2DE","#BADFF4","#F9D9F9","#DEDAF7"
   "#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;

+ 2 - 1
public/app/plugins/panel/graph/legend.js

@@ -45,7 +45,8 @@ function (angular, _, $) {
             popoverSrv.show({
             popoverSrv.show({
               element: el[0],
               element: el[0],
               position: 'bottom center',
               position: 'bottom center',
-              template: '<gf-color-picker></gf-color-picker>',
+              template: '<series-color-picker series="series" onToggleAxis="toggleAxis" onColorChange="colorSelected">' +
+                '</series-color-picker>',
               openOn: 'hover',
               openOn: 'hover',
               model: {
               model: {
                 series: series,
                 series: series,

+ 3 - 3
public/app/plugins/panel/singlestat/editor.html

@@ -69,13 +69,13 @@
     <div class="gf-form">
     <div class="gf-form">
       <label class="gf-form-label width-8">Colors</label>
       <label class="gf-form-label width-8">Colors</label>
       <span class="gf-form-label">
       <span class="gf-form-label">
-        <spectrum-picker ng-model="ctrl.panel.colors[0]" ng-change="ctrl.render()" ></spectrum-picker>
+        <color-picker color="ctrl.panel.colors[0]" onChange="ctrl.onColorChange(0)"></color-picker>
       </span>
       </span>
       <span class="gf-form-label">
       <span class="gf-form-label">
-        <spectrum-picker ng-model="ctrl.panel.colors[1]" ng-change="ctrl.render()" ></spectrum-picker>
+        <color-picker color="ctrl.panel.colors[1]" onChange="ctrl.onColorChange(1)"></color-picker>
       </span>
       </span>
       <span class="gf-form-label">
       <span class="gf-form-label">
-        <spectrum-picker ng-model="ctrl.panel.colors[2]" ng-change="ctrl.render()" ></spectrum-picker>
+        <color-picker color="ctrl.panel.colors[2]" onChange="ctrl.onColorChange(2)"></color-picker>
       </span>
       </span>
       <span class="gf-form-label">
       <span class="gf-form-label">
         <a  ng-click="ctrl.invertColorOrder()">
         <a  ng-click="ctrl.invertColorOrder()">

+ 7 - 0
public/app/plugins/panel/singlestat/module.ts

@@ -214,6 +214,13 @@ class SingleStatCtrl extends MetricsPanelCtrl {
     this.render();
     this.render();
   }
   }
 
 
+  onColorChange(panelColorIndex) {
+    return (color) => {
+      this.panel.colors[panelColorIndex] = color;
+      this.render();
+    };
+  }
+
   getDecimalsForValue(value) {
   getDecimalsForValue(value) {
     if (_.isNumber(this.panel.decimals)) {
     if (_.isNumber(this.panel.decimals)) {
       return {decimals: this.panel.decimals, scaledDecimals: null};
       return {decimals: this.panel.decimals, scaledDecimals: null};

+ 10 - 0
public/sass/components/_color_picker.scss

@@ -35,3 +35,13 @@
   float: left;
   float: left;
   z-index: 0;
   z-index: 0;
 }
 }
+
+.colorpicker-container {
+  min-height: 190px;
+}
+
+.drop-popover.gf-color-picker {
+  .drop-content {
+    width: 210px;
+  }
+}