This is an automated email from the ASF dual-hosted git repository.

hulk pushed a commit to branch unstable
in repository https://gitbox.apache.org/repos/asf/kvrocks-controller.git


The following commit(s) were added to refs/heads/unstable by this push:
     new 7fe3a2d  feat(webui): implemented shard master failover, added delete 
icon in list view (#337)
7fe3a2d is described below

commit 7fe3a2de5bb300cbe08efa938549b398e0cf1866
Author: Agnik Misra <[email protected]>
AuthorDate: Fri Aug 15 21:39:25 2025 +0530

    feat(webui): implemented shard master failover, added delete icon in list 
view (#337)
---
 webui/src/app/lib/api.ts                           | 106 +++---
 .../[namespace]/clusters/[cluster]/page.tsx        | 233 ++++++++++--
 .../clusters/[cluster]/shards/[shard]/page.tsx     |  92 ++++-
 webui/src/app/namespaces/[namespace]/page.tsx      | 129 ++++++-
 webui/src/app/ui/failoverDialog.tsx                | 419 +++++++++++++++++++++
 webui/src/app/ui/formCreation.tsx                  | 148 ++++++--
 6 files changed, 1019 insertions(+), 108 deletions(-)

diff --git a/webui/src/app/lib/api.ts b/webui/src/app/lib/api.ts
index 7351cbe..03a0f0e 100644
--- a/webui/src/app/lib/api.ts
+++ b/webui/src/app/lib/api.ts
@@ -28,6 +28,23 @@ export interface Cluster {
     shards: {};
 }
 
+// Helper methods to handle common response patterns
+function handleCreateResponse(responseData: any): string {
+    if (responseData?.data != undefined) {
+        return "";
+    } else {
+        return handleError(responseData);
+    }
+}
+
+function handleDeleteResponse(responseData: any): string {
+    if (responseData == null || responseData.data == null || responseData.data 
=== "ok") {
+        return "";
+    } else {
+        return handleError(responseData);
+    }
+}
+
 export async function fetchNamespaces(): Promise<string[]> {
     try {
         const { data: responseData } = await 
axios.get(`${apiHost}/namespaces`);
@@ -43,11 +60,7 @@ export async function createNamespace(name: string): 
Promise<string> {
         const { data: responseData } = await 
axios.post(`${apiHost}/namespaces`, {
             namespace: name,
         });
-        if (responseData?.data != undefined) {
-            return "";
-        } else {
-            return handleError(responseData);
-        }
+        return handleCreateResponse(responseData);
     } catch (error) {
         return handleError(error);
     }
@@ -56,11 +69,7 @@ export async function createNamespace(name: string): 
Promise<string> {
 export async function deleteNamespace(name: string): Promise<string> {
     try {
         const { data: responseData } = await 
axios.delete(`${apiHost}/namespaces/${name}`);
-        if (responseData.data == null) {
-            return "";
-        } else {
-            return handleError(responseData);
-        }
+        return handleDeleteResponse(responseData);
     } catch (error) {
         return handleError(error);
     }
@@ -83,11 +92,7 @@ export async function createCluster(
                 password,
             }
         );
-        if (responseData?.data != undefined) {
-            return "";
-        } else {
-            return handleError(responseData);
-        }
+        return handleCreateResponse(responseData);
     } catch (error) {
         return handleError(error);
     }
@@ -122,11 +127,7 @@ export async function deleteCluster(namespace: string, 
cluster: string): Promise
         const { data: responseData } = await axios.delete(
             `${apiHost}/namespaces/${namespace}/clusters/${cluster}`
         );
-        if (responseData.data == null) {
-            return "";
-        } else {
-            return handleError(responseData);
-        }
+        return handleDeleteResponse(responseData);
     } catch (error) {
         return handleError(error);
     }
@@ -144,11 +145,7 @@ export async function importCluster(
             { nodes, password }
         );
         console.log("importCluster response", responseData);
-        if (responseData?.data != undefined) {
-            return "";
-        } else {
-            return handleError(responseData);
-        }
+        return handleCreateResponse(responseData);
     } catch (error) {
         return handleError(error);
     }
@@ -166,15 +163,11 @@ export async function migrateSlot(
             `${apiHost}/namespaces/${namespace}/clusters/${cluster}/migrate`,
             {
                 target: target,
-                slot: slot,
+                slot: slot.toString(), // SlotRange expects string 
representation like "123"
                 slot_only: slotOnly,
             }
         );
-        if (responseData?.data != undefined) {
-            return "";
-        } else {
-            return handleError(responseData);
-        }
+        return handleCreateResponse(responseData);
     } catch (error) {
         return handleError(error);
     }
@@ -191,11 +184,7 @@ export async function createShard(
             `${apiHost}/namespaces/${namespace}/clusters/${cluster}/shards`,
             { nodes, password }
         );
-        if (responseData?.data != undefined) {
-            return "";
-        } else {
-            return handleError(responseData);
-        }
+        return handleCreateResponse(responseData);
     } catch (error) {
         return handleError(error);
     }
@@ -238,11 +227,7 @@ export async function deleteShard(
         const { data: responseData } = await axios.delete(
             
`${apiHost}/namespaces/${namespace}/clusters/${cluster}/shards/${shard}`
         );
-        if (responseData.data == null) {
-            return "";
-        } else {
-            return handleError(responseData);
-        }
+        return handleDeleteResponse(responseData);
     } catch (error) {
         return handleError(error);
     }
@@ -261,11 +246,7 @@ export async function createNode(
             
`${apiHost}/namespaces/${namespace}/clusters/${cluster}/shards/${shard}/nodes`,
             { addr, role, password }
         );
-        if (responseData?.data == null) {
-            return "";
-        } else {
-            return handleError(responseData);
-        }
+        return handleCreateResponse(responseData);
     } catch (error) {
         return handleError(error);
     }
@@ -297,17 +278,40 @@ export async function deleteNode(
         const { data: responseData } = await axios.delete(
             
`${apiHost}/namespaces/${namespace}/clusters/${cluster}/shards/${shard}/nodes/${nodeId}`
         );
-        if (responseData.data == null) {
-            return "";
-        } else {
-            return handleError(responseData);
-        }
+        return handleDeleteResponse(responseData);
     } catch (error) {
         console.log(error);
         return handleError(error);
     }
 }
 
