-
-
+ return (
+
+
+
+
+
+
{title}
+ {children &&
{children}
}
+
+ {/* If onRemove is specified, giving preference to onRemove */}
+ {onRemove ? (
+
+ ) : onButtonClick ? (
+
+ ) : null}
-
-
{title}
- {children &&
{children}
}
-
- {/* If onRemove is specified, giving preference to onRemove */}
- {onRemove ? (
-
- ) : onButtonClick ? (
-
- ) : null}
-
- );
-};
+ );
+ }
+);
+
+Alert.displayName = 'Alert';
const getStyles = (theme: GrafanaTheme, severity: AlertVariant, outline: boolean) => {
const { white } = theme.palette;
@@ -107,6 +103,8 @@ const getStyles = (theme: GrafanaTheme, severity: AlertVariant, outline: boolean
body: css`
flex-grow: 1;
margin: 0 ${theme.spacing.md} 0 0;
+ overflow-wrap: break-word;
+ word-break: break-word;
a {
color: ${white};
diff --git a/packages/grafana-ui/src/components/BarGauge/BarGauge.tsx b/packages/grafana-ui/src/components/BarGauge/BarGauge.tsx
index f051c6e862d..d67f110b53d 100644
--- a/packages/grafana-ui/src/components/BarGauge/BarGauge.tsx
+++ b/packages/grafana-ui/src/components/BarGauge/BarGauge.tsx
@@ -14,6 +14,7 @@ import {
getFieldColorMode,
getColorForTheme,
FALLBACK_COLOR,
+ TextDisplayOptions,
} from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
@@ -42,6 +43,7 @@ export interface Props extends Themeable {
display?: DisplayProcessor;
value: DisplayValue;
orientation: VizOrientation;
+ text?: TextDisplayOptions;
itemSpacing?: number;
lcdCellWidth?: number;
displayMode: BarGaugeDisplayMode;
@@ -172,7 +174,7 @@ export class BarGauge extends PureComponent
{
}
renderRetroBars(): ReactNode {
- const { field, value, itemSpacing, alignmentFactors, orientation, lcdCellWidth } = this.props;
+ const { field, value, itemSpacing, alignmentFactors, orientation, lcdCellWidth, text } = this.props;
const {
valueHeight,
valueWidth,
@@ -193,7 +195,7 @@ export class BarGauge extends PureComponent {
const valueColor = getValueColor(this.props);
const valueToBaseSizeOn = alignmentFactors ? alignmentFactors : value;
- const valueStyles = getValueStyles(valueToBaseSizeOn, valueColor, valueWidth, valueHeight, orientation);
+ const valueStyles = getValueStyles(valueToBaseSizeOn, valueColor, valueWidth, valueHeight, orientation, text);
const containerStyles: CSSProperties = {
width: `${wrapperWidth}px`,
@@ -270,7 +272,7 @@ function isVertical(orientation: VizOrientation) {
}
function calculateTitleDimensions(props: Props): TitleDimensions {
- const { height, width, alignmentFactors, orientation } = props;
+ const { height, width, alignmentFactors, orientation, text } = props;
const title = alignmentFactors ? alignmentFactors.title : props.value.title;
if (!title) {
@@ -278,16 +280,26 @@ function calculateTitleDimensions(props: Props): TitleDimensions {
}
if (isVertical(orientation)) {
+ const fontSize = text?.titleSize ?? 14;
return {
- fontSize: 14,
+ fontSize: fontSize,
width: width,
- height: 14 * TITLE_LINE_HEIGHT,
+ height: fontSize * TITLE_LINE_HEIGHT,
placement: 'below',
};
}
// if height above 40 put text to above bar
if (height > 40) {
+ if (text?.titleSize) {
+ return {
+ fontSize: text?.titleSize,
+ width: 0,
+ height: text.titleSize * TITLE_LINE_HEIGHT,
+ placement: 'above',
+ };
+ }
+
const maxTitleHeightRatio = 0.45;
const titleHeight = Math.max(Math.min(height * maxTitleHeightRatio, MAX_VALUE_HEIGHT), 17);
@@ -306,7 +318,7 @@ function calculateTitleDimensions(props: Props): TitleDimensions {
const textSize = measureText(title, titleFontSize);
return {
- fontSize: titleFontSize,
+ fontSize: text?.titleSize ?? titleFontSize,
height: 0,
width: textSize.width + 15,
placement: 'left',
@@ -370,7 +382,7 @@ interface BarAndValueDimensions {
}
function calculateBarAndValueDimensions(props: Props): BarAndValueDimensions {
- const { height, width, orientation } = props;
+ const { height, width, orientation, text } = props;
const titleDim = calculateTitleDimensions(props);
let maxBarHeight = 0;
@@ -381,14 +393,23 @@ function calculateBarAndValueDimensions(props: Props): BarAndValueDimensions {
let wrapperHeight = 0;
if (isVertical(orientation)) {
- valueHeight = Math.min(Math.max(height * 0.1, MIN_VALUE_HEIGHT), MAX_VALUE_HEIGHT);
+ if (text?.valueSize) {
+ valueHeight = text.valueSize * VALUE_LINE_HEIGHT;
+ } else {
+ valueHeight = Math.min(Math.max(height * 0.1, MIN_VALUE_HEIGHT), MAX_VALUE_HEIGHT);
+ }
valueWidth = width;
maxBarHeight = height - (titleDim.height + valueHeight);
maxBarWidth = width;
wrapperWidth = width;
wrapperHeight = height - titleDim.height;
} else {
- valueHeight = height - titleDim.height;
+ if (text?.valueSize) {
+ valueHeight = text.valueSize * VALUE_LINE_HEIGHT;
+ } else {
+ valueHeight = height - titleDim.height;
+ }
+
valueWidth = Math.max(Math.min(width * 0.2, MAX_VALUE_WIDTH), MIN_VALUE_WIDTH);
maxBarHeight = height - titleDim.height;
maxBarWidth = width - valueWidth - titleDim.width;
@@ -420,14 +441,14 @@ export function getValuePercent(value: number, minValue: number, maxValue: numbe
* Only exported to for unit test
*/
export function getBasicAndGradientStyles(props: Props): BasicAndGradientStyles {
- const { displayMode, field, value, alignmentFactors, orientation, theme } = props;
+ const { displayMode, field, value, alignmentFactors, orientation, theme, text } = props;
const { valueWidth, valueHeight, maxBarHeight, maxBarWidth } = calculateBarAndValueDimensions(props);
const valuePercent = getValuePercent(value.numeric, field.min!, field.max!);
const valueColor = getValueColor(props);
const valueToBaseSizeOn = alignmentFactors ? alignmentFactors : value;
- const valueStyles = getValueStyles(valueToBaseSizeOn, valueColor, valueWidth, valueHeight, orientation);
+ const valueStyles = getValueStyles(valueToBaseSizeOn, valueColor, valueWidth, valueHeight, orientation, text);
const isBasic = displayMode === 'basic';
const wrapperStyles: CSSProperties = {
@@ -581,7 +602,8 @@ function getValueStyles(
color: string,
width: number,
height: number,
- orientation: VizOrientation
+ orientation: VizOrientation,
+ text?: TextDisplayOptions
): CSSProperties {
const styles: CSSProperties = {
color,
@@ -597,15 +619,12 @@ function getValueStyles(
const formattedValueString = formattedValueToString(value);
if (isVertical(orientation)) {
- styles.fontSize = calculateFontSize(formattedValueString, textWidth, height, VALUE_LINE_HEIGHT);
+ styles.fontSize = text?.valueSize ?? calculateFontSize(formattedValueString, textWidth, height, VALUE_LINE_HEIGHT);
styles.justifyContent = `center`;
} else {
- styles.fontSize = calculateFontSize(
- formattedValueString,
- textWidth - VALUE_LEFT_PADDING * 2,
- height,
- VALUE_LINE_HEIGHT
- );
+ styles.fontSize =
+ text?.valueSize ??
+ calculateFontSize(formattedValueString, textWidth - VALUE_LEFT_PADDING * 2, height, VALUE_LINE_HEIGHT);
styles.justifyContent = `flex-end`;
styles.paddingLeft = `${VALUE_LEFT_PADDING}px`;
styles.paddingRight = `${VALUE_LEFT_PADDING}px`;
diff --git a/packages/grafana-ui/src/components/BigValue/BigValue.tsx b/packages/grafana-ui/src/components/BigValue/BigValue.tsx
index 41fdc774a3b..ade30ba069c 100644
--- a/packages/grafana-ui/src/components/BigValue/BigValue.tsx
+++ b/packages/grafana-ui/src/components/BigValue/BigValue.tsx
@@ -1,6 +1,6 @@
// Library
import React, { PureComponent } from 'react';
-import { DisplayValue, GraphSeriesValue, DisplayValueAlignmentFactors } from '@grafana/data';
+import { DisplayValue, GraphSeriesValue, DisplayValueAlignmentFactors, TextDisplayOptions } from '@grafana/data';
// Types
import { Themeable } from '../../types';
@@ -64,6 +64,8 @@ export interface Props extends Themeable {
justifyMode?: BigValueJustifyMode;
/** Factors that should influence the positioning of the text */
alignmentFactors?: DisplayValueAlignmentFactors;
+ /** Explicit font size control */
+ text?: TextDisplayOptions;
/** Specify which text should be visible in the BigValue */
textMode?: BigValueTextMode;
diff --git a/packages/grafana-ui/src/components/BigValue/BigValueLayout.tsx b/packages/grafana-ui/src/components/BigValue/BigValueLayout.tsx
index 8ec540bee55..7e1e7bbbf42 100644
--- a/packages/grafana-ui/src/components/BigValue/BigValueLayout.tsx
+++ b/packages/grafana-ui/src/components/BigValue/BigValueLayout.tsx
@@ -29,7 +29,7 @@ export abstract class BigValueLayout {
textValues: BigValueTextValues;
constructor(private props: Props) {
- const { width, height, value, theme } = props;
+ const { width, height, value, theme, text } = props;
this.valueColor = getColorForTheme(value.color || 'green', theme);
this.panelPadding = height > 100 ? 12 : 8;
@@ -43,6 +43,18 @@ export abstract class BigValueLayout {
this.chartWidth = 0;
this.maxTextWidth = width - this.panelPadding * 2;
this.maxTextHeight = height - this.panelPadding * 2;
+
+ // Explicit font sizing
+ if (text) {
+ if (text.titleSize) {
+ this.titleFontSize = text.titleSize;
+ this.titleToAlignTo = undefined;
+ }
+ if (text.valueSize) {
+ this.valueFontSize = text.valueSize;
+ this.valueToAlignTo = '';
+ }
+ }
}
getTitleStyles(): CSSProperties {
@@ -235,9 +247,9 @@ export class WideNoChartLayout extends BigValueLayout {
constructor(props: Props) {
super(props);
- const valueWidthPercent = 0.3;
+ const valueWidthPercent = this.titleToAlignTo?.length ? 0.3 : 1.0;
- if (this.titleToAlignTo && this.titleToAlignTo.length > 0) {
+ if (this.valueToAlignTo.length) {
// initial value size
this.valueFontSize = calculateFontSize(
this.valueToAlignTo,
@@ -245,7 +257,9 @@ export class WideNoChartLayout extends BigValueLayout {
this.maxTextHeight,
LINE_HEIGHT
);
+ }
+ if (this.titleToAlignTo?.length) {
// How big can we make the title and still have it fit
this.titleFontSize = calculateFontSize(
this.titleToAlignTo,
@@ -257,9 +271,6 @@ export class WideNoChartLayout extends BigValueLayout {
// make sure it's a bit smaller than valueFontSize
this.titleFontSize = Math.min(this.valueFontSize * 0.7, this.titleFontSize);
- } else {
- // if no title wide
- this.valueFontSize = calculateFontSize(this.valueToAlignTo, this.maxTextWidth, this.maxTextHeight, LINE_HEIGHT);
}
}
@@ -292,6 +303,7 @@ export class WideWithChartLayout extends BigValueLayout {
super(props);
const { width, height } = props;
+
const chartHeightPercent = 0.5;
const titleWidthPercent = 0.6;
const valueWidthPercent = 1 - titleWidthPercent;
@@ -300,7 +312,7 @@ export class WideWithChartLayout extends BigValueLayout {
this.chartWidth = width;
this.chartHeight = height * chartHeightPercent;
- if (this.titleToAlignTo && this.titleToAlignTo.length > 0) {
+ if (this.titleToAlignTo?.length) {
this.titleFontSize = calculateFontSize(
this.titleToAlignTo,
this.maxTextWidth * titleWidthPercent,
@@ -310,12 +322,14 @@ export class WideWithChartLayout extends BigValueLayout {
);
}
- this.valueFontSize = calculateFontSize(
- this.valueToAlignTo,
- this.maxTextWidth * valueWidthPercent,
- this.maxTextHeight * chartHeightPercent,
- LINE_HEIGHT
- );
+ if (this.valueToAlignTo.length) {
+ this.valueFontSize = calculateFontSize(
+ this.valueToAlignTo,
+ this.maxTextWidth * valueWidthPercent,
+ this.maxTextHeight * chartHeightPercent,
+ LINE_HEIGHT
+ );
+ }
}
getValueAndTitleContainerStyles() {
@@ -350,7 +364,7 @@ export class StackedWithChartLayout extends BigValueLayout {
this.chartHeight = height * chartHeightPercent;
this.chartWidth = width;
- if (this.titleToAlignTo && this.titleToAlignTo.length > 0) {
+ if (this.titleToAlignTo?.length) {
this.titleFontSize = calculateFontSize(
this.titleToAlignTo,
this.maxTextWidth,
@@ -358,19 +372,22 @@ export class StackedWithChartLayout extends BigValueLayout {
LINE_HEIGHT,
MAX_TITLE_SIZE
);
+ }
+ titleHeight = this.titleFontSize * LINE_HEIGHT;
- titleHeight = this.titleFontSize * LINE_HEIGHT;
+ if (this.valueToAlignTo.length) {
+ this.valueFontSize = calculateFontSize(
+ this.valueToAlignTo,
+ this.maxTextWidth,
+ this.maxTextHeight - this.chartHeight - titleHeight,
+ LINE_HEIGHT
+ );
}
- this.valueFontSize = calculateFontSize(
- this.valueToAlignTo,
- this.maxTextWidth,
- this.maxTextHeight - this.chartHeight - titleHeight,
- LINE_HEIGHT
- );
-
// make title fontsize it's a bit smaller than valueFontSize
- this.titleFontSize = Math.min(this.valueFontSize * 0.7, this.titleFontSize);
+ if (this.titleToAlignTo?.length) {
+ this.titleFontSize = Math.min(this.valueFontSize * 0.7, this.titleFontSize);
+ }
// make chart take up unused space
this.chartHeight = height - this.titleFontSize * LINE_HEIGHT - this.valueFontSize * LINE_HEIGHT;
@@ -398,7 +415,7 @@ export class StackedWithNoChartLayout extends BigValueLayout {
const titleHeightPercent = 0.15;
let titleHeight = 0;
- if (this.titleToAlignTo && this.titleToAlignTo.length > 0) {
+ if (this.titleToAlignTo?.length) {
this.titleFontSize = calculateFontSize(
this.titleToAlignTo,
this.maxTextWidth,
@@ -410,12 +427,14 @@ export class StackedWithNoChartLayout extends BigValueLayout {
titleHeight = this.titleFontSize * LINE_HEIGHT;
}
- this.valueFontSize = calculateFontSize(
- this.valueToAlignTo,
- this.maxTextWidth,
- this.maxTextHeight - titleHeight,
- LINE_HEIGHT
- );
+ if (this.valueToAlignTo.length) {
+ this.valueFontSize = calculateFontSize(
+ this.valueToAlignTo,
+ this.maxTextWidth,
+ this.maxTextHeight - titleHeight,
+ LINE_HEIGHT
+ );
+ }
// make title fontsize it's a bit smaller than valueFontSize
this.titleFontSize = Math.min(this.valueFontSize * 0.7, this.titleFontSize);
diff --git a/packages/grafana-ui/src/components/Button/Button.story.tsx b/packages/grafana-ui/src/components/Button/Button.story.tsx
index 16612418b99..c73414e90a5 100644
--- a/packages/grafana-ui/src/components/Button/Button.story.tsx
+++ b/packages/grafana-ui/src/components/Button/Button.story.tsx
@@ -20,17 +20,13 @@ export default {
docs: {
page: mdx,
},
+ knobs: {
+ disabled: true,
+ },
},
};
-export const Simple: Story = ({ disabled, icon, children, size, variant }) => {
- return (
-
- );
-};
-
+export const Simple: Story = ({ children, ...args }) => ;
Simple.args = {
variant: 'primary',
size: 'md',
diff --git a/packages/grafana-ui/src/components/ClickOutsideWrapper/ClickOutsideWrapper.tsx b/packages/grafana-ui/src/components/ClickOutsideWrapper/ClickOutsideWrapper.tsx
index 227058a4530..e976629d1c6 100644
--- a/packages/grafana-ui/src/components/ClickOutsideWrapper/ClickOutsideWrapper.tsx
+++ b/packages/grafana-ui/src/components/ClickOutsideWrapper/ClickOutsideWrapper.tsx
@@ -1,5 +1,4 @@
-import { PureComponent } from 'react';
-import ReactDOM from 'react-dom';
+import React, { PureComponent, createRef } from 'react';
export interface Props {
/**
@@ -28,6 +27,7 @@ export class ClickOutsideWrapper extends PureComponent {
parent: window,
useCapture: false,
};
+ myRef = createRef();
state = {
hasEventListener: false,
};
@@ -48,7 +48,7 @@ export class ClickOutsideWrapper extends PureComponent {
}
onOutsideClick = (event: any) => {
- const domNode = ReactDOM.findDOMNode(this) as Element;
+ const domNode = this.myRef.current;
if (!domNode || !domNode.contains(event.target)) {
this.props.onClick();
@@ -56,6 +56,6 @@ export class ClickOutsideWrapper extends PureComponent {
};
render() {
- return this.props.children;
+ return {this.props.children}
;
}
}
diff --git a/packages/grafana-ui/src/components/ColorPicker/SeriesColorPickerPopover.tsx b/packages/grafana-ui/src/components/ColorPicker/SeriesColorPickerPopover.tsx
index 91c59ffb417..26e3f64fdee 100644
--- a/packages/grafana-ui/src/components/ColorPicker/SeriesColorPickerPopover.tsx
+++ b/packages/grafana-ui/src/components/ColorPicker/SeriesColorPickerPopover.tsx
@@ -19,20 +19,22 @@ export const SeriesColorPickerPopover: FunctionComponent (
- {
- if (onToggleAxis) {
- onToggleAxis();
- }
- }}
- />
- ),
+ tabComponent() {
+ return (
+ {
+ if (onToggleAxis) {
+ onToggleAxis();
+ }
+ }}
+ />
+ );
+ },
},
}}
/>
diff --git a/packages/grafana-ui/src/components/ColorPicker/SpectrumPalette.tsx b/packages/grafana-ui/src/components/ColorPicker/SpectrumPalette.tsx
index bd22d22b6ad..cc76477cd66 100644
--- a/packages/grafana-ui/src/components/ColorPicker/SpectrumPalette.tsx
+++ b/packages/grafana-ui/src/components/ColorPicker/SpectrumPalette.tsx
@@ -13,6 +13,7 @@ export interface SpectrumPaletteProps extends Themeable {
onChange: (color: string) => void;
}
+// eslint-disable-next-line react/display-name
const renderPointer = (theme: GrafanaTheme) => (props: SpectrumPalettePointerProps) => (
);
diff --git a/packages/grafana-ui/src/components/ContextMenu/ContextMenu.tsx b/packages/grafana-ui/src/components/ContextMenu/ContextMenu.tsx
index 6079ac5d3a6..7a584f08f1e 100644
--- a/packages/grafana-ui/src/components/ContextMenu/ContextMenu.tsx
+++ b/packages/grafana-ui/src/components/ContextMenu/ContextMenu.tsx
@@ -191,6 +191,7 @@ const ContextMenuItemComponent: React.FC = React.memo(
);
}
);
+ContextMenuItemComponent.displayName = 'ContextMenuItemComponent';
interface ContextMenuGroupProps {
group: ContextMenuGroup;
diff --git a/packages/grafana-ui/src/components/DataLinks/DataLinkInput.tsx b/packages/grafana-ui/src/components/DataLinks/DataLinkInput.tsx
index 821cde4c7ce..fa696622a1e 100644
--- a/packages/grafana-ui/src/components/DataLinks/DataLinkInput.tsx
+++ b/packages/grafana-ui/src/components/DataLinks/DataLinkInput.tsx
@@ -6,7 +6,7 @@ import { SelectionReference } from './SelectionReference';
import { Portal, getFormStyles } from '../index';
// @ts-ignore
-import Prism from 'prismjs';
+import Prism, { Grammar, LanguageMap } from 'prismjs';
import { Editor } from '@grafana/slate-react';
import { Value } from 'slate';
import Plain from 'slate-plain-serializer';
@@ -27,11 +27,20 @@ interface DataLinkInputProps {
placeholder?: string;
}
+const datalinksSyntax: Grammar = {
+ builtInVariable: {
+ pattern: /(\${\S+?})/,
+ },
+};
+
const plugins = [
- SlatePrism({
- onlyIn: (node: any) => node.type === 'code_block',
- getSyntax: () => 'links',
- }),
+ SlatePrism(
+ {
+ onlyIn: (node: any) => node.type === 'code_block',
+ getSyntax: () => 'links',
+ },
+ { ...(Prism.languages as LanguageMap), links: datalinksSyntax }
+ ),
];
const getStyles = stylesFactory((theme: GrafanaTheme) => ({
@@ -56,19 +65,10 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => ({
`,
}));
-export const enableDatalinksPrismSyntax = () => {
- Prism.languages['links'] = {
- builtInVariable: {
- pattern: /(\${\S+?})/,
- },
- };
-};
-
// This memoised also because rerendering the slate editor grabs focus which created problem in some cases this
// was used and changes to different state were propagated here.
export const DataLinkInput: React.FC = memo(
({ value, onChange, suggestions, placeholder = 'http://your-grafana.com/d/000000010/annotations' }) => {
- enableDatalinksPrismSyntax();
const editorRef = useRef() as RefObject;
const theme = useContext(ThemeContext);
const styles = getStyles(theme);
diff --git a/packages/grafana-ui/src/components/FormField/__snapshots__/FormField.test.tsx.snap b/packages/grafana-ui/src/components/FormField/__snapshots__/FormField.test.tsx.snap
index 049d927b50e..0d5632f1989 100644
--- a/packages/grafana-ui/src/components/FormField/__snapshots__/FormField.test.tsx.snap
+++ b/packages/grafana-ui/src/components/FormField/__snapshots__/FormField.test.tsx.snap
@@ -4,11 +4,11 @@ exports[`FormField should render component with custom inputEl 1`] = `
-
Test
-
+
Input
@@ -22,11 +22,11 @@ exports[`FormField should render component with default inputEl 1`] = `
-
Test
-
+
{
- const CONTAINER_GROUP = 'Container options';
- // ---
- const containerWidth = number(
- 'Container width',
- 300,
- {
- range: true,
- min: 100,
- max: 500,
- step: 10,
- },
- CONTAINER_GROUP
- );
+export const Simple: Story
= args => (
+
+
+
+
+
+);
- const BEHAVIOUR_GROUP = 'Behaviour props';
- const disabled = boolean('Disabled', false, BEHAVIOUR_GROUP);
- const invalid = boolean('Invalid', false, BEHAVIOUR_GROUP);
- const loading = boolean('Loading', false, BEHAVIOUR_GROUP);
- const error = text('Error message', '', BEHAVIOUR_GROUP);
-
- return { containerWidth, disabled, invalid, loading, error };
+Simple.args = {
+ label: 'Graphite API key',
+ description: 'Your Graphite instance API key',
+ disabled: false,
+ invalid: false,
+ loading: false,
+ error: 'Not valid input',
+ horizontal: false,
};
-export const Simple = () => {
- const { containerWidth, ...otherProps } = getKnobs();
- return (
-
-
-
-
-
- );
-};
-
-export const HorizontalLayout = () => {
+export const HorizontalLayout: Story = args => {
const [checked, setChecked] = useState(false);
const onChange = useCallback(e => setChecked(e.currentTarget.checked), [setChecked]);
- const { containerWidth, ...otherProps } = getKnobs();
return (
-
-
+
+
);
};
+
+HorizontalLayout.args = {
+ label: 'Show labels',
+ description: 'Display threshold labels',
+ disabled: false,
+ invalid: false,
+ loading: false,
+ error: 'Not valid input',
+ horizontal: true,
+};
diff --git a/packages/grafana-ui/src/components/Forms/Form.story.tsx b/packages/grafana-ui/src/components/Forms/Form.story.tsx
index ded2eb74857..4e76f241696 100644
--- a/packages/grafana-ui/src/components/Forms/Form.story.tsx
+++ b/packages/grafana-ui/src/components/Forms/Form.story.tsx
@@ -197,7 +197,7 @@ export const AsyncValidation = () => {
const validateAsync = (shouldPass: boolean) => async () => {
try {
- await new Promise((resolve, reject) => {
+ await new Promise((resolve, reject) => {
setTimeout(() => {
if (shouldPass) {
resolve();
diff --git a/packages/grafana-ui/src/components/Forms/Legacy/Select/ButtonSelect.tsx b/packages/grafana-ui/src/components/Forms/Legacy/Select/ButtonSelect.tsx
index 21e0427b5b2..8cceb55a4c4 100644
--- a/packages/grafana-ui/src/components/Forms/Legacy/Select/ButtonSelect.tsx
+++ b/packages/grafana-ui/src/components/Forms/Legacy/Select/ButtonSelect.tsx
@@ -11,6 +11,7 @@ interface ButtonComponentProps {
iconClass?: string;
}
+// eslint-disable-next-line react/display-name
const ButtonComponent = (buttonProps: ButtonComponentProps) => (props: any) => {
const { label, className, iconClass } = buttonProps;
diff --git a/packages/grafana-ui/src/components/Forms/RadioButtonGroup/RadioButton.tsx b/packages/grafana-ui/src/components/Forms/RadioButtonGroup/RadioButton.tsx
index 5ceccd9e7ce..624e93e20e6 100644
--- a/packages/grafana-ui/src/components/Forms/RadioButtonGroup/RadioButton.tsx
+++ b/packages/grafana-ui/src/components/Forms/RadioButtonGroup/RadioButton.tsx
@@ -80,9 +80,8 @@ const getRadioButtonStyles = stylesFactory((theme: GrafanaTheme, size: RadioButt
background: ${bg};
cursor: pointer;
z-index: 1;
- flex-grow: ${fullWidth ? 1 : 0};
+ flex: ${fullWidth ? `1 0 0` : 'none'};
text-align: center;
-
user-select: none;
&:hover {
diff --git a/packages/grafana-ui/src/components/Forms/RadioButtonGroup/RadioButtonGroup.tsx b/packages/grafana-ui/src/components/Forms/RadioButtonGroup/RadioButtonGroup.tsx
index d3c720c9715..aed2703e34f 100644
--- a/packages/grafana-ui/src/components/Forms/RadioButtonGroup/RadioButtonGroup.tsx
+++ b/packages/grafana-ui/src/components/Forms/RadioButtonGroup/RadioButtonGroup.tsx
@@ -1,5 +1,5 @@
import React, { useCallback, useRef } from 'react';
-import { css } from 'emotion';
+import { css, cx } from 'emotion';
import uniqueId from 'lodash/uniqueId';
import { SelectableValue } from '@grafana/data';
import { RadioButtonSize, RadioButton } from './RadioButton';
@@ -44,6 +44,7 @@ interface RadioButtonGroupProps {
onChange?: (value?: T) => void;
size?: RadioButtonSize;
fullWidth?: boolean;
+ className?: string;
}
export function RadioButtonGroup({
@@ -53,6 +54,7 @@ export function RadioButtonGroup({
disabled,
disabledOptions,
size = 'md',
+ className,
fullWidth = false,
}: RadioButtonGroupProps) {
const handleOnChange = useCallback(
@@ -70,7 +72,7 @@ export function RadioButtonGroup({
const styles = getRadioButtonGroupStyles();
return (
-
+
{options.map((o, i) => {
const isItemDisabled = disabledOptions && o.value && disabledOptions.includes(o.value);
return (
diff --git a/packages/grafana-ui/src/components/Gauge/Gauge.tsx b/packages/grafana-ui/src/components/Gauge/Gauge.tsx
index 2e98f8f7cd0..0372c5d9ae5 100644
--- a/packages/grafana-ui/src/components/Gauge/Gauge.tsx
+++ b/packages/grafana-ui/src/components/Gauge/Gauge.tsx
@@ -10,6 +10,7 @@ import {
getColorForTheme,
FieldColorModeId,
FALLBACK_COLOR,
+ TextDisplayOptions,
} from '@grafana/data';
import { Themeable } from '../../types';
import { calculateFontSize } from '../../utils/measureText';
@@ -21,6 +22,7 @@ export interface Props extends Themeable {
showThresholdLabels: boolean;
width: number;
value: DisplayValue;
+ text?: TextDisplayOptions;
onClick?: React.MouseEventHandler
;
className?: string;
}
@@ -108,7 +110,7 @@ export class Gauge extends PureComponent {
// remove gauge & marker width (on left and right side)
// and 10px is some padding that flot adds to the outer canvas
const valueWidth = valueWidthBase - ((gaugeWidth + (showThresholdMarkers ? thresholdMarkersWidth : 0)) * 2 + 10);
- const fontSize = calculateFontSize(text, valueWidth, dimension, 1, gaugeWidth * 1.7);
+ const fontSize = this.props.text?.valueSize ?? calculateFontSize(text, valueWidth, dimension, 1, gaugeWidth * 1.7);
const thresholdLabelFontSize = fontSize / 2.5;
let min = field.min!;
@@ -180,7 +182,7 @@ export class Gauge extends PureComponent {
}
renderVisualization = () => {
- const { width, value, height, onClick } = this.props;
+ const { width, value, height, onClick, text } = this.props;
const autoProps = calculateGaugeAutoProps(width, height, value.title);
return (
@@ -194,7 +196,7 @@ export class Gauge extends PureComponent {
{
- const data = new MutableDataFrame();
-
- data.addField({
- type: FieldType.time,
- name: 'Time',
- values: new ArrayVector([1602630000000, 1602633600000, 1602637200000]),
- config: {},
- });
-
- data.addField({
- type: FieldType.number,
- name: 'Value',
- values: new ArrayVector([10, 20, 5]),
- config: {
- custom: {
- mode: GraphMode.Line,
- },
- } as FieldConfig
,
- });
-
- const timeRange = {
- from: dateTime(1602673200000),
- to: dateTime(1602680400000),
- raw: { from: '1602673200000', to: '1602680400000' },
- };
- return { data, timeRange };
-};
-
-// const defaultLegendOptions: LegendOptions = {
-// displayMode: LegendDisplayMode.List,
-// placement: 'bottom',
-// };
-
-describe('GraphNG', () => {
- // describe('data update', () => {
- // it('does not re-initialise uPlot when there are no field config changes', () => {
- // const { data, timeRange } = mockData();
- // const onDataUpdateSpy = jest.fn();
- // const onPlotInitSpy = jest.fn();
- //
- // const { rerender } = render(
- //
- // );
- //
- // data.fields[1].values.set(0, 1);
- //
- // rerender(
- //
- // );
- //
- // expect(onPlotInitSpy).toBeCalledTimes(1);
- // expect(onDataUpdateSpy).toHaveBeenLastCalledWith([
- // [1602630000, 1602633600, 1602637200],
- // [1, 20, 5],
- // ]);
- // });
- // });
-
- describe('config update', () => {
- it('should skip plot intialization for width and height equal 0', () => {
- const { data, timeRange } = mockData();
-
- const { queryAllByTestId } = render(
-
- );
-
- expect(queryAllByTestId('uplot-main-div')).toHaveLength(1);
- });
-
- // it('reinitializes plot when number of series change', () => {
- // const { data, timeRange } = mockData();
- // const onPlotInitSpy = jest.fn();
- //
- // const { rerender } = render(
- //
- // );
- //
- // data.addField({
- // name: 'Value1',
- // type: FieldType.number,
- // values: new ArrayVector([1, 2, 3]),
- // config: {
- // custom: {
- // line: { show: true },
- // },
- // } as FieldConfig,
- // });
- //
- // rerender(
- //
- // );
- //
- // expect(onPlotInitSpy).toBeCalledTimes(2);
- // });
- //
- // it('reinitializes plot when series field config changes', () => {
- // const { data, timeRange } = mockData();
- // const onPlotInitSpy = jest.fn();
- //
- // const { rerender } = render(
- //
- // );
- // expect(onPlotInitSpy).toBeCalledTimes(1);
- //
- // data.fields[1].config.custom.line.width = 5;
- //
- // rerender(
- //
- // );
- //
- // expect(onPlotInitSpy).toBeCalledTimes(2);
- // });
- });
-});
diff --git a/packages/grafana-ui/src/components/GraphNG/GraphNG.tsx b/packages/grafana-ui/src/components/GraphNG/GraphNG.tsx
index 4d4268d688f..5320f371f19 100755
--- a/packages/grafana-ui/src/components/GraphNG/GraphNG.tsx
+++ b/packages/grafana-ui/src/components/GraphNG/GraphNG.tsx
@@ -1,4 +1,4 @@
-import React, { useCallback, useMemo, useRef } from 'react';
+import React, { useCallback, useLayoutEffect, useMemo, useRef } from 'react';
import {
compareDataFrameStructures,
DataFrame,
@@ -8,11 +8,12 @@ import {
formattedValueToString,
getFieldColorModeForField,
getFieldDisplayName,
+ TimeRange,
} from '@grafana/data';
import { alignDataFrames } from './utils';
import { UPlotChart } from '../uPlot/Plot';
import { PlotProps } from '../uPlot/types';
-import { AxisPlacement, GraphFieldConfig, GraphMode, PointMode } from '../uPlot/config';
+import { AxisPlacement, GraphFieldConfig, DrawStyle, PointVisibility } from '../uPlot/config';
import { useTheme } from '../../themes';
import { VizLayout } from '../VizLayout/VizLayout';
import { LegendDisplayMode, LegendItem, LegendOptions } from '../Legend/Legend';
@@ -34,8 +35,8 @@ export interface GraphNGProps extends Omit {
}
const defaultConfig: GraphFieldConfig = {
- mode: GraphMode.Line,
- points: PointMode.Auto,
+ drawStyle: DrawStyle.Line,
+ showPoints: PointVisibility.Auto,
axisPlacement: AxisPlacement.Auto,
};
@@ -51,34 +52,43 @@ export const GraphNG: React.FC = ({
...plotProps
}) => {
const alignedFrameWithGapTest = useMemo(() => alignDataFrames(data, fields), [data, fields]);
-
- if (alignedFrameWithGapTest == null) {
- return (
-
-
No data found in response
-
- );
- }
-
const theme = useTheme();
const legendItemsRef = useRef([]);
const hasLegend = useRef(legend && legend.displayMode !== LegendDisplayMode.Hidden);
- const alignedFrame = alignedFrameWithGapTest.frame;
- const compareFrames = useCallback(
- (a: DataFrame, b: DataFrame) => compareDataFrameStructures(a, b, ['min', 'max']),
- []
- );
+ const alignedFrame = alignedFrameWithGapTest?.frame;
+
+ const compareFrames = useCallback((a?: DataFrame | null, b?: DataFrame | null) => {
+ if (a && b) {
+ return compareDataFrameStructures(a, b);
+ }
+ return false;
+ }, []);
+
+ // reference change will not triger re-render
+ const currentTimeRange = useRef(timeRange);
+ useLayoutEffect(() => {
+ currentTimeRange.current = timeRange;
+ }, [timeRange]);
+
const configRev = useRevision(alignedFrame, compareFrames);
const configBuilder = useMemo(() => {
const builder = new UPlotConfigBuilder();
+ if (!alignedFrame) {
+ return builder;
+ }
+
// X is the first field in the alligned frame
const xField = alignedFrame.fields[0];
if (xField.type === FieldType.time) {
builder.addScale({
scaleKey: 'x',
isTime: true,
+ range: () => {
+ const r = currentTimeRange.current!;
+ return [r.from.valueOf(), r.to.valueOf()];
+ },
});
builder.addAxis({
scaleKey: 'x',
@@ -105,21 +115,25 @@ export const GraphNG: React.FC = ({
for (let i = 0; i < alignedFrame.fields.length; i++) {
const field = alignedFrame.fields[i];
const config = field.config as FieldConfig;
- const customConfig = config.custom || defaultConfig;
+ const customConfig: GraphFieldConfig = {
+ ...defaultConfig,
+ ...config.custom,
+ };
if (field === xField || field.type !== FieldType.number) {
continue;
}
const fmt = field.display ?? defaultFormatter;
- const scale = config.unit || '__fixed';
- const isNewScale = !builder.hasScale(scale);
+ const scaleKey = config.unit || '__fixed';
- if (isNewScale && customConfig.axisPlacement !== AxisPlacement.Hidden) {
- builder.addScale({ scaleKey: scale, min: field.config.min, max: field.config.max });
+ if (customConfig.axisPlacement !== AxisPlacement.Hidden) {
+ // The builder will manage unique scaleKeys and combine where appropriate
+ builder.addScale({ scaleKey, min: field.config.min, max: field.config.max });
builder.addAxis({
- scaleKey: scale,
+ scaleKey,
label: customConfig.axisLabel,
+ size: customConfig.axisWidth,
placement: customConfig.axisPlacement ?? AxisPlacement.Auto,
formatValue: v => formattedValueToString(fmt(v)),
theme,
@@ -131,23 +145,24 @@ export const GraphNG: React.FC = ({
const colorMode = getFieldColorModeForField(field);
const seriesColor = colorMode.getCalculator(field, theme)(0, 0);
- const pointsMode = customConfig.mode === GraphMode.Points ? PointMode.Always : customConfig.points;
+ const showPoints = customConfig.drawStyle === DrawStyle.Points ? PointVisibility.Always : customConfig.showPoints;
builder.addSeries({
- scaleKey: scale,
- line: (customConfig.mode ?? GraphMode.Line) === GraphMode.Line,
+ scaleKey,
+ drawStyle: customConfig.drawStyle!,
lineColor: seriesColor,
lineWidth: customConfig.lineWidth,
- points: pointsMode,
- pointSize: customConfig.pointRadius,
+ lineInterpolation: customConfig.lineInterpolation,
+ showPoints,
+ pointSize: customConfig.pointSize,
pointColor: seriesColor,
- fill: customConfig.fillAlpha !== undefined,
- fillOpacity: customConfig.fillAlpha,
+ fillOpacity: customConfig.fillOpacity,
fillColor: seriesColor,
+ spanNulls: customConfig.spanNulls || false,
});
if (hasLegend.current) {
- const axisPlacement = builder.getAxisPlacement(scale);
+ const axisPlacement = builder.getAxisPlacement(scaleKey);
legendItems.push({
color: seriesColor,
@@ -161,7 +176,15 @@ export const GraphNG: React.FC = ({
legendItemsRef.current = legendItems;
return builder;
- }, [configRev]);
+ }, [configRev, timeZone]);
+
+ if (alignedFrameWithGapTest == null) {
+ return (
+
+
No data found in response
+
+ );
+ }
let legendElement: React.ReactElement | undefined;
diff --git a/packages/grafana-ui/src/components/GraphNG/utils.ts b/packages/grafana-ui/src/components/GraphNG/utils.ts
index b865a33f1e6..2d6376708f6 100755
--- a/packages/grafana-ui/src/components/GraphNG/utils.ts
+++ b/packages/grafana-ui/src/components/GraphNG/utils.ts
@@ -43,6 +43,7 @@ export function mapDimesions(match: XYFieldMatchers, frame: DataFrame, frames?:
export function alignDataFrames(frames: DataFrame[], fields?: XYFieldMatchers): AlignedFrameWithGapTest | null {
const valuesFromFrames: AlignedData[] = [];
const sourceFields: Field[] = [];
+ const skipGaps: boolean[][] = [];
// Default to timeseries config
if (!fields) {
@@ -54,6 +55,7 @@ export function alignDataFrames(frames: DataFrame[], fields?: XYFieldMatchers):
for (const frame of frames) {
const dims = mapDimesions(fields, frame, frames);
+
if (!(dims.x.length && dims.y.length)) {
continue; // both x and y matched something!
}
@@ -62,9 +64,12 @@ export function alignDataFrames(frames: DataFrame[], fields?: XYFieldMatchers):
throw new Error('Only a single x field is supported');
}
+ let skipGapsFrame: boolean[] = [];
+
// Add the first X axis
if (!sourceFields.length) {
sourceFields.push(dims.x[0]);
+ skipGapsFrame.push(true);
}
const alignedData: AlignedData = [
@@ -74,10 +79,15 @@ export function alignDataFrames(frames: DataFrame[], fields?: XYFieldMatchers):
// Add the Y values
for (const field of dims.y) {
let values = field.values.toArray();
+ let spanNulls = field.config.custom.spanNulls || false;
+
if (field.config.nullValueMode === NullValueMode.AsZero) {
values = values.map(v => (v === null ? 0 : v));
+ spanNulls = true;
}
+
alignedData.push(values);
+ skipGapsFrame.push(spanNulls);
// This will cache an appropriate field name in the field state
getFieldDisplayName(field, frame, frames);
@@ -85,6 +95,7 @@ export function alignDataFrames(frames: DataFrame[], fields?: XYFieldMatchers):
}
valuesFromFrames.push(alignedData);
+ skipGaps.push(skipGapsFrame);
}
if (valuesFromFrames.length === 0) {
@@ -92,7 +103,7 @@ export function alignDataFrames(frames: DataFrame[], fields?: XYFieldMatchers):
}
// do the actual alignment (outerJoin on the first arrays)
- const { data: alignedData, isGap } = outerJoinValues(valuesFromFrames);
+ let { data: alignedData, isGap } = outerJoinValues(valuesFromFrames, skipGaps);
if (alignedData!.length !== sourceFields.length) {
throw new Error('outerJoinValues lost a field?');
@@ -111,18 +122,20 @@ export function alignDataFrames(frames: DataFrame[], fields?: XYFieldMatchers):
};
}
-export function outerJoinValues(tables: AlignedData[]): AlignedDataWithGapTest {
+// skipGaps is a tables-matched bool array indicating which series can skip storing indices of original nulls
+export function outerJoinValues(tables: AlignedData[], skipGaps?: boolean[][]): AlignedDataWithGapTest {
if (tables.length === 1) {
return {
data: tables[0],
- isGap: () => true,
+ isGap: skipGaps ? (u: uPlot, seriesIdx: number, dataIdx: number) => !skipGaps[0][seriesIdx] : () => true,
};
}
let xVals: Set = new Set();
let xNulls: Array> = [new Set()];
- for (const t of tables) {
+ for (let ti = 0; ti < tables.length; ti++) {
+ let t = tables[ti];
let xs = t[0];
let len = xs.length;
let nulls: Set = new Set();
@@ -132,11 +145,13 @@ export function outerJoinValues(tables: AlignedData[]): AlignedDataWithGapTest {
}
for (let j = 1; j < t.length; j++) {
- let ys = t[j];
+ if (skipGaps == null || !skipGaps[ti][j]) {
+ let ys = t[j];
- for (let i = 0; i < len; i++) {
- if (ys[i] == null) {
- nulls.add(xs[i]);
+ for (let i = 0; i < len; i++) {
+ if (ys[i] == null) {
+ nulls.add(xs[i]);
+ }
}
}
}
diff --git a/packages/grafana-ui/src/components/Icon/assets/Import.tsx b/packages/grafana-ui/src/components/Icon/assets/Import.tsx
index 4659a307c0a..2eb978da784 100644
--- a/packages/grafana-ui/src/components/Icon/assets/Import.tsx
+++ b/packages/grafana-ui/src/components/Icon/assets/Import.tsx
@@ -11,7 +11,7 @@ export const Import: FunctionComponent = ({ size, ...rest }) => {
height={size}
{...rest}
>
-