| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272 |
- import { PermissionAction } from '@supabase/shared-types/out/constants'
- import { AlertCircle, ChevronDown, Copy, Download, LoaderCircle, Trash2, X } from 'lucide-react'
- import SVG from 'react-inlinesvg'
- import {
- Button,
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuTrigger,
- } from 'ui'
- import { URL_EXPIRY_DURATION } from '../Storage.constants'
- import { StorageItem } from '../Storage.types'
- import { getPathAlongOpenedFolders } from './StorageExplorer.utils'
- import { useCopyUrl } from './useCopyUrl'
- import { useFetchFileUrlQuery } from './useFetchFileUrlQuery'
- import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
- import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
- import { BASE_PATH } from '@/lib/constants'
- import { formatBytes } from '@/lib/helpers'
- import { useStorageExplorerStateSnapshot } from '@/state/storage-explorer'
- const PREVIEW_SIZE_LIMIT = 10 * 1024 * 1024 // 10MB
- const PreviewFile = ({ item }: { item: StorageItem }) => {
- const { projectRef, selectedBucket, openedFolders } = useStorageExplorerStateSnapshot()
- const folderPath = getPathAlongOpenedFolders({ openedFolders, selectedBucket }, false)
- const path = [folderPath, item.name].filter(Boolean).join('/')
- const { data: previewUrl, isPending: isLoading } = useFetchFileUrlQuery({
- path,
- projectRef: projectRef,
- bucket: selectedBucket,
- })
- // if the size is not available, we set it to be greater than the max size
- const size = +(item.metadata?.size ?? PREVIEW_SIZE_LIMIT + 1)
- const mimeType = item.metadata?.mimetype
- const isSkipped = !!mimeType && !!size && size > PREVIEW_SIZE_LIMIT
- if (isLoading) {
- return (
- <div className="flex h-full w-full items-center justify-center text-foreground-lighter">
- <LoaderCircle size={14} strokeWidth={2} className="animate-spin text-foreground-lighter" />
- </div>
- )
- }
- if (isSkipped) {
- return (
- <div className="flex h-full w-full flex-col items-center justify-center">
- <SVG
- src={`${BASE_PATH}/img/file-filled.svg`}
- preProcessor={(code) =>
- code.replace(/svg/, 'svg class="mx-auto w-32 h-32 text-color-inherit opacity-75"')
- }
- />
- <p className="mt-2 w-2/5 text-center text-sm">
- File size is too large to preview in the explorer
- </p>
- </div>
- )
- }
- if (!mimeType || !previewUrl) {
- return (
- <SVG
- src={`${BASE_PATH}/img/file-filled.svg`}
- preProcessor={(code) =>
- code.replace(/svg/, 'svg class="mx-auto w-32 h-32 text-color-inherit opacity-75"')
- }
- />
- )
- }
- if (mimeType.includes('image')) {
- return (
- <div
- className="flex h-full w-full items-center justify-center bg-contain bg-center bg-no-repeat"
- style={{ backgroundImage: `url('${previewUrl}')` }}
- />
- )
- }
- if (mimeType.includes('audio')) {
- return (
- <div className="flex h-full w-full items-center justify-center px-10">
- <audio key={previewUrl} controls style={{ width: 'inherit' }}>
- <source src={previewUrl} type="audio/mpeg" />
- <p className="text-sm text-foreground-light">
- Your browser does not support the audio element.
- </p>
- </audio>
- </div>
- )
- }
- if (mimeType.includes('video')) {
- return (
- <div className="flex h-full w-full items-center justify-center">
- <video key={previewUrl} controls style={{ maxHeight: '100%' }}>
- <source src={previewUrl} type="video/mp4" />
- <p className="text-sm text-foreground-light">
- Your browser does not support the video tag.
- </p>
- </video>
- </div>
- )
- }
- return (
- <SVG
- src={`${BASE_PATH}/img/file-filled.svg`}
- preProcessor={(code) =>
- code.replace(/svg/, 'svg class="mx-auto w-32 h-32 text-color-inherit opacity-75"')
- }
- />
- )
- }
- export const PreviewPane = () => {
- const {
- selectedBucket,
- selectedFilePreview: file,
- setSelectedItemsToDelete,
- setSelectedFilePreview,
- setSelectedFileCustomExpiry,
- downloadFile,
- } = useStorageExplorerStateSnapshot()
- const { onCopyUrl } = useCopyUrl()
- const { can: canUpdateFiles } = useAsyncCheckPermissions(PermissionAction.STORAGE_WRITE, '*')
- if (!file) return null
- const width = 450
- const size = file.metadata ? formatBytes(file.metadata.size) : null
- const mimeType = file.metadata ? file.metadata.mimetype : undefined
- const createdAt = file.created_at ? new Date(file.created_at).toLocaleString() : 'Unknown'
- const updatedAt = file.updated_at ? new Date(file.updated_at).toLocaleString() : 'Unknown'
- return (
- <div
- className="h-full border-l border-overlay bg-surface-100 p-4 overflow-y-auto"
- style={{ width }}
- >
- {/* Preview Header */}
- <div className="flex w-full justify-end text-foreground-lighter transition-colors hover:text-foreground">
- <X
- className="cursor-pointer"
- size={14}
- strokeWidth={2}
- onClick={() => setSelectedFilePreview(undefined)}
- />
- </div>
- {/* Preview Thumbnail*/}
- <div className="my-4 border border-overlay">
- <div className="flex h-56 w-full items-center 2xl:h-72">
- <PreviewFile item={file} />
- </div>
- </div>
- <div className="w-full space-y-6">
- {/* Preview Information */}
- <div className="space-y-1">
- <h5 className="wrap-break-word text-base text-foreground">{file.name}</h5>
- {file.isCorrupted && (
- <div className="flex items-center space-x-2">
- <AlertCircle size={14} strokeWidth={2} className="text-foreground-light" />
- <p className="text-sm text-foreground-light">
- File is corrupted, please delete and reupload this file again
- </p>
- </div>
- )}
- {mimeType && (
- <p className="text-sm text-foreground-light">
- {mimeType}
- {size && <span> - {size}</span>}
- </p>
- )}
- </div>
- {/* Preview Metadata */}
- <div className="space-y-2">
- <div>
- <label className="mb-1 text-xs text-foreground-lighter">Added on</label>
- <p className="text-sm text-foreground-light">{createdAt}</p>
- </div>
- <div>
- <label className="mb-1 text-xs text-foreground-lighter">Last modified</label>
- <p className="text-sm text-foreground-light">{updatedAt}</p>
- </div>
- </div>
- {/* Actions */}
- <div className="flex space-x-2 border-b border-overlay pb-4">
- <Button
- type="default"
- icon={<Download />}
- disabled={file.isCorrupted}
- onClick={() => downloadFile(file)}
- >
- Download
- </Button>
- {selectedBucket.public ? (
- <Button
- type="outline"
- icon={<Copy />}
- onClick={() => onCopyUrl(file.path!)}
- disabled={file.isCorrupted}
- >
- Get URL
- </Button>
- ) : (
- <DropdownMenu>
- <DropdownMenuTrigger asChild>
- <Button
- type="outline"
- icon={<Copy />}
- iconRight={<ChevronDown />}
- disabled={file.isCorrupted}
- >
- Get URL
- </Button>
- </DropdownMenuTrigger>
- <DropdownMenuContent side="bottom" align="center">
- <DropdownMenuItem
- key="expires-one-week"
- onClick={() => onCopyUrl(file.path!, URL_EXPIRY_DURATION.WEEK)}
- >
- Expire in 1 week
- </DropdownMenuItem>
- <DropdownMenuItem
- key="expires-one-month"
- onClick={() => onCopyUrl(file.path!, URL_EXPIRY_DURATION.MONTH)}
- >
- Expire in 1 month
- </DropdownMenuItem>
- <DropdownMenuItem
- key="expires-one-year"
- onClick={() => onCopyUrl(file.path!, URL_EXPIRY_DURATION.YEAR)}
- >
- Expire in 1 year
- </DropdownMenuItem>
- <DropdownMenuItem
- key="custom-expiry"
- onClick={() => setSelectedFileCustomExpiry(file)}
- >
- Custom expiry
- </DropdownMenuItem>
- </DropdownMenuContent>
- </DropdownMenu>
- )}
- </div>
- <ButtonTooltip
- type="outline"
- disabled={!canUpdateFiles}
- size="tiny"
- icon={<Trash2 strokeWidth={2} />}
- onClick={() => setSelectedItemsToDelete([file])}
- tooltip={{
- content: {
- side: 'bottom',
- text: !canUpdateFiles
- ? 'You need additional permissions to delete this file'
- : undefined,
- },
- }}
- >
- Delete file
- </ButtonTooltip>
- </div>
- </div>
- )
- }
|