From 917f8546f8a9b936a15f8614611a65ac382fde11 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Fri, 22 Mar 2024 11:37:47 +0100 Subject: [PATCH] [ML] Decouple edit-anomaly-swimlane UI action (#179073) ## Summary - Decouples edit-anomaly-swimlane and apply-time-range-selection UI actions from embeddable framework Part of #178375 Part of #174967 --- .../anomaly_charts_embeddable.tsx | 8 ++ .../embeddables/anomaly_charts/types.ts | 22 +++++ .../anomaly_swimlane_embeddable.tsx | 33 +++++++- .../anomaly_swimlane_embeddable_factory.ts | 7 +- .../anomaly_swimlane_initializer.tsx | 8 +- .../anomaly_swimlane_setup_flyout.tsx | 7 +- .../embeddables/anomaly_swimlane/types.ts | 48 +++++++++++ .../common/anomaly_detection_embeddable.ts | 10 +-- x-pack/plugins/ml/public/embeddables/types.ts | 26 +++++- .../apply_influencer_filters_action.tsx | 23 +++--- .../ui_actions/apply_time_range_action.tsx | 32 +++++--- .../ui_actions/edit_swimlane_panel_action.tsx | 44 ++++++---- x-pack/plugins/ml/public/ui_actions/index.ts | 32 ++++---- .../open_in_anomaly_explorer_action.tsx | 82 +++++-------------- .../plugins/ml/public/ui_actions/triggers.ts | 18 ++++ 15 files changed, 259 insertions(+), 141 deletions(-) create mode 100644 x-pack/plugins/ml/public/embeddables/anomaly_charts/types.ts create mode 100644 x-pack/plugins/ml/public/embeddables/anomaly_swimlane/types.ts 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 1b8e77514e1089..ded0f536ae96ae 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 @@ -12,6 +12,7 @@ import { i18n } from '@kbn/i18n'; 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 { embeddableInputToSubject } 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'; @@ -43,6 +44,7 @@ export class AnomalyChartsEmbeddable extends AnomalyDetectionEmbeddable< public readonly type: string = ANOMALY_EXPLORER_CHARTS_EMBEDDABLE_TYPE; // API + public readonly jobIds: BehaviorSubject; public entityFields: BehaviorSubject; private apiSubscriptions = new Subscription(); @@ -54,6 +56,12 @@ export class AnomalyChartsEmbeddable extends AnomalyDetectionEmbeddable< ) { super(initialInput, services[2].anomalyDetectorService, services[1].data.dataViews, parent); + this.jobIds = embeddableInputToSubject( + this.apiSubscriptions, + this, + 'jobIds' + ); + this.entityFields = embeddableOutputToSubject( this.apiSubscriptions, this, diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_charts/types.ts b/x-pack/plugins/ml/public/embeddables/anomaly_charts/types.ts new file mode 100644 index 00000000000000..198602b836c1a5 --- /dev/null +++ b/x-pack/plugins/ml/public/embeddables/anomaly_charts/types.ts @@ -0,0 +1,22 @@ +/* + * 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 type { MlEntityField } from '@kbn/ml-anomaly-utils'; +import type { HasType, PublishingSubject } from '@kbn/presentation-publishing'; +import type { JobId } from '../../shared'; +import type { AnomalyExplorerChartsEmbeddableType } from '../constants'; +import type { MlEmbeddableBaseApi } from '../types'; + +export interface AnomalyChartsFieldSelectionApi { + jobIds: PublishingSubject; + entityFields: PublishingSubject; +} + +export interface AnomalyChartsEmbeddableApi + extends HasType, + MlEmbeddableBaseApi, + AnomalyChartsFieldSelectionApi {} 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 968d505dc46eea..6e5a8a02ac35e6 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 @@ -16,11 +16,13 @@ import { Subject, Subscription, type BehaviorSubject } from 'rxjs'; import type { AnomalySwimlaneEmbeddableInput, AnomalySwimlaneEmbeddableOutput, + AnomalySwimlaneEmbeddableUserInput, AnomalySwimlaneServices, } from '..'; import { ANOMALY_SWIMLANE_EMBEDDABLE_TYPE } from '..'; import type { JobId } from '../../../common/types/anomaly_detection_jobs'; import type { MlDependencies } from '../../application/app'; +import type { SwimlaneType } from '../../application/explorer/explorer_constants'; import { SWIM_LANE_SELECTION_TRIGGER } from '../../ui_actions'; import { AnomalyDetectionEmbeddable } from '../common/anomaly_detection_embeddable'; import { EmbeddableLoading } from '../common/components/embeddable_loading_fallback'; @@ -43,9 +45,12 @@ export class AnomalySwimlaneEmbeddable extends AnomalyDetectionEmbeddable< public readonly type: string = ANOMALY_SWIMLANE_EMBEDDABLE_TYPE; // API - public viewBy: BehaviorSubject; - public perPage: BehaviorSubject; - public fromPage: BehaviorSubject; + public readonly jobIds: BehaviorSubject; + public readonly viewBy: BehaviorSubject; + public readonly swimlaneType: BehaviorSubject; + public readonly perPage: BehaviorSubject; + public readonly fromPage: BehaviorSubject; + public readonly interval: BehaviorSubject; private apiSubscriptions = new Subscription(); @@ -56,12 +61,24 @@ export class AnomalySwimlaneEmbeddable extends AnomalyDetectionEmbeddable< ) { super(initialInput, services[2].anomalyDetectorService, services[1].data.dataViews, parent); + this.jobIds = embeddableInputToSubject( + this.apiSubscriptions, + this, + 'jobIds' + ); + this.viewBy = embeddableInputToSubject( this.apiSubscriptions, this, 'viewBy' ); + this.swimlaneType = embeddableInputToSubject( + this.apiSubscriptions, + this, + 'swimlaneType' + ); + this.perPage = embeddableOutputToSubject( this.apiSubscriptions, this, @@ -73,6 +90,16 @@ export class AnomalySwimlaneEmbeddable extends AnomalyDetectionEmbeddable< this, 'fromPage' ); + + this.interval = embeddableOutputToSubject( + this.apiSubscriptions, + this, + 'interval' + ); + } + + public updateUserInput(update: AnomalySwimlaneEmbeddableUserInput) { + this.updateInput(update); } public reportsEmbeddableLoad() { diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.ts b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.ts index a9ab9c9f3424a5..ebab6f5356994e 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.ts +++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.ts @@ -56,7 +56,12 @@ export class AnomalySwimlaneEmbeddableFactory try { const { resolveAnomalySwimlaneUserInput } = await import('./anomaly_swimlane_setup_flyout'); - return await resolveAnomalySwimlaneUserInput(coreStart, deps.data.dataViews); + const userInput = await resolveAnomalySwimlaneUserInput(coreStart, deps.data.dataViews); + + return { + ...userInput, + title: userInput.panelTitle, + }; } catch (e) { return Promise.reject(); } diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_initializer.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_initializer.tsx index d6875a9ce7a0d1..92943f4286c12f 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_initializer.tsx +++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_initializer.tsx @@ -25,13 +25,9 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; import type { SwimlaneType } from '../../application/explorer/explorer_constants'; import { SWIMLANE_TYPE } from '../../application/explorer/explorer_constants'; -import type { AnomalySwimlaneEmbeddableInput } from '..'; +import type { AnomalySwimlaneEmbeddableInput, AnomalySwimlaneEmbeddableUserInput } from '..'; -interface ExplicitInput { - panelTitle: string; - swimlaneType: SwimlaneType; - viewBy?: string; -} +export type ExplicitInput = Omit; export interface AnomalySwimlaneInitializerProps { defaultTitle: string; diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_setup_flyout.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_setup_flyout.tsx index 34ba32cd4a127b..fa9185bb026a94 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_setup_flyout.tsx +++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_setup_flyout.tsx @@ -14,15 +14,15 @@ import { VIEW_BY_JOB_LABEL } from '../../application/explorer/explorer_constants import { AnomalySwimlaneInitializer } from './anomaly_swimlane_initializer'; import { getDefaultSwimlanePanelTitle } from './anomaly_swimlane_embeddable'; import { HttpService } from '../../application/services/http_service'; -import type { AnomalySwimlaneEmbeddableInput } from '..'; +import type { AnomalySwimlaneEmbeddableInput, AnomalySwimlaneEmbeddableUserInput } from '..'; import { resolveJobSelection } from '../common/resolve_job_selection'; import { mlApiServicesProvider } from '../../application/services/ml_api_service'; export async function resolveAnomalySwimlaneUserInput( coreStart: CoreStart, dataViews: DataViewsContract, - input?: AnomalySwimlaneEmbeddableInput -): Promise> { + input?: Partial +): Promise { const { http, overlays, theme, i18n } = coreStart; const { getJobs } = mlApiServicesProvider(new HttpService(http)); @@ -44,7 +44,6 @@ export async function resolveAnomalySwimlaneUserInput( modalSession.close(); resolve({ jobIds, - title: explicitInput.panelTitle, ...explicitInput, }); }} diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/types.ts b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/types.ts new file mode 100644 index 00000000000000..bb9548f78f61ab --- /dev/null +++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/types.ts @@ -0,0 +1,48 @@ +/* + * 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 type { + HasType, + PublishesWritablePanelTitle, + PublishingSubject, +} from '@kbn/presentation-publishing'; +import { apiIsOfType } from '@kbn/presentation-publishing'; +import { isPopulatedObject } from '@kbn/ml-is-populated-object'; +import type { SwimlaneType } from '../../application/explorer/explorer_constants'; +import type { JobId } from '../../shared'; +import type { AnomalySwimLaneEmbeddableType } from '../constants'; +import { ANOMALY_SWIMLANE_EMBEDDABLE_TYPE } from '../constants'; +import type { AnomalySwimlaneEmbeddableUserInput, MlEmbeddableBaseApi } from '../types'; +import type { AppStateSelectedCells } from '../../application/explorer/explorer_utils'; + +export interface AnomalySwimLaneComponentApi { + jobIds: PublishingSubject; + swimlaneType: PublishingSubject; + viewBy: PublishingSubject; + perPage: PublishingSubject; + fromPage: PublishingSubject; + interval: PublishingSubject; + updateUserInput: (input: AnomalySwimlaneEmbeddableUserInput) => void; +} + +export interface AnomalySwimLaneEmbeddableApi + extends HasType, + PublishesWritablePanelTitle, + MlEmbeddableBaseApi, + AnomalySwimLaneComponentApi {} + +export interface AnomalySwimLaneActionContext { + embeddable: AnomalySwimLaneEmbeddableApi; + data?: AppStateSelectedCells; +} + +export function isSwimLaneEmbeddableContext(arg: unknown): arg is AnomalySwimLaneActionContext { + return ( + isPopulatedObject(arg, ['embeddable']) && + apiIsOfType(arg.embeddable, ANOMALY_SWIMLANE_EMBEDDABLE_TYPE) + ); +} 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 08c00273c44a0b..84a03b05e869f7 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 @@ -5,14 +5,15 @@ * 2.0. */ +import { type DataView } from '@kbn/data-views-plugin/common'; +import { type DataViewsContract } from '@kbn/data-views-plugin/public'; import { Embeddable, type EmbeddableInput, type EmbeddableOutput, type IContainer, } from '@kbn/embeddable-plugin/public'; -import { type DataView } from '@kbn/data-views-plugin/common'; -import { type DataViewsContract } from '@kbn/data-views-plugin/public'; +import type { BehaviorSubject } from 'rxjs'; import { firstValueFrom } from 'rxjs'; import { type AnomalyDetectorService } from '../../application/services/anomaly_detector_service'; import type { JobId } from '../../shared'; @@ -28,7 +29,8 @@ export abstract class AnomalyDetectionEmbeddable< // Need to defer embeddable load in order to resolve data views deferEmbeddableLoad = true; - public jobIds: JobId[] = []; + // API + public abstract jobIds: BehaviorSubject; protected constructor( initialInput: Input, @@ -46,8 +48,6 @@ 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/embeddables/types.ts b/x-pack/plugins/ml/public/embeddables/types.ts index a0bd799352d4f4..14ee0e7c6d444a 100644 --- a/x-pack/plugins/ml/public/embeddables/types.ts +++ b/x-pack/plugins/ml/public/embeddables/types.ts @@ -11,6 +11,13 @@ import type { DataView } from '@kbn/data-views-plugin/common'; 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 { + EmbeddableApiContext, + HasParentApi, + HasType, + PublishesUnifiedSearch, + PublishesViewMode, +} from '@kbn/presentation-publishing'; import type { JobId } from '../../common/types/anomaly_detection_jobs'; import type { MlDependencies } from '../application/app'; import type { MlCapabilitiesService } from '../application/capabilities/check_capabilities'; @@ -30,6 +37,18 @@ import type { MlEmbeddableTypes, } from './constants'; +export type MlEmbeddableBaseApi = Partial< + HasParentApi & PublishesViewMode & PublishesUnifiedSearch +>; + +/** Manual input by the user */ +export interface AnomalySwimlaneEmbeddableUserInput { + jobIds: JobId[]; + panelTitle: string; + swimlaneType: SwimlaneType; + viewBy?: string; +} + export interface AnomalySwimlaneEmbeddableCustomInput { jobIds: JobId[]; swimlaneType: SwimlaneType; @@ -66,8 +85,11 @@ export interface AnomalySwimlaneEmbeddableCustomOutput { export type AnomalySwimlaneEmbeddableOutput = EmbeddableOutput & AnomalySwimlaneEmbeddableCustomOutput; -export interface EditSwimlanePanelContext { - embeddable: IEmbeddable; +export type EditSwimLaneActionApi = HasType & + Partial>; + +export interface EditSwimlanePanelContext extends EmbeddableApiContext { + embeddable: EditSwimLaneActionApi; } export interface SwimLaneDrilldownContext extends EditSwimlanePanelContext { diff --git a/x-pack/plugins/ml/public/ui_actions/apply_influencer_filters_action.tsx b/x-pack/plugins/ml/public/ui_actions/apply_influencer_filters_action.tsx index 96508aae89771d..b2d785bf6dab3c 100644 --- a/x-pack/plugins/ml/public/ui_actions/apply_influencer_filters_action.tsx +++ b/x-pack/plugins/ml/public/ui_actions/apply_influencer_filters_action.tsx @@ -5,16 +5,17 @@ * 2.0. */ +import { DASHBOARD_APP_ID } from '@kbn/dashboard-plugin/public'; import type { Filter } from '@kbn/es-query'; import { FilterStateStore } from '@kbn/es-query'; import { i18n } from '@kbn/i18n'; +import type { EmbeddableApiContext } from '@kbn/presentation-publishing'; import type { UiActionsActionDefinition } from '@kbn/ui-actions-plugin/public'; import { firstValueFrom } from 'rxjs'; -import { DASHBOARD_APP_ID } from '@kbn/dashboard-plugin/public'; -import type { MlCoreSetup } from '../plugin'; +import { isAnomalySwimlaneSelectionTriggerContext } from './triggers'; import { SWIMLANE_TYPE, VIEW_BY_JOB_LABEL } from '../application/explorer/explorer_constants'; import type { SwimLaneDrilldownContext } from '../embeddables'; -import { ANOMALY_SWIMLANE_EMBEDDABLE_TYPE } from '../embeddables'; +import type { MlCoreSetup } from '../plugin'; import { CONTROLLED_BY_SWIM_LANE_FILTER } from './constants'; export const APPLY_INFLUENCER_FILTERS_ACTION = 'applyInfluencerFiltersAction'; @@ -27,7 +28,7 @@ export function createApplyInfluencerFiltersAction( return { id: 'apply-to-current-view', type: APPLY_INFLUENCER_FILTERS_ACTION, - getIconType(context: SwimLaneDrilldownContext): string { + getIconType(): string { return 'filter'; }, getDisplayName() { @@ -73,18 +74,18 @@ export function createApplyInfluencerFiltersAction( }) ); }, - async isCompatible({ embeddable, data }) { + async isCompatible(context: EmbeddableApiContext) { const [{ application }] = await getStartServices(); const appId = await firstValueFrom(application.currentAppId$); // Only compatible with view by influencer swim lanes and single selection return ( - embeddable.type === ANOMALY_SWIMLANE_EMBEDDABLE_TYPE && - data !== undefined && - data.type === SWIMLANE_TYPE.VIEW_BY && - data.viewByFieldName !== VIEW_BY_JOB_LABEL && - data.lanes.length === 1 && - supportedApps.includes(appId!) + supportedApps.includes(appId!) && + isAnomalySwimlaneSelectionTriggerContext(context) && + context.data !== undefined && + context.data.type === SWIMLANE_TYPE.VIEW_BY && + context.data.viewByFieldName !== VIEW_BY_JOB_LABEL && + context.data.lanes.length === 1 ); }, }; diff --git a/x-pack/plugins/ml/public/ui_actions/apply_time_range_action.tsx b/x-pack/plugins/ml/public/ui_actions/apply_time_range_action.tsx index 2b6071db2388c7..30dc1b0d54c73a 100644 --- a/x-pack/plugins/ml/public/ui_actions/apply_time_range_action.tsx +++ b/x-pack/plugins/ml/public/ui_actions/apply_time_range_action.tsx @@ -5,22 +5,32 @@ * 2.0. */ +import { DASHBOARD_APP_ID } from '@kbn/dashboard-plugin/public'; import { i18n } from '@kbn/i18n'; -import moment from 'moment'; +import type { EmbeddableApiContext } from '@kbn/presentation-publishing'; import type { UiActionsActionDefinition } from '@kbn/ui-actions-plugin/public'; -import { DASHBOARD_APP_ID } from '@kbn/dashboard-plugin/public'; +import moment from 'moment'; import { firstValueFrom } from 'rxjs'; +import { isAnomalySwimlaneSelectionTriggerContext } from './triggers'; +import type { AppStateSelectedCells } from '../application/explorer/explorer_utils'; +import type { AnomalySwimLaneEmbeddableApi } from '../embeddables/anomaly_swimlane/types'; import type { MlCoreSetup } from '../plugin'; -import type { SwimLaneDrilldownContext } from '../embeddables'; -import { ANOMALY_SWIMLANE_EMBEDDABLE_TYPE } from '../embeddables'; export const APPLY_TIME_RANGE_SELECTION_ACTION = 'applyTimeRangeSelectionAction'; const supportedApps = [DASHBOARD_APP_ID]; +export interface ApplyTimeRangeSelectionActionContext extends EmbeddableApiContext { + embeddable: AnomalySwimLaneEmbeddableApi; + /** + * Optional data provided by swim lane selection + */ + data?: AppStateSelectedCells; +} + export function createApplyTimeRangeSelectionAction( getStartServices: MlCoreSetup['getStartServices'] -): UiActionsActionDefinition { +): UiActionsActionDefinition { return { id: 'apply-time-range-selection', type: APPLY_TIME_RANGE_SELECTION_ACTION, @@ -37,9 +47,9 @@ export function createApplyTimeRangeSelectionAction( } const [, pluginStart] = await getStartServices(); const timefilter = pluginStart.data.query.timefilter.timefilter; - const { interval } = embeddable.getOutput(); + const { interval } = embeddable; - if (!interval) { + if (!interval.getValue()) { throw new Error('Interval is required to set a time range'); } @@ -53,14 +63,10 @@ export function createApplyTimeRangeSelectionAction( mode: 'absolute', }); }, - async isCompatible({ embeddable, data }) { + async isCompatible(context) { const [{ application }] = await getStartServices(); const appId = await firstValueFrom(application.currentAppId$); - return ( - embeddable.type === ANOMALY_SWIMLANE_EMBEDDABLE_TYPE && - data !== undefined && - supportedApps.includes(appId!) - ); + return isAnomalySwimlaneSelectionTriggerContext(context) && supportedApps.includes(appId!); }, }; } diff --git a/x-pack/plugins/ml/public/ui_actions/edit_swimlane_panel_action.tsx b/x-pack/plugins/ml/public/ui_actions/edit_swimlane_panel_action.tsx index c2933014e9feb7..bdeea57f5e2981 100644 --- a/x-pack/plugins/ml/public/ui_actions/edit_swimlane_panel_action.tsx +++ b/x-pack/plugins/ml/public/ui_actions/edit_swimlane_panel_action.tsx @@ -6,30 +6,36 @@ */ import { i18n } from '@kbn/i18n'; +import type { EmbeddableApiContext } from '@kbn/presentation-publishing'; import type { UiActionsActionDefinition } from '@kbn/ui-actions-plugin/public'; -import { ViewMode } from '@kbn/embeddable-plugin/public'; +import { IncompatibleActionError } from '@kbn/ui-actions-plugin/public'; +import type { AnomalySwimLaneEmbeddableApi } from '../embeddables/anomaly_swimlane/types'; import type { MlCoreSetup } from '../plugin'; -import type { EditSwimlanePanelContext } from '../embeddables'; -import { ANOMALY_SWIMLANE_EMBEDDABLE_TYPE } from '../embeddables'; +import { isSwimLaneEmbeddableContext } from '../embeddables/anomaly_swimlane/types'; export const EDIT_SWIMLANE_PANEL_ACTION = 'editSwimlanePanelAction'; +export type EditSwimlanePanelActionContext = EmbeddableApiContext & { + embeddable: AnomalySwimLaneEmbeddableApi; +}; + export function createEditSwimlanePanelAction( getStartServices: MlCoreSetup['getStartServices'] -): UiActionsActionDefinition { +): UiActionsActionDefinition { return { id: 'edit-anomaly-swimlane', type: EDIT_SWIMLANE_PANEL_ACTION, - getIconType(context): string { + order: 50, + getIconType(): string { return 'pencil'; }, getDisplayName: () => i18n.translate('xpack.ml.actions.editSwimlaneTitle', { defaultMessage: 'Edit swim lane', }), - async execute({ embeddable }) { - if (!embeddable) { - throw new Error('Not possible to execute an action without the embeddable context'); + async execute(context) { + if (!isSwimLaneEmbeddableContext(context)) { + throw new IncompatibleActionError(); } const [coreStart, deps] = await getStartServices(); @@ -39,20 +45,24 @@ export function createEditSwimlanePanelAction( '../embeddables/anomaly_swimlane/anomaly_swimlane_setup_flyout' ); - const result = await resolveAnomalySwimlaneUserInput( - coreStart, - deps.data.dataViews, - embeddable.getInput() - ); - embeddable.updateInput(result); + const { jobIds, viewBy, swimlaneType, panelTitle } = context.embeddable; + + const result = await resolveAnomalySwimlaneUserInput(coreStart, deps.data.dataViews, { + jobIds: jobIds.getValue(), + swimlaneType: swimlaneType.getValue(), + viewBy: viewBy.getValue(), + title: panelTitle?.getValue(), + }); + + context.embeddable.updateUserInput(result); + context.embeddable.setPanelTitle(result.panelTitle); } catch (e) { return Promise.reject(); } }, - async isCompatible({ embeddable }) { + async isCompatible(context: EmbeddableApiContext) { return ( - embeddable.type === ANOMALY_SWIMLANE_EMBEDDABLE_TYPE && - embeddable.getInput().viewMode === ViewMode.EDIT + isSwimLaneEmbeddableContext(context) && context.embeddable.viewMode?.getValue() === 'edit' ); }, }; diff --git a/x-pack/plugins/ml/public/ui_actions/index.ts b/x-pack/plugins/ml/public/ui_actions/index.ts index bdbd5f3f3e8587..e55b1fc1c7b003 100644 --- a/x-pack/plugins/ml/public/ui_actions/index.ts +++ b/x-pack/plugins/ml/public/ui_actions/index.ts @@ -6,31 +6,31 @@ */ import type { CoreSetup } from '@kbn/core/public'; -import type { UiActionsSetup } from '@kbn/ui-actions-plugin/public'; import { CONTEXT_MENU_TRIGGER } from '@kbn/embeddable-plugin/public'; import { CREATE_PATTERN_ANALYSIS_TO_ML_AD_JOB_TRIGGER } from '@kbn/ml-ui-actions'; +import type { UiActionsSetup } from '@kbn/ui-actions-plugin/public'; +import type { MlPluginStart, MlStartDependencies } from '../plugin'; +import { createApplyEntityFieldFiltersAction } from './apply_entity_filters_action'; +import { createApplyInfluencerFiltersAction } from './apply_influencer_filters_action'; +import { createApplyTimeRangeSelectionAction } from './apply_time_range_action'; +import { createClearSelectionAction } from './clear_selection_action'; +import { createEditAnomalyChartsPanelAction } from './edit_anomaly_charts_panel_action'; import { createEditSwimlanePanelAction } from './edit_swimlane_panel_action'; -import { createOpenInExplorerAction } from './open_in_anomaly_explorer_action'; -import { createVisToADJobAction } from './open_vis_in_ml_action'; import { createCategorizationADJobAction, createCategorizationADJobTrigger, } from './open_create_categorization_job_action'; -import type { MlPluginStart, MlStartDependencies } from '../plugin'; -import { createApplyInfluencerFiltersAction } from './apply_influencer_filters_action'; +import { createOpenInExplorerAction } from './open_in_anomaly_explorer_action'; +import { createVisToADJobAction } from './open_vis_in_ml_action'; import { entityFieldSelectionTrigger, EXPLORER_ENTITY_FIELD_SELECTION_TRIGGER, - SWIM_LANE_SELECTION_TRIGGER, swimLaneSelectionTrigger, + SWIM_LANE_SELECTION_TRIGGER, } from './triggers'; -import { createApplyTimeRangeSelectionAction } from './apply_time_range_action'; -import { createClearSelectionAction } from './clear_selection_action'; -import { createEditAnomalyChartsPanelAction } from './edit_anomaly_charts_panel_action'; -import { createApplyEntityFieldFiltersAction } from './apply_entity_filters_action'; +export { APPLY_INFLUENCER_FILTERS_ACTION } from './apply_influencer_filters_action'; export { APPLY_TIME_RANGE_SELECTION_ACTION } from './apply_time_range_action'; export { EDIT_SWIMLANE_PANEL_ACTION } from './edit_swimlane_panel_action'; -export { APPLY_INFLUENCER_FILTERS_ACTION } from './apply_influencer_filters_action'; export { OPEN_IN_ANOMALY_EXPLORER_ACTION } from './open_in_anomaly_explorer_action'; export { CREATE_LENS_VIS_TO_ML_AD_JOB_ACTION } from './open_vis_in_ml_action'; export { SWIM_LANE_SELECTION_TRIGGER }; @@ -53,18 +53,14 @@ export function registerMlUiActions( const categorizationADJobAction = createCategorizationADJobAction(core.getStartServices); // Register actions - uiActions.registerAction(editSwimlanePanelAction); - uiActions.registerAction(applyInfluencerFiltersAction); uiActions.registerAction(applyEntityFieldFilterAction); uiActions.registerAction(applyTimeRangeSelectionAction); - uiActions.registerAction(clearSelectionAction); - uiActions.registerAction(editExplorerPanelAction); uiActions.registerAction(categorizationADJobAction); // Assign triggers - uiActions.attachAction(CONTEXT_MENU_TRIGGER, editSwimlanePanelAction.id); - uiActions.attachAction(CONTEXT_MENU_TRIGGER, editExplorerPanelAction.id); - uiActions.attachAction(CONTEXT_MENU_TRIGGER, openInExplorerAction.id); + uiActions.addTriggerAction(CONTEXT_MENU_TRIGGER, editSwimlanePanelAction); + uiActions.addTriggerAction(CONTEXT_MENU_TRIGGER, editExplorerPanelAction); + uiActions.addTriggerAction(CONTEXT_MENU_TRIGGER, openInExplorerAction); uiActions.registerTrigger(swimLaneSelectionTrigger); uiActions.registerTrigger(entityFieldSelectionTrigger); 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 e955245adec86a..d9a0d8dc060ba7 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 @@ -10,42 +10,22 @@ import { i18n } from '@kbn/i18n'; import type { MlEntityField } from '@kbn/ml-anomaly-utils'; import { ML_ENTITY_FIELD_OPERATIONS } from '@kbn/ml-anomaly-utils'; import { isPopulatedObject } from '@kbn/ml-is-populated-object'; -import type { - EmbeddableApiContext, - HasParentApi, - HasType, - PublishesUnifiedSearch, - PublishingSubject, -} from '@kbn/presentation-publishing'; -import { apiHasType, apiIsOfType } from '@kbn/presentation-publishing'; -import { createAction } from '@kbn/ui-actions-plugin/public'; +import type { EmbeddableApiContext } from '@kbn/presentation-publishing'; +import { apiIsOfType } from '@kbn/presentation-publishing'; +import type { UiActionsActionDefinition } 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, -} from '../embeddables'; -import { - ANOMALY_EXPLORER_CHARTS_EMBEDDABLE_TYPE, - ANOMALY_SWIMLANE_EMBEDDABLE_TYPE, -} from '../embeddables'; +import type { MlEmbeddableBaseApi } from '../embeddables'; +import { ANOMALY_EXPLORER_CHARTS_EMBEDDABLE_TYPE } from '../embeddables'; +import type { AnomalyChartsEmbeddableApi } from '../embeddables/anomaly_charts/types'; +import type { AnomalySwimLaneEmbeddableApi } from '../embeddables/anomaly_swimlane/types'; import type { MlCoreSetup } from '../plugin'; -import type { JobId } from '../shared'; - -export interface AnomalyChartsFieldSelectionApi { - entityFields: PublishingSubject; -} - -export interface SwimLaneDrilldownApi { - viewBy: PublishingSubject; - perPage: PublishingSubject; - fromPage: PublishingSubject; -} +import { isSwimLaneEmbeddableContext } from '../embeddables/anomaly_swimlane/types'; export interface OpenInAnomalyExplorerSwimLaneActionContext extends EmbeddableApiContext { - embeddable: OpenInAnomalyExplorerFromSwimLaneActionApi; + embeddable: AnomalySwimLaneEmbeddableApi; /** * Optional data provided by swim lane selection */ @@ -53,37 +33,15 @@ export interface OpenInAnomalyExplorerSwimLaneActionContext extends EmbeddableAp } export interface OpenInAnomalyExplorerAnomalyChartsActionContext extends EmbeddableApiContext { - embeddable: OpenInAnomalyExplorerFromAnomalyChartActionApi; + embeddable: AnomalyChartsEmbeddableApi; /** * Optional fields selected using anomaly charts */ data?: MlEntityField[]; } -export type OpenInAnomalyExplorerBaseActionApi = Partial< - HasParentApi & PublishesUnifiedSearch & { jobIds: JobId[] } ->; - -export type OpenInAnomalyExplorerFromSwimLaneActionApi = HasType & - OpenInAnomalyExplorerBaseActionApi & - SwimLaneDrilldownApi; - -export type OpenInAnomalyExplorerFromAnomalyChartActionApi = - HasType & - OpenInAnomalyExplorerBaseActionApi & - AnomalyChartsFieldSelectionApi; - export const OPEN_IN_ANOMALY_EXPLORER_ACTION = 'openInAnomalyExplorerAction'; -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 { @@ -93,17 +51,19 @@ export function isAnomalyChartsEmbeddableContext( ); } -export const isApiCompatible = (api: unknown | null): api is OpenInAnomalyExplorerBaseActionApi => - Boolean(apiHasType(api)); - -const getTimeRange = (embeddable: OpenInAnomalyExplorerBaseActionApi): TimeRange | undefined => { +const getTimeRange = (embeddable: MlEmbeddableBaseApi): TimeRange | undefined => { return embeddable.timeRange$?.getValue() ?? embeddable.parentApi?.timeRange$?.getValue(); }; -export function createOpenInExplorerAction(getStartServices: MlCoreSetup['getStartServices']) { - return createAction({ +export function createOpenInExplorerAction( + getStartServices: MlCoreSetup['getStartServices'] +): UiActionsActionDefinition< + OpenInAnomalyExplorerSwimLaneActionContext | OpenInAnomalyExplorerAnomalyChartsActionContext +> { + return { id: 'open-in-anomaly-explorer', type: OPEN_IN_ANOMALY_EXPLORER_ACTION, + order: 40, getIconType(): string { return 'visTable'; }, @@ -124,7 +84,7 @@ export function createOpenInExplorerAction(getStartServices: MlCoreSetup['getSta return locator.getUrl({ page: 'explorer', pageState: { - jobIds, + jobIds: jobIds.getValue(), timeRange: getTimeRange(embeddable), mlExplorerSwimlane: { viewByFromPage: fromPage.getValue(), @@ -177,7 +137,7 @@ export function createOpenInExplorerAction(getStartServices: MlCoreSetup['getSta return locator.getUrl({ page: 'explorer', pageState: { - jobIds, + jobIds: jobIds.getValue(), timeRange: getTimeRange(embeddable), // @ts-ignore QueryDslQueryContainer is not compatible with SerializableRecord ...(mlExplorerFilter ? ({ mlExplorerFilter } as SerializableRecord) : {}), @@ -199,5 +159,5 @@ export function createOpenInExplorerAction(getStartServices: MlCoreSetup['getSta async isCompatible(context: EmbeddableApiContext) { return isSwimLaneEmbeddableContext(context) || isAnomalyChartsEmbeddableContext(context); }, - }); + }; } diff --git a/x-pack/plugins/ml/public/ui_actions/triggers.ts b/x-pack/plugins/ml/public/ui_actions/triggers.ts index d13af630388e39..049f8544f0e95e 100644 --- a/x-pack/plugins/ml/public/ui_actions/triggers.ts +++ b/x-pack/plugins/ml/public/ui_actions/triggers.ts @@ -6,6 +6,10 @@ */ import type { Trigger } from '@kbn/ui-actions-plugin/public'; +import type { EmbeddableApiContext } from '@kbn/presentation-publishing'; +import type { AppStateSelectedCells } from '../application/explorer/explorer_utils'; +import type { AnomalySwimLaneEmbeddableApi } from '../embeddables/anomaly_swimlane/types'; +import { isSwimLaneEmbeddableContext } from '../embeddables/anomaly_swimlane/types'; export const SWIM_LANE_SELECTION_TRIGGER = 'SWIM_LANE_SELECTION_TRIGGER'; @@ -25,3 +29,17 @@ export const entityFieldSelectionTrigger: Trigger = { title: '', description: 'Entity field selection triggered', }; + +export interface AnomalySwimLaneSelectionTriggerContext extends EmbeddableApiContext { + embeddable: AnomalySwimLaneEmbeddableApi; + /** + * Data provided by swim lane selection + */ + data: AppStateSelectedCells; +} + +export const isAnomalySwimlaneSelectionTriggerContext = ( + context: unknown +): context is AnomalySwimLaneSelectionTriggerContext => { + return isSwimLaneEmbeddableContext(context) && context.data !== undefined; +};