diff --git a/x-pack/plugins/ml/public/application/app.tsx b/x-pack/plugins/ml/public/application/app.tsx index cfc9826e4280b..c83c965629fb1 100644 --- a/x-pack/plugins/ml/public/application/app.tsx +++ b/x-pack/plugins/ml/public/application/app.tsx @@ -22,7 +22,6 @@ import useObservable from 'react-use/lib/useObservable'; import type { ExperimentalFeatures, MlFeatures } from '../../common/constants/app'; import { ML_STORAGE_KEYS } from '../../common/types/storage'; import type { MlSetupDependencies, MlStartDependencies } from '../plugin'; -import { clearCache, setDependencyCache } from './util/dependency_cache'; import { setLicenseCache } from './license'; import { MlRouter } from './routing'; import type { PageDependencies } from './routing/router'; @@ -97,7 +96,7 @@ const App: FC = ({ uiActions: deps.uiActions, unifiedSearch: deps.unifiedSearch, usageCollection: deps.usageCollection, - mlServices: getMlGlobalServices(coreStart.http, deps.data.dataViews, deps.usageCollection), + mlServices: getMlGlobalServices(coreStart, deps.data.dataViews, deps.usageCollection), }; }, [deps, coreStart]); @@ -160,18 +159,6 @@ export const renderApp = ( mlFeatures: MlFeatures, experimentalFeatures: ExperimentalFeatures ) => { - setDependencyCache({ - timefilter: deps.data.query.timefilter, - fieldFormats: deps.fieldFormats, - config: coreStart.uiSettings!, - docLinks: coreStart.docLinks!, - toastNotifications: coreStart.notifications.toasts, - recentlyAccessed: coreStart.chrome!.recentlyAccessed, - application: coreStart.application, - http: coreStart.http, - maps: deps.maps, - }); - appMountParams.onAppLeave((actions) => actions.default()); ReactDOM.render( @@ -187,7 +174,6 @@ export const renderApp = ( ); return () => { - clearCache(); ReactDOM.unmountComponentAtNode(appMountParams.element); deps.data.search.session.clear(); }; diff --git a/x-pack/plugins/ml/public/application/capabilities/check_capabilities.ts b/x-pack/plugins/ml/public/application/capabilities/check_capabilities.ts index e7102560e0e02..ee7868adb41fa 100644 --- a/x-pack/plugins/ml/public/application/capabilities/check_capabilities.ts +++ b/x-pack/plugins/ml/public/application/capabilities/check_capabilities.ts @@ -197,10 +197,11 @@ export function checkGetManagementMlJobsResolver({ mlCapabilities }: MlGlobalSer } export function checkCreateJobsCapabilitiesResolver( + mlApiServices: MlApiServices, redirectToJobsManagementPage: () => Promise ): Promise { return new Promise((resolve, reject) => { - getCapabilities() + getCapabilities(mlApiServices) .then(async ({ capabilities, isPlatinumOrTrialLicense }) => { _capabilities = capabilities; // if the license is basic (isPlatinumOrTrialLicense === false) then do not redirect, diff --git a/x-pack/plugins/ml/public/application/capabilities/get_capabilities.ts b/x-pack/plugins/ml/public/application/capabilities/get_capabilities.ts index ed4725b9ffde3..2f7ad039b2700 100644 --- a/x-pack/plugins/ml/public/application/capabilities/get_capabilities.ts +++ b/x-pack/plugins/ml/public/application/capabilities/get_capabilities.ts @@ -5,10 +5,10 @@ * 2.0. */ -import { ml } from '../services/ml_api_service'; +import type { MlApiServices } from '../services/ml_api_service'; import type { MlCapabilitiesResponse } from '../../../common/types/capabilities'; -export function getCapabilities(): Promise { +export function getCapabilities(ml: MlApiServices): Promise { return ml.checkMlCapabilities(); } diff --git a/x-pack/plugins/ml/public/application/components/annotations/annotation_flyout/index.test.tsx b/x-pack/plugins/ml/public/application/components/annotations/annotation_flyout/index.test.tsx index ad78c4e5f0270..81cafabbae827 100644 --- a/x-pack/plugins/ml/public/application/components/annotations/annotation_flyout/index.test.tsx +++ b/x-pack/plugins/ml/public/application/components/annotations/annotation_flyout/index.test.tsx @@ -9,7 +9,9 @@ import useObservable from 'react-use/lib/useObservable'; import mockAnnotations from '../annotations_table/__mocks__/mock_annotations.json'; import React from 'react'; import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import type { CoreStart } from '@kbn/core/public'; import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; +import { createKibanaReactContext } from '@kbn/kibana-react-plugin/public'; import type { Annotation } from '../../../../../common/types/annotations'; import { AnnotationUpdatesService } from '../../../services/annotations_service'; @@ -17,9 +19,17 @@ import { AnnotationUpdatesService } from '../../../services/annotations_service' import { AnnotationFlyout } from '.'; import { MlAnnotationUpdatesContext } from '../../../contexts/ml/ml_annotation_updates_context'; -jest.mock('../../../util/dependency_cache', () => ({ - getToastNotifications: () => ({ addSuccess: jest.fn(), addDanger: jest.fn() }), -})); +const kibanaReactContextMock = createKibanaReactContext({ + mlServices: { + mlApiServices: { + annotations: { + indexAnnotation: jest.fn().mockResolvedValue({}), + deleteAnnotation: jest.fn().mockResolvedValue({}), + }, + }, + }, + notifications: { toasts: { addDanger: jest.fn(), addSuccess: jest.fn() } }, +} as unknown as Partial); const MlAnnotationUpdatesContextProvider = ({ annotationUpdatesService, @@ -30,7 +40,9 @@ const MlAnnotationUpdatesContextProvider = ({ }) => { return ( - {children} + + {children} + ); }; diff --git a/x-pack/plugins/ml/public/application/components/annotations/annotation_flyout/index.tsx b/x-pack/plugins/ml/public/application/components/annotations/annotation_flyout/index.tsx index 5b14f6d350873..3bea429042299 100644 --- a/x-pack/plugins/ml/public/application/components/annotations/annotation_flyout/index.tsx +++ b/x-pack/plugins/ml/public/application/components/annotations/annotation_flyout/index.tsx @@ -30,6 +30,7 @@ import { import type { CommonProps } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; +import { context } from '@kbn/kibana-react-plugin/public'; import { type MlPartitionFieldsType, ML_PARTITION_FIELDS } from '@kbn/ml-anomaly-utils'; import { ANNOTATION_MAX_LENGTH_CHARS, @@ -42,13 +43,12 @@ import type { import { annotationsRefreshed } from '../../../services/annotations_service'; import { AnnotationDescriptionList } from '../annotation_description_list'; import { DeleteAnnotationModal } from '../delete_annotation_modal'; -import { ml } from '../../../services/ml_api_service'; -import { getToastNotifications } from '../../../util/dependency_cache'; import { getAnnotationFieldName, getAnnotationFieldValue, } from '../../../../../common/types/annotations'; import { MlAnnotationUpdatesContext } from '../../../contexts/ml/ml_annotation_updates_context'; +import type { MlKibanaReactContextValue } from '../../../contexts/kibana'; interface ViewableDetector { index: number; @@ -78,6 +78,9 @@ interface State { } export class AnnotationFlyoutUI extends Component { + static contextType = context; + declare context: MlKibanaReactContextValue; + private deletionInProgress = false; public state: State = { @@ -126,7 +129,6 @@ export class AnnotationFlyoutUI extends Component { if (this.deletionInProgress) return; const { annotationState } = this.state; - const toastNotifications = getToastNotifications(); if (annotationState === null || annotationState._id === undefined) { return; @@ -134,6 +136,8 @@ export class AnnotationFlyoutUI extends Component { this.deletionInProgress = true; + const ml = this.context.services.mlServices.mlApiServices; + const toastNotifications = this.context.services.notifications.toasts; try { await ml.annotations.deleteAnnotation(annotationState._id); toastNotifications.addSuccess( @@ -237,11 +241,12 @@ export class AnnotationFlyoutUI extends Component { annotation.event = annotation.event ?? ANNOTATION_EVENT_USER; annotationUpdatesService.setValue(null); + const ml = this.context.services.mlServices.mlApiServices; + const toastNotifications = this.context.services.notifications.toasts; ml.annotations .indexAnnotation(annotation) .then(() => { annotationsRefreshed(); - const toastNotifications = getToastNotifications(); if (typeof annotation._id === 'undefined') { toastNotifications.addSuccess( i18n.translate( @@ -265,7 +270,6 @@ export class AnnotationFlyoutUI extends Component { } }) .catch((resp) => { - const toastNotifications = getToastNotifications(); if (typeof annotation._id === 'undefined') { toastNotifications.addDanger( i18n.translate( 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 9436209c5f3bb..d9d98029c3eee 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 @@ -29,10 +29,11 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; +import { withKibana } from '@kbn/kibana-react-plugin/public'; import { addItemToRecentlyAccessed } from '../../../util/recently_accessed'; -import { ml } from '../../../services/ml_api_service'; -import { mlJobService } from '../../../services/job_service'; +import { mlJobServiceFactory } from '../../../services/job_service'; +import { toastNotificationServiceProvider } from '../../../services/toast_notification_service'; import { mlTableService } from '../../../services/table_service'; import { ANNOTATIONS_TABLE_DEFAULT_QUERY_SIZE } from '../../../../../common/constants/search'; import { @@ -45,7 +46,6 @@ import { ANNOTATION_EVENT_USER, ANNOTATION_EVENT_DELAYED_DATA, } from '../../../../../common/constants/annotations'; -import { withKibana } from '@kbn/kibana-react-plugin/public'; import { ML_APP_LOCATOR, ML_PAGES } from '../../../../../common/constants/locator'; import { timeFormatter } from '@kbn/ml-date-utils'; import { MlAnnotationUpdatesContext } from '../../../contexts/ml/ml_annotation_updates_context'; @@ -90,10 +90,8 @@ class AnnotationsTableUI extends Component { queryText: `event:(${ANNOTATION_EVENT_USER} or ${ANNOTATION_EVENT_DELAYED_DATA})`, searchError: undefined, jobId: - Array.isArray(this.props.jobs) && - this.props.jobs.length > 0 && - this.props.jobs[0] !== undefined - ? this.props.jobs[0].job_id + Array.isArray(props.jobs) && props.jobs.length > 0 && props.jobs[0] !== undefined + ? props.jobs[0].job_id : undefined, datafeedFlyoutVisible: false, modelSnapshot: null, @@ -103,6 +101,10 @@ class AnnotationsTableUI extends Component { this.sorting = { sort: { field: 'timestamp', direction: 'asc' }, }; + this.mlJobService = mlJobServiceFactory( + toastNotificationServiceProvider(props.kibana.services.notifications.toasts), + props.kibana.services.mlServices.mlApiServices + ); } getAnnotations() { @@ -113,6 +115,8 @@ class AnnotationsTableUI extends Component { isLoading: true, }); + const ml = this.props.kibana.services.mlServices.mlApiServices; + if (dataCounts.processed_record_count > 0) { // Load annotations for the selected job. ml.annotations @@ -177,7 +181,7 @@ class AnnotationsTableUI extends Component { } } - return mlJobService.getJob(jobId); + return this.mlJobService.getJob(jobId); } annotationsRefreshSubscription = null; diff --git a/x-pack/plugins/ml/public/application/components/anomalies_table/anomalies_table.js b/x-pack/plugins/ml/public/application/components/anomalies_table/anomalies_table.js index 220497ba13b10..80ee0f7a64820 100644 --- a/x-pack/plugins/ml/public/application/components/anomalies_table/anomalies_table.js +++ b/x-pack/plugins/ml/public/application/components/anomalies_table/anomalies_table.js @@ -26,7 +26,6 @@ import { AnomalyDetails } from './anomaly_details'; import { mlTableService } from '../../services/table_service'; import { RuleEditorFlyout } from '../rule_editor'; -import { ml } from '../../services/ml_api_service'; import { INFLUENCERS_LIMIT, ANOMALIES_TABLE_TABS, MAX_CHARS } from './anomalies_table_constants'; export class AnomaliesTableInternal extends Component { @@ -69,6 +68,7 @@ export class AnomaliesTableInternal extends Component { } toggleRow = async (item, tab = ANOMALIES_TABLE_TABS.DETAILS) => { + const ml = this.context.services.mlServices.mlApiServices; const itemIdToExpandedRowMap = { ...this.state.itemIdToExpandedRowMap }; if (itemIdToExpandedRowMap[item.rowId]) { delete itemIdToExpandedRowMap[item.rowId]; diff --git a/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.tsx b/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.tsx index ca3ffc38f816b..5b7d4b3be128c 100644 --- a/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.tsx +++ b/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.tsx @@ -52,14 +52,13 @@ import { parseInterval } from '../../../../common/util/parse_interval'; import { ML_APP_LOCATOR, ML_PAGES } from '../../../../common/constants/locator'; import { getFiltersForDSLQuery } from '../../../../common/util/job_utils'; -import { mlJobService } from '../../services/job_service'; -import { ml } from '../../services/ml_api_service'; +import { useMlJobService } from '../../services/job_service'; import { escapeKueryForFieldValuePair, replaceStringTokens } from '../../util/string_utils'; import { getUrlForRecord, openCustomUrlWindow } from '../../util/custom_url_utils'; import type { SourceIndicesWithGeoFields } from '../../explorer/explorer_utils'; import { escapeDoubleQuotes, getDateFormatTz } from '../../explorer/explorer_utils'; import { usePermissionCheck } from '../../capabilities/check_capabilities'; -import { useMlKibana } from '../../contexts/kibana'; +import { useMlApiContext, useMlKibana } from '../../contexts/kibana'; import { useMlIndexUtils } from '../../util/index_service'; import { getQueryStringForInfluencers } from './get_query_string_for_influencers'; @@ -101,13 +100,24 @@ export const LinksMenuUI = (props: LinksMenuProps) => { const kibana = useMlKibana(); const { - services: { data, share, application, uiActions }, + services: { + data, + share, + application, + uiActions, + uiSettings, + notifications: { toasts }, + }, } = kibana; const { getDataViewById, getDataViewIdFromName } = useMlIndexUtils(); + const ml = useMlApiContext(); + const mlJobService = useMlJobService(); const job = useMemo(() => { if (props.selectedJob !== undefined) return props.selectedJob; return mlJobService.getJob(props.anomaly.jobId); + // skip mlJobService from deps + // eslint-disable-next-line react-hooks/exhaustive-deps }, [props.anomaly.jobId, props.selectedJob]); const categorizationFieldName = job.analysis_config.categorization_field_name; @@ -145,7 +155,9 @@ export const LinksMenuUI = (props: LinksMenuProps) => { const getAnomaliesMapsLink = async (anomaly: MlAnomaliesTableRecord) => { const initialLayers = getInitialAnomaliesLayers(anomaly.jobId); - const anomalyBucketStartMoment = moment(anomaly.source.timestamp).tz(getDateFormatTz()); + const anomalyBucketStartMoment = moment(anomaly.source.timestamp).tz( + getDateFormatTz(uiSettings) + ); const anomalyBucketStart = anomalyBucketStartMoment.toISOString(); const anomalyBucketEnd = anomalyBucketStartMoment .add(anomaly.source.bucket_span, 'seconds') @@ -186,7 +198,9 @@ export const LinksMenuUI = (props: LinksMenuProps) => { sourceIndicesWithGeoFields[anomaly.jobId] ); // Widen the timerange by one bucket span on start/end to increase chances of always having data on the map - const anomalyBucketStartMoment = moment(anomaly.source.timestamp).tz(getDateFormatTz()); + const anomalyBucketStartMoment = moment(anomaly.source.timestamp).tz( + getDateFormatTz(uiSettings) + ); const anomalyBucketStart = anomalyBucketStartMoment .subtract(anomaly.source.bucket_span, 'seconds') .toISOString(); @@ -513,7 +527,6 @@ export const LinksMenuUI = (props: LinksMenuProps) => { .catch((resp) => { // eslint-disable-next-line no-console console.log('openCustomUrl(): error loading categoryDefinition:', resp); - const { toasts } = kibana.services.notifications; toasts.addDanger( i18n.translate('xpack.ml.anomaliesTable.linksMenu.unableToOpenLinkErrorMessage', { defaultMessage: @@ -615,7 +628,6 @@ export const LinksMenuUI = (props: LinksMenuProps) => { if (job === undefined) { // eslint-disable-next-line no-console console.log(`viewExamples(): no job found with ID: ${props.anomaly.jobId}`); - const { toasts } = kibana.services.notifications; toasts.addDanger( i18n.translate('xpack.ml.anomaliesTable.linksMenu.unableToViewExamplesErrorMessage', { defaultMessage: 'Unable to view examples as no details could be found for job ID {jobId}', @@ -702,7 +714,6 @@ export const LinksMenuUI = (props: LinksMenuProps) => { .catch((resp) => { // eslint-disable-next-line no-console console.log('viewExamples(): error loading categoryDefinition:', resp); - const { toasts } = kibana.services.notifications; toasts.addDanger( i18n.translate('xpack.ml.anomaliesTable.linksMenu.loadingDetailsErrorMessage', { defaultMessage: @@ -736,7 +747,6 @@ export const LinksMenuUI = (props: LinksMenuProps) => { `viewExamples(): error finding type of field ${categorizationFieldName} in indices:`, datafeedIndices ); - const { toasts } = kibana.services.notifications; toasts.addDanger( i18n.translate('xpack.ml.anomaliesTable.linksMenu.noMappingCouldBeFoundErrorMessage', { defaultMessage: diff --git a/x-pack/plugins/ml/public/application/components/custom_hooks/use_create_ad_links.ts b/x-pack/plugins/ml/public/application/components/custom_hooks/use_create_ad_links.ts index 51a096512ee7d..50a4c7f1efda9 100644 --- a/x-pack/plugins/ml/public/application/components/custom_hooks/use_create_ad_links.ts +++ b/x-pack/plugins/ml/public/application/components/custom_hooks/use_create_ad_links.ts @@ -11,7 +11,7 @@ import { ANOMALY_DETECTION_DEFAULT_TIME_RANGE, ANOMALY_DETECTION_ENABLE_TIME_RANGE, } from '../../../../common/constants/settings'; -import { mlJobService } from '../../services/job_service'; +import { useMlJobService } from '../../services/job_service'; export const useCreateADLinks = () => { const { @@ -19,6 +19,7 @@ export const useCreateADLinks = () => { http: { basePath }, }, } = useMlKibana(); + const mlJobService = useMlJobService(); const useUserTimeSettings = useUiSettings().get(ANOMALY_DETECTION_ENABLE_TIME_RANGE); const userTimeSettings = useUiSettings().get(ANOMALY_DETECTION_DEFAULT_TIME_RANGE); diff --git a/x-pack/plugins/ml/public/application/components/custom_urls/custom_url_editor/list.tsx b/x-pack/plugins/ml/public/application/components/custom_urls/custom_url_editor/list.tsx index f10e644ffffee..16dba857ed6cc 100644 --- a/x-pack/plugins/ml/public/application/components/custom_urls/custom_url_editor/list.tsx +++ b/x-pack/plugins/ml/public/application/components/custom_urls/custom_url_editor/list.tsx @@ -29,7 +29,7 @@ import { } from '@kbn/ml-data-frame-analytics-utils'; import { parseUrlState } from '@kbn/ml-url-state'; -import { useMlKibana } from '../../../contexts/kibana'; +import { useMlApiContext, useMlKibana } from '../../../contexts/kibana'; import { useToastNotificationService } from '../../../services/toast_notification_service'; import { isValidLabel, openCustomUrlWindow } from '../../../util/custom_url_utils'; import { getTestUrl } from './utils'; @@ -73,6 +73,7 @@ export const CustomUrlList: FC = ({ data: { dataViews }, }, } = useMlKibana(); + const ml = useMlApiContext(); const { displayErrorToast } = useToastNotificationService(); const [expandedUrlIndex, setExpandedUrlIndex] = useState(null); @@ -160,7 +161,14 @@ export const CustomUrlList: FC = ({ if (index < customUrls.length) { try { - const testUrl = await getTestUrl(job, customUrl, timefieldName, undefined, isPartialDFAJob); + const testUrl = await getTestUrl( + ml, + job, + customUrl, + timefieldName, + undefined, + isPartialDFAJob + ); openCustomUrlWindow(testUrl, customUrl, http.basePath.get()); } catch (error) { displayErrorToast( diff --git a/x-pack/plugins/ml/public/application/components/custom_urls/custom_url_editor/utils.ts b/x-pack/plugins/ml/public/application/components/custom_urls/custom_url_editor/utils.ts index b151ddcb808fd..cd0a52e170453 100644 --- a/x-pack/plugins/ml/public/application/components/custom_urls/custom_url_editor/utils.ts +++ b/x-pack/plugins/ml/public/application/components/custom_urls/custom_url_editor/utils.ts @@ -42,12 +42,12 @@ import { replaceTokensInDFAUrlValue, isValidLabel, } from '../../../util/custom_url_utils'; -import { ml } from '../../../services/ml_api_service'; import { escapeForElasticsearchQuery } from '../../../util/string_utils'; import type { CombinedJob, Job } from '../../../../../common/types/anomaly_detection_jobs'; import { isAnomalyDetectionJob } from '../../../../../common/types/anomaly_detection_jobs'; import type { TimeRangeType } from './constants'; +import type { MlApiServices } from '../../../services/ml_api_service'; export interface TimeRange { type: TimeRangeType; @@ -426,7 +426,11 @@ function buildAppStateQueryParam(queryFieldNames: string[]) { // Builds the full URL for testing out a custom URL configuration, which // may contain dollar delimited partition / influencer entity tokens and // drilldown time range settings. -async function getAnomalyDetectionJobTestUrl(job: Job, customUrl: MlUrlConfig): Promise { +async function getAnomalyDetectionJobTestUrl( + ml: MlApiServices, + job: Job, + customUrl: MlUrlConfig +): Promise { const interval = parseInterval(job.analysis_config.bucket_span!); const bucketSpanSecs = interval !== null ? interval.asSeconds() : 0; @@ -516,6 +520,7 @@ async function getAnomalyDetectionJobTestUrl(job: Job, customUrl: MlUrlConfig): } async function getDataFrameAnalyticsTestUrl( + ml: MlApiServices, job: DataFrameAnalyticsConfig, customUrl: MlKibanaUrlConfig, timeFieldName: string | null, @@ -589,6 +594,7 @@ async function getDataFrameAnalyticsTestUrl( } export function getTestUrl( + ml: MlApiServices, job: Job | DataFrameAnalyticsConfig, customUrl: MlUrlConfig, timeFieldName: string | null, @@ -597,6 +603,7 @@ export function getTestUrl( ) { if (isDataFrameAnalyticsConfigs(job) || isPartialDFAJob) { return getDataFrameAnalyticsTestUrl( + ml, job as DataFrameAnalyticsConfig, customUrl, timeFieldName, @@ -605,5 +612,5 @@ export function getTestUrl( ); } - return getAnomalyDetectionJobTestUrl(job, customUrl); + return getAnomalyDetectionJobTestUrl(ml, job, customUrl); } diff --git a/x-pack/plugins/ml/public/application/components/custom_urls/custom_urls.tsx b/x-pack/plugins/ml/public/application/components/custom_urls/custom_urls.tsx index 56b0064e69ddd..8c92a4c4515a5 100644 --- a/x-pack/plugins/ml/public/application/components/custom_urls/custom_urls.tsx +++ b/x-pack/plugins/ml/public/application/components/custom_urls/custom_urls.tsx @@ -42,7 +42,7 @@ import { } from './custom_url_editor/utils'; import { openCustomUrlWindow } from '../../util/custom_url_utils'; import type { CustomUrlsWrapperProps } from './custom_urls_wrapper'; -import { indexServiceFactory } from '../../util/index_service'; +import { indexServiceFactory, type MlIndexUtils } from '../../util/index_service'; interface CustomUrlsState { customUrls: MlUrlConfig[]; @@ -62,9 +62,10 @@ export class CustomUrls extends Component { static contextType = context; declare context: MlKibanaReactContextValue; - private toastNotificationService: ToastNotificationService | undefined; + private toastNotificationService: ToastNotificationService; + private mlIndexUtils: MlIndexUtils; - constructor(props: CustomUrlsProps) { + constructor(props: CustomUrlsProps, constructorContext: MlKibanaReactContextValue) { super(props); this.state = { @@ -74,6 +75,11 @@ export class CustomUrls extends Component { editorOpen: false, supportedFilterFields: [], }; + + this.toastNotificationService = toastNotificationServiceProvider( + constructorContext.services.notifications.toasts + ); + this.mlIndexUtils = indexServiceFactory(constructorContext.services.data.dataViews); } static getDerivedStateFromProps(props: CustomUrlsProps) { @@ -84,10 +90,7 @@ export class CustomUrls extends Component { } componentDidMount() { - const { toasts } = this.context.services.notifications; - this.toastNotificationService = toastNotificationServiceProvider(toasts); const { dashboardService } = this.props; - const mlIndexUtils = indexServiceFactory(this.context.services.data.dataViews); dashboardService .fetchDashboards() @@ -106,7 +109,7 @@ export class CustomUrls extends Component { ); }); - mlIndexUtils + this.mlIndexUtils .loadDataViewListItems() .then((dataViewListItems) => { this.setState({ dataViewListItems }); @@ -175,6 +178,7 @@ export class CustomUrls extends Component { http: { basePath }, data: { dataViews }, dashboard, + mlServices: { mlApiServices: ml }, } = this.context.services; const dataViewId = this.state?.editorSettings?.kibanaSettings?.discoverIndexPatternId; const job = this.props.job; @@ -190,6 +194,7 @@ export class CustomUrls extends Component { buildCustomUrlFromSettings(dashboard, this.state.editorSettings as CustomUrlSettings).then( (customUrl) => { getTestUrl( + ml, job, customUrl, timefieldName, diff --git a/x-pack/plugins/ml/public/application/components/data_recognizer/data_recognizer.js b/x-pack/plugins/ml/public/application/components/data_recognizer/data_recognizer.js index 64cbf60dee385..409002011926d 100644 --- a/x-pack/plugins/ml/public/application/components/data_recognizer/data_recognizer.js +++ b/x-pack/plugins/ml/public/application/components/data_recognizer/data_recognizer.js @@ -6,13 +6,15 @@ */ import PropTypes from 'prop-types'; - import React, { Component } from 'react'; -import { RecognizedResult } from './recognized_result'; -import { ml } from '../../services/ml_api_service'; +import { context } from '@kbn/kibana-react-plugin/public'; + +import { RecognizedResult } from './recognized_result'; export class DataRecognizer extends Component { + static contextType = context; + constructor(props) { super(props); @@ -27,6 +29,7 @@ export class DataRecognizer extends Component { } componentDidMount() { + const ml = this.context.services.mlServices.mlApiServices; // once the mount is complete, call the recognize endpoint to see if the index format is known to us, ml.recognizeIndex({ indexPatternTitle: this.indexPattern.title }) .then((resp) => { diff --git a/x-pack/plugins/ml/public/application/components/items_grid/items_grid_pagination.js b/x-pack/plugins/ml/public/application/components/items_grid/items_grid_pagination.js index 95079800fc358..c95b1db622ff8 100644 --- a/x-pack/plugins/ml/public/application/components/items_grid/items_grid_pagination.js +++ b/x-pack/plugins/ml/public/application/components/items_grid/items_grid_pagination.js @@ -101,13 +101,18 @@ export class ItemsGridPagination extends Component { - + diff --git a/x-pack/plugins/ml/public/application/components/ml_inference/components/reindex_with_pipeline.tsx b/x-pack/plugins/ml/public/application/components/ml_inference/components/reindex_with_pipeline.tsx index 513ad5849d258..17128208e7aec 100644 --- a/x-pack/plugins/ml/public/application/components/ml_inference/components/reindex_with_pipeline.tsx +++ b/x-pack/plugins/ml/public/application/components/ml_inference/components/reindex_with_pipeline.tsx @@ -29,7 +29,7 @@ import { extractErrorMessage } from '@kbn/ml-error-utils'; import { i18n } from '@kbn/i18n'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { debounce } from 'lodash'; -import { useMlKibana } from '../../../contexts/kibana'; +import { useMlApiContext, useMlKibana } from '../../../contexts/kibana'; import { isValidIndexName } from '../../../../../common/util/es_utils'; import { createKibanaDataView, checkIndexExists } from '../retry_create_data_view'; import { useToastNotificationService } from '../../../services/toast_notification_service'; @@ -82,12 +82,11 @@ export const ReindexWithPipeline: FC = ({ pipelineName, sourceIndex }) => application: { capabilities }, share, data, - mlServices: { - mlApiServices: { getIndices, reindexWithPipeline, hasPrivileges }, - }, docLinks: { links }, }, } = useMlKibana(); + const ml = useMlApiContext(); + const { getIndices, reindexWithPipeline, hasPrivileges } = ml; const { displayErrorToast } = useToastNotificationService(); @@ -124,7 +123,7 @@ export const ReindexWithPipeline: FC = ({ pipelineName, sourceIndex }) => ); const debouncedIndexCheck = debounce(async () => { - const checkResp = await checkIndexExists(destinationIndex); + const checkResp = await checkIndexExists(destinationIndex, ml); if (checkResp.errorMessage !== undefined) { displayErrorToast( checkResp.errorMessage, @@ -237,7 +236,11 @@ export const ReindexWithPipeline: FC = ({ pipelineName, sourceIndex }) => useEffect( function createDiscoverLink() { async function createDataView() { - const dataViewCreationResult = await createKibanaDataView(destinationIndex, data.dataViews); + const dataViewCreationResult = await createKibanaDataView( + destinationIndex, + data.dataViews, + ml + ); if ( dataViewCreationResult?.success === true && dataViewCreationResult?.dataViewId && @@ -251,6 +254,8 @@ export const ReindexWithPipeline: FC = ({ pipelineName, sourceIndex }) => createDataView(); } }, + // Skip ml API services from deps check + // eslint-disable-next-line react-hooks/exhaustive-deps [ reindexingTaskId, destinationIndex, diff --git a/x-pack/plugins/ml/public/application/components/ml_inference/components/test_pipeline.tsx b/x-pack/plugins/ml/public/application/components/ml_inference/components/test_pipeline.tsx index a9745e6e16aa5..48ae32d615d93 100644 --- a/x-pack/plugins/ml/public/application/components/ml_inference/components/test_pipeline.tsx +++ b/x-pack/plugins/ml/public/application/components/ml_inference/components/test_pipeline.tsx @@ -67,10 +67,7 @@ export const TestPipeline: FC = memo(({ state, sourceIndex, mode }) => { const [lastFetchedSampleDocsString, setLastFetchedSampleDocsString] = useState(''); const [isValid, setIsValid] = useState(true); const [showCallOut, setShowCallOut] = useState(true); - const { - esSearch, - trainedModels: { trainedModelPipelineSimulate }, - } = useMlApiContext(); + const ml = useMlApiContext(); const { notifications: { toasts }, services: { @@ -91,7 +88,7 @@ export const TestPipeline: FC = memo(({ state, sourceIndex, mode }) => { const simulatePipeline = async () => { try { - const result = await trainedModelPipelineSimulate( + const result = await ml.trainedModels.trainedModelPipelineSimulate( pipelineConfig, JSON.parse(sampleDocsString) as IngestSimulateDocument[] ); @@ -130,7 +127,7 @@ export const TestPipeline: FC = memo(({ state, sourceIndex, mode }) => { let records: IngestSimulateDocument[] = []; let resp; try { - resp = await esSearch(body); + resp = await ml.esSearch(body); if (resp && resp.hits.total.value > 0) { records = resp.hits.hits; @@ -144,7 +141,9 @@ export const TestPipeline: FC = memo(({ state, sourceIndex, mode }) => { setLastFetchedSampleDocsString(JSON.stringify(records, null, 2)); setIsValid(true); }, - [esSearch] + // skip ml API service from deps + // eslint-disable-next-line react-hooks/exhaustive-deps + [] ); const { getSampleDoc, getRandomSampleDoc } = useMemo( @@ -178,7 +177,7 @@ export const TestPipeline: FC = memo(({ state, sourceIndex, mode }) => { useEffect( function checkSourceIndexExists() { async function ensureSourceIndexExists() { - const resp = await checkIndexExists(sourceIndex!); + const resp = await checkIndexExists(sourceIndex!, ml); const indexExists = resp.resp && resp.resp[sourceIndex!] && resp.resp[sourceIndex!].exists; if (indexExists === false) { setSourceIndexMissingError(sourceIndexMissingMessage); @@ -188,6 +187,8 @@ export const TestPipeline: FC = memo(({ state, sourceIndex, mode }) => { ensureSourceIndexExists(); } }, + // skip ml API service from deps + // eslint-disable-next-line react-hooks/exhaustive-deps [sourceIndex, sourceIndexMissingError] ); diff --git a/x-pack/plugins/ml/public/application/components/ml_inference/retry_create_data_view.ts b/x-pack/plugins/ml/public/application/components/ml_inference/retry_create_data_view.ts index 4f6e493d1aa34..fd270ae70dfd7 100644 --- a/x-pack/plugins/ml/public/application/components/ml_inference/retry_create_data_view.ts +++ b/x-pack/plugins/ml/public/application/components/ml_inference/retry_create_data_view.ts @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; import { extractErrorMessage } from '@kbn/ml-error-utils'; import type { DataViewsContract } from '@kbn/data-views-plugin/public'; import { DuplicateDataViewError } from '@kbn/data-plugin/public'; -import { ml } from '../../services/ml_api_service'; +import type { MlApiServices } from '../../services/ml_api_service'; import type { FormMessage } from '../../data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state'; interface CreateKibanaDataViewResponse { @@ -25,7 +25,7 @@ function delay(ms = 1000) { }); } -export async function checkIndexExists(destIndex: string) { +export async function checkIndexExists(destIndex: string, ml: MlApiServices) { let resp; let errorMessage; try { @@ -36,20 +36,23 @@ export async function checkIndexExists(destIndex: string) { return { resp, errorMessage }; } -export async function retryIndexExistsCheck(destIndex: string): Promise<{ +export async function retryIndexExistsCheck( + destIndex: string, + ml: MlApiServices +): Promise<{ success: boolean; indexExists: boolean; errorMessage?: string; }> { let retryCount = 15; - let resp = await checkIndexExists(destIndex); + let resp = await checkIndexExists(destIndex, ml); let indexExists = resp.resp && resp.resp[destIndex] && resp.resp[destIndex].exists; while (retryCount > 1 && !indexExists) { retryCount--; await delay(1000); - resp = await checkIndexExists(destIndex); + resp = await checkIndexExists(destIndex, ml); indexExists = resp.resp && resp.resp[destIndex] && resp.resp[destIndex].exists; } @@ -67,12 +70,13 @@ export async function retryIndexExistsCheck(destIndex: string): Promise<{ export const createKibanaDataView = async ( destinationIndex: string, dataViewsService: DataViewsContract, + ml: MlApiServices, timeFieldName?: string, callback?: (response: FormMessage) => void ) => { const response: CreateKibanaDataViewResponse = { success: false, message: '' }; const dataViewName = destinationIndex; - const exists = await retryIndexExistsCheck(destinationIndex); + const exists = await retryIndexExistsCheck(destinationIndex, ml); if (exists?.success === true) { // index exists - create data view if (exists?.indexExists === true) { diff --git a/x-pack/plugins/ml/public/application/components/model_snapshots/edit_model_snapshot_flyout/edit_model_snapshot_flyout.tsx b/x-pack/plugins/ml/public/application/components/model_snapshots/edit_model_snapshot_flyout/edit_model_snapshot_flyout.tsx index 0e34435d93ac4..1be4c7f479ea2 100644 --- a/x-pack/plugins/ml/public/application/components/model_snapshots/edit_model_snapshot_flyout/edit_model_snapshot_flyout.tsx +++ b/x-pack/plugins/ml/public/application/components/model_snapshots/edit_model_snapshot_flyout/edit_model_snapshot_flyout.tsx @@ -30,8 +30,7 @@ import type { ModelSnapshot, CombinedJobWithStats, } from '../../../../../common/types/anomaly_detection_jobs'; -import { ml } from '../../../services/ml_api_service'; -import { useNotifications } from '../../../contexts/kibana'; +import { useMlApiContext, useNotifications } from '../../../contexts/kibana'; interface Props { snapshot: ModelSnapshot; @@ -40,6 +39,7 @@ interface Props { } export const EditModelSnapshotFlyout: FC = ({ snapshot, job, closeFlyout }) => { + const ml = useMlApiContext(); const { toasts } = useNotifications(); const [description, setDescription] = useState(snapshot.description); const [retain, setRetain] = useState(snapshot.retain); diff --git a/x-pack/plugins/ml/public/application/components/model_snapshots/model_snapshots_table.tsx b/x-pack/plugins/ml/public/application/components/model_snapshots/model_snapshots_table.tsx index f36668cd1a8f9..2c74bb9f67969 100644 --- a/x-pack/plugins/ml/public/application/components/model_snapshots/model_snapshots_table.tsx +++ b/x-pack/plugins/ml/public/application/components/model_snapshots/model_snapshots_table.tsx @@ -12,10 +12,10 @@ import { i18n } from '@kbn/i18n'; import type { EuiBasicTableColumn } from '@elastic/eui'; import { EuiFlexGroup, EuiFlexItem, EuiInMemoryTable, EuiLoadingSpinner } from '@elastic/eui'; import { timeFormatter } from '@kbn/ml-date-utils'; +import { useMlApiContext } from '../../contexts/kibana'; import { usePermissionCheck } from '../../capabilities/check_capabilities'; import { EditModelSnapshotFlyout } from './edit_model_snapshot_flyout'; import { RevertModelSnapshotFlyout } from './revert_model_snapshot_flyout'; -import { ml } from '../../services/ml_api_service'; import { DATAFEED_STATE, JOB_STATE } from '../../../../common/constants/states'; import { CloseJobConfirm } from './close_job_confirm'; import type { @@ -36,6 +36,8 @@ export enum COMBINED_JOB_STATE { } export const ModelSnapshotTable: FC = ({ job, refreshJobList }) => { + const ml = useMlApiContext(); + const [canCreateJob, canStartStopDatafeed] = usePermissionCheck([ 'canCreateJob', 'canStartStopDatafeed', @@ -71,7 +73,8 @@ export const ModelSnapshotTable: FC = ({ job, refreshJobList }) => { const checkJobIsClosed = useCallback( async (snapshot: ModelSnapshot) => { - const state = await getCombinedJobState(job.job_id); + const jobs = await ml.jobs.jobs([job.job_id]); + const state = getCombinedJobState(jobs); if (state === COMBINED_JOB_STATE.UNKNOWN) { // this will only happen if the job has been deleted by another user // between the time the row has been expended and now @@ -90,6 +93,8 @@ export const ModelSnapshotTable: FC = ({ job, refreshJobList }) => { setCloseJobModalVisible(snapshot); } }, + // skip mlApiServices from deps + // eslint-disable-next-line react-hooks/exhaustive-deps [job] ); @@ -101,12 +106,15 @@ export const ModelSnapshotTable: FC = ({ job, refreshJobList }) => { const forceCloseJob = useCallback(async () => { await ml.jobs.forceStopAndCloseJob(job.job_id); if (closeJobModalVisible !== null) { - const state = await getCombinedJobState(job.job_id); + const jobs = await ml.jobs.jobs([job.job_id]); + const state = getCombinedJobState(jobs); if (state === COMBINED_JOB_STATE.CLOSED) { setRevertSnapshot(closeJobModalVisible); } } hideCloseJobModalVisible(); + // skip mlApiServices from deps + // eslint-disable-next-line react-hooks/exhaustive-deps }, [job, closeJobModalVisible]); const closeEditFlyout = useCallback((reload: boolean) => { @@ -260,9 +268,7 @@ export const ModelSnapshotTable: FC = ({ job, refreshJobList }) => { ); }; -async function getCombinedJobState(jobId: string) { - const jobs = await ml.jobs.jobs([jobId]); - +function getCombinedJobState(jobs: CombinedJobWithStats[]) { if (jobs.length !== 1) { return COMBINED_JOB_STATE.UNKNOWN; } diff --git a/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/revert_model_snapshot_flyout.tsx b/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/revert_model_snapshot_flyout.tsx index ac1e31ced032c..c25bcee687e04 100644 --- a/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/revert_model_snapshot_flyout.tsx +++ b/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/revert_model_snapshot_flyout.tsx @@ -36,10 +36,9 @@ import type { ModelSnapshot, CombinedJobWithStats, } from '../../../../../common/types/anomaly_detection_jobs'; -import { ml } from '../../../services/ml_api_service'; -import { useNotifications } from '../../../contexts/kibana'; +import { useMlApiContext, useNotifications } from '../../../contexts/kibana'; import { chartLoaderProvider } from './chart_loader'; -import { mlResultsService } from '../../../services/results_service'; +import { mlResultsServiceProvider } from '../../../services/results_service'; import type { LineChartPoint } from '../../../jobs/new_job/common/chart_loader'; import { EventRateChart } from '../../../jobs/new_job/pages/components/charts/event_rate_chart/event_rate_chart'; import type { Anomaly } from '../../../jobs/new_job/common/results_loader/results_loader'; @@ -64,9 +63,11 @@ export const RevertModelSnapshotFlyout: FC = ({ closeFlyout, refresh, }) => { + const ml = useMlApiContext(); const { toasts } = useNotifications(); const { loadAnomalyDataForJob, loadEventRateForJob } = useMemo( - () => chartLoaderProvider(mlResultsService), + () => chartLoaderProvider(mlResultsServiceProvider(ml)), + // eslint-disable-next-line react-hooks/exhaustive-deps [] ); const [currentSnapshot, setCurrentSnapshot] = useState(snapshot); diff --git a/x-pack/plugins/ml/public/application/components/rule_editor/rule_editor_flyout.js b/x-pack/plugins/ml/public/application/components/rule_editor/rule_editor_flyout.js index abed4bf9819d8..36c7d6cc00a88 100644 --- a/x-pack/plugins/ml/public/application/components/rule_editor/rule_editor_flyout.js +++ b/x-pack/plugins/ml/public/application/components/rule_editor/rule_editor_flyout.js @@ -53,7 +53,7 @@ import { } from './utils'; import { getPartitioningFieldNames } from '../../../../common/util/job_utils'; -import { mlJobService } from '../../services/job_service'; +import { mlJobServiceFactory } from '../../services/job_service'; import { toastNotificationServiceProvider } from '../../services/toast_notification_service'; class RuleEditorFlyoutUI extends Component { @@ -80,6 +80,11 @@ class RuleEditorFlyoutUI extends Component { this.partitioningFieldNames = []; this.canGetFilters = checkPermission('canGetFilters'); + + this.mlJobService = mlJobServiceFactory( + toastNotificationServiceProvider(props.kibana.services.notifications.toasts), + props.kibana.services.mlServices.mlApiServices + ); } componentDidMount() { @@ -101,7 +106,7 @@ class RuleEditorFlyoutUI extends Component { showFlyout = (anomaly) => { let ruleIndex = -1; - const job = this.props.selectedJob ?? mlJobService.getJob(anomaly.jobId); + const job = this.props.selectedJob ?? this.mlJobService.getJob(anomaly.jobId); if (job === undefined) { // No details found for this job, display an error and // don't open the Flyout as no edits can be made without the job. @@ -337,6 +342,7 @@ class RuleEditorFlyoutUI extends Component { }; updateRuleAtIndex = (ruleIndex, editedRule) => { + const mlJobService = this.mlJobService; const { toasts } = this.props.kibana.services.notifications; const { mlApiServices } = this.props.kibana.services.mlServices; const { job, anomaly } = this.state; @@ -344,7 +350,7 @@ class RuleEditorFlyoutUI extends Component { const jobId = job.job_id; const detectorIndex = anomaly.detectorIndex; - saveJobRule(job, detectorIndex, ruleIndex, editedRule, mlApiServices) + saveJobRule(mlJobService, job, detectorIndex, ruleIndex, editedRule, mlApiServices) .then((resp) => { if (resp.success) { toasts.add({ @@ -392,13 +398,14 @@ class RuleEditorFlyoutUI extends Component { }; deleteRuleAtIndex = (index) => { + const mlJobService = this.mlJobService; const { toasts } = this.props.kibana.services.notifications; const { mlApiServices } = this.props.kibana.services.mlServices; const { job, anomaly } = this.state; const jobId = job.job_id; const detectorIndex = anomaly.detectorIndex; - deleteJobRule(job, detectorIndex, index, mlApiServices) + deleteJobRule(mlJobService, job, detectorIndex, index, mlApiServices) .then((resp) => { if (resp.success) { toasts.addSuccess( diff --git a/x-pack/plugins/ml/public/application/components/rule_editor/rule_editor_flyout.test.js b/x-pack/plugins/ml/public/application/components/rule_editor/rule_editor_flyout.test.js index 67ace1427eb13..8dce7f8f45f78 100644 --- a/x-pack/plugins/ml/public/application/components/rule_editor/rule_editor_flyout.test.js +++ b/x-pack/plugins/ml/public/application/components/rule_editor/rule_editor_flyout.test.js @@ -7,7 +7,7 @@ // Mock the services required for reading and writing job data. jest.mock('../../services/job_service', () => ({ - mlJobService: { + mlJobServiceFactory: () => ({ getJob: () => { return { job_id: 'farequote_no_by', @@ -43,9 +43,8 @@ jest.mock('../../services/job_service', () => ({ }, }; }, - }, + }), })); -jest.mock('../../services/ml_api_service', () => 'ml'); jest.mock('../../capabilities/check_capabilities', () => ({ checkPermission: () => true, })); @@ -93,6 +92,7 @@ function prepareTest() { }, }, }, + mlServices: { mlApiServices: {} }, notifications: { toasts: { addDanger: () => {}, diff --git a/x-pack/plugins/ml/public/application/components/rule_editor/select_rule_action/__snapshots__/rule_action_panel.test.js.snap b/x-pack/plugins/ml/public/application/components/rule_editor/select_rule_action/__snapshots__/rule_action_panel.test.js.snap deleted file mode 100644 index 5cd4ef44c07a2..0000000000000 --- a/x-pack/plugins/ml/public/application/components/rule_editor/select_rule_action/__snapshots__/rule_action_panel.test.js.snap +++ /dev/null @@ -1,182 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`RuleActionPanel renders panel for rule with a condition 1`] = ` - - , - }, - Object { - "description": , - "title": , - }, - Object { - "description": - - , - "title": "", - }, - Object { - "description": , - "title": "", - }, - ] - } - type="column" - /> - -`; - -exports[`RuleActionPanel renders panel for rule with a condition and scope, value not in filter list 1`] = ` - - , - }, - Object { - "description": , - "title": , - }, - Object { - "description": - - , - "title": "", - }, - Object { - "description": , - "title": "", - }, - ] - } - type="column" - /> - -`; - -exports[`RuleActionPanel renders panel for rule with scope, value in filter list 1`] = ` - - , - }, - Object { - "description": - - , - "title": , - }, - Object { - "description": , - "title": "", - }, - ] - } - type="column" - /> - -`; diff --git a/x-pack/plugins/ml/public/application/components/rule_editor/select_rule_action/rule_action_panel.js b/x-pack/plugins/ml/public/application/components/rule_editor/select_rule_action/rule_action_panel.js index 9ee71aff3d54a..aeca2d6425fd7 100644 --- a/x-pack/plugins/ml/public/application/components/rule_editor/select_rule_action/rule_action_panel.js +++ b/x-pack/plugins/ml/public/application/components/rule_editor/select_rule_action/rule_action_panel.js @@ -11,19 +11,21 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; +import { cloneDeep } from 'lodash'; import { EuiDescriptionList, EuiLink, EuiPanel } from '@elastic/eui'; -import { cloneDeep } from 'lodash'; +import { context } from '@kbn/kibana-react-plugin/public'; import { AddToFilterListLink } from './add_to_filter_list_link'; import { DeleteRuleModal } from './delete_rule_modal'; import { EditConditionLink } from './edit_condition_link'; import { buildRuleDescription } from '../utils'; -import { ml } from '../../../services/ml_api_service'; import { FormattedMessage } from '@kbn/i18n-react'; export class RuleActionPanel extends Component { + static contextType = context; + constructor(props) { super(props); @@ -41,6 +43,7 @@ export class RuleActionPanel extends Component { } componentDidMount() { + const ml = this.context.services.mlServices.mlApiServices; // If the rule has a scope section with a single partitioning field key, // load the filter list to check whether to add a link to add the // anomaly partitioning field value to the filter list. diff --git a/x-pack/plugins/ml/public/application/components/rule_editor/select_rule_action/rule_action_panel.test.js b/x-pack/plugins/ml/public/application/components/rule_editor/select_rule_action/rule_action_panel.test.js index 6d5b2e38346d3..79414b02a5e48 100644 --- a/x-pack/plugins/ml/public/application/components/rule_editor/select_rule_action/rule_action_panel.test.js +++ b/x-pack/plugins/ml/public/application/components/rule_editor/select_rule_action/rule_action_panel.test.js @@ -5,6 +5,15 @@ * 2.0. */ +import React from 'react'; +import { render, screen, waitFor } from '@testing-library/react'; + +import { createKibanaReactContext } from '@kbn/kibana-react-plugin/public'; +import { ML_DETECTOR_RULE_ACTION } from '@kbn/ml-anomaly-utils'; +import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; + +import { RuleActionPanel } from './rule_action_panel'; + jest.mock('../../../services/job_service', () => 'mlJobService'); // Mock the call for loading a filter. @@ -19,22 +28,18 @@ const mockTestFilter = { jobs: ['farequote'], }, }; -jest.mock('../../../services/ml_api_service', () => ({ - ml: { - filters: { - filters: () => { - return Promise.resolve(mockTestFilter); + +const kibanaReactContextMock = createKibanaReactContext({ + mlServices: { + mlApiServices: { + filters: { + filters: () => { + return Promise.resolve(mockTestFilter); + }, }, }, }, -})); - -import React from 'react'; - -import { shallowWithIntl } from '@kbn/test-jest-helpers'; -import { ML_DETECTOR_RULE_ACTION } from '@kbn/ml-anomaly-utils'; - -import { RuleActionPanel } from './rule_action_panel'; +}); describe('RuleActionPanel', () => { const job = { @@ -117,9 +122,21 @@ describe('RuleActionPanel', () => { ruleIndex: 0, }; - const component = shallowWithIntl(); - - expect(component).toMatchSnapshot(); + render( + + + + + + ); + + expect(screen.getByText('Rule')).toBeInTheDocument(); + expect(screen.getByText('skip result when actual is less than 1')).toBeInTheDocument(); + expect(screen.getByText('Actions')).toBeInTheDocument(); + expect(screen.getByText('Update rule condition from 1 to')).toBeInTheDocument(); + expect(screen.getByText('Update')).toBeInTheDocument(); + expect(screen.getByText('Edit rule')).toBeInTheDocument(); + expect(screen.getByText('Delete rule')).toBeInTheDocument(); }); test('renders panel for rule with scope, value in filter list', () => { @@ -128,19 +145,46 @@ describe('RuleActionPanel', () => { ruleIndex: 1, }; - const component = shallowWithIntl(); - - expect(component).toMatchSnapshot(); + render( + + + + + + ); + + expect(screen.getByText('Rule')).toBeInTheDocument(); + expect( + screen.getByText('skip model update when airline is not in eu-airlines') + ).toBeInTheDocument(); + expect(screen.getByText('Actions')).toBeInTheDocument(); + expect(screen.getByText('Edit rule')).toBeInTheDocument(); + expect(screen.getByText('Delete rule')).toBeInTheDocument(); }); - test('renders panel for rule with a condition and scope, value not in filter list', () => { + test('renders panel for rule with a condition and scope, value not in filter list', async () => { const props = { ...requiredProps, ruleIndex: 1, }; - const wrapper = shallowWithIntl(); - wrapper.setState({ showAddToFilterListLink: true }); - expect(wrapper).toMatchSnapshot(); + await waitFor(() => { + render( + + + + + + ); + }); + + expect(screen.getByText('Rule')).toBeInTheDocument(); + expect( + screen.getByText('skip model update when airline is not in eu-airlines') + ).toBeInTheDocument(); + expect(screen.getByText('Actions')).toBeInTheDocument(); + expect(screen.getByText('Add AAL to eu-airlines')).toBeInTheDocument(); + expect(screen.getByText('Edit rule')).toBeInTheDocument(); + expect(screen.getByText('Delete rule')).toBeInTheDocument(); }); }); diff --git a/x-pack/plugins/ml/public/application/components/rule_editor/utils.js b/x-pack/plugins/ml/public/application/components/rule_editor/utils.js index 275f32b28bb51..a2c8c6d11f8e5 100644 --- a/x-pack/plugins/ml/public/application/components/rule_editor/utils.js +++ b/x-pack/plugins/ml/public/application/components/rule_editor/utils.js @@ -15,7 +15,6 @@ import { ML_DETECTOR_RULE_OPERATOR, } from '@kbn/ml-anomaly-utils'; -import { mlJobService } from '../../services/job_service'; import { processCreatedBy } from '../../../../common/util/job_utils'; export function getNewConditionDefaults() { @@ -69,7 +68,14 @@ export function isValidRule(rule) { return isValid; } -export function saveJobRule(job, detectorIndex, ruleIndex, editedRule, mlApiServices) { +export function saveJobRule( + mlJobService, + job, + detectorIndex, + ruleIndex, + editedRule, + mlApiServices +) { const detector = job.analysis_config.detectors[detectorIndex]; // Filter out any scope expression where the UI=specific 'enabled' @@ -102,16 +108,16 @@ export function saveJobRule(job, detectorIndex, ruleIndex, editedRule, mlApiServ } } - return updateJobRules(job, detectorIndex, rules, mlApiServices); + return updateJobRules(mlJobService, job, detectorIndex, rules, mlApiServices); } -export function deleteJobRule(job, detectorIndex, ruleIndex, mlApiServices) { +export function deleteJobRule(mlJobService, job, detectorIndex, ruleIndex, mlApiServices) { const detector = job.analysis_config.detectors[detectorIndex]; let customRules = []; if (detector.custom_rules !== undefined && ruleIndex < detector.custom_rules.length) { customRules = cloneDeep(detector.custom_rules); customRules.splice(ruleIndex, 1); - return updateJobRules(job, detectorIndex, customRules, mlApiServices); + return updateJobRules(mlJobService, job, detectorIndex, customRules, mlApiServices); } else { return Promise.reject( new Error( @@ -127,7 +133,7 @@ export function deleteJobRule(job, detectorIndex, ruleIndex, mlApiServices) { } } -export function updateJobRules(job, detectorIndex, rules, mlApiServices) { +export function updateJobRules(mlJobService, job, detectorIndex, rules, mlApiServices) { // Pass just the detector with the edited rule to the updateJob endpoint. const jobId = job.job_id; const jobData = { @@ -149,17 +155,14 @@ export function updateJobRules(job, detectorIndex, rules, mlApiServices) { mlApiServices .updateJob({ jobId: jobId, job: jobData }) .then(() => { - // If using mlJobService, refresh the job data in the job service before resolving. - if (mlJobService) { - mlJobService - .refreshJob(jobId) - .then(() => { - resolve({ success: true }); - }) - .catch((refreshResp) => { - reject(refreshResp); - }); - } + mlJobService + .refreshJob(jobId) + .then(() => { + resolve({ success: true }); + }) + .catch((refreshResp) => { + reject(refreshResp); + }); }) .catch((resp) => { reject(resp); diff --git a/x-pack/plugins/ml/public/application/components/validate_job/__snapshots__/validate_job_view.test.js.snap b/x-pack/plugins/ml/public/application/components/validate_job/__snapshots__/validate_job_view.test.js.snap deleted file mode 100644 index 6a4418074f626..0000000000000 --- a/x-pack/plugins/ml/public/application/components/validate_job/__snapshots__/validate_job_view.test.js.snap +++ /dev/null @@ -1,64 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ValidateJob renders button and modal with a message 1`] = ` - -
- - - -
-
-`; - -exports[`ValidateJob renders the button 1`] = ` - -
- - - -
-
-`; - -exports[`ValidateJob renders the button and modal with a success message 1`] = ` - -
- - - -
-
-`; diff --git a/x-pack/plugins/ml/public/application/components/validate_job/validate_job_view.d.ts b/x-pack/plugins/ml/public/application/components/validate_job/validate_job_view.d.ts index d903533bfb73e..c6517cecd6924 100644 --- a/x-pack/plugins/ml/public/application/components/validate_job/validate_job_view.d.ts +++ b/x-pack/plugins/ml/public/application/components/validate_job/validate_job_view.d.ts @@ -9,7 +9,6 @@ import type { FC } from 'react'; declare const ValidateJob: FC<{ getJobConfig: any; getDuration: any; - ml: any; embedded?: boolean; setIsValid?: (valid: boolean) => void; idFilterList?: string[]; diff --git a/x-pack/plugins/ml/public/application/components/validate_job/validate_job_view.js b/x-pack/plugins/ml/public/application/components/validate_job/validate_job_view.js index 64910f5abfd21..acc17d6b8c0da 100644 --- a/x-pack/plugins/ml/public/application/components/validate_job/validate_job_view.js +++ b/x-pack/plugins/ml/public/application/components/validate_job/validate_job_view.js @@ -26,8 +26,6 @@ import { import { FormattedMessage } from '@kbn/i18n-react'; -import { getDocLinks } from '../../util/dependency_cache'; - import { parseMessages } from '../../../../common/constants/messages'; import { VALIDATION_STATUS } from '../../../../common/constants/validation'; import { Callout, statusToEuiIconType } from '../callout'; @@ -76,7 +74,7 @@ MessageList.propTypes = { const LoadingSpinner = () => ( - + ); @@ -120,6 +118,7 @@ export class ValidateJobUI extends Component { }; validate = () => { + const docLinks = this.props.kibana.services.docLinks; const job = this.props.getJobConfig(); const getDuration = this.props.getDuration; const duration = typeof getDuration === 'function' ? getDuration() : undefined; @@ -131,10 +130,10 @@ export class ValidateJobUI extends Component { if (typeof duration === 'object' && duration.start !== null && duration.end !== null) { let shouldShowLoadingIndicator = true; - this.props.ml + this.props.kibana.services.mlServices.mlApiServices .validateJob({ duration, fields, job }) .then((validationMessages) => { - const messages = parseMessages(validationMessages, getDocLinks()); + const messages = parseMessages(validationMessages, docLinks); shouldShowLoadingIndicator = false; const messagesContainError = messages.some((m) => m.status === VALIDATION_STATUS.ERROR); @@ -229,7 +228,7 @@ export class ValidateJobUI extends Component { }; render() { - const jobTipsUrl = getDocLinks().links.ml.anomalyDetectionJobTips; + const jobTipsUrl = this.props.kibana.services.docLinks.links.ml.anomalyDetectionJobTips; // only set to false if really false and not another falsy value, so it defaults to true. const fill = this.props.fill === false ? false : true; // default to false if not explicitly set to true @@ -244,7 +243,8 @@ export class ValidateJobUI extends Component { {embedded === false ? (
this.validate(e)} size="s" fill={fill} iconType={isCurrentJobConfig ? this.state.ui.iconType : defaultIconType} @@ -260,6 +260,7 @@ export class ValidateJobUI extends Component { {!isDisabled && this.state.ui.isModalVisible && ( ({ - getDocLinks: () => ({ - links: { - ml: { - anomalyDetectionJobTips: 'jest-metadata-mock-url', +const mockValidateJob = jest.fn().mockImplementation(({ job }) => { + console.log('job', job); + if (job.job_id === 'job1') { + return Promise.resolve([]); + } else if (job.job_id === 'job2') { + return Promise.resolve([ + { + fieldName: 'airline', + id: 'over_field_low_cardinality', + status: 'warning', + text: 'Cardinality of over_field "airline" is low and therefore less suitable for population analysis.', + url: 'https://www.elastic.co/blog/sizing-machine-learning-with-elasticsearch', }, - }, - }), -})); + ]); + } else { + return Promise.reject(new Error('Unknown job')); + } +}); + +const mockKibanaContext = { + services: { + docLinks: { links: { ml: { anomalyDetectionJobTips: 'https://anomalyDetectionJobTips' } } }, + notifications: { toasts: { addDanger: jest.fn(), addError: jest.fn() } }, + mlServices: { mlApiServices: { validateJob: mockValidateJob } }, + }, +}; +const mockReact = React; jest.mock('@kbn/kibana-react-plugin/public', () => ({ - withKibana: (comp) => { - return comp; + withKibana: (type) => { + const EnhancedType = (props) => { + return mockReact.createElement(type, { + ...props, + kibana: mockKibanaContext, + }); + }; + return EnhancedType; }, })); const job = { - job_id: 'test-id', + job_id: 'job2', }; const getJobConfig = () => job; const getDuration = () => ({ start: 0, end: 1 }); -function prepareTest(messages) { - const p = Promise.resolve(messages); - - const ml = { - validateJob: () => Promise.resolve(messages), - }; - const kibana = { - services: { - notifications: { toasts: { addDanger: jest.fn(), addError: jest.fn() } }, - }, - }; - - const component = ( - - ); - - const wrapper = shallowWithIntl(component); - - return { wrapper, p }; -} - describe('ValidateJob', () => { - const test1 = prepareTest({ - success: true, - messages: [], - }); - - test('renders the button', () => { - expect(test1.wrapper).toMatchSnapshot(); - }); - - test('renders the button and modal with a success message', () => { - test1.wrapper.instance().validate(); - test1.p.then(() => { - test1.wrapper.update(); - expect(test1.wrapper).toMatchSnapshot(); - }); - }); - - const test2 = prepareTest({ - success: true, - messages: [ - { - fieldName: 'airline', - id: 'over_field_low_cardinality', - status: 'warning', - text: 'Cardinality of over_field "airline" is low and therefore less suitable for population analysis.', - url: 'https://www.elastic.co/blog/sizing-machine-learning-with-elasticsearch', - }, - ], + test('renders the button when not in embedded mode', () => { + const { getByTestId, queryByTestId } = render( + + + + ); + + const button = getByTestId('mlValidateJobButton'); + expect(button).toBeInTheDocument(); + + const loadingSpinner = queryByTestId('mlValidateJobLoadingSpinner'); + expect(loadingSpinner).not.toBeInTheDocument(); + const modal = queryByTestId('mlValidateJobModal'); + expect(modal).not.toBeInTheDocument(); }); - test('renders button and modal with a message', () => { - test2.wrapper.instance().validate(); - test2.p.then(() => { - test2.wrapper.update(); - expect(test2.wrapper).toMatchSnapshot(); + test('renders no button when in embedded mode', async () => { + const { queryByTestId, getByTestId } = render( + + + + ); + + expect(queryByTestId('mlValidateJobButton')).not.toBeInTheDocument(); + expect(getByTestId('mlValidateJobLoadingSpinner')).toBeInTheDocument(); + expect(queryByTestId('mlValidateJobModal')).not.toBeInTheDocument(); + + await waitFor(() => expect(mockValidateJob).toHaveBeenCalledTimes(1)); + + // wait for the loading spinner to disappear and show a callout instead + await waitFor(() => { + expect(queryByTestId('mlValidateJobLoadingSpinner')).not.toBeInTheDocument(); + expect(queryByTestId('mlValidationCallout warning')).toBeInTheDocument(); }); }); }); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts index e025b808e2fcc..a636974e68b94 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts @@ -19,8 +19,8 @@ import { ANALYSIS_CONFIG_TYPE, } from '@kbn/ml-data-frame-analytics-utils'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { ml } from '../../services/ml_api_service'; import type { Dictionary } from '../../../../common/types/common'; +import type { MlApiServices } from '../../services/ml_api_service'; export type IndexPattern = string; @@ -289,6 +289,7 @@ export enum REGRESSION_STATS { } interface LoadEvalDataConfig { + mlApiServices: MlApiServices; isTraining?: boolean; index: string; dependentVariable: string; @@ -303,6 +304,7 @@ interface LoadEvalDataConfig { } export const loadEvalData = async ({ + mlApiServices, isTraining, index, dependentVariable, @@ -360,7 +362,7 @@ export const loadEvalData = async ({ }; try { - const evalResult = await ml.dataFrameAnalytics.evaluateDataFrameAnalytics(config); + const evalResult = await mlApiServices.dataFrameAnalytics.evaluateDataFrameAnalytics(config); results.success = true; results.eval = evalResult; return results; @@ -371,6 +373,7 @@ export const loadEvalData = async ({ }; interface LoadDocsCountConfig { + mlApiServices: MlApiServices; ignoreDefaultQuery?: boolean; isTraining?: boolean; searchQuery: estypes.QueryDslQueryContainer; @@ -384,6 +387,7 @@ interface LoadDocsCountResponse { } export const loadDocsCount = async ({ + mlApiServices, ignoreDefaultQuery = true, isTraining, searchQuery, @@ -398,7 +402,7 @@ export const loadDocsCount = async ({ query, }; - const resp: TrackTotalHitsSearchResponse = await ml.esSearch({ + const resp: TrackTotalHitsSearchResponse = await mlApiServices.esSearch({ index: destIndex, size: 0, body, diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/get_index_data.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/get_index_data.ts index 68ed524ec5c12..bd289f3a1cdd7 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/get_index_data.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/get_index_data.ts @@ -11,16 +11,19 @@ import { type DataFrameAnalyticsConfig } from '@kbn/ml-data-frame-analytics-util import type { EsSorting, UseDataGridReturnType } from '@kbn/ml-data-grid'; import { getProcessedFields, INDEX_STATUS } from '@kbn/ml-data-grid'; -import { ml } from '../../services/ml_api_service'; -import { newJobCapsServiceAnalytics } from '../../services/new_job_capabilities/new_job_capabilities_service_analytics'; +import { mlJobCapsServiceAnalyticsFactory } from '../../services/new_job_capabilities/new_job_capabilities_service_analytics'; +import type { MlApiServices } from '../../services/ml_api_service'; export const getIndexData = async ( + mlApiServices: MlApiServices, jobConfig: DataFrameAnalyticsConfig | undefined, dataGrid: UseDataGridReturnType, searchQuery: estypes.QueryDslQueryContainer, options: { didCancel: boolean } ) => { if (jobConfig !== undefined) { + const newJobCapsServiceAnalytics = mlJobCapsServiceAnalyticsFactory(mlApiServices); + const { pagination, setErrorMessage, @@ -47,7 +50,7 @@ export const getIndexData = async ( const { pageIndex, pageSize } = pagination; // TODO: remove results_field from `fields` when possible - const resp: estypes.SearchResponse = await ml.esSearch({ + const resp: estypes.SearchResponse = await mlApiServices.esSearch({ index: jobConfig.dest.index, body: { fields: ['*'], diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/get_index_fields.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/get_index_fields.ts index 54f76fdc1b52f..0cc1bb0b587c0 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/get_index_fields.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/get_index_fields.ts @@ -8,19 +8,22 @@ import type { ES_FIELD_TYPES } from '@kbn/field-types'; import type { DataFrameAnalyticsConfig } from '@kbn/ml-data-frame-analytics-utils'; -import { newJobCapsServiceAnalytics } from '../../services/new_job_capabilities/new_job_capabilities_service_analytics'; +import { mlJobCapsServiceAnalyticsFactory } from '../../services/new_job_capabilities/new_job_capabilities_service_analytics'; +import type { MlApiServices } from '../../services/ml_api_service'; export interface FieldTypes { [key: string]: ES_FIELD_TYPES; } export const getIndexFields = ( + mlApiServices: MlApiServices, jobConfig: DataFrameAnalyticsConfig | undefined, needsDestIndexFields: boolean ) => { if (jobConfig !== undefined) { - const { selectedFields: defaultSelected, docFields } = - newJobCapsServiceAnalytics.getDefaultFields(jobConfig, needsDestIndexFields); + const { selectedFields: defaultSelected, docFields } = mlJobCapsServiceAnalyticsFactory( + mlApiServices + ).getDefaultFields(jobConfig, needsDestIndexFields); const types: FieldTypes = {}; const allFields: string[] = []; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/use_results_view_config.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/use_results_view_config.ts index 22acd894d63ef..fba862558b766 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/use_results_view_config.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/use_results_view_config.ts @@ -19,14 +19,13 @@ import { type TotalFeatureImportance, } from '@kbn/ml-data-frame-analytics-utils'; -import { useMlKibana } from '../../contexts/kibana'; -import { ml } from '../../services/ml_api_service'; -import { newJobCapsServiceAnalytics } from '../../services/new_job_capabilities/new_job_capabilities_service_analytics'; +import { useMlApiContext, useMlKibana } from '../../contexts/kibana'; +import { useNewJobCapsServiceAnalytics } from '../../services/new_job_capabilities/new_job_capabilities_service_analytics'; import { useMlIndexUtils } from '../../util/index_service'; import { isGetDataFrameAnalyticsStatsResponseOk } from '../pages/analytics_management/services/analytics_service/get_analytics'; import { useTrainedModelsApiService } from '../../services/ml_api_service/trained_models'; -import { getToastNotificationService } from '../../services/toast_notification_service'; +import { useToastNotificationService } from '../../services/toast_notification_service'; import { getDestinationIndex } from './get_destination_index'; export const useResultsViewConfig = (jobId: string) => { @@ -35,8 +34,11 @@ export const useResultsViewConfig = (jobId: string) => { data: { dataViews }, }, } = useMlKibana(); + const toastNotificationService = useToastNotificationService(); + const ml = useMlApiContext(); const { getDataViewIdFromName } = useMlIndexUtils(); const trainedModelsApiService = useTrainedModelsApiService(); + const newJobCapsServiceAnalytics = useNewJobCapsServiceAnalytics(); const [dataView, setDataView] = useState(undefined); const [dataViewErrorMessage, setDataViewErrorMessage] = useState(undefined); @@ -92,7 +94,7 @@ export const useResultsViewConfig = (jobId: string) => { setTotalFeatureImportance(inferenceModel?.metadata?.total_feature_importance); } } catch (e) { - getToastNotificationService().displayErrorToast(e); + toastNotificationService.displayErrorToast(e); } } diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/advanced_step_form.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/advanced_step_form.tsx index 24577dd0dcc04..9b8dec570a5e9 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/advanced_step_form.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/advanced_step_form.tsx @@ -32,7 +32,7 @@ import { import { HyperParameters } from './hyper_parameters'; import type { CreateAnalyticsStepProps } from '../../../analytics_management/hooks/use_create_analytics_form'; import { getModelMemoryLimitErrors } from '../../../analytics_management/hooks/use_create_analytics_form/reducer'; -import { useMlKibana } from '../../../../../contexts/kibana'; +import { useMlKibana, useMlApiContext } from '../../../../../contexts/kibana'; import { DEFAULT_MODEL_MEMORY_LIMIT } from '../../../analytics_management/hooks/use_create_analytics_form/state'; import { ANALYTICS_STEPS } from '../../page'; import { fetchExplainData } from '../shared'; @@ -134,6 +134,7 @@ export const AdvancedStepForm: FC = ({ const { services: { docLinks }, } = useMlKibana(); + const mlApiServices = useMlApiContext(); const classAucRocDocLink = docLinks.links.ml.classificationAucRoc; const { setEstimatedModelMemoryLimit, setFormState } = actions; @@ -205,7 +206,10 @@ export const AdvancedStepForm: FC = ({ useEffect(() => { setFetchingAdvancedParamErrors(true); (async function () { - const { success, errorMessage, errorReason, expectedMemory } = await fetchExplainData(form); + const { success, errorMessage, errorReason, expectedMemory } = await fetchExplainData( + mlApiServices, + form + ); const paramErrors: AdvancedParamErrors = {}; if (success) { diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx index a06e3575cfce9..4f737c1137663 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx @@ -29,13 +29,13 @@ import { } from '@kbn/ml-data-frame-analytics-utils'; import { DataGrid } from '@kbn/ml-data-grid'; import { SEARCH_QUERY_LANGUAGE } from '@kbn/ml-query-utils'; -import { useMlKibana } from '../../../../../contexts/kibana'; +import { useMlApiContext, useMlKibana } from '../../../../../contexts/kibana'; import { EuiComboBoxWithFieldStats, FieldStatsFlyoutProvider, } from '../../../../../components/field_stats_flyout'; import type { FieldForStats } from '../../../../../components/field_stats_flyout/field_stats_info_button'; -import { newJobCapsServiceAnalytics } from '../../../../../services/new_job_capabilities/new_job_capabilities_service_analytics'; +import { useNewJobCapsServiceAnalytics } from '../../../../../services/new_job_capabilities/new_job_capabilities_service_analytics'; import { useDataSource } from '../../../../../contexts/ml'; import { getScatterplotMatrixLegendType } from '../../../../common/get_scatterplot_matrix_legend_type'; @@ -44,7 +44,6 @@ import { Messages } from '../shared'; import type { State } from '../../../analytics_management/hooks/use_create_analytics_form/state'; import { DEFAULT_MODEL_MEMORY_LIMIT } from '../../../analytics_management/hooks/use_create_analytics_form/state'; import { handleExplainErrorMessage, shouldAddAsDepVarOption } from './form_options_validation'; -import { getToastNotifications } from '../../../../../util/dependency_cache'; import { ANALYTICS_STEPS } from '../../page'; import { ContinueButton } from '../continue_button'; @@ -115,6 +114,10 @@ export const ConfigurationStepForm: FC = ({ setCurrentStep, sourceDataViewTitle, }) => { + const { services } = useMlKibana(); + const toastNotifications = services.notifications.toasts; + const mlApiServices = useMlApiContext(); + const newJobCapsServiceAnalytics = useNewJobCapsServiceAnalytics(); const { selectedDataView, selectedSavedSearch } = useDataSource(); const { savedSearchQuery, savedSearchQueryStr } = useSavedSearch(); @@ -167,8 +170,6 @@ export const ConfigurationStepForm: FC = ({ language: jobConfigQueryLanguage ?? SEARCH_QUERY_LANGUAGE.KUERY, }); - const toastNotifications = getToastNotifications(); - const setJobConfigQuery: ExplorationQueryBarProps['setSearchQuery'] = (update) => { if (update.query) { setFormState({ @@ -287,7 +288,7 @@ export const ConfigurationStepForm: FC = ({ fieldSelection, errorMessage, noDocsContainMappedFields: noDocsWithFields, - } = await fetchExplainData(formToUse); + } = await fetchExplainData(mlApiServices, formToUse); if (success) { if (shouldUpdateEstimatedMml) { @@ -443,7 +444,7 @@ export const ConfigurationStepForm: FC = ({ fieldSelection, errorMessage, noDocsContainMappedFields: noDocsWithFields, - } = await fetchExplainData(formCopy); + } = await fetchExplainData(mlApiServices, formCopy); if (success) { // update the field selection table const hasRequiredFields = fieldSelection.some( @@ -547,7 +548,6 @@ export const ConfigurationStepForm: FC = ({ // eslint-disable-next-line react-hooks/exhaustive-deps [dependentVariableEmpty, jobType, scatterplotMatrixProps.fields.length] ); - const { services } = useMlKibana(); const fieldStatsServices: FieldStatsServices = useMemo(() => { const { uiSettings, data, fieldFormats, charts } = services; return { diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/supported_fields_message.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/supported_fields_message.tsx index f7c8f30bff1a2..d8f3404cf2887 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/supported_fields_message.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/supported_fields_message.tsx @@ -19,7 +19,7 @@ import { } from '@kbn/ml-data-frame-analytics-utils'; import type { AnalyticsJobType } from '../../../analytics_management/hooks/use_create_analytics_form/state'; import { CATEGORICAL_TYPES } from './form_options_validation'; -import { newJobCapsServiceAnalytics } from '../../../../../services/new_job_capabilities/new_job_capabilities_service_analytics'; +import { useNewJobCapsServiceAnalytics } from '../../../../../services/new_job_capabilities/new_job_capabilities_service_analytics'; const containsClassificationFieldsCb = ({ name, type }: Field) => !OMIT_FIELDS.includes(name) && @@ -73,7 +73,7 @@ export const SupportedFieldsMessage: FC = ({ jobType }) => { const [sourceIndexContainsSupportedFields, setSourceIndexContainsSupportedFields] = useState(true); const [sourceIndexFieldsCheckFailed, setSourceIndexFieldsCheckFailed] = useState(false); - const { fields } = newJobCapsServiceAnalytics; + const { fields } = useNewJobCapsServiceAnalytics(); // Find out if data view contains supported fields for job type. Provides a hint in the form // that job may not run correctly if no supported fields are found. diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_analytics_advanced_editor/create_analytics_advanced_editor.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_analytics_advanced_editor/create_analytics_advanced_editor.tsx index cdc93fbeb4feb..a2a6e965c3e70 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_analytics_advanced_editor/create_analytics_advanced_editor.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_analytics_advanced_editor/create_analytics_advanced_editor.tsx @@ -14,8 +14,7 @@ import { i18n } from '@kbn/i18n'; import { extractErrorMessage } from '@kbn/ml-error-utils'; import { dynamic } from '@kbn/shared-ux-utility'; -import { useNotifications } from '../../../../../contexts/kibana'; -import { ml } from '../../../../../services/ml_api_service'; +import { useMlApiContext, useNotifications } from '../../../../../contexts/kibana'; import type { CreateAnalyticsFormProps } from '../../../analytics_management/hooks/use_create_analytics_form'; import { CreateStep } from '../create_step'; import { ANALYTICS_STEPS } from '../../page'; @@ -25,6 +24,7 @@ const EditorComponent = dynamic(async () => ({ })); export const CreateAnalyticsAdvancedEditor: FC = (props) => { + const ml = useMlApiContext(); const { actions, state } = props; const { setAdvancedEditorRawString, setFormState } = actions; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_step_footer/create_step_footer.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_step_footer/create_step_footer.tsx index 307308ddb65b9..b7dac69e1226e 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_step_footer/create_step_footer.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_step_footer/create_step_footer.tsx @@ -16,8 +16,7 @@ import { } from '@kbn/ml-data-frame-analytics-utils'; import { getDataFrameAnalyticsProgressPhase } from '../../../analytics_management/components/analytics_list/common'; import { isGetDataFrameAnalyticsStatsResponseOk } from '../../../analytics_management/services/analytics_service/get_analytics'; -import { useMlKibana } from '../../../../../contexts/kibana'; -import { ml } from '../../../../../services/ml_api_service'; +import { useMlApiContext, useMlKibana } from '../../../../../contexts/kibana'; import { BackToListPanel } from '../back_to_list_panel'; import { ViewResultsPanel } from '../view_results_panel'; import { ProgressStats } from './progress_stats'; @@ -49,6 +48,7 @@ export const CreateStepFooter: FC = ({ jobId, jobType, showProgress }) => const { services: { notifications }, } = useMlKibana(); + const ml = useMlApiContext(); useEffect(() => { setInitialized(true); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_form.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_form.tsx index 3cc53d3c95ad0..d80c23732dcbd 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_form.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_form.tsx @@ -14,12 +14,11 @@ import { extractErrorMessage } from '@kbn/ml-error-utils'; import { CreateDataViewForm } from '@kbn/ml-data-view-utils/components/create_data_view_form_row'; import { DestinationIndexForm } from '@kbn/ml-creation-wizard-utils/components/destination_index_form'; -import { useMlKibana } from '../../../../../contexts/kibana'; +import { useMlApiContext, useMlKibana } from '../../../../../contexts/kibana'; import type { CreateAnalyticsStepProps } from '../../../analytics_management/hooks/use_create_analytics_form'; import { JOB_ID_MAX_LENGTH } from '../../../../../../../common/constants/validation'; import { ContinueButton } from '../continue_button'; import { ANALYTICS_STEPS } from '../../page'; -import { ml } from '../../../../../services/ml_api_service'; import { useCanCreateDataView } from '../../hooks/use_can_create_data_view'; import { useDataViewTimeFields } from '../../hooks/use_data_view_time_fields'; import { AdditionalSection } from './additional_section'; @@ -43,6 +42,7 @@ export const DetailsStepForm: FC = ({ const { services: { docLinks, notifications }, } = useMlKibana(); + const ml = useMlApiContext(); const canCreateDataView = useCanCreateDataView(); const { dataViewAvailableTimeFields, onTimeFieldChanged } = useDataViewTimeFields({ diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/shared/fetch_explain_data.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/shared/fetch_explain_data.ts index 95a6381ac3fc4..680415e189354 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/shared/fetch_explain_data.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/shared/fetch_explain_data.ts @@ -11,11 +11,11 @@ import type { DfAnalyticsExplainResponse, FieldSelectionItem, } from '@kbn/ml-data-frame-analytics-utils'; -import { ml } from '../../../../../services/ml_api_service'; import type { State } from '../../../analytics_management/hooks/use_create_analytics_form/state'; import { getJobConfigFromFormState } from '../../../analytics_management/hooks/use_create_analytics_form/state'; +import type { MlApiServices } from '../../../../../services/ml_api_service'; -export const fetchExplainData = async (formState: State['form']) => { +export const fetchExplainData = async (mlApiServices: MlApiServices, formState: State['form']) => { const jobConfig = getJobConfigFromFormState(formState); let errorMessage = ''; let errorReason = ''; @@ -28,9 +28,8 @@ export const fetchExplainData = async (formState: State['form']) => { delete jobConfig.dest; delete jobConfig.model_memory_limit; delete jobConfig.analyzed_fields; - const resp: DfAnalyticsExplainResponse = await ml.dataFrameAnalytics.explainDataFrameAnalytics( - jobConfig - ); + const resp: DfAnalyticsExplainResponse = + await mlApiServices.dataFrameAnalytics.explainDataFrameAnalytics(jobConfig); expectedMemory = resp.memory_estimation?.expected_memory_without_disk; fieldSelection = resp.field_selection || []; } catch (error) { diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/hooks/use_index_data.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/hooks/use_index_data.ts index 110307fdb4024..80abafecd83ed 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/hooks/use_index_data.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/hooks/use_index_data.ts @@ -34,10 +34,9 @@ import { INDEX_STATUS, } from '@kbn/ml-data-grid'; +import { useMlApiContext } from '../../../../contexts/kibana'; import { DataLoader } from '../../../../datavisualizer/index_based/data_loader'; -import { ml } from '../../../../services/ml_api_service'; - type IndexSearchResponse = estypes.SearchResponse; interface MLEuiDataGridColumn extends EuiDataGridColumn { @@ -82,6 +81,7 @@ export const useIndexData = ( toastNotifications: CoreSetup['notifications']['toasts'], runtimeMappings?: RuntimeMappings ): UseIndexDataReturnType => { + const ml = useMlApiContext(); // Fetch 500 random documents to determine populated fields. // This is a workaround to avoid passing potentially thousands of unpopulated fields // (for example, as part of filebeat/metricbeat/ECS based indices) @@ -258,7 +258,7 @@ export const useIndexData = ( ]); const dataLoader = useMemo( - () => new DataLoader(dataView, toastNotifications), + () => new DataLoader(dataView, ml), // eslint-disable-next-line react-hooks/exhaustive-deps [dataView] ); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/page.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/page.tsx index c9b345c460030..4890ac59ffbe6 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/page.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/page.tsx @@ -21,7 +21,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import type { DataFrameAnalyticsId } from '@kbn/ml-data-frame-analytics-utils'; import { useDataSource } from '../../../contexts/ml/data_source_context'; -import { ml } from '../../../services/ml_api_service'; +import { useMlApiContext } from '../../../contexts/kibana'; import { useCreateAnalyticsForm } from '../analytics_management/hooks/use_create_analytics_form'; import { CreateAnalyticsAdvancedEditor } from './components/create_analytics_advanced_editor'; import { @@ -46,6 +46,7 @@ interface Props { } export const Page: FC = ({ jobId }) => { + const ml = useMlApiContext(); const [currentStep, setCurrentStep] = useState(ANALYTICS_STEPS.CONFIGURATION); const [activatedSteps, setActivatedSteps] = useState([ true, diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/use_confusion_matrix.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/use_confusion_matrix.ts index 8c638c0617f89..74ca62a874081 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/use_confusion_matrix.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/use_confusion_matrix.ts @@ -16,11 +16,11 @@ import { type DataFrameAnalyticsConfig, } from '@kbn/ml-data-frame-analytics-utils'; -import { newJobCapsServiceAnalytics } from '../../../../../services/new_job_capabilities/new_job_capabilities_service_analytics'; +import { useNewJobCapsServiceAnalytics } from '../../../../../services/new_job_capabilities/new_job_capabilities_service_analytics'; +import { useMlApiContext } from '../../../../../contexts/kibana'; import type { ResultsSearchQuery, ClassificationMetricItem } from '../../../../common/analytics'; import { isClassificationEvaluateResponse } from '../../../../common/analytics'; - import { loadEvalData, loadDocsCount } from '../../../../common'; import { isTrainingFilter } from './is_training_filter'; @@ -60,6 +60,8 @@ export const useConfusionMatrix = ( jobConfig: DataFrameAnalyticsConfig, searchQuery: ResultsSearchQuery ) => { + const mlApiServices = useMlApiContext(); + const newJobCapsServiceAnalytics = useNewJobCapsServiceAnalytics(); const [confusionMatrixData, setConfusionMatrixData] = useState([]); const [overallAccuracy, setOverallAccuracy] = useState(null); const [avgRecall, setAvgRecall] = useState(null); @@ -87,6 +89,7 @@ export const useConfusionMatrix = ( } const evalData = await loadEvalData({ + mlApiServices, isTraining, index: jobConfig.dest.index, dependentVariable, @@ -98,6 +101,7 @@ export const useConfusionMatrix = ( }); const docsCountResp = await loadDocsCount({ + mlApiServices, isTraining, searchQuery, resultsField, diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/use_roc_curve.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/use_roc_curve.ts index c411a624bd434..88dd6f20cd75d 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/use_roc_curve.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/use_roc_curve.ts @@ -15,7 +15,8 @@ import { type RocCurveItem, } from '@kbn/ml-data-frame-analytics-utils'; -import { newJobCapsServiceAnalytics } from '../../../../../services/new_job_capabilities/new_job_capabilities_service_analytics'; +import { useNewJobCapsServiceAnalytics } from '../../../../../services/new_job_capabilities/new_job_capabilities_service_analytics'; +import { useMlApiContext } from '../../../../../contexts/kibana'; import type { ResultsSearchQuery } from '../../../../common/analytics'; import { isClassificationEvaluateResponse } from '../../../../common/analytics'; @@ -39,6 +40,8 @@ export const useRocCurve = ( searchQuery: ResultsSearchQuery, columns: string[] ) => { + const mlApiServices = useMlApiContext(); + const newJobCapsServiceAnalytics = useNewJobCapsServiceAnalytics(); const classificationClasses = columns.filter( (d) => d !== ACTUAL_CLASS_ID && d !== OTHER_CLASS_ID ); @@ -74,6 +77,7 @@ export const useRocCurve = ( for (let i = 0; i < classificationClasses.length; i++) { const rocCurveClassName = classificationClasses[i]; const evalData = await loadEvalData({ + mlApiServices, isTraining: isTrainingFilter(searchQuery, resultsField), index: jobConfig.dest.index, dependentVariable, diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/expandable_section/expandable_section_analytics.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/expandable_section/expandable_section_analytics.tsx index 84ce34eb8d8b7..10e302fad2249 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/expandable_section/expandable_section_analytics.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/expandable_section/expandable_section_analytics.tsx @@ -17,13 +17,11 @@ import { type DataFrameAnalysisConfigType, } from '@kbn/ml-data-frame-analytics-utils'; -import { ml } from '../../../../../services/ml_api_service'; - import { isGetDataFrameAnalyticsStatsResponseOk } from '../../../analytics_management/services/analytics_service/get_analytics'; import type { DataFrameAnalyticsListRow } from '../../../analytics_management/components/analytics_list/common'; import { DATA_FRAME_MODE } from '../../../analytics_management/components/analytics_list/common'; import { ExpandedRow } from '../../../analytics_management/components/analytics_list/expanded_row'; - +import { useMlApiContext } from '../../../../../contexts/kibana'; import type { ExpandableSectionProps } from './expandable_section'; import { ExpandableSection, HEADER_ITEMS_LOADING } from './expandable_section'; @@ -77,6 +75,8 @@ interface ExpandableSectionAnalyticsProps { } export const ExpandableSectionAnalytics: FC = ({ jobId }) => { + const ml = useMlApiContext(); + const [expandedRowItem, setExpandedRowItem] = useState(); const fetchStats = async () => { diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/expandable_section/expandable_section_results.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/expandable_section/expandable_section_results.tsx index 9bbd3996fa3b6..212c0fa6d2cf7 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/expandable_section/expandable_section_results.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/expandable_section/expandable_section_results.tsx @@ -40,7 +40,6 @@ import { import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { SEARCH_QUERY_LANGUAGE } from '@kbn/ml-query-utils'; -import { getToastNotifications } from '../../../../../util/dependency_cache'; import type { useColorRange } from '../../../../../components/color_range_legend'; import { ColorRangeLegend } from '../../../../../components/color_range_legend'; import { useMlKibana } from '../../../../../contexts/kibana'; @@ -140,6 +139,7 @@ export const ExpandableSectionResults: FC = ({ share, data, http: { basePath }, + notifications: { toasts }, }, } = useMlKibana(); @@ -394,7 +394,7 @@ export const ExpandableSectionResults: FC = ({ } dataTestSubj="mlExplorationDataGrid" renderCellPopover={renderCellPopover} - toastNotifications={getToastNotifications()} + toastNotifications={toasts} /> )} diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/exploration_results_table.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/exploration_results_table.tsx index e92e8f665bc37..c3d41cb4c4307 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/exploration_results_table.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/exploration_results_table.tsx @@ -14,9 +14,6 @@ import type { DataFrameTaskStateType, } from '@kbn/ml-data-frame-analytics-utils'; -import { getToastNotifications } from '../../../../../util/dependency_cache'; -import { useMlKibana } from '../../../../../contexts/kibana'; - import type { ResultsSearchQuery } from '../../../../common/analytics'; import { ExpandableSectionResults } from '../expandable_section'; @@ -33,19 +30,7 @@ interface Props { export const ExplorationResultsTable: FC = React.memo( ({ dataView, jobConfig, needsDestDataView, searchQuery }) => { - const { - services: { - mlServices: { mlApiServices }, - }, - } = useMlKibana(); - - const classificationData = useExplorationResults( - dataView, - jobConfig, - searchQuery, - getToastNotifications(), - mlApiServices - ); + const classificationData = useExplorationResults(dataView, jobConfig, searchQuery); if (jobConfig === undefined || classificationData === undefined) { return null; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/use_exploration_results.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/use_exploration_results.ts index f34d597bf4265..34ad61d02cd3b 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/use_exploration_results.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/use_exploration_results.ts @@ -9,7 +9,6 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import type { EuiDataGridColumn } from '@elastic/eui'; -import type { CoreSetup } from '@kbn/core/public'; import { i18n } from '@kbn/i18n'; import type { DataView } from '@kbn/data-views-plugin/public'; import { extractErrorMessage } from '@kbn/ml-error-utils'; @@ -35,7 +34,7 @@ import { } from '@kbn/ml-data-grid'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import type { MlApiServices } from '../../../../../services/ml_api_service'; +import { useMlApiContext, useMlKibana } from '../../../../../contexts/kibana'; import { DataLoader } from '../../../../../datavisualizer/index_based/data_loader'; import { getIndexData, getIndexFields } from '../../../../common'; @@ -45,10 +44,14 @@ import { useExplorationDataGrid } from './use_exploration_data_grid'; export const useExplorationResults = ( dataView: DataView | undefined, jobConfig: DataFrameAnalyticsConfig | undefined, - searchQuery: estypes.QueryDslQueryContainer, - toastNotifications: CoreSetup['notifications']['toasts'], - mlApiServices: MlApiServices + searchQuery: estypes.QueryDslQueryContainer ): UseIndexDataReturnType => { + const { + services: { + notifications: { toasts }, + }, + } = useMlKibana(); + const ml = useMlApiContext(); const [baseline, setBaseLine] = useState(); const trainedModelsApiService = useTrainedModelsApiService(); @@ -60,7 +63,7 @@ export const useExplorationResults = ( if (jobConfig !== undefined) { const resultsField = jobConfig.dest.results_field!; - const { fieldTypes } = getIndexFields(jobConfig, needsDestIndexFields); + const { fieldTypes } = getIndexFields(ml, jobConfig, needsDestIndexFields); columns.push( ...getDataGridSchemasFromFieldTypes(fieldTypes, resultsField).sort((a: any, b: any) => sortExplorationResultsFields(a.id, b.id, jobConfig) @@ -81,7 +84,7 @@ export const useExplorationResults = ( // passed on to `getIndexData`. useEffect(() => { const options = { didCancel: false }; - getIndexData(jobConfig, dataGrid, searchQuery, options); + getIndexData(ml, jobConfig, dataGrid, searchQuery, options); return () => { options.didCancel = true; }; @@ -90,7 +93,7 @@ export const useExplorationResults = ( }, [jobConfig && jobConfig.id, dataGrid.pagination, searchQuery, dataGrid.sortingColumns]); const dataLoader = useMemo( - () => (dataView !== undefined ? new DataLoader(dataView, toastNotifications) : undefined), + () => (dataView !== undefined ? new DataLoader(dataView, ml) : undefined), // eslint-disable-next-line react-hooks/exhaustive-deps [dataView] ); @@ -110,7 +113,7 @@ export const useExplorationResults = ( dataGrid.setColumnCharts(columnChartsData); } } catch (e) { - showDataGridColumnChartErrorMessageToast(e, toastNotifications); + showDataGridColumnChartErrorMessageToast(e, toasts); } }; @@ -158,7 +161,7 @@ export const useExplorationResults = ( } catch (e) { const error = extractErrorMessage(e); - toastNotifications.addDanger({ + toasts.addDanger({ title: i18n.translate( 'xpack.ml.dataframe.analytics.explorationResults.baselineErrorMessageToast', { @@ -169,7 +172,7 @@ export const useExplorationResults = ( }); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [mlApiServices, jobConfig]); + }, [jobConfig]); useEffect(() => { getAnalyticsBaseline(); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.ts index 38d439067897a..5b9ce4646b9e7 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.ts @@ -33,7 +33,7 @@ import { COLOR_RANGE, COLOR_RANGE_SCALE, } from '../../../../../components/color_range_legend'; -import { getToastNotifications } from '../../../../../util/dependency_cache'; +import { useMlApiContext, useMlKibana } from '../../../../../contexts/kibana'; import { getIndexData, getIndexFields } from '../../../../common'; @@ -45,6 +45,12 @@ export const useOutlierData = ( jobConfig: DataFrameAnalyticsConfig | undefined, searchQuery: estypes.QueryDslQueryContainer ): UseIndexDataReturnType => { + const { + services: { + notifications: { toasts }, + }, + } = useMlKibana(); + const ml = useMlApiContext(); const needsDestIndexFields = dataView !== undefined && dataView.title === jobConfig?.source.index[0]; @@ -53,7 +59,7 @@ export const useOutlierData = ( if (jobConfig !== undefined && dataView !== undefined) { const resultsField = jobConfig.dest.results_field; - const { fieldTypes } = getIndexFields(jobConfig, needsDestIndexFields); + const { fieldTypes } = getIndexFields(ml, jobConfig, needsDestIndexFields); newColumns.push( ...getDataGridSchemasFromFieldTypes(fieldTypes, resultsField!).sort((a: any, b: any) => sortExplorationResultsFields(a.id, b.id, jobConfig) @@ -86,7 +92,7 @@ export const useOutlierData = ( // passed on to `getIndexData`. useEffect(() => { const options = { didCancel: false }; - getIndexData(jobConfig, dataGrid, searchQuery, options); + getIndexData(ml, jobConfig, dataGrid, searchQuery, options); return () => { options.didCancel = true; }; @@ -95,7 +101,9 @@ export const useOutlierData = ( }, [jobConfig && jobConfig.id, dataGrid.pagination, searchQuery, dataGrid.sortingColumns]); const dataLoader = useMemo( - () => (dataView !== undefined ? new DataLoader(dataView, getToastNotifications()) : undefined), + () => (dataView !== undefined ? new DataLoader(dataView, ml) : undefined), + // skip ml API services from deps check + // eslint-disable-next-line react-hooks/exhaustive-deps [dataView] ); @@ -114,7 +122,7 @@ export const useOutlierData = ( dataGrid.setColumnCharts(columnChartsData); } } catch (e) { - showDataGridColumnChartErrorMessageToast(e, getToastNotifications()); + showDataGridColumnChartErrorMessageToast(e, toasts); } }; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx index 958714e9b98bd..398e0ecb61505 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx @@ -28,7 +28,7 @@ import { } from '@kbn/ml-data-frame-analytics-utils'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { useMlKibana } from '../../../../../contexts/kibana'; +import { useMlApiContext, useMlKibana } from '../../../../../contexts/kibana'; import type { Eval } from '../../../../common'; import { getValuesFromResponse, loadEvalData, loadDocsCount } from '../../../../common'; @@ -65,6 +65,7 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, searchQuery }) const { services: { docLinks }, } = useMlKibana(); + const mlApiServices = useMlApiContext(); const docLink = docLinks.links.ml.regressionEvaluation; const [trainingEval, setTrainingEval] = useState(defaultEval); const [generalizationEval, setGeneralizationEval] = useState(defaultEval); @@ -84,6 +85,7 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, searchQuery }) setIsLoadingGeneralization(true); const genErrorEval = await loadEvalData({ + mlApiServices, isTraining: false, index, dependentVariable, @@ -122,6 +124,7 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, searchQuery }) setIsLoadingTraining(true); const trainingErrorEval = await loadEvalData({ + mlApiServices, isTraining: true, index, dependentVariable, @@ -159,6 +162,7 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, searchQuery }) const loadData = async () => { loadGeneralizationData(false); const genDocsCountResp = await loadDocsCount({ + mlApiServices, ignoreDefaultQuery: false, isTraining: false, searchQuery, @@ -173,6 +177,7 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, searchQuery }) loadTrainingData(false); const trainDocsCountResp = await loadDocsCount({ + mlApiServices, ignoreDefaultQuery: false, isTraining: true, searchQuery, diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/delete_action_name.test.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/delete_action_name.test.tsx index 03364165095d2..2ae6a2ae1b265 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/delete_action_name.test.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/delete_action_name.test.tsx @@ -21,11 +21,8 @@ jest.mock('../../../../../capabilities/check_capabilities', () => ({ createPermissionFailureMessage: jest.fn(), })); -jest.mock('../../../../../util/dependency_cache', () => ({ - getToastNotifications: () => ({ addSuccess: jest.fn(), addDanger: jest.fn() }), -})); - jest.mock('../../../../../contexts/kibana', () => ({ + useMlApiContext: jest.fn(), useMlKibana: () => ({ services: { ...mockCoreServices.createStart(), data: { data_view: { find: jest.fn() } } }, }), diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/use_delete_action.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/use_delete_action.tsx index 3357febd9398f..01a89135c9c34 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/use_delete_action.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/use_delete_action.tsx @@ -14,9 +14,9 @@ import { useMlKibana } from '../../../../../contexts/kibana'; import { useToastNotificationService } from '../../../../../services/toast_notification_service'; import { - deleteAnalytics, - deleteAnalyticsAndDestIndex, - canDeleteIndex, + useDeleteAnalytics, + useDeleteAnalyticsAndDestIndex, + useCanDeleteIndex, } from '../../services/analytics_service'; import type { @@ -56,6 +56,9 @@ export const useDeleteAction = (canDeleteDataFrameAnalytics: boolean) => { const indexName = getDestinationIndex(item?.config); const toastNotificationService = useToastNotificationService(); + const deleteAnalytics = useDeleteAnalytics(); + const deleteAnalyticsAndDestIndex = useDeleteAnalyticsAndDestIndex(); + const canDeleteIndex = useCanDeleteIndex(); const checkDataViewExists = async () => { try { @@ -83,7 +86,7 @@ export const useDeleteAction = (canDeleteDataFrameAnalytics: boolean) => { }; const checkUserIndexPermission = async () => { try { - const userCanDelete = await canDeleteIndex(indexName, toastNotificationService); + const userCanDelete = await canDeleteIndex(indexName); if (userCanDelete) { setUserCanDeleteIndex(true); } @@ -133,11 +136,10 @@ export const useDeleteAction = (canDeleteDataFrameAnalytics: boolean) => { deleteAnalyticsAndDestIndex( item.config, deleteTargetIndex, - dataViewExists && deleteDataView, - toastNotificationService + dataViewExists && deleteDataView ); } else { - deleteAnalytics(item.config, toastNotificationService); + deleteAnalytics(item.config); } } }; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_edit/edit_action_flyout.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_edit/edit_action_flyout.tsx index 90850dd3a9666..cc66cf0c34dbf 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_edit/edit_action_flyout.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_edit/edit_action_flyout.tsx @@ -37,7 +37,6 @@ import { } from '@kbn/ml-data-frame-analytics-utils'; import { useMlKibana, useMlApiContext } from '../../../../../contexts/kibana'; -import { ml } from '../../../../../services/ml_api_service'; import { useToastNotificationService } from '../../../../../services/toast_notification_service'; import type { MemoryInputValidatorResult } from '../../../../../../../common/util/validators'; import { memoryInputValidator } from '../../../../../../../common/util/validators'; @@ -70,10 +69,10 @@ export const EditActionFlyout: FC> = ({ closeFlyout, item } } = useMlKibana(); const { refresh } = useRefreshAnalyticsList(); - const mlApiServices = useMlApiContext(); + const ml = useMlApiContext(); const { dataFrameAnalytics: { getDataFrameAnalytics }, - } = mlApiServices; + } = ml; const toastNotificationService = useToastNotificationService(); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_start/use_start_action.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_start/use_start_action.tsx index 78bed8273de5d..dbe12f0e2c287 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_start/use_start_action.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_start/use_start_action.tsx @@ -16,8 +16,7 @@ import { isDataFrameAnalyticsFailed, isDataFrameAnalyticsRunning, } from '../analytics_list/common'; -import { startAnalytics } from '../../services/analytics_service'; -import { useToastNotificationService } from '../../../../../services/toast_notification_service'; +import { useStartAnalytics } from '../../services/analytics_service'; import { startActionNameText, StartActionName } from './start_action_name'; @@ -27,13 +26,13 @@ export const useStartAction = (canStartStopDataFrameAnalytics: boolean) => { const [item, setItem] = useState(); - const toastNotificationService = useToastNotificationService(); + const startAnalytics = useStartAnalytics(); const closeModal = () => setModalVisible(false); const startAndCloseModal = () => { if (item !== undefined) { setModalVisible(false); - startAnalytics(item, toastNotificationService); + startAnalytics(item); } }; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_stop/use_stop_action.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_stop/use_stop_action.tsx index e972c85e19f2e..8cc223e8aebe6 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_stop/use_stop_action.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_stop/use_stop_action.tsx @@ -12,14 +12,15 @@ import type { DataFrameAnalyticsListRow, } from '../analytics_list/common'; import { isDataFrameAnalyticsFailed, isDataFrameAnalyticsRunning } from '../analytics_list/common'; -import { stopAnalytics } from '../../services/analytics_service'; +import { useStopAnalytics } from '../../services/analytics_service'; import { stopActionNameText, StopActionName } from './stop_action_name'; export type StopAction = ReturnType; export const useStopAction = (canStartStopDataFrameAnalytics: boolean) => { - const [isModalVisible, setModalVisible] = useState(false); + const stopAnalytics = useStopAnalytics(); + const [isModalVisible, setModalVisible] = useState(false); const [item, setItem] = useState(); const closeModal = () => setModalVisible(false); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx index 9cf3636604450..8f192f3919e16 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx @@ -30,7 +30,7 @@ import { ML_PAGES } from '../../../../../../../common/constants/locator'; import type { DataFrameAnalyticsListRow, ItemIdToExpandedRowMap } from './common'; import { DataFrameAnalyticsListColumn } from './common'; -import { getAnalyticsFactory } from '../../services/analytics_service'; +import { useGetAnalytics } from '../../services/analytics_service'; import { getJobTypeBadge, getTaskStateBadge, useColumns } from './use_columns'; import { ExpandedRow } from './expanded_row'; import type { AnalyticStatsBarStats } from '../../../../../components/stats_bar'; @@ -127,7 +127,7 @@ export const DataFrameAnalyticsList: FC = ({ const disabled = !canCreateDataFrameAnalytics || !canStartStopDataFrameAnalytics; - const getAnalytics = getAnalyticsFactory( + const getAnalytics = useGetAnalytics( setAnalytics, setAnalyticsStats, setErrorMessage, diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row_messages_pane.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row_messages_pane.tsx index 9336959a8f0cc..cdfd3f00ff3dc 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row_messages_pane.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row_messages_pane.tsx @@ -10,7 +10,7 @@ import './expanded_row_messages_pane.scss'; import type { FC } from 'react'; import React, { useState, useEffect, useCallback } from 'react'; import { i18n } from '@kbn/i18n'; -import { ml } from '../../../../../services/ml_api_service'; +import { useMlApiContext } from '../../../../../contexts/kibana'; import { useRefreshAnalyticsList } from '../../../../common'; import { JobMessages } from '../../../../../components/job_messages'; import type { JobMessage } from '../../../../../../../common/types/audit_message'; @@ -22,6 +22,7 @@ interface Props { } export const ExpandedRowMessagesPane: FC = ({ analyticsId, dataTestSubj }) => { + const ml = useMlApiContext(); const [messages, setMessages] = useState([]); const [isLoading, setIsLoading] = useState(false); const [errorMessage, setErrorMessage] = useState(''); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts index c6239a139ee7d..f458c11551698 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts @@ -13,9 +13,8 @@ import { extractErrorMessage } from '@kbn/ml-error-utils'; import { extractErrorProperties } from '@kbn/ml-error-utils'; import type { DataFrameAnalyticsConfig } from '@kbn/ml-data-frame-analytics-utils'; -import { useMlKibana } from '../../../../../contexts/kibana'; +import { useMlApiContext, useMlKibana } from '../../../../../contexts/kibana'; import type { DeepReadonly } from '../../../../../../../common/types/common'; -import { ml } from '../../../../../services/ml_api_service'; import { useRefreshAnalyticsList } from '../../../../common'; import { extractCloningConfig, isAdvancedConfig } from '../../components/action_clone'; @@ -50,6 +49,7 @@ export const useCreateAnalyticsForm = (): CreateAnalyticsFormProps => { data: { dataViews }, }, } = useMlKibana(); + const ml = useMlApiContext(); const [state, dispatch] = useReducer(reducer, getInitialState()); const { refresh } = useRefreshAnalyticsList(); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/delete_analytics.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/delete_analytics.ts index b7eeacb1b8c66..281ebeccabac9 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/delete_analytics.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/delete_analytics.ts @@ -8,143 +8,159 @@ import { i18n } from '@kbn/i18n'; import { extractErrorMessage } from '@kbn/ml-error-utils'; -import { ml } from '../../../../../services/ml_api_service'; -import type { ToastNotificationService } from '../../../../../services/toast_notification_service'; +import { useMlApiContext } from '../../../../../contexts/kibana'; +import { useToastNotificationService } from '../../../../../services/toast_notification_service'; import { refreshAnalyticsList$, REFRESH_ANALYTICS_LIST_STATE } from '../../../../common'; import type { DataFrameAnalyticsListRow } from '../../components/analytics_list/common'; -export const deleteAnalytics = async ( - analyticsConfig: DataFrameAnalyticsListRow['config'], - toastNotificationService: ToastNotificationService -) => { - try { - await ml.dataFrameAnalytics.deleteDataFrameAnalytics(analyticsConfig.id); - toastNotificationService.displaySuccessToast( - i18n.translate('xpack.ml.dataframe.analyticsList.deleteAnalyticsSuccessMessage', { - defaultMessage: 'Request to delete data frame analytics job {analyticsId} acknowledged.', - values: { analyticsId: analyticsConfig.id }, - }) - ); - } catch (e) { - toastNotificationService.displayErrorToast( - e, - i18n.translate('xpack.ml.dataframe.analyticsList.deleteAnalyticsErrorMessage', { - defaultMessage: 'An error occurred deleting the data frame analytics job {analyticsId}', - values: { analyticsId: analyticsConfig.id }, - }) - ); - } - refreshAnalyticsList$.next(REFRESH_ANALYTICS_LIST_STATE.REFRESH); -}; +export const useDeleteAnalytics = () => { + const toastNotificationService = useToastNotificationService(); + const ml = useMlApiContext(); -export const deleteAnalyticsAndDestIndex = async ( - analyticsConfig: DataFrameAnalyticsListRow['config'], - deleteDestIndex: boolean, - deleteDestDataView: boolean, - toastNotificationService: ToastNotificationService -) => { - const destinationIndex = analyticsConfig.dest.index; - try { - const status = await ml.dataFrameAnalytics.deleteDataFrameAnalyticsAndDestIndex( - analyticsConfig.id, - deleteDestIndex, - deleteDestDataView - ); - if (status.analyticsJobDeleted?.success) { + return async (analyticsConfig: DataFrameAnalyticsListRow['config']) => { + try { + await ml.dataFrameAnalytics.deleteDataFrameAnalytics(analyticsConfig.id); toastNotificationService.displaySuccessToast( i18n.translate('xpack.ml.dataframe.analyticsList.deleteAnalyticsSuccessMessage', { defaultMessage: 'Request to delete data frame analytics job {analyticsId} acknowledged.', values: { analyticsId: analyticsConfig.id }, }) ); - } - if (status.analyticsJobDeleted?.error) { + } catch (e) { toastNotificationService.displayErrorToast( - status.analyticsJobDeleted.error, + e, i18n.translate('xpack.ml.dataframe.analyticsList.deleteAnalyticsErrorMessage', { defaultMessage: 'An error occurred deleting the data frame analytics job {analyticsId}', values: { analyticsId: analyticsConfig.id }, }) ); } + refreshAnalyticsList$.next(REFRESH_ANALYTICS_LIST_STATE.REFRESH); + }; +}; - if (status.destIndexDeleted?.success) { - toastNotificationService.displaySuccessToast( - i18n.translate('xpack.ml.dataframe.analyticsList.deleteAnalyticsWithIndexSuccessMessage', { - defaultMessage: 'Request to delete destination index {destinationIndex} acknowledged.', - values: { destinationIndex }, - }) +export const useDeleteAnalyticsAndDestIndex = () => { + const toastNotificationService = useToastNotificationService(); + const ml = useMlApiContext(); + + return async ( + analyticsConfig: DataFrameAnalyticsListRow['config'], + deleteDestIndex: boolean, + deleteDestDataView: boolean + ) => { + const destinationIndex = analyticsConfig.dest.index; + try { + const status = await ml.dataFrameAnalytics.deleteDataFrameAnalyticsAndDestIndex( + analyticsConfig.id, + deleteDestIndex, + deleteDestDataView ); - } - if (status.destIndexDeleted?.error) { + if (status.analyticsJobDeleted?.success) { + toastNotificationService.displaySuccessToast( + i18n.translate('xpack.ml.dataframe.analyticsList.deleteAnalyticsSuccessMessage', { + defaultMessage: + 'Request to delete data frame analytics job {analyticsId} acknowledged.', + values: { analyticsId: analyticsConfig.id }, + }) + ); + } + if (status.analyticsJobDeleted?.error) { + toastNotificationService.displayErrorToast( + status.analyticsJobDeleted.error, + i18n.translate('xpack.ml.dataframe.analyticsList.deleteAnalyticsErrorMessage', { + defaultMessage: 'An error occurred deleting the data frame analytics job {analyticsId}', + values: { analyticsId: analyticsConfig.id }, + }) + ); + } + + if (status.destIndexDeleted?.success) { + toastNotificationService.displaySuccessToast( + i18n.translate( + 'xpack.ml.dataframe.analyticsList.deleteAnalyticsWithIndexSuccessMessage', + { + defaultMessage: + 'Request to delete destination index {destinationIndex} acknowledged.', + values: { destinationIndex }, + } + ) + ); + } + if (status.destIndexDeleted?.error) { + toastNotificationService.displayErrorToast( + status.destIndexDeleted.error, + i18n.translate('xpack.ml.dataframe.analyticsList.deleteAnalyticsWithIndexErrorMessage', { + defaultMessage: 'An error occurred deleting destination index {destinationIndex}', + values: { destinationIndex }, + }) + ); + } + + if (status.destDataViewDeleted?.success) { + toastNotificationService.displaySuccessToast( + i18n.translate( + 'xpack.ml.dataframe.analyticsList.deleteAnalyticsWithDataViewSuccessMessage', + { + defaultMessage: 'Request to delete data view {destinationIndex} acknowledged.', + values: { destinationIndex }, + } + ) + ); + } + if (status.destDataViewDeleted?.error) { + const error = extractErrorMessage(status.destDataViewDeleted.error); + toastNotificationService.displayDangerToast( + i18n.translate( + 'xpack.ml.dataframe.analyticsList.deleteAnalyticsWithDataViewErrorMessage', + { + defaultMessage: 'An error occurred deleting data view {destinationIndex}: {error}', + values: { destinationIndex, error }, + } + ) + ); + } + } catch (e) { toastNotificationService.displayErrorToast( - status.destIndexDeleted.error, - i18n.translate('xpack.ml.dataframe.analyticsList.deleteAnalyticsWithIndexErrorMessage', { - defaultMessage: 'An error occurred deleting destination index {destinationIndex}', - values: { destinationIndex }, + e, + i18n.translate('xpack.ml.dataframe.analyticsList.deleteAnalyticsErrorMessage', { + defaultMessage: 'An error occurred deleting the data frame analytics job {analyticsId}', + values: { analyticsId: analyticsConfig.id }, }) ); } + refreshAnalyticsList$.next(REFRESH_ANALYTICS_LIST_STATE.REFRESH); + }; +}; - if (status.destDataViewDeleted?.success) { - toastNotificationService.displaySuccessToast( - i18n.translate( - 'xpack.ml.dataframe.analyticsList.deleteAnalyticsWithDataViewSuccessMessage', +export const useCanDeleteIndex = () => { + const toastNotificationService = useToastNotificationService(); + const ml = useMlApiContext(); + + return async (indexName: string) => { + try { + const privilege = await ml.hasPrivileges({ + index: [ { - defaultMessage: 'Request to delete data view {destinationIndex} acknowledged.', - values: { destinationIndex }, - } - ) + names: [indexName], // uses wildcard + privileges: ['delete_index'], + }, + ], + }); + if (!privilege) { + return false; + } + + return ( + privilege.hasPrivileges === undefined || privilege.hasPrivileges.has_all_requested === true ); - } - if (status.destDataViewDeleted?.error) { - const error = extractErrorMessage(status.destDataViewDeleted.error); + } catch (e) { + const error = extractErrorMessage(e); toastNotificationService.displayDangerToast( - i18n.translate('xpack.ml.dataframe.analyticsList.deleteAnalyticsWithDataViewErrorMessage', { - defaultMessage: 'An error occurred deleting data view {destinationIndex}: {error}', - values: { destinationIndex, error }, + i18n.translate('xpack.ml.dataframe.analyticsList.deleteAnalyticsPrivilegeErrorMessage', { + defaultMessage: 'User does not have permission to delete index {indexName}: {error}', + values: { indexName, error }, }) ); } - } catch (e) { - toastNotificationService.displayErrorToast( - e, - i18n.translate('xpack.ml.dataframe.analyticsList.deleteAnalyticsErrorMessage', { - defaultMessage: 'An error occurred deleting the data frame analytics job {analyticsId}', - values: { analyticsId: analyticsConfig.id }, - }) - ); - } - refreshAnalyticsList$.next(REFRESH_ANALYTICS_LIST_STATE.REFRESH); -}; - -export const canDeleteIndex = async ( - indexName: string, - toastNotificationService: ToastNotificationService -) => { - try { - const privilege = await ml.hasPrivileges({ - index: [ - { - names: [indexName], // uses wildcard - privileges: ['delete_index'], - }, - ], - }); - if (!privilege) { - return false; - } - - return ( - privilege.hasPrivileges === undefined || privilege.hasPrivileges.has_all_requested === true - ); - } catch (e) { - const error = extractErrorMessage(e); - toastNotificationService.displayDangerToast( - i18n.translate('xpack.ml.dataframe.analyticsList.deleteAnalyticsPrivilegeErrorMessage', { - defaultMessage: 'User does not have permission to delete index {indexName}: {error}', - values: { indexName, error }, - }) - ); - } + }; }; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/get_analytics.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/get_analytics.ts index 6f40a026e1355..9a7cccc77aa3f 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/get_analytics.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/get_analytics.ts @@ -11,7 +11,7 @@ import { type DataFrameAnalysisConfigType, DATA_FRAME_TASK_STATE, } from '@kbn/ml-data-frame-analytics-utils'; -import { ml } from '../../../../../services/ml_api_service'; +import { useMlApiContext } from '../../../../../contexts/kibana'; import type { GetDataFrameAnalyticsStatsResponseError, GetDataFrameAnalyticsStatsResponseOk, @@ -106,7 +106,7 @@ export function getAnalyticsJobsStats( return resultStats; } -export const getAnalyticsFactory = ( +export const useGetAnalytics = ( setAnalytics: React.Dispatch>, setAnalyticsStats: (update: AnalyticStatsBarStats | undefined) => void, setErrorMessage: React.Dispatch< @@ -116,6 +116,8 @@ export const getAnalyticsFactory = ( setJobsAwaitingNodeCount: React.Dispatch>, blockRefresh: boolean ): GetAnalytics => { + const ml = useMlApiContext(); + let concurrentLoads = 0; const getAnalytics = async (forceRefresh = false) => { diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/index.ts index 028378c294840..47b0ab3d576c4 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/index.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/index.ts @@ -5,7 +5,11 @@ * 2.0. */ -export { getAnalyticsFactory } from './get_analytics'; -export { deleteAnalytics, deleteAnalyticsAndDestIndex, canDeleteIndex } from './delete_analytics'; -export { startAnalytics } from './start_analytics'; -export { stopAnalytics } from './stop_analytics'; +export { useGetAnalytics } from './get_analytics'; +export { + useDeleteAnalytics, + useDeleteAnalyticsAndDestIndex, + useCanDeleteIndex, +} from './delete_analytics'; +export { useStartAnalytics } from './start_analytics'; +export { useStopAnalytics } from './stop_analytics'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/start_analytics.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/start_analytics.ts index 3d7f46a00c0a2..a5b0dfd32ffa5 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/start_analytics.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/start_analytics.ts @@ -6,32 +6,35 @@ */ import { i18n } from '@kbn/i18n'; -import { ml } from '../../../../../services/ml_api_service'; -import type { ToastNotificationService } from '../../../../../services/toast_notification_service'; + +import { useMlApiContext } from '../../../../../contexts/kibana'; +import { useToastNotificationService } from '../../../../../services/toast_notification_service'; import { refreshAnalyticsList$, REFRESH_ANALYTICS_LIST_STATE } from '../../../../common'; import type { DataFrameAnalyticsListRow } from '../../components/analytics_list/common'; -export const startAnalytics = async ( - d: DataFrameAnalyticsListRow, - toastNotificationService: ToastNotificationService -) => { - try { - await ml.dataFrameAnalytics.startDataFrameAnalytics(d.config.id); - toastNotificationService.displaySuccessToast( - i18n.translate('xpack.ml.dataframe.analyticsList.startAnalyticsSuccessMessage', { - defaultMessage: 'Request to start data frame analytics {analyticsId} acknowledged.', - values: { analyticsId: d.config.id }, - }) - ); - } catch (e) { - toastNotificationService.displayErrorToast( - e, - i18n.translate('xpack.ml.dataframe.analyticsList.startAnalyticsErrorTitle', { - defaultMessage: 'Error starting job', - }) - ); - } - refreshAnalyticsList$.next(REFRESH_ANALYTICS_LIST_STATE.REFRESH); +export const useStartAnalytics = () => { + const toastNotificationService = useToastNotificationService(); + const ml = useMlApiContext(); + + return async (d: DataFrameAnalyticsListRow) => { + try { + await ml.dataFrameAnalytics.startDataFrameAnalytics(d.config.id); + toastNotificationService.displaySuccessToast( + i18n.translate('xpack.ml.dataframe.analyticsList.startAnalyticsSuccessMessage', { + defaultMessage: 'Request to start data frame analytics {analyticsId} acknowledged.', + values: { analyticsId: d.config.id }, + }) + ); + } catch (e) { + toastNotificationService.displayErrorToast( + e, + i18n.translate('xpack.ml.dataframe.analyticsList.startAnalyticsErrorTitle', { + defaultMessage: 'Error starting job', + }) + ); + } + refreshAnalyticsList$.next(REFRESH_ANALYTICS_LIST_STATE.REFRESH); + }; }; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/stop_analytics.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/stop_analytics.ts index 4058f0dfb7568..af0b746a17332 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/stop_analytics.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/stop_analytics.ts @@ -6,35 +6,41 @@ */ import { i18n } from '@kbn/i18n'; -import { getToastNotifications } from '../../../../../util/dependency_cache'; -import { ml } from '../../../../../services/ml_api_service'; + +import { useMlApiContext } from '../../../../../contexts/kibana'; +import { useToastNotificationService } from '../../../../../services/toast_notification_service'; import { refreshAnalyticsList$, REFRESH_ANALYTICS_LIST_STATE } from '../../../../common'; import type { DataFrameAnalyticsListRow } from '../../components/analytics_list/common'; import { isDataFrameAnalyticsFailed } from '../../components/analytics_list/common'; -export const stopAnalytics = async (d: DataFrameAnalyticsListRow) => { - const toastNotifications = getToastNotifications(); - try { - await ml.dataFrameAnalytics.stopDataFrameAnalytics( - d.config.id, - isDataFrameAnalyticsFailed(d.stats.state) - ); - toastNotifications.addSuccess( - i18n.translate('xpack.ml.dataframe.analyticsList.stopAnalyticsSuccessMessage', { - defaultMessage: 'Request to stop data frame analytics {analyticsId} acknowledged.', - values: { analyticsId: d.config.id }, - }) - ); - } catch (e) { - toastNotifications.addDanger( - i18n.translate('xpack.ml.dataframe.analyticsList.stopAnalyticsErrorMessage', { - defaultMessage: - 'An error occurred stopping the data frame analytics {analyticsId}: {error}', - values: { analyticsId: d.config.id, error: JSON.stringify(e) }, - }) - ); - } - refreshAnalyticsList$.next(REFRESH_ANALYTICS_LIST_STATE.REFRESH); +export const useStopAnalytics = () => { + const toastNotificationService = useToastNotificationService(); + const ml = useMlApiContext(); + + return async (d: DataFrameAnalyticsListRow) => { + try { + await ml.dataFrameAnalytics.stopDataFrameAnalytics( + d.config.id, + isDataFrameAnalyticsFailed(d.stats.state) + ); + toastNotificationService.displaySuccessToast( + i18n.translate('xpack.ml.dataframe.analyticsList.stopAnalyticsSuccessMessage', { + defaultMessage: 'Request to stop data frame analytics {analyticsId} acknowledged.', + values: { analyticsId: d.config.id }, + }) + ); + } catch (e) { + toastNotificationService.displayErrorToast( + e, + i18n.translate('xpack.ml.dataframe.analyticsList.stopAnalyticsErrorMessage', { + defaultMessage: + 'An error occurred stopping the data frame analytics {analyticsId}: {error}', + values: { analyticsId: d.config.id, error: JSON.stringify(e) }, + }) + ); + } + refreshAnalyticsList$.next(REFRESH_ANALYTICS_LIST_STATE.REFRESH); + }; }; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/use_fetch_analytics_map_data.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/use_fetch_analytics_map_data.ts index 701900bd32285..d56cd4b7c638d 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/use_fetch_analytics_map_data.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/use_fetch_analytics_map_data.ts @@ -14,8 +14,7 @@ import { JOB_MAP_NODE_TYPES, type AnalyticsMapReturnType, } from '@kbn/ml-data-frame-analytics-utils'; -import { ml } from '../../../services/ml_api_service'; - +import { useMlApiContext } from '../../../contexts/kibana'; interface GetDataObjectParameter { analyticsId?: string; id?: string; @@ -24,6 +23,7 @@ interface GetDataObjectParameter { } export const useFetchAnalyticsMapData = () => { + const ml = useMlApiContext(); const [isLoading, setIsLoading] = useState(false); const [elements, setElements] = useState([]); const [error, setError] = useState(); diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/file_datavisualizer.tsx b/x-pack/plugins/ml/public/application/datavisualizer/file_based/file_datavisualizer.tsx index 168ae4a44656d..bc4d204fda223 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/file_based/file_datavisualizer.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/file_datavisualizer.tsx @@ -17,7 +17,7 @@ import type { import { useTimefilter } from '@kbn/ml-date-picker'; import type { ResultLinks } from '@kbn/data-visualizer-plugin/common/app'; import { HelpMenu } from '../../components/help_menu'; -import { useMlKibana, useMlLocator } from '../../contexts/kibana'; +import { useMlApiContext, useMlKibana, useMlLocator } from '../../contexts/kibana'; import { ML_PAGES } from '../../../../common/constants/locator'; import { isFullLicense } from '../../license'; @@ -36,8 +36,9 @@ export const FileDataVisualizerPage: FC = () => { }, }, } = useMlKibana(); + const mlApiServices = useMlApiContext(); const mlLocator = useMlLocator()!; - getMlNodeCount(); + getMlNodeCount(mlApiServices); const [FileDataVisualizer, setFileDataVisualizer] = useState(null); const [resultLinks, setResultLinks] = useState(null); @@ -104,7 +105,7 @@ export const FileDataVisualizerPage: FC = () => { useEffect(() => { // ML uses this function if (dataVisualizer !== undefined) { - getMlNodeCount(); + getMlNodeCount(mlApiServices); const { getFileDataVisualizerComponent } = dataVisualizer; getFileDataVisualizerComponent().then((resp) => { const items = resp(); diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/data_loader/data_loader.ts b/x-pack/plugins/ml/public/application/datavisualizer/index_based/data_loader/data_loader.ts index da50b4538fa89..3ca5acb5a41bc 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/data_loader/data_loader.ts +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/data_loader/data_loader.ts @@ -5,8 +5,6 @@ * 2.0. */ -import type { CoreSetup } from '@kbn/core/public'; - import type { DataView } from '@kbn/data-views-plugin/public'; import { DEFAULT_SAMPLER_SHARD_SIZE } from '@kbn/ml-agg-utils'; import { OMIT_FIELDS } from '@kbn/ml-anomaly-utils'; @@ -14,23 +12,21 @@ import { type RuntimeMappings } from '@kbn/ml-runtime-field-utils'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import type { IndexPatternTitle } from '../../../../../common/types/kibana'; +import type { MlApiServices } from '../../../services/ml_api_service'; -import { ml } from '../../../services/ml_api_service'; import type { FieldHistogramRequestConfig } from '../common/request'; // Maximum number of examples to obtain for text type fields. const MAX_EXAMPLES_DEFAULT: number = 10; export class DataLoader { - private _indexPattern: DataView; private _runtimeMappings: RuntimeMappings; private _indexPatternTitle: IndexPatternTitle = ''; private _maxExamples: number = MAX_EXAMPLES_DEFAULT; - constructor(indexPattern: DataView, toastNotifications?: CoreSetup['notifications']['toasts']) { - this._indexPattern = indexPattern; + constructor(private _indexPattern: DataView, private _mlApiServices: MlApiServices) { this._runtimeMappings = this._indexPattern.getComputedFields().runtimeFields as RuntimeMappings; - this._indexPatternTitle = indexPattern.title; + this._indexPatternTitle = _indexPattern.title; } async loadFieldHistograms( @@ -39,7 +35,7 @@ export class DataLoader { samplerShardSize = DEFAULT_SAMPLER_SHARD_SIZE, editorRuntimeMappings?: RuntimeMappings ): Promise { - const stats = await ml.getVisualizerFieldHistograms({ + const stats = await this._mlApiServices.getVisualizerFieldHistograms({ indexPattern: this._indexPatternTitle, query, fields, diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/index_data_visualizer.tsx b/x-pack/plugins/ml/public/application/datavisualizer/index_based/index_data_visualizer.tsx index 157aa89522d75..ac980bf21c32c 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/index_data_visualizer.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/index_data_visualizer.tsx @@ -18,7 +18,7 @@ import type { import { useTimefilter } from '@kbn/ml-date-picker'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import useMountedState from 'react-use/lib/useMountedState'; -import { useMlKibana, useMlLocator } from '../../contexts/kibana'; +import { useMlApiContext, useMlKibana, useMlLocator } from '../../contexts/kibana'; import { HelpMenu } from '../../components/help_menu'; import { ML_PAGES } from '../../../../common/constants/locator'; import { isFullLicense } from '../../license'; @@ -40,10 +40,11 @@ export const IndexDataVisualizerPage: FC<{ esql: boolean }> = ({ esql = false }) }, }, } = useMlKibana(); + const mlApiServices = useMlApiContext(); const { showNodeInfo } = useEnabledFeatures(); const mlLocator = useMlLocator()!; const mlFeaturesDisabled = !isFullLicense(); - getMlNodeCount(); + getMlNodeCount(mlApiServices); const [IndexDataVisualizer, setIndexDataVisualizer] = useState( null diff --git a/x-pack/plugins/ml/public/application/explorer/actions/job_selection.ts b/x-pack/plugins/ml/public/application/explorer/actions/job_selection.ts index 60149ef7d4017..2523db6fa8165 100644 --- a/x-pack/plugins/ml/public/application/explorer/actions/job_selection.ts +++ b/x-pack/plugins/ml/public/application/explorer/actions/job_selection.ts @@ -9,12 +9,13 @@ import { from } from 'rxjs'; import { map } from 'rxjs'; import type { MlFieldFormatService } from '../../services/field_format_service'; -import { mlJobService } from '../../services/job_service'; +import type { MlJobService } from '../../services/job_service'; import { EXPLORER_ACTION } from '../explorer_constants'; import { createJobs } from '../explorer_utils'; export function jobSelectionActionCreator( + mlJobService: MlJobService, mlFieldFormatService: MlFieldFormatService, selectedJobIds: string[] ) { diff --git a/x-pack/plugins/ml/public/application/explorer/actions/load_explorer_data.ts b/x-pack/plugins/ml/public/application/explorer/actions/load_explorer_data.ts index faa2b009c131d..80f32a3df82fc 100644 --- a/x-pack/plugins/ml/public/application/explorer/actions/load_explorer_data.ts +++ b/x-pack/plugins/ml/public/application/explorer/actions/load_explorer_data.ts @@ -18,6 +18,7 @@ import type { TimefilterContract } from '@kbn/data-plugin/public'; import { useTimefilter } from '@kbn/ml-date-picker'; import type { InfluencersFilterQuery } from '@kbn/ml-anomaly-utils'; import type { TimeBucketsInterval, TimeRangeBounds } from '@kbn/ml-time-buckets'; +import type { IUiSettingsClient } from '@kbn/core/public'; import type { AppStateSelectedCells, ExplorerJob } from '../explorer_utils'; import { getDateFormatTz, @@ -31,11 +32,13 @@ import { loadOverallAnnotations, } from '../explorer_utils'; import type { ExplorerState } from '../reducers'; -import { useMlKibana } from '../../contexts/kibana'; +import { useMlApiContext, useUiSettings } from '../../contexts/kibana'; import type { MlResultsService } from '../../services/results_service'; import { mlResultsServiceProvider } from '../../services/results_service'; import type { AnomalyExplorerChartsService } from '../../services/anomaly_explorer_charts_service'; import { useAnomalyExplorerContext } from '../anomaly_explorer_context'; +import type { MlApiServices } from '../../services/ml_api_service'; +import { useMlJobService, type MlJobService } from '../../services/job_service'; // Memoize the data fetching methods. // wrapWithLastRefreshArg() wraps any given function and preprends a `lastRefresh` argument @@ -93,6 +96,9 @@ export const isLoadExplorerDataConfig = (arg: any): arg is LoadExplorerDataConfi * Fetches the data necessary for the Anomaly Explorer using observables. */ const loadExplorerDataProvider = ( + uiSettings: IUiSettingsClient, + mlApiServices: MlApiServices, + mlJobService: MlJobService, mlResultsService: MlResultsService, anomalyExplorerChartsService: AnomalyExplorerChartsService, timefilter: TimefilterContract @@ -120,14 +126,20 @@ const loadExplorerDataProvider = ( const timerange = getSelectionTimeRange(selectedCells, bounds); - const dateFormatTz = getDateFormatTz(); + const dateFormatTz = getDateFormatTz(uiSettings); // First get the data where we have all necessary args at hand using forkJoin: // annotationsData, anomalyChartRecords, influencers, overallState, tableData return forkJoin({ - overallAnnotations: memoizedLoadOverallAnnotations(lastRefresh, selectedJobs, bounds), + overallAnnotations: memoizedLoadOverallAnnotations( + lastRefresh, + mlApiServices, + selectedJobs, + bounds + ), annotationsData: memoizedLoadAnnotationsTableData( lastRefresh, + mlApiServices, selectedCells, selectedJobs, bounds @@ -155,6 +167,8 @@ const loadExplorerDataProvider = ( : Promise.resolve({}), tableData: memoizedLoadAnomaliesTableData( lastRefresh, + mlApiServices, + mlJobService, selectedCells, selectedJobs, dateFormatTz, @@ -202,20 +216,23 @@ const loadExplorerDataProvider = ( }; export const useExplorerData = (): [Partial | undefined, (d: any) => void] => { + const uiSettings = useUiSettings(); const timefilter = useTimefilter(); - - const { - services: { - mlServices: { mlApiServices }, - }, - } = useMlKibana(); - + const mlApiServices = useMlApiContext(); + const mlJobService = useMlJobService(); const { anomalyExplorerChartsService } = useAnomalyExplorerContext(); const loadExplorerData = useMemo(() => { const mlResultsService = mlResultsServiceProvider(mlApiServices); - return loadExplorerDataProvider(mlResultsService, anomalyExplorerChartsService, timefilter); + return loadExplorerDataProvider( + uiSettings, + mlApiServices, + mlJobService, + mlResultsService, + anomalyExplorerChartsService, + timefilter + ); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); diff --git a/x-pack/plugins/ml/public/application/explorer/anomaly_explorer_context.tsx b/x-pack/plugins/ml/public/application/explorer/anomaly_explorer_context.tsx index dec8d5df57ceb..35b552a95cac8 100644 --- a/x-pack/plugins/ml/public/application/explorer/anomaly_explorer_context.tsx +++ b/x-pack/plugins/ml/public/application/explorer/anomaly_explorer_context.tsx @@ -19,6 +19,7 @@ import { AnomalyExplorerChartsService } from '../services/anomaly_explorer_chart import { useTableSeverity } from '../components/controls/select_severity'; import { AnomalyDetectionAlertsStateService } from './alerts'; import { explorerServiceFactory, type ExplorerService } from './explorer_dashboard_service'; +import { useMlJobService } from '../services/job_service'; export interface AnomalyExplorerContextValue { anomalyExplorerChartsService: AnomalyExplorerChartsService; @@ -65,6 +66,7 @@ export const AnomalyExplorerContextProvider: FC> = ({ data, }, } = useMlKibana(); + const mlJobService = useMlJobService(); const [, , tableSeverityState] = useTableSeverity(); @@ -80,7 +82,7 @@ export const AnomalyExplorerContextProvider: FC> = ({ // updates so using `useEffect` is the right thing to do here to not get errors // related to React lifecycle methods. useEffect(() => { - const explorerService = explorerServiceFactory(mlFieldFormatService); + const explorerService = explorerServiceFactory(mlJobService, mlFieldFormatService); const anomalyTimelineService = new AnomalyTimelineService( timefilter, @@ -93,6 +95,7 @@ export const AnomalyExplorerContextProvider: FC> = ({ ); const anomalyTimelineStateService = new AnomalyTimelineStateService( + mlJobService, anomalyExplorerUrlStateService, anomalyExplorerCommonStateService, anomalyTimelineService, diff --git a/x-pack/plugins/ml/public/application/explorer/anomaly_timeline_state_service.ts b/x-pack/plugins/ml/public/application/explorer/anomaly_timeline_state_service.ts index 799e4d4ab9b05..371284d0ac047 100644 --- a/x-pack/plugins/ml/public/application/explorer/anomaly_timeline_state_service.ts +++ b/x-pack/plugins/ml/public/application/explorer/anomaly_timeline_state_service.ts @@ -38,8 +38,7 @@ import { SWIMLANE_TYPE, VIEW_BY_JOB_LABEL, } from './explorer_constants'; -// FIXME get rid of the static import -import { mlJobService } from '../services/job_service'; +import type { MlJobService } from '../services/job_service'; import { getSelectionInfluencers, getSelectionTimeRange } from './explorer_utils'; import type { Refresh } from '../routing/use_refresh'; import { StateService } from '../services/state_service'; @@ -107,6 +106,7 @@ export class AnomalyTimelineStateService extends StateService { ); constructor( + private mlJobService: MlJobService, private anomalyExplorerUrlStateService: AnomalyExplorerUrlStateService, private anomalyExplorerCommonStateService: AnomalyExplorerCommonStateService, private anomalyTimelineService: AnomalyTimelineService, @@ -482,6 +482,7 @@ export class AnomalyTimelineStateService extends StateService { selectedCells: AppStateSelectedCells | undefined | null, selectedJobs: ExplorerJob[] | undefined ) { + const mlJobService = this.mlJobService; const selectedJobIds = selectedJobs?.map((d) => d.id) ?? []; // Unique influencers for the selected job(s). diff --git a/x-pack/plugins/ml/public/application/explorer/explorer.tsx b/x-pack/plugins/ml/public/application/explorer/explorer.tsx index 65b862bc548e6..2b6971077a086 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer.tsx +++ b/x-pack/plugins/ml/public/application/explorer/explorer.tsx @@ -380,6 +380,7 @@ export const Explorer: FC = ({ services: { charts: chartsService, data: { dataViews: dataViewsService }, + uiSettings, }, } = useMlKibana(); const { euiTheme } = useEuiTheme(); @@ -442,7 +443,7 @@ export const Explorer: FC = ({ ); const jobSelectorProps = { - dateFormatTz: getDateFormatTz(), + dateFormatTz: getDateFormatTz(uiSettings), } as JobSelectorProps; const noJobsSelected = !selectedJobs || selectedJobs.length === 0; diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/__snapshots__/explorer_chart_config_builder.test.js.snap b/x-pack/plugins/ml/public/application/explorer/explorer_charts/__snapshots__/explorer_chart_config_builder.test.js.snap deleted file mode 100644 index e2e1140f5c5b4..0000000000000 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/__snapshots__/explorer_chart_config_builder.test.js.snap +++ /dev/null @@ -1,53 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`buildConfig get dataConfig for anomaly record 1`] = ` -Object { - "bucketSpanSeconds": 900, - "datafeedConfig": Object { - "chunking_config": Object { - "mode": "auto", - }, - "datafeed_id": "datafeed-mock-job-id", - "indices": Array [ - "farequote-2017", - ], - "job_id": "mock-job-id", - "query": Object { - "match_all": Object { - "boost": 1, - }, - }, - "query_delay": "86658ms", - "scroll_size": 1000, - "state": "stopped", - }, - "detectorIndex": 0, - "detectorLabel": "mean(responsetime)", - "entityFields": Array [ - Object { - "fieldName": "airline", - "fieldType": "partition", - "fieldValue": "JAL", - }, - ], - "fieldName": "responsetime", - "functionDescription": "mean", - "infoTooltip": Object { - "aggregationInterval": "15m", - "chartFunction": "avg responsetime", - "entityFields": Array [ - Object { - "fieldName": "airline", - "fieldValue": "JAL", - }, - ], - "jobId": "mock-job-id", - }, - "interval": "15m", - "jobId": "mock-job-id", - "metricFieldName": "responsetime", - "metricFunction": "avg", - "summaryCountFieldName": undefined, - "timeField": "@timestamp", -} -`; diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_config_builder.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_config_builder.js deleted file mode 100644 index 5fdfd95040937..0000000000000 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_config_builder.js +++ /dev/null @@ -1,74 +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. - */ - -/* - * Builds the configuration object used to plot a chart showing where the anomalies occur in - * the raw data in the Explorer dashboard. - */ - -import { parseInterval } from '../../../../common/util/parse_interval'; -import { getEntityFieldList, ML_JOB_AGGREGATION } from '@kbn/ml-anomaly-utils'; -import { buildConfigFromDetector } from '../../util/chart_config_builder'; -import { mlJobService } from '../../services/job_service'; -import { mlFunctionToESAggregation } from '../../../../common/util/job_utils'; - -// Builds the chart configuration for the provided anomaly record, returning -// an object with properties used for the display (series function and field, aggregation interval etc), -// and properties for the datafeed used for the job (indices, time field etc). -export function buildConfig(record) { - const job = mlJobService.getJob(record.job_id); - const detectorIndex = record.detector_index; - const config = buildConfigFromDetector(job, detectorIndex); - - // Add extra properties used by the explorer dashboard charts. - config.functionDescription = record.function_description; - config.bucketSpanSeconds = parseInterval(job.analysis_config.bucket_span).asSeconds(); - - config.detectorLabel = record.function; - if ( - mlJobService.detectorsByJob[record.job_id] !== undefined && - detectorIndex < mlJobService.detectorsByJob[record.job_id].length - ) { - config.detectorLabel = - mlJobService.detectorsByJob[record.job_id][detectorIndex].detector_description; - } else { - if (record.field_name !== undefined) { - config.detectorLabel += ` ${config.fieldName}`; - } - } - - if (record.field_name !== undefined) { - config.fieldName = record.field_name; - config.metricFieldName = record.field_name; - } - - // Add the 'entity_fields' i.e. the partition, by, over fields which - // define the metric series to be plotted. - config.entityFields = getEntityFieldList(record); - - if (record.function === ML_JOB_AGGREGATION.METRIC) { - config.metricFunction = mlFunctionToESAggregation(record.function_description); - } - - // Build the tooltip data for the chart info icon, showing further details on what is being plotted. - let functionLabel = config.metricFunction; - if (config.metricFieldName !== undefined) { - functionLabel += ` ${config.metricFieldName}`; - } - - config.infoTooltip = { - jobId: record.job_id, - aggregationInterval: config.interval, - chartFunction: functionLabel, - entityFields: config.entityFields.map((f) => ({ - fieldName: f.fieldName, - fieldValue: f.fieldValue, - })), - }; - - return config; -} diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_config_builder.test.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_config_builder.test.js deleted file mode 100644 index 9fe35f137d66f..0000000000000 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_config_builder.test.js +++ /dev/null @@ -1,28 +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 mockAnomalyRecord from './__mocks__/mock_anomaly_record.json'; -import mockDetectorsByJob from './__mocks__/mock_detectors_by_job.json'; -import mockJobConfig from './__mocks__/mock_job_config.json'; - -jest.mock('../../services/job_service', () => ({ - mlJobService: { - getJob() { - return mockJobConfig; - }, - detectorsByJob: mockDetectorsByJob, - }, -})); - -import { buildConfig } from './explorer_chart_config_builder'; - -describe('buildConfig', () => { - test('get dataConfig for anomaly record', () => { - const dataConfig = buildConfig(mockAnomalyRecord); - expect(dataConfig).toMatchSnapshot(); - }); -}); diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js index 702aeed891ebc..c2ce5450bd7ba 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js @@ -95,7 +95,6 @@ function ExplorerChartContainer({ timefilter, timeRange, onSelectEntity, - recentlyAccessed, tooManyBucketsCalloutMsg, showSelectedInterval, chartsService, @@ -105,6 +104,7 @@ function ExplorerChartContainer({ const { services: { + chrome: { recentlyAccessed }, share, application: { navigateToApp }, }, @@ -389,11 +389,7 @@ export const ExplorerChartsContainerUI = ({ chartsService, }) => { const { - services: { - chrome: { recentlyAccessed }, - embeddable: embeddablePlugin, - maps: mapsPlugin, - }, + services: { embeddable: embeddablePlugin, maps: mapsPlugin }, } = kibana; let seriesToPlotFiltered; @@ -452,7 +448,6 @@ export const ExplorerChartsContainerUI = ({ timefilter={timefilter} timeRange={timeRange} onSelectEntity={onSelectEntity} - recentlyAccessed={recentlyAccessed} tooManyBucketsCalloutMsg={tooManyBucketsCalloutMsg} showSelectedInterval={showSelectedInterval} chartsService={chartsService} diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.test.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.test.js index 38f8eca99ae68..dbbb00ac85fb7 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.test.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.test.js @@ -21,16 +21,11 @@ import { kibanaContextMock } from '../../contexts/kibana/__mocks__/kibana_contex import { timeBucketsMock } from '../../util/__mocks__/time_buckets'; import { timefilterMock } from '../../contexts/kibana/__mocks__/use_timefilter'; -jest.mock('../../services/job_service', () => ({ - mlJobService: { - getJob: jest.fn(), - }, -})); - jest.mock('../../contexts/kibana', () => ({ useMlKibana: () => { return { services: { + chrome: { recentlyAccessed: { add: jest.fn() } }, share: { url: { locators: { diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_dashboard_service.ts b/x-pack/plugins/ml/public/application/explorer/explorer_dashboard_service.ts index 1bcef70b54c25..1cf723f5145d6 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_dashboard_service.ts +++ b/x-pack/plugins/ml/public/application/explorer/explorer_dashboard_service.ts @@ -20,6 +20,7 @@ import { EXPLORER_ACTION } from './explorer_constants'; import type { ExplorerState } from './reducers'; import { explorerReducer, getExplorerDefaultState } from './reducers'; import type { MlFieldFormatService } from '../services/field_format_service'; +import type { MlJobService } from '../services/job_service'; type ExplorerAction = Action | Observable; export const explorerAction$ = new Subject(); @@ -52,7 +53,10 @@ const setExplorerDataActionCreator = (payload: DeepPartial) => ({ }); // Export observable state and action dispatchers as service -export const explorerServiceFactory = (mlFieldFormatService: MlFieldFormatService) => ({ +export const explorerServiceFactory = ( + mlJobService: MlJobService, + mlFieldFormatService: MlFieldFormatService +) => ({ state$: explorerState$, clearExplorerData: () => { explorerAction$.next({ type: EXPLORER_ACTION.CLEAR_EXPLORER_DATA }); @@ -64,7 +68,9 @@ export const explorerServiceFactory = (mlFieldFormatService: MlFieldFormatServic explorerAction$.next({ type: EXPLORER_ACTION.CLEAR_JOBS }); }, updateJobSelection: (selectedJobIds: string[]) => { - explorerAction$.next(jobSelectionActionCreator(mlFieldFormatService, selectedJobIds)); + explorerAction$.next( + jobSelectionActionCreator(mlJobService, mlFieldFormatService, selectedJobIds) + ); }, setExplorerData: (payload: DeepPartial) => { explorerAction$.next(setExplorerDataActionCreator(payload)); diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_utils.ts b/x-pack/plugins/ml/public/application/explorer/explorer_utils.ts index 4f8ee3556c872..fbfad277ff45f 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_utils.ts +++ b/x-pack/plugins/ml/public/application/explorer/explorer_utils.ts @@ -24,9 +24,10 @@ import { type MlRecordForInfluencer, ML_JOB_AGGREGATION, } from '@kbn/ml-anomaly-utils'; - import type { InfluencersFilterQuery } from '@kbn/ml-anomaly-utils'; import type { TimeRangeBounds } from '@kbn/ml-time-buckets'; +import type { IUiSettingsClient } from '@kbn/core/public'; + import { ANNOTATIONS_TABLE_DEFAULT_QUERY_SIZE, ANOMALIES_TABLE_DEFAULT_QUERY_SIZE, @@ -39,9 +40,7 @@ import { isTimeSeriesViewJob, } from '../../../common/util/job_utils'; import { parseInterval } from '../../../common/util/parse_interval'; -import { ml } from '../services/ml_api_service'; -import { mlJobService } from '../services/job_service'; -import { getUiSettings } from '../util/dependency_cache'; +import type { MlJobService } from '../services/job_service'; import type { SwimlaneType } from './explorer_constants'; import { @@ -53,6 +52,7 @@ import { import type { CombinedJob } from '../../../common/types/anomaly_detection_jobs'; import type { MlResultsService } from '../services/results_service'; import type { Annotations, AnnotationsTable } from '../../../common/types/annotations'; +import type { MlApiServices } from '../services/ml_api_service'; export interface ExplorerJob { id: string; @@ -239,7 +239,7 @@ export async function loadFilteredTopInfluencers( )) as any[]; } -export function getInfluencers(selectedJobs: any[]): string[] { +export function getInfluencers(mlJobService: MlJobService, selectedJobs: any[]): string[] { const influencers: string[] = []; selectedJobs.forEach((selectedJob) => { const job = mlJobService.getJob(selectedJob.id); @@ -250,15 +250,14 @@ export function getInfluencers(selectedJobs: any[]): string[] { return influencers; } -export function getDateFormatTz(): string { - const uiSettings = getUiSettings(); +export function getDateFormatTz(uiSettings: IUiSettingsClient): string { // Pass the timezone to the server for use when aggregating anomalies (by day / hour) for the table. const tzConfig = uiSettings.get('dateFormat:tz'); const dateFormatTz = tzConfig !== 'Browser' ? tzConfig : moment.tz.guess(); return dateFormatTz; } -export function getFieldsByJob() { +export function getFieldsByJob(mlJobService: MlJobService) { return mlJobService.jobs.reduce( (reducedFieldsByJob, job) => { // Add the list of distinct by, over, partition and influencer fields for each job. @@ -353,6 +352,7 @@ export function getSelectionJobIds( } export function loadOverallAnnotations( + mlApiServices: MlApiServices, selectedJobs: ExplorerJob[], bounds: TimeRangeBounds ): Promise { @@ -361,7 +361,7 @@ export function loadOverallAnnotations( return new Promise((resolve) => { lastValueFrom( - ml.annotations.getAnnotations$({ + mlApiServices.annotations.getAnnotations$({ jobIds, earliestMs: timeRange.earliestMs, latestMs: timeRange.latestMs, @@ -407,6 +407,7 @@ export function loadOverallAnnotations( } export function loadAnnotationsTableData( + mlApiServices: MlApiServices, selectedCells: AppStateSelectedCells | undefined | null, selectedJobs: ExplorerJob[], bounds: Required @@ -416,7 +417,7 @@ export function loadAnnotationsTableData( return new Promise((resolve) => { lastValueFrom( - ml.annotations.getAnnotations$({ + mlApiServices.annotations.getAnnotations$({ jobIds, earliestMs: timeRange.earliestMs, latestMs: timeRange.latestMs, @@ -465,6 +466,8 @@ export function loadAnnotationsTableData( } export async function loadAnomaliesTableData( + mlApiServices: MlApiServices, + mlJobService: MlJobService, selectedCells: AppStateSelectedCells | undefined | null, selectedJobs: ExplorerJob[], dateFormatTz: string, @@ -479,7 +482,7 @@ export async function loadAnomaliesTableData( const timeRange = getSelectionTimeRange(selectedCells, bounds); return new Promise((resolve, reject) => { - ml.results + mlApiServices.results .getAnomaliesTableData( jobIds, [], diff --git a/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/get_index_pattern.ts b/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/get_index_pattern.ts index 7bc74698db2cd..ca360a9c4cb69 100644 --- a/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/get_index_pattern.ts +++ b/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/get_index_pattern.ts @@ -7,15 +7,12 @@ import { ML_RESULTS_INDEX_PATTERN } from '../../../../../common/constants/index_patterns'; -import type { ExplorerJob } from '../../explorer_utils'; -import { getInfluencers } from '../../explorer_utils'; - // Creates index pattern in the format expected by the kuery bar/kuery autocomplete provider // Field objects required fields: name, type, aggregatable, searchable -export function getIndexPattern(selectedJobs: ExplorerJob[]) { +export function getIndexPattern(influencers: string[]) { return { title: ML_RESULTS_INDEX_PATTERN, - fields: getInfluencers(selectedJobs).map((influencer) => ({ + fields: influencers.map((influencer) => ({ name: influencer, type: 'string', aggregatable: true, diff --git a/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/job_selection_change.ts b/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/job_selection_change.ts index dbf5dc2c8a8be..da657c3f18bc7 100644 --- a/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/job_selection_change.ts +++ b/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/job_selection_change.ts @@ -6,16 +6,15 @@ */ import type { ActionPayload } from '../../explorer_dashboard_service'; -import { getInfluencers } from '../../explorer_utils'; import { getIndexPattern } from './get_index_pattern'; import type { ExplorerState } from './state'; export const jobSelectionChange = (state: ExplorerState, payload: ActionPayload): ExplorerState => { - const { selectedJobs } = payload; + const { selectedJobs, noInfluencersConfigured } = payload; const stateUpdate: ExplorerState = { ...state, - noInfluencersConfigured: getInfluencers(selectedJobs).length === 0, + noInfluencersConfigured, selectedJobs, }; diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/confirm_modals/close_jobs_confirm_modal.tsx b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/confirm_modals/close_jobs_confirm_modal.tsx index 499ef14ecca36..605ebc0a412ea 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/confirm_modals/close_jobs_confirm_modal.tsx +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/confirm_modals/close_jobs_confirm_modal.tsx @@ -19,8 +19,10 @@ import { EuiButton, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { useMlKibana } from '../../../../contexts/kibana'; import type { MlSummaryJob } from '../../../../../../common/types/anomaly_detection_jobs'; import { isManagedJob } from '../../../jobs_utils'; +import { useMlJobService } from '../../../../services/job_service'; import { closeJobs } from '../utils'; import { ManagedJobsWarningCallout } from './managed_jobs_warning_callout'; @@ -37,6 +39,12 @@ export const CloseJobsConfirmModal: FC = ({ unsetShowFunction, refreshJobs, }) => { + const { + services: { + notifications: { toasts }, + }, + } = useMlKibana(); + const mlJobService = useMlJobService(); const [modalVisible, setModalVisible] = useState(false); const [hasManagedJob, setHasManaged] = useState(true); const [jobsToReset, setJobsToReset] = useState([]); @@ -113,7 +121,7 @@ export const CloseJobsConfirmModal: FC = ({ { - closeJobs(jobsToReset, refreshJobs); + closeJobs(toasts, mlJobService, jobsToReset, refreshJobs); closeModal(); }} fill diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/confirm_modals/stop_datafeeds_confirm_modal.tsx b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/confirm_modals/stop_datafeeds_confirm_modal.tsx index 355e186291508..265e0c58986aa 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/confirm_modals/stop_datafeeds_confirm_modal.tsx +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/confirm_modals/stop_datafeeds_confirm_modal.tsx @@ -19,8 +19,10 @@ import { EuiButton, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { useMlKibana } from '../../../../contexts/kibana'; import type { MlSummaryJob } from '../../../../../../common/types/anomaly_detection_jobs'; import { isManagedJob } from '../../../jobs_utils'; +import { useMlJobService } from '../../../../services/job_service'; import { stopDatafeeds } from '../utils'; import { ManagedJobsWarningCallout } from './managed_jobs_warning_callout'; @@ -38,6 +40,12 @@ export const StopDatafeedsConfirmModal: FC = ({ unsetShowFunction, refreshJobs, }) => { + const { + services: { + notifications: { toasts }, + }, + } = useMlKibana(); + const mlJobService = useMlJobService(); const [modalVisible, setModalVisible] = useState(false); const [hasManagedJob, setHasManaged] = useState(true); const [jobsToStop, setJobsToStop] = useState([]); @@ -114,7 +122,7 @@ export const StopDatafeedsConfirmModal: FC = ({ { - stopDatafeeds(jobsToStop, refreshJobs); + stopDatafeeds(toasts, mlJobService, jobsToStop, refreshJobs); closeModal(); }} fill diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/datafeed_chart_flyout/datafeed_chart_flyout.tsx b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/datafeed_chart_flyout/datafeed_chart_flyout.tsx index 93ad352c6b2d9..510149ada47e6 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/datafeed_chart_flyout/datafeed_chart_flyout.tsx +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/datafeed_chart_flyout/datafeed_chart_flyout.tsx @@ -110,6 +110,7 @@ export const DatafeedChartFlyout: FC = ({ onClose, onModelSnapshotAnnotationClick, }) => { + const mlApiServices = useMlApiContext(); const [data, setData] = useState<{ datafeedConfig: CombinedJobWithStats['datafeed_config'] | undefined; bucketSpan: string | undefined; @@ -212,7 +213,7 @@ export const DatafeedChartFlyout: FC = ({ const getJobAndSnapshotData = useCallback(async () => { try { - const job: CombinedJobWithStats = await loadFullJob(jobId); + const job: CombinedJobWithStats = await loadFullJob(mlApiServices, jobId); const modelSnapshotResultsLine: LineAnnotationDatumWithModelSnapshot[] = []; const modelSnapshotsResp = await getModelSnapshots(jobId); const modelSnapshots = modelSnapshotsResp.model_snapshots ?? []; @@ -659,6 +660,7 @@ export const JobListDatafeedChartFlyout: FC = ( unsetShowFunction, refreshJobs, }) => { + const mlApiServices = useMlApiContext(); const [isVisible, setIsVisible] = useState(false); const [job, setJob] = useState(); const [jobWithStats, setJobWithStats] = useState(); @@ -675,9 +677,11 @@ export const JobListDatafeedChartFlyout: FC = ( const showRevertModelSnapshot = useCallback(async () => { // Need to load the full job with stats, as the model snapshot // flyout needs the timestamp of the last result. - const fullJob: CombinedJobWithStats = await loadFullJob(job!.id); + const fullJob: CombinedJobWithStats = await loadFullJob(mlApiServices, job!.id); setJobWithStats(fullJob); setIsRevertModelSnapshotFlyoutVisible(true); + // exclude mlApiServices from deps + // eslint-disable-next-line react-hooks/exhaustive-deps }, [job]); useEffect(() => { diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/delete_job_modal/delete_job_modal.tsx b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/delete_job_modal/delete_job_modal.tsx index 1626a8d3ec661..75d3fb270d226 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/delete_job_modal/delete_job_modal.tsx +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/delete_job_modal/delete_job_modal.tsx @@ -23,9 +23,11 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { useMlKibana } from '../../../../contexts/kibana'; import { deleteJobs } from '../utils'; import { BLOCKED_JOBS_REFRESH_INTERVAL_MS } from '../../../../../../common/constants/jobs_list'; import { DeleteSpaceAwareItemCheckModal } from '../../../../components/delete_space_aware_item_check_modal'; +import { useMlJobService } from '../../../../services/job_service'; import type { MlSummaryJob } from '../../../../../../common/types/anomaly_detection_jobs'; import { isManagedJob } from '../../../jobs_utils'; import { ManagedJobsWarningCallout } from '../confirm_modals/managed_jobs_warning_callout'; @@ -39,6 +41,12 @@ interface Props { } export const DeleteJobModal: FC = ({ setShowFunction, unsetShowFunction, refreshJobs }) => { + const { + services: { + notifications: { toasts }, + }, + } = useMlKibana(); + const mlJobService = useMlJobService(); const [deleting, setDeleting] = useState(false); const [modalVisible, setModalVisible] = useState(false); const [adJobs, setAdJobs] = useState([]); @@ -83,6 +91,8 @@ export const DeleteJobModal: FC = ({ setShowFunction, unsetShowFunction, const deleteJob = useCallback(() => { setDeleting(true); deleteJobs( + toasts, + mlJobService, jobIds.map((id) => ({ id })), deleteUserAnnotations, deleteAlertingRules @@ -92,6 +102,8 @@ export const DeleteJobModal: FC = ({ setShowFunction, unsetShowFunction, closeModal(); refreshJobs(); }, BLOCKED_JOBS_REFRESH_INTERVAL_MS); + // exclude mlJobservice from deps + // eslint-disable-next-line react-hooks/exhaustive-deps }, [jobIds, deleteUserAnnotations, deleteAlertingRules, closeModal, refreshJobs]); if (modalVisible === false || jobIds.length === 0) { diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/edit_job_flyout.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/edit_job_flyout.js index 8c739877b7037..03d8745b20ee1 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/edit_job_flyout.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/edit_job_flyout.js @@ -10,6 +10,7 @@ import React, { Component } from 'react'; import { cloneDeep, isEqual, pick } from 'lodash'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; +import { withKibana } from '@kbn/kibana-react-plugin/public'; import { EuiButton, EuiButtonEmpty, @@ -30,8 +31,6 @@ import { saveJob } from './edit_utils'; import { loadFullJob } from '../utils'; import { validateModelMemoryLimit, validateGroupNames } from '../validate_job'; import { toastNotificationServiceProvider } from '../../../../services/toast_notification_service'; -import { ml } from '../../../../services/ml_api_service'; -import { withKibana } from '@kbn/kibana-react-plugin/public'; import { XJson } from '@kbn/es-ui-shared-plugin/public'; import { DATAFEED_STATE, JOB_STATE } from '../../../../../../common/constants/states'; import { CustomUrlsWrapper, isValidCustomUrls } from '../../../../components/custom_urls'; @@ -43,8 +42,8 @@ const { collapseLiteralStrings } = XJson; export class EditJobFlyoutUI extends Component { _initialJobFormState = null; - constructor(props) { - super(props); + constructor(props, constructorContext) { + super(props, constructorContext); this.state = { job: {}, @@ -121,7 +120,7 @@ export class EditJobFlyoutUI extends Component { showFlyout = (jobLite) => { const hasDatafeed = jobLite.hasDatafeed; - loadFullJob(jobLite.id) + loadFullJob(this.props.kibana.services.mlServices.mlApiServices, jobLite.id) .then((job) => { this.extractJob(job, hasDatafeed); this.setState({ @@ -204,6 +203,8 @@ export class EditJobFlyoutUI extends Component { ).message; } + const ml = this.props.kibana.services.mlServices.mlApiServices; + if (jobDetails.jobGroups !== undefined) { jobGroupsValidationError = validateGroupNames(jobDetails.jobGroups).message; if (jobGroupsValidationError === '') { @@ -272,10 +273,11 @@ export class EditJobFlyoutUI extends Component { customUrls: this.state.jobCustomUrls, }; + const mlApiServices = this.props.kibana.services.mlServices.mlApiServices; const { toasts } = this.props.kibana.services.notifications; const toastNotificationService = toastNotificationServiceProvider(toasts); - saveJob(this.state.job, newJobData) + saveJob(mlApiServices, this.state.job, newJobData) .then(() => { toasts.addSuccess( i18n.translate('xpack.ml.jobsList.editJobFlyout.changesSavedNotificationMessage', { diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/edit_utils.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/edit_utils.js index 8870f04f498d0..b3c36304ed381 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/edit_utils.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/edit_utils.js @@ -8,9 +8,8 @@ import { difference } from 'lodash'; import { getNewJobLimits } from '../../../../services/ml_server_info'; import { processCreatedBy } from '../../../../../../common/util/job_utils'; -import { ml } from '../../../../services/ml_api_service'; -export function saveJob(job, newJobData, finish) { +export function saveJob(mlApiServices, job, newJobData, finish) { return new Promise((resolve, reject) => { const jobData = { ...extractDescription(job, newJobData), @@ -30,7 +29,7 @@ export function saveJob(job, newJobData, finish) { } const saveDatafeedWrapper = () => { - saveDatafeed(datafeedData, job, finish) + saveDatafeed(mlApiServices, datafeedData, job, finish) .then(() => { resolve(); }) @@ -41,7 +40,8 @@ export function saveJob(job, newJobData, finish) { // if anything has changed, post the changes if (Object.keys(jobData).length) { - ml.updateJob({ jobId: job.job_id, job: jobData }) + mlApiServices + .updateJob({ jobId: job.job_id, job: jobData }) .then(() => { saveDatafeedWrapper(); }) @@ -54,11 +54,12 @@ export function saveJob(job, newJobData, finish) { }); } -function saveDatafeed(datafeedConfig, job) { +function saveDatafeed(mlApiServices, datafeedConfig, job) { return new Promise((resolve, reject) => { if (Object.keys(datafeedConfig).length) { const datafeedId = job.datafeed_config.datafeed_id; - ml.updateDatafeed({ datafeedId, datafeedConfig }) + mlApiServices + .updateDatafeed({ datafeedId, datafeedConfig }) .then(() => { resolve(); }) diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/detectors.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/detectors.js index 14df6ccb8d8f4..01fe676a4133b 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/detectors.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/detectors.js @@ -7,16 +7,26 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; -import { FormattedMessage } from '@kbn/i18n-react'; import { EuiFieldText, EuiForm, EuiFormRow, EuiSpacer, EuiTitle } from '@elastic/eui'; -import { mlJobService } from '../../../../../services/job_service'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { context } from '@kbn/kibana-react-plugin/public'; + +import { mlJobServiceFactory } from '../../../../../services/job_service'; +import { toastNotificationServiceProvider } from '../../../../../services/toast_notification_service'; import { detectorToString } from '../../../../../util/string_utils'; export class Detectors extends Component { - constructor(props) { - super(props); + static contextType = context; + + constructor(props, constructorContext) { + super(props, constructorContext); + + const mlJobService = mlJobServiceFactory( + toastNotificationServiceProvider(constructorContext.services.notifications.toasts), + constructorContext.services.mlServices.mlApiServices + ); this.detectors = mlJobService.getJobGroups().map((g) => ({ label: g.id })); diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/job_details.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/job_details.js index 9bfa9caa0c875..5cb7b9edd607e 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/job_details.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/job_details.js @@ -17,12 +17,13 @@ import { EuiFieldNumber, } from '@elastic/eui'; -import { ml } from '../../../../../services/ml_api_service'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; +import { withKibana } from '@kbn/kibana-react-plugin/public'; + import { tabColor } from '../../../../../../../common/util/group_color_utils'; -export class JobDetails extends Component { +export class JobDetailsUI extends Component { constructor(props) { super(props); @@ -41,6 +42,7 @@ export class JobDetails extends Component { } componentDidMount() { + const ml = this.props.kibana.services.mlServices.mlApiServices; // load groups to populate the select options ml.jobs .groups() @@ -259,10 +261,12 @@ export class JobDetails extends Component { ); } } -JobDetails.propTypes = { +JobDetailsUI.propTypes = { datafeedRunning: PropTypes.bool.isRequired, jobDescription: PropTypes.string.isRequired, jobGroups: PropTypes.array.isRequired, jobModelMemoryLimit: PropTypes.string.isRequired, setJobDetails: PropTypes.func.isRequired, }; + +export const JobDetails = withKibana(JobDetailsUI); diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_actions/management.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_actions/management.js index 29045ad826bdf..1734182ff3ebc 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_actions/management.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_actions/management.js @@ -22,6 +22,10 @@ import { i18n } from '@kbn/i18n'; import { isManagedJob } from '../../../jobs_utils'; export function actionsMenuContent( + toastNotifications, + application, + mlApiServices, + mlJobService, showEditJobFlyout, showDatafeedChartFlyout, showDeleteJobModal, @@ -73,7 +77,7 @@ export function actionsMenuContent( if (isManagedJob(item)) { showStopDatafeedsConfirmModal([item]); } else { - stopDatafeeds([item], refreshJobs); + stopDatafeeds(toastNotifications, mlJobService, [item], refreshJobs); } closeMenu(true); @@ -110,7 +114,7 @@ export function actionsMenuContent( if (isManagedJob(item)) { showCloseJobsConfirmModal([item]); } else { - closeJobs([item], refreshJobs); + closeJobs(toastNotifications, mlJobService, [item], refreshJobs); } closeMenu(true); @@ -149,7 +153,7 @@ export function actionsMenuContent( return isJobBlocked(item) === false && canCreateJob; }, onClick: (item) => { - cloneJob(item.id); + cloneJob(toastNotifications, application, mlApiServices, mlJobService, item.id); closeMenu(true); }, 'data-test-subj': 'mlActionButtonCloneJob', diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/forecasts_table/forecasts_table.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/forecasts_table/forecasts_table.js index db36d97e0a9bd..6ab1aae6be895 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/forecasts_table/forecasts_table.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/forecasts_table/forecasts_table.js @@ -39,13 +39,15 @@ const MAX_FORECASTS = 500; * Table component for rendering the lists of forecasts run on an ML job. */ export class ForecastsTable extends Component { - constructor(props) { - super(props); + constructor(props, constructorContext) { + super(props, constructorContext); this.state = { isLoading: props.job.data_counts.processed_record_count !== 0, forecasts: [], }; - this.mlForecastService; + this.mlForecastService = forecastServiceFactory( + constructorContext.services.mlServices.mlApiServices + ); } /** @@ -54,7 +56,6 @@ export class ForecastsTable extends Component { static contextType = context; componentDidMount() { - this.mlForecastService = forecastServiceFactory(this.context.services.mlServices.mlApiServices); const dataCounts = this.props.job.data_counts; if (dataCounts.processed_record_count > 0) { // Get the list of all the forecasts with results at or later than the specified 'from' time. diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_messages_pane.tsx b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_messages_pane.tsx index 684cccb0afdba..eb0ce2d6be817 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_messages_pane.tsx +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_messages_pane.tsx @@ -11,7 +11,6 @@ import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiToolTip } from '@el import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { extractErrorMessage } from '@kbn/ml-error-utils'; -import { ml } from '../../../../services/ml_api_service'; import { JobMessages } from '../../../../components/job_messages'; import type { JobMessage } from '../../../../../../common/types/audit_message'; import { useToastNotificationService } from '../../../../services/toast_notification_service'; @@ -38,9 +37,7 @@ export const JobMessagesPane: FC = React.memo( const [isClearing, setIsClearing] = useState(false); const toastNotificationService = useToastNotificationService(); - const { - jobs: { clearJobAuditMessages }, - } = useMlApiContext(); + const ml = useMlApiContext(); const fetchMessages = async () => { setIsLoading(true); @@ -70,7 +67,7 @@ export const JobMessagesPane: FC = React.memo( const clearMessages = useCallback(async () => { setIsClearing(true); try { - await clearJobAuditMessages(jobId, notificationIndices); + await ml.jobs.clearJobAuditMessages(jobId, notificationIndices); setIsClearing(false); if (typeof refreshJobList === 'function') { refreshJobList(); diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js index b72bbf41269cc..e6fd50d96a65d 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js @@ -11,6 +11,7 @@ import { sortBy } from 'lodash'; import moment from 'moment'; import { TIME_FORMAT } from '@kbn/ml-date-utils'; +import { withKibana } from '@kbn/kibana-react-plugin/public'; import { toLocaleString } from '../../../../util/string_utils'; import { JobIcon } from '../../../../components/job_message_icon'; @@ -31,10 +32,12 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { AnomalyDetectionJobIdLink } from './job_id_link'; import { isManagedJob } from '../../../jobs_utils'; +import { mlJobServiceFactory } from '../../../../services/job_service'; +import { toastNotificationServiceProvider } from '../../../../services/toast_notification_service'; const PAGE_SIZE_OPTIONS = [10, 25, 50]; -export class JobsList extends Component { +export class JobsListUI extends Component { constructor(props) { super(props); @@ -42,6 +45,12 @@ export class JobsList extends Component { jobsSummaryList: props.jobsSummaryList, itemIdToExpandedRowMap: {}, }; + + this.mlApiServices = props.kibana.services.mlServices.mlApiServices; + this.mlJobService = mlJobServiceFactory( + toastNotificationServiceProvider(props.kibana.services.notifications.toasts), + this.mlApiServices + ); } static getDerivedStateFromProps(props) { @@ -329,6 +338,10 @@ export class JobsList extends Component { defaultMessage: 'Actions', }), actions: actionsMenuContent( + this.props.kibana.services.notifications.toasts, + this.props.kibana.services.application, + this.mlApiServices, + this.mlJobService, this.props.showEditJobFlyout, this.props.showDatafeedChartFlyout, this.props.showDeleteJobModal, @@ -399,7 +412,7 @@ export class JobsList extends Component { ); } } -JobsList.propTypes = { +JobsListUI.propTypes = { jobsSummaryList: PropTypes.array.isRequired, fullJobsList: PropTypes.object.isRequired, isMlEnabledInSpace: PropTypes.bool, @@ -419,7 +432,9 @@ JobsList.propTypes = { jobsViewState: PropTypes.object, onJobsViewStateUpdate: PropTypes.func, }; -JobsList.defaultProps = { +JobsListUI.defaultProps = { isMlEnabledInSpace: true, loading: false, }; + +export const JobsList = withKibana(JobsListUI); diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js index 40df5bd90915b..efb9211e99c0e 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js @@ -8,7 +8,8 @@ import React, { Component } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; -import { ml } from '../../../../services/ml_api_service'; +import { withKibana } from '@kbn/kibana-react-plugin/public'; + import { checkForAutoStartDatafeed, filterJobs, loadFullJob } from '../utils'; import { JobsList } from '../jobs_list'; import { JobDetails } from '../job_details'; @@ -36,10 +37,12 @@ import { StopDatafeedsConfirmModal } from '../confirm_modals/stop_datafeeds_conf import { CloseJobsConfirmModal } from '../confirm_modals/close_jobs_confirm_modal'; import { AnomalyDetectionEmptyState } from '../anomaly_detection_empty_state'; import { removeNodeInfo } from '../../../../../../common/util/job_utils'; +import { mlJobServiceFactory } from '../../../../services/job_service'; +import { toastNotificationServiceProvider } from '../../../../services/toast_notification_service'; let blockingJobsRefreshTimeout = null; -export class JobsListView extends Component { +export class JobsListViewUI extends Component { constructor(props) { super(props); @@ -77,6 +80,11 @@ export class JobsListView extends Component { * @private */ this._isFiltersSet = false; + + this.mlJobService = mlJobServiceFactory( + toastNotificationServiceProvider(props.kibana.services.notifications.toasts), + props.kibana.services.mlServices.mlApiServices + ); } componentDidMount() { @@ -98,7 +106,7 @@ export class JobsListView extends Component { } openAutoStartDatafeedModal() { - const job = checkForAutoStartDatafeed(); + const job = checkForAutoStartDatafeed(this.mlJobService); if (job !== undefined) { this.showStartDatafeedModal([job]); } @@ -139,7 +147,7 @@ export class JobsListView extends Component { } this.setState({ itemIdToExpandedRowMap }, () => { - loadFullJob(jobId) + loadFullJob(this.props.kibana.services.mlServices.mlApiServices, jobId) .then((job) => { const fullJobsList = { ...this.state.fullJobsList }; if (this.props.showNodeInfo === false) { @@ -316,6 +324,7 @@ export class JobsListView extends Component { this.setState({ loading: true }); } + const ml = this.props.kibana.services.mlServices.mlApiServices; const expandedJobsIds = Object.keys(this.state.itemIdToExpandedRowMap); try { let jobsAwaitingNodeCount = 0; @@ -378,6 +387,7 @@ export class JobsListView extends Component { return; } + const ml = this.props.kibana.services.mlServices.mlApiServices; const { jobs } = await ml.jobs.blockingJobTasks(); const blockingJobIds = jobs.map((j) => Object.keys(j)[0]).sort(); const taskListHasChanged = blockingJobIds.join() !== this.state.blockingJobIds.join(); @@ -552,3 +562,5 @@ export class JobsListView extends Component { return
{this.renderJobsListComponents()}
; } } + +export const JobsListView = withKibana(JobsListViewUI); diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/actions_menu.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/actions_menu.js index 73757a0091d5e..e12b770039b8d 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/actions_menu.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/actions_menu.js @@ -5,13 +5,22 @@ * 2.0. */ -import { checkPermission } from '../../../../capabilities/check_capabilities'; -import { mlNodesAvailable } from '../../../../ml_nodes_check/check_ml_nodes'; import PropTypes from 'prop-types'; import React, { Component } from 'react'; import { EuiButtonIcon, EuiContextMenuPanel, EuiContextMenuItem, EuiPopover } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { context } from '@kbn/kibana-react-plugin/public'; + +import { checkPermission } from '../../../../capabilities/check_capabilities'; +import { mlNodesAvailable } from '../../../../ml_nodes_check/check_ml_nodes'; +import { mlJobServiceFactory } from '../../../../services/job_service'; +import { toastNotificationServiceProvider } from '../../../../services/toast_notification_service'; + +import { isManagedJob } from '../../../jobs_utils'; + import { closeJobs, stopDatafeeds, @@ -20,13 +29,12 @@ import { isClosable, isResettable, } from '../utils'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { isManagedJob } from '../../../jobs_utils'; class MultiJobActionsMenuUI extends Component { - constructor(props) { - super(props); + static contextType = context; + + constructor(props, constructorContext) { + super(props, constructorContext); this.state = { isOpen: false, @@ -37,6 +45,13 @@ class MultiJobActionsMenuUI extends Component { this.canCloseJob = checkPermission('canCloseJob') && mlNodesAvailable(); this.canResetJob = checkPermission('canResetJob') && mlNodesAvailable(); this.canCreateMlAlerts = checkPermission('canCreateMlAlerts'); + + this.toastNoticiations = constructorContext.services.notifications.toasts; + const mlApiServices = constructorContext.services.mlServices.mlApiServices; + const toastNotificationService = toastNotificationServiceProvider( + constructorContext.services.notifications.toasts + ); + this.mlJobService = mlJobServiceFactory(toastNotificationService, mlApiServices); } onButtonClick = () => { @@ -101,7 +116,7 @@ class MultiJobActionsMenuUI extends Component { if (this.props.jobs.some((j) => isManagedJob(j))) { this.props.showCloseJobsConfirmModal(this.props.jobs); } else { - closeJobs(this.props.jobs); + closeJobs(this.toastNotifications, this.mlJobService, this.props.jobs); } this.closePopover(); diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/group_selector.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/group_selector.js index 056bd58d045a5..9fe8bbf230322 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/group_selector.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/group_selector.js @@ -9,6 +9,7 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; +import { withKibana } from '@kbn/kibana-react-plugin/public'; import { EuiButton, @@ -24,11 +25,10 @@ import { import { cloneDeep } from 'lodash'; -import { ml } from '../../../../../services/ml_api_service'; import { checkPermission } from '../../../../../capabilities/check_capabilities'; import { GroupList } from './group_list'; import { NewGroupInput } from './new_group_input'; -import { getToastNotificationService } from '../../../../../services/toast_notification_service'; +import { toastNotificationServiceProvider } from '../../../../../services/toast_notification_service'; function createSelectedGroups(jobs, groups) { const jobIds = jobs.map((j) => j.id); @@ -54,15 +54,15 @@ function createSelectedGroups(jobs, groups) { return selectedGroups; } -export class GroupSelector extends Component { +export class GroupSelectorUI extends Component { static propTypes = { jobs: PropTypes.array.isRequired, allJobIds: PropTypes.array.isRequired, refreshJobs: PropTypes.func.isRequired, }; - constructor(props) { - super(props); + constructor(props, constructorContext) { + super(props, constructorContext); this.state = { isPopoverOpen: false, @@ -73,6 +73,9 @@ export class GroupSelector extends Component { this.refreshJobs = this.props.refreshJobs; this.canUpdateJob = checkPermission('canUpdateJob'); + this.toastNotificationsService = toastNotificationServiceProvider( + props.kibana.services.notifications.toasts + ); } static getDerivedStateFromProps(props, state) { @@ -88,6 +91,7 @@ export class GroupSelector extends Component { if (this.state.isPopoverOpen) { this.closePopover(); } else { + const ml = this.props.kibana.services.mlServices.mlApiServices; ml.jobs .groups() .then((groups) => { @@ -133,6 +137,7 @@ export class GroupSelector extends Component { }; applyChanges = () => { + const toastNotificationsService = this.toastNotificationsService; const { selectedGroups } = this.state; const { jobs } = this.props; const newJobs = jobs.map((j) => ({ @@ -153,6 +158,7 @@ export class GroupSelector extends Component { } const tempJobs = newJobs.map((j) => ({ jobId: j.id, groups: j.newGroups })); + const ml = this.props.kibana.services.mlServices.mlApiServices; ml.jobs .updateGroups(tempJobs) .then((resp) => { @@ -161,7 +167,7 @@ export class GroupSelector extends Component { // check success of each job update if (Object.hasOwn(resp, jobId)) { if (resp[jobId].success === false) { - getToastNotificationService().displayErrorToast(resp[jobId].error); + toastNotificationsService.displayErrorToast(resp[jobId].error); success = false; } } @@ -176,7 +182,7 @@ export class GroupSelector extends Component { } }) .catch((error) => { - getToastNotificationService().displayErrorToast(error); + toastNotificationsService.displayErrorToast(error); console.error(error); }); }; @@ -271,3 +277,5 @@ export class GroupSelector extends Component { ); } } + +export const GroupSelector = withKibana(GroupSelectorUI); diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/reset_job_modal/reset_job_modal.tsx b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/reset_job_modal/reset_job_modal.tsx index 5049610c325b4..1658c428d9b00 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/reset_job_modal/reset_job_modal.tsx +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/reset_job_modal/reset_job_modal.tsx @@ -25,6 +25,8 @@ import { i18n } from '@kbn/i18n'; import { resetJobs } from '../utils'; import type { MlSummaryJob } from '../../../../../../common/types/anomaly_detection_jobs'; import { RESETTING_JOBS_REFRESH_INTERVAL_MS } from '../../../../../../common/constants/jobs_list'; +import { useMlKibana } from '../../../../contexts/kibana'; +import { useMlJobService } from '../../../../services/job_service'; import { OpenJobsWarningCallout } from './open_jobs_warning_callout'; import { isManagedJob } from '../../../jobs_utils'; import { ManagedJobsWarningCallout } from '../confirm_modals/managed_jobs_warning_callout'; @@ -38,6 +40,12 @@ interface Props { } export const ResetJobModal: FC = ({ setShowFunction, unsetShowFunction, refreshJobs }) => { + const { + services: { + notifications: { toasts }, + }, + } = useMlKibana(); + const mlJobService = useMlJobService(); const [resetting, setResetting] = useState(false); const [modalVisible, setModalVisible] = useState(false); const [jobIds, setJobIds] = useState([]); @@ -73,11 +81,13 @@ export const ResetJobModal: FC = ({ setShowFunction, unsetShowFunction, r const resetJob = useCallback(async () => { setResetting(true); - await resetJobs(jobIds, deleteUserAnnotations); + await resetJobs(toasts, mlJobService, jobIds, deleteUserAnnotations); closeModal(); setTimeout(() => { refreshJobs(); }, RESETTING_JOBS_REFRESH_INTERVAL_MS); + // exclude mlJobservice from deps + // eslint-disable-next-line react-hooks/exhaustive-deps }, [closeModal, deleteUserAnnotations, jobIds, refreshJobs]); if (modalVisible === false || jobIds.length === 0) { diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/start_datafeed_modal/start_datafeed_modal.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/start_datafeed_modal/start_datafeed_modal.js index c4c53b00591f4..d56fc973a0249 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/start_datafeed_modal/start_datafeed_modal.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/start_datafeed_modal/start_datafeed_modal.js @@ -7,6 +7,7 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; +import moment from 'moment'; import { EuiButton, @@ -20,18 +21,23 @@ import { EuiCheckbox, } from '@elastic/eui'; -import moment from 'moment'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { context } from '@kbn/kibana-react-plugin/public'; + +import { mlJobServiceFactory } from '../../../../services/job_service'; +import { toastNotificationServiceProvider } from '../../../../services/toast_notification_service'; + +import { isManagedJob } from '../../../jobs_utils'; import { forceStartDatafeeds } from '../utils'; import { TimeRangeSelector } from './time_range_selector'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { isManagedJob } from '../../../jobs_utils'; - export class StartDatafeedModal extends Component { - constructor(props) { - super(props); + static contextType = context; + + constructor(props, constructorContext) { + super(props, constructorContext); const now = moment(); this.state = { @@ -50,6 +56,11 @@ export class StartDatafeedModal extends Component { this.initialSpecifiedStartTime = now; this.refreshJobs = this.props.refreshJobs; this.getShowCreateAlertFlyoutFunction = this.props.getShowCreateAlertFlyoutFunction; + this.toastNotifications = constructorContext.services.notifications.toasts; + this.mlJobService = mlJobServiceFactory( + toastNotificationServiceProvider(this.toastNotifications), + constructorContext.services.mlServices.mlApiServices + ); } componentDidMount() { @@ -114,7 +125,7 @@ export class StartDatafeedModal extends Component { ? this.state.endTime.valueOf() : this.state.endTime; - forceStartDatafeeds(jobs, start, end, () => { + forceStartDatafeeds(this.toastNotifications, this.mlJobService, jobs, start, end, () => { if (this.state.createAlert && jobs.length > 0) { this.getShowCreateAlertFlyoutFunction()(jobs.map((job) => job.id)); } diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.d.ts b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.d.ts index 7dd542dc3107f..a4c20309129f3 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.d.ts +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.d.ts @@ -5,19 +5,79 @@ * 2.0. */ -import type { CombinedJobWithStats } from '../../../../../common/types/anomaly_detection_jobs'; +import type { ApplicationStart, ToastsStart } from '@kbn/core/public'; -export function stopDatafeeds(jobs: Array<{ id: string }>, callback?: () => void): Promise; -export function closeJobs(jobs: Array<{ id: string }>, callback?: () => void): Promise; +import type { DATAFEED_STATE } from '../../../../../common/constants/states'; +import type { + CombinedJobWithStats, + MlSummaryJob, +} from '../../../../../common/types/anomaly_detection_jobs'; +import type { MlJobService } from '../../../services/job_service'; +import type { MlApiServices } from '../../../services/ml_api_service'; + +export function loadFullJob( + mlApiServices: MlApiServices, + jobId: string +): Promise; +export function loadJobForCloning(mlApiServices: MlApiServices, jobId: string): Promise; +export function isStartable(jobs: CombinedJobWithStats[]): boolean; +export function isClosable(jobs: CombinedJobWithStats[]): boolean; +export function isResettable(jobs: CombinedJobWithStats[]): boolean; +export function forceStartDatafeeds( + toastNotifications: ToastsStart, + mlJobService: MlJobService, + jobs: CombinedJobWithStats[], + start: number | undefined, + end: number | undefined, + finish?: () => void +): Promise; +export function stopDatafeeds( + toastNotifications: ToastsStart, + mlJobService: MlJobService, + jobs: CombinedJobWithStats[] | MlSummaryJob[], + finish?: () => void +): Promise; +export function showResults( + toastNotifications: ToastsStart, + resp: any, + action: DATAFEED_STATE +): void; +export function cloneJob( + toastNotifications: ToastsStart, + application: ApplicationStart, + mlApiServices: MlApiServices, + mlJobService: MlJobService, + jobId: string +): Promise; +export function closeJobs( + toastNotifications: ToastsStart, + mlJobService: MlJobService, + jobs: CombinedJobWithStats[] | MlSummaryJob[], + finish?: () => void +): Promise; export function deleteJobs( + toastNotifications: ToastsStart, + mlJobService: MlJobService, jobs: Array<{ id: string }>, deleteUserAnnotations?: boolean, deleteAlertingRules?: boolean, - callback?: () => void + finish?: () => void ): Promise; export function resetJobs( + toastNotifications: ToastsStart, + mlJobService: MlJobService, jobIds: string[], deleteUserAnnotations?: boolean, - callback?: () => void + finish?: () => void ): Promise; -export function loadFullJob(jobId: string): Promise; +export function filterJobs( + jobs: CombinedJobWithStats[], + clauses: Array<{ field: string; match: string; type: string; value: any }> +): CombinedJobWithStats[]; +export function jobProperty(job: CombinedJobWithStats, prop: string): any; +export function jobTagFilter(jobs: CombinedJobWithStats[], value: string): CombinedJobWithStats[]; +export function checkForAutoStartDatafeed( + mlJobService: MlJobService +): + | { id: string; hasDatafeed: boolean; latestTimestampSortValue: number; datafeedId: string } + | undefined; diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.js index 86ed4a125aead..471e56be7a840 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.js @@ -8,13 +8,7 @@ import { each } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { mlJobService } from '../../../services/job_service'; -import { - getToastNotificationService, - toastNotificationServiceProvider, -} from '../../../services/toast_notification_service'; -import { getApplication, getToastNotifications } from '../../../util/dependency_cache'; -import { ml } from '../../../services/ml_api_service'; +import { toastNotificationServiceProvider } from '../../../services/toast_notification_service'; import { stringMatch } from '../../../util/string_utils'; import { JOB_STATE, DATAFEED_STATE } from '../../../../../common/constants/states'; import { JOB_ACTION } from '../../../../../common/constants/job_actions'; @@ -25,9 +19,9 @@ import { ML_PAGES } from '../../../../../common/constants/locator'; import { PLUGIN_ID } from '../../../../../common/constants/app'; import { CREATED_BY_LABEL } from '../../../../../common/constants/new_job'; -export function loadFullJob(jobId) { +export function loadFullJob(mlApiServices, jobId) { return new Promise((resolve, reject) => { - ml.jobs + mlApiServices.jobs .jobs([jobId]) .then((jobs) => { if (jobs.length) { @@ -42,9 +36,9 @@ export function loadFullJob(jobId) { }); } -export function loadJobForCloning(jobId) { +export function loadJobForCloning(mlApiServices, jobId) { return new Promise((resolve, reject) => { - ml.jobs + mlApiServices.jobs .jobForCloning(jobId) .then((resp) => { if (resp) { @@ -86,16 +80,22 @@ export function isResettable(jobs) { ); } -export function forceStartDatafeeds(jobs, start, end, finish = () => {}) { +export function forceStartDatafeeds( + toastNotifications, + mlJobService, + jobs, + start, + end, + finish = () => {} +) { const datafeedIds = jobs.filter((j) => j.hasDatafeed).map((j) => j.datafeedId); mlJobService .forceStartDatafeeds(datafeedIds, start, end) .then((resp) => { - showResults(resp, DATAFEED_STATE.STARTED); + showResults(toastNotifications, resp, DATAFEED_STATE.STARTED); finish(); }) .catch((error) => { - const toastNotifications = getToastNotifications(); toastNotifications.addDanger( i18n.translate('xpack.ml.jobsList.startJobErrorMessage', { defaultMessage: 'Jobs failed to start', @@ -106,16 +106,15 @@ export function forceStartDatafeeds(jobs, start, end, finish = () => {}) { }); } -export function stopDatafeeds(jobs, finish = () => {}) { +export function stopDatafeeds(toastNotifications, mlJobService, jobs, finish = () => {}) { const datafeedIds = jobs.filter((j) => j.hasDatafeed).map((j) => j.datafeedId); mlJobService .stopDatafeeds(datafeedIds) .then((resp) => { - showResults(resp, DATAFEED_STATE.STOPPED); + showResults(toastNotifications, resp, DATAFEED_STATE.STOPPED); finish(); }) .catch((error) => { - const toastNotifications = getToastNotifications(); toastNotifications.addDanger( i18n.translate('xpack.ml.jobsList.stopJobErrorMessage', { defaultMessage: 'Jobs failed to stop', @@ -126,7 +125,7 @@ export function stopDatafeeds(jobs, finish = () => {}) { }); } -function showResults(resp, action) { +function showResults(toastNotifications, resp, action) { const successes = []; const failures = []; for (const d in resp) { @@ -184,7 +183,6 @@ function showResults(resp, action) { }); } - const toastNotifications = getToastNotifications(); if (successes.length > 0) { toastNotifications.addSuccess( i18n.translate('xpack.ml.jobsList.actionExecuteSuccessfullyNotificationMessage', { @@ -216,11 +214,17 @@ function showResults(resp, action) { } } -export async function cloneJob(jobId) { +export async function cloneJob( + toastNotifications, + application, + mlApiServices, + mlJobService, + jobId +) { try { const [{ job: cloneableJob, datafeed }, originalJob] = await Promise.all([ - loadJobForCloning(jobId), - loadFullJob(jobId, false), + loadJobForCloning(mlApiServices, jobId), + loadFullJob(mlApiServices, jobId), ]); const createdBy = originalJob?.custom_settings?.created_by; @@ -273,13 +277,14 @@ export async function cloneJob(jobId) { if (originalJob.calendars) { mlJobService.tempJobCloningObjects.calendars = await mlCalendarService.fetchCalendarsByIds( + mlApiServices, originalJob.calendars ); } - getApplication().navigateToApp(PLUGIN_ID, { path: ML_PAGES.ANOMALY_DETECTION_CREATE_JOB }); + application.navigateToApp(PLUGIN_ID, { path: ML_PAGES.ANOMALY_DETECTION_CREATE_JOB }); } catch (error) { - getToastNotificationService().displayErrorToast( + toastNotificationServiceProvider(toastNotifications).displayErrorToast( error, i18n.translate('xpack.ml.jobsList.cloneJobErrorMessage', { defaultMessage: 'Could not clone {jobId}. Job could not be found', @@ -289,16 +294,16 @@ export async function cloneJob(jobId) { } } -export function closeJobs(jobs, finish = () => {}) { +export function closeJobs(toastNotifications, mlJobService, jobs, finish = () => {}) { const jobIds = jobs.map((j) => j.id); mlJobService .closeJobs(jobIds) .then((resp) => { - showResults(resp, JOB_STATE.CLOSED); + showResults(toastNotifications, resp, JOB_STATE.CLOSED); finish(); }) .catch((error) => { - getToastNotificationService().displayErrorToast( + toastNotificationServiceProvider(toastNotifications).displayErrorToast( error, i18n.translate('xpack.ml.jobsList.closeJobErrorMessage', { defaultMessage: 'Jobs failed to close', @@ -308,15 +313,21 @@ export function closeJobs(jobs, finish = () => {}) { }); } -export function resetJobs(jobIds, deleteUserAnnotations, finish = () => {}) { +export function resetJobs( + toastNotifications, + mlJobService, + jobIds, + deleteUserAnnotations, + finish = () => {} +) { mlJobService .resetJobs(jobIds, deleteUserAnnotations) .then((resp) => { - showResults(resp, JOB_ACTION.RESET); + showResults(toastNotifications, resp, JOB_ACTION.RESET); finish(); }) .catch((error) => { - getToastNotificationService().displayErrorToast( + toastNotificationServiceProvider(toastNotifications).displayErrorToast( error, i18n.translate('xpack.ml.jobsList.resetJobErrorMessage', { defaultMessage: 'Jobs failed to reset', @@ -326,16 +337,23 @@ export function resetJobs(jobIds, deleteUserAnnotations, finish = () => {}) { }); } -export function deleteJobs(jobs, deleteUserAnnotations, deleteAlertingRules, finish = () => {}) { +export function deleteJobs( + toastNotifications, + mlJobService, + jobs, + deleteUserAnnotations, + deleteAlertingRules, + finish = () => {} +) { const jobIds = jobs.map((j) => j.id); mlJobService .deleteJobs(jobIds, deleteUserAnnotations, deleteAlertingRules) .then((resp) => { - showResults(resp, JOB_STATE.DELETED); + showResults(toastNotifications, resp, JOB_STATE.DELETED); finish(); }) .catch((error) => { - getToastNotificationService().displayErrorToast( + toastNotificationServiceProvider(toastNotifications).displayErrorToast( error, i18n.translate('xpack.ml.jobsList.deleteJobErrorMessage', { defaultMessage: 'Jobs failed to delete', @@ -440,7 +458,7 @@ function jobTagFilter(jobs, value) { // check to see if a job has been stored in mlJobService.tempJobCloningObjects // if it has, return an object with the minimum properties needed for the // start datafeed modal. -export function checkForAutoStartDatafeed() { +export function checkForAutoStartDatafeed(mlJobService) { const job = mlJobService.tempJobCloningObjects.job; const datafeed = mlJobService.tempJobCloningObjects.datafeed; if (job !== undefined) { diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/chart_loader/chart_loader.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/chart_loader/chart_loader.ts index 893b60046a85a..68ddfe1b83b44 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/chart_loader/chart_loader.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/chart_loader/chart_loader.ts @@ -11,10 +11,10 @@ import type { DataView } from '@kbn/data-views-plugin/common'; import type { Field, SplitField, AggFieldPair } from '@kbn/ml-anomaly-utils'; import type { RuntimeMappings } from '@kbn/ml-runtime-field-utils'; import type { IndicesOptions } from '../../../../../../common/types/anomaly_detection_jobs'; -import { ml } from '../../../../services/ml_api_service'; -import { mlResultsService } from '../../../../services/results_service'; +import { mlResultsServiceProvider } from '../../../../services/results_service'; import { getCategoryFields as getCategoryFieldsOrig } from './searches'; import { aggFieldPairsCanBeCharted } from '../job_creator/util/general'; +import type { MlApiServices } from '../../../../services/ml_api_service'; type DetectorIndex = number; export interface LineChartPoint { @@ -28,18 +28,29 @@ const eq = (newArgs: any[], lastArgs: any[]) => isEqual(newArgs, lastArgs); export class ChartLoader { protected _dataView: DataView; + protected _mlApiServices: MlApiServices; + private _timeFieldName: string = ''; private _query: object = {}; - private _newJobLineChart = memoizeOne(ml.jobs.newJobLineChart, eq); - private _newJobPopulationsChart = memoizeOne(ml.jobs.newJobPopulationsChart, eq); - private _getEventRateData = memoizeOne(mlResultsService.getEventRateData, eq); - private _getCategoryFields = memoizeOne(getCategoryFieldsOrig, eq); + private _newJobLineChart; + private _newJobPopulationsChart; + private _getEventRateData; + private _getCategoryFields; - constructor(indexPattern: DataView, query: object) { + constructor(mlApiServices: MlApiServices, indexPattern: DataView, query: object) { + this._mlApiServices = mlApiServices; this._dataView = indexPattern; this._query = query; + this._newJobLineChart = memoizeOne(mlApiServices.jobs.newJobLineChart, eq); + this._newJobPopulationsChart = memoizeOne(mlApiServices.jobs.newJobPopulationsChart, eq); + this._getEventRateData = memoizeOne( + mlResultsServiceProvider(mlApiServices).getEventRateData, + eq + ); + this._getCategoryFields = memoizeOne(getCategoryFieldsOrig, eq); + if (typeof indexPattern.timeFieldName === 'string') { this._timeFieldName = indexPattern.timeFieldName; } @@ -155,6 +166,7 @@ export class ChartLoader { indicesOptions?: IndicesOptions ): Promise { const { results } = await this._getCategoryFields( + this._mlApiServices, this._dataView.getIndexPattern(), field.name, 10, diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/chart_loader/searches.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/chart_loader/searches.ts index 971cf9b5ca315..3c4ca1c5f54d7 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/chart_loader/searches.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/chart_loader/searches.ts @@ -8,7 +8,7 @@ import { get } from 'lodash'; import type { RuntimeMappings } from '@kbn/ml-runtime-field-utils'; -import { ml } from '../../../../services/ml_api_service'; +import type { MlApiServices } from '../../../../services/ml_api_service'; import type { IndicesOptions } from '../../../../../../common/types/anomaly_detection_jobs'; interface CategoryResults { @@ -17,6 +17,7 @@ interface CategoryResults { } export function getCategoryFields( + mlApiServices: MlApiServices, indexPatternName: string, fieldName: string, size: number, @@ -25,23 +26,24 @@ export function getCategoryFields( indicesOptions?: IndicesOptions ): Promise { return new Promise((resolve, reject) => { - ml.esSearch({ - index: indexPatternName, - size: 0, - body: { - query, - aggs: { - catFields: { - terms: { - field: fieldName, - size, + mlApiServices + .esSearch({ + index: indexPatternName, + size: 0, + body: { + query, + aggs: { + catFields: { + terms: { + field: fieldName, + size, + }, }, }, + ...(runtimeMappings !== undefined ? { runtime_mappings: runtimeMappings } : {}), }, - ...(runtimeMappings !== undefined ? { runtime_mappings: runtimeMappings } : {}), - }, - ...(indicesOptions ?? {}), - }) + ...(indicesOptions ?? {}), + }) .then((resp: any) => { const catFields = get(resp, ['aggregations', 'catFields', 'buckets'], []); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/advanced_job_creator.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/advanced_job_creator.ts index 150b57402ee5e..30a472348d587 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/advanced_job_creator.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/advanced_job_creator.ts @@ -10,6 +10,8 @@ import type { DataView } from '@kbn/data-views-plugin/public'; import type { Field, Aggregation, SplitField } from '@kbn/ml-anomaly-utils'; import type { SavedSearch } from '@kbn/saved-search-plugin/public'; +import type { MlJobService } from '../../../../services/job_service'; +import type { MlApiServices } from '../../../../services/ml_api_service'; import { JobCreator } from './job_creator'; import type { Job, @@ -21,6 +23,7 @@ import { createBasicDetector } from './util/default_configs'; import { CREATED_BY_LABEL, JOB_TYPE } from '../../../../../../common/constants/new_job'; import { getRichDetectors } from './util/general'; import { isValidJson } from '../../../../../../common/util/validation_utils'; +import type { NewJobCapsService } from '../../../../services/new_job_capabilities/new_job_capabilities_service'; export interface RichDetector { agg: Aggregation | null; @@ -39,8 +42,15 @@ export class AdvancedJobCreator extends JobCreator { private _richDetectors: RichDetector[] = []; private _queryString: string; - constructor(indexPattern: DataView, savedSearch: SavedSearch | null, query: object) { - super(indexPattern, savedSearch, query); + constructor( + mlApiServices: MlApiServices, + mlJobService: MlJobService, + newJobCapsService: NewJobCapsService, + indexPattern: DataView, + savedSearch: SavedSearch | null, + query: object + ) { + super(mlApiServices, mlJobService, newJobCapsService, indexPattern, savedSearch, query); this.createdBy = CREATED_BY_LABEL.ADVANCED; this._queryString = JSON.stringify(this._datafeed_config.query); @@ -184,7 +194,13 @@ export class AdvancedJobCreator extends JobCreator { public cloneFromExistingJob(job: Job, datafeed: Datafeed) { this._overrideConfigs(job, datafeed); this.createdBy = CREATED_BY_LABEL.ADVANCED; - const detectors = getRichDetectors(job, datafeed, this.additionalFields, true); + const detectors = getRichDetectors( + this.newJobCapsService, + job, + datafeed, + this.additionalFields, + true + ); // keep track of the custom rules for each detector const customRules = this._detectors.map((d) => d.custom_rules); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/categorization_job_creator.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/categorization_job_creator.ts index 7d074ca0576cb..a12266e556e73 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/categorization_job_creator.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/categorization_job_creator.ts @@ -34,6 +34,9 @@ import { DEFAULT_BUCKET_SPAN, DEFAULT_RARE_BUCKET_SPAN, } from '../../../../../../common/constants/new_job'; +import type { MlJobService } from '../../../../services/job_service'; +import type { MlApiServices } from '../../../../services/ml_api_service'; +import type { NewJobCapsService } from '../../../../services/new_job_capabilities/new_job_capabilities_service'; import { getRichDetectors } from './util/general'; import { CategorizationExamplesLoader } from '../results_loader'; @@ -61,8 +64,15 @@ export class CategorizationJobCreator extends JobCreator { private _partitionFieldName: string | null = null; private _ccsVersionFailure: boolean = false; - constructor(indexPattern: DataView, savedSearch: SavedSearch | null, query: object) { - super(indexPattern, savedSearch, query); + constructor( + mlApiServices: MlApiServices, + mlJobService: MlJobService, + newJobCapsService: NewJobCapsService, + indexPattern: DataView, + savedSearch: SavedSearch | null, + query: object + ) { + super(mlApiServices, mlJobService, newJobCapsService, indexPattern, savedSearch, query); this.createdBy = CREATED_BY_LABEL.CATEGORIZATION; this._examplesLoader = new CategorizationExamplesLoader(this, indexPattern, query); @@ -254,7 +264,13 @@ export class CategorizationJobCreator extends JobCreator { public cloneFromExistingJob(job: Job, datafeed: Datafeed) { this._overrideConfigs(job, datafeed); this.createdBy = CREATED_BY_LABEL.CATEGORIZATION; - const detectors = getRichDetectors(job, datafeed, this.additionalFields, false); + const detectors = getRichDetectors( + this.newJobCapsService, + job, + datafeed, + this.additionalFields, + false + ); const dtr = detectors[0]; if (dtr !== undefined && dtr.agg !== null && dtr.field !== null) { diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/geo_job_creator.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/geo_job_creator.ts index 59e89070b38dd..76fda339afa44 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/geo_job_creator.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/geo_job_creator.ts @@ -8,6 +8,9 @@ import type { DataView } from '@kbn/data-views-plugin/public'; import type { Field, Aggregation, SplitField, AggFieldPair } from '@kbn/ml-anomaly-utils'; import type { SavedSearch } from '@kbn/saved-search-plugin/public'; +import type { MlJobService } from '../../../../services/job_service'; +import type { MlApiServices } from '../../../../services/ml_api_service'; +import type { NewJobCapsService } from '../../../../services/new_job_capabilities/new_job_capabilities_service'; import { JobCreator } from './job_creator'; import type { Job, @@ -27,8 +30,15 @@ export class GeoJobCreator extends JobCreator { protected _type: JOB_TYPE = JOB_TYPE.GEO; - constructor(indexPattern: DataView, savedSearch: SavedSearch | null, query: object) { - super(indexPattern, savedSearch, query); + constructor( + mlApiServices: MlApiServices, + mlJobService: MlJobService, + newJobCapsService: NewJobCapsService, + indexPattern: DataView, + savedSearch: SavedSearch | null, + query: object + ) { + super(mlApiServices, mlJobService, newJobCapsService, indexPattern, savedSearch, query); this.createdBy = CREATED_BY_LABEL.GEO; this._wizardInitialized$.next(true); } @@ -110,7 +120,13 @@ export class GeoJobCreator extends JobCreator { this._overrideConfigs(job, datafeed); this.createdBy = CREATED_BY_LABEL.GEO; this._sparseData = isSparseDataJob(job, datafeed); - const detectors = getRichDetectors(job, datafeed, this.additionalFields, false); + const detectors = getRichDetectors( + this.newJobCapsService, + job, + datafeed, + this.additionalFields, + false + ); this.removeSplitField(); this.removeAllDetectors(); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator.ts index c7eda482aafad..82e5fe1209a13 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator.ts @@ -22,6 +22,7 @@ import { import type { RuntimeMappings } from '@kbn/ml-runtime-field-utils'; import type { SavedSearch } from '@kbn/saved-search-plugin/public'; import { isPopulatedObject } from '@kbn/ml-is-populated-object'; +import type { MlApiServices } from '../../../../services/ml_api_service'; import type { IndexPatternTitle } from '../../../../../../common/types/kibana'; import { getQueryFromSavedSearchObject } from '../../../../util/index_utils'; import type { @@ -35,7 +36,7 @@ import type { } from '../../../../../../common/types/anomaly_detection_jobs'; import { combineFieldsAndAggs } from '../../../../../../common/util/fields_utils'; import { createEmptyJob, createEmptyDatafeed } from './util/default_configs'; -import { mlJobService } from '../../../../services/job_service'; +import type { MlJobService } from '../../../../services/job_service'; import { JobRunner, type ProgressSubscriber } from '../job_runner'; import type { CREATED_BY_LABEL } from '../../../../../../common/constants/new_job'; import { JOB_TYPE, SHARED_RESULTS_INDEX_NAME } from '../../../../../../common/constants/new_job'; @@ -46,7 +47,7 @@ import type { Calendar } from '../../../../../../common/types/calendars'; import { mlCalendarService } from '../../../../services/calendar_service'; import { getDatafeedAggregations } from '../../../../../../common/util/datafeed_utils'; import { getFirstKeyInObject } from '../../../../../../common/util/object_utils'; -import { ml } from '../../../../services/ml_api_service'; +import type { NewJobCapsService } from '../../../../services/new_job_capabilities/new_job_capabilities_service'; export class JobCreator { protected _type: JOB_TYPE = JOB_TYPE.SINGLE_METRIC; @@ -78,8 +79,21 @@ export class JobCreator { protected _wizardInitialized$ = new BehaviorSubject(false); public wizardInitialized$ = this._wizardInitialized$.asObservable(); - - constructor(indexPattern: DataView, savedSearch: SavedSearch | null, query: object) { + public mlApiServices: MlApiServices; + public mlJobService: MlJobService; + public newJobCapsService: NewJobCapsService; + + constructor( + mlApiServices: MlApiServices, + mlJobService: MlJobService, + newJobCapsService: NewJobCapsService, + indexPattern: DataView, + savedSearch: SavedSearch | null, + query: object + ) { + this.mlApiServices = mlApiServices; + this.mlJobService = mlJobService; + this.newJobCapsService = newJobCapsService; this._indexPattern = indexPattern; this._savedSearch = savedSearch; @@ -478,7 +492,7 @@ export class JobCreator { } for (const calendar of this._calendars) { - await mlCalendarService.assignNewJobId(calendar, this.jobId); + await mlCalendarService.assignNewJobId(this.mlApiServices, calendar, this.jobId); } } @@ -608,7 +622,7 @@ export class JobCreator { public async createJob(): Promise { try { - const { success, resp } = await mlJobService.saveNewJob(this._job_config); + const { success, resp } = await this.mlJobService.saveNewJob(this._job_config); await this._updateCalendars(); if (success === true) { @@ -624,7 +638,7 @@ export class JobCreator { public async createDatafeed(): Promise { try { const tempDatafeed = this._getDatafeedWithFilteredRuntimeMappings(); - return await mlJobService.saveNewDatafeed(tempDatafeed, this._job_config.job_id); + return await this.mlJobService.saveNewDatafeed(tempDatafeed, this._job_config.job_id); } catch (error) { throw error; } @@ -831,7 +845,7 @@ export class JobCreator { // load the start and end times for the selected index // and apply them to the job creator public async autoSetTimeRange(excludeFrozenData = true) { - const { start, end } = await ml.getTimeFieldRange({ + const { start, end } = await this.mlApiServices.getTimeFieldRange({ index: this._indexPatternTitle, timeFieldName: this.timeFieldName, query: excludeFrozenData ? addExcludeFrozenToQuery(this.query) : this.query, diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator_factory.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator_factory.ts index 1c599bc99a117..9e5f9988b292f 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator_factory.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator_factory.ts @@ -7,6 +7,9 @@ import type { DataView } from '@kbn/data-views-plugin/public'; import type { SavedSearch } from '@kbn/saved-search-plugin/public'; +import type { MlJobService } from '../../../../services/job_service'; +import type { MlApiServices } from '../../../../services/ml_api_service'; +import type { NewJobCapsService } from '../../../../services/new_job_capabilities/new_job_capabilities_service'; import { SingleMetricJobCreator } from './single_metric_job_creator'; import { MultiMetricJobCreator } from './multi_metric_job_creator'; import { PopulationJobCreator } from './population_job_creator'; @@ -19,7 +22,14 @@ import { JOB_TYPE } from '../../../../../../common/constants/new_job'; export const jobCreatorFactory = (jobType: JOB_TYPE) => - (indexPattern: DataView, savedSearch: SavedSearch | null, query: object) => { + ( + mlApiServices: MlApiServices, + mlJobService: MlJobService, + newJobCapsService: NewJobCapsService, + indexPattern: DataView, + savedSearch: SavedSearch | null, + query: object + ) => { let jc; switch (jobType) { case JOB_TYPE.SINGLE_METRIC: @@ -47,5 +57,5 @@ export const jobCreatorFactory = jc = SingleMetricJobCreator; break; } - return new jc(indexPattern, savedSearch, query); + return new jc(mlApiServices, mlJobService, newJobCapsService, indexPattern, savedSearch, query); }; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/multi_metric_job_creator.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/multi_metric_job_creator.ts index 39ad966b595e7..69ca14a40c4e4 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/multi_metric_job_creator.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/multi_metric_job_creator.ts @@ -8,6 +8,9 @@ import type { DataView } from '@kbn/data-views-plugin/public'; import type { Field, Aggregation, SplitField, AggFieldPair } from '@kbn/ml-anomaly-utils'; import type { SavedSearch } from '@kbn/saved-search-plugin/public'; +import type { MlJobService } from '../../../../services/job_service'; +import type { MlApiServices } from '../../../../services/ml_api_service'; +import type { NewJobCapsService } from '../../../../services/new_job_capabilities/new_job_capabilities_service'; import { JobCreator } from './job_creator'; import type { Job, @@ -26,8 +29,15 @@ export class MultiMetricJobCreator extends JobCreator { protected _type: JOB_TYPE = JOB_TYPE.MULTI_METRIC; - constructor(indexPattern: DataView, savedSearch: SavedSearch | null, query: object) { - super(indexPattern, savedSearch, query); + constructor( + mlApiServices: MlApiServices, + mlJobService: MlJobService, + newJobCapsService: NewJobCapsService, + indexPattern: DataView, + savedSearch: SavedSearch | null, + query: object + ) { + super(mlApiServices, mlJobService, newJobCapsService, indexPattern, savedSearch, query); this.createdBy = CREATED_BY_LABEL.MULTI_METRIC; this._wizardInitialized$.next(true); } @@ -90,7 +100,13 @@ export class MultiMetricJobCreator extends JobCreator { this._overrideConfigs(job, datafeed); this.createdBy = CREATED_BY_LABEL.MULTI_METRIC; this._sparseData = isSparseDataJob(job, datafeed); - const detectors = getRichDetectors(job, datafeed, this.additionalFields, false); + const detectors = getRichDetectors( + this.newJobCapsService, + job, + datafeed, + this.additionalFields, + false + ); if (datafeed.aggregations !== undefined) { // if we've converting from a single metric job, diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/population_job_creator.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/population_job_creator.ts index 08de5d30d11eb..342583636d37f 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/population_job_creator.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/population_job_creator.ts @@ -8,6 +8,9 @@ import type { DataView } from '@kbn/data-views-plugin/public'; import type { Field, Aggregation, SplitField, AggFieldPair } from '@kbn/ml-anomaly-utils'; import type { SavedSearch } from '@kbn/saved-search-plugin/public'; +import type { MlJobService } from '../../../../services/job_service'; +import type { MlApiServices } from '../../../../services/ml_api_service'; +import type { NewJobCapsService } from '../../../../services/new_job_capabilities/new_job_capabilities_service'; import { JobCreator } from './job_creator'; import type { Job, @@ -25,8 +28,15 @@ export class PopulationJobCreator extends JobCreator { private _byFields: SplitField[] = []; protected _type: JOB_TYPE = JOB_TYPE.POPULATION; - constructor(indexPattern: DataView, savedSearch: SavedSearch | null, query: object) { - super(indexPattern, savedSearch, query); + constructor( + mlApiServices: MlApiServices, + mlJobService: MlJobService, + newJobCapsService: NewJobCapsService, + indexPattern: DataView, + savedSearch: SavedSearch | null, + query: object + ) { + super(mlApiServices, mlJobService, newJobCapsService, indexPattern, savedSearch, query); this.createdBy = CREATED_BY_LABEL.POPULATION; this._wizardInitialized$.next(true); } @@ -132,7 +142,13 @@ export class PopulationJobCreator extends JobCreator { public cloneFromExistingJob(job: Job, datafeed: Datafeed) { this._overrideConfigs(job, datafeed); this.createdBy = CREATED_BY_LABEL.POPULATION; - const detectors = getRichDetectors(job, datafeed, this.additionalFields, false); + const detectors = getRichDetectors( + this.newJobCapsService, + job, + datafeed, + this.additionalFields, + false + ); this.removeAllDetectors(); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/rare_job_creator.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/rare_job_creator.ts index cc9601fc85634..aaaa7d101c09a 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/rare_job_creator.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/rare_job_creator.ts @@ -13,6 +13,9 @@ import { ML_JOB_AGGREGATION, } from '@kbn/ml-anomaly-utils'; import type { SavedSearch } from '@kbn/saved-search-plugin/public'; +import type { MlJobService } from '../../../../services/job_service'; +import type { MlApiServices } from '../../../../services/ml_api_service'; +import type { NewJobCapsService } from '../../../../services/new_job_capabilities/new_job_capabilities_service'; import { JobCreator } from './job_creator'; import type { Job, @@ -33,8 +36,15 @@ export class RareJobCreator extends JobCreator { private _rareAgg: Aggregation; private _freqRareAgg: Aggregation; - constructor(indexPattern: DataView, savedSearch: SavedSearch | null, query: object) { - super(indexPattern, savedSearch, query); + constructor( + mlApiServices: MlApiServices, + mlJobService: MlJobService, + newJobCapsService: NewJobCapsService, + indexPattern: DataView, + savedSearch: SavedSearch | null, + query: object + ) { + super(mlApiServices, mlJobService, newJobCapsService, indexPattern, savedSearch, query); this.createdBy = CREATED_BY_LABEL.RARE; this._wizardInitialized$.next(true); this._rareAgg = {} as Aggregation; @@ -154,7 +164,13 @@ export class RareJobCreator extends JobCreator { this._overrideConfigs(job, datafeed); this.createdBy = CREATED_BY_LABEL.RARE; this._sparseData = isSparseDataJob(job, datafeed); - const detectors = getRichDetectors(job, datafeed, this.additionalFields, false); + const detectors = getRichDetectors( + this.newJobCapsService, + job, + datafeed, + this.additionalFields, + false + ); this.removeSplitField(); this.removePopulationField(); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/single_metric_job_creator.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/single_metric_job_creator.ts index 07a949b6a34a3..2b7a1133c1d1f 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/single_metric_job_creator.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/single_metric_job_creator.ts @@ -15,6 +15,8 @@ import { ES_AGGREGATION, } from '@kbn/ml-anomaly-utils'; import type { SavedSearch } from '@kbn/saved-search-plugin/public'; +import type { MlJobService } from '../../../../services/job_service'; +import type { MlApiServices } from '../../../../services/ml_api_service'; import { parseInterval } from '../../../../../../common/util/parse_interval'; import { JobCreator } from './job_creator'; import type { @@ -27,12 +29,20 @@ import { createBasicDetector } from './util/default_configs'; import { JOB_TYPE, CREATED_BY_LABEL } from '../../../../../../common/constants/new_job'; import { getRichDetectors } from './util/general'; import { isSparseDataJob } from './util/general'; +import type { NewJobCapsService } from '../../../../services/new_job_capabilities/new_job_capabilities_service'; export class SingleMetricJobCreator extends JobCreator { protected _type: JOB_TYPE = JOB_TYPE.SINGLE_METRIC; - constructor(indexPattern: DataView, savedSearch: SavedSearch | null, query: object) { - super(indexPattern, savedSearch, query); + constructor( + mlApiServices: MlApiServices, + mlJobService: MlJobService, + newJobCapsService: NewJobCapsService, + indexPattern: DataView, + savedSearch: SavedSearch | null, + query: object + ) { + super(mlApiServices, mlJobService, newJobCapsService, indexPattern, savedSearch, query); this.createdBy = CREATED_BY_LABEL.SINGLE_METRIC; this._wizardInitialized$.next(true); } @@ -203,7 +213,13 @@ export class SingleMetricJobCreator extends JobCreator { this._overrideConfigs(job, datafeed); this.createdBy = CREATED_BY_LABEL.SINGLE_METRIC; this._sparseData = isSparseDataJob(job, datafeed); - const detectors = getRichDetectors(job, datafeed, this.additionalFields, false); + const detectors = getRichDetectors( + this.newJobCapsService, + job, + datafeed, + this.additionalFields, + false + ); this.removeAllDetectors(); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/general.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/general.ts index ca9a7f89b3cb9..f1cbd2dc87135 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/general.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/general.ts @@ -25,28 +25,30 @@ import type { Datafeed, Detector, } from '../../../../../../../common/types/anomaly_detection_jobs'; -import { newJobCapsService } from '../../../../../services/new_job_capabilities/new_job_capabilities_service'; +import type { NewJobCapsService } from '../../../../../services/new_job_capabilities/new_job_capabilities_service'; import type { NavigateToPath } from '../../../../../contexts/kibana'; import { ML_PAGES } from '../../../../../../../common/constants/locator'; -import { mlJobService } from '../../../../../services/job_service'; +import type { MlJobService } from '../../../../../services/job_service'; import type { JobCreatorType } from '..'; import { CREATED_BY_LABEL, JOB_TYPE } from '../../../../../../../common/constants/new_job'; -const getFieldByIdFactory = (additionalFields: Field[]) => (id: string) => { - let field = newJobCapsService.getFieldById(id); - // if no field could be found it may be a pretend field, like mlcategory or a script field - if (field === null) { - if (id === MLCATEGORY) { - field = mlCategory; - } else if (additionalFields.length) { - field = additionalFields.find((f) => f.id === id) || null; +const getFieldByIdFactory = + (newJobCapsService: NewJobCapsService, additionalFields: Field[]) => (id: string) => { + let field = newJobCapsService.getFieldById(id); + // if no field could be found it may be a pretend field, like mlcategory or a script field + if (field === null) { + if (id === MLCATEGORY) { + field = mlCategory; + } else if (additionalFields.length) { + field = additionalFields.find((f) => f.id === id) || null; + } } - } - return field; -}; + return field; + }; // populate the detectors with Field and Agg objects loaded from the job capabilities service export function getRichDetectors( + newJobCapsService: NewJobCapsService, job: Job, datafeed: Datafeed, additionalFields: Field[], @@ -54,7 +56,7 @@ export function getRichDetectors( ) { const detectors = advanced ? getDetectorsAdvanced(job, datafeed) : getDetectors(job, datafeed); - const getFieldById = getFieldByIdFactory(additionalFields); + const getFieldById = getFieldByIdFactory(newJobCapsService, additionalFields); return detectors.map((d) => { let field = null; @@ -234,64 +236,54 @@ export function isSparseDataJob(job: Job, datafeed: Datafeed): boolean { return false; } -export function stashJobForCloning( - jobCreator: JobCreatorType, - skipTimeRangeStep: boolean = false, - includeTimeRange: boolean = false, - autoSetTimeRange: boolean = false -) { - mlJobService.tempJobCloningObjects.job = jobCreator.jobConfig; - mlJobService.tempJobCloningObjects.datafeed = jobCreator.datafeedConfig; - mlJobService.tempJobCloningObjects.createdBy = jobCreator.createdBy ?? undefined; - - // skip over the time picker step of the wizard - mlJobService.tempJobCloningObjects.skipTimeRangeStep = skipTimeRangeStep; - - if (includeTimeRange === true && autoSetTimeRange === false) { - // auto select the start and end dates of the time picker - mlJobService.tempJobCloningObjects.start = jobCreator.start; - mlJobService.tempJobCloningObjects.end = jobCreator.end; - } else if (autoSetTimeRange === true) { - mlJobService.tempJobCloningObjects.autoSetTimeRange = true; - } - - mlJobService.tempJobCloningObjects.calendars = jobCreator.calendars; -} - export function convertToMultiMetricJob( + mlJobService: MlJobService, jobCreator: JobCreatorType, navigateToPath: NavigateToPath ) { jobCreator.createdBy = CREATED_BY_LABEL.MULTI_METRIC; jobCreator.modelPlot = false; - stashJobForCloning(jobCreator, true, true); + mlJobService.stashJobForCloning(jobCreator, true, true); navigateToPath(ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_CONVERT_TO_MULTI_METRIC, true); } -export function convertToAdvancedJob(jobCreator: JobCreatorType, navigateToPath: NavigateToPath) { +export function convertToAdvancedJob( + mlJobService: MlJobService, + jobCreator: JobCreatorType, + navigateToPath: NavigateToPath +) { jobCreator.createdBy = null; - stashJobForCloning(jobCreator, true, true); + mlJobService.stashJobForCloning(jobCreator, true, true); navigateToPath(ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_CONVERT_TO_ADVANCED, true); } -export function resetAdvancedJob(jobCreator: JobCreatorType, navigateToPath: NavigateToPath) { +export function resetAdvancedJob( + mlJobService: MlJobService, + jobCreator: JobCreatorType, + navigateToPath: NavigateToPath +) { jobCreator.createdBy = null; - stashJobForCloning(jobCreator, true, false); + mlJobService.stashJobForCloning(jobCreator, true, false); navigateToPath(ML_PAGES.ANOMALY_DETECTION_CREATE_JOB); } -export function resetJob(jobCreator: JobCreatorType, navigateToPath: NavigateToPath) { +export function resetJob( + mlJobService: MlJobService, + jobCreator: JobCreatorType, + navigateToPath: NavigateToPath +) { jobCreator.jobId = ''; - stashJobForCloning(jobCreator, true, true); + mlJobService.stashJobForCloning(jobCreator, true, true); navigateToPath(ML_PAGES.ANOMALY_DETECTION_CREATE_JOB); } export function advancedStartDatafeed( + mlJobService: MlJobService, jobCreator: JobCreatorType | null, navigateToPath: NavigateToPath ) { if (jobCreator !== null) { - stashJobForCloning(jobCreator, false, false); + mlJobService.stashJobForCloning(jobCreator, false, false); } navigateToPath('/jobs'); } diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/model_memory_estimator.test.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/model_memory_estimator.test.ts index 44e2d3de6a0c8..f73c54653f93b 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/model_memory_estimator.test.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/model_memory_estimator.test.ts @@ -10,28 +10,17 @@ import { useFakeTimers } from 'sinon'; import type { CalculatePayload } from './model_memory_estimator'; import { modelMemoryEstimatorProvider } from './model_memory_estimator'; import type { JobValidator } from '../../job_validator'; -import { ml } from '../../../../../services/ml_api_service'; +import type { MlApiServices } from '../../../../../services/ml_api_service'; import type { JobCreator } from '../job_creator'; import { BehaviorSubject } from 'rxjs'; -jest.mock('../../../../../services/ml_api_service', () => { - return { - ml: { - calculateModelMemoryLimit$: jest.fn(() => { - // eslint-disable-next-line @typescript-eslint/no-var-requires - const { of } = require('rxjs'); - return of({ modelMemoryLimit: '15MB' }); - }), - }, - }; -}); - describe('delay', () => { let clock: SinonFakeTimers; let modelMemoryEstimator: ReturnType; let mockJobCreator: JobCreator; let wizardInitialized$: BehaviorSubject; let mockJobValidator: JobValidator; + let mockMlApiServices: MlApiServices; beforeEach(() => { clock = useFakeTimers(); @@ -42,7 +31,19 @@ describe('delay', () => { mockJobCreator = { wizardInitialized$, } as unknown as JobCreator; - modelMemoryEstimator = modelMemoryEstimatorProvider(mockJobCreator, mockJobValidator); + mockMlApiServices = { + calculateModelMemoryLimit$: jest.fn(() => { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { of } = require('rxjs'); + return of({ modelMemoryLimit: '15MB' }); + }), + } as unknown as MlApiServices; + + modelMemoryEstimator = modelMemoryEstimatorProvider( + mockJobCreator, + mockJobValidator, + mockMlApiServices + ); }); afterEach(() => { clock.restore(); @@ -56,7 +57,7 @@ describe('delay', () => { modelMemoryEstimator.update({ analysisConfig: { detectors: [{}] } } as CalculatePayload); clock.tick(601); - expect(ml.calculateModelMemoryLimit$).not.toHaveBeenCalled(); + expect(mockMlApiServices.calculateModelMemoryLimit$).not.toHaveBeenCalled(); expect(spy).not.toHaveBeenCalled(); }); @@ -97,7 +98,7 @@ describe('delay', () => { } as CalculatePayload); clock.tick(601); - expect(ml.calculateModelMemoryLimit$).toHaveBeenCalledTimes(1); + expect(mockMlApiServices.calculateModelMemoryLimit$).toHaveBeenCalledTimes(1); expect(spy).toHaveBeenCalledTimes(1); }); @@ -115,6 +116,6 @@ describe('delay', () => { mockJobValidator.isModelMemoryEstimationPayloadValid = false; clock.tick(601); - expect(ml.calculateModelMemoryLimit$).not.toHaveBeenCalled(); + expect(mockMlApiServices.calculateModelMemoryLimit$).not.toHaveBeenCalled(); }); }); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/model_memory_estimator.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/model_memory_estimator.ts index 6bcd6adb53c81..b3550b039a862 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/model_memory_estimator.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/model_memory_estimator.ts @@ -26,19 +26,20 @@ import { i18n } from '@kbn/i18n'; import { type MLHttpFetchError, extractErrorMessage } from '@kbn/ml-error-utils'; import { DEFAULT_MODEL_MEMORY_LIMIT } from '../../../../../../../common/constants/new_job'; -import { ml } from '../../../../../services/ml_api_service'; import type { JobValidator } from '../../job_validator/job_validator'; import { VALIDATION_DELAY_MS } from '../../job_validator/job_validator'; import { useMlKibana } from '../../../../../contexts/kibana'; import type { JobCreator } from '../job_creator'; +import type { MlApiServices } from '../../../../../services/ml_api_service'; -export type CalculatePayload = Parameters[0]; +export type CalculatePayload = Parameters[0]; type ModelMemoryEstimator = ReturnType; export const modelMemoryEstimatorProvider = ( jobCreator: JobCreator, - jobValidator: JobValidator + jobValidator: JobValidator, + mlApiServices: MlApiServices ) => { const modelMemoryCheck$ = new Subject(); const error$ = new Subject(); @@ -64,7 +65,7 @@ export const modelMemoryEstimatorProvider = ( // don't call the endpoint with invalid payload filter(() => jobValidator.isModelMemoryEstimationPayloadValid), switchMap((payload) => { - return ml.calculateModelMemoryLimit$(payload).pipe( + return mlApiServices.calculateModelMemoryLimit$(payload).pipe( pluck('modelMemoryLimit'), catchError((error) => { // eslint-disable-next-line no-console @@ -90,13 +91,16 @@ export const useModelMemoryEstimator = ( jobCreatorUpdated: number ) => { const { - services: { notifications }, + services: { + notifications, + mlServices: { mlApiServices }, + }, } = useMlKibana(); // Initialize model memory estimator only once const modelMemoryEstimator = useMemo( - () => modelMemoryEstimatorProvider(jobCreator, jobValidator), - [jobCreator, jobValidator] + () => modelMemoryEstimatorProvider(jobCreator, jobValidator, mlApiServices), + [jobCreator, jobValidator, mlApiServices] ); // Listen for estimation results and errors diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_runner/job_runner.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_runner/job_runner.ts index 13d85419c2c6e..bd3d68afdc6d0 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_runner/job_runner.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_runner/job_runner.ts @@ -6,8 +6,8 @@ */ import { BehaviorSubject } from 'rxjs'; -import { ml } from '../../../../services/ml_api_service'; -import { mlJobService } from '../../../../services/job_service'; +import type { MlApiServices } from '../../../../services/ml_api_service'; +import type { MlJobService } from '../../../../services/job_service'; import type { JobCreator } from '../job_creator'; import type { DatafeedId, JobId } from '../../../../../../common/types/anomaly_detection_jobs'; import { DATAFEED_STATE } from '../../../../../../common/constants/states'; @@ -22,6 +22,8 @@ export type ProgressSubscriber = (progress: number) => void; export type JobAssignmentSubscriber = (assigned: boolean) => void; export class JobRunner { + private _mlApiServices: MlApiServices; + private _mlJobService: MlJobService; private _jobId: JobId; private _datafeedId: DatafeedId; private _start: number = 0; @@ -42,6 +44,8 @@ export class JobRunner { private _jobAssignedToNode$: BehaviorSubject; constructor(jobCreator: JobCreator) { + this._mlApiServices = jobCreator.mlApiServices; + this._mlJobService = jobCreator.mlJobService; this._jobId = jobCreator.jobId; this._datafeedId = jobCreator.datafeedId; this._start = jobCreator.start; @@ -68,7 +72,7 @@ export class JobRunner { private async openJob(): Promise { try { - const { node }: { node?: string } = await mlJobService.openJob(this._jobId); + const { node }: { node?: string } = await this._mlJobService.openJob(this._jobId); this._jobAssignedToNode = node !== undefined && node.length > 0; this._jobAssignedToNode$.next(this._jobAssignedToNode); } catch (error) { @@ -92,7 +96,7 @@ export class JobRunner { pollProgress === true ? this._subscribers.map((s) => this._progress$.subscribe(s)) : []; await this.openJob(); - const { started } = await mlJobService.startDatafeed( + const { started } = await this._mlJobService.startDatafeed( this._datafeedId, this._jobId, start, @@ -189,7 +193,7 @@ export class JobRunner { } private async _isJobAssigned(): Promise { - const { jobs } = await ml.getJobStats({ jobId: this._jobId }); + const { jobs } = await this._mlApiServices.getJobStats({ jobId: this._jobId }); return jobs.length > 0 && jobs[0].node !== undefined; } @@ -208,7 +212,7 @@ export class JobRunner { isRunning: boolean; isJobClosed: boolean; }> { - return await ml.jobs.getLookBackProgress(this._jobId, this._start, this._end); + return await this._mlApiServices.jobs.getLookBackProgress(this._jobId, this._start, this._end); } public subscribeToProgress(func: ProgressSubscriber) { diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_validator/validators.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_validator/validators.ts index 61bf8487b09e6..0ec37e612a3fa 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_validator/validators.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_validator/validators.ts @@ -13,7 +13,6 @@ import type { CardinalityModelPlotHigh, CardinalityValidationResult, } from '../../../../services/ml_api_service'; -import { ml } from '../../../../services/ml_api_service'; import type { JobCreator } from '../job_creator'; import type { CombinedJob } from '../../../../../../common/types/anomaly_detection_jobs'; import type { BasicValidations } from './job_validator'; @@ -82,7 +81,7 @@ export function cardinalityValidator( }), switchMap(({ jobCreator }) => { // Perform a cardinality check only with enabled model plot. - return ml + return jobCreator.mlApiServices .validateCardinality$({ ...jobCreator.jobConfig, datafeed_config: jobCreator.datafeedConfig, @@ -114,12 +113,11 @@ export function cardinalityValidator( export function jobIdValidator(jobCreator$: Subject): Observable { return jobCreator$.pipe( - map((jobCreator) => { - return jobCreator.jobId; - }), // No need to perform an API call if the analysis configuration hasn't been changed - distinctUntilChanged((prevJobId, currJobId) => prevJobId === currJobId), - switchMap((jobId) => ml.jobs.jobsExist$([jobId], true)), + distinctUntilChanged( + (prevJobCreator, currJobCreator) => prevJobCreator.jobId === currJobCreator.jobId + ), + switchMap((jobCreator) => jobCreator.mlApiServices.jobs.jobsExist$([jobCreator.jobId], true)), map((jobExistsResults) => { const jobs = Object.values(jobExistsResults); const valid = jobs?.[0].exists === false; @@ -135,13 +133,13 @@ export function jobIdValidator(jobCreator$: Subject): Observable): Observable { return jobCreator$.pipe( - map((jobCreator) => jobCreator.groups), // No need to perform an API call if the analysis configuration hasn't been changed distinctUntilChanged( - (prevGroups, currGroups) => JSON.stringify(prevGroups) === JSON.stringify(currGroups) + (prevJobCreator, currJobCreator) => + JSON.stringify(prevJobCreator.groups) === JSON.stringify(currJobCreator.groups) ), - switchMap((groups) => { - return ml.jobs.jobsExist$(groups, true); + switchMap((jobCreator) => { + return jobCreator.mlApiServices.jobs.jobsExist$(jobCreator.groups, true); }), map((jobExistsResults) => { const groups = Object.values(jobExistsResults); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/map_loader/map_loader.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/map_loader/map_loader.ts index 242ce9d472495..a75152e93bd88 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/map_loader/map_loader.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/map_loader/map_loader.ts @@ -13,13 +13,20 @@ import type { CreateLayerDescriptorParams, MapsStartApi } from '@kbn/maps-plugin import type { Query } from '@kbn/es-query'; import type { Field, SplitField } from '@kbn/ml-anomaly-utils'; import { ChartLoader } from '../chart_loader'; +import type { MlApiServices } from '../../../../services/ml_api_service'; + const eq = (newArgs: any[], lastArgs: any[]) => isEqual(newArgs, lastArgs); export class MapLoader extends ChartLoader { private _getMapData; - constructor(indexPattern: DataView, query: object, mapsPlugin: MapsStartApi | undefined) { - super(indexPattern, query); + constructor( + mlApiServices: MlApiServices, + indexPattern: DataView, + query: object, + mapsPlugin: MapsStartApi | undefined + ) { + super(mlApiServices, indexPattern, query); this._getMapData = mapsPlugin ? memoizeOne(mapsPlugin.createLayerDescriptors.createESSearchSourceLayerDescriptor, eq) diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/results_loader/categorization_examples_loader.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/results_loader/categorization_examples_loader.ts index b95eb08a3725f..de24a7cff2995 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/results_loader/categorization_examples_loader.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/results_loader/categorization_examples_loader.ts @@ -10,7 +10,6 @@ import { CATEGORY_EXAMPLES_VALIDATION_STATUS } from '@kbn/ml-category-validator' import { NUMBER_OF_CATEGORY_EXAMPLES } from '../../../../../../common/constants/new_job'; import type { IndexPatternTitle } from '../../../../../../common/types/kibana'; import type { CategorizationJobCreator } from '../job_creator'; -import { ml } from '../../../../services/ml_api_service'; export class CategorizationExamplesLoader { private _jobCreator: CategorizationJobCreator; @@ -40,7 +39,7 @@ export class CategorizationExamplesLoader { }; } - const resp = await ml.jobs.categorizationFieldExamples( + const resp = await this._jobCreator.mlApiServices.jobs.categorizationFieldExamples( this._indexPatternTitle, this._query, NUMBER_OF_CATEGORY_EXAMPLES, diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/results_loader/results_loader.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/results_loader/results_loader.ts index 23b16bbd32499..742d0b476ebac 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/results_loader/results_loader.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/results_loader/results_loader.ts @@ -15,7 +15,10 @@ import { parseInterval } from '../../../../../../common/util/parse_interval'; import { JOB_TYPE } from '../../../../../../common/constants/new_job'; import type { ModelPlotOutputResults } from '../../../../services/results_service'; -import { mlResultsService } from '../../../../services/results_service'; +import { + mlResultsServiceProvider, + type MlResultsService, +} from '../../../../services/results_service'; import type { JobCreatorType } from '../job_creator'; import { isMultiMetricJobCreator } from '../job_creator'; @@ -66,6 +69,7 @@ export class ResultsLoader { private _lastModelTimeStamp: number = 0; private _lastResultsTimeout: any = null; private _chartLoader: ChartLoader; + private _mlResultsService: MlResultsService; private _results: Results = { progress: 0, @@ -81,6 +85,7 @@ export class ResultsLoader { this._chartInterval = chartInterval; this._results$ = new BehaviorSubject(this._results); this._chartLoader = chartLoader; + this._mlResultsService = mlResultsServiceProvider(jobCreator.mlApiServices); jobCreator.subscribeToProgress(this.progressSubscriber); } @@ -162,7 +167,7 @@ export class ResultsLoader { return { [dtrIndex]: [emptyModelItem] }; } const resp = await lastValueFrom( - mlResultsService.getModelPlotOutput( + this._mlResultsService.getModelPlotOutput( this._jobCreator.jobId, dtrIndex, [], @@ -214,7 +219,7 @@ export class ResultsLoader { } private async _loadJobAnomalyData(dtrIndex: number): Promise> { - const resp = await mlResultsService.getScoresByBucket( + const resp = await this._mlResultsService.getScoresByBucket( [this._jobCreator.jobId], this._jobCreator.start, this._jobCreator.end, @@ -237,6 +242,7 @@ export class ResultsLoader { private async _loadDetectorsAnomalyData(): Promise> { const resp = await getScoresByRecord( + this._jobCreator.mlApiServices, this._jobCreator.jobId, this._jobCreator.start, this._jobCreator.end, diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/results_loader/searches.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/results_loader/searches.ts index fa72c2c3e10f4..738bb880bd700 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/results_loader/searches.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/results_loader/searches.ts @@ -8,7 +8,7 @@ import { get } from 'lodash'; import { escapeForElasticsearchQuery } from '../../../../util/string_utils'; -import { ml } from '../../../../services/ml_api_service'; +import type { MlApiServices } from '../../../../services/ml_api_service'; interface SplitFieldWithValue { name: string; @@ -30,6 +30,7 @@ interface ProcessedResults { // detector swimlane search export function getScoresByRecord( + mlApiServices: MlApiServices, jobId: string, earliestMs: number, latestMs: number, @@ -53,7 +54,7 @@ export function getScoresByRecord( jobIdFilterStr += `"${String(firstSplitField.value).replace(/\\/g, '\\\\')}"`; } - ml.results + mlApiServices.results .anomalySearch( { body: { diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/job_from_dashboard/quick_create_job_base.ts b/x-pack/plugins/ml/public/application/jobs/new_job/job_from_dashboard/quick_create_job_base.ts index d6bfc450a4cfa..b685bb181f7c6 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/job_from_dashboard/quick_create_job_base.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/job_from_dashboard/quick_create_job_base.ts @@ -22,6 +22,7 @@ import { FilterStateStore } from '@kbn/es-query'; import type { ErrorType } from '@kbn/ml-error-utils'; import type { DataViewsContract } from '@kbn/data-views-plugin/public'; import type { MlApiServices } from '../../../services/ml_api_service'; +import type { MlJobService } from '../../../services/job_service'; import type { Job, Datafeed } from '../../../../../common/types/anomaly_detection_jobs'; import { getFiltersForDSLQuery } from '../../../../../common/util/job_utils'; import type { CREATED_BY_LABEL } from '../../../../../common/constants/new_job'; @@ -56,7 +57,8 @@ export class QuickJobCreatorBase { protected readonly kibanaConfig: IUiSettingsClient, protected readonly timeFilter: TimefilterContract, protected readonly dashboardService: DashboardStart, - protected readonly mlApiServices: MlApiServices + protected readonly mlApiServices: MlApiServices, + protected readonly mlJobService: MlJobService ) {} protected async putJobAndDataFeed({ diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/job_from_lens/quick_create_job.ts b/x-pack/plugins/ml/public/application/jobs/new_job/job_from_lens/quick_create_job.ts index 88ad820c059f2..3ce3289a424b8 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/job_from_lens/quick_create_job.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/job_from_lens/quick_create_job.ts @@ -19,8 +19,8 @@ import type { DashboardStart } from '@kbn/dashboard-plugin/public'; import type { LensApi } from '@kbn/lens-plugin/public'; import type { JobCreatorType } from '../common/job_creator'; import { createEmptyJob, createEmptyDatafeed } from '../common/job_creator/util/default_configs'; -import { stashJobForCloning } from '../common/job_creator/util/general'; import type { MlApiServices } from '../../../services/ml_api_service'; +import type { MlJobService } from '../../../services/job_service'; import { CREATED_BY_LABEL, DEFAULT_BUCKET_SPAN, @@ -42,9 +42,10 @@ export class QuickLensJobCreator extends QuickJobCreatorBase { kibanaConfig: IUiSettingsClient, timeFilter: TimefilterContract, dashboardService: DashboardStart, - mlApiServices: MlApiServices + mlApiServices: MlApiServices, + mlJobService: MlJobService ) { - super(dataViews, kibanaConfig, timeFilter, dashboardService, mlApiServices); + super(dataViews, kibanaConfig, timeFilter, dashboardService, mlApiServices, mlJobService); } public async createAndSaveJob( @@ -115,7 +116,7 @@ export class QuickLensJobCreator extends QuickJobCreatorBase { // add job config and start and end dates to the // job cloning stash, so they can be used // by the new job wizards - stashJobForCloning( + this.mlJobService.stashJobForCloning( { jobConfig, datafeedConfig, diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/job_from_lens/route_resolver.ts b/x-pack/plugins/ml/public/application/jobs/new_job/job_from_lens/route_resolver.ts index d6a815777fa9d..dd6fb765e158b 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/job_from_lens/route_resolver.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/job_from_lens/route_resolver.ts @@ -15,6 +15,7 @@ import type { DashboardStart } from '@kbn/dashboard-plugin/public'; import type { DataViewsContract } from '@kbn/data-views-plugin/public'; import { QuickLensJobCreator } from './quick_create_job'; import type { MlApiServices } from '../../../services/ml_api_service'; +import type { MlJobService } from '../../../services/job_service'; import { getDefaultQuery, getRisonValue } from '../utils/new_job_utils'; @@ -25,6 +26,7 @@ interface Dependencies { timeFilter: TimefilterContract; dashboardService: DashboardStart; mlApiServices: MlApiServices; + mlJobService: MlJobService; } export async function resolver( deps: Dependencies, @@ -35,7 +37,15 @@ export async function resolver( filtersRisonString: string, layerIndexRisonString: string ) { - const { dataViews, lens, mlApiServices, timeFilter, kibanaConfig, dashboardService } = deps; + const { + dataViews, + lens, + mlApiServices, + mlJobService, + timeFilter, + kibanaConfig, + dashboardService, + } = deps; if (lensSavedObjectRisonString === undefined) { throw new Error('Cannot create visualization'); } @@ -57,7 +67,8 @@ export async function resolver( kibanaConfig, timeFilter, dashboardService, - mlApiServices + mlApiServices, + mlJobService ); await jobCreator.createAndStashADJob(vis, from, to, query, filters, layerIndex); } diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/job_from_map/quick_create_job.ts b/x-pack/plugins/ml/public/application/jobs/new_job/job_from_map/quick_create_job.ts index 82a22a854af02..0f14eaaf515fe 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/job_from_map/quick_create_job.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/job_from_map/quick_create_job.ts @@ -13,6 +13,7 @@ import type { DataView, DataViewsContract } from '@kbn/data-views-plugin/public' import type { DashboardStart } from '@kbn/dashboard-plugin/public'; import type { MapApi } from '@kbn/maps-plugin/public'; import type { MlApiServices } from '../../../services/ml_api_service'; +import type { MlJobService } from '../../../services/job_service'; import { CREATED_BY_LABEL, JOB_TYPE, @@ -20,7 +21,6 @@ import { } from '../../../../../common/constants/new_job'; import { createEmptyJob, createEmptyDatafeed } from '../common/job_creator/util/default_configs'; import type { JobCreatorType } from '../common/job_creator'; -import { stashJobForCloning } from '../common/job_creator/util/general'; import { getJobsItemsFromEmbeddable } from './utils'; import type { CreateState } from '../job_from_dashboard'; import { QuickJobCreatorBase } from '../job_from_dashboard'; @@ -43,9 +43,10 @@ export class QuickGeoJobCreator extends QuickJobCreatorBase { kibanaConfig: IUiSettingsClient, timeFilter: TimefilterContract, dashboardService: DashboardStart, - mlApiServices: MlApiServices + mlApiServices: MlApiServices, + mlJobService: MlJobService ) { - super(dataViews, kibanaConfig, timeFilter, dashboardService, mlApiServices); + super(dataViews, kibanaConfig, timeFilter, dashboardService, mlApiServices, mlJobService); } public async createAndSaveGeoJob({ @@ -144,7 +145,7 @@ export class QuickGeoJobCreator extends QuickJobCreatorBase { // add job config and start and end dates to the // job cloning stash, so they can be used // by the new job wizards - stashJobForCloning( + this.mlJobService.stashJobForCloning( { jobConfig, datafeedConfig, diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/job_from_map/route_resolver.ts b/x-pack/plugins/ml/public/application/jobs/new_job/job_from_map/route_resolver.ts index c102f0ee6ddd9..455802fcc3f4a 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/job_from_map/route_resolver.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/job_from_map/route_resolver.ts @@ -10,6 +10,7 @@ import type { TimefilterContract } from '@kbn/data-plugin/public'; import type { DashboardStart } from '@kbn/dashboard-plugin/public'; import type { DataViewsContract } from '@kbn/data-views-plugin/public'; import type { MlApiServices } from '../../../services/ml_api_service'; +import type { MlJobService } from '../../../services/job_service'; import { QuickGeoJobCreator } from './quick_create_job'; import { getDefaultQuery, getRisonValue } from '../utils/new_job_utils'; @@ -20,6 +21,7 @@ interface Dependencies { timeFilter: TimefilterContract; dashboardService: DashboardStart; mlApiServices: MlApiServices; + mlJobService: MlJobService; } export async function resolver( deps: Dependencies, @@ -32,7 +34,8 @@ export async function resolver( toRisonString: string, layerRisonString?: string ) { - const { dataViews, kibanaConfig, timeFilter, dashboardService, mlApiServices } = deps; + const { dataViews, kibanaConfig, timeFilter, dashboardService, mlApiServices, mlJobService } = + deps; const defaultLayer = { query: getDefaultQuery(), filters: [] }; const dashboard = getRisonValue(dashboardRisonString, defaultLayer); @@ -55,7 +58,8 @@ export async function resolver( kibanaConfig, timeFilter, dashboardService, - mlApiServices + mlApiServices, + mlJobService ); await jobCreator.createAndStashGeoJob( diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/job_from_pattern_analysis/quick_create_job.ts b/x-pack/plugins/ml/public/application/jobs/new_job/job_from_pattern_analysis/quick_create_job.ts index 169ff6fdf5c89..0b81525f4013d 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/job_from_pattern_analysis/quick_create_job.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/job_from_pattern_analysis/quick_create_job.ts @@ -17,8 +17,8 @@ import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/type import { CREATED_BY_LABEL, DEFAULT_BUCKET_SPAN } from '../../../../../common/constants/new_job'; import { type CreateState, QuickJobCreatorBase } from '../job_from_dashboard/quick_create_job_base'; import type { MlApiServices } from '../../../services/ml_api_service'; +import type { MlJobService } from '../../../services/job_service'; import { createEmptyDatafeed, createEmptyJob } from '../common/job_creator/util/default_configs'; -import { stashJobForCloning } from '../common/job_creator/util/general'; import type { JobCreatorType } from '../common/job_creator'; export const CATEGORIZATION_TYPE = { @@ -36,9 +36,10 @@ export class QuickCategorizationJobCreator extends QuickJobCreatorBase { timeFilter: TimefilterContract, dashboardService: DashboardStart, private data: DataPublicPluginStart, - mlApiServices: MlApiServices + mlApiServices: MlApiServices, + mlJobService: MlJobService ) { - super(dataViews, kibanaConfig, timeFilter, dashboardService, mlApiServices); + super(dataViews, kibanaConfig, timeFilter, dashboardService, mlApiServices, mlJobService); } public async createAndSaveJob( @@ -118,7 +119,7 @@ export class QuickCategorizationJobCreator extends QuickJobCreatorBase { // add job config and start and end dates to the // job cloning stash, so they can be used // by the new job wizards - stashJobForCloning( + this.mlJobService.stashJobForCloning( { jobConfig, datafeedConfig, diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/job_from_pattern_analysis/route_resolver.ts b/x-pack/plugins/ml/public/application/jobs/new_job/job_from_pattern_analysis/route_resolver.ts index 69ca29f9a5ab4..a2277babcc195 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/job_from_pattern_analysis/route_resolver.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/job_from_pattern_analysis/route_resolver.ts @@ -15,6 +15,7 @@ import { CATEGORIZATION_TYPE, } from './quick_create_job'; import type { MlApiServices } from '../../../services/ml_api_service'; +import type { MlJobService } from '../../../services/job_service'; import { getDefaultDatafeedQuery, getRisonValue } from '../utils/new_job_utils'; @@ -24,6 +25,7 @@ interface Dependencies { dashboardService: DashboardStart; data: DataPublicPluginStart; mlApiServices: MlApiServices; + mlJobService: MlJobService; } export async function resolver( deps: Dependencies, @@ -36,7 +38,7 @@ export async function resolver( toRisonString: string, queryRisonString: string ) { - const { mlApiServices, timeFilter, kibanaConfig, dashboardService, data } = deps; + const { mlApiServices, mlJobService, timeFilter, kibanaConfig, dashboardService, data } = deps; const query = getRisonValue(queryRisonString, getDefaultDatafeedQuery()); const from = getRisonValue(fromRisonString, ''); @@ -57,7 +59,8 @@ export async function resolver( timeFilter, dashboardService, data, - mlApiServices + mlApiServices, + mlJobService ); await jobCreator.createAndStashADJob( categorizationType, diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/common/settings.ts b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/common/settings.ts index bea93b891466a..d521573f646ce 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/common/settings.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/common/settings.ts @@ -5,6 +5,7 @@ * 2.0. */ +import type { IUiSettingsClient } from '@kbn/core/public'; import type { TimeBuckets } from '@kbn/ml-time-buckets'; import { useCurrentThemeVars } from '../../../../../../contexts/kibana'; import type { JobCreatorType } from '../../../../common/job_creator'; @@ -57,7 +58,11 @@ export const seriesStyle = { }, }; -export function getChartSettings(jobCreator: JobCreatorType, chartInterval: TimeBuckets) { +export function getChartSettings( + uiSettings: IUiSettingsClient, + jobCreator: JobCreatorType, + chartInterval: TimeBuckets +) { const cs = { ...defaultChartSettings, intervalMs: chartInterval.getInterval().asMilliseconds(), @@ -68,7 +73,7 @@ export function getChartSettings(jobCreator: JobCreatorType, chartInterval: Time // the calculation from TimeBuckets, but without the // bar target and max bars which have been set for the // general chartInterval - const interval = getTimeBucketsFromCache(); + const interval = getTimeBucketsFromCache(uiSettings); interval.setInterval('auto'); interval.setBounds(chartInterval.getBounds()); cs.intervalMs = interval.getInterval().asMilliseconds(); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/data_view/change_data_view.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/data_view/change_data_view.tsx index 9fe90643c2f84..cb6a636a8c296 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/data_view/change_data_view.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/data_view/change_data_view.tsx @@ -27,6 +27,7 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { SavedObjectFinder } from '@kbn/saved-objects-finder-plugin/public'; import { extractErrorMessage } from '@kbn/ml-error-utils'; +import { useMlJobService } from '../../../../../../../services/job_service'; import { JobCreatorContext } from '../../../job_creator_context'; import type { AdvancedJobCreator } from '../../../../../common/job_creator'; import { resetAdvancedJob } from '../../../../../common/job_creator/util/general'; @@ -66,6 +67,7 @@ export const ChangeDataViewModal: FC = ({ onClose }) => { const { jobCreator: jc } = useContext(JobCreatorContext); const jobCreator = jc as AdvancedJobCreator; + const mlJobService = useMlJobService(); const [validating, setValidating] = useState(false); const [step, setStep] = useState(STEP.PICK_DATA_VIEW); @@ -123,7 +125,9 @@ export const ChangeDataViewModal: FC = ({ onClose }) => { const applyDataView = useCallback(() => { const newIndices = newDataViewTitle.split(','); jobCreator.indices = newIndices; - resetAdvancedJob(jobCreator, navigateToPath); + resetAdvancedJob(mlJobService, jobCreator, navigateToPath); + // exclude mlJobService from deps + // eslint-disable-next-line react-hooks/exhaustive-deps }, [jobCreator, newDataViewTitle, navigateToPath]); return ( diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/time_field/time_field.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/time_field/time_field.tsx index fec9b164c3e9d..bfbcbe76499de 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/time_field/time_field.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/time_field/time_field.tsx @@ -10,14 +10,14 @@ import React, { useContext, useEffect, useState } from 'react'; import { TimeFieldSelect } from './time_field_select'; import { JobCreatorContext } from '../../../job_creator_context'; -import { newJobCapsService } from '../../../../../../../services/new_job_capabilities/new_job_capabilities_service'; +import { useNewJobCapsService } from '../../../../../../../services/new_job_capabilities/new_job_capabilities_service'; import type { AdvancedJobCreator } from '../../../../../common/job_creator'; import { Description } from './description'; export const TimeField: FC = () => { const { jobCreator: jc, jobCreatorUpdate, jobCreatorUpdated } = useContext(JobCreatorContext); const jobCreator = jc as AdvancedJobCreator; - const { dateFields } = newJobCapsService; + const { dateFields } = useNewJobCapsService(); const [timeFieldName, setTimeFieldName] = useState(jobCreator.timeFieldName); useEffect(() => { diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/additional_section/components/calendars/calendars_selection.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/additional_section/components/calendars/calendars_selection.tsx index 3949339570242..bd3b52f5bb003 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/additional_section/components/calendars/calendars_selection.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/additional_section/components/calendars/calendars_selection.tsx @@ -22,10 +22,9 @@ import { import { i18n } from '@kbn/i18n'; import { JobCreatorContext } from '../../../../../job_creator_context'; import { Description } from './description'; -import { ml } from '../../../../../../../../../services/ml_api_service'; import { PLUGIN_ID } from '../../../../../../../../../../../common/constants/app'; import type { Calendar } from '../../../../../../../../../../../common/types/calendars'; -import { useMlKibana } from '../../../../../../../../../contexts/kibana'; +import { useMlApiContext, useMlKibana } from '../../../../../../../../../contexts/kibana'; import { GLOBAL_CALENDAR } from '../../../../../../../../../../../common/constants/calendars'; import { ML_PAGES } from '../../../../../../../../../../../common/constants/locator'; @@ -35,6 +34,7 @@ export const CalendarsSelection: FC = () => { application: { getUrlForApp }, }, } = useMlKibana(); + const ml = useMlApiContext(); const { jobCreator, jobCreatorUpdate } = useContext(JobCreatorContext); const [selectedCalendars, setSelectedCalendars] = useState(jobCreator.calendars); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/metric_selection.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/metric_selection.tsx index 925de492084fb..88e360f837de8 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/metric_selection.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/metric_selection.tsx @@ -11,7 +11,7 @@ import React, { Fragment, useContext, useState, useEffect } from 'react'; import type { Aggregation, Field } from '@kbn/ml-anomaly-utils'; import { JobCreatorContext } from '../../../job_creator_context'; import type { AdvancedJobCreator } from '../../../../../common/job_creator'; -import { newJobCapsService } from '../../../../../../../services/new_job_capabilities/new_job_capabilities_service'; +import { useNewJobCapsService } from '../../../../../../../services/new_job_capabilities/new_job_capabilities_service'; import { MetricSelector } from './metric_selector'; import type { RichDetector } from '../../../../../common/job_creator/advanced_job_creator'; import { DetectorList } from './detector_list'; @@ -37,7 +37,7 @@ export const AdvancedDetectors: FC = ({ setIsValid }) => { const { jobCreator: jc, jobCreatorUpdate, jobCreatorUpdated } = useContext(JobCreatorContext); const jobCreator = jc as AdvancedJobCreator; - const { fields, aggs } = newJobCapsService; + const { fields, aggs } = useNewJobCapsService(); const [modalPayload, setModalPayload] = useState(null); useEffect(() => { diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/bucket_span_estimator/estimate_bucket_span.ts b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/bucket_span_estimator/estimate_bucket_span.ts index 9caa4c89510a2..de279236d8755 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/bucket_span_estimator/estimate_bucket_span.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/bucket_span_estimator/estimate_bucket_span.ts @@ -8,6 +8,7 @@ import { useContext, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { EVENT_RATE_FIELD_ID } from '@kbn/ml-anomaly-utils'; +import { useMlApiContext } from '../../../../../../../contexts/kibana'; import { JobCreatorContext } from '../../../job_creator_context'; import type { BucketSpanEstimatorData } from '../../../../../../../../../common/types/job_service'; import { @@ -16,9 +17,8 @@ import { isAdvancedJobCreator, isRareJobCreator, } from '../../../../../common/job_creator'; -import { ml } from '../../../../../../../services/ml_api_service'; import { useDataSource } from '../../../../../../../contexts/ml'; -import { getToastNotificationService } from '../../../../../../../services/toast_notification_service'; +import { useToastNotificationService } from '../../../../../../../services/toast_notification_service'; export enum ESTIMATE_STATUS { NOT_RUNNING, @@ -26,6 +26,8 @@ export enum ESTIMATE_STATUS { } export function useEstimateBucketSpan() { + const toastNotificationService = useToastNotificationService(); + const ml = useMlApiContext(); const { jobCreator, jobCreatorUpdate } = useContext(JobCreatorContext); const dataSourceContext = useDataSource(); @@ -85,7 +87,7 @@ export function useEstimateBucketSpan() { defaultMessage: 'Bucket span could not be estimated', } ); - getToastNotificationService().displayWarningToast({ title, text }); + toastNotificationService.displayWarningToast({ title, text }); } else { jobCreator.bucketSpan = name; jobCreatorUpdate(); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/by_field/by_field.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/by_field/by_field.tsx index 038a7433a54ee..f52bf8a5dbe3f 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/by_field/by_field.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/by_field/by_field.tsx @@ -13,7 +13,7 @@ import type { Field } from '@kbn/ml-anomaly-utils'; import { SplitFieldSelect } from '../split_field_select'; import { JobCreatorContext } from '../../../job_creator_context'; import { filterCategoryFields } from '../../../../../../../../../common/util/fields_utils'; -import { newJobCapsService } from '../../../../../../../services/new_job_capabilities/new_job_capabilities_service'; +import { useNewJobCapsService } from '../../../../../../../services/new_job_capabilities/new_job_capabilities_service'; import type { PopulationJobCreator } from '../../../../../common/job_creator'; interface Props { @@ -23,6 +23,7 @@ interface Props { export const ByFieldSelector: FC = ({ detectorIndex }) => { const { jobCreator: jc, jobCreatorUpdate, jobCreatorUpdated } = useContext(JobCreatorContext); const jobCreator = jc as PopulationJobCreator; + const newJobCapsService = useNewJobCapsService(); // eslint-disable-next-line react-hooks/exhaustive-deps const runtimeCategoryFields = useMemo(() => filterCategoryFields(jobCreator.runtimeFields), []); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_field/categorization_field.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_field/categorization_field.tsx index f6331d7e55e66..706249cb88b37 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_field/categorization_field.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_field/categorization_field.tsx @@ -10,7 +10,7 @@ import React, { useContext, useEffect, useState } from 'react'; import { CategorizationFieldSelect } from './categorization_field_select'; import { JobCreatorContext } from '../../../job_creator_context'; -import { newJobCapsService } from '../../../../../../../services/new_job_capabilities/new_job_capabilities_service'; +import { useNewJobCapsService } from '../../../../../../../services/new_job_capabilities/new_job_capabilities_service'; import type { AdvancedJobCreator, CategorizationJobCreator, @@ -21,7 +21,7 @@ import { Description } from './description'; export const CategorizationField: FC = () => { const { jobCreator: jc, jobCreatorUpdate, jobCreatorUpdated } = useContext(JobCreatorContext); const jobCreator = jc as AdvancedJobCreator | CategorizationJobCreator; - const { catFields } = newJobCapsService; + const { catFields } = useNewJobCapsService(); const [categorizationFieldName, setCategorizationFieldName] = useState( jobCreator.categorizationFieldName ); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_partition_field/categorization_per_partition_dropdown.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_partition_field/categorization_per_partition_dropdown.tsx index 3435472ef8548..96e3249ede655 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_partition_field/categorization_per_partition_dropdown.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_partition_field/categorization_per_partition_dropdown.tsx @@ -11,7 +11,7 @@ import { EuiFormRow } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { JobCreatorContext } from '../../../job_creator_context'; import type { CategorizationJobCreator } from '../../../../../common/job_creator'; -import { newJobCapsService } from '../../../../../../../services/new_job_capabilities/new_job_capabilities_service'; +import { useNewJobCapsService } from '../../../../../../../services/new_job_capabilities/new_job_capabilities_service'; import { CategorizationPerPartitionFieldSelect } from './categorization_per_partition_input'; export const CategorizationPerPartitionFieldDropdown = ({ @@ -25,7 +25,7 @@ export const CategorizationPerPartitionFieldDropdown = ({ const [categorizationPartitionFieldName, setCategorizationPartitionFieldName] = useState< string | null >(jobCreator.categorizationPerPartitionField); - const { categoryFields } = newJobCapsService; + const { categoryFields } = useNewJobCapsService(); const filteredCategories = useMemo( () => categoryFields.filter((c) => c.id !== jobCreator.categorizationFieldName), diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/category_stopped_partitions.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/category_stopped_partitions.tsx index 30c93bae2f440..912bc1af40e12 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/category_stopped_partitions.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/category_stopped_partitions.tsx @@ -13,12 +13,13 @@ import { i18n } from '@kbn/i18n'; import { from } from 'rxjs'; import { switchMap, takeWhile, tap } from 'rxjs'; import { extractErrorProperties } from '@kbn/ml-error-utils'; +import { useMlApiContext } from '../../../../../../../contexts/kibana'; import { JobCreatorContext } from '../../../job_creator_context'; import type { CategorizationJobCreator } from '../../../../../common/job_creator'; -import { ml } from '../../../../../../../services/ml_api_service'; const NUMBER_OF_PREVIEW = 5; export const CategoryStoppedPartitions: FC = () => { + const ml = useMlApiContext(); const { jobCreator: jc, resultsLoader } = useContext(JobCreatorContext); const jobCreator = jc as CategorizationJobCreator; const [tableRow, setTableRow] = useState>([]); @@ -62,6 +63,8 @@ export const CategoryStoppedPartitions: FC = () => { setStoppedPartitionsError(error.message); } } + // skipping the ml service from deps + // eslint-disable-next-line react-hooks/exhaustive-deps }, [jobCreator.jobId]); useEffect(() => { diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/metric_selection.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/metric_selection.tsx index 5d5532069005e..5f25c8c718303 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/metric_selection.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/metric_selection.tsx @@ -13,7 +13,7 @@ import { type CategoryFieldExample, type FieldExampleCheck, } from '@kbn/ml-category-validator'; -import { getToastNotificationService } from '../../../../../../../services/toast_notification_service'; +import { useToastNotificationService } from '../../../../../../../services/toast_notification_service'; import { JobCreatorContext } from '../../../job_creator_context'; import type { CategorizationJobCreator } from '../../../../../common/job_creator'; @@ -33,6 +33,7 @@ interface Props { export const CategorizationDetectors: FC = ({ setIsValid }) => { const { jobCreator: jc, jobCreatorUpdate, jobCreatorUpdated } = useContext(JobCreatorContext); const jobCreator = jc as CategorizationJobCreator; + const toastNotificationService = useToastNotificationService(); const [loadingData, setLoadingData] = useState(false); const [ccsVersionFailure, setCcsVersionFailure] = useState(false); @@ -102,7 +103,7 @@ export const CategorizationDetectors: FC = ({ setIsValid }) => { setFieldExamples(null); setValidationChecks([]); setOverallValidStatus(CATEGORY_EXAMPLES_VALIDATION_STATUS.INVALID); - getToastNotificationService().displayErrorToast(error); + toastNotificationService.displayErrorToast(error); setCcsVersionFailure(false); } } else { diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/top_categories.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/top_categories.tsx index 2b86c166ef36a..1a9db2f5c67fb 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/top_categories.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/top_categories.tsx @@ -10,14 +10,16 @@ import React, { useContext, useEffect, useState } from 'react'; import { EuiBasicTable, EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { extractErrorProperties } from '@kbn/ml-error-utils'; +import { useMlApiContext } from '../../../../../../../contexts/kibana'; import { NUMBER_OF_CATEGORY_EXAMPLES } from '../../../../../../../../../common/constants/new_job'; import { JobCreatorContext } from '../../../job_creator_context'; import type { CategorizationJobCreator } from '../../../../../common/job_creator'; import type { Results } from '../../../../../common/results_loader'; -import { ml } from '../../../../../../../services/ml_api_service'; import { useToastNotificationService } from '../../../../../../../services/toast_notification_service'; export const TopCategories: FC = () => { + const ml = useMlApiContext(); + const { displayErrorToast } = useToastNotificationService(); const { jobCreator: jc, resultsLoader } = useContext(JobCreatorContext); const jobCreator = jc as CategorizationJobCreator; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/geo_field/geo_field.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/geo_field/geo_field.tsx index 307493585ae3a..be8767d010f4c 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/geo_field/geo_field.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/geo_field/geo_field.tsx @@ -10,14 +10,14 @@ import React, { useContext, useEffect, useState } from 'react'; import { GeoFieldSelect } from './geo_field_select'; import { JobCreatorContext } from '../../../job_creator_context'; -import { newJobCapsService } from '../../../../../../../services/new_job_capabilities/new_job_capabilities_service'; +import { useNewJobCapsService } from '../../../../../../../services/new_job_capabilities/new_job_capabilities_service'; import type { GeoJobCreator } from '../../../../../common/job_creator'; import { Description } from './description'; export const GeoField: FC = () => { const { jobCreator: jc, jobCreatorUpdate, jobCreatorUpdated } = useContext(JobCreatorContext); const jobCreator = jc as GeoJobCreator; - const { geoFields } = newJobCapsService; + const { geoFields } = useNewJobCapsService(); const [geoField, setGeoField] = useState(jobCreator.geoField); useEffect(() => { diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/influencers/influencers.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/influencers/influencers.tsx index 482870434a522..3026cd7484675 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/influencers/influencers.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/influencers/influencers.tsx @@ -10,7 +10,7 @@ import React, { useContext, useEffect, useState } from 'react'; import { InfluencersSelect } from './influencers_select'; import { JobCreatorContext } from '../../../job_creator_context'; -import { newJobCapsService } from '../../../../../../../services/new_job_capabilities/new_job_capabilities_service'; +import { useNewJobCapsService } from '../../../../../../../services/new_job_capabilities/new_job_capabilities_service'; import type { MultiMetricJobCreator, PopulationJobCreator, @@ -21,7 +21,7 @@ import { Description } from './description'; export const Influencers: FC = () => { const { jobCreator: jc, jobCreatorUpdate, jobCreatorUpdated } = useContext(JobCreatorContext); const jobCreator = jc as MultiMetricJobCreator | PopulationJobCreator | AdvancedJobCreator; - const { fields } = newJobCapsService; + const { fields } = useNewJobCapsService(); const [influencers, setInfluencers] = useState([...jobCreator.influencers]); useEffect(() => { diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/metric_selector/index.ts b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/metric_selector/index.ts new file mode 100644 index 0000000000000..a046f9c4ade2b --- /dev/null +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/metric_selector/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 { MetricSelector } from './metric_selector'; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/metric_selector.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/metric_selector/metric_selector.tsx similarity index 97% rename from x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/metric_selector.tsx rename to x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/metric_selector/metric_selector.tsx index 450e6ea99ea9d..601d94065282a 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/metric_selector.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/metric_selector/metric_selector.tsx @@ -34,7 +34,7 @@ export const MetricSelector: FC = ({ diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/metric_selection.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/metric_selection.tsx index cc8801181db7e..b187447f8b81c 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/metric_selection.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/metric_selection.tsx @@ -8,22 +8,25 @@ import type { FC } from 'react'; import React, { Fragment, useContext, useEffect, useState, useMemo } from 'react'; import type { AggFieldPair } from '@kbn/ml-anomaly-utils'; + +import { useUiSettings } from '../../../../../../../contexts/kibana'; import { JobCreatorContext } from '../../../job_creator_context'; import type { MultiMetricJobCreator } from '../../../../../common/job_creator'; import type { LineChartData } from '../../../../../common/chart_loader'; import type { DropDownLabel, DropDownProps } from '../agg_select'; -import { newJobCapsService } from '../../../../../../../services/new_job_capabilities/new_job_capabilities_service'; +import { useNewJobCapsService } from '../../../../../../../services/new_job_capabilities/new_job_capabilities_service'; import { sortFields } from '../../../../../../../../../common/util/fields_utils'; import { getChartSettings, defaultChartSettings } from '../../../charts/common/settings'; -import { MetricSelector } from './metric_selector'; +import { MetricSelector } from '../metric_selector'; import { ChartGrid } from './chart_grid'; -import { getToastNotificationService } from '../../../../../../../services/toast_notification_service'; +import { useToastNotificationService } from '../../../../../../../services/toast_notification_service'; interface Props { setIsValid: (na: boolean) => void; } export const MultiMetricDetectors: FC = ({ setIsValid }) => { + const uiSettings = useUiSettings(); const { jobCreator: jc, jobCreatorUpdate, @@ -31,8 +34,9 @@ export const MultiMetricDetectors: FC = ({ setIsValid }) => { chartLoader, chartInterval, } = useContext(JobCreatorContext); - const jobCreator = jc as MultiMetricJobCreator; + const toastNotificationService = useToastNotificationService(); + const newJobCapsService = useNewJobCapsService(); const fields = useMemo( () => sortFields([...newJobCapsService.fields, ...jobCreator.runtimeFields]), @@ -121,7 +125,7 @@ export const MultiMetricDetectors: FC = ({ setIsValid }) => { ) .then(setFieldValues) .catch((error) => { - getToastNotificationService().displayErrorToast(error); + toastNotificationService.displayErrorToast(error); }); } else { setFieldValues([]); @@ -140,7 +144,7 @@ export const MultiMetricDetectors: FC = ({ setIsValid }) => { if (allDataReady()) { setLoadingData(true); try { - const cs = getChartSettings(jobCreator, chartInterval); + const cs = getChartSettings(uiSettings, jobCreator, chartInterval); setChartSettings(cs); const resp: LineChartData = await chartLoader.loadLineCharts( jobCreator.start, @@ -154,7 +158,7 @@ export const MultiMetricDetectors: FC = ({ setIsValid }) => { ); setLineChartsData(resp); } catch (error) { - getToastNotificationService().displayErrorToast(error); + toastNotificationService.displayErrorToast(error); setLineChartsData([]); } setLoadingData(false); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/metric_selection_summary.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/metric_selection_summary.tsx index 4c3e624757f90..281163ef0fa24 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/metric_selection_summary.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/metric_selection_summary.tsx @@ -8,23 +8,25 @@ import type { FC } from 'react'; import React, { Fragment, useContext, useEffect, useState } from 'react'; +import { useUiSettings } from '../../../../../../../contexts/kibana'; import { JobCreatorContext } from '../../../job_creator_context'; import type { MultiMetricJobCreator } from '../../../../../common/job_creator'; import type { Results, ModelItem, Anomaly } from '../../../../../common/results_loader'; import type { LineChartData } from '../../../../../common/chart_loader'; import { getChartSettings, defaultChartSettings } from '../../../charts/common/settings'; import { ChartGrid } from './chart_grid'; -import { getToastNotificationService } from '../../../../../../../services/toast_notification_service'; +import { useToastNotificationService } from '../../../../../../../services/toast_notification_service'; export const MultiMetricDetectorsSummary: FC = () => { + const uiSettings = useUiSettings(); const { jobCreator: jc, chartLoader, resultsLoader, chartInterval, } = useContext(JobCreatorContext); - const jobCreator = jc as MultiMetricJobCreator; + const toastNotificationService = useToastNotificationService(); const [lineChartsData, setLineChartsData] = useState({}); const [loadingData, setLoadingData] = useState(false); @@ -52,7 +54,7 @@ export const MultiMetricDetectorsSummary: FC = () => { ); setFieldValues(tempFieldValues); } catch (error) { - getToastNotificationService().displayErrorToast(error); + toastNotificationService.displayErrorToast(error); } } })(); @@ -60,6 +62,8 @@ export const MultiMetricDetectorsSummary: FC = () => { return () => { subscription.unsubscribe(); }; + // skip toastNotificationService from deps + // eslint-disable-next-line react-hooks/exhaustive-deps }, [chartLoader, resultsLoader, jobCreator]); useEffect(() => { @@ -73,7 +77,7 @@ export const MultiMetricDetectorsSummary: FC = () => { if (allDataReady()) { setLoadingData(true); try { - const cs = getChartSettings(jobCreator, chartInterval); + const cs = getChartSettings(uiSettings, jobCreator, chartInterval); setChartSettings(cs); const resp: LineChartData = await chartLoader.loadLineCharts( jobCreator.start, @@ -87,7 +91,7 @@ export const MultiMetricDetectorsSummary: FC = () => { ); setLineChartsData(resp); } catch (error) { - getToastNotificationService().displayErrorToast(error); + toastNotificationService.displayErrorToast(error); setLineChartsData({}); } setLoadingData(false); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/metric_selector.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/metric_selector.tsx deleted file mode 100644 index 8d193c38bdf11..0000000000000 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/metric_selector.tsx +++ /dev/null @@ -1,51 +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 type { FC } from 'react'; -import React from 'react'; -import { i18n } from '@kbn/i18n'; -import { EuiFormRow, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import type { Field, AggFieldPair } from '@kbn/ml-anomaly-utils'; -import type { DropDownLabel, DropDownProps } from '../agg_select'; -import { AggSelect } from '../agg_select'; - -interface Props { - fields: Field[]; - detectorChangeHandler: (options: DropDownLabel[]) => void; - selectedOptions: DropDownProps; - maxWidth?: number; - removeOptions: AggFieldPair[]; -} - -const MAX_WIDTH = 560; - -export const MetricSelector: FC = ({ - fields, - detectorChangeHandler, - selectedOptions, - maxWidth, - removeOptions, -}) => { - return ( - - - - - - - - ); -}; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_field/population_field.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_field/population_field.tsx index 4e19acb99914f..02b6130bb49e3 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_field/population_field.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_field/population_field.tsx @@ -12,7 +12,7 @@ import type { Field } from '@kbn/ml-anomaly-utils'; import { SplitFieldSelect } from '../split_field_select'; import { JobCreatorContext } from '../../../job_creator_context'; import { filterCategoryFields } from '../../../../../../../../../common/util/fields_utils'; -import { newJobCapsService } from '../../../../../../../services/new_job_capabilities/new_job_capabilities_service'; +import { useNewJobCapsService } from '../../../../../../../services/new_job_capabilities/new_job_capabilities_service'; import { Description } from './description'; import type { PopulationJobCreator, RareJobCreator } from '../../../../../common/job_creator'; import { isPopulationJobCreator } from '../../../../../common/job_creator'; @@ -20,6 +20,7 @@ import { isPopulationJobCreator } from '../../../../../common/job_creator'; export const PopulationFieldSelector: FC = () => { const { jobCreator: jc, jobCreatorUpdate, jobCreatorUpdated } = useContext(JobCreatorContext); const jobCreator = jc as PopulationJobCreator | RareJobCreator; + const newJobCapsService = useNewJobCapsService(); // eslint-disable-next-line react-hooks/exhaustive-deps const runtimeCategoryFields = useMemo(() => filterCategoryFields(jobCreator.runtimeFields), []); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/metric_selection.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/metric_selection.tsx index cca8f5af98342..81dd83b9e157c 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/metric_selection.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/metric_selection.tsx @@ -9,17 +9,19 @@ import type { FC } from 'react'; import React, { Fragment, useContext, useEffect, useState, useReducer, useMemo } from 'react'; import { EuiHorizontalRule } from '@elastic/eui'; import type { Field, AggFieldPair } from '@kbn/ml-anomaly-utils'; + +import { useUiSettings } from '../../../../../../../contexts/kibana'; import { JobCreatorContext } from '../../../job_creator_context'; import type { PopulationJobCreator } from '../../../../../common/job_creator'; import type { LineChartData } from '../../../../../common/chart_loader'; import type { DropDownLabel, DropDownProps } from '../agg_select'; -import { newJobCapsService } from '../../../../../../../services/new_job_capabilities/new_job_capabilities_service'; +import { useNewJobCapsService } from '../../../../../../../services/new_job_capabilities/new_job_capabilities_service'; import { sortFields } from '../../../../../../../../../common/util/fields_utils'; import { getChartSettings, defaultChartSettings } from '../../../charts/common/settings'; -import { MetricSelector } from './metric_selector'; +import { MetricSelector } from '../metric_selector'; import { PopulationFieldSelector } from '../population_field'; import { ChartGrid } from './chart_grid'; -import { getToastNotificationService } from '../../../../../../../services/toast_notification_service'; +import { useToastNotificationService } from '../../../../../../../services/toast_notification_service'; interface Props { setIsValid: (na: boolean) => void; @@ -28,6 +30,7 @@ interface Props { type DetectorFieldValues = Record; export const PopulationDetectors: FC = ({ setIsValid }) => { + const uiSettings = useUiSettings(); const { jobCreator: jc, jobCreatorUpdate, @@ -36,6 +39,8 @@ export const PopulationDetectors: FC = ({ setIsValid }) => { chartInterval, } = useContext(JobCreatorContext); const jobCreator = jc as PopulationJobCreator; + const toastNotificationService = useToastNotificationService(); + const newJobCapsService = useNewJobCapsService(); const fields = useMemo( () => sortFields([...newJobCapsService.fields, ...jobCreator.runtimeFields]), @@ -157,7 +162,7 @@ export const PopulationDetectors: FC = ({ setIsValid }) => { if (allDataReady()) { setLoadingData(true); try { - const cs = getChartSettings(jobCreator, chartInterval); + const cs = getChartSettings(uiSettings, jobCreator, chartInterval); setChartSettings(cs); const resp: LineChartData = await chartLoader.loadPopulationCharts( jobCreator.start, @@ -171,7 +176,7 @@ export const PopulationDetectors: FC = ({ setIsValid }) => { setLineChartsData(resp); } catch (error) { - getToastNotificationService().displayErrorToast(error); + toastNotificationService.displayErrorToast(error); setLineChartsData([]); } setLoadingData(false); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/metric_selection_summary.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/metric_selection_summary.tsx index 3b52b455208a0..907bbc48f2aab 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/metric_selection_summary.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/metric_selection_summary.tsx @@ -9,19 +9,21 @@ import type { FC } from 'react'; import React, { Fragment, useContext, useEffect, useState } from 'react'; import { EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; - import type { Field, AggFieldPair } from '@kbn/ml-anomaly-utils'; + +import { useUiSettings } from '../../../../../../../contexts/kibana'; import { JobCreatorContext } from '../../../job_creator_context'; import type { PopulationJobCreator } from '../../../../../common/job_creator'; import type { Results, ModelItem, Anomaly } from '../../../../../common/results_loader'; import type { LineChartData } from '../../../../../common/chart_loader'; import { getChartSettings, defaultChartSettings } from '../../../charts/common/settings'; import { ChartGrid } from './chart_grid'; -import { getToastNotificationService } from '../../../../../../../services/toast_notification_service'; +import { useToastNotificationService } from '../../../../../../../services/toast_notification_service'; type DetectorFieldValues = Record; export const PopulationDetectorsSummary: FC = () => { + const uiSettings = useUiSettings(); const { jobCreator: jc, chartLoader, @@ -29,6 +31,7 @@ export const PopulationDetectorsSummary: FC = () => { chartInterval, } = useContext(JobCreatorContext); const jobCreator = jc as PopulationJobCreator; + const toastNotificationService = useToastNotificationService(); const [aggFieldPairList, setAggFieldPairList] = useState( jobCreator.aggFieldPairs @@ -77,7 +80,7 @@ export const PopulationDetectorsSummary: FC = () => { if (allDataReady()) { setLoadingData(true); try { - const cs = getChartSettings(jobCreator, chartInterval); + const cs = getChartSettings(uiSettings, jobCreator, chartInterval); setChartSettings(cs); const resp: LineChartData = await chartLoader.loadPopulationCharts( jobCreator.start, @@ -91,7 +94,7 @@ export const PopulationDetectorsSummary: FC = () => { setLineChartsData(resp); } catch (error) { - getToastNotificationService().displayErrorToast(error); + toastNotificationService.displayErrorToast(error); setLineChartsData({}); } setLoadingData(false); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/rare_field/rare_field.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/rare_field/rare_field.tsx index ecb88faecfe33..31c70d0d9c22d 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/rare_field/rare_field.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/rare_field/rare_field.tsx @@ -12,13 +12,14 @@ import type { Field } from '@kbn/ml-anomaly-utils'; import { RareFieldSelect } from './rare_field_select'; import { JobCreatorContext } from '../../../job_creator_context'; import { filterCategoryFields } from '../../../../../../../../../common/util/fields_utils'; -import { newJobCapsService } from '../../../../../../../services/new_job_capabilities/new_job_capabilities_service'; +import { useNewJobCapsService } from '../../../../../../../services/new_job_capabilities/new_job_capabilities_service'; import { Description } from './description'; import type { RareJobCreator } from '../../../../../common/job_creator'; export const RareFieldSelector: FC = () => { const { jobCreator: jc, jobCreatorUpdate, jobCreatorUpdated } = useContext(JobCreatorContext); const jobCreator = jc as RareJobCreator; + const newJobCapsService = useNewJobCapsService(); // eslint-disable-next-line react-hooks/exhaustive-deps const runtimeCategoryFields = useMemo(() => filterCategoryFields(jobCreator.runtimeFields), []); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/metric_selection.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/metric_selection.tsx index 39cb00dd3b8c3..e37b1722e9bc2 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/metric_selection.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/metric_selection.tsx @@ -8,16 +8,18 @@ import type { FC } from 'react'; import React, { Fragment, useContext, useEffect, useState, useMemo } from 'react'; import type { AggFieldPair } from '@kbn/ml-anomaly-utils'; + +import { useUiSettings } from '../../../../../../../contexts/kibana'; import { JobCreatorContext } from '../../../job_creator_context'; import type { SingleMetricJobCreator } from '../../../../../common/job_creator'; import type { LineChartData } from '../../../../../common/chart_loader'; import type { DropDownLabel, DropDownProps } from '../agg_select'; import { AggSelect, createLabel } from '../agg_select'; -import { newJobCapsService } from '../../../../../../../services/new_job_capabilities/new_job_capabilities_service'; +import { useNewJobCapsService } from '../../../../../../../services/new_job_capabilities/new_job_capabilities_service'; import { sortFields } from '../../../../../../../../../common/util/fields_utils'; import { AnomalyChart, CHART_TYPE } from '../../../charts/anomaly_chart'; import { getChartSettings } from '../../../charts/common/settings'; -import { getToastNotificationService } from '../../../../../../../services/toast_notification_service'; +import { useToastNotificationService } from '../../../../../../../services/toast_notification_service'; interface Props { setIsValid: (na: boolean) => void; @@ -26,6 +28,7 @@ interface Props { const DTR_IDX = 0; export const SingleMetricDetectors: FC = ({ setIsValid }) => { + const uiSettings = useUiSettings(); const { jobCreator: jc, jobCreatorUpdate, @@ -34,6 +37,8 @@ export const SingleMetricDetectors: FC = ({ setIsValid }) => { chartInterval, } = useContext(JobCreatorContext); const jobCreator = jc as SingleMetricJobCreator; + const toastNotificationService = useToastNotificationService(); + const newJobCapsService = useNewJobCapsService(); const fields = useMemo( () => sortFields([...newJobCapsService.fields, ...jobCreator.runtimeFields]), @@ -90,7 +95,7 @@ export const SingleMetricDetectors: FC = ({ setIsValid }) => { if (aggFieldPair !== null) { setLoadingData(true); try { - const cs = getChartSettings(jobCreator, chartInterval); + const cs = getChartSettings(uiSettings, jobCreator, chartInterval); const resp: LineChartData = await chartLoader.loadLineCharts( jobCreator.start, jobCreator.end, @@ -105,7 +110,7 @@ export const SingleMetricDetectors: FC = ({ setIsValid }) => { setLineChartData(resp); } } catch (error) { - getToastNotificationService().displayErrorToast(error); + toastNotificationService.displayErrorToast(error); setLineChartData({}); } setLoadingData(false); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/metric_selection_summary.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/metric_selection_summary.tsx index eddc3d7f14886..bc65018cbffa8 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/metric_selection_summary.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/metric_selection_summary.tsx @@ -7,17 +7,19 @@ import type { FC } from 'react'; import React, { Fragment, useContext, useEffect, useState } from 'react'; +import { useUiSettings } from '../../../../../../../contexts/kibana'; import { JobCreatorContext } from '../../../job_creator_context'; import type { SingleMetricJobCreator } from '../../../../../common/job_creator'; import type { Results, ModelItem, Anomaly } from '../../../../../common/results_loader'; import type { LineChartData } from '../../../../../common/chart_loader'; import { AnomalyChart, CHART_TYPE } from '../../../charts/anomaly_chart'; import { getChartSettings } from '../../../charts/common/settings'; -import { getToastNotificationService } from '../../../../../../../services/toast_notification_service'; +import { useToastNotificationService } from '../../../../../../../services/toast_notification_service'; const DTR_IDX = 0; export const SingleMetricDetectorsSummary: FC = () => { + const uiSettings = useUiSettings(); const { jobCreator: jc, chartLoader, @@ -25,6 +27,7 @@ export const SingleMetricDetectorsSummary: FC = () => { chartInterval, } = useContext(JobCreatorContext); const jobCreator = jc as SingleMetricJobCreator; + const toastNotificationService = useToastNotificationService(); const [lineChartsData, setLineChartData] = useState({}); const [loadingData, setLoadingData] = useState(false); @@ -56,7 +59,7 @@ export const SingleMetricDetectorsSummary: FC = () => { if (jobCreator.aggFieldPair !== null) { setLoadingData(true); try { - const cs = getChartSettings(jobCreator, chartInterval); + const cs = getChartSettings(uiSettings, jobCreator, chartInterval); const resp: LineChartData = await chartLoader.loadLineCharts( jobCreator.start, jobCreator.end, @@ -71,7 +74,7 @@ export const SingleMetricDetectorsSummary: FC = () => { setLineChartData(resp); } } catch (error) { - getToastNotificationService().displayErrorToast(error); + toastNotificationService.displayErrorToast(error); setLineChartData({}); } setLoadingData(false); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/settings.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/settings.tsx index f8181709ad408..a68832ae3aea3 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/settings.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/settings.tsx @@ -10,6 +10,7 @@ import React, { Fragment, useContext } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty } from '@elastic/eui'; +import { useMlJobService } from '../../../../../../../services/job_service'; import { useNavigateToPath } from '../../../../../../../contexts/kibana'; import { convertToMultiMetricJob } from '../../../../../common/job_creator/util/general'; @@ -25,10 +26,11 @@ interface Props { export const SingleMetricSettings: FC = ({ setIsValid }) => { const { jobCreator } = useContext(JobCreatorContext); + const mlJobService = useMlJobService(); const navigateToPath = useNavigateToPath(); const convertToMultiMetric = () => { - convertToMultiMetricJob(jobCreator, navigateToPath); + convertToMultiMetricJob(mlJobService, jobCreator, navigateToPath); }; return ( diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_field/split_field.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_field/split_field.tsx index b096798538da2..d98ad4256844d 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_field/split_field.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_field/split_field.tsx @@ -12,7 +12,7 @@ import type { Field } from '@kbn/ml-anomaly-utils'; import { SplitFieldSelect } from '../split_field_select'; import { JobCreatorContext } from '../../../job_creator_context'; import { filterCategoryFields } from '../../../../../../../../../common/util/fields_utils'; -import { newJobCapsService } from '../../../../../../../services/new_job_capabilities/new_job_capabilities_service'; +import { useNewJobCapsService } from '../../../../../../../services/new_job_capabilities/new_job_capabilities_service'; import { Description } from './description'; import type { MultiMetricJobCreator, RareJobCreator } from '../../../../../common/job_creator'; import { isMultiMetricJobCreator } from '../../../../../common/job_creator'; @@ -20,6 +20,7 @@ import { isMultiMetricJobCreator } from '../../../../../common/job_creator'; export const SplitFieldSelector: FC = () => { const { jobCreator: jc, jobCreatorUpdate, jobCreatorUpdated } = useContext(JobCreatorContext); const jobCreator = jc as MultiMetricJobCreator | RareJobCreator; + const newJobCapsService = useNewJobCapsService(); // eslint-disable-next-line react-hooks/exhaustive-deps const runtimeCategoryFields = useMemo(() => filterCategoryFields(jobCreator.runtimeFields), []); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/summary_count_field.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/summary_count_field.tsx index 106a437ba9089..5f5465745a1c5 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/summary_count_field.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/summary_count_field.tsx @@ -10,7 +10,7 @@ import React, { useContext, useEffect, useState } from 'react'; import { SummaryCountFieldSelect } from './summary_count_field_select'; import { JobCreatorContext } from '../../../job_creator_context'; -import { newJobCapsService } from '../../../../../../../services/new_job_capabilities/new_job_capabilities_service'; +import { useNewJobCapsService } from '../../../../../../../services/new_job_capabilities/new_job_capabilities_service'; import type { MultiMetricJobCreator, PopulationJobCreator, @@ -28,7 +28,7 @@ export const SummaryCountField: FC = () => { } = useContext(JobCreatorContext); const jobCreator = jc as MultiMetricJobCreator | PopulationJobCreator | AdvancedJobCreator; - const { fields } = newJobCapsService; + const { fields } = useNewJobCapsService(); const [summaryCountFieldName, setSummaryCountFieldName] = useState( jobCreator.summaryCountFieldName ); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/summary.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/summary.tsx index aabbb0b3aea63..446c16e50618d 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/summary.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/summary.tsx @@ -24,7 +24,7 @@ import type { StepProps } from '../step_types'; import { WIZARD_STEPS } from '../step_types'; import { JobCreatorContext } from '../job_creator_context'; import type { JobRunner } from '../../../common/job_runner'; -import { mlJobService } from '../../../../../services/job_service'; +import { useMlJobService } from '../../../../../services/job_service'; import { JsonEditorFlyout, EDITOR_MODE } from '../common/json_editor_flyout'; import { isSingleMetricJobCreator, isAdvancedJobCreator } from '../../../common/job_creator'; import { JobDetails } from './components/job_details'; @@ -49,6 +49,7 @@ export const SummaryStep: FC = ({ setCurrentStep, isCurrentStep }) => http: { basePath }, }, } = useMlKibana(); + const mlJobService = useMlJobService(); const navigateToPath = useNavigateToPath(); @@ -107,7 +108,7 @@ export const SummaryStep: FC = ({ setCurrentStep, isCurrentStep }) => try { await jobCreator.createJob(); await jobCreator.createDatafeed(); - advancedStartDatafeed(showStartModal ? jobCreator : null, navigateToPath); + advancedStartDatafeed(mlJobService, showStartModal ? jobCreator : null, navigateToPath); } catch (error) { handleJobCreationError(error); } @@ -135,11 +136,11 @@ export const SummaryStep: FC = ({ setCurrentStep, isCurrentStep }) => } function clickResetJob() { - resetJob(jobCreator, navigateToPath); + resetJob(mlJobService, jobCreator, navigateToPath); } const convertToAdvanced = () => { - convertToAdvancedJob(jobCreator, navigateToPath); + convertToAdvancedJob(mlJobService, jobCreator, navigateToPath); }; useEffect(() => { diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/validation_step/validation.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/validation_step/validation.tsx index ea2752c279a6c..4f92bfeee19c0 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/validation_step/validation.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/validation_step/validation.tsx @@ -11,7 +11,6 @@ import { WizardNav } from '../wizard_nav'; import type { StepProps } from '../step_types'; import { WIZARD_STEPS } from '../step_types'; import { JobCreatorContext } from '../job_creator_context'; -import { ml } from '../../../../../services/ml_api_service'; import { ValidateJob } from '../../../../../components/validate_job'; import { JOB_TYPE } from '../../../../../../../common/constants/new_job'; import { SkipValidationButton } from './skip_validatoin'; @@ -67,7 +66,6 @@ export const ValidationStep: FC = ({ setCurrentStep, isCurrentStep }) { const { services: { + chrome: { recentlyAccessed }, share, notifications: { toasts }, }, @@ -133,7 +134,12 @@ export const Page: FC = () => { { absolute: true } ); - addItemToRecentlyAccessed(ML_PAGES.DATA_VISUALIZER_INDEX_VIEWER, title, dataVisualizerLink); + addItemToRecentlyAccessed( + ML_PAGES.DATA_VISUALIZER_INDEX_VIEWER, + title, + dataVisualizerLink, + recentlyAccessed + ); navigateToPath(`/jobs/new_job/datavisualizer${getUrlParams()}`); }; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/new_job/page.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/new_job/page.tsx index a5286d400513c..e4cc4dd88a4e2 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/new_job/page.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/new_job/page.tsx @@ -33,10 +33,10 @@ import { MapLoader } from '../../common/map_loader'; import { ResultsLoader } from '../../common/results_loader'; import { JobValidator } from '../../common/job_validator'; import { useDataSource } from '../../../../contexts/ml'; -import { useMlKibana } from '../../../../contexts/kibana'; +import { useMlApiContext, useMlKibana } from '../../../../contexts/kibana'; import type { ExistingJobsAndGroups } from '../../../../services/job_service'; -import { mlJobService } from '../../../../services/job_service'; -import { newJobCapsService } from '../../../../services/new_job_capabilities/new_job_capabilities_service'; +import { useMlJobService } from '../../../../services/job_service'; +import { useNewJobCapsService } from '../../../../services/new_job_capabilities/new_job_capabilities_service'; import { getNewJobDefaults } from '../../../../services/ml_server_info'; import { useToastNotificationService } from '../../../../services/toast_notification_service'; import { MlPageHeader } from '../../../../components/page_header'; @@ -56,12 +56,18 @@ export const Page: FC = ({ existingJobsAndGroups, jobType }) => { const { services: { maps: mapsPlugin, uiSettings }, } = useMlKibana(); + const ml = useMlApiContext(); + const mlJobService = useMlJobService(); + const newJobCapsService = useNewJobCapsService(); const chartInterval = useTimeBuckets(uiSettings); const jobCreator = useMemo( () => jobCreatorFactory(jobType)( + ml, + mlJobService, + newJobCapsService, dataSourceContext.selectedDataView, dataSourceContext.selectedSavedSearch, dataSourceContext.combinedQuery @@ -202,13 +208,13 @@ export const Page: FC = ({ existingJobsAndGroups, jobType }) => { chartInterval.setInterval('auto'); const chartLoader = useMemo( - () => new ChartLoader(dataSourceContext.selectedDataView, jobCreator.query), - [dataSourceContext.selectedDataView, jobCreator.query] + () => new ChartLoader(ml, dataSourceContext.selectedDataView, jobCreator.query), + [ml, dataSourceContext.selectedDataView, jobCreator.query] ); const mapLoader = useMemo( - () => new MapLoader(dataSourceContext.selectedDataView, jobCreator.query, mapsPlugin), - [dataSourceContext.selectedDataView, jobCreator.query, mapsPlugin] + () => new MapLoader(ml, dataSourceContext.selectedDataView, jobCreator.query, mapsPlugin), + [ml, dataSourceContext.selectedDataView, jobCreator.query, mapsPlugin] ); const resultsLoader = useMemo( diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/new_job/wizard.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/new_job/wizard.tsx index 00950df1c3136..29a6166cd1ead 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/new_job/wizard.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/new_job/wizard.tsx @@ -20,7 +20,7 @@ import type { ChartLoader } from '../../common/chart_loader'; import type { MapLoader } from '../../common/map_loader'; import type { ResultsLoader } from '../../common/results_loader'; import type { JobValidator } from '../../common/job_validator'; -import { newJobCapsService } from '../../../../services/new_job_capabilities/new_job_capabilities_service'; +import { useNewJobCapsService } from '../../../../services/new_job_capabilities/new_job_capabilities_service'; import { WizardSteps } from './wizard_steps'; import { WizardHorizontalSteps } from './wizard_horizontal_steps'; import { JOB_TYPE } from '../../../../../../common/constants/new_job'; @@ -46,6 +46,7 @@ export const Wizard: FC = ({ existingJobsAndGroups, firstWizardStep = WIZARD_STEPS.TIME_RANGE, }) => { + const newJobCapsService = useNewJobCapsService(); const [jobCreatorUpdated, setJobCreatorUpdate] = useState(0); const jobCreatorUpdate = useCallback(() => { setJobCreatorUpdate((prev) => prev + 1); diff --git a/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx b/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx index 6c31aa8d043bf..7dbcbf5809ae2 100644 --- a/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx +++ b/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx @@ -80,8 +80,8 @@ export const JobsListPage: FC = ({ const [refreshJobs, setRefreshJobs] = useState<(() => void) | null>(null); const mlServices = useMemo( - () => getMlGlobalServices(coreStart.http, data.dataViews, usageCollection), - [coreStart.http, data.dataViews, usageCollection] + () => getMlGlobalServices(coreStart, data.dataViews, usageCollection), + [coreStart, data.dataViews, usageCollection] ); const check = async () => { diff --git a/x-pack/plugins/ml/public/application/ml_nodes_check/check_ml_nodes.ts b/x-pack/plugins/ml/public/application/ml_nodes_check/check_ml_nodes.ts index 2d96721204654..78afdd80fcc24 100644 --- a/x-pack/plugins/ml/public/application/ml_nodes_check/check_ml_nodes.ts +++ b/x-pack/plugins/ml/public/application/ml_nodes_check/check_ml_nodes.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { ml } from '../services/ml_api_service'; import type { MlApiServices } from '../services/ml_api_service'; import type { MlNodeCount } from '../../../common/types/ml_server_info'; @@ -13,9 +12,9 @@ let mlNodeCount: number = 0; let lazyMlNodeCount: number = 0; let userHasPermissionToViewMlNodeCount: boolean = false; -export async function getMlNodeCount(mlApiServices?: MlApiServices): Promise { +export async function getMlNodeCount(mlApiServices: MlApiServices): Promise { try { - const nodes = await (mlApiServices ?? ml).mlNodeCount(); + const nodes = await mlApiServices.mlNodeCount(); mlNodeCount = nodes.count; lazyMlNodeCount = nodes.lazyNodeCount; userHasPermissionToViewMlNodeCount = true; diff --git a/x-pack/plugins/ml/public/application/overview/components/analytics_panel/analytics_panel.tsx b/x-pack/plugins/ml/public/application/overview/components/analytics_panel/analytics_panel.tsx index 902fbddc518c4..c94f594f548c5 100644 --- a/x-pack/plugins/ml/public/application/overview/components/analytics_panel/analytics_panel.tsx +++ b/x-pack/plugins/ml/public/application/overview/components/analytics_panel/analytics_panel.tsx @@ -20,7 +20,7 @@ import { import type { MlStorageKey, TMlStorageMapped } from '../../../../../common/types/storage'; import { ML_OVERVIEW_PANELS } from '../../../../../common/types/storage'; import { AnalyticsTable } from './table'; -import { getAnalyticsFactory } from '../../../data_frame_analytics/pages/analytics_management/services/analytics_service'; +import { useGetAnalytics } from '../../../data_frame_analytics/pages/analytics_management/services/analytics_service'; import type { DataFrameAnalyticsListRow } from '../../../data_frame_analytics/pages/analytics_management/components/analytics_list/common'; import { useMlLink } from '../../../contexts/kibana'; import { ML_PAGES } from '../../../../../common/constants/locator'; @@ -60,7 +60,7 @@ export const AnalyticsPanel: FC = ({ setLazyJobCount }) => { setAnalyticsStats(result); }, []); - const getAnalytics = getAnalyticsFactory( + const getAnalytics = useGetAnalytics( setAnalytics, setAnalyticsStatsCustom, setErrorMessage, diff --git a/x-pack/plugins/ml/public/application/overview/components/anomaly_detection_panel/anomaly_detection_panel.tsx b/x-pack/plugins/ml/public/application/overview/components/anomaly_detection_panel/anomaly_detection_panel.tsx index dba7581577058..c33681f69f76a 100644 --- a/x-pack/plugins/ml/public/application/overview/components/anomaly_detection_panel/anomaly_detection_panel.tsx +++ b/x-pack/plugins/ml/public/application/overview/components/anomaly_detection_panel/anomaly_detection_panel.tsx @@ -17,9 +17,8 @@ import { ML_OVERVIEW_PANELS } from '../../../../../common/types/storage'; import { ML_PAGES } from '../../../../../common/constants/locator'; import { OverviewStatsBar } from '../../../components/collapsible_panel/collapsible_panel'; import { CollapsiblePanel } from '../../../components/collapsible_panel'; -import { useMlKibana, useMlLink } from '../../../contexts/kibana'; +import { useMlApiContext, useMlKibana, useMlLink } from '../../../contexts/kibana'; import { AnomalyDetectionTable } from './table'; -import { ml } from '../../../services/ml_api_service'; import { getGroupsFromJobs, getStatsBarData } from './utils'; import type { Dictionary } from '../../../../../common/types/common'; import type { @@ -57,6 +56,7 @@ export const AnomalyDetectionPanel: FC = ({ anomalyTimelineService, setLa const { services: { charts: chartsService }, } = useMlKibana(); + const ml = useMlApiContext(); const { displayErrorToast } = useToastNotificationService(); const { showNodeInfo } = useEnabledFeatures(); diff --git a/x-pack/plugins/ml/public/application/routing/resolvers.ts b/x-pack/plugins/ml/public/application/routing/resolvers.ts index ed7b6af3a48da..e94f4e189f63c 100644 --- a/x-pack/plugins/ml/public/application/routing/resolvers.ts +++ b/x-pack/plugins/ml/public/application/routing/resolvers.ts @@ -7,9 +7,10 @@ import { getMlNodeCount } from '../ml_nodes_check/check_ml_nodes'; import { loadMlServerInfo } from '../services/ml_server_info'; +import type { MlApiServices } from '../services/ml_api_service'; export interface Resolvers { - [name: string]: () => Promise; + [name: string]: (mlApiServices: MlApiServices) => Promise; } export type ResolverResults = | { diff --git a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_creation.tsx b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_creation.tsx index 0fd239559a30d..ffbcb0b8564ae 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_creation.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_creation.tsx @@ -13,6 +13,7 @@ import { dynamic } from '@kbn/shared-ux-utility'; import { DataSourceContextProvider } from '../../../contexts/ml'; import { ML_PAGES } from '../../../../locator'; import type { NavigateToPath } from '../../../contexts/kibana'; +import { useMlApiContext } from '../../../contexts/kibana'; import { useMlKibana } from '../../../contexts/kibana'; import type { MlRoute, PageProps } from '../../router'; import { createPath, PageLoader } from '../../router'; @@ -57,6 +58,7 @@ const PageWrapper: FC = ({ location }) => { savedSearch: savedSearchService, }, } = useMlKibana(); + const mlApiServices = useMlApiContext(); const { context } = useRouteResolver( 'full', @@ -67,6 +69,7 @@ const PageWrapper: FC = ({ location }) => { loadNewJobCapabilities( index, savedSearchId, + mlApiServices, dataViewsService, savedSearchService, DATA_FRAME_ANALYTICS diff --git a/x-pack/plugins/ml/public/application/routing/routes/explorer/explorer.tsx b/x-pack/plugins/ml/public/application/routing/routes/explorer/explorer.tsx index a96e5b7c4e736..04cd415527ace 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/explorer/explorer.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/explorer/explorer.tsx @@ -19,7 +19,7 @@ import { useMlKibana } from '../../../contexts/kibana'; import type { MlRoute, PageProps } from '../../router'; import { createPath, PageLoader } from '../../router'; import { useRouteResolver } from '../../use_resolver'; -import { mlJobService } from '../../../services/job_service'; +import { useMlJobService } from '../../../services/job_service'; import { getDateFormatTz } from '../../../explorer/explorer_utils'; import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; import { MlAnnotationUpdatesContext } from '../../../contexts/ml/ml_annotation_updates_context'; @@ -57,13 +57,15 @@ const PageWrapper: FC = () => { const { services: { mlServices: { mlApiServices }, + uiSettings, }, } = useMlKibana(); + const mlJobService = useMlJobService(); const { context, results } = useRouteResolver('full', ['canGetJobs'], { ...basicResolvers(), jobs: mlJobService.loadJobsWrapper, - jobsWithTimeRange: () => mlApiServices.jobs.jobsWithTimerange(getDateFormatTz()), + jobsWithTimeRange: () => mlApiServices.jobs.jobsWithTimerange(getDateFormatTz(uiSettings)), }); const annotationUpdatesService = useMemo(() => new AnnotationUpdatesService(), []); diff --git a/x-pack/plugins/ml/public/application/routing/routes/explorer/state_manager.tsx b/x-pack/plugins/ml/public/application/routing/routes/explorer/state_manager.tsx index 1c32c29804711..1aa59116be935 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/explorer/state_manager.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/explorer/state_manager.tsx @@ -21,7 +21,6 @@ import { useMlKibana } from '../../../contexts/kibana'; import type { MlJobWithTimeRange } from '../../../../../common/types/anomaly_detection_jobs'; import { useRefresh } from '../../use_refresh'; import { Explorer } from '../../../explorer'; -import { ml } from '../../../services/ml_api_service'; import { useExplorerData } from '../../../explorer/actions'; import { useJobSelection } from '../../../components/job_selector/use_job_selection'; import { useTableInterval } from '../../../components/controls/select_interval'; @@ -40,8 +39,9 @@ export const ExplorerUrlStateManager: FC = ({ jobsWithTimeRange, }) => { const { - services: { cases, presentationUtil, uiSettings }, + services: { cases, presentationUtil, uiSettings, mlServices }, } = useMlKibana(); + const { mlApiServices: ml } = mlServices; const [globalState] = useUrlState('_g'); const [stoppedPartitions, setStoppedPartitions] = useState(); @@ -94,6 +94,7 @@ export const ExplorerUrlStateManager: FC = ({ // eslint-disable-next-line no-console console.error(error); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); useEffect( diff --git a/x-pack/plugins/ml/public/application/routing/routes/new_job/from_lens.tsx b/x-pack/plugins/ml/public/application/routing/routes/new_job/from_lens.tsx index 0602332d33380..210198b54afbb 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/new_job/from_lens.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/new_job/from_lens.tsx @@ -15,6 +15,7 @@ import type { MlRoute, PageProps } from '../../router'; import { createPath, PageLoader } from '../../router'; import { useRouteResolver } from '../../use_resolver'; import { resolver } from '../../../jobs/new_job/job_from_lens'; +import { useMlJobService } from '../../../services/job_service'; export const fromLensRouteFactory = (): MlRoute => ({ path: createPath(ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_FROM_LENS), @@ -43,11 +44,20 @@ const PageWrapper: FC = ({ location }) => { lens, }, } = useMlKibana(); + const mlJobService = useMlJobService(); const { context } = useRouteResolver('full', ['canCreateJob'], { redirect: () => resolver( - { dataViews, lens, mlApiServices, timeFilter, kibanaConfig, dashboardService }, + { + dataViews, + lens, + mlApiServices, + mlJobService, + timeFilter, + kibanaConfig, + dashboardService, + }, vis, from, to, diff --git a/x-pack/plugins/ml/public/application/routing/routes/new_job/from_map.tsx b/x-pack/plugins/ml/public/application/routing/routes/new_job/from_map.tsx index ff30edf35e84a..0ba538ec24348 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/new_job/from_map.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/new_job/from_map.tsx @@ -15,6 +15,7 @@ import type { MlRoute, PageProps } from '../../router'; import { createPath, PageLoader } from '../../router'; import { useRouteResolver } from '../../use_resolver'; import { resolver } from '../../../jobs/new_job/job_from_map'; +import { useMlJobService } from '../../../services/job_service'; export const fromMapRouteFactory = (): MlRoute => ({ path: createPath(ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_FROM_MAP), @@ -49,11 +50,12 @@ const PageWrapper: FC = ({ location }) => { mlServices: { mlApiServices }, }, } = useMlKibana(); + const mlJobService = useMlJobService(); const { context } = useRouteResolver('full', ['canCreateJob'], { redirect: () => resolver( - { dataViews, mlApiServices, timeFilter, kibanaConfig, dashboardService }, + { dataViews, mlApiServices, mlJobService, timeFilter, kibanaConfig, dashboardService }, dashboard, dataViewId, embeddable, diff --git a/x-pack/plugins/ml/public/application/routing/routes/new_job/from_pattern_analysis.tsx b/x-pack/plugins/ml/public/application/routing/routes/new_job/from_pattern_analysis.tsx index bf1327abe1770..e6bc8f046a97b 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/new_job/from_pattern_analysis.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/new_job/from_pattern_analysis.tsx @@ -15,6 +15,7 @@ import type { MlRoute, PageProps } from '../../router'; import { createPath, PageLoader } from '../../router'; import { useRouteResolver } from '../../use_resolver'; import { resolver } from '../../../jobs/new_job/job_from_pattern_analysis'; +import { useMlJobService } from '../../../services/job_service'; export const fromPatternAnalysisRouteFactory = (): MlRoute => ({ path: createPath(ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_FROM_PATTERN_ANALYSIS), @@ -43,12 +44,14 @@ const PageWrapper: FC = ({ location }) => { mlServices: { mlApiServices }, }, } = useMlKibana(); + const mlJobService = useMlJobService(); const { context } = useRouteResolver('full', ['canCreateJob'], { redirect: () => resolver( { mlApiServices, + mlJobService, timeFilter: data.query.timefilter.timefilter, kibanaConfig, dashboardService, diff --git a/x-pack/plugins/ml/public/application/routing/routes/new_job/index_or_search.tsx b/x-pack/plugins/ml/public/application/routing/routes/new_job/index_or_search.tsx index d08ab910fdb26..8cc537990b192 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/new_job/index_or_search.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/new_job/index_or_search.tsx @@ -21,6 +21,7 @@ import { basicResolvers } from '../../resolvers'; import { preConfiguredJobRedirect } from '../../../jobs/new_job/pages/index_or_search'; import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; import { NavigateToPageButton } from '../../components/navigate_to_page_button'; +import { useMlJobService } from '../../../services/job_service'; enum MODE { NEW_JOB, @@ -218,11 +219,12 @@ const PageWrapper: FC = ({ nextStepPath, mode, extraButt data: { dataViews: dataViewsService }, }, } = useMlKibana(); + const mlJobService = useMlJobService(); const newJobResolvers = { ...basicResolvers(), preConfiguredJobRedirect: () => - preConfiguredJobRedirect(dataViewsService, basePath.get(), navigateToUrl), + preConfiguredJobRedirect(mlJobService, dataViewsService, basePath.get(), navigateToUrl), }; const { context } = useRouteResolver( diff --git a/x-pack/plugins/ml/public/application/routing/routes/new_job/recognize.tsx b/x-pack/plugins/ml/public/application/routing/routes/new_job/recognize.tsx index fc954a44c1377..0a6166c3ff3b0 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/new_job/recognize.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/new_job/recognize.tsx @@ -13,7 +13,7 @@ import { dynamic } from '@kbn/shared-ux-utility'; import { basicResolvers } from '../../resolvers'; import { ML_PAGES } from '../../../../locator'; import type { NavigateToPath } from '../../../contexts/kibana'; -import { useMlKibana, useNavigateToPath } from '../../../contexts/kibana'; +import { useMlApiContext, useMlKibana, useNavigateToPath } from '../../../contexts/kibana'; import type { MlRoute, PageProps } from '../../router'; import { createPath, PageLoader } from '../../router'; import { useRouteResolver } from '../../use_resolver'; @@ -21,6 +21,7 @@ import { mlJobServiceFactory } from '../../../services/job_service'; import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; import { useCreateADLinks } from '../../../components/custom_hooks/use_create_ad_links'; import { DataSourceContextProvider } from '../../../contexts/ml'; +import { useToastNotificationService } from '../../../services/toast_notification_service'; const Page = dynamic(async () => ({ default: (await import('../../../jobs/new_job/recognize')).Page, @@ -54,15 +55,13 @@ export const checkViewOrCreateRouteFactory = (): MlRoute => ({ const PageWrapper: FC = ({ location }) => { const { id } = parse(location.search, { sort: false }); - const { - services: { - mlServices: { mlApiServices }, - }, - } = useMlKibana(); + const mlApiServices = useMlApiContext(); + const toastNotificationService = useToastNotificationService(); const { context, results } = useRouteResolver('full', ['canGetJobs'], { ...basicResolvers(), - existingJobsAndGroups: () => mlJobServiceFactory(undefined, mlApiServices).getJobAndGroupIds(), + existingJobsAndGroups: () => + mlJobServiceFactory(toastNotificationService, mlApiServices).getJobAndGroupIds(), }); return ( diff --git a/x-pack/plugins/ml/public/application/routing/routes/new_job/wizard.tsx b/x-pack/plugins/ml/public/application/routing/routes/new_job/wizard.tsx index 7955e8cb7dc9d..66ef374b4879e 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/new_job/wizard.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/new_job/wizard.tsx @@ -20,6 +20,7 @@ import { createPath, PageLoader } from '../../router'; import { useRouteResolver } from '../../use_resolver'; import { JOB_TYPE } from '../../../../../common/constants/new_job'; import { mlJobServiceFactory } from '../../../services/job_service'; +import { useToastNotificationService } from '../../../services/toast_notification_service'; import { loadNewJobCapabilities, ANOMALY_DETECTOR, @@ -209,20 +210,24 @@ const PageWrapper: FC = ({ location, jobType }) => { mlServices: { mlApiServices }, }, } = useMlKibana(); + const toastNotificationService = useToastNotificationService(); const { context, results } = useRouteResolver('full', ['canGetJobs', 'canCreateJob'], { ...basicResolvers(), // TODO useRouteResolver should be responsible for the redirect - privileges: () => checkCreateJobsCapabilitiesResolver(redirectToJobsManagementPage), + privileges: () => + checkCreateJobsCapabilitiesResolver(mlApiServices, redirectToJobsManagementPage), jobCaps: () => loadNewJobCapabilities( index, savedSearchId, + mlApiServices, dataViewsService, savedSearchService, ANOMALY_DETECTOR ), - existingJobsAndGroups: () => mlJobServiceFactory(undefined, mlApiServices).getJobAndGroupIds(), + existingJobsAndGroups: () => + mlJobServiceFactory(toastNotificationService, mlApiServices).getJobAndGroupIds(), }); return ( diff --git a/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer/state_manager.tsx b/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer/state_manager.tsx index 5b74ef608c110..fa1753a4342fc 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer/state_manager.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer/state_manager.tsx @@ -15,11 +15,11 @@ import { useTimefilter } from '@kbn/ml-date-picker'; import type { IUiSettingsClient } from '@kbn/core/public'; import type { TimeRangeBounds } from '@kbn/ml-time-buckets'; import { getViewableDetectors } from '../../../timeseriesexplorer/timeseriesexplorer_utils/get_viewable_detectors'; -import { useMlKibana, useNotifications } from '../../../contexts/kibana'; +import { useNotifications } from '../../../contexts/kibana'; import type { MlJobWithTimeRange } from '../../../../../common/types/anomaly_detection_jobs'; import { isTimeSeriesViewJob } from '../../../../../common/util/job_utils'; import { TimeSeriesExplorer } from '../../../timeseriesexplorer'; -import { mlJobService } from '../../../services/job_service'; +import { useMlJobService } from '../../../services/job_service'; import { useForecastService } from '../../../services/forecast_service'; import { useTimeSeriesExplorerService } from '../../../util/time_series_explorer_service'; import { APP_STATE_ACTION } from '../../../timeseriesexplorer/timeseriesexplorer_constants'; @@ -28,7 +28,6 @@ import { TimeSeriesExplorerPage } from '../../../timeseriesexplorer/timeseriesex import { TimeseriesexplorerNoJobsFound } from '../../../timeseriesexplorer/components/timeseriesexplorer_no_jobs_found'; import { useTableInterval } from '../../../components/controls/select_interval'; import { useTableSeverity } from '../../../components/controls/select_severity'; -import { useToastNotificationService } from '../../../services/toast_notification_service'; import { useTimeSeriesExplorerUrlState } from '../../../timeseriesexplorer/hooks/use_timeseriesexplorer_url_state'; import type { TimeSeriesExplorerAppState } from '../../../../../common/types/locator'; import { useJobSelectionFlyout } from '../../../contexts/ml/use_job_selection_flyout'; @@ -46,14 +45,9 @@ export const TimeSeriesExplorerUrlStateManager: FC { - const { - services: { - data: { dataViews: dataViewsService }, - }, - } = useMlKibana(); + const mlJobService = useMlJobService(); const { toasts } = useNotifications(); const mlForecastService = useForecastService(); - const toastNotificationService = useToastNotificationService(); const [timeSeriesExplorerUrlState, setTimeSeriesExplorerUrlState] = useTimeSeriesExplorerUrlState(); const [globalState, setGlobalState] = useUrlState('_g'); @@ -204,6 +198,7 @@ export const TimeSeriesExplorerUrlStateManager: FC @@ -294,8 +289,6 @@ export const TimeSeriesExplorerUrlStateManager: FC = ({ deps }) => { const mlApi = useMlApiContext(); + const mlJobService = useMlJobService(); const uiSettings = useUiSettings(); const { context, results } = useRouteResolver('full', ['canGetJobs'], { ...basicResolvers(), jobs: mlJobService.loadJobsWrapper, - jobsWithTimeRange: () => mlApi.jobs.jobsWithTimerange(getDateFormatTz()), + jobsWithTimeRange: () => mlApi.jobs.jobsWithTimerange(getDateFormatTz(uiSettings)), }); const annotationUpdatesService = useMemo(() => new AnnotationUpdatesService(), []); diff --git a/x-pack/plugins/ml/public/application/routing/use_resolver.tsx b/x-pack/plugins/ml/public/application/routing/use_resolver.tsx index 5c67ed9769426..d7e7e8762caaf 100644 --- a/x-pack/plugins/ml/public/application/routing/use_resolver.tsx +++ b/x-pack/plugins/ml/public/application/routing/use_resolver.tsx @@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n'; import useMount from 'react-use/lib/useMount'; import { AccessDeniedCallout } from '../access_denied'; import { PLUGIN_ID } from '../../../common/constants/app'; -import { useMlKibana, useMlLicenseInfo } from '../contexts/kibana'; +import { useMlApiContext, useMlKibana, useMlLicenseInfo } from '../contexts/kibana'; import { type MlCapabilitiesKey } from '../../../common/types/capabilities'; import { usePermissionCheck } from '../capabilities/check_capabilities'; import type { ResolverResults, Resolvers } from './resolvers'; @@ -54,7 +54,7 @@ export const useRouteResolver = ( }, }, } = useMlKibana(); - + const mlApiServices = useMlApiContext(); const mlLicenseInfo = useMlLicenseInfo(); useMount(function refreshCapabilitiesOnMount() { @@ -119,10 +119,12 @@ export const useRouteResolver = ( p[c] = {}; return p; }, {} as Exclude); - const res = await Promise.all(funcs.map((r) => r())); + const res = await Promise.all(funcs.map((r) => r(mlApiServices))); res.forEach((r, i) => (tempResults[funcNames[i]] = r)); return tempResults; + // skip mlApiServices from deps + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); useEffect( diff --git a/x-pack/plugins/ml/public/application/services/calendar_service.ts b/x-pack/plugins/ml/public/application/services/calendar_service.ts index 3db4c5a3b9020..d44653e5ee86b 100644 --- a/x-pack/plugins/ml/public/application/services/calendar_service.ts +++ b/x-pack/plugins/ml/public/application/services/calendar_service.ts @@ -6,19 +6,20 @@ */ import { i18n } from '@kbn/i18n'; -import { ml } from './ml_api_service'; import type { Calendar, CalendarId } from '../../../common/types/calendars'; import type { JobId } from '../../../common/types/anomaly_detection_jobs'; +import type { MlApiServices } from './ml_api_service'; + class CalendarService { /** * Assigns a job id to the calendar. * @param calendar * @param jobId */ - async assignNewJobId(calendar: Calendar, jobId: JobId) { + async assignNewJobId(mlApiServices: MlApiServices, calendar: Calendar, jobId: JobId) { const { calendar_id: calendarId } = calendar; try { - await ml.updateCalendar({ + await mlApiServices.updateCalendar({ ...calendar, calendarId, job_ids: [...calendar.job_ids, jobId], @@ -37,9 +38,12 @@ class CalendarService { * Fetches calendars by the list of ids. * @param calendarIds */ - async fetchCalendarsByIds(calendarIds: CalendarId[]): Promise { + async fetchCalendarsByIds( + mlApiServices: MlApiServices, + calendarIds: CalendarId[] + ): Promise { try { - const calendars = await ml.calendars({ calendarIds }); + const calendars = await mlApiServices.calendars({ calendarIds }); return Array.isArray(calendars) ? calendars : [calendars]; } catch (e) { throw new Error( diff --git a/x-pack/plugins/ml/public/application/services/field_format_service.ts b/x-pack/plugins/ml/public/application/services/field_format_service.ts index d01a7294d9c0a..509e791cc75a4 100644 --- a/x-pack/plugins/ml/public/application/services/field_format_service.ts +++ b/x-pack/plugins/ml/public/application/services/field_format_service.ts @@ -6,7 +6,7 @@ */ import { mlFunctionToESAggregation } from '../../../common/util/job_utils'; -import { mlJobService } from './job_service'; +import type { MlJobService } from './job_service'; import type { MlIndexUtils } from '../util/index_service'; import type { MlApiServices } from './ml_api_service'; @@ -19,7 +19,11 @@ export class FieldFormatService { indexPatternIdsByJob: IndexPatternIdsByJob = {}; formatsByJob: FormatsByJobId = {}; - constructor(private mlApiServices: MlApiServices, private mlIndexUtils: MlIndexUtils) {} + constructor( + private mlApiServices: MlApiServices, + private mlIndexUtils: MlIndexUtils, + private mlJobService: MlJobService + ) {} // Populate the service with the FieldFormats for the list of jobs with the // specified IDs. List of Kibana data views is passed, with a title @@ -40,7 +44,7 @@ export class FieldFormatService { const { jobs } = await this.mlApiServices.getJobs({ jobId }); jobObj = jobs[0]; } else { - jobObj = mlJobService.getJob(jobId); + jobObj = this.mlJobService.getJob(jobId); } return { jobId, @@ -85,7 +89,7 @@ export class FieldFormatService { const { jobs } = await this.mlApiServices.getJobs({ jobId }); jobObj = jobs[0]; } else { - jobObj = mlJobService.getJob(jobId); + jobObj = this.mlJobService.getJob(jobId); } const detectors = jobObj.analysis_config.detectors || []; const formatsByDetector: any[] = []; diff --git a/x-pack/plugins/ml/public/application/services/field_format_service_factory.ts b/x-pack/plugins/ml/public/application/services/field_format_service_factory.ts index daefab69154c5..fc59dbdf03ce4 100644 --- a/x-pack/plugins/ml/public/application/services/field_format_service_factory.ts +++ b/x-pack/plugins/ml/public/application/services/field_format_service_factory.ts @@ -8,10 +8,12 @@ import { type MlFieldFormatService, FieldFormatService } from './field_format_service'; import type { MlIndexUtils } from '../util/index_service'; import type { MlApiServices } from './ml_api_service'; +import type { MlJobService } from './job_service'; export function fieldFormatServiceFactory( mlApiServices: MlApiServices, - mlIndexUtils: MlIndexUtils + mlIndexUtils: MlIndexUtils, + mlJobService: MlJobService ): MlFieldFormatService { - return new FieldFormatService(mlApiServices, mlIndexUtils); + return new FieldFormatService(mlApiServices, mlIndexUtils, mlJobService); } diff --git a/x-pack/plugins/ml/public/application/services/http_service.ts b/x-pack/plugins/ml/public/application/services/http_service.ts index cd283c5d58652..86e2266d22351 100644 --- a/x-pack/plugins/ml/public/application/services/http_service.ts +++ b/x-pack/plugins/ml/public/application/services/http_service.ts @@ -7,7 +7,6 @@ import { Observable } from 'rxjs'; import type { HttpFetchOptionsWithPath, HttpFetchOptions, HttpStart } from '@kbn/core/public'; -import { getHttp } from '../util/dependency_cache'; function getResultHeaders(headers: HeadersInit) { return { @@ -48,17 +47,6 @@ function getFetchOptions(options: HttpFetchOptionsWithPath): { }; } -/** - * Function for making HTTP requests to Kibana's backend. - * Wrapper for Kibana's HttpHandler. - * - * @deprecated use {@link HttpService} instead - */ -export async function http(options: HttpFetchOptionsWithPath): Promise { - const { path, fetchOptions } = getFetchOptions(options); - return getHttp().fetch(path, fetchOptions); -} - /** * ML Http Service */ diff --git a/x-pack/plugins/ml/public/application/services/job_service.d.ts b/x-pack/plugins/ml/public/application/services/job_service.d.ts index 509144e96b221..685a0cf86cf9a 100644 --- a/x-pack/plugins/ml/public/application/services/job_service.d.ts +++ b/x-pack/plugins/ml/public/application/services/job_service.d.ts @@ -10,6 +10,7 @@ import type { CombinedJob, Datafeed, Job } from '../../../common/types/anomaly_d import type { Calendar } from '../../../common/types/calendars'; import type { ToastNotificationService } from './toast_notification_service'; import type { MlApiServices } from './ml_api_service'; +import type { JobCreatorType } from '../jobs/new_job/common/job_creator'; export interface ExistingJobsAndGroups { jobIds: string[]; @@ -40,16 +41,28 @@ export declare interface MlJobService { start: number | undefined, end: number | undefined ): Promise; + forceStartDatafeeds( + dIds: string[], + start: number | undefined, + end: number | undefined + ): Promise; createResultsUrl(jobId: string[], start: number, end: number, location: string): string; getJobAndGroupIds(): Promise; getJob(jobId: string): CombinedJob; loadJobsWrapper(): Promise; customUrlsByJob: Record; detectorsByJob: Record; + stashJobForCloning( + jobCreator: JobCreatorType, + skipTimeRangeStep: boolean = false, + includeTimeRange: boolean = false, + autoSetTimeRange: boolean = false + ): void; } -export const mlJobService: MlJobService; export const mlJobServiceFactory: ( - toastNotificationServiceOverride?: ToastNotificationService, - mlOverride?: MlApiServices + toastNotificationService: ToastNotificationService, + mlApiServices: MlApiServices ) => MlJobService; + +export const useMlJobService: () => MlJobService; diff --git a/x-pack/plugins/ml/public/application/services/job_service.js b/x-pack/plugins/ml/public/application/services/job_service.js index a82c1af038489..c37502ab570ac 100644 --- a/x-pack/plugins/ml/public/application/services/job_service.js +++ b/x-pack/plugins/ml/public/application/services/job_service.js @@ -13,27 +13,17 @@ import { validateTimeRange, TIME_FORMAT } from '@kbn/ml-date-utils'; import { parseInterval } from '../../../common/util/parse_interval'; import { isWebUrl } from '../util/url_utils'; +import { useMlApiContext } from '../contexts/kibana'; -import { ml } from './ml_api_service'; -import { getToastNotificationService } from './toast_notification_service'; +import { useToastNotificationService } from './toast_notification_service'; let jobs = []; let datafeedIds = {}; class JobService { - // The overrides allow the use of JobService in contexts where - // the dependency cache is not available, for example when embedding - // the Single Metric Viewer chart. Note we cannot set the members here - // already based on the dependency cache because they will not be - // initialized yet. So this wouldn't work: - // - // this.ml = mlOverride ?? ml; - // - // That's why we have the getters like getMl() below to only access them - // when the methods of this class are being called. - constructor(toastNotificationServiceOverride, mlOverride) { - this.toastNotificationService = toastNotificationServiceOverride; - this.ml = mlOverride; + constructor(toastNotificationService, ml) { + this.toastNotificationService = toastNotificationService; + this.ml = ml; // tempJobCloningObjects -> used to pass a job object between the job management page and // and the advanced wizard. @@ -61,25 +51,17 @@ class JobService { this.customUrlsByJob = {}; } - getMl() { - return this.ml ?? ml; - } - - getToastNotificationService() { - return this.toastNotificationService ?? getToastNotificationService(); - } - loadJobs() { return new Promise((resolve, reject) => { jobs = []; datafeedIds = {}; - this.getMl() + this.ml .getJobs() .then((resp) => { jobs = resp.jobs; // load jobs stats - this.getMl() + this.ml .getJobStats() .then((statsResp) => { // merge jobs stats into jobs @@ -130,7 +112,7 @@ class JobService { function error(err) { console.log('jobService error getting list of jobs:', err); - this.getToastNotificationService().displayErrorToast(err); + this.toastNotificationService.displayErrorToast(err); reject({ jobs, err }); } }); @@ -150,14 +132,14 @@ class JobService { refreshJob(jobId) { return new Promise((resolve, reject) => { - this.getMl() + this.ml .getJobs({ jobId }) .then((resp) => { if (resp.jobs && resp.jobs.length) { const newJob = resp.jobs[0]; // load jobs stats - this.getMl() + this.ml .getJobStats({ jobId }) .then((statsResp) => { // merge jobs stats into jobs @@ -213,7 +195,7 @@ class JobService { function error(err) { console.log('JobService error getting list of jobs:', err); - this.getToastNotificationService().displayErrorToast(err); + this.toastNotificationService.displayErrorToast(err); reject({ jobs, err }); } }); @@ -223,13 +205,13 @@ class JobService { return new Promise((resolve, reject) => { const sId = datafeedId !== undefined ? { datafeed_id: datafeedId } : undefined; - this.getMl() + this.ml .getDatafeeds(sId) .then((resp) => { const datafeeds = resp.datafeeds; // load datafeeds stats - this.getMl() + this.ml .getDatafeedStats() .then((statsResp) => { // merge datafeeds stats into datafeeds @@ -253,7 +235,7 @@ class JobService { function error(err) { console.log('loadDatafeeds error getting list of datafeeds:', err); - this.getToastNotificationService().displayErrorToast(err); + this.toastNotificationService.displayErrorToast(err); reject({ jobs, err }); } }); @@ -263,7 +245,7 @@ class JobService { return new Promise((resolve, reject) => { const datafeedId = this.getDatafeedId(jobId); - this.getMl() + this.ml .getDatafeedStats({ datafeedId }) .then((resp) => { // console.log('updateSingleJobCounts controller query response:', resp); @@ -289,7 +271,7 @@ class JobService { } // return the promise chain - return this.getMl().addJob({ jobId: job.job_id, job }).then(func).catch(func); + return this.ml.addJob({ jobId: job.job_id, job }).then(func).catch(func); } cloneDatafeed(datafeed) { @@ -313,18 +295,18 @@ class JobService { } openJob(jobId) { - return this.getMl().openJob({ jobId }); + return this.ml.openJob({ jobId }); } closeJob(jobId) { - return this.getMl().closeJob({ jobId }); + return this.ml.closeJob({ jobId }); } saveNewDatafeed(datafeedConfig, jobId) { const datafeedId = `datafeed-${jobId}`; datafeedConfig.job_id = jobId; - return this.getMl().addDatafeed({ + return this.ml.addDatafeed({ datafeedId, datafeedConfig, }); @@ -340,7 +322,7 @@ class JobService { end++; } - this.getMl() + this.ml .startDatafeed({ datafeedId, start, @@ -357,29 +339,29 @@ class JobService { } forceStartDatafeeds(dIds, start, end) { - return this.getMl().jobs.forceStartDatafeeds(dIds, start, end); + return this.ml.jobs.forceStartDatafeeds(dIds, start, end); } stopDatafeeds(dIds) { - return this.getMl().jobs.stopDatafeeds(dIds); + return this.ml.jobs.stopDatafeeds(dIds); } deleteJobs(jIds, deleteUserAnnotations, deleteAlertingRules) { - return this.getMl().jobs.deleteJobs(jIds, deleteUserAnnotations, deleteAlertingRules); + return this.ml.jobs.deleteJobs(jIds, deleteUserAnnotations, deleteAlertingRules); } closeJobs(jIds) { - return this.getMl().jobs.closeJobs(jIds); + return this.ml.jobs.closeJobs(jIds); } resetJobs(jIds, deleteUserAnnotations) { - return this.getMl().jobs.resetJobs(jIds, deleteUserAnnotations); + return this.ml.jobs.resetJobs(jIds, deleteUserAnnotations); } validateDetector(detector) { return new Promise((resolve, reject) => { if (detector) { - this.getMl() + this.ml .validateDetector({ detector }) .then((resp) => { resolve(resp); @@ -432,7 +414,7 @@ class JobService { async getJobAndGroupIds() { try { - return await this.getMl().jobs.getAllJobAndGroupIds(); + return await this.ml.jobs.getAllJobAndGroupIds(); } catch (error) { return { jobIds: [], @@ -440,6 +422,26 @@ class JobService { }; } } + + stashJobForCloning(jobCreator, skipTimeRangeStep, includeTimeRange, autoSetTimeRange) { + const tempJobCloningObjects = { + job: jobCreator.jobConfig, + datafeed: jobCreator.datafeedConfig, + createdBy: jobCreator.createdBy ?? undefined, + // skip over the time picker step of the wizard + skipTimeRangeStep, + calendars: jobCreator.calendars, + ...(includeTimeRange === true && autoSetTimeRange === false + ? // auto select the start and end dates of the time picker + { + start: jobCreator.start, + end: jobCreator.end, + } + : { autoSetTimeRange: true }), + }; + + this.tempJobCloningObjects = tempJobCloningObjects; + } } // private function used to check the job saving response @@ -592,6 +594,17 @@ function createResultsUrl(jobIds, start, end, resultsPage, mode = 'absolute') { return path; } -export const mlJobService = new JobService(); -export const mlJobServiceFactory = (toastNotificationServiceOverride, mlOverride) => - new JobService(toastNotificationServiceOverride, mlOverride); +// This is to retain the singleton behavior of the previous direct instantiation and export. +let mlJobService; +export const mlJobServiceFactory = (toastNotificationService, mlApiServices) => { + if (mlJobService) return mlJobService; + + mlJobService = new JobService(toastNotificationService, mlApiServices); + return mlJobService; +}; + +export const useMlJobService = () => { + const toastNotificationService = useToastNotificationService(); + const mlApiServices = useMlApiContext(); + return mlJobServiceFactory(toastNotificationService, mlApiServices); +}; diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts index 6d086f99bce08..77c074a97bec4 100644 --- a/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts +++ b/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts @@ -9,7 +9,6 @@ import type { Observable } from 'rxjs'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import type { HttpStart } from '@kbn/core/public'; import type { RuntimeMappings } from '@kbn/ml-runtime-field-utils'; import { ML_INTERNAL_BASE_PATH } from '../../../../common/constants/app'; @@ -39,9 +38,8 @@ import type { import type { DatafeedValidationResponse } from '../../../../common/types/job_validation'; import type { FieldHistogramRequestConfig } from '../../datavisualizer/index_based/common/request'; -import { getHttp } from '../../util/dependency_cache'; -import { HttpService } from '../http_service'; +import type { HttpService } from '../http_service'; import { jsonSchemaProvider } from './json_schema'; import { annotationsApiProvider } from './annotations'; @@ -101,26 +99,6 @@ export interface GetModelSnapshotsResponse { model_snapshots: ModelSnapshot[]; } -/** - * Temp solution to allow {@link ml} service to use http from - * the dependency_cache. - */ -const proxyHttpStart = new Proxy({} as unknown as HttpStart, { - get(obj, prop: keyof HttpStart) { - try { - return getHttp()[prop]; - } catch (e) { - if (prop === 'getLoadingCount$') { - return () => {}; - } - // eslint-disable-next-line no-console - console.error(e); - } - }, -}); - -export const ml = mlApiServicesProvider(new HttpService(proxyHttpStart)); - export function mlApiServicesProvider(httpService: HttpService) { return { getJobs(obj?: { jobId?: string }) { diff --git a/x-pack/plugins/ml/public/application/services/ml_server_info.test.ts b/x-pack/plugins/ml/public/application/services/ml_server_info.test.ts index 4aabe52b034d1..bb10e9b466f6e 100644 --- a/x-pack/plugins/ml/public/application/services/ml_server_info.test.ts +++ b/x-pack/plugins/ml/public/application/services/ml_server_info.test.ts @@ -5,6 +5,7 @@ * 2.0. */ +import type { MlApiServices } from './ml_api_service'; import { loadMlServerInfo, getCloudDeploymentId, @@ -16,11 +17,9 @@ import { } from './ml_server_info'; import mockMlInfoResponse from './__mocks__/ml_info_response.json'; -jest.mock('./ml_api_service', () => ({ - ml: { - mlInfo: jest.fn(() => Promise.resolve(mockMlInfoResponse)), - }, -})); +const mlApiServicesMock = { + mlInfo: jest.fn(() => Promise.resolve(mockMlInfoResponse)), +} as unknown as MlApiServices; describe('ml_server_info initial state', () => { it('should fail to get server info ', () => { @@ -31,7 +30,7 @@ describe('ml_server_info initial state', () => { describe('ml_server_info', () => { beforeEach(async () => { - await loadMlServerInfo(); + await loadMlServerInfo(mlApiServicesMock); }); describe('cloud information', () => { diff --git a/x-pack/plugins/ml/public/application/services/ml_server_info.ts b/x-pack/plugins/ml/public/application/services/ml_server_info.ts index 0e262c2bf0f95..101cb33264502 100644 --- a/x-pack/plugins/ml/public/application/services/ml_server_info.ts +++ b/x-pack/plugins/ml/public/application/services/ml_server_info.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ml } from './ml_api_service'; +import type { MlApiServices } from './ml_api_service'; import type { MlServerDefaults, MlServerLimits } from '../../../common/types/ml_server_info'; export interface CloudInfo { @@ -28,9 +28,9 @@ const cloudInfo: CloudInfo = { deploymentId: null, }; -export async function loadMlServerInfo() { +export async function loadMlServerInfo(mlApiServices: MlApiServices) { try { - const resp = await ml.mlInfo(); + const resp = await mlApiServices.mlInfo(); defaults = resp.defaults; limits = resp.limits; cloudInfo.cloudId = resp.cloudId ?? null; diff --git a/x-pack/plugins/ml/public/application/services/new_job_capabilities/load_new_job_capabilities.ts b/x-pack/plugins/ml/public/application/services/new_job_capabilities/load_new_job_capabilities.ts index 4570d838e348f..6c25d43020d52 100644 --- a/x-pack/plugins/ml/public/application/services/new_job_capabilities/load_new_job_capabilities.ts +++ b/x-pack/plugins/ml/public/application/services/new_job_capabilities/load_new_job_capabilities.ts @@ -9,8 +9,9 @@ import type { DataView, DataViewsContract } from '@kbn/data-views-plugin/public' import type { SavedSearchPublicPluginStart } from '@kbn/saved-search-plugin/public'; import { getDataViewAndSavedSearchCallback } from '../../util/index_utils'; import type { JobType } from '../../../../common/types/saved_objects'; -import { newJobCapsServiceAnalytics } from './new_job_capabilities_service_analytics'; -import { newJobCapsService } from './new_job_capabilities_service'; +import type { MlApiServices } from '../ml_api_service'; +import { mlJobCapsServiceAnalyticsFactory } from './new_job_capabilities_service_analytics'; +import { mlJobCapsServiceFactory } from './new_job_capabilities_service'; export const ANOMALY_DETECTOR = 'anomaly-detector'; export const DATA_FRAME_ANALYTICS = 'data-frame-analytics'; @@ -20,6 +21,7 @@ export const DATA_FRAME_ANALYTICS = 'data-frame-analytics'; export function loadNewJobCapabilities( dataViewId: string, savedSearchId: string, + mlApiServices: MlApiServices, dataViewsService: DataViewsContract, savedSearchService: SavedSearchPublicPluginStart, jobType: JobType @@ -27,7 +29,9 @@ export function loadNewJobCapabilities( return new Promise(async (resolve, reject) => { try { const serviceToUse = - jobType === ANOMALY_DETECTOR ? newJobCapsService : newJobCapsServiceAnalytics; + jobType === ANOMALY_DETECTOR + ? mlJobCapsServiceFactory(mlApiServices) + : mlJobCapsServiceAnalyticsFactory(mlApiServices); if (dataViewId !== undefined) { // index pattern is being used diff --git a/x-pack/plugins/ml/public/application/services/new_job_capabilities/new_job_capabilities._service.test.ts b/x-pack/plugins/ml/public/application/services/new_job_capabilities/new_job_capabilities_service.test.ts similarity index 81% rename from x-pack/plugins/ml/public/application/services/new_job_capabilities/new_job_capabilities._service.test.ts rename to x-pack/plugins/ml/public/application/services/new_job_capabilities/new_job_capabilities_service.test.ts index ec7ef70f8d0de..ed257199db16b 100644 --- a/x-pack/plugins/ml/public/application/services/new_job_capabilities/new_job_capabilities._service.test.ts +++ b/x-pack/plugins/ml/public/application/services/new_job_capabilities/new_job_capabilities_service.test.ts @@ -5,20 +5,20 @@ * 2.0. */ -import { newJobCapsService } from './new_job_capabilities_service'; +import { mlJobCapsServiceFactory } from './new_job_capabilities_service'; import type { DataView } from '@kbn/data-views-plugin/public'; +import type { MlApiServices } from '../ml_api_service'; + // there is magic happening here. starting the include name with `mock..` // ensures it can be lazily loaded by the jest.mock function below. import mockCloudwatchResponse from '../__mocks__/cloudwatch_job_caps_response.json'; -jest.mock('../ml_api_service', () => ({ - ml: { - jobs: { - newJobCaps: jest.fn(() => Promise.resolve(mockCloudwatchResponse)), - }, +const mlApiServicesMock = { + jobs: { + newJobCaps: jest.fn(() => Promise.resolve(mockCloudwatchResponse)), }, -})); +} as unknown as MlApiServices; const dataView = { id: 'cloudwatch-*', @@ -28,6 +28,7 @@ const dataView = { describe('new_job_capabilities_service', () => { describe('cloudwatch newJobCaps()', () => { it('can construct job caps objects from endpoint json', async () => { + const newJobCapsService = mlJobCapsServiceFactory(mlApiServicesMock); await newJobCapsService.initializeFromDataVIew(dataView); const { fields, aggs } = await newJobCapsService.newJobCaps; @@ -47,6 +48,7 @@ describe('new_job_capabilities_service', () => { }); it('job caps including text fields', async () => { + const newJobCapsService = mlJobCapsServiceFactory(mlApiServicesMock); await newJobCapsService.initializeFromDataVIew(dataView, true, false); const { fields, aggs } = await newJobCapsService.newJobCaps; @@ -55,6 +57,7 @@ describe('new_job_capabilities_service', () => { }); it('job caps excluding event rate', async () => { + const newJobCapsService = mlJobCapsServiceFactory(mlApiServicesMock); await newJobCapsService.initializeFromDataVIew(dataView, false, true); const { fields, aggs } = await newJobCapsService.newJobCaps; diff --git a/x-pack/plugins/ml/public/application/services/new_job_capabilities/new_job_capabilities_service.ts b/x-pack/plugins/ml/public/application/services/new_job_capabilities/new_job_capabilities_service.ts index 0df13dfe2814b..990d411779a6f 100644 --- a/x-pack/plugins/ml/public/application/services/new_job_capabilities/new_job_capabilities_service.ts +++ b/x-pack/plugins/ml/public/application/services/new_job_capabilities/new_job_capabilities_service.ts @@ -4,6 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ + import { ES_FIELD_TYPES } from '@kbn/field-types'; import type { DataView } from '@kbn/data-views-plugin/public'; import { @@ -14,8 +15,9 @@ import { EVENT_RATE_FIELD_ID, } from '@kbn/ml-anomaly-utils'; import { DataViewType } from '@kbn/data-views-plugin/public'; +import { useMlApiContext } from '../../contexts/kibana'; import { getGeoFields, filterCategoryFields } from '../../../../common/util/fields_utils'; -import { ml, type MlApiServices } from '../ml_api_service'; +import type { MlApiServices } from '../ml_api_service'; import { processTextAndKeywordFields, NewJobCapabilitiesServiceBase } from './new_job_capabilities'; export class NewJobCapsService extends NewJobCapabilitiesServiceBase { @@ -185,4 +187,16 @@ function addEventRateField(aggs: Aggregation[], fields: Field[]) { fields.splice(0, 0, eventRateField); } -export const newJobCapsService = new NewJobCapsService(ml); +// This is to retain the singleton behavior of the previous direct instantiation and export. +let newJobCapsService: NewJobCapsService; +export const mlJobCapsServiceFactory = (mlApiServices: MlApiServices) => { + if (newJobCapsService) return newJobCapsService; + + newJobCapsService = new NewJobCapsService(mlApiServices); + return newJobCapsService; +}; + +export const useNewJobCapsService = () => { + const mlApiServices = useMlApiContext(); + return mlJobCapsServiceFactory(mlApiServices); +}; diff --git a/x-pack/plugins/ml/public/application/services/new_job_capabilities/new_job_capabilities_service_analytics.ts b/x-pack/plugins/ml/public/application/services/new_job_capabilities/new_job_capabilities_service_analytics.ts index ef9b4219c2994..c2b7a4dc24906 100644 --- a/x-pack/plugins/ml/public/application/services/new_job_capabilities/new_job_capabilities_service_analytics.ts +++ b/x-pack/plugins/ml/public/application/services/new_job_capabilities/new_job_capabilities_service_analytics.ts @@ -25,8 +25,9 @@ import { TOP_CLASSES, type DataFrameAnalyticsConfig, } from '@kbn/ml-data-frame-analytics-utils'; +import { useMlApiContext } from '../../contexts/kibana'; import { processTextAndKeywordFields, NewJobCapabilitiesServiceBase } from './new_job_capabilities'; -import { ml } from '../ml_api_service'; +import type { MlApiServices } from '../ml_api_service'; // Keep top nested field and remove all .* fields export function removeNestedFieldChildren(resp: NewJobCapsResponse, indexPatternTitle: string) { @@ -59,13 +60,21 @@ export function removeNestedFieldChildren(resp: NewJobCapsResponse, indexPattern return fields; } -class NewJobCapsServiceAnalytics extends NewJobCapabilitiesServiceBase { +export class NewJobCapsServiceAnalytics extends NewJobCapabilitiesServiceBase { + private _mlApiService: MlApiServices; + + constructor(mlApiService: MlApiServices) { + super(); + this._mlApiService = mlApiService; + } + public async initializeFromDataVIew(dataView: DataView) { try { - const resp: NewJobCapsResponse = await ml.dataFrameAnalytics.newJobCapsAnalytics( - dataView.getIndexPattern(), - dataView.type === DataViewType.ROLLUP - ); + const resp: NewJobCapsResponse = + await this._mlApiService.dataFrameAnalytics.newJobCapsAnalytics( + dataView.getIndexPattern(), + dataView.type === DataViewType.ROLLUP + ); const allFields = removeNestedFieldChildren(resp, dataView.getIndexPattern()); @@ -216,4 +225,16 @@ class NewJobCapsServiceAnalytics extends NewJobCapabilitiesServiceBase { } } -export const newJobCapsServiceAnalytics = new NewJobCapsServiceAnalytics(); +// This is to retain the singleton behavior of the previous direct instantiation and export. +let newJobCapsServiceAnalytics: NewJobCapsServiceAnalytics; +export const mlJobCapsServiceAnalyticsFactory = (mlApiServices: MlApiServices) => { + if (newJobCapsServiceAnalytics) return newJobCapsServiceAnalytics; + + newJobCapsServiceAnalytics = new NewJobCapsServiceAnalytics(mlApiServices); + return newJobCapsServiceAnalytics; +}; + +export const useNewJobCapsServiceAnalytics = () => { + const mlApiServices = useMlApiContext(); + return mlJobCapsServiceAnalyticsFactory(mlApiServices); +}; diff --git a/x-pack/plugins/ml/public/application/services/results_service/index.ts b/x-pack/plugins/ml/public/application/services/results_service/index.ts index a8f1a5a9bd105..d8621eff633a4 100644 --- a/x-pack/plugins/ml/public/application/services/results_service/index.ts +++ b/x-pack/plugins/ml/public/application/services/results_service/index.ts @@ -5,14 +5,13 @@ * 2.0. */ -import { useMemo } from 'react'; import { resultsServiceRxProvider } from './result_service_rx'; import { resultsServiceProvider } from './results_service'; import type { MlApiServices } from '../ml_api_service'; -import { ml } from '../ml_api_service'; -import { useMlKibana } from '../../contexts/kibana'; +import { useMlApiContext } from '../../contexts/kibana'; -export type MlResultsService = typeof mlResultsService; +export type MlResultsService = ReturnType & + ReturnType; type Time = string; export interface ModelPlotOutputResults { @@ -24,22 +23,20 @@ export interface CriteriaField { fieldValue: any; } -export const mlResultsService = mlResultsServiceProvider(ml); - +// This is to retain the singleton behavior of the previous direct instantiation and export. +let mlResultsService: MlResultsService; export function mlResultsServiceProvider(mlApiServices: MlApiServices) { - return { + if (mlResultsService) return mlResultsService; + + mlResultsService = { ...resultsServiceProvider(mlApiServices), ...resultsServiceRxProvider(mlApiServices), }; + + return mlResultsService; } export function useMlResultsService(): MlResultsService { - const { - services: { - mlServices: { mlApiServices }, - }, - } = useMlKibana(); - - const resultsService = useMemo(() => mlResultsServiceProvider(mlApiServices), [mlApiServices]); - return resultsService; + const mlApiServices = useMlApiContext(); + return mlResultsServiceProvider(mlApiServices); } diff --git a/x-pack/plugins/ml/public/application/services/toast_notification_service/index.ts b/x-pack/plugins/ml/public/application/services/toast_notification_service/index.ts index d2f946777284d..21614dd4d6710 100644 --- a/x-pack/plugins/ml/public/application/services/toast_notification_service/index.ts +++ b/x-pack/plugins/ml/public/application/services/toast_notification_service/index.ts @@ -9,5 +9,4 @@ export type { ToastNotificationService } from './toast_notification_service'; export { toastNotificationServiceProvider, useToastNotificationService, - getToastNotificationService, } from './toast_notification_service'; diff --git a/x-pack/plugins/ml/public/application/services/toast_notification_service/toast_notification_service.ts b/x-pack/plugins/ml/public/application/services/toast_notification_service/toast_notification_service.ts index e07cc1493b5da..f209c985e57a9 100644 --- a/x-pack/plugins/ml/public/application/services/toast_notification_service/toast_notification_service.ts +++ b/x-pack/plugins/ml/public/application/services/toast_notification_service/toast_notification_service.ts @@ -9,7 +9,6 @@ import { i18n } from '@kbn/i18n'; import type { ToastInput, ToastOptions, ToastsStart } from '@kbn/core/public'; import { useMemo } from 'react'; import { extractErrorProperties, type ErrorType, MLRequestFailure } from '@kbn/ml-error-utils'; -import { getToastNotifications } from '../../util/dependency_cache'; import { useNotifications } from '../../contexts/kibana'; export type ToastNotificationService = ReturnType; @@ -42,11 +41,6 @@ export function toastNotificationServiceProvider(toastNotifications: ToastsStart return { displayDangerToast, displayWarningToast, displaySuccessToast, displayErrorToast }; } -export function getToastNotificationService() { - const toastNotifications = getToastNotifications(); - return toastNotificationServiceProvider(toastNotifications); -} - /** * Hook to use {@link ToastNotificationService} in React components. */ diff --git a/x-pack/plugins/ml/public/application/settings/anomaly_detection_settings.tsx b/x-pack/plugins/ml/public/application/settings/anomaly_detection_settings.tsx index 6073c563187ca..5161963e90a46 100644 --- a/x-pack/plugins/ml/public/application/settings/anomaly_detection_settings.tsx +++ b/x-pack/plugins/ml/public/application/settings/anomaly_detection_settings.tsx @@ -22,13 +22,15 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; +import { useMlApiContext } from '../contexts/kibana'; import { AnomalyDetectionSettingsContext } from './anomaly_detection_settings_context'; -import { ml } from '../services/ml_api_service'; import { useToastNotificationService } from '../services/toast_notification_service'; import { ML_PAGES } from '../../../common/constants/locator'; import { useCreateAndNavigateToMlLink } from '../contexts/kibana/use_create_url'; export const AnomalyDetectionSettings: FC = () => { + const ml = useMlApiContext(); + const [calendarsCount, setCalendarsCount] = useState(0); const [filterListsCount, setFilterListsCount] = useState(0); diff --git a/x-pack/plugins/ml/public/application/settings/calendars/edit/__snapshots__/new_calendar.test.js.snap b/x-pack/plugins/ml/public/application/settings/calendars/edit/__snapshots__/new_calendar.test.js.snap deleted file mode 100644 index 075c7388acb25..0000000000000 --- a/x-pack/plugins/ml/public/application/settings/calendars/edit/__snapshots__/new_calendar.test.js.snap +++ /dev/null @@ -1,42 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`NewCalendar Renders new calendar form 1`] = ` - -
- - - -
- -
-`; diff --git a/x-pack/plugins/ml/public/application/settings/calendars/edit/new_calendar.js b/x-pack/plugins/ml/public/application/settings/calendars/edit/new_calendar.js index 6375dfab79857..79f4d82093f21 100644 --- a/x-pack/plugins/ml/public/application/settings/calendars/edit/new_calendar.js +++ b/x-pack/plugins/ml/public/application/settings/calendars/edit/new_calendar.js @@ -16,12 +16,10 @@ import { getCalendarSettingsData, validateCalendarId } from './utils'; import { CalendarForm } from './calendar_form'; import { NewEventModal } from './new_event_modal'; import { ImportModal } from './import_modal'; -import { ml } from '../../../services/ml_api_service'; import { withKibana } from '@kbn/kibana-react-plugin/public'; import { GLOBAL_CALENDAR } from '../../../../../common/constants/calendars'; import { ML_PAGES } from '../../../../../common/constants/locator'; import { toastNotificationServiceProvider } from '../../../services/toast_notification_service'; -import { getDocLinks } from '../../../util/dependency_cache'; import { HelpMenu } from '../../../components/help_menu'; class NewCalendarUI extends Component { @@ -73,7 +71,9 @@ class NewCalendarUI extends Component { async formSetup() { try { - const { jobIds, groupIds, calendars } = await getCalendarSettingsData(); + const { jobIds, groupIds, calendars } = await getCalendarSettingsData( + this.props.kibana.services.mlServices.mlApiServices + ); const jobIdOptions = jobIds.map((jobId) => ({ label: jobId })); const groupIdOptions = groupIds.map((groupId) => ({ label: groupId })); @@ -145,6 +145,7 @@ class NewCalendarUI extends Component { }; onCreate = async () => { + const ml = this.props.kibana.services.mlServices.mlApiServices; const { formCalendarId } = this.state; if (this.isDuplicateId()) { @@ -176,6 +177,7 @@ class NewCalendarUI extends Component { }; onEdit = async () => { + const ml = this.props.kibana.services.mlServices.mlApiServices; const calendar = this.setUpCalendarForApi(); this.setState({ saving: true }); @@ -331,7 +333,7 @@ class NewCalendarUI extends Component { isGlobalCalendar, } = this.state; - const helpLink = getDocLinks().links.ml.calendars; + const helpLink = this.props.kibana.services.docLinks.links.ml.calendars; let modal = ''; diff --git a/x-pack/plugins/ml/public/application/settings/calendars/edit/new_calendar.test.js b/x-pack/plugins/ml/public/application/settings/calendars/edit/new_calendar.test.js index 4e9a16858f816..1e538668d9ac3 100644 --- a/x-pack/plugins/ml/public/application/settings/calendars/edit/new_calendar.test.js +++ b/x-pack/plugins/ml/public/application/settings/calendars/edit/new_calendar.test.js @@ -5,6 +5,12 @@ * 2.0. */ +import React from 'react'; +import { render, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import '@testing-library/jest-dom/extend-expect'; +import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; + jest.mock('../../../contexts/kibana/use_create_url', () => ({ useCreateAndNavigateToMlLink: jest.fn(), })); @@ -13,14 +19,6 @@ jest.mock('../../../components/help_menu', () => ({ HelpMenu: () =>
, })); -jest.mock('../../../util/dependency_cache', () => ({ - getDocLinks: () => ({ - links: { - ml: { calendars: jest.fn() }, - }, - }), -})); - jest.mock('../../../capabilities/check_capabilities', () => ({ checkPermission: () => true, })); @@ -34,44 +32,8 @@ jest.mock('../../../capabilities/get_capabilities', () => ({ jest.mock('../../../ml_nodes_check/check_ml_nodes', () => ({ mlNodesAvailable: () => true, })); -jest.mock('../../../services/ml_api_service', () => ({ - ml: { - calendars: () => { - return Promise.resolve([]); - }, - jobs: { - jobsSummary: () => { - return Promise.resolve([]); - }, - groups: () => { - return Promise.resolve([]); - }, - }, - }, -})); -jest.mock('./utils', () => ({ - getCalendarSettingsData: jest.fn().mockImplementation( - () => - new Promise((resolve) => { - resolve({ - jobIds: ['test-job-one', 'test-job-2'], - groupIds: ['test-group-one', 'test-group-two'], - calendars: [], - }); - }) - ), -})); -jest.mock('@kbn/kibana-react-plugin/public', () => ({ - withKibana: (comp) => { - return comp; - }, -})); -import { shallowWithIntl, mountWithIntl } from '@kbn/test-jest-helpers'; -import React from 'react'; -import { NewCalendar } from './new_calendar'; - -const calendars = [ +const calendarsMock = [ { calendar_id: 'farequote-calendar', job_ids: ['farequote'], @@ -102,65 +64,129 @@ const calendars = [ }, ]; -const props = { - canCreateCalendar: true, - canDeleteCalendar: true, - kibana: { - services: { - data: { - query: { - timefilter: { - timefilter: { - disableTimeRangeSelector: jest.fn(), - disableAutoRefreshSelector: jest.fn(), - }, - }, +jest.mock('./utils', () => ({ + ...jest.requireActual('./utils'), + getCalendarSettingsData: jest.fn().mockImplementation( + () => + new Promise((resolve) => { + resolve({ + jobIds: ['test-job-one', 'test-job-2'], + groupIds: ['test-group-one', 'test-group-two'], + calendars: calendarsMock, + }); + }) + ), +})); + +const mockAddDanger = jest.fn(); +const mockKibanaContext = { + services: { + docLinks: { links: { ml: { calendars: 'test' } } }, + notifications: { toasts: { addDanger: mockAddDanger, addError: jest.fn() } }, + mlServices: { + mlApiServices: { + calendars: () => { + return Promise.resolve([]); }, - }, - notifications: { - toasts: { - addDanger: () => {}, + jobs: { + jobsSummary: () => { + return Promise.resolve([]); + }, + groups: () => { + return Promise.resolve([]); + }, }, }, }, }, }; +const mockReact = React; +jest.mock('@kbn/kibana-react-plugin/public', () => ({ + withKibana: (type) => { + const EnhancedType = (props) => { + return mockReact.createElement(type, { + ...props, + kibana: mockKibanaContext, + }); + }; + return EnhancedType; + }, +})); + +import { NewCalendar } from './new_calendar'; + +const props = { + canCreateCalendar: true, + canDeleteCalendar: true, +}; + describe('NewCalendar', () => { test('Renders new calendar form', () => { - const wrapper = shallowWithIntl(); + const { getByTestId } = render( + + + + ); - expect(wrapper).toMatchSnapshot(); + expect(getByTestId('mlPageCalendarEdit')).toBeInTheDocument(); }); test('Import modal button is disabled', () => { - const wrapper = mountWithIntl(); + const { getByTestId } = render( + + + + ); + + const importEventsButton = getByTestId('mlCalendarImportEventsButton'); + expect(importEventsButton).toBeInTheDocument(); + expect(importEventsButton).toBeDisabled(); + }); + + test('New event modal button is disabled', async () => { + const { getByTestId } = render( + + + + ); - const importButton = wrapper.find('[data-test-subj="mlCalendarImportEventsButton"]'); - const button = importButton.find('EuiButton'); - expect(button.prop('isDisabled')).toBe(true); + const newEventButton = getByTestId('mlCalendarNewEventButton'); + expect(newEventButton).toBeInTheDocument(); + expect(newEventButton).toBeDisabled(); }); - test('New event modal button is disabled', () => { - const wrapper = mountWithIntl(); + test('isDuplicateId returns true if form calendar id already exists in calendars', async () => { + const { getByTestId, queryByTestId, getByText } = render( + + + + ); - const importButton = wrapper.find('[data-test-subj="mlCalendarNewEventButton"]'); - const button = importButton.find('EuiButton button'); - button.simulate('click'); + const mlCalendarIdFormRow = getByText('Calendar ID'); + expect(mlCalendarIdFormRow).toBeInTheDocument(); + const mlCalendarIdInput = queryByTestId('mlCalendarIdInput'); + expect(mlCalendarIdInput).toBeInTheDocument(); - expect(button.prop('disabled')).toBe(true); - }); + await waitFor(() => { + expect(mlCalendarIdInput).toBeEnabled(); + }); - test('isDuplicateId returns true if form calendar id already exists in calendars', () => { - const wrapper = mountWithIntl(); + await userEvent.type(mlCalendarIdInput, 'this-is-a-new-calendar'); - const instance = wrapper.instance(); - instance.setState({ - calendars, - formCalendarId: calendars[0].calendar_id, + await waitFor(() => { + expect(mlCalendarIdInput).toHaveValue('this-is-a-new-calendar'); }); - wrapper.update(); - expect(instance.isDuplicateId()).toBe(true); + + const mlSaveCalendarButton = getByTestId('mlSaveCalendarButton'); + expect(mlSaveCalendarButton).toBeInTheDocument(); + expect(mlSaveCalendarButton).toBeEnabled(); + + await userEvent.click(mlSaveCalendarButton); + + expect(mockAddDanger).toHaveBeenCalledWith( + 'Cannot create calendar with id [this-is-a-new-calendar] as it already exists.' + ); }); test('Save button is disabled if canCreateCalendar is false', () => { @@ -169,11 +195,13 @@ describe('NewCalendar', () => { canCreateCalendar: false, }; - const wrapper = mountWithIntl(); - - const buttons = wrapper.find('[data-test-subj="mlSaveCalendarButton"]'); - const saveButton = buttons.find('EuiButton'); + const { getByTestId } = render( + + + + ); - expect(saveButton.prop('isDisabled')).toBe(true); + const saveButton = getByTestId('mlSaveCalendarButton'); + expect(saveButton).toBeDisabled(); }); }); diff --git a/x-pack/plugins/ml/public/application/settings/calendars/edit/utils.js b/x-pack/plugins/ml/public/application/settings/calendars/edit/utils.js index 113a266e39a8d..34fa2f5292f62 100644 --- a/x-pack/plugins/ml/public/application/settings/calendars/edit/utils.js +++ b/x-pack/plugins/ml/public/application/settings/calendars/edit/utils.js @@ -5,13 +5,12 @@ * 2.0. */ -import { ml } from '../../../services/ml_api_service'; import { isJobIdValid } from '../../../../../common/util/job_utils'; import { i18n } from '@kbn/i18n'; -function getJobIds() { +function getJobIds(mlApiServices) { return new Promise((resolve, reject) => { - ml.jobs + mlApiServices.jobs .jobsSummary() .then((resp) => { resolve(resp.map((job) => job.id)); @@ -30,9 +29,9 @@ function getJobIds() { }); } -function getGroupIds() { +function getGroupIds(mlApiServices) { return new Promise((resolve, reject) => { - ml.jobs + mlApiServices.jobs .groups() .then((resp) => { resolve(resp.map((group) => group.id)); @@ -51,9 +50,10 @@ function getGroupIds() { }); } -function getCalendars() { +function getCalendars(mlApiServices) { return new Promise((resolve, reject) => { - ml.calendars() + mlApiServices + .calendars() .then((resp) => { resolve(resp); }) @@ -71,13 +71,13 @@ function getCalendars() { }); } -export function getCalendarSettingsData() { +export function getCalendarSettingsData(mlApiServices) { return new Promise(async (resolve, reject) => { try { const [jobIds, groupIds, calendars] = await Promise.all([ - getJobIds(), - getGroupIds(), - getCalendars(), + getJobIds(mlApiServices), + getGroupIds(mlApiServices), + getCalendars(mlApiServices), ]); resolve({ diff --git a/x-pack/plugins/ml/public/application/settings/calendars/list/__snapshots__/calendars_list.test.js.snap b/x-pack/plugins/ml/public/application/settings/calendars/list/__snapshots__/calendars_list.test.js.snap deleted file mode 100644 index ff4b38527bd63..0000000000000 --- a/x-pack/plugins/ml/public/application/settings/calendars/list/__snapshots__/calendars_list.test.js.snap +++ /dev/null @@ -1,66 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`CalendarsList Renders calendar list with calendars 1`] = ` - -
- - -
- -
-`; diff --git a/x-pack/plugins/ml/public/application/settings/calendars/list/calendars_list.js b/x-pack/plugins/ml/public/application/settings/calendars/list/calendars_list.js index 27c6b4de8389c..413aae794fc15 100644 --- a/x-pack/plugins/ml/public/application/settings/calendars/list/calendars_list.js +++ b/x-pack/plugins/ml/public/application/settings/calendars/list/calendars_list.js @@ -12,13 +12,11 @@ import { EuiConfirmModal, EUI_MODAL_CONFIRM_BUTTON } from '@elastic/eui'; import { CalendarsListHeader } from './header'; import { CalendarsListTable } from './table'; -import { ml } from '../../../services/ml_api_service'; import { toastNotificationServiceProvider } from '../../../services/toast_notification_service'; import { mlNodesAvailable } from '../../../ml_nodes_check/check_ml_nodes'; import { deleteCalendars } from './delete_calendars'; import { i18n } from '@kbn/i18n'; import { withKibana } from '@kbn/kibana-react-plugin/public'; -import { getDocLinks } from '../../../util/dependency_cache'; import { HelpMenu } from '../../../components/help_menu'; export class CalendarsListUI extends Component { @@ -40,6 +38,7 @@ export class CalendarsListUI extends Component { } loadCalendars = async () => { + const ml = this.props.kibana.services.mlServices.mlApiServices; this.setState({ loading: true }); try { @@ -82,10 +81,12 @@ export class CalendarsListUI extends Component { }; deleteCalendars = () => { + const ml = this.props.kibana.services.mlServices.mlApiServices; + const toasts = this.props.kibana.services.notifications.toasts; const { selectedForDeletion } = this.state; this.closeDestroyModal(); - deleteCalendars(selectedForDeletion, this.loadCalendars); + deleteCalendars(ml, toasts, selectedForDeletion, this.loadCalendars); }; addRequiredFieldsToList = (calendarsList = []) => { @@ -106,7 +107,7 @@ export class CalendarsListUI extends Component { const { canCreateCalendar, canDeleteCalendar } = this.props; let destroyModal = ''; - const helpLink = getDocLinks().links.ml.calendars; + const helpLink = this.props.kibana.services.docLinks.links.ml.calendars; if (this.state.isDestroyModalVisible) { destroyModal = ( diff --git a/x-pack/plugins/ml/public/application/settings/calendars/list/calendars_list.test.js b/x-pack/plugins/ml/public/application/settings/calendars/list/calendars_list.test.js index bbfdf7ef6b0e4..970c1afbe4fbc 100644 --- a/x-pack/plugins/ml/public/application/settings/calendars/list/calendars_list.test.js +++ b/x-pack/plugins/ml/public/application/settings/calendars/list/calendars_list.test.js @@ -6,21 +6,28 @@ */ import React from 'react'; -import { shallowWithIntl } from '@kbn/test-jest-helpers'; -import { ml } from '../../../services/ml_api_service'; +import { render, screen, waitFor } from '@testing-library/react'; +import { cloneDeep } from 'lodash'; import { CalendarsList } from './calendars_list'; +// Mocking the child components to just assert that they get the data +// received via the async call using mlApiServices in the main component. jest.mock('../../../components/help_menu', () => ({ - HelpMenu: () =>
, + HelpMenu: ({ docLink }) =>
, })); - -jest.mock('../../../util/dependency_cache', () => ({ - getDocLinks: () => ({ - links: { - ml: { calendars: jest.fn() }, - }, - }), +jest.mock('./header', () => ({ + CalendarsListHeader: ({ totalCount }) => ( +
{totalCount}
+ ), +})); +jest.mock('./table', () => ({ + CalendarsListTable: ({ calendarsList }) => ( +
+ ), })); jest.mock('../../../capabilities/check_capabilities', () => ({ @@ -36,100 +43,100 @@ jest.mock('../../../capabilities/get_capabilities', () => ({ jest.mock('../../../ml_nodes_check/check_ml_nodes', () => ({ mlNodesAvailable: () => true, })); -jest.mock('../../../services/ml_api_service', () => ({ - ml: { - calendars: () => { - return Promise.resolve([]); - }, - delete: jest.fn(), - }, -})); - -jest.mock('react', () => { - const r = jest.requireActual('react'); - return { ...r, memo: (x) => x }; -}); -jest.mock('@kbn/kibana-react-plugin/public', () => ({ - withKibana: (node) => { - return node; +const mockCalendars = [ + { + calendar_id: 'farequote-calendar', + job_ids: ['farequote'], + description: 'test ', + events: [ + { + description: 'Downtime feb 9 2017 10:10 to 10:30', + start_time: 1486656600000, + end_time: 1486657800000, + calendar_id: 'farequote-calendar', + event_id: 'Ee-YgGcBxHgQWEhCO_xj', + }, + ], }, -})); - -const testingState = { - loading: false, - calendars: [ - { - calendar_id: 'farequote-calendar', - job_ids: ['farequote'], - description: 'test ', - events: [ - { - description: 'Downtime feb 9 2017 10:10 to 10:30', - start_time: 1486656600000, - end_time: 1486657800000, - calendar_id: 'farequote-calendar', - event_id: 'Ee-YgGcBxHgQWEhCO_xj', + { + calendar_id: 'this-is-a-new-calendar', + job_ids: ['test'], + description: 'new calendar', + events: [ + { + description: 'New event!', + start_time: 1544076000000, + end_time: 1544162400000, + calendar_id: 'this-is-a-new-calendar', + event_id: 'ehWKhGcBqHkXuWNrIrSV', + }, + ], + }, +]; +// need to pass in a copy of mockCalendars because it will be mutated +const mockCalendarsFn = jest.fn(() => Promise.resolve(cloneDeep(mockCalendars))); +const mockKibanaProp = { + services: { + docLinks: { links: { ml: { calendars: 'https://calendars' } } }, + mlServices: { mlApiServices: { calendars: mockCalendarsFn } }, + data: { + query: { + timefilter: { + timefilter: { + disableTimeRangeSelector: jest.fn(), + disableAutoRefreshSelector: jest.fn(), + }, }, - ], + }, }, - { - calendar_id: 'this-is-a-new-calendar', - job_ids: ['test'], - description: 'new calendar', - events: [ - { - description: 'New event!', - start_time: 1544076000000, - end_time: 1544162400000, - calendar_id: 'this-is-a-new-calendar', - event_id: 'ehWKhGcBqHkXuWNrIrSV', - }, - ], + notifications: { + toasts: { + addDanger: jest.fn(), + }, }, - ], - isDestroyModalVisible: false, - calendarId: null, - selectedForDeletion: [], - nodesAvailable: true, + }, }; +const mockReact = React; +jest.mock('@kbn/kibana-react-plugin/public', () => ({ + withKibana: (type) => { + const EnhancedType = (props) => { + return mockReact.createElement(type, { + ...props, + kibana: mockKibanaProp, + }); + }; + return EnhancedType; + }, +})); + const props = { canCreateCalendar: true, canDeleteCalendar: true, - kibana: { - services: { - data: { - query: { - timefilter: { - timefilter: { - disableTimeRangeSelector: jest.fn(), - disableAutoRefreshSelector: jest.fn(), - }, - }, - }, - }, - notifications: { - toasts: { - addDanger: () => {}, - }, - }, - }, - }, }; describe('CalendarsList', () => { - test('loads calendars on mount', () => { - ml.calendars = jest.fn(() => []); - shallowWithIntl(); + test('Renders calendar list with calendars', async () => { + render(); - expect(ml.calendars).toHaveBeenCalled(); - }); + await waitFor(() => { + // Select element by data-test-subj and assert text content + const calendarsListHeaderElement = screen.getByTestId('mockCalendarsListHeader'); + expect(calendarsListHeaderElement).toHaveTextContent('2'); + + // Select element by data-test-subj and assert data attributes + const calendarsListTableElement = screen.getByTestId('mockCalendarsListTable'); + const calendarListData = JSON.parse( + calendarsListTableElement.getAttribute('data-calendar-list') + ); - test('Renders calendar list with calendars', () => { - const wrapper = shallowWithIntl(); - wrapper.instance().setState(testingState); - wrapper.update(); - expect(wrapper).toMatchSnapshot(); + const expectedCalendarsData = cloneDeep(mockCalendars); + expectedCalendarsData[0].events_length = 1; + expectedCalendarsData[0].job_ids_string = 'farequote'; + expectedCalendarsData[1].events_length = 1; + expectedCalendarsData[1].job_ids_string = 'test'; + expect(calendarListData).toEqual(expectedCalendarsData); + }); }); }); diff --git a/x-pack/plugins/ml/public/application/settings/calendars/list/delete_calendars.js b/x-pack/plugins/ml/public/application/settings/calendars/list/delete_calendars.js index a767d7b65e4f3..13de491f10825 100644 --- a/x-pack/plugins/ml/public/application/settings/calendars/list/delete_calendars.js +++ b/x-pack/plugins/ml/public/application/settings/calendars/list/delete_calendars.js @@ -8,14 +8,15 @@ import { i18n } from '@kbn/i18n'; import { extractErrorMessage } from '@kbn/ml-error-utils'; -import { getToastNotifications } from '../../../util/dependency_cache'; -import { ml } from '../../../services/ml_api_service'; - -export async function deleteCalendars(calendarsToDelete, callback) { +export async function deleteCalendars( + mlApiServices, + toastNotifications, + calendarsToDelete, + callback +) { if (calendarsToDelete === undefined || calendarsToDelete.length === 0) { return; } - const toastNotifications = getToastNotifications(); // Delete each of the specified calendars in turn, waiting for each response // before deleting the next to minimize load on the cluster. @@ -36,7 +37,7 @@ export async function deleteCalendars(calendarsToDelete, callback) { for (const calendar of calendarsToDelete) { const calendarId = calendar.calendar_id; try { - await ml.deleteCalendar({ calendarId }); + await mlApiServices.deleteCalendar({ calendarId }); } catch (error) { console.log('Error deleting calendar:', error); toastNotifications.addDanger({ diff --git a/x-pack/plugins/ml/public/application/settings/filter_lists/components/add_item_popover/__snapshots__/add_item_popover.test.js.snap b/x-pack/plugins/ml/public/application/settings/filter_lists/components/add_item_popover/__snapshots__/add_item_popover.test.js.snap index 61cbe9f028874..c29a40eaba6b9 100644 --- a/x-pack/plugins/ml/public/application/settings/filter_lists/components/add_item_popover/__snapshots__/add_item_popover.test.js.snap +++ b/x-pack/plugins/ml/public/application/settings/filter_lists/components/add_item_popover/__snapshots__/add_item_popover.test.js.snap @@ -31,7 +31,9 @@ exports[`AddItemPopover calls addItems with multiple items on clicking Add butto panelPaddingSize="m" repositionToCrossAxis={true} > - + - + - + - + -
- - - - - - - - - - - - - - - - -
- - -`; - -exports[`EditFilterList renders after selecting an item and deleting it 1`] = ` - -
- - - - - - - - - - - - - - - - -
- -
-`; - -exports[`EditFilterList renders after selecting an item and deleting it 2`] = ` - -
- - - - - - - - - - - - - - - - -
- -
-`; - -exports[`EditFilterList renders the edit page for a new filter list and updates ID 1`] = ` - -
- - - - - - - - - - - - - - - - -
- -
-`; - -exports[`EditFilterList renders the edit page for a new filter list and updates ID 2`] = ` - -
- - - - - - - - - - - - - - - - -
- -
-`; - -exports[`EditFilterList renders the edit page for an existing filter list and updates description 1`] = ` - -
- - - - - - - - - - - - - - - - -
- -
-`; - -exports[`EditFilterList renders the edit page for an existing filter list and updates description 2`] = ` - -
- - - - - - - - - - - - - - - - -
- -
-`; - -exports[`EditFilterList updates the items per page 1`] = ` - -
- - - - - - - - - - - - - - - - -
- -
-`; diff --git a/x-pack/plugins/ml/public/application/settings/filter_lists/edit/edit_filter_list.js b/x-pack/plugins/ml/public/application/settings/filter_lists/edit/edit_filter_list.js index d05484a9d3b89..2f53963a5b3f2 100644 --- a/x-pack/plugins/ml/public/application/settings/filter_lists/edit/edit_filter_list.js +++ b/x-pack/plugins/ml/public/application/settings/filter_lists/edit/edit_filter_list.js @@ -30,10 +30,8 @@ import { EditFilterListHeader } from './header'; import { EditFilterListToolbar } from './toolbar'; import { ItemsGrid } from '../../../components/items_grid'; import { isValidFilterListId, saveFilterList } from './utils'; -import { ml } from '../../../services/ml_api_service'; import { toastNotificationServiceProvider } from '../../../services/toast_notification_service'; import { ML_PAGES } from '../../../../../common/constants/locator'; -import { getDocLinks } from '../../../util/dependency_cache'; import { HelpMenu } from '../../../components/help_menu'; const DEFAULT_ITEMS_PER_PAGE = 50; @@ -116,6 +114,7 @@ export class EditFilterListUI extends Component { }; loadFilterList = (filterId) => { + const ml = this.props.kibana.services.mlServices.mlApiServices; ml.filters .filters({ filterId }) .then((filter) => { @@ -285,7 +284,14 @@ export class EditFilterListUI extends Component { const { loadedFilter, newFilterId, description, items } = this.state; const filterId = this.props.filterId !== undefined ? this.props.filterId : newFilterId; - saveFilterList(filterId, description, items, loadedFilter) + saveFilterList( + this.props.kibana.services.notifications.toasts, + this.props.kibana.services.mlServices.mlApiServices, + filterId, + description, + items, + loadedFilter + ) .then((savedFilter) => { this.setLoadedFilterState(savedFilter); this.returnToFiltersList(); @@ -321,7 +327,7 @@ export class EditFilterListUI extends Component { const totalItemCount = items !== undefined ? items.length : 0; - const helpLink = getDocLinks().links.ml.customRules; + const helpLink = this.props.kibana.services.docLinks.links.ml.customRules; return ( <> diff --git a/x-pack/plugins/ml/public/application/settings/filter_lists/edit/edit_filter_list.test.js b/x-pack/plugins/ml/public/application/settings/filter_lists/edit/edit_filter_list.test.js index e3e740f1f7d78..a277457151fa7 100644 --- a/x-pack/plugins/ml/public/application/settings/filter_lists/edit/edit_filter_list.test.js +++ b/x-pack/plugins/ml/public/application/settings/filter_lists/edit/edit_filter_list.test.js @@ -5,22 +5,16 @@ * 2.0. */ -jest.mock('../../../components/help_menu', () => ({ - HelpMenu: () =>
, -})); +import React from 'react'; +import { render, waitFor, within } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import '@testing-library/jest-dom/extend-expect'; +import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; -jest.mock('../../../util/dependency_cache', () => ({ - getDocLinks: () => ({ - links: { - ml: { customRules: jest.fn() }, - }, - }), -})); +import { EditFilterList } from './edit_filter_list'; -// Define the required mocks used for loading, saving and validating the filter list. -jest.mock('./utils', () => ({ - isValidFilterListId: () => true, - saveFilterList: jest.fn(), +jest.mock('../../../components/help_menu', () => ({ + HelpMenu: () =>
, })); // Mock the call for loading the list of filters. @@ -35,104 +29,243 @@ const mockTestFilter = { jobs: ['dns_exfiltration'], }, }; -jest.mock('../../../services/ml_api_service', () => ({ - ml: { - filters: { - filters: () => { - return Promise.resolve(mockTestFilter); +const mockFilters = jest.fn().mockImplementation(() => Promise.resolve(mockTestFilter)); +const mockKibanaContext = { + services: { + docLinks: { links: { ml: { customRules: 'test' } } }, + notifications: { toasts: { addDanger: jest.fn(), addError: jest.fn() } }, + mlServices: { + mlApiServices: { + filters: { + filters: mockFilters, + }, }, }, }, -})); +}; +const mockReact = React; jest.mock('@kbn/kibana-react-plugin/public', () => ({ - withKibana: (node) => { - return node; + withKibana: (type) => { + const EnhancedType = (props) => { + return mockReact.createElement(type, { + ...props, + kibana: mockKibanaContext, + }); + }; + return EnhancedType; }, })); -import { shallowWithIntl } from '@kbn/test-jest-helpers'; -import React from 'react'; - -import { EditFilterList } from './edit_filter_list'; - const props = { canCreateFilter: true, canDeleteFilter: true, - kibana: { - services: { - notifications: { - toasts: { - addWarning: () => {}, - }, - }, - }, - }, }; -function prepareEditTest() { - const wrapper = shallowWithIntl(); +describe('EditFilterList', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('renders the edit page for a new filter list and updates ID', async () => { + const { getByTestId, getByText } = render( + + + + ); - // Cannot find a way to generate the snapshot after the Promise in the mock ml.filters - // has resolved. - // So set the loaded filter state directly to ensure the snapshot is generated against - // the test filter and not the default empty state. - const instance = wrapper.instance(); - instance.setLoadedFilterState(mockTestFilter); - wrapper.update(); + // The filter list should be empty. + expect(getByText('No items have been added')).toBeInTheDocument(); - return wrapper; -} + const mlNewFilterListIdInput = getByTestId('mlNewFilterListIdInput'); + expect(mlNewFilterListIdInput).toBeInTheDocument(); -describe('EditFilterList', () => { - test('renders the edit page for a new filter list and updates ID', () => { - const wrapper = shallowWithIntl(); - expect(wrapper).toMatchSnapshot(); - - const instance = wrapper.instance(); - instance.updateNewFilterId('new_filter_list'); - wrapper.update(); - expect(wrapper).toMatchSnapshot(); + await userEvent.type(mlNewFilterListIdInput, 'new_filter_list'); + + await waitFor(() => { + expect(mlNewFilterListIdInput).toHaveValue('new_filter_list'); + }); + + // After entering a valid ID, the save button should be enabled. + expect(getByTestId('mlFilterListSaveButton')).toBeEnabled(); + + await userEvent.clear(mlNewFilterListIdInput); + + // Emptied again, the save button should be disabled. + await waitFor(() => { + expect(getByTestId('mlFilterListSaveButton')).toBeDisabled(); + }); + + await userEvent.type(mlNewFilterListIdInput, '#invalid#$%^', { delay: 1 }); + + await waitFor(() => { + expect(mlNewFilterListIdInput).toHaveValue('#invalid#$%^'); + }); + + // After entering an invalid ID, the save button should still be disabled. + await waitFor(() => { + expect(getByTestId('mlFilterListSaveButton')).toBeDisabled(); + }); + + expect(mockFilters).toHaveBeenCalledTimes(0); }); - test('renders the edit page for an existing filter list and updates description', () => { - const wrapper = prepareEditTest(); - expect(wrapper).toMatchSnapshot(); + // There is a bug in `v13.5.0` of `@testing-library/user-event` that doesn't + // allow to click on elements that (wrongly ?) inherit pointer-events. + // A PR to update the lib is up here: https://github.com/elastic/kibana/pull/189949 + test.skip('renders the edit page for an existing filter list and updates description', async () => { + const { getByTestId } = render( + + + + ); + + expect(mockFilters).toHaveBeenCalledWith({ filterId: 'safe_domains' }); + + waitFor(() => { + expect(getByTestId('mlNewFilterListDescriptionText')).toHaveValue( + 'List of known safe domains' + ); + }); + + const mlFilterListEditDescriptionButton = getByTestId('mlFilterListEditDescriptionButton'); + + expect(mlFilterListEditDescriptionButton).toBeInTheDocument(); - const instance = wrapper.instance(); - instance.updateDescription('Known safe web domains'); - wrapper.update(); - expect(wrapper).toMatchSnapshot(); + // Workaround with `pointerEventsCheck` so we don't get "Error: unable to click element as it has or inherits pointer-events set to "none"." + await userEvent.click(mlFilterListEditDescriptionButton, { pointerEventsCheck: 0 }); + + const mlFilterListDescriptionInput = getByTestId('mlFilterListDescriptionInput'); + + waitFor(() => { + expect(mlFilterListDescriptionInput).toBeInTheDocument(); + expect(mlFilterListDescriptionInput).toHaveValue('List of known safe domains'); + }); + + await userEvent.clear(mlFilterListDescriptionInput); + await userEvent.type(mlFilterListDescriptionInput, 'Known safe web domains'); + await userEvent.click(mlFilterListEditDescriptionButton); + + waitFor(() => { + expect(getByTestId('mlNewFilterListDescriptionText')).toHaveValue('Known safe web domains'); + }); }); - test('updates the items per page', () => { - const wrapper = prepareEditTest(); - const instance = wrapper.instance(); + test('updates the items per page', async () => { + const { findByText, findByTestId, getByTestId, queryByText } = render( + + + + ); + + expect(mockFilters).toHaveBeenCalledWith({ filterId: 'safe_domains' }); + + // Use findByText to be able to wait for the page to be updated. + expect(await findByText('Items per page: 50')).toBeInTheDocument(); + + const mlItemsGridPaginationPopover = getByTestId('mlItemsGridPaginationPopover'); + expect(mlItemsGridPaginationPopover).toBeInTheDocument(); - instance.setItemsPerPage(500); - wrapper.update(); - expect(wrapper).toMatchSnapshot(); + // Click to open the popover + await userEvent.click(mlItemsGridPaginationPopover.querySelector('button')); + + // Use findByText to be able to wait for the page to be updated. + expect(await findByTestId('mlItemsGridPaginationMenuPanel')).toBeInTheDocument(); + // The popover should include the option for 500 items. + expect(await findByText('500 items')).toBeInTheDocument(); + + // Next we want to click the '500 items' button. + const mlItemsGridPaginationMenuPanel = getByTestId('mlItemsGridPaginationMenuPanel'); + const buttons = within(mlItemsGridPaginationMenuPanel).getAllByRole('button'); + expect(buttons.length).toBe(4); + await userEvent.click(buttons[2]); + + // Use findByText to be able to wait for the page to be updated. + expect(await queryByText('Items per page: 50')).not.toBeInTheDocument(); + expect(await findByText('Items per page: 500')).toBeInTheDocument(); }); - test('renders after selecting an item and deleting it', () => { - const wrapper = prepareEditTest(); - const instance = wrapper.instance(); + test('renders after selecting an item and deleting it', async () => { + const { findByText, getAllByTestId, getByTestId, queryByText } = render( + + + + ); + + expect(mockFilters).toHaveBeenCalledWith({ filterId: 'safe_domains' }); - instance.setItemSelected(mockTestFilter.items[1], true); - wrapper.update(); - expect(wrapper).toMatchSnapshot(); + // Use findByText to be able to wait for the page to be updated. + expect(await findByText('google.com')).toBeInTheDocument(); + expect(await findByText('google.co.uk')).toBeInTheDocument(); + expect(await findByText('elastic.co')).toBeInTheDocument(); + expect(await findByText('youtube.com')).toBeInTheDocument(); - instance.deleteSelectedItems(); - wrapper.update(); - expect(wrapper).toMatchSnapshot(); + const checkboxes = getAllByTestId('mlGridItemCheckbox'); + expect(checkboxes.length).toBe(4); + + // Click the checkbox for google.co.uk and then the delete button. + await userEvent.click(checkboxes[1]); + await userEvent.click(getByTestId('mlFilterListDeleteItemButton')); + + expect(await findByText('google.com')).toBeInTheDocument(); + expect(await queryByText('google.co.uk')).not.toBeInTheDocument(); + expect(await findByText('elastic.co')).toBeInTheDocument(); + expect(await findByText('youtube.com')).toBeInTheDocument(); + expect(getAllByTestId('mlGridItemCheckbox')).toHaveLength(3); }); - test('adds new items to filter list', () => { - const wrapper = prepareEditTest(); - const instance = wrapper.instance(); + // There is a bug in `v13.5.0` of `@testing-library/user-event` that doesn't + // allow to click on elements that (wrongly ?) inherit pointer-events. + // A PR to update the lib is up here: https://github.com/elastic/kibana/pull/189949 + test.skip('adds new items to filter list', async () => { + const { getByTestId, getByText, findByText, findByTestId, queryByTestId, queryByText } = render( + + + + ); + + expect(mockFilters).toHaveBeenCalledWith({ filterId: 'safe_domains' }); + + // Use findByText to be able to wait for the page to be updated. + expect(await findByText('google.com')).toBeInTheDocument(); + expect(await findByText('google.co.uk')).toBeInTheDocument(); + expect(await findByText('elastic.co')).toBeInTheDocument(); + expect(await findByText('youtube.com')).toBeInTheDocument(); + expect(await queryByText('amazon.com')).not.toBeInTheDocument(); + expect(await queryByText('spotify.com')).not.toBeInTheDocument(); + + const mlFilterListOpenNewItemsPopoverButton = queryByTestId( + 'mlFilterListOpenNewItemsPopoverButton' + ); + expect(mlFilterListOpenNewItemsPopoverButton).toBeInTheDocument(); + await userEvent.click(mlFilterListOpenNewItemsPopoverButton); + + // Assert that the popover was opened. + expect(await findByTestId('mlFilterListAddItemPopoverContent')).toBeInTheDocument(); + + // Assert that the textarea is present and empty. + const mlFilterListAddItemTextArea = getByTestId('mlFilterListAddItemTextArea'); + expect(mlFilterListAddItemTextArea).toBeInTheDocument(); + expect(mlFilterListAddItemTextArea).toHaveValue(''); + + // Assert that the add items button prenset but disabled. + const mlFilterListAddItemsButton = getByTestId('mlFilterListAddItemsButton'); + expect(mlFilterListAddItemsButton).toBeInTheDocument(); + expect(mlFilterListAddItemsButton).toBeDisabled(); + + // Enter items in the textarea and click the add items button + await userEvent.type(mlFilterListAddItemTextArea, 'amazon.com\nspotify.com'); + await userEvent.click(mlFilterListAddItemsButton); + + // Assert that the popover is closed again + expect(await queryByTestId('mlFilterListAddItemPopover')).not.toBeInTheDocument(); - instance.addItems(['amazon.com', 'spotify.com']); - wrapper.update(); - expect(wrapper).toMatchSnapshot(); + // Assert that the item grid has been updated. + expect(getByText('google.com')).toBeInTheDocument(); + expect(getByText('google.co.uk')).toBeInTheDocument(); + expect(getByText('elastic.co')).toBeInTheDocument(); + expect(getByText('youtube.com')).toBeInTheDocument(); + expect(getByText('amazon.com')).toBeInTheDocument(); + expect(getByText('spotify.com')).toBeInTheDocument(); }); }); diff --git a/x-pack/plugins/ml/public/application/settings/filter_lists/edit/utils.js b/x-pack/plugins/ml/public/application/settings/filter_lists/edit/utils.js index 2565a53411298..e1f42e2e636ad 100644 --- a/x-pack/plugins/ml/public/application/settings/filter_lists/edit/utils.js +++ b/x-pack/plugins/ml/public/application/settings/filter_lists/edit/utils.js @@ -6,9 +6,7 @@ */ import { i18n } from '@kbn/i18n'; -import { getToastNotifications } from '../../../util/dependency_cache'; import { isJobIdValid } from '../../../../../common/util/job_utils'; -import { ml } from '../../../services/ml_api_service'; export function isValidFilterListId(id) { // Filter List ID requires the same format as a Job ID, therefore isJobIdValid can be used @@ -17,11 +15,18 @@ export function isValidFilterListId(id) { // Saves a filter list, running an update if the supplied loadedFilterList, holding the // original filter list to which edits are being applied, is defined with a filter_id property. -export function saveFilterList(filterId, description, items, loadedFilterList) { +export function saveFilterList( + toastNotifications, + mlApiServices, + filterId, + description, + items, + loadedFilterList +) { return new Promise((resolve, reject) => { if (loadedFilterList === undefined || loadedFilterList.filter_id === undefined) { // Create a new filter. - addFilterList(filterId, description, items) + addFilterList(toastNotifications, mlApiServices, filterId, description, items) .then((newFilter) => { resolve(newFilter); }) @@ -30,7 +35,7 @@ export function saveFilterList(filterId, description, items, loadedFilterList) { }); } else { // Edit to existing filter. - updateFilterList(loadedFilterList, description, items) + updateFilterList(mlApiServices, loadedFilterList, description, items) .then((updatedFilter) => { resolve(updatedFilter); }) @@ -41,7 +46,7 @@ export function saveFilterList(filterId, description, items, loadedFilterList) { }); } -export function addFilterList(filterId, description, items) { +export function addFilterList(toastNotifications, mlApiServices, filterId, description, items) { const filterWithIdExistsErrorMessage = i18n.translate( 'xpack.ml.settings.filterLists.filterWithIdExistsErrorMessage', { @@ -54,13 +59,13 @@ export function addFilterList(filterId, description, items) { return new Promise((resolve, reject) => { // First check the filterId isn't already in use by loading the current list of filters. - ml.filters + mlApiServices.filters .filtersStats() .then((filterLists) => { const savedFilterIds = filterLists.map((filterList) => filterList.filter_id); if (savedFilterIds.indexOf(filterId) === -1) { // Save the new filter. - ml.filters + mlApiServices.filters .addFilter(filterId, description, items) .then((newFilter) => { resolve(newFilter); @@ -69,7 +74,6 @@ export function addFilterList(filterId, description, items) { reject(error); }); } else { - const toastNotifications = getToastNotifications(); toastNotifications.addDanger(filterWithIdExistsErrorMessage); reject(new Error(filterWithIdExistsErrorMessage)); } @@ -80,14 +84,14 @@ export function addFilterList(filterId, description, items) { }); } -export function updateFilterList(loadedFilterList, description, items) { +export function updateFilterList(mlApiServices, loadedFilterList, description, items) { return new Promise((resolve, reject) => { // Get items added and removed from loaded filter. const loadedItems = loadedFilterList.items; const addItems = items.filter((item) => loadedItems.includes(item) === false); const removeItems = loadedItems.filter((item) => items.includes(item) === false); - ml.filters + mlApiServices.filters .updateFilter(loadedFilterList.filter_id, description, addItems, removeItems) .then((updatedFilter) => { resolve(updatedFilter); diff --git a/x-pack/plugins/ml/public/application/settings/filter_lists/list/__snapshots__/filter_lists.test.js.snap b/x-pack/plugins/ml/public/application/settings/filter_lists/list/__snapshots__/filter_lists.test.js.snap deleted file mode 100644 index 3078fb3d504c9..0000000000000 --- a/x-pack/plugins/ml/public/application/settings/filter_lists/list/__snapshots__/filter_lists.test.js.snap +++ /dev/null @@ -1,38 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Filter Lists renders a list of filters 1`] = ` - -
- - -
- -
-`; diff --git a/x-pack/plugins/ml/public/application/settings/filter_lists/list/filter_lists.js b/x-pack/plugins/ml/public/application/settings/filter_lists/list/filter_lists.js index a55473a731322..c955cce8f79d4 100644 --- a/x-pack/plugins/ml/public/application/settings/filter_lists/list/filter_lists.js +++ b/x-pack/plugins/ml/public/application/settings/filter_lists/list/filter_lists.js @@ -18,10 +18,8 @@ import { withKibana } from '@kbn/kibana-react-plugin/public'; import { FilterListsHeader } from './header'; import { FilterListsTable } from './table'; -import { ml } from '../../../services/ml_api_service'; import { toastNotificationServiceProvider } from '../../../services/toast_notification_service'; -import { getDocLinks } from '../../../util/dependency_cache'; import { HelpMenu } from '../../../components/help_menu'; export class FilterListsUI extends Component { @@ -64,6 +62,7 @@ export class FilterListsUI extends Component { }; refreshFilterLists = () => { + const ml = this.props.kibana.services.mlServices.mlApiServices; // Load the list of filters. ml.filters .filtersStats() @@ -97,7 +96,7 @@ export class FilterListsUI extends Component { render() { const { filterLists, selectedFilterLists } = this.state; const { canCreateFilter, canDeleteFilter } = this.props; - const helpLink = getDocLinks().links.ml.customRules; + const helpLink = this.props.kibana.services.docLinks.links.ml.customRules; return ( <> diff --git a/x-pack/plugins/ml/public/application/settings/filter_lists/list/filter_lists.test.js b/x-pack/plugins/ml/public/application/settings/filter_lists/list/filter_lists.test.js index 5d0306564e312..e4a27809019b5 100644 --- a/x-pack/plugins/ml/public/application/settings/filter_lists/list/filter_lists.test.js +++ b/x-pack/plugins/ml/public/application/settings/filter_lists/list/filter_lists.test.js @@ -5,50 +5,69 @@ * 2.0. */ -import { shallowWithIntl } from '@kbn/test-jest-helpers'; import React from 'react'; +import { render, screen, waitFor } from '@testing-library/react'; +import '@testing-library/jest-dom/extend-expect'; import { FilterLists } from './filter_lists'; +// Mocking the child components to just assert that they get the data +// received via the async call using mlApiServices in the main component. jest.mock('../../../components/help_menu', () => ({ - HelpMenu: () =>
, + HelpMenu: ({ docLink }) =>
, })); - -jest.mock('../../../util/dependency_cache', () => ({ - getDocLinks: () => ({ - links: { - ml: { customRules: jest.fn() }, - }, - }), +jest.mock('./header', () => ({ + FilterListsHeader: ({ totalCount }) => ( +
{totalCount}
+ ), +})); +jest.mock('./table', () => ({ + FilterListsTable: ({ filterLists, selectedFilterLists }) => ( +
+ ), })); jest.mock('../../../capabilities/check_capabilities', () => ({ checkPermission: () => true, })); -jest.mock('@kbn/kibana-react-plugin/public', () => ({ - withKibana: (node) => { - return node; - }, -})); - // Mock the call for loading the list of filters. -// The mock is hoisted to the top, so need to prefix the filter variable -// with 'mock' so it can be used lazily. const mockTestFilter = { filter_id: 'safe_domains', description: 'List of known safe domains', item_count: 500, used_by: { jobs: ['dns_exfiltration'] }, }; -jest.mock('../../../services/ml_api_service', () => ({ - ml: { - filters: { - filtersStats: () => { - return Promise.resolve([mockTestFilter]); +const mockKibanaProp = { + services: { + docLinks: { links: { ml: { customRules: 'https://customRules' } } }, + mlServices: { + mlApiServices: { + filters: { + filtersStats: () => { + return Promise.resolve([mockTestFilter]); + }, + }, }, }, }, +}; + +const mockReact = React; +jest.mock('@kbn/kibana-react-plugin/public', () => ({ + withKibana: (type) => { + const EnhancedType = (props) => { + return mockReact.createElement(type, { + ...props, + kibana: mockKibanaProp, + }); + }; + return EnhancedType; + }, })); const props = { @@ -57,15 +76,23 @@ const props = { }; describe('Filter Lists', () => { - test('renders a list of filters', () => { - const wrapper = shallowWithIntl(); + test('renders a list of filters', async () => { + render(); + + // Wait for the elements to appear + await waitFor(() => { + expect(screen.getByTestId('mockFilterListsHeader')).toHaveTextContent('1'); + }); + + // Assert that the child components receive the data based on async calls and kibana context. + const filterListsTableElement = screen.getByTestId('mockFilterListsTable'); + expect(filterListsTableElement).toHaveAttribute( + 'data-filter-lists', + JSON.stringify([mockTestFilter]) + ); + expect(filterListsTableElement).toHaveAttribute('data-selected-filter-lists', '[]'); - // Cannot find a way to generate the snapshot after the Promise in the mock ml.filters - // has resolved. - // So set the filter lists directly to ensure the snapshot is generated against - // the test list and not the default empty state. - wrapper.instance().setFilterLists([mockTestFilter]); - wrapper.update(); - expect(wrapper).toMatchSnapshot(); + const helpMenuElement = screen.getByTestId('mockHelpMenu'); + expect(helpMenuElement).toHaveAttribute('data-link', 'https://customRules'); }); }); diff --git a/x-pack/plugins/ml/public/application/settings/settings.test.tsx b/x-pack/plugins/ml/public/application/settings/settings.test.tsx index 89ad2a965df91..38ef8c784c114 100644 --- a/x-pack/plugins/ml/public/application/settings/settings.test.tsx +++ b/x-pack/plugins/ml/public/application/settings/settings.test.tsx @@ -16,22 +16,19 @@ jest.mock('../components/help_menu', () => ({ })); jest.mock('../contexts/kibana', () => ({ - useNotifications: () => { - return { - toasts: { addDanger: jest.fn(), addError: jest.fn() }, - }; - }, - useMlKibana: () => { - return { - services: { - docLinks: { - links: { - ml: { guide: jest.fn() }, - }, + useNotifications: () => ({ + toasts: { addDanger: jest.fn(), addError: jest.fn() }, + }), + useMlApiContext: jest.fn(), + useMlKibana: () => ({ + services: { + docLinks: { + links: { + ml: { guide: jest.fn() }, }, }, - }; - }, + }, + }), })); jest.mock('../contexts/kibana/use_create_url', () => ({ 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 6ad84f892eb31..3a44cd19f645d 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 @@ -10,7 +10,7 @@ import { EuiFlexItem, EuiFormRow, EuiSelect } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { ML_JOB_AGGREGATION } from '@kbn/ml-anomaly-utils'; import type { MlJob } from '@elastic/elasticsearch/lib/api/types'; -import { mlJobService } from '../../../services/job_service'; +import { useMlJobService } from '../../../services/job_service'; import { getFunctionDescription, isMetricDetector } from '../../get_function_description'; import { useToastNotificationService } from '../../../services/toast_notification_service'; import { useMlResultsService } from '../../../services/results_service'; @@ -56,19 +56,18 @@ export const PlotByFunctionControls = ({ }) => { const toastNotificationService = useToastNotificationService(); const mlResultsService = useMlResultsService(); + const mlJobService = useMlJobService(); const getFunctionDescriptionToPlot = useCallback( async ( _selectedDetectorIndex: number, _selectedEntities: MlEntity | undefined, - _selectedJobId: string, _selectedJob: CombinedJob ) => { const functionToPlot = await getFunctionDescription( { selectedDetectorIndex: _selectedDetectorIndex, selectedEntities: _selectedEntities, - selectedJobId: _selectedJobId, selectedJob: _selectedJob, }, toastNotificationService, @@ -95,12 +94,7 @@ export const PlotByFunctionControls = ({ ) { const detector = selectedJob.analysis_config.detectors[selectedDetectorIndex]; if (detector?.function === ML_JOB_AGGREGATION.METRIC) { - getFunctionDescriptionToPlot( - selectedDetectorIndex, - selectedEntities, - selectedJobId, - selectedJob - ); + getFunctionDescriptionToPlot(selectedDetectorIndex, selectedEntities, selectedJob); } } // eslint-disable-next-line react-hooks/exhaustive-deps 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 ead36e4ffe3a4..1d9d61e3ddc32 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 @@ -16,7 +16,7 @@ import { useStorage } from '@kbn/ml-local-storage'; import type { MlEntityFieldType } from '@kbn/ml-anomaly-utils'; import type { MlJob } from '@elastic/elasticsearch/lib/api/types'; import { EntityControl } from '../entity_control'; -import { mlJobService } from '../../../services/job_service'; +import { useMlJobService } from '../../../services/job_service'; import type { CombinedJob, Detector, @@ -105,6 +105,7 @@ export const SeriesControls: FC> = ({ }, }, } = useMlKibana(); + const mlJobService = useMlJobService(); const selectedJob: CombinedJob | MlJob = useMemo( () => job ?? mlJobService.getJob(selectedJobId), @@ -128,7 +129,6 @@ export const SeriesControls: FC> = ({ return getControlsForDetector( selectedDetectorIndex, selectedEntities, - selectedJobId, selectedJob as CombinedJob ); // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js index d1e75c0bea9b0..7882fdfc21513 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js @@ -161,9 +161,18 @@ class TimeseriesChartIntl extends Component { rowMouseenterSubscriber = null; rowMouseleaveSubscriber = null; - constructor(props) { + constructor(props, constructorContext) { super(props); this.state = { popoverData: null, popoverCoords: [0, 0], showRuleEditorFlyout: () => {} }; + + this.mlTimeSeriesExplorer = timeSeriesExplorerServiceFactory( + constructorContext.services.uiSettings, + constructorContext.services.mlServices.mlApiServices, + constructorContext.services.mlServices.mlResultsService + ); + this.getTimeBuckets = timeBucketsServiceFactory( + constructorContext.services.uiSettings + ).getTimeBuckets; } componentWillUnmount() { @@ -179,15 +188,6 @@ class TimeseriesChartIntl extends Component { } componentDidMount() { - this.mlTimeSeriesExplorer = timeSeriesExplorerServiceFactory( - this.context.services.uiSettings, - this.context.services.mlServices.mlApiServices, - this.context.services.mlServices.mlResultsService - ); - this.getTimeBuckets = timeBucketsServiceFactory( - this.context.services.uiSettings - ).getTimeBuckets; - const { svgWidth, svgHeight } = this.props; const { focusHeight: focusHeightIncoming, focusChartHeight: focusChartIncoming } = svgHeight ? getChartHeights(svgHeight) diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.test.js b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.test.js index cdd8540dc22bf..1e9da4aa72787 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.test.js +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.test.js @@ -7,7 +7,7 @@ import moment from 'moment-timezone'; import { mountWithIntl } from '@kbn/test-jest-helpers'; -import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; +import { createKibanaReactContext } from '@kbn/kibana-react-plugin/public'; import React from 'react'; import { TimeseriesChart } from './timeseries_chart'; @@ -37,10 +37,6 @@ jest.mock('../../../util/time_series_explorer_service', () => ({ }, })); -jest.mock('../../../services/field_format_service', () => ({ - mlFieldFormatService: {}, -})); - function getTimeseriesChartPropsMock() { return { contextChartSelected: jest.fn(), @@ -55,12 +51,13 @@ function getTimeseriesChartPropsMock() { }; } -const servicesMock = { +const kibanaReactContextMock = createKibanaReactContext({ mlServices: { mlApiServices: {}, mlResultsService: {}, }, -}; + notifications: { toasts: { addDanger: jest.fn(), addSuccess: jest.fn() } }, +}); describe('TimeseriesChart', () => { const mockedGetBBox = { x: 0, y: -10, width: 40, height: 20 }; @@ -78,9 +75,9 @@ describe('TimeseriesChart', () => { const props = getTimeseriesChartPropsMock(); const wrapper = mountWithIntl( - + - + ); expect(wrapper.html()).toBe('
'); diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart_with_tooltip.tsx b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart_with_tooltip.tsx index 3a5fe63f7a81a..14c57cbb6c20e 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart_with_tooltip.tsx +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart_with_tooltip.tsx @@ -86,7 +86,7 @@ export const TimeSeriesChartWithTooltips: FC = useEffect(() => { let unmounted = false; - const entities = getControlsForDetector(detectorIndex, selectedEntities, selectedJob.job_id); + const entities = getControlsForDetector(detectorIndex, selectedEntities, selectedJob); const nonBlankEntities = Array.isArray(entities) ? entities.filter((entity) => entity.fieldValue !== null) : undefined; 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 214fe8bf45f47..f4dc6bceaa237 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 @@ -5,9 +5,8 @@ * 2.0. */ -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 { CombinedJob } from '../../../common/types/anomaly_detection_jobs'; import type { MlEntity } from '../../embeddables'; /** @@ -16,11 +15,8 @@ import type { MlEntity } from '../../embeddables'; export function getControlsForDetector( selectedDetectorIndex: number, selectedEntities: MlEntity | undefined, - selectedJobId: JobId, - job?: CombinedJob + selectedJob: CombinedJob ): Entity[] { - const selectedJob = job ?? mlJobService.getJob(selectedJobId); - const entities: Entity[] = []; if (selectedJob === undefined) { 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 274bfd3c6f27c..ed060c3c66dd4 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 @@ -34,12 +34,10 @@ export const getFunctionDescription = async ( { selectedDetectorIndex, selectedEntities, - selectedJobId, selectedJob, }: { selectedDetectorIndex: number; selectedEntities: MlEntity | undefined; - selectedJobId: string; selectedJob: CombinedJob; }, toastNotificationService: ToastNotificationService, @@ -52,7 +50,7 @@ export const getFunctionDescription = async ( const entityControls = getControlsForDetector( selectedDetectorIndex, selectedEntities, - selectedJobId + selectedJob ); const criteriaFields = getCriteriaFields(selectedDetectorIndex, entityControls); diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.d.ts b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.d.ts index ec201d29cdc3a..90dfe24946195 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.d.ts +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.d.ts @@ -9,20 +9,21 @@ import React from 'react'; import type { TimeRangeBounds } from '@kbn/ml-time-buckets'; -interface Props { +interface TimeSeriesExplorerProps { appStateHandler: (action: string, payload: any) => void; autoZoomDuration: number | undefined; bounds: TimeRangeBounds | undefined; - dateFormatTz: string; lastRefresh: number; - selectedJobId: string | undefined; - selectedDetectorIndex: number; - selectedEntities: Record | undefined; + previousRefresh?: number; + dateFormatTz: string; + selectedJobId: string; + selectedDetectorIndex?: number; + selectedEntities?: Record; selectedForecastId?: string; - tableInterval: string; - tableSeverity: number; + tableInterval?: string; + tableSeverity?: number; zoom?: { from?: string; to?: string }; } // eslint-disable-next-line react/prefer-stateless-function -declare class TimeSeriesExplorer extends React.Component {} +declare class TimeSeriesExplorer extends React.Component {} diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js index 0eb8182fa3e3c..4bfcb8e7fd98c 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js @@ -42,7 +42,6 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { context } from '@kbn/kibana-react-plugin/public'; import { getBoundsRoundedToInterval } from '@kbn/ml-time-buckets'; import { ResizeChecker } from '@kbn/kibana-utils-plugin/public'; -import { TimeSeriesExplorerHelpPopover } from './timeseriesexplorer_help_popover'; import { ANOMALIES_TABLE_DEFAULT_QUERY_SIZE } from '../../../common/constants/search'; import { @@ -56,18 +55,19 @@ import { import { AnnotationFlyout } from '../components/annotations/annotation_flyout'; import { AnnotationsTable } from '../components/annotations/annotations_table'; import { AnomaliesTable } from '../components/anomalies_table/anomalies_table'; -import { ForecastingModal } from './components/forecasting_modal/forecasting_modal'; import { LoadingIndicator } from '../components/loading_indicator/loading_indicator'; import { SelectInterval } from '../components/controls/select_interval/select_interval'; import { SelectSeverity } from '../components/controls/select_severity/select_severity'; -import { TimeseriesexplorerNoChartData } from './components/timeseriesexplorer_no_chart_data'; -import { TimeSeriesExplorerPage } from './timeseriesexplorer_page'; - -import { ml } from '../services/ml_api_service'; import { forecastServiceFactory } from '../services/forecast_service'; import { timeSeriesExplorerServiceFactory } from '../util/time_series_explorer_service'; -import { mlJobService } from '../services/job_service'; +import { mlJobServiceFactory } from '../services/job_service'; import { mlResultsServiceProvider } from '../services/results_service'; +import { toastNotificationServiceProvider } from '../services/toast_notification_service'; + +import { ForecastingModal } from './components/forecasting_modal/forecasting_modal'; +import { TimeseriesexplorerNoChartData } from './components/timeseriesexplorer_no_chart_data'; +import { TimeSeriesExplorerPage } from './timeseriesexplorer_page'; +import { TimeSeriesExplorerHelpPopover } from './timeseriesexplorer_help_popover'; import { APP_STATE_ACTION, @@ -99,13 +99,15 @@ const allValuesLabel = i18n.translate('xpack.ml.timeSeriesExplorer.allPartitionV const containerPadding = 34; export class TimeSeriesExplorer extends React.Component { + static contextType = context; + static propTypes = { appStateHandler: PropTypes.func.isRequired, autoZoomDuration: PropTypes.number.isRequired, bounds: PropTypes.object.isRequired, dateFormatTz: PropTypes.string.isRequired, lastRefresh: PropTypes.number.isRequired, - previousRefresh: PropTypes.number.isRequired, + previousRefresh: PropTypes.number, selectedJobId: PropTypes.string.isRequired, selectedDetectorIndex: PropTypes.number, selectedEntities: PropTypes.object, @@ -113,8 +115,6 @@ export class TimeSeriesExplorer extends React.Component { tableInterval: PropTypes.string, tableSeverity: PropTypes.number, zoom: PropTypes.object, - toastNotificationService: PropTypes.object, - dataViewsService: PropTypes.object, }; state = getTimeseriesexplorerDefaultState(); @@ -141,11 +141,37 @@ export class TimeSeriesExplorer extends React.Component { */ static contextType = context; - mlTimeSeriesExplorer; - mlTimeSeriesSearchService; + dataViewsService; + toastNotificationService; + mlApiServices; mlForecastService; - mlResultsService; mlIndexUtils; + mlJobService; + mlResultsService; + mlTimeSeriesExplorer; + mlTimeSeriesSearchService; + + constructor(props, constructorContext) { + super(props, constructorContext); + this.dataViewsService = constructorContext.services.data.dataViews; + this.toastNotificationService = toastNotificationServiceProvider( + constructorContext.services.notifications.toasts + ); + this.mlApiServices = constructorContext.services.mlServices.mlApiServices; + this.mlForecastService = forecastServiceFactory(this.mlApiServices); + this.mlIndexUtils = indexServiceFactory(this.dataViewsService); + this.mlJobService = mlJobServiceFactory(this.toastNotificationService, this.mlApiServices); + this.mlResultsService = mlResultsServiceProvider(this.mlApiServices); + this.mlTimeSeriesExplorer = timeSeriesExplorerServiceFactory( + constructorContext.services.uiSettings, + this.mlApiServices, + this.mlResultsService + ); + this.mlTimeSeriesSearchService = timeSeriesSearchServiceFactory( + this.mlResultsService, + this.mlApiServices + ); + } /** * Returns field names that don't have a selection yet. @@ -226,7 +252,7 @@ export class TimeSeriesExplorer extends React.Component { getFocusAggregationInterval(selection) { const { selectedJobId } = this.props; - const selectedJob = mlJobService.getJob(selectedJobId); + const selectedJob = this.mlJobService.getJob(selectedJobId); // Calculate the aggregation interval for the focus chart. const bounds = { min: moment(selection.from), max: moment(selection.to) }; @@ -245,7 +271,7 @@ export class TimeSeriesExplorer extends React.Component { const { selectedJobId, selectedForecastId, selectedDetectorIndex, functionDescription } = this.props; const { modelPlotEnabled } = this.state; - const selectedJob = mlJobService.getJob(selectedJobId); + const selectedJob = this.mlJobService.getJob(selectedJobId); if (isMetricDetector(selectedJob, selectedDetectorIndex) && functionDescription === undefined) { return; } @@ -301,9 +327,11 @@ export class TimeSeriesExplorer extends React.Component { tableSeverity, functionDescription, } = this.props; + const mlJobService = this.mlJobService; const selectedJob = mlJobService.getJob(selectedJobId); const entityControls = this.getControlsForDetector(); + const ml = this.mlApiServices; return ml.results .getAnomaliesTableData( [selectedJob.job_id], @@ -365,13 +393,14 @@ export class TimeSeriesExplorer extends React.Component { }; displayErrorToastMessages = (error, errorMsg) => { - if (this.props.toastNotificationService) { - this.props.toastNotificationService.displayErrorToast(error, errorMsg, 2000); + if (this.toastNotificationService) { + this.toastNotificationService.displayErrorToast(error, errorMsg, 2000); } this.setState({ loading: false, chartDataError: errorMsg }); }; loadSingleMetricData = (fullRefresh = true) => { + const mlJobService = this.mlJobService; const { autoZoomDuration, bounds, @@ -653,7 +682,8 @@ export class TimeSeriesExplorer extends React.Component { */ getControlsForDetector = () => { const { selectedDetectorIndex, selectedEntities, selectedJobId } = this.props; - return getControlsForDetector(selectedDetectorIndex, selectedEntities, selectedJobId); + const selectedJob = this.mlJobService.getJob(selectedJobId); + return getControlsForDetector(selectedDetectorIndex, selectedEntities, selectedJob); }; /** @@ -676,7 +706,7 @@ export class TimeSeriesExplorer extends React.Component { loadForJobId(jobId) { const { appStateHandler, selectedDetectorIndex } = this.props; - const selectedJob = mlJobService.getJob(jobId); + const selectedJob = this.mlJobService.getJob(jobId); if (selectedJob === undefined) { return; @@ -698,8 +728,8 @@ export class TimeSeriesExplorer extends React.Component { }, } ); - if (this.props.toastNotificationService) { - this.props.toastNotificationService.displayWarningToast(warningText); + if (this.toastNotificationService) { + this.toastNotificationService.displayWarningToast(warningText); } detectorIndex = detectors[0].index; @@ -715,25 +745,13 @@ export class TimeSeriesExplorer extends React.Component { } componentDidMount() { - const { mlApiServices } = this.context.services.mlServices; - this.mlResultsService = mlResultsServiceProvider(mlApiServices); - this.mlTimeSeriesSearchService = timeSeriesSearchServiceFactory( - this.mlResultsService, - mlApiServices - ); - this.mlTimeSeriesExplorer = timeSeriesExplorerServiceFactory( - this.context.services.uiSettings, - mlApiServices, - this.mlResultsService - ); - this.mlIndexUtils = indexServiceFactory(this.context.services.data.dataViews); - this.mlForecastService = forecastServiceFactory(mlApiServices); + const mlJobService = this.mlJobService; // if timeRange used in the url is incorrect // perhaps due to user's advanced setting using incorrect date-maths const { invalidTimeRangeError } = this.props; if (invalidTimeRangeError) { - if (this.props.toastNotificationService) { - this.props.toastNotificationService.displayWarningToast( + if (this.toastNotificationService) { + this.toastNotificationService.displayWarningToast( i18n.translate('xpack.ml.timeSeriesExplorer.invalidTimeRangeInUrlCallout', { defaultMessage: 'The time filter was changed to the full range for this job due to an invalid default time filter. Check the advanced settings for {field}.', @@ -843,13 +861,9 @@ export class TimeSeriesExplorer extends React.Component { componentDidUpdate(previousProps) { if (previousProps === undefined || previousProps.selectedJobId !== this.props.selectedJobId) { - const selectedJob = mlJobService.getJob(this.props.selectedJobId); + const selectedJob = this.mlJobService.getJob(this.props.selectedJobId); this.contextChartSelectedInitCallDone = false; - getDataViewsAndIndicesWithGeoFields( - [selectedJob], - this.props.dataViewsService, - this.mlIndexUtils - ) + getDataViewsAndIndicesWithGeoFields([selectedJob], this.dataViewsService, this.mlIndexUtils) .then(({ getSourceIndicesWithGeoFieldsResp }) => this.setState( { @@ -931,6 +945,7 @@ export class TimeSeriesExplorer extends React.Component { } render() { + const mlJobService = this.mlJobService; const { autoZoomDuration, bounds, 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 5ff5b88060d70..c574e5d1ce741 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 @@ -613,13 +613,8 @@ export class TimeSeriesExplorerEmbeddableChart extends React.Component { * @param callback to invoke after a state update. */ getControlsForDetector = () => { - const { selectedDetectorIndex, selectedEntities, selectedJobId, selectedJob } = this.props; - return getControlsForDetector( - selectedDetectorIndex, - selectedEntities, - selectedJobId, - selectedJob - ); + const { selectedDetectorIndex, selectedEntities, selectedJob } = this.props; + return getControlsForDetector(selectedDetectorIndex, selectedEntities, selectedJob); }; /** diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/validate_job_selection.ts b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/validate_job_selection.ts index 617d3047056ac..eb07fb02b09dc 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/validate_job_selection.ts +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/validate_job_selection.ts @@ -13,7 +13,7 @@ import type { ToastsStart } from '@kbn/core/public'; import type { MlJobWithTimeRange } from '../../../../common/types/anomaly_detection_jobs'; import { isTimeSeriesViewJob } from '../../../../common/util/job_utils'; -import { mlJobService } from '../../services/job_service'; +import type { MlJobService } from '../../services/job_service'; import type { GetJobSelection } from '../../contexts/ml/use_job_selection_flyout'; @@ -26,6 +26,7 @@ export function validateJobSelection( jobsWithTimeRange: MlJobWithTimeRange[], selectedJobIds: string[], setGlobalState: (...args: any) => void, + mlJobService: MlJobService, toastNotifications: ToastsStart, getJobSelection: GetJobSelection ): boolean | string { diff --git a/x-pack/plugins/ml/public/application/util/chart_utils.test.js b/x-pack/plugins/ml/public/application/util/chart_utils.test.js index f7bc96e1c18d1..0498ec9f468f1 100644 --- a/x-pack/plugins/ml/public/application/util/chart_utils.test.js +++ b/x-pack/plugins/ml/public/application/util/chart_utils.test.js @@ -7,27 +7,6 @@ import seriesConfig from '../explorer/explorer_charts/__mocks__/mock_series_config_filebeat.json'; -jest.mock('./dependency_cache', () => { - const dateMath = require('@kbn/datemath'); - let _time = undefined; - const timefilter = { - setTime: (time) => { - _time = time; - }, - getActiveBounds: () => { - return { - min: dateMath.parse(_time.from), - max: dateMath.parse(_time.to), - }; - }, - }; - return { - getTimefilter: () => timefilter, - }; -}); -import { getTimefilter } from './dependency_cache'; -const timefilter = getTimefilter(); - import d3 from 'd3'; import moment from 'moment'; import React from 'react'; @@ -46,11 +25,6 @@ import { import { CHART_TYPE } from '../explorer/explorer_constants'; -timefilter.setTime({ - from: moment(seriesConfig.selectedEarliest).toISOString(), - to: moment(seriesConfig.selectedLatest).toISOString(), -}); - describe('ML - chart utils', () => { describe('getChartType', () => { const singleMetricConfig = { diff --git a/x-pack/plugins/ml/public/application/util/dependency_cache.ts b/x-pack/plugins/ml/public/application/util/dependency_cache.ts deleted file mode 100644 index b462207e67d9b..0000000000000 --- a/x-pack/plugins/ml/public/application/util/dependency_cache.ts +++ /dev/null @@ -1,126 +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 type { DataPublicPluginSetup } from '@kbn/data-plugin/public'; -import type { - IUiSettingsClient, - ApplicationStart, - HttpStart, - I18nStart, - DocLinksStart, - ToastsStart, - ChromeRecentlyAccessed, -} from '@kbn/core/public'; -import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; -import type { MapsStartApi } from '@kbn/maps-plugin/public'; - -export interface DependencyCache { - timefilter: DataPublicPluginSetup['query']['timefilter'] | null; - config: IUiSettingsClient | null; - docLinks: DocLinksStart | null; - toastNotifications: ToastsStart | null; - recentlyAccessed: ChromeRecentlyAccessed | null; - fieldFormats: FieldFormatsStart | null; - application: ApplicationStart | null; - http: HttpStart | null; - i18n: I18nStart | null; - maps: MapsStartApi | null; -} - -const cache: DependencyCache = { - timefilter: null, - config: null, - docLinks: null, - toastNotifications: null, - recentlyAccessed: null, - fieldFormats: null, - application: null, - http: null, - i18n: null, - maps: null, -}; - -export function setDependencyCache(deps: Partial) { - cache.timefilter = deps.timefilter || null; - cache.config = deps.config || null; - cache.docLinks = deps.docLinks || null; - cache.toastNotifications = deps.toastNotifications || null; - cache.recentlyAccessed = deps.recentlyAccessed || null; - cache.fieldFormats = deps.fieldFormats || null; - cache.application = deps.application || null; - cache.http = deps.http || null; - cache.i18n = deps.i18n || null; -} - -export function getTimefilter() { - if (cache.timefilter === null) { - throw new Error("timefilter hasn't been initialized"); - } - return cache.timefilter.timefilter; -} - -export function getDocLinks() { - if (cache.docLinks === null) { - throw new Error("docLinks hasn't been initialized"); - } - return cache.docLinks; -} - -export function getToastNotifications() { - if (cache.toastNotifications === null) { - throw new Error("toast notifications haven't been initialized"); - } - return cache.toastNotifications; -} - -export function getUiSettings() { - if (cache.config === null) { - throw new Error("uiSettings hasn't been initialized"); - } - return cache.config; -} - -export function getRecentlyAccessed() { - if (cache.recentlyAccessed === null) { - throw new Error("recentlyAccessed hasn't been initialized"); - } - return cache.recentlyAccessed; -} - -export function getFieldFormats() { - if (cache.fieldFormats === null) { - throw new Error("fieldFormats hasn't been initialized"); - } - return cache.fieldFormats; -} - -export function getApplication() { - if (cache.application === null) { - throw new Error("application hasn't been initialized"); - } - return cache.application; -} - -export function getHttp() { - if (cache.http === null) { - throw new Error("http hasn't been initialized"); - } - return cache.http; -} - -export function getI18n() { - if (cache.i18n === null) { - throw new Error("i18n hasn't been initialized"); - } - return cache.i18n; -} - -export function clearCache() { - Object.keys(cache).forEach((k) => { - cache[k as keyof DependencyCache] = null; - }); -} diff --git a/x-pack/plugins/ml/public/application/util/get_services.ts b/x-pack/plugins/ml/public/application/util/get_services.ts index d0d6beb1e3279..330bdc17c8fe9 100644 --- a/x-pack/plugins/ml/public/application/util/get_services.ts +++ b/x-pack/plugins/ml/public/application/util/get_services.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { HttpStart } from '@kbn/core-http-browser'; +import type { CoreStart } from '@kbn/core/public'; import type { DataViewsContract } from '@kbn/data-views-plugin/public'; import type { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public'; @@ -16,18 +16,22 @@ import { fieldFormatServiceFactory } from '../services/field_format_service_fact import { HttpService } from '../services/http_service'; import { mlApiServicesProvider } from '../services/ml_api_service'; import { mlUsageCollectionProvider } from '../services/usage_collection'; +import { mlJobServiceFactory } from '../services/job_service'; +import { toastNotificationServiceProvider } from '../services/toast_notification_service'; import { indexServiceFactory } from './index_service'; /** * Provides global services available across the entire ML app. */ export function getMlGlobalServices( - httpStart: HttpStart, + coreStart: CoreStart, dataViews: DataViewsContract, usageCollection?: UsageCollectionSetup ) { - const httpService = new HttpService(httpStart); + const httpService = new HttpService(coreStart.http); const mlApiServices = mlApiServicesProvider(httpService); + const toastNotificationService = toastNotificationServiceProvider(coreStart.notifications.toasts); + const mlJobService = mlJobServiceFactory(toastNotificationService, mlApiServices); // Note on the following services: // - `mlIndexUtils` is just instantiated here to be passed on to `mlFieldFormatService`, // but it's not being made available as part of global services. Since it's just @@ -38,7 +42,7 @@ export function getMlGlobalServices( // its own context or possibly without having a singleton like state at all, since the // way this manages its own state right now doesn't consider React component lifecycles. const mlIndexUtils = indexServiceFactory(dataViews); - const mlFieldFormatService = fieldFormatServiceFactory(mlApiServices, mlIndexUtils); + const mlFieldFormatService = fieldFormatServiceFactory(mlApiServices, mlIndexUtils, mlJobService); return { httpService, diff --git a/x-pack/plugins/ml/public/application/util/get_time_buckets_from_cache.ts b/x-pack/plugins/ml/public/application/util/get_time_buckets_from_cache.ts index 01ce59744ac1b..ce15a6c85c109 100644 --- a/x-pack/plugins/ml/public/application/util/get_time_buckets_from_cache.ts +++ b/x-pack/plugins/ml/public/application/util/get_time_buckets_from_cache.ts @@ -7,11 +7,9 @@ import { UI_SETTINGS } from '@kbn/data-plugin/public'; import { TimeBuckets } from '@kbn/ml-time-buckets'; +import type { IUiSettingsClient } from '@kbn/core/public'; -import { getUiSettings } from './dependency_cache'; - -export function getTimeBucketsFromCache() { - const uiSettings = getUiSettings(); +export function getTimeBucketsFromCache(uiSettings: IUiSettingsClient) { return new TimeBuckets({ [UI_SETTINGS.HISTOGRAM_MAX_BARS]: uiSettings.get(UI_SETTINGS.HISTOGRAM_MAX_BARS), [UI_SETTINGS.HISTOGRAM_BAR_TARGET]: uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET), diff --git a/x-pack/plugins/ml/public/application/util/recently_accessed.ts b/x-pack/plugins/ml/public/application/util/recently_accessed.ts index ff995be1c4b82..aa2b0aca4cf8c 100644 --- a/x-pack/plugins/ml/public/application/util/recently_accessed.ts +++ b/x-pack/plugins/ml/public/application/util/recently_accessed.ts @@ -10,13 +10,12 @@ import { i18n } from '@kbn/i18n'; import type { ChromeRecentlyAccessed } from '@kbn/core/public'; -import { getRecentlyAccessed } from './dependency_cache'; export function addItemToRecentlyAccessed( page: string, itemId: string, url: string, - recentlyAccessedService?: ChromeRecentlyAccessed + recentlyAccessedService: ChromeRecentlyAccessed ) { let pageLabel = ''; let id = `ml-job-${itemId}`; @@ -44,6 +43,6 @@ export function addItemToRecentlyAccessed( return; } - const recentlyAccessed = recentlyAccessedService ?? getRecentlyAccessed(); + const recentlyAccessed = recentlyAccessedService; recentlyAccessed.add(url, `ML - ${itemId} - ${pageLabel}`, id); } diff --git a/x-pack/plugins/ml/public/application/util/time_series_explorer_service.ts b/x-pack/plugins/ml/public/application/util/time_series_explorer_service.ts index 7c2d91016555d..05763f62d082c 100644 --- a/x-pack/plugins/ml/public/application/util/time_series_explorer_service.ts +++ b/x-pack/plugins/ml/public/application/util/time_series_explorer_service.ts @@ -30,24 +30,24 @@ import { TIME_FIELD_NAME, } from '../timeseriesexplorer/timeseriesexplorer_constants'; import type { MlApiServices } from '../services/ml_api_service'; -import { mlResultsServiceProvider, type MlResultsService } from '../services/results_service'; +import { useMlResultsService, type MlResultsService } from '../services/results_service'; import { forecastServiceFactory } from '../services/forecast_service'; import { timeSeriesSearchServiceFactory } from '../timeseriesexplorer/timeseriesexplorer_utils/time_series_search_service'; -import { useMlKibana } from '../contexts/kibana'; +import { useMlApiContext, useMlKibana } from '../contexts/kibana'; export interface Interval { asMilliseconds: () => number; expression: string; } -interface ChartDataPoint { +export interface ChartDataPoint { date: Date; value: number | null; upper?: number | null; lower?: number | null; } -interface FocusData { +export interface FocusData { focusChartData: ChartDataPoint[]; anomalyRecords: MlAnomalyRecordDoc[]; scheduledEvents: any; @@ -57,8 +57,6 @@ interface FocusData { focusForecastData?: any; } -// TODO Consolidate with legacy code in -// `ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/timeseriesexplorer_utils.js`. export function timeSeriesExplorerServiceFactory( uiSettings: IUiSettingsClient, mlApiServices: MlApiServices, @@ -648,19 +646,15 @@ export function timeSeriesExplorerServiceFactory( } export function useTimeSeriesExplorerService(): TimeSeriesExplorerService { - const { - services: { - uiSettings, - mlServices: { mlApiServices }, - }, - } = useMlKibana(); - const mlResultsService = mlResultsServiceProvider(mlApiServices); - - const mlTimeSeriesExplorer = useMemo( - () => timeSeriesExplorerServiceFactory(uiSettings, mlApiServices, mlResultsService), - [uiSettings, mlApiServices, mlResultsService] + const { services } = useMlKibana(); + const mlApiServices = useMlApiContext(); + const mlResultsService = useMlResultsService(); + return useMemo( + () => timeSeriesExplorerServiceFactory(services.uiSettings, mlApiServices, mlResultsService), + // initialize only once + // eslint-disable-next-line react-hooks/exhaustive-deps + [] ); - return mlTimeSeriesExplorer; } export type TimeSeriesExplorerService = ReturnType; diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_setup_flyout.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_setup_flyout.tsx index 1b1ce4070703b..a40d2eb1d74d6 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_setup_flyout.tsx +++ b/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_setup_flyout.tsx @@ -26,7 +26,7 @@ export async function resolveEmbeddableAnomalyChartsUserInput( ): Promise> { const { http, overlays, ...startServices } = coreStart; const adJobsApiService = jobsApiProvider(new HttpService(http)); - const mlServices = getMlGlobalServices(http, pluginStart.data.dataViews); + const mlServices = getMlGlobalServices(coreStart, pluginStart.data.dataViews); const overlayTracker = tracksOverlays(parentApi) ? parentApi : undefined; return new Promise(async (resolve, reject) => { diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_charts/get_anomaly_charts_services_dependencies.ts b/x-pack/plugins/ml/public/embeddables/anomaly_charts/get_anomaly_charts_services_dependencies.ts index b0c365088beed..b7eec6ab24f5f 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_charts/get_anomaly_charts_services_dependencies.ts +++ b/x-pack/plugins/ml/public/embeddables/anomaly_charts/get_anomaly_charts_services_dependencies.ts @@ -12,28 +12,34 @@ import type { AnomalyChartsEmbeddableServices } from '..'; import { AnomalyExplorerChartsService } from '../../application/services/anomaly_explorer_charts_service'; export const getAnomalyChartsServiceDependencies = async ( - coreStartServices: CoreStart, - pluginsStartServices: MlStartDependencies + coreStart: CoreStart, + pluginsStart: MlStartDependencies ): Promise => { const [ { AnomalyDetectorService }, { fieldFormatServiceFactory }, { indexServiceFactory }, { mlApiServicesProvider }, + { mlJobServiceFactory }, { mlResultsServiceProvider }, + { toastNotificationServiceProvider }, ] = await Promise.all([ await import('../../application/services/anomaly_detector_service'), await import('../../application/services/field_format_service_factory'), await import('../../application/util/index_service'), await import('../../application/services/ml_api_service'), + await import('../../application/services/job_service'), await import('../../application/services/results_service'), + await import('../../application/services/toast_notification_service'), ]); - const httpService = new HttpService(coreStartServices.http); + const httpService = new HttpService(coreStart.http); const anomalyDetectorService = new AnomalyDetectorService(httpService); const mlApiServices = mlApiServicesProvider(httpService); + const toastNotificationService = toastNotificationServiceProvider(coreStart.notifications.toasts); + const mlJobService = mlJobServiceFactory(toastNotificationService, mlApiServices); const mlResultsService = mlResultsServiceProvider(mlApiServices); const anomalyExplorerService = new AnomalyExplorerChartsService( - pluginsStartServices.data.query.timefilter.timefilter, + pluginsStart.data.query.timefilter.timefilter, mlApiServices, mlResultsService ); @@ -47,12 +53,12 @@ export const getAnomalyChartsServiceDependencies = async ( // In the long run we should again try to get rid of it here and make it available via // its own context or possibly without having a singleton like state at all, since the // way this manages its own state right now doesn't consider React component lifecycles. - const mlIndexUtils = indexServiceFactory(pluginsStartServices.data.dataViews); - const mlFieldFormatService = fieldFormatServiceFactory(mlApiServices, mlIndexUtils); + const mlIndexUtils = indexServiceFactory(pluginsStart.data.dataViews); + const mlFieldFormatService = fieldFormatServiceFactory(mlApiServices, mlIndexUtils, mlJobService); const anomalyChartsEmbeddableServices: AnomalyChartsEmbeddableServices = [ - coreStartServices, - pluginsStartServices as MlDependencies, + coreStart, + pluginsStart as MlDependencies, { anomalyDetectorService, anomalyExplorerService, diff --git a/x-pack/plugins/ml/public/embeddables/job_creation/aiops/flyout/create_job.tsx b/x-pack/plugins/ml/public/embeddables/job_creation/aiops/flyout/create_job.tsx index df5048d7261ad..d52aed02d88a3 100644 --- a/x-pack/plugins/ml/public/embeddables/job_creation/aiops/flyout/create_job.tsx +++ b/x-pack/plugins/ml/public/embeddables/job_creation/aiops/flyout/create_job.tsx @@ -34,6 +34,8 @@ import { } from '../../../../application/jobs/new_job/job_from_pattern_analysis'; import { useMlFromLensKibanaContext } from '../../common/context'; import { JobDetails, type CreateADJobParams } from '../../common/job_details'; +import { mlJobServiceFactory } from '../../../../application/services/job_service'; +import { toastNotificationServiceProvider } from '../../../../application/services/toast_notification_service'; interface Props { dataView: DataView; @@ -48,10 +50,13 @@ export const CreateJob: FC = ({ dataView, field, query, timeRange }) => { data, share, uiSettings, - mlServices: { mlApiServices }, dashboardService, + notifications: { toasts }, + mlServices: { mlApiServices }, }, } = useMlFromLensKibanaContext(); + const toastNotificationService = toastNotificationServiceProvider(toasts); + const mlJobService = mlJobServiceFactory(toastNotificationService, mlApiServices); const [categorizationType, setCategorizationType] = useState( CATEGORIZATION_TYPE.COUNT @@ -91,10 +96,10 @@ export const CreateJob: FC = ({ dataView, field, query, timeRange }) => { data.query.timefilter.timefilter, dashboardService, data, - mlApiServices + mlApiServices, + mlJobService ), - - [dashboardService, data, mlApiServices, uiSettings] + [dashboardService, data, mlApiServices, mlJobService, uiSettings] ); function createADJobInWizard() { diff --git a/x-pack/plugins/ml/public/embeddables/job_creation/common/create_flyout.tsx b/x-pack/plugins/ml/public/embeddables/job_creation/common/create_flyout.tsx index 5bab4967f0cdb..0016a5c8d3a9a 100644 --- a/x-pack/plugins/ml/public/embeddables/job_creation/common/create_flyout.tsx +++ b/x-pack/plugins/ml/public/embeddables/job_creation/common/create_flyout.tsx @@ -52,7 +52,7 @@ export function createFlyout( data, lens, dashboardService, - mlServices: getMlGlobalServices(http, data.dataViews), + mlServices: getMlGlobalServices(coreStart, data.dataViews), }} > = ({ layer, layerIndex, embeddable }) => data, share, uiSettings, - mlServices: { mlApiServices }, lens, dashboardService, + notifications: { toasts }, + mlServices: { mlApiServices }, }, } = useMlFromLensKibanaContext(); + const toastNotificationService = toastNotificationServiceProvider(toasts); + const mlJobService = mlJobServiceFactory(toastNotificationService, mlApiServices); const quickJobCreator = useMemo( () => @@ -46,7 +51,8 @@ export const CompatibleLayer: FC = ({ layer, layerIndex, embeddable }) => uiSettings, data.query.timefilter.timefilter, dashboardService, - mlApiServices + mlApiServices, + mlJobService ), // eslint-disable-next-line react-hooks/exhaustive-deps [data, uiSettings] diff --git a/x-pack/plugins/ml/public/embeddables/job_creation/map/map_vis_layer_selection_flyout/layer/compatible_layer.tsx b/x-pack/plugins/ml/public/embeddables/job_creation/map/map_vis_layer_selection_flyout/layer/compatible_layer.tsx index b5f3f30420188..76beaa6788f13 100644 --- a/x-pack/plugins/ml/public/embeddables/job_creation/map/map_vis_layer_selection_flyout/layer/compatible_layer.tsx +++ b/x-pack/plugins/ml/public/embeddables/job_creation/map/map_vis_layer_selection_flyout/layer/compatible_layer.tsx @@ -29,6 +29,8 @@ import { import { useMlFromLensKibanaContext } from '../../../common/context'; import type { CreateADJobParams } from '../../../common/job_details'; import { JobDetails } from '../../../common/job_details'; +import { mlJobServiceFactory } from '../../../../../application/services/job_service'; +import { toastNotificationServiceProvider } from '../../../../../application/services/toast_notification_service'; interface DropDownLabel { label: string; @@ -51,9 +53,12 @@ export const CompatibleLayer: FC = ({ embeddable, layer, layerIndex }) => share, uiSettings, dashboardService, + notifications: { toasts }, mlServices: { mlApiServices }, }, } = useMlFromLensKibanaContext(); + const toastNotificationService = toastNotificationServiceProvider(toasts); + const mlJobService = mlJobServiceFactory(toastNotificationService, mlApiServices); const quickJobCreator = useMemo( () => @@ -62,7 +67,8 @@ export const CompatibleLayer: FC = ({ embeddable, layer, layerIndex }) => uiSettings, data.query.timefilter.timefilter, dashboardService, - mlApiServices + mlApiServices, + mlJobService ), // eslint-disable-next-line react-hooks/exhaustive-deps [data, uiSettings] diff --git a/x-pack/plugins/ml/public/embeddables/single_metric_viewer/get_services.ts b/x-pack/plugins/ml/public/embeddables/single_metric_viewer/get_services.ts index 8dab0f4b65ea0..f4460331e6256 100644 --- a/x-pack/plugins/ml/public/embeddables/single_metric_viewer/get_services.ts +++ b/x-pack/plugins/ml/public/embeddables/single_metric_viewer/get_services.ts @@ -74,7 +74,7 @@ export const getMlServices = async ( // its own context or possibly without having a singleton like state at all, since the // way this manages its own state right now doesn't consider React component lifecycles. const mlIndexUtils = indexServiceFactory(pluginsStart.data.dataViews); - const mlFieldFormatService = fieldFormatServiceFactory(mlApiServices, mlIndexUtils); + const mlFieldFormatService = fieldFormatServiceFactory(mlApiServices, mlIndexUtils, mlJobService); return { anomalyDetectorService, anomalyExplorerService, diff --git a/x-pack/plugins/ml/public/plugin.ts b/x-pack/plugins/ml/public/plugin.ts index ae41b31d3eaaf..456259fd6d28d 100644 --- a/x-pack/plugins/ml/public/plugin.ts +++ b/x-pack/plugins/ml/public/plugin.ts @@ -57,7 +57,6 @@ import { getMlSharedServices } from './application/services/get_shared_ml_servic import { registerManagementSection } from './application/management'; import type { MlLocatorParams } from './locator'; import { MlLocatorDefinition, type MlLocator } from './locator'; -import { setDependencyCache } from './application/util/dependency_cache'; import { registerHomeFeature } from './register_home_feature'; import { isFullLicense, isMlEnabled } from '../common/license'; import { @@ -318,12 +317,6 @@ export class MlPlugin implements Plugin { mlApi?: MlApiServices; components: { AnomalySwimLane: typeof AnomalySwimLane }; } { - setDependencyCache({ - docLinks: core.docLinks!, - http: core.http, - i18n: core.i18n, - }); - return { locator: this.locator, elasticModels: this.sharedMlServices?.elasticModels, diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index d34c2e6f98c5e..ae1cd3466a70a 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -29613,13 +29613,12 @@ "xpack.ml.newJob.wizard.pickFieldsStep.influencers.title": "Influenceurs", "xpack.ml.newJob.wizard.pickFieldsStep.invalidCssVersionCallout.mesage": "Impossible de trouver d'exemples de catégories, cela peut provenir d'une version non prise en charge de l'un des clusters.", "xpack.ml.newJob.wizard.pickFieldsStep.invalidCssVersionCallout.title": "La vue de données semble être inter-clusters.", - "xpack.ml.newJob.wizard.pickFieldsStep.multiMetricView.addMetric": "Ajouter un indicateur", + "xpack.ml.newJob.wizard.pickFieldsStep.metricSelector.addMetric": "Ajouter un indicateur", "xpack.ml.newJob.wizard.pickFieldsStep.noDetectorsCallout.message": "Au moins un détecteur est nécessaire pour créer une tâche.", "xpack.ml.newJob.wizard.pickFieldsStep.noDetectorsCallout.title": "Aucun détecteur", "xpack.ml.newJob.wizard.pickFieldsStep.populationField.description": "Toutes les valeurs du champ sélectionné seront modélisées ensemble en tant que population.", "xpack.ml.newJob.wizard.pickFieldsStep.populationField.placeholder": "Diviser les données", "xpack.ml.newJob.wizard.pickFieldsStep.populationField.title": "Champ de population", - "xpack.ml.newJob.wizard.pickFieldsStep.populationView.addMetric": "Ajouter un indicateur", "xpack.ml.newJob.wizard.pickFieldsStep.populationView.splitFieldTitle": "Population divisée par {field}", "xpack.ml.newJob.wizard.pickFieldsStep.rareDetectorSelect.freqRareCard.description": "Recherchez les membres d'une population qui présentent fréquemment des valeurs rares.", "xpack.ml.newJob.wizard.pickFieldsStep.rareDetectorSelect.freqRareCard.title": "Fréquemment rare dans la population", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 71939a6d5abac..ae562a1a8e79a 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -29602,13 +29602,12 @@ "xpack.ml.newJob.wizard.pickFieldsStep.influencers.title": "影響", "xpack.ml.newJob.wizard.pickFieldsStep.invalidCssVersionCallout.mesage": "カテゴリの例が見つかりませんでした。これはクラスターの1つがサポートされていないバージョンであることが原因です。", "xpack.ml.newJob.wizard.pickFieldsStep.invalidCssVersionCallout.title": "データビューがクロスクラスターである可能性があります", - "xpack.ml.newJob.wizard.pickFieldsStep.multiMetricView.addMetric": "メトリックを追加", + "xpack.ml.newJob.wizard.pickFieldsStep.metricSelector.addMetric": "メトリックを追加", "xpack.ml.newJob.wizard.pickFieldsStep.noDetectorsCallout.message": "ジョブを作成するには最低 1 つのディテクターが必要です。", "xpack.ml.newJob.wizard.pickFieldsStep.noDetectorsCallout.title": "ディテクターがありません", "xpack.ml.newJob.wizard.pickFieldsStep.populationField.description": "選択されたフィールドのすべての値が集団として一緒にモデリングされます。", "xpack.ml.newJob.wizard.pickFieldsStep.populationField.placeholder": "データを分割", "xpack.ml.newJob.wizard.pickFieldsStep.populationField.title": "集団フィールド", - "xpack.ml.newJob.wizard.pickFieldsStep.populationView.addMetric": "メトリックを追加", "xpack.ml.newJob.wizard.pickFieldsStep.populationView.splitFieldTitle": "{field} で分割された集団", "xpack.ml.newJob.wizard.pickFieldsStep.rareDetectorSelect.freqRareCard.description": "頻繁にまれな値になる母集団のメンバーを検索します。", "xpack.ml.newJob.wizard.pickFieldsStep.rareDetectorSelect.freqRareCard.title": "母集団で頻繁にまれ", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 8e87dc6c73fc8..aee5456710144 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -29637,13 +29637,12 @@ "xpack.ml.newJob.wizard.pickFieldsStep.influencers.title": "影响因素", "xpack.ml.newJob.wizard.pickFieldsStep.invalidCssVersionCallout.mesage": "找不到任何示例类别,这可能由于有一个集群的版本不受支持。", "xpack.ml.newJob.wizard.pickFieldsStep.invalidCssVersionCallout.title": "数据视图似乎跨集群", - "xpack.ml.newJob.wizard.pickFieldsStep.multiMetricView.addMetric": "添加指标", + "xpack.ml.newJob.wizard.pickFieldsStep.metricSelector.addMetric": "添加指标", "xpack.ml.newJob.wizard.pickFieldsStep.noDetectorsCallout.message": "至少需要一个检测工具,才能创建作业。", "xpack.ml.newJob.wizard.pickFieldsStep.noDetectorsCallout.title": "无检测工具", "xpack.ml.newJob.wizard.pickFieldsStep.populationField.description": "选定字段中的所有值将作为一个群体一起进行建模。", "xpack.ml.newJob.wizard.pickFieldsStep.populationField.placeholder": "分割数据", "xpack.ml.newJob.wizard.pickFieldsStep.populationField.title": "群体字段", - "xpack.ml.newJob.wizard.pickFieldsStep.populationView.addMetric": "添加指标", "xpack.ml.newJob.wizard.pickFieldsStep.populationView.splitFieldTitle": "群体由 {field} 分割", "xpack.ml.newJob.wizard.pickFieldsStep.rareDetectorSelect.freqRareCard.description": "查找群体中经常有罕见值的成员。", "xpack.ml.newJob.wizard.pickFieldsStep.rareDetectorSelect.freqRareCard.title": "群体中极罕见", diff --git a/x-pack/test/functional/apps/ml/anomaly_detection_result_views/single_metric_viewer.ts b/x-pack/test/functional/apps/ml/anomaly_detection_result_views/single_metric_viewer.ts index c9ceb71459e4a..7667b0896cfce 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection_result_views/single_metric_viewer.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection_result_views/single_metric_viewer.ts @@ -162,6 +162,13 @@ export default function ({ getService }: FtrProviderContext) { await ml.jobTable.clickOpenJobInSingleMetricViewerButton(jobConfig.job_id); await ml.commonUI.waitForMlLoadingIndicatorToDisappear(); + + // assert that the results view is not displayed when no entity is selected + await ml.testExecution.logTestStep('does not display the chart'); + await ml.singleMetricViewer.assertChartNotExist(); + + await ml.testExecution.logTestStep('does not display the anomalies table'); + await ml.anomaliesTable.assertTableNotExists(); }); it('render entity control', async () => { diff --git a/x-pack/test/functional/services/ml/anomalies_table.ts b/x-pack/test/functional/services/ml/anomalies_table.ts index 35a53d90f6b9c..4d023a7ccc5a5 100644 --- a/x-pack/test/functional/services/ml/anomalies_table.ts +++ b/x-pack/test/functional/services/ml/anomalies_table.ts @@ -18,6 +18,10 @@ export function MachineLearningAnomaliesTableProvider({ getService }: FtrProvide await testSubjects.existOrFail('mlAnomaliesTable'); }, + async assertTableNotExists() { + await testSubjects.missingOrFail('mlAnomaliesTable'); + }, + async getTableRows() { return await testSubjects.findAll('mlAnomaliesTable > ~mlAnomaliesListRow'); }, diff --git a/x-pack/test/functional/services/ml/single_metric_viewer.ts b/x-pack/test/functional/services/ml/single_metric_viewer.ts index 753d5384b80af..3dede4734a47e 100644 --- a/x-pack/test/functional/services/ml/single_metric_viewer.ts +++ b/x-pack/test/functional/services/ml/single_metric_viewer.ts @@ -74,6 +74,10 @@ export function MachineLearningSingleMetricViewerProvider( await testSubjects.existOrFail('mlSingleMetricViewerChart'); }, + async assertChartNotExist() { + await testSubjects.missingOrFail('mlSingleMetricViewerChart'); + }, + async assertAnomalyMarkerExist() { await testSubjects.existOrFail('mlAnomalyMarker'); }, @@ -181,7 +185,10 @@ export function MachineLearningSingleMetricViewerProvider( `mlSingleMetricViewerEntitySelectionConfigOrder_${entityFieldName}`, order ); - await this.assertEntityConfig(entityFieldName, anomalousOnly, sortBy, order); + + await retry.tryForTime(30 * 1000, async () => { + await this.assertEntityConfig(entityFieldName, anomalousOnly, sortBy, order); + }); }, async assertToastMessageExists(dataTestSubj: string) {