mirror of
https://github.com/Noooste/garage-ui.git
synced 2026-08-27 15:37:03 +00:00
d494c811a0
Signed-off-by: Noste <83548733+Noooste@users.noreply.github.com>
291 lines
9.0 KiB
TypeScript
291 lines
9.0 KiB
TypeScript
import { useState, useEffect } from 'react';
|
|
import { useQueryClient } from '@tanstack/react-query';
|
|
import { useSearchParams } from 'react-router-dom';
|
|
import { Header } from '@/components/layout/header';
|
|
import { useBuckets, useCreateBucket, useDeleteBucket, useGrantBucketPermission } from '@/hooks/useApi';
|
|
import { useBucketObjects } from '@/hooks/useBucketObjects';
|
|
import { BucketListView } from '@/components/buckets/BucketListView';
|
|
import { ObjectBrowserView } from '@/components/buckets/ObjectBrowserView';
|
|
import { CreateBucketDialog } from '@/components/buckets/CreateBucketDialog';
|
|
import { DeleteBucketDialog } from '@/components/buckets/DeleteBucketDialog';
|
|
import { BucketSettingsDialog } from '@/components/buckets/BucketSettingsDialog';
|
|
import { BucketWebsiteDialog } from '@/components/buckets/BucketWebsiteDialog';
|
|
import type { Bucket } from '@/types';
|
|
import { toast } from 'sonner';
|
|
import { bucketsApi } from '@/lib/api';
|
|
|
|
export function Buckets() {
|
|
const [searchParams, setSearchParams] = useSearchParams();
|
|
|
|
// Bucket state
|
|
const [searchQuery, setSearchQuery] = useState('');
|
|
const [createDialogOpen, setCreateDialogOpen] = useState(false);
|
|
const [deleteBucketDialogOpen, setDeleteBucketDialogOpen] = useState(false);
|
|
const [selectedBucket, setSelectedBucket] = useState<Bucket | null>(null);
|
|
const [settingsDialogOpen, setSettingsDialogOpen] = useState(false);
|
|
const [settingsBucket, setSettingsBucket] = useState<Bucket | null>(null);
|
|
const [websiteDialogOpen, setWebsiteDialogOpen] = useState(false);
|
|
const [websiteBucket, setWebsiteBucket] = useState<Bucket | null>(null);
|
|
|
|
// Object browser state - initialize from URL params
|
|
const [viewingBucket, setViewingBucket] = useState<string | null>(searchParams.get('bucket'));
|
|
const [currentPath, setCurrentPath] = useState<string>(searchParams.get('prefix') || '');
|
|
const [objectSearchQuery, setObjectSearchQuery] = useState('');
|
|
const [initialPageToken, setInitialPageToken] = useState<string | undefined>(
|
|
searchParams.get('page') || undefined
|
|
);
|
|
const [initialItemsPerPage, setInitialItemsPerPage] = useState<number>(
|
|
parseInt(searchParams.get('limit') || '25', 10)
|
|
);
|
|
|
|
// Sync URL params with state on mount and when URL changes
|
|
useEffect(() => {
|
|
const bucketParam = searchParams.get('bucket');
|
|
const prefixParam = searchParams.get('prefix') || '';
|
|
const pageParam = searchParams.get('page') || undefined;
|
|
const limitParam = parseInt(searchParams.get('limit') || '25', 10);
|
|
|
|
if (bucketParam !== viewingBucket) {
|
|
setViewingBucket(bucketParam);
|
|
}
|
|
if (prefixParam !== currentPath) {
|
|
setCurrentPath(prefixParam);
|
|
}
|
|
setInitialPageToken(pageParam);
|
|
setInitialItemsPerPage(limitParam);
|
|
}, [searchParams]);
|
|
|
|
// Custom hooks
|
|
const queryClient = useQueryClient();
|
|
const { data: buckets = [], isLoading: bucketsLoading } = useBuckets();
|
|
const createBucketMutation = useCreateBucket();
|
|
const deleteBucketMutation = useDeleteBucket();
|
|
const grantPermissionMutation = useGrantBucketPermission();
|
|
const {
|
|
objects,
|
|
isLoading: objectsLoading,
|
|
isRefreshing,
|
|
isNavigating,
|
|
isTruncated,
|
|
nextContinuationToken,
|
|
itemsPerPage,
|
|
setItemsPerPage,
|
|
uploadFiles,
|
|
uploadTasks,
|
|
deleteObject,
|
|
deleteMultipleObjects,
|
|
createDirectory,
|
|
fetchObjects
|
|
} = useBucketObjects(
|
|
viewingBucket,
|
|
currentPath
|
|
);
|
|
|
|
const handleViewBucket = (bucketName: string) => {
|
|
setViewingBucket(bucketName);
|
|
setCurrentPath('');
|
|
setObjectSearchQuery('');
|
|
setSearchParams({ bucket: bucketName });
|
|
};
|
|
|
|
const handleBackToBuckets = () => {
|
|
setViewingBucket(null);
|
|
setCurrentPath('');
|
|
setObjectSearchQuery('');
|
|
setSearchParams({});
|
|
};
|
|
|
|
const handleNavigateToFolder = (path: string) => {
|
|
setCurrentPath(path);
|
|
if (viewingBucket) {
|
|
const params: Record<string, string> = { bucket: viewingBucket };
|
|
if (path) {
|
|
params.prefix = path;
|
|
}
|
|
// Reset pagination when navigating to a new folder
|
|
setSearchParams(params);
|
|
}
|
|
};
|
|
|
|
const handlePageChange = (token?: string) => {
|
|
fetchObjects(token);
|
|
// Update URL with page token
|
|
if (viewingBucket) {
|
|
const params: Record<string, string> = { bucket: viewingBucket };
|
|
if (currentPath) {
|
|
params.prefix = currentPath;
|
|
}
|
|
if (token) {
|
|
params.page = token;
|
|
}
|
|
if (itemsPerPage !== 25) {
|
|
params.limit = itemsPerPage.toString();
|
|
}
|
|
setSearchParams(params);
|
|
}
|
|
};
|
|
|
|
const handleItemsPerPageChange = (count: number) => {
|
|
setItemsPerPage(count);
|
|
// Update URL with new limit
|
|
if (viewingBucket) {
|
|
const params: Record<string, string> = { bucket: viewingBucket };
|
|
if (currentPath) {
|
|
params.prefix = currentPath;
|
|
}
|
|
if (count !== 25) {
|
|
params.limit = count.toString();
|
|
}
|
|
setSearchParams(params);
|
|
}
|
|
};
|
|
|
|
const handleOpenSettings = (bucket: Bucket) => {
|
|
setSettingsBucket(bucket);
|
|
setSettingsDialogOpen(true);
|
|
};
|
|
|
|
const handleOpenWebsiteSettings = (bucket: Bucket) => {
|
|
setWebsiteBucket(bucket);
|
|
setWebsiteDialogOpen(true);
|
|
};
|
|
|
|
const handleSaveWebsite = async (
|
|
bucketName: string,
|
|
payload: { enabled: boolean; indexDocument?: string; errorDocument?: string }
|
|
): Promise<boolean> => {
|
|
try {
|
|
await bucketsApi.updateBucketWebsite(bucketName, payload);
|
|
queryClient.invalidateQueries({ queryKey: ['buckets'] });
|
|
toast.success('Website configuration updated');
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
};
|
|
|
|
const handleRefreshObjects = async () => {
|
|
if (isRefreshing) return;
|
|
try {
|
|
await fetchObjects(undefined, true);
|
|
toast.success('Objects refreshed successfully');
|
|
} catch (error) {
|
|
console.error('Refresh error:', error);
|
|
}
|
|
};
|
|
|
|
// Wrapper functions for mutations to match dialog APIs
|
|
const createBucket = async (name: string, region?: string) => {
|
|
try {
|
|
await createBucketMutation.mutateAsync({ name, region });
|
|
return true;
|
|
} catch (error) {
|
|
return false;
|
|
}
|
|
};
|
|
|
|
const deleteBucket = async (name: string) => {
|
|
try {
|
|
await deleteBucketMutation.mutateAsync(name);
|
|
return true;
|
|
} catch (error) {
|
|
return false;
|
|
}
|
|
};
|
|
|
|
const grantPermission = async (
|
|
bucketName: string,
|
|
accessKeyId: string,
|
|
permissions: { read: boolean; write: boolean; owner: boolean }
|
|
) => {
|
|
try {
|
|
await grantPermissionMutation.mutateAsync({ bucketName, accessKeyId, permissions });
|
|
return true;
|
|
} catch (error) {
|
|
return false;
|
|
}
|
|
};
|
|
|
|
// If viewing a bucket's objects, show the object browser view
|
|
if (viewingBucket) {
|
|
return (
|
|
<ObjectBrowserView
|
|
bucketName={viewingBucket}
|
|
objects={objects}
|
|
currentPath={currentPath}
|
|
searchQuery={objectSearchQuery}
|
|
isLoading={objectsLoading}
|
|
isTruncated={isTruncated}
|
|
nextContinuationToken={nextContinuationToken}
|
|
itemsPerPage={itemsPerPage}
|
|
onSearchChange={setObjectSearchQuery}
|
|
onNavigateToFolder={handleNavigateToFolder}
|
|
onBackToBuckets={handleBackToBuckets}
|
|
onUploadFiles={uploadFiles}
|
|
uploadTasks={uploadTasks}
|
|
onDeleteObject={deleteObject}
|
|
onDeleteMultipleObjects={deleteMultipleObjects}
|
|
onCreateDirectory={createDirectory}
|
|
onRefresh={handleRefreshObjects}
|
|
onPageChange={handlePageChange}
|
|
onItemsPerPageChange={handleItemsPerPageChange}
|
|
isRefreshing={isRefreshing}
|
|
isNavigating={isNavigating}
|
|
initialPageToken={initialPageToken}
|
|
initialItemsPerPage={initialItemsPerPage}
|
|
/>
|
|
);
|
|
}
|
|
|
|
// Default view: show buckets list
|
|
return (
|
|
<div>
|
|
<Header title="Buckets" />
|
|
<div className="p-4 sm:p-6">
|
|
<BucketListView
|
|
buckets={buckets}
|
|
searchQuery={searchQuery}
|
|
isLoading={bucketsLoading}
|
|
onSearchChange={setSearchQuery}
|
|
onViewBucket={handleViewBucket}
|
|
onOpenSettings={handleOpenSettings}
|
|
onWebsiteSettings={handleOpenWebsiteSettings}
|
|
onCreateBucket={() => setCreateDialogOpen(true)}
|
|
onDeleteBucket={(bucket) => {
|
|
setSelectedBucket(bucket);
|
|
setDeleteBucketDialogOpen(true);
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
{/* Dialogs */}
|
|
<CreateBucketDialog
|
|
open={createDialogOpen}
|
|
onOpenChange={setCreateDialogOpen}
|
|
onCreateBucket={createBucket}
|
|
/>
|
|
|
|
<DeleteBucketDialog
|
|
open={deleteBucketDialogOpen}
|
|
onOpenChange={setDeleteBucketDialogOpen}
|
|
bucket={selectedBucket}
|
|
onDeleteBucket={deleteBucket}
|
|
/>
|
|
|
|
<BucketSettingsDialog
|
|
open={settingsDialogOpen}
|
|
onOpenChange={setSettingsDialogOpen}
|
|
bucket={settingsBucket}
|
|
onGrantPermission={grantPermission}
|
|
/>
|
|
|
|
<BucketWebsiteDialog
|
|
open={websiteDialogOpen}
|
|
onOpenChange={setWebsiteDialogOpen}
|
|
bucket={websiteBucket}
|
|
onSave={handleSaveWebsite}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|