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>
This commit is contained in:
Dominik Prokop 2020-04-09 21:22:43 +02:00 committed by GitHub
parent 278c312d58
commit 76827d2152
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 155 additions and 57 deletions

View File

@ -256,6 +256,7 @@ export class PanelPlugin<TOptions = any, TFieldConfigOptions extends object = an
for (const customProp of builder.getRegistry().list()) {
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
// problem is id (registry index) is used as property path
// so sort of need a property path on the FieldPropertyEditorItem

View File

@ -9,6 +9,7 @@ export interface OptionsEditorItem<TOptions, TSettings, TEditorProps, TValue> ex
path: (keyof TOptions & string) | string;
editor: ComponentType<TEditorProps>;
settings?: TSettings;
category?: string[];
defaultValue?: TValue;
showIf?: (currentConfig: TOptions) => boolean;
}

View File

@ -57,6 +57,7 @@ export interface FieldConfigEditorConfig<TOptions, TSettings = any, TValue = any
path: (keyof TOptions & string) | string;
name: string;
description: string;
category?: string[];
settings?: TSettings;
shouldApply?: (field: Field) => boolean;
defaultValue?: TValue;

View File

@ -123,6 +123,7 @@ export interface PanelOptionsEditorConfig<TOptions, TSettings = any, TValue = an
name: string;
description: string;
settings?: TSettings;
category?: string[];
defaultValue?: TValue;
showIf?: (currentConfig: TOptions) => boolean;
}

View File

