Browse Source

Merge pull request #15235 from grafana/core/theming

POC - Enable js defined theme to be used in SASS
Torkel Ödegaard 6 năm trước cách đây
mục cha
commit
3e317b7d20
52 tập tin đã thay đổi với 730 bổ sung316 xóa
  1. 6 1
      packages/grafana-ui/.storybook/config.ts
  2. 10 2
      packages/grafana-ui/.storybook/webpack.config.js
  3. 14 18
      packages/grafana-ui/src/components/ColorPicker/ColorPicker.story.tsx
  4. 7 20
      packages/grafana-ui/src/components/ColorPicker/ColorPicker.tsx
  5. 14 27
      packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.story.tsx
  6. 6 5
      packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.test.tsx
  7. 10 18
      packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.tsx
  8. 13 4
      packages/grafana-ui/src/components/ColorPicker/NamedColorsGroup.tsx
  9. 11 4
      packages/grafana-ui/src/components/ColorPicker/NamedColorsPalette.story.tsx
  10. 5 4
      packages/grafana-ui/src/components/ColorPicker/NamedColorsPalette.test.tsx
  11. 4 1
      packages/grafana-ui/src/components/ColorPicker/SeriesColorPickerPopover.tsx
  12. 3 6
      packages/grafana-ui/src/components/ColorPicker/SpectrumPalette.story.tsx
  13. 2 2
      packages/grafana-ui/src/components/ColorPicker/SpectrumPalette.tsx
  14. 11 6
      packages/grafana-ui/src/components/ColorPicker/SpectrumPalettePointer.tsx
  15. 2 0
      packages/grafana-ui/src/components/Gauge/Gauge.test.tsx
  16. 12 11
      packages/grafana-ui/src/components/Gauge/Gauge.tsx
  17. 33 22
      packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx
  18. 2 2
      packages/grafana-ui/src/components/index.ts
  19. 2 0
      packages/grafana-ui/src/index.ts
  20. 20 0
      packages/grafana-ui/src/themes/ThemeContext.tsx
  21. 69 0
      packages/grafana-ui/src/themes/dark.ts
  22. 62 0
      packages/grafana-ui/src/themes/default.ts
  23. 14 0
      packages/grafana-ui/src/themes/index.ts
  24. 70 0
      packages/grafana-ui/src/themes/light.ts
  25. 52 0
      packages/grafana-ui/src/themes/selectThemeVariant.test.ts
  26. 9 0
      packages/grafana-ui/src/themes/selectThemeVariant.ts
  27. 2 9
      packages/grafana-ui/src/types/index.ts
  28. 129 0
      packages/grafana-ui/src/types/theme.ts
  29. 8 8
      packages/grafana-ui/src/utils/namedColorsPalette.test.ts
  30. 7 7
      packages/grafana-ui/src/utils/namedColorsPalette.ts
  31. 0 14
      packages/grafana-ui/src/utils/storybook/themeKnob.ts
  32. 41 0
      packages/grafana-ui/src/utils/storybook/withTheme.tsx
  33. 2 2
      public/app/core/angular_wrappers.ts
  34. 4 0
      public/app/core/config.ts
  35. 11 7
      public/app/core/utils/ConfigProvider.tsx
  36. 2 2
      public/app/core/utils/react2angular.ts
  37. 3 4
      public/app/plugins/panel/gauge/GaugePanel.tsx
  38. 9 17
      public/app/plugins/panel/gauge/GaugePanelOptions.tsx
  39. 11 19
      public/app/plugins/panel/graph/Legend/LegendSeriesItem.tsx
  40. 2 2
      public/app/plugins/panel/graph/data_processor.ts
  41. 7 6
      public/app/plugins/panel/graph/graph.ts
  42. 2 2
      public/app/plugins/panel/graph/module.ts
  43. 4 4
      public/app/plugins/panel/graph/time_region_manager.ts
  44. 2 2
      public/app/plugins/panel/heatmap/color_legend.ts
  45. 2 2
      public/app/plugins/panel/heatmap/rendering.ts
  46. 4 10
      public/app/plugins/panel/singlestat/module.ts
  47. 1 2
      public/app/plugins/panel/table/module.ts
  48. 2 2
      public/app/plugins/panel/table/renderer.ts
  49. 2 1
      public/app/routes/ReactContainer.tsx
  50. 7 2
      scripts/webpack/sass.rule.js
  51. 3 1
      scripts/webpack/webpack.hot.js
  52. 0 38
      scripts/webpack/webpack.test.js

+ 6 - 1
packages/grafana-ui/.storybook/config.ts

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

+ 10 - 2
packages/grafana-ui/.storybook/webpack.config.js

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

