mirror of
https://github.com/khairul169/garage-webui.git
synced 2026-09-15 00:43:21 +07:00
feat: add bucket object browser
This commit is contained in:
+3
-1
@@ -4,10 +4,12 @@ type FetchOptions = Omit<RequestInit, "headers" | "body"> & {
|
||||
body?: any;
|
||||
};
|
||||
|
||||
export const API_URL = "/api";
|
||||
|
||||
const api = {
|
||||
async fetch<T = any>(url: string, options?: Partial<FetchOptions>) {
|
||||
const headers: Record<string, string> = {};
|
||||
const _url = new URL("/api" + url, window.location.origin);
|
||||
const _url = new URL(API_URL + url, window.location.origin);
|
||||
|
||||
if (options?.params) {
|
||||
Object.entries(options.params).forEach(([key, value]) => {
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import clsx from "clsx";
|
||||
import { toast } from "sonner";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import dayjsRelativeTime from "dayjs/plugin/relativeTime";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
dayjs.extend(dayjsRelativeTime);
|
||||
export { dayjs };
|
||||
|
||||
export const cn = (...args: any[]) => {
|
||||
return twMerge(clsx(...args));
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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 = () => {
|
||||
|
||||
Reference in New Issue
Block a user