mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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
This commit is contained in:
parent
7859d4ffca
commit
2aae2556a5
45
public/app/core/components/colorpicker/ColorPalette.tsx
Normal file
45
public/app/core/components/colorpicker/ColorPalette.tsx
Normal file
@ -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)}>
|
||||
</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
public/app/core/components/colorpicker/ColorPicker.tsx
Normal file
81
public/app/core/components/colorpicker/ColorPicker.tsx
Normal file
@ -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
public/app/core/components/colorpicker/ColorPickerPopover.tsx
Normal file
127
public/app/core/components/colorpicker/ColorPickerPopover.tsx
Normal file
@ -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
public/app/core/components/colorpicker/SeriesColorPicker.tsx
Normal file
49
public/app/core/components/colorpicker/SeriesColorPicker.tsx
Normal file
@ -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
public/app/core/components/colorpicker/SpectrumPicker.tsx
Normal file
76
public/app/core/components/colorpicker/SpectrumPicker.tsx
Normal file
@ -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']);
|
||||
});
|
@ -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';
|
||||
|
@ -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;
|
||||
|
@ -45,7 +45,8 @@ function (angular, _, $) {
|
||||
popoverSrv.show({
|
||||
element: el[0],
|
||||
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',
|
||||
model: {
|
||||
series: series,
|
||||
|
@ -69,13 +69,13 @@
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-8">Colors</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 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 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 class="gf-form-label">
|
||||
<a ng-click="ctrl.invertColorOrder()">
|
||||
|
@ -214,6 +214,13 @@ class SingleStatCtrl extends MetricsPanelCtrl {
|
||||
this.render();
|
||||
}
|
||||
|
||||
onColorChange(panelColorIndex) {
|
||||
return (color) => {
|
||||
this.panel.colors[panelColorIndex] = color;
|
||||
this.render();
|
||||
};
|
||||
}
|
||||
|
||||
getDecimalsForValue(value) {
|
||||
if (_.isNumber(this.panel.decimals)) {
|
||||
return {decimals: this.panel.decimals, scaledDecimals: null};
|
||||
|
@ -35,3 +35,13 @@
|
||||
float: left;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.colorpicker-container {
|
||||
min-height: 190px;
|
||||
}
|
||||
|
||||
.drop-popover.gf-color-picker {
|
||||
.drop-content {
|
||||
width: 210px;
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user