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] Guided onboarding, alerts & cases design updates #144249

Merged
merged 57 commits into from
Nov 9, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
636d34e
next buttons
stephmilovic Oct 28, 2022
c48c2ca
set initial value and expose submit function from getCreateCaseFlyout
stephmilovic Oct 28, 2022
50fddb9
Merge branch 'main' into guided_onboarding_design
stephmilovic Oct 31, 2022
a19b090
push
stephmilovic Oct 31, 2022
50fe19c
button focus
stephmilovic Oct 31, 2022
dea3873
Merge branch 'main' into guided_onboarding_design
stephmilovic Nov 1, 2022
095a436
fixing
stephmilovic Nov 1, 2022
b17c725
merge main
stephmilovic Nov 1, 2022
bbe2a1d
fix a couple tests
stephmilovic Nov 1, 2022
17e8c0e
fix
stephmilovic Nov 1, 2022
d1932bc
ok
stephmilovic Nov 1, 2022
bdc4cb4
useAddToCaseActions tests
stephmilovic Nov 1, 2022
810f0ea
make sure AlertsCasesTourSteps is used everywhere
stephmilovic Nov 1, 2022
b1d12e4
add tests to cases area
stephmilovic Nov 1, 2022
80ec3c1
rm comment
stephmilovic Nov 1, 2022
c969319
add import
stephmilovic Nov 1, 2022
47eb7e0
add test in cases
stephmilovic Nov 2, 2022
a134f3f
Merge branch 'main' into guided_onboarding_design
stephmilovic Nov 2, 2022
8a8333f
add fun markdown
stephmilovic Nov 2, 2022
37988ed
test fix
stephmilovic Nov 2, 2022
ad08ae6
Merge branch 'main' into guided_onboarding_design
kibanamachine Nov 2, 2022
6f4e9c3
Merge branch 'main' into guided_onboarding_design
stephmilovic Nov 2, 2022
afc9902
Merge branch 'guided_onboarding_design' of github.com:stephmilovic/ki…
stephmilovic Nov 2, 2022
f6b59ae
codeowners
stephmilovic Nov 2, 2022
5e24730
useMemo on context provider
stephmilovic Nov 2, 2022
f98b2ea
fix for mr michael
stephmilovic Nov 2, 2022
d854113
Merge branch 'main' into guided_onboarding_design
stephmilovic Nov 3, 2022
a1d439c
isSubmit to autoSubmit
stephmilovic Nov 3, 2022
aa83e89
autoSubmit to props
stephmilovic Nov 3, 2022
64aaf95
move bool
stephmilovic Nov 3, 2022
d0e8a42
fix keys
stephmilovic Nov 3, 2022
506da35
Merge branch 'main' into guided_onboarding_design
stephmilovic Nov 4, 2022
442f5ad
fix tests
stephmilovic Nov 4, 2022
6c22869
end tour on case details page
stephmilovic Nov 7, 2022
b2bf83d
Merge branch 'main' into guided_onboarding_design
stephmilovic Nov 7, 2022
1ba3eed
test fix
stephmilovic Nov 7, 2022
4b77eeb
more better
stephmilovic Nov 7, 2022
15740ac
better now
stephmilovic Nov 8, 2022
53f2228
Merge branch 'main' into guided_onboarding_design
stephmilovic Nov 8, 2022
fe6ef7b
finish tour on case details page vs onclick of the link
stephmilovic Nov 8, 2022
95403c2
revert submit callback in create case
stephmilovic Nov 8, 2022
6d04afb
revert more cases
stephmilovic Nov 8, 2022
1f821a4
test fix
stephmilovic Nov 8, 2022
0a26e9d
add tests
stephmilovic Nov 8, 2022
b26a7bc
fix side effects and tests
stephmilovic Nov 8, 2022
3dcb219
just a fix up
stephmilovic Nov 8, 2022
5f9c56d
adds tests for tour context in related_cases
stephmilovic Nov 8, 2022
3cc0718
data-test-subj to tour-step
stephmilovic Nov 8, 2022
3b91a69
tests for cases_tour_steps
stephmilovic Nov 8, 2022
2df8a1c
more test
stephmilovic Nov 8, 2022
02b28b1
pr fixes
stephmilovic Nov 9, 2022
c7615c6
fix type
stephmilovic Nov 9, 2022
92d0ac8
Merge branch 'main' into guided_onboarding_design
stephmilovic Nov 9, 2022
b37e65c
setStep function added and type fix
stephmilovic Nov 9, 2022
1a0c157
fix whoops
stephmilovic Nov 9, 2022
4156066
Merge branch 'main' into guided_onboarding_design
stephmilovic Nov 9, 2022
48b739a
fix test
stephmilovic Nov 9, 2022
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
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,7 @@
/x-pack/plugins/security_solution/cypress/tasks/network @elastic/security-threat-hunting-explore
/x-pack/plugins/security_solution/cypress/upgrade_e2e/threat_hunting/cases @elastic/security-threat-hunting-explore

