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][Detections] Value Lists Management Modal #67068

Merged
merged 31 commits into from
Jul 14, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
f807600
Add Frontend components for Value Lists Management Modal
rylnd Jun 26, 2020
cc34d86
Update value list components to use newest Lists API
rylnd Jun 27, 2020
af6a86e
Close modal on outside click
rylnd Jun 27, 2020
bf7fb72
Add hook for using a cursor with paged API calls.
rylnd Jun 29, 2020
38fa27c
Better implementation of useCursor
rylnd Jun 29, 2020
b606f57
Fixes useCursor hook functionality
rylnd Jun 29, 2020
ce1edb2
Add cursor to lists query
rylnd Jun 29, 2020
e39116c
Do not validate response of export
rylnd Jun 29, 2020
df70e5c
Fix double callback post-import
rylnd Jun 29, 2020
344b81c
Update ValueListsForm to manually abort import request
rylnd Jun 30, 2020
23fd1ee
Default modal table to five rows
rylnd Jul 1, 2020
e3670ea
Update translation keys following plugin rename
rylnd Jul 1, 2020
c36e959
Try to fit table contents on a single row
rylnd Jul 1, 2020
312fbde
Add helper function to prevent tests from logging errors
rylnd Jul 2, 2020
9587b7e
Add jest tests for our form, table, and modal components
rylnd Jul 2, 2020
ab66483
Fix translation conflict
rylnd Jul 2, 2020
7789c91
Merge branch 'master' into value_lists_ui
rylnd Jul 2, 2020
f16cf4e
Add more waitForUpdates to new overview page tests
rylnd Jul 2, 2020
5be0e87
Fix bad merge resolution
rylnd Jul 2, 2020
0e4fee7
Make cursor an optional parameter to findLists
rylnd Jul 2, 2020
728b272
Tweaking Table column sizes
rylnd Jul 2, 2020
0abbfc4
Fix bug where onSuccess is called upon pagination change
rylnd Jul 3, 2020
e0cb6a5
Merge branch 'master' into value_lists_ui
elasticmachine Jul 6, 2020
18459c1
Merge branch 'master' into value_lists_ui
rylnd Jul 8, 2020
551a435
Merge branch 'master' into value_lists_ui
rylnd Jul 13, 2020
8fb7e4c
Fix failing test
rylnd Jul 13, 2020
e496614
Hide page size options on ValueLists modal table
rylnd Jul 13, 2020
3f12007
Update error callbacks now that we have Errors
rylnd Jul 13, 2020
bc984c4
Synchronize delete with the subsequent fetch
rylnd Jul 13, 2020
66d8241
Cast our unknown error to an Error
rylnd Jul 14, 2020
59e3b02
Import lists code from our new, standardized modules
rylnd Jul 14, 2020
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
1 change: 1 addition & 0 deletions x-pack/plugins/lists/common/shared_exports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,5 @@ export {
entriesList,
namespaceType,
ExceptionListType,
Type,
} from './schemas';
118 changes: 118 additions & 0 deletions x-pack/plugins/lists/public/common/hooks/use_cursor.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/*
* 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 { act, renderHook } from '@testing-library/react-hooks';

import { UseCursorProps, useCursor } from './use_cursor';

describe('useCursor', () => {
it('returns undefined cursor if no values have been set', () => {
const { result } = renderHook((props: UseCursorProps) => useCursor(props), {
initialProps: { pageIndex: 0, pageSize: 0 },
});

expect(result.current[0]).toBeUndefined();
});

it('retrieves a cursor for the next page of a given page size', () => {
const { rerender, result } = renderHook((props: UseCursorProps) => useCursor(props), {
initialProps: { pageIndex: 0, pageSize: 0 },
});
rerender({ pageIndex: 1, pageSize: 1 });
act(() => {
result.current[1]('new_cursor');
});

expect(result.current[0]).toBeUndefined();

rerender({ pageIndex: 2, pageSize: 1 });
expect(result.current[0]).toEqual('new_cursor');
});

it('returns undefined cursor for an unknown search', () => {
const { rerender, result } = renderHook((props: UseCursorProps) => useCursor(props), {
initialProps: { pageIndex: 0, pageSize: 0 },
});
act(() => {
result.current[1]('new_cursor');
});

rerender({ pageIndex: 1, pageSize: 2 });
expect(result.current[0]).toBeUndefined();
});

it('remembers cursor through rerenders', () => {
const { rerender, result } = renderHook((props: UseCursorProps) => useCursor(props), {
initialProps: { pageIndex: 0, pageSize: 0 },
});

rerender({ pageIndex: 1, pageSize: 1 });
act(() => {
result.current[1]('new_cursor');
});

rerender({ pageIndex: 2, pageSize: 1 });
expect(result.current[0]).toEqual('new_cursor');

rerender({ pageIndex: 0, pageSize: 0 });
expect(result.current[0]).toBeUndefined();

rerender({ pageIndex: 2, pageSize: 1 });
expect(result.current[0]).toEqual('new_cursor');
});

it('remembers multiple cursors', () => {
const { rerender, result } = renderHook((props: UseCursorProps) => useCursor(props), {
initialProps: { pageIndex: 0, pageSize: 0 },
});

rerender({ pageIndex: 1, pageSize: 1 });
act(() => {
result.current[1]('new_cursor');
});
rerender({ pageIndex: 2, pageSize: 2 });
act(() => {
result.current[1]('another_cursor');
});

rerender({ pageIndex: 2, pageSize: 1 });
expect(result.current[0]).toEqual('new_cursor');

rerender({ pageIndex: 3, pageSize: 2 });
expect(result.current[0]).toEqual('another_cursor');
});

it('returns the "nearest" cursor for the given page size', () => {
const { rerender, result } = renderHook((props: UseCursorProps) => useCursor(props), {
initialProps: { pageIndex: 0, pageSize: 0 },
});

rerender({ pageIndex: 1, pageSize: 2 });
act(() => {
result.current[1]('cursor1');
});
rerender({ pageIndex: 2, pageSize: 2 });
act(() => {
result.current[1]('cursor2');
});
rerender({ pageIndex: 3, pageSize: 2 });
act(() => {
result.current[1]('cursor3');
});

rerender({ pageIndex: 2, pageSize: 2 });
expect(result.current[0]).toEqual('cursor1');

rerender({ pageIndex: 3, pageSize: 2 });
expect(result.current[0]).toEqual('cursor2');

rerender({ pageIndex: 4, pageSize: 2 });
expect(result.current[0]).toEqual('cursor3');

rerender({ pageIndex: 6, pageSize: 2 });
expect(result.current[0]).toEqual('cursor3');
});
});
43 changes: 43 additions & 0 deletions x-pack/plugins/lists/public/common/hooks/use_cursor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* 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, useState } from 'react';

export interface UseCursorProps {
pageIndex: number;
pageSize: number;
}
type Cursor = string | undefined;
type SetCursor = (cursor: Cursor) => void;
type UseCursor = (props: UseCursorProps) => [Cursor, SetCursor];

const hash = (props: UseCursorProps): string => JSON.stringify(props);

export const useCursor: UseCursor = ({ pageIndex, pageSize }) => {
const [cache, setCache] = useState<Record<string, Cursor>>({});

const setCursor = useCallback<SetCursor>(
(cursor) => {
setCache({
...cache,
[hash({ pageIndex: pageIndex + 1, pageSize })]: cursor,
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[pageIndex, pageSize]
);

let cursor: Cursor;
for (let i = pageIndex; i >= 0; i--) {
const currentProps = { pageIndex: i, pageSize };
cursor = cache[hash(currentProps)];
if (cursor) {
break;
}
}

return [cursor, setCursor];
};
100 changes: 47 additions & 53 deletions x-pack/plugins/lists/public/lists/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ describe('Value Lists API', () => {
it('sends pagination as query parameters', async () => {
const abortCtrl = new AbortController();
await findLists({
cursor: 'cursor',
http: httpMock,
pageIndex: 1,
pageSize: 10,
Expand All @@ -123,14 +124,21 @@ describe('Value Lists API', () => {
expect(httpMock.fetch).toHaveBeenCalledWith(
'/api/lists/_find',
expect.objectContaining({
query: { page: 1, per_page: 10 },
query: {
cursor: 'cursor',
page: 1,
per_page: 10,
},
})
);
});

it('rejects with an error if request payload is invalid (and does not make API call)', async () => {
const abortCtrl = new AbortController();
const payload: ApiPayload<FindListsParams> = { pageIndex: 10, pageSize: 0 };
const payload: ApiPayload<FindListsParams> = {
pageIndex: 10,
pageSize: 0,
};

await expect(
findLists({
Expand All @@ -144,7 +152,10 @@ describe('Value Lists API', () => {

it('rejects with an error if response payload is invalid', async () => {
const abortCtrl = new AbortController();
const payload: ApiPayload<FindListsParams> = { pageIndex: 1, pageSize: 10 };
const payload: ApiPayload<FindListsParams> = {
pageIndex: 1,
pageSize: 10,
};
const badResponse = { ...getFoundListSchemaMock(), cursor: undefined };
httpMock.fetch.mockResolvedValue(badResponse);

Expand Down Expand Up @@ -269,7 +280,7 @@ describe('Value Lists API', () => {

describe('exportList', () => {
beforeEach(() => {
httpMock.fetch.mockResolvedValue(getListResponseMock());
httpMock.fetch.mockResolvedValue({});
});

it('POSTs to the export endpoint', async () => {
Expand Down Expand Up @@ -319,66 +330,49 @@ describe('Value Lists API', () => {
).rejects.toEqual(new Error('Invalid value "23" supplied to "list_id"'));
expect(httpMock.fetch).not.toHaveBeenCalled();
});
});

describe('readListIndex', () => {
beforeEach(() => {
httpMock.fetch.mockResolvedValue(getListItemIndexExistSchemaResponseMock());
});

it('rejects with an error if response payload is invalid', async () => {
it('GETs the list index', async () => {
const abortCtrl = new AbortController();
const payload: ApiPayload<ExportListParams> = {
listId: 'list-id',
};
const badResponse = { ...getListResponseMock(), id: undefined };
httpMock.fetch.mockResolvedValue(badResponse);
await readListIndex({
http: httpMock,
signal: abortCtrl.signal,
});

await expect(
exportList({
http: httpMock,
...payload,
signal: abortCtrl.signal,
expect(httpMock.fetch).toHaveBeenCalledWith(
'/api/lists/index',
expect.objectContaining({
method: 'GET',
})
).rejects.toEqual(new Error('Invalid value "undefined" supplied to "id"'));
);
});

describe('readListIndex', () => {
beforeEach(() => {
httpMock.fetch.mockResolvedValue(getListItemIndexExistSchemaResponseMock());
it('returns the response when valid', async () => {
const abortCtrl = new AbortController();
const result = await readListIndex({
http: httpMock,
signal: abortCtrl.signal,
});

it('GETs the list index', async () => {
const abortCtrl = new AbortController();
await readListIndex({
http: httpMock,
signal: abortCtrl.signal,
});

expect(httpMock.fetch).toHaveBeenCalledWith(
'/api/lists/index',
expect.objectContaining({
method: 'GET',
})
);
});
expect(result).toEqual(getListItemIndexExistSchemaResponseMock());
});

it('rejects with an error if response payload is invalid', async () => {
const abortCtrl = new AbortController();
const badResponse = { ...getListItemIndexExistSchemaResponseMock(), list_index: undefined };
httpMock.fetch.mockResolvedValue(badResponse);

it('returns the response when valid', async () => {
const abortCtrl = new AbortController();
const result = await readListIndex({
await expect(
readListIndex({
http: httpMock,
signal: abortCtrl.signal,
});

expect(result).toEqual(getListItemIndexExistSchemaResponseMock());
});

it('rejects with an error if response payload is invalid', async () => {
const abortCtrl = new AbortController();
const badResponse = { ...getListItemIndexExistSchemaResponseMock(), list_index: undefined };
httpMock.fetch.mockResolvedValue(badResponse);

await expect(
readListIndex({
http: httpMock,
signal: abortCtrl.signal,
})
).rejects.toEqual(new Error('Invalid value "undefined" supplied to "list_index"'));
});
})
).rejects.toEqual(new Error('Invalid value "undefined" supplied to "list_index"'));
});
});

Expand Down
7 changes: 4 additions & 3 deletions x-pack/plugins/lists/public/lists/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,15 +59,17 @@ const findLists = async ({
};

const findListsWithValidation = async ({
cursor,
http,
pageIndex,
pageSize,
signal,
}: FindListsParams): Promise<FoundListSchema> =>
pipe(
{
page: String(pageIndex),
per_page: String(pageSize),
cursor: cursor?.toString(),
page: pageIndex?.toString(),
per_page: pageSize?.toString(),
},
(payload) => fromEither(validateEither(findListSchema, payload)),
chain((payload) => tryCatch(() => findLists({ http, signal, ...payload }), toError)),
Expand Down Expand Up @@ -170,7 +172,6 @@ const exportListWithValidation = async ({
{ list_id: listId },
(payload) => fromEither(validateEither(exportListItemQuerySchema, payload)),
chain((payload) => tryCatch(() => exportList({ http, signal, ...payload }), toError)),
chain((response) => fromEither(validateEither(listSchema, response))),
flow(toPromise)
);

Expand Down
1 change: 1 addition & 0 deletions x-pack/plugins/lists/public/lists/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export interface ApiParams {
export type ApiPayload<T extends ApiParams> = Omit<T, 'http' | 'signal'>;

export interface FindListsParams extends ApiParams {
cursor?: string | undefined;
pageSize: number | undefined;
pageIndex: number | undefined;
}
Expand Down
2 changes: 2 additions & 0 deletions x-pack/plugins/lists/public/shared_exports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export { useExceptionList } from './exceptions/hooks/use_exception_list';
export { useFindLists } from './lists/hooks/use_find_lists';
export { useImportList } from './lists/hooks/use_import_list';
export { useDeleteList } from './lists/hooks/use_delete_list';
export { exportList } from './lists/api';
export { useCursor } from './common/hooks/use_cursor';
export { useExportList } from './lists/hooks/use_export_list';
export { useReadListIndex } from './lists/hooks/use_read_list_index';
export { useCreateListIndex } from './lists/hooks/use_create_list_index';
Expand Down
1 change: 1 addition & 0 deletions x-pack/plugins/security_solution/common/shared_imports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,5 @@ export {
entriesList,
namespaceType,
ExceptionListType,
Type,
} from '../../lists/common';
Loading