mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Field config API: add slider editor (#28007)
* Field config: implementation slider editor (#27592) * PR-28007 feedback fixed * Field config: implementation slider editor (#27592) * PR-28007 feedback fixed * processed review PR-28007 * Field config: implementation slider editor (#27592) * PR-28007 feedback fixed * Field config: implementation slider editor (#27592) * processed review PR-28007 * fixing leftover number[] bugs * RichHistoryQueriesTab.test fix + slider vertical feat fixed * fixed Slider.test.tsx expectation * Added @docs to prevent build-frontend-docs from failing Co-authored-by: Isa Ozler <contactme@isaozler.com>
This commit is contained in:
parent
1bff2fdeea
commit
85a04794ac
@ -24,6 +24,12 @@ export const numberOverrideProcessor = (
|
||||
return parseFloat(value);
|
||||
};
|
||||
|
||||
export interface SliderFieldConfigSettings {
|
||||
min: number;
|
||||
max: number;
|
||||
step?: number;
|
||||
}
|
||||
|
||||
export interface DataLinksFieldConfigSettings {}
|
||||
|
||||
export const dataLinksOverrideProcessor = (
|
||||
|
@ -1,6 +1,11 @@
|
||||
import { ComponentType } from 'react';
|
||||
import { RegistryItem, Registry } from '../utils/Registry';
|
||||
import { NumberFieldConfigSettings, SelectFieldConfigSettings, StringFieldConfigSettings } from '../field';
|
||||
import {
|
||||
NumberFieldConfigSettings,
|
||||
SliderFieldConfigSettings,
|
||||
SelectFieldConfigSettings,
|
||||
StringFieldConfigSettings,
|
||||
} from '../field';
|
||||
|
||||
/**
|
||||
* Option editor registry item
|
||||
@ -71,6 +76,10 @@ export interface OptionsUIRegistryBuilderAPI<
|
||||
config: OptionEditorConfig<TOptions, TSettings, number>
|
||||
): this;
|
||||
|
||||
addSliderInput?<TSettings extends SliderFieldConfigSettings = SliderFieldConfigSettings>(
|
||||
config: OptionEditorConfig<TOptions, TSettings, number>
|
||||
): this;
|
||||
|
||||
addTextInput?<TSettings extends StringFieldConfigSettings = StringFieldConfigSettings>(
|
||||
config: OptionEditorConfig<TOptions, TSettings, string>
|
||||
): this;
|
||||
|
@ -12,6 +12,7 @@ import {
|
||||
StandardEditorProps,
|
||||
StringFieldConfigSettings,
|
||||
NumberFieldConfigSettings,
|
||||
SliderFieldConfigSettings,
|
||||
ColorFieldConfigSettings,
|
||||
identityOverrideProcessor,
|
||||
UnitFieldConfigSettings,
|
||||
@ -39,6 +40,18 @@ export class FieldConfigEditorBuilder<TOptions> extends OptionsUIRegistryBuilder
|
||||
});
|
||||
}
|
||||
|
||||
addSliderInput<TSettings>(config: FieldConfigEditorConfig<TOptions, TSettings & SliderFieldConfigSettings, number>) {
|
||||
return this.addCustomEditor({
|
||||
...config,
|
||||
id: config.path,
|
||||
override: standardEditorsRegistry.get('slider').editor as any,
|
||||
editor: standardEditorsRegistry.get('slider').editor as any,
|
||||
process: numberOverrideProcessor,
|
||||
shouldApply: config.shouldApply ? config.shouldApply : field => field.type === FieldType.number,
|
||||
settings: config.settings || {},
|
||||
});
|
||||
}
|
||||
|
||||
addTextInput<TSettings>(config: FieldConfigEditorConfig<TOptions, TSettings & StringFieldConfigSettings, string>) {
|
||||
return this.addCustomEditor({
|
||||
...config,
|
||||
@ -136,6 +149,14 @@ export class PanelOptionsEditorBuilder<TOptions> extends OptionsUIRegistryBuilde
|
||||
});
|
||||
}
|
||||
|
||||
addSliderInput<TSettings>(config: PanelOptionsEditorConfig<TOptions, TSettings & SliderFieldConfigSettings, number>) {
|
||||
return this.addCustomEditor({
|
||||
...config,
|
||||
id: config.path,
|
||||
editor: standardEditorsRegistry.get('slider').editor as any,
|
||||
});
|
||||
}
|
||||
|
||||
addTextInput<TSettings>(config: PanelOptionsEditorConfig<TOptions, TSettings & StringFieldConfigSettings, string>) {
|
||||
return this.addCustomEditor({
|
||||
...config,
|
||||
|
27
packages/grafana-ui/src/components/OptionsUI/slider.tsx
Normal file
27
packages/grafana-ui/src/components/OptionsUI/slider.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { FieldConfigEditorProps, SliderFieldConfigSettings } from '@grafana/data';
|
||||
import { Slider } from '../Slider/Slider';
|
||||
|
||||
export const SliderValueEditor: React.FC<FieldConfigEditorProps<number, SliderFieldConfigSettings>> = ({
|
||||
value,
|
||||
onChange,
|
||||
item,
|
||||
}) => {
|
||||
const { settings } = item;
|
||||
const onValueAfterChange = useCallback(
|
||||
(value?: number) => {
|
||||
onChange(value);
|
||||
},
|
||||
[onChange]
|
||||
);
|
||||
const initialValue = typeof value === 'number' ? value : typeof value === 'string' ? +value : 0;
|
||||
return (
|
||||
<Slider
|
||||
value={initialValue}
|
||||
min={settings?.min || 0}
|
||||
max={settings?.max || 100}
|
||||
step={settings?.step}
|
||||
onAfterChange={onValueAfterChange}
|
||||
/>
|
||||
);
|
||||
};
|
12
packages/grafana-ui/src/components/Slider/RangeSlider.mdx
Normal file
12
packages/grafana-ui/src/components/Slider/RangeSlider.mdx
Normal file
@ -0,0 +1,12 @@
|
||||
import { Meta, Props } from '@storybook/addon-docs/blocks';
|
||||
import { RangeSliderProps } from './types';
|
||||
|
||||
<Meta title="MDX|RangeSlider" />
|
||||
|
||||
# Range-slider
|
||||
|
||||
The `Range-slider` component is an input element where users can manipulate two values on a one-dimensional axis.
|
||||
|
||||
`Range-slider` can be implemented in horizontal or vertical orientation. You can set the default starting values for the slider with the `value` prop.
|
||||
|
||||
<Props of={RangeSliderProps} />
|
@ -0,0 +1,30 @@
|
||||
import React from 'react';
|
||||
import { RangeSlider } from '@grafana/ui';
|
||||
import { select, number, boolean } from '@storybook/addon-knobs';
|
||||
|
||||
export default {
|
||||
title: 'Forms/Slider/Range',
|
||||
component: RangeSlider,
|
||||
};
|
||||
|
||||
const getKnobs = () => {
|
||||
return {
|
||||
min: number('min', 0),
|
||||
max: number('max', 100),
|
||||
step: boolean('enable step', false),
|
||||
orientation: select('orientation', ['horizontal', 'vertical'], 'horizontal'),
|
||||
reverse: boolean('reverse', false),
|
||||
};
|
||||
};
|
||||
|
||||
const SliderWrapper = () => {
|
||||
const { min, max, orientation, reverse, step } = getKnobs();
|
||||
const stepValue = step ? 10 : undefined;
|
||||
return (
|
||||
<div style={{ width: '200px', height: '200px' }}>
|
||||
<RangeSlider min={min} max={max} step={stepValue} orientation={orientation} value={[10, 20]} reverse={reverse} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const basic = () => <SliderWrapper />;
|
@ -0,0 +1,17 @@
|
||||
import React from 'react';
|
||||
import { RangeSlider } from './RangeSlider';
|
||||
import { RangeSliderProps } from './types';
|
||||
import { render } from '@testing-library/react';
|
||||
|
||||
const sliderProps: RangeSliderProps = {
|
||||
min: 10,
|
||||
max: 20,
|
||||
};
|
||||
|
||||
describe('RangeSlider', () => {
|
||||
it('renders without error', () => {
|
||||
expect(() => {
|
||||
render(<RangeSlider {...sliderProps} />);
|
||||
});
|
||||
});
|
||||
});
|
53
packages/grafana-ui/src/components/Slider/RangeSlider.tsx
Normal file
53
packages/grafana-ui/src/components/Slider/RangeSlider.tsx
Normal file
@ -0,0 +1,53 @@
|
||||
import React, { FunctionComponent } from 'react';
|
||||
import { Range as RangeComponent, createSliderWithTooltip } from 'rc-slider';
|
||||
import { cx } from 'emotion';
|
||||
import { Global } from '@emotion/core';
|
||||
import { useTheme } from '../../themes/ThemeContext';
|
||||
import { getStyles } from './styles';
|
||||
import { RangeSliderProps } from './types';
|
||||
|
||||
/**
|
||||
* @public
|
||||
*
|
||||
* RichHistoryQueriesTab uses this Range Component
|
||||
*/
|
||||
export const RangeSlider: FunctionComponent<RangeSliderProps> = ({
|
||||
min,
|
||||
max,
|
||||
onChange,
|
||||
onAfterChange,
|
||||
orientation = 'horizontal',
|
||||
reverse,
|
||||
step,
|
||||
formatTooltipResult,
|
||||
value,
|
||||
tooltipAlwaysVisible = true,
|
||||
}) => {
|
||||
const isHorizontal = orientation === 'horizontal';
|
||||
const theme = useTheme();
|
||||
const styles = getStyles(theme, isHorizontal);
|
||||
const RangeWithTooltip = createSliderWithTooltip(RangeComponent);
|
||||
return (
|
||||
<div className={cx(styles.container, styles.slider)}>
|
||||
{/** Slider tooltip's parent component is body and therefore we need Global component to do css overrides for it. */}
|
||||
<Global styles={styles.tooltip} />
|
||||
<RangeWithTooltip
|
||||
tipProps={{
|
||||
visible: tooltipAlwaysVisible,
|
||||
placement: isHorizontal ? 'top' : 'right',
|
||||
}}
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
defaultValue={value}
|
||||
tipFormatter={(value: number) => (formatTooltipResult ? formatTooltipResult(value) : value)}
|
||||
onChange={onChange}
|
||||
onAfterChange={onAfterChange}
|
||||
vertical={!isHorizontal}
|
||||
reverse={reverse}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
RangeSlider.displayName = 'Range';
|
@ -1,12 +1,12 @@
|
||||
import { Meta, Props } from '@storybook/addon-docs/blocks';
|
||||
import { Slider } from './Slider';
|
||||
import { SliderProps } from './types';
|
||||
|
||||
<Meta title="MDX|Slider" />
|
||||
|
||||
# Slider
|
||||
|
||||
The `Slider` component is an input element where users can manipulate one or two values on a one-dimensional axis.
|
||||
The `Slider` component is an input element where users can manipulate one value on a one-dimensional axis.
|
||||
|
||||
`Slider` can be implemented in horizontal or vertical orientation. You can set the default starting value(s) for the slider with the `value` prop.
|
||||
`Slider` can be implemented in horizontal or vertical orientation. You can set the default starting value(s) for the slider with the `value` prop.
|
||||
|
||||
<Props of={Slider} />
|
||||
<Props of={SliderProps} />
|
||||
|
@ -13,24 +13,16 @@ const getKnobs = () => {
|
||||
max: number('max', 100),
|
||||
step: boolean('enable step', false),
|
||||
orientation: select('orientation', ['horizontal', 'vertical'], 'horizontal'),
|
||||
reverse: boolean('reverse', true),
|
||||
singleValue: boolean('single value', false),
|
||||
reverse: boolean('reverse', false),
|
||||
};
|
||||
};
|
||||
|
||||
const SliderWrapper = () => {
|
||||
const { min, max, orientation, reverse, singleValue, step } = getKnobs();
|
||||
const { min, max, orientation, reverse, step } = getKnobs();
|
||||
const stepValue = step ? 10 : undefined;
|
||||
return (
|
||||
<div style={{ width: '200px', height: '200px' }}>
|
||||
<Slider
|
||||
min={min}
|
||||
max={max}
|
||||
step={stepValue}
|
||||
orientation={orientation}
|
||||
value={singleValue ? [10] : undefined}
|
||||
reverse={reverse}
|
||||
/>
|
||||
<Slider min={min} max={max} step={stepValue} orientation={orientation} value={10} reverse={reverse} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -1,8 +1,9 @@
|
||||
import React from 'react';
|
||||
import { Slider, Props } from './Slider';
|
||||
import { Slider } from './Slider';
|
||||
import { SliderProps } from './types';
|
||||
import { mount } from 'enzyme';
|
||||
|
||||
const sliderProps: Props = {
|
||||
const sliderProps: SliderProps = {
|
||||
min: 10,
|
||||
max: 20,
|
||||
};
|
||||
@ -17,11 +18,10 @@ describe('Slider', () => {
|
||||
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(<Slider {...sliderProps} value={[15]} />);
|
||||
const wrapper = mount(<Slider {...sliderProps} value={15} />);
|
||||
expect(wrapper.html()).toContain('aria-valuenow="15"');
|
||||
expect(wrapper.html()).not.toContain('aria-valuenow="20"');
|
||||
expect(wrapper.html()).not.toContain('aria-valuenow="10"');
|
||||
|
@ -1,104 +1,15 @@
|
||||
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 React, { useState, useCallback, ChangeEvent, FunctionComponent } from 'react';
|
||||
import SliderComponent from 'rc-slider';
|
||||
import { cx } from 'emotion';
|
||||
import { Global } from '@emotion/core';
|
||||
import { useTheme } from '../../themes/ThemeContext';
|
||||
import { Orientation } from '../../types/orientation';
|
||||
import { getStyles } from './styles';
|
||||
import { SliderProps } from './types';
|
||||
|
||||
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;
|
||||
step?: number;
|
||||
tooltipAlwaysVisible?: boolean;
|
||||
formatTooltipResult?: (value: number) => number | string;
|
||||
onChange?: (values: number[]) => void;
|
||||
onAfterChange?: (values: number[]) => void;
|
||||
}
|
||||
|
||||
const getStyles = stylesFactory((theme: GrafanaTheme, isHorizontal: boolean) => {
|
||||
const trackColor = theme.isLight ? theme.palette.gray5 : theme.palette.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.palette.blue77};
|
||||
background-color: ${theme.palette.blue77};
|
||||
}
|
||||
.rc-slider-handle:hover {
|
||||
border-color: ${theme.palette.blue77};
|
||||
}
|
||||
.rc-slider-handle:focus {
|
||||
border-color: ${theme.palette.blue77};
|
||||
box-shadow: none;
|
||||
}
|
||||
.rc-slider-handle:active {
|
||||
border-color: ${theme.palette.blue77};
|
||||
box-shadow: none;
|
||||
}
|
||||
.rc-slider-handle-click-focused:focus {
|
||||
border-color: ${theme.palette.blue77};
|
||||
}
|
||||
.rc-slider-dot-active {
|
||||
border-color: ${theme.palette.blue77};
|
||||
}
|
||||
.rc-slider-track {
|
||||
background-color: ${theme.palette.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;
|
||||
z-index: ${theme.zIndex.tooltip};
|
||||
}
|
||||
|
||||
.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<Props> = ({
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export const Slider: FunctionComponent<SliderProps> = ({
|
||||
min,
|
||||
max,
|
||||
onChange,
|
||||
@ -106,33 +17,63 @@ export const Slider: FunctionComponent<Props> = ({
|
||||
orientation = 'horizontal',
|
||||
reverse,
|
||||
step,
|
||||
formatTooltipResult,
|
||||
value,
|
||||
tooltipAlwaysVisible = true,
|
||||
}) => {
|
||||
const isHorizontal = orientation === 'horizontal';
|
||||
const theme = useTheme();
|
||||
const styles = getStyles(theme, isHorizontal);
|
||||
const RangeWithTooltip = createSliderWithTooltip(Range);
|
||||
const SliderWithTooltip = SliderComponent;
|
||||
const [slidervalue, setSliderValue] = useState<number>(value || min);
|
||||
const onSliderChange = useCallback((v: number) => {
|
||||
setSliderValue(v);
|
||||
|
||||
if (onChange) {
|
||||
onChange(v);
|
||||
}
|
||||
}, []);
|
||||
const onSliderInputChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
|
||||
let v = +e.target.value;
|
||||
|
||||
v > max && (v = max);
|
||||
v < min && (v = min);
|
||||
|
||||
setSliderValue(v);
|
||||
|
||||
if (onChange) {
|
||||
onChange(v);
|
||||
}
|
||||
|
||||
if (onAfterChange) {
|
||||
onAfterChange(v);
|
||||
}
|
||||
}, []);
|
||||
const sliderInputClassNames = !isHorizontal ? [styles.sliderInputVertical] : [];
|
||||
const sliderInputFieldClassNames = !isHorizontal ? [styles.sliderInputFieldVertical] : [];
|
||||
return (
|
||||
<div className={cx(styles.container, styles.slider)}>
|
||||
{/** Slider tooltip's parent component is body and therefore we need Global component to do css overrides for it. */}
|
||||
<Global styles={styles.tooltip} />
|
||||
<RangeWithTooltip
|
||||
tipProps={{
|
||||
visible: tooltipAlwaysVisible,
|
||||
placement: isHorizontal ? 'top' : 'right',
|
||||
}}
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
defaultValue={value || [min, max]}
|
||||
tipFormatter={(value: number) => (formatTooltipResult ? formatTooltipResult(value) : value)}
|
||||
onChange={onChange}
|
||||
onAfterChange={onAfterChange}
|
||||
vertical={!isHorizontal}
|
||||
reverse={reverse}
|
||||
/>
|
||||
<label className={cx(styles.sliderInput, ...sliderInputClassNames)}>
|
||||
<SliderWithTooltip
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
defaultValue={value}
|
||||
value={slidervalue}
|
||||
onChange={onSliderChange}
|
||||
onAfterChange={onAfterChange}
|
||||
vertical={!isHorizontal}
|
||||
reverse={reverse}
|
||||
/>
|
||||
<input
|
||||
className={cx(styles.sliderInputField, ...sliderInputFieldClassNames)}
|
||||
type="number"
|
||||
value={`${slidervalue}`} // to fix the react leading zero issue
|
||||
onChange={onSliderInputChange}
|
||||
min={min}
|
||||
max={max}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
122
packages/grafana-ui/src/components/Slider/styles.ts
Normal file
122
packages/grafana-ui/src/components/Slider/styles.ts
Normal file
@ -0,0 +1,122 @@
|
||||
import { stylesFactory } from '../../themes';
|
||||
import { GrafanaTheme } from '@grafana/data';
|
||||
import { focusCss } from '../../themes/mixins';
|
||||
import { css as cssCore } from '@emotion/core';
|
||||
import { css } from 'emotion';
|
||||
|
||||
export const getFocusStyle = (theme: GrafanaTheme) => css`
|
||||
&:focus {
|
||||
${focusCss(theme)}
|
||||
}
|
||||
`;
|
||||
|
||||
export const getStyles = stylesFactory((theme: GrafanaTheme, isHorizontal: boolean) => {
|
||||
const trackColor = theme.isLight ? theme.palette.gray5 : theme.palette.dark6;
|
||||
const container = isHorizontal
|
||||
? css`
|
||||
width: 100%;
|
||||
`
|
||||
: css`
|
||||
height: 100%;
|
||||
margin: ${theme.spacing.sm} ${theme.spacing.lg} ${theme.spacing.sm} ${theme.spacing.sm};
|
||||
`;
|
||||
|
||||
return {
|
||||
container,
|
||||
slider: css`
|
||||
.rc-slider {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
margin-left: 7px; // half the size of the handle to align handle to the left on 0 value
|
||||
}
|
||||
.rc-slider-vertical .rc-slider-handle {
|
||||
margin-top: -10px;
|
||||
}
|
||||
.rc-slider-handle {
|
||||
border: solid 2px ${theme.palette.blue77};
|
||||
background-color: ${theme.palette.blue77};
|
||||
}
|
||||
.rc-slider-handle:hover {
|
||||
border-color: ${theme.palette.blue77};
|
||||
}
|
||||
.rc-slider-handle:focus {
|
||||
border-color: ${theme.palette.blue77};
|
||||
box-shadow: none;
|
||||
}
|
||||
.rc-slider-handle:active {
|
||||
border-color: ${theme.palette.blue77};
|
||||
box-shadow: none;
|
||||
}
|
||||
.rc-slider-handle-click-focused:focus {
|
||||
border-color: ${theme.palette.blue77};
|
||||
}
|
||||
.rc-slider-dot-active {
|
||||
border-color: ${theme.palette.blue77};
|
||||
}
|
||||
.rc-slider-track {
|
||||
background-color: ${theme.palette.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;
|
||||
z-index: ${theme.zIndex.tooltip};
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
`,
|
||||
sliderInput: css`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
`,
|
||||
sliderInputVertical: css`
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
|
||||
.rc-slider {
|
||||
margin: 0;
|
||||
order: 2;
|
||||
}
|
||||
`,
|
||||
sliderInputField: css`
|
||||
display: flex;
|
||||
flex-grow: 0;
|
||||
flex-basis: 50px;
|
||||
margin-left: ${theme.spacing.lg};
|
||||
height: ${theme.spacing.formInputHeight}px;
|
||||
text-align: center;
|
||||
border-radius: ${theme.border.radius.sm};
|
||||
border: 1px solid ${theme.colors.formInputBorder};
|
||||
${getFocusStyle(theme)};
|
||||
`,
|
||||
sliderInputFieldVertical: css`
|
||||
margin: 0 0 ${theme.spacing.lg} 0;
|
||||
order: 1;
|
||||
`,
|
||||
};
|
||||
});
|
29
packages/grafana-ui/src/components/Slider/types.ts
Normal file
29
packages/grafana-ui/src/components/Slider/types.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { Orientation } from '../../types/orientation';
|
||||
|
||||
export interface SliderProps {
|
||||
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;
|
||||
step?: number;
|
||||
tooltipAlwaysVisible?: boolean;
|
||||
formatTooltipResult?: (value: number) => number;
|
||||
onChange?: (value: number) => void;
|
||||
onAfterChange?: (value?: number) => void;
|
||||
}
|
||||
|
||||
export interface RangeSliderProps {
|
||||
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;
|
||||
step?: number;
|
||||
tooltipAlwaysVisible?: boolean;
|
||||
formatTooltipResult?: (value: number) => number | string;
|
||||
onChange?: (value: number[]) => void;
|
||||
onAfterChange?: (value: number[]) => void;
|
||||
}
|
@ -126,11 +126,13 @@ export { default as Chart } from './Chart';
|
||||
export { TooltipContainer } from './Chart/TooltipContainer';
|
||||
export { Drawer } from './Drawer/Drawer';
|
||||
export { Slider } from './Slider/Slider';
|
||||
export { RangeSlider } from './Slider/RangeSlider';
|
||||
|
||||
// TODO: namespace!!
|
||||
export { StringValueEditor } from './OptionsUI/string';
|
||||
export { StringArrayEditor } from './OptionsUI/strings';
|
||||
export { NumberValueEditor } from './OptionsUI/number';
|
||||
export { SliderValueEditor } from './OptionsUI/slider';
|
||||
export { SelectValueEditor } from './OptionsUI/select';
|
||||
export { FieldConfigItemHeaderTitle } from './FieldConfigs/FieldConfigItemHeaderTitle';
|
||||
|
||||
|
@ -25,6 +25,7 @@ import {
|
||||
import { Switch } from '../components/Switch/Switch';
|
||||
import {
|
||||
NumberValueEditor,
|
||||
SliderValueEditor,
|
||||
RadioButtonGroup,
|
||||
StringValueEditor,
|
||||
StringArrayEditor,
|
||||
@ -229,6 +230,13 @@ export const getStandardOptionEditors = () => {
|
||||
editor: NumberValueEditor as any,
|
||||
};
|
||||
|
||||
const slider: StandardEditorsRegistryItem<number> = {
|
||||
id: 'slider',
|
||||
name: 'Slider',
|
||||
description: 'Allows numeric values input',
|
||||
editor: SliderValueEditor as any,
|
||||
};
|
||||
|
||||
const text: StandardEditorsRegistryItem<string> = {
|
||||
id: 'text',
|
||||
name: 'Text',
|
||||
@ -323,6 +331,7 @@ export const getStandardOptionEditors = () => {
|
||||
return [
|
||||
text,
|
||||
number,
|
||||
slider,
|
||||
boolean,
|
||||
radio,
|
||||
select,
|
||||
|
@ -3,7 +3,7 @@ import { mount } from 'enzyme';
|
||||
import { ExploreId } from '../../../types/explore';
|
||||
import { SortOrder } from 'app/core/utils/richHistory';
|
||||
import { RichHistoryQueriesTab, Props } from './RichHistoryQueriesTab';
|
||||
import { Slider } from '@grafana/ui';
|
||||
import { RangeSlider } from '@grafana/ui';
|
||||
|
||||
jest.mock('../state/selectors', () => ({ getExploreDatasources: jest.fn() }));
|
||||
|
||||
@ -30,7 +30,7 @@ describe('RichHistoryQueriesTab', () => {
|
||||
describe('slider', () => {
|
||||
it('should render slider', () => {
|
||||
const wrapper = setup();
|
||||
expect(wrapper.find(Slider)).toHaveLength(1);
|
||||
expect(wrapper.find(RangeSlider)).toHaveLength(1);
|
||||
});
|
||||
it('should render slider with correct timerange', () => {
|
||||
const wrapper = setup();
|
||||
|
@ -20,7 +20,7 @@ import {
|
||||
// Components
|
||||
import RichHistoryCard from './RichHistoryCard';
|
||||
import { sortOrderOptions } from './RichHistory';
|
||||
import { Slider, Select } from '@grafana/ui';
|
||||
import { RangeSlider, Select } from '@grafana/ui';
|
||||
import { FilterInput } from 'app/core/components/FilterInput/FilterInput';
|
||||
|
||||
export interface Props {
|
||||
@ -186,7 +186,7 @@ export function RichHistoryQueriesTab(props: Props) {
|
||||
<div className="label-slider">Filter history</div>
|
||||
<div className="label-slider">{mapNumbertoTimeInSlider(timeFilter[0])}</div>
|
||||
<div className="slider">
|
||||
<Slider
|
||||
<RangeSlider
|
||||
tooltipAlwaysVisible={false}
|
||||
min={0}
|
||||
max={retentionPeriod}
|
||||
|
@ -24,23 +24,14 @@ export const plugin = new PanelPlugin<Options, GraphCustomFieldConfig>(GraphPane
|
||||
description: '',
|
||||
defaultValue: true,
|
||||
})
|
||||
.addSelect({
|
||||
.addSliderInput({
|
||||
path: 'line.width',
|
||||
name: 'Line width',
|
||||
defaultValue: 1,
|
||||
settings: {
|
||||
options: [
|
||||
{ value: 1, label: '1 • thin' },
|
||||
{ value: 2, label: '2' },
|
||||
{ value: 3, label: '3' },
|
||||
{ value: 4, label: '4' },
|
||||
{ value: 5, label: '5' },
|
||||
{ value: 6, label: '6' },
|
||||
{ value: 7, label: '7' },
|
||||
{ value: 8, label: '8' },
|
||||
{ value: 9, label: '9' },
|
||||
{ value: 10, label: '10 • thick' },
|
||||
],
|
||||
min: 1,
|
||||
max: 10,
|
||||
step: 1,
|
||||
},
|
||||
showIf: c => {
|
||||
return c.line.show;
|
||||
@ -52,23 +43,14 @@ export const plugin = new PanelPlugin<Options, GraphCustomFieldConfig>(GraphPane
|
||||
description: '',
|
||||
defaultValue: false,
|
||||
})
|
||||
.addSelect({
|
||||
.addSliderInput({
|
||||
path: 'points.radius',
|
||||
name: 'Point radius',
|
||||
defaultValue: 4,
|
||||
settings: {
|
||||
options: [
|
||||
{ value: 1, label: '1 • thin' },
|
||||
{ value: 2, label: '2' },
|
||||
{ value: 3, label: '3' },
|
||||
{ value: 4, label: '4' },
|
||||
{ value: 5, label: '5' },
|
||||
{ value: 6, label: '6' },
|
||||
{ value: 7, label: '7' },
|
||||
{ value: 8, label: '8' },
|
||||
{ value: 9, label: '9' },
|
||||
{ value: 10, label: '10 • thick' },
|
||||
],
|
||||
min: 1,
|
||||
max: 10,
|
||||
step: 1,
|
||||
},
|
||||
showIf: c => c.points.show,
|
||||
})
|
||||
@ -78,24 +60,14 @@ export const plugin = new PanelPlugin<Options, GraphCustomFieldConfig>(GraphPane
|
||||
description: '',
|
||||
defaultValue: false,
|
||||
})
|
||||
.addSelect({
|
||||
.addSliderInput({
|
||||
path: 'fill.alpha',
|
||||
name: 'Fill area opacity',
|
||||
defaultValue: 0.1,
|
||||
settings: {
|
||||
options: [
|
||||
{ value: 0, label: 'No Fill' },
|
||||
{ value: 0.1, label: '10% • transparent' },
|
||||
{ value: 0.2, label: '20%' },
|
||||
{ value: 0.3, label: '30%' },
|
||||
{ value: 0.4, label: '40% ' },
|
||||
{ value: 0.5, label: '50%' },
|
||||
{ value: 0.6, label: '60%' },
|
||||
{ value: 0.7, label: '70%' },
|
||||
{ value: 0.8, label: '80%' },
|
||||
{ value: 0.9, label: '90%' },
|
||||
{ value: 1, label: '100% • opaque' },
|
||||
],
|
||||
min: 0,
|
||||
max: 1,
|
||||
step: 0.1,
|
||||
},
|
||||
})
|
||||
.addTextInput({
|
||||
|
Loading…
Reference in New Issue
Block a user