Skip to content

Commit

Permalink
[ML] Decouple open-in-anomaly-explorer action (#178729)
Browse files Browse the repository at this point in the history
## Summary

Decouples open-in-anomaly-explorer UI action from embeddable framework.

- Modifies and exports helper utils from the embeddable plugin to
convert embeddable inputs and outputs to APIs
- Updates anomaly swim lane and anomaly charts embeddables to expose
required API for the "Open in Anomaly Explorer" action

Part of #178375
  • Loading branch information
darnautov authored Mar 19, 2024
1 parent e77dc09 commit a9968cb
Show file tree
Hide file tree
Showing 8 changed files with 197 additions and 92 deletions.
5 changes: 5 additions & 0 deletions src/plugins/embeddable/public/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
map,
Subscription,
} from 'rxjs';
import { IEmbeddable } from '../..';
import { Container } from '../../containers';
import { ViewMode as LegacyViewMode } from '../../types';
import {
Expand All @@ -23,10 +24,13 @@ import {
CommonLegacyOutput,
} from './legacy_embeddable_to_api';

export const embeddableInputToSubject = <T extends unknown = unknown>(
export const embeddableInputToSubject = <
T extends unknown = unknown,
LegacyInput extends CommonLegacyInput = CommonLegacyInput
>(
subscription: Subscription,
embeddable: CommonLegacyEmbeddable,
key: keyof CommonLegacyInput,
embeddable: IEmbeddable<LegacyInput>,
key: keyof LegacyInput,
useExplicitInput = false
) => {
const subject = new BehaviorSubject<T | undefined>(embeddable.getExplicitInput()?.[key] as T);
Expand All @@ -36,12 +40,10 @@ export const embeddableInputToSubject = <T extends unknown = unknown>(
.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);
})
)
Expand All @@ -58,10 +60,13 @@ export const embeddableInputToSubject = <T extends unknown = unknown>(
return subject;
};

export const embeddableOutputToSubject = <T extends unknown = unknown>(
export const embeddableOutputToSubject = <
T extends unknown = unknown,
LegacyOutput extends CommonLegacyOutput = CommonLegacyOutput
>(
subscription: Subscription,
embeddable: CommonLegacyEmbeddable,
key: keyof CommonLegacyOutput
embeddable: IEmbeddable<CommonLegacyInput, LegacyOutput>,
key: keyof LegacyOutput
) => {
const subject = new BehaviorSubject<T | undefined>(embeddable.getOutput()[key] as T);
subscription.add(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ 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 { 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';
import type { MlDependencies } from '../../application/app';
Expand Down Expand Up @@ -40,12 +42,23 @@ export class AnomalyChartsEmbeddable extends AnomalyDetectionEmbeddable<
private reload$ = new Subject<void>();
public readonly type: string = ANOMALY_EXPLORER_CHARTS_EMBEDDABLE_TYPE;

// API
public entityFields: BehaviorSubject<MlEntityField[] | undefined>;

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.entityFields = embeddableOutputToSubject<MlEntityField[], AnomalyChartsEmbeddableOutput>(
this.apiSubscriptions,
this,
'entityFields'
);
}

public onLoading() {
Expand Down Expand Up @@ -108,6 +121,9 @@ export class AnomalyChartsEmbeddable extends AnomalyDetectionEmbeddable<

public destroy() {
super.destroy();

this.apiSubscriptions.unsubscribe();

if (this.node) {
ReactDOM.unmountComponentAtNode(this.node);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,26 @@
* 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 { embeddableInputToSubject, embeddableOutputToSubject } 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 { Subject, Subscription, type BehaviorSubject } 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', {
Expand All @@ -41,12 +42,37 @@ export class AnomalySwimlaneEmbeddable extends AnomalyDetectionEmbeddable<
private reload$ = new Subject<void>();
public readonly type: string = ANOMALY_SWIMLANE_EMBEDDABLE_TYPE;

// API
public viewBy: BehaviorSubject<string | undefined>;
public perPage: BehaviorSubject<number | undefined>;
public fromPage: BehaviorSubject<number | undefined>;

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.viewBy = embeddableInputToSubject<string, AnomalySwimlaneEmbeddableInput>(
this.apiSubscriptions,
this,
'viewBy'
);

this.perPage = embeddableOutputToSubject<number, AnomalySwimlaneEmbeddableOutput>(
this.apiSubscriptions,
this,
'perPage'
);

this.fromPage = embeddableOutputToSubject<number, AnomalySwimlaneEmbeddableOutput>(
this.apiSubscriptions,
this,
'fromPage'
);
}

public reportsEmbeddableLoad() {
Expand Down Expand Up @@ -104,6 +130,7 @@ export class AnomalySwimlaneEmbeddable extends AnomalyDetectionEmbeddable<
}

public destroy() {
this.apiSubscriptions.unsubscribe();
super.destroy();
if (this.node) {
ReactDOM.unmountComponentAtNode(this.node);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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,
Expand All @@ -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));

Expand Down
47 changes: 13 additions & 34 deletions x-pack/plugins/ml/public/embeddables/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,34 +6,29 @@
*/

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 { 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 { 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';
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[];
Expand Down Expand Up @@ -65,7 +60,7 @@ export interface AnomalySwimlaneEmbeddableCustomOutput {
perPage?: number;
fromPage?: number;
interval?: number;
indexPatterns?: DataView[];
indexPatterns: DataView[];
}

export type AnomalySwimlaneEmbeddableOutput = EmbeddableOutput &
Expand All @@ -82,14 +77,6 @@ export interface SwimLaneDrilldownContext extends EditSwimlanePanelContext {
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
*/
Expand Down Expand Up @@ -152,27 +139,19 @@ export type SingleMetricViewerEmbeddableServices = [
export interface AnomalyChartsCustomOutput {
entityFields?: MlEntityField[];
severity?: number;
indexPatterns?: DataView[];
indexPatterns: DataView[];
}
export type AnomalyChartsEmbeddableOutput = EmbeddableOutput & AnomalyChartsCustomOutput;
export interface EditAnomalyChartsPanelContext {
embeddable: IEmbeddable<AnomalyChartsEmbeddableInput, AnomalyChartsEmbeddableOutput>;
}

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 MlEmbeddableTypes> =
TEmbeddableType extends AnomalySwimLaneEmbeddableType
Expand Down
1 change: 0 additions & 1 deletion x-pack/plugins/ml/public/ui_actions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ export function registerMlUiActions(

// Register actions
uiActions.registerAction(editSwimlanePanelAction);
uiActions.registerAction(openInExplorerAction);
uiActions.registerAction(applyInfluencerFiltersAction);
uiActions.registerAction(applyEntityFieldFilterAction);
uiActions.registerAction(applyTimeRangeSelectionAction);
Expand Down
Loading

0 comments on commit a9968cb

Please sign in to comment.