mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Cases] Increase default page size cases table and save preferences in url/localStorage (#144228)
## Summary Issues: https://github.com/elastic/kibana/issues/131806 https://github.com/elastic/kibana/issues/140008 * Increase the default table size of the cases table to 10 * Changed the available page sizes to 10, 25, 50 and 100 * Save the visualization preferences of the cases table in localStorage * Display the current visualization preferences of the cases table in the URL * This logic is not applied if the cases table is opened in a modal ### Screenshots <img width="1441" alt="Screenshot 2022-10-31 at 12 19 10" src="https://user-images.githubusercontent.com/1533137/198996468-f33ef67b-4f18-467e-841c-dfcff1574c06.png"> ### Checklist Delete any items that are not applicable to this PR. - [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) --- Fixes #140008 ## Release notes * Increase the default table size of the cases table to 10 * Save the visualization preferences of the cases table in localStorage * Display the current visualization preferences of the cases table in the URL Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
10fcf61d56
commit
46a71979c0
15 changed files with 489 additions and 113 deletions
|
@ -185,3 +185,8 @@ export const NO_ASSIGNEES_FILTERING_KEYWORD = 'none';
|
|||
* Delays
|
||||
*/
|
||||
export const SEARCH_DEBOUNCE_MS = 500;
|
||||
|
||||
/**
|
||||
* Local storage keys
|
||||
*/
|
||||
export const LOCAL_STORAGE_KEYS = { casesFiltering: 'cases.list.filtering' };
|
||||
|
|
|
@ -97,7 +97,15 @@ export interface QueryParams {
|
|||
sortField: SortFieldCase;
|
||||
sortOrder: 'asc' | 'desc';
|
||||
}
|
||||
export type UrlQueryParams = Partial<QueryParams>;
|
||||
|
||||
export type ParsedUrlQueryParams = Partial<Omit<QueryParams, 'page' | 'perPage'>> & {
|
||||
page?: string;
|
||||
perPage?: string;
|
||||
[index: string]: string | string[] | undefined | null;
|
||||
};
|
||||
|
||||
export type LocalStorageQueryParams = Partial<Omit<QueryParams, 'page'>>;
|
||||
export interface FilterOptions {
|
||||
search: string;
|
||||
searchFields: string[];
|
||||
|
|
|
@ -19,10 +19,13 @@ const useApplicationMock = useApplication as jest.Mock;
|
|||
describe('hooks', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
useApplicationMock.mockReturnValue({ appId: 'management', appTitle: 'Management' });
|
||||
});
|
||||
|
||||
describe('useIsMainApplication', () => {
|
||||
beforeEach(() => {
|
||||
useApplicationMock.mockReturnValue({ appId: 'management', appTitle: 'Management' });
|
||||
});
|
||||
|
||||
it('returns true if it is the main application', () => {
|
||||
const { result } = renderHook(() => useIsMainApplication(), {
|
||||
wrapper: ({ children }) => <TestProviders>{children}</TestProviders>,
|
||||
|
@ -33,6 +36,7 @@ describe('hooks', () => {
|
|||
|
||||
it('returns false if it is not the main application', () => {
|
||||
useApplicationMock.mockReturnValue({ appId: 'testAppId', appTitle: 'Test app' });
|
||||
|
||||
const { result } = renderHook(() => useIsMainApplication(), {
|
||||
wrapper: ({ children }) => <TestProviders>{children}</TestProviders>,
|
||||
});
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
/* eslint-disable no-console */
|
||||
|
||||
import React from 'react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { euiDarkVars } from '@kbn/ui-theme';
|
||||
import { I18nProvider } from '@kbn/i18n-react';
|
||||
import { ThemeProvider } from 'styled-components';
|
||||
|
@ -73,17 +74,19 @@ const TestProvidersComponent: React.FC<TestProviderProps> = ({
|
|||
<KibanaContextProvider services={services}>
|
||||
<ThemeProvider theme={() => ({ eui: euiDarkVars, darkMode: true })}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<CasesProvider
|
||||
value={{
|
||||
externalReferenceAttachmentTypeRegistry,
|
||||
persistableStateAttachmentTypeRegistry,
|
||||
features,
|
||||
owner,
|
||||
permissions,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</CasesProvider>
|
||||
<MemoryRouter>
|
||||
<CasesProvider
|
||||
value={{
|
||||
externalReferenceAttachmentTypeRegistry,
|
||||
persistableStateAttachmentTypeRegistry,
|
||||
features,
|
||||
owner,
|
||||
permissions,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</CasesProvider>
|
||||
</MemoryRouter>
|
||||
</QueryClientProvider>
|
||||
</ThemeProvider>
|
||||
</KibanaContextProvider>
|
||||
|
@ -149,18 +152,20 @@ export const createAppMockRenderer = ({
|
|||
<KibanaContextProvider services={services}>
|
||||
<ThemeProvider theme={() => ({ eui: euiDarkVars, darkMode: true })}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<CasesProvider
|
||||
value={{
|
||||
externalReferenceAttachmentTypeRegistry,
|
||||
persistableStateAttachmentTypeRegistry,
|
||||
features,
|
||||
owner,
|
||||
permissions,
|
||||
releasePhase,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</CasesProvider>
|
||||
<MemoryRouter>
|
||||
<CasesProvider
|
||||
value={{
|
||||
externalReferenceAttachmentTypeRegistry,
|
||||
persistableStateAttachmentTypeRegistry,
|
||||
features,
|
||||
owner,
|
||||
permissions,
|
||||
releasePhase,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</CasesProvider>
|
||||
</MemoryRouter>
|
||||
</QueryClientProvider>
|
||||
</ThemeProvider>
|
||||
</KibanaContextProvider>
|
||||
|
|
|
@ -23,7 +23,7 @@ import {
|
|||
} from '../../common/mock';
|
||||
import { useGetCasesMockState, connectorsMock } from '../../containers/mock';
|
||||
|
||||
import { StatusAll } from '../../../common/ui/types';
|
||||
import { SortFieldCase, StatusAll } from '../../../common/ui/types';
|
||||
import { CaseSeverity, CaseStatuses } from '../../../common/api';
|
||||
import { SECURITY_SOLUTION_OWNER } from '../../../common/constants';
|
||||
import { getEmptyTagValue } from '../empty_value';
|
||||
|
@ -39,7 +39,7 @@ import { useCreateAttachments } from '../../containers/use_create_attachments';
|
|||
import { useGetConnectors } from '../../containers/configure/use_connectors';
|
||||
import { useGetTags } from '../../containers/use_get_tags';
|
||||
import { useUpdateCase } from '../../containers/use_update_case';
|
||||
import { useGetCases } from '../../containers/use_get_cases';
|
||||
import { useGetCases, DEFAULT_QUERY_PARAMS } from '../../containers/use_get_cases';
|
||||
import { useGetCurrentUserProfile } from '../../containers/user_profiles/use_get_current_user_profile';
|
||||
import { userProfiles, userProfilesMap } from '../../containers/user_profiles/api.mock';
|
||||
import { useBulkGetUserProfiles } from '../../containers/user_profiles/use_bulk_get_user_profiles';
|
||||
|
@ -127,6 +127,7 @@ describe('AllCasesListGeneric', () => {
|
|||
useLicenseMock.mockReturnValue({ isAtLeastPlatinum: () => false });
|
||||
mockKibana();
|
||||
moment.tz.setDefault('UTC');
|
||||
window.localStorage.clear();
|
||||
});
|
||||
|
||||
it('should render AllCasesList', async () => {
|
||||
|
@ -255,9 +256,7 @@ describe('AllCasesListGeneric', () => {
|
|||
expect(useGetCasesMock).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
queryParams: {
|
||||
page: 1,
|
||||
perPage: 5,
|
||||
sortField: 'createdAt',
|
||||
...DEFAULT_QUERY_PARAMS,
|
||||
sortOrder: 'asc',
|
||||
},
|
||||
})
|
||||
|
@ -402,12 +401,7 @@ describe('AllCasesListGeneric', () => {
|
|||
await waitFor(() => {
|
||||
expect(useGetCasesMock).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
queryParams: {
|
||||
page: 1,
|
||||
perPage: 5,
|
||||
sortField: 'closedAt',
|
||||
sortOrder: 'desc',
|
||||
},
|
||||
queryParams: { ...DEFAULT_QUERY_PARAMS, sortField: SortFieldCase.closedAt },
|
||||
})
|
||||
);
|
||||
});
|
||||
|
@ -421,12 +415,7 @@ describe('AllCasesListGeneric', () => {
|
|||
await waitFor(() => {
|
||||
expect(useGetCasesMock).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
queryParams: {
|
||||
page: 1,
|
||||
perPage: 5,
|
||||
sortField: 'createdAt',
|
||||
sortOrder: 'desc',
|
||||
},
|
||||
queryParams: DEFAULT_QUERY_PARAMS,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
@ -440,12 +429,7 @@ describe('AllCasesListGeneric', () => {
|
|||
await waitFor(() => {
|
||||
expect(useGetCasesMock).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
queryParams: {
|
||||
page: 1,
|
||||
perPage: 5,
|
||||
sortField: 'createdAt',
|
||||
sortOrder: 'desc',
|
||||
},
|
||||
queryParams: DEFAULT_QUERY_PARAMS,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
@ -618,7 +602,7 @@ describe('AllCasesListGeneric', () => {
|
|||
assignees: [],
|
||||
owner: ['securitySolution', 'observability'],
|
||||
},
|
||||
queryParams: { page: 1, perPage: 5, sortField: 'createdAt', sortOrder: 'desc' },
|
||||
queryParams: DEFAULT_QUERY_PARAMS,
|
||||
});
|
||||
|
||||
userEvent.click(getByTestId('options-filter-popover-button-Solution'));
|
||||
|
@ -644,7 +628,7 @@ describe('AllCasesListGeneric', () => {
|
|||
assignees: [],
|
||||
owner: ['securitySolution'],
|
||||
},
|
||||
queryParams: { page: 1, perPage: 5, sortField: 'createdAt', sortOrder: 'desc' },
|
||||
queryParams: DEFAULT_QUERY_PARAMS,
|
||||
});
|
||||
|
||||
userEvent.click(
|
||||
|
@ -666,7 +650,7 @@ describe('AllCasesListGeneric', () => {
|
|||
assignees: [],
|
||||
owner: ['securitySolution', 'observability'],
|
||||
},
|
||||
queryParams: { page: 1, perPage: 5, sortField: 'createdAt', sortOrder: 'desc' },
|
||||
queryParams: DEFAULT_QUERY_PARAMS,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -698,7 +682,7 @@ describe('AllCasesListGeneric', () => {
|
|||
assignees: [],
|
||||
owner: ['securitySolution'],
|
||||
},
|
||||
queryParams: { page: 1, perPage: 5, sortField: 'createdAt', sortOrder: 'desc' },
|
||||
queryParams: DEFAULT_QUERY_PARAMS,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -11,12 +11,7 @@ import { EuiProgress } from '@elastic/eui';
|
|||
import { difference, head, isEmpty } from 'lodash/fp';
|
||||
import styled, { css } from 'styled-components';
|
||||
|
||||
import type {
|
||||
Case,
|
||||
CaseStatusWithAllStatus,
|
||||
FilterOptions,
|
||||
QueryParams,
|
||||
} from '../../../common/ui/types';
|
||||
import type { Case, CaseStatusWithAllStatus, FilterOptions } from '../../../common/ui/types';
|
||||
import { SortFieldCase, StatusAll } from '../../../common/ui/types';
|
||||
import { CaseStatuses, caseStatuses } from '../../../common/api';
|
||||
|
||||
|
@ -29,16 +24,12 @@ import { CasesTable } from './table';
|
|||
import { useCasesContext } from '../cases_context/use_cases_context';
|
||||
import { CasesMetrics } from './cases_metrics';
|
||||
import { useGetConnectors } from '../../containers/configure/use_connectors';
|
||||
import {
|
||||
DEFAULT_FILTER_OPTIONS,
|
||||
DEFAULT_QUERY_PARAMS,
|
||||
initialData,
|
||||
useGetCases,
|
||||
} from '../../containers/use_get_cases';
|
||||
import { DEFAULT_FILTER_OPTIONS, initialData, useGetCases } from '../../containers/use_get_cases';
|
||||
import { useBulkGetUserProfiles } from '../../containers/user_profiles/use_bulk_get_user_profiles';
|
||||
import { useGetCurrentUserProfile } from '../../containers/user_profiles/use_get_current_user_profile';
|
||||
import { getAllPermissionsExceptFrom, isReadOnlyPermissions } from '../../utils/permissions';
|
||||
import { useIsLoadingCases } from './use_is_loading_cases';
|
||||
import { useAllCasesQueryParams } from './use_all_cases_query_params';
|
||||
|
||||
const ProgressLoader = styled(EuiProgress)`
|
||||
${({ $isShow }: { $isShow: boolean }) =>
|
||||
|
@ -69,7 +60,6 @@ export const AllCasesList = React.memo<AllCasesListProps>(
|
|||
const isLoading = useIsLoadingCases();
|
||||
|
||||
const hasOwner = !!owner.length;
|
||||
|
||||
const firstAvailableStatus = head(difference(caseStatuses, hiddenStatuses));
|
||||
const initialFilterOptions = {
|
||||
...(!isEmpty(hiddenStatuses) && firstAvailableStatus && { status: firstAvailableStatus }),
|
||||
|
@ -80,7 +70,7 @@ export const AllCasesList = React.memo<AllCasesListProps>(
|
|||
...DEFAULT_FILTER_OPTIONS,
|
||||
...initialFilterOptions,
|
||||
});
|
||||
const [queryParams, setQueryParams] = useState<QueryParams>(DEFAULT_QUERY_PARAMS);
|
||||
const { queryParams, setQueryParams } = useAllCasesQueryParams(isSelectorView);
|
||||
const [selectedCases, setSelectedCases] = useState<Case[]>([]);
|
||||
|
||||
const { data = initialData, isFetching: isLoadingCases } = useGetCases({
|
||||
|
@ -112,7 +102,10 @@ export const AllCasesList = React.memo<AllCasesListProps>(
|
|||
|
||||
const sorting = useMemo(
|
||||
() => ({
|
||||
sort: { field: queryParams.sortField, direction: queryParams.sortOrder },
|
||||
sort: {
|
||||
field: queryParams.sortField,
|
||||
direction: queryParams.sortOrder,
|
||||
},
|
||||
}),
|
||||
[queryParams.sortField, queryParams.sortOrder]
|
||||
);
|
||||
|
@ -150,23 +143,14 @@ export const AllCasesList = React.memo<AllCasesListProps>(
|
|||
const onFilterChangedCallback = useCallback(
|
||||
(newFilterOptions: Partial<FilterOptions>) => {
|
||||
if (newFilterOptions.status && newFilterOptions.status === CaseStatuses.closed) {
|
||||
setQueryParams((prevQueryParams) => ({
|
||||
...prevQueryParams,
|
||||
sortField: SortFieldCase.closedAt,
|
||||
}));
|
||||
} else if (newFilterOptions.status && newFilterOptions.status === CaseStatuses.open) {
|
||||
setQueryParams((prevQueryParams) => ({
|
||||
...prevQueryParams,
|
||||
sortField: SortFieldCase.createdAt,
|
||||
}));
|
||||
setQueryParams({ sortField: SortFieldCase.closedAt });
|
||||
} else if (
|
||||
newFilterOptions.status &&
|
||||
newFilterOptions.status === CaseStatuses['in-progress']
|
||||
[CaseStatuses.open, CaseStatuses['in-progress'], StatusAll].includes(
|
||||
newFilterOptions.status
|
||||
)
|
||||
) {
|
||||
setQueryParams((prevQueryParams) => ({
|
||||
...prevQueryParams,
|
||||
sortField: SortFieldCase.createdAt,
|
||||
}));
|
||||
setQueryParams({ sortField: SortFieldCase.createdAt });
|
||||
}
|
||||
|
||||
deselectCases();
|
||||
|
@ -193,7 +177,7 @@ export const AllCasesList = React.memo<AllCasesListProps>(
|
|||
: {}),
|
||||
}));
|
||||
},
|
||||
[deselectCases, hasOwner, availableSolutions, owner]
|
||||
[deselectCases, hasOwner, availableSolutions, owner, setQueryParams]
|
||||
);
|
||||
|
||||
const { columns } = useCasesColumns({
|
||||
|
@ -208,10 +192,10 @@ export const AllCasesList = React.memo<AllCasesListProps>(
|
|||
|
||||
const pagination = useMemo(
|
||||
() => ({
|
||||
pageIndex: (queryParams?.page ?? DEFAULT_QUERY_PARAMS.page) - 1,
|
||||
pageSize: queryParams?.perPage ?? DEFAULT_QUERY_PARAMS.perPage,
|
||||
pageIndex: queryParams.page - 1,
|
||||
pageSize: queryParams.perPage,
|
||||
totalItemCount: data.total ?? 0,
|
||||
pageSizeOptions: [5, 10, 15, 20, 25],
|
||||
pageSizeOptions: [10, 25, 50, 100],
|
||||
}),
|
||||
[data, queryParams]
|
||||
);
|
||||
|
|
|
@ -0,0 +1,155 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
|
||||
import { TestProviders } from '../../common/mock';
|
||||
import {
|
||||
useAllCasesQueryParams,
|
||||
getQueryParamsLocalStorageKey,
|
||||
} from './use_all_cases_query_params';
|
||||
import { DEFAULT_QUERY_PARAMS } from '../../containers/use_get_cases';
|
||||
import { stringify } from 'query-string';
|
||||
import { DEFAULT_TABLE_ACTIVE_PAGE, DEFAULT_TABLE_LIMIT } from '../../containers/constants';
|
||||
|
||||
const LOCAL_STORAGE_DEFAULTS = {
|
||||
perPage: DEFAULT_QUERY_PARAMS.perPage,
|
||||
sortOrder: DEFAULT_QUERY_PARAMS.sortOrder,
|
||||
};
|
||||
const URL_DEFAULTS = {
|
||||
page: DEFAULT_QUERY_PARAMS.page,
|
||||
perPage: DEFAULT_QUERY_PARAMS.perPage,
|
||||
sortOrder: DEFAULT_QUERY_PARAMS.sortOrder,
|
||||
};
|
||||
|
||||
const mockLocation = { search: '' };
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useLocation: jest.fn().mockImplementation(() => {
|
||||
return mockLocation;
|
||||
}),
|
||||
useHistory: jest.fn().mockReturnValue({
|
||||
push: jest.fn(),
|
||||
location: {
|
||||
search: '',
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
const APP_ID = 'testAppId';
|
||||
const LOCALSTORAGE_KEY = getQueryParamsLocalStorageKey(APP_ID);
|
||||
|
||||
describe('useAllCasesQueryParams', () => {
|
||||
beforeEach(() => {
|
||||
global.localStorage.clear();
|
||||
});
|
||||
|
||||
it('calls setState with default values on first run', () => {
|
||||
const { result } = renderHook(() => useAllCasesQueryParams(), {
|
||||
wrapper: ({ children }) => <TestProviders>{children}</TestProviders>,
|
||||
});
|
||||
|
||||
expect(result.current.queryParams).toStrictEqual(DEFAULT_QUERY_PARAMS);
|
||||
});
|
||||
|
||||
it('updates localstorage with default values on first run', () => {
|
||||
expect(global.localStorage.getItem(LOCALSTORAGE_KEY)).toStrictEqual(null);
|
||||
|
||||
renderHook(() => useAllCasesQueryParams(), {
|
||||
wrapper: ({ children }) => <TestProviders>{children}</TestProviders>,
|
||||
});
|
||||
|
||||
expect(JSON.parse(global.localStorage.getItem(LOCALSTORAGE_KEY) ?? '{}')).toMatchObject({
|
||||
...LOCAL_STORAGE_DEFAULTS,
|
||||
});
|
||||
});
|
||||
|
||||
it('calls history.push with default values on first run', () => {
|
||||
renderHook(() => useAllCasesQueryParams(), {
|
||||
wrapper: ({ children }) => <TestProviders>{children}</TestProviders>,
|
||||
});
|
||||
|
||||
expect(useHistory().push).toHaveBeenCalledWith({
|
||||
search: stringify(URL_DEFAULTS),
|
||||
});
|
||||
});
|
||||
|
||||
it('takes into account existing localStorage values on first run', () => {
|
||||
const existingLocalStorageValues = { perPage: DEFAULT_TABLE_LIMIT + 10, sortOrder: 'asc' };
|
||||
|
||||
global.localStorage.setItem(LOCALSTORAGE_KEY, JSON.stringify(existingLocalStorageValues));
|
||||
|
||||
const { result } = renderHook(() => useAllCasesQueryParams(), {
|
||||
wrapper: ({ children }) => <TestProviders>{children}</TestProviders>,
|
||||
});
|
||||
|
||||
expect(result.current.queryParams).toMatchObject({
|
||||
...LOCAL_STORAGE_DEFAULTS,
|
||||
...existingLocalStorageValues,
|
||||
});
|
||||
});
|
||||
|
||||
it('takes into account existing urlParams on first run', () => {
|
||||
const nonDefaultUrlParams = {
|
||||
page: DEFAULT_TABLE_ACTIVE_PAGE + 1,
|
||||
perPage: DEFAULT_TABLE_LIMIT + 5,
|
||||
};
|
||||
const expectedUrl = { ...URL_DEFAULTS, ...nonDefaultUrlParams };
|
||||
|
||||
mockLocation.search = stringify(nonDefaultUrlParams);
|
||||
|
||||
renderHook(() => useAllCasesQueryParams(), {
|
||||
wrapper: ({ children }) => <TestProviders>{children}</TestProviders>,
|
||||
});
|
||||
|
||||
expect(useHistory().push).toHaveBeenCalledWith({
|
||||
search: stringify(expectedUrl),
|
||||
});
|
||||
});
|
||||
|
||||
it('preserves other url parameters', () => {
|
||||
const nonDefaultUrlParams = {
|
||||
foo: 'bar',
|
||||
};
|
||||
const expectedUrl = { ...URL_DEFAULTS, ...nonDefaultUrlParams };
|
||||
|
||||
mockLocation.search = stringify(nonDefaultUrlParams);
|
||||
|
||||
renderHook(() => useAllCasesQueryParams(), {
|
||||
wrapper: ({ children }) => <TestProviders>{children}</TestProviders>,
|
||||
});
|
||||
|
||||
expect(useHistory().push).toHaveBeenCalledWith({
|
||||
search: stringify(expectedUrl),
|
||||
});
|
||||
});
|
||||
|
||||
it('urlParams take precedence over localStorage values', () => {
|
||||
const nonDefaultUrlParams = {
|
||||
perPage: DEFAULT_TABLE_LIMIT + 5,
|
||||
};
|
||||
|
||||
mockLocation.search = stringify(nonDefaultUrlParams);
|
||||
|
||||
global.localStorage.setItem(
|
||||
LOCALSTORAGE_KEY,
|
||||
JSON.stringify({ perPage: DEFAULT_TABLE_LIMIT + 10 }) // existingLocalStorageValues
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useAllCasesQueryParams(), {
|
||||
wrapper: ({ children }) => <TestProviders>{children}</TestProviders>,
|
||||
});
|
||||
|
||||
expect(result.current.queryParams).toMatchObject({
|
||||
...DEFAULT_QUERY_PARAMS,
|
||||
...nonDefaultUrlParams,
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,127 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
import { useLocation, useHistory } from 'react-router-dom';
|
||||
import { isEqual } from 'lodash';
|
||||
|
||||
import useLocalStorage from 'react-use/lib/useLocalStorage';
|
||||
import { parse, stringify } from 'query-string';
|
||||
|
||||
import { DEFAULT_QUERY_PARAMS } from '../../containers/use_get_cases';
|
||||
import { parseUrlQueryParams } from './utils';
|
||||
import { LOCAL_STORAGE_KEYS } from '../../../common/constants';
|
||||
|
||||
import type {
|
||||
LocalStorageQueryParams,
|
||||
ParsedUrlQueryParams,
|
||||
QueryParams,
|
||||
UrlQueryParams,
|
||||
} from '../../../common/ui/types';
|
||||
import { useCasesContext } from '../cases_context/use_cases_context';
|
||||
|
||||
export const getQueryParamsLocalStorageKey = (appId: string) => {
|
||||
const filteringKey = LOCAL_STORAGE_KEYS.casesFiltering;
|
||||
return `${appId}.${filteringKey}`;
|
||||
};
|
||||
|
||||
const getQueryParams = (
|
||||
params: UrlQueryParams,
|
||||
queryParams: UrlQueryParams,
|
||||
urlParams: UrlQueryParams,
|
||||
localStorageQueryParams?: LocalStorageQueryParams
|
||||
): QueryParams => {
|
||||
const result = { ...DEFAULT_QUERY_PARAMS };
|
||||
|
||||
result.perPage =
|
||||
params.perPage ??
|
||||
urlParams.perPage ??
|
||||
localStorageQueryParams?.perPage ??
|
||||
DEFAULT_QUERY_PARAMS.perPage;
|
||||
|
||||
result.sortField = params.sortField ?? queryParams.sortField ?? DEFAULT_QUERY_PARAMS.sortField;
|
||||
|
||||
result.sortOrder =
|
||||
params.sortOrder ??
|
||||
urlParams.sortOrder ??
|
||||
localStorageQueryParams?.sortOrder ??
|
||||
DEFAULT_QUERY_PARAMS.sortOrder;
|
||||
|
||||
result.page = params.page ?? urlParams.page ?? DEFAULT_QUERY_PARAMS.page;
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export function useAllCasesQueryParams(isModalView: boolean = false) {
|
||||
const { appId } = useCasesContext();
|
||||
const location = useLocation();
|
||||
const history = useHistory();
|
||||
const isFirstRenderRef = useRef(true);
|
||||
|
||||
const [queryParams, setQueryParams] = useState<QueryParams>({ ...DEFAULT_QUERY_PARAMS });
|
||||
|
||||
const [localStorageQueryParams, setLocalStorageQueryParams] =
|
||||
useLocalStorage<LocalStorageQueryParams>(getQueryParamsLocalStorageKey(appId));
|
||||
|
||||
const persistAndUpdateQueryParams = useCallback(
|
||||
(params) => {
|
||||
if (isModalView) {
|
||||
setQueryParams((prevParams) => ({ ...prevParams, ...params }));
|
||||
return;
|
||||
}
|
||||
|
||||
const parsedUrlParams: ParsedUrlQueryParams = parse(location.search);
|
||||
const urlParams: UrlQueryParams = parseUrlQueryParams(parsedUrlParams);
|
||||
const newQueryParams: QueryParams = getQueryParams(
|
||||
params,
|
||||
queryParams,
|
||||
urlParams,
|
||||
localStorageQueryParams
|
||||
);
|
||||
const newLocalStorageQueryParams = {
|
||||
perPage: newQueryParams.perPage,
|
||||
sortOrder: newQueryParams.sortOrder,
|
||||
};
|
||||
const newUrlParams = {
|
||||
page: newQueryParams.page,
|
||||
...newLocalStorageQueryParams,
|
||||
};
|
||||
|
||||
if (!isEqual(newUrlParams, urlParams)) {
|
||||
try {
|
||||
history.push({
|
||||
...location,
|
||||
search: stringify({ ...parsedUrlParams, ...newUrlParams }),
|
||||
});
|
||||
} catch {
|
||||
// silently fail
|
||||
}
|
||||
}
|
||||
|
||||
setLocalStorageQueryParams(newLocalStorageQueryParams);
|
||||
setQueryParams(newQueryParams);
|
||||
},
|
||||
[
|
||||
isModalView,
|
||||
location,
|
||||
localStorageQueryParams,
|
||||
queryParams,
|
||||
setLocalStorageQueryParams,
|
||||
history,
|
||||
]
|
||||
);
|
||||
|
||||
if (isFirstRenderRef.current) {
|
||||
persistAndUpdateQueryParams(isModalView ? DEFAULT_QUERY_PARAMS : {});
|
||||
isFirstRenderRef.current = false;
|
||||
}
|
||||
|
||||
return {
|
||||
queryParams,
|
||||
setQueryParams: persistAndUpdateQueryParams,
|
||||
};
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { parseUrlQueryParams } from './utils';
|
||||
import { DEFAULT_QUERY_PARAMS } from '../../containers/use_get_cases';
|
||||
|
||||
const DEFAULT_STRING_QUERY_PARAMS = {
|
||||
...DEFAULT_QUERY_PARAMS,
|
||||
page: String(DEFAULT_QUERY_PARAMS.page),
|
||||
perPage: String(DEFAULT_QUERY_PARAMS.perPage),
|
||||
};
|
||||
|
||||
describe('utils', () => {
|
||||
describe('parseUrlQueryParams', () => {
|
||||
it('valid input is processed correctly', () => {
|
||||
expect(parseUrlQueryParams(DEFAULT_STRING_QUERY_PARAMS)).toStrictEqual(DEFAULT_QUERY_PARAMS);
|
||||
});
|
||||
|
||||
it('empty string value for page/perPage is ignored', () => {
|
||||
expect(
|
||||
parseUrlQueryParams({
|
||||
...DEFAULT_STRING_QUERY_PARAMS,
|
||||
page: '',
|
||||
perPage: '',
|
||||
})
|
||||
).toStrictEqual({
|
||||
sortField: DEFAULT_QUERY_PARAMS.sortField,
|
||||
sortOrder: DEFAULT_QUERY_PARAMS.sortOrder,
|
||||
});
|
||||
});
|
||||
|
||||
it('0 value for page/perPage is ignored', () => {
|
||||
expect(
|
||||
parseUrlQueryParams({
|
||||
...DEFAULT_STRING_QUERY_PARAMS,
|
||||
page: '0',
|
||||
perPage: '0',
|
||||
})
|
||||
).toStrictEqual({
|
||||
sortField: DEFAULT_QUERY_PARAMS.sortField,
|
||||
sortOrder: DEFAULT_QUERY_PARAMS.sortOrder,
|
||||
});
|
||||
});
|
||||
|
||||
it('invalid string values for page/perPage are ignored', () => {
|
||||
expect(
|
||||
parseUrlQueryParams({
|
||||
...DEFAULT_STRING_QUERY_PARAMS,
|
||||
page: 'foo',
|
||||
perPage: 'bar',
|
||||
})
|
||||
).toStrictEqual({
|
||||
sortField: DEFAULT_QUERY_PARAMS.sortField,
|
||||
sortOrder: DEFAULT_QUERY_PARAMS.sortOrder,
|
||||
});
|
||||
});
|
||||
|
||||
it('additional URL parameters are ignored', () => {
|
||||
expect(
|
||||
parseUrlQueryParams({
|
||||
...DEFAULT_STRING_QUERY_PARAMS,
|
||||
foo: 'bar',
|
||||
})
|
||||
).toStrictEqual(DEFAULT_QUERY_PARAMS);
|
||||
});
|
||||
});
|
||||
});
|
30
x-pack/plugins/cases/public/components/all_cases/utils.ts
Normal file
30
x-pack/plugins/cases/public/components/all_cases/utils.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { ParsedUrlQueryParams, UrlQueryParams } from '../../../common/ui/types';
|
||||
|
||||
export const parseUrlQueryParams = (parsedUrlParams: ParsedUrlQueryParams): UrlQueryParams => {
|
||||
const urlParams: UrlQueryParams = {
|
||||
...(parsedUrlParams.sortField && { sortField: parsedUrlParams.sortField }),
|
||||
...(parsedUrlParams.sortOrder && { sortOrder: parsedUrlParams.sortOrder }),
|
||||
};
|
||||
|
||||
const intPage = parsedUrlParams.page && parseInt(parsedUrlParams.page, 10);
|
||||
const intPerPage = parsedUrlParams.perPage && parseInt(parsedUrlParams.perPage, 10);
|
||||
|
||||
// page=0 is deliberately ignored
|
||||
if (intPage) {
|
||||
urlParams.page = intPage;
|
||||
}
|
||||
|
||||
// perPage=0 is deliberately ignored
|
||||
if (intPerPage) {
|
||||
urlParams.perPage = intPerPage;
|
||||
}
|
||||
|
||||
return urlParams;
|
||||
};
|
|
@ -8,7 +8,7 @@
|
|||
import type { SingleCaseMetricsFeature } from './types';
|
||||
|
||||
export const DEFAULT_TABLE_ACTIVE_PAGE = 1;
|
||||
export const DEFAULT_TABLE_LIMIT = 5;
|
||||
export const DEFAULT_TABLE_LIMIT = 10;
|
||||
|
||||
export const casesQueriesKeys = {
|
||||
all: ['cases'] as const,
|
||||
|
|
|
@ -17,16 +17,11 @@ import type { AppLeaveHandler, AppMountParameters } from '@kbn/core/public';
|
|||
|
||||
import { EuiThemeProvider } from '@kbn/kibana-react-plugin/common';
|
||||
import { ManageUserInfo } from '../detections/components/user_info';
|
||||
import { DEFAULT_DARK_MODE, APP_NAME, APP_ID } from '../../common/constants';
|
||||
import { DEFAULT_DARK_MODE, APP_NAME } from '../../common/constants';
|
||||
import { ErrorToastDispatcher } from '../common/components/error_toast_dispatcher';
|
||||
import { MlCapabilitiesProvider } from '../common/components/ml/permissions/ml_capabilities_provider';
|
||||
import { GlobalToaster, ManageGlobalToaster } from '../common/components/toasters';
|
||||
import {
|
||||
KibanaContextProvider,
|
||||
useGetUserCasesPermissions,
|
||||
useKibana,
|
||||
useUiSetting$,
|
||||
} from '../common/lib/kibana';
|
||||
import { KibanaContextProvider, useKibana, useUiSetting$ } from '../common/lib/kibana';
|
||||
import type { State } from '../common/store';
|
||||
|
||||
import type { StartServices } from '../types';
|
||||
|
@ -54,11 +49,8 @@ const StartAppComponent: FC<StartAppComponent> = ({
|
|||
const {
|
||||
i18n,
|
||||
application: { capabilities },
|
||||
cases,
|
||||
} = useKibana().services;
|
||||
const [darkMode] = useUiSetting$<boolean>(DEFAULT_DARK_MODE);
|
||||
const userCasesPermissions = useGetUserCasesPermissions();
|
||||
const CasesContext = cases.ui.getCasesContext();
|
||||
return (
|
||||
<EuiErrorBoundary>
|
||||
<i18n.Context>
|
||||
|
@ -70,15 +62,13 @@ const StartAppComponent: FC<StartAppComponent> = ({
|
|||
<UserPrivilegesProvider kibanaCapabilities={capabilities}>
|
||||
<ManageUserInfo>
|
||||
<ReactQueryClientProvider>
|
||||
<CasesContext owner={[APP_ID]} permissions={userCasesPermissions}>
|
||||
<PageRouter
|
||||
history={history}
|
||||
onAppLeave={onAppLeave}
|
||||
setHeaderActionMenu={setHeaderActionMenu}
|
||||
>
|
||||
{children}
|
||||
</PageRouter>
|
||||
</CasesContext>
|
||||
<PageRouter
|
||||
history={history}
|
||||
onAppLeave={onAppLeave}
|
||||
setHeaderActionMenu={setHeaderActionMenu}
|
||||
>
|
||||
{children}
|
||||
</PageRouter>
|
||||
</ReactQueryClientProvider>
|
||||
</ManageUserInfo>
|
||||
</UserPrivilegesProvider>
|
||||
|
|
|
@ -11,11 +11,13 @@ import React, { memo, useEffect } from 'react';
|
|||
import { Router, Switch } from 'react-router-dom';
|
||||
import { Route } from '@kbn/kibana-react-plugin/public';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import type { AppLeaveHandler, AppMountParameters } from '@kbn/core/public';
|
||||
import { ManageRoutesSpy } from '../common/utils/route/manage_spy_routes';
|
||||
|
||||
import { APP_ID } from '../../common/constants';
|
||||
import { RouteCapture } from '../common/components/endpoint/route_capture';
|
||||
import { useGetUserCasesPermissions, useKibana } from '../common/lib/kibana';
|
||||
import type { AppAction } from '../common/store/actions';
|
||||
import { ManageRoutesSpy } from '../common/utils/route/manage_spy_routes';
|
||||
import { NotFoundPage } from './404';
|
||||
import { HomePage } from './home';
|
||||
|
||||
|
@ -32,6 +34,9 @@ const PageRouterComponent: FC<RouterProps> = ({
|
|||
onAppLeave,
|
||||
setHeaderActionMenu,
|
||||
}) => {
|
||||
const { cases } = useKibana().services;
|
||||
const CasesContext = cases.ui.getCasesContext();
|
||||
const userCasesPermissions = useGetUserCasesPermissions();
|
||||
const dispatch = useDispatch<(action: AppAction) => void>();
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
|
@ -50,7 +55,9 @@ const PageRouterComponent: FC<RouterProps> = ({
|
|||
<RouteCapture>
|
||||
<Switch>
|
||||
<Route path="/">
|
||||
<HomePage setHeaderActionMenu={setHeaderActionMenu}>{children}</HomePage>
|
||||
<CasesContext owner={[APP_ID]} permissions={userCasesPermissions}>
|
||||
<HomePage setHeaderActionMenu={setHeaderActionMenu}>{children}</HomePage>
|
||||
</CasesContext>
|
||||
</Route>
|
||||
<Route>
|
||||
<NotFoundPage />
|
||||
|
|
|
@ -376,7 +376,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => {
|
|||
|
||||
describe('pagination', () => {
|
||||
before(async () => {
|
||||
await cases.api.createNthRandomCases(8);
|
||||
await cases.api.createNthRandomCases(12);
|
||||
await header.waitUntilLoadingHasFinished();
|
||||
await cases.casesTable.waitForCasesToBeListed();
|
||||
});
|
||||
|
@ -388,7 +388,10 @@ export default ({ getPageObject, getService }: FtrProviderContext) => {
|
|||
|
||||
it('paginates cases correctly', async () => {
|
||||
await testSubjects.click('tablePaginationPopoverButton');
|
||||
await testSubjects.click('tablePagination-5-rows');
|
||||
await testSubjects.click('tablePagination-25-rows');
|
||||
await testSubjects.missingOrFail('pagination-button-1');
|
||||
await testSubjects.click('tablePaginationPopoverButton');
|
||||
await testSubjects.click('tablePagination-10-rows');
|
||||
await testSubjects.isEnabled('pagination-button-1');
|
||||
await testSubjects.click('pagination-button-1');
|
||||
await testSubjects.isEnabled('pagination-button-0');
|
||||
|
|
|
@ -16,6 +16,7 @@ import {
|
|||
EuiButton,
|
||||
EuiFlexGroup,
|
||||
} from '@elastic/eui';
|
||||
import { Router } from 'react-router-dom';
|
||||
import { AppMountParameters, CoreStart } from '@kbn/core/public';
|
||||
import { CasesUiStart } from '@kbn/cases-plugin/public';
|
||||
import { CommentType } from '@kbn/cases-plugin/common';
|
||||
|
@ -92,7 +93,7 @@ const CasesFixtureAppWithContext: React.FC<CasesFixtureAppDeps> = (props) => {
|
|||
|
||||
const CasesFixtureApp: React.FC<{ deps: RenderAppProps }> = ({ deps }) => {
|
||||
const { mountParams, coreStart, pluginsStart } = deps;
|
||||
const { theme$ } = mountParams;
|
||||
const { history, theme$ } = mountParams;
|
||||
const { cases } = pluginsStart;
|
||||
|
||||
const CasesContext = cases.ui.getCasesContext();
|
||||
|
@ -108,9 +109,11 @@ const CasesFixtureApp: React.FC<{ deps: RenderAppProps }> = ({ deps }) => {
|
|||
}}
|
||||
>
|
||||
<StyledComponentsThemeProvider>
|
||||
<CasesContext owner={[]} permissions={permissions}>
|
||||
<CasesFixtureAppWithContext cases={cases} />
|
||||
</CasesContext>
|
||||
<Router history={history}>
|
||||
<CasesContext owner={[]} permissions={permissions}>
|
||||
<CasesFixtureAppWithContext cases={cases} />
|
||||
</CasesContext>
|
||||
</Router>
|
||||
</StyledComponentsThemeProvider>
|
||||
</KibanaContextProvider>
|
||||
</KibanaThemeProvider>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue