diff --git a/CHANGELOG.md b/CHANGELOG.md index 9809ca6c975a..05e8efef8616 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 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/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/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. `, 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/README.md b/src/plugins/ui_actions/README.md index c4e02b551c88..28e3b2d63d2e 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,83 @@ 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) + +## 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. + +```sh +yarn start --run-examples +``` 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> => {