mirror of
https://github.com/khairul169/garage-webui.git
synced 2026-09-15 00:43:21 +07:00
feat: fix responsive layout, add theme switcher
This commit is contained in:
@@ -27,7 +27,7 @@ const MenuButton = () => {
|
||||
return (
|
||||
<Dropdown end>
|
||||
<Dropdown.Toggle button={false}>
|
||||
<Button icon={EllipsisVertical} />
|
||||
<Button icon={EllipsisVertical} color="ghost" />
|
||||
</Dropdown.Toggle>
|
||||
|
||||
<Dropdown.Menu>
|
||||
|
||||
@@ -1,32 +1,111 @@
|
||||
import { Button } from "react-daisyui";
|
||||
import { Modal } from "react-daisyui";
|
||||
import { Plus } from "lucide-react";
|
||||
import Chips from "@/components/ui/chips";
|
||||
import { Bucket } from "../../types";
|
||||
import { useDisclosure } from "@/hooks/useDisclosure";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { AddAliasSchema, addAliasSchema } from "../schema";
|
||||
import Button from "@/components/ui/button";
|
||||
import { useAddAlias, useRemoveAlias } from "../hooks";
|
||||
import { toast } from "sonner";
|
||||
import { handleError } from "@/lib/utils";
|
||||
import { InputField } from "@/components/ui/input";
|
||||
import { useEffect } from "react";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
type Props = {
|
||||
data?: Bucket;
|
||||
};
|
||||
|
||||
const AliasesSection = ({ data }: Props) => {
|
||||
const aliases = data?.globalAliases?.slice(1);
|
||||
const queryClient = useQueryClient();
|
||||
const removeAlias = useRemoveAlias(data?.id, {
|
||||
onSuccess: () => {
|
||||
toast.success("Alias removed!");
|
||||
queryClient.invalidateQueries({ queryKey: ["bucket", data?.id] });
|
||||
},
|
||||
onError: handleError,
|
||||
});
|
||||
|
||||
const onRemoveAlias = (alias: string) => {
|
||||
if (window.confirm("Are you sure you want to remove this alias?")) {
|
||||
removeAlias.mutate(alias);
|
||||
}
|
||||
};
|
||||
|
||||
const aliases = data?.globalAliases || [];
|
||||
|
||||
return (
|
||||
<div className="mt-2">
|
||||
<p className="inline label label-text">Aliases</p>
|
||||
|
||||
<div className="flex flex-row flex-wrap gap-2 mt-1">
|
||||
{aliases?.map((alias: string) => (
|
||||
<Chips key={alias} onRemove={() => {}}>
|
||||
<div className="flex flex-row flex-wrap gap-2 mt-2">
|
||||
{aliases.map((alias: string) => (
|
||||
<Chips key={alias} onRemove={() => onRemoveAlias(alias)}>
|
||||
{alias}
|
||||
</Chips>
|
||||
))}
|
||||
<Button size="sm">
|
||||
<Plus className="-ml-1" size={18} />
|
||||
Add Alias
|
||||
</Button>
|
||||
<AddAliasDialog id={data?.id} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const AddAliasDialog = ({ id }: { id?: string }) => {
|
||||
const { dialogRef, isOpen, onOpen, onClose } = useDisclosure();
|
||||
const form = useForm<AddAliasSchema>({
|
||||
resolver: zodResolver(addAliasSchema),
|
||||
defaultValues: { alias: "" },
|
||||
});
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) form.setFocus("alias");
|
||||
}, [isOpen]);
|
||||
|
||||
const addAlias = useAddAlias(id, {
|
||||
onSuccess: () => {
|
||||
form.reset();
|
||||
onClose();
|
||||
toast.success("Alias added!");
|
||||
queryClient.invalidateQueries({ queryKey: ["bucket", id] });
|
||||
},
|
||||
onError: handleError,
|
||||
});
|
||||
|
||||
const onSubmit = form.handleSubmit((values) => {
|
||||
addAlias.mutate(values.alias);
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button size="sm" onClick={onOpen} icon={Plus}>
|
||||
Add Alias
|
||||
</Button>
|
||||
|
||||
<Modal ref={dialogRef} open={isOpen}>
|
||||
<Modal.Header>Add Alias</Modal.Header>
|
||||
|
||||
<Modal.Body>
|
||||
<form onSubmit={onSubmit}>
|
||||
<InputField form={form} name="alias" title="Name" />
|
||||
</form>
|
||||
</Modal.Body>
|
||||
|
||||
<Modal.Actions>
|
||||
<Button onClick={onClose}>Cancel</Button>
|
||||
<Button
|
||||
color="primary"
|
||||
onClick={onSubmit}
|
||||
disabled={addAlias.isPending}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</Modal.Actions>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AliasesSection;
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Controller, DeepPartial, useForm, useWatch } from "react-hook-form";
|
||||
import { DeepPartial, useForm, useWatch } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { QuotaSchema, quotaSchema } from "../schema";
|
||||
import { useEffect } from "react";
|
||||
import { Input, Toggle } from "react-daisyui";
|
||||
import FormControl from "@/components/ui/form-control";
|
||||
import { useDebounce } from "@/hooks/useDebounce";
|
||||
import { useUpdateBucket } from "../hooks";
|
||||
import { Bucket } from "../../types";
|
||||
import { InputField } from "@/components/ui/input";
|
||||
import { ToggleField } from "@/components/ui/toggle";
|
||||
|
||||
type Props = {
|
||||
data?: Bucket;
|
||||
@@ -49,44 +49,22 @@ const QuotaSection = ({ data }: Props) => {
|
||||
|
||||
return (
|
||||
<div className="mt-8">
|
||||
<p className="label label-text py-0">Quotas</p>
|
||||
|
||||
<label className="inline-flex label label-text gap-2 cursor-pointer">
|
||||
<Controller
|
||||
control={form.control}
|
||||
name="enabled"
|
||||
render={({ field }) => (
|
||||
<Toggle {...(field as any)} checked={field.value} />
|
||||
)}
|
||||
/>
|
||||
Enabled
|
||||
</label>
|
||||
<ToggleField form={form} name="enabled" title="Quotas" label="Enabled" />
|
||||
|
||||
{isEnabled && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<FormControl
|
||||
<InputField
|
||||
form={form}
|
||||
name="maxObjects"
|
||||
title="Max Objects"
|
||||
render={(field) => (
|
||||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
value={String(field.value || "")}
|
||||
/>
|
||||
)}
|
||||
type="number"
|
||||
/>
|
||||
<FormControl
|
||||
|
||||
<InputField
|
||||
form={form}
|
||||
name="maxSize"
|
||||
title="Max Size (GB)"
|
||||
render={(field) => (
|
||||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
value={String(field.value || "")}
|
||||
/>
|
||||
)}
|
||||
type="number"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { Controller, DeepPartial, useForm, useWatch } from "react-hook-form";
|
||||
import { DeepPartial, useForm, useWatch } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { websiteConfigSchema, WebsiteConfigSchema } from "../schema";
|
||||
import { useEffect } from "react";
|
||||
import { Input, Toggle } from "react-daisyui";
|
||||
import FormControl from "@/components/ui/form-control";
|
||||
import { useDebounce } from "@/hooks/useDebounce";
|
||||
import { useUpdateBucket } from "../hooks";
|
||||
import { useConfig } from "@/hooks/useConfig";
|
||||
import { Info, LinkIcon } from "lucide-react";
|
||||
import Button from "@/components/ui/button";
|
||||
import { Bucket } from "../../types";
|
||||
import { InputField } from "@/components/ui/input";
|
||||
import { ToggleField } from "@/components/ui/toggle";
|
||||
|
||||
type Props = {
|
||||
data?: Bucket;
|
||||
@@ -72,39 +72,24 @@ const WebsiteAccessSection = ({ data }: Props) => {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<label className="inline-flex label label-text gap-2 cursor-pointer">
|
||||
<Controller
|
||||
control={form.control}
|
||||
name="websiteAccess"
|
||||
render={({ field }) => (
|
||||
<Toggle {...(field as any)} checked={field.value} />
|
||||
)}
|
||||
/>
|
||||
Enabled
|
||||
</label>
|
||||
<ToggleField form={form} name="websiteAccess" label="Enabled" />
|
||||
|
||||
{isEnabled && (
|
||||
<>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<FormControl
|
||||
<InputField
|
||||
form={form}
|
||||
name="websiteConfig.indexDocument"
|
||||
title="Index Document"
|
||||
render={(field) => (
|
||||
<Input {...field} value={String(field.value || "")} />
|
||||
)}
|
||||
/>
|
||||
<FormControl
|
||||
<InputField
|
||||
form={form}
|
||||
name="websiteConfig.errorDocument"
|
||||
title="Error Document"
|
||||
render={(field) => (
|
||||
<Input {...field} value={String(field.value || "")} />
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 alert flex flex-row flex-wrap">
|
||||
<div className="mt-4 alert flex flex-row flex-wrap text-sm gap-x-2 gap-y-1">
|
||||
<a
|
||||
href={`http://${bucketName}`}
|
||||
className="inline-flex items-center flex-row gap-2 font-medium hover:link"
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import api from "@/lib/api";
|
||||
import { MutationOptions, useMutation, useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
MutationOptions,
|
||||
useMutation,
|
||||
UseMutationOptions,
|
||||
useQuery,
|
||||
} from "@tanstack/react-query";
|
||||
import { Bucket, Permissions } from "../types";
|
||||
|
||||
export const useBucket = (id?: string | null) => {
|
||||
@@ -18,6 +23,34 @@ export const useUpdateBucket = (id?: string | null) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const useAddAlias = (
|
||||
bucketId?: string | null,
|
||||
options?: UseMutationOptions<any, Error, string>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: (alias: string) => {
|
||||
return api.put("/v1/bucket/alias/global", {
|
||||
params: { id: bucketId, alias },
|
||||
});
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useRemoveAlias = (
|
||||
bucketId?: string | null,
|
||||
options?: UseMutationOptions<any, Error, string>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: (alias: string) => {
|
||||
return api.delete("/v1/bucket/alias/global", {
|
||||
params: { id: bucketId, alias },
|
||||
});
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useAllowKey = (
|
||||
bucketId?: string | null,
|
||||
options?: MutationOptions<
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const addAliasSchema = z.object({
|
||||
alias: z.string().min(1, "Alias is required"),
|
||||
});
|
||||
|
||||
export type AddAliasSchema = z.infer<typeof addAliasSchema>;
|
||||
|
||||
export const websiteConfigSchema = z.object({
|
||||
websiteAccess: z.boolean(),
|
||||
websiteConfig: z
|
||||
|
||||
@@ -12,7 +12,7 @@ const BucketsPage = () => {
|
||||
<Page title="Buckets" />
|
||||
|
||||
<div>
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-2">
|
||||
<Input placeholder="Search..." />
|
||||
<div className="flex-1" />
|
||||
<CreateBucketDialog />
|
||||
|
||||
@@ -128,7 +128,7 @@ const NodesList = ({ nodes }: NodeListProps) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-row items-center my-2 gap-4">
|
||||
<div className="flex flex-col sm:flex-row items-stretch sm:items-center my-2 gap-x-4 gap-y-2">
|
||||
<Input
|
||||
placeholder="Search..."
|
||||
value={filter.search}
|
||||
@@ -185,7 +185,7 @@ const NodesList = ({ nodes }: NodeListProps) => {
|
||||
) : null}
|
||||
|
||||
<div className="w-full overflow-x-auto min-h-[400px] pb-16">
|
||||
<Table size="sm">
|
||||
<Table size="sm" className="min-w-[800px]">
|
||||
<Table.Head>
|
||||
<span>#</span>
|
||||
<span>ID</span>
|
||||
|
||||
@@ -41,7 +41,7 @@ type DetailItemProps = {
|
||||
const DetailItem = ({ title, value }: DetailItemProps) => {
|
||||
return (
|
||||
<div className="flex flex-row items-start max-w-xl gap-3 text-left text-sm">
|
||||
<div className="shrink-0 w-[200px]">
|
||||
<div className="shrink-0 w-1/3 max-w-[200px]">
|
||||
<p className="text-base-content/80">{title}</p>
|
||||
</div>
|
||||
<div className="flex-1 truncate">
|
||||
|
||||
@@ -30,7 +30,7 @@ const StatsCard = ({
|
||||
{typeof value === "undefined" ? "..." : value}
|
||||
</p>
|
||||
)}
|
||||
<p className="text-sm mt-0.5">{title}</p>
|
||||
<p className="text-sm mt-0.5 truncate">{title}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
+15
-2
@@ -10,17 +10,25 @@ import {
|
||||
HardDrive,
|
||||
HardDriveUpload,
|
||||
Leaf,
|
||||
PieChart,
|
||||
} from "lucide-react";
|
||||
import { cn, ucfirst } from "@/lib/utils";
|
||||
import { cn, readableBytes, ucfirst } from "@/lib/utils";
|
||||
import { useBuckets } from "../buckets/hooks";
|
||||
import { useMemo } from "react";
|
||||
|
||||
const HomePage = () => {
|
||||
const { data: health } = useNodesHealth();
|
||||
const { data: buckets } = useBuckets();
|
||||
|
||||
const totalUsage = useMemo(() => {
|
||||
return buckets?.reduce((acc, bucket) => acc + bucket.bytes, 0);
|
||||
}, [buckets]);
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<Page title="Dashboard" />
|
||||
|
||||
<section className="grid grid-cols-1 md:grid-cols-3 gap-4 md:gap-6">
|
||||
<section className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 md:gap-6">
|
||||
<StatsCard
|
||||
title="Status"
|
||||
icon={Leaf}
|
||||
@@ -65,6 +73,11 @@ const HomePage = () => {
|
||||
icon={FileCheck}
|
||||
value={health?.partitionsAllOk}
|
||||
/>
|
||||
<StatsCard
|
||||
title="Total Usage"
|
||||
icon={PieChart}
|
||||
value={readableBytes(totalUsage)}
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -28,7 +28,7 @@ const KeysPage = () => {
|
||||
<div className="container">
|
||||
<Page title="Keys" />
|
||||
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-2">
|
||||
<Input placeholder="Search..." />
|
||||
<div className="flex-1" />
|
||||
<CreateKeyDialog />
|
||||
|
||||
Reference in New Issue
Block a user