+export async function failoverShard(
+    namespace: string,
+    cluster: string,
+    shard: string,
+    preferredNodeId?: string
+): Promise<{ newMasterId?: string; error?: string }> {
+    try {
+        const requestBody: { preferred_node_id?: string } = {};
+        if (preferredNodeId) {
+            requestBody.preferred_node_id = preferredNodeId;
+        }
+
+        const { data: responseData } = await axios.post(
+            
`${apiHost}/namespaces/${namespace}/clusters/${cluster}/shards/${shard}/failover`,
+            requestBody
+        );
+
+        if (responseData?.data?.new_master_id) {
+            return { newMasterId: responseData.data.new_master_id };
+        } else {
+            return { error: handleError(responseData) };
+        }
+    } catch (error) {
+        return { error: handleError(error) };
+    }
+}
+
 function handleError(error: any): string {
     let message: string = "";
     if (error instanceof AxiosError) {
diff --git a/webui/src/app/namespaces/[namespace]/clusters/[cluster]/page.tsx 
b/webui/src/app/namespaces/[namespace]/clusters/[cluster]/page.tsx
index 2231ff3..3b6bc7b 100644
--- a/webui/src/app/namespaces/[namespace]/clusters/[cluster]/page.tsx
+++ b/webui/src/app/namespaces/[namespace]/clusters/[cluster]/page.tsx
@@ -34,10 +34,12 @@ import {
     Radio,
     Fade,
     Badge,
+    Collapse,
+    Divider,
 } from "@mui/material";
 import { ClusterSidebar } from "../../../../ui/sidebar";
 import { useState, useEffect } from "react";
-import { listShards, listNodes, fetchCluster } from "@/app/lib/api";
+import { listShards, listNodes, fetchCluster, deleteShard } from 
"@/app/lib/api";
 import { AddShardCard, ResourceCard } from "@/app/ui/createCard";
 import Link from "next/link";
 import { useRouter } from "next/navigation";
@@ -57,7 +59,10 @@ import AddIcon from "@mui/icons-material/Add";
 import WarningIcon from "@mui/icons-material/Warning";
 import InfoIcon from "@mui/icons-material/Info";
 import SwapHorizIcon from "@mui/icons-material/SwapHoriz";
+import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
+import ExpandLessIcon from "@mui/icons-material/ExpandLess";
 import { ShardCreation, MigrateSlot } from "@/app/ui/formCreation";
+import DeleteIcon from "@mui/icons-material/Delete";
 
 interface ResourceCounts {
     shards: number;
@@ -98,11 +103,13 @@ export default function Cluster({ params }: { params: { 
namespace: string; clust
         migrating: 0,
     });
     const [loading, setLoading] = useState<boolean>(true);
+    const [deletingShardIndex, setDeletingShardIndex] = useState<number | 
null>(null);
     const [searchTerm, setSearchTerm] = useState<string>("");
     const [filterAnchorEl, setFilterAnchorEl] = useState<null | 
HTMLElement>(null);
     const [sortAnchorEl, setSortAnchorEl] = useState<null | HTMLElement>(null);
     const [filterOption, setFilterOption] = useState<FilterOption>("all");
     const [sortOption, setSortOption] = useState<SortOption>("index-asc");
+    const [expandedSlots, setExpandedSlots] = useState<Set<number>>(new Set());
     const router = useRouter();
 
     useEffect(() => {
@@ -174,6 +181,74 @@ export default function Cluster({ params }: { params: { 
namespace: string; clust
         fetchData();
     }, [namespace, cluster, router]);
 
+    const handleDeleteShard = async (index: number) => {
+        if (
+            !confirm(
+                `Are you sure you want to delete Shard ${index + 1}? This 
action cannot be undone.`
+            )
+        )
+            return;
+
+        try {
+            setDeletingShardIndex(index);
+            const responseMessage = await deleteShard(namespace, cluster, 
index.toString());
+            if (responseMessage && responseMessage !== "") {
+                alert(`Failed to delete shard: ${responseMessage}`);
+                return;
+            }
+
+            const fetchedShards = await listShards(namespace, cluster);
+            let totalNodes = 0;
+            let withSlots = 0;
+            let migrating = 0;
+
+            const processedShards = (fetchedShards || []).map((shard: any, 
idx: number) => {
+                const nodeCount = shard.nodes?.length || 0;
+                totalNodes += nodeCount;
+
+                const hasSlots = shard.slot_ranges && shard.slot_ranges.length 
> 0;
+                if (hasSlots) withSlots++;
+
+                const migratingSlot =
+                    shard.migrating_slot !== null && shard.migrating_slot !== 
undefined
+                        ? shard.migrating_slot
+                        : -1;
+                const importingSlot =
+                    shard.import_slot !== null && shard.import_slot !== 
undefined
+                        ? shard.import_slot
+                        : -1;
+
+                const hasMigration = migratingSlot >= 0;
+                if (hasMigration) migrating++;
+
+                return {
+                    index: idx,
+                    nodes: shard.nodes || [],
+                    slotRanges: shard.slot_ranges || [],
+                    migratingSlot,
+                    importingSlot,
+                    targetShardIndex: shard.target_shard_index || -1,
+                    nodeCount,
+                    hasSlots,
+                    hasMigration,
+                    hasImporting: importingSlot >= 0,
+                } as ShardData;
+            });
+
+            setShardsData(processedShards);
+            setResourceCounts({
+                shards: processedShards.length,
+                nodes: totalNodes,
+                withSlots,
+                migrating,
+            });
+        } catch (error) {
+            alert(`Failed to delete shard: ${error}`);
+        } finally {
+            setDeletingShardIndex(null);
+        }
+    };
+
     const handleFilterClick = (event: React.MouseEvent<HTMLElement>) => {
         setFilterAnchorEl(event.currentTarget);
     };
@@ -235,12 +310,43 @@ export default function Cluster({ params }: { params: { 
namespace: string; clust
         return <LoadingSpinner />;
     }
 
-    const formatSlotRanges = (ranges: string[]) => {
+    const formatSlotRanges = (ranges: string[], showAll: boolean = false) => {
         if (!ranges || ranges.length === 0) return "None";
-        if (ranges.length <= 2) return ranges.join(", ");
+        if (showAll || ranges.length <= 2) return ranges.join(", ");
         return `${ranges[0]}, ${ranges[1]}, ... (+${ranges.length - 2} more)`;
     };
 
+    const expandSlotRanges = (ranges: string[]) => {
+        if (!ranges || ranges.length === 0) return [];
+        const slots: number[] = [];
+        for (const range of ranges) {
+            if (range.includes("-")) {
+                const [start, end] = range.split("-").map(Number);
+                if (!isNaN(start) && !isNaN(end)) {
+                    for (let i = start; i <= end; i++) {
+                        slots.push(i);
+                    }
+                }
+            } else {
+                const slot = Number(range);
+                if (!isNaN(slot)) {
+                    slots.push(slot);
+                }
+            }
+        }
+        return slots.sort((a, b) => a - b);
+    };
+
+    const toggleSlotExpansion = (shardIndex: number) => {
+        const newExpanded = new Set(expandedSlots);
+        if (newExpanded.has(shardIndex)) {
+            newExpanded.delete(shardIndex);
+        } else {
+            newExpanded.add(shardIndex);
+        }
+        setExpandedSlots(newExpanded);
+    };
+
     return (
         <div className="flex h-full">
             <div className="relative h-full">
@@ -1051,33 +1157,96 @@ export default function Cluster({ params }: { params: { 
namespace: string; clust
                                                                 </Typography>
 
                                                                 
{shard.hasSlots ? (
-                                                                    <div 
className="flex items-center">
-                                                                        
<Typography
-                                                                            
variant="body2"
-                                                                            
className="flex items-center text-gray-500 dark:text-gray-400"
-                                                                        >
-                                                                            
<StorageIcon
+                                                                    <div>
+                                                                        <div 
className="flex items-center">
+                                                                            
<Typography
+                                                                               
 variant="body2"
+                                                                               
 className="flex items-center text-gray-500 dark:text-gray-400"
+                                                                            >
+                                                                               
 <StorageIcon
+                                                                               
     sx={{
+                                                                               
         fontSize: 14,
+                                                                               
     }}
+                                                                               
     className="mr-1"
+                                                                               
 />
+                                                                               
 Slots:{" "}
+                                                                               
 {formatSlotRanges(
+                                                                               
     shard.slotRanges
+                                                                               
 )}
+                                                                            
</Typography>
+                                                                            
<Chip
+                                                                               
 size="small"
+                                                                               
 label={`${shard.slotRanges.length} range${shard.slotRanges.length !== 1 ? "s" 
: ""}`}
+                                                                               
 color="primary"
+                                                                               
 variant="outlined"
+                                                                               
 className="ml-2"
                                                                                
 sx={{
-                                                                               
     fontSize: 14,
+                                                                               
     height: 20,
+                                                                               
     fontSize:
+                                                                               
         "0.7rem",
                                                                                
 }}
-                                                                               
 className="mr-1"
                                                                             />
-                                                                            
Slots:{" "}
-                                                                            
{formatSlotRanges(
-                                                                               
 shard.slotRanges
+                                                                            
{shard.slotRanges
+                                                                               
 .length > 2 && (
+                                                                               
 <IconButton
+                                                                               
     size="small"
+                                                                               
     onClick={(
+                                                                               
         e
+                                                                               
     ) => {
+                                                                               
         e.preventDefault();
+                                                                               
         e.stopPropagation();
+                                                                               
         toggleSlotExpansion(
+                                                                               
             shard.index
+                                                                               
         );
+                                                                               
     }}
+                                                                               
     className="ml-1 p-1"
+                                                                               
     sx={{
+                                                                               
         fontSize: 16,
+                                                                               
     }}
+                                                                               
 >
+                                                                               
     {expandedSlots.has(
+                                                                               
         shard.index
+                                                                               
     ) ? (
+                                                                               
         <ExpandLessIcon fontSize="small" />
+                                                                               
     ) : (
+                                                                               
         <ExpandMoreIcon fontSize="small" />
+                                                                               
     )}
+                                                                               
 </IconButton>
                                                                             )}
-                                                                        
</Typography>
-                                                                        <Chip
-                                                                            
size="small"
-                                                                            
label={`${shard.slotRanges.length} range${shard.slotRanges.length !== 1 ? "s" : 
""}`}
-                                                                            
color="primary"
-                                                                            
variant="outlined"
-                                                                            
className="ml-2"
-                                                                            
sx={{
-                                                                               
 height: 20,
-                                                                               
 fontSize: "0.7rem",
-                                                                            }}
-                                                                        />
+                                                                        </div>
+                                                                        
<Collapse
+                                                                            
in={expandedSlots.has(
+                                                                               
 shard.index
+                                                                            )}
+                                                                        >
+                                                                            
<div className="mt-2 rounded-lg bg-gray-50 p-2 dark:bg-gray-800/50">
+                                                                               
 <Typography
+                                                                               
     variant="caption"
+                                                                               
     className="mb-1 block font-medium text-gray-600 dark:text-gray-300"
+                                                                               
 >
+                                                                               
     All Slot Ranges
+                                                                               
     (
+                                                                               
     {
+                                                                               
         expandSlotRanges(
+                                                                               
             shard.slotRanges
+                                                                               
         ).length
+                                                                               
     }{" "}
+                                                                               
     total slots):
+                                                                               
 </Typography>
+                                                                               
 <Typography
+                                                                               
     variant="body2"
+                                                                               
     className="text-gray-700 dark:text-gray-200"
+                                                                               
     sx={{
+                                                                               
         fontFamily:
+                                                                               
             "monospace",
+                                                                               
     }}
+                                                                               
 >
+                                                                               
     {shard.slotRanges.join(
+                                                                               
         ", "
+                                                                               
     )}
+                                                                               
 </Typography>
+                                                                            
</div>
+                                                                        
</Collapse>
                                                                     </div>
                                                                 ) : (
                                                                     <Typography
@@ -1232,6 +1401,18 @@ export default function Cluster({ params }: { params: { 
namespace: string; clust
                                                     </div>
 
                                                     <div className="ml-2 mt-3 
flex items-center space-x-2 sm:mt-0">
+                                                        <button
+                                                            onClick={() =>
+                                                                
handleDeleteShard(shard.index)
+                                                            }
+                                                            disabled={
+                                                                
deletingShardIndex === shard.index
+                                                            }
+                                                            
className="bg-gray-100 p-2 text-gray-600 transition-colors hover:bg-red-100 
hover:text-red-600 disabled:cursor-not-allowed disabled:opacity-50 
dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-red-900/30 
dark:hover:text-red-400"
+                                                            style={{ 
borderRadius: "16px" }}
+                                                        >
+                                                            <DeleteIcon />
+                                                        </button>
                                                         <Link
                                                             
href={`/namespaces/${namespace}/clusters/${cluster}/shards/${shard.index}`}
                                                             
className="rounded-full bg-primary/10 p-2 text-primary transition-colors 
hover:bg-primary/20 dark:bg-primary-dark/20 dark:text-primary-light 
dark:hover:bg-primary-dark/30"
diff --git 
a/webui/src/app/namespaces/[namespace]/clusters/[cluster]/shards/[shard]/page.tsx
 
b/webui/src/app/namespaces/[namespace]/clusters/[cluster]/shards/[shard]/page.tsx
index 6b7c642..e0d5b80 100644
--- 
a/webui/src/app/namespaces/[namespace]/clusters/[cluster]/shards/[shard]/page.tsx
+++ 
b/webui/src/app/namespaces/[namespace]/clusters/[cluster]/shards/[shard]/page.tsx
@@ -35,7 +35,7 @@ import {
     Fade,
 } from "@mui/material";
 import { ShardSidebar } from "@/app/ui/sidebar";
-import { fetchShard } from "@/app/lib/api";
+import { fetchShard, deleteNode } from "@/app/lib/api";
 import { useRouter } from "next/navigation";
 import { useState, useEffect } from "react";
 import { AddNodeCard } from "@/app/ui/createCard";
@@ -56,6 +56,9 @@ import ArrowUpwardIcon from "@mui/icons-material/ArrowUpward";
 import ArrowDownwardIcon from "@mui/icons-material/ArrowDownward";
 import { NodeCreation } from "@/app/ui/formCreation";
 import AddIcon from "@mui/icons-material/Add";
+import DeleteIcon from "@mui/icons-material/Delete";
+import SwapHorizIcon from "@mui/icons-material/SwapHoriz";
+import { FailoverDialog } from "@/app/ui/failoverDialog";
 
 export default function Shard({
     params,
@@ -65,11 +68,13 @@ export default function Shard({
     const { namespace, cluster, shard } = params;
     const [nodesData, setNodesData] = useState<any>(null);
     const [loading, setLoading] = useState<boolean>(true);
+    const [deletingNodeIndex, setDeletingNodeIndex] = useState<number | 
null>(null);
     const [searchTerm, setSearchTerm] = useState<string>("");
     const [filterAnchorEl, setFilterAnchorEl] = useState<null | 
HTMLElement>(null);
     const [sortAnchorEl, setSortAnchorEl] = useState<null | HTMLElement>(null);
     const [filterOption, setFilterOption] = useState<string>("all");
     const [sortOption, setSortOption] = useState<string>("index-asc");
+    const [failoverDialogOpen, setFailoverDialogOpen] = 
useState<boolean>(false);
     const router = useRouter();
 
     useEffect(() => {
@@ -91,6 +96,20 @@ export default function Shard({
         fetchData();
     }, [namespace, cluster, shard, router]);
 
+    const refreshShardData = async () => {
+        setLoading(true);
+        try {
+            const fetchedNodes = await fetchShard(namespace, cluster, shard);
+            setNodesData(fetchedNodes);
+        } catch (error) {
+            console.error("Error refreshing shard data:", error);
+        } finally {
+            setLoading(false);
+        }
+    };
+
+    const hasReplicaNodes = nodesData?.nodes?.some((node: any) => node.role 
=== "slave") || false;
+
     if (loading) {
         return <LoadingSpinner />;
     }
@@ -206,6 +225,19 @@ export default function Shard({
                                 </div>
                             </div>
                             <div className="flex flex-shrink-0 gap-3">
+                                <Button
+                                    variant="outlined"
+                                    color="primary"
+                                    className="whitespace-nowrap px-5 py-2.5 
font-medium shadow-sm transition-all hover:shadow-md"
+                                    style={{ borderRadius: "16px" }}
+                                    startIcon={<SwapHorizIcon />}
+                                    disableElevation
+                                    size="medium"
+                                    disabled={!hasReplicaNodes}
+                                    onClick={() => setFailoverDialogOpen(true)}
+                                >
+                                    Failover
+                                </Button>
                                 <NodeCreation
                                     position="card"
                                     namespace={namespace}
@@ -636,6 +668,54 @@ export default function Shard({
                                                             </Link>
                                                         </div>
                                                     </div>
+                                                    <div className="ml-2 mt-3 
flex items-center space-x-2 sm:mt-0">
+                                                        <button
+                                                            onClick={async () 
=> {
+                                                                if (
+                                                                    !confirm(
+                                                                        `Are 
you sure you want to delete Node ${index + 1}? This action cannot be undone.`
+                                                                    )
+                                                                )
+                                                                    return;
+                                                                try {
+                                                                    
setDeletingNodeIndex(index);
+                                                                    const res 
= await deleteNode(
+                                                                        
namespace,
+                                                                        
cluster,
+                                                                        shard,
+                                                                        node.id
+                                                                    );
+                                                                    if (res) {
+                                                                        alert(
+                                                                            
`Failed to delete node: ${res}`
+                                                                        );
+                                                                        return;
+                                                                    }
+                                                                    // Refetch 
shard
+                                                                    
setLoading(true);
+                                                                    const 
fetchedNodes =
+                                                                        await 
fetchShard(
+                                                                            
namespace,
+                                                                            
cluster,
+                                                                            
shard
+                                                                        );
+                                                                    
setNodesData(fetchedNodes);
+                                                                } catch (e) {
+                                                                    alert(
+                                                                        
`Failed to delete node: ${e}`
+                                                                    );
+                                                                } finally {
+                                                                    
setDeletingNodeIndex(null);
+                                                                    
setLoading(false);
+                                                                }
+                                                            }}
+                                                            
disabled={deletingNodeIndex === index}
+                                                            
className="bg-gray-100 p-2 text-gray-600 transition-colors hover:bg-red-100 
hover:text-red-600 disabled:cursor-not-allowed disabled:opacity-50 
dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-red-900/30 
dark:hover:text-red-400"
+                                                            style={{ 
borderRadius: "16px" }}
+                                                        >
+                                                            <DeleteIcon />
+                                                        </button>
+                                                    </div>
                                                 </div>
                                             </Paper>
                                         </div>
@@ -674,6 +754,16 @@ export default function Shard({
                         )}
                     </Paper>
                 </Box>
+
+                <FailoverDialog
+                    open={failoverDialogOpen}
+                    onClose={() => setFailoverDialogOpen(false)}
+                    namespace={namespace}
+                    cluster={cluster}
+                    shard={shard}
+                    nodes={nodesData?.nodes || []}
+                    onSuccess={refreshShardData}
+                />
             </div>
         </div>
     );
diff --git a/webui/src/app/namespaces/[namespace]/page.tsx 
b/webui/src/app/namespaces/[namespace]/page.tsx
index f9b1de0..e1ede3d 100644
--- a/webui/src/app/namespaces/[namespace]/page.tsx
+++ b/webui/src/app/namespaces/[namespace]/page.tsx
@@ -35,7 +35,14 @@ import {
     Fade,
 } from "@mui/material";
 import { NamespaceSidebar } from "../../ui/sidebar";
-import { fetchCluster, fetchClusters, fetchNamespaces, listShards, listNodes } 
from "@/app/lib/api";
+import {
+    fetchCluster,
+    fetchClusters,
+    fetchNamespaces,
+    listShards,
+    listNodes,
+    deleteCluster,
+} from "@/app/lib/api";
 import Link from "next/link";
 import { useRouter, notFound } from "next/navigation";
 import { useState, useEffect } from "react";
@@ -60,6 +67,7 @@ import FileUploadIcon from "@mui/icons-material/FileUpload";
 import WarningIcon from "@mui/icons-material/Warning";
 import InfoIcon from "@mui/icons-material/Info";
 import { ClusterCreation, ImportCluster } from "@/app/ui/formCreation";
+import DeleteIcon from "@mui/icons-material/Delete";
 
 interface ResourceCounts {
     clusters: number;
@@ -112,6 +120,7 @@ export default function Namespace({ params }: { params: { 
namespace: string } })
     const [filterOption, setFilterOption] = useState<FilterOption>("all");
     const [sortOption, setSortOption] = useState<SortOption>("name-asc");
     const router = useRouter();
+    const [deletingCluster, setDeletingCluster] = useState<string | 
null>(null);
 
     useEffect(() => {
         const fetchData = async () => {
@@ -224,6 +233,112 @@ export default function Namespace({ params }: { params: { 
namespace: string } })
         fetchData();
     }, [params.namespace, router]);
 
+    const handleDeleteCluster = async (clusterName: string) => {
+        if (!confirm(`Are you sure you want to delete cluster 
"${clusterName}"?`)) return;
+
+        try {
+            setDeletingCluster(clusterName);
+            const res = await deleteCluster(params.namespace, clusterName);
+            if (res) {
+                alert(`Failed to delete cluster: ${res}`);
+                return;
+            }
+
+            // Re-fetch clusters and rebuild data
+            setLoading(true);
+            const clusters = await fetchClusters(params.namespace);
+            let totalShards = 0;
+            let totalNodes = 0;
+
+            const data = await Promise.all(
+                clusters.map(async (cluster) => {
+                    try {
+                        const clusterInfo = await 
fetchCluster(params.namespace, cluster);
+                        if (
+                            clusterInfo &&
+                            typeof clusterInfo === "object" &&
+                            "shards" in clusterInfo
+                        ) {
+                            const shards = (clusterInfo as any).shards || [];
+
+                            let clusterNodeCount = 0;
+                            for (let i = 0; i < shards.length; i++) {
+                                try {
+                                    const nodes = await listNodes(
+                                        params.namespace,
+                                        cluster,
+                                        i.toString()
+                                    );
+                                    if (Array.isArray(nodes)) {
+                                        clusterNodeCount += nodes.length;
+                                    }
+                                } catch (error) {
+                                    console.error(`Failed to fetch nodes for 
shard ${i}:`, error);
+                                }
+                            }
+
+                            totalShards += shards.length;
+                            totalNodes += clusterNodeCount;
+
+                            const hasSlots = shards.some(
+                                (s: any) => s.slot_ranges && 
s.slot_ranges.length > 0
+                            );
+                            const hasMigration = shards.some((s: any) => 
s.migrating_slot >= 0);
+                            const hasNoMigration = shards.every(
+                                (s: any) => s.migrating_slot === -1
+                            );
+                            const hasImporting = shards.some((s: any) => 
s.import_slot >= 0);
+
+                            const slotRanges =
+                                shards.find((s: any) => s.slot_ranges && 
s.slot_ranges.length > 0)
+                                    ?.slot_ranges || [];
+                            const migratingSlot =
+                                shards.find((s: any) => s.migrating_slot >= 
0)?.migrating_slot ||
+                                -1;
+                            const importingSlot =
+                                shards.find((s: any) => s.import_slot >= 
0)?.import_slot || -1;
+                            const targetShardIndex =
+                                shards.find((s: any) => s.target_shard_index 
>= 0)
+                                    ?.target_shard_index || -1;
+
+                            return {
+                                ...clusterInfo,
+                                shards,
+                                shardCount: shards.length,
+                                nodeCount: clusterNodeCount,
+                                hasSlots,
+                                hasMigration,
+                                hasNoMigration,
+                                hasImporting,
+                                slotRanges,
+                                migratingSlot,
+                                importingSlot,
+                                targetShardIndex,
+                            } as ClusterData;
+                        }
+                        return null;
+                    } catch (error) {
+                        console.error(`Failed to fetch data for cluster 
${cluster}:`, error);
+                        return null;
+                    }
+                })
+            );
+
+            const validData = data.filter(Boolean) as ClusterData[];
+            setClusterData(validData);
+            setResourceCounts({
+                clusters: validData.length,
+                shards: totalShards,
+                nodes: totalNodes,
+            });
+        } catch (e) {
+            alert(`Failed to delete cluster: ${e}`);
+        } finally {
+            setDeletingCluster(null);
+            setLoading(false);
+        }
+    };
+
     const handleFilterClick = (event: React.MouseEvent<HTMLElement>) => {
         setFilterAnchorEl(event.currentTarget);
     };
@@ -1221,6 +1336,18 @@ export default function Namespace({ params }: { params: 
{ namespace: string } })
                                                     </div>
 
                                                     <div className="ml-2 mt-3 
flex items-center space-x-2 sm:mt-0">
+                                                        <button
+                                                            onClick={() =>
+                                                                
handleDeleteCluster(cluster.name)
+                                                            }
+                                                            disabled={
+                                                                
deletingCluster === cluster.name
+                                                            }
+                                                            
className="bg-gray-100 p-2 text-gray-600 transition-colors hover:bg-red-100 
hover:text-red-600 disabled:cursor-not-allowed disabled:opacity-50 
dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-red-900/30 
dark:hover:text-red-400"
+                                                            style={{ 
borderRadius: "16px" }}
+                                                        >
+                                                            <DeleteIcon />
+                                                        </button>
                                                         <Link
                                                             
href={`/namespaces/${params.namespace}/clusters/${cluster.name}`}
                                                             
className="bg-primary/10 p-2 text-primary transition-colors hover:bg-primary/20 
dark:bg-primary-dark/20 dark:text-primary-light dark:hover:bg-primary-dark/30"
diff --git a/webui/src/app/ui/failoverDialog.tsx 
b/webui/src/app/ui/failoverDialog.tsx
new file mode 100644
index 0000000..9958704
--- /dev/null
+++ b/webui/src/app/ui/failoverDialog.tsx
@@ -0,0 +1,419 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+"use client";
+
+import React, { useState } from "react";
+import {
+    Dialog,
+    DialogTitle,
+    DialogContent,
+    DialogActions,
+    Button,
+    Typography,
+    FormControl,
+    FormLabel,
+    RadioGroup,
+    FormControlLabel,
+    Radio,
+    Box,
+    Chip,
+    CircularProgress,
+    Alert,
+    Snackbar,
+    alpha,
+    useTheme,
+} from "@mui/material";
+import SwapHorizIcon from "@mui/icons-material/SwapHoriz";
+import CheckCircleIcon from "@mui/icons-material/CheckCircle";
+import DeviceHubIcon from "@mui/icons-material/DeviceHub";
+import { failoverShard } from "@/app/lib/api";
+
+interface Node {
+    id: string;
+    addr: string;
+    role: string;
+    created_at: number;
+}
+
+interface FailoverDialogProps {
+    open: boolean;
+    onClose: () => void;
+    namespace: string;
+    cluster: string;
+    shard: string;
+    nodes: Node[];
+    onSuccess: () => void;
+}
+
+export const FailoverDialog: React.FC<FailoverDialogProps> = ({
+    open,
+    onClose,
+    namespace,
+    cluster,
+    shard,
+    nodes,
+    onSuccess,
+}) => {
+    const [selectedNodeId, setSelectedNodeId] = useState<string>("auto");
+    const [loading, setLoading] = useState(false);
+    const [error, setError] = useState<string>("");
+    const theme = useTheme();
+
+    const masterNode = nodes.find((node) => node.role === "master");
+    const slaveNodes = nodes.filter((node) => node.role === "slave");
+
+    const handleFailover = async () => {
+        setLoading(true);
+        setError("");
+
+        try {
+            const result = await failoverShard(
+                namespace,
+                cluster,
+                shard,
+                selectedNodeId === "auto" ? undefined : selectedNodeId
+            );
+
+            if (result.error) {
+                setError(result.error);
+            } else {
+                onSuccess();
+                onClose();
+                setSelectedNodeId("auto");
+            }
+        } catch (err) {
+            setError("An unexpected error occurred during failover");
+        } finally {
+            setLoading(false);
+        }
+    };
+
+    const handleClose = () => {
+        if (!loading) {
+            onClose();
+            setSelectedNodeId("auto");
+            setError("");
+        }
+    };
+
+    const truncateId = (id: string, length: number = 8) => {
+        return id.length > length ? `${id.substring(0, length)}...` : id;
+    };
+
+    return (
+        <>
+            <Dialog
+                open={open}
+                onClose={handleClose}
+                maxWidth="sm"
+                fullWidth
+                PaperProps={{
+                    sx: {
+                        borderRadius: "24px",
+                        boxShadow: "0 25px 50px -12px rgba(0, 0, 0, 0.25)",
+                        backgroundImage:
+                            theme.palette.mode === "dark"
+                                ? "linear-gradient(to bottom, rgba(66, 66, 66, 
0.8), rgba(33, 33, 33, 0.9))"
+                                : "linear-gradient(to bottom, #ffffff, 
#f9fafb)",
+                        backdropFilter: "blur(20px)",
+                        overflow: "hidden",
+                    },
+                }}
+            >
+                <DialogTitle
+                    sx={{
+                        background:
+                            theme.palette.mode === "dark"
+                                ? alpha(theme.palette.background.paper, 0.5)
+                                : alpha(theme.palette.primary.light, 0.1),
+                        borderBottom: `1px solid ${
+                            theme.palette.mode === "dark"
+                                ? theme.palette.grey[800]
+                                : theme.palette.grey[200]
+                        }`,
+                        padding: "24px",
+                    }}
+                >
+                    <Box display="flex" alignItems="center" gap={2}>
+                        <SwapHorizIcon className="text-primary" sx={{ 
fontSize: 28 }} />
+                        <Box>
+                            <Typography
+                                variant="h6"
+                                className="font-semibold text-gray-800 
dark:text-gray-100"
+                            >
+                                Failover Shard Master
+                            </Typography>
+                            <Typography
+                                variant="body2"
+                                className="text-gray-500 dark:text-gray-400"
+                            >
+                                Promote a replica node to master
+                            </Typography>
+                        </Box>
+                    </Box>
+                </DialogTitle>
+
+                <DialogContent sx={{ padding: "24px" }}>
+                    <Box mb={2}>
+                        <Typography
+                            variant="subtitle1"
+                            className="mb-2 font-medium text-gray-700 
dark:text-gray-300"
+                        >
+                            Current Master
+                        </Typography>
+                        {masterNode && (
+                            <Box
+                                sx={{
+                                    p: 2,
+                                    border: `1px solid 
${theme.palette.success.light}`,
+                                    borderRadius: "16px",
+                                    backgroundColor: 
alpha(theme.palette.success.light, 0.1),
+                                }}
+                            >
+                                <Box display="flex" alignItems="center" 
gap={2}>
+                                    <CheckCircleIcon className="text-success" 
/>
+                                    <Box flex={1}>
+                                        <Typography variant="body1" 
className="font-medium">
+                                            {masterNode.addr}
+                                        </Typography>
+                                        <Typography variant="body2" 
className="text-gray-500">
+                                            ID: {truncateId(masterNode.id)}
+                                        </Typography>
+                                    </Box>
+                                    <Chip
+                                        label="Master"
+                                        size="small"
+                                        className="bg-green-100 text-green-800 
dark:bg-green-900 dark:text-green-200"
+                                    />
+                                </Box>
+                            </Box>
+                        )}
+                    </Box>
+
+                    {slaveNodes.length > 0 ? (
+                        <Box>
+                            <Typography
+                                variant="subtitle1"
+                                className="mb-3 font-medium text-gray-700 
dark:text-gray-300"
+                            >
+                                Select New Master
+                            </Typography>
+                            <FormControl component="fieldset" fullWidth>
+                                <RadioGroup
+                                    value={selectedNodeId}
+                                    onChange={(e) => 
setSelectedNodeId(e.target.value)}
+                                >
+                                    <FormControlLabel
+                                        value="auto"
+                                        control={
+                                            <Radio
+                                                sx={{
+                                                    color: 
theme.palette.primary.main,
+                                                    "&.Mui-checked": {
+                                                        color: 
theme.palette.primary.main,
+                                                    },
+                                                }}
+                                            />
+                                        }
+                                        label={
+                                            <Box>
+                                                <Typography variant="body1" 
className="font-medium">
+                                                    Automatic Selection
+                                                </Typography>
+                                                <Typography
+                                                    variant="body2"
+                                                    className="text-gray-500"
+                                                >
+                                                    Let the controller choose 
the best replica
+                                                </Typography>
+                                            </Box>
+                                        }
+                                        sx={{
+                                            p: 2,
+                                            m: 0,
+                                            mb: 2,
+                                            border: `1px solid ${
+                                                selectedNodeId === "auto"
+                                                    ? 
theme.palette.primary.main
+                                                    : theme.palette.grey[300]
+                                            }`,
+                                            borderRadius: "16px",
+                                            backgroundColor:
+                                                selectedNodeId === "auto"
+                                                    ? 
alpha(theme.palette.primary.main, 0.1)
+                                                    : "transparent",
+                                            transition: "all 0.2s ease",
+                                            "&:hover": {
+                                                backgroundColor: alpha(
+                                                    theme.palette.primary.main,
+                                                    0.05
+                                                ),
+                                            },
+                                        }}
+                                    />
+
+                                    {slaveNodes.map((node) => (
+                                        <FormControlLabel
+                                            key={node.id}
+                                            value={node.id}
+                                            control={
+                                                <Radio
+                                                    sx={{
+                                                        color: 
theme.palette.primary.main,
+                                                        "&.Mui-checked": {
+                                                            color: 
theme.palette.primary.main,
+                                                        },
+                                                    }}
+                                                />
+                                            }
+                                            label={
+                                                <Box
+                                                    display="flex"
+                                                    alignItems="center"
+                                                    gap={2}
+                                                    flex={1}
+                                                >
+                                                    <DeviceHubIcon 
className="text-info" />
+                                                    <Box flex={1}>
+                                                        <Typography
+                                                            variant="body1"
+                                                            
className="font-medium"
+                                                        >
+                                                            {node.addr}
+                                                        </Typography>
+                                                        <Typography
+                                                            variant="body2"
+                                                            
className="text-gray-500"
+                                                        >
+                                                            ID: 
{truncateId(node.id)}
+                                                        </Typography>
+                                                    </Box>
+                                                    <Chip
+                                                        label="Replica"
+                                                        size="small"
+                                                        className="bg-blue-100 
text-blue-800 dark:bg-blue-900 dark:text-blue-200"
+                                                    />
+                                                </Box>
+                                            }
+                                            sx={{
+                                                p: 2,
+                                                m: 0,
+                                                mb: 2,
+                                                border: `1px solid ${
+                                                    selectedNodeId === node.id
+                                                        ? 
theme.palette.primary.main
+                                                        : 
theme.palette.grey[300]
+                                                }`,
+                                                borderRadius: "16px",
+                                                backgroundColor:
+                                                    selectedNodeId === node.id
+                                                        ? 
alpha(theme.palette.primary.main, 0.1)
+                                                        : "transparent",
+                                                transition: "all 0.2s ease",
+                                                "&:hover": {
+                                                    backgroundColor: alpha(
+                                                        
theme.palette.primary.main,
+                                                        0.05
+                                                    ),
+                                                },
+                                            }}
+                                        />
+                                    ))}
+                                </RadioGroup>
+                            </FormControl>
+                        </Box>
+                    ) : (
+                        <Alert severity="warning" sx={{ borderRadius: "16px" 
}}>
+                            No replica nodes available for failover. At least 
one replica node is
+                            required.
+                        </Alert>
+                    )}
+                </DialogContent>
+
+                <DialogActions
+                    sx={{
+                        background:
+                            theme.palette.mode === "dark"
+                                ? alpha(theme.palette.background.paper, 0.5)
+                                : alpha(theme.palette.primary.light, 0.05),
+                        borderTop: `1px solid ${
+                            theme.palette.mode === "dark"
+                                ? theme.palette.grey[800]
+                                : theme.palette.grey[200]
+                        }`,
+                        padding: "24px",
+                        justifyContent: "space-between",
+                    }}
+                >
+                    <Button
+                        onClick={handleClose}
+                        disabled={loading}
+                        sx={{
+                            textTransform: "none",
+                            fontWeight: 500,
+                            borderRadius: "16px",
+                            px: 3,
+                            py: 1,
+                        }}
+                    >
+                        Cancel
+                    </Button>
+                    <Button
+                        onClick={handleFailover}
+                        variant="contained"
+                        disabled={loading || slaveNodes.length === 0}
+                        startIcon={loading ? <CircularProgress size={16} /> : 
<SwapHorizIcon />}
+                        sx={{
+                            textTransform: "none",
+                            fontWeight: 600,
+                            borderRadius: "16px",
+                            px: 4,
+                            py: 1,
+                            backgroundColor: theme.palette.primary.main,
+                            "&:hover": {
+                                backgroundColor: theme.palette.primary.dark,
+                                transform: "translateY(-1px)",
+                                boxShadow: "0 6px 15px rgba(0, 0, 0, 0.1)",
+                            },
+                        }}
+                    >
+                        {loading ? "Processing..." : "Start Failover"}
+                    </Button>
+                </DialogActions>
+            </Dialog>
+
+            <Snackbar
+                open={!!error}
+                autoHideDuration={6000}
+                onClose={() => setError("")}
+                anchorOrigin={{ vertical: "bottom", horizontal: "right" }}
+            >
+                <Alert
+                    onClose={() => setError("")}
+                    severity="error"
+                    variant="filled"
+                    sx={{ borderRadius: "16px" }}
+                >
+                    {error}
+                </Alert>
+            </Snackbar>
+        </>
+    );
+};
diff --git a/webui/src/app/ui/formCreation.tsx 
b/webui/src/app/ui/formCreation.tsx
index ff8cdc4..b4d7e89 100644
--- a/webui/src/app/ui/formCreation.tsx
+++ b/webui/src/app/ui/formCreation.tsx
@@ -27,6 +27,7 @@ import {
     createShard,
     importCluster,
     migrateSlot,
+    listShards,
 } from "../lib/api";
 import { useRouter } from "next/navigation";
 
@@ -84,7 +85,7 @@ export const NamespaceCreation: React.FC<NamespaceFormProps> 
= ({ position, chil
         if (response === "") {
             router.push(`/namespaces/${formObj["name"]}`);
         } else {
-            return "Invalid form data";
+            return response || "Failed to create namespace";
         }
     };
 
@@ -129,7 +130,7 @@ export const ClusterCreation: React.FC<ClusterFormProps> = 
({ position, namespac
         if (response === "") {
             
router.push(`/namespaces/${namespace}/clusters/${formObj["name"]}`);
         } else {
-            return "Invalid form data";
+            return response || "Failed to create cluster";
         }
     };
 
@@ -168,31 +169,51 @@ export const ShardCreation: React.FC<ShardFormProps> = ({
 }) => {
     const router = useRouter();
     const handleSubmit = async (formData: FormData) => {
-        const fieldsToValidate = ["nodes"];
-        const errorMessage = validateFormData(formData, fieldsToValidate);
-        if (errorMessage) {
-            return errorMessage;
-        }
+        try {
+            const fieldsToValidate = ["nodes"];
+            const errorMessage = validateFormData(formData, fieldsToValidate);
+            if (errorMessage) {
+                return errorMessage;
+            }
 
-        const formObj = Object.fromEntries(formData.entries());
-        const nodes = JSON.parse(formObj["nodes"] as string) as string[];
-        const password = formObj["password"] as string;
+            const formObj = Object.fromEntries(formData.entries());
+
+            let nodes: string[];
+            try {
+                const nodesString = formObj["nodes"] as string;
+                if (!nodesString) {
+                    return "Nodes field is required.";
+                }
+                nodes = JSON.parse(nodesString) as string[];
+            } catch (parseError) {
+                return "Invalid nodes format. Please check your input.";
+            }
 
-        if (nodes.length === 0) {
-            return "Nodes cannot be empty.";
-        }
+            const password = (formObj["password"] as string) || "";
 
-        for (const node of nodes) {
-            if (containsWhitespace(node)) {
-                return "Nodes cannot contain any whitespace characters.";
+            if (!Array.isArray(nodes) || nodes.length === 0) {
+                return "At least one node is required.";
             }
-        }
 
-        const response = await createShard(namespace, cluster, nodes, 
password);
-        if (response === "") {
-            router.push(`/namespaces/${namespace}/clusters/${cluster}`);
-        } else {
-            return "Invalid form data";
+            for (const node of nodes) {
+                if (!node || typeof node !== "string") {
+                    return "All nodes must be valid address strings.";
+                }
+                if (containsWhitespace(node)) {
+                    return "Node addresses cannot contain whitespace 
characters.";
+                }
+            }
+
+            const response = await createShard(namespace, cluster, nodes, 
password);
+            if (response === "") {
+                // Refresh the page to show the new shard
+                window.location.reload();
+            } else {
+                return response || "Failed to create shard";
+            }
+        } catch (error) {
+            console.error("Error in shard creation:", error);
+            return `Failed to create shard: ${error instanceof Error ? 
error.message : "Unknown error"}`;
         }
     };
 
@@ -244,7 +265,7 @@ export const ImportCluster: React.FC<ClusterFormProps> = ({ 
position, namespace,
         if (response === "") {
             router.push(`/namespaces/${namespace}/clusters/${cluster}`);
         } else {
-            return "Invalid form data";
+            return response || "Failed to import cluster";
         }
     };
 
@@ -288,11 +309,80 @@ export const MigrateSlot: React.FC<ShardFormProps> = ({ 
position, namespace, clu
         const slot = parseInt(formObj["slot"] as string);
         const slotOnly = formObj["slot_only"] === "true";
 
+        // Basic Validation for numeric inputs
+        if (isNaN(target) || target < 0) {
+            return "Target shard index must be a valid non-negative number.";
+        }
+        if (isNaN(slot) || slot < 0 || slot > 16383) {
+            return "Slot must be a valid number between 0 and 16383.";
+        }
+
+        try {
+            const shards = await listShards(namespace, cluster);
+            if (!shards || !Array.isArray(shards)) {
+                return "Failed to fetch shard information. Please check if the 
cluster exists.";
+            }
+
+            if (target >= shards.length) {
+                return `Target shard index ${target} does not exist. Available 
shards: 0-${shards.length - 1}.`;
+            }
+
+            let sourceShardIndex = -1;
+            for (let i = 0; i < shards.length; i++) {
+                const shard = shards[i] as any;
+                if (shard.slot_ranges && Array.isArray(shard.slot_ranges)) {
+                    for (const range of shard.slot_ranges) {
+                        let start: number, end: number;
+                        if (range.includes("-")) {
+                            [start, end] = range.split("-").map(Number);
+                        } else {
+                            // Single slot, not a range
+                            start = end = Number(range);
+                        }
+                        if (slot >= start && slot <= end) {
+                            sourceShardIndex = i;
+                            break;
+                        }
+                    }
+                    if (sourceShardIndex !== -1) break;
+                }
+            }
+
+            if (sourceShardIndex === target) {
+                return `Cannot migrate slot ${slot} to the same shard 
(${target}). The slot is already in shard ${sourceShardIndex}.`;
+            }
+
+            if (sourceShardIndex === -1) {
+                return `Slot ${slot} is not currently assigned to any shard 
and cannot be migrated.`;
+            }
+
+            const sourceShard = shards[sourceShardIndex] as any;
+            if (sourceShard.migrating_slot === slot) {
+                return `Slot ${slot} is already being migrated from shard 
${sourceShardIndex}.`;
+            }
+
+            const targetShard = shards[target] as any;
+            if (targetShard.import_slot === slot) {
+                return `Slot ${slot} is already being imported to shard 
${target}.`;
+            }
+        } catch (error) {
+            console.error("Error validating migration:", error);
+        }
+
         const response = await migrateSlot(namespace, cluster, target, slot, 
slotOnly);
         if (response === "") {
             window.location.reload();
         } else {
-            return "Invalid form data";
+            // Handle specific error messages from the API
+            if (response.includes("source and target shard is same")) {
+                return "Migration failed: The source and target shards are the 
same. Please select a different target shard.";
+            } else if (response.includes("the entry does not exist")) {
+                return "Migration failed: The specified cluster, shard, or 
slot does not exist.";
+            } else if (response.includes("already existed")) {
+                return "Migration failed: The slot is already being migrated 
or exists in the target shard.";
+            } else {
+                return `Migration failed: ${response}`;
+            }
         }
     };
 
@@ -302,13 +392,13 @@ export const MigrateSlot: React.FC<ShardFormProps> = ({ 
position, namespace, clu
             title="Migrate Slot"
             submitButtonLabel="Migrate"
             formFields={[
-                { name: "target", label: "Input Target", type: "text", 
required: true },
-                { name: "slot", label: "Input Slot", type: "text", required: 
true },
+                { name: "target", label: "Target Shard Index", type: "text", 
required: true },
+                { name: "slot", label: "Slot Number (0-16383)", type: "text", 
required: true },
                 {
                     name: "slot_only",
-                    label: "Slot Only",
+                    label: "Slot Only Migration",
                     type: "enum",
-                    values: ["true", "false"],
+                    values: ["false", "true"],
                     required: true,
                 },
             ]}
@@ -346,7 +436,7 @@ export const NodeCreation: React.FC<NodeFormProps> = ({
         if (response === "") {
             window.location.reload();
         } else {
-            return "Invalid form data";
+            return response || "Failed to create node";
         }
     };
 

Reply via email to