mirror of
https://github.com/supabase/supabase.git
synced 2026-05-06 22:18:00 +08:00
Merge pull request #15208 from supabase/chore/shift-storage-buckets-to-react-query
Chore/shift storage buckets to react query
This commit is contained in:
@@ -1,29 +1,29 @@
|
||||
import clsx from 'clsx'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useEffect, useState } from 'react'
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Modal,
|
||||
Input,
|
||||
Toggle,
|
||||
Form,
|
||||
Collapsible,
|
||||
Form,
|
||||
IconChevronDown,
|
||||
Input,
|
||||
Listbox,
|
||||
Modal,
|
||||
Toggle,
|
||||
} from 'ui'
|
||||
import { BucketCreatePayload } from './Storage.types'
|
||||
import { useStorageStore } from 'localStores/storageExplorer/StorageExplorerStore'
|
||||
|
||||
import { useParams } from 'common'
|
||||
import { StorageSizeUnits } from 'components/to-be-cleaned/Storage/StorageSettings/StorageSettings.constants'
|
||||
import {
|
||||
convertToBytes,
|
||||
convertFromBytes,
|
||||
convertToBytes,
|
||||
} from 'components/to-be-cleaned/Storage/StorageSettings/StorageSettings.utils'
|
||||
import { useStore } from 'hooks'
|
||||
import { useParams } from 'common'
|
||||
import { IS_PLATFORM } from 'lib/constants'
|
||||
import { useProjectStorageConfigQuery } from 'data/config/project-storage-config-query'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useBucketCreateMutation } from 'data/storage/bucket-create-mutation'
|
||||
import { useStore } from 'hooks'
|
||||
import { IS_PLATFORM } from 'lib/constants'
|
||||
|
||||
export interface CreateBucketModalProps {
|
||||
visible: boolean
|
||||
@@ -34,9 +34,8 @@ const CreateBucketModal = ({ visible, onClose }: CreateBucketModalProps) => {
|
||||
const { ui } = useStore()
|
||||
const { ref } = useParams()
|
||||
const router = useRouter()
|
||||
const storageExplorerStore = useStorageStore()
|
||||
const { createBucket } = storageExplorerStore
|
||||
|
||||
const { mutateAsync: createBucket } = useBucketCreateMutation()
|
||||
const { data } = useProjectStorageConfigQuery({ projectRef: ref }, { enabled: IS_PLATFORM })
|
||||
const { value, unit } = convertFromBytes(data?.fileSizeLimit ?? 0)
|
||||
const formattedGlobalUploadLimit = `${value} ${unit}`
|
||||
@@ -65,29 +64,36 @@ const CreateBucketModal = ({ visible, onClose }: CreateBucketModalProps) => {
|
||||
}
|
||||
|
||||
const onSubmit = async (values: any, { setSubmitting }: any) => {
|
||||
const payload: BucketCreatePayload = {
|
||||
id: values.name,
|
||||
public: values.public,
|
||||
file_size_limit: values.has_file_size_limit
|
||||
? convertToBytes(values.formatted_size_limit, selectedUnit)
|
||||
: null,
|
||||
allowed_mime_types:
|
||||
values.allowed_mime_types.length > 0
|
||||
? values.allowed_mime_types.split(',').map((x: string) => x.trim())
|
||||
: null,
|
||||
}
|
||||
|
||||
if (!ref) return console.error('Project ref is required')
|
||||
setSubmitting(true)
|
||||
const res = await createBucket(payload)
|
||||
if (res.error) {
|
||||
setSubmitting(false)
|
||||
} else {
|
||||
|
||||
try {
|
||||
const res = await createBucket({
|
||||
projectRef: ref,
|
||||
id: values.name,
|
||||
isPublic: values.public,
|
||||
file_size_limit: values.has_file_size_limit
|
||||
? convertToBytes(values.formatted_size_limit, selectedUnit)
|
||||
: null,
|
||||
allowed_mime_types:
|
||||
values.allowed_mime_types.length > 0
|
||||
? values.allowed_mime_types.split(',').map((x: string) => x.trim())
|
||||
: null,
|
||||
})
|
||||
|
||||
ui.setNotification({
|
||||
category: 'success',
|
||||
message: `Successfully created bucket "${res.name}"`,
|
||||
message: `Successfully created bucket ${res.name}`,
|
||||
})
|
||||
router.push(`/project/${ref}/storage/buckets/${res.name}`)
|
||||
|
||||
onClose()
|
||||
} catch (error: any) {
|
||||
ui.setNotification({
|
||||
category: 'error',
|
||||
message: `Failed to create bucket: ${error.message}`,
|
||||
})
|
||||
setSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,7 +110,7 @@ const CreateBucketModal = ({ visible, onClose }: CreateBucketModalProps) => {
|
||||
visible={visible}
|
||||
size="medium"
|
||||
header="Create storage bucket"
|
||||
onCancel={onClose}
|
||||
onCancel={() => onClose()}
|
||||
>
|
||||
<Form
|
||||
validateOnBlur={false}
|
||||
@@ -193,6 +199,7 @@ const CreateBucketModal = ({ visible, onClose }: CreateBucketModalProps) => {
|
||||
</div>
|
||||
<div className="col-span-4">
|
||||
<Listbox
|
||||
id="size_limit_units"
|
||||
disabled={false}
|
||||
value={selectedUnit}
|
||||
onChange={setSelectedUnit}
|
||||
@@ -234,7 +241,12 @@ const CreateBucketModal = ({ visible, onClose }: CreateBucketModalProps) => {
|
||||
<div className="w-full border-t border-scale-500 !mt-0" />
|
||||
<Modal.Content>
|
||||
<div className="flex items-center space-x-2 justify-end">
|
||||
<Button type="default" disabled={isSubmitting} onClick={() => onClose()}>
|
||||
<Button
|
||||
type="default"
|
||||
htmlType="button"
|
||||
disabled={isSubmitting}
|
||||
onClick={() => onClose()}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
import clsx from 'clsx'
|
||||
import { useParams } from 'common'
|
||||
import { StorageSizeUnits } from 'components/to-be-cleaned/Storage/StorageSettings/StorageSettings.constants'
|
||||
import {
|
||||
convertFromBytes,
|
||||
convertToBytes,
|
||||
} from 'components/to-be-cleaned/Storage/StorageSettings/StorageSettings.utils'
|
||||
import { useProjectStorageConfigQuery } from 'data/config/project-storage-config-query'
|
||||
import { useBucketUpdateMutation } from 'data/storage/bucket-update-mutation'
|
||||
import { useStore } from 'hooks'
|
||||
import { IS_PLATFORM } from 'lib/constants'
|
||||
import Link from 'next/link'
|
||||
import { useEffect, useState } from 'react'
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Modal,
|
||||
Input,
|
||||
Toggle,
|
||||
Form,
|
||||
Collapsible,
|
||||
Form,
|
||||
IconChevronDown,
|
||||
Input,
|
||||
Listbox,
|
||||
Modal,
|
||||
Toggle,
|
||||
} from 'ui'
|
||||
import { BucketUpdatePayload, StorageBucket } from './Storage.types'
|
||||
import { useStorageStore } from 'localStores/storageExplorer/StorageExplorerStore'
|
||||
import { StorageSizeUnits } from 'components/to-be-cleaned/Storage/StorageSettings/StorageSettings.constants'
|
||||
import {
|
||||
convertToBytes,
|
||||
convertFromBytes,
|
||||
} from 'components/to-be-cleaned/Storage/StorageSettings/StorageSettings.utils'
|
||||
import { useStore } from 'hooks'
|
||||
import { useParams } from 'common'
|
||||
import { useProjectStorageConfigQuery } from 'data/config/project-storage-config-query'
|
||||
import { IS_PLATFORM } from 'lib/constants'
|
||||
import { StorageBucket } from './Storage.types'
|
||||
|
||||
export interface EditBucketModalProps {
|
||||
visible: boolean
|
||||
@@ -33,9 +33,8 @@ export interface EditBucketModalProps {
|
||||
const EditBucketModal = ({ visible, bucket, onClose }: EditBucketModalProps) => {
|
||||
const { ui } = useStore()
|
||||
const { ref } = useParams()
|
||||
const storageExplorerStore = useStorageStore()
|
||||
const { editBucket } = storageExplorerStore
|
||||
|
||||
const { mutateAsync: updateBucket } = useBucketUpdateMutation()
|
||||
const { data } = useProjectStorageConfigQuery({ projectRef: ref }, { enabled: IS_PLATFORM })
|
||||
const { value, unit } = convertFromBytes(data?.fileSizeLimit ?? 0)
|
||||
const formattedGlobalUploadLimit = `${value} ${unit}`
|
||||
@@ -52,31 +51,33 @@ const EditBucketModal = ({ visible, bucket, onClose }: EditBucketModalProps) =>
|
||||
}
|
||||
|
||||
const onSubmit = async (values: any, { setSubmitting }: any) => {
|
||||
if (bucket === undefined) {
|
||||
return console.error('Bucket is required')
|
||||
}
|
||||
if (bucket === undefined) return console.error('Bucket is required')
|
||||
if (ref === undefined) return console.error('Project ref is required')
|
||||
|
||||
const payload: BucketUpdatePayload = {
|
||||
public: values.public,
|
||||
file_size_limit: values.has_file_size_limit
|
||||
? convertToBytes(values.formatted_size_limit, selectedUnit)
|
||||
: null,
|
||||
allowed_mime_types:
|
||||
values.allowed_mime_types.length > 0
|
||||
? values.allowed_mime_types.split(',').map((x: string) => x.trim())
|
||||
try {
|
||||
await updateBucket({
|
||||
projectRef: ref,
|
||||
id: bucket.id,
|
||||
isPublic: values.public,
|
||||
file_size_limit: values.has_file_size_limit
|
||||
? convertToBytes(values.formatted_size_limit, selectedUnit)
|
||||
: null,
|
||||
}
|
||||
|
||||
setSubmitting(true)
|
||||
const res = await editBucket(bucket, payload)
|
||||
if (res.error) {
|
||||
setSubmitting(false)
|
||||
} else {
|
||||
allowed_mime_types:
|
||||
values.allowed_mime_types.length > 0
|
||||
? values.allowed_mime_types.split(',').map((x: string) => x.trim())
|
||||
: null,
|
||||
})
|
||||
ui.setNotification({
|
||||
category: 'success',
|
||||
message: `Successfully updated bucket "${bucket.name}"`,
|
||||
})
|
||||
onClose()
|
||||
} catch (error: any) {
|
||||
setSubmitting(false)
|
||||
ui.setNotification({
|
||||
category: 'success',
|
||||
message: `Failed to update bucket: ${error.message}`,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,6 +217,7 @@ const EditBucketModal = ({ visible, bucket, onClose }: EditBucketModalProps) =>
|
||||
</div>
|
||||
<div className="col-span-4">
|
||||
<Listbox
|
||||
id="size_limit_units"
|
||||
disabled={false}
|
||||
value={selectedUnit}
|
||||
onChange={setSelectedUnit}
|
||||
|
||||
@@ -14,16 +14,16 @@ import { IS_PLATFORM, PROJECT_STATUS } from 'lib/constants'
|
||||
// is a unique project id/marker so we'll redirect the user to the
|
||||
// highest common route with just projectRef in the router queries.
|
||||
|
||||
const sanitizeRoute = (route: string, routerQueries: ParsedUrlQuery) => {
|
||||
export const sanitizeRoute = (route: string, routerQueries: ParsedUrlQuery) => {
|
||||
const queryArray = Object.entries(routerQueries)
|
||||
|
||||
if (queryArray.length > 1) {
|
||||
// account for query string, if exists (example: /logs/explorer?q=select...)
|
||||
const hasQueryString = queryArray.some(([key]) => key === 'q')
|
||||
|
||||
// [Joshen] Ideally we shouldn't use hard coded numbers, but temp workaround
|
||||
// for storage bucket route since its longer
|
||||
const isStorageBucketRoute = 'bucketId' in routerQueries
|
||||
return route
|
||||
.split('/')
|
||||
.slice(0, hasQueryString ? 5 : 4)
|
||||
.slice(0, isStorageBucketRoute ? 5 : 4)
|
||||
.join('/')
|
||||
} else {
|
||||
return route
|
||||
|
||||
@@ -16,7 +16,11 @@ const NavigationIconButton: FC<Props> = ({ route, isActive = false }) => {
|
||||
<Tooltip.Trigger>
|
||||
<ConditionalWrap
|
||||
condition={route.link !== undefined}
|
||||
wrap={(children) => <Link href={route.link!}>{children}</Link>}
|
||||
wrap={(children) => (
|
||||
<Link href={route.link!}>
|
||||
<a>{children}</a>
|
||||
</Link>
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className={[
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import clsx from 'clsx'
|
||||
import Link from 'next/link'
|
||||
import { noop } from 'lodash'
|
||||
import { Badge, Button, Dropdown, IconChevronDown, IconEdit2, IconLoader, IconTrash } from 'ui'
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
import clsx from 'clsx'
|
||||
import { noop } from 'lodash'
|
||||
import Link from 'next/link'
|
||||
import { Badge, Button, Dropdown, IconChevronDown, IconEdit2, IconLoader, IconTrash } from 'ui'
|
||||
|
||||
import { Bucket } from 'data/storage/buckets-query'
|
||||
import { checkPermissions } from 'hooks'
|
||||
import { STORAGE_ROW_STATUS } from 'components/to-be-cleaned/Storage/Storage.constants'
|
||||
|
||||
interface BucketRowProps {
|
||||
bucket: any
|
||||
export interface BucketRowProps {
|
||||
bucket: Bucket
|
||||
projectRef?: string
|
||||
isSelected: boolean
|
||||
onSelectDeleteBucket: (bucket: any) => void
|
||||
@@ -16,7 +16,7 @@ interface BucketRowProps {
|
||||
}
|
||||
|
||||
const BucketRow = ({
|
||||
bucket = {},
|
||||
bucket,
|
||||
projectRef = '',
|
||||
isSelected = false,
|
||||
onSelectDeleteBucket = noop,
|
||||
@@ -46,9 +46,10 @@ const BucketRow = ({
|
||||
</a>
|
||||
</Link>
|
||||
<div className="pr-3">
|
||||
{bucket.status === STORAGE_ROW_STATUS.LOADING ? (
|
||||
{/* [JOSHEN TODO] need to change this */}
|
||||
{false ? (
|
||||
<IconLoader className="animate-spin" size={16} strokeWidth={2} />
|
||||
) : canUpdateBuckets && bucket.status === STORAGE_ROW_STATUS.READY && isSelected ? (
|
||||
) : canUpdateBuckets && isSelected ? (
|
||||
<Dropdown
|
||||
side="bottom"
|
||||
align="start"
|
||||
|
||||
@@ -1,33 +1,27 @@
|
||||
import { FC, ReactNode, useEffect } from 'react'
|
||||
import { find, filter, get as _get } from 'lodash'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { ReactNode, useEffect } from 'react'
|
||||
|
||||
import { useStore, withAuth } from 'hooks'
|
||||
import { useParams } from 'common/hooks'
|
||||
import DeleteBucketModal from 'components/to-be-cleaned/Storage/DeleteBucketModal'
|
||||
import { AutoApiService, useProjectApiQuery } from 'data/config/project-api-query'
|
||||
import { useStore, withAuth } from 'hooks'
|
||||
import { PROJECT_STATUS } from 'lib/constants'
|
||||
import { useStorageStore } from 'localStores/storageExplorer/StorageExplorerStore'
|
||||
import ProjectLayout from '../'
|
||||
import StorageMenu from './StorageMenu'
|
||||
import { useStorageStore } from 'localStores/storageExplorer/StorageExplorerStore'
|
||||
import { formatPoliciesForStorage } from 'components/to-be-cleaned/Storage/Storage.utils'
|
||||
import DeleteBucketModal from 'components/to-be-cleaned/Storage/DeleteBucketModal'
|
||||
import { PROJECT_STATUS } from 'lib/constants'
|
||||
|
||||
interface Props {
|
||||
export interface StorageLayoutProps {
|
||||
title: string
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
const StorageLayout: FC<Props> = ({ title, children }) => {
|
||||
const { ui, meta } = useStore()
|
||||
const StorageLayout = ({ title, children }: StorageLayoutProps) => {
|
||||
const { ui } = useStore()
|
||||
const { ref: projectRef } = useParams()
|
||||
|
||||
const storageExplorerStore = useStorageStore()
|
||||
const {
|
||||
selectedBucketToEdit,
|
||||
closeDeleteBucketModal,
|
||||
showDeleteBucketModal,
|
||||
deleteBucket,
|
||||
buckets,
|
||||
} = storageExplorerStore || {}
|
||||
const { selectedBucketToEdit, closeDeleteBucketModal, showDeleteBucketModal } =
|
||||
storageExplorerStore || {}
|
||||
|
||||
const { data: settings, isLoading } = useProjectApiQuery({ projectRef })
|
||||
const apiService = settings?.autoApiService
|
||||
@@ -48,7 +42,6 @@ const StorageLayout: FC<Props> = ({ title, children }) => {
|
||||
apiService.serviceApiKey,
|
||||
apiService.protocol
|
||||
)
|
||||
await storageExplorerStore.fetchBuckets()
|
||||
} else {
|
||||
ui.setNotification({
|
||||
category: 'error',
|
||||
@@ -59,38 +52,13 @@ const StorageLayout: FC<Props> = ({ title, children }) => {
|
||||
storageExplorerStore.setLoaded(true)
|
||||
}
|
||||
|
||||
const onSelectDeleteBucket = async (bucket: any) => {
|
||||
const res = await deleteBucket(bucket)
|
||||
// Ideally this should be within deleteBucket as its a necessary side effect
|
||||
// but we'll do so once we refactor to remove the StorageExplorerStore
|
||||
if (res) {
|
||||
const policies = meta.policies.list()
|
||||
const storageObjectsPolicies = filter(policies, { table: 'objects' })
|
||||
const formattedStorageObjectPolicies = formatPoliciesForStorage(
|
||||
buckets,
|
||||
storageObjectsPolicies
|
||||
)
|
||||
const bucketPolicies = _get(
|
||||
find(formattedStorageObjectPolicies, { name: bucket.name }),
|
||||
['policies'],
|
||||
[]
|
||||
)
|
||||
await Promise.all(
|
||||
bucketPolicies.map((policy: any) => {
|
||||
meta.policies.del(policy.id)
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<ProjectLayout title={title || 'Storage'} product="Storage" productMenu={<StorageMenu />}>
|
||||
{children}
|
||||
<DeleteBucketModal
|
||||
visible={showDeleteBucketModal}
|
||||
bucket={selectedBucketToEdit}
|
||||
onSelectCancel={closeDeleteBucketModal}
|
||||
onSelectDelete={onSelectDeleteBucket}
|
||||
onClose={closeDeleteBucketModal}
|
||||
/>
|
||||
</ProjectLayout>
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FC, useState } from 'react'
|
||||
import { useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
@@ -14,10 +14,9 @@ import { StorageBucket } from 'components/interfaces/Storage/Storage.types'
|
||||
import EditBucketModal from 'components/interfaces/Storage/EditBucketModal'
|
||||
import CreateBucketModal from 'components/interfaces/Storage/CreateBucketModal'
|
||||
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
|
||||
import { useBucketsQuery } from 'data/storage/buckets-query'
|
||||
|
||||
interface Props {}
|
||||
|
||||
const StorageMenu: FC<Props> = () => {
|
||||
const StorageMenu = () => {
|
||||
const router = useRouter()
|
||||
const { ref, bucketId } = useParams()
|
||||
const [showCreateBucketModal, setShowCreateBucketModal] = useState(false)
|
||||
@@ -32,7 +31,10 @@ const StorageMenu: FC<Props> = () => {
|
||||
| 'logs'
|
||||
|
||||
const storageExplorerStore = useStorageStore()
|
||||
const { loaded, buckets, openDeleteBucketModal } = storageExplorerStore || {}
|
||||
const { data, isLoading } = useBucketsQuery({ projectRef: ref })
|
||||
const { openDeleteBucketModal } = storageExplorerStore || {}
|
||||
|
||||
const buckets = data ?? []
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -78,11 +80,11 @@ const StorageMenu: FC<Props> = () => {
|
||||
<div className="">
|
||||
<div>
|
||||
<Menu.Group title="All buckets" />
|
||||
{!loaded ? (
|
||||
<div className="space-y-2">
|
||||
{isLoading ? (
|
||||
<div className="space-y-2 mx-2">
|
||||
<ShimmeringLoader className="!py-2.5" />
|
||||
<ShimmeringLoader className="!py-2.5" />
|
||||
<ShimmeringLoader className="!py-2.5" />
|
||||
<ShimmeringLoader className="!py-2.5 w-3/4" />
|
||||
<ShimmeringLoader className="!py-2.5 w-1/2" />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
|
||||
@@ -1,30 +1,74 @@
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
import TextConfirmModal from 'components/ui/Modals/TextConfirmModal'
|
||||
import { get as _get, find } from 'lodash'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
interface Props {
|
||||
import { useParams } from 'common'
|
||||
import TextConfirmModal from 'components/ui/Modals/TextConfirmModal'
|
||||
import { useBucketDeleteMutation } from 'data/storage/bucket-delete-mutation'
|
||||
import { Bucket, useBucketsQuery } from 'data/storage/buckets-query'
|
||||
import { useStore } from 'hooks'
|
||||
import { formatPoliciesForStorage } from './Storage.utils'
|
||||
|
||||
export interface DeleteBucketModalProps {
|
||||
visible: boolean
|
||||
bucket: any
|
||||
onSelectCancel: () => void
|
||||
onSelectDelete: (bucket: any) => void
|
||||
bucket: Bucket
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
const DeleteBucketModal: FC<Props> = ({
|
||||
visible = false,
|
||||
bucket = {},
|
||||
onSelectCancel,
|
||||
onSelectDelete,
|
||||
}) => {
|
||||
const DeleteBucketModal = ({ visible = false, bucket, onClose }: DeleteBucketModalProps) => {
|
||||
const router = useRouter()
|
||||
const { ui, meta } = useStore()
|
||||
const { ref: projectRef } = useParams()
|
||||
|
||||
const [deleting, setDeleting] = useState(false)
|
||||
const [validationInput, setValidationInput] = useState('')
|
||||
const { data } = useBucketsQuery({ projectRef })
|
||||
const { mutateAsync: deleteBucket } = useBucketDeleteMutation()
|
||||
|
||||
const buckets = data ?? []
|
||||
|
||||
useEffect(() => {
|
||||
setValidationInput('')
|
||||
setDeleting(false)
|
||||
}, [visible])
|
||||
|
||||
const onConfirmDelete = () => {
|
||||
const onDeleteBucket = async () => {
|
||||
if (!projectRef) return console.error('Project ref is required')
|
||||
|
||||
setDeleting(true)
|
||||
onSelectDelete(bucket)
|
||||
try {
|
||||
await deleteBucket({ projectRef, id: bucket.id })
|
||||
|
||||
// Clean up policies from the corresponding bucket that was deleted
|
||||
await meta.policies.loadBySchema('storage')
|
||||
const policies = meta.policies.list()
|
||||
const storageObjectsPolicies = policies.filter((policy) => policy.table === 'objects')
|
||||
const formattedStorageObjectPolicies = formatPoliciesForStorage(
|
||||
buckets,
|
||||
storageObjectsPolicies
|
||||
)
|
||||
const bucketPolicies = _get(
|
||||
find(formattedStorageObjectPolicies, { name: bucket.name }),
|
||||
['policies'],
|
||||
[]
|
||||
)
|
||||
await Promise.all(
|
||||
bucketPolicies.map((policy: any) => {
|
||||
meta.policies.del(policy.id)
|
||||
})
|
||||
)
|
||||
|
||||
ui.setNotification({
|
||||
category: 'success',
|
||||
message: `Successfully deleted bucket ${bucket.name}`,
|
||||
})
|
||||
router.push(`/project/${projectRef}/storage/buckets`)
|
||||
onClose()
|
||||
} catch (error: any) {
|
||||
setDeleting(false)
|
||||
ui.setNotification({
|
||||
category: 'error',
|
||||
message: `Failed to delete bucket: ${error.message}`,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -32,8 +76,8 @@ const DeleteBucketModal: FC<Props> = ({
|
||||
visible={visible}
|
||||
title={`Confirm deletion of ${bucket.name}`}
|
||||
confirmPlaceholder="Type in name of bucket"
|
||||
onConfirm={onConfirmDelete}
|
||||
onCancel={onSelectCancel}
|
||||
onConfirm={onDeleteBucket}
|
||||
onCancel={onClose}
|
||||
confirmString={bucket.name}
|
||||
loading={deleting}
|
||||
text={
|
||||
|
||||
@@ -184,7 +184,7 @@ const FileExplorerColumn = ({
|
||||
<div
|
||||
className={`
|
||||
${fullWidth ? 'w-full' : 'w-64 border-r border-gray-500'}
|
||||
my-1 flex flex-shrink-0 flex-col space-y-1 overflow-auto
|
||||
px-2 py-1 my-1 flex flex-shrink-0 flex-col space-y-2 overflow-auto
|
||||
`}
|
||||
>
|
||||
<ShimmeringLoader />
|
||||
|
||||
@@ -12,11 +12,18 @@ import ConfirmModal from 'components/ui/Dialogs/ConfirmDialog'
|
||||
import { useStorageStore } from 'localStores/storageExplorer/StorageExplorerStore'
|
||||
|
||||
import { PolicyEditorModal } from 'components/interfaces/Auth/Policies'
|
||||
import { useBucketsQuery } from 'data/storage/buckets-query'
|
||||
import { useParams } from 'common'
|
||||
|
||||
const StoragePolicies = () => {
|
||||
const { ui, meta } = useStore()
|
||||
const { ref: projectRef } = useParams()
|
||||
|
||||
const storageStore = useStorageStore()
|
||||
const { loaded, buckets } = storageStore
|
||||
const { loaded } = storageStore
|
||||
|
||||
const { data } = useBucketsQuery({ projectRef })
|
||||
const buckets = data ?? []
|
||||
|
||||
const roles = meta.roles.list((role) => !meta.roles.systemRoles.includes(role.name))
|
||||
|
||||
|
||||
56
studio/data/storage/bucket-create-mutation.ts
Normal file
56
studio/data/storage/bucket-create-mutation.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
|
||||
import { post } from 'lib/common/fetch'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { storageKeys } from './keys'
|
||||
|
||||
export type BucketCreateVariables = {
|
||||
projectRef: string
|
||||
id: string
|
||||
isPublic: boolean
|
||||
file_size_limit: number | null
|
||||
allowed_mime_types: string[] | null
|
||||
}
|
||||
|
||||
export async function createBucket({
|
||||
projectRef,
|
||||
id,
|
||||
isPublic,
|
||||
file_size_limit,
|
||||
allowed_mime_types,
|
||||
}: BucketCreateVariables) {
|
||||
if (!projectRef) throw new Error('projectRef is required')
|
||||
if (!id) throw new Error('Bucket name is requried')
|
||||
|
||||
const response = await post(`${API_URL}/storage/${projectRef}/buckets`, {
|
||||
id,
|
||||
public: isPublic,
|
||||
file_size_limit,
|
||||
allowed_mime_types,
|
||||
})
|
||||
if (response.error) throw response.error
|
||||
return response
|
||||
}
|
||||
|
||||
type BucketCreateData = Awaited<ReturnType<typeof createBucket>>
|
||||
|
||||
export const useBucketCreateMutation = ({
|
||||
onSuccess,
|
||||
...options
|
||||
}: Omit<
|
||||
UseMutationOptions<BucketCreateData, unknown, BucketCreateVariables>,
|
||||
'mutationFn'
|
||||
> = {}) => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation<BucketCreateData, unknown, BucketCreateVariables>(
|
||||
(vars) => createBucket(vars),
|
||||
{
|
||||
async onSuccess(data, variables, context) {
|
||||
const { projectRef } = variables
|
||||
await queryClient.invalidateQueries(storageKeys.buckets(projectRef))
|
||||
await onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
}
|
||||
)
|
||||
}
|
||||
45
studio/data/storage/bucket-delete-mutation.ts
Normal file
45
studio/data/storage/bucket-delete-mutation.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
|
||||
import { delete_, post } from 'lib/common/fetch'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { storageKeys } from './keys'
|
||||
|
||||
export type BucketDeleteVariables = {
|
||||
projectRef: string
|
||||
id: string
|
||||
}
|
||||
|
||||
export async function deleteBucket({ projectRef, id }: BucketDeleteVariables) {
|
||||
if (!projectRef) throw new Error('projectRef is required')
|
||||
if (!id) throw new Error('Bucket name is requried')
|
||||
|
||||
const emptyBucketRes = await post(`${API_URL}/storage/${projectRef}/buckets/${id}/empty`, {})
|
||||
if (emptyBucketRes.error) throw emptyBucketRes.error
|
||||
|
||||
const response = await delete_(`${API_URL}/storage/${projectRef}/buckets/${id}`)
|
||||
if (response.error) throw response.error
|
||||
return response
|
||||
}
|
||||
|
||||
type BucketDeleteData = Awaited<ReturnType<typeof deleteBucket>>
|
||||
|
||||
export const useBucketDeleteMutation = ({
|
||||
onSuccess,
|
||||
...options
|
||||
}: Omit<
|
||||
UseMutationOptions<BucketDeleteData, unknown, BucketDeleteVariables>,
|
||||
'mutationFn'
|
||||
> = {}) => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation<BucketDeleteData, unknown, BucketDeleteVariables>(
|
||||
(vars) => deleteBucket(vars),
|
||||
{
|
||||
async onSuccess(data, variables, context) {
|
||||
const { projectRef } = variables
|
||||
await queryClient.invalidateQueries(storageKeys.buckets(projectRef))
|
||||
await onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
}
|
||||
)
|
||||
}
|
||||
55
studio/data/storage/bucket-update-mutation.ts
Normal file
55
studio/data/storage/bucket-update-mutation.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
|
||||
import { patch } from 'lib/common/fetch'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { storageKeys } from './keys'
|
||||
|
||||
export type BucketUpdateVariables = {
|
||||
projectRef: string
|
||||
id: string
|
||||
isPublic: boolean
|
||||
file_size_limit: number | null
|
||||
allowed_mime_types: string[] | null
|
||||
}
|
||||
|
||||
export async function updateBucket({
|
||||
projectRef,
|
||||
id,
|
||||
isPublic,
|
||||
file_size_limit,
|
||||
allowed_mime_types,
|
||||
}: BucketUpdateVariables) {
|
||||
if (!projectRef) throw new Error('projectRef is required')
|
||||
if (!id) throw new Error('Bucket name is requried')
|
||||
|
||||
const response = await patch(`${API_URL}/storage/${projectRef}/buckets/${id}`, {
|
||||
public: isPublic,
|
||||
file_size_limit,
|
||||
allowed_mime_types,
|
||||
})
|
||||
if (response.error) throw response.error
|
||||
return response
|
||||
}
|
||||
|
||||
type BucketUpdateData = Awaited<ReturnType<typeof updateBucket>>
|
||||
|
||||
export const useBucketUpdateMutation = ({
|
||||
onSuccess,
|
||||
...options
|
||||
}: Omit<
|
||||
UseMutationOptions<BucketUpdateData, unknown, BucketUpdateVariables>,
|
||||
'mutationFn'
|
||||
> = {}) => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation<BucketUpdateData, unknown, BucketUpdateVariables>(
|
||||
(vars) => updateBucket(vars),
|
||||
{
|
||||
async onSuccess(data, variables, context) {
|
||||
const { projectRef } = variables
|
||||
await queryClient.invalidateQueries(storageKeys.buckets(projectRef))
|
||||
await onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
}
|
||||
)
|
||||
}
|
||||
51
studio/data/storage/buckets-query.ts
Normal file
51
studio/data/storage/buckets-query.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { useQuery, useQueryClient, UseQueryOptions } from '@tanstack/react-query'
|
||||
import { get } from 'lib/common/fetch'
|
||||
import { API_URL, IS_PLATFORM } from 'lib/constants'
|
||||
import { useCallback } from 'react'
|
||||
import { storageKeys } from './keys'
|
||||
|
||||
export type BucketsVariables = { projectRef?: string }
|
||||
|
||||
export type Bucket = {
|
||||
id: string
|
||||
name: string
|
||||
owner: string
|
||||
public: boolean
|
||||
created_at: string
|
||||
updated_at: string
|
||||
file_size_limit: null | number
|
||||
allowed_mime_types: null | string[]
|
||||
}
|
||||
|
||||
export async function getBuckets({ projectRef }: BucketsVariables, signal?: AbortSignal) {
|
||||
if (!projectRef) throw new Error('projectRef is required')
|
||||
|
||||
const response = await get(`${API_URL}/storage/${projectRef}/buckets`, { signal })
|
||||
if (response.error) throw response.error
|
||||
return response as Bucket[]
|
||||
}
|
||||
|
||||
export type BucketsData = Awaited<ReturnType<typeof getBuckets>>
|
||||
export type BucketsError = unknown
|
||||
|
||||
export const useBucketsQuery = <TData = BucketsData>(
|
||||
{ projectRef }: BucketsVariables,
|
||||
{ enabled = true, ...options }: UseQueryOptions<BucketsData, BucketsError, TData> = {}
|
||||
) =>
|
||||
useQuery<BucketsData, BucketsError, TData>(
|
||||
storageKeys.buckets(projectRef),
|
||||
({ signal }) => getBuckets({ projectRef }, signal),
|
||||
{ enabled: IS_PLATFORM && enabled && typeof projectRef !== 'undefined', ...options }
|
||||
)
|
||||
|
||||
export const useBucketsPrefetch = ({ projectRef }: BucketsVariables) => {
|
||||
const client = useQueryClient()
|
||||
|
||||
return useCallback(() => {
|
||||
if (projectRef) {
|
||||
client.prefetchQuery(storageKeys.buckets(projectRef), ({ signal }) =>
|
||||
getBuckets({ projectRef }, signal)
|
||||
)
|
||||
}
|
||||
}, [projectRef])
|
||||
}
|
||||
3
studio/data/storage/keys.ts
Normal file
3
studio/data/storage/keys.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export const storageKeys = {
|
||||
buckets: (projectRef: string | undefined) => ['projects', projectRef, 'buckets'] as const,
|
||||
}
|
||||
@@ -440,18 +440,6 @@ class StorageExplorerStore {
|
||||
|
||||
/* Methods that involve the storage client library */
|
||||
/* Bucket CRUD */
|
||||
|
||||
createBucket = async (payload) => {
|
||||
const res = await post(`${this.endpoint}/buckets`, payload)
|
||||
if (res.error) {
|
||||
this.ui.setNotification({ category: 'error', message: res.error.message })
|
||||
return res
|
||||
} else {
|
||||
await this.fetchBuckets()
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
||||
openBucket = async (bucket) => {
|
||||
const { id, name } = bucket
|
||||
const columnIndex = -1
|
||||
@@ -461,59 +449,6 @@ class StorageExplorerStore {
|
||||
}
|
||||
}
|
||||
|
||||
fetchBuckets = async () => {
|
||||
const res = await get(`${this.endpoint}/buckets`)
|
||||
if (res.error) return this.ui.setNotification({ category: 'error', message: res.error.message })
|
||||
|
||||
const formattedBuckets = res.map((bucket) => {
|
||||
return { ...bucket, type: STORAGE_ROW_TYPES.BUCKET, status: STORAGE_ROW_STATUS.READY }
|
||||
})
|
||||
this.buckets = formattedBuckets
|
||||
return formattedBuckets
|
||||
}
|
||||
|
||||
deleteBucket = async (bucket) => {
|
||||
// Deleting a bucket requires the bucket to be empty first
|
||||
// hence delete bucket and empty bucket are coupled tightly here
|
||||
const { id, name: bucketName } = bucket
|
||||
|
||||
const emptyBucketRes = await post(`${this.endpoint}/buckets/${id}/empty`, {})
|
||||
if (emptyBucketRes.error) {
|
||||
this.ui.setNotification({ category: 'error', message: emptyBucketRes.error.message })
|
||||
return false
|
||||
}
|
||||
|
||||
const deleteBucketRes = await delete_(`${this.endpoint}/buckets/${id}`)
|
||||
if (deleteBucketRes.error) {
|
||||
this.ui.setNotification({ category: 'error', message: deleteBucketRes.error.message })
|
||||
return false
|
||||
}
|
||||
|
||||
await this.fetchBuckets()
|
||||
if (bucketName === this.selectedBucket.name) {
|
||||
this.setSelectedBucket({})
|
||||
this.clearColumns()
|
||||
this.clearOpenedFolders()
|
||||
}
|
||||
this.clearSelectedItemsToDelete()
|
||||
this.closeDeleteBucketModal()
|
||||
return true
|
||||
}
|
||||
|
||||
editBucket = async (bucket, payload) => {
|
||||
const res = await patch(`${this.endpoint}/buckets/${bucket.id}`, payload)
|
||||
if (res.error) {
|
||||
this.ui.setNotification({ category: 'error', message: res.error.message })
|
||||
return res
|
||||
}
|
||||
|
||||
this.openBucket({ ...bucket, ...payload })
|
||||
this.fetchBuckets()
|
||||
this.clearFilePreviewCache()
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
/* Files CRUD */
|
||||
|
||||
getFile = async (fileEntry) => {
|
||||
|
||||
@@ -1,30 +1,24 @@
|
||||
import React, { useEffect } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { find } from 'lodash'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { useParams } from 'common'
|
||||
import { StorageLayout } from 'components/layouts'
|
||||
import { StorageExplorer } from 'components/to-be-cleaned/Storage'
|
||||
import { useBucketsQuery } from 'data/storage/buckets-query'
|
||||
import { useFlag, useStore } from 'hooks'
|
||||
import { post } from 'lib/common/fetch'
|
||||
import { PROJECT_STATUS } from 'lib/constants'
|
||||
import { StorageLayout } from 'components/layouts'
|
||||
import ProductEmptyState from 'components/to-be-cleaned/ProductEmptyState'
|
||||
import { StorageExplorer } from 'components/to-be-cleaned/Storage'
|
||||
import { useStorageStore } from 'localStores/storageExplorer/StorageExplorerStore'
|
||||
import { API_URL, PROJECT_STATUS } from 'lib/constants'
|
||||
import { NextPageWithLayout } from 'types'
|
||||
|
||||
/**
|
||||
* PageLayout is used to setup layout - as usual it will requires inject global store
|
||||
*/
|
||||
const PageLayout: NextPageWithLayout = () => {
|
||||
const router = useRouter()
|
||||
const { ref, bucketId } = router.query
|
||||
const { ref, bucketId } = useParams()
|
||||
|
||||
const { ui } = useStore()
|
||||
const project = ui.selectedProject
|
||||
|
||||
const storageStore = useStorageStore()
|
||||
const { buckets, loaded } = storageStore
|
||||
const { data, isSuccess } = useBucketsQuery({ projectRef: ref })
|
||||
const buckets = data ?? []
|
||||
|
||||
const kpsEnabled = useFlag('initWithKps')
|
||||
|
||||
@@ -40,7 +34,7 @@ const PageLayout: NextPageWithLayout = () => {
|
||||
|
||||
return (
|
||||
<div className="storage-container flex flex-grow p-4">
|
||||
{loaded ? (
|
||||
{isSuccess ? (
|
||||
!bucket ? (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<p className="text-sm text-scale-1100">Bucket {bucketId} cannot be found</p>
|
||||
|
||||
@@ -1,18 +1,13 @@
|
||||
import React, { useEffect } from 'react'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { useFlag, useStore } from 'hooks'
|
||||
import { useParams } from 'common/hooks'
|
||||
import { post } from 'lib/common/fetch'
|
||||
import { PROJECT_STATUS } from 'lib/constants'
|
||||
import { StorageLayout } from 'components/layouts'
|
||||
import ProductEmptyState from 'components/to-be-cleaned/ProductEmptyState'
|
||||
import { useFlag, useStore } from 'hooks'
|
||||
import { post } from 'lib/common/fetch'
|
||||
import { API_URL, PROJECT_STATUS } from 'lib/constants'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { useEffect } from 'react'
|
||||
import { NextPageWithLayout } from 'types'
|
||||
|
||||
/**
|
||||
* PageLayout is used to setup layout - as usual it will requires inject global store
|
||||
*/
|
||||
const PageLayout: NextPageWithLayout = ({}) => {
|
||||
const { ref } = useParams()
|
||||
const { ui } = useStore()
|
||||
|
||||
22
studio/tests/components/ProjectDropdown.test.ts
Normal file
22
studio/tests/components/ProjectDropdown.test.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { sanitizeRoute } from 'components/layouts/ProjectLayout/LayoutHeader/ProjectDropdown'
|
||||
|
||||
test('Should sanitize project routes correctly when switching projects by removing project specific parameters', () => {
|
||||
expect(sanitizeRoute('/project/[ref]', { ref: 'abc' })).toBe('/project/[ref]')
|
||||
expect(sanitizeRoute('/project/[ref]/editor', { ref: 'abc' })).toBe('/project/[ref]/editor')
|
||||
expect(sanitizeRoute('/project/[ref]/storage/buckets', { ref: 'abc' })).toBe(
|
||||
'/project/[ref]/storage/buckets'
|
||||
)
|
||||
expect(sanitizeRoute('/project/[ref]/settings/billing/subscription', { ref: 'abc' })).toBe(
|
||||
'/project/[ref]/settings/billing/subscription'
|
||||
)
|
||||
|
||||
expect(sanitizeRoute('/project/[ref]/editor/[tableId]', { ref: 'abc', tableId: '10' })).toBe(
|
||||
'/project/[ref]/editor'
|
||||
)
|
||||
expect(
|
||||
sanitizeRoute('/project/[ref]/storage/buckets/[bucketId]', { ref: 'abc', bucketId: 'bucket-1' })
|
||||
).toBe('/project/[ref]/storage/buckets')
|
||||
expect(sanitizeRoute('/project/[ref]/logs/explorer?q=select', { ref: 'abc' })).toBe(
|
||||
'/project/[ref]/logs/explorer?q=select'
|
||||
)
|
||||
})
|
||||
Reference in New Issue
Block a user