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] All Cases Refactor & getAllCasesSelectorModal introduced #97149

Merged
merged 27 commits into from
Apr 21, 2021

Conversation

stephmilovic
Copy link
Contributor

@stephmilovic stephmilovic commented Apr 14, 2021

  • AllCases broken up into smaller components

    • new components: CasesTableHeader, Count, NavButtons, and CasesTableUtilityBar, able to remove useGetCasesStatus, useDeleteCases and useUpdateCases from AllCases
    • getCasesColumns upgraded to a hook, useCasesColumns move actions logic into useCasesColumns, able to remove useDeleteCases and dispatchUpdateCaseProperty from AllCases
  • Refactor in AllCases to move anything in jsx markup contained within a {} to a useCallback. See this commit: 9e0b6a6 Don't do this, see this slack thread for official recommendation

  • New AllCases components to manage props

    • <AllCases /> (props are same as getAllCases below)
    • <AllCasesSelectorModal />(props are same as getAllCasesSelectorModal below)
    • <AllCasesGeneric /> - this is formerly just AllCases. This is the management component. These props are a conditional combo of the 2 above and not exposed to the consumer.
  • New casesUi method, getAllCasesSelectorModal. Change underlying getAllCases component to have more specific props. Here is there result:

getAllCases

Arguments:

Property Description
caseDetailsNavigation CasesNavigation<CaseDetailsHrefSchema, 'configurable'> route configuration to generate the case details url for the case details page
configureCasesNavigation CasesNavigation route configuration for configure cases page
createCaseNavigation CasesNavigation route configuration for create cases page
userCanCrud boolean; user permissions to crud

getAllCasesSelectorModal

Arguments:

Property Description
alertData? Omit<CommentRequestAlertType, 'type'>; alert data to post to case
createCaseNavigation CasesNavigation route configuration for create cases page
disabledStatuses? CaseStatuses[]; array of disabled statuses
onRowClick (theCase?: Case ! SubCase) => void; callback for row click, passing case in row
updateCase? (theCase: Case ! SubCase) => void; callback after case has been updated
userCanCrud boolean; user permissions to crud

UI component:
all_cases_selector_modal

@stephmilovic stephmilovic added WIP Work in progress v8.0.0 release_note:skip Skip the PR/issue when compiling release notes backport:skip This commit does not require backporting Team:Threat Hunting Security Solution Threat Hunting Team Team: SecuritySolution Security Solutions Team working on SIEM, Endpoint, Timeline, Resolver, etc. Feature:Cases Cases feature Feature:Cases-RAC-UI labels Apr 14, 2021
@stephmilovic stephmilovic changed the title [Security Solution] [Cases] [WIP] Break up All Cases into smaller components and make modals nicer [Security Solution] [Cases] [skip-ci] Break up All Cases into smaller components and make modals nicer Apr 14, 2021
@@ -58,4 +58,4 @@ export const MAX_GENERATED_ALERTS_PER_SUB_CASE = 50;
/**
* This flag governs enabling the case as a connector feature. It is disabled by default as the feature is not complete.
*/
export const ENABLE_CASE_CONNECTOR = false;
export const ENABLE_CASE_CONNECTOR = true;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

switch to false before merging!

@@ -16,13 +16,11 @@ import * as i18n from './translations';
import { isIndividual } from './helpers';