+ 14 - 18
packages/grafana-ui/src/components/ColorPicker/ColorPicker.story.tsx

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

+ 7 - 20
packages/grafana-ui/src/components/ColorPicker/ColorPicker.tsx

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

+ 14 - 27
packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.story.tsx

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

+ 6 - 5
packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.test.tsx

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

+ 10 - 18
packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.tsx

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

+ 13 - 4
packages/grafana-ui/src/components/ColorPicker/NamedColorsGroup.tsx

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

+ 11 - 4
packages/grafana-ui/src/components/ColorPicker/NamedColorsPalette.story.tsx

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

+ 5 - 4
packages/grafana-ui/src/components/ColorPicker/NamedColorsPalette.test.tsx

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

+ 4 - 1
packages/grafana-ui/src/components/ColorPicker/SeriesColorPickerPopover.tsx

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

+ 3 - 6
packages/grafana-ui/src/components/ColorPicker/SpectrumPalette.story.tsx

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

+ 2 - 2
packages/grafana-ui/src/components/ColorPicker/SpectrumPalette.tsx

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

+ 11 - 6
packages/grafana-ui/src/components/ColorPicker/SpectrumPalettePointer.tsx

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

+ 2 - 0
packages/grafana-ui/src/components/Gauge/Gauge.test.tsx

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

+ 12 - 11
packages/grafana-ui/src/components/Gauge/Gauge.tsx

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

+ 33 - 22
packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx

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

+ 2 - 2
packages/grafana-ui/src/components/index.ts

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

+ 2 - 0
packages/grafana-ui/src/index.ts

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

+ 20 - 0
packages/grafana-ui/src/themes/ThemeContext.tsx

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

+ 69 - 0
packages/grafana-ui/src/themes/dark.ts

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

+ 62 - 0
packages/grafana-ui/src/themes/default.ts

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

+ 14 - 0
packages/grafana-ui/src/themes/index.ts

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

+ 70 - 0
packages/grafana-ui/src/themes/light.ts

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

+ 52 - 0
packages/grafana-ui/src/themes/selectThemeVariant.test.ts

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

+ 9 - 0
packages/grafana-ui/src/themes/selectThemeVariant.ts

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

+ 2 - 9
packages/grafana-ui/src/types/index.ts

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

+ 129 - 0
packages/grafana-ui/src/types/theme.ts

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

+ 8 - 8
packages/grafana-ui/src/utils/namedColorsPalette.test.ts

@@ -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', () => {

+ 7 - 7
packages/grafana-ui/src/utils/namedColorsPalette.ts

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

+ 0 - 14
packages/grafana-ui/src/utils/storybook/themeKnob.ts

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

+ 41 - 0
packages/grafana-ui/src/utils/storybook/withTheme.tsx

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

+ 2 - 2
public/app/core/angular_wrappers.ts

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

+ 4 - 0
public/app/core/config.ts

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

+ 11 - 7
public/app/core/utils/ConfigProvider.tsx

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

+ 2 - 2
public/app/core/utils/react2angular.ts

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

+ 3 - 4
public/app/plugins/panel/gauge/GaugePanel.tsx

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

+ 9 - 17
public/app/plugins/panel/gauge/GaugePanelOptions.tsx

@@ -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>
+      <>
+        <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} />
-          </>
-        )}
-      </ThemeProvider>
+        <ValueMappingsEditor onChange={this.onValueMappingsChanged} valueMappings={options.valueMappings} />
+      </>
     );
   }
 }

+ 11 - 19
public/app/plugins/panel/graph/Legend/LegendSeriesItem.tsx

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

+ 2 - 2
public/app/plugins/panel/graph/data_processor.ts

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

+ 7 - 6
public/app/plugins/panel/graph/graph.ts

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

+ 2 - 2
public/app/plugins/panel/graph/module.ts

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

+ 4 - 4
public/app/plugins/panel/graph/time_region_manager.ts

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

+ 2 - 2
public/app/plugins/panel/heatmap/color_legend.ts

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

+ 2 - 2
public/app/plugins/panel/heatmap/rendering.ts

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

+ 4 - 10
public/app/plugins/panel/singlestat/module.ts

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

+ 1 - 2
public/app/plugins/panel/table/module.ts

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

+ 2 - 2
public/app/plugins/panel/table/renderer.ts

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

+ 2 - 1
public/app/routes/ReactContainer.tsx

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

+ 7 - 2
scripts/webpack/sass.rule.js

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

+ 3 - 1
scripts/webpack/webpack.hot.js

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

+ 0 - 38
scripts/webpack/webpack.test.js

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