Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions webui/src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,70 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

@layer components {
.card {
@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;
}

.sidebar-item {
@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;
}

.sidebar-item-active {
@apply bg-primary-light/10 text-primary dark:text-primary-light;
}

.btn {
@apply px-4 py-2 rounded-md transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2;
}

.btn-primary {
@apply bg-primary text-white hover:bg-primary-dark focus:ring-primary;
}

.btn-outline {
@apply border border-primary text-primary hover:bg-primary hover:text-white focus:ring-primary;
}

.container-inner {
@apply p-6 max-w-screen-2xl mx-auto;
}
}

/* custom scrollbar */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}

::-webkit-scrollbar-track {
background: transparent;
}

::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 4px;
}

::-webkit-scrollbar-thumb:hover {
background: #a1a1a1;
}

.dark ::-webkit-scrollbar-thumb {
background: #555;
}

.dark ::-webkit-scrollbar-thumb:hover {
background: #777;
}

/* smooth transitions for dark mode */
body {
transition: background-color 0.3s ease;
}

.dark body {
background-color: #121212;
color: #e0e0e0;
}
19 changes: 11 additions & 8 deletions webui/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,13 @@ import { Inter } from "next/font/google";
import "./globals.css";
import Banner from "./ui/banner";
import { Container } from "@mui/material";
import { ThemeProvider } from "./theme-provider";

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

export const metadata: Metadata = {
title: "Kvrocks Controller",
description: "Kvrocks Controller",
title: "Apache Kvrocks Controller",
description: "Management UI for Apache Kvrocks clusters",
};

export default function RootLayout({
Expand All @@ -36,12 +37,14 @@ export default function RootLayout({
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={inter.className}>
<Banner />
<Container sx={{marginTop: '64px', height: 'calc(100vh - 64px)'}} maxWidth={false} disableGutters>
{children}
</Container>
<html lang="en" suppressHydrationWarning>
<body className={`${inter.className} bg-light dark:bg-dark min-h-screen`}>
<ThemeProvider>
<Banner />
<Container sx={{marginTop: '64px', height: 'calc(100vh - 64px)'}} maxWidth={false} disableGutters>
{children}
</Container>
</ThemeProvider>
</body>
</html>
);
Expand Down
124 changes: 89 additions & 35 deletions webui/src/app/namespaces/[namespace]/clusters/[cluster]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,20 @@
import {
Box,
Container,
Typography
Typography,
Chip,
Badge,
} from "@mui/material";
import { ClusterSidebar } from "../../../../ui/sidebar";
import { useState, useEffect } from "react";
import { listShards } from "@/app/lib/api";
import { AddShardCard, CreateCard } from "@/app/ui/createCard";
import { AddShardCard, ResourceCard } from "@/app/ui/createCard";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { LoadingSpinner } from "@/app/ui/loadingSpinner";
import DnsIcon from '@mui/icons-material/Dns';
import StorageIcon from '@mui/icons-material/Storage';
import EmptyState from "@/app/ui/emptyState";

export default function Cluster({
params,
Expand Down Expand Up @@ -67,42 +72,91 @@ export default function Cluster({
return <LoadingSpinner />;
}

const formatSlotRanges = (ranges: string[]) => {
if (!ranges || ranges.length === 0) return "None";
if (ranges.length <= 2) return ranges.join(", ");
return `${ranges[0]}, ${ranges[1]}, ... (+${ranges.length - 2} more)`;
};

return (
<div className="flex h-full">
<ClusterSidebar namespace={namespace} />
<Container
maxWidth={false}
disableGutters
sx={{ height: "100%", overflowY: "auto", marginLeft: "16px" }}
>
<div className="flex flex-row flex-wrap">
<AddShardCard namespace={namespace} cluster={cluster} />
{shardsData.map((shard, index) => (
<Link
key={index}
href={`/namespaces/${namespace}/clusters/${cluster}/shards/${index}`}
>
<CreateCard>
<Typography variant="h6" gutterBottom noWrap>
Shard {index + 1}
</Typography>
<Typography variant="body2" gutterBottom>
Nodes : {shard.nodes.length}
</Typography>
<Typography variant="body2" gutterBottom>
Slots: {shard.slot_ranges.join(", ")}
</Typography>
<Typography variant="body2" gutterBottom>
Target Shard Index: {shard.target_shard_index}
</Typography>
<Typography variant="body2" gutterBottom>
Migrating Slot: {shard.migrating_slot}
</Typography>
</CreateCard>
</Link>
))}
</div>
</Container>
<div className="flex-1 overflow-auto">
<Box className="container-inner">
<Box className="flex items-center justify-between mb-6">
<div>
<Typography variant="h5" className="font-medium text-gray-800 dark:text-gray-100 flex items-center">
<StorageIcon className="mr-2 text-primary dark:text-primary-light" />
{cluster}
<Chip
label={`${shardsData.length} shards`}
size="small"
color="primary"
className="ml-3"
/>
</Typography>
<Typography variant="body2" className="text-gray-500 dark:text-gray-400 mt-1">
Cluster in namespace: {namespace}
</Typography>
</div>
</Box>

<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
<Box className="col-span-1">
<AddShardCard namespace={namespace} cluster={cluster} />
</Box>

{shardsData.length > 0 ? (
shardsData.map((shard, index) => (
<Link
key={index}
href={`/namespaces/${namespace}/clusters/${cluster}/shards/${index}`}
className="col-span-1"
>
<ResourceCard
title={`Shard ${index + 1}`}
tags={[
{ label: `${shard.nodes.length} nodes`, color: "secondary" },
shard.migrating_slot >= 0 ? { label: "Migrating", color: "warning" } : undefined
].filter(Boolean)}
>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-gray-500 dark:text-gray-400">Slots:</span>
<span className="font-medium">{formatSlotRanges(shard.slot_ranges)}</span>
</div>

{shard.target_shard_index >= 0 && (
<div className="flex justify-between">
<span className="text-gray-500 dark:text-gray-400">Target Shard:</span>
<span className="font-medium">{shard.target_shard_index + 1}</span>
</div>
)}

{shard.migrating_slot >= 0 && (
<div className="flex justify-between">
<span className="text-gray-500 dark:text-gray-400">Migrating Slot:</span>
<Badge color="warning" variant="dot">
<span className="font-medium">{shard.migrating_slot}</span>
</Badge>
</div>
)}
</div>
</ResourceCard>
</Link>
))
) : (
<Box className="col-span-full">
<EmptyState
title="No shards found"
description="Create a shard to get started"
icon={<DnsIcon sx={{ fontSize: 60 }} />}
/>
</Box>
)}
</div>
</Box>
</div>
</div>
);
}
Loading