+ + + {routes.map((route, i) => ( diff --git a/x-pack/plugins/apm/public/components/app/Settings/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/index.tsx index 36c36e3957e96..61f68a74be9b7 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/index.tsx @@ -16,8 +16,6 @@ import { import { i18n } from '@kbn/i18n'; import React, { ReactNode, useState } from 'react'; import { RouteComponentProps } from 'react-router-dom'; -import { HeaderMenuPortal } from '../../../../../observability/public'; -import { ActionMenu } from '../../../application/action_menu'; import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; import { getAPMHref } from '../../shared/Links/apm/APMLink'; import { HomeLink } from '../../shared/Links/apm/HomeLink'; @@ -27,7 +25,7 @@ interface SettingsProps extends RouteComponentProps<{}> { } export function Settings({ children, location }: SettingsProps) { - const { appMountParameters, core } = useApmPluginContext(); + const { core } = useApmPluginContext(); const { basePath } = core.http; const canAccessML = !!core.application.capabilities.ml?.canAccessML; const { search, pathname } = location; @@ -44,11 +42,6 @@ export function Settings({ children, location }: SettingsProps) { return ( <> - - - diff --git a/x-pack/plugins/apm/public/components/shared/ApmHeader/index.tsx b/x-pack/plugins/apm/public/components/shared/ApmHeader/index.tsx index 20a589f3126c4..2ba2ae4b5acb6 100644 --- a/x-pack/plugins/apm/public/components/shared/ApmHeader/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/ApmHeader/index.tsx @@ -8,9 +8,6 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import React, { ReactNode } from 'react'; import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; -import { HeaderMenuPortal } from '../../../../../observability/public'; -import { ActionMenu } from '../../../application/action_menu'; -import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; import { EnvironmentFilter } from '../EnvironmentFilter'; const HeaderFlexGroup = euiStyled(EuiFlexGroup)` @@ -19,13 +16,8 @@ const HeaderFlexGroup = euiStyled(EuiFlexGroup)` `; export function ApmHeader({ children }: { children: ReactNode }) { - const { setHeaderActionMenu } = useApmPluginContext().appMountParameters; - return ( - - - {children} diff --git a/x-pack/plugins/observability/public/components/shared/index.tsx b/x-pack/plugins/observability/public/components/shared/index.tsx index 9f738a6b9143a..bdeb4a1ea9990 100644 --- a/x-pack/plugins/observability/public/components/shared/index.tsx +++ b/x-pack/plugins/observability/public/components/shared/index.tsx @@ -8,8 +8,9 @@ import React, { lazy, Suspense } from 'react'; import { CoreVitalProps, HeaderMenuPortalProps } from './types'; +const CoreVitalsLazy = lazy(() => import('./core_web_vitals/index')); + export function getCoreVitalsComponent(props: CoreVitalProps) { - const CoreVitalsLazy = lazy(() => import('./core_web_vitals/index')); return ( @@ -17,8 +18,9 @@ export function getCoreVitalsComponent(props: CoreVitalProps) { ); } +const HeaderMenuPortalLazy = lazy(() => import('./header_menu_portal')); + export function HeaderMenuPortal(props: HeaderMenuPortalProps) { - const HeaderMenuPortalLazy = lazy(() => import('./header_menu_portal')); return ( From df8a8cb2afd4b4aaf85c33570db25f4f0ded6822 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Mon, 22 Feb 2021 19:18:22 +0100 Subject: [PATCH 19/22] [Search Sessions] Fix completed session icon (#92206) --- .../ui/search_session_indicator/search_session_indicator.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.tsx b/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.tsx index 24ffc1359acae..c27a42d8d3d60 100644 --- a/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.tsx +++ b/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.tsx @@ -163,7 +163,7 @@ const searchSessionIndicatorViewStateToProps: { [SearchSessionState.Completed]: { button: { color: 'subdued', - iconType: 'clock', + iconType: 'check', 'aria-label': i18n.translate('xpack.data.searchSessionIndicator.resultsLoadedIconAriaLabel', { defaultMessage: 'Search session complete', }), From c0de6680f0bfa3f1c2de1e1155b5a8c94b8f9e18 Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Mon, 22 Feb 2021 13:51:13 -0500 Subject: [PATCH 20/22] [ML] Data Frame Analytics: adds api integration tests for job creation (#92101) * add create endpoint integration tests * update test suite name --- .../ml/data_frame_analytics/create_job.ts | 143 ++++++++++++++++++ .../apis/ml/data_frame_analytics/index.ts | 1 + 2 files changed, 144 insertions(+) create mode 100644 x-pack/test/api_integration/apis/ml/data_frame_analytics/create_job.ts diff --git a/x-pack/test/api_integration/apis/ml/data_frame_analytics/create_job.ts b/x-pack/test/api_integration/apis/ml/data_frame_analytics/create_job.ts new file mode 100644 index 0000000000000..4a4f5c1e555b4 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/data_frame_analytics/create_job.ts @@ -0,0 +1,143 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { USER } from '../../../../functional/services/ml/security_common'; +import { DataFrameAnalyticsConfig } from '../../../../../plugins/ml/public/application/data_frame_analytics/common'; +import { DeepPartial } from '../../../../../plugins/ml/common/types/common'; +import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common_api'; + +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + const jobId = `bm_${Date.now()}`; + const generateDestinationIndex = (analyticsId: string) => `user-${analyticsId}`; + const commonJobConfig = { + source: { + index: ['ft_bank_marketing'], + query: { + match_all: {}, + }, + }, + analyzed_fields: { + includes: [], + excludes: [], + }, + model_memory_limit: '60mb', + allow_lazy_start: false, // default value + max_num_threads: 1, // default value + }; + + const jobTypes = ['classification', 'regression', 'outlier_detection']; + const jobAnalyses: any = { + classification: { + dependent_variable: 'y', + training_percent: 20, + }, + regression: { + dependent_variable: 'y', + training_percent: 20, + }, + outlier_detection: { + compute_feature_influence: true, + standardization_enabled: true, + }, + }; + + const testJobConfigs: Array<{ + jobId: string; + jobType: string; + config: DeepPartial; + }> = ['Test classification job', 'Test regression job', 'Test outlier detection job'].map( + (description, idx) => { + const analyticsId = `${jobId}_${idx}`; + const jobType = jobTypes[idx]; + return { + jobId: analyticsId, + jobType, + config: { + description, + dest: { + index: generateDestinationIndex(analyticsId), + results_field: 'ml', + }, + analysis: { [jobType]: jobAnalyses[jobType] }, + ...commonJobConfig, + }, + }; + } + ); + + describe('PUT data_frame/analytics/{analyticsId}', () => { + before(async () => { + await esArchiver.loadIfNeeded('ml/bm_classification'); + await ml.testResources.setKibanaTimeZoneToUTC(); + }); + + after(async () => { + await ml.api.cleanMlIndices(); + }); + + describe('CreateDataFrameAnalytics', () => { + testJobConfigs.forEach((testConfig) => { + it(`should create ${testConfig.jobType} job with given config`, async () => { + const analyticsId = `${testConfig.jobId}`; + const requestBody = testConfig.config; + + const { body } = await supertest + .put(`/api/ml/data_frame/analytics/${analyticsId}`) + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .send(requestBody) + .expect(200); + + expect(body).not.to.be(undefined); + + expect(body.description).to.eql(requestBody.description); + expect(body.allow_lazy_start).to.eql(requestBody.allow_lazy_start); + expect(body.model_memory_limit).to.eql(requestBody.model_memory_limit); + expect(body.max_num_threads).to.eql(requestBody.max_num_threads); + + expect(Object.keys(body.analysis)).to.eql(Object.keys(requestBody.analysis!)); + }); + }); + + it('should not allow analytics job creation for unauthorized user', async () => { + const analyticsId = `${testJobConfigs[0].jobId}`; + const requestBody = testJobConfigs[0].config; + + const { body } = await supertest + .put(`/api/ml/data_frame/analytics/${analyticsId}`) + .auth(USER.ML_UNAUTHORIZED, ml.securityCommon.getPasswordForUser(USER.ML_UNAUTHORIZED)) + .set(COMMON_REQUEST_HEADERS) + .send(requestBody) + .expect(403); + + expect(body.error).to.eql('Forbidden'); + expect(body.message).to.eql('Forbidden'); + }); + + it('should not allow analytics job creation for the user with only view permission', async () => { + const analyticsId = `${testJobConfigs[0].jobId}`; + const requestBody = testJobConfigs[0].config; + + const { body } = await supertest + .put(`/api/ml/data_frame/analytics/${analyticsId}`) + .auth(USER.ML_VIEWER, ml.securityCommon.getPasswordForUser(USER.ML_VIEWER)) + .set(COMMON_REQUEST_HEADERS) + .send(requestBody) + .expect(403); + + expect(body.error).to.eql('Forbidden'); + expect(body.message).to.eql('Forbidden'); + }); + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/data_frame_analytics/index.ts b/x-pack/test/api_integration/apis/ml/data_frame_analytics/index.ts index a576a4d825da0..b15e6d892b9e3 100644 --- a/x-pack/test/api_integration/apis/ml/data_frame_analytics/index.ts +++ b/x-pack/test/api_integration/apis/ml/data_frame_analytics/index.ts @@ -12,6 +12,7 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./get')); loadTestFile(require.resolve('./delete')); loadTestFile(require.resolve('./update')); + loadTestFile(require.resolve('./create_job')); loadTestFile(require.resolve('./get_spaces')); loadTestFile(require.resolve('./update_spaces')); loadTestFile(require.resolve('./delete_spaces')); From d9cdb99c077b8330698753b837875325ca0b0ba8 Mon Sep 17 00:00:00 2001 From: Daniil Date: Mon, 22 Feb 2021 22:03:01 +0300 Subject: [PATCH 21/22] [Data Table] Fix visualization layout in split mode (#91799) * Fix table vis layout in split mode * Stick pagination at the bottom * Fix functional tests * Fix jest test Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../public/components/table_visualization.scss | 4 ++++ .../public/components/table_visualization.test.tsx | 1 - .../public/components/table_visualization.tsx | 14 ++++++-------- test/functional/apps/visualize/_data_table.ts | 2 ++ 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/plugins/vis_type_table/public/components/table_visualization.scss b/src/plugins/vis_type_table/public/components/table_visualization.scss index 7bc51ed5c3d93..28dbf17b18739 100644 --- a/src/plugins/vis_type_table/public/components/table_visualization.scss +++ b/src/plugins/vis_type_table/public/components/table_visualization.scss @@ -18,6 +18,10 @@ padding: $euiSizeS; margin-bottom: $euiSizeL; + display: flex; + flex-direction: column; + flex: 1 0 0; + > h3 { text-align: center; } diff --git a/src/plugins/vis_type_table/public/components/table_visualization.test.tsx b/src/plugins/vis_type_table/public/components/table_visualization.test.tsx index 3d169531f5757..44c315cdbd9e4 100644 --- a/src/plugins/vis_type_table/public/components/table_visualization.test.tsx +++ b/src/plugins/vis_type_table/public/components/table_visualization.test.tsx @@ -46,7 +46,6 @@ describe('TableVisualizationComponent', () => { ); expect(useUiState).toHaveBeenLastCalledWith(handlers.uiState); expect(comp.find('.tbvChart__splitColumns').exists()).toBeFalsy(); - expect(comp.find('.tbvChart__split').exists()).toBeTruthy(); }); it('should render split table', () => { diff --git a/src/plugins/vis_type_table/public/components/table_visualization.tsx b/src/plugins/vis_type_table/public/components/table_visualization.tsx index ad47fbc2ae4fa..c5a4f42cbb65e 100644 --- a/src/plugins/vis_type_table/public/components/table_visualization.tsx +++ b/src/plugins/vis_type_table/public/components/table_visualization.tsx @@ -48,14 +48,12 @@ const TableVisualizationComponent = ({
{table ? ( -
- -
+ ) : ( Date: Mon, 22 Feb 2021 20:08:17 +0100 Subject: [PATCH 22/22] [Security Solution][Detections] - Fix loading indicators in the rules management table (#91925) **Base PR:** https://github.com/elastic/kibana/pull/91342 **Fixes:** https://github.com/elastic/kibana/issues/91336 ## Summary This PR fixes loading indicators used in the rules management table. - [Added] Blocking indicator. We show a spinner and "freeze" (fade out) the table when any of these changes: filters, sorting, pagination, manual click on Refresh button. - [Adjusted] Non-blocking indicator. We show a non-blocking "ribbon" (progress bar) only when auto-refresh is in progress. - Initial loading indicator. We show it only on the first table load. Code and tests are slightly adjusted. Things to note are marked below in additional GH comments. Co-authored-by: Yara Tercero --- .../detection_rules/custom_query_rule.spec.ts | 15 ++- .../event_correlation_rule.spec.ts | 15 +-- .../indicator_match_rule.spec.ts | 10 +- .../machine_learning_rule.spec.ts | 10 +- .../detection_rules/override.spec.ts | 10 +- .../detection_rules/prebuilt_rules.spec.ts | 32 ++--- .../detection_rules/sorting.spec.ts | 62 +++++++--- .../detection_rules/threshold_rule.spec.ts | 10 +- ...table.spec.ts => exceptions_table.spec.ts} | 4 +- .../cypress/screens/alerts_detection_rules.ts | 20 ++-- .../cypress/tasks/alerts_detection_rules.ts | 71 +++++++----- .../rules/rules_table/rules_table_facade.ts | 8 ++ .../rules_table/rules_table_reducer.test.ts | 1 + .../rules/rules_table/rules_table_reducer.ts | 14 ++- .../rules/rules_table/use_rules_table.ts | 1 + .../detection_engine/rules/all/index.test.tsx | 20 +++- .../rules/all/rules_tables.tsx | 109 ++++++++++++------ 17 files changed, 257 insertions(+), 155 deletions(-) rename x-pack/plugins/security_solution/cypress/integration/exceptions/{alerts_detection_exceptions_table.spec.ts => exceptions_table.spec.ts} (97%) diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/custom_query_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/custom_query_rule.spec.ts index bedbccadc797f..ecfa96d59170f 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/custom_query_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/custom_query_rule.spec.ts @@ -80,7 +80,7 @@ import { waitForAlertsPanelToBeLoaded, } from '../../tasks/alerts'; import { - changeToThreeHundredRowsPerPage, + changeRowsPerPageTo300, deleteFirstRule, deleteSelectedRules, editFirstRule, @@ -88,8 +88,8 @@ import { goToCreateNewRule, goToRuleDetails, selectNumberOfRules, - waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded, - waitForRulesToBeLoaded, + waitForRulesTableToBeLoaded, + waitForRulesTableToBeRefreshed, } from '../../tasks/alerts_detection_rules'; import { createCustomRuleActivated } from '../../tasks/api_calls/rules'; import { createTimeline } from '../../tasks/api_calls/timelines'; @@ -136,7 +136,7 @@ describe('Custom detection rules creation', () => { waitForAlertsPanelToBeLoaded(); waitForAlertsIndexToBeCreated(); goToManageAlertsDetectionRules(); - waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded(); + waitForRulesTableToBeLoaded(); goToCreateNewRule(); fillDefineCustomRuleWithImportedQueryAndContinue(this.rule); fillAboutRuleAndContinue(this.rule); @@ -158,8 +158,7 @@ describe('Custom detection rules creation', () => { cy.get(CUSTOM_RULES_BTN).should('have.text', 'Custom rules (1)'); - changeToThreeHundredRowsPerPage(); - waitForRulesToBeLoaded(); + changeRowsPerPageTo300(); cy.get(RULES_TABLE).then(($table) => { cy.wrap($table.find(RULES_ROW).length).should('eql', expectedNumberOfRules); @@ -245,7 +244,7 @@ describe('Custom detection rules deletion and edition', () => { cy.get(SHOWING_RULES_TEXT).should('have.text', `Showing ${initialNumberOfRules} rules`); deleteFirstRule(); - waitForRulesToBeLoaded(); + waitForRulesTableToBeRefreshed(); cy.get(RULES_TABLE).then(($table) => { cy.wrap($table.find(RULES_ROW).length).should( @@ -275,7 +274,7 @@ describe('Custom detection rules deletion and edition', () => { selectNumberOfRules(numberOfRulesToBeDeleted); deleteSelectedRules(); - waitForRulesToBeLoaded(); + waitForRulesTableToBeRefreshed(); cy.get(RULES_TABLE).then(($table) => { cy.wrap($table.find(RULES_ROW).length).should( diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/event_correlation_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/event_correlation_rule.spec.ts index fac1056310523..93c200309fff6 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/event_correlation_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/event_correlation_rule.spec.ts @@ -56,12 +56,11 @@ import { waitForAlertsPanelToBeLoaded, } from '../../tasks/alerts'; import { - changeToThreeHundredRowsPerPage, + changeRowsPerPageTo300, filterByCustomRules, goToCreateNewRule, goToRuleDetails, - waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded, - waitForRulesToBeLoaded, + waitForRulesTableToBeLoaded, } from '../../tasks/alerts_detection_rules'; import { createTimeline } from '../../tasks/api_calls/timelines'; import { cleanKibana } from '../../tasks/common'; @@ -104,7 +103,7 @@ describe('Detection rules, EQL', () => { waitForAlertsPanelToBeLoaded(); waitForAlertsIndexToBeCreated(); goToManageAlertsDetectionRules(); - waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded(); + waitForRulesTableToBeLoaded(); goToCreateNewRule(); selectEqlRuleType(); fillDefineEqlRuleAndContinue(this.rule); @@ -114,8 +113,7 @@ describe('Detection rules, EQL', () => { cy.get(CUSTOM_RULES_BTN).should('have.text', 'Custom rules (1)'); - changeToThreeHundredRowsPerPage(); - waitForRulesToBeLoaded(); + changeRowsPerPageTo300(); cy.get(RULES_TABLE).then(($table) => { cy.wrap($table.find(RULES_ROW).length).should('eql', expectedNumberOfRules); @@ -200,7 +198,7 @@ describe('Detection rules, sequence EQL', () => { waitForAlertsPanelToBeLoaded(); waitForAlertsIndexToBeCreated(); goToManageAlertsDetectionRules(); - waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded(); + waitForRulesTableToBeLoaded(); goToCreateNewRule(); selectEqlRuleType(); fillDefineEqlRuleAndContinue(this.rule); @@ -210,8 +208,7 @@ describe('Detection rules, sequence EQL', () => { cy.get(CUSTOM_RULES_BTN).should('have.text', 'Custom rules (1)'); - changeToThreeHundredRowsPerPage(); - waitForRulesToBeLoaded(); + changeRowsPerPageTo300(); cy.get(RULES_TABLE).then(($table) => { cy.wrap($table.find(RULES_ROW).length).should('eql', expectedNumberOfRules); diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts index db29f44ceb98c..966ce3098d6a7 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts @@ -59,12 +59,11 @@ import { waitForAlertsPanelToBeLoaded, } from '../../tasks/alerts'; import { - changeToThreeHundredRowsPerPage, + changeRowsPerPageTo300, filterByCustomRules, goToCreateNewRule, goToRuleDetails, - waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded, - waitForRulesToBeLoaded, + waitForRulesTableToBeLoaded, } from '../../tasks/alerts_detection_rules'; import { cleanKibana } from '../../tasks/common'; import { @@ -375,7 +374,7 @@ describe('indicator match', () => { waitForAlertsPanelToBeLoaded(); waitForAlertsIndexToBeCreated(); goToManageAlertsDetectionRules(); - waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded(); + waitForRulesTableToBeLoaded(); goToCreateNewRule(); selectIndicatorMatchType(); }); @@ -388,8 +387,7 @@ describe('indicator match', () => { cy.get(CUSTOM_RULES_BTN).should('have.text', 'Custom rules (1)'); - changeToThreeHundredRowsPerPage(); - waitForRulesToBeLoaded(); + changeRowsPerPageTo300(); cy.get(RULES_TABLE).then(($table) => { cy.wrap($table.find(RULES_ROW).length).should('eql', expectedNumberOfRules); diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/machine_learning_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/machine_learning_rule.spec.ts index f4e5aaf513190..e420b970ad85f 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/machine_learning_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/machine_learning_rule.spec.ts @@ -46,12 +46,11 @@ import { waitForAlertsPanelToBeLoaded, } from '../../tasks/alerts'; import { - changeToThreeHundredRowsPerPage, + changeRowsPerPageTo300, filterByCustomRules, goToCreateNewRule, goToRuleDetails, - waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded, - waitForRulesToBeLoaded, + waitForRulesTableToBeLoaded, } from '../../tasks/alerts_detection_rules'; import { cleanKibana } from '../../tasks/common'; import { @@ -81,7 +80,7 @@ describe('Detection rules, machine learning', () => { waitForAlertsPanelToBeLoaded(); waitForAlertsIndexToBeCreated(); goToManageAlertsDetectionRules(); - waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded(); + waitForRulesTableToBeLoaded(); goToCreateNewRule(); selectMachineLearningRuleType(); fillDefineMachineLearningRuleAndContinue(machineLearningRule); @@ -91,8 +90,7 @@ describe('Detection rules, machine learning', () => { cy.get(CUSTOM_RULES_BTN).should('have.text', 'Custom rules (1)'); - changeToThreeHundredRowsPerPage(); - waitForRulesToBeLoaded(); + changeRowsPerPageTo300(); cy.get(RULES_TABLE).then(($table) => { cy.wrap($table.find(RULES_ROW).length).should('eql', expectedNumberOfRules); diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/override.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/override.spec.ts index 8bd30e54c77a5..82402019fa1e2 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/override.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/override.spec.ts @@ -68,12 +68,11 @@ import { waitForAlertsPanelToBeLoaded, } from '../../tasks/alerts'; import { - changeToThreeHundredRowsPerPage, + changeRowsPerPageTo300, filterByCustomRules, goToCreateNewRule, goToRuleDetails, - waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded, - waitForRulesToBeLoaded, + waitForRulesTableToBeLoaded, } from '../../tasks/alerts_detection_rules'; import { createTimeline } from '../../tasks/api_calls/timelines'; import { cleanKibana } from '../../tasks/common'; @@ -113,7 +112,7 @@ describe('Detection rules, override', () => { waitForAlertsPanelToBeLoaded(); waitForAlertsIndexToBeCreated(); goToManageAlertsDetectionRules(); - waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded(); + waitForRulesTableToBeLoaded(); goToCreateNewRule(); fillDefineCustomRuleWithImportedQueryAndContinue(this.rule); fillAboutRuleWithOverrideAndContinue(this.rule); @@ -122,8 +121,7 @@ describe('Detection rules, override', () => { cy.get(CUSTOM_RULES_BTN).should('have.text', 'Custom rules (1)'); - changeToThreeHundredRowsPerPage(); - waitForRulesToBeLoaded(); + changeRowsPerPageTo300(); const expectedNumberOfRules = 1; cy.get(RULES_TABLE).then(($table) => { diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/prebuilt_rules.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/prebuilt_rules.spec.ts index 14954f4811139..d290773d425e2 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/prebuilt_rules.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/prebuilt_rules.spec.ts @@ -20,16 +20,15 @@ import { waitForAlertsPanelToBeLoaded, } from '../../tasks/alerts'; import { - changeToThreeHundredRowsPerPage, + changeRowsPerPageTo300, deleteFirstRule, deleteSelectedRules, loadPrebuiltDetectionRules, - paginate, + goToNextPage, reloadDeletedRules, selectNumberOfRules, - waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded, + waitForRulesTableToBeLoaded, waitForPrebuiltDetectionRulesToBeLoaded, - waitForRulesToBeLoaded, } from '../../tasks/alerts_detection_rules'; import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login'; @@ -51,20 +50,18 @@ describe('Alerts rules, prebuilt rules', () => { waitForAlertsPanelToBeLoaded(); waitForAlertsIndexToBeCreated(); goToManageAlertsDetectionRules(); - waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded(); + waitForRulesTableToBeLoaded(); loadPrebuiltDetectionRules(); waitForPrebuiltDetectionRulesToBeLoaded(); cy.get(ELASTIC_RULES_BTN).should('have.text', expectedElasticRulesBtnText); - changeToThreeHundredRowsPerPage(); - waitForRulesToBeLoaded(); + changeRowsPerPageTo300(); cy.get(SHOWING_RULES_TEXT).should('have.text', `Showing ${expectedNumberOfRules} rules`); cy.get(RULES_TABLE).then(($table1) => { const firstScreenRules = $table1.find(RULES_ROW).length; - paginate(); - waitForRulesToBeLoaded(); + goToNextPage(); cy.get(RULES_TABLE).then(($table2) => { const secondScreenRules = $table2.find(RULES_ROW).length; const totalNumberOfRules = firstScreenRules + secondScreenRules; @@ -85,14 +82,13 @@ describe('Deleting prebuilt rules', () => { waitForAlertsPanelToBeLoaded(); waitForAlertsIndexToBeCreated(); goToManageAlertsDetectionRules(); - waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded(); + waitForRulesTableToBeLoaded(); loadPrebuiltDetectionRules(); waitForPrebuiltDetectionRulesToBeLoaded(); cy.get(ELASTIC_RULES_BTN).should('have.text', expectedElasticRulesBtnText); - changeToThreeHundredRowsPerPage(); - waitForRulesToBeLoaded(); + changeRowsPerPageTo300(); }); it('Does not allow to delete one rule when more than one is selected', () => { @@ -110,8 +106,7 @@ describe('Deleting prebuilt rules', () => { deleteFirstRule(); cy.reload(); - changeToThreeHundredRowsPerPage(); - waitForRulesToBeLoaded(); + changeRowsPerPageTo300(); cy.get(ELASTIC_RULES_BTN).should( 'have.text', @@ -125,8 +120,7 @@ describe('Deleting prebuilt rules', () => { cy.get(RELOAD_PREBUILT_RULES_BTN).should('not.exist'); cy.reload(); - changeToThreeHundredRowsPerPage(); - waitForRulesToBeLoaded(); + changeRowsPerPageTo300(); cy.get(ELASTIC_RULES_BTN).should( 'have.text', @@ -142,8 +136,7 @@ describe('Deleting prebuilt rules', () => { selectNumberOfRules(numberOfRulesToBeSelected); deleteSelectedRules(); cy.reload(); - changeToThreeHundredRowsPerPage(); - waitForRulesToBeLoaded(); + changeRowsPerPageTo300(); cy.get(RELOAD_PREBUILT_RULES_BTN).should('exist'); cy.get(RELOAD_PREBUILT_RULES_BTN).should( @@ -160,8 +153,7 @@ describe('Deleting prebuilt rules', () => { cy.get(RELOAD_PREBUILT_RULES_BTN).should('not.exist'); cy.reload(); - changeToThreeHundredRowsPerPage(); - waitForRulesToBeLoaded(); + changeRowsPerPageTo300(); cy.get(ELASTIC_RULES_BTN).should( 'have.text', diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/sorting.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/sorting.spec.ts index 26d87f9ce9e17..0cf3caa09814c 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/sorting.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/sorting.spec.ts @@ -12,6 +12,8 @@ import { SECOND_RULE, RULE_AUTO_REFRESH_IDLE_MODAL, FOURTH_RULE, + RULES_TABLE, + pageSelector, } from '../../screens/alerts_detection_rules'; import { @@ -21,12 +23,14 @@ import { } from '../../tasks/alerts'; import { activateRule, + changeRowsPerPageTo, checkAllRulesIdleModal, checkAutoRefresh, dismissAllRulesIdleModal, + goToPage, resetAllRulesIdleModalTimeout, sortByActivatedRules, - waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded, + waitForRulesTableToBeLoaded, waitForRuleToBeActivated, } from '../../tasks/alerts_detection_rules'; import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login'; @@ -49,12 +53,9 @@ describe('Alerts detection rules', () => { createCustomRule(newThresholdRule, '4'); }); - after(() => { - cy.clock().invoke('restore'); - }); - it('Sorts by activated rules', () => { goToManageAlertsDetectionRules(); + waitForRulesTableToBeLoaded(); cy.get(RULE_NAME) .eq(SECOND_RULE) @@ -88,15 +89,50 @@ describe('Alerts detection rules', () => { }); }); - // FIXME: UI hangs on loading - it.skip('Auto refreshes rules', () => { + it('Pagination updates page number and results', () => { + createCustomRule({ ...newRule, name: 'Test a rule' }, '5'); + createCustomRule({ ...newRule, name: 'Not same as first rule' }, '6'); + + goToManageAlertsDetectionRules(); + waitForRulesTableToBeLoaded(); + + changeRowsPerPageTo(5); + + const FIRST_PAGE_SELECTOR = pageSelector(1); + const SECOND_PAGE_SELECTOR = pageSelector(2); + + cy.get(RULES_TABLE) + .find(FIRST_PAGE_SELECTOR) + .should('have.class', 'euiPaginationButton-isActive'); + + cy.get(RULES_TABLE) + .find(RULE_NAME) + .first() + .invoke('text') + .then((ruleNameFirstPage) => { + goToPage(2); + cy.get(RULES_TABLE) + .find(RULE_NAME) + .first() + .invoke('text') + .should((ruleNameSecondPage) => { + expect(ruleNameFirstPage).not.to.eq(ruleNameSecondPage); + }); + }); + + cy.get(RULES_TABLE) + .find(FIRST_PAGE_SELECTOR) + .should('not.have.class', 'euiPaginationButton-isActive'); + cy.get(RULES_TABLE) + .find(SECOND_PAGE_SELECTOR) + .should('have.class', 'euiPaginationButton-isActive'); + }); + + it('Auto refreshes rules', () => { cy.clock(Date.now()); - loginAndWaitForPageWithoutDateRange(DETECTIONS_URL); - waitForAlertsPanelToBeLoaded(); - waitForAlertsIndexToBeCreated(); goToManageAlertsDetectionRules(); - waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded(); + waitForRulesTableToBeLoaded(); // mock 1 minute passing to make sure refresh // is conducted @@ -105,7 +141,7 @@ describe('Alerts detection rules', () => { // mock 45 minutes passing to check that idle modal shows // and refreshing is paused checkAllRulesIdleModal('be.visible'); - checkAutoRefresh(DEFAULT_RULE_REFRESH_INTERVAL_VALUE, 'not.be.visible'); + checkAutoRefresh(DEFAULT_RULE_REFRESH_INTERVAL_VALUE, 'not.exist'); // clicking on modal to continue, should resume refreshing dismissAllRulesIdleModal(); @@ -115,7 +151,5 @@ describe('Alerts detection rules', () => { // show after 45 min resetAllRulesIdleModalTimeout(); cy.get(RULE_AUTO_REFRESH_IDLE_MODAL).should('not.exist'); - - cy.clock().invoke('restore'); }); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/threshold_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/threshold_rule.spec.ts index 3c188345111c8..572422a4936df 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/threshold_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/threshold_rule.spec.ts @@ -58,12 +58,11 @@ import { waitForAlertsPanelToBeLoaded, } from '../../tasks/alerts'; import { - changeToThreeHundredRowsPerPage, + changeRowsPerPageTo300, filterByCustomRules, goToCreateNewRule, goToRuleDetails, - waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded, - waitForRulesToBeLoaded, + waitForRulesTableToBeLoaded, } from '../../tasks/alerts_detection_rules'; import { createTimeline } from '../../tasks/api_calls/timelines'; import { cleanKibana } from '../../tasks/common'; @@ -102,7 +101,7 @@ describe.skip('Threshold Rules', () => { waitForAlertsPanelToBeLoaded(); waitForAlertsIndexToBeCreated(); goToManageAlertsDetectionRules(); - waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded(); + waitForRulesTableToBeLoaded(); goToCreateNewRule(); selectThresholdRuleType(); fillDefineThresholdRuleAndContinue(rule); @@ -112,8 +111,7 @@ describe.skip('Threshold Rules', () => { cy.get(CUSTOM_RULES_BTN).should('have.text', 'Custom rules (1)'); - changeToThreeHundredRowsPerPage(); - waitForRulesToBeLoaded(); + changeRowsPerPageTo300(); const expectedNumberOfRules = 1; cy.get(RULES_TABLE).then(($table) => { diff --git a/x-pack/plugins/security_solution/cypress/integration/exceptions/alerts_detection_exceptions_table.spec.ts b/x-pack/plugins/security_solution/cypress/integration/exceptions/exceptions_table.spec.ts similarity index 97% rename from x-pack/plugins/security_solution/cypress/integration/exceptions/alerts_detection_exceptions_table.spec.ts rename to x-pack/plugins/security_solution/cypress/integration/exceptions/exceptions_table.spec.ts index aa469a0cb2531..fdc8a268ca368 100644 --- a/x-pack/plugins/security_solution/cypress/integration/exceptions/alerts_detection_exceptions_table.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/exceptions/exceptions_table.spec.ts @@ -12,7 +12,7 @@ import { RULE_STATUS } from '../../screens/create_new_rule'; import { goToManageAlertsDetectionRules, waitForAlertsIndexToBeCreated } from '../../tasks/alerts'; import { createCustomRule } from '../../tasks/api_calls/rules'; -import { goToRuleDetails, waitForRulesToBeLoaded } from '../../tasks/alerts_detection_rules'; +import { goToRuleDetails, waitForRulesTableToBeLoaded } from '../../tasks/alerts_detection_rules'; import { esArchiverLoad, esArchiverUnload } from '../../tasks/es_archiver'; import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login'; import { @@ -61,7 +61,7 @@ describe('Exceptions Table', () => { createExceptionList(exceptionList).as('exceptionListResponse'); goBackToAllRulesTable(); - waitForRulesToBeLoaded(); + waitForRulesTableToBeLoaded(); }); after(() => { diff --git a/x-pack/plugins/security_solution/cypress/screens/alerts_detection_rules.ts b/x-pack/plugins/security_solution/cypress/screens/alerts_detection_rules.ts index 1a22d14e396ed..68baad7d3d259 100644 --- a/x-pack/plugins/security_solution/cypress/screens/alerts_detection_rules.ts +++ b/x-pack/plugins/security_solution/cypress/screens/alerts_detection_rules.ts @@ -31,14 +31,12 @@ export const FOURTH_RULE = 3; export const LOAD_PREBUILT_RULES_BTN = '[data-test-subj="load-prebuilt-rules"]'; -export const LOADING_INITIAL_PREBUILT_RULES_TABLE = +export const RULES_TABLE_INITIAL_LOADING_INDICATOR = '[data-test-subj="initialLoadingPanelAllRulesTable"]'; -export const ASYNC_LOADING_PROGRESS = '[data-test-subj="loadingRulesInfoProgress"]'; +export const RULES_TABLE_REFRESH_INDICATOR = '[data-test-subj="loading-spinner"]'; -export const NEXT_BTN = '[data-test-subj="pagination-button-next"]'; - -export const PAGINATION_POPOVER_BTN = '[data-test-subj="tablePaginationPopoverButton"]'; +export const RULES_TABLE_AUTOREFRESH_INDICATOR = '[data-test-subj="loadingRulesInfoProgress"]'; export const RISK_SCORE = '[data-test-subj="riskScore"]'; @@ -66,8 +64,16 @@ export const SHOWING_RULES_TEXT = '[data-test-subj="showingRules"]'; export const SORT_RULES_BTN = '[data-test-subj="tableHeaderSortButton"]'; -export const THREE_HUNDRED_ROWS = '[data-test-subj="tablePagination-300-rows"]'; - export const RULE_AUTO_REFRESH_IDLE_MODAL = '[data-test-subj="allRulesIdleModal"]'; export const RULE_AUTO_REFRESH_IDLE_MODAL_CONTINUE = '[data-test-subj="allRulesIdleModal"] button'; + +export const PAGINATION_POPOVER_BTN = '[data-test-subj="tablePaginationPopoverButton"]'; + +export const rowsPerPageSelector = (count: number) => + `[data-test-subj="tablePagination-${count}-rows"]`; + +export const pageSelector = (pageNumber: number) => + `[data-test-subj="pagination-button-${pageNumber - 1}"]`; + +export const NEXT_BTN = '[data-test-subj="pagination-button-next"]'; diff --git a/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts b/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts index 799124190b18d..3553889449e6d 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts @@ -13,7 +13,9 @@ import { DELETE_RULE_ACTION_BTN, DELETE_RULE_BULK_BTN, LOAD_PREBUILT_RULES_BTN, - LOADING_INITIAL_PREBUILT_RULES_TABLE, + RULES_TABLE_INITIAL_LOADING_INDICATOR, + RULES_TABLE_REFRESH_INDICATOR, + RULES_TABLE_AUTOREFRESH_INDICATOR, PAGINATION_POPOVER_BTN, RELOAD_PREBUILT_RULES_BTN, RULE_CHECKBOX, @@ -22,13 +24,13 @@ import { RULE_SWITCH_LOADER, RULES_TABLE, SORT_RULES_BTN, - THREE_HUNDRED_ROWS, EXPORT_ACTION_BTN, EDIT_RULE_ACTION_BTN, NEXT_BTN, - ASYNC_LOADING_PROGRESS, RULE_AUTO_REFRESH_IDLE_MODAL, RULE_AUTO_REFRESH_IDLE_MODAL_CONTINUE, + rowsPerPageSelector, + pageSelector, } from '../screens/alerts_detection_rules'; import { ALL_ACTIONS, DELETE_RULE } from '../screens/rule_details'; @@ -36,11 +38,6 @@ export const activateRule = (rulePosition: number) => { cy.get(RULE_SWITCH).eq(rulePosition).click({ force: true }); }; -export const changeToThreeHundredRowsPerPage = () => { - cy.get(PAGINATION_POPOVER_BTN).click({ force: true }); - cy.get(THREE_HUNDRED_ROWS).click(); -}; - export const editFirstRule = () => { cy.get(COLLAPSED_ACTION_BTN).should('be.visible'); cy.get(COLLAPSED_ACTION_BTN).first().click({ force: true }); @@ -71,8 +68,7 @@ export const exportFirstRule = () => { export const filterByCustomRules = () => { cy.get(CUSTOM_RULES_BTN).click({ force: true }); - cy.get(ASYNC_LOADING_PROGRESS).should('exist'); - cy.get(ASYNC_LOADING_PROGRESS).should('not.exist'); + waitForRulesTableToBeRefreshed(); }; export const goToCreateNewRule = () => { @@ -87,10 +83,6 @@ export const loadPrebuiltDetectionRules = () => { cy.get(LOAD_PREBUILT_RULES_BTN).should('exist').click({ force: true }); }; -export const paginate = () => { - cy.get(NEXT_BTN).click(); -}; - export const reloadDeletedRules = () => { cy.get(RELOAD_PREBUILT_RULES_BTN).click({ force: true }); }; @@ -103,14 +95,24 @@ export const selectNumberOfRules = (numberOfRules: number) => { export const sortByActivatedRules = () => { cy.get(SORT_RULES_BTN).contains('Activated').click({ force: true }); - waitForRulesToBeLoaded(); + waitForRulesTableToBeRefreshed(); cy.get(SORT_RULES_BTN).contains('Activated').click({ force: true }); - waitForRulesToBeLoaded(); + waitForRulesTableToBeRefreshed(); }; -export const waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded = () => { - cy.get(LOADING_INITIAL_PREBUILT_RULES_TABLE).should('exist'); - cy.get(LOADING_INITIAL_PREBUILT_RULES_TABLE).should('not.exist'); +export const waitForRulesTableToBeLoaded = () => { + cy.get(RULES_TABLE_INITIAL_LOADING_INDICATOR).should('exist'); + cy.get(RULES_TABLE_INITIAL_LOADING_INDICATOR).should('not.exist'); +}; + +export const waitForRulesTableToBeRefreshed = () => { + cy.get(RULES_TABLE_REFRESH_INDICATOR).should('exist'); + cy.get(RULES_TABLE_REFRESH_INDICATOR).should('not.exist'); +}; + +export const waitForRulesTableToBeAutoRefreshed = () => { + cy.get(RULES_TABLE_AUTOREFRESH_INDICATOR).should('exist'); + cy.get(RULES_TABLE_AUTOREFRESH_INDICATOR).should('not.exist'); }; export const waitForPrebuiltDetectionRulesToBeLoaded = () => { @@ -123,15 +125,10 @@ export const waitForRuleToBeActivated = () => { cy.get(RULE_SWITCH_LOADER).should('not.exist'); }; -export const waitForRulesToBeLoaded = () => { - cy.get(ASYNC_LOADING_PROGRESS).should('exist'); - cy.get(ASYNC_LOADING_PROGRESS).should('not.exist'); -}; - export const checkAutoRefresh = (ms: number, condition: string) => { - cy.get(ASYNC_LOADING_PROGRESS).should('not.exist'); + cy.get(RULES_TABLE_AUTOREFRESH_INDICATOR).should('not.exist'); cy.tick(ms); - cy.get(ASYNC_LOADING_PROGRESS).should(condition); + cy.get(RULES_TABLE_AUTOREFRESH_INDICATOR).should(condition); }; export const dismissAllRulesIdleModal = () => { @@ -152,3 +149,25 @@ export const resetAllRulesIdleModalTimeout = () => { cy.window().trigger('mousemove', { force: true }); cy.tick(700000); }; + +export const changeRowsPerPageTo = (rowsCount: number) => { + cy.get(PAGINATION_POPOVER_BTN).click({ force: true }); + cy.get(rowsPerPageSelector(rowsCount)).click(); + waitForRulesTableToBeRefreshed(); +}; + +export const changeRowsPerPageTo300 = () => { + changeRowsPerPageTo(300); +}; + +export const goToPage = (pageNumber: number) => { + cy.get(RULES_TABLE_REFRESH_INDICATOR).should('not.exist'); + cy.get(pageSelector(pageNumber)).last().click({ force: true }); + waitForRulesTableToBeRefreshed(); +}; + +export const goToNextPage = () => { + cy.get(RULES_TABLE_REFRESH_INDICATOR).should('not.exist'); + cy.get(NEXT_BTN).click({ force: true }); + waitForRulesTableToBeRefreshed(); +}; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/rules_table/rules_table_facade.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/rules_table/rules_table_facade.ts index 77c327c9f7939..e9fec425d467e 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/rules_table/rules_table_facade.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/rules_table/rules_table_facade.ts @@ -18,6 +18,7 @@ export interface RulesTableFacade { setShowIdleModal(show: boolean): void; setLastRefreshDate(): void; setAutoRefreshOn(on: boolean): void; + setIsRefreshing(isRefreshing: boolean): void; } export const createRulesTableFacade = (dispatch: Dispatch): RulesTableFacade => { @@ -80,5 +81,12 @@ export const createRulesTableFacade = (dispatch: Dispatch): Ru on, }); }, + + setIsRefreshing: (isRefreshing: boolean) => { + dispatch({ + type: 'setIsRefreshing', + isRefreshing, + }); + }, }; }; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/rules_table/rules_table_reducer.test.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/rules_table/rules_table_reducer.test.ts index 1a45c60dba58a..c90bfe2b0267f 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/rules_table/rules_table_reducer.test.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/rules_table/rules_table_reducer.test.ts @@ -30,6 +30,7 @@ const initialState: RulesTableState = { exportRuleIds: [], lastUpdated: 0, isRefreshOn: false, + isRefreshing: false, showIdleModal: false, }; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/rules_table/rules_table_reducer.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/rules_table/rules_table_reducer.ts index edcf4f6395d89..92f21f6b508aa 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/rules_table/rules_table_reducer.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/rules_table/rules_table_reducer.ts @@ -28,6 +28,7 @@ export interface RulesTableState { exportRuleIds: string[]; lastUpdated: number; isRefreshOn: boolean; + isRefreshing: boolean; showIdleModal: boolean; } @@ -44,6 +45,7 @@ export type RulesTableAction = | { type: 'exportRuleIds'; ids: string[] } | { type: 'setLastRefreshDate' } | { type: 'setAutoRefreshOn'; on: boolean } + | { type: 'setIsRefreshing'; isRefreshing: boolean } | { type: 'setShowIdleModal'; show: boolean } | { type: 'failure' }; @@ -53,11 +55,7 @@ export const createRulesTableReducer = ( const rulesTableReducer = (state: RulesTableState, action: RulesTableAction): RulesTableState => { switch (action.type) { case 'setRules': { - if ( - tableRef != null && - tableRef.current != null && - tableRef.current.changeSelection != null - ) { + if (tableRef?.current?.changeSelection != null) { // for future devs: eui basic table is not giving us a prop to set the value, so // we are using the ref in setTimeout to reset on the next loop so that we // do not get a warning telling us we are trying to update during a render @@ -142,6 +140,12 @@ export const createRulesTableReducer = ( isRefreshOn: action.on, }; } + case 'setIsRefreshing': { + return { + ...state, + isRefreshing: action.isRefreshing, + }; + } case 'setShowIdleModal': { return { ...state, diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/rules_table/use_rules_table.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/rules_table/use_rules_table.ts index f31b2894301ba..e36474a2fdddd 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/rules_table/use_rules_table.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/rules_table/use_rules_table.ts @@ -38,6 +38,7 @@ const initialStateDefaults: RulesTableState = { exportRuleIds: [], lastUpdated: 0, isRefreshOn: true, + isRefreshing: false, showIdleModal: false, }; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.test.tsx index 7f9061b6abc83..d552afdd9f13f 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.test.tsx @@ -11,10 +11,9 @@ import { waitFor } from '@testing-library/react'; import '../../../../../common/mock/match_media'; import '../../../../../common/mock/formatted_relative'; -import { AllRules } from './index'; -import { useKibana, useUiSetting$ } from '../../../../../common/lib/kibana'; -import { useRulesTable, useRulesStatuses } from '../../../../containers/detection_engine/rules'; import { TestProviders } from '../../../../../common/mock'; + +import { useKibana, useUiSetting$ } from '../../../../../common/lib/kibana'; import { createUseUiSetting$Mock } from '../../../../../common/lib/kibana/kibana_react.mock'; import { DEFAULT_RULE_REFRESH_INTERVAL_ON, @@ -23,6 +22,14 @@ import { DEFAULT_RULES_TABLE_REFRESH_SETTING, } from '../../../../../../common/constants'; +import { + useRulesTable, + useRulesStatuses, + RulesTableState, +} from '../../../../containers/detection_engine/rules'; + +import { AllRules } from './index'; + jest.mock('react-router-dom', () => { const original = jest.requireActual('react-router-dom'); @@ -64,10 +71,11 @@ describe('AllRules', () => { }); mockUseRulesTable.mockImplementation(({ initialStateOverride }) => { - const initialState = { + const initialState: RulesTableState = { rules: [ { actions: [], + author: [], created_at: '2020-02-14T19:49:28.178Z', created_by: 'elastic', description: 'jibber jabber', @@ -86,8 +94,10 @@ describe('AllRules', () => { query: 'host.name:*', references: [], risk_score: 73, + risk_score_mapping: [], rule_id: '571afc56-5ed9-465d-a2a9-045f099f6e7e', severity: 'high', + severity_mapping: [], tags: ['Elastic', 'Endpoint'], threat: [], throttle: null, @@ -117,6 +127,7 @@ describe('AllRules', () => { exportRuleIds: [], lastUpdated: 0, isRefreshOn: true, + isRefreshing: false, showIdleModal: false, }; @@ -132,6 +143,7 @@ describe('AllRules', () => { setShowIdleModal: jest.fn(), setLastRefreshDate: jest.fn(), setAutoRefreshOn: jest.fn(), + setIsRefreshing: jest.fn(), }; }); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_tables.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_tables.tsx index a40833d8d14ac..411e817c4407f 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_tables.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_tables.tsx @@ -5,6 +5,8 @@ * 2.0. */ +/* eslint-disable complexity */ + import { EuiBasicTable, EuiLoadingContent, @@ -130,6 +132,7 @@ export const RulesTables = React.memo( lastUpdated, showIdleModal, isRefreshOn, + isRefreshing, } = rulesTable.state; const { @@ -139,11 +142,10 @@ export const RulesTables = React.memo( setShowIdleModal, setLastRefreshDate, setAutoRefreshOn, + setIsRefreshing, reFetchRules, } = rulesTable; - const isLoadingRules = loadingRulesAction === 'load'; - const { loading: isLoadingRulesStatuses, rulesStatuses } = useRulesStatuses(rules); const [, dispatchToaster] = useStateToaster(); const mlCapabilities = useMlCapabilities(); @@ -151,6 +153,19 @@ export const RulesTables = React.memo( // TODO: Refactor license check + hasMlAdminPermissions to common check const hasMlPermissions = hasMlLicense(mlCapabilities) && hasMlAdminPermissions(mlCapabilities); + const isLoadingRules = loadingRulesAction === 'load'; + const isLoadingAnActionOnRule = useMemo(() => { + if ( + loadingRuleIds.length > 0 && + (loadingRulesAction === 'disable' || loadingRulesAction === 'enable') + ) { + return false; + } else if (loadingRuleIds.length > 0) { + return true; + } + return false; + }, [loadingRuleIds, loadingRulesAction]); + const sorting = useMemo( (): SortingType => ({ sort: { @@ -225,8 +240,9 @@ export const RulesTables = React.memo( }, { page: page.index + 1, perPage: page.size } ); + setLastRefreshDate(); }, - [updateOptions] + [updateOptions, setLastRefreshDate] ); const rulesColumns = useMemo(() => { @@ -292,25 +308,41 @@ export const RulesTables = React.memo( [loadingRuleIds, dispatch] ); - const isLoadingAnActionOnRule = useMemo(() => { - if ( - loadingRuleIds.length > 0 && - (loadingRulesAction === 'disable' || loadingRulesAction === 'enable') - ) { - return false; - } else if (loadingRuleIds.length > 0) { - return true; - } - return false; - }, [loadingRuleIds, loadingRulesAction]); + const refreshTable = useCallback( + async (mode: 'auto' | 'manual' = 'manual'): Promise => { + if (isLoadingAnActionOnRule) { + return; + } + + const isAutoRefresh = mode === 'auto'; + if (isAutoRefresh) { + setIsRefreshing(true); + } - const handleRefreshData = useCallback(async (): Promise => { - if (!isLoadingAnActionOnRule) { await reFetchRules(); await refetchPrePackagedRulesStatus(); setLastRefreshDate(); - } - }, [reFetchRules, isLoadingAnActionOnRule, setLastRefreshDate, refetchPrePackagedRulesStatus]); + + if (isAutoRefresh) { + setIsRefreshing(false); + } + }, + [ + isLoadingAnActionOnRule, + setIsRefreshing, + reFetchRules, + refetchPrePackagedRulesStatus, + setLastRefreshDate, + ] + ); + + const handleAutoRefresh = useCallback(async (): Promise => { + await refreshTable('auto'); + }, [refreshTable]); + + const handleManualRefresh = useCallback(async (): Promise => { + await refreshTable(); + }, [refreshTable]); const handleResetIdleTimer = useCallback((): void => { if (isRefreshOn) { @@ -326,29 +358,29 @@ export const RulesTables = React.memo( useEffect(() => { const interval = setInterval(() => { if (isRefreshOn) { - handleRefreshData(); + handleAutoRefresh(); } }, defaultAutoRefreshSetting.value); return () => { clearInterval(interval); }; - }, [isRefreshOn, handleRefreshData, defaultAutoRefreshSetting.value]); + }, [isRefreshOn, handleAutoRefresh, defaultAutoRefreshSetting.value]); const handleIdleModalContinue = useCallback((): void => { setShowIdleModal(false); - handleRefreshData(); + handleAutoRefresh(); setAutoRefreshOn(true); - }, [setShowIdleModal, setAutoRefreshOn, handleRefreshData]); + }, [setShowIdleModal, setAutoRefreshOn, handleAutoRefresh]); const handleAutoRefreshSwitch = useCallback( (refreshOn: boolean) => { if (refreshOn) { - handleRefreshData(); + handleAutoRefresh(); } setAutoRefreshOn(refreshOn); }, - [setAutoRefreshOn, handleRefreshData] + [setAutoRefreshOn, handleAutoRefresh] ); const shouldShowRulesTable = useMemo( @@ -401,14 +433,16 @@ export const RulesTables = React.memo( data-test-subj="allRulesPanel" > <> - {(isLoadingRules || isLoadingRulesStatuses) && ( - - )} + {!initLoading && + (loading || isLoadingRules || isLoadingAnActionOnRule) && + isRefreshing && ( + + )} ( )} - {isLoadingAnActionOnRule && !initLoading && ( - - )} + {!initLoading && + (loading || isLoadingRules || isLoadingAnActionOnRule) && + !isRefreshing && ( + + )} + {shouldShowPrepackagedRulesPrompt && ( ( paginationTotal={pagination.total ?? 0} numberSelectedItems={selectedRuleIds.length} onGetBatchItemsPopoverContent={getBatchItemsPopoverContent} - onRefresh={handleRefreshData} + onRefresh={handleManualRefresh} isAutoRefreshOn={isRefreshOn} onRefreshSwitch={handleAutoRefreshSwitch} showBulkActions