mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge branch 'master' into kbn-formats-refactor
This commit is contained in:
@@ -23,7 +23,10 @@
|
||||
"react-highlight-words": "0.11.0",
|
||||
"react-popper": "^1.3.0",
|
||||
"react-transition-group": "^2.2.1",
|
||||
"react-virtualized": "^9.21.0"
|
||||
"react-virtualized": "^9.21.0",
|
||||
"tether": "^1.4.0",
|
||||
"tether-drop": "https://github.com/torkelo/drop/tarball/master",
|
||||
"tinycolor2": "^1.4.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/classnames": "^2.2.6",
|
||||
@@ -33,6 +36,8 @@
|
||||
"@types/react": "^16.7.6",
|
||||
"@types/react-custom-scrollbars": "^4.0.5",
|
||||
"@types/react-test-renderer": "^16.0.3",
|
||||
"@types/tether-drop": "^1.4.8",
|
||||
"@types/tinycolor2": "^1.4.1",
|
||||
"react-test-renderer": "^16.7.0",
|
||||
"typescript": "^3.2.2"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import React from 'react';
|
||||
import renderer from 'react-test-renderer';
|
||||
import { ColorPalette } from './ColorPalette';
|
||||
|
||||
describe('CollorPalette', () => {
|
||||
it('renders correctly', () => {
|
||||
const tree = renderer.create(<ColorPalette color="#EAB839" onColorSelect={jest.fn()} />).toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
import React from 'react';
|
||||
import { sortedColors } from '../../utils';
|
||||
|
||||
export interface Props {
|
||||
color: string;
|
||||
onColorSelect: (c: string) => void;
|
||||
}
|
||||
|
||||
export class ColorPalette extends React.Component<Props, any> {
|
||||
paletteColors: string[];
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.paletteColors = sortedColors;
|
||||
this.onColorSelect = this.onColorSelect.bind(this);
|
||||
}
|
||||
|
||||
onColorSelect(color: string) {
|
||||
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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import Drop from 'tether-drop';
|
||||
import { ColorPickerPopover } from './ColorPickerPopover';
|
||||
|
||||
export interface Props {
|
||||
color: string;
|
||||
onChange: (c: string) => void;
|
||||
}
|
||||
|
||||
export class ColorPicker extends React.Component<Props, any> {
|
||||
pickerElem: HTMLElement | null;
|
||||
colorPickerDrop: any;
|
||||
|
||||
openColorPicker = () => {
|
||||
const dropContent = <ColorPickerPopover color={this.props.color} onColorSelect={this.onColorSelect} />;
|
||||
|
||||
const dropContentElem = document.createElement('div');
|
||||
ReactDOM.render(dropContent, dropContentElem);
|
||||
|
||||
const drop = new Drop({
|
||||
target: this.pickerElem as Element,
|
||||
content: dropContentElem,
|
||||
position: 'top center',
|
||||
classes: 'drop-popover',
|
||||
openOn: 'click',
|
||||
hoverCloseDelay: 200,
|
||||
tetherOptions: {
|
||||
constraints: [{ to: 'scrollParent', attachment: 'none both' }],
|
||||
attachment: 'bottom center',
|
||||
},
|
||||
});
|
||||
|
||||
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: string) => {
|
||||
this.props.onChange(color);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="sp-replacer sp-light" onClick={this.openColorPicker} ref={element => (this.pickerElem = element)}>
|
||||
<div className="sp-preview">
|
||||
<div className="sp-preview-inner" style={{ backgroundColor: this.props.color }} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
import React from 'react';
|
||||
import $ from 'jquery';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import { ColorPalette } from './ColorPalette';
|
||||
import { SpectrumPicker } from './SpectrumPicker';
|
||||
|
||||
const DEFAULT_COLOR = '#000000';
|
||||
|
||||
export interface Props {
|
||||
color: string;
|
||||
onColorSelect: (c: string) => void;
|
||||
}
|
||||
|
||||
export class ColorPickerPopover extends React.Component<Props, any> {
|
||||
pickerNavElem: any;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
tab: 'palette',
|
||||
color: this.props.color || DEFAULT_COLOR,
|
||||
colorString: this.props.color || DEFAULT_COLOR,
|
||||
};
|
||||
}
|
||||
|
||||
setPickerNavElem(elem: any) {
|
||||
this.pickerNavElem = $(elem);
|
||||
}
|
||||
|
||||
setColor(color: string) {
|
||||
const newColor = tinycolor(color);
|
||||
if (newColor.isValid()) {
|
||||
this.setState({ color: newColor.toString(), colorString: newColor.toString() });
|
||||
this.props.onColorSelect(color);
|
||||
}
|
||||
}
|
||||
|
||||
sampleColorSelected(color: string) {
|
||||
this.setColor(color);
|
||||
}
|
||||
|
||||
spectrumColorSelected(color: any) {
|
||||
const rgbColor = color.toRgbString();
|
||||
this.setColor(rgbColor);
|
||||
}
|
||||
|
||||
onColorStringChange(e: any) {
|
||||
const colorString = e.target.value;
|
||||
this.setState({ colorString: colorString });
|
||||
|
||||
const newColor = tinycolor(colorString);
|
||||
if (newColor.isValid()) {
|
||||
// Update only color state
|
||||
const newColorString = newColor.toString();
|
||||
this.setState({ color: newColorString });
|
||||
this.props.onColorSelect(newColorString);
|
||||
}
|
||||
}
|
||||
|
||||
onColorStringBlur(e: any) {
|
||||
const colorString = e.target.value;
|
||||
this.setColor(colorString);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.pickerNavElem.find('li:first').addClass('active');
|
||||
this.pickerNavElem.on('show', (e: any) => {
|
||||
// use href attr (#name => name)
|
||||
const tab = e.target.hash.slice(1);
|
||||
this.setState({ tab: tab });
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const paletteTab = (
|
||||
<div id="palette">
|
||||
<ColorPalette color={this.state.color} onColorSelect={this.sampleColorSelected.bind(this)} />
|
||||
</div>
|
||||
);
|
||||
const spectrumTab = (
|
||||
<div id="spectrum">
|
||||
<SpectrumPicker color={this.state.color} onColorSelect={this.spectrumColorSelected.bind(this)} 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.bind(this)}>
|
||||
<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">
|
||||
Custom
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div className="gf-color-picker__body">{currentTab}</div>
|
||||
<div>
|
||||
<input
|
||||
className="gf-form-input gf-form-input--small"
|
||||
value={this.state.colorString}
|
||||
onChange={this.onColorStringChange.bind(this)}
|
||||
onBlur={this.onColorStringBlur.bind(this)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import Drop from 'tether-drop';
|
||||
import { SeriesColorPickerPopover } from './SeriesColorPickerPopover';
|
||||
|
||||
export interface SeriesColorPickerProps {
|
||||
color: string;
|
||||
yaxis?: number;
|
||||
optionalClass?: string;
|
||||
onColorChange: (newColor: string) => void;
|
||||
onToggleAxis?: () => void;
|
||||
}
|
||||
|
||||
export class SeriesColorPicker extends React.Component<SeriesColorPickerProps> {
|
||||
pickerElem: any;
|
||||
colorPickerDrop: any;
|
||||
|
||||
static defaultProps = {
|
||||
optionalClass: '',
|
||||
yaxis: undefined,
|
||||
onToggleAxis: () => {},
|
||||
};
|
||||
|
||||
constructor(props: SeriesColorPickerProps) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.destroyDrop();
|
||||
}
|
||||
|
||||
onClickToOpen = () => {
|
||||
if (this.colorPickerDrop) {
|
||||
this.destroyDrop();
|
||||
}
|
||||
|
||||
const { color, yaxis, onColorChange, onToggleAxis } = this.props;
|
||||
const dropContent = (
|
||||
<SeriesColorPickerPopover color={color} yaxis={yaxis} onColorChange={onColorChange} onToggleAxis={onToggleAxis} />
|
||||
);
|
||||
const dropContentElem = document.createElement('div');
|
||||
ReactDOM.render(dropContent, dropContentElem);
|
||||
|
||||
const drop = new Drop({
|
||||
target: this.pickerElem,
|
||||
content: dropContentElem,
|
||||
position: 'bottom center',
|
||||
classes: 'drop-popover',
|
||||
openOn: 'hover',
|
||||
hoverCloseDelay: 200,
|
||||
remove: true,
|
||||
tetherOptions: {
|
||||
constraints: [{ to: 'scrollParent', attachment: 'none both' }],
|
||||
attachment: 'bottom center',
|
||||
},
|
||||
});
|
||||
|
||||
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 (
|
||||
<div className={optionalClass} ref={e => (this.pickerElem = e)} onClick={this.onClickToOpen}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import React from 'react';
|
||||
import { ColorPickerPopover } from './ColorPickerPopover';
|
||||
|
||||
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: AxisSelectorProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
yaxis: this.props.yaxis,
|
||||
};
|
||||
this.onToggleAxis = this.onToggleAxis.bind(this);
|
||||
}
|
||||
|
||||
onToggleAxis() {
|
||||
this.setState({
|
||||
yaxis: this.state.yaxis === 2 ? 1 : 2,
|
||||
});
|
||||
|
||||
if (this.props.onToggleAxis) {
|
||||
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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import React from 'react';
|
||||
import _ from 'lodash';
|
||||
import $ from 'jquery';
|
||||
import 'vendor/spectrum';
|
||||
|
||||
export interface Props {
|
||||
color: string;
|
||||
options: object;
|
||||
onColorSelect: (c: string) => void;
|
||||
}
|
||||
|
||||
export class SpectrumPicker extends React.Component<Props, any> {
|
||||
elem: any;
|
||||
isMoving: boolean;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.onSpectrumMove = this.onSpectrumMove.bind(this);
|
||||
this.setComponentElem = this.setComponentElem.bind(this);
|
||||
}
|
||||
|
||||
setComponentElem(elem: any) {
|
||||
this.elem = $(elem);
|
||||
}
|
||||
|
||||
onSpectrumMove(color: any) {
|
||||
this.isMoving = true;
|
||||
this.props.onColorSelect(color);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const 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: any) {
|
||||
// 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} />;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,628 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CollorPalette renders correctly 1`] = `
|
||||
<div
|
||||
className="graph-legend-popover"
|
||||
>
|
||||
<p
|
||||
className="m-b-0"
|
||||
>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#890f02",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#58140c",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#99440a",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#c15c17",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#967302",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#cca300",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#3f6833",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#2f575e",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#64b0c8",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#052b51",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#0a50a1",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#584477",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#3f2b5b",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#511749",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#e24d42",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#bf1b00",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#ef843c",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#f4d598",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#e5ac0e",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#9ac48a",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#508642",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#6ed0e0",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#65c5db",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#0a437c",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#447ebc",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#614d93",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#d683ce",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#6d1f62",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#ea6460",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#e0752d",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#f9934e",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#fceaca",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle-o"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#eab839",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#b7dbab",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#629e51",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#70dbed",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#82b5d8",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#1f78c1",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#aea2e0",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#705da0",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#e5a8e2",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#962d82",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#f29191",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#fce2de",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#f9ba8f",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#f9e2d2",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#f2c96d",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#e0f9d7",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#7eb26d",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#cffaff",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#badff4",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#5195ce",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#dedaf7",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#806eb7",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#f9d9f9",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#ba43a9",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
</p>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,11 @@
|
||||
import React, { SFC } from 'react';
|
||||
|
||||
interface LoadingPlaceholderProps {
|
||||
text: string;
|
||||
}
|
||||
|
||||
export const LoadingPlaceholder: SFC<LoadingPlaceholderProps> = ({ text }) => (
|
||||
<div className="gf-form-group">
|
||||
{text} <i className="fa fa-spinner fa-spin" />
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
|
||||
// Ignoring because I couldn't get @types/react-select work wih Torkel's fork
|
||||
// @ts-ignore
|
||||
import { components } from '@torkelo/react-select';
|
||||
|
||||
export const IndicatorsContainer = (props: any) => {
|
||||
const isOpen = props.selectProps.menuIsOpen;
|
||||
return (
|
||||
<components.IndicatorsContainer {...props}>
|
||||
<span
|
||||
className={`gf-form-select-box__select-arrow ${isOpen ? `gf-form-select-box__select-arrow--reversed` : ''}`}
|
||||
/>
|
||||
</components.IndicatorsContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default IndicatorsContainer;
|
||||
@@ -0,0 +1,24 @@
|
||||
import React from 'react';
|
||||
|
||||
// Ignoring because I couldn't get @types/react-select work wih Torkel's fork
|
||||
// @ts-ignore
|
||||
import { components } from '@torkelo/react-select';
|
||||
// @ts-ignore
|
||||
import { OptionProps } from '@torkelo/react-select/lib/components/Option';
|
||||
|
||||
export interface Props {
|
||||
children: Element;
|
||||
}
|
||||
|
||||
export const NoOptionsMessage = (props: OptionProps<any>) => {
|
||||
const { children } = props;
|
||||
return (
|
||||
<components.Option {...props}>
|
||||
<div className="gf-form-select-box__desc-option">
|
||||
<div className="gf-form-select-box__desc-option__body">{children}</div>
|
||||
</div>
|
||||
</components.Option>
|
||||
);
|
||||
};
|
||||
|
||||
export default NoOptionsMessage;
|
||||
237
packages/grafana-ui/src/components/Select/Select.tsx
Normal file
237
packages/grafana-ui/src/components/Select/Select.tsx
Normal file
@@ -0,0 +1,237 @@
|
||||
// Libraries
|
||||
import classNames from 'classnames';
|
||||
import React, { PureComponent } from 'react';
|
||||
|
||||
// Ignoring because I couldn't get @types/react-select work wih Torkel's fork
|
||||
// @ts-ignore
|
||||
import { default as ReactSelect } from '@torkelo/react-select';
|
||||
// @ts-ignore
|
||||
import { default as ReactAsyncSelect } from '@torkelo/react-select/lib/Async';
|
||||
// @ts-ignore
|
||||
import { components } from '@torkelo/react-select';
|
||||
|
||||
// Components
|
||||
import { SelectOption, SingleValue } from './SelectOption';
|
||||
import SelectOptionGroup from './SelectOptionGroup';
|
||||
import IndicatorsContainer from './IndicatorsContainer';
|
||||
import NoOptionsMessage from './NoOptionsMessage';
|
||||
import resetSelectStyles from './resetSelectStyles';
|
||||
import { CustomScrollbar } from '@grafana/ui';
|
||||
|
||||
export interface SelectOptionItem {
|
||||
label?: string;
|
||||
value?: any;
|
||||
imgUrl?: string;
|
||||
description?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
interface CommonProps {
|
||||
defaultValue?: any;
|
||||
getOptionLabel?: (item: SelectOptionItem) => string;
|
||||
getOptionValue?: (item: SelectOptionItem) => string;
|
||||
onChange: (item: SelectOptionItem) => {} | void;
|
||||
placeholder?: string;
|
||||
width?: number;
|
||||
value?: SelectOptionItem;
|
||||
className?: string;
|
||||
isDisabled?: boolean;
|
||||
isSearchable?: boolean;
|
||||
isClearable?: boolean;
|
||||
autoFocus?: boolean;
|
||||
openMenuOnFocus?: boolean;
|
||||
onBlur?: () => void;
|
||||
maxMenuHeight?: number;
|
||||
isLoading: boolean;
|
||||
noOptionsMessage?: () => string;
|
||||
isMulti?: boolean;
|
||||
backspaceRemovesValue: boolean;
|
||||
}
|
||||
|
||||
interface SelectProps {
|
||||
options: SelectOptionItem[];
|
||||
}
|
||||
|
||||
interface AsyncProps {
|
||||
defaultOptions: boolean;
|
||||
loadOptions: (query: string) => Promise<SelectOptionItem[]>;
|
||||
loadingMessage?: () => string;
|
||||
}
|
||||
|
||||
export const MenuList = (props: any) => {
|
||||
return (
|
||||
<components.MenuList {...props}>
|
||||
<CustomScrollbar autoHide={false}>{props.children}</CustomScrollbar>
|
||||
</components.MenuList>
|
||||
);
|
||||
};
|
||||
|
||||
export class Select extends PureComponent<CommonProps & SelectProps> {
|
||||
static defaultProps = {
|
||||
width: null,
|
||||
className: '',
|
||||
isDisabled: false,
|
||||
isSearchable: true,
|
||||
isClearable: false,
|
||||
isMulti: false,
|
||||
openMenuOnFocus: false,
|
||||
autoFocus: false,
|
||||
isLoading: false,
|
||||
backspaceRemovesValue: true,
|
||||
maxMenuHeight: 300,
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
defaultValue,
|
||||
getOptionLabel,
|
||||
getOptionValue,
|
||||
onChange,
|
||||
options,
|
||||
placeholder,
|
||||
width,
|
||||
value,
|
||||
className,
|
||||
isDisabled,
|
||||
isLoading,
|
||||
isSearchable,
|
||||
isClearable,
|
||||
backspaceRemovesValue,
|
||||
isMulti,
|
||||
autoFocus,
|
||||
openMenuOnFocus,
|
||||
onBlur,
|
||||
maxMenuHeight,
|
||||
noOptionsMessage,
|
||||
} = this.props;
|
||||
|
||||
let widthClass = '';
|
||||
if (width) {
|
||||
widthClass = 'width-' + width;
|
||||
}
|
||||
|
||||
const selectClassNames = classNames('gf-form-input', 'gf-form-input--form-dropdown', widthClass, className);
|
||||
|
||||
return (
|
||||
<ReactSelect
|
||||
classNamePrefix="gf-form-select-box"
|
||||
className={selectClassNames}
|
||||
components={{
|
||||
Option: SelectOption,
|
||||
SingleValue,
|
||||
IndicatorsContainer,
|
||||
MenuList,
|
||||
Group: SelectOptionGroup,
|
||||
}}
|
||||
defaultValue={defaultValue}
|
||||
value={value}
|
||||
getOptionLabel={getOptionLabel}
|
||||
getOptionValue={getOptionValue}
|
||||
menuShouldScrollIntoView={false}
|
||||
isSearchable={isSearchable}
|
||||
onChange={onChange}
|
||||
options={options}
|
||||
placeholder={placeholder || 'Choose'}
|
||||
styles={resetSelectStyles()}
|
||||
isDisabled={isDisabled}
|
||||
isLoading={isLoading}
|
||||
isClearable={isClearable}
|
||||
autoFocus={autoFocus}
|
||||
onBlur={onBlur}
|
||||
openMenuOnFocus={openMenuOnFocus}
|
||||
maxMenuHeight={maxMenuHeight}
|
||||
noOptionsMessage={noOptionsMessage}
|
||||
isMulti={isMulti}
|
||||
backspaceRemovesValue={backspaceRemovesValue}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class AsyncSelect extends PureComponent<CommonProps & AsyncProps> {
|
||||
static defaultProps = {
|
||||
width: null,
|
||||
className: '',
|
||||
components: {},
|
||||
loadingMessage: () => 'Loading...',
|
||||
isDisabled: false,
|
||||
isClearable: false,
|
||||
isMulti: false,
|
||||
isSearchable: true,
|
||||
backspaceRemovesValue: true,
|
||||
autoFocus: false,
|
||||
openMenuOnFocus: false,
|
||||
maxMenuHeight: 300,
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
defaultValue,
|
||||
getOptionLabel,
|
||||
getOptionValue,
|
||||
onChange,
|
||||
placeholder,
|
||||
width,
|
||||
value,
|
||||
className,
|
||||
loadOptions,
|
||||
defaultOptions,
|
||||
isLoading,
|
||||
loadingMessage,
|
||||
noOptionsMessage,
|
||||
isDisabled,
|
||||
isSearchable,
|
||||
isClearable,
|
||||
backspaceRemovesValue,
|
||||
autoFocus,
|
||||
onBlur,
|
||||
openMenuOnFocus,
|
||||
maxMenuHeight,
|
||||
isMulti,
|
||||
} = this.props;
|
||||
|
||||
let widthClass = '';
|
||||
if (width) {
|
||||
widthClass = 'width-' + width;
|
||||
}
|
||||
|
||||
const selectClassNames = classNames('gf-form-input', 'gf-form-input--form-dropdown', widthClass, className);
|
||||
|
||||
return (
|
||||
<ReactAsyncSelect
|
||||
classNamePrefix="gf-form-select-box"
|
||||
className={selectClassNames}
|
||||
components={{
|
||||
Option,
|
||||
SingleValue,
|
||||
IndicatorsContainer,
|
||||
NoOptionsMessage,
|
||||
}}
|
||||
defaultValue={defaultValue}
|
||||
value={value}
|
||||
getOptionLabel={getOptionLabel}
|
||||
getOptionValue={getOptionValue}
|
||||
menuShouldScrollIntoView={false}
|
||||
onChange={onChange}
|
||||
loadOptions={loadOptions}
|
||||
isLoading={isLoading}
|
||||
defaultOptions={defaultOptions}
|
||||
placeholder={placeholder || 'Choose'}
|
||||
styles={resetSelectStyles()}
|
||||
loadingMessage={loadingMessage}
|
||||
noOptionsMessage={noOptionsMessage}
|
||||
isDisabled={isDisabled}
|
||||
isSearchable={isSearchable}
|
||||
isClearable={isClearable}
|
||||
autoFocus={autoFocus}
|
||||
onBlur={onBlur}
|
||||
openMenuOnFocus={openMenuOnFocus}
|
||||
maxMenuHeight={maxMenuHeight}
|
||||
isMulti={isMulti}
|
||||
backspaceRemovesValue={backspaceRemovesValue}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Select;
|
||||
@@ -0,0 +1,48 @@
|
||||
import React from 'react';
|
||||
import renderer from 'react-test-renderer';
|
||||
import SelectOption from './SelectOption';
|
||||
import { OptionProps } from 'react-select/lib/components/Option';
|
||||
|
||||
const model: OptionProps<any> = {
|
||||
cx: jest.fn(),
|
||||
clearValue: jest.fn(),
|
||||
getStyles: jest.fn(),
|
||||
getValue: jest.fn(),
|
||||
hasValue: true,
|
||||
isMulti: false,
|
||||
options: [],
|
||||
selectOption: jest.fn(),
|
||||
selectProps: {},
|
||||
setValue: jest.fn(),
|
||||
isDisabled: false,
|
||||
isFocused: false,
|
||||
isSelected: false,
|
||||
innerRef: null,
|
||||
innerProps: {
|
||||
id: '',
|
||||
key: '',
|
||||
onClick: jest.fn(),
|
||||
onMouseOver: jest.fn(),
|
||||
tabIndex: 1,
|
||||
},
|
||||
label: 'Option label',
|
||||
type: 'option',
|
||||
children: 'Model title',
|
||||
className: 'class-for-user-picker',
|
||||
};
|
||||
|
||||
describe('SelectOption', () => {
|
||||
it('renders correctly', () => {
|
||||
const tree = renderer
|
||||
.create(
|
||||
<SelectOption
|
||||
{...model}
|
||||
data={{
|
||||
imgUrl: 'url/to/avatar',
|
||||
}}
|
||||
/>
|
||||
)
|
||||
.toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
47
packages/grafana-ui/src/components/Select/SelectOption.tsx
Normal file
47
packages/grafana-ui/src/components/Select/SelectOption.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import React from 'react';
|
||||
|
||||
// Ignoring because I couldn't get @types/react-select work wih Torkel's fork
|
||||
// @ts-ignore
|
||||
import { components } from '@torkelo/react-select';
|
||||
import { OptionProps } from 'react-select/lib/components/Option';
|
||||
|
||||
// https://github.com/JedWatson/react-select/issues/3038
|
||||
interface ExtendedOptionProps extends OptionProps<any> {
|
||||
data: {
|
||||
description?: string;
|
||||
imgUrl?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export const SelectOption = (props: ExtendedOptionProps) => {
|
||||
const { children, isSelected, data } = props;
|
||||
|
||||
return (
|
||||
<components.Option {...props}>
|
||||
<div className="gf-form-select-box__desc-option">
|
||||
{data.imgUrl && <img className="gf-form-select-box__desc-option__img" src={data.imgUrl} />}
|
||||
<div className="gf-form-select-box__desc-option__body">
|
||||
<div>{children}</div>
|
||||
{data.description && <div className="gf-form-select-box__desc-option__desc">{data.description}</div>}
|
||||
</div>
|
||||
{isSelected && <i className="fa fa-check" aria-hidden="true" />}
|
||||
</div>
|
||||
</components.Option>
|
||||
);
|
||||
};
|
||||
|
||||
// was not able to type this without typescript error
|
||||
export const SingleValue = (props: any) => {
|
||||
const { children, data } = props;
|
||||
|
||||
return (
|
||||
<components.SingleValue {...props}>
|
||||
<div className="gf-form-select-box__img-value">
|
||||
{data.imgUrl && <img className="gf-form-select-box__desc-option__img" src={data.imgUrl} />}
|
||||
{children}
|
||||
</div>
|
||||
</components.SingleValue>
|
||||
);
|
||||
};
|
||||
|
||||
export default SelectOption;
|
||||
@@ -0,0 +1,53 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { GroupProps } from 'react-select/lib/components/Group';
|
||||
|
||||
interface ExtendedGroupProps extends GroupProps<any> {
|
||||
data: any;
|
||||
}
|
||||
|
||||
interface State {
|
||||
expanded: boolean;
|
||||
}
|
||||
|
||||
export default class SelectOptionGroup extends PureComponent<ExtendedGroupProps, State> {
|
||||
state = {
|
||||
expanded: false,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.selectProps) {
|
||||
const value = this.props.selectProps.value[this.props.selectProps.value.length - 1];
|
||||
|
||||
if (value && this.props.options.some(option => option.value === value)) {
|
||||
this.setState({ expanded: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(nextProps: ExtendedGroupProps) {
|
||||
if (nextProps.selectProps.inputValue !== '') {
|
||||
this.setState({ expanded: true });
|
||||
}
|
||||
}
|
||||
|
||||
onToggleChildren = () => {
|
||||
this.setState(prevState => ({
|
||||
expanded: !prevState.expanded,
|
||||
}));
|
||||
};
|
||||
|
||||
render() {
|
||||
const { children, label } = this.props;
|
||||
const { expanded } = this.state;
|
||||
|
||||
return (
|
||||
<div className="gf-form-select-box__option-group">
|
||||
<div className="gf-form-select-box__option-group__header" onClick={this.onToggleChildren}>
|
||||
<span className="flex-grow">{label}</span>
|
||||
<i className={`fa ${expanded ? 'fa-caret-left' : 'fa-caret-down'}`} />{' '}
|
||||
</div>
|
||||
{expanded && children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
189
packages/grafana-ui/src/components/Select/_Select.scss
Normal file
189
packages/grafana-ui/src/components/Select/_Select.scss
Normal file
@@ -0,0 +1,189 @@
|
||||
$select-input-height: 35px;
|
||||
$select-input-bg-disabled: $input-bg-disabled;
|
||||
|
||||
@mixin select-control() {
|
||||
width: 100%;
|
||||
margin-right: $gf-form-margin;
|
||||
@include border-radius($input-border-radius-sm);
|
||||
background-color: $input-bg;
|
||||
}
|
||||
|
||||
@mixin select-control-focus() {
|
||||
border-color: $input-border-focus;
|
||||
outline: none;
|
||||
$shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px $input-box-shadow-focus;
|
||||
@include box-shadow($shadow);
|
||||
}
|
||||
|
||||
.gf-form-select-box__control {
|
||||
@include select-control();
|
||||
border: 1px solid $input-border-color;
|
||||
color: $input-color;
|
||||
cursor: default;
|
||||
display: table;
|
||||
border-spacing: 0;
|
||||
border-collapse: separate;
|
||||
height: $select-input-height;
|
||||
outline: none;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.gf-form-select-box__control--is-focused {
|
||||
background-color: $input-bg;
|
||||
@include select-control-focus();
|
||||
}
|
||||
|
||||
.gf-form-select-box__control--is-disabled {
|
||||
background-color: $select-input-bg-disabled;
|
||||
}
|
||||
|
||||
.gf-form-select-box__control--menu-right {
|
||||
.gf-form-select-box__menu {
|
||||
right: 0;
|
||||
left: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.gf-form-select-box__input {
|
||||
padding-left: 5px;
|
||||
input {
|
||||
line-height: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.gf-form-select-box__menu {
|
||||
background: $input-bg;
|
||||
box-shadow: $menu-dropdown-shadow;
|
||||
position: absolute;
|
||||
z-index: $zindex-dropdown;
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.gf-form-select-box__menu-list {
|
||||
overflow-y: auto;
|
||||
max-height: 300px;
|
||||
}
|
||||
|
||||
.tag-filter .gf-form-select-box__menu {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* .gf-form-select-box__single-value { */
|
||||
/* } */
|
||||
|
||||
.gf-form-select-box__multi-value {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.gf-form-select-box__option {
|
||||
border-left: 2px solid transparent;
|
||||
white-space: nowrap;
|
||||
background-color: $input-bg;
|
||||
|
||||
&.gf-form-select-box__option--is-focused {
|
||||
color: $dropdownLinkColorHover;
|
||||
background: $menu-dropdown-hover-bg;
|
||||
@include left-brand-border-gradient();
|
||||
}
|
||||
|
||||
&.gf-form-select-box__option--is-selected {
|
||||
.fa {
|
||||
color: $input-color-select-arrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.gf-form-select-box__control--is-focused .gf-form-select-box__placeholder {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.gf-form-select-box__value-container {
|
||||
display: table-cell;
|
||||
padding: 6px 10px;
|
||||
> div {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.gf-form-select-box__indicators {
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
text-align: right;
|
||||
padding-right: 10px;
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
.gf-form-select-box__select-arrow {
|
||||
border-color: $input-color-select-arrow transparent transparent;
|
||||
border-style: solid;
|
||||
border-width: 4px 4px 2.5px;
|
||||
display: inline-block;
|
||||
height: 0;
|
||||
width: 0;
|
||||
position: relative;
|
||||
|
||||
&.gf-form-select-box__select-arrow--reversed {
|
||||
border-color: transparent transparent $input-color-select-arrow;
|
||||
top: -2px;
|
||||
border-width: 0 4px 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.gf-form-input--form-dropdown {
|
||||
padding: 0;
|
||||
border: 0;
|
||||
overflow: visible;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.gf-form--has-input-icon {
|
||||
.gf-form-select-box__value-container {
|
||||
padding-left: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.gf-form-select-box__desc-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
justify-items: center;
|
||||
cursor: pointer;
|
||||
padding: 7px 10px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.gf-form-select-box__desc-option__body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
padding-right: 10px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.gf-form-select-box__desc-option__desc {
|
||||
font-weight: normal;
|
||||
font-size: $font-size-sm;
|
||||
color: $text-muted;
|
||||
}
|
||||
|
||||
.gf-form-select-box__desc-option__img {
|
||||
width: 16px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.gf-form-select-box__option-group__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
justify-items: center;
|
||||
cursor: pointer;
|
||||
padding: 7px 10px;
|
||||
width: 100%;
|
||||
border-bottom: 1px solid $hr-border-color;
|
||||
text-transform: capitalize;
|
||||
|
||||
.fa {
|
||||
padding-right: 2px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`SelectOption renders correctly 1`] = `
|
||||
<div
|
||||
id=""
|
||||
onClick={[MockFunction]}
|
||||
onMouseOver={[MockFunction]}
|
||||
tabIndex={1}
|
||||
>
|
||||
<div
|
||||
className="gf-form-select-box__desc-option"
|
||||
>
|
||||
<img
|
||||
className="gf-form-select-box__desc-option__img"
|
||||
src="url/to/avatar"
|
||||
/>
|
||||
<div
|
||||
className="gf-form-select-box__desc-option__body"
|
||||
>
|
||||
<div>
|
||||
Model title
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,27 @@
|
||||
export default function resetSelectStyles() {
|
||||
return {
|
||||
clearIndicator: () => ({}),
|
||||
container: () => ({}),
|
||||
control: () => ({}),
|
||||
dropdownIndicator: () => ({}),
|
||||
group: () => ({}),
|
||||
groupHeading: () => ({}),
|
||||
indicatorsContainer: () => ({}),
|
||||
indicatorSeparator: () => ({}),
|
||||
input: () => ({}),
|
||||
loadingIndicator: () => ({}),
|
||||
loadingMessage: () => ({}),
|
||||
menu: () => ({}),
|
||||
menuList: ({ maxHeight }: { maxHeight: number }) => ({
|
||||
maxHeight,
|
||||
}),
|
||||
multiValue: () => ({}),
|
||||
multiValueLabel: () => ({}),
|
||||
multiValueRemove: () => ({}),
|
||||
noOptionsMessage: () => ({}),
|
||||
option: () => ({}),
|
||||
placeholder: () => ({}),
|
||||
singleValue: () => ({}),
|
||||
valueContainer: () => ({}),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { ThresholdsEditor, Props } from './ThresholdsEditor';
|
||||
import { BasicGaugeColor } from '../../types';
|
||||
|
||||
const setup = (propOverrides?: object) => {
|
||||
const props: Props = {
|
||||
onChange: jest.fn(),
|
||||
thresholds: [],
|
||||
};
|
||||
|
||||
Object.assign(props, propOverrides);
|
||||
|
||||
return shallow(<ThresholdsEditor {...props} />).instance() as ThresholdsEditor;
|
||||
};
|
||||
|
||||
describe('Add threshold', () => {
|
||||
it('should add threshold', () => {
|
||||
const instance = setup();
|
||||
|
||||
instance.onAddThreshold(0);
|
||||
|
||||
expect(instance.state.thresholds).toEqual([{ index: 0, value: 50, color: 'rgb(127, 115, 64)' }]);
|
||||
});
|
||||
|
||||
it('should add another threshold above a first', () => {
|
||||
const instance = setup({
|
||||
thresholds: [{ index: 0, value: 50, color: 'rgb(127, 115, 64)' }],
|
||||
});
|
||||
|
||||
instance.onAddThreshold(1);
|
||||
|
||||
expect(instance.state.thresholds).toEqual([
|
||||
{ index: 1, value: 75, color: 'rgb(170, 95, 61)' },
|
||||
{ index: 0, value: 50, color: 'rgb(127, 115, 64)' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('change threshold value', () => {
|
||||
it('should update value and resort rows', () => {
|
||||
const instance = setup();
|
||||
const mockThresholds = [
|
||||
{ index: 0, value: 50, color: 'rgba(237, 129, 40, 0.89)' },
|
||||
{ index: 1, value: 75, color: 'rgba(237, 129, 40, 0.89)' },
|
||||
];
|
||||
|
||||
instance.state = {
|
||||
baseColor: BasicGaugeColor.Green,
|
||||
thresholds: mockThresholds,
|
||||
};
|
||||
|
||||
const mockEvent = { target: { value: 78 } };
|
||||
|
||||
instance.onChangeThresholdValue(mockEvent, mockThresholds[0]);
|
||||
|
||||
expect(instance.state.thresholds).toEqual([
|
||||
{ index: 0, value: 78, color: 'rgba(237, 129, 40, 0.89)' },
|
||||
{ index: 1, value: 75, color: 'rgba(237, 129, 40, 0.89)' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,222 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import tinycolor, { ColorInput } from 'tinycolor2';
|
||||
|
||||
import { Threshold, BasicGaugeColor } from '../../types';
|
||||
import { ColorPicker } from '../ColorPicker/ColorPicker';
|
||||
|
||||
export interface Props {
|
||||
thresholds: Threshold[];
|
||||
onChange: (thresholds: Threshold[]) => void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
thresholds: Threshold[];
|
||||
baseColor: string;
|
||||
}
|
||||
|
||||
export class ThresholdsEditor extends PureComponent<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = { thresholds: props.thresholds, baseColor: BasicGaugeColor.Green };
|
||||
}
|
||||
|
||||
onAddThreshold = (index: number) => {
|
||||
const maxValue = 100; // hardcoded for now before we add the base threshold
|
||||
const minValue = 0; // hardcoded for now before we add the base threshold
|
||||
const { thresholds } = this.state;
|
||||
|
||||
const newThresholds = thresholds.map(threshold => {
|
||||
if (threshold.index >= index) {
|
||||
threshold = {
|
||||
...threshold,
|
||||
index: threshold.index + 1,
|
||||
};
|
||||
}
|
||||
|
||||
return threshold;
|
||||
});
|
||||
|
||||
// Setting value to a value between the previous thresholds
|
||||
let value;
|
||||
|
||||
if (index === 0 && thresholds.length === 0) {
|
||||
value = maxValue - (maxValue - minValue) / 2;
|
||||
} else if (index === 0 && thresholds.length > 0) {
|
||||
value = newThresholds[index + 1].value - (newThresholds[index + 1].value - minValue) / 2;
|
||||
} else if (index > newThresholds[newThresholds.length - 1].index) {
|
||||
value = maxValue - (maxValue - newThresholds[index - 1].value) / 2;
|
||||
}
|
||||
|
||||
// Set a color that lies between the previous thresholds
|
||||
let color;
|
||||
if (index === 0 && thresholds.length === 0) {
|
||||
color = tinycolor.mix(BasicGaugeColor.Green, BasicGaugeColor.Red, 50).toRgbString();
|
||||
} else {
|
||||
color = tinycolor.mix(thresholds[index - 1].color as ColorInput, BasicGaugeColor.Red, 50).toRgbString();
|
||||
}
|
||||
|
||||
this.setState(
|
||||
{
|
||||
thresholds: this.sortThresholds([
|
||||
...newThresholds,
|
||||
{
|
||||
index,
|
||||
value: value as number,
|
||||
color,
|
||||
},
|
||||
]),
|
||||
},
|
||||
() => this.updateGauge()
|
||||
);
|
||||
};
|
||||
|
||||
onRemoveThreshold = (threshold: Threshold) => {
|
||||
this.setState(
|
||||
prevState => ({ thresholds: prevState.thresholds.filter(t => t !== threshold) }),
|
||||
() => this.updateGauge()
|
||||
);
|
||||
};
|
||||
|
||||
onChangeThresholdValue = (event: any, threshold: Threshold) => {
|
||||
const { thresholds } = this.state;
|
||||
|
||||
const newThresholds = thresholds.map(t => {
|
||||
if (t === threshold) {
|
||||
t = { ...t, value: event.target.value };
|
||||
}
|
||||
|
||||
return t;
|
||||
});
|
||||
|
||||
this.setState({ thresholds: newThresholds });
|
||||
};
|
||||
|
||||
onChangeThresholdColor = (threshold: Threshold, color: string) => {
|
||||
const { thresholds } = this.state;
|
||||
|
||||
const newThresholds = thresholds.map(t => {
|
||||
if (t === threshold) {
|
||||
t = { ...t, color: color };
|
||||
}
|
||||
|
||||
return t;
|
||||
});
|
||||
|
||||
this.setState(
|
||||
{
|
||||
thresholds: newThresholds,
|
||||
},
|
||||
() => this.updateGauge()
|
||||
);
|
||||
};
|
||||
|
||||
onChangeBaseColor = (color: string) => this.props.onChange(this.state.thresholds);
|
||||
onBlur = () => {
|
||||
this.setState(prevState => ({ thresholds: this.sortThresholds(prevState.thresholds) }));
|
||||
|
||||
this.updateGauge();
|
||||
};
|
||||
|
||||
updateGauge = () => {
|
||||
this.props.onChange(this.state.thresholds);
|
||||
};
|
||||
|
||||
sortThresholds = (thresholds: Threshold[]) => {
|
||||
return thresholds.sort((t1, t2) => {
|
||||
return t2.value - t1.value;
|
||||
});
|
||||
};
|
||||
|
||||
renderThresholds() {
|
||||
const { thresholds } = this.state;
|
||||
|
||||
return thresholds.map((threshold, index) => {
|
||||
return (
|
||||
<div className="threshold-row" key={`${threshold.index}-${index}`}>
|
||||
<div className="threshold-row-inner">
|
||||
<div className="threshold-row-color">
|
||||
{threshold.color && (
|
||||
<div className="threshold-row-color-inner">
|
||||
<ColorPicker
|
||||
color={threshold.color}
|
||||
onChange={color => this.onChangeThresholdColor(threshold, color)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<input
|
||||
className="threshold-row-input"
|
||||
type="text"
|
||||
onChange={event => this.onChangeThresholdValue(event, threshold)}
|
||||
value={threshold.value}
|
||||
onBlur={this.onBlur}
|
||||
/>
|
||||
<div onClick={() => this.onRemoveThreshold(threshold)} className="threshold-row-remove">
|
||||
<i className="fa fa-times" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
renderIndicator() {
|
||||
const { thresholds } = this.state;
|
||||
|
||||
return thresholds.map((t, i) => {
|
||||
return (
|
||||
<div key={`${t.value}-${i}`} className="indicator-section">
|
||||
<div onClick={() => this.onAddThreshold(t.index + 1)} style={{ height: '50%', backgroundColor: t.color }} />
|
||||
<div onClick={() => this.onAddThreshold(t.index)} style={{ height: '50%', backgroundColor: t.color }} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
renderBaseIndicator() {
|
||||
return (
|
||||
<div className="indicator-section" style={{ height: '100%' }}>
|
||||
<div
|
||||
onClick={() => this.onAddThreshold(0)}
|
||||
style={{ height: '100%', backgroundColor: BasicGaugeColor.Green }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderBase() {
|
||||
const baseColor = BasicGaugeColor.Green;
|
||||
|
||||
return (
|
||||
<div className="threshold-row threshold-row-base">
|
||||
<div className="threshold-row-inner threshold-row-inner--base">
|
||||
<div className="threshold-row-color">
|
||||
<div className="threshold-row-color-inner">
|
||||
<ColorPicker color={baseColor} onChange={color => this.onChangeBaseColor(color)} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="threshold-row-label">Base</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="section gf-form-group">
|
||||
<h5 className="section-heading">Thresholds</h5>
|
||||
<div className="thresholds">
|
||||
<div className="color-indicators">
|
||||
{this.renderIndicator()}
|
||||
{this.renderBaseIndicator()}
|
||||
</div>
|
||||
<div className="threshold-rows">
|
||||
{this.renderThresholds()}
|
||||
{this.renderBase()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
.thresholds {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.threshold-rows {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.threshold-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 3px;
|
||||
padding: 5px;
|
||||
|
||||
&::before {
|
||||
font-family: 'FontAwesome';
|
||||
content: '\f0d9';
|
||||
color: $input-label-border-color;
|
||||
}
|
||||
}
|
||||
|
||||
.threshold-row-inner {
|
||||
border: 1px solid $input-label-border-color;
|
||||
border-radius: $border-radius;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
height: 37px;
|
||||
|
||||
&--base {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.threshold-row-color {
|
||||
width: 36px;
|
||||
border-right: 1px solid $input-label-border-color;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: $input-bg;
|
||||
}
|
||||
|
||||
.threshold-row-color-inner {
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.threshold-row-input {
|
||||
padding: 8px 10px;
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.threshold-row-label {
|
||||
background-color: $input-label-bg;
|
||||
padding: 5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.threshold-row-add-label {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
padding: 5px 8px;
|
||||
}
|
||||
|
||||
.threshold-row-remove {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 37px;
|
||||
width: 37px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.threshold-row-add {
|
||||
border-right: $border-width solid $input-label-border-color;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
background-color: $green;
|
||||
}
|
||||
|
||||
.threshold-row-label {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
.indicator-section {
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.color-indicators {
|
||||
width: 15px;
|
||||
border-bottom-left-radius: $border-radius;
|
||||
border-bottom-right-radius: $border-radius;
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
@import 'CustomScrollbar/CustomScrollbar';
|
||||
@import 'DeleteButton/DeleteButton';
|
||||
@import 'ThresholdsEditor/ThresholdsEditor';
|
||||
@import 'Tooltip/Tooltip';
|
||||
@import 'Select/Select';
|
||||
|
||||
@@ -2,3 +2,15 @@ export { DeleteButton } from './DeleteButton/DeleteButton';
|
||||
export { Tooltip } from './Tooltip/Tooltip';
|
||||
export { Portal } from './Portal/Portal';
|
||||
export { CustomScrollbar } from './CustomScrollbar/CustomScrollbar';
|
||||
|
||||
// Select
|
||||
export { Select, AsyncSelect, SelectOptionItem } from './Select/Select';
|
||||
export { IndicatorsContainer } from './Select/IndicatorsContainer';
|
||||
export { NoOptionsMessage } from './Select/NoOptionsMessage';
|
||||
export { default as resetSelectStyles } from './Select/resetSelectStyles';
|
||||
|
||||
export { LoadingPlaceholder } from './LoadingPlaceholder/LoadingPlaceholder';
|
||||
export { ColorPicker } from './ColorPicker/ColorPicker';
|
||||
export { SeriesColorPickerPopover } from './ColorPicker/SeriesColorPickerPopover';
|
||||
export { SeriesColorPicker } from './ColorPicker/SeriesColorPicker';
|
||||
export { ThresholdsEditor } from './ThresholdsEditor/ThresholdsEditor';
|
||||
|
||||
16
packages/grafana-ui/src/types/gauge.ts
Normal file
16
packages/grafana-ui/src/types/gauge.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { RangeMap, Threshold, ValueMap } from './panel';
|
||||
|
||||
export interface GaugeOptions {
|
||||
baseColor: string;
|
||||
decimals: number;
|
||||
mappings: Array<RangeMap | ValueMap>;
|
||||
maxValue: number;
|
||||
minValue: number;
|
||||
prefix: string;
|
||||
showThresholdLabels: boolean;
|
||||
showThresholdMarkers: boolean;
|
||||
stat: string;
|
||||
suffix: string;
|
||||
thresholds: Threshold[];
|
||||
unit: string;
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './series';
|
||||
export * from './time';
|
||||
export * from './panel';
|
||||
export * from './gauge';
|
||||
|
||||
@@ -29,3 +29,35 @@ export interface PanelMenuItem {
|
||||
shortcut?: string;
|
||||
subMenu?: PanelMenuItem[];
|
||||
}
|
||||
|
||||
export interface Threshold {
|
||||
index: number;
|
||||
value: number;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
export enum BasicGaugeColor {
|
||||
Green = '#299c46',
|
||||
Red = '#d44a3a',
|
||||
}
|
||||
|
||||
export enum MappingType {
|
||||
ValueToText = 1,
|
||||
RangeToText = 2,
|
||||
}
|
||||
|
||||
interface BaseMap {
|
||||
id: number;
|
||||
operator: string;
|
||||
text: string;
|
||||
type: MappingType;
|
||||
}
|
||||
|
||||
export interface ValueMap extends BaseMap {
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface RangeMap extends BaseMap {
|
||||
from: string;
|
||||
to: string;
|
||||
}
|
||||
|
||||
93
packages/grafana-ui/src/utils/colors.ts
Normal file
93
packages/grafana-ui/src/utils/colors.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import _ from 'lodash';
|
||||
import tinycolor from 'tinycolor2';
|
||||
|
||||
export const PALETTE_ROWS = 4;
|
||||
export const PALETTE_COLUMNS = 14;
|
||||
export const DEFAULT_ANNOTATION_COLOR = 'rgba(0, 211, 255, 1)';
|
||||
export const OK_COLOR = 'rgba(11, 237, 50, 1)';
|
||||
export const ALERTING_COLOR = 'rgba(237, 46, 24, 1)';
|
||||
export const NO_DATA_COLOR = 'rgba(150, 150, 150, 1)';
|
||||
export const PENDING_COLOR = 'rgba(247, 149, 32, 1)';
|
||||
export const REGION_FILL_ALPHA = 0.09;
|
||||
|
||||
export const colors = [
|
||||
'#7EB26D', // 0: pale green
|
||||
'#EAB839', // 1: mustard
|
||||
'#6ED0E0', // 2: light blue
|
||||
'#EF843C', // 3: orange
|
||||
'#E24D42', // 4: red
|
||||
'#1F78C1', // 5: ocean
|
||||
'#BA43A9', // 6: purple
|
||||
'#705DA0', // 7: violet
|
||||
'#508642', // 8: dark green
|
||||
'#CCA300', // 9: dark sand
|
||||
'#447EBC',
|
||||
'#C15C17',
|
||||
'#890F02',
|
||||
'#0A437C',
|
||||
'#6D1F62',
|
||||
'#584477',
|
||||
'#B7DBAB',
|
||||
'#F4D598',
|
||||
'#70DBED',
|
||||
'#F9BA8F',
|
||||
'#F29191',
|
||||
'#82B5D8',
|
||||
'#E5A8E2',
|
||||
'#AEA2E0',
|
||||
'#629E51',
|
||||
'#E5AC0E',
|
||||
'#64B0C8',
|
||||
'#E0752D',
|
||||
'#BF1B00',
|
||||
'#0A50A1',
|
||||
'#962D82',
|
||||
'#614D93',
|
||||
'#9AC48A',
|
||||
'#F2C96D',
|
||||
'#65C5DB',
|
||||
'#F9934E',
|
||||
'#EA6460',
|
||||
'#5195CE',
|
||||
'#D683CE',
|
||||
'#806EB7',
|
||||
'#3F6833',
|
||||
'#967302',
|
||||
'#2F575E',
|
||||
'#99440A',
|
||||
'#58140C',
|
||||
'#052B51',
|
||||
'#511749',
|
||||
'#3F2B5B',
|
||||
'#E0F9D7',
|
||||
'#FCEACA',
|
||||
'#CFFAFF',
|
||||
'#F9E2D2',
|
||||
'#FCE2DE',
|
||||
'#BADFF4',
|
||||
'#F9D9F9',
|
||||
'#DEDAF7',
|
||||
];
|
||||
|
||||
function sortColorsByHue(hexColors: string[]) {
|
||||
const hslColors = _.map(hexColors, hexToHsl);
|
||||
|
||||
const sortedHSLColors = _.sortBy(hslColors, ['h']);
|
||||
const chunkedHSLColors = _.chunk(sortedHSLColors, PALETTE_ROWS);
|
||||
const sortedChunkedHSLColors = _.map(chunkedHSLColors, chunk => {
|
||||
return _.sortBy(chunk, 'l');
|
||||
});
|
||||
const flattenedZippedSortedChunkedHSLColors = _.flattenDeep(_.zip(...sortedChunkedHSLColors));
|
||||
|
||||
return _.map(flattenedZippedSortedChunkedHSLColors, hslToHex);
|
||||
}
|
||||
|
||||
function hexToHsl(color: string) {
|
||||
return tinycolor(color).toHsl();
|
||||
}
|
||||
|
||||
function hslToHex(color: any) {
|
||||
return tinycolor(color).toHexString();
|
||||
}
|
||||
|
||||
export let sortedColors = sortColorsByHue(colors);
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './processTimeSeries';
|
||||
export * from './valueFormats/valueFormats';
|
||||
export * from './colors';
|
||||
|
||||
Reference in New Issue
Block a user