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';