{noInfluencersConfigured && (
@@ -462,142 +325,12 @@ export class Explorer extends React.Component {
)}
-
-
-
-
-
-
-
- {showOverallSwimlane && (
-
- {(tooltipService) => (
-
- )}
-
- )}
-
-
- {viewBySwimlaneOptions.length > 0 && (
- <>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {viewByLoadedForTimeFormatted && (
-
- )}
- {viewByLoadedForTimeFormatted === undefined && (
-
- )}
- {filterActive === true && viewBySwimlaneFieldName === VIEW_BY_JOB_LABEL && (
-
- )}
-
-
-
-
-
- {showViewBySwimlane && (
- <>
-
-
-
- {(tooltipService) => (
-
- )}
-
-
- >
- )}
-
- {viewBySwimlaneDataLoading &&
}
-
- {!showViewBySwimlane &&
- !viewBySwimlaneDataLoading &&
- viewBySwimlaneFieldName !== null && (
-
- )}
- >
- )}
+
+
+
{annotationsData.length > 0 && (
<>
diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_constants.ts b/x-pack/plugins/ml/public/application/explorer/explorer_constants.ts
index 1cfd29e2f60d22..d1adf8c7ad744c 100644
--- a/x-pack/plugins/ml/public/application/explorer/explorer_constants.ts
+++ b/x-pack/plugins/ml/public/application/explorer/explorer_constants.ts
@@ -37,10 +37,12 @@ export const FILTER_ACTION = {
REMOVE: '-',
};
-export enum SWIMLANE_TYPE {
- OVERALL = 'overall',
- VIEW_BY = 'viewBy',
-}
+export const SWIMLANE_TYPE = {
+ OVERALL: 'overall',
+ VIEW_BY: 'viewBy',
+} as const;
+
+export type SwimlaneType = typeof SWIMLANE_TYPE[keyof typeof SWIMLANE_TYPE];
export const CHART_TYPE = {
EVENT_DISTRIBUTION: 'event_distribution',
diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_swimlane.tsx b/x-pack/plugins/ml/public/application/explorer/explorer_swimlane.tsx
index 18b5de1d51f9c5..4e6dcdcc5129ca 100644
--- a/x-pack/plugins/ml/public/application/explorer/explorer_swimlane.tsx
+++ b/x-pack/plugins/ml/public/application/explorer/explorer_swimlane.tsx
@@ -22,7 +22,7 @@ import { numTicksForDateFormat } from '../util/chart_utils';
import { getSeverityColor } from '../../../common/util/anomaly_utils';
import { mlEscape } from '../util/string_utils';
import { ALLOW_CELL_RANGE_SELECTION, dragSelect$ } from './explorer_dashboard_service';
-import { DRAG_SELECT_ACTION } from './explorer_constants';
+import { DRAG_SELECT_ACTION, SwimlaneType } from './explorer_constants';
import { EMPTY_FIELD_VALUE_LABEL } from '../timeseriesexplorer/components/entity_control/entity_control';
import { TimeBuckets as TimeBucketsClass } from '../util/time_buckets';
import {
@@ -58,7 +58,7 @@ export interface ExplorerSwimlaneProps {
timeBuckets: InstanceType
;
swimlaneCellClick?: Function;
swimlaneData: OverallSwimlaneData;
- swimlaneType: string;
+ swimlaneType: SwimlaneType;
selection?: {
lanes: any[];
type: string;
diff --git a/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/state.ts b/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/state.ts
index 0a2dbf5bcff35f..4e1a2af9b13a60 100644
--- a/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/state.ts
+++ b/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/state.ts
@@ -16,8 +16,9 @@ import {
AnomaliesTableData,
ExplorerJob,
AppStateSelectedCells,
- SwimlaneData,
TimeRangeBounds,
+ OverallSwimlaneData,
+ SwimlaneData,
} from '../../explorer_utils';
export interface ExplorerState {
@@ -35,7 +36,7 @@ export interface ExplorerState {
loading: boolean;
maskAll: boolean;
noInfluencersConfigured: boolean;
- overallSwimlaneData: SwimlaneData;
+ overallSwimlaneData: SwimlaneData | OverallSwimlaneData;
queryString: string;
selectedCells: AppStateSelectedCells | undefined;
selectedJobs: ExplorerJob[] | null;
@@ -45,7 +46,7 @@ export interface ExplorerState {
tableData: AnomaliesTableData;
tableQueryString: string;
viewByLoadedForTimeFormatted: string | null;
- viewBySwimlaneData: SwimlaneData;
+ viewBySwimlaneData: SwimlaneData | OverallSwimlaneData;
viewBySwimlaneDataLoading: boolean;
viewBySwimlaneFieldName?: string;
viewBySwimlaneOptions: string[];
diff --git a/x-pack/plugins/ml/public/application/explorer/select_limit/select_limit.tsx b/x-pack/plugins/ml/public/application/explorer/select_limit/select_limit.tsx
index 7f7a8fc5a70bd0..7a2df1a0f05350 100644
--- a/x-pack/plugins/ml/public/application/explorer/select_limit/select_limit.tsx
+++ b/x-pack/plugins/ml/public/application/explorer/select_limit/select_limit.tsx
@@ -36,5 +36,5 @@ export const SelectLimit = () => {
setLimit(parseInt(e.target.value, 10));
}
- return ;
+ return ;
};
diff --git a/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx b/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx
new file mode 100644
index 00000000000000..57d1fd81000b7e
--- /dev/null
+++ b/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx
@@ -0,0 +1,61 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { FC, useCallback, useState } from 'react';
+import { EuiResizeObserver, EuiText } from '@elastic/eui';
+
+import { throttle } from 'lodash';
+import {
+ ExplorerSwimlane,
+ ExplorerSwimlaneProps,
+} from '../../application/explorer/explorer_swimlane';
+
+import { MlTooltipComponent } from '../../application/components/chart_tooltip';
+
+const RESIZE_THROTTLE_TIME_MS = 500;
+
+export const SwimlaneContainer: FC<
+ Omit & {
+ onResize: (width: number) => void;
+ }
+> = ({ children, onResize, ...props }) => {
+ const [chartWidth, setChartWidth] = useState(0);
+
+ const resizeHandler = useCallback(
+ throttle((e: { width: number; height: number }) => {
+ const labelWidth = 200;
+ setChartWidth(e.width - labelWidth);
+ onResize(e.width);
+ }, RESIZE_THROTTLE_TIME_MS),
+ []
+ );
+
+ return (
+
+ {(resizeRef) => (
+ {
+ resizeRef(el);
+ }}
+ >
+
+
+
+ {(tooltipService) => (
+
+ )}
+
+
+
+
+ )}
+
+ );
+};
diff --git a/x-pack/plugins/ml/public/application/services/dashboard_service.test.ts b/x-pack/plugins/ml/public/application/services/dashboard_service.test.ts
new file mode 100644
index 00000000000000..6cab23eb187c7c
--- /dev/null
+++ b/x-pack/plugins/ml/public/application/services/dashboard_service.test.ts
@@ -0,0 +1,174 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { dashboardServiceProvider } from './dashboard_service';
+import { savedObjectsServiceMock } from '../../../../../../src/core/public/mocks';
+import { SavedObjectDashboard } from '../../../../../../src/plugins/dashboard/public/saved_dashboards';
+import {
+ DashboardUrlGenerator,
+ SavedDashboardPanel,
+} from '../../../../../../src/plugins/dashboard/public';
+
+jest.mock('@elastic/eui', () => {
+ return {
+ htmlIdGenerator: jest.fn(() => {
+ return jest.fn(() => 'test-panel-id');
+ }),
+ };
+});
+
+describe('DashboardService', () => {
+ const mockSavedObjectClient = savedObjectsServiceMock.createStartContract().client;
+ const dashboardUrlGenerator = ({
+ createUrl: jest.fn(),
+ } as unknown) as DashboardUrlGenerator;
+ const dashboardService = dashboardServiceProvider(
+ mockSavedObjectClient,
+ '8.0.0',
+ dashboardUrlGenerator
+ );
+
+ test('should fetch dashboard', () => {
+ // act
+ dashboardService.fetchDashboards('test');
+ // assert
+ expect(mockSavedObjectClient.find).toHaveBeenCalledWith({
+ type: 'dashboard',
+ perPage: 10,
+ search: `test*`,
+ searchFields: ['title^3', 'description'],
+ });
+ });
+
+ test('should attach panel to the dashboard', () => {
+ // act
+ dashboardService.attachPanels(
+ 'test-dashboard',
+ ({
+ title: 'ML Test',
+ hits: 0,
+ description: '',
+ panelsJSON: JSON.stringify([
+ {
+ version: '8.0.0',
+ type: 'ml_anomaly_swimlane',
+ gridData: { x: 0, y: 0, w: 24, h: 15, i: 'i63c960b1-ab1b-11ea-809d-f5c60c43347f' },
+ panelIndex: 'i63c960b1-ab1b-11ea-809d-f5c60c43347f',
+ embeddableConfig: {
+ title: 'Panel test!',
+ jobIds: ['cw_multi_1'],
+ swimlaneType: 'overall',
+ },
+ title: 'Panel test!',
+ },
+ {
+ version: '8.0.0',
+ type: 'ml_anomaly_swimlane',
+ gridData: { x: 24, y: 0, w: 24, h: 15, i: '0aa334bd-8308-4ded-9462-80dbd37680ee' },
+ panelIndex: '0aa334bd-8308-4ded-9462-80dbd37680ee',
+ embeddableConfig: {
+ title: 'ML anomaly swimlane for fb_population_1',
+ jobIds: ['fb_population_1'],
+ limit: 5,
+ swimlaneType: 'overall',
+ },
+ title: 'ML anomaly swimlane for fb_population_1',
+ },
+ {
+ version: '8.0.0',
+ gridData: { x: 0, y: 15, w: 24, h: 15, i: 'abd36eb7-4774-4216-891e-12100752b46d' },
+ panelIndex: 'abd36eb7-4774-4216-891e-12100752b46d',
+ embeddableConfig: {},
+ panelRefName: 'panel_2',
+ },
+ ]),
+ optionsJSON: '{"hidePanelTitles":false,"useMargins":true}',
+ version: 1,
+ timeRestore: false,
+ kibanaSavedObjectMeta: {
+ searchSourceJSON: '{"query":{"language":"kuery","query":""},"filter":[]}',
+ },
+ } as unknown) as SavedObjectDashboard,
+ [{ title: 'Test title', type: 'test-panel', embeddableConfig: { testConfig: '' } }]
+ );
+ // assert
+ expect(mockSavedObjectClient.update).toHaveBeenCalledWith('dashboard', 'test-dashboard', {
+ title: 'ML Test',
+ hits: 0,
+ description: '',
+ panelsJSON: JSON.stringify([
+ {
+ version: '8.0.0',
+ type: 'ml_anomaly_swimlane',
+ gridData: { x: 0, y: 0, w: 24, h: 15, i: 'i63c960b1-ab1b-11ea-809d-f5c60c43347f' },
+ panelIndex: 'i63c960b1-ab1b-11ea-809d-f5c60c43347f',
+ embeddableConfig: {
+ title: 'Panel test!',
+ jobIds: ['cw_multi_1'],
+ swimlaneType: 'overall',
+ },
+ title: 'Panel test!',
+ },
+ {
+ version: '8.0.0',
+ type: 'ml_anomaly_swimlane',
+ gridData: { x: 24, y: 0, w: 24, h: 15, i: '0aa334bd-8308-4ded-9462-80dbd37680ee' },
+ panelIndex: '0aa334bd-8308-4ded-9462-80dbd37680ee',
+ embeddableConfig: {
+ title: 'ML anomaly swimlane for fb_population_1',
+ jobIds: ['fb_population_1'],
+ limit: 5,
+ swimlaneType: 'overall',
+ },
+ title: 'ML anomaly swimlane for fb_population_1',
+ },
+ {
+ version: '8.0.0',
+ gridData: { x: 0, y: 15, w: 24, h: 15, i: 'abd36eb7-4774-4216-891e-12100752b46d' },
+ panelIndex: 'abd36eb7-4774-4216-891e-12100752b46d',
+ embeddableConfig: {},
+ panelRefName: 'panel_2',
+ },
+ {
+ panelIndex: 'test-panel-id',
+ embeddableConfig: { testConfig: '' },
+ title: 'Test title',
+ type: 'test-panel',
+ version: '8.0.0',
+ gridData: { h: 15, i: 'test-panel-id', w: 24, x: 24, y: 15 },
+ },
+ ]),
+ optionsJSON: '{"hidePanelTitles":false,"useMargins":true}',
+ version: 1,
+ timeRestore: false,
+ kibanaSavedObjectMeta: {
+ searchSourceJSON: '{"query":{"language":"kuery","query":""},"filter":[]}',
+ },
+ });
+ });
+
+ test('should generate edit url to the dashboard', () => {
+ dashboardService.getDashboardEditUrl('test-id');
+ expect(dashboardUrlGenerator.createUrl).toHaveBeenCalledWith({
+ dashboardId: 'test-id',
+ useHash: false,
+ viewMode: 'edit',
+ });
+ });
+
+ test('should find the panel positioned at the end', () => {
+ expect(
+ dashboardService.getLastPanel([
+ { gridData: { y: 15, x: 7 } },
+ { gridData: { y: 17, x: 9 } },
+ { gridData: { y: 15, x: 1 } },
+ { gridData: { y: 17, x: 10 } },
+ { gridData: { y: 15, x: 22 } },
+ { gridData: { y: 17, x: 9 } },
+ ] as SavedDashboardPanel[])
+ ).toEqual({ gridData: { y: 17, x: 10 } });
+ });
+});
diff --git a/x-pack/plugins/ml/public/application/services/dashboard_service.ts b/x-pack/plugins/ml/public/application/services/dashboard_service.ts
new file mode 100644
index 00000000000000..7f2bb71d18eb98
--- /dev/null
+++ b/x-pack/plugins/ml/public/application/services/dashboard_service.ts
@@ -0,0 +1,136 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { SavedObjectsClientContract } from 'kibana/public';
+import { htmlIdGenerator } from '@elastic/eui';
+import { useMemo } from 'react';
+import {
+ DASHBOARD_APP_URL_GENERATOR,
+ DashboardUrlGenerator,
+ SavedDashboardPanel,
+ SavedObjectDashboard,
+} from '../../../../../../src/plugins/dashboard/public';
+import { useMlKibana } from '../contexts/kibana';
+import { ViewMode } from '../../../../../../src/plugins/embeddable/public';
+
+export type DashboardService = ReturnType;
+
+export function dashboardServiceProvider(
+ savedObjectClient: SavedObjectsClientContract,
+ kibanaVersion: string,
+ dashboardUrlGenerator: DashboardUrlGenerator
+) {
+ const generateId = htmlIdGenerator();
+ const DEFAULT_PANEL_WIDTH = 24;
+ const DEFAULT_PANEL_HEIGHT = 15;
+
+ return {
+ /**
+ * Fetches dashboards
+ */
+ async fetchDashboards(query?: string) {
+ return await savedObjectClient.find({
+ type: 'dashboard',
+ perPage: 10,
+ search: query ? `${query}*` : '',
+ searchFields: ['title^3', 'description'],
+ });
+ },
+ /**
+ * Resolves the last positioned panel from the collection.
+ */
+ getLastPanel(panels: SavedDashboardPanel[]): SavedDashboardPanel | null {
+ return panels.length > 0
+ ? panels.reduce((prev, current) =>
+ prev.gridData.y >= current.gridData.y
+ ? prev.gridData.y === current.gridData.y
+ ? prev.gridData.x > current.gridData.x
+ ? prev
+ : current
+ : prev
+ : current
+ )
+ : null;
+ },
+ /**
+ * Attaches embeddable panels to the dashboard
+ */
+ async attachPanels(
+ dashboardId: string,
+ dashboardAttributes: SavedObjectDashboard,
+ panelsData: Array>
+ ) {
+ const panels = JSON.parse(dashboardAttributes.panelsJSON) as SavedDashboardPanel[];
+ const version = kibanaVersion;
+ const rowWidth = DEFAULT_PANEL_WIDTH * 2;
+
+ for (const panelData of panelsData) {
+ const panelIndex = generateId();
+ const lastPanel = this.getLastPanel(panels);
+
+ const xOffset = lastPanel ? lastPanel.gridData.w + lastPanel.gridData.x : 0;
+ const availableRowSpace = rowWidth - xOffset;
+ const xPosition = availableRowSpace - DEFAULT_PANEL_WIDTH >= 0 ? xOffset : 0;
+
+ panels.push({
+ panelIndex,
+ embeddableConfig: panelData.embeddableConfig as { [key: string]: any },
+ title: panelData.title,
+ type: panelData.type,
+ version,
+ gridData: {
+ h: DEFAULT_PANEL_HEIGHT,
+ i: panelIndex,
+ w: DEFAULT_PANEL_WIDTH,
+ x: xPosition,
+ y: lastPanel
+ ? xPosition > 0
+ ? lastPanel.gridData.y
+ : lastPanel.gridData.y + lastPanel.gridData.h
+ : 0,
+ },
+ });
+ }
+
+ await savedObjectClient.update('dashboard', dashboardId, {
+ ...dashboardAttributes,
+ panelsJSON: JSON.stringify(panels),
+ });
+ },
+ /**
+ * Generates dashboard url with edit mode
+ */
+ async getDashboardEditUrl(dashboardId: string) {
+ return await dashboardUrlGenerator.createUrl({
+ dashboardId,
+ useHash: false,
+ viewMode: ViewMode.EDIT,
+ });
+ },
+ };
+}
+
+/**
+ * Hook to use {@link DashboardService} in react components
+ */
+export function useDashboardService(): DashboardService {
+ const {
+ services: {
+ savedObjects: { client: savedObjectClient },
+ kibanaVersion,
+ share: { urlGenerators },
+ },
+ } = useMlKibana();
+ return useMemo(
+ () =>
+ dashboardServiceProvider(
+ savedObjectClient,
+ kibanaVersion,
+ urlGenerators.getUrlGenerator(DASHBOARD_APP_URL_GENERATOR)
+ ),
+ [savedObjectClient, kibanaVersion]
+ );
+}
diff --git a/x-pack/plugins/ml/public/application/services/http_service.ts b/x-pack/plugins/ml/public/application/services/http_service.ts
index 7144411c2885d2..bd927dc0e30111 100644
--- a/x-pack/plugins/ml/public/application/services/http_service.ts
+++ b/x-pack/plugins/ml/public/application/services/http_service.ts
@@ -37,6 +37,8 @@ function getFetchOptions(
/**
* Function for making HTTP requests to Kibana's backend.
* Wrapper for Kibana's HttpHandler.
+ *
+ * @deprecated use {@link HttpService} instead
*/
export async function http(options: HttpFetchOptionsWithPath): Promise {
const { path, fetchOptions } = getFetchOptions(options);
@@ -46,6 +48,8 @@ export async function http(options: HttpFetchOptionsWithPath): Promise {
/**
* Function for making HTTP requests to Kibana's backend which returns an Observable
* with request cancellation support.
+ *
+ * @deprecated use {@link HttpService} instead
*/
export function http$(options: HttpFetchOptionsWithPath): Observable {
const { path, fetchOptions } = getFetchOptions(options);
@@ -55,7 +59,7 @@ export function http$(options: HttpFetchOptionsWithPath): Observable {
/**
* Creates an Observable from Kibana's HttpHandler.
*/
-export function fromHttpHandler(input: string, init?: RequestInit): Observable {
+function fromHttpHandler(input: string, init?: RequestInit): Observable {
return new Observable((subscriber) => {
const controller = new AbortController();
const signal = controller.signal;
diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts
index fdaa3c2ffe79ec..6d32fca6a645c1 100644
--- a/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts
+++ b/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts
@@ -5,12 +5,13 @@
*/
import { Observable } from 'rxjs';
-import { http, http$ } from '../http_service';
+import { HttpStart } from 'kibana/public';
+import { HttpService } from '../http_service';
import { annotations } from './annotations';
import { dataFrameAnalytics } from './data_frame_analytics';
import { filters } from './filters';
-import { results } from './results';
+import { resultsApiProvider } from './results';
import { jobs } from './jobs';
import { fileDatavisualizer } from './datavisualizer';
import { MlServerDefaults, MlServerLimits } from '../../../../common/types/ml_server_info';
@@ -28,6 +29,7 @@ import {
import { ES_AGGREGATION } from '../../../../common/constants/aggregation_types';
import { FieldRequestConfig } from '../../datavisualizer/index_based/common';
import { DataRecognizerConfigResponse, Module } from '../../../../common/types/modules';
+import { getHttp } from '../../util/dependency_cache';
export interface MlInfoResponse {
defaults: MlServerDefaults;
@@ -87,327 +89,330 @@ export function basePath() {
return '/api/ml';
}
-export const ml = {
- getJobs(obj?: { jobId?: string }) {
- const jobId = obj && obj.jobId ? `/${obj.jobId}` : '';
- return http({
- path: `${basePath()}/anomaly_detectors${jobId}`,
- });
- },
-
- getJobStats(obj: { jobId?: string }) {
- const jobId = obj && obj.jobId ? `/${obj.jobId}` : '';
- return http({
- path: `${basePath()}/anomaly_detectors${jobId}/_stats`,
- });
- },
-
- addJob({ jobId, job }: { jobId: string; job: Job }) {
- const body = JSON.stringify(job);
- return http({
- path: `${basePath()}/anomaly_detectors/${jobId}`,
- method: 'PUT',
- body,
- });
- },
-
- openJob({ jobId }: { jobId: string }) {
- return http({
- path: `${basePath()}/anomaly_detectors/${jobId}/_open`,
- method: 'POST',
- });
- },
-
- closeJob({ jobId }: { jobId: string }) {
- return http({
- path: `${basePath()}/anomaly_detectors/${jobId}/_close`,
- method: 'POST',
- });
- },
-
- forceCloseJob({ jobId }: { jobId: string }) {
- return http({
- path: `${basePath()}/anomaly_detectors/${jobId}/_close?force=true`,
- method: 'POST',
- });
- },
-
- deleteJob({ jobId }: { jobId: string }) {
- return http({
- path: `${basePath()}/anomaly_detectors/${jobId}`,
- method: 'DELETE',
- });
- },
-
- forceDeleteJob({ jobId }: { jobId: string }) {
- return http({
- path: `${basePath()}/anomaly_detectors/${jobId}?force=true`,
- method: 'DELETE',
- });
- },
-
- updateJob({ jobId, job }: { jobId: string; job: Job }) {
- const body = JSON.stringify(job);
- return http({
- path: `${basePath()}/anomaly_detectors/${jobId}/_update`,
- method: 'POST',
- body,
- });
- },
-
- estimateBucketSpan(obj: BucketSpanEstimatorData) {
- const body = JSON.stringify(obj);
- return http({
- path: `${basePath()}/validate/estimate_bucket_span`,
- method: 'POST',
- body,
- });
- },
-
- validateJob(payload: {
- job: Job;
- duration: {
- start?: number;
- end?: number;
- };
- fields?: any[];
- }) {
- const body = JSON.stringify(payload);
- return http({
- path: `${basePath()}/validate/job`,
- method: 'POST',
- body,
- });
- },
-
- validateCardinality$(job: CombinedJob): Observable {
- const body = JSON.stringify(job);
- return http$({
- path: `${basePath()}/validate/cardinality`,
- method: 'POST',
- body,
- });
- },
-
- getDatafeeds(obj: { datafeedId: string }) {
- const datafeedId = obj && obj.datafeedId ? `/${obj.datafeedId}` : '';
- return http({
- path: `${basePath()}/datafeeds${datafeedId}`,
- });
- },
-
- getDatafeedStats(obj: { datafeedId: string }) {
- const datafeedId = obj && obj.datafeedId ? `/${obj.datafeedId}` : '';
- return http({
- path: `${basePath()}/datafeeds${datafeedId}/_stats`,
- });
- },
-
- addDatafeed({ datafeedId, datafeedConfig }: { datafeedId: string; datafeedConfig: Datafeed }) {
- const body = JSON.stringify(datafeedConfig);
- return http({
- path: `${basePath()}/datafeeds/${datafeedId}`,
- method: 'PUT',
- body,
- });
- },
-
- updateDatafeed({ datafeedId, datafeedConfig }: { datafeedId: string; datafeedConfig: Datafeed }) {
- const body = JSON.stringify(datafeedConfig);
- return http({
- path: `${basePath()}/datafeeds/${datafeedId}/_update`,
- method: 'POST',
- body,
- });
- },
-
- deleteDatafeed({ datafeedId }: { datafeedId: string }) {
- return http({
- path: `${basePath()}/datafeeds/${datafeedId}`,
- method: 'DELETE',
- });
- },
-
- forceDeleteDatafeed({ datafeedId }: { datafeedId: string }) {
- return http({
- path: `${basePath()}/datafeeds/${datafeedId}?force=true`,
- method: 'DELETE',
- });
- },
-
- startDatafeed({ datafeedId, start, end }: { datafeedId: string; start: number; end: number }) {
- const body = JSON.stringify({
- ...(start !== undefined ? { start } : {}),
- ...(end !== undefined ? { end } : {}),
- });
-
- return http({
- path: `${basePath()}/datafeeds/${datafeedId}/_start`,
- method: 'POST',
- body,
- });
- },
-
- stopDatafeed({ datafeedId }: { datafeedId: string }) {
- return http({
- path: `${basePath()}/datafeeds/${datafeedId}/_stop`,
- method: 'POST',
- });
- },
-
- forceStopDatafeed({ datafeedId }: { datafeedId: string }) {
- return http({
- path: `${basePath()}/datafeeds/${datafeedId}/_stop?force=true`,
- method: 'POST',
- });
- },
-
- datafeedPreview({ datafeedId }: { datafeedId: string }) {
- return http({
- path: `${basePath()}/datafeeds/${datafeedId}/_preview`,
- method: 'GET',
- });
- },
-
- validateDetector({ detector }: { detector: Detector }) {
- const body = JSON.stringify(detector);
- return http({
- path: `${basePath()}/anomaly_detectors/_validate/detector`,
- method: 'POST',
- body,
- });
- },
-
- forecast({ jobId, duration }: { jobId: string; duration?: string }) {
- const body = JSON.stringify({
- ...(duration !== undefined ? { duration } : {}),
- });
-
- return http({
- path: `${basePath()}/anomaly_detectors/${jobId}/_forecast`,
- method: 'POST',
- body,
- });
- },
-
- overallBuckets({
- jobId,
- topN,
- bucketSpan,
- start,
- end,
- }: {
- jobId: string;
- topN: string;
- bucketSpan: string;
- start: number;
- end: number;
- }) {
- const body = JSON.stringify({ topN, bucketSpan, start, end });
- return http({
- path: `${basePath()}/anomaly_detectors/${jobId}/results/overall_buckets`,
- method: 'POST',
- body,
- });
- },
-
- hasPrivileges(obj: any) {
- const body = JSON.stringify(obj);
- return http({
- path: `${basePath()}/_has_privileges`,
- method: 'POST',
- body,
- });
- },
-
- checkMlCapabilities() {
- return http({
- path: `${basePath()}/ml_capabilities`,
- method: 'GET',
- });
- },
-
- checkManageMLCapabilities() {
- return http({
- path: `${basePath()}/ml_capabilities`,
- method: 'GET',
- });
- },
-
- getNotificationSettings() {
- return http({
- path: `${basePath()}/notification_settings`,
- method: 'GET',
- });
- },
-
- getFieldCaps({ index, fields }: { index: string; fields: string[] }) {
- const body = JSON.stringify({
- ...(index !== undefined ? { index } : {}),
- ...(fields !== undefined ? { fields } : {}),
- });
-
- return http({
- path: `${basePath()}/indices/field_caps`,
- method: 'POST',
- body,
- });
- },
-
- recognizeIndex({ indexPatternTitle }: { indexPatternTitle: string }) {
- return http({
- path: `${basePath()}/modules/recognize/${indexPatternTitle}`,
- method: 'GET',
- });
- },
-
- listDataRecognizerModules() {
- return http({
- path: `${basePath()}/modules/get_module`,
- method: 'GET',
- });
- },
-
- getDataRecognizerModule({ moduleId }: { moduleId: string }) {
- return http({
- path: `${basePath()}/modules/get_module/${moduleId}`,
- method: 'GET',
- });
- },
-
- dataRecognizerModuleJobsExist({ moduleId }: { moduleId: string }) {
- return http({
- path: `${basePath()}/modules/jobs_exist/${moduleId}`,
- method: 'GET',
- });
+/**
+ * Temp solution to allow {@link ml} service to use http from
+ * the dependency_cache.
+ */
+const proxyHttpStart = new Proxy(({} as unknown) as HttpStart, {
+ get(obj, prop: keyof HttpStart) {
+ try {
+ return getHttp()[prop];
+ } catch (e) {
+ // eslint-disable-next-line no-console
+ console.error(e);
+ }
},
-
- setupDataRecognizerConfig({
- moduleId,
- prefix,
- groups,
- indexPatternName,
- query,
- useDedicatedIndex,
- startDatafeed,
- start,
- end,
- jobOverrides,
- estimateModelMemory,
- }: {
- moduleId: string;
- prefix?: string;
- groups?: string[];
- indexPatternName?: string;
- query?: any;
- useDedicatedIndex?: boolean;
- startDatafeed?: boolean;
- start?: number;
- end?: number;
- jobOverrides?: Array>;
- estimateModelMemory?: boolean;
- }) {
- const body = JSON.stringify({
+});
+
+export type MlApiServices = ReturnType;
+
+export const ml = mlApiServicesProvider(new HttpService(proxyHttpStart));
+
+export function mlApiServicesProvider(httpService: HttpService) {
+ const { http } = httpService;
+ return {
+ getJobs(obj?: { jobId?: string }) {
+ const jobId = obj && obj.jobId ? `/${obj.jobId}` : '';
+ return httpService.http({
+ path: `${basePath()}/anomaly_detectors${jobId}`,
+ });
+ },
+
+ getJobStats(obj: { jobId?: string }) {
+ const jobId = obj && obj.jobId ? `/${obj.jobId}` : '';
+ return httpService.http({
+ path: `${basePath()}/anomaly_detectors${jobId}/_stats`,
+ });
+ },
+
+ addJob({ jobId, job }: { jobId: string; job: Job }) {
+ const body = JSON.stringify(job);
+ return httpService.http({
+ path: `${basePath()}/anomaly_detectors/${jobId}`,
+ method: 'PUT',
+ body,
+ });
+ },
+
+ openJob({ jobId }: { jobId: string }) {
+ return httpService.http({
+ path: `${basePath()}/anomaly_detectors/${jobId}/_open`,
+ method: 'POST',
+ });
+ },
+
+ closeJob({ jobId }: { jobId: string }) {
+ return http({
+ path: `${basePath()}/anomaly_detectors/${jobId}/_close`,
+ method: 'POST',
+ });
+ },
+
+ forceCloseJob({ jobId }: { jobId: string }) {
+ return http({
+ path: `${basePath()}/anomaly_detectors/${jobId}/_close?force=true`,
+ method: 'POST',
+ });
+ },
+
+ deleteJob({ jobId }: { jobId: string }) {
+ return httpService.http({
+ path: `${basePath()}/anomaly_detectors/${jobId}`,
+ method: 'DELETE',
+ });
+ },
+
+ forceDeleteJob({ jobId }: { jobId: string }) {
+ return httpService.http({
+ path: `${basePath()}/anomaly_detectors/${jobId}?force=true`,
+ method: 'DELETE',
+ });
+ },
+
+ updateJob({ jobId, job }: { jobId: string; job: Job }) {
+ const body = JSON.stringify(job);
+ return httpService.http({
+ path: `${basePath()}/anomaly_detectors/${jobId}/_update`,
+ method: 'POST',
+ body,
+ });
+ },
+
+ estimateBucketSpan(obj: BucketSpanEstimatorData) {
+ const body = JSON.stringify(obj);
+ return httpService.http({
+ path: `${basePath()}/validate/estimate_bucket_span`,
+ method: 'POST',
+ body,
+ });
+ },
+
+ validateJob(payload: {
+ job: Job;
+ duration: {
+ start?: number;
+ end?: number;
+ };
+ fields?: any[];
+ }) {
+ const body = JSON.stringify(payload);
+ return httpService.http({
+ path: `${basePath()}/validate/job`,
+ method: 'POST',
+ body,
+ });
+ },
+
+ validateCardinality$(job: CombinedJob): Observable {
+ const body = JSON.stringify(job);
+ return httpService.http$({
+ path: `${basePath()}/validate/cardinality`,
+ method: 'POST',
+ body,
+ });
+ },
+
+ getDatafeeds(obj: { datafeedId: string }) {
+ const datafeedId = obj && obj.datafeedId ? `/${obj.datafeedId}` : '';
+ return httpService.http({
+ path: `${basePath()}/datafeeds${datafeedId}`,
+ });
+ },
+
+ getDatafeedStats(obj: { datafeedId: string }) {
+ const datafeedId = obj && obj.datafeedId ? `/${obj.datafeedId}` : '';
+ return httpService.http({
+ path: `${basePath()}/datafeeds${datafeedId}/_stats`,
+ });
+ },
+
+ addDatafeed({ datafeedId, datafeedConfig }: { datafeedId: string; datafeedConfig: Datafeed }) {
+ const body = JSON.stringify(datafeedConfig);
+ return httpService.http({
+ path: `${basePath()}/datafeeds/${datafeedId}`,
+ method: 'PUT',
+ body,
+ });
+ },
+
+ updateDatafeed({
+ datafeedId,
+ datafeedConfig,
+ }: {
+ datafeedId: string;
+ datafeedConfig: Datafeed;
+ }) {
+ const body = JSON.stringify(datafeedConfig);
+ return httpService.http({
+ path: `${basePath()}/datafeeds/${datafeedId}/_update`,
+ method: 'POST',
+ body,
+ });
+ },
+
+ deleteDatafeed({ datafeedId }: { datafeedId: string }) {
+ return httpService.http({
+ path: `${basePath()}/datafeeds/${datafeedId}`,
+ method: 'DELETE',
+ });
+ },
+
+ forceDeleteDatafeed({ datafeedId }: { datafeedId: string }) {
+ return httpService.http({
+ path: `${basePath()}/datafeeds/${datafeedId}?force=true`,
+ method: 'DELETE',
+ });
+ },
+
+ startDatafeed({ datafeedId, start, end }: { datafeedId: string; start: number; end: number }) {
+ const body = JSON.stringify({
+ ...(start !== undefined ? { start } : {}),
+ ...(end !== undefined ? { end } : {}),
+ });
+
+ return httpService.http({
+ path: `${basePath()}/datafeeds/${datafeedId}/_start`,
+ method: 'POST',
+ body,
+ });
+ },
+
+ stopDatafeed({ datafeedId }: { datafeedId: string }) {
+ return http({
+ path: `${basePath()}/datafeeds/${datafeedId}/_stop`,
+ method: 'POST',
+ });
+ },
+
+ forceStopDatafeed({ datafeedId }: { datafeedId: string }) {
+ return http({
+ path: `${basePath()}/datafeeds/${datafeedId}/_stop?force=true`,
+ method: 'POST',
+ });
+ },
+
+ datafeedPreview({ datafeedId }: { datafeedId: string }) {
+ return httpService.http({
+ path: `${basePath()}/datafeeds/${datafeedId}/_preview`,
+ method: 'GET',
+ });
+ },
+
+ validateDetector({ detector }: { detector: Detector }) {
+ const body = JSON.stringify(detector);
+ return httpService.http({
+ path: `${basePath()}/anomaly_detectors/_validate/detector`,
+ method: 'POST',
+ body,
+ });
+ },
+
+ forecast({ jobId, duration }: { jobId: string; duration?: string }) {
+ const body = JSON.stringify({
+ ...(duration !== undefined ? { duration } : {}),
+ });
+
+ return httpService.http({
+ path: `${basePath()}/anomaly_detectors/${jobId}/_forecast`,
+ method: 'POST',
+ body,
+ });
+ },
+
+ overallBuckets({
+ jobId,
+ topN,
+ bucketSpan,
+ start,
+ end,
+ }: {
+ jobId: string;
+ topN: string;
+ bucketSpan: string;
+ start: number;
+ end: number;
+ }) {
+ const body = JSON.stringify({ topN, bucketSpan, start, end });
+ return httpService.http({
+ path: `${basePath()}/anomaly_detectors/${jobId}/results/overall_buckets`,
+ method: 'POST',
+ body,
+ });
+ },
+
+ hasPrivileges(obj: any) {
+ const body = JSON.stringify(obj);
+ return httpService.http({
+ path: `${basePath()}/_has_privileges`,
+ method: 'POST',
+ body,
+ });
+ },
+
+ checkMlCapabilities() {
+ return httpService.http({
+ path: `${basePath()}/ml_capabilities`,
+ method: 'GET',
+ });
+ },
+
+ checkManageMLCapabilities() {
+ return httpService.http({
+ path: `${basePath()}/ml_capabilities`,
+ method: 'GET',
+ });
+ },
+
+ getNotificationSettings() {
+ return httpService.http({
+ path: `${basePath()}/notification_settings`,
+ method: 'GET',
+ });
+ },
+
+ getFieldCaps({ index, fields }: { index: string; fields: string[] }) {
+ const body = JSON.stringify({
+ ...(index !== undefined ? { index } : {}),
+ ...(fields !== undefined ? { fields } : {}),
+ });
+
+ return httpService.http({
+ path: `${basePath()}/indices/field_caps`,
+ method: 'POST',
+ body,
+ });
+ },
+
+ recognizeIndex({ indexPatternTitle }: { indexPatternTitle: string }) {
+ return httpService.http({
+ path: `${basePath()}/modules/recognize/${indexPatternTitle}`,
+ method: 'GET',
+ });
+ },
+
+ listDataRecognizerModules() {
+ return httpService.http({
+ path: `${basePath()}/modules/get_module`,
+ method: 'GET',
+ });
+ },
+
+ getDataRecognizerModule({ moduleId }: { moduleId: string }) {
+ return httpService.http({
+ path: `${basePath()}/modules/get_module/${moduleId}`,
+ method: 'GET',
+ });
+ },
+
+ dataRecognizerModuleJobsExist({ moduleId }: { moduleId: string }) {
+ return httpService.http({
+ path: `${basePath()}/modules/jobs_exist/${moduleId}`,
+ method: 'GET',
+ });
+ },
+
+ setupDataRecognizerConfig({
+ moduleId,
prefix,
groups,
indexPatternName,
@@ -418,37 +423,41 @@ export const ml = {
end,
jobOverrides,
estimateModelMemory,
- });
-
- return http({
- path: `${basePath()}/modules/setup/${moduleId}`,
- method: 'POST',
- body,
- });
- },
-
- getVisualizerFieldStats({
- indexPatternTitle,
- query,
- timeFieldName,
- earliest,
- latest,
- samplerShardSize,
- interval,
- fields,
- maxExamples,
- }: {
- indexPatternTitle: string;
- query: any;
- timeFieldName?: string;
- earliest?: number;
- latest?: number;
- samplerShardSize?: number;
- interval?: string;
- fields?: FieldRequestConfig[];
- maxExamples?: number;
- }) {
- const body = JSON.stringify({
+ }: {
+ moduleId: string;
+ prefix?: string;
+ groups?: string[];
+ indexPatternName?: string;
+ query?: any;
+ useDedicatedIndex?: boolean;
+ startDatafeed?: boolean;
+ start?: number;
+ end?: number;
+ jobOverrides?: Array>;
+ estimateModelMemory?: boolean;
+ }) {
+ const body = JSON.stringify({
+ prefix,
+ groups,
+ indexPatternName,
+ query,
+ useDedicatedIndex,
+ startDatafeed,
+ start,
+ end,
+ jobOverrides,
+ estimateModelMemory,
+ });
+
+ return httpService.http({
+ path: `${basePath()}/modules/setup/${moduleId}`,
+ method: 'POST',
+ body,
+ });
+ },
+
+ getVisualizerFieldStats({
+ indexPatternTitle,
query,
timeFieldName,
earliest,
@@ -457,35 +466,37 @@ export const ml = {
interval,
fields,
maxExamples,
- });
-
- return http({
- path: `${basePath()}/data_visualizer/get_field_stats/${indexPatternTitle}`,
- method: 'POST',
- body,
- });
- },
-
- getVisualizerOverallStats({
- indexPatternTitle,
- query,
- timeFieldName,
- earliest,
- latest,
- samplerShardSize,
- aggregatableFields,
- nonAggregatableFields,
- }: {
- indexPatternTitle: string;
- query: any;
- timeFieldName?: string;
- earliest?: number;
- latest?: number;
- samplerShardSize?: number;
- aggregatableFields: string[];
- nonAggregatableFields: string[];
- }) {
- const body = JSON.stringify({
+ }: {
+ indexPatternTitle: string;
+ query: any;
+ timeFieldName?: string;
+ earliest?: number;
+ latest?: number;
+ samplerShardSize?: number;
+ interval?: string;
+ fields?: FieldRequestConfig[];
+ maxExamples?: number;
+ }) {
+ const body = JSON.stringify({
+ query,
+ timeFieldName,
+ earliest,
+ latest,
+ samplerShardSize,
+ interval,
+ fields,
+ maxExamples,
+ });
+
+ return httpService.http({
+ path: `${basePath()}/data_visualizer/get_field_stats/${indexPatternTitle}`,
+ method: 'POST',
+ body,
+ });
+ },
+
+ getVisualizerOverallStats({
+ indexPatternTitle,
query,
timeFieldName,
earliest,
@@ -493,204 +504,230 @@ export const ml = {
samplerShardSize,
aggregatableFields,
nonAggregatableFields,
- });
-
- return http({
- path: `${basePath()}/data_visualizer/get_overall_stats/${indexPatternTitle}`,
- method: 'POST',
- body,
- });
- },
-
- /**
- * Gets a list of calendars
- * @param obj
- * @returns {Promise}
- */
- calendars(obj?: { calendarId?: CalendarId; calendarIds?: CalendarId[] }) {
- const { calendarId, calendarIds } = obj || {};
- let calendarIdsPathComponent = '';
- if (calendarId) {
- calendarIdsPathComponent = `/${calendarId}`;
- } else if (calendarIds) {
- calendarIdsPathComponent = `/${calendarIds.join(',')}`;
- }
- return http({
- path: `${basePath()}/calendars${calendarIdsPathComponent}`,
- method: 'GET',
- });
- },
-
- addCalendar(obj: Calendar) {
- const body = JSON.stringify(obj);
- return http({
- path: `${basePath()}/calendars`,
- method: 'PUT',
- body,
- });
- },
-
- updateCalendar(obj: UpdateCalendar) {
- const calendarId = obj && obj.calendarId ? `/${obj.calendarId}` : '';
- const body = JSON.stringify(obj);
- return http({
- path: `${basePath()}/calendars${calendarId}`,
- method: 'PUT',
- body,
- });
- },
-
- deleteCalendar({ calendarId }: { calendarId?: string }) {
- return http({
- path: `${basePath()}/calendars/${calendarId}`,
- method: 'DELETE',
- });
- },
-
- mlNodeCount() {
- return http<{ count: number }>({
- path: `${basePath()}/ml_node_count`,
- method: 'GET',
- });
- },
-
- mlInfo() {
- return http({
- path: `${basePath()}/info`,
- method: 'GET',
- });
- },
-
- calculateModelMemoryLimit$({
- analysisConfig,
- indexPattern,
- query,
- timeFieldName,
- earliestMs,
- latestMs,
- }: {
- analysisConfig: AnalysisConfig;
- indexPattern: string;
- query: any;
- timeFieldName: string;
- earliestMs: number;
- latestMs: number;
- }) {
- const body = JSON.stringify({
+ }: {
+ indexPatternTitle: string;
+ query: any;
+ timeFieldName?: string;
+ earliest?: number;
+ latest?: number;
+ samplerShardSize?: number;
+ aggregatableFields: string[];
+ nonAggregatableFields: string[];
+ }) {
+ const body = JSON.stringify({
+ query,
+ timeFieldName,
+ earliest,
+ latest,
+ samplerShardSize,
+ aggregatableFields,
+ nonAggregatableFields,
+ });
+
+ return httpService.http({
+ path: `${basePath()}/data_visualizer/get_overall_stats/${indexPatternTitle}`,
+ method: 'POST',
+ body,
+ });
+ },
+
+ /**
+ * Gets a list of calendars
+ * @param obj
+ * @returns {Promise}
+ */
+ calendars(obj?: { calendarId?: CalendarId; calendarIds?: CalendarId[] }) {
+ const { calendarId, calendarIds } = obj || {};
+ let calendarIdsPathComponent = '';
+ if (calendarId) {
+ calendarIdsPathComponent = `/${calendarId}`;
+ } else if (calendarIds) {
+ calendarIdsPathComponent = `/${calendarIds.join(',')}`;
+ }
+ return httpService.http({
+ path: `${basePath()}/calendars${calendarIdsPathComponent}`,
+ method: 'GET',
+ });
+ },
+
+ addCalendar(obj: Calendar) {
+ const body = JSON.stringify(obj);
+ return httpService.http({
+ path: `${basePath()}/calendars`,
+ method: 'PUT',
+ body,
+ });
+ },
+
+ updateCalendar(obj: UpdateCalendar) {
+ const calendarId = obj && obj.calendarId ? `/${obj.calendarId}` : '';
+ const body = JSON.stringify(obj);
+ return httpService.http({
+ path: `${basePath()}/calendars${calendarId}`,
+ method: 'PUT',
+ body,
+ });
+ },
+
+ deleteCalendar({ calendarId }: { calendarId?: string }) {
+ return httpService.http({
+ path: `${basePath()}/calendars/${calendarId}`,
+ method: 'DELETE',
+ });
+ },
+
+ mlNodeCount() {
+ return httpService.http<{ count: number }>({
+ path: `${basePath()}/ml_node_count`,
+ method: 'GET',
+ });
+ },
+
+ mlInfo() {
+ return httpService.http({
+ path: `${basePath()}/info`,
+ method: 'GET',
+ });
+ },
+
+ calculateModelMemoryLimit$({
analysisConfig,
indexPattern,
query,
timeFieldName,
earliestMs,
latestMs,
- });
-
- return http$<{ modelMemoryLimit: string }>({
- path: `${basePath()}/validate/calculate_model_memory_limit`,
- method: 'POST',
- body,
- });
- },
-
- getCardinalityOfFields({
- index,
- fieldNames,
- query,
- timeFieldName,
- earliestMs,
- latestMs,
- }: {
- index: string;
- fieldNames: string[];
- query: any;
- timeFieldName: string;
- earliestMs: number;
- latestMs: number;
- }) {
- const body = JSON.stringify({ index, fieldNames, query, timeFieldName, earliestMs, latestMs });
-
- return http({
- path: `${basePath()}/fields_service/field_cardinality`,
- method: 'POST',
- body,
- });
- },
-
- getTimeFieldRange({
- index,
- timeFieldName,
- query,
- }: {
- index: string;
- timeFieldName?: string;
- query: any;
- }) {
- const body = JSON.stringify({ index, timeFieldName, query });
-
- return http({
- path: `${basePath()}/fields_service/time_field_range`,
- method: 'POST',
- body,
- });
- },
-
- esSearch(obj: any) {
- const body = JSON.stringify(obj);
- return http({
- path: `${basePath()}/es_search`,
- method: 'POST',
- body,
- });
- },
-
- esSearch$(obj: any) {
- const body = JSON.stringify(obj);
- return http$({
- path: `${basePath()}/es_search`,
- method: 'POST',
- body,
- });
- },
-
- getIndices() {
- const tempBasePath = '/api';
- return http>({
- path: `${tempBasePath}/index_management/indices`,
- method: 'GET',
- });
- },
-
- getModelSnapshots(jobId: string, snapshotId?: string) {
- return http({
- path: `${basePath()}/anomaly_detectors/${jobId}/model_snapshots${
- snapshotId !== undefined ? `/${snapshotId}` : ''
- }`,
- });
- },
-
- updateModelSnapshot(
- jobId: string,
- snapshotId: string,
- body: { description?: string; retain?: boolean }
- ) {
- return http({
- path: `${basePath()}/anomaly_detectors/${jobId}/model_snapshots/${snapshotId}/_update`,
- method: 'POST',
- body: JSON.stringify(body),
- });
- },
-
- deleteModelSnapshot(jobId: string, snapshotId: string) {
- return http({
- path: `${basePath()}/anomaly_detectors/${jobId}/model_snapshots/${snapshotId}`,
- method: 'DELETE',
- });
- },
-
- annotations,
- dataFrameAnalytics,
- filters,
- results,
- jobs,
- fileDatavisualizer,
-};
+ }: {
+ analysisConfig: AnalysisConfig;
+ indexPattern: string;
+ query: any;
+ timeFieldName: string;
+ earliestMs: number;
+ latestMs: number;
+ }) {
+ const body = JSON.stringify({
+ analysisConfig,
+ indexPattern,
+ query,
+ timeFieldName,
+ earliestMs,
+ latestMs,
+ });
+
+ return httpService.http$<{ modelMemoryLimit: string }>({
+ path: `${basePath()}/validate/calculate_model_memory_limit`,
+ method: 'POST',
+ body,
+ });
+ },
+
+ getCardinalityOfFields({
+ index,
+ fieldNames,
+ query,
+ timeFieldName,
+ earliestMs,
+ latestMs,
+ }: {
+ index: string;
+ fieldNames: string[];
+ query: any;
+ timeFieldName: string;
+ earliestMs: number;
+ latestMs: number;
+ }) {
+ const body = JSON.stringify({
+ index,
+ fieldNames,
+ query,
+ timeFieldName,
+ earliestMs,
+ latestMs,
+ });
+
+ return httpService.http({
+ path: `${basePath()}/fields_service/field_cardinality`,
+ method: 'POST',
+ body,
+ });
+ },
+
+ getTimeFieldRange({
+ index,
+ timeFieldName,
+ query,
+ }: {
+ index: string;
+ timeFieldName?: string;
+ query: any;
+ }) {
+ const body = JSON.stringify({ index, timeFieldName, query });
+
+ return httpService.http({
+ path: `${basePath()}/fields_service/time_field_range`,
+ method: 'POST',
+ body,
+ });
+ },
+
+ esSearch(obj: any) {
+ const body = JSON.stringify(obj);
+ return httpService.http({
+ path: `${basePath()}/es_search`,
+ method: 'POST',
+ body,
+ });
+ },
+
+ esSearch$(obj: any) {
+ const body = JSON.stringify(obj);
+ return httpService.http$({
+ path: `${basePath()}/es_search`,
+ method: 'POST',
+ body,
+ });
+ },
+
+ getIndices() {
+ const tempBasePath = '/api';
+ return httpService.http>({
+ path: `${tempBasePath}/index_management/indices`,
+ method: 'GET',
+ });
+ },
+
+ getModelSnapshots(jobId: string, snapshotId?: string) {
+ return http({
+ path: `${basePath()}/anomaly_detectors/${jobId}/model_snapshots${
+ snapshotId !== undefined ? `/${snapshotId}` : ''
+ }`,
+ });
+ },
+
+ updateModelSnapshot(
+ jobId: string,
+ snapshotId: string,
+ body: { description?: string; retain?: boolean }
+ ) {
+ return http({
+ path: `${basePath()}/anomaly_detectors/${jobId}/model_snapshots/${snapshotId}/_update`,
+ method: 'POST',
+ body: JSON.stringify(body),
+ });
+ },
+
+ deleteModelSnapshot(jobId: string, snapshotId: string) {
+ return http({
+ path: `${basePath()}/anomaly_detectors/${jobId}/model_snapshots/${snapshotId}`,
+ method: 'DELETE',
+ });
+ },
+
+ annotations,
+ dataFrameAnalytics,
+ filters,
+ results: resultsApiProvider(httpService),
+ jobs,
+ fileDatavisualizer,
+ };
+}
diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/results.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/results.ts
index 830e6fab4163a3..521fd306847eba 100644
--- a/x-pack/plugins/ml/public/application/services/ml_api_service/results.ts
+++ b/x-pack/plugins/ml/public/application/services/ml_api_service/results.ts
@@ -5,14 +5,14 @@
*/
// Service for obtaining data for the ML Results dashboards.
-import { http, http$ } from '../http_service';
+import { HttpService } from '../http_service';
import { basePath } from './index';
import { JobId } from '../../../../common/types/anomaly_detection_jobs';
import { PartitionFieldsDefinition } from '../results_service/result_service_rx';
-export const results = {
+export const resultsApiProvider = (httpService: HttpService) => ({
getAnomaliesTableData(
jobIds: string[],
criteriaFields: string[],
@@ -40,7 +40,7 @@ export const results = {
influencersFilterQuery,
});
- return http$({
+ return httpService.http$({
path: `${basePath()}/results/anomalies_table_data`,
method: 'POST',
body,
@@ -53,7 +53,7 @@ export const results = {
earliestMs,
latestMs,
});
- return http({
+ return httpService.http({
path: `${basePath()}/results/max_anomaly_score`,
method: 'POST',
body,
@@ -62,7 +62,7 @@ export const results = {
getCategoryDefinition(jobId: string, categoryId: string) {
const body = JSON.stringify({ jobId, categoryId });
- return http({
+ return httpService.http({
path: `${basePath()}/results/category_definition`,
method: 'POST',
body,
@@ -75,7 +75,7 @@ export const results = {
categoryIds,
maxExamples,
});
- return http({
+ return httpService.http({
path: `${basePath()}/results/category_examples`,
method: 'POST',
body,
@@ -90,10 +90,10 @@ export const results = {
latestMs: number
) {
const body = JSON.stringify({ jobId, searchTerm, criteriaFields, earliestMs, latestMs });
- return http$({
+ return httpService.http$({
path: `${basePath()}/results/partition_fields_values`,
method: 'POST',
body,
});
},
-};
+});
diff --git a/x-pack/plugins/ml/public/application/services/results_service/index.ts b/x-pack/plugins/ml/public/application/services/results_service/index.ts
index cc02248f4d5a9a..6c508422e70632 100644
--- a/x-pack/plugins/ml/public/application/services/results_service/index.ts
+++ b/x-pack/plugins/ml/public/application/services/results_service/index.ts
@@ -4,47 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import {
- getMetricData,
- getModelPlotOutput,
- getRecordsForCriteria,
- getScheduledEventsByBucket,
- fetchPartitionFieldsValues,
-} from './result_service_rx';
-import {
- getEventDistributionData,
- getEventRateData,
- getInfluencerValueMaxScoreByTime,
- getOverallBucketScores,
- getRecordInfluencers,
- getRecordMaxScoreByTime,
- getRecords,
- getRecordsForDetector,
- getRecordsForInfluencer,
- getScoresByBucket,
- getTopInfluencers,
- getTopInfluencerValues,
-} from './results_service';
-
-export const mlResultsService = {
- getScoresByBucket,
- getScheduledEventsByBucket,
- getTopInfluencers,
- getTopInfluencerValues,
- getOverallBucketScores,
- getInfluencerValueMaxScoreByTime,
- getRecordInfluencers,
- getRecordsForInfluencer,
- getRecordsForDetector,
- getRecords,
- getRecordsForCriteria,
- getMetricData,
- getEventRateData,
- getEventDistributionData,
- getModelPlotOutput,
- getRecordMaxScoreByTime,
- fetchPartitionFieldsValues,
-};
+import { resultsServiceRxProvider } from './result_service_rx';
+import { resultsServiceProvider } from './results_service';
+import { ml, MlApiServices } from '../ml_api_service';
export type MlResultsService = typeof mlResultsService;
@@ -57,3 +19,12 @@ export interface CriteriaField {
fieldName: string;
fieldValue: any;
}
+
+export const mlResultsService = mlResultsServiceProvider(ml);
+
+export function mlResultsServiceProvider(mlApiServices: MlApiServices) {
+ return {
+ ...resultsServiceProvider(mlApiServices),
+ ...resultsServiceRxProvider(mlApiServices),
+ };
+}
diff --git a/x-pack/plugins/ml/public/application/services/results_service/result_service_rx.ts b/x-pack/plugins/ml/public/application/services/results_service/result_service_rx.ts
index a21d0caaedd339..1bcbd8dbcdd639 100644
--- a/x-pack/plugins/ml/public/application/services/results_service/result_service_rx.ts
+++ b/x-pack/plugins/ml/public/application/services/results_service/result_service_rx.ts
@@ -17,7 +17,7 @@ import _ from 'lodash';
import { Dictionary } from '../../../../common/types/common';
import { ML_MEDIAN_PERCENTS } from '../../../../common/util/job_utils';
import { JobId } from '../../../../common/types/anomaly_detection_jobs';
-import { ml } from '../ml_api_service';
+import { MlApiServices } from '../ml_api_service';
import { ML_RESULTS_INDEX_PATTERN } from '../../../../common/constants/index_patterns';
import { CriteriaField } from './index';
@@ -46,524 +46,528 @@ export type PartitionFieldsDefinition = {
[field in FieldTypes]: FieldDefinition;
};
-export function getMetricData(
- index: string,
- entityFields: any[],
- query: object | undefined,
- metricFunction: string, // ES aggregation name
- metricFieldName: string,
- timeFieldName: string,
- earliestMs: number,
- latestMs: number,
- interval: string
-): Observable {
- // Build the criteria to use in the bool filter part of the request.
- // Add criteria for the time range, entity fields,
- // plus any additional supplied query.
- const shouldCriteria: object[] = [];
- const mustCriteria: object[] = [
- {
- range: {
- [timeFieldName]: {
- gte: earliestMs,
- lte: latestMs,
- format: 'epoch_millis',
- },
- },
- },
- ...(query ? [query] : []),
- ];
-
- entityFields.forEach((entity) => {
- if (entity.fieldValue.length !== 0) {
- mustCriteria.push({
- term: {
- [entity.fieldName]: entity.fieldValue,
- },
- });
- } else {
- // Add special handling for blank entity field values, checking for either
- // an empty string or the field not existing.
- shouldCriteria.push({
- bool: {
- must: [
- {
- term: {
- [entity.fieldName]: '',
- },
- },
- ],
- },
- });
- shouldCriteria.push({
- bool: {
- must_not: [
- {
- exists: { field: entity.fieldName },
- },
- ],
- },
- });
- }
- });
-
- const body: any = {
- query: {
- bool: {
- must: mustCriteria,
- },
- },
- size: 0,
- _source: {
- excludes: [],
- },
- aggs: {
- byTime: {
- date_histogram: {
- field: timeFieldName,
- interval,
- min_doc_count: 0,
- },
- },
- },
- };
-
- if (shouldCriteria.length > 0) {
- body.query.bool.should = shouldCriteria;
- body.query.bool.minimum_should_match = shouldCriteria.length / 2;
- }
-
- if (metricFieldName !== undefined && metricFieldName !== '') {
- body.aggs.byTime.aggs = {};
-
- const metricAgg: any = {
- [metricFunction]: {
- field: metricFieldName,
- },
- };
-
- if (metricFunction === 'percentiles') {
- metricAgg[metricFunction].percents = [ML_MEDIAN_PERCENTS];
- }
- body.aggs.byTime.aggs.metric = metricAgg;
- }
-
- return ml.esSearch$({ index, body }).pipe(
- map((resp: any) => {
- const obj: MetricData = { success: true, results: {} };
- const dataByTime = resp?.aggregations?.byTime?.buckets ?? [];
- dataByTime.forEach((dataForTime: any) => {
- if (metricFunction === 'count') {
- obj.results[dataForTime.key] = dataForTime.doc_count;
- } else {
- const value = dataForTime?.metric?.value;
- const values = dataForTime?.metric?.values;
- if (dataForTime.doc_count === 0) {
- obj.results[dataForTime.key] = null;
- } else if (value !== undefined) {
- obj.results[dataForTime.key] = value;
- } else if (values !== undefined) {
- // Percentiles agg currently returns NaN rather than null when none of the docs in the
- // bucket contain the field used in the aggregation
- // (see elasticsearch issue https://github.com/elastic/elasticsearch/issues/29066).
- // Store as null, so values can be handled in the same manner downstream as other aggs
- // (min, mean, max) which return null.
- const medianValues = values[ML_MEDIAN_PERCENTS];
- obj.results[dataForTime.key] = !isNaN(medianValues) ? medianValues : null;
- } else {
- obj.results[dataForTime.key] = null;
- }
- }
- });
-
- return obj;
- })
- );
-}
-
export interface ModelPlotOutput extends ResultResponse {
results: Record;
}
-export function getModelPlotOutput(
- jobId: string,
- detectorIndex: number,
- criteriaFields: any[],
- earliestMs: number,
- latestMs: number,
- interval: string,
- aggType?: { min: any; max: any }
-): Observable {
- const obj: ModelPlotOutput = {
- success: true,
- results: {},
- };
+export interface RecordsForCriteria extends ResultResponse {
+ records: any[];
+}
- // if an aggType object has been passed in, use it.
- // otherwise default to min and max aggs for the upper and lower bounds
- const modelAggs =
- aggType === undefined
- ? { max: 'max', min: 'min' }
- : {
- max: aggType.max,
- min: aggType.min,
- };
+export interface ScheduledEventsByBucket extends ResultResponse {
+ events: Record;
+}
- // Build the criteria to use in the bool filter part of the request.
- // Add criteria for the job ID and time range.
- const mustCriteria: object[] = [
- {
- term: { job_id: jobId },
- },
- {
- range: {
- timestamp: {
- gte: earliestMs,
- lte: latestMs,
- format: 'epoch_millis',
- },
- },
- },
- ];
-
- // Add in term queries for each of the specified criteria.
- _.each(criteriaFields, (criteria) => {
- mustCriteria.push({
- term: {
- [criteria.fieldName]: criteria.fieldValue,
- },
- });
- });
-
- // Add criteria for the detector index. Results from jobs created before 6.1 will not
- // contain a detector_index field, so use a should criteria with a 'not exists' check.
- const shouldCriteria = [
- {
- term: { detector_index: detectorIndex },
- },
- {
- bool: {
- must_not: [
- {
- exists: { field: 'detector_index' },
+export function resultsServiceRxProvider(mlApiServices: MlApiServices) {
+ return {
+ getMetricData(
+ index: string,
+ entityFields: any[],
+ query: object | undefined,
+ metricFunction: string, // ES aggregation name
+ metricFieldName: string,
+ timeFieldName: string,
+ earliestMs: number,
+ latestMs: number,
+ interval: string
+ ): Observable {
+ // Build the criteria to use in the bool filter part of the request.
+ // Add criteria for the time range, entity fields,
+ // plus any additional supplied query.
+ const shouldCriteria: object[] = [];
+ const mustCriteria: object[] = [
+ {
+ range: {
+ [timeFieldName]: {
+ gte: earliestMs,
+ lte: latestMs,
+ format: 'epoch_millis',
+ },
},
- ],
- },
- },
- ];
+ },
+ ...(query ? [query] : []),
+ ];
+
+ entityFields.forEach((entity) => {
+ if (entity.fieldValue.length !== 0) {
+ mustCriteria.push({
+ term: {
+ [entity.fieldName]: entity.fieldValue,
+ },
+ });
+ } else {
+ // Add special handling for blank entity field values, checking for either
+ // an empty string or the field not existing.
+ shouldCriteria.push({
+ bool: {
+ must: [
+ {
+ term: {
+ [entity.fieldName]: '',
+ },
+ },
+ ],
+ },
+ });
+ shouldCriteria.push({
+ bool: {
+ must_not: [
+ {
+ exists: { field: entity.fieldName },
+ },
+ ],
+ },
+ });
+ }
+ });
- return ml
- .esSearch$({
- index: ML_RESULTS_INDEX_PATTERN,
- size: 0,
- body: {
+ const body: any = {
query: {
bool: {
- filter: [
- {
- query_string: {
- query: 'result_type:model_plot',
- analyze_wildcard: true,
- },
- },
- {
- bool: {
- must: mustCriteria,
- should: shouldCriteria,
- minimum_should_match: 1,
- },
- },
- ],
+ must: mustCriteria,
},
},
+ size: 0,
+ _source: {
+ excludes: [],
+ },
aggs: {
- times: {
+ byTime: {
date_histogram: {
- field: 'timestamp',
+ field: timeFieldName,
interval,
min_doc_count: 0,
},
- aggs: {
- actual: {
- avg: {
- field: 'actual',
- },
- },
- modelUpper: {
- [modelAggs.max]: {
- field: 'model_upper',
- },
- },
- modelLower: {
- [modelAggs.min]: {
- field: 'model_lower',
- },
- },
- },
},
},
- },
- })
- .pipe(
- map((resp) => {
- const aggregationsByTime = _.get(resp, ['aggregations', 'times', 'buckets'], []);
- _.each(aggregationsByTime, (dataForTime: any) => {
- const time = dataForTime.key;
- const modelUpper: number | undefined = _.get(dataForTime, ['modelUpper', 'value']);
- const modelLower: number | undefined = _.get(dataForTime, ['modelLower', 'value']);
- const actual = _.get(dataForTime, ['actual', 'value']);
-
- obj.results[time] = {
- actual,
- modelUpper:
- modelUpper === undefined || isFinite(modelUpper) === false ? null : modelUpper,
- modelLower:
- modelLower === undefined || isFinite(modelLower) === false ? null : modelLower,
- };
- });
+ };
- return obj;
- })
- );
-}
+ if (shouldCriteria.length > 0) {
+ body.query.bool.should = shouldCriteria;
+ body.query.bool.minimum_should_match = shouldCriteria.length / 2;
+ }
-export interface RecordsForCriteria extends ResultResponse {
- records: any[];
-}
+ if (metricFieldName !== undefined && metricFieldName !== '') {
+ body.aggs.byTime.aggs = {};
-// Queries Elasticsearch to obtain the record level results matching the given criteria,
-// for the specified job(s), time range, and record score threshold.
-// criteriaFields parameter must be an array, with each object in the array having 'fieldName'
-// 'fieldValue' properties.
-// Pass an empty array or ['*'] to search over all job IDs.
-export function getRecordsForCriteria(
- jobIds: string[] | undefined,
- criteriaFields: CriteriaField[],
- threshold: any,
- earliestMs: number,
- latestMs: number,
- maxResults: number | undefined
-): Observable {
- const obj: RecordsForCriteria = { success: true, records: [] };
-
- // Build the criteria to use in the bool filter part of the request.
- // Add criteria for the time range, record score, plus any specified job IDs.
- const boolCriteria: any[] = [
- {
- range: {
- timestamp: {
- gte: earliestMs,
- lte: latestMs,
- format: 'epoch_millis',
- },
- },
- },
- {
- range: {
- record_score: {
- gte: threshold,
- },
- },
- },
- ];
+ const metricAgg: any = {
+ [metricFunction]: {
+ field: metricFieldName,
+ },
+ };
- if (jobIds && jobIds.length > 0 && !(jobIds.length === 1 && jobIds[0] === '*')) {
- let jobIdFilterStr = '';
- _.each(jobIds, (jobId, i) => {
- if (i > 0) {
- jobIdFilterStr += ' OR ';
+ if (metricFunction === 'percentiles') {
+ metricAgg[metricFunction].percents = [ML_MEDIAN_PERCENTS];
+ }
+ body.aggs.byTime.aggs.metric = metricAgg;
}
- jobIdFilterStr += 'job_id:';
- jobIdFilterStr += jobId;
- });
- boolCriteria.push({
- query_string: {
- analyze_wildcard: false,
- query: jobIdFilterStr,
- },
- });
- }
-
- // Add in term queries for each of the specified criteria.
- _.each(criteriaFields, (criteria) => {
- boolCriteria.push({
- term: {
- [criteria.fieldName]: criteria.fieldValue,
- },
- });
- });
-
- return ml
- .esSearch$({
- index: ML_RESULTS_INDEX_PATTERN,
- rest_total_hits_as_int: true,
- size: maxResults !== undefined ? maxResults : 100,
- body: {
- query: {
- bool: {
- filter: [
- {
- query_string: {
- query: 'result_type:record',
- analyze_wildcard: false,
- },
- },
- {
- bool: {
- must: boolCriteria,
- },
- },
- ],
- },
- },
- sort: [{ record_score: { order: 'desc' } }],
- },
- })
- .pipe(
- map((resp) => {
- if (resp.hits.total !== 0) {
- _.each(resp.hits.hits, (hit: any) => {
- obj.records.push(hit._source);
+
+ return mlApiServices.esSearch$({ index, body }).pipe(
+ map((resp: any) => {
+ const obj: MetricData = { success: true, results: {} };
+ const dataByTime = resp?.aggregations?.byTime?.buckets ?? [];
+ dataByTime.forEach((dataForTime: any) => {
+ if (metricFunction === 'count') {
+ obj.results[dataForTime.key] = dataForTime.doc_count;
+ } else {
+ const value = dataForTime?.metric?.value;
+ const values = dataForTime?.metric?.values;
+ if (dataForTime.doc_count === 0) {
+ obj.results[dataForTime.key] = null;
+ } else if (value !== undefined) {
+ obj.results[dataForTime.key] = value;
+ } else if (values !== undefined) {
+ // Percentiles agg currently returns NaN rather than null when none of the docs in the
+ // bucket contain the field used in the aggregation
+ // (see elasticsearch issue https://github.com/elastic/elasticsearch/issues/29066).
+ // Store as null, so values can be handled in the same manner downstream as other aggs
+ // (min, mean, max) which return null.
+ const medianValues = values[ML_MEDIAN_PERCENTS];
+ obj.results[dataForTime.key] = !isNaN(medianValues) ? medianValues : null;
+ } else {
+ obj.results[dataForTime.key] = null;
+ }
+ }
});
- }
- return obj;
- })
- );
-}
-export interface ScheduledEventsByBucket extends ResultResponse {
- events: Record;
-}
+ return obj;
+ })
+ );
+ },
-// Obtains a list of scheduled events by job ID and time.
-// Pass an empty array or ['*'] to search over all job IDs.
-// Returned response contains a events property, which will only
-// contains keys for jobs which have scheduled events for the specified time range.
-export function getScheduledEventsByBucket(
- jobIds: string[] | undefined,
- earliestMs: number,
- latestMs: number,
- interval: string,
- maxJobs: number,
- maxEvents: number
-): Observable {
- const obj: ScheduledEventsByBucket = {
- success: true,
- events: {},
- };
+ getModelPlotOutput(
+ jobId: string,
+ detectorIndex: number,
+ criteriaFields: any[],
+ earliestMs: number,
+ latestMs: number,
+ interval: string,
+ aggType?: { min: any; max: any }
+ ): Observable {
+ const obj: ModelPlotOutput = {
+ success: true,
+ results: {},
+ };
+
+ // if an aggType object has been passed in, use it.
+ // otherwise default to min and max aggs for the upper and lower bounds
+ const modelAggs =
+ aggType === undefined
+ ? { max: 'max', min: 'min' }
+ : {
+ max: aggType.max,
+ min: aggType.min,
+ };
+
+ // Build the criteria to use in the bool filter part of the request.
+ // Add criteria for the job ID and time range.
+ const mustCriteria: object[] = [
+ {
+ term: { job_id: jobId },
+ },
+ {
+ range: {
+ timestamp: {
+ gte: earliestMs,
+ lte: latestMs,
+ format: 'epoch_millis',
+ },
+ },
+ },
+ ];
- // Build the criteria to use in the bool filter part of the request.
- // Adds criteria for the time range plus any specified job IDs.
- const boolCriteria: any[] = [
- {
- range: {
- timestamp: {
- gte: earliestMs,
- lte: latestMs,
- format: 'epoch_millis',
+ // Add in term queries for each of the specified criteria.
+ _.each(criteriaFields, (criteria) => {
+ mustCriteria.push({
+ term: {
+ [criteria.fieldName]: criteria.fieldValue,
+ },
+ });
+ });
+
+ // Add criteria for the detector index. Results from jobs created before 6.1 will not
+ // contain a detector_index field, so use a should criteria with a 'not exists' check.
+ const shouldCriteria = [
+ {
+ term: { detector_index: detectorIndex },
},
- },
- },
- {
- exists: { field: 'scheduled_events' },
- },
- ];
-
- if (jobIds && jobIds.length > 0 && !(jobIds.length === 1 && jobIds[0] === '*')) {
- let jobIdFilterStr = '';
- _.each(jobIds, (jobId, i) => {
- jobIdFilterStr += `${i > 0 ? ' OR ' : ''}job_id:${jobId}`;
- });
- boolCriteria.push({
- query_string: {
- analyze_wildcard: false,
- query: jobIdFilterStr,
- },
- });
- }
-
- return ml
- .esSearch$({
- index: ML_RESULTS_INDEX_PATTERN,
- size: 0,
- body: {
- query: {
+ {
bool: {
- filter: [
+ must_not: [
{
- query_string: {
- query: 'result_type:bucket',
- analyze_wildcard: false,
- },
- },
- {
- bool: {
- must: boolCriteria,
- },
+ exists: { field: 'detector_index' },
},
],
},
},
- aggs: {
- jobs: {
- terms: {
- field: 'job_id',
- min_doc_count: 1,
- size: maxJobs,
+ ];
+
+ return mlApiServices
+ .esSearch$({
+ index: ML_RESULTS_INDEX_PATTERN,
+ size: 0,
+ body: {
+ query: {
+ bool: {
+ filter: [
+ {
+ query_string: {
+ query: 'result_type:model_plot',
+ analyze_wildcard: true,
+ },
+ },
+ {
+ bool: {
+ must: mustCriteria,
+ should: shouldCriteria,
+ minimum_should_match: 1,
+ },
+ },
+ ],
+ },
},
aggs: {
times: {
date_histogram: {
field: 'timestamp',
interval,
- min_doc_count: 1,
+ min_doc_count: 0,
},
aggs: {
- events: {
- terms: {
- field: 'scheduled_events',
- size: maxEvents,
+ actual: {
+ avg: {
+ field: 'actual',
+ },
+ },
+ modelUpper: {
+ [modelAggs.max]: {
+ field: 'model_upper',
+ },
+ },
+ modelLower: {
+ [modelAggs.min]: {
+ field: 'model_lower',
},
},
},
},
},
},
+ })
+ .pipe(
+ map((resp) => {
+ const aggregationsByTime = _.get(resp, ['aggregations', 'times', 'buckets'], []);
+ _.each(aggregationsByTime, (dataForTime: any) => {
+ const time = dataForTime.key;
+ const modelUpper: number | undefined = _.get(dataForTime, ['modelUpper', 'value']);
+ const modelLower: number | undefined = _.get(dataForTime, ['modelLower', 'value']);
+ const actual = _.get(dataForTime, ['actual', 'value']);
+
+ obj.results[time] = {
+ actual,
+ modelUpper:
+ modelUpper === undefined || isFinite(modelUpper) === false ? null : modelUpper,
+ modelLower:
+ modelLower === undefined || isFinite(modelLower) === false ? null : modelLower,
+ };
+ });
+
+ return obj;
+ })
+ );
+ },
+
+ // Queries Elasticsearch to obtain the record level results matching the given criteria,
+ // for the specified job(s), time range, and record score threshold.
+ // criteriaFields parameter must be an array, with each object in the array having 'fieldName'
+ // 'fieldValue' properties.
+ // Pass an empty array or ['*'] to search over all job IDs.
+ getRecordsForCriteria(
+ jobIds: string[] | undefined,
+ criteriaFields: CriteriaField[],
+ threshold: any,
+ earliestMs: number,
+ latestMs: number,
+ maxResults: number | undefined
+ ): Observable {
+ const obj: RecordsForCriteria = { success: true, records: [] };
+
+ // Build the criteria to use in the bool filter part of the request.
+ // Add criteria for the time range, record score, plus any specified job IDs.
+ const boolCriteria: any[] = [
+ {
+ range: {
+ timestamp: {
+ gte: earliestMs,
+ lte: latestMs,
+ format: 'epoch_millis',
+ },
+ },
},
- },
- })
- .pipe(
- map((resp) => {
- const dataByJobId = _.get(resp, ['aggregations', 'jobs', 'buckets'], []);
- _.each(dataByJobId, (dataForJob: any) => {
- const jobId: string = dataForJob.key;
- const resultsForTime: Record = {};
- const dataByTime = _.get(dataForJob, ['times', 'buckets'], []);
- _.each(dataByTime, (dataForTime: any) => {
- const time: string = dataForTime.key;
- const events: object[] = _.get(dataForTime, ['events', 'buckets']);
- resultsForTime[time] = _.map(events, 'key');
- });
- obj.events[jobId] = resultsForTime;
+ {
+ range: {
+ record_score: {
+ gte: threshold,
+ },
+ },
+ },
+ ];
+
+ if (jobIds && jobIds.length > 0 && !(jobIds.length === 1 && jobIds[0] === '*')) {
+ let jobIdFilterStr = '';
+ _.each(jobIds, (jobId, i) => {
+ if (i > 0) {
+ jobIdFilterStr += ' OR ';
+ }
+ jobIdFilterStr += 'job_id:';
+ jobIdFilterStr += jobId;
+ });
+ boolCriteria.push({
+ query_string: {
+ analyze_wildcard: false,
+ query: jobIdFilterStr,
+ },
});
+ }
- return obj;
- })
- );
-}
+ // Add in term queries for each of the specified criteria.
+ _.each(criteriaFields, (criteria) => {
+ boolCriteria.push({
+ term: {
+ [criteria.fieldName]: criteria.fieldValue,
+ },
+ });
+ });
-export function fetchPartitionFieldsValues(
- jobId: JobId,
- searchTerm: Dictionary,
- criteriaFields: Array<{ fieldName: string; fieldValue: any }>,
- earliestMs: number,
- latestMs: number
-) {
- return ml.results.fetchPartitionFieldsValues(
- jobId,
- searchTerm,
- criteriaFields,
- earliestMs,
- latestMs
- );
+ return mlApiServices
+ .esSearch$({
+ index: ML_RESULTS_INDEX_PATTERN,
+ rest_total_hits_as_int: true,
+ size: maxResults !== undefined ? maxResults : 100,
+ body: {
+ query: {
+ bool: {
+ filter: [
+ {
+ query_string: {
+ query: 'result_type:record',
+ analyze_wildcard: false,
+ },
+ },
+ {
+ bool: {
+ must: boolCriteria,
+ },
+ },
+ ],
+ },
+ },
+ sort: [{ record_score: { order: 'desc' } }],
+ },
+ })
+ .pipe(
+ map((resp) => {
+ if (resp.hits.total !== 0) {
+ _.each(resp.hits.hits, (hit: any) => {
+ obj.records.push(hit._source);
+ });
+ }
+ return obj;
+ })
+ );
+ },
+
+ // Obtains a list of scheduled events by job ID and time.
+ // Pass an empty array or ['*'] to search over all job IDs.
+ // Returned response contains a events property, which will only
+ // contains keys for jobs which have scheduled events for the specified time range.
+ getScheduledEventsByBucket(
+ jobIds: string[] | undefined,
+ earliestMs: number,
+ latestMs: number,
+ interval: string,
+ maxJobs: number,
+ maxEvents: number
+ ): Observable {
+ const obj: ScheduledEventsByBucket = {
+ success: true,
+ events: {},
+ };
+
+ // Build the criteria to use in the bool filter part of the request.
+ // Adds criteria for the time range plus any specified job IDs.
+ const boolCriteria: any[] = [
+ {
+ range: {
+ timestamp: {
+ gte: earliestMs,
+ lte: latestMs,
+ format: 'epoch_millis',
+ },
+ },
+ },
+ {
+ exists: { field: 'scheduled_events' },
+ },
+ ];
+
+ if (jobIds && jobIds.length > 0 && !(jobIds.length === 1 && jobIds[0] === '*')) {
+ let jobIdFilterStr = '';
+ _.each(jobIds, (jobId, i) => {
+ jobIdFilterStr += `${i > 0 ? ' OR ' : ''}job_id:${jobId}`;
+ });
+ boolCriteria.push({
+ query_string: {
+ analyze_wildcard: false,
+ query: jobIdFilterStr,
+ },
+ });
+ }
+
+ return mlApiServices
+ .esSearch$({
+ index: ML_RESULTS_INDEX_PATTERN,
+ size: 0,
+ body: {
+ query: {
+ bool: {
+ filter: [
+ {
+ query_string: {
+ query: 'result_type:bucket',
+ analyze_wildcard: false,
+ },
+ },
+ {
+ bool: {
+ must: boolCriteria,
+ },
+ },
+ ],
+ },
+ },
+ aggs: {
+ jobs: {
+ terms: {
+ field: 'job_id',
+ min_doc_count: 1,
+ size: maxJobs,
+ },
+ aggs: {
+ times: {
+ date_histogram: {
+ field: 'timestamp',
+ interval,
+ min_doc_count: 1,
+ },
+ aggs: {
+ events: {
+ terms: {
+ field: 'scheduled_events',
+ size: maxEvents,
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ })
+ .pipe(
+ map((resp) => {
+ const dataByJobId = _.get(resp, ['aggregations', 'jobs', 'buckets'], []);
+ _.each(dataByJobId, (dataForJob: any) => {
+ const jobId: string = dataForJob.key;
+ const resultsForTime: Record = {};
+ const dataByTime = _.get(dataForJob, ['times', 'buckets'], []);
+ _.each(dataByTime, (dataForTime: any) => {
+ const time: string = dataForTime.key;
+ const events: object[] = _.get(dataForTime, ['events', 'buckets']);
+ resultsForTime[time] = _.map(events, 'key');
+ });
+ obj.events[jobId] = resultsForTime;
+ });
+
+ return obj;
+ })
+ );
+ },
+
+ fetchPartitionFieldsValues(
+ jobId: JobId,
+ searchTerm: Dictionary,
+ criteriaFields: Array<{ fieldName: string; fieldValue: any }>,
+ earliestMs: number,
+ latestMs: number
+ ) {
+ return mlApiServices.results.fetchPartitionFieldsValues(
+ jobId,
+ searchTerm,
+ criteriaFields,
+ earliestMs,
+ latestMs
+ );
+ },
+ };
}
diff --git a/x-pack/plugins/ml/public/application/services/results_service/results_service.d.ts b/x-pack/plugins/ml/public/application/services/results_service/results_service.d.ts
index 4af08994432bdb..1b2c01ab73fcef 100644
--- a/x-pack/plugins/ml/public/application/services/results_service/results_service.d.ts
+++ b/x-pack/plugins/ml/public/application/services/results_service/results_service.d.ts
@@ -4,43 +4,49 @@
* you may not use this file except in compliance with the Elastic License.
*/
-export function getScoresByBucket(
- jobIds: string[],
- earliestMs: number,
- latestMs: number,
- interval: string | number,
- maxResults: number
-): Promise;
-export function getTopInfluencers(): Promise;
-export function getTopInfluencerValues(): Promise;
-export function getOverallBucketScores(
- jobIds: any,
- topN: any,
- earliestMs: any,
- latestMs: any,
- interval?: any
-): Promise;
-export function getInfluencerValueMaxScoreByTime(
- jobIds: string[],
- influencerFieldName: string,
- influencerFieldValues: string[],
- earliestMs: number,
- latestMs: number,
- interval: string,
- maxResults: number,
- influencersFilterQuery: any
-): Promise;
-export function getRecordInfluencers(): Promise;
-export function getRecordsForInfluencer(): Promise;
-export function getRecordsForDetector(): Promise;
-export function getRecords(): Promise;
-export function getEventRateData(
- index: string,
- query: any,
- timeFieldName: string,
- earliestMs: number,
- latestMs: number,
- interval: string | number
-): Promise;
-export function getEventDistributionData(): Promise;
-export function getRecordMaxScoreByTime(): Promise;
+import { MlApiServices } from '../ml_api_service';
+
+export function resultsServiceProvider(
+ mlApiServices: MlApiServices
+): {
+ getScoresByBucket(
+ jobIds: string[],
+ earliestMs: number,
+ latestMs: number,
+ interval: string | number,
+ maxResults: number
+ ): Promise;
+ getTopInfluencers(): Promise;
+ getTopInfluencerValues(): Promise;
+ getOverallBucketScores(
+ jobIds: any,
+ topN: any,
+ earliestMs: any,
+ latestMs: any,
+ interval?: any
+ ): Promise;
+ getInfluencerValueMaxScoreByTime(
+ jobIds: string[],
+ influencerFieldName: string,
+ influencerFieldValues: string[],
+ earliestMs: number,
+ latestMs: number,
+ interval: string,
+ maxResults: number,
+ influencersFilterQuery: any
+ ): Promise;
+ getRecordInfluencers(): Promise;
+ getRecordsForInfluencer(): Promise;
+ getRecordsForDetector(): Promise;
+ getRecords(): Promise;
+ getEventRateData(
+ index: string,
+ query: any,
+ timeFieldName: string,
+ earliestMs: number,
+ latestMs: number,
+ interval: string | number
+ ): Promise;
+ getEventDistributionData(): Promise;
+ getRecordMaxScoreByTime(): Promise