BucketFilePickerPreviewPane.tsx 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. import { useParams } from 'common'
  2. import { AlertCircle, LoaderCircle, X } from 'lucide-react'
  3. import SVG from 'react-inlinesvg'
  4. import { Button } from 'ui'
  5. import { StorageItemWithColumn } from '../Storage.types'
  6. import { useFetchFileUrlQuery } from '../StorageExplorer/useFetchFileUrlQuery'
  7. import { useBucketFilePickerStateSnapshot } from './BucketFilePickerState'
  8. import { BASE_PATH } from '@/lib/constants'
  9. import { formatBytes } from '@/lib/helpers'
  10. const PREVIEW_SIZE_LIMIT = 10 * 1024 * 1024 // 10MB
  11. const PreviewFile = ({ item }: { item: StorageItemWithColumn }) => {
  12. const { ref: projectRef } = useParams()
  13. const { bucket, columns } = useBucketFilePickerStateSnapshot()
  14. const path = columns.slice(0, item.columnIndex).concat(item.name).join('/')
  15. const { data: previewUrl, isPending: isLoading } = useFetchFileUrlQuery({
  16. path,
  17. projectRef: projectRef!,
  18. bucket,
  19. })
  20. // if the size is not available, we set it to be greater than the max size
  21. const size = +(item.metadata?.size ?? PREVIEW_SIZE_LIMIT + 1)
  22. const mimeType = item.metadata?.mimetype
  23. const isSkipped = !!mimeType && !!size && size > PREVIEW_SIZE_LIMIT
  24. if (isLoading) {
  25. return (
  26. <div className="flex h-full w-full items-center justify-center text-foreground-lighter">
  27. <LoaderCircle size={14} strokeWidth={2} className="animate-spin text-foreground-lighter" />
  28. </div>
  29. )
  30. }
  31. if (isSkipped) {
  32. return (
  33. <div className="flex h-full w-full flex-col items-center justify-center">
  34. <SVG
  35. src={`${BASE_PATH}/img/file-filled.svg`}
  36. preProcessor={(code) =>
  37. code.replace(/svg/, 'svg class="mx-auto w-32 h-32 text-color-inherit opacity-75"')
  38. }
  39. />
  40. <p className="mt-2 w-2/5 text-center text-sm">
  41. File size is too large to preview in the explorer
  42. </p>
  43. </div>
  44. )
  45. }
  46. if (!mimeType || !previewUrl) {
  47. return (
  48. <SVG
  49. src={`${BASE_PATH}/img/file-filled.svg`}
  50. preProcessor={(code) =>
  51. code.replace(/svg/, 'svg class="mx-auto w-32 h-32 text-color-inherit opacity-75"')
  52. }
  53. />
  54. )
  55. }
  56. if (mimeType.includes('image')) {
  57. return (
  58. <div
  59. className="flex h-full w-full items-center justify-center bg-contain bg-center bg-no-repeat"
  60. style={{ backgroundImage: `url('${previewUrl}')` }}
  61. />
  62. )
  63. }
  64. if (mimeType.includes('audio')) {
  65. return (
  66. <div className="flex h-full w-full items-center justify-center px-10">
  67. <audio key={previewUrl} controls style={{ width: 'inherit' }}>
  68. <source src={previewUrl} type="audio/mpeg" />
  69. <p className="text-sm text-foreground-light">
  70. Your browser does not support the audio element.
  71. </p>
  72. </audio>
  73. </div>
  74. )
  75. }
  76. if (mimeType.includes('video')) {
  77. return (
  78. <div className="flex h-full w-full items-center justify-center">
  79. <video key={previewUrl} controls style={{ maxHeight: '100%' }}>
  80. <source src={previewUrl} type="video/mp4" />
  81. <p className="text-sm text-foreground-light">
  82. Your browser does not support the video tag.
  83. </p>
  84. </video>
  85. </div>
  86. )
  87. }
  88. return (
  89. <SVG
  90. src={`${BASE_PATH}/img/file-filled.svg`}
  91. preProcessor={(code) =>
  92. code.replace(/svg/, 'svg class="mx-auto w-32 h-32 text-color-inherit opacity-75"')
  93. }
  94. />
  95. )
  96. }
  97. export const PreviewPane = ({ onSelect }: { onSelect: (url: string) => void }) => {
  98. const { ref: projectRef } = useParams()
  99. const {
  100. selectedFilePreview: file,
  101. setSelectedFilePreview,
  102. bucket,
  103. columns,
  104. } = useBucketFilePickerStateSnapshot()
  105. const path = file ? columns.slice(0, file.columnIndex).concat(file.name).join('/') : ''
  106. const { data: previewUrl, isLoading } = useFetchFileUrlQuery(
  107. { path, projectRef: projectRef!, bucket },
  108. { enabled: !!file }
  109. )
  110. if (!file) return null
  111. const size = file.metadata ? formatBytes(file.metadata.size) : null
  112. const mimeType = file.metadata ? file.metadata.mimetype : undefined
  113. const createdAt = file.created_at ? new Date(file.created_at).toLocaleString() : 'Unknown'
  114. const updatedAt = file.updated_at ? new Date(file.updated_at).toLocaleString() : 'Unknown'
  115. return (
  116. <div className="h-full border-l border-overlay bg-surface-100 overflow-y-auto w-[450px] pb-4">
  117. {/* Preview Header */}
  118. <div className="flex w-full justify-end items-center gap-2 sticky top-0 bg-surface-100 p-4 border-b">
  119. <Button
  120. size="tiny"
  121. onClick={() => onSelect(previewUrl!)}
  122. disabled={!previewUrl}
  123. loading={isLoading}
  124. >
  125. Select
  126. </Button>
  127. <div className="text-foreground-lighter transition-colors hover:text-foreground">
  128. <X
  129. className="cursor-pointer"
  130. size={14}
  131. strokeWidth={2}
  132. onClick={() => setSelectedFilePreview(undefined)}
  133. />
  134. </div>
  135. </div>
  136. {/* Preview Thumbnail*/}
  137. <div className="my-4 border border-overlay mx-4">
  138. <div className="flex h-56 w-full items-center 2xl:h-72">
  139. <PreviewFile item={file} />
  140. </div>
  141. </div>
  142. <div className="w-full space-y-6 px-4">
  143. {/* Preview Information */}
  144. <div className="space-y-1">
  145. <h5 className="wrap-break-word text-base text-foreground">{file.name}</h5>
  146. {file.isCorrupted && (
  147. <div className="flex items-center space-x-2">
  148. <AlertCircle size={14} strokeWidth={2} className="text-foreground-light" />
  149. <p className="text-sm text-foreground-light">
  150. File is corrupted, please delete and reupload this file again
  151. </p>
  152. </div>
  153. )}
  154. {mimeType && (
  155. <p className="text-sm text-foreground-light">
  156. {mimeType}
  157. {size && <span> - {size}</span>}
  158. </p>
  159. )}
  160. </div>
  161. {/* Preview Metadata */}
  162. <div className="space-y-2">
  163. <div>
  164. <label className="mb-1 text-xs text-foreground-lighter">Added on</label>
  165. <p className="text-sm text-foreground-light">{createdAt}</p>
  166. </div>
  167. <div>
  168. <label className="mb-1 text-xs text-foreground-lighter">Last modified</label>
  169. <p className="text-sm text-foreground-light">{updatedAt}</p>
  170. </div>
  171. </div>
  172. </div>
  173. </div>
  174. )
  175. }