diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json
index 937475b9cc5..6a5d362b7fa 100644
--- a/packages/grafana-ui/package.json
+++ b/packages/grafana-ui/package.json
@@ -27,6 +27,7 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
+ "@emotion/core": "^10.0.27",
"@grafana/data": "6.7.0-pre",
"@grafana/slate-react": "0.22.9-grafana",
"@grafana/tsconfig": "^1.0.0-rc1",
@@ -47,6 +48,7 @@
"papaparse": "4.6.3",
"rc-cascader": "0.17.5",
"rc-drawer": "3.0.2",
+ "rc-slider": "8.7.1",
"rc-time-picker": "^3.7.2",
"react": "16.12.0",
"react-calendar": "2.19.2",
@@ -78,6 +80,7 @@
"@types/node": "10.14.1",
"@types/papaparse": "4.5.9",
"@types/pretty-format": "20.0.1",
+ "@types/rc-slider": "8.6.5",
"@types/react": "16.8.16",
"@types/react-custom-scrollbars": "4.0.5",
"@types/react-test-renderer": "16.9.0",
diff --git a/packages/grafana-ui/src/components/Slider/Slider.story.tsx b/packages/grafana-ui/src/components/Slider/Slider.story.tsx
new file mode 100644
index 00000000000..2bd81439a5b
--- /dev/null
+++ b/packages/grafana-ui/src/components/Slider/Slider.story.tsx
@@ -0,0 +1,29 @@
+import React from 'react';
+import { Slider } from './Slider';
+import { select, number, boolean } from '@storybook/addon-knobs';
+
+export default {
+ title: 'General/Slider',
+ component: Slider,
+};
+
+const getKnobs = () => {
+ return {
+ min: number('min', 0),
+ max: number('max', 100),
+ orientation: select('orientation', ['horizontal', 'vertical'], 'horizontal'),
+ reverse: boolean('reverse', true),
+ singleValue: boolean('single value', false),
+ };
+};
+
+const SliderWrapper = () => {
+ const { min, max, orientation, reverse, singleValue } = getKnobs();
+ return (
+
+
+
+ );
+};
+
+export const basic = () => ;
diff --git a/packages/grafana-ui/src/components/Slider/Slider.test.tsx b/packages/grafana-ui/src/components/Slider/Slider.test.tsx
new file mode 100644
index 00000000000..90b4d605f44
--- /dev/null
+++ b/packages/grafana-ui/src/components/Slider/Slider.test.tsx
@@ -0,0 +1,27 @@
+import React from 'react';
+import { Slider, Props } from './Slider';
+import { mount } from 'enzyme';
+
+const sliderProps: Props = {
+ min: 10,
+ max: 20,
+};
+
+describe('Slider', () => {
+ it('renders without error', () => {
+ mount();
+ });
+ it('renders correct contents', () => {
+ const wrapper = mount();
+ expect(wrapper.html()).toContain('aria-valuemin="10"');
+ expect(wrapper.html()).toContain('aria-valuemax="20"');
+ expect(wrapper.html()).toContain('aria-valuenow="10"');
+ expect(wrapper.html()).toContain('aria-valuenow="20"');
+ });
+ it('renders correct contents with a value', () => {
+ const wrapper = mount();
+ expect(wrapper.html()).toContain('aria-valuenow="15"');
+ expect(wrapper.html()).not.toContain('aria-valuenow="20"');
+ expect(wrapper.html()).not.toContain('aria-valuenow="10"');
+ });
+});
diff --git a/packages/grafana-ui/src/components/Slider/Slider.tsx b/packages/grafana-ui/src/components/Slider/Slider.tsx
new file mode 100644
index 00000000000..585e0e0e037
--- /dev/null
+++ b/packages/grafana-ui/src/components/Slider/Slider.tsx
@@ -0,0 +1,128 @@
+import React, { FunctionComponent } from 'react';
+import { Range, createSliderWithTooltip } from 'rc-slider';
+import { cx, css } from 'emotion';
+import { Global, css as cssCore } from '@emotion/core';
+import { stylesFactory } from '../../themes';
+import { GrafanaTheme } from '@grafana/data';
+import { useTheme } from '../../themes/ThemeContext';
+import { Orientation } from '../../types/orientation';
+
+export interface Props {
+ min: number;
+ max: number;
+ orientation?: Orientation;
+ /** Set current positions of handle(s). If only 1 value supplied, only 1 handle displayed. */
+ value?: number[];
+ reverse?: boolean;
+ formatTooltipResult?: (value: number) => number | string;
+ onChange?: (values: number[]) => void;
+}
+
+const getStyles = stylesFactory((theme: GrafanaTheme, isHorizontal: boolean) => {
+ const trackColor = theme.isLight ? theme.colors.gray5 : theme.colors.dark6;
+ const container = isHorizontal
+ ? css`
+ width: 100%;
+ margin: ${theme.spacing.lg} ${theme.spacing.sm} ${theme.spacing.sm} ${theme.spacing.sm};
+ `
+ : css`
+ height: 100%;
+ margin: ${theme.spacing.sm} ${theme.spacing.lg} ${theme.spacing.sm} ${theme.spacing.sm};
+ `;
+
+ return {
+ container,
+ slider: css`
+ .rc-slider-vertical .rc-slider-handle {
+ margin-top: -10px;
+ }
+ .rc-slider-handle {
+ border: solid 2px ${theme.colors.blue77};
+ background-color: ${theme.colors.blue77};
+ }
+ .rc-slider-handle:hover {
+ border-color: ${theme.colors.blue77};
+ }
+ .rc-slider-handle:focus {
+ border-color: ${theme.colors.blue77};
+ box-shadow: none;
+ }
+ .rc-slider-handle:active {
+ border-color: ${theme.colors.blue77};
+ box-shadow: none;
+ }
+ .rc-slider-handle-click-focused:focus {
+ border-color: ${theme.colors.blue77};
+ }
+ .rc-slider-dot-active {
+ border-color: ${theme.colors.blue77};
+ }
+ .rc-slider-track {
+ background-color: ${theme.colors.blue77};
+ }
+ .rc-slider-rail {
+ background-color: ${trackColor};
+ border: 1px solid ${trackColor};
+ }
+ `,
+ /** Global component from @emotion/core doesn't accept computed classname string returned from css from emotion.
+ * It accepts object containing the computed name and flattened styles returned from css from @emotion/core
+ * */
+ tooltip: cssCore`
+ body {
+ .rc-slider-tooltip {
+ cursor: grab;
+ user-select: none;
+ }
+
+ .rc-slider-tooltip-inner {
+ color: ${theme.colors.text};
+ background-color: transparent !important;
+ border-radius: 0;
+ box-shadow: none;
+ }
+
+ .rc-slider-tooltip-placement-top .rc-slider-tooltip-arrow {
+ display: none;
+ }
+
+ .rc-slider-tooltip-placement-top {
+ padding: 0;
+ }
+ }
+ `,
+ };
+});
+
+export const Slider: FunctionComponent = ({
+ min,
+ max,
+ onChange,
+ orientation = 'horizontal',
+ reverse,
+ formatTooltipResult,
+ value,
+}) => {
+ const isHorizontal = orientation === 'horizontal';
+ const theme = useTheme();
+ const styles = getStyles(theme, isHorizontal);
+ const RangeWithTooltip = createSliderWithTooltip(Range);
+ return (
+
+ {/** Slider tooltip's parent component is body and therefore we need Global component to do css overrides for it. */}
+
+ (formatTooltipResult ? formatTooltipResult(value) : value)}
+ onChange={onChange}
+ vertical={!isHorizontal}
+ reverse={reverse}
+ />
+
+ );
+};
+
+Slider.displayName = 'Slider';
diff --git a/packages/grafana-ui/src/components/Slider/_Slider.scss b/packages/grafana-ui/src/components/Slider/_Slider.scss
new file mode 100644
index 00000000000..271134d811b
--- /dev/null
+++ b/packages/grafana-ui/src/components/Slider/_Slider.scss
@@ -0,0 +1 @@
+@import '../../node_modules/rc-slider/assets/index.css';
diff --git a/packages/grafana-ui/src/components/index.scss b/packages/grafana-ui/src/components/index.scss
index c22a9f5406c..d6c5dd824ad 100644
--- a/packages/grafana-ui/src/components/index.scss
+++ b/packages/grafana-ui/src/components/index.scss
@@ -15,3 +15,4 @@
@import 'Tooltip/Tooltip';
@import 'ValueMappingsEditor/ValueMappingsEditor';
@import 'Alert/Alert';
+@import 'Slider/Slider';
diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts
index 2efde49203d..f7d678b254b 100644
--- a/packages/grafana-ui/src/components/index.ts
+++ b/packages/grafana-ui/src/components/index.ts
@@ -115,6 +115,7 @@ export { Segment, SegmentAsync, SegmentInput, SegmentSelect } from './Segment/';
export { default as Chart } from './Chart';
export { Icon } from './Icon/Icon';
export { Drawer } from './Drawer/Drawer';
+export { Slider } from './Slider/Slider';
// TODO: namespace!!
export {
diff --git a/packages/grafana-ui/src/types/orientation.ts b/packages/grafana-ui/src/types/orientation.ts
new file mode 100644
index 00000000000..5c7be1d0550
--- /dev/null
+++ b/packages/grafana-ui/src/types/orientation.ts
@@ -0,0 +1 @@
+export type Orientation = 'horizontal' | 'vertical';