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 84fbb702f121bf..06e57b138386cb 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 @@ -74,6 +74,7 @@ const getDefaultFieldConfig = ( interface SeriesControlsProps { appStateHandler: Function; bounds: any; + direction?: 'column' | 'row'; functionDescription?: string; job?: CombinedJob | MlJob; selectedDetectorIndex: number; @@ -89,6 +90,7 @@ export const SeriesControls: FC> = ({ appStateHandler, bounds, children, + direction = 'row', functionDescription, job, selectedDetectorIndex, @@ -297,7 +299,7 @@ export const SeriesControls: FC> = ({ return (
- + true, + getTypeDisplayName: () => + i18n.translate('xpack.ml.singleMetricViewerEmbeddable.typeDisplayName', { + defaultMessage: 'Single metric viewer', + }), + onEdit: async () => { + try { + const { resolveEmbeddableSingleMetricViewerUserInput } = await import( + './single_metric_viewer_setup_flyout' + ); + const [coreStart, { data }, { mlApiServices }] = services; + const result = await resolveEmbeddableSingleMetricViewerUserInput( + coreStart, + data, + mlApiServices + ); + + singleMetricViewerControlsApi.updateUserInput(result); + } catch (e) { + return Promise.reject(); + } + }, ...titlesApi, ...timeRangeApi, ...singleMetricViewerControlsApi, 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 ba7171badeadf1..3570c9e40ea692 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 @@ -6,51 +6,64 @@ */ import type { FC } from 'react'; -import React, { useState } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import { EuiButton, EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, EuiForm, EuiFormRow, - EuiModalBody, - EuiModalFooter, - EuiModalHeader, - EuiModalHeaderTitle, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlyoutHeader, + EuiTitle, EuiFieldText, - EuiModal, EuiSpacer, } from '@elastic/eui'; +import type { ToastsStart } from '@kbn/core/public'; import { FormattedMessage } from '@kbn/i18n-react'; +import { i18n } from '@kbn/i18n'; +import useMountedState from 'react-use/lib/useMountedState'; +import { extractErrorMessage } from '@kbn/ml-error-utils'; import type { MlJob } from '@elastic/elasticsearch/lib/api/types'; import type { TimeRangeBounds } from '@kbn/ml-time-buckets'; +import type { MlApiServices } from '../../application/services/ml_api_service'; import type { SingleMetricViewerEmbeddableInput } from '..'; import { SeriesControls } from '../../application/timeseriesexplorer/components/series_controls'; import { APP_STATE_ACTION, type TimeseriesexplorerActionType, } from '../../application/timeseriesexplorer/timeseriesexplorer_constants'; +import { JobSelectorControl } from '../../alerting/job_selector'; import type { SingleMetricViewerEmbeddableUserInput, MlEntity } from '..'; +import { getDefaultSingleMetricViewerPanelTitle } from './get_default_panel_title'; export interface SingleMetricViewerInitializerProps { bounds: TimeRangeBounds; - defaultTitle: string; initialInput?: Partial; - job: MlJob; - onCreate: (props: Partial) => void; + mlApiServices: MlApiServices; + onCreate: (props: SingleMetricViewerEmbeddableUserInput) => void; onCancel: () => void; + toasts: ToastsStart; } export const SingleMetricViewerInitializer: FC = ({ - defaultTitle, bounds, initialInput, - job, onCreate, onCancel, + mlApiServices, + toasts, }) => { - const isNewJob = initialInput?.jobIds !== undefined && initialInput?.jobIds[0] !== job.job_id; + const isMounted = useMountedState(); + const titleManuallyChanged = useRef(false); - const [panelTitle, setPanelTitle] = useState(defaultTitle); + const [jobIds, setJobIds] = useState(initialInput?.jobIds ?? []); + const [job, setJob] = useState(); + const isNewJob = + initialInput?.jobIds !== undefined && jobIds.length && initialInput?.jobIds[0] !== jobIds[0]; + const [panelTitle, setPanelTitle] = useState(initialInput?.title ?? ''); const [functionDescription, setFunctionDescription] = useState( initialInput?.functionDescription ); @@ -61,9 +74,42 @@ export const SingleMetricViewerInitializer: FC( !isNewJob && initialInput?.selectedEntities ? initialInput.selectedEntities : undefined ); - const isPanelTitleValid = panelTitle.length > 0; + useEffect( + function setUpPanel() { + if (isMounted()) { + async function fetchJob() { + const { jobs } = await mlApiServices.getJobs({ jobId: jobIds.join(',') }); + if (jobs.length > 0) { + setJob(jobs[0]); + } + } + + if (jobIds.length === 1) { + if (!titleManuallyChanged.current) { + setPanelTitle(getDefaultSingleMetricViewerPanelTitle(jobIds)); + } + if (mlApiServices) { + fetchJob().catch((error) => { + toasts.addDanger( + i18n.translate( + 'xpack.ml.SingleMetricViewerEmbeddable.setupModal.fetchJobErrorNotificationMessage', + { + defaultMessage: + 'The following error occurred loading the selected job. {error}', + values: { error: extractErrorMessage(error) }, + } + ) + ); + }); + } + } + } + }, + [isMounted, jobIds, mlApiServices, toasts] + ); + const handleStateUpdate = ( action: TimeseriesexplorerActionType, payload: string | number | MlEntity @@ -84,23 +130,28 @@ export const SingleMetricViewerInitializer: FC - - - - - + <> + + +

+ +

+
+
- + + { + setJobIds([...(update?.jobIds ?? []), ...(update?.groupIds ?? [])]); + }} + /> } isInvalid={!isPanelTitleValid} + fullWidth > setPanelTitle(e.target.value)} + onChange={(e) => { + titleManuallyChanged.current = true; + setPanelTitle(e.target.value); + }} isInvalid={!isPanelTitleValid} + fullWidth /> - + {job && jobIds.length ? ( + + ) : null} - - - - - - - - - - - - + + + + + + + + + + + + + + + + ); }; 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 4023313bf8fa8d..3f426a9badd45d 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 @@ -11,32 +11,21 @@ import { toMountPoint } from '@kbn/react-kibana-mount'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; import type { SingleMetricViewerEmbeddableUserInput, SingleMetricViewerEmbeddableInput } from '..'; -import { resolveJobSelection } from '../common/resolve_job_selection'; import { SingleMetricViewerInitializer } from './single_metric_viewer_initializer'; import type { MlApiServices } from '../../application/services/ml_api_service'; -import { getDefaultSingleMetricViewerPanelTitle } from './get_default_panel_title'; export async function resolveEmbeddableSingleMetricViewerUserInput( coreStart: CoreStart, data: DataPublicPluginStart, mlApiServices: MlApiServices, input?: Partial -): Promise> { - const { overlays, ...startServices } = coreStart; +): Promise { + const { http, overlays, ...startServices } = coreStart; const timefilter = data.query.timefilter.timefilter; return new Promise(async (resolve, reject) => { try { - const { jobIds } = await resolveJobSelection( - coreStart, - data.dataViews, - input?.jobIds ? input.jobIds : undefined, - true - ); - const title = input?.title ?? getDefaultSingleMetricViewerPanelTitle(jobIds[0]); - const { jobs } = await mlApiServices.getJobs({ jobId: jobIds.join(',') }); - - const modalSession = overlays.openModal( + const flyoutSession = overlays.openFlyout( toMountPoint( { - modalSession.close(); - resolve({ - jobIds, - ...explicitInput, - }); + flyoutSession.close(); + resolve(explicitInput); }} onCancel={() => { - modalSession.close(); + flyoutSession.close(); reject(); }} /> , startServices - ) + ), + { + type: 'push', + ownFocus: true, + size: 's', + onClose: () => { + flyoutSession.close(); + reject(); + }, + } ); } catch (error) { reject(error); diff --git a/x-pack/plugins/ml/public/embeddables/types.ts b/x-pack/plugins/ml/public/embeddables/types.ts index 7e731e863239af..34e0b1afb32659 100644 --- a/x-pack/plugins/ml/public/embeddables/types.ts +++ b/x-pack/plugins/ml/public/embeddables/types.ts @@ -18,6 +18,7 @@ import type { Filter, Query, TimeRange } from '@kbn/es-query'; import type { MlEntityField } from '@kbn/ml-anomaly-utils'; import type { EmbeddableApiContext, + HasEditCapabilities, HasParentApi, HasType, PublishesUnifiedSearch, @@ -154,6 +155,7 @@ export interface SingleMetricViewerEmbeddableState export type SingleMetricViewerEmbeddableApi = MlEmbeddableBaseApi & PublishesWritablePanelTitle & + HasEditCapabilities & SingleMetricViewerComponentApi; /** 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 deleted file mode 100644 index 2122cec2877e43..00000000000000 --- a/x-pack/plugins/ml/public/ui_actions/edit_single_metric_viewer_panel_action.tsx +++ /dev/null @@ -1,83 +0,0 @@ -/* - * 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, { data }] = 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, - data, - mlApiServices, - { - jobIds: jobIds.getValue(), - selectedDetectorIndex: selectedDetectorIndex.getValue(), - selectedEntities: selectedEntities?.getValue(), - title: panelTitle?.getValue(), - } as SingleMetricViewerEmbeddableInput - ); - - context.embeddable.updateUserInput(result as SingleMetricViewerEmbeddableInput); - context.embeddable.setPanelTitle(result.panelTitle); - } catch (e) { - return Promise.reject(); - } - }, - async isCompatible(context: EmbeddableApiContext) { - return ( - isSingleMetricViewerEmbeddableContext(context) && - context.embeddable.parentApi?.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 15014205b8c830..cc0aece064c7c5 100644 --- a/x-pack/plugins/ml/public/ui_actions/index.ts +++ b/x-pack/plugins/ml/public/ui_actions/index.ts @@ -17,7 +17,6 @@ import { createClearSelectionAction } from './clear_selection_action'; import { createAddSwimlanePanelAction } from './create_swim_lane'; 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 { createAddSingleMetricViewerPanelAction } from './create_single_metric_viewer'; import { createCategorizationADJobAction, @@ -51,9 +50,6 @@ export function registerMlUiActions( ); const addSwimlanePanelAction = createAddSwimlanePanelAction(core.getStartServices); const editSwimlanePanelAction = createEditSwimlanePanelAction(core.getStartServices); - const editSingleMetricViewerPanelAction = createEditSingleMetricViewerPanelAction( - core.getStartServices - ); const openInExplorerAction = createOpenInExplorerAction(core.getStartServices); const openInSingleMetricViewerAction = createOpenInSingleMetricViewerAction( core.getStartServices @@ -75,7 +71,6 @@ export function registerMlUiActions( uiActions.addTriggerAction('ADD_PANEL_TRIGGER', addSingleMetricViewerPanelAction); uiActions.addTriggerAction('ADD_PANEL_TRIGGER', addSwimlanePanelAction); 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);