Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Lens] Make open in discover drilldown work #131237

Merged
merged 17 commits into from
May 9, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,
drewdaemon marked this conversation as resolved.
Show resolved Hide resolved
})
);
}

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