feat: add bucket object browser

This commit is contained in:
2024-08-18 22:57:08 +07:00
parent 3a147f4133
commit 934e0c409c
29 changed files with 656 additions and 18 deletions
+3 -1
View File
@@ -40,7 +40,9 @@ const BucketCard = ({ data }: Props) => {
<div className="flex flex-row justify-end gap-4">
<Button href={`/buckets/${data.id}`}>Manage</Button>
{/* <Button color="primary">Browse</Button> */}
<Button color="primary" href={`/buckets/${data.id}?tab=browse`}>
Browse
</Button>
</div>
</div>
);
@@ -0,0 +1,74 @@
import { useParams } from "react-router-dom";
import { Card } from "react-daisyui";
import ObjectList from "./object-list";
import { useBucket } from "../hooks";
import { useState } from "react";
import ObjectListNavigator from "./object-list-navigator";
import {
EllipsisVertical,
FilePlus,
FolderPlus,
UploadIcon,
} from "lucide-react";
import Button from "@/components/ui/button";
const BrowseTab = () => {
const { id } = useParams();
const { data: bucket } = useBucket(id);
const [curPrefix, setCurPrefix] = useState(-1);
const [prefixHistory, setPrefixHistory] = useState<string[]>([]);
const bucketName = bucket?.globalAliases[0];
const gotoPrefix = (prefix: string) => {
const history = prefixHistory.slice(0, curPrefix + 1);
setPrefixHistory([...history, prefix]);
setCurPrefix(history.length);
};
if (!bucket) {
return null;
}
if (!bucket.keys.find((k) => k.permissions.read && k.permissions.write)) {
return (
<div className="p-4 min-h-[200px] flex flex-col justify-center">
<p className="text-center">
You need to add a key to your bucket to be able to browse it.
</p>
</div>
);
}
return (
<div>
<Card>
<ObjectListNavigator
bucketName={bucketName}
curPrefix={curPrefix}
setCurPrefix={setCurPrefix}
prefixHistory={prefixHistory}
actions={
<>
<Button icon={FolderPlus} color="ghost" />
<Button icon={FilePlus} color="ghost" />
<Button icon={UploadIcon} color="ghost" />
<Button icon={EllipsisVertical} color="ghost" />
</>
}
/>
{bucketName ? (
<ObjectList
bucket={bucketName}
prefix={prefixHistory[curPrefix] || ""}
onPrefixChange={gotoPrefix}
/>
) : null}
</Card>
</div>
);
};
export default BrowseTab;
+14
View File
@@ -0,0 +1,14 @@
import api from "@/lib/api";
import { useQuery } from "@tanstack/react-query";
import { GetObjectsResult, UseBrowserObjectOptions } from "./types";
export const useBrowseObjects = (
bucket: string,
options?: UseBrowserObjectOptions
) => {
return useQuery({
queryKey: ["browse", bucket, options],
queryFn: () =>
api.get<GetObjectsResult>(`/browse/${bucket}`, { params: options }),
});
};
@@ -0,0 +1,102 @@
import Button from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { Fragment } from "react/jsx-runtime";
type Props = {
bucketName?: string;
curPrefix: number;
setCurPrefix: React.Dispatch<React.SetStateAction<number>>;
prefixHistory: string[];
actions?: React.ReactNode;
};
const ObjectListNavigator = ({
bucketName,
curPrefix,
setCurPrefix,
prefixHistory,
actions,
}: Props) => {
const onGoBack = () => {
if (curPrefix >= 0) setCurPrefix(curPrefix - 1);
};
const onGoForward = () => {
if (curPrefix < prefixHistory.length - 1) setCurPrefix(curPrefix + 1);
};
return (
<div className="flex flex-row flex-wrap items-center p-2 gap-y-2">
<div className="order-1 flex flex-row items-center">
<Button
icon={ChevronLeft}
color="ghost"
disabled={curPrefix < 0}
onClick={onGoBack}
className="col-span-2"
/>
<Button
icon={ChevronRight}
color="ghost"
disabled={curPrefix >= prefixHistory.length - 1}
onClick={onGoForward}
className="col-span-2"
/>
</div>
<div className="order-3 md:order-2 flex flex-row w-full overflow-x-auto items-center bg-base-200 h-10 flex-1 shrink-0 min-w-[80%] md:min-w-0 rounded-lg mx-2 pl-4">
<HistoryItem
title={bucketName}
isActive={curPrefix === -1}
onClick={() => setCurPrefix(-1)}
/>
{prefixHistory.map((prefix, i) => (
<Fragment key={prefix}>
<ChevronRight className="shrink-0" size={20} />
<HistoryItem
title={prefix
.substring(0, prefix.lastIndexOf("/"))
.split("/")
.pop()}
isActive={i === curPrefix}
onClick={() => setCurPrefix(i)}
/>
</Fragment>
))}
</div>
<div className="order-2 flex flex-row items-center flex-1 md:order-3 md:flex-initial justify-end">
{actions}
</div>
</div>
);
};
type HistoryItemProps = {
title?: string;
isActive: boolean;
onClick: () => void;
};
const HistoryItem = ({ title, isActive, onClick }: HistoryItemProps) => {
if (!title) {
return null;
}
return (
<a
href="#"
onClick={(e) => {
e.preventDefault();
onClick();
}}
className={cn("px-2 rounded-sm shrink-0", isActive && "bg-neutral")}
>
{title}
</a>
);
};
export default ObjectListNavigator;
@@ -0,0 +1,66 @@
import { Table } from "react-daisyui";
import { useBrowseObjects } from "./hooks";
import { dayjs, readableBytes } from "@/lib/utils";
import { Object } from "./types";
import { API_URL } from "@/lib/api";
type Props = {
bucket: string;
prefix?: string;
onPrefixChange?: (prefix: string) => void;
};
const ObjectList = ({ bucket, prefix, onPrefixChange }: Props) => {
const { data } = useBrowseObjects(bucket, { prefix });
const onObjectClick = (object: Object) => {
window.open(
API_URL + `/browse/${bucket}/${data?.prefix}${object.objectKey}?view=1`,
"_blank"
);
};
return (
<div className="overflow-x-auto">
<Table>
<Table.Head>
<span>Name</span>
<span>Size</span>
<span>Last Modified</span>
</Table.Head>
<Table.Body>
{data?.prefixes.map((prefix) => (
<Table.Row
key={prefix}
className="hover:bg-neutral cursor-pointer"
role="button"
onClick={() => onPrefixChange?.(prefix)}
>
<span>
{prefix.substring(0, prefix.lastIndexOf("/")).split("/").pop()}
</span>
<span />
<span />
</Table.Row>
))}
{data?.objects.map((object) => (
<Table.Row
key={object.objectKey}
className="hover:bg-neutral cursor-pointer"
role="button"
onClick={() => onObjectClick(object)}
>
<span>{object.objectKey}</span>
<span>{readableBytes(object.size)}</span>
<span>{dayjs(object.lastModified).fromNow()}</span>
</Table.Row>
))}
</Table.Body>
</Table>
</div>
);
};
export default ObjectList;
+18
View File
@@ -0,0 +1,18 @@
export type UseBrowserObjectOptions = Partial<{
prefix: string;
limit: number;
next: string;
}>;
export type GetObjectsResult = {
prefixes: string[];
objects: Object[];
prefix: string;
nextToken: string | null;
};
export type Object = {
objectKey: string;
lastModified: Date;
size: number;
};
+10 -8
View File
@@ -2,10 +2,11 @@ import { useParams } from "react-router-dom";
import { useBucket } from "./hooks";
import Page from "@/context/page-context";
import TabView, { Tab } from "@/components/containers/tab-view";
import { ChartLine, LockKeyhole } from "lucide-react";
import OverviewTab from "./components/overview-tab";
import PermissionsTab from "./components/permissions-tab";
import { ChartLine, FolderSearch, LockKeyhole } from "lucide-react";
import OverviewTab from "./overview/overview-tab";
import PermissionsTab from "./permissions/permissions-tab";
import MenuButton from "./components/menu-button";
import BrowseTab from "./browse/browse-tab";
const tabs: Tab[] = [
{
@@ -20,11 +21,12 @@ const tabs: Tab[] = [
icon: LockKeyhole,
Component: PermissionsTab,
},
// {
// name: "browse",
// title: "Browse",
// icon: FolderSearch,
// },
{
name: "browse",
title: "Browse",
icon: FolderSearch,
Component: BrowseTab,
},
];
const ManageBucketPage = () => {