Merge pull request #15099 from grafana/tooling/storybook-poc

Named colors & storybook
This commit is contained in:
Torkel Ödegaard 2019-01-28 20:47:46 +01:00 committed by GitHub
commit daab7a5f6e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
81 changed files with 6989 additions and 3830 deletions

View File

@ -0,0 +1,2 @@
import '@storybook/addon-knobs/register';
import '@storybook/addon-actions/register';

View File

@ -0,0 +1,12 @@
import { configure } from '@storybook/react';
import '../../../public/sass/grafana.light.scss';
// automatically import all files ending in *.stories.tsx
const req = require.context('../src/components', true, /.story.tsx$/);
function loadStories() {
req.keys().forEach(req);
}
configure(loadStories, module);

View File

@ -0,0 +1,56 @@
const path = require('path');
module.exports = (baseConfig, env, config) => {
config.module.rules.push({
test: /\.(ts|tsx)$/,
use: [
{
loader: require.resolve('awesome-typescript-loader'),
},
],
});
config.module.rules.push({
test: /\.scss$/,
use: [
{
loader: 'style-loader',
},
{
loader: 'css-loader',
options: {
importLoaders: 2,
url: false,
sourceMap: false,
minimize: false,
},
},
{
loader: 'postcss-loader',
options: {
sourceMap: false,
config: { path: __dirname + '../../../../scripts/webpack/postcss.config.js' },
},
},
{ loader: 'sass-loader', options: { sourceMap: false } },
],
});
config.module.rules.push({
test: require.resolve('jquery'),
use: [
{
loader: 'expose-loader',
query: 'jQuery',
},
{
loader: 'expose-loader',
query: '$',
},
],
});
config.resolve.extensions.push('.ts', '.tsx');
return config;
};

View File

@ -5,19 +5,20 @@
"main": "src/index.ts",
"scripts": {
"tslint": "tslint -c tslint.json --project tsconfig.json",
"typecheck": "tsc --noEmit"
"typecheck": "tsc --noEmit",
"storybook": "start-storybook -p 9001 -c .storybook -s ../../public"
},
"author": "",
"license": "ISC",
"dependencies": {
"@torkelo/react-select": "2.1.1",
"@types/react-test-renderer": "^16.0.3",
"@types/react-transition-group": "^2.0.15",
"@types/react-color": "^2.14.0",
"classnames": "^2.2.5",
"jquery": "^3.2.1",
"lodash": "^4.17.10",
"moment": "^2.22.2",
"react": "^16.6.3",
"react-color": "^2.17.0",
"react-custom-scrollbars": "^4.2.1",
"react-dom": "^16.6.3",
"react-highlight-words": "0.11.0",
@ -29,16 +30,32 @@
"tinycolor2": "^1.4.1"
},
"devDependencies": {
"@storybook/addon-actions": "^4.1.7",
"@storybook/addon-info": "^4.1.6",
"@storybook/addon-knobs": "^4.1.7",
"@storybook/react": "^4.1.4",
"@types/classnames": "^2.2.6",
"@types/jest": "^23.3.2",
"@types/jquery": "^1.10.35",
"@types/lodash": "^4.14.119",
"@types/node": "^10.12.18",
"@types/react": "^16.7.6",
"@types/react-custom-scrollbars": "^4.0.5",
"@types/react-test-renderer": "^16.0.3",
"@types/react-transition-group": "^2.0.15",
"@types/storybook__addon-actions": "^3.4.1",
"@types/storybook__addon-info": "^3.4.2",
"@types/storybook__addon-knobs": "^4.0.0",
"@types/storybook__react": "^4.0.0",
"@types/tether-drop": "^1.4.8",
"@types/tinycolor2": "^1.4.1",
"awesome-typescript-loader": "^5.2.1",
"react-docgen-typescript-loader": "^3.0.0",
"react-docgen-typescript-webpack-plugin": "^1.1.0",
"react-test-renderer": "^16.7.0",
"typescript": "^3.2.2"
},
"resolutions": {
"@types/lodash": "4.14.119"
}
}

View File

@ -0,0 +1,94 @@
import React from 'react';
import { ColorPickerProps } from './ColorPicker';
import tinycolor from 'tinycolor2';
import { debounce } from 'lodash';
interface ColorInputState {
previousColor: string;
value: string;
}
interface ColorInputProps extends ColorPickerProps {
style?: React.CSSProperties;
}
class ColorInput extends React.PureComponent<ColorInputProps, ColorInputState> {
constructor(props: ColorInputProps) {
super(props);
this.state = {
previousColor: props.color,
value: props.color,
};
this.updateColor = debounce(this.updateColor, 100);
}
static getDerivedStateFromProps(props: ColorPickerProps, state: ColorInputState) {
const newColor = tinycolor(props.color);
if (newColor.isValid() && props.color !== state.previousColor) {
return {
...state,
previousColor: props.color,
value: newColor.toString(),
};
}
return state;
}
updateColor = (color: string) => {
this.props.onChange(color);
};
handleChange = (event: React.SyntheticEvent<HTMLInputElement>) => {
const newColor = tinycolor(event.currentTarget.value);
this.setState({
value: event.currentTarget.value,
});
if (newColor.isValid()) {
this.updateColor(newColor.toString());
}
};
handleBlur = () => {
const newColor = tinycolor(this.state.value);
if (!newColor.isValid()) {
this.setState({
value: this.props.color,
});
}
};
render() {
const { value } = this.state;
return (
<div
style={{
display: 'flex',
...this.props.style,
}}
>
<div
style={{
background: this.props.color,
width: '35px',
height: '35px',
flexGrow: 0,
borderRadius: '3px 0 0 3px',
}}
/>
<div
style={{
flexGrow: 1,
}}
>
<input className="gf-form-input" value={value} onChange={this.handleChange} onBlur={this.handleBlur} />
</div>
</div>
);
}
}
export default ColorInput;

View File

@ -0,0 +1,63 @@
import React from 'react';
import { storiesOf } from '@storybook/react';
import { withKnobs, boolean } from '@storybook/addon-knobs';
import { SeriesColorPicker, ColorPicker } from './ColorPicker';
import { action } from '@storybook/addon-actions';
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
import { UseState } from '../../utils/storybook/UseState';
import { getThemeKnob } from '../../utils/storybook/themeKnob';
const getColorPickerKnobs = () => {
return {
selectedTheme: getThemeKnob(),
enableNamedColors: boolean('Enable named colors', false),
};
};
const ColorPickerStories = storiesOf('UI/ColorPicker/Pickers', module);
ColorPickerStories.addDecorator(withCenteredStory).addDecorator(withKnobs);
ColorPickerStories.add('default', () => {
const { selectedTheme, enableNamedColors } = getColorPickerKnobs();
return (
<UseState initialState="#00ff00">
{(selectedColor, updateSelectedColor) => {
return (
<ColorPicker
enableNamedColors={enableNamedColors}
color={selectedColor}
onChange={color => {
action('Color changed')(color);
updateSelectedColor(color);
}}
theme={selectedTheme || undefined}
/>
);
}}
</UseState>
);
});
ColorPickerStories.add('Series color picker', () => {
const { selectedTheme, enableNamedColors } = getColorPickerKnobs();
return (
<UseState initialState="#00ff00">
{(selectedColor, updateSelectedColor) => {
return (
<SeriesColorPicker
enableNamedColors={enableNamedColors}
yaxis={1}
onToggleAxis={() => {}}
color={selectedColor}
onChange={color => updateSelectedColor(color)}
theme={selectedTheme || undefined}
>
<div style={{ color: selectedColor, cursor: 'pointer' }}>Open color picker</div>
</SeriesColorPicker>
);
}}
</UseState>
);
});

View File

@ -1,61 +1,114 @@
import React from 'react';
import ReactDOM from 'react-dom';
import Drop from 'tether-drop';
import React, { Component, createRef } from 'react';
import PopperController from '../Tooltip/PopperController';
import Popper, { RenderPopperArrowFn } from '../Tooltip/Popper';
import { ColorPickerPopover } from './ColorPickerPopover';
import { Themeable, GrafanaTheme } from '../../types';
import { getColorFromHexRgbOrName } from '../../utils/namedColorsPalette';
import { SeriesColorPickerPopover } from './SeriesColorPickerPopover';
import propDeprecationWarning from '../../utils/propDeprecationWarning';
export interface Props {
type ColorPickerChangeHandler = (color: string) => void;
export interface ColorPickerProps extends Themeable {
color: string;
onChange: (c: string) => void;
onChange: ColorPickerChangeHandler;
/**
* @deprecated Use onChange instead
*/
onColorChange?: ColorPickerChangeHandler;
enableNamedColors?: boolean;
withArrow?: boolean;
children?: JSX.Element;
}
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>
);
export const warnAboutColorPickerPropsDeprecation = (componentName: string, props: ColorPickerProps) => {
const { onColorChange } = props;
if (onColorChange) {
propDeprecationWarning(componentName, 'onColorChange', 'onChange');
}
}
};
export const colorPickerFactory = <T extends ColorPickerProps>(
popover: React.ComponentType<T>,
displayName = 'ColorPicker',
renderPopoverArrowFunction?: RenderPopperArrowFn
) => {
return class ColorPicker extends Component<T, any> {
static displayName = displayName;
pickerTriggerRef = createRef<HTMLDivElement>();
handleColorChange = (color: string) => {
const { onColorChange, onChange } = this.props;
const changeHandler = (onColorChange || onChange) as ColorPickerChangeHandler;
return changeHandler(color);
};
render() {
const popoverElement = React.createElement(popover, {
...this.props,
onChange: this.handleColorChange,
});
const { theme, withArrow, children } = this.props;
const renderArrow: RenderPopperArrowFn = ({ arrowProps, placement }) => {
return (
<div
{...arrowProps}
data-placement={placement}
className={`ColorPicker__arrow ColorPicker__arrow--${theme === GrafanaTheme.Light ? 'light' : 'dark'}`}
/>
);
};
return (
<PopperController content={popoverElement} hideAfter={300}>
{(showPopper, hidePopper, popperProps) => {
return (
<>
{this.pickerTriggerRef.current && (
<Popper
{...popperProps}
referenceElement={this.pickerTriggerRef.current}
wrapperClassName="ColorPicker"
renderArrow={withArrow && (renderPopoverArrowFunction || renderArrow)}
onMouseLeave={hidePopper}
onMouseEnter={showPopper}
/>
)}
{children ? (
React.cloneElement(children as JSX.Element, {
ref: this.pickerTriggerRef,
onClick: showPopper,
onMouseLeave: hidePopper,
})
) : (
<div
ref={this.pickerTriggerRef}
onClick={showPopper}
onMouseLeave={hidePopper}
className="sp-replacer sp-light"
>
<div className="sp-preview">
<div
className="sp-preview-inner"
style={{
backgroundColor: getColorFromHexRgbOrName(this.props.color || '#000000', theme),
}}
/>
</div>
</div>
)}
</>
);
}}
</PopperController>
);
}
};
};
export const ColorPicker = colorPickerFactory(ColorPickerPopover, 'ColorPicker');
export const SeriesColorPicker = colorPickerFactory(SeriesColorPickerPopover, 'SeriesColorPicker');

View File

@ -0,0 +1,40 @@
import React from 'react';
import { storiesOf } from '@storybook/react';
import { ColorPickerPopover } from './ColorPickerPopover';
import { withKnobs } from '@storybook/addon-knobs';
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
import { getThemeKnob } from '../../utils/storybook/themeKnob';
import { SeriesColorPickerPopover } from './SeriesColorPickerPopover';
const ColorPickerPopoverStories = storiesOf('UI/ColorPicker/Popovers', module);
ColorPickerPopoverStories.addDecorator(withCenteredStory).addDecorator(withKnobs);
ColorPickerPopoverStories.add('default', () => {
const selectedTheme = getThemeKnob();
return (
<ColorPickerPopover
color="#BC67E6"
onChange={color => {
console.log(color);
}}
theme={selectedTheme || undefined}
/>
);
});
ColorPickerPopoverStories.add('SeriesColorPickerPopover', () => {
const selectedTheme = getThemeKnob();
return (
<SeriesColorPickerPopover
color="#BC67E6"
onChange={color => {
console.log(color);
}}
theme={selectedTheme || undefined}
/>
);
});

View File

@ -0,0 +1,75 @@
import React from 'react';
import { mount, ReactWrapper } from 'enzyme';
import { ColorPickerPopover } from './ColorPickerPopover';
import { getColorDefinitionByName, getNamedColorPalette } from '../../utils/namedColorsPalette';
import { ColorSwatch } from './NamedColorsGroup';
import { flatten } from 'lodash';
import { GrafanaTheme } from '../../types';
const allColors = flatten(Array.from(getNamedColorPalette().values()));
describe('ColorPickerPopover', () => {
const BasicGreen = getColorDefinitionByName('green');
const BasicBlue = getColorDefinitionByName('blue');
describe('rendering', () => {
it('should render provided color as selected if color provided by name', () => {
const wrapper = mount(<ColorPickerPopover color={BasicGreen.name} onChange={() => {}} />);
const selectedSwatch = wrapper.find(ColorSwatch).findWhere(node => node.key() === BasicGreen.name);
const notSelectedSwatches = wrapper.find(ColorSwatch).filterWhere(node => node.prop('isSelected') === false);
expect(selectedSwatch.length).toBe(1);
expect(notSelectedSwatches.length).toBe(allColors.length - 1);
expect(selectedSwatch.prop('isSelected')).toBe(true);
});
it('should render provided color as selected if color provided by hex', () => {
const wrapper = mount(<ColorPickerPopover color={BasicGreen.variants.dark} onChange={() => {}} />);
const selectedSwatch = wrapper.find(ColorSwatch).findWhere(node => node.key() === BasicGreen.name);
const notSelectedSwatches = wrapper.find(ColorSwatch).filterWhere(node => node.prop('isSelected') === false);
expect(selectedSwatch.length).toBe(1);
expect(notSelectedSwatches.length).toBe(allColors.length - 1);
expect(selectedSwatch.prop('isSelected')).toBe(true);
});
});
describe('named colors support', () => {
const onChangeSpy = jest.fn();
let wrapper: ReactWrapper;
afterEach(() => {
wrapper.unmount();
onChangeSpy.mockClear();
});
it('should pass hex color value to onChange prop by default', () => {
wrapper = mount(
<ColorPickerPopover color={BasicGreen.variants.dark} onChange={onChangeSpy} theme={GrafanaTheme.Light} />
);
const basicBlueSwatch = wrapper.find(ColorSwatch).findWhere(node => node.key() === BasicBlue.name);
basicBlueSwatch.simulate('click');
expect(onChangeSpy).toBeCalledTimes(1);
expect(onChangeSpy).toBeCalledWith(BasicBlue.variants.light);
});
it('should pass color name to onChange prop when named colors enabled', () => {
wrapper = mount(
<ColorPickerPopover
enableNamedColors
color={BasicGreen.variants.dark}
onChange={onChangeSpy}
theme={GrafanaTheme.Light}
/>
);
const basicBlueSwatch = wrapper.find(ColorSwatch).findWhere(node => node.key() === BasicBlue.name);
basicBlueSwatch.simulate('click');
expect(onChangeSpy).toBeCalledTimes(1);
expect(onChangeSpy).toBeCalledWith(BasicBlue.name);
});
});
});