/x-pack/plugins/security_solution/public/common/components/guided_onboarding_tour @elastic/security-threat-hunting-explore
/x-pack/plugins/security_solution/public/common/components/charts @elastic/security-threat-hunting-explore
/x-pack/plugins/security_solution/public/common/components/header_page @elastic/security-threat-hunting-explore
/x-pack/plugins/security_solution/public/common/components/header_section @elastic/security-threat-hunting-explore
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import styled, { createGlobalStyle } from 'styled-components';
import { EuiFlyout, EuiFlyoutHeader, EuiTitle, EuiFlyoutBody } from '@elastic/eui';

import { QueryClientProvider } from '@tanstack/react-query';
import type { CasePostRequest } from '../../../../common/api';
import * as i18n from '../translations';
import type { Case } from '../../../../common/ui/types';
import { CreateCaseForm } from '../form';
Expand All @@ -26,6 +27,7 @@ export interface CreateCaseFlyoutProps {
onSuccess?: (theCase: Case) => Promise<void>;
attachments?: CaseAttachmentsWithoutOwner;
headerContent?: React.ReactNode;
initialValue?: Pick<CasePostRequest, 'title' | 'description'>;
}

const StyledFlyout = styled(EuiFlyout)`
Expand Down Expand Up @@ -72,7 +74,7 @@ const FormWrapper = styled.div`
`;

export const CreateCaseFlyout = React.memo<CreateCaseFlyoutProps>(
({ afterCaseCreated, onClose, onSuccess, attachments, headerContent }) => {
({ afterCaseCreated, attachments, headerContent, initialValue, onClose, onSuccess }) => {
const handleCancel = onClose || function () {};
const handleOnSuccess = onSuccess || async function () {};

Expand All @@ -81,6 +83,7 @@ export const CreateCaseFlyout = React.memo<CreateCaseFlyoutProps>(
<GlobalStyle />
<StyledFlyout
onClose={onClose}
tour-step="create-case-flyout"
Copy link
Contributor Author

Choose a reason for hiding this comment

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

These are labels for my tour step anchor. Previously I was using data-test-subj="create-case-flyout" but it makes me nervous to call on a test ref in production.

data-test-subj="create-case-flyout"
// maskProps is needed in order to apply the z-index to the parent overlay element, not to the flyout only
maskProps={{ className: maskOverlayClassName }}
Expand All @@ -99,6 +102,7 @@ export const CreateCaseFlyout = React.memo<CreateCaseFlyoutProps>(
onCancel={handleCancel}
onSuccess={handleOnSuccess}
withSteps={false}
initialValue={initialValue}
/>
</FormWrapper>
</StyledEuiFlyoutBody>
Expand Down
32 changes: 31 additions & 1 deletion x-pack/plugins/cases/public/components/create/form.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import React from 'react';
import { mount } from 'enzyme';
import { act, render } from '@testing-library/react';
import { act, render, within } from '@testing-library/react';
import { licensingMock } from '@kbn/licensing-plugin/public/mocks';

import { NONE_CONNECTOR_ID } from '../../../common/api';
Expand Down Expand Up @@ -182,4 +182,34 @@ describe('CreateCaseForm', () => {

expect(result.getByTestId('createCaseAssigneesComboBox')).toBeInTheDocument();
});

it('should not prefill the form when no initialValue provided', () => {
const { getByTestId } = render(
<MockHookWrapperComponent>
<CreateCaseForm {...casesFormProps} />
</MockHookWrapperComponent>
);

const titleInput = within(getByTestId('caseTitle')).getByTestId('input');
const descriptionInput = within(getByTestId('caseDescription')).getByRole('textbox');
expect(titleInput).toHaveValue('');
expect(descriptionInput).toHaveValue('');
});

it('should prefill the form when provided with initialValue', () => {
const { getByTestId } = render(
<MockHookWrapperComponent>
<CreateCaseForm
{...casesFormProps}
initialValue={{ title: 'title', description: 'description' }}
/>
</MockHookWrapperComponent>
);

const titleInput = within(getByTestId('caseTitle')).getByTestId('input');
const descriptionInput = within(getByTestId('caseDescription')).getByRole('textbox');

expect(titleInput).toHaveValue('title');
expect(descriptionInput).toHaveValue('description');
});
});
6 changes: 5 additions & 1 deletion x-pack/plugins/cases/public/components/create/form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { Tags } from './tags';
import { Connector } from './connector';
import * as i18n from './translations';
import { SyncAlertsToggle } from './sync_alerts_toggle';
import type { ActionConnector } from '../../../common/api';
import type { ActionConnector, CasePostRequest } from '../../../common/api';
import type { Case } from '../../containers/types';
import type { CasesTimelineIntegration } from '../timeline_context';
import { CasesTimelineIntegrationProvider } from '../timeline_context';
Expand Down Expand Up @@ -70,6 +70,7 @@ export interface CreateCaseFormProps extends Pick<Partial<CreateCaseFormFieldsPr
) => Promise<void>;
timelineIntegration?: CasesTimelineIntegration;
attachments?: CaseAttachmentsWithoutOwner;
initialValue?: Pick<CasePostRequest, 'title' | 'description'>;
}

const empty: ActionConnector[] = [];
Expand All @@ -79,6 +80,7 @@ export const CreateCaseFormFields: React.FC<CreateCaseFormFieldsProps> = React.m
const { isSyncAlertsEnabled, caseAssignmentAuthorized } = useCasesFeatures();

const { owner } = useCasesContext();

const availableOwners = useAvailableCasesOwners();
const canShowCaseSolutionSelection = !owner.length && availableOwners.length;

Expand Down Expand Up @@ -181,12 +183,14 @@ export const CreateCaseForm: React.FC<CreateCaseFormProps> = React.memo(
onSuccess,
timelineIntegration,
attachments,
initialValue,
}) => (
<CasesTimelineIntegrationProvider timelineIntegration={timelineIntegration}>
<FormContext
afterCaseCreated={afterCaseCreated}
onSuccess={onSuccess}
attachments={attachments}
initialValue={initialValue}
>
<CreateCaseFormFields
connectors={empty}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { usePostCase } from '../../containers/use_post_case';
import { usePostPushToService } from '../../containers/use_post_push_to_service';

import type { Case } from '../../containers/types';
import type { CasePostRequest } from '../../../common/api';
import { CaseSeverity, NONE_CONNECTOR_ID } from '../../../common/api';
import type { UseCreateAttachments } from '../../containers/use_create_attachments';
import { useCreateAttachments } from '../../containers/use_create_attachments';
Expand Down Expand Up @@ -44,13 +45,15 @@ interface Props {
children?: JSX.Element | JSX.Element[];
onSuccess?: (theCase: Case) => Promise<void>;
attachments?: CaseAttachmentsWithoutOwner;
initialValue?: Pick<CasePostRequest, 'title' | 'description'>;
}

export const FormContext: React.FC<Props> = ({
afterCaseCreated,
children,
onSuccess,
attachments,
initialValue,
}) => {
const { data: connectors = [], isLoading: isLoadingConnectors } = useGetConnectors();
const { owner, appId } = useCasesContext();
Expand Down Expand Up @@ -128,7 +131,7 @@ export const FormContext: React.FC<Props> = ({
);

const { form } = useForm<FormProps>({
defaultValue: initialCaseValue,
defaultValue: { ...initialCaseValue, ...initialValue },
options: { stripEmptyFields: false },
schema,
onSubmit: submitCase,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const SubmitCaseButtonComponent: React.FC = () => {

return (
<EuiButton
tour-step="create-case-submit"
data-test-subj="create-case-submit"
fill
iconType="plusInCircle"
Expand Down
93 changes: 93 additions & 0 deletions x-pack/plugins/security_solution/public/cases/pages/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*
* 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 React from 'react';
import { Cases } from '.';
import { Router } from 'react-router-dom';
import { render } from '@testing-library/react';
import { TestProviders } from '../../common/mock';
import { useTourContext } from '../../common/components/guided_onboarding_tour';
import {
AlertsCasesTourSteps,
SecurityStepId,
} from '../../common/components/guided_onboarding_tour/tour_config';

jest.mock('../../common/components/guided_onboarding_tour');

type Action = 'PUSH' | 'POP' | 'REPLACE';
const pop: Action = 'POP';
const location = {
pathname: '/network',
search: '',
state: '',
hash: '',
};
const mockHistory = {
length: 2,
location,
action: pop,
push: jest.fn(),
replace: jest.fn(),
go: jest.fn(),
goBack: jest.fn(),
goForward: jest.fn(),
block: jest.fn(),
createHref: jest.fn(),
listen: jest.fn(),
};

describe('cases page in security', () => {
const endTourStep = jest.fn();
beforeEach(() => {
(useTourContext as jest.Mock).mockReturnValue({
activeStep: AlertsCasesTourSteps.viewCase,
incrementStep: () => null,
endTourStep,
isTourShown: () => true,
});
jest.clearAllMocks();
});
it('calls endTour on cases details page when SecurityStepId.alertsCases tour is active and step is AlertsCasesTourSteps.viewCase', () => {
render(
<Router history={mockHistory}>
<Cases />
</Router>,
{ wrapper: TestProviders }
);
expect(endTourStep).toHaveBeenCalledWith(SecurityStepId.alertsCases);
});
it('does not call endTour on cases details page when SecurityStepId.alertsCases tour is not active', () => {
(useTourContext as jest.Mock).mockReturnValue({
activeStep: AlertsCasesTourSteps.viewCase,
incrementStep: () => null,
endTourStep,
isTourShown: () => false,
});
render(
<Router history={mockHistory}>
<Cases />
</Router>,
{ wrapper: TestProviders }
);
expect(endTourStep).not.toHaveBeenCalled();
});
it('does not call endTour on cases details page when SecurityStepId.alertsCases tour is active and step is not AlertsCasesTourSteps.viewCase', () => {
(useTourContext as jest.Mock).mockReturnValue({
activeStep: AlertsCasesTourSteps.expandEvent,
incrementStep: () => null,
endTourStep,
isTourShown: () => true,
});
render(
<Router history={mockHistory}>
<Cases />
</Router>,
{ wrapper: TestProviders }
);
expect(endTourStep).not.toHaveBeenCalled();
});
});
17 changes: 16 additions & 1 deletion x-pack/plugins/security_solution/public/cases/pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,14 @@
* 2.0.
*/

import React, { useCallback, useRef } from 'react';
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
import { useDispatch } from 'react-redux';
import type { CaseViewRefreshPropInterface } from '@kbn/cases-plugin/common';
import { useTourContext } from '../../common/components/guided_onboarding_tour';
import {
AlertsCasesTourSteps,
SecurityStepId,
} from '../../common/components/guided_onboarding_tour/tour_config';
import { TimelineId } from '../../../common/types/timeline';

import { getRuleDetailsUrl, useFormatUrl } from '../../common/components/link_to';
Expand Down Expand Up @@ -91,6 +96,16 @@ const CaseContainerComponent: React.FC = () => {
}, [dispatch]);

const refreshRef = useRef<CaseViewRefreshPropInterface>(null);
const { activeStep, endTourStep, isTourShown } = useTourContext();

const isTourActive = useMemo(
() => activeStep === AlertsCasesTourSteps.viewCase && isTourShown(SecurityStepId.alertsCases),
[activeStep, isTourShown]
);

useEffect(() => {
if (isTourActive) endTourStep(SecurityStepId.alertsCases);
}, [endTourStep, isTourActive]);

return (
<SecuritySolutionPageWrapper noPadding>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,11 @@ import type { SearchHit } from '../../../../common/search_strategy';
import { getMitreComponentParts } from '../../../detections/mitre/get_mitre_threat_component';
import { GuidedOnboardingTourStep } from '../guided_onboarding_tour/tour_step';
import { isDetectionsAlertsTable } from '../top_n/helpers';
import { getTourAnchor, SecurityStepId } from '../guided_onboarding_tour/tour_config';
import {
AlertsCasesTourSteps,
getTourAnchor,
SecurityStepId,
} from '../guided_onboarding_tour/tour_config';
import type { AlertRawEventData } from './osquery_tab';
import { useOsqueryTab } from './osquery_tab';
import { EventFieldsBrowser } from './event_fields_browser';
Expand Down Expand Up @@ -448,8 +452,8 @@ const EventDetailsComponent: React.FC<Props> = ({
return (
<GuidedOnboardingTourStep
isTourAnchor={isTourAnchor}
step={3}
stepId={SecurityStepId.alertsCases}
step={AlertsCasesTourSteps.reviewAlertDetailsFlyout}
tourId={SecurityStepId.alertsCases}
>
<StyledEuiTabbedContent
{...tourAnchor}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,15 @@ interface Props {
renderContent: () => ReactNode;
extraAction?: EuiAccordionProps['extraAction'];
onToggle?: EuiAccordionProps['onToggle'];
forceState?: EuiAccordionProps['forceState'];
}

/**
* A special accordion that is used in the Insights section on the alert flyout.
* It wraps logic and custom styling around the loading, error and success states of an insight section.
*/
export const InsightAccordion = React.memo<Props>(
({ prefix, state, text, renderContent, onToggle = noop, extraAction }) => {
({ prefix, state, text, renderContent, onToggle = noop, extraAction, forceState }) => {
const accordionId = useGeneratedHtmlId({ prefix });

switch (state) {
Expand Down Expand Up @@ -62,11 +63,14 @@ export const InsightAccordion = React.memo<Props>(
// The accordion can display the content now
return (
<StyledAccordion
tour-step={`${prefix}-accordion`}
data-test-subj={`${prefix}-accordion`}
id={accordionId}
buttonContent={text}
onToggle={onToggle}
paddingSize="l"
extraAction={extraAction}
forceState={forceState}
>
{renderContent()}
</StyledAccordion>
Expand Down
Loading