Skip to content
This repository has been archived by the owner on Mar 25, 2024. It is now read-only.

Commit

Permalink
add category option for context menus
Browse files Browse the repository at this point in the history
Signed-off-by: David Sinclair <dsincla@rei.com>
  • Loading branch information
sikhote committed May 26, 2023
1 parent d9bf726 commit 8e7003a
Show file tree
Hide file tree
Showing 5 changed files with 291 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import { PanelViewWithSharingLong } from './panel_view_with_sharing_long';
import { PanelEdit } from './panel_edit';
import { PanelEditWithDrilldowns } from './panel_edit_with_drilldowns';
import { PanelEditWithDrilldownsAndContextActions } from './panel_edit_with_drilldowns_and_context_actions';
import { PanelGroupOptionsAndContextActions } from './panel_group_options_and_context_actions';

export const ContextMenuExamples: React.FC = () => {
return (
Expand All @@ -59,7 +60,6 @@ export const ContextMenuExamples: React.FC = () => {
<PanelViewWithSharingLong />
</EuiFlexItem>
</EuiFlexGroup>

<EuiFlexGroup>
<EuiFlexItem>
<PanelEdit />
Expand All @@ -71,6 +71,11 @@ export const ContextMenuExamples: React.FC = () => {
<PanelEditWithDrilldownsAndContextActions />
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup>
<EuiFlexItem>
<PanelGroupOptionsAndContextActions />
</EuiFlexItem>
</EuiFlexGroup>
</EuiText>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* The OpenSearch Contributors require contributions made to
* this file be licensed under the Apache-2.0 license or a
* compatible open source license.
*
* Any modifications Copyright OpenSearch Contributors. See
* GitHub history for details.
*/

/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import * as React from 'react';
import { EuiButton, EuiContextMenu, EuiPopover } from '@elastic/eui';
import useAsync from 'react-use/lib/useAsync';
import { buildContextMenuForActions, Action } from '../../../../src/plugins/ui_actions/public';
import { sampleAction } from './util';

export const PanelGroupOptionsAndContextActions: React.FC = () => {
const [open, setOpen] = React.useState(false);

const context = {};
const trigger: any = 'TEST_TRIGGER';
const drilldownGrouping: Action['grouping'] = [
{
id: 'drilldowns',
getDisplayName: () => 'Uncategorized group',
getIconType: () => 'popout',
order: 20,
},
];
const exampleGroup: Action['grouping'] = [
{
id: 'example',
getDisplayName: () => 'Example group',
getIconType: () => 'cloudStormy',
order: 20,
category: 'visAug',
},
];
const alertingGroup: Action['grouping'] = [
{
id: 'alerting',
getDisplayName: () => 'Alerting',
getIconType: () => 'cloudStormy',
order: 20,
category: 'visAug',
},
];
const anomaliesGroup: Action['grouping'] = [
{
id: 'anomalies',
getDisplayName: () => 'Anomalies',
getIconType: () => 'cloudStormy',
order: 30,
category: 'visAug',
},
];
const actions = [
sampleAction('test-1', 100, 'Edit visualization', 'pencil'),
sampleAction('test-2', 99, 'Clone panel', 'partial'),

sampleAction('test-9', 10, 'Create drilldown', 'plusInCircle', drilldownGrouping),
sampleAction('test-10', 9, 'Manage drilldowns', 'list', drilldownGrouping),

sampleAction('test-11', 10, 'Example action', 'dashboardApp', exampleGroup),
sampleAction('test-11', 10, 'Alertin action 1', 'dashboardApp', alertingGroup),
sampleAction('test-12', 9, 'Alertin action 2', 'dashboardApp', alertingGroup),
sampleAction('test-13', 8, 'Anomalies 1', 'cloudStormy', anomaliesGroup),
sampleAction('test-14', 7, 'Anomalies 2', 'link', anomaliesGroup),
];

const panels = useAsync(() =>
buildContextMenuForActions({
actions: actions.map((action) => ({ action, context, trigger })),
})
);

return (
<EuiPopover
button={<EuiButton onClick={() => setOpen((x) => !x)}>Grouping with categories</EuiButton>}
isOpen={open}
panelPaddingSize="none"
anchorPosition="downLeft"
closePopover={() => setOpen(false)}
>
<EuiContextMenu initialPanelId={'mainMenu'} panels={panels.value} />
</EuiPopover>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -448,3 +448,117 @@ test('groups with deep nesting', async () => {
]
`);
});

// Tests with:
// a regular action
// a group with 2 actions uncategorized
// a group with 2 actions with a category of "test-category"
// a group with 1 actions with a category of "test-category"
test('groups with categories', async () => {
const grouping1 = [
{
id: 'test-group',
getDisplayName: () => 'Test group',
getIconType: () => 'bell',
},
];
const grouping2 = [
{
id: 'test-group-2',
getDisplayName: () => 'Test group 2',
getIconType: () => 'bell',
category: 'test-category',
},
];
const grouping3 = [
{
id: 'test-group-3',
getDisplayName: () => 'Test group 3',
getIconType: () => 'bell',
category: 'test-category',
},
];

const actions = [
createTestAction({
dispayName: 'Foo 1',
}),
createTestAction({
dispayName: 'Bar 1',
grouping: grouping1,
}),
createTestAction({
dispayName: 'Bar 2',
grouping: grouping1,
}),
createTestAction({
dispayName: 'Qux 1',
grouping: grouping2,
}),
createTestAction({
dispayName: 'Qux 2',
grouping: grouping2,
}),
createTestAction({
dispayName: 'Waldo 1',
grouping: grouping3,
}),
];
const menu = await buildContextMenuForActions({
actions: actions.map((action) => ({ action, context: {}, trigger: 'TEST' as any })),
});

expect(menu.map(resultMapper)).toMatchInlineSnapshot(`
Array [
Object {
"items": Array [
Object {
"name": "Foo 1",
},
Object {
"isSeparator": true,
},
Object {
"name": "Test group",
},
Object {
"isSeparator": true,
},
Object {
"name": "Test group 2",
},
Object {
"name": "Waldo 1",
},
],
},
Object {
"items": Array [
Object {
"name": "Bar 1",
},
Object {
"name": "Bar 2",
},
],
},
Object {
"items": Array [
Object {
"name": "Qux 1",
},
Object {
"name": "Qux 2",
},
],
},
Object {
"items": Array [
Object {
"name": "Waldo 1",
},
],
},
]
`);
});
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ type PanelDescriptor = EuiContextMenuPanelDescriptor & {
_level?: number;
_icon?: string;
items: ItemDescriptor[];
_category?: string;
};

const onClick = (action: Action, context: ActionExecutionContext<object>, close: () => void) => (
Expand Down Expand Up @@ -125,7 +126,7 @@ const removeItemMetaFields = (items: ItemDescriptor[]): EuiContextMenuPanelItemD
const removePanelMetaFields = (panels: PanelDescriptor[]): EuiContextMenuPanelDescriptor[] => {
const euiPanels: EuiContextMenuPanelDescriptor[] = [];
for (const panel of panels) {
const { _level: omit, _icon: omit2, ...rest } = panel;
const { _level: omit, _icon: omit2, _category: omit3, ...rest } = panel;
euiPanels.push({ ...rest, items: removeItemMetaFields(rest.items) });
}
return euiPanels;
Expand Down Expand Up @@ -179,6 +180,7 @@ export async function buildContextMenuForActions({
items: [],
_level: i,
_icon: group.getIconType ? group.getIconType(context) : 'empty',
_category: group.category,
};

// If there are multiple groups and this is not the first group,
Expand Down Expand Up @@ -231,30 +233,68 @@ export async function buildContextMenuForActions({
// Any additional items are hidden behind a "more" item
wrapMainPanelItemsIntoSubmenu(panels, 'mainMenu');

// This will be used to store items that eventually are placed into the
// mainMenu panel. Specifying a category allows for placing groups into the
// mainMenu so they appear without the separator between them.
const categories = {};

for (const panel of Object.values(panels)) {
// If the panel is a root-level panel, such as the parent of a group,
// then create mainMenu item for this panel
if (panel._level === 0) {
// Add separator with unique key if needed
if (panels.mainMenu.items.length) {
panels.mainMenu.items.push({ isSeparator: true, key: `${panel.id}separator` });
}
// If a category is specified, store either a link to the panel or the
// item within. We will deal with it after looping through all panels.
if (panel._category) {
// Create array to store category items
if (!categories[panel._category]) {
categories[panel._category] = [];
}

// If a panel has more than one child, then allow items to be grouped
// and link to it in the mainMenu. Otherwise, flatten the group.
// Note: this only happens on the root level panels, not for inner groups.
if (panel.items.length > 1) {
panels.mainMenu.items.push({
name: panel.title || panel.id,
icon: panel._icon || 'empty',
panel: panel.id,
});
// If multiple items in the panel, store a link to this panel into the category.
// Otherwise, just store the single item into the category.
if (panel.items.length > 1) {
categories[panel._category].push({
name: panel.title || panel.id,
icon: panel._icon || 'empty',
panel: panel.id,
});
} else {
categories[panel._category].push(...panel.items);
}
} else {
panels.mainMenu.items.push(...panel.items);
// Add separator with unique key if needed
if (panels.mainMenu.items.length) {
panels.mainMenu.items.push({ isSeparator: true, key: `${panel.id}separator` });
}

// If a panel has more than one child, then allow items to be grouped
// and link to it in the mainMenu. Otherwise, flatten the group.
// Note: this only happens on the root level panels, not for inner groups.
if (panel.items.length > 1) {
panels.mainMenu.items.push({
name: panel.title || panel.id,
icon: panel._icon || 'empty',
panel: panel.id,
});
} else {
panels.mainMenu.items.push(...panel.items);
}
}
}
}

// For each category, add a separator before each one and then add category items.
// This is for the mainMenu panel.
Object.keys(categories).forEach((key) => {
// Add separator with unique key if needed
if (panels.mainMenu.items.length) {
panels.mainMenu.items.push({ isSeparator: true, key: `${key}separator` });
}

panels.mainMenu.items.push(...categories[key]);
});

const panelList = Object.values(panels);

return removePanelMetaFields(panelList);
}
8 changes: 8 additions & 0 deletions src/plugins/ui_actions/public/util/presentable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,14 @@ export interface PresentableGroup<Context extends object = object>
Pick<Presentable<Context>, 'getDisplayName' | 'getDisplayNameTooltip' | 'getIconType' | 'order'>
> {
id: string;
/**
* This allows groups to be categorized with other groups. Within a UI action
* context menu, this means that an item, which links to a group, will be
* placed in the menu adjacent to similar items that link to groups of the
* same category.
* See PanelGroupOptionsAndContextActions example to learn more.
*/
category?: string;
}

export type PresentableGrouping<Context extends object = object> = Array<PresentableGroup<Context>>;

0 comments on commit 8e7003a

Please sign in to comment.