From 353a156c44cbd71341f2939892d22686187b6c64 Mon Sep 17 00:00:00 2001 From: Emily Samoylov <93456589+emilys314@users.noreply.github.com> Date: Fri, 13 Sep 2024 14:31:06 -0400 Subject: [PATCH] Add delete modal for project connections (#3204) --- .../cypress/cypress/pages/connections.ts | 15 +++++ .../tests/mocked/projects/connections.cy.ts | 33 +++++++++-- .../pages/projects/ProjectDetailsContext.tsx | 3 + .../connections/ConnectionsDeleteModal.tsx | 48 +++++++++++++++ .../detail/connections/ConnectionsList.tsx | 8 ++- .../detail/connections/ConnectionsTable.tsx | 59 ++++++++++++++----- .../__tests__/ConnectionsTable.spec.tsx | 2 + 7 files changed, 146 insertions(+), 22 deletions(-) create mode 100644 frontend/src/__tests__/cypress/cypress/pages/connections.ts create mode 100644 frontend/src/pages/projects/screens/detail/connections/ConnectionsDeleteModal.tsx diff --git a/frontend/src/__tests__/cypress/cypress/pages/connections.ts b/frontend/src/__tests__/cypress/cypress/pages/connections.ts new file mode 100644 index 0000000000..778a1dd4ef --- /dev/null +++ b/frontend/src/__tests__/cypress/cypress/pages/connections.ts @@ -0,0 +1,15 @@ +import { TableRow } from './components/table'; + +class ConnectionsPage { + findTable() { + return cy.findByTestId('connection-table'); + } + + getConnectionRow(name: string) { + return new TableRow(() => + this.findTable().findAllByTestId(`table-row-title`).contains(name).parents('tr'), + ); + } +} + +export const connectionsPage = new ConnectionsPage(); diff --git a/frontend/src/__tests__/cypress/cypress/tests/mocked/projects/connections.cy.ts b/frontend/src/__tests__/cypress/cypress/tests/mocked/projects/connections.cy.ts index 4d1ef4212b..0c6b0cd9f8 100644 --- a/frontend/src/__tests__/cypress/cypress/tests/mocked/projects/connections.cy.ts +++ b/frontend/src/__tests__/cypress/cypress/tests/mocked/projects/connections.cy.ts @@ -1,4 +1,5 @@ import { + mock200Status, mockDashboardConfig, mockK8sResourceList, mockProjectK8sResource, @@ -7,6 +8,8 @@ import { import { mockConnectionTypeConfigMap } from '~/__mocks__/mockConnectionType'; import { projectDetails } from '~/__tests__/cypress/cypress/pages/projects'; import { ProjectModel, SecretModel } from '~/__tests__/cypress/cypress/utils/models'; +import { connectionsPage } from '~/__tests__/cypress/cypress/pages/connections'; +import { deleteModal } from '~/__tests__/cypress/cypress/pages/components/DeleteModal'; const initIntercepts = (isEmpty = false) => { cy.interceptK8sList( @@ -51,9 +54,31 @@ describe('Connections', () => { initIntercepts(); projectDetails.visitSection('test-project', 'connections'); projectDetails.shouldBeEmptyState('Connections', 'connections', false); - cy.findByTestId('connection-table').findByText('test1').should('exist'); - cy.findByTestId('connection-table').findByText('s3').should('exist'); - cy.findByTestId('connection-table').findByText('test2').should('exist'); - cy.findByTestId('connection-table').findByText('postgres').should('exist'); + connectionsPage.findTable().findByText('test1').should('exist'); + connectionsPage.findTable().findByText('s3').should('exist'); + connectionsPage.findTable().findByText('test2').should('exist'); + connectionsPage.findTable().findByText('postgres').should('exist'); + }); + + it('Delete a connection', () => { + initIntercepts(); + cy.interceptK8s( + 'DELETE', + { + model: SecretModel, + ns: 'test-project', + name: 'test1', + }, + mock200Status({}), + ).as('deleteConnection'); + + projectDetails.visitSection('test-project', 'connections'); + + connectionsPage.getConnectionRow('test1').findKebabAction('Delete').click(); + deleteModal.findSubmitButton().should('be.disabled'); + deleteModal.findInput().fill('test1'); + deleteModal.findSubmitButton().should('be.enabled').click(); + + cy.wait('@deleteConnection'); }); }); diff --git a/frontend/src/pages/projects/ProjectDetailsContext.tsx b/frontend/src/pages/projects/ProjectDetailsContext.tsx index ef58a5d2bb..d275017f67 100644 --- a/frontend/src/pages/projects/ProjectDetailsContext.tsx +++ b/frontend/src/pages/projects/ProjectDetailsContext.tsx @@ -102,6 +102,7 @@ const ProjectDetailsContextProvider: React.FC = () => { const notebookRefresh = notebooks.refresh; const pvcRefresh = pvcs.refresh; const dataConnectionRefresh = dataConnections.refresh; + const connectionRefresh = connections.refresh; const servingRuntimeRefresh = servingRuntimes.refresh; const servingRuntimeTemplateOrderRefresh = servingRuntimeTemplateOrder.refresh; const servingRuntimeTemplateDisablementRefresh = servingRuntimeTemplateDisablement.refresh; @@ -112,6 +113,7 @@ const ProjectDetailsContextProvider: React.FC = () => { setTimeout(notebookRefresh, 2000); pvcRefresh(); dataConnectionRefresh(); + connectionRefresh(); servingRuntimeRefresh(); inferenceServiceRefresh(); projectSharingRefresh(); @@ -122,6 +124,7 @@ const ProjectDetailsContextProvider: React.FC = () => { notebookRefresh, pvcRefresh, dataConnectionRefresh, + connectionRefresh, servingRuntimeRefresh, servingRuntimeTemplateOrderRefresh, servingRuntimeTemplateDisablementRefresh, diff --git a/frontend/src/pages/projects/screens/detail/connections/ConnectionsDeleteModal.tsx b/frontend/src/pages/projects/screens/detail/connections/ConnectionsDeleteModal.tsx new file mode 100644 index 0000000000..939a11c768 --- /dev/null +++ b/frontend/src/pages/projects/screens/detail/connections/ConnectionsDeleteModal.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import { K8sStatus } from '@openshift/dynamic-plugin-sdk-utils'; +import { getDisplayNameFromK8sResource } from '~/concepts/k8s/utils'; +import { Connection } from '~/concepts/connectionTypes/types'; +import DeleteModal from '~/pages/projects/components/DeleteModal'; + +type Props = { + deleteConnection: Connection; + onClose: (deleted?: boolean) => void; + onDelete: () => Promise; +}; + +export const ConnectionsDeleteModal: React.FC = ({ + deleteConnection, + onClose, + onDelete, +}) => { + const [isDeleting, setIsDeleting] = React.useState(false); + const [error, setError] = React.useState(); + + return ( + { + setIsDeleting(true); + setError(undefined); + + onDelete() + .then(() => { + onClose(true); + }) + .catch((e) => { + setError(e); + setIsDeleting(false); + }); + }} + deleting={isDeleting} + error={error} + deleteName={getDisplayNameFromK8sResource(deleteConnection)} + > + The {getDisplayNameFromK8sResource(deleteConnection)} connection will be deleted, and + its dependent resources will stop working. + + ); +}; diff --git a/frontend/src/pages/projects/screens/detail/connections/ConnectionsList.tsx b/frontend/src/pages/projects/screens/detail/connections/ConnectionsList.tsx index b7dfb3d2f2..204c038b3e 100644 --- a/frontend/src/pages/projects/screens/detail/connections/ConnectionsList.tsx +++ b/frontend/src/pages/projects/screens/detail/connections/ConnectionsList.tsx @@ -16,7 +16,7 @@ const ConnectionsDescription = const ConnectionsList: React.FC = () => { const { - connections: { data: connections, loaded, error }, + connections: { data: connections, loaded, error, refresh: refreshConnections }, } = React.useContext(ProjectDetailsContext); const [connectionTypes, connectionTypesLoaded, connectionTypesError] = useWatchConnectionTypes(); @@ -60,7 +60,11 @@ const ConnectionsList: React.FC = () => { /> } > - + ); }; diff --git a/frontend/src/pages/projects/screens/detail/connections/ConnectionsTable.tsx b/frontend/src/pages/projects/screens/detail/connections/ConnectionsTable.tsx index b1fd940535..0fc56fec91 100644 --- a/frontend/src/pages/projects/screens/detail/connections/ConnectionsTable.tsx +++ b/frontend/src/pages/projects/screens/detail/connections/ConnectionsTable.tsx @@ -1,29 +1,56 @@ import * as React from 'react'; import { Connection, ConnectionTypeConfigMapObj } from '~/concepts/connectionTypes/types'; +import { deleteSecret } from '~/api'; import { Table } from '~/components/table'; import ConnectionsTableRow from './ConnectionsTableRow'; import { columns } from './connectionsTableColumns'; +import { ConnectionsDeleteModal } from './ConnectionsDeleteModal'; type ConnectionsTableProps = { connections: Connection[]; connectionTypes?: ConnectionTypeConfigMapObj[]; + refreshConnections: () => void; }; -const ConnectionsTable: React.FC = ({ connections, connectionTypes }) => ( - ( - undefined} - onDeleteConnection={() => undefined} +const ConnectionsTable: React.FC = ({ + connections, + connectionTypes, + refreshConnections, +}) => { + const [deleteConnection, setDeleteConnection] = React.useState(); + + return ( + <> +
( + undefined} + onDeleteConnection={() => setDeleteConnection(connection)} + /> + )} + isStriped /> - )} - isStriped - /> -); + {deleteConnection && ( + { + setDeleteConnection(undefined); + if (deleted) { + refreshConnections(); + } + }} + onDelete={() => + deleteSecret(deleteConnection.metadata.namespace, deleteConnection.metadata.name) + } + /> + )} + + ); +}; export default ConnectionsTable; diff --git a/frontend/src/pages/projects/screens/detail/connections/__tests__/ConnectionsTable.spec.tsx b/frontend/src/pages/projects/screens/detail/connections/__tests__/ConnectionsTable.spec.tsx index 76778fcffe..7cd8b359c5 100644 --- a/frontend/src/pages/projects/screens/detail/connections/__tests__/ConnectionsTable.spec.tsx +++ b/frontend/src/pages/projects/screens/detail/connections/__tests__/ConnectionsTable.spec.tsx @@ -9,6 +9,7 @@ describe('ConnectionsTable', () => { render( undefined} />, ); @@ -25,6 +26,7 @@ describe('ConnectionsTable', () => { connectionTypes={[ mockConnectionTypeConfigMapObj({ name: 's3', displayName: 'S3 Buckets' }), ]} + refreshConnections={() => undefined} />, );