From 310a2653db99677072c01362d2af749a44aa91be Mon Sep 17 00:00:00 2001 From: Ashwin P Chandran Date: Mon, 13 Mar 2023 22:26:59 +0000 Subject: [PATCH 1/5] adds ui actions explorer Signed-off-by: Ashwin P Chandran --- .../public/components/explorer_tab.tsx | 1 - examples/ui_actions_explorer/public/app.tsx | 180 ++++++++---------- .../ui_actions_explorer/public/basic_tab.tsx | 109 +++++++++++ .../public/explorer_tab.tsx | 87 +++++++++ .../ui_actions_explorer/public/plugin.tsx | 29 ++- .../public/trigger_context_example.tsx | 16 +- examples/ui_actions_explorer/public/types.ts | 25 +++ .../public/service/ui_actions_service.ts | 4 + 8 files changed, 327 insertions(+), 124 deletions(-) create mode 100644 examples/ui_actions_explorer/public/basic_tab.tsx create mode 100644 examples/ui_actions_explorer/public/explorer_tab.tsx create mode 100644 examples/ui_actions_explorer/public/types.ts diff --git a/examples/expressions_example/public/components/explorer_tab.tsx b/examples/expressions_example/public/components/explorer_tab.tsx index 14030751e5b3..2104d103d2e8 100644 --- a/examples/expressions_example/public/components/explorer_tab.tsx +++ b/examples/expressions_example/public/components/explorer_tab.tsx @@ -43,7 +43,6 @@ export function ExplorerTab() { const allTypes = new Set(Object.values(functions).map((fn) => fn.type)); // Catch all filter and remove - allTypes.delete(undefined); allTypes.add('all'); return [...allTypes].map((type) => ({ text: type })); diff --git a/examples/ui_actions_explorer/public/app.tsx b/examples/ui_actions_explorer/public/app.tsx index 2afc32f3484e..4de6e928a95e 100644 --- a/examples/ui_actions_explorer/public/app.tsx +++ b/examples/ui_actions_explorer/public/app.tsx @@ -28,114 +28,96 @@ * under the License. */ -import React, { useState } from 'react'; +import React, { useMemo } from 'react'; import ReactDOM from 'react-dom'; +import { FormattedMessage, I18nProvider } from '@osd/i18n/react'; -import { EuiPage } from '@elastic/eui'; +import { + EuiPage, + EuiTitle, + EuiPageBody, + EuiPageContent, + EuiPageContentBody, + EuiPageHeader, + EuiTabbedContent, +} from '@elastic/eui'; +import { AppMountParameters, CoreStart } from '../../../src/core/public'; +import { UiActionsExplorerServices, UiActionsExplorerStartDependencies } from './types'; +import { OpenSearchDashboardsContextProvider } from '../../../src/plugins/opensearch_dashboards_react/public'; -import { EuiButton } from '@elastic/eui'; -import { EuiPageBody } from '@elastic/eui'; -import { EuiPageContent } from '@elastic/eui'; -import { EuiPageContentBody } from '@elastic/eui'; -import { EuiSpacer } from '@elastic/eui'; -import { EuiText } from '@elastic/eui'; -import { EuiFieldText } from '@elastic/eui'; -import { EuiCallOut } from '@elastic/eui'; -import { EuiPageHeader } from '@elastic/eui'; -import { EuiModalBody } from '@elastic/eui'; -import { toMountPoint } from '../../../src/plugins/opensearch_dashboards_react/public'; -import { UiActionsStart, createAction } from '../../../src/plugins/ui_actions/public'; -import { AppMountParameters, OverlayStart } from '../../../src/core/public'; -import { HELLO_WORLD_TRIGGER_ID, ACTION_HELLO_WORLD } from '../../ui_action_examples/public'; -import { TriggerContextExample } from './trigger_context_example'; -import { ContextMenuExamples } from './context_menu_examples'; +import { BasicTab } from './basic_tab'; +import { ExplorerTab } from './explorer_tab'; -interface Props { - uiActionsApi: UiActionsStart; - openModal: OverlayStart['openModal']; -} +const ActionsExplorer = () => { + const tabs = useMemo( + () => [ + { + id: 'demo-basic', + name: ( + + ), + content: , + }, + { + id: 'demo-explorer', + name: ( + + ), + content: , + }, + ], + [] + ); -const ActionsExplorer = ({ uiActionsApi, openModal }: Props) => { - const [name, setName] = useState('Waldo'); - const [confirmationText, setConfirmationText] = useState(''); return ( - - - Ui Actions Explorer - - - -

- By default there is a single action attached to the `HELLO_WORLD_TRIGGER`. Clicking - this button will cause it to be executed immediately. -

-
- uiActionsApi.executeTriggerActions(HELLO_WORLD_TRIGGER_ID, {})} - > - Say hello world! - - - -

