diff --git a/studio/components/interfaces/Database/Backups/BackupsList.tsx b/studio/components/interfaces/Database/Backups/BackupsList.tsx
index 7f4f41434c5..b444a8e913d 100644
--- a/studio/components/interfaces/Database/Backups/BackupsList.tsx
+++ b/studio/components/interfaces/Database/Backups/BackupsList.tsx
@@ -35,7 +35,7 @@ const BackupsList: FC = ({}) => {
icon={ }
primaryText="Free Plan does not include project backups."
projectRef={projectRef}
- secondaryText="Please upgrade to the Pro plan for up to 7 days of scheduled backups."
+ secondaryText="Upgrade to the Pro plan for up to 7 days of scheduled backups."
/>
)
}
diff --git a/studio/components/interfaces/Settings/Database/ConnectionPooling.tsx b/studio/components/interfaces/Settings/Database/ConnectionPooling.tsx
index 111a003258a..43ce15af121 100644
--- a/studio/components/interfaces/Settings/Database/ConnectionPooling.tsx
+++ b/studio/components/interfaces/Settings/Database/ConnectionPooling.tsx
@@ -1,6 +1,6 @@
import { FC, useState, useEffect, Fragment } from 'react'
import { observer } from 'mobx-react-lite'
-import { useStore } from 'hooks'
+import { checkPermissions, useStore } from 'hooks'
import { pluckObjectFields } from 'lib/helpers'
import Loading from 'components/ui/Loading'
import Panel from 'components/ui/Panel'
@@ -12,6 +12,7 @@ import ToggleField from 'components/to-be-cleaned/forms/ToggleField'
import SchemaFormPanel from 'components/to-be-cleaned/forms/SchemaFormPanel'
import { Input } from 'ui'
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
+import { PermissionAction } from '@supabase/shared-types/out/constants'
interface Props {}
@@ -127,6 +128,11 @@ export const PgbouncerConfig: FC = observer(
({ projectRef, bouncerInfo, connectionInfo }) => {
const { ui } = useStore()
+ const canUpdateConnectionPoolingConfiguration = checkPermissions(
+ PermissionAction.UPDATE,
+ 'projects'
+ )
+
const [updates, setUpdates] = useState({
pgbouncer_enabled: bouncerInfo.pgbouncer_enabled,
pool_mode: bouncerInfo.pool_mode || 'transaction',
@@ -198,6 +204,8 @@ export const PgbouncerConfig: FC = observer(
onChangeModel={(model: any) => setUpdates(model)}
onSubmit={(model: any) => updateConfig(model)}
onReset={() => setUpdates(bouncerInfo)}
+ disabled={!canUpdateConnectionPoolingConfiguration}
+ disabledMessage="You need additional permissions to update connection pooling settings"
>
diff --git a/studio/components/interfaces/Settings/Database/NetworkRestrictions/NetworkRestrictions.tsx b/studio/components/interfaces/Settings/Database/NetworkRestrictions/NetworkRestrictions.tsx
index 7fa753067e7..b3c47c59119 100644
--- a/studio/components/interfaces/Settings/Database/NetworkRestrictions/NetworkRestrictions.tsx
+++ b/studio/components/interfaces/Settings/Database/NetworkRestrictions/NetworkRestrictions.tsx
@@ -1,8 +1,10 @@
import Link from 'next/link'
-import { useState } from 'react'
+import { FC, useState } from 'react'
+import * as Tooltip from '@radix-ui/react-tooltip'
import { Button, IconAlertCircle, IconExternalLink, IconGlobe, IconLock } from 'ui'
+import { PermissionAction } from '@supabase/shared-types/out/constants'
-import { useParams } from 'hooks'
+import { checkPermissions, useParams } from 'hooks'
import Panel from 'components/ui/Panel'
import { FormPanel, FormHeader } from 'components/ui/Forms'
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
@@ -12,6 +14,62 @@ import DisallowAllModal from './DisallowAllModal'
import AllowAllModal from './AllowAllModal'
import { useNetworkRestrictionsQuery } from 'data/network-restrictions/network-restrictions-query'
+const AllowAllAccessButton: FC<{ disabled: boolean; onClick: (value: boolean) => void }> = ({
+ disabled,
+ onClick,
+}) => (
+
+
+ onClick(true)}>
+ Allow all access
+
+
+ {disabled && (
+
+
+
+
+ You need additional permissions to update network restrictions
+
+
+
+ )}
+
+)
+
+const DisallowAllAccessButton: FC<{ disabled: boolean; onClick: (value: boolean) => void }> = ({
+ disabled,
+ onClick,
+}) => (
+
+
+ onClick(true)}>
+ Restrict all access
+
+
+ {disabled && (
+
+
+
+
+ You need additional permissions to update network restrictions
+
+
+
+ )}
+
+)
+
const NetworkRestrictions = ({}) => {
const { ref } = useParams()
@@ -21,6 +79,8 @@ const NetworkRestrictions = ({}) => {
const [selectedRestrictionToRemove, setSelectedRestrictionToRemove] = useState
()
const { data, isLoading } = useNetworkRestrictionsQuery({ projectRef: ref })
+ const canUpdateNetworkRestrictions = checkPermissions(PermissionAction.UPDATE, 'projects')
+
const hasAccessToRestrictions = data?.entitlement === 'allowed'
const restrictedIps = data?.config.dbAllowedCidrs ?? []
const restrictionStatus = data?.status ?? ''
@@ -48,7 +108,31 @@ const NetworkRestrictions = ({}) => {
- setIsAddingAddress(true)}>Add restriction
+
+
+ setIsAddingAddress(true)}
+ >
+ Add restriction
+
+
+ {!canUpdateNetworkRestrictions && (
+
+
+
+
+ You need additional permissions to update network restrictions
+
+
+
+ )}
+
{isLoading ? (
@@ -75,12 +159,14 @@ const NetworkRestrictions = ({}) => {
-
setIsAllowingAll(true)}>
- Allow all access
-
-
setIsDisallowingAll(true)}>
- Restrict all access
-
+
+
@@ -101,9 +187,10 @@ const NetworkRestrictions = ({}) => {
- setIsDisallowingAll(true)}>
- Restrict all access
-
+
) : isDisallowedAll ? (
@@ -125,9 +212,10 @@ const NetworkRestrictions = ({}) => {
-
setIsAllowingAll(true)}>
- Allow all access
-
+
) : (
@@ -146,12 +234,14 @@ const NetworkRestrictions = ({}) => {
-
setIsAllowingAll(true)}>
- Allow all access
-
-
setIsDisallowingAll(true)}>
- Restrict all access
-
+
+
{restrictedIps.map((ip) => {
diff --git a/studio/components/interfaces/Settings/Database/SSLConfiguration.tsx b/studio/components/interfaces/Settings/Database/SSLConfiguration.tsx
index d1709896cf1..989df6e60fe 100644
--- a/studio/components/interfaces/Settings/Database/SSLConfiguration.tsx
+++ b/studio/components/interfaces/Settings/Database/SSLConfiguration.tsx
@@ -1,8 +1,10 @@
import Link from 'next/link'
import { useState, useEffect } from 'react'
-import { useParams, useStore } from 'hooks'
+import { checkPermissions, useParams, useStore } from 'hooks'
import * as Tooltip from '@radix-ui/react-tooltip'
import { Button, IconDownload, Toggle, IconExternalLink, IconLoader, Alert } from 'ui'
+import { PermissionAction } from '@supabase/shared-types/out/constants'
+
import {
FormHeader,
FormPanel,
@@ -25,6 +27,8 @@ const SSLConfiguration = () => {
})
const { mutateAsync: updateSSLEnforcement } = useSSLEnforcementUpdateMutation()
+ const canUpdateSSLEnforcement = checkPermissions(PermissionAction.UPDATE, 'projects')
+
const hasAccessToSSLEnforcement = !sslEnforcementConfiguration?.isNotAllowed
const env = process.env.NEXT_PUBLIC_ENVIRONMENT === 'prod' ? 'prod' : 'staging'
const hasSSLCertificate =
@@ -115,11 +119,30 @@ const SSLConfiguration = () => {
{(isLoading || isSubmitting) && (
)}
-
+
+
+
+
+ {!canUpdateSSLEnforcement && (
+
+
+
+
+ You need additional permissions to update SSL configurations
+
+
+
+ )}
+
diff --git a/studio/components/interfaces/Settings/ProjectUsageBars/ProjectUsageBars.tsx b/studio/components/interfaces/Settings/ProjectUsageBars/ProjectUsageBars.tsx
index ede0524c0a3..cfe5c5ac2e2 100644
--- a/studio/components/interfaces/Settings/ProjectUsageBars/ProjectUsageBars.tsx
+++ b/studio/components/interfaces/Settings/ProjectUsageBars/ProjectUsageBars.tsx
@@ -1,7 +1,10 @@
import { FC, useEffect } from 'react'
+import { useRouter } from 'next/router'
import { Badge, Button, IconAlertCircle, IconInfo, Loading } from 'ui'
+import * as Tooltip from '@radix-ui/react-tooltip'
+import { PermissionAction } from '@supabase/shared-types/out/constants'
-import { useStore } from 'hooks'
+import { checkPermissions, useStore } from 'hooks'
import { formatBytes } from 'lib/helpers'
import { PRICING_TIER_PRODUCT_IDS, USAGE_APPROACHING_THRESHOLD } from 'lib/constants'
import SparkBar from 'components/ui/SparkBar'
@@ -9,8 +12,6 @@ import ShimmeringLoader from 'components/ui/ShimmeringLoader'
import InformationBox from 'components/ui/InformationBox'
import { USAGE_BASED_PRODUCTS } from 'components/interfaces/Billing/Billing.constants'
import { ProjectUsageResponse, useProjectUsageQuery } from 'data/usage/project-usage-query'
-import { useRouter } from 'next/router'
-import * as Tooltip from '@radix-ui/react-tooltip'
interface Props {
projectRef?: string
@@ -21,6 +22,11 @@ const ProjectUsage: FC = ({ projectRef }) => {
const { data: usage, error, isLoading } = useProjectUsageQuery({ projectRef })
const router = useRouter()
+ const canUpdateSubscription = checkPermissions(
+ PermissionAction.BILLING_WRITE,
+ 'stripe.subscriptions'
+ )
+
const subscriptionTier = ui.selectedProject?.subscription_tier
const projectHasNoLimits =
@@ -128,14 +134,16 @@ const ProjectUsage: FC = ({ projectRef }) => {
usageElement = (
Not included in {planName} tier
-
- router.push(`/project/${projectRef}/settings/billing/update`)
- }
- >
- Upgrade to Pro
-
+ {canUpdateSubscription && (
+
+ router.push(`/project/${projectRef}/settings/billing/update`)
+ }
+ >
+ Upgrade to Pro
+
+ )}
)
} else if (showUsageExceedMessage) {
diff --git a/studio/components/to-be-cleaned/Storage/StorageSettings/StorageSettings.tsx b/studio/components/to-be-cleaned/Storage/StorageSettings/StorageSettings.tsx
index 4e05953129a..65662c22665 100644
--- a/studio/components/to-be-cleaned/Storage/StorageSettings/StorageSettings.tsx
+++ b/studio/components/to-be-cleaned/Storage/StorageSettings/StorageSettings.tsx
@@ -1,12 +1,14 @@
import { useState } from 'react'
+import * as Tooltip from '@radix-ui/react-tooltip'
import { Button, Form, IconClock, Input, Listbox } from 'ui'
-import { useStore } from 'hooks'
+import { checkPermissions, useStore } from 'hooks'
import { useProjectStorageConfigQuery } from 'data/config/project-storage-config-query'
import { useProjectStorageConfigUpdateUpdateMutation } from 'data/config/project-storage-config-update-mutation'
import UpgradeToPro from 'components/ui/UpgradeToPro'
import { convertFromBytes, convertToBytes } from './StorageSettings.utils'
import { StorageSizeUnits, STORAGE_FILE_SIZE_LIMIT_MAX_BYTES } from './StorageSettings.constants'
+import { PermissionAction } from '@supabase/shared-types/out/constants'
export type StorageSettingsProps = {
projectRef: string | undefined
@@ -42,6 +44,8 @@ const StorageConfig = ({ config, projectRef }: any) => {
const [selectedUnit, setSelectedUnit] = useState(unit)
let initialValues = { fileSizeLimit: value, unformattedFileSizeLimit: fileSizeLimit }
+ const canUpdateStorageSettings = checkPermissions(PermissionAction.UPDATE, 'projects')
+
const formattedMaxSizeBytes = `${new Intl.NumberFormat('en-US').format(
STORAGE_FILE_SIZE_LIMIT_MAX_BYTES
)} bytes`
@@ -184,13 +188,20 @@ const StorageConfig = ({ config, projectRef }: any) => {
icon={ }
primaryText="Free Plan has a fixed upload file size limit of 50 MB."
projectRef={projectRef}
- secondaryText="Please upgrade to Pro plan for a configurable upload file size limit of up to 5 GB."
+ secondaryText="Upgrade to the Pro plan for a configurable upload file size limit of up to 5 GB."
/>
)}
-
+
+ {!canUpdateStorageSettings ? (
+
+ You need additional permissions to update storage settings
+
+ ) : (
+
+ )}
{
type="primary"
htmlType="submit"
loading={isSubmitting}
- disabled={!hasChanges && hasChanges !== undefined}
+ disabled={
+ !canUpdateStorageSettings || (!hasChanges && hasChanges !== undefined)
+ }
>
Save
diff --git a/studio/components/to-be-cleaned/forms/SchemaForm.js b/studio/components/to-be-cleaned/forms/SchemaForm.js
index bc5e6ac20a0..fa65c7b1560 100644
--- a/studio/components/to-be-cleaned/forms/SchemaForm.js
+++ b/studio/components/to-be-cleaned/forms/SchemaForm.js
@@ -2,10 +2,19 @@ import { AutoForm } from 'uniforms-bootstrap4'
import Ajv from 'ajv'
import { JSONSchemaBridge } from 'uniforms-bridge-json-schema'
-const SchemaForm = ({ schema, model, children, formRef, onSubmit, onChangeModel = () => {} }) => {
+const SchemaForm = ({
+ schema,
+ model,
+ disabled = false,
+ children,
+ formRef,
+ onSubmit,
+ onChangeModel = () => {},
+}) => {
const validatedSchema = new JSONSchemaBridge(schema, createValidator(schema))
return (
{title || ''}
-
-
- {cancelLabel || 'Cancel'}
-
-
{disabledMessage}
+ ) : (
+
- {submitLabel || 'Save'}
-
-
+
+ {cancelLabel || 'Cancel'}
+
+
+ {submitLabel || 'Save'}
+
+
+ )}
{message}
{
setHasChanged(true)
if (onChangeModel) onChangeModel(model)
diff --git a/studio/components/ui/UpgradeToPro.tsx b/studio/components/ui/UpgradeToPro.tsx
index ca7b1b549b6..ccee10b1295 100644
--- a/studio/components/ui/UpgradeToPro.tsx
+++ b/studio/components/ui/UpgradeToPro.tsx
@@ -2,8 +2,11 @@ import { Button } from 'ui'
import { observer } from 'mobx-react-lite'
import Link from 'next/link'
import { FC, ReactNode } from 'react'
-import { useStore } from 'hooks'
+import * as Tooltip from '@radix-ui/react-tooltip'
+
+import { checkPermissions, useStore, useFlag, useParams } from 'hooks'
import { PRICING_TIER_PRODUCT_IDS } from 'lib/constants'
+import { PermissionAction } from '@supabase/shared-types/out/constants'
interface Props {
icon?: ReactNode
@@ -14,8 +17,16 @@ interface Props {
const UpgradeToPro: FC = ({ icon, primaryText, projectRef, secondaryText }) => {
const { ui } = useStore()
+ const { ref } = useParams()
const tier = ui.selectedProject?.subscription_tier
+ const canUpdateSubscription = checkPermissions(
+ PermissionAction.BILLING_WRITE,
+ 'stripe.subscriptions'
+ )
+ const projectUpdateDisabled = useFlag('disableProjectCreationAndUpdate')
+ const isEnterprise = tier === PRICING_TIER_PRODUCT_IDS.ENTERPRISE
+
return (
= ({ icon, primaryText, projectRef, secondaryText
{secondaryText}
-
-
-
- {tier === PRICING_TIER_PRODUCT_IDS.FREE ? 'Upgrade to Pro' : 'Modify subscription'}
+
+
+
+
+
+ {tier === PRICING_TIER_PRODUCT_IDS.FREE
+ ? 'Upgrade to Pro'
+ : 'Modify subscription'}
+
+
-
-
+
+ {!canUpdateSubscription || projectUpdateDisabled ? (
+
+
+
+
+ {projectUpdateDisabled ? (
+ <>
+ Subscription changes are currently disabled.
+
+ Our engineers are working on a fix.
+ >
+ ) : !canUpdateSubscription ? (
+ 'You need additional permissions to amend subscriptions'
+ ) : (
+ ''
+ )}
+
+
+
+ ) : (
+ <>>
+ )}
+
diff --git a/studio/pages/project/[ref]/database/backups/pitr.tsx b/studio/pages/project/[ref]/database/backups/pitr.tsx
index 5f3eb4aadad..694e62f97c7 100644
--- a/studio/pages/project/[ref]/database/backups/pitr.tsx
+++ b/studio/pages/project/[ref]/database/backups/pitr.tsx
@@ -65,7 +65,7 @@ const PITR = () => {
primaryText="Point in time recovery is a Pro plan add-on."
secondaryText={
tier === PRICING_TIER_PRODUCT_IDS.FREE
- ? 'Please upgrade to the Pro plan with the PITR add-on selected to enable point in time recovery for your project.'
+ ? 'Upgrade to the Pro plan with the PITR add-on selected to enable point in time recovery for your project.'
: 'Please enable the add-on to enable point in time recovery for your project.'
}
/>
diff --git a/studio/pages/project/[ref]/settings/billing/subscription.tsx b/studio/pages/project/[ref]/settings/billing/subscription.tsx
index 750af452736..a47a62ad90d 100644
--- a/studio/pages/project/[ref]/settings/billing/subscription.tsx
+++ b/studio/pages/project/[ref]/settings/billing/subscription.tsx
@@ -36,7 +36,6 @@ interface SettingsProps {
const Settings: FC = ({ project }) => {
const { ui } = useStore()
- const projectTier = ui.selectedProject?.subscription_tier
const {
data: subscription,
diff --git a/studio/sentry.client.config.js b/studio/sentry.client.config.js
index 456f62c88a0..23e597a09cb 100644
--- a/studio/sentry.client.config.js
+++ b/studio/sentry.client.config.js
@@ -39,6 +39,9 @@ Sentry.init({
// Safe to ignore since it's not a user-facing issue + we've not received any user feedback/report about it
// Ref: https://github.com/supabase/supabase/pull/9729
'The provided `href` (/org/[slug]/general) value is missing query values (slug)',
+ 'The provided `href` (/org/[slug]/team) value is missing query values (slug)',
+ 'The provided `href` (/org/[slug]/billing) value is missing query values (slug)',
+ 'The provided `href` (/org/[slug]/invoices) value is missing query values (slug)',
],
beforeSend: (event) => filterConsoleErrors(event),
})