Skip to content

Commit

Permalink
decouple dashboard drilldown from Embeddables framework (#176188)
Browse files Browse the repository at this point in the history
part of #175138

PR decouples FlyoutCreateDrilldownAction, FlyoutEditDrilldownAction, and
EmbeddableToDashboardDrilldown from Embeddable framework.

---------

Co-authored-by: Kibana Machine <[email protected]>
  • Loading branch information
nreese and kibanamachine authored Feb 12, 2024
1 parent bc3346f commit 788745e
Show file tree
Hide file tree
Showing 24 changed files with 463 additions and 437 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,18 @@ export type PresentationContainer = Partial<PublishesViewMode> &
removePanel: (panelId: string) => void;
canRemovePanels?: () => boolean;
replacePanel: (idToRemove: string, newPanel: PanelPackage) => Promise<string>;
getChildIds: () => string[];
getChild: (childId: string) => unknown;
};

export const apiIsPresentationContainer = (
unknownApi: unknown | null
): unknownApi is PresentationContainer => {
export const apiIsPresentationContainer = (api: unknown | null): api is PresentationContainer => {
return Boolean(
(unknownApi as PresentationContainer)?.removePanel !== undefined &&
(unknownApi as PresentationContainer)?.registerPanelApi !== undefined &&
(unknownApi as PresentationContainer)?.replacePanel !== undefined &&
(unknownApi as PresentationContainer)?.addNewPanel !== undefined
typeof (api as PresentationContainer)?.removePanel === 'function' &&
typeof (api as PresentationContainer)?.registerPanelApi === 'function' &&
typeof (api as PresentationContainer)?.replacePanel === 'function' &&
typeof (api as PresentationContainer)?.addNewPanel === 'function' &&
typeof (api as PresentationContainer)?.getChildIds === 'function' &&
typeof (api as PresentationContainer)?.getChild === 'function'
);
};

Expand Down
2 changes: 2 additions & 0 deletions packages/presentation/presentation_containers/mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,7 @@ export const getMockPresentationContainer = (): PresentationContainer => {
registerPanelApi: jest.fn(),
lastSavedState: new Subject<void>(),
getLastSavedStateForChild: jest.fn(),
getChildIds: jest.fn(),
getChild: jest.fn(),
};
};
4 changes: 4 additions & 0 deletions packages/presentation/presentation_publishing/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,10 @@ export {
} from './interfaces/publishes_saved_object_id';
export { apiHasUniqueId, type HasUniqueId } from './interfaces/has_uuid';
export { apiHasDisableTriggers, type HasDisableTriggers } from './interfaces/has_disable_triggers';
export {
apiHasSupportedTriggers,
type HasSupportedTriggers,
} from './interfaces/has_supported_triggers';
export {
apiPublishesViewMode,
apiPublishesWritableViewMode,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

export interface HasSupportedTriggers {
supportedTriggers: () => string[];
}

export const apiHasSupportedTriggers = (api: unknown | null): api is HasSupportedTriggers => {
return Boolean(api && typeof (api as HasSupportedTriggers).supportedTriggers === 'function');
};
Original file line number Diff line number Diff line change
Expand Up @@ -6,41 +6,34 @@
* Side Public License, v 1.
*/

import { isQuery, isTimeRange } from '@kbn/data-plugin/common';
import { Filter, isFilterPinned, Query, TimeRange } from '@kbn/es-query';
import { EmbeddableInput, IEmbeddable } from '@kbn/embeddable-plugin/public';
import { DashboardDrilldownOptions } from '@kbn/presentation-util-plugin/public';
import { DashboardLocatorParams } from '../../dashboard_container';

interface EmbeddableQueryInput extends EmbeddableInput {
query?: Query;
filters?: Filter[];
timeRange?: TimeRange;
}
import { isFilterPinned, type Query } from '@kbn/es-query';
import type { HasParentApi, PublishesLocalUnifiedSearch } from '@kbn/presentation-publishing';
import type { DashboardDrilldownOptions } from '@kbn/presentation-util-plugin/public';
import type { DashboardLocatorParams } from '../../dashboard_container';

export const getDashboardLocatorParamsFromEmbeddable = (
source: IEmbeddable<EmbeddableQueryInput>,
api: Partial<PublishesLocalUnifiedSearch & HasParentApi<Partial<PublishesLocalUnifiedSearch>>>,
options: DashboardDrilldownOptions
): Partial<DashboardLocatorParams> => {
const params: DashboardLocatorParams = {};

const input = source.getInput();
if (isQuery(input.query) && options.useCurrentFilters) {
params.query = input.query;
const query = api.parentApi?.localQuery?.value;
if (query && options.useCurrentFilters) {
params.query = query as Query;
}

// if useCurrentDashboardDataRange is enabled, then preserve current time range
// if undefined is passed, then destination dashboard will figure out time range itself
// for brush event this time range would be overwritten
if (isTimeRange(input.timeRange) && options.useCurrentDateRange) {
params.timeRange = input.timeRange;
const timeRange = api.localTimeRange?.value ?? api.parentApi?.localTimeRange?.value;
if (timeRange && options.useCurrentDateRange) {
params.timeRange = timeRange;
}

// if useCurrentDashboardFilters enabled, then preserve all the filters (pinned, unpinned, and from controls)
// otherwise preserve only pinned
params.filters = options.useCurrentFilters
? input.filters
: input.filters?.filter((f) => isFilterPinned(f));
const filters = api.parentApi?.localFilters?.value ?? [];
params.filters = options.useCurrentFilters ? filters : filters?.filter((f) => isFilterPinned(f));

return params;
};
Original file line number Diff line number Diff line change
Expand Up @@ -428,7 +428,9 @@ test('creates a control group from the control group factory and waits for it to
untilInitialized: jest.fn(),
getInput: jest.fn().mockReturnValue({}),
getInput$: jest.fn().mockReturnValue(new Observable()),
getOutput: jest.fn().mockReturnValue({}),
getOutput$: jest.fn().mockReturnValue(new Observable()),
onFiltersPublished$: new Observable(),
unsavedChanges: new BehaviorSubject(undefined),
} as unknown as ControlGroupContainer;
const mockControlGroupFactory = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,13 @@ import {
reactEmbeddableRegistryHasKey,
ViewMode,
} from '@kbn/embeddable-plugin/public';
import { TimeRange } from '@kbn/es-query';
import { compareFilters, Filter, TimeRange } from '@kbn/es-query';
import { lazyLoadReduxToolsPackage } from '@kbn/presentation-util-plugin/public';
import { cloneDeep, identity, omit, pickBy } from 'lodash';
import { Subject } from 'rxjs';
import { BehaviorSubject, combineLatest, Subject } from 'rxjs';
import { map, distinctUntilChanged, startWith } from 'rxjs/operators';
import { v4 } from 'uuid';
import { combineDashboardFiltersWithControlGroupFilters } from './controls/dashboard_control_group_integration';
import { DashboardContainerInput, DashboardPanelState } from '../../../../common';
import {
DEFAULT_DASHBOARD_INPUT,
Expand Down Expand Up @@ -462,5 +464,44 @@ export const initializeDashboard = async ({
setTimeout(() => dashboard.dispatch.setAnimatePanelTransforms(true), 500)
);

// --------------------------------------------------------------------------------------
// Set parentApi.localFilters to include dashboardContainer filters and control group filters
// --------------------------------------------------------------------------------------
untilDashboardReady().then((dashboardContainer) => {
if (!dashboardContainer.controlGroup) {
return;
}

function getCombinedFilters() {
return combineDashboardFiltersWithControlGroupFilters(
dashboardContainer.getInput().filters ?? [],
dashboardContainer.controlGroup!
);
}

const localFilters = new BehaviorSubject<Filter[] | undefined>(getCombinedFilters());
dashboardContainer.localFilters = localFilters;

const inputFilters$ = dashboardContainer.getInput$().pipe(
startWith(dashboardContainer.getInput()),
map((input) => input.filters),
distinctUntilChanged((previous, current) => {
return compareFilters(previous ?? [], current ?? []);
})
);

// Can not use onFiltersPublished$ directly since it does not have an intial value and
// combineLatest will not emit until each observable emits at least one value
const controlGroupFilters$ = dashboardContainer.controlGroup.onFiltersPublished$.pipe(
startWith(dashboardContainer.controlGroup.getOutput().filters)
);

dashboardContainer.integrationSubscriptions.add(
combineLatest([inputFilters$, controlGroupFilters$]).subscribe(() => {
localFilters.next(getCombinedFilters());
})
);
});

return { input: initialDashboardInput, searchSessionId: initialSearchSessionId };
};
3 changes: 3 additions & 0 deletions src/plugins/embeddable/public/lib/triggers/triggers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ import { Trigger, RowClickContext } from '@kbn/ui-actions-plugin/public';
import { BooleanRelation } from '@kbn/es-query';
import { IEmbeddable } from '..';

/**
* @deprecated use `EmbeddableApiContext` from `@kbn/presentation-publishing`
*/
export interface EmbeddableContext<T extends IEmbeddable = IEmbeddable> {
embeddable: T;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
DashboardDrilldownOptions,
DEFAULT_DASHBOARD_DRILLDOWN_OPTIONS,
} from '@kbn/presentation-util-plugin/public';
import type { HasParentApi, PublishesLocalUnifiedSearch } from '@kbn/presentation-publishing';

import {
DASHBOARD_LINK_TYPE,
Expand Down Expand Up @@ -115,7 +116,12 @@ export const DashboardLinkComponent = ({

const params: DashboardLocatorParams = {
dashboardId: link.destination,
...getDashboardLocatorParamsFromEmbeddable(linksEmbeddable, linkOptions),
...getDashboardLocatorParamsFromEmbeddable(
linksEmbeddable as Partial<
PublishesLocalUnifiedSearch & HasParentApi<Partial<PublishesLocalUnifiedSearch>>
>,
linkOptions
),
};

const locator = dashboardContainer.locator;
Expand Down
3 changes: 2 additions & 1 deletion src/plugins/links/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@
"@kbn/usage-collection-plugin",
"@kbn/visualizations-plugin",
"@kbn/core-mount-utils-browser",
"@kbn/presentation-containers"
"@kbn/presentation-containers",
"@kbn/presentation-publishing"
],
"exclude": ["target/**/*"]
}
3 changes: 2 additions & 1 deletion x-pack/plugins/dashboard_enhanced/kibana.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
"kibanaReact",
"kibanaUtils",
"imageEmbeddable",
"presentationUtil"
"presentationUtil",
"uiActions"
]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,17 @@
*/

import { APPLY_FILTER_TRIGGER } from '@kbn/data-plugin/public';
import { SELECT_RANGE_TRIGGER, VALUE_CLICK_TRIGGER } from '@kbn/embeddable-plugin/public';
import {
SELECT_RANGE_TRIGGER,
VALUE_CLICK_TRIGGER,
IEmbeddable,
Container as EmbeddableContainer,
} from '@kbn/embeddable-plugin/public';
import { isEnhancedEmbeddable } from '@kbn/embeddable-enhanced-plugin/public';
apiIsPresentationContainer,
type PresentationContainer,
} from '@kbn/presentation-containers';
import {
PublishesPanelTitle,
type HasUniqueId,
type HasParentApi,
} from '@kbn/presentation-publishing';
import { apiHasDynamicActions } from '@kbn/embeddable-enhanced-plugin/public';
import { UiActionsEnhancedDrilldownTemplate as DrilldownTemplate } from '@kbn/ui-actions-enhanced-plugin/public';

/**
Expand All @@ -36,39 +40,30 @@ export function ensureNestedTriggers(triggers: string[]): string[] {
return triggers;
}

const isEmbeddableContainer = (x: unknown): x is EmbeddableContainer =>
x instanceof EmbeddableContainer;

/**
* Given a dashboard panel embeddable, it will find the parent (dashboard
* container embeddable), then iterate through all the dashboard panels and
* generate DrilldownTemplate for each existing drilldown.
*/
export const createDrilldownTemplatesFromSiblings = (
embeddable: IEmbeddable
embeddable: Partial<HasUniqueId> & HasParentApi<Partial<PresentationContainer>>
): DrilldownTemplate[] => {
const templates: DrilldownTemplate[] = [];
const embeddableId = embeddable.id;

const container = embeddable.getRoot();
const parentApi = embeddable.parentApi;
if (!apiIsPresentationContainer(parentApi)) return [];

if (!container) return templates;
if (!isEmbeddableContainer(container)) return templates;

const childrenIds = (container as EmbeddableContainer).getChildIds();

for (const childId of childrenIds) {
const child = (container as EmbeddableContainer).getChild(childId);
if (child.id === embeddableId) continue;
if (!isEnhancedEmbeddable(child)) continue;
const templates: DrilldownTemplate[] = [];
for (const childId of parentApi.getChildIds()) {
const child = parentApi.getChild(childId) as Partial<HasUniqueId & PublishesPanelTitle>;
if (childId === embeddable.uuid) continue;
if (!apiHasDynamicActions(child)) continue;
const events = child.enhancements.dynamicActions.state.get().events;

for (const event of events) {
const template: DrilldownTemplate = {
id: event.eventId,
name: event.action.name,
icon: 'dashboardApp',
description: child.getTitle() || child.id,
description: child.panelTitle?.value ?? child.uuid ?? '',
config: event.action.config,
factoryId: event.action.factoryId,
triggers: event.triggers,
Expand Down
Loading

0 comments on commit 788745e

Please sign in to comment.