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 99cea5c feat(webui/node): redesign node page (#327)
99cea5c is described below
commit 99cea5c584442619785d1ca42c8cc2e5fff8dc2e
Author: Agnik Misra <[email protected]>
AuthorDate: Thu Jul 24 17:56:42 2025 +0530
feat(webui/node): redesign node page (#327)
---
.../clusters/[cluster]/shards/[shard]/page.tsx | 592 ++++++++++++++++++---
webui/src/app/ui/formCreation.tsx | 13 +-
webui/src/app/ui/formDialog.tsx | 7 +-
webui/src/app/ui/sidebar.tsx | 115 ++--
4 files changed, 607 insertions(+), 120 deletions(-)
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 aa62a56..67f7c10 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
@@ -19,12 +19,26 @@
"use client";
-import { Box, Typography, Chip, Badge } from "@mui/material";
+import {
+ Box,
+ Typography,
+ Chip,
+ Paper,
+ Grid,
+ Button,
+ IconButton,
+ Tooltip,
+ Popover,
+ RadioGroup,
+ FormControlLabel,
+ Radio,
+ Fade,
+} from "@mui/material";
import { ShardSidebar } from "@/app/ui/sidebar";
import { fetchShard } from "@/app/lib/api";
import { useRouter } from "next/navigation";
import { useState, useEffect } from "react";
-import { AddNodeCard, ResourceCard } from "@/app/ui/createCard";
+import { AddNodeCard } from "@/app/ui/createCard";
import Link from "next/link";
import { LoadingSpinner } from "@/app/ui/loadingSpinner";
import { truncateText } from "@/app/utils";
@@ -34,6 +48,14 @@ import EmptyState from "@/app/ui/emptyState";
import AlarmIcon from "@mui/icons-material/Alarm";
import CheckCircleIcon from "@mui/icons-material/CheckCircle";
import RemoveCircleIcon from "@mui/icons-material/RemoveCircle";
+import SearchIcon from "@mui/icons-material/Search";
+import FilterListIcon from "@mui/icons-material/FilterList";
+import SortIcon from "@mui/icons-material/Sort";
+import CheckIcon from "@mui/icons-material/Check";
+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";
export default function Shard({
params,
@@ -43,6 +65,11 @@ export default function Shard({
const { namespace, cluster, shard } = params;
const [nodesData, setNodesData] = useState<any>(null);
const [loading, setLoading] = useState<boolean>(true);
+ 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 router = useRouter();
useEffect(() => {
@@ -61,7 +88,6 @@ export default function Shard({
setLoading(false);
}
};
-
fetchData();
}, [namespace, cluster, shard, router]);
@@ -73,7 +99,6 @@ export default function Shard({
const calculateUptime = (timestamp: number) => {
const now = Math.floor(Date.now() / 1000);
const uptimeSeconds = now - timestamp;
-
if (uptimeSeconds < 60) return `${uptimeSeconds} seconds`;
if (uptimeSeconds < 3600) return `${Math.floor(uptimeSeconds / 60)}
minutes`;
if (uptimeSeconds < 86400) return `${Math.floor(uptimeSeconds / 3600)}
hours`;
@@ -94,104 +119,515 @@ export default function Shard({
};
};
+ // Filtering and sorting logic for nodes
+ const filteredAndSortedNodes = (nodesData?.nodes || [])
+ .filter((node: any, idx: number) => {
+ if (!`node ${idx +
1}`.toLowerCase().includes(searchTerm.toLowerCase())) {
+ return false;
+ }
+ switch (filterOption) {
+ case "master":
+ return node.role === "master";
+ case "replica":
+ return node.role !== "master";
+ default:
+ return true;
+ }
+ })
+ .sort((a: any, b: any) => {
+ switch (sortOption) {
+ case "index-asc":
+ return a.index - b.index;
+ case "index-desc":
+ return b.index - a.index;
+ case "uptime-desc":
+ return b.created_at - a.created_at;
+ case "uptime-asc":
+ return a.created_at - b.created_at;
+ default:
+ return 0;
+ }
+ });
+
+ const isFilterOpen = Boolean(filterAnchorEl);
+ const isSortOpen = Boolean(sortAnchorEl);
+ const filterId = isFilterOpen ? "filter-popover" : undefined;
+ const sortId = isSortOpen ? "sort-popover" : undefined;
+
return (
<div className="flex h-full">
<ShardSidebar namespace={namespace} cluster={cluster} />
- <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">
+ <div className="mb-4 flex flex-col gap-3 sm:mb-5
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"
>
- <DnsIcon className="mr-2 text-primary
dark:text-primary-light" />
+ <DnsIcon className="mr-3 text-primary
dark:text-primary-light" />
Shard {parseInt(shard) + 1}
- {nodesData?.nodes && (
- <Chip
- label={`${nodesData.nodes.length}
nodes`}
- size="small"
- color="secondary"
- className="ml-3"
- />
- )}
</Typography>
<Typography
- variant="body2"
- className="mt-1 text-gray-500
dark:text-gray-400"
+ variant="body1"
+ className="mt-0.5 text-gray-500
dark:text-gray-400"
>
- {cluster} cluster in namespace {namespace}
+ Manage nodes in this shard
</Typography>
</div>
- </Box>
-
- <div className="grid grid-cols-1 gap-4 sm:grid-cols-2
lg:grid-cols-3 xl:grid-cols-4">
- <Box className="col-span-1">
- <AddNodeCard namespace={namespace}
cluster={cluster} shard={shard} />
- </Box>
-
- {nodesData?.nodes && nodesData.nodes.length > 0 ? (
- nodesData.nodes.map((node: any, index: number) => {
- const roleInfo = getRoleInfo(node.role);
- return (
- <Link
-
href={`/namespaces/${namespace}/clusters/${cluster}/shards/${shard}/nodes/${index}`}
- key={index}
- className="col-span-1"
+ <div className="flex w-full flex-row items-center
gap-2 lg:w-auto">
+ <div className="search-container relative max-w-md
flex-grow transition-all duration-300 lg:min-w-[280px]">
+ <div className="search-inner relative w-full
rounded-lg bg-gray-50 transition-all duration-300 focus-within:bg-white
focus-within:shadow-md dark:bg-dark-paper/90 dark:focus-within:bg-dark-paper">
+ <div className="pointer-events-none
absolute inset-y-0 left-3 flex items-center">
+ <SearchIcon
+ className="text-gray-400"
+ sx={{ fontSize: 18 }}
+ />
+ </div>
+ <input
+ type="text"
+ placeholder="Search nodes..."
+ className="w-full rounded-lg border-0
bg-transparent py-2.5 pl-9 pr-4 text-sm text-gray-800 outline-none ring-1
ring-gray-200 transition-all focus:ring-2 focus:ring-primary dark:text-gray-200
dark:ring-gray-700 dark:focus:ring-primary-light"
+ value={searchTerm}
+ onChange={(e) =>
setSearchTerm(e.target.value)}
+ />
+ {searchTerm && (
+ <button
+ className="absolute inset-y-0
right-3 flex items-center text-gray-400 transition-colors hover:text-gray-600
dark:hover:text-gray-300"
+ onClick={() => setSearchTerm("")}
+ >
+ <span className="text-xs">✕</span>
+ </button>
+ )}
+ </div>
+ </div>
+ <div className="flex flex-shrink-0 gap-3">
+ <NodeCreation
+ position="card"
+ namespace={namespace}
+ cluster={cluster}
+ shard={shard}
+ >
+ <Button
+ variant="outlined"
+ color="primary"
+ className="whitespace-nowrap
rounded-lg px-5 py-2.5 font-medium shadow-sm transition-all hover:shadow-md"
+ startIcon={<AddIcon />}
+ disableElevation
+ size="medium"
>
- <ResourceCard
- title={`Node ${index + 1}`}
- tags={[
- { label: node.role, color:
roleInfo.color as any },
- ]}
+ Create Node
+ </Button>
+ </NodeCreation>
+ </div>
+ </div>
+ </div>
+ <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-3
dark:border-gray-800 sm:px-8">
+ <div className="flex items-center justify-between">
+ <Typography
+ variant="h6"
+ className="font-medium text-gray-800
dark:text-gray-100"
+ >
+ All Nodes
+ </Typography>
+ <div className="flex items-center gap-2">
+ <Tooltip title="Filter">
+ <IconButton
+ size="small"
+ onClick={(e) =>
setFilterAnchorEl(e.currentTarget)}
+ aria-describedby={filterId}
+ className="rounded-full bg-gray-50
text-gray-500 hover:bg-gray-100 dark:bg-gray-800 dark:text-gray-400
dark:hover:bg-gray-700"
>
- <div className="mt-2 space-y-2
text-sm">
- <div className="flex
items-center justify-between">
- <span
className="text-gray-500 dark:text-gray-400">
- ID:
+ <FilterListIcon fontSize="small" />
+ </IconButton>
+ </Tooltip>
+ <Tooltip title="Sort">
+ <IconButton
+ size="small"
+ onClick={(e) =>
setSortAnchorEl(e.currentTarget)}
+ aria-describedby={sortId}
+ className="rounded-full bg-gray-50
text-gray-500 hover:bg-gray-100 dark:bg-gray-800 dark:text-gray-400
dark:hover:bg-gray-700"
+ >
+ <SortIcon fontSize="small" />
+ </IconButton>
+ </Tooltip>
+ </div>
+ </div>
+ </div>
+ <Popover
+ id={filterId}
+ open={isFilterOpen}
+ anchorEl={filterAnchorEl}
+ onClose={() => setFilterAnchorEl(null)}
+ anchorOrigin={{ vertical: "bottom", horizontal:
"right" }}
+ transformOrigin={{ vertical: "top", horizontal:
"right" }}
+ TransitionComponent={Fade}
+ PaperProps={{
+ className:
+ "rounded-xl shadow-xl border
border-gray-100 dark:border-gray-700",
+ elevation: 3,
+ sx: { width: 220 },
+ }}
+ >
+ <div className="p-4">
+ <div className="mb-3 flex items-center
justify-between border-b border-gray-100 pb-2 dark:border-gray-700">
+ <Typography variant="subtitle1"
className="font-medium">
+ Filter Nodes
+ </Typography>
+ </div>
+ <RadioGroup
+ value={filterOption}
+ onChange={(e) =>
setFilterOption(e.target.value)}
+ >
+ <div className="space-y-2">
+ <div className="rounded-lg bg-gray-50
p-2 dark:bg-gray-800">
+ <FormControlLabel
+ value="all"
+ control={
+ <Radio
+ size="small"
+
className="text-primary"
+ checkedIcon={
+ <div
className="flex h-5 w-5 items-center justify-center rounded-full border-2
border-primary bg-primary text-white">
+ <CheckIcon
+ style={{
fontSize: 12 }}
+ />
+ </div>
+ }
+ />
+ }
+ label={
+ <span className="text-sm
font-medium">
+ All nodes
</span>
- <span
-
className="max-w-[120px] overflow-hidden text-ellipsis rounded bg-gray-100 px-2
py-0.5 font-mono text-xs dark:bg-dark-border"
- title={node.id}
- >
- {truncateText(node.id,
10)}
+ }
+ className="m-0 w-full"
+ />
+ </div>
+ <div className="rounded-lg bg-gray-50
p-2 dark:bg-gray-800">
+ <FormControlLabel
+ value="master"
+ control={
+ <Radio
+ size="small"
+
className="text-primary"
+ checkedIcon={
+ <div
className="flex h-5 w-5 items-center justify-center rounded-full border-2
border-primary bg-primary text-white">
+ <CheckIcon
+ style={{
fontSize: 12 }}
+ />
+ </div>
+ }
+ />
+ }
+ label={
+ <span className="text-sm
font-medium">
+ Master nodes
</span>
- </div>
-
- <div className="flex
justify-between">
- <span
className="text-gray-500 dark:text-gray-400">
- Address:
+ }
+ className="m-0 w-full"
+ />
+ </div>
+ <div className="rounded-lg bg-gray-50
p-2 dark:bg-gray-800">
+ <FormControlLabel
+ value="replica"
+ control={
+ <Radio
+ size="small"
+
className="text-primary"
+ checkedIcon={
+ <div
className="flex h-5 w-5 items-center justify-center rounded-full border-2
border-primary bg-primary text-white">
+ <CheckIcon
+ style={{
fontSize: 12 }}
+ />
+ </div>
+ }
+ />
+ }
+ label={
+ <span className="text-sm
font-medium">
+ Replica nodes
</span>
- <span
className="font-medium">{node.addr}</span>
- </div>
-
- <div className="flex
items-center justify-between">
- <span
className="text-gray-500 dark:text-gray-400">
- Uptime:
+ }
+ className="m-0 w-full"
+ />
+ </div>
+ </div>
+ </RadioGroup>
+ <div className="mt-4 flex justify-end">
+ <Button
+ variant="text"
+ size="small"
+ onClick={() => setFilterAnchorEl(null)}
+ className="rounded-lg px-3 py-1
text-xs"
+ >
+ Close
+ </Button>
+ </div>
+ </div>
+ </Popover>
+ <Popover
+ id={sortId}
+ open={isSortOpen}
+ anchorEl={sortAnchorEl}
+ onClose={() => setSortAnchorEl(null)}
+ anchorOrigin={{ vertical: "bottom", horizontal:
"right" }}
+ transformOrigin={{ vertical: "top", horizontal:
"right" }}
+ TransitionComponent={Fade}
+ PaperProps={{
+ className:
+ "rounded-xl shadow-xl border
border-gray-100 dark:border-gray-700",
+ elevation: 3,
+ sx: { width: 220 },
+ }}
+ >
+ <div className="p-4">
+ <div className="mb-3 flex items-center
justify-between border-b border-gray-100 pb-2 dark:border-gray-700">
+ <Typography variant="subtitle1"
className="font-medium">
+ Sort Nodes
+ </Typography>
+ </div>
+ <RadioGroup
+ value={sortOption}
+ onChange={(e) =>
setSortOption(e.target.value)}
+ >
+ <div className="space-y-2">
+ <div className="rounded-lg bg-gray-50
p-2 dark:bg-gray-800">
+ <FormControlLabel
+ value="index-asc"
+ control={
+ <Radio
+ size="small"
+
className="text-primary"
+ checkedIcon={
+ <div
className="flex h-5 w-5 items-center justify-center rounded-full border-2
border-primary bg-primary text-white">
+ <CheckIcon
+ style={{
fontSize: 12 }}
+ />
+ </div>
+ }
+ />
+ }
+ label={
+ <span className="text-sm
font-medium">
+ Index 1-N
</span>
- <span className="flex
items-center">
- <AlarmIcon
- fontSize="small"
- className="mr-1
text-gray-400 dark:text-gray-500"
- />
-
{calculateUptime(node.created_at)}
+ }
+ className="m-0 w-full"
+ />
+ </div>
+ <div className="rounded-lg bg-gray-50
p-2 dark:bg-gray-800">
+ <FormControlLabel
+ value="index-desc"
+ control={
+ <Radio
+ size="small"
+
className="text-primary"
+ checkedIcon={
+ <div
className="flex h-5 w-5 items-center justify-center rounded-full border-2
border-primary bg-primary text-white">
+ <CheckIcon
+ style={{
fontSize: 12 }}
+ />
+ </div>
+ }
+ />
+ }
+ label={
+ <span className="text-sm
font-medium">
+ Index N-1
</span>
+ }
+ className="m-0 w-full"
+ />
+ </div>
+ <div className="rounded-lg bg-gray-50
p-2 dark:bg-gray-800">
+ <FormControlLabel
+ value="uptime-desc"
+ control={
+ <Radio
+ size="small"
+
className="text-primary"
+ checkedIcon={
+ <div
className="flex h-5 w-5 items-center justify-center rounded-full border-2
border-primary bg-primary text-white">
+ <CheckIcon
+ style={{
fontSize: 12 }}
+ />
+ </div>
+ }
+ />
+ }
+ label={
+ <span className="text-sm
font-medium">
+ Newest
+ </span>
+ }
+ className="m-0 w-full"
+ />
+ </div>
+ <div className="rounded-lg bg-gray-50
p-2 dark:bg-gray-800">
+ <FormControlLabel
+ value="uptime-asc"
+ control={
+ <Radio
+ size="small"
+
className="text-primary"
+ checkedIcon={
+ <div
className="flex h-5 w-5 items-center justify-center rounded-full border-2
border-primary bg-primary text-white">
+ <CheckIcon
+ style={{
fontSize: 12 }}
+ />
+ </div>
+ }
+ />
+ }
+ label={
+ <span className="text-sm
font-medium">
+ Oldest
+ </span>
+ }
+ className="m-0 w-full"
+ />
+ </div>
+ </div>
+ </RadioGroup>
+ <div className="mt-4 flex justify-end">
+ <Button
+ variant="text"
+ size="small"
+ onClick={() => setSortAnchorEl(null)}
+ className="rounded-lg px-3 py-1
text-xs"
+ >
+ Close
+ </Button>
+ </div>
+ </div>
+ </Popover>
+ {filteredAndSortedNodes.length > 0 ? (
+ <div className="divide-y divide-gray-100
dark:divide-gray-800">
+ {filteredAndSortedNodes.map((node: any, index:
number) => {
+ const roleInfo = getRoleInfo(node.role);
+ return (
+ <div
+ key={index}
+ className="group p-2
transition-colors hover:bg-gray-50 dark:hover:bg-gray-800/30"
+ >
+ <Paper
+ elevation={0}
+ className="overflow-hidden
rounded-xl border border-transparent bg-white p-4 transition-all
group-hover:border-primary/10 group-hover:shadow-sm dark:bg-dark-paper
dark:group-hover:border-primary-dark/20"
+ >
+ <div className="flex flex-col
items-start sm:flex-row sm:items-center">
+ <div className="mb-3 flex
h-14 w-14 flex-shrink-0 items-center justify-center rounded-xl bg-green-50
text-green-500 dark:bg-green-900/30 dark:text-green-400 sm:mb-0">
+ <DeviceHubIcon sx={{
fontSize: 28 }} />
+ </div>
+ <div className="flex
flex-1 flex-col sm:ml-5 sm:flex-row sm:items-center sm:overflow-hidden">
+ <div className="flex-1
overflow-hidden">
+ <Link
+
href={`/namespaces/${namespace}/clusters/${cluster}/shards/${shard}/nodes/${index}`}
+
className="block"
+ >
+ <div
className="flex items-center gap-2">
+ <Typography
+
variant="h6"
+
className="truncate font-medium text-gray-900 transition-colors
hover:text-primary dark:text-gray-100 dark:hover:text-primary-light"
+ >
+ Node
{index + 1}
+
</Typography>
+ {node.role
=== "master" ? (
+ <div
className="flex items-center gap-1 rounded-full border border-green-200
bg-green-50 px-2.5 py-1 dark:border-green-800 dark:bg-green-900/30">
+
<div className="h-1.5 w-1.5 rounded-full bg-green-500"></div>
+
<span className="text-xs font-medium text-green-700 dark:text-green-300">
+
Master
+
</span>
+ </div>
+ ) : (
+ <div
className="flex items-center gap-1 rounded-full border border-blue-200
bg-blue-50 px-2.5 py-1 dark:border-blue-800 dark:bg-blue-900/30">
+
<div className="h-1.5 w-1.5 rounded-full bg-blue-500"></div>
+
<span className="text-xs font-medium text-blue-700 dark:text-blue-300">
+
Replica
+
</span>
+ </div>
+ )}
+ </div>
+ <div
className="mt-2 space-y-1">
+ <Typography
+
variant="body2"
+
className="flex items-center text-gray-500 dark:text-gray-400"
+ >
+ ID:{"
"}
+ <span
className="ml-1 font-mono text-xs">
+
{truncateText(
+
node.id,
+
10
+ )}
+ </span>
+
</Typography>
+ <Typography
+
variant="body2"
+
className="flex items-center text-gray-500 dark:text-gray-400"
+ >
+
Address:{" "}
+ <span
className="ml-1">
+
{node.addr}
+ </span>
+
</Typography>
+ <Typography
+
variant="body2"
+
className="flex items-center text-gray-500 dark:text-gray-400"
+ >
+
<AlarmIcon
+
fontSize="small"
+
className="mr-1 text-gray-400 dark:text-gray-500"
+ />
+
Uptime:{" "}
+
{calculateUptime(
+
node.created_at
+ )}
+
</Typography>
+ </div>
+ </Link>
+ </div>
+ </div>
</div>
- </div>
- </ResourceCard>
- </Link>
- );
- })
+ </Paper>
+ </div>
+ );
+ })}
+ </div>
) : (
- <Box className="col-span-full">
+ <div className="p-12">
<EmptyState
- title="No nodes found"
- description="Create a node to get started"
- icon={<DeviceHubIcon sx={{ fontSize: 60 }}
/>}
+ title={
+ filterOption !== "all"
+ ? "No matching nodes"
+ : "No nodes found"
+ }
+ description={
+ filterOption !== "all"
+ ? "Try changing your filter
settings"
+ : searchTerm
+ ? "Try adjusting your search
term"
+ : "Create a node to get started"
+ }
+ icon={<DeviceHubIcon sx={{ fontSize: 64 }}
/>}
/>
- </Box>
+ </div>
)}
- </div>
+ {filteredAndSortedNodes.length > 0 && (
+ <div className="bg-gray-50 px-6 py-4
dark:bg-gray-800/30 sm:px-8">
+ <Typography
+ variant="body2"
+ className="text-gray-500
dark:text-gray-400"
+ >
+ Showing {filteredAndSortedNodes.length}
of{" "}
+ {nodesData.nodes.length} nodes
+ </Typography>
+ </div>
+ )}
+ </Paper>
</Box>
</div>
</div>
diff --git a/webui/src/app/ui/formCreation.tsx
b/webui/src/app/ui/formCreation.tsx
index 0a731aa..ff8cdc4 100644
--- a/webui/src/app/ui/formCreation.tsx
+++ b/webui/src/app/ui/formCreation.tsx
@@ -53,6 +53,7 @@ type NodeFormProps = {
namespace: string;
cluster: string;
shard: string;
+ children?: React.ReactNode;
};
const containsWhitespace = (value: string): boolean => /\s/.test(value);
@@ -316,7 +317,13 @@ export const MigrateSlot: React.FC<ShardFormProps> = ({
position, namespace, clu
);
};
-export const NodeCreation: React.FC<NodeFormProps> = ({ position, namespace,
cluster, shard }) => {
+export const NodeCreation: React.FC<NodeFormProps> = ({
+ position,
+ namespace,
+ cluster,
+ shard,
+ children,
+}) => {
const router = useRouter();
const handleSubmit = async (formData: FormData) => {
@@ -369,6 +376,8 @@ export const NodeCreation: React.FC<NodeFormProps> = ({
position, namespace, clu
},
]}
onSubmit={handleSubmit}
- />
+ >
+ {children}
+ </FormDialog>
);
};
diff --git a/webui/src/app/ui/formDialog.tsx b/webui/src/app/ui/formDialog.tsx
index d5adc25..10a47bc 100644
--- a/webui/src/app/ui/formDialog.tsx
+++ b/webui/src/app/ui/formDialog.tsx
@@ -170,7 +170,7 @@ const FormDialog: React.FC<FormDialogProps> = ({
}}
>
<Typography
- variant="subtitle1"
+ variant="h6"
className="font-semibold text-gray-800
dark:text-gray-100"
>
{title}
@@ -281,6 +281,11 @@ const FormDialog: React.FC<FormDialogProps> = ({
alignItems: "center",
minHeight: "32px",
},
+ "& .MuiInputBase-input": {
+ display: "flex",
+ alignItems: "center",
+ minHeight: "35px",
+ },
}}
MenuProps={{
PaperProps: {
diff --git a/webui/src/app/ui/sidebar.tsx b/webui/src/app/ui/sidebar.tsx
index 9fcbe60..dcef803 100644
--- a/webui/src/app/ui/sidebar.tsx
+++ b/webui/src/app/ui/sidebar.tsx
@@ -290,6 +290,17 @@ export function ShardSidebar({ namespace, cluster }: {
namespace: string; cluste
const [shards, setShards] = useState<string[]>([]);
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 () => {
@@ -306,57 +317,83 @@ export function ShardSidebar({ namespace, cluster }: {
namespace: string; cluste
fetchData();
}, [namespace, cluster]);
+ 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">
- <ShardCreation namespace={namespace} cluster={cluster}
position="sidebar" />
- </Box>
+ {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>
+ )}
- <Box className="px-4 py-2">
- <SidebarHeader
- title="Shards"
- count={shards.length}
- isOpen={isOpen}
- toggleOpen={() => setIsOpen(!isOpen)}
- icon={<DnsIcon fontSize="small" />}
- />
- </Box>
+ <div className="sidebar-inner w-[260px]">
+ <Box className="p-4 pb-2">
+ <ShardCreation namespace={namespace} cluster={cluster}
position="sidebar" />
+ </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">
- {shards.map((shard, index) => (
- <Link
-
href={`/namespaces/${namespace}/clusters/${cluster}/shards/${index}`}
- passHref
- key={index}
- >
- <Item
- type="shard"
- item={shard}
- namespace={namespace}
- cluster={cluster}
- />
- </Link>
- ))}
- </List>
+ <Box className="px-4 py-2">
+ <SidebarHeader
+ title="Shards"
+ count={shards.length}
+ isOpen={isOpen}
+ toggleOpen={toggleSidebar}
+ icon={<DnsIcon 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">
+ {shards.map((shard, index) => (
+ <Link
+
href={`/namespaces/${namespace}/clusters/${cluster}/shards/${index}`}
+ passHref
+ key={index}
+ >
+ <Item
+ type="shard"
+ item={shard}
+ namespace={namespace}
+ cluster={cluster}
+ />
+ </Link>
+ ))}
+ </List>
+ </div>
</div>
- </div>
- </Collapse>
+ </Collapse>
+ </div>
</Paper>
);
}
