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 (
@@ -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')}