mirror of
https://github.com/supabase/supabase.git
synced 2026-10-13 05:09:12 +03:00
* Swap analytics buckets endpoint * Fix type issues * fix * Update API types * Fix tests * nit * Final fixes * Fix * chore(studio): analytics buckets contents (#39701) * first commit * improvements * misc * cleanup * remove tappable whole * design updates * analytics improvements * polish * add clashed case * fix pairing status * connect tables basics * add multiselect * restore prod version for non-feature preview users * fix padding inconsistency * terminology fix * better empty state * fix terminology * block table connection dialog button * Nit refactors * minor nits * Add comment --------- Co-authored-by: Joshen Lim <joshenlimek@gmail.com> * Default circumstance to fresh in ImportForeignSchemaDialog * Hook up connecting tables for analytics buckets (#39906) * Hook up connecting tables for analytics buckets * Address comments * Clean up iceberg wrapper when deleting analytics bucket (#39902) * Clean up iceberg wrapper when deleting analytics bucket * Clean up s3 access key when deleting analytics bucket * Make connect table disabled for now * Most clean up and refactoring * Refactor RQ * Nit * Refactor * nit --------- Co-authored-by: Danny White <3104761+dnywh@users.noreply.github.com>
224 lines
8.7 KiB
TypeScript
224 lines
8.7 KiB
TypeScript
import { Edit, FolderOpen, MoreVertical, Search, Trash2 } from 'lucide-react'
|
|
import Link from 'next/link'
|
|
import { useRouter } from 'next/router'
|
|
import { useState } from 'react'
|
|
|
|
import { useParams } from 'common'
|
|
import { ScaffoldSection } from 'components/layouts/Scaffold'
|
|
import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
|
|
import { useProjectStorageConfigQuery } from 'data/config/project-storage-config-query'
|
|
import { Bucket, useBucketsQuery } from 'data/storage/buckets-query'
|
|
import { useStoragePolicyCounts } from 'hooks/storage/useStoragePolicyCounts'
|
|
import { IS_PLATFORM } from 'lib/constants'
|
|
import { formatBytes } from 'lib/helpers'
|
|
import {
|
|
Badge,
|
|
Button,
|
|
Card,
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuSeparator,
|
|
DropdownMenuTrigger,
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from 'ui'
|
|
import { Input } from 'ui-patterns/DataInputs/Input'
|
|
import { CreateBucketModal } from './CreateBucketModal'
|
|
import { DeleteBucketModal } from './DeleteBucketModal'
|
|
import { EditBucketModal } from './EditBucketModal'
|
|
import { EmptyBucketModal } from './EmptyBucketModal'
|
|
import { EmptyBucketState } from './EmptyBucketState'
|
|
|
|
export const FilesBuckets = () => {
|
|
const router = useRouter()
|
|
const { ref } = useParams()
|
|
|
|
const [modal, setModal] = useState<'edit' | 'empty' | 'delete' | null>(null)
|
|
const [selectedBucket, setSelectedBucket] = useState<Bucket>()
|
|
const [filterString, setFilterString] = useState('')
|
|
|
|
const { data } = useProjectStorageConfigQuery({ projectRef: ref }, { enabled: IS_PLATFORM })
|
|
const { data: buckets = [], isLoading: isLoadingBuckets } = useBucketsQuery({ projectRef: ref })
|
|
const { getPolicyCount, isLoading: isLoadingPolicies } = useStoragePolicyCounts(buckets)
|
|
|
|
const formattedGlobalUploadLimit = formatBytes(data?.fileSizeLimit ?? 0)
|
|
|
|
const isLoading = isLoadingBuckets || isLoadingPolicies
|
|
const filesBuckets = buckets
|
|
.filter((bucket) => !('type' in bucket) || bucket.type === 'STANDARD')
|
|
.filter((bucket) =>
|
|
filterString.length === 0
|
|
? true
|
|
: bucket.name.toLowerCase().includes(filterString.toLowerCase())
|
|
)
|
|
return (
|
|
<>
|
|
{!isLoading &&
|
|
buckets.filter((bucket) => !('type' in bucket) || bucket.type === 'STANDARD').length === 0 ? (
|
|
<EmptyBucketState bucketType="files" />
|
|
) : (
|
|
// Add !pt-8 to override the default first:pt-12
|
|
<ScaffoldSection isFullWidth className="gap-y-4">
|
|
<div className="flex flex-grow justify-between gap-x-2 items-center">
|
|
<Input
|
|
size="tiny"
|
|
className="flex-grow lg:flex-grow-0 w-52"
|
|
placeholder="Search for a bucket"
|
|
value={filterString}
|
|
onChange={(e) => setFilterString(e.target.value)}
|
|
icon={<Search size={12} />}
|
|
/>
|
|
<CreateBucketModal buttonType="primary" buttonClassName="w-fit" />
|
|
</div>
|
|
|
|
{isLoading ? (
|
|
<GenericSkeletonLoader />
|
|
) : (
|
|
<Card>
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead>Name</TableHead>
|
|
<TableHead>Policies</TableHead>
|
|
<TableHead>File size limit</TableHead>
|
|
<TableHead>Allowed MIME types</TableHead>
|
|
<TableHead />
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{filesBuckets.length === 0 && filterString.length > 0 && (
|
|
<TableRow className="[&>td]:hover:bg-inherit">
|
|
<TableCell colSpan={3}>
|
|
<p className="text-sm text-foreground">No results found</p>
|
|
<p className="text-sm text-foreground-light">
|
|
Your search for "{filterString}" did not return any results
|
|
</p>
|
|
</TableCell>
|
|
</TableRow>
|
|
)}
|
|
{filesBuckets.map((bucket) => (
|
|
<TableRow key={bucket.id}>
|
|
<TableCell>
|
|
<div className="flex items-center gap-2">
|
|
<p className="text-foreground">{bucket.name}</p>
|
|
{bucket.public && <Badge variant="warning">Public</Badge>}
|
|
</div>
|
|
</TableCell>
|
|
|
|
<TableCell>
|
|
<p className="text-foreground-light">{getPolicyCount(bucket.name)}</p>
|
|
</TableCell>
|
|
|
|
<TableCell>
|
|
<p
|
|
className={
|
|
bucket.file_size_limit
|
|
? 'text-foreground-light'
|
|
: 'text-foreground-muted'
|
|
}
|
|
>
|
|
{bucket.file_size_limit
|
|
? formatBytes(bucket.file_size_limit)
|
|
: `Unset (${formattedGlobalUploadLimit})`}
|
|
</p>
|
|
</TableCell>
|
|
|
|
<TableCell>
|
|
<p
|
|
className={
|
|
bucket.allowed_mime_types
|
|
? 'text-foreground-light'
|
|
: 'text-foreground-muted'
|
|
}
|
|
>
|
|
{bucket.allowed_mime_types ? bucket.allowed_mime_types.join(', ') : 'Any'}
|
|
</p>
|
|
</TableCell>
|
|
|
|
<TableCell>
|
|
<div className="flex justify-end gap-2">
|
|
<Button asChild type="default">
|
|
<Link
|
|
href={`/project/${ref}/storage/files/buckets/${encodeURIComponent(bucket.id)}`}
|
|
>
|
|
View files
|
|
</Link>
|
|
</Button>
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button type="default" className="px-1" icon={<MoreVertical />} />
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent side="bottom" align="end" className="w-40">
|
|
<DropdownMenuItem
|
|
className="flex items-center space-x-2"
|
|
onClick={(e) => {
|
|
setModal('edit')
|
|
setSelectedBucket(bucket)
|
|
}}
|
|
>
|
|
<Edit size={12} />
|
|
<p>Edit bucket</p>
|
|
</DropdownMenuItem>
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuItem
|
|
className="flex items-center space-x-2"
|
|
onClick={(e) => {
|
|
setModal('empty')
|
|
setSelectedBucket(bucket)
|
|
}}
|
|
>
|
|
<FolderOpen size={12} />
|
|
<p>Empty bucket</p>
|
|
</DropdownMenuItem>
|
|
|
|
<DropdownMenuItem
|
|
className="flex items-center space-x-2"
|
|
onClick={(e) => {
|
|
setModal('delete')
|
|
setSelectedBucket(bucket)
|
|
}}
|
|
>
|
|
<Trash2 size={12} />
|
|
<p>Delete bucket</p>
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</div>
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</Card>
|
|
)}
|
|
</ScaffoldSection>
|
|
)}
|
|
|
|
{selectedBucket && (
|
|
<>
|
|
<EditBucketModal
|
|
visible={modal === 'edit'}
|
|
bucket={selectedBucket}
|
|
onClose={() => setModal(null)}
|
|
/>
|
|
<EmptyBucketModal
|
|
visible={modal === 'empty'}
|
|
bucket={selectedBucket}
|
|
onClose={() => setModal(null)}
|
|
/>
|
|
<DeleteBucketModal
|
|
visible={modal === `delete`}
|
|
bucket={selectedBucket}
|
|
onClose={() => setModal(null)}
|
|
/>
|
|
</>
|
|
)}
|
|
</>
|
|
)
|
|
}
|