diff --git a/packages/grafana-ui/src/components/ValuePicker/ValuePicker.tsx b/packages/grafana-ui/src/components/ValuePicker/ValuePicker.tsx index 7a86fb1ae10..93c97891dd2 100644 --- a/packages/grafana-ui/src/components/ValuePicker/ValuePicker.tsx +++ b/packages/grafana-ui/src/components/ValuePicker/ValuePicker.tsx @@ -1,7 +1,7 @@ import React, { useState } from 'react'; import { IconType } from '../Icon/types'; import { SelectableValue } from '@grafana/data'; -import { Button } from '../Forms/Button'; +import { Button, ButtonVariant } from '../Forms/Button'; import { Select } from '../Forms/Select/Select'; interface ValuePickerProps { @@ -12,15 +12,16 @@ interface ValuePickerProps { /** ValuePicker options */ options: Array>; onChange: (value: SelectableValue) => void; + variant?: ButtonVariant; } -export function ValuePicker({ label, icon, options, onChange }: ValuePickerProps) { +export function ValuePicker({ label, icon, options, onChange, variant }: ValuePickerProps) { const [isPicking, setIsPicking] = useState(false); return ( <> {!isPicking && ( - )} diff --git a/public/app/features/dashboard/components/PanelEditor/DynamicConfigValueEditor.tsx b/public/app/features/dashboard/components/PanelEditor/DynamicConfigValueEditor.tsx new file mode 100644 index 00000000000..0792d81e117 --- /dev/null +++ b/public/app/features/dashboard/components/PanelEditor/DynamicConfigValueEditor.tsx @@ -0,0 +1,82 @@ +import React from 'react'; +import { DynamicConfigValue, FieldConfigEditorRegistry, FieldOverrideContext, GrafanaTheme } from '@grafana/data'; +import { selectThemeVariant, stylesFactory, useTheme } from '@grafana/ui'; + +import { OverrideHeader } from './OverrideHeader'; +import { css } from 'emotion'; +interface DynamicConfigValueEditorProps { + property: DynamicConfigValue; + editorsRegistry: FieldConfigEditorRegistry; + onChange: (value: DynamicConfigValue) => void; + context: FieldOverrideContext; + onRemove: () => void; +} + +export const DynamicConfigValueEditor: React.FC = ({ + property, + context, + editorsRegistry, + onChange, + onRemove, +}) => { + const theme = useTheme(); + const styles = getStyles(theme); + const item = editorsRegistry?.getIfExists(property.prop); + return ( +
+ +
+ { + onChange(value); + }} + item={item} + context={context} + /> +
+
+ ); +}; + +const getStyles = stylesFactory((theme: GrafanaTheme) => { + const borderColor = selectThemeVariant( + { + light: theme.colors.gray85, + dark: theme.colors.dark9, + }, + theme.type + ); + + const highlightColor = selectThemeVariant( + { + light: theme.colors.blueLight, + dark: theme.colors.blueShade, + }, + theme.type + ); + return { + wrapper: css` + border-top: 1px dashed ${borderColor}; + position: relative; + &:hover { + &:before { + background: ${highlightColor}; + } + } + &:before { + content: ''; + position: absolute; + top: 0; + z-index: 1; + left: -1px; + width: 2px; + height: 100%; + transition: background 0.5s cubic-bezier(0.19, 1, 0.22, 1); + } + `, + property: css` + padding: ${theme.spacing.xs} ${theme.spacing.sm} ${theme.spacing.sm} ${theme.spacing.sm}; + `, + }; +}); diff --git a/public/app/features/dashboard/components/PanelEditor/FieldConfigEditor.tsx b/public/app/features/dashboard/components/PanelEditor/FieldConfigEditor.tsx index 3cf54f72d78..3357a2efc04 100644 --- a/public/app/features/dashboard/components/PanelEditor/FieldConfigEditor.tsx +++ b/public/app/features/dashboard/components/PanelEditor/FieldConfigEditor.tsx @@ -5,13 +5,14 @@ import { FieldConfigSource, DataFrame, FieldPropertyEditorItem, - DynamicConfigValue, VariableSuggestionsScope, standardFieldConfigEditorRegistry, + SelectableValue, } from '@grafana/data'; import { Forms, fieldMatchersUI, ValuePicker } from '@grafana/ui'; import { getDataLinksVariableSuggestions } from '../../../panel/panellinks/link_srv'; import { OptionsGroup } from './OptionsGroup'; +import { OverrideEditor } from './OverrideEditor'; interface Props { config: FieldConfigSource; @@ -53,39 +54,34 @@ export class FieldConfigEditor extends React.PureComponent { }); }; - onMatcherConfigChange = (index: number, matcherConfig?: any) => { + onOverrideChange = (index: number, override: any) => { const { config } = this.props; let overrides = cloneDeep(config.overrides); - if (matcherConfig === undefined) { - overrides = overrides.splice(index, 1); - } else { - overrides[index].matcher.options = matcherConfig; - } + overrides[index] = override; this.props.onChange({ ...config, overrides }); }; - onDynamicConfigValueAdd = (index: number, prop: string, custom?: boolean) => { + onOverrideRemove = (overrideIndex: number) => { const { config } = this.props; let overrides = cloneDeep(config.overrides); - - const propertyConfig: DynamicConfigValue = { - prop, - custom, - }; - if (overrides[index].properties) { - overrides[index].properties.push(propertyConfig); - } else { - overrides[index].properties = [propertyConfig]; - } - + overrides.splice(overrideIndex, 1); this.props.onChange({ ...config, overrides }); }; - onDynamicConfigValueChange = (overrideIndex: number, propertyIndex: number, value?: any) => { - const { config } = this.props; - let overrides = cloneDeep(config.overrides); - overrides[overrideIndex].properties[propertyIndex].value = value; - this.props.onChange({ ...config, overrides }); + onOverrideAdd = (value: SelectableValue) => { + const { onChange, config } = this.props; + onChange({ + ...config, + overrides: [ + ...config.overrides, + { + matcher: { + id: value.value!, + }, + properties: [], + }, + ], + }); }; renderEditor(item: FieldPropertyEditorItem, custom: boolean) { @@ -151,55 +147,17 @@ export class FieldConfigEditor extends React.PureComponent { return (
{config.overrides.map((o, i) => { - const matcherUi = fieldMatchersUI.get(o.matcher.id); // TODO: apply matcher to retrieve fields return ( -
- - <> - this.onMatcherConfigChange(i, option)} - /> - -
- {o.properties.map((p, j) => { - const reg = p.custom ? custom : standardFieldConfigEditorRegistry; - const item = reg?.getIfExists(p.prop); - if (!item) { - return
Unknown property: {p.prop}
; - } - return ( - - { - this.onDynamicConfigValueChange(i, j, value); - }} - item={item} - context={{ - data, - getSuggestions: (scope?: VariableSuggestionsScope) => - getDataLinksVariableSuggestions(data, scope), - }} - /> - - ); - })} - { - this.onDynamicConfigValueAdd(i, o.value!, o.custom); - }} - /> -
- -
-
+ this.onOverrideChange(i, value)} + onRemove={() => this.onOverrideRemove(i)} + configPropertiesOptions={configPropertiesOptions} + customPropertiesRegistry={custom} + /> ); })}
@@ -211,22 +169,10 @@ export class FieldConfigEditor extends React.PureComponent { ({ label: i.name, value: i.id, description: i.description }))} - onChange={value => { - const { onChange, config } = this.props; - onChange({ - ...config, - overrides: [ - ...config.overrides, - { - matcher: { - id: value.value!, - }, - properties: [], - }, - ], - }); - }} + options={fieldMatchersUI + .list() + .map>(i => ({ label: i.name, value: i.id, description: i.description }))} + onChange={value => this.onOverrideAdd(value)} /> ); }; diff --git a/public/app/features/dashboard/components/PanelEditor/OverrideEditor.tsx b/public/app/features/dashboard/components/PanelEditor/OverrideEditor.tsx new file mode 100644 index 00000000000..77e69978e52 --- /dev/null +++ b/public/app/features/dashboard/components/PanelEditor/OverrideEditor.tsx @@ -0,0 +1,177 @@ +import React, { useCallback } from 'react'; +import { + ConfigOverrideRule, + DataFrame, + DynamicConfigValue, + FieldConfigEditorRegistry, + standardFieldConfigEditorRegistry, + VariableSuggestionsScope, + SelectableValue, + GrafanaTheme, +} 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'; + +interface OverrideEditorProps { + data: DataFrame[]; + override: ConfigOverrideRule; + onChange: (config: ConfigOverrideRule) => void; + onRemove: () => void; + customPropertiesRegistry?: FieldConfigEditorRegistry; + configPropertiesOptions: Array>; +} + +export const OverrideEditor: React.FC = ({ + data, + override, + onChange, + onRemove, + customPropertiesRegistry, + configPropertiesOptions, +}) => { + const theme = useTheme(); + const onMatcherConfigChange = useCallback( + (matcherConfig: any) => { + override.matcher.options = matcherConfig; + onChange(override); + }, + [override, onChange] + ); + + const onDynamicConfigValueChange = useCallback( + (index: number, value: DynamicConfigValue) => { + override.properties[index].value = value; + onChange(override); + }, + [override, onChange] + ); + + const onDynamicConfigValueRemove = useCallback( + (index: number) => { + override.properties.splice(index, 1); + onChange(override); + }, + [override, onChange] + ); + + const onDynamicConfigValueAdd = useCallback( + (prop: string, custom?: boolean) => { + const propertyConfig: DynamicConfigValue = { + prop, + custom, + }; + if (override.properties) { + override.properties.push(propertyConfig); + } else { + override.properties = [propertyConfig]; + } + onChange(override); + }, + [override, onChange] + ); + + const matcherUi = fieldMatchersUI.get(override.matcher.id); + const styles = getStyles(theme); + return ( +
+
+ +
+ onMatcherConfigChange(option)} + /> +
+
+
+ {override.properties.map((p, j) => { + const reg = p.custom ? customPropertiesRegistry : standardFieldConfigEditorRegistry; + const item = reg?.getIfExists(p.prop); + + if (!item) { + return
Unknown property: {p.prop}
; + } + + return ( +
+ onDynamicConfigValueChange(j, value)} + onRemove={() => onDynamicConfigValueRemove(j)} + property={p} + editorsRegistry={reg} + context={{ + data, + getSuggestions: (scope?: VariableSuggestionsScope) => getDataLinksVariableSuggestions(data, scope), + }} + /> +
+ ); + })} +
+ { + onDynamicConfigValueAdd(o.value, o.custom); + }} + /> +
+
+
+ ); +}; + +const getStyles = stylesFactory((theme: GrafanaTheme) => { + const borderColor = selectThemeVariant( + { + light: theme.colors.gray85, + dark: theme.colors.dark9, + }, + theme.type + ); + + const headerBg = selectThemeVariant( + { + light: theme.colors.white, + dark: theme.colors.dark1, + }, + theme.type + ); + + const shadow = selectThemeVariant( + { + light: theme.colors.gray85, + dark: theme.colors.black, + }, + 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; + `, + matcherUi: css` + padding: ${theme.spacing.sm}; + `, + propertyPickerWrapper: css` + border-top: 1px solid ${borderColor}; + `, + }; +}); diff --git a/public/app/features/dashboard/components/PanelEditor/OverrideHeader.tsx b/public/app/features/dashboard/components/PanelEditor/OverrideHeader.tsx new file mode 100644 index 00000000000..813e8f8e8c0 --- /dev/null +++ b/public/app/features/dashboard/components/PanelEditor/OverrideHeader.tsx @@ -0,0 +1,39 @@ +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}; + `, + }; +}); diff --git a/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx b/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx index 783d74c69d7..5fa8743c0a1 100644 --- a/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx +++ b/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx @@ -1,6 +1,6 @@ import React, { PureComponent } from 'react'; import { GrafanaTheme, FieldConfigSource, PanelData, PanelPlugin, SelectableValue } from '@grafana/data'; -import { stylesFactory, Forms, CustomScrollbar, selectThemeVariant } from '@grafana/ui'; +import { stylesFactory, Forms, CustomScrollbar, selectThemeVariant, Icon } from '@grafana/ui'; import { css, cx } from 'emotion'; import config from 'app/core/config'; import AutoSizer from 'react-virtualized-auto-sizer'; @@ -219,7 +219,7 @@ export class PanelEditorUnconnected extends PureComponent {
@@ -265,7 +265,11 @@ export class PanelEditorUnconnected extends PureComponent { > {this.renderHorizontalSplit(styles)}
- + {this.renderFieldOptions()} {this.renderVisSettings()}