View File

@ -1,112 +1,129 @@
import React from 'react';
import $ from 'jquery';
import tinycolor from 'tinycolor2';
import { ColorPalette } from './ColorPalette';
import { SpectrumPicker } from './SpectrumPicker';
import { NamedColorsPalette } from './NamedColorsPalette';
import { getColorName, getColorFromHexRgbOrName } from '../../utils/namedColorsPalette';
import { ColorPickerProps, warnAboutColorPickerPropsDeprecation } from './ColorPicker';
import { GrafanaTheme } from '../../types';
import { PopperContentProps } from '../Tooltip/PopperController';
import SpectrumPalette from './SpectrumPalette';
const DEFAULT_COLOR = '#000000';
export interface Props {
color: string;
onColorSelect: (c: string) => void;
export interface Props<T> extends ColorPickerProps, PopperContentProps {
customPickers?: T;
}
export class ColorPickerPopover extends React.Component<Props, any> {
pickerNavElem: any;
type PickerType = 'palette' | 'spectrum';
constructor(props: Props) {
interface CustomPickersDescriptor {
[key: string]: {
tabComponent: React.ComponentType<ColorPickerProps>;
name: string;
};
}
interface State<T> {
activePicker: PickerType | keyof T;
}
export class ColorPickerPopover<T extends CustomPickersDescriptor> extends React.Component<Props<T>, State<T>> {
constructor(props: Props<T>) {
super(props);
this.state = {
tab: 'palette',
color: this.props.color || DEFAULT_COLOR,
colorString: this.props.color || DEFAULT_COLOR,
activePicker: 'palette',
};
warnAboutColorPickerPropsDeprecation('ColorPickerPopover', props);
}
setPickerNavElem(elem: any) {
this.pickerNavElem = $(elem);
}
getTabClassName = (tabName: PickerType | keyof T) => {
const { activePicker } = this.state;
return `ColorPickerPopover__tab ${activePicker === tabName && 'ColorPickerPopover__tab--active'}`;
};
setColor(color: string) {
const newColor = tinycolor(color);
if (newColor.isValid()) {
this.setState({ color: newColor.toString(), colorString: newColor.toString() });
this.props.onColorSelect(color);
handleChange = (color: any) => {
const { onColorChange, onChange, enableNamedColors, theme } = this.props;
const changeHandler = onColorChange || onChange;
if (enableNamedColors) {
return changeHandler(color);
}
}
changeHandler(getColorFromHexRgbOrName(color, theme));
};
sampleColorSelected(color: string) {
this.setColor(color);
}
handleTabChange = (tab: PickerType | keyof T) => {
return () => this.setState({ activePicker: tab });
};
spectrumColorSelected(color: any) {
const rgbColor = color.toRgbString();
this.setColor(rgbColor);
}
renderPicker = () => {
const { activePicker } = this.state;
const { color, theme } = this.props;
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);
switch (activePicker) {
case 'spectrum':
return <SpectrumPalette color={color} onChange={this.handleChange} theme={theme} />;
case 'palette':
return <NamedColorsPalette color={getColorName(color, theme)} onChange={this.handleChange} theme={theme} />;
default:
return this.renderCustomPicker(activePicker);
}
}
};
onColorStringBlur(e: any) {
const colorString = e.target.value;
this.setColor(colorString);
}
renderCustomPicker = (tabKey: keyof T) => {
const { customPickers, color, theme } = this.props;
if (!customPickers) {
return null;
}
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 });
return React.createElement(customPickers[tabKey].tabComponent, {
color,
theme,
onChange: this.handleChange,
});
}
};
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;
renderCustomPickerTabs = () => {
const { customPickers } = this.props;
if (!customPickers) {
return null;
}
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)}
/>
<>
{Object.keys(customPickers).map(key => {
return (
<div
className={this.getTabClassName(key)}
onClick={this.handleTabChange(key)}
key={key}
>
{customPickers[key].name}
</div>
);
})}
</>
);
};
render() {
const { theme } = this.props;
const colorPickerTheme = theme || GrafanaTheme.Dark;
return (
<div className={`ColorPickerPopover ColorPickerPopover--${colorPickerTheme}`}>
<div className="ColorPickerPopover__tabs">
<div
className={this.getTabClassName('palette')}
onClick={this.handleTabChange('palette')}
>
Colors
</div>
<div
className={this.getTabClassName('spectrum')}
onClick={this.handleTabChange('spectrum')}
>
Custom
</div>
{this.renderCustomPickerTabs()}
</div>
<div className="ColorPickerPopover__content">{this.renderPicker()}</div>
</div>
);
}

View File

@ -0,0 +1,110 @@
import React, { FunctionComponent } from 'react';
import { Themeable, GrafanaTheme } from '../../types';
import { ColorDefinition, getColorForTheme } from '../../utils/namedColorsPalette';
import { Color } from 'csstype';
import { find, upperFirst } from 'lodash';
type ColorChangeHandler = (color: ColorDefinition) => void;
export enum ColorSwatchVariant {
Small = 'small',
Large = 'large',
}
interface ColorSwatchProps extends Themeable, React.DOMAttributes<HTMLDivElement> {
color: string;
label?: string;
variant?: ColorSwatchVariant;
isSelected?: boolean;
}
export const ColorSwatch: FunctionComponent<ColorSwatchProps> = ({
color,
label,
variant = ColorSwatchVariant.Small,
isSelected,
theme,
...otherProps
}) => {
const isSmall = variant === ColorSwatchVariant.Small;
const swatchSize = isSmall ? '16px' : '32px';
const selectedSwatchBorder = theme === GrafanaTheme.Light ? '#ffffff' : '#1A1B1F';
const swatchStyles = {
width: swatchSize,
height: swatchSize,
borderRadius: '50%',
background: `${color}`,
marginRight: isSmall ? '0px' : '8px',
boxShadow: isSelected ? `inset 0 0 0 2px ${color}, inset 0 0 0 4px ${selectedSwatchBorder}` : 'none',
};
return (
<div
style={{
display: 'flex',
alignItems: 'center',
cursor: 'pointer',
}}
{...otherProps}
>
<div style={swatchStyles} />
{variant === ColorSwatchVariant.Large && <span>{label}</span>}
</div>
);
};
interface NamedColorsGroupProps extends Themeable {
colors: ColorDefinition[];
selectedColor?: Color;
onColorSelect: ColorChangeHandler;
key?: string;
}
const NamedColorsGroup: FunctionComponent<NamedColorsGroupProps> = ({
colors,
selectedColor,
onColorSelect,
theme,
...otherProps
}) => {
const primaryColor = find(colors, color => !!color.isPrimary);
return (
<div {...otherProps} style={{ display: 'flex', flexDirection: 'column' }}>
{primaryColor && (
<ColorSwatch
key={primaryColor.name}
isSelected={primaryColor.name === selectedColor}
variant={ColorSwatchVariant.Large}
color={getColorForTheme(primaryColor, theme)}
label={upperFirst(primaryColor.hue)}
onClick={() => onColorSelect(primaryColor)}
theme={theme}
/>
)}
<div
style={{
display: 'flex',
marginTop: '8px',
}}
>
{colors.map(
color =>
!color.isPrimary && (
<div key={color.name} style={{ marginRight: '4px' }}>
<ColorSwatch
key={color.name}
isSelected={color.name === selectedColor}
color={getColorForTheme(color, theme)}
onClick={() => onColorSelect(color)}
theme={theme}
/>
</div>
)
)}
</div>
</div>
);
};
export default NamedColorsGroup;

View File

@ -0,0 +1,52 @@
import React from 'react';
import { storiesOf } from '@storybook/react';
import { NamedColorsPalette } from './NamedColorsPalette';
import { getColorName, getColorDefinitionByName } from '../../utils/namedColorsPalette';
import { withKnobs, select } from '@storybook/addon-knobs';
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
import { UseState } from '../../utils/storybook/UseState';
const BasicGreen = getColorDefinitionByName('green');
const BasicBlue = getColorDefinitionByName('blue');
const LightBlue = getColorDefinitionByName('light-blue');
const NamedColorsPaletteStories = storiesOf('UI/ColorPicker/Palettes/NamedColorsPalette', module);
NamedColorsPaletteStories.addDecorator(withKnobs).addDecorator(withCenteredStory);
NamedColorsPaletteStories.add('Named colors swatch - support for named colors', () => {
const selectedColor = select(
'Selected color',
{
Green: 'green',
Red: 'red',
'Light blue': 'light-blue',
},
'red'
);
return (
<UseState initialState={selectedColor}>
{(selectedColor, updateSelectedColor) => {
return <NamedColorsPalette color={selectedColor} onChange={updateSelectedColor} />;
}}
</UseState>
);
}).add('Named colors swatch - support for hex values', () => {
const selectedColor = select(
'Selected color',
{
Green: BasicGreen.variants.dark,
Red: BasicBlue.variants.dark,
'Light blue': LightBlue.variants.dark,
},
'red'
);
return (
<UseState initialState={selectedColor}>
{(selectedColor, updateSelectedColor) => {
return <NamedColorsPalette color={getColorName(selectedColor)} onChange={updateSelectedColor} />;
}}
</UseState>
);
});

View File

@ -0,0 +1,36 @@
import React from 'react';
import { mount, ReactWrapper } from 'enzyme';
import { NamedColorsPalette } from './NamedColorsPalette';
import { ColorSwatch } from './NamedColorsGroup';
import { getColorDefinitionByName } from '../../utils';
import { GrafanaTheme } from '../../types';
describe('NamedColorsPalette', () => {
const BasicGreen = getColorDefinitionByName('green');
describe('theme support for named colors', () => {
let wrapper: ReactWrapper, selectedSwatch;
afterEach(() => {
wrapper.unmount();
});
it('should render provided color variant specific for theme', () => {
wrapper = mount(<NamedColorsPalette color={BasicGreen.name} theme={GrafanaTheme.Dark} onChange={() => {}} />);
selectedSwatch = wrapper.find(ColorSwatch).findWhere(node => node.key() === BasicGreen.name);
expect(selectedSwatch.prop('color')).toBe(BasicGreen.variants.dark);
wrapper.unmount();
wrapper = mount(<NamedColorsPalette color={BasicGreen.name} theme={GrafanaTheme.Light} onChange={() => {}} />);
selectedSwatch = wrapper.find(ColorSwatch).findWhere(node => node.key() === BasicGreen.name);
expect(selectedSwatch.prop('color')).toBe(BasicGreen.variants.light);
});
it('should render dar variant of provided color when theme not provided', () => {
wrapper = mount(<NamedColorsPalette color={BasicGreen.name} onChange={() => {}} />);
selectedSwatch = wrapper.find(ColorSwatch).findWhere(node => node.key() === BasicGreen.name);
expect(selectedSwatch.prop('color')).toBe(BasicGreen.variants.dark);
});
});
});

View File

@ -0,0 +1,39 @@
import React from 'react';
import { Color, getNamedColorPalette } from '../../utils/namedColorsPalette';
import { Themeable } from '../../types/index';
import NamedColorsGroup from './NamedColorsGroup';
interface NamedColorsPaletteProps extends Themeable {
color?: Color;
onChange: (colorName: string) => void;
}
export const NamedColorsPalette = ({ color, onChange, theme }: NamedColorsPaletteProps) => {
const swatches: JSX.Element[] = [];
getNamedColorPalette().forEach((colors, hue) => {
swatches.push(
<NamedColorsGroup
key={hue}
theme={theme}
selectedColor={color}
colors={colors}
onColorSelect={color => {
onChange(color.name);
}}
/>
);
});
return (
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(3, 1fr)',
gridRowGap: '24px',
gridColumnGap: '24px',
}}
>
{swatches}
</div>
);
};

View File

@ -1,85 +0,0 @@
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

