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 9926049 feat(webui/node): redesign node detailed page (#330)
9926049 is described below
commit 9926049a3ad066b077e41797dc045babd482821f
Author: Agnik Misra <[email protected]>
AuthorDate: Thu Jul 31 07:51:35 2025 +0530
feat(webui/node): redesign node detailed page (#330)
---
webui/src/app/layout.tsx | 5 +-
.../[cluster]/shards/[shard]/nodes/[node]/page.tsx | 465 ++++++++++++---------
webui/src/app/ui/sidebar.tsx | 134 +++---
3 files changed, 351 insertions(+), 253 deletions(-)
diff --git a/webui/src/app/layout.tsx b/webui/src/app/layout.tsx
index 6059eb0..f235d88 100644
--- a/webui/src/app/layout.tsx
+++ b/webui/src/app/layout.tsx
@@ -40,7 +40,10 @@ export default function RootLayout({
}>) {
return (
<html lang="en" suppressHydrationWarning>
- <body className={`${inter.className} min-h-screen bg-light
dark:bg-dark`}>
+ <body
+ className={`${inter.className} min-h-screen bg-light
dark:bg-dark`}
+ suppressHydrationWarning
+ >
<ThemeProvider>
<Banner />
<Container
diff --git
a/webui/src/app/namespaces/[namespace]/clusters/[cluster]/shards/[shard]/nodes/[node]/page.tsx
b/webui/src/app/namespaces/[namespace]/clusters/[cluster]/shards/[shard]/nodes/[node]/page.tsx
index 862fb33..3b8ea07 100644
---
a/webui/src/app/namespaces/[namespace]/clusters/[cluster]/shards/[shard]/nodes/[node]/page.tsx
+++
b/webui/src/app/namespaces/[namespace]/clusters/[cluster]/shards/[shard]/nodes/[node]/page.tsx
@@ -21,7 +21,7 @@
import { listNodes } from "@/app/lib/api";
import { NodeSidebar } from "@/app/ui/sidebar";
-import { Box, Typography, Chip, Paper, Divider, Grid, Alert } from
"@mui/material";
+import { Box, Typography, Chip, Paper, Divider, Grid, Alert, IconButton } from
"@mui/material";
import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { LoadingSpinner } from "@/app/ui/loadingSpinner";
@@ -33,6 +33,11 @@ import AccessTimeIcon from "@mui/icons-material/AccessTime";
import CheckCircleIcon from "@mui/icons-material/CheckCircle";
import StorageIcon from "@mui/icons-material/Storage";
import DnsIcon from "@mui/icons-material/Dns";
+import InfoIcon from "@mui/icons-material/Info";
+import SettingsIcon from "@mui/icons-material/Settings";
+import NetworkCheckIcon from "@mui/icons-material/NetworkCheck";
+import SecurityIcon from "@mui/icons-material/Security";
+import LinkIcon from "@mui/icons-material/Link";
export default function Node({
params,
@@ -90,12 +95,18 @@ export default function Node({
color: "success",
textClass: "text-success font-medium",
icon: <CheckCircleIcon fontSize="small" className="mr-1" />,
+ bgClass: "bg-green-50 dark:bg-green-900/30",
+ borderClass: "border-green-200 dark:border-green-800",
+ textColor: "text-green-700 dark:text-green-300",
};
}
return {
color: "info",
textClass: "text-info font-medium",
icon: <DeviceHubIcon fontSize="small" className="mr-1" />,
+ bgClass: "bg-blue-50 dark:bg-blue-900/30",
+ borderClass: "border-blue-200 dark:border-blue-800",
+ textColor: "text-blue-700 dark:text-blue-300",
};
};
@@ -106,259 +117,305 @@ export default function Node({
};
const formattedDate = new Date(currentNode.created_at *
1000).toLocaleString();
+ const roleStyles = getRoleStyles(currentNode.role);
return (
<div className="flex h-full">
<NodeSidebar namespace={namespace} cluster={cluster} shard={shard}
/>
- <div className="flex-1 overflow-auto">
- <Box className="container-inner">
- <Box className="mb-6 flex items-center justify-between">
+ <div className="no-scrollbar flex-1 overflow-y-auto bg-white pb-8
dark:bg-dark">
+ <Box className="px-6 py-4 sm:px-8 sm:py-6">
+ {/* Header Section */}
+ <div className="mb-6 flex flex-col gap-4 sm:mb-8
lg:flex-row lg:items-center lg:justify-between">
<div>
<Typography
- variant="h5"
- className="flex items-center font-medium
text-gray-800 dark:text-gray-100"
+ variant="h4"
+ className="flex items-center font-medium
text-gray-900 dark:text-white"
>
- <DeviceHubIcon className="mr-2 text-primary
dark:text-primary-light" />
+ <div className="mr-3 flex h-12 w-12
items-center justify-center rounded-2xl bg-gradient-to-br from-blue-50
to-indigo-50 text-blue-500 dark:from-blue-900/30 dark:to-indigo-900/30
dark:text-blue-400">
+ <DeviceHubIcon sx={{ fontSize: 28 }} />
+ </div>
Node {parseInt(node) + 1}
- <Chip
- label={currentNode.role}
- size="small"
-
color={getRoleStyles(currentNode.role).color as any}
- className="ml-3"
- icon={getRoleStyles(currentNode.role).icon}
- />
+ <div
+ className={`ml-3 flex items-center gap-1
rounded-full border px-3 py-1 ${roleStyles.bgClass} ${roleStyles.borderClass}`}
+ >
+ {roleStyles.icon}
+ <span className={`text-sm font-medium
${roleStyles.textColor}`}>
+ {currentNode.role}
+ </span>
+ </div>
</Typography>
<Typography
- variant="body2"
- className="mt-1 text-gray-500
dark:text-gray-400"
+ variant="body1"
+ className="mt-2 text-gray-500
dark:text-gray-400"
>
- Shard {parseInt(shard) + 1}, {cluster}
cluster, {namespace}{" "}
+ Shard {parseInt(shard) + 1} • {cluster}
cluster • {namespace}{" "}
namespace
</Typography>
</div>
- </Box>
+ </div>
- <Paper className="mb-6 rounded-lg border
border-light-border bg-white p-6 shadow-card dark:border-dark-border
dark:bg-dark-paper">
- <Typography variant="h6" className="mb-4 flex
items-center font-medium">
- <StorageIcon fontSize="small" className="mr-2" />
- Node Details
- </Typography>
- <Divider className="mb-4" />
+ {/* Node Details Section */}
+ <Paper
+ elevation={0}
+ className="overflow-hidden rounded-2xl border
border-gray-100 transition-all hover:shadow-md dark:border-gray-800
dark:bg-dark-paper"
+ >
+ <div className="border-b border-gray-100 px-6 py-4
dark:border-gray-800 sm:px-8">
+ <Typography
+ variant="h6"
+ className="flex items-center font-medium
text-gray-800 dark:text-gray-100"
+ >
+ <SettingsIcon className="mr-2 text-primary
dark:text-primary-light" />
+ Node Configuration
+ </Typography>
+ </div>
- <Grid container spacing={3}>
- <Grid item xs={12} md={6}>
- <div className="space-y-4">
- <div>
- <Typography
- variant="subtitle2"
- className="mb-1 text-gray-500
dark:text-gray-400"
- >
- ID
- </Typography>
- <div className="flex items-center">
+ <div className="p-6 sm:p-8">
+ <Grid container spacing={4}>
+ <Grid item xs={12} lg={6}>
+ <div className="space-y-6">
+ <div>
<Typography
- variant="body1"
- className="flex-1
overflow-hidden text-ellipsis rounded bg-gray-50 px-3 py-2 font-mono
dark:bg-dark-border"
+ variant="subtitle2"
+ className="mb-2 flex
items-center text-gray-500 dark:text-gray-400"
>
- {currentNode.id}
+ <LinkIcon fontSize="small"
className="mr-1" />
+ Node ID
</Typography>
- <IconButton
- onClick={() =>
-
copyToClipboard(currentNode.id, "id")
- }
- className="ml-2 text-gray-500
hover:text-primary"
- title="Copy ID"
+ <div className="flex items-center">
+ <div className="flex-1
overflow-hidden rounded-xl bg-gray-50 px-4 py-3 font-mono text-sm
dark:bg-gray-800/50">
+ <Typography
+ variant="body1"
+ className="truncate
text-gray-800 dark:text-gray-200"
+ >
+ {currentNode.id}
+ </Typography>
+ </div>
+ <IconButton
+ onClick={() =>
+
copyToClipboard(currentNode.id, "id")
+ }
+ className="ml-3
rounded-full bg-gray-100 p-2 text-gray-500 transition-all hover:bg-gray-200
hover:text-primary dark:bg-gray-700 dark:text-gray-400 dark:hover:bg-gray-600
dark:hover:text-primary-light"
+ title="Copy ID"
+ >
+ {copied === "id" ? (
+ <CheckCircleIcon
+ fontSize="small"
+
className="text-success"
+ />
+ ) : (
+ <ContentCopyIcon
fontSize="small" />
+ )}
+ </IconButton>
+ </div>
+ </div>
+
+ <div>
+ <Typography
+ variant="subtitle2"
+ className="mb-2 flex
items-center text-gray-500 dark:text-gray-400"
>
- {copied === "id" ? (
- <CheckCircleIcon
- fontSize="small"
-
className="text-success"
- />
- ) : (
- <ContentCopyIcon
fontSize="small" />
- )}
- </IconButton>
+ <NetworkCheckIcon
+ fontSize="small"
+ className="mr-1"
+ />
+ Address
+ </Typography>
+ <div className="flex items-center">
+ <div className="flex-1
overflow-hidden rounded-xl bg-gray-50 px-4 py-3 dark:bg-gray-800/50">
+ <Typography
+ variant="body1"
+
className="text-gray-800 dark:text-gray-200"
+ >
+ {currentNode.addr}
+ </Typography>
+ </div>
+ <IconButton
+ onClick={() =>
+
copyToClipboard(currentNode.addr, "addr")
+ }
+ className="ml-3
rounded-full bg-gray-100 p-2 text-gray-500 transition-all hover:bg-gray-200
hover:text-primary dark:bg-gray-700 dark:text-gray-400 dark:hover:bg-gray-600
dark:hover:text-primary-light"
+ title="Copy Address"
+ >
+ {copied === "addr" ? (
+ <CheckCircleIcon
+ fontSize="small"
+
className="text-success"
+ />
+ ) : (
+ <ContentCopyIcon
fontSize="small" />
+ )}
+ </IconButton>
+ </div>
</div>
</div>
+ </Grid>
- <div>
- <Typography
- variant="subtitle2"
- className="mb-1 text-gray-500
dark:text-gray-400"
- >
- Address
- </Typography>
- <div className="flex items-center">
+ <Grid item xs={12} lg={6}>
+ <div className="space-y-6">
+ <div>
<Typography
- variant="body1"
- className="flex-1 rounded
bg-gray-50 px-3 py-2 dark:bg-dark-border"
+ variant="subtitle2"
+ className="mb-2 flex
items-center text-gray-500 dark:text-gray-400"
>
- {currentNode.addr}
+ <DeviceHubIcon
fontSize="small" className="mr-1" />
+ Role
</Typography>
- <IconButton
- onClick={() =>
-
copyToClipboard(currentNode.addr, "addr")
- }
- className="ml-2 text-gray-500
hover:text-primary"
- title="Copy Address"
+ <div
+ className={`mt-1 inline-flex
items-center rounded-xl border px-3 py-2 ${roleStyles.bgClass}
${roleStyles.borderClass}`}
>
- {copied === "addr" ? (
- <CheckCircleIcon
+ {roleStyles.icon}
+ <Typography
+ variant="body1"
+ className={`font-medium
${roleStyles.textColor}`}
+ >
+ {currentNode.role}
+ </Typography>
+ </div>
+ </div>
+
+ <div>
+ <Typography
+ variant="subtitle2"
+ className="mb-2 flex
items-center text-gray-500 dark:text-gray-400"
+ >
+ <AccessTimeIcon
fontSize="small" className="mr-1" />
+ Created At
+ </Typography>
+ <div className="flex items-center
rounded-xl bg-gray-50 px-4 py-3 dark:bg-gray-800/50">
+ <Typography
+ variant="body1"
+ className="text-gray-800
dark:text-gray-200"
+ >
+ {formattedDate}
+ </Typography>
+ </div>
+ </div>
+
+ {currentNode.password && (
+ <div>
+ <Typography
+ variant="subtitle2"
+ className="mb-2 flex
items-center text-gray-500 dark:text-gray-400"
+ >
+ <SecurityIcon
fontSize="small"
-
className="text-success"
+ className="mr-1"
/>
- ) : (
- <ContentCopyIcon
fontSize="small" />
- )}
- </IconButton>
- </div>
+ Authentication
+ </Typography>
+ <div className="flex
items-center">
+ <div className="flex-1
rounded-xl bg-gray-50 px-4 py-3 dark:bg-gray-800/50">
+ <Typography
+ variant="body2"
+
className="font-mono text-gray-800 dark:text-gray-200"
+ >
+
{currentNode.password
+ ? "••••••••"
+ : "No password
set"}
+ </Typography>
+ </div>
+ <IconButton
+ onClick={() =>
+ copyToClipboard(
+
currentNode.password,
+ "pwd"
+ )
+ }
+ className="ml-3
rounded-full bg-gray-100 p-2 text-gray-500 transition-all hover:bg-gray-200
hover:text-primary dark:bg-gray-700 dark:text-gray-400 dark:hover:bg-gray-600
dark:hover:text-primary-light"
+ title="Copy Password"
+
disabled={!currentNode.password}
+ >
+ {copied === "pwd" ? (
+ <CheckCircleIcon
+
fontSize="small"
+
className="text-success"
+ />
+ ) : (
+ <LockIcon
fontSize="small" />
+ )}
+ </IconButton>
+ </div>
+ </div>
+ )}
</div>
- </div>
+ </Grid>
</Grid>
+ </div>
+ </Paper>
+
+ {/* Shard Information Section */}
+ <Paper
+ elevation={0}
+ className="mt-6 overflow-hidden rounded-2xl border
border-gray-100 transition-all hover:shadow-md dark:border-gray-800
dark:bg-dark-paper"
+ >
+ <div className="border-b border-gray-100 px-6 py-4
dark:border-gray-800 sm:px-8">
+ <Typography
+ variant="h6"
+ className="flex items-center font-medium
text-gray-800 dark:text-gray-100"
+ >
+ <DnsIcon className="mr-2 text-primary
dark:text-primary-light" />
+ Shard Information
+ </Typography>
+ </div>
- <Grid item xs={12} md={6}>
- <div className="space-y-4">
- <div>
+ <div className="p-6 sm:p-8">
+ <Grid container spacing={4}>
+ <Grid item xs={12} sm={4}>
+ <div className="rounded-xl border
border-gray-100 bg-gray-50 p-4 dark:border-gray-800 dark:bg-gray-800/50">
<Typography
variant="subtitle2"
- className="mb-1 text-gray-500
dark:text-gray-400"
+ className="mb-2 flex items-center
text-gray-500 dark:text-gray-400"
>
- Role
+ <DnsIcon fontSize="small"
className="mr-1" />
+ Shard
</Typography>
<Typography
- variant="body1"
-
className={`${getRoleStyles(currentNode.role).textClass} flex items-center`}
+ variant="h6"
+ className="font-semibold
text-gray-900 dark:text-white"
>
-
{getRoleStyles(currentNode.role).icon}{" "}
- {currentNode.role}
+ Shard {parseInt(shard) + 1}
</Typography>
</div>
-
- <div>
+ </Grid>
+ <Grid item xs={12} sm={4}>
+ <div className="rounded-xl border
border-gray-100 bg-gray-50 p-4 dark:border-gray-800 dark:bg-gray-800/50">
<Typography
variant="subtitle2"
- className="mb-1 text-gray-500
dark:text-gray-400"
+ className="mb-2 flex items-center
text-gray-500 dark:text-gray-400"
>
- Created At
+ <StorageIcon fontSize="small"
className="mr-1" />
+ Cluster
</Typography>
- <Typography variant="body1"
className="flex items-center">
- <AccessTimeIcon
- fontSize="small"
- className="mr-1 text-gray-500"
- />
- {formattedDate}
+ <Typography
+ variant="h6"
+ className="font-semibold
text-gray-900 dark:text-white"
+ >
+ {cluster}
</Typography>
</div>
-
- {currentNode.password && (
- <div>
- <Typography
- variant="subtitle2"
- className="mb-1 text-gray-500
dark:text-gray-400"
- >
- Authentication
- </Typography>
- <div className="flex items-center">
- <Typography
- variant="body2"
- className="flex-1 rounded
bg-gray-50 px-3 py-2 font-mono dark:bg-dark-border"
- >
- {currentNode.password
- ? "••••••••"
- : "No password set"}
- </Typography>
- <IconButton
- onClick={() =>
-
copyToClipboard(currentNode.password, "pwd")
- }
- className="ml-2
text-gray-500 hover:text-primary"
- title="Copy Password"
-
disabled={!currentNode.password}
- >
- {copied === "pwd" ? (
- <CheckCircleIcon
- fontSize="small"
-
className="text-success"
- />
- ) : (
- <LockIcon
fontSize="small" />
- )}
- </IconButton>
- </div>
- </div>
- )}
- </div>
- </Grid>
- </Grid>
- </Paper>
-
- <Paper className="rounded-lg border border-light-border
bg-white p-6 shadow-card dark:border-dark-border dark:bg-dark-paper">
- <Typography variant="h6" className="mb-4 flex
items-center font-medium">
- <DnsIcon fontSize="small" className="mr-2" />
- Shard Information
- </Typography>
- <Divider className="mb-4" />
-
- <Grid container spacing={3}>
- <Grid item xs={12} md={4}>
- <Typography
- variant="subtitle2"
- className="mb-1 text-gray-500
dark:text-gray-400"
- >
- Shard
- </Typography>
- <Typography variant="body1">Shard
{parseInt(shard) + 1}</Typography>
- </Grid>
- <Grid item xs={12} md={4}>
- <Typography
- variant="subtitle2"
- className="mb-1 text-gray-500
dark:text-gray-400"
- >
- Cluster
- </Typography>
- <Typography
variant="body1">{cluster}</Typography>
- </Grid>
- <Grid item xs={12} md={4}>
- <Typography
- variant="subtitle2"
- className="mb-1 text-gray-500
dark:text-gray-400"
- >
- Namespace
- </Typography>
- <Typography
variant="body1">{namespace}</Typography>
+ </Grid>
+ <Grid item xs={12} sm={4}>
+ <div className="rounded-xl border
border-gray-100 bg-gray-50 p-4 dark:border-gray-800 dark:bg-gray-800/50">
+ <Typography
+ variant="subtitle2"
+ className="mb-2 flex items-center
text-gray-500 dark:text-gray-400"
+ >
+ <InfoIcon fontSize="small"
className="mr-1" />
+ Namespace
+ </Typography>
+ <Typography
+ variant="h6"
+ className="font-semibold
text-gray-900 dark:text-white"
+ >
+ {namespace}
+ </Typography>
+ </div>
+ </Grid>
</Grid>
- </Grid>
+ </div>
</Paper>
</Box>
</div>
</div>
);
}
-
-interface IconButtonProps {
- onClick: () => void;
- className?: string;
- title?: string;
- disabled?: boolean;
- children: React.ReactNode;
-}
-
-// Custom IconButton component
-const IconButton: React.FC<IconButtonProps> = ({
- onClick,
- className = "",
- title,
- disabled = false,
- children,
-}) => {
- return (
- <button
- onClick={onClick}
- disabled={disabled}
- className={`flex h-8 w-8 items-center justify-center rounded-full
hover:bg-gray-100 dark:hover:bg-dark-border ${disabled ? "cursor-not-allowed
opacity-50" : ""} ${className}`}
- title={title}
- >
- {children}
- </button>
- );
-};
diff --git a/webui/src/app/ui/sidebar.tsx b/webui/src/app/ui/sidebar.tsx
index dcef803..abc25db 100644
--- a/webui/src/app/ui/sidebar.tsx
+++ b/webui/src/app/ui/sidebar.tsx
@@ -418,6 +418,18 @@ export function NodeSidebar({
const [nodes, setNodes] = useState<NodeItem[]>([]);
const [error, setError] = useState<string | null>(null);
const [isOpen, setIsOpen] = useState(true);
+ const [sidebarWidth, setSidebarWidth] = useState(260);
+ const [isMobile, setIsMobile] = useState(false);
+
+ useEffect(() => {
+ const checkMobile = () => {
+ setIsMobile(window.innerWidth < 768);
+ };
+
+ checkMobile();
+ window.addEventListener("resize", checkMobile);
+ return () => window.removeEventListener("resize", checkMobile);
+ }, []);
useEffect(() => {
const fetchData = async () => {
@@ -431,64 +443,90 @@ export function NodeSidebar({
fetchData();
}, [namespace, cluster, shard]);
+ const toggleSidebar = () => {
+ if (isMobile) {
+ setSidebarWidth(isOpen ? 0 : 260);
+ }
+ setIsOpen(!isOpen);
+ };
+
return (
<Paper
- className="flex h-full w-64 flex-col overflow-hidden border-r
border-light-border/50 bg-white/90 backdrop-blur-sm dark:border-dark-border/50
dark:bg-dark-paper/90"
+ className="sidebar-container flex h-full flex-col overflow-hidden
border-r border-light-border/50 bg-white/90 backdrop-blur-sm transition-all
duration-300 dark:border-dark-border/50 dark:bg-dark-paper/90"
elevation={0}
sx={{
+ width: `${sidebarWidth}px`,
+ minWidth: isMobile ? 0 : "260px",
+ maxWidth: "260px",
borderTopRightRadius: "16px",
borderBottomRightRadius: "16px",
boxShadow: "4px 0 15px rgba(0, 0, 0, 0.03)",
+ transition: "width 0.3s cubic-bezier(0.4, 0, 0.2, 1)",
}}
>
- <Box className="p-4 pb-2">
- <NodeCreation
- namespace={namespace}
- cluster={cluster}
- shard={shard}
- position="sidebar"
- />
- </Box>
-
- <Box className="px-4 py-2">
- <SidebarHeader
- title="Nodes"
- count={nodes.length}
- isOpen={isOpen}
- toggleOpen={() => setIsOpen(!isOpen)}
- icon={<DeviceHubIcon fontSize="small" />}
- />
- </Box>
-
- <Collapse in={isOpen} className="flex-1 overflow-hidden">
- <div className="h-full overflow-hidden px-4">
- <div className="custom-scrollbar max-h-[calc(100vh-180px)]
overflow-y-auto rounded-xl bg-gray-50/50 p-2 dark:bg-dark-border/20">
- {error && (
- <div className="my-2 rounded-lg bg-red-50 p-2
text-center text-sm text-red-600 dark:bg-red-900/20 dark:text-red-400">
- {error}
- </div>
- )}
- <List className="p-0">
- {nodes.map((node, index) => (
- <Link
-
href={`/namespaces/${namespace}/clusters/${cluster}/shards/${shard}/nodes/${index}`}
- passHref
- key={index}
- >
- <Item
- type="node"
- item={`Node\t${index + 1}`}
- id={node.id}
- namespace={namespace}
- cluster={cluster}
- shard={shard}
- />
- </Link>
- ))}
- </List>
+ {isMobile && (
+ <button
+ onClick={toggleSidebar}
+ className="sidebar-toggle-btn absolute -right-10 top-4
z-50 flex h-9 w-9 items-center justify-center rounded-full bg-white
text-gray-600 shadow-lg transition-all hover:bg-gray-50 dark:bg-dark-paper
dark:text-gray-300 dark:hover:bg-dark-border"
+ >
+ {isOpen ? (
+ <ChevronRightIcon />
+ ) : (
+ <ChevronRightIcon sx={{ transform: "rotate(180deg)" }}
/>
+ )}
+ </button>
+ )}
+
+ <div className="sidebar-inner w-[260px]">
+ <Box className="p-4 pb-2">
+ <NodeCreation
+ namespace={namespace}
+ cluster={cluster}
+ shard={shard}
+ position="sidebar"
+ />
+ </Box>
+
+ <Box className="px-4 py-2">
+ <SidebarHeader
+ title="Nodes"
+ count={nodes.length}
+ isOpen={isOpen}
+ toggleOpen={toggleSidebar}
+ icon={<DeviceHubIcon fontSize="small" />}
+ />
+ </Box>
+
+ <Collapse in={isOpen} className="flex-1 overflow-hidden">
+ <div className="h-full overflow-hidden px-4">
+ <div className="sidebar-scrollbar
max-h-[calc(100vh-200px)] overflow-y-auto rounded-xl bg-gray-50/50 p-2
dark:bg-dark-border/20">
+ {error && (
+ <div className="my-2 rounded-lg bg-red-50 p-2
text-center text-sm text-red-600 dark:bg-red-900/20 dark:text-red-400">
+ {error}
+ </div>
+ )}
+ <List className="p-0">
+ {nodes.map((node, index) => (
+ <Link
+
href={`/namespaces/${namespace}/clusters/${cluster}/shards/${shard}/nodes/${index}`}
+ passHref
+ key={index}
+ >
+ <Item
+ type="node"
+ item={`Node\t${index + 1}`}
+ id={node.id}
+ namespace={namespace}
+ cluster={cluster}
+ shard={shard}
+ />
+ </Link>
+ ))}
+ </List>
+ </div>
</div>
- </div>
- </Collapse>
+ </Collapse>
+ </div>
</Paper>
);
}
