mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -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()) {
|
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
|
|||||||
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
|
|||||||
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
|
|||||||
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 {
|
|||||||
/** 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> = ({
|
|||||||
// 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 && (
|
||||||
|
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 { 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) => {
|
|||||||
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';
|
|||||||
* 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 = () => {
|
|||||||
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 = () => {
|
|||||||
},
|
},
|
||||||
|
|
||||||
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 = () => {
|
|||||||
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 = () => {
|
|||||||
},
|
},
|
||||||
|
|
||||||
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 = () => {
|
|||||||
},
|
},
|
||||||
|
|
||||||
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 = () => {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
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 = () => {
|
|||||||
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 = () => {
|
|||||||
},
|
},
|
||||||
// ??? 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 = () => {
|
|||||||
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 = () => {
|
|||||||
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';
|
|||||||
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
|
|||||||
: 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
|
|||||||
[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 }) =>
|
|||||||
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) => {
|
|||||||
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) => {
|
|||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
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';
|
|||||||
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<{
|
|||||||
}
|
}
|
||||||
|
|
||||||
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]
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
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> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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
|
|||||||
|
|
||||||
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> = ({
|
|||||||
|
|
||||||
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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user