@ -9,7 +9,7 @@ export interface FieldProps {
/** Form input element, i.e Input or Switch */
children: React.ReactElement;
/** Label for the field */
label?: string;
label?: string | JSX.Element;
/** Description of the field */
description?: string;
/** Indicates if field is in invalid state */
@ -71,14 +71,18 @@ export const Field: React.FC<FieldProps> = ({
// Retrieve input's id to apply on the label for correct click interaction
inputId = (child as React.ReactElement<{ id?: string }>).props.id;
}
const labelElement =
typeof label === 'string' ? (
<Label htmlFor={inputId} description={description}>
{`${label}${required ? ' *' : ''}`}
</Label>
) : (
label
);
return (
<div className={cx(styles.field, horizontal && styles.fieldHorizontal, className)}>
{label && (
<Label htmlFor={inputId} description={description}>
{`${label}${required ? ' *' : ''}`}
</Label>
)}
{labelElement}
<div>
{React.cloneElement(children, { invalid, disabled, loading })}
{invalid && error && !horizontal && (

View File

@ -0,0 +1,19 @@
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>
);
};

View File

@ -2,10 +2,13 @@ import React from 'react';
import { useTheme, stylesFactory } from '../../themes';
import { GrafanaTheme } from '@grafana/data';
import { css, cx } from 'emotion';
import { Icon } from '../Icon/Icon';
import tinycolor from 'tinycolor2';
export interface LabelProps extends React.LabelHTMLAttributes<HTMLLabelElement> {
children: string;
description?: string;
category?: string[];
}
export const getLabelStyles = stylesFactory((theme: GrafanaTheme) => {
@ -23,18 +26,42 @@ export const getLabelStyles = stylesFactory((theme: GrafanaTheme) => {
color: ${theme.colors.formDescription};
font-size: ${theme.typography.size.sm};
font-weight: ${theme.typography.weight.regular};
margin-top: ${theme.spacing.xxs};
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 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 (
<div className={cx(styles.label, className)}>
<label {...labelProps}>
{categories}
{children}
{description && <span className={styles.description}>{description}</span>}
</label>

View File

@ -32,6 +32,7 @@ import { StatsPickerEditor } from '../components/OptionsUI/stats';
* Returns collection of common field config properties definitions
*/
export const getStandardFieldConfigs = () => {
const category = ['Standard field options'];
const title: FieldConfigPropertyItem<any, string, StringFieldConfigSettings> = {
id: 'title',
path: 'title',
@ -45,6 +46,7 @@ export const getStandardFieldConfigs = () => {
expandTemplateVars: true,
},
shouldApply: field => field.type !== FieldType.time,
category,
};
const unit: FieldConfigPropertyItem<any, string, StringFieldConfigSettings> = {
@ -62,6 +64,7 @@ export const getStandardFieldConfigs = () => {
},
shouldApply: field => field.type === FieldType.number,
category,
};
const min: FieldConfigPropertyItem<any, number, NumberFieldConfigSettings> = {
@ -78,6 +81,7 @@ export const getStandardFieldConfigs = () => {
placeholder: 'auto',
},
shouldApply: field => field.type === FieldType.number,
category,
};
const max: FieldConfigPropertyItem<any, number, NumberFieldConfigSettings> = {
@ -95,6 +99,7 @@ export const getStandardFieldConfigs = () => {
},
shouldApply: field => field.type === FieldType.number,
category,
};
const decimals: FieldConfigPropertyItem<any, number, NumberFieldConfigSettings> = {
@ -115,6 +120,7 @@ export const getStandardFieldConfigs = () => {
},
shouldApply: field => field.type === FieldType.number,
category,
};
const thresholds: FieldConfigPropertyItem<any, ThresholdsConfig, ThresholdsFieldConfigSettings> = {
@ -135,6 +141,7 @@ export const getStandardFieldConfigs = () => {
],
},
shouldApply: field => field.type === FieldType.number,
category: ['Color & thresholds'],
};
const mappings: FieldConfigPropertyItem<any, ValueMapping[], ValueMappingFieldConfigSettings> = {
@ -149,6 +156,7 @@ export const getStandardFieldConfigs = () => {
settings: {},
defaultValue: [],
shouldApply: field => field.type === FieldType.number,
category: ['Value mappings'],
};
const noValue: FieldConfigPropertyItem<any, string, StringFieldConfigSettings> = {
@ -166,6 +174,7 @@ export const getStandardFieldConfigs = () => {
},
// ??? any optionsUi with no value
shouldApply: () => true,
category,
};
const links: FieldConfigPropertyItem<any, DataLink[], StringFieldConfigSettings> = {
@ -180,6 +189,7 @@ export const getStandardFieldConfigs = () => {
placeholder: '-',
},
shouldApply: () => true,
category: ['Data links'],
};
const color: FieldConfigPropertyItem<any, string, StringFieldConfigSettings> = {
@ -194,9 +204,10 @@ export const getStandardFieldConfigs = () => {
placeholder: '-',
},
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];
};
/**

View File

@ -12,6 +12,8 @@ import { Forms, fieldMatchersUI, ValuePicker, useTheme } from '@grafana/ui';
import { getDataLinksVariableSuggestions } from '../../../panel/panellinks/link_srv';
import { OverrideEditor } from './OverrideEditor';
import { css } from 'emotion';
import groupBy from 'lodash/groupBy';
import { OptionsGroup } from './OptionsGroup';
interface Props {
plugin: PanelPlugin;
@ -153,8 +155,14 @@ export const DefaultFieldConfigEditor: React.FC<Props> = ({ data, onChange, conf
: undefined
: (defaults as any)[item.path];
const label = (
<Forms.Label description={item.description} category={item.category?.slice(1)}>
{item.name}
</Forms.Label>
);
return (
<Forms.Field label={item.name} description={item.description} key={`${item.id}`}>
<Forms.Field label={label} key={`${item.id}/${item.isCustom}`}>
<item.editor
item={item}
value={value}
@ -170,6 +178,21 @@ export const DefaultFieldConfigEditor: React.FC<Props> = ({ data, onChange, conf
[config]
);
// render all field configs
return <>{plugin.fieldConfigRegistry.list().map(renderEditor)}</>;
const groupedConfigs = groupBy(plugin.fieldConfigRegistry.list(), i => i.category && i.category[0]);
return (
<>
{Object.keys(groupedConfigs).map(k => {
return (
<OptionsGroup title={k}>
<>
{groupedConfigs[k].map(c => {
return renderEditor(c);
})}
</>
</OptionsGroup>
);
})}
</>
);
};

View File

@ -16,10 +16,10 @@ export const OptionsGroup: FC<Props> = ({ title, children, defaultToClosed }) =>
return (
<div className={styles.box}>
<div className={styles.header} onClick={() => toggleExpand(!isExpanded)}>
{title}
<div className={cx(styles.toggle, 'editor-options-group-toggle')}>
<Icon name={isExpanded ? 'angle-down' : 'angle-left'} />
<Icon name={isExpanded ? 'angle-down' : 'angle-right'} />
</div>
{title}
</div>
{isExpanded && <div className={styles.body}>{children}</div>}
</div>
@ -34,13 +34,13 @@ const getStyles = stylesFactory((theme: GrafanaTheme, isExpanded: boolean) => {
toggle: css`
color: ${theme.colors.textWeak};
font-size: ${theme.typography.size.lg};
margin-right: ${theme.spacing.sm};
`,
header: css`
display: flex;
cursor: pointer;
justify-content: space-between;
align-items: center;
padding: ${theme.spacing.sm} ${theme.spacing.md};
align-items: baseline;
padding: ${theme.spacing.sm} ${theme.spacing.md} ${theme.spacing.sm} ${theme.spacing.sm};
color: ${isExpanded ? theme.colors.text : theme.colors.formLabel};
font-weight: ${theme.typography.weight.semibold};
@ -51,7 +51,7 @@ const getStyles = stylesFactory((theme: GrafanaTheme, isExpanded: boolean) => {
}
`,
body: css`
padding: 0 ${theme.spacing.md} ${theme.spacing.md} ${theme.spacing.md};
padding: 0 ${theme.spacing.md} ${theme.spacing.md} ${theme.spacing.xl};
`,
};
});

View File

@ -2,18 +2,7 @@ import React, { useCallback, useState, CSSProperties } from 'react';
import Transition from 'react-transition-group/Transition';
import { FieldConfigSource, GrafanaTheme, PanelData, PanelPlugin, SelectableValue } from '@grafana/data';
import { DashboardModel, PanelModel } from '../../state';
import {
CustomScrollbar,
stylesFactory,
Tab,
TabContent,
TabsBar,
Select,
useTheme,
Container,
Icon,
Input,
} from '@grafana/ui';
import { CustomScrollbar, stylesFactory, Tab, TabContent, TabsBar, Select, useTheme, Icon, Input } from '@grafana/ui';
import { DefaultFieldConfigEditor, OverrideFieldConfigEditor } from './FieldConfigEditor';
import { css } from 'emotion';
import { PanelOptionsTab } from './PanelOptionsTab';
@ -54,14 +43,12 @@ export const OptionsPaneContent: React.FC<{
}
return (
<Container padding="md">
<DefaultFieldConfigEditor
config={fieldConfig}
plugin={plugin}
onChange={onFieldConfigsChange}
data={data.series}
/>
</Container>
<DefaultFieldConfigEditor
config={fieldConfig}
plugin={plugin}
onChange={onFieldConfigsChange}
data={data.series}
/>
);
},
[data, plugin, panel, onFieldConfigsChange]

View File

@ -1,7 +1,9 @@
import React, { useMemo } from 'react';
import { PanelOptionsEditorItem, PanelPlugin } from '@grafana/data';
import { set as lodashSet, get as lodashGet } from 'lodash';
import { PanelPlugin } from '@grafana/data';
import { Forms } from '@grafana/ui';
import groupBy from 'lodash/groupBy';
import { OptionsGroup } from './OptionsGroup';
interface PanelOptionsEditorProps<TOptions> {
plugin: PanelPlugin;
@ -10,7 +12,11 @@ interface PanelOptionsEditorProps<TOptions> {
}
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 newOptions = lodashSet({ ...options }, key, value);
@ -19,16 +25,35 @@ export const PanelOptionsEditor: React.FC<PanelOptionsEditorProps<any>> = ({ plu
return (
<>
{optionEditors.list().map(e => {
if (e.showIf && !e.showIf(options)) {
return null;
}
{Object.keys(optionEditors).map(c => {
const optionsToShow = optionEditors[c]
.map((e, i) => {
if (e.showIf && !e.showIf(options)) {
return null;
}
return (
<Forms.Field label={e.name} description={e.description} key={e.id}>
<e.editor value={lodashGet(options, e.path)} onChange={value => onOptionChange(e.path, value)} item={e} />
</Forms.Field>
);
const label = (
<Forms.Label description={e.description} category={e.category?.slice(1)}>
{e.name}
</Forms.Label>
);
return (
<Forms.Field label={label} key={`${e.id}/i`}>
<e.editor
value={lodashGet(options, e.path)}
onChange={value => onOptionChange(e.path, value)}
item={e}
/>
</Forms.Field>
);
})
.filter(e => e !== null);
return optionsToShow.length > 0 ? (
<OptionsGroup title={c} defaultToClosed>
<div>{optionsToShow}</div>
</OptionsGroup>
) : null;
})}
</>
);

View File

@ -73,14 +73,12 @@ export const PanelOptionsTab: FC<Props> = ({
if (plugin.optionEditors && panel) {
elements.push(
<OptionsGroup title="Display" key="panel plugin options">
<PanelOptionsEditor
key="panel options"
options={panel.getOptions()}
onChange={onPanelOptionsChanged}
plugin={plugin}
/>
</OptionsGroup>
<PanelOptionsEditor
key="panel options"
options={panel.getOptions()}
onChange={onPanelOptionsChanged}
plugin={plugin}
/>
);
}

View File

@ -70,7 +70,7 @@ else
if [ "${CIRCLE_BRANCH}" == "master" ]; then
exit_if_fail ./scripts/ci-metrics-publisher.sh "grafana.ci-buildtimes.$CIRCLE_JOB.$PACKAGE=$runtime"
fi
exit_status=$?
if [ $exit_status -eq 0 ]; then
unpublish_previous_canary "$PACKAGE"