"use client"; import { useEffect, useState, useTransition } from "react"; import { User } from "@prisma/client"; import { useTranslations } from "next-intl"; import { toast } from "sonner"; import useSWR, { useSWRConfig } from "swr"; import { UserFileData } from "@/lib/dto/files"; import { BucketItem, ClientStorageCredentials } from "@/lib/s3"; import { cn, fetcher } from "@/lib/utils"; import { useMediaQuery } from "@/hooks/use-media-query"; import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectSeparator, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Skeleton } from "@/components/ui/skeleton"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { ClickableTooltip } from "@/components/ui/tooltip"; import UserFileList from "@/components/file/file-list"; import { Icons } from "@/components/shared/icons"; import { EmptyPlaceholder } from "../shared/empty-placeholder"; import { PaginationWrapper } from "../shared/pagination"; import { Button } from "../ui/button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "../ui/dropdown-menu"; import { Input } from "../ui/input"; import { CircularStorageIndicator, FileSizeDisplay } from "./storage-size"; import { FileUploader } from "./upload"; export interface FileListProps { user: Pick; action: string; } export interface BucketInfo extends BucketItem { platform?: string; channel?: string; provider_name?: string; } export type DisplayType = "List" | "Grid"; export interface FileListData { total: number; totalSize: number; totalFiles: number; list: UserFileData[]; } export interface BucketUsage { bucket: string; provider: string; usage: { totalSize: number; totalFiles: number; }; limits: { maxStorage: number; maxFiles: number; maxSingleFileSize: number; }; } export interface StorageUserPlan { stMaxTotalSize: string; stMaxFileSize: string; stMaxFileCount: number; } export default function UserFileManager({ user, action }: FileListProps) { const { isMobile } = useMediaQuery(); const t = useTranslations("List"); const [currentPage, setCurrentPage] = useState(1); const [pageSize, setPageSize] = useState(20); const [displayType, setDisplayType] = useState("List"); const [showMutiCheckBox, setShowMutiCheckBox] = useState(false); const [currentBucketInfo, setCurrentBucketInfo] = useState({ bucket: "", custom_domain: "", prefix: "", platform: "", channel: "", provider_name: "", public: true, }); const [currentProvider, setCurrentProvider] = useState(null); const [selectedFiles, setSelectedFiles] = useState([]); const [isDeleting, startDeleteTransition] = useTransition(); const [currentSearchType, setCurrentSearchType] = useState("name"); const [searchParams, setSearchParams] = useState({ name: "", fileSize: "", mimeType: "", status: "1", }); const [bucketUsage, setBucketUsage] = useState(null); // const isAdmin = action.includes("/admin"); const { mutate } = useSWRConfig(); const { data: s3Configs, isLoading } = useSWR( `${action}/s3/files/configs`, fetcher, { revalidateOnFocus: false }, ); const { data: files, isLoading: isLoadingFiles, error, } = useSWR( currentBucketInfo.bucket ? `${action}/s3/files?provider=${currentBucketInfo.provider_name}&bucket=${currentBucketInfo.bucket}&page=${currentPage}&pageSize=${pageSize}&name=${searchParams.name}&fileSize=${searchParams.fileSize}&mimeType=${searchParams.mimeType}&status=${searchParams.status}` : null, fetcher, { revalidateOnFocus: false, dedupingInterval: 5000, // 防抖 }, ); const { data: plan } = useSWR( `/api/plan?team=${user.team}`, fetcher, ); useEffect(() => { if (s3Configs && s3Configs.length > 0) { setCurrentProvider(s3Configs[0]); setCurrentBucketInfo({ bucket: s3Configs[0].buckets[0].bucket, custom_domain: s3Configs[0].buckets[0].custom_domain, prefix: s3Configs[0].buckets[0].prefix, platform: s3Configs[0].platform, channel: s3Configs[0].channel, provider_name: s3Configs[0].provider_name, public: s3Configs[0].buckets[0].public, file_size: s3Configs[0].buckets[0].file_size, max_files: s3Configs[0].buckets[0].max_files, max_storage: s3Configs[0].buckets[0].max_storage, }); } }, [s3Configs]); useEffect(() => { if ( files && currentBucketInfo.bucket && currentBucketInfo.provider_name && plan ) { setBucketUsage({ bucket: currentBucketInfo.bucket, provider: currentBucketInfo.provider_name, usage: { totalSize: files.totalSize, totalFiles: files.totalFiles, }, limits: { maxStorage: currentBucketInfo.max_storage ? Number(currentBucketInfo.max_storage) : Number(plan.stMaxTotalSize), maxFiles: currentBucketInfo.max_files ? Number(currentBucketInfo.max_files) : Number(plan.stMaxFileCount), maxSingleFileSize: currentBucketInfo.file_size ? Number(currentBucketInfo.file_size) : Number(plan.stMaxFileSize), }, }); } }, [files, currentBucketInfo, plan]); const handleRefresh = () => { setSelectedFiles([]); mutate( `${action}/s3/files?provider=${currentBucketInfo.provider_name}&bucket=${currentBucketInfo.bucket}&page=${currentPage}&pageSize=${pageSize}&name=${searchParams.name}&fileSize=${searchParams.fileSize}&mimeType=${searchParams.mimeType}&status=${searchParams.status}`, undefined, ); }; const handleChangeBucket = ( provider: ClientStorageCredentials, bucket: string, ) => { const new_bucket = provider.buckets.find((b) => b.bucket === bucket); if (new_bucket) { setCurrentBucketInfo({ bucket: bucket, custom_domain: new_bucket.custom_domain, prefix: new_bucket.prefix, platform: provider.platform, channel: provider.channel, provider_name: provider.provider_name, public: true, file_size: new_bucket.file_size, max_files: new_bucket.max_files, max_storage: new_bucket.max_storage, }); } }; const handleSelectAllFiles = () => { if (selectedFiles.length === files?.list.length) { setSelectedFiles([]); } else { setSelectedFiles(files?.list || []); } }; const handleDeleteAllFiles = () => { startDeleteTransition(async () => { try { toast.promise( fetch(`${action}/s3/files`, { method: "DELETE", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ keys: selectedFiles.map((file) => file.path), ids: selectedFiles.map((file) => file.id), bucket: currentBucketInfo.bucket, provider: currentBucketInfo.provider_name, }), }), { loading: "Deleting files...", success: "Files deleted successfully!", error: "Error deleting files", finally: handleRefresh, }, ); } catch (error) { console.error("Error deleting files:", error); toast.success("Error deleting files"); } }); }; return (
setDisplayType("List")}> setDisplayType("Grid")}> {/* Search Input */}
{ setSearchParams({ ...searchParams, [currentSearchType]: e.target.value, }); setCurrentPage(1); }} />
{/* Storage */} {bucketUsage?.bucket && (
} > )} {/* Bucket Select */} {isLoading ? ( ) : ( s3Configs && s3Configs.length > 0 && ( ) )} {/* Uploader */} {!isLoading && s3Configs && s3Configs.length > 0 && currentBucketInfo && bucketUsage && ( )} {/* Muti Checkbox */}
{/* Refresh */} {isLoading && (
{t("Loading storage buckets")}...
)} {!isLoading && error && ( {t("Configuration Error")} {error.message}, Please check your bucket configuration and try again )} {!isLoading && !error && !s3Configs?.length && !currentBucketInfo.bucket && ( {t("No buckets found")} {t( "The administrator has not configured the storage bucket, no file can be uploaded", )} )} {!isLoading && !error && s3Configs && s3Configs.length > 0 && currentBucketInfo.bucket && ( )} {files && Math.ceil(files.total / pageSize) > 1 && ( )}
); }