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][Cases] Re-enable timeline functionality #96496

Merged
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 22 additions & 2 deletions x-pack/plugins/cases/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,16 @@ cases: CasesUiStart;
cases.getCreateCase({
onCancel: handleSetIsCancel,
onSuccess,
timelineIntegration?: {
plugins: {
parsingPlugin,
processingPluginRenderer,
uiPlugin,
},
hooks: {
useInsertTimeline,
},
},
})
```
##### Methods:
Expand Down Expand Up @@ -76,10 +86,15 @@ Arguments:
|onComponentInitialized?|`() => void;` callback when component has initialized
|onConfigureCasesNavClick|`(ev: React.MouseEvent) => void;` callback for configure case nav click
|onRuleDetailsClick|`(ruleId: string, null, undefined) => void;` callback for rule details nav click
|renderInvestigateInTimelineActionComponent?|: `(alertIds: string[]) => JSX.Element;` space to render `InvestigateInTimelineActionComponent`
|renderTimelineDetailsPanel?|: `() => JSX.Element;` space to render `TimelineDetailsPanel`
|showAlertDetails|: `(alertId: string, index: string) => void;` callback to show alert details
|subCaseId?|: `string;` subcase id
|timelineIntegration?.editor_plugins|: Plugins needed for integrating timeline into markdown editor.
|timelineIntegration?.editor_plugins.parsingPlugin|: `Plugin;`
|timelineIntegration?.editor_plugins.processingPluginRenderer|: `React.FC<TimelineProcessingPluginRendererProps & { position: EuiMarkdownAstNodePosition }>`
|timelineIntegration?.editor_plugins.uiPlugin?|: `EuiMarkdownEditorUiPlugin`
|timelineIntegration?.hooks.useInsertTimeline|: `(value: string, onChange: (newValue: string) => void): UseInsertTimelineReturn`
|timelineIntegration?.ui?.renderInvestigateInTimelineActionComponent?|: `(alertIds: string[]) => JSX.Element;` space to render `InvestigateInTimelineActionComponent`
|timelineIntegration?.ui?renderTimelineDetailsPanel?|: `() => JSX.Element;` space to render `TimelineDetailsPanel`
|useFetchAlertData|: `(alertIds: string[]) => [boolean, Record<string, Ecs>];` fetch alerts
|userCanCrud|: `boolean;` user permissions to crud

Expand All @@ -94,6 +109,11 @@ Arguments:
|afterCaseCreated?|`(theCase: Case) => Promise<void>;` callback passing newly created case before pushCaseToExternalService is called
|onCancel|`() => void;` callback when create case is canceled
|onSuccess|`(theCase: Case) => Promise<void>;` callback passing newly created case after pushCaseToExternalService is called
|timelineIntegration?.editor_plugins|: Plugins needed for integrating timeline into markdown editor.
|timelineIntegration?.editor_plugins.parsingPlugin|: `Plugin;`
|timelineIntegration?.editor_plugins.processingPluginRenderer|: `React.FC<TimelineProcessingPluginRendererProps & { position: EuiMarkdownAstNodePosition }>`
|timelineIntegration?.editor_plugins.uiPlugin?|: `EuiMarkdownEditorUiPlugin`
|timelineIntegration?.hooks.useInsertTimeline|: `(value: string, onChange: (newValue: string) => void): UseInsertTimelineReturn`

UI component:
![Create Component][create-img]
Expand Down
33 changes: 33 additions & 0 deletions x-pack/plugins/cases/public/components/__mock__/timeline.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* 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 { useTimelineContext } from '../timeline_context/use_timeline_context';
jest.mock('../timeline_context');

const mockTimelineComponent = (name: string) => <span data-test-subj={name}>{name}</span>;

export const timelineIntegrationMock = {
editor_plugins: {
parsingPlugin: jest.fn(),
processingPluginRenderer: () => mockTimelineComponent('plugin-renderer'),
uiPlugin: {
name: 'mock-timeline',
button: { label: 'mock-timeline-button', iconType: 'mock-timeline-icon' },
editor: () => mockTimelineComponent('plugin-timeline-editor'),
},
},
hooks: {
useInsertTimeline: jest.fn(),
},
ui: {
renderInvestigateInTimelineActionComponent: () =>
mockTimelineComponent('investigate-in-timeline'),
renderTimelineDetailsPanel: () => mockTimelineComponent('timeline-details-panel'),
},
};

export const useTimelineContextMock = useTimelineContext as jest.Mock;
61 changes: 31 additions & 30 deletions x-pack/plugins/cases/public/components/add_comment/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,20 @@
import React from 'react';
import { mount } from 'enzyme';
import { waitFor, act } from '@testing-library/react';
// import { noop } from 'lodash/fp';
import { noop } from 'lodash/fp';

import { TestProviders } from '../../common/mock';
import { Router, routeData, mockHistory, mockLocation } from '../__mock__/router';

import { CommentRequest, CommentType } from '../../../common';
// TODO: Timeline Integration
// import { useInsertTimeline } from '../use_insert_timeline';
import { usePostComment } from '../../containers/use_post_comment';
import { AddComment, AddCommentRefObject } from '.';
import { CasesTimelineIntegrationProvider } from '../timeline_context';
import { timelineIntegrationMock } from '../__mock__/timeline';

jest.mock('../../containers/use_post_comment');
// TODO: Timeline Integration
// jest.mock('../use_insert_timeline');

const usePostCommentMock = usePostComment as jest.Mock;
// TODO: Timeline Integration
// const useInsertTimelineMock = useInsertTimeline as jest.Mock;
const onCommentSaving = jest.fn();
const onCommentPosted = jest.fn();
const postComment = jest.fn();
Expand Down Expand Up @@ -150,27 +146,32 @@ describe('AddComment ', () => {
);
});

// TODO: Should re-enable the insert timeline action
// xit('it should insert a timeline', async () => {
// let attachTimeline = noop;
// useInsertTimelineMock.mockImplementation((comment, onTimelineAttached) => {
// attachTimeline = onTimelineAttached;
// });

// const wrapper = mount(
// <TestProviders>
// <Router history={mockHistory}>
// <AddComment {...{ ...addCommentProps }} />
// </Router>
// </TestProviders>
// );

// act(() => {
// attachTimeline('[title](url)');
// });

// await waitFor(() => {
// expect(wrapper.find(`[data-test-subj="add-comment"] textarea`).text()).toBe('[title](url)');
// });
// });
it('it should insert a timeline', async () => {
const useInsertTimelineMock = jest.fn();
let attachTimeline = noop;
useInsertTimelineMock.mockImplementation((comment, onTimelineAttached) => {
attachTimeline = onTimelineAttached;
});

const mockTimelineIntegration = { ...timelineIntegrationMock };
mockTimelineIntegration.hooks.useInsertTimeline = useInsertTimelineMock;

const wrapper = mount(
<TestProviders>
<CasesTimelineIntegrationProvider timelineIntegration={mockTimelineIntegration}>
<Router history={mockHistory}>
<AddComment {...{ ...addCommentProps }} />
</Router>
</CasesTimelineIntegrationProvider>
</TestProviders>
);

act(() => {
attachTimeline('[title](url)');
});

await waitFor(() => {
expect(wrapper.find(`[data-test-subj="add-comment"] textarea`).text()).toBe('[title](url)');
});
});
});
14 changes: 2 additions & 12 deletions x-pack/plugins/cases/public/components/add_comment/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,7 @@ import { Form, useForm, UseField, useFormData } from '../../common/shared_import

import * as i18n from './translations';
import { schema, AddCommentFormSchema } from './schema';

// TODO: Handle use insert in timeline
// import { useInsertTimeline } from '../use_insert_timeline';

import { InsertTimeline } from '../insert_timeline';
const MySpinner = styled(EuiLoadingSpinner)`
position: absolute;
top: 50%;
Expand Down Expand Up @@ -73,14 +70,6 @@ export const AddComment = React.memo(
addQuote,
}));

