mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 17:35:10 +03:00
Merge branch 'master' into chore/swr-to-react-query-1
This commit is contained in:
commit
20aca85df5
28 files changed
+930
-32
No files matched your search
@@ -3,6 +3,7 @@ import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { Fragment } from 'react'
|
||||
import { Badge } from '~/../../packages/ui'
|
||||
|
||||
const home = [
|
||||
[
|
||||
@@ -94,17 +95,19 @@ const home = [
|
||||
href: '/reference/dart/introduction',
|
||||
level: 'reference_dart',
|
||||
},
|
||||
{
|
||||
label: 'C#',
|
||||
icon: '/img/icons/menu/reference-csharp',
|
||||
href: '/reference/csharp/introduction',
|
||||
level: 'reference_csharp',
|
||||
},
|
||||
{
|
||||
label: 'Python',
|
||||
icon: '/img/icons/menu/reference-python',
|
||||
href: '/reference/python/introduction',
|
||||
level: 'reference_python',
|
||||
community: true,
|
||||
},
|
||||
{
|
||||
label: 'C#',
|
||||
icon: '/img/icons/menu/reference-csharp',
|
||||
href: '/reference/csharp/introduction',
|
||||
level: 'reference_csharp',
|
||||
community: true,
|
||||
},
|
||||
{
|
||||
label: 'Tools',
|
||||
@@ -177,6 +180,7 @@ const NavigationMenuHome = ({ active }) => {
|
||||
className="w-4 h-4 group-hover:scale-110 ease-out transition-all"
|
||||
/>
|
||||
{link.label}
|
||||
{link.community && <Badge size="small">Community</Badge>}
|
||||
</li>
|
||||
</a>
|
||||
</Link>
|
||||
|
||||
@@ -179,7 +179,6 @@ const Content: React.FC<INavigationMenuRefList> = ({ id, lib, commonSections, sp
|
||||
if (!sections) console.error('no common sections imported')
|
||||
|
||||
const menu = NavItems[id]
|
||||
//console.log(menu)
|
||||
const databaseFunctions = find(sections, { title: 'Database' })
|
||||
? find(sections, { title: 'Database' }).items
|
||||
: []
|
||||
|
||||
@@ -8,13 +8,24 @@ hideTitle: true
|
||||
<img src="/docs/img/icons/menu/reference-csharp.svg" className="w-8 h-8 rounded" />
|
||||
<div className="flex flex-col gap-2">
|
||||
<h1 className="text-3xl text-scale-1200 m-0">C# Client Library</h1>
|
||||
<h2 className="text-base font-mono text-scale-1100">supabase-csharp</h2>
|
||||
<h2 className="text-base font-mono text-scale-1100">@supabase-community/supabase-csharp</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="max-w-xl">
|
||||
This reference documents every object and method available in Supabase's C#
|
||||
library, [supabase-csharp](https://www.nuget.org/packages/supabase-csharp). You can
|
||||
use supabase-csharp to interact with your Postgres database, listen to database changes, invoke
|
||||
Deno Edge Functions, build login and user management functionality, and manage large files.
|
||||
This reference documents every object and method available in Supabase's C# library,
|
||||
[supabase-csharp](https://www.nuget.org/packages/supabase-csharp). You can use supabase-csharp to
|
||||
interact with your Postgres database, listen to database changes, invoke Deno Edge Functions,
|
||||
build login and user management functionality, and manage large files.
|
||||
</div>
|
||||
|
||||
<div className="max-w-xl bg-slate-300 px-4 py-2 rounded-md">
|
||||
<p>The C# client library is created and maintained by the Supabase community, and is not an official library. Please be tolerant of areas where the library is still being developed, and — as with all the libraries — feel free to contribute wherever you find issues.</p>
|
||||
|
||||
<p>
|
||||
Huge thanks to official maintainer, [Joseph Schultz](https://github.com/acupofjose), and to [Ben
|
||||
Randall](https://github.com/veleek) and [Rhuan Barros](https://github.com/rhuanbarros) for their
|
||||
help.
|
||||
</p>
|
||||
|
||||
</div>
|
||||
@@ -24,7 +24,7 @@ Let's start building the Svelte app from scratch.
|
||||
### Initialize a Svelte app
|
||||
|
||||
We can use the [SvelteKit Skeleton Project](https://kit.svelte.dev/docs) to initialize
|
||||
an app called `supabase-sveltekit` (for this tutorial you do not need TypeScript, ESLint, Prettier, or Playwright):
|
||||
an app called `supabase-sveltekit` (for this tutorial we will be using TypeScript):
|
||||
|
||||
```bash
|
||||
npm init svelte@next supabase-sveltekit
|
||||
|
||||
@@ -14,6 +14,15 @@ Custom Domains are currently in beta, and are being slowly made available to pro
|
||||
|
||||
Custom domains allow you to present a branded experience to your users. Custom domains are available as a [add-on for projects on a paid tier](https://app.supabase.com/project/_/settings/billing/update). Setting up a custom domain requires [Owner or Admin permissions](/docs/guides/platform/access-control#manage-team-members) for the project. Currently, you must use a subdomain (e.g., `api.example.com`, rather than `example.com`) for the purposes of this guide.
|
||||
|
||||
<div className="video-container">
|
||||
<iframe
|
||||
src="https://www.youtube-nocookie.com/embed/6rcGnW_Mh-0"
|
||||
frameBorder="1"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowFullScreen
|
||||
></iframe>
|
||||
</div>
|
||||
|
||||
## Configure a Custom Domain using the Supabase Dashboard
|
||||
|
||||
Follow the **Custom Domains** steps in the [General Settings](https://app.supabase.com/project/_/settings/general) page in the Dashboard to set up a custom domain for your project.
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
Generated
+45
@@ -20305,6 +20305,23 @@
|
||||
"integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/ip-address": {
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-8.1.0.tgz",
|
||||
"integrity": "sha512-Wz91gZKpNKoXtqvY8ScarKYwhXoK4r/b5QuT+uywe/azv0/nUCo7Bh0IRRI7F9DHR06kJNWtzMGLIbXavngbKA==",
|
||||
"dependencies": {
|
||||
"jsbn": "1.1.0",
|
||||
"sprintf-js": "1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/ip-address/node_modules/sprintf-js": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz",
|
||||
"integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug=="
|
||||
},
|
||||
"node_modules/ipaddr.js": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||
@@ -22939,6 +22956,11 @@
|
||||
"node": ">=4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jsbn": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz",
|
||||
"integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A=="
|
||||
},
|
||||
"node_modules/jsdom": {
|
||||
"version": "16.7.0",
|
||||
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.7.0.tgz",
|
||||
@@ -38330,6 +38352,7 @@
|
||||
"generate-password": "^1.7.0",
|
||||
"html-to-image": "^1.10.8",
|
||||
"immutability-helper": "^3.1.1",
|
||||
"ip-address": "^8.1.0",
|
||||
"json-logic-js": "^2.0.2",
|
||||
"lodash": "^4.17.21",
|
||||
"lottie-react": "^2.2.1",
|
||||
@@ -54251,6 +54274,22 @@
|
||||
"integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==",
|
||||
"dev": true
|
||||
},
|
||||
"ip-address": {
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-8.1.0.tgz",
|
||||
"integrity": "sha512-Wz91gZKpNKoXtqvY8ScarKYwhXoK4r/b5QuT+uywe/azv0/nUCo7Bh0IRRI7F9DHR06kJNWtzMGLIbXavngbKA==",
|
||||
"requires": {
|
||||
"jsbn": "1.1.0",
|
||||
"sprintf-js": "1.1.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"sprintf-js": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz",
|
||||
"integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"ipaddr.js": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||
@@ -56342,6 +56381,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"jsbn": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz",
|
||||
"integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A=="
|
||||
},
|
||||
"jsdom": {
|
||||
"version": "16.7.0",
|
||||
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.7.0.tgz",
|
||||
@@ -64863,6 +64907,7 @@
|
||||
"generate-password": "^1.7.0",
|
||||
"html-to-image": "^1.10.8",
|
||||
"immutability-helper": "^3.1.1",
|
||||
"ip-address": "^8.1.0",
|
||||
"jest": "^27.4.0",
|
||||
"jest-canvas-mock": "^2.3.1",
|
||||
"json-logic-js": "^2.0.2",
|
||||
|
||||
@@ -97,7 +97,7 @@ function Input({
|
||||
// }, [errors, touched])
|
||||
|
||||
function onCopy(value: any) {
|
||||
navigator.clipboard.writeText(value).then(
|
||||
navigator.clipboard.writeText(value)?.then(
|
||||
function () {
|
||||
/* clipboard successfully set */
|
||||
setCopyLabel('Copied')
|
||||
|
||||
@@ -28,7 +28,8 @@
|
||||
"reference_javascript_v1",
|
||||
"reference_dart_v0",
|
||||
"reference_dart_v1",
|
||||
"reference_python_v2"
|
||||
"reference_python_v2",
|
||||
"reference_csharp_v0"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -36,7 +37,12 @@
|
||||
"id": "upgrade-guide",
|
||||
"slug": "upgrade-guide",
|
||||
"type": "markdown",
|
||||
"excludes": ["reference_javascript_v2", "reference_dart_v1", "reference_python_v2"]
|
||||
"excludes": [
|
||||
"reference_javascript_v2",
|
||||
"reference_dart_v1",
|
||||
"reference_python_v2",
|
||||
"reference_csharp_v0"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Database",
|
||||
@@ -842,7 +848,6 @@
|
||||
{
|
||||
"title": "Misc",
|
||||
"excludes": ["reference_dart_v1", "reference_dart_v0"],
|
||||
|
||||
"items": [
|
||||
{
|
||||
"title": "Release Notes",
|
||||
|
||||
@@ -15,7 +15,7 @@ import PublicationsTables from './Publications/PublicationsTables'
|
||||
|
||||
import BackupsList from './Backups/BackupsList'
|
||||
|
||||
import ConnectionPooling from './Pooling/ConnectionPooling'
|
||||
import ConnectionPooling from '../Settings/Database/ConnectionPooling'
|
||||
|
||||
import CreateTrigger from './Triggers/CreateTrigger'
|
||||
import DeleteTrigger from './Triggers/DeleteTrigger'
|
||||
|
||||
File renamed without changes.
+244
@@ -0,0 +1,244 @@
|
||||
import { FC, useState } from 'react'
|
||||
import { Address4 } from 'ip-address'
|
||||
import { Button, Form, IconHelpCircle, Input, Modal } from 'ui'
|
||||
import * as Tooltip from '@radix-ui/react-tooltip'
|
||||
|
||||
import { useStore, useParams } from 'hooks'
|
||||
import InformationBox from 'components/ui/InformationBox'
|
||||
import { checkIfPrivate, getAddressEndRange } from './NetworkRestrictions.utils'
|
||||
import { useNetworkRestrictionsApplyMutation } from 'data/network-restrictions/network-retrictions-apply-mutation'
|
||||
import { get } from 'lib/common/fetch'
|
||||
|
||||
interface Props {
|
||||
restrictedIps: string[]
|
||||
visible: boolean
|
||||
hasOverachingRestriction: boolean
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
const AddRestrictionModal: FC<Props> = ({
|
||||
restrictedIps,
|
||||
visible,
|
||||
hasOverachingRestriction,
|
||||
onClose,
|
||||
}) => {
|
||||
const formId = 'add-restriction-form'
|
||||
const { ui } = useStore()
|
||||
const { ref } = useParams()
|
||||
const { mutateAsync: applyNetworkRestrictions } = useNetworkRestrictionsApplyMutation()
|
||||
|
||||
const validate = (values: any) => {
|
||||
const errors: any = {}
|
||||
const { ipAddress, cidrBlockSize } = values
|
||||
|
||||
// Validate CIDR block size
|
||||
const isOutOfCidrSizeRange = cidrBlockSize < 0 || cidrBlockSize > 32
|
||||
if (isOutOfCidrSizeRange) errors.cidrBlockSize = 'Size has to be between 0 to 32'
|
||||
|
||||
// Validate IP address
|
||||
const isValid = Address4.isValid(ipAddress)
|
||||
if (!isValid) {
|
||||
errors.ipAddress = 'Please enter a valid IP address'
|
||||
return errors
|
||||
}
|
||||
|
||||
const isPrivate = checkIfPrivate(ipAddress)
|
||||
if (isPrivate) errors.ipAddress = 'Private IP addresses are not supported'
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
const onSubmit = async (values: any, { setSubmitting }: any) => {
|
||||
if (!ref) return console.error('Project ref is required')
|
||||
|
||||
setSubmitting(true)
|
||||
const cidr = `${values.ipAddress}/${values.cidrBlockSize}`
|
||||
const alreadyExists = restrictedIps.includes(cidr)
|
||||
if (alreadyExists) {
|
||||
return ui.setNotification({
|
||||
category: 'info',
|
||||
message: `The address ${cidr} is already restricted`,
|
||||
})
|
||||
}
|
||||
|
||||
const dbAllowedCidrs = hasOverachingRestriction ? [cidr] : [...restrictedIps, cidr]
|
||||
|
||||
try {
|
||||
await applyNetworkRestrictions({ projectRef: ref, dbAllowedCidrs })
|
||||
onClose()
|
||||
} catch (error: any) {
|
||||
ui.setNotification({
|
||||
category: 'error',
|
||||
message: `Failed to add restriction: ${error.message}`,
|
||||
})
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
closable
|
||||
hideFooter
|
||||
size="medium"
|
||||
visible={visible}
|
||||
onCancel={onClose}
|
||||
header="Add a new restriction"
|
||||
>
|
||||
<Form
|
||||
validateOnBlur
|
||||
id={formId}
|
||||
className="!border-t-0"
|
||||
initialValues={{ ipAddress: '', cidrBlockSize: undefined }}
|
||||
validate={validate}
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
{({ isSubmitting, values, resetForm }: any) => {
|
||||
const isPrivate = Address4.isValid(values.ipAddress)
|
||||
? checkIfPrivate(values.ipAddress)
|
||||
: false
|
||||
const isValidBlockSize =
|
||||
values.cidrBlockSize !== '' && values.cidrBlockSize >= 0 && values.cidrBlockSize <= 32
|
||||
const availableAddresses = Math.pow(2, 32 - (values?.cidrBlockSize ?? 0))
|
||||
const addressRange = getAddressEndRange(`${values.ipAddress}/${values.cidrBlockSize}`)
|
||||
|
||||
const isValidCIDR = isValidBlockSize && !isPrivate && addressRange !== undefined
|
||||
|
||||
const [isFetchingAddress, setIsFetchingAddress] = useState(false)
|
||||
|
||||
const getClientIpAddress = async () => {
|
||||
setIsFetchingAddress(true)
|
||||
const res = await fetch('http://www.geoplugin.net/json.gp', { method: 'GET' })
|
||||
const { geoplugin_request } = await res.json()
|
||||
|
||||
if (geoplugin_request) {
|
||||
const updatedValues = { ...values, ipAddress: geoplugin_request, cidrBlockSize: 32 }
|
||||
resetForm({ initialValues: updatedValues, values: updatedValues })
|
||||
} else {
|
||||
ui.setNotification({
|
||||
category: 'error',
|
||||
duration: 4000,
|
||||
message: 'Failed to retrieve client IP address, please enter address manually',
|
||||
})
|
||||
}
|
||||
setIsFetchingAddress(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal.Content>
|
||||
<div className="py-6 space-y-4">
|
||||
<p className="text-sm text-scale-1100">
|
||||
This will add an IP address range to a list of allowed ranges that can access
|
||||
your database. Only IPv4 addresses are supported at the moment.
|
||||
</p>
|
||||
<InformationBox
|
||||
title="Note: Restrictions only apply to your database and PgBouncer"
|
||||
description="They do not currently apply to Supabase services such as PostgREST, Storage, or Authentication"
|
||||
/>
|
||||
<div className="flex space-x-4">
|
||||
<div className="w-[55%]">
|
||||
<Input
|
||||
label="IPv4 address"
|
||||
id="ipAddress"
|
||||
name="ipAddress"
|
||||
placeholder="0.0.0.0"
|
||||
className=""
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-grow">
|
||||
<Input
|
||||
label={
|
||||
<div className="flex items-center space-x-2">
|
||||
<p>CIDR Block Size</p>
|
||||
<Tooltip.Root delayDuration={0}>
|
||||
<Tooltip.Trigger>
|
||||
<IconHelpCircle size="tiny" strokeWidth={2} />
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content side="bottom">
|
||||
<Tooltip.Arrow className="radix-tooltip-arrow" />
|
||||
<div
|
||||
className={[
|
||||
'rounded bg-scale-100 py-1 px-2 leading-none shadow',
|
||||
'border border-scale-200 w-[300px]',
|
||||
].join(' ')}
|
||||
>
|
||||
<span className="text-xs text-scale-1200">
|
||||
Classless inter-domain routing (CIDR) notation is the notation
|
||||
used to identify networks and hosts in the networks. The block
|
||||
size tells us how many bits we need to take for the network
|
||||
prefix, and is a value between 0 to 32.
|
||||
</span>
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</div>
|
||||
}
|
||||
id="cidrBlockSize"
|
||||
name="cidrBlockSize"
|
||||
type="number"
|
||||
placeholder="0"
|
||||
min={0}
|
||||
max={32}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
type="default"
|
||||
loading={isFetchingAddress}
|
||||
disabled={isFetchingAddress}
|
||||
onClick={() => getClientIpAddress()}
|
||||
>
|
||||
Use my IP address
|
||||
</Button>
|
||||
</div>
|
||||
</Modal.Content>
|
||||
<Modal.Separator />
|
||||
{isValidCIDR ? (
|
||||
<Modal.Content>
|
||||
<div className="space-y-1 pt-2 pb-4">
|
||||
<p className="text-sm">
|
||||
The address range{' '}
|
||||
<code className="text-xs">
|
||||
{values.ipAddress}/{values.cidrBlockSize}
|
||||
</code>{' '}
|
||||
will be restricted
|
||||
</p>
|
||||
<p className="text-sm text-scale-1000">
|
||||
Selected address space: <code className="text-xs">{addressRange.start}</code>{' '}
|
||||
to <code className="text-xs">{addressRange.end}</code>{' '}
|
||||
</p>
|
||||
<p className="text-sm text-scale-1000">
|
||||
Number of addresses: {availableAddresses}
|
||||
</p>
|
||||
</div>
|
||||
</Modal.Content>
|
||||
) : (
|
||||
<Modal.Content>
|
||||
<div className="pt-2 pb-4">
|
||||
<div className="h-[68px] flex items-center">
|
||||
<p className="text-sm text-scale-1000">
|
||||
A summary of your restriction will be shown here after entering a valid IP
|
||||
address and CIDR block size
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Modal.Content>
|
||||
)}
|
||||
<div className="flex items-center justify-end px-6 py-4 border-t space-x-2">
|
||||
<Button type="default" disabled={isSubmitting} onClick={() => onClose()}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button htmlType="submit" loading={isSubmitting} disabled={isSubmitting}>
|
||||
Save restriction
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}}
|
||||
</Form>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default AddRestrictionModal
|
||||
@@ -0,0 +1,63 @@
|
||||
import { FC, useState } from 'react'
|
||||
import { Modal, Button } from 'ui'
|
||||
|
||||
import { useStore, useParams } from 'hooks'
|
||||
import { useNetworkRestrictionsApplyMutation } from 'data/network-restrictions/network-retrictions-apply-mutation'
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
const AllowAllModal: FC<Props> = ({ visible, onClose }) => {
|
||||
const { ui } = useStore()
|
||||
const { ref } = useParams()
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const { mutateAsync: applyNetworkRestrictions } = useNetworkRestrictionsApplyMutation()
|
||||
|
||||
const onSubmit = async () => {
|
||||
if (!ref) return console.error('Project ref is required')
|
||||
|
||||
setIsSubmitting(true)
|
||||
try {
|
||||
await applyNetworkRestrictions({ projectRef: ref, dbAllowedCidrs: ['0.0.0.0/0'] })
|
||||
onClose()
|
||||
} catch (error: any) {
|
||||
ui.setNotification({
|
||||
category: 'error',
|
||||
message: `Failed to update restriction: ${error.message}`,
|
||||
})
|
||||
} finally {
|
||||
setIsSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
closable
|
||||
hideFooter
|
||||
size="small"
|
||||
visible={visible}
|
||||
onCancel={onClose}
|
||||
header="Allow access from all IP addresses"
|
||||
>
|
||||
<Modal.Content>
|
||||
<div className="py-6 space-y-4">
|
||||
<p className="text-sm text-scale-1100">
|
||||
This will allow any IP address to access your project's database. Are you sure?
|
||||
</p>
|
||||
</div>
|
||||
</Modal.Content>
|
||||
<div className="flex items-center justify-end px-6 py-4 border-t space-x-2">
|
||||
<Button type="default" disabled={isSubmitting} onClick={() => onClose()}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button loading={isSubmitting} disabled={isSubmitting} onClick={() => onSubmit()}>
|
||||
Confirm
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default AllowAllModal
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
import { FC, useState } from 'react'
|
||||
import { Modal, Button } from 'ui'
|
||||
|
||||
import { useStore, useParams } from 'hooks'
|
||||
import { useNetworkRestrictionsApplyMutation } from 'data/network-restrictions/network-retrictions-apply-mutation'
|
||||
import InformationBox from 'components/ui/InformationBox'
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
const DisallowAllModal: FC<Props> = ({ visible, onClose }) => {
|
||||
const { ui } = useStore()
|
||||
const { ref } = useParams()
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const { mutateAsync: applyNetworkRestrictions } = useNetworkRestrictionsApplyMutation()
|
||||
|
||||
const onSubmit = async () => {
|
||||
if (!ref) return console.error('Project ref is required')
|
||||
|
||||
setIsSubmitting(true)
|
||||
try {
|
||||
await applyNetworkRestrictions({ projectRef: ref, dbAllowedCidrs: ['127.0.0.1/32'] })
|
||||
onClose()
|
||||
} catch (error: any) {
|
||||
ui.setNotification({
|
||||
category: 'error',
|
||||
message: `Failed to update restriction: ${error.message}`,
|
||||
})
|
||||
} finally {
|
||||
setIsSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
closable
|
||||
hideFooter
|
||||
size="medium"
|
||||
visible={visible}
|
||||
onCancel={onClose}
|
||||
header="Restrict access from all IP addresses"
|
||||
>
|
||||
<Modal.Content>
|
||||
<div className="py-6 space-y-4">
|
||||
<p className="text-sm text-scale-1100">
|
||||
This will prevent any external IP addresses from accessing your project's database. Are
|
||||
you sure?
|
||||
</p>
|
||||
<InformationBox
|
||||
defaultVisibility
|
||||
hideCollapse
|
||||
title="Note: Restrictions only apply to your database and PgBouncer"
|
||||
description="They do not currently apply to APIs offered over HTTPS, such as PostgREST, Storage, or Authentication"
|
||||
/>
|
||||
</div>
|
||||
</Modal.Content>
|
||||
<div className="flex items-center justify-end px-6 py-4 border-t space-x-2">
|
||||
<Button type="default" disabled={isSubmitting} onClick={() => onClose()}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button loading={isSubmitting} disabled={isSubmitting} onClick={() => onSubmit()}>
|
||||
Confirm
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default DisallowAllModal
|
||||
+195
@@ -0,0 +1,195 @@
|
||||
import Link from 'next/link'
|
||||
import { useState } from 'react'
|
||||
import { Button, IconAlertCircle, IconExternalLink, IconGlobe, IconLock } from 'ui'
|
||||
|
||||
import { useParams } from 'hooks'
|
||||
import Panel from 'components/ui/Panel'
|
||||
import { FormPanel, FormHeader } from 'components/ui/Forms'
|
||||
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
|
||||
import AddRestrictionModal from './AddRestrictionModal'
|
||||
import RemoveRestrictionModal from './RemoveRestrictionModal'
|
||||
import DisallowAllModal from './DisallowAllModal'
|
||||
import AllowAllModal from './AllowAllModal'
|
||||
import { useNetworkRestrictionsQuery } from 'data/network-restrictions/network-restrictions-query'
|
||||
|
||||
const NetworkRestrictions = ({}) => {
|
||||
const { ref } = useParams()
|
||||
|
||||
const [isAddingAddress, setIsAddingAddress] = useState(false)
|
||||
const [isAllowingAll, setIsAllowingAll] = useState(false)
|
||||
const [isDisallowingAll, setIsDisallowingAll] = useState(false)
|
||||
const [selectedRestrictionToRemove, setSelectedRestrictionToRemove] = useState<string>()
|
||||
const { data, isLoading } = useNetworkRestrictionsQuery({ projectRef: ref })
|
||||
|
||||
const hasAccessToRestrictions = data?.entitlement === 'allowed'
|
||||
const restrictedIps = data?.config.dbAllowedCidrs ?? []
|
||||
const restrictionStatus = data?.status ?? ''
|
||||
|
||||
const hasApplyError = restrictedIps.length === 0 && restrictionStatus === 'stored'
|
||||
const isUninitialized = restrictedIps.length === 0 && restrictionStatus.length === 0
|
||||
const isAllowedAll = restrictedIps.includes('0.0.0.0/0')
|
||||
const isDisallowedAll = restrictedIps.length === 1 && restrictedIps[0] === '127.0.0.1/32'
|
||||
|
||||
if (!hasAccessToRestrictions) return <></>
|
||||
|
||||
return (
|
||||
<>
|
||||
<section>
|
||||
<div className="flex items-center justify-between">
|
||||
<FormHeader
|
||||
title="Network Restrictions"
|
||||
description="Allow specific IP ranges to have access to your database."
|
||||
/>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Link href="https://supabase.com/docs/guides/platform/network-restrictions">
|
||||
<a target="_blank">
|
||||
<Button type="default" icon={<IconExternalLink />}>
|
||||
Documentation
|
||||
</Button>
|
||||
</a>
|
||||
</Link>
|
||||
<Button onClick={() => setIsAddingAddress(true)}>Add restriction</Button>
|
||||
</div>
|
||||
</div>
|
||||
{isLoading ? (
|
||||
<Panel>
|
||||
<Panel.Content>
|
||||
<div className="space-y-2">
|
||||
<ShimmeringLoader />
|
||||
<ShimmeringLoader className="w-[70%]" />
|
||||
<ShimmeringLoader className="w-[50%]" />
|
||||
</div>
|
||||
</Panel.Content>
|
||||
</Panel>
|
||||
) : hasApplyError ? (
|
||||
<Panel>
|
||||
<Panel.Content>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<IconAlertCircle strokeWidth={1.5} className="text-scale-1000" />
|
||||
<p className="text-sm">Your network restrictions were not applied correctly</p>
|
||||
</div>
|
||||
<p className="text-sm text-scale-1000">
|
||||
Please try to add your network restrictions again
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button type="default" onClick={() => setIsAllowingAll(true)}>
|
||||
Allow all access
|
||||
</Button>
|
||||
<Button type="default" onClick={() => setIsDisallowingAll(true)}>
|
||||
Restrict all access
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Panel.Content>
|
||||
</Panel>
|
||||
) : (
|
||||
<FormPanel>
|
||||
{isUninitialized || isAllowedAll ? (
|
||||
<div className="px-8 py-8 flex items-center justify-between">
|
||||
<div className="flex items-start space-x-4">
|
||||
<div className="space-y-1">
|
||||
<p className="text-scale-1100 text-sm">
|
||||
Your database can be accessed by all IP addresses
|
||||
</p>
|
||||
<p className="text-scale-1000 text-sm">
|
||||
You may start limiting access to your database by adding a network
|
||||
restriction.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Button type="default" onClick={() => setIsDisallowingAll(true)}>
|
||||
Restrict all access
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : isDisallowedAll ? (
|
||||
<div className="px-8 py-8 flex items-center justify-between">
|
||||
<div className="flex items-start space-x-4">
|
||||
<IconLock className="text-scale-1100" strokeWidth={1.5} />
|
||||
<div className="space-y-1">
|
||||
<p className="text-scale-1100 text-sm">
|
||||
Your database <span className="text-amber-900 opacity-80">cannot</span> be
|
||||
accessed externally
|
||||
</p>
|
||||
<p className="text-scale-1000 text-sm">
|
||||
All external IP addresses have been disallowed from accessing your project's
|
||||
database.
|
||||
</p>
|
||||
<p className="text-scale-1000 text-sm">
|
||||
Note: Restrictions only apply to your database, and not to Supabase services
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Button type="default" onClick={() => setIsAllowingAll(true)}>
|
||||
Allow all access
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="divide-y">
|
||||
<div className="px-8 py-3 flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-scale-1100 text-sm">
|
||||
Only the following IP addresses have access to your database
|
||||
</p>
|
||||
<p className="text-scale-1000 text-sm">
|
||||
You may remove all of them to allow all IP addresses to have access to your
|
||||
database
|
||||
</p>
|
||||
<p className="text-scale-1000 text-sm">
|
||||
Note: Restrictions only apply to your database, and not to Supabase services
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button type="default" onClick={() => setIsAllowingAll(true)}>
|
||||
Allow all access
|
||||
</Button>
|
||||
<Button type="default" onClick={() => setIsDisallowingAll(true)}>
|
||||
Restrict all access
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{restrictedIps.map((ip) => {
|
||||
return (
|
||||
<div key={ip} className="px-8 py-4 flex items-center justify-between">
|
||||
<div className="flex items-center space-x-5">
|
||||
<IconGlobe size={16} className="text-scale-1000" />
|
||||
<p className="text-sm font-mono">{ip}</p>
|
||||
</div>
|
||||
<Button type="default" onClick={() => setSelectedRestrictionToRemove(ip)}>
|
||||
Remove
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</FormPanel>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<AllowAllModal visible={isAllowingAll} onClose={() => setIsAllowingAll(false)} />
|
||||
<DisallowAllModal visible={isDisallowingAll} onClose={() => setIsDisallowingAll(false)} />
|
||||
|
||||
<AddRestrictionModal
|
||||
visible={isAddingAddress}
|
||||
restrictedIps={restrictedIps}
|
||||
hasOverachingRestriction={isAllowedAll || isDisallowedAll}
|
||||
onClose={() => setIsAddingAddress(false)}
|
||||
/>
|
||||
<RemoveRestrictionModal
|
||||
visible={selectedRestrictionToRemove !== undefined}
|
||||
restrictedIps={restrictedIps}
|
||||
selectedRestriction={selectedRestrictionToRemove}
|
||||
onClose={() => setSelectedRestrictionToRemove(undefined)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default NetworkRestrictions
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
import { Address4 } from 'ip-address'
|
||||
|
||||
const privateIPRanges = [
|
||||
new Address4('10.0.0.0/8'),
|
||||
new Address4('172.16.0.0/12'),
|
||||
new Address4('192.168.0.0/16'),
|
||||
]
|
||||
|
||||
export const checkIfPrivate = (cidr: string) => {
|
||||
const address = new Address4(cidr)
|
||||
const res = privateIPRanges.map((range) => address.isInSubnet(range))
|
||||
return res.includes(true)
|
||||
}
|
||||
|
||||
export const getAddressEndRange = (cidr: string) => {
|
||||
try {
|
||||
const address = new Address4(cidr)
|
||||
return { start: address.startAddress().address, end: address.endAddress().address }
|
||||
} catch (error: any) {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
import { FC, useState } from 'react'
|
||||
import { Alert, Button, Form, Input, Modal } from 'ui'
|
||||
|
||||
import { useStore, useParams } from 'hooks'
|
||||
import { useNetworkRestrictionsApplyMutation } from 'data/network-restrictions/network-retrictions-apply-mutation'
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
restrictedIps: string[]
|
||||
selectedRestriction?: string
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
const RemoveRestrictionModal: FC<Props> = ({
|
||||
visible,
|
||||
restrictedIps,
|
||||
selectedRestriction,
|
||||
onClose,
|
||||
}) => {
|
||||
const { ui } = useStore()
|
||||
const { ref } = useParams()
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const { mutateAsync: applyNetworkRestrictions } = useNetworkRestrictionsApplyMutation()
|
||||
|
||||
const isRemovingOnlyRestriction =
|
||||
restrictedIps.length === 1 && restrictedIps[0] === selectedRestriction
|
||||
|
||||
const onSubmit = async () => {
|
||||
if (!ref) return console.error('Project ref is required')
|
||||
|
||||
setIsSubmitting(true)
|
||||
const dbAllowedCidrs = restrictedIps.filter((ip) => ip !== selectedRestriction)
|
||||
|
||||
try {
|
||||
if (dbAllowedCidrs.length === 0) {
|
||||
await applyNetworkRestrictions({ projectRef: ref, dbAllowedCidrs: ['0.0.0.0/0'] })
|
||||
} else {
|
||||
await applyNetworkRestrictions({ projectRef: ref, dbAllowedCidrs })
|
||||
}
|
||||
onClose()
|
||||
} catch (error: any) {
|
||||
ui.setNotification({
|
||||
category: 'error',
|
||||
message: `Failed to remove restriction: ${error.message}`,
|
||||
})
|
||||
} finally {
|
||||
setIsSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
closable
|
||||
hideFooter
|
||||
size="medium"
|
||||
visible={visible}
|
||||
onCancel={onClose}
|
||||
header="Confirm to remove restriction"
|
||||
>
|
||||
<Modal.Content>
|
||||
<div className="py-6 space-y-4">
|
||||
<p className="text-sm text-scale-1100">
|
||||
The IPv4 address <code className="text-xs">{selectedRestriction}</code> will be removed
|
||||
from your list of network restrictions
|
||||
{isRemovingOnlyRestriction
|
||||
? '.'
|
||||
: ", and no longer have access to your project's database."}
|
||||
</p>
|
||||
{isRemovingOnlyRestriction && (
|
||||
<Alert withIcon variant="warning" title="Database access will no longer be restricted">
|
||||
Removing all network restrictions will default to your database being accessible from
|
||||
all IP addresses.
|
||||
</Alert>
|
||||
)}
|
||||
</div>
|
||||
</Modal.Content>
|
||||
<div className="flex items-center justify-end px-6 py-4 border-t space-x-2">
|
||||
<Button type="default" disabled={isSubmitting} onClick={() => onClose()}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button loading={isSubmitting} disabled={isSubmitting} onClick={() => onSubmit()}>
|
||||
Remove restriction
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default RemoveRestrictionModal
|
||||
@@ -0,0 +1,4 @@
|
||||
import ConnectionPooling from './ConnectionPooling'
|
||||
import NetworkRestrictions from './NetworkRestrictions/NetworkRestrictions'
|
||||
|
||||
export { ConnectionPooling, NetworkRestrictions }
|
||||
@@ -1,6 +1,7 @@
|
||||
import General from './General'
|
||||
import Infrastructure from './Infrastructure/Infrastructure'
|
||||
import CustomDomainConfig from './CustomDomainConfig/CustomDomainConfig'
|
||||
import DeleteProjectPanel from './DeleteProjectPanel/DeleteProjectPanel'
|
||||
import DeleteProjectButton from './DeleteProjectPanel/DeleteProjectButton'
|
||||
|
||||
export { General, Infrastructure, DeleteProjectPanel, DeleteProjectButton }
|
||||
export { General, Infrastructure, CustomDomainConfig, DeleteProjectPanel, DeleteProjectButton }
|
||||
@@ -0,0 +1,4 @@
|
||||
export const networkRestrictionKeys = {
|
||||
list: (projectRef: string | undefined) =>
|
||||
['projects', projectRef, 'network-restrictions'] as const,
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import { useCallback } from 'react'
|
||||
import { useQuery, useQueryClient, UseQueryOptions } from '@tanstack/react-query'
|
||||
|
||||
import { networkRestrictionKeys } from './keys'
|
||||
import { get } from 'lib/common/fetch'
|
||||
import { API_ADMIN_URL } from 'lib/constants'
|
||||
|
||||
export type NetworkRestrictionsVariables = { projectRef?: string }
|
||||
|
||||
export type NetworkRestrictionsResponse = {
|
||||
entitlement: 'disallowed' | 'allowed'
|
||||
status: '' | 'stored' | 'applied'
|
||||
config: { dbAllowedCidrs: string[] }
|
||||
old_config?: { dbAllowedCidrs: string[] }
|
||||
error?: any
|
||||
}
|
||||
|
||||
export async function getNetworkRestrictions(
|
||||
{ projectRef }: NetworkRestrictionsVariables,
|
||||
signal?: AbortSignal
|
||||
) {
|
||||
if (!projectRef) throw new Error('projectRef is required')
|
||||
|
||||
const response = (await get(`${API_ADMIN_URL}/projects/${projectRef}/network-restrictions`, {
|
||||
signal,
|
||||
})) as NetworkRestrictionsResponse
|
||||
|
||||
// Not allowed error is a valid response to denote if a project
|
||||
// has access to the network restrictions UI, so we'll handle it here
|
||||
if (response.error) {
|
||||
const isNotAllowedError =
|
||||
(response.error as any)?.code === 400 &&
|
||||
(response.error as any)?.message?.includes('not allowed to set up network restrictions')
|
||||
|
||||
if (isNotAllowedError) {
|
||||
return {
|
||||
entitlement: 'disallowed',
|
||||
config: { dbAllowedCidrs: [] },
|
||||
status: '',
|
||||
} as NetworkRestrictionsResponse
|
||||
} else {
|
||||
throw response.error
|
||||
}
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
export type NetworkRestrictionsData = Awaited<ReturnType<typeof getNetworkRestrictions>>
|
||||
export type NetworkRestrictionsError = unknown
|
||||
|
||||
export const useNetworkRestrictionsQuery = <TData = NetworkRestrictionsData>(
|
||||
{ projectRef }: NetworkRestrictionsVariables,
|
||||
{
|
||||
enabled = true,
|
||||
...options
|
||||
}: UseQueryOptions<NetworkRestrictionsData, NetworkRestrictionsError, TData> = {}
|
||||
) =>
|
||||
useQuery<NetworkRestrictionsData, NetworkRestrictionsError, TData>(
|
||||
networkRestrictionKeys.list(projectRef),
|
||||
({ signal }) => getNetworkRestrictions({ projectRef }, signal),
|
||||
{ enabled: enabled && typeof projectRef !== 'undefined', ...options }
|
||||
)
|
||||
|
||||
export const useNetworkRestrictionsPrefetch = ({ projectRef }: NetworkRestrictionsVariables) => {
|
||||
const client = useQueryClient()
|
||||
|
||||
return useCallback(() => {
|
||||
if (projectRef) {
|
||||
client.prefetchQuery(networkRestrictionKeys.list(projectRef), ({ signal }) =>
|
||||
getNetworkRestrictions({ projectRef }, signal)
|
||||
)
|
||||
}
|
||||
}, [projectRef])
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
|
||||
import { post } from 'lib/common/fetch'
|
||||
import { API_ADMIN_URL } from 'lib/constants'
|
||||
import { networkRestrictionKeys } from './keys'
|
||||
|
||||
export type NetworkRestrictionsApplyVariables = {
|
||||
projectRef: string
|
||||
dbAllowedCidrs: string[]
|
||||
}
|
||||
|
||||
export async function applyNetworkRestrictions({
|
||||
projectRef,
|
||||
dbAllowedCidrs,
|
||||
}: NetworkRestrictionsApplyVariables) {
|
||||
if (!projectRef) throw new Error('projectRef is required')
|
||||
|
||||
const response = await post(
|
||||
`${API_ADMIN_URL}/projects/${projectRef}/network-restrictions/apply`,
|
||||
{ dbAllowedCidrs }
|
||||
)
|
||||
if (response.error) throw response.error
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
type NetworkRestrictionsApplyData = Awaited<ReturnType<typeof applyNetworkRestrictions>>
|
||||
|
||||
export const useNetworkRestrictionsApplyMutation = ({
|
||||
onSuccess,
|
||||
...options
|
||||
}: Omit<
|
||||
UseMutationOptions<NetworkRestrictionsApplyData, unknown, NetworkRestrictionsApplyVariables>,
|
||||
'mutationFn'
|
||||
> = {}) => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation<NetworkRestrictionsApplyData, unknown, NetworkRestrictionsApplyVariables>(
|
||||
(vars) => applyNetworkRestrictions(vars),
|
||||
{
|
||||
async onSuccess(data, variables, context) {
|
||||
const { projectRef } = variables
|
||||
await queryClient.invalidateQueries(networkRestrictionKeys.list(projectRef))
|
||||
await onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -1485,7 +1485,7 @@ class StorageExplorerStore {
|
||||
.list(formattedPathToFolder, options)
|
||||
folderContents = folderContents.concat(data)
|
||||
options.offset += options.limit
|
||||
if (data.length < options.limit) {
|
||||
if ((data || []).length < options.limit) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
"generate-password": "^1.7.0",
|
||||
"html-to-image": "^1.10.8",
|
||||
"immutability-helper": "^3.1.1",
|
||||
"ip-address": "^8.1.0",
|
||||
"json-logic-js": "^2.0.2",
|
||||
"lodash": "^4.17.21",
|
||||
"lottie-react": "^2.2.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { toJS } from 'mobx'
|
||||
import * as Tooltip from '@radix-ui/react-tooltip'
|
||||
import { FC, useState, useRef, useEffect } from 'react'
|
||||
import { toJS } from 'mobx'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { debounce } from 'lodash'
|
||||
import generator from 'generate-password'
|
||||
@@ -8,7 +8,7 @@ import { Input, Button, IconDownload, Tabs, Modal } from 'ui'
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
|
||||
import { NextPageWithLayout } from 'types'
|
||||
import { checkPermissions, useParams, useStore } from 'hooks'
|
||||
import { checkPermissions, useFlag, useParams, useStore } from 'hooks'
|
||||
import { patch } from 'lib/common/fetch'
|
||||
import { pluckObjectFields, passwordStrength } from 'lib/helpers'
|
||||
import { API_URL, DEFAULT_MINIMUM_PASSWORD_STRENGTH } from 'lib/constants'
|
||||
@@ -17,16 +17,19 @@ import { useProjectSettingsQuery } from 'data/config/project-settings-query'
|
||||
import { SettingsLayout } from 'components/layouts'
|
||||
import PasswordStrengthBar from 'components/ui/PasswordStrengthBar'
|
||||
import Panel from 'components/ui/Panel'
|
||||
import ConnectionPooling from 'components/interfaces/Database/Pooling/ConnectionPooling'
|
||||
import { ConnectionPooling, NetworkRestrictions } from 'components/interfaces/Settings/Database'
|
||||
|
||||
const ProjectSettings: NextPageWithLayout = () => {
|
||||
const { ref: projectRef } = useParams()
|
||||
|
||||
const networkRestrictions = useFlag('networkRestrictions')
|
||||
|
||||
return (
|
||||
<div className="1xl:px-28 mx-auto flex flex-col gap-4 px-5 py-6 lg:px-16 xl:px-24 2xl:px-32">
|
||||
<div className="1xl:px-28 mx-auto flex flex-col gap-4 px-5 pt-6 pb-14 lg:px-16 xl:px-24 2xl:px-32">
|
||||
<div className="content h-full w-full overflow-y-auto">
|
||||
<GeneralSettings projectRef={projectRef} />
|
||||
<ConnectionPooling />
|
||||
{networkRestrictions && <NetworkRestrictions />}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
import { observer } from 'mobx-react-lite'
|
||||
|
||||
import { NextPageWithLayout } from 'types'
|
||||
import { SettingsLayout } from 'components/layouts'
|
||||
import { General, Infrastructure, DeleteProjectPanel } from 'components/interfaces/Settings/General'
|
||||
import CustomDomainConfig from 'components/interfaces/Settings/General/CustomDomainConfig/CustomDomainConfig'
|
||||
import {
|
||||
General,
|
||||
Infrastructure,
|
||||
CustomDomainConfig,
|
||||
DeleteProjectPanel,
|
||||
} from 'components/interfaces/Settings/General'
|
||||
|
||||
const ProjectSettings: NextPageWithLayout = () => {
|
||||
// [Joshen] Opting for larger gap instead of gap-8 as compared to other pages for better grouping of content
|
||||
return (
|
||||
<div className="1xl:px-28 mx-auto flex flex-col gap-8 px-5 py-6 lg:px-16 xl:px-24 2xl:px-32 ">
|
||||
<div className="1xl:px-28 mx-auto flex flex-col gap-10 px-5 py-6 lg:px-16 xl:px-24 2xl:px-32 ">
|
||||
<General />
|
||||
<Infrastructure />
|
||||
<CustomDomainConfig />
|
||||
|
||||
@@ -10,8 +10,7 @@ import Divider from 'components/ui/Divider'
|
||||
import { Dictionary } from 'components/grid'
|
||||
|
||||
import { useStore, withAuth } from 'hooks'
|
||||
import { API_URL, IS_PLATFORM } from 'lib/constants'
|
||||
import { PROJECT_ENDPOINT_PROTOCOL } from 'pages/api/constants'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { get } from 'lib/common/fetch'
|
||||
import {
|
||||
VERCEL_INTEGRATION_CONFIGS,
|
||||
@@ -370,8 +369,8 @@ const ProjectLinks: FC = observer(() => {
|
||||
|
||||
// Then create env for vercel project with supabase project
|
||||
const vercelEnvs = prepareVercelEvns(defaultVercelEnvs, {
|
||||
endpoint: `${projectDetails.apiService.protocol ?? 'https'}://${
|
||||
projectDetails.apiService.endpoint ?? '-'
|
||||
endpoint: `${projectDetails.autoApiService.protocol ?? 'https'}://${
|
||||
projectDetails.autoApiService.endpoint ?? '-'
|
||||
}`,
|
||||
anon_key: projectDetails.autoApiService.defaultApiKey,
|
||||
service_key: projectDetails.autoApiService.serviceApiKey,
|
||||
|
||||
@@ -38,7 +38,7 @@ Sentry.init({
|
||||
// the URL params instead of the store, but we cannot repro locally, staging nor on prod
|
||||
// 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]/settings) value is missing query values (slug)',
|
||||
'The provided `href` (/org/[slug]/general) value is missing query values (slug)',
|
||||
],
|
||||
beforeSend: (event) => filterConsoleErrors(event),
|
||||
})
|
||||
|
||||
Reference in new issue
Block a user