Skip to content

Commit

Permalink
[SIEM] [CASES] Status / Batch update (#59856)
Browse files Browse the repository at this point in the history
* add global status count + add status count with find + rename state to status + allow batch update for cases

* fix all my bug and integrate API with UI

* add reporters functionality

* review II

* review III

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
  • Loading branch information
XavierM and elasticmachine committed Mar 13, 2020
1 parent ce1836b commit c6ec5d4
Show file tree
Hide file tree
Showing 49 changed files with 1,042 additions and 481 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -29,19 +29,15 @@ const ScrollableDiv = styled.div`
overflow: auto;
`;

export const toggleSelectedGroup = (
group: string,
selectedGroups: string[],
setSelectedGroups: Dispatch<SetStateAction<string[]>>
): void => {
const toggleSelectedGroup = (group: string, selectedGroups: string[]): string[] => {
const selectedGroupIndex = selectedGroups.indexOf(group);
const updatedSelectedGroups = [...selectedGroups];
if (selectedGroupIndex >= 0) {
updatedSelectedGroups.splice(selectedGroupIndex, 1);
} else {
updatedSelectedGroups.push(group);
return [
...selectedGroups.slice(0, selectedGroupIndex),
...selectedGroups.slice(selectedGroupIndex + 1),
];
}
return setSelectedGroups(updatedSelectedGroups);
return [...selectedGroups, group];
};

/**
Expand All @@ -64,7 +60,7 @@ export const FilterPopoverComponent = ({

const setIsPopoverOpenCb = useCallback(() => setIsPopoverOpen(!isPopoverOpen), [isPopoverOpen]);
const toggleSelectedGroupCb = useCallback(
option => toggleSelectedGroup(option, selectedOptions, onSelectedOptionsChanged),
option => onSelectedOptionsChanged(toggleSelectedGroup(option, selectedOptions)),
[selectedOptions, onSelectedOptionsChanged]
);

Expand Down
56 changes: 37 additions & 19 deletions x-pack/legacy/plugins/siem/public/containers/case/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,26 @@
import {
CaseResponse,
CasesResponse,
CasesFindResponse,
CaseRequest,
CasesStatusResponse,
CommentRequest,
CommentResponse,
User,
} from '../../../../../../plugins/case/common/api';
import { KibanaServices } from '../../lib/kibana';
import { AllCases, Case, Comment, FetchCasesProps, SortFieldCase } from './types';
import { AllCases, Case, CasesStatus, Comment, FetchCasesProps, SortFieldCase } from './types';
import { CASES_URL } from './constants';
import {
convertToCamelCase,
convertAllCasesToCamel,
decodeCaseResponse,
decodeCasesResponse,
decodeCasesFindResponse,
decodeCasesStatusResponse,
decodeCommentResponse,
} from './utils';

const CaseSavedObjectType = 'cases';

export const getCase = async (caseId: string, includeComments: boolean = true): Promise<Case> => {
const response = await KibanaServices.get().http.fetch<CaseResponse>(`${CASES_URL}/${caseId}`, {
method: 'GET',
Expand All @@ -34,17 +37,37 @@ export const getCase = async (caseId: string, includeComments: boolean = true):
return convertToCamelCase<CaseResponse, Case>(decodeCaseResponse(response));
};

export const getCasesStatus = async (signal: AbortSignal): Promise<CasesStatus> => {
const response = await KibanaServices.get().http.fetch<CasesStatusResponse>(
`${CASES_URL}/status`,
{
method: 'GET',
signal,
}
);
return convertToCamelCase<CasesStatusResponse, CasesStatus>(decodeCasesStatusResponse(response));
};

export const getTags = async (): Promise<string[]> => {
const response = await KibanaServices.get().http.fetch<string[]>(`${CASES_URL}/tags`, {
method: 'GET',
});
return response ?? [];
};

export const getReporters = async (signal: AbortSignal): Promise<User[]> => {
const response = await KibanaServices.get().http.fetch<User[]>(`${CASES_URL}/reporters`, {
method: 'GET',
signal,
});
return response ?? [];
};

export const getCases = async ({
filterOptions = {
search: '',
state: 'open',
reporters: [],
status: 'open',
tags: [],
},
queryParams = {
Expand All @@ -54,23 +77,18 @@ export const getCases = async ({
sortOrder: 'desc',
},
}: FetchCasesProps): Promise<AllCases> => {
const stateFilter = `${CaseSavedObjectType}.attributes.state: ${filterOptions.state}`;
const tags = [
...(filterOptions.tags?.reduce(
(acc, t) => [...acc, `${CaseSavedObjectType}.attributes.tags: ${t}`],
[stateFilter]
) ?? [stateFilter]),
];
const query = {
...queryParams,
...(tags.length > 0 ? { filter: tags.join(' AND ') } : {}),
reporters: filterOptions.reporters.map(r => r.username),
tags: filterOptions.tags,
...(filterOptions.status !== '' ? { status: filterOptions.status } : {}),
...(filterOptions.search.length > 0 ? { search: filterOptions.search } : {}),
...queryParams,
};
const response = await KibanaServices.get().http.fetch<CasesResponse>(`${CASES_URL}/_find`, {
const response = await KibanaServices.get().http.fetch<CasesFindResponse>(`${CASES_URL}/_find`, {
method: 'GET',
query,
});
return convertAllCasesToCamel(decodeCasesResponse(response));
return convertAllCasesToCamel(decodeCasesFindResponse(response));
};

export const postCase = async (newCase: CaseRequest): Promise<Case> => {
Expand All @@ -85,12 +103,12 @@ export const patchCase = async (
caseId: string,
updatedCase: Partial<CaseRequest>,
version: string
): Promise<Case> => {
const response = await KibanaServices.get().http.fetch(`${CASES_URL}`, {
): Promise<Case[]> => {
const response = await KibanaServices.get().http.fetch<CasesResponse>(`${CASES_URL}`, {
method: 'PATCH',
body: JSON.stringify({ ...updatedCase, id: caseId, version }),
body: JSON.stringify({ cases: [{ ...updatedCase, id: caseId, version }] }),
});
return convertToCamelCase<CaseResponse, Case>(decodeCaseResponse(response));
return convertToCamelCase<CasesResponse, Case[]>(decodeCasesResponse(response));
};

export const postComment = async (newComment: CommentRequest, caseId: string): Promise<Comment> => {
Expand Down
14 changes: 11 additions & 3 deletions x-pack/legacy/plugins/siem/public/containers/case/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { User } from '../../../../../../plugins/case/common/api';

export interface Comment {
id: string;
createdAt: string;
Expand All @@ -20,7 +22,7 @@ export interface Case {
createdAt: string;
createdBy: ElasticUser;
description: string;
state: string;
status: string;
tags: string[];
title: string;
updatedAt: string;
Expand All @@ -36,11 +38,17 @@ export interface QueryParams {

export interface FilterOptions {
search: string;
state: string;
status: string;
tags: string[];
reporters: User[];
}

export interface CasesStatus {
countClosedCases: number | null;
countOpenCases: number | null;
}

export interface AllCases {
export interface AllCases extends CasesStatus {
cases: Case[];
page: number;
perPage: number;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ const initialData: Case = {
username: '',
},
description: '',
state: '',
status: '',
tags: [],
title: '',
updatedAt: '',
Expand Down
77 changes: 19 additions & 58 deletions x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import { UpdateByKey } from './use_update_case';
import { getCases, patchCase } from './api';

export interface UseGetCasesState {
caseCount: CaseCount;
data: AllCases;
filterOptions: FilterOptions;
isError: boolean;
Expand All @@ -22,20 +21,18 @@ export interface UseGetCasesState {
selectedCases: Case[];
}

export interface CaseCount {
open: number;
closed: number;
}

export interface UpdateCase extends UpdateByKey {
caseId: string;
version: string;
refetchCasesStatus: () => void;
}

export type Action =
| { type: 'FETCH_INIT'; payload: string }
| { type: 'FETCH_CASE_COUNT_SUCCESS'; payload: Partial<CaseCount> }
| { type: 'FETCH_CASES_SUCCESS'; payload: AllCases }
| {
type: 'FETCH_CASES_SUCCESS';
payload: AllCases;
}
| { type: 'FETCH_FAILURE'; payload: string }
| { type: 'FETCH_UPDATE_CASE_SUCCESS' }
| { type: 'UPDATE_FILTER_OPTIONS'; payload: FilterOptions }
Expand All @@ -55,20 +52,11 @@ const dataFetchReducer = (state: UseGetCasesState, action: Action): UseGetCasesS
...state,
loading: state.loading.filter(e => e !== 'caseUpdate'),
};
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,
isError: false,
data: action.payload,
isError: false,
loading: state.loading.filter(e => e !== 'cases'),
};
case 'FETCH_FAILURE':
Expand Down Expand Up @@ -102,28 +90,32 @@ const dataFetchReducer = (state: UseGetCasesState, action: Action): UseGetCasesS

const initialData: AllCases = {
cases: [],
countClosedCases: null,
countOpenCases: null,
page: 0,
perPage: 0,
total: 0,
};
interface UseGetCases extends UseGetCasesState {
dispatchUpdateCaseProperty: ({ updateKey, updateValue, caseId, version }: UpdateCase) => void;
getCaseCount: (caseState: keyof CaseCount) => void;
dispatchUpdateCaseProperty: ({
updateKey,
updateValue,
caseId,
version,
refetchCasesStatus,
}: UpdateCase) => void;
refetchCases: (filters: FilterOptions, queryParams: QueryParams) => void;
setFilters: (filters: FilterOptions) => void;
setQueryParams: (queryParams: QueryParams) => void;
setSelectedCases: (mySelectedCases: Case[]) => void;
}
export const useGetCases = (): UseGetCases => {
const [state, dispatch] = useReducer(dataFetchReducer, {
caseCount: {
open: 0,
closed: 0,
},
data: initialData,
filterOptions: {
search: '',
state: 'open',
reporters: [],
status: 'open',
tags: [],
},
isError: false,
Expand Down Expand Up @@ -187,35 +179,8 @@ export const useGetCases = (): UseGetCases => {
state.filterOptions,
]);

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 = useCallback(
({ updateKey, updateValue, caseId, version }: UpdateCase) => {
({ updateKey, updateValue, caseId, refetchCasesStatus, version }: UpdateCase) => {
let didCancel = false;
const fetchData = async () => {
dispatch({ type: 'FETCH_INIT', payload: 'caseUpdate' });
Expand All @@ -228,8 +193,7 @@ export const useGetCases = (): UseGetCases => {
if (!didCancel) {
dispatch({ type: 'FETCH_UPDATE_CASE_SUCCESS' });
fetchCases(state.filterOptions, state.queryParams);
getCaseCount('open');
getCaseCount('closed');
refetchCasesStatus();
}
} catch (error) {
if (!didCancel) {
Expand All @@ -248,14 +212,11 @@ export const useGetCases = (): UseGetCases => {

const refetchCases = useCallback(() => {
fetchCases(state.filterOptions, state.queryParams);
getCaseCount('open');
getCaseCount('closed');
}, [state.filterOptions, state.queryParams]);

return {
...state,
dispatchUpdateCaseProperty,
getCaseCount,
refetchCases,
setFilters,
setQueryParams,
Expand Down
Loading

0 comments on commit c6ec5d4

Please sign in to comment.