Skip to content

Commit

Permalink
feat(ws): implement delete workspace action with a confirmation popup (
Browse files Browse the repository at this point in the history
…#178)

* feat(ws): Notebooks 2.0 // Frontend // Delete workspace

Signed-off-by: yelias <[email protected]>

* Rename deleteModal.tsx

Signed-off-by: yelias <[email protected]>

---------

Signed-off-by: yelias <[email protected]>
Co-authored-by: yelias <[email protected]>
  • Loading branch information
YosiElias and yelias authored Jan 23, 2025
1 parent 9479c7b commit 8ceb835
Show file tree
Hide file tree
Showing 3 changed files with 206 additions and 5 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { mockNamespaces } from '~/__mocks__/mockNamespaces';
import { mockBFFResponse } from '~/__mocks__/utils';

describe('Workspaces Component', () => {
beforeEach(() => {
// Mock the namespaces API response
cy.intercept('GET', '/api/v1/namespaces', {
body: mockBFFResponse(mockNamespaces),
}).as('getNamespaces');
cy.visit('/');
cy.wait('@getNamespaces');
});

function openDeleteModal() {
cy.findAllByTestId('table-body').first().findByTestId('action-column').click();
cy.findByTestId('action-delete').click();
cy.findByTestId('delete-modal-input').should('have.value', '');
}

it('should test the close mechanisms of the delete modal', () => {
const closeModalActions = [
() => cy.get('button').contains('Cancel').click(),
() => cy.get('[aria-label="Close"]').click(),
];

closeModalActions.forEach((closeAction) => {
openDeleteModal();
cy.findByTestId('delete-modal-input').type('Some Text');
cy.findByTestId('delete-modal').should('be.visible');
closeAction();
cy.findByTestId('delete-modal').should('not.exist');
});

// Check that clicking outside the modal does not close it
openDeleteModal();
cy.findByTestId('delete-modal').should('be.visible');
cy.get('body').click(0, 0);
cy.findByTestId('delete-modal').should('be.visible');
});

it('should verify the delete modal verification mechanism', () => {
openDeleteModal();
cy.findByTestId('delete-modal').within(() => {
cy.get('strong')
.first()
.invoke('text')
.then((resourceName) => {
// Type incorrect resource name
cy.findByTestId('delete-modal-input').type('Wrong Name');
cy.findByTestId('delete-modal-input').should('have.value', 'Wrong Name');
cy.findByTestId('delete-modal-helper-text').should('be.visible');
cy.get('button').contains('Delete').should('have.css', 'pointer-events', 'none');

// Clear and type correct resource name
cy.findByTestId('delete-modal-input').clear();
cy.findByTestId('delete-modal-input').type(resourceName);
cy.findByTestId('delete-modal-input').should('have.value', resourceName);
cy.findByTestId('delete-modal-helper-text').should('not.be.exist');
cy.get('button').contains('Delete').should('not.have.css', 'pointer-events', 'none');
cy.get('button').contains('Delete').click();
cy.findByTestId('delete-modal').should('not.exist');
});
});
});
});
32 changes: 27 additions & 5 deletions workspaces/frontend/src/app/pages/Workspaces/Workspaces.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { useState } from 'react';
import { Workspace, WorkspacesColumnNames, WorkspaceState } from '~/shared/types';
import { WorkspaceDetails } from '~/app/pages/Workspaces/Details/WorkspaceDetails';
import { ExpandedWorkspaceRow } from '~/app/pages/Workspaces/ExpandedWorkspaceRow';
import DeleteModal from '~/shared/components/DeleteModal';
import Filter, { FilteredColumn } from 'shared/components/Filter';
import { formatRam } from 'shared/utilities/WorkspaceResources';

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

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

const handleDeleteClick = React.useCallback((workspace: Workspace) => {
const buttonElement = document.activeElement as HTMLElement;
buttonElement.blur(); // Remove focus from the currently focused button
setWorkspaceToDelete(workspace); // Open the modal and set workspace to delete
}, []);

const defaultActions = React.useCallback(
(workspace: Workspace): IActions =>
[
Expand All @@ -301,7 +309,7 @@ export const Workspaces: React.FunctionComponent = () => {
},
{
title: 'Delete',
onClick: () => deleteAction(workspace),
onClick: () => handleDeleteClick(workspace),
},
{
isSeparator: true,
Expand All @@ -315,7 +323,7 @@ export const Workspaces: React.FunctionComponent = () => {
onClick: () => stopAction(workspace),
},
] as IActions,
[selectWorkspace, editAction, deleteAction, startRestartAction, stopAction],
[selectWorkspace, editAction, handleDeleteClick, startRestartAction, stopAction],
);

// States
Expand Down Expand Up @@ -360,7 +368,7 @@ export const Workspaces: React.FunctionComponent = () => {
workspace={selectedWorkspace}
onCloseClick={() => selectWorkspace(null)}
onEditClick={() => editAction(selectedWorkspace)}
onDeleteClick={() => deleteAction(selectedWorkspace)}
onDeleteClick={() => handleDeleteClick(selectedWorkspace)}
/>
)}
</>
Expand Down Expand Up @@ -399,6 +407,7 @@ export const Workspaces: React.FunctionComponent = () => {
id="workspaces-table-content"
key={rowIndex}
isExpanded={isWorkspaceExpanded(workspace)}
data-testid="table-body"
>
<Tr id={`workspaces-table-row-${rowIndex + 1}`}>
<Td
Expand Down Expand Up @@ -429,8 +438,13 @@ export const Workspaces: React.FunctionComponent = () => {
1 hour ago
</Timestamp>
</Td>
<Td isActionCell>
<ActionsColumn items={defaultActions(workspace)} />
<Td isActionCell data-testid="action-column">
<ActionsColumn
items={defaultActions(workspace).map((action) => ({
...action,
'data-testid': `action-${typeof action.title === 'string' ? action.title.toLowerCase() : ''}`,
}))}
/>
</Td>
</Tr>
{isWorkspaceExpanded(workspace) && (
Expand All @@ -439,6 +453,14 @@ export const Workspaces: React.FunctionComponent = () => {
</Tbody>
))}
</Table>
<DeleteModal
isOpen={workspaceToDelete != null}
resourceName={workspaceToDelete?.name || ''}
namespace={workspaceToDelete?.namespace || ''}
title="Delete Workspace?"
onClose={() => setWorkspaceToDelete(null)}
onDelete={() => workspaceToDelete && deleteAction(workspaceToDelete)}
/>
<Pagination
itemCount={333}
widgetId="bottom-example"
Expand Down
114 changes: 114 additions & 0 deletions workspaces/frontend/src/shared/components/DeleteModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import React, { useState, useEffect } from 'react';
import {
Modal,
ModalBody,
ModalFooter,
ModalHeader,
ModalVariant,
Button,
TextInput,
Stack,
StackItem,
FlexItem,
HelperText,
HelperTextItem,
} from '@patternfly/react-core';
import { default as ExclamationCircleIcon } from '@patternfly/react-icons/dist/esm/icons/exclamation-circle-icon';

interface DeleteModalProps {
isOpen: boolean;
resourceName: string;
namespace: string;
onClose: () => void;
onDelete: (resourceName: string) => void;
title: string;
}

const DeleteModal: React.FC<DeleteModalProps> = ({
isOpen,
resourceName,
namespace,
title,
onClose,
onDelete,
}) => {
const [inputValue, setInputValue] = useState('');

useEffect(() => {
if (!isOpen) {
setInputValue('');
}
}, [isOpen]);

const handleDelete = () => {
if (inputValue === resourceName) {
onDelete(resourceName);
onClose();
} else {
alert('Resource name does not match.');
}
};

const handleInputChange = (event: React.FormEvent<HTMLInputElement>, value: string) => {
setInputValue(value);
};

const showWarning = inputValue !== '' && inputValue !== resourceName;

return (
<Modal
data-testid="delete-modal"
variant={ModalVariant.small}
title="Confirm Deletion"
isOpen={isOpen}
onClose={onClose}
>
<ModalHeader title={title} titleIconVariant="warning" />
<ModalBody>
<Stack hasGutter>
<StackItem>
<FlexItem>
Are you sure you want to delete <strong>{resourceName}</strong> in namespace{' '}
<strong>{namespace}</strong>?
<br />
<br />
Please type the resource name to confirm:
</FlexItem>
<TextInput
value={inputValue}
type="text"
onChange={handleInputChange}
aria-label="Resource name confirmation"
validated={showWarning ? 'error' : 'default'}
data-testid="delete-modal-input"
/>
{showWarning && (
<HelperText data-testid="delete-modal-helper-text">
<HelperTextItem icon={<ExclamationCircleIcon />} variant="error">
The name doesn&apos;t match. Please enter exactly: {resourceName}
</HelperTextItem>
</HelperText>
)}
</StackItem>
</Stack>
</ModalBody>
<ModalFooter>
<div style={{ marginTop: '1rem' }}>
<Button
onClick={handleDelete}
variant="danger"
isDisabled={inputValue !== resourceName}
aria-disabled={inputValue !== resourceName}
>
Delete
</Button>
<Button onClick={onClose} variant="link" style={{ marginLeft: '1rem' }}>
Cancel
</Button>
</div>
</ModalFooter>
</Modal>
);
};

export default DeleteModal;

0 comments on commit 8ceb835

Please sign in to comment.