diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 7899a0afa723..9346def6aafd 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -1 +1 @@
-* @ananzh @kavilla @seanneumann @AMoo-Miki @ashwin-pc @joshuarrrr @abbyhu2000 @zengyan-amazon @kristenTian @zhongnansu @manasvinibs
+* @ananzh @kavilla @seanneumann @AMoo-Miki @ashwin-pc @joshuarrrr @abbyhu2000 @zengyan-amazon @kristenTian @zhongnansu @manasvinibs @ZilongX @Flyingliuhub
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b07970998b3f..b4b173aff837 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -50,6 +50,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
- [VisBuilder] Fix Firefox legend selection issue ([#3732](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3732))
- [VisBuilder] Fix type errors ([#3732](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3732))
- [VisBuilder] Fix indexpattern selection in filter bar ([#3751](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3751))
+- Remove `lmdb-store` to fix backport issue ([#4266](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4266))
### 🚞 Infrastructure
@@ -90,6 +91,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
- [Multiple DataSource] Present the authentication type choices in a drop-down ([#3693](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3693))
- [Table Visualization] Move format table, consolidate types and add unit tests ([#3397](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3397))
- [Table Visualization] Remove custom styling for text-align:center in favor of OUI utility class. ([#4164](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4164))
+- Replace the use of `bluebird` in `saved_objects` plugin ([#4026](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4026))
### 🔩 Tests
diff --git a/MAINTAINERS.md b/MAINTAINERS.md
index ecb3f2928616..ae42e47b014b 100644
--- a/MAINTAINERS.md
+++ b/MAINTAINERS.md
@@ -17,6 +17,8 @@ This document contains a list of maintainers in this repo. See [opensearch-proje
| Kristen Tian | [kristenTian](https://github.com/kristenTian) | Amazon |
| Zhongnan Su | [zhongnansu](https://github.com/zhongnansu) | Amazon |
| Manasvini B Suryanarayana | [manasvinibs](https://github.com/manasvinibs) | Amazon |
+| Tao Liu | [Flyingliuhub](https://github.com/Flyingliuhub) | Amazon |
+| Zilong Xia | [ZilongX](https://github.com/ZilongX) | Amazon |
## Emeritus
diff --git a/examples/ui_actions_explorer/public/context_menu_examples/context_menu_examples.tsx b/examples/ui_actions_explorer/public/context_menu_examples/context_menu_examples.tsx
index b01d04c1608b..1f6ba03e966b 100644
--- a/examples/ui_actions_explorer/public/context_menu_examples/context_menu_examples.tsx
+++ b/examples/ui_actions_explorer/public/context_menu_examples/context_menu_examples.tsx
@@ -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 (
@@ -59,7 +60,6 @@ export const ContextMenuExamples: React.FC = () => {
-
@@ -71,6 +71,11 @@ export const ContextMenuExamples: React.FC = () => {
+
+
+
+
+
);
};
diff --git a/examples/ui_actions_explorer/public/context_menu_examples/panel_group_options_and_context_actions.tsx b/examples/ui_actions_explorer/public/context_menu_examples/panel_group_options_and_context_actions.tsx
new file mode 100644
index 000000000000..20dc73406c55
--- /dev/null
+++ b/examples/ui_actions_explorer/public/context_menu_examples/panel_group_options_and_context_actions.tsx
@@ -0,0 +1,83 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+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 (
+ setOpen((x) => !x)}>Grouping with categories}
+ isOpen={open}
+ panelPaddingSize="none"
+ anchorPosition="downLeft"
+ closePopover={() => setOpen(false)}
+ >
+
+
+ );
+};
diff --git a/packages/osd-optimizer/package.json b/packages/osd-optimizer/package.json
index d174e6207b40..6302f61357a1 100644
--- a/packages/osd-optimizer/package.json
+++ b/packages/osd-optimizer/package.json
@@ -28,7 +28,6 @@
"jest-diff": "^27.5.1",
"js-yaml": "^3.14.0",
"json-stable-stringify": "^1.0.1",
- "lmdb-store": "^1.6.11",
"node-sass": "^8.0.0",
"lmdb": "^2.8.0",
"normalize-path": "^3.0.0",
diff --git a/release-notes/opensearch-dashboards.release-notes-2.8.0.md b/release-notes/opensearch-dashboards.release-notes-2.8.0.md
new file mode 100644
index 000000000000..9de27211245e
--- /dev/null
+++ b/release-notes/opensearch-dashboards.release-notes-2.8.0.md
@@ -0,0 +1,30 @@
+## Version 2.8.0 Release Notes
+
+### 🛡 Security
+
+- [CVE-2023-2251] Bump yaml to `2.2.2` ([#3947](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3947))
+
+### 📈 Features/Enhancements
+
+- [Multiple Datasource] Support Amazon OpenSearch Serverless ([#3957](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3957))
+- Add support for Node.js >=14.20.1 <19 ([#4071](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4071))
+- Bundle Node.js 14 as a fallback for operating systems that cannot run Node.js 18 ([#4151](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4151))
+- Enhance grouping for context menus ([#3924](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3924))
+
+### 🐛 Bug Fixes
+
+- [BUG] Fix bottom bar visibility using createPortal ([#3978](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3978))
+
+### 🚞 Infrastructure
+
+- Add threshold to code coverage config to prevent workflow failures ([#4040](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4040))
+- [CI] Skip checksum verification on OpenSearch snapshot for cypress tests ([#4188](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4188))
+
+### 🛠 Maintenance
+
+- Use `exec` in the CLI shell scripts to prevent new process creation ([#3955](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3955))
+- Remove timeline application ([#3971](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3971))
+
+## 🎉 Welcome
+
+Thank you to all the first-time contributors who made this release possible: @sikhote, @SergeyMyssak!
diff --git a/src/core/server/legacy/legacy_service.ts b/src/core/server/legacy/legacy_service.ts
index ae7e85b98d67..a48ff12e859a 100644
--- a/src/core/server/legacy/legacy_service.ts
+++ b/src/core/server/legacy/legacy_service.ts
@@ -275,6 +275,7 @@ export class LegacyService implements CoreService {
addClientWrapper: setupDeps.core.savedObjects.addClientWrapper,
registerType: setupDeps.core.savedObjects.registerType,
getImportExportObjectLimit: setupDeps.core.savedObjects.getImportExportObjectLimit,
+ setRepositoryFactoryProvider: setupDeps.core.savedObjects.setRepositoryFactoryProvider,
},
status: {
isStatusPageAnonymous: setupDeps.core.status.isStatusPageAnonymous,
diff --git a/src/core/server/plugins/plugin_context.ts b/src/core/server/plugins/plugin_context.ts
index 521a8dd2f7b0..7782fd93041e 100644
--- a/src/core/server/plugins/plugin_context.ts
+++ b/src/core/server/plugins/plugin_context.ts
@@ -204,6 +204,7 @@ export function createPluginSetupContext(
addClientWrapper: deps.savedObjects.addClientWrapper,
registerType: deps.savedObjects.registerType,
getImportExportObjectLimit: deps.savedObjects.getImportExportObjectLimit,
+ setRepositoryFactoryProvider: deps.savedObjects.setRepositoryFactoryProvider,
},
status: {
core$: deps.status.core$,
diff --git a/src/core/server/saved_objects/saved_objects_service.mock.ts b/src/core/server/saved_objects/saved_objects_service.mock.ts
index ae36b83c0cdd..74168c436c3d 100644
--- a/src/core/server/saved_objects/saved_objects_service.mock.ts
+++ b/src/core/server/saved_objects/saved_objects_service.mock.ts
@@ -79,6 +79,7 @@ const createSetupContractMock = () => {
addClientWrapper: jest.fn(),
registerType: jest.fn(),
getImportExportObjectLimit: jest.fn(),
+ setRepositoryFactoryProvider: jest.fn(),
};
setupContract.getImportExportObjectLimit.mockReturnValue(100);
diff --git a/src/core/server/saved_objects/saved_objects_service.test.ts b/src/core/server/saved_objects/saved_objects_service.test.ts
index 42ee52567e5f..98d1da393319 100644
--- a/src/core/server/saved_objects/saved_objects_service.test.ts
+++ b/src/core/server/saved_objects/saved_objects_service.test.ts
@@ -41,7 +41,7 @@ import { errors as opensearchErrors } from '@opensearch-project/opensearch';
import { SavedObjectsService } from './saved_objects_service';
import { mockCoreContext } from '../core_context.mock';
import { Env } from '../config';
-import { configServiceMock } from '../mocks';
+import { configServiceMock, savedObjectsRepositoryMock } from '../mocks';
import { opensearchServiceMock } from '../opensearch/opensearch_service.mock';
import { opensearchClientMock } from '../opensearch/client/mocks';
import { httpServiceMock } from '../http/http_service.mock';
@@ -49,6 +49,7 @@ import { httpServerMock } from '../http/http_server.mocks';
import { SavedObjectsClientFactoryProvider } from './service/lib';
import { NodesVersionCompatibility } from '../opensearch/version_check/ensure_opensearch_version';
import { SavedObjectsRepository } from './service/lib/repository';
+import { SavedObjectRepositoryFactoryProvider } from './service/lib/scoped_client_provider';
jest.mock('./service/lib/repository');
@@ -169,6 +170,27 @@ describe('SavedObjectsService', () => {
expect(typeRegistryInstanceMock.registerType).toHaveBeenCalledWith(type);
});
});
+
+ describe('#setRepositoryFactoryProvider', () => {
+ it('throws error if a repository is already registered', async () => {
+ const coreContext = createCoreContext();
+ const soService = new SavedObjectsService(coreContext);
+ const setup = await soService.setup(createSetupDeps());
+
+ const firstRepository: SavedObjectRepositoryFactoryProvider = () =>
+ savedObjectsRepositoryMock.create();
+ const secondRepository: SavedObjectRepositoryFactoryProvider = () =>
+ savedObjectsRepositoryMock.create();
+
+ setup.setRepositoryFactoryProvider(firstRepository);
+
+ expect(() => {
+ setup.setRepositoryFactoryProvider(secondRepository);
+ }).toThrowErrorMatchingInlineSnapshot(
+ `"custom repository factory is already set, and can only be set once"`
+ );
+ });
+ });
});
describe('#start()', () => {
@@ -281,6 +303,15 @@ describe('SavedObjectsService', () => {
}).toThrowErrorMatchingInlineSnapshot(
`"cannot call \`registerType\` after service startup."`
);
+
+ const customRpository: SavedObjectRepositoryFactoryProvider = () =>
+ savedObjectsRepositoryMock.create();
+
+ expect(() => {
+ setup.setRepositoryFactoryProvider(customRpository);
+ }).toThrowErrorMatchingInlineSnapshot(
+ '"cannot call `setRepositoryFactoryProvider` after service startup."'
+ );
});
describe('#getTypeRegistry', () => {
@@ -368,6 +399,36 @@ describe('SavedObjectsService', () => {
expect(includedHiddenTypes).toEqual(['someHiddenType']);
});
+
+ it('Should not create SavedObjectsRepository when custom repository is registered ', async () => {
+ const coreContext = createCoreContext({ skipMigration: false });
+ const soService = new SavedObjectsService(coreContext);
+ const coreSetup = createSetupDeps();
+ const setup = await soService.setup(coreSetup);
+
+ const customRpository: SavedObjectRepositoryFactoryProvider = () =>
+ savedObjectsRepositoryMock.create();
+ setup.setRepositoryFactoryProvider(customRpository);
+
+ const coreStart = createStartDeps();
+ const { createInternalRepository } = await soService.start(coreStart);
+ createInternalRepository();
+
+ expect(SavedObjectsRepository.createRepository as jest.Mocked).not.toHaveBeenCalled();
+ });
+
+ it('Should create SavedObjectsRepository when no custom repository is registered ', async () => {
+ const coreContext = createCoreContext({ skipMigration: false });
+ const soService = new SavedObjectsService(coreContext);
+ const coreSetup = createSetupDeps();
+ await soService.setup(coreSetup);
+
+ const coreStart = createStartDeps();
+ const { createInternalRepository } = await soService.start(coreStart);
+ createInternalRepository();
+
+ expect(SavedObjectsRepository.createRepository as jest.Mocked).toHaveBeenCalled();
+ });
});
});
});
diff --git a/src/core/server/saved_objects/saved_objects_service.ts b/src/core/server/saved_objects/saved_objects_service.ts
index 92649d36beb4..b6fc21617bcc 100644
--- a/src/core/server/saved_objects/saved_objects_service.ts
+++ b/src/core/server/saved_objects/saved_objects_service.ts
@@ -56,6 +56,7 @@ import { ISavedObjectsRepository, SavedObjectsRepository } from './service/lib/r
import {
SavedObjectsClientFactoryProvider,
SavedObjectsClientWrapperFactory,
+ SavedObjectRepositoryFactoryProvider,
} from './service/lib/scoped_client_provider';
import { Logger } from '../logging';
import { SavedObjectTypeRegistry, ISavedObjectTypeRegistry } from './saved_objects_type_registry';
@@ -166,6 +167,14 @@ export interface SavedObjectsServiceSetup {
* Returns the maximum number of objects allowed for import or export operations.
*/
getImportExportObjectLimit: () => number;
+
+ /**
+ * Set the default {@link SavedObjectRepositoryFactoryProvider | factory provider} for creating Saved Objects repository.
+ * Only one repository can be set, subsequent calls to this method will fail.
+ */
+ setRepositoryFactoryProvider: (
+ respositoryFactoryProvider: SavedObjectRepositoryFactoryProvider
+ ) => void;
}
/**
@@ -291,6 +300,8 @@ export class SavedObjectsService
private typeRegistry = new SavedObjectTypeRegistry();
private started = false;
+ private respositoryFactoryProvider?: SavedObjectRepositoryFactoryProvider;
+
constructor(private readonly coreContext: CoreContext) {
this.logger = coreContext.logger.get('savedobjects-service');
}
@@ -348,6 +359,15 @@ export class SavedObjectsService
this.typeRegistry.registerType(type);
},
getImportExportObjectLimit: () => this.config!.maxImportExportSize,
+ setRepositoryFactoryProvider: (repositoryProvider) => {
+ if (this.started) {
+ throw new Error('cannot call `setRepositoryFactoryProvider` after service startup.');
+ }
+ if (this.respositoryFactoryProvider) {
+ throw new Error('custom repository factory is already set, and can only be set once');
+ }
+ this.respositoryFactoryProvider = repositoryProvider;
+ },
};
}
@@ -422,13 +442,21 @@ export class SavedObjectsService
opensearchClient: OpenSearchClient,
includedHiddenTypes: string[] = []
) => {
- return SavedObjectsRepository.createRepository(
- migrator,
- this.typeRegistry,
- opensearchDashboardsConfig.index,
- opensearchClient,
- includedHiddenTypes
- );
+ if (this.respositoryFactoryProvider) {
+ return this.respositoryFactoryProvider({
+ migrator,
+ typeRegistry: this.typeRegistry,
+ includedHiddenTypes,
+ });
+ } else {
+ return SavedObjectsRepository.createRepository(
+ migrator,
+ this.typeRegistry,
+ opensearchDashboardsConfig.index,
+ opensearchClient,
+ includedHiddenTypes
+ );
+ }
};
const repositoryFactory: SavedObjectsRepositoryFactory = {
diff --git a/src/core/server/saved_objects/service/lib/scoped_client_provider.ts b/src/core/server/saved_objects/service/lib/scoped_client_provider.ts
index 439cd31afa9c..fea1a1641a6f 100644
--- a/src/core/server/saved_objects/service/lib/scoped_client_provider.ts
+++ b/src/core/server/saved_objects/service/lib/scoped_client_provider.ts
@@ -31,8 +31,31 @@
import { PriorityCollection } from './priority_collection';
import { SavedObjectsClientContract } from '../../types';
import { SavedObjectsRepositoryFactory } from '../../saved_objects_service';
-import { ISavedObjectTypeRegistry } from '../../saved_objects_type_registry';
+import {
+ ISavedObjectTypeRegistry,
+ SavedObjectTypeRegistry,
+} from '../../saved_objects_type_registry';
import { OpenSearchDashboardsRequest } from '../../../http';
+import { ISavedObjectsRepository } from './repository';
+import { IOpenSearchDashboardsMigrator } from '../../migrations';
+
+/**
+ * Options passed to each SavedObjectRepositoryFactoryProvider to aid in creating the repository instance.
+ * @public
+ */
+export interface SavedObjectsRepositoryOptions {
+ migrator: IOpenSearchDashboardsMigrator;
+ typeRegistry: SavedObjectTypeRegistry;
+ includedHiddenTypes: string[];
+}
+
+/**
+ * Provider to invoke to a factory function for creating ISavedObjectRepository {@link ISavedObjectRepository} instances.
+ * @public
+ */
+export type SavedObjectRepositoryFactoryProvider = (
+ options: SavedObjectsRepositoryOptions
+) => ISavedObjectsRepository;
/**
* Options passed to each SavedObjectsClientWrapperFactory to aid in creating the wrapper instance.
diff --git a/src/plugins/saved_objects/public/saved_object/saved_object.test.ts b/src/plugins/saved_objects/public/saved_object/saved_object.test.ts
index 7e10a68c2d1f..3b1eea303f23 100644
--- a/src/plugins/saved_objects/public/saved_object/saved_object.test.ts
+++ b/src/plugins/saved_objects/public/saved_object/saved_object.test.ts
@@ -28,7 +28,6 @@
* under the License.
*/
-import Bluebird from 'bluebird';
import { createSavedObjectClass } from './saved_object';
import {
SavedObject,
@@ -76,16 +75,16 @@ describe('Saved Object', () => {
*/
function stubOpenSearchResponse(mockDocResponse: SimpleSavedObject) {
// Stub out search for duplicate title:
- savedObjectsClientStub.get = jest.fn().mockReturnValue(Bluebird.resolve(mockDocResponse));
- savedObjectsClientStub.update = jest.fn().mockReturnValue(Bluebird.resolve(mockDocResponse));
+ savedObjectsClientStub.get = jest.fn().mockReturnValue(Promise.resolve(mockDocResponse));
+ savedObjectsClientStub.update = jest.fn().mockReturnValue(Promise.resolve(mockDocResponse));
savedObjectsClientStub.find = jest
.fn()
- .mockReturnValue(Bluebird.resolve({ savedObjects: [], total: 0 }));
+ .mockReturnValue(Promise.resolve({ savedObjects: [], total: 0 }));
savedObjectsClientStub.bulkGet = jest
.fn()
- .mockReturnValue(Bluebird.resolve({ savedObjects: [mockDocResponse] }));
+ .mockReturnValue(Promise.resolve({ savedObjects: [mockDocResponse] }));
}
function stubSavedObjectsClientCreate(
@@ -94,7 +93,7 @@ describe('Saved Object', () => {
) {
savedObjectsClientStub.create = jest
.fn()
- .mockReturnValue(resolve ? Bluebird.resolve(resp) : Bluebird.reject(resp));
+ .mockReturnValue(resolve ? Promise.resolve(resp) : Promise.reject(resp));
}
/**
@@ -193,7 +192,7 @@ describe('Saved Object', () => {
return createInitializedSavedObject({ type: 'dashboard', id: myId }).then((savedObject) => {
savedObjectsClientStub.create = jest.fn().mockImplementation(() => {
expect(savedObject.id).toBe(myId);
- return Bluebird.resolve({ id: myId });
+ return Promise.resolve({ id: myId });
});
savedObject.copyOnSave = false;
@@ -227,7 +226,7 @@ describe('Saved Object', () => {
return createInitializedSavedObject({ type: 'dashboard', id }).then((savedObject) => {
savedObjectsClientStub.create = jest.fn().mockImplementation(() => {
expect(savedObject.isSaving).toBe(true);
- return Bluebird.resolve({
+ return Promise.resolve({
type: 'dashboard',
id,
_version: 'foo',
@@ -246,7 +245,7 @@ describe('Saved Object', () => {
return createInitializedSavedObject({ type: 'dashboard' }).then((savedObject) => {
savedObjectsClientStub.create = jest.fn().mockImplementation(() => {
expect(savedObject.isSaving).toBe(true);
- return Bluebird.reject('');
+ return Promise.reject();
});
expect(savedObject.isSaving).toBe(false);
@@ -681,7 +680,7 @@ describe('Saved Object', () => {
);
indexPattern.title = indexPattern.id!;
savedObject.searchSource!.setField('index', indexPattern);
- return Bluebird.resolve(indexPattern);
+ return Promise.resolve(indexPattern);
});
expect(!!savedObject.searchSource!.getField('index')).toBe(false);
diff --git a/src/plugins/ui_actions/README.md b/src/plugins/ui_actions/README.md
index 28e3b2d63d2e..4431a47a06ed 100644
--- a/src/plugins/ui_actions/README.md
+++ b/src/plugins/ui_actions/README.md
@@ -97,3 +97,12 @@ Use the UI actions explorer in the Developer examples to learn more about the se
```sh
yarn start --run-examples
```
+
+## Action Properties
+
+Refer to [./public/actions/action.ts](./public/actions/action.ts) for all properties, keeping in mind it extends the [presentable](./public/util/presentable.ts) interface. Here are some properties that provide special functionality and customization.
+
+- `order` is used when there is more than one action matched to a trigger and within context menus. Higher numbers are displayed first.
+- `getDisplayName` is a function that can return either a string or a JSX element. Returning a JSX element allows flexibility with formatting.
+- `getIconType` can be used to add an icon before the display name.
+- `grouping` determines where this item should appear as a submenu. Each group can also contain a category, which is used within context menus to organize similar groups into the same section of the menu. See examples explorer for more details about what this looks like within a context menu.
diff --git a/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.test.ts b/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.test.ts
index b9afca9fb99c..e70561bea221 100644
--- a/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.test.ts
+++ b/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.test.ts
@@ -448,3 +448,123 @@ 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" and low order of 10
+// a group with 1 actions with a category of "test-category" and high order of 20
+test('groups with categories and order', 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',
+ order: 10,
+ },
+ ];
+ const grouping3 = [
+ {
+ id: 'test-group-3',
+ getDisplayName: () => 'Test group 3',
+ getIconType: () => 'bell',
+ category: 'test-category',
+ order: 20,
+ },
+ ];
+
+ 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,
+ }),
+ // It is expected that, because there is only 1 action within this group,
+ // it will be added to the mainMenu as a single item, but next to other
+ // groups of the same category. When a group has a category, but only one
+ // item, we just add that single item; otherwise, we add a link to the group
+ 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": "Waldo 1",
+ },
+ Object {
+ "name": "Test group 2",
+ },
+ ],
+ },
+ 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",
+ },
+ ],
+ },
+ ]
+ `);
+});
diff --git a/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.tsx b/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.tsx
index 6d69be1f3faa..81710767e0a9 100644
--- a/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.tsx
+++ b/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.tsx
@@ -64,6 +64,8 @@ type PanelDescriptor = EuiContextMenuPanelDescriptor & {
_level?: number;
_icon?: string;
items: ItemDescriptor[];
+ _category?: string;
+ _order?: number;
};
const onClick = (action: Action, context: ActionExecutionContext