mirror of
https://github.com/khairul169/garage-webui.git
synced 2026-09-15 00:43:21 +07:00
feat: add cluster & bucket management
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
import { Cylinder, HardDrive, LayoutDashboard } from "lucide-react";
|
||||
import { Menu } from "react-daisyui";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
const Sidebar = () => {
|
||||
return (
|
||||
<aside className="bg-base-100 border-r border-base-300/30 w-[220px] overflow-y-auto">
|
||||
<div className="p-4">
|
||||
<img
|
||||
src="https://garagehq.deuxfleurs.fr/images/garage-logo.svg"
|
||||
alt="logo"
|
||||
className="w-full max-w-[100px] mx-auto"
|
||||
/>
|
||||
<p className="text-sm font-medium text-center">WebUI</p>
|
||||
</div>
|
||||
<Menu className="gap-y-1">
|
||||
<Menu.Item>
|
||||
<Link to="/">
|
||||
<LayoutDashboard />
|
||||
<p>Dashboard</p>
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
<Link to="/cluster">
|
||||
<HardDrive />
|
||||
<p>Cluster</p>
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
<Link to="/buckets">
|
||||
<Cylinder />
|
||||
<p>Buckets</p>
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
</aside>
|
||||
);
|
||||
};
|
||||
|
||||
export default Sidebar;
|
||||
@@ -0,0 +1,64 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import { LucideIcon } from "lucide-react";
|
||||
import { useMemo } from "react";
|
||||
import { Tabs } from "react-daisyui";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
|
||||
export type Tab = {
|
||||
name: string;
|
||||
title?: string;
|
||||
icon?: LucideIcon;
|
||||
Component?: () => JSX.Element;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
tabs: Tab[];
|
||||
name?: string;
|
||||
className?: string;
|
||||
contentClassName?: string;
|
||||
};
|
||||
|
||||
const TabView = ({
|
||||
tabs,
|
||||
name = "tab",
|
||||
className,
|
||||
contentClassName,
|
||||
}: Props) => {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const curTab = searchParams.get(name) || tabs[0].name;
|
||||
|
||||
const content = useMemo(() => {
|
||||
const Comp = tabs.find((tab) => tab.name === curTab)?.Component;
|
||||
return Comp ? <Comp /> : null;
|
||||
}, [curTab, tabs]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tabs
|
||||
variant="boxed"
|
||||
className={cn("w-auto inline-flex flex-row items-stretch", className)}
|
||||
>
|
||||
{tabs.map(({ icon: Icon, ...tab }) => (
|
||||
<Tabs.Tab
|
||||
key={tab.name}
|
||||
active={curTab === tab.name}
|
||||
className="flex flex-row items-center gap-x-2 h-auto"
|
||||
onClick={() => {
|
||||
setSearchParams((params) => {
|
||||
params.set(name, tab.name);
|
||||
return params;
|
||||
});
|
||||
}}
|
||||
>
|
||||
{Icon ? <Icon size={20} /> : null}
|
||||
<span>{tab.title || tab.name}</span>
|
||||
</Tabs.Tab>
|
||||
))}
|
||||
</Tabs>
|
||||
|
||||
<div className={cn("mt-4", contentClassName)}>{content}</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default TabView;
|
||||
@@ -1,8 +1,9 @@
|
||||
import { PageContext } from "@/context/page-context";
|
||||
import { useContext } from "react";
|
||||
import { Link, Outlet } from "react-router-dom";
|
||||
import { Menu } from "react-daisyui";
|
||||
import { HardDrive, LayoutDashboard } from "lucide-react";
|
||||
import { Suspense, useContext } from "react";
|
||||
import { Outlet, useNavigate } from "react-router-dom";
|
||||
import Sidebar from "../containers/sidebar";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
import Button from "../ui/button";
|
||||
|
||||
const MainLayout = () => {
|
||||
return (
|
||||
@@ -13,50 +14,33 @@ const MainLayout = () => {
|
||||
<Header />
|
||||
|
||||
<main className="flex-1 overflow-y-auto p-4 md:p-8">
|
||||
<div className="container max-w-5xl">
|
||||
<Suspense>
|
||||
<Outlet />
|
||||
</div>
|
||||
</Suspense>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Sidebar = () => {
|
||||
return (
|
||||
<aside className="bg-base-100 border-r border-base-300/30 w-[220px] overflow-y-auto">
|
||||
<div className="p-4">
|
||||
<img
|
||||
src="https://garagehq.deuxfleurs.fr/images/garage-logo.svg"
|
||||
alt="logo"
|
||||
className="w-full max-w-[100px] mx-auto"
|
||||
/>
|
||||
<p className="text-sm font-medium text-center">WebUI</p>
|
||||
</div>
|
||||
<Menu className="gap-y-1">
|
||||
<Menu.Item>
|
||||
<Link to="/">
|
||||
<LayoutDashboard />
|
||||
<p>Dashboard</p>
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
<Link to="/cluster">
|
||||
<HardDrive />
|
||||
<p>Cluster</p>
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
</aside>
|
||||
);
|
||||
};
|
||||
|
||||
const Header = () => {
|
||||
const page = useContext(PageContext);
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<header className="bg-base-100 p-4 md:p-8 md:py-6 flex flex-row items-center gap-4">
|
||||
<h1 className="text-2xl font-medium">{page?.title || "Dashboard"}</h1>
|
||||
<header className="bg-base-100 px-4 h-16 md:px-8 md:h-20 flex flex-row items-center gap-4">
|
||||
{page?.prev ? (
|
||||
<Button
|
||||
href={page.prev}
|
||||
onClick={() => navigate(page.prev!, { replace: true })}
|
||||
color="ghost"
|
||||
shape="circle"
|
||||
className="-ml-4"
|
||||
>
|
||||
<ArrowLeft />
|
||||
</Button>
|
||||
) : null}
|
||||
<h1 className="text-xl">{page?.title || "Dashboard"}</h1>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentPropsWithoutRef, forwardRef } from "react";
|
||||
import { Button as BaseButton } from "react-daisyui";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
type ButtonProps = ComponentPropsWithoutRef<typeof BaseButton> & {
|
||||
href?: string;
|
||||
target?: "_blank" | "_self" | "_parent" | "_top";
|
||||
};
|
||||
|
||||
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ href, ...props }, ref) => {
|
||||
return (
|
||||
<BaseButton
|
||||
ref={ref}
|
||||
tag={href ? Link : undefined}
|
||||
{...props}
|
||||
{...(href ? { to: href } : {})}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export default Button;
|
||||
@@ -0,0 +1,41 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import { X } from "lucide-react";
|
||||
import React, { forwardRef } from "react";
|
||||
import { Button } from "react-daisyui";
|
||||
|
||||
type Props = React.ComponentPropsWithoutRef<"div"> & {
|
||||
onClick?: () => void;
|
||||
onRemove?: () => void;
|
||||
};
|
||||
|
||||
const Chips = forwardRef<HTMLDivElement, Props>(
|
||||
({ className, children, onRemove, ...props }, ref) => {
|
||||
const Comp = props.onClick ? "button" : "div";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
ref={ref as never}
|
||||
className={cn(
|
||||
"inline-flex flex-row items-center h-8 px-4 rounded-full text-sm border border-primary/80 text-base-content cursor-default",
|
||||
className
|
||||
)}
|
||||
{...(props as any)}
|
||||
>
|
||||
{children}
|
||||
{onRemove ? (
|
||||
<Button
|
||||
color="ghost"
|
||||
shape="circle"
|
||||
size="sm"
|
||||
className="-mr-3"
|
||||
onClick={onRemove}
|
||||
>
|
||||
<X size={16} />
|
||||
</Button>
|
||||
) : null}
|
||||
</Comp>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export default Chips;
|
||||
Reference in New Issue
Block a user