Ui Actions explorer example (#57006)

* wip

* Move action registration out of AppMountContext fn

* Move all registration to setup

* Fix type error
This commit is contained in:
Stacey Gammon 2020-02-11 09:08:47 -05:00 committed by GitHub
parent 62e3189c34
commit 2ae70d9592
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 929 additions and 1 deletions

View file

@ -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`.

View file

@ -0,0 +1,10 @@
{
"id": "uiActionsExamples",
"version": "0.0.1",
"kibanaVersion": "kibana",
"configPath": ["ui_actions_examples"],
"server": false,
"ui": true,
"requiredPlugins": ["uiActions"],
"optionalPlugins": []
}

View file

@ -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"
}
}

View file

@ -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(
<EuiModalBody>
<EuiText data-test-subj="helloWorldActionText">Hello world!</EuiText>
<EuiButton data-test-subj="closeModal" onClick={() => overlay.close()}>
Close
</EuiButton>
</EuiModalBody>
)
);
},
});

View file

@ -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],
};

View file

@ -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<void, void> = () => new UiActionExamplesPlugin();
export { HELLO_WORLD_TRIGGER_ID } from './hello_world_trigger';
export { HELLO_WORLD_ACTION_TYPE } from './hello_world_action';

View file

@ -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<void, void, UiActionExamplesSetupDependencies, UiActionExamplesStartDependencies> {
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() {}
}

View file

@ -0,0 +1,15 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./target",
"skipLibCheck": true
},
"include": [
"index.ts",
"public/**/*.ts",
"public/**/*.tsx",
"server/**/*.ts",
"../../typings/**/*",
],
"exclude": []
}

View file

@ -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`.

View file

@ -0,0 +1,10 @@
{
"id": "uiActionsExplorer",
"version": "0.0.1",
"kibanaVersion": "kibana",
"configPath": ["ui_actions_explorer"],
"server": false,
"ui": true,
"requiredPlugins": ["uiActions", "uiActionsExamples"],
"optionalPlugins": []
}

View file

@ -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"
}
}

View file

@ -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 (
<EuiModalBody>
<EuiFieldText prepend="Name" value={name} onChange={e => setName(e.target.value)} />
<EuiButton
onClick={() => {
update({ ...user, name });
close();
}}
>
Update
</EuiButton>
</EuiModalBody>
);
}
export const createEditUserAction = (getOpenModal: () => Promise<OverlayStart['openModal']>) =>
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(<EditUserModal user={user} update={update} close={() => overlay.close()} />)
);
},
});
export const createPhoneUserAction = (getUiActionsApi: () => Promise<UiActionsStart>) =>
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 });
},
});

View file

@ -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 (
<EuiPage>
<EuiPageBody>
<EuiPageHeader>Ui Actions Explorer</EuiPageHeader>
<EuiPageContent>
<EuiPageContentBody>
<EuiText>
<p>
By default there is a single action attached to the `HELLO_WORLD_TRIGGER`. Clicking
this button will cause it to be executed immediately.
</p>
</EuiText>
<EuiButton
data-test-subj="emitHelloWorldTrigger"
onClick={() => uiActionsApi.executeTriggerActions(HELLO_WORLD_TRIGGER_ID, {})}
>
Say hello world!
</EuiButton>
<EuiText>
<p>
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!
</p>
<EuiFieldText prepend="Name" value={name} onChange={e => setName(e.target.value)} />
<EuiButton
data-test-subj="addDynamicAction"
onClick={() => {
const dynamicAction = createAction<{}>({
type: `${HELLO_WORLD_ACTION_TYPE}-${name}`,
getDisplayName: () => `Say hello to ${name}`,
execute: async () => {
const overlay = openModal(
toMountPoint(
<EuiModalBody>
<EuiText data-test-subj="dynamicHelloWorldActionText">
{`Hello ${name}`}
</EuiText>{' '}
<EuiButton data-test-subj="closeModal" onClick={() => overlay.close()}>
Close
</EuiButton>
</EuiModalBody>
)
);
},
});
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!
</EuiButton>
{confirmationText !== '' ? <EuiCallOut>{confirmationText}</EuiCallOut> : undefined}
</EuiText>
<EuiSpacer />
<TriggerContextExample uiActionsApi={uiActionsApi} />
</EuiPageContentBody>
</EuiPageContent>
</EuiPageBody>
</EuiPage>
);
};
export const renderApp = (props: Props, { element }: AppMountParameters) => {
ReactDOM.render(<ActionsExplorer {...props} />, element);
return () => ReactDOM.unmountComponentAtNode(element);
};

View file

@ -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();

View file

@ -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 (
<EuiPageBody data-test-subj="searchTestPage">
<EuiPageHeader>
<EuiPageHeaderSection>
<EuiTitle size="l">
<h1>{title}</h1>
</EuiTitle>
</EuiPageHeaderSection>
</EuiPageHeader>
<EuiPageContent>
<EuiPageContentBody>{children}</EuiPageContentBody>
</EuiPageContent>
</EuiPageBody>
);
}

View file

@ -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<void, void, {}, StartDeps> {
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() {}
}

View file

@ -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: (
<Fragment>
<EuiButtonEmpty
onClick={() => {
uiActionsApi.executeTriggerActions(COUNTRY_TRIGGER, {
country: user.countryOfResidence,
});
}}
>
{user.countryOfResidence}
</EuiButtonEmpty>
</Fragment>
),
phone: (
<Fragment>
<EuiButtonEmpty
onClick={() => {
uiActionsApi.executeTriggerActions(PHONE_TRIGGER, {
phone: user.phone,
});
}}
>
{user.phone}
</EuiButtonEmpty>
</Fragment>
),
rowActions: (
<Fragment>
<EuiButtonEmpty
onClick={() => {
uiActionsApi.executeTriggerActions(USER_TRIGGER, {
user,
update: (newUser: User) => update(newUser, user.name),
});
}}
>
Actions
</EuiButtonEmpty>
</Fragment>
),
});
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 (
<EuiText>
<h1>Triggers that emit context</h1>
<p>
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.
</p>
<EuiDataGrid
aria-label="Action and trigger data demo"
columns={columns}
renderCellValue={renderCellValue}
rowCount={rawData.length}
columnVisibility={{
visibleColumns: ['name', 'phone', 'countryOfResidence', 'rowActions'],
setVisibleColumns: () => {},
}}
/>
</EuiText>
);
}

View file

@ -0,0 +1,14 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./target",
"skipLibCheck": true
},
"include": [
"index.ts",
"public/**/*.ts",
"public/**/*.tsx",
"../../typings/**/*",
],
"exclude": []
}

View file

@ -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,

View file

@ -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'));
});
}

View file

@ -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');
});
});
}