diff --git a/apps/studio/components/interfaces/DiskManagement/DiskManagementForm.tsx b/apps/studio/components/interfaces/DiskManagement/DiskManagementForm.tsx index 85e194c4f3..8cb6d6c6ee 100644 --- a/apps/studio/components/interfaces/DiskManagement/DiskManagementForm.tsx +++ b/apps/studio/components/interfaces/DiskManagement/DiskManagementForm.tsx @@ -25,7 +25,7 @@ import { useProjectAddonUpdateMutation } from 'data/subscriptions/project-addon- import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query' import { AddonVariantId } from 'data/subscriptions/types' import { useResourceWarningsQuery } from 'data/usage/resource-warnings-query' -import { useCheckPermissions, usePermissionsLoaded } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { useIsAwsCloudProvider, @@ -66,8 +66,7 @@ import { NoticeBar } from './ui/NoticeBar' import { SpendCapDisabledSection } from './ui/SpendCapDisabledSection' export function DiskManagementForm() { - // isLoading is used to avoid a useCheckPermissions() race condition - const { data: project, isLoading: isProjectLoading } = useSelectedProjectQuery() + const { data: project } = useSelectedProjectQuery() const { data: org } = useSelectedOrganizationQuery() const { ref: projectRef } = useParams() const queryClient = useQueryClient() @@ -80,27 +79,18 @@ export function DiskManagementForm() { const isAws = useIsAwsCloudProvider() const isAwsK8s = useIsAwsK8sCloudProvider() - /** - * Permissions - */ - const isPermissionsLoaded = usePermissionsLoaded() - const canUpdateDiskConfiguration = useCheckPermissions(PermissionAction.UPDATE, 'projects', { - resource: { - project_id: project?.id, - }, - }) + const { can: canUpdateDiskConfiguration, isSuccess: isPermissionsLoaded } = + useAsyncCheckProjectPermissions(PermissionAction.UPDATE, 'projects', { + resource: { + project_id: project?.id, + }, + }) - /** - * Component States - */ const [isDialogOpen, setIsDialogOpen] = useState(false) const [refetchInterval, setRefetchInterval] = useState(false) const [message, setMessageState] = useState(null) const [advancedSettingsOpen, setAdvancedSettingsOpenState] = useState(false) - /** - * Fetch form data - */ const { data: databases, isSuccess: isReadReplicasSuccess } = useReadReplicasQuery({ projectRef }) const { data, isSuccess: isDiskAttributesSuccess } = useDiskAttributesQuery( { projectRef }, @@ -146,9 +136,6 @@ export function DiskManagementForm() { const { data: diskAutoscaleConfig, isSuccess: isDiskAutoscaleConfigSuccess } = useDiskAutoscaleCustomConfigQuery({ projectRef }, { enabled: project != null && isAws }) - /** - * Handle default values - */ const computeSize = project?.infra_compute_size ? mapComputeSizeNameToAddonVariantId(project?.infra_compute_size) : undefined @@ -190,10 +177,6 @@ export function DiskManagementForm() { } }, [modifiedComputeSize, isDialogOpen, project]) - /** - * State handling - */ - const isSuccess = isAddonsSuccess && isDiskAttributesSuccess && @@ -228,7 +211,7 @@ export function DiskManagementForm() { const isDirty = !!Object.keys(form.formState.dirtyFields).length const isProjectResizing = project?.status === PROJECT_STATUS.RESIZING const isProjectRequestingDiskChanges = isRequestingChanges && !isProjectResizing - const noPermissions = isPermissionsLoaded && !canUpdateDiskConfiguration && !isProjectLoading + const noPermissions = isPermissionsLoaded && !canUpdateDiskConfiguration const { mutateAsync: updateDiskConfiguration, isLoading: isUpdatingDisk } = useUpdateDiskAttributesMutation({ diff --git a/apps/studio/components/interfaces/DiskManagement/DiskManagementReviewAndSubmitDialog.tsx b/apps/studio/components/interfaces/DiskManagement/DiskManagementReviewAndSubmitDialog.tsx index 03b3d42a7f..89512705ff 100644 --- a/apps/studio/components/interfaces/DiskManagement/DiskManagementReviewAndSubmitDialog.tsx +++ b/apps/studio/components/interfaces/DiskManagement/DiskManagementReviewAndSubmitDialog.tsx @@ -5,7 +5,7 @@ import { UseFormReturn } from 'react-hook-form' import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { formatCurrency } from 'lib/helpers' @@ -153,11 +153,15 @@ export const DiskManagementReviewAndSubmitDialog = ({ const { formState, getValues } = form - const canUpdateDiskConfiguration = useCheckPermissions(PermissionAction.UPDATE, 'projects', { - resource: { - project_id: project?.id, - }, - }) + const { can: canUpdateDiskConfiguration } = useAsyncCheckProjectPermissions( + PermissionAction.UPDATE, + 'projects', + { + resource: { + project_id: project?.id, + }, + } + ) /** * Queries diff --git a/apps/studio/components/interfaces/Docs/Description.tsx b/apps/studio/components/interfaces/Docs/Description.tsx index dc9dfb112d..b2e6176400 100644 --- a/apps/studio/components/interfaces/Docs/Description.tsx +++ b/apps/studio/components/interfaces/Docs/Description.tsx @@ -5,7 +5,7 @@ import { toast } from 'sonner' import AutoTextArea from 'components/to-be-cleaned/forms/AutoTextArea' import { executeSql } from 'data/sql/execute-sql-query' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { timeout } from 'lib/helpers' import { Loader } from 'lucide-react' @@ -42,7 +42,10 @@ const Description = ({ content, metadata, onChange = noop }: DescrptionProps) => const hasChanged = value != contentText const animateCss = `transition duration-150` - const canUpdateDescription = useCheckPermissions(PermissionAction.TENANT_SQL_QUERY, '*') + const { can: canUpdateDescription } = useAsyncCheckProjectPermissions( + PermissionAction.TENANT_SQL_QUERY, + '*' + ) const updateDescription = async () => { if (isUpdating || !canUpdateDescription) return false @@ -76,7 +79,9 @@ const Description = ({ content, metadata, onChange = noop }: DescrptionProps) => if (!canUpdateDescription) { return ( - {value || 'No description'} + + {value || 'No description'} + ) } diff --git a/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionDetails.tsx b/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionDetails.tsx index 6358d77eac..efde692501 100644 --- a/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionDetails.tsx +++ b/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionDetails.tsx @@ -18,7 +18,7 @@ import { useCustomDomainsQuery } from 'data/custom-domains/custom-domains-query' import { useEdgeFunctionQuery } from 'data/edge-functions/edge-function-query' import { useEdgeFunctionDeleteMutation } from 'data/edge-functions/edge-functions-delete-mutation' import { useEdgeFunctionUpdateMutation } from 'data/edge-functions/edge-functions-update-mutation' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { Alert_Shadcn_, AlertDescription_Shadcn_, @@ -59,7 +59,10 @@ export const EdgeFunctionDetails = () => { const router = useRouter() const { ref: projectRef, functionSlug } = useParams() const [showDeleteModal, setShowDeleteModal] = useState(false) - const canUpdateEdgeFunction = useCheckPermissions(PermissionAction.FUNCTIONS_WRITE, '*') + const { can: canUpdateEdgeFunction } = useAsyncCheckProjectPermissions( + PermissionAction.FUNCTIONS_WRITE, + '*' + ) const { data: apiKeys } = useAPIKeysQuery({ projectRef }) const { data: settings } = useProjectSettingsV2Query({ projectRef }) diff --git a/apps/studio/components/interfaces/Functions/EdgeFunctionSecrets/EdgeFunctionSecret.tsx b/apps/studio/components/interfaces/Functions/EdgeFunctionSecrets/EdgeFunctionSecret.tsx index 7bdc4c9ef7..16edc741a3 100644 --- a/apps/studio/components/interfaces/Functions/EdgeFunctionSecrets/EdgeFunctionSecret.tsx +++ b/apps/studio/components/interfaces/Functions/EdgeFunctionSecrets/EdgeFunctionSecret.tsx @@ -1,12 +1,11 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' import { Trash } from 'lucide-react' -import Table from 'components/to-be-cleaned/Table' import { ButtonTooltip } from 'components/ui/ButtonTooltip' import type { ProjectSecret } from 'data/secrets/secrets-query' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' -import { TimestampInfo } from 'ui-patterns' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { TableCell, TableRow } from 'ui' +import { TimestampInfo } from 'ui-patterns' interface EdgeFunctionSecretProps { secret: ProjectSecret @@ -14,7 +13,10 @@ interface EdgeFunctionSecretProps { } const EdgeFunctionSecret = ({ secret, onSelectDelete }: EdgeFunctionSecretProps) => { - const canUpdateSecrets = useCheckPermissions(PermissionAction.SECRETS_WRITE, '*') + const { can: canUpdateSecrets } = useAsyncCheckProjectPermissions( + PermissionAction.SECRETS_WRITE, + '*' + ) // [Joshen] Following API's validation: // https://github.com/supabase/infrastructure/blob/develop/api/src/routes/v1/projects/ref/secrets/secrets.controller.ts#L106 const isReservedSecret = !!secret.name.match(/^(SUPABASE_).*/) diff --git a/apps/studio/components/interfaces/Functions/EdgeFunctionSecrets/EdgeFunctionSecrets.tsx b/apps/studio/components/interfaces/Functions/EdgeFunctionSecrets/EdgeFunctionSecrets.tsx index 5312e2611d..00d859249f 100644 --- a/apps/studio/components/interfaces/Functions/EdgeFunctionSecrets/EdgeFunctionSecrets.tsx +++ b/apps/studio/components/interfaces/Functions/EdgeFunctionSecrets/EdgeFunctionSecrets.tsx @@ -9,18 +9,8 @@ import NoPermission from 'components/ui/NoPermission' import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' import { useSecretsDeleteMutation } from 'data/secrets/secrets-delete-mutation' import { ProjectSecret, useSecretsQuery } from 'data/secrets/secrets-query' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' -import { - Badge, - Separator, - Table, - TableHead, - TableHeader, - TableRow, - TableCell, - TableBody, - Card, -} from 'ui' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' +import { Badge, Card, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from 'ui' import { Input } from 'ui-patterns/DataInputs/Input' import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' import AddNewSecretForm from './AddNewSecretForm' @@ -31,8 +21,14 @@ const EdgeFunctionSecrets = () => { const [searchString, setSearchString] = useState('') const [selectedSecret, setSelectedSecret] = useState() - const canReadSecrets = useCheckPermissions(PermissionAction.SECRETS_READ, '*') - const canUpdateSecrets = useCheckPermissions(PermissionAction.SECRETS_WRITE, '*') + const { can: canReadSecrets, isLoading: isLoadingPermissions } = useAsyncCheckProjectPermissions( + PermissionAction.SECRETS_READ, + '*' + ) + const { can: canUpdateSecrets } = useAsyncCheckProjectPermissions( + PermissionAction.SECRETS_WRITE, + '*' + ) const { data, error, isLoading, isSuccess, isError } = useSecretsQuery({ projectRef: projectRef, @@ -65,70 +61,76 @@ const EdgeFunctionSecrets = () => { return ( <> - {isLoading && } - {isError && } - {isSuccess && ( + {isLoading || isLoadingPermissions ? ( + + ) : ( <> -
- {!canUpdateSecrets ? ( - - ) : ( - - )} -
- {canUpdateSecrets && !canReadSecrets ? ( - - ) : canReadSecrets ? ( -
-
- setSearchString(e.target.value)} - icon={} - /> -
+ {isError && } - - - - {headers} - - - {secrets.length > 0 ? ( - secrets.map((secret) => ( - setSelectedSecret(secret)} - /> - )) - ) : secrets.length === 0 && searchString.length > 0 ? ( - - -

No results found

-

- Your search for "{searchString}" did not return any results -

-
-
- ) : ( - - -

No secrets created

-

- There are no secrets associated with your project yet -

-
-
- )} -
-
-
-
- ) : null} + {isSuccess && ( + <> +
+ {!canUpdateSecrets ? ( + + ) : ( + + )} +
+ {canUpdateSecrets && !canReadSecrets ? ( + + ) : canReadSecrets ? ( +
+
+ setSearchString(e.target.value)} + icon={} + /> +
+ + + + + {headers} + + + {secrets.length > 0 ? ( + secrets.map((secret) => ( + setSelectedSecret(secret)} + /> + )) + ) : secrets.length === 0 && searchString.length > 0 ? ( + + +

No results found

+

+ Your search for "{searchString}" did not return any results +

+
+
+ ) : ( + + +

No secrets created

+

+ There are no secrets associated with your project yet +

+
+
+ )} +
+
+
+
+ ) : null} + + )} )} diff --git a/apps/studio/components/interfaces/GraphQL/GraphiQL.tsx b/apps/studio/components/interfaces/GraphQL/GraphiQL.tsx index 82f5714e96..c7351a4db7 100644 --- a/apps/studio/components/interfaces/GraphQL/GraphiQL.tsx +++ b/apps/studio/components/interfaces/GraphQL/GraphiQL.tsx @@ -35,12 +35,12 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' import { AlertTriangle, XIcon } from 'lucide-react' import { MouseEventHandler, useCallback, useEffect, useState } from 'react' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { LOCAL_STORAGE_KEYS } from 'common' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useLocalStorage } from 'hooks/misc/useLocalStorage' import { AlertDescription_Shadcn_, AlertTitle_Shadcn_, Alert_Shadcn_, Button, cn } from 'ui' import { RoleImpersonationSelector } from '../RoleImpersonationSelector' import styles from './graphiql.module.css' -import { LOCAL_STORAGE_KEYS } from 'common' export interface GraphiQLProps { fetcher: Fetcher @@ -76,7 +76,10 @@ const GraphiQLInterface = ({ theme }: GraphiQLInterfaceProps) => { const merge = useMergeQuery() const prettify = usePrettifyEditors() - const canReadJWTSecret = useCheckPermissions(PermissionAction.READ, 'field.jwt_secret') + const { can: canReadJWTSecret } = useAsyncCheckProjectPermissions( + PermissionAction.READ, + 'field.jwt_secret' + ) const [rlsBypassedWarningDismissed, setRlsBypassedWarningDismissed] = useLocalStorage( LOCAL_STORAGE_KEYS.GRAPHIQL_RLS_BYPASS_WARNING, diff --git a/apps/studio/components/interfaces/Home/NewProjectPanel/APIKeys.tsx b/apps/studio/components/interfaces/Home/NewProjectPanel/APIKeys.tsx index 5c62c1c2a5..05872a5834 100644 --- a/apps/studio/components/interfaces/Home/NewProjectPanel/APIKeys.tsx +++ b/apps/studio/components/interfaces/Home/NewProjectPanel/APIKeys.tsx @@ -9,7 +9,7 @@ import Panel from 'components/ui/Panel' import { getKeys, useAPIKeysQuery } from 'data/api-keys/api-keys-query' import { useJwtSecretUpdatingStatusQuery } from 'data/config/jwt-secret-updating-status-query' import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { Input, SimpleCodeBlock } from 'ui' const generateInitSnippet = (endpoint: string) => ({ @@ -65,7 +65,10 @@ const APIKeys = () => { const jwtSecretUpdateStatus = data?.jwtSecretUpdateStatus - const canReadAPIKeys = useCheckPermissions(PermissionAction.READ, 'service_api_keys') + const { can: canReadAPIKeys } = useAsyncCheckProjectPermissions( + PermissionAction.READ, + 'service_api_keys' + ) const isNotUpdatingJwtSecret = jwtSecretUpdateStatus === undefined || jwtSecretUpdateStatus === JwtSecretUpdateStatus.Updated diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/CreateCronJobSheet.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/CreateCronJobSheet.tsx index bc6dcfa996..fb97c5c828 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/CreateCronJobSheet.tsx +++ b/apps/studio/components/interfaces/Integrations/CronJobs/CreateCronJobSheet.tsx @@ -16,7 +16,7 @@ import { useDatabaseCronJobCreateMutation } from 'data/database-cron-jobs/databa import { CronJob } from 'data/database-cron-jobs/database-cron-jobs-infinite-query' import { useDatabaseExtensionsQuery } from 'data/database-extensions/database-extensions-query' import { useSendEventMutation } from 'data/telemetry/send-event-mutation' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { @@ -220,7 +220,7 @@ export const CreateCronJobSheet = ({ const { mutate: upsertCronJob, isLoading: isUpserting } = useDatabaseCronJobCreateMutation() const isLoading = isLoadingGetCronJob || isUpserting - const canToggleExtensions = useCheckPermissions( + const { can: canToggleExtensions } = useAsyncCheckProjectPermissions( PermissionAction.TENANT_SQL_ADMIN_WRITE, 'extensions' ) diff --git a/apps/studio/components/interfaces/Integrations/Queues/CreateQueueSheet.tsx b/apps/studio/components/interfaces/Integrations/Queues/CreateQueueSheet.tsx index d09f681b64..b9720548c4 100644 --- a/apps/studio/components/interfaces/Integrations/Queues/CreateQueueSheet.tsx +++ b/apps/studio/components/interfaces/Integrations/Queues/CreateQueueSheet.tsx @@ -74,7 +74,7 @@ const FORM_ID = 'create-queue-sidepanel' export const CreateQueueSheet = ({ isClosing, setIsClosing, onClose }: CreateQueueSheetProps) => { // This is for enabling pg_partman extension which will be used for partitioned queues (3rd kind of queue) // const [showEnableExtensionModal, setShowEnableExtensionModal] = useState(false) - // const canToggleExtensions = useCheckPermissions( + // const { can: canToggleExtensions } = useAsyncCheckPermissions( // PermissionAction.TENANT_SQL_ADMIN_WRITE, // 'extensions' // ) diff --git a/apps/studio/components/interfaces/Integrations/Queues/QueuesSettings.tsx b/apps/studio/components/interfaces/Integrations/Queues/QueuesSettings.tsx index a1ce96787c..2b5b6bd079 100644 --- a/apps/studio/components/interfaces/Integrations/Queues/QueuesSettings.tsx +++ b/apps/studio/components/interfaces/Integrations/Queues/QueuesSettings.tsx @@ -21,7 +21,7 @@ import { } from 'data/database-queues/database-queues-toggle-postgrest-mutation' import { useTableUpdateMutation } from 'data/tables/table-update-mutation' import { useTablesQuery } from 'data/tables/tables-query' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { Button, @@ -39,7 +39,7 @@ import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' export const QueuesSettings = () => { const { data: project } = useSelectedProjectQuery() - const canUpdatePostgrestConfig = useCheckPermissions( + const { can: canUpdatePostgrestConfig } = useAsyncCheckProjectPermissions( PermissionAction.UPDATE, 'custom_config_postgrest' ) diff --git a/apps/studio/components/interfaces/Integrations/Vault/Secrets/SecretRow.tsx b/apps/studio/components/interfaces/Integrations/Vault/Secrets/SecretRow.tsx index 20a1ace90e..b69576d545 100644 --- a/apps/studio/components/interfaces/Integrations/Vault/Secrets/SecretRow.tsx +++ b/apps/studio/components/interfaces/Integrations/Vault/Secrets/SecretRow.tsx @@ -6,16 +6,14 @@ import { Button, DropdownMenu, DropdownMenuContent, - DropdownMenuItem, + DropdownMenuSeparator, DropdownMenuTrigger, Input, - Tooltip, - TooltipContent, - TooltipTrigger, } from 'ui' +import { DropdownMenuItemTooltip } from 'components/ui/DropdownMenuItemTooltip' import { useVaultSecretDecryptedValueQuery } from 'data/vault/vault-secret-decrypted-value-query' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { Edit3, Eye, EyeOff, Key, Loader, MoreVertical, Trash } from 'lucide-react' import type { VaultSecret } from 'types' @@ -33,7 +31,10 @@ const SecretRow = ({ secret, onSelectRemove }: SecretRowProps) => { const [revealSecret, setRevealSecret] = useState(false) const name = secret?.name ?? 'No name provided' - const canManageSecrets = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'tables') + const { can: canManageSecrets } = useAsyncCheckProjectPermissions( + PermissionAction.TENANT_SQL_ADMIN_WRITE, + 'tables' + ) const { data: revealedValue, isFetching } = useVaultSecretDecryptedValueQuery( { @@ -102,41 +103,34 @@ const SecretRow = ({ secret, onSelectRemove }: SecretRowProps) => {