From 6a9e9031d7e6c3e878c4eb221973375e19ae3ef6 Mon Sep 17 00:00:00 2001 From: Robert Jaszczurek <92210485+rbrtj@users.noreply.github.com> Date: Fri, 20 Sep 2024 14:06:29 +0200 Subject: [PATCH] [ML] Anomaly Explorer: Hide top influencers panel for jobs without influencers (#192987) # Summary Fix for [#192679](https://github.com/elastic/kibana/issues/192679) Hiding the top influencers panel when there are no influencers for the selected job. Added a functional test to ensure the panel is hidden. Expanded a few types to improve type safety. (cherry picked from commit 23b2595be39401214a1ef9e39b684f917020b9ad) --- .../explorer/actions/job_selection.ts | 8 +- .../explorer/explorer_constants.ts | 2 +- .../explorer/explorer_dashboard_service.ts | 46 +- .../explorer_reducer/get_index_pattern.ts | 5 +- .../explorer_reducer/job_selection_change.ts | 8 +- .../reducers/explorer_reducer/reducer.ts | 17 +- .../anomaly_explorer.ts | 887 ++++++++++-------- .../services/ml/anomaly_explorer.ts | 4 + 8 files changed, 536 insertions(+), 441 deletions(-) diff --git a/x-pack/plugins/ml/public/application/explorer/actions/job_selection.ts b/x-pack/plugins/ml/public/application/explorer/actions/job_selection.ts index 2523db6fa8165..bd6bcd6e95657 100644 --- a/x-pack/plugins/ml/public/application/explorer/actions/job_selection.ts +++ b/x-pack/plugins/ml/public/application/explorer/actions/job_selection.ts @@ -5,6 +5,7 @@ * 2.0. */ +import type { Observable } from 'rxjs'; import { from } from 'rxjs'; import { map } from 'rxjs'; @@ -12,13 +13,14 @@ import type { MlFieldFormatService } from '../../services/field_format_service'; import type { MlJobService } from '../../services/job_service'; import { EXPLORER_ACTION } from '../explorer_constants'; -import { createJobs } from '../explorer_utils'; +import { createJobs, getInfluencers } from '../explorer_utils'; +import type { ExplorerActions } from '../explorer_dashboard_service'; export function jobSelectionActionCreator( mlJobService: MlJobService, mlFieldFormatService: MlFieldFormatService, selectedJobIds: string[] -) { +): Observable { return from(mlFieldFormatService.populateFormats(selectedJobIds)).pipe( map((resp) => { if (resp.error) { @@ -31,12 +33,14 @@ export function jobSelectionActionCreator( }); const selectedJobs = jobs.filter((job) => job.selected); + const noInfluencersConfigured = getInfluencers(mlJobService, selectedJobs).length === 0; return { type: EXPLORER_ACTION.JOB_SELECTION_CHANGE, payload: { loading: false, selectedJobs, + noInfluencersConfigured, }, }; }) 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 2be1a88b55880..9deb2ca8aa74f 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_constants.ts +++ b/x-pack/plugins/ml/public/application/explorer/explorer_constants.ts @@ -24,7 +24,7 @@ export const EXPLORER_ACTION = { JOB_SELECTION_CHANGE: 'jobSelectionChange', SET_CHARTS_DATA_LOADING: 'setChartsDataLoading', SET_EXPLORER_DATA: 'setExplorerData', -}; +} as const; export const FILTER_ACTION = { ADD: '+', diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_dashboard_service.ts b/x-pack/plugins/ml/public/application/explorer/explorer_dashboard_service.ts index 1cf723f5145d6..2f5ed99c9a401 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_dashboard_service.ts +++ b/x-pack/plugins/ml/public/application/explorer/explorer_dashboard_service.ts @@ -21,21 +21,38 @@ import type { ExplorerState } from './reducers'; import { explorerReducer, getExplorerDefaultState } from './reducers'; import type { MlFieldFormatService } from '../services/field_format_service'; import type { MlJobService } from '../services/job_service'; +import type { ExplorerJob } from './explorer_utils'; -type ExplorerAction = Action | Observable; -export const explorerAction$ = new Subject(); +type ExplorerAction = (typeof EXPLORER_ACTION)[keyof typeof EXPLORER_ACTION]; -export type ActionPayload = any; - -export interface Action { - type: string; - payload?: ActionPayload; +export interface ExplorerActionPayloads { + [EXPLORER_ACTION.SET_EXPLORER_DATA]: DeepPartial; + [EXPLORER_ACTION.JOB_SELECTION_CHANGE]: { + loading: boolean; + selectedJobs: ExplorerJob[]; + noInfluencersConfigured: boolean; + }; } +export type ExplorerActions = { + [K in ExplorerAction]: K extends keyof ExplorerActionPayloads + ? { + type: K; + payload: ExplorerActionPayloads[K]; + } + : { + type: K; + }; +}[ExplorerAction]; + +type ExplorerActionMaybeObservable = ExplorerActions | Observable; + +export const explorerAction$ = new Subject(); + const explorerFilteredAction$ = explorerAction$.pipe( // consider observables as side-effects - flatMap((action: ExplorerAction) => - isObservable(action) ? action : (from([action]) as Observable) + flatMap((action: ExplorerActionMaybeObservable) => + isObservable(action) ? action : (from([action]) as Observable) ), distinctUntilChanged(isEqual) ); @@ -47,11 +64,6 @@ const explorerState$: Observable = explorerFilteredAction$.pipe( shareReplay(1) ); -const setExplorerDataActionCreator = (payload: DeepPartial) => ({ - type: EXPLORER_ACTION.SET_EXPLORER_DATA, - payload, -}); - // Export observable state and action dispatchers as service export const explorerServiceFactory = ( mlJobService: MlJobService, @@ -62,7 +74,9 @@ export const explorerServiceFactory = ( explorerAction$.next({ type: EXPLORER_ACTION.CLEAR_EXPLORER_DATA }); }, clearInfluencerFilterSettings: () => { - explorerAction$.next({ type: EXPLORER_ACTION.CLEAR_INFLUENCER_FILTER_SETTINGS }); + explorerAction$.next({ + type: EXPLORER_ACTION.CLEAR_INFLUENCER_FILTER_SETTINGS, + }); }, clearJobs: () => { explorerAction$.next({ type: EXPLORER_ACTION.CLEAR_JOBS }); @@ -73,7 +87,7 @@ export const explorerServiceFactory = ( ); }, setExplorerData: (payload: DeepPartial) => { - explorerAction$.next(setExplorerDataActionCreator(payload)); + explorerAction$.next({ type: EXPLORER_ACTION.SET_EXPLORER_DATA, payload }); }, setChartsDataLoading: () => { explorerAction$.next({ type: EXPLORER_ACTION.SET_CHARTS_DATA_LOADING }); diff --git a/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/get_index_pattern.ts b/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/get_index_pattern.ts index ca360a9c4cb69..878ba9370c95b 100644 --- a/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/get_index_pattern.ts +++ b/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/get_index_pattern.ts @@ -6,14 +6,15 @@ */ import { ML_RESULTS_INDEX_PATTERN } from '../../../../../common/constants/index_patterns'; +import type { ExplorerJob } from '../../explorer_utils'; // Creates index pattern in the format expected by the kuery bar/kuery autocomplete provider // Field objects required fields: name, type, aggregatable, searchable -export function getIndexPattern(influencers: string[]) { +export function getIndexPattern(influencers: ExplorerJob[]) { return { title: ML_RESULTS_INDEX_PATTERN, fields: influencers.map((influencer) => ({ - name: influencer, + name: influencer.id, type: 'string', aggregatable: true, searchable: true, diff --git a/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/job_selection_change.ts b/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/job_selection_change.ts index da657c3f18bc7..58f7461b11047 100644 --- a/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/job_selection_change.ts +++ b/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/job_selection_change.ts @@ -5,12 +5,16 @@ * 2.0. */ -import type { ActionPayload } from '../../explorer_dashboard_service'; +import type { EXPLORER_ACTION } from '../../explorer_constants'; +import type { ExplorerActionPayloads } from '../../explorer_dashboard_service'; import { getIndexPattern } from './get_index_pattern'; import type { ExplorerState } from './state'; -export const jobSelectionChange = (state: ExplorerState, payload: ActionPayload): ExplorerState => { +export const jobSelectionChange = ( + state: ExplorerState, + payload: ExplorerActionPayloads[typeof EXPLORER_ACTION.JOB_SELECTION_CHANGE] +): ExplorerState => { const { selectedJobs, noInfluencersConfigured } = payload; const stateUpdate: ExplorerState = { ...state, diff --git a/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/reducer.ts b/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/reducer.ts index d7f52a9277a3c..4be342c9333ad 100644 --- a/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/reducer.ts +++ b/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/reducer.ts @@ -7,7 +7,7 @@ import { getDefaultChartsData } from '../../explorer_charts/explorer_charts_container_service'; import { EXPLORER_ACTION } from '../../explorer_constants'; -import type { Action } from '../../explorer_dashboard_service'; +import type { ExplorerActionPayloads, ExplorerActions } from '../../explorer_dashboard_service'; import { getClearedSelectedAnomaliesState } from '../../explorer_utils'; import { clearInfluencerFilterSettings } from './clear_influencer_filter_settings'; @@ -16,8 +16,12 @@ import type { ExplorerState } from './state'; import { getExplorerDefaultState } from './state'; import { setKqlQueryBarPlaceholder } from './set_kql_query_bar_placeholder'; -export const explorerReducer = (state: ExplorerState, nextAction: Action): ExplorerState => { - const { type, payload } = nextAction; +export const explorerReducer = ( + state: ExplorerState, + nextAction: ExplorerActions +): ExplorerState => { + const { type } = nextAction; + const payload = 'payload' in nextAction ? nextAction.payload : {}; let nextState: ExplorerState; @@ -40,7 +44,10 @@ export const explorerReducer = (state: ExplorerState, nextAction: Action): Explo break; case EXPLORER_ACTION.JOB_SELECTION_CHANGE: - nextState = jobSelectionChange(state, payload); + nextState = jobSelectionChange( + state, + payload as ExplorerActionPayloads[typeof EXPLORER_ACTION.JOB_SELECTION_CHANGE] + ); break; case EXPLORER_ACTION.SET_CHARTS_DATA_LOADING: @@ -52,7 +59,7 @@ export const explorerReducer = (state: ExplorerState, nextAction: Action): Explo break; case EXPLORER_ACTION.SET_EXPLORER_DATA: - nextState = { ...state, ...payload }; + nextState = { ...state, ...(payload as Partial) }; break; default: diff --git a/x-pack/test/functional/apps/ml/anomaly_detection_result_views/anomaly_explorer.ts b/x-pack/test/functional/apps/ml/anomaly_detection_result_views/anomaly_explorer.ts index c323356c73e6e..e4d87f05bc9a5 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection_result_views/anomaly_explorer.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection_result_views/anomaly_explorer.ts @@ -31,6 +31,15 @@ const JOB_CONFIG: Job = { model_plot_config: { enabled: true }, }; +const JOB_CONFIG_NO_INFLUENCERS: Job = { + ...JOB_CONFIG, + job_id: `${JOB_CONFIG.job_id}_no_influencers`, + analysis_config: { + ...JOB_CONFIG.analysis_config, + influencers: [], + }, +}; + // @ts-expect-error not full interface const DATAFEED_CONFIG: Datafeed = { datafeed_id: 'datafeed-fq_multi_1_ae', @@ -39,7 +48,13 @@ const DATAFEED_CONFIG: Datafeed = { query: { bool: { must: [{ match_all: {} }] } }, }; -const testDataList = [ +const DATAFEED_CONFIG_NO_INFLUENCERS: Datafeed = { + ...DATAFEED_CONFIG, + datafeed_id: `datafeed-${JOB_CONFIG_NO_INFLUENCERS.job_id}`, + job_id: JOB_CONFIG_NO_INFLUENCERS.job_id, +}; + +const testDataListWithInfluencers = [ { suiteSuffix: 'with farequote based multi metric job', jobConfig: JOB_CONFIG, @@ -56,6 +71,17 @@ const testDataList = [ }, ]; +const testDataListWithNoInfluencers = [ + { + suiteSuffix: 'with no influencers', + jobConfig: JOB_CONFIG_NO_INFLUENCERS, + datafeedConfig: DATAFEED_CONFIG_NO_INFLUENCERS, + expected: { + influencers: [], + }, + }, +]; + const cellSize = 15; const overallSwimLaneTestSubj = 'mlAnomalyExplorerSwimlaneOverall'; @@ -70,7 +96,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('anomaly explorer', function () { this.tags(['ml']); - before(async () => { await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/farequote'); await ml.testResources.createDataViewIfNeeded('ft_farequote', '@timestamp'); @@ -85,495 +110,531 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await ml.testResources.deleteDataViewByTitle('ft_farequote'); }); - for (const testData of testDataList) { - describe(testData.suiteSuffix, function () { - before(async () => { - await ml.api.createAndRunAnomalyDetectionLookbackJob( - testData.jobConfig, - testData.datafeedConfig - ); - }); + describe('with influencers', function () { + for (const testData of testDataListWithInfluencers) { + describe(testData.suiteSuffix, function () { + before(async () => { + await ml.api.createAndRunAnomalyDetectionLookbackJob( + testData.jobConfig, + testData.datafeedConfig + ); + }); - after(async () => { - await elasticChart.setNewChartUiDebugFlag(false); - await ml.api.cleanMlIndices(); - }); + after(async () => { + await elasticChart.setNewChartUiDebugFlag(false); + await ml.api.cleanMlIndices(); + }); - it('opens a job from job list link', async () => { - await ml.testExecution.logTestStep('navigate to job list'); - await ml.navigation.navigateToMl(); - // Set debug state has to happen at this point - // because page refresh happens after navigation to the ML app. - await elasticChart.setNewChartUiDebugFlag(true); - await ml.navigation.navigateToJobManagement(); + it('opens a job from job list link', async () => { + await ml.testExecution.logTestStep('navigate to job list'); + await ml.navigation.navigateToMl(); + // Set debug state has to happen at this point + // because page refresh happens after navigation to the ML app. + await elasticChart.setNewChartUiDebugFlag(true); + await ml.navigation.navigateToJobManagement(); - await ml.testExecution.logTestStep('open job in anomaly explorer'); - await ml.jobTable.filterWithSearchString(testData.jobConfig.job_id, 1); + await ml.testExecution.logTestStep('open job in anomaly explorer'); + await ml.jobTable.filterWithSearchString(testData.jobConfig.job_id, 1); - await ml.jobTable.clickOpenJobInAnomalyExplorerButton(testData.jobConfig.job_id); - await ml.commonUI.waitForMlLoadingIndicatorToDisappear(); - }); + await ml.jobTable.clickOpenJobInAnomalyExplorerButton(testData.jobConfig.job_id); + await ml.commonUI.waitForMlLoadingIndicatorToDisappear(); + }); - it('displays job results', async () => { - await ml.testExecution.logTestStep('pre-fills the job selection'); - await ml.jobSelection.assertJobSelection([testData.jobConfig.job_id]); + it('displays job results', async () => { + await ml.testExecution.logTestStep('pre-fills the job selection'); + await ml.jobSelection.assertJobSelection([testData.jobConfig.job_id]); - await ml.testExecution.logTestStep('displays the influencers list'); - await ml.anomalyExplorer.assertInfluencerListExists(); - for (const influencerBlock of testData.expected.influencers) { - await ml.anomalyExplorer.assertInfluencerFieldExists(influencerBlock.field); - await ml.anomalyExplorer.assertInfluencerFieldListLength( - influencerBlock.field, - influencerBlock.count - ); - for (const influencerLabel of influencerBlock.labelsContained) { - await ml.anomalyExplorer.assertInfluencerListContainsLabel( + await ml.testExecution.logTestStep('displays the influencers list'); + await ml.anomalyExplorer.assertInfluencerListExists(); + for (const influencerBlock of testData.expected.influencers) { + await ml.anomalyExplorer.assertInfluencerFieldExists(influencerBlock.field); + await ml.anomalyExplorer.assertInfluencerFieldListLength( influencerBlock.field, - influencerLabel + influencerBlock.count ); + for (const influencerLabel of influencerBlock.labelsContained) { + await ml.anomalyExplorer.assertInfluencerListContainsLabel( + influencerBlock.field, + influencerLabel + ); + } } - } - - await ml.testExecution.logTestStep('displays the swim lanes'); - await ml.anomalyExplorer.assertOverallSwimlaneExists(); - await ml.anomalyExplorer.assertSwimlaneViewByExists(); - - await ml.testExecution.logTestStep('should display the annotations panel'); - await ml.anomalyExplorer.assertAnnotationsPanelExists('loaded'); - await ml.testExecution.logTestStep('displays the anomalies table'); - await ml.anomaliesTable.assertTableExists(); + await ml.testExecution.logTestStep('displays the swim lanes'); + await ml.anomalyExplorer.assertOverallSwimlaneExists(); + await ml.anomalyExplorer.assertSwimlaneViewByExists(); - await ml.testExecution.logTestStep('anomalies table is not empty'); - await ml.anomaliesTable.assertTableNotEmpty(); - }); + await ml.testExecution.logTestStep('should display the annotations panel'); + await ml.anomalyExplorer.assertAnnotationsPanelExists('loaded'); - it('should allow filtering by influencer', async () => { - const fieldName = testData.expected.influencers[0].field; - const fieldValue = testData.expected.influencers[0].labelsContained[0]; + await ml.testExecution.logTestStep('displays the anomalies table'); + await ml.anomaliesTable.assertTableExists(); - await ml.testExecution.logTestStep( - 'adds influencer filter by clicking on the influencer add filter button' - ); - await ml.anomalyExplorer.addFilterForInfluencer(fieldName, fieldValue); - await ml.testExecution.logTestStep('query bar and table rows reflect filter'); - await ml.anomalyExplorer.assertQueryBarContent(`${fieldName}:"${fieldValue}"`); - await ml.anomaliesTable.assertInfluencersCellsContainFilter( - `${fieldName}: ${fieldValue}` - ); - await ml.testExecution.logTestStep('influencers list and swimlane reflect filter'); - await ml.swimLane.assertAxisLabels(viewBySwimLaneTestSubj, 'y', [fieldValue]); - await ml.anomalyExplorer.assertInfluencerFieldListLength('airline', 1); - await ml.testExecution.logTestStep( - 'removes influencer filter by clicking on the influencer remove filter button' - ); - await ml.anomalyExplorer.removeFilterForInfluencer(fieldName, fieldValue); - await ml.testExecution.logTestStep('query bar reflects filter removal'); - await ml.anomalyExplorer.assertQueryBarContent(''); - await ml.testExecution.logTestStep( - 'influencers list and swimlane reflect filter removal' - ); - await ml.swimLane.assertAxisLabels(viewBySwimLaneTestSubj, 'y', [ - 'AAL', - 'EGF', - 'VRD', - 'SWR', - 'JZA', - 'AMX', - 'TRS', - 'ACA', - 'BAW', - 'ASA', - ]); - await ml.anomalyExplorer.assertInfluencerFieldListLength('airline', 10); - }); + await ml.testExecution.logTestStep('anomalies table is not empty'); + await ml.anomaliesTable.assertTableNotEmpty(); + }); - it('has enabled Single Metric Viewer button', async () => { - await ml.anomalyExplorer.assertSingleMetricViewerButtonEnabled(true); - }); + it('should allow filtering by influencer', async () => { + const fieldName = testData.expected.influencers[0].field; + const fieldValue = testData.expected.influencers[0].labelsContained[0]; - it('renders Overall swim lane', async () => { - await ml.testExecution.logTestStep('has correct axes labels'); - // The showTimeline prop is set to false and no axis labels are rendered - await ml.swimLane.assertAxisLabels(overallSwimLaneTestSubj, 'x', []); - await ml.swimLane.assertAxisLabels(overallSwimLaneTestSubj, 'y', ['Overall']); - }); + await ml.testExecution.logTestStep( + 'adds influencer filter by clicking on the influencer add filter button' + ); + await ml.anomalyExplorer.addFilterForInfluencer(fieldName, fieldValue); + await ml.testExecution.logTestStep('query bar and table rows reflect filter'); + await ml.anomalyExplorer.assertQueryBarContent(`${fieldName}:"${fieldValue}"`); + await ml.anomaliesTable.assertInfluencersCellsContainFilter( + `${fieldName}: ${fieldValue}` + ); + await ml.testExecution.logTestStep('influencers list and swimlane reflect filter'); + await ml.swimLane.assertAxisLabels(viewBySwimLaneTestSubj, 'y', [fieldValue]); + await ml.anomalyExplorer.assertInfluencerFieldListLength('airline', 1); + await ml.testExecution.logTestStep( + 'removes influencer filter by clicking on the influencer remove filter button' + ); + await ml.anomalyExplorer.removeFilterForInfluencer(fieldName, fieldValue); + await ml.testExecution.logTestStep('query bar reflects filter removal'); + await ml.anomalyExplorer.assertQueryBarContent(''); + await ml.testExecution.logTestStep( + 'influencers list and swimlane reflect filter removal' + ); + await ml.swimLane.assertAxisLabels(viewBySwimLaneTestSubj, 'y', [ + 'AAL', + 'EGF', + 'VRD', + 'SWR', + 'JZA', + 'AMX', + 'TRS', + 'ACA', + 'BAW', + 'ASA', + ]); + await ml.anomalyExplorer.assertInfluencerFieldListLength('airline', 10); + }); - it('renders View By swim lane', async () => { - await ml.testExecution.logTestStep('has correct axes labels'); - await ml.swimLane.assertAxisLabels(viewBySwimLaneTestSubj, 'x', [ - '2016-02-07 00:00', - '2016-02-07 20:00', - '2016-02-08 16:00', - '2016-02-09 12:00', - '2016-02-10 08:00', - '2016-02-11 04:00', - ]); - await ml.swimLane.assertAxisLabels(viewBySwimLaneTestSubj, 'y', [ - 'AAL', - 'EGF', - 'VRD', - 'SWR', - 'JZA', - 'AMX', - 'TRS', - 'ACA', - 'BAW', - 'ASA', - ]); - }); + it('has enabled Single Metric Viewer button', async () => { + await ml.anomalyExplorer.assertSingleMetricViewerButtonEnabled(true); + }); - it('supports cell selection by click on Overall swim lane', async () => { - await ml.testExecution.logTestStep('checking page state before the cell selection'); - await ml.anomalyExplorer.assertClearSelectionButtonVisible(false); - await ml.anomaliesTable.assertTableRowsCount(25); - await ml.anomalyExplorer.assertInfluencerFieldListLength('airline', 10); - await ml.anomalyExplorer.assertAnomalyExplorerChartsCount(0); - - await ml.testExecution.logTestStep('clicks on the Overall swim lane cell'); - const sampleCell = (await ml.swimLane.getCells(overallSwimLaneTestSubj))[0]; - await ml.swimLane.selectSingleCell(overallSwimLaneTestSubj, { - x: sampleCell.x + cellSize, - y: sampleCell.y + cellSize, + it('renders Overall swim lane', async () => { + await ml.testExecution.logTestStep('has correct axes labels'); + // The showTimeline prop is set to false and no axis labels are rendered + await ml.swimLane.assertAxisLabels(overallSwimLaneTestSubj, 'x', []); + await ml.swimLane.assertAxisLabels(overallSwimLaneTestSubj, 'y', ['Overall']); }); - await ml.swimLane.waitForSwimLanesToLoad(); - // TODO extend cell data with X and Y values, and cell width - await ml.swimLane.assertSelection(overallSwimLaneTestSubj, { - x: [1454846400000, 1454860800000], - y: ['Overall'], + it('renders View By swim lane', async () => { + await ml.testExecution.logTestStep('has correct axes labels'); + await ml.swimLane.assertAxisLabels(viewBySwimLaneTestSubj, 'x', [ + '2016-02-07 00:00', + '2016-02-07 20:00', + '2016-02-08 16:00', + '2016-02-09 12:00', + '2016-02-10 08:00', + '2016-02-11 04:00', + ]); + await ml.swimLane.assertAxisLabels(viewBySwimLaneTestSubj, 'y', [ + 'AAL', + 'EGF', + 'VRD', + 'SWR', + 'JZA', + 'AMX', + 'TRS', + 'ACA', + 'BAW', + 'ASA', + ]); }); - await ml.anomalyExplorer.assertClearSelectionButtonVisible(true); - await ml.testExecution.logTestStep('updates the View By swim lane'); - await ml.swimLane.assertAxisLabels(viewBySwimLaneTestSubj, 'y', ['EGF', 'DAL']); + it('supports cell selection by click on Overall swim lane', async () => { + await ml.testExecution.logTestStep('checking page state before the cell selection'); + await ml.anomalyExplorer.assertClearSelectionButtonVisible(false); + await ml.anomaliesTable.assertTableRowsCount(25); + await ml.anomalyExplorer.assertInfluencerFieldListLength('airline', 10); + await ml.anomalyExplorer.assertAnomalyExplorerChartsCount(0); - await ml.testExecution.logTestStep('renders anomaly explorer charts'); - await ml.anomalyExplorer.assertAnomalyExplorerChartsCount(5); + await ml.testExecution.logTestStep('clicks on the Overall swim lane cell'); + const sampleCell = (await ml.swimLane.getCells(overallSwimLaneTestSubj))[0]; + await ml.swimLane.selectSingleCell(overallSwimLaneTestSubj, { + x: sampleCell.x + cellSize, + y: sampleCell.y + cellSize, + }); + await ml.swimLane.waitForSwimLanesToLoad(); - await ml.testExecution.logTestStep('updates top influencers list'); - await ml.anomalyExplorer.assertInfluencerFieldListLength('airline', 2); + // TODO extend cell data with X and Y values, and cell width + await ml.swimLane.assertSelection(overallSwimLaneTestSubj, { + x: [1454846400000, 1454860800000], + y: ['Overall'], + }); + await ml.anomalyExplorer.assertClearSelectionButtonVisible(true); - await ml.testExecution.logTestStep('updates anomalies table'); - await ml.anomaliesTable.assertTableRowsCount(4); + await ml.testExecution.logTestStep('updates the View By swim lane'); + await ml.swimLane.assertAxisLabels(viewBySwimLaneTestSubj, 'y', ['EGF', 'DAL']); - await ml.testExecution.logTestStep('updates the URL state'); - await ml.navigation.assertCurrentURLContains( - 'selectedLanes%3A!(Overall)%2CselectedTimes%3A!(1454846400%2C1454860800)%2CselectedType%3Aoverall%2CshowTopFieldValues%3A!t' - ); + await ml.testExecution.logTestStep('renders anomaly explorer charts'); + await ml.anomalyExplorer.assertAnomalyExplorerChartsCount(5); - await ml.testExecution.logTestStep('restores app state from the URL state'); - await browser.refresh(); - await elasticChart.setNewChartUiDebugFlag(true); - await ml.swimLane.waitForSwimLanesToLoad(); - await ml.swimLane.assertSelection(overallSwimLaneTestSubj, { - x: [1454846400000, 1454860800000], - y: ['Overall'], - }); - await ml.swimLane.assertAxisLabels(viewBySwimLaneTestSubj, 'y', ['EGF', 'DAL']); - await ml.anomalyExplorer.assertAnomalyExplorerChartsCount(5); - await ml.anomalyExplorer.assertInfluencerFieldListLength('airline', 2); - await ml.anomaliesTable.assertTableRowsCount(4); + await ml.testExecution.logTestStep('updates top influencers list'); + await ml.anomalyExplorer.assertInfluencerFieldListLength('airline', 2); - await ml.testExecution.logTestStep('clears the selection'); - await ml.anomalyExplorer.clearSwimLaneSelection(); - await ml.swimLane.waitForSwimLanesToLoad(); + await ml.testExecution.logTestStep('updates anomalies table'); + await ml.anomaliesTable.assertTableRowsCount(4); - await ml.navigation.assertCurrentURLNotContain( - 'selectedLanes%3A!(Overall)%2CselectedTimes%3A!(1454846400%2C1454860800)%2CselectedType%3Aoverall%2CshowTopFieldValues%3A!t%2CviewByFieldName%3Aairline%2CviewByFromPage%3A1%2CviewByPerPage%3A10' - ); - await ml.anomaliesTable.assertTableRowsCount(25); - await ml.anomalyExplorer.assertInfluencerFieldListLength('airline', 10); - await ml.anomalyExplorer.assertAnomalyExplorerChartsCount(0); - }); + await ml.testExecution.logTestStep('updates the URL state'); + await ml.navigation.assertCurrentURLContains( + 'selectedLanes%3A!(Overall)%2CselectedTimes%3A!(1454846400%2C1454860800)%2CselectedType%3Aoverall%2CshowTopFieldValues%3A!t' + ); - it('allows to change the swim lane pagination', async () => { - await ml.testExecution.logTestStep('checks default pagination'); - await ml.swimLane.assertPageSize(viewBySwimLaneTestSubj, 10); - await ml.swimLane.assertActivePage(viewBySwimLaneTestSubj, 1); + await ml.testExecution.logTestStep('restores app state from the URL state'); + await browser.refresh(); + await elasticChart.setNewChartUiDebugFlag(true); + await ml.swimLane.waitForSwimLanesToLoad(); + await ml.swimLane.assertSelection(overallSwimLaneTestSubj, { + x: [1454846400000, 1454860800000], + y: ['Overall'], + }); + await ml.swimLane.assertAxisLabels(viewBySwimLaneTestSubj, 'y', ['EGF', 'DAL']); + await ml.anomalyExplorer.assertAnomalyExplorerChartsCount(5); + await ml.anomalyExplorer.assertInfluencerFieldListLength('airline', 2); + await ml.anomaliesTable.assertTableRowsCount(4); - await ml.testExecution.logTestStep('updates pagination'); - await ml.swimLane.setPageSize(viewBySwimLaneTestSubj, 5); + await ml.testExecution.logTestStep('clears the selection'); + await ml.anomalyExplorer.clearSwimLaneSelection(); + await ml.swimLane.waitForSwimLanesToLoad(); - await ml.swimLane.assertAxisLabelCount(viewBySwimLaneTestSubj, 'y', 5); + await ml.navigation.assertCurrentURLNotContain( + 'selectedLanes%3A!(Overall)%2CselectedTimes%3A!(1454846400%2C1454860800)%2CselectedType%3Aoverall%2CshowTopFieldValues%3A!t%2CviewByFieldName%3Aairline%2CviewByFromPage%3A1%2CviewByPerPage%3A10' + ); + await ml.anomaliesTable.assertTableRowsCount(25); + await ml.anomalyExplorer.assertInfluencerFieldListLength('airline', 10); + await ml.anomalyExplorer.assertAnomalyExplorerChartsCount(0); + }); - await ml.swimLane.selectPage(viewBySwimLaneTestSubj, 3); + it('allows to change the swim lane pagination', async () => { + await ml.testExecution.logTestStep('checks default pagination'); + await ml.swimLane.assertPageSize(viewBySwimLaneTestSubj, 10); + await ml.swimLane.assertActivePage(viewBySwimLaneTestSubj, 1); - await ml.testExecution.logTestStep('resets pagination'); - await ml.swimLane.setPageSize(viewBySwimLaneTestSubj, 10); - await ml.swimLane.assertActivePage(viewBySwimLaneTestSubj, 1); - }); + await ml.testExecution.logTestStep('updates pagination'); + await ml.swimLane.setPageSize(viewBySwimLaneTestSubj, 5); - it('supports cell selection by click on View By swim lane', async () => { - await ml.testExecution.logTestStep('checking page state before the cell selection'); - await ml.anomalyExplorer.assertClearSelectionButtonVisible(false); - await ml.anomaliesTable.assertTableRowsCount(25); - await ml.anomalyExplorer.assertInfluencerFieldListLength('airline', 10); - await ml.anomalyExplorer.assertAnomalyExplorerChartsCount(0); - - await ml.testExecution.logTestStep('clicks on the View By swim lane cell'); - await ml.anomalyExplorer.assertSwimlaneViewByExists(); - const sampleCell = (await ml.swimLane.getCells(viewBySwimLaneTestSubj))[0]; - await ml.swimLane.selectSingleCell(viewBySwimLaneTestSubj, { - x: sampleCell.x + cellSize, - y: sampleCell.y + cellSize, - }); - await ml.swimLane.waitForSwimLanesToLoad(); + await ml.swimLane.assertAxisLabelCount(viewBySwimLaneTestSubj, 'y', 5); + + await ml.swimLane.selectPage(viewBySwimLaneTestSubj, 3); - await ml.testExecution.logTestStep('check page content'); - await ml.swimLane.assertSelection(viewBySwimLaneTestSubj, { - x: [1454817600000, 1454832000000], - y: ['AAL'], + await ml.testExecution.logTestStep('resets pagination'); + await ml.swimLane.setPageSize(viewBySwimLaneTestSubj, 10); + await ml.swimLane.assertActivePage(viewBySwimLaneTestSubj, 1); }); - await ml.anomaliesTable.assertTableRowsCount(1); - await ml.anomalyExplorer.assertInfluencerFieldListLength('airline', 1); - await ml.anomalyExplorer.assertAnomalyExplorerChartsCount(1); + it('supports cell selection by click on View By swim lane', async () => { + await ml.testExecution.logTestStep('checking page state before the cell selection'); + await ml.anomalyExplorer.assertClearSelectionButtonVisible(false); + await ml.anomaliesTable.assertTableRowsCount(25); + await ml.anomalyExplorer.assertInfluencerFieldListLength('airline', 10); + await ml.anomalyExplorer.assertAnomalyExplorerChartsCount(0); + + await ml.testExecution.logTestStep('clicks on the View By swim lane cell'); + await ml.anomalyExplorer.assertSwimlaneViewByExists(); + const sampleCell = (await ml.swimLane.getCells(viewBySwimLaneTestSubj))[0]; + await ml.swimLane.selectSingleCell(viewBySwimLaneTestSubj, { + x: sampleCell.x + cellSize, + y: sampleCell.y + cellSize, + }); + await ml.swimLane.waitForSwimLanesToLoad(); + + await ml.testExecution.logTestStep('check page content'); + await ml.swimLane.assertSelection(viewBySwimLaneTestSubj, { + x: [1454817600000, 1454832000000], + y: ['AAL'], + }); - await ml.testExecution.logTestStep('highlights the Overall swim lane'); - await ml.swimLane.assertSelection(overallSwimLaneTestSubj, { - x: [1454817600000, 1454832000000], - y: ['Overall'], - }); + await ml.anomaliesTable.assertTableRowsCount(1); + await ml.anomalyExplorer.assertInfluencerFieldListLength('airline', 1); + await ml.anomalyExplorer.assertAnomalyExplorerChartsCount(1); - await ml.testExecution.logTestStep('restores app state from the URL state'); - await browser.refresh(); - await elasticChart.setNewChartUiDebugFlag(true); - await ml.swimLane.waitForSwimLanesToLoad(); - await ml.swimLane.assertSelection(viewBySwimLaneTestSubj, { - x: [1454817600000, 1454832000000], - y: ['AAL'], - }); - await ml.anomaliesTable.assertTableRowsCount(1); - await ml.anomalyExplorer.assertInfluencerFieldListLength('airline', 1); - await ml.anomalyExplorer.assertAnomalyExplorerChartsCount(1); - await ml.swimLane.assertSelection(overallSwimLaneTestSubj, { - x: [1454817600000, 1454832000000], - y: ['Overall'], - }); + await ml.testExecution.logTestStep('highlights the Overall swim lane'); + await ml.swimLane.assertSelection(overallSwimLaneTestSubj, { + x: [1454817600000, 1454832000000], + y: ['Overall'], + }); - await ml.testExecution.logTestStep('clears the selection'); - await ml.anomalyExplorer.clearSwimLaneSelection(); - await ml.swimLane.waitForSwimLanesToLoad(); + await ml.testExecution.logTestStep('restores app state from the URL state'); + await browser.refresh(); + await elasticChart.setNewChartUiDebugFlag(true); + await ml.swimLane.waitForSwimLanesToLoad(); + await ml.swimLane.assertSelection(viewBySwimLaneTestSubj, { + x: [1454817600000, 1454832000000], + y: ['AAL'], + }); + await ml.anomaliesTable.assertTableRowsCount(1); + await ml.anomalyExplorer.assertInfluencerFieldListLength('airline', 1); + await ml.anomalyExplorer.assertAnomalyExplorerChartsCount(1); + await ml.swimLane.assertSelection(overallSwimLaneTestSubj, { + x: [1454817600000, 1454832000000], + y: ['Overall'], + }); - await ml.anomaliesTable.assertTableRowsCount(25); - await ml.anomalyExplorer.assertInfluencerFieldListLength('airline', 10); - await ml.anomalyExplorer.assertAnomalyExplorerChartsCount(0); - }); + await ml.testExecution.logTestStep('clears the selection'); + await ml.anomalyExplorer.clearSwimLaneSelection(); + await ml.swimLane.waitForSwimLanesToLoad(); - it('supports cell selection by brush action', async () => { - await ml.testExecution.logTestStep('checking page state before the cell selection'); - await ml.anomalyExplorer.assertClearSelectionButtonVisible(false); - await ml.anomaliesTable.assertTableRowsCount(25); - await ml.anomalyExplorer.assertInfluencerFieldListLength('airline', 10); - await ml.anomalyExplorer.assertAnomalyExplorerChartsCount(0); - - await ml.anomalyExplorer.assertSwimlaneViewByExists(); - const cells = await ml.swimLane.getCells(viewBySwimLaneTestSubj); - - const sampleCell1 = cells[0]; - // Get cell from another row - const sampleCell2 = cells.find((c) => c.y !== sampleCell1.y); - - await ml.swimLane.selectCells(viewBySwimLaneTestSubj, { - x1: sampleCell1.x + cellSize, - y1: sampleCell1.y + cellSize, - x2: sampleCell2!.x + cellSize, - y2: sampleCell2!.y + cellSize, + await ml.anomaliesTable.assertTableRowsCount(25); + await ml.anomalyExplorer.assertInfluencerFieldListLength('airline', 10); + await ml.anomalyExplorer.assertAnomalyExplorerChartsCount(0); }); - await ml.swimLane.waitForSwimLanesToLoad(); - await ml.swimLane.assertSelection(viewBySwimLaneTestSubj, { - x: [1454817600000, 1454860800000], - y: ['AAL', 'EGF'], - }); + it('supports cell selection by brush action', async () => { + await ml.testExecution.logTestStep('checking page state before the cell selection'); + await ml.anomalyExplorer.assertClearSelectionButtonVisible(false); + await ml.anomaliesTable.assertTableRowsCount(25); + await ml.anomalyExplorer.assertInfluencerFieldListLength('airline', 10); + await ml.anomalyExplorer.assertAnomalyExplorerChartsCount(0); + + await ml.anomalyExplorer.assertSwimlaneViewByExists(); + const cells = await ml.swimLane.getCells(viewBySwimLaneTestSubj); + + const sampleCell1 = cells[0]; + // Get cell from another row + const sampleCell2 = cells.find((c) => c.y !== sampleCell1.y); + + await ml.swimLane.selectCells(viewBySwimLaneTestSubj, { + x1: sampleCell1.x + cellSize, + y1: sampleCell1.y + cellSize, + x2: sampleCell2!.x + cellSize, + y2: sampleCell2!.y + cellSize, + }); + await ml.swimLane.waitForSwimLanesToLoad(); - await ml.anomaliesTable.assertTableRowsCount(3); - await ml.anomalyExplorer.assertInfluencerFieldListLength('airline', 2); - await ml.anomalyExplorer.assertAnomalyExplorerChartsCount(3); + await ml.swimLane.assertSelection(viewBySwimLaneTestSubj, { + x: [1454817600000, 1454860800000], + y: ['AAL', 'EGF'], + }); - await ml.testExecution.logTestStep('clears the selection'); - await ml.anomalyExplorer.clearSwimLaneSelection(); - await ml.swimLane.waitForSwimLanesToLoad(); + await ml.anomaliesTable.assertTableRowsCount(3); + await ml.anomalyExplorer.assertInfluencerFieldListLength('airline', 2); + await ml.anomalyExplorer.assertAnomalyExplorerChartsCount(3); - await ml.anomaliesTable.assertTableRowsCount(25); - await ml.anomalyExplorer.assertInfluencerFieldListLength('airline', 10); - await ml.anomalyExplorer.assertAnomalyExplorerChartsCount(0); - }); + await ml.testExecution.logTestStep('clears the selection'); + await ml.anomalyExplorer.clearSwimLaneSelection(); + await ml.swimLane.waitForSwimLanesToLoad(); - it('allows to change the anomalies table pagination', async () => { - await ml.testExecution.logTestStep('displays the anomalies table with default config'); - await ml.anomaliesTable.assertTableExists(); - await ml.anomaliesTable.assertRowsNumberPerPage(25); - await ml.anomaliesTable.assertTableRowsCount(25); + await ml.anomaliesTable.assertTableRowsCount(25); + await ml.anomalyExplorer.assertInfluencerFieldListLength('airline', 10); + await ml.anomalyExplorer.assertAnomalyExplorerChartsCount(0); + }); - await ml.testExecution.logTestStep('updates table pagination'); - await ml.anomaliesTable.setRowsNumberPerPage(10); - await ml.anomaliesTable.assertTableRowsCount(10); - }); + it('allows to change the anomalies table pagination', async () => { + await ml.testExecution.logTestStep('displays the anomalies table with default config'); + await ml.anomaliesTable.assertTableExists(); + await ml.anomaliesTable.assertRowsNumberPerPage(25); + await ml.anomaliesTable.assertTableRowsCount(25); - it('renders swim lanes correctly on the time bounds change', async () => { - const fromTime = 'Jul 7, 2012 @ 00:00:00.000'; - const toTime = 'Feb 12, 2016 @ 23:59:54.000'; - - await PageObjects.timePicker.pauseAutoRefresh(); - await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); - - await ml.commonUI.waitForDatePickerIndicatorLoaded(); - - await ml.swimLane.waitForSwimLanesToLoad(); - await ml.swimLane.assertAxisLabels(viewBySwimLaneTestSubj, 'x', [ - '2012-06-19', - '2012-11-16', - '2013-04-15', - '2013-09-12', - '2014-02-09', - '2014-07-09', - '2014-12-06', - '2015-05-05', - '2015-10-02', - ]); - }); + await ml.testExecution.logTestStep('updates table pagination'); + await ml.anomaliesTable.setRowsNumberPerPage(10); + await ml.anomaliesTable.assertTableRowsCount(10); + }); + + it('renders swim lanes correctly on the time bounds change', async () => { + const fromTime = 'Jul 7, 2012 @ 00:00:00.000'; + const toTime = 'Feb 12, 2016 @ 23:59:54.000'; + + await PageObjects.timePicker.pauseAutoRefresh(); + await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); - describe('Anomaly Swim Lane as embeddable', function () { - beforeEach(async () => { - await ml.navigation.navigateToAnomalyExplorer(testData.jobConfig.job_id, { - from: '2016-02-07T00%3A00%3A00.000Z', - to: '2016-02-11T23%3A59%3A54.000Z', - }); - await ml.commonUI.waitForMlLoadingIndicatorToDisappear(); await ml.commonUI.waitForDatePickerIndicatorLoaded(); + + await ml.swimLane.waitForSwimLanesToLoad(); + await ml.swimLane.assertAxisLabels(viewBySwimLaneTestSubj, 'x', [ + '2012-06-19', + '2012-11-16', + '2013-04-15', + '2013-09-12', + '2014-02-09', + '2014-07-09', + '2014-12-06', + '2015-05-05', + '2015-10-02', + ]); }); - it('attaches swim lane embeddable to a case', async () => { - await ml.anomalyExplorer.attachSwimLaneToCase('viewBy', { - title: 'ML Test case', - description: 'Case with an anomaly swim lane', - tag: 'ml_swim_lane_case', + describe('Anomaly Swim Lane as embeddable', function () { + beforeEach(async () => { + await ml.navigation.navigateToAnomalyExplorer(testData.jobConfig.job_id, { + from: '2016-02-07T00%3A00%3A00.000Z', + to: '2016-02-11T23%3A59%3A54.000Z', + }); + await ml.commonUI.waitForMlLoadingIndicatorToDisappear(); + await ml.commonUI.waitForDatePickerIndicatorLoaded(); }); - const expectedAttachment = { - swimlaneType: 'viewBy', - viewBy: 'airline', - jobIds: [testData.jobConfig.job_id], - timeRange: { - from: '2016-02-07T00:00:00.000Z', - to: '2016-02-11T23:59:54.000Z', - }, - } as AnomalySwimLaneEmbeddableState; - - expectedAttachment.id = stringHash(JSON.stringify(expectedAttachment)).toString(); - - await ml.cases.assertCaseWithAnomalySwimLaneAttachment( - { + it('attaches swim lane embeddable to a case', async () => { + await ml.anomalyExplorer.attachSwimLaneToCase('viewBy', { title: 'ML Test case', description: 'Case with an anomaly swim lane', tag: 'ml_swim_lane_case', - reporter: USER.ML_POWERUSER, - }, - expectedAttachment, - { - yAxisLabelCount: 10, - } - ); - }); + }); + + const expectedAttachment = { + swimlaneType: 'viewBy', + viewBy: 'airline', + jobIds: [testData.jobConfig.job_id], + timeRange: { + from: '2016-02-07T00:00:00.000Z', + to: '2016-02-11T23:59:54.000Z', + }, + } as AnomalySwimLaneEmbeddableState; + + expectedAttachment.id = stringHash(JSON.stringify(expectedAttachment)).toString(); + + await ml.cases.assertCaseWithAnomalySwimLaneAttachment( + { + title: 'ML Test case', + description: 'Case with an anomaly swim lane', + tag: 'ml_swim_lane_case', + reporter: USER.ML_POWERUSER, + }, + expectedAttachment, + { + yAxisLabelCount: 10, + } + ); + }); - it('adds swim lane embeddable to a dashboard', async () => { - await ml.testExecution.logTestStep( - 'should allow to attach anomaly swim lane embeddable to the dashboard' - ); - await ml.anomalyExplorer.openAddToDashboardControl(); - await ml.anomalyExplorer.addAndEditSwimlaneInDashboard('ML Test'); + it('adds swim lane embeddable to a dashboard', async () => { + await ml.testExecution.logTestStep( + 'should allow to attach anomaly swim lane embeddable to the dashboard' + ); + await ml.anomalyExplorer.openAddToDashboardControl(); + await ml.anomalyExplorer.addAndEditSwimlaneInDashboard('ML Test'); + }); }); - }); - describe('Anomaly Charts as embeddable', function () { - beforeEach(async () => { - await ml.navigation.navigateToAnomalyExplorer( - testData.jobConfig.job_id, - { - from: '2016-02-07T00%3A00%3A00.000Z', - to: '2016-02-11T23%3A59%3A54.000Z', - }, - () => elasticChart.setNewChartUiDebugFlag(true) - ); + describe('Anomaly Charts as embeddable', function () { + beforeEach(async () => { + await ml.navigation.navigateToAnomalyExplorer( + testData.jobConfig.job_id, + { + from: '2016-02-07T00%3A00%3A00.000Z', + to: '2016-02-11T23%3A59%3A54.000Z', + }, + () => elasticChart.setNewChartUiDebugFlag(true) + ); - await ml.commonUI.waitForMlLoadingIndicatorToDisappear(); - await ml.commonUI.waitForDatePickerIndicatorLoaded(); + await ml.commonUI.waitForMlLoadingIndicatorToDisappear(); + await ml.commonUI.waitForDatePickerIndicatorLoaded(); - await ml.testExecution.logTestStep('clicks on the Overall swim lane cell'); - const sampleCell = (await ml.swimLane.getCells(overallSwimLaneTestSubj))[0]; - await ml.swimLane.selectSingleCell(overallSwimLaneTestSubj, { - x: sampleCell.x + cellSize, - y: sampleCell.y + cellSize, + await ml.testExecution.logTestStep('clicks on the Overall swim lane cell'); + const sampleCell = (await ml.swimLane.getCells(overallSwimLaneTestSubj))[0]; + await ml.swimLane.selectSingleCell(overallSwimLaneTestSubj, { + x: sampleCell.x + cellSize, + y: sampleCell.y + cellSize, + }); + await ml.swimLane.waitForSwimLanesToLoad(); + }); + + it('attaches an embeddable to a case', async () => { + await ml.anomalyExplorer.attachAnomalyChartsToCase({ + title: 'ML Charts Test case', + description: 'Case with an anomaly charts attachment', + tag: 'ml_anomaly_charts', + }); + + const expectedAttachment = { + jobIds: [testData.jobConfig.job_id], + maxSeriesToPlot: 6, + }; + + // @ts-expect-error Setting id to be undefined here + // since time range expected is of the chart plotEarliest/plotLatest, not of the global time range + // but, chart time range might vary depends on the time of the test + // we don't know the hashed string id for sure + expectedAttachment.id = undefined; + + await ml.cases.assertCaseWithAnomalyChartsAttachment( + { + title: 'ML Charts Test case', + description: 'Case with an anomaly charts attachment', + tag: 'ml_anomaly_charts', + reporter: USER.ML_POWERUSER, + }, + expectedAttachment, + 6 + ); }); - await ml.swimLane.waitForSwimLanesToLoad(); }); - it('attaches an embeddable to a case', async () => { - await ml.anomalyExplorer.attachAnomalyChartsToCase({ - title: 'ML Charts Test case', - description: 'Case with an anomaly charts attachment', - tag: 'ml_anomaly_charts', + describe('Use anomaly table action to view in Discover', function () { + beforeEach(async () => { + await ml.navigation.navigateToAnomalyExplorer( + testData.jobConfig.job_id, + { + from: '2016-02-07T00%3A00%3A00.000Z', + to: '2016-02-11T23%3A59%3A54.000Z', + }, + () => elasticChart.setNewChartUiDebugFlag(true) + ); + + await ml.commonUI.waitForMlLoadingIndicatorToDisappear(); + await ml.commonUI.waitForDatePickerIndicatorLoaded(); + await ml.swimLane.waitForSwimLanesToLoad(); }); - const expectedAttachment = { - jobIds: [testData.jobConfig.job_id], - maxSeriesToPlot: 6, - }; + it('should render the anomaly table', async () => { + await ml.testExecution.logTestStep('displays the anomalies table'); + await ml.anomaliesTable.assertTableExists(); - // @ts-expect-error Setting id to be undefined here - // since time range expected is of the chart plotEarliest/plotLatest, not of the global time range - // but, chart time range might vary depends on the time of the test - // we don't know the hashed string id for sure - expectedAttachment.id = undefined; + await ml.testExecution.logTestStep('anomalies table is not empty'); + await ml.anomaliesTable.assertTableNotEmpty(); + }); - await ml.cases.assertCaseWithAnomalyChartsAttachment( - { - title: 'ML Charts Test case', - description: 'Case with an anomaly charts attachment', - tag: 'ml_anomaly_charts', - reporter: USER.ML_POWERUSER, - }, - expectedAttachment, - 6 - ); + it('should click the Discover action in the anomaly table', async () => { + await ml.anomaliesTable.assertAnomalyActionsMenuButtonExists(0); + await ml.anomaliesTable.scrollRowIntoView(0); + await ml.anomaliesTable.assertAnomalyActionsMenuButtonEnabled(0, true); + await ml.anomaliesTable.assertAnomalyActionDiscoverButtonExists(0); + await ml.anomaliesTable.ensureAnomalyActionDiscoverButtonClicked(0); + }); }); }); + } + }); + describe('with no influencers', function () { + for (const testData of testDataListWithNoInfluencers) { + before(async () => { + await ml.api.createAndRunAnomalyDetectionLookbackJob( + testData.jobConfig, + testData.datafeedConfig + ); + }); - describe('Use anomaly table action to view in Discover', function () { - beforeEach(async () => { - await ml.navigation.navigateToAnomalyExplorer( - testData.jobConfig.job_id, - { - from: '2016-02-07T00%3A00%3A00.000Z', - to: '2016-02-11T23%3A59%3A54.000Z', - }, - () => elasticChart.setNewChartUiDebugFlag(true) - ); + after(async () => { + await elasticChart.setNewChartUiDebugFlag(false); + await ml.api.cleanMlIndices(); + }); - await ml.commonUI.waitForMlLoadingIndicatorToDisappear(); - await ml.commonUI.waitForDatePickerIndicatorLoaded(); - await ml.swimLane.waitForSwimLanesToLoad(); - }); + it('should not display the influencers panel', async () => { + await ml.testExecution.logTestStep('navigate to job list'); + await ml.navigation.navigateToMl(); + // Set debug state has to happen at this point + // because page refresh happens after navigation to the ML app. + await elasticChart.setNewChartUiDebugFlag(true); + await ml.navigation.navigateToJobManagement(); - it('should render the anomaly table', async () => { - await ml.testExecution.logTestStep('displays the anomalies table'); - await ml.anomaliesTable.assertTableExists(); + await ml.testExecution.logTestStep('open job in anomaly explorer'); + await ml.jobTable.filterWithSearchString(testData.jobConfig.job_id, 1); - await ml.testExecution.logTestStep('anomalies table is not empty'); - await ml.anomaliesTable.assertTableNotEmpty(); - }); + await ml.jobTable.clickOpenJobInAnomalyExplorerButton(testData.jobConfig.job_id); + await ml.commonUI.waitForMlLoadingIndicatorToDisappear(); + await ml.jobSelection.assertJobSelection([testData.jobConfig.job_id]); - it('should click the Discover action in the anomaly table', async () => { - await ml.anomaliesTable.assertAnomalyActionsMenuButtonExists(0); - await ml.anomaliesTable.scrollRowIntoView(0); - await ml.anomaliesTable.assertAnomalyActionsMenuButtonEnabled(0, true); - await ml.anomaliesTable.assertAnomalyActionDiscoverButtonExists(0); - await ml.anomaliesTable.ensureAnomalyActionDiscoverButtonClicked(0); - }); + await ml.testExecution.logTestStep('does not display the influencers list'); + await ml.anomalyExplorer.assertInfluencerListDoesNotExist(); }); - }); - } + } + }); }); } diff --git a/x-pack/test/functional/services/ml/anomaly_explorer.ts b/x-pack/test/functional/services/ml/anomaly_explorer.ts index cb183eee0c543..6774bd097f001 100644 --- a/x-pack/test/functional/services/ml/anomaly_explorer.ts +++ b/x-pack/test/functional/services/ml/anomaly_explorer.ts @@ -30,6 +30,10 @@ export function MachineLearningAnomalyExplorerProvider( await testSubjects.existOrFail('mlAnomalyExplorerInfluencerList'); }, + async assertInfluencerListDoesNotExist() { + await testSubjects.missingOrFail('mlAnomalyExplorerInfluencerList'); + }, + async assertInfluencerFieldExists(influencerField: string) { await testSubjects.existOrFail(`mlInfluencerFieldName ${influencerField}`); },