feat: add cluster & bucket management

This commit is contained in:
2024-08-16 01:23:55 +07:00
parent b0e5d53ee0
commit dfb4e30e23
41 changed files with 1394 additions and 67 deletions
@@ -0,0 +1,32 @@
import { Button } from "react-daisyui";
import { Plus } from "lucide-react";
import Chips from "@/components/ui/chips";
import { Bucket } from "../../types";
type Props = {
data: Bucket;
};
const AliasesSection = ({ data }: Props) => {
const aliases = data?.globalAliases?.slice(1);
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={() => {}}>
{alias}
</Chips>
))}
<Button size="sm">
<Plus className="-ml-1" size={18} />
Add Alias
</Button>
</div>
</div>
);
};
export default AliasesSection;
@@ -0,0 +1,97 @@
import { Controller, 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";
type Props = {
data: Bucket;
};
const QuotaSection = ({ data }: Props) => {
const form = useForm<QuotaSchema>({
resolver: zodResolver(quotaSchema),
});
const isEnabled = useWatch({ control: form.control, name: "enabled" });
const updateMutation = useUpdateBucket(data?.id);
const onChange = useDebounce((values: DeepPartial<QuotaSchema>) => {
const { enabled } = values;
const maxObjects = Number(values.maxObjects);
const maxSize = Math.round(Number(values.maxSize) * 1024 * 1024);
const data = {
maxObjects: enabled && maxObjects > 0 ? maxObjects : null,
maxSize: enabled && maxSize > 0 ? maxSize : null,
};
updateMutation.mutate({ quotas: data });
});
useEffect(() => {
form.reset({
enabled:
data?.quotas?.maxSize != null || data?.quotas?.maxObjects != null,
maxSize: data?.quotas?.maxSize
? data?.quotas?.maxSize / 1024 / 1024
: null,
maxObjects: data?.quotas?.maxObjects || null,
});
const { unsubscribe } = form.watch((values) => onChange(values));
return unsubscribe;
}, [data]);
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>
{isEnabled && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormControl
form={form}
name="maxObjects"
title="Max Objects"
render={(field) => (
<Input
{...field}
type="number"
value={String(field.value || "")}
/>
)}
/>
<FormControl
form={form}
name="maxSize"
title="Max Size (GB)"
render={(field) => (
<Input
{...field}
type="number"
value={String(field.value || "")}
/>
)}
/>
</div>
)}
</div>
);
};
export default QuotaSection;
@@ -0,0 +1,51 @@
import { Card } from "react-daisyui";
import { useParams } from "react-router-dom";
import { useBucket } from "../hooks";
import { ChartPie, ChartScatter } from "lucide-react";
import { readableBytes } from "@/lib/utils";
import WebsiteAccessSection from "./overview-website-access";
import AliasesSection from "./overview-aliases";
import QuotaSection from "./overview-quota";
const OverviewTab = () => {
const { id } = useParams();
const { data } = useBucket(id);
return (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 md:gap-8">
<Card className="card-body gap-0 items-start">
<Card.Title>Summary</Card.Title>
<AliasesSection data={data} />
<WebsiteAccessSection data={data} />
<QuotaSection data={data} />
</Card>
<Card className="card-body">
<Card.Title>Usage</Card.Title>
<div className="grid grid-cols-2 gap-4 mt-4">
<div className="flex flex-row gap-3">
<ChartPie className="mt-1" size={20} />
<div className="flex-1">
<p className="text-sm flex items-center gap-1">Storage</p>
<p className="text-2xl font-medium">
{readableBytes(data?.bytes)}
</p>
</div>
</div>
<div className="flex flex-row gap-3">
<ChartScatter className="mt-1" size={20} />
<div className="flex-1">
<p className="text-sm flex items-center gap-1">Objects</p>
<p className="text-2xl font-medium">{data?.objects}</p>
</div>
</div>
</div>
</Card>
</div>
);
};
export default OverviewTab;
@@ -0,0 +1,143 @@
import { Controller, 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";
type Props = {
data: Bucket;
};
const WebsiteAccessSection = ({ data }: Props) => {
const { data: config } = useConfig();
const form = useForm<WebsiteConfigSchema>({
resolver: zodResolver(websiteConfigSchema),
});
const bucketName = data?.globalAliases[0] || "";
const isEnabled = useWatch({ control: form.control, name: "websiteAccess" });
const websitePort = config?.s3_web?.bind_addr?.split(":").pop() || "80";
const rootDomain = config?.s3_web?.root_domain;
const updateMutation = useUpdateBucket(data?.id);
const onChange = useDebounce((values: DeepPartial<WebsiteConfigSchema>) => {
const data = {
enabled: values.websiteAccess,
indexDocument: values.websiteAccess
? values.websiteConfig?.indexDocument
: undefined,
errorDocument: values.websiteAccess
? values.websiteConfig?.errorDocument
: undefined,
};
updateMutation.mutate({
websiteAccess: data,
});
});
useEffect(() => {
form.reset({
websiteAccess: data?.websiteAccess,
websiteConfig: {
indexDocument: data?.websiteConfig?.indexDocument || "index.html",
errorDocument: data?.websiteConfig?.errorDocument || "error/400.html",
},
});
const { unsubscribe } = form.watch((values) => onChange(values));
return unsubscribe;
}, [data]);
return (
<div className="mt-8">
<div className="flex flex-row gap-2">
<p className="label label-text py-0 grow-0">Website Access</p>
<Button
href="https://garagehq.deuxfleurs.fr/documentation/cookbook/exposing-websites"
target="_blank"
size="sm"
shape="circle"
color="ghost"
>
<Info size={16} />
</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>
{isEnabled && (
<>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormControl
form={form}
name="websiteConfig.indexDocument"
title="Index Document"
render={(field) => (
<Input {...field} value={String(field.value || "")} />
)}
/>
<FormControl
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">
<a
href={`http://${bucketName}`}
className="inline-flex items-center flex-row gap-2 font-medium hover:link"
target="_blank"
>
<LinkIcon size={14} />
{bucketName}
</a>
{rootDomain ? (
<>
<a
href={`http://${bucketName}${rootDomain}`}
className="inline-flex items-center flex-row gap-2 font-medium hover:link"
target="_blank"
>
<LinkIcon size={14} />
{bucketName + rootDomain}
</a>
<a
href={`http://${bucketName}${rootDomain}:${websitePort}`}
className="inline-flex items-center flex-row gap-2 font-medium hover:link"
target="_blank"
>
<LinkIcon size={14} />
{bucketName + rootDomain + ":" + websitePort}
</a>
</>
) : null}
</div>
</>
)}
</div>
);
};
export default WebsiteAccessSection;