interface GetActions {
caseStatus: string;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

unused

}: GetCasesColumn): CasesColumns[] => {
const columns = [
const { loading: isLoadingCases, dispatchUpdateCaseProperty } = useGetCases();
Copy link
Contributor Author

Choose a reason for hiding this comment

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

move actions for deleting/updating a single case into columns, along with the confirmDeleteModal

Copy link
Contributor

Choose a reason for hiding this comment

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

dope

@stephmilovic stephmilovic changed the title [Security Solution] [Cases] [skip-ci] Break up All Cases into smaller components and make modals nicer [Security Solution] [Cases] Break up All Cases into smaller components and make modals nicer Apr 15, 2021
@stephmilovic stephmilovic marked this pull request as ready for review April 15, 2021 17:44
@stephmilovic stephmilovic requested a review from a team as a code owner April 15, 2021 17:44
@elasticmachine
Copy link
Contributor

Pinging @elastic/security-solution (Team: SecuritySolution)

@elasticmachine
Copy link
Contributor

Pinging @elastic/security-threat-hunting (Team:Threat Hunting)

@@ -18,7 +18,7 @@ import * as i18n from './translations';

export interface CasesNavigation<T = React.MouseEvent | MouseEvent, K = null> {
href: K extends 'configurable' ? (arg: T) => string : string;
onClick?: (arg: T) => void;
onClick: (arg: T) => void;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

made this required, href triggers app reload. we need href for right click > open in new tab

@@ -45,8 +45,8 @@ const CaseDetailsLinkComponent: React.FC<{
const { href: getHref, onClick } = caseDetailsNavigation;
const goToCaseDetails = useCallback(
(ev) => {
ev.preventDefault();
Copy link
Contributor Author

Choose a reason for hiding this comment

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

yikes, this made href only unclickable

const ProgressLoader = styled(EuiProgress)`
${({ $isShow }: { $isShow: boolean }) =>
$isShow
? css`
Copy link
Contributor

Choose a reason for hiding this comment

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

👍🏾 Nice, didn't know about this

[dispatchUpdateCaseProperty, refreshCases]
);

const actions = useMemo(
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think this needs a useMemo

Copy link
Contributor Author

Choose a reason for hiding this comment

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

why not? otherwise it is called each render

Copy link
Contributor

@michaelolo24 michaelolo24 Apr 21, 2021

Choose a reason for hiding this comment

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

You can ignore this. I dug into the actions call a bit deeper. What I was thinking was that since all it's doing is just setting properties and returning an object, the actual call isn't that computationally expensive, but I missed maintaining the referential equality when it's being passed to the CasesTable and expandedRowMap.

<span>{theCase.title}</span>
);
const caseDetailsLinkComponent =
caseDetailsNavigation != null ? (
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe just caseDetailsNavigation ? to handle any situation where it may be undefined as well

Copy link
Contributor Author

@stephmilovic stephmilovic Apr 20, 2021

Choose a reason for hiding this comment

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

!= null checks that the value is not null and that the value is not undefined. if it was !== null, it would only check against null and allow undefined

Copy link
Contributor

Choose a reason for hiding this comment

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

Ahh sorry! Didn't realize it was single equals 👍🏾

<AllCasesSelectorModal {...fullProps} />
</TestProviders>
);
// @ts-ignore idk what this mock style is but it works ¯\_(ツ)_/¯
Copy link
Contributor

Choose a reason for hiding this comment

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

lol, love the comment. This just to check the props?

@@ -0,0 +1,55 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
Copy link
Contributor

Choose a reason for hiding this comment

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

This is so much cleaner!

import { useGetCasesStatus } from '../../containers/use_get_cases_status';

interface CountProps {
refresh?: number;
Copy link
Member

Choose a reason for hiding this comment

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

Why refresh is a number?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This way the parent can trigger an update in the child. When the number changes, a useEffect runs the re-fetch function

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Contributor

@XavierM XavierM Apr 21, 2021

Choose a reason for hiding this comment

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

I think we can do better now, we are smarter than a year ago

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Speak for yourself 😂

Copy link
Contributor

Choose a reason for hiding this comment

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

Lol, I know we did something similar in resolver (the code may have been removed now), but it was to make sure that if multiple requests are made we could abort an earlier one so we didn't get out of sync data. @kqualters-elastic worked on it I believe.

Copy link
Member

Choose a reason for hiding this comment

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

I see. Smart!

@cnasikas
Copy link
Member

cnasikas commented Apr 21, 2021

I really like this PR and the effort you put to make the AllCases component better, scalable, and cleaner. I love the smaller components like CasesTableFilters and CasesTable. Amazing job!

I see that the iseSelectorView is used by:

  1. The goToCreateCase function
  2. showActions variable
  3. TableWrap component
  4. tableRowProps function
  5. In the CasesTable. It is being passed as a prop.

All other code inside the AllCasesGeneric component is the same for both the AllCases component and the AllCasesSelectorModal component. I am thinking, without being of course right, now that we have this beautiful components, maybe it is better to create a hook called useAllCases that will contain all the common logic that is right now inside AllCasesGeneric component. the AllCases component and the AllCasesSelectorModal component can use this hook to get what they need and each one of the component can define the pieces in the code they differ (the numbered list above). Example:

export const AllCases: React.FC<AllCasesProps> = (props) => {
  // The hook exports all needed variables that will be passed as props
  // to the components
  const { actionLicense, columns, data, filterOptions, ... } = useAllCases();

  const goToCreateCase = useCallback(
      (ev) => {
        ev.preventDefault();
        onCreateCaseNavClick(ev);
      },
      [onCreateCaseNavClick]
    );

    const tableRowProps = useCallback(
      (theCase: Case) => {
        return {
          'data-test-subj': `cases-table-row-${theCase.id}`,
          className: classnames({ isDisabled: theCase.type === CaseType.collection }),
        };
      },
      [isSelectorView, alertData, onRowClick, postComment, updateCase]
    );

    // I omitted the props and the logic for readability
    <Callout />
    <CasesTableHeader />
    <ProgressLoader />
    // TableWrap is a Panel for AllCases
    <Panel>
      <CasesTableFilters/>
      <CasesTable isSelectorView={false} showActions={true}/>
    </Panel>

};
export const AllCasesSelectorModal: React.FC<AllCasesSelectorModalProps> = ({
  alertData,
  createCaseNavigation,
  disabledStatuses,
  onRowClick,
  updateCase,
  userCanCrud,
}) => {
  // modal logic omitted for readability

  const { actionLicense, columns, data, filterOptions, ... } = useAllCases();
  const { postComment, isLoading: isCommentUpdating } = usePostComment();

  const tableRowProps = useCallback(
      (theCase: Case) => {
        const onTableRowClick = memoize(async () => {
          if (alertData != null) {
            await postComment({
              caseId: theCase.id,
              data: {
                type: CommentType.alert,
                ...alertData,
              },
              updateCase,
            });
          }
          if (onRowClick) {
            onRowClick(theCase);
          }
        });

        return {
          'data-test-subj': `cases-table-row-${theCase.id}`,
          className: classnames({ isDisabled: theCase.type === CaseType.collection }),
          { onClick: onTableRowClick }
        };
      },
      [isSelectorView, alertData, onRowClick, postComment, updateCase]
    );

  // I omitted the props and the logic for readability
    <Callout />
    // TableWrap is a span for AllCasesSelectorModal
    <span>
      <CasesTableFilters/>
      <CasesTable isSelectorView={true} showActions={false}/>
    </span>
};
// eslint-disable-next-line import/no-default-export
export { AllCasesSelectorModal as default };

@stephmilovic @michaelolo24 @XavierM What do you think?

@XavierM
Copy link
Contributor

XavierM commented Apr 21, 2021

@cnasikas about your last comment

TBH, I do not see a big difference between the hook and the generic component (new vs old, you are younger @cnasikas ❤️ ) . I think that with the hook we will have some duplication functionality in these two components. I do not mind either way but I do like that we were able to make our API for these components simpler for our users and not having branch logic on props to show one or the other components. I do like the generic component for now, the hook did not come in my mind when we discussed the problem. To conclude, I think we can keep the way it is. However if @stephmilovic see 👀 to 😍 with you, I will support your decision.

@stephmilovic
Copy link
Contributor Author

Responding to @cnasikas without copy/pasting his comment. I mean... we could. What value does it add? It seems like you're just reorganizing the code. The end user would still get the same casesUi API, same methods getAllCases and getAllCasesSelectorModal, same arguments passed for each. IMO it doesn't add value, it's just a different way to do it. Neither way is right, neither is wrong. I don't think one is better than the other. If you would like to spend your time on this, go for it, I won't stop you. 😋

@XavierM
Copy link
Contributor

XavierM commented Apr 21, 2021

That's a good refactor
image

@cnasikas
Copy link
Member

cnasikas commented Apr 21, 2021

Thank you @stephmilovic @XavierM for your answers. I like this type of discussions because I learn a lot from them. I agree that both approaches have their merits and neither is better than the other one. @michaelolo24 made me realise that the view level is so similar as we only have a single boolean check. If these component diverge more we can then think about implementing the hook. Let's the old live a bit longer 😋

Copy link
Contributor

@michaelolo24 michaelolo24 left a comment

Choose a reason for hiding this comment

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

Way to rock this LGTM!

@kibanamachine
Copy link
Contributor

💚 Build Succeeded

Metrics [docs]

Module Count

Fewer modules leads to a faster build time

id before after diff
cases 243 252 +9
securitySolution 1984 1981 -3
total +6

Async chunks

Total size of all lazy-loaded chunks that will be downloaded as the user navigates the app

id before after diff
cases 497.7KB 491.4KB -6.3KB
securitySolution 6.5MB 6.5MB -3.4KB
total -9.8KB

Page load bundle

Size of the bundles that are downloaded on every page load. Target size is below 100kb

id before after diff
cases 124.7KB 126.8KB +2.0KB
Unknown metric groups

API count

id before after diff
cases 348 349 +1

API count missing comments

id before after diff
cases 336 337 +1

async chunk count

id before after diff
cases 12 14 +2

Non-exported public API item count

id before after diff
cases 6 7 +1

History

To update your PR or re-run it, just comment with:
@elasticmachine merge upstream

@stephmilovic
Copy link
Contributor Author

I'm going to merge cuz i got that ✅ . I will pair with @XavierM on whatever smart refresh child method he has in store for me. Look for a quick follow up PR 🎢

@stephmilovic stephmilovic merged commit 54c97d4 into elastic:cases_rac_ui Apr 21, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
backport:skip This commit does not require backporting Feature:Cases Cases feature release_note:skip Skip the PR/issue when compiling release notes Team: SecuritySolution Security Solutions Team working on SIEM, Endpoint, Timeline, Resolver, etc. Team:Threat Hunting Security Solution Threat Hunting Team v8.0.0 WIP Work in progress
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants