Skip to content

Commit bf1ba47

Browse files
feat(webui): redesign the controller web management UI (#281)
* Fix edge case handling in Mock engine List method * There's a typo in the main UI heading - Controler should be Controller * This code launches a new goroutine for each node in the cluster without any concurrency limits. * redesign the webpage * Update cluster.go * fix conflict * conflict * Update webui/src/app/page.tsx * Update webui/src/app/page.tsx * Update webui/src/app/layout.tsx * Update webui/src/app/ui/banner.tsx * Update webui/src/app/page.tsx * Update webui/src/app/page.tsx --------- Co-authored-by: Twice <twice@apache.org>
1 parent 1be45f1 commit bf1ba47

File tree

20 files changed

+1766
-501
lines changed

20 files changed

+1766
-501
lines changed

webui/src/app/globals.css

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,70 @@
2020
@tailwind base;
2121
@tailwind components;
2222
@tailwind utilities;
23+
24+
@layer components {
25+
.card {
26+
@apply bg-white dark:bg-dark-paper border border-light-border dark:border-dark-border rounded-lg shadow-card hover:shadow-card-hover transition-all p-5 flex flex-col;
27+
}
28+
29+
.sidebar-item {
30+
@apply flex items-center px-4 py-2 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-dark-paper rounded-lg my-1 transition-colors;
31+
}
32+
33+
.sidebar-item-active {
34+
@apply bg-primary-light/10 text-primary dark:text-primary-light;
35+
}
36+
37+
.btn {
38+
@apply px-4 py-2 rounded-md transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2;
39+
}
40+
41+
.btn-primary {
42+
@apply bg-primary text-white hover:bg-primary-dark focus:ring-primary;
43+
}
44+
45+
.btn-outline {
46+
@apply border border-primary text-primary hover:bg-primary hover:text-white focus:ring-primary;
47+
}
48+
49+
.container-inner {
50+
@apply p-6 max-w-screen-2xl mx-auto;
51+
}
52+
}
53+
54+
/* custom scrollbar */
55+
::-webkit-scrollbar {
56+
width: 8px;
57+
height: 8px;
58+
}
59+
60+
::-webkit-scrollbar-track {
61+
background: transparent;
62+
}
63+
64+
::-webkit-scrollbar-thumb {
65+
background: #c1c1c1;
66+
border-radius: 4px;
67+
}
68+
69+
::-webkit-scrollbar-thumb:hover {
70+
background: #a1a1a1;
71+
}
72+
73+
.dark ::-webkit-scrollbar-thumb {
74+
background: #555;
75+
}
76+
77+
.dark ::-webkit-scrollbar-thumb:hover {
78+
background: #777;
79+
}
80+
81+
/* smooth transitions for dark mode */
82+
body {
83+
transition: background-color 0.3s ease;
84+
}
85+
86+
.dark body {
87+
background-color: #121212;
88+
color: #e0e0e0;
89+
}

webui/src/app/layout.tsx

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,13 @@ import { Inter } from "next/font/google";
2222
import "./globals.css";
2323
import Banner from "./ui/banner";
2424
import { Container } from "@mui/material";
25+
import { ThemeProvider } from "./theme-provider";
2526

2627
const inter = Inter({ subsets: ["latin"] });
2728

2829
export const metadata: Metadata = {
29-
title: "Kvrocks Controller",
30-
description: "Kvrocks Controller",
30+
title: "Apache Kvrocks Controller",
31+
description: "Management UI for Apache Kvrocks clusters",
3132
};
3233

3334
export default function RootLayout({
@@ -36,12 +37,14 @@ export default function RootLayout({
3637
children: React.ReactNode;
3738
}>) {
3839
return (
39-
<html lang="en">
40-
<body className={inter.className}>
41-
<Banner />
42-
<Container sx={{marginTop: '64px', height: 'calc(100vh - 64px)'}} maxWidth={false} disableGutters>
43-
{children}
44-
</Container>
40+
<html lang="en" suppressHydrationWarning>
41+
<body className={`${inter.className} bg-light dark:bg-dark min-h-screen`}>
42+
<ThemeProvider>
43+
<Banner />
44+
<Container sx={{marginTop: '64px', height: 'calc(100vh - 64px)'}} maxWidth={false} disableGutters>
45+
{children}
46+
</Container>
47+
</ThemeProvider>
4548
</body>
4649
</html>
4750
);

webui/src/app/namespaces/[namespace]/clusters/[cluster]/page.tsx

Lines changed: 89 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,20 @@
2222
import {
2323
Box,
2424
Container,
25-
Typography
25+
Typography,
26+
Chip,
27+
Badge,
2628
} from "@mui/material";
2729
import { ClusterSidebar } from "../../../../ui/sidebar";
2830
import { useState, useEffect } from "react";
2931
import { listShards } from "@/app/lib/api";
30-
import { AddShardCard, CreateCard } from "@/app/ui/createCard";
32+
import { AddShardCard, ResourceCard } from "@/app/ui/createCard";
3133
import Link from "next/link";
3234
import { useRouter } from "next/navigation";
3335
import { LoadingSpinner } from "@/app/ui/loadingSpinner";
36+
import DnsIcon from '@mui/icons-material/Dns';
37+
import StorageIcon from '@mui/icons-material/Storage';
38+
import EmptyState from "@/app/ui/emptyState";
3439

3540
export default function Cluster({
3641
params,
@@ -67,42 +72,91 @@ export default function Cluster({
6772
return <LoadingSpinner />;
6873
}
6974

75+
const formatSlotRanges = (ranges: string[]) => {
76+
if (!ranges || ranges.length === 0) return "None";
77+
if (ranges.length <= 2) return ranges.join(", ");
78+
return `${ranges[0]}, ${ranges[1]}, ... (+${ranges.length - 2} more)`;
79+
};
80+
7081
return (
7182
<div className="flex h-full">
7283
<ClusterSidebar namespace={namespace} />
73-
<Container
74-
maxWidth={false}
75-
disableGutters
76-
sx={{ height: "100%", overflowY: "auto", marginLeft: "16px" }}
77-
>
78-
<div className="flex flex-row flex-wrap">
79-
<AddShardCard namespace={namespace} cluster={cluster} />
80-
{shardsData.map((shard, index) => (
81-
<Link
82-
key={index}
83-
href={`/namespaces/${namespace}/clusters/${cluster}/shards/${index}`}
84-
>
85-
<CreateCard>
86-
<Typography variant="h6" gutterBottom noWrap>
87-
Shard {index + 1}
88-
</Typography>
89-
<Typography variant="body2" gutterBottom>
90-
Nodes : {shard.nodes.length}
91-
</Typography>
92-
<Typography variant="body2" gutterBottom>
93-
Slots: {shard.slot_ranges.join(", ")}
94-
</Typography>
95-
<Typography variant="body2" gutterBottom>
96-
Target Shard Index: {shard.target_shard_index}
97-
</Typography>
98-
<Typography variant="body2" gutterBottom>
99-
Migrating Slot: {shard.migrating_slot}
100-
</Typography>
101-
</CreateCard>
102-
</Link>
103-
))}
104-
</div>
105-
</Container>
84+
<div className="flex-1 overflow-auto">
85+
<Box className="container-inner">
86+
<Box className="flex items-center justify-between mb-6">
87+
<div>
88+
<Typography variant="h5" className="font-medium text-gray-800 dark:text-gray-100 flex items-center">
89+
<StorageIcon className="mr-2 text-primary dark:text-primary-light" />
90+
{cluster}
91+
<Chip
92+
label={`${shardsData.length} shards`}
93+
size="small"
94+
color="primary"
95+
className="ml-3"
96+
/>
97+
</Typography>
98+
<Typography variant="body2" className="text-gray-500 dark:text-gray-400 mt-1">
99+
Cluster in namespace: {namespace}
100+
</Typography>
101+
</div>
102+
</Box>
103+
104+
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
105+
<Box className="col-span-1">
106+
<AddShardCard namespace={namespace} cluster={cluster} />
107+
</Box>
108+
109+
{shardsData.length > 0 ? (
110+
shardsData.map((shard, index) => (
111+
<Link
112+
key={index}
113+
href={`/namespaces/${namespace}/clusters/${cluster}/shards/${index}`}
114+
className="col-span-1"
115+
>
116+
<ResourceCard
117+
title={`Shard ${index + 1}`}
118+
tags={[
119+
{ label: `${shard.nodes.length} nodes`, color: "secondary" },
120+
shard.migrating_slot >= 0 ? { label: "Migrating", color: "warning" } : undefined
121+
].filter(Boolean)}
122+
>
123+
<div className="space-y-2 text-sm">
124+
<div className="flex justify-between">
125+
<span className="text-gray-500 dark:text-gray-400">Slots:</span>
126+
<span className="font-medium">{formatSlotRanges(shard.slot_ranges)}</span>
127+
</div>
128+
129+
{shard.target_shard_index >= 0 && (
130+
<div className="flex justify-between">
131+
<span className="text-gray-500 dark:text-gray-400">Target Shard:</span>
132+
<span className="font-medium">{shard.target_shard_index + 1}</span>
133+
</div>
134+
)}
135+
136+
{shard.migrating_slot >= 0 && (
137+
<div className="flex justify-between">
138+
<span className="text-gray-500 dark:text-gray-400">Migrating Slot:</span>
139+
<Badge color="warning" variant="dot">
140+
<span className="font-medium">{shard.migrating_slot}</span>
141+
</Badge>
142+
</div>
143+
)}
144+
</div>
145+
</ResourceCard>
146+
</Link>
147+
))
148+
) : (
149+
<Box className="col-span-full">
150+
<EmptyState
151+
title="No shards found"
152+
description="Create a shard to get started"
153+
icon={<DnsIcon sx={{ fontSize: 60 }} />}
154+
/>
155+
</Box>
156+
)}
157+
</div>
158+
</Box>
159+
</div>
106160
</div>
107161
);
108162
}

0 commit comments

Comments
 (0)