Merge pull request #15235 from grafana/core/theming

POC - Enable js defined theme to be used in SASS
This commit is contained in:
Torkel Ödegaard 2019-02-08 16:10:17 +01:00 committed by GitHub
commit 3e317b7d20
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
52 changed files with 731 additions and 317 deletions

View File

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

View File

@ -1,7 +1,6 @@
const path = require('path');
module.exports = (baseConfig, env, config) => {
config.module.rules.push({
test: /\.(ts|tsx)$/,
use: [
@ -33,7 +32,12 @@ module.exports = (baseConfig, env, config) => {
config: { path: __dirname + '../../../../scripts/webpack/postcss.config.js' },
},
},
{ loader: 'sass-loader', options: { sourceMap: false } },
{
loader: 'sass-loader',
options: {
sourceMap: false
},
},
],
});
@ -52,5 +56,9 @@ module.exports = (baseConfig, env, config) => {
});
config.resolve.extensions.push('.ts', '.tsx');
// Remove pure js loading rules as Storybook's Babel config is causing problems when mixing ES6 and CJS
// More about the problem we encounter: https://github.com/webpack/webpack/issues/4039
config.module.rules = config.module.rules.filter(rule => rule.test.toString() !== /\.(mjs|jsx?)$/.toString());
return config;
};

View File

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

View File

