diff --git a/x-pack/plugins/ml/public/application/app.tsx b/x-pack/plugins/ml/public/application/app.tsx index 449f03d81b9fd..0413529ca4d67 100644 --- a/x-pack/plugins/ml/public/application/app.tsx +++ b/x-pack/plugins/ml/public/application/app.tsx @@ -10,6 +10,7 @@ import './_index.scss'; import ReactDOM from 'react-dom'; import { pick } from 'lodash'; +import type { DataViewsContract } from '@kbn/data-views-plugin/public'; import type { AppMountParameters, CoreStart, HttpStart } from '@kbn/core/public'; import type { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public'; import { DatePickerContextProvider, type DatePickerDependencies } from '@kbn/ml-date-picker'; @@ -33,6 +34,8 @@ import { HttpService } from './services/http_service'; import type { PageDependencies } from './routing/router'; import { EnabledFeaturesContextProvider } from './contexts/ml'; import type { StartServices } from './contexts/kibana'; +import { fieldFormatServiceFactory } from './services/field_format_service_factory'; +import { indexServiceFactory } from './util/index_service'; export type MlDependencies = Omit< MlSetupDependencies, @@ -53,13 +56,29 @@ const localStorage = new Storage(window.localStorage); /** * Provides global services available across the entire ML app. */ -export function getMlGlobalServices(httpStart: HttpStart, usageCollection?: UsageCollectionSetup) { +export function getMlGlobalServices( + httpStart: HttpStart, + dataViews: DataViewsContract, + usageCollection?: UsageCollectionSetup +) { const httpService = new HttpService(httpStart); const mlApiServices = mlApiServicesProvider(httpService); + // 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 + // some stateless utils `useMlIndexUtils()` should be used from within components. + // - `mlFieldFormatService` is a stateful legacy service that relied on "dependency cache", + // so because of its own state it needs to be made available as a global service. + // 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(dataViews); + const mlFieldFormatService = fieldFormatServiceFactory(mlApiServices, mlIndexUtils); return { httpService, mlApiServices, + mlFieldFormatService, mlUsageCollection: mlUsageCollectionProvider(usageCollection), mlCapabilities: new MlCapabilitiesService(mlApiServices), mlLicense: new MlLicense(), @@ -106,7 +125,7 @@ const App: FC = ({ coreStart, deps, appMountParams, isServerless, mlFe unifiedSearch: deps.unifiedSearch, usageCollection: deps.usageCollection, ...coreStart, - mlServices: getMlGlobalServices(coreStart.http, deps.usageCollection), + mlServices: getMlGlobalServices(coreStart.http, deps.data.dataViews, deps.usageCollection), }; }, [deps, coreStart]); @@ -168,25 +187,13 @@ export const renderApp = ( setDependencyCache({ timefilter: deps.data.query.timefilter, fieldFormats: deps.fieldFormats, - autocomplete: deps.unifiedSearch.autocomplete, config: coreStart.uiSettings!, - chrome: coreStart.chrome!, docLinks: coreStart.docLinks!, toastNotifications: coreStart.notifications.toasts, - overlays: coreStart.overlays, - theme: coreStart.theme, recentlyAccessed: coreStart.chrome!.recentlyAccessed, - basePath: coreStart.http.basePath, - savedSearch: deps.savedSearch, application: coreStart.application, http: coreStart.http, - security: deps.security, - dashboard: deps.dashboard, maps: deps.maps, - dataVisualizer: deps.dataVisualizer, - dataViews: deps.data.dataViews, - share: deps.share, - lens: deps.lens, }); appMountParams.onAppLeave((actions) => actions.default()); 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 0edb008184aae..220497ba13b10 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 @@ -18,6 +18,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiInMemoryTable, EuiText } from '@elastic/e import { FormattedMessage } from '@kbn/i18n-react'; import { usePageUrlState } from '@kbn/ml-url-state'; +import { context } from '@kbn/kibana-react-plugin/public'; import { getColumns } from './anomalies_table_columns'; @@ -29,6 +30,8 @@ import { ml } from '../../services/ml_api_service'; import { INFLUENCERS_LIMIT, ANOMALIES_TABLE_TABS, MAX_CHARS } from './anomalies_table_constants'; export class AnomaliesTableInternal extends Component { + static contextType = context; + constructor(props) { super(props); @@ -189,6 +192,7 @@ export class AnomaliesTableInternal extends Component { } const columns = getColumns( + this.context.services.mlServices.mlFieldFormatService, tableData.anomalies, tableData.jobIds, tableData.examplesByJobId, diff --git a/x-pack/plugins/ml/public/application/components/anomalies_table/anomalies_table.test.js b/x-pack/plugins/ml/public/application/components/anomalies_table/anomalies_table.test.js index d474969475d64..d8f00b92992c6 100644 --- a/x-pack/plugins/ml/public/application/components/anomalies_table/anomalies_table.test.js +++ b/x-pack/plugins/ml/public/application/components/anomalies_table/anomalies_table.test.js @@ -18,9 +18,6 @@ jest.mock('../../license', () => ({ jest.mock('../../capabilities/get_capabilities', () => ({ getCapabilities: () => {}, })); -jest.mock('../../services/field_format_service', () => ({ - getFieldFormat: () => {}, -})); jest.mock('./links_menu', () => () =>
mocked link component
); jest.mock('./description_cell', () => () => (
mocked description component
@@ -31,6 +28,10 @@ jest.mock('./influencers_cell', () => () => (
mocked influencer component
)); +const mlFieldFormatServiceMock = { + getFieldFormat: () => {}, +}; + const columnData = { items: mockAnomaliesTableData.default.anomalies, jobIds: mockAnomaliesTableData.default.jobIds, @@ -48,6 +49,7 @@ const columnData = { describe('AnomaliesTable', () => { test('all columns created', () => { const columns = getColumns( + mlFieldFormatServiceMock, columnData.items, columnData.jobIds, columnData.examplesByJobId, @@ -103,6 +105,7 @@ describe('AnomaliesTable', () => { }; const columns = getColumns( + mlFieldFormatServiceMock, noEntityValueColumnData.items, noEntityValueColumnData.jobIds, noEntityValueColumnData.examplesByJobId, @@ -133,6 +136,7 @@ describe('AnomaliesTable', () => { }; const columns = getColumns( + mlFieldFormatServiceMock, noInfluencersColumnData.items, noInfluencersColumnData.jobIds, noInfluencersColumnData.examplesByJobId, @@ -163,6 +167,7 @@ describe('AnomaliesTable', () => { }; const columns = getColumns( + mlFieldFormatServiceMock, noActualColumnData.items, noActualColumnData.jobIds, noActualColumnData.examplesByJobId, @@ -193,6 +198,7 @@ describe('AnomaliesTable', () => { }; const columns = getColumns( + mlFieldFormatServiceMock, noTypicalColumnData.items, noTypicalColumnData.jobIds, noTypicalColumnData.examplesByJobId, @@ -223,6 +229,7 @@ describe('AnomaliesTable', () => { }; const columns = getColumns( + mlFieldFormatServiceMock, multipleJobIdsData.items, multipleJobIdsData.jobIds, multipleJobIdsData.examplesByJobId, diff --git a/x-pack/plugins/ml/public/application/components/anomalies_table/anomalies_table_columns.js b/x-pack/plugins/ml/public/application/components/anomalies_table/anomalies_table_columns.js index cfc8c128f11a7..1f9eadff694f3 100644 --- a/x-pack/plugins/ml/public/application/components/anomalies_table/anomalies_table_columns.js +++ b/x-pack/plugins/ml/public/application/components/anomalies_table/anomalies_table_columns.js @@ -25,7 +25,6 @@ import { EntityCell } from '../entity_cell'; import { InfluencersCell } from './influencers_cell'; import { LinksMenu } from './links_menu'; import { checkPermission } from '../../capabilities/check_capabilities'; -import { mlFieldFormatService } from '../../services/field_format_service'; import { formatValue } from '../../formatters/format_value'; import { INFLUENCERS_LIMIT, ANOMALIES_TABLE_TABS } from './anomalies_table_constants'; import { SeverityCell } from './severity_cell'; @@ -56,6 +55,7 @@ function showLinksMenuForItem(item, showViewSeriesLink, sourceIndicesWithGeoFiel } export function getColumns( + mlFieldFormatService, items, jobIds, examplesByJobId, 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 c67092978d38a..cafb4bfdfa435 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 @@ -42,7 +42,7 @@ import { escapeQuotes } from '@kbn/es-query'; import { isQuery } from '@kbn/data-plugin/public'; import { PLUGIN_ID } from '../../../../common/constants/app'; -import { findMessageField, getDataViewIdFromName } from '../../util/index_utils'; +import { findMessageField } from '../../util/index_utils'; import { getInitialAnomaliesLayers, getInitialSourceIndexFieldLayers } from '../../../maps/util'; import { parseInterval } from '../../../../common/util/parse_interval'; import { ML_APP_LOCATOR, ML_PAGES } from '../../../../common/constants/locator'; @@ -61,6 +61,7 @@ import { usePermissionCheck } from '../../capabilities/check_capabilities'; import type { TimeRangeBounds } from '../../util/time_buckets'; import { useMlKibana } from '../../contexts/kibana'; import { getFieldTypeFromMapping } from '../../services/mapping_service'; +import { useMlIndexUtils } from '../../util/index_service'; import { getQueryStringForInfluencers } from './get_query_string_for_influencers'; @@ -97,6 +98,7 @@ export const LinksMenuUI = (props: LinksMenuProps) => { const { services: { data, share, application, uiActions }, } = kibana; + const { getDataViewIdFromName } = useMlIndexUtils(); const job = useMemo(() => { return mlJobService.getJob(props.anomaly.jobId); 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 c64f7f3ee2861..0b4b68981471a 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 @@ -24,7 +24,7 @@ import { import { FormattedMessage } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; -import { withKibana } from '@kbn/kibana-react-plugin/public'; +import { context } from '@kbn/kibana-react-plugin/public'; import type { DataViewListItem } from '@kbn/data-views-plugin/common'; import type { MlUrlConfig } from '@kbn/ml-anomaly-utils'; import { isDataFrameAnalyticsConfigs } from '@kbn/ml-data-frame-analytics-utils'; @@ -42,9 +42,9 @@ import { getTestUrl, type CustomUrlSettings, } from './custom_url_editor/utils'; -import { loadDataViewListItems } from '../../jobs/jobs_list/components/edit_job_flyout/edit_utils'; import { openCustomUrlWindow } from '../../util/custom_url_utils'; import type { CustomUrlsWrapperProps } from './custom_urls_wrapper'; +import { indexServiceFactory } from '../../util/index_service'; interface CustomUrlsState { customUrls: MlUrlConfig[]; @@ -55,13 +55,15 @@ interface CustomUrlsState { supportedFilterFields: string[]; } interface CustomUrlsProps extends CustomUrlsWrapperProps { - kibana: MlKibanaReactContextValue; currentTimeFilter?: EsQueryTimeRange; dashboardService: DashboardService; isPartialDFAJob?: boolean; } -class CustomUrlsUI extends Component { +export class CustomUrls extends Component { + static contextType = context; + declare context: MlKibanaReactContextValue; + private toastNotificationService: ToastNotificationService | undefined; constructor(props: CustomUrlsProps) { @@ -84,9 +86,10 @@ class CustomUrlsUI extends Component { } componentDidMount() { - const { toasts } = this.props.kibana.services.notifications; + const { toasts } = this.context.services.notifications; this.toastNotificationService = toastNotificationServiceProvider(toasts); const { dashboardService } = this.props; + const mlIndexUtils = indexServiceFactory(this.context.services.data.dataViews); dashboardService .fetchDashboards() @@ -105,7 +108,8 @@ class CustomUrlsUI extends Component { ); }); - loadDataViewListItems() + mlIndexUtils + .loadDataViewListItems() .then((dataViewListItems) => { this.setState({ dataViewListItems }); }) @@ -146,7 +150,7 @@ class CustomUrlsUI extends Component { }; addNewCustomUrl = () => { - const { dashboard } = this.props.kibana.services; + const { dashboard } = this.context.services; buildCustomUrlFromSettings(dashboard, this.state.editorSettings as CustomUrlSettings) .then((customUrl) => { @@ -173,7 +177,7 @@ class CustomUrlsUI extends Component { http: { basePath }, data: { dataViews }, dashboard, - } = this.props.kibana.services; + } = this.context.services; const dataViewId = this.state?.editorSettings?.kibanaSettings?.discoverIndexPatternId; const job = this.props.job; dataViews @@ -361,5 +365,3 @@ class CustomUrlsUI extends Component { ); } } - -export const CustomUrls = withKibana(CustomUrlsUI); diff --git a/x-pack/plugins/ml/public/application/contexts/kibana/__mocks__/kibana_context.ts b/x-pack/plugins/ml/public/application/contexts/kibana/__mocks__/kibana_context.ts index ab19e7c19b93e..671df792a9375 100644 --- a/x-pack/plugins/ml/public/application/contexts/kibana/__mocks__/kibana_context.ts +++ b/x-pack/plugins/ml/public/application/contexts/kibana/__mocks__/kibana_context.ts @@ -65,6 +65,9 @@ export const kibanaContextMock = { mlCapabilities: { refreshCapabilities: jest.fn(), }, + mlFieldFormatService: { + getFieldFormat: jest.fn(), + }, }, notifications: notificationServiceMock.createStartContract(), }, 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 139b49f7c2b45..22acd894d63ef 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 @@ -20,9 +20,9 @@ import { } from '@kbn/ml-data-frame-analytics-utils'; import { useMlKibana } from '../../contexts/kibana'; -import { getDataViewIdFromName } from '../../util/index_utils'; import { ml } from '../../services/ml_api_service'; import { newJobCapsServiceAnalytics } 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'; @@ -35,6 +35,7 @@ export const useResultsViewConfig = (jobId: string) => { data: { dataViews }, }, } = useMlKibana(); + const { getDataViewIdFromName } = useMlIndexUtils(); const trainedModelsApiService = useTrainedModelsApiService(); const [dataView, setDataView] = useState(undefined); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/controls.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/controls.tsx index 2786046610fc7..e637b2718f233 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/controls.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/controls.tsx @@ -39,13 +39,13 @@ import { useMlKibana, } from '../../../../contexts/kibana'; import { useEnabledFeatures } from '../../../../contexts/ml'; -import { getDataViewIdFromName } from '../../../../util/index_utils'; import { useNavigateToWizardWithClonedJob } from '../../analytics_management/components/action_clone/clone_action_name'; import { useDeleteAction, DeleteActionModal, } from '../../analytics_management/components/action_delete'; import { DeleteSpaceAwareItemCheckModal } from '../../../../components/delete_space_aware_item_check_modal'; +import { useMlIndexUtils } from '../../../../util/index_service'; interface Props { details: Record; @@ -115,6 +115,7 @@ export const Controls: FC = React.memo( application: { navigateToUrl, capabilities }, }, } = useMlKibana(); + const { getDataViewIdFromName } = useMlIndexUtils(); const hasIngestPipelinesCapabilities = capabilities.management?.ingest?.ingest_pipelines === true; 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 ed244cbd894ba..5ace1a3422aab 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 @@ -8,13 +8,16 @@ import { from } from 'rxjs'; import { map } from 'rxjs/operators'; -import { mlFieldFormatService } from '../../services/field_format_service'; +import type { MlFieldFormatService } from '../../services/field_format_service'; import { mlJobService } from '../../services/job_service'; import { EXPLORER_ACTION } from '../explorer_constants'; import { createJobs } from '../explorer_utils'; -export function jobSelectionActionCreator(selectedJobIds: string[]) { +export function jobSelectionActionCreator( + mlFieldFormatService: MlFieldFormatService, + selectedJobIds: string[] +) { return from(mlFieldFormatService.populateFormats(selectedJobIds)).pipe( map((resp) => { if (resp.error) { 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 53b9edc97cb6b..4228881a5e4fe 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 @@ -17,29 +17,29 @@ import { AnomalyChartsStateService } from './anomaly_charts_state_service'; import { AnomalyExplorerChartsService } from '../services/anomaly_explorer_charts_service'; import { useTableSeverity } from '../components/controls/select_severity'; import { AnomalyDetectionAlertsStateService } from './alerts'; - -export type AnomalyExplorerContextValue = - | { - anomalyExplorerChartsService: AnomalyExplorerChartsService; - anomalyExplorerCommonStateService: AnomalyExplorerCommonStateService; - anomalyTimelineService: AnomalyTimelineService; - anomalyTimelineStateService: AnomalyTimelineStateService; - chartsStateService: AnomalyChartsStateService; - anomalyDetectionAlertsStateService: AnomalyDetectionAlertsStateService; - } - | undefined; +import { explorerServiceFactory, type ExplorerService } from './explorer_dashboard_service'; + +export interface AnomalyExplorerContextValue { + anomalyExplorerChartsService: AnomalyExplorerChartsService; + anomalyExplorerCommonStateService: AnomalyExplorerCommonStateService; + anomalyTimelineService: AnomalyTimelineService; + anomalyTimelineStateService: AnomalyTimelineStateService; + chartsStateService: AnomalyChartsStateService; + anomalyDetectionAlertsStateService: AnomalyDetectionAlertsStateService; + explorerService: ExplorerService; +} /** * Context of the Anomaly Explorer page. */ -export const AnomalyExplorerContext = React.createContext(undefined); +export const AnomalyExplorerContext = React.createContext( + undefined +); /** * Hook for consuming {@link AnomalyExplorerContext}. */ -export function useAnomalyExplorerContext(): - | Exclude - | never { +export function useAnomalyExplorerContext() { const context = useContext(AnomalyExplorerContext); if (context === undefined) { @@ -59,7 +59,7 @@ export const AnomalyExplorerContextProvider: FC = ({ children }) => { const { services: { - mlServices: { mlApiServices }, + mlServices: { mlApiServices, mlFieldFormatService }, uiSettings, data, }, @@ -70,10 +70,17 @@ export const AnomalyExplorerContextProvider: FC = ({ children }) => { // eslint-disable-next-line react-hooks/exhaustive-deps const mlResultsService = useMemo(() => mlResultsServiceProvider(mlApiServices), []); - const [anomalyExplorerContextValue, setAnomalyExplorerContextValue] = - useState(undefined); + const [anomalyExplorerContextValue, setAnomalyExplorerContextValue] = useState< + AnomalyExplorerContextValue | undefined + >(undefined); + // It might look tempting to refactor this into `useMemo()` and just return + // `anomalyExplorerContextValue`, but these services internally might call other state + // 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 anomalyTimelineService = new AnomalyTimelineService( timefilter, uiSettings, @@ -118,6 +125,7 @@ export const AnomalyExplorerContextProvider: FC = ({ children }) => { anomalyTimelineStateService, chartsStateService, anomalyDetectionAlertsStateService, + explorerService, }); return () => { diff --git a/x-pack/plugins/ml/public/application/explorer/explorer.tsx b/x-pack/plugins/ml/public/application/explorer/explorer.tsx index 7d04cfee36c66..04e12740059b8 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer.tsx +++ b/x-pack/plugins/ml/public/application/explorer/explorer.tsx @@ -79,6 +79,7 @@ import { useMlKibana, useMlLocator } from '../contexts/kibana'; import { useAnomalyExplorerContext } from './anomaly_explorer_context'; import { ML_ANOMALY_EXPLORER_PANELS } from '../../../common/types/storage'; import { AlertsPanel } from './alerts'; +import { useMlIndexUtils } from '../util/index_service'; interface ExplorerPageProps { jobSelectorProps: JobSelectorProps; @@ -371,6 +372,7 @@ export const Explorer: FC = ({ }, } = useMlKibana(); const { euiTheme } = useEuiTheme(); + const mlIndexUtils = useMlIndexUtils(); const mlLocator = useMlLocator(); const { @@ -444,7 +446,7 @@ export const Explorer: FC = ({ useEffect(() => { if (!noJobsSelected) { - getDataViewsAndIndicesWithGeoFields(selectedJobs, dataViewsService) + getDataViewsAndIndicesWithGeoFields(selectedJobs, dataViewsService, mlIndexUtils) .then(({ sourceIndicesWithGeoFieldsMap, dataViews: dv }) => { setSourceIndicesWithGeoFields(sourceIndicesWithGeoFieldsMap); setDataViews(dv); diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.js index 175b58a72c0a5..54c079c745cd2 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.js @@ -24,6 +24,7 @@ import { getSeverityWithLow, } from '@kbn/ml-anomaly-utils'; import { formatHumanReadableDateTime } from '@kbn/ml-date-utils'; +import { context } from '@kbn/kibana-react-plugin/public'; import { formatValue } from '../../formatters/format_value'; import { @@ -34,7 +35,6 @@ import { chartExtendedLimits, } from '../../util/chart_utils'; import { LoadingIndicator } from '../../components/loading_indicator/loading_indicator'; -import { mlFieldFormatService } from '../../services/field_format_service'; import { CHART_TYPE } from '../explorer_constants'; import { TRANSPARENT_BACKGROUND } from './constants'; @@ -49,6 +49,8 @@ const CONTENT_WRAPPER_HEIGHT = 215; const Y_AXIS_LABEL_THRESHOLD = 10; export class ExplorerChartDistribution extends React.Component { + static contextType = context; + static propTypes = { seriesConfig: PropTypes.object, severity: PropTypes.number, @@ -83,7 +85,10 @@ export class ExplorerChartDistribution extends React.Component { return; } - const fieldFormat = mlFieldFormatService.getFieldFormat(config.jobId, config.detectorIndex); + const fieldFormat = this.context.services.mlServices.mlFieldFormatService.getFieldFormat( + config.jobId, + config.detectorIndex + ); let vizWidth = 0; const chartHeight = 170; diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.test.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.test.js index 5a146c51b61c1..123c377506d4a 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.test.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.test.js @@ -7,13 +7,9 @@ import { chartData as mockChartData } from './__mocks__/mock_chart_data_rare'; import seriesConfig from './__mocks__/mock_series_config_rare.json'; -jest.mock('../../services/field_format_service', () => ({ - mlFieldFormatService: { - getFieldFormat: jest.fn(), - }, -})); import { mountWithIntl } from '@kbn/test-jest-helpers'; +import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import React from 'react'; import { ExplorerChartDistribution } from './explorer_chart_distribution'; @@ -28,6 +24,15 @@ const utilityProps = { x: 10432423, }, }; + +const servicesMock = { + mlServices: { + mlFieldFormatService: { + getFieldFormat: jest.fn(), + }, + }, +}; + describe('ExplorerChart', () => { const mlSelectSeverityServiceMock = { state: { @@ -49,11 +54,13 @@ describe('ExplorerChart', () => { }; const wrapper = mountWithIntl( - + + + ); // without setting any attributes and corresponding data @@ -74,12 +81,14 @@ describe('ExplorerChart', () => { }; const wrapper = mountWithIntl( - + + + ); // test if the loading indicator is shown @@ -106,14 +115,16 @@ describe('ExplorerChart', () => { // We create the element including a wrapper which sets the width: return mountWithIntl( -
- -
+ +
+ +
+
); } diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js index 95933451d6f47..82c7fb4eb4daf 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js @@ -24,6 +24,7 @@ import { getSeverityWithLow, } from '@kbn/ml-anomaly-utils'; import { formatHumanReadableDateTime } from '@kbn/ml-date-utils'; +import { context } from '@kbn/kibana-react-plugin/public'; import { formatValue } from '../../formatters/format_value'; import { @@ -39,13 +40,14 @@ import { getMultiBucketImpactTooltipValue, } from '../../util/chart_utils'; import { LoadingIndicator } from '../../components/loading_indicator/loading_indicator'; -import { mlFieldFormatService } from '../../services/field_format_service'; import { TRANSPARENT_BACKGROUND } from './constants'; const CONTENT_WRAPPER_HEIGHT = 215; const CONTENT_WRAPPER_CLASS = 'ml-explorer-chart-content-wrapper'; export class ExplorerChartSingleMetric extends React.Component { + static contextType = context; + static propTypes = { tooManyBuckets: PropTypes.bool, seriesConfig: PropTypes.object, @@ -85,7 +87,10 @@ export class ExplorerChartSingleMetric extends React.Component { return; } - const fieldFormat = mlFieldFormatService.getFieldFormat(config.jobId, config.detectorIndex); + const fieldFormat = this.context.services.mlServices.mlFieldFormatService.getFieldFormat( + config.jobId, + config.detectorIndex + ); let vizWidth = 0; const chartHeight = 170; diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.test.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.test.js index 3bc38c5754e88..baaca53324a72 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.test.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.test.js @@ -7,13 +7,9 @@ import { chartData as mockChartData } from './__mocks__/mock_chart_data'; import seriesConfig from './__mocks__/mock_series_config_filebeat.json'; -jest.mock('../../services/field_format_service', () => ({ - mlFieldFormatService: { - getFieldFormat: jest.fn(), - }, -})); import { mountWithIntl } from '@kbn/test-jest-helpers'; +import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import React from 'react'; import { ExplorerChartSingleMetric } from './explorer_chart_single_metric'; @@ -50,12 +46,14 @@ describe('ExplorerChart', () => { }; const wrapper = mountWithIntl( - + + + ); // without setting any attributes and corresponding data @@ -76,13 +74,15 @@ describe('ExplorerChart', () => { }; const wrapper = mountWithIntl( - + + + ); // test if the loading indicator is shown @@ -110,15 +110,17 @@ describe('ExplorerChart', () => { // We create the element including a wrapper which sets the width: return mountWithIntl( -
- -
+ +
+ +
+
); } 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 766ab0ee7f723..38f8eca99ae68 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 @@ -9,6 +9,7 @@ import React from 'react'; import { mount, shallow } from 'enzyme'; import { I18nProvider } from '@kbn/i18n-react'; +import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { getDefaultChartsData } from './explorer_charts_container_service'; import { ExplorerChartsContainer, getEntitiesQuery } from './explorer_charts_container'; @@ -20,23 +21,12 @@ 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/field_format_service', () => ({ - mlFieldFormatService: { - getFieldFormat: jest.fn(), - }, -})); jest.mock('../../services/job_service', () => ({ mlJobService: { getJob: jest.fn(), }, })); -jest.mock('@kbn/kibana-react-plugin/public', () => ({ - withKibana: (comp) => { - return comp; - }, -})); - jest.mock('../../contexts/kibana', () => ({ useMlKibana: () => { return { @@ -95,7 +85,13 @@ describe('ExplorerChartsContainer', () => { test('Minimal Initialization', () => { const wrapper = shallow( - + + + ); @@ -120,7 +116,9 @@ describe('ExplorerChartsContainer', () => { }; const wrapper = mount( - + + + ); @@ -147,7 +145,9 @@ describe('ExplorerChartsContainer', () => { }; const wrapper = mount( - + + + ); 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 5bcd305389d39..457f588573f1c 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 @@ -17,6 +17,7 @@ import { DeepPartial } from '../../../common/types/common'; import { jobSelectionActionCreator } from './actions'; import { EXPLORER_ACTION } from './explorer_constants'; import { explorerReducer, getExplorerDefaultState, ExplorerState } from './reducers'; +import type { MlFieldFormatService } from '../services/field_format_service'; type ExplorerAction = Action | Observable; export const explorerAction$ = new Subject(); @@ -49,7 +50,7 @@ const setExplorerDataActionCreator = (payload: DeepPartial) => ({ }); // Export observable state and action dispatchers as service -export const explorerService = { +export const explorerServiceFactory = (mlFieldFormatService: MlFieldFormatService) => ({ state$: explorerState$, clearExplorerData: () => { explorerAction$.next({ type: EXPLORER_ACTION.CLEAR_EXPLORER_DATA }); @@ -61,7 +62,7 @@ export const explorerService = { explorerAction$.next({ type: EXPLORER_ACTION.CLEAR_JOBS }); }, updateJobSelection: (selectedJobIds: string[]) => { - explorerAction$.next(jobSelectionActionCreator(selectedJobIds)); + explorerAction$.next(jobSelectionActionCreator(mlFieldFormatService, selectedJobIds)); }, setExplorerData: (payload: DeepPartial) => { explorerAction$.next(setExplorerDataActionCreator(payload)); @@ -69,6 +70,6 @@ export const explorerService = { setChartsDataLoading: () => { explorerAction$.next({ type: EXPLORER_ACTION.SET_CHARTS_DATA_LOADING }); }, -}; +}); -export type ExplorerService = typeof explorerService; +export type ExplorerService = ReturnType; 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 a3c464898ca77..9aa68e0a3fa43 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_utils.ts +++ b/x-pack/plugins/ml/public/application/explorer/explorer_utils.ts @@ -30,7 +30,7 @@ import { ANNOTATIONS_TABLE_DEFAULT_QUERY_SIZE, ANOMALIES_TABLE_DEFAULT_QUERY_SIZE, } from '../../../common/constants/search'; -import { getDataViewIdFromName } from '../util/index_utils'; +import type { MlIndexUtils } from '../util/index_service'; import { isSourceDataChartableForDetector, isModelPlotChartableForDetector, @@ -633,7 +633,8 @@ export function removeFilterFromQueryString( // Returns an object mapping job ids to source indices which map to geo fields for that index export async function getDataViewsAndIndicesWithGeoFields( selectedJobs: Array, - dataViewsService: DataViewsContract + dataViewsService: DataViewsContract, + mlIndexUtils: MlIndexUtils ): Promise<{ sourceIndicesWithGeoFieldsMap: SourceIndicesWithGeoFields; dataViews: DataView[] }> { const sourceIndicesWithGeoFieldsMap: SourceIndicesWithGeoFields = {}; // Avoid searching for data view again if previous job already has same source index @@ -654,7 +655,8 @@ export async function getDataViewsAndIndicesWithGeoFields( if (Array.isArray(sourceIndices)) { for (const sourceIndex of sourceIndices) { const cachedDV = dataViewsMap.get(sourceIndex); - const dataViewId = cachedDV?.id ?? (await getDataViewIdFromName(sourceIndex)); + const dataViewId = + cachedDV?.id ?? (await mlIndexUtils.getDataViewIdFromName(sourceIndex)); if (dataViewId) { const dataView = cachedDV ?? (await dataViewsService.get(dataViewId)); diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/edit_utils.d.ts b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/edit_utils.d.ts deleted file mode 100644 index ea58dc1bfb26f..0000000000000 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/edit_utils.d.ts +++ /dev/null @@ -1,10 +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 { DataViewListItem } from '@kbn/data-views-plugin/common'; - -export function loadDataViewListItems(): Promise; 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 9798fbe119556..d587f83a50b5e 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,7 +8,6 @@ import { difference } from 'lodash'; import { getNewJobLimits } from '../../../../services/ml_server_info'; import { processCreatedBy } from '../../../../../../common/util/job_utils'; -import { getDataViews } from '../../../../util/dependency_cache'; import { ml } from '../../../../services/ml_api_service'; export function saveJob(job, newJobData, finish) { @@ -72,11 +71,6 @@ function saveDatafeed(datafeedConfig, job) { }); } -export async function loadDataViewListItems() { - const dataViewsService = getDataViews(); - return (await dataViewsService.getIdsWithTitle()).sort((a, b) => a.title.localeCompare(b.title)); -} - function extractDescription(job, newJobData) { const description = newJobData.description; if (newJobData.description !== job.description) { 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 f5f491426b0a3..456cf54142812 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 @@ -17,6 +17,7 @@ import { FilterStateStore } from '@kbn/es-query'; import type { Embeddable } from '@kbn/lens-plugin/public'; import type { MapEmbeddable } from '@kbn/maps-plugin/public'; 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 { Job, Datafeed } from '../../../../../common/types/anomaly_detection_jobs'; import { getFiltersForDSLQuery } from '../../../../../common/util/job_utils'; @@ -58,6 +59,7 @@ function mergeQueriesCheck( export class QuickJobCreatorBase { constructor( + protected readonly dataViews: DataViewsContract, protected readonly kibanaConfig: IUiSettingsClient, protected readonly timeFilter: TimefilterContract, protected readonly dashboardService: DashboardStart, 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 4df2b74347f47..0860f8c9f4d86 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 @@ -14,6 +14,7 @@ import type { } from '@kbn/lens-plugin/public'; import type { IUiSettingsClient } from '@kbn/core/public'; import type { TimefilterContract } from '@kbn/data-plugin/public'; +import type { DataViewsContract } from '@kbn/data-views-plugin/public'; import type { Filter, Query } from '@kbn/es-query'; import type { DashboardStart } from '@kbn/dashboard-plugin/public'; @@ -38,12 +39,13 @@ import { QuickJobCreatorBase, type CreateState } from '../job_from_dashboard'; export class QuickLensJobCreator extends QuickJobCreatorBase { constructor( private readonly lens: LensPublicStart, + dataViews: DataViewsContract, kibanaConfig: IUiSettingsClient, timeFilter: TimefilterContract, dashboardService: DashboardStart, mlApiServices: MlApiServices ) { - super(kibanaConfig, timeFilter, dashboardService, mlApiServices); + super(dataViews, kibanaConfig, timeFilter, dashboardService, mlApiServices); } public async createAndSaveJob( 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 c8ad1ee6942e4..d6a815777fa9d 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 @@ -12,6 +12,7 @@ import type { LensPublicStart, LensSavedObjectAttributes } from '@kbn/lens-plugi import type { IUiSettingsClient } from '@kbn/core-ui-settings-browser'; 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 { QuickLensJobCreator } from './quick_create_job'; import type { MlApiServices } from '../../../services/ml_api_service'; @@ -19,6 +20,7 @@ import { getDefaultQuery, getRisonValue } from '../utils/new_job_utils'; interface Dependencies { lens: LensPublicStart; + dataViews: DataViewsContract; kibanaConfig: IUiSettingsClient; timeFilter: TimefilterContract; dashboardService: DashboardStart; @@ -33,7 +35,7 @@ export async function resolver( filtersRisonString: string, layerIndexRisonString: string ) { - const { lens, mlApiServices, timeFilter, kibanaConfig, dashboardService } = deps; + const { dataViews, lens, mlApiServices, timeFilter, kibanaConfig, dashboardService } = deps; if (lensSavedObjectRisonString === undefined) { throw new Error('Cannot create visualization'); } @@ -51,6 +53,7 @@ export async function resolver( const jobCreator = new QuickLensJobCreator( lens, + dataViews, kibanaConfig, timeFilter, dashboardService, 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 4142891daef41..4331c665172a7 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 @@ -10,11 +10,10 @@ import type { MapEmbeddable } from '@kbn/maps-plugin/public'; import type { IUiSettingsClient } from '@kbn/core/public'; import type { TimefilterContract } from '@kbn/data-plugin/public'; import type { Filter, Query } from '@kbn/es-query'; -import type { DataView } from '@kbn/data-views-plugin/public'; +import type { DataView, DataViewsContract } from '@kbn/data-views-plugin/public'; import type { DashboardStart } from '@kbn/dashboard-plugin/public'; import type { MlApiServices } from '../../../services/ml_api_service'; -import { getDataViews } from '../../../util/dependency_cache'; import { CREATED_BY_LABEL, JOB_TYPE, @@ -40,12 +39,13 @@ interface VisDescriptor { export class QuickGeoJobCreator extends QuickJobCreatorBase { constructor( + dataViews: DataViewsContract, kibanaConfig: IUiSettingsClient, timeFilter: TimefilterContract, dashboardService: DashboardStart, mlApiServices: MlApiServices ) { - super(kibanaConfig, timeFilter, dashboardService, mlApiServices); + super(dataViews, kibanaConfig, timeFilter, dashboardService, mlApiServices); } public async createAndSaveGeoJob({ @@ -250,7 +250,7 @@ export class QuickGeoJobCreator extends QuickJobCreatorBase { }: VisDescriptor) { const dataView: DataView = sourceDataView ? sourceDataView - : await getDataViews().get(dataViewId!, true); + : await this.dataViews.get(dataViewId!, true); const jobConfig = createEmptyJob(); const datafeedConfig = createEmptyDatafeed(dataView.getIndexPattern()); 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 0728ca3c61d59..c102f0ee6ddd9 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 @@ -8,12 +8,14 @@ import type { IUiSettingsClient } from '@kbn/core-ui-settings-browser'; 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 { QuickGeoJobCreator } from './quick_create_job'; import { getDefaultQuery, getRisonValue } from '../utils/new_job_utils'; interface Dependencies { + dataViews: DataViewsContract; kibanaConfig: IUiSettingsClient; timeFilter: TimefilterContract; dashboardService: DashboardStart; @@ -30,7 +32,7 @@ export async function resolver( toRisonString: string, layerRisonString?: string ) { - const { kibanaConfig, timeFilter, dashboardService, mlApiServices } = deps; + const { dataViews, kibanaConfig, timeFilter, dashboardService, mlApiServices } = deps; const defaultLayer = { query: getDefaultQuery(), filters: [] }; const dashboard = getRisonValue(dashboardRisonString, defaultLayer); @@ -49,6 +51,7 @@ export async function resolver( const to = getRisonValue(toRisonString, ''); const jobCreator = new QuickGeoJobCreator( + dataViews, kibanaConfig, timeFilter, dashboardService, 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 220b0c0446670..f1073b4cb39bc 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 @@ -8,7 +8,8 @@ import type { IUiSettingsClient } from '@kbn/core/public'; import type { DataPublicPluginStart, TimefilterContract } from '@kbn/data-plugin/public'; import type { DashboardStart } from '@kbn/dashboard-plugin/public'; -import { DataViewField, DataView } from '@kbn/data-views-plugin/common'; +import type { DataViewField, DataView } from '@kbn/data-views-plugin/common'; +import type { DataViewsContract } from '@kbn/data-views-plugin/public'; import type { TimeRange } from '@kbn/es-query'; import { i18n } from '@kbn/i18n'; import { MLCATEGORY, ML_JOB_AGGREGATION } from '@kbn/ml-anomaly-utils'; @@ -30,13 +31,14 @@ export type CategorizationType = typeof CATEGORIZATION_TYPE[keyof typeof CATEGOR export class QuickCategorizationJobCreator extends QuickJobCreatorBase { constructor( + dataViews: DataViewsContract, kibanaConfig: IUiSettingsClient, timeFilter: TimefilterContract, dashboardService: DashboardStart, private data: DataPublicPluginStart, mlApiServices: MlApiServices ) { - super(kibanaConfig, timeFilter, dashboardService, mlApiServices); + super(dataViews, kibanaConfig, timeFilter, dashboardService, mlApiServices); } public async createAndSaveJob( 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 0f8462128d2cf..69ca29f9a5ab4 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 @@ -52,6 +52,7 @@ export async function resolver( const stopOnWarn = getRisonValue(stopOnWarnRisonString, false); const jobCreator = new QuickCategorizationJobCreator( + data.dataViews, kibanaConfig, timeFilter, dashboardService, 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 0ea71f4220858..258ebeb148dbf 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 @@ -76,8 +76,8 @@ export const JobsListPage: FC = ({ const theme$ = coreStart.theme.theme$; const mlServices = useMemo( - () => getMlGlobalServices(coreStart.http, usageCollection), - [coreStart.http, usageCollection] + () => getMlGlobalServices(coreStart.http, data.dataViews, usageCollection), + [coreStart.http, data.dataViews, usageCollection] ); const check = async () => { diff --git a/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx b/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx index 5a3e129bbfaaa..5a8399418b399 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx @@ -28,7 +28,6 @@ import { Explorer } from '../../explorer'; import { mlJobService } from '../../services/job_service'; import { ml } from '../../services/ml_api_service'; import { useExplorerData } from '../../explorer/actions'; -import { explorerService } from '../../explorer/explorer_dashboard_service'; import { getDateFormatTz } from '../../explorer/explorer_utils'; import { useJobSelection } from '../../components/job_selector/use_job_selection'; import { useTableInterval } from '../../components/controls/select_interval'; @@ -118,8 +117,9 @@ const ExplorerUrlStateManager: FC = ({ jobsWithTim (job) => jobIds.includes(job.id) && job.isRunning === true ); - const explorerState = useObservable(explorerService.state$); const anomalyExplorerContext = useAnomalyExplorerContext(); + const { explorerService } = anomalyExplorerContext; + const explorerState = useObservable(anomalyExplorerContext.explorerService.state$); const refresh = useRefresh(); const lastRefresh = refresh?.lastRefresh ?? 0; @@ -177,6 +177,7 @@ const ExplorerUrlStateManager: FC = ({ jobsWithTim // clear any data to prevent next page from rendering old charts explorerService.clearExplorerData(); }; + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const [explorerData, loadExplorerData] = useExplorerData(); @@ -185,6 +186,7 @@ const ExplorerUrlStateManager: FC = ({ jobsWithTim if (explorerData !== undefined && Object.keys(explorerData).length > 0) { explorerService.setExplorerData(explorerData); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [explorerData]); const [tableInterval] = useTableInterval(); 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 c030cd8eeb34b..1ad7058b43622 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 @@ -30,6 +30,7 @@ const PageWrapper: FC = ({ location }) => { const { services: { data: { + dataViews, query: { timefilter: { timefilter: timeFilter }, }, @@ -44,7 +45,7 @@ const PageWrapper: FC = ({ location }) => { const { context } = useRouteResolver('full', ['canCreateJob'], { redirect: () => resolver( - { lens, mlApiServices, timeFilter, kibanaConfig, dashboardService }, + { dataViews, lens, mlApiServices, 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 2c0d1d77b3d47..81dfc5b218ef6 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 @@ -37,6 +37,7 @@ const PageWrapper: FC = ({ location }) => { const { services: { data: { + dataViews, query: { timefilter: { timefilter: timeFilter }, }, @@ -50,7 +51,7 @@ const PageWrapper: FC = ({ location }) => { const { context } = useRouteResolver('full', ['canCreateJob'], { redirect: () => resolver( - { mlApiServices, timeFilter, kibanaConfig, dashboardService }, + { dataViews, mlApiServices, timeFilter, kibanaConfig, dashboardService }, dashboard, dataViewId, embeddable, 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 8519a13e7d7bc..de4885861d331 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,6 @@ */ import { mlFunctionToESAggregation } from '../../../common/util/job_utils'; -import { getDataViewById, getDataViewIdFromName } from '../util/index_utils'; import { mlJobService } from './job_service'; import type { MlIndexUtils } from '../util/index_service'; import type { MlApiServices } from './ml_api_service'; @@ -20,7 +19,7 @@ export class FieldFormatService { indexPatternIdsByJob: IndexPatternIdsByJob = {}; formatsByJob: FormatsByJobId = {}; - constructor(private mlApiServices?: MlApiServices, private mlIndexUtils?: MlIndexUtils) {} + constructor(private mlApiServices: MlApiServices, private mlIndexUtils: MlIndexUtils) {} // 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 @@ -36,7 +35,6 @@ export class FieldFormatService { ( await Promise.all( jobIds.map(async (jobId) => { - const getDataViewId = this.mlIndexUtils?.getDataViewIdFromName ?? getDataViewIdFromName; let jobObj; if (this.mlApiServices) { const { jobs } = await this.mlApiServices.getJobs({ jobId }); @@ -46,7 +44,9 @@ export class FieldFormatService { } return { jobId, - dataViewId: await getDataViewId(jobObj.datafeed_config!.indices.join(',')), + dataViewId: await this.mlIndexUtils.getDataViewIdFromName( + jobObj.datafeed_config!.indices.join(',') + ), }; }) ) @@ -81,7 +81,6 @@ export class FieldFormatService { async getFormatsForJob(jobId: string): Promise { let jobObj; - const getDataView = this.mlIndexUtils?.getDataViewById ?? getDataViewById; if (this.mlApiServices) { const { jobs } = await this.mlApiServices.getJobs({ jobId }); jobObj = jobs[0]; @@ -94,7 +93,7 @@ export class FieldFormatService { const dataViewId = this.indexPatternIdsByJob[jobId]; if (dataViewId !== undefined) { // Load the full data view configuration to obtain the formats of each field. - const dataView = await getDataView(dataViewId); + const dataView = await this.mlIndexUtils.getDataViewById(dataViewId); // Store the FieldFormat for each job by detector_index. const fieldList = dataView.fields; detectors.forEach((dtr) => { @@ -114,5 +113,4 @@ export class FieldFormatService { } } -export const mlFieldFormatService = new FieldFormatService(); -export type MlFieldFormatService = typeof mlFieldFormatService; +export type MlFieldFormatService = FieldFormatService; 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 465e4528bd9c5..d21dc7ea03d50 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 @@ -8,13 +8,15 @@ import { TimeRange } from '@kbn/data-plugin/common/query/timefilter/types'; import { CombinedJob, Datafeed, Job } from '../../../common/types/anomaly_detection_jobs'; import { Calendar } from '../../../common/types/calendars'; +import type { ToastNotificationService } from './toast_notification_service'; +import type { MlApiServices } from './ml_api_service'; export interface ExistingJobsAndGroups { jobIds: string[]; groupIds: string[]; } -declare interface JobService { +export declare interface MlJobService { jobs: CombinedJob[]; createResultsUrlForJobs: (jobs: any[], target: string, timeRange?: TimeRange) => string; tempJobCloningObjects: { @@ -46,4 +48,8 @@ declare interface JobService { detectorsByJob: Record; } -export const mlJobService: JobService; +export const mlJobService: MlJobService; +export const mlJobServiceFactory: ( + toastNotificationServiceOverride?: ToastNotificationService, + mlOverride?: MlApiServices +) => 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 0fc78db628fcb..a82c1af038489 100644 --- a/x-pack/plugins/ml/public/application/services/job_service.js +++ b/x-pack/plugins/ml/public/application/services/job_service.js @@ -21,7 +21,20 @@ let jobs = []; let datafeedIds = {}; class JobService { - constructor() { + // 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; + // tempJobCloningObjects -> used to pass a job object between the job management page and // and the advanced wizard. // if populated when loading the advanced wizard, the job is used for cloning. @@ -48,16 +61,26 @@ class JobService { this.customUrlsByJob = {}; } + getMl() { + return this.ml ?? ml; + } + + getToastNotificationService() { + return this.toastNotificationService ?? getToastNotificationService(); + } + loadJobs() { return new Promise((resolve, reject) => { jobs = []; datafeedIds = {}; - ml.getJobs() + this.getMl() + .getJobs() .then((resp) => { jobs = resp.jobs; // load jobs stats - ml.getJobStats() + this.getMl() + .getJobStats() .then((statsResp) => { // merge jobs stats into jobs for (let i = 0; i < jobs.length; i++) { @@ -107,7 +130,7 @@ class JobService { function error(err) { console.log('jobService error getting list of jobs:', err); - getToastNotificationService().displayErrorToast(err); + this.getToastNotificationService().displayErrorToast(err); reject({ jobs, err }); } }); @@ -127,13 +150,15 @@ class JobService { refreshJob(jobId) { return new Promise((resolve, reject) => { - ml.getJobs({ jobId }) + this.getMl() + .getJobs({ jobId }) .then((resp) => { if (resp.jobs && resp.jobs.length) { const newJob = resp.jobs[0]; // load jobs stats - ml.getJobStats({ jobId }) + this.getMl() + .getJobStats({ jobId }) .then((statsResp) => { // merge jobs stats into jobs for (let j = 0; j < statsResp.jobs.length; j++) { @@ -188,7 +213,7 @@ class JobService { function error(err) { console.log('JobService error getting list of jobs:', err); - getToastNotificationService().displayErrorToast(err); + this.getToastNotificationService().displayErrorToast(err); reject({ jobs, err }); } }); @@ -198,12 +223,14 @@ class JobService { return new Promise((resolve, reject) => { const sId = datafeedId !== undefined ? { datafeed_id: datafeedId } : undefined; - ml.getDatafeeds(sId) + this.getMl() + .getDatafeeds(sId) .then((resp) => { const datafeeds = resp.datafeeds; // load datafeeds stats - ml.getDatafeedStats() + this.getMl() + .getDatafeedStats() .then((statsResp) => { // merge datafeeds stats into datafeeds for (let i = 0; i < datafeeds.length; i++) { @@ -226,7 +253,7 @@ class JobService { function error(err) { console.log('loadDatafeeds error getting list of datafeeds:', err); - getToastNotificationService().displayErrorToast(err); + this.getToastNotificationService().displayErrorToast(err); reject({ jobs, err }); } }); @@ -236,7 +263,8 @@ class JobService { return new Promise((resolve, reject) => { const datafeedId = this.getDatafeedId(jobId); - ml.getDatafeedStats({ datafeedId }) + this.getMl() + .getDatafeedStats({ datafeedId }) .then((resp) => { // console.log('updateSingleJobCounts controller query response:', resp); const datafeeds = resp.datafeeds; @@ -261,7 +289,7 @@ class JobService { } // return the promise chain - return ml.addJob({ jobId: job.job_id, job }).then(func).catch(func); + return this.getMl().addJob({ jobId: job.job_id, job }).then(func).catch(func); } cloneDatafeed(datafeed) { @@ -285,18 +313,18 @@ class JobService { } openJob(jobId) { - return ml.openJob({ jobId }); + return this.getMl().openJob({ jobId }); } closeJob(jobId) { - return ml.closeJob({ jobId }); + return this.getMl().closeJob({ jobId }); } saveNewDatafeed(datafeedConfig, jobId) { const datafeedId = `datafeed-${jobId}`; datafeedConfig.job_id = jobId; - return ml.addDatafeed({ + return this.getMl().addDatafeed({ datafeedId, datafeedConfig, }); @@ -312,11 +340,12 @@ class JobService { end++; } - ml.startDatafeed({ - datafeedId, - start, - end, - }) + this.getMl() + .startDatafeed({ + datafeedId, + start, + end, + }) .then((resp) => { resolve(resp); }) @@ -328,29 +357,30 @@ class JobService { } forceStartDatafeeds(dIds, start, end) { - return ml.jobs.forceStartDatafeeds(dIds, start, end); + return this.getMl().jobs.forceStartDatafeeds(dIds, start, end); } stopDatafeeds(dIds) { - return ml.jobs.stopDatafeeds(dIds); + return this.getMl().jobs.stopDatafeeds(dIds); } deleteJobs(jIds, deleteUserAnnotations, deleteAlertingRules) { - return ml.jobs.deleteJobs(jIds, deleteUserAnnotations, deleteAlertingRules); + return this.getMl().jobs.deleteJobs(jIds, deleteUserAnnotations, deleteAlertingRules); } closeJobs(jIds) { - return ml.jobs.closeJobs(jIds); + return this.getMl().jobs.closeJobs(jIds); } resetJobs(jIds, deleteUserAnnotations) { - return ml.jobs.resetJobs(jIds, deleteUserAnnotations); + return this.getMl().jobs.resetJobs(jIds, deleteUserAnnotations); } validateDetector(detector) { return new Promise((resolve, reject) => { if (detector) { - ml.validateDetector({ detector }) + this.getMl() + .validateDetector({ detector }) .then((resp) => { resolve(resp); }) @@ -402,7 +432,7 @@ class JobService { async getJobAndGroupIds() { try { - return await ml.jobs.getAllJobAndGroupIds(); + return await this.getMl().jobs.getAllJobAndGroupIds(); } catch (error) { return { jobIds: [], @@ -563,3 +593,5 @@ function createResultsUrl(jobIds, start, end, resultsPage, mode = 'absolute') { } export const mlJobService = new JobService(); +export const mlJobServiceFactory = (toastNotificationServiceOverride, mlOverride) => + new JobService(toastNotificationServiceOverride, mlOverride); 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 9ed67ed6ec446..b8f90717d29ee 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 @@ -22,6 +22,7 @@ import { EuiPopover } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { getFormattedSeverityScore, getSeverityWithLow } from '@kbn/ml-anomaly-utils'; import { formatHumanReadableDateTimeSeconds } from '@kbn/ml-date-utils'; +import { context } from '@kbn/kibana-react-plugin/public'; import { formatValue } from '../../../formatters/format_value'; import { @@ -41,7 +42,6 @@ import { mlTableService } from '../../../services/table_service'; import { ContextChartMask } from '../context_chart_mask'; import { findChartPointForAnomalyTime } from '../../timeseriesexplorer_utils'; import { mlEscape } from '../../../util/string_utils'; -import { mlFieldFormatService } from '../../../services/field_format_service'; import { ANNOTATION_MASK_ID, getAnnotationBrush, @@ -53,7 +53,6 @@ import { ANNOTATION_MIN_WIDTH, } from './timeseries_chart_annotations'; import { MlAnnotationUpdatesContext } from '../../../contexts/ml/ml_annotation_updates_context'; -import { context } from '@kbn/kibana-react-plugin/public'; import { LinksMenuUI } from '../../../components/anomalies_table/links_menu'; import { RuleEditorFlyout } from '../../../components/rule_editor'; @@ -304,12 +303,10 @@ class TimeseriesChartIntl extends Component { chartElement.selectAll('*').remove(); if (typeof selectedJob !== 'undefined') { - this.fieldFormat = this.context?.services?.mlServices?.mlFieldFormatService - ? this.context.services.mlServices.mlFieldFormatService.getFieldFormat( - selectedJob.job_id, - detectorIndex - ) - : mlFieldFormatService.getFieldFormat(selectedJob.job_id, detectorIndex); + this.fieldFormat = this.context.services.mlServices.mlFieldFormatService.getFieldFormat( + selectedJob.job_id, + detectorIndex + ); } else { return; } diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js index cb1f18fd82358..21a2c1488a13b 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js @@ -19,6 +19,7 @@ import React, { createRef, Fragment } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; +import { context } from '@kbn/kibana-react-plugin/public'; import { EuiCallOut, @@ -55,7 +56,6 @@ import { TimeseriesexplorerNoChartData } from './components/timeseriesexplorer_n import { TimeSeriesExplorerPage } from './timeseriesexplorer_page'; import { ml } from '../services/ml_api_service'; -import { mlFieldFormatService } from '../services/field_format_service'; import { mlForecastService } from '../services/forecast_service'; import { mlJobService } from '../services/job_service'; import { mlResultsService } from '../services/results_service'; @@ -137,6 +137,11 @@ export class TimeSeriesExplorer extends React.Component { */ contextChart$ = new Subject(); + /** + * Access ML services in react context. + */ + static contextType = context; + /** * Returns field names that don't have a selection yet. */ @@ -691,7 +696,7 @@ export class TimeSeriesExplorer extends React.Component { appStateHandler(APP_STATE_ACTION.SET_DETECTOR_INDEX, detectorId); } // Populate the map of jobs / detectors / field formatters for the selected IDs and refresh. - mlFieldFormatService.populateFormats([jobId]); + this.context.services.mlServices.mlFieldFormatService.populateFormats([jobId]); } componentDidMount() { 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 fd6b0239199bc..457b2994d3626 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 @@ -9,10 +9,10 @@ * React component for rendering Single Metric Viewer. */ -import { isEqual } from 'lodash'; +import { get, has, isEqual } from 'lodash'; import moment from 'moment-timezone'; import { Subject, Subscription, forkJoin } from 'rxjs'; -import { debounceTime, switchMap, tap, withLatestFrom } from 'rxjs/operators'; +import { map, debounceTime, switchMap, tap, withLatestFrom } from 'rxjs/operators'; import PropTypes from 'prop-types'; import React, { Fragment } from 'react'; @@ -32,6 +32,7 @@ import { } from '@elastic/eui'; import { TimeSeriesExplorerHelpPopover } from '../timeseriesexplorer_help_popover'; +import { ANOMALIES_TABLE_DEFAULT_QUERY_SIZE } from '../../../../common/constants/search'; import { isModelPlotEnabled, isModelPlotChartableForDetector, @@ -75,6 +76,8 @@ export class TimeSeriesExplorerEmbeddableChart extends React.Component { selectedDetectorIndex: PropTypes.number, selectedEntities: PropTypes.object, selectedForecastId: PropTypes.string, + tableInterval: PropTypes.string, + tableSeverity: PropTypes.number, zoom: PropTypes.object, toastNotificationService: PropTypes.object, dataViewsService: PropTypes.object, @@ -242,6 +245,74 @@ export class TimeSeriesExplorerEmbeddableChart extends React.Component { this.props.appStateHandler(APP_STATE_ACTION.SET_ZOOM, zoomState); }; + loadAnomaliesTableData = (earliestMs, latestMs) => { + const { + dateFormatTz, + selectedDetectorIndex, + selectedJob, + tableInterval, + tableSeverity, + functionDescription, + } = this.props; + const entityControls = this.getControlsForDetector(); + + return this.context.services.mlServices.mlApiServices.results + .getAnomaliesTableData( + [selectedJob.job_id], + this.getCriteriaFields(selectedDetectorIndex, entityControls), + [], + tableInterval, + tableSeverity, + earliestMs, + latestMs, + dateFormatTz, + ANOMALIES_TABLE_DEFAULT_QUERY_SIZE, + undefined, + undefined, + functionDescription + ) + .pipe( + map((resp) => { + const { mlJobService } = this.context.services.mlServices; + const anomalies = resp.anomalies; + const detectorsByJob = mlJobService.detectorsByJob; + anomalies.forEach((anomaly) => { + // Add a detector property to each anomaly. + // Default to functionDescription if no description available. + // TODO - when job_service is moved server_side, move this to server endpoint. + const jobId = anomaly.jobId; + const detector = get(detectorsByJob, [jobId, anomaly.detectorIndex]); + anomaly.detector = get( + detector, + ['detector_description'], + anomaly.source.function_description + ); + + // For detectors with rules, add a property with the rule count. + const customRules = detector.custom_rules; + if (customRules !== undefined) { + anomaly.rulesLength = customRules.length; + } + + // Add properties used for building the links menu. + // TODO - when job_service is moved server_side, move this to server endpoint. + if (has(mlJobService.customUrlsByJob, jobId)) { + anomaly.customUrls = mlJobService.customUrlsByJob[jobId]; + } + }); + + return { + tableData: { + anomalies, + interval: resp.interval, + examplesByJobId: resp.examplesByJobId, + showViewSeriesLink: false, + }, + }; + }) + ); + }; + setForecastId = (forecastId) => { this.props.appStateHandler(APP_STATE_ACTION.SET_FORECAST_ID, forecastId); }; @@ -562,7 +633,26 @@ export class TimeSeriesExplorerEmbeddableChart extends React.Component { } }), switchMap((selection) => { - return forkJoin([this.getFocusData(selection)]); + // Calculate the aggregation interval for the focus chart. + const bounds = { min: moment(selection.from), max: moment(selection.to) }; + + // Ensure the search bounds align to the bucketing interval so that the first and last buckets are complete. + // For sum or count detectors, short buckets would hold smaller values, and model bounds would also be affected + // to some extent with all detector functions if not searching complete buckets. + const searchBounds = this.getBoundsRoundedToInterval( + bounds, + this.getFocusAggregationInterval({ + from: selection.from, + to: selection.to, + }), + false + ); + + return forkJoin([ + this.getFocusData(selection), + // Load the data for the anomalies table. + this.loadAnomaliesTableData(searchBounds.min.valueOf(), searchBounds.max.valueOf()), + ]); }), withLatestFrom(this.contextChart$) ) @@ -674,11 +764,13 @@ export class TimeSeriesExplorerEmbeddableChart extends React.Component { showModelBounds, showModelBoundsCheckbox, swimlaneData, + tableData, zoomFrom, zoomTo, zoomFromFocusLoaded, zoomToFocusLoaded, chartDataError, + sourceIndicesWithGeoFields, } = this.state; const chartProps = { modelPlotEnabled, @@ -888,6 +980,8 @@ export class TimeSeriesExplorerEmbeddableChart extends React.Component { showForecast={showForecast} showModelBounds={showModelBounds} lastRefresh={lastRefresh} + tableData={tableData} + sourceIndicesWithGeoFields={sourceIndicesWithGeoFields} /> )} diff --git a/x-pack/plugins/ml/public/application/util/dependency_cache.ts b/x-pack/plugins/ml/public/application/util/dependency_cache.ts index d69b299b53cfd..b462207e67d9b 100644 --- a/x-pack/plugins/ml/public/application/util/dependency_cache.ts +++ b/x-pack/plugins/ml/public/application/util/dependency_cache.ts @@ -8,100 +8,52 @@ import type { DataPublicPluginSetup } from '@kbn/data-plugin/public'; import type { IUiSettingsClient, - ChromeStart, ApplicationStart, HttpStart, I18nStart, DocLinksStart, ToastsStart, - OverlayStart, - ThemeServiceStart, ChromeRecentlyAccessed, - IBasePath, } from '@kbn/core/public'; -import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; -import type { DashboardStart } from '@kbn/dashboard-plugin/public'; import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; -import type { DataViewsContract } from '@kbn/data-views-plugin/public'; -import type { SecurityPluginStart } from '@kbn/security-plugin/public'; import type { MapsStartApi } from '@kbn/maps-plugin/public'; -import type { DataVisualizerPluginStart } from '@kbn/data-visualizer-plugin/public'; -import type { SharePluginStart } from '@kbn/share-plugin/public'; -import type { LensPublicStart } from '@kbn/lens-plugin/public'; -import type { SavedSearchPublicPluginStart } from '@kbn/saved-search-plugin/public'; export interface DependencyCache { timefilter: DataPublicPluginSetup['query']['timefilter'] | null; config: IUiSettingsClient | null; - chrome: ChromeStart | null; docLinks: DocLinksStart | null; toastNotifications: ToastsStart | null; - overlays: OverlayStart | null; - theme: ThemeServiceStart | null; recentlyAccessed: ChromeRecentlyAccessed | null; fieldFormats: FieldFormatsStart | null; - autocomplete: UnifiedSearchPublicPluginStart['autocomplete'] | null; - basePath: IBasePath | null; - savedSearch: SavedSearchPublicPluginStart | null; application: ApplicationStart | null; http: HttpStart | null; - security: SecurityPluginStart | undefined | null; i18n: I18nStart | null; - dashboard: DashboardStart | null; maps: MapsStartApi | null; - dataVisualizer: DataVisualizerPluginStart | null; - dataViews: DataViewsContract | null; - share: SharePluginStart | null; - lens: LensPublicStart | null; } const cache: DependencyCache = { timefilter: null, config: null, - chrome: null, docLinks: null, toastNotifications: null, - overlays: null, - theme: null, recentlyAccessed: null, fieldFormats: null, - autocomplete: null, - basePath: null, - savedSearch: null, application: null, http: null, - security: null, i18n: null, - dashboard: null, maps: null, - dataVisualizer: null, - dataViews: null, - share: null, - lens: null, }; export function setDependencyCache(deps: Partial) { cache.timefilter = deps.timefilter || null; cache.config = deps.config || null; - cache.chrome = deps.chrome || null; cache.docLinks = deps.docLinks || null; cache.toastNotifications = deps.toastNotifications || null; - cache.overlays = deps.overlays || null; - cache.theme = deps.theme || null; cache.recentlyAccessed = deps.recentlyAccessed || null; cache.fieldFormats = deps.fieldFormats || null; - cache.autocomplete = deps.autocomplete || null; - cache.basePath = deps.basePath || null; - cache.savedSearch = deps.savedSearch || null; cache.application = deps.application || null; cache.http = deps.http || null; - cache.security = deps.security || null; cache.i18n = deps.i18n || null; - cache.dashboard = deps.dashboard || null; - cache.dataVisualizer = deps.dataVisualizer || null; - cache.dataViews = deps.dataViews || null; - cache.share = deps.share || null; - cache.lens = deps.lens || null; } export function getTimefilter() { @@ -110,12 +62,6 @@ export function getTimefilter() { } return cache.timefilter.timefilter; } -export function getTimeHistory() { - if (cache.timefilter === null) { - throw new Error("timefilter hasn't been initialized"); - } - return cache.timefilter.history; -} export function getDocLinks() { if (cache.docLinks === null) { @@ -131,20 +77,6 @@ export function getToastNotifications() { return cache.toastNotifications; } -export function getOverlays() { - if (cache.overlays === null) { - throw new Error("overlays haven't been initialized"); - } - return cache.overlays; -} - -export function getTheme() { - if (cache.theme === null) { - throw new Error("theme hasn't been initialized"); - } - return cache.theme; -} - export function getUiSettings() { if (cache.config === null) { throw new Error("uiSettings hasn't been initialized"); @@ -166,27 +98,6 @@ export function getFieldFormats() { return cache.fieldFormats; } -export function getAutocomplete() { - if (cache.autocomplete === null) { - throw new Error("autocomplete hasn't been initialized"); - } - return cache.autocomplete; -} - -export function getChrome() { - if (cache.chrome === null) { - throw new Error("chrome hasn't been initialized"); - } - return cache.chrome; -} - -export function getBasePath() { - if (cache.basePath === null) { - throw new Error("basePath hasn't been initialized"); - } - return cache.basePath; -} - export function getApplication() { if (cache.application === null) { throw new Error("application hasn't been initialized"); @@ -201,13 +112,6 @@ export function getHttp() { return cache.http; } -export function getSecurity() { - if (cache.security === null) { - throw new Error("security hasn't been initialized"); - } - return cache.security; -} - export function getI18n() { if (cache.i18n === null) { throw new Error("i18n hasn't been initialized"); @@ -215,41 +119,6 @@ export function getI18n() { return cache.i18n; } -export function getDashboard() { - if (cache.dashboard === null) { - throw new Error("dashboard hasn't been initialized"); - } - return cache.dashboard; -} - -export function getDataViews() { - if (cache.dataViews === null) { - throw new Error("dataViews hasn't been initialized"); - } - return cache.dataViews; -} - -export function getFileDataVisualizer() { - if (cache.dataVisualizer === null) { - throw new Error("dataVisualizer hasn't been initialized"); - } - return cache.dataVisualizer; -} - -export function getShare() { - if (cache.share === null) { - throw new Error("share hasn't been initialized"); - } - return cache.share; -} - -export function getLens() { - if (cache.lens === null) { - throw new Error("lens hasn't been initialized"); - } - return cache.lens; -} - export function clearCache() { Object.keys(cache).forEach((k) => { cache[k as keyof DependencyCache] = null; diff --git a/x-pack/plugins/ml/public/application/util/index_service.ts b/x-pack/plugins/ml/public/application/util/index_service.ts index f4b6a1fc13d77..62488208642ff 100644 --- a/x-pack/plugins/ml/public/application/util/index_service.ts +++ b/x-pack/plugins/ml/public/application/util/index_service.ts @@ -6,8 +6,11 @@ */ import type { DataView, DataViewsContract } from '@kbn/data-views-plugin/public'; + import type { Job } from '../../../common/types/anomaly_detection_jobs'; +import { useMlKibana } from '../contexts/kibana'; + // TODO Consolidate with legacy code in `ml/public/application/util/index_utils.ts`. export function indexServiceFactory(dataViewsService: DataViewsContract) { return { @@ -19,9 +22,6 @@ export function indexServiceFactory(dataViewsService: DataViewsContract) { * @returns The data view ID or null if it doesn't exist. */ async getDataViewIdFromName(name: string, job?: Job): Promise { - if (dataViewsService === null) { - throw new Error('Data views are not initialized!'); - } const dataViews = await dataViewsService.find(name); const dataView = dataViews.find((dv) => dv.getIndexPattern() === name); if (!dataView) { @@ -39,17 +39,20 @@ export function indexServiceFactory(dataViewsService: DataViewsContract) { return dataView.id ?? dataView.getIndexPattern(); }, getDataViewById(id: string): Promise { - if (dataViewsService === null) { - throw new Error('Data views are not initialized!'); - } - if (id) { return dataViewsService.get(id); } else { return dataViewsService.create({}); } }, + async loadDataViewListItems() { + return (await dataViewsService.getIdsWithTitle()).sort((a, b) => + a.title.localeCompare(b.title) + ); + }, }; } export type MlIndexUtils = ReturnType; + +export const useMlIndexUtils = () => indexServiceFactory(useMlKibana().services.data.dataViews); diff --git a/x-pack/plugins/ml/public/application/util/index_utils.ts b/x-pack/plugins/ml/public/application/util/index_utils.ts index 88ab0e2b1cfb8..a9074e573e55a 100644 --- a/x-pack/plugins/ml/public/application/util/index_utils.ts +++ b/x-pack/plugins/ml/public/application/util/index_utils.ts @@ -9,58 +9,7 @@ import { i18n } from '@kbn/i18n'; import type { SavedSearch, SavedSearchPublicPluginStart } from '@kbn/saved-search-plugin/public'; import type { Query, Filter } from '@kbn/es-query'; import type { DataView, DataViewField, DataViewsContract } from '@kbn/data-views-plugin/common'; -import type { Job } from '../../../common/types/anomaly_detection_jobs'; -import { getToastNotifications, getDataViews } from './dependency_cache'; - -export async function getDataViewNames() { - const dataViewsService = getDataViews(); - if (dataViewsService === null) { - throw new Error('Data views are not initialized!'); - } - return (await dataViewsService.getIdsWithTitle()).map(({ title }) => title); -} - -/** - * Retrieves the data view ID from the given name. - * If a job is passed in, a temporary data view will be created if the requested data view doesn't exist. - * @param name - The name or index pattern of the data view. - * @param job - Optional job object. - * @returns The data view ID or null if it doesn't exist. - */ -export async function getDataViewIdFromName(name: string, job?: Job): Promise { - const dataViewsService = getDataViews(); - if (dataViewsService === null) { - throw new Error('Data views are not initialized!'); - } - const dataViews = await dataViewsService.find(name); - const dataView = dataViews.find((dv) => dv.getIndexPattern() === name); - if (!dataView) { - if (job !== undefined) { - const tempDataView = await dataViewsService.create({ - id: undefined, - name, - title: name, - timeFieldName: job.data_description.time_field!, - }); - return tempDataView.id ?? null; - } - return null; - } - return dataView.id ?? dataView.getIndexPattern(); -} - -export function getDataViewById(id: string): Promise { - const dataViewsService = getDataViews(); - if (dataViewsService === null) { - throw new Error('Data views are not initialized!'); - } - - if (id) { - return dataViewsService.get(id); - } else { - return dataViewsService.create({}); - } -} +import { getToastNotifications } from './dependency_cache'; export interface DataViewAndSavedSearch { savedSearch: SavedSearch | null; diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_embeddable.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_embeddable.tsx index d1a68091f2fee..e4a51f656249c 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_embeddable.tsx +++ b/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_embeddable.tsx @@ -76,7 +76,15 @@ export class AnomalyChartsEmbeddable extends AnomalyDetectionEmbeddable< ReactDOM.render( - + }> { expect(Object.keys(createServices[2])).toEqual([ 'anomalyDetectorService', 'anomalyExplorerService', + 'mlFieldFormatService', 'mlResultsService', ]); }); diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_embeddable_factory.ts b/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_embeddable_factory.ts index e502d03bcd964..f91cbb6433aec 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_embeddable_factory.ts +++ b/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_embeddable_factory.ts @@ -55,42 +55,66 @@ export class AnomalyChartsEmbeddableFactory } public async getExplicitInput(): Promise> { - const [coreStart] = await this.getServices(); + const [coreStart, deps] = await this.getServices(); try { const { resolveEmbeddableAnomalyChartsUserInput } = await import( './anomaly_charts_setup_flyout' ); - return await resolveEmbeddableAnomalyChartsUserInput(coreStart); + return await resolveEmbeddableAnomalyChartsUserInput(coreStart, deps.data.dataViews); } catch (e) { return Promise.reject(); } } private async getServices(): Promise { - const [coreStart, pluginsStart] = await this.getStartServices(); - - const { AnomalyDetectorService } = await import( - '../../application/services/anomaly_detector_service' - ); - const { mlApiServicesProvider } = await import('../../application/services/ml_api_service'); - const { mlResultsServiceProvider } = await import('../../application/services/results_service'); + const [ + [coreStart, pluginsStart], + { AnomalyDetectorService }, + { fieldFormatServiceFactory }, + { indexServiceFactory }, + { mlApiServicesProvider }, + { mlResultsServiceProvider }, + ] = await Promise.all([ + await this.getStartServices(), + 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/results_service'), + ]); const httpService = new HttpService(coreStart.http); const anomalyDetectorService = new AnomalyDetectorService(httpService); const mlApiServices = mlApiServicesProvider(httpService); const mlResultsService = mlResultsServiceProvider(mlApiServices); - const anomalyExplorerService = new AnomalyExplorerChartsService( pluginsStart.data.query.timefilter.timefilter, mlApiServices, mlResultsService ); + // 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 + // some stateless utils `useMlIndexUtils()` should be used from within components. + // - `mlFieldFormatService` is a stateful legacy service that relied on "dependency cache", + // so because of its own state it needs to be made available as a global service. + // 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(pluginsStart.data.dataViews); + const mlFieldFormatService = fieldFormatServiceFactory(mlApiServices, mlIndexUtils); + return [ coreStart, pluginsStart as MlDependencies, - { anomalyDetectorService, anomalyExplorerService, mlResultsService }, + { + anomalyDetectorService, + anomalyExplorerService, + mlFieldFormatService, + mlResultsService, + }, ]; } 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 92aea068c5b15..2dd09620dbbd1 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 @@ -8,6 +8,7 @@ import React from 'react'; import type { CoreStart } from '@kbn/core/public'; import { toMountPoint } from '@kbn/react-kibana-mount'; +import type { DataViewsContract } from '@kbn/data-views-plugin/public'; import { extractInfluencers } from '../../../common/util/job_utils'; import { VIEW_BY_JOB_LABEL } from '../../application/explorer/explorer_constants'; import { getDefaultExplorerChartsPanelTitle } from './anomaly_charts_embeddable'; @@ -19,6 +20,7 @@ import { mlApiServicesProvider } from '../../application/services/ml_api_service export async function resolveEmbeddableAnomalyChartsUserInput( coreStart: CoreStart, + dataViews: DataViewsContract, input?: AnomalyChartsEmbeddableInput ): Promise> { const { http, overlays, theme, i18n } = coreStart; @@ -27,7 +29,7 @@ export async function resolveEmbeddableAnomalyChartsUserInput( return new Promise(async (resolve, reject) => { try { - const { jobIds } = await resolveJobSelection(coreStart, input?.jobIds); + const { jobIds } = await resolveJobSelection(coreStart, dataViews, input?.jobIds); const title = input?.title ?? getDefaultExplorerChartsPanelTitle(jobIds); const { jobs } = await getJobs({ jobId: jobIds.join(',') }); const influencers = extractInfluencers(jobs); diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.ts b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.ts index 3e7f958ea778e..45c75a92e2e6c 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.ts +++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.ts @@ -55,11 +55,11 @@ export class AnomalySwimlaneEmbeddableFactory } public async getExplicitInput(): Promise> { - const [coreStart] = await this.getServices(); + const [coreStart, deps] = await this.getServices(); try { const { resolveAnomalySwimlaneUserInput } = await import('./anomaly_swimlane_setup_flyout'); - return await resolveAnomalySwimlaneUserInput(coreStart); + return await resolveAnomalySwimlaneUserInput(coreStart, deps.data.dataViews); } catch (e) { return Promise.reject(); } diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_setup_flyout.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_setup_flyout.tsx index dc2ca931cc805..34ba32cd4a127 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_setup_flyout.tsx +++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_setup_flyout.tsx @@ -8,6 +8,7 @@ import React from 'react'; import type { CoreStart } from '@kbn/core/public'; import { toMountPoint } from '@kbn/react-kibana-mount'; +import type { DataViewsContract } from '@kbn/data-views-plugin/public'; import { extractInfluencers } from '../../../common/util/job_utils'; import { VIEW_BY_JOB_LABEL } from '../../application/explorer/explorer_constants'; import { AnomalySwimlaneInitializer } from './anomaly_swimlane_initializer'; @@ -19,6 +20,7 @@ import { mlApiServicesProvider } from '../../application/services/ml_api_service export async function resolveAnomalySwimlaneUserInput( coreStart: CoreStart, + dataViews: DataViewsContract, input?: AnomalySwimlaneEmbeddableInput ): Promise> { const { http, overlays, theme, i18n } = coreStart; @@ -27,7 +29,7 @@ export async function resolveAnomalySwimlaneUserInput( return new Promise(async (resolve, reject) => { try { - const { jobIds } = await resolveJobSelection(coreStart, input?.jobIds); + const { jobIds } = await resolveJobSelection(coreStart, dataViews, input?.jobIds); const title = input?.title ?? getDefaultSwimlanePanelTitle(jobIds); const { jobs } = await getJobs({ jobId: jobIds.join(',') }); const influencers = extractInfluencers(jobs); diff --git a/x-pack/plugins/ml/public/embeddables/common/resolve_job_selection.tsx b/x-pack/plugins/ml/public/embeddables/common/resolve_job_selection.tsx index 182d070266c9a..73b5fc1305ff4 100644 --- a/x-pack/plugins/ml/public/embeddables/common/resolve_job_selection.tsx +++ b/x-pack/plugins/ml/public/embeddables/common/resolve_job_selection.tsx @@ -11,6 +11,7 @@ import { from } from 'rxjs'; import React from 'react'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { toMountPoint } from '@kbn/react-kibana-mount'; +import type { DataViewsContract } from '@kbn/data-views-plugin/public'; import { getInitialGroupsMap } from '../../application/components/job_selector/job_selector'; import { getMlGlobalServices } from '../../application/app'; import type { JobId } from '../../../common/types/anomaly_detection_jobs'; @@ -26,6 +27,7 @@ import { JobSelectorFlyout } from './components/job_selector_flyout'; */ export async function resolveJobSelection( coreStart: CoreStart, + dataViews: DataViewsContract, selectedJobIds?: JobId[], singleSelection: boolean = false ): Promise<{ jobIds: string[]; groups: Array<{ groupId: string; jobIds: string[] }> }> { @@ -70,7 +72,9 @@ export async function resolveJobSelection( const flyoutSession = coreStart.overlays.openFlyout( toMountPoint( - + = ({ dataView, field, query, timeRange }) => { const quickJobCreator = useMemo( () => new QuickCategorizationJobCreator( + data.dataViews, uiSettings, data.query.timefilter.timefilter, dashboardService, 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 b1bc9ec47ba94..f5ba980a4d54f 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 @@ -54,7 +54,7 @@ export function createFlyout( data, lens, dashboardService, - mlServices: getMlGlobalServices(http), + mlServices: getMlGlobalServices(http, data.dataViews), }} > = ({ layer, layerIndex, embeddable }) => () => new QuickLensJobCreator( lens, + data.dataViews, uiSettings, data.query.timefilter.timefilter, dashboardService, 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 8f368dc0a82c0..52fcdd8aba8f3 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 @@ -56,6 +56,7 @@ export const CompatibleLayer: FC = ({ embeddable, layer, layerIndex }) => const quickJobCreator = useMemo( () => new QuickGeoJobCreator( + data.dataViews, uiSettings, data.query.timefilter.timefilter, dashboardService, diff --git a/x-pack/plugins/ml/public/embeddables/single_metric_viewer/embeddable_single_metric_viewer_container.tsx b/x-pack/plugins/ml/public/embeddables/single_metric_viewer/embeddable_single_metric_viewer_container.tsx index 88c120c9747e1..1ee3021aecc6b 100644 --- a/x-pack/plugins/ml/public/embeddables/single_metric_viewer/embeddable_single_metric_viewer_container.tsx +++ b/x-pack/plugins/ml/public/embeddables/single_metric_viewer/embeddable_single_metric_viewer_container.tsx @@ -6,6 +6,7 @@ */ import React, { FC, useCallback, useEffect, useRef, useState } from 'react'; +import moment from 'moment'; import { EuiResizeObserver } from '@elastic/eui'; import { Observable } from 'rxjs'; import { throttle } from 'lodash'; @@ -48,18 +49,7 @@ export interface EmbeddableSingleMetricViewerContainerProps { export const EmbeddableSingleMetricViewerContainer: FC< EmbeddableSingleMetricViewerContainerProps -> = ({ - id, - embeddableContext, - embeddableInput, - services, - refresh, - onInputChange, - onOutputChange, - onRenderComplete, - onError, - onLoading, -}) => { +> = ({ id, embeddableContext, embeddableInput, services, refresh, onRenderComplete }) => { useEmbeddableExecutionContext( services[0].executionContext, embeddableInput, @@ -72,8 +62,9 @@ export const EmbeddableSingleMetricViewerContainer: FC< const [detectorIndex, setDetectorIndex] = useState(0); const [selectedJob, setSelectedJob] = useState(); const [autoZoomDuration, setAutoZoomDuration] = useState(); + const [jobsLoaded, setJobsLoaded] = useState(false); - const { mlApiServices } = services[2]; + const { mlApiServices, mlJobService } = services[2]; const { data, bounds, lastRefresh } = useSingleMetricViewerInputResolver( embeddableInput, refresh, @@ -81,6 +72,10 @@ export const EmbeddableSingleMetricViewerContainer: FC< onRenderComplete ); const selectedJobId = data?.jobIds[0]; + // Need to make sure we fall back to `undefined` if `functionDescription` is an empty string, + // otherwise anomaly table data will not be loaded. + const functionDescription = + (data?.functionDescription ?? '') === '' ? undefined : data.functionDescription; const previousRefresh = usePrevious(lastRefresh ?? 0); const mlTimeSeriesExplorer = useTimeSeriesExplorerService(); @@ -88,6 +83,15 @@ export const EmbeddableSingleMetricViewerContainer: FC< const containerHeightRef = useRef(); const toastNotificationService = useToastNotificationService(); + useEffect(function setUpJobsLoaded() { + async function loadJobs() { + await mlJobService.loadJobsWrapper(); + setJobsLoaded(true); + } + loadJobs(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + useEffect( function setUpSelectedJob() { async function fetchSelectedJob() { @@ -174,7 +178,7 @@ export const EmbeddableSingleMetricViewerContainer: FC< ref={resizeRef} className="ml-time-series-explorer" > - {data !== undefined && autoZoomDuration !== undefined && ( + {data !== undefined && autoZoomDuration !== undefined && jobsLoaded && ( )} diff --git a/x-pack/plugins/ml/public/embeddables/single_metric_viewer/single_metric_viewer_embeddable.tsx b/x-pack/plugins/ml/public/embeddables/single_metric_viewer/single_metric_viewer_embeddable.tsx index 82a1b5abc8b63..23ac9e7ee2193 100644 --- a/x-pack/plugins/ml/public/embeddables/single_metric_viewer/single_metric_viewer_embeddable.tsx +++ b/x-pack/plugins/ml/public/embeddables/single_metric_viewer/single_metric_viewer_embeddable.tsx @@ -94,6 +94,7 @@ export class SingleMetricViewerEmbeddable extends Embeddable< ...this.services[2], }, ...this.services[0], + ...this.services[1], }} > diff --git a/x-pack/plugins/ml/public/embeddables/single_metric_viewer/single_metric_viewer_embeddable_factory.ts b/x-pack/plugins/ml/public/embeddables/single_metric_viewer/single_metric_viewer_embeddable_factory.ts index 06b2f9b024bfa..c377ce79ddef1 100644 --- a/x-pack/plugins/ml/public/embeddables/single_metric_viewer/single_metric_viewer_embeddable_factory.ts +++ b/x-pack/plugins/ml/public/embeddables/single_metric_viewer/single_metric_viewer_embeddable_factory.ts @@ -78,47 +78,69 @@ export class SingleMetricViewerEmbeddableFactory { fieldFormatServiceFactory }, { indexServiceFactory }, { mlApiServicesProvider }, + { mlJobServiceFactory }, { mlResultsServiceProvider }, + { MlCapabilitiesService }, { timeSeriesSearchServiceFactory }, + { toastNotificationServiceProvider }, ] = await Promise.all([ await this.getStartServices(), 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/capabilities/check_capabilities'), await import( '../../application/timeseriesexplorer/timeseriesexplorer_utils/time_series_search_service' ), + await import('../../application/services/toast_notification_service'), ]); const httpService = new HttpService(coreStart.http); const anomalyDetectorService = new AnomalyDetectorService(httpService); const mlApiServices = mlApiServicesProvider(httpService); + const mlJobService = mlJobServiceFactory( + toastNotificationServiceProvider(coreStart.notifications.toasts), + mlApiServices + ); const mlResultsService = mlResultsServiceProvider(mlApiServices); - const mlIndexUtils = indexServiceFactory(pluginsStart.data.dataViews); const mlTimeSeriesSearchService = timeSeriesSearchServiceFactory( mlResultsService, mlApiServices ); - const mlFieldFormatService = fieldFormatServiceFactory(mlApiServices, mlIndexUtils); - + const mlCapabilities = new MlCapabilitiesService(mlApiServices); const anomalyExplorerService = new AnomalyExplorerChartsService( pluginsStart.data.query.timefilter.timefilter, mlApiServices, mlResultsService ); + // 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 + // some stateless utils `useMlIndexUtils()` should be used from within components. + // - `mlFieldFormatService` is a stateful legacy service that relied on "dependency cache", + // so because of its own state it needs to be made available as a global service. + // 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(pluginsStart.data.dataViews); + const mlFieldFormatService = fieldFormatServiceFactory(mlApiServices, mlIndexUtils); + return [ coreStart, pluginsStart as MlDependencies, { anomalyDetectorService, anomalyExplorerService, - mlResultsService, mlApiServices, - mlTimeSeriesSearchService, + mlCapabilities, mlFieldFormatService, + mlJobService, + mlResultsService, + mlTimeSeriesSearchService, }, ]; } diff --git a/x-pack/plugins/ml/public/embeddables/single_metric_viewer/single_metric_viewer_setup_flyout.tsx b/x-pack/plugins/ml/public/embeddables/single_metric_viewer/single_metric_viewer_setup_flyout.tsx index e9822c01f865a..b20d032f5b907 100644 --- a/x-pack/plugins/ml/public/embeddables/single_metric_viewer/single_metric_viewer_setup_flyout.tsx +++ b/x-pack/plugins/ml/public/embeddables/single_metric_viewer/single_metric_viewer_setup_flyout.tsx @@ -26,7 +26,12 @@ export async function resolveEmbeddableSingleMetricViewerUserInput( return new Promise(async (resolve, reject) => { try { - const { jobIds } = await resolveJobSelection(coreStart, undefined, true); + const { jobIds } = await resolveJobSelection( + coreStart, + pluginStart.data.dataViews, + undefined, + true + ); const title = getDefaultSingleMetricViewerPanelTitle(jobIds); const { jobs } = await mlApiServices.getJobs({ jobId: jobIds.join(',') }); diff --git a/x-pack/plugins/ml/public/embeddables/types.ts b/x-pack/plugins/ml/public/embeddables/types.ts index 56a33f488d534..83c4a00213cad 100644 --- a/x-pack/plugins/ml/public/embeddables/types.ts +++ b/x-pack/plugins/ml/public/embeddables/types.ts @@ -19,6 +19,7 @@ import type { AnomalyTimelineService } from '../application/services/anomaly_tim import type { MlDependencies } from '../application/app'; import type { AppStateSelectedCells } from '../application/explorer/explorer_utils'; import { AnomalyExplorerChartsService } from '../application/services/anomaly_explorer_charts_service'; +import type { MlJobService } from '../application/services/job_service'; import { ANOMALY_EXPLORER_CHARTS_EMBEDDABLE_TYPE, ANOMALY_SWIMLANE_EMBEDDABLE_TYPE, @@ -30,6 +31,7 @@ import { MlResultsService } from '../application/services/results_service'; import type { MlApiServices } from '../application/services/ml_api_service'; import type { MlFieldFormatService } from '../application/services/field_format_service'; import type { MlTimeSeriesSeachService } from '../application/timeseriesexplorer/timeseriesexplorer_utils/time_series_search_service'; +import type { MlCapabilitiesService } from '../application/capabilities/check_capabilities'; export interface AnomalySwimlaneEmbeddableCustomInput { jobIds: JobId[]; @@ -123,6 +125,7 @@ export type SingleMetricViewerEmbeddableInput = EmbeddableInput & export interface AnomalyChartsServices { anomalyDetectorService: AnomalyDetectorService; anomalyExplorerService: AnomalyExplorerChartsService; + mlFieldFormatService: MlFieldFormatService; mlResultsService: MlResultsService; mlApiServices?: MlApiServices; } @@ -131,7 +134,9 @@ export interface SingleMetricViewerServices { anomalyExplorerService: AnomalyExplorerChartsService; anomalyDetectorService: AnomalyDetectorService; mlApiServices: MlApiServices; + mlCapabilities: MlCapabilitiesService; mlFieldFormatService: MlFieldFormatService; + mlJobService: MlJobService; mlResultsService: MlResultsService; mlTimeSeriesSearchService?: MlTimeSeriesSeachService; } diff --git a/x-pack/plugins/ml/public/plugin.ts b/x-pack/plugins/ml/public/plugin.ts index 9bbfe52bbca07..3744ac6aaa615 100644 --- a/x-pack/plugins/ml/public/plugin.ts +++ b/x-pack/plugins/ml/public/plugin.ts @@ -292,10 +292,8 @@ export class MlPlugin implements Plugin { } { setDependencyCache({ docLinks: core.docLinks!, - basePath: core.http.basePath, http: core.http, i18n: core.i18n, - lens: deps.lens, }); return { diff --git a/x-pack/plugins/ml/public/ui_actions/edit_anomaly_charts_panel_action.tsx b/x-pack/plugins/ml/public/ui_actions/edit_anomaly_charts_panel_action.tsx index d79c897958554..6aba5c1d61290 100644 --- a/x-pack/plugins/ml/public/ui_actions/edit_anomaly_charts_panel_action.tsx +++ b/x-pack/plugins/ml/public/ui_actions/edit_anomaly_charts_panel_action.tsx @@ -34,7 +34,7 @@ export function createEditAnomalyChartsPanelAction( throw new Error('Not possible to execute an action without the embeddable context'); } - const [coreStart] = await getStartServices(); + const [coreStart, deps] = await getStartServices(); try { const { resolveEmbeddableAnomalyChartsUserInput } = await import( @@ -43,6 +43,7 @@ export function createEditAnomalyChartsPanelAction( const result = await resolveEmbeddableAnomalyChartsUserInput( coreStart, + deps.data.dataViews, embeddable.getInput() ); embeddable.updateInput(result); diff --git a/x-pack/plugins/ml/public/ui_actions/edit_swimlane_panel_action.tsx b/x-pack/plugins/ml/public/ui_actions/edit_swimlane_panel_action.tsx index 4352dc2df89bf..b81e7b7c988a0 100644 --- a/x-pack/plugins/ml/public/ui_actions/edit_swimlane_panel_action.tsx +++ b/x-pack/plugins/ml/public/ui_actions/edit_swimlane_panel_action.tsx @@ -31,14 +31,18 @@ export function createEditSwimlanePanelAction( throw new Error('Not possible to execute an action without the embeddable context'); } - const [coreStart] = await getStartServices(); + const [coreStart, deps] = await getStartServices(); try { const { resolveAnomalySwimlaneUserInput } = await import( '../embeddables/anomaly_swimlane/anomaly_swimlane_setup_flyout' ); - const result = await resolveAnomalySwimlaneUserInput(coreStart, embeddable.getInput()); + const result = await resolveAnomalySwimlaneUserInput( + coreStart, + deps.data.dataViews, + embeddable.getInput() + ); embeddable.updateInput(result); } catch (e) { return Promise.reject();