diff --git a/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js b/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js index f9aa282b4b118..7703ca87317e1 100644 --- a/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js +++ b/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js @@ -278,7 +278,6 @@ class AnnotationsTableUI extends Component { entityCondition[annotation.by_field_name] = annotation.by_field_value; } mlTimeSeriesExplorer.entities = entityCondition; - // appState.mlTimeSeriesExplorer = mlTimeSeriesExplorer; const mlLocator = share.url.locators.get(ML_APP_LOCATOR); const singleMetricViewerLink = await mlLocator.getUrl( diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/job_type/page.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/job_type/page.tsx index 9ee4f5dcdc991..ddea5031d4bb0 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/job_type/page.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/job_type/page.tsx @@ -6,7 +6,7 @@ */ import type { FC } from 'react'; -import React, { useState, useMemo } from 'react'; +import React, { useEffect, useState, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiTitle, @@ -24,7 +24,6 @@ import { useMlKibana, useNavigateToPath } from '../../../../contexts/kibana'; import { useDataSource } from '../../../../contexts/ml'; import { DataRecognizer } from '../../../../components/data_recognizer'; import { addItemToRecentlyAccessed } from '../../../../util/recently_accessed'; -import { timeBasedIndexCheck } from '../../../../util/index_utils'; import { LinkCard } from '../../../../components/link_card'; import { CategorizationIcon } from './categorization_job_icon'; import { ML_APP_LOCATOR, ML_PAGES } from '../../../../../../common/constants/locator'; @@ -35,7 +34,10 @@ import { MlPageHeader } from '../../../../components/page_header'; export const Page: FC = () => { const { - services: { share }, + services: { + share, + notifications: { toasts }, + }, } = useMlKibana(); const dataSourceContext = useDataSource(); @@ -48,7 +50,22 @@ export const Page: FC = () => { const { selectedDataView, selectedSavedSearch } = dataSourceContext; - const isTimeBasedIndex = timeBasedIndexCheck(selectedDataView); + const isTimeBasedIndex: boolean = selectedDataView.isTimeBased(); + + useEffect(() => { + if (!isTimeBasedIndex) { + toasts.addWarning({ + title: i18n.translate('xpack.ml.dataViewNotBasedOnTimeSeriesNotificationTitle', { + defaultMessage: 'The data view {dataViewIndexPattern} is not based on a time series', + values: { dataViewIndexPattern: selectedDataView.getIndexPattern() }, + }), + text: i18n.translate('xpack.ml.dataViewNotBasedOnTimeSeriesNotificationDescription', { + defaultMessage: 'Anomaly detection only runs over time-based indices', + }), + }); + } + }, [isTimeBasedIndex, selectedDataView, toasts]); + const hasGeoFields = useMemo( () => [ diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/plot_function_controls/plot_function_controls.tsx b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/plot_function_controls/plot_function_controls.tsx index b26dc876a33e1..6ad84f892eb31 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/plot_function_controls/plot_function_controls.tsx +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/plot_function_controls/plot_function_controls.tsx @@ -15,6 +15,7 @@ import { getFunctionDescription, isMetricDetector } from '../../get_function_des import { useToastNotificationService } from '../../../services/toast_notification_service'; import { useMlResultsService } from '../../../services/results_service'; import type { CombinedJob } from '../../../../../common/types/anomaly_detection_jobs'; +import type { MlEntity } from '../../../../embeddables'; const plotByFunctionOptions = [ { @@ -50,7 +51,7 @@ export const PlotByFunctionControls = ({ setFunctionDescription: (func: string) => void; selectedDetectorIndex: number; selectedJobId: string; - selectedEntities: Record; + selectedEntities?: MlEntity; entityControlsCount: number; }) => { const toastNotificationService = useToastNotificationService(); @@ -59,7 +60,7 @@ export const PlotByFunctionControls = ({ const getFunctionDescriptionToPlot = useCallback( async ( _selectedDetectorIndex: number, - _selectedEntities: Record, + _selectedEntities: MlEntity | undefined, _selectedJobId: string, _selectedJob: CombinedJob ) => { diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/series_controls/series_controls.tsx b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/series_controls/series_controls.tsx index 26556f9d1f4c6..ee96494a77e09 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/series_controls/series_controls.tsx +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/series_controls/series_controls.tsx @@ -37,6 +37,7 @@ import { import type { FieldDefinition } from '../../../services/results_service/result_service_rx'; import { getViewableDetectors } from '../../timeseriesexplorer_utils/get_viewable_detectors'; import { PlotByFunctionControls } from '../plot_function_controls'; +import type { MlEntity } from '../../../../embeddables'; function getEntityControlOptions(fieldValues: FieldDefinition['values']): ComboBoxOption[] { if (!Array.isArray(fieldValues)) { @@ -76,7 +77,7 @@ interface SeriesControlsProps { functionDescription?: string; job?: CombinedJob | MlJob; selectedDetectorIndex: number; - selectedEntities: Record; + selectedEntities?: MlEntity; selectedJobId: JobId; setFunctionDescription: (func: string) => void; } diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/get_controls_for_detector.ts b/x-pack/plugins/ml/public/application/timeseriesexplorer/get_controls_for_detector.ts index 69b85acad8665..214fe8bf45f47 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/get_controls_for_detector.ts +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/get_controls_for_detector.ts @@ -8,13 +8,14 @@ import { mlJobService } from '../services/job_service'; import type { Entity } from './components/entity_control/entity_control'; import type { JobId, CombinedJob } from '../../../common/types/anomaly_detection_jobs'; +import type { MlEntity } from '../../embeddables'; /** * Extracts entities from the detector configuration */ export function getControlsForDetector( selectedDetectorIndex: number, - selectedEntities: Record, + selectedEntities: MlEntity | undefined, selectedJobId: JobId, job?: CombinedJob ): Entity[] { diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/get_function_description.ts b/x-pack/plugins/ml/public/application/timeseriesexplorer/get_function_description.ts index 30e6afb9cf66f..274bfd3c6f27c 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/get_function_description.ts +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/get_function_description.ts @@ -14,6 +14,7 @@ import { getControlsForDetector } from './get_controls_for_detector'; import { getCriteriaFields } from './get_criteria_fields'; import type { CombinedJob } from '../../../common/types/anomaly_detection_jobs'; import { getViewableDetectors } from './timeseriesexplorer_utils/get_viewable_detectors'; +import type { MlEntity } from '../../embeddables'; export function isMetricDetector(selectedJob: CombinedJob, selectedDetectorIndex: number) { const detectors = getViewableDetectors(selectedJob); @@ -37,7 +38,7 @@ export const getFunctionDescription = async ( selectedJob, }: { selectedDetectorIndex: number; - selectedEntities: Record; + selectedEntities: MlEntity | undefined; selectedJobId: string; selectedJob: CombinedJob; }, diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_embeddable_chart/timeseriesexplorer_embeddable_chart.js b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_embeddable_chart/timeseriesexplorer_embeddable_chart.js index 945a765616f8f..42e942ae907fc 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_embeddable_chart/timeseriesexplorer_embeddable_chart.js +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_embeddable_chart/timeseriesexplorer_embeddable_chart.js @@ -717,6 +717,7 @@ export class TimeSeriesExplorerEmbeddableChart extends React.Component { !isEqual(previousProps.selectedDetectorIndex, this.props.selectedDetectorIndex) || !isEqual(previousProps.selectedEntities, this.props.selectedEntities) || previousProps.selectedForecastId !== this.props.selectedForecastId || + previousProps.selectedJob?.job_id !== this.props.selectedJob?.job_id || previousProps.selectedJobId !== this.props.selectedJobId || previousProps.functionDescription !== this.props.functionDescription ) { @@ -727,6 +728,7 @@ export class TimeSeriesExplorerEmbeddableChart extends React.Component { !isEqual(previousProps.selectedEntities, this.props.selectedEntities) || previousProps.selectedForecastId !== this.props.selectedForecastId || previousProps.selectedJobId !== this.props.selectedJobId || + previousProps.selectedJob?.job_id !== this.props.selectedJob?.job_id || previousProps.functionDescription !== this.props.functionDescription; this.loadSingleMetricData(fullRefresh); } diff --git a/x-pack/plugins/ml/public/application/util/index_service.ts b/x-pack/plugins/ml/public/application/util/index_service.ts index 84da4ee567ab4..bbdddbd637268 100644 --- a/x-pack/plugins/ml/public/application/util/index_service.ts +++ b/x-pack/plugins/ml/public/application/util/index_service.ts @@ -11,7 +11,6 @@ import type { Job } from '../../../common/types/anomaly_detection_jobs'; import { useMlKibana } from '../contexts/kibana'; -// TODO Consolidate with legacy code in `ml/public/application/util/index_utils.ts`. export function indexServiceFactory(dataViewsService: DataViewsContract) { /** * Retrieves the data view ID from the given name. diff --git a/x-pack/plugins/ml/public/application/util/index_utils.ts b/x-pack/plugins/ml/public/application/util/index_utils.ts index a9074e573e55a..4d8eddde1a54b 100644 --- a/x-pack/plugins/ml/public/application/util/index_utils.ts +++ b/x-pack/plugins/ml/public/application/util/index_utils.ts @@ -5,11 +5,9 @@ * 2.0. */ -import { i18n } from '@kbn/i18n'; import type { SavedSearch, SavedSearchPublicPluginStart } from '@kbn/saved-search-plugin/public'; import type { Query, Filter } from '@kbn/es-query'; import type { DataView, DataViewField, DataViewsContract } from '@kbn/data-views-plugin/common'; -import { getToastNotifications } from './dependency_cache'; export interface DataViewAndSavedSearch { savedSearch: SavedSearch | null; @@ -48,31 +46,6 @@ export function getQueryFromSavedSearchObject(savedSearch: SavedSearch) { }; } -/** - * Returns true if the index passed in is time based - * an optional flag will trigger the display a notification at the top of the page - * warning that the index is not time based - */ -export function timeBasedIndexCheck(dataView: DataView, showNotification = false) { - if (!dataView.isTimeBased()) { - if (showNotification) { - const toastNotifications = getToastNotifications(); - toastNotifications.addWarning({ - title: i18n.translate('xpack.ml.dataViewNotBasedOnTimeSeriesNotificationTitle', { - defaultMessage: 'The data view {dataViewIndexPattern} is not based on a time series', - values: { dataViewIndexPattern: dataView.getIndexPattern() }, - }), - text: i18n.translate('xpack.ml.dataViewNotBasedOnTimeSeriesNotificationDescription', { - defaultMessage: 'Anomaly detection only runs over time-based indices', - }), - }); - } - return false; - } else { - return true; - } -} - /** * Returns true if the index pattern contains a : * which means it is cross-cluster diff --git a/x-pack/plugins/ml/public/embeddables/constants.ts b/x-pack/plugins/ml/public/embeddables/constants.ts index 1001cd89c7498..1e42301676dfc 100644 --- a/x-pack/plugins/ml/public/embeddables/constants.ts +++ b/x-pack/plugins/ml/public/embeddables/constants.ts @@ -11,5 +11,7 @@ export const ANOMALY_SINGLE_METRIC_VIEWER_EMBEDDABLE_TYPE = 'ml_single_metric_vi export type AnomalySwimLaneEmbeddableType = typeof ANOMALY_SWIMLANE_EMBEDDABLE_TYPE; export type AnomalyExplorerChartsEmbeddableType = typeof ANOMALY_EXPLORER_CHARTS_EMBEDDABLE_TYPE; +export type AnomalySingleMetricViewerEmbeddableType = + typeof ANOMALY_SINGLE_METRIC_VIEWER_EMBEDDABLE_TYPE; export type MlEmbeddableTypes = AnomalySwimLaneEmbeddableType | AnomalyExplorerChartsEmbeddableType; diff --git a/x-pack/plugins/ml/public/embeddables/single_metric_viewer/embeddable_single_metric_viewer_container.tsx b/x-pack/plugins/ml/public/embeddables/single_metric_viewer/embeddable_single_metric_viewer_container.tsx index 3aa0e332dcaf5..d7dae512e1935 100644 --- a/x-pack/plugins/ml/public/embeddables/single_metric_viewer/embeddable_single_metric_viewer_container.tsx +++ b/x-pack/plugins/ml/public/embeddables/single_metric_viewer/embeddable_single_metric_viewer_container.tsx @@ -38,7 +38,7 @@ interface AppStateZoom { export interface EmbeddableSingleMetricViewerContainerProps { id: string; embeddableContext: InstanceType; - embeddableInput: Observable; + embeddableInput$: Observable; services: SingleMetricViewerEmbeddableServices; refresh: Observable; onInputChange: (input: Partial) => void; @@ -50,10 +50,10 @@ export interface EmbeddableSingleMetricViewerContainerProps { export const EmbeddableSingleMetricViewerContainer: FC< EmbeddableSingleMetricViewerContainerProps -> = ({ id, embeddableContext, embeddableInput, services, refresh, onRenderComplete }) => { +> = ({ id, embeddableContext, embeddableInput$, services, refresh, onRenderComplete }) => { useEmbeddableExecutionContext( services[0].executionContext, - embeddableInput, + embeddableInput$, ANOMALY_SINGLE_METRIC_VIEWER_EMBEDDABLE_TYPE, id ); @@ -66,7 +66,7 @@ export const EmbeddableSingleMetricViewerContainer: FC< const { mlApiServices, mlJobService } = services[2]; const { data, bounds, lastRefresh } = useSingleMetricViewerInputResolver( - embeddableInput, + embeddableInput$, refresh, services[1].data.query.timefilter.timefilter, onRenderComplete diff --git a/x-pack/plugins/ml/public/embeddables/single_metric_viewer/single_metric_viewer_embeddable.tsx b/x-pack/plugins/ml/public/embeddables/single_metric_viewer/single_metric_viewer_embeddable.tsx index 719d300162d09..8cbd2ad30ae8e 100644 --- a/x-pack/plugins/ml/public/embeddables/single_metric_viewer/single_metric_viewer_embeddable.tsx +++ b/x-pack/plugins/ml/public/embeddables/single_metric_viewer/single_metric_viewer_embeddable.tsx @@ -9,11 +9,11 @@ import React, { Suspense } from 'react'; import ReactDOM from 'react-dom'; import { pick } from 'lodash'; -import { Embeddable } from '@kbn/embeddable-plugin/public'; +import { Embeddable, embeddableInputToSubject } from '@kbn/embeddable-plugin/public'; +import { Subject, Subscription, type BehaviorSubject } from 'rxjs'; import type { CoreStart } from '@kbn/core/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 { DatePickerContextProvider, type DatePickerDependencies } from '@kbn/ml-date-picker'; @@ -28,6 +28,7 @@ import type { } from '..'; import { ANOMALY_SINGLE_METRIC_VIEWER_EMBEDDABLE_TYPE } from '..'; import { EmbeddableLoading } from '../common/components/embeddable_loading_fallback'; +import type { MlEntity } from '..'; export const getDefaultSingleMetricViewerPanelTitle = (jobIds: JobId[]) => i18n.translate('xpack.ml.singleMetricViewerEmbeddable.title', { @@ -45,12 +46,49 @@ export class SingleMetricViewerEmbeddable extends Embeddable< private reload$ = new Subject(); public readonly type: string = ANOMALY_SINGLE_METRIC_VIEWER_EMBEDDABLE_TYPE; + // API + public readonly functionDescription: BehaviorSubject; + public readonly jobIds: BehaviorSubject; + public readonly selectedDetectorIndex: BehaviorSubject; + public readonly selectedEntities: BehaviorSubject; + + private apiSubscriptions = new Subscription(); + constructor( initialInput: SingleMetricViewerEmbeddableInput, public services: [CoreStart, MlDependencies, SingleMetricViewerServices], parent?: IContainer ) { super(initialInput, {} as AnomalyChartsEmbeddableOutput, parent); + + this.jobIds = embeddableInputToSubject( + this.apiSubscriptions, + this, + 'jobIds' + ); + + this.functionDescription = embeddableInputToSubject< + string | undefined, + SingleMetricViewerEmbeddableInput + >(this.apiSubscriptions, this, 'functionDescription'); + + this.selectedDetectorIndex = embeddableInputToSubject< + number | undefined, + SingleMetricViewerEmbeddableInput + >(this.apiSubscriptions, this, 'selectedDetectorIndex'); + + this.selectedEntities = embeddableInputToSubject< + MlEntity | undefined, + SingleMetricViewerEmbeddableInput + >(this.apiSubscriptions, this, 'selectedEntities'); + } + + public updateUserInput(update: SingleMetricViewerEmbeddableInput) { + this.updateInput(update); + } + + public reportsEmbeddableLoad() { + return true; } public onLoading() { @@ -65,7 +103,7 @@ export class SingleMetricViewerEmbeddable extends Embeddable< public onRenderComplete() { this.renderComplete.dispatchComplete(); - this.updateOutput({ loading: false, error: undefined }); + this.updateOutput({ loading: false, rendered: true, error: undefined }); } public render(node: HTMLElement) { @@ -102,7 +140,7 @@ export class SingleMetricViewerEmbeddable extends Embeddable< @@ -58,11 +59,16 @@ export class SingleMetricViewerEmbeddableFactory const { resolveEmbeddableSingleMetricViewerUserInput } = await import( './single_metric_viewer_setup_flyout' ); - return await resolveEmbeddableSingleMetricViewerUserInput( + const userInput = await resolveEmbeddableSingleMetricViewerUserInput( coreStart, pluginStart, - singleMetricServices + singleMetricServices.mlApiServices ); + + return { + ...userInput, + title: userInput.panelTitle, + }; } catch (e) { return Promise.reject(); } @@ -142,7 +148,10 @@ export class SingleMetricViewerEmbeddableFactory ]; } - public async create(initialInput: SingleMetricViewerEmbeddableInput, parent?: IContainer) { + public async create( + initialInput: SingleMetricViewerEmbeddableInput, + parent?: IContainer + ): Promise> { const services = await this.getServices(); const { SingleMetricViewerEmbeddable } = await import('./single_metric_viewer_embeddable'); return new SingleMetricViewerEmbeddable(initialInput, services, parent); diff --git a/x-pack/plugins/ml/public/embeddables/single_metric_viewer/single_metric_viewer_initializer.tsx b/x-pack/plugins/ml/public/embeddables/single_metric_viewer/single_metric_viewer_initializer.tsx index b14539480a955..e3d35fa472b98 100644 --- a/x-pack/plugins/ml/public/embeddables/single_metric_viewer/single_metric_viewer_initializer.tsx +++ b/x-pack/plugins/ml/public/embeddables/single_metric_viewer/single_metric_viewer_initializer.tsx @@ -23,52 +23,60 @@ import { import { FormattedMessage } from '@kbn/i18n-react'; import type { MlJob } from '@elastic/elasticsearch/lib/api/types'; import type { TimeRangeBounds } from '@kbn/ml-time-buckets'; -import type { SingleMetricViewerServices } from '..'; +import type { SingleMetricViewerEmbeddableInput } from '..'; import { SeriesControls } from '../../application/timeseriesexplorer/components/series_controls'; import { APP_STATE_ACTION, type TimeseriesexplorerActionType, } from '../../application/timeseriesexplorer/timeseriesexplorer_constants'; +import type { SingleMetricViewerEmbeddableCustomInput, MlEntity } from '..'; export interface SingleMetricViewerInitializerProps { bounds: TimeRangeBounds; defaultTitle: string; - initialInput?: SingleMetricViewerServices; + initialInput?: Partial; job: MlJob; - onCreate: (props: { - panelTitle: string; - functionDescription?: string; - selectedDetectorIndex: number; - selectedEntities: any; - }) => void; + onCreate: (props: Partial) => void; onCancel: () => void; } export const SingleMetricViewerInitializer: FC = ({ - bounds, defaultTitle, + bounds, initialInput, job, onCreate, onCancel, }) => { + const isNewJob = initialInput?.jobIds !== undefined && initialInput?.jobIds[0] !== job.job_id; + const [panelTitle, setPanelTitle] = useState(defaultTitle); - const [functionDescription, setFunctionDescription] = useState(); - const [selectedDetectorIndex, setSelectedDetectorIndex] = useState(0); - const [selectedEntities, setSelectedEntities] = useState(); + const [functionDescription, setFunctionDescription] = useState( + initialInput?.functionDescription + ); + // Reset detector index and entities if the job has changed + const [selectedDetectorIndex, setSelectedDetectorIndex] = useState( + !isNewJob && initialInput?.selectedDetectorIndex ? initialInput.selectedDetectorIndex : 0 + ); + const [selectedEntities, setSelectedEntities] = useState( + !isNewJob && initialInput?.selectedEntities ? initialInput.selectedEntities : undefined + ); const isPanelTitleValid = panelTitle.length > 0; - const handleStateUpdate = (action: TimeseriesexplorerActionType, payload: any) => { + const handleStateUpdate = ( + action: TimeseriesexplorerActionType, + payload: string | number | MlEntity + ) => { switch (action) { case APP_STATE_ACTION.SET_ENTITIES: - setSelectedEntities(payload); + setSelectedEntities(payload as MlEntity); break; case APP_STATE_ACTION.SET_FUNCTION_DESCRIPTION: - setFunctionDescription(payload); + setFunctionDescription(payload as string); break; case APP_STATE_ACTION.SET_DETECTOR_INDEX: - setSelectedDetectorIndex(payload); + setSelectedDetectorIndex(payload as number); break; default: break; diff --git a/x-pack/plugins/ml/public/embeddables/single_metric_viewer/single_metric_viewer_setup_flyout.tsx b/x-pack/plugins/ml/public/embeddables/single_metric_viewer/single_metric_viewer_setup_flyout.tsx index b20d032f5b907..11927e9bca377 100644 --- a/x-pack/plugins/ml/public/embeddables/single_metric_viewer/single_metric_viewer_setup_flyout.tsx +++ b/x-pack/plugins/ml/public/embeddables/single_metric_viewer/single_metric_viewer_setup_flyout.tsx @@ -10,18 +10,19 @@ import type { CoreStart } from '@kbn/core/public'; import { toMountPoint } from '@kbn/react-kibana-mount'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { getDefaultSingleMetricViewerPanelTitle } from './single_metric_viewer_embeddable'; -import type { SingleMetricViewerEmbeddableInput, SingleMetricViewerServices } from '..'; +import type { SingleMetricViewerEmbeddableInput } from '..'; import { resolveJobSelection } from '../common/resolve_job_selection'; import { SingleMetricViewerInitializer } from './single_metric_viewer_initializer'; import type { MlStartDependencies } from '../../plugin'; +import type { MlApiServices } from '../../application/services/ml_api_service'; export async function resolveEmbeddableSingleMetricViewerUserInput( coreStart: CoreStart, pluginStart: MlStartDependencies, - input: SingleMetricViewerServices + mlApiServices: MlApiServices, + input?: SingleMetricViewerEmbeddableInput ): Promise> { const { overlays, theme, i18n } = coreStart; - const { mlApiServices } = input; const timefilter = pluginStart.data.query.timefilter.timefilter; return new Promise(async (resolve, reject) => { @@ -29,39 +30,30 @@ export async function resolveEmbeddableSingleMetricViewerUserInput( const { jobIds } = await resolveJobSelection( coreStart, pluginStart.data.dataViews, - undefined, + input?.jobIds ? input.jobIds : undefined, true ); - const title = getDefaultSingleMetricViewerPanelTitle(jobIds); + const title = input?.title ?? getDefaultSingleMetricViewerPanelTitle(jobIds); const { jobs } = await mlApiServices.getJobs({ jobId: jobIds.join(',') }); const modalSession = overlays.openModal( toMountPoint( { + onCreate={(explicitInput) => { modalSession.close(); resolve({ jobIds, - title: panelTitle, - functionDescription, - panelTitle, - selectedDetectorIndex, - selectedEntities, + ...explicitInput, }); }} onCancel={() => { diff --git a/x-pack/plugins/ml/public/embeddables/types.ts b/x-pack/plugins/ml/public/embeddables/types.ts index 14ee0e7c6d444..38032b7d80b91 100644 --- a/x-pack/plugins/ml/public/embeddables/types.ts +++ b/x-pack/plugins/ml/public/embeddables/types.ts @@ -17,6 +17,8 @@ import type { HasType, PublishesUnifiedSearch, PublishesViewMode, + PublishesWritablePanelTitle, + PublishingSubject, } from '@kbn/presentation-publishing'; import type { JobId } from '../../common/types/anomaly_detection_jobs'; import type { MlDependencies } from '../application/app'; @@ -33,6 +35,7 @@ import type { MlResultsService } from '../application/services/results_service'; import type { MlTimeSeriesSearchService } from '../application/timeseriesexplorer/timeseriesexplorer_utils/time_series_search_service'; import type { AnomalyExplorerChartsEmbeddableType, + AnomalySingleMetricViewerEmbeddableType, AnomalySwimLaneEmbeddableType, MlEmbeddableTypes, } from './constants'; @@ -41,6 +44,8 @@ export type MlEmbeddableBaseApi = Partial< HasParentApi & PublishesViewMode & PublishesUnifiedSearch >; +export type MlEntity = Record; + /** Manual input by the user */ export interface AnomalySwimlaneEmbeddableUserInput { jobIds: JobId[]; @@ -122,7 +127,7 @@ export interface SingleMetricViewerEmbeddableCustomInput { functionDescription?: string; panelTitle: string; selectedDetectorIndex: number; - selectedEntities: MlEntityField[]; + selectedEntities?: MlEntity; // Embeddable inputs which are not included in the default interface filters: Filter[]; query: Query; @@ -133,6 +138,21 @@ export interface SingleMetricViewerEmbeddableCustomInput { export type SingleMetricViewerEmbeddableInput = EmbeddableInput & SingleMetricViewerEmbeddableCustomInput; +export interface SingleMetricViewerComponentApi { + functionDescription?: PublishingSubject; + jobIds: PublishingSubject; + selectedDetectorIndex: PublishingSubject; + selectedEntities?: PublishingSubject; + + updateUserInput: (input: Partial) => void; +} + +export interface SingleMetricViewerEmbeddableApi + extends HasType, + PublishesWritablePanelTitle, + MlEmbeddableBaseApi, + SingleMetricViewerComponentApi {} + export interface AnomalyChartsServices { anomalyDetectorService: AnomalyDetectorService; anomalyExplorerService: AnomalyExplorerChartsService; diff --git a/x-pack/plugins/ml/public/ui_actions/edit_single_metric_viewer_panel_action.tsx b/x-pack/plugins/ml/public/ui_actions/edit_single_metric_viewer_panel_action.tsx new file mode 100644 index 0000000000000..bb953b739d712 --- /dev/null +++ b/x-pack/plugins/ml/public/ui_actions/edit_single_metric_viewer_panel_action.tsx @@ -0,0 +1,83 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import type { EmbeddableApiContext } from '@kbn/presentation-publishing'; +import type { UiActionsActionDefinition } from '@kbn/ui-actions-plugin/public'; +import { IncompatibleActionError } from '@kbn/ui-actions-plugin/public'; +import { isSingleMetricViewerEmbeddableContext } from './open_in_single_metric_viewer_action'; +import type { MlCoreSetup } from '../plugin'; +import { HttpService } from '../application/services/http_service'; +import type { + SingleMetricViewerEmbeddableInput, + SingleMetricViewerEmbeddableApi, +} from '../embeddables/types'; + +export const EDIT_SINGLE_METRIC_VIEWER_PANEL_ACTION = 'editSingleMetricViewerPanelAction'; + +export type EditSingleMetricViewerPanelActionContext = EmbeddableApiContext & { + embeddable: SingleMetricViewerEmbeddableApi; +}; + +export function createEditSingleMetricViewerPanelAction( + getStartServices: MlCoreSetup['getStartServices'] +): UiActionsActionDefinition { + return { + id: 'edit-single-metric-viewer', + type: EDIT_SINGLE_METRIC_VIEWER_PANEL_ACTION, + order: 50, + getIconType(): string { + return 'pencil'; + }, + getDisplayName: () => + i18n.translate('xpack.ml.actions.editSingleMetricViewerTitle', { + defaultMessage: 'Edit single metric viewer', + }), + async execute(context) { + if (!isSingleMetricViewerEmbeddableContext(context)) { + throw new IncompatibleActionError(); + } + + const [coreStart, pluginStart] = await getStartServices(); + + try { + const { resolveEmbeddableSingleMetricViewerUserInput } = await import( + '../embeddables/single_metric_viewer/single_metric_viewer_setup_flyout' + ); + + const { mlApiServicesProvider } = await import('../application/services/ml_api_service'); + const httpService = new HttpService(coreStart.http); + const mlApiServices = mlApiServicesProvider(httpService); + + const { jobIds, selectedEntities, selectedDetectorIndex, panelTitle } = context.embeddable; + + const result = await resolveEmbeddableSingleMetricViewerUserInput( + coreStart, + pluginStart, + mlApiServices, + { + jobIds: jobIds.getValue(), + selectedDetectorIndex: selectedDetectorIndex.getValue(), + selectedEntities: selectedEntities?.getValue(), + title: panelTitle?.getValue(), + } as SingleMetricViewerEmbeddableInput + ); + + context.embeddable.updateUserInput(result); + context.embeddable.setPanelTitle(result.panelTitle); + } catch (e) { + return Promise.reject(); + } + }, + async isCompatible(context: EmbeddableApiContext) { + return ( + isSingleMetricViewerEmbeddableContext(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 e55b1fc1c7b00..1d0a7d39c2871 100644 --- a/x-pack/plugins/ml/public/ui_actions/index.ts +++ b/x-pack/plugins/ml/public/ui_actions/index.ts @@ -16,11 +16,13 @@ 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 { createEditSingleMetricViewerPanelAction } from './edit_single_metric_viewer_panel_action'; import { createCategorizationADJobAction, createCategorizationADJobTrigger, } from './open_create_categorization_job_action'; import { createOpenInExplorerAction } from './open_in_anomaly_explorer_action'; +import { createOpenInSingleMetricViewerAction } from './open_in_single_metric_viewer_action'; import { createVisToADJobAction } from './open_vis_in_ml_action'; import { entityFieldSelectionTrigger, @@ -43,7 +45,13 @@ export function registerMlUiActions( ) { // Initialize actions const editSwimlanePanelAction = createEditSwimlanePanelAction(core.getStartServices); + const editSingleMetricViewerPanelAction = createEditSingleMetricViewerPanelAction( + core.getStartServices + ); const openInExplorerAction = createOpenInExplorerAction(core.getStartServices); + const openInSingleMetricViewerAction = createOpenInSingleMetricViewerAction( + core.getStartServices + ); const applyInfluencerFiltersAction = createApplyInfluencerFiltersAction(core.getStartServices); const applyEntityFieldFilterAction = createApplyEntityFieldFiltersAction(core.getStartServices); const applyTimeRangeSelectionAction = createApplyTimeRangeSelectionAction(core.getStartServices); @@ -59,8 +67,10 @@ export function registerMlUiActions( // Assign triggers uiActions.addTriggerAction(CONTEXT_MENU_TRIGGER, editSwimlanePanelAction); + uiActions.addTriggerAction(CONTEXT_MENU_TRIGGER, editSingleMetricViewerPanelAction); uiActions.addTriggerAction(CONTEXT_MENU_TRIGGER, editExplorerPanelAction); uiActions.addTriggerAction(CONTEXT_MENU_TRIGGER, openInExplorerAction); + uiActions.attachAction(CONTEXT_MENU_TRIGGER, openInSingleMetricViewerAction.id); uiActions.registerTrigger(swimLaneSelectionTrigger); uiActions.registerTrigger(entityFieldSelectionTrigger); @@ -69,6 +79,7 @@ export function registerMlUiActions( uiActions.addTriggerAction(SWIM_LANE_SELECTION_TRIGGER, applyInfluencerFiltersAction); uiActions.addTriggerAction(SWIM_LANE_SELECTION_TRIGGER, applyTimeRangeSelectionAction); uiActions.addTriggerAction(SWIM_LANE_SELECTION_TRIGGER, openInExplorerAction); + uiActions.addTriggerAction(SWIM_LANE_SELECTION_TRIGGER, openInSingleMetricViewerAction); uiActions.addTriggerAction(SWIM_LANE_SELECTION_TRIGGER, clearSelectionAction); uiActions.addTriggerAction(EXPLORER_ENTITY_FIELD_SELECTION_TRIGGER, applyEntityFieldFilterAction); uiActions.addTriggerAction(CONTEXT_MENU_TRIGGER, visToAdJobAction); diff --git a/x-pack/plugins/ml/public/ui_actions/open_in_single_metric_viewer_action.tsx b/x-pack/plugins/ml/public/ui_actions/open_in_single_metric_viewer_action.tsx new file mode 100644 index 0000000000000..7e13830ecdc5c --- /dev/null +++ b/x-pack/plugins/ml/public/ui_actions/open_in_single_metric_viewer_action.tsx @@ -0,0 +1,98 @@ +/* + * 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 { TimeRange } from '@kbn/es-query'; +import { i18n } from '@kbn/i18n'; +import { isPopulatedObject } from '@kbn/ml-is-populated-object'; +import { type EmbeddableApiContext, apiIsOfType } from '@kbn/presentation-publishing'; +import { + type UiActionsActionDefinition, + IncompatibleActionError, +} from '@kbn/ui-actions-plugin/public'; +import { ML_APP_LOCATOR, ML_PAGES } from '../../common/constants/locator'; +import type { MlEmbeddableBaseApi, SingleMetricViewerEmbeddableApi } from '../embeddables'; +import { ANOMALY_SINGLE_METRIC_VIEWER_EMBEDDABLE_TYPE } from '../embeddables'; + +import type { MlCoreSetup } from '../plugin'; + +export interface OpenInSingleMetricViewerActionContext extends EmbeddableApiContext { + embeddable: SingleMetricViewerEmbeddableApi; +} + +export const OPEN_IN_SINGLE_METRIC_VIEWER_ACTION = 'openInSingleMetricViewerAction'; + +export function isSingleMetricViewerEmbeddableContext( + arg: unknown +): arg is OpenInSingleMetricViewerActionContext { + return ( + isPopulatedObject(arg, ['embeddable']) && + apiIsOfType(arg.embeddable, ANOMALY_SINGLE_METRIC_VIEWER_EMBEDDABLE_TYPE) + ); +} + +const getTimeRange = (embeddable: MlEmbeddableBaseApi): TimeRange | undefined => { + return embeddable.timeRange$?.getValue() ?? embeddable.parentApi?.timeRange$?.getValue(); +}; + +export function createOpenInSingleMetricViewerAction( + getStartServices: MlCoreSetup['getStartServices'] +): UiActionsActionDefinition { + return { + id: 'open-in-single-metric-viewer', + type: OPEN_IN_SINGLE_METRIC_VIEWER_ACTION, + order: 100, + getIconType(): string { + return 'visLine'; + }, + getDisplayName() { + return i18n.translate('xpack.ml.actions.openInSingleMetricViewerTitle', { + defaultMessage: 'Open in Single Metric Viewer', + }); + }, + async getHref(context): Promise { + const [, pluginsStart] = await getStartServices(); + const locator = pluginsStart.share.url.locators.get(ML_APP_LOCATOR)!; + + if (isSingleMetricViewerEmbeddableContext(context)) { + const { embeddable } = context; + const { jobIds, query$, selectedEntities } = embeddable; + + return locator.getUrl( + { + page: ML_PAGES.SINGLE_METRIC_VIEWER, + // @ts-ignore entities is not compatible with SerializableRecord + pageState: { + timeRange: getTimeRange(embeddable), + refreshInterval: { + display: 'Off', + pause: true, + value: 0, + }, + jobIds: jobIds.getValue(), + query: query$?.getValue(), + entities: selectedEntities?.getValue(), + }, + }, + { absolute: true } + ); + } + }, + async execute(context) { + if (!isSingleMetricViewerEmbeddableContext(context)) { + throw new IncompatibleActionError(); + } + const [{ application }] = await getStartServices(); + const singleMetricViewerUrl = await this.getHref!(context); + if (singleMetricViewerUrl) { + await application.navigateToUrl(singleMetricViewerUrl!); + } + }, + async isCompatible(context: EmbeddableApiContext) { + return isSingleMetricViewerEmbeddableContext(context); + }, + }; +} diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 693f98cc170e3..90104a4663582 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -25569,7 +25569,6 @@ "xpack.ml.settings.filterLists.filterWithIdExistsErrorMessage": "Un filtre ayant l'ID {filterId} existe déjà", "xpack.ml.settings.filterLists.listHeader.filterListsContainsNotAllowedValuesDescription": "Les listes de filtres contiennent des valeurs que vous pouvez utiliser pour inclure ou exclure des événements dans l'analyse de Machine Learning. Vous pouvez utiliser une même liste de filtres dans plusieurs tâches.{br}{learnMoreLink}", "xpack.ml.settings.filterLists.listHeader.filterListsDescription": "{totalCount} en tout", - "xpack.ml.singleMetricViewerEmbeddable.title": "Graphique de visionneuse d'indicateur unique pour {jobIds}", "xpack.ml.splom.arrayFieldsWarningMessage": "{filteredDocsCount} sur {originalDocsCount} documents récupérés incluent des champs avec des tableaux de valeurs et ne peuvent pas être visualisés.", "xpack.ml.stepDefineForm.queryPlaceholderKql": "Rechercher par ex. {example}", "xpack.ml.stepDefineForm.queryPlaceholderLucene": "Rechercher par ex. {example}", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index b972c17909d9c..01cd7275c866a 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -25543,7 +25543,6 @@ "xpack.ml.settings.filterLists.filterWithIdExistsErrorMessage": "ID {filterId} のフィルターがすでに存在します", "xpack.ml.settings.filterLists.listHeader.filterListsContainsNotAllowedValuesDescription": "フィルターリストには、イベントを機械学習分析に含める、または除外するのに使用する値が含まれています。同じフィルターリストを複数ジョブに使用できます。{br}{learnMoreLink}", "xpack.ml.settings.filterLists.listHeader.filterListsDescription": "合計 {totalCount}", - "xpack.ml.singleMetricViewerEmbeddable.title": "{jobIds}のMLシングルメトリックビューアーグラフ", "xpack.ml.splom.arrayFieldsWarningMessage": "{originalDocsCount}件中{filteredDocsCount}件の取得されたドキュメントには配列の値のフィールドが含まれ、可視化できません。", "xpack.ml.stepDefineForm.queryPlaceholderKql": "{example}の検索", "xpack.ml.stepDefineForm.queryPlaceholderLucene": "{example}の検索", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 55fc9de23b316..b29ac4795d310 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -25580,7 +25580,6 @@ "xpack.ml.settings.filterLists.filterWithIdExistsErrorMessage": "ID 为 {filterId} 的筛选已存在", "xpack.ml.settings.filterLists.listHeader.filterListsContainsNotAllowedValuesDescription": "筛选列表包含可用于在 Machine Learning 分析中包括或排除事件的值。您可以在多个作业中使用相同的筛选列表。{br}{learnMoreLink}", "xpack.ml.settings.filterLists.listHeader.filterListsDescription": "合计 {totalCount} 个", - "xpack.ml.singleMetricViewerEmbeddable.title": "{jobIds} 的 ML Single Metric Viewer 图表", "xpack.ml.splom.arrayFieldsWarningMessage": "{originalDocsCount} 个提取的文档中有 {filteredDocsCount} 个包含具有值数组的字段,无法可视化。", "xpack.ml.stepDefineForm.queryPlaceholderKql": "搜索,如 {example})", "xpack.ml.stepDefineForm.queryPlaceholderLucene": "搜索,如 {example})",