diff --git a/examples/ui_action_examples/README.md b/examples/ui_action_examples/README.md new file mode 100644 index 00000000000000..4e4f1c2ffe8419 --- /dev/null +++ b/examples/ui_action_examples/README.md @@ -0,0 +1,8 @@ +## Ui actions examples + +These ui actions examples shows how to: + - Register new actions + - Register custom triggers + - Attach an action to a trigger + +To run this example, use the command `yarn start --run-examples`. \ No newline at end of file diff --git a/examples/ui_action_examples/kibana.json b/examples/ui_action_examples/kibana.json new file mode 100644 index 00000000000000..d5c3f0f2ec33a9 --- /dev/null +++ b/examples/ui_action_examples/kibana.json @@ -0,0 +1,10 @@ +{ + "id": "uiActionsExamples", + "version": "0.0.1", + "kibanaVersion": "kibana", + "configPath": ["ui_actions_examples"], + "server": false, + "ui": true, + "requiredPlugins": ["uiActions"], + "optionalPlugins": [] +} diff --git a/examples/ui_action_examples/package.json b/examples/ui_action_examples/package.json new file mode 100644 index 00000000000000..3d1201ad68b3bf --- /dev/null +++ b/examples/ui_action_examples/package.json @@ -0,0 +1,17 @@ +{ + "name": "ui_actions_examples", + "version": "1.0.0", + "main": "target/examples/ui_actions_examples", + "kibana": { + "version": "kibana", + "templateVersion": "1.0.0" + }, + "license": "Apache-2.0", + "scripts": { + "kbn": "node ../../scripts/kbn.js", + "build": "rm -rf './target' && tsc" + }, + "devDependencies": { + "typescript": "3.5.3" + } +} diff --git a/examples/ui_action_examples/public/hello_world_action.tsx b/examples/ui_action_examples/public/hello_world_action.tsx new file mode 100644 index 00000000000000..e07855a6f422cb --- /dev/null +++ b/examples/ui_action_examples/public/hello_world_action.tsx @@ -0,0 +1,43 @@ +/* + * 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 React from 'react'; +import { EuiText, EuiModalBody, EuiButton } from '@elastic/eui'; +import { OverlayStart } from '../../../src/core/public'; +import { createAction } from '../../../src/plugins/ui_actions/public'; +import { toMountPoint } from '../../../src/plugins/kibana_react/public'; + +export const HELLO_WORLD_ACTION_TYPE = 'HELLO_WORLD_ACTION_TYPE'; + +export const createHelloWorldAction = (openModal: OverlayStart['openModal']) => + createAction<{}>({ + type: HELLO_WORLD_ACTION_TYPE, + getDisplayName: () => 'Hello World!', + execute: async () => { + const overlay = openModal( + toMountPoint( + + Hello world! + overlay.close()}> + Close + + + ) + ); + }, + }); diff --git a/examples/ui_action_examples/public/hello_world_trigger.ts b/examples/ui_action_examples/public/hello_world_trigger.ts new file mode 100644 index 00000000000000..999a7d9864707e --- /dev/null +++ b/examples/ui_action_examples/public/hello_world_trigger.ts @@ -0,0 +1,28 @@ +/* + * 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 { Trigger } from '../../../src/plugins/ui_actions/public'; +import { HELLO_WORLD_ACTION_TYPE } from './hello_world_action'; + +export const HELLO_WORLD_TRIGGER_ID = 'HELLO_WORLD_TRIGGER_ID'; + +export const helloWorldTrigger: Trigger = { + id: HELLO_WORLD_TRIGGER_ID, + actionIds: [HELLO_WORLD_ACTION_TYPE], +}; diff --git a/examples/ui_action_examples/public/index.ts b/examples/ui_action_examples/public/index.ts new file mode 100644 index 00000000000000..9dce2191d2670d --- /dev/null +++ b/examples/ui_action_examples/public/index.ts @@ -0,0 +1,26 @@ +/* + * 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 { UiActionExamplesPlugin } from './plugin'; +import { PluginInitializer } from '../../../src/core/public'; + +export const plugin: PluginInitializer = () => new UiActionExamplesPlugin(); + +export { HELLO_WORLD_TRIGGER_ID } from './hello_world_trigger'; +export { HELLO_WORLD_ACTION_TYPE } from './hello_world_action'; diff --git a/examples/ui_action_examples/public/plugin.ts b/examples/ui_action_examples/public/plugin.ts new file mode 100644 index 00000000000000..ef0689227d6bdf --- /dev/null +++ b/examples/ui_action_examples/public/plugin.ts @@ -0,0 +1,45 @@ +/* + * 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 { Plugin, CoreSetup, CoreStart } from '../../../src/core/public'; +import { UiActionsSetup, UiActionsStart } from '../../../src/plugins/ui_actions/public'; +import { createHelloWorldAction } from './hello_world_action'; +import { helloWorldTrigger } from './hello_world_trigger'; + +interface UiActionExamplesSetupDependencies { + uiActions: UiActionsSetup; +} + +interface UiActionExamplesStartDependencies { + uiActions: UiActionsStart; +} + +export class UiActionExamplesPlugin + implements + Plugin { + public setup(core: CoreSetup, deps: UiActionExamplesSetupDependencies) { + deps.uiActions.registerTrigger(helloWorldTrigger); + } + + public start(coreStart: CoreStart, deps: UiActionExamplesStartDependencies) { + deps.uiActions.registerAction(createHelloWorldAction(coreStart.overlays.openModal)); + } + + public stop() {} +} diff --git a/examples/ui_action_examples/tsconfig.json b/examples/ui_action_examples/tsconfig.json new file mode 100644 index 00000000000000..d508076b331990 --- /dev/null +++ b/examples/ui_action_examples/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./target", + "skipLibCheck": true + }, + "include": [ + "index.ts", + "public/**/*.ts", + "public/**/*.tsx", + "server/**/*.ts", + "../../typings/**/*", + ], + "exclude": [] +} diff --git a/examples/ui_actions_explorer/README.md b/examples/ui_actions_explorer/README.md new file mode 100644 index 00000000000000..0037d77d916cf3 --- /dev/null +++ b/examples/ui_actions_explorer/README.md @@ -0,0 +1,8 @@ +## Ui actions explorer + +This example ui actions explorer app shows how to: + - Add custom ui actions to existing triggers + - Add custom triggers + + +To run this example, use the command `yarn start --run-examples`. \ No newline at end of file diff --git a/examples/ui_actions_explorer/kibana.json b/examples/ui_actions_explorer/kibana.json new file mode 100644 index 00000000000000..126e79eb35757e --- /dev/null +++ b/examples/ui_actions_explorer/kibana.json @@ -0,0 +1,10 @@ +{ + "id": "uiActionsExplorer", + "version": "0.0.1", + "kibanaVersion": "kibana", + "configPath": ["ui_actions_explorer"], + "server": false, + "ui": true, + "requiredPlugins": ["uiActions", "uiActionsExamples"], + "optionalPlugins": [] +} diff --git a/examples/ui_actions_explorer/package.json b/examples/ui_actions_explorer/package.json new file mode 100644 index 00000000000000..d13bf860286808 --- /dev/null +++ b/examples/ui_actions_explorer/package.json @@ -0,0 +1,17 @@ +{ + "name": "ui_actions_explorer", + "version": "1.0.0", + "main": "target/examples/ui_actions_explorer", + "kibana": { + "version": "kibana", + "templateVersion": "1.0.0" + }, + "license": "Apache-2.0", + "scripts": { + "kbn": "node ../../scripts/kbn.js", + "build": "rm -rf './target' && tsc" + }, + "devDependencies": { + "typescript": "3.5.3" + } +} diff --git a/examples/ui_actions_explorer/public/actions/actions.tsx b/examples/ui_actions_explorer/public/actions/actions.tsx new file mode 100644 index 00000000000000..821a1205861e65 --- /dev/null +++ b/examples/ui_actions_explorer/public/actions/actions.tsx @@ -0,0 +1,131 @@ +/* + * 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 React from 'react'; +import { OverlayStart } from 'kibana/public'; +import { EuiFieldText, EuiModalBody, EuiButton } from '@elastic/eui'; +import { useState } from 'react'; +import { toMountPoint } from '../../../../src/plugins/kibana_react/public'; +import { createAction, UiActionsStart } from '../../../../src/plugins/ui_actions/public'; + +export const USER_TRIGGER = 'USER_TRIGGER'; +export const COUNTRY_TRIGGER = 'COUNTRY_TRIGGER'; +export const PHONE_TRIGGER = 'PHONE_TRIGGER'; + +export const VIEW_IN_MAPS_ACTION = 'VIEW_IN_MAPS_ACTION'; +export const TRAVEL_GUIDE_ACTION = 'TRAVEL_GUIDE_ACTION'; +export const CALL_PHONE_NUMBER_ACTION = 'CALL_PHONE_NUMBER_ACTION'; +export const EDIT_USER_ACTION = 'EDIT_USER_ACTION'; +export const PHONE_USER_ACTION = 'PHONE_USER_ACTION'; +export const SHOWCASE_PLUGGABILITY_ACTION = 'SHOWCASE_PLUGGABILITY_ACTION'; + +export const showcasePluggability = createAction<{}>({ + type: SHOWCASE_PLUGGABILITY_ACTION, + getDisplayName: () => 'This is pluggable! Any plugin can inject their actions here.', + execute: async ({}) => alert("Isn't that cool?!"), +}); + +export const makePhoneCallAction = createAction<{ phone: string }>({ + type: CALL_PHONE_NUMBER_ACTION, + getDisplayName: () => 'Call phone number', + execute: async ({ phone }) => alert(`Pretend calling ${phone}...`), +}); + +export const lookUpWeatherAction = createAction<{ country: string }>({ + type: TRAVEL_GUIDE_ACTION, + getIconType: () => 'popout', + getDisplayName: () => 'View travel guide', + execute: async ({ country }) => { + window.open(`https://www.worldtravelguide.net/?s=${country},`, '_blank'); + }, +}); + +export const viewInMapsAction = createAction<{ country: string }>({ + type: VIEW_IN_MAPS_ACTION, + getIconType: () => 'popout', + getDisplayName: () => 'View in maps', + execute: async ({ country }) => { + window.open(`https://www.google.com/maps/place/${country}`, '_blank'); + }, +}); + +export interface User { + phone?: string; + countryOfResidence: string; + name: string; +} + +function EditUserModal({ + user, + update, + close, +}: { + user: User; + update: (user: User) => void; + close: () => void; +}) { + const [name, setName] = useState(user.name); + return ( + + setName(e.target.value)} /> + { + update({ ...user, name }); + close(); + }} + > + Update + + + ); +} + +export const createEditUserAction = (getOpenModal: () => Promise) => + createAction<{ + user: User; + update: (user: User) => void; + }>({ + type: EDIT_USER_ACTION, + getIconType: () => 'pencil', + getDisplayName: () => 'Edit user', + execute: async ({ user, update }) => { + const overlay = (await getOpenModal())( + toMountPoint( overlay.close()} />) + ); + }, + }); + +export const createPhoneUserAction = (getUiActionsApi: () => Promise) => + createAction<{ + user: User; + update: (user: User) => void; + }>({ + type: PHONE_USER_ACTION, + getDisplayName: () => 'Call phone number', + isCompatible: async ({ user }) => user.phone !== undefined, + execute: async ({ user }) => { + // One option - execute the more specific action directly. + // makePhoneCallAction.execute({ phone: user.phone }); + + // Another option - emit the trigger and automatically get *all* the actions attached + // to the phone number trigger. + // TODO: we need to figure out the best way to handle these nested actions however, since + // we don't want multiple context menu's to pop up. + (await getUiActionsApi()).executeTriggerActions(PHONE_TRIGGER, { phone: user.phone }); + }, + }); diff --git a/examples/ui_actions_explorer/public/app.tsx b/examples/ui_actions_explorer/public/app.tsx new file mode 100644 index 00000000000000..bd7ba05def1f24 --- /dev/null +++ b/examples/ui_actions_explorer/public/app.tsx @@ -0,0 +1,124 @@ +/* + * 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 React, { useState } from 'react'; +import ReactDOM from 'react-dom'; + +import { EuiPage } from '@elastic/eui'; + +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/kibana_react/public'; +import { UiActionsStart, createAction } from '../../../src/plugins/ui_actions/public'; +import { AppMountParameters, OverlayStart } from '../../../src/core/public'; +import { HELLO_WORLD_TRIGGER_ID, HELLO_WORLD_ACTION_TYPE } from '../../ui_action_examples/public'; +import { TriggerContextExample } from './trigger_context_example'; + +interface Props { + uiActionsApi: UiActionsStart; + openModal: OverlayStart['openModal']; +} + +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<{}>({ + type: `${HELLO_WORLD_ACTION_TYPE}-${name}`, + getDisplayName: () => `Say hello to ${name}`, + execute: async () => { + const overlay = openModal( + toMountPoint( + + + {`Hello ${name}`} + {' '} + overlay.close()}> + Close + + + ) + ); + }, + }); + uiActionsApi.registerAction(dynamicAction); + uiActionsApi.attachAction(HELLO_WORLD_TRIGGER_ID, dynamicAction.type); + setConfirmationText( + `You've successfully added a new action: ${dynamicAction.getDisplayName( + {} + )}. 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); + + return () => ReactDOM.unmountComponentAtNode(element); +}; diff --git a/examples/ui_actions_explorer/public/index.ts b/examples/ui_actions_explorer/public/index.ts new file mode 100644 index 00000000000000..9bf99911e946a7 --- /dev/null +++ b/examples/ui_actions_explorer/public/index.ts @@ -0,0 +1,22 @@ +/* + * 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 { UiActionsExplorerPlugin } from './plugin'; + +export const plugin = () => new UiActionsExplorerPlugin(); diff --git a/examples/ui_actions_explorer/public/page.tsx b/examples/ui_actions_explorer/public/page.tsx new file mode 100644 index 00000000000000..90bea358048221 --- /dev/null +++ b/examples/ui_actions_explorer/public/page.tsx @@ -0,0 +1,51 @@ +/* + * 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 React from 'react'; + +import { + EuiPageBody, + EuiPageContent, + EuiPageContentBody, + EuiPageHeader, + EuiPageHeaderSection, + EuiTitle, +} from '@elastic/eui'; + +interface PageProps { + title: string; + children: React.ReactNode; +} + +export function Page({ title, children }: PageProps) { + return ( + + + + +

