mirror of
https://github.com/supabase/supabase.git
synced 2026-10-07 02:15:05 +03:00
42 files changed
+10347
-6923
No files matched your search
+4
-1
@@ -9,4 +9,7 @@ NEXT_PUBLIC_DOCS_URL="http://localhost:3005"
|
||||
NEXT_PUBLIC_STUDIO_URL="https://localhost:8082"
|
||||
NEXT_PUBLIC_LAUNCHWEEKSITE_URL="https://localhost:3008"
|
||||
NEXT_PUBLIC_REFERENCE_DOCS_URL="https://localhost:3010"
|
||||
|
||||
NEXT_PUBLIC_LW_STARTED=true
|
||||
NEXT_PUBLIC_SITE_ORIGIN=http://localhost:3000
|
||||
NEXT_PUBLIC_SUPABASE_URL=https://obuldanrptloktxcffvn.supabase.co
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im9idWxkYW5ycHRsb2t0eGNmZnZuIiwicm9sZSI6ImFub24iLCJpYXQiOjE2Njk3MjcwMTIsImV4cCI6MTk4NTMwMzAxMn0.SZLqryz_-stF8dgzeVXmzZWPOqdOrBwqJROlFES8v3I
|
||||
@@ -264,7 +264,7 @@ export default function launchweek() {
|
||||
justified={false}
|
||||
bordered={false}
|
||||
chevronAlign="right"
|
||||
defaultValue={day1.d.toString()}
|
||||
defaultValue={[day1.d.toString()]}
|
||||
>
|
||||
<div className="border-b pb-3">
|
||||
<Accordion.Item
|
||||
|
||||
@@ -18,8 +18,9 @@ type Props = {
|
||||
}
|
||||
|
||||
const supabaseAdmin = createClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.SUPABASE_SERVICE_ROLE_SECRET!
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL ?? 'http://localhost:54321',
|
||||
process.env.SUPABASE_SERVICE_ROLE_SECRET ??
|
||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im9idWxkYW5ycHRsb2t0eGNmZnZuIiwicm9sZSI6ImFub24iLCJpYXQiOjE2Njk3MjcwMTIsImV4cCI6MTk4NTMwMzAxMn0.SZLqryz_-stF8dgzeVXmzZWPOqdOrBwqJROlFES8v3I'
|
||||
)
|
||||
|
||||
export default function TicketShare({ username, ticketNumber, name, golden }: Props) {
|
||||
|
||||
Generated
+26
@@ -10268,6 +10268,12 @@
|
||||
"@babel/types": "^7.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/blueimp-md5": {
|
||||
"version": "2.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/blueimp-md5/-/blueimp-md5-2.18.0.tgz",
|
||||
"integrity": "sha512-f4A+++lGZGJvVSgeyMkqA7BEf2BVQli6F+qEykKb49c5ieWQBkfpn6CP5c1IZr2Yi2Ofl6Fj+v0e1fN18Z8Cnw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/btoa-lite": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/btoa-lite/-/btoa-lite-1.0.0.tgz",
|
||||
@@ -12819,6 +12825,11 @@
|
||||
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
|
||||
"integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="
|
||||
},
|
||||
"node_modules/blueimp-md5": {
|
||||
"version": "2.19.0",
|
||||
"resolved": "https://registry.npmjs.org/blueimp-md5/-/blueimp-md5-2.19.0.tgz",
|
||||
"integrity": "sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w=="
|
||||
},
|
||||
"node_modules/bn.js": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz",
|
||||
@@ -37844,6 +37855,7 @@
|
||||
"ajv": "^8.6.3",
|
||||
"analytics-node": "^3.5.0",
|
||||
"awesome-debounce-promise": "^2.1.0",
|
||||
"blueimp-md5": "^2.19.0",
|
||||
"clipboard": "^2.0.8",
|
||||
"common": "*",
|
||||
"config": "*",
|
||||
@@ -37913,6 +37925,7 @@
|
||||
"@testing-library/dom": "^8.11.1",
|
||||
"@testing-library/react": "^12.1.2",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"@types/blueimp-md5": "^2.18.0",
|
||||
"@types/crypto-js": "^4.0.2",
|
||||
"@types/file-saver": "^2.0.2",
|
||||
"@types/json-logic-js": "^1.2.1",
|
||||
@@ -45119,6 +45132,12 @@
|
||||
"@babel/types": "^7.3.0"
|
||||
}
|
||||
},
|
||||
"@types/blueimp-md5": {
|
||||
"version": "2.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/blueimp-md5/-/blueimp-md5-2.18.0.tgz",
|
||||
"integrity": "sha512-f4A+++lGZGJvVSgeyMkqA7BEf2BVQli6F+qEykKb49c5ieWQBkfpn6CP5c1IZr2Yi2Ofl6Fj+v0e1fN18Z8Cnw==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/btoa-lite": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/btoa-lite/-/btoa-lite-1.0.0.tgz",
|
||||
@@ -47178,6 +47197,11 @@
|
||||
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
|
||||
"integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="
|
||||
},
|
||||
"blueimp-md5": {
|
||||
"version": "2.19.0",
|
||||
"resolved": "https://registry.npmjs.org/blueimp-md5/-/blueimp-md5-2.19.0.tgz",
|
||||
"integrity": "sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w=="
|
||||
},
|
||||
"bn.js": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz",
|
||||
@@ -63844,6 +63868,7 @@
|
||||
"@testing-library/dom": "^8.11.1",
|
||||
"@testing-library/react": "^12.1.2",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"@types/blueimp-md5": "^2.18.0",
|
||||
"@types/crypto-js": "^4.0.2",
|
||||
"@types/file-saver": "^2.0.2",
|
||||
"@types/json-logic-js": "^1.2.1",
|
||||
@@ -63869,6 +63894,7 @@
|
||||
"autoprefixer": "10.4.2",
|
||||
"awesome-debounce-promise": "^2.1.0",
|
||||
"babel-loader": "^8.2.3",
|
||||
"blueimp-md5": "^2.19.0",
|
||||
"clipboard": "^2.0.8",
|
||||
"common": "*",
|
||||
"config": "*",
|
||||
|
||||
@@ -1116,18 +1116,18 @@
|
||||
"PostgrestConfigResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"max_rows": { "type": "integer" },
|
||||
"db_schema": { "type": "string" },
|
||||
"max_rows": { "type": "number" },
|
||||
"db_extra_search_path": { "type": "string" }
|
||||
},
|
||||
"required": ["db_schema", "max_rows", "db_extra_search_path"]
|
||||
"required": ["max_rows", "db_schema", "db_extra_search_path"]
|
||||
},
|
||||
"UpdatePostgrestConfigBody": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"max_rows": { "type": "integer", "minimum": 0, "maximum": 1000000 },
|
||||
"db_extra_search_path": { "type": "string" },
|
||||
"db_schema": { "type": "string" },
|
||||
"max_rows": { "type": "number", "maximum": 1000000 }
|
||||
"db_schema": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"UpdateCustomHostnameResponse": {
|
||||
|
||||
@@ -387,6 +387,13 @@
|
||||
"title": "Auth",
|
||||
"libs": ["js", "js_v1", "dart", "dart_v0"],
|
||||
"items": [
|
||||
{
|
||||
"id": "auth-api",
|
||||
"title": "Overview",
|
||||
"slug": "auth-api",
|
||||
"libs": ["js"],
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"id": "sign-up",
|
||||
"title": "Create a new user",
|
||||
@@ -547,7 +554,6 @@
|
||||
"type": "function",
|
||||
"libs": ["js"]
|
||||
},
|
||||
|
||||
{
|
||||
"id": "on-auth-state-change",
|
||||
"title": "Listen to auth events",
|
||||
@@ -564,86 +570,154 @@
|
||||
"type": "function",
|
||||
"libs": ["js", "js_v1", "dart", "dart_v0"]
|
||||
},
|
||||
{
|
||||
"id": "mfa-enroll",
|
||||
"title": "Enroll a factor",
|
||||
"slug": "auth-mfa-enroll",
|
||||
"product": "auth",
|
||||
"type": "function",
|
||||
"libs": ["js"]
|
||||
},
|
||||
{
|
||||
"id": "mfa-challenge",
|
||||
"title": "Create a challenge",
|
||||
"slug": "auth-mfa-challenge",
|
||||
"product": "auth",
|
||||
"type": "function",
|
||||
"libs": ["js"]
|
||||
},
|
||||
{
|
||||
"id": "mfa-verify",
|
||||
"title": "Verify a challenge",
|
||||
"slug": "auth-mfa-verify",
|
||||
"product": "auth",
|
||||
"type": "function",
|
||||
"libs": ["js"]
|
||||
},
|
||||
{
|
||||
"id": "mfa-challenge-and-verify",
|
||||
"title": "Create and verify a challenge",
|
||||
"slug": "auth-mfa-challengeandverify",
|
||||
"product": "auth",
|
||||
"type": "function",
|
||||
"libs": ["js"]
|
||||
},
|
||||
{
|
||||
"id": "mfa-unenroll",
|
||||
"title": "Unenroll a factor",
|
||||
"slug": "auth-mfa-unenroll",
|
||||
"product": "auth",
|
||||
"type": "function",
|
||||
"libs": ["js"]
|
||||
},
|
||||
{
|
||||
"id": "mfa-get-authenticator-assurance-level",
|
||||
"title": "Get Authenticator Assurance Level",
|
||||
"slug": "auth-mfa-getauthenticatorassurancelevel",
|
||||
"product": "auth",
|
||||
"type": "function",
|
||||
"libs": ["js"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Auth Admin",
|
||||
"libs": ["js", "js_v1"],
|
||||
"items": [
|
||||
{
|
||||
"id": "admin-api",
|
||||
"title": "Auth (server-only)",
|
||||
"title": "Overview",
|
||||
"slug": "admin-api",
|
||||
"libs": ["js", "js_v1"],
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"id": "get-user-by-id",
|
||||
"title": "Retrieve a user",
|
||||
"slug": "auth-admin-getuserbyid",
|
||||
"product": "auth-admin",
|
||||
"type": "function",
|
||||
"items": [
|
||||
{
|
||||
"id": "get-user-by-id",
|
||||
"title": "Retrieve a user",
|
||||
"slug": "auth-admin-getuserbyid",
|
||||
"product": "auth-admin",
|
||||
"type": "function",
|
||||
"libs": ["js"]
|
||||
},
|
||||
{
|
||||
"id": "list-users",
|
||||
"title": "List all users",
|
||||
"slug": "auth-admin-listusers",
|
||||
"product": "auth-admin",
|
||||
"type": "function",
|
||||
"libs": ["js", "js_v1"]
|
||||
},
|
||||
{
|
||||
"id": "create-user",
|
||||
"title": "Create a user",
|
||||
"slug": "auth-admin-createuser",
|
||||
"product": "auth-admin",
|
||||
"type": "function",
|
||||
"libs": ["js", "js_v1"]
|
||||
},
|
||||
{
|
||||
"id": "delete-user",
|
||||
"title": "Delete a user",
|
||||
"slug": "auth-admin-deleteuser",
|
||||
"product": "auth-admin",
|
||||
"type": "function",
|
||||
"libs": ["js", "js_v1"]
|
||||
},
|
||||
{
|
||||
"id": "invite-user-by-email",
|
||||
"title": "Send an email invite link",
|
||||
"slug": "auth-admin-inviteuserbyemail",
|
||||
"product": "auth-admin",
|
||||
"type": "function",
|
||||
"libs": ["js", "js_v1"]
|
||||
},
|
||||
{
|
||||
"id": "reset-password-for-email",
|
||||
"title": "Send a password reset request",
|
||||
"slug": "auth-admin-resetpasswordforemail",
|
||||
"product": "auth-admin",
|
||||
"type": "function",
|
||||
"libs": ["js"]
|
||||
},
|
||||
{
|
||||
"id": "generate-link",
|
||||
"title": "Generate an email link",
|
||||
"slug": "auth-admin-generatelink",
|
||||
"product": "auth-admin",
|
||||
"type": "function",
|
||||
"libs": ["js"]
|
||||
},
|
||||
{
|
||||
"id": "send-mobile-otp",
|
||||
"title": "Send a one-time passcode",
|
||||
"slug": "auth-api-sendmobileotp",
|
||||
"product": "auth-server",
|
||||
"type": "function",
|
||||
"libs": ["js_v1"]
|
||||
},
|
||||
{
|
||||
"id": "update-user-by-id",
|
||||
"title": "Update a user",
|
||||
"slug": "auth-admin-updateuserbyid",
|
||||
"product": "auth-admin",
|
||||
"type": "function",
|
||||
"libs": ["js", "js_v1"]
|
||||
}
|
||||
]
|
||||
"libs": ["js"]
|
||||
},
|
||||
{
|
||||
"id": "list-users",
|
||||
"title": "List all users",
|
||||
"slug": "auth-admin-listusers",
|
||||
"product": "auth-admin",
|
||||
"type": "function",
|
||||
"libs": ["js", "js_v1"]
|
||||
},
|
||||
{
|
||||
"id": "create-user",
|
||||
"title": "Create a user",
|
||||
"slug": "auth-admin-createuser",
|
||||
"product": "auth-admin",
|
||||
"type": "function",
|
||||
"libs": ["js", "js_v1"]
|
||||
},
|
||||
{
|
||||
"id": "delete-user",
|
||||
"title": "Delete a user",
|
||||
"slug": "auth-admin-deleteuser",
|
||||
"product": "auth-admin",
|
||||
"type": "function",
|
||||
"libs": ["js", "js_v1"]
|
||||
},
|
||||
{
|
||||
"id": "invite-user-by-email",
|
||||
"title": "Send an email invite link",
|
||||
"slug": "auth-admin-inviteuserbyemail",
|
||||
"product": "auth-admin",
|
||||
"type": "function",
|
||||
"libs": ["js", "js_v1"]
|
||||
},
|
||||
{
|
||||
"id": "reset-password-for-email",
|
||||
"title": "Send a password reset request",
|
||||
"slug": "auth-admin-resetpasswordforemail",
|
||||
"product": "auth-admin",
|
||||
"type": "function",
|
||||
"libs": ["js"]
|
||||
},
|
||||
{
|
||||
"id": "generate-link",
|
||||
"title": "Generate an email link",
|
||||
"slug": "auth-admin-generatelink",
|
||||
"product": "auth-admin",
|
||||
"type": "function",
|
||||
"libs": ["js"]
|
||||
},
|
||||
{
|
||||
"id": "send-mobile-otp",
|
||||
"title": "Send a one-time passcode",
|
||||
"slug": "auth-api-sendmobileotp",
|
||||
"product": "auth-server",
|
||||
"type": "function",
|
||||
"libs": ["js_v1"]
|
||||
},
|
||||
{
|
||||
"id": "update-user-by-id",
|
||||
"title": "Update a user",
|
||||
"slug": "auth-admin-updateuserbyid",
|
||||
"product": "auth-admin",
|
||||
"type": "function",
|
||||
"libs": ["js", "js_v1"]
|
||||
},
|
||||
{
|
||||
"id": "mfa-list-factors",
|
||||
"title": "List all factors for a user",
|
||||
"slug": "auth-admin-mfa-listfactors",
|
||||
"product": "auth-admin",
|
||||
"type": "function",
|
||||
"libs": ["js"]
|
||||
},
|
||||
{
|
||||
"id": "mfa-delete-factor",
|
||||
"title": "Delete a factor for a user",
|
||||
"slug": "auth-admin-mfa-deletefactor",
|
||||
"product": "auth-admin",
|
||||
"type": "function",
|
||||
"libs": ["js"]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
+2754
-2362
File diff suppressed because it is too large.
Load diff
+1733
-1645
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
+237
-38
@@ -103,6 +103,35 @@ functions:
|
||||
```
|
||||
|
||||
For React Native we recommend using `AsyncStorage` as the storage implementation for Supabase Auth.
|
||||
- id: auth-api
|
||||
title: 'Overview'
|
||||
notes: |
|
||||
- The auth methods can be accessed via the `supabase.auth` namespace.
|
||||
examples:
|
||||
- id: create-auth-client
|
||||
name: Create auth client
|
||||
isSpotlight: true
|
||||
code: |
|
||||
```js
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
|
||||
const supabase = createClient(supabase_url, anon_key)
|
||||
```
|
||||
- id: create-auth-client-server-side
|
||||
name: Create auth client (server-side)
|
||||
isSpotlight: false
|
||||
code: |
|
||||
```js
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
|
||||
const supabase = createClient(supabase_url, anon_key, {
|
||||
auth: {
|
||||
autoRefreshToken: false,
|
||||
persistSession: false,
|
||||
detectSessionInUrl: false
|
||||
}
|
||||
})
|
||||
```
|
||||
- id: sign-up
|
||||
title: 'signUp()'
|
||||
$ref: '@supabase/gotrue-js.GoTrueClient.signUp'
|
||||
@@ -118,7 +147,7 @@ functions:
|
||||
- To fetch the currently logged-in user, refer to [`getUser()`](/docs/reference/javascript/auth-getuser).
|
||||
examples:
|
||||
- id: sign-up
|
||||
name: Sign up.
|
||||
name: Sign up
|
||||
isSpotlight: true
|
||||
code: |
|
||||
```js
|
||||
@@ -128,7 +157,7 @@ functions:
|
||||
})
|
||||
```
|
||||
- id: sign-up-with-additional-user-metadata.
|
||||
name: Sign up with additional user metadata.
|
||||
name: Sign up with additional user metadata
|
||||
isSpotlight: false
|
||||
code: |
|
||||
```js
|
||||
@@ -188,7 +217,7 @@ functions:
|
||||
- The magic link's destination URL is determined by the [`SITE_URL`](https://supabase.com/docs/reference/auth/config#site_url). You can modify the `SITE_URL` or add additional redirect urls in [your project](https://app.supabase.com/project/_/auth/settings).
|
||||
examples:
|
||||
- id: sign-in-with-email
|
||||
name: Sign in with email.
|
||||
name: Sign in with email
|
||||
isSpotlight: true
|
||||
description: The user will be sent an email which contains either a magiclink or a OTP or both. By default, a given user can only request a OTP once every 60 seconds.
|
||||
code: |
|
||||
@@ -198,7 +227,7 @@ functions:
|
||||
})
|
||||
```
|
||||
- id: sign-in-with-sms-otp
|
||||
name: Sign in with SMS OTP.
|
||||
name: Sign in with SMS OTP
|
||||
isSpotlight: false
|
||||
description: The user will be sent a SMS which contains a OTP. By default, a given user can only request a OTP once every 60 seconds.
|
||||
code: |
|
||||
@@ -314,7 +343,7 @@ functions:
|
||||
const { data: { user } } = await supabase.auth.getUser()
|
||||
```
|
||||
- id: get-the-logged-in-user-with-a-custom-access-token-jwt
|
||||
name: Get the logged in user with a custom access token jwt.
|
||||
name: Get the logged in user with a custom access token jwt
|
||||
isSpotlight: false
|
||||
code: |
|
||||
```js
|
||||
@@ -367,34 +396,41 @@ functions:
|
||||
isSpotlight: true
|
||||
code: |
|
||||
```js
|
||||
const { data, error } = supabase.auth.setSession(refresh_token)
|
||||
const { data, error } = supabase.auth.setSession({
|
||||
access_token,
|
||||
refresh_token
|
||||
)
|
||||
```
|
||||
- id: refresh-session
|
||||
title: 'refreshSession()'
|
||||
$ref: '@supabase/gotrue-js.GoTrueClient.refreshSession'
|
||||
notes: |
|
||||
- This method will refresh the session whether the current one is expired or not.
|
||||
- Both examples destructure `user` and `session` from `data`. This is not required; so `const { data, error } =` is also valid.
|
||||
examples:
|
||||
- id: refresh-session-using-the-current-session
|
||||
name: Refresh session using the current session
|
||||
isSpotlight: true
|
||||
code: |
|
||||
```js
|
||||
const { data: { user, session }, error } = await supabase.auth.refreshSession()
|
||||
const { data, error } = await supabase.auth.refreshSession()
|
||||
const { session, user } = data
|
||||
```
|
||||
- id: refresh-session-using-a-passed-in-session
|
||||
name: Refresh session using a passed-in session
|
||||
isSpotlight: true
|
||||
name: Refresh session using a refresh token
|
||||
isSpotlight: false
|
||||
code: |
|
||||
```js
|
||||
const { data: { user, session }, error } = await supabase.auth.refreshSession({ refresh_token })
|
||||
const { data, error } = await supabase.auth.refreshSession({ refresh_token })
|
||||
const { session, user } = data
|
||||
```
|
||||
- id: on-auth-state-change
|
||||
title: 'onAuthStateChange()'
|
||||
$ref: '@supabase/gotrue-js.GoTrueClient.onAuthStateChange'
|
||||
notes: |
|
||||
- Types of auth events: `SIGNED_IN`, `SIGNED_OUT`, `TOKEN_REFRESHED`, `USER_UPDATED`, `USER_DELETED`, `PASSWORD_RECOVERY`
|
||||
- Types of auth events: `SIGNED_IN`, `SIGNED_OUT`, `TOKEN_REFRESHED`, `USER_UPDATED`, `PASSWORD_RECOVERY`
|
||||
- Currently, `onAuthStateChange()` does not work across tabs.
|
||||
For instance, in the case of a password reset flow, the original tab which requested for the password reset link will not receive the `SIGNED_IN` and `PASSWORD_RECOVERY` event when the user clicks on the link.
|
||||
|
||||
examples:
|
||||
- id: listen-to-auth-changes
|
||||
name: Listen to auth changes
|
||||
@@ -405,6 +441,19 @@ functions:
|
||||
console.log(event, session)
|
||||
})
|
||||
```
|
||||
- id: listen-to-password-recovery-events
|
||||
name: Listen to password recovery events
|
||||
code: |
|
||||
```js
|
||||
supabase.auth.onAuthStateChange((event, session) => {
|
||||
if (event == 'PASSWORD_RECOVERY') {
|
||||
console.log('PASSWORD_RECOVERY', session)
|
||||
|
||||
// show screen to update user's password
|
||||
showPasswordResetScreen(true)
|
||||
}
|
||||
})
|
||||
```
|
||||
- id: listen-to-sign-in
|
||||
name: Listen to sign in
|
||||
code: |
|
||||
@@ -437,27 +486,136 @@ functions:
|
||||
if (event == 'USER_UPDATED') console.log('USER_UPDATED', session)
|
||||
})
|
||||
```
|
||||
- id: listen-to-user-deleted
|
||||
name: Listen to user deleted
|
||||
- id: mfa-enroll
|
||||
title: 'mfa.enroll()'
|
||||
$ref: '@supabase/gotrue-js.GoTrueMFAApi.enroll'
|
||||
notes: |
|
||||
- Currently, `totp` is the only supported `factorType`. The returned `id` should be used to create a challenge.
|
||||
- To create a challenge, see [`mfa.challenge()`](/docs/reference/javascript/auth-mfa-challenge).
|
||||
- To verify a challenge, see [`mfa.verify()`](/docs/reference/javascript/auth-mfa-verify).
|
||||
- To create and verify a challenge in a single step, see [`mfa.challengeAndVerify()`](/docs/reference/javascript/auth-mfa-challengeandverify).
|
||||
- To generate a QR code for the `totp` secret in nextjs, you can do the following:
|
||||
```html
|
||||
<Image src={data.totp.qr_code} alt={data.totp.uri} layout="fill"></Image>
|
||||
```
|
||||
examples:
|
||||
- id: enroll-totp-factor
|
||||
name: Enroll a time-based, one-time password (TOTP) factor
|
||||
isSpotlight: true
|
||||
code: |
|
||||
```js
|
||||
supabase.auth.onAuthStateChange((event, session) => {
|
||||
if (event == 'USER_DELETED') console.log('USER_DELETED', session)
|
||||
const { data, error } = await supabase.auth.mfa.enroll({
|
||||
factorType: 'totp'
|
||||
})
|
||||
|
||||
// Use the id to create a challenge.
|
||||
// The challenge can be verified by entering the code generated from the authenticator app.
|
||||
// The code will be generated upon scanning the qr_code or entering the secret into the authenticator app.
|
||||
const { id, type, totp: { qr_code, secret, uri } } = data
|
||||
```
|
||||
- id: mfa-challenge
|
||||
title: 'mfa.challenge()'
|
||||
$ref: '@supabase/gotrue-js.GoTrueMFAApi.challenge'
|
||||
notes: |
|
||||
- An [enrolled factor](/docs/reference/javascript/auth-mfa-enroll) is required before creating a challenge.
|
||||
- To verify a challenge, see [`mfa.verify()`](/docs/reference/javascript/auth-mfa-verify).
|
||||
examples:
|
||||
- id: create-mfa-challenge
|
||||
name: Create a challenge for a factor
|
||||
isSpotlight: true
|
||||
code: |
|
||||
```js
|
||||
const { data, error } = await supabase.auth.mfa.challenge({
|
||||
factorId: '34e770dd-9ff9-416c-87fa-43b31d7ef225'
|
||||
})
|
||||
```
|
||||
- id: listen-to-password-recovery-events
|
||||
name: Listen to password recovery events
|
||||
- id: mfa-verify
|
||||
title: 'mfa.verify()'
|
||||
$ref: '@supabase/gotrue-js.GoTrueMFAApi.verify'
|
||||
notes: |
|
||||
- To verify a challenge, please [create a challenge](/docs/reference/javascript/auth-mfa-challenge) first.
|
||||
examples:
|
||||
- id: verify-challenge
|
||||
name: Verify a challenge for a factor
|
||||
isSpotlight: true
|
||||
code: |
|
||||
```js
|
||||
supabase.auth.onAuthStateChange((event, session) => {
|
||||
if (event == 'PASSWORD_RECOVERY') console.log('PASSWORD_RECOVERY', session)
|
||||
const { data, error } = await supabase.auth.mfa.verify({
|
||||
factorId: '34e770dd-9ff9-416c-87fa-43b31d7ef225',
|
||||
challengeId: '4034ae6f-a8ce-4fb5-8ee5-69a5863a7c15',
|
||||
code: '123456'
|
||||
})
|
||||
```
|
||||
- id: mfa-challenge-and-verify
|
||||
title: 'mfa.challengeAndVerify()'
|
||||
$ref: '@supabase/gotrue-js.GoTrueMFAApi.challengeAndVerify'
|
||||
notes: |
|
||||
- An [enrolled factor](/docs/reference/javascript/auth-mfa-enroll) is required before invoking `challengeAndVerify()`.
|
||||
- Executes [`mfa.challenge()`](/docs/reference/javascript/auth-mfa-challenge) and [`mfa.verify()`](/docs/reference/javascript/auth-mfa-verify) in a single step.
|
||||
examples:
|
||||
- id: challenge-and-verify
|
||||
name: Create and verify a challenge for a factor
|
||||
isSpotlight: true
|
||||
code: |
|
||||
```js
|
||||
const { data, error } = await supabase.auth.mfa.challengeAndVerify({
|
||||
factorId: '34e770dd-9ff9-416c-87fa-43b31d7ef225',
|
||||
challengeId: '4034ae6f-a8ce-4fb5-8ee5-69a5863a7c15',
|
||||
code: '123456'
|
||||
})
|
||||
```
|
||||
- id: mfa-unenroll
|
||||
title: 'mfa.unenroll()'
|
||||
$ref: '@supabase/gotrue-js.GoTrueMFAApi.unenroll'
|
||||
examples:
|
||||
- id: unenroll-a-factor
|
||||
name: Unenroll a factor
|
||||
isSpotlight: true
|
||||
code: |
|
||||
```js
|
||||
const { data, error } = await supabase.auth.mfa.unenroll({
|
||||
factorId: '34e770dd-9ff9-416c-87fa-43b31d7ef225',
|
||||
})
|
||||
```
|
||||
- id: mfa-get-authenticator-assurance-level
|
||||
title: 'mfa.getAuthenticatorAssuranceLevel()'
|
||||
$ref: '@supabase/gotrue-js.GoTrueMFAApi.getAuthenticatorAssuranceLevel'
|
||||
notes: |
|
||||
- Authenticator Assurance Level (AAL) is the measure of the strength of an authentication mechanism.
|
||||
- In Supabase, having an AAL of `aal1` refers to having the 1st factor of authentication such as an email and password or OAuth sign-in while `aal2` refers to the 2nd factor of authentication such as a time-based, one-time-password (TOTP).
|
||||
- If the user has a verified factor, the `nextLevel` field will return `aal2`, else, it will return `aal1`.
|
||||
examples:
|
||||
- id: get-aal
|
||||
name: Get the AAL details of a session
|
||||
isSpotlight: true
|
||||
code: |
|
||||
```js
|
||||
const { data, error } = await supabase.auth.mfa.getAuthenticatorAssuranceLevel()
|
||||
const { currentLevel, nextLevel, currentAuthenticationMethods } = data
|
||||
```
|
||||
- id: admin-api
|
||||
title: 'Overview'
|
||||
notes: |
|
||||
- Any method under the `supabase.auth.admin` namespace requires a `service_role` key.
|
||||
- These methods are considered admin methods and should be called on a trusted server. Never expose your `service_role` key in the browser.
|
||||
examples:
|
||||
- id: create-auth-admin-client
|
||||
name: Create server-side auth client
|
||||
isSpotlight: true
|
||||
code: |
|
||||
```js
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
|
||||
const supabase = createClient(supabase_url, service_role_key, {
|
||||
auth: {
|
||||
autoRefreshToken: false,
|
||||
persistSession: false
|
||||
}
|
||||
})
|
||||
|
||||
// Access auth admin api
|
||||
const adminAuthClient = supabase.auth.admin
|
||||
```
|
||||
|
||||
- id: get-user-by-id
|
||||
title: 'getUserById()'
|
||||
@@ -467,7 +625,7 @@ functions:
|
||||
- The `getUserById()` method requires the user's id which maps to the `auth.users.id` column.
|
||||
examples:
|
||||
- id: fetch-the-user-object-using-the-access-token-jwt
|
||||
name: Fetch the user object using the access_token jwt.
|
||||
name: Fetch the user object using the access_token jwt
|
||||
isSpotlight: true
|
||||
code: |
|
||||
```js
|
||||
@@ -477,14 +635,26 @@ functions:
|
||||
- id: list-users
|
||||
title: 'listUsers()'
|
||||
$ref: '@supabase/gotrue-js.GoTrueAdminApi.listUsers'
|
||||
notes: |
|
||||
- Defaults to return 50 users per page.
|
||||
examples:
|
||||
- id: get-a-full-list-of-users
|
||||
name: Get a full list of users.
|
||||
name: Get a full list of users
|
||||
isSpotlight: true
|
||||
code: |
|
||||
```js
|
||||
const { data: { users }, error } = await supabase.auth.admin.listUsers()
|
||||
```
|
||||
- id: get-paginated-list-of-users
|
||||
name: Paginated list of users
|
||||
isSpotlight: false
|
||||
code: |
|
||||
```js
|
||||
const { data: { users }, error } = await supabase.auth.admin.listUsers({
|
||||
page: 1,
|
||||
perPage: 1000
|
||||
})
|
||||
```
|
||||
- id: create-user
|
||||
title: 'createUser()'
|
||||
$ref: '@supabase/gotrue-js.GoTrueAdminApi.createUser'
|
||||
@@ -492,7 +662,7 @@ functions:
|
||||
- To confirm the user's email address or phone number, set `email_confirm` or `phone_confirm` to true. Both arguments default to false.
|
||||
examples:
|
||||
- id: create-a-new-user-with-custom-user-metadata
|
||||
name: Create a new user with custom user metadata
|
||||
name: With custom user metadata
|
||||
isSpotlight: true
|
||||
code: |
|
||||
```js
|
||||
@@ -554,12 +724,17 @@ functions:
|
||||
title: 'resetPasswordForEmail()'
|
||||
$ref: '@supabase/gotrue-js.GoTrueClient.resetPasswordForEmail'
|
||||
notes: |
|
||||
Sends a password reset request to an email address.
|
||||
When the user clicks the reset link in the email they are redirected back to your application.
|
||||
Prompt the user for a new password and call `auth.updateUser()`:
|
||||
- The password reset flow consist of 2 broad steps: (i) Allow the user to login via the password reset link; (ii) Update the user's password.
|
||||
- The `resetPasswordForEmail()` only sends a password reset link to the user's email.
|
||||
To update the user's password, see [`updateUser()`](/docs/reference/javascript/auth-updateuser).
|
||||
- A `SIGNED_IN` and `PASSWORD_RECOVERY` event will be emitted when the password recovery link is clicked.
|
||||
You can use [`onAuthStateChange()`](/docs/reference/javascript/auth-onauthstatechange) to listen and invoke a callback function on these events.
|
||||
- When the user clicks the reset link in the email they are redirected back to your application.
|
||||
Prompt the user for a new password and call `updateUser()`:
|
||||
```js
|
||||
const { data, error } = await supabase.auth
|
||||
.updateUser({ password: new_password })
|
||||
const { data, error } = await supabase.auth.updateUser({
|
||||
password: new_password
|
||||
})
|
||||
```
|
||||
examples:
|
||||
- id: reset-password
|
||||
@@ -673,7 +848,7 @@ functions:
|
||||
$ref: '@supabase/gotrue-js.GoTrueAdminApi.updateUserById'
|
||||
examples:
|
||||
- id: updates-a-users-email
|
||||
name: Updates a user's email.
|
||||
name: Updates a user's email
|
||||
isSpotlight: false
|
||||
code: |
|
||||
```js
|
||||
@@ -683,7 +858,7 @@ functions:
|
||||
)
|
||||
```
|
||||
- id: updates-a-users-password
|
||||
name: Updates a user's password.
|
||||
name: Updates a user's password
|
||||
isSpotlight: false
|
||||
code: |
|
||||
```js
|
||||
@@ -693,7 +868,7 @@ functions:
|
||||
)
|
||||
```
|
||||
- id: updates-a-users-metadata
|
||||
name: Updates a user's metadata.
|
||||
name: Updates a user's metadata
|
||||
isSpotlight: true
|
||||
code: |
|
||||
```js
|
||||
@@ -703,7 +878,7 @@ functions:
|
||||
)
|
||||
```
|
||||
- id: updates-a-users-app-metadata
|
||||
name: Updates a user's app_metadata.
|
||||
name: Updates a user's app_metadata
|
||||
isSpotlight: false
|
||||
code: |
|
||||
```js
|
||||
@@ -713,7 +888,7 @@ functions:
|
||||
)
|
||||
```
|
||||
- id: confirms-a-users-email-address
|
||||
name: Confirms a user's email address.
|
||||
name: Confirms a user's email address
|
||||
isSpotlight: false
|
||||
code: |
|
||||
```js
|
||||
@@ -723,7 +898,7 @@ functions:
|
||||
)
|
||||
```
|
||||
- id: confirms-a-users-phone-number
|
||||
name: Confirms a user's phone number.
|
||||
name: Confirms a user's phone number
|
||||
isSpotlight: false
|
||||
code: |
|
||||
```js
|
||||
@@ -732,7 +907,31 @@ functions:
|
||||
{ phone_confirm: true }
|
||||
)
|
||||
```
|
||||
|
||||
- id: mfa-list-factors
|
||||
title: 'mfa.listFactors()'
|
||||
$ref: '@supabase/gotrue-js.GoTrueAdminMFAApi.listFactors'
|
||||
examples:
|
||||
- id: list-factors
|
||||
name: List all factors for a user
|
||||
isSpotlight: true
|
||||
code: |
|
||||
```js
|
||||
const { data, error } = await supabase.auth.admin.mfa.listFactors()
|
||||
```
|
||||
- id: mfa-delete-factor
|
||||
title: 'mfa.deleteFactor()'
|
||||
$ref: '@supabase/gotrue-js.GoTrueAdminMFAApi.deleteFactor'
|
||||
examples:
|
||||
- id: delete-factor
|
||||
name: Delete a factor for a user
|
||||
isSpotlight: true
|
||||
code: |
|
||||
```js
|
||||
const { data, error } = await supabase.auth.admin.mfa.deleteFactor({
|
||||
id: '34e770dd-9ff9-416c-87fa-43b31d7ef225',
|
||||
userId: 'a89baba7-b1b7-440f-b4bb-91026967f66b',
|
||||
})
|
||||
```
|
||||
- id: select
|
||||
title: 'Fetch data: select()'
|
||||
$ref: '@supabase/postgrest-js.PostgrestQueryBuilder.select'
|
||||
@@ -6765,7 +6964,7 @@ functions:
|
||||
- Responses are automatically parsed as `json`, `blob` and `form-data` depending on the `Content-Type` header sent by your function. Responses are parsed as `text` by default.
|
||||
examples:
|
||||
- id: basic-invocation
|
||||
name: Basic invocation.
|
||||
name: Basic invocation
|
||||
description:
|
||||
isSpotlight: true
|
||||
code: |
|
||||
@@ -6775,7 +6974,7 @@ functions:
|
||||
})
|
||||
```
|
||||
- id: error-handling
|
||||
name: Error handling.
|
||||
name: Error handling
|
||||
description: |
|
||||
A `FunctionsHttpError` error is returned if your function throws an error, `FunctionsRelayError` if the Supabase Relay has an error processing your function and `FunctionsFetchError` if there is a network error in calling your function.
|
||||
isSpotlight: true
|
||||
@@ -6799,7 +6998,7 @@ functions:
|
||||
}
|
||||
```
|
||||
- id: passing-custom-headers
|
||||
name: Passing custom headers.
|
||||
name: Passing custom headers
|
||||
description: |
|
||||
You can pass custom headers to your function. Note: supabase-js automatically passes the `Authorization` header with the signed in user's JWT.
|
||||
isSpotlight: true
|
||||
|
||||
@@ -1078,17 +1078,17 @@
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"max_rows": {
|
||||
"type": "integer"
|
||||
},
|
||||
"db_schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"max_rows": {
|
||||
"type": "number"
|
||||
},
|
||||
"db_extra_search_path": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["db_schema", "max_rows", "db_extra_search_path"]
|
||||
"required": ["max_rows", "db_schema", "db_extra_search_path"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1130,15 +1130,16 @@
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"max_rows": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 1000000
|
||||
},
|
||||
"db_extra_search_path": {
|
||||
"type": "string"
|
||||
},
|
||||
"db_schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"max_rows": {
|
||||
"type": "number",
|
||||
"maximum": 1000000
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1153,17 +1154,17 @@
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"max_rows": {
|
||||
"type": "integer"
|
||||
},
|
||||
"db_schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"max_rows": {
|
||||
"type": "number"
|
||||
},
|
||||
"db_extra_search_path": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["db_schema", "max_rows", "db_extra_search_path"]
|
||||
"required": ["max_rows", "db_schema", "db_extra_search_path"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2396,30 +2397,31 @@
|
||||
"PostgrestConfigResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"max_rows": {
|
||||
"type": "integer"
|
||||
},
|
||||
"db_schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"max_rows": {
|
||||
"type": "number"
|
||||
},
|
||||
"db_extra_search_path": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["db_schema", "max_rows", "db_extra_search_path"]
|
||||
"required": ["max_rows", "db_schema", "db_extra_search_path"]
|
||||
},
|
||||
"UpdatePostgrestConfigBody": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"max_rows": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 1000000
|
||||
},
|
||||
"db_extra_search_path": {
|
||||
"type": "string"
|
||||
},
|
||||
"db_schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"max_rows": {
|
||||
"type": "number",
|
||||
"maximum": 1000000
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -185,7 +185,7 @@ const PITRSelection = ({}) => {
|
||||
<Button
|
||||
as="span"
|
||||
type="warning"
|
||||
disabled={isSelectedOutOfRange}
|
||||
disabled={isSelectedOutOfRange || !selectedDate}
|
||||
onClick={() => setShowConfirmation(true)}
|
||||
>
|
||||
Review restore details
|
||||
|
||||
@@ -0,0 +1,384 @@
|
||||
import Link from 'next/link'
|
||||
import Image from 'next/image'
|
||||
import { isEmpty } from 'lodash'
|
||||
import { FC, useState } from 'react'
|
||||
import {
|
||||
Collapsible,
|
||||
Input,
|
||||
IconCheck,
|
||||
IconChevronUp,
|
||||
Button,
|
||||
IconEdit,
|
||||
IconTrash,
|
||||
IconEye,
|
||||
IconEyeOff,
|
||||
IconCheckCircle,
|
||||
IconExternalLink,
|
||||
IconLoader,
|
||||
} from 'ui'
|
||||
|
||||
import { useStore } from 'hooks'
|
||||
import { Wrapper } from './Wrappers.types'
|
||||
import { makeValidateRequired } from './Wrappers.utils'
|
||||
import { useFDWCreateMutation } from 'data/fdw/fdw-create-mutation'
|
||||
import { useFDWDeleteMutation } from 'data/fdw/fdw-delete-mutation'
|
||||
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
|
||||
import WrapperTableEditor from './WrapperTableEditor'
|
||||
import InformationBox from 'components/ui/InformationBox'
|
||||
import { confirmAlert } from 'components/to-be-cleaned/ModalsDeprecated/ConfirmModal'
|
||||
|
||||
interface Props {
|
||||
wrapper: Wrapper
|
||||
isLoading: boolean
|
||||
isEnabled: boolean
|
||||
isOpen: boolean
|
||||
onOpen: (wrapper: string) => void
|
||||
}
|
||||
|
||||
const InputField: FC<{ option: any; value: any; error: any; onChange: any }> = ({
|
||||
option,
|
||||
value,
|
||||
error,
|
||||
onChange,
|
||||
}: any) => {
|
||||
const [showHidden, setShowHidden] = useState(!option.hidden)
|
||||
return (
|
||||
<Input
|
||||
key={option.name}
|
||||
id={option.name}
|
||||
name={option.name}
|
||||
label={option.label}
|
||||
defaultValue={option.defaultValue ?? ''}
|
||||
required={option.required ?? false}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
error={error}
|
||||
className="input-mono"
|
||||
type={!option.hidden ? 'text' : showHidden ? 'text' : 'password'}
|
||||
actions={
|
||||
option.hidden ? (
|
||||
<div className="flex items-center justify-center mr-1">
|
||||
<Button
|
||||
type="default"
|
||||
icon={showHidden ? <IconEye /> : <IconEyeOff />}
|
||||
onClick={() => setShowHidden(!showHidden)}
|
||||
/>
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const WrapperRow: FC<Props> = ({ wrapper, isLoading, isEnabled, isOpen, onOpen }) => {
|
||||
const getInitialFormState = () =>
|
||||
Object.fromEntries(
|
||||
wrapper.server.options.map((option) => [option.name, option.defaultValue ?? ''])
|
||||
)
|
||||
|
||||
const { ui } = useStore()
|
||||
const { project } = useProjectContext()
|
||||
const { mutateAsync: createFDW } = useFDWCreateMutation()
|
||||
const { mutateAsync: deleteFDW } = useFDWDeleteMutation()
|
||||
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const [isEditingTable, setIsEditingTable] = useState(false)
|
||||
const [selectedTableToEdit, setSelectedTableToEdit] = useState()
|
||||
|
||||
const [newTables, setNewTables] = useState<any[]>([])
|
||||
const [formState, setFormState] = useState(getInitialFormState)
|
||||
const [formErrors, setFormErrors] = useState<{ [k: string]: string }>({})
|
||||
|
||||
// [Joshen TODO] Fix this logic
|
||||
const noChanges = false
|
||||
// const noChanges = JSON.stringify(getInitialFormState()) === JSON.stringify(formState)
|
||||
|
||||
const onUpdateTable = (values: any) => {
|
||||
setNewTables((prev) => {
|
||||
// if the new values have tableIndex, we are editing an existing table
|
||||
if (values.tableIndex !== undefined) {
|
||||
const tableIndex = values.tableIndex
|
||||
const newTables = [...prev]
|
||||
delete values.tableIndex
|
||||
newTables[tableIndex] = values
|
||||
return newTables
|
||||
}
|
||||
return [...prev, values]
|
||||
})
|
||||
setIsEditingTable(false)
|
||||
setSelectedTableToEdit(undefined)
|
||||
}
|
||||
|
||||
const onSaveWrapper = async () => {
|
||||
const validate = makeValidateRequired(wrapper.server.options)
|
||||
const errors: any = validate(formState)
|
||||
if (newTables.length === 0) errors.tables = 'Please add at least one table'
|
||||
|
||||
if (!isEmpty(errors)) {
|
||||
setFormErrors(errors)
|
||||
return
|
||||
}
|
||||
|
||||
setIsSubmitting(true)
|
||||
try {
|
||||
await createFDW({
|
||||
projectRef: project?.ref,
|
||||
connectionString: project?.connectionString,
|
||||
wrapper,
|
||||
formState,
|
||||
newTables,
|
||||
})
|
||||
ui.setNotification({
|
||||
category: 'success',
|
||||
message: `Successfully created ${wrapper.label} foreign data wrapper`,
|
||||
})
|
||||
setNewTables([])
|
||||
setFormState(getInitialFormState)
|
||||
} catch (error: any) {
|
||||
ui.setNotification({
|
||||
error,
|
||||
category: 'error',
|
||||
message: `Failed to create ${wrapper.label} foreign data wrapper: ${error.message}`,
|
||||
})
|
||||
} finally {
|
||||
setIsSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
const onDeleteWrapper = () => {
|
||||
confirmAlert({
|
||||
title: `Confirm to disable ${wrapper.label} wrapper`,
|
||||
message: `Are you sure you want to disable the ${wrapper.label} wrapper? This will also remove all tables created with this wrapper.`,
|
||||
onAsyncConfirm: async () => {
|
||||
try {
|
||||
await deleteFDW({
|
||||
projectRef: project?.ref,
|
||||
connectionString: project?.connectionString,
|
||||
wrapper,
|
||||
})
|
||||
} catch (error: any) {
|
||||
ui.setNotification({
|
||||
error,
|
||||
category: 'error',
|
||||
message: `Disabling ${wrapper.name} failed: ${error.message}`,
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Collapsible
|
||||
open={isOpen}
|
||||
onOpenChange={() => onOpen(wrapper.name)}
|
||||
className={[
|
||||
'bg-scale-100 dark:bg-scale-300 ',
|
||||
'hover:bg-scale-200 dark:hover:bg-scale-500',
|
||||
'data-open:bg-scale-200 dark:data-open:bg-scale-500',
|
||||
'border-scale-300',
|
||||
'dark:border-scale-500 hover:border-scale-500',
|
||||
'dark:hover:border-scale-700 data-open:border-scale-700',
|
||||
'data-open:pb-px col-span-12 mx-auto',
|
||||
'-space-y-px overflow-hidden',
|
||||
'transition border shadow hover:z-50',
|
||||
'first:rounded-tl first:rounded-tr',
|
||||
'last:rounded-bl last:rounded-br',
|
||||
].join(' ')}
|
||||
>
|
||||
<Collapsible.Trigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center justify-between w-full px-6 py-3 rounded group text-scale-1200"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<IconChevronUp
|
||||
className="transition text-scale-800 data-open-parent:rotate-0 data-closed-parent:rotate-180"
|
||||
strokeWidth={2}
|
||||
width={14}
|
||||
/>
|
||||
<Image
|
||||
src={wrapper.icon}
|
||||
width={20}
|
||||
height={20}
|
||||
alt={`${wrapper.name} wrapper icon`}
|
||||
/>
|
||||
<span className="text-sm capitalize">{wrapper.label}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
{isLoading ? (
|
||||
<div className="flex items-center gap-1 px-1 py-1 text-xs rounded-full">
|
||||
<IconLoader className="animate-spin" size={18} />
|
||||
</div>
|
||||
) : isEnabled ? (
|
||||
<div className="flex items-center gap-1 px-1 py-1 text-xs border rounded-full border-brand-700 bg-brand-200 text-brand-900">
|
||||
<span className="rounded-full bg-brand-900 p-0.5 text-xs text-brand-200">
|
||||
<IconCheck strokeWidth={2} size={12} />
|
||||
</span>
|
||||
<span className="px-1">Enabled</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="px-3 py-1 text-xs border rounded-md border-scale-500 bg-scale-100 text-scale-900 dark:border-scale-700 dark:bg-scale-300">
|
||||
Disabled
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
</Collapsible.Trigger>
|
||||
{!isLoading && (
|
||||
<>
|
||||
{isEnabled ? (
|
||||
<Collapsible.Content>
|
||||
<div className="px-6 py-6 border-t group border-scale-500 bg-scale-100 text-scale-1200 dark:bg-scale-300">
|
||||
<div className="max-w-lg mx-auto my-6 space-y-6">
|
||||
<InformationBox
|
||||
hideCollapse
|
||||
defaultVisibility
|
||||
icon={
|
||||
<IconCheckCircle strokeWidth={1.5} size={18} className="text-brand-900" />
|
||||
}
|
||||
title={`${wrapper.label} foreign data wrapper is currently enabled`}
|
||||
description="If you'd like to edit this wrapper, you'll need to disable the wrapper first and create it again with any updated configuration."
|
||||
/>
|
||||
<div className="flex items-center justify-end !mt-8">
|
||||
<div className="flex items-center space-x-3">
|
||||
<Link href={wrapper.docsUrl}>
|
||||
<a target="_blank">
|
||||
<Button type="default" icon={<IconExternalLink />}>
|
||||
Documentation
|
||||
</Button>
|
||||
</a>
|
||||
</Link>
|
||||
<Button
|
||||
type="default"
|
||||
loading={isSubmitting}
|
||||
disabled={noChanges}
|
||||
onClick={() => onDeleteWrapper()}
|
||||
>
|
||||
Disable wrapper
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Collapsible.Content>
|
||||
) : (
|
||||
<Collapsible.Content>
|
||||
<div className="px-6 py-6 border-t group border-scale-500 bg-scale-100 text-scale-1200 dark:bg-scale-300">
|
||||
<div className="max-w-lg mx-auto my-6 space-y-6">
|
||||
{wrapper.server.options.map((option) => (
|
||||
<InputField
|
||||
option={option}
|
||||
value={formState[option.name]}
|
||||
error={formErrors[option.name]}
|
||||
onChange={(e: any) => {
|
||||
setFormState((prev) => ({ ...prev, [option.name]: e.target.value }))
|
||||
setFormErrors((prev) => ({ ...prev, [option.name]: '' }))
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-sm text-scale-1100">Foreign Tables</p>
|
||||
<Button type="default" onClick={() => setIsEditingTable(true)}>
|
||||
Add table
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{newTables.length === 0 && (
|
||||
<div className="flex items-center justify-center px-4 py-4 border rounded-md border-scale-600">
|
||||
<p className="text-sm text-scale-1000">
|
||||
Add foreign tables to query from after the wrapper is enabled
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{newTables.map((table, i) => (
|
||||
<div className="flex items-center justify-between px-4 py-2 space-y-1 border rounded-md border-scale-600">
|
||||
<div>
|
||||
<p className="text-sm">
|
||||
{table.schema_name}.{table.table_name}
|
||||
</p>
|
||||
<p className="text-sm text-scale-1000">
|
||||
{wrapper.tables[table.index].label}: {table.columns.join(', ')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
type="default"
|
||||
className="px-1"
|
||||
icon={<IconEdit />}
|
||||
onClick={() => {
|
||||
setIsEditingTable(true)
|
||||
setSelectedTableToEdit({ ...table, tableIndex: i })
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
type="default"
|
||||
className="px-1"
|
||||
icon={<IconTrash />}
|
||||
onClick={() => {
|
||||
setNewTables((prev) => prev.filter((_, j) => j !== i))
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{newTables.length === 0 && formErrors.tables && (
|
||||
<p className="text-sm text-red-900">{formErrors.tables}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center justify-between !mt-8">
|
||||
<Link href={wrapper.docsUrl}>
|
||||
<a target="_blank">
|
||||
<Button type="default" icon={<IconExternalLink />}>
|
||||
Documentation
|
||||
</Button>
|
||||
</a>
|
||||
</Link>
|
||||
<div className="flex items-center space-x-3">
|
||||
<Button
|
||||
type="default"
|
||||
htmlType="reset"
|
||||
onClick={() => {
|
||||
onOpen('')
|
||||
setNewTables([])
|
||||
setFormState(getInitialFormState)
|
||||
}}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
loading={isSubmitting}
|
||||
disabled={noChanges}
|
||||
onClick={() => onSaveWrapper()}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Collapsible.Content>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Collapsible>
|
||||
<WrapperTableEditor
|
||||
visible={isEditingTable}
|
||||
tables={wrapper.tables}
|
||||
onCancel={() => {
|
||||
setSelectedTableToEdit(undefined)
|
||||
setIsEditingTable(false)
|
||||
}}
|
||||
onSave={onUpdateTable}
|
||||
initialData={selectedTableToEdit}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default WrapperRow
|
||||
@@ -0,0 +1,231 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Form, IconDatabase, Input, Listbox, SidePanel, Modal, IconPlus } from 'ui'
|
||||
import { useStore } from 'hooks'
|
||||
import { Table, TableOption } from './Wrappers.types'
|
||||
import { makeValidateRequired } from './Wrappers.utils'
|
||||
import MultiSelect from 'components/ui/MultiSelect'
|
||||
import ActionBar from 'components/interfaces/TableGridEditor/SidePanelEditor/ActionBar'
|
||||
|
||||
export type WrapperTableEditorProps = {
|
||||
visible: boolean
|
||||
onCancel: () => void
|
||||
onSave: (values: any) => void
|
||||
|
||||
tables: Table[]
|
||||
initialData: any
|
||||
}
|
||||
|
||||
type OnSubmitFn = (values: any, { resetForm }: { resetForm: () => void }) => void
|
||||
|
||||
const WrapperTableEditor = ({
|
||||
visible,
|
||||
onCancel,
|
||||
onSave,
|
||||
tables,
|
||||
initialData,
|
||||
}: WrapperTableEditorProps) => {
|
||||
const [selectedTableIndex, setSelectedTableIndex] = useState<string>('')
|
||||
|
||||
useEffect(() => {
|
||||
if (initialData) {
|
||||
setSelectedTableIndex(String(initialData.index))
|
||||
}
|
||||
}, [initialData])
|
||||
|
||||
const selectedTable = selectedTableIndex === '' ? undefined : tables[parseInt(selectedTableIndex)]
|
||||
|
||||
const handleCancel = () => {
|
||||
setSelectedTableIndex('')
|
||||
onCancel()
|
||||
}
|
||||
|
||||
const onSubmit: OnSubmitFn = (values, { resetForm }) => {
|
||||
onSave({
|
||||
...values,
|
||||
index: parseInt(selectedTableIndex),
|
||||
schema_name: values.schema === 'custom' ? values.schema_name : values.schema,
|
||||
is_new_schema: values.schema === 'custom',
|
||||
})
|
||||
resetForm()
|
||||
setSelectedTableIndex('')
|
||||
}
|
||||
|
||||
return (
|
||||
<SidePanel
|
||||
key="WrapperTableEditor"
|
||||
size="medium"
|
||||
visible={visible}
|
||||
onCancel={handleCancel}
|
||||
header={<span>Edit foreign table</span>}
|
||||
customFooter={
|
||||
<ActionBar
|
||||
backButtonLabel="Cancel"
|
||||
applyButtonLabel="Save"
|
||||
formId="wrapper-table-editor-form"
|
||||
closePanel={handleCancel}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<SidePanel.Content>
|
||||
<div className="mt-4 space-y-6">
|
||||
<Listbox
|
||||
label="Select a target the table will point to"
|
||||
value={selectedTableIndex}
|
||||
onChange={(value) => setSelectedTableIndex(value)}
|
||||
>
|
||||
<Listbox.Option key="empty" value="" label="---">
|
||||
---
|
||||
</Listbox.Option>
|
||||
|
||||
{tables.map((table, i) => {
|
||||
return (
|
||||
<Listbox.Option
|
||||
className="group"
|
||||
key={String(i)}
|
||||
value={String(i)}
|
||||
label={table.label}
|
||||
>
|
||||
<div className="space-y-1">
|
||||
<p>{table.label}</p>
|
||||
<p className="opacity-50">{table.description}</p>
|
||||
</div>
|
||||
</Listbox.Option>
|
||||
)
|
||||
})}
|
||||
</Listbox>
|
||||
|
||||
{selectedTable && (
|
||||
<TableForm table={selectedTable} onSubmit={onSubmit} initialData={initialData} />
|
||||
)}
|
||||
</div>
|
||||
</SidePanel.Content>
|
||||
</SidePanel>
|
||||
)
|
||||
}
|
||||
|
||||
export default WrapperTableEditor
|
||||
|
||||
const Option = ({ option }: { option: TableOption }) => {
|
||||
return (
|
||||
<Input
|
||||
key={option.name}
|
||||
id={option.name}
|
||||
name={option.name}
|
||||
label={option.label}
|
||||
placeholder={option.placeholder ?? ''}
|
||||
defaultValue={option.defaultValue ?? ''}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const TableForm = ({
|
||||
table,
|
||||
onSubmit,
|
||||
initialData,
|
||||
}: {
|
||||
table: Table
|
||||
onSubmit: OnSubmitFn
|
||||
initialData: any
|
||||
}) => {
|
||||
const { meta } = useStore()
|
||||
const schemas = meta.schemas.list()
|
||||
|
||||
const requiredOptions =
|
||||
table.options.filter((option) => option.editable && option.required && !option.defaultValue) ??
|
||||
[]
|
||||
const optionalOptions =
|
||||
table.options.filter(
|
||||
(option) => option.editable && (!option.required || option.defaultValue)
|
||||
) ?? []
|
||||
|
||||
const initialValues = initialData ?? {
|
||||
table_name: '',
|
||||
columns: table.availableColumns.map((column) => column.name),
|
||||
...Object.fromEntries(table.options.map((option) => [option.name, option.defaultValue ?? ''])),
|
||||
schema: 'public',
|
||||
schema_name: '',
|
||||
}
|
||||
|
||||
const validate = makeValidateRequired([
|
||||
...table.options,
|
||||
{ name: 'table_name', required: true },
|
||||
{ name: 'columns', required: true },
|
||||
])
|
||||
|
||||
return (
|
||||
<Form
|
||||
id="wrapper-table-editor-form"
|
||||
initialValues={initialValues}
|
||||
validate={validate}
|
||||
onSubmit={onSubmit}
|
||||
enableReinitialize={true}
|
||||
>
|
||||
{({ errors, values, resetForm }: any) => {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<Listbox size="small" name="schema" label="Select a schema for the foreign table">
|
||||
<Listbox.Option
|
||||
key="custom"
|
||||
id="custom"
|
||||
label={`Create a new schema`}
|
||||
value="custom"
|
||||
addOnBefore={() => <IconPlus size={16} strokeWidth={1.5} />}
|
||||
>
|
||||
Create a new schema
|
||||
</Listbox.Option>
|
||||
<Modal.Separator />
|
||||
{/* @ts-ignore */}
|
||||
{schemas.map((schema: PostgresSchema) => {
|
||||
return (
|
||||
<Listbox.Option
|
||||
key={schema.id}
|
||||
id={schema.name}
|
||||
label={schema.name}
|
||||
value={schema.name}
|
||||
addOnBefore={() => <IconDatabase size={16} strokeWidth={1.5} />}
|
||||
>
|
||||
{schema.name}
|
||||
</Listbox.Option>
|
||||
)
|
||||
})}
|
||||
</Listbox>
|
||||
{values.schema === 'custom' && (
|
||||
<Input id="schema_name" name="schema_name" label="Schema name" />
|
||||
)}
|
||||
<Input
|
||||
id="table_name"
|
||||
name="table_name"
|
||||
label="Table name"
|
||||
descriptionText="You can query from this table after the wrapper is enabled."
|
||||
/>
|
||||
{requiredOptions.map((option) => (
|
||||
<Option key={option.name} option={option} />
|
||||
))}
|
||||
<MultiSelect
|
||||
label="Columns"
|
||||
options={table.availableColumns.map((column) => {
|
||||
return {
|
||||
id: column.name,
|
||||
value: column.name,
|
||||
name: column.name,
|
||||
description: column.type,
|
||||
disabled: false,
|
||||
}
|
||||
})}
|
||||
error={errors.columns}
|
||||
value={values.columns}
|
||||
placeholder="Select at least one column"
|
||||
searchPlaceholder="Search for a column"
|
||||
onChange={(columns) => {
|
||||
resetForm({ values: { ...values, columns } })
|
||||
}}
|
||||
/>
|
||||
{optionalOptions.map((option) => (
|
||||
<Option key={option.name} option={option} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,536 @@
|
||||
import { Wrapper } from './Wrappers.types'
|
||||
|
||||
export const WRAPPERS: Wrapper[] = [
|
||||
{
|
||||
name: 'stripe_wrapper',
|
||||
handlerName: 'stripe_fdw_handler',
|
||||
validatorName: 'stripe_fdw_validator',
|
||||
icon: '/img/icons/stripe-icon.svg',
|
||||
extensionName: 'StripeFdw',
|
||||
label: 'Stripe',
|
||||
docsUrl: 'https://supabase.github.io/wrappers/stripe/',
|
||||
server: {
|
||||
name: 'stripe_server',
|
||||
options: [
|
||||
{
|
||||
name: 'api_key_id',
|
||||
label: 'Stripe Secret Key',
|
||||
required: true,
|
||||
encrypted: true,
|
||||
hidden: true,
|
||||
},
|
||||
{
|
||||
name: 'api_url',
|
||||
label: 'Stripe API URL',
|
||||
defaultValue: 'https://api.stripe.com/v1',
|
||||
required: false,
|
||||
encrypted: false,
|
||||
hidden: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
tables: [
|
||||
{
|
||||
label: 'Balance',
|
||||
description: 'The balance currently on your Stripe account',
|
||||
availableColumns: [
|
||||
{
|
||||
name: 'balance_type',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'amount',
|
||||
type: 'bigint',
|
||||
},
|
||||
{
|
||||
name: 'currency',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'attrs',
|
||||
type: 'jsonb',
|
||||
},
|
||||
],
|
||||
options: [
|
||||
{
|
||||
name: 'object',
|
||||
defaultValue: 'balance',
|
||||
editable: false,
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Balance Transactions',
|
||||
description: 'Transactions that have contributed to the balance on your Stripe account',
|
||||
availableColumns: [
|
||||
{
|
||||
name: 'id',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'amount',
|
||||
type: 'bigint',
|
||||
},
|
||||
{
|
||||
name: 'currency',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'fee',
|
||||
type: 'bigint',
|
||||
},
|
||||
{
|
||||
name: 'net',
|
||||
type: 'bigint',
|
||||
},
|
||||
{
|
||||
name: 'status',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'type',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'created',
|
||||
type: 'timestamp',
|
||||
},
|
||||
{
|
||||
name: 'attrs',
|
||||
type: 'jsonb',
|
||||
},
|
||||
],
|
||||
options: [
|
||||
{
|
||||
name: 'object',
|
||||
defaultValue: 'balance_transactions',
|
||||
editable: false,
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Charges',
|
||||
description: 'Charges made on your Stripe account',
|
||||
availableColumns: [
|
||||
{
|
||||
name: 'id',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'amount',
|
||||
type: 'bigint',
|
||||
},
|
||||
{
|
||||
name: 'currency',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'customer',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'invoice',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'payment_intent',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'status',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'created',
|
||||
type: 'timestamp',
|
||||
},
|
||||
{
|
||||
name: 'attrs',
|
||||
type: 'jsonb',
|
||||
},
|
||||
],
|
||||
options: [
|
||||
{
|
||||
name: 'object',
|
||||
defaultValue: 'charges',
|
||||
editable: false,
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Customers',
|
||||
description: 'Customers on your Stripe account',
|
||||
availableColumns: [
|
||||
{
|
||||
name: 'id',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'email',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'created',
|
||||
type: 'timestamp',
|
||||
},
|
||||
{
|
||||
name: 'attrs',
|
||||
type: 'jsonb',
|
||||
},
|
||||
],
|
||||
options: [
|
||||
{
|
||||
name: 'object',
|
||||
defaultValue: 'customers',
|
||||
editable: false,
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'rowid_column',
|
||||
label: 'Row ID Column',
|
||||
defaultValue: 'id',
|
||||
editable: true,
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Invoices',
|
||||
description: 'Invoices on your Stripe account',
|
||||
availableColumns: [
|
||||
{
|
||||
name: 'id',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'customer',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'subscription',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'status',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'total',
|
||||
type: 'bigint',
|
||||
},
|
||||
{
|
||||
name: 'currency',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'period_start',
|
||||
type: 'timestamp',
|
||||
},
|
||||
{
|
||||
name: 'period_end',
|
||||
type: 'timestamp',
|
||||
},
|
||||
{
|
||||
name: 'attrs',
|
||||
type: 'jsonb',
|
||||
},
|
||||
],
|
||||
options: [
|
||||
{
|
||||
name: 'object',
|
||||
defaultValue: 'invoices',
|
||||
editable: false,
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Payment Intents',
|
||||
description: 'Payment Intents on your Stripe account',
|
||||
availableColumns: [
|
||||
{
|
||||
name: 'id',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'customer',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'amount',
|
||||
type: 'bigint',
|
||||
},
|
||||
{
|
||||
name: 'currency',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'payment_method',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'created',
|
||||
type: 'timestamp',
|
||||
},
|
||||
{
|
||||
name: 'attrs',
|
||||
type: 'jsonb',
|
||||
},
|
||||
],
|
||||
options: [
|
||||
{
|
||||
name: 'object',
|
||||
defaultValue: 'payment_intents',
|
||||
editable: false,
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Products',
|
||||
description: 'Products on your Stripe account',
|
||||
availableColumns: [
|
||||
{
|
||||
name: 'id',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'active',
|
||||
type: 'bool',
|
||||
},
|
||||
{
|
||||
name: 'default_price',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'created',
|
||||
type: 'timestamp',
|
||||
},
|
||||
{
|
||||
name: 'updated',
|
||||
type: 'timestamp',
|
||||
},
|
||||
{
|
||||
name: 'attrs',
|
||||
type: 'jsonb',
|
||||
},
|
||||
],
|
||||
options: [
|
||||
{
|
||||
name: 'object',
|
||||
defaultValue: 'products',
|
||||
editable: false,
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'rowid_column',
|
||||
label: 'Row ID Column',
|
||||
defaultValue: 'id',
|
||||
editable: true,
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Subscriptions',
|
||||
description: 'Subscriptions on your Stripe account',
|
||||
availableColumns: [
|
||||
{
|
||||
name: 'id',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'customer',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'currency',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'current_period_start',
|
||||
type: 'timestamp',
|
||||
},
|
||||
{
|
||||
name: 'current_period_end',
|
||||
type: 'timestamp',
|
||||
},
|
||||
{
|
||||
name: 'attrs',
|
||||
type: 'jsonb',
|
||||
},
|
||||
],
|
||||
options: [
|
||||
{
|
||||
name: 'object',
|
||||
defaultValue: 'subscriptions',
|
||||
editable: false,
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'rowid_column',
|
||||
label: 'Row ID Column',
|
||||
defaultValue: 'id',
|
||||
editable: true,
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'firebase_wrapper',
|
||||
handlerName: 'firebase_fdw_handler',
|
||||
validatorName: 'firebase_fdw_validator',
|
||||
icon: '/img/icons/firebase-icon.svg',
|
||||
extensionName: 'FirebaseFdw',
|
||||
label: 'Firebase',
|
||||
docsUrl: 'https://supabase.github.io/wrappers/firebase/',
|
||||
server: {
|
||||
name: 'firebase_server',
|
||||
options: [
|
||||
{
|
||||
name: 'project_id',
|
||||
label: 'Project ID',
|
||||
required: true,
|
||||
encrypted: false,
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
name: 'sa_key_id',
|
||||
label: 'Service Account Key',
|
||||
required: true,
|
||||
encrypted: true,
|
||||
hidden: true,
|
||||
},
|
||||
// NOTE(alaister): this is a valid option, but it may confuse the basic use case
|
||||
// so I'm omitting it for now. We can add back once we make it's use clearer.
|
||||
// {
|
||||
// name: 'access_token',
|
||||
// label: 'OAuth2 token to access Firebase',
|
||||
// required: false,
|
||||
// encrypted: false,
|
||||
// hidden: false,
|
||||
// },
|
||||
],
|
||||
},
|
||||
tables: [
|
||||
{
|
||||
label: 'Users',
|
||||
description: 'Shows your Firebase users',
|
||||
availableColumns: [
|
||||
{
|
||||
name: 'uid',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'email',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'created_at',
|
||||
type: 'timestamp',
|
||||
},
|
||||
{
|
||||
name: 'attrs',
|
||||
type: 'jsonb',
|
||||
},
|
||||
],
|
||||
options: [
|
||||
{
|
||||
name: 'object',
|
||||
defaultValue: 'auth/users',
|
||||
editable: false,
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'base_url',
|
||||
label: 'Base URL',
|
||||
defaultValue: 'https://identitytoolkit.googleapis.com/v1/projects',
|
||||
editable: true,
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'limit',
|
||||
label: 'Limit',
|
||||
defaultValue: '10000',
|
||||
editable: true,
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Firestore Collection',
|
||||
description: 'Map to a Firestore collection',
|
||||
availableColumns: [
|
||||
{
|
||||
name: 'name',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'created_at',
|
||||
type: 'timestamp',
|
||||
},
|
||||
{
|
||||
name: 'updated_at',
|
||||
type: 'timestamp',
|
||||
},
|
||||
{
|
||||
name: 'attrs',
|
||||
type: 'jsonb',
|
||||
},
|
||||
],
|
||||
options: [
|
||||
{
|
||||
name: 'object',
|
||||
label: 'Object',
|
||||
placeholder: 'firestore/[collection_id]',
|
||||
editable: true,
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'base_url',
|
||||
label: 'Base URL',
|
||||
defaultValue: 'https://firestore.googleapis.com/v1beta1/projects',
|
||||
editable: true,
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'limit',
|
||||
label: 'Limit',
|
||||
defaultValue: '10000',
|
||||
editable: true,
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,239 @@
|
||||
import Link from 'next/link'
|
||||
import { useState } from 'react'
|
||||
import { Button, IconExternalLink } from 'ui'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import * as Tooltip from '@radix-ui/react-tooltip'
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
|
||||
import { checkPermissions, useParams, useStore } from 'hooks'
|
||||
import { useFDWsQuery } from 'data/fdw/fdws-query'
|
||||
import { WRAPPERS } from './Wrappers.constants'
|
||||
import WrapperRow from './WrapperRow'
|
||||
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
|
||||
|
||||
const Wrappers = () => {
|
||||
const { ref } = useParams()
|
||||
const { ui, meta } = useStore()
|
||||
const { project } = useProjectContext()
|
||||
const { data, isLoading } = useFDWsQuery({
|
||||
projectRef: project?.ref,
|
||||
connectionString: project?.connectionString,
|
||||
})
|
||||
const enabledWrapperNamesSet = new Set(data?.result.map((fdw) => fdw.name))
|
||||
|
||||
const [open, setOpen] = useState<string>('')
|
||||
const [isEnabling, setIsEnabling] = useState<boolean>(false)
|
||||
|
||||
const wrappersExtension = meta.extensions.byId('wrappers')
|
||||
const vaultExtension = meta.extensions.byId('supabase_vault')
|
||||
const isNotAvailable = wrappersExtension === undefined || vaultExtension === undefined
|
||||
|
||||
const isWrappersEnabled =
|
||||
wrappersExtension !== undefined &&
|
||||
wrappersExtension?.installed_version !== null &&
|
||||
vaultExtension !== undefined &&
|
||||
vaultExtension?.installed_version !== null
|
||||
|
||||
const canToggleWrappers = checkPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'extensions')
|
||||
|
||||
const onEnableWrappers = async () => {
|
||||
if (wrappersExtension === undefined || vaultExtension === undefined) return
|
||||
setIsEnabling(true)
|
||||
|
||||
const requiredExtensions = await Promise.all([
|
||||
await meta.extensions.create({
|
||||
schema: wrappersExtension.schema ?? 'extensions',
|
||||
name: wrappersExtension.name,
|
||||
version: wrappersExtension.default_version,
|
||||
cascade: true,
|
||||
}),
|
||||
await meta.extensions.create({
|
||||
schema: vaultExtension.schema ?? 'vault',
|
||||
name: vaultExtension.name,
|
||||
version: vaultExtension.default_version,
|
||||
cascade: true,
|
||||
}),
|
||||
])
|
||||
const errors = requiredExtensions.filter(
|
||||
(res) => res.error && !res.error.message.includes('already exists')
|
||||
)
|
||||
|
||||
if (errors.length > 0) {
|
||||
ui.setNotification({
|
||||
error: errors,
|
||||
category: 'error',
|
||||
message: `Failed to enable Wrappers for your project: ${errors
|
||||
.map((x) => x.message)
|
||||
.join(', ')}`,
|
||||
})
|
||||
} else {
|
||||
ui.setNotification({
|
||||
category: 'success',
|
||||
message: 'Wrappers is now enabled for your project!',
|
||||
})
|
||||
}
|
||||
|
||||
setIsEnabling(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h3 className="mb-2 text-xl text-scale-1200">Foreign Data Wrappers</h3>
|
||||
<div className="text-sm text-scale-900">
|
||||
Query your data warehouse directly from your database, or third-party APIs using SQL.
|
||||
</div>
|
||||
</div>
|
||||
{/* [Joshen TODO] For when we support multiple instances per wrapper */}
|
||||
{/* <div>
|
||||
<Dropdown
|
||||
side="bottom"
|
||||
align="end"
|
||||
size="small"
|
||||
overlay={
|
||||
<>
|
||||
{WRAPPERS.map((wrapper, idx) => (
|
||||
<>
|
||||
<Dropdown.Item
|
||||
key={wrapper.name}
|
||||
icon={
|
||||
<Image
|
||||
src={wrapper.icon}
|
||||
width={20}
|
||||
height={20}
|
||||
alt={`${wrapper.name} wrapper icon`}
|
||||
/>
|
||||
}
|
||||
onClick={() => {}}
|
||||
>
|
||||
{wrapper.label}
|
||||
</Dropdown.Item>
|
||||
{idx !== WRAPPERS.length - 1 && <Dropdown.Separator />}
|
||||
</>
|
||||
))}
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Button type="primary">Add wrapper</Button>
|
||||
</Dropdown>
|
||||
</div> */}
|
||||
</div>
|
||||
|
||||
{isWrappersEnabled ? (
|
||||
<div>
|
||||
{WRAPPERS.map((wrapper, i) => {
|
||||
return (
|
||||
<WrapperRow
|
||||
key={i}
|
||||
wrapper={wrapper}
|
||||
isLoading={isLoading}
|
||||
isEnabled={enabledWrapperNamesSet.has(wrapper.server.name)}
|
||||
isOpen={open === wrapper.name}
|
||||
onOpen={(wrapperName) => {
|
||||
if (open !== wrapperName) setOpen(wrapperName)
|
||||
else setOpen('')
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<div
|
||||
className="w-full px-12 py-12 bg-white bg-no-repeat border rounded dark:bg-scale-200 border-scale-500"
|
||||
style={{
|
||||
backgroundSize: '45%',
|
||||
backgroundPosition: '105% 40%',
|
||||
backgroundImage: ui.isDarkTheme
|
||||
? 'url("/img/wrappers-dark.png")'
|
||||
: 'url("/img/wrappers-light.png")',
|
||||
}}
|
||||
>
|
||||
<div className="w-3/5 space-y-8">
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-lg">Supabase Wrappers</h4>
|
||||
<p className="text-sm text-scale-1100">
|
||||
Supabase Wrappers is a framework for building Postgres Foreign Data Wrappers (FDW)
|
||||
which connect Postgres to external systems. Query your data warehouse or
|
||||
third-party APIs directly from your database.
|
||||
</p>
|
||||
</div>
|
||||
{isNotAvailable ? (
|
||||
<div className="space-y-4">
|
||||
<div className="rounded border border-scale-500 px-4 py-2 flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-scale-1100 text-sm">
|
||||
Wrappers is not available for this project yet.
|
||||
</p>
|
||||
<p className="text-scale-1000 text-sm">
|
||||
Do reach out to us if you're interested!
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Link
|
||||
href={`/support/new?ref=${ref}&category=sales&subject=Request%20for%20access%20to%20wrappers`}
|
||||
>
|
||||
<a target="_blank">
|
||||
<Button type="primary">Contact us</Button>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 my-1 ml-[1px]">
|
||||
<Link href="https://supabase.com/docs">
|
||||
<a target="_blank">
|
||||
<Button type="default" icon={<IconExternalLink />}>
|
||||
About Wrappers
|
||||
</Button>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center space-x-2">
|
||||
<Link href="https://supabase.com/docs">
|
||||
<a target="_blank">
|
||||
<Button type="default" icon={<IconExternalLink />}>
|
||||
About Wrappers
|
||||
</Button>
|
||||
</a>
|
||||
</Link>
|
||||
<Tooltip.Root delayDuration={0}>
|
||||
<Tooltip.Trigger>
|
||||
<Button
|
||||
type="primary"
|
||||
loading={isEnabling}
|
||||
disabled={isNotAvailable || isEnabling || !canToggleWrappers}
|
||||
onClick={() => onEnableWrappers()}
|
||||
>
|
||||
Enable Wrappers
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
{!canToggleWrappers && (
|
||||
<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',
|
||||
].join(' ')}
|
||||
>
|
||||
<span className="text-xs text-scale-1200">
|
||||
You need additional permissions to enable Wrappers for this project
|
||||
</span>
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
)}
|
||||
</Tooltip.Root>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default observer(Wrappers)
|
||||
@@ -0,0 +1,46 @@
|
||||
export type Wrapper = {
|
||||
name: string
|
||||
handlerName: string
|
||||
validatorName: string
|
||||
icon: string
|
||||
extensionName: string
|
||||
label: string
|
||||
server: Server
|
||||
tables: Table[]
|
||||
docsUrl: string
|
||||
}
|
||||
|
||||
export type ServerOption = {
|
||||
name: string
|
||||
label: string
|
||||
required: boolean
|
||||
encrypted: boolean
|
||||
hidden: boolean
|
||||
defaultValue?: string
|
||||
}
|
||||
|
||||
export type Server = {
|
||||
name: string
|
||||
options: ServerOption[]
|
||||
}
|
||||
|
||||
export type TableOption = {
|
||||
name: string
|
||||
defaultValue?: string
|
||||
editable: boolean
|
||||
required: boolean
|
||||
label?: string
|
||||
placeholder?: string
|
||||
}
|
||||
|
||||
export type Table = {
|
||||
label: string
|
||||
description?: string
|
||||
availableColumns: AvailableColumn[]
|
||||
options: TableOption[]
|
||||
}
|
||||
|
||||
export type AvailableColumn = {
|
||||
name: string
|
||||
type: string
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
export const makeValidateRequired =
|
||||
(options: { name: string; required: boolean }[]) => (values: any) => {
|
||||
const requiredOptionsSet = new Set(
|
||||
options.filter((option) => option.required).map((option) => option.name)
|
||||
)
|
||||
|
||||
const errors = Object.fromEntries(
|
||||
Object.entries(values)
|
||||
.filter(
|
||||
([key, value]) =>
|
||||
requiredOptionsSet.has(key) && (Array.isArray(value) ? value.length < 1 : !value)
|
||||
)
|
||||
.map(([key]) => {
|
||||
if (key === 'table_name') return [key, 'Please provide a name for your table']
|
||||
else if (key === 'columns') return [key, 'Please select at least one column']
|
||||
else return [key, 'This field is required']
|
||||
})
|
||||
)
|
||||
|
||||
return errors
|
||||
}
|
||||
@@ -6,6 +6,8 @@ import RolesSettings from './Roles/RolesSettings'
|
||||
|
||||
import Extensions from './Extensions/Extensions'
|
||||
|
||||
import Wrappers from './Wrappers/Wrappers'
|
||||
|
||||
import PublicationsList from './Publications/PublicationsList'
|
||||
import PublicationsTables from './Publications/PublicationsTables'
|
||||
|
||||
@@ -25,6 +27,7 @@ export {
|
||||
RolesList,
|
||||
RolesSettings,
|
||||
Extensions,
|
||||
Wrappers,
|
||||
PublicationsList,
|
||||
PublicationsTables,
|
||||
BackupsList,
|
||||
|
||||
@@ -19,7 +19,10 @@ const AddNewSecretModal: FC<Props> = ({ visible, onClose }) => {
|
||||
const defaultKey = keys.find((key) => key.name === 'default_vault_key') || keys[0]
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) setSelectedKeyId(defaultKey?.id)
|
||||
if (visible) {
|
||||
setShowSecretValue(false)
|
||||
setSelectedKeyId(defaultKey?.id)
|
||||
}
|
||||
}, [visible])
|
||||
|
||||
const validate = (values: any) => {
|
||||
|
||||
@@ -2,11 +2,10 @@ import Link from 'next/link'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { FC, useState } from 'react'
|
||||
import * as Tooltip from '@radix-ui/react-tooltip'
|
||||
import { Button, IconAlertCircle, IconExternalLink } from 'ui'
|
||||
import { Button, IconExternalLink } from 'ui'
|
||||
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
import { checkPermissions, useParams, useStore } from 'hooks'
|
||||
import InformationBox from 'components/ui/InformationBox'
|
||||
|
||||
interface Props {}
|
||||
|
||||
@@ -77,35 +76,36 @@ const VaultToggle: FC<Props> = () => {
|
||||
</p>
|
||||
</div>
|
||||
{isNotAvailable ? (
|
||||
<InformationBox
|
||||
hideCollapse
|
||||
defaultVisibility
|
||||
icon={<IconAlertCircle strokeWidth={1.5} size={18} />}
|
||||
title="Vault is not available for your project"
|
||||
description={
|
||||
<>
|
||||
<p className="mb-4">
|
||||
Do reach out to us if you're interested in having Vault for this project!
|
||||
<div className="space-y-4">
|
||||
<div className="rounded border border-scale-500 px-4 py-2 flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-scale-1100 text-sm">
|
||||
Vault is not available for this project yet.
|
||||
</p>
|
||||
<div className="flex items-center space-x-2 my-1 ml-[1px]">
|
||||
<Link href="https://supabase.com/docs">
|
||||
<a target="_blank">
|
||||
<Button type="default" icon={<IconExternalLink />}>
|
||||
About Vault
|
||||
</Button>
|
||||
</a>
|
||||
</Link>
|
||||
<Link
|
||||
href={`/support/new?ref=${ref}&category=sales&subject=Request%20for%20access%20to%20vault`}
|
||||
>
|
||||
<a target="_blank">
|
||||
<Button type="primary">Contact us</Button>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<p className="text-scale-1000 text-sm">
|
||||
Do reach out to us if you're interested!
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Link
|
||||
href={`/support/new?ref=${ref}&category=sales&subject=Request%20for%20access%20to%20vault`}
|
||||
>
|
||||
<a target="_blank">
|
||||
<Button type="primary">Contact us</Button>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 my-1 ml-[1px]">
|
||||
<Link href="https://supabase.com/docs">
|
||||
<a target="_blank">
|
||||
<Button type="default" icon={<IconExternalLink />}>
|
||||
About Vault
|
||||
</Button>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center space-x-2">
|
||||
<Link href="https://supabase.com/docs">
|
||||
|
||||
@@ -10,6 +10,7 @@ interface ActionBarProps {
|
||||
backButtonLabel?: string
|
||||
applyFunction?: (resolve: any) => void
|
||||
closePanel: () => void
|
||||
formId?: string
|
||||
}
|
||||
const ActionBar: FC<ActionBarProps> = ({
|
||||
loading = false,
|
||||
@@ -20,6 +21,7 @@ const ActionBar: FC<ActionBarProps> = ({
|
||||
backButtonLabel = 'Back',
|
||||
applyFunction = undefined,
|
||||
closePanel = () => {},
|
||||
formId,
|
||||
}) => {
|
||||
const [isRunning, setIsRunning] = useState(false)
|
||||
|
||||
@@ -53,7 +55,13 @@ const ActionBar: FC<ActionBarProps> = ({
|
||||
) : !hideApply ? (
|
||||
// New solution, when using the Form component, loading is handled by the Form itself
|
||||
// Does not require applyFunction() callback
|
||||
<Button size="small" disabled={disableApply} loading={loading} htmlType="submit">
|
||||
<Button
|
||||
size="small"
|
||||
disabled={disableApply}
|
||||
loading={loading}
|
||||
htmlType="submit"
|
||||
form={formId}
|
||||
>
|
||||
{applyButtonLabel}
|
||||
</Button>
|
||||
) : (
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Project } from 'types'
|
||||
import { ProductMenuGroup } from 'components/ui/ProductMenu/ProductMenu.types'
|
||||
import { useFlag } from 'hooks'
|
||||
|
||||
export const generateDatabaseMenu = (project?: Project): ProductMenuGroup[] => {
|
||||
const ref = project?.ref ?? 'default'
|
||||
@@ -7,6 +8,8 @@ export const generateDatabaseMenu = (project?: Project): ProductMenuGroup[] => {
|
||||
const HOOKS_RELEASED = '2021-07-30T15:33:54.383Z'
|
||||
const showHooksRoute = project?.inserted_at ? project.inserted_at > HOOKS_RELEASED : false
|
||||
|
||||
const foreignDataWrappersEnabled = useFlag('foreignDataWrappers')
|
||||
|
||||
return [
|
||||
{
|
||||
title: 'Database',
|
||||
@@ -32,6 +35,16 @@ export const generateDatabaseMenu = (project?: Project): ProductMenuGroup[] => {
|
||||
url: `/project/${ref}/database/extensions`,
|
||||
items: [],
|
||||
},
|
||||
...(foreignDataWrappersEnabled
|
||||
? [
|
||||
{
|
||||
name: 'Wrappers',
|
||||
key: 'wrappers',
|
||||
url: `/project/${ref}/database/wrappers`,
|
||||
items: [],
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{ name: 'Roles', key: 'roles', url: `/project/${ref}/database/roles`, items: [] },
|
||||
{
|
||||
name: 'Replication',
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { useStore } from 'hooks'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { createContext, PropsWithChildren, useContext, useMemo } from 'react'
|
||||
import { Project } from 'types'
|
||||
|
||||
type ProjectContextType = {
|
||||
project: Project | undefined
|
||||
isLoading: boolean
|
||||
}
|
||||
|
||||
const ProjectContext = createContext<ProjectContextType>({
|
||||
project: undefined,
|
||||
isLoading: true,
|
||||
})
|
||||
|
||||
export default ProjectContext
|
||||
|
||||
export const useProjectContext = () => useContext(ProjectContext)
|
||||
|
||||
type ProjectContextProviderProps = {
|
||||
projectRef: string | undefined
|
||||
}
|
||||
|
||||
export const ProjectContextProvider = observer(
|
||||
({ projectRef, children }: PropsWithChildren<ProjectContextProviderProps>) => {
|
||||
// TODO(alaister): This will eventually be replaced with a react-query query
|
||||
// once we remove mobx
|
||||
const { ui } = useStore()
|
||||
|
||||
const value = useMemo<ProjectContextType>(() => {
|
||||
if (ui.selectedProject?.ref === projectRef) {
|
||||
return {
|
||||
project: ui.selectedProject,
|
||||
isLoading: false,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
project: undefined,
|
||||
isLoading: true,
|
||||
}
|
||||
}, [ui.selectedProject?.ref, projectRef])
|
||||
|
||||
return <ProjectContext.Provider value={value}>{children}</ProjectContext.Provider>
|
||||
}
|
||||
)
|
||||
@@ -2,7 +2,7 @@ import Head from 'next/head'
|
||||
import { FC, ReactNode, PropsWithChildren, Fragment } from 'react'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useStore, withAuth, useFlag } from 'hooks'
|
||||
import { useStore, withAuth, useFlag, useParams } from 'hooks'
|
||||
import { PROJECT_STATUS } from 'lib/constants'
|
||||
|
||||
import Connecting from 'components/ui/Loading'
|
||||
@@ -12,6 +12,7 @@ import LayoutHeader from './LayoutHeader'
|
||||
import ConnectingState from './ConnectingState'
|
||||
import PausingState from './PausingState'
|
||||
import BuildingState from './BuildingState'
|
||||
import { ProjectContextProvider } from './ProjectContext'
|
||||
import RestoringState from './RestoringState'
|
||||
|
||||
interface Props {
|
||||
@@ -32,12 +33,13 @@ const ProjectLayout = ({
|
||||
hideHeader = false,
|
||||
hideIconBar = false,
|
||||
}: PropsWithChildren<Props>) => {
|
||||
const { ref: projectRef } = useParams()
|
||||
const { ui } = useStore()
|
||||
const ongoingIncident = useFlag('ongoingIncident')
|
||||
const projectName = ui.selectedProject?.name
|
||||
|
||||
return (
|
||||
<>
|
||||
<ProjectContextProvider projectRef={projectRef}>
|
||||
<Head>
|
||||
<title>
|
||||
{title ? `${title} | Supabase` : projectName ? `${projectName} | Supabase` : 'Supabase'}
|
||||
@@ -55,14 +57,14 @@ const ProjectLayout = ({
|
||||
</MenuBarWrapper>
|
||||
|
||||
<main
|
||||
className="flex w-full flex-1 flex-col overflow-x-hidden"
|
||||
className="flex flex-col flex-1 w-full overflow-x-hidden"
|
||||
style={{ height: ongoingIncident ? 'calc(100vh - 44px)' : '100vh' }}
|
||||
>
|
||||
{!hideHeader && <LayoutHeader />}
|
||||
<ContentWrapper isLoading={isLoading}>{children}</ContentWrapper>
|
||||
</main>
|
||||
</div>
|
||||
</>
|
||||
</ProjectContextProvider>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ export const BadgeSelected: FC<{ name: string; handleRemove: () => void }> = ({
|
||||
'flex items-center space-x-2 rounded bg-gray-500',
|
||||
'py-0.5 px-2 text-sm',
|
||||
].join(' ')}
|
||||
onClick={(e: any) => e.preventDefault()}
|
||||
>
|
||||
<span>{name}</span>
|
||||
<IconX
|
||||
|
||||
@@ -8,6 +8,7 @@ export interface MultiSelectOption {
|
||||
id: string | number
|
||||
value: string
|
||||
name: string
|
||||
description?: string
|
||||
disabled: boolean
|
||||
}
|
||||
|
||||
@@ -112,7 +113,7 @@ export default function MultiSelect({
|
||||
|
||||
return (
|
||||
<div className={`form-group ${disabled ? 'pointer-events-none opacity-50' : ''}`}>
|
||||
{label && <label>{label}</label>}
|
||||
{label && <label className="!w-full">{label}</label>}
|
||||
<div
|
||||
className={[
|
||||
'form-control form-control--multi-select',
|
||||
@@ -166,7 +167,12 @@ export default function MultiSelect({
|
||||
`${active ? ' dark:bg-green-600 dark:bg-opacity-25' : ''}`,
|
||||
].join(' ')}
|
||||
>
|
||||
<span>{option.name}</span>
|
||||
<div className="flex items-center space-x-2">
|
||||
<p>{option.name}</p>
|
||||
{option.description !== undefined && (
|
||||
<p className="opacity-50">{option.description}</p>
|
||||
)}
|
||||
</div>
|
||||
{active && (
|
||||
<IconCheck
|
||||
size={16}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
|
||||
import { delete_ } from 'lib/common/fetch'
|
||||
import { getCustomDomains } from './custom-domains-query'
|
||||
import { customDomainKeys } from './keys'
|
||||
|
||||
export type CustomDomainDeleteVariables = {
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
|
||||
import { AvailableColumn, Wrapper } from 'components/interfaces/Database/Wrappers/Wrappers.types'
|
||||
import { executeSql } from 'data/sql/execute-sql-query'
|
||||
import { sqlKeys } from 'data/sql/keys'
|
||||
import { useStore } from 'hooks'
|
||||
|
||||
export type FDWCreateVariables = {
|
||||
projectRef?: string
|
||||
connectionString?: string
|
||||
wrapper: Wrapper
|
||||
formState: {
|
||||
[k: string]: string
|
||||
}
|
||||
newTables: any[]
|
||||
}
|
||||
|
||||
export function getFDWCreateSql({
|
||||
wrapper,
|
||||
formState,
|
||||
newTables,
|
||||
}: Pick<FDWCreateVariables, 'wrapper' | 'formState' | 'newTables'>) {
|
||||
const newSchemasSql = newTables
|
||||
.filter((table) => table.is_new_schema)
|
||||
.map((table) => /* SQL */ `create schema if not exists ${table.schema_name};`)
|
||||
.join('\n')
|
||||
|
||||
const createWrapperSql = /* SQL */ `
|
||||
create foreign data wrapper ${wrapper.name}
|
||||
handler ${wrapper.handlerName}
|
||||
validator ${wrapper.validatorName};
|
||||
`
|
||||
|
||||
const encryptedOptions = wrapper.server.options.filter((option) => option.encrypted)
|
||||
const unencryptedOptions = wrapper.server.options.filter((option) => !option.encrypted)
|
||||
|
||||
const createEncryptedKeysSqlArray = encryptedOptions.map((option) => {
|
||||
const key = `${wrapper.name}_${option.name}`
|
||||
// Escape single quotes in postgresql by doubling them up
|
||||
const value = (formState[option.name] || '').replace(/'/g, "''")
|
||||
|
||||
return /* SQL */ `
|
||||
select pgsodium.create_key(
|
||||
name := '${key}'
|
||||
);
|
||||
|
||||
select vault.create_secret (
|
||||
new_secret := '${value}',
|
||||
new_name := '${key}',
|
||||
new_key_id := (select id from pgsodium.valid_key where name = '${key}')
|
||||
);
|
||||
`
|
||||
})
|
||||
|
||||
const createEncryptedKeysSql = createEncryptedKeysSqlArray.join('\n')
|
||||
|
||||
const encryptedOptionsSqlArray = encryptedOptions.map((option) => `${option.name} ''%s''`)
|
||||
const unencryptedOptionsSqlArray = unencryptedOptions.map(
|
||||
(option) => `${option.name} ''${formState[option.name]}''`
|
||||
)
|
||||
const optionsSqlArray = [...encryptedOptionsSqlArray, ...unencryptedOptionsSqlArray].join(',')
|
||||
|
||||
const createServerSql = /* SQL */ `
|
||||
do $$
|
||||
declare
|
||||
${encryptedOptions.map((option) => `v_${option.name} text;`).join('\n')}
|
||||
begin
|
||||
${encryptedOptions
|
||||
.map(
|
||||
(option) =>
|
||||
/* SQL */ `select id into v_${option.name} from pgsodium.valid_key where name = '${wrapper.name}_${option.name}' limit 1;`
|
||||
)
|
||||
.join('\n')}
|
||||
|
||||
execute format(
|
||||
E'create server ${wrapper.server.name}\\n'
|
||||
' foreign data wrapper ${wrapper.name}\\n'
|
||||
' options (\\n'
|
||||
' ${optionsSqlArray}\\n'
|
||||
' );',
|
||||
${encryptedOptions.map((option) => `v_${option.name}`).join(',\n')}
|
||||
);
|
||||
end $$;
|
||||
`
|
||||
|
||||
const createTablesSql = newTables
|
||||
.map((newTable) => {
|
||||
const table = wrapper.tables[newTable.index]
|
||||
|
||||
const columns: AvailableColumn[] = newTable.columns
|
||||
.map((name: string) => table.availableColumns.find((c) => c.name === name))
|
||||
.filter(Boolean)
|
||||
|
||||
return /* SQL */ `
|
||||
create foreign table ${newTable.schema_name}.${newTable.table_name} (
|
||||
${columns.map((column) => `${column.name} ${column.type}`).join(',\n ')}
|
||||
)
|
||||
server ${wrapper.server.name}
|
||||
options (
|
||||
${Object.entries(newTable)
|
||||
.filter(
|
||||
([key]) =>
|
||||
key !== 'table_name' &&
|
||||
key !== 'schema_name' &&
|
||||
key !== 'columns' &&
|
||||
key !== 'index'
|
||||
)
|
||||
.map(([key, value]) => `${key} '${value}'`)
|
||||
.join(',\n ')}
|
||||
);
|
||||
`
|
||||
})
|
||||
.join('\n\n')
|
||||
|
||||
const sql = /* SQL */ `
|
||||
begin;
|
||||
|
||||
${newSchemasSql}
|
||||
|
||||
${createWrapperSql}
|
||||
|
||||
${createEncryptedKeysSql}
|
||||
|
||||
${createServerSql}
|
||||
|
||||
${createTablesSql}
|
||||
|
||||
commit;
|
||||
`
|
||||
|
||||
return sql
|
||||
}
|
||||
|
||||
export async function createFDW({
|
||||
projectRef,
|
||||
connectionString,
|
||||
wrapper,
|
||||
formState,
|
||||
newTables,
|
||||
}: FDWCreateVariables) {
|
||||
const sql = getFDWCreateSql({ wrapper, formState, newTables })
|
||||
|
||||
const { result } = await executeSql({ projectRef, connectionString, sql })
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
type FDWCreateData = Awaited<ReturnType<typeof createFDW>>
|
||||
|
||||
export const useFDWCreateMutation = ({
|
||||
onSuccess,
|
||||
...options
|
||||
}: Omit<UseMutationOptions<FDWCreateData, unknown, FDWCreateVariables>, 'mutationFn'> = {}) => {
|
||||
const queryClient = useQueryClient()
|
||||
const { vault } = useStore()
|
||||
|
||||
return useMutation<FDWCreateData, unknown, FDWCreateVariables>((vars) => createFDW(vars), {
|
||||
async onSuccess(data, variables, context) {
|
||||
const { projectRef } = variables
|
||||
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries(sqlKeys.query(projectRef, ['fdws'])),
|
||||
vault.load(),
|
||||
])
|
||||
|
||||
await onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
|
||||
import { Wrapper } from 'components/interfaces/Database/Wrappers/Wrappers.types'
|
||||
import { executeSql } from 'data/sql/execute-sql-query'
|
||||
import { sqlKeys } from 'data/sql/keys'
|
||||
import { useStore } from 'hooks'
|
||||
|
||||
export type FDWDeleteVariables = {
|
||||
projectRef?: string
|
||||
connectionString?: string
|
||||
wrapper: Wrapper
|
||||
}
|
||||
|
||||
export const getDeleteFDWSql = ({ wrapper }: Pick<FDWDeleteVariables, 'wrapper'>) => {
|
||||
const encryptedOptions = wrapper.server.options.filter((option) => option.encrypted)
|
||||
|
||||
const deleteEncryptedSecretsSqlArray = encryptedOptions.map((option) => {
|
||||
const key = `${wrapper.name}_${option.name}`
|
||||
|
||||
return /* SQL */ `
|
||||
delete from vault.secrets where key_id = (select id from pgsodium.valid_key where name = '${key}');
|
||||
|
||||
delete from pgsodium.key where name = '${key}';
|
||||
`
|
||||
})
|
||||
|
||||
const deleteEncryptedSecretsSql = deleteEncryptedSecretsSqlArray.join('\n')
|
||||
|
||||
const sql = /* SQL */ `
|
||||
begin;
|
||||
|
||||
drop foreign data wrapper if exists ${wrapper.name} cascade;
|
||||
|
||||
${deleteEncryptedSecretsSql}
|
||||
|
||||
commit;
|
||||
`
|
||||
|
||||
return sql
|
||||
}
|
||||
|
||||
export async function deleteFDW({ projectRef, connectionString, wrapper }: FDWDeleteVariables) {
|
||||
if (!projectRef) {
|
||||
throw new Error('projectRef is required')
|
||||
}
|
||||
|
||||
const sql = getDeleteFDWSql({ wrapper })
|
||||
|
||||
const { result } = await executeSql({ projectRef, connectionString, sql })
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
type FDWDeleteData = Awaited<ReturnType<typeof deleteFDW>>
|
||||
|
||||
export const useFDWDeleteMutation = ({
|
||||
onSuccess,
|
||||
...options
|
||||
}: Omit<UseMutationOptions<FDWDeleteData, unknown, FDWDeleteVariables>, 'mutationFn'> = {}) => {
|
||||
const queryClient = useQueryClient()
|
||||
const { vault } = useStore()
|
||||
|
||||
return useMutation<FDWDeleteData, unknown, FDWDeleteVariables>((vars) => deleteFDW(vars), {
|
||||
async onSuccess(data, variables, context) {
|
||||
const { projectRef } = variables
|
||||
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries(sqlKeys.query(projectRef, ['fdws'])),
|
||||
vault.load(),
|
||||
])
|
||||
|
||||
await onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import { UseQueryOptions } from '@tanstack/react-query'
|
||||
import { ExecuteSqlData, useExecuteSqlPrefetch, useExecuteSqlQuery } from '../sql/execute-sql-query'
|
||||
|
||||
export const getFDWsSql = () => {
|
||||
const sql = /* SQL */ `
|
||||
select
|
||||
s.oid as "id",
|
||||
s.srvname as "name",
|
||||
(
|
||||
select jsonb_agg(
|
||||
jsonb_build_object(
|
||||
'id', c.oid,
|
||||
'name', c.relname,
|
||||
'columns', (
|
||||
select jsonb_agg(
|
||||
jsonb_build_object(
|
||||
'name', a.attname,
|
||||
'type', pg_catalog.format_type(a.atttypid, a.atttypmod)
|
||||
)
|
||||
)
|
||||
from pg_catalog.pg_attribute a
|
||||
where a.attrelid = c.oid and a.attnum > 0 and not a.attisdropped
|
||||
)
|
||||
)
|
||||
)
|
||||
from pg_catalog.pg_class c
|
||||
where c.oid = any (select t.ftrelid from pg_catalog.pg_foreign_table t where t.ftserver = s.oid)
|
||||
) as "tables"
|
||||
from pg_catalog.pg_foreign_server s;
|
||||
`
|
||||
|
||||
return sql
|
||||
}
|
||||
|
||||
export type FDWColumn = {
|
||||
name: string
|
||||
type: string
|
||||
}
|
||||
|
||||
export type FDWTable = {
|
||||
id: string
|
||||
name: string
|
||||
columns: FDWColumn[]
|
||||
}
|
||||
|
||||
export type FDW = {
|
||||
id: string
|
||||
name: string
|
||||
tables: FDWTable[]
|
||||
}
|
||||
|
||||
export type FDWsResponse = {
|
||||
result: FDW[]
|
||||
}
|
||||
|
||||
export type FDWsVariables = {
|
||||
projectRef?: string
|
||||
connectionString?: string
|
||||
}
|
||||
|
||||
export type FDWsData = FDWsResponse
|
||||
export type FDWsError = unknown
|
||||
|
||||
export const useFDWsQuery = <TData extends FDWsData = FDWsData>(
|
||||
{ projectRef, connectionString }: FDWsVariables,
|
||||
options: UseQueryOptions<ExecuteSqlData, FDWsError, TData> = {}
|
||||
) =>
|
||||
useExecuteSqlQuery(
|
||||
{
|
||||
projectRef,
|
||||
connectionString,
|
||||
sql: getFDWsSql(),
|
||||
queryKey: ['fdws'],
|
||||
},
|
||||
options
|
||||
)
|
||||
|
||||
export const useFDWsPrefetch = ({ projectRef, connectionString }: FDWsVariables) => {
|
||||
return useExecuteSqlPrefetch({
|
||||
projectRef,
|
||||
connectionString,
|
||||
sql: getFDWsSql(),
|
||||
queryKey: ['fdws'],
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { useMutation, UseMutationOptions } from '@tanstack/react-query'
|
||||
import { executeSql, ExecuteSqlData, ExecuteSqlVariables } from './execute-sql-query'
|
||||
|
||||
/* Execute Query */
|
||||
|
||||
export const useExecuteSqlMutation = ({
|
||||
onSuccess,
|
||||
...options
|
||||
}: Omit<UseMutationOptions<ExecuteSqlData, unknown, ExecuteSqlVariables>, 'mutationFn'> = {}) => {
|
||||
return useMutation<ExecuteSqlData, unknown, ExecuteSqlVariables>((args) => executeSql(args), {
|
||||
async onSuccess(data, variables, context) {
|
||||
await onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import { QueryKey, useQuery, useQueryClient, UseQueryOptions } from '@tanstack/react-query'
|
||||
import md5 from 'blueimp-md5'
|
||||
import { post } from 'lib/common/fetch'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { useCallback } from 'react'
|
||||
import { sqlKeys } from './keys'
|
||||
|
||||
export type ExecuteSqlVariables = {
|
||||
projectRef?: string
|
||||
connectionString?: string
|
||||
sql: string
|
||||
queryKey?: QueryKey
|
||||
}
|
||||
|
||||
export async function executeSql(
|
||||
{ projectRef, connectionString, sql }: ExecuteSqlVariables,
|
||||
signal?: AbortSignal
|
||||
) {
|
||||
if (!projectRef) {
|
||||
throw new Error('projectRef is required')
|
||||
}
|
||||
|
||||
let headers = new Headers()
|
||||
|
||||
if (connectionString) {
|
||||
headers.set('x-connection-encrypted', connectionString)
|
||||
}
|
||||
|
||||
const response = await post(
|
||||
`${API_URL}/pg-meta/${projectRef}/query`,
|
||||
{ query: sql },
|
||||
{ headers: Object.fromEntries(headers), signal }
|
||||
)
|
||||
if (response.error) {
|
||||
throw response.error
|
||||
}
|
||||
|
||||
return { result: response }
|
||||
}
|
||||
|
||||
export type ExecuteSqlData = Awaited<ReturnType<typeof executeSql>>
|
||||
export type ExecuteSqlError = unknown
|
||||
|
||||
export const useExecuteSqlQuery = <TData = ExecuteSqlData>(
|
||||
{ projectRef, connectionString, sql, queryKey }: ExecuteSqlVariables,
|
||||
{ enabled = true, ...options }: UseQueryOptions<ExecuteSqlData, ExecuteSqlError, TData> = {}
|
||||
) =>
|
||||
useQuery<ExecuteSqlData, ExecuteSqlError, TData>(
|
||||
sqlKeys.query(projectRef, queryKey ?? [md5(sql)]),
|
||||
({ signal }) => executeSql({ projectRef, connectionString, sql }, signal),
|
||||
{ enabled: enabled && typeof projectRef !== 'undefined', ...options }
|
||||
)
|
||||
|
||||
export const useExecuteSqlPrefetch = ({
|
||||
projectRef,
|
||||
connectionString,
|
||||
sql,
|
||||
queryKey,
|
||||
}: ExecuteSqlVariables) => {
|
||||
const client = useQueryClient()
|
||||
|
||||
return useCallback(() => {
|
||||
if (projectRef) {
|
||||
client.prefetchQuery(sqlKeys.query(projectRef, queryKey ?? [md5(sql)]), ({ signal }) =>
|
||||
executeSql({ projectRef, connectionString, sql }, signal)
|
||||
)
|
||||
}
|
||||
}, [projectRef])
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { QueryKey } from '@tanstack/react-query'
|
||||
|
||||
export const sqlKeys = {
|
||||
query: (projectRef: string | undefined, queryKey: QueryKey) =>
|
||||
['projects', projectRef, 'query', ...queryKey] as const,
|
||||
}
|
||||
@@ -34,6 +34,7 @@
|
||||
"ajv": "^8.6.3",
|
||||
"analytics-node": "^3.5.0",
|
||||
"awesome-debounce-promise": "^2.1.0",
|
||||
"blueimp-md5": "^2.19.0",
|
||||
"clipboard": "^2.0.8",
|
||||
"common": "*",
|
||||
"config": "*",
|
||||
@@ -103,6 +104,7 @@
|
||||
"@testing-library/dom": "^8.11.1",
|
||||
"@testing-library/react": "^12.1.2",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"@types/blueimp-md5": "^2.18.0",
|
||||
"@types/crypto-js": "^4.0.2",
|
||||
"@types/file-saver": "^2.0.2",
|
||||
"@types/json-logic-js": "^1.2.1",
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
|
||||
import { NextPageWithLayout } from 'types'
|
||||
import { checkPermissions } from 'hooks'
|
||||
import { DatabaseLayout } from 'components/layouts'
|
||||
import { Wrappers } from 'components/interfaces/Database'
|
||||
import NoPermission from 'components/ui/NoPermission'
|
||||
import { FormsContainer } from 'components/ui/Forms'
|
||||
|
||||
const DatabaseWrappers: NextPageWithLayout = () => {
|
||||
const canReadWrappers = checkPermissions(PermissionAction.TENANT_SQL_ADMIN_READ, 'wrappers')
|
||||
if (!canReadWrappers) {
|
||||
return <NoPermission isFullPage resourceText="view foreign data wrappers" />
|
||||
}
|
||||
|
||||
return <Wrappers />
|
||||
}
|
||||
|
||||
DatabaseWrappers.getLayout = (page) => (
|
||||
<DatabaseLayout title="Wrappers">
|
||||
<FormsContainer>{page}</FormsContainer>
|
||||
</DatabaseLayout>
|
||||
)
|
||||
|
||||
export default observer(DatabaseWrappers)
|
||||
@@ -0,0 +1,51 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="111px" height="151px" viewBox="0 0 111 151" version="1.1">
|
||||
<!-- Generator: Sketch 47.1 (45422) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>logo_lockup_firebase_vertical</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs>
|
||||
<linearGradient x1="-3.48177083%" y1="17.0266727%" x2="100.833333%" y2="92.8868033%" id="linearGradient-1">
|
||||
<stop stop-color="#FFFFFF" stop-opacity="0.1" offset="0%"/>
|
||||
<stop stop-color="#FFFFFF" stop-opacity="0" offset="100%"/>
|
||||
</linearGradient>
|
||||
<path d="M95.7984919,33.6246194 C95.5859256,32.3610745 94.6848395,31.3206592 93.4615125,30.9262876 C92.2381855,30.5319161 90.8962602,30.8492378 89.9812105,31.7492661 L72.2727216,49.5651223 L58.2428077,23.0544463 C57.6801918,21.9189555 56.5194912,21.2001264 55.2486188,21.2001264 C53.9777463,21.2001264 52.8170458,21.9189555 52.2544298,23.0544463 L44.6406351,37.5458126 L25.3922776,1.91410025 C23.852409,-0.984173003 19.489448,-0.216983024 18.9761585,3.02226355 L0.497735304,121.16952 L50.0301752,148.873603 C53.1471684,150.578603 56.9223279,150.578603 60.0393211,148.873603 L109.999502,121.16952 L95.7984919,33.6246194 Z" id="path-2"/>
|
||||
</defs>
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="styleguide" transform="translate(-664.000000, -1272.000000)">
|
||||
<g id="Group-16" transform="translate(40.000000, 1187.000000)">
|
||||
<g id="Group-9" transform="translate(114.000000, 0.000000)">
|
||||
<g id="Group-26" transform="translate(405.000000, 1.000000)">
|
||||
<g id="logo_lockup_firebase_vertical" transform="translate(105.193370, 83.977901)">
|
||||
<g id="_Group_">
|
||||
<mask id="mask-3" fill="white">
|
||||
<use xlink:href="#path-2"/>
|
||||
</mask>
|
||||
<use id="_Path_5" fill="url(#linearGradient-1)" fill-rule="nonzero" xlink:href="#path-2"/>
|
||||
<g id="_Group_2" mask="url(#mask-3)">
|
||||
<g transform="translate(-6.346125, -0.160154)">
|
||||
<g id="_Group_3" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<path d="M6.84386044,121.329674 L25.4078319,3.18241769 C25.9211214,-0.0568288873 30.2840824,-0.824018866 31.823951,2.07425439 L50.9867602,37.7059667 L58.600555,23.2146005 C59.189165,22.0849984 60.360174,21.3760986 61.637518,21.3760986 C62.9148621,21.3760986 64.0858711,22.0849984 64.6744811,23.2146005 L116.345627,121.329674 L6.84386044,121.329674 Z" id="_Path_" fill="#FFA000" fill-rule="nonzero"/>
|
||||
<g id="_Group_4" transform="translate(0.000000, 30.119310)">
|
||||
<image id="Bitmap" opacity="0.12" x="0" y="0" width="74.4269822" height="94.6200974" xlink:href="data:image/png;base64,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 truncated
|
||||
<polygon id="Shape" fill="#F57C00" fill-rule="nonzero" points="70.8339555 45.1789654 50.901212 7.58665646 6.84386044 91.2103641"/>
|
||||
</g>
|
||||
</g>
|
||||
<g id="_Group_5" stroke-width="1" fill="none" fill-rule="evenodd" transform="translate(3.421930, 23.299844)">
|
||||
<image id="_Image_" opacity="0.2" x="0" y="0" width="119.767558" height="131.27473" xlink:href="data:image/png;base64,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 truncated
|
||||
<g id="_Group_6" transform="translate(3.421930, 7.387755)" fill-rule="nonzero" fill="#FFCA28">
|
||||
<path d="M109.501767,90.6420753 L95.3007566,3.09717436 C95.0881903,1.8336295 94.1871042,0.793214154 92.9637772,0.398842606 C91.7404502,0.00447105695 90.3985249,0.321792836 89.4834752,1.22182108 L0,90.6420753 L49.5324399,118.346158 C52.6494331,120.051158 56.4245926,120.051158 59.5415858,118.346158 L109.501767,90.6420753 Z" id="_Path_2"/>
|
||||
</g>
|
||||
</g>
|
||||
<path d="M102.144617,33.7847735 C101.932051,32.5212286 101.030965,31.4808133 99.8076376,31.0864418 C98.5843106,30.6920702 97.2423854,31.009392 96.3273356,31.9094202 L78.6188467,49.7252764 L64.5889329,23.2146005 C64.0263169,22.0791096 62.8656164,21.3602806 61.5947439,21.3602806 C60.3238715,21.3602806 59.1631709,22.0791096 58.600555,23.2146005 L50.9867602,37.7059667 L31.7384028,2.07425439 C30.1985342,-0.824018866 25.8355731,-0.0568288873 25.3222836,3.18241769 L6.84386044,121.329674 L6.75831218,121.329674 L6.84386044,121.414918 L7.35714997,121.670648 L78.5332985,50.5777097 L96.3273356,32.8470969 C97.2423854,31.9470686 98.5843106,31.6297468 99.8076376,32.0241184 C101.030965,32.4184899 101.932051,33.4589053 102.144617,34.7224501 L116.174531,121.414918 L116.345627,121.329674 L102.144617,33.7847735 Z M7.01495695,121.159188 L25.4078319,4.034851 C25.9211214,0.795604422 30.2840824,0.0284144437 31.823951,2.9266877 L50.9867602,38.5584 L58.600555,24.0670338 C59.189165,22.9374317 60.360174,22.2285319 61.637518,22.2285319 C62.9148621,22.2285319 64.0858711,22.9374317 64.6744811,24.0670338 L78.362202,49.9810064 L7.01495695,121.159188 Z" id="_Compound_Path_" fill-opacity="0.2" fill="#FFFFFF" fill-rule="nonzero"/>
|
||||
<path d="M66.3854462,148.181324 C63.2684531,149.886324 59.4932935,149.886324 56.3763003,148.181324 L6.92940869,120.562484 L6.84386044,121.329674 L56.3763003,149.033757 C59.4932935,150.738757 63.2684531,150.738757 66.3854462,149.033757 L116.345627,121.329674 L116.260079,120.562484 L66.3854462,148.181324 Z" id="_Path_3" fill="#A52714" fill-rule="nonzero" opacity="0.2"/>
|
||||
</g>
|
||||
</g>
|
||||
<polygon id="_Path_4" mask="url(#mask-3)" points="-26.8777064 -6.69547618 137.374944 -6.69547618 137.374944 156.971719 -26.8777064 156.971719"/>
|
||||
<path d="M95.7984919,33.6246194 C95.5859256,32.3610745 94.6848395,31.3206592 93.4615125,30.9262876 C92.2381855,30.5319161 90.8962602,30.8492378 89.9812105,31.7492661 L72.2727216,49.5651223 L58.2428077,23.0544463 C57.6801918,21.9189555 56.5194912,21.2001264 55.2486188,21.2001264 C53.9777463,21.2001264 52.8170458,21.9189555 52.2544298,23.0544463 L44.6406351,37.5458126 L25.3922776,1.91410025 C23.852409,-0.984173003 19.489448,-0.216983024 18.9761585,3.02226355 L0.497735304,121.16952 L50.0301752,148.873603 C53.1471684,150.578603 56.9223279,150.578603 60.0393211,148.873603 L109.999502,121.16952 L95.7984919,33.6246194 Z" id="_Path_5" fill="url(#linearGradient-1)" fill-rule="nonzero" mask="url(#mask-3)"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 21 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 28.87 28.87"><g data-name="Layer 2"><g data-name="Layer 1"><rect width="28.87" height="28.87" fill="#6772e5" rx="6.48" ry="6.48"/><path fill="#fff" fill-rule="evenodd" d="M13.3 11.2c0-.69.57-1 1.49-1a9.84 9.84 0 0 1 4.37 1.13V7.24a11.6 11.6 0 0 0-4.36-.8c-3.56 0-5.94 1.86-5.94 5 0 4.86 6.68 4.07 6.68 6.17 0 .81-.71 1.07-1.68 1.07A11.06 11.06 0 0 1 9 17.25v4.19a12.19 12.19 0 0 0 4.8 1c3.65 0 6.17-1.8 6.17-5 .03-5.21-6.67-4.27-6.67-6.24z"/></g></g></svg>
|
||||
|
After Width: | Height: | Size: 509 B |
Binary file not shown.
|
After Width: | Height: | Size: 247 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 197 KiB |
Reference in new issue
Block a user