Merge branch 'supabase:master' into master

This commit is contained in:
Nikhil Nayak authored and GitHub committed 2021-12-26 18:15:25 +05:30
commit 39749fabca
145 files changed
+16962 -2512

No files matched your search

+2
View File
@@ -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).
+2 -2
View File
@@ -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.
+2 -2
View File
@@ -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
+4
View File
@@ -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
View File
@@ -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
+1 -1
View File
@@ -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;
+3 -3
View File
@@ -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
+1 -1
View File
@@ -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"
},
+142 -92
View File
@@ -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
+34 -5
View File
@@ -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 }
@@ -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])
@@ -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)
}
@@ -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
@@ -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: {},
}
@@ -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>
@@ -0,0 +1,8 @@
import { Dictionary } from '@supabase/grid'
export interface SpreadsheetData {
headers: string[]
rows: any[]
rowCount: number
columnTypeMap: Dictionary<string>
}
@@ -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'
}
@@ -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
@@ -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>
}
@@ -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"'
+1 -1
View File
@@ -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(' ')}>
+7 -10
View File
@@ -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}
-75
View File
@@ -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
-79
View File
@@ -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
+3 -3
View File
@@ -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>
+25 -4
View File
@@ -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 })
}
}
}
})
+42 -15
View File
@@ -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
}
+53
View File
@@ -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
}
+4 -8
View File
@@ -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,
})
+4 -8
View File
@@ -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)
+4 -8
View File
@@ -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,
})
+4 -8
View File
@@ -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)
-811
View File
@@ -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',
}
+12
View File
@@ -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',
}
+36
View File
@@ -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 }
}
}
+2 -2
View File
@@ -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}`)
}
+14174 -153
View File
File diff suppressed because it is too large. Load diff
+4 -3
View File
@@ -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",
+2 -2
View File
@@ -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
View File
@@ -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}>
+23 -13
View File
@@ -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">
+6 -3
View File
@@ -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)
+1 -1
View File
@@ -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 />
+1 -1
View File
@@ -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))
+1 -1
View File
@@ -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 (
+4 -6
View File
@@ -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
}
}
+2 -1
View File
@@ -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 -2
View File
@@ -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',
+21 -26
View File
@@ -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)
}
}
}
+1 -1
View File
@@ -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
View File
@@ -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;
+1 -1
View File
@@ -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
View File
@@ -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;
}
}
+27
View File
@@ -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
}
}
+5 -5
View File
@@ -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 -1
View File
@@ -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={[{
+90 -76
View File
@@ -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
View File
@@ -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)
}
+14 -52
View File
@@ -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
+1 -1
View File
@@ -167,7 +167,7 @@ console.log(JSON.stringify(data, null, 2))
returns:
```js
;[
[
{
textarray: ['Harry', 'Larry', 'Moe'],
},
+2
View File
@@ -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)
+14 -12
View File
@@ -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>
+2 -2
View File
@@ -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
+9 -3
View File
@@ -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,
}
],
+1 -1
View File
@@ -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
View File
@@ -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:
![Docker Supabase](/img/docker-supabase.png)
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
View File
@@ -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."
+40
View File
@@ -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
View File
@@ -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
View File
@@ -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. -->
+1 -1
View File
@@ -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)
![Supabase Studio](/images/blog/2021-nov/studio.jpg)
## 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).
![We acquired Logflare](/images/blog/2021-nov/logflare.png)
## 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!).
![Realtime Row Level Security](/images/blog/2021-nov/realtime.png)
## 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.
![GraphQL is coming to Supabase](/images/blog/2021-nov/graphql.png)
## 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).
![Supabase now runs on PostgreSQL 14](/images/blog/2021-nov/pg14.png)
## 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.
![PostgREST 9.0 ](/images/blog/2021-nov/postgrest9.png)
## 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.
![Building a SaaS? ](/images/blog/2021-nov/course.jpg)
## 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).
![New Storage CDN](/images/blog/2021-nov/too_damn_high.png)
## 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
![How we Launch](/images/blog/2021-nov/supabase-universe.jpg)
## 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](/images/blog/2021-nov/tiktok.png)
### GitHub
We hit 25K stars!! [github.com/supabase/supabase](http://github.com/supabase/supabase)
![GitHub](/images/blog/2021-nov/stars.png)
## 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