mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
TimeSeries: Render thresholds as lines and areas (#33357)
* TimeSeries: First take on uplot thresholds rendering * Updating theme for color picker and fixing issues * Updating theme for color picker and fixing issues * Minor progress * Added test dashboard * Adding transparent and text colors to color picker and making them visible * Good progress on migrations * Fixed selected issue in color picker * Fixes * migration fix * Fixed test * Fixing line rendering and refactoring * Minor ordering fix * fixed test * Draw thresholds before axes & series and other tweaks * Update packages/grafana-ui/src/components/uPlot/config/UPlotThresholds.ts Co-authored-by: Dominik Prokop <dominik.prokop@grafana.com> Co-authored-by: Dominik Prokop <dominik.prokop@grafana.com>
This commit is contained in:
parent
06dc2b24bf
commit
b62e87f753
1248
devenv/dev-dashboards/panel-graph/graph-ng-thresholds.json
Normal file
1248
devenv/dev-dashboards/panel-graph/graph-ng-thresholds.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -35,7 +35,9 @@ export type Color =
|
||||
| 'semi-dark-purple'
|
||||
| 'light-purple'
|
||||
| 'super-light-purple'
|
||||
| 'panel-bg';
|
||||
| 'panel-bg'
|
||||
| 'transparent'
|
||||
| 'text';
|
||||
|
||||
type ThemeVariants = {
|
||||
dark: string;
|
||||
@ -84,6 +86,8 @@ export function buildColorsMapForTheme(theme: GrafanaTheme): Record<Color, strin
|
||||
}
|
||||
|
||||
colorsMap['panel-bg'] = theme.colors.panelBg;
|
||||
colorsMap['transparent'] = 'rgba(0,0,0,0)';
|
||||
colorsMap['text'] = theme.colors.text;
|
||||
|
||||
return colorsMap;
|
||||
}
|
||||
|
@ -1,12 +1,12 @@
|
||||
import React from 'react';
|
||||
import renderer from 'react-test-renderer';
|
||||
import { ColorPicker } from './ColorPicker';
|
||||
import { ColorPickerTrigger } from './ColorPickerTrigger';
|
||||
import { ColorSwatch } from './ColorSwatch';
|
||||
|
||||
describe('ColorPicker', () => {
|
||||
it('renders ColorPickerTrigger component by default', () => {
|
||||
expect(
|
||||
renderer.create(<ColorPicker color="#EAB839" onChange={() => {}} />).root.findByType(ColorPickerTrigger)
|
||||
renderer.create(<ColorPicker color="#EAB839" onChange={() => {}} />).root.findByType(ColorSwatch)
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
|
@ -2,12 +2,12 @@ import React, { Component, createRef } from 'react';
|
||||
import { PopoverController } from '../Tooltip/PopoverController';
|
||||
import { Popover } from '../Tooltip/Popover';
|
||||
import { ColorPickerPopover, ColorPickerProps, ColorPickerChangeHandler } from './ColorPickerPopover';
|
||||
import { getColorForTheme, GrafanaTheme } from '@grafana/data';
|
||||
import { getColorForTheme, GrafanaTheme2 } from '@grafana/data';
|
||||
import { SeriesColorPickerPopover } from './SeriesColorPickerPopover';
|
||||
|
||||
import { css } from '@emotion/css';
|
||||
import { withTheme, stylesFactory } from '../../themes';
|
||||
import { ColorPickerTrigger } from './ColorPickerTrigger';
|
||||
import { withTheme2, stylesFactory } from '../../themes';
|
||||
import { ColorSwatch } from './ColorSwatch';
|
||||
|
||||
/**
|
||||
* If you need custom trigger for the color picker you can do that with a render prop pattern and supply a function
|
||||
@ -71,11 +71,11 @@ export const colorPickerFactory = <T extends ColorPickerProps>(
|
||||
hideColorPicker: hidePopper,
|
||||
})
|
||||
) : (
|
||||
<ColorPickerTrigger
|
||||
<ColorSwatch
|
||||
ref={this.pickerTriggerRef}
|
||||
onClick={showPopper}
|
||||
onMouseLeave={hidePopper}
|
||||
color={getColorForTheme(this.props.color || '#000000', theme)}
|
||||
color={getColorForTheme(this.props.color || '#000000', theme.v1)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
@ -87,15 +87,15 @@ export const colorPickerFactory = <T extends ColorPickerProps>(
|
||||
};
|
||||
};
|
||||
|
||||
export const ColorPicker = withTheme(colorPickerFactory(ColorPickerPopover, 'ColorPicker'));
|
||||
export const SeriesColorPicker = withTheme(colorPickerFactory(SeriesColorPickerPopover, 'SeriesColorPicker'));
|
||||
export const ColorPicker = withTheme2(colorPickerFactory(ColorPickerPopover, 'ColorPicker'));
|
||||
export const SeriesColorPicker = withTheme2(colorPickerFactory(SeriesColorPickerPopover, 'SeriesColorPicker'));
|
||||
|
||||
const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
const getStyles = stylesFactory((theme: GrafanaTheme2) => {
|
||||
return {
|
||||
colorPicker: css`
|
||||
position: absolute;
|
||||
z-index: ${theme.zIndex.tooltip};
|
||||
color: ${theme.colors.text};
|
||||
color: ${theme.colors.text.primary};
|
||||
max-width: 400px;
|
||||
font-size: ${theme.typography.size.sm};
|
||||
// !important because these styles are also provided to popper via .popper classes from Tooltip component
|
||||
|
@ -1,9 +1,9 @@
|
||||
import React from 'react';
|
||||
import { mount, ReactWrapper } from 'enzyme';
|
||||
import { ColorPickerPopover } from './ColorPickerPopover';
|
||||
import { ColorSwatch } from './NamedColorsGroup';
|
||||
import { flatten } from 'lodash';
|
||||
import { getNamedColorPalette, getColorFromHexRgbOrName } from '@grafana/data';
|
||||
import { ColorSwatch } from './ColorSwatch';
|
||||
|
||||
const allColors = flatten(Array.from(getNamedColorPalette().values()));
|
||||
|
||||
@ -15,7 +15,7 @@ describe('ColorPickerPopover', () => {
|
||||
const notSelectedSwatches = wrapper.find(ColorSwatch).filterWhere((node) => node.prop('isSelected') === false);
|
||||
|
||||
expect(selectedSwatch.length).toBe(1);
|
||||
expect(notSelectedSwatches.length).toBe(allColors.length - 1);
|
||||
expect(notSelectedSwatches.length).toBe(allColors.length + 1);
|
||||
expect(selectedSwatch.prop('isSelected')).toBe(true);
|
||||
});
|
||||
|
||||
@ -26,7 +26,7 @@ describe('ColorPickerPopover', () => {
|
||||
const notSelectedSwatches = wrapper.find(ColorSwatch).filterWhere((node) => node.prop('isSelected') === false);
|
||||
|
||||
expect(selectedSwatch.length).toBe(1);
|
||||
expect(notSelectedSwatches.length).toBe(allColors.length - 1);
|
||||
expect(notSelectedSwatches.length).toBe(allColors.length + 1);
|
||||
expect(selectedSwatch.prop('isSelected')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
@ -2,15 +2,15 @@ import React from 'react';
|
||||
import { NamedColorsPalette } from './NamedColorsPalette';
|
||||
import { PopoverContentProps } from '../Tooltip/Tooltip';
|
||||
import SpectrumPalette from './SpectrumPalette';
|
||||
import { Themeable } from '../../types/theme';
|
||||
import { Themeable2 } from '../../types/theme';
|
||||
import { warnAboutColorPickerPropsDeprecation } from './warnAboutColorPickerPropsDeprecation';
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { GrafanaTheme, GrafanaThemeType, getColorForTheme } from '@grafana/data';
|
||||
import { stylesFactory, withTheme } from '../../themes';
|
||||
import { css } from '@emotion/css';
|
||||
import { GrafanaTheme2, getColorForTheme } from '@grafana/data';
|
||||
import { stylesFactory, withTheme2 } from '../../themes';
|
||||
|
||||
export type ColorPickerChangeHandler = (color: string) => void;
|
||||
|
||||
export interface ColorPickerProps extends Themeable {
|
||||
export interface ColorPickerProps extends Themeable2 {
|
||||
color: string;
|
||||
onChange: ColorPickerChangeHandler;
|
||||
|
||||
@ -59,7 +59,7 @@ class UnThemedColorPickerPopover<T extends CustomPickersDescriptor> extends Reac
|
||||
if (enableNamedColors) {
|
||||
return changeHandler(color);
|
||||
}
|
||||
changeHandler(getColorForTheme(color, theme));
|
||||
changeHandler(getColorForTheme(color, theme.v1));
|
||||
};
|
||||
|
||||
onTabChange = (tab: PickerType | keyof T) => {
|
||||
@ -68,13 +68,13 @@ class UnThemedColorPickerPopover<T extends CustomPickersDescriptor> extends Reac
|
||||
|
||||
renderPicker = () => {
|
||||
const { activePicker } = this.state;
|
||||
const { color, theme } = this.props;
|
||||
const { color } = this.props;
|
||||
|
||||
switch (activePicker) {
|
||||
case 'spectrum':
|
||||
return <SpectrumPalette color={color} onChange={this.handleChange} />;
|
||||
case 'palette':
|
||||
return <NamedColorsPalette color={color} onChange={this.handleChange} theme={theme} />;
|
||||
return <NamedColorsPalette color={color} onChange={this.handleChange} />;
|
||||
default:
|
||||
return this.renderCustomPicker(activePicker);
|
||||
}
|
||||
@ -117,12 +117,7 @@ class UnThemedColorPickerPopover<T extends CustomPickersDescriptor> extends Reac
|
||||
const { theme } = this.props;
|
||||
const styles = getStyles(theme);
|
||||
return (
|
||||
<div
|
||||
className={cx(
|
||||
styles.colorPickerPopover,
|
||||
theme.type === GrafanaThemeType.Light ? styles.colorPickerPopoverLight : styles.colorPickerPopoverDark
|
||||
)}
|
||||
>
|
||||
<div className={styles.colorPickerPopover}>
|
||||
<div className={styles.colorPickerPopoverTabs}>
|
||||
<div className={this.getTabClassName('palette')} onClick={this.onTabChange('palette')}>
|
||||
Colors
|
||||
@ -132,62 +127,50 @@ class UnThemedColorPickerPopover<T extends CustomPickersDescriptor> extends Reac
|
||||
</div>
|
||||
{this.renderCustomPickerTabs()}
|
||||
</div>
|
||||
|
||||
<div className={styles.colorPickerPopoverContent}>{this.renderPicker()}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const ColorPickerPopover = withTheme(UnThemedColorPickerPopover);
|
||||
export const ColorPickerPopover = withTheme2(UnThemedColorPickerPopover);
|
||||
ColorPickerPopover.displayName = 'ColorPickerPopover';
|
||||
|
||||
const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
const getStyles = stylesFactory((theme: GrafanaTheme2) => {
|
||||
return {
|
||||
colorPickerPopover: css`
|
||||
border-radius: ${theme.border.radius.md};
|
||||
`,
|
||||
colorPickerPopoverLight: css`
|
||||
color: ${theme.palette.black};
|
||||
background: linear-gradient(180deg, ${theme.palette.white} 0%, #f7f8fa 104.25%);
|
||||
box-shadow: 0px 2px 4px #dde4ed, 0px 0px 2px #dde4ed;
|
||||
border-radius: ${theme.shape.borderRadius()};
|
||||
box-shadow: ${theme.shadows.z3};
|
||||
background: ${theme.colors.background.primary};
|
||||
|
||||
.ColorPickerPopover__tab {
|
||||
width: 50%;
|
||||
text-align: center;
|
||||
padding: ${theme.spacing.sm} 0;
|
||||
background: #dde4ed;
|
||||
}
|
||||
.ColorPickerPopover__tab--active {
|
||||
background: ${theme.palette.white};
|
||||
}
|
||||
`,
|
||||
colorPickerPopoverDark: css`
|
||||
color: #d8d9da;
|
||||
background: linear-gradient(180deg, #1e2028 0%, #161719 104.25%);
|
||||
box-shadow: 0px 2px 4px ${theme.palette.black}, 0px 0px 2px ${theme.palette.black};
|
||||
|
||||
.ColorPickerPopover__tab {
|
||||
width: 50%;
|
||||
text-align: center;
|
||||
padding: ${theme.spacing.sm} 0;
|
||||
background: #303133;
|
||||
color: ${theme.palette.white};
|
||||
padding: ${theme.spacing(1, 0)};
|
||||
background: ${theme.colors.background.secondary};
|
||||
color: ${theme.colors.text.secondary};
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ColorPickerPopover__tab--active {
|
||||
background: none;
|
||||
color: ${theme.colors.text.primary};
|
||||
font-weight: ${theme.typography.fontWeightMedium};
|
||||
background: ${theme.colors.background.primary};
|
||||
}
|
||||
`,
|
||||
colorPickerPopoverContent: css`
|
||||
width: 336px;
|
||||
font-size: ${theme.typography.bodySmall.fontSize};
|
||||
min-height: 184px;
|
||||
padding: ${theme.spacing.lg};
|
||||
padding: ${theme.spacing(2)};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`,
|
||||
colorPickerPopoverTabs: css`
|
||||
display: flex;
|
||||
width: 100%;
|
||||
border-radius: ${theme.border.radius.md} ${theme.border.radius.md} 0 0;
|
||||
border-radius: ${theme.shape.borderRadius()} ${theme.shape.borderRadius()} 0 0;
|
||||
overflow: hidden;
|
||||
`,
|
||||
};
|
||||
|
@ -1,56 +0,0 @@
|
||||
import React, { forwardRef } from 'react';
|
||||
|
||||
interface ColorPickerTriggerProps {
|
||||
onClick: () => void;
|
||||
onMouseLeave: () => void;
|
||||
color: string;
|
||||
}
|
||||
|
||||
export const ColorPickerTrigger = forwardRef(function ColorPickerTrigger(
|
||||
props: ColorPickerTriggerProps,
|
||||
ref: React.Ref<HTMLDivElement>
|
||||
) {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
onClick={props.onClick}
|
||||
onMouseLeave={props.onMouseLeave}
|
||||
style={{
|
||||
overflow: 'hidden',
|
||||
background: 'inherit',
|
||||
border: 'none',
|
||||
color: 'inherit',
|
||||
padding: 0,
|
||||
borderRadius: 10,
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: 'relative',
|
||||
width: 15,
|
||||
height: 15,
|
||||
border: 'none',
|
||||
margin: 0,
|
||||
float: 'left',
|
||||
zIndex: 0,
|
||||
backgroundImage:
|
||||
// eslint-disable-next-line max-len
|
||||
'url(data:image/png,base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg==)',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: props.color,
|
||||
display: 'block',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
@ -0,0 +1,60 @@
|
||||
import React, { CSSProperties } from 'react';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import { useTheme2 } from '../../themes/ThemeContext';
|
||||
|
||||
/** @internal */
|
||||
export enum ColorSwatchVariant {
|
||||
Small = 'small',
|
||||
Large = 'large',
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export interface Props extends React.DOMAttributes<HTMLDivElement> {
|
||||
color: string;
|
||||
label?: string;
|
||||
variant?: ColorSwatchVariant;
|
||||
isSelected?: boolean;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export const ColorSwatch = React.forwardRef<HTMLDivElement, Props>(
|
||||
({ color, label, variant = ColorSwatchVariant.Small, isSelected, ...otherProps }, ref) => {
|
||||
const theme = useTheme2();
|
||||
const tc = tinycolor(color);
|
||||
const isSmall = variant === ColorSwatchVariant.Small;
|
||||
const hasLabel = !!label;
|
||||
const swatchSize = isSmall ? '16px' : '32px';
|
||||
|
||||
const swatchStyles: CSSProperties = {
|
||||
width: swatchSize,
|
||||
height: swatchSize,
|
||||
borderRadius: '50%',
|
||||
background: `${color}`,
|
||||
marginRight: hasLabel ? '8px' : '0px',
|
||||
boxShadow: isSelected
|
||||
? `inset 0 0 0 2px ${color}, inset 0 0 0 4px ${theme.colors.getContrastText(color)}`
|
||||
: 'none',
|
||||
};
|
||||
|
||||
if (tc.getAlpha() < 0.1) {
|
||||
swatchStyles.border = `2px solid ${theme.colors.border.medium}`;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
{...otherProps}
|
||||
>
|
||||
<div style={swatchStyles} />
|
||||
{hasLabel && <span>{label}</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
ColorSwatch.displayName = 'ColorSwatch';
|
@ -1,59 +1,13 @@
|
||||
import React, { FunctionComponent } from 'react';
|
||||
import { Themeable } from '../../types';
|
||||
import { ColorDefinition } from '@grafana/data';
|
||||
import { Color } from 'csstype';
|
||||
import { upperFirst, find } from 'lodash';
|
||||
import { ColorSwatch, ColorSwatchVariant } from './ColorSwatch';
|
||||
import { useTheme2 } from '../../themes/ThemeContext';
|
||||
|
||||
type ColorChangeHandler = (color: ColorDefinition) => void;
|
||||
|
||||
export enum ColorSwatchVariant {
|
||||
Small = 'small',
|
||||
Large = 'large',
|
||||
}
|
||||
|
||||
interface ColorSwatchProps extends Themeable, React.DOMAttributes<HTMLDivElement> {
|
||||
color: string;
|
||||
label?: string;
|
||||
variant?: ColorSwatchVariant;
|
||||
isSelected?: boolean;
|
||||
}
|
||||
|
||||
export const ColorSwatch: FunctionComponent<ColorSwatchProps> = ({
|
||||
color,
|
||||
label,
|
||||
variant = ColorSwatchVariant.Small,
|
||||
isSelected,
|
||||
theme,
|
||||
...otherProps
|
||||
}) => {
|
||||
const isSmall = variant === ColorSwatchVariant.Small;
|
||||
const swatchSize = isSmall ? '16px' : '32px';
|
||||
|
||||
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 ${theme.colors.bg1}` : 'none',
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
{...otherProps}
|
||||
>
|
||||
<div style={swatchStyles} />
|
||||
{variant === ColorSwatchVariant.Large && <span>{label}</span>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface NamedColorsGroupProps extends Themeable {
|
||||
interface NamedColorsGroupProps {
|
||||
colors: ColorDefinition[];
|
||||
selectedColor?: Color;
|
||||
onColorSelect: ColorChangeHandler;
|
||||
@ -64,9 +18,9 @@ const NamedColorsGroup: FunctionComponent<NamedColorsGroupProps> = ({
|
||||
colors,
|
||||
selectedColor,
|
||||
onColorSelect,
|
||||
theme,
|
||||
...otherProps
|
||||
}) => {
|
||||
const theme = useTheme2();
|
||||
const primaryColor = find(colors, (color) => !!color.isPrimary);
|
||||
|
||||
return (
|
||||
@ -76,10 +30,9 @@ const NamedColorsGroup: FunctionComponent<NamedColorsGroupProps> = ({
|
||||
key={primaryColor.name}
|
||||
isSelected={primaryColor.name === selectedColor}
|
||||
variant={ColorSwatchVariant.Large}
|
||||
color={primaryColor.variants[theme.type]}
|
||||
color={primaryColor.variants[theme.colors.mode]}
|
||||
label={upperFirst(primaryColor.hue)}
|
||||
onClick={() => onColorSelect(primaryColor)}
|
||||
theme={theme}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
@ -95,9 +48,8 @@ const NamedColorsGroup: FunctionComponent<NamedColorsGroupProps> = ({
|
||||
<ColorSwatch
|
||||
key={color.name}
|
||||
isSelected={color.name === selectedColor}
|
||||
color={color.variants[theme.type]}
|
||||
color={color.variants[theme.colors.mode]}
|
||||
onClick={() => onColorSelect(color)}
|
||||
theme={theme}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
@ -1,9 +1,7 @@
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { NamedColorsPalette, NamedColorsPaletteProps } from './NamedColorsPalette';
|
||||
import { Meta, Story } from '@storybook/react';
|
||||
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
|
||||
import { renderComponentWithTheme } from '../../utils/storybook/withTheme';
|
||||
import { UseState } from '../../utils/storybook/UseState';
|
||||
import mdx from './ColorPicker.mdx';
|
||||
|
||||
export default {
|
||||
@ -31,17 +29,10 @@ interface StoryProps extends Partial<NamedColorsPaletteProps> {
|
||||
}
|
||||
|
||||
export const NamedColors: Story<StoryProps> = ({ selectedColor }) => {
|
||||
return (
|
||||
<UseState initialState={selectedColor}>
|
||||
{(selectedColor, updateSelectedColor) => {
|
||||
return renderComponentWithTheme(NamedColorsPalette, {
|
||||
color: selectedColor,
|
||||
onChange: updateSelectedColor,
|
||||
});
|
||||
}}
|
||||
</UseState>
|
||||
);
|
||||
const [color, setColor] = useState('green');
|
||||
return <NamedColorsPalette color={color} onChange={setColor} />;
|
||||
};
|
||||
|
||||
NamedColors.args = {
|
||||
selectedColor: 'red',
|
||||
color: 'green',
|
||||
};
|
||||
|
@ -1,9 +1,9 @@
|
||||
import React from 'react';
|
||||
import { mount, ReactWrapper } from 'enzyme';
|
||||
import { NamedColorsPalette } from './NamedColorsPalette';
|
||||
import { ColorSwatch } from './NamedColorsGroup';
|
||||
import { getColorDefinitionByName, GrafanaThemeType } from '@grafana/data';
|
||||
import { getTheme } from '../../themes';
|
||||
import { createTheme, getColorDefinitionByName } from '@grafana/data';
|
||||
import { ColorSwatch } from './ColorSwatch';
|
||||
import { ThemeContext } from '../../themes';
|
||||
|
||||
describe('NamedColorsPalette', () => {
|
||||
const BasicGreen = getColorDefinitionByName('green');
|
||||
@ -16,22 +16,21 @@ describe('NamedColorsPalette', () => {
|
||||
});
|
||||
|
||||
it('should render provided color variant specific for theme', () => {
|
||||
wrapper = mount(<NamedColorsPalette color={BasicGreen.name} theme={getTheme()} onChange={() => {}} />);
|
||||
wrapper = mount(<NamedColorsPalette color={BasicGreen.name} 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={getTheme(GrafanaThemeType.Light)} onChange={() => {}} />
|
||||
|
||||
const withLightTheme = (
|
||||
<ThemeContext.Provider value={createTheme({ colors: { mode: 'light' } })}>
|
||||
<NamedColorsPalette color={BasicGreen.name} onChange={() => {}} />
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
|
||||
wrapper = mount(withLightTheme);
|
||||
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={() => {}} theme={getTheme()} />);
|
||||
selectedSwatch = wrapper.find(ColorSwatch).findWhere((node) => node.key() === BasicGreen.name);
|
||||
expect(selectedSwatch.prop('color')).toBe(BasicGreen.variants.dark);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,20 +1,23 @@
|
||||
import React from 'react';
|
||||
import { getNamedColorPalette } from '@grafana/data';
|
||||
import { Themeable } from '../../types/index';
|
||||
import NamedColorsGroup from './NamedColorsGroup';
|
||||
import { VerticalGroup } from '../Layout/Layout';
|
||||
import { ColorSwatch } from './ColorSwatch';
|
||||
import { useTheme2 } from '../../themes/ThemeContext';
|
||||
|
||||
export interface NamedColorsPaletteProps extends Themeable {
|
||||
export interface NamedColorsPaletteProps {
|
||||
color?: string;
|
||||
onChange: (colorName: string) => void;
|
||||
}
|
||||
|
||||
export const NamedColorsPalette = ({ color, onChange, theme }: NamedColorsPaletteProps) => {
|
||||
export const NamedColorsPalette = ({ color, onChange }: NamedColorsPaletteProps) => {
|
||||
const theme = useTheme2();
|
||||
|
||||
const swatches: JSX.Element[] = [];
|
||||
getNamedColorPalette().forEach((colors, hue) => {
|
||||
swatches.push(
|
||||
<NamedColorsGroup
|
||||
key={hue}
|
||||
theme={theme}
|
||||
selectedColor={color}
|
||||
colors={colors}
|
||||
onColorSelect={(color) => {
|
||||
@ -25,15 +28,30 @@ export const NamedColorsPalette = ({ color, onChange, theme }: NamedColorsPalett
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(3, 1fr)',
|
||||
gridRowGap: '24px',
|
||||
gridColumnGap: '24px',
|
||||
}}
|
||||
>
|
||||
{swatches}
|
||||
</div>
|
||||
<VerticalGroup spacing="md">
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(3, 1fr)',
|
||||
gridRowGap: theme.spacing(2),
|
||||
gridColumnGap: theme.spacing(2),
|
||||
flexGrow: 1,
|
||||
}}
|
||||
>
|
||||
{swatches}
|
||||
<ColorSwatch
|
||||
isSelected={color === 'transparent'}
|
||||
color={'rgba(0,0,0,0)'}
|
||||
label="Transparent"
|
||||
onClick={() => onChange('transparent')}
|
||||
/>
|
||||
<ColorSwatch
|
||||
isSelected={color === 'text'}
|
||||
color={theme.colors.text.primary}
|
||||
label="Text color"
|
||||
onClick={() => onChange('text')}
|
||||
/>
|
||||
</div>
|
||||
</VerticalGroup>
|
||||
);
|
||||
};
|
||||
|
@ -4,7 +4,7 @@ import { ColorPickerPopover, ColorPickerProps } from './ColorPickerPopover';
|
||||
import { PopoverContentProps } from '../Tooltip/Tooltip';
|
||||
import { Switch } from '../Forms/Legacy/Switch/Switch';
|
||||
import { css } from '@emotion/css';
|
||||
import { withTheme, useStyles } from '../../themes';
|
||||
import { withTheme2, useStyles } from '../../themes';
|
||||
import { Button } from '../Button';
|
||||
|
||||
export interface SeriesColorPickerPopoverProps extends ColorPickerProps, PopoverContentProps {
|
||||
@ -89,7 +89,7 @@ export class AxisSelector extends React.PureComponent<AxisSelectorProps, AxisSel
|
||||
}
|
||||
|
||||
// This component is to enable SeriesColorPickerPopover usage via series-color-picker-popover directive
|
||||
export const SeriesColorPickerPopoverWithTheme = withTheme(SeriesColorPickerPopover);
|
||||
export const SeriesColorPickerPopoverWithTheme = withTheme2(SeriesColorPickerPopover);
|
||||
|
||||
const getStyles = () => {
|
||||
return {
|
||||
|
@ -5,7 +5,7 @@ import tinycolor from 'tinycolor2';
|
||||
import ColorInput from './ColorInput';
|
||||
import { GrafanaTheme, getColorForTheme } from '@grafana/data';
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { useStyles, useTheme } from '../../themes';
|
||||
import { useStyles, useTheme2 } from '../../themes';
|
||||
import { useThrottleFn } from 'react-use';
|
||||
|
||||
export interface SpectrumPaletteProps {
|
||||
@ -17,25 +17,27 @@ const SpectrumPalette: React.FunctionComponent<SpectrumPaletteProps> = ({ color,
|
||||
const [currentColor, setColor] = useState(color);
|
||||
useThrottleFn(onChange, 500, [currentColor]);
|
||||
|
||||
const theme = useTheme();
|
||||
const theme = useTheme2();
|
||||
const styles = useStyles(getStyles);
|
||||
|
||||
const rgbaString = useMemo(() => {
|
||||
return currentColor.startsWith('rgba')
|
||||
? currentColor
|
||||
: tinycolor(getColorForTheme(currentColor, theme)).toRgbString();
|
||||
: tinycolor(getColorForTheme(currentColor, theme.v1)).toRgbString();
|
||||
}, [currentColor, theme]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.wrapper}>
|
||||
<RgbaStringColorPicker className={cx(styles.root)} color={rgbaString} onChange={setColor} />
|
||||
|
||||
<ColorInput theme={theme} color={currentColor} onChange={setColor} className={styles.colorInput} />
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = (theme: GrafanaTheme) => ({
|
||||
wrapper: css`
|
||||
flex-grow: 1;
|
||||
`,
|
||||
root: css`
|
||||
&.react-colorful {
|
||||
width: auto;
|
||||
|
@ -22,6 +22,7 @@ import {
|
||||
AxisPlacement,
|
||||
DrawStyle,
|
||||
GraphFieldConfig,
|
||||
GraphTresholdsStyleMode,
|
||||
PointVisibility,
|
||||
ScaleDirection,
|
||||
ScaleOrientation,
|
||||
@ -224,6 +225,19 @@ export function preparePlotConfigBuilder(
|
||||
hideInLegend: customConfig.hideFrom?.legend,
|
||||
});
|
||||
|
||||
// Render thresholds in graph
|
||||
if (customConfig.thresholdsStyle && config.thresholds) {
|
||||
const thresholdDisplay = customConfig.thresholdsStyle.mode ?? GraphTresholdsStyleMode.Off;
|
||||
if (thresholdDisplay !== GraphTresholdsStyleMode.Off) {
|
||||
builder.addThresholds({
|
||||
config: customConfig.thresholdsStyle,
|
||||
thresholds: config.thresholds,
|
||||
scaleKey,
|
||||
theme,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
collectStackingGroups(field, stackingGroups, seriesIndex);
|
||||
}
|
||||
|
||||
|
@ -193,7 +193,7 @@ export const getInputStyles = stylesFactory(({ theme, invalid = false, width }:
|
||||
css`
|
||||
label: input-suffix;
|
||||
padding-left: ${theme.spacing(1)};
|
||||
padding-right: ${theme.spacing(0.5)};
|
||||
padding-right: ${theme.spacing(1)};
|
||||
margin-bottom: -2px;
|
||||
border-left: none;
|
||||
border-top-left-radius: 0;
|
||||
|
@ -3,7 +3,7 @@ import { getColorForTheme, GrafanaTheme } from '@grafana/data';
|
||||
import { ColorPicker } from '../ColorPicker/ColorPicker';
|
||||
import { stylesFactory, useTheme } from '../../themes';
|
||||
import { css } from '@emotion/css';
|
||||
import { ColorPickerTrigger } from '../ColorPicker/ColorPickerTrigger';
|
||||
import { ColorSwatch } from '../ColorPicker/ColorSwatch';
|
||||
|
||||
/**
|
||||
* @alpha
|
||||
@ -26,7 +26,7 @@ export const ColorValueEditor: React.FC<ColorValueEditorProps> = ({ value, onCha
|
||||
return (
|
||||
<div className={styles.spot} onBlur={hideColorPicker}>
|
||||
<div className={styles.colorPicker}>
|
||||
<ColorPickerTrigger
|
||||
<ColorSwatch
|
||||
ref={ref}
|
||||
onClick={showColorPicker}
|
||||
onMouseLeave={hideColorPicker}
|
||||
|
@ -16,7 +16,6 @@ import { stylesFactory } from '../../themes';
|
||||
import { Icon } from '../Icon/Icon';
|
||||
import { RadioButtonGroup } from '../Forms/RadioButtonGroup/RadioButtonGroup';
|
||||
import { Button } from '../Button';
|
||||
import { FullWidthButtonContainer } from '../Button/FullWidthButtonContainer';
|
||||
import { Label } from '../Forms/Label';
|
||||
import { isNumber } from 'lodash';
|
||||
|
||||
@ -232,9 +231,7 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
|
||||
|
||||
<div>
|
||||
<Label description="Percentage means thresholds relative to min & max">Thresholds mode</Label>
|
||||
<FullWidthButtonContainer>
|
||||
<RadioButtonGroup size="sm" options={modes} onChange={this.onModeChanged} value={thresholds.mode} />
|
||||
</FullWidthButtonContainer>
|
||||
<RadioButtonGroup options={modes} onChange={this.onModeChanged} value={thresholds.mode} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -192,6 +192,24 @@ export interface StackingConfig {
|
||||
group?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @alpha
|
||||
*/
|
||||
export enum GraphTresholdsStyleMode {
|
||||
Off = 'off',
|
||||
Line = 'line',
|
||||
Area = 'area',
|
||||
LineAndArea = 'line+area',
|
||||
Series = 'series',
|
||||
}
|
||||
|
||||
/**
|
||||
* @alpha
|
||||
*/
|
||||
export interface GraphThresholdsStyleConfig {
|
||||
mode: GraphTresholdsStyleMode;
|
||||
}
|
||||
|
||||
/**
|
||||
* @alpha
|
||||
*/
|
||||
@ -205,6 +223,7 @@ export interface GraphFieldConfig
|
||||
drawStyle?: DrawStyle;
|
||||
gradientMode?: GraphGradientMode;
|
||||
stacking?: StackingConfig;
|
||||
thresholdsStyle?: GraphThresholdsStyleConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -254,4 +273,11 @@ export const graphFieldOptions = {
|
||||
{ label: 'Off', value: StackingMode.None },
|
||||
{ label: 'Normal', value: StackingMode.Normal },
|
||||
] as Array<SelectableValue<StackingMode>>,
|
||||
|
||||
thresholdsDisplayModes: [
|
||||
{ label: 'Off', value: GraphTresholdsStyleMode.Off },
|
||||
{ label: 'As lines', value: GraphTresholdsStyleMode.Line },
|
||||
{ label: 'As filled regions', value: GraphTresholdsStyleMode.Area },
|
||||
{ label: 'As filled regions and lines', value: GraphTresholdsStyleMode.LineAndArea },
|
||||
] as Array<SelectableValue<GraphTresholdsStyleMode>>,
|
||||
};
|
||||
|
@ -9,8 +9,9 @@ import {
|
||||
ScaleDistribution,
|
||||
ScaleOrientation,
|
||||
ScaleDirection,
|
||||
GraphTresholdsStyleMode,
|
||||
} from '../config';
|
||||
import { createTheme } from '@grafana/data';
|
||||
import { createTheme, ThresholdsMode } from '@grafana/data';
|
||||
|
||||
describe('UPlotConfigBuilder', () => {
|
||||
const darkTheme = createTheme();
|
||||
@ -658,4 +659,37 @@ describe('UPlotConfigBuilder', () => {
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Thresholds', () => {
|
||||
it('Only adds one threshold per scale', () => {
|
||||
const builder = new UPlotConfigBuilder();
|
||||
const addHookFn = jest.fn();
|
||||
builder.addHook = addHookFn;
|
||||
|
||||
builder.addThresholds({
|
||||
scaleKey: 'A',
|
||||
thresholds: {
|
||||
mode: ThresholdsMode.Absolute,
|
||||
steps: [],
|
||||
},
|
||||
config: {
|
||||
mode: GraphTresholdsStyleMode.Area,
|
||||
},
|
||||
theme: darkTheme,
|
||||
});
|
||||
builder.addThresholds({
|
||||
scaleKey: 'A',
|
||||
thresholds: {
|
||||
mode: ThresholdsMode.Absolute,
|
||||
steps: [],
|
||||
},
|
||||
config: {
|
||||
mode: GraphTresholdsStyleMode.Area,
|
||||
},
|
||||
theme: darkTheme,
|
||||
});
|
||||
|
||||
expect(addHookFn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -7,6 +7,7 @@ import uPlot, { Cursor, Band, Hooks, BBox } from 'uplot';
|
||||
import { defaultsDeep } from 'lodash';
|
||||
import { DefaultTimeZone, getTimeZoneInfo } from '@grafana/data';
|
||||
import { pluginLog } from '../utils';
|
||||
import { getThresholdsDrawHook, UPlotThresholdOptions } from './UPlotThresholds';
|
||||
|
||||
export class UPlotConfigBuilder {
|
||||
private series: UPlotSeriesBuilder[] = [];
|
||||
@ -21,6 +22,8 @@ export class UPlotConfigBuilder {
|
||||
private hasBottomAxis = false;
|
||||
private hooks: Hooks.Arrays = {};
|
||||
private tz: string | undefined = undefined;
|
||||
// to prevent more than one threshold per scale
|
||||
private thresholds: Record<string, UPlotThresholdOptions> = {};
|
||||
|
||||
constructor(getTimeZone = () => DefaultTimeZone) {
|
||||
this.tz = getTimeZoneInfo(getTimeZone(), Date.now())?.ianaName;
|
||||
@ -36,6 +39,13 @@ export class UPlotConfigBuilder {
|
||||
this.hooks[type]!.push(hook as any);
|
||||
}
|
||||
|
||||
addThresholds(options: UPlotThresholdOptions) {
|
||||
if (!this.thresholds[options.scaleKey]) {
|
||||
this.thresholds[options.scaleKey] = options;
|
||||
this.addHook('drawClear', getThresholdsDrawHook(options));
|
||||
}
|
||||
}
|
||||
|
||||
addAxis(props: AxisProps) {
|
||||
props.placement = props.placement ?? AxisPlacement.Auto;
|
||||
|
||||
|
@ -0,0 +1,126 @@
|
||||
import { getColorForTheme, GrafanaTheme2, ThresholdsConfig } from '@grafana/data';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import { GraphThresholdsStyleConfig, GraphTresholdsStyleMode } from '../config';
|
||||
|
||||
export interface UPlotThresholdOptions {
|
||||
scaleKey: string;
|
||||
thresholds: ThresholdsConfig;
|
||||
config: GraphThresholdsStyleConfig;
|
||||
theme: GrafanaTheme2;
|
||||
}
|
||||
|
||||
export function getThresholdsDrawHook(options: UPlotThresholdOptions) {
|
||||
return (u: uPlot) => {
|
||||
const ctx = u.ctx;
|
||||
const { scaleKey, thresholds, theme, config } = options;
|
||||
const { steps } = thresholds;
|
||||
const { min: xMin, max: xMax } = u.scales.x;
|
||||
const { min: yMin, max: yMax } = u.scales[scaleKey];
|
||||
|
||||
if (xMin === undefined || xMax === undefined || yMin === undefined || yMax === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
function addLines() {
|
||||
// Thresholds below a transparent threshold is treated like "less than", and line drawn previous threshold
|
||||
let transparentIndex = 0;
|
||||
|
||||
for (let idx = 0; idx < steps.length; idx++) {
|
||||
const step = steps[idx];
|
||||
if (step.color === 'transparent') {
|
||||
transparentIndex = idx;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Ignore the base -Infinity threshold by always starting on index 1
|
||||
for (let idx = 1; idx < steps.length; idx++) {
|
||||
const step = steps[idx];
|
||||
let color: tinycolor.Instance;
|
||||
|
||||
// if we are below a transparent index treat this a less then threshold, use previous thresholds color
|
||||
if (transparentIndex >= idx && idx > 0) {
|
||||
color = tinycolor(getColorForTheme(steps[idx - 1].color, theme.v1));
|
||||
} else {
|
||||
color = tinycolor(getColorForTheme(step.color, theme.v1));
|
||||
}
|
||||
|
||||
// Unless alpha specififed set to default value
|
||||
if (color.getAlpha() === 1) {
|
||||
color.setAlpha(0.7);
|
||||
}
|
||||
|
||||
let x0 = u.valToPos(xMin!, 'x', true);
|
||||
let y0 = u.valToPos(step.value, scaleKey, true);
|
||||
let x1 = u.valToPos(xMax!, 'x', true);
|
||||
let y1 = u.valToPos(step.value, scaleKey, true);
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.lineWidth = 2;
|
||||
ctx.strokeStyle = color.toString();
|
||||
ctx.moveTo(x0, y0);
|
||||
ctx.lineTo(x1, y1);
|
||||
ctx.stroke();
|
||||
ctx.closePath();
|
||||
}
|
||||
}
|
||||
|
||||
function addAreas() {
|
||||
for (let idx = 0; idx < steps.length; idx++) {
|
||||
const step = steps[idx];
|
||||
|
||||
// skip thresholds that cannot be seen
|
||||
if (step.value > yMax!) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// if this is the last step make the next step the same color but +Infinity
|
||||
const nextStep =
|
||||
idx + 1 < steps.length
|
||||
? steps[idx + 1]
|
||||
: {
|
||||
...step,
|
||||
value: Infinity,
|
||||
};
|
||||
|
||||
let color = tinycolor(getColorForTheme(step.color, theme.v1));
|
||||
|
||||
// Ignore fully transparent colors
|
||||
const alpha = color.getAlpha();
|
||||
if (alpha === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/// if no alpha set automatic alpha
|
||||
if (alpha === 1) {
|
||||
color = color.setAlpha(0.15);
|
||||
}
|
||||
|
||||
let value = step.value === -Infinity ? yMin : step.value;
|
||||
let nextValue = nextStep.value === Infinity || nextStep.value > yMax! ? yMax : nextStep.value;
|
||||
|
||||
let x0 = u.valToPos(xMin ?? 0, 'x', true);
|
||||
let y0 = u.valToPos(value ?? 0, scaleKey, true);
|
||||
let x1 = u.valToPos(xMax ?? 1, 'x', true);
|
||||
let y1 = u.valToPos(nextValue ?? 1, scaleKey, true);
|
||||
|
||||
ctx.save();
|
||||
ctx.fillStyle = color.toString();
|
||||
ctx.fillRect(x0, y0, x1 - x0, y1 - y0);
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
|
||||
switch (config.mode) {
|
||||
case GraphTresholdsStyleMode.Line:
|
||||
addLines();
|
||||
break;
|
||||
case GraphTresholdsStyleMode.Area:
|
||||
addAreas();
|
||||
break;
|
||||
case GraphTresholdsStyleMode.LineAndArea:
|
||||
addLines();
|
||||
addAreas();
|
||||
}
|
||||
};
|
||||
}
|
@ -190,6 +190,16 @@ export function getGraphFieldConfig(cfg: GraphFieldConfig): SetFieldConfigOption
|
||||
addStackingConfig(builder, cfg.stacking);
|
||||
addAxisConfig(builder, cfg);
|
||||
addHideFrom(builder);
|
||||
|
||||
builder.addSelect({
|
||||
path: 'thresholdsStyle.mode',
|
||||
name: 'Show thresholds',
|
||||
category: ['Thresholds'],
|
||||
defaultValue: graphFieldOptions.thresholdsDisplayModes[0].value,
|
||||
settings: {
|
||||
options: graphFieldOptions.thresholdsDisplayModes,
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -67,6 +67,130 @@ describe('Graph Migrations', () => {
|
||||
expect(panel).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('thresholds', () => {
|
||||
test('Only gt thresholds', () => {
|
||||
const old: any = {
|
||||
angular: {
|
||||
thresholds: [
|
||||
{
|
||||
colorMode: 'critical',
|
||||
fill: true,
|
||||
line: false,
|
||||
op: 'gt',
|
||||
value: 80,
|
||||
yaxis: 'left',
|
||||
},
|
||||
{
|
||||
colorMode: 'warning',
|
||||
fill: true,
|
||||
line: false,
|
||||
op: 'gt',
|
||||
value: 50,
|
||||
yaxis: 'left',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
const panel = {} as PanelModel;
|
||||
panel.options = graphPanelChangedHandler(panel, 'graph', old);
|
||||
expect(panel.fieldConfig.defaults.custom.thresholdsStyle.mode).toBe('area');
|
||||
expect(panel.fieldConfig.defaults.thresholds?.steps).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"color": "transparent",
|
||||
"value": -Infinity,
|
||||
},
|
||||
Object {
|
||||
"color": "orange",
|
||||
"value": 50,
|
||||
},
|
||||
Object {
|
||||
"color": "red",
|
||||
"value": 80,
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
test('gt & lt thresholds', () => {
|
||||
const old: any = {
|
||||
angular: {
|
||||
thresholds: [
|
||||
{
|
||||
colorMode: 'critical',
|
||||
fill: true,
|
||||
line: true,
|
||||
op: 'gt',
|
||||
value: 80,
|
||||
yaxis: 'left',
|
||||
},
|
||||
{
|
||||
colorMode: 'warning',
|
||||
fill: true,
|
||||
line: true,
|
||||
op: 'lt',
|
||||
value: 40,
|
||||
yaxis: 'left',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const panel = {} as PanelModel;
|
||||
panel.options = graphPanelChangedHandler(panel, 'graph', old);
|
||||
expect(panel.fieldConfig.defaults.custom.thresholdsStyle.mode).toBe('line+area');
|
||||
expect(panel.fieldConfig.defaults.thresholds?.steps).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"color": "orange",
|
||||
"value": -Infinity,
|
||||
},
|
||||
Object {
|
||||
"color": "transparent",
|
||||
"value": 40,
|
||||
},
|
||||
Object {
|
||||
"color": "red",
|
||||
"value": 80,
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
test('Only lt thresholds', () => {
|
||||
const old: any = {
|
||||
angular: {
|
||||
thresholds: [
|
||||
{
|
||||
colorMode: 'warning',
|
||||
fill: true,
|
||||
line: true,
|
||||
op: 'lt',
|
||||
value: 40,
|
||||
yaxis: 'left',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const panel = {} as PanelModel;
|
||||
panel.options = graphPanelChangedHandler(panel, 'graph', old);
|
||||
expect(panel.fieldConfig.defaults.custom.thresholdsStyle.mode).toBe('line+area');
|
||||
expect(panel.fieldConfig.defaults.thresholds?.steps).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"color": "orange",
|
||||
"value": -Infinity,
|
||||
},
|
||||
Object {
|
||||
"color": "transparent",
|
||||
"value": 40,
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const stairscase = {
|
||||
|
@ -9,12 +9,15 @@ import {
|
||||
fieldReducers,
|
||||
NullValueMode,
|
||||
PanelModel,
|
||||
Threshold,
|
||||
ThresholdsMode,
|
||||
} from '@grafana/data';
|
||||
import {
|
||||
AxisPlacement,
|
||||
DrawStyle,
|
||||
GraphFieldConfig,
|
||||
GraphGradientMode,
|
||||
GraphTresholdsStyleMode,
|
||||
LegendDisplayMode,
|
||||
LineInterpolation,
|
||||
LineStyle,
|
||||
@ -312,6 +315,83 @@ export function flotToGraphOptions(angular: any): { fieldConfig: FieldConfigSour
|
||||
}
|
||||
}
|
||||
|
||||
if (angular.thresholds && angular.thresholds.length > 0) {
|
||||
let steps: Threshold[] = [];
|
||||
let area = false;
|
||||
let line = false;
|
||||
|
||||
const sorted = (angular.thresholds as AngularThreshold[]).sort((a, b) => (a.value > b.value ? 1 : -1));
|
||||
|
||||
for (let idx = 0; idx < sorted.length; idx++) {
|
||||
const threshold = sorted[idx];
|
||||
const next = sorted.length > idx + 1 ? sorted[idx + 1] : null;
|
||||
|
||||
if (threshold.fill) {
|
||||
area = true;
|
||||
}
|
||||
|
||||
if (threshold.line) {
|
||||
line = true;
|
||||
}
|
||||
|
||||
if (threshold.op === 'gt') {
|
||||
steps.push({
|
||||
value: threshold.value,
|
||||
color: getThresholdColor(threshold),
|
||||
});
|
||||
}
|
||||
|
||||
if (threshold.op === 'lt') {
|
||||
if (steps.length === 0) {
|
||||
steps.push({
|
||||
value: -Infinity,
|
||||
color: getThresholdColor(threshold),
|
||||
});
|
||||
}
|
||||
|
||||
// next op is gt and there is a gap set color to transparent
|
||||
if (next && next.op === 'gt' && next.value > threshold.value) {
|
||||
steps.push({
|
||||
value: threshold.value,
|
||||
color: 'transparent',
|
||||
});
|
||||
// if next is a lt we need to use it's color
|
||||
} else if (next && next.op === 'lt') {
|
||||
steps.push({
|
||||
value: threshold.value,
|
||||
color: getThresholdColor(next),
|
||||
});
|
||||
} else {
|
||||
steps.push({
|
||||
value: threshold.value,
|
||||
color: 'transparent',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if now less then threshold add an -Infinity base that is transparent
|
||||
if (steps.length > 0 && steps[0].value !== -Infinity) {
|
||||
steps.unshift({
|
||||
color: 'transparent',
|
||||
value: -Infinity,
|
||||
});
|
||||
}
|
||||
|
||||
let displayMode = area ? GraphTresholdsStyleMode.Area : GraphTresholdsStyleMode.Line;
|
||||
if (line && area) {
|
||||
displayMode = GraphTresholdsStyleMode.LineAndArea;
|
||||
}
|
||||
|
||||
// TODO move into standard ThresholdConfig ?
|
||||
y1.custom.thresholdsStyle = { mode: displayMode };
|
||||
|
||||
y1.thresholds = {
|
||||
mode: ThresholdsMode.Absolute,
|
||||
steps,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
fieldConfig: {
|
||||
defaults: omitBy(y1, isNil),
|
||||
@ -321,6 +401,33 @@ export function flotToGraphOptions(angular: any): { fieldConfig: FieldConfigSour
|
||||
};
|
||||
}
|
||||
|
||||
function getThresholdColor(threshold: AngularThreshold): string {
|
||||
if (threshold.colorMode === 'critical') {
|
||||
return 'red';
|
||||
}
|
||||
|
||||
if (threshold.colorMode === 'warning') {
|
||||
return 'orange';
|
||||
}
|
||||
|
||||
if (threshold.colorMode === 'custom') {
|
||||
return threshold.fillColor || threshold.lineColor;
|
||||
}
|
||||
|
||||
return 'red';
|
||||
}
|
||||
|
||||
interface AngularThreshold {
|
||||
op: string;
|
||||
fill: boolean;
|
||||
line: boolean;
|
||||
value: number;
|
||||
colorMode: 'critical' | 'warning' | 'custom';
|
||||
yaxis?: 'left' | 'right';
|
||||
fillColor: string;
|
||||
lineColor: string;
|
||||
}
|
||||
|
||||
// {
|
||||
// "label": "Y111",
|
||||
// "show": true,
|
||||
|
Loading…
Reference in New Issue
Block a user