mirror of
https://github.com/grafana/grafana.git
synced 2025-12-18 22:16:21 -05:00
Chore: Convert more class components to functional (#114311)
* refactor ColorPicker to functional components * don't need memo for these components * convert CustomHeadersSettings to a functional component * ignore Legacy form components * ignore legacy forms in some lint rules * convert JSONFormatter to a functional component * convert WrapperWithState to a functional component * convert StatsPicker to a functional component * convert PopoverController to a functional component * convert UnitPicker to a functional component * fix linting * fix flaky dashboardcontrolsmenu test
This commit is contained in:
parent
026a000304
commit
11a27ab870
14 changed files with 390 additions and 532 deletions
|
|
@ -567,14 +567,6 @@
|
|||
"packages/grafana-ui/src/components/ColorPicker/ColorPicker.tsx": {
|
||||
"@typescript-eslint/no-explicit-any": {
|
||||
"count": 2
|
||||
},
|
||||
"react-prefer-function-component/react-prefer-function-component": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.tsx": {
|
||||
"react-prefer-function-component/react-prefer-function-component": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"packages/grafana-ui/src/components/Combobox/Combobox.story.tsx": {
|
||||
|
|
@ -605,9 +597,6 @@
|
|||
"packages/grafana-ui/src/components/DataSourceSettings/CustomHeadersSettings.tsx": {
|
||||
"@typescript-eslint/no-explicit-any": {
|
||||
"count": 2
|
||||
},
|
||||
"react-prefer-function-component/react-prefer-function-component": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"packages/grafana-ui/src/components/DataSourceSettings/types.ts": {
|
||||
|
|
@ -644,14 +633,8 @@
|
|||
"@typescript-eslint/consistent-type-assertions": {
|
||||
"count": 2
|
||||
},
|
||||
"@typescript-eslint/no-explicit-any": {
|
||||
"count": 1
|
||||
},
|
||||
"no-restricted-syntax": {
|
||||
"count": 1
|
||||
},
|
||||
"react-prefer-function-component/react-prefer-function-component": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"packages/grafana-ui/src/components/Forms/Legacy/Select/NoOptionsMessage.tsx": {
|
||||
|
|
@ -660,38 +643,18 @@
|
|||
}
|
||||
},
|
||||
"packages/grafana-ui/src/components/Forms/Legacy/Select/Select.tsx": {
|
||||
"@typescript-eslint/no-explicit-any": {
|
||||
"count": 1
|
||||
},
|
||||
"no-restricted-syntax": {
|
||||
"count": 6
|
||||
},
|
||||
"react-prefer-function-component/react-prefer-function-component": {
|
||||
"count": 3
|
||||
}
|
||||
},
|
||||
"packages/grafana-ui/src/components/Forms/Legacy/Select/SelectOption.tsx": {
|
||||
"@typescript-eslint/no-explicit-any": {
|
||||
"count": 2
|
||||
},
|
||||
"no-restricted-syntax": {
|
||||
"count": 4
|
||||
}
|
||||
},
|
||||
"packages/grafana-ui/src/components/Forms/Legacy/Select/SelectOptionGroup.tsx": {
|
||||
"@typescript-eslint/no-explicit-any": {
|
||||
"count": 3
|
||||
},
|
||||
"react-prefer-function-component/react-prefer-function-component": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"packages/grafana-ui/src/components/Forms/Legacy/Switch/Switch.tsx": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 3
|
||||
},
|
||||
"react-prefer-function-component/react-prefer-function-component": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"packages/grafana-ui/src/components/Gauge/Gauge.tsx": {
|
||||
|
|
@ -709,11 +672,6 @@
|
|||
"count": 3
|
||||
}
|
||||
},
|
||||
"packages/grafana-ui/src/components/JSONFormatter/JSONFormatter.tsx": {
|
||||
"react-prefer-function-component/react-prefer-function-component": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"packages/grafana-ui/src/components/JSONFormatter/json_explorer/json_explorer.ts": {
|
||||
"@typescript-eslint/no-explicit-any": {
|
||||
"count": 2
|
||||
|
|
@ -838,16 +796,6 @@
|
|||
"count": 1
|
||||
}
|
||||
},
|
||||
"packages/grafana-ui/src/components/StatsPicker/StatsPicker.story.tsx": {
|
||||
"react-prefer-function-component/react-prefer-function-component": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"packages/grafana-ui/src/components/StatsPicker/StatsPicker.tsx": {
|
||||
"react-prefer-function-component/react-prefer-function-component": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"packages/grafana-ui/src/components/Table/Cells/TableCell.tsx": {
|
||||
"@typescript-eslint/consistent-type-assertions": {
|
||||
"count": 3
|
||||
|
|
@ -933,21 +881,11 @@
|
|||
"count": 1
|
||||
}
|
||||
},
|
||||
"packages/grafana-ui/src/components/Tooltip/PopoverController.tsx": {
|
||||
"react-prefer-function-component/react-prefer-function-component": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"packages/grafana-ui/src/components/Typeahead/Typeahead.tsx": {
|
||||
"react-prefer-function-component/react-prefer-function-component": {
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"packages/grafana-ui/src/components/UnitPicker/UnitPicker.tsx": {
|
||||
"react-prefer-function-component/react-prefer-function-component": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"packages/grafana-ui/src/components/ValuePicker/ValuePicker.tsx": {
|
||||
"@grafana/no-aria-label-selectors": {
|
||||
"count": 1
|
||||
|
|
|
|||
|
|
@ -132,6 +132,7 @@ module.exports = [
|
|||
reportUnusedDisableDirectives: false,
|
||||
},
|
||||
files: ['**/*.{ts,tsx,js}'],
|
||||
ignores: ['packages/grafana-ui/src/components/Forms/Legacy/**'],
|
||||
plugins: {
|
||||
'@emotion': emotionPlugin,
|
||||
lodash: lodashPlugin,
|
||||
|
|
@ -254,6 +255,9 @@ module.exports = [
|
|||
name: 'grafana/jsx-a11y-overrides',
|
||||
files: ['**/*.tsx'],
|
||||
ignores: ['**/*.{spec,test}.tsx'],
|
||||
plugins: {
|
||||
'jsx-a11y': jsxA11yPlugin,
|
||||
},
|
||||
rules: {
|
||||
...jsxA11yPlugin.configs.recommended.rules,
|
||||
'jsx-a11y/no-autofocus': [
|
||||
|
|
@ -278,6 +282,9 @@ module.exports = [
|
|||
name: 'grafana/packages',
|
||||
files: ['packages/**/*.{ts,tsx}'],
|
||||
ignores: [],
|
||||
plugins: {
|
||||
import: importPlugin,
|
||||
},
|
||||
rules: {
|
||||
'import/no-extraneous-dependencies': ['error', { includeInternal: true }],
|
||||
'no-restricted-imports': [
|
||||
|
|
@ -378,6 +385,7 @@ module.exports = [
|
|||
plugins: {
|
||||
'testing-library': testingLibraryPlugin,
|
||||
'jest-dom': jestDomPlugin,
|
||||
jest: jestPlugin,
|
||||
},
|
||||
files: [
|
||||
'public/app/features/alerting/**/__tests__/**/*.[jt]s?(x)',
|
||||
|
|
@ -508,10 +516,12 @@ module.exports = [
|
|||
// Old betterer rules config:
|
||||
{
|
||||
files: ['**/*.{js,jsx,ts,tsx}'],
|
||||
ignores:
|
||||
ignores: [
|
||||
// FIXME: Remove once all enterprise issues are fixed -
|
||||
// we don't have a suppressions file/approach for enterprise code yet
|
||||
enterpriseIgnores,
|
||||
...enterpriseIgnores,
|
||||
'packages/grafana-ui/src/components/Forms/Legacy/**',
|
||||
],
|
||||
rules: {
|
||||
'@typescript-eslint/no-explicit-any': 'error',
|
||||
'@grafana/no-aria-label-selectors': 'error',
|
||||
|
|
|
|||
|
|
@ -1,11 +1,16 @@
|
|||
import { css } from '@emotion/css';
|
||||
import { Component, createRef } from 'react';
|
||||
import * as React from 'react';
|
||||
import {
|
||||
type ComponentType,
|
||||
createElement,
|
||||
type PropsWithChildren,
|
||||
type ReactNode,
|
||||
type RefObject,
|
||||
useRef,
|
||||
} from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
|
||||
import { withTheme2 } from '../../themes/ThemeContext';
|
||||
import { stylesFactory } from '../../themes/stylesFactory';
|
||||
import { useTheme2 } from '../../themes/ThemeContext';
|
||||
import { closePopover } from '../../utils/closePopover';
|
||||
import { Popover } from '../Tooltip/Popover';
|
||||
import { PopoverController } from '../Tooltip/PopoverController';
|
||||
|
|
@ -21,76 +26,81 @@ import { SeriesColorPickerPopover } from './SeriesColorPickerPopover';
|
|||
* component as a custom trigger you will need to forward the reference to first HTMLElement child.
|
||||
*/
|
||||
type ColorPickerTriggerRenderer = (props: {
|
||||
// This should be a React.RefObject<HTMLElement> but due to how object refs are defined you cannot downcast from that
|
||||
// to a specific type like React.RefObject<HTMLDivElement> even though it would be fine in runtime.
|
||||
ref: React.RefObject<any>;
|
||||
// This should be a RefObject<HTMLElement> but due to how object refs are defined you cannot downcast from that
|
||||
// to a specific type like RefObject<HTMLDivElement> even though it would be fine in runtime.
|
||||
ref: RefObject<any>;
|
||||
showColorPicker: () => void;
|
||||
hideColorPicker: () => void;
|
||||
}) => React.ReactNode;
|
||||
}) => ReactNode;
|
||||
|
||||
export const colorPickerFactory = <T extends ColorPickerProps>(
|
||||
popover: React.ComponentType<React.PropsWithChildren<T>>,
|
||||
popover: ComponentType<PropsWithChildren<T>>,
|
||||
displayName = 'ColorPicker'
|
||||
) => {
|
||||
return class ColorPicker extends Component<T & { children?: ColorPickerTriggerRenderer }> {
|
||||
static displayName = displayName;
|
||||
pickerTriggerRef = createRef<any>();
|
||||
const ColorPickerComponent = (props: T & { children?: ColorPickerTriggerRenderer }) => {
|
||||
const { children, onChange, color, id } = props;
|
||||
const theme = useTheme2();
|
||||
const pickerTriggerRef = useRef<any>(null);
|
||||
const styles = getStyles(theme);
|
||||
|
||||
render() {
|
||||
const { theme, children, onChange, color, id } = this.props;
|
||||
const styles = getStyles(theme);
|
||||
const popoverElement = React.createElement(popover, {
|
||||
...{ ...this.props, children: null },
|
||||
const popoverElement = createElement(
|
||||
popover,
|
||||
{
|
||||
...props,
|
||||
onChange,
|
||||
});
|
||||
return (
|
||||
<PopoverController content={popoverElement} hideAfter={300}>
|
||||
{(showPopper, hidePopper, popperProps) => {
|
||||
return (
|
||||
<>
|
||||
{this.pickerTriggerRef.current && (
|
||||
<Popover
|
||||
{...popperProps}
|
||||
referenceElement={this.pickerTriggerRef.current}
|
||||
wrapperClassName={styles.colorPicker}
|
||||
onMouseLeave={hidePopper}
|
||||
onMouseEnter={showPopper}
|
||||
onKeyDown={(event) => closePopover(event, hidePopper)}
|
||||
/>
|
||||
)}
|
||||
},
|
||||
null
|
||||
);
|
||||
|
||||
{children ? (
|
||||
children({
|
||||
ref: this.pickerTriggerRef,
|
||||
showColorPicker: showPopper,
|
||||
hideColorPicker: hidePopper,
|
||||
})
|
||||
) : (
|
||||
<ColorSwatch
|
||||
id={id}
|
||||
ref={this.pickerTriggerRef}
|
||||
onClick={showPopper}
|
||||
onMouseLeave={hidePopper}
|
||||
color={theme.visualization.getColorByName(color || '#000000')}
|
||||
aria-label={color}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</PopoverController>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<PopoverController content={popoverElement} hideAfter={300}>
|
||||
{(showPopper, hidePopper, popperProps) => {
|
||||
return (
|
||||
<>
|
||||
{pickerTriggerRef.current && (
|
||||
<Popover
|
||||
{...popperProps}
|
||||
referenceElement={pickerTriggerRef.current}
|
||||
wrapperClassName={styles.colorPicker}
|
||||
onMouseLeave={hidePopper}
|
||||
onMouseEnter={showPopper}
|
||||
onKeyDown={(event) => closePopover(event, hidePopper)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{children ? (
|
||||
children({
|
||||
ref: pickerTriggerRef,
|
||||
showColorPicker: showPopper,
|
||||
hideColorPicker: hidePopper,
|
||||
})
|
||||
) : (
|
||||
<ColorSwatch
|
||||
id={id}
|
||||
ref={pickerTriggerRef}
|
||||
onClick={showPopper}
|
||||
onMouseLeave={hidePopper}
|
||||
color={theme.visualization.getColorByName(color || '#000000')}
|
||||
aria-label={color}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</PopoverController>
|
||||
);
|
||||
};
|
||||
|
||||
return ColorPickerComponent;
|
||||
};
|
||||
|
||||
/**
|
||||
* https://developers.grafana.com/ui/latest/index.html?path=/docs/pickers-colorpicker--docs
|
||||
*/
|
||||
export const ColorPicker = withTheme2(colorPickerFactory(ColorPickerPopover, 'ColorPicker'));
|
||||
export const SeriesColorPicker = withTheme2(colorPickerFactory(SeriesColorPickerPopover, 'SeriesColorPicker'));
|
||||
export const ColorPicker = colorPickerFactory(ColorPickerPopover, 'ColorPicker');
|
||||
export const SeriesColorPicker = colorPickerFactory(SeriesColorPickerPopover, 'SeriesColorPicker');
|
||||
|
||||
const getStyles = stylesFactory((theme: GrafanaTheme2) => {
|
||||
const getStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
colorPicker: css({
|
||||
position: 'absolute',
|
||||
|
|
@ -102,4 +112,4 @@ const getStyles = stylesFactory((theme: GrafanaTheme2) => {
|
|||
overflow: 'auto',
|
||||
}),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -72,7 +72,6 @@ export const ColorPickerInput = forwardRef<HTMLInputElement, ColorPickerInputPro
|
|||
)}
|
||||
<ColorInput
|
||||
{...inputProps}
|
||||
theme={theme}
|
||||
color={currentColor}
|
||||
onChange={setColor}
|
||||
buttonAriaLabel="Open color picker"
|
||||
|
|
|
|||
|
|
@ -1,14 +1,11 @@
|
|||
import { css } from '@emotion/css';
|
||||
import { FocusScope } from '@react-aria/focus';
|
||||
import { Component } from 'react';
|
||||
import * as React from 'react';
|
||||
import { type ComponentType, createElement, useState } from 'react';
|
||||
|
||||
import { GrafanaTheme2, colorManipulator } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
|
||||
import { withTheme2 } from '../../themes/ThemeContext';
|
||||
import { stylesFactory } from '../../themes/stylesFactory';
|
||||
import { Themeable2 } from '../../types/theme';
|
||||
import { useTheme2 } from '../../themes/ThemeContext';
|
||||
import { Tab } from '../Tabs/Tab';
|
||||
import { TabsBar } from '../Tabs/TabsBar';
|
||||
import { PopoverContentProps } from '../Tooltip/types';
|
||||
|
|
@ -18,7 +15,7 @@ import SpectrumPalette from './SpectrumPalette';
|
|||
|
||||
export type ColorPickerChangeHandler = (color: string) => void;
|
||||
|
||||
export interface ColorPickerProps extends Themeable2 {
|
||||
export interface ColorPickerProps {
|
||||
color: string;
|
||||
onChange: ColorPickerChangeHandler;
|
||||
enableNamedColors?: boolean;
|
||||
|
|
@ -29,69 +26,56 @@ export interface Props<T> extends ColorPickerProps, PopoverContentProps {
|
|||
customPickers?: T;
|
||||
}
|
||||
|
||||
type PickerType = 'palette' | 'spectrum';
|
||||
|
||||
export interface CustomPickersDescriptor {
|
||||
[key: string]: {
|
||||
tabComponent: React.ComponentType<ColorPickerProps>;
|
||||
tabComponent: ComponentType<ColorPickerProps>;
|
||||
name: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface State<T> {
|
||||
activePicker: PickerType | keyof T;
|
||||
}
|
||||
type PickerType = 'palette' | 'spectrum';
|
||||
|
||||
class UnThemedColorPickerPopover<T extends CustomPickersDescriptor> extends Component<Props<T>, State<T>> {
|
||||
constructor(props: Props<T>) {
|
||||
super(props);
|
||||
this.state = {
|
||||
activePicker: 'palette',
|
||||
};
|
||||
}
|
||||
export const ColorPickerPopover = <T extends CustomPickersDescriptor>(props: Props<T>) => {
|
||||
const { color, onChange, enableNamedColors, customPickers } = props;
|
||||
const theme = useTheme2();
|
||||
const [activePicker, setActivePicker] = useState<PickerType | keyof T>('palette');
|
||||
|
||||
handleChange = (color: string) => {
|
||||
const { onChange, enableNamedColors, theme } = this.props;
|
||||
const styles = getStyles(theme);
|
||||
|
||||
const handleChange = (color: string) => {
|
||||
if (enableNamedColors) {
|
||||
return onChange(color);
|
||||
}
|
||||
onChange(colorManipulator.asHexString(theme.visualization.getColorByName(color)));
|
||||
};
|
||||
|
||||
onTabChange = (tab: PickerType | keyof T) => {
|
||||
return () => this.setState({ activePicker: tab });
|
||||
const onTabChange = (tab: PickerType | keyof T) => {
|
||||
return () => setActivePicker(tab);
|
||||
};
|
||||
|
||||
renderPicker = () => {
|
||||
const { activePicker } = this.state;
|
||||
const { color } = this.props;
|
||||
|
||||
switch (activePicker) {
|
||||
case 'spectrum':
|
||||
return <SpectrumPalette color={color} onChange={this.handleChange} />;
|
||||
case 'palette':
|
||||
return <NamedColorsPalette color={color} onChange={this.handleChange} />;
|
||||
default:
|
||||
return this.renderCustomPicker(activePicker);
|
||||
}
|
||||
};
|
||||
|
||||
renderCustomPicker = (tabKey: keyof T) => {
|
||||
const { customPickers, color, theme } = this.props;
|
||||
const renderCustomPicker = (tabKey: keyof T) => {
|
||||
if (!customPickers) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return React.createElement(customPickers[tabKey].tabComponent, {
|
||||
return createElement(customPickers[tabKey].tabComponent, {
|
||||
color,
|
||||
theme,
|
||||
onChange: this.handleChange,
|
||||
onChange: handleChange,
|
||||
});
|
||||
};
|
||||
|
||||
renderCustomPickerTabs = () => {
|
||||
const { customPickers } = this.props;
|
||||
const renderPicker = () => {
|
||||
switch (activePicker) {
|
||||
case 'spectrum':
|
||||
return <SpectrumPalette color={color} onChange={handleChange} />;
|
||||
case 'palette':
|
||||
return <NamedColorsPalette color={color} onChange={handleChange} />;
|
||||
default:
|
||||
return renderCustomPicker(activePicker);
|
||||
}
|
||||
};
|
||||
|
||||
const renderCustomPickerTabs = () => {
|
||||
if (!customPickers) {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -99,49 +83,39 @@ class UnThemedColorPickerPopover<T extends CustomPickersDescriptor> extends Comp
|
|||
return (
|
||||
<>
|
||||
{Object.keys(customPickers).map((key) => {
|
||||
return <Tab label={customPickers[key].name} onChangeTab={this.onTabChange(key)} key={key} />;
|
||||
return <Tab label={customPickers[key].name} onChangeTab={onTabChange(key)} key={key} />;
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { theme } = this.props;
|
||||
const { activePicker } = this.state;
|
||||
return (
|
||||
<FocusScope contain restoreFocus autoFocus>
|
||||
{/*
|
||||
tabIndex=-1 is needed here to support highlighting text within the picker when using FocusScope
|
||||
see https://github.com/adobe/react-spectrum/issues/1604#issuecomment-781574668
|
||||
*/}
|
||||
<div tabIndex={-1} className={styles.colorPickerPopover}>
|
||||
<TabsBar>
|
||||
<Tab
|
||||
label={t('grafana-ui.color-picker-popover.palette-tab', 'Colors')}
|
||||
onChangeTab={onTabChange('palette')}
|
||||
active={activePicker === 'palette'}
|
||||
/>
|
||||
<Tab
|
||||
label={t('grafana-ui.color-picker-popover.spectrum-tab', 'Custom')}
|
||||
onChangeTab={onTabChange('spectrum')}
|
||||
active={activePicker === 'spectrum'}
|
||||
/>
|
||||
{renderCustomPickerTabs()}
|
||||
</TabsBar>
|
||||
<div className={styles.colorPickerPopoverContent}>{renderPicker()}</div>
|
||||
</div>
|
||||
</FocusScope>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = getStyles(theme);
|
||||
|
||||
return (
|
||||
<FocusScope contain restoreFocus autoFocus>
|
||||
{/*
|
||||
tabIndex=-1 is needed here to support highlighting text within the picker when using FocusScope
|
||||
see https://github.com/adobe/react-spectrum/issues/1604#issuecomment-781574668
|
||||
*/}
|
||||
<div tabIndex={-1} className={styles.colorPickerPopover}>
|
||||
<TabsBar>
|
||||
<Tab
|
||||
label={t('grafana-ui.color-picker-popover.palette-tab', 'Colors')}
|
||||
onChangeTab={this.onTabChange('palette')}
|
||||
active={activePicker === 'palette'}
|
||||
/>
|
||||
<Tab
|
||||
label={t('grafana-ui.color-picker-popover.spectrum-tab', 'Custom')}
|
||||
onChangeTab={this.onTabChange('spectrum')}
|
||||
active={activePicker === 'spectrum'}
|
||||
/>
|
||||
{this.renderCustomPickerTabs()}
|
||||
</TabsBar>
|
||||
<div className={styles.colorPickerPopoverContent}>{this.renderPicker()}</div>
|
||||
</div>
|
||||
</FocusScope>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const ColorPickerPopover = withTheme2(UnThemedColorPickerPopover);
|
||||
ColorPickerPopover.displayName = 'ColorPickerPopover';
|
||||
|
||||
const getStyles = stylesFactory((theme: GrafanaTheme2) => {
|
||||
const getStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
colorPickerPopover: css({
|
||||
borderRadius: theme.shape.radius.default,
|
||||
|
|
@ -165,4 +139,4 @@ const getStyles = stylesFactory((theme: GrafanaTheme2) => {
|
|||
borderRadius: `${theme.shape.radius.default} ${theme.shape.radius.default} 0 0`,
|
||||
}),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { t } from '@grafana/i18n';
|
||||
|
||||
import { withTheme2 } from '../../themes/ThemeContext';
|
||||
import { InlineField } from '../Forms/InlineField';
|
||||
import { InlineSwitch } from '../Switch/Switch';
|
||||
import { PopoverContentProps } from '../Tooltip/types';
|
||||
|
|
@ -36,4 +35,4 @@ export const SeriesColorPickerPopover = (props: SeriesColorPickerPopoverProps) =
|
|||
};
|
||||
|
||||
// This component is to enable SeriesColorPickerPopover usage via series-color-picker-popover directive
|
||||
export const SeriesColorPickerPopoverWithTheme = withTheme2(SeriesColorPickerPopover);
|
||||
export const SeriesColorPickerPopoverWithTheme = SeriesColorPickerPopover;
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ const SpectrumPalette = ({ color, onChange }: SpectrumPaletteProps) => {
|
|||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<RgbaStringColorPicker className={styles.root} color={rgbaString} onChange={setColor} />
|
||||
<ColorInput theme={theme} color={rgbaString} onChange={setColor} className={styles.colorInput} />
|
||||
<ColorInput color={rgbaString} onChange={setColor} className={styles.colorInput} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { css } from '@emotion/css';
|
||||
import { uniqueId } from 'lodash';
|
||||
import { PureComponent } from 'react';
|
||||
import { memo, useState } from 'react';
|
||||
|
||||
import { DataSourceSettings } from '@grafana/data';
|
||||
import { t, Trans } from '@grafana/i18n';
|
||||
|
|
@ -27,10 +27,6 @@ export interface Props {
|
|||
onChange: (config: DataSourceSettings) => void;
|
||||
}
|
||||
|
||||
export interface State {
|
||||
headers: CustomHeaders;
|
||||
}
|
||||
|
||||
interface CustomHeaderRowProps {
|
||||
header: CustomHeader;
|
||||
onReset: (id: string) => void;
|
||||
|
|
@ -98,150 +94,129 @@ const CustomHeaderRow = ({ header, onBlur, onChange, onRemove, onReset }: Custom
|
|||
|
||||
CustomHeaderRow.displayName = 'CustomHeaderRow';
|
||||
|
||||
export class CustomHeadersSettings extends PureComponent<Props, State> {
|
||||
state: State = {
|
||||
headers: [],
|
||||
};
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
const { jsonData, secureJsonData, secureJsonFields } = this.props.dataSourceConfig;
|
||||
this.state = {
|
||||
headers: Object.keys(jsonData)
|
||||
.sort()
|
||||
.filter((key) => key.startsWith('httpHeaderName'))
|
||||
.map((key, index) => {
|
||||
return {
|
||||
id: uniqueId(),
|
||||
name: jsonData[key],
|
||||
value: secureJsonData !== undefined ? secureJsonData[key] : '',
|
||||
configured: (secureJsonFields && secureJsonFields[`httpHeaderValue${index + 1}`]) || false,
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
updateSettings = () => {
|
||||
const { headers } = this.state;
|
||||
export const CustomHeadersSettings = memo<Props>(({ dataSourceConfig, onChange }) => {
|
||||
const [headers, setHeaders] = useState<CustomHeaders>(() => {
|
||||
const { jsonData, secureJsonData, secureJsonFields } = dataSourceConfig;
|
||||
return Object.keys(jsonData)
|
||||
.sort()
|
||||
.filter((key) => key.startsWith('httpHeaderName'))
|
||||
.map((key, index) => {
|
||||
return {
|
||||
id: uniqueId(),
|
||||
name: jsonData[key],
|
||||
value: secureJsonData !== undefined ? secureJsonData[key] : '',
|
||||
configured: (secureJsonFields && secureJsonFields[`httpHeaderValue${index + 1}`]) || false,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
const updateSettings = (newHeaders: CustomHeaders) => {
|
||||
// we remove every httpHeaderName* field
|
||||
const newJsonData = Object.fromEntries(
|
||||
Object.entries(this.props.dataSourceConfig.jsonData).filter(([key, val]) => !key.startsWith('httpHeaderName'))
|
||||
Object.entries(dataSourceConfig.jsonData).filter(([key, val]) => !key.startsWith('httpHeaderName'))
|
||||
);
|
||||
|
||||
// we remove every httpHeaderValue* field
|
||||
const newSecureJsonData = Object.fromEntries(
|
||||
Object.entries(this.props.dataSourceConfig.secureJsonData || {}).filter(
|
||||
([key, val]) => !key.startsWith('httpHeaderValue')
|
||||
)
|
||||
Object.entries(dataSourceConfig.secureJsonData || {}).filter(([key, val]) => !key.startsWith('httpHeaderValue'))
|
||||
);
|
||||
|
||||
// then we add the current httpHeader-fields
|
||||
for (const [index, header] of headers.entries()) {
|
||||
for (const [index, header] of newHeaders.entries()) {
|
||||
newJsonData[`httpHeaderName${index + 1}`] = header.name;
|
||||
if (!header.configured) {
|
||||
newSecureJsonData[`httpHeaderValue${index + 1}`] = header.value;
|
||||
}
|
||||
}
|
||||
|
||||
this.props.onChange({
|
||||
...this.props.dataSourceConfig,
|
||||
onChange({
|
||||
...dataSourceConfig,
|
||||
jsonData: newJsonData,
|
||||
secureJsonData: newSecureJsonData,
|
||||
});
|
||||
};
|
||||
|
||||
onHeaderAdd = () => {
|
||||
this.setState((prevState) => {
|
||||
return { headers: [...prevState.headers, { id: uniqueId(), name: '', value: '', configured: false }] };
|
||||
});
|
||||
const onHeaderAdd = () => {
|
||||
setHeaders((prevHeaders) => [...prevHeaders, { id: uniqueId(), name: '', value: '', configured: false }]);
|
||||
};
|
||||
|
||||
onHeaderChange = (headerIndex: number, value: CustomHeader) => {
|
||||
this.setState(({ headers }) => {
|
||||
return {
|
||||
headers: headers.map((item, index) => {
|
||||
if (headerIndex !== index) {
|
||||
return item;
|
||||
}
|
||||
return { ...value };
|
||||
}),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
onHeaderReset = (headerId: string) => {
|
||||
this.setState(({ headers }) => {
|
||||
return {
|
||||
headers: headers.map((h, i) => {
|
||||
if (h.id !== headerId) {
|
||||
return h;
|
||||
}
|
||||
return {
|
||||
...h,
|
||||
value: '',
|
||||
configured: false,
|
||||
};
|
||||
}),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
onHeaderRemove = (headerId: string) => {
|
||||
this.setState(
|
||||
({ headers }) => ({
|
||||
headers: headers.filter((h) => h.id !== headerId),
|
||||
}),
|
||||
this.updateSettings
|
||||
const onHeaderChange = (headerIndex: number, value: CustomHeader) => {
|
||||
setHeaders((prevHeaders) =>
|
||||
prevHeaders.map((item, index) => {
|
||||
if (headerIndex !== index) {
|
||||
return item;
|
||||
}
|
||||
return { ...value };
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { headers } = this.state;
|
||||
const { dataSourceConfig } = this.props;
|
||||
const onHeaderReset = (headerId: string) => {
|
||||
setHeaders((prevHeaders) =>
|
||||
prevHeaders.map((h) => {
|
||||
if (h.id !== headerId) {
|
||||
return h;
|
||||
}
|
||||
return {
|
||||
...h,
|
||||
value: '',
|
||||
configured: false,
|
||||
};
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box marginBottom={5}>
|
||||
const onHeaderRemove = (headerId: string) => {
|
||||
setHeaders((prevHeaders) => {
|
||||
const newHeaders = prevHeaders.filter((h) => h.id !== headerId);
|
||||
updateSettings(newHeaders);
|
||||
return newHeaders;
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Box marginBottom={5}>
|
||||
<Box marginBottom={0.5} position="relative">
|
||||
<Stack direction="row" alignItems="baseline">
|
||||
<h6>
|
||||
<Trans i18nKey="grafana-ui.data-source-settings.custom-headers-title">Custom HTTP Headers</Trans>
|
||||
</h6>
|
||||
</Stack>
|
||||
</Box>
|
||||
<div>
|
||||
{headers.map((header, i) => (
|
||||
<CustomHeaderRow
|
||||
key={header.id}
|
||||
header={header}
|
||||
onChange={(h) => {
|
||||
onHeaderChange(i, h);
|
||||
}}
|
||||
onBlur={() => updateSettings(headers)}
|
||||
onRemove={onHeaderRemove}
|
||||
onReset={onHeaderReset}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{!dataSourceConfig.readOnly && (
|
||||
<Box marginBottom={0.5} position="relative">
|
||||
<Stack direction="row" alignItems="baseline">
|
||||
<h6>
|
||||
<Trans i18nKey="grafana-ui.data-source-settings.custom-headers-title">Custom HTTP Headers</Trans>
|
||||
</h6>
|
||||
<Button
|
||||
variant="secondary"
|
||||
icon="plus"
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
onHeaderAdd();
|
||||
}}
|
||||
>
|
||||
<Trans i18nKey="grafana-ui.data-source-settings.custom-headers-add">Add header</Trans>
|
||||
</Button>
|
||||
</Stack>
|
||||
</Box>
|
||||
<div>
|
||||
{headers.map((header, i) => (
|
||||
<CustomHeaderRow
|
||||
key={header.id}
|
||||
header={header}
|
||||
onChange={(h) => {
|
||||
this.onHeaderChange(i, h);
|
||||
}}
|
||||
onBlur={this.updateSettings}
|
||||
onRemove={this.onHeaderRemove}
|
||||
onReset={this.onHeaderReset}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{!dataSourceConfig.readOnly && (
|
||||
<Box marginBottom={0.5} position="relative">
|
||||
<Stack direction="row" alignItems="baseline">
|
||||
<Button
|
||||
variant="secondary"
|
||||
icon="plus"
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
this.onHeaderAdd();
|
||||
}}
|
||||
>
|
||||
<Trans i18nKey="grafana-ui.data-source-settings.custom-headers-add">Add header</Trans>
|
||||
</Button>
|
||||
</Stack>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
|
||||
CustomHeadersSettings.displayName = 'CustomHeadersSettings';
|
||||
|
||||
export default CustomHeadersSettings;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { PureComponent, createRef } from 'react';
|
||||
import { memo, useRef, useEffect } from 'react';
|
||||
|
||||
import { JsonExplorer, JsonExplorerConfig } from './json_explorer/json_explorer'; // We have made some monkey-patching of json-formatter-js so we can't switch right now
|
||||
|
||||
|
|
@ -10,45 +10,32 @@ interface Props {
|
|||
onDidRender?: (formattedJson: {}) => void;
|
||||
}
|
||||
|
||||
export class JSONFormatter extends PureComponent<Props> {
|
||||
private wrapperRef = createRef<HTMLDivElement>();
|
||||
export const JSONFormatter = memo<Props>(
|
||||
({ className, json, config = { animateOpen: true }, open = 3, onDidRender }) => {
|
||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
static defaultProps = {
|
||||
open: 3,
|
||||
config: {
|
||||
animateOpen: true,
|
||||
},
|
||||
};
|
||||
useEffect(() => {
|
||||
const wrapperEl = wrapperRef.current;
|
||||
if (!wrapperEl) {
|
||||
return;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.renderJson();
|
||||
const formatter = new JsonExplorer(json, open, config);
|
||||
const hasChildren = wrapperEl.hasChildNodes();
|
||||
|
||||
if (hasChildren && wrapperEl.lastChild) {
|
||||
wrapperEl.replaceChild(formatter.render(), wrapperEl.lastChild);
|
||||
} else {
|
||||
wrapperEl.appendChild(formatter.render());
|
||||
}
|
||||
|
||||
if (onDidRender) {
|
||||
onDidRender(formatter.json);
|
||||
}
|
||||
}, [json, config, open, onDidRender]);
|
||||
|
||||
return <div className={className} ref={wrapperRef} />;
|
||||
}
|
||||
);
|
||||
|
||||
componentDidUpdate() {
|
||||
this.renderJson();
|
||||
}
|
||||
|
||||
renderJson = () => {
|
||||
const { json, config, open, onDidRender } = this.props;
|
||||
const wrapperEl = this.wrapperRef.current;
|
||||
const formatter = new JsonExplorer(json, open, config);
|
||||
// @ts-ignore
|
||||
const hasChildren: boolean = wrapperEl.hasChildNodes();
|
||||
if (hasChildren) {
|
||||
// @ts-ignore
|
||||
wrapperEl.replaceChild(formatter.render(), wrapperEl.lastChild);
|
||||
} else {
|
||||
// @ts-ignore
|
||||
wrapperEl.appendChild(formatter.render());
|
||||
}
|
||||
|
||||
if (onDidRender) {
|
||||
onDidRender(formatter.json);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { className } = this.props;
|
||||
return <div className={className} ref={this.wrapperRef} />;
|
||||
}
|
||||
}
|
||||
JSONFormatter.displayName = 'JSONFormatter';
|
||||
|
|
|
|||
|
|
@ -1,45 +1,33 @@
|
|||
import { action } from '@storybook/addon-actions';
|
||||
import { Meta, StoryFn } from '@storybook/react';
|
||||
import { PureComponent } from 'react';
|
||||
import { memo, useState } from 'react';
|
||||
|
||||
import { Field } from '../Forms/Field';
|
||||
|
||||
import { Props, StatsPicker } from './StatsPicker';
|
||||
|
||||
interface State {
|
||||
stats: string[];
|
||||
}
|
||||
const WrapperWithState = memo<Props>(({ placeholder, allowMultiple, menuPlacement, width }) => {
|
||||
const [stats, setStats] = useState<string[]>([]);
|
||||
|
||||
class WrapperWithState extends PureComponent<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
stats: [],
|
||||
};
|
||||
}
|
||||
return (
|
||||
<Field label="Pick stats">
|
||||
<StatsPicker
|
||||
inputId="stats-picker"
|
||||
placeholder={placeholder}
|
||||
allowMultiple={allowMultiple}
|
||||
stats={stats}
|
||||
onChange={(newStats: string[]) => {
|
||||
action('Picked:')(newStats);
|
||||
setStats(newStats);
|
||||
}}
|
||||
menuPlacement={menuPlacement}
|
||||
width={width}
|
||||
/>
|
||||
</Field>
|
||||
);
|
||||
});
|
||||
|
||||
render() {
|
||||
const { placeholder, allowMultiple, menuPlacement, width } = this.props;
|
||||
const { stats } = this.state;
|
||||
|
||||
return (
|
||||
<Field label="Pick stats">
|
||||
<StatsPicker
|
||||
inputId="stats-picker"
|
||||
placeholder={placeholder}
|
||||
allowMultiple={allowMultiple}
|
||||
stats={stats}
|
||||
onChange={(stats: string[]) => {
|
||||
action('Picked:')(stats);
|
||||
this.setState({ stats });
|
||||
}}
|
||||
menuPlacement={menuPlacement}
|
||||
width={width}
|
||||
/>
|
||||
</Field>
|
||||
);
|
||||
}
|
||||
}
|
||||
WrapperWithState.displayName = 'WrapperWithState';
|
||||
|
||||
const meta: Meta<typeof StatsPicker> = {
|
||||
title: 'Pickers/StatsPicker',
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { difference } from 'lodash';
|
||||
import { PureComponent } from 'react';
|
||||
import { memo, useEffect } from 'react';
|
||||
|
||||
import { fieldReducers, SelectableValue, FieldReducerInfo } from '@grafana/data';
|
||||
|
||||
|
|
@ -18,54 +18,47 @@ export interface Props {
|
|||
filterOptions?: (ext: FieldReducerInfo) => boolean;
|
||||
}
|
||||
|
||||
export class StatsPicker extends PureComponent<Props> {
|
||||
static defaultProps: Partial<Props> = {
|
||||
allowMultiple: false,
|
||||
};
|
||||
export const StatsPicker = memo<Props>(
|
||||
({
|
||||
placeholder,
|
||||
onChange,
|
||||
stats,
|
||||
allowMultiple = false,
|
||||
defaultStat,
|
||||
className,
|
||||
width,
|
||||
menuPlacement,
|
||||
inputId,
|
||||
filterOptions,
|
||||
}) => {
|
||||
useEffect(() => {
|
||||
const current = fieldReducers.list(stats);
|
||||
if (current.length !== stats.length) {
|
||||
const found = current.map((v) => v.id);
|
||||
const notFound = difference(stats, found);
|
||||
console.warn('Unknown stats', notFound, stats);
|
||||
onChange(current.map((stat) => stat.id));
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.checkInput();
|
||||
}
|
||||
// Make sure there is only one
|
||||
if (!allowMultiple && stats.length > 1) {
|
||||
console.warn('Removing extra stat', stats);
|
||||
onChange([stats[0]]);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Props) {
|
||||
this.checkInput();
|
||||
}
|
||||
// Set the reducer from callback
|
||||
if (defaultStat && stats.length < 1) {
|
||||
onChange([defaultStat]);
|
||||
}
|
||||
}, [stats, allowMultiple, defaultStat, onChange]);
|
||||
|
||||
checkInput = () => {
|
||||
const { stats, allowMultiple, defaultStat, onChange } = this.props;
|
||||
|
||||
const current = fieldReducers.list(stats);
|
||||
if (current.length !== stats.length) {
|
||||
const found = current.map((v) => v.id);
|
||||
const notFound = difference(stats, found);
|
||||
console.warn('Unknown stats', notFound, stats);
|
||||
onChange(current.map((stat) => stat.id));
|
||||
}
|
||||
|
||||
// Make sure there is only one
|
||||
if (!allowMultiple && stats.length > 1) {
|
||||
console.warn('Removing extra stat', stats);
|
||||
onChange([stats[0]]);
|
||||
}
|
||||
|
||||
// Set the reducer from callback
|
||||
if (defaultStat && stats.length < 1) {
|
||||
onChange([defaultStat]);
|
||||
}
|
||||
};
|
||||
|
||||
onSelectionChange = (item: SelectableValue<string>) => {
|
||||
const { onChange } = this.props;
|
||||
if (Array.isArray(item)) {
|
||||
onChange(item.map((v) => v.value));
|
||||
} else {
|
||||
onChange(item && item.value ? [item.value] : []);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { stats, allowMultiple, defaultStat, placeholder, className, menuPlacement, width, inputId, filterOptions } =
|
||||
this.props;
|
||||
const onSelectionChange = (item: SelectableValue<string>) => {
|
||||
if (Array.isArray(item)) {
|
||||
onChange(item.map((v) => v.value));
|
||||
} else {
|
||||
onChange(item && item.value ? [item.value] : []);
|
||||
}
|
||||
};
|
||||
|
||||
const select = fieldReducers.selectOptions(stats, filterOptions);
|
||||
return (
|
||||
|
|
@ -78,10 +71,12 @@ export class StatsPicker extends PureComponent<Props> {
|
|||
isSearchable={true}
|
||||
options={select.options}
|
||||
placeholder={placeholder}
|
||||
onChange={this.onSelectionChange}
|
||||
onChange={onSelectionChange}
|
||||
menuPlacement={menuPlacement}
|
||||
inputId={inputId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
StatsPicker.displayName = 'StatsPicker';
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Placement } from '@popperjs/core';
|
||||
import { Component, type JSX } from 'react';
|
||||
import { useState, useRef, useCallback, type JSX } from 'react';
|
||||
|
||||
import { PopoverContent } from './types';
|
||||
|
||||
|
|
@ -21,37 +21,28 @@ interface Props {
|
|||
hideAfter?: number;
|
||||
}
|
||||
|
||||
interface State {
|
||||
show: boolean;
|
||||
}
|
||||
const PopoverController = ({ placement = 'auto', content, children, hideAfter }: Props) => {
|
||||
const [show, setShow] = useState(false);
|
||||
const hideTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
class PopoverController extends Component<Props, State> {
|
||||
private hideTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
state = { show: false };
|
||||
|
||||
showPopper = () => {
|
||||
if (this.hideTimeout) {
|
||||
clearTimeout(this.hideTimeout);
|
||||
const showPopper = useCallback(() => {
|
||||
if (hideTimeoutRef.current) {
|
||||
clearTimeout(hideTimeoutRef.current);
|
||||
}
|
||||
this.setState({ show: true });
|
||||
};
|
||||
setShow(true);
|
||||
}, []);
|
||||
|
||||
hidePopper = () => {
|
||||
this.hideTimeout = setTimeout(() => {
|
||||
this.setState({ show: false });
|
||||
}, this.props.hideAfter);
|
||||
};
|
||||
const hidePopper = useCallback(() => {
|
||||
hideTimeoutRef.current = setTimeout(() => {
|
||||
setShow(false);
|
||||
}, hideAfter);
|
||||
}, [hideAfter]);
|
||||
|
||||
render() {
|
||||
const { children, content, placement = 'auto' } = this.props;
|
||||
const { show } = this.state;
|
||||
|
||||
return children(this.showPopper, this.hidePopper, {
|
||||
show,
|
||||
placement,
|
||||
content,
|
||||
});
|
||||
}
|
||||
}
|
||||
return children(showPopper, hidePopper, {
|
||||
show,
|
||||
placement,
|
||||
content,
|
||||
});
|
||||
};
|
||||
|
||||
export { PopoverController };
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { PureComponent } from 'react';
|
||||
import { memo } from 'react';
|
||||
|
||||
import { getValueFormats, SelectableValue } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
|
|
@ -19,58 +19,52 @@ function formatCreateLabel(input: string) {
|
|||
/**
|
||||
* https://developers.grafana.com/ui/latest/index.html?path=/docs/pickers-unitpicker--docs
|
||||
*/
|
||||
export class UnitPicker extends PureComponent<UnitPickerProps> {
|
||||
onChange = (value: SelectableValue<string>) => {
|
||||
this.props.onChange(value.value);
|
||||
};
|
||||
export const UnitPicker = memo<UnitPickerProps>(({ onChange, value, width, id }) => {
|
||||
// Set the current selection
|
||||
let current: SelectableValue<string> | undefined = undefined;
|
||||
|
||||
render() {
|
||||
const { value, width, id } = this.props;
|
||||
// All units
|
||||
const unitGroups = getValueFormats();
|
||||
|
||||
// Set the current selection
|
||||
let current: SelectableValue<string> | undefined = undefined;
|
||||
|
||||
// All units
|
||||
const unitGroups = getValueFormats();
|
||||
|
||||
// Need to transform the data structure to work well with Select
|
||||
const groupOptions: CascaderOption[] = unitGroups.map((group) => {
|
||||
const options = group.submenu.map((unit) => {
|
||||
const sel = {
|
||||
label: unit.text,
|
||||
value: unit.value,
|
||||
};
|
||||
if (unit.value === value) {
|
||||
current = sel;
|
||||
}
|
||||
return sel;
|
||||
});
|
||||
|
||||
return {
|
||||
label: group.text,
|
||||
value: group.text,
|
||||
items: options,
|
||||
// Need to transform the data structure to work well with Select
|
||||
const groupOptions: CascaderOption[] = unitGroups.map((group) => {
|
||||
const options = group.submenu.map((unit) => {
|
||||
const sel = {
|
||||
label: unit.text,
|
||||
value: unit.value,
|
||||
};
|
||||
if (unit.value === value) {
|
||||
current = sel;
|
||||
}
|
||||
return sel;
|
||||
});
|
||||
|
||||
// Show the custom unit
|
||||
if (value && !current) {
|
||||
current = { value, label: value };
|
||||
}
|
||||
return {
|
||||
label: group.text,
|
||||
value: group.text,
|
||||
items: options,
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<Cascader
|
||||
id={id}
|
||||
width={width}
|
||||
initialValue={current && current.label}
|
||||
allowCustomValue
|
||||
changeOnSelect={false}
|
||||
formatCreateLabel={formatCreateLabel}
|
||||
options={groupOptions}
|
||||
placeholder={t('grafana-ui.unit-picker.placeholder', 'Choose')}
|
||||
isClearable
|
||||
onSelect={this.props.onChange}
|
||||
/>
|
||||
);
|
||||
// Show the custom unit
|
||||
if (value && !current) {
|
||||
current = { value, label: value };
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Cascader
|
||||
id={id}
|
||||
width={width}
|
||||
initialValue={current && current.label}
|
||||
allowCustomValue
|
||||
changeOnSelect={false}
|
||||
formatCreateLabel={formatCreateLabel}
|
||||
options={groupOptions}
|
||||
placeholder={t('grafana-ui.unit-picker.placeholder', 'Choose')}
|
||||
isClearable
|
||||
onSelect={onChange}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
UnitPicker.displayName = 'UnitPicker';
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { act, render, screen } from '@testing-library/react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import { VariableHide } from '@grafana/data';
|
||||
|
|
@ -65,15 +65,13 @@ describe('DashboardControlsMenu', () => {
|
|||
}),
|
||||
];
|
||||
|
||||
act(() => {
|
||||
render(<DashboardControlsButton dashboard={getDashboard(variables)} />);
|
||||
});
|
||||
render(<DashboardControlsButton dashboard={getDashboard(variables)} />);
|
||||
|
||||
// Should have rendered a dropdown
|
||||
expect(screen.getByRole('button')).toBeInTheDocument();
|
||||
|
||||
// Open the dropdown
|
||||
userEvent.click(screen.getByRole('button'));
|
||||
await userEvent.click(screen.getByRole('button'));
|
||||
expect(await screen.findByText('textVar1')).toBeInTheDocument();
|
||||
expect(await screen.findByText('textVar2')).toBeInTheDocument();
|
||||
expect(await screen.findByText('queryVar')).toBeInTheDocument();
|
||||
|
|
@ -104,7 +102,7 @@ describe('DashboardControlsMenu', () => {
|
|||
expect(screen.getByRole('button')).toBeInTheDocument();
|
||||
|
||||
// Open the dropdown
|
||||
userEvent.click(screen.getByRole('button'));
|
||||
await userEvent.click(screen.getByRole('button'));
|
||||
expect(await screen.findByText('textVar1')).toBeInTheDocument();
|
||||
expect(await screen.findByText('customVar')).toBeInTheDocument();
|
||||
expect(screen.queryByText('textVar2')).not.toBeInTheDocument();
|
||||
|
|
|
|||
Loading…
Reference in a new issue