{title}

+
+
+
+ + {children} + +
+ ); +} diff --git a/examples/ui_actions_explorer/public/plugin.tsx b/examples/ui_actions_explorer/public/plugin.tsx new file mode 100644 index 00000000000000..9c5f967a466bf8 --- /dev/null +++ b/examples/ui_actions_explorer/public/plugin.tsx @@ -0,0 +1,110 @@ +/* + * 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 { Plugin, CoreSetup, AppMountParameters } from 'kibana/public'; +import { UiActionsStart, UiActionsSetup } from 'src/plugins/ui_actions/public'; +import { ISearchAppMountContext } from '../../../src/plugins/data/public'; +import { + PHONE_TRIGGER, + USER_TRIGGER, + COUNTRY_TRIGGER, + createPhoneUserAction, + lookUpWeatherAction, + viewInMapsAction, + createEditUserAction, + CALL_PHONE_NUMBER_ACTION, + VIEW_IN_MAPS_ACTION, + TRAVEL_GUIDE_ACTION, + PHONE_USER_ACTION, + EDIT_USER_ACTION, + makePhoneCallAction, + showcasePluggability, + SHOWCASE_PLUGGABILITY_ACTION, +} from './actions/actions'; + +declare module 'kibana/public' { + interface AppMountContext { + search?: ISearchAppMountContext; + } +} + +interface StartDeps { + uiActions: UiActionsStart; +} + +interface SetupDeps { + uiActions: UiActionsSetup; +} + +export class UiActionsExplorerPlugin implements Plugin { + public setup(core: CoreSetup<{ uiActions: UiActionsStart }>, deps: SetupDeps) { + deps.uiActions.registerTrigger({ + id: COUNTRY_TRIGGER, + actionIds: [], + }); + deps.uiActions.registerTrigger({ + id: PHONE_TRIGGER, + actionIds: [], + }); + deps.uiActions.registerTrigger({ + id: USER_TRIGGER, + actionIds: [], + }); + deps.uiActions.registerAction(lookUpWeatherAction); + deps.uiActions.registerAction(viewInMapsAction); + deps.uiActions.registerAction(makePhoneCallAction); + deps.uiActions.registerAction(showcasePluggability); + + const startServices = core.getStartServices(); + deps.uiActions.registerAction( + createPhoneUserAction(async () => (await startServices)[1].uiActions) + ); + deps.uiActions.registerAction( + createEditUserAction(async () => (await startServices)[0].overlays.openModal) + ); + deps.uiActions.attachAction(USER_TRIGGER, PHONE_USER_ACTION); + deps.uiActions.attachAction(USER_TRIGGER, EDIT_USER_ACTION); + + // What's missing here is type analysis to ensure the context emitted by the trigger + // is the same context that the action requires. + deps.uiActions.attachAction(COUNTRY_TRIGGER, VIEW_IN_MAPS_ACTION); + deps.uiActions.attachAction(COUNTRY_TRIGGER, TRAVEL_GUIDE_ACTION); + deps.uiActions.attachAction(COUNTRY_TRIGGER, SHOWCASE_PLUGGABILITY_ACTION); + deps.uiActions.attachAction(PHONE_TRIGGER, CALL_PHONE_NUMBER_ACTION); + deps.uiActions.attachAction(PHONE_TRIGGER, SHOWCASE_PLUGGABILITY_ACTION); + deps.uiActions.attachAction(USER_TRIGGER, SHOWCASE_PLUGGABILITY_ACTION); + + core.application.register({ + id: 'uiActionsExplorer', + title: 'Ui Actions Explorer', + async mount(params: AppMountParameters) { + const [coreStart, depsStart] = await core.getStartServices(); + const { renderApp } = await import('./app'); + return renderApp( + { uiActionsApi: depsStart.uiActions, openModal: coreStart.overlays.openModal }, + params + ); + }, + }); + } + + public start() {} + + public stop() {} +} diff --git a/examples/ui_actions_explorer/public/trigger_context_example.tsx b/examples/ui_actions_explorer/public/trigger_context_example.tsx new file mode 100644 index 00000000000000..09e1de05bb3139 --- /dev/null +++ b/examples/ui_actions_explorer/public/trigger_context_example.tsx @@ -0,0 +1,151 @@ +/* + * 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 React, { Fragment, useMemo, useState } from 'react'; +import { EuiButtonEmpty } from '@elastic/eui'; +import { EuiText } from '@elastic/eui'; +import { EuiDataGrid } from '@elastic/eui'; +import { EuiDataGridCellValueElementProps } from '@elastic/eui'; +import { UiActionsStart } from '../../../src/plugins/ui_actions/public'; +import { USER_TRIGGER, PHONE_TRIGGER, COUNTRY_TRIGGER, User } from './actions/actions'; + +export interface Props { + uiActionsApi: UiActionsStart; +} + +interface UserRowData { + name: string; + countryOfResidence: React.ReactNode; + phone: React.ReactNode; + rowActions: React.ReactNode; + [key: string]: any; +} + +const createRowData = ( + user: User, + uiActionsApi: UiActionsStart, + update: (newUser: User, oldName: string) => void +) => ({ + name: user.name, + countryOfResidence: ( + + { + uiActionsApi.executeTriggerActions(COUNTRY_TRIGGER, { + country: user.countryOfResidence, + }); + }} + > + {user.countryOfResidence} + + + ), + phone: ( + + { + uiActionsApi.executeTriggerActions(PHONE_TRIGGER, { + phone: user.phone, + }); + }} + > + {user.phone} + + + ), + rowActions: ( + + { + uiActionsApi.executeTriggerActions(USER_TRIGGER, { + user, + update: (newUser: User) => update(newUser, user.name), + }); + }} + > + Actions + + + ), +}); + +export function TriggerContextExample({ uiActionsApi }: Props) { + const columns = [ + { + id: 'name', + }, + { + id: 'countryOfResidence', + }, + { + id: 'phone', + }, + { + id: 'rowActions', + }, + ]; + + const rawData = [ + { name: 'Sue', countryOfResidence: 'USA', phone: '1-519-555-1234' }, + { name: 'Bob', countryOfResidence: 'Germany' }, + { name: 'Tom', countryOfResidence: 'Russia', phone: '45-555-444-1234' }, + ]; + + 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)); + setRows(newRows); + }; + + const initialRows: UserRowData[] = rawData.map((user: User) => + createRowData(user, uiActionsApi, updateUser) + ); + + const [rows, setRows] = useState(initialRows); + + const renderCellValue = useMemo(() => { + return ({ rowIndex, columnId }: EuiDataGridCellValueElementProps) => { + return rows.hasOwnProperty(rowIndex) ? rows[rowIndex][columnId] : null; + }; + }, [rows]); + + return ( + +

Triggers that emit context

+

+ The trigger above did not emit any context, but a trigger can, and if it does, it will be + passed to the action when it is executed. This is helpful for dynamic data that is only + known at the time the trigger is emitted. Lets explore a use case where the is dynamic. The + following data grid emits a few triggers, each with a some actions attached. +

+ + {}, + }} + /> +
+ ); +} diff --git a/examples/ui_actions_explorer/tsconfig.json b/examples/ui_actions_explorer/tsconfig.json new file mode 100644 index 00000000000000..199fbe1fcfa269 --- /dev/null +++ b/examples/ui_actions_explorer/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./target", + "skipLibCheck": true + }, + "include": [ + "index.ts", + "public/**/*.ts", + "public/**/*.tsx", + "../../typings/**/*", + ], + "exclude": [] +} diff --git a/test/examples/config.js b/test/examples/config.js index f747a7fab5bb9b..d9411be2679307 100644 --- a/test/examples/config.js +++ b/test/examples/config.js @@ -24,7 +24,11 @@ export default async function({ readConfigFile }) { const functionalConfig = await readConfigFile(require.resolve('../functional/config')); return { - testFiles: [require.resolve('./search'), require.resolve('./embeddables')], + testFiles: [ + require.resolve('./search'), + require.resolve('./embeddables'), + require.resolve('./ui_actions'), + ], services: { ...functionalConfig.get('services'), ...services, diff --git a/test/examples/ui_actions/index.ts b/test/examples/ui_actions/index.ts new file mode 100644 index 00000000000000..d69e6a876cfa2e --- /dev/null +++ b/test/examples/ui_actions/index.ts @@ -0,0 +1,41 @@ +/* + * 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 { PluginFunctionalProviderContext } from 'test/plugin_functional/services'; + +// eslint-disable-next-line import/no-default-export +export default function({ + getService, + getPageObjects, + loadTestFile, +}: PluginFunctionalProviderContext) { + const browser = getService('browser'); + const appsMenu = getService('appsMenu'); + const PageObjects = getPageObjects(['common', 'header']); + + describe('ui actions explorer', function() { + before(async () => { + await browser.setWindowSize(1300, 900); + await PageObjects.common.navigateToApp('settings'); + await appsMenu.clickLink('Ui Actions Explorer'); + }); + + loadTestFile(require.resolve('./ui_actions')); + }); +} diff --git a/test/examples/ui_actions/ui_actions.ts b/test/examples/ui_actions/ui_actions.ts new file mode 100644 index 00000000000000..f047bfa333d88d --- /dev/null +++ b/test/examples/ui_actions/ui_actions.ts @@ -0,0 +1,53 @@ +/* + * 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 expect from '@kbn/expect'; + +import { PluginFunctionalProviderContext } from 'test/plugin_functional/services'; + +// eslint-disable-next-line import/no-default-export +export default function({ getService }: PluginFunctionalProviderContext) { + const testSubjects = getService('testSubjects'); + const retry = getService('retry'); + + describe('', () => { + it('hello world action', async () => { + await testSubjects.click('emitHelloWorldTrigger'); + await retry.try(async () => { + const text = await testSubjects.getVisibleText('helloWorldActionText'); + expect(text).to.be('Hello world!'); + }); + + await testSubjects.click('closeModal'); + }); + + it('dynamic hello world action', async () => { + await testSubjects.click('addDynamicAction'); + await retry.try(async () => { + await testSubjects.click('emitHelloWorldTrigger'); + await testSubjects.click('embeddablePanelAction-HELLO_WORLD_ACTION_TYPE-Waldo'); + }); + await retry.try(async () => { + const text = await testSubjects.getVisibleText('dynamicHelloWorldActionText'); + expect(text).to.be('Hello Waldo'); + }); + await testSubjects.click('closeModal'); + }); + }); +}