@ -1,12 +1,12 @@
import React, { Component, createRef } from 'react';
import PopperController from '../Tooltip/PopperController';
import Popper, { RenderPopperArrowFn } from '../Tooltip/Popper';
import Popper from '../Tooltip/Popper';
import { ColorPickerPopover } from './ColorPickerPopover';
import { Themeable, GrafanaTheme } from '../../types';
import { Themeable } from '../../types';
import { getColorFromHexRgbOrName } from '../../utils/namedColorsPalette';
import { SeriesColorPickerPopover } from './SeriesColorPickerPopover';
import propDeprecationWarning from '../../utils/propDeprecationWarning';
import { withTheme } from '../../themes/ThemeContext';
type ColorPickerChangeHandler = (color: string) => void;
export interface ColorPickerProps extends Themeable {
@ -18,7 +18,6 @@ export interface ColorPickerProps extends Themeable {
*/
onColorChange?: ColorPickerChangeHandler;
enableNamedColors?: boolean;
withArrow?: boolean;
children?: JSX.Element;
}
@ -32,7 +31,6 @@ export const warnAboutColorPickerPropsDeprecation = (componentName: string, prop
export const colorPickerFactory = <T extends ColorPickerProps>(
popover: React.ComponentType<T>,
displayName = 'ColorPicker',
renderPopoverArrowFunction?: RenderPopperArrowFn
) => {
return class ColorPicker extends Component<T, any> {
static displayName = displayName;
@ -50,17 +48,7 @@ export const colorPickerFactory = <T extends ColorPickerProps>(
...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'}`}
/>
);
};
const { theme, children } = this.props;
return (
<PopperController content={popoverElement} hideAfter={300}>
@ -72,7 +60,6 @@ export const colorPickerFactory = <T extends ColorPickerProps>(
{...popperProps}
referenceElement={this.pickerTriggerRef.current}
wrapperClassName="ColorPicker"
renderArrow={withArrow && (renderPopoverArrowFunction || renderArrow)}
onMouseLeave={hidePopper}
onMouseEnter={showPopper}
/>
@ -95,7 +82,7 @@ export const colorPickerFactory = <T extends ColorPickerProps>(
<div
className="sp-preview-inner"
style={{
backgroundColor: getColorFromHexRgbOrName(this.props.color || '#000000', theme),
backgroundColor: getColorFromHexRgbOrName(this.props.color || '#000000', theme.type),
}}
/>
</div>
@ -110,5 +97,5 @@ export const colorPickerFactory = <T extends ColorPickerProps>(
};
};
export const ColorPicker = colorPickerFactory(ColorPickerPopover, 'ColorPicker');
export const SeriesColorPicker = colorPickerFactory(SeriesColorPickerPopover, 'SeriesColorPicker');
export const ColorPicker = withTheme(colorPickerFactory(ColorPickerPopover, 'ColorPicker'));
export const SeriesColorPicker = withTheme(colorPickerFactory(SeriesColorPickerPopover, 'SeriesColorPicker'));

View File

@ -1,40 +1,27 @@
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';
import { renderComponentWithTheme } from '../../utils/storybook/withTheme';
const ColorPickerPopoverStories = storiesOf('UI/ColorPicker/Popovers', module);
ColorPickerPopoverStories.addDecorator(withCenteredStory).addDecorator(withKnobs);
ColorPickerPopoverStories.addDecorator(withCenteredStory);
ColorPickerPopoverStories.add('default', () => {
const selectedTheme = getThemeKnob();
return (
<ColorPickerPopover
color="#BC67E6"
onChange={color => {
console.log(color);
}}
theme={selectedTheme || undefined}
/>
);
return renderComponentWithTheme(ColorPickerPopover, {
color: '#BC67E6',
onChange: (color: any) => {
console.log(color);
},
});
});
ColorPickerPopoverStories.add('SeriesColorPickerPopover', () => {
const selectedTheme = getThemeKnob();
return (
<SeriesColorPickerPopover
color="#BC67E6"
onChange={color => {
console.log(color);
}}
theme={selectedTheme || undefined}
/>
);
return renderComponentWithTheme(SeriesColorPickerPopover, {
color: '#BC67E6',
onChange: (color: any) => {
console.log(color);
},
});
});

View File

@ -4,7 +4,8 @@ import { ColorPickerPopover } from './ColorPickerPopover';
import { getColorDefinitionByName, getNamedColorPalette } from '../../utils/namedColorsPalette';
import { ColorSwatch } from './NamedColorsGroup';
import { flatten } from 'lodash';
import { GrafanaTheme } from '../../types';
import { GrafanaThemeType } from '../../types';
import { getTheme } from '../../themes';
const allColors = flatten(Array.from(getNamedColorPalette().values()));
@ -14,7 +15,7 @@ describe('ColorPickerPopover', () => {
describe('rendering', () => {
it('should render provided color as selected if color provided by name', () => {
const wrapper = mount(<ColorPickerPopover color={BasicGreen.name} onChange={() => {}} />);
const wrapper = mount(<ColorPickerPopover color={BasicGreen.name} onChange={() => {}} theme={getTheme()}/>);
const selectedSwatch = wrapper.find(ColorSwatch).findWhere(node => node.key() === BasicGreen.name);
const notSelectedSwatches = wrapper.find(ColorSwatch).filterWhere(node => node.prop('isSelected') === false);
@ -24,7 +25,7 @@ describe('ColorPickerPopover', () => {
});
it('should render provided color as selected if color provided by hex', () => {
const wrapper = mount(<ColorPickerPopover color={BasicGreen.variants.dark} onChange={() => {}} />);
const wrapper = mount(<ColorPickerPopover color={BasicGreen.variants.dark} onChange={() => {}} theme={getTheme()} />);
const selectedSwatch = wrapper.find(ColorSwatch).findWhere(node => node.key() === BasicGreen.name);
const notSelectedSwatches = wrapper.find(ColorSwatch).filterWhere(node => node.prop('isSelected') === false);
@ -45,7 +46,7 @@ describe('ColorPickerPopover', () => {
it('should pass hex color value to onChange prop by default', () => {
wrapper = mount(
<ColorPickerPopover color={BasicGreen.variants.dark} onChange={onChangeSpy} theme={GrafanaTheme.Light} />
<ColorPickerPopover color={BasicGreen.variants.dark} onChange={onChangeSpy} theme={getTheme(GrafanaThemeType.Light)} />
);
const basicBlueSwatch = wrapper.find(ColorSwatch).findWhere(node => node.key() === BasicBlue.name);
@ -61,7 +62,7 @@ describe('ColorPickerPopover', () => {
enableNamedColors
color={BasicGreen.variants.dark}
onChange={onChangeSpy}
theme={GrafanaTheme.Light}
theme={getTheme(GrafanaThemeType.Light)}
/>
);
const basicBlueSwatch = wrapper.find(ColorSwatch).findWhere(node => node.key() === BasicBlue.name);

View File

@ -2,9 +2,9 @@ import React from 'react';
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';
import { GrafanaThemeType } from '@grafana/ui';
export interface Props<T> extends ColorPickerProps, PopperContentProps {
customPickers?: T;
@ -43,7 +43,7 @@ export class ColorPickerPopover<T extends CustomPickersDescriptor> extends React
if (enableNamedColors) {
return changeHandler(color);
}
changeHandler(getColorFromHexRgbOrName(color, theme));
changeHandler(getColorFromHexRgbOrName(color, theme.type));
};
handleTabChange = (tab: PickerType | keyof T) => {
@ -58,7 +58,9 @@ export class ColorPickerPopover<T extends CustomPickersDescriptor> extends React
case 'spectrum':
return <SpectrumPalette color={color} onChange={this.handleChange} theme={theme} />;
case 'palette':
return <NamedColorsPalette color={getColorName(color, theme)} onChange={this.handleChange} theme={theme} />;
return (
<NamedColorsPalette color={getColorName(color, theme.type)} onChange={this.handleChange} theme={theme} />
);
default:
return this.renderCustomPicker(activePicker);
}
@ -88,11 +90,7 @@ export class ColorPickerPopover<T extends CustomPickersDescriptor> extends React
<>
{Object.keys(customPickers).map(key => {
return (
<div
className={this.getTabClassName(key)}
onClick={this.handleTabChange(key)}
key={key}
>
<div className={this.getTabClassName(key)} onClick={this.handleTabChange(key)} key={key}>
{customPickers[key].name}
</div>
);
@ -103,21 +101,14 @@ export class ColorPickerPopover<T extends CustomPickersDescriptor> extends React
render() {
const { theme } = this.props;
const colorPickerTheme = theme || GrafanaTheme.Dark;
const colorPickerTheme = theme.type || GrafanaThemeType.Dark;
return (
<div className={`ColorPickerPopover ColorPickerPopover--${colorPickerTheme}`}>
<div className="ColorPickerPopover__tabs">
<div
className={this.getTabClassName('palette')}
onClick={this.handleTabChange('palette')}
>
<div className={this.getTabClassName('palette')} onClick={this.handleTabChange('palette')}>
Colors
</div>
<div
className={this.getTabClassName('spectrum')}
onClick={this.handleTabChange('spectrum')}
>
<div className={this.getTabClassName('spectrum')} onClick={this.handleTabChange('spectrum')}>
Custom
</div>
{this.renderCustomPickerTabs()}
@ -128,3 +119,4 @@ export class ColorPickerPopover<T extends CustomPickersDescriptor> extends React
);
}
}

View File

@ -1,8 +1,9 @@
import React, { FunctionComponent } from 'react';
import { Themeable, GrafanaTheme } from '../../types';
import { Themeable } from '../../types';
import { ColorDefinition, getColorForTheme } from '../../utils/namedColorsPalette';
import { Color } from 'csstype';
import { find, upperFirst } from 'lodash';
import { selectThemeVariant } from '../../themes/selectThemeVariant';
type ColorChangeHandler = (color: ColorDefinition) => void;
@ -28,7 +29,15 @@ export const ColorSwatch: FunctionComponent<ColorSwatchProps> = ({
}) => {
const isSmall = variant === ColorSwatchVariant.Small;
const swatchSize = isSmall ? '16px' : '32px';
const selectedSwatchBorder = theme === GrafanaTheme.Light ? '#ffffff' : '#1A1B1F';
const selectedSwatchBorder = selectThemeVariant(
{
light: theme.colors.white,
dark: theme.colors.black,
},
theme.type
);
const swatchStyles = {
width: swatchSize,
height: swatchSize,
@ -76,7 +85,7 @@ const NamedColorsGroup: FunctionComponent<NamedColorsGroupProps> = ({
key={primaryColor.name}
isSelected={primaryColor.name === selectedColor}
variant={ColorSwatchVariant.Large}
color={getColorForTheme(primaryColor, theme)}
color={getColorForTheme(primaryColor, theme.type)}
label={upperFirst(primaryColor.hue)}
onClick={() => onColorSelect(primaryColor)}
theme={theme}
@ -95,7 +104,7 @@ const NamedColorsGroup: FunctionComponent<NamedColorsGroupProps> = ({
<ColorSwatch
key={color.name}
isSelected={color.name === selectedColor}
color={getColorForTheme(color, theme)}
color={getColorForTheme(color, theme.type)}
onClick={() => onColorSelect(color)}
theme={theme}
/>

View File

@ -2,8 +2,9 @@ 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 { select } from '@storybook/addon-knobs';
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
import { renderComponentWithTheme } from '../../utils/storybook/withTheme';
import { UseState } from '../../utils/storybook/UseState';
const BasicGreen = getColorDefinitionByName('green');
@ -12,7 +13,7 @@ const LightBlue = getColorDefinitionByName('light-blue');
const NamedColorsPaletteStories = storiesOf('UI/ColorPicker/Palettes/NamedColorsPalette', module);
NamedColorsPaletteStories.addDecorator(withKnobs).addDecorator(withCenteredStory);
NamedColorsPaletteStories.addDecorator(withCenteredStory);
NamedColorsPaletteStories.add('Named colors swatch - support for named colors', () => {
const selectedColor = select(
@ -28,7 +29,10 @@ NamedColorsPaletteStories.add('Named colors swatch - support for named colors',
return (
<UseState initialState={selectedColor}>
{(selectedColor, updateSelectedColor) => {
return <NamedColorsPalette color={selectedColor} onChange={updateSelectedColor} />;
return renderComponentWithTheme(NamedColorsPalette, {
color: selectedColor,
onChange: updateSelectedColor,
});
}}
</UseState>
);
@ -45,7 +49,10 @@ NamedColorsPaletteStories.add('Named colors swatch - support for named colors',
return (
<UseState initialState={selectedColor}>
{(selectedColor, updateSelectedColor) => {
return <NamedColorsPalette color={getColorName(selectedColor)} onChange={updateSelectedColor} />;
return renderComponentWithTheme(NamedColorsPalette, {
color: getColorName(selectedColor),
onChange: updateSelectedColor,
});
}}
</UseState>
);

View File

@ -3,7 +3,8 @@ import { mount, ReactWrapper } from 'enzyme';
import { NamedColorsPalette } from './NamedColorsPalette';
import { ColorSwatch } from './NamedColorsGroup';
import { getColorDefinitionByName } from '../../utils';
import { GrafanaTheme } from '../../types';
import { getTheme } from '../../themes';
import { GrafanaThemeType } from '../../types';
describe('NamedColorsPalette', () => {
@ -17,18 +18,18 @@ describe('NamedColorsPalette', () => {
});
it('should render provided color variant specific for theme', () => {
wrapper = mount(<NamedColorsPalette color={BasicGreen.name} theme={GrafanaTheme.Dark} onChange={() => {}} />);
wrapper = mount(<NamedColorsPalette color={BasicGreen.name} theme={getTheme()} 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={() => {}} />);
wrapper = mount(<NamedColorsPalette color={BasicGreen.name} theme={getTheme(GrafanaThemeType.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={() => {}} />);
wrapper = mount(<NamedColorsPalette color={BasicGreen.name} onChange={() => {}} theme={getTheme()}/>);
selectedSwatch = wrapper.find(ColorSwatch).findWhere(node => node.key() === BasicGreen.name);
expect(selectedSwatch.prop('color')).toBe(BasicGreen.variants.dark);
});

View File

@ -4,6 +4,7 @@ import { ColorPickerPopover } from './ColorPickerPopover';
import { ColorPickerProps } from './ColorPicker';
import { PopperContentProps } from '../Tooltip/PopperController';
import { Switch } from '../Switch/Switch';
import { withTheme } from '../../themes/ThemeContext';
export interface SeriesColorPickerPopoverProps extends ColorPickerProps, PopperContentProps {
yaxis?: number;
@ -12,7 +13,6 @@ export interface SeriesColorPickerPopoverProps extends ColorPickerProps, PopperC
export const SeriesColorPickerPopover: FunctionComponent<SeriesColorPickerPopoverProps> = props => {
const { yaxis, onToggleAxis, color, ...colorPickerProps } = props;
return (
<ColorPickerPopover
{...colorPickerProps}
@ -85,3 +85,6 @@ export class AxisSelector extends React.PureComponent<AxisSelectorProps, AxisSel
);
}
}
// This component is to enable SeriecColorPickerPopover usage via series-color-picker-popover directive
export const SeriesColorPickerPopoverWithTheme = withTheme(SeriesColorPickerPopover);

View File

@ -1,22 +1,19 @@
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';
import { renderComponentWithTheme } from '../../utils/storybook/withTheme';
const SpectrumPaletteStories = storiesOf('UI/ColorPicker/Palettes/SpectrumPalette', module);
SpectrumPaletteStories.addDecorator(withCenteredStory).addDecorator(withKnobs);
SpectrumPaletteStories.addDecorator(withCenteredStory);
SpectrumPaletteStories.add('default', () => {
const selectedTheme = getThemeKnob();
return (
<UseState initialState="red">
{(selectedColor, updateSelectedColor) => {
return <SpectrumPalette theme={selectedTheme} color={selectedColor} onChange={updateSelectedColor} />;
return renderComponentWithTheme(SpectrumPalette, { color: selectedColor, onChange: updateSelectedColor });
}}
</UseState>
);

View File

@ -13,7 +13,7 @@ export interface SpectrumPaletteProps extends Themeable {
onChange: (color: string) => void;
}
const renderPointer = (theme?: GrafanaTheme) => (props: SpectrumPalettePointerProps) => (
const renderPointer = (theme: GrafanaTheme) => (props: SpectrumPalettePointerProps) => (
<SpectrumPalettePointer {...props} theme={theme} />
);
@ -92,7 +92,7 @@ const SpectrumPalette: React.FunctionComponent<SpectrumPaletteProps> = ({ color,
}}
theme={theme}
/>
<ColorInput color={color} onChange={onChange} style={{ marginTop: '16px' }} />
<ColorInput theme={theme} color={color} onChange={onChange} style={{ marginTop: '16px' }} />
</div>
);
};

View File

@ -1,14 +1,12 @@
import React from 'react';
import { GrafanaTheme, Themeable } from '../../types';
import { Themeable } from '../../types';
import { selectThemeVariant } from '../../themes/selectThemeVariant';
export interface SpectrumPalettePointerProps extends Themeable {
direction?: string;
}
const SpectrumPalettePointer: React.FunctionComponent<SpectrumPalettePointerProps> = ({
theme,
direction,
}) => {
const SpectrumPalettePointer: React.FunctionComponent<SpectrumPalettePointerProps> = ({ theme, direction }) => {
const styles = {
picker: {
width: '16px',
@ -17,7 +15,14 @@ const SpectrumPalettePointer: React.FunctionComponent<SpectrumPalettePointerProp
},
};
const pointerColor = theme === GrafanaTheme.Light ? '#3F444D' : '#8E8E8E';
const pointerColor = selectThemeVariant(
{
light: theme.colors.dark3,
dark: theme.colors.gray2,
},
theme.type
);
let pointerStyles: React.CSSProperties = {
position: 'absolute',

View File

@ -3,6 +3,7 @@ import { shallow } from 'enzyme';
import { Gauge, Props } from './Gauge';
import { ValueMapping, MappingType } from '../../types';
import { getTheme } from '../../themes';
jest.mock('jquery', () => ({
plot: jest.fn(),
@ -24,6 +25,7 @@ const setup = (propOverrides?: object) => {
width: 300,
value: 25,
decimals: 0,
theme: getTheme()
};
Object.assign(props, propOverrides);

View File

@ -1,13 +1,14 @@
import React, { PureComponent } from 'react';
import $ from 'jquery';
import { ValueMapping, Threshold, BasicGaugeColor, GrafanaTheme } from '../../types';
import { ValueMapping, Threshold, BasicGaugeColor, GrafanaThemeType } from '../../types';
import { getMappedValue } from '../../utils/valueMappings';
import { getColorFromHexRgbOrName, getValueFormat } from '../../utils';
import { Themeable } from '../../index';
type TimeSeriesValue = string | number | null;
export interface Props {
export interface Props extends Themeable {
decimals: number;
height: number;
valueMappings: ValueMapping[];
@ -22,7 +23,6 @@ export interface Props {
unit: string;
width: number;
value: number;
theme?: GrafanaTheme;
}
const FONT_SCALE = 1;
@ -41,7 +41,7 @@ export class Gauge extends PureComponent<Props> {
thresholds: [],
unit: 'none',
stat: 'avg',
theme: GrafanaTheme.Dark,
theme: GrafanaThemeType.Dark,
};
componentDidMount() {
@ -77,19 +77,19 @@ export class Gauge extends PureComponent<Props> {
const { thresholds, theme } = this.props;
if (thresholds.length === 1) {
return getColorFromHexRgbOrName(thresholds[0].color, theme);
return getColorFromHexRgbOrName(thresholds[0].color, theme.type);
}
const atThreshold = thresholds.filter(threshold => (value as number) === threshold.value)[0];
if (atThreshold) {
return getColorFromHexRgbOrName(atThreshold.color, theme);
return getColorFromHexRgbOrName(atThreshold.color, theme.type);
}
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 getColorFromHexRgbOrName(nearestThreshold.color, theme);
return getColorFromHexRgbOrName(nearestThreshold.color, theme.type);
}
return BasicGaugeColor.Red;
@ -104,13 +104,13 @@ export class Gauge extends PureComponent<Props> {
return [
...thresholdsSortedByIndex.map(threshold => {
if (threshold.index === 0) {
return { value: minValue, color: getColorFromHexRgbOrName(threshold.color, theme) };
return { value: minValue, color: getColorFromHexRgbOrName(threshold.color, theme.type) };
}
const previousThreshold = thresholdsSortedByIndex[threshold.index - 1];
return { value: threshold.value, color: getColorFromHexRgbOrName(previousThreshold.color, theme) };
return { value: threshold.value, color: getColorFromHexRgbOrName(previousThreshold.color, theme.type) };
}),
{ value: maxValue, color: getColorFromHexRgbOrName(lastThreshold.color, theme) },
{ value: maxValue, color: getColorFromHexRgbOrName(lastThreshold.color, theme.type) },
];
}
@ -126,7 +126,8 @@ export class Gauge extends PureComponent<Props> {
const formattedValue = this.formatValue(value) as string;
const dimension = Math.min(width, height * 1.3);
const backgroundColor = theme === GrafanaTheme.Light ? 'rgb(230,230,230)' : 'rgb(38,38,38)';
const backgroundColor = theme.type === GrafanaThemeType.Light ? 'rgb(230,230,230)' : theme.colors.dark3;
const gaugeWidthReduceRatio = showThresholdLabels ? 1.5 : 1;
const gaugeWidth = Math.min(dimension / 6, 60) / gaugeWidthReduceRatio;
const thresholdMarkersWidth = gaugeWidth / 5;

View File

@ -1,11 +1,11 @@
import React, { PureComponent } from 'react';
import { Threshold, Themeable } from '../../types';
import { Threshold } from '../../types';
import { ColorPicker } from '../ColorPicker/ColorPicker';
import { PanelOptionsGroup } from '../PanelOptionsGroup/PanelOptionsGroup';
import { colors } from '../../utils';
import { getColorFromHexRgbOrName } from '@grafana/ui';
import { getColorFromHexRgbOrName, ThemeContext } from '@grafana/ui';
export interface Props extends Themeable {
export interface Props {
thresholds: Threshold[];
onChange: (thresholds: Threshold[]) => void;
}
@ -164,7 +164,10 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
<div className="thresholds-row-input-inner-color">
{threshold.color && (
<div className="thresholds-row-input-inner-color-colorpicker">
<ColorPicker color={threshold.color} onChange={color => this.onChangeThresholdColor(threshold, color)} />
<ColorPicker
color={threshold.color}
onChange={color => this.onChangeThresholdColor(threshold, color)}
/>
</div>
)}
</div>
@ -188,27 +191,35 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
render() {
const { thresholds } = this.state;
const { theme } = this.props;
return (
<PanelOptionsGroup title="Thresholds">
<div className="thresholds">
{thresholds.map((threshold, index) => {
return (
<div className="thresholds-row" key={`${threshold.index}-${index}`}>
<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: getColorFromHexRgbOrName(threshold.color, theme) }}
/>
<div className="thresholds-row-input">{this.renderInput(threshold)}</div>
<ThemeContext.Consumer>
{theme => {
return (
<PanelOptionsGroup title="Thresholds">
<div className="thresholds">
{thresholds.map((threshold, index) => {
return (
<div className="thresholds-row" key={`${threshold.index}-${index}`}>
<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: getColorFromHexRgbOrName(threshold.color, theme.type) }}
/>
<div className="thresholds-row-input">{this.renderInput(threshold)}</div>
</div>
);
})}
</div>
);
})}
</div>
</PanelOptionsGroup>
</PanelOptionsGroup>
);
}}
</ThemeContext.Consumer>
);
}
}

View File

@ -14,8 +14,8 @@ export { FormLabel } from './FormLabel/FormLabel';
export { FormField } from './FormField/FormField';
export { LoadingPlaceholder } from './LoadingPlaceholder/LoadingPlaceholder';
export { ColorPicker, SeriesColorPicker } from './ColorPicker/ColorPicker';
export { SeriesColorPickerPopover } from './ColorPicker/SeriesColorPickerPopover';
export { ColorPicker, SeriesColorPicker } from './ColorPicker/ColorPicker';
export { SeriesColorPickerPopover, SeriesColorPickerPopoverWithTheme } from './ColorPicker/SeriesColorPickerPopover';
export { ThresholdsEditor } from './ThresholdsEditor/ThresholdsEditor';
export { Graph } from './Graph/Graph';
export { PanelOptionsGroup } from './PanelOptionsGroup/PanelOptionsGroup';

View File

@ -1,3 +1,5 @@
export * from './components';
export * from './types';
export * from './utils';
export * from './themes';
export * from './themes/ThemeContext';

View File

@ -0,0 +1,20 @@
import React from 'react';
import { GrafanaThemeType, Themeable } from '../types';
import { getTheme } from './index';
type Omit<T, K> = Pick<T, Exclude<keyof T, K>>;
type Subtract<T, K> = Omit<T, keyof K>;
// Use Grafana Dark theme by default
export const ThemeContext = React.createContext(getTheme(GrafanaThemeType.Dark));
export const withTheme = <P extends Themeable>(Component: React.ComponentType<P>) => {
const WithTheme: React.FunctionComponent<Subtract<P, Themeable>> = props => {
// @ts-ignore
return <ThemeContext.Consumer>{theme => <Component {...props} theme={theme} />}</ThemeContext.Consumer>;
};
WithTheme.displayName = `WithTheme(${Component.displayName})`;
return WithTheme;
};

View File

@ -0,0 +1,69 @@
import tinycolor from 'tinycolor2';
import defaultTheme from './default';
import { GrafanaTheme, GrafanaThemeType } from '../types/theme';
const basicColors = {
black: '#000000',
white: '#ffffff',
dark1: '#141414',
dark2: '#1f1f20',
dark3: '#262628',
dark4: '#333333',
dark5: '#444444',
gray1: '#555555',
gray2: '#8e8e8e',
gray3: '#b3b3b3',
gray4: '#d8d9da',
gray5: '#ececec',
gray6: '#f4f5f8',
gray7: '#fbfbfb',
grayBlue: '#212327',
blue: '#33b5e5',
blueDark: '#005f81',
blueLight: '#00a8e6', // not used in dark theme
green: '#299c46',
red: '#d44a3a',
yellow: '#ecbb13',
pink: '#ff4444',
purple: '#9933cc',
variable: '#32d1df',
orange: '#eb7b18',
};
const darkTheme: GrafanaTheme = {
...defaultTheme,
type: GrafanaThemeType.Dark,
name: 'Grafana Dark',
colors: {
...basicColors,
inputBlack: '#09090b',
queryRed: '#e24d42',
queryGreen: '#74e680',
queryPurple: '#fe85fc',
queryKeyword: '#66d9ef',
queryOrange: 'eb7b18',
online: '#10a345',
warn: '#f79520',
critical: '#ed2e18',
bodyBg: '#171819',
pageBg: '#161719',
bodyColor: basicColors.gray4,
textColor: basicColors.gray4,
textColorStrong: basicColors.white,
textColorWeak: basicColors.gray2,
textColorEmphasis: basicColors.gray5,
textColorFaint: basicColors.dark5,
linkColor: new tinycolor(basicColors.white).darken(11).toString(),
linkColorDisabled: new tinycolor(basicColors.white).darken(11).toString(),
linkColorHover: basicColors.white,
linkColorExternal: basicColors.blue,
headingColor: new tinycolor(basicColors.white).darken(11).toString(),
},
background: {
dropdown: basicColors.dark3,
scrollbar: '#aeb5df',
scrollbar2: '#3a3a3a',
},
};
export default darkTheme;

View File

@ -0,0 +1,62 @@
const theme = {
name: 'Grafana Default',
typography: {
fontFamily: {
sansSerif: "'Roboto', Helvetica, Arial, sans-serif;",
serif: "Georgia, 'Times New Roman', Times, serif;",
monospace: "Menlo, Monaco, Consolas, 'Courier New', monospace;"
},
size: {
base: '13px',
xs: '10px',
s: '12px',
m: '14px',
l: '18px',
},
heading: {
h1: '2rem',
h2: '1.75rem',
h3: '1.5rem',
h4: '1.3rem',
h5: '1.2rem',
h6: '1rem',
},
weight: {
light: 300,
normal: 400,
semibold: 500,
},
lineHeight: {
xs: 1,
s: 1.1,
m: 4/3,
l: 1.5
}
},
brakpoints: {
xs: '0',
s: '544px',
m: '768px',
l: '992px',
xl: '1200px'
},
spacing: {
xs: '0',
s: '0.2rem',
m: '1rem',
l: '1.5rem',
xl: '3rem',
gutter: '30px',
},
border: {
radius: {
xs: '2px',
s: '3px',
m: '5px',
}
}
};
export default theme;

View File

@ -0,0 +1,14 @@
import darkTheme from './dark';
import lightTheme from './light';
import { GrafanaTheme } from '../types/theme';
let themeMock: ((name?: string) => GrafanaTheme) | null;
export let getTheme = (name?: string) => (themeMock && themeMock(name)) || (name === 'light' ? lightTheme : darkTheme);
export const mockTheme = (mock: (name: string) => GrafanaTheme) => {
themeMock = mock;
return () => {
themeMock = null;
};
};

View File

@ -0,0 +1,70 @@
import tinycolor from 'tinycolor2';
import defaultTheme from './default';
import { GrafanaTheme, GrafanaThemeType } from '../types/theme';
const basicColors = {
black: '#000000',
white: '#ffffff',
dark1: '#13161d',
dark2: '#1e2028',
dark3: '#303133',
dark4: '#35373f',
dark5: '#41444b',
gray1: '#52545c',
gray2: '#767980',
gray3: '#acb6bf',
gray4: '#c7d0d9',
gray5: '#dde4ed',
gray6: '#e9edf2',
gray7: '#f7f8fa',
grayBlue: '#212327', // not used in light theme
blue: '#0083b3',
blueDark: '#005f81',
blueLight: '#00a8e6',
green: '#3aa655',
red: '#d44939',
yellow: '#ff851b',
pink: '#e671b8',
purple: '#9954bb',
variable: '#0083b3',
orange: '#ff7941',
};
const lightTheme: GrafanaTheme = {
...defaultTheme,
type: GrafanaThemeType.Light,
name: 'Grafana Light',
colors: {
...basicColors,
variable: basicColors.blue,
inputBlack: '#09090b',
queryRed: basicColors.red,
queryGreen: basicColors.green,
queryPurple: basicColors.purple,
queryKeyword: basicColors.blue,
queryOrange: basicColors.orange,
online: '#01a64f',
warn: '#f79520',
critical: '#ec2128',
bodyBg: basicColors.gray7,
pageBg: basicColors.gray7,
bodyColor: basicColors.gray1,
textColor: basicColors.gray1,
textColorStrong: basicColors.dark2,
textColorWeak: basicColors.gray2,
textColorEmphasis: basicColors.gray5,
textColorFaint: basicColors.dark4,
linkColor: basicColors.gray1,
linkColorDisabled: new tinycolor(basicColors.gray1).lighten(30).toString(),
linkColorHover: new tinycolor(basicColors.gray1).darken(20).toString(),
linkColorExternal: basicColors.blueLight,
headingColor: basicColors.gray1,
},
background: {
dropdown: basicColors.white,
scrollbar: basicColors.gray5,
scrollbar2: basicColors.gray5,
},
};
export default lightTheme;

View File

@ -0,0 +1,52 @@
import { GrafanaThemeType } from '../types/theme';
import { selectThemeVariant } from './selectThemeVariant';
import { mockTheme } from './index';
const lightThemeMock = {
color: {
red: '#ff0000',
green: '#00ff00',
},
};
const darkThemeMock = {
color: {
red: '#ff0000',
green: '#00ff00',
},
};
describe('Theme variable variant selector', () => {
// @ts-ignore
const restoreTheme = mockTheme(name => (name === GrafanaThemeType.Light ? lightThemeMock : darkThemeMock));
afterAll(() => {
restoreTheme();
});
it('return correct variable value for given theme', () => {
const theme = lightThemeMock;
const selectedValue = selectThemeVariant(
{
dark: theme.color.red,
light: theme.color.green,
},
GrafanaThemeType.Light
);
expect(selectedValue).toBe(lightThemeMock.color.green);
});
it('return dark theme variant if no theme given', () => {
const theme = lightThemeMock;
const selectedValue = selectThemeVariant(
{
dark: theme.color.red,
light: theme.color.green,
}
);
expect(selectedValue).toBe(lightThemeMock.color.red);
});
});

View File

@ -0,0 +1,9 @@
import { GrafanaThemeType } from '../types/theme';
type VariantDescriptor = {
[key in GrafanaThemeType]: string | number;
};
export const selectThemeVariant = (variants: VariantDescriptor, currentTheme?: GrafanaThemeType) => {
return variants[currentTheme || GrafanaThemeType.Dark];
};

View File

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

View File

@ -0,0 +1,129 @@
export enum GrafanaThemeType {
Light = 'light',
Dark = 'dark',
}
export interface GrafanaTheme {
type: GrafanaThemeType;
name: string;
// TODO: not sure if should be a part of theme
brakpoints: {
xs: string;
s: string;
m: string;
l: string;
xl: string;
};
typography: {
fontFamily: {
sansSerif: string;
serif: string;
monospace: string;
};
size: {
base: string;
xs: string;
s: string;
m: string;
l: string;
};
weight: {
light: number;
normal: number;
semibold: number;
};
lineHeight: {
xs: number; //1
s: number; //1.1
m: number; // 4/3
l: number; // 1.5
};
// TODO: Refactor to use size instead of custom defs
heading: {
h1: string;
h2: string;
h3: string;
h4: string;
h5: string;
h6: string;
};
};
spacing: {
xs: string;
s: string;
m: string;
l: string;
gutter: string;
};
border: {
radius: {
xs: string;
s: string;
m: string;
};
};
background: {
dropdown: string;
scrollbar: string;
scrollbar2: string;
};
colors: {
black: string;
white: string;
dark1: string;
dark2: string;
dark3: string;
dark4: string;
dark5: string;
gray1: string;
gray2: string;
gray3: string;
gray4: string;
gray5: string;
gray6: string;
gray7: string;
grayBlue: string;
inputBlack: string;
// Accent colors
blue: string;
blueLight: string;
blueDark: string;
green: string;
red: string;
yellow: string;
pink: string;
purple: string;
variable: string;
orange: string;
queryRed: string;
queryGreen: string;
queryPurple: string;
queryKeyword: string;
queryOrange: string;
// Status colors
online: string;
warn: string;
critical: string;
// TODO: move to background section
bodyBg: string;
pageBg: string;
bodyColor: string;
textColor: string;
textColorStrong: string;
textColorWeak: string;
textColorFaint: string;
textColorEmphasis: string;
linkColor: string;
linkColorDisabled: string;
linkColorHover: string;
linkColorExternal: string;
headingColor: string;
};
}
export interface Themeable {
theme: GrafanaTheme;
}

View File

@ -5,20 +5,20 @@ import {
getColorFromHexRgbOrName,
getColorDefinitionByName,
} from './namedColorsPalette';
import { GrafanaTheme } from '../types/index';
import { GrafanaThemeType } 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();
expect(getColorDefinition('#ff0000', GrafanaThemeType.Light)).toBeUndefined();
expect(getColorDefinition('#ff0000', GrafanaThemeType.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);
expect(getColorDefinition(SemiDarkBlue.variants.light, GrafanaThemeType.Light)).toEqual(SemiDarkBlue);
expect(getColorDefinition(SemiDarkBlue.variants.dark, GrafanaThemeType.Dark)).toEqual(SemiDarkBlue);
});
});
@ -28,8 +28,8 @@ describe('colors', () => {
});
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);
expect(getColorName(SemiDarkBlue.variants.light, GrafanaThemeType.Light)).toEqual(SemiDarkBlue.name);
expect(getColorName(SemiDarkBlue.variants.dark, GrafanaThemeType.Dark)).toEqual(SemiDarkBlue.name);
});
});
@ -53,7 +53,7 @@ describe('colors', () => {
});
it("returns correct variant's hex for known color if theme specified", () => {
expect(getColorFromHexRgbOrName(SemiDarkBlue.name, GrafanaTheme.Light)).toBe(SemiDarkBlue.variants.light);
expect(getColorFromHexRgbOrName(SemiDarkBlue.name, GrafanaThemeType.Light)).toBe(SemiDarkBlue.variants.light);
});
it('returns color if specified as hex or rgb/a', () => {

View File

@ -1,5 +1,5 @@
import { flatten } from 'lodash';
import { GrafanaTheme } from '../types';
import { GrafanaThemeType } from '../types';
type Hue = 'green' | 'yellow' | 'red' | 'blue' | 'orange' | 'purple';
@ -68,7 +68,7 @@ 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 => {
export const getColorDefinition = (hex: string, theme: GrafanaThemeType): ColorDefinition | undefined => {
return flatten(Array.from(getNamedColorPalette().values())).filter(definition => definition.variants[theme] === hex)[0];
};
@ -77,7 +77,7 @@ const isHex = (color: string) => {
return hexRegex.test(color);
};
export const getColorName = (color?: string, theme?: GrafanaTheme): Color | undefined => {
export const getColorName = (color?: string, theme?: GrafanaThemeType): Color | undefined => {
if (!color) {
return undefined;
}
@ -86,7 +86,7 @@ export const getColorName = (color?: string, theme?: GrafanaTheme): Color | unde
return undefined;
}
if (isHex(color)) {
const definition = getColorDefinition(color, theme || GrafanaTheme.Dark);
const definition = getColorDefinition(color, theme || GrafanaThemeType.Dark);
return definition ? definition.name : undefined;
}
@ -98,7 +98,7 @@ export const getColorByName = (colorName: string) => {
return definition.length > 0 ? definition[0] : undefined;
};
export const getColorFromHexRgbOrName = (color: string, theme?: GrafanaTheme): string => {
export const getColorFromHexRgbOrName = (color: string, theme?: GrafanaThemeType): string => {
if (color.indexOf('rgb') > -1 || isHex(color)) {
return color;
}
@ -112,14 +112,14 @@ export const getColorFromHexRgbOrName = (color: string, theme?: GrafanaTheme): s
return theme ? colorDefinition.variants[theme] : colorDefinition.variants.dark;
};
export const getColorForTheme = (color: ColorDefinition, theme?: GrafanaTheme) => {
export const getColorForTheme = (color: ColorDefinition, theme?: GrafanaThemeType) => {
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 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']);

View File

@ -1,14 +0,0 @@
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,41 @@
import React from 'react';
import { RenderFunction } from '@storybook/react';
import { ThemeContext } from '../../themes/ThemeContext';
import { select } from '@storybook/addon-knobs';
import { getTheme } from '../../themes';
import { GrafanaThemeType } from '../../types';
const ThemableStory: React.FunctionComponent<{}> = ({ children }) => {
const themeKnob = select(
'Theme',
{
Light: GrafanaThemeType.Light,
Dark: GrafanaThemeType.Dark,
},
GrafanaThemeType.Dark
);
return (
<ThemeContext.Provider value={getTheme(themeKnob)}>
{children}
</ThemeContext.Provider>
);
};
// Temporary solution. When we update to Storybook V5 we will be able to pass data from decorator to story
// https://github.com/storybooks/storybook/issues/340#issuecomment-456013702
export const renderComponentWithTheme = (component: React.ComponentType<any>, props: any) => {
return (
<ThemeContext.Consumer>
{theme => {
return React.createElement(component, {
...props,
theme,
});
}}
</ThemeContext.Consumer>
);
};
export const withTheme = (story: RenderFunction) => <ThemableStory>{story()}</ThemableStory>;

View File

@ -9,7 +9,7 @@ import { TagFilter } from './components/TagFilter/TagFilter';
import { SideMenu } from './components/sidemenu/SideMenu';
import { MetricSelect } from './components/Select/MetricSelect';
import AppNotificationList from './components/AppNotifications/AppNotificationList';
import { ColorPicker, SeriesColorPickerPopover } from '@grafana/ui';
import { ColorPicker, SeriesColorPickerPopoverWithTheme } from '@grafana/ui';
export function registerAngularDirectives() {
react2AngularDirective('passwordStrength', PasswordStrength, ['password']);
@ -27,7 +27,7 @@ export function registerAngularDirectives() {
'color',
['onChange', { watchDepth: 'reference', wrapApply: true }],
]);
react2AngularDirective('seriesColorPickerPopover', SeriesColorPickerPopover, [
react2AngularDirective('seriesColorPickerPopover', SeriesColorPickerPopoverWithTheme, [
'color',
'series',
'onColorChange',

View File

@ -1,5 +1,6 @@
import _ from 'lodash';
import { PanelPlugin } from 'app/types/plugins';
import { GrafanaTheme, getTheme, GrafanaThemeType } from '@grafana/ui';
export interface BuildInfo {
version: string;
@ -36,8 +37,11 @@ export class Settings {
loginError: any;
viewersCanEdit: boolean;
disableSanitizeHtml: boolean;
theme: GrafanaTheme;
constructor(options: Settings) {
this.theme = options.bootData.user.lightTheme ? getTheme(GrafanaThemeType.Light) : getTheme(GrafanaThemeType.Dark);
const defaults = {
datasources: {},
windowTitlePrefix: 'Grafana - ',

View File

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

View File

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

View File

@ -2,7 +2,7 @@
import React, { PureComponent } from 'react';
// Services & Utils
import { processTimeSeries } from '@grafana/ui';
import { processTimeSeries, ThemeContext } from '@grafana/ui';
// Components
import { Gauge } from '@grafana/ui';
@ -10,7 +10,6 @@ import { Gauge } from '@grafana/ui';
// Types
import { GaugeOptions } from './types';
import { PanelProps, NullValueMode, TimeSeriesValue } from '@grafana/ui/src/types';
import { ThemeProvider } from 'app/core/utils/ConfigProvider';
interface Props extends PanelProps<GaugeOptions> {}
@ -38,7 +37,7 @@ export class GaugePanel extends PureComponent<Props> {
}
return (
<ThemeProvider>
<ThemeContext.Consumer>
{theme => (
<Gauge
value={value}
@ -50,7 +49,7 @@ export class GaugePanel extends PureComponent<Props> {
theme={theme}
/>
)}
</ThemeProvider>
</ThemeContext.Consumer>
);
}
}

View File

@ -11,7 +11,6 @@ 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,24 +45,17 @@ export default class GaugePanelOptions extends PureComponent<PanelOptionsProps<G
render() {
const { onChange, options } = this.props;
return (
<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} />
</>
)}
</ThemeProvider>
return (
<>
<PanelOptionsGrid>
<ValueOptions onChange={onChange} options={options} />
<GaugeOptionsEditor onChange={onChange} options={options} />
<ThresholdsEditor onChange={this.onThresholdsChanged} thresholds={options.thresholds} />
</PanelOptionsGrid>
<ValueMappingsEditor onChange={this.onValueMappingsChanged} valueMappings={options.valueMappings} />
</>
);
}
}

View File

@ -2,7 +2,6 @@ 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'];
@ -168,24 +167,17 @@ class LegendSeriesIcon extends PureComponent<LegendSeriesIconProps, LegendSeries
render() {
return (
<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>
<SeriesColorPicker
yaxis={this.props.yaxis}
color={this.props.color}
onChange={this.props.onColorChange}
onToggleAxis={this.props.onToggleAxis}
enableNamedColors
>
<span className="graph-legend-icon">
<SeriesIcon color={this.props.color} />
</span>
</SeriesColorPicker>
);
}
}

View File

@ -1,5 +1,5 @@
import _ from 'lodash';
import { colors, GrafanaTheme, getColorFromHexRgbOrName } from '@grafana/ui';
import { colors, getColorFromHexRgbOrName } from '@grafana/ui';
import TimeSeries from 'app/core/time_series2';
import config from 'app/core/config';
@ -113,7 +113,7 @@ export class DataProcessor {
const series = new TimeSeries({
datapoints: datapoints,
alias: alias,
color: getColorFromHexRgbOrName(color, config.bootData.user.lightTheme ? GrafanaTheme.Light : GrafanaTheme.Dark),
color: getColorFromHexRgbOrName(color, config.theme.type),
unit: seriesData.unit,
});

View File

@ -25,7 +25,10 @@ import ReactDOM from 'react-dom';
import { Legend, GraphLegendProps } from './Legend/Legend';
import { GraphCtrl } from './module';
import { GrafanaTheme, getValueFormat } from '@grafana/ui';
import { getValueFormat } from '@grafana/ui';
import { provideTheme } from 'app/core/utils/ConfigProvider';
const LegendWithThemeProvider = provideTheme(Legend);
class GraphElement {
ctrl: GraphCtrl;
@ -43,6 +46,7 @@ class GraphElement {
legendElem: HTMLElement;
constructor(private scope, private elem, private timeSrv) {
this.ctrl = scope.ctrl;
this.dashboard = this.ctrl.dashboard;
this.panel = this.ctrl.panel;
@ -51,10 +55,7 @@ class GraphElement {
this.panelWidth = 0;
this.eventManager = new EventManager(this.ctrl);
this.thresholdManager = new ThresholdManager(this.ctrl);
this.timeRegionManager = new TimeRegionManager(
this.ctrl,
config.bootData.user.lightTheme ? GrafanaTheme.Light : GrafanaTheme.Dark
);
this.timeRegionManager = new TimeRegionManager(this.ctrl, config.theme.type);
this.tooltip = new GraphTooltip(this.elem, this.ctrl.dashboard, this.scope, () => {
return this.sortedSeries;
});
@ -109,7 +110,7 @@ class GraphElement {
onToggleAxis: this.ctrl.onToggleAxis,
};
const legendReactElem = React.createElement(Legend, legendProps);
const legendReactElem = React.createElement(LegendWithThemeProvider, legendProps);
ReactDOM.render(legendReactElem, this.legendElem, () => this.renderPanel());
}

View File

@ -10,7 +10,7 @@ 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';
import { getColorFromHexRgbOrName } from '@grafana/ui';
class GraphCtrl extends MetricsPanelCtrl {
static template = template;
@ -244,7 +244,7 @@ class GraphCtrl extends MetricsPanelCtrl {
}
onColorChange = (series, color) => {
series.setColor(getColorFromHexRgbOrName(color, config.bootData.user.lightTheme ? GrafanaTheme.Light : GrafanaTheme.Dark));
series.setColor(getColorFromHexRgbOrName(color, config.theme.type));
this.panel.aliasColors[series.alias] = color;
this.render();
};

View File

@ -1,7 +1,7 @@
import 'vendor/flot/jquery.flot';
import _ from 'lodash';
import moment from 'moment';
import { GrafanaTheme, getColorFromHexRgbOrName } from '@grafana/ui';
import { GrafanaThemeType, getColorFromHexRgbOrName } from '@grafana/ui';
type TimeRegionColorDefinition = {
fill: string;
@ -43,7 +43,7 @@ export function getColorModes() {
});
}
function getColor(timeRegion, theme: GrafanaTheme): TimeRegionColorDefinition {
function getColor(timeRegion, theme: GrafanaThemeType): TimeRegionColorDefinition {
if (Object.keys(colorModes).indexOf(timeRegion.colorMode) === -1) {
timeRegion.colorMode = 'red';
}
@ -58,7 +58,7 @@ function getColor(timeRegion, theme: GrafanaTheme): TimeRegionColorDefinition {
const colorMode = colorModes[timeRegion.colorMode];
if (colorMode.themeDependent === true) {
return theme === GrafanaTheme.Light ? colorMode.lightColor : colorMode.darkColor;
return theme === GrafanaThemeType.Light ? colorMode.lightColor : colorMode.darkColor;
}
return {
@ -71,7 +71,7 @@ export class TimeRegionManager {
plot: any;
timeRegions: any;
constructor(private panelCtrl, private theme: GrafanaTheme = GrafanaTheme.Dark) {}
constructor(private panelCtrl, private theme: GrafanaThemeType = GrafanaThemeType.Dark) {}
draw(plot) {
this.timeRegions = this.panelCtrl.panel.timeRegions;

View File

@ -5,7 +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';
import { GrafanaThemeType, getColorFromHexRgbOrName } from '@grafana/ui';
const LEGEND_HEIGHT_PX = 6;
const LEGEND_WIDTH_PX = 100;
@ -250,7 +250,7 @@ function drawSimpleOpacityLegend(elem, options) {
.attr('stroke-width', 0)
.attr(
'fill',
getColorFromHexRgbOrName(options.cardColor, contextSrv.user.lightTheme ? GrafanaTheme.Light : GrafanaTheme.Dark)
getColorFromHexRgbOrName(options.cardColor, contextSrv.user.lightTheme ? GrafanaThemeType.Light : GrafanaThemeType.Dark)
)
.style('opacity', d => legendOpacityScale(d));
}

View File

@ -7,7 +7,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, getValueFormat } from '@grafana/ui';
import { GrafanaThemeType, getColorFromHexRgbOrName, getValueFormat } from '@grafana/ui';
const MIN_CARD_SIZE = 1,
CARD_PADDING = 1,
@ -663,7 +663,7 @@ export class HeatmapRenderer {
if (this.panel.color.mode === 'opacity') {
return getColorFromHexRgbOrName(
this.panel.color.cardColor,
contextSrv.user.lightTheme ? GrafanaTheme.Light : GrafanaTheme.Dark
contextSrv.user.lightTheme ? GrafanaThemeType.Light : GrafanaThemeType.Dark
);
} else {
return this.colorScale(d.count);

View File

@ -8,7 +8,7 @@ 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 { GrafanaTheme, getValueFormat, getColorFromHexRgbOrName } from '@grafana/ui';
import { GrafanaThemeType, getValueFormat, getColorFromHexRgbOrName } from '@grafana/ui';
class SingleStatCtrl extends MetricsPanelCtrl {
static templateUrl = 'module.html';
@ -588,10 +588,7 @@ class SingleStatCtrl extends MetricsPanelCtrl {
fill: 1,
zero: false,
lineWidth: 1,
fillColor: getColorFromHexRgbOrName(
panel.sparkline.fillColor,
config.bootData.user.lightTheme ? GrafanaTheme.Light : GrafanaTheme.Dark
),
fillColor: getColorFromHexRgbOrName(panel.sparkline.fillColor, config.theme.type),
},
},
yaxes: { show: false },
@ -608,10 +605,7 @@ class SingleStatCtrl extends MetricsPanelCtrl {
const plotSeries = {
data: data.flotpairs,
color: getColorFromHexRgbOrName(
panel.sparkline.lineColor,
config.bootData.user.lightTheme ? GrafanaTheme.Light : GrafanaTheme.Dark
),
color: getColorFromHexRgbOrName(panel.sparkline.lineColor, config.theme.type),
};
$.plot(plotCanvas, [plotSeries], options);
@ -630,7 +624,7 @@ class SingleStatCtrl extends MetricsPanelCtrl {
// 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)
getColorFromHexRgbOrName(color, config.bootData.user.lightTheme ? GrafanaThemeType.Light : GrafanaThemeType.Dark)
);
const body = panel.gauge.show ? '' : getBigValueHtml();

View File

@ -6,7 +6,6 @@ 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';
@ -131,7 +130,7 @@ class TablePanelCtrl extends MetricsPanelCtrl {
this.dashboard.isTimezoneUtc(),
this.$sanitize,
this.templateSrv,
config.bootData.user.lightTheme ? GrafanaTheme.Light : GrafanaTheme.Dark,
config.theme.type
);
return super.render(this.table);

View File

@ -1,7 +1,7 @@
import _ from 'lodash';
import moment from 'moment';
import kbn from 'app/core/utils/kbn';
import { GrafanaTheme, getValueFormat, getColorFromHexRgbOrName } from '@grafana/ui';
import { getValueFormat, getColorFromHexRgbOrName, GrafanaThemeType } from '@grafana/ui';
export class TableRenderer {
formatters: any[];
@ -13,7 +13,7 @@ export class TableRenderer {
private isUtc,
private sanitize,
private templateSrv,
private theme?: GrafanaTheme
private theme?: GrafanaThemeType
) {
this.initColumns();
}

View File

@ -5,6 +5,7 @@ import { Provider } from 'react-redux';
import coreModule from 'app/core/core_module';
import { store } from 'app/store/store';
import { ContextSrv } from 'app/core/services/context_srv';
import { provideTheme } from 'app/core/utils/ConfigProvider';
function WrapInProvider(store, Component, props) {
return (
@ -49,7 +50,7 @@ export function reactContainer(
document.body.classList.add('is-react');
ReactDOM.render(WrapInProvider(store, component, props), elem[0]);
ReactDOM.render(WrapInProvider(store, provideTheme(component), props), elem[0]);
scope.$on('$destroy', () => {
document.body.classList.remove('is-react');

View File

@ -1,6 +1,6 @@
'use strict';
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = function(options) {
return {
@ -23,7 +23,12 @@ module.exports = function(options) {
config: { path: __dirname + '/postcss.config.js' },
},
},
{ loader: 'sass-loader', options: { sourceMap: options.sourceMap } },
{
loader: 'sass-loader',
options: {
sourceMap: options.sourceMap
},
},
],
};
};

View File

@ -85,7 +85,9 @@ module.exports = merge(common, {
config: { path: __dirname + '/postcss.config.js' },
},
},
'sass-loader', // compiles Sass to CSS
{
loader: 'sass-loader'
}
],
},
{

View File

@ -1,38 +0,0 @@
const webpack = require('webpack');
const merge = require('webpack-merge');
const common = require('./webpack.common.js');
config = merge(common, {
mode: 'development',
devtool: 'cheap-module-source-map',
externals: {
'react/addons': true,
'react/lib/ExecutionEnvironment': true,
'react/lib/ReactContext': true,
},
module: {
rules: [
{
test: /\.tsx?$/,
exclude: /node_modules/,
use: {
loader: 'ts-loader',
options: {
transpileOnly: true,
},
},
},
],
},
plugins: [
new webpack.SourceMapDevToolPlugin({
filename: null, // if no value is provided the sourcemap is inlined
test: /\.(ts|js)($|\?)/i, // process .js and .ts files only
}),
],
});
module.exports = config;