diff --git a/x-pack/plugins/cases/public/components/all_cases/all_cases_list.test.tsx b/x-pack/plugins/cases/public/components/all_cases/all_cases_list.test.tsx index 180a8828c88b1..10b55c0e979b3 100644 --- a/x-pack/plugins/cases/public/components/all_cases/all_cases_list.test.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/all_cases_list.test.tsx @@ -191,7 +191,10 @@ describe('AllCasesListGeneric', () => { expect( screen.getAllByTestId('case-table-column-createdAt')[0].querySelector('.euiToolTipAnchor') ).toHaveTextContent(removeMsFromDate(useGetCasesMockState.data.cases[0].createdAt)); - expect(screen.getByTestId('case-table-case-count')).toHaveTextContent('Showing 10 cases'); + expect(screen.getByTestId('case-table-case-count')).toHaveTextContent( + `Showing 10 of ${useGetCasesMockState.data.total} cases` + ); + expect(screen.queryByTestId('all-cases-maximum-limit-warning')).not.toBeInTheDocument(); }); }); diff --git a/x-pack/plugins/cases/public/components/all_cases/table.tsx b/x-pack/plugins/cases/public/components/all_cases/table.tsx index dc5e7042ec9f7..ae60e942dc75a 100644 --- a/x-pack/plugins/cases/public/components/all_cases/table.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/table.tsx @@ -79,6 +79,7 @@ export const CasesTable: FunctionComponent = ({ ) : ( <> defaultMessage: 'Selected {totalRules} {totalRules, plural, =1 {case} other {cases}}', }); -export const SHOWING_CASES = (totalRules: number) => +export const SHOWING_CASES = (totalRules: number, pageSize: number) => i18n.translate('xpack.cases.caseTable.showingCasesTitle', { - values: { totalRules }, - defaultMessage: 'Showing {totalRules} {totalRules, plural, =1 {case} other {cases}}', + values: { totalRules, pageSize }, + defaultMessage: + 'Showing {pageSize} of {totalRules} {totalRules, plural, =1 {case} other {cases}}', + }); + +export const MAX_CASES = (maxCases: number) => + i18n.translate('xpack.cases.caseTable.maxCases', { + values: { maxCases }, + defaultMessage: + 'The results were capped at {maxCases} to maintain performance. Try limiting your search to reduce the results.', }); +export const DISMISS = i18n.translate('xpack.cases.caseTable.dismiss', { + defaultMessage: 'Dismiss', +}); + +export const NOT_SHOW_AGAIN = i18n.translate('xpack.cases.caseTable.notShowAgain', { + defaultMessage: 'Do not show again', +}); + export const UNIT = (totalCount: number) => i18n.translate('xpack.cases.caseTable.unit', { values: { totalCount }, diff --git a/x-pack/plugins/cases/public/components/all_cases/utility_bar.test.tsx b/x-pack/plugins/cases/public/components/all_cases/utility_bar.test.tsx index 2fddd22a9083c..4d740c2fb9552 100644 --- a/x-pack/plugins/cases/public/components/all_cases/utility_bar.test.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/utility_bar.test.tsx @@ -5,10 +5,11 @@ * 2.0. */ -import { act, waitFor } from '@testing-library/react'; +import { act, waitFor, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; import type { AppMockRenderer } from '../../common/mock'; +import { MAX_DOCS_PER_PAGE } from '../../../common/constants'; import { noCasesPermissions, onlyDeleteCasesPermission, @@ -22,11 +23,17 @@ import { CasesTableUtilityBar } from './utility_bar'; describe('Severity form field', () => { let appMockRender: AppMockRenderer; const deselectCases = jest.fn(); + const localStorageKey = 'cases.testAppId.utilityBar.hideMaxLimitWarning'; const props = { totalCases: 5, selectedCases: [basicCase], deselectCases, + pagination: { + pageIndex: 1, + pageSize: 10, + totalItemCount: 5, + }, }; beforeEach(() => { @@ -34,52 +41,89 @@ describe('Severity form field', () => { }); it('renders', async () => { - const result = appMockRender.render(); - expect(result.getByText('Showing 5 cases')).toBeInTheDocument(); - expect(result.getByText('Selected 1 case')).toBeInTheDocument(); - expect(result.getByTestId('case-table-bulk-actions-link-icon')).toBeInTheDocument(); - expect(result.getByTestId('all-cases-refresh-link-icon')).toBeInTheDocument(); + appMockRender.render(); + expect(screen.getByText('Showing 5 of 5 cases')).toBeInTheDocument(); + expect(screen.getByText('Selected 1 case')).toBeInTheDocument(); + expect(screen.getByTestId('case-table-bulk-actions-link-icon')).toBeInTheDocument(); + expect(screen.getByTestId('all-cases-refresh-link-icon')).toBeInTheDocument(); + expect(screen.queryByTestId('all-cases-maximum-limit-warning')).not.toBeInTheDocument(); + }); + + it('renders showing cases correctly', async () => { + const updatedProps = { + ...props, + totalCases: 20, + pagination: { + ...props.pagination, + totalItemCount: 20, + }, + }; + appMockRender.render(); + expect(screen.getByText('Showing 10 of 20 cases')).toBeInTheDocument(); + expect(screen.getByText('Selected 1 case')).toBeInTheDocument(); + }); + + it('renders showing cases correctly for second page', async () => { + const updatedProps = { + ...props, + totalCases: 20, + pagination: { + ...props.pagination, + pageIndex: 2, + totalItemCount: 20, + }, + }; + appMockRender.render(); + expect(screen.getByText('Showing 10 of 20 cases')).toBeInTheDocument(); + expect(screen.getByText('Selected 1 case')).toBeInTheDocument(); + }); + + it('renders showing cases correctly when no cases available', async () => { + const updatedProps = { + totalCases: 0, + selectedCases: [], + deselectCases, + pagination: { + pageSize: 10, + pageIndex: 1, + totalItemCount: 0, + }, + }; + appMockRender.render(); + expect(screen.getByText('Showing 0 of 0 cases')).toBeInTheDocument(); }); it('opens the bulk actions correctly', async () => { - const result = appMockRender.render(); + appMockRender.render(); - act(() => { - userEvent.click(result.getByTestId('case-table-bulk-actions-link-icon')); - }); + userEvent.click(screen.getByTestId('case-table-bulk-actions-link-icon')); await waitFor(() => { - expect(result.getByTestId('case-table-bulk-actions-context-menu')); + expect(screen.getByTestId('case-table-bulk-actions-context-menu')); }); }); it('closes the bulk actions correctly', async () => { - const result = appMockRender.render(); + appMockRender.render(); - act(() => { - userEvent.click(result.getByTestId('case-table-bulk-actions-link-icon')); - }); + userEvent.click(screen.getByTestId('case-table-bulk-actions-link-icon')); await waitFor(() => { - expect(result.getByTestId('case-table-bulk-actions-context-menu')); + expect(screen.getByTestId('case-table-bulk-actions-context-menu')); }); - act(() => { - userEvent.click(result.getByTestId('case-table-bulk-actions-link-icon')); - }); + userEvent.click(screen.getByTestId('case-table-bulk-actions-link-icon')); await waitFor(() => { - expect(result.queryByTestId('case-table-bulk-actions-context-menu')).toBeFalsy(); + expect(screen.queryByTestId('case-table-bulk-actions-context-menu')).toBeFalsy(); }); }); it('refresh correctly', async () => { - const result = appMockRender.render(); + appMockRender.render(); const queryClientSpy = jest.spyOn(appMockRender.queryClient, 'invalidateQueries'); - act(() => { - userEvent.click(result.getByTestId('all-cases-refresh-link-icon')); - }); + userEvent.click(screen.getByTestId('all-cases-refresh-link-icon')); await waitFor(() => { expect(deselectCases).toHaveBeenCalled(); @@ -91,29 +135,198 @@ describe('Severity form field', () => { it('does not show the bulk actions without update & delete permissions', async () => { appMockRender = createAppMockRenderer({ permissions: noCasesPermissions() }); - const result = appMockRender.render(); + appMockRender.render(); - expect(result.queryByTestId('case-table-bulk-actions-link-icon')).toBeFalsy(); + expect(screen.queryByTestId('case-table-bulk-actions-link-icon')).toBeFalsy(); }); it('does show the bulk actions with only delete permissions', async () => { appMockRender = createAppMockRenderer({ permissions: onlyDeleteCasesPermission() }); - const result = appMockRender.render(); + appMockRender.render(); - expect(result.getByTestId('case-table-bulk-actions-link-icon')).toBeInTheDocument(); + expect(screen.getByTestId('case-table-bulk-actions-link-icon')).toBeInTheDocument(); }); it('does show the bulk actions with update permissions', async () => { appMockRender = createAppMockRenderer({ permissions: writeCasesPermissions() }); - const result = appMockRender.render(); + appMockRender.render(); - expect(result.getByTestId('case-table-bulk-actions-link-icon')).toBeInTheDocument(); + expect(screen.getByTestId('case-table-bulk-actions-link-icon')).toBeInTheDocument(); }); it('does not show the bulk actions if there are not selected cases', async () => { - const result = appMockRender.render(); + appMockRender.render(); + + expect(screen.queryByTestId('case-table-bulk-actions-link-icon')).toBeFalsy(); + expect(screen.queryByText('Showing 0 cases')).toBeFalsy(); + }); + + describe('Maximum number of cases', () => { + const newProps = { + ...props, + selectedCaseS: [], + totalCases: MAX_DOCS_PER_PAGE, + pagination: { + ...props.pagination, + totalItemCount: MAX_DOCS_PER_PAGE, + }, + }; + + const allCasesPageSize = [10, 25, 50, 100]; + + it.each(allCasesPageSize)( + `does not show warning when totalCases = ${MAX_DOCS_PER_PAGE} but pageSize(%s) * pageIndex + 1 < ${MAX_DOCS_PER_PAGE}`, + (size) => { + const newPageIndex = MAX_DOCS_PER_PAGE / size - 2; + + appMockRender.render( + + ); + + expect( + screen.getByText(`Showing ${size} of ${MAX_DOCS_PER_PAGE} cases`) + ).toBeInTheDocument(); + expect(screen.queryByTestId('all-cases-maximum-limit-warning')).not.toBeInTheDocument(); + } + ); + + it.each(allCasesPageSize)( + `shows warning when totalCases = ${MAX_DOCS_PER_PAGE} but pageSize(%s) * pageIndex + 1 = ${MAX_DOCS_PER_PAGE}`, + (size) => { + const newPageIndex = MAX_DOCS_PER_PAGE / size - 1; + + appMockRender.render( + + ); + + expect( + screen.getByText(`Showing ${size} of ${MAX_DOCS_PER_PAGE} cases`) + ).toBeInTheDocument(); + expect(screen.getByTestId('all-cases-maximum-limit-warning')).toBeInTheDocument(); + } + ); + + it.each(allCasesPageSize)( + `shows warning when totalCases = ${MAX_DOCS_PER_PAGE} but pageSize(%s) * pageIndex + 1 > ${MAX_DOCS_PER_PAGE}`, + (size) => { + const newPageIndex = MAX_DOCS_PER_PAGE / size; + + appMockRender.render( + + ); - expect(result.queryByTestId('case-table-bulk-actions-link-icon')).toBeFalsy(); - expect(result.queryByText('Showing 0 cases')).toBeFalsy(); + expect( + screen.getByText(`Showing ${size} of ${MAX_DOCS_PER_PAGE} cases`) + ).toBeInTheDocument(); + expect(screen.getByTestId('all-cases-maximum-limit-warning')).toBeInTheDocument(); + } + ); + + it('should show dismiss and do not show again buttons correctly', () => { + appMockRender.render( + + ); + + expect(screen.getByTestId('all-cases-maximum-limit-warning')).toBeInTheDocument(); + expect(screen.getByTestId('dismiss-warning')).toBeInTheDocument(); + + expect(screen.getByTestId('do-not-show-warning')).toBeInTheDocument(); + }); + + it('should dismiss warning correctly', () => { + appMockRender.render( + + ); + + expect(screen.getByTestId('all-cases-maximum-limit-warning')).toBeInTheDocument(); + expect(screen.getByTestId('dismiss-warning')).toBeInTheDocument(); + + userEvent.click(screen.getByTestId('dismiss-warning')); + + expect(screen.queryByTestId('all-cases-maximum-limit-warning')).not.toBeInTheDocument(); + }); + + describe('do not show button', () => { + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.clearAllTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + sessionStorage.removeItem(localStorageKey); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should set storage key correctly', () => { + appMockRender.render( + + ); + + expect(screen.getByTestId('all-cases-maximum-limit-warning')).toBeInTheDocument(); + expect(screen.getByTestId('do-not-show-warning')).toBeInTheDocument(); + + expect(localStorage.getItem(localStorageKey)).toBe(null); + }); + + it('should hide warning correctly when do not show button clicked', () => { + appMockRender.render( + + ); + + expect(screen.getByTestId('all-cases-maximum-limit-warning')).toBeInTheDocument(); + expect(screen.getByTestId('do-not-show-warning')).toBeInTheDocument(); + + userEvent.click(screen.getByTestId('do-not-show-warning')); + + act(() => { + jest.advanceTimersByTime(1000); + }); + + expect(screen.queryByTestId('all-cases-maximum-limit-warning')).not.toBeInTheDocument(); + expect(localStorage.getItem(localStorageKey)).toBe('true'); + }); + }); }); }); diff --git a/x-pack/plugins/cases/public/components/all_cases/utility_bar.tsx b/x-pack/plugins/cases/public/components/all_cases/utility_bar.tsx index 4afc8e7e681a0..2c2aebdb30bb2 100644 --- a/x-pack/plugins/cases/public/components/all_cases/utility_bar.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/utility_bar.tsx @@ -6,7 +6,10 @@ */ import type { FunctionComponent } from 'react'; +import { css } from '@emotion/react'; +import useLocalStorage from 'react-use/lib/useLocalStorage'; import React, { useCallback, useState } from 'react'; +import type { Pagination } from '@elastic/eui'; import { EuiButtonEmpty, EuiContextMenu, @@ -15,9 +18,12 @@ import { EuiPopover, EuiText, useEuiTheme, + EuiCallOut, + EuiSpacer, } from '@elastic/eui'; import * as i18n from './translations'; import type { CasesUI } from '../../../common/ui/types'; +import { MAX_DOCS_PER_PAGE } from '../../../common/constants'; import { useRefreshCases } from './use_on_refresh_cases'; import { useBulkActions } from './use_bulk_actions'; import { useCasesContext } from '../cases_context/use_cases_context'; @@ -27,16 +33,27 @@ interface Props { totalCases: number; selectedCases: CasesUI; deselectCases: () => void; + pagination: Pagination; } export const CasesTableUtilityBar: FunctionComponent = React.memo( - ({ isSelectorView, totalCases, selectedCases, deselectCases }) => { + ({ isSelectorView, totalCases, selectedCases, deselectCases, pagination }) => { const { euiTheme } = useEuiTheme(); + const refreshCases = useRefreshCases(); + const { permissions, appId } = useCasesContext(); + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const [isMessageDismissed, setIsMessageDismissed] = useState(false); + const localStorageKey = `cases.${appId}.utilityBar.hideMaxLimitWarning`; + const [localStorageWarning, setLocalStorageWarning] = useLocalStorage(localStorageKey); + const togglePopover = useCallback(() => setIsPopoverOpen(!isPopoverOpen), [isPopoverOpen]); const closePopover = useCallback(() => setIsPopoverOpen(false), []); - const refreshCases = useRefreshCases(); - const { permissions } = useCasesContext(); + + const toggleWarning = useCallback( + () => setIsMessageDismissed(!isMessageDismissed), + [isMessageDismissed] + ); const onRefresh = useCallback(() => { deselectCases(); @@ -49,6 +66,10 @@ export const CasesTableUtilityBar: FunctionComponent = React.memo( onActionSuccess: onRefresh, }); + const handleNotShowAgain = () => { + setLocalStorageWarning(true); + }; + /** * At least update or delete permissions needed to show bulk actions. * Granular permission check for each action is performed @@ -56,6 +77,52 @@ export const CasesTableUtilityBar: FunctionComponent = React.memo( */ const showBulkActions = (permissions.update || permissions.delete) && selectedCases.length > 0; + const visibleCases = + pagination?.pageSize && totalCases > pagination.pageSize ? pagination.pageSize : totalCases; + + const hasReachedMaxCases = + pagination.pageSize && + totalCases >= MAX_DOCS_PER_PAGE && + pagination.pageSize * (pagination.pageIndex + 1) >= MAX_DOCS_PER_PAGE; + + const isDoNotShowAgainSelected = localStorageWarning && localStorageWarning === true; + + const renderMaxLimitWarning = (): React.ReactNode => ( + + + + {i18n.MAX_CASES(MAX_DOCS_PER_PAGE)} + + + + + {i18n.DISMISS} + + + + + {i18n.NOT_SHOW_AGAIN} + + + + ); + return ( <> = React.memo( }} > - {i18n.SHOWING_CASES(totalCases)} + {i18n.SHOWING_CASES(totalCases, visibleCases)} @@ -136,6 +203,22 @@ export const CasesTableUtilityBar: FunctionComponent = React.memo( {modals} {flyouts} + {hasReachedMaxCases && !isMessageDismissed && !isDoNotShowAgainSelected && ( + <> + + + + + + + + + )} ); } diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index e125ad908942f..fa46583300b92 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -10462,7 +10462,6 @@ "xpack.cases.caseTable.caseDetailsLinkAria": "cliquez pour visiter le cas portant le titre {detailName}", "xpack.cases.caseTable.pushLinkAria": "cliquez pour afficher l'incident relatif à {thirdPartyName}.", "xpack.cases.caseTable.selectedCasesTitle": "{totalRules} {totalRules, plural, =1 {cas} one {aux cas suivants} many {cas} other {cas}} sélectionné(s)", - "xpack.cases.caseTable.showingCasesTitle": "Affichage de {totalRules} {totalRules, plural, =1 {cas} one {aux cas suivants} many {cas} other {cas}}", "xpack.cases.caseTable.unit": "{totalCount, plural, =1 {cas} one {aux cas suivants} many {cas} other {cas}}", "xpack.cases.caseView.actionLabel.selectedThirdParty": "{thirdParty} sélectionné comme système de gestion des incidents", "xpack.cases.caseView.actionLabel.viewIncident": "Afficher {incidentNumber}", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 11be15c392ef4..ca0918a710e07 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -10477,7 +10477,6 @@ "xpack.cases.caseTable.caseDetailsLinkAria": "クリックすると、タイトル{detailName}のケースを表示します", "xpack.cases.caseTable.pushLinkAria": "クリックすると、{thirdPartyName}でインシデントを表示します。", "xpack.cases.caseTable.selectedCasesTitle": "{totalRules}件の{totalRules, plural, =1 {ケース} other {ケース}}が選択済み", - "xpack.cases.caseTable.showingCasesTitle": "{totalRules} {totalRules, plural, =1 {ケース} other {ケース}}を表示中", "xpack.cases.caseTable.unit": "{totalCount, plural, =1 {ケース} other {ケース}}", "xpack.cases.caseView.actionLabel.selectedThirdParty": "{thirdParty}をインシデント管理システムとして選択しました", "xpack.cases.caseView.actionLabel.viewIncident": "{incidentNumber}を表示", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 013f01bd36f54..192f11122e88d 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -10477,7 +10477,6 @@ "xpack.cases.caseTable.caseDetailsLinkAria": "单击以访问标题为 {detailName} 的案例", "xpack.cases.caseTable.pushLinkAria": "单击可在 {thirdPartyName} 上查看该事件。", "xpack.cases.caseTable.selectedCasesTitle": "已选定 {totalRules} 个{totalRules, plural, =1 {案例} other {案例}}", - "xpack.cases.caseTable.showingCasesTitle": "正在显示 {totalRules} 个 {totalRules, plural, =1 {案例} other {案例}}", "xpack.cases.caseTable.unit": "{totalCount, plural, =1 {案例} other {案例}}", "xpack.cases.caseView.actionLabel.selectedThirdParty": "已选择 {thirdParty} 作为事件管理系统", "xpack.cases.caseView.actionLabel.viewIncident": "查看 {incidentNumber}",