From f2742d4a28d93667844694f309f9d5a445911ac9 Mon Sep 17 00:00:00 2001 From: Dominik Prokop Date: Thu, 19 Mar 2020 12:28:05 +0100 Subject: [PATCH] NewPanelEdit: Refactor value mappings UI to work better with new panel edit (#22808) * Refactor value mappings UI to work better with new panel edit * TS fix --- .../FieldConfigItemHeaderTitle.tsx | 63 ++++++++++++ .../src/components/FieldConfigs/mappings.tsx | 12 ++- .../ValueMappingsEditor/MappingRow.tsx | 95 +++++++++++++++++++ .../ValueMappingsEditor.story.tsx | 10 ++ .../ValueMappingsEditor.test.tsx | 86 +++++++++++++++++ .../ValueMappingsEditor.tsx | 75 +++++++++++++++ packages/grafana-ui/src/components/index.ts | 1 + packages/grafana-ui/src/themes/mixins.ts | 53 +++++++++++ .../PanelEditor/DynamicConfigValueEditor.tsx | 26 ++--- .../components/PanelEditor/OverrideEditor.tsx | 20 +--- .../components/PanelEditor/OverrideHeader.tsx | 39 -------- 11 files changed, 409 insertions(+), 71 deletions(-) create mode 100644 packages/grafana-ui/src/components/FieldConfigs/FieldConfigItemHeaderTitle.tsx create mode 100644 packages/grafana-ui/src/components/ValueMappingsEditor/MappingRow.tsx create mode 100644 packages/grafana-ui/src/components/ValueMappingsEditor/ValueMappingsEditor.story.tsx create mode 100644 packages/grafana-ui/src/components/ValueMappingsEditor/ValueMappingsEditor.test.tsx create mode 100644 packages/grafana-ui/src/components/ValueMappingsEditor/ValueMappingsEditor.tsx delete mode 100644 public/app/features/dashboard/components/PanelEditor/OverrideHeader.tsx diff --git a/packages/grafana-ui/src/components/FieldConfigs/FieldConfigItemHeaderTitle.tsx b/packages/grafana-ui/src/components/FieldConfigs/FieldConfigItemHeaderTitle.tsx new file mode 100644 index 00000000000..62a91fe2f69 --- /dev/null +++ b/packages/grafana-ui/src/components/FieldConfigs/FieldConfigItemHeaderTitle.tsx @@ -0,0 +1,63 @@ +import React from 'react'; +import { GrafanaTheme } from '@grafana/data'; +import { css } from 'emotion'; +import { selectThemeVariant, stylesFactory, useTheme } from '../../themes'; +import Forms from '../Forms'; +import { Icon } from '../Icon/Icon'; + +interface FieldConfigItemHeaderTitleProps { + title: string; + description?: string; + transparent?: boolean; + onRemove: () => void; +} + +export const FieldConfigItemHeaderTitle: React.FC = ({ + title, + description, + onRemove, + children, + transparent, +}) => { + const theme = useTheme(); + const styles = getFieldConfigItemHeaderTitleStyles(theme); + return ( +
+
+ {title} +
onRemove()} aria-label="FieldConfigItemHeaderTitle remove button"> + +
+
+ {children} +
+ ); +}; + +const getFieldConfigItemHeaderTitleStyles = stylesFactory((theme: GrafanaTheme) => { + const headerBg = selectThemeVariant( + { + light: theme.colors.white, + dark: theme.colors.dark1, + }, + theme.type + ); + + return { + headerWrapper: css` + background: ${headerBg}; + padding: ${theme.spacing.xs} 0; + `, + header: css` + display: flex; + justify-content: space-between; + padding: ${theme.spacing.xs} ${theme.spacing.xs} 0 ${theme.spacing.xs}; + `, + remove: css` + flex-grow: 0; + flex-shrink: 0; + cursor: pointer; + color: ${theme.colors.red88}; + `, + }; +}); diff --git a/packages/grafana-ui/src/components/FieldConfigs/mappings.tsx b/packages/grafana-ui/src/components/FieldConfigs/mappings.tsx index 232b2d4ee4e..b15f45902cf 100644 --- a/packages/grafana-ui/src/components/FieldConfigs/mappings.tsx +++ b/packages/grafana-ui/src/components/FieldConfigs/mappings.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { FieldOverrideContext, FieldOverrideEditorProps, FieldConfigEditorProps, ValueMapping } from '@grafana/data'; -import { LegacyValueMappingsEditor } from '..'; +import { ValueMappingsEditor } from '../ValueMappingsEditor/ValueMappingsEditor'; export interface ValueMappingFieldConfigSettings {} @@ -27,7 +27,7 @@ export class ValueMappingsValueEditor extends React.PureComponent< value = []; } - return ; + return ; } } @@ -39,6 +39,12 @@ export class ValueMappingsOverrideEditor extends React.PureComponent< } render() { - return
VALUE MAPPINGS OVERRIDE EDITOR {this.props.item.name}
; + const { onChange } = this.props; + let value = this.props.value; + if (!value) { + value = []; + } + + return ; } } diff --git a/packages/grafana-ui/src/components/ValueMappingsEditor/MappingRow.tsx b/packages/grafana-ui/src/components/ValueMappingsEditor/MappingRow.tsx new file mode 100644 index 00000000000..47f3acb4f80 --- /dev/null +++ b/packages/grafana-ui/src/components/ValueMappingsEditor/MappingRow.tsx @@ -0,0 +1,95 @@ +import React, { ChangeEvent } from 'react'; +import { HorizontalGroup } from '../Layout/Layout'; +import Forms from '../Forms'; +import { MappingType, RangeMap, ValueMap, ValueMapping } from '@grafana/data'; +import { styleMixins } from '../../themes/mixins'; +import { useTheme } from '../../themes'; +import { FieldConfigItemHeaderTitle } from '../FieldConfigs/FieldConfigItemHeaderTitle'; + +export interface Props { + valueMapping: ValueMapping; + updateValueMapping: (valueMapping: ValueMapping) => void; + removeValueMapping: () => void; +} + +const MAPPING_OPTIONS = [ + { value: MappingType.ValueToText, label: 'Value' }, + { value: MappingType.RangeToText, label: 'Range' }, +]; + +export const MappingRow: React.FC = ({ valueMapping, updateValueMapping, removeValueMapping }) => { + const theme = useTheme(); + const { type } = valueMapping; + + const onMappingValueChange = (event: ChangeEvent) => { + updateValueMapping({ ...valueMapping, value: event.target.value }); + }; + + const onMappingFromChange = (event: ChangeEvent) => { + updateValueMapping({ ...valueMapping, from: event.target.value }); + }; + + const onMappingToChange = (event: ChangeEvent) => { + updateValueMapping({ ...valueMapping, to: event.target.value }); + }; + + const onMappingTextChange = (event: ChangeEvent) => { + updateValueMapping({ ...valueMapping, text: event.target.value }); + }; + + const onMappingTypeChange = (mappingType: MappingType) => { + updateValueMapping({ ...valueMapping, type: mappingType }); + }; + + const renderRow = () => { + if (type === MappingType.RangeToText) { + return ( + <> + + + + + + + + + + + + + + ); + } + + return ( + <> + + + + + + + + + ); + }; + + const styles = styleMixins.panelEditorNestedListStyles(theme); + + return ( +
+ +
+ o.value === type)} + onChange={type => onMappingTypeChange(type.value!)} + /> +
+
+
{renderRow()}
+
+ ); +}; diff --git a/packages/grafana-ui/src/components/ValueMappingsEditor/ValueMappingsEditor.story.tsx b/packages/grafana-ui/src/components/ValueMappingsEditor/ValueMappingsEditor.story.tsx new file mode 100644 index 00000000000..31444604102 --- /dev/null +++ b/packages/grafana-ui/src/components/ValueMappingsEditor/ValueMappingsEditor.story.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import { storiesOf } from '@storybook/react'; +import { action } from '@storybook/addon-actions'; +import { ValueMappingsEditor } from './ValueMappingsEditor'; + +const ValueMappingsEditorStories = storiesOf('Panel/ValueMappingsEditor', module); + +ValueMappingsEditorStories.add('default', () => { + return ; +}); diff --git a/packages/grafana-ui/src/components/ValueMappingsEditor/ValueMappingsEditor.test.tsx b/packages/grafana-ui/src/components/ValueMappingsEditor/ValueMappingsEditor.test.tsx new file mode 100644 index 00000000000..15a37497392 --- /dev/null +++ b/packages/grafana-ui/src/components/ValueMappingsEditor/ValueMappingsEditor.test.tsx @@ -0,0 +1,86 @@ +import React from 'react'; +import { mount } from 'enzyme'; + +import { ValueMappingsEditor, Props } from './ValueMappingsEditor'; +import { MappingType } from '@grafana/data'; + +const setup = (spy?: any, propOverrides?: object) => { + const props: Props = { + onChange: (mappings: any) => { + if (spy) { + spy(mappings); + } + }, + valueMappings: [ + { id: 1, operator: '', type: MappingType.ValueToText, value: '20', text: 'Ok' }, + { id: 2, operator: '', type: MappingType.RangeToText, from: '21', to: '30', text: 'Meh' }, + ], + }; + + Object.assign(props, propOverrides); + + const wrapper = mount(); + + const instance = wrapper.instance() as ValueMappingsEditor; + + return { + instance, + wrapper, + }; +}; + +describe('Render', () => { + it('should render component', () => { + expect(setup).not.toThrow(); + }); +}); + +describe('On remove mapping', () => { + it('Should remove mapping at index 0', () => { + const onChangeSpy = jest.fn(); + const { wrapper } = setup(onChangeSpy); + const remove = wrapper.find('*[aria-label="FieldConfigItemHeaderTitle remove button"]'); + remove.at(0).simulate('click'); + expect(onChangeSpy).toBeCalledWith([ + { id: 2, operator: '', type: MappingType.RangeToText, from: '21', to: '30', text: 'Meh' }, + ]); + }); + + it('should remove mapping at index 1', () => { + const onChangeSpy = jest.fn(); + const { wrapper } = setup(onChangeSpy); + + const remove = wrapper.find('*[aria-label="FieldConfigItemHeaderTitle remove button"]'); + remove.at(1).simulate('click'); + + expect(onChangeSpy).toBeCalledWith([ + { id: 1, operator: '', type: MappingType.ValueToText, value: '20', text: 'Ok' }, + ]); + }); +}); + +describe('Next id to add', () => { + it('should be 3', () => { + const onChangeSpy = jest.fn(); + const { wrapper } = setup(onChangeSpy); + + const add = wrapper.find('*[aria-label="ValueMappingsEditor add mapping button"]'); + add.at(0).simulate('click'); + + expect(onChangeSpy).toBeCalledWith([ + { id: 1, operator: '', type: MappingType.ValueToText, value: '20', text: 'Ok' }, + { id: 2, operator: '', type: MappingType.RangeToText, from: '21', to: '30', text: 'Meh' }, + { id: 3, operator: '', type: MappingType.ValueToText, from: '', to: '', text: '' }, + ]); + }); + + it('should default to 0', () => { + const onChangeSpy = jest.fn(); + const { wrapper } = setup(onChangeSpy, { valueMappings: [] }); + const add = wrapper.find('*[aria-label="ValueMappingsEditor add mapping button"]'); + add.at(0).simulate('click'); + expect(onChangeSpy).toBeCalledWith([ + { id: 0, operator: '', type: MappingType.ValueToText, from: '', to: '', text: '' }, + ]); + }); +}); diff --git a/packages/grafana-ui/src/components/ValueMappingsEditor/ValueMappingsEditor.tsx b/packages/grafana-ui/src/components/ValueMappingsEditor/ValueMappingsEditor.tsx new file mode 100644 index 00000000000..22e453e1413 --- /dev/null +++ b/packages/grafana-ui/src/components/ValueMappingsEditor/ValueMappingsEditor.tsx @@ -0,0 +1,75 @@ +import React from 'react'; +import { MappingType, ValueMapping } from '@grafana/data'; +import Forms from '../Forms'; +import { FullWidthButtonContainer } from '../Button/FullWidthButtonContainer'; +import { MappingRow } from './MappingRow'; + +export interface Props { + valueMappings?: ValueMapping[]; + onChange: (valueMappings: ValueMapping[]) => void; +} + +export const ValueMappingsEditor: React.FC = ({ valueMappings, onChange, children }) => { + const onAdd = () => { + let update = valueMappings; + const defaultMapping = { + type: MappingType.ValueToText, + from: '', + to: '', + operator: '', + text: '', + }; + const id = update && update.length > 0 ? Math.max(...update.map(v => v.id)) + 1 : 0; + + if (update) { + update.push({ + id, + ...defaultMapping, + }); + } else { + update = [ + { + id, + ...defaultMapping, + }, + ]; + } + + onChange(update); + }; + + const onRemove = (index: number) => { + const update = valueMappings; + update!.splice(index, 1); + onChange(update!); + }; + + const onMappingChange = (index: number, value: ValueMapping) => { + const update = valueMappings; + update![index] = value; + onChange(update!); + }; + + return ( + <> + {valueMappings && valueMappings.length > 0 && ( + <> + {valueMappings.length > 0 && + valueMappings.map((valueMapping, index) => ( + onMappingChange(index, value)} + removeValueMapping={() => onRemove(index)} + /> + ))} + + )} + + + Add mapping + + + + ); +}; diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts index 747525de681..1f023b4da73 100644 --- a/packages/grafana-ui/src/components/index.ts +++ b/packages/grafana-ui/src/components/index.ts @@ -145,6 +145,7 @@ export { SelectOverrideEditor, SelectFieldConfigSettings, } from './FieldConfigs/select'; +export { FieldConfigItemHeaderTitle } from './FieldConfigs/FieldConfigItemHeaderTitle'; // Next-gen forms export { default as Forms, ButtonVariant } from './Forms'; diff --git a/packages/grafana-ui/src/themes/mixins.ts b/packages/grafana-ui/src/themes/mixins.ts index d7f9ee20520..80addcd321a 100644 --- a/packages/grafana-ui/src/themes/mixins.ts +++ b/packages/grafana-ui/src/themes/mixins.ts @@ -1,4 +1,7 @@ import { GrafanaTheme } from '@grafana/data'; +import { selectThemeVariant } from './selectThemeVariant'; +import { css } from 'emotion'; +import { stylesFactory } from './stylesFactory'; // eslint-disable-next-line @typescript-eslint/no-namespace export namespace styleMixins { @@ -45,4 +48,54 @@ export namespace styleMixins { border-radius: ${theme.border.radius.md}; `; } + + export const panelEditorNestedListStyles = stylesFactory((theme: GrafanaTheme) => { + const borderColor = selectThemeVariant( + { + light: theme.colors.gray85, + dark: theme.colors.dark9, + }, + theme.type + ); + const shadow = selectThemeVariant( + { + light: theme.colors.gray85, + dark: theme.colors.black, + }, + theme.type + ); + const headerBg = selectThemeVariant( + { + light: theme.colors.white, + dark: theme.colors.dark1, + }, + theme.type + ); + + return { + wrapper: css` + border: 1px dashed ${borderColor}; + margin-bottom: ${theme.spacing.md}; + transition: box-shadow 0.5s cubic-bezier(0.19, 1, 0.22, 1); + box-shadow: none; + &:hover { + box-shadow: 0 0 10px ${shadow}; + } + `, + headerWrapper: css` + background: ${headerBg}; + padding: ${theme.spacing.xs} 0; + `, + + content: css` + padding: ${theme.spacing.xs} ${theme.spacing.sm} ${theme.spacing.sm} ${theme.spacing.sm}; + border-top: 1px dashed ${borderColor}; + > *:last-child { + margin-bottom: 0; + `, + itemContent: css` + padding: ${theme.spacing.xs} ${theme.spacing.sm} ${theme.spacing.sm} ${theme.spacing.sm}; + `, + }; + }); } diff --git a/public/app/features/dashboard/components/PanelEditor/DynamicConfigValueEditor.tsx b/public/app/features/dashboard/components/PanelEditor/DynamicConfigValueEditor.tsx index 11b410f927e..9e055348c27 100644 --- a/public/app/features/dashboard/components/PanelEditor/DynamicConfigValueEditor.tsx +++ b/public/app/features/dashboard/components/PanelEditor/DynamicConfigValueEditor.tsx @@ -1,8 +1,7 @@ import React from 'react'; import { DynamicConfigValue, FieldConfigEditorRegistry, FieldOverrideContext, GrafanaTheme } from '@grafana/data'; -import { selectThemeVariant, stylesFactory, useTheme } from '@grafana/ui'; +import { FieldConfigItemHeaderTitle, selectThemeVariant, stylesFactory, useTheme } from '@grafana/ui'; -import { OverrideHeader } from './OverrideHeader'; import { css } from 'emotion'; interface DynamicConfigValueEditorProps { property: DynamicConfigValue; @@ -29,17 +28,18 @@ export const DynamicConfigValueEditor: React.FC = return (
- -
- { - onChange(value); - }} - item={item} - context={context} - /> -
+ +
+ { + onChange(value); + }} + item={item} + context={context} + /> +
+
); }; diff --git a/public/app/features/dashboard/components/PanelEditor/OverrideEditor.tsx b/public/app/features/dashboard/components/PanelEditor/OverrideEditor.tsx index 77e69978e52..f13d389cf6f 100644 --- a/public/app/features/dashboard/components/PanelEditor/OverrideEditor.tsx +++ b/public/app/features/dashboard/components/PanelEditor/OverrideEditor.tsx @@ -11,9 +11,10 @@ import { } from '@grafana/data'; import { fieldMatchersUI, stylesFactory, useTheme, ValuePicker, selectThemeVariant } from '@grafana/ui'; import { DynamicConfigValueEditor } from './DynamicConfigValueEditor'; -import { OverrideHeader } from './OverrideHeader'; + import { getDataLinksVariableSuggestions } from '../../../panel/panellinks/link_srv'; import { css } from 'emotion'; +import { FieldConfigItemHeaderTitle } from '@grafana/ui/src/components/FieldConfigs/FieldConfigItemHeaderTitle'; interface OverrideEditorProps { data: DataFrame[]; @@ -77,8 +78,7 @@ export const OverrideEditor: React.FC = ({ const styles = getStyles(theme); return (
-
- +
= ({ onChange={option => onMatcherConfigChange(option)} />
-
+
{override.properties.map((p, j) => { const reg = p.custom ? customPropertiesRegistry : standardFieldConfigEditorRegistry; @@ -137,14 +137,6 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => { theme.type ); - const headerBg = selectThemeVariant( - { - light: theme.colors.white, - dark: theme.colors.dark1, - }, - theme.type - ); - const shadow = selectThemeVariant( { light: theme.colors.gray85, @@ -163,10 +155,6 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => { box-shadow: 0 0 10px ${shadow}; } `, - headerWrapper: css` - background: ${headerBg}; - padding: ${theme.spacing.xs} 0; - `, matcherUi: css` padding: ${theme.spacing.sm}; `, diff --git a/public/app/features/dashboard/components/PanelEditor/OverrideHeader.tsx b/public/app/features/dashboard/components/PanelEditor/OverrideHeader.tsx deleted file mode 100644 index 813e8f8e8c0..00000000000 --- a/public/app/features/dashboard/components/PanelEditor/OverrideHeader.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import React from 'react'; -import { Forms, Icon, stylesFactory, useTheme } from '@grafana/ui'; -import { GrafanaTheme } from '@grafana/data'; -import { css } from 'emotion'; - -interface OverrideHeaderProps { - title: string; - description?: string; - onRemove: () => void; -} - -export const OverrideHeader: React.FC = ({ title, description, onRemove }) => { - const theme = useTheme(); - const styles = getOverrideHeaderStyles(theme); - return ( -
- {title} -
onRemove()}> - -
-
- ); -}; - -const getOverrideHeaderStyles = stylesFactory((theme: GrafanaTheme) => { - return { - header: css` - display: flex; - justify-content: space-between; - padding: ${theme.spacing.xs} ${theme.spacing.xs} 0 ${theme.spacing.xs}; - `, - remove: css` - flex-grow: 0; - flex-shrink: 0; - cursor: pointer; - color: ${theme.colors.red88}; - `, - }; -});