From 09970a3b71b1faee1d40667ca5cbc22b7f4e0301 Mon Sep 17 00:00:00 2001 From: Dawson Toth Date: Tue, 15 Jul 2025 11:15:19 -0400 Subject: [PATCH 1/2] Extract and make cluster deletion confirmation generic --- .../ConfirmDeletionModal.tsx} | 26 +++++++++--------- src/features/clusters/ClustersList.tsx | 27 ++++++++++--------- 2 files changed, 28 insertions(+), 25 deletions(-) rename src/{features/clusters/modals/DeleteClusterConfirmationModal/index.tsx => components/ConfirmDeletionModal.tsx} (68%) diff --git a/src/features/clusters/modals/DeleteClusterConfirmationModal/index.tsx b/src/components/ConfirmDeletionModal.tsx similarity index 68% rename from src/features/clusters/modals/DeleteClusterConfirmationModal/index.tsx rename to src/components/ConfirmDeletionModal.tsx index e09097526..32f990269 100644 --- a/src/features/clusters/modals/DeleteClusterConfirmationModal/index.tsx +++ b/src/components/ConfirmDeletionModal.tsx @@ -9,24 +9,26 @@ import { } from '@/components/ui/dialog'; import { ArrowLeft, Trash, TriangleAlert } from 'lucide-react'; -export function DeleteClusterConfirmationModal({ - clusterInfo, +export function ConfirmDeletionModal({ + typeOfThingBeingDeleted, + nameOfThingBeingDeleted, isModalOpen, - isDeletingClusterPending, - handleDeleteCluster, setIsModalOpen, + deletionConfirmed, + deletionPending, }: { - clusterInfo: { id: string; name: string }; + typeOfThingBeingDeleted: string; + nameOfThingBeingDeleted: string; isModalOpen: boolean; - isDeletingClusterPending: boolean; - handleDeleteCluster: () => void; setIsModalOpen: (isOpen: boolean) => void; + deletionConfirmed: () => void; + deletionPending: boolean; }) { return ( - Are you sure you want to delete this cluster? + Are you sure you want to delete this {typeOfThingBeingDeleted}? This action cannot be undone.
@@ -34,8 +36,8 @@ export function DeleteClusterConfirmationModal({ Warning

- By deleting cluster {clusterInfo.name} you will lose the data stored - in it permanently. + By deleting {typeOfThingBeingDeleted} {nameOfThingBeingDeleted} + you will lose the data stored in it permanently.

@@ -46,8 +48,8 @@ export function DeleteClusterConfirmationModal({ diff --git a/src/features/clusters/ClustersList.tsx b/src/features/clusters/ClustersList.tsx index 078b747ea..56804ef34 100644 --- a/src/features/clusters/ClustersList.tsx +++ b/src/features/clusters/ClustersList.tsx @@ -1,17 +1,17 @@ -import { getRouteApi } from '@tanstack/react-router'; -import { ClusterCard } from '@/features/organization/components/ClusterCard'; -import { Plus } from 'lucide-react'; +import { ConfirmDeletionModal } from '@/components/ConfirmDeletionModal'; import { Button } from '@/components/ui/button'; -import { getOrganizationQueryOptions } from '@/features/organization/queries/getOrganizationQuery'; import { NewClusterModal } from '@/features/clusters/modals/NewClusterModal'; +import { ClusterCard } from '@/features/organization/components/ClusterCard'; +import { getOrganizationQueryOptions } from '@/features/organization/queries/getOrganizationQuery'; +import { sortByName } from '@/lib/arrays/sort/byName'; +import { groupBy } from '@/lib/group-by'; +import { queryKeys } from '@/react-query/constants'; import { useQueryClient, useSuspenseQuery } from '@tanstack/react-query'; +import { getRouteApi } from '@tanstack/react-router'; +import { Plus } from 'lucide-react'; import { useCallback, useMemo, useState } from 'react'; -import { DeleteClusterConfirmationModal } from './modals/DeleteClusterConfirmationModal'; -import { useDeleteClusterMutation } from './mutations/deleteCluster'; import { toast } from 'sonner'; -import { queryKeys } from '@/react-query/constants'; -import { groupBy } from '@/lib/group-by'; -import { sortByName } from '@/lib/arrays/sort/byName'; +import { useDeleteClusterMutation } from './mutations/deleteCluster'; const route = getRouteApi(''); @@ -139,12 +139,13 @@ export function ClustersList() { isModalOpen={isNewClusterModalOpen} setIsModalOpen={() => setIsNewClusterModalOpen(false)} /> - handleDeleteCluster(deleteClusterInfo)} setIsModalOpen={() => setIsDeleteClusterModalOpen(false)} + deletionConfirmed={() => handleDeleteCluster(deleteClusterInfo)} + deletionPending={isDeletingClusterPending} /> ); From 354fe31384a327c4ff801ec02deaea0fa7d20d69 Mon Sep 17 00:00:00 2001 From: Dawson Toth Date: Tue, 15 Jul 2025 11:39:25 -0400 Subject: [PATCH 2/2] Confirm before deleting databases and tables https://harperdb.atlassian.net/browse/STUDIO-176 --- .../browse/components/BrowseSidebar.tsx | 125 +++++++++++------- 1 file changed, 77 insertions(+), 48 deletions(-) diff --git a/src/features/instance/browse/components/BrowseSidebar.tsx b/src/features/instance/browse/components/BrowseSidebar.tsx index c0d635df0..c652e85c8 100644 --- a/src/features/instance/browse/components/BrowseSidebar.tsx +++ b/src/features/instance/browse/components/BrowseSidebar.tsx @@ -1,21 +1,22 @@ -import { useState } from 'react'; -import { getRouteApi, useRouter } from '@tanstack/react-router'; -import { useQueryClient } from '@tanstack/react-query'; -import { useForm } from 'react-hook-form'; -import { zodResolver } from '@hookform/resolvers/zod'; -import { z } from 'zod'; -import { CreateNewTableModal } from '@/features/instance/browse/modals/CreateNewTableModal'; -import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { ConfirmDeletionModal } from '@/components/ConfirmDeletionModal'; +import { Button } from '@/components/ui/button'; +import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; import { ScrollArea } from '@/components/ui/scrollArea'; +import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; -import { Button } from '@/components/ui/button'; -import { ArrowRight, Check, Minus, Plus, Trash } from 'lucide-react'; +import { CreateNewTableModal } from '@/features/instance/browse/modals/CreateNewTableModal'; import { useCreateDatabaseSubmitMutation } from '@/features/instance/operations/mutations/createDatabase'; -import { Input } from '@/components/ui/input'; -import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage } from '@/components/ui/form'; -import { toast } from 'sonner'; import { useDeleteDatabaseMutation } from '@/features/instance/operations/mutations/deleteDatabase'; -import { DeleteTableData, useDeleteTableMutation } from '@/features/instance/operations/mutations/deleteTable'; +import { useDeleteTableMutation } from '@/features/instance/operations/mutations/deleteTable'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { useQueryClient } from '@tanstack/react-query'; +import { getRouteApi, useRouter } from '@tanstack/react-router'; +import { ArrowRight, Check, Minus, Plus, Trash } from 'lucide-react'; +import { useCallback, useState } from 'react'; +import { useForm } from 'react-hook-form'; +import { toast } from 'sonner'; +import { z } from 'zod'; type BrowseSidebarProps = { databases: string[]; @@ -40,6 +41,10 @@ export function BrowseSidebar({ databases, onSelectDatabase, tables, onSelectTab const router = useRouter(); const queryClient = useQueryClient(); const { instanceId, schemaName, tableName } = route.useParams(); + const [typeOfThingBeingDeleted, setTypeOfThingBeingDeleted] = useState(""); + const [nameOfThingBeingDeleted, setNameOfThingBeingDeleted] = useState(""); + const [deletionTarget, setDeletionTarget] = useState<{ databaseName?: string; tableName?: string; }>({}); + const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [isCreatingDatabase, setIsCreatingDatabase] = useState(false); @@ -51,8 +56,8 @@ export function BrowseSidebar({ databases, onSelectDatabase, tables, onSelectTab }); const { mutate: createNewDatabase } = useCreateDatabaseSubmitMutation(); - const { mutate: deleteDatabase } = useDeleteDatabaseMutation(); - const { mutate: deleteTable } = useDeleteTableMutation(); + const { mutate: deleteDatabase, isPending: isDeletingDatabase } = useDeleteDatabaseMutation(); + const { mutate: deleteTable, isPending: isDeletingTable } = useDeleteTableMutation(); const submitNewDatabase = (formData: z.infer) => { createNewDatabase(formData, { @@ -67,34 +72,41 @@ export function BrowseSidebar({ databases, onSelectDatabase, tables, onSelectTab }); }; - const deleteSelectedDatabase = (databaseName: string) => { - deleteDatabase(databaseName, { - onSuccess: async () => { - await queryClient.invalidateQueries({ queryKey: [instanceId, 'describe_all'], refetchType: 'all' }); - await router.invalidate(); - toast.success(`Database ${databaseName} deleted successfully`); - onSelectDatabase(undefined); - }, - }); - }; - - const deleteSelectedTable = (data: DeleteTableData) => { - deleteTable(data, { - onSuccess: async () => { - await queryClient.invalidateQueries({ queryKey: [instanceId, 'describe_all'], refetchType: 'all' }); - await router.invalidate(); - toast.success(`Table ${data.tableName} deleted successfully`); - if (data.tableName === tableName) { - onSelectTable(undefined); - } - }, - }); - }; - - const handleDeleteTable = (tableName: string) => { - if (!tableName) return; - deleteSelectedTable({ databaseName: schemaName, tableName }); - }; + const onDeletionConfirmed = useCallback(() => { + const targetDatabaseName = deletionTarget.databaseName; + const targetTableName = deletionTarget.tableName; + if (targetDatabaseName) { + if (targetTableName) { + deleteTable({ databaseName: targetDatabaseName, tableName: targetTableName }, { + onSuccess: async () => { + setIsDeleteModalOpen(false); + await queryClient.invalidateQueries({ + queryKey: [instanceId, 'describe_all'], + refetchType: 'all', + }); + await router.invalidate(); + toast.success(`Table ${targetTableName} deleted successfully`); + if (targetTableName === tableName) { + onSelectTable(undefined); + } + }, + }); + } else { + deleteDatabase(targetDatabaseName, { + onSuccess: async () => { + setIsDeleteModalOpen(false); + await queryClient.invalidateQueries({ + queryKey: [instanceId, 'describe_all'], + refetchType: 'all', + }); + await router.invalidate(); + toast.success(`Database ${targetDatabaseName} deleted successfully`); + onSelectDatabase(undefined); + }, + }); + } + } + }, [deleteDatabase, deleteTable, deletionTarget, instanceId, onSelectDatabase, onSelectTable, queryClient, router, tableName]); return (
@@ -135,8 +147,12 @@ export function BrowseSidebar({ databases, onSelectDatabase, tables, onSelectTab variant="destructiveOutline" disabled={!schemaName} onClick={() => { - if (!schemaName) return; - deleteSelectedDatabase(schemaName); + if (schemaName) { + setTypeOfThingBeingDeleted("database"); + setNameOfThingBeingDeleted(schemaName); + setDeletionTarget({ databaseName: schemaName }); + setIsDeleteModalOpen(true); + } }} > @@ -191,9 +207,14 @@ export function BrowseSidebar({ databases, onSelectDatabase, tables, onSelectTab {(tables ?? []).map((table) => (
  • ); }