From 026525bc12f671adf99aaa2531775672d3791552 Mon Sep 17 00:00:00 2001 From: liberty-rising Date: Mon, 5 Feb 2024 20:09:52 +0100 Subject: [PATCH] add ability to delete data profiles --- backend/database/data_profile_manager.py | 8 + backend/routes/data_profile_routes.py | 25 ++- frontend/package-lock.json | 14 ++ frontend/package.json | 1 + .../src/pages/upload/DataProfileSelector.jsx | 42 ++++- .../pages/upload/DeleteDataProfileWindow.jsx | 149 ++++++++++++++++++ frontend/src/pages/upload/UploadPage.jsx | 39 +++++ 7 files changed, 269 insertions(+), 9 deletions(-) create mode 100644 frontend/src/pages/upload/DeleteDataProfileWindow.jsx diff --git a/backend/database/data_profile_manager.py b/backend/database/data_profile_manager.py index 9034f36..01b20fa 100644 --- a/backend/database/data_profile_manager.py +++ b/backend/database/data_profile_manager.py @@ -41,3 +41,11 @@ def get_dataprofile_by_id(self, data_profile_id: int): .filter(DataProfile.id == data_profile_id) .first() ) + + def delete_dataprofile(self, data_profile_id: int): + """Delete a DataProfile.""" + self.session.query(DataProfile).filter( + DataProfile.id == data_profile_id + ).delete() + self.session.commit() + return True diff --git a/backend/routes/data_profile_routes.py b/backend/routes/data_profile_routes.py index 1dd59d6..6c3331e 100644 --- a/backend/routes/data_profile_routes.py +++ b/backend/routes/data_profile_routes.py @@ -76,7 +76,6 @@ async def save_data_profile( column_names_and_types=request.column_names_and_types, ) - print(table_name) # Create the data profile new_data_profile = DataProfile( name=request.name, @@ -172,8 +171,6 @@ async def generate_suggested_column_types( column_names, request.data ) - print(suggested_column_types) - return suggested_column_types @@ -188,10 +185,8 @@ async def get_data_profile_table_column_names( ) if data_profile is None: raise HTTPException(status_code=404, detail="Data Profile not found") - print("data_profile.table_name", data_profile.table_name) table_manager = TableManager(session) column_names = table_manager.get_table_column_names(data_profile.table_name) - print("column_names", column_names) return column_names @@ -277,3 +272,23 @@ async def save_extracted_data( space_manager.upload_files() return {"message": "Extracted data saved successfully"} + + +@data_profile_router.delete("/data-profiles/{data_profile_name}/") +async def delete_data_profile( + data_profile_name: str, current_user: User = Depends(get_current_user) +): + with DatabaseManager() as session: + data_profile_manager = DataProfileManager(session) + data_profile = data_profile_manager.get_dataprofile_by_name_and_org( + data_profile_name, current_user.organization_id + ) + if data_profile is None: + raise HTTPException(status_code=404, detail="Data Profile not found") + + if data_profile.table_name: + table_manager = TableManager(session) + table_manager.drop_table(data_profile.table_name) + + data_profile_manager.delete_dataprofile(data_profile.id) + return {"detail": "Data Profile deleted successfully"} diff --git a/frontend/package-lock.json b/frontend/package-lock.json index a4a237d..b5f89b6 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -16,6 +16,7 @@ "@nivo/line": "^0.84.0", "@nivo/pie": "^0.84.0", "axios": "^1.6.2", + "copy-to-clipboard": "^3.3.3", "date-fns": "^2.30.0", "js-cookie": "^3.0.5", "primereact": "^10.4.0", @@ -2393,6 +2394,14 @@ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true }, + "node_modules/copy-to-clipboard": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz", + "integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==", + "dependencies": { + "toggle-selection": "^1.0.6" + } + }, "node_modules/cosmiconfig": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", @@ -4990,6 +4999,11 @@ "node": ">=4" } }, + "node_modules/toggle-selection": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", + "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index e31daa3..5a8a9a3 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -18,6 +18,7 @@ "@nivo/line": "^0.84.0", "@nivo/pie": "^0.84.0", "axios": "^1.6.2", + "copy-to-clipboard": "^3.3.3", "date-fns": "^2.30.0", "js-cookie": "^3.0.5", "primereact": "^10.4.0", diff --git a/frontend/src/pages/upload/DataProfileSelector.jsx b/frontend/src/pages/upload/DataProfileSelector.jsx index 009256d..ec3df4d 100644 --- a/frontend/src/pages/upload/DataProfileSelector.jsx +++ b/frontend/src/pages/upload/DataProfileSelector.jsx @@ -1,10 +1,32 @@ -import React from "react"; -import { FormControl, InputLabel, MenuItem, Select } from "@mui/material"; +import React, { useState } from "react"; +import { + FormControl, + IconButton, + InputLabel, + ListItemText, + MenuItem, + Select, +} from "@mui/material"; +import Delete from "@mui/icons-material/Delete"; -const DataProfileSelector = ({ dataProfiles, dataProfile, setDataProfile }) => { +const DataProfileSelector = ({ + dataProfiles, + dataProfile, + setDataProfile, + handleOpenDeleteDialog, +}) => { + const [isOpen, setIsOpen] = useState(false); const isValidDataProfile = dataProfiles.includes(dataProfile); const safeDataProfile = isValidDataProfile ? dataProfile : ""; + const handleOpen = () => { + setIsOpen(true); + }; + + const handleClose = () => { + setIsOpen(false); + }; + return ( Choose a data profile @@ -13,10 +35,22 @@ const DataProfileSelector = ({ dataProfiles, dataProfile, setDataProfile }) => { value={safeDataProfile} label="Choose a data profile" onChange={(e) => setDataProfile(e.target.value)} + onOpen={handleOpen} + onClose={handleClose} + renderValue={(selected) => } > {dataProfiles.map((profile, index) => ( - {profile} + + {isOpen && ( + handleOpenDeleteDialog(profile)} + > + + + )} ))} diff --git a/frontend/src/pages/upload/DeleteDataProfileWindow.jsx b/frontend/src/pages/upload/DeleteDataProfileWindow.jsx new file mode 100644 index 0000000..3fd70e8 --- /dev/null +++ b/frontend/src/pages/upload/DeleteDataProfileWindow.jsx @@ -0,0 +1,149 @@ +import React, { useState } from "react"; +import { + Box, + Button, + Dialog, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle, + IconButton, + TextField, + Typography, +} from "@mui/material"; +import CheckIcon from "@mui/icons-material/Check"; +import CloseIcon from "@mui/icons-material/Close"; +import ContentCopyIcon from "@mui/icons-material/ContentCopy"; +import copy from "copy-to-clipboard"; + +function DeleteDataProfileWindow({ + open, + onClose, + dataProfileToDelete, + onDelete, +}) { + const [isCopied, setIsCopied] = useState(false); + const [isFilled, setIsFilled] = useState(false); + const [confirmDataProfile, setConfirmDataProfile] = useState(""); + + const handleDelete = () => { + if (confirmDataProfile === dataProfileToDelete) { + setConfirmDataProfile(""); + setIsFilled(false); + onDelete(); + } else { + alert("The entered name does not match the data profile to be deleted."); + } + }; + + const handleCopy = () => { + copy(dataProfileToDelete); + setIsCopied(true); + setTimeout(() => { + setIsCopied(false); + }, 2000); // change back to original icon after 2 seconds + }; + + const handleClose = () => { + setConfirmDataProfile(""); + setIsFilled(false); + onClose(); + }; + + return ( + + + + + Delete Data Profile + + + Deleting a data profile is permanent and cannot be undone. +

