| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342 |
- import { useParams } from 'common'
- import { isEmpty } from 'lodash'
- import { parseAsString, useQueryState } from 'nuqs'
- import { useMemo, useState } from 'react'
- import { toast } from 'sonner'
- import { GenericSkeletonLoader } from 'ui-patterns'
- import { ConfirmationModal } from 'ui-patterns/Dialogs/ConfirmationModal'
- import { PageContainer } from 'ui-patterns/PageContainer'
- import {
- PageSection,
- PageSectionContent,
- PageSectionDescription,
- PageSectionMeta,
- PageSectionSummary,
- PageSectionTitle,
- } from 'ui-patterns/PageSection'
- import { formatPoliciesForStorage, UNGROUPED_POLICY_SYMBOL } from '../Storage.utils'
- import { StoragePoliciesBucketRow } from './StoragePoliciesBucketRow'
- import { BucketsPolicies, type SelectBucketPolicyForAction } from './StoragePoliciesBucketsSection'
- import { StoragePoliciesEditPolicyModal } from './StoragePoliciesEditPolicyModal'
- import type {
- PostgresPolicyCreatePayload,
- PostgresPolicyUpdatePayload,
- } from '@/components/interfaces/Auth/Policies/Policies.types'
- import { PolicyEditorModal } from '@/components/interfaces/Auth/Policies/PolicyEditorModal'
- import type { Policy } from '@/components/interfaces/Auth/Policies/PolicyTableRow/PolicyTableRow.utils'
- import { useDatabasePoliciesQuery } from '@/data/database-policies/database-policies-query'
- import { useDatabasePolicyCreateMutation } from '@/data/database-policies/database-policy-create-mutation'
- import { useDatabasePolicyDeleteMutation } from '@/data/database-policies/database-policy-delete-mutation'
- import { useDatabasePolicyUpdateMutation } from '@/data/database-policies/database-policy-update-mutation'
- import { usePaginatedBucketsQuery } from '@/data/storage/buckets-query'
- import { useDebouncedValue } from '@/hooks/misc/useDebouncedValue'
- import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
- export const StoragePolicies = () => {
- const { ref: projectRef } = useParams()
- const { data: project } = useSelectedProjectQuery()
- const [selectedPolicyToEdit, setSelectedPolicyToEdit] = useState<Policy>()
- const [selectedPolicyToDelete, setSelectedPolicyToDelete] = useState<Policy>()
- const [isEditingPolicyForBucket, setIsEditingPolicyForBucket] = useState<{
- bucket: string
- table: string
- }>()
- const [searchString, setSearchString] = useQueryState(
- 'search',
- parseAsString.withDefault('').withOptions({ history: 'replace', clearOnDefault: true })
- )
- const debouncedSearchString = useDebouncedValue(searchString, 250)
- const {
- data: bucketsData,
- isPending: isLoadingBuckets,
- hasNextPage,
- isFetchingNextPage,
- fetchNextPage,
- } = usePaginatedBucketsQuery({
- projectRef,
- search: debouncedSearchString || undefined,
- })
- const buckets = useMemo(() => bucketsData?.pages.flatMap((page) => page) ?? [], [bucketsData])
- const {
- data: policies = [],
- refetch,
- isPending: isLoadingPolicies,
- } = useDatabasePoliciesQuery({
- projectRef: project?.ref,
- connectionString: project?.connectionString,
- schema: 'storage',
- })
- const isLoading = isLoadingBuckets || isLoadingPolicies
- const { mutateAsync: createDatabasePolicy } = useDatabasePolicyCreateMutation({
- onError: () => {},
- })
- const { mutateAsync: updateDatabasePolicy } = useDatabasePolicyUpdateMutation()
- const { mutate: deleteDatabasePolicy, isPending: isDeletingPolicy } =
- useDatabasePolicyDeleteMutation({
- onSuccess: async () => {
- await refetch()
- toast.success('Successfully deleted policy!')
- setSelectedPolicyToDelete(undefined)
- },
- onError: (error: any) => {
- toast.error(`Failed to delete policy: ${error.message}`)
- },
- })
- // Only use storage policy editor when creating new policies for buckets
- const showStoragePolicyEditor =
- isEmpty(selectedPolicyToEdit) &&
- !isEmpty(isEditingPolicyForBucket) &&
- (isEditingPolicyForBucket.bucket ?? '').length > 0
- const showGeneralPolicyEditor = !isEmpty(isEditingPolicyForBucket) && !showStoragePolicyEditor
- // Policies under storage.buckets
- const storageBucketPolicies = policies.filter(
- (x) => x.schema === 'storage' && x.table === 'buckets'
- )
- // Policies under storage.objects
- const storageObjectsPolicies = policies.filter(
- (x) => x.schema === 'storage' && x.table === 'objects'
- )
- const formattedStorageObjectPolicies = useMemo(
- () => formatPoliciesForStorage(buckets, storageObjectsPolicies),
- [buckets, storageObjectsPolicies]
- )
- const ungroupedPolicies =
- formattedStorageObjectPolicies.find((x) => x.name === UNGROUPED_POLICY_SYMBOL)?.policies ?? []
- const bucketsWithPolicies = useMemo(() => {
- // Get policies for filtered buckets (show all policies, don't filter them)
- // Show all filtered buckets, even if they don't have policies
- return buckets.map((bucket) => {
- const bucketPolicies =
- formattedStorageObjectPolicies.find((x) => x.name === bucket.name)?.policies ?? []
- return { bucket, policies: bucketPolicies }
- })
- }, [buckets, formattedStorageObjectPolicies])
- const onSelectPolicyAdd: SelectBucketPolicyForAction['addPolicy'] = (
- bucketName = '',
- table = ''
- ) => {
- setSelectedPolicyToEdit(undefined)
- setIsEditingPolicyForBucket({ bucket: bucketName, table })
- }
- const onSelectPolicyEdit: SelectBucketPolicyForAction['editPolicy'] = (
- policy,
- bucketName = '',
- table = ''
- ) => {
- setIsEditingPolicyForBucket({ bucket: bucketName, table })
- setSelectedPolicyToEdit(policy)
- }
- const onCancelPolicyEdit = () => {
- setIsEditingPolicyForBucket(undefined)
- }
- const onSelectPolicyDelete: SelectBucketPolicyForAction['deletePolicy'] = (policy: any) =>
- setSelectedPolicyToDelete(policy)
- const onCancelPolicyDelete = () => setSelectedPolicyToDelete(undefined)
- const onSavePolicySuccess = async () => {
- toast.success('Successfully saved policy!')
- await refetch()
- onCancelPolicyEdit()
- }
- /*
- Functions that involve the CRUD for policies
- For each API call within the Promise.all, return true if an error occurred, else return false
- */
- const onCreatePolicies = async (payloads: PostgresPolicyCreatePayload[]) => {
- if (!project) {
- console.error('Project is required')
- return true
- }
- try {
- return await Promise.all(
- payloads.map(async (payload) => {
- try {
- await createDatabasePolicy({
- projectRef: project?.ref,
- connectionString: project?.connectionString,
- payload,
- })
- return false
- } catch (error: any) {
- toast.error(`Error adding policy: ${error.message}`)
- return true
- }
- })
- )
- } finally {
- }
- }
- const onCreatePolicy = async (payload: PostgresPolicyCreatePayload) => {
- if (!project) {
- console.error('Project is required')
- return true
- }
- try {
- await createDatabasePolicy({
- projectRef: project?.ref,
- connectionString: project?.connectionString,
- payload,
- })
- return false
- } catch (error: any) {
- toast.error(`Error adding policy: ${error.message}`)
- return true
- }
- }
- const onUpdatePolicy = async (payload: PostgresPolicyUpdatePayload) => {
- if (!project) {
- console.error('Project is required')
- return true
- }
- if (!selectedPolicyToEdit) {
- console.error('Unable to find policy')
- return true
- }
- try {
- await updateDatabasePolicy({
- projectRef: project?.ref,
- connectionString: project?.connectionString,
- originalPolicy: selectedPolicyToEdit,
- payload,
- })
- return false
- } catch (error: any) {
- toast.error(`Error updating policy: ${error.message}`)
- return true
- }
- }
- const onDeletePolicy = async () => {
- if (!project) return console.error('Project is required')
- if (!selectedPolicyToDelete) return console.error('Unable to find policy')
- deleteDatabasePolicy({
- projectRef: project?.ref,
- connectionString: project?.connectionString,
- originalPolicy: selectedPolicyToDelete,
- })
- }
- return (
- <>
- <PageContainer>
- {isLoading ? (
- <PageSection>
- <PageSectionContent>
- <GenericSkeletonLoader />
- </PageSectionContent>
- </PageSection>
- ) : (
- <div>
- <BucketsPolicies
- buckets={bucketsWithPolicies}
- search={searchString}
- debouncedSearch={debouncedSearchString}
- setSearch={setSearchString}
- actions={{
- addPolicy: onSelectPolicyAdd,
- editPolicy: onSelectPolicyEdit,
- deletePolicy: onSelectPolicyDelete,
- }}
- pagination={{
- hasNextPage,
- isFetchingNextPage,
- fetchNextPage,
- }}
- />
- <PageSection>
- <PageSectionMeta>
- <PageSectionSummary>
- <PageSectionTitle>Schema</PageSectionTitle>
- <PageSectionDescription>
- Write policies for the tables under the storage schema directly for greater
- control
- </PageSectionDescription>
- </PageSectionSummary>
- </PageSectionMeta>
- <PageSectionContent>
- <div className="flex flex-col gap-y-4">
- {/* Section for policies under storage.objects that are not tied to any buckets */}
- <StoragePoliciesBucketRow
- table="objects"
- label="Other policies under storage.objects"
- policies={ungroupedPolicies}
- onSelectPolicyAdd={onSelectPolicyAdd}
- onSelectPolicyEdit={onSelectPolicyEdit}
- onSelectPolicyDelete={onSelectPolicyDelete}
- />
- {/* Section for policies under storage.buckets */}
- <StoragePoliciesBucketRow
- table="buckets"
- label="Policies under storage.buckets"
- policies={storageBucketPolicies}
- onSelectPolicyAdd={onSelectPolicyAdd}
- onSelectPolicyEdit={onSelectPolicyEdit}
- onSelectPolicyDelete={onSelectPolicyDelete}
- />
- </div>
- </PageSectionContent>
- </PageSection>
- </div>
- )}
- </PageContainer>
- {/* Only used for adding policies to buckets */}
- <StoragePoliciesEditPolicyModal
- visible={showStoragePolicyEditor}
- bucketName={isEditingPolicyForBucket?.bucket}
- onSelectCancel={onCancelPolicyEdit}
- onCreatePolicies={onCreatePolicies}
- onSaveSuccess={onSavePolicySuccess}
- />
- {/* Adding policies to objets/buckets table or editting any policy uses the general policy editor */}
- <PolicyEditorModal
- schema="storage"
- visible={showGeneralPolicyEditor}
- table={isEditingPolicyForBucket?.table ?? ''}
- selectedPolicyToEdit={selectedPolicyToEdit}
- onSelectCancel={onCancelPolicyEdit}
- onCreatePolicy={onCreatePolicy}
- onUpdatePolicy={onUpdatePolicy}
- onSaveSuccess={onSavePolicySuccess}
- />
- <ConfirmationModal
- visible={!isEmpty(selectedPolicyToDelete)}
- variant="destructive"
- title="Delete policy"
- description={`Are you sure you want to delete the policy “${selectedPolicyToDelete?.name}”? This action cannot be undone.`}
- confirmLabel="Delete"
- confirmLabelLoading="Deleting"
- loading={isDeletingPolicy}
- onCancel={onCancelPolicyDelete}
- onConfirm={onDeletePolicy}
- />
- </>
- )
- }
|