diff --git a/public/locales/gsa-de.json b/public/locales/gsa-de.json index a302ea7032..89bbabef69 100644 --- a/public/locales/gsa-de.json +++ b/public/locales/gsa-de.json @@ -132,6 +132,8 @@ "Apply to page contents": "Auf Seiteninhalt anwenden", "Apply to selection": "Auf Auswahl anwenden", "Apps": "Apps", + "Are you sure you want to empty the trash?": "Sind Sie sicher, dass Sie den Pap,ierkorb leeren möchten?", + "An error occurred while emptying the trash, please try again.": "Beim Leeren des Papierkorbs ist ein Fehler aufgetreten, bitte versuchen Sie es erneut.", "As a short-cut the following steps will be done for you:": "Als Abkürzung wird folgendes durchgeführt:", "As soon as the scan progress is beyond 1%, you can already jump to the scan report by clicking on the progress bar in the \"Status\" column and review the results collected so far.": "Sobald der Scanfortschritt 1 % überschritten hat, können Sie über die Statusanzeige in der Spalte \"Status\" auf der Seite \"Aufgaben\" die bereits gesammelten Ergebnisse einsehen.", "Ascending": "Aufsteigend", diff --git a/src/web/components/dialog/confirmationdialog.jsx b/src/web/components/dialog/confirmationdialog.jsx index d0df2ec22c..6494310baa 100644 --- a/src/web/components/dialog/confirmationdialog.jsx +++ b/src/web/components/dialog/confirmationdialog.jsx @@ -26,7 +26,7 @@ const ConfirmationDialogContent = props => { } }; - const {content, moveprops, title, rightButtonTitle} = props; + const {content, moveprops, title, rightButtonTitle, loading} = props; return ( @@ -38,6 +38,7 @@ const ConfirmationDialogContent = props => { rightButtonTitle={rightButtonTitle} onLeftButtonClick={props.close} onRightButtonClick={handleResume} + loading={loading} /> ); @@ -50,6 +51,7 @@ ConfirmationDialogContent.propTypes = { rightButtonTitle: PropTypes.string, title: PropTypes.string.isRequired, onResumeClick: PropTypes.func.isRequired, + loading: PropTypes.bool, }; const ConfirmationDialog = ({ @@ -59,6 +61,7 @@ const ConfirmationDialog = ({ rightButtonTitle = _('OK'), onClose, onResumeClick, + loading, }) => { return ( @@ -70,6 +73,7 @@ const ConfirmationDialog = ({ title={title} rightButtonTitle={rightButtonTitle} onResumeClick={onResumeClick} + loading={loading} /> )} @@ -83,6 +87,7 @@ ConfirmationDialog.propTypes = { width: PropTypes.string, onClose: PropTypes.func.isRequired, onResumeClick: PropTypes.func.isRequired, + loading: PropTypes.bool, }; export default ConfirmationDialog; diff --git a/src/web/pages/extras/__tests__/trashcanpage.jsx b/src/web/pages/extras/__tests__/trashcanpage.jsx new file mode 100644 index 0000000000..d4cdce5e56 --- /dev/null +++ b/src/web/pages/extras/__tests__/trashcanpage.jsx @@ -0,0 +1,156 @@ +/* SPDX-FileCopyrightText: 2024 Greenbone AG + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import {describe, test, expect, testing} from '@gsa/testing'; + +import {userEvent, rendererWith, waitFor} from 'web/utils/testing'; + +import TrashcanPage from 'web/pages/extras/trashcanpage'; +import Capabilities from 'gmp/capabilities/capabilities'; + +/* + * The following is a workaround for userEvent v14 and fake timers https://github.com/testing-library/react-testing-library/issues/1197 + */ + +testing.useFakeTimers({ + shouldAdvanceTime: true, +}); + +const gmp = { + trashcan: { + empty: testing.fn().mockResolvedValueOnce(), + get: testing.fn().mockReturnValue( + Promise.resolve({ + data: [], + }), + ), + }, + settings: { + manualUrl: 'http://docs.greenbone.net/GSM-Manual/gos-5/', + }, + user: { + renewSession: testing.fn().mockReturnValue( + Promise.resolve({ + data: 'foo', + }), + ), + }, +}; + +const capabilities = new Capabilities(['everything']); + +describe('Trashcan page tests', () => { + test('Should render with empty trashcan button and empty out trash', async () => { + const {render} = rendererWith({ + gmp, + capabilities, + store: true, + }); + + const {getByText, queryByTestId, getByRole} = render(); + expect(queryByTestId('loading')).toBeVisible(); + await waitFor(() => { + expect(queryByTestId('loading')).not.toBeInTheDocument(); + }); + const emptyTrashcanButton = getByRole('button', { + name: /Empty Trash/i, + }); + + userEvent.click(emptyTrashcanButton); + await waitFor(() => { + expect( + getByText('Are you sure you want to empty the trash?'), + ).toBeVisible(); + }); + + const confirmButton = getByRole('button', {name: /Confirm/i}); + + await userEvent.click(confirmButton); + + await waitFor(() => { + expect(gmp.trashcan.empty).toHaveBeenCalled(); + }); + + testing.advanceTimersByTime(1000); + + await waitFor(() => { + expect(confirmButton).not.toBeVisible(); + }); + }); + + test('Should render with empty trashcan button and handle error case', async () => { + const errorGmp = { + ...gmp, + trashcan: { + ...gmp.trashcan, + empty: testing + .fn() + .mockRejectedValue(new Error('Failed to empty trash')), + }, + }; + const {render} = rendererWith({ + gmp: errorGmp, + capabilities, + store: true, + }); + + const {getByText, queryByTestId, getByRole} = render(); + expect(queryByTestId('loading')).toBeVisible(); + await waitFor(() => { + expect(queryByTestId('loading')).not.toBeInTheDocument(); + }); + const emptyTrashcanButton = getByRole('button', { + name: /Empty Trash/i, + }); + + userEvent.click(emptyTrashcanButton); + await waitFor(() => { + expect( + getByText('Are you sure you want to empty the trash?'), + ).toBeVisible(); + }); + + const confirmButton = getByRole('button', {name: /Confirm/i}); + + await userEvent.click(confirmButton); + + expect(errorGmp.trashcan.empty).toHaveBeenCalled(); + + expect( + getByText( + 'An error occurred while emptying the trash, please try again.', + ), + ).toBeVisible(); + }); + + test('Should render open and close dialog', async () => { + const {render} = rendererWith({ + gmp, + capabilities, + store: true, + }); + + const {getByText, queryByTestId, getByRole} = render(); + expect(queryByTestId('loading')).toBeVisible(); + await waitFor(() => { + expect(queryByTestId('loading')).not.toBeInTheDocument(); + }); + const emptyTrashcanButton = getByRole('button', { + name: /Empty Trash/i, + }); + + await userEvent.click(emptyTrashcanButton); + + expect( + getByText('Are you sure you want to empty the trash?'), + ).toBeVisible(); + + const cancelButton = getByRole('button', {name: /Cancel/i}); + + await userEvent.click(cancelButton); + + expect(cancelButton).not.toBeVisible(); + }); +}); diff --git a/src/web/pages/extras/trashcanpage.jsx b/src/web/pages/extras/trashcanpage.jsx index 48bcc9a428..273cf6176b 100644 --- a/src/web/pages/extras/trashcanpage.jsx +++ b/src/web/pages/extras/trashcanpage.jsx @@ -13,9 +13,7 @@ import _ from 'gmp/locale'; import {isDefined} from 'gmp/utils/identity'; -import ErrorDialog from 'web/components/dialog/errordialog'; - -import LoadingButton from 'web/components/form/loadingbutton'; +import Button from 'web/components/form/button'; import ManualIcon from 'web/components/icon/manualicon'; import TrashcanIcon from 'web/components/icon/trashcanicon'; @@ -65,6 +63,7 @@ import TasksTable from '../tasks/table'; import TicketsTable from '../tickets/table'; import TrashActions from './trashactions'; +import ConfirmationDialog from 'web/components/dialog/confirmationdialog'; const Col = styled.col` width: 50%; @@ -78,7 +77,7 @@ const ToolBarIcons = () => ( /> ); -const EmptyTrashButton = ({onClick, isLoading}) => { +const EmptyTrashButton = ({onClick}) => { const capabilities = useCapabilities(); if (!capabilities.mayOp('empty_trashcan')) { @@ -86,15 +85,12 @@ const EmptyTrashButton = ({onClick, isLoading}) => { } return ( - - {_('Empty Trash')} - + ); }; EmptyTrashButton.propTypes = { - isLoading: PropTypes.bool.isRequired, onClick: PropTypes.func.isRequired, }; @@ -119,6 +115,8 @@ class Trashcan extends React.Component { this.state = { trash: undefined, isLoading: false, + isEmptyTrashDialogVisible: false, + isEmptyingTrash: false, }; this.createContentRow = this.createContentRow.bind(this); @@ -128,6 +126,8 @@ class Trashcan extends React.Component { this.handleDelete = this.handleDelete.bind(this); this.handleRestore = this.handleRestore.bind(this); this.handleErrorClose = this.handleErrorClose.bind(this); + this.closeEmptyTrashDialog = this.closeEmptyTrashDialog.bind(this); + this.openEmptyTrashDialog = this.openEmptyTrashDialog.bind(this); } componentDidMount() { @@ -187,19 +187,27 @@ class Trashcan extends React.Component { }); } - handleEmpty() { + async handleEmpty() { const {gmp} = this.props; this.handleInteraction(); - gmp.trashcan - .empty() - .then(this.getTrash) - .catch(error => { - this.setState({ - error, + this.setState({isEmptyingTrash: true}); + + try { + await gmp.trashcan.empty(); + this.getTrash(); + } catch (error) { + this.setState({error}); + } finally { + setTimeout(() => { + this.setState({isEmptyingTrash: false}, () => { + if (!this.state.isLoading && !this.state.error) { + this.closeEmptyTrashDialog(); + } }); - }); + }, 1000); + } } handleErrorClose() { @@ -219,6 +227,15 @@ class Trashcan extends React.Component { ); } + openEmptyTrashDialog = () => { + this.setState({isEmptyTrashDialogVisible: true}); + }; + + closeEmptyTrashDialog = () => { + this.setState({isEmptyTrashDialogVisible: false}); + this.setState({error: undefined}); + }; + createContentsTable(trash) { const render_alerts = isDefined(trash.alert_list); const render_credentials = isDefined(trash.credential_list); @@ -375,15 +392,26 @@ class Trashcan extends React.Component { {/* span prevents Toolbar from growing */} - {error && ( - } title={_('Trashcan')} /> + + {this.state.isEmptyTrashDialogVisible && ( + )} -
} title={_('Trashcan')} /> -

{_('Contents')}