Skip to content

Commit

Permalink
[SIEM] [Case] Enable case by default. Snake to camel on UI (#57936)
Browse files Browse the repository at this point in the history
  • Loading branch information
stephmilovic authored Feb 22, 2020
1 parent 9c8c47b commit 98aa1d2
Show file tree
Hide file tree
Showing 26 changed files with 605 additions and 101 deletions.
5 changes: 4 additions & 1 deletion x-pack/legacy/plugins/siem/public/components/links/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,10 @@ const CaseDetailsLinkComponent: React.FC<{ children?: React.ReactNode; detailNam
children,
detailName,
}) => (
<EuiLink href={getCaseDetailsUrl(encodeURIComponent(detailName))}>
<EuiLink
href={getCaseDetailsUrl(encodeURIComponent(detailName))}
data-test-subj="case-details-link"
>
{children ? children : detailName}
</EuiLink>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ describe('SIEM Navigation', () => {
detailName: undefined,
navTabs: {
case: {
disabled: true,
disabled: false,
href: '#/link-to/case',
id: 'case',
name: 'Case',
Expand Down Expand Up @@ -160,7 +160,7 @@ describe('SIEM Navigation', () => {
filters: [],
navTabs: {
case: {
disabled: true,
disabled: false,
href: '#/link-to/case',
id: 'case',
name: 'Case',
Expand Down
21 changes: 11 additions & 10 deletions x-pack/legacy/plugins/siem/public/containers/case/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@
*/

import { KibanaServices } from '../../lib/kibana';
import { AllCases, FetchCasesProps, Case, NewCase, SortFieldCase } from './types';
import { Direction } from '../../graphql/types';
import { FetchCasesProps, Case, NewCase, SortFieldCase, AllCases, CaseSnake } from './types';
import { throwIfNotOk } from '../../hooks/api/api';
import { CASES_URL } from './constants';
import { convertToCamelCase, convertAllCasesToCamel } from './utils';

export const getCase = async (caseId: string, includeComments: boolean) => {
export const getCase = async (caseId: string, includeComments: boolean): Promise<Case> => {
const response = await KibanaServices.get().http.fetch(`${CASES_URL}/${caseId}`, {
method: 'GET',
asResponse: true,
Expand All @@ -19,7 +19,7 @@ export const getCase = async (caseId: string, includeComments: boolean) => {
},
});
await throwIfNotOk(response.response);
return response.body!;
return convertToCamelCase<CaseSnake, Case>(response.body!);
};

export const getCases = async ({
Expand All @@ -31,7 +31,7 @@ export const getCases = async ({
page: 1,
perPage: 20,
sortField: SortFieldCase.createdAt,
sortOrder: Direction.desc,
sortOrder: 'desc',
},
}: FetchCasesProps): Promise<AllCases> => {
const tags = [...(filterOptions.tags?.map(t => `case-workflow.attributes.tags: ${t}`) ?? [])];
Expand All @@ -46,7 +46,7 @@ export const getCases = async ({
asResponse: true,
});
await throwIfNotOk(response.response);
return response.body!;
return convertAllCasesToCamel(response.body!);
};

export const createCase = async (newCase: NewCase): Promise<Case> => {
Expand All @@ -56,18 +56,19 @@ export const createCase = async (newCase: NewCase): Promise<Case> => {
body: JSON.stringify(newCase),
});
await throwIfNotOk(response.response);
return response.body!;
return convertToCamelCase<CaseSnake, Case>(response.body!);
};

export const updateCaseProperty = async (
caseId: string,
updatedCase: Partial<Case>
updatedCase: Partial<Case>,
version: string
): Promise<Partial<Case>> => {
const response = await KibanaServices.get().http.fetch(`${CASES_URL}/${caseId}`, {
method: 'PATCH',
asResponse: true,
body: JSON.stringify(updatedCase),
body: JSON.stringify({ case: updatedCase, version }),
});
await throwIfNotOk(response.response);
return response.body!;
return convertToCamelCase<Partial<CaseSnake>, Partial<Case>>(response.body!);
};
40 changes: 32 additions & 8 deletions x-pack/legacy/plugins/siem/public/containers/case/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { Direction } from '../../graphql/types';
interface FormData {
isNew?: boolean;
}
Expand All @@ -15,44 +14,69 @@ export interface NewCase extends FormData {
title: string;
}

export interface Case {
export interface CaseSnake {
case_id: string;
created_at: string;
created_by: ElasticUser;
created_by: ElasticUserSnake;
description: string;
state: string;
tags: string[];
title: string;
updated_at: string;
version?: string;
}

export interface Case {
caseId: string;
createdAt: string;
createdBy: ElasticUser;
description: string;
state: string;
tags: string[];
title: string;
updatedAt: string;
version?: string;
}

export interface QueryParams {
page: number;
perPage: number;
sortField: SortFieldCase;
sortOrder: Direction;
sortOrder: 'asc' | 'desc';
}

export interface FilterOptions {
search: string;
tags: string[];
}

export interface AllCasesSnake {
cases: CaseSnake[];
page: number;
per_page: number;
total: number;
}

export interface AllCases {
cases: Case[];
page: number;
per_page: number;
perPage: number;
total: number;
}
export enum SortFieldCase {
createdAt = 'created_at',
createdAt = 'createdAt',
state = 'state',
updatedAt = 'updated_at',
updatedAt = 'updatedAt',
}

export interface ElasticUserSnake {
readonly username: string;
readonly full_name?: string | null;
}

export interface ElasticUser {
readonly username: string;
readonly full_name?: string;
readonly fullName?: string | null;
}

export interface FetchCasesProps {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,16 +50,16 @@ const dataFetchReducer = (state: CaseState, action: Action): CaseState => {
}
};
const initialData: Case = {
case_id: '',
created_at: '',
created_by: {
caseId: '',
createdAt: '',
createdBy: {
username: '',
},
description: '',
state: '',
tags: [],
title: '',
updated_at: '',
updatedAt: '',
};

export const useGetCase = (caseId: string): [CaseState] => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import {
} from './constants';
import { AllCases, SortFieldCase, FilterOptions, QueryParams } from './types';
import { getTypedPayload } from './utils';
import { Direction } from '../../graphql/types';
import { errorToToaster } from '../../components/ml/api/error_to_toaster';
import { useStateToaster } from '../../components/toasters';
import * as i18n from './translations';
Expand All @@ -31,16 +30,9 @@ export interface UseGetCasesState {
filterOptions: FilterOptions;
}

export interface QueryArgs {
page?: number;
perPage?: number;
sortField?: SortFieldCase;
sortOrder?: Direction;
}

export interface Action {
type: string;
payload?: AllCases | QueryArgs | FilterOptions;
payload?: AllCases | Partial<QueryParams> | FilterOptions;
}
const dataFetchReducer = (state: UseGetCasesState, action: Action): UseGetCasesState => {
switch (action.type) {
Expand Down Expand Up @@ -83,13 +75,13 @@ const dataFetchReducer = (state: UseGetCasesState, action: Action): UseGetCasesS

const initialData: AllCases = {
page: 0,
per_page: 0,
perPage: 0,
total: 0,
cases: [],
};
export const useGetCases = (): [
UseGetCasesState,
Dispatch<SetStateAction<QueryArgs>>,
Dispatch<SetStateAction<Partial<QueryParams>>>,
Dispatch<SetStateAction<FilterOptions>>
] => {
const [state, dispatch] = useReducer(dataFetchReducer, {
Expand All @@ -104,11 +96,11 @@ export const useGetCases = (): [
page: DEFAULT_TABLE_ACTIVE_PAGE,
perPage: DEFAULT_TABLE_LIMIT,
sortField: SortFieldCase.createdAt,
sortOrder: Direction.desc,
sortOrder: 'desc',
},
});
const [queryParams, setQueryParams] = useState(state.queryParams as QueryArgs);
const [filterQuery, setFilters] = useState(state.filterOptions as FilterOptions);
const [queryParams, setQueryParams] = useState<Partial<QueryParams>>(state.queryParams);
const [filterQuery, setFilters] = useState<FilterOptions>(state.filterOptions);
const [, dispatchToaster] = useStateToaster();

useEffect(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,11 @@ export const useUpdateCase = (
const updateData = async (updateKey: keyof Case) => {
dispatch({ type: FETCH_INIT });
try {
const response = await updateCaseProperty(caseId, { [updateKey]: state.data[updateKey] });
const response = await updateCaseProperty(
caseId,
{ [updateKey]: state.data[updateKey] },
state.data.version ?? '' // saved object versions are typed as string | undefined, hope that's not true
);
dispatch({ type: FETCH_SUCCESS, payload: response });
} catch (error) {
errorToToaster({ title: i18n.ERROR_TITLE, error, dispatchToaster });
Expand Down
33 changes: 33 additions & 0 deletions x-pack/legacy/plugins/siem/public/containers/case/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,37 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { camelCase, isArray, isObject, set } from 'lodash';
import { AllCases, AllCasesSnake, Case, CaseSnake } from './types';

export const getTypedPayload = <T>(a: unknown): T => a as T;

export const convertArrayToCamelCase = (arrayOfSnakes: unknown[]): unknown[] =>
arrayOfSnakes.reduce((acc: unknown[], value) => {
if (isArray(value)) {
return [...acc, convertArrayToCamelCase(value)];
} else if (isObject(value)) {
return [...acc, convertToCamelCase(value)];
} else {
return [...acc, value];
}
}, []);

export const convertToCamelCase = <T, U extends {}>(snakeCase: T): U =>
Object.entries(snakeCase).reduce((acc, [key, value]) => {
if (isArray(value)) {
set(acc, camelCase(key), convertArrayToCamelCase(value));
} else if (isObject(value)) {
set(acc, camelCase(key), convertToCamelCase(value));
} else {
set(acc, camelCase(key), value);
}
return acc;
}, {} as U);

export const convertAllCasesToCamel = (snakeCases: AllCasesSnake): AllCases => ({
cases: snakeCases.cases.map(snakeCase => convertToCamelCase<CaseSnake, Case>(snakeCase)),
page: snakeCases.page,
perPage: snakeCases.per_page,
total: snakeCases.total,
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* 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 { SortFieldCase } from '../../../../../containers/case/types';
import { UseGetCasesState } from '../../../../../containers/case/use_get_cases';

export const useGetCasesMockState: UseGetCasesState = {
data: {
cases: [
{
caseId: '3c4ddcc0-4e99-11ea-9290-35d05cb55c15',
createdAt: '2020-02-13T19:44:23.627Z',
createdBy: { username: 'elastic' },
description: 'Security banana Issue',
state: 'open',
tags: ['defacement'],
title: 'Another horrible breach',
updatedAt: '2020-02-13T19:44:23.627Z',
},
{
caseId: '362a5c10-4e99-11ea-9290-35d05cb55c15',
createdAt: '2020-02-13T19:44:13.328Z',
createdBy: { username: 'elastic' },
description: 'Security banana Issue',
state: 'open',
tags: ['phishing'],
title: 'Bad email',
updatedAt: '2020-02-13T19:44:13.328Z',
},
{
caseId: '34f8b9e0-4e99-11ea-9290-35d05cb55c15',
createdAt: '2020-02-13T19:44:11.328Z',
createdBy: { username: 'elastic' },
description: 'Security banana Issue',
state: 'open',
tags: ['phishing'],
title: 'Bad email',
updatedAt: '2020-02-13T19:44:11.328Z',
},
{
caseId: '31890e90-4e99-11ea-9290-35d05cb55c15',
createdAt: '2020-02-13T19:44:05.563Z',
createdBy: { username: 'elastic' },
description: 'Security banana Issue',
state: 'closed',
tags: ['phishing'],
title: 'Uh oh',
updatedAt: '2020-02-18T21:32:24.056Z',
},
{
caseId: '2f5b3210-4e99-11ea-9290-35d05cb55c15',
createdAt: '2020-02-13T19:44:01.901Z',
createdBy: { username: 'elastic' },
description: 'Security banana Issue',
state: 'open',
tags: ['phishing'],
title: 'Uh oh',
updatedAt: '2020-02-13T19:44:01.901Z',
},
],
page: 1,
perPage: 5,
total: 10,
},
isLoading: false,
isError: false,
queryParams: {
page: 1,
perPage: 5,
sortField: SortFieldCase.createdAt,
sortOrder: 'desc',
},
filterOptions: { search: '', tags: [] },
};
Loading

0 comments on commit 98aa1d2

Please sign in to comment.