- Lets dynamically add new actions to this trigger. After you click this button, click - the above button again. This time it should offer you multiple options to choose - from. Using the UI Action and Trigger API makes your plugin extensible by other - plugins. Any actions attached to the `HELLO_WORLD_TRIGGER_ID` will show up here! -

- setName(e.target.value)} /> - { - const dynamicAction = createAction({ - id: `${ACTION_HELLO_WORLD}-${name}`, - type: ACTION_HELLO_WORLD, - getDisplayName: () => `Say hello to ${name}`, - execute: async () => { - const overlay = openModal( - toMountPoint( - - - {`Hello ${name}`} - {' '} - overlay.close()}> - Close - - - ) - ); - }, - }); - uiActionsApi.addTriggerAction(HELLO_WORLD_TRIGGER_ID, dynamicAction); - setConfirmationText( - `You've successfully added a new action: ${dynamicAction.getDisplayName({ - trigger: uiActionsApi.getTrigger(HELLO_WORLD_TRIGGER_ID), - })}. Refresh the page to reset state. It's up to the user of the system to persist state like this.` - ); - }} - > - Say hello to me! - - {confirmationText !== '' ? {confirmationText} : undefined} -
- - - - - - - - -
-
-
-
+ + + + + +

+ +

+
+
+ + + + + +
+
+
); }; -export const renderApp = (props: Props, { element }: AppMountParameters) => { - ReactDOM.render(, element); +export const renderApp = ( + coreStart: CoreStart, + { uiActions }: UiActionsExplorerStartDependencies, + { element }: AppMountParameters +) => { + const services: UiActionsExplorerServices = { + ...coreStart, + uiActions, + }; + ReactDOM.render( + + + , + element + ); return () => ReactDOM.unmountComponentAtNode(element); }; diff --git a/examples/ui_actions_explorer/public/basic_tab.tsx b/examples/ui_actions_explorer/public/basic_tab.tsx new file mode 100644 index 000000000000..21f6f647df70 --- /dev/null +++ b/examples/ui_actions_explorer/public/basic_tab.tsx @@ -0,0 +1,109 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useState } from 'react'; + +import { + EuiButton, + EuiCallOut, + EuiSpacer, + EuiText, + EuiFieldText, + EuiModalBody, +} from '@elastic/eui'; + +import { TriggerContextExample } from './trigger_context_example'; +import { ContextMenuExamples } from './context_menu_examples'; +import { UiActionsExplorerServices } from './types'; +import { HELLO_WORLD_TRIGGER_ID, ACTION_HELLO_WORLD } from '../../ui_action_examples/public'; +import { + toMountPoint, + useOpenSearchDashboards, +} from '../../../src/plugins/opensearch_dashboards_react/public'; +import { createAction } from '../../../src/plugins/ui_actions/public'; + +export const BasicTab = () => { + const [name, setName] = useState('Waldo'); + const [confirmationText, setConfirmationText] = useState(''); + const { + services: { + uiActions, + overlays: { openModal }, + }, + } = useOpenSearchDashboards(); + + return ( + <> + + +

+ By default there is a single action attached to the `HELLO_WORLD_TRIGGER`. Clicking this + button will cause it to be executed immediately. +

+
+ + uiActions.executeTriggerActions(HELLO_WORLD_TRIGGER_ID, {})} + > + Say hello world! + + + + +

+ Lets dynamically add new actions to this trigger. After you click this button, click the + above button again. This time it should offer you multiple options to choose from. Using + the UI Action and Trigger API makes your plugin extensible by other plugins. Any actions + attached to the `HELLO_WORLD_TRIGGER_ID` will show up here! +

+ setName(e.target.value)} /> + + + { + const dynamicAction = createAction({ + id: `${ACTION_HELLO_WORLD}-${name}`, + type: ACTION_HELLO_WORLD, + getDisplayName: () => `Say hello to ${name}`, + execute: async () => { + const overlay = openModal( + toMountPoint( + + + {`Hello ${name}`} + {' '} + overlay.close()}> + Close + + + ) + ); + }, + }); + uiActions.addTriggerAction(HELLO_WORLD_TRIGGER_ID, dynamicAction); + setConfirmationText( + `You've successfully added a new action: ${dynamicAction.getDisplayName({ + trigger: uiActions.getTrigger(HELLO_WORLD_TRIGGER_ID), + })}. Refresh the page to reset state. It's up to the user of the system to persist state like this.` + ); + }} + > + Say hello to me! + + {confirmationText !== '' ? {confirmationText} : undefined} +
+ + + + + + + + + + ); +}; diff --git a/examples/ui_actions_explorer/public/explorer_tab.tsx b/examples/ui_actions_explorer/public/explorer_tab.tsx new file mode 100644 index 000000000000..dc9a6355e6c0 --- /dev/null +++ b/examples/ui_actions_explorer/public/explorer_tab.tsx @@ -0,0 +1,87 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { EuiCallOut, EuiTitle, EuiSpacer, EuiBasicTable } from '@elastic/eui'; +import { i18n } from '@osd/i18n'; +import { FormattedMessage } from '@osd/i18n/react'; +import React, { useMemo } from 'react'; +import { UiActionsExplorerServices } from './types'; +import { useOpenSearchDashboards } from '../../../src/plugins/opensearch_dashboards_react/public'; +import {} from '../../../src/plugins/ui_actions/public'; + +interface TriggerItem { + actions: string[]; + id: any; + title?: string | undefined; + description?: string | undefined; +} + +export const ExplorerTab = () => { + const { + services: { uiActions }, + } = useOpenSearchDashboards(); + const triggers: TriggerItem[] = useMemo( + () => + Array.from(uiActions.getTriggers().values()).map(({ trigger }) => { + return { + ...trigger, + actions: uiActions.getTriggerActions(trigger.id).map((action) => action.id), + }; + }), + [uiActions] + ); + + return ( + <> + + + + + + + +

Triggers

+
+ + ( +
    + {actions.map((action) => ( +
  • {action}
  • + ))} +
+ ), + }, + ]} + items={triggers} + /> + + ); +}; diff --git a/examples/ui_actions_explorer/public/plugin.tsx b/examples/ui_actions_explorer/public/plugin.tsx index 819ddf5feb4d..a0eaf1d8e0d7 100644 --- a/examples/ui_actions_explorer/public/plugin.tsx +++ b/examples/ui_actions_explorer/public/plugin.tsx @@ -28,7 +28,6 @@ * under the License. */ -import { UiActionsStart, UiActionsSetup } from '../../../src/plugins/ui_actions/public'; import { Plugin, CoreSetup, AppMountParameters, AppNavLinkStatus } from '../../../src/core/public'; import { PHONE_TRIGGER, @@ -50,17 +49,12 @@ import { ACTION_TRIGGER_PHONE_USER, createTriggerPhoneTriggerAction, } from './actions/actions'; -import { DeveloperExamplesSetup } from '../../developer_examples/public'; import image from './ui_actions.png'; - -interface StartDeps { - uiActions: UiActionsStart; -} - -interface SetupDeps { - uiActions: UiActionsSetup; - developerExamples: DeveloperExamplesSetup; -} +import { + UiActionsExplorerPluginSetup, + UiActionsExplorerPluginStart, + UiActionsExplorerStartDependencies, +} from './types'; declare module '../../../src/plugins/ui_actions/public' { export interface TriggerContextMapping { @@ -79,8 +73,12 @@ declare module '../../../src/plugins/ui_actions/public' { } } -export class UiActionsExplorerPlugin implements Plugin { - public setup(core: CoreSetup, deps: SetupDeps) { +export class UiActionsExplorerPlugin + implements Plugin { + public setup( + core: CoreSetup, + deps: UiActionsExplorerPluginSetup + ) { deps.uiActions.registerTrigger({ id: COUNTRY_TRIGGER, }); @@ -116,10 +114,7 @@ export class UiActionsExplorerPlugin implements Plugin(); + const columns = [ { id: 'name', @@ -118,12 +120,12 @@ export function TriggerContextExample({ uiActionsApi }: Props) { const updateUser = (newUser: User, oldName: string) => { const index = rows.findIndex((u) => u.name === oldName); const newRows = [...rows]; - newRows.splice(index, 1, createRowData(newUser, uiActionsApi, updateUser)); + newRows.splice(index, 1, createRowData(newUser, uiActions, updateUser)); setRows(newRows); }; const initialRows: UserRowData[] = rawData.map((user: User) => - createRowData(user, uiActionsApi, updateUser) + createRowData(user, uiActions, updateUser) ); const [rows, setRows] = useState(initialRows); diff --git a/examples/ui_actions_explorer/public/types.ts b/examples/ui_actions_explorer/public/types.ts new file mode 100644 index 000000000000..d2e009dbd9f0 --- /dev/null +++ b/examples/ui_actions_explorer/public/types.ts @@ -0,0 +1,25 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { DeveloperExamplesSetup } from '../../developer_examples/public'; +import { CoreStart } from '../../../src/core/public'; +import { UiActionsSetup, UiActionsStart } from '../../../src/plugins/ui_actions/public'; + +export interface UiActionsExplorerPluginStart { + uiActions: UiActionsStart; +} + +export interface UiActionsExplorerPluginSetup { + uiActions: UiActionsSetup; + developerExamples: DeveloperExamplesSetup; +} + +export interface UiActionsExplorerStartDependencies { + uiActions: UiActionsStart; +} + +export interface UiActionsExplorerServices extends CoreStart { + uiActions: UiActionsStart; +} diff --git a/src/plugins/ui_actions/public/service/ui_actions_service.ts b/src/plugins/ui_actions/public/service/ui_actions_service.ts index 276dfb24519a..0dd2fc4cde40 100644 --- a/src/plugins/ui_actions/public/service/ui_actions_service.ts +++ b/src/plugins/ui_actions/public/service/ui_actions_service.ts @@ -88,6 +88,10 @@ export class UiActionsService { return trigger.contract; }; + public readonly getTriggers = (): TriggerRegistry => { + return this.triggers; + }; + public readonly registerAction = ( definition: A ): Action> => { From 3d5cdd9cef89c804226b44971b4fde358f464d2e Mon Sep 17 00:00:00 2001 From: Ashwin P Chandran Date: Tue, 14 Mar 2023 00:34:46 +0000 Subject: [PATCH 2/5] updates UI actions readme Signed-off-by: Ashwin P Chandran --- src/plugins/ui_actions/README.md | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/plugins/ui_actions/README.md b/src/plugins/ui_actions/README.md index c4e02b551c88..2816fe45c947 100644 --- a/src/plugins/ui_actions/README.md +++ b/src/plugins/ui_actions/README.md @@ -1,6 +1,15 @@ # UI Actions -An API for: +This plugin exposes a global event bus for the OpenSearch Dashboards UI that allows other plugins to expand the ui capabilities of the application using `actions` and `triggers`. Plugins can not only register actions and triggers that trigger an action, but also use existing triggers and actions for their own use case. Multiple actions can be associated with a single trigger. All the capabilities are exposed using the uiActions service. + +Some of the uses in Dashboards for UI Actions are: + +1. For the context menus in a dashboard panel +2. Interacting directly with a visualization to trigger filters and to select time ranges. + +## API + +You can use the UI Actions service API's for the following use cases: - creating custom functionality (`actions`) - creating custom user interaction events (`triggers`) @@ -8,3 +17,13 @@ An API for: - emitting `trigger` events - executing `actions` attached to a given `trigger`. - exposing a context menu for the user to choose the appropriate action when there are multiple actions attached to a single trigger. + +The API for the service can be found in [./public/service/ui_actions_service.ts](./public/service/ui_actions_service.ts) + +## Explorer + +Use the UI actions explorer in the Developer examples to learn more about the service and its features. It can be started up using the `--run-examples` flag and found under the `Developer examples` option in the main menu. + +```sh +yarn start --run-examples +``` From a4868e818fd77d39696d2ab8f783ee1c17df5407 Mon Sep 17 00:00:00 2001 From: Ashwin P Chandran Date: Tue, 14 Mar 2023 00:37:16 +0000 Subject: [PATCH 3/5] updates UI actions readme again Signed-off-by: Ashwin P Chandran --- src/plugins/ui_actions/README.md | 70 ++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/src/plugins/ui_actions/README.md b/src/plugins/ui_actions/README.md index 2816fe45c947..28e3b2d63d2e 100644 --- a/src/plugins/ui_actions/README.md +++ b/src/plugins/ui_actions/README.md @@ -20,6 +20,76 @@ You can use the UI Actions service API's for the following use cases: The API for the service can be found in [./public/service/ui_actions_service.ts](./public/service/ui_actions_service.ts) +## Usage + +### Creating an action + +```ts +const ACTION_ID = 'ACTION_ID'; + +// Declare the context mapping so that it is clear to the user what context the action should receive +declare module '../../../src/plugins/ui_actions/public' { + export interface ActionContextMapping { + [ACTION_ID]: ActionContext; + } +} + +// Create the action +const action = createAction({ + execute: async (context: ActionContext) => {}, // Action to execute when called + id: ACTION_ID, + // ...other action properties +}); + +// Register the action with the service +uiActions.registerAction(action); +``` + +### Creating a trigger + +```ts +const TRIGGER_ID = 'TRIGGER_ID'; + +// Declare the context mapping so that it is clear to the user what context the trigger should be called with +declare module '../../../src/plugins/ui_actions/public' { + export interface TriggerContextMapping { + [TRIGGER_ID]: TriggerContext; // The context that the trigger will execute with + } +} + +// Create the trigger +const trigger: Trigger<'TRIGGER_ID'> = { + id: TRIGGER_ID, +}; + +// Register the trigger +uiActions.registerTrigger(trigger); +``` + +### Attach an action to a trigger + +There are two ways to do this: + +1. Attach a registered action to a registered trigger + +```ts +uiActions.attachAction(TRIGGER_ID, ACTION_ID); +``` + +2. Register a action to a registered trigger (If the action is not registered, this method also registers the action) + +```ts +uiActions.addTriggerAction(TRIGGER_ID, action); +``` + +### Trigger an event + +Triggering an action is very simple. Just get the trigger using its ID and execute it with the appropriate context. + +```ts +uiActions.getTrigger(trigger.id).exec(context); +``` + ## Explorer Use the UI actions explorer in the Developer examples to learn more about the service and its features. It can be started up using the `--run-examples` flag and found under the `Developer examples` option in the main menu. From 8a18e3ab2f4c9d38f2e9065b9ee034d601f92623 Mon Sep 17 00:00:00 2001 From: Ashwin P Chandran Date: Wed, 15 Mar 2023 22:41:32 +0000 Subject: [PATCH 4/5] cleans up developer examples view Signed-off-by: Ashwin P Chandran --- examples/developer_examples/public/app.tsx | 44 +++++++++++-------- .../public/plugin.ts | 6 +-- 2 files changed, 29 insertions(+), 21 deletions(-) diff --git a/examples/developer_examples/public/app.tsx b/examples/developer_examples/public/app.tsx index 90c28e3be8c6..974c114f526d 100644 --- a/examples/developer_examples/public/app.tsx +++ b/examples/developer_examples/public/app.tsx @@ -36,13 +36,12 @@ import { EuiPageContent, EuiCard, EuiPageContentHeader, - EuiFlexGroup, - EuiFlexItem, EuiFieldSearch, EuiListGroup, EuiHighlight, EuiLink, EuiButtonIcon, + EuiPage, } from '@elastic/eui'; import { AppMountParameters } from '../../../src/core/public'; import { ExampleDefinition } from './types'; @@ -66,12 +65,14 @@ function DeveloperExamples({ examples, navigateToApp, getUrlForApp }: Props) { }); return ( - - - -

Developer examples

-

- The following examples showcase services and APIs that are available to developers. + + + + +

Developer examples

+

+ The following examples showcase services and APIs that are available to developers. +

-

-
-
- - {filteredExamples.map((def) => ( - + + +
+ {filteredExamples.map((def) => ( {def.description} @@ -114,11 +120,13 @@ function DeveloperExamples({ examples, navigateToApp, getUrlForApp }: Props) { } image={def.image} footer={def.links ? : undefined} + titleSize="xs" + textAlign="left" /> - - ))} - - + ))} +
+
+ ); } diff --git a/examples/state_containers_examples/public/plugin.ts b/examples/state_containers_examples/public/plugin.ts index 2da5d36b3b90..e404c48ccd86 100644 --- a/examples/state_containers_examples/public/plugin.ts +++ b/examples/state_containers_examples/public/plugin.ts @@ -82,7 +82,7 @@ export class StateContainersExamplesPlugin implements Plugin { developerExamples.register({ appId: 'stateContainersExampleBrowserHistory', - title: 'State containers using browser history', + title: 'State containers: browser history', description: `An example todo app that uses browser history and state container utilities like createStateContainerReactHelpers, createStateContainer, createOsdUrlStateStorage, createSessionStorageStateStorage, syncStates and getStateFromOsdUrl to keep state in sync with the URL. Change some parameters, navigate away and then back, and the @@ -101,7 +101,7 @@ export class StateContainersExamplesPlugin implements Plugin { developerExamples.register({ appId: 'stateContainersExampleHashHistory', - title: 'State containers using hash history', + title: 'State containers: hash history', description: `An example todo app that uses hash history and state container utilities like createStateContainerReactHelpers, createStateContainer, createOsdUrlStateStorage, createSessionStorageStateStorage, syncStates and getStateFromOsdUrl to keep state in sync with the URL. Change some parameters, navigate away and then back, and the @@ -120,7 +120,7 @@ export class StateContainersExamplesPlugin implements Plugin { developerExamples.register({ appId: PLUGIN_ID, - title: 'Sync state from a query bar with the url', + title: 'State containers: Sync with the url', description: `Shows how to use data.syncQueryStateWitUrl in combination with state container utilities from opensearch_dashboards_utils to show a query bar that stores state in the url and is kept in sync. `, From 5f6c86a203b88f2d1428c3f922b0a85d9b0b7ab8 Mon Sep 17 00:00:00 2001 From: Ashwin P Chandran Date: Thu, 16 Mar 2023 16:37:04 +0000 Subject: [PATCH 5/5] adds changelog Signed-off-by: Ashwin P Chandran --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6178d1836203..131217fb0619 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,7 +20,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - [Security] Bumps hapi/statehood to 7.0.4 ([#3411](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3411)) - [CVE-2023-25166] Bump formula to 3.0.1 ([#3416](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3416)) - [CVE-2023-25653] Bump node-jose to 2.2.0 ([#3445](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3445)) -- [CVE-2023-26486][CVE-2023-26487] Bump vega from 5.22.1 to 5.23.0 ([#3533](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3533)) +- [CVE-2023-26486][cve-2023-26487] Bump vega from 5.22.1 to 5.23.0 ([#3533](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3533)) ### 📈 Features/Enhancements @@ -132,6 +132,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - Correct copyright date range of NOTICE file and notice generator ([#3308](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3308)) - Simplify the in-code instructions for upgrading `re2` ([#3328](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3328)) - [Doc] Add docker dev set up instruction ([#3444](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3444)) +- [Doc] UI actions explorer ([#3614](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3614)) ### 🛠 Maintenance