diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseriesexplorer_controls/index.ts b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseriesexplorer_controls/index.ts new file mode 100644 index 0000000000000..cb611ea7f6afc --- /dev/null +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseriesexplorer_controls/index.ts @@ -0,0 +1,8 @@ +/* + * 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. + */ + +export { TimeSeriesExplorerControls } from './timeseriesexplorer_controls'; diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseriesexplorer_controls/timeseriesexplorer_controls.tsx b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseriesexplorer_controls/timeseriesexplorer_controls.tsx new file mode 100644 index 0000000000000..78c4add1026b7 --- /dev/null +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseriesexplorer_controls/timeseriesexplorer_controls.tsx @@ -0,0 +1,230 @@ +/* + * 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 { FC } from 'react'; +import React, { useCallback, useState } from 'react'; +import type { EuiContextMenuPanelDescriptor } from '@elastic/eui'; +import { + EuiButtonIcon, + EuiCheckbox, + EuiContextMenu, + EuiFlexGroup, + EuiFlexItem, + EuiPopover, + htmlIdGenerator, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import type { SaveModalDashboardProps } from '@kbn/presentation-util-plugin/public'; +import { + LazySavedObjectSaveModalDashboard, + withSuspense, +} from '@kbn/presentation-util-plugin/public'; +import type { JobId } from '../../../../../common/types/anomaly_detection_jobs/job'; +import { useMlKibana } from '../../../contexts/kibana'; +import { getDefaultSingleMetricViewerPanelTitle } from '../../../../embeddables/single_metric_viewer/get_default_panel_title'; +import type { MlEntity } from '../../../../embeddables'; +import { ANOMALY_SINGLE_METRIC_VIEWER_EMBEDDABLE_TYPE } from '../../../../embeddables/constants'; +import type { SingleMetricViewerEmbeddableState } from '../../../../embeddables/types'; + +interface Props { + selectedDetectorIndex: number; + selectedEntities?: MlEntity; + selectedJobId: JobId; + showAnnotationsCheckbox: boolean; + showAnnotations: boolean; + showForecastCheckbox: boolean; + showForecast: boolean; + showModelBoundsCheckbox: boolean; + showModelBounds: boolean; + onShowModelBoundsChange: () => void; + onShowAnnotationsChange: () => void; + onShowForecastChange: () => void; +} + +const SavedObjectSaveModalDashboard = withSuspense(LazySavedObjectSaveModalDashboard); + +function getDefaultEmbeddablePanelConfig(jobId: JobId, queryString?: string) { + return { + title: getDefaultSingleMetricViewerPanelTitle(jobId).concat( + queryString ? `- ${queryString}` : '' + ), + id: htmlIdGenerator()(), + }; +} + +export const TimeSeriesExplorerControls: FC = ({ + selectedDetectorIndex, + selectedEntities, + selectedJobId, + showAnnotations, + showAnnotationsCheckbox, + showForecast, + showForecastCheckbox, + showModelBounds, + showModelBoundsCheckbox, + onShowAnnotationsChange, + onShowModelBoundsChange, + onShowForecastChange, +}) => { + const [isMenuOpen, setIsMenuOpen] = useState(false); + const [createInDashboard, setCreateInDashboard] = useState(false); + + const { + services: { + application: { capabilities }, + embeddable, + }, + } = useMlKibana(); + + const canEditDashboards = capabilities.dashboard?.createNew ?? false; + + const closePopoverOnAction = useCallback( + (actionCallback: Function) => { + return () => { + setIsMenuOpen(false); + actionCallback(); + }; + }, + [setIsMenuOpen] + ); + + const menuPanels: EuiContextMenuPanelDescriptor[] = [ + { + id: 0, + items: [ + { + name: ( + + ), + onClick: closePopoverOnAction(() => { + setCreateInDashboard(true); + }), + }, + ], + }, + ]; + + const onSaveCallback: SaveModalDashboardProps['onSave'] = useCallback( + ({ dashboardId, newTitle, newDescription }) => { + const stateTransfer = embeddable!.getStateTransfer(); + const config = getDefaultEmbeddablePanelConfig(selectedJobId); + + const embeddableInput: Partial = { + id: config.id, + title: newTitle, + description: newDescription, + jobIds: [selectedJobId], + selectedDetectorIndex, + selectedEntities, + }; + + const state = { + input: embeddableInput, + type: ANOMALY_SINGLE_METRIC_VIEWER_EMBEDDABLE_TYPE, + }; + + const path = dashboardId === 'new' ? '#/create' : `#/view/${dashboardId}`; + + stateTransfer.navigateToWithEmbeddablePackage('dashboards', { + state, + path, + }); + }, + [embeddable, selectedJobId, selectedDetectorIndex, selectedEntities] + ); + + return ( + <> + + {showModelBoundsCheckbox && ( + + + + )} + + {showAnnotationsCheckbox && ( + + + + )} + + {showForecastCheckbox && ( + + + {i18n.translate('xpack.ml.timeSeriesExplorer.showForecastLabel', { + defaultMessage: 'show forecast', + })} + + } + checked={showForecast} + onChange={onShowForecastChange} + /> + + )} + + {canEditDashboards ? ( + + + } + isOpen={isMenuOpen} + closePopover={setIsMenuOpen.bind(null, false)} + panelPaddingSize="none" + anchorPosition="downLeft" + > + + + + ) : null} + + {createInDashboard ? ( + setCreateInDashboard(false)} + onSave={onSaveCallback} + /> + ) : null} + + ); +}; diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js index e3f5bd0a3792c..8a53631cd7f22 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js @@ -26,7 +26,6 @@ import React, { createRef, Fragment } from 'react'; import { EuiCallOut, - EuiCheckbox, EuiFlexGroup, EuiFlexItem, EuiFormRow, @@ -88,6 +87,7 @@ import { TimeseriesexplorerChartDataError } from './components/timeseriesexplore import { ExplorerNoJobsSelected } from '../explorer/components'; import { getDataViewsAndIndicesWithGeoFields } from '../explorer/explorer_utils'; import { indexServiceFactory } from '../util/index_service'; +import { TimeSeriesExplorerControls } from './components/timeseriesexplorer_controls'; // Used to indicate the chart is being plotted across // all partition field values, where the cardinality of the field cannot be @@ -937,6 +937,7 @@ export class TimeSeriesExplorer extends React.Component { dateFormatTz, lastRefresh, selectedDetectorIndex, + selectedEntities, selectedJobId, } = this.props; @@ -1160,50 +1161,21 @@ export class TimeSeriesExplorer extends React.Component { - - {showModelBoundsCheckbox && ( - - - - )} - - {showAnnotationsCheckbox && ( - - - - )} - - {showForecastCheckbox && ( - - - {i18n.translate('xpack.ml.timeSeriesExplorer.showForecastLabel', { - defaultMessage: 'show forecast', - })} - - } - checked={showForecast} - onChange={this.toggleShowForecastHandler} - /> - - )} - + + { const { - services: { docLinks }, + services: { presentationUtil, docLinks }, } = useMlKibana(); + const PresentationContextProvider = presentationUtil?.ContextProvider ?? React.Fragment; const helpLink = docLinks.links.ml.anomalyDetection; return ( <> @@ -61,8 +62,7 @@ export const TimeSeriesExplorerPage: FC )} - - {children} + {children} diff --git a/x-pack/plugins/ml/public/embeddables/single_metric_viewer/get_default_panel_title.ts b/x-pack/plugins/ml/public/embeddables/single_metric_viewer/get_default_panel_title.ts index 9ffd506b37a29..8309d24ef7f82 100644 --- a/x-pack/plugins/ml/public/embeddables/single_metric_viewer/get_default_panel_title.ts +++ b/x-pack/plugins/ml/public/embeddables/single_metric_viewer/get_default_panel_title.ts @@ -8,8 +8,8 @@ import { i18n } from '@kbn/i18n'; import type { JobId } from '../../../common/types/anomaly_detection_jobs'; -export const getDefaultSingleMetricViewerPanelTitle = (jobIds: JobId[]) => +export const getDefaultSingleMetricViewerPanelTitle = (jobId: JobId) => i18n.translate('xpack.ml.singleMetricViewerEmbeddable.title', { - defaultMessage: 'ML single metric viewer chart for {jobIds}', - values: { jobIds: jobIds.join(', ') }, + defaultMessage: 'ML single metric viewer chart for {jobId}', + values: { jobId }, }); 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 6473d4818d93f..4023313bf8fa8 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 @@ -33,7 +33,7 @@ export async function resolveEmbeddableSingleMetricViewerUserInput( input?.jobIds ? input.jobIds : undefined, true ); - const title = input?.title ?? getDefaultSingleMetricViewerPanelTitle(jobIds); + const title = input?.title ?? getDefaultSingleMetricViewerPanelTitle(jobIds[0]); const { jobs } = await mlApiServices.getJobs({ jobId: jobIds.join(',') }); const modalSession = overlays.openModal(