Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Security Solution][Detections] Update read-only callouts from warning to info so they persist when dismissed #84904

Merged
Merged
1 change: 1 addition & 0 deletions x-pack/plugins/security_solution/common/test/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

// For the source of these roles please consult the PR these were introduced https://github.com/elastic/kibana/pull/81866#issue-511165754
export enum ROLES {
reader = 'reader',
t1_analyst = 't1_analyst',
t2_analyst = 't2_analyst',
hunter = 'hunter',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import { ROLES } from '../../common/test';
import { DETECTIONS_RULE_MANAGEMENT_URL, DETECTIONS_URL } from '../urls/navigation';
import { newRule } from '../objects/rule';
import { PAGE_TITLE } from '../screens/common/page';

import {
login,
loginAndWaitForPageWithoutDateRange,
waitForPageWithoutDateRange,
deleteRoleAndUser,
} from '../tasks/login';
import { waitForAlertsIndexToBeCreated } from '../tasks/alerts';
import { goToRuleDetails } from '../tasks/alerts_detection_rules';
import { createCustomRule, deleteCustomRule, removeSignalsIndex } from '../tasks/api_calls/rules';
import { getCallOut, waitForCallOutToBeShown, dismissCallOut } from '../tasks/common/callouts';

const loadPageAsReadOnlyUser = (url: string) => {
waitForPageWithoutDateRange(url, ROLES.reader);
waitForPageTitleToBeShown();
};

const reloadPage = () => {
cy.reload();
waitForPageTitleToBeShown();
};

const waitForPageTitleToBeShown = () => {
cy.get(PAGE_TITLE).should('be.visible');
};

describe('Detections > Callouts indicating read-only access to resources', () => {
const ALERTS_CALLOUT = 'read-only-access-to-alerts';
const RULES_CALLOUT = 'read-only-access-to-rules';

before(() => {
// First, we have to open the app on behalf of a priviledged user in order to initialize it.
// Otherwise the app will be disabled and show a "welcome"-like page.
loginAndWaitForPageWithoutDateRange(DETECTIONS_URL, ROLES.platform_engineer);
waitForAlertsIndexToBeCreated();

// After that we can login as a read-only user.
login(ROLES.reader);
});

after(() => {
deleteRoleAndUser(ROLES.reader);
deleteRoleAndUser(ROLES.platform_engineer);
removeSignalsIndex();
});

context('On Detections home page', () => {
beforeEach(() => {
loadPageAsReadOnlyUser(DETECTIONS_URL);
});

it('We show one primary callout', () => {
waitForCallOutToBeShown(ALERTS_CALLOUT, 'primary');
});

context('When a user clicks Dismiss on the callout', () => {
it('We hide it and persist the dismissal', () => {
waitForCallOutToBeShown(ALERTS_CALLOUT, 'primary');
dismissCallOut(ALERTS_CALLOUT);
reloadPage();
getCallOut(ALERTS_CALLOUT).should('not.exist');
});
});
});

context('On Rules Management page', () => {
beforeEach(() => {
loadPageAsReadOnlyUser(DETECTIONS_RULE_MANAGEMENT_URL);
});

it('We show one primary callout', () => {
waitForCallOutToBeShown(RULES_CALLOUT, 'primary');
});

context('When a user clicks Dismiss on the callout', () => {
it('We hide it and persist the dismissal', () => {
waitForCallOutToBeShown(RULES_CALLOUT, 'primary');
dismissCallOut(RULES_CALLOUT);
reloadPage();
getCallOut(RULES_CALLOUT).should('not.exist');
});
});
});

context('On Rule Details page', () => {
beforeEach(() => {
createCustomRule(newRule);

loadPageAsReadOnlyUser(DETECTIONS_RULE_MANAGEMENT_URL);
waitForPageTitleToBeShown();
goToRuleDetails();
});

afterEach(() => {
deleteCustomRule();
});

it('We show two primary callouts', () => {
waitForCallOutToBeShown(ALERTS_CALLOUT, 'primary');
waitForCallOutToBeShown(RULES_CALLOUT, 'primary');
});

context('When a user clicks Dismiss on the callouts', () => {
it('We hide them and persist the dismissal', () => {
waitForCallOutToBeShown(ALERTS_CALLOUT, 'primary');
waitForCallOutToBeShown(RULES_CALLOUT, 'primary');

dismissCallOut(ALERTS_CALLOUT);
reloadPage();

getCallOut(ALERTS_CALLOUT).should('not.exist');
getCallOut(RULES_CALLOUT).should('be.visible');

dismissCallOut(RULES_CALLOUT);
reloadPage();

getCallOut(ALERTS_CALLOUT).should('not.exist');
getCallOut(RULES_CALLOUT).should('not.exist');
});
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

export const CALLOUT = '[data-test-subj^="callout-"]';

export const callOutWithId = (id: string) => `[data-test-subj="callout-${id}"]`;

export const CALLOUT_DISMISS_BTN = '[data-test-subj^="callout-dismiss-"]';
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

export const PAGE_TITLE = '[data-test-subj="header-page-title"]';
24 changes: 24 additions & 0 deletions x-pack/plugins/security_solution/cypress/tasks/common/callouts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import { callOutWithId, CALLOUT_DISMISS_BTN } from '../../screens/common/callouts';

export const getCallOut = (id: string, options?: Cypress.Timeoutable) => {
return cy.get(callOutWithId(id), options);
};

export const waitForCallOutToBeShown = (id: string, color: string) => {
getCallOut(id, { timeout: 10000 })
.should('be.visible')
.should('have.class', `euiCallOut--${color}`);
};

export const dismissCallOut = (id: string) => {
getCallOut(id, { timeout: 10000 }).within(() => {
cy.get(CALLOUT_DISMISS_BTN).should('be.visible').click();
cy.root().should('not.exist');
});
};
5 changes: 5 additions & 0 deletions x-pack/plugins/security_solution/cypress/tasks/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,3 +239,8 @@ export const loginAndWaitForTimeline = (timelineId: string, role?: RolesType) =>
cy.get('[data-test-subj="headerGlobalNav"]');
cy.get(TIMELINE_FLYOUT_BODY).should('be.visible');
};

export const waitForPageWithoutDateRange = (url: string, role?: RolesType) => {
cy.visit(role ? getUrlWithRoute(role, url) : url);
cy.get('[data-test-subj="headerGlobalNav"]', { timeout: 120000 });
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: We should capture this selector in a constant, I think.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes +1, there's a room for cleanup in this file, I just didn't want to touch too many things in this PR.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason this timeout is set so high?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure. What I did is took an existing function loginAndWaitForPageWithoutDateRange (it's above this one) and removed the login part from it to be able to implement the tests.

loginAndWaitForPageWithoutDateRange with this timeout was introduced by @XavierM along with the "defaultCommandTimeout": 120000 in cypress.json. See here:

1216b0f

I guess that's some legacy, because the current "defaultCommandTimeout": 60000

};
4 changes: 4 additions & 0 deletions x-pack/plugins/security_solution/cypress/urls/navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
*/

export const DETECTIONS_URL = 'app/security/detections';
export const DETECTIONS_RULE_MANAGEMENT_URL = 'app/security/detections/rules';
export const detectionsRuleDetailsUrl = (ruleId: string) =>
`app/security/detections/rules/id/${ruleId}`;

export const CASES_URL = '/app/security/cases';
export const DETECTIONS = '/app/siem#/detections';
export const HOSTS_URL = '/app/security/hosts/allHosts';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import React, { FC, memo } from 'react';
import { EuiCallOut } from '@elastic/eui';

import { CallOutType, CallOutMessage } from './callout_types';
import { CallOutDescription } from './callout_description';
import { CallOutDismissButton } from './callout_dismiss_button';

export interface CallOutProps {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably don't need to export all these prop interfaces

message: CallOutMessage;
iconType?: string;
dismissButtonText?: string;
onDismiss?: (message: CallOutMessage) => void;
}

const CallOutComponent: FC<CallOutProps> = ({
message,
iconType,
dismissButtonText,
onDismiss,
}) => {
const { type, id, title } = message;
const finalIconType = iconType ?? getDefaultIconType(type);

return (
<EuiCallOut
color={type}
title={title}
iconType={finalIconType}
data-test-subj={`callout-${id}`}
data-test-messages={`[${id}]`}
>
<CallOutDescription messages={message} />
<CallOutDismissButton message={message} text={dismissButtonText} onClick={onDismiss} />
</EuiCallOut>
);
};

const getDefaultIconType = (type: CallOutType): string => {
switch (type) {
case 'primary':
return 'iInCircle';
case 'success':
return 'cheer';
case 'warning':
return 'help';
case 'danger':
return 'alert';
default:
return '';
}
};

export const CallOut = memo(CallOutComponent);
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import React, { FC } from 'react';
import { EuiDescriptionList } from '@elastic/eui';
import { CallOutMessage } from './callout_types';

export interface CallOutDescriptionProps {
messages: CallOutMessage | CallOutMessage[];
}

export const CallOutDescription: FC<CallOutDescriptionProps> = ({ messages }) => {
if (!Array.isArray(messages)) {
return messages.description;
}

if (messages.length < 1) {
return null;
}

return <EuiDescriptionList listItems={messages} />;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import React, { FC, useCallback } from 'react';
import { EuiButton } from '@elastic/eui';
import { noop } from 'lodash/fp';
import { CallOutMessage } from './callout_types';
import * as i18n from './translations';

export interface CallOutDismissButtonProps {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same export comment as above

message: CallOutMessage;
text?: string;
onClick?: (message: CallOutMessage) => void;
}

export const CallOutDismissButton: FC<CallOutDismissButtonProps> = ({
message,
text,
onClick = noop,
}) => {
const { type } = message;
const buttonColor = type === 'success' ? 'secondary' : type;
const buttonText = text ?? i18n.DISMISS_BUTTON;
const handleClick = useCallback(() => onClick(message), [onClick, message]);

return (
<EuiButton color={buttonColor} data-test-subj="callout-dismiss-btn" onClick={handleClick}>
{buttonText}
</EuiButton>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import React, { FC, memo } from 'react';

import { CallOutMessage } from './callout_types';
import { CallOut } from './callout';
import { useCallOutStorage } from './use_callout_storage';

export interface CallOutSwitcherProps {
namespace?: string;
condition: boolean;
message: CallOutMessage;
}

const CallOutSwitcherComponent: FC<CallOutSwitcherProps> = ({ namespace, condition, message }) => {
const { isVisible, dismiss } = useCallOutStorage([message], namespace);

const shouldRender = condition && isVisible(message);
return shouldRender ? <CallOut message={message} onDismiss={dismiss} /> : null;
};

export const CallOutSwitcher = memo(CallOutSwitcherComponent);
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

export type CallOutType = 'primary' | 'success' | 'warning' | 'danger';

export interface CallOutMessage {
type: CallOutType;
id: string;
title: string;
description: JSX.Element;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

export * from './callout_switcher';
export * from './callout_types';
export * from './callout';
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import { i18n } from '@kbn/i18n';

export const DISMISS_BUTTON = i18n.translate('xpack.securitySolution.callouts.dismissButton', {
defaultMessage: 'Dismiss',
});
Loading