[ML] Functional tests - basic tests for single metric viewer and anomaly explorer (#54699) (#54746)

This PR adds basic functional UI tests for the single metric viewer and the anomaly explorer.
This commit is contained in:
Robert Oskamp 2020-01-14 20:11:48 +01:00 committed by GitHub
parent b1d2c00c3c
commit 2a3a9f7260
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 437 additions and 4 deletions

View file

@ -195,6 +195,7 @@ class AnomaliesTable extends Component {
return {
onMouseOver: () => this.onMouseOverRow(item),
onMouseLeave: () => this.onMouseLeaveRow(),
'data-test-subj': `mlAnomaliesListRow row-${item.rowId}`,
};
};

View file

@ -80,11 +80,13 @@ export function getColumns(
})
}
data-row-id={item.rowId}
data-test-subj="mlJobListRowDetailsToggle"
/>
),
},
{
field: 'time',
'data-test-subj': 'mlAnomaliesListColumnTime',
name: i18n.translate('xpack.ml.anomaliesTable.timeColumnName', {
defaultMessage: 'time',
}),
@ -95,6 +97,7 @@ export function getColumns(
},
{
field: 'severity',
'data-test-subj': 'mlAnomaliesListColumnSeverity',
name: i18n.translate('xpack.ml.anomaliesTable.severityColumnName', {
defaultMessage: 'severity',
}),
@ -105,6 +108,7 @@ export function getColumns(
},
{
field: 'detector',
'data-test-subj': 'mlAnomaliesListColumnDetector',
name: i18n.translate('xpack.ml.anomaliesTable.detectorColumnName', {
defaultMessage: 'detector',
}),
@ -119,6 +123,7 @@ export function getColumns(
if (items.some(item => item.entityValue !== undefined)) {
columns.push({
field: 'entityValue',
'data-test-subj': 'mlAnomaliesListColumnFoundFor',
name: i18n.translate('xpack.ml.anomaliesTable.entityValueColumnName', {
defaultMessage: 'found for',
}),
@ -138,6 +143,7 @@ export function getColumns(
if (items.some(item => item.influencers !== undefined)) {
columns.push({
field: 'influencers',
'data-test-subj': 'mlAnomaliesListColumnInfluencers',
name: i18n.translate('xpack.ml.anomaliesTable.influencersColumnName', {
defaultMessage: 'influenced by',
}),
@ -159,6 +165,7 @@ export function getColumns(
if (items.some(item => item.actual !== undefined)) {
columns.push({
field: 'actualSort',
'data-test-subj': 'mlAnomaliesListColumnActual',
name: i18n.translate('xpack.ml.anomaliesTable.actualSortColumnName', {
defaultMessage: 'actual',
}),
@ -176,6 +183,7 @@ export function getColumns(
if (items.some(item => item.typical !== undefined)) {
columns.push({
field: 'typicalSort',
'data-test-subj': 'mlAnomaliesListColumnTypical',
name: i18n.translate('xpack.ml.anomaliesTable.typicalSortColumnName', {
defaultMessage: 'typical',
}),
@ -198,6 +206,7 @@ export function getColumns(
if (nonTimeOfDayOrWeek === true) {
columns.push({
field: 'metricDescriptionSort',
'data-test-subj': 'mlAnomaliesListColumnDescription',
name: i18n.translate('xpack.ml.anomaliesTable.metricDescriptionSortColumnName', {
defaultMessage: 'description',
}),
@ -213,6 +222,7 @@ export function getColumns(
if (jobIds && jobIds.length > 1) {
columns.push({
field: 'jobId',
'data-test-subj': 'mlAnomaliesListColumnJobID',
name: i18n.translate('xpack.ml.anomaliesTable.jobIdColumnName', {
defaultMessage: 'job ID',
}),
@ -223,6 +233,7 @@ export function getColumns(
const showExamples = items.some(item => item.entityName === 'mlcategory');
if (showExamples === true) {
columns.push({
'data-test-subj': 'mlAnomaliesListColumnCategoryExamples',
name: i18n.translate('xpack.ml.anomaliesTable.categoryExamplesColumnName', {
defaultMessage: 'category examples',
}),
@ -254,6 +265,7 @@ export function getColumns(
if (showLinks === true) {
columns.push({
'data-test-subj': 'mlAnomaliesListColumnAction',
name: i18n.translate('xpack.ml.anomaliesTable.actionsColumnName', {
defaultMessage: 'actions',
}),

View file

@ -56,7 +56,7 @@ function Influencer({ influencerFieldName, influencerFilter, valueData }) {
const tooltipContent = getTooltipContent(maxScoreLabel, totalScoreLabel);
return (
<div>
<div data-test-subj={`mlInfluencerEntry field-${influencerFieldName}`}>
<div className="field-label">
{influencerFieldName !== 'mlcategory' ? (
<EntityCell
@ -114,7 +114,7 @@ function InfluencersByName({ influencerFieldName, influencerFilter, fieldValues
return (
<React.Fragment key={influencerFieldName}>
<EuiTitle size="xs">
<EuiTitle size="xs" data-test-subj={`mlInfluencerFieldName ${influencerFieldName}`}>
<h4>{influencerFieldName}</h4>
</EuiTitle>
<EuiSpacer size="xs" />

View file

@ -33,7 +33,7 @@ export function JobSelectorBadge({ icon, id, isGroup = false, numJobs, removeId
}
return (
<EuiBadge key={`${id}-id`} {...props}>
<EuiBadge key={`${id}-id`} data-test-subj={`mlJobSelectionBadge ${id}`} {...props}>
{`${id}${jobCount ? jobCount : ''}`}
</EuiBadge>
);

View file

@ -12,7 +12,11 @@ import { EuiLoadingChart, EuiSpacer } from '@elastic/eui';
export function LoadingIndicator({ height, label }) {
height = height ? +height : 100;
return (
<div className="ml-loading-indicator" style={{ height: `${height}px` }}>
<div
className="ml-loading-indicator"
style={{ height: `${height}px` }}
data-test-subj="mlLoadingIndicator"
>
<EuiLoadingChart size="xl" mono />
{label && (
<>

View file

@ -1314,6 +1314,7 @@ export class TimeSeriesExplorer extends React.Component {
onChange={this.detectorIndexChangeHandler}
value={selectedDetectorIndex}
options={detectorSelectOptions}
data-test-subj="mlSingleMetricViewerDetectorSelect"
/>
</EuiFormRow>
</EuiFlexItem>

View file

@ -0,0 +1,95 @@
/*
* 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 expect from '@kbn/expect';
import { FtrProviderContext } from '../../../ftr_provider_context';
import {
Job,
Datafeed,
} from '../../../../..//legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/configs';
const JOB_CONFIG: Job = {
job_id: `fq_multi_1_ae`,
description:
'mean/min/max(responsetime) partition=airline on farequote dataset with 1h bucket span',
groups: ['farequote', 'automated', 'multi-metric'],
analysis_config: {
bucket_span: '1h',
influencers: ['airline'],
detectors: [
{ function: 'mean', field_name: 'responsetime', partition_field_name: 'airline' },
{ function: 'min', field_name: 'responsetime', partition_field_name: 'airline' },
{ function: 'max', field_name: 'responsetime', partition_field_name: 'airline' },
],
},
data_description: { time_field: '@timestamp' },
analysis_limits: { model_memory_limit: '20mb' },
model_plot_config: { enabled: true },
};
const DATAFEED_CONFIG: Datafeed = {
datafeed_id: 'datafeed-fq_multi_1_se',
indices: ['farequote'],
job_id: 'fq_multi_1_ae',
query: { bool: { must: [{ match_all: {} }] } },
};
// eslint-disable-next-line import/no-default-export
export default function({ getService }: FtrProviderContext) {
const esArchiver = getService('esArchiver');
const ml = getService('ml');
describe('anomaly explorer', function() {
this.tags(['smoke', 'mlqa']);
before(async () => {
await esArchiver.load('ml/farequote');
await ml.api.createAndRunAnomalyDetectionLookbackJob(JOB_CONFIG, DATAFEED_CONFIG);
});
after(async () => {
await esArchiver.unload('ml/farequote');
await ml.api.cleanMlIndices();
});
it('loads from job list row link', async () => {
await ml.navigation.navigateToMl();
await ml.navigation.navigateToJobManagement();
await ml.jobTable.waitForJobsToLoad();
await ml.jobTable.filterWithSearchString(JOB_CONFIG.job_id);
const rows = await ml.jobTable.parseJobTable();
expect(rows.filter(row => row.id === JOB_CONFIG.job_id)).to.have.length(1);
await ml.jobTable.clickOpenJobInAnomalyExplorerButton(JOB_CONFIG.job_id);
await ml.common.waitForMlLoadingIndicatorToDisappear();
});
it('pre-fills the job selection', async () => {
await ml.jobSelection.assertJobSelection([JOB_CONFIG.job_id]);
});
it('displays the influencers list', async () => {
await ml.anomalyExplorer.assertInfluencerListExists();
for (const influencerField of JOB_CONFIG.analysis_config.influencers) {
await ml.anomalyExplorer.assertInfluencerFieldExists(influencerField);
await ml.anomalyExplorer.assertInfluencerFieldListNotEmpty(influencerField);
}
});
it('displays the swimlanes', async () => {
await ml.anomalyExplorer.assertOverallSwimlaneExists();
await ml.anomalyExplorer.assertSwimlaneViewByExists();
});
it('displays the anomalies table', async () => {
await ml.anomaliesTable.assertTableExists();
});
it('anomalies table is not empty', async () => {
await ml.anomaliesTable.assertTableNotEmpty();
});
});
}

View file

@ -14,5 +14,7 @@ export default function({ loadTestFile }: FtrProviderContext) {
loadTestFile(require.resolve('./population_job'));
loadTestFile(require.resolve('./saved_search_job'));
loadTestFile(require.resolve('./advanced_job'));
loadTestFile(require.resolve('./single_metric_viewer'));
loadTestFile(require.resolve('./anomaly_explorer'));
});
}

View file

@ -0,0 +1,91 @@
/*
* 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 expect from '@kbn/expect';
import { FtrProviderContext } from '../../../ftr_provider_context';
import {
Job,
Datafeed,
} from '../../../../..//legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/configs';
const JOB_CONFIG: Job = {
job_id: `fq_single_1_smv`,
description: 'mean(responsetime) on farequote dataset with 15m bucket span',
groups: ['farequote', 'automated', 'single-metric'],
analysis_config: {
bucket_span: '15m',
influencers: [],
detectors: [
{
function: 'mean',
field_name: 'responsetime',
},
],
},
data_description: { time_field: '@timestamp' },
analysis_limits: { model_memory_limit: '10mb' },
model_plot_config: { enabled: true },
};
const DATAFEED_CONFIG: Datafeed = {
datafeed_id: 'datafeed-fq_single_1_smv',
indices: ['farequote'],
job_id: 'fq_single_1_smv',
query: { bool: { must: [{ match_all: {} }] } },
};
// eslint-disable-next-line import/no-default-export
export default function({ getService }: FtrProviderContext) {
const esArchiver = getService('esArchiver');
const ml = getService('ml');
describe('single metric viewer', function() {
this.tags(['smoke', 'mlqa']);
before(async () => {
await esArchiver.load('ml/farequote');
await ml.api.createAndRunAnomalyDetectionLookbackJob(JOB_CONFIG, DATAFEED_CONFIG);
});
after(async () => {
await esArchiver.unload('ml/farequote');
await ml.api.cleanMlIndices();
});
it('loads from job list row link', async () => {
await ml.navigation.navigateToMl();
await ml.navigation.navigateToJobManagement();
await ml.jobTable.waitForJobsToLoad();
await ml.jobTable.filterWithSearchString(JOB_CONFIG.job_id);
const rows = await ml.jobTable.parseJobTable();
expect(rows.filter(row => row.id === JOB_CONFIG.job_id)).to.have.length(1);
await ml.jobTable.clickOpenJobInSingleMetricViewerButton(JOB_CONFIG.job_id);
await ml.common.waitForMlLoadingIndicatorToDisappear();
});
it('pre-fills the job selection', async () => {
await ml.jobSelection.assertJobSelection([JOB_CONFIG.job_id]);
});
it('pre-fills the detector input', async () => {
await ml.singleMetricViewer.assertDetectorInputExsist();
await ml.singleMetricViewer.assertDetectorInputValue('0');
});
it('displays the chart', async () => {
await ml.singleMetricViewer.assertChartExsist();
});
it('displays the anomalies table', async () => {
await ml.anomaliesTable.assertTableExists();
});
it('anomalies table is not empty', async () => {
await ml.anomaliesTable.assertTableNotEmpty();
});
});
}

View file

@ -0,0 +1,26 @@
/*
* 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 expect from '@kbn/expect';
import { FtrProviderContext } from '../../ftr_provider_context';
export function MachineLearningAnomaliesTableProvider({ getService }: FtrProviderContext) {
const testSubjects = getService('testSubjects');
return {
async assertTableExists() {
await testSubjects.existOrFail('mlAnomaliesTable');
},
async assertTableNotEmpty() {
const tableRows = await testSubjects.findAll('mlAnomaliesTable > ~mlAnomaliesListRow');
expect(tableRows.length).to.be.greaterThan(
0,
'Anomalies table should have at least one row (got 0)'
);
},
};
}

View file

@ -3,6 +3,7 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../ftr_provider_context';
@ -13,5 +14,31 @@ export function MachineLearningAnomalyExplorerProvider({ getService }: FtrProvid
async assertAnomalyExplorerEmptyListMessageExists() {
await testSubjects.existOrFail('mlNoJobsFound');
},
async assertInfluencerListExists() {
await testSubjects.existOrFail('mlAnomalyExplorerInfluencerList');
},
async assertInfluencerFieldExists(influencerField: string) {
await testSubjects.existOrFail(`mlInfluencerFieldName ${influencerField}`);
},
async assertInfluencerFieldListNotEmpty(influencerField: string) {
const influencerFieldEntries = await testSubjects.findAll(
`mlInfluencerEntry field-${influencerField}`
);
expect(influencerFieldEntries.length).to.be.greaterThan(
0,
`Influencer list for field '${influencerField}' should have at least one entry (got 0)`
);
},
async assertOverallSwimlaneExists() {
await testSubjects.existOrFail('mlAnomalyExplorerSwimlaneOverall');
},
async assertSwimlaneViewByExists() {
await testSubjects.existOrFail('mlAnomalyExplorerSwimlaneViewBy');
},
};
}

View file

@ -10,6 +10,8 @@ import { FtrProviderContext } from '../../ftr_provider_context';
import { JOB_STATE, DATAFEED_STATE } from '../../../../legacy/plugins/ml/common/constants/states';
import { DATA_FRAME_TASK_STATE } from '../../../../legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common';
import { Job } from '../../../..//legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/configs/job';
import { Datafeed } from '../../../..//legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/configs/datafeed';
export type MlApi = ProvidedType<typeof MachineLearningAPIProvider>;
@ -270,5 +272,89 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) {
}
});
},
async getAnomalyDetectionJob(jobId: string) {
return await esSupertest.get(`/_ml/anomaly_detectors/${jobId}`).expect(200);
},
async createAnomalyDetectionJob(jobConfig: Job) {
const jobId = jobConfig.job_id;
log.debug(`Creating anomaly detection job with id '${jobId}'...`);
await esSupertest
.put(`/_ml/anomaly_detectors/${jobId}`)
.send(jobConfig)
.expect(200);
await retry.waitForWithTimeout(`'${jobId}' to be created`, 5 * 1000, async () => {
if (await this.getAnomalyDetectionJob(jobId)) {
return true;
} else {
throw new Error(`expected anomaly detection job '${jobId}' to be created`);
}
});
},
async getDatafeed(datafeedId: string) {
return await esSupertest.get(`/_ml/datafeeds/${datafeedId}`).expect(200);
},
async createDatafeed(datafeedConfig: Datafeed) {
const datafeedId = datafeedConfig.datafeed_id;
log.debug(`Creating datafeed with id '${datafeedId}'...`);
await esSupertest
.put(`/_ml/datafeeds/${datafeedId}`)
.send(datafeedConfig)
.expect(200);
await retry.waitForWithTimeout(`'${datafeedId}' to be created`, 5 * 1000, async () => {
if (await this.getDatafeed(datafeedId)) {
return true;
} else {
throw new Error(`expected datafeed '${datafeedId}' to be created`);
}
});
},
async openAnomalyDetectionJob(jobId: string) {
log.debug(`Opening anomaly detection job '${jobId}'...`);
const openResponse = await esSupertest
.post(`/_ml/anomaly_detectors/${jobId}/_open`)
.send({ timeout: '10s' })
.set({ 'Content-Type': 'application/json' })
.expect(200)
.then((res: any) => res.body);
expect(openResponse)
.to.have.property('opened')
.eql(true, 'Response for open job request should be acknowledged');
},
async startDatafeed(
datafeedId: string,
startConfig: { start?: string; end?: string } = { start: '0' }
) {
log.debug(
`Starting datafeed '${datafeedId}' with start: '${startConfig.start}', end: '${startConfig.end}'...`
);
const startResponse = await esSupertest
.post(`/_ml/datafeeds/${datafeedId}/_start`)
.send(startConfig)
.set({ 'Content-Type': 'application/json' })
.expect(200)
.then((res: any) => res.body);
expect(startResponse)
.to.have.property('started')
.eql(true, 'Response for start datafeed request should be acknowledged');
},
async createAndRunAnomalyDetectionLookbackJob(jobConfig: Job, datafeedConfig: Datafeed) {
await this.createAnomalyDetectionJob(jobConfig);
await this.createDatafeed(datafeedConfig);
await this.openAnomalyDetectionJob(jobConfig.job_id);
await this.startDatafeed(datafeedConfig.datafeed_id, { start: '0', end: `${Date.now()}` });
await this.waitForDatafeedState(datafeedConfig.datafeed_id, DATAFEED_STATE.STOPPED);
await this.waitForJobState(jobConfig.job_id, JOB_STATE.CLOSED);
},
};
}

View file

@ -72,5 +72,11 @@ export function MachineLearningCommonProvider({ getService }: FtrProviderContext
}
});
},
async waitForMlLoadingIndicatorToDisappear() {
await retry.tryForTime(10 * 1000, async () => {
await testSubjects.missingOrFail('mlLoadingIndicator');
});
},
};
}

View file

@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
export { MachineLearningAnomaliesTableProvider } from './anomalies_table';
export { MachineLearningAnomalyExplorerProvider } from './anomaly_explorer';
export { MachineLearningAPIProvider } from './api';
export { MachineLearningCommonProvider } from './common';
@ -14,6 +15,7 @@ export { MachineLearningDataFrameAnalyticsTableProvider } from './data_frame_ana
export { MachineLearningDataVisualizerProvider } from './data_visualizer';
export { MachineLearningDataVisualizerIndexBasedProvider } from './data_visualizer_index_based';
export { MachineLearningJobManagementProvider } from './job_management';
export { MachineLearningJobSelectionProvider } from './job_selection';
export { MachineLearningJobSourceSelectionProvider } from './job_source_selection';
export { MachineLearningJobTableProvider } from './job_table';
export { MachineLearningJobTypeSelectionProvider } from './job_type_selection';

View file

@ -0,0 +1,27 @@
/*
* 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 expect from '@kbn/expect';
import { FtrProviderContext } from '../../ftr_provider_context';
export function MachineLearningJobSelectionProvider({ getService }: FtrProviderContext) {
const testSubjects = getService('testSubjects');
return {
async assertJobSelection(jobOrGroupIds: string[]) {
const selectedJobsOrGroups = await testSubjects.findAll(
'mlJobSelectionBadges > ~mlJobSelectionBadge'
);
const actualJobOrGroupLabels = await Promise.all(
selectedJobsOrGroups.map(async badge => await badge.getVisibleText())
);
expect(actualJobOrGroupLabels).to.eql(
jobOrGroupIds,
`Job selection should display jobs or groups '${jobOrGroupIds}' (got '${actualJobOrGroupLabels}')`
);
},
};
}

View file

@ -235,5 +235,15 @@ export function MachineLearningJobTableProvider({ getService }: FtrProviderConte
await testSubjects.click('mlDeleteJobConfirmModal > confirmModalConfirmButton');
await testSubjects.missingOrFail('mlDeleteJobConfirmModal', { timeout: 30 * 1000 });
}
public async clickOpenJobInSingleMetricViewerButton(jobId: string) {
await testSubjects.click(`~openJobsInSingleMetricViewer-${jobId}`);
await testSubjects.existOrFail('~mlPageSingleMetricViewer');
}
public async clickOpenJobInAnomalyExplorerButton(jobId: string) {
await testSubjects.click(`~openJobsInSingleAnomalyExplorer-${jobId}`);
await testSubjects.existOrFail('~mlPageAnomalyExplorer');
}
})();
}

View file

@ -3,6 +3,7 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../ftr_provider_context';
@ -13,5 +14,40 @@ export function MachineLearningSingleMetricViewerProvider({ getService }: FtrPro
async assertSingleMetricViewerEmptyListMessageExsist() {
await testSubjects.existOrFail('mlNoSingleMetricJobsFound');
},
async assertForecastButtonExistsExsist() {
await testSubjects.existOrFail(
'mlSingleMetricViewerSeriesControls > mlSingleMetricViewerButtonForecast'
);
},
async assertDetectorInputExsist() {
await testSubjects.existOrFail(
'mlSingleMetricViewerSeriesControls > mlSingleMetricViewerDetectorSelect'
);
},
async assertDetectorInputValue(expectedDetectorOptionValue: string) {
const actualDetectorValue = await testSubjects.getAttribute(
'mlSingleMetricViewerSeriesControls > mlSingleMetricViewerDetectorSelect',
'value'
);
expect(actualDetectorValue).to.eql(
expectedDetectorOptionValue,
`Detector input option value should be '${expectedDetectorOptionValue}' (got '${actualDetectorValue}')`
);
},
async setDetectorInputValue(detectorOptionValue: string) {
await testSubjects.selectValue(
'mlSingleMetricViewerSeriesControls > mlSingleMetricViewerDetectorSelect',
detectorOptionValue
);
await this.assertDetectorInputValue(detectorOptionValue);
},
async assertChartExsist() {
await testSubjects.existOrFail('mlSingleMetricViewerChart');
},
};
}

View file

@ -7,6 +7,7 @@
import { FtrProviderContext } from '../ftr_provider_context';
import {
MachineLearningAnomaliesTableProvider,
MachineLearningAnomalyExplorerProvider,
MachineLearningAPIProvider,
MachineLearningCommonProvider,
@ -17,6 +18,7 @@ import {
MachineLearningDataVisualizerProvider,
MachineLearningDataVisualizerIndexBasedProvider,
MachineLearningJobManagementProvider,
MachineLearningJobSelectionProvider,
MachineLearningJobSourceSelectionProvider,
MachineLearningJobTableProvider,
MachineLearningJobTypeSelectionProvider,
@ -32,6 +34,7 @@ import {
export function MachineLearningProvider(context: FtrProviderContext) {
const common = MachineLearningCommonProvider(context);
const anomaliesTable = MachineLearningAnomaliesTableProvider(context);
const anomalyExplorer = MachineLearningAnomalyExplorerProvider(context);
const api = MachineLearningAPIProvider(context);
const customUrls = MachineLearningCustomUrlsProvider(context);
@ -41,6 +44,7 @@ export function MachineLearningProvider(context: FtrProviderContext) {
const dataVisualizer = MachineLearningDataVisualizerProvider(context);
const dataVisualizerIndexBased = MachineLearningDataVisualizerIndexBasedProvider(context);
const jobManagement = MachineLearningJobManagementProvider(context, api);
const jobSelection = MachineLearningJobSelectionProvider(context);
const jobSourceSelection = MachineLearningJobSourceSelectionProvider(context);
const jobTable = MachineLearningJobTableProvider(context);
const jobTypeSelection = MachineLearningJobTypeSelectionProvider(context);
@ -53,8 +57,10 @@ export function MachineLearningProvider(context: FtrProviderContext) {
const singleMetricViewer = MachineLearningSingleMetricViewerProvider(context);
return {
anomaliesTable,
anomalyExplorer,
api,
common,
customUrls,
dataFrameAnalytics,
dataFrameAnalyticsCreation,
@ -62,6 +68,7 @@ export function MachineLearningProvider(context: FtrProviderContext) {
dataVisualizer,
dataVisualizerIndexBased,
jobManagement,
jobSelection,
jobSourceSelection,
jobTable,
jobTypeSelection,