@ -1,23 +1,44 @@
import React from 'react';
import { ColorPickerPopover } from './ColorPickerPopover';
import React, { FunctionComponent } from 'react';
export interface SeriesColorPickerPopoverProps {
color: string;
import { ColorPickerPopover } from './ColorPickerPopover';
import { ColorPickerProps } from './ColorPicker';
import { PopperContentProps } from '../Tooltip/PopperController';
import { Switch } from '../Switch/Switch';
export interface SeriesColorPickerPopoverProps extends ColorPickerProps, PopperContentProps {
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>
);
}
}
export const SeriesColorPickerPopover: FunctionComponent<SeriesColorPickerPopoverProps> = props => {
const { yaxis, onToggleAxis, color, ...colorPickerProps } = props;
return (
<ColorPickerPopover
{...colorPickerProps}
color={color || '#000000'}
customPickers={{
yaxis: {
name: 'Y-Axis',
tabComponent: () => (
<Switch
key="yaxisSwitch"
label="Use right y-axis"
className="ColorPicker__axisSwitch"
labelClass="ColorPicker__axisSwitchLabel"
checked={yaxis === 2}
onChange={() => {
if (onToggleAxis) {
onToggleAxis();
}
}}
/>
),
},
}}
/>
);
};
interface AxisSelectorProps {
yaxis: number;

View File

@ -0,0 +1,23 @@
import React from 'react';
import { storiesOf } from '@storybook/react';
import { withKnobs } from '@storybook/addon-knobs';
import SpectrumPalette from './SpectrumPalette';
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
import { UseState } from '../../utils/storybook/UseState';
import { getThemeKnob } from '../../utils/storybook/themeKnob';
const SpectrumPaletteStories = storiesOf('UI/ColorPicker/Palettes/SpectrumPalette', module);
SpectrumPaletteStories.addDecorator(withCenteredStory).addDecorator(withKnobs);
SpectrumPaletteStories.add('Named colors swatch - support for named colors', () => {
const selectedTheme = getThemeKnob();
return (
<UseState initialState="red">
{(selectedColor, updateSelectedColor) => {
return <SpectrumPalette theme={selectedTheme} color={selectedColor} onChange={updateSelectedColor} />;
}}
</UseState>
);
});

View File

@ -0,0 +1,100 @@
import React from 'react';
import { CustomPicker, ColorResult } from 'react-color';
import { Saturation, Hue, Alpha } from 'react-color/lib/components/common';
import { getColorFromHexRgbOrName } from '../../utils/namedColorsPalette';
import tinycolor from 'tinycolor2';
import ColorInput from './ColorInput';
import { Themeable, GrafanaTheme } from '../../types';
import SpectrumPalettePointer, { SpectrumPalettePointerProps } from './SpectrumPalettePointer';
export interface SpectrumPaletteProps extends Themeable {
color: string;
onChange: (color: string) => void;
}
const renderPointer = (theme?: GrafanaTheme) => (props: SpectrumPalettePointerProps) => (
<SpectrumPalettePointer {...props} theme={theme} />
);
// @ts-ignore
const SpectrumPicker = CustomPicker<Themeable>(({ rgb, hsl, onChange, theme }) => {
return (
<div
style={{
display: 'flex',
width: '100%',
flexDirection: 'column',
}}
>
<div
style={{
display: 'flex',
}}
>
<div
style={{
display: 'flex',
flexGrow: 1,
flexDirection: 'column',
}}
>
<div
style={{
position: 'relative',
height: '100px',
width: '100%',
}}
>
{/*
// @ts-ignore */}
<Saturation onChange={onChange} hsl={hsl} hsv={tinycolor(hsl).toHsv()} />
</div>
<div
style={{
width: '100%',
height: '16px',
marginTop: '16px',
position: 'relative',
background: 'white',
}}
>
{/*
// @ts-ignore */}
<Alpha rgb={rgb} hsl={hsl} a={rgb.a} onChange={onChange} pointer={renderPointer(theme)} />
</div>
</div>
<div
style={{
position: 'relative',
width: '16px',
height: '100px',
marginLeft: '16px',
}}
>
{/*
// @ts-ignore */}
<Hue onChange={onChange} hsl={hsl} direction="vertical" pointer={renderPointer(theme)} />
</div>
</div>
</div>
);
});
const SpectrumPalette: React.FunctionComponent<SpectrumPaletteProps> = ({ color, onChange, theme }) => {
return (
<div>
<SpectrumPicker
color={tinycolor(getColorFromHexRgbOrName(color)).toRgb()}
onChange={(a: ColorResult) => {
onChange(tinycolor(a.rgb).toString());
}}
theme={theme}
/>
<ColorInput color={color} onChange={onChange} style={{ marginTop: '16px' }} />
</div>
);
};
export default SpectrumPalette;

View File

@ -0,0 +1,80 @@
import React from 'react';
import { GrafanaTheme, Themeable } from '../../types';
export interface SpectrumPalettePointerProps extends Themeable {
direction?: string;
}
const SpectrumPalettePointer: React.FunctionComponent<SpectrumPalettePointerProps> = ({
theme,
direction,
}) => {
const styles = {
picker: {
width: '16px',
height: '16px',
transform: direction === 'vertical' ? 'translate(0, -8px)' : 'translate(-8px, 0)',
},
};
const pointerColor = theme === GrafanaTheme.Light ? '#3F444D' : '#8E8E8E';
let pointerStyles: React.CSSProperties = {
position: 'absolute',
left: '6px',
width: '0',
height: '0',
borderStyle: 'solid',
background: 'none',
};
let topArrowStyles: React.CSSProperties = {
top: '-7px',
borderWidth: '6px 3px 0px 3px',
borderColor: `${pointerColor} transparent transparent transparent`,
};
let bottomArrowStyles: React.CSSProperties = {
bottom: '-7px',
borderWidth: '0px 3px 6px 3px',
borderColor: ` transparent transparent ${pointerColor} transparent`,
};
if (direction === 'vertical') {
pointerStyles = {
...pointerStyles,
left: 'auto',
};
topArrowStyles = {
borderWidth: '3px 0px 3px 6px',
borderColor: `transparent transparent transparent ${pointerColor}`,
left: '-7px',
top: '7px',
};
bottomArrowStyles = {
borderWidth: '3px 6px 3px 0px',
borderColor: `transparent ${pointerColor} transparent transparent`,
right: '-7px',
top: '7px',
};
}
return (
<div style={styles.picker}>
<div
style={{
...pointerStyles,
...topArrowStyles,
}}
/>
<div
style={{
...pointerStyles,
...bottomArrowStyles,
}}
/>
</div>
);
};
export default SpectrumPalettePointer;

View File

@ -1,72 +0,0 @@
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

@ -1,8 +1,172 @@
$arrowSize: 15px;
.ColorPicker {
@extend .popper;
font-size: 12px;
}
.ColorPicker__arrow {
width: 0;
height: 0;
border-style: solid;
position: absolute;
margin: 0px;
&[data-placement^='top'] {
border-width: $arrowSize $arrowSize 0 $arrowSize;
border-left-color: transparent;
border-right-color: transparent;
border-bottom-color: transparent;
bottom: -$arrowSize;
left: calc(50%-#{$arrowSize});
padding-top: $arrowSize;
}
&[data-placement^='bottom'] {
border-width: 0 $arrowSize $arrowSize $arrowSize;
border-left-color: transparent;
border-right-color: transparent;
border-top-color: transparent;
top: 0;
left: calc(50%-#{$arrowSize});
}
&[data-placement^='bottom-start'] {
border-width: 0 $arrowSize $arrowSize $arrowSize;
border-left-color: transparent;
border-right-color: transparent;
border-top-color: transparent;
top: 0;
left: $arrowSize;
}
&[data-placement^='bottom-end'] & {
border-width: 0 $arrowSize $arrowSize $arrowSize;
border-left-color: transparent;
border-right-color: transparent;
border-top-color: transparent;
top: 0;
left: calc(100% -$arrowSize);
}
&[data-placement^='right'] {
border-width: $arrowSize $arrowSize $arrowSize 0;
border-left-color: transparent;
border-top-color: transparent;
border-bottom-color: transparent;
left: 0;
top: calc(50%-#{$arrowSize});
}
&[data-placement^='left'] {
border-width: $arrowSize 0 $arrowSize $arrowSize;
border-top-color: transparent;
border-right-color: transparent;
border-bottom-color: transparent;
right: -$arrowSize;
top: calc(50%-#{$arrowSize});
}
}
.ColorPicker__arrow--light {
border-color: #ffffff;
}
.ColorPicker__arrow--dark {
border-color: #1e2028;
}
// Top
.ColorPicker[data-placement^='top'] {
padding-bottom: $arrowSize;
}
// Bottom
.ColorPicker[data-placement^='bottom'] {
padding-top: $arrowSize;
}
.ColorPicker[data-placement^='bottom-start'] {
padding-top: $arrowSize;
}
.ColorPicker[data-placement^='bottom-end'] {
padding-top: $arrowSize;
}
// Right
.ColorPicker[data-placement^='right'] {
padding-left: $arrowSize;
}
// Left
.ColorPicker[data-placement^='left'] {
padding-right: $arrowSize;
}
.ColorPickerPopover {
border-radius: 3px;
}
.ColorPickerPopover--light {
color: black;
background: linear-gradient(180deg, #ffffff 0%, #f7f8fa 104.25%);
box-shadow: 0px 2px 4px #dde4ed, 0px 0px 2px #dde4ed;
}
.ColorPickerPopover--dark {
color: #d8d9da;
background: linear-gradient(180deg, #1e2028 0%, #161719 104.25%);
box-shadow: 0px 2px 4px #000000, 0px 0px 2px #000000;
.ColorPickerPopover__tab {
background: #303133;
color: white;
cursor: pointer;
}
.ColorPickerPopover__tab--active {
background: none;
}
}
.ColorPickerPopover__content {
width: 336px;
min-height: 184px;
padding: 24px;
}
.ColorPickerPopover__tabs {
display: flex;
width: 100%;
border-radius: 3px 3px 0 0;
overflow: hidden;
}
.ColorPickerPopover__tab {
width: 50%;
text-align: center;
padding: 8px 0;
background: #dde4ed;
}
.ColorPickerPopover__tab--active {
background: white;
}
.ColorPicker__axisSwitch {
width: 100%;
}
.ColorPicker__axisSwitchLabel {
display: flex;
flex-grow: 1;
}
.sp-replacer {
background: inherit;
border: none;
color: inherit;
padding: 0;
border-radius: 10px;
}
.sp-replacer:hover,
@ -35,10 +199,22 @@
margin: 0;
float: left;
z-index: 0;
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg==);
}
.sp-preview-inner,
.sp-alpha-inner,
.sp-thumb-inner {
display: block;
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
}
.gf-color-picker__body {
padding-bottom: 10px;
padding-bottom: $arrowSize;
padding-left: 6px;
}
@ -47,3 +223,18 @@
width: 210px;
}
}
// TODO: Remove. This is a temporary solution until color picker popovers are used
// with Drop.js.
.drop-popover.drop-popover--transparent {
.drop-content {
border: none;
background: none;
padding: 0;
max-width: none;
&:before {
display: none;
}
}
}

View File

@ -9,6 +9,8 @@ interface Props {
autoHideDuration?: number;
autoHeightMax?: string;
hideTracksWhenNotNeeded?: boolean;
renderTrackHorizontal?: React.FunctionComponent<any>;
renderTrackVertical?: React.FunctionComponent<any>;
scrollTop?: number;
setScrollTop: (event: any) => void;
autoHeightMin?: number | string;
@ -66,6 +68,8 @@ export class CustomScrollbar extends PureComponent<Props> {
autoHide,
autoHideTimeout,
hideTracksWhenNotNeeded,
renderTrackHorizontal,
renderTrackVertical,
} = this.props;
return (
@ -81,8 +85,8 @@ export class CustomScrollbar extends PureComponent<Props> {
// Before these where set to inhert but that caused problems with cut of legends in firefox
autoHeightMax={autoHeightMax}
autoHeightMin={autoHeightMin}
renderTrackHorizontal={props => <div {...props} className="track-horizontal" />}
renderTrackVertical={props => <div {...props} className="track-vertical" />}
renderTrackHorizontal={renderTrackHorizontal || (props => <div {...props} className="track-horizontal" />)}
renderTrackVertical={renderTrackVertical || (props => <div {...props} className="track-vertical" />)}
renderThumbHorizontal={props => <div {...props} className="thumb-horizontal" />}
renderThumbVertical={props => <div {...props} className="thumb-vertical" />}
renderView={props => <div {...props} className="view" />}

View File

@ -0,0 +1,24 @@
import React, { FunctionComponent } from 'react';
import { storiesOf } from '@storybook/react';
import { DeleteButton } from '@grafana/ui';
const CenteredStory: FunctionComponent<{}> = ({ children }) => {
return (
<div
style={{
height: '100vh ',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
{children}
</div>
);
};
storiesOf('UI/DeleteButton', module)
.addDecorator(story => <CenteredStory>{story()}</CenteredStory>)
.add('default', () => {
return <DeleteButton onConfirm={() => {}} />;
});

View File

@ -1,10 +1,14 @@
import React, { PureComponent } from 'react';
import $ from 'jquery';
import { ValueMapping, Threshold, ThemeName, BasicGaugeColor, ThemeNames } from '../../types/panel';
import { ValueMapping, Threshold, BasicGaugeColor } from '../../types/panel';
import { TimeSeriesVMs } from '../../types/series';
import { GrafanaTheme } from '../../types';
import { getMappedValue } from '../../utils/valueMappings';
import { getValueFormat } from '../../utils/valueFormats/valueFormats';
import { TimeSeriesValue, getMappedValue } from '../../utils/valueMappings';
import { getColorFromHexRgbOrName } from '../../utils/namedColorsPalette';
type TimeSeriesValue = string | number | null;
export interface Props {
decimals: number;
@ -21,7 +25,7 @@ export interface Props {
suffix: string;
unit: string;
width: number;
theme?: ThemeName;
theme?: GrafanaTheme;
}
export class Gauge extends PureComponent<Props> {
@ -38,7 +42,7 @@ export class Gauge extends PureComponent<Props> {
thresholds: [],
unit: 'none',
stat: 'avg',
theme: ThemeNames.Dark,
theme: GrafanaTheme.Dark,
};
componentDidMount() {
@ -71,29 +75,29 @@ export class Gauge extends PureComponent<Props> {
}
getFontColor(value: TimeSeriesValue) {
const { thresholds } = this.props;
const { thresholds, theme } = this.props;
if (thresholds.length === 1) {
return thresholds[0].color;
return getColorFromHexRgbOrName(thresholds[0].color, theme);
}
const atThreshold = thresholds.filter(threshold => (value as number) === threshold.value)[0];
if (atThreshold) {
return atThreshold.color;
return getColorFromHexRgbOrName(atThreshold.color, theme);
}
const belowThreshold = thresholds.filter(threshold => (value as number) > threshold.value);
if (belowThreshold.length > 0) {
const nearestThreshold = belowThreshold.sort((t1, t2) => t2.value - t1.value)[0];
return nearestThreshold.color;
return getColorFromHexRgbOrName(nearestThreshold.color, theme);
}
return BasicGaugeColor.Red;
}
getFormattedThresholds() {
const { maxValue, minValue, thresholds } = this.props;
const { maxValue, minValue, thresholds, theme } = this.props;
const thresholdsSortedByIndex = [...thresholds].sort((t1, t2) => t1.index - t2.index);
const lastThreshold = thresholdsSortedByIndex[thresholdsSortedByIndex.length - 1];
@ -101,13 +105,13 @@ export class Gauge extends PureComponent<Props> {
const formattedThresholds = [
...thresholdsSortedByIndex.map(threshold => {
if (threshold.index === 0) {
return { value: minValue, color: threshold.color };
return { value: minValue, color: getColorFromHexRgbOrName(threshold.color, theme) };
}
const previousThreshold = thresholdsSortedByIndex[threshold.index - 1];
return { value: threshold.value, color: previousThreshold.color };
return { value: threshold.value, color: getColorFromHexRgbOrName(previousThreshold.color, theme) };
}),
{ value: maxValue, color: lastThreshold.color },
{ value: maxValue, color: getColorFromHexRgbOrName(lastThreshold.color, theme) },
];
return formattedThresholds;
@ -135,7 +139,7 @@ export class Gauge extends PureComponent<Props> {
}
const dimension = Math.min(width, height * 1.3);
const backgroundColor = theme === ThemeNames.Light ? 'rgb(230,230,230)' : 'rgb(38,38,38)';
const backgroundColor = theme === GrafanaTheme.Light ? 'rgb(230,230,230)' : 'rgb(38,38,38)';
const fontScale = parseInt('80', 10) / 100;
const fontSize = Math.min(dimension / 5, 100) * fontScale;
const gaugeWidthReduceRatio = showThresholdLabels ? 1.5 : 1;

View File

@ -3,6 +3,7 @@ import renderer from 'react-test-renderer';
import SelectOption from './SelectOption';
import { OptionProps } from 'react-select/lib/components/Option';
// @ts-ignore
const model: OptionProps<any> = {
data: jest.fn(),
cx: jest.fn(),

View File

@ -4,10 +4,11 @@ import _ from 'lodash';
export interface Props {
label: string;
checked: boolean;
className?: string;
labelClass?: string;
switchClass?: string;
transparent?: boolean;
onChange: (event) => any;
onChange: (event?: React.SyntheticEvent<HTMLInputElement>) => void;
}
export interface State {
@ -19,20 +20,21 @@ export class Switch extends PureComponent<Props, State> {
id: _.uniqueId(),
};
internalOnChange = event => {
internalOnChange = (event: React.FormEvent<HTMLInputElement>) => {
event.stopPropagation();
this.props.onChange(event);
this.props.onChange();
};
render() {
const { labelClass = '', switchClass = '', label, checked, transparent } = this.props;
const { labelClass = '', switchClass = '', label, checked, transparent, className } = this.props;
const labelId = `check-${this.state.id}`;
const labelClassName = `gf-form-label ${labelClass} ${transparent ? 'gf-form-label--transparent' : ''} pointer`;
const switchClassName = `gf-form-switch ${switchClass} ${transparent ? 'gf-form-switch--transparent' : ''}`;
return (
<label htmlFor={labelId} className="gf-form gf-form-switch-container">
<label htmlFor={labelId} className={`gf-form gf-form-switch-container ${className}`}>
{label && <div className={labelClassName}>{label}</div>}
<div className={switchClassName}>
<input id={labelId} type="checkbox" checked={checked} onChange={this.internalOnChange} />

View File

@ -1,12 +1,11 @@
import React, { PureComponent } from 'react';
// import tinycolor, { ColorInput } from 'tinycolor2';
import { Threshold } from '../../types';
import { Threshold, Themeable } from '../../types';
import { ColorPicker } from '../ColorPicker/ColorPicker';
import { PanelOptionsGroup } from '../PanelOptionsGroup/PanelOptionsGroup';
import { colors } from '../../utils';
import { getColorFromHexRgbOrName } from '@grafana/ui';
export interface Props {
export interface Props extends Themeable {
thresholds: Threshold[];
onChange: (thresholds: Threshold[]) => void;
}
@ -189,6 +188,7 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
render() {
const { thresholds } = this.state;
const { theme } = this.props;
return (
<PanelOptionsGroup title="Thresholds">
@ -199,7 +199,10 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
<div className="thresholds-row-add-button" onClick={() => this.onAddThreshold(threshold.index + 1)}>
<i className="fa fa-plus" />
</div>
<div className="thresholds-row-color-indicator" style={{ backgroundColor: threshold.color }} />
<div
className="thresholds-row-color-indicator"
style={{ backgroundColor: getColorFromHexRgbOrName(threshold.color, theme) }}
/>
<div className="thresholds-row-input">{this.renderInput(threshold)}</div>
</div>
);

View File

@ -1,73 +1,88 @@
import React, { PureComponent } from 'react';
import * as PopperJS from 'popper.js';
import { Manager, Popper as ReactPopper } from 'react-popper';
import { Manager, Popper as ReactPopper, PopperArrowProps } from 'react-popper';
import { Portal } from '@grafana/ui';
import Transition from 'react-transition-group/Transition';
export enum Themes {
Default = 'popper__background--default',
Error = 'popper__background--error',
Brand = 'popper__background--brand',
}
import { PopperContent } from './PopperController';
const defaultTransitionStyles = {
transition: 'opacity 200ms linear',
opacity: 0,
};
const transitionStyles: {[key: string]: object} = {
const transitionStyles: { [key: string]: object } = {
exited: { opacity: 0 },
entering: { opacity: 0 },
entered: { opacity: 1 },
exiting: { opacity: 0 },
entered: { opacity: 1, transitionDelay: '0s' },
exiting: { opacity: 0, transitionDelay: '500ms' },
};
interface Props extends React.DOMAttributes<HTMLDivElement> {
renderContent: (content: any) => any;
export type RenderPopperArrowFn = (
props: {
arrowProps: PopperArrowProps;
placement: string;
}
) => JSX.Element;
interface Props extends React.HTMLAttributes<HTMLDivElement> {
show: boolean;
placement?: PopperJS.Placement;
content: string | ((props: any) => JSX.Element);
content: PopperContent<any>;
referenceElement: PopperJS.ReferenceObject;
theme?: Themes;
wrapperClassName?: string;
renderArrow?: RenderPopperArrowFn;
}
class Popper extends PureComponent<Props> {
render() {
const { renderContent, show, placement, onMouseEnter, onMouseLeave, theme } = this.props;
const { show, placement, onMouseEnter, onMouseLeave, className, wrapperClassName, renderArrow } = this.props;
const { content } = this.props;
const popperBackgroundClassName = 'popper__background' + (theme ? ' ' + theme : '');
return (
<Manager>
<Transition in={show} timeout={100} mountOnEnter={true} unmountOnExit={true}>
{transitionState => (
<Portal>
<ReactPopper placement={placement} referenceElement={this.props.referenceElement}>
{({ ref, style, placement, arrowProps }) => {
return (
<div
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
ref={ref}
style={{
...style,
...defaultTransitionStyles,
...transitionStyles[transitionState],
}}
data-placement={placement}
className="popper"
>
<div className={popperBackgroundClassName}>
{renderContent(content)}
<div ref={arrowProps.ref} data-placement={placement} className="popper__arrow" />
{transitionState => {
return (
<Portal>
<ReactPopper
placement={placement}
referenceElement={this.props.referenceElement}
// TODO: move modifiers config to popper controller
modifiers={{ preventOverflow: { enabled: true, boundariesElement: 'window' } }}
>
{({ ref, style, placement, arrowProps, scheduleUpdate }) => {
return (
<div
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
ref={ref}
style={{
...style,
...defaultTransitionStyles,
...transitionStyles[transitionState],
}}
data-placement={placement}
className={`${wrapperClassName}`}
>
<div className={className}>
{typeof content === 'string'
? content
: React.cloneElement(content, {
updatePopperPosition: scheduleUpdate,
})}
{renderArrow &&
renderArrow({
arrowProps,
placement,
})}
</div>
</div>
</div>
);
}}
</ReactPopper>
</Portal>
)}
);
}}
</ReactPopper>
</Portal>
);
}}
</Transition>
</Manager>
);

View File

@ -1,16 +1,19 @@
import React from 'react';
import * as PopperJS from 'popper.js';
import { Themes } from './Popper';
type PopperContent = string | (() => JSX.Element);
// This API allows popovers to update Popper's position when e.g. popover content changes
// updatePopperPosition is delivered to content by react-popper
export interface PopperContentProps {
updatePopperPosition?: () => void;
}
export type PopperContent<T extends PopperContentProps> = string | React.ReactElement<T>;
export interface UsingPopperProps {
show?: boolean;
placement?: PopperJS.Placement;
content: PopperContent;
content: PopperContent<any>;
children: JSX.Element;
renderContent?: (content: PopperContent) => JSX.Element;
theme?: Themes;
}
type PopperControllerRenderProp = (
@ -19,18 +22,16 @@ type PopperControllerRenderProp = (
popperProps: {
show: boolean;
placement: PopperJS.Placement;
content: string | ((props: any) => JSX.Element);
renderContent: (content: any) => any;
theme?: Themes;
content: PopperContent<any>;
}
) => JSX.Element;
interface Props {
placement?: PopperJS.Placement;
content: PopperContent;
content: PopperContent<any>;
className?: string;
children: PopperControllerRenderProp;
theme?: Themes;
hideAfter?: number;
}
interface State {
@ -39,6 +40,8 @@ interface State {
}
class PopperController extends React.Component<Props, State> {
private hideTimeout: any;
constructor(props: Props) {
super(props);
@ -60,6 +63,10 @@ class PopperController extends React.Component<Props, State> {
}
showPopper = () => {
if (this.hideTimeout) {
clearTimeout(this.hideTimeout);
}
this.setState(prevState => ({
...prevState,
show: true,
@ -67,31 +74,29 @@ class PopperController extends React.Component<Props, State> {
};
hidePopper = () => {
if (this.props.hideAfter !== 0) {
this.hideTimeout = setTimeout(() => {
this.setState(prevState => ({
...prevState,
show: false,
}));
}, this.props.hideAfter);
return;
}
this.setState(prevState => ({
...prevState,
show: false,
}));
};
renderContent(content: PopperContent) {
if (typeof content === 'function') {
// If it's a function we assume it's a React component
const ReactComponent = content;
return <ReactComponent />;
}
return content;
}
render() {
const { children, content, theme } = this.props;
const { children, content } = this.props;
const { show, placement } = this.state;
return children(this.showPopper, this.hidePopper, {
show,
placement,
content,
renderContent: this.renderContent,
theme,
});
}
}

View File

@ -3,8 +3,18 @@ import * as PopperJS from 'popper.js';
import Popper from './Popper';
import PopperController, { UsingPopperProps } from './PopperController';
export const Tooltip = ({ children, renderContent, ...controllerProps }: UsingPopperProps) => {
export enum Themes {
Default = 'popper__background--default',
Error = 'popper__background--error',
Brand = 'popper__background--brand',
}
interface TooltipProps extends UsingPopperProps {
theme?: Themes;
}
export const Tooltip = ({ children, theme, ...controllerProps }: TooltipProps) => {
const tooltipTriggerRef = createRef<PopperJS.ReferenceObject>();
const popperBackgroundClassName = 'popper__background' + (theme ? ' ' + theme : '');
return (
<PopperController {...controllerProps}>
@ -17,6 +27,11 @@ export const Tooltip = ({ children, renderContent, ...controllerProps }: UsingPo
onMouseEnter={showPopper}
onMouseLeave={hidePopper}
referenceElement={tooltipTriggerRef.current}
wrapperClassName='popper'
className={popperBackgroundClassName}
renderArrow={({ arrowProps, placement }) => (
<div className="popper__arrow" data-placement={placement} {...arrowProps} />
)}
/>
)}
{React.cloneElement(children, {

View File

@ -1,6 +1,5 @@
$popper-margin-from-ref: 5px;
@mixin popper-theme($backgroundColor, $arrowColor) {
background: $backgroundColor;
.popper__arrow {
@ -22,6 +21,10 @@ $popper-margin-from-ref: 5px;
box-shadow: 0 0 2px rgba(0, 0, 0, 0.5);
padding: 10px;
.popper__arrow {
border-color: $tooltipBackground;
}
// Themes
&.popper__background--error {
@include popper-theme($tooltipBackgroundError, $tooltipBackgroundError);
@ -41,10 +44,6 @@ $popper-margin-from-ref: 5px;
margin: 0px;
}
.popper__arrow {
border-color: $tooltipBackground;
}
// Top
.popper[data-placement^='top'] {
padding-bottom: $popper-margin-from-ref;

View File

@ -14,12 +14,12 @@ export { FormLabel } from './FormLabel/FormLabel';
export { FormField } from './FormField/FormField';
export { LoadingPlaceholder } from './LoadingPlaceholder/LoadingPlaceholder';
export { ColorPicker } from './ColorPicker/ColorPicker';
export { ColorPicker, SeriesColorPicker } from './ColorPicker/ColorPicker';
export { SeriesColorPickerPopover } from './ColorPicker/SeriesColorPickerPopover';
export { SeriesColorPicker } from './ColorPicker/SeriesColorPicker';
export { ThresholdsEditor } from './ThresholdsEditor/ThresholdsEditor';
export { Graph } from './Graph/Graph';
export { PanelOptionsGroup } from './PanelOptionsGroup/PanelOptionsGroup';
export { PanelOptionsGrid } from './PanelOptionsGrid/PanelOptionsGrid';
export { ValueMappingsEditor } from './ValueMappingsEditor/ValueMappingsEditor';
export { Gauge } from './Gauge/Gauge';
export { Switch } from './Switch/Switch';

View File

@ -1,3 +1 @@
@import 'vendor/spectrum';
@import 'components/index';

View File

@ -3,3 +3,12 @@ export * from './time';
export * from './panel';
export * from './plugin';
export * from './datasource';
export enum GrafanaTheme {
Light = 'light',
Dark = 'dark',
}
export interface Themeable {
theme?: GrafanaTheme;
}

View File

@ -36,7 +36,7 @@ export interface PanelMenuItem {
export interface Threshold {
index: number;
value: number;
color?: string;
color: string;
}
export enum BasicGaugeColor {
@ -66,10 +66,3 @@ export interface RangeMap extends BaseMap {
from: string;
to: string;
}
export type ThemeName = 'dark' | 'light';
export enum ThemeNames {
Dark = 'dark',
Light = 'light',
}

View File

@ -9,7 +9,6 @@ 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

View File

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

View File

@ -0,0 +1,66 @@
import {
getColorName,
getColorDefinition,
getColorByName,
getColorFromHexRgbOrName,
getColorDefinitionByName,
} from './namedColorsPalette';
import { GrafanaTheme } from '../types/index';
describe('colors', () => {
const SemiDarkBlue = getColorDefinitionByName('semi-dark-blue');
describe('getColorDefinition', () => {
it('returns undefined for unknown hex', () => {
expect(getColorDefinition('#ff0000', GrafanaTheme.Light)).toBeUndefined();
expect(getColorDefinition('#ff0000', GrafanaTheme.Dark)).toBeUndefined();
});
it('returns definition for known hex', () => {
expect(getColorDefinition(SemiDarkBlue.variants.light, GrafanaTheme.Light)).toEqual(SemiDarkBlue);
expect(getColorDefinition(SemiDarkBlue.variants.dark, GrafanaTheme.Dark)).toEqual(SemiDarkBlue);
});
});
describe('getColorName', () => {
it('returns undefined for unknown hex', () => {
expect(getColorName('#ff0000')).toBeUndefined();
});
it('returns name for known hex', () => {
expect(getColorName(SemiDarkBlue.variants.light, GrafanaTheme.Light)).toEqual(SemiDarkBlue.name);
expect(getColorName(SemiDarkBlue.variants.dark, GrafanaTheme.Dark)).toEqual(SemiDarkBlue.name);
});
});
describe('getColorByName', () => {
it('returns undefined for unknown color', () => {
expect(getColorByName('aruba-sunshine')).toBeUndefined();
});
it('returns color definiton for known color', () => {
expect(getColorByName(SemiDarkBlue.name)).toBe(SemiDarkBlue);
});
});
describe('getColorFromHexRgbOrName', () => {
it('returns undefined for unknown color', () => {
expect(() => getColorFromHexRgbOrName('aruba-sunshine')).toThrow();
});
it('returns dark hex variant for known color if theme not specified', () => {
expect(getColorFromHexRgbOrName(SemiDarkBlue.name)).toBe(SemiDarkBlue.variants.dark);
});
it("returns correct variant's hex for known color if theme specified", () => {
expect(getColorFromHexRgbOrName(SemiDarkBlue.name, GrafanaTheme.Light)).toBe(SemiDarkBlue.variants.light);
});
it('returns color if specified as hex or rgb/a', () => {
expect(getColorFromHexRgbOrName('ff0000')).toBe('ff0000');
expect(getColorFromHexRgbOrName('#ff0000')).toBe('#ff0000');
expect(getColorFromHexRgbOrName('rgb(0,0,0)')).toBe('rgb(0,0,0)');
expect(getColorFromHexRgbOrName('rgba(0,0,0,1)')).toBe('rgba(0,0,0,1)');
});
});
});

View File

@ -0,0 +1,182 @@
import { flatten } from 'lodash';
import { GrafanaTheme } from '../types';
type Hue = 'green' | 'yellow' | 'red' | 'blue' | 'orange' | 'purple';
export type Color =
| 'green'
| 'dark-green'
| 'semi-dark-green'
| 'light-green'
| 'super-light-green'
| 'yellow'
| 'dark-yellow'
| 'semi-dark-yellow'
| 'light-yellow'
| 'super-light-yellow'
| 'red'
| 'dark-red'
| 'semi-dark-red'
| 'light-red'
| 'super-light-red'
| 'blue'
| 'dark-blue'
| 'semi-dark-blue'
| 'light-blue'
| 'super-light-blue'
| 'orange'
| 'dark-orange'
| 'semi-dark-orange'
| 'light-orange'
| 'super-light-orange'
| 'purple'
| 'dark-purple'
| 'semi-dark-purple'
| 'light-purple'
| 'super-light-purple';
type ThemeVariants = {
dark: string;
light: string;
};
export type ColorDefinition = {
hue: Hue;
isPrimary?: boolean;
name: Color;
variants: ThemeVariants;
};
let colorsPaletteInstance: Map<Hue, ColorDefinition[]>;
const buildColorDefinition = (
hue: Hue,
name: Color,
[light, dark]: string[],
isPrimary?: boolean
): ColorDefinition => ({
hue,
name,
variants: {
light,
dark,
},
isPrimary: !!isPrimary,
});
export const getColorDefinitionByName = (name: Color): ColorDefinition => {
return flatten(Array.from(getNamedColorPalette().values())).filter(definition => definition.name === name)[0];
};
export const getColorDefinition = (hex: string, theme: GrafanaTheme): ColorDefinition | undefined => {
return flatten(Array.from(getNamedColorPalette().values())).filter(definition => definition.variants[theme] === hex)[0];
};
const isHex = (color: string) => {
const hexRegex = /^((0x){0,1}|#{0,1})([0-9A-F]{8}|[0-9A-F]{6})$/gi;
return hexRegex.test(color);
};
export const getColorName = (color?: string, theme?: GrafanaTheme): Color | undefined => {
if (!color) {
return undefined;
}
if (color.indexOf('rgb') > -1) {
return undefined;
}
if (isHex(color)) {
const definition = getColorDefinition(color, theme || GrafanaTheme.Dark);
return definition ? definition.name : undefined;
}
return color as Color;
};
export const getColorByName = (colorName: string) => {
const definition = flatten(Array.from(getNamedColorPalette().values())).filter(definition => definition.name === colorName);
return definition.length > 0 ? definition[0] : undefined;
};
export const getColorFromHexRgbOrName = (color: string, theme?: GrafanaTheme): string => {
if (color.indexOf('rgb') > -1 || isHex(color)) {
return color;
}
const colorDefinition = getColorByName(color);
if (!colorDefinition) {
throw new Error('Unknown color');
}
return theme ? colorDefinition.variants[theme] : colorDefinition.variants.dark;
};
export const getColorForTheme = (color: ColorDefinition, theme?: GrafanaTheme) => {
return theme ? color.variants[theme] : color.variants.dark;
};
const buildNamedColorsPalette = () => {
const palette = new Map<Hue, ColorDefinition[]>();
const BasicGreen = buildColorDefinition('green', 'green', ['#56A64B', '#73BF69'], true);
const DarkGreen = buildColorDefinition('green', 'dark-green', ['#19730E', '#37872D']);
const SemiDarkGreen = buildColorDefinition('green', 'semi-dark-green', ['#37872D', '#56A64B']);
const LightGreen = buildColorDefinition('green', 'light-green', ['#73BF69', '#96D98D']);
const SuperLightGreen = buildColorDefinition('green', 'super-light-green', ['#96D98D', '#C8F2C2']);
const BasicYellow = buildColorDefinition('yellow', 'yellow', ['#F2CC0C', '#FADE2A'], true);
const DarkYellow = buildColorDefinition('yellow', 'dark-yellow', ['#CC9D00', '#E0B400']);
const SemiDarkYellow = buildColorDefinition('yellow', 'semi-dark-yellow', ['#E0B400', '#F2CC0C']);
const LightYellow = buildColorDefinition('yellow', 'light-yellow', ['#FADE2A', '#FFEE52']);
const SuperLightYellow = buildColorDefinition('yellow', 'super-light-yellow', ['#FFEE52', '#FFF899']);
const BasicRed = buildColorDefinition('red', 'red', ['#E02F44', '#F2495C'], true);
const DarkRed = buildColorDefinition('red', 'dark-red', ['#AD0317', '#C4162A']);
const SemiDarkRed = buildColorDefinition('red', 'semi-dark-red', ['#C4162A', '#E02F44']);
const LightRed = buildColorDefinition('red', 'light-red', ['#F2495C', '#FF7383']);
const SuperLightRed = buildColorDefinition('red', 'super-light-red', ['#FF7383', '#FFA6B0']);
const BasicBlue = buildColorDefinition('blue', 'blue', ['#3274D9', '#5794F2'], true);
const DarkBlue = buildColorDefinition('blue', 'dark-blue', ['#1250B0', '#1F60C4']);
const SemiDarkBlue = buildColorDefinition('blue', 'semi-dark-blue', ['#1F60C4', '#3274D9']);
const LightBlue = buildColorDefinition('blue', 'light-blue', ['#5794F2', '#8AB8FF']);
const SuperLightBlue = buildColorDefinition('blue', 'super-light-blue', ['#8AB8FF', '#C0D8FF']);
const BasicOrange = buildColorDefinition('orange', 'orange', ['#FF780A', '#FF9830'], true);
const DarkOrange = buildColorDefinition('orange', 'dark-orange', ['#E55400', '#FA6400']);
const SemiDarkOrange = buildColorDefinition('orange', 'semi-dark-orange', ['#FA6400', '#FF780A']);
const LightOrange = buildColorDefinition('orange', 'light-orange', ['#FF9830', '#FFB357']);
const SuperLightOrange = buildColorDefinition('orange', 'super-light-orange', ['#FFB357', '#FFCB7D']);
const BasicPurple = buildColorDefinition('purple', 'purple', ['#A352CC', '#B877D9'], true);
const DarkPurple = buildColorDefinition('purple', 'dark-purple', ['#7C2EA3', '#8F3BB8']);
const SemiDarkPurple = buildColorDefinition('purple', 'semi-dark-purple', ['#8F3BB8', '#A352CC']);
const LightPurple = buildColorDefinition('purple', 'light-purple', ['#B877D9', '#CA95E5']);
const SuperLightPurple = buildColorDefinition('purple', 'super-light-purple', ['#CA95E5', '#DEB6F2']);
const greens = [BasicGreen, DarkGreen, SemiDarkGreen, LightGreen, SuperLightGreen];
const yellows = [BasicYellow, DarkYellow, SemiDarkYellow, LightYellow, SuperLightYellow];
const reds = [BasicRed, DarkRed, SemiDarkRed, LightRed, SuperLightRed];
const blues = [BasicBlue, DarkBlue, SemiDarkBlue, LightBlue, SuperLightBlue];
const oranges = [BasicOrange, DarkOrange, SemiDarkOrange, LightOrange, SuperLightOrange];
const purples = [BasicPurple, DarkPurple, SemiDarkPurple, LightPurple, SuperLightPurple];
palette.set('green', greens);
palette.set('yellow', yellows);
palette.set('red', reds);
palette.set('blue', blues);
palette.set('orange', oranges);
palette.set('purple', purples);
return palette;
};
export const getNamedColorPalette = () => {
if (colorsPaletteInstance) {
return colorsPaletteInstance;
}
colorsPaletteInstance = buildNamedColorsPalette();
return colorsPaletteInstance;
};

View File

@ -0,0 +1,6 @@
const propDeprecationWarning = (componentName: string, propName: string, newPropName: string) => {
const message = `[Deprecation warning] ${componentName}: ${propName} is deprecated. Use ${newPropName} instead`;
console.warn(message);
};
export default propDeprecationWarning;

View File

@ -0,0 +1,38 @@
import React from 'react';
interface StateHolderProps<T> {
initialState: T;
children: (currentState: T, updateState: (nextState: T) => void) => JSX.Element;
}
export class UseState<T> extends React.Component<StateHolderProps<T>, { value: T; initialState: T }> {
constructor(props: StateHolderProps<T>) {
super(props);
this.state = {
value: props.initialState,
initialState: props.initialState, // To enable control from knobs
};
}
// @ts-ignore
static getDerivedStateFromProps(props: StateHolderProps<{}>, state: { value: any; initialState: any }) {
if (props.initialState !== state.initialState) {
return {
initialState: props.initialState,
value: props.initialState,
};
}
return {
...state,
value: state.value,
};
}
handleStateUpdate = (nextState: T) => {
console.log(nextState);
this.setState({ value: nextState });
};
render() {
return this.props.children(this.state.value, this.handleStateUpdate);
}
}

View File

@ -0,0 +1,14 @@
import { select } from '@storybook/addon-knobs';
import { GrafanaTheme } from '../../types';
export const getThemeKnob = (defaultTheme: GrafanaTheme = GrafanaTheme.Dark) => {
return select(
'Theme',
{
Default: defaultTheme,
Light: GrafanaTheme.Light,
Dark: GrafanaTheme.Dark,
},
defaultTheme
);
};

View File

@ -0,0 +1,19 @@
import React from 'react';
import { RenderFunction } from '@storybook/react';
const CenteredStory: React.FunctionComponent<{}> = ({ children }) => {
return (
<div
style={{
height: '100vh ',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
{children}
</div>
);
};
export const withCenteredStory = (story: RenderFunction) => <CenteredStory>{story()}</CenteredStory>;

View File

@ -1,509 +0,0 @@
/***
Spectrum Colorpicker v1.3.0
https://github.com/bgrins/spectrum
Author: Brian Grinstead
License: MIT
***/
.sp-container {
position:absolute;
top:0;
left:0;
display:inline-block;
*display: inline;
*zoom: 1;
/* https://github.com/bgrins/spectrum/issues/40 */
z-index: 9999994;
overflow: hidden;
}
.sp-container.sp-flat {
position: relative;
}
/* Fix for * { box-sizing: border-box; } */
.sp-container,
.sp-container * {
-webkit-box-sizing: content-box;
-moz-box-sizing: content-box;
box-sizing: content-box;
}
/* http://ansciath.tumblr.com/post/7347495869/css-aspect-ratio */
.sp-top {
position:relative;
width: 100%;
display:inline-block;
}
.sp-top-inner {
position:absolute;
top:0;
left:0;
bottom:0;
right:0;
}
.sp-color {
position: absolute;
top:0;
left:0;
bottom:0;
right:20%;
}
.sp-hue {
position: absolute;
top:0;
right:0;
bottom:0;
left:84%;
height: 100%;
}
.sp-clear-enabled .sp-hue {
top:33px;
height: 77.5%;
}
.sp-fill {
padding-top: 80%;
}
.sp-sat, .sp-val {
position: absolute;
top:0;
left:0;
right:0;
bottom:0;
}
.sp-alpha-enabled .sp-top {
margin-bottom: 18px;
}
.sp-alpha-enabled .sp-alpha {
display: block;
}
.sp-alpha-handle {
position:absolute;
top:-4px;
bottom: -4px;
width: 6px;
left: 50%;
cursor: pointer;
border: 1px solid black;
background: white;
opacity: .8;
}
.sp-alpha {
display: none;
position: absolute;
bottom: -14px;
right: 0;
left: 0;
height: 8px;
}
.sp-alpha-inner {
border: solid 1px #333;
}
.sp-clear {
display: none;
}
.sp-clear.sp-clear-display {
background-position: center;
}
.sp-clear-enabled .sp-clear {
display: block;
position:absolute;
top:0px;
right:0;
bottom:0;
left:84%;
height: 28px;
}
/* Don't allow text selection */
.sp-container, .sp-replacer, .sp-preview, .sp-dragger, .sp-slider, .sp-alpha, .sp-clear, .sp-alpha-handle, .sp-container.sp-dragging .sp-input, .sp-container button {
-webkit-user-select:none;
-moz-user-select: -moz-none;
-o-user-select:none;
user-select: none;
}
.sp-container.sp-input-disabled .sp-input-container {
display: none;
}
.sp-container.sp-buttons-disabled .sp-button-container {
display: none;
}
.sp-palette-only .sp-picker-container {
display: none;
}
.sp-palette-disabled .sp-palette-container {
display: none;
}
.sp-initial-disabled .sp-initial {
display: none;
}
/* Gradients for hue, saturation and value instead of images. Not pretty... but it works */
.sp-sat {
background-image: linear-gradient(to right, #fff, rgba(204, 154, 129, 0));
-ms-filter: "progid:DXImageTransform.Microsoft.gradient(GradientType = 1, startColorstr=#FFFFFFFF, endColorstr=#00CC9A81)";
filter : progid:DXImageTransform.Microsoft.gradient(GradientType = 1, startColorstr='#FFFFFFFF', endColorstr='#00CC9A81');
}
.sp-val {
background-image: linear-gradient(to top, #000, rgba(204, 154, 129, 0));
-ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorstr=#00CC9A81, endColorstr=#FF000000)";
filter : progid:DXImageTransform.Microsoft.gradient(startColorstr='#00CC9A81', endColorstr='#FF000000');
}
.sp-hue {
background: -moz-linear-gradient(top, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000 100%);
background: -ms-linear-gradient(top, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000 100%);
background: -o-linear-gradient(top, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000 100%);
background: -webkit-gradient(linear, left top, left bottom, from(#ff0000), color-stop(0.17, #ffff00), color-stop(0.33, #00ff00), color-stop(0.5, #00ffff), color-stop(0.67, #0000ff), color-stop(0.83, #ff00ff), to(#ff0000));
background: -webkit-linear-gradient(top, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000 100%);
}
/* IE filters do not support multiple color stops.
Generate 6 divs, line them up, and do two color gradients for each.
Yes, really.
*/
.sp-1 {
height:17%;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0000', endColorstr='#ffff00');
}
.sp-2 {
height:16%;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffff00', endColorstr='#00ff00');
}
.sp-3 {
height:17%;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00ff00', endColorstr='#00ffff');
}
.sp-4 {
height:17%;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00ffff', endColorstr='#0000ff');
}
.sp-5 {
height:16%;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#0000ff', endColorstr='#ff00ff');
}
.sp-6 {
height:17%;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff00ff', endColorstr='#ff0000');
}
.sp-hidden {
display: none !important;
}
/* Clearfix hack */
.sp-cf:before, .sp-cf:after { content: ""; display: table; }
.sp-cf:after { clear: both; }
.sp-cf { *zoom: 1; }
/* Mobile devices, make hue slider bigger so it is easier to slide */
@media (max-device-width: 480px) {
.sp-color { right: 40%; }
.sp-hue { left: 63%; }
.sp-fill { padding-top: 60%; }
}
.sp-dragger {
border-radius: 5px;
height: 5px;
width: 5px;
border: 1px solid #fff;
background: #000;
cursor: pointer;
position:absolute;
top:0;
left: 0;
}
.sp-slider {
position: absolute;
top:0;
cursor:pointer;
height: 3px;
left: -1px;
right: -1px;
border: 1px solid #000;
background: white;
opacity: .8;
}
/*
Theme authors:
Here are the basic themeable display options (colors, fonts, global widths).
See http://bgrins.github.io/spectrum/themes/ for instructions.
*/
.sp-container {
border-radius: 0;
background-color: #ECECEC;
border: solid 1px #f0c49B;
padding: 0;
}
.sp-container, .sp-container button, .sp-container input, .sp-color, .sp-hue, .sp-clear
{
font: normal 12px "Lucida Grande", "Lucida Sans Unicode", "Lucida Sans", Geneva, Verdana, sans-serif;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
-ms-box-sizing: border-box;
box-sizing: border-box;
}
.sp-top
{
margin-bottom: 3px;
}
.sp-color, .sp-hue, .sp-clear
{
border: solid 1px #666;
}
/* Input */
.sp-input-container {
float:right;
width: 100px;
margin-bottom: 4px;
}
.sp-initial-disabled .sp-input-container {
width: 100%;
}
.sp-input {
font-size: 12px !important;
border: 1px inset;
padding: 4px 5px;
margin: 0;
width: 100%;
background:transparent;
border-radius: 3px;
color: #222;
}
.sp-input:focus {
border: 1px solid orange;
}
.sp-input.sp-validation-error
{
border: 1px solid red;
background: #fdd;
}
.sp-picker-container , .sp-palette-container
{
float:left;
position: relative;
padding: 10px;
padding-bottom: 300px;
margin-bottom: -290px;
}
.sp-picker-container
{
width: 172px;
border-left: solid 1px #fff;
}
/* Palettes */
.sp-palette-container
{
border-right: solid 1px #ccc;
}
.sp-palette .sp-thumb-el {
display: block;
position:relative;
float:left;
width: 24px;
height: 15px;
margin: 3px;
cursor: pointer;
border:solid 2px transparent;
}
.sp-palette .sp-thumb-el:hover, .sp-palette .sp-thumb-el.sp-thumb-active {
border-color: orange;
}
.sp-thumb-el
{
position:relative;
}
/* Initial */
.sp-initial
{
float: left;
border: solid 1px #333;
}
.sp-initial span {
width: 30px;
height: 25px;
border:none;
display:block;
float:left;
margin:0;
}
.sp-initial .sp-clear-display {
background-position: center;
}
/* Buttons */
.sp-button-container {
float: right;
}
/* Replacer (the little preview div that shows up instead of the <input>) */
.sp-replacer {
margin:0;
overflow:hidden;
cursor:pointer;
padding: 4px;
display:inline-block;
*zoom: 1;
*display: inline;
border: solid 1px #91765d;
background: #eee;
color: #333;
vertical-align: middle;
}
.sp-replacer:hover, .sp-replacer.sp-active {
border-color: #F0C49B;
color: #111;
}
.sp-replacer.sp-disabled {
cursor:default;
border-color: silver;
color: silver;
}
.sp-dd {
padding: 2px 0;
height: 16px;
line-height: 16px;
float:left;
font-size:10px;
}
.sp-preview
{
position:relative;
width:25px;
height: 20px;
border: solid 1px #222;
margin-right: 5px;
float:left;
z-index: 0;
}
.sp-palette
{
*width: 220px;
max-width: 220px;
}
.sp-palette .sp-thumb-el
{
width:16px;
height: 16px;
margin:2px 1px;
border: solid 1px #d0d0d0;
}
.sp-container
{
padding-bottom:0;
}
/* Buttons: http://hellohappy.org/css3-buttons/ */
.sp-container button {
background-color: #eeeeee;
background-image: -webkit-linear-gradient(top, #eeeeee, #cccccc);
background-image: -moz-linear-gradient(top, #eeeeee, #cccccc);
background-image: -ms-linear-gradient(top, #eeeeee, #cccccc);
background-image: -o-linear-gradient(top, #eeeeee, #cccccc);
background-image: linear-gradient(to bottom, #eeeeee, #cccccc);
border: 1px solid #ccc;
border-bottom: 1px solid #bbb;
border-radius: 3px;
color: #333;
font-size: 14px;
line-height: 1;
padding: 5px 4px;
text-align: center;
text-shadow: 0 1px 0 #eee;
vertical-align: middle;
}
.sp-container button:hover {
background-color: #dddddd;
background-image: -webkit-linear-gradient(top, #dddddd, #bbbbbb);
background-image: -moz-linear-gradient(top, #dddddd, #bbbbbb);
background-image: -ms-linear-gradient(top, #dddddd, #bbbbbb);
background-image: -o-linear-gradient(top, #dddddd, #bbbbbb);
background-image: linear-gradient(to bottom, #dddddd, #bbbbbb);
border: 1px solid #bbb;
border-bottom: 1px solid #999;
cursor: pointer;
text-shadow: 0 1px 0 #ddd;
}
.sp-container button:active {
border: 1px solid #aaa;
border-bottom: 1px solid #888;
-webkit-box-shadow: inset 0 0 5px 2px #aaaaaa, 0 1px 0 0 #eeeeee;
-moz-box-shadow: inset 0 0 5px 2px #aaaaaa, 0 1px 0 0 #eeeeee;
-ms-box-shadow: inset 0 0 5px 2px #aaaaaa, 0 1px 0 0 #eeeeee;
-o-box-shadow: inset 0 0 5px 2px #aaaaaa, 0 1px 0 0 #eeeeee;
box-shadow: inset 0 0 5px 2px #aaaaaa, 0 1px 0 0 #eeeeee;
}
.sp-cancel
{
font-size: 11px;
color: #d93f3f !important;
margin:0;
padding:2px;
margin-right: 5px;
vertical-align: middle;
text-decoration:none;
}
.sp-cancel:hover
{
color: #d93f3f !important;
text-decoration: underline;
}
.sp-palette span:hover, .sp-palette span.sp-thumb-active
{
border-color: #000;
}
.sp-preview, .sp-alpha, .sp-thumb-el
{
position:relative;
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg==);
}
.sp-preview-inner, .sp-alpha-inner, .sp-thumb-inner
{
display:block;
position:absolute;
top:0;left:0;bottom:0;right:0;
}
.sp-palette .sp-thumb-inner
{
background-position: 50% 50%;
background-repeat: no-repeat;
}
.sp-palette .sp-thumb-light.sp-thumb-active .sp-thumb-inner
{
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAYAAABWzo5XAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAIVJREFUeNpiYBhsgJFMffxAXABlN5JruT4Q3wfi/0DsT64h8UD8HmpIPCWG/KemIfOJCUB+Aoacx6EGBZyHBqI+WsDCwuQ9mhxeg2A210Ntfo8klk9sOMijaURm7yc1UP2RNCMbKE9ODK1HM6iegYLkfx8pligC9lCD7KmRof0ZhjQACDAAceovrtpVBRkAAAAASUVORK5CYII=);
}
.sp-palette .sp-thumb-dark.sp-thumb-active .sp-thumb-inner
{
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAYAAABWzo5XAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAadEVYdFNvZnR3YXJlAFBhaW50Lk5FVCB2My41LjEwMPRyoQAAAMdJREFUOE+tkgsNwzAMRMugEAahEAahEAZhEAqlEAZhEAohEAYh81X2dIm8fKpEspLGvudPOsUYpxE2BIJCroJmEW9qJ+MKaBFhEMNabSy9oIcIPwrB+afvAUFoK4H0tMaQ3XtlrggDhOVVMuT4E5MMG0FBbCEYzjYT7OxLEvIHQLY2zWwQ3D+9luyOQTfKDiFD3iUIfPk8VqrKjgAiSfGFPecrg6HN6m/iBcwiDAo7WiBeawa+Kwh7tZoSCGLMqwlSAzVDhoK+6vH4G0P5wdkAAAAASUVORK5CYII=);
}
.sp-clear-display {
background-repeat:no-repeat;
background-position: center;
background-image: url(data:image/gif;base64,R0lGODlhFAAUAPcAAAAAAJmZmZ2dnZ6enqKioqOjo6SkpKWlpaampqenp6ioqKmpqaqqqqurq/Hx8fLy8vT09PX19ff39/j4+Pn5+fr6+vv7+wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAP8ALAAAAAAUABQAAAihAP9FoPCvoMGDBy08+EdhQAIJCCMybCDAAYUEARBAlFiQQoMABQhKUJBxY0SPICEYHBnggEmDKAuoPMjS5cGYMxHW3IiT478JJA8M/CjTZ0GgLRekNGpwAsYABHIypcAgQMsITDtWJYBR6NSqMico9cqR6tKfY7GeBCuVwlipDNmefAtTrkSzB1RaIAoXodsABiZAEFB06gIBWC1mLVgBa0AAOw==);
}

File diff suppressed because it is too large Load Diff

View File

@ -5,14 +5,17 @@
"src/**/*.tsx"
],
"exclude": [
"dist"
"dist",
"node_modules"
],
"compilerOptions": {
"rootDir": ".",
"rootDirs": [".", "stories"],
"module": "esnext",
"outDir": "dist",
"declaration": true,
"noImplicitAny": true,
"strictNullChecks": true
}
"strictNullChecks": true,
"typeRoots": ["./node_modules/@types", "types"],
"skipLibCheck": true // Temp workaround for Duplicate identifier tsc errors
},
}

View File

@ -28,6 +28,7 @@ export function registerAngularDirectives() {
['onChange', { watchDepth: 'reference', wrapApply: true }],
]);
react2AngularDirective('seriesColorPickerPopover', SeriesColorPickerPopover, [
'color',
'series',
'onColorChange',
'onToggleAxis',

View File

@ -2,7 +2,6 @@ import config from 'app/core/config';
import _ from 'lodash';
import coreModule from 'app/core/core_module';
import store from 'app/core/store';
import { ThemeNames, ThemeName } from '@grafana/ui';
export class User {
isGrafanaAdmin: any;
@ -64,10 +63,6 @@ export class ContextSrv {
hasAccessToExplore() {
return (this.isEditor || config.viewersCanEdit) && config.exploreEnabled;
}
getTheme(): ThemeName {
return this.user.lightTheme ? ThemeNames.Light : ThemeNames.Dark;
}
}
const contextSrv = new ContextSrv();

View File

@ -356,7 +356,7 @@ export default class TimeSeries {
return false;
}
setColor(color) {
setColor(color: string) {
this.color = color;
this.bars.fillColor = color;
}

View File

@ -0,0 +1,28 @@
import React from 'react';
import config, { Settings } from 'app/core/config';
import { GrafanaTheme } from '@grafana/ui';
export const ConfigContext = React.createContext<Settings>(config);
export const ConfigConsumer = ConfigContext.Consumer;
export const provideConfig = (component: React.ComponentType<any>) => {
const ConfigProvider = (props: any) => (
<ConfigContext.Provider value={config}>{React.createElement(component, { ...props })}</ConfigContext.Provider>
);
return ConfigProvider;
};
interface ThemeProviderProps {
children: (theme: GrafanaTheme) => JSX.Element;
}
export const ThemeProvider = ({ children }: ThemeProviderProps) => {
return (
<ConfigConsumer>
{({ bootData }) => {
return children(bootData.user.lightTheme ? GrafanaTheme.Light : GrafanaTheme.Dark);
}}
</ConfigConsumer>
);
};

View File

@ -1,10 +1,11 @@
import coreModule from 'app/core/core_module';
import { provideConfig } from 'app/core/utils/ConfigProvider';
export function react2AngularDirective(name: string, component: any, options: any) {
coreModule.directive(name, [
'reactDirective',
reactDirective => {
return reactDirective(component, options);
return reactDirective(provideConfig(component), options);
},
]);
}

View File

@ -70,7 +70,7 @@ export class DashboardPermissions extends PureComponent<Props, State> {
<div className="dashboard-settings__header">
<div className="page-action-bar">
<h3 className="d-inline-block">Permissions</h3>
<Tooltip placement="auto" content={PermissionsInfo}>
<Tooltip placement="auto" content={<PermissionsInfo />}>
<div className="page-sub-heading-icon">
<i className="gicon gicon-question gicon--has-hover" />
</div>

View File

@ -1,7 +1,7 @@
// Library
import React, { Component } from 'react';
import { Tooltip } from '@grafana/ui';
import { Themes } from '@grafana/ui/src/components/Tooltip/Popper';
import { Themes } from '@grafana/ui/src/components/Tooltip/Tooltip';
import ErrorBoundary from 'app/core/components/ErrorBoundary/ErrorBoundary';

View File

@ -77,7 +77,7 @@ export class PanelHeaderCorner extends Component<Props> {
<>
{infoMode === InfoModes.Info || infoMode === InfoModes.Links ? (
<Tooltip
content={this.getInfoContent}
content={this.getInfoContent()}
placement="bottom-start"
>
<div

View File

@ -16,7 +16,7 @@ import { DashboardModel } from '../dashboard_model';
import { PanelPlugin } from 'app/types/plugins';
import { Tooltip } from '@grafana/ui';
import { Themes } from '@grafana/ui/src/components/Tooltip/Popper';
import { Themes } from '@grafana/ui/src/components/Tooltip/Tooltip';
interface PanelEditorProps {
panel: PanelModel;

View File

@ -5,7 +5,7 @@ import React, { PureComponent } from 'react';
import { isValidTimeSpan } from 'app/core/utils/rangeutil';
// Components
import { Switch } from 'app/core/components/Switch/Switch';
import { Switch } from '@grafana/ui';
import { Input } from 'app/core/components/Form';
import { EventsWithValidation } from 'app/core/components/Form/Input';
import { InputStatus } from 'app/core/components/Form/Input';

View File

@ -1,6 +1,5 @@
import React, { FC } from 'react';
import { FormLabel } from '@grafana/ui';
import { Switch } from '../../../core/components/Switch/Switch';
import { FormLabel, Switch } from '@grafana/ui';
export interface Props {
dataSourceName: string;
@ -31,6 +30,8 @@ const BasicSettings: FC<Props> = ({ dataSourceName, isDefault, onDefaultChange,
required
/>
</div>
{/*
//@ts-ignore */}
<Switch label="Default" checked={isDefault} onChange={event => onDefaultChange(event.target.checked)} />
</div>
</div>

View File

@ -2,7 +2,7 @@ import _ from 'lodash';
import React, { PureComponent } from 'react';
import * as rangeUtil from 'app/core/utils/rangeutil';
import { RawTimeRange } from '@grafana/ui';
import { RawTimeRange, Switch } from '@grafana/ui';
import {
LogsDedupDescription,
LogsDedupStrategy,
@ -13,7 +13,6 @@ import {
LogsMetaKind,
} from 'app/core/logs_model';
import { Switch } from 'app/core/components/Switch/Switch';
import ToggleButtonGroup, { ToggleButton } from 'app/core/components/ToggleButtonGroup/ToggleButtonGroup';
import Graph from './Graph';

View File

@ -84,7 +84,7 @@ export class FolderPermissions extends PureComponent<Props, State> {
<div className="page-container page-body">
<div className="page-action-bar">
<h3 className="page-sub-heading">Folder Permissions</h3>
<Tooltip placement="auto" content={PermissionsInfo}>
<Tooltip placement="auto" content={<PermissionsInfo />}>
<div className="page-sub-heading-icon">
<i className="gicon gicon-question gicon--has-hover" />
</div>

View File

@ -1,7 +1,6 @@
import React, { PureComponent } from 'react';
import { FormField, PanelOptionsProps, PanelOptionsGroup } from '@grafana/ui';
import { FormField, PanelOptionsProps, PanelOptionsGroup, Switch } from '@grafana/ui';
import { Switch } from 'app/core/components/Switch/Switch';
import { GaugeOptions } from './types';
export default class GaugeOptionsEditor extends PureComponent<PanelOptionsProps<GaugeOptions>> {

View File

@ -2,7 +2,6 @@
import React, { PureComponent } from 'react';
// Services & Utils
import { contextSrv } from 'app/core/core';
import { processTimeSeries } from '@grafana/ui';
// Components
@ -11,11 +10,11 @@ import { Gauge } from '@grafana/ui';
// Types
import { GaugeOptions } from './types';
import { PanelProps, NullValueMode } from '@grafana/ui/src/types';
import { ThemeProvider } from 'app/core/utils/ConfigProvider';
interface Props extends PanelProps<GaugeOptions> {}
export class GaugePanel extends PureComponent<Props> {
render() {
const { timeSeries, width, height, onInterpolate, options } = this.props;
@ -28,15 +27,19 @@ export class GaugePanel extends PureComponent<Props> {
});
return (
<Gauge
timeSeries={vmSeries}
{...this.props.options}
width={width}
height={height}
prefix={prefix}
suffix={suffix}
theme={contextSrv.getTheme()}
/>
<ThemeProvider>
{(theme) => (
<Gauge
timeSeries={vmSeries}
{...this.props.options}
width={width}
height={height}
prefix={prefix}
suffix={suffix}
theme={theme}
/>
)}
</ThemeProvider>
);
}
}

View File

@ -11,6 +11,7 @@ import {
import ValueOptions from 'app/plugins/panel/gauge/ValueOptions';
import GaugeOptionsEditor from './GaugeOptionsEditor';
import { GaugeOptions } from './types';
import { ThemeProvider } from 'app/core/utils/ConfigProvider';
export const defaultProps = {
options: {
@ -46,15 +47,23 @@ export default class GaugePanelOptions extends PureComponent<PanelOptionsProps<G
render() {
const { onChange, options } = this.props;
return (
<>
<PanelOptionsGrid>
<ValueOptions onChange={onChange} options={options} />
<GaugeOptionsEditor onChange={onChange} options={options} />
<ThresholdsEditor onChange={this.onThresholdsChanged} thresholds={options.thresholds} />
</PanelOptionsGrid>
<ThemeProvider>
{(theme) => (
<>
<PanelOptionsGrid>
<ValueOptions onChange={onChange} options={options} />
<GaugeOptionsEditor onChange={onChange} options={options} />
<ThresholdsEditor
onChange={this.onThresholdsChanged}
thresholds={options.thresholds}
theme={theme}
/>
</PanelOptionsGrid>
<ValueMappingsEditor onChange={this.onValueMappingsChanged} valueMappings={options.valueMappings} />
</>
<ValueMappingsEditor onChange={this.onValueMappingsChanged} valueMappings={options.valueMappings} />
</>
)}
</ThemeProvider>
);
}
}

View File

@ -311,7 +311,7 @@ class LegendTableHeaderItem extends PureComponent<LegendTableHeaderProps & Legen
export class Legend extends PureComponent<GraphLegendProps> {
render() {
return (
<CustomScrollbar>
<CustomScrollbar renderTrackHorizontal={(props) => <div {...props} style={{visibility: 'none'}} />}>
<GraphLegend {...this.props} />
</CustomScrollbar>
);

View File

@ -2,6 +2,7 @@ import React, { PureComponent } from 'react';
import classNames from 'classnames';
import { TimeSeries } from 'app/core/core';
import { SeriesColorPicker } from '@grafana/ui';
import { ThemeProvider } from 'app/core/utils/ConfigProvider';
export const LEGEND_STATS = ['min', 'max', 'avg', 'current', 'total'];
@ -154,8 +155,8 @@ interface LegendSeriesIconState {
color: string;
}
function SeriesIcon(props) {
return <i className="fa fa-minus pointer" style={{ color: props.color }} />;
function SeriesIcon({ color }) {
return <i className="fa fa-minus pointer" style={{ color }} />;
}
class LegendSeriesIcon extends PureComponent<LegendSeriesIconProps, LegendSeriesIconState> {
@ -167,15 +168,24 @@ class LegendSeriesIcon extends PureComponent<LegendSeriesIconProps, LegendSeries
render() {
return (
<SeriesColorPicker
optionalClass="graph-legend-icon"
yaxis={this.props.yaxis}
color={this.props.color}
onColorChange={this.props.onColorChange}
onToggleAxis={this.props.onToggleAxis}
>
<SeriesIcon color={this.props.color} />
</SeriesColorPicker>
<ThemeProvider>
{theme => {
return (
<SeriesColorPicker
yaxis={this.props.yaxis}
color={this.props.color}
onChange={this.props.onColorChange}
onToggleAxis={this.props.onToggleAxis}
theme={theme}
enableNamedColors
>
<span className="graph-legend-icon">
<SeriesIcon color={this.props.color} />
</span>
</SeriesColorPicker>
);
}}
</ThemeProvider>
);
}
}

View File

@ -1,7 +1,7 @@
import _ from 'lodash';
import { colors } from '@grafana/ui';
import { colors, GrafanaTheme, getColorFromHexRgbOrName } from '@grafana/ui';
import TimeSeries from 'app/core/time_series2';
import config from 'app/core/config';
export class DataProcessor {
constructor(private panel) {}
@ -107,12 +107,13 @@ export class DataProcessor {
const alias = seriesData.target;
const colorIndex = index % colors.length;
const color = this.panel.aliasColors[alias] || colors[colorIndex];
const series = new TimeSeries({
datapoints: datapoints,
alias: alias,
color: color,
color: getColorFromHexRgbOrName(color, config.bootData.user.lightTheme ? GrafanaTheme.Light : GrafanaTheme.Dark),
unit: seriesData.unit,
});

View File

@ -26,6 +26,7 @@ import ReactDOM from 'react-dom';
import { Legend, GraphLegendProps } from './Legend/Legend';
import { GraphCtrl } from './module';
import { GrafanaTheme } from '@grafana/ui';
class GraphElement {
ctrl: GraphCtrl;
@ -51,7 +52,10 @@ class GraphElement {
this.panelWidth = 0;
this.eventManager = new EventManager(this.ctrl);
this.thresholdManager = new ThresholdManager(this.ctrl);
this.timeRegionManager = new TimeRegionManager(this.ctrl);
this.timeRegionManager = new TimeRegionManager(
this.ctrl,
config.bootData.user.lightTheme ? GrafanaTheme.Light : GrafanaTheme.Dark
);
this.tooltip = new GraphTooltip(this.elem, this.ctrl.dashboard, this.scope, () => {
return this.sortedSeries;
});

View File

@ -9,6 +9,8 @@ import _ from 'lodash';
import { MetricsPanelCtrl } from 'app/plugins/sdk';
import { DataProcessor } from './data_processor';
import { axesEditorComponent } from './axes_editor';
import config from 'app/core/config';
import { GrafanaTheme, getColorFromHexRgbOrName } from '@grafana/ui';
class GraphCtrl extends MetricsPanelCtrl {
static template = template;
@ -242,8 +244,8 @@ class GraphCtrl extends MetricsPanelCtrl {
}
onColorChange = (series, color) => {
series.setColor(color);
this.panel.aliasColors[series.alias] = series.color;
series.setColor(getColorFromHexRgbOrName(color, config.bootData.user.lightTheme ? GrafanaTheme.Light : GrafanaTheme.Dark));
this.panel.aliasColors[series.alias] = color;
this.render();
};

View File

@ -53,11 +53,13 @@ export function SeriesOverridesCtrl($scope, $element, popoverSrv) {
element: $element.find('.dropdown')[0],
position: 'top center',
openOn: 'click',
template: '<series-color-picker-popover series="series" onColorChange="colorSelected" />',
template: '<series-color-picker-popover color="color" onColorChange="colorSelected" />',
classNames: 'drop-popover drop-popover--transparent',
model: {
autoClose: true,
colorSelected: $scope.colorSelected,
series: fakeSeries,
color,
},
onClose: () => {
$scope.ctrl.render();

View File

@ -1,6 +1,7 @@
import 'vendor/flot/jquery.flot';
import $ from 'jquery';
import _ from 'lodash';
import { getColorFromHexRgbOrName } from '@grafana/ui';
export class ThresholdManager {
plot: any;
@ -197,6 +198,7 @@ export class ThresholdManager {
}
let fillColor, lineColor;
switch (threshold.colorMode) {
case 'critical': {
fillColor = 'rgba(234, 112, 112, 0.12)';
@ -214,6 +216,12 @@ export class ThresholdManager {
break;
}
case 'custom': {
if (!threshold.fillColor) {
threshold.fillColor = 'rgba(255, 255, 255, 1)';
}
if (!threshold.lineColor) {
threshold.lineColor = 'rgba(255, 255, 255, 0)';
}
fillColor = threshold.fillColor;
lineColor = threshold.lineColor;
break;
@ -225,12 +233,12 @@ export class ThresholdManager {
if (threshold.yaxis === 'right' && this.hasSecondYAxis) {
options.grid.markings.push({
y2axis: { from: threshold.value, to: limit },
color: fillColor,
color: getColorFromHexRgbOrName(fillColor),
});
} else {
options.grid.markings.push({
yaxis: { from: threshold.value, to: limit },
color: fillColor,
color: getColorFromHexRgbOrName(fillColor),
});
}
}
@ -238,12 +246,12 @@ export class ThresholdManager {
if (threshold.yaxis === 'right' && this.hasSecondYAxis) {
options.grid.markings.push({
y2axis: { from: threshold.value, to: threshold.value },
color: lineColor,
color: getColorFromHexRgbOrName(lineColor),
});
} else {
options.grid.markings.push({
yaxis: { from: threshold.value, to: threshold.value },
color: lineColor,
color: getColorFromHexRgbOrName(lineColor),
});
}
}

View File

@ -1,7 +1,12 @@
import 'vendor/flot/jquery.flot';
import _ from 'lodash';
import moment from 'moment';
import config from 'app/core/config';
import { GrafanaTheme, getColorFromHexRgbOrName } from '@grafana/ui';
type TimeRegionColorDefinition = {
fill: string;
line: string;
};
export const colorModes = {
gray: {
@ -38,31 +43,35 @@ export function getColorModes() {
});
}
function getColor(timeRegion) {
function getColor(timeRegion, theme: GrafanaTheme): TimeRegionColorDefinition {
if (Object.keys(colorModes).indexOf(timeRegion.colorMode) === -1) {
timeRegion.colorMode = 'red';
}
if (timeRegion.colorMode === 'custom') {
return {
fill: timeRegion.fillColor,
line: timeRegion.lineColor,
fill: getColorFromHexRgbOrName(timeRegion.fillColor, theme),
line: getColorFromHexRgbOrName(timeRegion.lineColor, theme),
};
}
const colorMode = colorModes[timeRegion.colorMode];
if (colorMode.themeDependent === true) {
return config.bootData.user.lightTheme ? colorMode.lightColor : colorMode.darkColor;
return theme === GrafanaTheme.Light ? colorMode.lightColor : colorMode.darkColor;
}
return colorMode.color;
return {
fill: getColorFromHexRgbOrName(colorMode.color.fill, theme),
line: getColorFromHexRgbOrName(colorMode.color.line, theme),
};
}
export class TimeRegionManager {
plot: any;
timeRegions: any;
constructor(private panelCtrl) {}
constructor(private panelCtrl, private theme: GrafanaTheme = GrafanaTheme.Dark) {}
draw(plot) {
this.timeRegions = this.panelCtrl.panel.timeRegions;
@ -76,7 +85,7 @@ export class TimeRegionManager {
const tRange = { from: moment(this.panelCtrl.range.from).utc(), to: moment(this.panelCtrl.range.to).utc() };
let i, hRange, timeRegion, regions, fromStart, fromEnd, timeRegionColor;
let i, hRange, timeRegion, regions, fromStart, fromEnd, timeRegionColor: TimeRegionColorDefinition;
const timeRegionsCopy = panel.timeRegions.map(a => ({ ...a }));
@ -200,7 +209,7 @@ export class TimeRegionManager {
}
}
timeRegionColor = getColor(timeRegion);
timeRegionColor = getColor(timeRegion, this.theme);
for (let j = 0; j < regions.length; j++) {
const r = regions[j];

View File

@ -35,6 +35,9 @@ export class TimeRegionFormCtrl {
colorMode: 'background6',
fill: true,
line: false,
// Default colors for new
fillColor: 'rgba(234, 112, 112, 0.12)',
lineColor: 'rgba(237, 46, 24, 0.60)'
});
this.panelCtrl.render();
}

View File

@ -2,11 +2,9 @@
import _ from 'lodash';
import React, { PureComponent } from 'react';
// Components
import { Switch } from 'app/core/components/Switch/Switch';
// Types
import { PanelOptionsProps } from '@grafana/ui';
import { PanelOptionsProps, Switch } from '@grafana/ui';
import { Options } from './types';
export class GraphPanelOptions extends PureComponent<PanelOptionsProps<Options>> {

View File

@ -5,6 +5,7 @@ import { contextSrv } from 'app/core/core';
import { tickStep } from 'app/core/utils/ticks';
import { getColorScale, getOpacityScale } from './color_scale';
import coreModule from 'app/core/core_module';
import { GrafanaTheme, getColorFromHexRgbOrName } from '@grafana/ui';
const LEGEND_HEIGHT_PX = 6;
const LEGEND_WIDTH_PX = 100;
@ -247,7 +248,10 @@ function drawSimpleOpacityLegend(elem, options) {
.attr('width', rangeStep)
.attr('height', legendHeight)
.attr('stroke-width', 0)
.attr('fill', options.cardColor)
.attr(
'fill',
getColorFromHexRgbOrName(options.cardColor, contextSrv.user.lightTheme ? GrafanaTheme.Light : GrafanaTheme.Dark)
)
.style('opacity', d => legendOpacityScale(d));
}
}

View File

@ -8,6 +8,7 @@ import * as ticksUtils from 'app/core/utils/ticks';
import { HeatmapTooltip } from './heatmap_tooltip';
import { mergeZeroBuckets } from './heatmap_data_converter';
import { getColorScale, getOpacityScale } from './color_scale';
import { GrafanaTheme, getColorFromHexRgbOrName } from '@grafana/ui';
const MIN_CARD_SIZE = 1,
CARD_PADDING = 1,
@ -521,7 +522,6 @@ export class HeatmapRenderer {
const maxValueAuto = this.data.cardStats.max;
const maxValue = this.panel.color.max || maxValueAuto;
const minValue = this.panel.color.min || 0;
const colorScheme = _.find(this.ctrl.colorSchemes, {
value: this.panel.color.colorScheme,
});
@ -662,7 +662,10 @@ export class HeatmapRenderer {
getCardColor(d) {
if (this.panel.color.mode === 'opacity') {
return this.panel.color.cardColor;
return getColorFromHexRgbOrName(
this.panel.color.cardColor,
contextSrv.user.lightTheme ? GrafanaTheme.Light : GrafanaTheme.Dark
);
} else {
return this.colorScale(d.count);
}

View File

@ -8,6 +8,8 @@ import kbn from 'app/core/utils/kbn';
import config from 'app/core/config';
import TimeSeries from 'app/core/time_series2';
import { MetricsPanelCtrl } from 'app/plugins/sdk';
import { getColorFromHexRgbOrName } from '@grafana/ui';
import { GrafanaTheme } from '@grafana/ui';
class SingleStatCtrl extends MetricsPanelCtrl {
static templateUrl = 'module.html';
@ -479,6 +481,7 @@ class SingleStatCtrl extends MetricsPanelCtrl {
plotCanvas.css(plotCss);
const thresholds = [];
for (let i = 0; i < data.thresholds.length; i++) {
thresholds.push({
value: data.thresholds[i],
@ -586,7 +589,10 @@ class SingleStatCtrl extends MetricsPanelCtrl {
fill: 1,
zero: false,
lineWidth: 1,
fillColor: panel.sparkline.fillColor,
fillColor: getColorFromHexRgbOrName(
panel.sparkline.fillColor,
config.bootData.user.lightTheme ? GrafanaTheme.Light : GrafanaTheme.Dark
),
},
},
yaxes: { show: false },
@ -603,7 +609,10 @@ class SingleStatCtrl extends MetricsPanelCtrl {
const plotSeries = {
data: data.flotpairs,
color: panel.sparkline.lineColor,
color: getColorFromHexRgbOrName(
panel.sparkline.lineColor,
config.bootData.user.lightTheme ? GrafanaTheme.Light : GrafanaTheme.Dark
),
};
$.plot(plotCanvas, [plotSeries], options);
@ -619,12 +628,17 @@ class SingleStatCtrl extends MetricsPanelCtrl {
data.thresholds = panel.thresholds.split(',').map(strVale => {
return Number(strVale.trim());
});
data.colorMap = panel.colors;
// Map panel colors to hex or rgb/a values
data.colorMap = panel.colors.map(color =>
getColorFromHexRgbOrName(color, config.bootData.user.lightTheme ? GrafanaTheme.Light : GrafanaTheme.Dark)
);
const body = panel.gauge.show ? '' : getBigValueHtml();
if (panel.colorBackground) {
const color = getColorForValue(data, data.value);
console.log(color);
if (color) {
$panelContainer.css('background-color', color);
if (scope.fullscreen) {

View File

@ -1,10 +1,12 @@
import _ from 'lodash';
import $ from 'jquery';
import { MetricsPanelCtrl } from 'app/plugins/sdk';
import config from 'app/core/config';
import { transformDataToTable } from './transformers';
import { tablePanelEditor } from './editor';
import { columnOptionsTab } from './column_options';
import { TableRenderer } from './renderer';
import { GrafanaTheme } from '@grafana/ui';
class TablePanelCtrl extends MetricsPanelCtrl {
static templateUrl = 'module.html';
@ -129,7 +131,8 @@ class TablePanelCtrl extends MetricsPanelCtrl {
this.table,
this.dashboard.isTimezoneUtc(),
this.$sanitize,
this.templateSrv
this.templateSrv,
config.bootData.user.lightTheme ? GrafanaTheme.Light : GrafanaTheme.Dark,
);
return super.render(this.table);

View File

@ -1,12 +1,21 @@
import _ from 'lodash';
import moment from 'moment';
import kbn from 'app/core/utils/kbn';
import { getColorFromHexRgbOrName } from '@grafana/ui';
import { GrafanaTheme } from '@grafana/ui';
export class TableRenderer {
formatters: any[];
colorState: any;
constructor(private panel, private table, private isUtc, private sanitize, private templateSrv) {
constructor(
private panel,
private table,
private isUtc,
private sanitize,
private templateSrv,
private theme?: GrafanaTheme
) {
this.initColumns();
}
@ -49,10 +58,10 @@ export class TableRenderer {
}
for (let i = style.thresholds.length; i > 0; i--) {
if (value >= style.thresholds[i - 1]) {
return style.colors[i];
return getColorFromHexRgbOrName(style.colors[i], this.theme);
}
}
return _.first(style.colors);
return getColorFromHexRgbOrName(_.first(style.colors), this.theme);
}
defaultCellFormatter(v, style) {

View File

@ -1,8 +1,11 @@
import _ from 'lodash';
import TableModel from 'app/core/table_model';
import { TableRenderer } from '../renderer';
import { getColorDefinitionByName } from '@grafana/ui';
describe('when rendering table', () => {
const SemiDarkOrange = getColorDefinitionByName('semi-dark-orange');
describe('given 13 columns', () => {
const table = new TableModel();
table.columns = [
@ -47,7 +50,7 @@ describe('when rendering table', () => {
decimals: 1,
colorMode: 'value',
thresholds: [50, 80],
colors: ['green', 'orange', 'red'],
colors: ['#00ff00', SemiDarkOrange.name, 'rgb(1,0,0)'],
},
{
pattern: 'String',
@ -138,7 +141,7 @@ describe('when rendering table', () => {
],
colorMode: 'value',
thresholds: [1, 2],
colors: ['green', 'orange', 'red'],
colors: ['#00ff00', SemiDarkOrange.name, 'rgb(1,0,0)'],
},
{
pattern: 'RangeMappingColored',
@ -158,7 +161,7 @@ describe('when rendering table', () => {
],
colorMode: 'value',
thresholds: [2, 5],
colors: ['green', 'orange', 'red'],
colors: ['#00ff00', SemiDarkOrange.name, 'rgb(1,0,0)'],
},
],
};
@ -226,19 +229,19 @@ describe('when rendering table', () => {
expect(html).toBe('<td>asd</td>');
});
it('colored cell should have style', () => {
it('colored cell should have style (handles HEX color values)', () => {
const html = renderer.renderCell(2, 0, 40);
expect(html).toBe('<td style="color:green">40.0</td>');
expect(html).toBe('<td style="color:#00ff00">40.0</td>');
});
it('colored cell should have style', () => {
it('colored cell should have style (handles named color values', () => {
const html = renderer.renderCell(2, 0, 55);
expect(html).toBe('<td style="color:orange">55.0</td>');
expect(html).toBe(`<td style="color:${SemiDarkOrange.variants.dark}">55.0</td>`);
});
it('colored cell should have style', () => {
it('colored cell should have style handles(rgb color values)', () => {
const html = renderer.renderCell(2, 0, 85);
expect(html).toBe('<td style="color:red">85.0</td>');
expect(html).toBe('<td style="color:rgb(1,0,0)">85.0</td>');
});
it('unformated undefined should be rendered as string', () => {
@ -333,47 +336,47 @@ describe('when rendering table', () => {
it('value should be mapped to text and colored cell should have style', () => {
const html = renderer.renderCell(11, 0, 1);
expect(html).toBe('<td style="color:orange">on</td>');
expect(html).toBe(`<td style="color:${SemiDarkOrange.variants.dark}">on</td>`);
});
it('value should be mapped to text and colored cell should have style', () => {
const html = renderer.renderCell(11, 0, '1');
expect(html).toBe('<td style="color:orange">on</td>');
expect(html).toBe(`<td style="color:${SemiDarkOrange.variants.dark}">on</td>`);
});
it('value should be mapped to text and colored cell should have style', () => {
const html = renderer.renderCell(11, 0, 0);
expect(html).toBe('<td style="color:green">off</td>');
expect(html).toBe('<td style="color:#00ff00">off</td>');
});
it('value should be mapped to text and colored cell should have style', () => {
const html = renderer.renderCell(11, 0, '0');
expect(html).toBe('<td style="color:green">off</td>');
expect(html).toBe('<td style="color:#00ff00">off</td>');
});
it('value should be mapped to text and colored cell should have style', () => {
const html = renderer.renderCell(11, 0, '2.1');
expect(html).toBe('<td style="color:red">2.1</td>');
expect(html).toBe('<td style="color:rgb(1,0,0)">2.1</td>');
});
it('value should be mapped to text (range) and colored cell should have style', () => {
const html = renderer.renderCell(12, 0, 0);
expect(html).toBe('<td style="color:green">0</td>');
expect(html).toBe('<td style="color:#00ff00">0</td>');
});
it('value should be mapped to text (range) and colored cell should have style', () => {
const html = renderer.renderCell(12, 0, 1);
expect(html).toBe('<td style="color:green">on</td>');
expect(html).toBe('<td style="color:#00ff00">on</td>');
});
it('value should be mapped to text (range) and colored cell should have style', () => {
const html = renderer.renderCell(12, 0, 4);
expect(html).toBe('<td style="color:orange">off</td>');
expect(html).toBe(`<td style="color:${SemiDarkOrange.variants.dark}">off</td>`);
});
it('value should be mapped to text (range) and colored cell should have style', () => {
const html = renderer.renderCell(12, 0, '7.1');
expect(html).toBe('<td style="color:red">7.1</td>');
expect(html).toBe('<td style="color:rgb(1,0,0)">7.1</td>');
});
});
});

View File

@ -1,6 +1,6 @@
import { createStore, applyMiddleware, compose, combineReducers } from 'redux';
import thunk from 'redux-thunk';
import { createLogger } from 'redux-logger';
// import { createLogger } from 'redux-logger';
import sharedReducers from 'app/core/reducers';
import alertingReducers from 'app/features/alerting/state/reducers';
import teamsReducers from 'app/features/teams/state/reducers';
@ -39,7 +39,7 @@ export function configureStore() {
if (process.env.NODE_ENV !== 'production') {
// DEV builds we had the logger middleware
setStore(createStore(rootReducer, {}, composeEnhancers(applyMiddleware(thunk, createLogger()))));
setStore(createStore(rootReducer, {}, composeEnhancers(applyMiddleware(thunk))));
} else {
setStore(createStore(rootReducer, {}, composeEnhancers(applyMiddleware(thunk))));
}

View File

@ -4,7 +4,7 @@
"outDir": "public/dist",
"target": "es5",
"lib": ["es6", "dom"],
"rootDir": "public/",
"rootDirs": ["public/"],
"jsx": "react",
"module": "esnext",
"declaration": false,

5492
yarn.lock

File diff suppressed because it is too large Load Diff