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
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
e0cfb51
create case modal to use method
stephmilovic Apr 13, 2021
e91a2e7
gobbledegook
stephmilovic Apr 14, 2021
97244cf
ok working
stephmilovic Apr 14, 2021
e8675f3
pulled second layer filters into ocmponet
stephmilovic Apr 14, 2021
6b92abc
cleanup
stephmilovic Apr 14, 2021
e6c6956
more
stephmilovic Apr 14, 2021
d693aa1
switched to modal from cases
stephmilovic Apr 14, 2021
895a76e
Merge branch 'cases_rac_ui' into modals_cases
stephmilovic Apr 14, 2021
a4d4abc
working, need tests to fix
stephmilovic Apr 14, 2021
20971db
switch from hook to component
stephmilovic Apr 14, 2021
7f812c6
pairing sesh
stephmilovic Apr 15, 2021
59a8cfd
get them tests right
stephmilovic Apr 15, 2021
c4c2042
fix all cases test
stephmilovic Apr 15, 2021
ba8b6f9
update readme
stephmilovic Apr 15, 2021
9e0b6a6
callback city
stephmilovic Apr 15, 2021
af12c84
rm type
stephmilovic Apr 15, 2021
cc64b38
clean up conditional renders
stephmilovic Apr 19, 2021
3ebfc8e
fix skipped tests
stephmilovic Apr 19, 2021
2db2cc5
fix skipped tests
stephmilovic Apr 19, 2021
aeea4ac
rm unused translation
stephmilovic Apr 19, 2021
a7df6c8
Merge branch 'cases_rac_ui' into modals_cases
stephmilovic Apr 20, 2021
3f00989
Merge branch 'cases_rac_ui' into modals_cases
stephmilovic Apr 20, 2021
cefcc6d
pr changes 1
stephmilovic Apr 20, 2021
f3a7c56
Merge branch 'cases_rac_ui' into modals_cases
stephmilovic Apr 20, 2021
6989af2
update renamed var in the tests whoops
stephmilovic Apr 21, 2021
5192797
fix type
stephmilovic Apr 21, 2021
9e98bd9
revert to useMemo
stephmilovic Apr 21, 2021
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
21 changes: 17 additions & 4 deletions x-pack/plugins/cases/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,20 +54,32 @@ cases: CasesUiStart;

##### Methods:
### `getAllCases`
Arguments:
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

UI component:
![All Cases Component][all-cases-img]

### `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
|isModal?|`boolean;` is All Cases table a modal
|onRowClick?|`(theCase?: Case ! SubCase) => void;` callback for row click, passing case in row
|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 Component][all-cases-img]
![All Cases Selector Modal Component][all-cases-modal-img]

### `getCaseView`
Arguments:
Expand Down Expand Up @@ -248,6 +260,7 @@ For IBM Resilient connectors:
[configure-img]: images/configure.png
[create-img]: images/create.png
[all-cases-img]: images/all_cases.png
[all-cases-modal-img]: images/all_cases_selector_modal.png
[recent-cases-img]: images/recent_cases.png
[case-view-img]: images/case_view.png

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions x-pack/plugins/cases/public/common/translations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -254,3 +254,6 @@ export const SELECTABLE_MESSAGE_COLLECTIONS = i18n.translate(
defaultMessage: 'Cases with sub-cases cannot be selected',
}
);
export const SELECT_CASE_TITLE = i18n.translate('xpack.cases.common.allCases.caseModal.title', {
defaultMessage: 'Select case',
});
2 changes: 0 additions & 2 deletions x-pack/plugins/cases/public/components/all_cases/actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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

dispatchUpdate: Dispatch<Omit<UpdateCase, 'refetchCasesStatus'>>;
deleteCaseOnClick: (deleteCase: Case) => void;
}

