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>
This commit is contained in:
parent
9479c7bebf
commit
8ceb835f1e
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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';
|
||||
|
||||
|
@ -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) => {
|
||||
|
@ -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 =>
|
||||
[
|
||||
|
@ -301,7 +309,7 @@ export const Workspaces: React.FunctionComponent = () => {
|
|||
},
|
||||
{
|
||||
title: 'Delete',
|
||||
onClick: () => deleteAction(workspace),
|
||||
onClick: () => handleDeleteClick(workspace),
|
||||
},
|
||||
{
|
||||
isSeparator: true,
|
||||
|
@ -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
|
||||
|
@ -360,7 +368,7 @@ export const Workspaces: React.FunctionComponent = () => {
|
|||
workspace={selectedWorkspace}
|
||||
onCloseClick={() => selectWorkspace(null)}
|
||||
onEditClick={() => editAction(selectedWorkspace)}
|
||||
onDeleteClick={() => deleteAction(selectedWorkspace)}
|
||||
onDeleteClick={() => handleDeleteClick(selectedWorkspace)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
@ -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
|
||||
|
@ -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) && (
|
||||
|
@ -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"
|
||||
|
|
|
@ -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'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;
|
Loading…
Reference in New Issue