diff --git a/src/app/styles.css b/src/app/styles.css index 8751453..c49feab 100644 --- a/src/app/styles.css +++ b/src/app/styles.css @@ -20,6 +20,10 @@ @apply bg-base-100; } + .card-body { + @apply p-4 md:p-8; + } + .dropdown-content { @apply z-10; } diff --git a/src/components/containers/sidebar.tsx b/src/components/containers/sidebar.tsx index f5db872..84983e3 100644 --- a/src/components/containers/sidebar.tsx +++ b/src/components/containers/sidebar.tsx @@ -45,7 +45,7 @@ const Sidebar = () => { className={cn( "h-12 flex items-center px-6", isActive && - "bg-primary text-primary-content hover:bg-primary/60 focus:bg-primary" + "bg-primary text-primary-content hover:bg-primary/60 focus:bg-primary focus:text-primary-content" )} > <page.icon size={18} /> diff --git a/src/components/layouts/main-layout.tsx b/src/components/layouts/main-layout.tsx index b9632b2..db6c261 100644 --- a/src/components/layouts/main-layout.tsx +++ b/src/components/layouts/main-layout.tsx @@ -45,29 +45,33 @@ const Header = ({ onSidebarOpen }: HeaderProps) => { const navigate = useNavigate(); return ( - <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="-mx-2" - > - <ArrowLeft /> - </Button> - ) : ( - <Button - icon={MenuIcon} - color="ghost" - className="md:hidden -mx-2" - onClick={onSidebarOpen} - /> - )} + <header className="bg-base-100 px-4 md:px-8"> + <div className="container h-16 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="-mx-2" + > + <ArrowLeft /> + </Button> + ) : ( + <Button + icon={MenuIcon} + color="ghost" + className="md:hidden -mx-2" + onClick={onSidebarOpen} + /> + )} - <h1 className="text-xl flex-1 truncate">{page?.title || "Dashboard"}</h1> + <h1 className="text-xl flex-1 truncate"> + {page?.title || "Dashboard"} + </h1> - {page?.actions} + {page?.actions} + </div> </header> ); }; diff --git a/src/components/ui/toggle.tsx b/src/components/ui/toggle.tsx index 62bf409..6a3d1a0 100644 --- a/src/components/ui/toggle.tsx +++ b/src/components/ui/toggle.tsx @@ -42,7 +42,13 @@ export const ToggleField = <T extends FieldValues>({ title={title} className={className} render={(field) => ( - <Toggle {...props} {...field} className={inputClassName} /> + <Toggle + {...props} + {...field} + className={inputClassName} + checked={field.value || false} + onChange={(e) => field.onChange(e.target.checked)} + /> )} /> ); diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 1fd65bf..380b23e 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -22,3 +22,12 @@ export const readableBytes = (bytes?: number | null, divider = 1024) => { export const handleError = (err: unknown) => { toast.error((err as Error)?.message || "Unknown error"); }; + +export const copyToClipboard = async (text: string) => { + try { + await navigator.clipboard.writeText(text); + toast.success("Copied to clipboard"); + } catch (err) { + handleError(err); + } +}; diff --git a/src/pages/buckets/components/bucket-card.tsx b/src/pages/buckets/components/bucket-card.tsx index 449e201..7225a08 100644 --- a/src/pages/buckets/components/bucket-card.tsx +++ b/src/pages/buckets/components/bucket-card.tsx @@ -10,8 +10,8 @@ type Props = { const BucketCard = ({ data }: Props) => { return ( <div className="card card-body p-6"> - <div className="grid grid-cols-2 md:grid-cols-3 items-start gap-4 p-2 pb-0"> - <div className="flex flex-row items-start gap-x-3 col-span-2 md:col-span-1"> + <div className="grid grid-cols-2 items-start gap-4 p-2 pb-0"> + <div className="flex flex-row items-start gap-x-3 col-span-2"> <ArchiveIcon size={28} className="shrink-0" /> <p className="text-xl font-medium truncate"> @@ -38,7 +38,7 @@ const BucketCard = ({ data }: Props) => { </div> </div> - <div className="mt-1 flex flex-row justify-end gap-4"> + <div className="flex flex-row justify-end gap-4"> <Button href={`/buckets/${data.id}`}>Manage</Button> {/* <Button color="primary">Browse</Button> */} </div> diff --git a/src/pages/buckets/page.tsx b/src/pages/buckets/page.tsx index 48b6c2d..5240543 100644 --- a/src/pages/buckets/page.tsx +++ b/src/pages/buckets/page.tsx @@ -3,9 +3,24 @@ import { useBuckets } from "./hooks"; import { Input } from "react-daisyui"; import BucketCard from "./components/bucket-card"; import CreateBucketDialog from "./components/create-bucket-dialog"; +import { useMemo, useState } from "react"; const BucketsPage = () => { const { data } = useBuckets(); + const [search, setSearch] = useState(""); + + const items = useMemo(() => { + if (!search?.length) { + return data; + } + + const q = search.toLowerCase(); + return data?.filter( + (bucket) => + bucket.id.includes(q) || + bucket.globalAliases.find((alias) => alias.includes(q)) + ); + }, [data, search]); return ( <div className="container"> @@ -13,13 +28,17 @@ const BucketsPage = () => { <div> <div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-2"> - <Input placeholder="Search..." /> + <Input + placeholder="Search..." + value={search} + onChange={(e) => setSearch(e.target.value)} + /> <div className="flex-1" /> <CreateBucketDialog /> </div> <div className="grid grid-cols-1 lg:grid-cols-2 gap-4 md:gap-8 items-stretch mt-4 md:mt-8"> - {data?.map((bucket) => ( + {items?.map((bucket) => ( <BucketCard key={bucket.id} data={bucket} /> ))} </div> diff --git a/src/pages/keys/page.tsx b/src/pages/keys/page.tsx index da5fa79..e8ce2e2 100644 --- a/src/pages/keys/page.tsx +++ b/src/pages/keys/page.tsx @@ -1,14 +1,18 @@ import Button from "@/components/ui/button"; import Page from "@/context/page-context"; -import { Trash } from "lucide-react"; +import { Copy, Eye, Trash } from "lucide-react"; import { Card, Input, Table } from "react-daisyui"; import { useKeys, useRemoveKey } from "./hooks"; import CreateKeyDialog from "./components/create-key-dialog"; import { toast } from "sonner"; -import { handleError } from "@/lib/utils"; +import { copyToClipboard, handleError } from "@/lib/utils"; +import { useCallback, useMemo, useState } from "react"; +import api from "@/lib/api"; const KeysPage = () => { const { data, refetch } = useKeys(); + const [search, setSearch] = useState(""); + const [secretKeys, setSecretKeys] = useState<Record<string, string>>({}); const removeKey = useRemoveKey({ onSuccess: () => { @@ -18,18 +22,45 @@ const KeysPage = () => { onError: handleError, }); + const fetchSecretKey = useCallback(async (id: string) => { + try { + const result = await api.get("/v1/key", { + params: { id, showSecretKey: "true" }, + }); + if (!result?.secretAccessKey) { + throw new Error("Failed to fetch secret key"); + } + setSecretKeys((prev) => ({ ...prev, [id]: result.secretAccessKey })); + } catch (err) { + handleError(err); + } + }, []); + const onRemove = (id: string) => { if (window.confirm("Are you sure you want to remove this key?")) { removeKey.mutate(id); } }; + const items = useMemo(() => { + if (!search?.length) { + return data; + } + + const q = search.toLowerCase(); + return data?.filter((item) => item.id.includes(q) || item.name.includes(q)); + }, [data, search]); + return ( <div className="container"> <Page title="Keys" /> <div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-2"> - <Input placeholder="Search..." /> + <Input + placeholder="Search..." + value={search} + onChange={(e) => setSearch(e.target.value)} + /> <div className="flex-1" /> <CreateKeyDialog /> </div> @@ -39,17 +70,51 @@ const KeysPage = () => { <Table zebra> <Table.Head> <span>#</span> - <span>Key ID</span> <span>Name</span> + <span>Key ID</span> + <span>Secret Key</span> <span /> </Table.Head> <Table.Body> - {data?.map((key, idx) => ( + {items?.map((key, idx) => ( <Table.Row key={key.id}> <span>{idx + 1}</span> - <span className="truncate">{key.id}</span> <span>{key.name}</span> + <div className="flex flex-row items-center"> + <p className="truncate max-w-20" title={key.id}> + {key.id} + </p> + <Button + size="sm" + icon={Copy} + onClick={() => copyToClipboard(key.id)} + /> + </div> + {!secretKeys[key.id] ? ( + <Button + icon={Eye} + size="sm" + onClick={() => fetchSecretKey(key.id)} + > + View + </Button> + ) : ( + <div className="flex flex-row items-center"> + <p + className="font-mono max-w-20 truncate" + title={secretKeys[key.id]} + > + {secretKeys[key.id]} + </p> + <Button + size="sm" + icon={Copy} + onClick={() => copyToClipboard(secretKeys[key.id])} + /> + </div> + )} + <span> <Button color="ghost"