Skip to content

Commit

Permalink
[Lens] Make open in discover drilldown work (#131237)
Browse files Browse the repository at this point in the history
* make open in discover drilldown work

* cleanup and tests

* fix test

* fix icon

* fix type

* fix open in new tab

* fix open in new tab

* fix test

* make it possible to filter out drilldowns from list based on context

* review comments

* remove isConfigurable from the actionfactory

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Stratoula Kalafateli <efstratia.kalafateli@elastic.co>
  • Loading branch information
3 people committed May 9, 2022
1 parent 58f480b commit a8017df
Show file tree
Hide file tree
Showing 12 changed files with 328 additions and 24 deletions.
1 change: 1 addition & 0 deletions x-pack/plugins/lens/kibana.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"visualizations",
"dashboard",
"uiActions",
"uiActionsEnhanced",
"embeddable",
"share",
"presentationUtil",
Expand Down
21 changes: 17 additions & 4 deletions x-pack/plugins/lens/public/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import { VISUALIZE_EDITOR_TRIGGER } from '@kbn/visualizations-plugin/public';
import { createStartServicesGetter } from '@kbn/kibana-utils-plugin/public';
import type { DiscoverSetup, DiscoverStart } from '@kbn/discover-plugin/public';
import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public';
import { AdvancedUiActionsSetup } from '@kbn/ui-actions-enhanced-plugin/public';
import type { EditorFrameService as EditorFrameServiceType } from './editor_frame_service';
import type {
IndexPatternDatasource as IndexPatternDatasourceType,
Expand Down Expand Up @@ -93,6 +94,7 @@ import type { SaveModalContainerProps } from './app_plugin/save_modal_container'

import { setupExpressions } from './expressions';
import { getSearchProvider } from './search_provider';
import { OpenInDiscoverDrilldown } from './trigger_actions/open_in_discover_drilldown';

export interface LensPluginSetupDependencies {
urlForwarding: UrlForwardingSetup;
Expand All @@ -106,6 +108,7 @@ export interface LensPluginSetupDependencies {
globalSearch?: GlobalSearchPluginSetup;
usageCollection?: UsageCollectionSetup;
discover?: DiscoverSetup;
uiActionsEnhanced: AdvancedUiActionsSetup;
}

export interface LensPluginStartDependencies {
Expand Down Expand Up @@ -224,6 +227,7 @@ export class LensPlugin {
private heatmapVisualization: HeatmapVisualizationType | undefined;
private gaugeVisualization: GaugeVisualizationType | undefined;
private topNavMenuEntries: LensTopNavMenuEntryGenerator[] = [];
private hasDiscoverAccess: boolean = false;

private stopReportManager?: () => void;

Expand All @@ -240,6 +244,8 @@ export class LensPlugin {
eventAnnotation,
globalSearch,
usageCollection,
uiActionsEnhanced,
discover,
}: LensPluginSetupDependencies
) {
const startServices = createStartServicesGetter(core.getStartServices);
Expand Down Expand Up @@ -285,6 +291,15 @@ export class LensPlugin {

visualizations.registerAlias(getLensAliasConfig());

if (discover) {
uiActionsEnhanced.registerDrilldown(
new OpenInDiscoverDrilldown({
discover,
hasDiscoverAccess: () => this.hasDiscoverAccess,
})
);
}

setupExpressions(
expressions,
() => startServices().plugins.fieldFormats.deserialize,
Expand Down Expand Up @@ -427,6 +442,7 @@ export class LensPlugin {
}

start(core: CoreStart, startDependencies: LensPluginStartDependencies): LensPublicStart {
this.hasDiscoverAccess = core.application.capabilities.discover.show as boolean;
// unregisters the Visualize action and registers the lens one
if (startDependencies.uiActions.hasAction(ACTION_VISUALIZE_FIELD)) {
startDependencies.uiActions.unregisterAction(ACTION_VISUALIZE_FIELD);
Expand All @@ -443,10 +459,7 @@ export class LensPlugin {

startDependencies.uiActions.addTriggerAction(
CONTEXT_MENU_TRIGGER,
createOpenInDiscoverAction(
startDependencies.discover!,
core.application.capabilities.discover.show as boolean
)
createOpenInDiscoverAction(startDependencies.discover!, this.hasDiscoverAccess)
);

return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ describe('open in discover action', () => {

const embeddable = {
getViewUnderlyingDataArgs: jest.fn(() => viewUnderlyingDataArgs),
type: 'lens',
};

const discoverUrl = 'https://discover-redirect-url';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,23 @@
* 2.0.
*/

import type { IEmbeddable } from '@kbn/embeddable-plugin/public';
import { i18n } from '@kbn/i18n';
import { createAction } from '@kbn/ui-actions-plugin/public';
import type { DiscoverStart } from '@kbn/discover-plugin/public';
import type { Embeddable } from '../embeddable';
import { DOC_TYPE } from '../../common';
import { IEmbeddable } from '@kbn/embeddable-plugin/public';
import { execute, isCompatible } from './open_in_discover_helpers';

const ACTION_OPEN_IN_DISCOVER = 'ACTION_OPEN_IN_DISCOVER';

export const createOpenInDiscoverAction = (discover: DiscoverStart, hasDiscoverAccess: boolean) =>
createAction<{ embeddable: IEmbeddable }>({
interface Context {
embeddable: IEmbeddable;
}

export const createOpenInDiscoverAction = (
discover: Pick<DiscoverStart, 'locator'>,
hasDiscoverAccess: boolean
) =>
createAction<Context>({
type: ACTION_OPEN_IN_DISCOVER,
id: ACTION_OPEN_IN_DISCOVER,
order: 19, // right after Inspect which is 20
Expand All @@ -24,18 +30,10 @@ export const createOpenInDiscoverAction = (discover: DiscoverStart, hasDiscoverA
i18n.translate('xpack.lens.app.exploreDataInDiscover', {
defaultMessage: 'Explore data in Discover',
}),
isCompatible: async (context: { embeddable: IEmbeddable }) => {
if (!hasDiscoverAccess) return false;
return (
context.embeddable.type === DOC_TYPE &&
(await (context.embeddable as Embeddable).canViewUnderlyingData())
);
isCompatible: async (context: Context) => {
return isCompatible({ hasDiscoverAccess, discover, embeddable: context.embeddable });
},
execute: async (context: { embeddable: Embeddable }) => {
const args = context.embeddable.getViewUnderlyingDataArgs()!;
const discoverUrl = discover.locator?.getRedirectUrl({
...args,
});
window.open(discoverUrl, '_blank');
execute: async (context: Context) => {
return execute({ ...context, discover, hasDiscoverAccess });
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import React, { FormEvent } from 'react';
import { IEmbeddable, EmbeddableInput } from '@kbn/embeddable-plugin/public';
import { DiscoverSetup } from '@kbn/discover-plugin/public';
import { execute, isCompatible } from './open_in_discover_helpers';
import { mount } from 'enzyme';
import { Filter } from '@kbn/es-query';
import {
ActionFactoryContext,
CollectConfigProps,
OpenInDiscoverDrilldown,
} from './open_in_discover_drilldown';

jest.mock('./open_in_discover_helpers', () => ({
isCompatible: jest.fn(() => true),
execute: jest.fn(),
}));

describe('open in discover drilldown', () => {
let drilldown: OpenInDiscoverDrilldown;
beforeEach(() => {
drilldown = new OpenInDiscoverDrilldown({
discover: {} as DiscoverSetup,
hasDiscoverAccess: () => true,
});
});
it('provides UI to edit config', () => {
const Component = (drilldown as unknown as { ReactCollectConfig: React.FC<CollectConfigProps> })
.ReactCollectConfig;
const setConfig = jest.fn();
const instance = mount(
<Component
config={{ openInNewTab: false }}
onConfig={setConfig}
context={{} as ActionFactoryContext}
/>
);
instance.find('EuiSwitch').prop('onChange')!({} as unknown as FormEvent<{}>);
expect(setConfig).toHaveBeenCalledWith({ openInNewTab: true });
});
it('calls through to isCompatible helper', () => {
const filters: Filter[] = [{ meta: { disabled: false } }];
drilldown.isCompatible(
{ openInNewTab: true },
{ embeddable: { type: 'lens' } as IEmbeddable<EmbeddableInput>, filters }
);
expect(isCompatible).toHaveBeenCalledWith(expect.objectContaining({ filters }));
});
it('calls through to execute helper', () => {
const filters: Filter[] = [{ meta: { disabled: false } }];
drilldown.execute(
{ openInNewTab: true },
{ embeddable: { type: 'lens' } as IEmbeddable<EmbeddableInput>, filters }
);
expect(execute).toHaveBeenCalledWith(
expect.objectContaining({ filters, openInSameTab: false })
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import React from 'react';
import { IEmbeddable, EmbeddableInput } from '@kbn/embeddable-plugin/public';
import {
Query,
Filter,
TimeRange,
extractTimeRange,
APPLY_FILTER_TRIGGER,
} from '@kbn/data-plugin/public';
import { CollectConfigProps as CollectConfigPropsBase } from '@kbn/kibana-utils-plugin/public';
import { reactToUiComponent } from '@kbn/kibana-react-plugin/public';
import {
UiActionsEnhancedDrilldownDefinition as Drilldown,
UiActionsEnhancedBaseActionFactoryContext as BaseActionFactoryContext,
} from '@kbn/ui-actions-enhanced-plugin/public';
import { EuiFormRow, EuiSwitch } from '@elastic/eui';
import { DiscoverSetup } from '@kbn/discover-plugin/public';
import { ApplyGlobalFilterActionContext } from '@kbn/unified-search-plugin/public';
import { i18n } from '@kbn/i18n';
import { execute, isCompatible, isLensEmbeddable } from './open_in_discover_helpers';

interface EmbeddableQueryInput extends EmbeddableInput {
query?: Query;
filters?: Filter[];
timeRange?: TimeRange;
}

/** @internal */
export type EmbeddableWithQueryInput = IEmbeddable<EmbeddableQueryInput>;

interface UrlDrilldownDeps {
discover: Pick<DiscoverSetup, 'locator'>;
hasDiscoverAccess: () => boolean;
}

export type ActionContext = ApplyGlobalFilterActionContext;

// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
export type Config = {
openInNewTab: boolean;
};

export type OpenInDiscoverTrigger = typeof APPLY_FILTER_TRIGGER;

export interface ActionFactoryContext extends BaseActionFactoryContext {
embeddable?: EmbeddableWithQueryInput;
}
export type CollectConfigProps = CollectConfigPropsBase<Config, ActionFactoryContext>;

const OPEN_IN_DISCOVER_DRILLDOWN = 'OPEN_IN_DISCOVER_DRILLDOWN';

export class OpenInDiscoverDrilldown
implements Drilldown<Config, ActionContext, ActionFactoryContext>
{
public readonly id = OPEN_IN_DISCOVER_DRILLDOWN;

constructor(private readonly deps: UrlDrilldownDeps) {}

public readonly order = 8;

public readonly getDisplayName = () =>
i18n.translate('xpack.lens.app.exploreDataInDiscoverDrilldown', {
defaultMessage: 'Open in Discover',
});

public readonly euiIcon = 'discoverApp';

supportedTriggers(): OpenInDiscoverTrigger[] {
return [APPLY_FILTER_TRIGGER];
}

private readonly ReactCollectConfig: React.FC<CollectConfigProps> = ({
config,
onConfig,
context,
}) => {
return (
<EuiFormRow hasChildLabel={false}>
<EuiSwitch
id="openInNewTab"
name="openInNewTab"
label={i18n.translate('xpack.lens.app.exploreDataInDiscoverDrilldown.newTabConfig', {
defaultMessage: 'Open in new tab',
})}
checked={config.openInNewTab}
onChange={() => onConfig({ ...config, openInNewTab: !config.openInNewTab })}
data-test-subj="openInDiscoverDrilldownOpenInNewTab"
/>
</EuiFormRow>
);
};

public readonly CollectConfig = reactToUiComponent(this.ReactCollectConfig);

public readonly createConfig = () => ({
openInNewTab: true,
});

public readonly isConfigValid = (config: Config): config is Config => {
return true;
};

public readonly isCompatible = async (config: Config, context: ActionContext) => {
return isCompatible({
discover: this.deps.discover,
hasDiscoverAccess: this.deps.hasDiscoverAccess(),
...context,
embeddable: context.embeddable as IEmbeddable,
...config,
});
};

public readonly isConfigurable = (context: ActionFactoryContext) => {
return this.deps.hasDiscoverAccess() && isLensEmbeddable(context.embeddable as IEmbeddable);
};

public readonly execute = async (config: Config, context: ActionContext) => {
const { restOfFilters: filters, timeRange: timeRange } = extractTimeRange(
context.filters,
context.timeFieldName
);
execute({
discover: this.deps.discover,
hasDiscoverAccess: this.deps.hasDiscoverAccess(),
...context,
embeddable: context.embeddable as IEmbeddable,
openInSameTab: !config.openInNewTab,
filters,
timeRange,
});
};
}
Loading

0 comments on commit a8017df

Please sign in to comment.