mirror of
https://github.com/supabase/supabase.git
synced 2026-10-12 21:05:06 +03:00
Merge branch 'supabase:master' into master
This commit is contained in:
145 files changed
+16962
-2512
No files matched your search
@@ -146,3 +146,5 @@ To debug code, and to see changes in real time, it is often useful to have a loc
|
||||
```sh
|
||||
http://localhost:8082/
|
||||
```
|
||||
|
||||
For more information on Supabase Studio, see the [Supabase Studio readme](./studio/README.md).
|
||||
@@ -10,9 +10,9 @@ Want to contribute? Why not jump into our GitHub repo and:
|
||||
|
||||
- [Sponsor Supabase](https://github.com/sponsors/supabase).
|
||||
- [Answer Discussions](https://github.com/supabase/supabase/discussions).
|
||||
- Submit an issue.
|
||||
- [Submit an issue](https://github.com/supabase/supabase/issue)
|
||||
- Report a performance issue or a part of the documentation that you find confusing.
|
||||
- Create a pull request.
|
||||
- [Create a pull request](https://github.com/supabase/supabase/pulls)
|
||||
- [Translate our Readme](https://github.com/supabase/supabase/issues/1341).
|
||||
- Try our products and give feedback.
|
||||
- Spread the word if you like what we are doing.
|
||||
@@ -23,7 +23,7 @@ Help maintain Supabase repositories. This includes building client libraries, ma
|
||||
|
||||
### Expert
|
||||
|
||||
Answer user questions on Github [Discussions](https://github.com/supabase/supabase/discussions), Discord, and various other social platforms.
|
||||
Answer user questions on Github [Discussions](https://github.com/supabase/supabase/discussions), [Discord](https://discord.supabase.com), and various other social platforms.
|
||||
|
||||
### Advocate
|
||||
|
||||
@@ -39,7 +39,7 @@ Write guest blog posts, create documentation, and help Supabase global expansion
|
||||
|
||||
### Moderator
|
||||
|
||||
Help us maintain the community guidelines in our GitHub and Community-led communities such as Discord, [Reddit](https://reddit.com/r/Supabase/),
|
||||
Help us maintain the community guidelines in our GitHub and Community-led communities such as [Discord](https://discord.supabase.com), [Reddit](https://reddit.com/r/Supabase/),
|
||||
[StackOverflow](https://stackoverflow.com/questions/tagged/supabase) etc.
|
||||
|
||||
## Benefits for SupaSquad members
|
||||
|
||||
@@ -13,4 +13,8 @@ services:
|
||||
- 5555:8080
|
||||
db:
|
||||
volumes:
|
||||
- /var/lib/postgresql/data
|
||||
- ./dev/data.sql:/docker-entrypoint-initdb.d/data.sql
|
||||
storage:
|
||||
volumes:
|
||||
- /var/lib/storage
|
||||
+13
-14
@@ -1,4 +1,3 @@
|
||||
|
||||
# Usage
|
||||
# Start: docker-compose up
|
||||
# With helpers: docker-compose -f docker-compose.yml -f ./dev/docker-compose.dev.yml up
|
||||
@@ -17,6 +16,7 @@ services:
|
||||
environment:
|
||||
SUPABASE_URL: http://kong:8000
|
||||
STUDIO_PG_META_URL: http://meta:8080
|
||||
SUPABASE_ANON_KEY: ${ANON_KEY}
|
||||
SUPABASE_SERVICE_KEY: ${SERVICE_ROLE_KEY}
|
||||
|
||||
kong:
|
||||
@@ -37,7 +37,7 @@ services:
|
||||
|
||||
auth:
|
||||
container_name: supabase-auth
|
||||
image: supabase/gotrue:v2.2.6
|
||||
image: supabase/gotrue:v2.2.12
|
||||
depends_on:
|
||||
- db
|
||||
restart: unless-stopped
|
||||
@@ -46,7 +46,7 @@ services:
|
||||
GOTRUE_API_PORT: 9999
|
||||
|
||||
GOTRUE_DB_DRIVER: postgres
|
||||
GOTRUE_DB_DATABASE_URL: postgres://postgres:${POSTGRES_PASSWORD}@db:5432/postgres?sslmode=disable&search_path=auth
|
||||
GOTRUE_DB_DATABASE_URL: postgres://postgres:${POSTGRES_PASSWORD}@db:5432/postgres?search_path=auth
|
||||
|
||||
GOTRUE_SITE_URL: ${SITE_URL}
|
||||
GOTRUE_URI_ALLOW_LIST: ${ADDITIONAL_REDIRECT_URLS}
|
||||
@@ -74,20 +74,20 @@ services:
|
||||
|
||||
rest:
|
||||
container_name: supabase-rest
|
||||
image: postgrest/postgrest:v8.0.0.20211102
|
||||
image: postgrest/postgrest:v9.0.0
|
||||
depends_on:
|
||||
- db
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
PGRST_DB_URI: postgres://postgres:${POSTGRES_PASSWORD}@db:5432/postgres
|
||||
PGRST_DB_SCHEMA: public, storage
|
||||
PGRST_DB_SCHEMA: public,storage
|
||||
PGRST_DB_ANON_ROLE: anon
|
||||
PGRST_JWT_SECRET: ${JWT_SECRET}
|
||||
PGRST_DB_USE_LEGACY_GUCS: "false"
|
||||
|
||||
realtime:
|
||||
container_name: supabase-realtime
|
||||
image: supabase/realtime:v0.19.0
|
||||
image: supabase/realtime:v0.19.3
|
||||
depends_on:
|
||||
- db
|
||||
restart: unless-stopped
|
||||
@@ -111,7 +111,7 @@ services:
|
||||
|
||||
storage:
|
||||
container_name: supabase-storage
|
||||
image: supabase/storage-api:v0.9.3
|
||||
image: supabase/storage-api:v0.10.0
|
||||
depends_on:
|
||||
- db
|
||||
- rest
|
||||
@@ -122,12 +122,11 @@ services:
|
||||
POSTGREST_URL: http://rest:3000
|
||||
PGRST_JWT_SECRET: ${JWT_SECRET}
|
||||
DATABASE_URL: postgres://postgres:${POSTGRES_PASSWORD}@db:5432/postgres
|
||||
PGOPTIONS: -c search_path=storage
|
||||
PGOPTIONS: -c search_path=storage,public
|
||||
FILE_SIZE_LIMIT: 52428800
|
||||
STORAGE_BACKEND: file
|
||||
FILE_STORAGE_BACKEND_PATH: /var/lib/storage
|
||||
# TODO: https://github.com/supabase/storage-api/commit/a836fc9666c2434d89ca4b31402f74772d50fb6d
|
||||
PROJECT_REF: stub
|
||||
TENANT_ID: stub
|
||||
# TODO: https://github.com/supabase/storage-api/issues/55
|
||||
REGION: stub
|
||||
GLOBAL_S3_BUCKET: stub
|
||||
@@ -136,7 +135,7 @@ services:
|
||||
|
||||
meta:
|
||||
container_name: supabase-meta
|
||||
image: supabase/postgres-meta:v0.28.0
|
||||
image: supabase/postgres-meta:v0.29.0
|
||||
depends_on:
|
||||
- db
|
||||
restart: unless-stopped
|
||||
@@ -147,13 +146,13 @@ services:
|
||||
|
||||
db:
|
||||
container_name: supabase-db
|
||||
image: supabase/postgres:13.3.0
|
||||
command: postgres -c wal_level=logical
|
||||
image: supabase/postgres:14.1.0
|
||||
command: postgres -c config_file=/etc/postgresql/postgresql.conf
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- ${POSTGRES_PORT}:5432
|
||||
environment:
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
volumes:
|
||||
- /var/lib/postgresql/data
|
||||
- ./volumes/db/data:/var/lib/postgresql/data
|
||||
- ./volumes/db/init:/docker-entrypoint-initdb.d
|
||||
@@ -8,7 +8,7 @@ create user supabase_admin;
|
||||
alter user supabase_admin with superuser createdb createrole replication bypassrls;
|
||||
|
||||
-- Extension namespacing
|
||||
create SCHEMA IF NOT exists extensions;
|
||||
create schema if not exists extensions;
|
||||
create extension if not exists "uuid-ossp" with schema extensions;
|
||||
create extension if not exists pgcrypto with schema extensions;
|
||||
create extension if not exists pgjwt with schema extensions;
|
||||
|
||||
@@ -30,7 +30,7 @@ const signUpSubmitted = (event) => {
|
||||
response.error ? alert(response.error.message) : setToken(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
alert(err.response.text)
|
||||
alert(err)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ const logoutSubmitted = (event) => {
|
||||
|
||||
supabase.auth
|
||||
.signOut()
|
||||
.then((response) => {
|
||||
.then((_response) => {
|
||||
document.querySelector('#access-token').value = ''
|
||||
document.querySelector('#refresh-token').value = ''
|
||||
alert('Logout successful')
|
||||
@@ -69,7 +69,7 @@ const logoutSubmitted = (event) => {
|
||||
}
|
||||
|
||||
function setToken(response) {
|
||||
if (response.data.confirmation_sent_at && !response.data.access_token) {
|
||||
if (response.user.confirmation_sent_at && !response?.data?.access_token) {
|
||||
alert('Confirmation Email Sent')
|
||||
} else {
|
||||
document.querySelector('#access-token').value = response.data.access_token
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"@supabase/supabase-js": "^1.11.15",
|
||||
"@supabase/ui": "^0.26.1",
|
||||
"@tailwindcss/typography": "^0.4.0",
|
||||
"next": "10.2.2",
|
||||
"next": "11.1.3",
|
||||
"react": "17.0.2",
|
||||
"react-dom": "17.0.2"
|
||||
},
|
||||
|
||||
@@ -9,11 +9,21 @@
|
||||
dependencies:
|
||||
"@babel/highlight" "^7.10.4"
|
||||
|
||||
"@babel/helper-plugin-utils@^7.14.5":
|
||||
version "7.14.5"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.14.5.tgz#5ac822ce97eec46741ab70a517971e443a70c5a9"
|
||||
integrity sha512-/37qQCE3K0vvZKwoK4XU/irIJQdIfCJuhU5eKnNxpFDsOkgFaUAwbv+RYw6eYgsC0E4hS7r5KqGULUogqui0fQ==
|
||||
|
||||
"@babel/helper-validator-identifier@^7.14.0":
|
||||
version "7.14.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.0.tgz#d26cad8a47c65286b15df1547319a5d0bcf27288"
|
||||
integrity sha512-V3ts7zMSu5lfiwWDVWzRDGIN+lnCEUdaXgtVHJgLb1rGaA6jMrtB9EmE7L18foXJIE8Un/A/h6NJfGQp/e1J4A==
|
||||
|
||||
"@babel/helper-validator-identifier@^7.14.9":
|
||||
version "7.15.7"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz#220df993bfe904a4a6b02ab4f3385a5ebf6e2389"
|
||||
integrity sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==
|
||||
|
||||
"@babel/highlight@^7.10.4":
|
||||
version "7.14.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.14.0.tgz#3197e375711ef6bf834e67d0daec88e4f46113cf"
|
||||
@@ -23,20 +33,26 @@
|
||||
chalk "^2.0.0"
|
||||
js-tokens "^4.0.0"
|
||||
|
||||
"@babel/runtime@7.12.5":
|
||||
version "7.12.5"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.5.tgz#410e7e487441e1b360c29be715d870d9b985882e"
|
||||
integrity sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg==
|
||||
"@babel/plugin-syntax-jsx@7.14.5":
|
||||
version "7.14.5"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.14.5.tgz#000e2e25d8673cce49300517a3eda44c263e4201"
|
||||
integrity sha512-ohuFIsOMXJnbOMRfX7/w7LocdR6R7whhuRD4ax8IipLcLPlZGJKkBxgHp++U4N/vKyU16/YDQr2f5seajD3jIw==
|
||||
dependencies:
|
||||
"@babel/helper-plugin-utils" "^7.14.5"
|
||||
|
||||
"@babel/runtime@7.15.3":
|
||||
version "7.15.3"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.15.3.tgz#2e1c2880ca118e5b2f9988322bd8a7656a32502b"
|
||||
integrity sha512-OvwMLqNXkCXSz1kSm58sEsNuhqOx/fKpnUnKnFB5v8uDda5bLNEHNgKPvhDN6IU0LDcnHQ90LlJ0Q6jnyBSIBA==
|
||||
dependencies:
|
||||
regenerator-runtime "^0.13.4"
|
||||
|
||||
"@babel/types@7.8.3":
|
||||
version "7.8.3"
|
||||
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.8.3.tgz#5a383dffa5416db1b73dedffd311ffd0788fb31c"
|
||||
integrity sha512-jBD+G8+LWpMBBWvVcdr4QysjUE4mU/syrhN17o1u3gx0/WzJB1kwiVZAXRtWbsIPOwW8pF/YJV5+nmetPzepXg==
|
||||
"@babel/types@7.15.0":
|
||||
version "7.15.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.15.0.tgz#61af11f2286c4e9c69ca8deb5f4375a73c72dcbd"
|
||||
integrity sha512-OBvfqnllOIdX4ojTHpwZbpvz4j3EWyjkZEdmjH0/cgsd6QOdSgU8rLSk6ard/pcW7rlmjdVSX/AWOaORR1uNOQ==
|
||||
dependencies:
|
||||
esutils "^2.0.2"
|
||||
lodash "^4.17.13"
|
||||
"@babel/helper-validator-identifier" "^7.14.9"
|
||||
to-fast-properties "^2.0.0"
|
||||
|
||||
"@fullhuman/postcss-purgecss@^3.1.3":
|
||||
@@ -76,20 +92,25 @@
|
||||
resolved "https://registry.yarnpkg.com/@heroicons/react/-/react-1.0.1.tgz#66d25f6441920bd5c2146ea27fd33995885452dd"
|
||||
integrity sha512-uikw2gKCmqnvjVxitecWfFLMOKyL9BTFcU4VM3hHj9OMwpkCr5Ke+MRMyY2/aQVmsYs4VTq7NCFX05MYwAHi3g==
|
||||
|
||||
"@next/env@10.2.2":
|
||||
version "10.2.2"
|
||||
resolved "https://registry.yarnpkg.com/@next/env/-/env-10.2.2.tgz#cb61f6c29f349815108184c2f1926ada65458e28"
|
||||
integrity sha512-m0xOpl4F9z7R7Yt2OtJoo6ZUsFPdx+kuvZeoctH7T6lO66DmZL3W6MQDxso/ArkH8VOlDPZgeQVVBPf+I7wflA==
|
||||
"@napi-rs/triples@^1.0.3":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@napi-rs/triples/-/triples-1.1.0.tgz#88c35b72e79a20b79bb4c9b3e2817241a1c9f4f9"
|
||||
integrity sha512-XQr74QaLeMiqhStEhLn1im9EOMnkypp7MZOwQhGzqp2Weu5eQJbpPxWxixxlYRKWPOmJjsk6qYfYH9kq43yc2w==
|
||||
|
||||
"@next/polyfill-module@10.2.2":
|
||||
version "10.2.2"
|
||||
resolved "https://registry.yarnpkg.com/@next/polyfill-module/-/polyfill-module-10.2.2.tgz#48d108dd562ed021dd23510c0ffe96badf5f9c04"
|
||||
integrity sha512-0t5Hw1Dr18TWP65qAnakRa8+jza6SAFOz0b2v67s5AVquAwXXlclR4SfUy3ahrRtjCqlbLEE/oFIzCGbyMYfVA==
|
||||
"@next/env@11.1.3":
|
||||
version "11.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@next/env/-/env-11.1.3.tgz#dc698e00259242012955e43a40788fcf21ba9e37"
|
||||
integrity sha512-5+vaeooJuWmICSlmVaAC8KG3O8hwKasACVfkHj58xQuCB5SW0TKW3hWxgxkBuefMBn1nM0yEVPKokXCsYjBtng==
|
||||
|
||||
"@next/react-dev-overlay@10.2.2":
|
||||
version "10.2.2"
|
||||
resolved "https://registry.yarnpkg.com/@next/react-dev-overlay/-/react-dev-overlay-10.2.2.tgz#88fb5013d3df70bd37b854234c41d884e108a149"
|
||||
integrity sha512-uPslFPWvvZ8AdadGdK2/834UnJy6F+7071/ere6QpN88Ngzqx9lDIhjslEeFLRtpyBst4s1YUdbm69btVPdE5w==
|
||||
"@next/polyfill-module@11.1.3":
|
||||
version "11.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@next/polyfill-module/-/polyfill-module-11.1.3.tgz#95163973fe19f1827da32703d1fcb8198fb2c79a"
|
||||
integrity sha512-7yr9cr4a0SrBoVE8psxXWK1wTFc8UzsY8Wc2cWGL7qA0hgtqACHaXC47M1ByJB410hFZenGrpE+KFaT1unQMyw==
|
||||
|
||||
"@next/react-dev-overlay@11.1.3":
|
||||
version "11.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@next/react-dev-overlay/-/react-dev-overlay-11.1.3.tgz#5d08336931e48ebdb07d82b566223d0ee5941d2a"
|
||||
integrity sha512-zIwtMliSUR+IKl917ToFNB+0fD7bI5kYMdjHU/UEKpfIXAZPnXRHHISCvPDsczlr+bRsbjlUFW1CsNiuFedeuQ==
|
||||
dependencies:
|
||||
"@babel/code-frame" "7.12.11"
|
||||
anser "1.4.9"
|
||||
@@ -103,10 +124,37 @@
|
||||
stacktrace-parser "0.1.10"
|
||||
strip-ansi "6.0.0"
|
||||
|
||||
"@next/react-refresh-utils@10.2.2":
|
||||
version "10.2.2"
|
||||
resolved "https://registry.yarnpkg.com/@next/react-refresh-utils/-/react-refresh-utils-10.2.2.tgz#69dd150e3656aebe63fc828916105f1472810488"
|
||||
integrity sha512-OL7r0iz+SiE9SMWcxZocUtEAHv0/TlBWxIE3KjjO1vWSU1r0gMrE2l2RxHfMLIPsl6CjAkcPxoaXlosFsJ2S5w==
|
||||
"@next/react-refresh-utils@11.1.3":
|
||||
version "11.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@next/react-refresh-utils/-/react-refresh-utils-11.1.3.tgz#fc2c1a4f2403db1a0179d31caa0a4cc811b8ab58"
|
||||
integrity sha512-144kD8q2nChw67V3AJJlPQ6NUJVFczyn10bhTynn9o2rY5DEnkzuBipcyMuQl2DqfxMkV7sn+yOCOYbrLCk9zg==
|
||||
|
||||
"@next/swc-darwin-arm64@11.1.3":
|
||||
version "11.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-11.1.3.tgz#062eb7871048fdb313304e42ace5f91402dbc39f"
|
||||
integrity sha512-TwP4krjhs+uU9pesDYCShEXZrLSbJr78p12e7XnLBBaNf20SgWLlVmQUT9gX9KbWan5V0sUbJfmcS8MRNHgYuA==
|
||||
|
||||
"@next/swc-darwin-x64@11.1.3":
|
||||
version "11.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-11.1.3.tgz#8bd515768d02e4c1e0cd80d33f3f29456ee890ee"
|
||||
integrity sha512-ZSWmkg/PxccHFNUSeBdrfaH8KwSkoeUtewXKvuYYt7Ph0yRsbqSyNIvhUezDua96lApiXXq6EL2d1THfeWomvw==
|
||||
|
||||
"@next/swc-linux-x64-gnu@11.1.3":
|
||||
version "11.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-11.1.3.tgz#40030577e6ee272afb0080b45468bea73208f46d"
|
||||
integrity sha512-PrTBN0iZudAuj4jSbtXcdBdmfpaDCPIneG4Oms4zcs93KwMgLhivYW082Mvlgx9QVEiRm7+RkFpIVtG/i7JitA==
|
||||
|
||||
"@next/swc-win32-x64-msvc@11.1.3":
|
||||
version "11.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-11.1.3.tgz#2951cbc127f6ea57032a241fb94439cddb5d2482"
|
||||
integrity sha512-mRwbscVjRoHk+tDY7XbkT5d9FCwujFIQJpGp0XNb1i5OHCSDO8WW/C9cLEWS4LxKRbIZlTLYg1MTXqLQkvva8w==
|
||||
|
||||
"@node-rs/helper@1.2.1":
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@node-rs/helper/-/helper-1.2.1.tgz#e079b05f21ff4329d82c4e1f71c0290e4ecdc70c"
|
||||
integrity sha512-R5wEmm8nbuQU0YGGmYVjEc0OHtYsuXdpRG+Ut/3wZ9XAvQWyThN08bTh2cBJgoZxHQUPtvRfeQuxcAgLuiBISg==
|
||||
dependencies:
|
||||
"@napi-rs/triples" "^1.0.3"
|
||||
|
||||
"@nodelib/fs.scandir@2.1.4":
|
||||
version "2.1.4"
|
||||
@@ -129,18 +177,6 @@
|
||||
"@nodelib/fs.scandir" "2.1.4"
|
||||
fastq "^1.6.0"
|
||||
|
||||
"@opentelemetry/api@0.14.0":
|
||||
version "0.14.0"
|
||||
resolved "https://registry.yarnpkg.com/@opentelemetry/api/-/api-0.14.0.tgz#4e17d8d2f1da72b19374efa7b6526aa001267cae"
|
||||
integrity sha512-L7RMuZr5LzMmZiQSQDy9O1jo0q+DaLy6XpYJfIGfYSfoJA5qzYwUP3sP1uMIQ549DvxAgM3ng85EaPTM/hUHwQ==
|
||||
dependencies:
|
||||
"@opentelemetry/context-base" "^0.14.0"
|
||||
|
||||
"@opentelemetry/context-base@^0.14.0":
|
||||
version "0.14.0"
|
||||
resolved "https://registry.yarnpkg.com/@opentelemetry/context-base/-/context-base-0.14.0.tgz#c67fc20a4d891447ca1a855d7d70fa79a3533001"
|
||||
integrity sha512-sDOAZcYwynHFTbLo6n8kIbLiVF3a3BLkrmehJUyEbT9F+Smbi47kLGS2gG2g0fjBLR/Lr1InPD7kXL7FaTqEkw==
|
||||
|
||||
"@supabase/gotrue-js@^1.15.1":
|
||||
version "1.15.4"
|
||||
resolved "https://registry.yarnpkg.com/@supabase/gotrue-js/-/gotrue-js-1.15.4.tgz#a21ec6aaac18af7b50242d0d358297960a144c52"
|
||||
@@ -329,11 +365,6 @@ available-typed-arrays@^1.0.2:
|
||||
dependencies:
|
||||
array.prototype.filter "^1.0.0"
|
||||
|
||||
babel-plugin-syntax-jsx@6.18.0:
|
||||
version "6.18.0"
|
||||
resolved "https://registry.yarnpkg.com/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz#0af32a9a6e13ca7a3fd5069e62d7b0f58d0d8946"
|
||||
integrity sha1-CvMqmm4Tyno/1QaeYtew9Y0NiUY=
|
||||
|
||||
balanced-match@^1.0.0:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
|
||||
@@ -718,19 +749,19 @@ cssesc@^3.0.0:
|
||||
resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
|
||||
integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==
|
||||
|
||||
cssnano-preset-simple@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/cssnano-preset-simple/-/cssnano-preset-simple-2.0.0.tgz#b55e72cb970713f425560a0e141b0335249e2f96"
|
||||
integrity sha512-HkufSLkaBJbKBFx/7aj5HmCK9Ni/JedRQm0mT2qBzMG/dEuJOLnMt2lK6K1rwOOyV4j9aSY+knbW9WoS7BYpzg==
|
||||
cssnano-preset-simple@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/cssnano-preset-simple/-/cssnano-preset-simple-3.0.0.tgz#e95d0012699ca2c741306e9a3b8eeb495a348dbe"
|
||||
integrity sha512-vxQPeoMRqUT3c/9f0vWeVa2nKQIHFpogtoBvFdW4GQ3IvEJ6uauCP6p3Y5zQDLFcI7/+40FTgX12o7XUL0Ko+w==
|
||||
dependencies:
|
||||
caniuse-lite "^1.0.30001202"
|
||||
|
||||
cssnano-simple@2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/cssnano-simple/-/cssnano-simple-2.0.0.tgz#930d9dcd8ba105c5a62ce719cb00854da58b5c05"
|
||||
integrity sha512-0G3TXaFxlh/szPEG/o3VcmCwl0N3E60XNb9YZZijew5eIs6fLjJuOPxQd9yEBaX2p/YfJtt49i4vYi38iH6/6w==
|
||||
cssnano-simple@3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/cssnano-simple/-/cssnano-simple-3.0.0.tgz#a4b8ccdef4c7084af97e19bc5b93b4ecf211e90f"
|
||||
integrity sha512-oU3ueli5Dtwgh0DyeohcIEE00QVfbPR3HzyXdAl89SfnQG3y0/qcpfLVW+jPIh3/rgMZGwuW96rejZGaYE9eUg==
|
||||
dependencies:
|
||||
cssnano-preset-simple "^2.0.0"
|
||||
cssnano-preset-simple "^3.0.0"
|
||||
|
||||
d@1, d@^1.0.1:
|
||||
version "1.0.1"
|
||||
@@ -922,11 +953,6 @@ escape-string-regexp@^1.0.5:
|
||||
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
|
||||
integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=
|
||||
|
||||
esutils@^2.0.2:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
|
||||
integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
|
||||
|
||||
etag@1.8.1:
|
||||
version "1.8.1"
|
||||
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
|
||||
@@ -1188,6 +1214,13 @@ ieee754@^1.1.4:
|
||||
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
|
||||
integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
|
||||
|
||||
image-size@1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/image-size/-/image-size-1.0.0.tgz#58b31fe4743b1cec0a0ac26f5c914d3c5b2f0750"
|
||||
integrity sha512-JLJ6OwBfO1KcA+TvJT+v8gbE6iWbj24LyDNFgFEN0lzegn6cC6a/p3NIDaepMsJjQjlUWqIC7wJv8lBFxPNjcw==
|
||||
dependencies:
|
||||
queue "6.0.2"
|
||||
|
||||
inflight@^1.0.4:
|
||||
version "1.0.6"
|
||||
resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
|
||||
@@ -1438,7 +1471,7 @@ lodash.uniq@^4.5.0:
|
||||
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
|
||||
integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=
|
||||
|
||||
lodash@^4.17.13, lodash@^4.17.20, lodash@^4.17.21:
|
||||
lodash@^4.17.20, lodash@^4.17.21:
|
||||
version "4.17.21"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
||||
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
||||
@@ -1524,7 +1557,7 @@ ms@2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
|
||||
integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=
|
||||
|
||||
nanoid@^3.1.22, nanoid@^3.1.23:
|
||||
nanoid@^3.1.23:
|
||||
version "3.1.23"
|
||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.23.tgz#f744086ce7c2bc47ee0a8472574d5c78e4183a81"
|
||||
integrity sha512-FiB0kzdP0FFVGDKlRLEQ1BgDzU87dy5NnzjeW9YZNt+/c3+q82EQDUwniSAUxp/F0gFNI1ZhKU1FqYsMuqZVnw==
|
||||
@@ -1541,18 +1574,18 @@ next-tick@~1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c"
|
||||
integrity sha1-yobR/ogoFpsBICCOPchCS524NCw=
|
||||
|
||||
next@10.2.2:
|
||||
version "10.2.2"
|
||||
resolved "https://registry.yarnpkg.com/next/-/next-10.2.2.tgz#aadcc526e420282b4a735b0595041195d748a91d"
|
||||
integrity sha512-HPGSLrflWPvf3zEZSIk/uj0CZ+YYrpZwZS0PFAgXbEwb894iRuAPzglagqlzcCh7lg12RBEaKNIxhrVa5xgjtQ==
|
||||
next@11.1.3:
|
||||
version "11.1.3"
|
||||
resolved "https://registry.yarnpkg.com/next/-/next-11.1.3.tgz#0226b283cb9890e446aea759db8a867de2b279ef"
|
||||
integrity sha512-ud/gKmnKQ8wtHC+pd1ZiqPRa7DdgulPkAk94MbpsspfNliwZkYs9SIYWhlLSyg+c661LzdUI2nZshvrtggSYWA==
|
||||
dependencies:
|
||||
"@babel/runtime" "7.12.5"
|
||||
"@babel/runtime" "7.15.3"
|
||||
"@hapi/accept" "5.0.2"
|
||||
"@next/env" "10.2.2"
|
||||
"@next/polyfill-module" "10.2.2"
|
||||
"@next/react-dev-overlay" "10.2.2"
|
||||
"@next/react-refresh-utils" "10.2.2"
|
||||
"@opentelemetry/api" "0.14.0"
|
||||
"@next/env" "11.1.3"
|
||||
"@next/polyfill-module" "11.1.3"
|
||||
"@next/react-dev-overlay" "11.1.3"
|
||||
"@next/react-refresh-utils" "11.1.3"
|
||||
"@node-rs/helper" "1.2.1"
|
||||
assert "2.0.0"
|
||||
ast-types "0.13.2"
|
||||
browserify-zlib "0.2.0"
|
||||
@@ -1563,13 +1596,14 @@ next@10.2.2:
|
||||
chokidar "3.5.1"
|
||||
constants-browserify "1.0.0"
|
||||
crypto-browserify "3.12.0"
|
||||
cssnano-simple "2.0.0"
|
||||
cssnano-simple "3.0.0"
|
||||
domain-browser "4.19.0"
|
||||
encoding "0.1.13"
|
||||
etag "1.8.1"
|
||||
find-cache-dir "3.3.1"
|
||||
get-orientation "1.1.2"
|
||||
https-browserify "1.0.0"
|
||||
image-size "1.0.0"
|
||||
jest-worker "27.0.0-next.5"
|
||||
native-url "0.3.4"
|
||||
node-fetch "2.6.1"
|
||||
@@ -1579,23 +1613,27 @@ next@10.2.2:
|
||||
p-limit "3.1.0"
|
||||
path-browserify "1.0.1"
|
||||
pnp-webpack-plugin "1.6.4"
|
||||
postcss "8.2.13"
|
||||
postcss "8.2.15"
|
||||
process "0.11.10"
|
||||
prop-types "15.7.2"
|
||||
querystring-es3 "0.2.1"
|
||||
raw-body "2.4.1"
|
||||
react-is "16.13.1"
|
||||
react-is "17.0.2"
|
||||
react-refresh "0.8.3"
|
||||
stream-browserify "3.0.0"
|
||||
stream-http "3.1.1"
|
||||
string_decoder "1.3.0"
|
||||
styled-jsx "3.3.2"
|
||||
styled-jsx "4.0.1"
|
||||
timers-browserify "2.0.12"
|
||||
tty-browserify "0.0.1"
|
||||
use-subscription "1.5.1"
|
||||
util "0.12.3"
|
||||
util "0.12.4"
|
||||
vm-browserify "1.1.2"
|
||||
watchpack "2.1.1"
|
||||
optionalDependencies:
|
||||
"@next/swc-darwin-arm64" "11.1.3"
|
||||
"@next/swc-darwin-x64" "11.1.3"
|
||||
"@next/swc-linux-x64-gnu" "11.1.3"
|
||||
"@next/swc-win32-x64-msvc" "11.1.3"
|
||||
|
||||
node-emoji@^1.8.1:
|
||||
version "1.10.0"
|
||||
@@ -1870,13 +1908,13 @@ postcss-value-parser@^4.1.0:
|
||||
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz#443f6a20ced6481a2bda4fa8532a6e55d789a2cb"
|
||||
integrity sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==
|
||||
|
||||
postcss@8.2.13:
|
||||
version "8.2.13"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.2.13.tgz#dbe043e26e3c068e45113b1ed6375d2d37e2129f"
|
||||
integrity sha512-FCE5xLH+hjbzRdpbRb1IMCvPv9yZx2QnDarBEYSN0N0HYk+TcXsEhwdFcFb+SRWOKzKGErhIEbBK2ogyLdTtfQ==
|
||||
postcss@8.2.15:
|
||||
version "8.2.15"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.2.15.tgz#9e66ccf07292817d226fc315cbbf9bc148fbca65"
|
||||
integrity sha512-2zO3b26eJD/8rb106Qu2o7Qgg52ND5HPjcyQiK2B98O388h43A448LCslC0dI2P97wCAQRJsFvwTRcXxTKds+Q==
|
||||
dependencies:
|
||||
colorette "^1.2.2"
|
||||
nanoid "^3.1.22"
|
||||
nanoid "^3.1.23"
|
||||
source-map "^0.6.1"
|
||||
|
||||
postcss@^6.0.9:
|
||||
@@ -1917,7 +1955,7 @@ process@0.11.10, process@^0.11.10:
|
||||
resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
|
||||
integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI=
|
||||
|
||||
prop-types@15.7.2, prop-types@^15.7.2:
|
||||
prop-types@^15.7.2:
|
||||
version "15.7.2"
|
||||
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
|
||||
integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
|
||||
@@ -1983,6 +2021,13 @@ queue-microtask@^1.2.2:
|
||||
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
|
||||
integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
|
||||
|
||||
queue@6.0.2:
|
||||
version "6.0.2"
|
||||
resolved "https://registry.yarnpkg.com/queue/-/queue-6.0.2.tgz#b91525283e2315c7553d2efa18d83e76432fed65"
|
||||
integrity sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==
|
||||
dependencies:
|
||||
inherits "~2.0.3"
|
||||
|
||||
quick-lru@^5.1.1:
|
||||
version "5.1.1"
|
||||
resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932"
|
||||
@@ -2022,7 +2067,12 @@ react-dom@17.0.2:
|
||||
object-assign "^4.1.1"
|
||||
scheduler "^0.20.2"
|
||||
|
||||
react-is@16.13.1, react-is@^16.8.1:
|
||||
react-is@17.0.2:
|
||||
version "17.0.2"
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
|
||||
integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
|
||||
|
||||
react-is@^16.8.1:
|
||||
version "16.13.1"
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
||||
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
|
||||
@@ -2288,13 +2338,13 @@ strip-ansi@6.0.0:
|
||||
dependencies:
|
||||
ansi-regex "^5.0.0"
|
||||
|
||||
styled-jsx@3.3.2:
|
||||
version "3.3.2"
|
||||
resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-3.3.2.tgz#2474601a26670a6049fb4d3f94bd91695b3ce018"
|
||||
integrity sha512-daAkGd5mqhbBhLd6jYAjYBa9LpxYCzsgo/f6qzPdFxVB8yoGbhxvzQgkC0pfmCVvW3JuAEBn0UzFLBfkHVZG1g==
|
||||
styled-jsx@4.0.1:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-4.0.1.tgz#ae3f716eacc0792f7050389de88add6d5245b9e9"
|
||||
integrity sha512-Gcb49/dRB1k8B4hdK8vhW27Rlb2zujCk1fISrizCcToIs+55B4vmUM0N9Gi4nnVfFZWe55jRdWpAqH1ldAKWvQ==
|
||||
dependencies:
|
||||
"@babel/types" "7.8.3"
|
||||
babel-plugin-syntax-jsx "6.18.0"
|
||||
"@babel/plugin-syntax-jsx" "7.14.5"
|
||||
"@babel/types" "7.15.0"
|
||||
convert-source-map "1.7.0"
|
||||
loader-utils "1.2.3"
|
||||
source-map "0.7.3"
|
||||
@@ -2493,10 +2543,10 @@ util@0.10.3:
|
||||
dependencies:
|
||||
inherits "2.0.1"
|
||||
|
||||
util@0.12.3, util@^0.12.0:
|
||||
version "0.12.3"
|
||||
resolved "https://registry.yarnpkg.com/util/-/util-0.12.3.tgz#971bb0292d2cc0c892dab7c6a5d37c2bec707888"
|
||||
integrity sha512-I8XkoQwE+fPQEhy9v012V+TSdH2kp9ts29i20TaaDUXsg7x/onePbhFJUExBfv/2ay1ZOp/Vsm3nDlmnFGSAog==
|
||||
util@0.12.4, util@^0.12.0:
|
||||
version "0.12.4"
|
||||
resolved "https://registry.yarnpkg.com/util/-/util-0.12.4.tgz#66121a31420df8f01ca0c464be15dfa1d1850253"
|
||||
integrity sha512-bxZ9qtSlGUWSOy9Qa9Xgk11kSslpuZwaxCg4sNIDj6FLucDab2JxnHwyNTCpHMtK1MjoQiWQ6DiUMZYbSrO+Sw==
|
||||
dependencies:
|
||||
inherits "^2.0.3"
|
||||
is-arguments "^1.0.4"
|
||||
|
||||
@@ -4,19 +4,16 @@ import { observer } from 'mobx-react-lite'
|
||||
import { Button, Input, IconSearch, IconX } from '@supabase/ui'
|
||||
|
||||
import { PageContext } from 'pages/project/[ref]/auth/users'
|
||||
import { IS_PLATFORM } from 'lib/constants'
|
||||
import UsersList from './UsersList'
|
||||
import InviteUserModal from './InviteUserModal'
|
||||
|
||||
const Users = () => {
|
||||
const PageState: any = useContext(PageContext)
|
||||
const inviteEnabled =
|
||||
IS_PLATFORM &&
|
||||
semver.gte(
|
||||
// @ts-ignore
|
||||
semver.coerce(PageState.projectKpsVersion || 'kps-v2.5.4'),
|
||||
semver.coerce('kps-v2.5.3')
|
||||
)
|
||||
const inviteEnabled = semver.gte(
|
||||
// @ts-ignore
|
||||
semver.coerce(PageState.projectKpsVersion || 'kps-v2.5.4'),
|
||||
semver.coerce('kps-v2.5.3')
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
PageState.fetchData(1)
|
||||
|
||||
@@ -46,7 +46,11 @@ const UsersList = ({}) => {
|
||||
>
|
||||
<div className="flex items-center space-x-3 opacity-75">
|
||||
<IconAlertCircle size={16} strokeWidth={2} />
|
||||
<Typography.Text type="secondary">No users in your project yet</Typography.Text>
|
||||
<Typography.Text type="secondary">
|
||||
{PageState.filterKeywords
|
||||
? `No users matched the search query "${PageState.filterKeywords}"`
|
||||
: 'No users in your project yet'}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</Table.td>
|
||||
</Table.tr>
|
||||
|
||||
@@ -5,7 +5,6 @@ import { Badge, Typography } from '@supabase/ui'
|
||||
import Table from 'components/to-be-cleaned/Table'
|
||||
|
||||
import SimpleCodeBlock from 'components/to-be-cleaned/SimpleCodeBlock'
|
||||
import { IS_PLATFORM } from 'lib/constants'
|
||||
import UserDropdown from './UserDropdown'
|
||||
|
||||
interface Props {
|
||||
@@ -46,7 +45,9 @@ const UserListItem: FC<Props> = ({ user }) => {
|
||||
{user.id}
|
||||
</SimpleCodeBlock>
|
||||
</Table.td>
|
||||
<Table.td className="text-right">{IS_PLATFORM && <UserDropdown user={user} />}</Table.td>
|
||||
<Table.td className="text-right">
|
||||
<UserDropdown user={user} />
|
||||
</Table.td>
|
||||
</Table.tr>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { FC, useEffect, createContext, useContext, FormEvent } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { makeAutoObservable } from 'mobx'
|
||||
import { observer, useLocalObservable } from 'mobx-react-lite'
|
||||
import { isEmpty, mapValues, has, without, union, filter, keyBy } from 'lodash'
|
||||
import { isEmpty, mapValues, has, without, union, filter, keyBy, isUndefined } from 'lodash'
|
||||
import { Dictionary } from '@supabase/grid'
|
||||
import {
|
||||
Input,
|
||||
@@ -244,7 +244,7 @@ class CreateHookStore implements ICreateHookStore {
|
||||
break
|
||||
}
|
||||
default: {
|
||||
if (has(this.formState, key) && idx) {
|
||||
if (has(this.formState, key) && !isUndefined(idx)) {
|
||||
;(this.formState as any)[key].value[idx] = value
|
||||
} else {
|
||||
;(this.formState as any)[key] = { value: [{ value }] }
|
||||
@@ -357,11 +357,11 @@ function isValidHttpUrl(value: string) {
|
||||
|
||||
const CreateHookContext = createContext<ICreateHookStore | null>(null)
|
||||
|
||||
type CreateHookProps = {
|
||||
interface CreateHookProps {
|
||||
hook?: any
|
||||
visible: boolean
|
||||
setVisible: (value: boolean) => void
|
||||
} & any
|
||||
}
|
||||
|
||||
const CreateHook: FC<CreateHookProps> = ({ hook, visible = true, setVisible }) => {
|
||||
const { meta, ui } = useStore()
|
||||
@@ -724,7 +724,7 @@ const RadioGroupHookService: FC = observer(({}) => {
|
||||
)
|
||||
})
|
||||
|
||||
type RadioHookService = {
|
||||
interface RadioHookService {
|
||||
id: string
|
||||
description: string
|
||||
label: string
|
||||
@@ -910,7 +910,7 @@ const InputMultiServiceHeaders: FC = observer(({}) => {
|
||||
)
|
||||
})
|
||||
|
||||
type InputServiceHeaderProps = {
|
||||
interface InputServiceHeaderProps {
|
||||
idx: number
|
||||
name: string
|
||||
value: string
|
||||
@@ -1019,7 +1019,7 @@ const InputMultiServiceParams: FC = observer(({}) => {
|
||||
)
|
||||
})
|
||||
|
||||
type InputServiceParamsProps = {
|
||||
interface InputServiceParamsProps {
|
||||
idx: number
|
||||
name: string
|
||||
value: string
|
||||
|
||||
@@ -2,15 +2,32 @@ import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import Head from 'next/head'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { Button, Typography } from '@supabase/ui'
|
||||
import { Button, IconGitHub, Typography } from '@supabase/ui'
|
||||
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { useStore } from 'hooks'
|
||||
import { auth, GOTRUE_ENABLED } from 'lib/gotrue'
|
||||
|
||||
const Landing = () => {
|
||||
const { ui } = useStore()
|
||||
const { theme } = ui
|
||||
|
||||
async function handleGithubSignIn() {
|
||||
try {
|
||||
const { error } = await auth.signIn(
|
||||
{
|
||||
provider: 'github',
|
||||
},
|
||||
{
|
||||
redirectTo: process.env.NEXT_PUBLIC_SITE_URL,
|
||||
}
|
||||
)
|
||||
if (error) throw error
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="z-10 relative h-screen flex flex-col">
|
||||
<Head>
|
||||
@@ -38,7 +55,13 @@ const Landing = () => {
|
||||
type="success"
|
||||
className="ml-8 font-medium hover:text-gray-300 focus:outline-none focus:text-gray-300 transition duration-150 ease-in-out"
|
||||
>
|
||||
<Link href={`${API_URL}/login`}>Sign In</Link>
|
||||
{GOTRUE_ENABLED ? (
|
||||
<Button onClick={handleGithubSignIn} icon={<IconGitHub />}>
|
||||
Sign In With Github
|
||||
</Button>
|
||||
) : (
|
||||
<Link href={`${API_URL}/login`}>Sign In</Link>
|
||||
)}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</nav>
|
||||
@@ -55,9 +78,15 @@ const Landing = () => {
|
||||
</p>
|
||||
</Typography.Text>
|
||||
<div className="mt-5 sm:mt-8 sm:flex sm:justify-center space-x-3">
|
||||
<Link href={`${API_URL}/login`}>
|
||||
<Button size="large">Sign In</Button>
|
||||
</Link>
|
||||
{GOTRUE_ENABLED ? (
|
||||
<Button onClick={handleGithubSignIn} size="large" icon={<IconGitHub />}>
|
||||
Sign In With Github
|
||||
</Button>
|
||||
) : (
|
||||
<Link href={`${API_URL}/login`}>
|
||||
<Button size="large">Sign In</Button>
|
||||
</Link>
|
||||
)}
|
||||
<Link href="https://supabase.com/docs">
|
||||
<Button size="large" type="default">
|
||||
Docs
|
||||
|
||||
@@ -26,7 +26,7 @@ const ProjectUsage: FC<Props> = ({ project }) => {
|
||||
const { data, error }: any = useSWR(
|
||||
// only fetch when browser window is active
|
||||
`${API_URL}/projects/${ref}/log-stats`,
|
||||
get,
|
||||
get
|
||||
// increase refresh rate x10 to 30s when focus lost
|
||||
// conditional fetching will cause cached data to clear (not desirable)
|
||||
// { refreshInterval: isActive ? 3000 : 30000 }
|
||||
@@ -46,8 +46,7 @@ const ProjectUsage: FC<Props> = ({ project }) => {
|
||||
<PanelHeader
|
||||
icon={<IconDatabase size="small" />}
|
||||
title="Database"
|
||||
href={'/project/[ref]/editor/table'}
|
||||
as={`/project/${ref}/editor/table`}
|
||||
href={`/project/${ref}/editor`}
|
||||
/>
|
||||
<ChartHandler
|
||||
startDate={startDate}
|
||||
@@ -73,8 +72,7 @@ const ProjectUsage: FC<Props> = ({ project }) => {
|
||||
<PanelHeader
|
||||
icon={<IconKey size="small" />}
|
||||
title="Auth"
|
||||
href={'/project/[ref]/auth/users'}
|
||||
as={`/project/${ref}/auth/users`}
|
||||
href={`/project/${ref}/auth/users`}
|
||||
/>
|
||||
<ChartHandler
|
||||
startDate={startDate}
|
||||
@@ -100,8 +98,7 @@ const ProjectUsage: FC<Props> = ({ project }) => {
|
||||
<PanelHeader
|
||||
icon={<IconArchive size="small" />}
|
||||
title="Storage"
|
||||
href={'/project/[ref]/storage/buckets'}
|
||||
as={`/project/${ref}/storage/buckets`}
|
||||
href={`/project/${ref}/storage/buckets`}
|
||||
/>
|
||||
<ChartHandler
|
||||
startDate={startDate}
|
||||
@@ -157,7 +154,7 @@ export default ProjectUsage
|
||||
const PanelHeader = (props: any) => {
|
||||
const Tag = props?.href ? Link : 'div'
|
||||
return (
|
||||
<Tag href={props.href} as={props.as}>
|
||||
<Tag href={props.href}>
|
||||
<div
|
||||
className={
|
||||
'flex items-center space-x-3 opacity-80 ' +
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
import { FC } from 'react'
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
Dropdown,
|
||||
Typography,
|
||||
IconChevronDown,
|
||||
IconRefreshCw,
|
||||
IconX,
|
||||
Toggle,
|
||||
IconSearch,
|
||||
} from '@supabase/ui'
|
||||
import { LogTemplate } from '.'
|
||||
|
||||
interface Props {
|
||||
searchValue?: string
|
||||
templates?: any
|
||||
isLoading: boolean
|
||||
isCustomQuery: boolean
|
||||
newCount: number
|
||||
showReset: boolean
|
||||
onReset: () => void
|
||||
onRefresh?: () => void
|
||||
onSearch?: (query: string) => void
|
||||
onCustomClick?: () => void
|
||||
onSelectTemplate: (template: LogTemplate) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs control panel header + wrapper
|
||||
*/
|
||||
const LogPanel: FC<Props> = ({
|
||||
searchValue,
|
||||
templates = [],
|
||||
isLoading,
|
||||
isCustomQuery,
|
||||
newCount,
|
||||
showReset,
|
||||
onReset,
|
||||
onRefresh,
|
||||
onSearch = () => {},
|
||||
onCustomClick,
|
||||
onSelectTemplate,
|
||||
}) => {
|
||||
return (
|
||||
<div className="bg-panel-header-light dark:bg-panel-header-dark">
|
||||
<div className="px-2 py-1 flex items-center justify-between w-full">
|
||||
<div className="flex flex-row gap-x-4 items-center">
|
||||
<Button
|
||||
type="text"
|
||||
icon={
|
||||
<div className="relative">
|
||||
{newCount > 0 && (
|
||||
<div
|
||||
className={[
|
||||
'absolute flex items-center justify-center -top-0.5 -right-0.5',
|
||||
'h-3 w-3 z-50',
|
||||
].join(' ')}
|
||||
>
|
||||
<div className="absolute ">
|
||||
<Typography.Text style={{ fontSize: '0.5rem' }}>{newCount}</Typography.Text>
|
||||
</div>
|
||||
<div className="bg-green-500 rounded-full w-full h-full"></div>
|
||||
<div className="absolute top-0 right-0 bg-green-500 rounded-full w-full h-full animate-ping"></div>
|
||||
</div>
|
||||
)}
|
||||
<IconRefreshCw />
|
||||
</div>
|
||||
}
|
||||
loading={isLoading}
|
||||
onClick={onRefresh}
|
||||
>
|
||||
Refresh
|
||||
</Button>
|
||||
<Dropdown
|
||||
side="bottom"
|
||||
align="start"
|
||||
overlay={templates.map((template: LogTemplate) => (
|
||||
<Dropdown.Item key={template.label} onClick={() => onSelectTemplate(template)}>
|
||||
<Typography.Text>{template.label}</Typography.Text>
|
||||
</Dropdown.Item>
|
||||
))}
|
||||
>
|
||||
<Button type="text" iconRight={<IconChevronDown />}>
|
||||
Templates
|
||||
</Button>
|
||||
</Dropdown>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<Typography.Text type="secondary" small>
|
||||
Search logs via query
|
||||
</Typography.Text>
|
||||
<Toggle size="tiny" checked={isCustomQuery} onChange={onCustomClick} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-x-4">
|
||||
<Input
|
||||
size="tiny"
|
||||
icon={<IconSearch size={16} />}
|
||||
placeholder="Search event messages"
|
||||
onChange={(e) => onSearch(e.target.value)}
|
||||
value={searchValue}
|
||||
actions={
|
||||
showReset && <IconX size="tiny" className="cursor-pointer mx-1" title="Clear search" onClick={onReset} />
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default LogPanel
|
||||
@@ -0,0 +1,63 @@
|
||||
import dayjs from 'dayjs'
|
||||
import { FC } from 'react'
|
||||
import { Input, Typography, IconX } from '@supabase/ui'
|
||||
|
||||
import { LogData } from './Logs.types'
|
||||
|
||||
interface Props {
|
||||
log: LogData
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Log selection display
|
||||
*/
|
||||
const LogSelection: FC<Props> = ({ log, onClose }) => {
|
||||
return (
|
||||
<div
|
||||
className={[
|
||||
'h-full flex flex-col flex-grow border border-l-0',
|
||||
'border-panel-border-light dark:border-panel-border-dark',
|
||||
].join(' ')}
|
||||
>
|
||||
<div
|
||||
className={[
|
||||
'bg-panel-header-light dark:bg-panel-header-dark',
|
||||
'border-b border-panel-border-interior-light',
|
||||
'dark:border-panel-border-interior-dark',
|
||||
].join(' ')}
|
||||
>
|
||||
<div className="px-6 py-4 flex items-center">
|
||||
<div className="flex flex-row justify-between items-center w-full">
|
||||
<Typography.Title level={5}>{dayjs(log.timestamp / 1000).toString()}</Typography.Title>
|
||||
<div className="cursor-pointer" onClick={onClose}>
|
||||
<IconX size={14} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-grow overflow-y-auto bg-panel-body-light dark:bg-panel-body-dark">
|
||||
<div className="p-4 flex-col space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Typography.Text small>Event message</Typography.Text>
|
||||
<Typography.Text code className="block w-full overflow-x-auto" small>
|
||||
{log.event_message}{' '}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Typography.Text small>Metadata</Typography.Text>
|
||||
<Input.TextArea
|
||||
className="font-mono mt-4"
|
||||
size="tiny"
|
||||
rows={25}
|
||||
disabled
|
||||
value={JSON.stringify(log.metadata, null, 2)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default LogSelection
|
||||
@@ -0,0 +1,90 @@
|
||||
import dayjs from 'dayjs'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Typography } from '@supabase/ui'
|
||||
import DataGrid from '@supabase/react-data-grid'
|
||||
|
||||
import LogSelection from './LogSelection'
|
||||
import { LogData } from './Logs.types'
|
||||
|
||||
interface Props {
|
||||
isCustomQuery: boolean
|
||||
data?: LogData[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs table view with focus side panel
|
||||
*/
|
||||
const LogTable = ({ isCustomQuery, data }: Props) => {
|
||||
const [focusedLog, setFocusedLog] = useState<LogData | null>(null)
|
||||
const columns = ['timestamp', 'event_message'].map((v) => ({
|
||||
key: v,
|
||||
name: v,
|
||||
width: v === 'timestamp' ? 210 : undefined,
|
||||
resizable: true,
|
||||
headerRenderer: () => {
|
||||
return <div className="flex items-center text-xs font-mono h-full">{v}</div>
|
||||
},
|
||||
formatter: ({ row }: any) => {
|
||||
let value = row[v]
|
||||
if (v === 'timestamp') {
|
||||
value = dayjs(Number(row['timestamp']) / 1000).toISOString()
|
||||
}
|
||||
return (
|
||||
<p
|
||||
className={[
|
||||
'block whitespace-wrap font-mono',
|
||||
`${row.id === focusedLog?.id ? 'font-bold' : ''}`,
|
||||
`${v === 'timestamp' ? 'text-green-500' : ''}`,
|
||||
].join(' ')}
|
||||
>
|
||||
{value}
|
||||
</p>
|
||||
)
|
||||
},
|
||||
}))
|
||||
|
||||
const logMap = (data || []).reduce((acc: any, d) => {
|
||||
acc[d.id] = d
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
useEffect(() => {
|
||||
if (!data) return
|
||||
if (focusedLog && !(focusedLog.id in logMap)) {
|
||||
setFocusedLog(null)
|
||||
}
|
||||
}, [Object.keys(logMap)])
|
||||
|
||||
if (!data) return null
|
||||
|
||||
// [Joshen] Hmm quite hacky now, but will do
|
||||
const maxHeight = isCustomQuery ? 'calc(100vh - 42px - 10rem)' : 'calc(100vh - 42px - 3rem)'
|
||||
|
||||
return (
|
||||
<section className="flex flex-1 flex-row" style={{ maxHeight }}>
|
||||
<DataGrid
|
||||
style={{ height: '100%' }}
|
||||
className="flex-grow flex-1"
|
||||
onSelectedCellChange={({ idx, rowIdx }) => setFocusedLog(data[rowIdx])}
|
||||
noRowsFallback={
|
||||
<div className="p-4">
|
||||
<Typography.Text type="secondary" small className="font-mono">
|
||||
No data returned from query
|
||||
</Typography.Text>
|
||||
</div>
|
||||
}
|
||||
columns={columns as any}
|
||||
rowClass={(r) => `${r.id === focusedLog?.id ? 'bg-green-800' : 'cursor-pointer'}`}
|
||||
rows={data}
|
||||
rowKeyGetter={(r) => r.id}
|
||||
onRowClick={(r) => setFocusedLog(logMap[r.id])}
|
||||
/>
|
||||
{focusedLog && (
|
||||
<div className="w-2/5 flex flex-col">
|
||||
<LogSelection onClose={() => setFocusedLog(null)} log={focusedLog} />
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
export default LogTable
|
||||
@@ -0,0 +1,13 @@
|
||||
import { LogTemplate } from '.'
|
||||
|
||||
export const TEMPLATES: LogTemplate[] = [
|
||||
{ label: 'Recent Errors', mode: 'simple', searchString: '[Ee]rror|\\s[45][0-9][0-9]\\s' },
|
||||
{
|
||||
label: 'POST or PATCH',
|
||||
mode: 'custom',
|
||||
searchString:
|
||||
"REGEXP_CONTAINS(event_message, 'POST') OR REGEXP_CONTAINS(event_message, 'PATCH')",
|
||||
},
|
||||
]
|
||||
|
||||
export const DEFAULT_QUERY = 'SELECT timestamp, event_message FROM postgres_logs;'
|
||||
@@ -0,0 +1,31 @@
|
||||
interface Metadata {
|
||||
[key: string]: string | number | Object | Object[]
|
||||
}
|
||||
|
||||
export interface LogData {
|
||||
id: string
|
||||
timestamp: number
|
||||
event_message: string
|
||||
metadata: Metadata
|
||||
}
|
||||
|
||||
export interface LogTemplate {
|
||||
label: string
|
||||
mode: 'custom' | 'simple'
|
||||
searchString: string
|
||||
}
|
||||
|
||||
export interface CountData {
|
||||
count: number
|
||||
}
|
||||
|
||||
export interface Count {
|
||||
data: [CountData] | []
|
||||
error?: any
|
||||
}
|
||||
|
||||
export interface Logs {
|
||||
data: LogData[]
|
||||
count: number
|
||||
error: any
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import LogPanel from './LogPanel'
|
||||
import LogTable from './LogTable'
|
||||
|
||||
export * from './Logs.types'
|
||||
export * from './Logs.constants'
|
||||
export { LogPanel, LogTable }
|
||||
+5
-5
@@ -49,13 +49,13 @@ const DrilldownPane: FC<Props> = ({ pane, jsonData, activeKey, onSelectKey = ()
|
||||
<div
|
||||
key={key}
|
||||
className={`
|
||||
${key === activeKey ? 'bg-gray-400' : ''}
|
||||
${key === activeKey ? 'bg-gray-100 dark:bg-gray-400' : ''}
|
||||
group flex items-center justify-between
|
||||
cursor-pointer py-2 px-5 hover:bg-gray-500
|
||||
cursor-pointer py-2 px-5 hover:bg-gray-100 dark:hover:bg-gray-500
|
||||
`}
|
||||
onClick={() => onSelectKey(key, pane)}
|
||||
>
|
||||
<Typography.Text small className="font-mono !text-blue-300">
|
||||
<Typography.Text small className="font-mono !text-blue-700 dark:!text-blue-300">
|
||||
{key}
|
||||
</Typography.Text>
|
||||
<div className={`${key === activeKey ? 'block' : 'hidden'} group-hover:block`}>
|
||||
@@ -67,13 +67,13 @@ const DrilldownPane: FC<Props> = ({ pane, jsonData, activeKey, onSelectKey = ()
|
||||
))}
|
||||
{keysWithoutChildren.map((key: string) => (
|
||||
<div key={key} className="py-2 px-5 flex space-x-2">
|
||||
<Typography.Text small className="font-mono !text-blue-300">
|
||||
<Typography.Text small className="font-mono !text-blue-700 dark:!text-blue-300">
|
||||
{key}:
|
||||
</Typography.Text>
|
||||
<Typography.Text
|
||||
small
|
||||
className={`font-mono break-all ${
|
||||
typeof jsonData[key] !== 'string' ? '!text-green-400' : '!text-yellow-500'
|
||||
typeof jsonData[key] !== 'string' ? '!text-green-700 dark:!text-green-400' : '!text-yellow-700 dark:!text-yellow-500'
|
||||
}`}
|
||||
>
|
||||
{isNull(jsonData[key])
|
||||
|
||||
+20
-20
@@ -18,7 +18,6 @@ import {
|
||||
DraggableProvided,
|
||||
} from 'react-beautiful-dnd'
|
||||
|
||||
// import Hooks from 'lib/Hooks'
|
||||
import Column from './Column'
|
||||
import InformationBox from 'components/ui/InformationBox'
|
||||
import ForeignKeySelector from '../ForeignKeySelector/ForeignKeySelector'
|
||||
@@ -60,25 +59,26 @@ const ColumnManagement: FC<Props> = ({
|
||||
const saveColumnForeignKey = (
|
||||
foreignKeyConfiguration: { table: PostgresTable; column: PostgresColumn } | undefined
|
||||
) => {
|
||||
// @ts-ignore
|
||||
onUpdateColumn(selectedColumnToEditRelation, {
|
||||
foreignKey: !isUndefined(foreignKeyConfiguration)
|
||||
? {
|
||||
id: 0,
|
||||
constraint_name: '',
|
||||
source_schema: table?.schema,
|
||||
source_table_name: table?.name,
|
||||
source_column_name: selectedColumnToEditRelation?.name,
|
||||
target_table_schema: foreignKeyConfiguration.table.schema,
|
||||
target_table_name: foreignKeyConfiguration.table.name,
|
||||
target_column_name: foreignKeyConfiguration.column.name,
|
||||
}
|
||||
: undefined,
|
||||
...(!isUndefined(foreignKeyConfiguration) && {
|
||||
format: foreignKeyConfiguration.column.format,
|
||||
defaultValue: '',
|
||||
}),
|
||||
})
|
||||
if (!isUndefined(selectedColumnToEditRelation)) {
|
||||
onUpdateColumn(selectedColumnToEditRelation, {
|
||||
foreignKey: !isUndefined(foreignKeyConfiguration)
|
||||
? {
|
||||
id: 0,
|
||||
constraint_name: '',
|
||||
source_schema: table?.schema ?? '',
|
||||
source_table_name: table?.name ?? '',
|
||||
source_column_name: selectedColumnToEditRelation?.name,
|
||||
target_table_schema: foreignKeyConfiguration.table.schema,
|
||||
target_table_name: foreignKeyConfiguration.table.name,
|
||||
target_column_name: foreignKeyConfiguration.column.name,
|
||||
}
|
||||
: undefined,
|
||||
...(!isUndefined(foreignKeyConfiguration) && {
|
||||
format: foreignKeyConfiguration.column.format,
|
||||
defaultValue: '',
|
||||
}),
|
||||
})
|
||||
}
|
||||
setSelectedColumnToEditRelation(undefined)
|
||||
}
|
||||
|
||||
|
||||
+6
-3
@@ -1,5 +1,5 @@
|
||||
import { DragEvent, useRef, useState, FC } from 'react'
|
||||
import { Button, Typography, IconLoader } from '@supabase/ui'
|
||||
import { Button, Typography, IconLoader, IconFileText } from '@supabase/ui'
|
||||
import SparkBar from 'components/ui/SparkBar'
|
||||
|
||||
interface Props {
|
||||
@@ -62,8 +62,11 @@ const SpreadSheetFileUpload: FC<Props> = ({
|
||||
</Typography.Text>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center border dark:border-gray-500 border-dashed rounded-md h-48">
|
||||
<Typography.Title level={4}>{uploadedFile.name}</Typography.Title>
|
||||
<div className="flex flex-col items-center justify-center border dark:border-gray-500 border-dashed rounded-md h-32 space-y-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<IconFileText size={16} strokeWidth={2} />
|
||||
<Typography.Title level={4}>{uploadedFile.name}</Typography.Title>
|
||||
</div>
|
||||
{parseProgress === 100 ? (
|
||||
<Button type="outline" onClick={removeUploadedFile}>
|
||||
Remove File
|
||||
|
||||
+9
@@ -1,5 +1,14 @@
|
||||
import { SpreadsheetData } from './SpreadsheetImport.types'
|
||||
|
||||
export const UPLOAD_FILE_TYPES = [
|
||||
'text/csv',
|
||||
'text/tab-separated-values',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
]
|
||||
|
||||
export const EMPTY_SPREADSHEET_DATA: SpreadsheetData = {
|
||||
headers: [],
|
||||
rows: [],
|
||||
rowCount: 0,
|
||||
columnTypeMap: {},
|
||||
}
|
||||
+120
-98
@@ -1,14 +1,15 @@
|
||||
import { useCallback, useState, FC } from 'react'
|
||||
import { useCallback, useState, FC, useEffect } from 'react'
|
||||
import { debounce, includes } from 'lodash'
|
||||
import { SidePanel, Typography } from '@supabase/ui'
|
||||
import { SidePanel, Typography, Tabs, IconArrowRight, IconChevronRight } from '@supabase/ui'
|
||||
|
||||
import { useStore } from 'hooks'
|
||||
import Telemetry from 'lib/telemetry'
|
||||
import ActionBar from '../../ActionBar'
|
||||
import { parseSpreadsheet, parseSpreadsheetText } from './SpreadsheetImport.utils'
|
||||
import { UPLOAD_FILE_TYPES } from './SpreadsheetImport.constants'
|
||||
import SpreadSheetTextInput from './SpreadSheetTextInput'
|
||||
import SpreadSheetFileUpload from './SpreadSheetFileUpload'
|
||||
import SpreadsheetPreview from './SpreadsheetPreview'
|
||||
import { SpreadsheetData } from './SpreadsheetImport.types'
|
||||
import { parseSpreadsheet, parseSpreadsheetText } from './SpreadsheetImport.utils'
|
||||
import { UPLOAD_FILE_TYPES, EMPTY_SPREADSHEET_DATA } from './SpreadsheetImport.constants'
|
||||
|
||||
interface Props {
|
||||
debounceDuration?: number
|
||||
@@ -29,17 +30,25 @@ const SpreadsheetImport: FC<Props> = ({
|
||||
}) => {
|
||||
const { ui } = useStore()
|
||||
|
||||
const [spreadsheetData, setSpreadsheetData] = useState<any>({
|
||||
headers: headers,
|
||||
rows: rows,
|
||||
rowCount: 0,
|
||||
})
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
if (headers.length === 0) {
|
||||
resetSpreadsheetImport()
|
||||
}
|
||||
}
|
||||
}, [visible])
|
||||
|
||||
const [input, setInput] = useState<string>('')
|
||||
const [uploadedFile, setUploadedFile] = useState<any>(null)
|
||||
|
||||
const [view, setView] = useState<string>('fileUpload')
|
||||
const [parseProgress, setParseProgress] = useState<number>(0)
|
||||
const [spreadsheetData, setSpreadsheetData] = useState<SpreadsheetData>({
|
||||
headers: headers,
|
||||
rows: rows,
|
||||
rowCount: 0,
|
||||
columnTypeMap: {},
|
||||
})
|
||||
const [errors, setErrors] = useState<any>([])
|
||||
const [expandedErrors, setExpandedErrors] = useState<string[]>([])
|
||||
|
||||
const onProgressUpdate = (progress: number) => {
|
||||
setParseProgress(progress)
|
||||
@@ -56,71 +65,73 @@ const SpreadsheetImport: FC<Props> = ({
|
||||
})
|
||||
} else {
|
||||
setUploadedFile(file)
|
||||
const { headers, rowCount, rowPreview, errors } = await parseSpreadsheet(
|
||||
const { headers, rowCount, columnTypeMap, errors } = await parseSpreadsheet(
|
||||
file,
|
||||
onProgressUpdate
|
||||
)
|
||||
if (errors.length <= 5) {
|
||||
errors.map((error: any) => {
|
||||
ui.setNotification({
|
||||
category: 'error',
|
||||
message: `Error found at row ${error.row} - ${error.type}: ${error.code}`,
|
||||
})
|
||||
})
|
||||
} else if (errors.length > 5) {
|
||||
if (errors.length > 0) {
|
||||
ui.setNotification({
|
||||
error: errors,
|
||||
category: 'error',
|
||||
message: `Multiple errors have been detected on ${errors.length} rows. Do check the file you have uploaded for any discrepancies.`,
|
||||
message: `Some issues have been detected on ${errors.length} rows. More details below the content preview.`,
|
||||
})
|
||||
}
|
||||
setSpreadsheetData({ headers, rowCount, rows: rowPreview })
|
||||
setErrors(errors)
|
||||
setSpreadsheetData({ headers, rows: [], rowCount, columnTypeMap })
|
||||
}
|
||||
event.target.value = ''
|
||||
}
|
||||
|
||||
const removeUploadedFile = () => {
|
||||
setSpreadsheetData({ headers: [], rows: [], rowCount: 0 })
|
||||
const resetSpreadsheetImport = () => {
|
||||
setInput('')
|
||||
setSpreadsheetData(EMPTY_SPREADSHEET_DATA)
|
||||
setUploadedFile(null)
|
||||
setErrors([])
|
||||
setExpandedErrors([])
|
||||
}
|
||||
|
||||
const readInputSpreadsheet = async (text: string) => {
|
||||
const readSpreadsheetText = async (text: string) => {
|
||||
if (text.length > 0) {
|
||||
const { headers, rows, errors } = await parseSpreadsheetText(text)
|
||||
if (errors.length <= 5) {
|
||||
errors.map((error: any) => {
|
||||
ui.setNotification({
|
||||
error,
|
||||
category: 'error',
|
||||
message: `Error found at row ${error.row || 0} - ${error.type}: ${error.code}`,
|
||||
})
|
||||
})
|
||||
} else if (errors.length > 5) {
|
||||
const { headers, rows, columnTypeMap, errors } = await parseSpreadsheetText(text)
|
||||
if (errors.length > 0) {
|
||||
ui.setNotification({
|
||||
error: errors,
|
||||
category: 'error',
|
||||
message: `Multiple errors have been detected on ${errors.length} rows. Do check your input for any discrepancies.`,
|
||||
message: `Some issues have been detected on ${errors.length} rows. More details below the content preview.`,
|
||||
})
|
||||
}
|
||||
setSpreadsheetData({ headers, rows, rowCount: rows.length })
|
||||
setErrors(errors)
|
||||
setSpreadsheetData({ headers, rows, rowCount: rows.length, columnTypeMap })
|
||||
} else {
|
||||
setSpreadsheetData({ headers: [], rows: [], rowCount: 0 })
|
||||
setSpreadsheetData(EMPTY_SPREADSHEET_DATA)
|
||||
}
|
||||
}
|
||||
|
||||
const handler = useCallback(debounce(readInputSpreadsheet, debounceDuration), [])
|
||||
const handler = useCallback(debounce(readSpreadsheetText, debounceDuration), [])
|
||||
const onInputChange = (event: any) => {
|
||||
setInput(event.target.value)
|
||||
handler(event.target.value)
|
||||
}
|
||||
|
||||
const onSelectExpandError = (key: string) => {
|
||||
if (expandedErrors.includes(key)) {
|
||||
setExpandedErrors(expandedErrors.filter((error) => error !== key))
|
||||
} else {
|
||||
setExpandedErrors(expandedErrors.concat([key]))
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<SidePanel
|
||||
wide
|
||||
visible={visible}
|
||||
align="right"
|
||||
title="Add content to new table"
|
||||
onCancel={closePanel}
|
||||
onCancel={(event: any) => {
|
||||
// Only close if specifically hit the X button, this is to have the
|
||||
// side panel work with toast messages (clicking on toast will close the panel)
|
||||
if (event?.target) closePanel()
|
||||
}}
|
||||
customFooter={
|
||||
<ActionBar
|
||||
backButtonLabel="Cancel"
|
||||
@@ -131,72 +142,83 @@ const SpreadsheetImport: FC<Props> = ({
|
||||
file: uploadedFile,
|
||||
...spreadsheetData,
|
||||
})
|
||||
Telemetry.sendEvent('table_editor', 'spreadsheet_import_method', view)
|
||||
}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex items-center justify-center border dark:border-dark rounded-md w-full mb-4">
|
||||
<div
|
||||
className={`
|
||||
flex items-center justify-center w-1/2 p-2 cursor-pointer hover:bg-bg-secondary-light dark:hover:bg-bg-alt-dark
|
||||
transition ease-in-out duration-150 border-r dark:border-dark
|
||||
${
|
||||
view === 'fileUpload'
|
||||
? ' text-typography-body-light dark:text-typography-body-dark bg-bg-alt-light dark:bg-bg-alt-dark'
|
||||
: ' text-typography-body-secondary-light dark:text-typography-body-secondary-dark'
|
||||
}
|
||||
`}
|
||||
onClick={() => setView('fileUpload')}
|
||||
>
|
||||
<p className="text-sm">Upload CSV</p>
|
||||
</div>
|
||||
<div
|
||||
className={`
|
||||
flex items-center justify-center w-1/2 p-2 cursor-pointer hover:bg-bg-secondary-light dark:hover:bg-bg-alt-dark
|
||||
transition ease-in-out duration-150
|
||||
${
|
||||
view === 'pasteText'
|
||||
? ' text-typography-body-light dark:text-typography-body-dark bg-bg-alt-light dark:bg-bg-alt-dark'
|
||||
: ' text-typography-body-secondary-light dark:text-typography-body-secondary-dark'
|
||||
}
|
||||
`}
|
||||
onClick={() => setView('pasteText')}
|
||||
>
|
||||
<p className="text-sm">Paste Text</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{view === 'pasteText' ? (
|
||||
<SpreadSheetTextInput input={input} onInputChange={onInputChange} />
|
||||
) : (
|
||||
<SpreadSheetFileUpload
|
||||
parseProgress={parseProgress}
|
||||
uploadedFile={uploadedFile}
|
||||
onFileUpload={onFileUpload}
|
||||
removeUploadedFile={removeUploadedFile}
|
||||
/>
|
||||
)}
|
||||
<Tabs block>
|
||||
{/* @ts-ignore */}
|
||||
<Tabs.Panel id="fileUpload" label="Upload CSV">
|
||||
<SpreadSheetFileUpload
|
||||
parseProgress={parseProgress}
|
||||
uploadedFile={uploadedFile}
|
||||
onFileUpload={onFileUpload}
|
||||
removeUploadedFile={resetSpreadsheetImport}
|
||||
/>
|
||||
</Tabs.Panel>
|
||||
{/* @ts-ignore */}
|
||||
<Tabs.Panel id="pasteText" label="Paste text">
|
||||
<SpreadSheetTextInput input={input} onInputChange={onInputChange} />
|
||||
</Tabs.Panel>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
{spreadsheetData.headers.length > 0 && (
|
||||
<div className="py-5">
|
||||
<Typography.Text>
|
||||
<p>Content Preview</p>
|
||||
<p className="mt-2 text-sm">
|
||||
Your table will have {spreadsheetData.rowCount.toLocaleString()} rows and the
|
||||
following {spreadsheetData.headers.length} columns.
|
||||
</p>
|
||||
</Typography.Text>
|
||||
<div className="flex flex-wrap items-center mt-3">
|
||||
{spreadsheetData.headers.map((header: string) => (
|
||||
<Typography.Text key={`preview_${header}`}>
|
||||
<div className="rounded-md border dark:border-gray-500 border-dashed py-1 px-3 mr-2 mb-2 text-sm">
|
||||
{header}
|
||||
</div>
|
||||
<div className="py-5 space-y-5">
|
||||
<div className="space-y-2">
|
||||
<div className="flex flex-col space-y-1">
|
||||
<Typography.Text>Content Preview</Typography.Text>
|
||||
<Typography.Text type="secondary">
|
||||
Your table will have {spreadsheetData.rowCount.toLocaleString()} rows and the
|
||||
following {spreadsheetData.headers.length} columns.
|
||||
</Typography.Text>
|
||||
))}
|
||||
</div>
|
||||
<SpreadsheetPreview headers={spreadsheetData.headers} />
|
||||
</div>
|
||||
{errors.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<div className="flex flex-col space-y-1">
|
||||
<Typography.Text>Issues found in spreadsheet</Typography.Text>
|
||||
<Typography.Text type="secondary">
|
||||
Your table can still be created nonetheless despite issues in the following rows.
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{errors.map((error: any, idx: number) => {
|
||||
const key = `import-error-${idx}`
|
||||
const isExpanded = expandedErrors.includes(key)
|
||||
return (
|
||||
<div key={key} className="space-y-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<IconChevronRight
|
||||
className={`cursor-pointer transform ${isExpanded ? 'rotate-90' : ''}`}
|
||||
size={14}
|
||||
onClick={() => onSelectExpandError(key)}
|
||||
/>
|
||||
<Typography.Text className="w-14">Row: {error.row}</Typography.Text>
|
||||
<Typography.Text>{error.message}</Typography.Text>
|
||||
{error.data?.__parsed_extra && (
|
||||
<>
|
||||
<IconArrowRight size={14} />
|
||||
<Typography.Text>Extra field(s):</Typography.Text>
|
||||
{error.data?.__parsed_extra.map((value: any) => (
|
||||
<Typography.Text code small>
|
||||
{value}
|
||||
</Typography.Text>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{isExpanded && (
|
||||
<SpreadsheetPreview headers={spreadsheetData.headers} rows={[error.data]} />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</SidePanel>
|
||||
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
import { Dictionary } from '@supabase/grid'
|
||||
|
||||
export interface SpreadsheetData {
|
||||
headers: string[]
|
||||
rows: any[]
|
||||
rowCount: number
|
||||
columnTypeMap: Dictionary<string>
|
||||
}
|
||||
+90
-13
@@ -1,18 +1,32 @@
|
||||
import dayjs from 'dayjs'
|
||||
import Papa from 'papaparse'
|
||||
import { has, includes } from 'lodash'
|
||||
import { tryParseJson } from 'lib/helpers'
|
||||
|
||||
const CHUNK_SIZE = 1024 * 1024 * 0.25 // 0.25MB
|
||||
|
||||
export const parseSpreadsheetText: any = (text: string) => {
|
||||
const columnTypeMap: any = {}
|
||||
return new Promise((resolve) => {
|
||||
Papa.parse(text, {
|
||||
header: true,
|
||||
dynamicTyping: true,
|
||||
skipEmptyLines: true,
|
||||
complete: (results) => {
|
||||
const headers = results.meta.fields
|
||||
const headers = results.meta.fields || []
|
||||
const rows = results.data
|
||||
const errors = results.errors
|
||||
resolve({ headers, rows, errors })
|
||||
|
||||
headers.forEach((header) => {
|
||||
const type = inferColumnType(header, results.data as any[])
|
||||
if (!has(columnTypeMap, header)) {
|
||||
columnTypeMap[header] = type
|
||||
} else if (columnTypeMap[header] !== type) {
|
||||
columnTypeMap[header] = 'text'
|
||||
}
|
||||
})
|
||||
|
||||
resolve({ headers, rows, columnTypeMap, errors })
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -21,17 +35,19 @@ export const parseSpreadsheetText: any = (text: string) => {
|
||||
/**
|
||||
* For SpreadsheetImport side panel
|
||||
* @param file File object (CSV or TSV)
|
||||
* @returns {Object<headers, rows, errors>}
|
||||
* headers: String[]
|
||||
* rows: any[]
|
||||
* errors: Object[]
|
||||
* @returns SpreadsheetData
|
||||
*/
|
||||
export const parseSpreadsheet = (file: File, onProgressUpdate: (progress: number) => void): any => {
|
||||
export const parseSpreadsheet = (
|
||||
file: File,
|
||||
onProgressUpdate: (progress: number) => void
|
||||
): Promise<any> => {
|
||||
let headers: string[] = []
|
||||
let chunkNumber = 0
|
||||
let rowCount = 0
|
||||
const rowPreview: any[] = []
|
||||
|
||||
const columnTypeMap: any = {}
|
||||
const errors: any[] = []
|
||||
|
||||
return new Promise((resolve) => {
|
||||
Papa.parse(file, {
|
||||
header: true,
|
||||
@@ -43,14 +59,22 @@ export const parseSpreadsheet = (file: File, onProgressUpdate: (progress: number
|
||||
chunk: (results) => {
|
||||
headers = results.meta.fields as string[]
|
||||
|
||||
if (rowCount === 0) {
|
||||
rowPreview.push(...results.data.slice(0, 50))
|
||||
}
|
||||
headers.forEach((header) => {
|
||||
const type = inferColumnType(header, results.data as any[])
|
||||
if (!has(columnTypeMap, header)) {
|
||||
columnTypeMap[header] = type
|
||||
} else if (columnTypeMap[header] !== type) {
|
||||
columnTypeMap[header] = 'text'
|
||||
}
|
||||
})
|
||||
|
||||
rowCount += results.data.length
|
||||
|
||||
if (results.errors.length > 0) {
|
||||
errors.push(...results.errors)
|
||||
const formattedErrors = results.errors.map((error) => {
|
||||
return { ...error, data: results.data[error.row] }
|
||||
})
|
||||
errors.push(...formattedErrors)
|
||||
}
|
||||
|
||||
chunkNumber += 1
|
||||
@@ -58,7 +82,7 @@ export const parseSpreadsheet = (file: File, onProgressUpdate: (progress: number
|
||||
onProgressUpdate(progress > 1 ? 100 : Number((progress * 100).toFixed(2)))
|
||||
},
|
||||
complete: () => {
|
||||
const data = { headers, rowCount, rowPreview, errors }
|
||||
const data = { headers, rowCount, columnTypeMap, errors }
|
||||
resolve(data)
|
||||
},
|
||||
})
|
||||
@@ -68,3 +92,56 @@ export const parseSpreadsheet = (file: File, onProgressUpdate: (progress: number
|
||||
export const revertSpreadsheet = (headers: string[], rows: any[]) => {
|
||||
return Papa.unparse(rows, { columns: headers })
|
||||
}
|
||||
|
||||
const inferColumnType = (column: string, rows: object[]) => {
|
||||
// General strategy is to check the first row first, before checking across all the rows
|
||||
// to ensure uniformity in data type. Thinking we do this as an optimization instead of
|
||||
// checking all the rows up front.
|
||||
|
||||
// If there are no rows to infer for, default to text
|
||||
if (rows.length === 0) return 'text'
|
||||
|
||||
const columnData = (rows[0] as any)[column]
|
||||
const columnDataAcrossRows = rows.map((row: object) => (row as any)[column])
|
||||
|
||||
// Unable to infer any type as there's no data, default to text
|
||||
if (!columnData) {
|
||||
return 'text'
|
||||
}
|
||||
|
||||
// Infer numerical data type (defaults to either int8 or float8)
|
||||
if (Number(columnData)) {
|
||||
const columnNumberCheck = rows.map((row: object) => Number((row as any)[column]))
|
||||
if (columnNumberCheck.includes(NaN)) {
|
||||
return 'text'
|
||||
} else {
|
||||
const columnFloatCheck = columnNumberCheck.map((num: number) => num % 1)
|
||||
return columnFloatCheck.every((item) => item === 0) ? 'int8' : 'float8'
|
||||
}
|
||||
}
|
||||
|
||||
// Infer boolean type
|
||||
if (includes(['true', 'false'], columnData.toLowerCase())) {
|
||||
const isAllBoolean = columnDataAcrossRows.every((item: any) =>
|
||||
includes(['true', 'false'], item.toLowerCase())
|
||||
)
|
||||
if (isAllBoolean) {
|
||||
return 'boolean'
|
||||
}
|
||||
}
|
||||
|
||||
// Infer json type
|
||||
if (tryParseJson(columnData)) {
|
||||
const isAllJson = columnDataAcrossRows.every((item: any) => tryParseJson(columnData))
|
||||
if (isAllJson) {
|
||||
return 'jsonb'
|
||||
}
|
||||
}
|
||||
|
||||
// Infer datetime type
|
||||
if (dayjs(columnData, 'YYYY-MM-DD hh:mm:ss').isValid() && Date.parse(columnData)) {
|
||||
return 'timestamptz'
|
||||
}
|
||||
|
||||
return 'text'
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
import { FC } from 'react'
|
||||
import { isUndefined } from 'lodash'
|
||||
import DataGrid from '@supabase/react-data-grid'
|
||||
import { Typography } from '@supabase/ui'
|
||||
|
||||
interface Props {
|
||||
headers: string[]
|
||||
rows?: any[]
|
||||
}
|
||||
|
||||
const SpreadsheetPreview: FC<Props> = ({ headers = [], rows = [] }) => {
|
||||
return (
|
||||
<DataGrid
|
||||
columns={headers.map((header) => {
|
||||
return {
|
||||
key: header,
|
||||
name: header,
|
||||
width: header.length * 10,
|
||||
resizable: true,
|
||||
headerRenderer: () => (
|
||||
<div className="flex items-center justify-center font-mono h-full">
|
||||
<Typography.Text small>{header}</Typography.Text>
|
||||
</div>
|
||||
),
|
||||
formatter: ({ row }: { row: any }) => (
|
||||
<span className="font-mono text-xs">
|
||||
{isUndefined(row[header]) ? 'NULL' : row[header]}
|
||||
</span>
|
||||
),
|
||||
}
|
||||
})}
|
||||
rows={rows}
|
||||
className="!border-l !border-r"
|
||||
style={{ height: `${34 + 34 * rows.length}px` }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default SpreadsheetPreview
|
||||
+2
@@ -1,3 +1,4 @@
|
||||
import { Dictionary } from '@supabase/grid'
|
||||
import { ColumnField } from '../SidePanelEditor.types'
|
||||
|
||||
export interface TableField {
|
||||
@@ -13,4 +14,5 @@ export interface ImportContent {
|
||||
headers: string[]
|
||||
rowCount: number
|
||||
rows: object[]
|
||||
columnTypeMap: Dictionary<any>
|
||||
}
|
||||
+3
-57
@@ -1,8 +1,6 @@
|
||||
import { some, includes } from 'lodash'
|
||||
import dayjs from 'dayjs'
|
||||
import { some } from 'lodash'
|
||||
import { PostgresColumn, PostgresTable } from '@supabase/postgres-meta'
|
||||
|
||||
import { tryParseJson } from 'lib/helpers'
|
||||
import { ImportContent, TableField } from './TableEditor.types'
|
||||
import { DEFAULT_COLUMNS } from './TableEditor.constants'
|
||||
import { ColumnField } from '../SidePanelEditor.types'
|
||||
@@ -43,6 +41,7 @@ export const generateTableFieldFromPostgresTable = (
|
||||
id: table.id,
|
||||
name: isDuplicating ? `${table.name}_duplicate` : table.name,
|
||||
comment: isDuplicating ? `This is a duplicate of ${table.name}` : table?.comment ?? '',
|
||||
// @ts-ignore
|
||||
columns: table.columns.map((column: PostgresColumn) => {
|
||||
return generateColumnFieldFromPostgresColumn(column, table)
|
||||
}),
|
||||
@@ -52,61 +51,8 @@ export const generateTableFieldFromPostgresTable = (
|
||||
|
||||
export const formatImportedContentToColumnFields = (importContent: ImportContent) => {
|
||||
const columnFields = importContent.headers.map((header: string) => {
|
||||
const columnType = inferColumnType(header, importContent.rows)
|
||||
const columnType = importContent.columnTypeMap[header]
|
||||
return generateColumnField({ name: header, format: columnType })
|
||||
})
|
||||
return columnFields
|
||||
}
|
||||
|
||||
export const inferColumnType = (column: string, rows: object[]) => {
|
||||
// General strategy is to check the first row first, before checking across all the rows
|
||||
// to ensure uniformity in data type. Thinking we do this as an optimization instead of
|
||||
// checking all the rows up front.
|
||||
|
||||
// If there are no rows to infer for, default to text
|
||||
if (rows.length === 0) return 'text'
|
||||
|
||||
const columnData = (rows[0] as any)[column]
|
||||
const columnDataAcrossRows = rows.map((row: object) => (row as any)[column])
|
||||
|
||||
// Unable to infer any type as there's no data, default to text
|
||||
if (!columnData) {
|
||||
return 'text'
|
||||
}
|
||||
|
||||
// Infer numerical data type (defaults to either int8 or float8)
|
||||
if (Number(columnData)) {
|
||||
const columnNumberCheck = rows.map((row: object) => Number((row as any)[column]))
|
||||
if (columnNumberCheck.includes(NaN)) {
|
||||
return 'text'
|
||||
} else {
|
||||
const columnFloatCheck = columnNumberCheck.map((num: number) => num % 1)
|
||||
return columnFloatCheck.every((item) => item === 0) ? 'int8' : 'float8'
|
||||
}
|
||||
}
|
||||
|
||||
// Infer boolean type
|
||||
if (includes(['true', 'false'], columnData.toLowerCase())) {
|
||||
const isAllBoolean = columnDataAcrossRows.every((item: any) =>
|
||||
includes(['true', 'false'], item.toLowerCase())
|
||||
)
|
||||
if (isAllBoolean) {
|
||||
return 'boolean'
|
||||
}
|
||||
}
|
||||
|
||||
// Infer json type
|
||||
if (tryParseJson(columnData)) {
|
||||
const isAllJson = columnDataAcrossRows.every((item: any) => tryParseJson(columnData))
|
||||
if (isAllJson) {
|
||||
return 'jsonb'
|
||||
}
|
||||
}
|
||||
|
||||
// Infer datetime type
|
||||
if (dayjs(columnData, 'YYYY-MM-DD hh:mm:ss').isValid() && Date.parse(columnData)) {
|
||||
return 'timestamptz'
|
||||
}
|
||||
|
||||
return 'text'
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { observer } from 'mobx-react-lite'
|
||||
import { API_URL, IS_PLATFORM } from 'lib/constants'
|
||||
import { useStore } from 'hooks'
|
||||
import WithSidebar from './WithSidebar'
|
||||
import { auth, GOTRUE_ENABLED } from 'lib/gotrue'
|
||||
|
||||
/**
|
||||
* layout for dashboard homepage, account and org settings
|
||||
@@ -19,13 +20,28 @@ const AccountLayout = ({ children, title, breadcrumbs }: any) => {
|
||||
|
||||
const { app, ui } = useStore()
|
||||
|
||||
const onClickLogout = async () => {
|
||||
await auth.signOut()
|
||||
router.reload()
|
||||
}
|
||||
|
||||
let baseLogoutLink = {
|
||||
icon: '/icons/feather/power.svg',
|
||||
label: 'Logout',
|
||||
href: `${API_URL}/logout`,
|
||||
key: `${API_URL}/logout`,
|
||||
}
|
||||
|
||||
let logoutLink = GOTRUE_ENABLED
|
||||
? { ...baseLogoutLink, href: undefined, onClick: onClickLogout }
|
||||
: baseLogoutLink
|
||||
|
||||
const organizationsLinks = app.organizations
|
||||
.list()
|
||||
.map((x: any) => ({
|
||||
isActive: router.pathname.startsWith('/org/') && ui.selectedOrganization?.slug == x.slug,
|
||||
label: x.name,
|
||||
href: '/org/[slug]/settings',
|
||||
as: `/org/${x.slug}/settings`,
|
||||
href: `/org/${x.slug}/settings`,
|
||||
}))
|
||||
.sort((a, b) => a.label.localeCompare(b.label))
|
||||
|
||||
@@ -37,7 +53,6 @@ const AccountLayout = ({ children, title, breadcrumbs }: any) => {
|
||||
isActive: router.pathname == '/',
|
||||
label: 'All projects',
|
||||
href: '/',
|
||||
as: `/`,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -61,12 +76,7 @@ const AccountLayout = ({ children, title, breadcrumbs }: any) => {
|
||||
href: `/account/me`,
|
||||
key: `/account/me`,
|
||||
},
|
||||
{
|
||||
icon: '/img/power.svg',
|
||||
label: 'Logout',
|
||||
href: `${API_URL}/logout`,
|
||||
key: `${API_URL}/logout`,
|
||||
},
|
||||
logoutLink,
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import Link from 'next/link'
|
||||
import { CSSProperties, FC, ReactNode } from 'react'
|
||||
import { Menu, Typography, IconArrowUpRight, Badge } from '@supabase/ui'
|
||||
import { isUndefined } from 'lodash'
|
||||
import LayoutHeader from '../ProjectLayout/LayoutHeader'
|
||||
|
||||
interface Props {
|
||||
@@ -116,7 +117,6 @@ const LinksWithoutHeaders: FC<any> = ({ links, subitems, subitemsParentKey }) =>
|
||||
isActive={isActive}
|
||||
label={x.label}
|
||||
href={x.href}
|
||||
as={x.as}
|
||||
onClick={x.onClick}
|
||||
external={x.external || false}
|
||||
/>
|
||||
@@ -143,9 +143,26 @@ const LinksWithoutHeaders: FC<any> = ({ links, subitems, subitemsParentKey }) =>
|
||||
)
|
||||
}
|
||||
|
||||
const SidebarItem: FC<any> = ({ id, label, as, href, isActive, isSubitem, onClick, external }) => {
|
||||
const SidebarItem: FC<any> = ({ id, label, href, isActive, isSubitem, onClick, external }) => {
|
||||
if (isUndefined(href)) {
|
||||
return (
|
||||
<Menu.Item
|
||||
rounded
|
||||
key={id}
|
||||
style={{
|
||||
marginLeft: isSubitem && '.5rem',
|
||||
}}
|
||||
active={isActive ? true : false}
|
||||
onClick={onClick || (() => {})}
|
||||
icon={external && <IconArrowUpRight size={'tiny'} />}
|
||||
>
|
||||
{isSubitem ? <Typography.Text small>{label}</Typography.Text> : label}
|
||||
</Menu.Item>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Link href={href || ''} as={as || ''}>
|
||||
<Link href={href || ''}>
|
||||
<a className="block" target={external ? '_blank' : '_self'}>
|
||||
<Menu.Item
|
||||
rounded
|
||||
|
||||
@@ -70,7 +70,7 @@ const HelpPopover: FC<Props> = () => {
|
||||
alt="discord illustration header"
|
||||
/>
|
||||
<a
|
||||
href="https://discord.com/invite/AYybku5cUz"
|
||||
href="https://discord.supabase.com"
|
||||
target="_blank"
|
||||
className="block cursor-pointer"
|
||||
>
|
||||
|
||||
@@ -38,7 +38,7 @@ const LayoutHeader = ({ customHeaderComponents, breadcrumbs = [], headerBorder =
|
||||
</>
|
||||
) : (
|
||||
<Typography.Text small className="mx-2">
|
||||
<Link as="/" href="/">
|
||||
<Link href="/">
|
||||
<a
|
||||
className={`block px-2 py-1 focus:outline-none focus:bg-transparent cursor-pointer dark:hover:text-white`}
|
||||
>
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { FC } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { Typography } from '@supabase/ui'
|
||||
import * as Tooltip from '@radix-ui/react-tooltip'
|
||||
|
||||
import { Route } from 'components/ui/ui.types'
|
||||
|
||||
interface Props {
|
||||
@@ -9,18 +12,34 @@ interface Props {
|
||||
|
||||
const NavigationIconButton: FC<Props> = ({ route, isActive = false }) => {
|
||||
return (
|
||||
<Link href={route.link}>
|
||||
<a
|
||||
className={[
|
||||
'flex items-center justify-center h-10 w-10 rounded', // Layout
|
||||
'text-gray-600 hover:bg-gray-100', // Light mode
|
||||
'dark:text-gray-400 dark:hover:bg-bg-alt-dark dark:hover:text-white', // Dark mode
|
||||
`${isActive ? 'bg-gray-100 dark:bg-bg-alt-dark dark:!text-white' : ''}`,
|
||||
].join(' ')}
|
||||
>
|
||||
{route.icon}
|
||||
</a>
|
||||
</Link>
|
||||
<Tooltip.Root delayDuration={0}>
|
||||
<Tooltip.Trigger>
|
||||
<Link href={route.link}>
|
||||
<a
|
||||
className={[
|
||||
'flex items-center justify-center h-10 w-10 rounded', // Layout
|
||||
'text-gray-600 hover:bg-gray-100', // Light mode
|
||||
'dark:text-gray-400 dark:hover:bg-bg-alt-dark dark:hover:text-white', // Dark mode
|
||||
`${isActive ? 'bg-gray-100 dark:bg-bg-alt-dark dark:!text-white' : ''}`,
|
||||
].join(' ')}
|
||||
>
|
||||
{route.icon}
|
||||
</a>
|
||||
</Link>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content side="right">
|
||||
<Tooltip.Arrow className="radix-tooltip-arrow" />
|
||||
<div
|
||||
className={[
|
||||
'shadow px-2 py-1 rounded border',
|
||||
'bg-white',
|
||||
'dark:bg-gray-800 dark:border-gray-700',
|
||||
].join(' ')}
|
||||
>
|
||||
<Typography.Text small>{route.label}</Typography.Text>
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { FC, ReactNode } from 'react'
|
||||
import { isUndefined } from 'lodash'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
|
||||
import { useStore } from 'hooks'
|
||||
@@ -9,24 +10,26 @@ import { useRouter } from 'next/router'
|
||||
|
||||
interface Props {
|
||||
title?: string
|
||||
className?: string
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
const SettingsLayout: FC<Props> = ({ title, className = "", children }) => {
|
||||
const SettingsLayout: FC<Props> = ({ title, children }) => {
|
||||
const { ui } = useStore()
|
||||
const projectRef = ui.selectedProject?.ref ?? 'default'
|
||||
|
||||
const router = useRouter()
|
||||
const page = router.pathname.split('/')[4]
|
||||
const logType = router.query.type
|
||||
|
||||
const pageKey = !isUndefined(logType) ? `logs-${logType}` : page
|
||||
|
||||
return (
|
||||
<BaseLayout
|
||||
title={title || 'Settings'}
|
||||
product="Settings"
|
||||
productMenu={<ProductMenu page={page} menu={generateSettingsMenu(projectRef)} />}
|
||||
productMenu={<ProductMenu page={pageKey} menu={generateSettingsMenu(projectRef)} />}
|
||||
>
|
||||
<main style={{ maxHeight: '100vh' }} className={"flex-1 overflow-y-auto " + className}>
|
||||
<main style={{ maxHeight: '100vh' }} className="flex-1 overflow-y-auto">
|
||||
{children}
|
||||
</main>
|
||||
</BaseLayout>
|
||||
|
||||
@@ -1,15 +1,7 @@
|
||||
import { ProductMenuGroup } from 'components/ui/ProductMenu/ProductMenu.types'
|
||||
// import { LOG_TYPE_LABEL_MAPPING } from "lib/constants"
|
||||
export const generateSettingsMenu = (ref: string): ProductMenuGroup[] => {
|
||||
// const logItems = ['rest', 'realtime', 'auth', 'storage', 'database'].map((type: string) => {
|
||||
// return {
|
||||
// name: LOG_TYPE_LABEL_MAPPING[type],
|
||||
// key: `logs-${type}`,
|
||||
// url: `/project/${ref}/settings/logs/${type}`,
|
||||
// items: []
|
||||
// }
|
||||
// })
|
||||
import { LOG_TYPE_LABEL_MAPPING } from 'lib/constants'
|
||||
|
||||
export const generateSettingsMenu = (ref: string): ProductMenuGroup[] => {
|
||||
return [
|
||||
{
|
||||
title: 'Project settings',
|
||||
@@ -26,9 +18,17 @@ export const generateSettingsMenu = (ref: string): ProductMenuGroup[] => {
|
||||
},
|
||||
],
|
||||
},
|
||||
// {
|
||||
// title: 'Logs',
|
||||
// items: logItems,
|
||||
// },
|
||||
{
|
||||
title: 'Logs',
|
||||
// items: ['database', 'auth', 'realtime', 'rest', 'storage'].map((type: string) => {
|
||||
items: ['database', 'rest'].map((type: string) => {
|
||||
return {
|
||||
name: LOG_TYPE_LABEL_MAPPING[type],
|
||||
key: `logs-${type}`,
|
||||
url: `/project/${ref}/settings/logs/${type}`,
|
||||
items: [],
|
||||
}
|
||||
}),
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -75,7 +75,9 @@ const TableEditorMenu: FC<Props> = ({
|
||||
}
|
||||
|
||||
const schemas: PostgresSchema[] = meta.schemas.list()
|
||||
const tables: PostgresTable[] = meta.tables.list((table: PostgresTable) => table.schema === selectedSchema)
|
||||
const tables: PostgresTable[] = meta.tables.list(
|
||||
(table: PostgresTable) => table.schema === selectedSchema
|
||||
)
|
||||
|
||||
const schemaTables =
|
||||
searchText.length === 0
|
||||
@@ -89,7 +91,7 @@ const TableEditorMenu: FC<Props> = ({
|
||||
|
||||
// Temp fix - Ideally we'd just take up all the remaining space but
|
||||
// can't seem to figure that out immediately
|
||||
const maxScrollHeight = schemaViews.length > 0 ? 270 : 520
|
||||
const maxScrollHeight = schemaViews.length > 0 ? 270 : 515
|
||||
|
||||
return (
|
||||
<div className="my-6 flex flex-col flex-grow">
|
||||
|
||||
@@ -57,7 +57,6 @@ const PolicySelection = ({
|
||||
target="_blank"
|
||||
className="text-green-400"
|
||||
href="https://supabase.com/docs/guides/auth#policies"
|
||||
as="https://supabase.com/docs/guides/auth#policies"
|
||||
>
|
||||
here
|
||||
</a>
|
||||
|
||||
@@ -78,7 +78,7 @@ const ChartHandler: FC<Props> = ({
|
||||
|
||||
const { error, ...res } = await get(url)
|
||||
|
||||
if (error && !cancel) {
|
||||
if ((error || isUndefined(res)) && !cancel) {
|
||||
setFetching(false)
|
||||
setFetchedData(undefined)
|
||||
return console.error('Chart error:', error)
|
||||
@@ -86,12 +86,14 @@ const ChartHandler: FC<Props> = ({
|
||||
|
||||
// Convert null values to 0
|
||||
// TODO: Chart endpoint should handle this data formatting to 0 instead of client.
|
||||
const formattedChartData = res.data.map((dataPoint: Dictionary<any>, idx: number) => {
|
||||
return {
|
||||
...dataPoint,
|
||||
[attribute]: dataPoint[attribute] ? Number(dataPoint[attribute]) : 0,
|
||||
const formattedChartData = (res?.data ?? []).map(
|
||||
(dataPoint: Dictionary<any>, idx: number) => {
|
||||
return {
|
||||
...dataPoint,
|
||||
[attribute]: dataPoint[attribute] ? Number(dataPoint[attribute]) : 0,
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
if (!cancel) {
|
||||
setFetchedData({ ...res, data: formattedChartData })
|
||||
|
||||
@@ -8,6 +8,7 @@ import { API_URL } from 'lib/constants'
|
||||
import Panel from './Panel'
|
||||
|
||||
const { Text, Title } = Typography
|
||||
const API_SERVICE_ID = 1
|
||||
|
||||
export const DisplayApiSettings = () => {
|
||||
const router = useRouter()
|
||||
@@ -36,12 +37,9 @@ export const DisplayApiSettings = () => {
|
||||
</ApiContentWrapper>
|
||||
)
|
||||
|
||||
const { services } = data
|
||||
|
||||
// Get the API service
|
||||
const API_SERVICE_ID = 1
|
||||
let apiService = services.find((x: any) => x.app.id == API_SERVICE_ID)
|
||||
let apiKeys = apiService.service_api_keys
|
||||
const apiService = (data?.services ?? []).find((x: any) => x.app.id == API_SERVICE_ID)
|
||||
const apiKeys = apiService.service_api_keys
|
||||
|
||||
return (
|
||||
<ApiContentWrapper>
|
||||
@@ -124,13 +122,10 @@ export const DisplayConfigSettings = () => {
|
||||
</ConfigContentWrapper>
|
||||
)
|
||||
|
||||
const { project, services } = data
|
||||
|
||||
// Get the API service
|
||||
const API_SERVICE_ID = 1
|
||||
let jwtSecret = project.jwt_secret ?? ''
|
||||
let apiService = services.find((x: any) => x.app.id == API_SERVICE_ID)
|
||||
let apiConfig = apiService.app_config
|
||||
const jwtSecret = data?.project.jwt_secret ?? ''
|
||||
const apiService = (data?.services ?? []).find((x: any) => x.app.id == API_SERVICE_ID)
|
||||
const apiConfig = apiService.app_config
|
||||
|
||||
return (
|
||||
<section>
|
||||
@@ -159,7 +154,7 @@ export const DisplayConfigSettings = () => {
|
||||
readOnly
|
||||
copy
|
||||
reveal
|
||||
disabled={true}
|
||||
disabled
|
||||
value={jwtSecret}
|
||||
className="input-mono"
|
||||
descriptionText="Used to decode your JWTs. You can also use this to mint your own JWTs."
|
||||
|
||||
@@ -22,7 +22,7 @@ const Param = ({
|
||||
{format && format != type && (
|
||||
<div className="mb-4">
|
||||
<h5 className="uppercase text-xs font-medium text-gray-400">Format</h5>
|
||||
<code className="p-0 text-sm bg-gray-700">{format}</code>
|
||||
<code className="p-0 text-sm bg-white dark:bg-gray-700">{format}</code>
|
||||
</div>
|
||||
)}
|
||||
{description !== false && (
|
||||
|
||||
@@ -3,11 +3,11 @@ const snippets = {
|
||||
title: 'API URL',
|
||||
bash: {
|
||||
language: 'bash',
|
||||
code: `https://${endpoint}`,
|
||||
code: `${endpoint}`,
|
||||
},
|
||||
js: {
|
||||
language: 'bash',
|
||||
code: `https://${endpoint}`,
|
||||
code: `${endpoint}`,
|
||||
},
|
||||
}),
|
||||
install: () => ({
|
||||
@@ -29,7 +29,7 @@ const snippets = {
|
||||
code: `
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
|
||||
const supabaseUrl = 'https://${endpoint}'
|
||||
const supabaseUrl = '${endpoint}'
|
||||
const supabaseKey = process.env.SUPABASE_KEY
|
||||
const supabase = createClient(supabaseUrl, supabaseKey)`,
|
||||
},
|
||||
@@ -50,7 +50,7 @@ const supabase = createClient(supabaseUrl, supabaseKey)`,
|
||||
bash: {
|
||||
language: 'bash',
|
||||
code: `
|
||||
curl 'https://${endpoint}/rest/v1/' \\
|
||||
curl '${endpoint}/rest/v1/' \\
|
||||
-H "apikey: ${defaultApiKey}" ${
|
||||
showBearer
|
||||
? `\\
|
||||
@@ -62,7 +62,7 @@ curl 'https://${endpoint}/rest/v1/' \\
|
||||
js: {
|
||||
language: 'js',
|
||||
code: `
|
||||
const SUPABASE_URL = "https://${endpoint}"
|
||||
const SUPABASE_URL = "${endpoint}"
|
||||
|
||||
const supabase = createClient(SUPABASE_URL, process.env.${keyName || 'SUPABASE_KEY'});
|
||||
`,
|
||||
@@ -87,7 +87,7 @@ const supabase = createClient(SUPABASE_URL, process.env.${keyName || 'SUPABASE_K
|
||||
bash: {
|
||||
language: 'bash',
|
||||
code: `
|
||||
curl -X POST 'https://${endpoint}/rest/v1/rpc/${rpcName}' \\${bashParams}
|
||||
curl -X POST '${endpoint}/rest/v1/rpc/${rpcName}' \\${bashParams}
|
||||
-H "Content-Type: application/json" \\
|
||||
-H "apikey: ${apiKey}" ${
|
||||
showBearer
|
||||
@@ -199,7 +199,7 @@ const ${listenerName} = supabase
|
||||
bash: {
|
||||
language: 'bash',
|
||||
code: `
|
||||
curl 'https://${endpoint}/rest/v1/${resourceId}?select=*' \\
|
||||
curl '${endpoint}/rest/v1/${resourceId}?select=*' \\
|
||||
-H "apikey: ${apiKey}" \\
|
||||
-H "Authorization: Bearer ${apiKey}"
|
||||
`,
|
||||
@@ -224,7 +224,7 @@ let { data: ${resourceId}, error } = await supabase
|
||||
bash: {
|
||||
language: 'bash',
|
||||
code: `
|
||||
curl 'https://${endpoint}/rest/v1/${resourceId}?select=${columnName}' \\
|
||||
curl '${endpoint}/rest/v1/${resourceId}?select=${columnName}' \\
|
||||
-H "apikey: ${apiKey}" \\
|
||||
-H "Authorization: Bearer ${apiKey}"
|
||||
`,
|
||||
@@ -243,7 +243,7 @@ let { data: ${resourceId}, error } = await supabase
|
||||
bash: {
|
||||
language: 'bash',
|
||||
code: `
|
||||
curl 'https://${endpoint}/rest/v1/${resourceId}?select=some_column,other_table(foreign_key)' \\
|
||||
curl '${endpoint}/rest/v1/${resourceId}?select=some_column,other_table(foreign_key)' \\
|
||||
-H "apikey: ${apiKey}" \\
|
||||
-H "Authorization: Bearer ${apiKey}"
|
||||
`,
|
||||
@@ -267,7 +267,7 @@ let { data: ${resourceId}, error } = await supabase
|
||||
bash: {
|
||||
language: 'bash',
|
||||
code: `
|
||||
curl 'https://${endpoint}/rest/v1/${resourceId}?select=*' \\
|
||||
curl '${endpoint}/rest/v1/${resourceId}?select=*' \\
|
||||
-H "apikey: ${apiKey}" \\
|
||||
-H "Authorization: Bearer ${apiKey}" \\
|
||||
-H "Range: 0-9"
|
||||
@@ -288,7 +288,7 @@ let { data: ${resourceId}, error } = await supabase
|
||||
bash: {
|
||||
language: 'bash',
|
||||
code: `
|
||||
curl 'https://${endpoint}/rest/v1/${resourceId}?id=eq.1&select=*' \\
|
||||
curl '${endpoint}/rest/v1/${resourceId}?id=eq.1&select=*' \\
|
||||
-H "apikey: ${apiKey}" \\
|
||||
-H "Authorization: Bearer ${apiKey}" \\
|
||||
-H "Range: 0-9"
|
||||
@@ -325,7 +325,7 @@ let { data: ${resourceId}, error } = await supabase
|
||||
bash: {
|
||||
language: 'bash',
|
||||
code: `
|
||||
curl -X POST 'https://${endpoint}/rest/v1/${resourceId}' \\
|
||||
curl -X POST '${endpoint}/rest/v1/${resourceId}' \\
|
||||
-H "apikey: ${apiKey}" \\
|
||||
-H "Authorization: Bearer ${apiKey}" \\
|
||||
-H "Content-Type: application/json" \\
|
||||
@@ -349,7 +349,7 @@ const { data, error } = await supabase
|
||||
bash: {
|
||||
language: 'bash',
|
||||
code: `
|
||||
curl -X POST 'https://${endpoint}/rest/v1/${resourceId}' \\
|
||||
curl -X POST '${endpoint}/rest/v1/${resourceId}' \\
|
||||
-H "apikey: ${apiKey}" \\
|
||||
-H "Authorization: Bearer ${apiKey}" \\
|
||||
-H "Content-Type: application/json" \\
|
||||
@@ -373,7 +373,7 @@ const { data, error } = await supabase
|
||||
bash: {
|
||||
language: 'bash',
|
||||
code: `
|
||||
curl -X POST 'https://${endpoint}/rest/v1/${resourceId}' \\
|
||||
curl -X POST '${endpoint}/rest/v1/${resourceId}' \\
|
||||
-H "apikey: ${apiKey}" \\
|
||||
-H "Authorization: Bearer ${apiKey}" \\
|
||||
-H "Content-Type: application/json" \\
|
||||
@@ -395,7 +395,7 @@ const { data, error } = await supabase
|
||||
bash: {
|
||||
language: 'bash',
|
||||
code: `
|
||||
curl -X PATCH 'https://${endpoint}/rest/v1/${resourceId}?some_column=eq.someValue' \\
|
||||
curl -X PATCH '${endpoint}/rest/v1/${resourceId}?some_column=eq.someValue' \\
|
||||
-H "apikey: ${apiKey}" \\
|
||||
-H "Authorization: Bearer ${apiKey}" \\
|
||||
-H "Content-Type: application/json" \\
|
||||
@@ -418,7 +418,7 @@ const { data, error } = await supabase
|
||||
bash: {
|
||||
language: 'bash',
|
||||
code: `
|
||||
curl -X DELETE 'https://${endpoint}/rest/v1/${resourceId}?some_column=eq.someValue' \\
|
||||
curl -X DELETE '${endpoint}/rest/v1/${resourceId}?some_column=eq.someValue' \\
|
||||
-H "apikey: ${apiKey}" \\
|
||||
-H "Authorization: Bearer ${apiKey}"
|
||||
`,
|
||||
@@ -438,7 +438,7 @@ const { data, error } = await supabase
|
||||
bash: {
|
||||
language: 'bash',
|
||||
code: `
|
||||
curl -X POST 'https://${endpoint}/auth/v1/signup' \\
|
||||
curl -X POST '${endpoint}/auth/v1/signup' \\
|
||||
-H "apikey: ${apiKey}" \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-d '{
|
||||
@@ -462,7 +462,7 @@ let { user, error } = await supabase.auth.signUp({
|
||||
bash: {
|
||||
language: 'bash',
|
||||
code: `
|
||||
curl -X POST 'https://${endpoint}/auth/v1/token?grant_type=password' \\
|
||||
curl -X POST '${endpoint}/auth/v1/token?grant_type=password' \\
|
||||
-H "apikey: ${apiKey}" \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-d '{
|
||||
@@ -486,7 +486,7 @@ let { user, error } = await supabase.auth.signIn({
|
||||
bash: {
|
||||
language: 'bash',
|
||||
code: `
|
||||
curl -X POST 'https://${endpoint}/auth/v1/magiclink' \\
|
||||
curl -X POST '${endpoint}/auth/v1/magiclink' \\
|
||||
-H "apikey: ${apiKey}" \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-d '{
|
||||
@@ -508,7 +508,7 @@ let { user, error } = await supabase.auth.signIn({
|
||||
bash: {
|
||||
language: 'bash',
|
||||
code: `
|
||||
curl -X POST 'https://${endpoint}/auth/v1/signup' \\
|
||||
curl -X POST '${endpoint}/auth/v1/signup' \\
|
||||
-H "apikey: ${apiKey}" \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-d '{
|
||||
@@ -532,7 +532,7 @@ let { user, error } = await supabase.auth.signUp({
|
||||
bash: {
|
||||
language: 'bash',
|
||||
code: `
|
||||
curl -X POST 'https://${endpoint}/auth/v1/otp' \\
|
||||
curl -X POST '${endpoint}/auth/v1/otp' \\
|
||||
-H "apikey: ${apiKey}" \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-d '{
|
||||
@@ -554,7 +554,7 @@ let { user, error } = await supabase.auth.signIn({
|
||||
bash: {
|
||||
language: 'bash',
|
||||
code: `
|
||||
curl -X POST 'https://${endpoint}/auth/v1/verify' \\
|
||||
curl -X POST '${endpoint}/auth/v1/verify' \\
|
||||
-H "apikey: ${apiKey}" \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-d '{
|
||||
@@ -579,7 +579,7 @@ let { session, error } = await supabase.auth.verifyOTP({
|
||||
bash: {
|
||||
language: 'bash',
|
||||
code: `
|
||||
curl -X POST 'https://${endpoint}/auth/v1/invite' \\
|
||||
curl -X POST '${endpoint}/auth/v1/invite' \\
|
||||
-H "apikey: ${apiKey}" \\
|
||||
-H "Authorization: Bearer ${apiKey}" \\
|
||||
-H "Content-Type: application/json" \\
|
||||
@@ -617,7 +617,7 @@ let { user, error } = await supabase.auth.signIn({
|
||||
bash: {
|
||||
language: 'bash',
|
||||
code: `
|
||||
curl -X GET 'https://${endpoint}/auth/v1/user' \\
|
||||
curl -X GET '${endpoint}/auth/v1/user' \\
|
||||
-H "apikey: ${apiKey}" \\
|
||||
-H "Authorization: Bearer USER_TOKEN"
|
||||
`,
|
||||
@@ -634,7 +634,7 @@ const user = supabase.auth.user()
|
||||
bash: {
|
||||
language: 'bash',
|
||||
code: `
|
||||
curl -X POST 'https://${endpoint}/auth/v1/recover' \\
|
||||
curl -X POST '${endpoint}/auth/v1/recover' \\
|
||||
-H "apikey: ${apiKey}" \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-d '{
|
||||
@@ -654,7 +654,7 @@ let { data, error } = await supabase.auth.api.resetPasswordForEmail(email)
|
||||
bash: {
|
||||
language: 'bash',
|
||||
code: `
|
||||
curl -X PUT 'https://${endpoint}/auth/v1/user' \\
|
||||
curl -X PUT '${endpoint}/auth/v1/user' \\
|
||||
-H "apikey: ${apiKey}" \\
|
||||
-H "Authorization: Bearer <USERS-ACCESS-TOKEN>" \\
|
||||
-H "Content-Type: application/json" \\
|
||||
@@ -683,7 +683,7 @@ const { user, error } = await supabase.auth.update({
|
||||
bash: {
|
||||
language: 'bash',
|
||||
code: `
|
||||
curl -X POST 'https://${endpoint}/auth/v1/logout' \\
|
||||
curl -X POST '${endpoint}/auth/v1/logout' \\
|
||||
-H "apikey: ${apiKey}" \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-H "Authorization: Bearer USER_TOKEN"'
|
||||
|
||||
@@ -22,7 +22,7 @@ function Panel(props: any) {
|
||||
<div
|
||||
className={`
|
||||
border border-panel-border-light dark:border-panel-border-dark
|
||||
rounded ${props.noMargin? "" : "mb-8"} ${props.className}`}
|
||||
rounded ${props.noMargin ? '' : 'mb-8'} ${props.className}`}
|
||||
>
|
||||
{props.title && (
|
||||
<div className={headerClasses.join(' ')}>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { FC, useEffect, useRef } from 'react'
|
||||
import Editor, { useMonaco } from '@monaco-editor/react'
|
||||
import Editor from '@monaco-editor/react'
|
||||
|
||||
import { timeout } from 'lib/helpers'
|
||||
import Connecting from '../Loading'
|
||||
@@ -13,20 +13,17 @@ interface Props {
|
||||
onInputChange: (value?: string) => void
|
||||
onInputRun?: (value: string) => void
|
||||
hideLineNumbers?: boolean
|
||||
className?: string
|
||||
}
|
||||
|
||||
const CodeEditor: FC<Props> = ({
|
||||
id,
|
||||
language,
|
||||
defaultValue,
|
||||
isReadOnly,
|
||||
onInputChange = () => { },
|
||||
onInputRun = () => { },
|
||||
hideLineNumbers,
|
||||
className = ""
|
||||
isReadOnly = false,
|
||||
hideLineNumbers = false,
|
||||
onInputChange = () => {},
|
||||
onInputRun = () => {},
|
||||
}) => {
|
||||
const monaco = useMonaco()
|
||||
const editorRef = useRef()
|
||||
|
||||
useEffect(() => {
|
||||
@@ -60,7 +57,7 @@ const CodeEditor: FC<Props> = ({
|
||||
<Editor
|
||||
path={id}
|
||||
theme="supabase"
|
||||
className={"monaco-editor " + className}
|
||||
className="monaco-editor"
|
||||
defaultLanguage={language}
|
||||
defaultValue={defaultValue}
|
||||
loading={<Connecting />}
|
||||
@@ -75,7 +72,7 @@ const CodeEditor: FC<Props> = ({
|
||||
lineNumbers: hideLineNumbers ? 'off' : undefined,
|
||||
glyphMargin: hideLineNumbers ? false : undefined,
|
||||
lineNumbersMinChars: hideLineNumbers ? 0 : undefined,
|
||||
folding: hideLineNumbers? false : undefined
|
||||
folding: hideLineNumbers ? false : undefined,
|
||||
}}
|
||||
onMount={onMount}
|
||||
onChange={onInputChange}
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
import React from 'react'
|
||||
import { Button, Input, Dropdown, Typography, IconChevronDown } from '@supabase/ui'
|
||||
import Panel from 'components/to-be-cleaned/Panel'
|
||||
|
||||
interface Props {
|
||||
showReset: boolean
|
||||
onReset: () => void
|
||||
onRefresh?: () => void
|
||||
onSearch?: (query: string) => void
|
||||
onCustomClick?: () => void
|
||||
isLoading: boolean
|
||||
children?: JSX.Element
|
||||
searchValue?: string
|
||||
templates?: {
|
||||
label: string
|
||||
onClick: () => void
|
||||
}[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs control panel header + wrapper
|
||||
*/
|
||||
const LogPanel = ({
|
||||
onRefresh,
|
||||
isLoading,
|
||||
searchValue,
|
||||
children,
|
||||
onSearch,
|
||||
onCustomClick,
|
||||
showReset,
|
||||
onReset,
|
||||
templates = [] }: Props) => (
|
||||
<Panel
|
||||
title={
|
||||
<div className="flex items-center justify-between w-full" >
|
||||
<div className="flex flex-row gap-x-4">
|
||||
<Dropdown
|
||||
side="bottom"
|
||||
align="start"
|
||||
overlay={templates.map(p =>
|
||||
<Dropdown.Item key={p.label} onClick={p.onClick}>
|
||||
<Typography.Text>{p.label}</Typography.Text>
|
||||
</Dropdown.Item>
|
||||
)}
|
||||
>
|
||||
<Button
|
||||
iconRight={<IconChevronDown />}
|
||||
type="secondary" onClick={() => console.log("templates clicked")}>Templates</Button>
|
||||
</Dropdown>
|
||||
|
||||
<Button type="secondary" onClick={onCustomClick}>Custom query</Button>
|
||||
{showReset && (
|
||||
<Button type="dashed" onClick={onReset}>Reset filters</Button>)}
|
||||
</div>
|
||||
<div className="flex flex-row gap-x-4">
|
||||
<Input className="max-w-32" placeholder="Filter" onChange={e => {
|
||||
if (onSearch) onSearch(e.target.value)
|
||||
}} value={searchValue} />
|
||||
<Button
|
||||
type="outline"
|
||||
loading={isLoading}
|
||||
disabled={isLoading ? true : false}
|
||||
onClick={onRefresh}
|
||||
>
|
||||
Refresh
|
||||
</Button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</Panel>
|
||||
)
|
||||
export default LogPanel
|
||||
@@ -1,32 +0,0 @@
|
||||
import React from 'react'
|
||||
import { Button, Input, Typography } from '@supabase/ui'
|
||||
import Panel from 'components/to-be-cleaned/Panel'
|
||||
import { LogData } from "./LogTable"
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
interface Props {
|
||||
log: LogData
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Log selection display
|
||||
*/
|
||||
const LogSelection = ({ log, onClose }: Props) => (
|
||||
<Panel
|
||||
title={
|
||||
<div className="flex flex-row justify-between items-center w-full">
|
||||
<Typography.Title level={5}>{dayjs(log.timestamp / 1000).toString()}</Typography.Title>
|
||||
<Button size="tiny" type="secondary" onClick={onClose}>Close</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="p-4 h-full flex-col">
|
||||
<Typography.Text code className="block w-full overflow-x-auto" small>{log.event_message} </Typography.Text>
|
||||
<Input.TextArea label="Metadata" className="font-mono mt-4" size="tiny" rows={25} disabled value={JSON.stringify(log.metadata, null, 2)} />
|
||||
</div>
|
||||
|
||||
|
||||
</Panel>
|
||||
)
|
||||
export default LogSelection
|
||||
@@ -1,79 +0,0 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { Typography } from '@supabase/ui'
|
||||
import dayjs from 'dayjs'
|
||||
import LogSelection from './LogSelection'
|
||||
import DataGrid from '@supabase/react-data-grid'
|
||||
interface Props {
|
||||
data: LogData[] | null | undefined
|
||||
}
|
||||
|
||||
interface Metadata {
|
||||
[key: string]: string | number | Object | Object[]
|
||||
}
|
||||
export interface LogData {
|
||||
id: string
|
||||
timestamp: number
|
||||
event_message: string
|
||||
metadata: Metadata
|
||||
}
|
||||
|
||||
interface Column {
|
||||
key: string
|
||||
name: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs table view with focus side panel
|
||||
*/
|
||||
const LogTable = ({ data }: Props) => {
|
||||
const [focusedLog, setFocusedLog] = useState<LogData | null>(null);
|
||||
const columns = ["timestamp", "event_message"].map(v => ({
|
||||
key: v, name: v,
|
||||
width: v === "timestamp" ? 240 : undefined,
|
||||
resizable: true,
|
||||
headerRenderer: () => {
|
||||
return <div className="flex items-center justify-center font-mono h-full">{v}</div>
|
||||
},
|
||||
formatter: ({ row }: any) => {
|
||||
let value = row[v]
|
||||
if (v === 'timestamp') {
|
||||
value = dayjs(Number(row["timestamp"]) / 1000).toString()
|
||||
}
|
||||
return <p className={`block whitespace-line-wrap ${row.id === focusedLog?.id ? "font-bold" : ""}`}>{value}</p>
|
||||
}
|
||||
}))
|
||||
|
||||
const logMap = (data || []).reduce((acc: any, d) => {
|
||||
acc[d.id] = d
|
||||
return acc
|
||||
}, {})
|
||||
useEffect(() => {
|
||||
if (!data) return
|
||||
if (focusedLog && !(focusedLog.id in logMap)) {
|
||||
setFocusedLog(null)
|
||||
}
|
||||
}, [Object.keys(logMap)])
|
||||
|
||||
if (!data) return null
|
||||
|
||||
return (<section className="flex flex-row gap-x-4">
|
||||
<DataGrid
|
||||
style={{ height: "unset", minHeight: "75vh" }}
|
||||
className="flex-grow flex-1"
|
||||
onSelectedCellChange={({ idx, rowIdx }) => setFocusedLog(data[rowIdx])}
|
||||
noRowsFallback={<Typography.Text className="text-center">No data returned from query</Typography.Text>}
|
||||
columns={columns as any}
|
||||
rowClass={r => `${r.id === focusedLog?.id ? "bg-green-800" : "cursor-pointer"}`}
|
||||
rows={data}
|
||||
rowKeyGetter={r => r.id}
|
||||
onRowClick={r => setFocusedLog(logMap[r.id])}
|
||||
/>
|
||||
{focusedLog && (
|
||||
<div className="w-2/5">
|
||||
<LogSelection onClose={() => setFocusedLog(null)} log={focusedLog} />
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
export default LogTable
|
||||
@@ -29,11 +29,11 @@ const SparkBar: FC<Props> = ({
|
||||
return (
|
||||
<div className="flex flex-col w-full">
|
||||
{hasLabels && (
|
||||
<div className="flex justify-between pb-1">
|
||||
<span className="text-typography-body-light dark:text-typography-body-dark text-sm">
|
||||
<div className="flex justify-between pb-1 space-x-8">
|
||||
<span className="text-typography-body-light dark:text-typography-body-dark text-sm truncate">
|
||||
{labelBottom}
|
||||
</span>
|
||||
<span className="text-typography-body-light dark:text-typography-body-dark text-sm">
|
||||
<span className="text-typography-body-light dark:text-typography-body-dark text-sm tabular-nums">
|
||||
{labelTop}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { useMonaco } from '@monaco-editor/react'
|
||||
import { getTheme } from 'components/ui/CodeEditor'
|
||||
import { autorun } from 'mobx'
|
||||
import { createContext, FC, useContext, useEffect } from 'react'
|
||||
import toast from 'react-hot-toast'
|
||||
|
||||
import { IRootStore } from 'stores'
|
||||
import { getTheme } from 'components/ui/CodeEditor'
|
||||
import SparkBar from 'components/ui/SparkBar'
|
||||
|
||||
const StoreContext = createContext<IRootStore>(undefined!)
|
||||
|
||||
@@ -36,17 +38,36 @@ export const StoreProvider: FC<StoreProvider> = ({ children, rootStore }) => {
|
||||
|
||||
autorun(() => {
|
||||
if (ui.notification) {
|
||||
const { id, category, error, message } = ui.notification
|
||||
const { id, category, error, message, progress } = ui.notification
|
||||
switch (category) {
|
||||
case 'info':
|
||||
return toast(message, { id })
|
||||
case 'success':
|
||||
return toast.success(message, { id })
|
||||
case 'error':
|
||||
console.error('Notification', { error, message })
|
||||
console.error('Error:', { error, message })
|
||||
return toast.error(message, { id })
|
||||
case 'loading':
|
||||
return toast.loading(message, { id })
|
||||
if (progress) {
|
||||
return toast.loading(
|
||||
<div
|
||||
className="flex flex-col space-y-1"
|
||||
style={{ minWidth: '200px', maxWidth: '267px' }}
|
||||
>
|
||||
<SparkBar
|
||||
value={progress}
|
||||
max={100}
|
||||
type="horizontal"
|
||||
barClass="bg-green-500"
|
||||
labelBottom={message}
|
||||
labelTop={`${progress.toFixed(2)}%`}
|
||||
/>
|
||||
</div>,
|
||||
{ id }
|
||||
)
|
||||
} else {
|
||||
return toast.loading(message, { id })
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -4,6 +4,8 @@ import { readOnly } from './supabaseClient'
|
||||
import { SupaResponse, User } from 'types'
|
||||
import { auth0 } from './auth0'
|
||||
import { flattenNamespaceOnUser } from './apiHelpers'
|
||||
import { config } from 'process'
|
||||
import { getAuth0Id, getAuthUser, getIdentity, GOTRUE_ENABLED } from 'lib/gotrue'
|
||||
|
||||
/**
|
||||
* Use this method on api routes to check if user is authenticated and having required permissions.
|
||||
@@ -32,7 +34,7 @@ export async function apiAuthenticate(
|
||||
try {
|
||||
// Check that they are logged in
|
||||
// If no error throw from getAccessToken. That's good, we can continue
|
||||
await auth0.getAccessToken(req, res)
|
||||
if (!(GOTRUE_ENABLED)) await auth0.getAccessToken(req, res)
|
||||
|
||||
const user = await fetchUser(req, res)
|
||||
if (!user) {
|
||||
@@ -53,16 +55,41 @@ export async function apiAuthenticate(
|
||||
* user with only id prop or detail object. It depends on requireUserDetail config
|
||||
*/
|
||||
async function fetchUser(req: NextApiRequest, res: NextApiResponse): Promise<any> {
|
||||
const session = auth0.getSession(req, res)
|
||||
let user_id_supabase = null
|
||||
let user_id_auth0 = null
|
||||
let gotrue_id = null
|
||||
let email = null
|
||||
if (GOTRUE_ENABLED) {
|
||||
const token = req.headers.authorization
|
||||
if (!token) {
|
||||
return res.status(401).end('Unauthorized: missing access token')
|
||||
}
|
||||
let { user: gotrue_user, error: authError } = await getAuthUser(token)
|
||||
if (authError) {
|
||||
throw authError
|
||||
}
|
||||
if (gotrue_user !== null) {
|
||||
gotrue_id = gotrue_user?.id
|
||||
email = gotrue_user.email
|
||||
|
||||
if (!session) {
|
||||
return null
|
||||
let { identity, error } = getIdentity(gotrue_user)
|
||||
if (error) throw error
|
||||
if (identity?.provider !== undefined) {
|
||||
user_id_auth0 = getAuth0Id(identity?.provider, identity?.id)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const session = auth0.getSession(req, res)
|
||||
if (!session) {
|
||||
return null
|
||||
}
|
||||
const { user: auth0_user } = session
|
||||
const flattened = flattenNamespaceOnUser('https://supabase.io', auth0_user)
|
||||
user_id_supabase = flattened.user_id_supabase
|
||||
user_id_auth0 = flattened.user_id_auth0
|
||||
email = flattened.email
|
||||
}
|
||||
|
||||
const { user: auth0_user } = session
|
||||
const flattened = flattenNamespaceOnUser('https://supabase.io', auth0_user)
|
||||
const { user_id_supabase, user_id_auth0, email } = flattened
|
||||
|
||||
if (user_id_supabase) {
|
||||
return {
|
||||
id: user_id_supabase,
|
||||
@@ -70,15 +97,15 @@ async function fetchUser(req: NextApiRequest, res: NextApiResponse): Promise<any
|
||||
}
|
||||
}
|
||||
|
||||
const { data } = await readOnly
|
||||
.from('users')
|
||||
.select(
|
||||
`
|
||||
const query = readOnly.from('users').select(
|
||||
`
|
||||
id, auth0_id, primary_email, username, first_name, last_name, mobile, is_alpha_user
|
||||
`
|
||||
)
|
||||
.eq('auth0_id', user_id_auth0)
|
||||
.single()
|
||||
)
|
||||
|
||||
const { data } = GOTRUE_ENABLED
|
||||
? await query.eq('gotrue_id', gotrue_id).single()
|
||||
: await await query.eq('auth0_id', user_id_auth0).single()
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { GOTRUE_ENABLED } from 'lib/gotrue'
|
||||
import { tryParseJson } from 'lib/helpers'
|
||||
import { isUndefined } from 'lodash'
|
||||
import { SupaResponse } from 'types/base'
|
||||
|
||||
export function handleError<T>(e: any, requestId: string): SupaResponse<T> {
|
||||
@@ -65,3 +68,53 @@ export async function handleResponseError<T = unknown>(
|
||||
return { error } as unknown as SupaResponse<T>
|
||||
}
|
||||
}
|
||||
|
||||
export function getAccessToken() {
|
||||
// ignore if server-side
|
||||
if (typeof window === 'undefined') return ''
|
||||
|
||||
const tokenData = window?.localStorage['supabase.auth.token']
|
||||
if (!tokenData) {
|
||||
// try to get from url fragment
|
||||
const access_token = getParameterByName('access_token')
|
||||
if (access_token) return access_token
|
||||
else return undefined
|
||||
}
|
||||
const tokenObj = tryParseJson(tokenData)
|
||||
if (tokenObj === false) {
|
||||
return ''
|
||||
}
|
||||
return tokenObj.currentSession.access_token
|
||||
}
|
||||
|
||||
// get param from URL fragment
|
||||
export function getParameterByName(name: string, url?: string) {
|
||||
// ignore if server-side
|
||||
if (typeof window === 'undefined') return ''
|
||||
|
||||
if (!url) url = window?.location?.href || ''
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
name = name.replace(/[\[\]]/g, '\\$&')
|
||||
const regex = new RegExp('[?&#]' + name + '(=([^&#]*)|&|#|$)'),
|
||||
results = regex.exec(url)
|
||||
if (!results) return null
|
||||
if (!results[2]) return ''
|
||||
return decodeURIComponent(results[2].replace(/\+/g, ' '))
|
||||
}
|
||||
|
||||
export function constructHeaders(requestId: string, optionHeaders?: { [prop: string]: any }) {
|
||||
let headers: { [prop: string]: any } = {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
'X-Request-Id': requestId,
|
||||
...optionHeaders,
|
||||
}
|
||||
|
||||
const hasAuthHeader = !isUndefined(optionHeaders) && 'Authorization' in optionHeaders
|
||||
if (GOTRUE_ENABLED && !hasAuthHeader) {
|
||||
const accessToken = getAccessToken()
|
||||
if (accessToken) headers.Authorization = `Bearer ${accessToken}`
|
||||
}
|
||||
|
||||
return headers
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { handleError, handleResponse, handleResponseError } from './base'
|
||||
import { handleError, handleResponse, handleResponseError, constructHeaders } from './base'
|
||||
import { uuidv4 } from '../../helpers'
|
||||
import { SupaResponse } from 'types/base'
|
||||
|
||||
@@ -8,19 +8,15 @@ export async function delete_<T = any>(
|
||||
options?: { [prop: string]: any }
|
||||
): Promise<SupaResponse<T>> {
|
||||
const requestId = uuidv4()
|
||||
const { headers, ...otherOptions } = options ?? {}
|
||||
try {
|
||||
const { headers: optionHeaders, ...otherOptions } = options ?? {}
|
||||
const headers = constructHeaders(requestId, optionHeaders)
|
||||
const response = await fetch(url, {
|
||||
method: 'DELETE',
|
||||
body: JSON.stringify(data),
|
||||
credentials: 'include',
|
||||
referrerPolicy: 'no-referrer-when-downgrade',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
'X-Request-Id': requestId,
|
||||
...headers,
|
||||
},
|
||||
headers,
|
||||
...otherOptions,
|
||||
})
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { handleError, handleResponse, handleResponseError } from './base'
|
||||
import { handleError, handleResponse, handleResponseError, constructHeaders } from './base'
|
||||
import { uuidv4 } from '../../helpers'
|
||||
import { SupaResponse } from 'types/base'
|
||||
|
||||
@@ -8,17 +8,13 @@ export async function get<T = any>(
|
||||
): Promise<SupaResponse<T>> {
|
||||
const requestId = uuidv4()
|
||||
try {
|
||||
const { headers, ...otherOptions } = options ?? {}
|
||||
const { headers: optionHeaders, ...otherOptions } = options ?? {}
|
||||
const headers = constructHeaders(requestId, optionHeaders)
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
credentials: 'include',
|
||||
referrerPolicy: 'no-referrer-when-downgrade',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
'X-Request-Id': requestId,
|
||||
...headers,
|
||||
},
|
||||
headers,
|
||||
...otherOptions,
|
||||
})
|
||||
if (!response.ok) return handleResponseError(response, requestId)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { handleError, handleResponse, handleResponseError } from './base'
|
||||
import { constructHeaders, handleError, handleResponse, handleResponseError } from './base'
|
||||
import { uuidv4 } from '../../helpers'
|
||||
import { SupaResponse } from 'types/base'
|
||||
|
||||
@@ -9,18 +9,14 @@ export async function patch<T = any>(
|
||||
): Promise<SupaResponse<T>> {
|
||||
const requestId = uuidv4()
|
||||
try {
|
||||
const { headers, ...otherOptions } = options ?? {}
|
||||
const { headers: optionHeaders, ...otherOptions } = options ?? {}
|
||||
const headers = constructHeaders(requestId, optionHeaders)
|
||||
const response = await fetch(url, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(data),
|
||||
credentials: 'include',
|
||||
referrerPolicy: 'no-referrer-when-downgrade',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
'X-Request-Id': requestId,
|
||||
...headers,
|
||||
},
|
||||
headers,
|
||||
...otherOptions,
|
||||
})
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { handleError, handleResponse, handleResponseError } from './base'
|
||||
import { handleError, handleResponse, handleResponseError, constructHeaders } from './base'
|
||||
import { uuidv4 } from 'lib/helpers'
|
||||
import { SupaResponse } from 'types/base'
|
||||
|
||||
@@ -9,18 +9,14 @@ export async function post<T = any>(
|
||||
): Promise<SupaResponse<T>> {
|
||||
const requestId = uuidv4()
|
||||
try {
|
||||
const { headers, ...otherOptions } = options ?? {}
|
||||
const { headers: optionHeaders, ...otherOptions } = options ?? {}
|
||||
const headers = constructHeaders(requestId, optionHeaders)
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
credentials: 'include',
|
||||
referrerPolicy: 'no-referrer-when-downgrade',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
'X-Request-Id': requestId,
|
||||
...headers,
|
||||
},
|
||||
headers,
|
||||
...otherOptions,
|
||||
})
|
||||
if (!response.ok) return handleResponseError(response, requestId)
|
||||
|
||||
@@ -1,811 +0,0 @@
|
||||
// @ts-nocheck
|
||||
import { IconArchive, IconDatabase, IconHeart, IconKey } from '@supabase/ui'
|
||||
import { concat, sortBy } from 'lodash'
|
||||
|
||||
// Ideally we'd split this into smaller, more scoped and easily readable files
|
||||
// And shift them under common/constants/xxx.ts
|
||||
|
||||
export const IS_PLATFORM = process.env.NEXT_PUBLIC_IS_PLATFORM === 'true'
|
||||
export const API_URL = IS_PLATFORM ? process.env.NEXT_PUBLIC_API_URL : '/api'
|
||||
export const PG_META_URL = IS_PLATFORM
|
||||
? process.env.PLATFORM_PG_META_URL
|
||||
: process.env.STUDIO_PG_META_URL
|
||||
|
||||
export const POSTGRES_SUPER_ADMIN = 'supabase_admin'
|
||||
export const POSTGRES_AUTH_ADMIN = 'supabase_auth_admin'
|
||||
export const POSTGRES_STORAGE_ADMIN = 'supabase_storage_admin'
|
||||
export const POSTGRES_POSTGREST = 'authenticator'
|
||||
export const POSTGRES_PGBOUNCER = 'pgbouncer'
|
||||
|
||||
export const DATE_FORMAT = 'YYYY-MM-DDTHH:mm:ssZ'
|
||||
|
||||
export const QUEUES = {
|
||||
// All infra tasks
|
||||
INFRA: {
|
||||
id: 'infra',
|
||||
tasks: {
|
||||
CREATE: 'create', // initialises a new server
|
||||
DELETE: 'delete', // destroys a server
|
||||
UPDATE: 'update', // updates the server's config
|
||||
REPLACE: 'replace', // replace the server
|
||||
},
|
||||
},
|
||||
DB: {
|
||||
id: 'database',
|
||||
tasks: {
|
||||
CREATE: 'create', // initialises a new server
|
||||
DELETE: 'delete', // destroys a server
|
||||
UPDATE: 'update', // updates the server's config
|
||||
RESTORE: 'restore', // restore a db with a given backup file
|
||||
},
|
||||
},
|
||||
DUMP: {
|
||||
id: 'dump',
|
||||
tasks: {
|
||||
BULK_PREPARE: 'bulk_prepare',
|
||||
DUMP: 'dump',
|
||||
PREPARE: 'prepare',
|
||||
},
|
||||
},
|
||||
RESTORE: {
|
||||
id: 'restore',
|
||||
tasks: {
|
||||
PREPARE: 'prepare',
|
||||
RESTORE: 'restore',
|
||||
},
|
||||
},
|
||||
EVENTS: {
|
||||
id: 'events',
|
||||
tasks: {
|
||||
organization_created: 'organization.created',
|
||||
organization_updated: 'organization.updated',
|
||||
organization_deleted: 'organization.deleted',
|
||||
project_created: 'project.created',
|
||||
project_updated: 'project.updated',
|
||||
project_deleted: 'project.deleted',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// alias regions remain as the starting point for project creation
|
||||
// they are immediately translated to their respective cloud regions
|
||||
// and are afterward never referred to
|
||||
export const REGIONS = {
|
||||
WEST_US: 'West US (North California)',
|
||||
EAST_US: 'East US (North Virginia)',
|
||||
CENTRAL_CANADA: 'Canada (Central)',
|
||||
WEST_EU: 'West EU (Ireland)',
|
||||
WEST_EU_2: 'West EU (London)',
|
||||
// 'North EU': 'North EU',
|
||||
CENTRAL_EU: 'Central EU (Frankfurt)',
|
||||
SOUTH_ASIA: 'South Asia (Mumbai)',
|
||||
SOUTHEAST_ASIA: 'Southeast Asia (Singapore)',
|
||||
NORTHEAST_ASIA: 'Northeast Asia (Tokyo)',
|
||||
NORTHEAST_ASIA_2: 'Northeast Asia (Seoul)',
|
||||
OCEANIA: 'Oceania (Sydney)',
|
||||
SOUTH_AMERICA: 'South America (São Paulo)',
|
||||
// SOUTH_AFRICA: 'South Africa (Cape Town)',
|
||||
}
|
||||
|
||||
export const REGIONS_DEFAULT = REGIONS.EAST_US
|
||||
|
||||
export const PROVIDERS = {
|
||||
// Digital Ocean
|
||||
DO: {
|
||||
id: 'DO',
|
||||
name: 'digitalocean',
|
||||
DEFAULT_SSH_KEY: '26347828',
|
||||
regions: {
|
||||
EAST_US: 'nyc1', // New York City, United States
|
||||
// 'North EU': 'ams3', //Amsterdam, the Netherlands
|
||||
WEST_US: 'sfo2', // San Francisco, United States
|
||||
SOUTHEAST_ASIA: 'sgp1', // Singapore
|
||||
WEST_US: 'lon1', // London
|
||||
CENTRAL_EU: 'fra1', // Frankfurt
|
||||
CENTRAL_CANADA: 'tor1', // Toronto
|
||||
SOUTH_ASIA: 'blr1', // Bangalore
|
||||
},
|
||||
sizes: {
|
||||
512: '512mb', // Don't use - https://developers.digitalocean.com/documentation/changelog/api-v2/new-size-slugs-for-droplet-plan-changes/
|
||||
's-1vcpu-1gb': {
|
||||
id: 's-1vcpu-1gb',
|
||||
description: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
AWS: {
|
||||
id: 'AWS',
|
||||
name: 'aws',
|
||||
DEFAULT_SSH_KEY: 'supabase-app-instance',
|
||||
regions: {
|
||||
EAST_US: 'us-east-1', // North Virginia
|
||||
// 'North EU': 'eu-north-1', // Stockholm
|
||||
WEST_US: 'us-west-1', // North California
|
||||
SOUTHEAST_ASIA: 'ap-southeast-1', // Singapore
|
||||
NORTHEAST_ASIA: 'ap-northeast-1', // Tokyo
|
||||
NORTHEAST_ASIA_2: 'ap-northeast-2', //Seoul
|
||||
OCEANIA: 'ap-southeast-2', // Sydney
|
||||
WEST_EU: 'eu-west-1', // Ireland
|
||||
WEST_EU_2: 'eu-west-2', // London
|
||||
CENTRAL_EU: 'eu-central-1', // Frankfurt
|
||||
CENTRAL_CANADA: 'ca-central-1', // Central Canada
|
||||
SOUTH_ASIA: 'ap-south-1', // Mumbai
|
||||
SOUTH_AMERICA: 'sa-east-1', // Sao Paulo
|
||||
// SOUTH_AFRICA: 'af-south-1', // Cape Town
|
||||
},
|
||||
sizes: {
|
||||
nano: {
|
||||
x86_64: 't3.nano',
|
||||
arm64: 't4g.nano',
|
||||
description: '512mb RAM',
|
||||
},
|
||||
micro: {
|
||||
x86_64: 't3.micro',
|
||||
arm64: 't4g.micro',
|
||||
description: '1gb RAM',
|
||||
},
|
||||
medium: {
|
||||
x86_64: 'm5a.large',
|
||||
arm64: 'm6g.medium',
|
||||
description: '4gb RAM',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// @todo ini update for prod
|
||||
export const AWS_SUPPORTED_AZ = {
|
||||
EAST_US: ['us-east-1a', 'us-east-1b', 'us-east-1c', 'us-east-1d', 'us-east-1e', 'us-east-1f'],
|
||||
SOUTHEAST_ASIA: ['ap-southeast-1a', 'ap-southeast-1b', 'ap-southeast-1c'],
|
||||
WEST_EU: ['eu-west-1a', 'eu-west-1b', 'eu-west-1c'],
|
||||
}
|
||||
|
||||
export const SERVICE_STATUS = {
|
||||
INACTIVE: 'INACTIVE',
|
||||
ACTIVE_HEALTHY: 'ACTIVE_HEALTHY',
|
||||
ACTIVE_UNHEALTHY: 'ACTIVE_UNHEALTHY',
|
||||
COMING_UP: 'COMING_UP',
|
||||
UNKNOWN: 'UNKNOWN',
|
||||
GOING_DOWN: 'GOING_DOWN',
|
||||
INIT_FAILED: 'INIT_FAILED',
|
||||
REMOVED: 'REMOVED',
|
||||
}
|
||||
|
||||
export const PROJECT_STATUS = {
|
||||
INACTIVE: 'INACTIVE',
|
||||
ACTIVE_HEALTHY: 'ACTIVE_HEALTHY',
|
||||
ACTIVE_UNHEALTHY: 'ACTIVE_UNHEALTHY',
|
||||
COMING_UP: 'COMING_UP',
|
||||
UNKNOWN: 'UNKNOWN',
|
||||
GOING_DOWN: 'GOING_DOWN',
|
||||
INIT_FAILED: 'INIT_FAILED',
|
||||
REMOVED: 'REMOVED',
|
||||
RESTORING: 'RESTORING',
|
||||
}
|
||||
|
||||
// Not used anywhere but referenced in a comment in interfaces.js (L12)
|
||||
export const APPS = {
|
||||
AUTO_API: {
|
||||
id: 'AUTO_API',
|
||||
name: 'Auto API',
|
||||
},
|
||||
}
|
||||
|
||||
export const SIDE_PANEL_KEYS = {
|
||||
ROW: 'ROW',
|
||||
TABLE: 'TABLE',
|
||||
COLUMN: 'COLUMN',
|
||||
}
|
||||
|
||||
export const FILTER_CONDITIONS = {
|
||||
EQ: {
|
||||
name: 'EQ',
|
||||
tooltip: 'Equals to',
|
||||
},
|
||||
NEQ: {
|
||||
name: 'NEQ',
|
||||
tooltip: 'Not equals to',
|
||||
},
|
||||
GT: {
|
||||
name: 'GT',
|
||||
tooltip: 'Greater than',
|
||||
},
|
||||
LT: {
|
||||
name: 'LT',
|
||||
tooltip: 'Less than',
|
||||
},
|
||||
GTE: {
|
||||
name: 'GTE',
|
||||
tooltip: 'Greater than or equals to',
|
||||
},
|
||||
LTE: {
|
||||
name: 'LTE',
|
||||
tooltip: 'Less than or equals to',
|
||||
},
|
||||
LIKE: {
|
||||
name: 'LIKE',
|
||||
tooltip: 'Pattern matching with %',
|
||||
},
|
||||
ILIKE: {
|
||||
name: 'ILIKE',
|
||||
tooltip: 'Case sensitive matching',
|
||||
},
|
||||
IS: {
|
||||
name: 'IS',
|
||||
tooltip: 'Checks for exact quality',
|
||||
},
|
||||
IN: {
|
||||
name: 'IN',
|
||||
tooltip: 'Checks if value in list',
|
||||
},
|
||||
}
|
||||
|
||||
// Data types primarily for mapping icon in editor
|
||||
|
||||
export const NUMERICAL_TYPES = ['int2', 'int4', 'int8', 'float4', 'float8']
|
||||
export const JSON_TYPES = ['json', 'jsonb']
|
||||
export const TEXT_TYPES = ['text', 'varchar']
|
||||
export const TIMESTAMP_TYPES = ['date', 'time', 'timestamp', 'timetz', 'timestamptz']
|
||||
export const OTHER_DATA_TYPES = ['uuid', 'bool']
|
||||
export const POSTGRES_DATA_TYPES = sortBy(
|
||||
concat(NUMERICAL_TYPES, JSON_TYPES, TEXT_TYPES, TIMESTAMP_TYPES, OTHER_DATA_TYPES)
|
||||
)
|
||||
|
||||
// Keyboard Shortcuts Related
|
||||
export const SHORTCUT_KEYS = {
|
||||
VIEW_ALL_SHORTCUTS: 'VIEW_ALL_SHORTCUTS',
|
||||
TOGGLE_FILTER_MENU: 'TOGGLE_FILTER_MENU',
|
||||
TOGGLE_SORT_MENU: 'TOGGLE_SORT_MENU',
|
||||
CLOSE_SIDE_PANEL: 'CLOSE_SIDE_PANEL',
|
||||
EDIT_CELL: 'EDIT_CELL',
|
||||
COPY_CELL: 'COPY_CELL',
|
||||
PASTE_CELL: 'PASTE_CELL',
|
||||
CREATE_ROW: 'CREATE_ROW',
|
||||
EDIT_ROW: 'EDIT_ROW',
|
||||
DELETE_ROW: 'DELETE_ROW',
|
||||
SCROLL_EDGES: 'SCROLL_EDGES',
|
||||
}
|
||||
|
||||
export const GENERAL_SHORTCUTS = [
|
||||
{
|
||||
key: SHORTCUT_KEYS.VIEW_ALL_SHORTCUTS,
|
||||
name: 'View all shortcuts',
|
||||
keys: ['Shift+/'],
|
||||
},
|
||||
{
|
||||
key: SHORTCUT_KEYS.CLOSE_SIDE_PANEL,
|
||||
name: 'Close side panel',
|
||||
keys: ['Esc'],
|
||||
},
|
||||
]
|
||||
|
||||
export const generateEditorShortcuts = (clientOS) => {
|
||||
let metaKey = '⌘'
|
||||
if (clientOS === 'windows') metaKey = 'Ctrl'
|
||||
return [
|
||||
{
|
||||
key: SHORTCUT_KEYS.EDIT_CELL,
|
||||
name: 'Edit current cell',
|
||||
keys: ['Enter'],
|
||||
},
|
||||
{
|
||||
key: SHORTCUT_KEYS.COPY_CELL,
|
||||
name: 'Copy value of current cell',
|
||||
keys: [`${metaKey}+C`],
|
||||
},
|
||||
{
|
||||
key: SHORTCUT_KEYS.PASTE_CELL,
|
||||
name: 'Paste value into current cell',
|
||||
keys: [`${metaKey}+V`],
|
||||
},
|
||||
{
|
||||
key: SHORTCUT_KEYS.CREATE_ROW,
|
||||
name: 'Create a new row',
|
||||
keys: ['Shift+Enter'],
|
||||
},
|
||||
{
|
||||
key: SHORTCUT_KEYS.SCROLL_EDGES,
|
||||
name: 'Scroll to edges of the table',
|
||||
keys: [`${metaKey}+↑`, `${metaKey}+→`, `${metaKey}+↓`, `${metaKey}+←`],
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
export const EDITOR_STATUS = {
|
||||
SAVING: 'SAVING',
|
||||
READY: 'READY',
|
||||
}
|
||||
|
||||
export const POLICY_MODAL_VIEWS = {
|
||||
SELECTION: 'SELECTION',
|
||||
TEMPLATES: 'TEMPLATES',
|
||||
EDITOR: 'EDITOR',
|
||||
REVIEW: 'REVIEW',
|
||||
}
|
||||
|
||||
export const STRIPE_PRODUCT_IDS = {
|
||||
PAYG: process?.env?.NEXT_PUBLIC_STRIPE_PAYG_TIER_ID,
|
||||
PRO: process?.env?.NEXT_PUBLIC_STRIPE_PRO_TIER_ID,
|
||||
FREE: process?.env?.NEXT_PUBLIC_STRIPE_FREE_TIER_ID,
|
||||
}
|
||||
|
||||
export const TIER_NAMES = {
|
||||
PAYG: 'Pay as you go',
|
||||
PRO: 'Pro',
|
||||
FREE: 'Free',
|
||||
}
|
||||
|
||||
export const DEFAULT_PRODUCTS = [
|
||||
{
|
||||
// Free tier
|
||||
price: process?.env?.NEXT_PUBLIC_STRIPE_PRODUCT_FREE_TIER_PRICE_FREE,
|
||||
},
|
||||
]
|
||||
|
||||
export const PGBOUNCER_CONFIG = {
|
||||
pool_mode: 'transaction',
|
||||
default_pool_size: 15,
|
||||
ignore_startup_parameters: 'extra_float_digits',
|
||||
}
|
||||
|
||||
export const DEFAULT_ORG_PROJECTS_LIMIT = 3
|
||||
export const DEFAULT_MINIMUM_PASSWORD_STRENGTH = 4
|
||||
|
||||
const METRIC_CATEGORIES_ICON_SIZE = 16
|
||||
|
||||
export const METRIC_CATEGORIES = {
|
||||
// API: {
|
||||
// label: 'All API usage',
|
||||
// icon: <IconActivity />,
|
||||
// key: 'api',
|
||||
// },
|
||||
API_DATABASE: {
|
||||
label: 'Database API',
|
||||
icon: <IconDatabase size={ METRIC_CATEGORIES_ICON_SIZE } />,
|
||||
key: 'api_database',
|
||||
},
|
||||
API_AUTH: {
|
||||
label: 'Auth API',
|
||||
icon: <IconKey size={ METRIC_CATEGORIES_ICON_SIZE } />,
|
||||
key: 'api_auth',
|
||||
},
|
||||
API_STORAGE: {
|
||||
label: 'Storage API',
|
||||
icon: <IconArchive size={ METRIC_CATEGORIES_ICON_SIZE } />,
|
||||
key: 'api_storage',
|
||||
},
|
||||
// API_REALTIME: {
|
||||
// label: 'Realtime API',
|
||||
// icon: '',
|
||||
// key: 'api_realtime',
|
||||
// },
|
||||
INSTANCE: {
|
||||
label: 'Instance health',
|
||||
icon: <IconHeart size={ METRIC_CATEGORIES_ICON_SIZE } />,
|
||||
key: 'instance',
|
||||
},
|
||||
// POSTGRES: {
|
||||
// label: 'Postgres usage',
|
||||
// icon: '',
|
||||
// key: 'postgres',
|
||||
// },
|
||||
}
|
||||
|
||||
export const METRICS = [
|
||||
{
|
||||
key: 'cpu_usage',
|
||||
label: 'CPU % usage',
|
||||
provider: 'infra-monitoring',
|
||||
category: METRIC_CATEGORIES.INSTANCE,
|
||||
},
|
||||
{
|
||||
key: 'ram_usage',
|
||||
label: 'Memory % usage',
|
||||
provider: 'infra-monitoring',
|
||||
category: METRIC_CATEGORIES.INSTANCE,
|
||||
},
|
||||
{
|
||||
key: 'total_realtime_egress',
|
||||
label: 'Realtime Connection Egress',
|
||||
provider: 'daily-stats',
|
||||
category: METRIC_CATEGORIES.API,
|
||||
},
|
||||
{
|
||||
key: 'total_realtime_get_requests',
|
||||
label: 'Realtime Connection Requests',
|
||||
provider: 'daily-stats',
|
||||
category: METRIC_CATEGORIES.API_REALTIME,
|
||||
},
|
||||
{
|
||||
key: 'total_realtime_ingress',
|
||||
label: 'Realtime Connection Ingress',
|
||||
provider: 'daily-stats',
|
||||
category: METRIC_CATEGORIES.API_REALTIME,
|
||||
},
|
||||
// {
|
||||
// key: 'total_realtime_post_requests',
|
||||
// label: 'total_realtime_post_requests',
|
||||
// provider: 'daily-stats',
|
||||
// category: METRIC_CATEGORIES.API_REALTIME,
|
||||
// },
|
||||
{
|
||||
key: 'total_realtime_requests',
|
||||
label: 'Connection Requests',
|
||||
provider: 'daily-stats',
|
||||
category: METRIC_CATEGORIES.API_REALTIME,
|
||||
},
|
||||
|
||||
/**
|
||||
* API
|
||||
*/
|
||||
{
|
||||
key: 'total_rest_ingress',
|
||||
label: 'API Ingress',
|
||||
provider: 'daily-stats',
|
||||
category: METRIC_CATEGORIES.API_DATABASE,
|
||||
},
|
||||
{
|
||||
key: 'total_rest_egress',
|
||||
label: 'API Egress',
|
||||
provider: 'daily-stats',
|
||||
category: METRIC_CATEGORIES.API_DATABASE,
|
||||
},
|
||||
{
|
||||
key: 'total_rest_requests',
|
||||
label: 'API Requests',
|
||||
provider: 'daily-stats',
|
||||
category: METRIC_CATEGORIES.API_DATABASE,
|
||||
},
|
||||
{
|
||||
key: 'total_rest_get_requests',
|
||||
label: 'API GET Requests',
|
||||
provider: 'daily-stats',
|
||||
category: METRIC_CATEGORIES.API_DATABASE,
|
||||
},
|
||||
{
|
||||
key: 'total_rest_post_requests',
|
||||
label: 'API POST Requests',
|
||||
provider: 'daily-stats',
|
||||
category: METRIC_CATEGORIES.API_DATABASE,
|
||||
},
|
||||
{
|
||||
key: 'total_rest_patch_requests',
|
||||
label: 'API PATCH Requests',
|
||||
provider: 'daily-stats',
|
||||
category: METRIC_CATEGORIES.API_DATABASE,
|
||||
},
|
||||
{
|
||||
key: 'total_rest_delete_requests',
|
||||
label: 'API DELETE Requests',
|
||||
provider: 'daily-stats',
|
||||
category: METRIC_CATEGORIES.API_DATABASE,
|
||||
},
|
||||
{
|
||||
key: 'total_rest_options_requests',
|
||||
label: 'API OPTIONS Requests',
|
||||
provider: 'daily-stats',
|
||||
category: METRIC_CATEGORIES.API_DATABASE,
|
||||
},
|
||||
|
||||
/**
|
||||
* Auth
|
||||
*/
|
||||
|
||||
// {
|
||||
// key: 'total_auth_users',
|
||||
// label: 'Total Auth Users',
|
||||
// provider: 'daily-stats',
|
||||
// category: METRIC_CATEGORIES.API_AUTH,
|
||||
// },
|
||||
{
|
||||
key: 'total_auth_ingress',
|
||||
label: 'Auth Ingress',
|
||||
provider: 'daily-stats',
|
||||
category: METRIC_CATEGORIES.API_AUTH,
|
||||
},
|
||||
{
|
||||
key: 'total_auth_egress',
|
||||
label: 'Auth Egress',
|
||||
provider: 'daily-stats',
|
||||
category: METRIC_CATEGORIES.API_AUTH,
|
||||
},
|
||||
{
|
||||
key: 'total_auth_requests',
|
||||
label: 'Auth Requests',
|
||||
provider: 'daily-stats',
|
||||
category: METRIC_CATEGORIES.API_AUTH,
|
||||
},
|
||||
{
|
||||
key: 'total_auth_get_requests',
|
||||
label: 'Auth GET Requests',
|
||||
provider: 'daily-stats',
|
||||
category: METRIC_CATEGORIES.API_AUTH,
|
||||
},
|
||||
{
|
||||
key: 'total_auth_post_requests',
|
||||
label: 'Auth POST Requests',
|
||||
provider: 'daily-stats',
|
||||
category: METRIC_CATEGORIES.API_AUTH,
|
||||
},
|
||||
{
|
||||
key: 'total_auth_patch_requests',
|
||||
label: 'Auth PATCH requests',
|
||||
provider: 'daily-stats',
|
||||
category: METRIC_CATEGORIES.API_AUTH,
|
||||
},
|
||||
{
|
||||
key: 'total_auth_options_requests',
|
||||
label: 'Auth OPTIONS requests',
|
||||
provider: 'daily-stats',
|
||||
category: METRIC_CATEGORIES.API_AUTH,
|
||||
},
|
||||
|
||||
/**
|
||||
* Storage
|
||||
*/
|
||||
{
|
||||
key: 'total_storage_ingress',
|
||||
label: 'Storage Ingress',
|
||||
provider: 'daily-stats',
|
||||
category: METRIC_CATEGORIES.API_STORAGE,
|
||||
},
|
||||
{
|
||||
key: 'total_storage_egress',
|
||||
label: 'Storage Egress',
|
||||
provider: 'daily-stats',
|
||||
category: METRIC_CATEGORIES.API_STORAGE,
|
||||
},
|
||||
{
|
||||
key: 'total_storage_requests',
|
||||
label: 'Storage Requests',
|
||||
provider: 'daily-stats',
|
||||
category: METRIC_CATEGORIES.API_STORAGE,
|
||||
},
|
||||
{
|
||||
key: 'total_storage_get_requests',
|
||||
label: 'Storage GET Requests',
|
||||
provider: 'daily-stats',
|
||||
category: METRIC_CATEGORIES.API_STORAGE,
|
||||
},
|
||||
{
|
||||
key: 'total_storage_post_requests',
|
||||
label: 'Storage POST Requests',
|
||||
provider: 'daily-stats',
|
||||
category: METRIC_CATEGORIES.API_STORAGE,
|
||||
},
|
||||
{
|
||||
key: 'total_storage_delete_requests',
|
||||
label: 'Storage DELETE Requests',
|
||||
provider: 'daily-stats',
|
||||
category: METRIC_CATEGORIES.API_STORAGE,
|
||||
},
|
||||
{
|
||||
key: 'total_storage_options_requests',
|
||||
label: 'Storage OPTIONS Requests',
|
||||
provider: 'daily-stats',
|
||||
category: METRIC_CATEGORIES.API_STORAGE,
|
||||
},
|
||||
{
|
||||
key: 'total_auth_delete_requests',
|
||||
label: 'Auth DELETE Requests',
|
||||
provider: 'daily-stats',
|
||||
category: METRIC_CATEGORIES.API_AUTH,
|
||||
},
|
||||
|
||||
{
|
||||
key: 'total_egress',
|
||||
label: 'All Egress',
|
||||
provider: 'daily-stats',
|
||||
category: METRIC_CATEGORIES.API,
|
||||
},
|
||||
// {
|
||||
// key: 'total_auth_emails',
|
||||
// name: 'total_auth_emails',
|
||||
// provider: 'daily-stats',
|
||||
// category: METRIC_CATEGORIES.API,
|
||||
// },
|
||||
// {
|
||||
// key: 'total_auth_texts',
|
||||
// name: 'total_auth_texts',
|
||||
// provider: 'daily-stats',
|
||||
// category: METRIC_CATEGORIES.API,
|
||||
// },
|
||||
|
||||
{
|
||||
key: 'total_get_requests',
|
||||
label: 'All GET Requests',
|
||||
provider: 'daily-stats',
|
||||
category: METRIC_CATEGORIES.API,
|
||||
},
|
||||
// {
|
||||
// key: 'total_db_size_bytes',
|
||||
// name: 'total_db_size_bytes',
|
||||
// provider: 'daily-stats',
|
||||
// category: METRIC_CATEGORIES.API,
|
||||
// },
|
||||
// {
|
||||
// key: 'total_storage_size_bytes',
|
||||
// name: 'total_storage_size_bytes',
|
||||
// provider: 'daily-stats',
|
||||
// category: METRIC_CATEGORIES.API,
|
||||
// },
|
||||
// {
|
||||
// key: 'total_realtime_delete_requests',
|
||||
// label: 'total_realtime_delete_requests',
|
||||
// provider: 'daily-stats',
|
||||
// category: METRIC_CATEGORIES.API_REALTIME,
|
||||
// },
|
||||
{
|
||||
key: 'total_storage_patch_requests',
|
||||
label: 'Storage PATCH Requests',
|
||||
provider: 'daily-stats',
|
||||
category: METRIC_CATEGORIES.API_STORAGE,
|
||||
},
|
||||
{
|
||||
key: 'total_requests',
|
||||
label: 'All Requests',
|
||||
provider: 'daily-stats',
|
||||
category: METRIC_CATEGORIES.API,
|
||||
},
|
||||
// {
|
||||
// key: 'total_realtime_options_requests',
|
||||
// label: 'total_realtime_options_requests',
|
||||
// provider: 'daily-stats',
|
||||
// category: METRIC_CATEGORIES.API_REALTIME,
|
||||
// },
|
||||
|
||||
{
|
||||
key: 'total_patch_requests',
|
||||
label: 'All PATCH Requests',
|
||||
provider: 'daily-stats',
|
||||
category: METRIC_CATEGORIES.API,
|
||||
},
|
||||
{
|
||||
key: 'total_post_requests',
|
||||
label: 'All POST Requests',
|
||||
provider: 'daily-stats',
|
||||
category: METRIC_CATEGORIES.API,
|
||||
},
|
||||
|
||||
{
|
||||
key: 'total_ingress',
|
||||
label: 'All Ingress',
|
||||
provider: 'daily-stats',
|
||||
category: METRIC_CATEGORIES.API,
|
||||
},
|
||||
// {
|
||||
// key: 'total_realtime_patch_requests',
|
||||
// label: 'total_realtime_patch_requests',
|
||||
// provider: 'daily-stats',
|
||||
// category: METRIC_CATEGORIES.API_REALTIME,
|
||||
// },
|
||||
{
|
||||
key: 'total_delete_requests',
|
||||
label: 'All DELETE Requests',
|
||||
provider: 'daily-stats',
|
||||
category: METRIC_CATEGORIES.API,
|
||||
},
|
||||
{
|
||||
key: 'total_options_requests',
|
||||
label: 'All OPTIONS Requests',
|
||||
provider: 'daily-stats',
|
||||
category: METRIC_CATEGORIES.API,
|
||||
},
|
||||
// {
|
||||
// key: 'total_db_egress_bytes',
|
||||
// name: 'total_db_egress_bytes',
|
||||
// provider: 'daily-stats',
|
||||
// category: METRIC_CATEGORIES.API,
|
||||
// },
|
||||
]
|
||||
|
||||
export const TIME_PERIODS_BILLING = [
|
||||
{
|
||||
key: 'currentBillingCycle',
|
||||
label: 'Current billing cycle',
|
||||
interval: '1d',
|
||||
},
|
||||
{
|
||||
key: 'previousBillingCycle',
|
||||
label: 'Previous billing cycle',
|
||||
interval: '1d',
|
||||
},
|
||||
]
|
||||
|
||||
export const TIME_PERIODS_REPORTS = [
|
||||
{
|
||||
key: '7d',
|
||||
label: 'Last 7 days',
|
||||
interval: '1d',
|
||||
},
|
||||
{
|
||||
key: '30d',
|
||||
label: 'Last 30 days',
|
||||
interval: '1d',
|
||||
},
|
||||
// {
|
||||
// key: '60d',
|
||||
// label: 'Last 60 days',
|
||||
// interval: '1d',
|
||||
// },
|
||||
// {
|
||||
// key: '120d',
|
||||
// label: 'Last 120 days',
|
||||
// interval: '1d',
|
||||
// },
|
||||
{
|
||||
key: 'startMonth',
|
||||
label: 'This month',
|
||||
interval: '1d',
|
||||
},
|
||||
]
|
||||
|
||||
export const TIME_PERIODS_INFRA = [
|
||||
{
|
||||
key: '1h',
|
||||
label: 'Last hour',
|
||||
interval: '1m',
|
||||
},
|
||||
{
|
||||
key: '3h',
|
||||
label: 'Last 3 hours',
|
||||
interval: '5m',
|
||||
},
|
||||
{
|
||||
key: '1d',
|
||||
label: 'Last 24 hours',
|
||||
interval: '30m',
|
||||
},
|
||||
]
|
||||
|
||||
export const NEW_REPORT_SKELETON = {
|
||||
name: 'new report',
|
||||
description: '',
|
||||
type: 'report',
|
||||
visibility: 'project',
|
||||
content: {
|
||||
schema_version: 1,
|
||||
period_start: {
|
||||
time_period: '7d',
|
||||
date: '',
|
||||
},
|
||||
period_end: {
|
||||
time_period: 'today',
|
||||
date: '',
|
||||
},
|
||||
interval: '1d',
|
||||
layout: [],
|
||||
},
|
||||
}
|
||||
|
||||
export const PASSWORD_STRENGTH = {
|
||||
0: 'This password is not acceptable.',
|
||||
1: 'This password is not secure enough.',
|
||||
2: 'This password is not secure enough.',
|
||||
3: 'Not bad, but your password must be harder to guess.',
|
||||
4: 'This password is strong.',
|
||||
}
|
||||
|
||||
export const PASSWORD_STRENGTH_COLOR = {
|
||||
0: 'bg-red-500',
|
||||
1: 'bg-red-400',
|
||||
2: 'bg-yellow-500',
|
||||
3: 'bg-yellow-500',
|
||||
4: 'bg-green-500',
|
||||
}
|
||||
|
||||
export const PASSWORD_STRENGTH_PERCENTAGE = {
|
||||
0: '10%',
|
||||
1: '30%',
|
||||
2: '50%',
|
||||
3: '80%',
|
||||
4: '100%',
|
||||
}
|
||||
|
||||
export const LOG_TYPE_LABEL_MAPPING: { [k: string]: string } = {
|
||||
rest: 'Edge - PostgREST',
|
||||
realtime: 'Edge - Realtime',
|
||||
auth: 'Edge - Auth',
|
||||
storage: 'Edge - Storage',
|
||||
database: 'Database',
|
||||
}
|
||||
@@ -87,3 +87,15 @@ export const POLICY_MODAL_VIEWS = {
|
||||
EDITOR: 'EDITOR',
|
||||
REVIEW: 'REVIEW',
|
||||
}
|
||||
|
||||
export const LOG_TYPE_LABEL_MAPPING: { [k: string]: string } = {
|
||||
rest: 'API',
|
||||
realtime: 'Realtime',
|
||||
auth: 'Auth',
|
||||
storage: 'Storage',
|
||||
database: 'Database',
|
||||
}
|
||||
|
||||
export const GOTRUE_ERRORS = {
|
||||
UNVERIFIED_GITHUB_USER: 'Error sending confirmation mail',
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { GoTrueClient, User } from '@supabase/gotrue-js'
|
||||
|
||||
export const GOTRUE_ENABLED = (process.env.NEXT_PUBLIC_GOTRUE_ENABLED === 'true') || false
|
||||
export const GOTRUE_URL = process.env.NEXT_PUBLIC_GOTRUE_URL || 'http://localhost:8000/auth/v1'
|
||||
|
||||
export const auth = new GoTrueClient({
|
||||
url: GOTRUE_URL,
|
||||
autoRefreshToken: true,
|
||||
})
|
||||
|
||||
export const getAuthUser = async (token: String): Promise<any> => {
|
||||
try {
|
||||
const { data: user, error } = await auth.api.getUser(token.replace('Bearer ', ''))
|
||||
if (error) throw error
|
||||
|
||||
return { user, error: null }
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
return { user: null, error: err }
|
||||
}
|
||||
}
|
||||
|
||||
export const getAuth0Id = (provider: String, providerId: String): String => {
|
||||
return `${provider}|${providerId}`
|
||||
}
|
||||
|
||||
export const getIdentity = (gotrueUser: User) => {
|
||||
try {
|
||||
if (gotrueUser !== undefined && gotrueUser.identities !== undefined) {
|
||||
return { identity: gotrueUser.identities[0], error: null }
|
||||
}
|
||||
throw 'Missing identity'
|
||||
} catch (err) {
|
||||
return { identity: null, error: err }
|
||||
}
|
||||
}
|
||||
@@ -45,8 +45,8 @@ export const timeout = (ms: number) => {
|
||||
|
||||
export const getURL = () => {
|
||||
const url =
|
||||
process?.env?.SITE_URL && process.env.SITE_URL !== ''
|
||||
? process.env.SITE_URL
|
||||
process?.env?.NEXT_PUBLIC_SITE_URL && process.env.NEXT_PUBLIC_SITE_URL !== ''
|
||||
? process.env.NEXT_PUBLIC_SITE_URL
|
||||
: process?.env?.VERCEL_URL && process.env.VERCEL_URL !== ''
|
||||
? process.env.VERCEL_URL
|
||||
: 'https://app.supabase.io'
|
||||
|
||||
@@ -7,6 +7,7 @@ import QueryTab from './QueryTab'
|
||||
import Favorite from './Favorite'
|
||||
// import { useStore } from 'hooks'
|
||||
import { SchemasQuery, TableColumnsQuery, AllFunctionsQuery } from './queries'
|
||||
import { isUndefined } from 'lodash'
|
||||
|
||||
let store = null
|
||||
let projectRef = null
|
||||
@@ -36,6 +37,7 @@ export const useSqlStore = () => {
|
||||
class SqlEditorStore {
|
||||
meta
|
||||
projectRef
|
||||
localStorageKey
|
||||
tabs = []
|
||||
favorites = []
|
||||
selectedTabId
|
||||
@@ -55,6 +57,7 @@ class SqlEditorStore {
|
||||
|
||||
this.meta = meta
|
||||
this.projectRef = projectRef
|
||||
this.localStorageKey = `supabase-sql-${projectRef}`
|
||||
|
||||
this.loadKeywords()
|
||||
this.loadSchemas()
|
||||
@@ -165,6 +168,8 @@ class SqlEditorStore {
|
||||
}
|
||||
|
||||
this.loadFavorites(json.favorites || [])
|
||||
|
||||
this.loadPreviousSession()
|
||||
}
|
||||
|
||||
loadInitialData() {
|
||||
@@ -215,6 +220,16 @@ class SqlEditorStore {
|
||||
this.favorites = values.map((x) => new Favorite(x.key, x.query, x.name, x.desc))
|
||||
}
|
||||
|
||||
loadPreviousSession() {
|
||||
const tabId = localStorage.getItem(this.localStorageKey)
|
||||
const allSnippets = this.tabs.concat(this.favorites)
|
||||
const selectedTab = allSnippets.find((tab) => tab.id === tabId)
|
||||
|
||||
if (!isUndefined(selectedTab)) {
|
||||
this.selectTab(selectedTab.id)
|
||||
}
|
||||
}
|
||||
|
||||
async loadKeywords() {
|
||||
const query = 'select * from pg_get_keywords();'
|
||||
const response = await this.meta.query(query)
|
||||
@@ -251,6 +266,9 @@ class SqlEditorStore {
|
||||
}
|
||||
|
||||
selectTab(id) {
|
||||
// Save selected tab in local storage
|
||||
localStorage.setItem(this.localStorageKey, id)
|
||||
|
||||
// reset new selected query tab
|
||||
const found = this.tabs.find((x) => x.id === this.selectedTabId)
|
||||
if (found && found.type == TAB_TYPES.SQL_QUERY) {
|
||||
|
||||
@@ -6,11 +6,13 @@ import {
|
||||
compact,
|
||||
isEqual,
|
||||
isNull,
|
||||
has,
|
||||
isNil,
|
||||
isUndefined,
|
||||
has,
|
||||
some,
|
||||
chunk,
|
||||
get,
|
||||
uniq,
|
||||
} from 'lodash'
|
||||
import toast from 'react-hot-toast'
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
@@ -63,6 +65,8 @@ class StorageExplorerStore {
|
||||
selectedItemsToMove = []
|
||||
selectedFilePreview = {}
|
||||
|
||||
DEFAULT_OPTIONS = { limit: LIMIT, offset: OFFSET, sortBy: { column: this.sortBy, order: 'asc' } }
|
||||
|
||||
/* Supabase client */
|
||||
supabaseClient = null
|
||||
|
||||
@@ -103,7 +107,16 @@ class StorageExplorerStore {
|
||||
initializeSupabaseClient = (serviceKey, serviceEndpoint) => {
|
||||
this.supabaseClient = createClient(
|
||||
this.normalizeSupabaseURL(`https://${serviceEndpoint}`),
|
||||
serviceKey
|
||||
serviceKey,
|
||||
{
|
||||
localStorage: {
|
||||
getItem: (key) => {
|
||||
return undefined
|
||||
},
|
||||
setItem: (key, value) => {},
|
||||
removeItem: (key) => {},
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -281,6 +294,8 @@ class StorageExplorerStore {
|
||||
return this.removeTempRows(columnIndex)
|
||||
}
|
||||
|
||||
this.updateFolderAfterEdit(formattedName, columnIndex)
|
||||
|
||||
const emptyPlaceholderFile = `${formattedName}/${EMPTY_FOLDER_PLACEHOLDER_FILE_NAME}`
|
||||
const pathToFolder = this.openedFolders
|
||||
.slice(0, columnIndex)
|
||||
@@ -292,13 +307,12 @@ class StorageExplorerStore {
|
||||
await this.supabaseClient.storage
|
||||
.from(this.selectedBucket.name)
|
||||
.upload(formattedPathToEmptyPlaceholderFile, new File([], EMPTY_FOLDER_PLACEHOLDER_FILE_NAME))
|
||||
|
||||
if (pathToFolder.length > 0) {
|
||||
await this.supabaseClient.storage
|
||||
.from(this.selectedBucket.name)
|
||||
.remove([`${pathToFolder}/${EMPTY_FOLDER_PLACEHOLDER_FILE_NAME}`])
|
||||
}
|
||||
|
||||
this.updateFolderAfterEdit(formattedName, columnIndex)
|
||||
}
|
||||
|
||||
setFilePreview = async (file) => {
|
||||
@@ -376,11 +390,15 @@ class StorageExplorerStore {
|
||||
/* Bucket CRUD */
|
||||
|
||||
createBucket = async (bucketName, isPublic = false) => {
|
||||
if (isNil(this.supabaseClient)) {
|
||||
return toast.error('Failed to initialize supabase client, try refreshing your browser.')
|
||||
}
|
||||
|
||||
const { error } = await this.supabaseClient.storage.createBucket(bucketName, {
|
||||
public: isPublic,
|
||||
})
|
||||
if (error) {
|
||||
toast(error.message)
|
||||
toast.error(error.message)
|
||||
return this.closeCreateBucketModal()
|
||||
}
|
||||
|
||||
@@ -455,6 +473,7 @@ class StorageExplorerStore {
|
||||
return await new Promise((resolve, reject) => fileEntry.file(resolve, reject))
|
||||
} catch (err) {
|
||||
console.error('getFile error:', err)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
@@ -470,8 +489,10 @@ class StorageExplorerStore {
|
||||
const entry = queue.shift() || {}
|
||||
if (entry.isFile) {
|
||||
const file = await this.getFile(entry)
|
||||
file.path = entry.fullPath.slice(1)
|
||||
files.push(file)
|
||||
if (!isUndefined(file)) {
|
||||
file.path = entry.fullPath.slice(1)
|
||||
files.push(file)
|
||||
}
|
||||
} else if (entry.isDirectory) {
|
||||
queue.push(...(await this.readAllDirectoryEntries(entry.createReader())))
|
||||
}
|
||||
@@ -747,7 +768,7 @@ class StorageExplorerStore {
|
||||
return null
|
||||
}
|
||||
|
||||
deleteFiles = async (files, showToast = true) => {
|
||||
deleteFiles = async (files, isDeleteFolder = false) => {
|
||||
this.closeFilePreview()
|
||||
|
||||
// If every file has the 'prefix' property, then just construct the prefix
|
||||
@@ -781,12 +802,22 @@ class StorageExplorerStore {
|
||||
)
|
||||
this.filePreviewCache = updatedFilePreviewCache
|
||||
|
||||
if (showToast) {
|
||||
toast.success(`Successfully deleted ${prefixes.length} file(s)`)
|
||||
}
|
||||
if (!isDeleteFolder) {
|
||||
// If parent folders are empty, reinstate .emptyFolderPlaceholder to persist them
|
||||
const parentFolderPrefixes = uniq(
|
||||
prefixes.map((prefix) => {
|
||||
const segments = prefix.split('/')
|
||||
return segments.slice(0, segments.length - 1).join('/')
|
||||
})
|
||||
)
|
||||
await Promise.all(
|
||||
parentFolderPrefixes.map((prefix) => this.validateParentFolderEmpty(prefix))
|
||||
)
|
||||
|
||||
await this.refetchAllOpenedFolders()
|
||||
this.clearSelectedItemsToDelete()
|
||||
toast.success(`Successfully deleted ${prefixes.length} file(s)`)
|
||||
await this.refetchAllOpenedFolders()
|
||||
this.clearSelectedItemsToDelete()
|
||||
}
|
||||
}
|
||||
|
||||
downloadSelectedFiles = async () => {
|
||||
@@ -976,11 +1007,32 @@ class StorageExplorerStore {
|
||||
this.openedFolders = updatedOpenedFolders
|
||||
}
|
||||
|
||||
deleteFolder = async (folder) => {
|
||||
const files = await this.getAllItemsAlongFolder(folder)
|
||||
const showFilesDeleteToast = false
|
||||
// Check parent folder if its empty, if yes, reinstate .emptyFolderPlaceholder
|
||||
// Used when deleting folder or deleting files
|
||||
validateParentFolderEmpty = async (parentFolderPrefix) => {
|
||||
const { data: items, error } = await this.supabaseClient.storage
|
||||
.from(this.selectedBucket.name)
|
||||
.list(parentFolderPrefix, this.DEFAULT_OPTIONS)
|
||||
if (!error && items.length === 0) {
|
||||
const prefixToPlaceholder = `${parentFolderPrefix}/${EMPTY_FOLDER_PLACEHOLDER_FILE_NAME}`
|
||||
await this.supabaseClient.storage
|
||||
.from(this.selectedBucket.name)
|
||||
.upload(prefixToPlaceholder, new File([], EMPTY_FOLDER_PLACEHOLDER_FILE_NAME))
|
||||
}
|
||||
}
|
||||
|
||||
await this.deleteFiles(files, showFilesDeleteToast)
|
||||
deleteFolder = async (folder) => {
|
||||
const isDeleteFolder = true
|
||||
const files = await this.getAllItemsAlongFolder(folder)
|
||||
await this.deleteFiles(files, isDeleteFolder)
|
||||
|
||||
const parentFolderPrefix = this.openedFolders.map((folder) => folder.name).join('/')
|
||||
if (parentFolderPrefix.length > 0) {
|
||||
await this.validateParentFolderEmpty(parentFolderPrefix)
|
||||
}
|
||||
|
||||
await this.refetchAllOpenedFolders()
|
||||
this.clearSelectedItemsToDelete()
|
||||
toast.success(`Successfully deleted ${folder.name}`)
|
||||
}
|
||||
|
||||
|
||||
Generated
+14174
-153
File diff suppressed because it is too large.
Load diff
+4
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "studio",
|
||||
"version": "0.0.3",
|
||||
"version": "0.0.8",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev -p 8082",
|
||||
@@ -15,12 +15,13 @@
|
||||
"@monaco-editor/react": "^4.3.1",
|
||||
"@next/bundle-analyzer": "^12.0.4",
|
||||
"@radix-ui/react-portal": "^0.1.1",
|
||||
"@radix-ui/react-tooltip": "^0.1.6",
|
||||
"@sentry/browser": "^6.15.0",
|
||||
"@sentry/nextjs": "^6.15.0",
|
||||
"@supabase/grid": "^2.3.9",
|
||||
"@supabase/postgres-meta": "^0.26.1",
|
||||
"@supabase/react-data-grid": "^7.1.0-beta.7",
|
||||
"@supabase/supabase-js": "^1.27.0",
|
||||
"@supabase/supabase-js": "^1.28.0",
|
||||
"@supabase/ui": "^0.35.0",
|
||||
"ajv": "^8.6.3",
|
||||
"analytics-node": "^3.5.0",
|
||||
@@ -33,7 +34,7 @@
|
||||
"markdown-table": "=2.0.0",
|
||||
"mobx": "^6.3.2",
|
||||
"mobx-react-lite": "^3.2.0",
|
||||
"next": "11.1.1",
|
||||
"next": "11.1.3",
|
||||
"next-compose-plugins": "^2.2.1",
|
||||
"openapi-types": "^9.1.0",
|
||||
"papaparse": "^5.3.1",
|
||||
|
||||
@@ -20,7 +20,7 @@ const Error404: NextPage = ({}) => {
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="w-full h-screen relative flex flex-col items-center justify-center mx-auto relative">
|
||||
<div className="w-full h-screen relative flex flex-col items-center justify-center mx-auto">
|
||||
<div className="absolute top-0 w-full max-w-7xl mx-auto pt-6 px-8 sm:px-6 lg:px-8">
|
||||
<nav className="relative flex items-center justify-between sm:h-10">
|
||||
<div className="flex items-center flex-grow flex-shrink-0 lg:flex-grow-0">
|
||||
@@ -39,7 +39,7 @@ const Error404: NextPage = ({}) => {
|
||||
</div>
|
||||
<div className="absolute">
|
||||
<Typography.Title
|
||||
className={`filter transition opacity-[5%] duration-200 ${
|
||||
className={`select-none filter transition opacity-[5%] duration-200 ${
|
||||
show404 ? 'blur-sm' : 'blur-none'
|
||||
}`}
|
||||
style={{ fontSize: '28rem' }}
|
||||
|
||||
+38
-3
@@ -10,13 +10,18 @@ import 'styles/contextMenu.scss'
|
||||
|
||||
import Head from 'next/head'
|
||||
import dynamic from 'next/dynamic'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useEffect, useState } from 'react'
|
||||
import type { AppProps } from 'next/app'
|
||||
import { Toaster } from 'react-hot-toast'
|
||||
import { Toaster, ToastBar, toast } from 'react-hot-toast'
|
||||
import { Button, IconX } from '@supabase/ui'
|
||||
|
||||
import { RootStore } from 'stores'
|
||||
import { StoreProvider } from 'hooks'
|
||||
import { getParameterByName } from 'lib/common/fetch'
|
||||
import { GOTRUE_ERRORS } from 'lib/constants'
|
||||
import PageTelemetry from 'components/ui/PageTelemetry'
|
||||
import FlagProvider from 'components/ui/Flag/FlagProvider'
|
||||
import { useState } from 'react'
|
||||
|
||||
const PortalRootWithNoSSR = dynamic(
|
||||
// @ts-ignore
|
||||
@@ -42,12 +47,42 @@ const PortalToast = () => (
|
||||
duration: 8000,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
>
|
||||
{(t) => (
|
||||
<ToastBar toast={t} style={t.style}>
|
||||
{({ icon, message }) => (
|
||||
<>
|
||||
{icon}
|
||||
{message}
|
||||
{t.type !== 'loading' && (
|
||||
<div className="ml-4">
|
||||
<Button className="!p-1" type="text" onClick={() => toast.dismiss(t.id)}>
|
||||
<IconX size={14} strokeWidth={2} />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</ToastBar>
|
||||
)}
|
||||
</Toaster>
|
||||
</PortalRootWithNoSSR>
|
||||
)
|
||||
|
||||
function MyApp({ Component, pageProps }: AppProps) {
|
||||
const [rootStore] = useState(() => new RootStore())
|
||||
const router = useRouter()
|
||||
|
||||
useEffect(() => {
|
||||
const errorDescription = getParameterByName('error_description', router.asPath)
|
||||
if (errorDescription === GOTRUE_ERRORS.UNVERIFIED_GITHUB_USER) {
|
||||
rootStore.ui.setNotification({
|
||||
category: 'error',
|
||||
message:
|
||||
'Please verify your email on Github first, then reach out to us at support@supabase.io to log into the dashboard',
|
||||
})
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<StoreProvider rootStore={rootStore}>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Typography } from '@supabase/ui'
|
||||
import { FC, createContext, useContext, useEffect, useState } from 'react'
|
||||
import { observer, useLocalObservable } from 'mobx-react-lite'
|
||||
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { API_URL, IS_PLATFORM } from 'lib/constants'
|
||||
import { useStore, withAuth } from 'hooks'
|
||||
import { get } from 'lib/common/fetch'
|
||||
import { snakeToCamel } from 'lib/helpers'
|
||||
@@ -78,7 +78,7 @@ const DocView: FC<any> = observer(({}) => {
|
||||
const swaggerUrl = data?.autoApiService?.restUrl
|
||||
const headers: any = { apikey: API_KEY }
|
||||
|
||||
if (API_KEY?.length > 40) headers['authorization'] = `Bearer ${API_KEY}`
|
||||
if (API_KEY?.length > 40) headers['Authorization'] = `Bearer ${API_KEY}`
|
||||
|
||||
const { data: jsonSchema, error: jsonSchemaError } = useSWR(
|
||||
() => swaggerUrl,
|
||||
@@ -112,7 +112,13 @@ const DocView: FC<any> = observer(({}) => {
|
||||
)
|
||||
|
||||
// Data Loaded
|
||||
const { autoApiService } = data
|
||||
const autoApiService = {
|
||||
...data.autoApiService,
|
||||
endpoint: IS_PLATFORM
|
||||
? `https://${data?.autoApiService?.endpoint}`
|
||||
: data.autoApiService.endpoint,
|
||||
}
|
||||
|
||||
const { query } = router
|
||||
const { page, resource, rpc } = query
|
||||
const { paths, definitions } = PageState.jsonSchema
|
||||
@@ -124,7 +130,7 @@ const DocView: FC<any> = observer(({}) => {
|
||||
<div className="Docs--inner-wrapper">
|
||||
<div className="sticky top-0 w-full flex flex-row-reverse z-40 ">
|
||||
<div
|
||||
className="border-b border-gray-500 dark:border-gray-500 bg-gray-700"
|
||||
className="border-b border-gray-200 dark:border-gray-500 bg-white dark:bg-gray-700"
|
||||
style={{ width: '50%' }}
|
||||
>
|
||||
<div className="z-0 flex ">
|
||||
@@ -132,8 +138,10 @@ const DocView: FC<any> = observer(({}) => {
|
||||
type="button"
|
||||
onClick={() => setSelectedLang('js')}
|
||||
className={`${
|
||||
selectedLang == 'js' ? 'text-gray-300' : 'text-gray-400'
|
||||
} relative inline-flex items-center p-1 px-2 bg-gray-600 border-r border-gray-500 dark:border-gray-500 text-sm font-medium hover:text-gray-300 focus:z-10 focus:outline-none focus:border-blue-300 focus:ring-blue active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150`}
|
||||
selectedLang == 'js'
|
||||
? 'text-gray-600 dark:text-gray-300'
|
||||
: 'text-gray-300 dark:text-gray-400'
|
||||
} relative inline-flex items-center p-1 px-2 bg-coolGray-100 dark:bg-gray-600 border-r border-gray-200 dark:border-gray-500 text-sm font-medium hover:text-gray-600 dark:hover:text-gray-300 focus:z-10 focus:outline-none focus:border-blue-300 focus:ring-blue active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150`}
|
||||
>
|
||||
JavaScript
|
||||
</button>
|
||||
@@ -141,16 +149,18 @@ const DocView: FC<any> = observer(({}) => {
|
||||
type="button"
|
||||
onClick={() => setSelectedLang('bash')}
|
||||
className={`${
|
||||
selectedLang == 'bash' ? 'text-gray-300' : 'text-gray-400'
|
||||
} relative inline-flex items-center p-1 px-2 bg-gray-600 border-r border-gray-500 dark:border-gray-500 text-sm font-medium hover:text-gray-300 focus:z-10 focus:outline-none focus:border-blue-300 focus:ring-blue active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150`}
|
||||
selectedLang == 'bash'
|
||||
? 'text-gray-600 dark:text-gray-300'
|
||||
: 'text-gray-300 dark:text-gray-400'
|
||||
} relative inline-flex items-center p-1 px-2 bg-coolGray-100 dark:bg-gray-600 border-r border-gray-200 dark:border-gray-500 text-sm font-medium hover:text-gray-600 dark:hover:text-gray-300 focus:z-10 focus:outline-none focus:border-blue-300 focus:ring-blue active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150`}
|
||||
>
|
||||
Bash
|
||||
</button>
|
||||
{selectedLang == 'bash' && (
|
||||
<div className="flex">
|
||||
<span className="text-sm text-gray-300 p-1 pl-2">Key:</span>
|
||||
<span className="text-sm text-gray-600 dark:text-gray-300 p-1 pl-2">Key:</span>
|
||||
<select
|
||||
className="text-sm text-gray-300 border-none cursor-pointer p-0 pl-2 pr-8"
|
||||
className="text-sm text-gray-600 dark:text-gray-300 border-none cursor-pointer p-0 pl-2 pr-8"
|
||||
value={showApiKey}
|
||||
onChange={(e) => setShowApiKey(e.target.value)}
|
||||
>
|
||||
@@ -335,7 +345,7 @@ const ResourceContent = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<h2 className="text-white mt-0">
|
||||
<h2 className="text-black dark:text-white mt-0">
|
||||
<code className="text-lg px-4 py-2">{resourceId}</code>
|
||||
</h2>
|
||||
|
||||
@@ -533,8 +543,8 @@ const ResourceContent = ({
|
||||
<div className="doc-section">
|
||||
<article className="text ">
|
||||
<p>
|
||||
Supabase provides realtime functionality and broadcasts database changes to authorized users depending on Row Level
|
||||
Security (RLS) policies.
|
||||
Supabase provides realtime functionality and broadcasts database changes to authorized
|
||||
users depending on Row Level Security (RLS) policies.
|
||||
</p>
|
||||
<p>
|
||||
<a href="https://supabase.com/docs/client/subscribe" target="_blank">
|
||||
|
||||
@@ -113,7 +113,9 @@ const TableEditorPage: NextPage = () => {
|
||||
|
||||
const onConfirmDeleteTable = async () => {
|
||||
try {
|
||||
const response: any = await meta.tables.del(selectedTableToDelete!.id)
|
||||
if (isUndefined(selectedTableToDelete)) return
|
||||
|
||||
const response: any = await meta.tables.del(selectedTableToDelete.id)
|
||||
if (response.error) throw response.error
|
||||
|
||||
const tables = meta.tables.list((table: PostgresTable) => table.schema === selectedSchema)
|
||||
@@ -126,12 +128,13 @@ const TableEditorPage: NextPage = () => {
|
||||
}
|
||||
ui.setNotification({
|
||||
category: 'success',
|
||||
message: `Successfully deleted ${selectedTableToDelete!.name}`,
|
||||
message: `Successfully deleted ${selectedTableToDelete.name}`,
|
||||
})
|
||||
} catch (error: any) {
|
||||
ui.setNotification({
|
||||
error,
|
||||
category: 'error',
|
||||
message: `Failed to delete ${selectedTableToDelete!.name}: ${error.message}`,
|
||||
message: `Failed to delete ${selectedTableToDelete?.name}: ${error.message}`,
|
||||
})
|
||||
} finally {
|
||||
setIsDeleting(false)
|
||||
|
||||
@@ -54,7 +54,7 @@ const Home: NextPage = () => {
|
||||
<IconLoader className="animate-spin" size={14} />
|
||||
<Typography.Text>Retrieving project usage statistics</Typography.Text>
|
||||
</div>
|
||||
) : hasProjectData ? (
|
||||
) : !usage.error && hasProjectData ? (
|
||||
<ProjectUsage project={project} />
|
||||
) : (
|
||||
<NewProjectPanel />
|
||||
|
||||
@@ -179,7 +179,7 @@ const Reports = () => {
|
||||
const payload = {
|
||||
content: config,
|
||||
}
|
||||
await contentStore.update(id, payload)
|
||||
await contentStore.update(id, payload, 'report')
|
||||
await fetchReport()
|
||||
setHasEdits(false)
|
||||
setSaving(false)
|
||||
|
||||
@@ -1,122 +1,144 @@
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import { SettingsLayout } from 'components/layouts/'
|
||||
import useSWR from 'swr'
|
||||
import debounce from 'lodash/debounce'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { NextPage } from 'next'
|
||||
import { useRouter } from 'next/router'
|
||||
import LogPanel from 'components/ui/Logs/LogPanel'
|
||||
import { LOG_TYPE_LABEL_MAPPING } from 'lib/constants'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { Typography, IconLoader, IconAlertCircle } from '@supabase/ui'
|
||||
|
||||
import { withAuth } from 'hooks'
|
||||
import { get } from 'lib/common/fetch'
|
||||
import LogTable, { LogData } from 'components/ui/Logs/LogTable'
|
||||
import useSWR from 'swr'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { Typography, Button } from '@supabase/ui'
|
||||
import debounce from 'lodash/debounce'
|
||||
import { API_URL, LOG_TYPE_LABEL_MAPPING } from 'lib/constants'
|
||||
import { SettingsLayout } from 'components/layouts/'
|
||||
import CodeEditor from 'components/ui/CodeEditor'
|
||||
import { ButtonProps } from '@supabase/ui/dist/cjs/components/Button/Button'
|
||||
import {
|
||||
LogPanel,
|
||||
LogTable,
|
||||
Count,
|
||||
Logs,
|
||||
LogTemplate,
|
||||
TEMPLATES,
|
||||
} from 'components/interfaces/Settings/Logs'
|
||||
import { uuidv4 } from 'lib/helpers'
|
||||
|
||||
interface CountData {
|
||||
count: number
|
||||
}
|
||||
/**
|
||||
* Acts as a container component for the entire log display
|
||||
*/
|
||||
export const LogPage = () => {
|
||||
export const LogPage: NextPage = () => {
|
||||
const router = useRouter()
|
||||
const { ref, type } = router.query
|
||||
|
||||
const [editorId, setEditorId] = useState<string>(uuidv4())
|
||||
const [search, setSearch] = useState<string>('')
|
||||
const [queryParams, setQueryParams] = useState<string>('')
|
||||
const [where, setWhere] = useState<string>('')
|
||||
const [mode, setMode] = useState<'simple' | 'custom'>('simple')
|
||||
const [latestRefresh, setLatestRefresh] = useState<string>(new Date().toISOString())
|
||||
|
||||
const title = LOG_TYPE_LABEL_MAPPING[type as string]
|
||||
|
||||
// handle log fetching
|
||||
const {
|
||||
data,
|
||||
isValidating: isLoading,
|
||||
mutate,
|
||||
} = useSWR<{ data: LogData[]; count: number; error: any }>(
|
||||
`${API_URL}/projects/${ref}/logs?${queryParams}`,
|
||||
get,
|
||||
{
|
||||
revalidateOnFocus: false,
|
||||
}
|
||||
)
|
||||
|
||||
const title = `Logs - ${LOG_TYPE_LABEL_MAPPING[type as string]}`
|
||||
const debouncedQueryParams = useRef(debounce(setQueryParams, 600)).current
|
||||
|
||||
useEffect(() => {
|
||||
const params = {
|
||||
type: type as string,
|
||||
search_query: search || '',
|
||||
where: where || '',
|
||||
where: mode === 'custom' ? where || '' : '',
|
||||
}
|
||||
const qs = new URLSearchParams(params).toString()
|
||||
debouncedQueryParams(qs)
|
||||
return () => debouncedQueryParams.cancel()
|
||||
}, [search, where, type])
|
||||
}, [mode, search, where, type])
|
||||
|
||||
const countKey = `${API_URL}/projects/${ref}/logs?${queryParams}&count=true&period_start=${latestRefresh}`
|
||||
const { data: countData } = useSWR<{ data: [CountData] | []; error?: any }>(countKey, get, {
|
||||
refreshInterval: 5000,
|
||||
})
|
||||
const newCount = countData?.data?.[0]?.count
|
||||
// handle log fetching
|
||||
const logUrl = `${API_URL}/projects/${ref}/logs?${queryParams}`
|
||||
const { data, isValidating, mutate } = useSWR<Logs>(logUrl, get, { revalidateOnFocus: false })
|
||||
const { data: logData, error } = data || {}
|
||||
|
||||
const countUrl = `${API_URL}/projects/${ref}/logs?${queryParams}&count=true&period_start=${latestRefresh}`
|
||||
const { data: countData } = useSWR<Count>(countUrl, get, { refreshInterval: 5000 })
|
||||
const newCount = countData?.data?.[0]?.count ?? 0
|
||||
|
||||
const handleReset = () => {
|
||||
setWhere('')
|
||||
setSearch('')
|
||||
setEditorId(uuidv4())
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
setLatestRefresh(new Date().toISOString())
|
||||
mutate()
|
||||
}
|
||||
|
||||
const handleModeToggle = () => {
|
||||
if (mode === 'simple') {
|
||||
setMode('custom')
|
||||
setSearch('')
|
||||
// setWhere(DEFAULT_QUERY)
|
||||
} else {
|
||||
setMode('simple')
|
||||
}
|
||||
}
|
||||
|
||||
const onSelectTemplate = (template: LogTemplate) => {
|
||||
setMode(template.mode)
|
||||
if (template.mode === 'simple') {
|
||||
setSearch(template.searchString)
|
||||
setWhere('')
|
||||
} else {
|
||||
setWhere(template.searchString)
|
||||
setSearch('')
|
||||
setEditorId(uuidv4())
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingsLayout title={title} className="p-4 space-y-4">
|
||||
<LogPanel
|
||||
showReset={(where || search) ? true : false}
|
||||
onReset={() => {
|
||||
setWhere('')
|
||||
setSearch('')
|
||||
setMode('simple') // this is necessary to reset the value of the monaco editor
|
||||
}}
|
||||
templates={[
|
||||
{ label: "Recent Errors", onClick: () => setSearch('[Ee]rror|\\s[45][0-9][0-9]\\s') },
|
||||
{ label: "POST or PATCH", onClick: () => {
|
||||
setSearch('')
|
||||
setMode('custom')
|
||||
setWhere("REGEXP_CONTAINS(event_message, 'POST') OR REGEXP_CONTAINS(event_message, 'PATCH') ")
|
||||
} },
|
||||
]}
|
||||
searchValue={search}
|
||||
onCustomClick={handleModeToggle} isLoading={isLoading} onRefresh={handleRefresh} onSearch={setSearch} />
|
||||
{mode === 'custom' &&
|
||||
<div>
|
||||
<CodeEditor
|
||||
className="p-4 h-24"
|
||||
hideLineNumbers
|
||||
id={'logs-where-editor'}
|
||||
language="pgsql"
|
||||
defaultValue={where}
|
||||
onInputChange={(v) => setWhere(v || '')}
|
||||
onInputRun={handleRefresh}
|
||||
/>
|
||||
</div>}
|
||||
{error && (
|
||||
<Typography.Text className="text-center w-full block">Could not fetch data</Typography.Text>
|
||||
)}
|
||||
{newCount && <LoadNewLogsButton onClick={handleRefresh} />}
|
||||
<LogTable data={logData} />
|
||||
<SettingsLayout title={title}>
|
||||
<div className="h-full flex flex-col flex-grow">
|
||||
<LogPanel
|
||||
isCustomQuery={mode === 'custom'}
|
||||
isLoading={isValidating}
|
||||
newCount={newCount}
|
||||
showReset={search.length > 0}
|
||||
searchValue={search}
|
||||
templates={TEMPLATES}
|
||||
onReset={handleReset}
|
||||
onRefresh={handleRefresh}
|
||||
onSearch={setSearch}
|
||||
onCustomClick={handleModeToggle}
|
||||
onSelectTemplate={onSelectTemplate}
|
||||
/>
|
||||
{mode === 'custom' && (
|
||||
<div className="min-h-[7rem] h-28">
|
||||
<CodeEditor
|
||||
id={editorId}
|
||||
language="pgsql"
|
||||
defaultValue={where}
|
||||
onInputChange={(v) => setWhere(v || '')}
|
||||
onInputRun={handleRefresh}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col flex-grow relative">
|
||||
{isValidating && (
|
||||
<div
|
||||
className={[
|
||||
'absolute top-0 w-full h-full bg-gray-800 flex items-center justify-center',
|
||||
`${isValidating ? 'bg-opacity-75 z-50' : ''}`,
|
||||
].join(' ')}
|
||||
>
|
||||
<IconLoader className="animate-spin" />
|
||||
</div>
|
||||
)}
|
||||
{error && (
|
||||
<div className="flex w-full h-full justify-center items-center space-x-2 mx-auto">
|
||||
<IconAlertCircle size={16} />
|
||||
<Typography.Text type="secondary">Sorry! Could not fetch data</Typography.Text>
|
||||
</div>
|
||||
)}
|
||||
<LogTable data={logData} isCustomQuery={mode === 'custom'} />
|
||||
</div>
|
||||
</div>
|
||||
</SettingsLayout>
|
||||
)
|
||||
}
|
||||
|
||||
const LoadNewLogsButton = (props: ButtonProps) => <Button type="dashed" block {...props}>Load new logs</Button>
|
||||
|
||||
export default withAuth(observer(LogPage))
|
||||
@@ -41,7 +41,7 @@ const PageConfig = () => {
|
||||
*/
|
||||
async function loadPersistantData() {
|
||||
if (sqlEditorStore === undefined) return
|
||||
sqlEditorStore.loadRemotePersistentData(contentStore, (user as any)?.id)
|
||||
await sqlEditorStore.loadRemotePersistentData(contentStore, (user as any)?.id)
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -106,12 +106,10 @@ export default class UiStore implements IUiStore {
|
||||
}
|
||||
|
||||
setProfile(value?: User) {
|
||||
if (value?.id != this.profile?.id) {
|
||||
this.profile = value
|
||||
|
||||
if (value) {
|
||||
Telemetry.sendIdentify(value)
|
||||
}
|
||||
if (value && value?.id != this.profile?.id) {
|
||||
Telemetry.sendIdentify(value)
|
||||
}
|
||||
|
||||
this.profile = value
|
||||
}
|
||||
}
|
||||
@@ -17,9 +17,10 @@ export default class ProjectStore extends PostgresMetaInterface<Project> {
|
||||
initialDataArray(value: Project[]) {
|
||||
const finalValue = value.map((project: any) => {
|
||||
const kpsVersion =
|
||||
project.services?.length > 0
|
||||
project?.services?.length > 0
|
||||
? project?.services[0]?.infrastructure[0]?.app_versions?.version
|
||||
: undefined
|
||||
|
||||
return { ...project, kpsVersion }
|
||||
})
|
||||
super.initialDataArray(finalValue)
|
||||
|
||||
@@ -2,7 +2,7 @@ import { makeAutoObservable } from 'mobx'
|
||||
import { keyBy } from 'lodash'
|
||||
|
||||
import { get, post, patch, delete_ } from 'lib/common/fetch'
|
||||
import { UserContentMap } from 'types'
|
||||
import { UserContent, UserContentMap } from 'types'
|
||||
import { IRootStore } from '../RootStore'
|
||||
import { API_URL } from 'lib/constants'
|
||||
|
||||
@@ -137,7 +137,7 @@ export default class ProjectContentStore implements IProjectContentStore {
|
||||
}
|
||||
}
|
||||
|
||||
async update(id: any, updates: any, type: string) {
|
||||
async update(id: any, updates: any, type: UserContent['type']) {
|
||||
try {
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
|
||||
@@ -116,6 +116,7 @@ export default class MetaStore implements IMetaStore {
|
||||
|
||||
connectionString?: string
|
||||
baseUrl: string
|
||||
queryBaseUrl: string
|
||||
excludedSchemas = [
|
||||
'auth',
|
||||
'extensions',
|
||||
@@ -132,6 +133,7 @@ export default class MetaStore implements IMetaStore {
|
||||
const { projectRef, connectionString } = options
|
||||
this.rootStore = rootStore
|
||||
this.baseUrl = `/api/pg-meta/${projectRef}`
|
||||
this.queryBaseUrl = `${API_URL}/pg-meta/${projectRef}`
|
||||
|
||||
const headers: any = {}
|
||||
if (IS_PLATFORM && connectionString) {
|
||||
@@ -166,7 +168,7 @@ export default class MetaStore implements IMetaStore {
|
||||
try {
|
||||
const headers: any = { 'Content-Type': 'application/json' }
|
||||
if (this.connectionString) headers['x-connection-encrypted'] = this.connectionString
|
||||
const url = `${this.baseUrl}/query`
|
||||
const url = `${this.queryBaseUrl}/query`
|
||||
const response = await post(url, { query: value }, { headers })
|
||||
if (response.error) throw response.error
|
||||
|
||||
@@ -302,6 +304,8 @@ export default class MetaStore implements IMetaStore {
|
||||
// Duplicate foreign key constraints over
|
||||
const relationships = metadata.duplicateTable.relationships
|
||||
if (relationships.length > 0) {
|
||||
// @ts-ignore, but might need to investigate, sounds bad:
|
||||
// Type instantiation is excessively deep and possibly infinite
|
||||
relationships.map(async (relationship: PostgresRelationship) => {
|
||||
const relation = await this.rootStore.meta.addForeignKey({
|
||||
...relationship,
|
||||
@@ -410,13 +414,12 @@ export default class MetaStore implements IMetaStore {
|
||||
const { error }: any = await this.insertRowsViaSpreadsheet(
|
||||
importContent.file,
|
||||
table,
|
||||
(progress: any) => {
|
||||
(progress: number) => {
|
||||
this.rootStore.ui.setNotification({
|
||||
id: toastId,
|
||||
progress,
|
||||
category: 'loading',
|
||||
message: `Adding ${importContent.rowCount.toLocaleString()} rows to ${
|
||||
table.name
|
||||
}... (${progress}%)`,
|
||||
message: `Adding ${importContent.rowCount.toLocaleString()} rows to ${table.name}`,
|
||||
})
|
||||
}
|
||||
)
|
||||
@@ -432,25 +435,24 @@ export default class MetaStore implements IMetaStore {
|
||||
|
||||
if (!isUndefined(error)) {
|
||||
this.rootStore.ui.setNotification({
|
||||
id: toastId,
|
||||
category: 'error',
|
||||
message: `Table ${table.name} has been created but we ran into an error while inserting rows:
|
||||
${error.message} - ${error.details}`,
|
||||
message: 'Do check your spreadsheet if there are any discrepancies.',
|
||||
})
|
||||
this.rootStore.ui.setNotification({
|
||||
category: 'error',
|
||||
message: 'Do check your spreadsheet if there are any discrepancies.',
|
||||
message: `Table ${table.name} has been created but we ran into an error while inserting rows:
|
||||
${error.message}`,
|
||||
error,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// Via text copy and paste
|
||||
await this.insertTableRows(table, importContent.rows, (progress: any) => {
|
||||
await this.insertTableRows(table, importContent.rows, (progress: number) => {
|
||||
this.rootStore.ui.setNotification({
|
||||
id: toastId,
|
||||
progress,
|
||||
category: 'loading',
|
||||
message: `Adding ${importContent.rows.length.toLocaleString()} rows to ${
|
||||
table.name
|
||||
}... (${progress}%)`,
|
||||
message: `Adding ${importContent.rows.length.toLocaleString()} rows to ${table.name}`,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -527,8 +529,7 @@ export default class MetaStore implements IMetaStore {
|
||||
...column,
|
||||
isPrimaryKey: false,
|
||||
})
|
||||
const addedColumn: any = await this.createColumn(columnPayload, column.foreignKey)
|
||||
if (addedColumn.error) throw addedColumn.error
|
||||
await this.createColumn(columnPayload, column.foreignKey)
|
||||
} else {
|
||||
const originalColumn = find(originalColumns, { id: column.id })
|
||||
if (originalColumn) {
|
||||
@@ -545,13 +546,7 @@ export default class MetaStore implements IMetaStore {
|
||||
category: 'loading',
|
||||
message: `Updating column ${column.name} from ${updatedTable.name}`,
|
||||
})
|
||||
const updatedColumn: any = await this.updateColumn(
|
||||
column.id,
|
||||
columnPayload,
|
||||
updatedTable,
|
||||
column.foreignKey
|
||||
)
|
||||
if (updatedColumn.error) throw updatedColumn.error
|
||||
await this.updateColumn(column.id, columnPayload, updatedTable, column.foreignKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -573,7 +568,7 @@ export default class MetaStore implements IMetaStore {
|
||||
async insertRowsViaSpreadsheet(
|
||||
file: any,
|
||||
table: PostgresTable,
|
||||
onProgressUpdate: (progress: any) => void
|
||||
onProgressUpdate: (progress: number) => void
|
||||
) {
|
||||
let chunkNumber = 0
|
||||
let insertError: any = undefined
|
||||
@@ -601,7 +596,7 @@ export default class MetaStore implements IMetaStore {
|
||||
} else {
|
||||
chunkNumber += 1
|
||||
const progress = (chunkNumber * CHUNK_SIZE) / file.size
|
||||
const progressPercentage = progress > 1 ? 100 : Number((progress * 100).toFixed(2))
|
||||
const progressPercentage = progress > 1 ? 100 : progress * 100
|
||||
onProgressUpdate(progressPercentage)
|
||||
parser.resume()
|
||||
}
|
||||
@@ -618,7 +613,7 @@ export default class MetaStore implements IMetaStore {
|
||||
async insertTableRows(
|
||||
table: PostgresTable,
|
||||
rows: any,
|
||||
onProgressUpdate: (progress: any) => void
|
||||
onProgressUpdate: (progress: number) => void
|
||||
) {
|
||||
let insertError = undefined
|
||||
let insertProgress = 0
|
||||
@@ -649,7 +644,7 @@ export default class MetaStore implements IMetaStore {
|
||||
if (hasFailedBatch) {
|
||||
break
|
||||
}
|
||||
onProgressUpdate((insertProgress * 100).toFixed(2))
|
||||
onProgressUpdate(insertProgress * 100)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -57,7 +57,7 @@ export default class OpenApiStore implements IOpenApiStore {
|
||||
const restApiUrl = projectConfig.autoApiService?.restUrl
|
||||
|
||||
const response = await get<OpenAPIV2.Document>(`${restApiUrl}?apikey=${apiKey}`, {
|
||||
headers: { ...headers, apiKey: apiKey, authorization: `Bearer ${apiKey}` },
|
||||
headers: { ...headers, apiKey: apiKey, Authorization: `Bearer ${apiKey}` },
|
||||
credentials: 'omit',
|
||||
})
|
||||
if (response.error) throw response.error
|
||||
|
||||
+13
-14
@@ -14,10 +14,15 @@
|
||||
@apply text-sm;
|
||||
}
|
||||
|
||||
.token.imports,
|
||||
.token.property-access {
|
||||
@apply text-blueGray-500 dark:text-blueGray-300 #{!important};
|
||||
}
|
||||
|
||||
.token.keyword.module,
|
||||
.token.function {
|
||||
font-style: normal !important;
|
||||
color: #ccc !important;
|
||||
@apply text-gray-600 dark:text-gray-300 #{!important};
|
||||
}
|
||||
|
||||
.token.plain,
|
||||
@@ -30,46 +35,40 @@
|
||||
.token.doc-comment.parameter.punctuation,
|
||||
.token.doc-comment.optional-parameter.punctuation {
|
||||
font-style: normal !important;
|
||||
color: #ccc !important;
|
||||
@apply text-gray-600 dark:text-gray-300 #{!important};
|
||||
}
|
||||
|
||||
.token.keyword,
|
||||
.token.doc-comment.keyword {
|
||||
font-style: normal !important;
|
||||
color: #569cd6 !important;
|
||||
@apply text-blue-600 dark:text-blue-300 #{!important};
|
||||
}
|
||||
|
||||
.token.string {
|
||||
font-style: normal !important;
|
||||
color: #24b47e !important;
|
||||
}
|
||||
|
||||
.token.console {
|
||||
font-style: normal !important;
|
||||
color: #49c1ad !important;
|
||||
@apply text-green-500 #{!important};
|
||||
}
|
||||
|
||||
.token.constant {
|
||||
font-style: normal !important;
|
||||
color: #569cd6 !important;
|
||||
@apply text-blue-600 dark:text-blue-300 #{!important};
|
||||
}
|
||||
|
||||
.token.comment {
|
||||
font-style: normal !important;
|
||||
color: #666 !important;
|
||||
@apply text-gray-300 dark:text-gray-400 #{!important};
|
||||
}
|
||||
|
||||
.token.number {
|
||||
color: #b5cea8 !important;
|
||||
@apply text-green-600 dark:text-green-200 #{!important};
|
||||
}
|
||||
|
||||
.token.boolean,
|
||||
.token.doc-comment.class-name {
|
||||
@apply text-green-500;
|
||||
@apply text-green-500 dark:text-green-500;
|
||||
}
|
||||
|
||||
.codeBlock {
|
||||
@apply bg-gray-600;
|
||||
margin-bottom: 0;
|
||||
overflow: hidden;
|
||||
overflow-wrap: break-word;
|
||||
|
||||
@@ -144,7 +144,7 @@ input[type='radio'] {
|
||||
}
|
||||
|
||||
code {
|
||||
@apply p-1 m-0 font-mono text-sm bg-gray-600 rounded;
|
||||
@apply p-1 m-0 font-mono text-sm bg-gray-100 dark:bg-gray-600 rounded;
|
||||
}
|
||||
|
||||
div[data-radix-portal]:not(.portal--toast) {
|
||||
|
||||
+15
-4
@@ -195,10 +195,14 @@ label:not(.sbui-formlayout__label):not(.sbui-checkbox__label-container__label):n
|
||||
/* Docs */
|
||||
.Docs {
|
||||
&--inner-wrapper {
|
||||
background: linear-gradient(90deg, #1f1f1f 50%, #2a2a2a 50%);
|
||||
background: linear-gradient(90deg, #ffffff 50%, #f3f4f6 50%); /* bg-white bg-coolGray-100 */
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dark &--inner-wrapper {
|
||||
background: linear-gradient(90deg, #1f1f1f 50%, #2a2a2a 50%); /* bg-gray-700 bg-gray-600 */
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3 {
|
||||
@@ -206,13 +210,13 @@ label:not(.sbui-formlayout__label):not(.sbui-checkbox__label-container__label):n
|
||||
}
|
||||
|
||||
.doc-heading {
|
||||
@apply text-white capitalize mt-4 w-1/2;
|
||||
@apply text-black dark:text-white capitalize mt-4 w-1/2;
|
||||
hyphens: auto;
|
||||
max-width: 50%;
|
||||
}
|
||||
|
||||
.doc-section {
|
||||
@apply border-b dark:border-dark text-gray-300 flex;
|
||||
@apply border-b dark:border-dark text-gray-600 dark:text-gray-300 flex;
|
||||
}
|
||||
|
||||
.text {
|
||||
@@ -233,7 +237,7 @@ label:not(.sbui-formlayout__label):not(.sbui-checkbox__label-container__label):n
|
||||
}
|
||||
|
||||
.code {
|
||||
@apply flex-1 p-2 w-1/2 bg-gray-600 pb-4;
|
||||
@apply flex-1 p-2 w-1/2 pb-4;
|
||||
max-width: 50%;
|
||||
|
||||
h4 {
|
||||
@@ -306,3 +310,10 @@ label:not(.sbui-formlayout__label):not(.sbui-checkbox__label-container__label):n
|
||||
cursor: row-resize;
|
||||
}
|
||||
}
|
||||
|
||||
.radix-tooltip-arrow {
|
||||
polygon {
|
||||
@apply dark:fill-[#1f1f1f];
|
||||
fill: #eeeeee;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { screen, getByText } from '@testing-library/react'
|
||||
|
||||
interface SelectorOptions {
|
||||
container?: HTMLElement
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the toggle button given a text matcher
|
||||
*
|
||||
* Defaults to screen if container option is not provided
|
||||
*/
|
||||
export const getToggleByText = (text: string | RegExp, options: SelectorOptions = {}): HTMLElement | null => {
|
||||
const container = options?.container
|
||||
let textNode;
|
||||
if (container) {
|
||||
textNode = getByText(container as HTMLElement, text)
|
||||
}
|
||||
else {
|
||||
textNode = screen.getByText(text)
|
||||
}
|
||||
if (textNode && textNode.parentElement) {
|
||||
return textNode.parentElement.querySelector("button[class*='toggle']")
|
||||
} else {
|
||||
return textNode
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import LogPanel from 'components/ui/Logs/LogPanel'
|
||||
import LogPanel from 'components/interfaces/Settings/Logs/LogPanel'
|
||||
import { render, waitFor, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
|
||||
@@ -7,7 +7,7 @@ test('templates', async () => {
|
||||
render(<LogPanel templates={[
|
||||
{ label: 'Some option', onClick: mockFn }
|
||||
]} />)
|
||||
const search = screen.getByPlaceholderText(/Filter/)
|
||||
const search = screen.getByPlaceholderText(/Search/)
|
||||
userEvent.type(search, "12345")
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ test('templates', async () => {
|
||||
test("filter input change", async () => {
|
||||
const mockFn = jest.fn()
|
||||
render(<LogPanel onSearch={mockFn} />)
|
||||
const search = screen.getByPlaceholderText(/Filter/)
|
||||
const search = screen.getByPlaceholderText(/Search/)
|
||||
userEvent.type(search, "12345")
|
||||
expect(mockFn).toBeCalled()
|
||||
})
|
||||
@@ -49,9 +49,9 @@ test("Manual refresh", async () => {
|
||||
test("reset filters", () => {
|
||||
const mockFn = jest.fn()
|
||||
const { rerender } = render(<LogPanel showReset={false} />)
|
||||
expect(() => screen.getByText(/Reset filters/)).toThrow()
|
||||
expect(() => screen.getByText(/Clear search/)).toThrow()
|
||||
|
||||
rerender(<LogPanel showReset={true} onReset={mockFn} />)
|
||||
userEvent.click(screen.getByText(/Reset filters/))
|
||||
userEvent.click(screen.getByTitle(/Clear search/))
|
||||
expect(mockFn).toBeCalled()
|
||||
})
|
||||
@@ -1,4 +1,4 @@
|
||||
import LogTable from 'components/ui/Logs/LogTable'
|
||||
import LogTable from 'components/interfaces/Settings/Logs/LogTable'
|
||||
import { render, fireEvent, waitFor, screen } from '@testing-library/react'
|
||||
test('can display log data', async () => {
|
||||
render(<LogTable data={[{
|
||||
|
||||
@@ -4,50 +4,49 @@ import { get } from 'lib/common/fetch'
|
||||
|
||||
// mock the settings layout
|
||||
jest.mock('components/layouts', () => ({
|
||||
SettingsLayout: jest.fn()
|
||||
.mockImplementation(({ children }) => <div>{children}</div>)
|
||||
SettingsLayout: jest.fn().mockImplementation(({ children }) => <div>{children}</div>),
|
||||
}))
|
||||
|
||||
// mock mobx
|
||||
jest.mock('mobx-react-lite')
|
||||
import { observer } from 'mobx-react-lite'
|
||||
observer.mockImplementation(v => v)
|
||||
observer.mockImplementation((v) => v)
|
||||
|
||||
// mock the router
|
||||
jest.mock('next/router')
|
||||
import { useRouter } from 'next/router'
|
||||
useRouter.mockReturnValue({ query: { ref: "123", type: "auth" } })
|
||||
useRouter.mockReturnValue({ query: { ref: '123', type: 'auth' } })
|
||||
|
||||
// mock monaco editor
|
||||
jest.mock("@monaco-editor/react")
|
||||
import Editor, { useMonaco } from "@monaco-editor/react"
|
||||
jest.mock('@monaco-editor/react')
|
||||
import Editor, { useMonaco } from '@monaco-editor/react'
|
||||
Editor = jest.fn()
|
||||
Editor.mockImplementation((props) => {
|
||||
return (
|
||||
<textarea
|
||||
className="monaco-editor"
|
||||
onChange={e => props.onChange(e.target.value)}
|
||||
></textarea>
|
||||
<textarea className="monaco-editor" onChange={(e) => props.onChange(e.target.value)}></textarea>
|
||||
)
|
||||
})
|
||||
useMonaco.mockImplementation(v => v)
|
||||
useMonaco.mockImplementation((v) => v)
|
||||
|
||||
import { LogPage } from 'pages/project/[ref]/settings/logs/[type]'
|
||||
import { render, fireEvent, waitFor, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import {getToggleByText} from "../../helpers"
|
||||
|
||||
beforeEach(() => {
|
||||
get.mockReset()
|
||||
})
|
||||
test('can display log data and metadata', async () => {
|
||||
const data = [{
|
||||
id: "seome-uuid",
|
||||
timestamp: 1621323232312,
|
||||
event_message: "some event happened",
|
||||
metadata: {
|
||||
my_key: "something_value"
|
||||
}
|
||||
}]
|
||||
const data = [
|
||||
{
|
||||
id: 'seome-uuid',
|
||||
timestamp: 1621323232312,
|
||||
event_message: 'some event happened',
|
||||
metadata: {
|
||||
my_key: 'something_value',
|
||||
},
|
||||
},
|
||||
]
|
||||
get.mockResolvedValue({ data })
|
||||
render(<LogPage />)
|
||||
|
||||
@@ -59,15 +58,16 @@ test('can display log data and metadata', async () => {
|
||||
})
|
||||
|
||||
test('Refresh', async () => {
|
||||
|
||||
const data = [{
|
||||
id: "some-uuid",
|
||||
timestamp: 1621323232312,
|
||||
event_message: "some event happened",
|
||||
metadata: {
|
||||
my_key: "something_value"
|
||||
}
|
||||
}]
|
||||
const data = [
|
||||
{
|
||||
id: 'some-uuid',
|
||||
timestamp: 1621323232312,
|
||||
event_message: 'some event happened',
|
||||
metadata: {
|
||||
my_key: 'something_value',
|
||||
},
|
||||
},
|
||||
]
|
||||
get.mockResolvedValueOnce({ data }).mockResolvedValueOnce({ data: [] })
|
||||
render(<LogPage />)
|
||||
|
||||
@@ -82,84 +82,98 @@ test('Refresh', async () => {
|
||||
await waitFor(() => screen.queryByText(/happened/) === null, { timeout: 1000 })
|
||||
})
|
||||
|
||||
|
||||
test("Search will trigger a log refresh", async () => {
|
||||
test('Search will trigger a log refresh', async () => {
|
||||
get.mockImplementation((url) => {
|
||||
|
||||
if (url.includes("search_query") && url.includes("something")) {
|
||||
if (url.includes('search_query') && url.includes('something')) {
|
||||
return {
|
||||
data: [{
|
||||
id: "some-uuid",
|
||||
timestamp: 1621323232312,
|
||||
event_message: "some event happened",
|
||||
metadata: {}
|
||||
}]
|
||||
data: [
|
||||
{
|
||||
id: 'some-uuid',
|
||||
timestamp: 1621323232312,
|
||||
event_message: 'some event happened',
|
||||
metadata: {},
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
return { data: [] }
|
||||
})
|
||||
render(<LogPage />)
|
||||
|
||||
userEvent.type(screen.getByPlaceholderText(/Filter/), "something")
|
||||
await waitFor(() => {
|
||||
expect(get).toHaveBeenCalledWith(expect.stringContaining("search_query"))
|
||||
expect(get).toHaveBeenCalledWith(expect.stringContaining("something"))
|
||||
}, { timeout: 1500 })
|
||||
userEvent.type(screen.getByPlaceholderText(/Search/), 'something')
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(get).toHaveBeenCalledWith(expect.stringContaining('search_query'))
|
||||
expect(get).toHaveBeenCalledWith(expect.stringContaining('something'))
|
||||
},
|
||||
{ timeout: 1500 }
|
||||
)
|
||||
|
||||
await waitFor(() => screen.getByText(/happened/), { timeout: 1000 })
|
||||
})
|
||||
|
||||
test("poll count for new messages", async () => {
|
||||
test('poll count for new messages', async () => {
|
||||
get.mockImplementation((url) => {
|
||||
if (url.includes("count")) {
|
||||
return { data: [{ count: 3 }] }
|
||||
if (url.includes('count')) {
|
||||
return { data: [{ count: 125 }] }
|
||||
}
|
||||
return {
|
||||
data: [{
|
||||
id: "some-uuid",
|
||||
timestamp: 1621323232312,
|
||||
event_message: "some event happened",
|
||||
metadata: {}
|
||||
}]
|
||||
data: [
|
||||
{
|
||||
id: 'some-uuid',
|
||||
timestamp: 1621323232312,
|
||||
event_message: 'some event happened',
|
||||
metadata: {},
|
||||
},
|
||||
],
|
||||
}
|
||||
})
|
||||
render(
|
||||
<LogPage />
|
||||
)
|
||||
render(<LogPage />)
|
||||
await waitFor(() => screen.queryByText(/happened/) === null)
|
||||
await waitFor(() => screen.getByText(/Load new logs/))
|
||||
// should display new logs count
|
||||
await waitFor(() => screen.getByText(/125/))
|
||||
|
||||
userEvent.click(screen.getByText(/Load new logs/))
|
||||
await waitFor(() => screen.queryByText(/Load new logs/) === null)
|
||||
userEvent.click(screen.getByText(/Refresh/))
|
||||
await waitFor(() => screen.queryByText(/125/) === null)
|
||||
await waitFor(() => screen.getByText(/happened/))
|
||||
})
|
||||
|
||||
|
||||
test("where clause will trigger a log refresh", async () => {
|
||||
test('where clause will trigger a log refresh', async () => {
|
||||
get.mockImplementation((url) => {
|
||||
|
||||
if (url.includes("where") && url.includes("something")) {
|
||||
if (url.includes('where') && url.includes('something')) {
|
||||
return {
|
||||
data: [{
|
||||
id: "some-uuid",
|
||||
timestamp: 1621323232312,
|
||||
event_message: "some event happened",
|
||||
metadata: {}
|
||||
}]
|
||||
data: [
|
||||
{
|
||||
id: 'some-uuid',
|
||||
timestamp: 1621323232312,
|
||||
event_message: 'some event happened',
|
||||
metadata: {},
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
return { data: [] }
|
||||
})
|
||||
const { container } = render(<LogPage />)
|
||||
let editor = container.querySelector('.monaco-editor');
|
||||
let editor = container.querySelector('.monaco-editor')
|
||||
expect(editor).toBeFalsy()
|
||||
userEvent.click(screen.getByText("Custom query"))
|
||||
editor = container.querySelector('.monaco-editor');
|
||||
userEvent.type(editor, "metadata.field = something")
|
||||
// TODO: abstract this out into a toggle selection helper
|
||||
const toggle = getToggleByText(/via query/)
|
||||
expect(toggle).toBeTruthy()
|
||||
userEvent.click(toggle)
|
||||
await waitFor(() => {
|
||||
expect(get).toHaveBeenCalledWith(expect.stringContaining("where"))
|
||||
expect(get).toHaveBeenCalledWith(expect.stringContaining("metadata.field"))
|
||||
}, { timeout: 1000 })
|
||||
editor = container.querySelector('.monaco-editor')
|
||||
expect(editor).toBeTruthy()
|
||||
})
|
||||
editor = container.querySelector('.monaco-editor')
|
||||
userEvent.type(editor, 'metadata.field = something')
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(get).toHaveBeenCalledWith(expect.stringContaining('where'))
|
||||
expect(get).toHaveBeenCalledWith(expect.stringContaining('metadata.field'))
|
||||
},
|
||||
{ timeout: 1000 }
|
||||
)
|
||||
|
||||
await waitFor(() => screen.getByText(/happened/))
|
||||
})
|
||||
})
|
||||
+3
-2
@@ -1,6 +1,7 @@
|
||||
export interface Notification {
|
||||
id?: string
|
||||
category: 'info' | 'error' | 'success' | 'loading'
|
||||
message: string // Readable message for users to understand
|
||||
error?: any // Any other errors that needs to be logged out in the console
|
||||
id?: string
|
||||
error?: any // Optional: Any other errors that needs to be logged out in the console
|
||||
progress?: number // Optional: For loading messages to show a progress bar (Out of 100)
|
||||
}
|
||||
@@ -106,66 +106,28 @@ alter table your_table replica identity full;
|
||||
|
||||
### Migrating between projects
|
||||
|
||||
If you have an existing Supabase Project and you want to migrate it into a new (fresh) Project, you can simply follow these steps:
|
||||
Migrating projects can be achieved using standard PostgreSQL tooling. This is particularly useful for older projects (e.g. to use a newer Postgres version).
|
||||
|
||||
#### Before you begin
|
||||
|
||||
- Make sure [Realtime is disabled](/docs/guides/api#managing-realtime). This will improve the performance of inserts on your new database.
|
||||
- Make sure you have [`psql`](https://www.postgresql.org/docs/13/app-psql.html) installed on your computer so that you can run `pg_dump`.
|
||||
- Make sure [Postgres](https://www.postgresql.org/download/) is installed so you can run `psql` and `pg_dump`.
|
||||
- Create a new Supabase project.
|
||||
- If you enabled Function Hooks on your old project, enable it on your new project.
|
||||
- Store the old project's database URL as `$OLD_DB_URL` and the new project's as `$NEW_DB_URL`.
|
||||
|
||||
#### Migrate the database
|
||||
|
||||
First we want to grab the database structure only:
|
||||
1. Run `ALTER ROLE postgres SUPERUSER` in the *old* project's SQL editor
|
||||
2. Run `pg_dump --clean --if-exists --quote-all-identifiers -d $OLD_DB_URL > dump.sql` from your terminal
|
||||
3. Run `ALTER ROLE postgres NOSUPERUSER` in the *old* project's SQL editor
|
||||
4. Run `ALTER ROLE postgres SUPERUSER` in the *new* project's SQL editor
|
||||
5. Run `psql $NEW_DB_URL -f dump.sql` from your terminal
|
||||
6. Run `TRUNCATE storage.objects` in the *new* project's SQL editor
|
||||
7. Run `ALTER ROLE postgres NOSUPERUSER` in the *new* project's SQL editor
|
||||
|
||||
```sh
|
||||
pg_dump \
|
||||
-h db.PROJECT_REF.supabase.co \
|
||||
-U postgres \
|
||||
--clean \
|
||||
--schema-only \
|
||||
> supabase_schema.sql
|
||||
```
|
||||
|
||||
This command creates a file on your computer called `supabase_schema.sql`.
|
||||
|
||||
After creating a new Supabase project we can use it to restore the database structure:
|
||||
|
||||
```sh
|
||||
psql \
|
||||
-h db.NEW_PROJECT_REF.supabase.co \
|
||||
-U postgres \
|
||||
< supabase_schema.sql
|
||||
```
|
||||
|
||||
You may see some errors - the restore will attempt to drop any tables if they are present,
|
||||
and if they aren't present then an error will be displayed. These errors are informative only and nothing to worry about.
|
||||
|
||||
|
||||
#### (Optionally) Migrate the data
|
||||
|
||||
We can migrate everything in our database, however we don't want to migrate
|
||||
the data in our `storage` schema because the files exist outside of the database.
|
||||
For now, we can just get all the data inside the `public` and `auth` schemas:
|
||||
|
||||
```sh
|
||||
pg_dump \
|
||||
-h db.PROJECT_REF.supabase.co \
|
||||
-U postgres \
|
||||
--data-only \
|
||||
-n public \
|
||||
-n auth \
|
||||
> supabase_data.sql
|
||||
```
|
||||
|
||||
This command creates a file on your computer called `supabase_data.sql`. We can use this file to restore the data in our new database:
|
||||
|
||||
```sh
|
||||
psql \
|
||||
-h db.NEW_PROJECT_REF.supabase.co \
|
||||
-U postgres \
|
||||
< supabase_data.sql
|
||||
```
|
||||
#### Caveats
|
||||
|
||||
- The new project will have the old project's Storage buckets, but not the objects. You will need to migrate Storage objects manually.
|
||||
|
||||
### Resetting your project password
|
||||
|
||||
|
||||
@@ -167,7 +167,7 @@ console.log(JSON.stringify(data, null, 2))
|
||||
returns:
|
||||
|
||||
```js
|
||||
;[
|
||||
[
|
||||
{
|
||||
textarray: ['Harry', 'Larry', 'Moe'],
|
||||
},
|
||||
|
||||
@@ -56,6 +56,7 @@ Build a basic Todo List with Supabase and your favorite frontend framework:
|
||||
- Supabase in 6 minutes [Video](https://www.youtube.com/watch?v=c8DNV9yl0mg)
|
||||
- Let's build SupaAuth - Build an Authentication System using Supabase, Next.js and Typescript [6-part Blog Series](https://aalam.in/blog/supabase-auth-intro-setup-next)
|
||||
- In-depth self-hosting guide using Nginx [Blog](https://dev.to/chronsyn/self-hosting-with-supabase-1aii)
|
||||
- Build an Email and Social Auth for Next JS with Supabase, Tailwind CSS 3.0 and TypeScript [Blog](https://creativedesignsguru.com/next-js-supabase-auth/)
|
||||
|
||||
### Example apps
|
||||
|
||||
@@ -86,3 +87,4 @@ Build a basic Todo List with Supabase and your favorite frontend framework:
|
||||
- Authentication with Supabase and React. [Blog](https://hyperfoo.io/posts/supabase-authentication-react)
|
||||
- Supabase Schema Visualizer. [Blog](https://dev.to/zernonia/supabase-schema-visualizer-no-installation-login-49kg)
|
||||
- Create a real-time UI using Next.js + Supabase. [Blog](https://pablopunk.com/posts/how-to-create-a-real-time-ui-with-nextjs-and-supabase)
|
||||
- How to add Twitter auth quickly with Supabase to your Next.js site ⚡ [Blog](https://blog.avneesh.tech/how-to-add-twitter-auth-quickly-with-supabase-to-your-nextjs-site)
|
||||
@@ -31,15 +31,17 @@ This command will create a `supabase` folder which holds all the configuration f
|
||||
supabase start
|
||||
```
|
||||
|
||||
This command uses Docker Compose to start all the open source [services](/docs/#how-it-works) of Supabase. This command will take a while to run, there are a lot of services to build.
|
||||
This command uses Docker to start all the open source [services](/docs/#how-it-works) of Supabase. This command will take a while to run, there are a lot of services to build.
|
||||
|
||||
Once this is running, you will see an output that contains your local Supabase credentials:
|
||||
|
||||
```txt
|
||||
Started local development setup.
|
||||
API URL: http://localhost:54321
|
||||
DB URL: postgresql://postgres:postgres@localhost:5432/postgres
|
||||
anon key: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiJ9.ZopqoUt20nEV9cklpv9e3yw3PVyZLmKs5qLD6nGL1SI
|
||||
|
||||
API URL: http://localhost:54321
|
||||
DB URL: postgresql://postgres:postgres@localhost:54322/postgres
|
||||
Studio URL: http://localhost:54323
|
||||
anon key: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiJ9.ZopqoUt20nEV9cklpv9e3yw3PVyZLmKs5qLD6nGL1SI
|
||||
service_role key: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoic2VydmljZV9yb2xlIn0.M2d2z4SFn5C7HlJlaSLfrzuYim9nbY_XI40uWFN3hEE
|
||||
```
|
||||
|
||||
@@ -55,7 +57,7 @@ values={[
|
||||
|
||||
```sh
|
||||
# Default URL:
|
||||
postgresql://postgres:postgres@localhost:5432/postgres
|
||||
postgresql://postgres:postgres@localhost:54322/postgres
|
||||
```
|
||||
|
||||
The local Postgres instance can be accessed through [`psql`](https://www.postgresql.org/docs/current/app-psql.html)
|
||||
@@ -64,7 +66,7 @@ or any other Postgres client, such as [pgadmin](https://www.pgadmin.org/).
|
||||
For example:
|
||||
|
||||
```bash
|
||||
psql 'postgresql://postgres:postgres@localhost:5432/postgres'
|
||||
psql 'postgresql://postgres:postgres@localhost:54322/postgres'
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
@@ -81,13 +83,13 @@ You can learn more about these JWT headers in our [Resources](/docs/learn/auth-d
|
||||
|
||||
```sh
|
||||
curl 'http://localhost:54321/rest/v1/' \
|
||||
-H "apikey: SUPABASE_KEY" \
|
||||
-H "Authorization: Bearer SUPABASE_KEY"
|
||||
-H "apikey: <anon key>" \
|
||||
-H "Authorization: Bearer <anon key>"
|
||||
|
||||
http://localhost:54321/v1/rest/v1/ # REST (PostgREST)
|
||||
http://localhost:54321/v1/realtime/v1/ # Realtime
|
||||
http://localhost:54321/v1/storage/v1/ # Storage
|
||||
http://localhost:54321/v1/auth/v1/ # Auth (GoTrue)
|
||||
http://localhost:54321/rest/v1/ # REST (PostgREST)
|
||||
http://localhost:54321/realtime/v1/ # Realtime
|
||||
http://localhost:54321/storage/v1/ # Storage
|
||||
http://localhost:54321/auth/v1/ # Auth (GoTrue)
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
@@ -23,7 +23,7 @@ Help maintain Supabase repositories. This includes building client libraries, ma
|
||||
|
||||
### Expert
|
||||
|
||||
Answer user questions on Github [Discussions](https://github.com/supabase/supabase/discussions), Discord, and various other social platforms.
|
||||
Answer user questions on Github [Discussions](https://github.com/supabase/supabase/discussions), [Discord](https://discord.supabase.com), and various other social platforms.
|
||||
|
||||
### Advocate
|
||||
|
||||
@@ -39,7 +39,7 @@ Write guest blog posts, create documentation, and help Supabase global expansion
|
||||
|
||||
### Moderator
|
||||
|
||||
Help us maintain the community guidelines in our GitHub and Community-led communities such as Discord, [Reddit](https://reddit.com/r/Supabase/),
|
||||
Help us maintain the community guidelines in our GitHub and Community-led communities such as [Discord](https://discord.supabase.com), [Reddit](https://reddit.com/r/Supabase/),
|
||||
[StackOverflow](https://stackoverflow.com/questions/tagged/supabase), etc.
|
||||
|
||||
## Benefits for SupaSquad members
|
||||
|
||||
@@ -3,13 +3,19 @@ module.exports = {
|
||||
{
|
||||
type: 'category',
|
||||
label: 'About',
|
||||
items: ['reference/cli/index', 'reference/cli/supabase-init'],
|
||||
items: ['reference/cli/index'],
|
||||
collapsed: true,
|
||||
},
|
||||
{
|
||||
type: 'category',
|
||||
label: 'Local Development',
|
||||
items: ['reference/cli/supabase-start', 'reference/cli/supabase-stop', 'reference/cli/supabase-eject'],
|
||||
label: 'Command reference',
|
||||
items: ['reference/cli/supabase-help', 'reference/cli/supabase-init', 'reference/cli/supabase-start', 'reference/cli/supabase-db-branch-list', 'reference/cli/supabase-db-branch-create', 'reference/cli/supabase-db-branch-delete', 'reference/cli/supabase-db-switch', 'reference/cli/supabase-db-changes', 'reference/cli/supabase-db-commit', 'reference/cli/supabase-db-reset', 'reference/cli/supabase-db-remote-set', 'reference/cli/supabase-db-remote-commit', 'reference/cli/supabase-db-push', 'reference/cli/supabase-migration-new'],
|
||||
collapsed: true,
|
||||
},
|
||||
{
|
||||
type: 'category',
|
||||
label: 'Config reference',
|
||||
items: ['reference/cli/config-reference'],
|
||||
collapsed: true,
|
||||
}
|
||||
],
|
||||
|
||||
@@ -27,7 +27,7 @@ module.exports = {
|
||||
{
|
||||
type: 'category',
|
||||
label: 'Realtime',
|
||||
items: ['reference/javascript/subscribe', 'reference/javascript/removesubscription', 'reference/javascript/getsubscriptions'],
|
||||
items: ['reference/javascript/subscribe', 'reference/javascript/removesubscription', 'reference/javascript/removeallsubscriptions', 'reference/javascript/getsubscriptions'],
|
||||
collapsed: true,
|
||||
},
|
||||
{
|
||||
|
||||
+164
-52
@@ -2,17 +2,13 @@ openref: 0.1
|
||||
|
||||
info:
|
||||
id: reference/cli
|
||||
title: Getting started
|
||||
title: About
|
||||
description: |
|
||||
|
||||
The Supabase CLI can be found in our [CLI](https://github.com/supabase/cli) repository.
|
||||
|
||||
The CLI is still under heavy development, but it will contain all the functionality for working with Supabase projects and the Supabase platform.
|
||||
|
||||
- [x] Running Supabase locally
|
||||
- [x] Self-hosting
|
||||
- [ ] Managing database migrations (in progress)
|
||||
- [ ] Pushing your local changes to production
|
||||
- [x] Managing database migrations
|
||||
- [x] Pushing your local changes to production
|
||||
- [ ] Manage your Supabase Account
|
||||
- [ ] Manage your Supabase Projects
|
||||
- [ ] Generating types directly from your database schema
|
||||
@@ -20,78 +16,194 @@ info:
|
||||
|
||||
## Installing the CLI
|
||||
|
||||
```bash
|
||||
npm install supabase -g
|
||||
```
|
||||
Installation instructions can be found [here](https://github.com/supabase/cli#install-the-cli).
|
||||
|
||||
There may be additional steps that you need to follow to setup the CLI. Located [here](https://supabase.com/docs/guides/local-development).
|
||||
## Support
|
||||
|
||||
Report issues to our [issue tracker](https://github.com/supabase/cli/issues).
|
||||
definition: spec/combined.json
|
||||
libraries:
|
||||
- name: 'sh'
|
||||
id: 'sh'
|
||||
version: '0.0.1'
|
||||
- name: "sh"
|
||||
id: "sh"
|
||||
version: "0.0.1"
|
||||
docs:
|
||||
path: reference/cli/
|
||||
sidebar:
|
||||
- name: 'About'
|
||||
- name: "About"
|
||||
items:
|
||||
- index
|
||||
- supabase init
|
||||
- name: 'Local Development'
|
||||
- name: "Command reference"
|
||||
items:
|
||||
- supabase help
|
||||
- supabase init
|
||||
- supabase start
|
||||
- supabase stop
|
||||
- supabase eject
|
||||
- supabase db branch list
|
||||
- supabase db branch create
|
||||
- supabase db branch delete
|
||||
- supabase db switch
|
||||
- supabase db changes
|
||||
- supabase db commit
|
||||
- supabase db reset
|
||||
- supabase db remote set
|
||||
- supabase db remote commit
|
||||
- supabase db push
|
||||
- supabase migration new
|
||||
- name: "Config reference"
|
||||
items:
|
||||
- Config reference
|
||||
|
||||
pages:
|
||||
supabase init:
|
||||
supabase help:
|
||||
description: |
|
||||
```bash
|
||||
supabase init
|
||||
```
|
||||
Help provides help for any command in the application.
|
||||
Simply type supabase help [path to command] for full details.
|
||||
|
||||
Usage:
|
||||
supabase help [command]
|
||||
```
|
||||
|
||||
This command will create a .supabase folder which holds all the configurations for developing your project locally. You don't need to check this into version control.
|
||||
supabase init:
|
||||
description: |
|
||||
```
|
||||
Initialize a project to use Supabase CLI.
|
||||
|
||||
After you run `supabase init` you will be guided through a series of questions, and if everything goes well, you will see an output similar to this:
|
||||
|
||||
```txt
|
||||
✔ Port for Supabase URL: · 8000
|
||||
✔ Port for PostgreSQL database: · 5432
|
||||
✔ Project initialized.
|
||||
Supabase URL: http://localhost:8000
|
||||
Supabase Key: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJzdXBhYmFzZSIsImlhdCI6MTYwMzk2ODgzNCwiZXhwIjoyNTUwNjUzNjM0LCJhdWQiOiIiLCJzdWIiOiIiLCJSb2xlIjoicG9zdGdyZXMifQ.magCcozTMKNrl76Tj2dsM7XTl_YH0v0ilajzAvIlw3U
|
||||
Database URL: postgres://postgres:postgres@localhost:5432/postgres
|
||||
|
||||
Run supabase start to start the local emulator.
|
||||
Usage:
|
||||
supabase init
|
||||
```
|
||||
|
||||
supabase start:
|
||||
description: |
|
||||
```bash
|
||||
supabase start
|
||||
```
|
||||
Start the Supabase local development setup.
|
||||
|
||||
Usage:
|
||||
supabase start
|
||||
```
|
||||
|
||||
This command uses Docker Compose to start all the open source [services](/docs/#how-it-works) of Supabase. This command will take a while to run, there are a lot of services to build.
|
||||
|
||||
Once this is running, you will see the health services in Docker Dashboard:
|
||||
|
||||

|
||||
|
||||
supabase stop:
|
||||
supabase db branch list:
|
||||
description: |
|
||||
```bash
|
||||
supabase stop
|
||||
```
|
||||
List branches.
|
||||
|
||||
Usage:
|
||||
supabase db branch list
|
||||
```
|
||||
|
||||
When you are finished with Supabase, run `supabase stop` to stop the Docker services.
|
||||
|
||||
supabase eject:
|
||||
supabase db branch create:
|
||||
description: |
|
||||
```bash
|
||||
supabase eject
|
||||
```
|
||||
Create a branch.
|
||||
|
||||
Usage:
|
||||
supabase db branch create <branch name>
|
||||
```
|
||||
|
||||
Run in any folder to create a `docker` folder with a `docker-compose.yml`. This is useful for self-hosting or adding custom configuration.
|
||||
supabase db branch delete:
|
||||
description: |
|
||||
```
|
||||
Delete a branch.
|
||||
|
||||
See our [Self Hosting](/docs/guides/hosting/overview) docs for more details.
|
||||
Usage:
|
||||
supabase db branch delete <branch name>
|
||||
```
|
||||
|
||||
supabase db switch:
|
||||
description: |
|
||||
```
|
||||
Switch branches.
|
||||
|
||||
Usage:
|
||||
supabase db switch <branch name>
|
||||
```
|
||||
|
||||
supabase db changes:
|
||||
description: |
|
||||
```
|
||||
Diffs the local database with current migrations, then print the diff to standard output.
|
||||
|
||||
Usage:
|
||||
supabase db changes
|
||||
```
|
||||
|
||||
supabase db commit:
|
||||
description: |
|
||||
```
|
||||
Diffs the local database with current migrations, writing it as a new migration.
|
||||
|
||||
Usage:
|
||||
supabase db commit <migration name>
|
||||
```
|
||||
|
||||
supabase db reset:
|
||||
description: |
|
||||
```
|
||||
Resets the local database to reflect current migrations. Any changes on the local database that is not committed will be lost.
|
||||
|
||||
Usage:
|
||||
supabase db reset
|
||||
```
|
||||
|
||||
supabase db remote set:
|
||||
description: |
|
||||
```
|
||||
Set the remote database to push migrations to.
|
||||
|
||||
Usage:
|
||||
supabase db remote set <remote database url>
|
||||
```
|
||||
|
||||
supabase db remote commit:
|
||||
description: |
|
||||
```
|
||||
Commit changes on the remote database since the last pushed migration.
|
||||
|
||||
Usage:
|
||||
supabase db remote commit
|
||||
```
|
||||
|
||||
supabase db push:
|
||||
description: |
|
||||
```
|
||||
Push new migrations to the remote database.
|
||||
|
||||
Usage:
|
||||
supabase db push
|
||||
```
|
||||
|
||||
supabase migration new:
|
||||
description: |
|
||||
```
|
||||
Create an empty migration.
|
||||
|
||||
Usage:
|
||||
supabase migration new <migration name>
|
||||
```
|
||||
|
||||
Config reference:
|
||||
description: |
|
||||
The config file resides in `supabase/config.json` after you run `supabase init`.
|
||||
|
||||
#### `projectId` (required)
|
||||
|
||||
A string used to distinguish different Supabase projects on the same host. Defaults to the working directory name when running `supabase init`.
|
||||
|
||||
#### `ports.api` (required)
|
||||
|
||||
Host port used for the API URL. Defaults to `54321`.
|
||||
|
||||
#### `ports.db` (required)
|
||||
|
||||
Host port used for the DB URL. Defaults to `54322`.
|
||||
|
||||
#### `ports.studio` (required)
|
||||
|
||||
Host port used for Supabase Studio. Defaults to `54323`.
|
||||
|
||||
#### `ports.inbucket` (optional)
|
||||
|
||||
Host port used for the web interface of Inbucket email testing server. When not specified, emails are automatically confirmed. When specified, emails are not sent to the recipient, but rather monitored and accessible via the web interface.
|
||||
|
||||
#### `dbVersion` (required)
|
||||
|
||||
Server version number used for the database. This needs to match the server version number of the remote database you intend to link to with `supabase db remote set`. You can retrieve it by running `SHOW server_version_num`.
|
||||
+42
-1
@@ -1210,6 +1210,46 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 336,
|
||||
"name": "removeAllSubscriptions",
|
||||
"kind": 2048,
|
||||
"kindString": "Method",
|
||||
"flags": {
|
||||
"isExported": true
|
||||
},
|
||||
"signatures": [
|
||||
{
|
||||
"id": 337,
|
||||
"name": "removeAllSubscriptions",
|
||||
"kind": 4096,
|
||||
"kindString": "Call signature",
|
||||
"flags": {
|
||||
"isExported": true
|
||||
},
|
||||
"comment": {
|
||||
"shortText": "Removes all subscriptions and disconnects the Websocket connection."
|
||||
},
|
||||
"type": {
|
||||
"type": "reference",
|
||||
"typeArguments": [
|
||||
{
|
||||
"type": "intrinsic",
|
||||
"name": "unknown"
|
||||
}
|
||||
],
|
||||
"name": "Promise"
|
||||
}
|
||||
}
|
||||
],
|
||||
"sources": [
|
||||
{
|
||||
"fileName": "src/SupabaseClient.ts",
|
||||
"line": 129,
|
||||
"character": 20
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 324,
|
||||
"name": "rpc",
|
||||
@@ -25206,7 +25246,8 @@
|
||||
"kind": 32768,
|
||||
"kindString": "Parameter",
|
||||
"flags": {
|
||||
"isExported": true
|
||||
"isExported": true,
|
||||
"isOptional": true
|
||||
},
|
||||
"comment": {
|
||||
"text": "The columns to retrieve, separated by commas."
|
||||
|
||||
@@ -1207,6 +1207,46 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 336,
|
||||
"name": "removeAllSubscriptions",
|
||||
"kind": 2048,
|
||||
"kindString": "Method",
|
||||
"flags": {
|
||||
"isExported": true
|
||||
},
|
||||
"signatures": [
|
||||
{
|
||||
"id": 337,
|
||||
"name": "removeAllSubscriptions",
|
||||
"kind": 4096,
|
||||
"kindString": "Call signature",
|
||||
"flags": {
|
||||
"isExported": true
|
||||
},
|
||||
"comment": {
|
||||
"shortText": "Removes all subscriptions and disconnects the Websocket connection."
|
||||
},
|
||||
"type": {
|
||||
"type": "reference",
|
||||
"typeArguments": [
|
||||
{
|
||||
"type": "intrinsic",
|
||||
"name": "unknown"
|
||||
}
|
||||
],
|
||||
"name": "Promise"
|
||||
}
|
||||
}
|
||||
],
|
||||
"sources": [
|
||||
{
|
||||
"fileName": "src/SupabaseClient.ts",
|
||||
"line": 129,
|
||||
"character": 20
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 324,
|
||||
"name": "rpc",
|
||||
|
||||
+41
-2
@@ -150,6 +150,7 @@ info:
|
||||
items:
|
||||
- subscribe()
|
||||
- removeSubscription()
|
||||
- removeAllSubscriptions()
|
||||
- getSubscriptions()
|
||||
- name: 'Storage'
|
||||
items:
|
||||
@@ -215,6 +216,11 @@ pages:
|
||||
```bash
|
||||
npm install @supabase/supabase-js
|
||||
```
|
||||
|
||||
Via Yarn
|
||||
```bash
|
||||
yarn add @supabase/supabase-js
|
||||
```
|
||||
|
||||
Find the source code on [GitHub](https://github.com/supabase/supabase-js).
|
||||
|
||||
@@ -275,8 +281,8 @@ pages:
|
||||
})
|
||||
```
|
||||
|
||||
By default the API server points to the `public` schema. You can enable others database schemas within the Dashboard.
|
||||
Go to `Settings > API > Schema` and add the schema which you want to expose to on the API.
|
||||
By default the API server points to the `public` schema. You can enable other database schemas within the Dashboard.
|
||||
Go to `Settings > API > Schema` and add the schema which you want to expose to the API.
|
||||
|
||||
Note: each client connection can only access a single schema, so the code above can access the `other_schema` schema but cannot access the `public` schema.
|
||||
- name: Custom `fetch` implementation
|
||||
@@ -1269,6 +1275,19 @@ pages:
|
||||
supabase.removeSubscription(mySubscription)
|
||||
```
|
||||
|
||||
removeAllSubscriptions():
|
||||
title: 'removeAllSubscriptions()'
|
||||
$ref: '@supabase/supabase-js."SupabaseClient".SupabaseClient.removeAllSubscriptions'
|
||||
notes: |
|
||||
- Removing subscriptions is a great way to maintain the performance of your project's database. Supabase will automatically handle cleanup 30 seconds after a user is disconnected, but unused subscriptions may cause degradation as more users are simultaneously subscribed.
|
||||
examples:
|
||||
- name: Removes all subscriptions
|
||||
isSpotlight: true
|
||||
js: |
|
||||
```js
|
||||
supabase.removeAllSubscriptions()
|
||||
```
|
||||
|
||||
getSubscriptions():
|
||||
title: 'getSubscriptions()'
|
||||
$ref: '@supabase/supabase-js."SupabaseClient".SupabaseClient.getSubscriptions'
|
||||
@@ -1689,6 +1708,26 @@ pages:
|
||||
.lt('population', 10000)
|
||||
```
|
||||
|
||||
### Conditional Chaining
|
||||
|
||||
Filters can be built up one step at a time and then executed. For example:
|
||||
|
||||
```js
|
||||
const filterByName = null
|
||||
const filterPopLow = 1000
|
||||
const filterPopHigh = 10000
|
||||
|
||||
let query = supabase
|
||||
.from('cities')
|
||||
.select('name, country_id')
|
||||
|
||||
if (filterByName) { query = query.eq('name', filterByName) }
|
||||
if (filterPopLow) { query = query.gte('population', filterPopLow) }
|
||||
if (filterPopHigh) { query = query.lt('population', filterPopHigh) }
|
||||
|
||||
const { data, error } = await query
|
||||
```
|
||||
|
||||
.or():
|
||||
$ref: '@supabase/postgrest-js."lib/PostgrestFilterBuilder".PostgrestFilterBuilder.or'
|
||||
examples:
|
||||
|
||||
+9
-14
@@ -6,23 +6,21 @@ This is a single repository for two different websites. Our documents were creat
|
||||
|
||||
Install the [Vercel CLI](https://vercel.com/cli).
|
||||
|
||||
You will need to run the marketing website and docs website separately.
|
||||
You will need to run the marketing website and docs website separately. To learn how to run both websites, see the [Development Guide](../DEVELOPERS.md) for more information.
|
||||
|
||||
```
|
||||
# tab 1
|
||||
make dev
|
||||
```sh
|
||||
# step 1
|
||||
cd supabase
|
||||
|
||||
# tab 2
|
||||
# step 2
|
||||
cd www
|
||||
|
||||
# step 3
|
||||
npm install
|
||||
npm run dev
|
||||
|
||||
# tab 3
|
||||
cd web/spec
|
||||
make
|
||||
npm install
|
||||
npm run gen:supabase
|
||||
npm run start
|
||||
# visit website
|
||||
http://localhost:3000
|
||||
```
|
||||
|
||||
In production, this setup is deployed as two different vercel websites (`docs` and `www`)
|
||||
@@ -31,6 +29,3 @@ In production, this setup is deployed as two different vercel websites (`docs` a
|
||||
|
||||
- Referencing resources in nested folders may not work for `web` (eg `/web/static/folder/nestedfolder/asset`) may not work.
|
||||
- Possibly need to prepend all images/assets in docs site with baseUrl.
|
||||
|
||||
<!-- // we need to check this -->
|
||||
<!-- - Don't use `Link` since that adds `/new` to the links. Use plain `a` tags when possible. -->
|
||||
@@ -9,7 +9,7 @@ authorURL: https://github.com/roryw10
|
||||
image: /images/blog/supabase-monitoro.png
|
||||
tags:
|
||||
- case-study
|
||||
- nocode
|
||||
- no-code
|
||||
date: '12-02-2020'
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
---
|
||||
title: 'Supabase Beta November 2021: Launch Week Recap'
|
||||
description: We wrapped up November with Supabase's third Launch Week. Here's all the awesome stuff that got shipped ...
|
||||
author: ant_wilson
|
||||
author_url: https://github.com/awalias
|
||||
author_image_url: https://github.com/awalias.png
|
||||
authorURL: https://github.com/awalias
|
||||
image: 2021-nov/release-nov-2021.jpg
|
||||
thumb: 2021-nov/release-nov-2021-cover.jpg
|
||||
tags:
|
||||
- release-notes
|
||||
date: '2021-12-15'
|
||||
toc_depth: 3
|
||||
---
|
||||
|
||||
We wrapped up November with Supabase's third Launch Week. It also happens to be 12 months since we moved from alpha to beta.
|
||||
|
||||
Here's all the awesome stuff that got shipped during Launch Week...
|
||||
|
||||
## Supabase Studio
|
||||
|
||||
We [open sourced the Dashboard](/blog/2021/11/30/supabase-studio) - it's now included in Self-Hosting, and the community are now able to contribute,
|
||||
which is going to help us move even faster! Expect many more updates to the dashboard over the coming weeks and months! [Read the code for yourself.](https://github.com/supabase/supabase/tree/master/studio)
|
||||
|
||||

|
||||
|
||||
## We acquired Logflare
|
||||
|
||||
We were early Logflare users, and found it to be one of the most impressive products we'd ever used. So impressive in fact, that we wanted to offer it's capabilities to our entire user base.
|
||||
You'll now find fully searchable Supabase API and database logs within the dashboard, and we're also in the process of open sourcing the codebase. [Read more about the acquisition](/blog/2021/12/02/supabase-acquires-logflare).
|
||||
|
||||

|
||||
|
||||
## Realtime Row Level Security
|
||||
|
||||
[Security rules now work for Realtime](/blog/2021/12/01/realtime-row-level-security-in-postgresql).
|
||||
All of your row level security policies now work with the realtime API, so you can limit streams and subscriptions on a per user basis (or any configuration you want!).
|
||||
|
||||
|
||||

|
||||
|
||||
## GraphQL is coming to Supabase
|
||||
|
||||
We heard you! And built a Postgres extension for [querying your database with GraphQL](/blog/2021/12/03/pg-graphql).
|
||||
This means that each GraphQL request is resolved by a single SQL statement and hits the theoretical minimum achievable network IO overhead of any GraphQL to SQL resolver.
|
||||
|
||||

|
||||
|
||||
## Supabase now runs on PostgreSQL 14
|
||||
|
||||
One of the most exciting things to happen in databases this year was the release of PG 14.
|
||||
Which is now the default database used on our hosted platform. Read more here about [why we're so excited about it](/blog/2021/11/28/whats-new-in-postgres-14).
|
||||
|
||||
|
||||

|
||||
|
||||
## PostgREST 9.0
|
||||
|
||||
PostgREST is the tool we use to introspect your schema and make your database available over a RESTful API.
|
||||
We also sponsor and help maintain this amazing open source project. Version 9.0 was just released and has also been rolled out to all new and existing Supabase projects on our hosted platform.
|
||||
[Check out the blog post](/blog/2021/11/28/postgrest-9) by Steve Chavez, the maintainer of PostgREST.
|
||||
|
||||

|
||||
|
||||
## Building a SaaS?
|
||||
|
||||
We have [a course for that](https://egghead.io/courses/build-a-saas-product-with-next-js-supabase-and-stripe-61f2bc20).
|
||||
You'll learn how to combine Next.js, Stripe, and Supabase to launch your business in no time at all.
|
||||
It's honestly mind blowing how quickly you can get products out the door with this stack. Oh and it's free.
|
||||
|
||||

|
||||
|
||||
## New Storage CDN
|
||||
|
||||
Serving your files in no time - Get eye wateringly fast load times for your media files. [Read all about the benefits](/blog/2021/12/03/launch-week-three-friday-five-more-things#supabase-cdn).
|
||||
|
||||

|
||||
|
||||
## How we Launch
|
||||
|
||||
We "open sourced" our launch methodology - Learn about how we plan, execute, and retrospect launch weeks internally. [Read the post](/blog/2021/11/26/supabase-how-we-launch).
|
||||
supabase monthly growth
|
||||
|
||||

|
||||
|
||||
## Community
|
||||
|
||||
### Highlights
|
||||
|
||||
- Community Day [blog](/blog/2021/11/29/community-day#new-tutorials-and-integration-guides)
|
||||
- The SupaSquad has been busy [blog](/blog/2021/11/29/community-day#supasquad-updates)
|
||||
- 10 startups on what they're building with Supabase [video](https://www.youtube.com/watch?v=QAm1x7KaLq4&list=PL5S4mPUpp4OuzQN-a_FY3OZQuYo4NmXvb&t=3s)
|
||||
- New official integration guides [blog](https://supabase.com/blog/2021/11/29/community-day#new-tutorials-and-integration-guides)
|
||||
- Add Supabase in Teta [video](https://www.youtube.com/watch?v=lt-Vebxk-Mk)
|
||||
- Auth flow with Remix and Supabase [video](https://www.youtube.com/watch?v=-KiH8feOHSc)
|
||||
- Discovering Supabase [video](https://www.youtube.com/watch?v=WToGeMIdoSY)
|
||||
- Next.js with Magic Links [blog](https://dev.to/dailydevtips1/authenticating-nextjs-with-supabase-auth-magic-links-363e)
|
||||
- Next.js + Auth0 + Supabase [blog](https://auth0.com/blog/using-nextjs-and-auth0-with-supabase/)
|
||||
|
||||
### TikTok
|
||||
|
||||
Supabase is [now on TikTok](https://www.tiktok.com/@supabase.com?) - Watch us ship to some questionable beats.
|
||||
|
||||

|
||||
|
||||
|
||||
### GitHub
|
||||
|
||||
We hit 25K stars!! [github.com/supabase/supabase](http://github.com/supabase/supabase)
|
||||
|
||||

|
||||
|
||||
## Get started
|
||||
|
||||
- Start using Supabase today: **[app.supabase.io](https://app.supabase.io/)**
|
||||
- Make sure to **[star us on GitHub](https://github.com/supabase/supabase)**
|
||||
- Follow us **[on Twitter](https://twitter.com/supabase)**
|
||||
- Subscribe to our **[YouTube channel](https://www.youtube.com/channel/UCNTVzV1InxHV-YR0fSajqPQ)**
|
||||
- Become a **[sponsor](https://github.com/sponsors/supabase)**
|
||||
Loaded 100 of 145 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user