Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SIEM] [Cases] Delete, comment, and reducer updates #59616

Merged
merged 19 commits into from
Mar 11, 2020
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ export const MarkdownEditor = React.memo<{
useEffect(() => {
onChange(content);
}, [content]);
useEffect(() => {
setContent(initialContent);
}, [initialContent]);
const tabs = useMemo(
() => [
{
Expand Down
22 changes: 22 additions & 0 deletions x-pack/legacy/plugins/siem/public/containers/case/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,3 +130,25 @@ export const patchComment = async (
await throwIfNotOk(response.response);
return convertToCamelCase<CommentResponse, Comment>(decodeCommentResponse(response.body));
};

export const deleteCases = async (caseIds: string[]): Promise<boolean> => {
Copy link
Member

Choose a reason for hiding this comment

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

What do you think about passing a signal (AbortController) so we can pass it to fetch?

Copy link
Member

@cnasikas cnasikas Mar 10, 2020

Choose a reason for hiding this comment

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

export const deleteCases = async ({caseIds: string[], signal: AbortSignal}): Promise<boolean> => {
  const response = await KibanaServices.get().http.fetch<string>(`${CASES_URL}`, {
    method: 'DELETE',
    asResponse: true,
    signal,
    query: { ids: JSON.stringify(caseIds) },
  });
  await throwIfNotOk(response.response);
  return response.body === 'true' ? true : false;
};

Copy link
Contributor

Choose a reason for hiding this comment

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

related to this comment #59616 (comment)

const response = await KibanaServices.get().http.fetch<string>(`${CASES_URL}`, {
method: 'DELETE',
asResponse: true,
query: { ids: JSON.stringify(caseIds) },
stephmilovic marked this conversation as resolved.
Show resolved Hide resolved
});
await throwIfNotOk(response.response);
return response.body === 'true' ? true : false;
stephmilovic marked this conversation as resolved.
Show resolved Hide resolved
};

export const deleteComment = async (caseId: string, commentId: string): Promise<boolean> => {
stephmilovic marked this conversation as resolved.
Show resolved Hide resolved
const response = await KibanaServices.get().http.fetch<string>(
`${CASES_URL}/${caseId}/comments/${commentId}`,
{
method: 'DELETE',
asResponse: true,
}
);
await throwIfNotOk(response.response);
return response.body === 'true' ? true : false;
stephmilovic marked this conversation as resolved.
Show resolved Hide resolved
};
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,3 @@
export const CASES_URL = `/api/cases`;
export const DEFAULT_TABLE_ACTIVE_PAGE = 1;
export const DEFAULT_TABLE_LIMIT = 5;
export const FETCH_FAILURE = 'FETCH_FAILURE';
Copy link
Contributor

Choose a reason for hiding this comment

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

why do we switch from variables to inline strings?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

to make the Action type in the hooks better

Copy link
Contributor

Choose a reason for hiding this comment

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

cannot we use ActionCreator from typescript-fsa as in the rest of the app?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Screen Shot 2020-03-10 at 11 48 05 AM

It doesn't resolve the type/value issue. take a look at the types in `use_get_cases.tsx`. do you think i should refactor to `ActionCreator`?

export const FETCH_INIT = 'FETCH_INIT';
export const FETCH_SUCCESS = 'FETCH_SUCCESS';
export const POST_NEW_CASE = 'POST_NEW_CASE';
export const POST_NEW_COMMENT = 'POST_NEW_COMMENT';
export const UPDATE_FILTER_OPTIONS = 'UPDATE_FILTER_OPTIONS';
export const UPDATE_TABLE_SELECTIONS = 'UPDATE_TABLE_SELECTIONS';
export const UPDATE_QUERY_PARAMS = 'UPDATE_QUERY_PARAMS';
1 change: 1 addition & 0 deletions x-pack/legacy/plugins/siem/public/containers/case/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export interface Comment {
export interface Case {
id: string;
comments: Comment[];
commentIds: string[];
Copy link
Member

@cnasikas cnasikas Mar 10, 2020

Choose a reason for hiding this comment

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

Why we need the comment ids in a different array? Aren't included in comments array?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@XavierM had reasons

Copy link
Contributor

@XavierM XavierM Mar 10, 2020

Choose a reason for hiding this comment

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

@cnasikas, you will only get the comments through the API if you set the query parameter includeComments but we still need to know how many comments are attached to a case to show it in the table. So to avoid a payload of data to just know the number of comment we are using commentIds.

Copy link
Member

@cnasikas cnasikas Mar 11, 2020

Choose a reason for hiding this comment

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

@XavierM Sorry to insist, but if this is the only use case why the API does not return the count of the comments? Do you think there will be another use case in the future where ids will be needed?

Copy link
Contributor

Choose a reason for hiding this comment

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

I agree with you but since we do not have aggregation on the saved object I did that to avoid query every comment for every case. Might not be the most robust way of doing it. However, it will be faster. When we have aggregation in saved_object, we will be able to use that to just get the count like you said

createdAt: string;
createdBy: ElasticUser;
description: string;
Expand Down
119 changes: 119 additions & 0 deletions x-pack/legacy/plugins/siem/public/containers/case/use_delete_cases.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/*
* 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 { useCallback, useReducer } from 'react';
import { errorToToaster } from '../../components/ml/api/error_to_toaster';
import * as i18n from './translations';
import { useStateToaster } from '../../components/toasters';
import { deleteCases } from './api';

interface DeleteState {
isDisplayConfirmDeleteModal: boolean;
isDeleted: boolean;
isLoading: boolean;
isError: boolean;
}
type Action =
| { type: 'DISPLAY_MODAL'; payload: boolean }
| { type: 'FETCH_INIT' }
| { type: 'FETCH_SUCCESS'; payload: boolean }
| { type: 'FETCH_FAILURE' }
| { type: 'RESET_IS_DELETED' };

const dataFetchReducer = (state: DeleteState, action: Action): DeleteState => {
switch (action.type) {
case 'DISPLAY_MODAL':
return {
...state,
isDisplayConfirmDeleteModal: action.payload,
};
case 'FETCH_INIT':
return {
...state,
isLoading: true,
isError: false,
};
case 'FETCH_SUCCESS':
return {
...state,
isLoading: false,
isError: false,
isDeleted: action.payload,
};
case 'FETCH_FAILURE':
return {
...state,
isLoading: false,
isError: true,
};
case 'RESET_IS_DELETED':
return {
...state,
isDeleted: false,
};
default:
throw new Error();
stephmilovic marked this conversation as resolved.
Show resolved Hide resolved
}
};
interface UseDeleteCase extends DeleteState {
dispatchResetIsDeleted: () => void;
handleOnDeleteConfirm: (caseIds: string[]) => void;
handleToggleModal: () => void;
}

export const useDeleteCases = (): UseDeleteCase => {
Copy link
Member

@cnasikas cnasikas Mar 10, 2020

Choose a reason for hiding this comment

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

What do you think about creating an AbortController, pass it to the API calls and abort in the return function?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

show me?

Copy link
Member

@cnasikas cnasikas Mar 10, 2020

Choose a reason for hiding this comment

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

  const dispatchDeleteCases = useCallback(async (caseIds: string[]) => {
    let cancel = false;
    **const abortCtrl = new AbortController();**
    try {
      dispatch({ type: 'FETCH_INIT' });
      await deleteCases(**{ caseIds, signal: abortCtrl.signal }**);
      if (!cancel) {
        dispatch({ type: 'FETCH_SUCCESS', payload: true });
      }
    } catch (error) {
      if (!cancel) {
        errorToToaster({
          title: i18n.ERROR_TITLE,
          error: error.body && error.body.message ? new Error(error.body.message) : error,
          dispatchToaster,
        });
        dispatch({ type: 'FETCH_FAILURE' });
      }
    }
    return () => {
      cancel = true;
      **abortCtrl.abort();**
    };
  }, []);

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@XavierM thoughts?

Copy link
Contributor

@XavierM XavierM Mar 10, 2020

Choose a reason for hiding this comment

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

That's correct, I notice that too but I was thinking that we can change it to a specific PR later on and create an issue to track it. Therefore we can move forward to this PR and staying true to just do deletions and showing comments. I am saying that because that's not the only places we will have to change it and try to get better to avoid big PR. does that make sense?

const [state, dispatch] = useReducer(dataFetchReducer, {
isDisplayConfirmDeleteModal: false,
isLoading: false,
isError: false,
isDeleted: false,
});
const [, dispatchToaster] = useStateToaster();

const dispatchDeleteCases = useCallback(async (caseIds: string[]) => {
let cancel = false;
try {
dispatch({ type: 'FETCH_INIT' });
await deleteCases(caseIds);
if (!cancel) {
dispatch({ type: 'FETCH_SUCCESS', payload: true });
}
} catch (error) {
if (!cancel) {
errorToToaster({
title: i18n.ERROR_TITLE,
error: error.body && error.body.message ? new Error(error.body.message) : error,
dispatchToaster,
});
dispatch({ type: 'FETCH_FAILURE' });
}
}
return () => {
cancel = true;
};
}, []);

const dispatchToggleDeleteModal = useCallback(() => {
dispatch({ type: 'DISPLAY_MODAL', payload: !state.isDisplayConfirmDeleteModal });
}, [state.isDisplayConfirmDeleteModal]);

const dispatchResetIsDeleted = useCallback(() => {
dispatch({ type: 'RESET_IS_DELETED' });
}, [state.isDisplayConfirmDeleteModal]);

const handleOnDeleteConfirm = useCallback(
caseIds => {
dispatchDeleteCases(caseIds);
stephmilovic marked this conversation as resolved.
Show resolved Hide resolved
dispatchToggleDeleteModal();
},
[state.isDisplayConfirmDeleteModal]
);
const handleToggleModal = useCallback(() => {
dispatchToggleDeleteModal();
}, [state.isDisplayConfirmDeleteModal]);

return { ...state, dispatchResetIsDeleted, handleOnDeleteConfirm, handleToggleModal };
};
30 changes: 15 additions & 15 deletions x-pack/legacy/plugins/siem/public/containers/case/use_get_case.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@
import { useEffect, useReducer } from 'react';

import { Case } from './types';
import { FETCH_INIT, FETCH_FAILURE, FETCH_SUCCESS } from './constants';
import { getTypedPayload } from './utils';
import { errorToToaster } from '../../components/ml/api/error_to_toaster';
import * as i18n from './translations';
import { useStateToaster } from '../../components/toasters';
Expand All @@ -19,27 +17,28 @@ interface CaseState {
isLoading: boolean;
isError: boolean;
}
interface Action {
type: string;
payload?: Case;
}

type Action =
| { type: 'FETCH_INIT' }
| { type: 'FETCH_SUCCESS'; payload: Case }
| { type: 'FETCH_FAILURE' };

const dataFetchReducer = (state: CaseState, action: Action): CaseState => {
switch (action.type) {
case FETCH_INIT:
case 'FETCH_INIT':
return {
...state,
isLoading: true,
isError: false,
};
case FETCH_SUCCESS:
case 'FETCH_SUCCESS':
return {
...state,
isLoading: false,
isError: false,
data: getTypedPayload<Case>(action.payload),
data: action.payload,
};
case FETCH_FAILURE:
case 'FETCH_FAILURE':
return {
...state,
isLoading: false,
Expand All @@ -53,6 +52,7 @@ const initialData: Case = {
id: '',
createdAt: '',
comments: [],
commentIds: [],
createdBy: {
username: '',
},
Expand All @@ -64,7 +64,7 @@ const initialData: Case = {
version: '',
};

export const useGetCase = (caseId: string): [CaseState] => {
export const useGetCase = (caseId: string): CaseState => {
const [state, dispatch] = useReducer(dataFetchReducer, {
isLoading: true,
isError: false,
Expand All @@ -75,11 +75,11 @@ export const useGetCase = (caseId: string): [CaseState] => {
const callFetch = () => {
let didCancel = false;
const fetchData = async () => {
dispatch({ type: FETCH_INIT });
dispatch({ type: 'FETCH_INIT' });
try {
const response = await getCase(caseId);
if (!didCancel) {
dispatch({ type: FETCH_SUCCESS, payload: response });
dispatch({ type: 'FETCH_SUCCESS', payload: response });
}
} catch (error) {
if (!didCancel) {
Expand All @@ -88,7 +88,7 @@ export const useGetCase = (caseId: string): [CaseState] => {
error: error.body && error.body.message ? new Error(error.body.message) : error,
dispatchToaster,
});
dispatch({ type: FETCH_FAILURE });
dispatch({ type: 'FETCH_FAILURE' });
}
}
};
Expand All @@ -101,5 +101,5 @@ export const useGetCase = (caseId: string): [CaseState] => {
useEffect(() => {
callFetch();
}, [caseId]);
return [state];
return state;
};
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ const initialData: AllCases = {
interface UseGetCases extends UseGetCasesState {
dispatchUpdateCaseProperty: ({ updateKey, updateValue, caseId, version }: UpdateCase) => void;
getCaseCount: (caseState: keyof CaseCount) => void;
refetchCases: (filters: FilterOptions, queryParams: QueryParams) => void;
setFilters: (filters: FilterOptions) => void;
setQueryParams: (queryParams: QueryParams) => void;
setSelectedCases: (mySelectedCases: Case[]) => void;
Expand Down Expand Up @@ -246,10 +247,17 @@ export const useGetCases = (): UseGetCases => {
[state.filterOptions, state.queryParams]
);

const refetchCases = useCallback(() => {
fetchCases(state.filterOptions, state.queryParams);
stephmilovic marked this conversation as resolved.
Show resolved Hide resolved
getCaseCount('open');
getCaseCount('closed');
}, [state.filterOptions, state.queryParams]);

return {
...state,
dispatchUpdateCaseProperty,
getCaseCount,
refetchCases,
setFilters,
setQueryParams,
setSelectedCases,
Expand Down
32 changes: 15 additions & 17 deletions x-pack/legacy/plugins/siem/public/containers/case/use_get_tags.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,36 +9,34 @@ import { useStateToaster } from '../../components/toasters';
import { errorToToaster } from '../../components/ml/api/error_to_toaster';

import { getTags } from './api';
import { FETCH_FAILURE, FETCH_INIT, FETCH_SUCCESS } from './constants';
import * as i18n from './translations';

interface TagsState {
data: string[];
tags: string[];
isLoading: boolean;
isError: boolean;
}
interface Action {
type: string;
payload?: string[];
}
type Action =
| { type: 'FETCH_INIT' }
| { type: 'FETCH_SUCCESS'; payload: string[] }
| { type: 'FETCH_FAILURE' };

const dataFetchReducer = (state: TagsState, action: Action): TagsState => {
switch (action.type) {
case FETCH_INIT:
case 'FETCH_INIT':
return {
...state,
isLoading: true,
isError: false,
};
case FETCH_SUCCESS:
const getTypedPayload = (a: Action['payload']) => a as string[];
case 'FETCH_SUCCESS':
return {
...state,
isLoading: false,
isError: false,
data: getTypedPayload(action.payload),
tags: action.payload,
};
case FETCH_FAILURE:
case 'FETCH_FAILURE':
return {
...state,
isLoading: false,
Expand All @@ -50,22 +48,22 @@ const dataFetchReducer = (state: TagsState, action: Action): TagsState => {
};
const initialData: string[] = [];

export const useGetTags = (): [TagsState] => {
export const useGetTags = (): TagsState => {
const [state, dispatch] = useReducer(dataFetchReducer, {
isLoading: false,
isError: false,
data: initialData,
tags: initialData,
});
const [, dispatchToaster] = useStateToaster();

useEffect(() => {
let didCancel = false;
const fetchData = async () => {
dispatch({ type: FETCH_INIT });
dispatch({ type: 'FETCH_INIT' });
try {
const response = await getTags();
if (!didCancel) {
dispatch({ type: FETCH_SUCCESS, payload: response });
dispatch({ type: 'FETCH_SUCCESS', payload: response });
}
} catch (error) {
if (!didCancel) {
Expand All @@ -74,7 +72,7 @@ export const useGetTags = (): [TagsState] => {
error: error.body && error.body.message ? new Error(error.body.message) : error,
dispatchToaster,
});
dispatch({ type: FETCH_FAILURE });
dispatch({ type: 'FETCH_FAILURE' });
}
}
};
Expand All @@ -83,5 +81,5 @@ export const useGetTags = (): [TagsState] => {
didCancel = true;
};
}, []);
return [state];
return state;
};
Loading