// TODO: Timeline integration
// const onTimelineAttached = useCallback(
// (newValue: string) => setFieldValue(fieldName, newValue),
// [setFieldValue]
// );

// useInsertTimeline(comment ?? '', onTimelineAttached);

const onSubmit = useCallback(async () => {
const { isValid, data } = await submit();
if (isValid) {
Expand Down Expand Up @@ -123,6 +112,7 @@ export const AddComment = React.memo(
),
}}
/>
<InsertTimeline fieldName="comment" />
</Form>
</span>
);
Expand Down
2 changes: 1 addition & 1 deletion x-pack/plugins/cases/public/components/all_cases/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,7 @@ export const AllCases = React.memo<AllCasesProps>(
newFilterOptions.status &&
newFilterOptions.status === CaseStatuses['in-progress']
) {
setQueryParams({ sortField: SortFieldCase.updatedAt });
setQueryParams({ sortField: SortFieldCase.createdAt });
}
setFilters(newFilterOptions);
refreshCases(false);
Expand Down
65 changes: 32 additions & 33 deletions x-pack/plugins/cases/public/components/case_view/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,15 @@ import {
import { StatusActionButton } from '../status/button';
import * as i18n from './translations';
import { Ecs } from '../../common/ecs_types';
import { CasesTimelineIntegration, CasesTimelineIntegrationProvider } from '../timeline_context';
import { useTimelineContext } from '../timeline_context/use_timeline_context';

// TODO: All below imports depend on Timeline or SecuritySolution in some form or another
// import { SpyRoute } from '../../../common/utils/route/spy_routes';

const gutterTimeline = '70px'; // seems to be a timeline reference from the original file
export interface CaseViewProps {

export interface CaseViewComponentProps {
allCasesHref: string;
backToAllCasesOnClick: (ev: MouseEvent) => void;
caseDetailsHref: string;
Expand All @@ -56,14 +59,15 @@ export interface CaseViewProps {
onComponentInitialized?: () => void;
onConfigureCasesNavClick: (ev: React.MouseEvent) => void;
onRuleDetailsClick: (ruleId: string | null | undefined) => void;
renderInvestigateInTimelineActionComponent?: (alertIds: string[]) => JSX.Element;
renderTimelineDetailsPanel?: () => JSX.Element;
showAlertDetails: (alertId: string, index: string) => void;
subCaseId?: string;
useFetchAlertData: (alertIds: string[]) => [boolean, Record<string, Ecs>];
userCanCrud: boolean;
}

export interface CaseViewProps extends CaseViewComponentProps {
timelineIntegration?: CasesTimelineIntegration;
}
export interface OnUpdateFields {
key: keyof Case;
value: Case[keyof Case];
Expand All @@ -87,7 +91,7 @@ const MyEuiHorizontalRule = styled(EuiHorizontalRule)`
}
`;

export interface CaseComponentProps extends CaseViewProps {
export interface CaseComponentProps extends CaseViewComponentProps {
fetchCase: () => void;
caseData: Case;
updateCase: (newCase: Case) => void;
Expand All @@ -107,8 +111,6 @@ export const CaseComponent = React.memo<CaseComponentProps>(
onComponentInitialized,
onConfigureCasesNavClick,
onRuleDetailsClick,
renderInvestigateInTimelineActionComponent,
renderTimelineDetailsPanel,
showAlertDetails,
subCaseId,
updateCase,
Expand All @@ -117,6 +119,7 @@ export const CaseComponent = React.memo<CaseComponentProps>(
}) => {
const [initLoadingData, setInitLoadingData] = useState(true);
const init = useRef(true);
const timelineUi = useTimelineContext()?.ui;

const {
caseUserActions,
Expand Down Expand Up @@ -404,9 +407,6 @@ export const CaseComponent = React.memo<CaseComponentProps>(
isLoadingUserActions={isLoadingUserActions}
onShowAlertDetails={onShowAlertDetails}
onUpdateField={onUpdateField}
renderInvestigateInTimelineActionComponent={
renderInvestigateInTimelineActionComponent
}
updateCase={updateCase}
useFetchAlertData={useFetchAlertData}
userCanCrud={userCanCrud}
Expand Down Expand Up @@ -476,7 +476,7 @@ export const CaseComponent = React.memo<CaseComponentProps>(
</EuiFlexGroup>
</MyWrapper>
</WhitePageWrapper>
{renderTimelineDetailsPanel ? renderTimelineDetailsPanel() : null}
{timelineUi?.renderTimelineDetailsPanel ? timelineUi.renderTimelineDetailsPanel() : null}
{/* TODO: Determine spyroute changes */}
{/* <SpyRoute state={spyState} pageName={SecurityPageName.case} /> */}
</>
Expand All @@ -496,10 +496,9 @@ export const CaseView = React.memo(
onComponentInitialized,
onConfigureCasesNavClick,
onRuleDetailsClick,
renderInvestigateInTimelineActionComponent,
renderTimelineDetailsPanel,
showAlertDetails,
subCaseId,
timelineIntegration,
useFetchAlertData,
userCanCrud,
}: CaseViewProps) => {
Expand All @@ -519,27 +518,27 @@ export const CaseView = React.memo(

return (
data && (
<CaseComponent
allCasesHref={allCasesHref}
backToAllCasesOnClick={backToAllCasesOnClick}
caseData={data}
caseDetailsHref={caseDetailsHref}
caseId={caseId}
configureCasesHref={configureCasesHref}
fetchCase={fetchCase}
getCaseDetailHrefWithCommentId={getCaseDetailHrefWithCommentId}
getRuleDetailsHref={getRuleDetailsHref}
onComponentInitialized={onComponentInitialized}
onConfigureCasesNavClick={onConfigureCasesNavClick}
onRuleDetailsClick={onRuleDetailsClick}
renderInvestigateInTimelineActionComponent={renderInvestigateInTimelineActionComponent}
renderTimelineDetailsPanel={renderTimelineDetailsPanel}
showAlertDetails={showAlertDetails}
subCaseId={subCaseId}
updateCase={updateCase}
useFetchAlertData={useFetchAlertData}
userCanCrud={userCanCrud}
/>
<CasesTimelineIntegrationProvider timelineIntegration={timelineIntegration}>
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm wondering if we should only render the CasesTimelineIntegrationProvider if timelineIntegration exists? I know you have the logic further down the line, but why go further down the line if we know timelineIntegration does not exist?

Copy link
Contributor

Choose a reason for hiding this comment

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

Because InsertTimeline always needs it? should we have a boolean where InsertTimeline is to render it or not depending on the timelineIntegration props? or is it simpler just to keep it as is

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No it would still work without the provider since all the useTimelineContext()'s can be the default value of null. So I could do something like below and it would still work, but nothing would really be functionally different. The only difference would be CasesTimelineIntegrationContext not showing up in the tree anymore. It may be simpler to just keep it as is for now?

CasesTimelineIntegrationProvider = ({ children, timelineIntegration }) => {
  const [activeTimelineIntegration] = useState(timelineIntegration ?? null);

  return activeTimelineIntegration ? (
    <CasesTimelineIntegrationContext.Provider value={activeTimelineIntegration}>
      {children}
    </CasesTimelineIntegrationContext.Provider>
  ) : (
    <>{children}</>
  );
};

useTimelineContext is used for InsertTimeline, but also all the editor plugin stuff, and the renderInvestigateInTimeline and renderTimelineDetailsPanel logic. Ideally once the timeline plugin is done we can just nuke all of that stuff. You can test it out by just setting the useState here to null:

https://github.com/elastic/kibana/pull/96496/files/94a75a11d4240237d5225db9ea8d1fb81591996d#diff-9610a1614eed5937c6929340eb693370c84e283a021bf51217b49bc477e1ffecR58

Copy link
Contributor

Choose a reason for hiding this comment

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

yeah i went in an passed null from create to see. im good w this

<CaseComponent
allCasesHref={allCasesHref}
backToAllCasesOnClick={backToAllCasesOnClick}
caseData={data}
caseDetailsHref={caseDetailsHref}
caseId={caseId}
configureCasesHref={configureCasesHref}
fetchCase={fetchCase}
getCaseDetailHrefWithCommentId={getCaseDetailHrefWithCommentId}
getRuleDetailsHref={getRuleDetailsHref}
onComponentInitialized={onComponentInitialized}
onConfigureCasesNavClick={onConfigureCasesNavClick}
onRuleDetailsClick={onRuleDetailsClick}
showAlertDetails={showAlertDetails}
subCaseId={subCaseId}
updateCase={updateCase}
useFetchAlertData={useFetchAlertData}
userCanCrud={userCanCrud}
/>
</CasesTimelineIntegrationProvider>
)
);
}
Expand Down
Loading