Drilldowns in examples (#75640)

* feat: 🎸 add telemetry for in-chart "Explore underlying data"

* feat: 🎸 add telemetry for in-chart "Explore underlying data"

* refactor: 💡 move all drilldowns into a sub-folder

* feat: 🎸 setup example app section for ui_actions_enhanced

* feat: 🎸 set up Drilldown Manager section

* feat: 🎸 open drilldown manager from example plugin

* refactor: 💡 rename supportedTriggers -> triggers prop

* feat: 🎸 show dev warning if triggers prop is empty

* refactor: 💡 rename "supportedTriggers" -> "triggers" props

* feat: 🎸 open and close drilldown manager from example plugin

* feat: 🎸 add sample ML job trigger

* feat: 🎸 add sample ML URL drilldown

* refactor: 💡 move KibanaURL to share plugin

* refactor: 💡 add index file to ml drilldown

* feat: 🎸 add AbstractDashboardDrilldown

* refactor: 💡 make dashboard drilldown use abstract drilldown

* refactor: 💡 rename dashboard drilldown to embeddable drilldown

* feat: 🎸 add Dashboard drilldown to sample plugin

* feat: 🎸 open dashboard drilldown in list view

* feat: 🎸 add drilldown execute button

* refactor: 💡 move drilldown React hooks into /hooks folder

* test: 💍 fix tests after renaming triggers

* chore: 🤖 populate "requireBundles"

* fix: 🐛 fix TypeScript errors

* fix: 🐛 fix Kibana plugin dependency

* chore: 🤖 remoe unused import

* feat: 🎸 persist drilldown manager state across app navigations

* refactor: 💡 move no-embeddable example into a seprate file

* feat: 🎸 set up example with embeddable

* feat: 🎸 improve embeddable example

* refactor: 💡 rename without embeddable example

* feat: 🎸 set up no-embeddable single click example

* feat: 🎸 add dashboard drilldown to single button example

* fix: 🐛 remove unused margin

* fix: 🐛 make "Get more actions" translation static

* chore: 🤖 remove old dashboard drilldown definition

* refactor: 💡 rename samples to generic names

* refactor: 💡 make app 1 example drilldown "hello world"

* chore: 🤖 remove unused required bundle

* chore: 🤖 add dashboardEnhanced back

* [kbn/optimizer] only build xpack examples when building xpack plugins

* move alerting_example into x-pack/examples

* remove filter for alertingExample plugin in oss plugins CI step

* revert unrelated change

* fix: 🐛 use correct prop name

* test: 💍 fix embeddable-to-dashboard drilldown mock

* test: 💍 fix a test after refactor

* chore: 🤖 remove unused import

* chore: 🤖 add dashboard_enahcned to example plugin

* chore: 🤖 address review comments

* feat: 🎸 add description to UI Actions Enhanced examples

* docs: ✏️ improve docs of example plugin

Co-authored-by: spalger <spalger@users.noreply.github.com>
Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Vadim Dalecky 2020-10-05 16:31:30 +02:00 committed by GitHub
parent 13a737e675
commit 59e4e06316
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
92 changed files with 1687 additions and 493 deletions

View file

@ -31,7 +31,12 @@ export {
} from './application';
export { DashboardConstants, createDashboardEditUrl } from './dashboard_constants';
export { DashboardStart, DashboardUrlGenerator, DashboardFeatureFlagConfig } from './plugin';
export {
DashboardSetup,
DashboardStart,
DashboardUrlGenerator,
DashboardFeatureFlagConfig,
} from './plugin';
export {
DASHBOARD_APP_URL_GENERATOR,
createDashboardUrlGenerator,

View file

@ -145,7 +145,7 @@ interface StartDependencies {
savedObjects: SavedObjectsStart;
}
export type Setup = void;
export type DashboardSetup = void;
export interface DashboardStart {
getSavedDashboardLoader: () => SavedObjectLoader;
@ -180,7 +180,7 @@ declare module '../../../plugins/ui_actions/public' {
}
export class DashboardPlugin
implements Plugin<Setup, DashboardStart, SetupDependencies, StartDependencies> {
implements Plugin<DashboardSetup, DashboardStart, SetupDependencies, StartDependencies> {
constructor(private initializerContext: PluginInitializerContext) {}
private appStateUpdater = new BehaviorSubject<AppUpdater>(() => ({}));
@ -193,17 +193,8 @@ export class DashboardPlugin
public setup(
core: CoreSetup<StartDependencies, DashboardStart>,
{
share,
uiActions,
embeddable,
home,
kibanaLegacy,
urlForwarding,
data,
usageCollection,
}: SetupDependencies
): Setup {
{ share, uiActions, embeddable, home, urlForwarding, data, usageCollection }: SetupDependencies
): DashboardSetup {
this.dashboardFeatureFlagConfig = this.initializerContext.config.get<
DashboardFeatureFlagConfig
>();

View file

@ -40,4 +40,6 @@ export {
import { SharePlugin } from './plugin';
export { KibanaURL } from './kibana_url';
export const plugin = () => new SharePlugin();

View file

@ -0,0 +1,44 @@
/*
* 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.
*/
// TODO: Replace this logic with KibanaURL once it is available.
// https://github.com/elastic/kibana/issues/64497
export class KibanaURL {
public readonly path: string;
public readonly appName: string;
public readonly appPath: string;
constructor(path: string) {
const match = path.match(/^.*\/app\/([^\/#]+)(.+)$/);
if (!match) {
throw new Error('Unexpected URL path.');
}
const [, appName, appPath] = match;
if (!appName || !appPath) {
throw new Error('Could not parse URL path.');
}
this.path = path;
this.appName = appName;
this.appPath = appPath;
}
}

View file

@ -5,7 +5,8 @@ To run this example plugin, use the command `yarn start --run-examples`.
## Drilldown examples
This plugin holds few examples on how to add drilldown types to dashboard.
This plugin holds few examples on how to add drilldown types to dashboard. See
`./public/drilldowns/` folder.
To play with drilldowns, open any dashboard, click "Edit" to put it in *edit mode*.
Now when opening context menu of dashboard panels you should see "Create drilldown" option.
@ -34,3 +35,74 @@ One can see how middle-click or Ctrl + click behavior could be supported using
### `dashboard_to_discover_drilldown`
`dashboard_to_discover_drilldown` shows how a real-world drilldown could look like.
## Drilldown Manager examples
*Drilldown Manager* is a collectio of code and React components that allows you
to add drilldowns to any app. To see examples of how drilldows can be added to
your app, run Kibana with `--run-examples` flag:
```
yarn start --run-examples
```
Then go to "Developer examples" and "UI Actions Enhanced", where you can see examples
where *Drilldown Manager* is used outside of the Dashboard app:
![image](https://user-images.githubusercontent.com/9773803/94044547-969a3400-fdce-11ea-826a-cbd0773a4000.png)
These examples show how you can create your custom UI Actions triggers and add
drilldowns to them, or use an embeddable in your app and add drilldows to it.
### Trigger examples
The `/public/triggers` folder shows how you can create custom triggers for your app.
Triggers are things that trigger some action in UI, like "user click".
Once you have defined your triggers, you need to register them in your plugin:
```ts
export class MyPlugin implements Plugin {
public setup(core, { uiActionsEnhanced: uiActions }: SetupDependencies) {
uiActions.registerTrigger(myTrigger);
}
}
```
### `app1_hello_world_drilldown`
`app1_hello_world_drilldown` is a basic example that shows how you can add the most
basic drilldown to your custom trigger.
### `appx_to_dashboard_drilldown`
`app1_to_dashboard_drilldown` and `app2_to_dashboard_drilldown` show how the Dashboard
drilldown can be used in other apps, outside of Dashboard.
Basically you define it:
```ts
type Trigger = typeof MY_TRIGGER_TRIGGER;
type Context = MyAppClickContext;
export class App1ToDashboardDrilldown extends AbstractDashboardDrilldown<Trigger> {
public readonly supportedTriggers = () => [MY_TRIGGER] as Trigger[];
protected async getURL(config: Config, context: Context): Promise<KibanaURL> {
return 'https://...';
}
}
```
and then you register it in your plugin:
```ts
export class MyPlugin implements Plugin {
public setup(core, { uiActionsEnhanced: uiActions }: SetupDependencies) {
const drilldown = new App2ToDashboardDrilldown(/* You can pass in dependencies here. */);
uiActions.registerDrilldown(drilldown);
}
}
```

View file

@ -5,10 +5,21 @@
"configPath": ["ui_actions_enhanced_examples"],
"server": false,
"ui": true,
"requiredPlugins": ["uiActions","uiActionsEnhanced", "data", "discover"],
"requiredPlugins": [
"uiActions",
"uiActionsEnhanced",
"data",
"discover",
"dashboard",
"dashboardEnhanced",
"developerExamples"
],
"optionalPlugins": [],
"requiredBundles": [
"dashboardEnhanced",
"embeddable",
"kibanaUtils",
"kibanaReact"
"kibanaReact",
"share"
]
}

View file

@ -0,0 +1,38 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import * as React from 'react';
import {
EuiPageBody,
EuiPageContent,
EuiPageContentBody,
EuiPageHeader,
EuiPageHeaderSection,
EuiTitle,
} from '@elastic/eui';
export interface PageProps {
title?: React.ReactNode;
}
export const Page: React.FC<PageProps> = ({ title = 'Untitled', children }) => {
return (
<EuiPageBody style={{ maxWidth: 1200, margin: '0 auto' }}>
<EuiPageHeader>
<EuiPageHeaderSection>
<EuiTitle size="l">
<h1>{title}</h1>
</EuiTitle>
</EuiPageHeaderSection>
</EuiPageHeader>
<EuiPageContent>
<EuiPageContentBody style={{ maxWidth: 800, margin: '0 auto' }}>
{children}
</EuiPageContentBody>
</EuiPageContent>
</EuiPageBody>
);
};

View file

@ -0,0 +1,7 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export * from './section';

View file

@ -0,0 +1,24 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { EuiTitle, EuiSpacer } from '@elastic/eui';
export interface Props {
title: React.ReactNode;
}
export const Section: React.FC<Props> = ({ title, children }) => {
return (
<section>
<EuiTitle size="m">
<h2>{title}</h2>
</EuiTitle>
<EuiSpacer />
{children}
</section>
);
};

View file

@ -0,0 +1,20 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { EuiPage } from '@elastic/eui';
import { Page } from '../../components/page';
import { DrilldownsManager } from '../drilldowns_manager';
export const App: React.FC = () => {
return (
<EuiPage>
<Page title={'UI Actions Enhanced'}>
<DrilldownsManager />
</Page>
</EuiPage>
);
};

View file

@ -0,0 +1,7 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export * from './app';

View file

@ -0,0 +1,39 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiHorizontalRule } from '@elastic/eui';
import React from 'react';
import { Section } from '../../components/section/section';
import { SampleMlJob, SampleApp1ClickContext } from '../../triggers';
import { DrilldownsWithoutEmbeddableExample } from '../drilldowns_without_embeddable_example';
import { DrilldownsWithoutEmbeddableSingleButtonExample } from '../drilldowns_without_embeddable_single_button_example/drilldowns_without_embeddable_single_button_example';
import { DrilldownsWithEmbeddableExample } from '../drilldowns_with_embeddable_example';
export const job: SampleMlJob = {
job_id: '123',
job_type: 'anomaly_detector',
description: 'This is some ML job.',
};
export const context: SampleApp1ClickContext = { job };
export const DrilldownsManager: React.FC = () => {
return (
<div>
<Section title={'Drilldowns Manager'}>
<DrilldownsWithoutEmbeddableExample />
<EuiHorizontalRule margin="xxl" />
<DrilldownsWithoutEmbeddableSingleButtonExample />
<EuiHorizontalRule margin="xxl" />
<DrilldownsWithEmbeddableExample />
</Section>
</div>
);
};

View file

@ -0,0 +1,7 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export * from './drilldowns_manager';

View file

@ -0,0 +1,134 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import {
EuiText,
EuiSpacer,
EuiContextMenuPanelDescriptor,
EuiButton,
EuiPopover,
EuiContextMenu,
EuiFlyout,
EuiCode,
EuiFlexItem,
EuiFlexGroup,
} from '@elastic/eui';
import { SampleMlJob, SampleApp1ClickContext } from '../../triggers';
import { EmbeddableRoot } from '../../../../../../src/plugins/embeddable/public';
import { ButtonEmbeddable } from '../../embeddables/button_embeddable';
import { useUiActions } from '../../context';
import { VALUE_CLICK_TRIGGER } from '../../../../../../src/plugins/ui_actions/public';
export const job: SampleMlJob = {
job_id: '123',
job_type: 'anomaly_detector',
description: 'This is some ML job.',
};
export const context: SampleApp1ClickContext = { job };
export const DrilldownsWithEmbeddableExample: React.FC = () => {
const { plugins, managerWithEmbeddable } = useUiActions();
const embeddable = React.useMemo(
() =>
new ButtonEmbeddable(
{ id: 'DrilldownsWithEmbeddableExample' },
{ uiActions: plugins.uiActionsEnhanced }
),
[plugins.uiActionsEnhanced]
);
const [showManager, setShowManager] = React.useState(false);
const [openPopup, setOpenPopup] = React.useState(false);
const viewRef = React.useRef<'create' | 'manage'>('create');
const panels: EuiContextMenuPanelDescriptor[] = [
{
id: 0,
items: [
{
name: 'Create new view',
icon: 'plusInCircle',
onClick: () => {
setOpenPopup(false);
viewRef.current = 'create';
setShowManager((x) => !x);
},
},
{
name: 'Drilldown list view',
icon: 'list',
onClick: () => {
setOpenPopup(false);
viewRef.current = 'manage';
setShowManager((x) => !x);
},
},
],
},
];
const openManagerButton = showManager ? (
<EuiButton onClick={() => setShowManager(false)}>Close</EuiButton>
) : (
<EuiPopover
id="contextMenuExample"
button={
<EuiButton
fill={!showManager}
iconType="arrowDown"
iconSide="right"
onClick={() => setOpenPopup((x) => !x)}
>
Open Drilldown Manager
</EuiButton>
}
isOpen={openPopup}
closePopover={() => setOpenPopup(false)}
panelPaddingSize="none"
withTitle
anchorPosition="downLeft"
>
<EuiContextMenu initialPanelId={0} panels={panels} />
</EuiPopover>
);
return (
<>
<EuiText>
<h3>With embeddable example</h3>
<p>
This example shows how drilldown manager can be added to an embeddable which executes{' '}
<EuiCode>VALUE_CLICK_TRIGGER</EuiCode> trigger. Below card is an embeddable which executes
<EuiCode>VALUE_CLICK_TRIGGER</EuiCode> when it is clicked on.
</p>
</EuiText>
<EuiSpacer />
<EuiFlexGroup>
<EuiFlexItem grow={false}>{openManagerButton}</EuiFlexItem>
<EuiFlexItem grow={false}>
<div style={{ maxWidth: 200 }}>
<EmbeddableRoot embeddable={embeddable} />
</div>
</EuiFlexItem>
</EuiFlexGroup>
{showManager && (
<EuiFlyout onClose={() => setShowManager(false)} aria-labelledby="Drilldown Manager">
<plugins.uiActionsEnhanced.FlyoutManageDrilldowns
onClose={() => setShowManager(false)}
viewMode={viewRef.current}
dynamicActionManager={managerWithEmbeddable}
triggers={[VALUE_CLICK_TRIGGER]}
placeContext={{ embeddable }}
/>
</EuiFlyout>
)}
</>
);
};

View file

@ -0,0 +1,7 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export * from './drilldowns_with_embeddable_example';

View file

@ -0,0 +1,129 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import {
EuiText,
EuiFlexGroup,
EuiFlexItem,
EuiButton,
EuiSpacer,
EuiFlyout,
EuiPopover,
EuiContextMenu,
EuiContextMenuPanelDescriptor,
} from '@elastic/eui';
import { useUiActions } from '../../context';
import { SAMPLE_APP1_CLICK_TRIGGER, SampleMlJob, SampleApp1ClickContext } from '../../triggers';
export const job: SampleMlJob = {
job_id: '123',
job_type: 'anomaly_detector',
description: 'This is some ML job.',
};
export const context: SampleApp1ClickContext = { job };
export const DrilldownsWithoutEmbeddableExample: React.FC = () => {
const { plugins, managerWithoutEmbeddable } = useUiActions();
const [showManager, setShowManager] = React.useState(false);
const [openPopup, setOpenPopup] = React.useState(false);
const viewRef = React.useRef<'create' | 'manage'>('create');
const panels: EuiContextMenuPanelDescriptor[] = [
{
id: 0,
items: [
{
name: 'Create new view',
icon: 'plusInCircle',
onClick: () => {
setOpenPopup(false);
viewRef.current = 'create';
setShowManager((x) => !x);
},
},
{
name: 'Drilldown list view',
icon: 'list',
onClick: () => {
setOpenPopup(false);
viewRef.current = 'manage';
setShowManager((x) => !x);
},
},
],
},
];
const openManagerButton = showManager ? (
<EuiButton onClick={() => setShowManager(false)}>Close</EuiButton>
) : (
<EuiPopover
id="contextMenuExample"
button={
<EuiButton
fill={!showManager}
iconType="arrowDown"
iconSide="right"
onClick={() => setOpenPopup((x) => !x)}
>
Open Drilldown Manager
</EuiButton>
}
isOpen={openPopup}
closePopover={() => setOpenPopup(false)}
panelPaddingSize="none"
withTitle
anchorPosition="downLeft"
>
<EuiContextMenu initialPanelId={0} panels={panels} />
</EuiPopover>
);
return (
<>
<EuiText>
<h3>Without embeddable example (app 1)</h3>
<p>
<em>Drilldown Manager</em> can be integrated into any app in Kibana. This example shows
that drilldown manager can be used in an app which does not use embeddables and executes
its custom UI Actions triggers.
</p>
</EuiText>
<EuiSpacer />
<EuiFlexGroup>
<EuiFlexItem grow={false}>{openManagerButton}</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
color="secondary"
fill
iconType="play"
iconSide="left"
onClick={() =>
plugins.uiActionsEnhanced.executeTriggerActions(SAMPLE_APP1_CLICK_TRIGGER, context)
}
>
Execute click action
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
{showManager && (
<EuiFlyout onClose={() => setShowManager(false)} aria-labelledby="Drilldown Manager">
<plugins.uiActionsEnhanced.FlyoutManageDrilldowns
onClose={() => setShowManager(false)}
viewMode={viewRef.current}
dynamicActionManager={managerWithoutEmbeddable}
triggers={[SAMPLE_APP1_CLICK_TRIGGER]}
/>
</EuiFlyout>
)}
</>
);
};

View file

@ -0,0 +1,7 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export * from './drilldowns_without_embeddable_example';

View file

@ -0,0 +1,62 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { EuiText, EuiFlexGroup, EuiFlexItem, EuiButton, EuiSpacer, EuiFlyout } from '@elastic/eui';
import { useUiActions } from '../../context';
import { sampleApp2ClickContext, SAMPLE_APP2_CLICK_TRIGGER } from '../../triggers';
export const DrilldownsWithoutEmbeddableSingleButtonExample: React.FC = () => {
const { plugins, managerWithoutEmbeddableSingleButton } = useUiActions();
const [showManager, setShowManager] = React.useState(false);
const viewRef = React.useRef<'create' | 'manage'>('create');
return (
<>
<EuiText>
<h3>Without embeddable example, single button (app 2)</h3>
<p>
This example is the same as <em>Without embeddable example</em> but it shows that
drilldown manager actions and user created drilldowns can be combined in one menu, this is
useful, for example, for Canvas where clicking on a Canvas element would show the combined
menu of drilldown manager actions and drilldown actions.
</p>
</EuiText>
<EuiSpacer />
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiButton
color="secondary"
fill
iconType="play"
iconSide="left"
onClick={() =>
plugins.uiActionsEnhanced.executeTriggerActions(
SAMPLE_APP2_CLICK_TRIGGER,
sampleApp2ClickContext
)
}
>
Click this element
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
{showManager && (
<EuiFlyout onClose={() => setShowManager(false)} aria-labelledby="Drilldown Manager">
<plugins.uiActionsEnhanced.FlyoutManageDrilldowns
onClose={() => setShowManager(false)}
viewMode={viewRef.current}
dynamicActionManager={managerWithoutEmbeddableSingleButton}
triggers={[SAMPLE_APP2_CLICK_TRIGGER]}
/>
</EuiFlyout>
)}
</>
);
};

View file

@ -0,0 +1,7 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export * from './drilldowns_without_embeddable_single_button_example';

View file

@ -0,0 +1,22 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { createContext, useContext } from 'react';
import { CoreStart } from 'src/core/public';
import { UiActionsEnhancedDynamicActionManager } from '../../../../plugins/ui_actions_enhanced/public';
import { StartDependencies } from '../plugin';
export interface UiActionsExampleAppContextValue {
appBasePath: string;
core: CoreStart;
plugins: StartDependencies;
managerWithoutEmbeddable: UiActionsEnhancedDynamicActionManager;
managerWithoutEmbeddableSingleButton: UiActionsEnhancedDynamicActionManager;
managerWithEmbeddable: UiActionsEnhancedDynamicActionManager;
}
export const context = createContext<UiActionsExampleAppContextValue | null>(null);
export const useUiActions = () => useContext(context)!;

View file

@ -4,4 +4,4 @@
* you may not use this file except in compliance with the Elastic License.
*/
export { CollectConfigContainer } from './collect_config_container';
export * from './context';

View file

@ -0,0 +1,66 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { EuiFieldText, EuiFormRow } from '@elastic/eui';
import { reactToUiComponent } from '../../../../../../src/plugins/kibana_react/public';
import { UiActionsEnhancedDrilldownDefinition as Drilldown } from '../../../../../plugins/ui_actions_enhanced/public';
import { CollectConfigProps as CollectConfigPropsBase } from '../../../../../../src/plugins/kibana_utils/public';
import { SAMPLE_APP1_CLICK_TRIGGER, SampleApp1ClickContext } from '../../triggers';
import { SerializableState } from '../../../../../../src/plugins/kibana_utils/common';
export interface Config extends SerializableState {
name: string;
}
type Trigger = typeof SAMPLE_APP1_CLICK_TRIGGER;
type Context = SampleApp1ClickContext;
export type CollectConfigProps = CollectConfigPropsBase<Config, { triggers: Trigger[] }>;
export const APP1_HELLO_WORLD_DRILLDOWN = 'APP1_HELLO_WORLD_DRILLDOWN';
export class App1HelloWorldDrilldown implements Drilldown<Config, Trigger> {
public readonly id = APP1_HELLO_WORLD_DRILLDOWN;
public readonly order = 8;
public readonly getDisplayName = () => 'Hello world (app 1)';
public readonly euiIcon = 'cheer';
supportedTriggers(): Trigger[] {
return [SAMPLE_APP1_CLICK_TRIGGER];
}
private readonly ReactCollectConfig: React.FC<CollectConfigProps> = ({
config,
onConfig,
context,
}) => (
<EuiFormRow label="Enter your name" fullWidth>
<EuiFieldText
fullWidth
value={config.name}
onChange={(event) => onConfig({ ...config, name: event.target.value })}
/>
</EuiFormRow>
);
public readonly CollectConfig = reactToUiComponent(this.ReactCollectConfig);
public readonly createConfig = () => ({
name: '',
});
public readonly isConfigValid = (config: Config): config is Config => {
return !!config.name;
};
public readonly execute = async (config: Config, context: Context) => {
alert(`Hello, ${config.name}`);
};
}

View file

@ -0,0 +1,7 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export * from './app1_hello_world_drilldown';

View file

@ -0,0 +1,32 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import {
DashboardEnhancedAbstractDashboardDrilldown as AbstractDashboardDrilldown,
DashboardEnhancedAbstractDashboardDrilldownConfig as Config,
} from '../../../../../plugins/dashboard_enhanced/public';
import { SAMPLE_APP1_CLICK_TRIGGER, SampleApp1ClickContext } from '../../triggers';
import { KibanaURL } from '../../../../../../src/plugins/share/public';
export const APP1_TO_DASHBOARD_DRILLDOWN = 'APP1_TO_DASHBOARD_DRILLDOWN';
type Trigger = typeof SAMPLE_APP1_CLICK_TRIGGER;
type Context = SampleApp1ClickContext;
export class App1ToDashboardDrilldown extends AbstractDashboardDrilldown<Trigger> {
public readonly id = APP1_TO_DASHBOARD_DRILLDOWN;
public readonly supportedTriggers = () => [SAMPLE_APP1_CLICK_TRIGGER] as Trigger[];
protected async getURL(config: Config, context: Context): Promise<KibanaURL> {
const path = await this.urlGenerator.createUrl({
dashboardId: config.dashboardId,
});
const url = new KibanaURL(path);
return url;
}
}

View file

@ -0,0 +1,7 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export * from './app1_to_dashboard_drilldown';

View file

@ -0,0 +1,32 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import {
DashboardEnhancedAbstractDashboardDrilldown as AbstractDashboardDrilldown,
DashboardEnhancedAbstractDashboardDrilldownConfig as Config,
} from '../../../../../plugins/dashboard_enhanced/public';
import { SAMPLE_APP2_CLICK_TRIGGER, SampleApp2ClickContext } from '../../triggers';
import { KibanaURL } from '../../../../../../src/plugins/share/public';
export const APP2_TO_DASHBOARD_DRILLDOWN = 'APP2_TO_DASHBOARD_DRILLDOWN';
type Trigger = typeof SAMPLE_APP2_CLICK_TRIGGER;
type Context = SampleApp2ClickContext;
export class App2ToDashboardDrilldown extends AbstractDashboardDrilldown<Trigger> {
public readonly id = APP2_TO_DASHBOARD_DRILLDOWN;
public readonly supportedTriggers = () => [SAMPLE_APP2_CLICK_TRIGGER] as Trigger[];
protected async getURL(config: Config, context: Context): Promise<KibanaURL> {
const path = await this.urlGenerator.createUrl({
dashboardId: config.dashboardId,
});
const url = new KibanaURL(path);
return url;
}
}

View file

@ -0,0 +1,7 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export * from './app2_to_dashboard_drilldown';

View file

@ -6,14 +6,14 @@
import React from 'react';
import { EuiFormRow, EuiFieldText } from '@elastic/eui';
import { reactToUiComponent } from '../../../../../src/plugins/kibana_react/public';
import { UiActionsEnhancedDrilldownDefinition as Drilldown } from '../../../../plugins/ui_actions_enhanced/public';
import { ChartActionContext } from '../../../../../src/plugins/embeddable/public';
import { CollectConfigProps } from '../../../../../src/plugins/kibana_utils/public';
import { reactToUiComponent } from '../../../../../../src/plugins/kibana_react/public';
import { UiActionsEnhancedDrilldownDefinition as Drilldown } from '../../../../../plugins/ui_actions_enhanced/public';
import { ChartActionContext } from '../../../../../../src/plugins/embeddable/public';
import { CollectConfigProps } from '../../../../../../src/plugins/kibana_utils/public';
import {
SELECT_RANGE_TRIGGER,
VALUE_CLICK_TRIGGER,
} from '../../../../../src/plugins/ui_actions/public';
} from '../../../../../../src/plugins/ui_actions/public';
export type ActionContext = ChartActionContext;

View file

@ -6,12 +6,12 @@
import React from 'react';
import { EuiFormRow, EuiFieldText } from '@elastic/eui';
import { reactToUiComponent } from '../../../../../src/plugins/kibana_react/public';
import { UiActionsEnhancedDrilldownDefinition as Drilldown } from '../../../../plugins/ui_actions_enhanced/public';
import { RangeSelectContext } from '../../../../../src/plugins/embeddable/public';
import { CollectConfigProps } from '../../../../../src/plugins/kibana_utils/public';
import { SELECT_RANGE_TRIGGER } from '../../../../../src/plugins/ui_actions/public';
import { BaseActionFactoryContext } from '../../../../plugins/ui_actions_enhanced/public/dynamic_actions';
import { reactToUiComponent } from '../../../../../../src/plugins/kibana_react/public';
import { UiActionsEnhancedDrilldownDefinition as Drilldown } from '../../../../../plugins/ui_actions_enhanced/public';
import { RangeSelectContext } from '../../../../../../src/plugins/embeddable/public';
import { CollectConfigProps } from '../../../../../../src/plugins/kibana_utils/public';
import { SELECT_RANGE_TRIGGER } from '../../../../../../src/plugins/ui_actions/public';
import { BaseActionFactoryContext } from '../../../../../plugins/ui_actions_enhanced/public/dynamic_actions';
export type Config = {
name: string;

View file

@ -5,15 +5,15 @@
*/
import React from 'react';
import { StartDependencies as Start } from '../plugin';
import { reactToUiComponent } from '../../../../../src/plugins/kibana_react/public';
import { StartServicesGetter } from '../../../../../src/plugins/kibana_utils/public';
import { StartDependencies as Start } from '../../plugin';
import { reactToUiComponent } from '../../../../../../src/plugins/kibana_react/public';
import { StartServicesGetter } from '../../../../../../src/plugins/kibana_utils/public';
import { ActionContext, Config, CollectConfigProps } from './types';
import { CollectConfigContainer } from './collect_config_container';
import { SAMPLE_DASHBOARD_TO_DISCOVER_DRILLDOWN } from './constants';
import { UiActionsEnhancedDrilldownDefinition as Drilldown } from '../../../../plugins/ui_actions_enhanced/public';
import { UiActionsEnhancedDrilldownDefinition as Drilldown } from '../../../../../plugins/ui_actions_enhanced/public';
import { txtGoToDiscover } from './i18n';
import { APPLY_FILTER_TRIGGER } from '../../../../../src/plugins/ui_actions/public';
import { APPLY_FILTER_TRIGGER } from '../../../../../../src/plugins/ui_actions/public';
const isOutputWithIndexPatterns = (
output: unknown

View file

@ -4,8 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { CollectConfigProps as CollectConfigPropsBase } from '../../../../../src/plugins/kibana_utils/public';
import { ApplyGlobalFilterActionContext } from '../../../../../src/plugins/data/public';
import { CollectConfigProps as CollectConfigPropsBase } from '../../../../../../src/plugins/kibana_utils/public';
import { ApplyGlobalFilterActionContext } from '../../../../../../src/plugins/data/public';
export type ActionContext = ApplyGlobalFilterActionContext;

View file

@ -0,0 +1,53 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { createElement } from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
import { AdvancedUiActionsStart } from '../../../../../plugins/ui_actions_enhanced/public';
import { Embeddable, EmbeddableInput } from '../../../../../../src/plugins/embeddable/public';
import { ButtonEmbeddableComponent } from './button_embeddable_component';
import { VALUE_CLICK_TRIGGER } from '../../../../../../src/plugins/ui_actions/public';
export const BUTTON_EMBEDDABLE = 'BUTTON_EMBEDDABLE';
export interface ButtonEmbeddableParams {
uiActions: AdvancedUiActionsStart;
}
export class ButtonEmbeddable extends Embeddable {
type = BUTTON_EMBEDDABLE;
constructor(input: EmbeddableInput, private readonly params: ButtonEmbeddableParams) {
super(input, {});
}
reload() {}
private el?: HTMLElement;
public render(el: HTMLElement): void {
super.render(el);
this.el = el;
render(
createElement(ButtonEmbeddableComponent, {
onClick: () => {
this.params.uiActions.getTrigger(VALUE_CLICK_TRIGGER).exec({
embeddable: this,
data: {
data: [],
},
});
},
}),
el
);
}
public destroy() {
super.destroy();
if (this.el) unmountComponentAtNode(this.el);
}
}

View file

@ -0,0 +1,27 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import * as React from 'react';
import { EuiCard, EuiFlexItem, EuiIcon } from '@elastic/eui';
export interface ButtonEmbeddableComponentProps {
onClick: () => void;
}
export const ButtonEmbeddableComponent: React.FC<ButtonEmbeddableComponentProps> = ({
onClick,
}) => {
return (
<EuiFlexItem>
<EuiCard
icon={<EuiIcon size="xxl" type={`logoKibana`} />}
title={`Click me!`}
description={'This embeddable fires "VALUE_CLICK" trigger on click'}
onClick={onClick}
/>
</EuiFlexItem>
);
};

View file

@ -0,0 +1,7 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export * from './button_embeddable';

View file

@ -0,0 +1,38 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import * as React from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
import { CoreSetup, AppMountParameters } from 'kibana/public';
import { StartDependencies, UiActionsEnhancedExamplesStart } from './plugin';
import { UiActionsExampleAppContextValue, context } from './context';
export const mount = (
coreSetup: CoreSetup<StartDependencies, UiActionsEnhancedExamplesStart>
) => async ({ appBasePath, element }: AppMountParameters) => {
const [
core,
plugins,
{ managerWithoutEmbeddable, managerWithoutEmbeddableSingleButton, managerWithEmbeddable },
] = await coreSetup.getStartServices();
const { App } = await import('./containers/app');
const deps: UiActionsExampleAppContextValue = {
appBasePath,
core,
plugins,
managerWithoutEmbeddable,
managerWithoutEmbeddableSingleButton,
managerWithEmbeddable,
};
const reactElement = (
<context.Provider value={deps}>
<App />
</context.Provider>
);
render(reactElement, element);
return () => unmountComponentAtNode(element);
};

View file

@ -4,44 +4,174 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { Plugin, CoreSetup, CoreStart } from '../../../../src/core/public';
import { createElement as h } from 'react';
import { toMountPoint } from '../../../../src/plugins/kibana_react/public';
import { Plugin, CoreSetup, CoreStart, AppNavLinkStatus } from '../../../../src/core/public';
import { DataPublicPluginSetup, DataPublicPluginStart } from '../../../../src/plugins/data/public';
import {
AdvancedUiActionsSetup,
AdvancedUiActionsStart,
} from '../../../../x-pack/plugins/ui_actions_enhanced/public';
import { DashboardHelloWorldDrilldown } from './dashboard_hello_world_drilldown';
import { DashboardToDiscoverDrilldown } from './dashboard_to_discover_drilldown';
import { DashboardHelloWorldDrilldown } from './drilldowns/dashboard_hello_world_drilldown';
import { DashboardToDiscoverDrilldown } from './drilldowns/dashboard_to_discover_drilldown';
import { App1ToDashboardDrilldown } from './drilldowns/app1_to_dashboard_drilldown';
import { App1HelloWorldDrilldown } from './drilldowns/app1_hello_world_drilldown';
import { createStartServicesGetter } from '../../../../src/plugins/kibana_utils/public';
import { DiscoverSetup, DiscoverStart } from '../../../../src/plugins/discover/public';
import { DashboardHelloWorldOnlyRangeSelectDrilldown } from './dashboard_hello_world_only_range_select_drilldown';
import { DashboardSetup, DashboardStart } from '../../../../src/plugins/dashboard/public';
import { DashboardHelloWorldOnlyRangeSelectDrilldown } from './drilldowns/dashboard_hello_world_only_range_select_drilldown';
import { DeveloperExamplesSetup } from '../../../../examples/developer_examples/public';
import {
sampleApp1ClickTrigger,
sampleApp2ClickTrigger,
SAMPLE_APP2_CLICK_TRIGGER,
SampleApp2ClickContext,
sampleApp2ClickContext,
} from './triggers';
import { mount } from './mount';
import {
UiActionsEnhancedMemoryActionStorage,
UiActionsEnhancedDynamicActionManager,
} from '../../../plugins/ui_actions_enhanced/public';
import { App2ToDashboardDrilldown } from './drilldowns/app2_to_dashboard_drilldown';
export interface SetupDependencies {
dashboard: DashboardSetup;
data: DataPublicPluginSetup;
developerExamples: DeveloperExamplesSetup;
discover: DiscoverSetup;
uiActionsEnhanced: AdvancedUiActionsSetup;
}
export interface StartDependencies {
dashboard: DashboardStart;
data: DataPublicPluginStart;
discover: DiscoverStart;
uiActionsEnhanced: AdvancedUiActionsStart;
}
export interface UiActionsEnhancedExamplesStart {
managerWithoutEmbeddable: UiActionsEnhancedDynamicActionManager;
managerWithoutEmbeddableSingleButton: UiActionsEnhancedDynamicActionManager;
managerWithEmbeddable: UiActionsEnhancedDynamicActionManager;
}
export class UiActionsEnhancedExamplesPlugin
implements Plugin<void, void, SetupDependencies, StartDependencies> {
implements Plugin<void, UiActionsEnhancedExamplesStart, SetupDependencies, StartDependencies> {
public setup(
core: CoreSetup<StartDependencies>,
{ uiActionsEnhanced: uiActions }: SetupDependencies
core: CoreSetup<StartDependencies, UiActionsEnhancedExamplesStart>,
{ uiActionsEnhanced: uiActions, developerExamples }: SetupDependencies
) {
const start = createStartServicesGetter(core.getStartServices);
uiActions.registerDrilldown(new DashboardHelloWorldDrilldown());
uiActions.registerDrilldown(new DashboardHelloWorldOnlyRangeSelectDrilldown());
uiActions.registerDrilldown(new DashboardToDiscoverDrilldown({ start }));
uiActions.registerDrilldown(new App1HelloWorldDrilldown());
uiActions.registerDrilldown(new App1ToDashboardDrilldown({ start }));
uiActions.registerDrilldown(new App2ToDashboardDrilldown({ start }));
uiActions.registerTrigger(sampleApp1ClickTrigger);
uiActions.registerTrigger(sampleApp2ClickTrigger);
uiActions.addTriggerAction(SAMPLE_APP2_CLICK_TRIGGER, {
id: 'SINGLE_ELEMENT_EXAMPLE_OPEN_FLYOUT_AT_CREATE',
order: 2,
getDisplayName: () => 'Add drilldown',
getIconType: () => 'plusInCircle',
isCompatible: async ({ workpadId, elementId }: SampleApp2ClickContext) =>
workpadId === '123' && elementId === '456',
execute: async () => {
const { core: coreStart, plugins: pluginsStart, self } = start();
const handle = coreStart.overlays.openFlyout(
toMountPoint(
h(pluginsStart.uiActionsEnhanced.FlyoutManageDrilldowns, {
onClose: () => handle.close(),
viewMode: 'create',
dynamicActionManager: self.managerWithoutEmbeddableSingleButton,
triggers: [SAMPLE_APP2_CLICK_TRIGGER],
placeContext: {},
})
),
{
ownFocus: true,
}
);
},
});
uiActions.addTriggerAction(SAMPLE_APP2_CLICK_TRIGGER, {
id: 'SINGLE_ELEMENT_EXAMPLE_OPEN_FLYOUT_AT_MANAGE',
order: 1,
getDisplayName: () => 'Manage drilldowns',
getIconType: () => 'list',
isCompatible: async ({ workpadId, elementId }: SampleApp2ClickContext) =>
workpadId === '123' && elementId === '456',
execute: async () => {
const { core: coreStart, plugins: pluginsStart, self } = start();
const handle = coreStart.overlays.openFlyout(
toMountPoint(
h(pluginsStart.uiActionsEnhanced.FlyoutManageDrilldowns, {
onClose: () => handle.close(),
viewMode: 'manage',
dynamicActionManager: self.managerWithoutEmbeddableSingleButton,
triggers: [SAMPLE_APP2_CLICK_TRIGGER],
placeContext: { sampleApp2ClickContext },
})
),
{
ownFocus: true,
}
);
},
});
core.application.register({
id: 'ui_actions_enhanced-explorer',
title: 'UI Actions Enhanced Explorer',
navLinkStatus: AppNavLinkStatus.hidden,
mount: mount(core),
});
developerExamples.register({
appId: 'ui_actions_enhanced-explorer',
title: 'UI Actions Enhanced',
description: 'Examples of how to use drilldowns.',
links: [
{
label: 'README',
href:
'https://github.com/elastic/kibana/tree/master/x-pack/examples/ui_actions_enhanced_examples#ui-actions-enhanced-examples',
iconType: 'logoGithub',
size: 's',
target: '_blank',
},
],
});
}
public start(core: CoreStart, plugins: StartDependencies) {}
public start(core: CoreStart, plugins: StartDependencies): UiActionsEnhancedExamplesStart {
const managerWithoutEmbeddable = new UiActionsEnhancedDynamicActionManager({
storage: new UiActionsEnhancedMemoryActionStorage(),
isCompatible: async () => true,
uiActions: plugins.uiActionsEnhanced,
});
const managerWithoutEmbeddableSingleButton = new UiActionsEnhancedDynamicActionManager({
storage: new UiActionsEnhancedMemoryActionStorage(),
isCompatible: async () => true,
uiActions: plugins.uiActionsEnhanced,
});
const managerWithEmbeddable = new UiActionsEnhancedDynamicActionManager({
storage: new UiActionsEnhancedMemoryActionStorage(),
isCompatible: async () => true,
uiActions: plugins.uiActionsEnhanced,
});
return {
managerWithoutEmbeddable,
managerWithoutEmbeddableSingleButton,
managerWithEmbeddable,
};
}
public stop() {}
}

View file

@ -0,0 +1,8 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export * from './sample_app1_trigger';
export * from './sample_app2_trigger';

View file

@ -0,0 +1,31 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { Trigger } from '../../../../../src/plugins/ui_actions/public';
export const SAMPLE_APP1_CLICK_TRIGGER = 'SAMPLE_APP1_CLICK_TRIGGER';
export const sampleApp1ClickTrigger: Trigger<'SAMPLE_APP1_CLICK_TRIGGER'> = {
id: SAMPLE_APP1_CLICK_TRIGGER,
title: 'App 1 trigger fired on click',
description: 'Could be a click on a ML job in ML app.',
};
declare module '../../../../../src/plugins/ui_actions/public' {
export interface TriggerContextMapping {
[SAMPLE_APP1_CLICK_TRIGGER]: SampleApp1ClickContext;
}
}
export interface SampleApp1ClickContext {
job: SampleMlJob;
}
export interface SampleMlJob {
job_id: string;
job_type: 'anomaly_detector';
description: string;
}

View file

@ -0,0 +1,31 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { Trigger } from '../../../../../src/plugins/ui_actions/public';
export const SAMPLE_APP2_CLICK_TRIGGER = 'SAMPLE_APP2_CLICK_TRIGGER';
export const sampleApp2ClickTrigger: Trigger<'SAMPLE_APP2_CLICK_TRIGGER'> = {
id: SAMPLE_APP2_CLICK_TRIGGER,
title: 'App 2 trigger fired on click',
description: 'Could be a click on an element in Canvas app.',
};
declare module '../../../../../src/plugins/ui_actions/public' {
export interface TriggerContextMapping {
[SAMPLE_APP2_CLICK_TRIGGER]: SampleApp2ClickContext;
}
}
export interface SampleApp2ClickContext {
workpadId: string;
elementId: string;
}
export const sampleApp2ClickContext: SampleApp2ClickContext = {
workpadId: '123',
elementId: '456',
};

View file

@ -6,6 +6,7 @@
"requiredPlugins": ["data", "uiActionsEnhanced", "embeddable", "dashboard", "share"],
"configPath": ["xpack", "dashboardEnhanced"],
"requiredBundles": [
"embeddable",
"kibanaUtils",
"embeddableEnhanced",
"kibanaReact",

View file

@ -14,6 +14,12 @@ export {
StartDependencies as DashboardEnhancedStartDependencies,
} from './plugin';
export {
AbstractDashboardDrilldown as DashboardEnhancedAbstractDashboardDrilldown,
AbstractDashboardDrilldownConfig as DashboardEnhancedAbstractDashboardDrilldownConfig,
AbstractDashboardDrilldownParams as DashboardEnhancedAbstractDashboardDrilldownParams,
} from './services/drilldowns/abstract_dashboard_drilldown';
export function plugin(context: PluginInitializerContext) {
return new DashboardEnhancedPlugin(context);
}

View file

@ -0,0 +1,89 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { DataPublicPluginStart } from 'src/plugins/data/public';
import { DashboardStart } from 'src/plugins/dashboard/public';
import { reactToUiComponent } from '../../../../../../../src/plugins/kibana_react/public';
import {
TriggerId,
TriggerContextMapping,
} from '../../../../../../../src/plugins/ui_actions/public';
import { CollectConfigContainer } from './components';
import {
UiActionsEnhancedDrilldownDefinition as Drilldown,
UiActionsEnhancedBaseActionFactoryContext as BaseActionFactoryContext,
AdvancedUiActionsStart,
} from '../../../../../ui_actions_enhanced/public';
import { txtGoToDashboard } from './i18n';
import {
StartServicesGetter,
CollectConfigProps,
} from '../../../../../../../src/plugins/kibana_utils/public';
import { KibanaURL } from '../../../../../../../src/plugins/share/public';
import { Config } from './types';
export interface Params {
start: StartServicesGetter<{
uiActionsEnhanced: AdvancedUiActionsStart;
data: DataPublicPluginStart;
dashboard: DashboardStart;
}>;
}
export abstract class AbstractDashboardDrilldown<T extends TriggerId>
implements Drilldown<Config, T, BaseActionFactoryContext<T>> {
constructor(protected readonly params: Params) {}
public abstract readonly id: string;
public abstract readonly supportedTriggers: () => T[];
protected abstract getURL(config: Config, context: TriggerContextMapping[T]): Promise<KibanaURL>;
public readonly order = 100;
public readonly getDisplayName = () => txtGoToDashboard;
public readonly euiIcon = 'dashboardApp';
private readonly ReactCollectConfig: React.FC<
CollectConfigProps<Config, BaseActionFactoryContext<T>>
> = (props) => <CollectConfigContainer {...props} params={this.params} />;
public readonly CollectConfig = reactToUiComponent(this.ReactCollectConfig);
public readonly createConfig = () => ({
dashboardId: '',
useCurrentFilters: true,
useCurrentDateRange: true,
});
public readonly isConfigValid = (config: Config): config is Config => {
if (!config.dashboardId) return false;
return true;
};
public readonly getHref = async (
config: Config,
context: TriggerContextMapping[T]
): Promise<string> => {
const url = await this.getURL(config, context);
return url.path;
};
public readonly execute = async (config: Config, context: TriggerContextMapping[T]) => {
const url = await this.getURL(config, context);
await this.params.start().core.application.navigateToApp(url.appName, { path: url.appPath });
};
protected get urlGenerator() {
const urlGenerator = this.params.start().plugins.dashboard.dashboardUrlGenerator;
if (!urlGenerator)
throw new Error('Dashboard URL generator is required for dashboard drilldown.');
return urlGenerator;
}
}

View file

@ -11,8 +11,8 @@ import { SimpleSavedObject } from '../../../../../../../../src/core/public';
import { DashboardDrilldownConfig } from './dashboard_drilldown_config';
import { txtDestinationDashboardNotFound } from './i18n';
import { CollectConfigProps } from '../../../../../../../../src/plugins/kibana_utils/public';
import { Config, FactoryContext } from '../types';
import { Params } from '../drilldown';
import { Config } from '../types';
import { Params } from '../abstract_dashboard_drilldown';
const mergeDashboards = (
dashboards: Array<EuiComboBoxOptionOption<string>>,
@ -34,7 +34,7 @@ const dashboardSavedObjectToMenuItem = (
label: savedObject.attributes.title,
});
interface DashboardDrilldownCollectConfigProps extends CollectConfigProps<Config, FactoryContext> {
export interface DashboardDrilldownCollectConfigProps extends CollectConfigProps<Config, object> {
params: Params;
}

View file

@ -4,9 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
export { DASHBOARD_TO_DASHBOARD_DRILLDOWN } from './constants';
export {
DashboardToDashboardDrilldown,
Params as DashboardToDashboardDrilldownParams,
} from './drilldown';
export { Config } from './types';
CollectConfigContainer,
DashboardDrilldownCollectConfigProps,
} from './collect_config_container';

View file

@ -0,0 +1,11 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export {
AbstractDashboardDrilldown,
Params as AbstractDashboardDrilldownParams,
} from './abstract_dashboard_drilldown';
export { Config as AbstractDashboardDrilldownConfig } from './types';

View file

@ -83,7 +83,7 @@ export class FlyoutCreateDrilldownAction implements ActionByType<typeof OPEN_FLY
onClose={() => handle.close()}
viewMode={'create'}
dynamicActionManager={embeddable.enhancements.dynamicActions}
supportedTriggers={ensureNestedTriggers(embeddable.supportedTriggers())}
triggers={ensureNestedTriggers(embeddable.supportedTriggers())}
placeContext={{ embeddable }}
/>
),

View file

@ -67,7 +67,7 @@ export class FlyoutEditDrilldownAction implements ActionByType<typeof OPEN_FLYOU
onClose={() => handle.close()}
viewMode={'manage'}
dynamicActionManager={embeddable.enhancements.dynamicActions}
supportedTriggers={ensureNestedTriggers(embeddable.supportedTriggers())}
triggers={ensureNestedTriggers(embeddable.supportedTriggers())}
placeContext={{ embeddable }}
/>
),

View file

@ -14,7 +14,7 @@ import {
OPEN_FLYOUT_ADD_DRILLDOWN,
OPEN_FLYOUT_EDIT_DRILLDOWN,
} from './actions';
import { DashboardToDashboardDrilldown } from './dashboard_to_dashboard_drilldown';
import { EmbeddableToDashboardDrilldown } from './embeddable_to_dashboard_drilldown';
import { createStartServicesGetter } from '../../../../../../src/plugins/kibana_utils/public';
declare module '../../../../../../src/plugins/ui_actions/public' {
@ -44,12 +44,6 @@ export class DashboardDrilldownsService {
{ uiActionsEnhanced: uiActions }: SetupDependencies
) {
const start = createStartServicesGetter(core.getStartServices);
const getDashboardUrlGenerator = () => {
const urlGenerator = start().plugins.dashboard.dashboardUrlGenerator;
if (!urlGenerator)
throw new Error('dashboardUrlGenerator is required for dashboard to dashboard drilldown');
return urlGenerator;
};
const actionFlyoutCreateDrilldown = new FlyoutCreateDrilldownAction({ start });
uiActions.addTriggerAction(CONTEXT_MENU_TRIGGER, actionFlyoutCreateDrilldown);
@ -57,10 +51,7 @@ export class DashboardDrilldownsService {
const actionFlyoutEditDrilldown = new FlyoutEditDrilldownAction({ start });
uiActions.addTriggerAction(CONTEXT_MENU_TRIGGER, actionFlyoutEditDrilldown);
const dashboardToDashboardDrilldown = new DashboardToDashboardDrilldown({
start,
getDashboardUrlGenerator,
});
const dashboardToDashboardDrilldown = new EmbeddableToDashboardDrilldown({ start });
uiActions.registerDrilldown(dashboardToDashboardDrilldown);
}
}

View file

@ -1,125 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { reactToUiComponent } from '../../../../../../../src/plugins/kibana_react/public';
import { APPLY_FILTER_TRIGGER } from '../../../../../../../src/plugins/ui_actions/public';
import {
DashboardUrlGenerator,
DashboardUrlGeneratorState,
} from '../../../../../../../src/plugins/dashboard/public';
import { CollectConfigContainer } from './components';
import { DASHBOARD_TO_DASHBOARD_DRILLDOWN } from './constants';
import { UiActionsEnhancedDrilldownDefinition as Drilldown } from '../../../../../ui_actions_enhanced/public';
import { txtGoToDashboard } from './i18n';
import {
ApplyGlobalFilterActionContext,
esFilters,
isFilters,
isQuery,
isTimeRange,
} from '../../../../../../../src/plugins/data/public';
import { StartServicesGetter } from '../../../../../../../src/plugins/kibana_utils/public';
import { StartDependencies } from '../../../plugin';
import { Config, FactoryContext } from './types';
import { SearchInput } from '../../../../../../../src/plugins/discover/public';
export interface Params {
start: StartServicesGetter<Pick<StartDependencies, 'data' | 'uiActionsEnhanced'>>;
getDashboardUrlGenerator: () => DashboardUrlGenerator;
}
export class DashboardToDashboardDrilldown
implements Drilldown<Config, typeof APPLY_FILTER_TRIGGER, FactoryContext> {
constructor(protected readonly params: Params) {}
public readonly id = DASHBOARD_TO_DASHBOARD_DRILLDOWN;
public readonly order = 100;
public readonly getDisplayName = () => txtGoToDashboard;
public readonly euiIcon = 'dashboardApp';
private readonly ReactCollectConfig: React.FC<CollectConfigContainer['props']> = (props) => (
<CollectConfigContainer {...props} params={this.params} />
);
public readonly CollectConfig = reactToUiComponent(this.ReactCollectConfig);
public readonly createConfig = () => ({
dashboardId: '',
useCurrentFilters: true,
useCurrentDateRange: true,
});
public readonly isConfigValid = (config: Config): config is Config => {
if (!config.dashboardId) return false;
return true;
};
public supportedTriggers(): Array<typeof APPLY_FILTER_TRIGGER> {
return [APPLY_FILTER_TRIGGER];
}
public readonly getHref = async (
config: Config,
context: ApplyGlobalFilterActionContext
): Promise<string> => {
return this.getDestinationUrl(config, context);
};
public readonly execute = async (config: Config, context: ApplyGlobalFilterActionContext) => {
const dashboardPath = await this.getDestinationUrl(config, context);
const dashboardHash = dashboardPath.split('#')[1];
await this.params.start().core.application.navigateToApp('dashboards', {
path: `#${dashboardHash}`,
});
};
private getDestinationUrl = async (
config: Config,
context: ApplyGlobalFilterActionContext
): Promise<string> => {
const state: DashboardUrlGeneratorState = {
dashboardId: config.dashboardId,
};
if (context.embeddable) {
const input = context.embeddable.getInput() as Readonly<SearchInput>;
if (isQuery(input.query) && config.useCurrentFilters) state.query = input.query;
// if useCurrentDashboardDataRange is enabled, then preserve current time range
// if undefined is passed, then destination dashboard will figure out time range itself
// for brush event this time range would be overwritten
if (isTimeRange(input.timeRange) && config.useCurrentDateRange)
state.timeRange = input.timeRange;
// if useCurrentDashboardFilters enabled, then preserve all the filters (pinned and unpinned)
// otherwise preserve only pinned
if (isFilters(input.filters))
state.filters = config.useCurrentFilters
? input.filters
: input.filters?.filter((f) => esFilters.isFilterPinned(f));
}
const {
restOfFilters: filtersFromEvent,
timeRange: timeRangeFromEvent,
} = esFilters.extractTimeRange(context.filters, context.timeFieldName);
if (filtersFromEvent) {
state.filters = [...(state.filters ?? []), ...filtersFromEvent];
}
if (timeRangeFromEvent) {
state.timeRange = timeRangeFromEvent;
}
return this.params.getDashboardUrlGenerator().createUrl(state);
};
}

View file

@ -5,10 +5,10 @@
*/
/**
* note:
* don't change this string without carefull consideration,
* because it is stored in saved objects.
* NOTE: DO NOT CHANGE THIS STRING WITHOUT CAREFUL CONSIDERATOIN, BECAUSE IT IS
* STORED IN SAVED OBJECTS.
*
* Also temporary dashboard drilldown migration code inside embeddable plugin relies on it
* x-pack/plugins/embeddable_enhanced/public/embeddables/embeddable_action_storage.ts
*/
export const DASHBOARD_TO_DASHBOARD_DRILLDOWN = 'DASHBOARD_TO_DASHBOARD_DRILLDOWN';
export const EMBEDDABLE_TO_DASHBOARD_DRILLDOWN = 'DASHBOARD_TO_DASHBOARD_DRILLDOWN';

View file

@ -4,8 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { DashboardToDashboardDrilldown } from './drilldown';
import { Config } from './types';
import { EmbeddableToDashboardDrilldown } from './embeddable_to_dashboard_drilldown';
import { AbstractDashboardDrilldownConfig as Config } from '../abstract_dashboard_drilldown';
import { coreMock, savedObjectsServiceMock } from '../../../../../../../src/core/public/mocks';
import {
Filter,
@ -18,7 +18,6 @@ import {
ApplyGlobalFilterActionContext,
esFilters,
} from '../../../../../../../src/plugins/data/public';
// convenient to use real implementation here.
import { createDashboardUrlGenerator } from '../../../../../../../src/plugins/dashboard/public/url_generator';
import { UrlGeneratorsService } from '../../../../../../../src/plugins/share/public/url_generators';
import { StartDependencies } from '../../../plugin';
@ -26,7 +25,7 @@ import { SavedObjectLoader } from '../../../../../../../src/plugins/saved_object
import { StartServicesGetter } from '../../../../../../../src/plugins/kibana_utils/public/core';
describe('.isConfigValid()', () => {
const drilldown = new DashboardToDashboardDrilldown({} as any);
const drilldown = new EmbeddableToDashboardDrilldown({} as any);
test('returns false for invalid config with missing dashboard id', () => {
expect(
@ -50,19 +49,19 @@ describe('.isConfigValid()', () => {
});
test('config component exist', () => {
const drilldown = new DashboardToDashboardDrilldown({} as any);
const drilldown = new EmbeddableToDashboardDrilldown({} as any);
expect(drilldown.CollectConfig).toEqual(expect.any(Function));
});
test('initial config: switches are ON', () => {
const drilldown = new DashboardToDashboardDrilldown({} as any);
const drilldown = new EmbeddableToDashboardDrilldown({} as any);
const { useCurrentDateRange, useCurrentFilters } = drilldown.createConfig();
expect(useCurrentDateRange).toBe(true);
expect(useCurrentFilters).toBe(true);
});
test('getHref is defined', () => {
const drilldown = new DashboardToDashboardDrilldown({} as any);
const drilldown = new EmbeddableToDashboardDrilldown({} as any);
expect(drilldown.getHref).toBeDefined();
});
@ -84,7 +83,7 @@ describe('.execute() & getHref', () => {
const getUrlForApp = jest.fn((app, opt) => `${app}/${opt.path}`);
const savedObjectsClient = savedObjectsServiceMock.createStartContract().client;
const drilldown = new DashboardToDashboardDrilldown({
const drilldown = new EmbeddableToDashboardDrilldown({
start: ((() => ({
core: {
application: {
@ -97,19 +96,24 @@ describe('.execute() & getHref', () => {
},
plugins: {
uiActionsEnhanced: {},
dashboard: {
dashboardUrlGenerator: new UrlGeneratorsService()
.setup(coreMock.createSetup())
.registerUrlGenerator(
createDashboardUrlGenerator(() =>
Promise.resolve({
appBasePath: 'xyz/app/dashboards',
useHashedUrl: false,
savedDashboardLoader: ({} as unknown) as SavedObjectLoader,
})
)
),
},
},
self: {},
})) as unknown) as StartServicesGetter<Pick<StartDependencies, 'data' | 'uiActionsEnhanced'>>,
getDashboardUrlGenerator: () =>
new UrlGeneratorsService().setup(coreMock.createSetup()).registerUrlGenerator(
createDashboardUrlGenerator(() =>
Promise.resolve({
appBasePath: 'test',
useHashedUrl: false,
savedDashboardLoader: ({} as unknown) as SavedObjectLoader,
})
)
),
})) as unknown) as StartServicesGetter<
Pick<StartDependencies, 'data' | 'uiActionsEnhanced' | 'dashboard'>
>,
});
const completeConfig: Config = {

View file

@ -0,0 +1,83 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import {
TriggerContextMapping,
APPLY_FILTER_TRIGGER,
} from '../../../../../../../src/plugins/ui_actions/public';
import { DashboardUrlGeneratorState } from '../../../../../../../src/plugins/dashboard/public';
import {
esFilters,
isFilters,
isQuery,
isTimeRange,
} from '../../../../../../../src/plugins/data/public';
import {
AbstractDashboardDrilldown,
AbstractDashboardDrilldownParams,
AbstractDashboardDrilldownConfig as Config,
} from '../abstract_dashboard_drilldown';
import { KibanaURL } from '../../../../../../../src/plugins/share/public';
import { EMBEDDABLE_TO_DASHBOARD_DRILLDOWN } from './constants';
type Trigger = typeof APPLY_FILTER_TRIGGER;
type Context = TriggerContextMapping[Trigger];
export type Params = AbstractDashboardDrilldownParams;
/**
* This drilldown is the "Go to Dashboard" you can find in Dashboard app panles.
* This drilldown can be used on any embeddable and it is tied to embeddables
* in two ways: (1) it works with APPLY_FILTER_TRIGGER, which is usually executed
* by embeddables (but not necessarily); (2) its `getURL` method depends on
* `embeddable` field being present in `context`.
*/
export class EmbeddableToDashboardDrilldown extends AbstractDashboardDrilldown<Trigger> {
public readonly id = EMBEDDABLE_TO_DASHBOARD_DRILLDOWN;
public readonly supportedTriggers = () => [APPLY_FILTER_TRIGGER] as Trigger[];
protected async getURL(config: Config, context: Context): Promise<KibanaURL> {
const state: DashboardUrlGeneratorState = {
dashboardId: config.dashboardId,
};
if (context.embeddable) {
const input = context.embeddable.getInput();
if (isQuery(input.query) && config.useCurrentFilters) state.query = input.query;
// if useCurrentDashboardDataRange is enabled, then preserve current time range
// if undefined is passed, then destination dashboard will figure out time range itself
// for brush event this time range would be overwritten
if (isTimeRange(input.timeRange) && config.useCurrentDateRange)
state.timeRange = input.timeRange;
// if useCurrentDashboardFilters enabled, then preserve all the filters (pinned and unpinned)
// otherwise preserve only pinned
if (isFilters(input.filters))
state.filters = config.useCurrentFilters
? input.filters
: input.filters?.filter((f) => esFilters.isFilterPinned(f));
}
const {
restOfFilters: filtersFromEvent,
timeRange: timeRangeFromEvent,
} = esFilters.extractTimeRange(context.filters, context.timeFieldName);
if (filtersFromEvent) {
state.filters = [...(state.filters ?? []), ...filtersFromEvent];
}
if (timeRangeFromEvent) {
state.timeRange = timeRangeFromEvent;
}
const path = await this.urlGenerator.createUrl(state);
const url = new KibanaURL(path);
return url;
}
}

View file

@ -0,0 +1,11 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export { EMBEDDABLE_TO_DASHBOARD_DRILLDOWN } from './constants';
export {
EmbeddableToDashboardDrilldown,
Params as EmbeddableToDashboardDrilldownParams,
} from './embeddable_to_dashboard_drilldown';

View file

@ -7,5 +7,5 @@
"requiredPlugins": ["uiActions", "embeddable", "discover"],
"optionalPlugins": ["share", "kibanaLegacy", "usageCollection"],
"configPath": ["xpack", "discoverEnhanced"],
"requiredBundles": ["kibanaUtils", "data"]
"requiredBundles": ["kibanaUtils", "data", "share"]
}

View file

@ -10,7 +10,7 @@ import { ViewMode, IEmbeddable } from '../../../../../../src/plugins/embeddable/
import { StartServicesGetter } from '../../../../../../src/plugins/kibana_utils/public';
import { KibanaLegacyStart } from '../../../../../../src/plugins/kibana_legacy/public';
import { CoreStart } from '../../../../../../src/core/public';
import { KibanaURL } from './kibana_url';
import { KibanaURL } from '../../../../../../src/plugins/share/public';
import * as shared from './shared';
export const ACTION_EXPLORE_DATA = 'ACTION_EXPLORE_DATA';

View file

@ -13,7 +13,7 @@ import {
ApplyGlobalFilterActionContext,
esFilters,
} from '../../../../../../src/plugins/data/public';
import { KibanaURL } from './kibana_url';
import { KibanaURL } from '../../../../../../src/plugins/share/public';
import * as shared from './shared';
import { AbstractExploreDataAction } from './abstract_explore_data_action';

View file

@ -7,7 +7,7 @@
import { Action } from '../../../../../../src/plugins/ui_actions/public';
import { EmbeddableContext } from '../../../../../../src/plugins/embeddable/public';
import { DiscoverUrlGeneratorState } from '../../../../../../src/plugins/discover/public';
import { KibanaURL } from './kibana_url';
import { KibanaURL } from '../../../../../../src/plugins/share/public';
import * as shared from './shared';
import { AbstractExploreDataAction } from './abstract_explore_data_action';

View file

@ -1,31 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
// TODO: Replace this logic with KibanaURL once it is available.
// https://github.com/elastic/kibana/issues/64497
export class KibanaURL {
public readonly path: string;
public readonly appName: string;
public readonly appPath: string;
constructor(path: string) {
const match = path.match(/^.*\/app\/([^\/#]+)(.+)$/);
if (!match) {
throw new Error('Unexpected Discover URL path.');
}
const [, appName, appPath] = match;
if (!appName || !appPath) {
throw new Error('Could not parse Discover URL path.');
}
this.path = path;
this.appName = appName;
this.appPath = appPath;
}
}

View file

@ -12,7 +12,6 @@ import {
import { UiActionsEnhancedSerializedEvent } from '../../../ui_actions_enhanced/public';
import { of } from '../../../../../src/plugins/kibana_utils/public';
// use real const to make test fail in case someone accidentally changes it
import { DASHBOARD_TO_DASHBOARD_DRILLDOWN } from '../../../dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown';
import { APPLY_FILTER_TRIGGER } from '../../../../../src/plugins/ui_actions/public';
class TestEmbeddable extends Embeddable<EmbeddableWithDynamicActionsInput> {
@ -555,7 +554,7 @@ describe('EmbeddableActionStorage', () => {
eventId: '1',
triggers: [OTHER_TRIGGER],
action: {
factoryId: DASHBOARD_TO_DASHBOARD_DRILLDOWN,
factoryId: 'DASHBOARD_TO_DASHBOARD_DRILLDOWN',
name: '',
config: {},
},

View file

@ -80,7 +80,7 @@ export interface ActionWizardProps<
/**
* List of possible triggers in current context
*/
supportedTriggers: TriggerId[];
triggers: TriggerId[];
triggerPickerDocsLink?: string;
}
@ -94,7 +94,7 @@ export const ActionWizard: React.FC<ActionWizardProps> = ({
context,
onSelectedTriggersChange,
getTriggerInfo,
supportedTriggers,
triggers,
triggerPickerDocsLink,
}) => {
// auto pick action factory if there is only 1 available
@ -108,14 +108,14 @@ export const ActionWizard: React.FC<ActionWizardProps> = ({
// auto pick selected trigger if none is picked
if (currentActionFactory && !((context.triggers?.length ?? 0) > 0)) {
const triggers = getTriggersForActionFactory(currentActionFactory, supportedTriggers);
if (triggers.length > 0) {
onSelectedTriggersChange([triggers[0]]);
const actionTriggers = getTriggersForActionFactory(currentActionFactory, triggers);
if (actionTriggers.length > 0) {
onSelectedTriggersChange([actionTriggers[0]]);
}
}
if (currentActionFactory && config) {
const allTriggers = getTriggersForActionFactory(currentActionFactory, supportedTriggers);
const allTriggers = getTriggersForActionFactory(currentActionFactory, triggers);
return (
<SelectedActionFactory
actionFactory={currentActionFactory}

View file

@ -254,7 +254,7 @@ export function Demo({ actionFactories }: { actionFactories: Array<ActionFactory
});
}}
getTriggerInfo={mockGetTriggerInfo}
supportedTriggers={[VALUE_CLICK_TRIGGER, APPLY_FILTER_TRIGGER, SELECT_RANGE_TRIGGER]}
triggers={[VALUE_CLICK_TRIGGER, APPLY_FILTER_TRIGGER, SELECT_RANGE_TRIGGER]}
/>
<div style={{ marginTop: '44px' }} />
<hr />

View file

@ -34,7 +34,7 @@ storiesOf('components/FlyoutManageDrilldowns', module)
<EuiFlyout onClose={() => {}}>
<FlyoutManageDrilldowns
dynamicActionManager={mockDynamicActionManager}
supportedTriggers={['VALUE_CLICK_TRIGGER', 'SELECT_RANGE_TRIGGER', 'FILTER_TRIGGER']}
triggers={['VALUE_CLICK_TRIGGER', 'SELECT_RANGE_TRIGGER', 'FILTER_TRIGGER']}
/>
</EuiFlyout>
))
@ -42,7 +42,7 @@ storiesOf('components/FlyoutManageDrilldowns', module)
<EuiFlyout onClose={() => {}}>
<FlyoutManageDrilldowns
dynamicActionManager={mockDynamicActionManager}
supportedTriggers={['FILTER_TRIGGER']}
triggers={['FILTER_TRIGGER']}
/>
</EuiFlyout>
));

View file

@ -19,7 +19,7 @@ import { TEST_SUBJ_DRILLDOWN_ITEM } from '../list_manage_drilldowns';
import { WELCOME_MESSAGE_TEST_SUBJ } from '../drilldown_hello_bar';
import { coreMock } from '../../../../../../../src/core/public/mocks';
import { NotificationsStart } from 'kibana/public';
import { toastDrilldownsCRUDError } from './i18n';
import { toastDrilldownsCRUDError } from '../../hooks/i18n';
const storage = new Storage(new StubBrowserStorage());
const toasts = coreMock.createStart().notifications.toasts;
@ -41,7 +41,7 @@ test('Allows to manage drilldowns', async () => {
const screen = render(
<FlyoutManageDrilldowns
dynamicActionManager={mockDynamicActionManager}
supportedTriggers={mockSupportedTriggers}
triggers={mockSupportedTriggers}
/>
);
@ -115,7 +115,7 @@ test('Can delete multiple drilldowns', async () => {
const screen = render(
<FlyoutManageDrilldowns
dynamicActionManager={mockDynamicActionManager}
supportedTriggers={mockSupportedTriggers}
triggers={mockSupportedTriggers}
/>
);
// wait for initial render. It is async because resolving compatible action factories is async
@ -157,7 +157,7 @@ test('Create only mode', async () => {
dynamicActionManager={mockDynamicActionManager}
viewMode={'create'}
onClose={onClose}
supportedTriggers={mockSupportedTriggers}
triggers={mockSupportedTriggers}
/>
);
// wait for initial render. It is async because resolving compatible action factories is async
@ -181,7 +181,7 @@ test('After switching between action factories state is restored', async () => {
<FlyoutManageDrilldowns
dynamicActionManager={mockDynamicActionManager}
viewMode={'create'}
supportedTriggers={mockSupportedTriggers}
triggers={mockSupportedTriggers}
/>
);
// wait for initial render. It is async because resolving compatible action factories is async
@ -222,7 +222,7 @@ test("Error when can't save drilldown changes", async () => {
const screen = render(
<FlyoutManageDrilldowns
dynamicActionManager={mockDynamicActionManager}
supportedTriggers={mockSupportedTriggers}
triggers={mockSupportedTriggers}
/>
);
// wait for initial render. It is async because resolving compatible action factories is async
@ -245,7 +245,7 @@ test('Should show drilldown welcome message. Should be able to dismiss it', asyn
let screen = render(
<FlyoutManageDrilldowns
dynamicActionManager={mockDynamicActionManager}
supportedTriggers={mockSupportedTriggers}
triggers={mockSupportedTriggers}
/>
);
@ -260,7 +260,7 @@ test('Should show drilldown welcome message. Should be able to dismiss it', asyn
screen = render(
<FlyoutManageDrilldowns
dynamicActionManager={mockDynamicActionManager}
supportedTriggers={mockSupportedTriggers}
triggers={mockSupportedTriggers}
/>
);
// wait for initial render. It is async because resolving compatible action factories is async
@ -272,7 +272,7 @@ test('Drilldown type is not shown if no supported trigger', async () => {
const screen = render(
<FlyoutManageDrilldowns
dynamicActionManager={mockDynamicActionManager}
supportedTriggers={['VALUE_CLICK_TRIGGER']}
triggers={['VALUE_CLICK_TRIGGER']}
viewMode={'create'}
/>
);
@ -286,7 +286,7 @@ test('Can pick a trigger', async () => {
const screen = render(
<FlyoutManageDrilldowns
dynamicActionManager={mockDynamicActionManager}
supportedTriggers={mockSupportedTriggers}
triggers={mockSupportedTriggers}
viewMode={'create'}
/>
);

View file

@ -4,33 +4,25 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { useEffect, useState, useMemo } from 'react';
import React, { useState, useMemo } from 'react';
import { ToastsStart } from 'kibana/public';
import useMountedState from 'react-use/lib/useMountedState';
import { intersection } from 'lodash';
import { DrilldownWizardConfig, FlyoutDrilldownWizard } from '../flyout_drilldown_wizard';
import { FlyoutListManageDrilldowns } from '../flyout_list_manage_drilldowns';
import { IStorageWrapper } from '../../../../../../../src/plugins/kibana_utils/public';
import { Trigger, TriggerId } from '../../../../../../../src/plugins/ui_actions/public';
import { useContainerState } from '../../../../../../../src/plugins/kibana_utils/public';
import { DrilldownListItem } from '../list_manage_drilldowns';
import {
insufficientLicenseLevel,
invalidDrilldownType,
toastDrilldownCreated,
toastDrilldownDeleted,
toastDrilldownEdited,
toastDrilldownsCRUDError,
toastDrilldownsDeleted,
} from './i18n';
import { insufficientLicenseLevel, invalidDrilldownType } from './i18n';
import {
ActionFactory,
BaseActionConfig,
BaseActionFactoryContext,
DynamicActionManager,
SerializedAction,
SerializedEvent,
} from '../../../dynamic_actions';
import { useWelcomeMessage } from '../../hooks/use_welcome_message';
import { useCompatibleActionFactoriesForCurrentContext } from '../../hooks/use_compatible_action_factories_for_current_context';
import { useDrilldownsStateManager } from '../../hooks/use_drilldown_state_manager';
import { ActionFactoryPlaceContext } from '../types';
interface ConnectedFlyoutManageDrilldownsProps<
@ -43,7 +35,7 @@ interface ConnectedFlyoutManageDrilldownsProps<
/**
* List of possible triggers in current context
*/
supportedTriggers: TriggerId[];
triggers: TriggerId[];
/**
* Extra action factory context passed into action factories CollectConfig, getIconType, getDisplayName and etc...
@ -74,7 +66,7 @@ export function createFlyoutManageDrilldowns({
toastService: ToastsStart;
docsLink?: string;
triggerPickerDocsLink?: string;
}) {
}): React.FC<ConnectedFlyoutManageDrilldownsProps> {
const allActionFactoriesById = allActionFactories.reduce((acc, next) => {
acc[next.id] = next;
return acc;
@ -84,8 +76,8 @@ export function createFlyoutManageDrilldowns({
const isCreateOnly = props.viewMode === 'create';
const factoryContext: BaseActionFactoryContext = useMemo(
() => ({ ...props.placeContext, triggers: props.supportedTriggers }),
[props.placeContext, props.supportedTriggers]
() => ({ ...props.placeContext, triggers: props.triggers }),
[props.placeContext, props.triggers]
);
const actionFactories = useCompatibleActionFactoriesForCurrentContext(
allActionFactories,
@ -210,7 +202,7 @@ export function createFlyoutManageDrilldowns({
}}
actionFactoryPlaceContext={props.placeContext}
initialDrilldownWizardConfig={resolveInitialDrilldownWizardConfig()}
supportedTriggers={props.supportedTriggers}
supportedTriggers={props.triggers}
getTrigger={getTrigger}
/>
);
@ -220,7 +212,7 @@ export function createFlyoutManageDrilldowns({
// show trigger column in case if there is more then 1 possible trigger in current context
const showTriggerColumn =
intersection(
props.supportedTriggers,
props.triggers,
actionFactories
.map((factory) => factory.supportedTriggers())
.reduce((res, next) => res.concat(next), [])
@ -250,108 +242,3 @@ export function createFlyoutManageDrilldowns({
}
};
}
function useCompatibleActionFactoriesForCurrentContext<
Context extends BaseActionFactoryContext = BaseActionFactoryContext
>(actionFactories: ActionFactory[], context: Context) {
const [compatibleActionFactories, setCompatibleActionFactories] = useState<ActionFactory[]>();
useEffect(() => {
let canceled = false;
async function updateCompatibleFactoriesForContext() {
const compatibility = await Promise.all(
actionFactories.map((factory) => factory.isCompatible(context))
);
if (canceled) return;
const compatibleFactories = actionFactories.filter((_, i) => compatibility[i]);
const triggerSupportedFactories = compatibleFactories.filter((factory) =>
factory.supportedTriggers().some((trigger) => context.triggers.includes(trigger))
);
setCompatibleActionFactories(triggerSupportedFactories);
}
updateCompatibleFactoriesForContext();
return () => {
canceled = true;
};
}, [context, actionFactories, context.triggers]);
return compatibleActionFactories;
}
function useWelcomeMessage(storage: IStorageWrapper): [boolean, () => void] {
const key = `drilldowns:hidWelcomeMessage`;
const [hidWelcomeMessage, setHidWelcomeMessage] = useState<boolean>(storage.get(key) ?? false);
return [
!hidWelcomeMessage,
() => {
if (hidWelcomeMessage) return;
setHidWelcomeMessage(true);
storage.set(key, true);
},
];
}
function useDrilldownsStateManager(actionManager: DynamicActionManager, toastService: ToastsStart) {
const { events: drilldowns } = useContainerState(actionManager.state);
const [isLoading, setIsLoading] = useState(false);
const isMounted = useMountedState();
async function run(op: () => Promise<void>) {
setIsLoading(true);
try {
await op();
} catch (e) {
toastService.addError(e, {
title: toastDrilldownsCRUDError,
});
if (!isMounted) return;
setIsLoading(false);
return;
}
}
async function createDrilldown(action: SerializedAction, selectedTriggers: TriggerId[]) {
await run(async () => {
await actionManager.createEvent(action, selectedTriggers);
toastService.addSuccess({
title: toastDrilldownCreated.title(action.name),
text: toastDrilldownCreated.text,
});
});
}
async function editDrilldown(
drilldownId: string,
action: SerializedAction,
selectedTriggers: TriggerId[]
) {
await run(async () => {
await actionManager.updateEvent(drilldownId, action, selectedTriggers);
toastService.addSuccess({
title: toastDrilldownEdited.title(action.name),
text: toastDrilldownEdited.text,
});
});
}
async function deleteDrilldown(drilldownIds: string | string[]) {
await run(async () => {
drilldownIds = Array.isArray(drilldownIds) ? drilldownIds : [drilldownIds];
await actionManager.deleteEvents(drilldownIds);
toastService.addSuccess(
drilldownIds.length === 1
? {
title: toastDrilldownDeleted.title,
text: toastDrilldownDeleted.text,
}
: {
title: toastDrilldownsDeleted.title(drilldownIds.length),
text: toastDrilldownsDeleted.text,
}
);
});
}
return { drilldowns, isLoading, createDrilldown, editDrilldown, deleteDrilldown };
}

View file

@ -6,87 +6,6 @@
import { i18n } from '@kbn/i18n';
export const toastDrilldownCreated = {
title: (drilldownName: string) =>
i18n.translate(
'xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.toast.drilldownCreatedTitle',
{
defaultMessage: 'Drilldown "{drilldownName}" created',
values: {
drilldownName,
},
}
),
text: i18n.translate(
'xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.toast.drilldownCreatedText',
{
// TODO: remove `Save your dashboard before testing.` part
// when drilldowns are used not only in dashboard
// or after https://github.com/elastic/kibana/issues/65179 implemented
defaultMessage: 'Save your dashboard before testing.',
}
),
};
export const toastDrilldownEdited = {
title: (drilldownName: string) =>
i18n.translate(
'xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.toast.drilldownEditedTitle',
{
defaultMessage: 'Drilldown "{drilldownName}" updated',
values: {
drilldownName,
},
}
),
text: i18n.translate(
'xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.toast.drilldownEditedText',
{
defaultMessage: 'Save your dashboard before testing.',
}
),
};
export const toastDrilldownDeleted = {
title: i18n.translate(
'xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.toast.drilldownDeletedTitle',
{
defaultMessage: 'Drilldown deleted',
}
),
text: i18n.translate(
'xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.toast.drilldownDeletedText',
{
defaultMessage: 'Save your dashboard before testing.',
}
),
};
export const toastDrilldownsDeleted = {
title: (n: number) =>
i18n.translate(
'xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.toast.drilldownsDeletedTitle',
{
defaultMessage: '{n} drilldowns deleted',
values: { n },
}
),
text: i18n.translate(
'xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.toast.drilldownsDeletedText',
{
defaultMessage: 'Save your dashboard before testing.',
}
),
};
export const toastDrilldownsCRUDError = i18n.translate(
'xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.toast.drilldownsCRUDErrorTitle',
{
defaultMessage: 'Error saving drilldown',
description: 'Title for generic error toast when persisting drilldown updates failed',
}
);
export const insufficientLicenseLevel = i18n.translate(
'xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.insufficientLicenseLevelError',
{

View file

@ -230,7 +230,7 @@ export function FlyoutDrilldownWizard<
actionFactories={drilldownActionFactories}
actionFactoryContext={actionFactoryContext}
onSelectedTriggersChange={setSelectedTriggers}
supportedTriggers={supportedTriggers}
triggers={supportedTriggers}
getTriggerInfo={getTrigger}
triggerPickerDocsLink={triggerPickerDocsLink}
/>

View file

@ -10,11 +10,7 @@ import { FormDrilldownWizard } from './index';
import { Trigger, TriggerId } from '../../../../../../../src/plugins/ui_actions/public';
const otherProps = {
supportedTriggers: [
'VALUE_CLICK_TRIGGER',
'SELECT_RANGE_TRIGGER',
'FILTER_TRIGGER',
] as TriggerId[],
triggers: ['VALUE_CLICK_TRIGGER', 'SELECT_RANGE_TRIGGER', 'FILTER_TRIGGER'] as TriggerId[],
getTriggerInfo: (id: TriggerId) => ({ id } as Trigger),
onSelectedTriggersChange: () => {},
actionFactoryContext: { triggers: [] as TriggerId[] },

View file

@ -13,11 +13,7 @@ import { Trigger, TriggerId } from '../../../../../../../src/plugins/ui_actions/
const otherProps = {
actionFactoryContext: { triggers: [] as TriggerId[] },
supportedTriggers: [
'VALUE_CLICK_TRIGGER',
'SELECT_RANGE_TRIGGER',
'FILTER_TRIGGER',
] as TriggerId[],
triggers: ['VALUE_CLICK_TRIGGER', 'SELECT_RANGE_TRIGGER', 'FILTER_TRIGGER'] as TriggerId[],
getTriggerInfo: (id: TriggerId) => ({ id } as Trigger),
onSelectedTriggersChange: () => {},
};

View file

@ -6,7 +6,8 @@
import React from 'react';
import { EuiFieldText, EuiForm, EuiFormRow, EuiLink, EuiSpacer, EuiText } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiCallOut } from '@elastic/eui';
import { EuiCode } from '@elastic/eui';
import { txtDrilldownAction, txtNameOfDrilldown, txtUntitledDrilldown } from './i18n';
import {
ActionFactory,
@ -15,6 +16,7 @@ import {
} from '../../../dynamic_actions';
import { ActionWizard } from '../../../components/action_wizard';
import { Trigger, TriggerId } from '../../../../../../../src/plugins/ui_actions/public';
import { txtGetMoreActions } from './i18n';
const GET_MORE_ACTIONS_LINK = 'https://www.elastic.co/subscriptions';
@ -46,7 +48,7 @@ export interface FormDrilldownWizardProps<
/**
* List of possible triggers in current context
*/
supportedTriggers: TriggerId[];
triggers: TriggerId[];
triggerPickerDocsLink?: string;
}
@ -62,9 +64,20 @@ export const FormDrilldownWizard: React.FC<FormDrilldownWizardProps> = ({
actionFactoryContext,
onSelectedTriggersChange,
getTriggerInfo,
supportedTriggers,
triggers,
triggerPickerDocsLink,
}) => {
if (!triggers || !triggers.length) {
// Below callout is not translated, because this message is only for developers.
return (
<EuiCallOut title="Sorry, there was an error" color="danger" iconType="alert">
<p>
No triggers provided in <EuiCode>trigger</EuiCode> prop.
</p>
</EuiCallOut>
);
}
const nameFragment = (
<EuiFormRow label={txtNameOfDrilldown}>
<EuiFieldText
@ -89,10 +102,7 @@ export const FormDrilldownWizard: React.FC<FormDrilldownWizardProps> = ({
external
data-test-subj={'getMoreActionsLink'}
>
<FormattedMessage
id="xpack.uiActionsEnhanced.drilldowns.components.FormDrilldownWizard.getMoreActionsLinkLabel"
defaultMessage="Get more actions"
/>
{txtGetMoreActions}
</EuiLink>
</EuiText>
);
@ -114,7 +124,7 @@ export const FormDrilldownWizard: React.FC<FormDrilldownWizardProps> = ({
context={actionFactoryContext}
onSelectedTriggersChange={onSelectedTriggersChange}
getTriggerInfo={getTriggerInfo}
supportedTriggers={supportedTriggers}
triggers={triggers}
triggerPickerDocsLink={triggerPickerDocsLink}
/>
</EuiFormRow>

View file

@ -26,3 +26,10 @@ export const txtDrilldownAction = i18n.translate(
defaultMessage: 'Action',
}
);
export const txtGetMoreActions = i18n.translate(
'xpack.uiActionsEnhanced.drilldowns.components.FormDrilldownWizard.getMoreActionsLinkLabel',
{
defaultMessage: 'Get more actions',
}
);

View file

@ -0,0 +1,88 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
export const toastDrilldownCreated = {
title: (drilldownName: string) =>
i18n.translate(
'xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.toast.drilldownCreatedTitle',
{
defaultMessage: 'Drilldown "{drilldownName}" created',
values: {
drilldownName,
},
}
),
text: i18n.translate(
'xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.toast.drilldownCreatedText',
{
// TODO: remove `Save your dashboard before testing.` part
// when drilldowns are used not only in dashboard
// or after https://github.com/elastic/kibana/issues/65179 implemented
defaultMessage: 'Save your dashboard before testing.',
}
),
};
export const toastDrilldownEdited = {
title: (drilldownName: string) =>
i18n.translate(
'xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.toast.drilldownEditedTitle',
{
defaultMessage: 'Drilldown "{drilldownName}" updated',
values: {
drilldownName,
},
}
),
text: i18n.translate(
'xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.toast.drilldownEditedText',
{
defaultMessage: 'Save your dashboard before testing.',
}
),
};
export const toastDrilldownDeleted = {
title: i18n.translate(
'xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.toast.drilldownDeletedTitle',
{
defaultMessage: 'Drilldown deleted',
}
),
text: i18n.translate(
'xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.toast.drilldownDeletedText',
{
defaultMessage: 'Save your dashboard before testing.',
}
),
};
export const toastDrilldownsDeleted = {
title: (n: number) =>
i18n.translate(
'xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.toast.drilldownsDeletedTitle',
{
defaultMessage: '{n} drilldowns deleted',
values: { n },
}
),
text: i18n.translate(
'xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.toast.drilldownsDeletedText',
{
defaultMessage: 'Save your dashboard before testing.',
}
),
};
export const toastDrilldownsCRUDError = i18n.translate(
'xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.toast.drilldownsCRUDErrorTitle',
{
defaultMessage: 'Error saving drilldown',
description: 'Title for generic error toast when persisting drilldown updates failed',
}
);

View file

@ -0,0 +1,35 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { useEffect, useState } from 'react';
import { ActionFactory, BaseActionFactoryContext } from '../../dynamic_actions';
export function useCompatibleActionFactoriesForCurrentContext<
Context extends BaseActionFactoryContext = BaseActionFactoryContext
>(actionFactories: ActionFactory[], context: Context) {
const [compatibleActionFactories, setCompatibleActionFactories] = useState<ActionFactory[]>();
useEffect(() => {
let canceled = false;
async function updateCompatibleFactoriesForContext() {
const compatibility = await Promise.all(
actionFactories.map((factory) => factory.isCompatible(context))
);
if (canceled) return;
const compatibleFactories = actionFactories.filter((_, i) => compatibility[i]);
const triggerSupportedFactories = compatibleFactories.filter((factory) =>
factory.supportedTriggers().some((trigger) => context.triggers.includes(trigger))
);
setCompatibleActionFactories(triggerSupportedFactories);
}
updateCompatibleFactoriesForContext();
return () => {
canceled = true;
};
}, [context, actionFactories, context.triggers]);
return compatibleActionFactories;
}

View file

@ -0,0 +1,86 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { useState } from 'react';
import { ToastsStart } from 'kibana/public';
import useMountedState from 'react-use/lib/useMountedState';
import { TriggerId } from '../../../../../../src/plugins/ui_actions/public';
import { useContainerState } from '../../../../../../src/plugins/kibana_utils/public';
import {
toastDrilldownCreated,
toastDrilldownDeleted,
toastDrilldownEdited,
toastDrilldownsCRUDError,
toastDrilldownsDeleted,
} from './i18n';
import { DynamicActionManager, SerializedAction } from '../../dynamic_actions';
export function useDrilldownsStateManager(
actionManager: DynamicActionManager,
toastService: ToastsStart
) {
const { events: drilldowns } = useContainerState(actionManager.state);
const [isLoading, setIsLoading] = useState(false);
const isMounted = useMountedState();
async function run(op: () => Promise<void>) {
setIsLoading(true);
try {
await op();
} catch (e) {
toastService.addError(e, {
title: toastDrilldownsCRUDError,
});
if (!isMounted) return;
setIsLoading(false);
return;
}
}
async function createDrilldown(action: SerializedAction, selectedTriggers: TriggerId[]) {
await run(async () => {
await actionManager.createEvent(action, selectedTriggers);
toastService.addSuccess({
title: toastDrilldownCreated.title(action.name),
text: toastDrilldownCreated.text,
});
});
}
async function editDrilldown(
drilldownId: string,
action: SerializedAction,
selectedTriggers: TriggerId[]
) {
await run(async () => {
await actionManager.updateEvent(drilldownId, action, selectedTriggers);
toastService.addSuccess({
title: toastDrilldownEdited.title(action.name),
text: toastDrilldownEdited.text,
});
});
}
async function deleteDrilldown(drilldownIds: string | string[]) {
await run(async () => {
drilldownIds = Array.isArray(drilldownIds) ? drilldownIds : [drilldownIds];
await actionManager.deleteEvents(drilldownIds);
toastService.addSuccess(
drilldownIds.length === 1
? {
title: toastDrilldownDeleted.title,
text: toastDrilldownDeleted.text,
}
: {
title: toastDrilldownsDeleted.title(drilldownIds.length),
text: toastDrilldownsDeleted.text,
}
);
});
}
return { drilldowns, isLoading, createDrilldown, editDrilldown, deleteDrilldown };
}

View file

@ -0,0 +1,22 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { useState } from 'react';
import { IStorageWrapper } from '../../../../../../src/plugins/kibana_utils/public';
export function useWelcomeMessage(storage: IStorageWrapper): [boolean, () => void] {
const key = `drilldowns:hidWelcomeMessage`;
const [hideWelcomeMessage, setHideWelcomeMessage] = useState<boolean>(storage.get(key) ?? false);
return [
!hideWelcomeMessage,
() => {
if (hideWelcomeMessage) return;
setHideWelcomeMessage(true);
storage.set(key, true);
},
];
}