From 00fb5dbc7e98b96b5205c5626340103a2654834b Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Thu, 14 Mar 2024 14:41:07 +0100 Subject: [PATCH 01/11] decoupling open-in-anomaly-explorer action --- x-pack/plugins/ml/public/ui_actions/index.ts | 1 - .../open_in_anomaly_explorer_action.tsx | 43 ++++++++++++------- 2 files changed, 27 insertions(+), 17 deletions(-) diff --git a/x-pack/plugins/ml/public/ui_actions/index.ts b/x-pack/plugins/ml/public/ui_actions/index.ts index 46f07ed516e10..bdbd5f3f3e858 100644 --- a/x-pack/plugins/ml/public/ui_actions/index.ts +++ b/x-pack/plugins/ml/public/ui_actions/index.ts @@ -54,7 +54,6 @@ export function registerMlUiActions( // Register actions uiActions.registerAction(editSwimlanePanelAction); - uiActions.registerAction(openInExplorerAction); uiActions.registerAction(applyInfluencerFiltersAction); uiActions.registerAction(applyEntityFieldFilterAction); uiActions.registerAction(applyTimeRangeSelectionAction); diff --git a/x-pack/plugins/ml/public/ui_actions/open_in_anomaly_explorer_action.tsx b/x-pack/plugins/ml/public/ui_actions/open_in_anomaly_explorer_action.tsx index e29f1ce5cf06c..867da3cdb5fd4 100644 --- a/x-pack/plugins/ml/public/ui_actions/open_in_anomaly_explorer_action.tsx +++ b/x-pack/plugins/ml/public/ui_actions/open_in_anomaly_explorer_action.tsx @@ -6,29 +6,41 @@ */ import { i18n } from '@kbn/i18n'; -import type { SerializableRecord } from '@kbn/utility-types'; -import type { UiActionsActionDefinition } from '@kbn/ui-actions-plugin/public'; import { ML_ENTITY_FIELD_OPERATIONS } from '@kbn/ml-anomaly-utils'; -import type { MlCoreSetup } from '../plugin'; +import type { EmbeddableApiContext, HasParentApi, HasType } from '@kbn/presentation-publishing'; +import { apiIsOfType } from '@kbn/presentation-publishing'; +import { apiHasType } from '@kbn/presentation-publishing'; +import { createAction } from '@kbn/ui-actions-plugin/public'; +import type { SerializableRecord } from '@kbn/utility-types'; import { ML_APP_LOCATOR } from '../../common/constants/locator'; -import type { AnomalyChartsFieldSelectionContext, SwimLaneDrilldownContext } from '../embeddables'; +import type { ExplorerAppState } from '../../common/types/locator'; +import type { + AnomalyExplorerChartsEmbeddableType, + AnomalySwimLaneEmbeddableType, +} from '../embeddables'; import { ANOMALY_EXPLORER_CHARTS_EMBEDDABLE_TYPE, ANOMALY_SWIMLANE_EMBEDDABLE_TYPE, isAnomalyExplorerEmbeddable, isSwimLaneEmbeddable, } from '../embeddables'; -import type { ExplorerAppState } from '../../common/types/locator'; +import type { MlCoreSetup } from '../plugin'; + +export type OpenInAnomalyExplorerActionApi = HasType< + AnomalySwimLaneEmbeddableType | AnomalyExplorerChartsEmbeddableType +> & + Partial>; export const OPEN_IN_ANOMALY_EXPLORER_ACTION = 'openInAnomalyExplorerAction'; -export function createOpenInExplorerAction( - getStartServices: MlCoreSetup['getStartServices'] -): UiActionsActionDefinition { - return { +export const isApiCompatible = (api: unknown | null): api is OpenInAnomalyExplorerActionApi => + Boolean(apiHasType(api)); + +export function createOpenInExplorerAction(getStartServices: MlCoreSetup['getStartServices']) { + return createAction({ id: 'open-in-anomaly-explorer', type: OPEN_IN_ANOMALY_EXPLORER_ACTION, - getIconType(context): string { + getIconType(): string { return 'visTable'; }, getDisplayName() { @@ -122,13 +134,12 @@ export function createOpenInExplorerAction( await application.navigateToUrl(anomalyExplorerUrl!); } }, - async isCompatible({ - embeddable, - }: SwimLaneDrilldownContext | AnomalyChartsFieldSelectionContext) { + async isCompatible({ embeddable }: EmbeddableApiContext) { + if (!isApiCompatible(embeddable)) return false; return ( - embeddable.type === ANOMALY_SWIMLANE_EMBEDDABLE_TYPE || - embeddable.type === ANOMALY_EXPLORER_CHARTS_EMBEDDABLE_TYPE + apiIsOfType(embeddable, ANOMALY_SWIMLANE_EMBEDDABLE_TYPE) || + apiIsOfType(embeddable, ANOMALY_EXPLORER_CHARTS_EMBEDDABLE_TYPE) ); }, - }; + }); } From 5f0c122dc2cc3adf236c91c72761dab5338a2ded Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Thu, 14 Mar 2024 21:28:29 +0100 Subject: [PATCH 02/11] update api --- x-pack/plugins/ml/public/embeddables/types.ts | 54 ++-------- .../open_in_anomaly_explorer_action.tsx | 98 +++++++++++++++---- 2 files changed, 87 insertions(+), 65 deletions(-) diff --git a/x-pack/plugins/ml/public/embeddables/types.ts b/x-pack/plugins/ml/public/embeddables/types.ts index 961fe0b78799a..af4da4daab6db 100644 --- a/x-pack/plugins/ml/public/embeddables/types.ts +++ b/x-pack/plugins/ml/public/embeddables/types.ts @@ -6,34 +6,28 @@ */ import type { CoreStart } from '@kbn/core/public'; -import type { Filter, Query, TimeRange } from '@kbn/es-query'; import type { RefreshInterval } from '@kbn/data-plugin/common'; -import type { EmbeddableInput, EmbeddableOutput, IEmbeddable } from '@kbn/embeddable-plugin/public'; import type { DataView } from '@kbn/data-views-plugin/common'; -import { isPopulatedObject } from '@kbn/ml-is-populated-object'; +import type { EmbeddableInput, EmbeddableOutput, IEmbeddable } from '@kbn/embeddable-plugin/public'; +import type { Filter, Query, TimeRange } from '@kbn/es-query'; import type { MlEntityField } from '@kbn/ml-anomaly-utils'; import type { JobId } from '../../common/types/anomaly_detection_jobs'; +import type { MlDependencies } from '../application/app'; +import type { MlCapabilitiesService } from '../application/capabilities/check_capabilities'; import type { SwimlaneType } from '../application/explorer/explorer_constants'; import type { AnomalyDetectorService } from '../application/services/anomaly_detector_service'; -import type { AnomalyTimelineService } from '../application/services/anomaly_timeline_service'; -import type { MlDependencies } from '../application/app'; -import type { AppStateSelectedCells } from '../application/explorer/explorer_utils'; import type { AnomalyExplorerChartsService } from '../application/services/anomaly_explorer_charts_service'; +import type { AnomalyTimelineService } from '../application/services/anomaly_timeline_service'; +import type { MlFieldFormatService } from '../application/services/field_format_service'; import type { MlJobService } from '../application/services/job_service'; +import type { MlApiServices } from '../application/services/ml_api_service'; +import type { MlResultsService } from '../application/services/results_service'; +import type { MlTimeSeriesSearchService } from '../application/timeseriesexplorer/timeseriesexplorer_utils/time_series_search_service'; import type { AnomalyExplorerChartsEmbeddableType, AnomalySwimLaneEmbeddableType, MlEmbeddableTypes, } from './constants'; -import { - ANOMALY_EXPLORER_CHARTS_EMBEDDABLE_TYPE, - ANOMALY_SWIMLANE_EMBEDDABLE_TYPE, -} from './constants'; -import type { MlResultsService } from '../application/services/results_service'; -import type { MlApiServices } from '../application/services/ml_api_service'; -import type { MlFieldFormatService } from '../application/services/field_format_service'; -import type { MlTimeSeriesSearchService } from '../application/timeseriesexplorer/timeseriesexplorer_utils/time_series_search_service'; -import type { MlCapabilitiesService } from '../application/capabilities/check_capabilities'; export interface AnomalySwimlaneEmbeddableCustomInput { jobIds: JobId[]; @@ -75,21 +69,6 @@ export interface EditSwimlanePanelContext { embeddable: IEmbeddable; } -export interface SwimLaneDrilldownContext extends EditSwimlanePanelContext { - /** - * Optional data provided by swim lane selection - */ - data?: AppStateSelectedCells; -} - -export function isSwimLaneEmbeddable(arg: unknown): arg is SwimLaneDrilldownContext { - return ( - isPopulatedObject(arg, ['embeddable']) && - isPopulatedObject(arg.embeddable, ['type']) && - arg.embeddable.type === ANOMALY_SWIMLANE_EMBEDDABLE_TYPE - ); -} - /** * Anomaly Explorer */ @@ -158,21 +137,6 @@ export type AnomalyChartsEmbeddableOutput = EmbeddableOutput & AnomalyChartsCust export interface EditAnomalyChartsPanelContext { embeddable: IEmbeddable; } -export interface AnomalyChartsFieldSelectionContext extends EditAnomalyChartsPanelContext { - /** - * Optional fields selected using anomaly charts - */ - data?: MlEntityField[]; -} -export function isAnomalyExplorerEmbeddable( - arg: unknown -): arg is AnomalyChartsFieldSelectionContext { - return ( - isPopulatedObject(arg, ['embeddable']) && - isPopulatedObject(arg.embeddable, ['type']) && - arg.embeddable.type === ANOMALY_EXPLORER_CHARTS_EMBEDDABLE_TYPE - ); -} export type MappedEmbeddableTypeOf = TEmbeddableType extends AnomalySwimLaneEmbeddableType diff --git a/x-pack/plugins/ml/public/ui_actions/open_in_anomaly_explorer_action.tsx b/x-pack/plugins/ml/public/ui_actions/open_in_anomaly_explorer_action.tsx index 867da3cdb5fd4..18e4d3a966c58 100644 --- a/x-pack/plugins/ml/public/ui_actions/open_in_anomaly_explorer_action.tsx +++ b/x-pack/plugins/ml/public/ui_actions/open_in_anomaly_explorer_action.tsx @@ -6,14 +6,21 @@ */ import { i18n } from '@kbn/i18n'; +import type { MlEntityField } from '@kbn/ml-anomaly-utils'; import { ML_ENTITY_FIELD_OPERATIONS } from '@kbn/ml-anomaly-utils'; -import type { EmbeddableApiContext, HasParentApi, HasType } from '@kbn/presentation-publishing'; -import { apiIsOfType } from '@kbn/presentation-publishing'; -import { apiHasType } from '@kbn/presentation-publishing'; +import { isPopulatedObject } from '@kbn/ml-is-populated-object'; +import type { + EmbeddableApiContext, + HasParentApi, + HasType, + PublishesLocalUnifiedSearch, +} from '@kbn/presentation-publishing'; +import { apiHasType, apiIsOfType } from '@kbn/presentation-publishing'; import { createAction } from '@kbn/ui-actions-plugin/public'; import type { SerializableRecord } from '@kbn/utility-types'; import { ML_APP_LOCATOR } from '../../common/constants/locator'; import type { ExplorerAppState } from '../../common/types/locator'; +import type { AppStateSelectedCells } from '../application/explorer/explorer_utils'; import type { AnomalyExplorerChartsEmbeddableType, AnomalySwimLaneEmbeddableType, @@ -21,19 +28,73 @@ import type { import { ANOMALY_EXPLORER_CHARTS_EMBEDDABLE_TYPE, ANOMALY_SWIMLANE_EMBEDDABLE_TYPE, - isAnomalyExplorerEmbeddable, - isSwimLaneEmbeddable, } from '../embeddables'; import type { MlCoreSetup } from '../plugin'; +import type { JobId } from '../shared'; + +export interface AnomalyChartsFieldSelectionApi { + // Props from embeddable output + entityFields: MlEntityField[]; +} + +export interface SwimLaneDrilldownApi { + // Props from embeddable input + viewBy: string; + // Props from embeddable output + perPage: number; + fromPage: number; +} + +export interface OpenInAnomalyExplorerSwimLaneActionContext extends EmbeddableApiContext { + embeddable: OpenInAnomalyExplorerFromSwimLaneActionApi; + /** + * Optional data provided by swim lane selection + */ + data?: AppStateSelectedCells; +} + +export interface OpenInAnomalyExplorerAnomalyChartsActionContext extends EmbeddableApiContext { + embeddable: OpenInAnomalyExplorerFromAnomalyChartActionApi; + /** + * Optional fields selected using anomaly charts + */ + data?: MlEntityField[]; +} + +export type OpenInAnomalyExplorerBaseActionApi = Partial< + HasParentApi & PublishesLocalUnifiedSearch & { jobIds: JobId[] } +>; -export type OpenInAnomalyExplorerActionApi = HasType< - AnomalySwimLaneEmbeddableType | AnomalyExplorerChartsEmbeddableType -> & - Partial>; +export type OpenInAnomalyExplorerFromSwimLaneActionApi = HasType & + OpenInAnomalyExplorerBaseActionApi & + SwimLaneDrilldownApi; + +export type OpenInAnomalyExplorerFromAnomalyChartActionApi = + HasType & + OpenInAnomalyExplorerBaseActionApi & + AnomalyChartsFieldSelectionApi; export const OPEN_IN_ANOMALY_EXPLORER_ACTION = 'openInAnomalyExplorerAction'; -export const isApiCompatible = (api: unknown | null): api is OpenInAnomalyExplorerActionApi => +export function isSwimLaneEmbeddableContext( + arg: unknown +): arg is OpenInAnomalyExplorerSwimLaneActionContext { + return ( + isPopulatedObject(arg, ['embeddable']) && + apiIsOfType(arg.embeddable, ANOMALY_SWIMLANE_EMBEDDABLE_TYPE) + ); +} + +export function isAnomalyChartsEmbeddableContext( + arg: unknown +): arg is OpenInAnomalyExplorerAnomalyChartsActionContext { + return ( + isPopulatedObject(arg, ['embeddable']) && + apiIsOfType(arg.embeddable, ANOMALY_EXPLORER_CHARTS_EMBEDDABLE_TYPE) + ); +} + +export const isApiCompatible = (api: unknown | null): api is OpenInAnomalyExplorerBaseActionApi => Boolean(apiHasType(api)); export function createOpenInExplorerAction(getStartServices: MlCoreSetup['getStartServices']) { @@ -52,17 +113,16 @@ export function createOpenInExplorerAction(getStartServices: MlCoreSetup['getSta const [, pluginsStart] = await getStartServices(); const locator = pluginsStart.share.url.locators.get(ML_APP_LOCATOR)!; - if (isSwimLaneEmbeddable(context)) { - const { embeddable, data } = context; + if (isSwimLaneEmbeddableContext(context)) { + const { data, embeddable } = context; - const { jobIds, timeRange, viewBy } = embeddable.getInput(); - const { perPage, fromPage } = embeddable.getOutput(); + const { localTimeRange, viewBy, jobIds, perPage, fromPage } = embeddable; return locator.getUrl({ page: 'explorer', pageState: { jobIds, - timeRange, + timeRange: localTimeRange?.getValue(), mlExplorerSwimlane: { viewByFromPage: fromPage, viewByPerPage: perPage, @@ -77,11 +137,9 @@ export function createOpenInExplorerAction(getStartServices: MlCoreSetup['getSta }, }, }); - } else if (isAnomalyExplorerEmbeddable(context)) { + } else if (isAnomalyChartsEmbeddableContext(context)) { const { embeddable } = context; - - const { jobIds, timeRange } = embeddable.getInput(); - const { entityFields } = embeddable.getOutput(); + const { jobIds, localTimeRange, entityFields } = embeddable; let mlExplorerFilter: ExplorerAppState['mlExplorerFilter'] | undefined; if ( @@ -116,7 +174,7 @@ export function createOpenInExplorerAction(getStartServices: MlCoreSetup['getSta page: 'explorer', pageState: { jobIds, - timeRange, + timeRange: localTimeRange?.getValue(), // @ts-ignore QueryDslQueryContainer is not compatible with SerializableRecord ...(mlExplorerFilter ? ({ mlExplorerFilter } as SerializableRecord) : {}), query: {}, From f2886b1911facd7e3a25c71981c5d65f41b7f349 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Thu, 14 Mar 2024 22:16:40 +0100 Subject: [PATCH 03/11] publishing subjects --- .../anomaly_swimlane_embeddable.tsx | 38 ++++++++++++++----- .../common/anomaly_detection_embeddable.ts | 5 +++ .../open_in_anomaly_explorer_action.tsx | 13 ++++--- 3 files changed, 41 insertions(+), 15 deletions(-) diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable.tsx index 21892e8c5bae2..7fcb7cc0ccf83 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable.tsx +++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable.tsx @@ -5,25 +5,25 @@ * 2.0. */ -import React, { Suspense } from 'react'; -import ReactDOM from 'react-dom'; import type { CoreStart } from '@kbn/core/public'; +import type { IContainer } from '@kbn/embeddable-plugin/public'; import { i18n } from '@kbn/i18n'; -import { Subject } from 'rxjs'; import { KibanaContextProvider, KibanaThemeProvider } from '@kbn/kibana-react-plugin/public'; -import type { IContainer } from '@kbn/embeddable-plugin/public'; -import { EmbeddableSwimLaneContainer } from './embeddable_swim_lane_container_lazy'; -import type { JobId } from '../../../common/types/anomaly_detection_jobs'; -import type { MlDependencies } from '../../application/app'; -import { SWIM_LANE_SELECTION_TRIGGER } from '../../ui_actions'; +import React, { Suspense } from 'react'; +import ReactDOM from 'react-dom'; +import { BehaviorSubject, Subject, Subscription } from 'rxjs'; import type { AnomalySwimlaneEmbeddableInput, AnomalySwimlaneEmbeddableOutput, AnomalySwimlaneServices, } from '..'; import { ANOMALY_SWIMLANE_EMBEDDABLE_TYPE } from '..'; -import { EmbeddableLoading } from '../common/components/embeddable_loading_fallback'; +import type { JobId } from '../../../common/types/anomaly_detection_jobs'; +import type { MlDependencies } from '../../application/app'; +import { SWIM_LANE_SELECTION_TRIGGER } from '../../ui_actions'; import { AnomalyDetectionEmbeddable } from '../common/anomaly_detection_embeddable'; +import { EmbeddableLoading } from '../common/components/embeddable_loading_fallback'; +import { EmbeddableSwimLaneContainer } from './embeddable_swim_lane_container_lazy'; export const getDefaultSwimlanePanelTitle = (jobIds: JobId[]) => i18n.translate('xpack.ml.swimlaneEmbeddable.title', { @@ -41,12 +41,31 @@ export class AnomalySwimlaneEmbeddable extends AnomalyDetectionEmbeddable< private reload$ = new Subject(); public readonly type: string = ANOMALY_SWIMLANE_EMBEDDABLE_TYPE; + // API + public viewBy = new BehaviorSubject(this.getInput().viewBy); + public perPage = new BehaviorSubject(this.getOutput().perPage); + public fromPage = new BehaviorSubject(this.getOutput().fromPage); + + private apiSubscriptions = new Subscription(); + constructor( initialInput: AnomalySwimlaneEmbeddableInput, public services: [CoreStart, MlDependencies, AnomalySwimlaneServices], parent?: IContainer ) { super(initialInput, services[2].anomalyDetectorService, services[1].data.dataViews, parent); + + this.apiSubscriptions.add( + this.getInput$().subscribe((input) => { + this.viewBy.next(input.viewBy); + }) + ); + this.apiSubscriptions.add( + this.getOutput$().subscribe((output) => { + this.perPage?.next(output.perPage); + this.fromPage?.next(output.fromPage); + }) + ); } public reportsEmbeddableLoad() { @@ -104,6 +123,7 @@ export class AnomalySwimlaneEmbeddable extends AnomalyDetectionEmbeddable< } public destroy() { + this.apiSubscriptions.unsubscribe(); super.destroy(); if (this.node) { ReactDOM.unmountComponentAtNode(this.node); diff --git a/x-pack/plugins/ml/public/embeddables/common/anomaly_detection_embeddable.ts b/x-pack/plugins/ml/public/embeddables/common/anomaly_detection_embeddable.ts index 0b24a5b47b18c..08c00273c44a0 100644 --- a/x-pack/plugins/ml/public/embeddables/common/anomaly_detection_embeddable.ts +++ b/x-pack/plugins/ml/public/embeddables/common/anomaly_detection_embeddable.ts @@ -15,6 +15,7 @@ import { type DataView } from '@kbn/data-views-plugin/common'; import { type DataViewsContract } from '@kbn/data-views-plugin/public'; import { firstValueFrom } from 'rxjs'; import { type AnomalyDetectorService } from '../../application/services/anomaly_detector_service'; +import type { JobId } from '../../shared'; export type CommonInput = { jobIds: string[] } & EmbeddableInput; @@ -27,6 +28,8 @@ export abstract class AnomalyDetectionEmbeddable< // Need to defer embeddable load in order to resolve data views deferEmbeddableLoad = true; + public jobIds: JobId[] = []; + protected constructor( initialInput: Input, private anomalyDetectorService: AnomalyDetectorService, @@ -43,6 +46,8 @@ export abstract class AnomalyDetectionEmbeddable< protected async initializeOutput(initialInput: CommonInput) { const { jobIds } = initialInput; + this.jobIds = jobIds; + try { const jobs = await firstValueFrom(this.anomalyDetectorService.getJobs$(jobIds)); diff --git a/x-pack/plugins/ml/public/ui_actions/open_in_anomaly_explorer_action.tsx b/x-pack/plugins/ml/public/ui_actions/open_in_anomaly_explorer_action.tsx index 18e4d3a966c58..aaad0c4782410 100644 --- a/x-pack/plugins/ml/public/ui_actions/open_in_anomaly_explorer_action.tsx +++ b/x-pack/plugins/ml/public/ui_actions/open_in_anomaly_explorer_action.tsx @@ -14,6 +14,7 @@ import type { HasParentApi, HasType, PublishesLocalUnifiedSearch, + PublishingSubject, } from '@kbn/presentation-publishing'; import { apiHasType, apiIsOfType } from '@kbn/presentation-publishing'; import { createAction } from '@kbn/ui-actions-plugin/public'; @@ -39,10 +40,10 @@ export interface AnomalyChartsFieldSelectionApi { export interface SwimLaneDrilldownApi { // Props from embeddable input - viewBy: string; + viewBy: PublishingSubject; // Props from embeddable output - perPage: number; - fromPage: number; + perPage: PublishingSubject; + fromPage: PublishingSubject; } export interface OpenInAnomalyExplorerSwimLaneActionContext extends EmbeddableApiContext { @@ -124,9 +125,9 @@ export function createOpenInExplorerAction(getStartServices: MlCoreSetup['getSta jobIds, timeRange: localTimeRange?.getValue(), mlExplorerSwimlane: { - viewByFromPage: fromPage, - viewByPerPage: perPage, - viewByFieldName: viewBy, + viewByFromPage: fromPage.getValue(), + viewByPerPage: perPage.getValue(), + viewByFieldName: viewBy.getValue(), ...(data ? { selectedType: data.type, From 0673c4776cc423acd8460e62d3d01340331498cf Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Thu, 14 Mar 2024 22:23:07 +0100 Subject: [PATCH 04/11] publishing subject for anomaly charts --- .../anomaly_charts_embeddable.tsx | 19 ++++++++++++++++++- .../open_in_anomaly_explorer_action.tsx | 11 ++++++----- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_embeddable.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_embeddable.tsx index e2524ee015200..67845ada311c5 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_embeddable.tsx +++ b/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_embeddable.tsx @@ -9,9 +9,10 @@ import React, { Suspense } from 'react'; import ReactDOM from 'react-dom'; import type { CoreStart } from '@kbn/core/public'; import { i18n } from '@kbn/i18n'; -import { Subject } from 'rxjs'; +import { BehaviorSubject, Subject, Subscription } from 'rxjs'; import { KibanaContextProvider, KibanaThemeProvider } from '@kbn/kibana-react-plugin/public'; import type { IContainer } from '@kbn/embeddable-plugin/public'; +import type { MlEntityField } from '@kbn/ml-anomaly-utils'; import { EmbeddableAnomalyChartsContainer } from './embeddable_anomaly_charts_container_lazy'; import type { JobId } from '../../../common/types/anomaly_detection_jobs'; import type { MlDependencies } from '../../application/app'; @@ -40,12 +41,25 @@ export class AnomalyChartsEmbeddable extends AnomalyDetectionEmbeddable< private reload$ = new Subject(); public readonly type: string = ANOMALY_EXPLORER_CHARTS_EMBEDDABLE_TYPE; + // API + public entityFields = new BehaviorSubject( + this.getOutput().entityFields + ); + + private apiSubscriptions = new Subscription(); + constructor( initialInput: AnomalyChartsEmbeddableInput, public services: [CoreStart, MlDependencies, AnomalyChartsServices], parent?: IContainer ) { super(initialInput, services[2].anomalyDetectorService, services[1].data.dataViews, parent); + + this.apiSubscriptions.add( + this.getOutput$().subscribe((output) => { + this.entityFields.next(output.entityFields); + }) + ); } public onLoading() { @@ -108,6 +122,9 @@ export class AnomalyChartsEmbeddable extends AnomalyDetectionEmbeddable< public destroy() { super.destroy(); + + this.apiSubscriptions.unsubscribe(); + if (this.node) { ReactDOM.unmountComponentAtNode(this.node); } diff --git a/x-pack/plugins/ml/public/ui_actions/open_in_anomaly_explorer_action.tsx b/x-pack/plugins/ml/public/ui_actions/open_in_anomaly_explorer_action.tsx index aaad0c4782410..492a5f4daac46 100644 --- a/x-pack/plugins/ml/public/ui_actions/open_in_anomaly_explorer_action.tsx +++ b/x-pack/plugins/ml/public/ui_actions/open_in_anomaly_explorer_action.tsx @@ -35,7 +35,7 @@ import type { JobId } from '../shared'; export interface AnomalyChartsFieldSelectionApi { // Props from embeddable output - entityFields: MlEntityField[]; + entityFields: PublishingSubject; } export interface SwimLaneDrilldownApi { @@ -143,12 +143,13 @@ export function createOpenInExplorerAction(getStartServices: MlCoreSetup['getSta const { jobIds, localTimeRange, entityFields } = embeddable; let mlExplorerFilter: ExplorerAppState['mlExplorerFilter'] | undefined; + const entityFieldsValue = entityFields.getValue(); if ( - Array.isArray(entityFields) && - entityFields.length === 1 && - entityFields[0].operation === ML_ENTITY_FIELD_OPERATIONS.ADD + Array.isArray(entityFieldsValue) && + entityFieldsValue.length === 1 && + entityFieldsValue[0].operation === ML_ENTITY_FIELD_OPERATIONS.ADD ) { - const { fieldName, fieldValue } = entityFields[0]; + const { fieldName, fieldValue } = entityFieldsValue[0]; if (fieldName !== undefined && fieldValue !== undefined) { const influencersFilterQuery = { bool: { From 4211c14b0ad753e7a92d392613b58789f9860e3f Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Mon, 18 Mar 2024 11:55:16 +0100 Subject: [PATCH 05/11] get time range --- .../open_in_anomaly_explorer_action.tsx | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/ml/public/ui_actions/open_in_anomaly_explorer_action.tsx b/x-pack/plugins/ml/public/ui_actions/open_in_anomaly_explorer_action.tsx index 79c74252ae5ea..29f731bb210f4 100644 --- a/x-pack/plugins/ml/public/ui_actions/open_in_anomaly_explorer_action.tsx +++ b/x-pack/plugins/ml/public/ui_actions/open_in_anomaly_explorer_action.tsx @@ -5,6 +5,7 @@ * 2.0. */ +import type { TimeRange } from '@kbn/es-query'; import { i18n } from '@kbn/i18n'; import type { MlEntityField } from '@kbn/ml-anomaly-utils'; import { ML_ENTITY_FIELD_OPERATIONS } from '@kbn/ml-anomaly-utils'; @@ -63,7 +64,7 @@ export interface OpenInAnomalyExplorerAnomalyChartsActionContext extends Embedda } export type OpenInAnomalyExplorerBaseActionApi = Partial< - HasParentApi & PublishesUnifiedSearch & { jobIds: JobId[] } + HasParentApi & PublishesUnifiedSearch & { jobIds: JobId[] } >; export type OpenInAnomalyExplorerFromSwimLaneActionApi = HasType & @@ -98,6 +99,10 @@ export function isAnomalyChartsEmbeddableContext( export const isApiCompatible = (api: unknown | null): api is OpenInAnomalyExplorerBaseActionApi => Boolean(apiHasType(api)); +const getTimeRange = (embeddable: OpenInAnomalyExplorerBaseActionApi): TimeRange | undefined => { + return embeddable.timeRange$?.getValue() ?? embeddable.parentApi?.timeRange$?.getValue(); +}; + export function createOpenInExplorerAction(getStartServices: MlCoreSetup['getStartServices']) { return createAction({ id: 'open-in-anomaly-explorer', @@ -117,13 +122,13 @@ export function createOpenInExplorerAction(getStartServices: MlCoreSetup['getSta if (isSwimLaneEmbeddableContext(context)) { const { data, embeddable } = context; - const { timeRange$, viewBy, jobIds, perPage, fromPage } = embeddable; + const { viewBy, jobIds, perPage, fromPage } = embeddable; return locator.getUrl({ page: 'explorer', pageState: { jobIds, - timeRange: timeRange$?.getValue(), + timeRange: getTimeRange(embeddable), mlExplorerSwimlane: { viewByFromPage: fromPage.getValue(), viewByPerPage: perPage.getValue(), @@ -140,7 +145,7 @@ export function createOpenInExplorerAction(getStartServices: MlCoreSetup['getSta }); } else if (isAnomalyChartsEmbeddableContext(context)) { const { embeddable } = context; - const { jobIds, timeRange$, entityFields } = embeddable; + const { jobIds, entityFields } = embeddable; let mlExplorerFilter: ExplorerAppState['mlExplorerFilter'] | undefined; const entityFieldsValue = entityFields.getValue(); @@ -176,7 +181,7 @@ export function createOpenInExplorerAction(getStartServices: MlCoreSetup['getSta page: 'explorer', pageState: { jobIds, - timeRange: timeRange$?.getValue(), + timeRange: getTimeRange(embeddable), // @ts-ignore QueryDslQueryContainer is not compatible with SerializableRecord ...(mlExplorerFilter ? ({ mlExplorerFilter } as SerializableRecord) : {}), query: {}, From fd352ad92c3b3d63a1004fbb7a81ea69fa3c0ccf Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Mon, 18 Mar 2024 12:45:45 +0100 Subject: [PATCH 06/11] use embeddable input and output utils --- src/plugins/embeddable/public/index.ts | 5 ++++ .../embeddable_compatibility_utils.ts | 25 +++++++++++-------- .../anomaly_swimlane_embeddable.tsx | 25 ++++++++++++------- x-pack/plugins/ml/public/embeddables/types.ts | 2 +- 4 files changed, 37 insertions(+), 20 deletions(-) diff --git a/src/plugins/embeddable/public/index.ts b/src/plugins/embeddable/public/index.ts index 3214e18aeff26..52e408e8ebcd6 100644 --- a/src/plugins/embeddable/public/index.ts +++ b/src/plugins/embeddable/public/index.ts @@ -112,3 +112,8 @@ export { registerSavedObjectToPanelMethod } from './registry/saved_object_to_pan export function plugin(initializerContext: PluginInitializerContext) { return new EmbeddablePublicPlugin(initializerContext); } + +export { + embeddableInputToSubject, + embeddableOutputToSubject, +} from './lib/embeddables/compatibility/embeddable_compatibility_utils'; diff --git a/src/plugins/embeddable/public/lib/embeddables/compatibility/embeddable_compatibility_utils.ts b/src/plugins/embeddable/public/lib/embeddables/compatibility/embeddable_compatibility_utils.ts index fb4ce7e7bfa03..611654173b09f 100644 --- a/src/plugins/embeddable/public/lib/embeddables/compatibility/embeddable_compatibility_utils.ts +++ b/src/plugins/embeddable/public/lib/embeddables/compatibility/embeddable_compatibility_utils.ts @@ -15,6 +15,7 @@ import { map, Subscription, } from 'rxjs'; +import { IEmbeddable } from '../..'; import { Container } from '../../containers'; import { ViewMode as LegacyViewMode } from '../../types'; import { @@ -23,10 +24,13 @@ import { CommonLegacyOutput, } from './legacy_embeddable_to_api'; -export const embeddableInputToSubject = ( +export const embeddableInputToSubject = < + T extends unknown = unknown, + LegacyInput extends CommonLegacyInput = CommonLegacyInput +>( subscription: Subscription, - embeddable: CommonLegacyEmbeddable, - key: keyof CommonLegacyInput, + embeddable: IEmbeddable, + key: keyof LegacyInput, useExplicitInput = false ) => { const subject = new BehaviorSubject(embeddable.getExplicitInput()?.[key] as T); @@ -36,12 +40,10 @@ export const embeddableInputToSubject = ( .getInput$() .pipe( distinctUntilChanged((prev, current) => { - const previousValue = (prev.panels[embeddable.id]?.explicitInput as CommonLegacyInput)[ + const previousValue = (prev.panels[embeddable.id]?.explicitInput as LegacyInput)[key]; + const currentValue = (current.panels[embeddable.id]?.explicitInput as LegacyInput)?.[ key ]; - const currentValue = ( - current.panels[embeddable.id]?.explicitInput as CommonLegacyInput - )?.[key]; return deepEqual(previousValue, currentValue); }) ) @@ -58,10 +60,13 @@ export const embeddableInputToSubject = ( return subject; }; -export const embeddableOutputToSubject = ( +export const embeddableOutputToSubject = < + T extends unknown = unknown, + LegacyOutput extends CommonLegacyOutput = CommonLegacyOutput +>( subscription: Subscription, - embeddable: CommonLegacyEmbeddable, - key: keyof CommonLegacyOutput + embeddable: IEmbeddable, + key: keyof LegacyOutput ) => { const subject = new BehaviorSubject(embeddable.getOutput()[key] as T); subscription.add( diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable.tsx index 7fcb7cc0ccf83..1257a5eaa78d7 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable.tsx +++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable.tsx @@ -7,6 +7,7 @@ import type { CoreStart } from '@kbn/core/public'; import type { IContainer } from '@kbn/embeddable-plugin/public'; +import { embeddableInputToSubject, embeddableOutputToSubject } from '@kbn/embeddable-plugin/public'; import { i18n } from '@kbn/i18n'; import { KibanaContextProvider, KibanaThemeProvider } from '@kbn/kibana-react-plugin/public'; import React, { Suspense } from 'react'; @@ -55,16 +56,22 @@ export class AnomalySwimlaneEmbeddable extends AnomalyDetectionEmbeddable< ) { super(initialInput, services[2].anomalyDetectorService, services[1].data.dataViews, parent); - this.apiSubscriptions.add( - this.getInput$().subscribe((input) => { - this.viewBy.next(input.viewBy); - }) + this.viewBy = embeddableInputToSubject( + this.apiSubscriptions, + this, + 'viewBy' ); - this.apiSubscriptions.add( - this.getOutput$().subscribe((output) => { - this.perPage?.next(output.perPage); - this.fromPage?.next(output.fromPage); - }) + + this.perPage = embeddableOutputToSubject( + this.apiSubscriptions, + this, + 'perPage' + ); + + this.fromPage = embeddableOutputToSubject( + this.apiSubscriptions, + this, + 'fromPage' ); } diff --git a/x-pack/plugins/ml/public/embeddables/types.ts b/x-pack/plugins/ml/public/embeddables/types.ts index af4da4daab6db..ddba403398dee 100644 --- a/x-pack/plugins/ml/public/embeddables/types.ts +++ b/x-pack/plugins/ml/public/embeddables/types.ts @@ -59,7 +59,7 @@ export interface AnomalySwimlaneEmbeddableCustomOutput { perPage?: number; fromPage?: number; interval?: number; - indexPatterns?: DataView[]; + indexPatterns: DataView[]; } export type AnomalySwimlaneEmbeddableOutput = EmbeddableOutput & From b2130b3cb242bfea2564328c6678133b101312f0 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Mon, 18 Mar 2024 13:19:49 +0100 Subject: [PATCH 07/11] fix ts issue, revert types --- x-pack/plugins/ml/public/embeddables/types.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/x-pack/plugins/ml/public/embeddables/types.ts b/x-pack/plugins/ml/public/embeddables/types.ts index ddba403398dee..d61e2c3c46b78 100644 --- a/x-pack/plugins/ml/public/embeddables/types.ts +++ b/x-pack/plugins/ml/public/embeddables/types.ts @@ -15,6 +15,7 @@ import type { JobId } from '../../common/types/anomaly_detection_jobs'; import type { MlDependencies } from '../application/app'; import type { MlCapabilitiesService } from '../application/capabilities/check_capabilities'; import type { SwimlaneType } from '../application/explorer/explorer_constants'; +import type { AppStateSelectedCells } from '../application/explorer/explorer_utils'; import type { AnomalyDetectorService } from '../application/services/anomaly_detector_service'; import type { AnomalyExplorerChartsService } from '../application/services/anomaly_explorer_charts_service'; import type { AnomalyTimelineService } from '../application/services/anomaly_timeline_service'; @@ -69,6 +70,13 @@ export interface EditSwimlanePanelContext { embeddable: IEmbeddable; } +export interface SwimLaneDrilldownContext extends EditSwimlanePanelContext { + /** + * Optional data provided by swim lane selection + */ + data?: AppStateSelectedCells; +} + /** * Anomaly Explorer */ @@ -138,6 +146,13 @@ export interface EditAnomalyChartsPanelContext { embeddable: IEmbeddable; } +export interface AnomalyChartsFieldSelectionContext extends EditAnomalyChartsPanelContext { + /** + * Optional fields selected using anomaly charts + */ + data?: MlEntityField[]; +} + export type MappedEmbeddableTypeOf = TEmbeddableType extends AnomalySwimLaneEmbeddableType ? AnomalySwimlaneEmbeddableInput From d4debca417ad190c407135df4c619c102944c9ee Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Tue, 19 Mar 2024 09:32:19 +0100 Subject: [PATCH 08/11] use embeddableOutputToSubject for chart embeddable --- .../anomaly_charts/anomaly_charts_embeddable.tsx | 15 +++++++-------- x-pack/plugins/ml/public/embeddables/types.ts | 2 +- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_embeddable.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_embeddable.tsx index 67845ada311c5..1b8e77514e108 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_embeddable.tsx +++ b/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_embeddable.tsx @@ -9,9 +9,10 @@ import React, { Suspense } from 'react'; import ReactDOM from 'react-dom'; import type { CoreStart } from '@kbn/core/public'; import { i18n } from '@kbn/i18n'; -import { BehaviorSubject, Subject, Subscription } from 'rxjs'; +import { Subject, Subscription, type BehaviorSubject } from 'rxjs'; import { KibanaContextProvider, KibanaThemeProvider } from '@kbn/kibana-react-plugin/public'; import type { IContainer } from '@kbn/embeddable-plugin/public'; +import { embeddableOutputToSubject } from '@kbn/embeddable-plugin/public'; import type { MlEntityField } from '@kbn/ml-anomaly-utils'; import { EmbeddableAnomalyChartsContainer } from './embeddable_anomaly_charts_container_lazy'; import type { JobId } from '../../../common/types/anomaly_detection_jobs'; @@ -42,9 +43,7 @@ export class AnomalyChartsEmbeddable extends AnomalyDetectionEmbeddable< public readonly type: string = ANOMALY_EXPLORER_CHARTS_EMBEDDABLE_TYPE; // API - public entityFields = new BehaviorSubject( - this.getOutput().entityFields - ); + public entityFields: BehaviorSubject; private apiSubscriptions = new Subscription(); @@ -55,10 +54,10 @@ export class AnomalyChartsEmbeddable extends AnomalyDetectionEmbeddable< ) { super(initialInput, services[2].anomalyDetectorService, services[1].data.dataViews, parent); - this.apiSubscriptions.add( - this.getOutput$().subscribe((output) => { - this.entityFields.next(output.entityFields); - }) + this.entityFields = embeddableOutputToSubject( + this.apiSubscriptions, + this, + 'entityFields' ); } diff --git a/x-pack/plugins/ml/public/embeddables/types.ts b/x-pack/plugins/ml/public/embeddables/types.ts index d61e2c3c46b78..a0bd799352d4f 100644 --- a/x-pack/plugins/ml/public/embeddables/types.ts +++ b/x-pack/plugins/ml/public/embeddables/types.ts @@ -139,7 +139,7 @@ export type SingleMetricViewerEmbeddableServices = [ export interface AnomalyChartsCustomOutput { entityFields?: MlEntityField[]; severity?: number; - indexPatterns?: DataView[]; + indexPatterns: DataView[]; } export type AnomalyChartsEmbeddableOutput = EmbeddableOutput & AnomalyChartsCustomOutput; export interface EditAnomalyChartsPanelContext { From 48d9c509eb99d59ee5f78a3d4fb8cd093dbaca7b Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Tue, 19 Mar 2024 09:35:22 +0100 Subject: [PATCH 09/11] fix initializers for APIs --- .../anomaly_swimlane/anomaly_swimlane_embeddable.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable.tsx index 1257a5eaa78d7..968d505dc46ee 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable.tsx +++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable.tsx @@ -12,7 +12,7 @@ import { i18n } from '@kbn/i18n'; import { KibanaContextProvider, KibanaThemeProvider } from '@kbn/kibana-react-plugin/public'; import React, { Suspense } from 'react'; import ReactDOM from 'react-dom'; -import { BehaviorSubject, Subject, Subscription } from 'rxjs'; +import { Subject, Subscription, type BehaviorSubject } from 'rxjs'; import type { AnomalySwimlaneEmbeddableInput, AnomalySwimlaneEmbeddableOutput, @@ -43,9 +43,9 @@ export class AnomalySwimlaneEmbeddable extends AnomalyDetectionEmbeddable< public readonly type: string = ANOMALY_SWIMLANE_EMBEDDABLE_TYPE; // API - public viewBy = new BehaviorSubject(this.getInput().viewBy); - public perPage = new BehaviorSubject(this.getOutput().perPage); - public fromPage = new BehaviorSubject(this.getOutput().fromPage); + public viewBy: BehaviorSubject; + public perPage: BehaviorSubject; + public fromPage: BehaviorSubject; private apiSubscriptions = new Subscription(); From 22df822cd4f95d8a9f4eea8b2bf3233bf268045d Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Tue, 19 Mar 2024 09:35:58 +0100 Subject: [PATCH 10/11] cleanup comments --- .../ml/public/ui_actions/open_in_anomaly_explorer_action.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/x-pack/plugins/ml/public/ui_actions/open_in_anomaly_explorer_action.tsx b/x-pack/plugins/ml/public/ui_actions/open_in_anomaly_explorer_action.tsx index 29f731bb210f4..fe28db3493d7e 100644 --- a/x-pack/plugins/ml/public/ui_actions/open_in_anomaly_explorer_action.tsx +++ b/x-pack/plugins/ml/public/ui_actions/open_in_anomaly_explorer_action.tsx @@ -35,14 +35,11 @@ import type { MlCoreSetup } from '../plugin'; import type { JobId } from '../shared'; export interface AnomalyChartsFieldSelectionApi { - // Props from embeddable output entityFields: PublishingSubject; } export interface SwimLaneDrilldownApi { - // Props from embeddable input viewBy: PublishingSubject; - // Props from embeddable output perPage: PublishingSubject; fromPage: PublishingSubject; } From dfaef990f4e791fa1219c5f39640ee83bf99d6bb Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Tue, 19 Mar 2024 09:37:18 +0100 Subject: [PATCH 11/11] update isCompatible --- .../public/ui_actions/open_in_anomaly_explorer_action.tsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/ml/public/ui_actions/open_in_anomaly_explorer_action.tsx b/x-pack/plugins/ml/public/ui_actions/open_in_anomaly_explorer_action.tsx index fe28db3493d7e..e955245adec86 100644 --- a/x-pack/plugins/ml/public/ui_actions/open_in_anomaly_explorer_action.tsx +++ b/x-pack/plugins/ml/public/ui_actions/open_in_anomaly_explorer_action.tsx @@ -196,12 +196,8 @@ export function createOpenInExplorerAction(getStartServices: MlCoreSetup['getSta await application.navigateToUrl(anomalyExplorerUrl!); } }, - async isCompatible({ embeddable }: EmbeddableApiContext) { - if (!isApiCompatible(embeddable)) return false; - return ( - apiIsOfType(embeddable, ANOMALY_SWIMLANE_EMBEDDABLE_TYPE) || - apiIsOfType(embeddable, ANOMALY_EXPLORER_CHARTS_EMBEDDABLE_TYPE) - ); + async isCompatible(context: EmbeddableApiContext) { + return isSwimLaneEmbeddableContext(context) || isAnomalyChartsEmbeddableContext(context); }, }); }