Merge branch 'master' of github.com:supabase/supabase

This commit is contained in:
Terry Sutton committed 2023-02-10 15:49:03 -03:30
commit 1cda2c3ce0
40 files changed
+1130 -672

No files matched your search

+16 -48
View File
@@ -1,53 +1,21 @@
import providers from '../data/authProviders'
import ButtonCard from './ButtonCard'
import { IconPanel } from 'ui'
import Link from 'next/link'
export default function AuthProviders({ type }: { type: string }) {
const filterProviders = providers.filter((item) => item.authType === type)
export default function AuthProviders() {
return (
<div className="grid md:grid-cols-12 gap-4">
{providers.map((x) => (
<div key={x.name} className="col-span-6">
<ButtonCard to={x.href} title={x.name}>
<div className="px-6 py-4">
<div className="flex justify-between">
<p className="mt-0">{x.name}</p>
<p className="mt-0">
{x.official ? (
<span className={`badge badge--official`}>Official</span>
) : (
<span className={`badge badge--unofficial`}>Unofficial</span>
)}
</p>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 5 }}>
<div
className="code-block"
style={{
width: '100%',
display: 'flex',
justifyContent: 'space-between',
fontSize: '0.7rem',
}}
>
<span>Platform:</span>
<span>{x.platform.toString()}</span>
</div>
<div
className="code-block"
style={{
width: '100%',
display: 'flex',
justifyContent: 'space-between',
fontSize: '0.7rem',
}}
>
<span>Self-Hosted:</span>
<span>{x.selfHosted.toString()}</span>
</div>
</div>
</div>
</ButtonCard>
</div>
))}
</div>
<>
<div className="grid grid-cols-12 gap-10 not-prose py-8">
{filterProviders.map((x) => (
<Link href={`${x.href}`} key={x.name} passHref>
<a className="col-span-6 lg:col-span-4 xl:col-span-3">
<IconPanel title={x.name} icon={x.logo} />
</a>
</Link>
))}
</div>
</>
)
}
+3 -3
View File
@@ -30,12 +30,12 @@ const questions = [
'How do I connect to my database?',
'How do I run migrations? ',
'How do I listen to changes in a table?',
'How do I setup authentication?',
'How do I set up authentication?',
]
function getEdgeFunctionUrl() {
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL.replace(/\/$/, '')
const isPlatform = supabaseUrl.match(/(supabase\.co)|(supabase\.in)/)
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL?.replace(/\/$/, '')
const isPlatform = supabaseUrl?.match(/(supabase\.co)|(supabase\.in)/)
if (isPlatform) {
const [schemeAndProjectId, domain, tld] = supabaseUrl.split('.')
+19 -20
View File
@@ -1,34 +1,33 @@
import { FC, useEffect, useState } from 'react'
import createConfigCatClient from 'configcat-js'
// import createConfigCatClient from 'configcat-js'
import FlagContext from './FlagContext'
const FlagProvider: FC = ({ children }) => {
const [store, setStore] = useState({})
const { Provider } = FlagContext
useEffect(() => {
getFlags()
}, [])
// useEffect(() => {
// getFlags()
// }, [])
const getFlags = async () => {
const setFlagValues = async () => {
const flagValues = await client.getAllValuesAsync()
const flagStore: any = {}
// const getFlags = async () => {
// const setFlagValues = async () => {
// const flagValues = await client.getAllValuesAsync()
// const flagStore: any = {}
flagValues.forEach((item: any) => {
flagStore[item.settingKey] = item.settingValue
})
setStore(flagStore)
}
// flagValues.forEach((item: any) => {
// flagStore[item.settingKey] = item.settingValue
// })
// setStore(flagStore)
// }
const client = createConfigCatClient(process.env.NEXT_PUBLIC_CONFIGCAT_SDK_KEY ?? '', {
configChanged: setFlagValues,
pollIntervalSeconds: 600,
})
// const client = createConfigCatClient(process.env.NEXT_PUBLIC_CONFIGCAT_SDK_KEY ?? '', {
// configChanged: setFlagValues,
// pollIntervalSeconds: 600,
// })
await setFlagValues()
}
// await setFlagValues()
// }
return <Provider value={store}>{children}</Provider>
}
@@ -658,6 +658,11 @@ export const resources = {
url: '/guides/resources/migrating-to-supabase/heroku',
items: [],
},
{
name: 'Render',
url: '/guides/resources/migrating-to-supabase/render',
items: [],
},
],
},
{
@@ -720,6 +725,7 @@ export const migrate = {
{ name: 'Firestore Data', url: '/guides/migrations/firestore-data', items: [] },
{ name: 'Firebase Storage', url: '/guides/migrations/firebase-storage', items: [] },
{ name: 'Heroku', url: '/guides/migrations/heroku', items: [] },
{ name: 'Render', url: '/guides/migrations/render', items: [] },
],
}
+38 -17
View File
@@ -1,172 +1,193 @@
const authProviders = [
{
name: 'Apple',
// logo: '/img/libraries/dart-icon.svg',
logo: '/docs/img/icons/apple-icon',
href: '/guides/auth/social-login/auth-apple',
official: true,
supporter: 'Supabase',
platform: true,
selfHosted: true,
authType: 'social',
},
{
name: 'Azure',
// logo: '/img/libraries/dart-icon.svg',
logo: '/docs/img/icons/microsoft-icon',
href: '/guides/auth/social-login/auth-azure',
official: false,
supporter: 'TBD',
platform: true,
selfHosted: true,
authType: 'social',
},
{
name: 'Bitbucket',
// logo: '/img/libraries/dart-icon.svg',
logo: '/docs/img/icons/bitbucket-icon',
href: '/guides/auth/social-login/auth-bitbucket',
official: true,
supporter: 'Supabase',
platform: true,
selfHosted: true,
authType: 'social',
},
{
name: 'Discord',
// logo: '/img/libraries/dart-icon.svg',
logo: '/docs/img/icons/discord-icon',
href: '/guides/auth/social-login/auth-discord',
official: true,
supporter: 'Supabase',
platform: true,
selfHosted: true,
authType: 'social',
},
{
name: 'Facebook',
// logo: '/img/libraries/dart-icon.svg',
logo: '/docs/img/icons/facebook-icon',
href: '/guides/auth/social-login/auth-facebook',
official: true,
supporter: 'Supabase',
platform: true,
selfHosted: true,
authType: 'social',
},
{
name: 'GitHub',
// logo: '/img/libraries/dart-icon.svg',
logo: '/docs/img/icons/github-icon',
href: '/guides/auth/social-login/auth-github',
official: true,
supporter: 'Supabase',
platform: true,
selfHosted: true,
authType: 'social',
},
{
name: 'GitLab',
// logo: '/img/libraries/dart-icon.svg',
logo: '/docs/img/icons/gitlab-icon',
href: '/guides/auth/social-login/auth-gitlab',
official: true,
supporter: 'Supabase',
platform: true,
selfHosted: true,
authType: 'social',
},
{
name: 'Google',
// logo: '/img/libraries/dart-icon.svg',
logo: '/docs/img/icons/google-icon',
href: '/guides/auth/social-login/auth-google',
official: true,
supporter: 'Supabase',
platform: true,
selfHosted: true,
authType: 'social',
},
{
name: 'Keycloak',
logo: '/docs/img/icons/keycloak-icon',
href: '/guides/auth/social-login/auth-keycloak',
official: true,
supporter: 'Supabase',
platform: true,
selfHosted: true,
authType: 'social',
},
{
name: 'LinkedIn',
// logo: '/img/libraries/dart-icon.svg',
logo: '/docs/img/icons/linkedin-icon',
href: '/guides/auth/social-login/auth-linkedin',
official: true,
supporter: 'Supabase',
platform: true,
selfHosted: true,
authType: 'social',
},
{
name: 'MessageBird',
// logo: '/img/libraries/dart-icon.svg',
logo: '/docs/img/icons/messagebird-icon',
href: '/guides/auth/phone-login/messagebird',
official: false,
supporter: 'MessageBird',
platform: true,
selfHosted: true,
authType: 'phone'
},
{
name: 'Notion',
// logo: '/img/libraries/notion-icon.svg',
logo: '/docs/img/icons/notion-icon',
href: '/guides/auth/social-login/auth-notion',
official: true,
supporter: 'Supabase',
platform: true,
selfHosted: true,
authType: 'social',
},
{
name: 'Slack',
// logo: '/img/libraries/dart-icon.svg',
logo: '/docs/img/icons/slack-icon',
href: '/guides/auth/social-login/auth-slack',
official: true,
supporter: 'Supabase',
platform: true,
selfHosted: true,
authType: 'social',
},
{
name: 'Spotify',
// logo: '/img/libraries/dart-icon.svg',
logo: '/docs/img/icons/spotify-icon',
href: '/guides/auth/social-login/auth-spotify',
official: true,
supporter: 'Supabase',
platform: true,
selfHosted: true,
authType: 'social',
},
{
name: 'Twitter',
// logo: '/img/libraries/dart-icon.svg',
logo: '/docs/img/icons/twitter-icon',
href: '/guides/auth/social-login/auth-twitter',
official: true,
supporter: 'Supabase',
platform: true,
selfHosted: true,
authType: 'social',
},
{
name: 'Twitch',
// logo: '/img/libraries/dart-icon.svg',
logo: '/docs/img/icons/twitch-icon',
href: '/guides/auth/social-login/auth-twitch',
official: true,
supporter: 'Supabase',
platform: true,
selfHosted: true,
authType: 'social',
},
{
name: 'Zoom',
// logo: '/img/libraries/dart-icon.svg',
logo: '/docs/img/icons/zoom-icon',
href: '/guides/auth/social-login/auth-zoom',
official: true,
supporter: 'Supabase',
platform: true,
selfHosted: true,
authType: 'social',
},
{
name: 'Twilio',
// logo: '/img/libraries/dart-icon.svg',
logo: '/docs/img/icons/twilio-icon',
href: '/guides/auth/phone-login/twilio',
official: true,
supporter: 'Supabase',
platform: true,
selfHosted: true,
authType: 'phone',
},
{
name: 'Vonage',
logo: '/docs/img/icons/vonage-icon',
href: '/guides/auth/phone-login/vonage',
official: false,
supporter: 'Supabase',
platform: true,
selfHosted: true,
authType: 'phone',
},
]
@@ -257,7 +257,7 @@ Wrap an Action to check that the user has a valid session. If they're not logged
```ts title=src/routes/posts/+page.server.ts
import type { Actions } from './$types'
import { getSupabase } from '@supabase/auth-helpers-sveltekit'
import { error, invalid } from '@sveltejs/kit'
import { error, fail } from '@sveltejs/kit'
export const actions: Actions = {
createPost: async (event) => {
@@ -276,7 +276,7 @@ export const actions: Actions = {
.insert({ content })
if (createPostError) {
return invalid(500, {
return fail(500, {
supabaseErrorMessage: createPostError.message,
})
}
@@ -293,7 +293,7 @@ If you try to submit a form with the action `?/createPost` without a valid sessi
```ts
import type { Actions } from './$types'
import { invalid, redirect } from '@sveltejs/kit'
import { fail, redirect } from '@sveltejs/kit'
import { getSupabase } from '@supabase/auth-helpers-sveltekit'
import { AuthApiError } from '@supabase/supabase-js'
@@ -313,14 +313,14 @@ export const actions: Actions = {
if (error) {
if (error instanceof AuthApiError && error.status === 400) {
return invalid(400, {
return fail(400, {
error: 'Invalid credentials.',
values: {
email,
},
})
}
return invalid(500, {
return fail(500, {
error: 'Server error. Try again later.',
values: {
email,
+9 -1
View File
@@ -44,8 +44,16 @@ You can authenticate your users in several ways:
We provide a suite of Providers and login methods, as well as [Auth helpers](/docs/guides/auth/auth-helpers/).
#### Social Auth
<div className="container" style={{ padding: 0 }}>
<AuthProviders />
<AuthProviders type="social" />
</div>
#### Phone Auth
<div className="container" style={{ padding: 0 }}>
<AuthProviders type="phone" />
</div>
### Configure third-party providers
+6
View File
@@ -135,6 +135,12 @@ export const migrationGuides = [
href: '/guides/resources/migrating-to-supabase/heroku',
description: 'Migrate your Heroku Postgres database to Supabase.',
},
{
title: 'Render',
icon: '/docs/img/icons/render-icon',
href: '/guides/resources/migrating-to-supabase/render',
description: 'Migrate your Render Postgres database to Supabase.',
},
]
export const postgres = [
@@ -0,0 +1,88 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
title: 'Migrate from Render to Supabase',
description: 'Migrate your Render Postgres database to Supabase.',
}
Render is a popular Web Hosting service in the online services category that also has a managed Postgres service. Render has a great developer experience, allowing users to deploy straight from GitHub or GitLab. This is the core of their product and they do it really well. However, when it comes to Postgres databases, it may not be the best option.
Supabase is one of the best free alternative to Render Postgres. Supabase provide all the backend features developers need to build a product: a Postgres database, authentication, instant APIs, edge functions, realtime subscriptions, and storage. Postgres is the core of Supabase—for example, you can use row-level security and there are more than 40 Postgres extensions available.
This guide demonstrates how to migrate from Render to Supabase to get the most out of Postgres while gaining access to all the features you need to build a project.
## Retrieve your Render database credentials [#retrieve-render-credentials]
1. Log in to your [Render account](https://render.com) and select the project you want to migrate.
1. Click **Dashboard** in the menu and click in your **Postgres** database.
1. Scroll down in the **Info** tab.
1. Click on **PSQL Command** and edit it adding the content after `PSQL_COMMAND=`.
![Copying PSQL command from Render dashboard](/docs/img/guides/resources/migrating-to-supabase/render/render_dashboard.png)
Example:
```bash
%env PSQL_COMMAND=PGPASSWORD=RgaMDfTS_password_FTPa7 psql -h dpg-a_server_in.oregon-postgres.render.com -U my_db_pxl0_user my_db_pxl0
```
## Retrieve your Supabase Host [#retrieve-supabase-host]
1. If you're new to Supabase, [create a project](https://app.supabase.com).
1. Go to the [Database settings](https://app.supabase.com/project/_/settings/database) for your project in the Supabase Dashboard.
1. Under **Connection Info**, note your Host (`$SUPABASE_HOST`).
1. Save your password or [reset it](https://app.supabase.com/project/_/settings/database) if you forgot it.
![Finding Supabase host address](/docs/img/guides/resources/migrating-to-supabase/render/supabase_dashboard.png)
## Migrate the database
The fastest way to migrate your database is with the Supabase migration tool on [Google Colab](https://colab.research.google.com/github/mansueli/Supa-Migrate/blob/main/Migrate_Postgres_Supabase.ipynb). Alternatively, you can use the [pg_dump](https://www.postgresql.org/docs/current/app-pgdump.html) and [psql](https://www.postgresql.org/docs/current/app-psql.html) command line tools, which are included in a full PostgreSQL installation.
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="colab"
>
<TabPanel id="colab" label="Migrate using Colab">
1. Set the environment variables (`PSQL_COMMAND`, `SUPABASE_HOST`, `SUPABASE_PASSWORD`) in the Colab notebook.
1. Run the first two steps in [the notebook](https://colab.research.google.com/github/mansueli/Supa-Migrate/blob/main/Migrate_Postgres_Supabase.ipynb) in order. The first sets the variables and the second installs PSQL and the migration script.
1. Run the third step to start the migration. This will take a few minutes.
</TabPanel>
<TabPanel id="cli" label="Migrate using CLI tools">
### Export your Render database to a file in console [#export-render-database]
Use `pg_dump` with your Render credentials to export your Render database to a file (e.g., `render_dump.sql`).
```bash
pg_dump --clean --if-exists --quote-all-identifiers \
-h $RENDER_HOST -U $RENDER_USER -d $RENDER_DATABASE \
--no-owner --no-privileges > render_dump.sql
```
### Import the database to your Supabase project [#import-database-to-supabase]
Use `psql` to import the Render database file to your Supabase project.
```bash
psql -h $SUPA_URL -U postgres --file render_dump.sql -p 6543 -d postgres
```
### Additional options
- To only migrate a single database schema, add the `--schema=PATTERN` parameter to your `pg_dump` command.
- To exclude a schema: `--exclude-schema=PATTERN`.
- To only migrate a single table: `--table=PATTERN`.
- To exclude a table: `--exclude-table=PATTERN`.
Run `pg_dump --help` for a full list of options.
</TabPanel>
</Tabs>
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
Binary file not shown.

After

Width:  |  Height:  |  Size: 789 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 764 KiB

@@ -0,0 +1,13 @@
<svg width="21" height="21" viewBox="0 0 21 21" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="17.4653" y="17.6948" width="2.81974" height="2.81974" fill="#46E7AA"/>
<rect x="13.1709" y="17.6948" width="2.81974" height="2.81974" fill="#46E7AA"/>
<rect x="13.1709" y="0.828125" width="2.81974" height="2.81974" fill="#46E7AA"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M0.285156 7.2979C0.285156 3.72312 3.18309 0.825195 6.75786 0.825195H11.6109V3.65042H6.75786C4.74341 3.65042 3.11038 5.28345 3.11038 7.2979V16.259H0.285156V7.2979Z" fill="#46E7AA"/>
<rect x="8.87671" y="17.6948" width="2.81974" height="2.81974" fill="#46E7AA"/>
<rect x="4.58228" y="17.6948" width="2.81974" height="2.81974" fill="#46E7AA"/>
<rect x="0.288086" y="17.6948" width="2.81974" height="2.81974" fill="#46E7AA"/>
<rect x="17.4653" y="13.478" width="2.81974" height="2.81974" fill="#46E7AA"/>
<rect x="17.4653" y="9.26123" width="2.81974" height="2.81974" fill="#46E7AA"/>
<rect x="17.4653" y="5.04492" width="2.81974" height="2.81974" fill="#46E7AA"/>
<rect x="17.4653" y="0.828125" width="2.81974" height="2.81974" fill="#46E7AA"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+1
View File
@@ -7,6 +7,7 @@
.DocSearch-Container {
background: rgba(0, 0, 0, 0.3);
@apply fixed top-0 left-0 z-[200] flex h-screen w-screen flex-col p-4 backdrop-blur-sm sm:p-6 md:p-[10vh] lg:p-[12vh];
@apply cursor-auto #{!important};
}
.DocSearch-LoadingIndicator svg {
Binary file not shown.

Before

Width:  |  Height:  |  Size: 661 KiB

After

Width:  |  Height:  |  Size: 667 KiB

@@ -2,7 +2,7 @@ import { Application, Router } from 'https://deno.land/x/oak/mod.ts'
const router = new Router()
router
// Note: path will be prefixed with function name
// Note: path should be prefixed with function name
.get('/oak-server', (context) => {
context.response.body = 'This is an example Oak server running on Edge Functions!'
})
+37 -30
View File
@@ -14115,20 +14115,6 @@
"resolved": "packages/config",
"link": true
},
"node_modules/configcat-common": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/configcat-common/-/configcat-common-5.3.0.tgz",
"integrity": "sha512-yM9SNuG8XQa0aHNK5eUhtzAD+a6yFEOQw0qg6f/ZvIWGHkY+7FYvDbSkvMHW1ERQaUmAIt2QLZB6rMJzFCDCmg=="
},
"node_modules/configcat-js": {
"version": "5.10.0",
"resolved": "https://registry.npmjs.org/configcat-js/-/configcat-js-5.10.0.tgz",
"integrity": "sha512-ta827/ONGd9sIKIRItQPS+URTb90BYjy+YXv5F0dF9T0X7tUZWt0b9V4Liy2Vlq+VaJR3DqrjyZyeD9vG7Z0Bg==",
"deprecated": "Please upgrade to version 6 or higher.",
"dependencies": {
"configcat-common": "^5.3.0"
}
},
"node_modules/console-browserify": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz",
@@ -38931,7 +38917,7 @@
"clipboard": "^2.0.8",
"common": "*",
"config": "*",
"configcat-js": "^5.6.0",
"configcat-js": "^7.0.0",
"dayjs": "^1.11.0",
"file-saver": "^2.0.5",
"generate-password": "^1.7.0",
@@ -39196,6 +39182,23 @@
"postcss": "^8.1.0"
}
},
"studio/node_modules/configcat-common": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/configcat-common/-/configcat-common-7.0.1.tgz",
"integrity": "sha512-9SxX4/6NzK1l+2iDWLyr/0tsli/eKzSJhBIepptF+GPy/7CprnA0coQz8fF8Gx1IF3wBZsJg0vRaC3LtNa0vRg==",
"dependencies": {
"tslib": "^2.4.1"
}
},
"studio/node_modules/configcat-js": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/configcat-js/-/configcat-js-7.0.0.tgz",
"integrity": "sha512-ECVS5rV4wzU+r+81Fc1Rho1pvbSMpZXUKpO/Ei0Ms+Qmo21U8x1NVMWF0hIplHmDdEsCtEIa5AgXVceDURGAuA==",
"dependencies": {
"configcat-common": "^7.0.1",
"tslib": "^2.4.1"
}
},
"studio/node_modules/d3-array": {
"version": "2.12.1",
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz",
@@ -46181,7 +46184,7 @@
"@sinclair/typebox": "^0.25.1",
"pg": "^8.7.1",
"pg-format": "^1.0.4",
"pgsql-parser": "^13.3.0",
"pgsql-parser": "13.4.0",
"postgres-array": "^3.0.1",
"prettier": "^2.6.0",
"prettier-plugin-sql": "^0.12.1"
@@ -49533,19 +49536,6 @@
}
}
},
"configcat-common": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/configcat-common/-/configcat-common-5.3.0.tgz",
"integrity": "sha512-yM9SNuG8XQa0aHNK5eUhtzAD+a6yFEOQw0qg6f/ZvIWGHkY+7FYvDbSkvMHW1ERQaUmAIt2QLZB6rMJzFCDCmg=="
},
"configcat-js": {
"version": "5.10.0",
"resolved": "https://registry.npmjs.org/configcat-js/-/configcat-js-5.10.0.tgz",
"integrity": "sha512-ta827/ONGd9sIKIRItQPS+URTb90BYjy+YXv5F0dF9T0X7tUZWt0b9V4Liy2Vlq+VaJR3DqrjyZyeD9vG7Z0Bg==",
"requires": {
"configcat-common": "^5.3.0"
}
},
"console-browserify": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz",
@@ -65797,7 +65787,7 @@
"clipboard": "^2.0.8",
"common": "*",
"config": "*",
"configcat-js": "^5.6.0",
"configcat-js": "^7.0.0",
"dayjs": "^1.11.0",
"file-saver": "^2.0.5",
"generate-password": "^1.7.0",
@@ -65960,6 +65950,23 @@
"postcss-value-parser": "^4.2.0"
}
},
"configcat-common": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/configcat-common/-/configcat-common-7.0.1.tgz",
"integrity": "sha512-9SxX4/6NzK1l+2iDWLyr/0tsli/eKzSJhBIepptF+GPy/7CprnA0coQz8fF8Gx1IF3wBZsJg0vRaC3LtNa0vRg==",
"requires": {
"tslib": "^2.4.1"
}
},
"configcat-js": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/configcat-js/-/configcat-js-7.0.0.tgz",
"integrity": "sha512-ECVS5rV4wzU+r+81Fc1Rho1pvbSMpZXUKpO/Ei0Ms+Qmo21U8x1NVMWF0hIplHmDdEsCtEIa5AgXVceDURGAuA==",
"requires": {
"configcat-common": "^7.0.1",
"tslib": "^2.4.1"
}
},
"d3-array": {
"version": "2.12.1",
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz",
@@ -18,8 +18,7 @@ function classNames(...classes: any) {
return classes.filter(Boolean).join(' ')
}
export interface Props
extends Omit<React.InputHTMLAttributes<HTMLButtonElement>, 'size'> {
export interface Props extends Omit<React.InputHTMLAttributes<HTMLButtonElement>, 'size'> {
className?: string
children: React.ReactNode
descriptionText?: string
@@ -73,14 +72,8 @@ function Listbox({
const triggerRef = useRef<HTMLButtonElement>(null)
const {
formContextOnChange,
values,
errors,
handleBlur,
touched,
fieldLevelValidation,
} = useFormContext()
const { formContextOnChange, values, errors, handleBlur, touched, fieldLevelValidation } =
useFormContext()
if (values && !value) {
defaultValue = values[id || name]
@@ -257,9 +250,7 @@ function Listbox({
className={__styles.options_container}
>
<div>
<SelectContext.Provider
value={{ onChange: handleOnChange, selected }}
>
<SelectContext.Provider value={{ onChange: handleOnChange, selected }}>
{children}
</SelectContext.Provider>
</div>
@@ -313,9 +304,7 @@ function SelectOption({
<div className={__styles.option_inner}>
{addOnBefore && addOnBefore({ active, selected })}
<span>
{typeof children === 'function'
? children({ active, selected })
: children}
{typeof children === 'function' ? children({ active, selected }) : children}
</span>
</div>
@@ -326,10 +315,7 @@ function SelectOption({
active ? __styles.option_check_active : ''
)}
>
<IconCheck
className={__styles.option_check_icon}
aria-hidden="true"
/>
<IconCheck className={__styles.option_check_icon} aria-hidden="true" />
</span>
) : null}
</DropdownMenuPrimitive.Item>
@@ -1,5 +1,5 @@
import { FC } from 'react'
import { Button } from 'ui'
import { Button, IconExternalLink } from 'ui'
interface Props {
showTemplates: boolean
@@ -8,15 +8,20 @@ interface Props {
}
const PolicyEditorFooter: FC<Props> = ({ showTemplates, onViewTemplates, onReviewPolicy }) => (
<div className="flex w-full items-center justify-end gap-2 border-t px-6 py-4 dark:border-dark">
{showTemplates && (
<Button type="default" onClick={onViewTemplates}>
View templates
<div className="flex justify-between items-center border-t px-6 py-4 dark:border-dark">
<a href='https://supabase.com/docs/learn/auth-deep-dive/auth-policies' target="_blank">
<Button type='link' icon={<IconExternalLink size={14} strokeWidth={1.5} />}>Documentation</Button>
</a>
<div className="flex w-full items-center justify-end gap-2">
{showTemplates && (
<Button type="default" onClick={onViewTemplates}>
View templates
</Button>
)}
<Button type="primary" onClick={onReviewPolicy}>
Review
</Button>
)}
<Button type="primary" onClick={onReviewPolicy}>
Review
</Button>
</div>
</div>
)
@@ -130,8 +130,9 @@ export const USAGE_BASED_PRODUCTS = [
costPerUnit: 0.1,
tooltip: (
<span>
Every single serverless function invocation independent of response status is counted.
Billing is based on the sum of all invocations throughout your billing period.
We continuously monitor the amount of serverless functions in your project. Billing is
based on the maximum amount of functions at any point in time throughout your billing
period.
</span>
),
},
@@ -144,9 +145,8 @@ export const USAGE_BASED_PRODUCTS = [
costPerUnit: 0.000002,
tooltip: (
<span>
We continuously monitor the amount of serverless functions in your project. Billing is
based on the maximum amount of functions at any point in time throughout your billing
period.
Every single serverless function invocation independent of response status is counted.
Billing is based on the sum of all invocations throughout your billing period.
</span>
),
},
@@ -0,0 +1,263 @@
import { FC } from 'react'
import { Input, Tabs } from 'ui'
import { pluckObjectFields } from 'lib/helpers'
import { useProjectSettingsQuery } from 'data/config/project-settings-query'
import Panel from 'components/ui/Panel'
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
import ResetDbPassword from './ResetDbPassword'
const DatabaseSettings: FC<any> = ({ projectRef }) => {
const { data, isLoading, isError } = useProjectSettingsQuery({ projectRef })
if (isError) {
return (
<div className="mx-auto p-6 text-center sm:w-full md:w-3/4">
<p className="text-scale-1000">Error loading database settings</p>
</div>
)
}
if (isLoading) {
return (
<div className="space-y-10">
<section className="space-y-6">
<h3 className="text-scale-1200 mb-2 text-xl">Database Settings</h3>
<Panel
title={
<h5 key="panel-title" className="mb-0">
Connection info
</h5>
}
className="!m-0"
>
<Panel.Content className="space-y-8">
{Array.from({ length: 5 }).map((_, i) => (
<div
key={i}
className="grid gap-2 items-center md:grid md:grid-cols-12 md:gap-x-4 w-full"
>
<ShimmeringLoader className="h-4 w-1/3 col-span-4" delayIndex={i} />
<ShimmeringLoader className="h-8 w-full col-span-8" delayIndex={i} />
</div>
))}
</Panel.Content>
</Panel>
</section>
<ResetDbPassword disabled={true} />
<section className="space-y-6">
<Panel
title={
<h5 key="panel-title" className="mb-0">
Connection string
</h5>
}
className="!m-0"
>
<Panel.Content>
<Tabs type="underlined" size="small">
<Tabs.Panel id="psql" label="PSQL">
<ShimmeringLoader className="h-8 w-full" />
</Tabs.Panel>
<Tabs.Panel id="uri" label="URI">
<ShimmeringLoader className="h-8 w-full" />
</Tabs.Panel>
<Tabs.Panel id="golang" label="Golang">
<ShimmeringLoader className="h-8 w-full" />
</Tabs.Panel>
<Tabs.Panel id="jdbc" label="JDBC">
<ShimmeringLoader className="h-8 w-full" />
</Tabs.Panel>
<Tabs.Panel id="dotnet" label=".NET">
<ShimmeringLoader className="h-8 w-full" />
</Tabs.Panel>
<Tabs.Panel id="nodejs" label="Nodejs">
<ShimmeringLoader className="h-8 w-full" />
</Tabs.Panel>
<Tabs.Panel id="php" label="PHP">
<ShimmeringLoader className="h-8 w-full" />
</Tabs.Panel>
<Tabs.Panel id="python" label="Python">
<ShimmeringLoader className="h-8 w-full" />
</Tabs.Panel>
</Tabs>
</Panel.Content>
</Panel>
</section>
</div>
)
}
const { project } = data
const DB_FIELDS = ['db_host', 'db_name', 'db_port', 'db_user', 'inserted_at']
const connectionInfo = pluckObjectFields(project, DB_FIELDS)
const uriConnString =
`postgresql://${connectionInfo.db_user}:[YOUR-PASSWORD]@` +
`${connectionInfo.db_host}:${connectionInfo.db_port.toString()}` +
`/${connectionInfo.db_name}`
const golangConnString =
`user=${connectionInfo.db_user} password=[YOUR-PASSWORD] ` +
`host=${connectionInfo.db_host} port=${connectionInfo.db_port.toString()}` +
` dbname=${connectionInfo.db_name}`
const psqlConnString =
`psql -h ${connectionInfo.db_host} -p ` +
`${connectionInfo.db_port.toString()} -d ${connectionInfo.db_name} ` +
`-U ${connectionInfo.db_user}`
return (
<div className="space-y-10">
<section className="space-y-6">
<h3 className="text-scale-1200 mb-2 text-xl">Database Settings</h3>
<Panel
title={
<h5 key="panel-title" className="mb-0">
Connection info
</h5>
}
className="!m-0"
>
<Panel.Content className="space-y-6">
<Input
className="input-mono"
layout="horizontal"
readOnly
copy
disabled
value={connectionInfo.db_host}
label="Host"
/>
<Input
className="input-mono"
layout="horizontal"
readOnly
copy
disabled
value={connectionInfo.db_name}
label="Database name"
/>
<Input
className="input-mono"
layout="horizontal"
readOnly
copy
disabled
value={connectionInfo.db_port.toString()}
label="Port"
/>
<Input
layout="horizontal"
className="input-mono table-input-cell text-base"
readOnly
copy
disabled
value={connectionInfo.db_user}
label="User"
/>
<Input
className="input-mono"
layout="horizontal"
disabled
readOnly
value={'[The password you provided when you created this project]'}
label="Password"
/>
</Panel.Content>
</Panel>
</section>
<ResetDbPassword />
<section className="space-y-6">
<Panel
title={
<h5 key="panel-title" className="mb-0">
Connection string
</h5>
}
className="!m-0"
>
<Panel.Content>
<Tabs type="underlined" size="small">
<Tabs.Panel id="psql" label="PSQL">
<Input copy readOnly disabled value={psqlConnString} />
</Tabs.Panel>
<Tabs.Panel id="uri" label="URI">
<Input copy readOnly disabled value={uriConnString} />
</Tabs.Panel>
<Tabs.Panel id="golang" label="Golang">
<Input copy readOnly disabled value={golangConnString} />
</Tabs.Panel>
<Tabs.Panel id="jdbc" label="JDBC">
<Input
copy
readOnly
disabled
value={
`jdbc:postgresql://${
connectionInfo.db_host
}:${connectionInfo.db_port.toString()}` +
`/${connectionInfo.db_name}?user=${connectionInfo.db_user}&password=[YOUR-PASSWORD]`
}
/>
</Tabs.Panel>
<Tabs.Panel id="dotnet" label=".NET">
<Input
copy
readOnly
disabled
value={
`User Id=${connectionInfo.db_user};Password=[YOUR-PASSWORD];` +
`Server=${connectionInfo.db_host};Port=${connectionInfo.db_port.toString()};` +
`Database=${connectionInfo.db_name}`
}
/>
</Tabs.Panel>
<Tabs.Panel id="nodejs" label="Nodejs">
<Input copy readOnly disabled value={uriConnString} />
</Tabs.Panel>
<Tabs.Panel id="php" label="PHP">
<Input copy readOnly disabled value={golangConnString} />
</Tabs.Panel>
<Tabs.Panel id="python" label="Python">
<Input
copy
readOnly
disabled
value={
`user=${connectionInfo.db_user} password=[YOUR-PASSWORD]` +
` host=${connectionInfo.db_host} port=${connectionInfo.db_port.toString()}` +
` database=${connectionInfo.db_name}`
}
/>
</Tabs.Panel>
</Tabs>
</Panel.Content>
</Panel>
</section>
</div>
)
}
export default DatabaseSettings
@@ -0,0 +1,180 @@
import * as Tooltip from '@radix-ui/react-tooltip'
import { FC, useState, useRef, useEffect } from 'react'
import { debounce } from 'lodash'
import generator from 'generate-password'
import { Input, Button, Modal } from 'ui'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { checkPermissions, useParams, useStore } from 'hooks'
import { patch } from 'lib/common/fetch'
import { passwordStrength } from 'lib/helpers'
import { API_URL, DEFAULT_MINIMUM_PASSWORD_STRENGTH } from 'lib/constants'
import Panel from 'components/ui/Panel'
import PasswordStrengthBar from 'components/ui/PasswordStrengthBar'
const ResetDbPassword: FC<any> = ({ disabled = false }) => {
const { ui, app, meta } = useStore()
const { ref } = useParams()
const canResetDbPassword = checkPermissions(PermissionAction.UPDATE, 'projects')
const [showResetDbPass, setShowResetDbPass] = useState<boolean>(false)
const [isUpdatingPassword, setIsUpdatingPassword] = useState<boolean>(false)
const [password, setPassword] = useState<string>('')
const [passwordStrengthMessage, setPasswordStrengthMessage] = useState<string>('')
const [passwordStrengthWarning, setPasswordStrengthWarning] = useState<string>('')
const [passwordStrengthScore, setPasswordStrengthScore] = useState<number>(0)
useEffect(() => {
if (showResetDbPass) {
setIsUpdatingPassword(false)
setPassword('')
setPasswordStrengthMessage('')
setPasswordStrengthWarning('')
setPasswordStrengthScore(0)
}
}, [showResetDbPass])
async function checkPasswordStrength(value: any) {
const { message, warning, strength } = await passwordStrength(value)
setPasswordStrengthScore(strength)
setPasswordStrengthWarning(warning)
setPasswordStrengthMessage(message)
}
const delayedCheckPasswordStrength = useRef(
debounce((value) => checkPasswordStrength(value), 300)
).current
const onDbPassChange = (e: any) => {
const value = e.target.value
setPassword(value)
if (value == '') {
setPasswordStrengthScore(-1)
setPasswordStrengthMessage('')
} else delayedCheckPasswordStrength(value)
}
const confirmResetDbPass = async () => {
if (!ref) return
if (passwordStrengthScore >= DEFAULT_MINIMUM_PASSWORD_STRENGTH) {
setIsUpdatingPassword(true)
const res = await patch(`${API_URL}/projects/${ref}/db-password`, { password })
if (!res.error) {
await app.projects.fetchDetail(ref, (project) => meta.setProjectDetails(project))
ui.setNotification({ category: 'success', message: res.message })
setShowResetDbPass(false)
} else {
ui.setNotification({ category: 'error', message: 'Failed to reset password' })
}
setIsUpdatingPassword(false)
}
}
function generateStrongPassword() {
const password = generator.generate({
length: 16,
numbers: true,
uppercase: true,
})
setPassword(password)
delayedCheckPasswordStrength(password)
}
return (
<>
<Panel className="!m-0">
<Panel.Content>
<div className="grid grid-cols-1 items-center lg:grid-cols-3">
<div className="col-span-2 space-y-1">
<p className="block">Database password</p>
<p className="text-sm opacity-50">
You can use this password to connect directly to your Postgres database.
</p>
</div>
<div className="flex items-end justify-end">
<Tooltip.Root delayDuration={0}>
<Tooltip.Trigger>
<Button
type="default"
disabled={!canResetDbPassword || disabled}
onClick={() => setShowResetDbPass(true)}
>
Reset Database Password
</Button>
</Tooltip.Trigger>
{!canResetDbPassword && (
<Tooltip.Content side="bottom">
<Tooltip.Arrow className="radix-tooltip-arrow" />
<div
className={[
'rounded bg-scale-100 py-1 px-2 leading-none shadow', // background
'border border-scale-200 ', //border
].join(' ')}
>
<span className="text-xs text-scale-1200">
You need additional permissions to reset the database password
</span>
</div>
</Tooltip.Content>
)}
</Tooltip.Root>
</div>
</div>
</Panel.Content>
</Panel>
<Modal
hideFooter
header={<h5 className="text-sm text-scale-1200">Reset database password</h5>}
confirmText="Reset password"
alignFooter="right"
size="medium"
visible={showResetDbPass}
loading={isUpdatingPassword}
onCancel={() => setShowResetDbPass(false)}
>
<Modal.Content>
<div className="w-full space-y-8 py-8">
<Input
type="password"
value={password}
copy={password.length > 0}
onChange={onDbPassChange}
error={passwordStrengthWarning}
// @ts-ignore
descriptionText={
<PasswordStrengthBar
passwordStrengthScore={passwordStrengthScore}
passwordStrengthMessage={passwordStrengthMessage}
password={password}
generateStrongPassword={generateStrongPassword}
/>
}
/>
</div>
</Modal.Content>
<Modal.Separator />
<Modal.Content>
<div className="flex space-x-2 pb-2">
<Button type="default" onClick={() => setShowResetDbPass(false)}>
Cancel
</Button>
<Button
type="primary"
loading={isUpdatingPassword}
disabled={isUpdatingPassword}
onClick={() => confirmResetDbPass()}
>
Reset password
</Button>
</div>
</Modal.Content>
</Modal>
</>
)
}
export default ResetDbPassword
@@ -7,7 +7,6 @@ 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[]
@@ -40,7 +40,7 @@ const NetworkRestrictions = ({}) => {
title="Network Restrictions"
description="Allow specific IP ranges to have access to your database."
/>
<div className="flex items-center space-x-2">
<div className="flex items-center space-x-2 mb-6">
<Link href="https://supabase.com/docs/guides/platform/network-restrictions">
<a target="_blank">
<Button type="default" icon={<IconExternalLink />}>
@@ -0,0 +1,172 @@
import Link from 'next/link'
import { useState, useEffect } from 'react'
import { useParams, useStore } from 'hooks'
import * as Tooltip from '@radix-ui/react-tooltip'
import { Button, IconDownload, Toggle, IconExternalLink, IconLoader, Alert } from 'ui'
import {
FormHeader,
FormPanel,
FormSection,
FormSectionContent,
FormSectionLabel,
} from 'components/ui/Forms'
import { useProjectSettingsQuery } from 'data/config/project-settings-query'
import { useSSLEnforcementQuery } from 'data/ssl-enforcement/ssl-enforcement-query'
import { useSSLEnforcementUpdateMutation } from 'data/ssl-enforcement/ssl-enforcement-update-mutation'
const SSLConfiguration = () => {
const { ui } = useStore()
const { ref } = useParams()
const [isEnforced, setIsEnforced] = useState(false)
const [isSubmitting, setIsSubmitting] = useState(false)
const { data: projectSettings } = useProjectSettingsQuery({ projectRef: ref })
const { data: sslEnforcementConfiguration, isLoading } = useSSLEnforcementQuery({
projectRef: ref,
})
const { mutateAsync: updateSSLEnforcement } = useSSLEnforcementUpdateMutation()
const hasAccessToSSLEnforcement = !sslEnforcementConfiguration?.isNotAllowed
const env = process.env.NEXT_PUBLIC_ENVIRONMENT === 'prod' ? 'prod' : 'staging'
const hasSSLCertificate =
projectSettings?.project !== undefined &&
new Date(projectSettings.project.inserted_at) >= new Date('2021-04-30')
useEffect(() => {
if (!isLoading && sslEnforcementConfiguration) {
setIsEnforced(
sslEnforcementConfiguration.appliedSuccessfully &&
sslEnforcementConfiguration.currentConfig.database
)
}
}, [isLoading])
const toggleSSLEnforcement = async () => {
if (!ref) return console.error('Project ref is required')
setIsEnforced(!isEnforced)
setIsSubmitting(true)
try {
await updateSSLEnforcement({
projectRef: ref,
requestedConfig: { database: !isEnforced },
})
} catch (error: any) {
ui.setNotification({
category: 'error',
message: `Failed to update SSL enforcement: ${error.message}`,
})
setIsEnforced(isEnforced)
} finally {
setIsSubmitting(false)
}
}
if (!hasAccessToSSLEnforcement) return <></>
return (
<div>
<div className="flex items-center justify-between">
<FormHeader title="SSL Configuration" description="" />
<div className="flex items-center space-x-2 mb-6">
<Link href="https://supabase.com/docs/guides/platform/ssl-enforcement">
<a target="_blank">
<Button type="default" icon={<IconExternalLink />}>
Documentation
</Button>
</a>
</Link>
</div>
</div>
<FormPanel>
<FormSection
header={
<FormSectionLabel
className="lg:col-span-7"
description={
<div className="space-y-4">
<p className="text-sm text-scale-1000">
Reject non-SSL connections to your database
</p>
{!sslEnforcementConfiguration?.appliedSuccessfully && (
<Alert
withIcon
variant="warning"
title="SSL enforcement was not updated successfully"
>
Please try updating again, or contact{' '}
<Link href="/support/new">
<a target="_blank" className="underline">
support
</a>
</Link>{' '}
if this error persists
</Alert>
)}
</div>
}
>
Enforce SSL on incoming connections
</FormSectionLabel>
}
>
<FormSectionContent loading={false} className="lg:!col-span-5">
<div className="flex items-center justify-end mt-2.5 space-x-2">
{(isLoading || isSubmitting) && (
<IconLoader className="animate-spin" strokeWidth={1.5} size={16} />
)}
<Toggle
checked={isEnforced}
disabled={isLoading || isSubmitting}
onChange={toggleSSLEnforcement}
/>
</div>
</FormSectionContent>
</FormSection>
<div className="grid grid-cols-1 items-center lg:grid-cols-2 p-8">
<div className="space-y-2">
<p className="block text-sm">SSL Certificate</p>
<div style={{ maxWidth: '420px' }}>
<p className="text-sm opacity-50">
Use this certificate when connecting to your database to prevent snooping and
man-in-the-middle attacks.
</p>
</div>
</div>
<div className="flex items-end justify-end">
<Tooltip.Root delayDuration={0}>
<Tooltip.Trigger>
<Button type="default" disabled={!hasSSLCertificate} icon={<IconDownload />}>
<a
href={`https://supabase-downloads.s3-ap-southeast-1.amazonaws.com/${env}/ssl/${env}-ca-2021.crt`}
>
Download Certificate
</a>
</Button>
</Tooltip.Trigger>
{!hasSSLCertificate && (
<Tooltip.Content align="center" 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-[250px]',
].join(' ')}
>
<span className="text-xs text-scale-1200">
Projects before 15:08 (GMT+08), 29th April 2021 do not have SSL certificates
installed
</span>
</div>
</Tooltip.Content>
)}
</Tooltip.Root>
</div>
</div>
</FormPanel>
</div>
)
}
export default SSLConfiguration
@@ -1,4 +1,5 @@
import ConnectionPooling from './ConnectionPooling'
import NetworkRestrictions from './NetworkRestrictions/NetworkRestrictions'
import DatabaseSettings from './DatabaseSettings/DatabaseSettings'
export { ConnectionPooling, NetworkRestrictions }
export { ConnectionPooling, DatabaseSettings, NetworkRestrictions }
@@ -122,7 +122,7 @@ const CustomDomainVerify = ({ projectRef, customDomain, settings }: CustomDomain
>
Please add a CAA record allowing "digicert.com" to issue certificates for{' '}
<code className="text-xs">{customDomain.hostname}</code>. For example:{' '}
<code className="text-xs">0 issue "digitcert.com"</code>
<code className="text-xs">0 issue "digicert.com"</code>
</Alert>
)}
@@ -1,48 +1,48 @@
export const CATEGORY_OPTIONS = [
{
value: 'problem',
value: 'Problem',
label: 'Issues with APIs / client libraries',
description: "Issues with your project's API and client libraries",
query: undefined,
},
{
value: 'dashboard_bug',
value: 'Dashboard_bug',
label: 'Dashboard bug',
description: 'Issues with the Supabase dashboard',
query: undefined,
},
{
value: 'database_unresponsive',
value: 'Database_unresponsive',
label: 'Database unresponsive',
description: 'Issues with connecting to your database',
query: 'Unable to connect',
},
{
value: 'performance',
value: 'Performance',
label: 'Performance issues',
description: 'Reporting of performance issues is only available on the Pro tier',
query: 'Performance',
},
{
value: 'sales',
value: 'Sales',
label: 'Sales enquiry',
description: 'Questions about pricing, paid plans and Enterprise plans',
query: undefined,
},
{
value: 'billing',
value: 'Billing',
label: 'Billing',
description: 'Issues with credit card charges | invoices | overcharging',
query: undefined,
},
{
value: 'problem',
value: 'Problem',
label: 'Abuse report',
description: 'Report abuse of a Supabase project or Supabase brand',
query: undefined,
},
{
value: 'refund',
value: 'Refund',
label: 'Refund enquiry',
description: 'Formal enquiry form for requesting refunds',
query: undefined,
@@ -375,7 +375,6 @@ const ColumnEditor: FC<Props> = ({
)}
<ForeignKeySelector
tables={tables}
column={columnFields}
visible={isEditingRelation}
closePanel={() => setIsEditingRelation(false)}
@@ -1,16 +1,16 @@
import React, { FC, useEffect, useState } from 'react'
import { get, find, isEmpty, sortBy } from 'lodash'
import { Dictionary } from 'components/grid'
import { SidePanel, Input, Listbox, IconHelpCircle } from 'ui'
import type { PostgresTable, PostgresColumn } from '@supabase/postgres-meta'
import { SidePanel, Input, Listbox, IconHelpCircle, IconDatabase } from 'ui'
import type { PostgresTable, PostgresColumn, PostgresSchema } from '@supabase/postgres-meta'
import { useStore } from 'hooks'
import ActionBar from '../ActionBar'
import { ForeignKey } from './ForeignKeySelector.types'
import { ColumnField } from '../SidePanelEditor.types'
import InformationBox from 'components/ui/InformationBox'
interface Props {
tables: PostgresTable[]
column: ColumnField
metadata?: any
visible: boolean
@@ -18,15 +18,19 @@ interface Props {
saveChanges: (value: { table: PostgresTable; column: PostgresColumn } | undefined) => void
}
const ForeignKeySelector: FC<Props> = ({
tables = [] as PostgresTable[],
column,
visible = false,
closePanel,
saveChanges,
}) => {
const ForeignKeySelector: FC<Props> = ({ column, visible = false, closePanel, saveChanges }) => {
const { meta } = useStore()
const [errors, setErrors] = useState<any>({})
const [selectedForeignKey, setSelectedForeignKey] = useState<ForeignKey>()
const [selectedForeignKey, setSelectedForeignKey] = useState<ForeignKey>({
schema: 'public',
table: '',
column: '',
})
const schemas = meta.schemas.list()
const tables = meta.tables.list(
(table: PostgresTable) => table.schema === selectedForeignKey.schema
)
const foreignKey = column?.foreignKey
const selectedTable: PostgresTable | undefined = find(tables, {
@@ -41,6 +45,7 @@ const ForeignKeySelector: FC<Props> = ({
// Reset the state of the side panel
if (visible) {
setErrors({})
if (foreignKey) {
setSelectedForeignKey({
schema: foreignKey.target_table_schema,
@@ -49,7 +54,7 @@ const ForeignKeySelector: FC<Props> = ({
})
} else {
setSelectedForeignKey({
schema: '',
schema: 'public',
table: '',
column: '',
})
@@ -57,6 +62,12 @@ const ForeignKeySelector: FC<Props> = ({
}
}, [visible])
const updateSelectedSchema = (schema: string) => {
meta.tables.loadBySchema(schema)
const updatedForeignKey = { ...selectedForeignKey, schema, table: '', column: '' } as ForeignKey
setSelectedForeignKey(updatedForeignKey)
}
const updateSelectedTable = (tableId: number) => {
setErrors({})
if (!tableId) {
@@ -87,6 +98,9 @@ const ForeignKeySelector: FC<Props> = ({
const onSaveChanges = (resolve: () => void) => {
const errors = {} as Dictionary<any>
if (!selectedForeignKey?.table) {
errors['table'] = 'Please select a table'
}
if (selectedForeignKey?.table && !selectedForeignKey.column) {
errors['column'] = `The table ${selectedForeignKey.table} has no columns`
}
@@ -141,12 +155,38 @@ const ForeignKeySelector: FC<Props> = ({
/>
<Listbox
id="schema"
label="Select a schema"
value={selectedForeignKey.schema}
error={errors.schema}
onChange={(value: string) => updateSelectedSchema(value)}
>
{schemas.map((schema: PostgresSchema) => {
return (
<Listbox.Option
key={schema.id}
value={schema.name}
label={schema.name}
addOnBefore={() => <IconDatabase size={16} strokeWidth={1.5} />}
>
<div className="flex items-center gap-2">
{/* For aria searching to target the schema name instead of schema */}
<span className="hidden">{schema.name}</span>
<span className="text-scale-1200">{schema.name}</span>
</div>
</Listbox.Option>
)
})}
</Listbox>
<Listbox
id="table"
label="Select a table to reference to"
value={selectedTable?.id}
value={selectedTable?.id ?? 1}
error={errors.table}
onChange={(value: string) => updateSelectedTable(Number(value))}
>
<Listbox.Option key="empty" value="" label="---">
<Listbox.Option key="empty" value={1} label="---">
---
</Listbox.Option>
{/* @ts-ignore */}
@@ -179,6 +219,7 @@ const ForeignKeySelector: FC<Props> = ({
/>
) : (
<Listbox
id="column"
value={selectedColumn?.id}
// @ts-ignore
label={
@@ -353,7 +353,6 @@ const ColumnManagement: FC<Props> = ({
</div>
</div>
<ForeignKeySelector
tables={tables}
column={selectedColumnToEditRelation as ColumnField}
visible={!isUndefined(selectedColumnToEditRelation)}
closePanel={() => setSelectedColumnToEditRelation(undefined)}
@@ -43,7 +43,7 @@ const TableEditorMenu: FC<Props> = ({
onDeleteTable = () => {},
onDuplicateTable = () => {},
}) => {
const { meta, ui } = useStore()
const { meta } = useStore()
const { id, ref } = useParams()
const schemas: PostgresSchema[] = meta.schemas.list()
+1 -1
View File
@@ -39,7 +39,7 @@ const FormSectionLabel = ({
}) => {
if (description !== undefined) {
return (
<div className={`flex flex-col space-y-4 col-span-12 lg:col-span-5 ${className}`}>
<div className={`flex flex-col space-y-2 col-span-12 lg:col-span-5 ${className}`}>
<label className="text-scale-1200 text-sm">{children}</label>
{description}
</div>
+3
View File
@@ -0,0 +1,3 @@
export const sslEnforcementKeys = {
list: (projectRef: string | undefined) => ['projects', projectRef, 'ssl-enforcement'] as const,
}
@@ -0,0 +1,73 @@
import { useQuery, useQueryClient, UseQueryOptions } from '@tanstack/react-query'
import { get } from 'lib/common/fetch'
import { API_ADMIN_URL } from 'lib/constants'
import { useCallback } from 'react'
import { sslEnforcementKeys } from './keys'
export type SSLEnforcementVariables = { projectRef?: string }
export type SSLEnforcementResponse = {
appliedSuccessfully: boolean
currentConfig: { database: boolean }
error?: any
isNotAllowed?: boolean
}
export async function getSSLEnforcementConfiguration(
{ projectRef }: SSLEnforcementVariables,
signal?: AbortSignal
) {
if (!projectRef) throw new Error('projectRef is required')
const response = (await get(`${API_ADMIN_URL}/projects/${projectRef}/ssl-enforcement`, {
signal,
})) as SSLEnforcementResponse
// Not allowed error is a valid response to denote if a project
// has access to the SSL enforcement 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 configure SSL enforcements')
if (isNotAllowedError) {
return {
appliedSuccessfully: false,
currentConfig: { database: false },
isNotAllowed: true,
} as SSLEnforcementResponse
} else {
throw response.error
}
}
return response as SSLEnforcementResponse
}
export type SSLEnforcementData = Awaited<ReturnType<typeof getSSLEnforcementConfiguration>>
export type SSLEnforcementError = unknown
export const useSSLEnforcementQuery = <TData = SSLEnforcementData>(
{ projectRef }: SSLEnforcementVariables,
{
enabled = true,
...options
}: UseQueryOptions<SSLEnforcementData, SSLEnforcementError, TData> = {}
) =>
useQuery<SSLEnforcementData, SSLEnforcementError, TData>(
sslEnforcementKeys.list(projectRef),
({ signal }) => getSSLEnforcementConfiguration({ projectRef }, signal),
{ enabled: enabled && typeof projectRef !== 'undefined', ...options }
)
export const useSSLEnforcementPrefetch = ({ projectRef }: SSLEnforcementVariables) => {
const client = useQueryClient()
return useCallback(() => {
if (projectRef) {
client.prefetchQuery(sslEnforcementKeys.list(projectRef), ({ signal }) =>
getSSLEnforcementConfiguration({ projectRef }, signal)
)
}
}, [projectRef])
}
@@ -0,0 +1,53 @@
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
import { put } from 'lib/common/fetch'
import { API_ADMIN_URL } from 'lib/constants'
import { sslEnforcementKeys } from './keys'
export type SSLEnforcementUpdateVariables = {
projectRef: string
requestedConfig: { database: boolean }
}
export type SSLEnforcementUpdateResponse = {
appliedSuccessfully: boolean
currentConfig: { database: boolean }
error?: any
}
export async function updateSSLEnforcement({
projectRef,
requestedConfig,
}: SSLEnforcementUpdateVariables) {
if (!projectRef) throw new Error('projectRef is required')
const response = (await put(`${API_ADMIN_URL}/projects/${projectRef}/ssl-enforcement`, {
requestedConfig,
})) as SSLEnforcementUpdateResponse
if (response.error) throw response.error
return response
}
type SSLEnforcementUpdateData = Awaited<ReturnType<typeof updateSSLEnforcement>>
export const useSSLEnforcementUpdateMutation = ({
onSuccess,
...options
}: Omit<
UseMutationOptions<SSLEnforcementUpdateData, unknown, SSLEnforcementUpdateVariables>,
'mutationFn'
> = {}) => {
const queryClient = useQueryClient()
return useMutation<SSLEnforcementUpdateData, unknown, SSLEnforcementUpdateVariables>(
(vars) => updateSSLEnforcement(vars),
{
async onSuccess(data, variables, context) {
const { projectRef } = variables
await queryClient.invalidateQueries(sslEnforcementKeys.list(projectRef))
await onSuccess?.(data, variables, context)
},
...options,
}
)
}
+1
View File
@@ -3,4 +3,5 @@ export * from './delete'
export * from './get'
export * from './patch'
export * from './post'
export * from './put'
export * from './head'
+28
View File
@@ -0,0 +1,28 @@
import { constructHeaders, handleError, handleResponse, handleResponseError } from './base'
import { uuidv4 } from '../../helpers'
import { SupaResponse } from 'types/base'
export async function put<T = any>(
url: string,
data: { [prop: string]: any },
options?: { [prop: string]: any }
): Promise<SupaResponse<T>> {
const requestId = uuidv4()
try {
const { headers: optionHeaders, ...otherOptions } = options ?? {}
const headers = await constructHeaders(requestId, optionHeaders)
const response = await fetch(url, {
method: 'PUT',
body: JSON.stringify(data),
credentials: 'include',
referrerPolicy: 'no-referrer-when-downgrade',
headers,
...otherOptions,
})
if (!response.ok) return handleResponseError(response, requestId)
return handleResponse(response, requestId)
} catch (error) {
return handleError(error, requestId)
}
}
+1 -1
View File
@@ -39,7 +39,7 @@
"clipboard": "^2.0.8",
"common": "*",
"config": "*",
"configcat-js": "^5.6.0",
"configcat-js": "^7.0.0",
"dayjs": "^1.11.0",
"file-saver": "^2.0.5",
"generate-password": "^1.7.0",
+7 -7
View File
@@ -138,14 +138,14 @@ const AuthPoliciesPage: NextPageWithLayout = () => {
icon={<IconSearch size="tiny" />}
/>
</div>
<Button type="link" icon={<IconExternalLink size={14} strokeWidth={1.5} />}>
<a
target="_blank"
href="https://supabase.com/docs/learn/auth-deep-dive/auth-row-level-security"
>
<a
target="_blank"
href="https://supabase.com/docs/learn/auth-deep-dive/auth-row-level-security"
>
<Button type="link" icon={<IconExternalLink size={14} strokeWidth={1.5} />}>
What is RLS?
</a>
</Button>
</Button>
</a>
</div>
</div>
<Policies tables={filteredTables} hasTables={tables.length > 0} isLocked={isLocked} />
+13 -475
View File
@@ -1,34 +1,27 @@
import * as Tooltip from '@radix-ui/react-tooltip'
import { FC, useState, useRef, useEffect } from 'react'
import { observer } from 'mobx-react-lite'
import { debounce } from 'lodash'
import generator from 'generate-password'
import { Input, Button, IconDownload, Tabs, Modal } from 'ui'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { NextPageWithLayout } from 'types'
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'
import { useProjectSettingsQuery } from 'data/config/project-settings-query'
import { useFlag, useParams } from 'hooks'
import { SettingsLayout } from 'components/layouts'
import PasswordStrengthBar from 'components/ui/PasswordStrengthBar'
import Panel from 'components/ui/Panel'
import { ConnectionPooling, NetworkRestrictions } from 'components/interfaces/Settings/Database'
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
import {
ConnectionPooling,
DatabaseSettings,
NetworkRestrictions,
} from 'components/interfaces/Settings/Database'
import SSLConfiguration from 'components/interfaces/Settings/Database/SSLConfiguration'
const ProjectSettings: NextPageWithLayout = () => {
const { ref: projectRef } = useParams()
const networkRestrictions = useFlag('networkRestrictions')
const sslEnforcement = useFlag('sslEnforcement')
return (
<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} />
<div className="1xl:px-28 mx-auto flex flex-col 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 space-y-10">
<DatabaseSettings projectRef={projectRef} />
<ConnectionPooling />
{sslEnforcement && <SSLConfiguration />}
{networkRestrictions && <NetworkRestrictions />}
</div>
</div>
@@ -38,458 +31,3 @@ const ProjectSettings: NextPageWithLayout = () => {
ProjectSettings.getLayout = (page) => <SettingsLayout title="Database">{page}</SettingsLayout>
export default observer(ProjectSettings)
const ResetDbPassword: FC<any> = ({ disabled = false }) => {
const { ui, app, meta } = useStore()
const { ref } = useParams()
const canResetDbPassword = checkPermissions(PermissionAction.UPDATE, 'projects')
const [showResetDbPass, setShowResetDbPass] = useState<boolean>(false)
const [isUpdatingPassword, setIsUpdatingPassword] = useState<boolean>(false)
const [password, setPassword] = useState<string>('')
const [passwordStrengthMessage, setPasswordStrengthMessage] = useState<string>('')
const [passwordStrengthWarning, setPasswordStrengthWarning] = useState<string>('')
const [passwordStrengthScore, setPasswordStrengthScore] = useState<number>(0)
useEffect(() => {
if (showResetDbPass) {
setIsUpdatingPassword(false)
setPassword('')
setPasswordStrengthMessage('')
setPasswordStrengthWarning('')
setPasswordStrengthScore(0)
}
}, [showResetDbPass])
async function checkPasswordStrength(value: any) {
const { message, warning, strength } = await passwordStrength(value)
setPasswordStrengthScore(strength)
setPasswordStrengthWarning(warning)
setPasswordStrengthMessage(message)
}
const delayedCheckPasswordStrength = useRef(
debounce((value) => checkPasswordStrength(value), 300)
).current
const onDbPassChange = (e: any) => {
const value = e.target.value
setPassword(value)
if (value == '') {
setPasswordStrengthScore(-1)
setPasswordStrengthMessage('')
} else delayedCheckPasswordStrength(value)
}
const confirmResetDbPass = async () => {
if (!ref) return
if (passwordStrengthScore >= DEFAULT_MINIMUM_PASSWORD_STRENGTH) {
setIsUpdatingPassword(true)
const res = await patch(`${API_URL}/projects/${ref}/db-password`, { password })
if (!res.error) {
await app.projects.fetchDetail(ref, (project) => meta.setProjectDetails(project))
ui.setNotification({ category: 'success', message: res.message })
setShowResetDbPass(false)
} else {
ui.setNotification({ category: 'error', message: 'Failed to reset password' })
}
setIsUpdatingPassword(false)
}
}
function generateStrongPassword() {
const password = generator.generate({
length: 16,
numbers: true,
uppercase: true,
})
setPassword(password)
delayedCheckPasswordStrength(password)
}
return (
<>
<Panel>
<Panel.Content>
<div className="grid grid-cols-1 items-center lg:grid-cols-2">
<div>
<p className="block">Database password</p>
<div style={{ maxWidth: '420px' }}>
<p className="text-sm opacity-50">
You can use this password to connect directly to your Postgres database.
</p>
</div>
</div>
<div className="flex items-end justify-end">
<Tooltip.Root delayDuration={0}>
<Tooltip.Trigger>
<Button
type="default"
disabled={!canResetDbPassword || disabled}
onClick={() => setShowResetDbPass(true)}
>
Reset Database Password
</Button>
</Tooltip.Trigger>
{!canResetDbPassword && (
<Tooltip.Content side="bottom">
<Tooltip.Arrow className="radix-tooltip-arrow" />
<div
className={[
'rounded bg-scale-100 py-1 px-2 leading-none shadow', // background
'border border-scale-200 ', //border
].join(' ')}
>
<span className="text-xs text-scale-1200">
You need additional permissions to reset the database password
</span>
</div>
</Tooltip.Content>
)}
</Tooltip.Root>
</div>
</div>
</Panel.Content>
</Panel>
<Modal
hideFooter
header={<h5 className="text-sm text-scale-1200">Reset database password</h5>}
confirmText="Reset password"
alignFooter="right"
size="medium"
visible={showResetDbPass}
loading={isUpdatingPassword}
onCancel={() => setShowResetDbPass(false)}
>
<Modal.Content>
<div className="w-full space-y-8 py-8">
<Input
type="password"
value={password}
copy={password.length > 0}
onChange={onDbPassChange}
error={passwordStrengthWarning}
// @ts-ignore
descriptionText={
<PasswordStrengthBar
passwordStrengthScore={passwordStrengthScore}
passwordStrengthMessage={passwordStrengthMessage}
password={password}
generateStrongPassword={generateStrongPassword}
/>
}
/>
</div>
</Modal.Content>
<Modal.Separator />
<Modal.Content>
<div className="flex space-x-2 pb-2">
<Button type="default" onClick={() => setShowResetDbPass(false)}>
Cancel
</Button>
<Button
type="primary"
loading={isUpdatingPassword}
disabled={isUpdatingPassword}
onClick={() => confirmResetDbPass()}
>
Reset password
</Button>
</div>
</Modal.Content>
</Modal>
</>
)
}
const DownloadCertificate: FC<any> = ({ createdAt, disabled = false }) => {
// instances before 3 : 08 pm sgt 29th April don't have certs installed
if (new Date(createdAt) < new Date('2021-04-30')) return null
const env = process.env.NEXT_PUBLIC_ENVIRONMENT === 'prod' ? 'prod' : 'staging'
return (
<Panel>
<Panel.Content>
<div className="grid grid-cols-1 items-center lg:grid-cols-2">
<div>
<p className="block">SSL Connection</p>
<div style={{ maxWidth: '420px' }}>
<p className="text-sm opacity-50">
Use this certificate when connecting to your database to prevent snooping and
man-in-the-middle attacks.
</p>
</div>
</div>
<div className="flex items-end justify-end">
<Button type="default" icon={<IconDownload />} disabled={disabled}>
<a
href={`https://supabase-downloads.s3-ap-southeast-1.amazonaws.com/${env}/ssl/${env}-ca-2021.crt`}
>
Download Certificate
</a>
</Button>
</div>
</div>
</Panel.Content>
</Panel>
)
}
const GeneralSettings: FC<any> = ({ projectRef }) => {
const { data, isLoading, isError } = useProjectSettingsQuery({ projectRef })
if (isError) {
return (
<div className="mx-auto p-6 text-center sm:w-full md:w-3/4">
<p className="text-scale-1000">Error loading database settings</p>
</div>
)
}
if (isLoading) {
return (
<>
<div className="mb-8">
<section className="space-y-6">
<h3 className="text-scale-1200 mb-2 text-xl">Database Settings</h3>
<Panel
title={
<h5 key="panel-title" className="mb-0">
Connection info
</h5>
}
>
<Panel.Content className="space-y-8">
{Array.from({ length: 5 }).map((_, i) => (
<div
key={i}
className="grid gap-2 items-center md:grid md:grid-cols-12 md:gap-x-4 w-full"
>
<ShimmeringLoader className="h-4 w-1/3 col-span-4" delayIndex={i} />
<ShimmeringLoader className="h-8 w-full col-span-8" delayIndex={i} />
</div>
))}
</Panel.Content>
</Panel>
</section>
<ResetDbPassword disabled={true} />
<DownloadCertificate disabled={true} />
</div>
<div className="mt-8">
<section className="space-y-6">
<Panel
title={
<h5 key="panel-title" className="mb-0">
Connection string
</h5>
}
>
<Panel.Content>
<Tabs type="underlined" size="small">
<Tabs.Panel id="psql" label="PSQL">
<ShimmeringLoader className="h-8 w-full" />
</Tabs.Panel>
<Tabs.Panel id="uri" label="URI">
<ShimmeringLoader className="h-8 w-full" />
</Tabs.Panel>
<Tabs.Panel id="golang" label="Golang">
<ShimmeringLoader className="h-8 w-full" />
</Tabs.Panel>
<Tabs.Panel id="jdbc" label="JDBC">
<ShimmeringLoader className="h-8 w-full" />
</Tabs.Panel>
<Tabs.Panel id="dotnet" label=".NET">
<ShimmeringLoader className="h-8 w-full" />
</Tabs.Panel>
<Tabs.Panel id="nodejs" label="Nodejs">
<ShimmeringLoader className="h-8 w-full" />
</Tabs.Panel>
<Tabs.Panel id="php" label="PHP">
<ShimmeringLoader className="h-8 w-full" />
</Tabs.Panel>
<Tabs.Panel id="python" label="Python">
<ShimmeringLoader className="h-8 w-full" />
</Tabs.Panel>
</Tabs>
</Panel.Content>
</Panel>
</section>
</div>
</>
)
}
const { project } = data
const DB_FIELDS = ['db_host', 'db_name', 'db_port', 'db_user', 'inserted_at']
const connectionInfo = pluckObjectFields(project, DB_FIELDS)
const uriConnString =
`postgresql://${connectionInfo.db_user}:[YOUR-PASSWORD]@` +
`${connectionInfo.db_host}:${connectionInfo.db_port.toString()}` +
`/${connectionInfo.db_name}`
const golangConnString =
`user=${connectionInfo.db_user} password=[YOUR-PASSWORD] ` +
`host=${connectionInfo.db_host} port=${connectionInfo.db_port.toString()}` +
` dbname=${connectionInfo.db_name}`
const psqlConnString =
`psql -h ${connectionInfo.db_host} -p ` +
`${connectionInfo.db_port.toString()} -d ${connectionInfo.db_name} ` +
`-U ${connectionInfo.db_user}`
return (
<>
<div className="mb-8">
<section className="space-y-6">
<h3 className="text-scale-1200 mb-2 text-xl">Database Settings</h3>
<Panel
title={
<h5 key="panel-title" className="mb-0">
Connection info
</h5>
}
>
<Panel.Content className="space-y-6">
<Input
className="input-mono"
layout="horizontal"
readOnly
copy
disabled
value={connectionInfo.db_host}
label="Host"
/>
<Input
className="input-mono"
layout="horizontal"
readOnly
copy
disabled
value={connectionInfo.db_name}
label="Database name"
/>
<Input
className="input-mono"
layout="horizontal"
readOnly
copy
disabled
value={connectionInfo.db_port.toString()}
label="Port"
/>
<Input
layout="horizontal"
className="input-mono table-input-cell text-base"
readOnly
copy
disabled
value={connectionInfo.db_user}
label="User"
/>
<Input
className="input-mono"
layout="horizontal"
disabled
readOnly
value={'[The password you provided when you created this project]'}
label="Password"
/>
</Panel.Content>
</Panel>
</section>
<ResetDbPassword />
<DownloadCertificate createdAt={connectionInfo.inserted_at} />
</div>
<div className="mt-8">
<section className="space-y-6">
<Panel
title={
<h5 key="panel-title" className="mb-0">
Connection string
</h5>
}
>
<Panel.Content>
<Tabs type="underlined" size="small">
<Tabs.Panel id="psql" label="PSQL">
<Input copy readOnly disabled value={psqlConnString} />
</Tabs.Panel>
<Tabs.Panel id="uri" label="URI">
<Input copy readOnly disabled value={uriConnString} />
</Tabs.Panel>
<Tabs.Panel id="golang" label="Golang">
<Input copy readOnly disabled value={golangConnString} />
</Tabs.Panel>
<Tabs.Panel id="jdbc" label="JDBC">
<Input
copy
readOnly
disabled
value={
`jdbc:postgresql://${
connectionInfo.db_host
}:${connectionInfo.db_port.toString()}` +
`/${connectionInfo.db_name}?user=${connectionInfo.db_user}&password=[YOUR-PASSWORD]`
}
/>
</Tabs.Panel>
<Tabs.Panel id="dotnet" label=".NET">
<Input
copy
readOnly
disabled
value={
`User Id=${connectionInfo.db_user};Password=[YOUR-PASSWORD];` +
`Server=${
connectionInfo.db_host
};Port=${connectionInfo.db_port.toString()};` +
`Database=${connectionInfo.db_name}`
}
/>
</Tabs.Panel>
<Tabs.Panel id="nodejs" label="Nodejs">
<Input copy readOnly disabled value={uriConnString} />
</Tabs.Panel>
<Tabs.Panel id="php" label="PHP">
<Input copy readOnly disabled value={golangConnString} />
</Tabs.Panel>
<Tabs.Panel id="python" label="Python">
<Input
copy
readOnly
disabled
value={
`user=${connectionInfo.db_user} password=[YOUR-PASSWORD]` +
` host=${connectionInfo.db_host} port=${connectionInfo.db_port.toString()}` +
` database=${connectionInfo.db_name}`
}
/>
</Tabs.Panel>
</Tabs>
</Panel.Content>
</Panel>
</section>
</div>
</>
)
}