mirror of
https://github.com/grafana/grafana.git
synced 2024-11-24 09:50:29 -06:00
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:
parent
278c312d58
commit
76827d2152
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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 && (
|
||||
|
19
packages/grafana-ui/src/components/Forms/Label.story.tsx
Normal file
19
packages/grafana-ui/src/components/Forms/Label.story.tsx
Normal 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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
|
@ -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];
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -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>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -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};
|
||||
`,
|
||||
};
|
||||
});
|
||||
|
@ -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]
|
||||
|
@ -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;
|
||||
})}
|
||||
</>
|
||||
);
|
||||
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user