diff --git a/src/plugins/embeddable/public/index.ts b/src/plugins/embeddable/public/index.ts index 52e408e8ebcd6..602713e4b8ae5 100644 --- a/src/plugins/embeddable/public/index.ts +++ b/src/plugins/embeddable/public/index.ts @@ -97,6 +97,7 @@ export { ReactEmbeddableRenderer, reactEmbeddableRegistryHasKey, registerReactEmbeddableFactory, + getReactEmbeddableFactory, type DefaultEmbeddableApi, type ReactEmbeddableFactory, type ReactEmbeddableRegistration, diff --git a/src/plugins/embeddable/public/react_embeddable_system/index.ts b/src/plugins/embeddable/public/react_embeddable_system/index.ts index b48289b7c4127..e80a36860a296 100644 --- a/src/plugins/embeddable/public/react_embeddable_system/index.ts +++ b/src/plugins/embeddable/public/react_embeddable_system/index.ts @@ -9,6 +9,7 @@ export { reactEmbeddableRegistryHasKey, registerReactEmbeddableFactory, + getReactEmbeddableFactory, } from './react_embeddable_registry'; export { ReactEmbeddableRenderer } from './react_embeddable_renderer'; export { diff --git a/src/plugins/presentation_panel/public/panel_component/types.ts b/src/plugins/presentation_panel/public/panel_component/types.ts index 61f6eaca887f5..52f2c57ff9e20 100644 --- a/src/plugins/presentation_panel/public/panel_component/types.ts +++ b/src/plugins/presentation_panel/public/panel_component/types.ts @@ -17,6 +17,7 @@ import { PublishesPanelTitle, HasParentApi, PublishesViewMode, + PublishesUnifiedSearch, } from '@kbn/presentation-publishing'; import { UiActionsService } from '@kbn/ui-actions-plugin/public'; import { MaybePromise } from '@kbn/utility-types'; @@ -68,9 +69,9 @@ export interface DefaultPresentationPanelApi PublishesBlockingError & PublishesPanelDescription & PublishesDisabledActionIds & - HasParentApi< - PresentationContainer & - Partial & PublishesViewMode> + HasParentApi & + Partial< + Pick & PublishesViewMode & PublishesUnifiedSearch > > {} 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 ebab6f5356994..aaca6eb555489 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 @@ -9,14 +9,75 @@ import { i18n } from '@kbn/i18n'; import type { StartServicesAccessor } from '@kbn/core/public'; -import type { EmbeddableFactoryDefinition, IContainer } from '@kbn/embeddable-plugin/public'; -import type { IAnomalySwimlaneEmbeddable } from './anomaly_swimlane_embeddable'; -import { PLUGIN_ID, PLUGIN_ICON, ML_APP_NAME } from '../../../common/constants/app'; -import { HttpService } from '../../application/services/http_service'; -import type { MlPluginStart, MlStartDependencies } from '../../plugin'; -import type { MlDependencies } from '../../application/app'; +import type { + EmbeddableFactoryDefinition, + IContainer, + ReactEmbeddableFactory, +} from '@kbn/embeddable-plugin/public'; +import { registerReactEmbeddableFactory } from '@kbn/embeddable-plugin/public'; import type { AnomalySwimlaneEmbeddableInput, AnomalySwimlaneEmbeddableServices } from '..'; import { ANOMALY_SWIMLANE_EMBEDDABLE_TYPE } from '..'; +import { ML_APP_NAME, PLUGIN_ICON, PLUGIN_ID } from '../../../common/constants/app'; +import type { MlDependencies } from '../../application/app'; +import { HttpService } from '../../application/services/http_service'; +import type { MlPluginStart, MlStartDependencies } from '../../plugin'; +import type { IAnomalySwimlaneEmbeddable } from './anomaly_swimlane_embeddable'; +import type { AnomalySwimLaneEmbeddableApi, AnomalySwimLaneEmbeddableState } from './types'; + +/** + * Provides the services required by the Anomaly Swimlane Embeddable. + */ +export const getServices = async ( + getStartServices: StartServicesAccessor +): Promise => { + const [coreStart, pluginsStart] = await getStartServices(); + + const { AnomalyDetectorService } = await import( + '../../application/services/anomaly_detector_service' + ); + const { AnomalyTimelineService } = await import( + '../../application/services/anomaly_timeline_service' + ); + const { mlApiServicesProvider } = await import('../../application/services/ml_api_service'); + const { mlResultsServiceProvider } = await import('../../application/services/results_service'); + + const httpService = new HttpService(coreStart.http); + const anomalyDetectorService = new AnomalyDetectorService(httpService); + const anomalyTimelineService = new AnomalyTimelineService( + pluginsStart.data.query.timefilter.timefilter, + coreStart.uiSettings, + mlResultsServiceProvider(mlApiServicesProvider(httpService)) + ); + + return [ + coreStart, + pluginsStart as MlDependencies, + { anomalyDetectorService, anomalyTimelineService }, + ]; +}; + +export const registerAnomalySwimLaneEmbeddableFactory = ( + getStartServices: StartServicesAccessor +) => { + const factory: ReactEmbeddableFactory< + AnomalySwimLaneEmbeddableState, + AnomalySwimLaneEmbeddableApi + > = { + type: ANOMALY_SWIMLANE_EMBEDDABLE_TYPE, + deserializeState: (state) => { + return state.rawState as AnomalySwimLaneEmbeddableState; + }, + buildEmbeddable: async (state, buildApi) => { + const { buildAnomalySwimLaneEmbeddable } = await import( + './build_anomaly_swim_lane_embeddable' + ); + const services = await getServices(getStartServices); + return await buildAnomalySwimLaneEmbeddable(state, buildApi, services); + }, + }; + + registerReactEmbeddableFactory(factory); +}; export class AnomalySwimlaneEmbeddableFactory implements EmbeddableFactoryDefinition diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/build_anomaly_swim_lane_embeddable.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/build_anomaly_swim_lane_embeddable.tsx new file mode 100644 index 0000000000000..7f509bf4db735 --- /dev/null +++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/build_anomaly_swim_lane_embeddable.tsx @@ -0,0 +1,182 @@ +/* + * 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 { initializeReactEmbeddableTitles } from '@kbn/embeddable-plugin/public'; +import type { + EmbeddableStateComparators, + ReactEmbeddableApiRegistration, +} from '@kbn/embeddable-plugin/public/react_embeddable_system/types'; +import type { TimeRange } from '@kbn/es-query'; +import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; +import { KibanaThemeProvider } from '@kbn/react-kibana-context-theme'; +import fastIsEqual from 'fast-deep-equal'; +import React, { Suspense, useEffect } from 'react'; +import { BehaviorSubject, Subscription } from 'rxjs'; +import type { AnomalySwimlaneEmbeddableServices, AnomalySwimlaneEmbeddableUserInput } from '..'; +import type { SwimlaneType } from '../../application/explorer/explorer_constants'; +import { SWIM_LANE_DEFAULT_PAGE_SIZE } from '../../application/explorer/explorer_constants'; +import type { JobId } from '../../shared'; +import { buildDataViewPublishingApi } from '../common/anomaly_detection_embeddable'; +import { EmbeddableLoading } from '../common/components/embeddable_loading_fallback'; +import EmbeddableSwimLaneContainer from './embeddable_swim_lane_container'; +import type { AnomalySwimLaneEmbeddableApi, AnomalySwimLaneEmbeddableState } from './types'; + +export const buildAnomalySwimLaneEmbeddable = async ( + state: AnomalySwimLaneEmbeddableState, + buildApi: ( + apiRegistration: ReactEmbeddableApiRegistration< + AnomalySwimLaneEmbeddableState, + AnomalySwimLaneEmbeddableApi + >, + comparators: EmbeddableStateComparators + ) => AnomalySwimLaneEmbeddableApi, + services: AnomalySwimlaneEmbeddableServices +) => { + const subscriptions = new Subscription(); + + const jobIds = new BehaviorSubject(state.jobIds); + const swimlaneType = new BehaviorSubject(state.swimlaneType); + const viewBy = new BehaviorSubject(state.viewBy); + const fromPage = new BehaviorSubject(1); + const perPage = new BehaviorSubject(state.perPage ?? SWIM_LANE_DEFAULT_PAGE_SIZE); + const interval = new BehaviorSubject(undefined); + + const { titlesApi, titleComparators, serializeTitles } = initializeReactEmbeddableTitles(state); + + const timeRange$ = new BehaviorSubject(state.timeRange); + function setTimeRange(nextTimeRange: TimeRange | undefined) { + timeRange$.next(nextTimeRange); + } + + const dataLoading$ = new BehaviorSubject(true); + + const api = buildApi( + { + ...titlesApi, + jobIds, + swimlaneType, + viewBy, + fromPage, + perPage, + interval, + setInterval: (v) => interval.next(v), + updateUserInput: (update: AnomalySwimlaneEmbeddableUserInput) => { + jobIds.next(update.jobIds); + swimlaneType.next(update.swimlaneType); + viewBy.next(update.viewBy); + titlesApi.setPanelTitle(update.panelTitle); + }, + updatePagination: (update: { perPage?: number; fromPage: number }) => { + fromPage.next(update.fromPage); + if (update.perPage) { + perPage.next(update.perPage); + } + }, + dataViews: buildDataViewPublishingApi( + { + anomalyDetectorService: services[2].anomalyDetectorService, + dataViewsService: services[1].data.dataViews, + }, + { jobIds }, + subscriptions + ), + timeRange$, + setTimeRange, + dataLoading: dataLoading$, + serializeState: () => { + return { + rawState: { + ...serializeTitles(), + jobIds: jobIds.value, + swimlaneType: swimlaneType.value, + viewBy: viewBy.value, + perPage: perPage.value, + timeRange: timeRange$.value, + }, + + references: [], + }; + }, + }, + { + timeRange: [timeRange$, setTimeRange, fastIsEqual], + ...titleComparators, + jobIds: [jobIds, jobIds.next, fastIsEqual], + swimlaneType: [swimlaneType, swimlaneType.next], + viewBy: [viewBy, viewBy.next], + // We do not want to store the current page + fromPage: [fromPage, fromPage.next, () => true], + perPage: [perPage, perPage.next], + } + ); + + const appliedTimeRange$ = new BehaviorSubject( + timeRange$.value ?? api.parentApi?.timeRange$?.value + ); + subscriptions.add( + api.timeRange$.subscribe((timeRange) => { + appliedTimeRange$.next(timeRange ?? api.parentApi?.timeRange$?.value); + }) + ); + if (api.parentApi?.timeRange$) { + subscriptions.add( + api.parentApi?.timeRange$.subscribe((parentTimeRange) => { + if (timeRange$?.value) { + return; + } + appliedTimeRange$.next(parentTimeRange); + }) + ); + } + api.appliedTimeRange$ = appliedTimeRange$; + + const onError = () => { + dataLoading$.next(false); + }; + + const onLoading = () => { + dataLoading$.next(true); + }; + + const refresh$ = new BehaviorSubject(undefined); + + const onRenderComplete = () => {}; + + return { + api, + Component: () => { + const I18nContext = services[0].i18n.Context; + const theme = services[0].theme; + + useEffect(function onUnmount() { + return () => { + subscriptions.unsubscribe(); + }; + }, []); + + return ( + + + + }> + + + + + + ); + }, + }; +}; diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/embeddable_swim_lane_container.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/embeddable_swim_lane_container.tsx index 89bce53f39fd3..cd4bbdca70134 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/embeddable_swim_lane_container.tsx +++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/embeddable_swim_lane_container.tsx @@ -5,92 +5,73 @@ * 2.0. */ -import type { FC } from 'react'; -import React, { useCallback, useState, useEffect } from 'react'; import { EuiCallOut, EuiEmptyPrompt } from '@elastic/eui'; +import type { FC } from 'react'; +import React, { useCallback, useState } from 'react'; import type { Observable } from 'rxjs'; +import { css } from '@emotion/react'; import type { CoreStart } from '@kbn/core/public'; import { FormattedMessage } from '@kbn/i18n-react'; -import { css } from '@emotion/react'; -import { Y_AXIS_LABEL_WIDTH } from '../../application/explorer/swimlane_annotation_container'; -import { useEmbeddableExecutionContext } from '../common/use_embeddable_execution_context'; -import type { IAnomalySwimlaneEmbeddable } from './anomaly_swimlane_embeddable'; -import { useSwimlaneInputResolver } from './swimlane_input_resolver'; +import { useBatchedPublishingSubjects } from '@kbn/presentation-publishing'; +import type { AnomalySwimlaneServices } from '..'; +import type { MlDependencies } from '../../application/app'; import type { SwimlaneType } from '../../application/explorer/explorer_constants'; +import type { AppStateSelectedCells } from '../../application/explorer/explorer_utils'; +import { Y_AXIS_LABEL_WIDTH } from '../../application/explorer/swimlane_annotation_container'; import { isViewBySwimLaneData, SwimlaneContainer, } from '../../application/explorer/swimlane_container'; -import type { AppStateSelectedCells } from '../../application/explorer/explorer_utils'; -import type { MlDependencies } from '../../application/app'; import { SWIM_LANE_SELECTION_TRIGGER } from '../../ui_actions'; -import type { - AnomalySwimlaneEmbeddableInput, - AnomalySwimlaneEmbeddableOutput, - AnomalySwimlaneServices, -} from '..'; -import { ANOMALY_SWIMLANE_EMBEDDABLE_TYPE } from '..'; +import { useSwimlaneInputResolver } from './swimlane_input_resolver'; +import type { AnomalySwimLaneEmbeddableApi } from './types'; +// import { useEmbeddableExecutionContext } from '../common/use_embeddable_execution_context'; export interface ExplorerSwimlaneContainerProps { id: string; - embeddableContext: InstanceType; - embeddableInput$: Observable; services: [CoreStart, MlDependencies, AnomalySwimlaneServices]; refresh: Observable; - onInputChange: (input: Partial) => void; - onOutputChange: (output: Partial) => void; onRenderComplete: () => void; onLoading: () => void; onError: (error: Error) => void; + api: AnomalySwimLaneEmbeddableApi; } export const EmbeddableSwimLaneContainer: FC = ({ id, - embeddableContext, - embeddableInput$, services, refresh, - onInputChange, - onOutputChange, onRenderComplete, onLoading, onError, + api, }) => { - useEmbeddableExecutionContext( - services[0].executionContext, - embeddableInput$, - ANOMALY_SWIMLANE_EMBEDDABLE_TYPE, - id - ); + // useEmbeddableExecutionContext( + // services[0].executionContext, + // embeddableInput$, + // ANOMALY_SWIMLANE_EMBEDDABLE_TYPE, + // id + // ); const [chartWidth, setChartWidth] = useState(0); - const [fromPage, setFromPage] = useState(1); + const [fromPage, perPage] = useBatchedPublishingSubjects(api.fromPage, api.perPage); const [{}, { uiActions, charts: chartsService }] = services; const [selectedCells, setSelectedCells] = useState(); - const [swimlaneType, swimlaneData, perPage, setPerPage, timeBuckets, isLoading, error] = - useSwimlaneInputResolver( - embeddableInput$, - onInputChange, - refresh, - services, - chartWidth, - fromPage, - { onError, onLoading } - ); - - useEffect(() => { - onOutputChange({ - perPage, - fromPage, - interval: swimlaneData?.interval, - }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [perPage, fromPage, swimlaneData]); + const [swimlaneType, swimlaneData, timeBuckets, isLoading, error] = useSwimlaneInputResolver( + api, + refresh, + services, + chartWidth, + { + onError, + onLoading, + } + ); const onCellsSelection = useCallback( (update?: AppStateSelectedCells) => { @@ -98,14 +79,14 @@ export const EmbeddableSwimLaneContainer: FC = ( if (update) { uiActions.getTrigger(SWIM_LANE_SELECTION_TRIGGER).exec({ - embeddable: embeddableContext, + embeddable: api, data: update, updateCallback: setSelectedCells.bind(null, undefined), }); } }, // eslint-disable-next-line react-hooks/exhaustive-deps - [swimlaneData, perPage, fromPage, setSelectedCells] + [swimlaneData, perPage, setSelectedCells] ); if (error) { @@ -137,7 +118,7 @@ export const EmbeddableSwimLaneContainer: FC = ( > = ( onCellsSelection={onCellsSelection} onPaginationChange={(update) => { if (update.fromPage) { - setFromPage(update.fromPage); + api.updatePagination({ fromPage: update.fromPage }); } if (update.perPage) { - setFromPage(1); - setPerPage(update.perPage); + api.updatePagination({ perPage: update.perPage, fromPage: 1 }); } }} isLoading={isLoading} diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/index.ts b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/index.ts index 66a08da87f14c..b8e82408dc14e 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/index.ts +++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/index.ts @@ -5,4 +5,4 @@ * 2.0. */ -export { AnomalySwimlaneEmbeddableFactory } from './anomaly_swimlane_embeddable_factory'; +export { registerAnomalySwimLaneEmbeddableFactory } from './anomaly_swimlane_embeddable_factory'; diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/swimlane_input_resolver.ts b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/swimlane_input_resolver.ts index 41eb4c115bc29..c950a4c6ec65f 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/swimlane_input_resolver.ts +++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/swimlane_input_resolver.ts @@ -5,52 +5,45 @@ * 2.0. */ +import type { CoreStart } from '@kbn/core/public'; +import { ViewMode } from '@kbn/embeddable-plugin/public'; +import type { TimeBuckets } from '@kbn/ml-time-buckets'; +import { useTimeBuckets } from '@kbn/ml-time-buckets'; import { useEffect, useMemo, useState } from 'react'; import type { Observable } from 'rxjs'; -import { combineLatest, from, of, Subject } from 'rxjs'; +import { BehaviorSubject, combineLatest, from, of } from 'rxjs'; import { catchError, debounceTime, distinctUntilChanged, map, - pluck, shareReplay, skipWhile, startWith, switchMap, tap, } from 'rxjs/operators'; -import type { CoreStart } from '@kbn/core/public'; -import { UI_SETTINGS } from '@kbn/data-plugin/public'; -import { ViewMode } from '@kbn/embeddable-plugin/public'; -import { TimeBuckets } from '@kbn/ml-time-buckets'; -import type { MlStartDependencies } from '../../plugin'; +import type { AnomalySwimlaneServices } from '..'; +import { CONTROLLED_BY_SWIM_LANE_FILTER } from '../..'; import type { SwimlaneType } from '../../application/explorer/explorer_constants'; import { ANOMALY_SWIM_LANE_HARD_LIMIT, - SWIM_LANE_DEFAULT_PAGE_SIZE, SWIMLANE_TYPE, } from '../../application/explorer/explorer_constants'; import type { OverallSwimlaneData } from '../../application/explorer/explorer_utils'; import { isViewBySwimLaneData } from '../../application/explorer/swimlane_container'; -import type { - AnomalySwimlaneEmbeddableInput, - AnomalySwimlaneEmbeddableOutput, - AnomalySwimlaneServices, -} from '..'; -import { processFilters } from '../common/process_filters'; -import { CONTROLLED_BY_SWIM_LANE_FILTER } from '../..'; +import type { MlStartDependencies } from '../../plugin'; import { getJobsObservable } from '../common/get_jobs_observable'; +import { processFilters } from '../common/process_filters'; +import type { AnomalySwimLaneEmbeddableApi } from './types'; const FETCH_RESULTS_DEBOUNCE_MS = 500; export function useSwimlaneInputResolver( - embeddableInput$: Observable, - onInputChange: (output: Partial) => void, + api: AnomalySwimLaneEmbeddableApi, refresh: Observable, services: [CoreStart, MlStartDependencies, AnomalySwimlaneServices], chartWidth: number, - fromPage: number, reportingCallbacks: { onLoading: () => void; onError: (error: Error) => void; @@ -58,36 +51,49 @@ export function useSwimlaneInputResolver( ): [ string | undefined, OverallSwimlaneData | undefined, - number, - (perPage: number) => void, TimeBuckets, boolean, Error | null | undefined ] { const [{ uiSettings }, , { anomalyTimelineService, anomalyDetectorService }] = services; + const timeBuckets = useTimeBuckets(uiSettings); + const [swimlaneData, setSwimlaneData] = useState(); const [swimlaneType, setSwimlaneType] = useState(); const [error, setError] = useState(); - const [perPage, setPerPage] = useState(); const [isLoading, setIsLoading] = useState(false); - const chartWidth$ = useMemo(() => new Subject(), []); + const chartWidth$ = useMemo(() => new BehaviorSubject(0), []); + + const embeddableInput$ = useMemo(() => { + return combineLatest({ + jobIds: api.jobIds, + swimlaneType: api.swimlaneType, + viewBy: api.viewBy, + perPage: api.perPage, + fromPage: api.fromPage, + }); + }, [api]); const selectedJobs$ = useMemo(() => { - return getJobsObservable(embeddableInput$, anomalyDetectorService, setError).pipe( - shareReplay(1) - ); + return getJobsObservable( + api.jobIds.pipe( + map((v) => { + return { jobIds: v }; + }) + ), + anomalyDetectorService, + setError + ).pipe(shareReplay(1)); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const bucketInterval$ = useMemo(() => { - return combineLatest([ - selectedJobs$, - chartWidth$, - embeddableInput$.pipe(pluck('timeRange')), - ]).pipe( - skipWhile(([jobs, width]) => !Array.isArray(jobs) || !width), + return combineLatest([selectedJobs$, chartWidth$, api.appliedTimeRange$]).pipe( + skipWhile(([jobs, width]) => { + return !Array.isArray(jobs) || !width; + }), tap(([, , timeRange]) => { anomalyTimelineService.setTimeRange(timeRange); }), @@ -99,32 +105,12 @@ export function useSwimlaneInputResolver( // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - const fromPage$ = useMemo(() => new Subject(), []); - const perPage$ = useMemo(() => new Subject(), []); - - const timeBuckets = useMemo(() => { - return new TimeBuckets({ - 'histogram:maxBars': uiSettings.get(UI_SETTINGS.HISTOGRAM_MAX_BARS), - 'histogram:barTarget': uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET), - dateFormat: uiSettings.get('dateFormat'), - 'dateFormat:scaled': uiSettings.get('dateFormat:scaled'), - }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - useEffect(() => { const subscription = combineLatest([ selectedJobs$, embeddableInput$, bucketInterval$, - fromPage$, - perPage$.pipe( - startWith(undefined), - // no need to emit when the initial value has been set - distinctUntilChanged( - (prev, curr) => prev === undefined && curr === SWIM_LANE_DEFAULT_PAGE_SIZE - ) - ), + (api.parentApi?.viewMode ?? of(ViewMode.VIEW)) as Observable, refresh.pipe(startWith(null)), ]) .pipe( @@ -133,7 +119,7 @@ export function useSwimlaneInputResolver( tap(() => { reportingCallbacks.onLoading(); }), - switchMap(([explorerJobs, input, bucketInterval, fromPageInput, perPageFromState]) => { + switchMap(([explorerJobs, input, bucketInterval, viewMode]) => { if (!explorerJobs) { // couldn't load the list of jobs return of(undefined); @@ -143,9 +129,9 @@ export function useSwimlaneInputResolver( viewBy, swimlaneType: swimlaneTypeInput, perPage: perPageInput, + fromPage: fromPageInput, filters, query, - viewMode, } = input; if (!swimlaneType) { @@ -170,16 +156,6 @@ export function useSwimlaneInputResolver( const { earliest, latest } = overallSwimlaneData; if (overallSwimlaneData && swimlaneTypeInput === SWIMLANE_TYPE.VIEW_BY) { - if (perPageFromState === undefined) { - // set initial pagination from the input or default one - setPerPage(perPageInput ?? SWIM_LANE_DEFAULT_PAGE_SIZE); - } - - if (viewMode === ViewMode.EDIT && perPageFromState !== perPageInput) { - // store per page value when the dashboard is in the edit mode - onInputChange({ perPage: perPageFromState }); - } - return from( anomalyTimelineService.loadViewBySwimlane( [], @@ -189,7 +165,7 @@ export function useSwimlaneInputResolver( isViewBySwimLaneData(swimlaneData) ? swimlaneData.cardinality : ANOMALY_SWIM_LANE_HARD_LIMIT, - perPageFromState ?? perPageInput ?? SWIM_LANE_DEFAULT_PAGE_SIZE, + perPageInput, fromPageInput, undefined, appliedFilters, @@ -215,6 +191,7 @@ export function useSwimlaneInputResolver( }) ) .subscribe((data) => { + api.setInterval(data?.interval); if (data !== undefined) { setError(null); setSwimlaneData(data); @@ -228,17 +205,6 @@ export function useSwimlaneInputResolver( // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - useEffect(() => { - fromPage$.next(fromPage); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [fromPage]); - - useEffect(() => { - if (perPage === undefined) return; - perPage$.next(perPage); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [perPage]); - useEffect(() => { chartWidth$.next(chartWidth); // eslint-disable-next-line react-hooks/exhaustive-deps @@ -251,13 +217,5 @@ export function useSwimlaneInputResolver( // eslint-disable-next-line react-hooks/exhaustive-deps }, [error]); - return [ - swimlaneType, - swimlaneData, - perPage ?? SWIM_LANE_DEFAULT_PAGE_SIZE, - setPerPage, - timeBuckets, - isLoading, - error, - ]; + return [swimlaneType, swimlaneData, timeBuckets, isLoading, error]; } diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/types.ts b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/types.ts index bb9548f78f61a..ef4e055275ee0 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/types.ts +++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/types.ts @@ -5,35 +5,35 @@ * 2.0. */ -import type { - HasType, - PublishesWritablePanelTitle, - PublishingSubject, -} from '@kbn/presentation-publishing'; -import { apiIsOfType } from '@kbn/presentation-publishing'; +import type { SerializedReactEmbeddableTitles } from '@kbn/embeddable-plugin/public'; import { isPopulatedObject } from '@kbn/ml-is-populated-object'; +import type { PublishesWritablePanelTitle, PublishingSubject } from '@kbn/presentation-publishing'; +import { apiIsOfType } from '@kbn/presentation-publishing'; import type { SwimlaneType } from '../../application/explorer/explorer_constants'; +import type { AppStateSelectedCells } from '../../application/explorer/explorer_utils'; 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'; +import type { + AnomalySwimlaneEmbeddableCustomInput, + AnomalySwimlaneEmbeddableUserInput, + MlEmbeddableBaseApi, +} from '../types'; export interface AnomalySwimLaneComponentApi { jobIds: PublishingSubject; swimlaneType: PublishingSubject; - viewBy: PublishingSubject; + viewBy: PublishingSubject; perPage: PublishingSubject; fromPage: PublishingSubject; interval: PublishingSubject; + setInterval: (interval: number | undefined) => void; updateUserInput: (input: AnomalySwimlaneEmbeddableUserInput) => void; + updatePagination: (update: { perPage?: number; fromPage: number }) => void; } -export interface AnomalySwimLaneEmbeddableApi - extends HasType, - PublishesWritablePanelTitle, - MlEmbeddableBaseApi, - AnomalySwimLaneComponentApi {} +export type AnomalySwimLaneEmbeddableApi = MlEmbeddableBaseApi & + PublishesWritablePanelTitle & + AnomalySwimLaneComponentApi; export interface AnomalySwimLaneActionContext { embeddable: AnomalySwimLaneEmbeddableApi; @@ -46,3 +46,10 @@ export function isSwimLaneEmbeddableContext(arg: unknown): arg is AnomalySwimLan apiIsOfType(arg.embeddable, ANOMALY_SWIMLANE_EMBEDDABLE_TYPE) ); } + +/** + * Persisted state for the Anomaly Swim Lane Embeddable. + */ +export interface AnomalySwimLaneEmbeddableState + extends SerializedReactEmbeddableTitles, + AnomalySwimlaneEmbeddableCustomInput {} 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 84a03b05e869f..e6efe04db5b81 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 @@ -7,21 +7,63 @@ import { type DataView } from '@kbn/data-views-plugin/common'; import { type DataViewsContract } from '@kbn/data-views-plugin/public'; +import type { IContainer } from '@kbn/embeddable-plugin/public'; import { Embeddable, type EmbeddableInput, type EmbeddableOutput, - type IContainer, } from '@kbn/embeddable-plugin/public'; -import type { BehaviorSubject } from 'rxjs'; -import { firstValueFrom } from 'rxjs'; +import type { PublishingSubject } from '@kbn/presentation-publishing'; +import type { Subscription } from 'rxjs'; +import { BehaviorSubject, firstValueFrom, forkJoin, from } from 'rxjs'; +import { map, switchMap } from 'rxjs/operators'; import { type AnomalyDetectorService } from '../../application/services/anomaly_detector_service'; import type { JobId } from '../../shared'; +import type { AnomalySwimLaneEmbeddableApi } from '../anomaly_swimlane/types'; export type CommonInput = { jobIds: string[] } & EmbeddableInput; export type CommonOutput = { indexPatterns?: DataView[] } & EmbeddableOutput; +export const buildDataViewPublishingApi = ( + services: { anomalyDetectorService: AnomalyDetectorService; dataViewsService: DataViewsContract }, + api: Pick, + subscription: Subscription +): PublishingSubject => { + const dataViews$ = new BehaviorSubject(undefined); + + subscription.add( + api.jobIds + .pipe( + // Get job definitions + switchMap((jobIds) => services.anomalyDetectorService.getJobs$(jobIds)), + // Get unique indices from the datafeed configs + map((jobs) => [...new Set(jobs.map((j) => j.datafeed_config!.indices).flat())]), + switchMap((indices) => + forkJoin( + indices.map((indexName) => + from( + services.dataViewsService.find(`"${indexName}"`).then((r) => { + const dView = r.find((obj) => + obj.getIndexPattern().toLowerCase().includes(indexName.toLowerCase()) + ); + + return dView; + }) + ) + ) + ) + ), + map((results) => { + return results.flat().filter((dView) => dView !== undefined) as DataView[]; + }) + ) + .subscribe(dataViews$) + ); + + return dataViews$; +}; + export abstract class AnomalyDetectionEmbeddable< Input extends CommonInput, Output extends CommonOutput diff --git a/x-pack/plugins/ml/public/embeddables/common/get_jobs_observable.ts b/x-pack/plugins/ml/public/embeddables/common/get_jobs_observable.ts index 406c7deb563c5..80c137cbee142 100644 --- a/x-pack/plugins/ml/public/embeddables/common/get_jobs_observable.ts +++ b/x-pack/plugins/ml/public/embeddables/common/get_jobs_observable.ts @@ -5,22 +5,22 @@ * 2.0. */ +import { isEqual } from 'lodash'; import type { Observable } from 'rxjs'; import { of } from 'rxjs'; -import { catchError, distinctUntilChanged, map, pluck, switchMap } from 'rxjs/operators'; -import { isEqual } from 'lodash'; -import type { AnomalyChartsEmbeddableInput, AnomalySwimlaneEmbeddableInput } from '../types'; -import type { AnomalyDetectorService } from '../../application/services/anomaly_detector_service'; -import type { ExplorerJob } from '../../application/explorer/explorer_utils'; +import { catchError, distinctUntilChanged, map, switchMap, tap } from 'rxjs/operators'; import { parseInterval } from '../../../common/util/parse_interval'; +import type { ExplorerJob } from '../../application/explorer/explorer_utils'; +import type { AnomalyDetectorService } from '../../application/services/anomaly_detector_service'; +import type { JobId } from '../../shared'; export function getJobsObservable( - embeddableInput: Observable, + embeddableInput: Observable<{ jobIds: JobId[] }>, anomalyDetectorService: AnomalyDetectorService, setErrorHandler: (e: Error) => void ) { return embeddableInput.pipe( - pluck('jobIds'), + map((v) => v.jobIds), distinctUntilChanged(isEqual), switchMap((jobsIds) => anomalyDetectorService.getJobs$(jobsIds)), map((jobs) => { diff --git a/x-pack/plugins/ml/public/embeddables/get_embeddable_component.tsx b/x-pack/plugins/ml/public/embeddables/get_embeddable_component.tsx index feeab0f8d7bff..d26f2d9a2a6c8 100644 --- a/x-pack/plugins/ml/public/embeddables/get_embeddable_component.tsx +++ b/x-pack/plugins/ml/public/embeddables/get_embeddable_component.tsx @@ -8,11 +8,13 @@ import React from 'react'; import type { CoreStart } from '@kbn/core/public'; import type { EmbeddableFactory, EmbeddableInput } from '@kbn/embeddable-plugin/public'; +import { getReactEmbeddableFactory } from '@kbn/embeddable-plugin/public'; import { EmbeddableRoot, useEmbeddableFactory } from '@kbn/embeddable-plugin/public'; import { EuiLoadingChart } from '@elastic/eui'; import type { MappedEmbeddableTypeOf } from './types'; import type { MlStartDependencies } from '../plugin'; import type { MlEmbeddableTypes } from './constants'; +import { ANOMALY_SWIMLANE_EMBEDDABLE_TYPE } from './constants'; /** * Gets an instance of an embeddable component of requested type. @@ -26,8 +28,16 @@ export function getEmbeddableComponent plugins: MlStartDependencies ) { const { embeddable: embeddableStart } = plugins; - const factory = - embeddableStart.getEmbeddableFactory>(embeddableType); + + let factory: EmbeddableFactory>; + if (embeddableType === ANOMALY_SWIMLANE_EMBEDDABLE_TYPE) { + // @ts-ignore TODO + factory = getReactEmbeddableFactory>(embeddableType); + } else { + // @ts-ignore TODO + factory = + embeddableStart.getEmbeddableFactory>(embeddableType); + } if (!factory) { throw new Error(`Embeddable type "${embeddableType}" has not been registered.`); diff --git a/x-pack/plugins/ml/public/embeddables/index.ts b/x-pack/plugins/ml/public/embeddables/index.ts index 9f0d2d75b1162..89527f3d99af4 100644 --- a/x-pack/plugins/ml/public/embeddables/index.ts +++ b/x-pack/plugins/ml/public/embeddables/index.ts @@ -6,24 +6,17 @@ */ import type { EmbeddableSetup } from '@kbn/embeddable-plugin/public'; -import { AnomalySwimlaneEmbeddableFactory } from './anomaly_swimlane'; import type { MlCoreSetup } from '../plugin'; import { AnomalyChartsEmbeddableFactory } from './anomaly_charts'; +import { registerAnomalySwimLaneEmbeddableFactory } from './anomaly_swimlane'; import { SingleMetricViewerEmbeddableFactory } from './single_metric_viewer'; export * from './constants'; -export * from './types'; - export { getEmbeddableComponent } from './get_embeddable_component'; +export * from './types'; export function registerEmbeddables(embeddable: EmbeddableSetup, core: MlCoreSetup) { - const anomalySwimlaneEmbeddableFactory = new AnomalySwimlaneEmbeddableFactory( - core.getStartServices - ); - embeddable.registerEmbeddableFactory( - anomalySwimlaneEmbeddableFactory.type, - anomalySwimlaneEmbeddableFactory - ); + registerAnomalySwimLaneEmbeddableFactory(core.getStartServices); const anomalyChartsFactory = new AnomalyChartsEmbeddableFactory(core.getStartServices); embeddable.registerEmbeddableFactory(anomalyChartsFactory.type, anomalyChartsFactory); diff --git a/x-pack/plugins/ml/public/embeddables/types.ts b/x-pack/plugins/ml/public/embeddables/types.ts index 14ee0e7c6d444..4c8d35b5048b5 100644 --- a/x-pack/plugins/ml/public/embeddables/types.ts +++ b/x-pack/plugins/ml/public/embeddables/types.ts @@ -8,15 +8,24 @@ import type { CoreStart } from '@kbn/core/public'; import type { RefreshInterval } from '@kbn/data-plugin/common'; import type { DataView } from '@kbn/data-views-plugin/common'; -import type { EmbeddableInput, EmbeddableOutput, IEmbeddable } from '@kbn/embeddable-plugin/public'; +import type { + DefaultEmbeddableApi, + 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, + PublishesDataLoading, + PublishesDataViews, PublishesUnifiedSearch, PublishesViewMode, + PublishesWritableUnifiedSearch, + PublishingSubject, } from '@kbn/presentation-publishing'; import type { JobId } from '../../common/types/anomaly_detection_jobs'; import type { MlDependencies } from '../application/app'; @@ -37,9 +46,19 @@ import type { MlEmbeddableTypes, } from './constants'; -export type MlEmbeddableBaseApi = Partial< - HasParentApi & PublishesViewMode & PublishesUnifiedSearch ->; +/** + * Common API for all ML embeddables + */ +export interface MlEmbeddableBaseApi + extends DefaultEmbeddableApi, + Pick, + PublishesDataViews, + PublishesViewMode { + /** + * Result time range based on the parent and panel time range APIs + */ + appliedTimeRange$?: PublishingSubject; +} /** Manual input by the user */ export interface AnomalySwimlaneEmbeddableUserInput { @@ -56,10 +75,10 @@ export interface AnomalySwimlaneEmbeddableCustomInput { perPage?: number; // Embeddable inputs which are not included in the default interface - filters: Filter[]; - query: Query; - refreshConfig: RefreshInterval; - timeRange: TimeRange; + filters?: Filter[]; + query?: Query; + refreshConfig?: RefreshInterval; + timeRange?: TimeRange; } export type AnomalySwimlaneEmbeddableInput = EmbeddableInput & AnomalySwimlaneEmbeddableCustomInput; 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 bdeea57f5e298..3612446e8f5af 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 @@ -62,7 +62,8 @@ export function createEditSwimlanePanelAction( }, async isCompatible(context: EmbeddableApiContext) { return ( - isSwimLaneEmbeddableContext(context) && context.embeddable.viewMode?.getValue() === 'edit' + isSwimLaneEmbeddableContext(context) && + context.embeddable.parentApi?.viewMode?.getValue() === 'edit' ); }, };