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

Improve empty trashcan btn #4164

Merged
merged 4 commits into from
Sep 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions public/locales/gsa-de.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
7 changes: 6 additions & 1 deletion src/web/components/dialog/confirmationdialog.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const ConfirmationDialogContent = props => {
}
};

const {content, moveprops, title, rightButtonTitle} = props;
const {content, moveprops, title, rightButtonTitle, loading} = props;

return (
<DialogContent>
Expand All @@ -38,6 +38,7 @@ const ConfirmationDialogContent = props => {
rightButtonTitle={rightButtonTitle}
onLeftButtonClick={props.close}
onRightButtonClick={handleResume}
loading={loading}
/>
</DialogContent>
);
Expand All @@ -50,6 +51,7 @@ ConfirmationDialogContent.propTypes = {
rightButtonTitle: PropTypes.string,
title: PropTypes.string.isRequired,
onResumeClick: PropTypes.func.isRequired,
loading: PropTypes.bool,
};

const ConfirmationDialog = ({
Expand All @@ -59,6 +61,7 @@ const ConfirmationDialog = ({
rightButtonTitle = _('OK'),
onClose,
onResumeClick,
loading,
}) => {
return (
<Dialog width={width} onClose={onClose} resizable={false}>
Expand All @@ -70,6 +73,7 @@ const ConfirmationDialog = ({
title={title}
rightButtonTitle={rightButtonTitle}
onResumeClick={onResumeClick}
loading={loading}
/>
)}
</Dialog>
Expand All @@ -83,6 +87,7 @@ ConfirmationDialog.propTypes = {
width: PropTypes.string,
onClose: PropTypes.func.isRequired,
onResumeClick: PropTypes.func.isRequired,
loading: PropTypes.bool,
};

export default ConfirmationDialog;
Expand Down
156 changes: 156 additions & 0 deletions src/web/pages/extras/__tests__/trashcanpage.jsx
Original file line number Diff line number Diff line change
@@ -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(<TrashcanPage />);
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(<TrashcanPage />);
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(<TrashcanPage />);
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();
});
});
74 changes: 51 additions & 23 deletions src/web/pages/extras/trashcanpage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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%;
Expand All @@ -78,23 +77,20 @@ const ToolBarIcons = () => (
/>
);

const EmptyTrashButton = ({onClick, isLoading}) => {
const EmptyTrashButton = ({onClick}) => {
const capabilities = useCapabilities();

if (!capabilities.mayOp('empty_trashcan')) {
return null;
}
return (
<Layout align="end">
<LoadingButton onClick={onClick} isLoading={isLoading}>
{_('Empty Trash')}
</LoadingButton>
<Button onClick={onClick}>{_('Empty Trash')}</Button>
</Layout>
);
};

EmptyTrashButton.propTypes = {
isLoading: PropTypes.bool.isRequired,
onClick: PropTypes.func.isRequired,
};

Expand All @@ -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);
Expand All @@ -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() {
Expand Down Expand Up @@ -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() {
Expand All @@ -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);
Expand Down Expand Up @@ -375,15 +392,26 @@ class Trashcan extends React.Component {
{/* span prevents Toolbar from growing */}
<ToolBarIcons />
</span>
{error && (
<ErrorDialog
text={error.message}
title={_('Error')}
onClose={this.handleErrorClose}

<Section img={<TrashcanIcon size="large" />} title={_('Trashcan')} />
<EmptyTrashButton onClick={this.openEmptyTrashDialog} />
{this.state.isEmptyTrashDialogVisible && (
<ConfirmationDialog
onClose={this.closeEmptyTrashDialog}
onResumeClick={this.handleEmpty}
content={
error
? _(
'An error occurred while emptying the trash, please try again.',
)
: _('Are you sure you want to empty the trash?')
}
title={_('Empty Trash')}
rightButtonTitle={_('Confirm')}
loading={this.state.isEmptyingTrash || isLoading}
width="500px"
/>
)}
<Section img={<TrashcanIcon size="large" />} title={_('Trashcan')} />
<EmptyTrashButton onClick={this.handleEmpty} isLoading={isLoading} />
<LinkTarget id="Contents" />
<h1>{_('Contents')}</h1>
<Table>
Expand Down
Loading