Skip to content

Commit 8ceb835

Browse files
YosiEliasyelias
andauthored
feat(ws): implement delete workspace action with a confirmation popup (#178)
* feat(ws): Notebooks 2.0 // Frontend // Delete workspace Signed-off-by: yelias <yossi.elias@nokia.com> * Rename deleteModal.tsx Signed-off-by: yelias <yossi.elias@nokia.com> --------- Signed-off-by: yelias <yossi.elias@nokia.com> Co-authored-by: yelias <yossi.elias@nokia.com>
1 parent 9479c7b commit 8ceb835

File tree

3 files changed

+206
-5
lines changed

3 files changed

+206
-5
lines changed
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { mockNamespaces } from '~/__mocks__/mockNamespaces';
2+
import { mockBFFResponse } from '~/__mocks__/utils';
3+
4+
describe('Workspaces Component', () => {
5+
beforeEach(() => {
6+
// Mock the namespaces API response
7+
cy.intercept('GET', '/api/v1/namespaces', {
8+
body: mockBFFResponse(mockNamespaces),
9+
}).as('getNamespaces');
10+
cy.visit('/');
11+
cy.wait('@getNamespaces');
12+
});
13+
14+
function openDeleteModal() {
15+
cy.findAllByTestId('table-body').first().findByTestId('action-column').click();
16+
cy.findByTestId('action-delete').click();
17+
cy.findByTestId('delete-modal-input').should('have.value', '');
18+
}
19+
20+
it('should test the close mechanisms of the delete modal', () => {
21+
const closeModalActions = [
22+
() => cy.get('button').contains('Cancel').click(),
23+
() => cy.get('[aria-label="Close"]').click(),
24+
];
25+
26+
closeModalActions.forEach((closeAction) => {
27+
openDeleteModal();
28+
cy.findByTestId('delete-modal-input').type('Some Text');
29+
cy.findByTestId('delete-modal').should('be.visible');
30+
closeAction();
31+
cy.findByTestId('delete-modal').should('not.exist');
32+
});
33+
34+
// Check that clicking outside the modal does not close it
35+
openDeleteModal();
36+
cy.findByTestId('delete-modal').should('be.visible');
37+
cy.get('body').click(0, 0);
38+
cy.findByTestId('delete-modal').should('be.visible');
39+
});
40+
41+
it('should verify the delete modal verification mechanism', () => {
42+
openDeleteModal();
43+
cy.findByTestId('delete-modal').within(() => {
44+
cy.get('strong')
45+
.first()
46+
.invoke('text')
47+
.then((resourceName) => {
48+
// Type incorrect resource name
49+
cy.findByTestId('delete-modal-input').type('Wrong Name');
50+
cy.findByTestId('delete-modal-input').should('have.value', 'Wrong Name');
51+
cy.findByTestId('delete-modal-helper-text').should('be.visible');
52+
cy.get('button').contains('Delete').should('have.css', 'pointer-events', 'none');
53+
54+
// Clear and type correct resource name
55+
cy.findByTestId('delete-modal-input').clear();
56+
cy.findByTestId('delete-modal-input').type(resourceName);
57+
cy.findByTestId('delete-modal-input').should('have.value', resourceName);
58+
cy.findByTestId('delete-modal-helper-text').should('not.be.exist');
59+
cy.get('button').contains('Delete').should('not.have.css', 'pointer-events', 'none');
60+
cy.get('button').contains('Delete').click();
61+
cy.findByTestId('delete-modal').should('not.exist');
62+
});
63+
});
64+
});
65+
});

workspaces/frontend/src/app/pages/Workspaces/Workspaces.tsx

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { useState } from 'react';
2727
import { Workspace, WorkspacesColumnNames, WorkspaceState } from '~/shared/types';
2828
import { WorkspaceDetails } from '~/app/pages/Workspaces/Details/WorkspaceDetails';
2929
import { ExpandedWorkspaceRow } from '~/app/pages/Workspaces/ExpandedWorkspaceRow';
30+
import DeleteModal from '~/shared/components/DeleteModal';
3031
import Filter, { FilteredColumn } from 'shared/components/Filter';
3132
import { formatRam } from 'shared/utilities/WorkspaceResources';
3233

@@ -158,6 +159,7 @@ export const Workspaces: React.FunctionComponent = () => {
158159
const [workspaces, setWorkspaces] = useState(initialWorkspaces);
159160
const [expandedWorkspacesNames, setExpandedWorkspacesNames] = React.useState<string[]>([]);
160161
const [selectedWorkspace, setSelectedWorkspace] = React.useState<Workspace | null>(null);
162+
const [workspaceToDelete, setWorkspaceToDelete] = React.useState<Workspace | null>(null);
161163

162164
const selectWorkspace = React.useCallback(
163165
(newSelectedWorkspace) => {
@@ -288,6 +290,12 @@ export const Workspaces: React.FunctionComponent = () => {
288290
console.log(`Clicked on stop, on row ${workspace.name}`);
289291
}, []);
290292

293+
const handleDeleteClick = React.useCallback((workspace: Workspace) => {
294+
const buttonElement = document.activeElement as HTMLElement;
295+
buttonElement.blur(); // Remove focus from the currently focused button
296+
setWorkspaceToDelete(workspace); // Open the modal and set workspace to delete
297+
}, []);
298+
291299
const defaultActions = React.useCallback(
292300
(workspace: Workspace): IActions =>
293301
[
@@ -301,7 +309,7 @@ export const Workspaces: React.FunctionComponent = () => {
301309
},
302310
{
303311
title: 'Delete',
304-
onClick: () => deleteAction(workspace),
312+
onClick: () => handleDeleteClick(workspace),
305313
},
306314
{
307315
isSeparator: true,
@@ -315,7 +323,7 @@ export const Workspaces: React.FunctionComponent = () => {
315323
onClick: () => stopAction(workspace),
316324
},
317325
] as IActions,
318-
[selectWorkspace, editAction, deleteAction, startRestartAction, stopAction],
326+
[selectWorkspace, editAction, handleDeleteClick, startRestartAction, stopAction],
319327
);
320328

321329
// States
@@ -360,7 +368,7 @@ export const Workspaces: React.FunctionComponent = () => {
360368
workspace={selectedWorkspace}
361369
onCloseClick={() => selectWorkspace(null)}
362370
onEditClick={() => editAction(selectedWorkspace)}
363-
onDeleteClick={() => deleteAction(selectedWorkspace)}
371+
onDeleteClick={() => handleDeleteClick(selectedWorkspace)}
364372
/>
365373
)}
366374
</>
@@ -399,6 +407,7 @@ export const Workspaces: React.FunctionComponent = () => {
399407
id="workspaces-table-content"
400408
key={rowIndex}
401409
isExpanded={isWorkspaceExpanded(workspace)}
410+
data-testid="table-body"
402411
>
403412
<Tr id={`workspaces-table-row-${rowIndex + 1}`}>
404413
<Td
@@ -429,8 +438,13 @@ export const Workspaces: React.FunctionComponent = () => {
429438
1 hour ago
430439
</Timestamp>
431440
</Td>
432-
<Td isActionCell>
433-
<ActionsColumn items={defaultActions(workspace)} />
441+
<Td isActionCell data-testid="action-column">
442+
<ActionsColumn
443+
items={defaultActions(workspace).map((action) => ({
444+
...action,
445+
'data-testid': `action-${typeof action.title === 'string' ? action.title.toLowerCase() : ''}`,
446+
}))}
447+
/>
434448
</Td>
435449
</Tr>
436450
{isWorkspaceExpanded(workspace) && (
@@ -439,6 +453,14 @@ export const Workspaces: React.FunctionComponent = () => {
439453
</Tbody>
440454
))}
441455
</Table>
456+
<DeleteModal
457+
isOpen={workspaceToDelete != null}
458+
resourceName={workspaceToDelete?.name || ''}
459+
namespace={workspaceToDelete?.namespace || ''}
460+
title="Delete Workspace?"
461+
onClose={() => setWorkspaceToDelete(null)}
462+
onDelete={() => workspaceToDelete && deleteAction(workspaceToDelete)}
463+
/>
442464
<Pagination
443465
itemCount={333}
444466
widgetId="bottom-example"
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import React, { useState, useEffect } from 'react';
2+
import {
3+
Modal,
4+
ModalBody,
5+
ModalFooter,
6+
ModalHeader,
7+
ModalVariant,
8+
Button,
9+
TextInput,
10+
Stack,
11+
StackItem,
12+
FlexItem,
13+
HelperText,
14+
HelperTextItem,
15+
} from '@patternfly/react-core';
16+
import { default as ExclamationCircleIcon } from '@patternfly/react-icons/dist/esm/icons/exclamation-circle-icon';
17+
18+
interface DeleteModalProps {
19+
isOpen: boolean;
20+
resourceName: string;
21+
namespace: string;
22+
onClose: () => void;
23+
onDelete: (resourceName: string) => void;
24+
title: string;
25+
}
26+
27+
const DeleteModal: React.FC<DeleteModalProps> = ({
28+
isOpen,
29+
resourceName,
30+
namespace,
31+
title,
32+
onClose,
33+
onDelete,
34+
}) => {
35+
const [inputValue, setInputValue] = useState('');
36+
37+
useEffect(() => {
38+
if (!isOpen) {
39+
setInputValue('');
40+
}
41+
}, [isOpen]);
42+
43+
const handleDelete = () => {
44+
if (inputValue === resourceName) {
45+
onDelete(resourceName);
46+
onClose();
47+
} else {
48+
alert('Resource name does not match.');
49+
}
50+
};
51+
52+
const handleInputChange = (event: React.FormEvent<HTMLInputElement>, value: string) => {
53+
setInputValue(value);
54+
};
55+
56+
const showWarning = inputValue !== '' && inputValue !== resourceName;
57+
58+
return (
59+
<Modal
60+
data-testid="delete-modal"
61+
variant={ModalVariant.small}
62+
title="Confirm Deletion"
63+
isOpen={isOpen}
64+
onClose={onClose}
65+
>
66+
<ModalHeader title={title} titleIconVariant="warning" />
67+
<ModalBody>
68+
<Stack hasGutter>
69+
<StackItem>
70+
<FlexItem>
71+
Are you sure you want to delete <strong>{resourceName}</strong> in namespace{' '}
72+
<strong>{namespace}</strong>?
73+
<br />
74+
<br />
75+
Please type the resource name to confirm:
76+
</FlexItem>
77+
<TextInput
78+
value={inputValue}
79+
type="text"
80+
onChange={handleInputChange}
81+
aria-label="Resource name confirmation"
82+
validated={showWarning ? 'error' : 'default'}
83+
data-testid="delete-modal-input"
84+
/>
85+
{showWarning && (
86+
<HelperText data-testid="delete-modal-helper-text">
87+
<HelperTextItem icon={<ExclamationCircleIcon />} variant="error">
88+
The name doesn&apos;t match. Please enter exactly: {resourceName}
89+
</HelperTextItem>
90+
</HelperText>
91+
)}
92+
</StackItem>
93+
</Stack>
94+
</ModalBody>
95+
<ModalFooter>
96+
<div style={{ marginTop: '1rem' }}>
97+
<Button
98+
onClick={handleDelete}
99+
variant="danger"
100+
isDisabled={inputValue !== resourceName}
101+
aria-disabled={inputValue !== resourceName}
102+
>
103+
Delete
104+
</Button>
105+
<Button onClick={onClose} variant="link" style={{ marginLeft: '1rem' }}>
106+
Cancel
107+
</Button>
108+
</div>
109+
</ModalFooter>
110+
</Modal>
111+
);
112+
};
113+
114+
export default DeleteModal;

0 commit comments

Comments
 (0)