Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -726,7 +726,12 @@ class RuleEditorFlyoutUI extends Component {
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton onClick={this.saveEdit} isDisabled={!isValidRule(rule)} fill>
<EuiButton
onClick={this.saveEdit}
isDisabled={!isValidRule(rule)}
fill
data-test-subj="mlRuleEditorSaveButton"
>
<FormattedMessage
id="xpack.ml.ruleEditor.ruleEditorFlyout.saveButtonLabel"
defaultMessage="Save"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ export class ScopeExpression extends Component {
<EuiSelect
value={filterType}
onChange={this.onChangeFilterType}
data-test-subj="mlScopeFilterTypeSelect"
options={[
{
value: ML_DETECTOR_RULE_FILTER_TYPE.INCLUDE,
Expand All @@ -104,6 +105,7 @@ export class ScopeExpression extends Component {
<EuiSelect
value={filterId}
onChange={this.onChangeFilterId}
data-test-subj="mlScopeFilterIdSelect"
options={getFilterListOptions(filterListIds)}
/>
</EuiFlexItem>
Expand Down Expand Up @@ -158,6 +160,7 @@ export class ScopeExpression extends Component {
value={filterId || ''}
isActive={this.state.isFilterListOpen}
onClick={this.openFilterList}
data-test-subj="mlScopeExpressionFilterSelector"
/>
}
isOpen={this.state.isFilterListOpen}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { getScopeFieldDefaults } from './utils';
import { FormattedMessage } from '@kbn/i18n-react';
import { ML_PAGES } from '../../../../common/constants/locator';
import { MANAGEMENT_SECTION_IDS } from '../../management';
import { useCreateAndNavigateToManagementMlLink } from '../../contexts/kibana';
import { useCreateAndNavigateToManagementMlLink } from '../../contexts/kibana/use_create_url';

function NoFilterListsCallOut() {
const redirectToFilterManagementPage = useCreateAndNavigateToManagementMlLink(
Expand All @@ -45,7 +45,10 @@ function NoFilterListsCallOut() {
to create the list of values you want to include or exclude in the job rule."
values={{
filterListsLink: (
<EuiLink onClick={redirectToFilterManagementPage}>
<EuiLink
onClick={redirectToFilterManagementPage}
data-test-subj="mlScopeNoFilterListsLink"
>
<FormattedMessage
id="xpack.ml.ruleEditor.scopeSection.createFilterListsDescription.filterListsLinkText"
defaultMessage="Filter Lists"
Expand Down Expand Up @@ -128,6 +131,7 @@ export function ScopeSection({
<EuiSpacer size="s" />
<EuiCheckbox
id="enable_scope_checkbox"
data-test-subj="mlScopeEnableCheckbox"
label={
<FormattedMessage
id="xpack.ml.ruleEditor.scopeSection.addFilterListLabel"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jest.mock('../../capabilities/check_capabilities', () => ({
checkPermission: (privilege) => mockCheckPermission(privilege),
}));

jest.mock('../../contexts/kibana', () => ({
jest.mock('../../contexts/kibana/use_create_url', () => ({
useCreateAndNavigateToManagementMlLink: jest.fn().mockReturnValue(jest.fn()),
}));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) {
loadTestFile(require.resolve('./anomaly_explorer'));
loadTestFile(require.resolve('./forecasts'));
loadTestFile(require.resolve('./single_metric_viewer'));
loadTestFile(require.resolve('./rule_editor_flyout'));
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import type { Job, Datafeed } from '@kbn/ml-plugin/common/types/anomaly_detection_jobs';
import type { FtrProviderContext } from '../../../ftr_provider_context';

// Test data: advanced job with partitioning fields so the Scope section is relevant.
// @ts-expect-error not full interface
const JOB_CONFIG: Job = {
job_id: 'ecom_rule_editor_flyout',
description:
'mean(taxless_total_price) over "geoip.city_name" partitionfield=day_of_week on ecommerce dataset with 15m bucket span',
groups: ['ecommerce', 'automated', 'advanced'],
analysis_config: {
bucket_span: '15m',
detectors: [
{
detector_description:
'mean(taxless_total_price) over "geoip.city_name" partitionfield=day_of_week',
function: 'mean',
field_name: 'taxless_total_price',
over_field_name: 'geoip.city_name',
partition_field_name: 'day_of_week',
},
],
influencers: ['day_of_week'],
},
data_description: {
time_field: 'order_date',
time_format: 'epoch_ms',
},
analysis_limits: {
model_memory_limit: '11mb',
categorization_examples_limit: 4,
},
model_plot_config: { enabled: true },
};

// @ts-expect-error not full interface
const DATAFEED_CONFIG: Datafeed = {
datafeed_id: 'datafeed-ecom_rule_editor_flyout',
indices: ['ft_ecommerce'],
job_id: 'ecom_rule_editor_flyout',
query: { bool: { must: [{ match_all: {} }] } },
};

export default function ({ getService }: FtrProviderContext) {
const esArchiver = getService('esArchiver');
const ml = getService('ml');

describe('rule editor flyout', function () {
this.tags(['ml']);

before(async () => {
await esArchiver.loadIfNeeded('x-pack/platform/test/fixtures/es_archives/ml/ecommerce');
await ml.testResources.createDataViewIfNeeded('ft_ecommerce', 'order_date');
await ml.testResources.setKibanaTimeZoneToUTC();
await ml.api.createAndRunAnomalyDetectionLookbackJob(JOB_CONFIG, DATAFEED_CONFIG);
await ml.securityUI.loginAsMlPowerUser();
});

after(async () => {
await ml.api.cleanMlIndices();
await ml.testResources.deleteDataViewByTitle('ft_ecommerce');
});

it('opens job in single metric viewer and selects entities', async () => {
await ml.testExecution.logTestStep('navigate to job list');
await ml.navigation.navigateToStackManagementMlSection('anomaly_detection', 'ml-jobs-list');

await ml.testExecution.logTestStep('open job in single metric viewer');
await ml.jobTable.filterWithSearchString(JOB_CONFIG.job_id, 1);
await ml.jobTable.clickOpenJobInSingleMetricViewerButton(JOB_CONFIG.job_id);
await ml.commonUI.waitForMlLoadingIndicatorToDisappear();

await ml.testExecution.logTestStep('select entity values to load results');
await ml.singleMetricViewer.assertDetectorInputExist();
await ml.singleMetricViewer.assertDetectorInputValue('0');
await ml.singleMetricViewer.assertEntityInputExist('day_of_week');
await ml.singleMetricViewer.selectEntityValue('day_of_week', 'Friday');
await ml.singleMetricViewer.assertEntityInputExist('geoip.city_name');
await ml.singleMetricViewer.selectEntityValue('geoip.city_name', 'Abu Dhabi');

await ml.testExecution.logTestStep('results are visible');
await ml.singleMetricViewer.assertChartExist();
await ml.anomaliesTable.assertTableExists();
await ml.anomaliesTable.assertTableNotEmpty();
});

it('opens rule editor flyout and navigates to Filter Lists via Scope callout', async () => {
await ml.testExecution.logTestStep('open anomalies actions menu and configure rules');
await ml.anomaliesTable.assertAnomalyActionsMenuButtonExists(0);
await ml.anomaliesTable.ensureAnomalyActionsMenuOpen(0);
await ml.anomaliesTable.assertAnomalyActionConfigureRulesButtonExists(0);
await ml.anomaliesTable.clickConfigureRulesButton(0);

await ml.testExecution.logTestStep(
'enable Scope section to show callout when no filter lists exist'
);
await ml.ruleEditorFlyout.enableScope();

await ml.testExecution.logTestStep('click Filter Lists link and verify navigation');
await ml.ruleEditorFlyout.navigateToFilterListsFromCallout();
});

it('shows scope controls when filter lists exist and allows save', async () => {
const filterId = 'ecom_rule_editor_test_filter';
await ml.testExecution.logTestStep('ensure at least one filter list exists');
await ml.api.createFilter(filterId, {
description: 'Functional test filter list for rule editor flyout',
items: ['Friday', 'Abu Dhabi'],
});

await ml.testExecution.logTestStep('re-open job in single metric viewer');
await ml.navigation.navigateToStackManagementMlSection('anomaly_detection', 'ml-jobs-list');
await ml.jobTable.filterWithSearchString(JOB_CONFIG.job_id, 1);
await ml.jobTable.clickOpenJobInSingleMetricViewerButton(JOB_CONFIG.job_id);
await ml.commonUI.waitForMlLoadingIndicatorToDisappear();
await ml.singleMetricViewer.assertDetectorInputExist();
await ml.singleMetricViewer.assertDetectorInputValue('0');
await ml.singleMetricViewer.selectEntityValue('day_of_week', 'Friday');
await ml.singleMetricViewer.selectEntityValue('geoip.city_name', 'Abu Dhabi');

await ml.testExecution.logTestStep('open flyout via actions and enable Scope');
await ml.anomaliesTable.ensureAnomalyActionsMenuOpen(0);
await ml.anomaliesTable.assertAnomalyActionConfigureRulesButtonExists(0);
await ml.anomaliesTable.clickConfigureRulesButton(0);
await ml.ruleEditorFlyout.enableScope();

await ml.testExecution.logTestStep('open scope filter selector popover');
await ml.ruleEditorFlyout.openScopeFilterSelector();

await ml.testExecution.logTestStep('save rule');
await ml.ruleEditorFlyout.save();
await ml.ruleEditorFlyout.closeIfOpen();

await ml.api.deleteFilter(filterId);
});
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,12 @@ export function MachineLearningAnomaliesTableProvider({ getService }: FtrProvide
);
},

async clickConfigureRulesButton(rowIndex: number) {
await this.ensureAnomalyActionsMenuOpen(rowIndex);
await testSubjects.click('mlAnomaliesListRowActionConfigureRulesButton');
await testSubjects.existOrFail('mlRuleEditorFlyout');
},

async assertAnomalyActionViewSeriesButtonEnabled(rowIndex: number, expectedValue: boolean) {
await this.ensureAnomalyActionsMenuOpen(rowIndex);
const isEnabled = await testSubjects.isEnabled('mlAnomaliesListRowActionViewSeriesButton');
Expand Down
3 changes: 3 additions & 0 deletions x-pack/platform/test/functional/services/ml/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import { MachineLearningSettingsProvider } from './settings';
import { MachineLearningSettingsCalendarProvider } from './settings_calendar';
import { MachineLearningSettingsFilterListProvider } from './settings_filter_list';
import { MachineLearningSingleMetricViewerProvider } from './single_metric_viewer';
import { MachineLearningRuleEditorFlyoutProvider } from './rule_editor_flyout';
import { MachineLearningStackManagementJobsProvider } from './stack_management_jobs';
import { MachineLearningTestExecutionProvider } from './test_execution';
import { MachineLearningTestResourcesProvider } from './test_resources';
Expand Down Expand Up @@ -168,6 +169,7 @@ export function MachineLearningProvider(context: FtrProviderContext) {
const settingsCalendar = MachineLearningSettingsCalendarProvider(context, commonUI);
const settingsFilterList = MachineLearningSettingsFilterListProvider(context, commonUI);
const singleMetricViewer = MachineLearningSingleMetricViewerProvider(context, commonUI);
const ruleEditorFlyout = MachineLearningRuleEditorFlyoutProvider(context);
const tableService = MlTableServiceProvider(context);
const stackManagementJobs = MachineLearningStackManagementJobsProvider(context, {
jobTable,
Expand Down Expand Up @@ -238,6 +240,7 @@ export function MachineLearningProvider(context: FtrProviderContext) {
settings,
settingsCalendar,
settingsFilterList,
ruleEditorFlyout,
singleMetricViewer,
stackManagementJobs,
suppliedConfigurations,
Expand Down
Loading