diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index b426621fed2963..5a4a60c2e628e3 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -504,6 +504,10 @@ in their infrastructure. |Contains HTTP endpoints and UiSettings that are slated for removal. +|{kib-repo}blob/{branch}/x-pack/plugins/drilldowns/url_drilldown/README.md[urlDrilldown] +|NOTE: This plugin contains implementation of URL drilldown. For drilldowns infrastructure code refer to ui_actions_enhanced plugin. + + |=== include::{kibana-root}/src/plugins/dashboard/README.asciidoc[leveloffset=+1] diff --git a/docs/user/dashboard/url-drilldown.asciidoc b/docs/user/dashboard/url-drilldown.asciidoc index e6daf89d727183..ee879256a1fae4 100644 --- a/docs/user/dashboard/url-drilldown.asciidoc +++ b/docs/user/dashboard/url-drilldown.asciidoc @@ -238,3 +238,14 @@ Tip: Consider using <> helper for date formatting. | Aggregation field behind the selected range, if available. |=== + +[float] +[[disable]] +==== Disable URL drilldown + +You can disable URL drilldown feature on your {kib} instance by disabling the plugin: + +["source","yml"] +----------- +url_drilldown.enabled: false +----------- diff --git a/src/plugins/ui_actions/public/mocks.ts b/src/plugins/ui_actions/public/mocks.ts index 3522ac4941ba0a..759430169b6131 100644 --- a/src/plugins/ui_actions/public/mocks.ts +++ b/src/plugins/ui_actions/public/mocks.ts @@ -48,6 +48,7 @@ const createStartContract = (): Start => { executeTriggerActions: jest.fn(), fork: jest.fn(), getAction: jest.fn(), + hasAction: jest.fn(), getTrigger: jest.fn(), getTriggerActions: jest.fn((id: TriggerId) => []), getTriggerCompatibleActions: jest.fn(), diff --git a/src/plugins/ui_actions/public/service/ui_actions_service.ts b/src/plugins/ui_actions/public/service/ui_actions_service.ts index 6028177964fb77..ec5f3afa19c943 100644 --- a/src/plugins/ui_actions/public/service/ui_actions_service.ts +++ b/src/plugins/ui_actions/public/service/ui_actions_service.ts @@ -99,6 +99,10 @@ export class UiActionsService { this.actions.delete(actionId); }; + public readonly hasAction = (actionId: string): boolean => { + return this.actions.has(actionId); + }; + public readonly attachAction = (triggerId: T, actionId: string): void => { const trigger = this.triggers.get(triggerId); diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index a700781438706a..aadd2ac57e824b 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -55,6 +55,7 @@ "xpack.triggersActionsUI": "plugins/triggers_actions_ui", "xpack.upgradeAssistant": "plugins/upgrade_assistant", "xpack.uptime": ["plugins/uptime"], + "xpack.urlDrilldown": "plugins/drilldowns/url_drilldown", "xpack.watcher": "plugins/watcher", "xpack.observability": "plugins/observability" }, diff --git a/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/README.md b/x-pack/plugins/drilldowns/url_drilldown/README.md similarity index 65% rename from x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/README.md rename to x-pack/plugins/drilldowns/url_drilldown/README.md index 996723ccb914d9..8eedc44ca35aeb 100644 --- a/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/README.md +++ b/x-pack/plugins/drilldowns/url_drilldown/README.md @@ -1,24 +1,26 @@ -# Basic url drilldown implementation +## URL drilldown + +> NOTE: This plugin contains implementation of URL drilldown. For drilldowns infrastructure code refer to `ui_actions_enhanced` plugin. Url drilldown allows navigating to external URL or to internal kibana URL. By using variables in url template result url can be dynamic and depend on user's interaction. URL drilldown has 3 sources for variables: -- Global static variables like, for example, `kibanaUrl`. Such variables won’t change depending on a place where url drilldown is used. -- Context variables are dynamic and different depending on where drilldown is created and used. -- Event variables depend on a trigger context. These variables are dynamically extracted from the action context when drilldown is executed. +1. Global static variables like, for example, `kibanaUrl`. Such variables won’t change depending on a place where url drilldown is used. +2. Context variables are dynamic and different depending on where drilldown is created and used. +3. Event variables depend on a trigger context. These variables are dynamically extracted from the action context when drilldown is executed. Difference between `event` and `context` variables, is that real `context` variables are available during drilldown creation (e.g. embeddable panel), but `event` variables mapped from trigger context. Since there is no trigger context during drilldown creation, we have to provide some _mock_ variables for validating and previewing the URL. In current implementation url drilldown has to be used inside the embeddable and with `ValueClickTrigger` or `RangeSelectTrigger`. -- `context` variables extracted from `embeddable` -- `event` variables extracted from `trigger` context +* `context` variables extracted from `embeddable` +* `event` variables extracted from `trigger` context In future this basic url drilldown implementation would allow injecting more variables into `context` (e.g. `dashboard` app specific variables) and would allow providing support for new trigger types from outside. This extensibility improvements are tracked here: https://github.com/elastic/kibana/issues/55324 In case a solution app has a use case for url drilldown that has to be different from current basic implementation and -just extending variables list is not enough, then recommendation is to create own custom url drilldown and reuse building blocks from `ui_actions_enhanced`. +just extending variables list is not enough, then recommendation is to create own custom url drilldown and reuse building blocks from `ui_actions_enhanced`. \ No newline at end of file diff --git a/x-pack/plugins/drilldowns/url_drilldown/kibana.json b/x-pack/plugins/drilldowns/url_drilldown/kibana.json new file mode 100644 index 00000000000000..9bdd13fbfea26d --- /dev/null +++ b/x-pack/plugins/drilldowns/url_drilldown/kibana.json @@ -0,0 +1,8 @@ +{ + "id": "urlDrilldown", + "version": "kibana", + "server": false, + "ui": true, + "requiredPlugins": ["embeddable", "uiActions", "uiActionsEnhanced"], + "requiredBundles": ["kibanaUtils", "kibanaReact"] +} diff --git a/x-pack/plugins/embeddable_enhanced/public/drilldowns/index.ts b/x-pack/plugins/drilldowns/url_drilldown/public/index.ts similarity index 53% rename from x-pack/plugins/embeddable_enhanced/public/drilldowns/index.ts rename to x-pack/plugins/drilldowns/url_drilldown/public/index.ts index a8d5a179dbac1f..b040ef625bc1f2 100644 --- a/x-pack/plugins/embeddable_enhanced/public/drilldowns/index.ts +++ b/x-pack/plugins/drilldowns/url_drilldown/public/index.ts @@ -4,4 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -export * from './url_drilldown'; +import { PluginInitializerContext } from 'src/core/public'; +import { UrlDrilldownPlugin } from './plugin'; + +export function plugin(context: PluginInitializerContext) { + return new UrlDrilldownPlugin(context); +} diff --git a/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/i18n.ts b/x-pack/plugins/drilldowns/url_drilldown/public/lib/i18n.ts similarity index 62% rename from x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/i18n.ts rename to x-pack/plugins/drilldowns/url_drilldown/public/lib/i18n.ts index 748f6f4cecedd5..7e91c6b8490358 100644 --- a/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/i18n.ts +++ b/x-pack/plugins/drilldowns/url_drilldown/public/lib/i18n.ts @@ -6,9 +6,6 @@ import { i18n } from '@kbn/i18n'; -export const txtUrlDrilldownDisplayName = i18n.translate( - 'xpack.embeddableEnhanced.drilldowns.urlDrilldownDisplayName', - { - defaultMessage: 'Go to URL', - } -); +export const txtUrlDrilldownDisplayName = i18n.translate('xpack.urlDrilldown.DisplayName', { + defaultMessage: 'Go to URL', +}); diff --git a/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/index.ts b/x-pack/plugins/drilldowns/url_drilldown/public/lib/index.ts similarity index 100% rename from x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/index.ts rename to x-pack/plugins/drilldowns/url_drilldown/public/lib/index.ts diff --git a/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/url_drilldown.test.ts b/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown.test.ts similarity index 100% rename from x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/url_drilldown.test.ts rename to x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown.test.ts diff --git a/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/url_drilldown.tsx b/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown.tsx similarity index 100% rename from x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/url_drilldown.tsx rename to x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown.tsx diff --git a/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/url_drilldown_scope.test.ts b/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown_scope.test.ts similarity index 100% rename from x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/url_drilldown_scope.test.ts rename to x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown_scope.test.ts diff --git a/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/url_drilldown_scope.ts b/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown_scope.ts similarity index 100% rename from x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/url_drilldown_scope.ts rename to x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown_scope.ts diff --git a/x-pack/plugins/drilldowns/url_drilldown/public/plugin.ts b/x-pack/plugins/drilldowns/url_drilldown/public/plugin.ts new file mode 100644 index 00000000000000..82ce7a129f4976 --- /dev/null +++ b/x-pack/plugins/drilldowns/url_drilldown/public/plugin.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 { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'src/core/public'; +import { EmbeddableSetup, EmbeddableStart } from '../../../../../src/plugins/embeddable/public'; +import { + AdvancedUiActionsSetup, + AdvancedUiActionsStart, + urlDrilldownGlobalScopeProvider, +} from '../../../ui_actions_enhanced/public'; +import { UrlDrilldown } from './lib'; +import { createStartServicesGetter } from '../../../../../src/plugins/kibana_utils/public'; + +export interface SetupDependencies { + embeddable: EmbeddableSetup; + uiActionsEnhanced: AdvancedUiActionsSetup; +} + +export interface StartDependencies { + embeddable: EmbeddableStart; + uiActionsEnhanced: AdvancedUiActionsStart; +} + +// eslint-disable-next-line +export interface SetupContract {} + +// eslint-disable-next-line +export interface StartContract {} + +export class UrlDrilldownPlugin + implements Plugin { + constructor(protected readonly context: PluginInitializerContext) {} + + public setup(core: CoreSetup, plugins: SetupDependencies): SetupContract { + const startServices = createStartServicesGetter(core.getStartServices); + plugins.uiActionsEnhanced.registerDrilldown( + new UrlDrilldown({ + getGlobalScope: urlDrilldownGlobalScopeProvider({ core }), + navigateToUrl: (url: string) => + core.getStartServices().then(([{ application }]) => application.navigateToUrl(url)), + getSyntaxHelpDocsLink: () => + startServices().core.docLinks.links.dashboard.urlDrilldownTemplateSyntax, + getVariablesHelpDocsLink: () => + startServices().core.docLinks.links.dashboard.urlDrilldownVariables, + }) + ); + + return {}; + } + + public start(core: CoreStart, plugins: StartDependencies): StartContract { + return {}; + } + + public stop() {} +} diff --git a/x-pack/plugins/embeddable_enhanced/kibana.json b/x-pack/plugins/embeddable_enhanced/kibana.json index acada946fe0d17..8d49e3e26eb7be 100644 --- a/x-pack/plugins/embeddable_enhanced/kibana.json +++ b/x-pack/plugins/embeddable_enhanced/kibana.json @@ -3,6 +3,5 @@ "version": "kibana", "server": false, "ui": true, - "requiredPlugins": ["embeddable", "kibanaReact", "uiActions", "uiActionsEnhanced"], - "requiredBundles": ["kibanaUtils"] + "requiredPlugins": ["embeddable", "kibanaReact", "uiActions", "uiActionsEnhanced"] } diff --git a/x-pack/plugins/embeddable_enhanced/public/plugin.ts b/x-pack/plugins/embeddable_enhanced/public/plugin.ts index 2138a372523b78..5d5ad852839d4e 100644 --- a/x-pack/plugins/embeddable_enhanced/public/plugin.ts +++ b/x-pack/plugins/embeddable_enhanced/public/plugin.ts @@ -28,11 +28,8 @@ import { UiActionsEnhancedDynamicActionManager as DynamicActionManager, AdvancedUiActionsSetup, AdvancedUiActionsStart, - urlDrilldownGlobalScopeProvider, } from '../../ui_actions_enhanced/public'; import { PanelNotificationsAction, ACTION_PANEL_NOTIFICATIONS } from './actions'; -import { UrlDrilldown } from './drilldowns'; -import { createStartServicesGetter } from '../../../../src/plugins/kibana_utils/public'; declare module '../../../../src/plugins/ui_actions/public' { export interface ActionContextMapping { @@ -64,23 +61,10 @@ export class EmbeddableEnhancedPlugin public setup(core: CoreSetup, plugins: SetupDependencies): SetupContract { this.setCustomEmbeddableFactoryProvider(plugins); - const startServices = createStartServicesGetter(core.getStartServices); const panelNotificationAction = new PanelNotificationsAction(); plugins.uiActionsEnhanced.registerAction(panelNotificationAction); plugins.uiActionsEnhanced.attachAction(PANEL_NOTIFICATION_TRIGGER, panelNotificationAction.id); - plugins.uiActionsEnhanced.registerDrilldown( - new UrlDrilldown({ - getGlobalScope: urlDrilldownGlobalScopeProvider({ core }), - navigateToUrl: (url: string) => - core.getStartServices().then(([{ application }]) => application.navigateToUrl(url)), - getSyntaxHelpDocsLink: () => - startServices().core.docLinks.links.dashboard.urlDrilldownTemplateSyntax, - getVariablesHelpDocsLink: () => - startServices().core.docLinks.links.dashboard.urlDrilldownVariables, - }) - ); - return {}; } diff --git a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.test.ts b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.test.ts index 83232bbce1ba7b..cdd357f3560b89 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.test.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.test.ts @@ -437,8 +437,7 @@ describe('DynamicActionManager', () => { name: 'foo', config: {}, }; - - await expect(manager.createEvent(action, ['SELECT_RANGE_TRIGGER'])).rejects; + await expect(manager.createEvent(action, ['SELECT_RANGE_TRIGGER'])).rejects.toThrow(); }); }); }); @@ -704,4 +703,18 @@ describe('DynamicActionManager', () => { expect(basicAndGoldActions).toHaveLength(2); }); + + test("failing to revive/kill an action doesn't fail action manager", async () => { + const { manager, uiActions, storage } = setup([event1, event3, event2]); + + uiActions.registerActionFactory(actionFactoryDefinition1); + + await manager.start(); + + expect(uiActions.getTriggerActions('VALUE_CLICK_TRIGGER')).toHaveLength(2); + expect(await storage.list()).toEqual([event1, event3, event2]); + + await manager.stop(); + expect(uiActions.getTriggerActions('VALUE_CLICK_TRIGGER')).toHaveLength(0); + }); }); diff --git a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.ts b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.ts index 471b929fdbc068..b414296690c9e2 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.ts @@ -34,7 +34,13 @@ export interface DynamicActionManagerParams { storage: ActionStorage; uiActions: Pick< StartContract, - 'registerAction' | 'attachAction' | 'unregisterAction' | 'detachAction' | 'getActionFactory' + | 'registerAction' + | 'attachAction' + | 'unregisterAction' + | 'detachAction' + | 'hasAction' + | 'getActionFactory' + | 'hasActionFactory' >; isCompatible: (context: C) => Promise; } @@ -73,8 +79,17 @@ export class DynamicActionManager { const actionId = this.generateActionId(eventId); + if (!uiActions.hasActionFactory(action.factoryId)) { + // eslint-disable-next-line no-console + console.warn( + `Action factory for action [action.factoryId = ${action.factoryId}] doesn't exist. Skipping action [action.name = ${action.name}] revive.` + ); + return; + } + const factory = uiActions.getActionFactory(event.action.factoryId); const actionDefinition: ActionDefinition = factory.create(action as SerializedAction); + uiActions.registerAction({ ...actionDefinition, id: actionId, @@ -100,6 +115,7 @@ export class DynamicActionManager { protected killAction({ eventId, triggers }: SerializedEvent) { const { uiActions } = this.params; const actionId = this.generateActionId(eventId); + if (!uiActions.hasAction(actionId)) return; for (const trigger of triggers) uiActions.detachAction(trigger as any, actionId); uiActions.unregisterAction(actionId); @@ -157,6 +173,7 @@ export class DynamicActionManager { try { const events = await this.params.storage.list(); for (const event of events) this.reviveAction(event); + this.ui.transitions.finishFetching(events); } catch (error) { this.ui.transitions.failFetching(error instanceof Error ? error : { message: String(error) }); diff --git a/x-pack/plugins/ui_actions_enhanced/public/mocks.ts b/x-pack/plugins/ui_actions_enhanced/public/mocks.ts index 9eb0a06b6dbaf0..1900f04b0c7d8e 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/mocks.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/mocks.ts @@ -29,6 +29,7 @@ const createStartContract = (): Start => { ...uiActionsPluginMock.createStartContract(), getActionFactories: jest.fn(), getActionFactory: jest.fn(), + hasActionFactory: jest.fn(), FlyoutManageDrilldowns: jest.fn(), telemetry: jest.fn(), extract: jest.fn(), diff --git a/x-pack/plugins/ui_actions_enhanced/public/plugin.ts b/x-pack/plugins/ui_actions_enhanced/public/plugin.ts index b05c08c4c77d02..31236d2ea97792 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/plugin.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/plugin.ts @@ -61,7 +61,12 @@ export interface StartContract extends UiActionsStart, Pick< UiActionsServiceEnhancements, - 'getActionFactory' | 'getActionFactories' | 'telemetry' | 'extract' | 'inject' + | 'getActionFactory' + | 'hasActionFactory' + | 'getActionFactories' + | 'telemetry' + | 'extract' + | 'inject' > { FlyoutManageDrilldowns: ReturnType; } diff --git a/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.ts b/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.ts index 5e40d803962de2..cbbd88e65e8416 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.ts @@ -79,6 +79,10 @@ export class UiActionsServiceEnhancements return actionFactory; }; + public readonly hasActionFactory = (actionFactoryId: string): boolean => { + return this.actionFactories.has(actionFactoryId); + }; + /** * Returns an array of all action factories. */