[Security Solution] Prevent rules table auto-refreshing on window focus when auto-refresh disabled (#165250)

**Fixes: https://github.com/elastic/kibana/issues/165221**

## Summary

This PR prevents rules management table from being auto-refreshed on window focus or on reconnect if auto-refresh is disabled.

*Before:*

Auto-refresh is switched off

9536cc47-27b3-424a-a0a6-b3d32d40b9e2

Auto-refresh is switched off AND a rule is selected

dbf9c899-85cc-465b-8f78-303ebee0cece

*After:*

Auto-refresh is switched off

e7b98d15-5f33-44dc-b064-1014ec9382f9

Auto-refresh is switched off AND a rule is selected

39a8fa9d-a2e8-451c-879b-5c08492518c9


## Checklist

Delete any items that are not applicable to this PR.

- [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios


## Flaky test runner

[rules_table_auto_refresh.cy.ts (150 runs)](https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/3027) 🟢
This commit is contained in:
Maxim Palenov 2023-09-15 01:31:38 +02:00 committed by GitHub
parent df996362bd
commit ca244ecf34
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 135 additions and 60 deletions

View file

@ -295,6 +295,9 @@ export const RulesTableContextProvider = ({ children }: RulesTableContextProvide
pagination,
},
{
// We don't need refreshes on windows focus and reconnects if auto-refresh if off
refetchOnWindowFocus: isRefreshOn && !isActionInProgress,
refetchOnReconnect: isRefreshOn && !isActionInProgress,
refetchInterval: isRefreshOn && !isActionInProgress && autoRefreshSettings.value,
keepPreviousData: true, // Use this option so that the state doesn't jump between "success" and "loading" on page change
}

View file

@ -22,6 +22,7 @@ import {
expectNumberOfRules,
selectRulesByName,
getRuleRow,
setRulesTableAutoRefreshIntervalSetting,
} from '../../../../tasks/alerts_detection_rules';
import { login, visit, visitWithoutDateRange } from '../../../../tasks/login';
@ -30,8 +31,7 @@ import { createRule } from '../../../../tasks/api_calls/rules';
import { cleanKibana } from '../../../../tasks/common';
import { getNewRule } from '../../../../objects/rule';
const DEFAULT_RULE_REFRESH_INTERVAL_VALUE = 60000;
const NUM_OF_TEST_RULES = 6;
const RULES_TABLE_REFRESH_INTERVAL_MS = 60000;
// TODO: https://github.com/elastic/kibana/issues/161540
describe(
@ -42,52 +42,21 @@ describe(
cleanKibana();
login();
for (let i = 1; i <= NUM_OF_TEST_RULES; ++i) {
createRule(getNewRule({ name: `Test rule ${i}`, rule_id: `${i}`, enabled: false }));
}
setRulesTableAutoRefreshIntervalSetting({
enabled: true,
refreshInterval: RULES_TABLE_REFRESH_INTERVAL_MS,
});
createRule(getNewRule({ name: 'Test rule 1', rule_id: '1', enabled: false }));
});
beforeEach(() => {
login();
});
it('Auto refreshes rules', () => {
mockGlobalClock();
visitWithoutDateRange(DETECTIONS_RULE_MANAGEMENT_URL);
expectNumberOfRules(RULES_MANAGEMENT_TABLE, NUM_OF_TEST_RULES);
// // mock 1 minute passing to make sure refresh is conducted
cy.get(RULES_TABLE_AUTOREFRESH_INDICATOR).should('not.exist');
cy.tick(DEFAULT_RULE_REFRESH_INTERVAL_VALUE);
cy.get(RULES_TABLE_AUTOREFRESH_INDICATOR).should('be.visible');
cy.contains(REFRESH_RULES_STATUS, 'Updated now');
});
it('should prevent table from rules refetch if any rule selected', () => {
mockGlobalClock();
visitWithoutDateRange(DETECTIONS_RULE_MANAGEMENT_URL);
expectNumberOfRules(RULES_MANAGEMENT_TABLE, NUM_OF_TEST_RULES);
selectRulesByName(['Test rule 1']);
// mock 1 minute passing to make sure refresh is not conducted
cy.get(RULES_TABLE_AUTOREFRESH_INDICATOR).should('not.exist');
cy.tick(DEFAULT_RULE_REFRESH_INTERVAL_VALUE);
cy.get(RULES_TABLE_AUTOREFRESH_INDICATOR).should('not.exist');
// ensure rule is still selected
getRuleRow('Test rule 1').find(EUI_CHECKBOX).should('be.checked');
cy.get(REFRESH_RULES_STATUS).should('have.not.text', 'Updated now');
});
it('should disable auto refresh when any rule selected and enable it after rules unselected', () => {
it('gets deactivated when any rule selected and activated after rules unselected', () => {
visit(DETECTIONS_RULE_MANAGEMENT_URL);
expectNumberOfRules(RULES_MANAGEMENT_TABLE, NUM_OF_TEST_RULES);
expectNumberOfRules(RULES_MANAGEMENT_TABLE, 1);
// check refresh settings if it's enabled before selecting
expectAutoRefreshIsEnabled();
@ -103,21 +72,98 @@ describe(
expectAutoRefreshIsEnabled();
});
it('should not enable auto refresh after rules were unselected if auto refresh was disabled', () => {
visit(DETECTIONS_RULE_MANAGEMENT_URL);
describe('when enabled', () => {
beforeEach(() => {
mockGlobalClock();
visitWithoutDateRange(DETECTIONS_RULE_MANAGEMENT_URL);
expectNumberOfRules(RULES_MANAGEMENT_TABLE, NUM_OF_TEST_RULES);
expectNumberOfRules(RULES_MANAGEMENT_TABLE, 1);
});
disableAutoRefresh();
it('refreshes rules after refresh interval has passed', () => {
cy.get(RULES_TABLE_AUTOREFRESH_INDICATOR).should('not.exist');
cy.tick(RULES_TABLE_REFRESH_INTERVAL_MS);
cy.get(RULES_TABLE_AUTOREFRESH_INDICATOR).should('be.visible');
selectAllRules();
cy.contains(REFRESH_RULES_STATUS, 'Updated now');
});
expectAutoRefreshIsDeactivated();
it('refreshes rules on window focus', () => {
cy.tick(RULES_TABLE_REFRESH_INTERVAL_MS / 2);
clearAllRuleSelection();
cy.window().trigger('blur');
cy.window().trigger('focus');
// after all rules unselected, auto refresh should still be disabled
expectAutoRefreshIsDisabled();
cy.contains(REFRESH_RULES_STATUS, 'Updated now');
});
});
describe('when disabled', () => {
beforeEach(() => {
mockGlobalClock();
visitWithoutDateRange(DETECTIONS_RULE_MANAGEMENT_URL);
expectNumberOfRules(RULES_MANAGEMENT_TABLE, 1);
});
it('does NOT refresh rules after refresh interval has passed', () => {
disableAutoRefresh();
cy.tick(RULES_TABLE_REFRESH_INTERVAL_MS * 2); // Make sure enough time has passed to verify auto-refresh doesn't happen
cy.contains(REFRESH_RULES_STATUS, 'Updated 2 minutes ago');
});
it('does NOT refresh rules on window focus', () => {
disableAutoRefresh();
cy.tick(RULES_TABLE_REFRESH_INTERVAL_MS * 2); // Make sure enough time has passed to verify auto-refresh doesn't happen
cy.window().trigger('blur');
cy.window().trigger('focus');
// We need to make sure window focus event doesn't cause refetching. Without some delay
// the following expectations always pass even. It happens since 'focus' event gets handled
// in an async way so the status text is updated with some delay.
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(1000);
// By using a custom timeout make sure it doesn't wait too long due to global timeout configuration
// so the expected text appears after a refresh and the test passes while it shouldn't.
cy.contains(REFRESH_RULES_STATUS, 'Updated 2 minutes ago', { timeout: 10000 });
});
it('does NOT get enabled after rules were unselected', () => {
disableAutoRefresh();
cy.tick(RULES_TABLE_REFRESH_INTERVAL_MS * 2); // Make sure enough time has passed to verify auto-refresh doesn't happen
selectAllRules();
expectAutoRefreshIsDeactivated();
clearAllRuleSelection();
// after all rules unselected, auto refresh should still be disabled
expectAutoRefreshIsDisabled();
});
});
describe('when one rule is selected', () => {
it('does NOT refresh after refresh interval has passed', () => {
mockGlobalClock();
visitWithoutDateRange(DETECTIONS_RULE_MANAGEMENT_URL);
expectNumberOfRules(RULES_MANAGEMENT_TABLE, 1);
selectRulesByName(['Test rule 1']);
// mock 1 minute passing to make sure refresh is not conducted
cy.get(RULES_TABLE_AUTOREFRESH_INDICATOR).should('not.exist');
cy.tick(RULES_TABLE_REFRESH_INTERVAL_MS * 2); // Make sure enough time has passed
cy.get(RULES_TABLE_AUTOREFRESH_INDICATOR).should('not.exist');
// ensure rule is still selected
getRuleRow('Test rule 1').find(EUI_CHECKBOX).should('be.checked');
cy.get(REFRESH_RULES_STATUS).should('have.not.text', 'Updated now');
});
});
}
);

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import { DEFAULT_RULES_TABLE_REFRESH_SETTING } from '@kbn/security-solution-plugin/common/constants';
import {
COLLAPSED_ACTION_BTN,
CUSTOM_RULES_BTN,
@ -64,6 +65,7 @@ import { PAGE_CONTENT_SPINNER } from '../screens/common/page';
import { goToRuleEditSettings } from './rule_details';
import { goToActionsStepTab } from './create_new_rule';
import { setKibanaSetting } from './api_calls/kibana_advanced_settings';
export const getRulesManagementTableRows = () => cy.get(RULES_MANAGEMENT_TABLE).find(RULES_ROW);
@ -516,3 +518,29 @@ const unselectRuleByName = (ruleName: string) => {
cy.log(`Make sure rule "${ruleName}" has been unselected`);
getRuleRow(ruleName).find(EUI_CHECKBOX).should('not.be.checked');
};
/**
* Set Kibana `securitySolution:rulesTableRefresh` setting looking like
*
* ```
* { "on": true, "value": 60000 }
* ```
*
* @param enabled whether the auto-refresh is enabled
* @param refreshInterval refresh interval in milliseconds
*/
export const setRulesTableAutoRefreshIntervalSetting = ({
enabled,
refreshInterval,
}: {
enabled: boolean;
refreshInterval: number; // milliseconds
}) => {
setKibanaSetting(
DEFAULT_RULES_TABLE_REFRESH_SETTING,
JSON.stringify({
on: enabled,
value: refreshInterval,
})
);
};

View file

@ -5,30 +5,27 @@
* 2.0.
*/
import { SECURITY_SOLUTION_SHOW_RELATED_INTEGRATIONS_ID } from '@kbn/management-settings-ids';
import { ENABLE_EXPANDABLE_FLYOUT_SETTING } from '@kbn/security-solution-plugin/common/constants';
import { rootRequest } from '../common';
const kibanaSettings = (body: Cypress.RequestBody) => {
export const setKibanaSetting = (key: string, value: boolean | number | string) => {
rootRequest({
method: 'POST',
url: 'internal/kibana/settings',
body,
body: { changes: { [key]: value } },
headers: { 'kbn-xsrf': 'cypress-creds', 'x-elastic-internal-origin': 'security-solution' },
});
};
const relatedIntegrationsBody = (status: boolean): Cypress.RequestBody => {
return { changes: { 'securitySolution:showRelatedIntegrations': status } };
};
export const enableRelatedIntegrations = () => {
kibanaSettings(relatedIntegrationsBody(true));
setKibanaSetting(SECURITY_SOLUTION_SHOW_RELATED_INTEGRATIONS_ID, true);
};
export const disableRelatedIntegrations = () => {
kibanaSettings(relatedIntegrationsBody(false));
setKibanaSetting(SECURITY_SOLUTION_SHOW_RELATED_INTEGRATIONS_ID, false);
};
export const disableExpandableFlyout = () => {
const body = { changes: { 'securitySolution:enableExpandableFlyout': false } };
kibanaSettings(body);
setKibanaSetting(ENABLE_EXPANDABLE_FLYOUT_SETTING, false);
};

View file

@ -37,6 +37,7 @@
"@kbn/config-schema",
"@kbn/lists-plugin",
"@kbn/securitysolution-list-constants",
"@kbn/security-plugin"
"@kbn/security-plugin",
"@kbn/management-settings-ids"
]
}