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

twice 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 0cffae9  feat(webui): implemented cluster slot migration (#341)
0cffae9 is described below

commit 0cffae9a12cf2a2b1e5d54e55437b93ed3ba50db
Author: Agnik Misra <[email protected]>
AuthorDate: Tue Aug 19 19:26:10 2025 +0530

    feat(webui): implemented cluster slot migration (#341)
    
    * feat(webui): implemented cluster slot migration
    
    * support of Slot range
    
    * TypeScript error fix
---
 webui/src/app/lib/api.ts                           |   4 +-
 .../[namespace]/clusters/[cluster]/page.tsx        | 207 ++++++---
 webui/src/app/ui/formCreation.tsx                  |   2 +-
 webui/src/app/ui/migrationDialog.tsx               | 484 +++++++++++++++++++++
 4 files changed, 625 insertions(+), 72 deletions(-)

diff --git a/webui/src/app/lib/api.ts b/webui/src/app/lib/api.ts
index 03a0f0e..8503bcc 100644
--- a/webui/src/app/lib/api.ts
+++ b/webui/src/app/lib/api.ts
@@ -155,7 +155,7 @@ export async function migrateSlot(
     namespace: string,
     cluster: string,
     target: number,
-    slot: number,
+    slot: string,
     slotOnly: boolean
 ): Promise<string> {
     try {
@@ -163,7 +163,7 @@ export async function migrateSlot(
             `${apiHost}/namespaces/${namespace}/clusters/${cluster}/migrate`,
             {
                 target: target,
-                slot: slot.toString(), // SlotRange expects string 
representation like "123"
+                slot: slot,
                 slot_only: slotOnly,
             }
         );
diff --git a/webui/src/app/namespaces/[namespace]/clusters/[cluster]/page.tsx 
b/webui/src/app/namespaces/[namespace]/clusters/[cluster]/page.tsx
index 3b6bc7b..5132a1c 100644
--- a/webui/src/app/namespaces/[namespace]/clusters/[cluster]/page.tsx
+++ b/webui/src/app/namespaces/[namespace]/clusters/[cluster]/page.tsx
@@ -61,8 +61,10 @@ 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 { ShardCreation } from "@/app/ui/formCreation";
 import DeleteIcon from "@mui/icons-material/Delete";
+import MoveUpIcon from "@mui/icons-material/MoveUp";
+import { MigrationDialog } from "@/app/ui/migrationDialog";
 
 interface ResourceCounts {
     shards: number;
@@ -75,8 +77,8 @@ interface ShardData {
     index: number;
     nodes: any[];
     slotRanges: string[];
-    migratingSlot: number;
-    importingSlot: number;
+    migratingSlot: string;
+    importingSlot: string;
     targetShardIndex: number;
     nodeCount: number;
     hasSlots: boolean;
@@ -110,8 +112,27 @@ export default function Cluster({ params }: { params: { 
namespace: string; clust
     const [filterOption, setFilterOption] = useState<FilterOption>("all");
     const [sortOption, setSortOption] = useState<SortOption>("index-asc");
     const [expandedSlots, setExpandedSlots] = useState<Set<number>>(new Set());
+    const [migrationDialogOpen, setMigrationDialogOpen] = 
useState<boolean>(false);
     const router = useRouter();
 
+    const isActiveMigration = (migratingSlot: string | null | undefined): 
boolean => {
+        return (
+            migratingSlot !== null &&
+            migratingSlot !== undefined &&
+            migratingSlot !== "" &&
+            migratingSlot !== "-1"
+        );
+    };
+
+    const isActiveImport = (importingSlot: string | null | undefined): boolean 
=> {
+        return (
+            importingSlot !== null &&
+            importingSlot !== undefined &&
+            importingSlot !== "" &&
+            importingSlot !== "-1"
+        );
+    };
+
     useEffect(() => {
         const fetchData = async () => {
             try {
@@ -135,19 +156,11 @@ export default function Cluster({ params }: { params: { 
namespace: string; clust
                         const hasSlots = shard.slot_ranges && 
shard.slot_ranges.length > 0;
                         if (hasSlots) withSlots++;
 
-                        // Ensure we're using the correct field names from the 
API
-                        // Handle null values properly - null means no 
migration/import
-                        // Also handle missing fields (import_slot might not 
be present in all responses)
-                        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;
+                        // Handle string values from API as per documentation
+                        const migratingSlot = shard.migrating_slot || "";
+                        const importingSlot = shard.import_slot || "";
+
+                        const hasMigration = isActiveMigration(migratingSlot);
                         if (hasMigration) migrating++;
 
                         return {
@@ -160,7 +173,7 @@ export default function Cluster({ params }: { params: { 
namespace: string; clust
                             nodeCount,
                             hasSlots,
                             hasMigration,
-                            hasImporting: importingSlot >= 0,
+                            hasImporting: isActiveImport(importingSlot),
                         };
                     })
                 );
@@ -181,6 +194,63 @@ export default function Cluster({ params }: { params: { 
namespace: string; clust
         fetchData();
     }, [namespace, cluster, router]);
 
+    const refreshShardData = async () => {
+        setLoading(true);
+        try {
+            const fetchedShards = await listShards(namespace, cluster);
+            if (!fetchedShards) {
+                console.error(`Shards not found`);
+                router.push("/404");
+                return;
+            }
+
+            let totalNodes = 0;
+            let withSlots = 0;
+            let migrating = 0;
+
+            const processedShards = await Promise.all(
+                fetchedShards.map(async (shard: any, index: 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 || "";
+                    const importingSlot = shard.import_slot || "";
+
+                    const hasMigration = isActiveMigration(migratingSlot);
+                    if (hasMigration) migrating++;
+
+                    return {
+                        index,
+                        nodes: shard.nodes || [],
+                        slotRanges: shard.slot_ranges || [],
+                        migratingSlot,
+                        importingSlot,
+                        targetShardIndex: shard.target_shard_index || -1,
+                        nodeCount,
+                        hasSlots,
+                        hasMigration,
+                        hasImporting: isActiveImport(importingSlot),
+                    };
+                })
+            );
+
+            setShardsData(processedShards);
+            setResourceCounts({
+                shards: processedShards.length,
+                nodes: totalNodes,
+                withSlots,
+                migrating,
+            });
+        } catch (error) {
+            console.error("Error fetching shards:", error);
+        } finally {
+            setLoading(false);
+        }
+    };
+
     const handleDeleteShard = async (index: number) => {
         if (
             !confirm(
@@ -420,23 +490,21 @@ export default function Cluster({ params }: { params: { 
namespace: string; clust
                                         Create Shard
                                     </Button>
                                 </ShardCreation>
-                                <MigrateSlot
-                                    position="page"
-                                    namespace={namespace}
-                                    cluster={cluster}
+                                <Button
+                                    variant="outlined"
+                                    color="warning"
+                                    className="whitespace-nowrap px-5 py-2.5 
font-medium shadow-sm transition-all hover:shadow-md"
+                                    startIcon={<MoveUpIcon />}
+                                    disableElevation
+                                    size="medium"
+                                    style={{ borderRadius: "16px" }}
+                                    onClick={() => 
setMigrationDialogOpen(true)}
+                                    disabled={
+                                        shardsData.filter((shard) => 
shard.hasSlots).length < 2
+                                    }
                                 >
-                                    <Button
-                                        variant="outlined"
-                                        color="warning"
-                                        className="whitespace-nowrap px-5 
py-2.5 font-medium shadow-sm transition-all hover:shadow-md"
-                                        startIcon={<SwapHorizIcon />}
-                                        disableElevation
-                                        size="medium"
-                                        style={{ borderRadius: "16px" }}
-                                    >
-                                        Migrate Slot
-                                    </Button>
-                                </MigrateSlot>
+                                    Migrate Slot
+                                </Button>
                             </div>
                         </div>
                     </div>
@@ -1089,24 +1157,20 @@ export default function Cluster({ params }: { params: { 
namespace: string; clust
                                                                     Shard 
{shard.index + 1}
                                                                 </Typography>
 
-                                                                
{shard.hasMigration &&
-                                                                    
shard.migratingSlot >= 0 && (
-                                                                        <div
-                                                                            
className="flex items-center gap-1 border border-orange-200 bg-orange-50 px-2.5 
py-1 dark:border-orange-800 dark:bg-orange-900/30"
-                                                                            
style={{
-                                                                               
 borderRadius:
-                                                                               
     "12px",
-                                                                            }}
-                                                                        >
-                                                                            
<div className="h-1.5 w-1.5 animate-pulse rounded-full bg-orange-500"></div>
-                                                                            
<span className="text-xs font-medium text-orange-700 dark:text-orange-300">
-                                                                               
 Migrating{" "}
-                                                                               
 {
-                                                                               
     shard.migratingSlot
-                                                                               
 }
-                                                                            
</span>
-                                                                        </div>
-                                                                    )}
+                                                                
{shard.hasMigration && (
+                                                                    <div
+                                                                        
className="flex items-center gap-1 border border-orange-200 bg-orange-50 px-2.5 
py-1 dark:border-orange-800 dark:bg-orange-900/30"
+                                                                        
style={{
+                                                                            
borderRadius: "12px",
+                                                                        }}
+                                                                    >
+                                                                        <div 
className="h-1.5 w-1.5 animate-pulse rounded-full bg-orange-500"></div>
+                                                                        <span 
className="text-xs font-medium text-orange-700 dark:text-orange-300">
+                                                                            
Migrating{" "}
+                                                                            
{shard.migratingSlot}
+                                                                        </span>
+                                                                    </div>
+                                                                )}
 
                                                                 
{!shard.hasMigration &&
                                                                     
!shard.hasImporting && (
@@ -1124,24 +1188,20 @@ export default function Cluster({ params }: { params: { 
namespace: string; clust
                                                                         </div>
                                                                     )}
 
-                                                                
{shard.hasImporting &&
-                                                                    
shard.importingSlot >= 0 && (
-                                                                        <div
-                                                                            
className="flex items-center gap-1 border border-blue-200 bg-blue-50 px-2.5 
py-1 dark:border-blue-800 dark:bg-blue-900/30"
-                                                                            
style={{
-                                                                               
 borderRadius:
-                                                                               
     "12px",
-                                                                            }}
-                                                                        >
-                                                                            
<div className="h-1.5 w-1.5 animate-pulse rounded-full bg-blue-500"></div>
-                                                                            
<span className="text-xs font-medium text-blue-700 dark:text-blue-300">
-                                                                               
 Importing{" "}
-                                                                               
 {
-                                                                               
     shard.importingSlot
-                                                                               
 }
-                                                                            
</span>
-                                                                        </div>
-                                                                    )}
+                                                                
{shard.hasImporting && (
+                                                                    <div
+                                                                        
className="flex items-center gap-1 border border-blue-200 bg-blue-50 px-2.5 
py-1 dark:border-blue-800 dark:bg-blue-900/30"
+                                                                        
style={{
+                                                                            
borderRadius: "12px",
+                                                                        }}
+                                                                    >
+                                                                        <div 
className="h-1.5 w-1.5 animate-pulse rounded-full bg-blue-500"></div>
+                                                                        <span 
className="text-xs font-medium text-blue-700 dark:text-blue-300">
+                                                                            
Importing{" "}
+                                                                            
{shard.importingSlot}
+                                                                        </span>
+                                                                    </div>
+                                                                )}
                                                             </div>
 
                                                             <div 
className="mt-2 space-y-1">
@@ -1471,6 +1531,15 @@ export default function Cluster({ params }: { params: { 
namespace: string; clust
                     </Paper>
                 </Box>
             </div>
+
+            <MigrationDialog
+                open={migrationDialogOpen}
+                onClose={() => setMigrationDialogOpen(false)}
+                namespace={namespace}
+                cluster={cluster}
+                shards={shardsData}
+                onSuccess={refreshShardData}
+            />
         </div>
     );
 }
diff --git a/webui/src/app/ui/formCreation.tsx 
b/webui/src/app/ui/formCreation.tsx
index b4d7e89..ad0d9de 100644
--- a/webui/src/app/ui/formCreation.tsx
+++ b/webui/src/app/ui/formCreation.tsx
@@ -369,7 +369,7 @@ export const MigrateSlot: React.FC<ShardFormProps> = ({ 
position, namespace, clu
             console.error("Error validating migration:", error);
         }
 
-        const response = await migrateSlot(namespace, cluster, target, slot, 
slotOnly);
+        const response = await migrateSlot(namespace, cluster, target, 
slot.toString(), slotOnly);
         if (response === "") {
             window.location.reload();
         } else {
diff --git a/webui/src/app/ui/migrationDialog.tsx 
b/webui/src/app/ui/migrationDialog.tsx
new file mode 100644
index 0000000..b823c3c
--- /dev/null
+++ b/webui/src/app/ui/migrationDialog.tsx
@@ -0,0 +1,484 @@
+/*
+ * 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,
+    TextField,
+    Switch,
+    alpha,
+    useTheme,
+} from "@mui/material";
+import MoveUpIcon from "@mui/icons-material/MoveUp";
+import StorageIcon from "@mui/icons-material/Storage";
+import DeviceHubIcon from "@mui/icons-material/DeviceHub";
+import { migrateSlot } from "@/app/lib/api";
+
+interface Shard {
+    index: number;
+    nodes: any[];
+    slotRanges: string[];
+    migratingSlot: string;
+    importingSlot: string;
+    targetShardIndex: number;
+    nodeCount: number;
+    hasSlots: boolean;
+    hasMigration: boolean;
+    hasImporting: boolean;
+}
+
+interface MigrationDialogProps {
+    open: boolean;
+    onClose: () => void;
+    namespace: string;
+    cluster: string;
+    shards: Shard[];
+    onSuccess: () => void;
+}
+
+export const MigrationDialog: React.FC<MigrationDialogProps> = ({
+    open,
+    onClose,
+    namespace,
+    cluster,
+    shards,
+    onSuccess,
+}) => {
+    const [targetShardIndex, setTargetShardIndex] = useState<number>(-1);
+    const [slotNumber, setSlotNumber] = useState<string>("");
+    const [slotOnly, setSlotOnly] = useState<boolean>(false);
+    const [loading, setLoading] = useState(false);
+    const [error, setError] = useState<string>("");
+    const theme = useTheme();
+
+    const availableTargetShards = shards.filter((shard) => shard.hasSlots);
+
+    const validateSlotInput = (input: string): { isValid: boolean; error?: 
string } => {
+        if (!input.trim()) {
+            return { isValid: false, error: "Please enter a slot number or 
range" };
+        }
+
+        // Check if it's a range (contains dash)
+        if (input.includes("-")) {
+            const parts = input.split("-");
+            if (parts.length !== 2) {
+                return {
+                    isValid: false,
+                    error: "Invalid range format. Use format: start-end (e.g., 
100-200)",
+                };
+            }
+
+            const start = parseInt(parts[0].trim());
+            const end = parseInt(parts[1].trim());
+
+            if (isNaN(start) || isNaN(end)) {
+                return {
+                    isValid: false,
+                    error: "Both start and end of range must be valid numbers",
+                };
+            }
+
+            if (start < 0 || end > 16383 || start > 16383 || end < 0) {
+                return { isValid: false, error: "Slot numbers must be between 
0 and 16383" };
+            }
+
+            if (start > end) {
+                return {
+                    isValid: false,
+                    error: "Start slot must be less than or equal to end slot",
+                };
+            }
+
+            return { isValid: true };
+        } else {
+            const slot = parseInt(input.trim());
+            if (isNaN(slot) || slot < 0 || slot > 16383) {
+                return { isValid: false, error: "Slot number must be between 0 
and 16383" };
+            }
+            return { isValid: true };
+        }
+    };
+
+    const handleMigration = async () => {
+        if (targetShardIndex === -1 || !slotNumber.trim()) {
+            setError("Please select a target shard and enter a slot number or 
range");
+            return;
+        }
+
+        // Validate slot input
+        const validation = validateSlotInput(slotNumber);
+        if (!validation.isValid) {
+            setError(validation.error || "Invalid slot input");
+            return;
+        }
+
+        setLoading(true);
+        setError("");
+
+        try {
+            const result = await migrateSlot(
+                namespace,
+                cluster,
+                targetShardIndex,
+                slotNumber.trim(),
+                slotOnly
+            );
+
+            if (result) {
+                setError(result);
+            } else {
+                onSuccess();
+                onClose();
+                resetForm();
+            }
+        } catch (err) {
+            setError("An unexpected error occurred during migration");
+        } finally {
+            setLoading(false);
+        }
+    };
+
+    const resetForm = () => {
+        setTargetShardIndex(-1);
+        setSlotNumber("");
+        setSlotOnly(false);
+        setError("");
+    };
+
+    const handleClose = () => {
+        if (!loading) {
+            onClose();
+            resetForm();
+        }
+    };
+
+    const getSlotRangeDisplay = (slotRanges: string[]) => {
+        if (!slotRanges || slotRanges.length === 0) return "No slots";
+        return slotRanges.join(", ");
+    };
+
+    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}>
+                        <MoveUpIcon className="text-primary" sx={{ fontSize: 
28 }} />
+                        <Box>
+                            <Typography
+                                variant="h6"
+                                className="font-semibold text-gray-800 
dark:text-gray-100"
+                            >
+                                Migrate Slot
+                            </Typography>
+                            <Typography
+                                variant="body2"
+                                className="text-gray-500 dark:text-gray-400"
+                            >
+                                Move a slot to a different shard
+                            </Typography>
+                        </Box>
+                    </Box>
+                </DialogTitle>
+
+                <DialogContent sx={{ padding: "24px" }}>
+                    <Box mb={1} mt={1}>
+                        <TextField
+                            label="Slot or Slot Range"
+                            value={slotNumber}
+                            onChange={(e) => setSlotNumber(e.target.value)}
+                            fullWidth
+                            variant="outlined"
+                            placeholder="e.g., 123 or 100-200"
+                            helperText="Enter a single slot (123) or slot 
range (100-200). Slots must be between 0 and 16383"
+                            sx={{
+                                "& .MuiOutlinedInput-root": {
+                                    borderRadius: "16px",
+                                    "&.Mui-focused": {
+                                        boxShadow: `0 0 0 2px 
${alpha(theme.palette.primary.main, 0.2)}`,
+                                    },
+                                },
+                            }}
+                        />
+                    </Box>
+
+                    <Box mb={1}>
+                        <FormControlLabel
+                            control={
+                                <Switch
+                                    checked={slotOnly}
+                                    onChange={(e) => 
setSlotOnly(e.target.checked)}
+                                    sx={{
+                                        "& .MuiSwitch-switchBase.Mui-checked": 
{
+                                            color: theme.palette.primary.main,
+                                        },
+                                        "& .MuiSwitch-switchBase.Mui-checked + 
.MuiSwitch-track": {
+                                            backgroundColor: 
theme.palette.primary.main,
+                                        },
+                                    }}
+                                />
+                            }
+                            label={
+                                <Box>
+                                    <Typography variant="body1" 
className="font-medium">
+                                        Slot-only migration
+                                    </Typography>
+                                    <Typography variant="body2" 
className="text-gray-500">
+                                        Migrate only the slot without data
+                                    </Typography>
+                                </Box>
+                            }
+                        />
+                    </Box>
+
+                    {availableTargetShards.length > 0 ? (
+                        <Box>
+                            <Typography
+                                variant="subtitle1"
+                                className="mb-3 font-medium text-gray-700 
dark:text-gray-300"
+                            >
+                                Select Target Shard
+                            </Typography>
+                            <FormControl component="fieldset" fullWidth>
+                                <RadioGroup
+                                    value={targetShardIndex.toString()}
+                                    onChange={(e) => 
setTargetShardIndex(parseInt(e.target.value))}
+                                >
+                                    {availableTargetShards.map((shard) => (
+                                        <FormControlLabel
+                                            key={shard.index}
+                                            value={shard.index.toString()}
+                                            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}
+                                                >
+                                                    <StorageIcon 
className="text-info" />
+                                                    <Box flex={1}>
+                                                        <Typography
+                                                            variant="body1"
+                                                            
className="font-medium"
+                                                        >
+                                                            Shard {shard.index}
+                                                        </Typography>
+                                                        <Typography
+                                                            variant="body2"
+                                                            
className="text-gray-500"
+                                                        >
+                                                            Slots:{" "}
+                                                            
{getSlotRangeDisplay(shard.slotRanges)}
+                                                        </Typography>
+                                                        <Typography
+                                                            variant="body2"
+                                                            
className="text-gray-500"
+                                                        >
+                                                            Nodes: 
{shard.nodeCount}
+                                                        </Typography>
+                                                    </Box>
+                                                    <Box display="flex" 
gap={1} flexWrap="wrap">
+                                                        <Chip
+                                                            
label={`${shard.nodeCount} nodes`}
+                                                            size="small"
+                                                            
className="bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200"
+                                                        />
+                                                        {shard.hasMigration && 
(
+                                                            <Chip
+                                                                
label="Migrating"
+                                                                size="small"
+                                                                
className="bg-orange-100 text-orange-800 dark:bg-orange-900 
dark:text-orange-200"
+                                                            />
+                                                        )}
+                                                        {shard.hasImporting && 
(
+                                                            <Chip
+                                                                
label="Importing"
+                                                                size="small"
+                                                                
className="bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200"
+                                                            />
+                                                        )}
+                                                    </Box>
+                                                </Box>
+                                            }
+                                            sx={{
+                                                p: 2,
+                                                m: 0,
+                                                mb: 2,
+                                                border: `1px solid ${
+                                                    targetShardIndex === 
shard.index
+                                                        ? 
theme.palette.primary.main
+                                                        : 
theme.palette.grey[300]
+                                                }`,
+                                                borderRadius: "16px",
+                                                backgroundColor:
+                                                    targetShardIndex === 
shard.index
+                                                        ? 
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 target shards available for migration. At least 
one shard with slots
+                            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={handleMigration}
+                        variant="contained"
+                        disabled={
+                            loading ||
+                            availableTargetShards.length === 0 ||
+                            targetShardIndex === -1 ||
+                            !slotNumber.trim()
+                        }
+                        startIcon={loading ? <CircularProgress size={16} /> : 
<MoveUpIcon />}
+                        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 Migration"}
+                    </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>
+        </>
+    );
+};


Reply via email to