diff --git a/packages/grafana-ui/.storybook/addons.ts b/packages/grafana-ui/.storybook/addons.ts new file mode 100644 index 00000000000..2efe80ebffa --- /dev/null +++ b/packages/grafana-ui/.storybook/addons.ts @@ -0,0 +1,2 @@ +import '@storybook/addon-knobs/register'; +import '@storybook/addon-actions/register'; diff --git a/packages/grafana-ui/.storybook/config.ts b/packages/grafana-ui/.storybook/config.ts new file mode 100644 index 00000000000..9e50c6b501a --- /dev/null +++ b/packages/grafana-ui/.storybook/config.ts @@ -0,0 +1,12 @@ +import { configure } from '@storybook/react'; + +import '../../../public/sass/grafana.light.scss'; + +// automatically import all files ending in *.stories.tsx +const req = require.context('../src/components', true, /.story.tsx$/); + +function loadStories() { + req.keys().forEach(req); +} + +configure(loadStories, module); diff --git a/packages/grafana-ui/.storybook/webpack.config.js b/packages/grafana-ui/.storybook/webpack.config.js new file mode 100644 index 00000000000..44de73a1e18 --- /dev/null +++ b/packages/grafana-ui/.storybook/webpack.config.js @@ -0,0 +1,56 @@ +const path = require('path'); + +module.exports = (baseConfig, env, config) => { + + config.module.rules.push({ + test: /\.(ts|tsx)$/, + use: [ + { + loader: require.resolve('awesome-typescript-loader'), + }, + ], + }); + + config.module.rules.push({ + test: /\.scss$/, + use: [ + { + loader: 'style-loader', + }, + { + loader: 'css-loader', + options: { + importLoaders: 2, + url: false, + sourceMap: false, + minimize: false, + }, + }, + { + loader: 'postcss-loader', + options: { + sourceMap: false, + config: { path: __dirname + '../../../../scripts/webpack/postcss.config.js' }, + }, + }, + { loader: 'sass-loader', options: { sourceMap: false } }, + ], + }); + + config.module.rules.push({ + test: require.resolve('jquery'), + use: [ + { + loader: 'expose-loader', + query: 'jQuery', + }, + { + loader: 'expose-loader', + query: '$', + }, + ], + }); + + config.resolve.extensions.push('.ts', '.tsx'); + return config; +}; diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index 91695dc5647..0d1b14a7150 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -5,19 +5,20 @@ "main": "src/index.ts", "scripts": { "tslint": "tslint -c tslint.json --project tsconfig.json", - "typecheck": "tsc --noEmit" + "typecheck": "tsc --noEmit", + "storybook": "start-storybook -p 9001 -c .storybook -s ../../public" }, "author": "", "license": "ISC", "dependencies": { "@torkelo/react-select": "2.1.1", - "@types/react-test-renderer": "^16.0.3", - "@types/react-transition-group": "^2.0.15", + "@types/react-color": "^2.14.0", "classnames": "^2.2.5", "jquery": "^3.2.1", "lodash": "^4.17.10", "moment": "^2.22.2", "react": "^16.6.3", + "react-color": "^2.17.0", "react-custom-scrollbars": "^4.2.1", "react-dom": "^16.6.3", "react-highlight-words": "0.11.0", @@ -29,16 +30,32 @@ "tinycolor2": "^1.4.1" }, "devDependencies": { + "@storybook/addon-actions": "^4.1.7", + "@storybook/addon-info": "^4.1.6", + "@storybook/addon-knobs": "^4.1.7", + "@storybook/react": "^4.1.4", "@types/classnames": "^2.2.6", "@types/jest": "^23.3.2", "@types/jquery": "^1.10.35", "@types/lodash": "^4.14.119", + "@types/node": "^10.12.18", "@types/react": "^16.7.6", "@types/react-custom-scrollbars": "^4.0.5", "@types/react-test-renderer": "^16.0.3", + "@types/react-transition-group": "^2.0.15", + "@types/storybook__addon-actions": "^3.4.1", + "@types/storybook__addon-info": "^3.4.2", + "@types/storybook__addon-knobs": "^4.0.0", + "@types/storybook__react": "^4.0.0", "@types/tether-drop": "^1.4.8", "@types/tinycolor2": "^1.4.1", + "awesome-typescript-loader": "^5.2.1", + "react-docgen-typescript-loader": "^3.0.0", + "react-docgen-typescript-webpack-plugin": "^1.1.0", "react-test-renderer": "^16.7.0", "typescript": "^3.2.2" + }, + "resolutions": { + "@types/lodash": "4.14.119" } } diff --git a/packages/grafana-ui/src/components/ColorPicker/ColorInput.tsx b/packages/grafana-ui/src/components/ColorPicker/ColorInput.tsx new file mode 100644 index 00000000000..4c5df5314b8 --- /dev/null +++ b/packages/grafana-ui/src/components/ColorPicker/ColorInput.tsx @@ -0,0 +1,94 @@ +import React from 'react'; +import { ColorPickerProps } from './ColorPicker'; +import tinycolor from 'tinycolor2'; +import { debounce } from 'lodash'; + +interface ColorInputState { + previousColor: string; + value: string; +} + +interface ColorInputProps extends ColorPickerProps { + style?: React.CSSProperties; +} + +class ColorInput extends React.PureComponent { + constructor(props: ColorInputProps) { + super(props); + this.state = { + previousColor: props.color, + value: props.color, + }; + + this.updateColor = debounce(this.updateColor, 100); + } + + static getDerivedStateFromProps(props: ColorPickerProps, state: ColorInputState) { + const newColor = tinycolor(props.color); + if (newColor.isValid() && props.color !== state.previousColor) { + return { + ...state, + previousColor: props.color, + value: newColor.toString(), + }; + } + + return state; + } + updateColor = (color: string) => { + this.props.onChange(color); + }; + + handleChange = (event: React.SyntheticEvent) => { + const newColor = tinycolor(event.currentTarget.value); + + this.setState({ + value: event.currentTarget.value, + }); + + if (newColor.isValid()) { + this.updateColor(newColor.toString()); + } + }; + + handleBlur = () => { + const newColor = tinycolor(this.state.value); + + if (!newColor.isValid()) { + this.setState({ + value: this.props.color, + }); + } + }; + + render() { + const { value } = this.state; + return ( +
+
+
+ +
+
+ ); + } +} + +export default ColorInput; diff --git a/packages/grafana-ui/src/components/ColorPicker/ColorPicker.story.tsx b/packages/grafana-ui/src/components/ColorPicker/ColorPicker.story.tsx new file mode 100644 index 00000000000..19ae2fda978 --- /dev/null +++ b/packages/grafana-ui/src/components/ColorPicker/ColorPicker.story.tsx @@ -0,0 +1,63 @@ +import React from 'react'; +import { storiesOf } from '@storybook/react'; +import { withKnobs, boolean } from '@storybook/addon-knobs'; +import { SeriesColorPicker, ColorPicker } from './ColorPicker'; +import { action } from '@storybook/addon-actions'; +import { withCenteredStory } from '../../utils/storybook/withCenteredStory'; +import { UseState } from '../../utils/storybook/UseState'; +import { getThemeKnob } from '../../utils/storybook/themeKnob'; + +const getColorPickerKnobs = () => { + return { + selectedTheme: getThemeKnob(), + enableNamedColors: boolean('Enable named colors', false), + }; +}; + +const ColorPickerStories = storiesOf('UI/ColorPicker/Pickers', module); + +ColorPickerStories.addDecorator(withCenteredStory).addDecorator(withKnobs); + +ColorPickerStories.add('default', () => { + const { selectedTheme, enableNamedColors } = getColorPickerKnobs(); + return ( + + {(selectedColor, updateSelectedColor) => { + return ( + { + action('Color changed')(color); + updateSelectedColor(color); + }} + theme={selectedTheme || undefined} + /> + ); + }} + + ); +}); + +ColorPickerStories.add('Series color picker', () => { + const { selectedTheme, enableNamedColors } = getColorPickerKnobs(); + + return ( + + {(selectedColor, updateSelectedColor) => { + return ( + {}} + color={selectedColor} + onChange={color => updateSelectedColor(color)} + theme={selectedTheme || undefined} + > +
Open color picker
+
+ ); + }} +
+ ); +}); diff --git a/packages/grafana-ui/src/components/ColorPicker/ColorPicker.tsx b/packages/grafana-ui/src/components/ColorPicker/ColorPicker.tsx index 485aa5f03d3..b6cf176a24b 100644 --- a/packages/grafana-ui/src/components/ColorPicker/ColorPicker.tsx +++ b/packages/grafana-ui/src/components/ColorPicker/ColorPicker.tsx @@ -1,61 +1,114 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import Drop from 'tether-drop'; +import React, { Component, createRef } from 'react'; +import PopperController from '../Tooltip/PopperController'; +import Popper, { RenderPopperArrowFn } from '../Tooltip/Popper'; import { ColorPickerPopover } from './ColorPickerPopover'; +import { Themeable, GrafanaTheme } from '../../types'; +import { getColorFromHexRgbOrName } from '../../utils/namedColorsPalette'; +import { SeriesColorPickerPopover } from './SeriesColorPickerPopover'; +import propDeprecationWarning from '../../utils/propDeprecationWarning'; -export interface Props { +type ColorPickerChangeHandler = (color: string) => void; + +export interface ColorPickerProps extends Themeable { color: string; - onChange: (c: string) => void; + onChange: ColorPickerChangeHandler; + + /** + * @deprecated Use onChange instead + */ + onColorChange?: ColorPickerChangeHandler; + enableNamedColors?: boolean; + withArrow?: boolean; + children?: JSX.Element; } -export class ColorPicker extends React.Component { - pickerElem: HTMLElement | null; - colorPickerDrop: any; - - openColorPicker = () => { - const dropContent = ; - - const dropContentElem = document.createElement('div'); - ReactDOM.render(dropContent, dropContentElem); - - const drop = new Drop({ - target: this.pickerElem as Element, - content: dropContentElem, - position: 'top center', - classes: 'drop-popover', - openOn: 'click', - hoverCloseDelay: 200, - tetherOptions: { - constraints: [{ to: 'scrollParent', attachment: 'none both' }], - attachment: 'bottom center', - }, - }); - - drop.on('close', this.closeColorPicker); - - this.colorPickerDrop = drop; - this.colorPickerDrop.open(); - }; - - closeColorPicker = () => { - setTimeout(() => { - if (this.colorPickerDrop && this.colorPickerDrop.tether) { - this.colorPickerDrop.destroy(); - } - }, 100); - }; - - onColorSelect = (color: string) => { - this.props.onChange(color); - }; - - render() { - return ( -
(this.pickerElem = element)}> -
-
-
-
- ); +export const warnAboutColorPickerPropsDeprecation = (componentName: string, props: ColorPickerProps) => { + const { onColorChange } = props; + if (onColorChange) { + propDeprecationWarning(componentName, 'onColorChange', 'onChange'); } -} +}; + +export const colorPickerFactory = ( + popover: React.ComponentType, + displayName = 'ColorPicker', + renderPopoverArrowFunction?: RenderPopperArrowFn +) => { + return class ColorPicker extends Component { + static displayName = displayName; + pickerTriggerRef = createRef(); + + handleColorChange = (color: string) => { + const { onColorChange, onChange } = this.props; + const changeHandler = (onColorChange || onChange) as ColorPickerChangeHandler; + + return changeHandler(color); + }; + + render() { + const popoverElement = React.createElement(popover, { + ...this.props, + onChange: this.handleColorChange, + }); + const { theme, withArrow, children } = this.props; + + const renderArrow: RenderPopperArrowFn = ({ arrowProps, placement }) => { + return ( +
+ ); + }; + + return ( + + {(showPopper, hidePopper, popperProps) => { + return ( + <> + {this.pickerTriggerRef.current && ( + + )} + + {children ? ( + React.cloneElement(children as JSX.Element, { + ref: this.pickerTriggerRef, + onClick: showPopper, + onMouseLeave: hidePopper, + }) + ) : ( +
+
+
+
+
+ )} + + ); + }} + + ); + } + }; +}; + +export const ColorPicker = colorPickerFactory(ColorPickerPopover, 'ColorPicker'); +export const SeriesColorPicker = colorPickerFactory(SeriesColorPickerPopover, 'SeriesColorPicker'); diff --git a/packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.story.tsx b/packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.story.tsx new file mode 100644 index 00000000000..dc51819a413 --- /dev/null +++ b/packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.story.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import { storiesOf } from '@storybook/react'; +import { ColorPickerPopover } from './ColorPickerPopover'; +import { withKnobs } from '@storybook/addon-knobs'; + +import { withCenteredStory } from '../../utils/storybook/withCenteredStory'; +import { getThemeKnob } from '../../utils/storybook/themeKnob'; +import { SeriesColorPickerPopover } from './SeriesColorPickerPopover'; + +const ColorPickerPopoverStories = storiesOf('UI/ColorPicker/Popovers', module); + +ColorPickerPopoverStories.addDecorator(withCenteredStory).addDecorator(withKnobs); + +ColorPickerPopoverStories.add('default', () => { + const selectedTheme = getThemeKnob(); + + return ( + { + console.log(color); + }} + theme={selectedTheme || undefined} + /> + ); +}); + +ColorPickerPopoverStories.add('SeriesColorPickerPopover', () => { + const selectedTheme = getThemeKnob(); + + return ( + { + console.log(color); + }} + theme={selectedTheme || undefined} + /> + ); +}); diff --git a/packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.test.tsx b/packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.test.tsx new file mode 100644 index 00000000000..28d66e7af86 --- /dev/null +++ b/packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.test.tsx @@ -0,0 +1,75 @@ +import React from 'react'; +import { mount, ReactWrapper } from 'enzyme'; +import { ColorPickerPopover } from './ColorPickerPopover'; +import { getColorDefinitionByName, getNamedColorPalette } from '../../utils/namedColorsPalette'; +import { ColorSwatch } from './NamedColorsGroup'; +import { flatten } from 'lodash'; +import { GrafanaTheme } from '../../types'; + +const allColors = flatten(Array.from(getNamedColorPalette().values())); + +describe('ColorPickerPopover', () => { + const BasicGreen = getColorDefinitionByName('green'); + const BasicBlue = getColorDefinitionByName('blue'); + + describe('rendering', () => { + it('should render provided color as selected if color provided by name', () => { + const wrapper = mount( {}} />); + const selectedSwatch = wrapper.find(ColorSwatch).findWhere(node => node.key() === BasicGreen.name); + const notSelectedSwatches = wrapper.find(ColorSwatch).filterWhere(node => node.prop('isSelected') === false); + + expect(selectedSwatch.length).toBe(1); + expect(notSelectedSwatches.length).toBe(allColors.length - 1); + expect(selectedSwatch.prop('isSelected')).toBe(true); + }); + + it('should render provided color as selected if color provided by hex', () => { + const wrapper = mount( {}} />); + const selectedSwatch = wrapper.find(ColorSwatch).findWhere(node => node.key() === BasicGreen.name); + const notSelectedSwatches = wrapper.find(ColorSwatch).filterWhere(node => node.prop('isSelected') === false); + + expect(selectedSwatch.length).toBe(1); + expect(notSelectedSwatches.length).toBe(allColors.length - 1); + expect(selectedSwatch.prop('isSelected')).toBe(true); + }); + }); + + describe('named colors support', () => { + const onChangeSpy = jest.fn(); + let wrapper: ReactWrapper; + + afterEach(() => { + wrapper.unmount(); + onChangeSpy.mockClear(); + }); + + it('should pass hex color value to onChange prop by default', () => { + wrapper = mount( + + ); + const basicBlueSwatch = wrapper.find(ColorSwatch).findWhere(node => node.key() === BasicBlue.name); + + basicBlueSwatch.simulate('click'); + + expect(onChangeSpy).toBeCalledTimes(1); + expect(onChangeSpy).toBeCalledWith(BasicBlue.variants.light); + }); + + it('should pass color name to onChange prop when named colors enabled', () => { + wrapper = mount( + + ); + const basicBlueSwatch = wrapper.find(ColorSwatch).findWhere(node => node.key() === BasicBlue.name); + + basicBlueSwatch.simulate('click'); + + expect(onChangeSpy).toBeCalledTimes(1); + expect(onChangeSpy).toBeCalledWith(BasicBlue.name); + }); + }); +}); diff --git a/packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.tsx b/packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.tsx index e8305c99319..d2937a1caba 100644 --- a/packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.tsx +++ b/packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.tsx @@ -1,112 +1,129 @@ import React from 'react'; -import $ from 'jquery'; -import tinycolor from 'tinycolor2'; -import { ColorPalette } from './ColorPalette'; -import { SpectrumPicker } from './SpectrumPicker'; +import { NamedColorsPalette } from './NamedColorsPalette'; +import { getColorName, getColorFromHexRgbOrName } from '../../utils/namedColorsPalette'; +import { ColorPickerProps, warnAboutColorPickerPropsDeprecation } from './ColorPicker'; +import { GrafanaTheme } from '../../types'; +import { PopperContentProps } from '../Tooltip/PopperController'; +import SpectrumPalette from './SpectrumPalette'; -const DEFAULT_COLOR = '#000000'; - -export interface Props { - color: string; - onColorSelect: (c: string) => void; +export interface Props extends ColorPickerProps, PopperContentProps { + customPickers?: T; } -export class ColorPickerPopover extends React.Component { - pickerNavElem: any; +type PickerType = 'palette' | 'spectrum'; - constructor(props: Props) { +interface CustomPickersDescriptor { + [key: string]: { + tabComponent: React.ComponentType; + name: string; + }; +} +interface State { + activePicker: PickerType | keyof T; +} + +export class ColorPickerPopover extends React.Component, State> { + constructor(props: Props) { super(props); this.state = { - tab: 'palette', - color: this.props.color || DEFAULT_COLOR, - colorString: this.props.color || DEFAULT_COLOR, + activePicker: 'palette', }; + warnAboutColorPickerPropsDeprecation('ColorPickerPopover', props); } - setPickerNavElem(elem: any) { - this.pickerNavElem = $(elem); - } + getTabClassName = (tabName: PickerType | keyof T) => { + const { activePicker } = this.state; + return `ColorPickerPopover__tab ${activePicker === tabName && 'ColorPickerPopover__tab--active'}`; + }; - setColor(color: string) { - const newColor = tinycolor(color); - if (newColor.isValid()) { - this.setState({ color: newColor.toString(), colorString: newColor.toString() }); - this.props.onColorSelect(color); + handleChange = (color: any) => { + const { onColorChange, onChange, enableNamedColors, theme } = this.props; + const changeHandler = onColorChange || onChange; + + if (enableNamedColors) { + return changeHandler(color); } - } + changeHandler(getColorFromHexRgbOrName(color, theme)); + }; - sampleColorSelected(color: string) { - this.setColor(color); - } + handleTabChange = (tab: PickerType | keyof T) => { + return () => this.setState({ activePicker: tab }); + }; - spectrumColorSelected(color: any) { - const rgbColor = color.toRgbString(); - this.setColor(rgbColor); - } + renderPicker = () => { + const { activePicker } = this.state; + const { color, theme } = this.props; - onColorStringChange(e: any) { - const colorString = e.target.value; - this.setState({ colorString: colorString }); - - const newColor = tinycolor(colorString); - if (newColor.isValid()) { - // Update only color state - const newColorString = newColor.toString(); - this.setState({ color: newColorString }); - this.props.onColorSelect(newColorString); + switch (activePicker) { + case 'spectrum': + return ; + case 'palette': + return ; + default: + return this.renderCustomPicker(activePicker); } - } + }; - onColorStringBlur(e: any) { - const colorString = e.target.value; - this.setColor(colorString); - } + renderCustomPicker = (tabKey: keyof T) => { + const { customPickers, color, theme } = this.props; + if (!customPickers) { + return null; + } - componentDidMount() { - this.pickerNavElem.find('li:first').addClass('active'); - this.pickerNavElem.on('show', (e: any) => { - // use href attr (#name => name) - const tab = e.target.hash.slice(1); - this.setState({ tab: tab }); + return React.createElement(customPickers[tabKey].tabComponent, { + color, + theme, + onChange: this.handleChange, }); - } + }; - render() { - const paletteTab = ( -
- -
- ); - const spectrumTab = ( -
- -
- ); - const currentTab = this.state.tab === 'palette' ? paletteTab : spectrumTab; + renderCustomPickerTabs = () => { + const { customPickers } = this.props; + + if (!customPickers) { + return null; + } return ( -
- -
{currentTab}
-
- + <> + {Object.keys(customPickers).map(key => { + return ( +
+ {customPickers[key].name} +
+ ); + })} + + ); + }; + + render() { + const { theme } = this.props; + const colorPickerTheme = theme || GrafanaTheme.Dark; + + return ( +
+
+
+ Colors +
+
+ Custom +
+ {this.renderCustomPickerTabs()}
+ +
{this.renderPicker()}
); } diff --git a/packages/grafana-ui/src/components/ColorPicker/NamedColorsGroup.tsx b/packages/grafana-ui/src/components/ColorPicker/NamedColorsGroup.tsx new file mode 100644 index 00000000000..91c4f21642a --- /dev/null +++ b/packages/grafana-ui/src/components/ColorPicker/NamedColorsGroup.tsx @@ -0,0 +1,110 @@ +import React, { FunctionComponent } from 'react'; +import { Themeable, GrafanaTheme } from '../../types'; +import { ColorDefinition, getColorForTheme } from '../../utils/namedColorsPalette'; +import { Color } from 'csstype'; +import { find, upperFirst } from 'lodash'; + +type ColorChangeHandler = (color: ColorDefinition) => void; + +export enum ColorSwatchVariant { + Small = 'small', + Large = 'large', +} + +interface ColorSwatchProps extends Themeable, React.DOMAttributes { + color: string; + label?: string; + variant?: ColorSwatchVariant; + isSelected?: boolean; +} + +export const ColorSwatch: FunctionComponent = ({ + color, + label, + variant = ColorSwatchVariant.Small, + isSelected, + theme, + ...otherProps +}) => { + const isSmall = variant === ColorSwatchVariant.Small; + const swatchSize = isSmall ? '16px' : '32px'; + const selectedSwatchBorder = theme === GrafanaTheme.Light ? '#ffffff' : '#1A1B1F'; + const swatchStyles = { + width: swatchSize, + height: swatchSize, + borderRadius: '50%', + background: `${color}`, + marginRight: isSmall ? '0px' : '8px', + boxShadow: isSelected ? `inset 0 0 0 2px ${color}, inset 0 0 0 4px ${selectedSwatchBorder}` : 'none', + }; + + return ( +
+
+ {variant === ColorSwatchVariant.Large && {label}} +
+ ); +}; + +interface NamedColorsGroupProps extends Themeable { + colors: ColorDefinition[]; + selectedColor?: Color; + onColorSelect: ColorChangeHandler; + key?: string; +} + +const NamedColorsGroup: FunctionComponent = ({ + colors, + selectedColor, + onColorSelect, + theme, + ...otherProps +}) => { + const primaryColor = find(colors, color => !!color.isPrimary); + + return ( +
+ {primaryColor && ( + onColorSelect(primaryColor)} + theme={theme} + /> + )} +
+ {colors.map( + color => + !color.isPrimary && ( +
+ onColorSelect(color)} + theme={theme} + /> +
+ ) + )} +
+
+ ); +}; + +export default NamedColorsGroup; diff --git a/packages/grafana-ui/src/components/ColorPicker/NamedColorsPalette.story.tsx b/packages/grafana-ui/src/components/ColorPicker/NamedColorsPalette.story.tsx new file mode 100644 index 00000000000..af5de3b2a2d --- /dev/null +++ b/packages/grafana-ui/src/components/ColorPicker/NamedColorsPalette.story.tsx @@ -0,0 +1,52 @@ +import React from 'react'; +import { storiesOf } from '@storybook/react'; +import { NamedColorsPalette } from './NamedColorsPalette'; +import { getColorName, getColorDefinitionByName } from '../../utils/namedColorsPalette'; +import { withKnobs, select } from '@storybook/addon-knobs'; +import { withCenteredStory } from '../../utils/storybook/withCenteredStory'; +import { UseState } from '../../utils/storybook/UseState'; + +const BasicGreen = getColorDefinitionByName('green'); +const BasicBlue = getColorDefinitionByName('blue'); +const LightBlue = getColorDefinitionByName('light-blue'); + +const NamedColorsPaletteStories = storiesOf('UI/ColorPicker/Palettes/NamedColorsPalette', module); + +NamedColorsPaletteStories.addDecorator(withKnobs).addDecorator(withCenteredStory); + +NamedColorsPaletteStories.add('Named colors swatch - support for named colors', () => { + const selectedColor = select( + 'Selected color', + { + Green: 'green', + Red: 'red', + 'Light blue': 'light-blue', + }, + 'red' + ); + + return ( + + {(selectedColor, updateSelectedColor) => { + return ; + }} + + ); +}).add('Named colors swatch - support for hex values', () => { + const selectedColor = select( + 'Selected color', + { + Green: BasicGreen.variants.dark, + Red: BasicBlue.variants.dark, + 'Light blue': LightBlue.variants.dark, + }, + 'red' + ); + return ( + + {(selectedColor, updateSelectedColor) => { + return ; + }} + + ); +}); diff --git a/packages/grafana-ui/src/components/ColorPicker/NamedColorsPalette.test.tsx b/packages/grafana-ui/src/components/ColorPicker/NamedColorsPalette.test.tsx new file mode 100644 index 00000000000..171d26f5c56 --- /dev/null +++ b/packages/grafana-ui/src/components/ColorPicker/NamedColorsPalette.test.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { mount, ReactWrapper } from 'enzyme'; +import { NamedColorsPalette } from './NamedColorsPalette'; +import { ColorSwatch } from './NamedColorsGroup'; +import { getColorDefinitionByName } from '../../utils'; +import { GrafanaTheme } from '../../types'; + +describe('NamedColorsPalette', () => { + + const BasicGreen = getColorDefinitionByName('green'); + + describe('theme support for named colors', () => { + let wrapper: ReactWrapper, selectedSwatch; + + afterEach(() => { + wrapper.unmount(); + }); + + it('should render provided color variant specific for theme', () => { + wrapper = mount( {}} />); + selectedSwatch = wrapper.find(ColorSwatch).findWhere(node => node.key() === BasicGreen.name); + expect(selectedSwatch.prop('color')).toBe(BasicGreen.variants.dark); + + wrapper.unmount(); + wrapper = mount( {}} />); + 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( {}} />); + selectedSwatch = wrapper.find(ColorSwatch).findWhere(node => node.key() === BasicGreen.name); + expect(selectedSwatch.prop('color')).toBe(BasicGreen.variants.dark); + }); + }); +}); diff --git a/packages/grafana-ui/src/components/ColorPicker/NamedColorsPalette.tsx b/packages/grafana-ui/src/components/ColorPicker/NamedColorsPalette.tsx new file mode 100644 index 00000000000..7bdd3c690b9 --- /dev/null +++ b/packages/grafana-ui/src/components/ColorPicker/NamedColorsPalette.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import { Color, getNamedColorPalette } from '../../utils/namedColorsPalette'; +import { Themeable } from '../../types/index'; +import NamedColorsGroup from './NamedColorsGroup'; + +interface NamedColorsPaletteProps extends Themeable { + color?: Color; + onChange: (colorName: string) => void; +} + +export const NamedColorsPalette = ({ color, onChange, theme }: NamedColorsPaletteProps) => { + const swatches: JSX.Element[] = []; + getNamedColorPalette().forEach((colors, hue) => { + swatches.push( + { + onChange(color.name); + }} + /> + ); + }); + + return ( +
+ {swatches} +
+ ); +}; diff --git a/packages/grafana-ui/src/components/ColorPicker/SeriesColorPicker.tsx b/packages/grafana-ui/src/components/ColorPicker/SeriesColorPicker.tsx deleted file mode 100644 index 7c3848f6868..00000000000 --- a/packages/grafana-ui/src/components/ColorPicker/SeriesColorPicker.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import Drop from 'tether-drop'; -import { SeriesColorPickerPopover } from './SeriesColorPickerPopover'; - -export interface SeriesColorPickerProps { - color: string; - yaxis?: number; - optionalClass?: string; - onColorChange: (newColor: string) => void; - onToggleAxis?: () => void; -} - -export class SeriesColorPicker extends React.Component { - pickerElem: any; - colorPickerDrop: any; - - static defaultProps = { - optionalClass: '', - yaxis: undefined, - onToggleAxis: () => {}, - }; - - constructor(props: SeriesColorPickerProps) { - super(props); - } - - componentWillUnmount() { - this.destroyDrop(); - } - - onClickToOpen = () => { - if (this.colorPickerDrop) { - this.destroyDrop(); - } - - const { color, yaxis, onColorChange, onToggleAxis } = this.props; - const dropContent = ( - - ); - const dropContentElem = document.createElement('div'); - ReactDOM.render(dropContent, dropContentElem); - - const drop = new Drop({ - target: this.pickerElem, - content: dropContentElem, - position: 'bottom center', - classes: 'drop-popover', - openOn: 'hover', - hoverCloseDelay: 200, - remove: true, - tetherOptions: { - constraints: [{ to: 'scrollParent', attachment: 'none both' }], - attachment: 'bottom center', - }, - }); - - drop.on('close', this.closeColorPicker.bind(this)); - - this.colorPickerDrop = drop; - this.colorPickerDrop.open(); - }; - - closeColorPicker() { - setTimeout(() => { - this.destroyDrop(); - }, 100); - } - - destroyDrop() { - if (this.colorPickerDrop && this.colorPickerDrop.tether) { - this.colorPickerDrop.destroy(); - this.colorPickerDrop = null; - } - } - - render() { - const { optionalClass, children } = this.props; - return ( -
(this.pickerElem = e)} onClick={this.onClickToOpen}> - {children} -
- ); - } -} diff --git a/packages/grafana-ui/src/components/ColorPicker/SeriesColorPickerPopover.tsx b/packages/grafana-ui/src/components/ColorPicker/SeriesColorPickerPopover.tsx index 541a77ddabc..3fa7a1f4a45 100644 --- a/packages/grafana-ui/src/components/ColorPicker/SeriesColorPickerPopover.tsx +++ b/packages/grafana-ui/src/components/ColorPicker/SeriesColorPickerPopover.tsx @@ -1,23 +1,44 @@ -import React from 'react'; -import { ColorPickerPopover } from './ColorPickerPopover'; +import React, { FunctionComponent } from 'react'; -export interface SeriesColorPickerPopoverProps { - color: string; +import { ColorPickerPopover } from './ColorPickerPopover'; +import { ColorPickerProps } from './ColorPicker'; +import { PopperContentProps } from '../Tooltip/PopperController'; +import { Switch } from '../Switch/Switch'; + +export interface SeriesColorPickerPopoverProps extends ColorPickerProps, PopperContentProps { yaxis?: number; - onColorChange: (color: string) => void; onToggleAxis?: () => void; } -export class SeriesColorPickerPopover extends React.PureComponent { - render() { - return ( -
- {this.props.yaxis && } - -
- ); - } -} +export const SeriesColorPickerPopover: FunctionComponent = props => { + const { yaxis, onToggleAxis, color, ...colorPickerProps } = props; + + return ( + ( + { + if (onToggleAxis) { + onToggleAxis(); + } + }} + /> + ), + }, + }} + /> + ); +}; interface AxisSelectorProps { yaxis: number; diff --git a/packages/grafana-ui/src/components/ColorPicker/SpectrumPalette.story.tsx b/packages/grafana-ui/src/components/ColorPicker/SpectrumPalette.story.tsx new file mode 100644 index 00000000000..407564cdfb2 --- /dev/null +++ b/packages/grafana-ui/src/components/ColorPicker/SpectrumPalette.story.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { storiesOf } from '@storybook/react'; +import { withKnobs } from '@storybook/addon-knobs'; + +import SpectrumPalette from './SpectrumPalette'; +import { withCenteredStory } from '../../utils/storybook/withCenteredStory'; +import { UseState } from '../../utils/storybook/UseState'; +import { getThemeKnob } from '../../utils/storybook/themeKnob'; + +const SpectrumPaletteStories = storiesOf('UI/ColorPicker/Palettes/SpectrumPalette', module); + +SpectrumPaletteStories.addDecorator(withCenteredStory).addDecorator(withKnobs); + +SpectrumPaletteStories.add('Named colors swatch - support for named colors', () => { + const selectedTheme = getThemeKnob(); + return ( + + {(selectedColor, updateSelectedColor) => { + return ; + }} + + ); +}); diff --git a/packages/grafana-ui/src/components/ColorPicker/SpectrumPalette.tsx b/packages/grafana-ui/src/components/ColorPicker/SpectrumPalette.tsx new file mode 100644 index 00000000000..cf001cf5629 --- /dev/null +++ b/packages/grafana-ui/src/components/ColorPicker/SpectrumPalette.tsx @@ -0,0 +1,100 @@ +import React from 'react'; +import { CustomPicker, ColorResult } from 'react-color'; + +import { Saturation, Hue, Alpha } from 'react-color/lib/components/common'; +import { getColorFromHexRgbOrName } from '../../utils/namedColorsPalette'; +import tinycolor from 'tinycolor2'; +import ColorInput from './ColorInput'; +import { Themeable, GrafanaTheme } from '../../types'; +import SpectrumPalettePointer, { SpectrumPalettePointerProps } from './SpectrumPalettePointer'; + +export interface SpectrumPaletteProps extends Themeable { + color: string; + onChange: (color: string) => void; +} + +const renderPointer = (theme?: GrafanaTheme) => (props: SpectrumPalettePointerProps) => ( + +); + +// @ts-ignore +const SpectrumPicker = CustomPicker(({ rgb, hsl, onChange, theme }) => { + return ( +
+
+
+
+ {/* + // @ts-ignore */} + +
+
+ {/* + // @ts-ignore */} + +
+
+ +
+ {/* + // @ts-ignore */} + +
+
+
+ ); +}); + +const SpectrumPalette: React.FunctionComponent = ({ color, onChange, theme }) => { + return ( +
+ { + onChange(tinycolor(a.rgb).toString()); + }} + theme={theme} + /> + +
+ ); +}; + +export default SpectrumPalette; diff --git a/packages/grafana-ui/src/components/ColorPicker/SpectrumPalettePointer.tsx b/packages/grafana-ui/src/components/ColorPicker/SpectrumPalettePointer.tsx new file mode 100644 index 00000000000..d0b2cbc4bff --- /dev/null +++ b/packages/grafana-ui/src/components/ColorPicker/SpectrumPalettePointer.tsx @@ -0,0 +1,80 @@ +import React from 'react'; +import { GrafanaTheme, Themeable } from '../../types'; + +export interface SpectrumPalettePointerProps extends Themeable { + direction?: string; +} + +const SpectrumPalettePointer: React.FunctionComponent = ({ + theme, + direction, +}) => { + const styles = { + picker: { + width: '16px', + height: '16px', + transform: direction === 'vertical' ? 'translate(0, -8px)' : 'translate(-8px, 0)', + }, + }; + + const pointerColor = theme === GrafanaTheme.Light ? '#3F444D' : '#8E8E8E'; + + let pointerStyles: React.CSSProperties = { + position: 'absolute', + left: '6px', + width: '0', + height: '0', + borderStyle: 'solid', + background: 'none', + }; + + let topArrowStyles: React.CSSProperties = { + top: '-7px', + borderWidth: '6px 3px 0px 3px', + borderColor: `${pointerColor} transparent transparent transparent`, + }; + + let bottomArrowStyles: React.CSSProperties = { + bottom: '-7px', + borderWidth: '0px 3px 6px 3px', + borderColor: ` transparent transparent ${pointerColor} transparent`, + }; + + if (direction === 'vertical') { + pointerStyles = { + ...pointerStyles, + left: 'auto', + }; + topArrowStyles = { + borderWidth: '3px 0px 3px 6px', + borderColor: `transparent transparent transparent ${pointerColor}`, + left: '-7px', + top: '7px', + }; + bottomArrowStyles = { + borderWidth: '3px 6px 3px 0px', + borderColor: `transparent ${pointerColor} transparent transparent`, + right: '-7px', + top: '7px', + }; + } + + return ( +
+
+
+
+ ); +}; + +export default SpectrumPalettePointer; diff --git a/packages/grafana-ui/src/components/ColorPicker/SpectrumPicker.tsx b/packages/grafana-ui/src/components/ColorPicker/SpectrumPicker.tsx deleted file mode 100644 index a225db09046..00000000000 --- a/packages/grafana-ui/src/components/ColorPicker/SpectrumPicker.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import React from 'react'; -import _ from 'lodash'; -import $ from 'jquery'; -import '../../vendor/spectrum'; - -export interface Props { - color: string; - options: object; - onColorSelect: (c: string) => void; -} - -export class SpectrumPicker extends React.Component { - elem: any; - isMoving: boolean; - - constructor(props: Props) { - super(props); - this.onSpectrumMove = this.onSpectrumMove.bind(this); - this.setComponentElem = this.setComponentElem.bind(this); - } - - setComponentElem(elem: any) { - this.elem = $(elem); - } - - onSpectrumMove(color: any) { - this.isMoving = true; - this.props.onColorSelect(color); - } - - componentDidMount() { - const spectrumOptions = _.assignIn( - { - flat: true, - showAlpha: true, - showButtons: false, - color: this.props.color, - appendTo: this.elem, - move: this.onSpectrumMove, - }, - this.props.options - ); - - this.elem.spectrum(spectrumOptions); - this.elem.spectrum('show'); - this.elem.spectrum('set', this.props.color); - } - - componentWillUpdate(nextProps: any) { - // If user move pointer over spectrum field this produce 'move' event and component - // may update props.color. We don't want to update spectrum color in this case, so we can use - // isMoving flag for tracking moving state. Flag should be cleared in componentDidUpdate() which - // is called after updating occurs (when user finished moving). - if (!this.isMoving) { - this.elem.spectrum('set', nextProps.color); - } - } - - componentDidUpdate() { - if (this.isMoving) { - this.isMoving = false; - } - } - - componentWillUnmount() { - this.elem.spectrum('destroy'); - } - - render() { - return
; - } -} diff --git a/packages/grafana-ui/src/components/ColorPicker/_ColorPicker.scss b/packages/grafana-ui/src/components/ColorPicker/_ColorPicker.scss index c0643342307..46eed5f7ff1 100644 --- a/packages/grafana-ui/src/components/ColorPicker/_ColorPicker.scss +++ b/packages/grafana-ui/src/components/ColorPicker/_ColorPicker.scss @@ -1,8 +1,172 @@ +$arrowSize: 15px; +.ColorPicker { + @extend .popper; + font-size: 12px; +} + +.ColorPicker__arrow { + width: 0; + height: 0; + border-style: solid; + position: absolute; + margin: 0px; + + &[data-placement^='top'] { + border-width: $arrowSize $arrowSize 0 $arrowSize; + border-left-color: transparent; + border-right-color: transparent; + border-bottom-color: transparent; + bottom: -$arrowSize; + left: calc(50%-#{$arrowSize}); + padding-top: $arrowSize; + } + + &[data-placement^='bottom'] { + border-width: 0 $arrowSize $arrowSize $arrowSize; + border-left-color: transparent; + border-right-color: transparent; + border-top-color: transparent; + top: 0; + left: calc(50%-#{$arrowSize}); + } + + &[data-placement^='bottom-start'] { + border-width: 0 $arrowSize $arrowSize $arrowSize; + border-left-color: transparent; + border-right-color: transparent; + border-top-color: transparent; + top: 0; + left: $arrowSize; + } + + &[data-placement^='bottom-end'] & { + border-width: 0 $arrowSize $arrowSize $arrowSize; + border-left-color: transparent; + border-right-color: transparent; + border-top-color: transparent; + top: 0; + left: calc(100% -$arrowSize); + } + + &[data-placement^='right'] { + border-width: $arrowSize $arrowSize $arrowSize 0; + border-left-color: transparent; + border-top-color: transparent; + border-bottom-color: transparent; + left: 0; + top: calc(50%-#{$arrowSize}); + } + + &[data-placement^='left'] { + border-width: $arrowSize 0 $arrowSize $arrowSize; + border-top-color: transparent; + border-right-color: transparent; + border-bottom-color: transparent; + right: -$arrowSize; + top: calc(50%-#{$arrowSize}); + } +} + +.ColorPicker__arrow--light { + border-color: #ffffff; +} + +.ColorPicker__arrow--dark { + border-color: #1e2028; +} + +// Top +.ColorPicker[data-placement^='top'] { + padding-bottom: $arrowSize; +} + +// Bottom +.ColorPicker[data-placement^='bottom'] { + padding-top: $arrowSize; +} + +.ColorPicker[data-placement^='bottom-start'] { + padding-top: $arrowSize; +} + +.ColorPicker[data-placement^='bottom-end'] { + padding-top: $arrowSize; +} + +// Right +.ColorPicker[data-placement^='right'] { + padding-left: $arrowSize; +} + +// Left +.ColorPicker[data-placement^='left'] { + padding-right: $arrowSize; +} + +.ColorPickerPopover { + border-radius: 3px; +} + +.ColorPickerPopover--light { + color: black; + background: linear-gradient(180deg, #ffffff 0%, #f7f8fa 104.25%); + box-shadow: 0px 2px 4px #dde4ed, 0px 0px 2px #dde4ed; +} + +.ColorPickerPopover--dark { + color: #d8d9da; + background: linear-gradient(180deg, #1e2028 0%, #161719 104.25%); + box-shadow: 0px 2px 4px #000000, 0px 0px 2px #000000; + + .ColorPickerPopover__tab { + background: #303133; + color: white; + cursor: pointer; + } + .ColorPickerPopover__tab--active { + background: none; + } +} + +.ColorPickerPopover__content { + width: 336px; + min-height: 184px; + padding: 24px; +} + +.ColorPickerPopover__tabs { + display: flex; + width: 100%; + border-radius: 3px 3px 0 0; + overflow: hidden; +} + +.ColorPickerPopover__tab { + width: 50%; + text-align: center; + padding: 8px 0; + background: #dde4ed; +} + +.ColorPickerPopover__tab--active { + background: white; +} + +.ColorPicker__axisSwitch { + width: 100%; +} + +.ColorPicker__axisSwitchLabel { + display: flex; + flex-grow: 1; +} + .sp-replacer { background: inherit; border: none; color: inherit; padding: 0; + border-radius: 10px; } .sp-replacer:hover, @@ -35,10 +199,22 @@ margin: 0; float: left; z-index: 0; + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg==); +} + +.sp-preview-inner, +.sp-alpha-inner, +.sp-thumb-inner { + display: block; + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; } .gf-color-picker__body { - padding-bottom: 10px; + padding-bottom: $arrowSize; padding-left: 6px; } @@ -47,3 +223,18 @@ width: 210px; } } + +// TODO: Remove. This is a temporary solution until color picker popovers are used +// with Drop.js. +.drop-popover.drop-popover--transparent { + .drop-content { + border: none; + background: none; + padding: 0; + max-width: none; + + &:before { + display: none; + } + } +} diff --git a/packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx b/packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx index 12b5ff8062e..40f6c6c3c37 100644 --- a/packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx +++ b/packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx @@ -9,6 +9,8 @@ interface Props { autoHideDuration?: number; autoHeightMax?: string; hideTracksWhenNotNeeded?: boolean; + renderTrackHorizontal?: React.FunctionComponent; + renderTrackVertical?: React.FunctionComponent; scrollTop?: number; setScrollTop: (event: any) => void; autoHeightMin?: number | string; @@ -66,6 +68,8 @@ export class CustomScrollbar extends PureComponent { autoHide, autoHideTimeout, hideTracksWhenNotNeeded, + renderTrackHorizontal, + renderTrackVertical, } = this.props; return ( @@ -81,8 +85,8 @@ export class CustomScrollbar extends PureComponent { // Before these where set to inhert but that caused problems with cut of legends in firefox autoHeightMax={autoHeightMax} autoHeightMin={autoHeightMin} - renderTrackHorizontal={props =>
} - renderTrackVertical={props =>
} + renderTrackHorizontal={renderTrackHorizontal || (props =>
)} + renderTrackVertical={renderTrackVertical || (props =>
)} renderThumbHorizontal={props =>
} renderThumbVertical={props =>
} renderView={props =>
} diff --git a/packages/grafana-ui/src/components/DeleteButton/DeleteButton.story.tsx b/packages/grafana-ui/src/components/DeleteButton/DeleteButton.story.tsx new file mode 100644 index 00000000000..e086663f065 --- /dev/null +++ b/packages/grafana-ui/src/components/DeleteButton/DeleteButton.story.tsx @@ -0,0 +1,24 @@ +import React, { FunctionComponent } from 'react'; +import { storiesOf } from '@storybook/react'; +import { DeleteButton } from '@grafana/ui'; + +const CenteredStory: FunctionComponent<{}> = ({ children }) => { + return ( +
+ {children} +
+ ); +}; + +storiesOf('UI/DeleteButton', module) + .addDecorator(story => {story()}) + .add('default', () => { + return {}} />; + }); diff --git a/packages/grafana-ui/src/components/Gauge/Gauge.tsx b/packages/grafana-ui/src/components/Gauge/Gauge.tsx index 2dce20543fd..90248a9b6b0 100644 --- a/packages/grafana-ui/src/components/Gauge/Gauge.tsx +++ b/packages/grafana-ui/src/components/Gauge/Gauge.tsx @@ -1,10 +1,14 @@ import React, { PureComponent } from 'react'; import $ from 'jquery'; -import { ValueMapping, Threshold, ThemeName, BasicGaugeColor, ThemeNames } from '../../types/panel'; +import { ValueMapping, Threshold, BasicGaugeColor } from '../../types/panel'; import { TimeSeriesVMs } from '../../types/series'; +import { GrafanaTheme } from '../../types'; +import { getMappedValue } from '../../utils/valueMappings'; import { getValueFormat } from '../../utils/valueFormats/valueFormats'; -import { TimeSeriesValue, getMappedValue } from '../../utils/valueMappings'; +import { getColorFromHexRgbOrName } from '../../utils/namedColorsPalette'; + +type TimeSeriesValue = string | number | null; export interface Props { decimals: number; @@ -21,7 +25,7 @@ export interface Props { suffix: string; unit: string; width: number; - theme?: ThemeName; + theme?: GrafanaTheme; } export class Gauge extends PureComponent { @@ -38,7 +42,7 @@ export class Gauge extends PureComponent { thresholds: [], unit: 'none', stat: 'avg', - theme: ThemeNames.Dark, + theme: GrafanaTheme.Dark, }; componentDidMount() { @@ -71,29 +75,29 @@ export class Gauge extends PureComponent { } getFontColor(value: TimeSeriesValue) { - const { thresholds } = this.props; + const { thresholds, theme } = this.props; if (thresholds.length === 1) { - return thresholds[0].color; + return getColorFromHexRgbOrName(thresholds[0].color, theme); } const atThreshold = thresholds.filter(threshold => (value as number) === threshold.value)[0]; if (atThreshold) { - return atThreshold.color; + return getColorFromHexRgbOrName(atThreshold.color, theme); } const belowThreshold = thresholds.filter(threshold => (value as number) > threshold.value); if (belowThreshold.length > 0) { const nearestThreshold = belowThreshold.sort((t1, t2) => t2.value - t1.value)[0]; - return nearestThreshold.color; + return getColorFromHexRgbOrName(nearestThreshold.color, theme); } return BasicGaugeColor.Red; } getFormattedThresholds() { - const { maxValue, minValue, thresholds } = this.props; + const { maxValue, minValue, thresholds, theme } = this.props; const thresholdsSortedByIndex = [...thresholds].sort((t1, t2) => t1.index - t2.index); const lastThreshold = thresholdsSortedByIndex[thresholdsSortedByIndex.length - 1]; @@ -101,13 +105,13 @@ export class Gauge extends PureComponent { const formattedThresholds = [ ...thresholdsSortedByIndex.map(threshold => { if (threshold.index === 0) { - return { value: minValue, color: threshold.color }; + return { value: minValue, color: getColorFromHexRgbOrName(threshold.color, theme) }; } const previousThreshold = thresholdsSortedByIndex[threshold.index - 1]; - return { value: threshold.value, color: previousThreshold.color }; + return { value: threshold.value, color: getColorFromHexRgbOrName(previousThreshold.color, theme) }; }), - { value: maxValue, color: lastThreshold.color }, + { value: maxValue, color: getColorFromHexRgbOrName(lastThreshold.color, theme) }, ]; return formattedThresholds; @@ -135,7 +139,7 @@ export class Gauge extends PureComponent { } const dimension = Math.min(width, height * 1.3); - const backgroundColor = theme === ThemeNames.Light ? 'rgb(230,230,230)' : 'rgb(38,38,38)'; + const backgroundColor = theme === GrafanaTheme.Light ? 'rgb(230,230,230)' : 'rgb(38,38,38)'; const fontScale = parseInt('80', 10) / 100; const fontSize = Math.min(dimension / 5, 100) * fontScale; const gaugeWidthReduceRatio = showThresholdLabels ? 1.5 : 1; diff --git a/packages/grafana-ui/src/components/Select/SelectOption.test.tsx b/packages/grafana-ui/src/components/Select/SelectOption.test.tsx index 1876e438d75..0a5a8864d64 100644 --- a/packages/grafana-ui/src/components/Select/SelectOption.test.tsx +++ b/packages/grafana-ui/src/components/Select/SelectOption.test.tsx @@ -3,6 +3,7 @@ import renderer from 'react-test-renderer'; import SelectOption from './SelectOption'; import { OptionProps } from 'react-select/lib/components/Option'; +// @ts-ignore const model: OptionProps = { data: jest.fn(), cx: jest.fn(), diff --git a/public/app/core/components/Switch/Switch.tsx b/packages/grafana-ui/src/components/Switch/Switch.tsx similarity index 76% rename from public/app/core/components/Switch/Switch.tsx rename to packages/grafana-ui/src/components/Switch/Switch.tsx index d53a3d68878..36475e55acb 100644 --- a/public/app/core/components/Switch/Switch.tsx +++ b/packages/grafana-ui/src/components/Switch/Switch.tsx @@ -4,10 +4,11 @@ import _ from 'lodash'; export interface Props { label: string; checked: boolean; + className?: string; labelClass?: string; switchClass?: string; transparent?: boolean; - onChange: (event) => any; + onChange: (event?: React.SyntheticEvent) => void; } export interface State { @@ -19,20 +20,21 @@ export class Switch extends PureComponent { id: _.uniqueId(), }; - internalOnChange = event => { + internalOnChange = (event: React.FormEvent) => { event.stopPropagation(); - this.props.onChange(event); + + this.props.onChange(); }; render() { - const { labelClass = '', switchClass = '', label, checked, transparent } = this.props; + const { labelClass = '', switchClass = '', label, checked, transparent, className } = this.props; const labelId = `check-${this.state.id}`; const labelClassName = `gf-form-label ${labelClass} ${transparent ? 'gf-form-label--transparent' : ''} pointer`; const switchClassName = `gf-form-switch ${switchClass} ${transparent ? 'gf-form-switch--transparent' : ''}`; return ( -