Commit 76827d21 by Dominik Prokop Committed by GitHub

NewPanelEdit: General options categorisation (#23145)

* First bar gauge panel option

* Update doc comments

* Minor changes

* progress

* Minor type updates

* Fixing typing errors

* Fix that TS!

* Bring satisfaction to that beast called typescript

* Prototype

* Remove import

* Experimenting with different named categories

* Experimenting with category naming

* Naming is very hard

* merge master

* Remove commented code

* Fix merge

* Categorise panel options into collapsible sections

* Remove categories from table panel

Co-authored-by: Torkel Ödegaard <torkel@grafana.com>
parent 278c312d
...@@ -256,6 +256,7 @@ export class PanelPlugin<TOptions = any, TFieldConfigOptions extends object = an ...@@ -256,6 +256,7 @@ export class PanelPlugin<TOptions = any, TFieldConfigOptions extends object = an
for (const customProp of builder.getRegistry().list()) { for (const customProp of builder.getRegistry().list()) {
customProp.isCustom = true; customProp.isCustom = true;
customProp.category = ['Custom field options'].concat(customProp.category || []);
// need to do something to make the custom items not conflict with standard ones // need to do something to make the custom items not conflict with standard ones
// problem is id (registry index) is used as property path // problem is id (registry index) is used as property path
// so sort of need a property path on the FieldPropertyEditorItem // so sort of need a property path on the FieldPropertyEditorItem
......
...@@ -9,6 +9,7 @@ export interface OptionsEditorItem<TOptions, TSettings, TEditorProps, TValue> ex ...@@ -9,6 +9,7 @@ export interface OptionsEditorItem<TOptions, TSettings, TEditorProps, TValue> ex
path: (keyof TOptions & string) | string; path: (keyof TOptions & string) | string;
editor: ComponentType<TEditorProps>; editor: ComponentType<TEditorProps>;
settings?: TSettings; settings?: TSettings;
category?: string[];
defaultValue?: TValue; defaultValue?: TValue;
showIf?: (currentConfig: TOptions) => boolean; showIf?: (currentConfig: TOptions) => boolean;
} }
......
...@@ -57,6 +57,7 @@ export interface FieldConfigEditorConfig<TOptions, TSettings = any, TValue = any ...@@ -57,6 +57,7 @@ export interface FieldConfigEditorConfig<TOptions, TSettings = any, TValue = any
path: (keyof TOptions & string) | string; path: (keyof TOptions & string) | string;
name: string; name: string;
description: string; description: string;
category?: string[];
settings?: TSettings; settings?: TSettings;
shouldApply?: (field: Field) => boolean; shouldApply?: (field: Field) => boolean;
defaultValue?: TValue; defaultValue?: TValue;
......
...@@ -123,6 +123,7 @@ export interface PanelOptionsEditorConfig<TOptions, TSettings = any, TValue = an ...@@ -123,6 +123,7 @@ export interface PanelOptionsEditorConfig<TOptions, TSettings = any, TValue = an
name: string; name: string;
description: string; description: string;
settings?: TSettings; settings?: TSettings;
category?: string[];
defaultValue?: TValue; defaultValue?: TValue;
showIf?: (currentConfig: TOptions) => boolean; showIf?: (currentConfig: TOptions) => boolean;
} }
......
...@@ -9,7 +9,7 @@ export interface FieldProps { ...@@ -9,7 +9,7 @@ export interface FieldProps {
/** Form input element, i.e Input or Switch */ /** Form input element, i.e Input or Switch */
children: React.ReactElement; children: React.ReactElement;
/** Label for the field */ /** Label for the field */
label?: string; label?: string | JSX.Element;
/** Description of the field */ /** Description of the field */
description?: string; description?: string;
/** Indicates if field is in invalid state */ /** Indicates if field is in invalid state */
...@@ -71,14 +71,18 @@ export const Field: React.FC<FieldProps> = ({ ...@@ -71,14 +71,18 @@ export const Field: React.FC<FieldProps> = ({
// Retrieve input's id to apply on the label for correct click interaction // Retrieve input's id to apply on the label for correct click interaction
inputId = (child as React.ReactElement<{ id?: string }>).props.id; inputId = (child as React.ReactElement<{ id?: string }>).props.id;
} }
const labelElement =
return ( typeof label === 'string' ? (
<div className={cx(styles.field, horizontal && styles.fieldHorizontal, className)}>
{label && (
<Label htmlFor={inputId} description={description}> <Label htmlFor={inputId} description={description}>
{`${label}${required ? ' *' : ''}`} {`${label}${required ? ' *' : ''}`}
</Label> </Label>
)} ) : (
label
);
return (
<div className={cx(styles.field, horizontal && styles.fieldHorizontal, className)}>
{labelElement}
<div> <div>
{React.cloneElement(children, { invalid, disabled, loading })} {React.cloneElement(children, { invalid, disabled, loading })}
{invalid && error && !horizontal && ( {invalid && error && !horizontal && (
......
import React from 'react';
import { Label } from './Label';
export default {
title: 'Forms/Label',
component: Label,
};
export const simple = () => {
return <Label description="Opton description">Option name</Label>;
};
export const categorised = () => {
return (
<Label category={['Category', 'Nested category']} description="Opton description">
Option name
</Label>
);
};
...@@ -2,10 +2,13 @@ import React from 'react'; ...@@ -2,10 +2,13 @@ import React from 'react';
import { useTheme, stylesFactory } from '../../themes'; import { useTheme, stylesFactory } from '../../themes';
import { GrafanaTheme } from '@grafana/data'; import { GrafanaTheme } from '@grafana/data';
import { css, cx } from 'emotion'; import { css, cx } from 'emotion';
import { Icon } from '../Icon/Icon';
import tinycolor from 'tinycolor2';
export interface LabelProps extends React.LabelHTMLAttributes<HTMLLabelElement> { export interface LabelProps extends React.LabelHTMLAttributes<HTMLLabelElement> {
children: string; children: string;
description?: string; description?: string;
category?: string[];
} }
export const getLabelStyles = stylesFactory((theme: GrafanaTheme) => { export const getLabelStyles = stylesFactory((theme: GrafanaTheme) => {
...@@ -23,18 +26,42 @@ export const getLabelStyles = stylesFactory((theme: GrafanaTheme) => { ...@@ -23,18 +26,42 @@ export const getLabelStyles = stylesFactory((theme: GrafanaTheme) => {
color: ${theme.colors.formDescription}; color: ${theme.colors.formDescription};
font-size: ${theme.typography.size.sm}; font-size: ${theme.typography.size.sm};
font-weight: ${theme.typography.weight.regular}; font-weight: ${theme.typography.weight.regular};
margin-top: ${theme.spacing.xxs};
display: block; display: block;
`, `,
categories: css`
color: ${theme.isLight
? tinycolor(theme.colors.formLabel)
.lighten(10)
.toHexString()
: tinycolor(theme.colors.formLabel)
.darken(10)
.toHexString()};
display: inline-flex;
align-items: center;
`,
chevron: css`
margin: 0 ${theme.spacing.xxs};
`,
}; };
}); });
export const Label: React.FC<LabelProps> = ({ children, description, className, ...labelProps }) => { export const Label: React.FC<LabelProps> = ({ children, description, className, category, ...labelProps }) => {
const theme = useTheme(); const theme = useTheme();
const styles = getLabelStyles(theme); const styles = getLabelStyles(theme);
const categories = category?.map(c => {
return (
<span className={styles.categories}>
<span>{c}</span>
<Icon name="angle-right" className={styles.chevron} />
</span>
);
});
return ( return (
<div className={cx(styles.label, className)}> <div className={cx(styles.label, className)}>
<label {...labelProps}> <label {...labelProps}>
{categories}
{children} {children}
{description && <span className={styles.description}>{description}</span>} {description && <span className={styles.description}>{description}</span>}
</label> </label>
......
...@@ -32,6 +32,7 @@ import { StatsPickerEditor } from '../components/OptionsUI/stats'; ...@@ -32,6 +32,7 @@ import { StatsPickerEditor } from '../components/OptionsUI/stats';
* Returns collection of common field config properties definitions * Returns collection of common field config properties definitions
*/ */
export const getStandardFieldConfigs = () => { export const getStandardFieldConfigs = () => {
const category = ['Standard field options'];
const title: FieldConfigPropertyItem<any, string, StringFieldConfigSettings> = { const title: FieldConfigPropertyItem<any, string, StringFieldConfigSettings> = {
id: 'title', id: 'title',
path: 'title', path: 'title',
...@@ -45,6 +46,7 @@ export const getStandardFieldConfigs = () => { ...@@ -45,6 +46,7 @@ export const getStandardFieldConfigs = () => {
expandTemplateVars: true, expandTemplateVars: true,
}, },
shouldApply: field => field.type !== FieldType.time, shouldApply: field => field.type !== FieldType.time,
category,
}; };
const unit: FieldConfigPropertyItem<any, string, StringFieldConfigSettings> = { const unit: FieldConfigPropertyItem<any, string, StringFieldConfigSettings> = {
...@@ -62,6 +64,7 @@ export const getStandardFieldConfigs = () => { ...@@ -62,6 +64,7 @@ export const getStandardFieldConfigs = () => {
}, },
shouldApply: field => field.type === FieldType.number, shouldApply: field => field.type === FieldType.number,
category,
}; };
const min: FieldConfigPropertyItem<any, number, NumberFieldConfigSettings> = { const min: FieldConfigPropertyItem<any, number, NumberFieldConfigSettings> = {
...@@ -78,6 +81,7 @@ export const getStandardFieldConfigs = () => { ...@@ -78,6 +81,7 @@ export const getStandardFieldConfigs = () => {
placeholder: 'auto', placeholder: 'auto',
}, },
shouldApply: field => field.type === FieldType.number, shouldApply: field => field.type === FieldType.number,
category,
}; };
const max: FieldConfigPropertyItem<any, number, NumberFieldConfigSettings> = { const max: FieldConfigPropertyItem<any, number, NumberFieldConfigSettings> = {
...@@ -95,6 +99,7 @@ export const getStandardFieldConfigs = () => { ...@@ -95,6 +99,7 @@ export const getStandardFieldConfigs = () => {
}, },
shouldApply: field => field.type === FieldType.number, shouldApply: field => field.type === FieldType.number,
category,
}; };
const decimals: FieldConfigPropertyItem<any, number, NumberFieldConfigSettings> = { const decimals: FieldConfigPropertyItem<any, number, NumberFieldConfigSettings> = {
...@@ -115,6 +120,7 @@ export const getStandardFieldConfigs = () => { ...@@ -115,6 +120,7 @@ export const getStandardFieldConfigs = () => {
}, },
shouldApply: field => field.type === FieldType.number, shouldApply: field => field.type === FieldType.number,
category,
}; };
const thresholds: FieldConfigPropertyItem<any, ThresholdsConfig, ThresholdsFieldConfigSettings> = { const thresholds: FieldConfigPropertyItem<any, ThresholdsConfig, ThresholdsFieldConfigSettings> = {
...@@ -135,6 +141,7 @@ export const getStandardFieldConfigs = () => { ...@@ -135,6 +141,7 @@ export const getStandardFieldConfigs = () => {
], ],
}, },
shouldApply: field => field.type === FieldType.number, shouldApply: field => field.type === FieldType.number,
category: ['Color & thresholds'],
}; };
const mappings: FieldConfigPropertyItem<any, ValueMapping[], ValueMappingFieldConfigSettings> = { const mappings: FieldConfigPropertyItem<any, ValueMapping[], ValueMappingFieldConfigSettings> = {
...@@ -149,6 +156,7 @@ export const getStandardFieldConfigs = () => { ...@@ -149,6 +156,7 @@ export const getStandardFieldConfigs = () => {
settings: {}, settings: {},
defaultValue: [], defaultValue: [],
shouldApply: field => field.type === FieldType.number, shouldApply: field => field.type === FieldType.number,
category: ['Value mappings'],
}; };
const noValue: FieldConfigPropertyItem<any, string, StringFieldConfigSettings> = { const noValue: FieldConfigPropertyItem<any, string, StringFieldConfigSettings> = {
...@@ -166,6 +174,7 @@ export const getStandardFieldConfigs = () => { ...@@ -166,6 +174,7 @@ export const getStandardFieldConfigs = () => {
}, },
// ??? any optionsUi with no value // ??? any optionsUi with no value
shouldApply: () => true, shouldApply: () => true,
category,
}; };
const links: FieldConfigPropertyItem<any, DataLink[], StringFieldConfigSettings> = { const links: FieldConfigPropertyItem<any, DataLink[], StringFieldConfigSettings> = {
...@@ -180,6 +189,7 @@ export const getStandardFieldConfigs = () => { ...@@ -180,6 +189,7 @@ export const getStandardFieldConfigs = () => {
placeholder: '-', placeholder: '-',
}, },
shouldApply: () => true, shouldApply: () => true,
category: ['Data links'],
}; };
const color: FieldConfigPropertyItem<any, string, StringFieldConfigSettings> = { const color: FieldConfigPropertyItem<any, string, StringFieldConfigSettings> = {
...@@ -194,9 +204,10 @@ export const getStandardFieldConfigs = () => { ...@@ -194,9 +204,10 @@ export const getStandardFieldConfigs = () => {
placeholder: '-', placeholder: '-',
}, },
shouldApply: () => true, shouldApply: () => true,
category: ['Color & thresholds'],
}; };
return [unit, min, max, decimals, title, noValue, thresholds, mappings, links, color]; return [unit, min, max, decimals, title, noValue, color, thresholds, mappings, links];
}; };
/** /**
......
...@@ -12,6 +12,8 @@ import { Forms, fieldMatchersUI, ValuePicker, useTheme } from '@grafana/ui'; ...@@ -12,6 +12,8 @@ import { Forms, fieldMatchersUI, ValuePicker, useTheme } from '@grafana/ui';
import { getDataLinksVariableSuggestions } from '../../../panel/panellinks/link_srv'; import { getDataLinksVariableSuggestions } from '../../../panel/panellinks/link_srv';
import { OverrideEditor } from './OverrideEditor'; import { OverrideEditor } from './OverrideEditor';
import { css } from 'emotion'; import { css } from 'emotion';
import groupBy from 'lodash/groupBy';
import { OptionsGroup } from './OptionsGroup';
interface Props { interface Props {
plugin: PanelPlugin; plugin: PanelPlugin;
...@@ -153,8 +155,14 @@ export const DefaultFieldConfigEditor: React.FC<Props> = ({ data, onChange, conf ...@@ -153,8 +155,14 @@ export const DefaultFieldConfigEditor: React.FC<Props> = ({ data, onChange, conf
: undefined : undefined
: (defaults as any)[item.path]; : (defaults as any)[item.path];
const label = (
<Forms.Label description={item.description} category={item.category?.slice(1)}>
{item.name}
</Forms.Label>
);
return ( return (
<Forms.Field label={item.name} description={item.description} key={`${item.id}`}> <Forms.Field label={label} key={`${item.id}/${item.isCustom}`}>
<item.editor <item.editor
item={item} item={item}
value={value} value={value}
...@@ -170,6 +178,21 @@ export const DefaultFieldConfigEditor: React.FC<Props> = ({ data, onChange, conf ...@@ -170,6 +178,21 @@ export const DefaultFieldConfigEditor: React.FC<Props> = ({ data, onChange, conf
[config] [config]
); );
// render all field configs const groupedConfigs = groupBy(plugin.fieldConfigRegistry.list(), i => i.category && i.category[0]);
return <>{plugin.fieldConfigRegistry.list().map(renderEditor)}</>;
return (
<>
{Object.keys(groupedConfigs).map(k => {
return (
<OptionsGroup title={k}>
<>
{groupedConfigs[k].map(c => {
return renderEditor(c);
})}
</>
</OptionsGroup>
);
})}
</>
);
}; };
...@@ -16,10 +16,10 @@ export const OptionsGroup: FC<Props> = ({ title, children, defaultToClosed }) => ...@@ -16,10 +16,10 @@ export const OptionsGroup: FC<Props> = ({ title, children, defaultToClosed }) =>
return ( return (
<div className={styles.box}> <div className={styles.box}>
<div className={styles.header} onClick={() => toggleExpand(!isExpanded)}> <div className={styles.header} onClick={() => toggleExpand(!isExpanded)}>
{title}
<div className={cx(styles.toggle, 'editor-options-group-toggle')}> <div className={cx(styles.toggle, 'editor-options-group-toggle')}>
<Icon name={isExpanded ? 'angle-down' : 'angle-left'} /> <Icon name={isExpanded ? 'angle-down' : 'angle-right'} />
</div> </div>
{title}
</div> </div>
{isExpanded && <div className={styles.body}>{children}</div>} {isExpanded && <div className={styles.body}>{children}</div>}
</div> </div>
...@@ -34,13 +34,13 @@ const getStyles = stylesFactory((theme: GrafanaTheme, isExpanded: boolean) => { ...@@ -34,13 +34,13 @@ const getStyles = stylesFactory((theme: GrafanaTheme, isExpanded: boolean) => {
toggle: css` toggle: css`
color: ${theme.colors.textWeak}; color: ${theme.colors.textWeak};
font-size: ${theme.typography.size.lg}; font-size: ${theme.typography.size.lg};
margin-right: ${theme.spacing.sm};
`, `,
header: css` header: css`
display: flex; display: flex;
cursor: pointer; cursor: pointer;
justify-content: space-between; align-items: baseline;
align-items: center; padding: ${theme.spacing.sm} ${theme.spacing.md} ${theme.spacing.sm} ${theme.spacing.sm};
padding: ${theme.spacing.sm} ${theme.spacing.md};
color: ${isExpanded ? theme.colors.text : theme.colors.formLabel}; color: ${isExpanded ? theme.colors.text : theme.colors.formLabel};
font-weight: ${theme.typography.weight.semibold}; font-weight: ${theme.typography.weight.semibold};
...@@ -51,7 +51,7 @@ const getStyles = stylesFactory((theme: GrafanaTheme, isExpanded: boolean) => { ...@@ -51,7 +51,7 @@ const getStyles = stylesFactory((theme: GrafanaTheme, isExpanded: boolean) => {
} }
`, `,
body: css` body: css`
padding: 0 ${theme.spacing.md} ${theme.spacing.md} ${theme.spacing.md}; padding: 0 ${theme.spacing.md} ${theme.spacing.md} ${theme.spacing.xl};
`, `,
}; };
}); });
...@@ -2,18 +2,7 @@ import React, { useCallback, useState, CSSProperties } from 'react'; ...@@ -2,18 +2,7 @@ import React, { useCallback, useState, CSSProperties } from 'react';
import Transition from 'react-transition-group/Transition'; import Transition from 'react-transition-group/Transition';
import { FieldConfigSource, GrafanaTheme, PanelData, PanelPlugin, SelectableValue } from '@grafana/data'; import { FieldConfigSource, GrafanaTheme, PanelData, PanelPlugin, SelectableValue } from '@grafana/data';
import { DashboardModel, PanelModel } from '../../state'; import { DashboardModel, PanelModel } from '../../state';
import { import { CustomScrollbar, stylesFactory, Tab, TabContent, TabsBar, Select, useTheme, Icon, Input } from '@grafana/ui';
CustomScrollbar,
stylesFactory,
Tab,
TabContent,
TabsBar,
Select,
useTheme,
Container,
Icon,
Input,
} from '@grafana/ui';
import { DefaultFieldConfigEditor, OverrideFieldConfigEditor } from './FieldConfigEditor'; import { DefaultFieldConfigEditor, OverrideFieldConfigEditor } from './FieldConfigEditor';
import { css } from 'emotion'; import { css } from 'emotion';
import { PanelOptionsTab } from './PanelOptionsTab'; import { PanelOptionsTab } from './PanelOptionsTab';
...@@ -54,14 +43,12 @@ export const OptionsPaneContent: React.FC<{ ...@@ -54,14 +43,12 @@ export const OptionsPaneContent: React.FC<{
} }
return ( return (
<Container padding="md">
<DefaultFieldConfigEditor <DefaultFieldConfigEditor
config={fieldConfig} config={fieldConfig}
plugin={plugin} plugin={plugin}
onChange={onFieldConfigsChange} onChange={onFieldConfigsChange}
data={data.series} data={data.series}
/> />
</Container>
); );
}, },
[data, plugin, panel, onFieldConfigsChange] [data, plugin, panel, onFieldConfigsChange]
......
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import { PanelOptionsEditorItem, PanelPlugin } from '@grafana/data';
import { set as lodashSet, get as lodashGet } from 'lodash'; import { set as lodashSet, get as lodashGet } from 'lodash';
import { PanelPlugin } from '@grafana/data';
import { Forms } from '@grafana/ui'; import { Forms } from '@grafana/ui';
import groupBy from 'lodash/groupBy';
import { OptionsGroup } from './OptionsGroup';
interface PanelOptionsEditorProps<TOptions> { interface PanelOptionsEditorProps<TOptions> {
plugin: PanelPlugin; plugin: PanelPlugin;
...@@ -10,7 +12,11 @@ interface PanelOptionsEditorProps<TOptions> { ...@@ -10,7 +12,11 @@ interface PanelOptionsEditorProps<TOptions> {
} }
export const PanelOptionsEditor: React.FC<PanelOptionsEditorProps<any>> = ({ plugin, options, onChange }) => { export const PanelOptionsEditor: React.FC<PanelOptionsEditorProps<any>> = ({ plugin, options, onChange }) => {
const optionEditors = useMemo(() => plugin.optionEditors, [plugin]); const optionEditors = useMemo<Record<string, PanelOptionsEditorItem[]>>(() => {
return groupBy(plugin.optionEditors.list(), i => {
return i.category ? i.category[0] : 'Display';
});
}, [plugin]);
const onOptionChange = (key: string, value: any) => { const onOptionChange = (key: string, value: any) => {
const newOptions = lodashSet({ ...options }, key, value); const newOptions = lodashSet({ ...options }, key, value);
...@@ -19,16 +25,35 @@ export const PanelOptionsEditor: React.FC<PanelOptionsEditorProps<any>> = ({ plu ...@@ -19,16 +25,35 @@ export const PanelOptionsEditor: React.FC<PanelOptionsEditorProps<any>> = ({ plu
return ( return (
<> <>
{optionEditors.list().map(e => { {Object.keys(optionEditors).map(c => {
const optionsToShow = optionEditors[c]
.map((e, i) => {
if (e.showIf && !e.showIf(options)) { if (e.showIf && !e.showIf(options)) {
return null; return null;
} }
const label = (
<Forms.Label description={e.description} category={e.category?.slice(1)}>
{e.name}
</Forms.Label>
);
return ( return (
<Forms.Field label={e.name} description={e.description} key={e.id}> <Forms.Field label={label} key={`${e.id}/i`}>
<e.editor value={lodashGet(options, e.path)} onChange={value => onOptionChange(e.path, value)} item={e} /> <e.editor
value={lodashGet(options, e.path)}
onChange={value => onOptionChange(e.path, value)}
item={e}
/>
</Forms.Field> </Forms.Field>
); );
})
.filter(e => e !== null);
return optionsToShow.length > 0 ? (
<OptionsGroup title={c} defaultToClosed>
<div>{optionsToShow}</div>
</OptionsGroup>
) : null;
})} })}
</> </>
); );
......
...@@ -73,14 +73,12 @@ export const PanelOptionsTab: FC<Props> = ({ ...@@ -73,14 +73,12 @@ export const PanelOptionsTab: FC<Props> = ({
if (plugin.optionEditors && panel) { if (plugin.optionEditors && panel) {
elements.push( elements.push(
<OptionsGroup title="Display" key="panel plugin options">
<PanelOptionsEditor <PanelOptionsEditor
key="panel options" key="panel options"
options={panel.getOptions()} options={panel.getOptions()}
onChange={onPanelOptionsChanged} onChange={onPanelOptionsChanged}
plugin={plugin} plugin={plugin}
/> />
</OptionsGroup>
); );
} }
......
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