[Drilldowns] Config to disable URL Drilldown (#77887)

This pr makes sure there is way to disable URL drilldown feature.
I decided to extract Url drilldown definition into a separate plugin to benefit from regular disabling a plugin feature.
Having it as a separate plugin also makes sense because we will start adding registries specific to URL drilldown implementation

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Anton Dosov 2020-09-23 11:12:12 +02:00 committed by GitHub
parent 0f8043ca8d
commit 4b6d77fa5d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 151 additions and 36 deletions

View file

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

View file

@ -238,3 +238,14 @@ Tip: Consider using <<helpers, date>> 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
-----------

View file

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

View file

@ -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 = <T extends TriggerId>(triggerId: T, actionId: string): void => {
const trigger = this.triggers.get(triggerId);

View file

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

View file

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

View file

@ -0,0 +1,8 @@
{
"id": "urlDrilldown",
"version": "kibana",
"server": false,
"ui": true,
"requiredPlugins": ["embeddable", "uiActions", "uiActionsEnhanced"],
"requiredBundles": ["kibanaUtils", "kibanaReact"]
}

View file

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

View file

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

View file

@ -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<SetupContract, StartContract, SetupDependencies, StartDependencies> {
constructor(protected readonly context: PluginInitializerContext) {}
public setup(core: CoreSetup<StartDependencies>, 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() {}
}

View file

@ -3,6 +3,5 @@
"version": "kibana",
"server": false,
"ui": true,
"requiredPlugins": ["embeddable", "kibanaReact", "uiActions", "uiActionsEnhanced"],
"requiredBundles": ["kibanaUtils"]
"requiredPlugins": ["embeddable", "kibanaReact", "uiActions", "uiActionsEnhanced"]
}

View file

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

View file

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

View file

@ -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: <C = unknown>(context: C) => Promise<boolean>;
}
@ -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) });

View file

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

View file

@ -61,7 +61,12 @@ export interface StartContract
extends UiActionsStart,
Pick<
UiActionsServiceEnhancements,
'getActionFactory' | 'getActionFactories' | 'telemetry' | 'extract' | 'inject'
| 'getActionFactory'
| 'hasActionFactory'
| 'getActionFactories'
| 'telemetry'
| 'extract'
| 'inject'
> {
FlyoutManageDrilldowns: ReturnType<typeof createFlyoutManageDrilldowns>;
}

View file

@ -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.
*/