+ Enter the data profile name below to confirm. +
+ + + {dataProfileToDelete} + + {isCopied ? : } + + + + + Data profile name * + + + { + setConfirmDataProfile(e.target.value); + setIsFilled(e.target.value !== ""); + }} + InputLabelProps={{ shrink: false }} + /> +
+ + + + +
+ ); +} + +export default DeleteDataProfileWindow; diff --git a/frontend/src/pages/upload/UploadPage.jsx b/frontend/src/pages/upload/UploadPage.jsx index f780254..9a7d12b 100644 --- a/frontend/src/pages/upload/UploadPage.jsx +++ b/frontend/src/pages/upload/UploadPage.jsx @@ -9,6 +9,7 @@ import { import axios from "axios"; import AlertSnackbar from "./AlertSnackbar"; import CreateDataProfileWindow from "./CreateDataProfileWindow"; +import DeleteDataProfileWindow from "./DeleteDataProfileWindow"; import DataProfileSelector from "./DataProfileSelector"; import FileUploader from "./FileUploader"; import PreviewTable from "./PreviewTable"; @@ -23,6 +24,8 @@ function UploadPage() { message: "", severity: "info", }); + const [showDeleteDataProfile, setShowDeleteDataProfile] = useState(false); + const [dataProfileToDelete, setDataProfileToDelete] = useState(null); const [showCreateDataProfile, setShowCreateDataProfile] = useState(false); const [columnNames, setColumnNames] = useState([]); const [previewData, setPreviewData] = useState(null); @@ -50,6 +53,35 @@ function UploadPage() { } }, [dataProfile]); + const handleOpenDeleteDialog = (dataProfile) => { + setDataProfileToDelete(dataProfile); + setShowDeleteDataProfile(true); + }; + + const handleCloseDeleteDialog = () => { + setShowDeleteDataProfile(false); + setDataProfileToDelete(null); + }; + + const handleDeleteDataProfile = async () => { + axios + .delete(`${API_URL}data-profiles/${dataProfileToDelete}/`) + .then(() => { + setDataProfiles((prevDataProfiles) => + prevDataProfiles.filter((profile) => profile !== dataProfileToDelete), + ); + if (dataProfileToDelete === dataProfile) { + setDataProfile(null); + } + setShowDeleteDataProfile(false); + }) + .catch((error) => { + console.error("Error deleting data profile:", error); + // handle the error as necessary + }); + handleCloseDeleteDialog(); + }; + const handleCreateDataProfile = ( name, extractInstructions, @@ -161,6 +193,13 @@ function UploadPage() { dataProfiles={dataProfiles} dataProfile={dataProfile} setDataProfile={setDataProfile} + handleOpenDeleteDialog={handleOpenDeleteDialog} + /> +