Commit e846a26c by Dominik Prokop Committed by GitHub

FieldOverrides: FieldOverrides UI (#22187)

parent f2fc7aa3
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<T> {
......@@ -12,15 +12,16 @@ interface ValuePickerProps<T> {
/** ValuePicker options */
options: Array<SelectableValue<T>>;
onChange: (value: SelectableValue<T>) => void;
variant?: ButtonVariant;
}
export function ValuePicker<T>({ label, icon, options, onChange }: ValuePickerProps<T>) {
export function ValuePicker<T>({ label, icon, options, onChange, variant }: ValuePickerProps<T>) {
const [isPicking, setIsPicking] = useState(false);
return (
<>
{!isPicking && (
<Button icon={`fa fa-${icon || 'plus'}`} onClick={() => setIsPicking(true)}>
<Button onClick={() => setIsPicking(true)} variant={variant} icon={`fa fa-${icon}`}>
{label}
</Button>
)}
......
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<DynamicConfigValueEditorProps> = ({
property,
context,
editorsRegistry,
onChange,
onRemove,
}) => {
const theme = useTheme();
const styles = getStyles(theme);
const item = editorsRegistry?.getIfExists(property.prop);
return (
<div className={styles.wrapper}>
<OverrideHeader onRemove={onRemove} title={item.name} description={item.description} />
<div className={styles.property}>
<item.override
value={property.value}
onChange={value => {
onChange(value);
}}
item={item}
context={context}
/>
</div>
</div>
);
};
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};
`,
};
});
......@@ -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<Props> {
});
};
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<string>) => {
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<Props> {
return (
<div>
{config.overrides.map((o, i) => {
const matcherUi = fieldMatchersUI.get(o.matcher.id);
// TODO: apply matcher to retrieve fields
return (
<div key={`${o.matcher.id}/${i}`} style={{ border: `2px solid red`, marginBottom: '10px' }}>
<Forms.Field label={matcherUi.name} description={matcherUi.description}>
<>
<matcherUi.component
matcher={matcherUi.matcher}
data={data}
options={o.matcher.options}
onChange={option => this.onMatcherConfigChange(i, option)}
/>
<div style={{ border: `2px solid blue`, marginBottom: '5px' }}>
{o.properties.map((p, j) => {
const reg = p.custom ? custom : standardFieldConfigEditorRegistry;
const item = reg?.getIfExists(p.prop);
if (!item) {
return <div>Unknown property: {p.prop}</div>;
}
return (
<Forms.Field label={item.name} description={item.description}>
<item.override
value={p.value}
onChange={value => {
this.onDynamicConfigValueChange(i, j, value);
}}
item={item}
context={{
data,
getSuggestions: (scope?: VariableSuggestionsScope) =>
getDataLinksVariableSuggestions(data, scope),
}}
/>
</Forms.Field>
);
})}
<ValuePicker
icon="plus"
label="Set config property"
options={configPropertiesOptions}
onChange={o => {
this.onDynamicConfigValueAdd(i, o.value!, o.custom);
}}
/>
</div>
</>
</Forms.Field>
</div>
<OverrideEditor
key={`${o.matcher.id}/${i}`}
data={data}
override={o}
onChange={value => this.onOverrideChange(i, value)}
onRemove={() => this.onOverrideRemove(i)}
configPropertiesOptions={configPropertiesOptions}
customPropertiesRegistry={custom}
/>
);
})}
</div>
......@@ -211,22 +169,10 @@ export class FieldConfigEditor extends React.PureComponent<Props> {
<ValuePicker
icon="plus"
label="Add override"
options={fieldMatchersUI.list().map(i => ({ 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<SelectableValue<string>>(i => ({ label: i.name, value: i.id, description: i.description }))}
onChange={value => this.onOverrideAdd(value)}
/>
);
};
......
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<SelectableValue<string>>;
}
export const OverrideEditor: React.FC<OverrideEditorProps> = ({
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 (
<div className={styles.wrapper}>
<div className={styles.headerWrapper}>
<OverrideHeader onRemove={onRemove} title={matcherUi.name} description={matcherUi.description} />
<div className={styles.matcherUi}>
<matcherUi.component
matcher={matcherUi.matcher}
data={data}
options={override.matcher.options}
onChange={option => onMatcherConfigChange(option)}
/>
</div>
</div>
<div>
{override.properties.map((p, j) => {
const reg = p.custom ? customPropertiesRegistry : standardFieldConfigEditorRegistry;
const item = reg?.getIfExists(p.prop);
if (!item) {
return <div>Unknown property: {p.prop}</div>;
}
return (
<div key={`${p.prop}/${j}`}>
<DynamicConfigValueEditor
onChange={value => onDynamicConfigValueChange(j, value)}
onRemove={() => onDynamicConfigValueRemove(j)}
property={p}
editorsRegistry={reg}
context={{
data,
getSuggestions: (scope?: VariableSuggestionsScope) => getDataLinksVariableSuggestions(data, scope),
}}
/>
</div>
);
})}
<div className={styles.propertyPickerWrapper}>
<ValuePicker
label="Set config property"
icon="plus"
options={configPropertiesOptions}
variant={'link'}
onChange={o => {
onDynamicConfigValueAdd(o.value, o.custom);
}}
/>
</div>
</div>
</div>
);
};
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};
`,
};
});
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<OverrideHeaderProps> = ({ title, description, onRemove }) => {
const theme = useTheme();
const styles = getOverrideHeaderStyles(theme);
return (
<div className={styles.header}>
<Forms.Label description={description}>{title}</Forms.Label>
<div className={styles.remove} onClick={() => onRemove()}>
<Icon name="trash" />
</div>
</div>
);
};
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};
`,
};
});
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<Props> {
<div className={styles.toolbar}>
<div className={styles.toolbarLeft}>
<button className="navbar-edit__back-btn" onClick={this.onPanelExit}>
<i className="fa fa-arrow-left"></i>
<Icon name="arrow-left" />
</button>
<PanelTitle value={panel.title} onChange={this.onPanelTitleChange} />
</div>
......@@ -265,7 +265,11 @@ export class PanelEditorUnconnected extends PureComponent<Props> {
>
{this.renderHorizontalSplit(styles)}
<div className={styles.panelOptionsPane}>
<CustomScrollbar>
<CustomScrollbar
className={css`
height: 100% !important;
`}
>
{this.renderFieldOptions()}
<OptionsGroup title="Old settings">{this.renderVisSettings()}</OptionsGroup>
</CustomScrollbar>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment