Merge branch 'master' into kbn-formats-refactor

This commit is contained in:
Torkel Ödegaard
2019-01-11 14:33:04 +01:00
105 changed files with 840 additions and 517 deletions

View File

@@ -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"
}

View File

@@ -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();
});
});

View File

@@ -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)}
>
&nbsp;
</i>
);
});
return (
<div className="graph-legend-popover">
<p className="m-b-0">{colorPaletteItems}</p>
</div>
);
}
}

View File

@@ -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>
);
}
}

View File

@@ -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>
);
}
}

View File

@@ -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>
);
}
}

View File

@@ -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>
);
}
}

View File

@@ -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} />;
}
}

View File

@@ -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>
`;

View File

@@ -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>
);

View File

@@ -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;

View File

@@ -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;

View 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;

View File

@@ -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();
});
});

View 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;

View File

@@ -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>
);
}
}

View 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;
}
}

View File

@@ -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>
`;

View File

@@ -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: () => ({}),
};
}

View File

@@ -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)' },
]);
});
});

View File

@@ -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>
);
}
}

View File

@@ -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;
}

View File

@@ -1,3 +1,5 @@
@import 'CustomScrollbar/CustomScrollbar';
@import 'DeleteButton/DeleteButton';
@import 'ThresholdsEditor/ThresholdsEditor';
@import 'Tooltip/Tooltip';
@import 'Select/Select';

View File

@@ -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';

View 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;
}

View File

@@ -1,3 +1,4 @@
export * from './series';
export * from './time';
export * from './panel';
export * from './gauge';

View File

@@ -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;
}

View 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);

View File

@@ -1,2 +1,3 @@
export * from './processTimeSeries';
export * from './valueFormats/valueFormats';
export * from './colors';