From 03524c5cf1b3c69d97984df7863cdd574014ace8 Mon Sep 17 00:00:00 2001 From: stephmilovic Date: Wed, 26 Feb 2020 06:09:55 -0700 Subject: [PATCH 01/12] columns update --- .../case/components/all_cases/columns.tsx | 69 ++++++++++--------- .../pages/case/components/all_cases/index.tsx | 7 ++ .../siem/public/pages/case/translations.ts | 8 ++- 3 files changed, 50 insertions(+), 34 deletions(-) diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx index 4c47bf605051d..77eb3af26b4ac 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx @@ -4,7 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ import React from 'react'; -import { EuiBadge, EuiTableFieldDataColumnType, EuiTableComputedColumnType } from '@elastic/eui'; +import { + EuiBadge, + EuiTableFieldDataColumnType, + EuiTableComputedColumnType, + EuiAvatar, +} from '@elastic/eui'; +import styled from 'styled-components'; import { getEmptyTagValue } from '../../../../components/empty_value'; import { Case } from '../../../../containers/case/types'; import { FormattedRelativePreferenceDate } from '../../../../components/formatted_date'; @@ -14,8 +20,9 @@ import * as i18n from './translations'; export type CasesColumns = EuiTableFieldDataColumnType | EuiTableComputedColumnType; -const renderStringField = (field: string, dataTestSubj: string) => - field != null ? {field} : getEmptyTagValue(); +const Spacer = styled.span` + margin-left: 8px; +`; export const getCasesColumns = (): CasesColumns[] => [ { @@ -27,6 +34,23 @@ export const getCasesColumns = (): CasesColumns[] => [ return getEmptyTagValue(); }, }, + { + field: 'createdBy', + name: i18n.REPORTER, + render: (createdBy: Case['createdBy']) => + createdBy != null ? ( + <> + + {createdBy.username} + + ) : ( + getEmptyTagValue() + ), + }, { field: 'tags', name: i18n.TAGS, @@ -50,9 +74,18 @@ export const getCasesColumns = (): CasesColumns[] => [ }, truncateText: true, }, + { + align: 'right', + field: 'updatedAt', // TO DO once we have commentCount returned in the API: https://github.com/elastic/kibana/issues/58525 + name: i18n.COMMENTS, + sortable: true, + render: () => { + return {1}; + }, + }, { field: 'createdAt', - name: i18n.CREATED_AT, + name: i18n.OPENED_ON, sortable: true, render: (createdAt: Case['createdAt']) => { if (createdAt != null) { @@ -66,32 +99,4 @@ export const getCasesColumns = (): CasesColumns[] => [ return getEmptyTagValue(); }, }, - { - field: 'createdBy.username', - name: i18n.REPORTER, - render: (createdBy: Case['createdBy']['username']) => - renderStringField(createdBy, `case-table-column-username`), - }, - { - field: 'updatedAt', - name: i18n.LAST_UPDATED, - sortable: true, - render: (updatedAt: Case['updatedAt']) => { - if (updatedAt != null) { - return ( - - ); - } - return getEmptyTagValue(); - }, - }, - { - field: 'state', - name: i18n.STATE, - sortable: true, - render: (state: Case['state']) => renderStringField(state, `case-table-column-state`), - }, ]; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx index 3253a036c2990..1a34e5171a719 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx @@ -31,6 +31,7 @@ import { UtilityBarText, } from '../../../../components/detection_engine/utility_bar'; import { getCreateCaseUrl } from '../../../../components/link_to'; +import { EuiTableSelectionType } from '@elastic/eui/src/components/basic_table/table_types'; const getSortField = (field: string): SortFieldCase => { if (field === SortFieldCase.createdAt) { return SortFieldCase.createdAt; @@ -92,6 +93,10 @@ export const AllCases = React.memo(() => { sort: { field: queryParams.sortField, direction: queryParams.sortOrder }, }; + const selection: EuiTableSelectionType = { + onSelectionChange: (selectedItems) => console.log('on selection change', selectedItems) + } + return ( @@ -116,6 +121,7 @@ export const AllCases = React.memo(() => { { } onChange={tableOnChangeCallback} pagination={memoizedPagination} + selection={selection} sorting={sorting} /> diff --git a/x-pack/legacy/plugins/siem/public/pages/case/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/translations.ts index 4e878ba58411e..50ec4fbe05483 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/case/translations.ts @@ -18,8 +18,8 @@ export const CASE_TITLE = i18n.translate('xpack.siem.case.caseView.caseTitle', { defaultMessage: 'Case Title', }); -export const CREATED_AT = i18n.translate('xpack.siem.case.caseView.createdAt', { - defaultMessage: 'Created at', +export const OPENED_ON = i18n.translate('xpack.siem.case.caseView.openedOn', { + defaultMessage: 'Opened on', }); export const REPORTER = i18n.translate('xpack.siem.case.caseView.createdBy', { @@ -90,6 +90,10 @@ export const TAGS = i18n.translate('xpack.siem.case.caseView.tags', { defaultMessage: 'Tags', }); +export const COMMENTS = i18n.translate('xpack.siem.case.allCases.comments', { + defaultMessage: 'Comments', +}); + export const TAGS_HELP = i18n.translate('xpack.siem.case.createCase.fieldTagsHelpText', { defaultMessage: 'Type one or more custom identifying tags for this case. Press enter after each tag to begin a new one.', From ae8908fe766e0af2afb2b047f88121c1f87e48be Mon Sep 17 00:00:00 2001 From: stephmilovic Date: Thu, 27 Feb 2020 11:21:26 -0700 Subject: [PATCH 02/12] basic table question --- .../siem/public/containers/case/constants.ts | 1 + .../public/containers/case/use_get_cases.tsx | 22 ++++++++++++++---- .../pages/case/components/all_cases/index.tsx | 23 +++++++++++-------- 3 files changed, 33 insertions(+), 13 deletions(-) diff --git a/x-pack/legacy/plugins/siem/public/containers/case/constants.ts b/x-pack/legacy/plugins/siem/public/containers/case/constants.ts index c8d668527ae32..d6ee2fad647ad 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/constants.ts +++ b/x-pack/legacy/plugins/siem/public/containers/case/constants.ts @@ -13,4 +13,5 @@ export const FETCH_SUCCESS = 'FETCH_SUCCESS'; export const POST_NEW_CASE = 'POST_NEW_CASE'; export const UPDATE_CASE_PROPERTY = 'UPDATE_CASE_PROPERTY'; export const UPDATE_FILTER_OPTIONS = 'UPDATE_FILTER_OPTIONS'; +export const UPDATE_TABLE_SELECTIONS = 'UPDATE_TABLE_SELECTIONS'; export const UPDATE_QUERY_PARAMS = 'UPDATE_QUERY_PARAMS'; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx index 4037823ccfc94..f85f7c08acb9c 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx @@ -14,8 +14,9 @@ import { FETCH_SUCCESS, UPDATE_QUERY_PARAMS, UPDATE_FILTER_OPTIONS, + UPDATE_TABLE_SELECTIONS, } from './constants'; -import { AllCases, SortFieldCase, FilterOptions, QueryParams } from './types'; +import { AllCases, SortFieldCase, FilterOptions, QueryParams, Case } from './types'; import { getTypedPayload } from './utils'; import { errorToToaster } from '../../components/ml/api/error_to_toaster'; import { useStateToaster } from '../../components/toasters'; @@ -28,11 +29,12 @@ export interface UseGetCasesState { isError: boolean; queryParams: QueryParams; filterOptions: FilterOptions; + selectedCases: Case[]; } export interface Action { type: string; - payload?: AllCases | Partial | FilterOptions; + payload?: AllCases | Partial | FilterOptions | Case[]; } const dataFetchReducer = (state: UseGetCasesState, action: Action): UseGetCasesState => { switch (action.type) { @@ -68,6 +70,11 @@ const dataFetchReducer = (state: UseGetCasesState, action: Action): UseGetCasesS ...state, filterOptions: getTypedPayload(action.payload), }; + case UPDATE_TABLE_SELECTIONS: + return { + ...state, + selectedCases: getTypedPayload(action.payload), + }; default: throw new Error(); } @@ -81,8 +88,9 @@ const initialData: AllCases = { }; export const useGetCases = (): [ UseGetCasesState, + Dispatch>, Dispatch>>, - Dispatch> + Dispatch> ] => { const [state, dispatch] = useReducer(dataFetchReducer, { isLoading: false, @@ -98,11 +106,17 @@ export const useGetCases = (): [ sortField: SortFieldCase.createdAt, sortOrder: 'desc', }, + selectedCases: [], }); const [queryParams, setQueryParams] = useState>(state.queryParams); const [filterQuery, setFilters] = useState(state.filterOptions); + const [selectedCases, setSelectedCases] = useState(state.selectedCases); const [, dispatchToaster] = useStateToaster(); + useEffect(() => { + dispatch({ type: UPDATE_TABLE_SELECTIONS, payload: selectedCases }); + }, [selectedCases]); + useEffect(() => { if (!isEqual(queryParams, state.queryParams)) { dispatch({ type: UPDATE_QUERY_PARAMS, payload: queryParams }); @@ -142,5 +156,5 @@ export const useGetCases = (): [ didCancel = true; }; }, [state.queryParams, state.filterOptions]); - return [state, setQueryParams, setFilters]; + return [state, setFilters, setQueryParams, setSelectedCases]; }; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx index 1a34e5171a719..feaa18e5d2181 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx @@ -13,6 +13,7 @@ import { EuiTableSortingType, } from '@elastic/eui'; import { isEmpty } from 'lodash/fp'; +import { EuiTableSelectionType } from '@elastic/eui/src/components/basic_table/table_types'; import * as i18n from './translations'; import { getCasesColumns } from './columns'; @@ -31,7 +32,7 @@ import { UtilityBarText, } from '../../../../components/detection_engine/utility_bar'; import { getCreateCaseUrl } from '../../../../components/link_to'; -import { EuiTableSelectionType } from '@elastic/eui/src/components/basic_table/table_types'; + const getSortField = (field: string): SortFieldCase => { if (field === SortFieldCase.createdAt) { return SortFieldCase.createdAt; @@ -44,9 +45,10 @@ const getSortField = (field: string): SortFieldCase => { }; export const AllCases = React.memo(() => { const [ - { data, isLoading, queryParams, filterOptions }, - setQueryParams, + { data, isLoading, queryParams, filterOptions, selectedCases }, setFilters, + setQueryParams, + setSelectedCases, ] = useGetCases(); const tableOnChangeCallback = useCallback( @@ -92,10 +94,13 @@ export const AllCases = React.memo(() => { const sorting: EuiTableSortingType = { sort: { field: queryParams.sortField, direction: queryParams.sortOrder }, }; - - const selection: EuiTableSelectionType = { - onSelectionChange: (selectedItems) => console.log('on selection change', selectedItems) - } + const euiBasicTableSelectionProps = useMemo>( + () => ({ + selectable: (item: Case) => true, + onSelectionChange: setSelectedCases, + }), + [selectedCases] + ); return ( @@ -121,7 +126,7 @@ export const AllCases = React.memo(() => { { } onChange={tableOnChangeCallback} pagination={memoizedPagination} - selection={selection} + selection={euiBasicTableSelectionProps} sorting={sorting} /> From b97116f227bca159e088ad5bb6b4315c03140d05 Mon Sep 17 00:00:00 2001 From: stephmilovic Date: Fri, 28 Feb 2020 16:14:10 -0700 Subject: [PATCH 03/12] fix id issue on table --- .../siem/public/pages/case/components/all_cases/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx index feaa18e5d2181..a753b63931b3f 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx @@ -127,7 +127,7 @@ export const AllCases = React.memo(() => { Date: Mon, 2 Mar 2020 08:38:27 -0700 Subject: [PATCH 04/12] updates to columns and filters --- .../components/filter_popover/index.tsx | 113 ++++++++++++++++++ .../siem/public/containers/case/api.ts | 8 +- .../siem/public/containers/case/types.ts | 2 +- .../public/containers/case/use_get_cases.tsx | 1 + .../case/components/all_cases/columns.tsx | 57 ++++++--- .../pages/case/components/all_cases/index.tsx | 25 ++-- .../components/all_cases/table_filters.tsx | 66 +++++++--- .../case/components/all_cases/translations.ts | 13 +- .../siem/public/pages/case/translations.ts | 11 ++ 9 files changed, 244 insertions(+), 52 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/public/components/filter_popover/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/filter_popover/index.tsx b/x-pack/legacy/plugins/siem/public/components/filter_popover/index.tsx new file mode 100644 index 0000000000000..d911331c6a6f4 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/filter_popover/index.tsx @@ -0,0 +1,113 @@ +/* + * 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, { Dispatch, SetStateAction, useState } from 'react'; +import { + EuiFilterButton, + EuiFilterSelectItem, + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiPopover, + EuiText, +} from '@elastic/eui'; +import styled from 'styled-components'; + +interface FilterPopoverProps { + buttonLabel: string; + onSelectedOptionsChanged: Dispatch>; + options: string[]; + optionsEmptyLabel: string; + selectedOptions: string[]; +} + +const ScrollableDiv = styled.div` + max-height: 250px; + overflow: auto; +`; + +export const toggleSelectedGroup = ( + group: string, + selectedGroups: string[], + setSelectedGroups: Dispatch> +): void => { + const selectedGroupIndex = selectedGroups.indexOf(group); + const updatedSelectedGroups = [...selectedGroups]; + if (selectedGroupIndex >= 0) { + updatedSelectedGroups.splice(selectedGroupIndex, 1); + } else { + updatedSelectedGroups.push(group); + } + return setSelectedGroups(updatedSelectedGroups); +}; + +/** + * Popover for selecting a field to filter on + * + * @param buttonLabel label on dropdwon button + * @param onSelectedOptionsChanged change listener to be notified when option selection changes + * @param options to display for filtering + * @param optionsEmptyLabel shows when options empty + * @param selectedOptions manage state of selectedOptions + */ +export const FilterPopoverComponent = ({ + buttonLabel, + onSelectedOptionsChanged, + options, + optionsEmptyLabel, + selectedOptions, +}: FilterPopoverProps) => { + const [isTagPopoverOpen, setIsTagPopoverOpen] = useState(false); + + return ( + setIsTagPopoverOpen(!isTagPopoverOpen)} + isSelected={isTagPopoverOpen} + numFilters={options.length} + hasActiveFilters={selectedOptions.length > 0} + numActiveFilters={selectedOptions.length} + > + {buttonLabel} + + } + isOpen={isTagPopoverOpen} + closePopover={() => setIsTagPopoverOpen(!isTagPopoverOpen)} + panelPaddingSize="none" + > + + {options.map((tag, index) => ( + toggleSelectedGroup(tag, selectedOptions, onSelectedOptionsChanged)} + > + {`${tag}`} + + ))} + + {options.length === 0 && ( + + + + {optionsEmptyLabel} + + + + )} + + ); +}; + +FilterPopoverComponent.displayName = 'FilterPopoverComponent'; + +export const FilterPopover = React.memo(FilterPopoverComponent); + +FilterPopover.displayName = 'FilterPopover'; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/api.ts b/x-pack/legacy/plugins/siem/public/containers/case/api.ts index bff3bfd62a85c..e70c75191e4e5 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/api.ts +++ b/x-pack/legacy/plugins/siem/public/containers/case/api.ts @@ -25,6 +25,7 @@ export const getCase = async (caseId: string, includeComments: boolean): Promise export const getCases = async ({ filterOptions = { search: '', + state: 'open', tags: [], }, queryParams = { @@ -34,7 +35,12 @@ export const getCases = async ({ sortOrder: 'desc', }, }: FetchCasesProps): Promise => { - const tags = [...(filterOptions.tags?.map(t => `case-workflow.attributes.tags: ${t}`) ?? [])]; + const stateFilter = `case-workflow.attributes.state: ${filterOptions.state}`; + const tags = [ + ...(filterOptions.tags?.reduce((acc, t) => [...acc, `case-workflow.attributes.tags: ${t}`], [ + stateFilter, + ]) ?? [stateFilter]), + ]; const query = { ...queryParams, filter: tags.join(' AND '), diff --git a/x-pack/legacy/plugins/siem/public/containers/case/types.ts b/x-pack/legacy/plugins/siem/public/containers/case/types.ts index 1aea0b0f50a89..3951c61a110d9 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/types.ts +++ b/x-pack/legacy/plugins/siem/public/containers/case/types.ts @@ -47,6 +47,7 @@ export interface QueryParams { export interface FilterOptions { search: string; + state: string; tags: string[]; } @@ -65,7 +66,6 @@ export interface AllCases { } export enum SortFieldCase { createdAt = 'createdAt', - state = 'state', updatedAt = 'updatedAt', } diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx index f85f7c08acb9c..7e1a7c68b0c9e 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx @@ -98,6 +98,7 @@ export const useGetCases = (): [ data: initialData, filterOptions: { search: '', + state: 'open', tags: [], }, queryParams: { diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx index 77eb3af26b4ac..1c42f4e363d38 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx @@ -20,16 +20,35 @@ import * as i18n from './translations'; export type CasesColumns = EuiTableFieldDataColumnType | EuiTableComputedColumnType; +const MediumShadeText = styled.p` + color: ${({ theme }) => theme.eui.euiColorMediumShade}; +`; + const Spacer = styled.span` - margin-left: 8px; + margin-left: ${({ theme }) => theme.eui.paddingSizes.s}; `; +const TempNumberComponent = () => {1}; +TempNumberComponent.displayName = 'TempNumberComponent'; + export const getCasesColumns = (): CasesColumns[] => [ { name: i18n.CASE_TITLE, render: (theCase: Case) => { if (theCase.caseId != null && theCase.title != null) { - return {theCase.title}; + const caseDetailsLinkComponent = ( + {theCase.title} + ); + return theCase.state === 'open' ? ( + caseDetailsLinkComponent + ) : ( + <> + + {caseDetailsLinkComponent} + {i18n.CLOSED} + + + ); } return getEmptyTagValue(); }, @@ -37,19 +56,21 @@ export const getCasesColumns = (): CasesColumns[] => [ { field: 'createdBy', name: i18n.REPORTER, - render: (createdBy: Case['createdBy']) => - createdBy != null ? ( - <> - - {createdBy.username} - - ) : ( - getEmptyTagValue() - ), + render: (createdBy: Case['createdBy']) => { + if (createdBy != null) { + return ( + <> + + {createdBy.username} + + ); + } + return getEmptyTagValue(); + }, }, { field: 'tags', @@ -76,12 +97,10 @@ export const getCasesColumns = (): CasesColumns[] => [ }, { align: 'right', - field: 'updatedAt', // TO DO once we have commentCount returned in the API: https://github.com/elastic/kibana/issues/58525 + field: 'commentCount', // TO DO once we have commentCount returned in the API: https://github.com/elastic/kibana/issues/58525 name: i18n.COMMENTS, sortable: true, - render: () => { - return {1}; - }, + render: TempNumberComponent, }, { field: 'createdAt', diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx index a753b63931b3f..950a11a424792 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx @@ -14,6 +14,7 @@ import { } from '@elastic/eui'; import { isEmpty } from 'lodash/fp'; import { EuiTableSelectionType } from '@elastic/eui/src/components/basic_table/table_types'; +import styled from 'styled-components'; import * as i18n from './translations'; import { getCasesColumns } from './columns'; @@ -22,7 +23,6 @@ import { SortFieldCase, Case, FilterOptions } from '../../../../containers/case/ import { useGetCases } from '../../../../containers/case/use_get_cases'; import { EuiBasicTableOnChange } from '../../../detection_engine/rules/types'; import { Panel } from '../../../../components/panel'; -import { HeaderSection } from '../../../../components/header_section'; import { CasesTableFilters } from './table_filters'; import { @@ -33,11 +33,12 @@ import { } from '../../../../components/detection_engine/utility_bar'; import { getCreateCaseUrl } from '../../../../components/link_to'; +const Div = styled.div` + margin-top: ${({ theme }) => theme.eui.paddingSizes.m}; +`; const getSortField = (field: string): SortFieldCase => { if (field === SortFieldCase.createdAt) { return SortFieldCase.createdAt; - } else if (field === SortFieldCase.state) { - return SortFieldCase.state; } else if (field === SortFieldCase.updatedAt) { return SortFieldCase.updatedAt; } @@ -104,17 +105,19 @@ export const AllCases = React.memo(() => { return ( - - - + {isLoading && isEmpty(data.cases) && ( )} {!isLoading && !isEmpty(data.cases) && ( - <> +
@@ -146,7 +149,7 @@ export const AllCases = React.memo(() => { selection={euiBasicTableSelectionProps} sorting={sorting} /> - +
)}
); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/table_filters.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/table_filters.tsx index e593623788046..5256fb6d7b3ee 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/table_filters.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/table_filters.tsx @@ -6,20 +6,22 @@ import React, { useCallback, useState } from 'react'; import { isEqual } from 'lodash/fp'; -import { EuiFieldSearch, EuiFilterGroup, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { + EuiFieldSearch, + EuiFilterButton, + EuiFilterGroup, + EuiFlexGroup, + EuiFlexItem, +} from '@elastic/eui'; import * as i18n from './translations'; import { FilterOptions } from '../../../../containers/case/types'; import { useGetTags } from '../../../../containers/case/use_get_tags'; -import { TagsFilterPopover } from '../../../../pages/detection_engine/rules/all/rules_table_filters/tags_filter_popover'; +import { FilterPopover } from '../../../../components/filter_popover'; -interface Initial { - search: string; - tags: string[]; -} interface CasesTableFiltersProps { onFilterChanged: (filterOptions: Partial) => void; - initial: Initial; + initial: FilterOptions; } /** @@ -31,17 +33,18 @@ interface CasesTableFiltersProps { const CasesTableFiltersComponent = ({ onFilterChanged, - initial = { search: '', tags: [] }, + initial = { search: '', tags: [], state: 'open' }, }: CasesTableFiltersProps) => { const [search, setSearch] = useState(initial.search); const [selectedTags, setSelectedTags] = useState(initial.tags); - const [{ isLoading, data }] = useGetTags(); + const [showOpenCases, setShowOpenCases] = useState(initial.state === 'open'); + const [{ data }] = useGetTags(); const handleSelectedTags = useCallback( newTags => { if (!isEqual(newTags, selectedTags)) { setSelectedTags(newTags); - onFilterChanged({ search, tags: newTags }); + onFilterChanged({ tags: newTags }); } }, [search, selectedTags] @@ -51,12 +54,20 @@ const CasesTableFiltersComponent = ({ const trimSearch = newSearch.trim(); if (!isEqual(trimSearch, search)) { setSearch(trimSearch); - onFilterChanged({ tags: selectedTags, search: trimSearch }); + onFilterChanged({ search: trimSearch }); } }, [search, selectedTags] ); - + const handleToggleFilter = useCallback( + showOpen => { + if (showOpen !== showOpenCases) { + setShowOpenCases(showOpen); + onFilterChanged({ state: showOpen ? 'open' : 'closed' }); + } + }, + [showOpenCases] + ); return ( @@ -71,11 +82,32 @@ const CasesTableFiltersComponent = ({ - + {i18n.OPEN_CASES} + + + {i18n.CLOSED_CASES} + + {}} + selectedOptions={[]} + options={[]} + optionsEmptyLabel={i18n.NO_REPORTERS_AVAILABLE} + /> + diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/translations.ts index ab8e22ebcf1be..a20fb066850a9 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/translations.ts @@ -8,9 +8,6 @@ import { i18n } from '@kbn/i18n'; export * from '../../translations'; -export const ALL_CASES = i18n.translate('xpack.siem.case.caseTable.title', { - defaultMessage: 'All Cases', -}); export const NO_CASES = i18n.translate('xpack.siem.case.caseTable.noCases.title', { defaultMessage: 'No Cases', }); @@ -46,3 +43,13 @@ export const SEARCH_PLACEHOLDER = i18n.translate( defaultMessage: 'e.g. case name', } ); +export const OPEN_CASES = i18n.translate('xpack.siem.case.caseTable.openCases', { + defaultMessage: 'Open cases', +}); +export const CLOSED_CASES = i18n.translate('xpack.siem.case.caseTable.closedCases', { + defaultMessage: 'Closed cases', +}); + +export const CLOSED = i18n.translate('xpack.siem.case.caseTable.closed', { + defaultMessage: 'Closed', +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/translations.ts index 70298b64ff069..71620cfdb54c5 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/case/translations.ts @@ -81,6 +81,17 @@ export const TAGS = i18n.translate('xpack.siem.case.caseView.tags', { defaultMessage: 'Tags', }); +export const NO_TAGS_AVAILABLE = i18n.translate('xpack.siem.case.allCases.noTagsAvailable', { + defaultMessage: 'No tags available', +}); + +export const NO_REPORTERS_AVAILABLE = i18n.translate( + 'xpack.siem.case.caseView.noReportersAvailable', + { + defaultMessage: 'No reporters available.', + } +); + export const COMMENTS = i18n.translate('xpack.siem.case.allCases.comments', { defaultMessage: 'Comments', }); From f470c65b95ade438f91ce5dd0c404ab27c3b7aa1 Mon Sep 17 00:00:00 2001 From: stephmilovic Date: Mon, 2 Mar 2020 20:41:04 -0700 Subject: [PATCH 05/12] working on bulk actions --- .../__snapshots__/utility_bar.test.tsx.snap | 0 .../utility_bar_action.test.tsx.snap | 0 .../utility_bar_group.test.tsx.snap | 0 .../utility_bar_section.test.tsx.snap | 0 .../utility_bar_text.test.tsx.snap | 0 .../utility_bar/index.ts | 0 .../utility_bar/styles.tsx | 0 .../utility_bar/utility_bar.test.tsx | 2 +- .../utility_bar/utility_bar.tsx | 0 .../utility_bar/utility_bar_action.test.tsx | 2 +- .../utility_bar/utility_bar_action.tsx | 2 +- .../utility_bar/utility_bar_group.test.tsx | 2 +- .../utility_bar/utility_bar_group.tsx | 0 .../utility_bar/utility_bar_section.test.tsx | 2 +- .../utility_bar/utility_bar_section.tsx | 0 .../utility_bar/utility_bar_text.test.tsx | 2 +- .../utility_bar/utility_bar_text.tsx | 0 .../pages/case/components/all_cases/index.tsx | 29 ++++++++- .../case/components/all_cases/translations.ts | 30 ++++++---- .../case/components/bulk_actions/index.tsx | 60 +++++++++++++++++++ .../components/bulk_actions/translations.ts | 21 +++++++ .../components/activity_monitor/index.tsx | 2 +- .../signals/signals_utility_bar/index.tsx | 2 +- .../detection_engine/rules/all/index.tsx | 2 +- 24 files changed, 135 insertions(+), 23 deletions(-) rename x-pack/legacy/plugins/siem/public/components/{detection_engine => }/utility_bar/__snapshots__/utility_bar.test.tsx.snap (100%) rename x-pack/legacy/plugins/siem/public/components/{detection_engine => }/utility_bar/__snapshots__/utility_bar_action.test.tsx.snap (100%) rename x-pack/legacy/plugins/siem/public/components/{detection_engine => }/utility_bar/__snapshots__/utility_bar_group.test.tsx.snap (100%) rename x-pack/legacy/plugins/siem/public/components/{detection_engine => }/utility_bar/__snapshots__/utility_bar_section.test.tsx.snap (100%) rename x-pack/legacy/plugins/siem/public/components/{detection_engine => }/utility_bar/__snapshots__/utility_bar_text.test.tsx.snap (100%) rename x-pack/legacy/plugins/siem/public/components/{detection_engine => }/utility_bar/index.ts (100%) rename x-pack/legacy/plugins/siem/public/components/{detection_engine => }/utility_bar/styles.tsx (100%) rename x-pack/legacy/plugins/siem/public/components/{detection_engine => }/utility_bar/utility_bar.test.tsx (98%) rename x-pack/legacy/plugins/siem/public/components/{detection_engine => }/utility_bar/utility_bar.tsx (100%) rename x-pack/legacy/plugins/siem/public/components/{detection_engine => }/utility_bar/utility_bar_action.test.tsx (95%) rename x-pack/legacy/plugins/siem/public/components/{detection_engine => }/utility_bar/utility_bar_action.tsx (97%) rename x-pack/legacy/plugins/siem/public/components/{detection_engine => }/utility_bar/utility_bar_group.test.tsx (93%) rename x-pack/legacy/plugins/siem/public/components/{detection_engine => }/utility_bar/utility_bar_group.tsx (100%) rename x-pack/legacy/plugins/siem/public/components/{detection_engine => }/utility_bar/utility_bar_section.test.tsx (94%) rename x-pack/legacy/plugins/siem/public/components/{detection_engine => }/utility_bar/utility_bar_section.tsx (100%) rename x-pack/legacy/plugins/siem/public/components/{detection_engine => }/utility_bar/utility_bar_text.test.tsx (92%) rename x-pack/legacy/plugins/siem/public/components/{detection_engine => }/utility_bar/utility_bar_text.tsx (100%) create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/bulk_actions/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/bulk_actions/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/__snapshots__/utility_bar.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/utility_bar/__snapshots__/utility_bar.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/__snapshots__/utility_bar.test.tsx.snap rename to x-pack/legacy/plugins/siem/public/components/utility_bar/__snapshots__/utility_bar.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/__snapshots__/utility_bar_action.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/utility_bar/__snapshots__/utility_bar_action.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/__snapshots__/utility_bar_action.test.tsx.snap rename to x-pack/legacy/plugins/siem/public/components/utility_bar/__snapshots__/utility_bar_action.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/__snapshots__/utility_bar_group.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/utility_bar/__snapshots__/utility_bar_group.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/__snapshots__/utility_bar_group.test.tsx.snap rename to x-pack/legacy/plugins/siem/public/components/utility_bar/__snapshots__/utility_bar_group.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/__snapshots__/utility_bar_section.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/utility_bar/__snapshots__/utility_bar_section.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/__snapshots__/utility_bar_section.test.tsx.snap rename to x-pack/legacy/plugins/siem/public/components/utility_bar/__snapshots__/utility_bar_section.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/__snapshots__/utility_bar_text.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/utility_bar/__snapshots__/utility_bar_text.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/__snapshots__/utility_bar_text.test.tsx.snap rename to x-pack/legacy/plugins/siem/public/components/utility_bar/__snapshots__/utility_bar_text.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/index.ts b/x-pack/legacy/plugins/siem/public/components/utility_bar/index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/index.ts rename to x-pack/legacy/plugins/siem/public/components/utility_bar/index.ts diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/styles.tsx b/x-pack/legacy/plugins/siem/public/components/utility_bar/styles.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/styles.tsx rename to x-pack/legacy/plugins/siem/public/components/utility_bar/styles.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar.test.tsx b/x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar.test.tsx similarity index 98% rename from x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar.test.tsx rename to x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar.test.tsx index eae0fc4ff422b..5fd010362be10 100644 --- a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar.test.tsx @@ -8,7 +8,7 @@ import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; import { mount, shallow } from 'enzyme'; import React from 'react'; -import { TestProviders } from '../../../mock'; +import { TestProviders } from '../../mock'; import { UtilityBar, UtilityBarAction, diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar.tsx b/x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar.tsx rename to x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_action.test.tsx b/x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar_action.test.tsx similarity index 95% rename from x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_action.test.tsx rename to x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar_action.test.tsx index 2a8a71955a986..09c62773fddd1 100644 --- a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_action.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar_action.test.tsx @@ -7,7 +7,7 @@ import { mount, shallow } from 'enzyme'; import React from 'react'; -import { TestProviders } from '../../../mock'; +import { TestProviders } from '../../mock'; import { UtilityBarAction } from './index'; describe('UtilityBarAction', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_action.tsx b/x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar_action.tsx similarity index 97% rename from x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_action.tsx rename to x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar_action.tsx index 4e850a0a11957..d3e2be0e8f816 100644 --- a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_action.tsx +++ b/x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar_action.tsx @@ -7,7 +7,7 @@ import { EuiPopover } from '@elastic/eui'; import React, { useCallback, useState } from 'react'; -import { LinkIcon, LinkIconProps } from '../../link_icon'; +import { LinkIcon, LinkIconProps } from '../link_icon'; import { BarAction } from './styles'; const Popover = React.memo( diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_group.test.tsx b/x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar_group.test.tsx similarity index 93% rename from x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_group.test.tsx rename to x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar_group.test.tsx index e18e7d5e0b524..8e184e5aaec30 100644 --- a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_group.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar_group.test.tsx @@ -7,7 +7,7 @@ import { shallow } from 'enzyme'; import React from 'react'; -import { TestProviders } from '../../../mock'; +import { TestProviders } from '../../mock'; import { UtilityBarGroup, UtilityBarText } from './index'; describe('UtilityBarGroup', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_group.tsx b/x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar_group.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_group.tsx rename to x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar_group.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_section.test.tsx b/x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar_section.test.tsx similarity index 94% rename from x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_section.test.tsx rename to x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar_section.test.tsx index f849fa4b4ee46..c6037c75670eb 100644 --- a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_section.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar_section.test.tsx @@ -7,7 +7,7 @@ import { shallow } from 'enzyme'; import React from 'react'; -import { TestProviders } from '../../../mock'; +import { TestProviders } from '../../mock'; import { UtilityBarGroup, UtilityBarSection, UtilityBarText } from './index'; describe('UtilityBarSection', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_section.tsx b/x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar_section.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_section.tsx rename to x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar_section.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_text.test.tsx b/x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar_text.test.tsx similarity index 92% rename from x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_text.test.tsx rename to x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar_text.test.tsx index 230dd80b1a86b..fcfc2b6b0cefa 100644 --- a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_text.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar_text.test.tsx @@ -7,7 +7,7 @@ import { shallow } from 'enzyme'; import React from 'react'; -import { TestProviders } from '../../../mock'; +import { TestProviders } from '../../mock'; import { UtilityBarText } from './index'; describe('UtilityBarText', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_text.tsx b/x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar_text.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_text.tsx rename to x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar_text.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx index 950a11a424792..92a2ba1200b52 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx @@ -8,6 +8,7 @@ import React, { useCallback, useMemo } from 'react'; import { EuiBasicTable, EuiButton, + EuiContextMenuPanel, EuiEmptyPrompt, EuiLoadingContent, EuiTableSortingType, @@ -27,11 +28,13 @@ import { CasesTableFilters } from './table_filters'; import { UtilityBar, + UtilityBarAction, UtilityBarGroup, UtilityBarSection, UtilityBarText, -} from '../../../../components/detection_engine/utility_bar'; +} from '../../../../components/utility_bar'; import { getCreateCaseUrl } from '../../../../components/link_to'; +import { getBulkItems } from '../bulk_actions'; const Div = styled.div` margin-top: ${({ theme }) => theme.eui.paddingSizes.m}; @@ -92,6 +95,18 @@ export const AllCases = React.memo(() => { [data, queryParams] ); + const getBulkItemsPopoverContent = useCallback( + (closePopover: () => void) => ( + + ), + [selectedCases] + ); + const sorting: EuiTableSortingType = { sort: { field: queryParams.sortField, direction: queryParams.sortOrder }, }; @@ -125,6 +140,18 @@ export const AllCases = React.memo(() => { {i18n.SHOWING_CASES(data.total ?? 0)} + + + {i18n.SELECTED_CASES(selectedCases.length)} + + + {i18n.BULK_ACTIONS} + + + i18n.translate('xpack.siem.case.caseTable.selectedCasesTitle', { + values: { totalRules }, + defaultMessage: 'Selected {totalRules} {totalRules, plural, =1 {case} other {cases}}', + }); + export const SHOWING_CASES = (totalRules: number) => i18n.translate('xpack.siem.case.caseTable.showingCasesTitle', { values: { totalRules }, @@ -30,19 +36,17 @@ export const UNIT = (totalCount: number) => defaultMessage: `{totalCount, plural, =1 {case} other {cases}}`, }); -export const SEARCH_CASES = i18n.translate( - 'xpack.siem.detectionEngine.case.caseTable.searchAriaLabel', - { - defaultMessage: 'Search cases', - } -); - -export const SEARCH_PLACEHOLDER = i18n.translate( - 'xpack.siem.detectionEngine.case.caseTable.searchPlaceholder', - { - defaultMessage: 'e.g. case name', - } -); +export const SEARCH_CASES = i18n.translate('xpack.siem.case.caseTable.searchAriaLabel', { + defaultMessage: 'Search cases', +}); + +export const BULK_ACTIONS = i18n.translate('xpack.siem.case.caseTable.bulkActions', { + defaultMessage: 'Bulk actions', +}); + +export const SEARCH_PLACEHOLDER = i18n.translate('xpack.siem.case.caseTable.searchPlaceholder', { + defaultMessage: 'e.g. case name', +}); export const OPEN_CASES = i18n.translate('xpack.siem.case.caseTable.openCases', { defaultMessage: 'Open cases', }); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/bulk_actions/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/bulk_actions/index.tsx new file mode 100644 index 0000000000000..eb9991f12a30e --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/bulk_actions/index.tsx @@ -0,0 +1,60 @@ +/* + * 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 { EuiContextMenuItem } from '@elastic/eui'; +import React from 'react'; +import * as i18n from './translations'; +import { Case } from '../../../../containers/case/types'; + +interface GetBulkItems { + // cases: Case[]; + closePopover: () => void; + // dispatch: Dispatch; + // dispatchToaster: Dispatch; + // reFetchCases: (refreshPrePackagedCase?: boolean) => void; + selectedCases: Case[]; +} + +export const getBulkItems = ({ + // cases, + closePopover, + // dispatch, + // dispatchToaster, + // reFetchCases, + selectedCases, +}: GetBulkItems) => { + return [ + { + closePopover(); + // await duplicateCasesAction( + // cases.filter(c => selectedCases.includes(c.caseId)), + // selectedCases, + // dispatch, + // dispatchToaster + // ); + // reFetchCases(true); + }} + > + {i18n.BULK_ACTION_DUPLICATE_SELECTED} + , + { + closePopover(); + // await deleteCasesAction(selectedCases, dispatch, dispatchToaster); + // reFetchCases(true); + }} + > + {i18n.BULK_ACTION_DELETE_SELECTED} + , + ]; +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/bulk_actions/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/bulk_actions/translations.ts new file mode 100644 index 0000000000000..a782ef9dcf8d5 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/bulk_actions/translations.ts @@ -0,0 +1,21 @@ +/* + * 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 BULK_ACTION_DUPLICATE_SELECTED = i18n.translate( + 'xpack.siem.case.caseTable.bulkActions.duplicateSelectedTitle', + { + defaultMessage: 'Duplicate selected…', + } +); + +export const BULK_ACTION_DELETE_SELECTED = i18n.translate( + 'xpack.siem.case.caseTable.bulkActions.deleteSelectedTitle', + { + defaultMessage: 'Delete selected…', + } +); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/activity_monitor/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/activity_monitor/index.tsx index 4c7cfac33c546..31420ad07cd50 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/activity_monitor/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/activity_monitor/index.tsx @@ -13,7 +13,7 @@ import { UtilityBarGroup, UtilityBarSection, UtilityBarText, -} from '../../../../components/detection_engine/utility_bar'; +} from '../../../../components/utility_bar'; import { columns } from './columns'; import { ColumnTypes, PageTypes, SortTypes } from './types'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/index.tsx index 86772eb0e155d..25c0424cadf11 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/index.tsx @@ -13,7 +13,7 @@ import { UtilityBarGroup, UtilityBarSection, UtilityBarText, -} from '../../../../../components/detection_engine/utility_bar'; +} from '../../../../../components/utility_bar'; import * as i18n from './translations'; import { useUiSetting$ } from '../../../../../lib/kibana'; import { DEFAULT_NUMBER_FORMAT } from '../../../../../../common/constants'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/index.tsx index 79fec526faf48..01e59ee282d8b 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/index.tsx @@ -29,7 +29,7 @@ import { UtilityBarGroup, UtilityBarSection, UtilityBarText, -} from '../../../../components/detection_engine/utility_bar'; +} from '../../../../components/utility_bar'; import { useStateToaster } from '../../../../components/toasters'; import { Loader } from '../../../../components/loader'; import { Panel } from '../../../../components/panel'; From 2c2925cadf77b1a99d2a4d2fa0fe7305c1062db1 Mon Sep 17 00:00:00 2001 From: stephmilovic Date: Tue, 3 Mar 2020 08:47:29 -0700 Subject: [PATCH 06/12] about to hook up open closed stats --- .../plugins/siem/public/pages/case/case.tsx | 22 ++++++++++- .../case/components/bulk_actions/index.tsx | 4 +- .../components/bulk_actions/translations.ts | 4 +- .../components/open_closed_stats/index.tsx | 37 +++++++++++++++++++ 4 files changed, 61 insertions(+), 6 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/open_closed_stats/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/case/case.tsx b/x-pack/legacy/plugins/siem/public/pages/case/case.tsx index 15a6d076f1009..f20de4b8e8430 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/case.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/case.tsx @@ -7,18 +7,36 @@ import React from 'react'; import { EuiButton, EuiButtonIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import styled, { css } from 'styled-components'; import { CaseHeaderPage } from './components/case_header_page'; import { WrapperPage } from '../../components/wrapper_page'; import { AllCases } from './components/all_cases'; import { SpyRoute } from '../../utils/route/spy_routes'; import * as i18n from './translations'; import { getCreateCaseUrl, getConfigureCasesUrl } from '../../components/link_to'; +import { OpenClosedStats } from './components/open_closed_stats'; + +const FlexItemDivider = styled(EuiFlexItem)` + ${({ theme }) => css` + .euiFlexGroup--gutterMedium > &.euiFlexItem { + border-right: ${theme.eui.euiBorderThin}; + padding-right: ${theme.eui.euiSize}; + margin-right: ${theme.eui.euiSize}; + } + `} +`; export const CasesPage = React.memo(() => ( <> - - + + + + + + + + {i18n.CREATE_TITLE} diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/bulk_actions/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/bulk_actions/index.tsx index eb9991f12a30e..a79a436da537e 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/bulk_actions/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/bulk_actions/index.tsx @@ -30,7 +30,7 @@ export const getBulkItems = ({ { closePopover(); // await duplicateCasesAction( @@ -47,7 +47,7 @@ export const getBulkItems = ({ { closePopover(); // await deleteCasesAction(selectedCases, dispatch, dispatchToaster); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/bulk_actions/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/bulk_actions/translations.ts index a782ef9dcf8d5..ce000b6b425dc 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/bulk_actions/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/bulk_actions/translations.ts @@ -9,13 +9,13 @@ import { i18n } from '@kbn/i18n'; export const BULK_ACTION_DUPLICATE_SELECTED = i18n.translate( 'xpack.siem.case.caseTable.bulkActions.duplicateSelectedTitle', { - defaultMessage: 'Duplicate selected…', + defaultMessage: 'Duplicate selected', } ); export const BULK_ACTION_DELETE_SELECTED = i18n.translate( 'xpack.siem.case.caseTable.bulkActions.deleteSelectedTitle', { - defaultMessage: 'Delete selected…', + defaultMessage: 'Delete selected', } ); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/open_closed_stats/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/open_closed_stats/index.tsx new file mode 100644 index 0000000000000..ab0dc98796ad0 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/open_closed_stats/index.tsx @@ -0,0 +1,37 @@ +/* + * 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, { useMemo } from 'react'; +import { EuiDescriptionList } from '@elastic/eui'; +import * as i18n from '../all_cases/translations'; + +export interface Props { + open?: number; + closed?: number; +} + +export const OpenClosedStats = React.memo(({ closed, open }) => { + const openClosedStats = useMemo( + () => + open + ? [ + { + title: i18n.OPEN_CASES, + description: open as number, + }, + ] + : [ + { + title: i18n.CLOSED_CASES, + description: closed as number, + }, + ], + [open, closed] + ); + return ; +}); + +OpenClosedStats.displayName = 'OpenClosedStats'; From 3512499cc5884d72d3d7662ec0442786834cd21f Mon Sep 17 00:00:00 2001 From: stephmilovic Date: Tue, 3 Mar 2020 10:18:10 -0700 Subject: [PATCH 07/12] errs, need to pause tho --- .../public/containers/case/use_get_cases.tsx | 115 +++++++++++------- .../plugins/siem/public/pages/case/case.tsx | 4 +- .../pages/case/components/all_cases/index.tsx | 8 +- .../components/open_closed_stats/index.tsx | 45 +++---- 4 files changed, 104 insertions(+), 68 deletions(-) diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx index 7e1a7c68b0c9e..1bca17a4a60ca 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx @@ -6,58 +6,77 @@ import { Dispatch, SetStateAction, useEffect, useReducer, useState } from 'react'; import { isEqual } from 'lodash/fp'; -import { - DEFAULT_TABLE_ACTIVE_PAGE, - DEFAULT_TABLE_LIMIT, - FETCH_FAILURE, - FETCH_INIT, - FETCH_SUCCESS, - UPDATE_QUERY_PARAMS, - UPDATE_FILTER_OPTIONS, - UPDATE_TABLE_SELECTIONS, -} from './constants'; +import { DEFAULT_TABLE_ACTIVE_PAGE, DEFAULT_TABLE_LIMIT } from './constants'; import { AllCases, SortFieldCase, FilterOptions, QueryParams, Case } from './types'; -import { getTypedPayload } from './utils'; import { errorToToaster } from '../../components/ml/api/error_to_toaster'; import { useStateToaster } from '../../components/toasters'; import * as i18n from './translations'; import { getCases } from './api'; export interface UseGetCasesState { + caseCount: CaseCount; data: AllCases; - isLoading: boolean; + filterOptions: FilterOptions; isError: boolean; + isLoading: boolean; + loading: string[]; queryParams: QueryParams; - filterOptions: FilterOptions; selectedCases: Case[]; } -export interface Action { - type: string; - payload?: AllCases | Partial | FilterOptions | Case[]; +interface CaseCount { + open: number; + closed: number; } + +export type Action = + | { type: 'FETCH_INIT'; payload: 'cases' | 'caseCount' } + | { type: 'FETCH_CASE_COUNT_SUCCESS'; payload: Partial } + | { type: 'FETCH_CASES_SUCCESS'; payload: AllCases } + | { type: 'FETCH_FAILURE'; payload: 'cases' | 'caseCount' } + | { type: 'UPDATE_FILTER_OPTIONS'; payload: FilterOptions } + | { type: 'UPDATE_QUERY_PARAMS'; payload: Partial } + | { type: 'UPDATE_TABLE_SELECTIONS'; payload: Case[] }; + const dataFetchReducer = (state: UseGetCasesState, action: Action): UseGetCasesState => { switch (action.type) { - case FETCH_INIT: + case 'FETCH_INIT': return { ...state, isLoading: true, isError: false, + loading: [...state.loading, action.payload], }; - case FETCH_SUCCESS: + case 'FETCH_CASE_COUNT_SUCCESS': + return { + ...state, + caseCount: { + ...state.caseCount, + ...action.payload, + }, + loading: state.loading.filter(e => e !== 'caseCount'), + }; + case 'FETCH_CASES_SUCCESS': return { ...state, isLoading: false, isError: false, - data: getTypedPayload(action.payload), + data: action.payload, + loading: state.loading.filter(e => e !== 'cases'), }; - case FETCH_FAILURE: + case 'FETCH_FAILURE': return { ...state, isLoading: false, isError: true, + loading: state.loading.filter(e => e !== action.payload), }; - case UPDATE_QUERY_PARAMS: + case 'UPDATE_FILTER_OPTIONS': + return { + ...state, + filterOptions: action.payload, + }; + case 'UPDATE_QUERY_PARAMS': return { ...state, queryParams: { @@ -65,15 +84,10 @@ const dataFetchReducer = (state: UseGetCasesState, action: Action): UseGetCasesS ...action.payload, }, }; - case UPDATE_FILTER_OPTIONS: - return { - ...state, - filterOptions: getTypedPayload(action.payload), - }; - case UPDATE_TABLE_SELECTIONS: + case 'UPDATE_TABLE_SELECTIONS': return { ...state, - selectedCases: getTypedPayload(action.payload), + selectedCases: action.payload, }; default: throw new Error(); @@ -81,26 +95,32 @@ const dataFetchReducer = (state: UseGetCasesState, action: Action): UseGetCasesS }; const initialData: AllCases = { + cases: [], page: 0, perPage: 0, total: 0, - cases: [], }; export const useGetCases = (): [ UseGetCasesState, Dispatch>, Dispatch>>, - Dispatch> + Dispatch>, + Dispatch ] => { const [state, dispatch] = useReducer(dataFetchReducer, { - isLoading: false, - isError: false, + caseCount: { + open: 0, + closed: 0, + }, data: initialData, filterOptions: { search: '', state: 'open', tags: [], }, + isError: false, + isLoading: false, + loading: [], queryParams: { page: DEFAULT_TABLE_ACTIVE_PAGE, perPage: DEFAULT_TABLE_LIMIT, @@ -109,31 +129,31 @@ export const useGetCases = (): [ }, selectedCases: [], }); - const [queryParams, setQueryParams] = useState>(state.queryParams); + const [, dispatchToaster] = useStateToaster(); const [filterQuery, setFilters] = useState(state.filterOptions); + const [queryParams, setQueryParams] = useState>(state.queryParams); const [selectedCases, setSelectedCases] = useState(state.selectedCases); - const [, dispatchToaster] = useStateToaster(); useEffect(() => { - dispatch({ type: UPDATE_TABLE_SELECTIONS, payload: selectedCases }); + dispatch({ type: 'UPDATE_TABLE_SELECTIONS', payload: selectedCases }); }, [selectedCases]); useEffect(() => { if (!isEqual(queryParams, state.queryParams)) { - dispatch({ type: UPDATE_QUERY_PARAMS, payload: queryParams }); + dispatch({ type: 'UPDATE_QUERY_PARAMS', payload: queryParams }); } }, [queryParams, state.queryParams]); useEffect(() => { if (!isEqual(filterQuery, state.filterOptions)) { - dispatch({ type: UPDATE_FILTER_OPTIONS, payload: filterQuery }); + dispatch({ type: 'UPDATE_FILTER_OPTIONS', payload: filterQuery }); } }, [filterQuery, state.filterOptions]); useEffect(() => { let didCancel = false; const fetchData = async () => { - dispatch({ type: FETCH_INIT }); + dispatch({ type: 'FETCH_INIT', payload: 'cases' }); try { const response = await getCases({ filterOptions: state.filterOptions, @@ -141,14 +161,14 @@ export const useGetCases = (): [ }); if (!didCancel) { dispatch({ - type: FETCH_SUCCESS, + type: 'FETCH_CASES_SUCCESS', payload: response, }); } } catch (error) { if (!didCancel) { errorToToaster({ title: i18n.ERROR_TITLE, error, dispatchToaster }); - dispatch({ type: FETCH_FAILURE }); + dispatch({ type: 'FETCH_FAILURE', payload: 'cases' }); } } }; @@ -157,5 +177,18 @@ export const useGetCases = (): [ didCancel = true; }; }, [state.queryParams, state.filterOptions]); - return [state, setFilters, setQueryParams, setSelectedCases]; + + const getCaseCount = async (caseState: keyof CaseCount) => { + dispatch({ type: 'FETCH_INIT', payload: 'caseCount' }); + try { + const response = await getCases({ + filterOptions: { search: '', state: caseState, tags: [] }, + }); + dispatch({ type: 'FETCH_CASE_COUNT_SUCCESS', payload: { [caseState]: response.total } }); + } catch (error) { + errorToToaster({ title: i18n.ERROR_TITLE, error, dispatchToaster }); + dispatch({ type: 'FETCH_FAILURE', payload: 'caseCount' }); + } + }; + return [state, setFilters, setQueryParams, setSelectedCases, getCaseCount]; }; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/case.tsx b/x-pack/legacy/plugins/siem/public/pages/case/case.tsx index f20de4b8e8430..4469a8bdc6534 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/case.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/case.tsx @@ -32,10 +32,10 @@ export const CasesPage = React.memo(() => ( - + - + diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx index 92a2ba1200b52..9775cb02f394f 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx @@ -49,7 +49,7 @@ const getSortField = (field: string): SortFieldCase => { }; export const AllCases = React.memo(() => { const [ - { data, isLoading, queryParams, filterOptions, selectedCases }, + { data, isLoading, loading, queryParams, filterOptions, selectedCases }, setFilters, setQueryParams, setSelectedCases, @@ -119,7 +119,7 @@ export const AllCases = React.memo(() => { ); return ( - + -1}> { state: filterOptions.state, }} /> - {isLoading && isEmpty(data.cases) && ( + {isLoading && loading.indexOf('cases') > -1 && isEmpty(data.cases) && ( )} - {!isLoading && !isEmpty(data.cases) && ( + {(!isLoading || loading.indexOf('cases') === -1) && !isEmpty(data.cases) && (
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/open_closed_stats/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/open_closed_stats/index.tsx index ab0dc98796ad0..9072f2bf0415b 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/open_closed_stats/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/open_closed_stats/index.tsx @@ -4,33 +4,36 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useMemo } from 'react'; -import { EuiDescriptionList } from '@elastic/eui'; +import React, { useEffect, useMemo } from 'react'; +import { EuiDescriptionList, EuiLoadingSpinner } from '@elastic/eui'; import * as i18n from '../all_cases/translations'; +import { useGetCases } from '../../../../containers/case/use_get_cases'; export interface Props { - open?: number; + caseState: 'open' | 'closed'; closed?: number; } -export const OpenClosedStats = React.memo(({ closed, open }) => { - const openClosedStats = useMemo( - () => - open - ? [ - { - title: i18n.OPEN_CASES, - description: open as number, - }, - ] - : [ - { - title: i18n.CLOSED_CASES, - description: closed as number, - }, - ], - [open, closed] - ); +export const OpenClosedStats = React.memo(({ caseState }) => { + const [{ caseCount, isLoading, loading }, , , , getCaseCount] = useGetCases(); + + useEffect(() => { + getCaseCount(caseState); + }, [caseState]); + + const openClosedStats = useMemo(() => { + return [ + { + title: caseState === 'open' ? i18n.OPEN_CASES : i18n.CLOSED_CASES, + description: + isLoading && loading.indexOf('caseCount') > -1 ? ( + + ) : ( + caseCount[caseState] + ), + }, + ]; + }, [caseCount, caseState, isLoading, loading]); return ; }); From be604dfc5ae695e9b140b0d5e97ac19258af99b2 Mon Sep 17 00:00:00 2001 From: stephmilovic Date: Tue, 3 Mar 2020 11:43:23 -0700 Subject: [PATCH 08/12] adding actions --- .../case/components/all_cases/columns.tsx | 15 +++++- .../pages/case/components/all_cases/index.tsx | 46 ++++++++++++++++++- 2 files changed, 57 insertions(+), 4 deletions(-) diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx index 8e15dd563844f..6b709a0ef8547 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx @@ -8,9 +8,11 @@ import { EuiBadge, EuiTableFieldDataColumnType, EuiTableComputedColumnType, + EuiTableActionsColumnType, EuiAvatar, } from '@elastic/eui'; import styled from 'styled-components'; +import { DefaultItemIconButtonAction } from '@elastic/eui/src/components/basic_table/action_types'; import { getEmptyTagValue } from '../../../../components/empty_value'; import { Case } from '../../../../containers/case/types'; import { FormattedRelativePreferenceDate } from '../../../../components/formatted_date'; @@ -18,7 +20,10 @@ import { CaseDetailsLink } from '../../../../components/links'; import { TruncatableText } from '../../../../components/truncatable_text'; import * as i18n from './translations'; -export type CasesColumns = EuiTableFieldDataColumnType | EuiTableComputedColumnType; +export type CasesColumns = + | EuiTableFieldDataColumnType + | EuiTableComputedColumnType + | EuiTableActionsColumnType; const MediumShadeText = styled.p` color: ${({ theme }) => theme.eui.euiColorMediumShade}; @@ -31,7 +36,9 @@ const Spacer = styled.span` const TempNumberComponent = () => {1}; TempNumberComponent.displayName = 'TempNumberComponent'; -export const getCasesColumns = (): CasesColumns[] => [ +export const getCasesColumns = ( + actions: Array> +): CasesColumns[] => [ { name: i18n.NAME, render: (theCase: Case) => { @@ -118,4 +125,8 @@ export const getCasesColumns = (): CasesColumns[] => [ return getEmptyTagValue(); }, }, + { + name: 'Actions', + actions, + }, ]; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx index 9775cb02f394f..cf2eb1fc57269 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx @@ -16,6 +16,7 @@ import { import { isEmpty } from 'lodash/fp'; import { EuiTableSelectionType } from '@elastic/eui/src/components/basic_table/table_types'; import styled from 'styled-components'; +import { DefaultItemIconButtonAction } from '@elastic/eui/src/components/basic_table/action_types'; import * as i18n from './translations'; import { getCasesColumns } from './columns'; @@ -84,7 +85,46 @@ export const AllCases = React.memo(() => { [filterOptions, setFilters] ); - const memoizedGetCasesColumns = useMemo(() => getCasesColumns(), []); + const actions: Array> = useMemo( + () => [ + { + description: 'Delete', + icon: 'trash', + name: 'Delete', + onClick: (theCase: Case) => console.log('Delete case', theCase), + type: 'icon', + 'data-test-subj': 'action-delete', + }, + filterOptions.state === 'open' + ? { + description: 'Close case', + icon: 'magnet', + name: 'Close case', + onClick: (theCase: Case) => console.log('Close case', theCase), + type: 'icon', + 'data-test-subj': 'action-close', + } + : { + description: 'Reopen case', + icon: 'magnet', + name: 'Reopen case', + onClick: (theCase: Case) => console.log('Reopen case', theCase), + type: 'icon', + 'data-test-subj': 'action-open', + }, + { + description: 'To do', + icon: 'magnet', + name: 'To do', + onClick: (theCase: Case) => console.log('To do', theCase), + type: 'icon', + 'data-test-subj': 'action-to-do', + }, + ], + [filterOptions.state] + ); + + const memoizedGetCasesColumns = useMemo(() => getCasesColumns(actions), [filterOptions.state]); const memoizedPagination = useMemo( () => ({ pageIndex: queryParams.page - 1, @@ -129,7 +169,9 @@ export const AllCases = React.memo(() => { }} /> {isLoading && loading.indexOf('cases') > -1 && isEmpty(data.cases) && ( - +
+ +
)} {(!isLoading || loading.indexOf('cases') === -1) && !isEmpty(data.cases) && (
From 4a5d798acb710703bbe95cc77d75dc56d96be55b Mon Sep 17 00:00:00 2001 From: stephmilovic Date: Tue, 3 Mar 2020 16:20:11 -0700 Subject: [PATCH 09/12] some actions added --- .../public/containers/case/use_get_cases.tsx | 69 ++++- .../containers/case/use_update_case.tsx | 2 +- .../plugins/siem/public/pages/case/case.tsx | 34 --- .../case/components/all_cases/actions.tsx | 49 ++++ .../pages/case/components/all_cases/index.tsx | 240 ++++++++++-------- .../case/components/all_cases/translations.ts | 12 + .../components/open_closed_stats/index.tsx | 51 ++-- 7 files changed, 285 insertions(+), 172 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/actions.tsx diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx index 1bca17a4a60ca..f5422b4d42a24 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx @@ -4,14 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Dispatch, SetStateAction, useEffect, useReducer, useState } from 'react'; +import { Dispatch, SetStateAction, useCallback, useEffect, useReducer, useState } from 'react'; import { isEqual } from 'lodash/fp'; import { DEFAULT_TABLE_ACTIVE_PAGE, DEFAULT_TABLE_LIMIT } from './constants'; import { AllCases, SortFieldCase, FilterOptions, QueryParams, Case } from './types'; import { errorToToaster } from '../../components/ml/api/error_to_toaster'; import { useStateToaster } from '../../components/toasters'; import * as i18n from './translations'; -import { getCases } from './api'; +import { UpdateByKey } from './use_update_case'; +import { getCases, updateCaseProperty } from './api'; export interface UseGetCasesState { caseCount: CaseCount; @@ -24,16 +25,22 @@ export interface UseGetCasesState { selectedCases: Case[]; } -interface CaseCount { +export interface CaseCount { open: number; closed: number; } +interface UpdateCase extends UpdateByKey { + caseId: string; + version: string; +} + export type Action = - | { type: 'FETCH_INIT'; payload: 'cases' | 'caseCount' } + | { type: 'FETCH_INIT'; payload: string } | { type: 'FETCH_CASE_COUNT_SUCCESS'; payload: Partial } | { type: 'FETCH_CASES_SUCCESS'; payload: AllCases } - | { type: 'FETCH_FAILURE'; payload: 'cases' | 'caseCount' } + | { type: 'FETCH_FAILURE'; payload: string } + | { type: 'FETCH_UPDATE_CASE_SUCCESS' } | { type: 'UPDATE_FILTER_OPTIONS'; payload: FilterOptions } | { type: 'UPDATE_QUERY_PARAMS'; payload: Partial } | { type: 'UPDATE_TABLE_SELECTIONS'; payload: Case[] }; @@ -45,7 +52,12 @@ const dataFetchReducer = (state: UseGetCasesState, action: Action): UseGetCasesS ...state, isLoading: true, isError: false, - loading: [...state.loading, action.payload], + loading: [...state.loading.filter(e => e !== action.payload), action.payload], + }; + case 'FETCH_UPDATE_CASE_SUCCESS': + return { + ...state, + loading: state.loading.filter(e => e !== 'caseUpdate'), }; case 'FETCH_CASE_COUNT_SUCCESS': return { @@ -105,7 +117,8 @@ export const useGetCases = (): [ Dispatch>, Dispatch>>, Dispatch>, - Dispatch + Dispatch, + Dispatch ] => { const [state, dispatch] = useReducer(dataFetchReducer, { caseCount: { @@ -133,6 +146,7 @@ export const useGetCases = (): [ const [filterQuery, setFilters] = useState(state.filterOptions); const [queryParams, setQueryParams] = useState>(state.queryParams); const [selectedCases, setSelectedCases] = useState(state.selectedCases); + const [doUpdate, setDoUpdate] = useState(false); useEffect(() => { dispatch({ type: 'UPDATE_TABLE_SELECTIONS', payload: selectedCases }); @@ -150,7 +164,7 @@ export const useGetCases = (): [ } }, [filterQuery, state.filterOptions]); - useEffect(() => { + const fetchCases = useCallback(() => { let didCancel = false; const fetchData = async () => { dispatch({ type: 'FETCH_INIT', payload: 'cases' }); @@ -177,6 +191,15 @@ export const useGetCases = (): [ didCancel = true; }; }, [state.queryParams, state.filterOptions]); + useEffect(() => fetchCases(), [state.queryParams, state.filterOptions]); + useEffect(() => { + if (doUpdate) { + fetchCases(); + getCaseCount('open'); + getCaseCount('closed'); + setDoUpdate(false); + } + }, [doUpdate]); const getCaseCount = async (caseState: keyof CaseCount) => { dispatch({ type: 'FETCH_INIT', payload: 'caseCount' }); @@ -190,5 +213,33 @@ export const useGetCases = (): [ dispatch({ type: 'FETCH_FAILURE', payload: 'caseCount' }); } }; - return [state, setFilters, setQueryParams, setSelectedCases, getCaseCount]; + + const dispatchUpdateCaseProperty = async ({ + updateKey, + updateValue, + caseId, + version, + }: UpdateCase) => { + dispatch({ type: 'FETCH_INIT', payload: 'caseUpdate' }); + try { + await updateCaseProperty( + caseId, + { [updateKey]: updateValue }, + version ?? '' // saved object versions are typed as string | undefined, hope that's not true + ); + setDoUpdate(true); + dispatch({ type: 'FETCH_UPDATE_CASE_SUCCESS' }); + } catch (error) { + errorToToaster({ title: i18n.ERROR_TITLE, error, dispatchToaster }); + dispatch({ type: 'FETCH_FAILURE', payload: 'caseUpdate' }); + } + }; + return [ + state, + setFilters, + setQueryParams, + setSelectedCases, + getCaseCount, + dispatchUpdateCaseProperty, + ]; }; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx index ebbb1e14dc237..f23be526fbeb7 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx @@ -22,7 +22,7 @@ interface NewCaseState { updateKey: UpdateKey | null; } -interface UpdateByKey { +export interface UpdateByKey { updateKey: UpdateKey; updateValue: Case[UpdateKey]; } diff --git a/x-pack/legacy/plugins/siem/public/pages/case/case.tsx b/x-pack/legacy/plugins/siem/public/pages/case/case.tsx index 4469a8bdc6534..9255dee461940 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/case.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/case.tsx @@ -6,47 +6,13 @@ import React from 'react'; -import { EuiButton, EuiButtonIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import styled, { css } from 'styled-components'; -import { CaseHeaderPage } from './components/case_header_page'; import { WrapperPage } from '../../components/wrapper_page'; import { AllCases } from './components/all_cases'; import { SpyRoute } from '../../utils/route/spy_routes'; -import * as i18n from './translations'; -import { getCreateCaseUrl, getConfigureCasesUrl } from '../../components/link_to'; -import { OpenClosedStats } from './components/open_closed_stats'; - -const FlexItemDivider = styled(EuiFlexItem)` - ${({ theme }) => css` - .euiFlexGroup--gutterMedium > &.euiFlexItem { - border-right: ${theme.eui.euiBorderThin}; - padding-right: ${theme.eui.euiSize}; - margin-right: ${theme.eui.euiSize}; - } - `} -`; export const CasesPage = React.memo(() => ( <> - - - - - - - - - - - {i18n.CREATE_TITLE} - - - - - - - diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/actions.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/actions.tsx new file mode 100644 index 0000000000000..b2d8de5721163 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/actions.tsx @@ -0,0 +1,49 @@ +/* + * 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 { DefaultItemIconButtonAction } from '@elastic/eui/src/components/basic_table/action_types'; +import { Case } from '../../../../containers/case/types'; +import * as i18n from './translations'; + +export const getActions = ( + currentState: string, + updateTheState: (theCase: Case, updateValue: 'open' | 'closed') => void +): Array> => [ + { + description: i18n.DELETE, + icon: 'trash', + name: i18n.DELETE, + // eslint-disable-next-line no-console + onClick: ({ caseId }: Case) => console.log('TO DO Delete case', caseId), + type: 'icon', + 'data-test-subj': 'action-delete', + }, + currentState === 'open' + ? { + description: i18n.CLOSE_CASE, + icon: 'magnet', + name: i18n.CLOSE_CASE, + onClick: (theCase: Case) => updateTheState(theCase, 'closed'), + type: 'icon', + 'data-test-subj': 'action-close', + } + : { + description: i18n.REOPEN_CASE, + icon: 'magnet', + name: i18n.REOPEN_CASE, + onClick: (theCase: Case) => updateTheState(theCase, 'open'), + type: 'icon', + 'data-test-subj': 'action-open', + }, + { + description: i18n.DUPLICATE_CASE, + icon: 'copy', + name: i18n.DUPLICATE_CASE, + // eslint-disable-next-line no-console + onClick: ({ caseId }: Case) => console.log('TO DO Duplicate case', caseId), + type: 'icon', + 'data-test-subj': 'action-to-do', + }, +]; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx index cf2eb1fc57269..ed33138a4b56f 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx @@ -8,19 +8,21 @@ import React, { useCallback, useMemo } from 'react'; import { EuiBasicTable, EuiButton, + EuiButtonIcon, EuiContextMenuPanel, EuiEmptyPrompt, + EuiFlexGroup, + EuiFlexItem, EuiLoadingContent, EuiTableSortingType, } from '@elastic/eui'; import { isEmpty } from 'lodash/fp'; import { EuiTableSelectionType } from '@elastic/eui/src/components/basic_table/table_types'; -import styled from 'styled-components'; -import { DefaultItemIconButtonAction } from '@elastic/eui/src/components/basic_table/action_types'; +import styled, { css } from 'styled-components'; import * as i18n from './translations'; import { getCasesColumns } from './columns'; -import { SortFieldCase, Case, FilterOptions } from '../../../../containers/case/types'; +import { Case, FilterOptions, SortFieldCase } from '../../../../containers/case/types'; import { useGetCases } from '../../../../containers/case/use_get_cases'; import { EuiBasicTableOnChange } from '../../../detection_engine/rules/types'; @@ -34,12 +36,24 @@ import { UtilityBarSection, UtilityBarText, } from '../../../../components/utility_bar'; -import { getCreateCaseUrl } from '../../../../components/link_to'; +import { getConfigureCasesUrl, getCreateCaseUrl } from '../../../../components/link_to'; import { getBulkItems } from '../bulk_actions'; +import { CaseHeaderPage } from '../case_header_page'; +import { OpenClosedStats } from '../open_closed_stats'; +import { getActions } from './actions'; const Div = styled.div` margin-top: ${({ theme }) => theme.eui.paddingSizes.m}; `; +const FlexItemDivider = styled(EuiFlexItem)` + ${({ theme }) => css` + .euiFlexGroup--gutterMedium > &.euiFlexItem { + border-right: ${theme.eui.euiBorderThin}; + padding-right: ${theme.eui.euiSize}; + margin-right: ${theme.eui.euiSize}; + } + `} +`; const getSortField = (field: string): SortFieldCase => { if (field === SortFieldCase.createdAt) { return SortFieldCase.createdAt; @@ -50,10 +64,12 @@ const getSortField = (field: string): SortFieldCase => { }; export const AllCases = React.memo(() => { const [ - { data, isLoading, loading, queryParams, filterOptions, selectedCases }, + { caseCount, data, isLoading, loading, queryParams, filterOptions, selectedCases }, setFilters, setQueryParams, setSelectedCases, + getCaseCount, + dispatchUpdateCaseProperty, ] = useGetCases(); const tableOnChangeCallback = useCallback( @@ -85,44 +101,15 @@ export const AllCases = React.memo(() => { [filterOptions, setFilters] ); - const actions: Array> = useMemo( - () => [ - { - description: 'Delete', - icon: 'trash', - name: 'Delete', - onClick: (theCase: Case) => console.log('Delete case', theCase), - type: 'icon', - 'data-test-subj': 'action-delete', - }, - filterOptions.state === 'open' - ? { - description: 'Close case', - icon: 'magnet', - name: 'Close case', - onClick: (theCase: Case) => console.log('Close case', theCase), - type: 'icon', - 'data-test-subj': 'action-close', - } - : { - description: 'Reopen case', - icon: 'magnet', - name: 'Reopen case', - onClick: (theCase: Case) => console.log('Reopen case', theCase), - type: 'icon', - 'data-test-subj': 'action-open', - }, - { - description: 'To do', - icon: 'magnet', - name: 'To do', - onClick: (theCase: Case) => console.log('To do', theCase), - type: 'icon', - 'data-test-subj': 'action-to-do', - }, - ], - [filterOptions.state] + const updateTheState = useCallback( + ({ caseId, version }: Case, updateValue: 'open' | 'closed') => { + dispatchUpdateCaseProperty({ updateKey: 'state', updateValue, caseId, version }); + }, + [] ); + const actions = useMemo(() => getActions(filterOptions.state, updateTheState), [ + filterOptions.state, + ]); const memoizedGetCasesColumns = useMemo(() => getCasesColumns(actions), [filterOptions.state]); const memoizedPagination = useMemo( @@ -157,70 +144,115 @@ export const AllCases = React.memo(() => { }), [selectedCases] ); + const isCasesLoading = useMemo( + () => + (isLoading && (loading.indexOf('cases') > -1 || loading.indexOf('caseUpdate') > -1)) || + (isLoading && isEmpty(data.cases)), + [isLoading, loading, data] + ); + const isCasesReady = useMemo( + () => + !isLoading && + !isEmpty(data.cases) && + !(loading.indexOf('cases') > -1 || loading.indexOf('caseUpdate') > -1), + [isLoading, loading, data] + ); return ( - -1}> - - {isLoading && loading.indexOf('cases') > -1 && isEmpty(data.cases) && ( -
- -
- )} - {(!isLoading || loading.indexOf('cases') === -1) && !isEmpty(data.cases) && ( -
- - - - - {i18n.SHOWING_CASES(data.total ?? 0)} - - - - - {i18n.SELECTED_CASES(selectedCases.length)} - - - {i18n.BULK_ACTIONS} - - - - - {i18n.NO_CASES}} - titleSize="xs" - body={i18n.NO_CASES_BODY} - actions={ - - {i18n.ADD_NEW_CASE} - - } - /> - } - onChange={tableOnChangeCallback} - pagination={memoizedPagination} - selection={euiBasicTableSelectionProps} - sorting={sorting} - /> -
- )} -
+ <> + + + + + + + + + + + {i18n.CREATE_TITLE} + + + + + + + + + + {isCasesLoading && ( +
+ +
+ )} + {isCasesReady && ( +
+ + + + + {i18n.SHOWING_CASES(data.total ?? 0)} + + + + + {i18n.SELECTED_CASES(selectedCases.length)} + + + {i18n.BULK_ACTIONS} + + + + + {i18n.NO_CASES}} + titleSize="xs" + body={i18n.NO_CASES_BODY} + actions={ + + {i18n.ADD_NEW_CASE} + + } + /> + } + onChange={tableOnChangeCallback} + pagination={memoizedPagination} + selection={euiBasicTableSelectionProps} + sorting={sorting} + /> +
+ )} +
+ ); }); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/translations.ts index f78dc82310a17..4fed5bd87e764 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/translations.ts @@ -57,3 +57,15 @@ export const CLOSED_CASES = i18n.translate('xpack.siem.case.caseTable.closedCase export const CLOSED = i18n.translate('xpack.siem.case.caseTable.closed', { defaultMessage: 'Closed', }); +export const DELETE = i18n.translate('xpack.siem.case.caseTable.delete', { + defaultMessage: 'Delete', +}); +export const REOPEN_CASE = i18n.translate('xpack.siem.case.caseTable.reopenCase', { + defaultMessage: 'Open cases', +}); +export const CLOSE_CASE = i18n.translate('xpack.siem.case.caseTable.closeCase', { + defaultMessage: 'Close case', +}); +export const DUPLICATE_CASE = i18n.translate('xpack.siem.case.caseTable.duplicateCase', { + defaultMessage: 'Duplicate case', +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/open_closed_stats/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/open_closed_stats/index.tsx index 9072f2bf0415b..37b505e00d245 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/open_closed_stats/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/open_closed_stats/index.tsx @@ -4,37 +4,40 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useEffect, useMemo } from 'react'; +import React, { Dispatch, useEffect, useMemo } from 'react'; import { EuiDescriptionList, EuiLoadingSpinner } from '@elastic/eui'; import * as i18n from '../all_cases/translations'; -import { useGetCases } from '../../../../containers/case/use_get_cases'; +import { CaseCount } from '../../../../containers/case/use_get_cases'; export interface Props { + caseCount: CaseCount; caseState: 'open' | 'closed'; - closed?: number; + getCaseCount: Dispatch; + isLoading: boolean; + loading: string[]; } -export const OpenClosedStats = React.memo(({ caseState }) => { - const [{ caseCount, isLoading, loading }, , , , getCaseCount] = useGetCases(); +export const OpenClosedStats = React.memo( + ({ caseCount, caseState, getCaseCount, isLoading, loading }) => { + useEffect(() => { + getCaseCount(caseState); + }, [caseState]); - useEffect(() => { - getCaseCount(caseState); - }, [caseState]); - - const openClosedStats = useMemo(() => { - return [ - { - title: caseState === 'open' ? i18n.OPEN_CASES : i18n.CLOSED_CASES, - description: - isLoading && loading.indexOf('caseCount') > -1 ? ( - - ) : ( - caseCount[caseState] - ), - }, - ]; - }, [caseCount, caseState, isLoading, loading]); - return ; -}); + const openClosedStats = useMemo(() => { + return [ + { + title: caseState === 'open' ? i18n.OPEN_CASES : i18n.CLOSED_CASES, + description: + isLoading && loading.indexOf('caseCount') > -1 ? ( + + ) : ( + caseCount[caseState] + ), + }, + ]; + }, [caseCount, caseState, isLoading, loading]); + return ; + } +); OpenClosedStats.displayName = 'OpenClosedStats'; From e0d985157fe10a184bf190c28af109e07bd974e4 Mon Sep 17 00:00:00 2001 From: stephmilovic Date: Tue, 3 Mar 2020 18:07:56 -0700 Subject: [PATCH 10/12] fix type --- .../components/all_cases/__mock__/index.tsx | 8 ++++- .../case/components/all_cases/columns.tsx | 2 +- .../case/components/all_cases/index.test.tsx | 34 +++++++++---------- .../pages/case/components/all_cases/index.tsx | 6 +++- 4 files changed, 29 insertions(+), 21 deletions(-) diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/__mock__/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/__mock__/index.tsx index 0169493773b74..667a0a8de666f 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/__mock__/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/__mock__/index.tsx @@ -75,6 +75,12 @@ export const useGetCasesMockState: UseGetCasesState = { perPage: 5, total: 10, }, + caseCount: { + open: 0, + closed: 0, + }, + loading: [], + selectedCases: [], isLoading: false, isError: false, queryParams: { @@ -83,5 +89,5 @@ export const useGetCasesMockState: UseGetCasesState = { sortField: SortFieldCase.createdAt, sortOrder: 'desc', }, - filterOptions: { search: '', tags: [] }, + filterOptions: { search: '', tags: [], state: 'open' }, }; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx index 6b709a0ef8547..41a2bdf52d5a1 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx @@ -72,7 +72,7 @@ export const getCasesColumns = ( name={createdBy.fullName ? createdBy.fullName : createdBy.username} size="s" /> - {createdBy.username} + {createdBy.username} ); } diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.test.tsx index 5a87cf53142f7..e0d0edcf15648 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.test.tsx @@ -13,13 +13,23 @@ import { useGetCasesMockState } from './__mock__'; import * as apiHook from '../../../../containers/case/use_get_cases'; describe('AllCases', () => { - const setQueryParams = jest.fn(); const setFilters = jest.fn(); + const setQueryParams = jest.fn(); + const setSelectedCases = jest.fn(); + const getCaseCount = jest.fn(); + const dispatchUpdateCaseProperty = jest.fn(); beforeEach(() => { jest.resetAllMocks(); jest .spyOn(apiHook, 'useGetCases') - .mockReturnValue([useGetCasesMockState, setQueryParams, setFilters]); + .mockReturnValue([ + useGetCasesMockState, + setFilters, + setQueryParams, + setSelectedCases, + getCaseCount, + dispatchUpdateCaseProperty, + ]); moment.tz.setDefault('UTC'); }); it('should render AllCases', () => { @@ -40,12 +50,6 @@ describe('AllCases', () => { .first() .text() ).toEqual(useGetCasesMockState.data.cases[0].title); - expect( - wrapper - .find(`[data-test-subj="case-table-column-state"]`) - .first() - .text() - ).toEqual(useGetCasesMockState.data.cases[0].state); expect( wrapper .find(`span[data-test-subj="case-table-column-tags-0"]`) @@ -54,7 +58,7 @@ describe('AllCases', () => { ).toEqual(useGetCasesMockState.data.cases[0].tags[0]); expect( wrapper - .find(`[data-test-subj="case-table-column-username"]`) + .find(`[data-test-subj="case-table-column-createdBy"]`) .first() .text() ).toEqual(useGetCasesMockState.data.cases[0].createdBy.username); @@ -64,13 +68,6 @@ describe('AllCases', () => { .first() .prop('value') ).toEqual(useGetCasesMockState.data.cases[0].createdAt); - expect( - wrapper - .find(`[data-test-subj="case-table-column-updatedAt"]`) - .first() - .prop('value') - ).toEqual(useGetCasesMockState.data.cases[0].updatedAt); - expect( wrapper .find(`[data-test-subj="case-table-case-count"]`) @@ -85,12 +82,13 @@ describe('AllCases', () => { ); wrapper - .find('[data-test-subj="tableHeaderCell_state_5"] [data-test-subj="tableHeaderSortButton"]') + .find('[data-test-subj="tableHeaderSortButton"]') + .first() .simulate('click'); expect(setQueryParams).toBeCalledWith({ page: 1, perPage: 5, - sortField: 'state', + sortField: 'createdAt', sortOrder: 'asc', }); }); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx index ed33138a4b56f..13a6eade1e6a5 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx @@ -186,7 +186,11 @@ export const AllCases = React.memo(() => { - + From 89ca2955193ee71180843dbaaee2a13da7d8f835 Mon Sep 17 00:00:00 2001 From: stephmilovic Date: Wed, 4 Mar 2020 18:01:17 -0700 Subject: [PATCH 11/12] pr changes --- .../components/filter_popover/index.tsx | 28 ++-- .../public/containers/case/use_get_cases.tsx | 139 ++++++++++-------- .../components/all_cases/__mock__/index.tsx | 1 - .../case/components/all_cases/actions.tsx | 43 ++++-- .../case/components/all_cases/index.test.tsx | 18 +-- .../pages/case/components/all_cases/index.tsx | 69 ++++----- .../case/components/all_cases/translations.ts | 2 +- .../case/components/bulk_actions/index.tsx | 56 ++++--- .../components/bulk_actions/translations.ts | 13 +- .../components/open_closed_stats/index.tsx | 19 +-- 10 files changed, 220 insertions(+), 168 deletions(-) diff --git a/x-pack/legacy/plugins/siem/public/components/filter_popover/index.tsx b/x-pack/legacy/plugins/siem/public/components/filter_popover/index.tsx index d911331c6a6f4..1d269dffeccf5 100644 --- a/x-pack/legacy/plugins/siem/public/components/filter_popover/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/filter_popover/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Dispatch, SetStateAction, useState } from 'react'; +import React, { Dispatch, SetStateAction, useCallback, useState } from 'react'; import { EuiFilterButton, EuiFilterSelectItem, @@ -60,7 +60,13 @@ export const FilterPopoverComponent = ({ optionsEmptyLabel, selectedOptions, }: FilterPopoverProps) => { - const [isTagPopoverOpen, setIsTagPopoverOpen] = useState(false); + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + + const setIsPopoverOpenCb = useCallback(() => setIsPopoverOpen(!isPopoverOpen), [isPopoverOpen]); + const toggleSelectedGroupCb = useCallback( + option => toggleSelectedGroup(option, selectedOptions, onSelectedOptionsChanged), + [selectedOptions, onSelectedOptionsChanged] + ); return ( setIsTagPopoverOpen(!isTagPopoverOpen)} - isSelected={isTagPopoverOpen} + onClick={setIsPopoverOpenCb} + isSelected={isPopoverOpen} numFilters={options.length} hasActiveFilters={selectedOptions.length > 0} numActiveFilters={selectedOptions.length} @@ -78,18 +84,18 @@ export const FilterPopoverComponent = ({ {buttonLabel} } - isOpen={isTagPopoverOpen} - closePopover={() => setIsTagPopoverOpen(!isTagPopoverOpen)} + isOpen={isPopoverOpen} + closePopover={setIsPopoverOpenCb} panelPaddingSize="none" > - {options.map((tag, index) => ( + {options.map((option, index) => ( toggleSelectedGroup(tag, selectedOptions, onSelectedOptionsChanged)} + checked={selectedOptions.includes(option) ? 'on' : undefined} + key={`${index}-${option}`} + onClick={toggleSelectedGroupCb.bind(null, option)} > - {`${tag}`} + {option} ))} diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx index f5422b4d42a24..e73b251477bf3 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx @@ -19,7 +19,6 @@ export interface UseGetCasesState { data: AllCases; filterOptions: FilterOptions; isError: boolean; - isLoading: boolean; loading: string[]; queryParams: QueryParams; selectedCases: Case[]; @@ -30,7 +29,7 @@ export interface CaseCount { closed: number; } -interface UpdateCase extends UpdateByKey { +export interface UpdateCase extends UpdateByKey { caseId: string; version: string; } @@ -50,7 +49,6 @@ const dataFetchReducer = (state: UseGetCasesState, action: Action): UseGetCasesS case 'FETCH_INIT': return { ...state, - isLoading: true, isError: false, loading: [...state.loading.filter(e => e !== action.payload), action.payload], }; @@ -71,7 +69,6 @@ const dataFetchReducer = (state: UseGetCasesState, action: Action): UseGetCasesS case 'FETCH_CASES_SUCCESS': return { ...state, - isLoading: false, isError: false, data: action.payload, loading: state.loading.filter(e => e !== 'cases'), @@ -79,7 +76,6 @@ const dataFetchReducer = (state: UseGetCasesState, action: Action): UseGetCasesS case 'FETCH_FAILURE': return { ...state, - isLoading: false, isError: true, loading: state.loading.filter(e => e !== action.payload), }; @@ -112,14 +108,14 @@ const initialData: AllCases = { perPage: 0, total: 0, }; -export const useGetCases = (): [ - UseGetCasesState, - Dispatch>, - Dispatch>>, - Dispatch>, - Dispatch, - Dispatch -] => { +interface UseGetCases extends UseGetCasesState { + dispatchUpdateCaseProperty: Dispatch; + getCaseCount: Dispatch; + setFilters: Dispatch>; + setQueryParams: Dispatch>>; + setSelectedCases: Dispatch; +} +export const useGetCases = (): UseGetCases => { const [state, dispatch] = useReducer(dataFetchReducer, { caseCount: { open: 0, @@ -132,7 +128,6 @@ export const useGetCases = (): [ tags: [], }, isError: false, - isLoading: false, loading: [], queryParams: { page: DEFAULT_TABLE_ACTIVE_PAGE, @@ -145,12 +140,10 @@ export const useGetCases = (): [ const [, dispatchToaster] = useStateToaster(); const [filterQuery, setFilters] = useState(state.filterOptions); const [queryParams, setQueryParams] = useState>(state.queryParams); - const [selectedCases, setSelectedCases] = useState(state.selectedCases); - const [doUpdate, setDoUpdate] = useState(false); - useEffect(() => { - dispatch({ type: 'UPDATE_TABLE_SELECTIONS', payload: selectedCases }); - }, [selectedCases]); + const setSelectedCases = useCallback((mySelectedCases: Case[]) => { + dispatch({ type: 'UPDATE_TABLE_SELECTIONS', payload: mySelectedCases }); + }, []); useEffect(() => { if (!isEqual(queryParams, state.queryParams)) { @@ -192,54 +185,72 @@ export const useGetCases = (): [ }; }, [state.queryParams, state.filterOptions]); useEffect(() => fetchCases(), [state.queryParams, state.filterOptions]); - useEffect(() => { - if (doUpdate) { - fetchCases(); - getCaseCount('open'); - getCaseCount('closed'); - setDoUpdate(false); - } - }, [doUpdate]); - const getCaseCount = async (caseState: keyof CaseCount) => { - dispatch({ type: 'FETCH_INIT', payload: 'caseCount' }); - try { - const response = await getCases({ - filterOptions: { search: '', state: caseState, tags: [] }, - }); - dispatch({ type: 'FETCH_CASE_COUNT_SUCCESS', payload: { [caseState]: response.total } }); - } catch (error) { - errorToToaster({ title: i18n.ERROR_TITLE, error, dispatchToaster }); - dispatch({ type: 'FETCH_FAILURE', payload: 'caseCount' }); - } - }; + const getCaseCount = useCallback((caseState: keyof CaseCount) => { + let didCancel = false; + const fetchData = async () => { + dispatch({ type: 'FETCH_INIT', payload: 'caseCount' }); + try { + const response = await getCases({ + filterOptions: { search: '', state: caseState, tags: [] }, + }); + if (!didCancel) { + dispatch({ + type: 'FETCH_CASE_COUNT_SUCCESS', + payload: { [caseState]: response.total }, + }); + } + } catch (error) { + if (!didCancel) { + errorToToaster({ title: i18n.ERROR_TITLE, error, dispatchToaster }); + dispatch({ type: 'FETCH_FAILURE', payload: 'caseCount' }); + } + } + }; + fetchData(); + return () => { + didCancel = true; + }; + }, []); - const dispatchUpdateCaseProperty = async ({ - updateKey, - updateValue, - caseId, - version, - }: UpdateCase) => { - dispatch({ type: 'FETCH_INIT', payload: 'caseUpdate' }); - try { - await updateCaseProperty( - caseId, - { [updateKey]: updateValue }, - version ?? '' // saved object versions are typed as string | undefined, hope that's not true - ); - setDoUpdate(true); - dispatch({ type: 'FETCH_UPDATE_CASE_SUCCESS' }); - } catch (error) { - errorToToaster({ title: i18n.ERROR_TITLE, error, dispatchToaster }); - dispatch({ type: 'FETCH_FAILURE', payload: 'caseUpdate' }); - } - }; - return [ - state, + const dispatchUpdateCaseProperty = useCallback( + ({ updateKey, updateValue, caseId, version }: UpdateCase) => { + let didCancel = false; + const fetchData = async () => { + dispatch({ type: 'FETCH_INIT', payload: 'caseUpdate' }); + try { + await updateCaseProperty( + caseId, + { [updateKey]: updateValue }, + version ?? '' // saved object versions are typed as string | undefined, hope that's not true + ); + if (!didCancel) { + dispatch({ type: 'FETCH_UPDATE_CASE_SUCCESS' }); + fetchCases(); + getCaseCount('open'); + getCaseCount('closed'); + } + } catch (error) { + if (!didCancel) { + errorToToaster({ title: i18n.ERROR_TITLE, error, dispatchToaster }); + dispatch({ type: 'FETCH_FAILURE', payload: 'caseUpdate' }); + } + } + }; + fetchData(); + return () => { + didCancel = true; + }; + }, + [filterQuery, state.filterOptions] + ); + + return { + ...state, + dispatchUpdateCaseProperty, + getCaseCount, setFilters, setQueryParams, setSelectedCases, - getCaseCount, - dispatchUpdateCaseProperty, - ]; + }; }; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/__mock__/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/__mock__/index.tsx index 667a0a8de666f..a054d685399bc 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/__mock__/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/__mock__/index.tsx @@ -81,7 +81,6 @@ export const useGetCasesMockState: UseGetCasesState = { }, loading: [], selectedCases: [], - isLoading: false, isError: false, queryParams: { page: 1, diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/actions.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/actions.tsx index b2d8de5721163..5dad19b1e54d3 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/actions.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/actions.tsx @@ -4,13 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ import { DefaultItemIconButtonAction } from '@elastic/eui/src/components/basic_table/action_types'; +import { Dispatch } from 'react'; import { Case } from '../../../../containers/case/types'; + import * as i18n from './translations'; +import { UpdateCase } from '../../../../containers/case/use_get_cases'; + +interface GetActions { + caseStatus: string; + dispatchUpdate: Dispatch; +} -export const getActions = ( - currentState: string, - updateTheState: (theCase: Case, updateValue: 'open' | 'closed') => void -): Array> => [ +export const getActions = ({ + caseStatus, + dispatchUpdate, +}: GetActions): Array> => [ { description: i18n.DELETE, icon: 'trash', @@ -20,12 +28,18 @@ export const getActions = ( type: 'icon', 'data-test-subj': 'action-delete', }, - currentState === 'open' + caseStatus === 'open' ? { description: i18n.CLOSE_CASE, icon: 'magnet', name: i18n.CLOSE_CASE, - onClick: (theCase: Case) => updateTheState(theCase, 'closed'), + onClick: (theCase: Case) => + dispatchUpdate({ + updateKey: 'state', + updateValue: 'closed', + caseId: theCase.caseId, + version: theCase.version, + }), type: 'icon', 'data-test-subj': 'action-close', } @@ -33,17 +47,14 @@ export const getActions = ( description: i18n.REOPEN_CASE, icon: 'magnet', name: i18n.REOPEN_CASE, - onClick: (theCase: Case) => updateTheState(theCase, 'open'), + onClick: (theCase: Case) => + dispatchUpdate({ + updateKey: 'state', + updateValue: 'open', + caseId: theCase.caseId, + version: theCase.version, + }), type: 'icon', 'data-test-subj': 'action-open', }, - { - description: i18n.DUPLICATE_CASE, - icon: 'copy', - name: i18n.DUPLICATE_CASE, - // eslint-disable-next-line no-console - onClick: ({ caseId }: Case) => console.log('TO DO Duplicate case', caseId), - type: 'icon', - 'data-test-subj': 'action-to-do', - }, ]; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.test.tsx index e0d0edcf15648..dd584f3f716b6 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.test.tsx @@ -20,16 +20,14 @@ describe('AllCases', () => { const dispatchUpdateCaseProperty = jest.fn(); beforeEach(() => { jest.resetAllMocks(); - jest - .spyOn(apiHook, 'useGetCases') - .mockReturnValue([ - useGetCasesMockState, - setFilters, - setQueryParams, - setSelectedCases, - getCaseCount, - dispatchUpdateCaseProperty, - ]); + jest.spyOn(apiHook, 'useGetCases').mockReturnValue({ + ...useGetCasesMockState, + dispatchUpdateCaseProperty, + getCaseCount, + setFilters, + setQueryParams, + setSelectedCases, + }); moment.tz.setDefault('UTC'); }); it('should render AllCases', () => { diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx index 13a6eade1e6a5..484d9051ee43f 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx @@ -14,9 +14,9 @@ import { EuiFlexGroup, EuiFlexItem, EuiLoadingContent, + EuiProgress, EuiTableSortingType, } from '@elastic/eui'; -import { isEmpty } from 'lodash/fp'; import { EuiTableSelectionType } from '@elastic/eui/src/components/basic_table/table_types'; import styled, { css } from 'styled-components'; import * as i18n from './translations'; @@ -54,6 +54,17 @@ const FlexItemDivider = styled(EuiFlexItem)` } `} `; + +const ProgressLoader = styled(EuiProgress)` + ${({ theme }) => css` + .euiFlexGroup--gutterMedium > &.euiFlexItem { + top: 2px; + border-radius: ${theme.eui.euiBorderRadius}; + z-index: ${theme.eui.euiZHeader}; + } + `} +`; + const getSortField = (field: string): SortFieldCase => { if (field === SortFieldCase.createdAt) { return SortFieldCase.createdAt; @@ -63,14 +74,19 @@ const getSortField = (field: string): SortFieldCase => { return SortFieldCase.createdAt; }; export const AllCases = React.memo(() => { - const [ - { caseCount, data, isLoading, loading, queryParams, filterOptions, selectedCases }, + const { + caseCount, + data, + dispatchUpdateCaseProperty, + filterOptions, + getCaseCount, + loading, + queryParams, + selectedCases, setFilters, setQueryParams, setSelectedCases, - getCaseCount, - dispatchUpdateCaseProperty, - ] = useGetCases(); + } = useGetCases(); const tableOnChangeCallback = useCallback( ({ page, sort }: EuiBasicTableOnChange) => { @@ -101,15 +117,11 @@ export const AllCases = React.memo(() => { [filterOptions, setFilters] ); - const updateTheState = useCallback( - ({ caseId, version }: Case, updateValue: 'open' | 'closed') => { - dispatchUpdateCaseProperty({ updateKey: 'state', updateValue, caseId, version }); - }, - [] + const actions = useMemo( + () => + getActions({ caseStatus: filterOptions.state, dispatchUpdate: dispatchUpdateCaseProperty }), + [filterOptions.state, dispatchUpdateCaseProperty] ); - const actions = useMemo(() => getActions(filterOptions.state, updateTheState), [ - filterOptions.state, - ]); const memoizedGetCasesColumns = useMemo(() => getCasesColumns(actions), [filterOptions.state]); const memoizedPagination = useMemo( @@ -128,10 +140,11 @@ export const AllCases = React.memo(() => { items={getBulkItems({ closePopover, selectedCases, + caseStatus: filterOptions.state, })} /> ), - [selectedCases] + [selectedCases, filterOptions.state] ); const sorting: EuiTableSortingType = { @@ -145,18 +158,10 @@ export const AllCases = React.memo(() => { [selectedCases] ); const isCasesLoading = useMemo( - () => - (isLoading && (loading.indexOf('cases') > -1 || loading.indexOf('caseUpdate') > -1)) || - (isLoading && isEmpty(data.cases)), - [isLoading, loading, data] - ); - const isCasesReady = useMemo( - () => - !isLoading && - !isEmpty(data.cases) && - !(loading.indexOf('cases') > -1 || loading.indexOf('caseUpdate') > -1), - [isLoading, loading, data] + () => loading.indexOf('cases') > -1 || loading.indexOf('caseUpdate') > -1, + [loading] ); + const isDataEmpty = useMemo(() => data.total === 0, [data]); return ( <> @@ -167,8 +172,7 @@ export const AllCases = React.memo(() => { caseCount={caseCount} caseState={'open'} getCaseCount={getCaseCount} - isLoading={isLoading} - loading={loading} + isLoading={loading.indexOf('caseCount') > -1} /> @@ -176,8 +180,7 @@ export const AllCases = React.memo(() => { caseCount={caseCount} caseState={'closed'} getCaseCount={getCaseCount} - isLoading={isLoading} - loading={loading} + isLoading={loading.indexOf('caseCount') > -1} /> @@ -194,6 +197,7 @@ export const AllCases = React.memo(() => { + {isCasesLoading && !isDataEmpty && } { state: filterOptions.state, }} /> - {isCasesLoading && ( + {isCasesLoading && isDataEmpty ? (
- )} - {isCasesReady && ( + ) : (
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/translations.ts index 4fed5bd87e764..19117136ed046 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/translations.ts @@ -61,7 +61,7 @@ export const DELETE = i18n.translate('xpack.siem.case.caseTable.delete', { defaultMessage: 'Delete', }); export const REOPEN_CASE = i18n.translate('xpack.siem.case.caseTable.reopenCase', { - defaultMessage: 'Open cases', + defaultMessage: 'Reopen case', }); export const CLOSE_CASE = i18n.translate('xpack.siem.case.caseTable.closeCase', { defaultMessage: 'Close case', diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/bulk_actions/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/bulk_actions/index.tsx index a79a436da537e..9e752ea0db6c3 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/bulk_actions/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/bulk_actions/index.tsx @@ -16,34 +16,56 @@ interface GetBulkItems { // dispatchToaster: Dispatch; // reFetchCases: (refreshPrePackagedCase?: boolean) => void; selectedCases: Case[]; + caseStatus: string; } export const getBulkItems = ({ // cases, closePopover, + caseStatus, // dispatch, // dispatchToaster, // reFetchCases, selectedCases, }: GetBulkItems) => { return [ - { - closePopover(); - // await duplicateCasesAction( - // cases.filter(c => selectedCases.includes(c.caseId)), - // selectedCases, - // dispatch, - // dispatchToaster - // ); - // reFetchCases(true); - }} - > - {i18n.BULK_ACTION_DUPLICATE_SELECTED} - , + caseStatus === 'open' ? ( + { + closePopover(); + // await duplicateCasesAction( + // cases.filter(c => selectedCases.includes(c.caseId)), + // selectedCases, + // dispatch, + // dispatchToaster + // ); + // reFetchCases(true); + }} + > + {i18n.BULK_ACTION_CLOSE_SELECTED} + + ) : ( + { + closePopover(); + // await duplicateCasesAction( + // cases.filter(c => selectedCases.includes(c.caseId)), + // selectedCases, + // dispatch, + // dispatchToaster + // ); + // reFetchCases(true); + }} + > + {i18n.BULK_ACTION_OPEN_SELECTED} + + ), ; isLoading: boolean; - loading: string[]; } export const OpenClosedStats = React.memo( - ({ caseCount, caseState, getCaseCount, isLoading, loading }) => { + ({ caseCount, caseState, getCaseCount, isLoading }) => { useEffect(() => { getCaseCount(caseState); }, [caseState]); - const openClosedStats = useMemo(() => { - return [ + const openClosedStats = useMemo( + () => [ { title: caseState === 'open' ? i18n.OPEN_CASES : i18n.CLOSED_CASES, - description: - isLoading && loading.indexOf('caseCount') > -1 ? ( - - ) : ( - caseCount[caseState] - ), + description: isLoading ? : caseCount[caseState], }, - ]; - }, [caseCount, caseState, isLoading, loading]); + ], + [caseCount, caseState, isLoading] + ); return ; } ); From faf883fd26ebcdb359e2f911dee147afc38f683c Mon Sep 17 00:00:00 2001 From: stephmilovic Date: Wed, 4 Mar 2020 18:09:23 -0700 Subject: [PATCH 12/12] small change to bulk action comments --- .../pages/case/components/bulk_actions/index.tsx | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/bulk_actions/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/bulk_actions/index.tsx index 9e752ea0db6c3..2fe25a7d1f5d0 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/bulk_actions/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/bulk_actions/index.tsx @@ -36,12 +36,7 @@ export const getBulkItems = ({ disabled={true} // TO DO onClick={async () => { closePopover(); - // await duplicateCasesAction( - // cases.filter(c => selectedCases.includes(c.caseId)), - // selectedCases, - // dispatch, - // dispatchToaster - // ); + // await deleteCasesAction(selectedCases, dispatch, dispatchToaster); // reFetchCases(true); }} > @@ -54,12 +49,7 @@ export const getBulkItems = ({ disabled={true} // TO DO onClick={async () => { closePopover(); - // await duplicateCasesAction( - // cases.filter(c => selectedCases.includes(c.caseId)), - // selectedCases, - // dispatch, - // dispatchToaster - // ); + // await deleteCasesAction(selectedCases, dispatch, dispatchToaster); // reFetchCases(true); }} >