mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Panel options UI: Allow collapsible categories (#30301)
This commit is contained in:
parent
6a2b0dde63
commit
bedd662c07
@ -24,7 +24,6 @@ export function createFieldConfigRegistry<TFieldConfigOptions>(
|
||||
|
||||
for (const customProp of builder.getRegistry().list()) {
|
||||
customProp.isCustom = true;
|
||||
customProp.category = [`${pluginName} 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
|
||||
|
@ -39,7 +39,7 @@ export interface OptionEditorConfig<TOptions, TSettings = any, TValue = any> {
|
||||
/**
|
||||
* Array of strings representing category of the option. First element in the array will make option render as collapsible section.
|
||||
*/
|
||||
category?: string[];
|
||||
category?: Array<string | undefined>;
|
||||
|
||||
/**
|
||||
* Set this value if undefined
|
||||
|
@ -127,7 +127,7 @@ export const Components = {
|
||||
backArrow: 'Go Back button',
|
||||
},
|
||||
OptionsGroup: {
|
||||
toggle: (title: string) => `Options group ${title}`,
|
||||
toggle: (title?: string) => (title ? `Options group ${title}` : 'Options group'),
|
||||
},
|
||||
PluginVisualization: {
|
||||
item: (title: string) => `Plugin visualization item ${title}`,
|
||||
|
@ -86,4 +86,178 @@ describe('DefaultFieldConfigEditor', () => {
|
||||
const editors = queryAllByLabelText(selectors.components.PanelEditor.FieldOptions.propertyEditor('Custom'));
|
||||
expect(editors).toHaveLength(2);
|
||||
});
|
||||
|
||||
describe('categories', () => {
|
||||
it('should render uncategorized options under panel category', () => {
|
||||
const plugin = new PanelPlugin(() => null).useFieldConfig({
|
||||
standardOptions: {},
|
||||
useCustomConfig: b => {
|
||||
b.addBooleanSwitch({
|
||||
name: 'a',
|
||||
path: 'a',
|
||||
} as FieldConfigEditorConfig<FakeFieldOptions>)
|
||||
.addBooleanSwitch({
|
||||
name: 'c',
|
||||
path: 'c',
|
||||
} as FieldConfigEditorConfig<FakeFieldOptions>)
|
||||
.addTextInput({
|
||||
name: 'b',
|
||||
path: 'b',
|
||||
} as FieldConfigEditorConfig<FakeFieldOptions>);
|
||||
},
|
||||
});
|
||||
plugin.meta.name = 'Test plugin';
|
||||
|
||||
const { queryAllByLabelText } = render(
|
||||
<DefaultFieldConfigEditor data={[]} onChange={jest.fn()} plugin={plugin} config={fieldConfigMock} />
|
||||
);
|
||||
|
||||
expect(
|
||||
queryAllByLabelText(selectors.components.OptionsGroup.toggle(`${plugin.meta.name} options/0`))
|
||||
).toHaveLength(1);
|
||||
|
||||
expect(queryAllByLabelText(selectors.components.OptionsGroup.toggle(), { exact: false })).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should render categorized options under custom category', () => {
|
||||
const CATEGORY_NAME = 'Cat1';
|
||||
const plugin = new PanelPlugin(() => null).useFieldConfig({
|
||||
standardOptions: {},
|
||||
useCustomConfig: b => {
|
||||
b.addTextInput({
|
||||
name: 'b',
|
||||
path: 'b',
|
||||
} as FieldConfigEditorConfig<FakeFieldOptions>)
|
||||
.addBooleanSwitch({
|
||||
name: 'a',
|
||||
path: 'a',
|
||||
category: [CATEGORY_NAME],
|
||||
} as FieldConfigEditorConfig<FakeFieldOptions>)
|
||||
.addBooleanSwitch({
|
||||
name: 'c',
|
||||
path: 'c',
|
||||
category: [CATEGORY_NAME],
|
||||
} as FieldConfigEditorConfig<FakeFieldOptions>);
|
||||
},
|
||||
});
|
||||
plugin.meta.name = 'Test plugin';
|
||||
|
||||
const { queryAllByLabelText } = render(
|
||||
<DefaultFieldConfigEditor data={[]} onChange={jest.fn()} plugin={plugin} config={fieldConfigMock} />
|
||||
);
|
||||
|
||||
expect(
|
||||
queryAllByLabelText(selectors.components.OptionsGroup.toggle(`${plugin.meta.name} options/0`))
|
||||
).toHaveLength(1);
|
||||
|
||||
expect(queryAllByLabelText(selectors.components.OptionsGroup.toggle(`${CATEGORY_NAME}/1`))).toHaveLength(1);
|
||||
|
||||
expect(queryAllByLabelText(selectors.components.OptionsGroup.toggle(), { exact: false })).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should allow subcategories in panel category', () => {
|
||||
const SUBCATEGORY_NAME = 'Sub1';
|
||||
const plugin = new PanelPlugin(() => null).useFieldConfig({
|
||||
standardOptions: {},
|
||||
useCustomConfig: b => {
|
||||
b.addTextInput({
|
||||
name: 'b',
|
||||
path: 'b',
|
||||
category: [undefined, SUBCATEGORY_NAME],
|
||||
} as FieldConfigEditorConfig<FakeFieldOptions>)
|
||||
.addBooleanSwitch({
|
||||
name: 'a',
|
||||
path: 'a',
|
||||
} as FieldConfigEditorConfig<FakeFieldOptions>)
|
||||
.addBooleanSwitch({
|
||||
name: 'c',
|
||||
path: 'c',
|
||||
} as FieldConfigEditorConfig<FakeFieldOptions>);
|
||||
},
|
||||
});
|
||||
plugin.meta.name = 'Test plugin';
|
||||
|
||||
const { queryAllByLabelText, queryAllByText } = render(
|
||||
<DefaultFieldConfigEditor data={[]} onChange={jest.fn()} plugin={plugin} config={fieldConfigMock} />
|
||||
);
|
||||
|
||||
expect(
|
||||
queryAllByLabelText(selectors.components.OptionsGroup.toggle(`${plugin.meta.name} options/0`))
|
||||
).toHaveLength(1);
|
||||
|
||||
expect(queryAllByText(SUBCATEGORY_NAME, { exact: false })).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should allow subcategories in custom category', () => {
|
||||
const CATEGORY_NAME = 'Cat1';
|
||||
const SUBCATEGORY_NAME = 'Sub1';
|
||||
const plugin = new PanelPlugin(() => null).useFieldConfig({
|
||||
standardOptions: {},
|
||||
useCustomConfig: b => {
|
||||
b.addBooleanSwitch({
|
||||
name: 'a',
|
||||
path: 'a',
|
||||
} as FieldConfigEditorConfig<FakeFieldOptions>)
|
||||
.addBooleanSwitch({
|
||||
name: 'c',
|
||||
path: 'c',
|
||||
} as FieldConfigEditorConfig<FakeFieldOptions>)
|
||||
.addTextInput({
|
||||
name: 'b',
|
||||
path: 'b',
|
||||
category: [CATEGORY_NAME, SUBCATEGORY_NAME],
|
||||
} as FieldConfigEditorConfig<FakeFieldOptions>);
|
||||
},
|
||||
});
|
||||
plugin.meta.name = 'Test plugin';
|
||||
|
||||
const { queryAllByLabelText, queryAllByText } = render(
|
||||
<DefaultFieldConfigEditor data={[]} onChange={jest.fn()} plugin={plugin} config={fieldConfigMock} />
|
||||
);
|
||||
|
||||
expect(
|
||||
queryAllByLabelText(selectors.components.OptionsGroup.toggle(`${plugin.meta.name} options/0`))
|
||||
).toHaveLength(1);
|
||||
expect(queryAllByLabelText(selectors.components.OptionsGroup.toggle(`${CATEGORY_NAME}/1`))).toHaveLength(1);
|
||||
|
||||
expect(queryAllByText(SUBCATEGORY_NAME, { exact: false })).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should not render categories with hidden fields only', () => {
|
||||
const CATEGORY_NAME = 'Cat1';
|
||||
const SUBCATEGORY_NAME = 'Sub1';
|
||||
const plugin = new PanelPlugin(() => null).useFieldConfig({
|
||||
standardOptions: {},
|
||||
useCustomConfig: b => {
|
||||
b.addBooleanSwitch({
|
||||
name: 'a',
|
||||
path: 'a',
|
||||
} as FieldConfigEditorConfig<FakeFieldOptions>)
|
||||
.addBooleanSwitch({
|
||||
name: 'c',
|
||||
path: 'c',
|
||||
} as FieldConfigEditorConfig<FakeFieldOptions>)
|
||||
.addTextInput({
|
||||
name: 'b',
|
||||
path: 'b',
|
||||
hideFromDefaults: true,
|
||||
category: [CATEGORY_NAME, SUBCATEGORY_NAME],
|
||||
} as FieldConfigEditorConfig<FakeFieldOptions>);
|
||||
},
|
||||
});
|
||||
plugin.meta.name = 'Test plugin';
|
||||
|
||||
const { queryAllByLabelText, queryAllByText } = render(
|
||||
<DefaultFieldConfigEditor data={[]} onChange={jest.fn()} plugin={plugin} config={fieldConfigMock} />
|
||||
);
|
||||
|
||||
expect(
|
||||
queryAllByLabelText(selectors.components.OptionsGroup.toggle(`${plugin.meta.name} options/0`))
|
||||
).toHaveLength(1);
|
||||
|
||||
expect(queryAllByLabelText(selectors.components.OptionsGroup.toggle(`${CATEGORY_NAME}/1`))).toHaveLength(0);
|
||||
|
||||
expect(queryAllByText(SUBCATEGORY_NAME, { exact: false })).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -34,7 +34,7 @@ export const DefaultFieldConfigEditor: React.FC<Props> = ({ data, onChange, conf
|
||||
: get(defaults, item.path);
|
||||
|
||||
let label: ReactNode | undefined = (
|
||||
<Label description={item.description} category={item.category?.slice(1)}>
|
||||
<Label description={item.description} category={item.category?.slice(1) as string[]}>
|
||||
{item.name}
|
||||
</Label>
|
||||
);
|
||||
@ -67,27 +67,39 @@ export const DefaultFieldConfigEditor: React.FC<Props> = ({ data, onChange, conf
|
||||
[config]
|
||||
);
|
||||
|
||||
const groupedConfigs = groupBy(plugin.fieldConfigRegistry.list(), i => i.category && i.category[0]);
|
||||
const GENERAL_OPTIONS_CATEGORY = `${plugin.meta.name} options`;
|
||||
|
||||
const groupedConfigs = groupBy(plugin.fieldConfigRegistry.list(), i => {
|
||||
if (!i.category) {
|
||||
return GENERAL_OPTIONS_CATEGORY;
|
||||
}
|
||||
return i.category[0] ? i.category[0] : GENERAL_OPTIONS_CATEGORY;
|
||||
});
|
||||
|
||||
return (
|
||||
<div aria-label={selectors.components.FieldConfigEditor.content}>
|
||||
{Object.keys(groupedConfigs).map((k, i) => {
|
||||
const groupItemsCounter = countGroupItems(groupedConfigs[k], config);
|
||||
{Object.keys(groupedConfigs).map((groupName, i) => {
|
||||
const group = groupedConfigs[groupName];
|
||||
const groupItemsCounter = countGroupItems(group, config);
|
||||
|
||||
if (!shouldRenderGroup(group)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return (
|
||||
<OptionsGroup
|
||||
renderTitle={isExpanded => {
|
||||
return (
|
||||
<>
|
||||
{k} {!isExpanded && groupItemsCounter && <Counter value={groupItemsCounter} />}
|
||||
{groupName} {!isExpanded && groupItemsCounter && <Counter value={groupItemsCounter} />}
|
||||
</>
|
||||
);
|
||||
}}
|
||||
id={`${k}/${i}`}
|
||||
key={`${k}/${i}`}
|
||||
id={`${groupName}/${i}`}
|
||||
key={`${groupName}/${i}`}
|
||||
>
|
||||
{groupedConfigs[k].map(c => {
|
||||
return renderEditor(c, groupedConfigs[k].length);
|
||||
{group.map(c => {
|
||||
return renderEditor(c, group.length);
|
||||
})}
|
||||
</OptionsGroup>
|
||||
);
|
||||
@ -96,7 +108,7 @@ export const DefaultFieldConfigEditor: React.FC<Props> = ({ data, onChange, conf
|
||||
);
|
||||
};
|
||||
|
||||
const countGroupItems = (group: FieldConfigPropertyItem[], config: FieldConfigSource) => {
|
||||
function countGroupItems(group: FieldConfigPropertyItem[], config: FieldConfigSource) {
|
||||
let counter = 0;
|
||||
|
||||
for (const item of group) {
|
||||
@ -111,4 +123,9 @@ const countGroupItems = (group: FieldConfigPropertyItem[], config: FieldConfigSo
|
||||
}
|
||||
|
||||
return counter === 0 ? undefined : counter;
|
||||
};
|
||||
}
|
||||
|
||||
function shouldRenderGroup(group: FieldConfigPropertyItem[]) {
|
||||
const hiddenPropertiesCount = group.filter(i => i.hideFromDefaults).length;
|
||||
return group.length - hiddenPropertiesCount > 0;
|
||||
}
|
||||
|
@ -35,7 +35,10 @@ export const DynamicConfigValueEditor: React.FC<DynamicConfigValueEditorProps> =
|
||||
// eslint-disable-next-line react/display-name
|
||||
const renderLabel = (includeDescription = true, includeCounter = false) => (isExpanded = false) => (
|
||||
<HorizontalGroup justify="space-between">
|
||||
<Label category={item.category?.splice(1)} description={includeDescription ? item.description : undefined}>
|
||||
<Label
|
||||
category={item.category?.filter(c => c !== undefined) as string[]}
|
||||
description={includeDescription ? item.description : undefined}
|
||||
>
|
||||
{item.name}
|
||||
{!isExpanded && includeCounter && item.getItemsCount && <Counter value={item.getItemsCount(property.value)} />}
|
||||
</Label>
|
||||
|
@ -22,7 +22,7 @@ interface PanelOptionsEditorProps<TOptions> {
|
||||
options: TOptions;
|
||||
onChange: (options: TOptions) => void;
|
||||
}
|
||||
|
||||
const DISPLAY_OPTIONS_CATEGORY = 'Display';
|
||||
export const PanelOptionsEditor: React.FC<PanelOptionsEditorProps<any>> = ({
|
||||
plugin,
|
||||
options,
|
||||
@ -33,7 +33,10 @@ export const PanelOptionsEditor: React.FC<PanelOptionsEditorProps<any>> = ({
|
||||
}) => {
|
||||
const optionEditors = useMemo<Record<string, PanelOptionsEditorItem[]>>(() => {
|
||||
return groupBy(plugin.optionEditors.list(), i => {
|
||||
return i.category ? i.category[0] : 'Display';
|
||||
if (!i.category) {
|
||||
return DISPLAY_OPTIONS_CATEGORY;
|
||||
}
|
||||
return i.category[0] ? i.category[0] : DISPLAY_OPTIONS_CATEGORY;
|
||||
});
|
||||
}, [plugin]);
|
||||
|
||||
@ -62,7 +65,7 @@ export const PanelOptionsEditor: React.FC<PanelOptionsEditorProps<any>> = ({
|
||||
}
|
||||
|
||||
const label = (
|
||||
<Label description={e.description} category={e.category?.slice(1)}>
|
||||
<Label description={e.description} category={e.category?.slice(1) as string[]}>
|
||||
{e.name}
|
||||
</Label>
|
||||
);
|
||||
|
Loading…
Reference in New Issue
Block a user