mirror of
https://github.com/khairul169/garage-webui.git
synced 2026-09-18 18:27:39 +07:00
feat: add keys & bucket permissions management
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
import Button from "@/components/ui/button";
|
||||
import { useKeys } from "@/pages/keys/hooks";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Plus } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { Checkbox, Modal, Table } from "react-daisyui";
|
||||
import { useFieldArray, useForm } from "react-hook-form";
|
||||
import { AllowKeysSchema, allowKeysSchema } from "../schema";
|
||||
import { useDisclosure } from "@/hooks/useDisclosure";
|
||||
import { CheckboxField } from "@/components/ui/checkbox";
|
||||
import { useAllowKey } from "../hooks";
|
||||
import { toast } from "sonner";
|
||||
import { handleError } from "@/lib/utils";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
type Props = {
|
||||
id?: string;
|
||||
currentKeys?: string[];
|
||||
};
|
||||
|
||||
const AllowKeyDialog = ({ id, currentKeys }: Props) => {
|
||||
const { dialogRef, isOpen, onOpen, onClose } = useDisclosure();
|
||||
const { data: keys } = useKeys();
|
||||
const form = useForm<AllowKeysSchema>({
|
||||
resolver: zodResolver(allowKeysSchema),
|
||||
});
|
||||
const { fields: keyFields } = useFieldArray({
|
||||
control: form.control,
|
||||
name: "keys",
|
||||
});
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const allowKey = useAllowKey(id, {
|
||||
onSuccess: () => {
|
||||
form.reset();
|
||||
onClose();
|
||||
toast.success("Key allowed!");
|
||||
queryClient.invalidateQueries({ queryKey: ["bucket", id] });
|
||||
},
|
||||
onError: handleError,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const _keys = keys
|
||||
?.filter((key) => !currentKeys?.includes(key.id))
|
||||
?.map((key) => ({
|
||||
checked: false,
|
||||
keyId: key.id,
|
||||
name: key.name,
|
||||
read: false,
|
||||
write: false,
|
||||
owner: false,
|
||||
}));
|
||||
|
||||
form.setValue("keys", _keys || []);
|
||||
}, [keys, currentKeys]);
|
||||
|
||||
const onToggleAll = (
|
||||
e: React.ChangeEvent<HTMLInputElement>,
|
||||
field: keyof AllowKeysSchema["keys"][number]
|
||||
) => {
|
||||
const curValues = form.getValues("keys");
|
||||
const newValues = curValues.map((item) => ({
|
||||
...item,
|
||||
[field]: e.target.checked,
|
||||
}));
|
||||
form.setValue("keys", newValues);
|
||||
};
|
||||
|
||||
const onSubmit = form.handleSubmit((values) => {
|
||||
const data = values.keys
|
||||
.filter((key) => key.checked)
|
||||
.map((key) => ({
|
||||
keyId: key.keyId,
|
||||
permissions: { read: key.read, write: key.write, owner: key.owner },
|
||||
}));
|
||||
allowKey.mutate(data);
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button icon={Plus} color="primary" onClick={onOpen}>
|
||||
Allow Key
|
||||
</Button>
|
||||
|
||||
<Modal ref={dialogRef} backdrop open={isOpen}>
|
||||
<Modal.Header className="mb-1">Allow Key</Modal.Header>
|
||||
<Modal.Body>
|
||||
<p>Enter the key you want to allow access to.</p>
|
||||
|
||||
<div className="overflow-x-auto mt-4">
|
||||
<Table>
|
||||
<Table.Head>
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<Checkbox
|
||||
color="primary"
|
||||
size="sm"
|
||||
onChange={(e) => onToggleAll(e, "checked")}
|
||||
/>
|
||||
Key
|
||||
</label>
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<Checkbox
|
||||
color="primary"
|
||||
size="sm"
|
||||
onChange={(e) => onToggleAll(e, "read")}
|
||||
/>
|
||||
Read
|
||||
</label>
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<Checkbox
|
||||
color="primary"
|
||||
size="sm"
|
||||
onChange={(e) => onToggleAll(e, "write")}
|
||||
/>
|
||||
Write
|
||||
</label>
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<Checkbox
|
||||
color="primary"
|
||||
size="sm"
|
||||
onChange={(e) => onToggleAll(e, "owner")}
|
||||
/>
|
||||
Owner
|
||||
</label>
|
||||
</Table.Head>
|
||||
|
||||
<Table.Body>
|
||||
{!keyFields.length ? (
|
||||
<tr>
|
||||
<td colSpan={4} className="text-center">
|
||||
No keys found
|
||||
</td>
|
||||
</tr>
|
||||
) : null}
|
||||
{keyFields.map((field, index) => (
|
||||
<Table.Row key={field.id}>
|
||||
<CheckboxField
|
||||
form={form}
|
||||
name={`keys.${index}.checked`}
|
||||
label={field.name || field.keyId?.substring(0, 8)}
|
||||
/>
|
||||
<CheckboxField form={form} name={`keys.${index}.read`} />
|
||||
<CheckboxField form={form} name={`keys.${index}.write`} />
|
||||
<CheckboxField form={form} name={`keys.${index}.owner`} />
|
||||
</Table.Row>
|
||||
))}
|
||||
</Table.Body>
|
||||
</Table>
|
||||
</div>
|
||||
</Modal.Body>
|
||||
|
||||
<Modal.Actions>
|
||||
<Button onClick={onClose}>Cancel</Button>
|
||||
<Button
|
||||
color="primary"
|
||||
disabled={allowKey.isPending}
|
||||
onClick={onSubmit}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</Modal.Actions>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AllowKeyDialog;
|
||||
@@ -0,0 +1,42 @@
|
||||
import Button from "@/components/ui/button";
|
||||
import { EllipsisVertical, Trash } from "lucide-react";
|
||||
import { Dropdown } from "react-daisyui";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { useRemoveBucket } from "../hooks";
|
||||
import { toast } from "sonner";
|
||||
import { handleError } from "@/lib/utils";
|
||||
|
||||
const MenuButton = () => {
|
||||
const { id } = useParams();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const removeBucket = useRemoveBucket({
|
||||
onSuccess: () => {
|
||||
toast.success("Bucket removed!");
|
||||
navigate("/buckets", { replace: true });
|
||||
},
|
||||
onError: handleError,
|
||||
});
|
||||
|
||||
const onRemove = () => {
|
||||
if (window.confirm("Are you sure you want to remove this bucket?")) {
|
||||
removeBucket.mutate(id!);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dropdown end>
|
||||
<Dropdown.Toggle button={false}>
|
||||
<Button icon={EllipsisVertical} />
|
||||
</Dropdown.Toggle>
|
||||
|
||||
<Dropdown.Menu>
|
||||
<Dropdown.Item onClick={onRemove} className="bg-error/10 text-error">
|
||||
<Trash /> Remove
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
|
||||
export default MenuButton;
|
||||
@@ -4,7 +4,7 @@ import Chips from "@/components/ui/chips";
|
||||
import { Bucket } from "../../types";
|
||||
|
||||
type Props = {
|
||||
data: Bucket;
|
||||
data?: Bucket;
|
||||
};
|
||||
|
||||
const AliasesSection = ({ data }: Props) => {
|
||||
|
||||
@@ -9,7 +9,7 @@ import { useUpdateBucket } from "../hooks";
|
||||
import { Bucket } from "../../types";
|
||||
|
||||
type Props = {
|
||||
data: Bucket;
|
||||
data?: Bucket;
|
||||
};
|
||||
|
||||
const QuotaSection = ({ data }: Props) => {
|
||||
|
||||
@@ -12,8 +12,8 @@ const OverviewTab = () => {
|
||||
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">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 md:gap-8 items-start">
|
||||
<Card className="card-body gap-0 items-start order-2 md:order-1">
|
||||
<Card.Title>Summary</Card.Title>
|
||||
|
||||
<AliasesSection data={data} />
|
||||
@@ -21,7 +21,7 @@ const OverviewTab = () => {
|
||||
<QuotaSection data={data} />
|
||||
</Card>
|
||||
|
||||
<Card className="card-body">
|
||||
<Card className="card-body order-1 md:order-2">
|
||||
<Card.Title>Usage</Card.Title>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4 mt-4">
|
||||
|
||||
@@ -12,7 +12,7 @@ import Button from "@/components/ui/button";
|
||||
import { Bucket } from "../../types";
|
||||
|
||||
type Props = {
|
||||
data: Bucket;
|
||||
data?: Bucket;
|
||||
};
|
||||
|
||||
const WebsiteAccessSection = ({ data }: Props) => {
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useBucket, useDenyKey } from "../hooks";
|
||||
import { Card, Checkbox, Table } from "react-daisyui";
|
||||
import Button from "@/components/ui/button";
|
||||
import { Trash } from "lucide-react";
|
||||
import AllowKeyDialog from "./allow-key-dialog";
|
||||
import { useMemo } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { handleError } from "@/lib/utils";
|
||||
|
||||
const PermissionsTab = () => {
|
||||
const { id } = useParams();
|
||||
const { data, refetch } = useBucket(id);
|
||||
|
||||
const denyKey = useDenyKey(id, {
|
||||
onSuccess: () => {
|
||||
toast.success("Key removed!");
|
||||
refetch();
|
||||
},
|
||||
onError: handleError,
|
||||
});
|
||||
|
||||
const keys = useMemo(() => {
|
||||
return data?.keys.filter(
|
||||
(key) =>
|
||||
key.permissions.read !== false ||
|
||||
key.permissions.write !== false ||
|
||||
key.permissions.owner !== false
|
||||
);
|
||||
}, [data?.keys]);
|
||||
|
||||
const onRemove = (id: string) => {
|
||||
if (window.confirm("Are you sure you want to remove this key?")) {
|
||||
denyKey.mutate({
|
||||
keyId: id,
|
||||
permissions: { read: true, write: true, owner: true },
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Card className="card-body">
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<Card.Title className="flex-1 truncate">Access Keys</Card.Title>
|
||||
<AllowKeyDialog
|
||||
id={id}
|
||||
currentKeys={keys?.map((key) => key.accessKeyId)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
<Table zebra size="sm">
|
||||
<Table.Head>
|
||||
<span>#</span>
|
||||
<span>Key</span>
|
||||
<span>Read</span>
|
||||
<span>Write</span>
|
||||
<span>Owner</span>
|
||||
<span />
|
||||
</Table.Head>
|
||||
|
||||
<Table.Body>
|
||||
{keys?.map((key, idx) => (
|
||||
<Table.Row>
|
||||
<span>{idx + 1}</span>
|
||||
<span>{key.name || key.accessKeyId?.substring(0, 8)}</span>
|
||||
<span>
|
||||
<Checkbox
|
||||
checked={key.permissions?.read}
|
||||
color="primary"
|
||||
className="cursor-default"
|
||||
/>
|
||||
</span>
|
||||
<span>
|
||||
<Checkbox
|
||||
checked={key.permissions?.write}
|
||||
color="primary"
|
||||
className="cursor-default"
|
||||
/>
|
||||
</span>
|
||||
<span>
|
||||
<Checkbox
|
||||
checked={key.permissions?.owner}
|
||||
color="primary"
|
||||
className="cursor-default"
|
||||
/>
|
||||
</span>
|
||||
<Button
|
||||
icon={Trash}
|
||||
onClick={() => onRemove(key.accessKeyId)}
|
||||
/>
|
||||
</Table.Row>
|
||||
))}
|
||||
</Table.Body>
|
||||
</Table>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PermissionsTab;
|
||||
Reference in New Issue
Block a user