export const getActions = ({
caseStatus,
dispatchUpdate,
deleteCaseOnClick,
}: GetActions): Array<DefaultItemIconButtonAction<Case>> => {
Expand Down
317 changes: 317 additions & 0 deletions x-pack/plugins/cases/public/components/all_cases/all_cases_generic.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,317 @@
/*
Copy link
Contributor Author

@stephmilovic stephmilovic Apr 15, 2021

Choose a reason for hiding this comment

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

This was ../cases/public/components/all_cases/index.tsx

Look at the diffchecker here, you'll thank me later (and ill thank you back for the thorough review)

Copy link
Member

Choose a reason for hiding this comment

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

Thank you for that!

Copy link
Contributor Author

@stephmilovic stephmilovic Apr 21, 2021

Choose a reason for hiding this comment

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

I knew you'd thank me later 🥳 Thank YOU for the thorough review!

* 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, { useCallback, useMemo, useRef, useState } from 'react';
import { EuiProgress } from '@elastic/eui';
import { EuiTableSelectionType } from '@elastic/eui/src/components/basic_table/table_types';
import { isEmpty, memoize } from 'lodash/fp';
import styled, { css } from 'styled-components';
import classnames from 'classnames';

import {
Case,
CaseStatuses,
CaseType,
CommentRequestAlertType,
CommentType,
FilterOptions,
SortFieldCase,
SubCase,
} from '../../../common';
import { SELECTABLE_MESSAGE_COLLECTIONS } from '../../common/translations';
import { useGetActionLicense } from '../../containers/use_get_action_license';
import { useGetCases } from '../../containers/use_get_cases';
import { usePostComment } from '../../containers/use_post_comment';
import { CaseCallOut } from '../callout';
import { CaseDetailsHrefSchema, CasesNavigation } from '../links';
import { Panel } from '../panel';
import { getActionLicenseError } from '../use_push_to_service/helpers';
import { ERROR_PUSH_SERVICE_CALLOUT_TITLE } from '../use_push_to_service/translations';
import { useCasesColumns } from './columns';
import { getExpandedRowMap } from './expanded_row';
import { CasesTableHeader } from './header';
import { CasesTableFilters } from './table_filters';
import { EuiBasicTableOnChange } from './types';

import { CasesTable } from './table';
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

top: 2px;
border-radius: ${({ theme }) => theme.eui.euiBorderRadius};
z-index: ${({ theme }) => theme.eui.euiZHeader};
`
: `
display: none;
`}
`;

const getSortField = (field: string): SortFieldCase =>
field === SortFieldCase.closedAt ? SortFieldCase.closedAt : SortFieldCase.createdAt;

interface AllCasesGenericProps {
alertData?: Omit<CommentRequestAlertType, 'type'>;
caseDetailsNavigation?: CasesNavigation<CaseDetailsHrefSchema, 'configurable'>; // if not passed, case name is not displayed as a link (Formerly dependant on isSelectorView)
configureCasesNavigation?: CasesNavigation; // if not passed, header with nav is not displayed (Formerly dependant on isSelectorView)
createCaseNavigation: CasesNavigation;
disabledStatuses?: CaseStatuses[];
isSelectorView?: boolean;
onRowClick?: (theCase?: Case | SubCase) => void;
updateCase?: (newCase: Case) => void;
userCanCrud: boolean;
}

export const AllCasesGeneric = React.memo<AllCasesGenericProps>(
({
alertData,
caseDetailsNavigation,
configureCasesNavigation,
createCaseNavigation,
disabledStatuses,
isSelectorView,
onRowClick,
updateCase,
userCanCrud,
}) => {
const { actionLicense } = useGetActionLicense();
const {
data,
filterOptions,
loading,
queryParams,
selectedCases,
refetchCases,
setFilters,
setQueryParams,
setSelectedCases,
} = useGetCases();
// Post Comment to Case
const { postComment, isLoading: isCommentUpdating } = usePostComment();

const sorting = useMemo(
() => ({
sort: { field: queryParams.sortField, direction: queryParams.sortOrder },
}),
[queryParams.sortField, queryParams.sortOrder]
);

const filterRefetch = useRef<() => void>();
const setFilterRefetch = useCallback(
(refetchFilter: () => void) => {
filterRefetch.current = refetchFilter;
},
[filterRefetch]
);
const [refresh, doRefresh] = useState<number>(0);
const [isLoading, handleIsLoading] = useState<boolean>(false);
const refreshCases = useCallback(
(dataRefresh = true) => {
if (dataRefresh) refetchCases();
doRefresh((prev) => prev + 1);
setSelectedCases([]);
if (filterRefetch.current != null) {
filterRefetch.current();
}
},
[filterRefetch, refetchCases, setSelectedCases]
);

const { onClick: onCreateCaseNavClick } = createCaseNavigation;
const goToCreateCase = useCallback(
(ev) => {
ev.preventDefault();
if (isSelectorView && onRowClick != null) {
onRowClick();
} else if (onCreateCaseNavClick) {
onCreateCaseNavClick(ev);
}
},
[isSelectorView, onCreateCaseNavClick, onRowClick]
);
const actionsErrors = useMemo(() => getActionLicenseError(actionLicense), [actionLicense]);

const tableOnChangeCallback = useCallback(
({ page, sort }: EuiBasicTableOnChange) => {
let newQueryParams = queryParams;
if (sort) {
newQueryParams = {
...newQueryParams,
sortField: getSortField(sort.field),
sortOrder: sort.direction,
};
}
if (page) {
newQueryParams = {
...newQueryParams,
page: page.index + 1,
perPage: page.size,
};
}
setQueryParams(newQueryParams);
refreshCases(false);
},
[queryParams, refreshCases, setQueryParams]
);

const onFilterChangedCallback = useCallback(
(newFilterOptions: Partial<FilterOptions>) => {
if (newFilterOptions.status && newFilterOptions.status === CaseStatuses.closed) {
setQueryParams({ sortField: SortFieldCase.closedAt });
} else if (newFilterOptions.status && newFilterOptions.status === CaseStatuses.open) {
setQueryParams({ sortField: SortFieldCase.createdAt });
} else if (
newFilterOptions.status &&
newFilterOptions.status === CaseStatuses['in-progress']
) {
setQueryParams({ sortField: SortFieldCase.createdAt });
}
setFilters(newFilterOptions);
refreshCases(false);
},
[refreshCases, setQueryParams, setFilters]
);

const showActions = userCanCrud && !isSelectorView;

const columns = useCasesColumns({
caseDetailsNavigation,
filterStatus: filterOptions.status,
refreshCases,
handleIsLoading,
showActions,
});

const itemIdToExpandedRowMap = useMemo(
() =>
getExpandedRowMap({
columns,
data: data.cases,
onSubCaseClick: onRowClick,
}),
[data.cases, columns, onRowClick]
);

const pagination = useMemo(
() => ({
pageIndex: queryParams.page - 1,
pageSize: queryParams.perPage,
totalItemCount: data.total,
pageSizeOptions: [5, 10, 15, 20, 25],
}),
[data, queryParams]
);

const euiBasicTableSelectionProps = useMemo<EuiTableSelectionType<Case>>(
() => ({
onSelectionChange: setSelectedCases,
selectableMessage: (selectable) => (!selectable ? SELECTABLE_MESSAGE_COLLECTIONS : ''),
initialSelected: selectedCases,
}),
[selectedCases, setSelectedCases]
);
const isCasesLoading = useMemo(() => loading.indexOf('cases') > -1, [loading]);
const isDataEmpty = useMemo(() => data.total === 0, [data]);

const TableWrap = useMemo(() => (isSelectorView ? 'span' : Panel), [isSelectorView]);

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 }),
...(isSelectorView && theCase.type !== CaseType.collection
? { onClick: onTableRowClick }
: {}),
};
},
[isSelectorView, alertData, onRowClick, postComment, updateCase]
);

return (
<>
{!isEmpty(actionsErrors) && (
<CaseCallOut title={ERROR_PUSH_SERVICE_CALLOUT_TITLE} messages={actionsErrors} />
)}
{configureCasesNavigation != null && (
<CasesTableHeader
actionsErrors={actionsErrors}
createCaseNavigation={createCaseNavigation}
configureCasesNavigation={configureCasesNavigation}
refresh={refresh}
userCanCrud={userCanCrud}
/>
)}
<ProgressLoader
size="xs"
color="accent"
className="essentialAnimation"
$isShow={(isCasesLoading || isLoading || isCommentUpdating) && !isDataEmpty}
/>
<TableWrap
data-test-subj="table-wrap"
loading={!isSelectorView ? isCasesLoading : undefined}
>
<CasesTableFilters
countClosedCases={data.countClosedCases}
countOpenCases={data.countOpenCases}
countInProgressCases={data.countInProgressCases}
onFilterChanged={onFilterChangedCallback}
initial={{
search: filterOptions.search,
reporters: filterOptions.reporters,
tags: filterOptions.tags,
status: filterOptions.status,
}}
setFilterRefetch={setFilterRefetch}
disabledStatuses={disabledStatuses}
/>
<CasesTable
columns={columns}
createCaseNavigation={createCaseNavigation}
data={data}
filterOptions={filterOptions}
goToCreateCase={goToCreateCase}
handleIsLoading={handleIsLoading}
isCasesLoading={isCasesLoading}
isCommentUpdating={isCommentUpdating}
isDataEmpty={isDataEmpty}
isSelectorView={isSelectorView}
itemIdToExpandedRowMap={itemIdToExpandedRowMap}
onChange={tableOnChangeCallback}
pagination={pagination}
refreshCases={refreshCases}
selectedCases={selectedCases}
selection={euiBasicTableSelectionProps}
showActions={showActions}
sorting={sorting}
tableRowProps={tableRowProps}
userCanCrud={userCanCrud}
/>
</TableWrap>
</>
);
}
);

AllCasesGeneric.displayName = 'AllCasesGeneric';
Loading