diff --git a/x-pack/plugins/ml/public/alerting/job_selector.tsx b/x-pack/plugins/ml/public/alerting/job_selector.tsx index 5e1835ebfdfe1c..11cc0e67181e8b 100644 --- a/x-pack/plugins/ml/public/alerting/job_selector.tsx +++ b/x-pack/plugins/ml/public/alerting/job_selector.tsx @@ -202,6 +202,7 @@ export const JobSelectorControl: FC = ({ return ( > = ({ 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, share }, { mlApiServices }] = services; + const result = await resolveEmbeddableSingleMetricViewerUserInput( + coreStart, + parentApi, + uuid, + { data, share }, + mlApiServices, + { + ...serializeTitles(), + ...serializeSingleMetricViewerState(), + } + ); + + 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..4e2e338eee8eed 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,64 +6,103 @@ */ 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 { FormattedMessage } from '@kbn/i18n-react'; +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 { ML_PAGES } from '../../../common/constants/locator'; import { SeriesControls } from '../../application/timeseriesexplorer/components/series_controls'; import { APP_STATE_ACTION, type TimeseriesexplorerActionType, } from '../../application/timeseriesexplorer/timeseriesexplorer_constants'; +import { useMlLink } from '../../application/contexts/kibana'; +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; } export const SingleMetricViewerInitializer: FC = ({ - defaultTitle, bounds, initialInput, - job, onCreate, onCancel, + mlApiServices, }) => { - const isNewJob = initialInput?.jobIds !== undefined && initialInput?.jobIds[0] !== job.job_id; + const isMounted = useMountedState(); + const newJobUrl = useMlLink({ page: ML_PAGES.ANOMALY_DETECTION_CREATE_JOB }); + const [jobId, setJobId] = useState( + initialInput?.jobIds && initialInput?.jobIds[0] + ); + const titleManuallyChanged = useRef(!!initialInput?.title); - const [panelTitle, setPanelTitle] = useState(defaultTitle); + const [job, setJob] = useState(); + const [panelTitle, setPanelTitle] = useState(initialInput?.title ?? ''); 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 + initialInput?.selectedDetectorIndex ?? 0 ); const [selectedEntities, setSelectedEntities] = useState( - !isNewJob && initialInput?.selectedEntities ? initialInput.selectedEntities : undefined + initialInput?.selectedEntities ); - + const [errorMessage, setErrorMessage] = useState(); const isPanelTitleValid = panelTitle.length > 0; + useEffect( + function setUpPanel() { + async function fetchJob() { + const { jobs } = await mlApiServices.getJobs({ jobId }); + + if (isMounted() && jobs.length === 1) { + setJob(jobs[0]); + setErrorMessage(undefined); + } + } + + if (jobId) { + if (!titleManuallyChanged.current) { + setPanelTitle(getDefaultSingleMetricViewerPanelTitle(jobId)); + } + // Fetch job if a jobId has been selected and if there is no corresponding fetched job or the job selection has changed + if (mlApiServices && jobId && jobId !== job?.job_id) { + fetchJob().catch((error) => { + const errorMsg = extractErrorMessage(error); + setErrorMessage(errorMsg); + }); + } + } + }, + [isMounted, jobId, mlApiServices, panelTitle, job?.job_id] + ); + const handleStateUpdate = ( action: TimeseriesexplorerActionType, payload: string | number | MlEntity @@ -84,23 +123,33 @@ export const SingleMetricViewerInitializer: FC - - - - - + <> + + +

+ +

+
+
- + + { + setJobId(update?.jobIds && update?.jobIds[0]); + // Reset values when selected job has changed + setSelectedDetectorIndex(0); + setSelectedEntities(undefined); + setFunctionDescription(undefined); + }} + {...(errorMessage && { errors: [errorMessage] })} + /> } isInvalid={!isPanelTitleValid} + fullWidth > setPanelTitle(e.target.value)} + onChange={(e) => { + titleManuallyChanged.current = true; + setPanelTitle(e.target.value); + }} isInvalid={!isPanelTitleValid} + fullWidth /> - + {job?.job_id && jobId && jobId === job.job_id ? ( + + ) : 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..1f54330a4829f7 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 @@ -9,62 +9,73 @@ import React from 'react'; import type { CoreStart } from '@kbn/core/public'; import { toMountPoint } from '@kbn/react-kibana-mount'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; +import { tracksOverlays } from '@kbn/presentation-containers'; import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; +import type { SharePluginStart } from '@kbn/share-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, + parentApi: unknown, + focusedPanelId: string, + services: { data: DataPublicPluginStart; share?: SharePluginStart }, mlApiServices: MlApiServices, input?: Partial -): Promise> { - const { overlays, ...startServices } = coreStart; +): Promise { + const { http, overlays, ...startServices } = coreStart; + const { data, share } = services; const timefilter = data.query.timefilter.timefilter; + const overlayTracker = tracksOverlays(parentApi) ? parentApi : undefined; 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); + overlayTracker?.clearOverlays(); }} onCancel={() => { - modalSession.close(); + flyoutSession.close(); reject(); + overlayTracker?.clearOverlays(); }} /> , startServices - ) + ), + { + type: 'push', + ownFocus: true, + size: 's', + onClose: () => { + flyoutSession.close(); + reject(); + }, + } ); + // Close the flyout when user navigates out of the current plugin + if (tracksOverlays(parentApi)) { + parentApi.openOverlay(flyoutSession, { + focusedPanelId, + }); + } } 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/create_single_metric_viewer.tsx b/x-pack/plugins/ml/public/ui_actions/create_single_metric_viewer.tsx index 209ef648f37216..55b3bdf44663ba 100644 --- a/x-pack/plugins/ml/public/ui_actions/create_single_metric_viewer.tsx +++ b/x-pack/plugins/ml/public/ui_actions/create_single_metric_viewer.tsx @@ -58,7 +58,7 @@ export function createAddSingleMetricViewerPanelAction( const presentationContainerParent = await parentApiIsCompatible(context.embeddable); if (!presentationContainerParent) throw new IncompatibleActionError(); - const [coreStart, { data }] = await getStartServices(); + const [coreStart, { data, share }] = await getStartServices(); try { const { resolveEmbeddableSingleMetricViewerUserInput } = await import( @@ -70,7 +70,9 @@ export function createAddSingleMetricViewerPanelAction( const initialState = await resolveEmbeddableSingleMetricViewerUserInput( coreStart, - data, + context.embeddable, + context.embeddable.uuid, + { data, share }, mlApiServices ); 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 41cdc5dcf6c4fe..08fda084b413a9 100644 --- a/x-pack/plugins/ml/public/ui_actions/index.ts +++ b/x-pack/plugins/ml/public/ui_actions/index.ts @@ -16,7 +16,6 @@ import { createApplyTimeRangeSelectionAction } from './apply_time_range_action'; import { createClearSelectionAction } from './clear_selection_action'; import { createAddSwimlanePanelAction } from './create_swim_lane'; import { createEditAnomalyChartsPanelAction } from './edit_anomaly_charts_panel_action'; -import { createEditSingleMetricViewerPanelAction } from './edit_single_metric_viewer_panel_action'; import { createAddSingleMetricViewerPanelAction } from './create_single_metric_viewer'; import { createCategorizationADJobAction, @@ -48,9 +47,6 @@ export function registerMlUiActions( core.getStartServices ); const addSwimlanePanelAction = createAddSwimlanePanelAction(core.getStartServices); - const editSingleMetricViewerPanelAction = createEditSingleMetricViewerPanelAction( - core.getStartServices - ); const openInExplorerAction = createOpenInExplorerAction(core.getStartServices); const openInSingleMetricViewerAction = createOpenInSingleMetricViewerAction( core.getStartServices @@ -71,7 +67,6 @@ export function registerMlUiActions( // Assign triggers uiActions.addTriggerAction('ADD_PANEL_TRIGGER', addSingleMetricViewerPanelAction); uiActions.addTriggerAction('ADD_PANEL_TRIGGER', addSwimlanePanelAction); - 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); diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 845fe18530a5a6..dd4e29faf5bff2 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -27792,7 +27792,6 @@ "xpack.ml.severitySelector.formControlAriaLabel": "Sélectionner le seuil de sévérité", "xpack.ml.severitySelector.formControlLabel": "Sévérité", "xpack.ml.singleMetricViewerEmbeddable.panelTitleLabel": "Titre du panneau", - "xpack.ml.singleMetricViewerEmbeddable.setupModal.cancelButtonLabel": "Annuler", "xpack.ml.singleMetricViewerEmbeddable.setupModal.confirmButtonLabel": "Confirmer les configurations", "xpack.ml.SingleMetricViewerEmbeddable.setupModal.title": "Configuration de la visionneuse d'indicateur unique", "xpack.ml.singleMetricViewerPageLabel": "Single Metric Viewer (Visionneuse d'indicateur unique)", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 351327102a5f18..2c2370a0e80bd3 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -27765,7 +27765,6 @@ "xpack.ml.severitySelector.formControlAriaLabel": "重要度のしきい値を選択", "xpack.ml.severitySelector.formControlLabel": "深刻度", "xpack.ml.singleMetricViewerEmbeddable.panelTitleLabel": "パネルタイトル", - "xpack.ml.singleMetricViewerEmbeddable.setupModal.cancelButtonLabel": "キャンセル", "xpack.ml.singleMetricViewerEmbeddable.setupModal.confirmButtonLabel": "構成を確認", "xpack.ml.SingleMetricViewerEmbeddable.setupModal.title": "シングルメトリックビューアー構成", "xpack.ml.singleMetricViewerPageLabel": "シングルメトリックビューアー", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 69f740ad5ac0ed..f012ea9199349c 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -27803,7 +27803,6 @@ "xpack.ml.severitySelector.formControlAriaLabel": "选择严重性阈值", "xpack.ml.severitySelector.formControlLabel": "严重性", "xpack.ml.singleMetricViewerEmbeddable.panelTitleLabel": "面板标题", - "xpack.ml.singleMetricViewerEmbeddable.setupModal.cancelButtonLabel": "取消", "xpack.ml.singleMetricViewerEmbeddable.setupModal.confirmButtonLabel": "确认配置", "xpack.ml.SingleMetricViewerEmbeddable.setupModal.title": "Single Metric Viewer 配置", "xpack.ml.singleMetricViewerPageLabel": "Single Metric Viewer", diff --git a/x-pack/test/functional/apps/ml/anomaly_detection_integrations/single_metric_viewer_dashboard_embeddables.ts b/x-pack/test/functional/apps/ml/anomaly_detection_integrations/single_metric_viewer_dashboard_embeddables.ts index 1caa425d80ee4b..26905bb9e2a550 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection_integrations/single_metric_viewer_dashboard_embeddables.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection_integrations/single_metric_viewer_dashboard_embeddables.ts @@ -67,15 +67,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('can select jobs', async () => { - await ml.dashboardJobSelectionTable.setRowRadioButtonState( - testData.jobConfig.job_id, - true - ); - await ml.dashboardJobSelectionTable.applyJobSelection(); + await ml.alerting.selectJobs([testData.jobConfig.job_id]); + await ml.alerting.assertJobSelection([testData.jobConfig.job_id]); }); it('can configure single metric viewer panel', async () => { - await ml.dashboardEmbeddables.assertSingleMetricViewerEmbeddableInitializerExists(); await ml.singleMetricViewer.assertDetectorInputExist(); await ml.singleMetricViewer.assertDetectorInputValue( testData.expected.detectorInputValue diff --git a/x-pack/test/functional/services/ml/dashboard_embeddables.ts b/x-pack/test/functional/services/ml/dashboard_embeddables.ts index 685a1a5755b184..cf403bab147d84 100644 --- a/x-pack/test/functional/services/ml/dashboard_embeddables.ts +++ b/x-pack/test/functional/services/ml/dashboard_embeddables.ts @@ -25,14 +25,6 @@ export function MachineLearningDashboardEmbeddablesProvider( }); }, - async assertSingleMetricViewerEmbeddableInitializerExists() { - await retry.tryForTime(10 * 1000, async () => { - await testSubjects.existOrFail('mlSingleMetricViewerEmbeddableInitializer', { - timeout: 1000, - }); - }); - }, - async assertSingleMetricViewerEmbeddableInitializerNotExists() { await retry.tryForTime(10 * 1000, async () => { await testSubjects.missingOrFail('mlSingleMetricViewerEmbeddableInitializer', { @@ -133,11 +125,11 @@ export function MachineLearningDashboardEmbeddablesProvider( if (mlEmbeddableType === 'ml_single_metric_viewer') { await dashboardAddPanel.clickAddNewPanelFromUIActionLink('Single metric viewer'); + await testSubjects.existOrFail('mlAnomalyJobSelectionControls', { timeout: 2000 }); } else { await dashboardAddPanel.clickAddNewEmbeddableLink(mlEmbeddableType); + await mlDashboardJobSelectionTable.assertJobSelectionTableExists(); } - - await mlDashboardJobSelectionTable.assertJobSelectionTableExists(); }); }, };