diff --git a/x-pack/examples/ui_actions_enhanced_examples/public/plugin.ts b/x-pack/examples/ui_actions_enhanced_examples/public/plugin.ts
index 8034c378cc64f..f31b8dd569f43 100644
--- a/x-pack/examples/ui_actions_enhanced_examples/public/plugin.ts
+++ b/x-pack/examples/ui_actions_enhanced_examples/public/plugin.ts
@@ -11,7 +11,7 @@ import {
AdvancedUiActionsStart,
} from '../../../../x-pack/plugins/ui_actions_enhanced/public';
import { DashboardHelloWorldDrilldown } from './dashboard_hello_world_drilldown';
-import { DashboardToUrlDrilldown } from './dashboard_to_url_drilldown';
+import { SampleUrlDrilldown } from './sample_url_drilldown';
import { DashboardToDiscoverDrilldown } from './dashboard_to_discover_drilldown';
import { createStartServicesGetter } from '../../../../src/plugins/kibana_utils/public';
import { DiscoverSetup, DiscoverStart } from '../../../../src/plugins/discover/public';
@@ -37,7 +37,11 @@ export class UiActionsEnhancedExamplesPlugin
const start = createStartServicesGetter(core.getStartServices);
uiActions.registerDrilldown(new DashboardHelloWorldDrilldown());
- uiActions.registerDrilldown(new DashboardToUrlDrilldown());
+ uiActions.registerDrilldown(
+ new SampleUrlDrilldown({
+ getGlobalScope: () => ({ kibanaUrl: window.location.origin + core.http.basePath.get() }),
+ })
+ );
uiActions.registerDrilldown(new DashboardToDiscoverDrilldown({ start }));
}
diff --git a/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_url_drilldown/index.tsx b/x-pack/examples/ui_actions_enhanced_examples/public/sample_url_drilldown/index.tsx
similarity index 57%
rename from x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_url_drilldown/index.tsx
rename to x-pack/examples/ui_actions_enhanced_examples/public/sample_url_drilldown/index.tsx
index 5e4ba54864461..d0dbf47bd457b 100644
--- a/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_url_drilldown/index.tsx
+++ b/x-pack/examples/ui_actions_enhanced_examples/public/sample_url_drilldown/index.tsx
@@ -5,13 +5,15 @@
*/
import React from 'react';
-import { EuiFormRow, EuiSwitch, EuiFieldText, EuiCallOut, EuiSpacer } from '@elastic/eui';
+import { EuiCallOut, EuiSpacer } from '@elastic/eui';
import { reactToUiComponent } from '../../../../../src/plugins/kibana_react/public';
-import { UiActionsEnhancedDrilldownDefinition as Drilldown } from '../../../../plugins/ui_actions_enhanced/public';
import {
- RangeSelectTriggerContext,
- ValueClickTriggerContext,
-} from '../../../../../src/plugins/embeddable/public';
+ UiActionsEnhancedDrilldownDefinition as Drilldown,
+ UiActionsEnhancedUrlDrilldownCollectConfig as UrlDrilldownCollectConfig,
+ UiActionsEnhancedUrlDrilldownConfig as UrlDrilldownConfig,
+ UiActionsEnhancedUrlDrilldownGlobalScope as UrlDrilldownGlobalScope,
+ uiActionsEnhancedUrlDrilldownCompile as compile,
+} from '../../../../plugins/ui_actions_enhanced/public';
import { CollectConfigProps as CollectConfigPropsBase } from '../../../../../src/plugins/kibana_utils/public';
function isValidUrl(url: string) {
@@ -23,8 +25,6 @@ function isValidUrl(url: string) {
}
}
-export type ActionContext = RangeSelectTriggerContext | ValueClickTriggerContext;
-
export interface Config {
url: string;
openInNewTab: boolean;
@@ -32,10 +32,10 @@ export interface Config {
export type CollectConfigProps = CollectConfigPropsBase;
-const SAMPLE_DASHBOARD_TO_URL_DRILLDOWN = 'SAMPLE_DASHBOARD_TO_URL_DRILLDOWN';
+const SAMPLE_URL_DRILLDOWN = 'SAMPLE_URL_DRILLDOWN';
-export class DashboardToUrlDrilldown implements Drilldown {
- public readonly id = SAMPLE_DASHBOARD_TO_URL_DRILLDOWN;
+export class SampleUrlDrilldown implements Drilldown {
+ public readonly id = SAMPLE_URL_DRILLDOWN;
public readonly order = 8;
@@ -45,6 +45,8 @@ export class DashboardToUrlDrilldown implements Drilldown
public readonly euiIcon = 'link';
+ constructor(private params: { getGlobalScope: () => UrlDrilldownGlobalScope }) {}
+
private readonly ReactCollectConfig: React.FC = ({ config, onConfig }) => (
<>
@@ -59,28 +61,11 @@ export class DashboardToUrlDrilldown implements Drilldown
-
- onConfig({ ...config, url: event.target.value })}
- onBlur={() => {
- if (!config.url) return;
- if (/https?:\/\//.test(config.url)) return;
- onConfig({ ...config, url: 'https://' + config.url });
- }}
- />
-
-
- onConfig({ ...config, openInNewTab: !config.openInNewTab })}
- />
-
+
>
);
@@ -100,12 +85,12 @@ export class DashboardToUrlDrilldown implements Drilldown
* `getHref` is need to support mouse middle-click and Cmd + Click behavior
* to open a link in new tab.
*/
- public readonly getHref = async (config: Config, context: ActionContext) => {
- return config.url;
+ public readonly getHref = async (config: Config) => {
+ return compile(config.url, this.params.getGlobalScope());
};
- public readonly execute = async (config: Config, context: ActionContext) => {
- const url = await this.getHref(config, context);
+ public readonly execute = async (config: Config) => {
+ const url = await this.getHref(config);
if (config.openInNewTab) {
window.open(url, '_blank');
diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.tsx
index 4804a700c6cff..bf9d8b616984a 100644
--- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.tsx
+++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.tsx
@@ -66,6 +66,7 @@ export class FlyoutCreateDrilldownAction implements ActionByType handle.close()}
viewMode={'create'}
dynamicActionManager={embeddable.enhancements.dynamicActions}
+ context={{ embeddable }}
/>
),
{
diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.tsx
index af1ae67454463..f94b23a5b9a74 100644
--- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.tsx
+++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.tsx
@@ -62,6 +62,7 @@ export class FlyoutEditDrilldownAction implements ActionByType handle.close()}
viewMode={'manage'}
dynamicActionManager={embeddable.enhancements.dynamicActions}
+ context={{ embeddable }}
/>
),
{
diff --git a/x-pack/plugins/embeddable_enhanced/kibana.json b/x-pack/plugins/embeddable_enhanced/kibana.json
index 5663671de7bd9..14a1ae9f4e5df 100644
--- a/x-pack/plugins/embeddable_enhanced/kibana.json
+++ b/x-pack/plugins/embeddable_enhanced/kibana.json
@@ -3,5 +3,5 @@
"version": "kibana",
"server": false,
"ui": true,
- "requiredPlugins": ["embeddable", "uiActionsEnhanced"]
+ "requiredPlugins": ["embeddable", "uiActionsEnhanced", "data"]
}
diff --git a/x-pack/plugins/embeddable_enhanced/public/drilldowns/embeddable_to_url_drilldown/README.md b/x-pack/plugins/embeddable_enhanced/public/drilldowns/embeddable_to_url_drilldown/README.md
new file mode 100644
index 0000000000000..3d9c7c64d432f
--- /dev/null
+++ b/x-pack/plugins/embeddable_enhanced/public/drilldowns/embeddable_to_url_drilldown/README.md
@@ -0,0 +1,3 @@
+# Embeddable to URL drilldown
+
+Specific url drilldown implementation which relies on the `IEmbeddable` as context and on `ValueClickTrigger` and `RangeSelectTrigger` for action triggers
diff --git a/x-pack/plugins/embeddable_enhanced/public/drilldowns/embeddable_to_url_drilldown/embeddable_to_url_drilldown.tsx b/x-pack/plugins/embeddable_enhanced/public/drilldowns/embeddable_to_url_drilldown/embeddable_to_url_drilldown.tsx
new file mode 100644
index 0000000000000..9d472ada13606
--- /dev/null
+++ b/x-pack/plugins/embeddable_enhanced/public/drilldowns/embeddable_to_url_drilldown/embeddable_to_url_drilldown.tsx
@@ -0,0 +1,286 @@
+/*
+ * 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 _ from 'lodash';
+import { reactToUiComponent } from '../../../../../../src/plugins/kibana_react/public';
+import {
+ UiActionsEnhancedDrilldownDefinition as DrilldownDefinition,
+ UiActionsEnhancedUrlDrilldownConfig as UrlDrilldownConfig,
+ UiActionsEnhancedUrlDrilldownScope as UrlDrilldownScope,
+ UiActionsEnhancedUrlDrilldownGlobalScope as UrlDrilldownGlobalScope,
+ UiActionsEnhancedUrlDrilldownCollectConfig as UrlDrilldownCollectConfig,
+ uiActionsEnhancedUrlDrilldownCompile as compile,
+} from '../../../../ui_actions_enhanced/public';
+import {
+ IEmbeddable,
+ isRangeSelectTriggerContext,
+ isValueClickTriggerContext,
+ RangeSelectTriggerContext,
+ ValueClickTriggerContext,
+} from '../../../../../../src/plugins/embeddable/public';
+import { CollectConfigProps as CollectConfigPropsBase } from '../../../../../../src/plugins/kibana_utils/public';
+import {
+ DataPublicPluginStart,
+ esFilters,
+ Filter,
+ Query,
+ TimeRange,
+} from '../../../../../../src/plugins/data/public';
+
+export type ActionContext = RangeSelectTriggerContext | ValueClickTriggerContext;
+export type CollectConfigProps = CollectConfigPropsBase<
+ UrlDrilldownConfig,
+ { embeddable?: IEmbeddable }
+>;
+
+interface EmbeddableContextScope {
+ panelId?: string;
+ panelTitle?: string;
+ savedObjectId?: string;
+ filters?: Filter[];
+ query?: Query;
+ timeRange?: TimeRange;
+}
+
+type EmbeddableToUrlDrilldownScope = UrlDrilldownScope<
+ EmbeddableContextScope,
+ EmbeddableTriggerEventScope
+>;
+
+interface EmbeddableTriggerEventScope {
+ /**
+ * More then one filter could come, for example, from heat map visualization
+ */
+ filters: EmbeddableTriggerFilter[];
+ /**
+ * 1st el from {@link filters}. just a shortcut.
+ */
+ filter?: EmbeddableTriggerFilter;
+}
+
+/**
+ * Generalized & simplified interface which covers possible filters
+ * that can be created from {@link ValueClickTriggerContext} & {@link RangeSelectTriggerContext} triggers
+ */
+interface EmbeddableTriggerFilter {
+ key: string;
+ value: string;
+ negate: boolean;
+ from: string;
+ to: string;
+}
+
+const mockEventScope: EmbeddableTriggerEventScope = {
+ filter: {
+ key: '__testValueKey__',
+ value: '__testValueValue__',
+ from: '__testValueFrom__',
+ to: '__testValueTo__',
+ negate: false,
+ },
+ filters: [
+ {
+ key: '__testValueKey__',
+ value: '__testValueValue__',
+ from: '__testValueFrom__',
+ to: '__testValueTo__',
+ negate: false,
+ },
+ ],
+};
+
+function buildScope(
+ global: UrlDrilldownGlobalScope,
+ context: EmbeddableContextScope,
+ event: EmbeddableTriggerEventScope = mockEventScope
+): EmbeddableToUrlDrilldownScope {
+ return {
+ ...global,
+ context,
+ event,
+ };
+}
+
+type DataActionsHelpers = Pick<
+ DataPublicPluginStart['actions'],
+ 'createFiltersFromValueClickAction' | 'createFiltersFromRangeSelectAction'
+>;
+export interface Params {
+ /**
+ * Inject global static variables
+ */
+ getGlobalScope: () => UrlDrilldownGlobalScope;
+
+ /**
+ * Dependency on data plugin to extract filters from Click & Range actions
+ */
+ getDataActionsHelpers: () => DataActionsHelpers;
+}
+
+export class EmbeddableToUrlDrilldownDefinition
+ implements DrilldownDefinition {
+ public readonly id = 'EMB_TO_URL_DRILLDOWN';
+
+ public readonly minimalLicense = 'gold';
+
+ public readonly order = 8;
+
+ public readonly getDisplayName = () => 'Go to URL';
+
+ public readonly euiIcon = 'link';
+
+ constructor(private params: Params) {}
+
+ private readonly ReactCollectConfig: React.FC = ({
+ config,
+ onConfig,
+ context,
+ }) => {
+ const { getGlobalScope } = this.params;
+ // eslint-disable-next-line react-hooks/rules-of-hooks
+ const scope = React.useMemo(
+ () => buildScope(getGlobalScope(), getContextScopeFromEmbeddable(context.embeddable)),
+ [getGlobalScope, context]
+ );
+
+ return ;
+ };
+
+ public readonly CollectConfig = reactToUiComponent(this.ReactCollectConfig);
+
+ public readonly createConfig = () => ({
+ url: '',
+ openInNewTab: false,
+ });
+
+ public readonly isConfigValid = (config: UrlDrilldownConfig): config is UrlDrilldownConfig => {
+ if (!config.url) return false;
+ return isValidUrl(config.url);
+ };
+
+ /**
+ * `getHref` is need to support mouse middle-click and Cmd + Click behavior
+ * to open a link in new tab.
+ */
+ public readonly getHref = async (config: UrlDrilldownConfig, context: ActionContext) => {
+ const globalScope = this.params.getGlobalScope();
+ const contextScope = getContextScopeFromEmbeddable(context.embeddable);
+ const eventScope = await getEventScopeFromActionContext(
+ context,
+ this.params.getDataActionsHelpers()
+ );
+
+ const scope = buildScope(globalScope, contextScope, eventScope);
+ const url = compile(config.url, scope);
+
+ return url;
+ };
+
+ public readonly execute = async (config: UrlDrilldownConfig, context: ActionContext) => {
+ const url = await this.getHref(config, context);
+
+ if (config.openInNewTab) {
+ window.open(url, '_blank', 'noopener');
+ } else {
+ window.location.href = url;
+ }
+ };
+}
+
+function getContextScopeFromEmbeddable(embeddable?: IEmbeddable): EmbeddableContextScope {
+ if (!embeddable) return {};
+ const input = embeddable.getInput();
+ const output = embeddable.getOutput();
+ // TODO: type it better
+ return {
+ panelId: input.id,
+ panelTitle: output.title,
+ ..._.pick(input, ['query', 'timeRange', 'filters']),
+ ...(output.savedObjectId
+ ? { savedObjectId: output.savedObjectId }
+ : _.pick(input, 'savedObjectId')),
+ };
+}
+
+async function getEventScopeFromActionContext(
+ context: ActionContext,
+ { createFiltersFromRangeSelectAction, createFiltersFromValueClickAction }: DataActionsHelpers
+): Promise {
+ const filtersFromEvent = await (async () => {
+ try {
+ if (isRangeSelectTriggerContext(context))
+ return await createFiltersFromRangeSelectAction(context.data);
+ if (isValueClickTriggerContext(context))
+ return await createFiltersFromValueClickAction(context.data);
+
+ // eslint-disable-next-line no-console
+ console.warn(
+ `
+ Url drilldown: can't extract filters from action.
+ Is it not supported action?`,
+ context
+ );
+
+ return [];
+ } catch (e) {
+ // eslint-disable-next-line no-console
+ console.warn(
+ `
+ URL drilldown: error extracting filters from action.
+ Continuing without applying filters from event`,
+ e
+ );
+ return [];
+ }
+ })();
+
+ function dataFilterToEmbeddableTriggerFilter(filter: Filter): EmbeddableTriggerFilter {
+ if (esFilters.isRangeFilter(filter)) {
+ const rangeKey = Object.keys(filter.range)[0];
+ const range = filter.range[rangeKey];
+ return {
+ key: rangeKey ?? filter.meta.key ?? '',
+ value: (range.from ?? range.gt ?? range.gte ?? '').toString(),
+ from: (range.from ?? range.gt ?? range.gte ?? '').toString(),
+ to: (range.to ?? range.lt ?? range.lte ?? '').toString(),
+ negate: filter.meta.negate ?? false,
+ };
+ } else {
+ const value =
+ (filter.meta.value &&
+ (typeof filter.meta.value === 'string' ? filter.meta.value : filter.meta.value())) ??
+ '';
+ return {
+ key: filter.meta.key ?? '',
+ value:
+ (filter.meta.value &&
+ (typeof filter.meta.value === 'string' ? filter.meta.value : filter.meta.value())) ??
+ '',
+ from: value,
+ to: value,
+ negate: filter.meta.negate ?? false,
+ };
+ }
+ }
+
+ const eventFilters = filtersFromEvent.map(dataFilterToEmbeddableTriggerFilter);
+ const eventScope: EmbeddableTriggerEventScope = {
+ filters: eventFilters,
+ filter: eventFilters[0],
+ };
+
+ return eventScope;
+}
+
+export function isValidUrl(url: string) {
+ try {
+ new URL(url);
+ return true;
+ } catch {
+ return false;
+ }
+}
diff --git a/x-pack/plugins/embeddable_enhanced/public/drilldowns/embeddable_to_url_drilldown/index.ts b/x-pack/plugins/embeddable_enhanced/public/drilldowns/embeddable_to_url_drilldown/index.ts
new file mode 100644
index 0000000000000..966488da4cc6c
--- /dev/null
+++ b/x-pack/plugins/embeddable_enhanced/public/drilldowns/embeddable_to_url_drilldown/index.ts
@@ -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 { EmbeddableToUrlDrilldownDefinition } from './embeddable_to_url_drilldown';
diff --git a/x-pack/plugins/embeddable_enhanced/public/drilldowns/index.ts b/x-pack/plugins/embeddable_enhanced/public/drilldowns/index.ts
new file mode 100644
index 0000000000000..388a97c14cfda
--- /dev/null
+++ b/x-pack/plugins/embeddable_enhanced/public/drilldowns/index.ts
@@ -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 './embeddable_to_url_drilldown';
diff --git a/x-pack/plugins/embeddable_enhanced/public/plugin.ts b/x-pack/plugins/embeddable_enhanced/public/plugin.ts
index fd0bcc2023269..4c114445eab3f 100644
--- a/x-pack/plugins/embeddable_enhanced/public/plugin.ts
+++ b/x-pack/plugins/embeddable_enhanced/public/plugin.ts
@@ -30,6 +30,9 @@ import {
AdvancedUiActionsStart,
} from '../../ui_actions_enhanced/public';
import { PanelNotificationsAction, ACTION_PANEL_NOTIFICATIONS } from './actions';
+import { EmbeddableToUrlDrilldownDefinition } from './drilldowns';
+import { createStartServicesGetter } from '../../../../src/plugins/kibana_utils/public';
+import { DataPublicPluginSetup, DataPublicPluginStart } from '../../../../src/plugins/data/public';
declare module '../../../../src/plugins/ui_actions/public' {
export interface ActionContextMapping {
@@ -40,11 +43,13 @@ declare module '../../../../src/plugins/ui_actions/public' {
export interface SetupDependencies {
embeddable: EmbeddableSetup;
uiActionsEnhanced: AdvancedUiActionsSetup;
+ data: DataPublicPluginSetup;
}
export interface StartDependencies {
embeddable: EmbeddableStart;
uiActionsEnhanced: AdvancedUiActionsStart;
+ data: DataPublicPluginStart;
}
// eslint-disable-next-line
@@ -62,9 +67,22 @@ export class EmbeddableEnhancedPlugin
public setup(core: CoreSetup, plugins: SetupDependencies): SetupContract {
this.setCustomEmbeddableFactoryProvider(plugins);
+ const start = createStartServicesGetter(core.getStartServices);
+ const getDataActionsHelpers = () => {
+ return start().plugins.data.actions;
+ };
+
const panelNotificationAction = new PanelNotificationsAction();
plugins.uiActionsEnhanced.registerAction(panelNotificationAction);
plugins.uiActionsEnhanced.attachAction(PANEL_NOTIFICATION_TRIGGER, panelNotificationAction.id);
+ plugins.uiActionsEnhanced.registerDrilldown(
+ new EmbeddableToUrlDrilldownDefinition({
+ getGlobalScope: () => ({
+ kibanaUrl: window.location.origin + core.http.basePath.get(),
+ }),
+ getDataActionsHelpers,
+ })
+ );
return {};
}
diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.tsx b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.tsx
index 20d15b4f4d2bd..2e7f734a170fa 100644
--- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.tsx
+++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.tsx
@@ -37,6 +37,11 @@ interface ConnectedFlyoutManageDrilldownsProps {
dynamicActionManager: DynamicActionManager;
viewMode?: 'create' | 'manage';
onClose?: () => void;
+
+ /**
+ * TODO?
+ */
+ context?: object;
}
/**
@@ -74,9 +79,10 @@ export function createFlyoutManageDrilldowns({
const factoryContext: object = React.useMemo(
() => ({
+ ...props.context,
triggers: selectedTriggers,
}),
- [selectedTriggers]
+ [props.context, selectedTriggers]
);
const actionFactories = useCompatibleActionFactoriesForCurrentContext(
diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/index.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/index.ts
index 0d469e46fa9fd..e15381ad1b33d 100644
--- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/index.ts
+++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/index.ts
@@ -6,3 +6,4 @@
export * from './drilldown_definition';
export * from './components';
+export * from './url_drilldown_lib';
diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown_lib/README.md b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown_lib/README.md
new file mode 100644
index 0000000000000..b9bd4ecf1ac04
--- /dev/null
+++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown_lib/README.md
@@ -0,0 +1 @@
+# Building blocks for implementing custom URL Drilldown
diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown_lib/components/index.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown_lib/components/index.ts
new file mode 100644
index 0000000000000..be1eabf734236
--- /dev/null
+++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown_lib/components/index.ts
@@ -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 './url_drilldown_collect_config';
diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown_lib/components/url_drilldown_collect_config.tsx b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown_lib/components/url_drilldown_collect_config.tsx
new file mode 100644
index 0000000000000..f73b668a4ceb7
--- /dev/null
+++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown_lib/components/url_drilldown_collect_config.tsx
@@ -0,0 +1,150 @@
+/*
+ * 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, { useState } from 'react';
+import {
+ EuiButtonIcon,
+ EuiContextMenuItem,
+ EuiContextMenuPanel,
+ EuiFormRow,
+ EuiLink,
+ EuiPopover,
+ EuiSwitch,
+ EuiText,
+ EuiTextArea,
+} from '@elastic/eui';
+import { getFlattenedObject } from '../../../../../../../src/core/public';
+import { UrlDrilldownConfig, UrlDrilldownScope } from '../types';
+import { compile } from '../url_template';
+import { isValidUrl } from '../utils';
+
+function buildVariableListForSuggestions(scope: UrlDrilldownScope): string[] {
+ return Object.keys(getFlattenedObject(scope));
+}
+
+export interface UrlDrilldownCollectConfig {
+ config: UrlDrilldownConfig;
+ onConfig: (newConfig: UrlDrilldownConfig) => void;
+ scope: UrlDrilldownScope;
+}
+
+export const UrlDrilldownCollectConfig: React.FC = ({
+ config,
+ onConfig,
+ scope,
+}: UrlDrilldownCollectConfig) => {
+ const compiledUrl = React.useMemo(() => compile(config.url, scope), [config.url, scope]);
+ const variables = React.useMemo(() => buildVariableListForSuggestions(scope), [scope]);
+ const isValid = !compiledUrl || isValidUrl(compiledUrl);
+
+ return (
+ <>
+ {
+ // TODO: better insert logic depending on selection?
+ onConfig({ ...config, url: config.url + `{{${variable}}}` });
+ }}
+ />
+ }
+ >
+ onConfig({ ...config, url: event.target.value })}
+ onBlur={() => {
+ if (!compiledUrl) return;
+ if (/https?:\/\//.test(compiledUrl)) return;
+ onConfig({ ...config, url: 'https://' + config.url });
+ }}
+ />
+
+
+
+ Preview
+
+
+ }
+ >
+
+
+
+ onConfig({ ...config, openInNewTab: !config.openInNewTab })}
+ />
+
+ >
+ );
+};
+
+function AddVariableButton({
+ variables,
+ onSelect,
+}: {
+ variables: string[];
+ onSelect: (variable: string) => void;
+}) {
+ const [isVariablesPopoverOpen, setIsVariablesPopoverOpen] = useState(false);
+
+ const renderVariables = () =>
+ variables.map((variable: string, i: number) => (
+ {
+ onSelect(variable);
+ setIsVariablesPopoverOpen(false);
+ }}
+ >
+ {`{{${variable}}}`}
+
+ ));
+
+ return (
+ setIsVariablesPopoverOpen(true)}
+ iconType="indexOpen"
+ title={'Add variable'}
+ aria-label={'Add variable'}
+ />
+ }
+ isOpen={isVariablesPopoverOpen}
+ closePopover={() => setIsVariablesPopoverOpen(false)}
+ panelPaddingSize="none"
+ anchorPosition="downLeft"
+ >
+
+
+ );
+}
diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown_lib/index.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown_lib/index.ts
new file mode 100644
index 0000000000000..e98f4d5a7ded9
--- /dev/null
+++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown_lib/index.ts
@@ -0,0 +1,9 @@
+/*
+ * 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 './types';
+export * from './components';
+export { compile } from './url_template';
diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown_lib/types.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown_lib/types.ts
new file mode 100644
index 0000000000000..039b02b0abd7b
--- /dev/null
+++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown_lib/types.ts
@@ -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.
+ */
+
+export interface UrlDrilldownConfig {
+ url: string;
+ openInNewTab: boolean;
+}
+
+export interface UrlDrilldownScope<
+ ContextScope extends object = object,
+ EventScope extends object = object
+> extends UrlDrilldownGlobalScope {
+ context?: ContextScope;
+ event?: EventScope;
+}
+
+export interface UrlDrilldownGlobalScope {
+ kibanaUrl: string;
+}
diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown_lib/url_template.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown_lib/url_template.ts
new file mode 100644
index 0000000000000..ba817a15f19e5
--- /dev/null
+++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown_lib/url_template.ts
@@ -0,0 +1,59 @@
+/*
+ * 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 { create as createHandlebars } from 'handlebars';
+import { encode } from 'rison-node';
+import dateMath from '@elastic/datemath';
+import moment from 'moment';
+
+const handlebars = createHandlebars();
+
+handlebars.registerHelper('json', (v) => {
+ try {
+ return JSON.stringify(v);
+ } catch (e) {
+ return v;
+ }
+});
+
+handlebars.registerHelper('rison', (v) => {
+ try {
+ return encode(v);
+ } catch (e) {
+ return v;
+ }
+});
+
+handlebars.registerHelper('date', (date: string | Date, format: string) => {
+ const momentDate = typeof date === 'string' ? dateMath.parse(date) : moment(date);
+
+ if (!momentDate || !momentDate.isValid()) {
+ // eslint-disable-next-line no-console
+ console.warn(`urlTemplate: Can\'t parse date string ${date}. Returning original string.`);
+ return date;
+ }
+
+ return format
+ ? (() => {
+ try {
+ return momentDate.format(format);
+ } catch (e) {
+ return momentDate.toISOString();
+ }
+ })()
+ : momentDate.toISOString();
+});
+
+export function compile(url: string, context: object): string {
+ try {
+ const template = handlebars.compile(url);
+ return template(context);
+ } catch (e) {
+ // eslint-disable-next-line no-console
+ console.warn(e);
+ return url;
+ }
+}
diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown_lib/utils.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown_lib/utils.ts
new file mode 100644
index 0000000000000..33a5eb721580b
--- /dev/null
+++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown_lib/utils.ts
@@ -0,0 +1,14 @@
+/*
+ * 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 function isValidUrl(url: string) {
+ try {
+ new URL(url);
+ return true;
+ } catch {
+ return false;
+ }
+}
diff --git a/x-pack/plugins/ui_actions_enhanced/public/index.ts b/x-pack/plugins/ui_actions_enhanced/public/index.ts
index a3cfddb31d663..68b56b7f3ccdb 100644
--- a/x-pack/plugins/ui_actions_enhanced/public/index.ts
+++ b/x-pack/plugins/ui_actions_enhanced/public/index.ts
@@ -30,4 +30,11 @@ export {
MemoryActionStorage as UiActionsEnhancedMemoryActionStorage,
} from './dynamic_actions';
-export { DrilldownDefinition as UiActionsEnhancedDrilldownDefinition } from './drilldowns';
+export {
+ DrilldownDefinition as UiActionsEnhancedDrilldownDefinition,
+ UrlDrilldownCollectConfig as UiActionsEnhancedUrlDrilldownCollectConfig,
+ UrlDrilldownConfig as UiActionsEnhancedUrlDrilldownConfig,
+ UrlDrilldownGlobalScope as UiActionsEnhancedUrlDrilldownGlobalScope,
+ UrlDrilldownScope as UiActionsEnhancedUrlDrilldownScope,
+ compile as uiActionsEnhancedUrlDrilldownCompile,
+} from './drilldowns';